Assessor_2.0/tests/results-header-browser-chec...

42 lines
5.2 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Isolated synthetic Results headers; no backend or running app mutation.
import assert from 'node:assert/strict';
import {readFile} from 'node:fs/promises';
import {createServer} from 'node:http';
import {pathToFileURL} from 'node:url';
import {build} from 'esbuild';
const {chromium}=await import(pathToFileURL(process.env.PLAYWRIGHT_MODULE).href);
const main=await readFile('src/main.tsx','utf8');
const styles=[...main.matchAll(/^import '([^']+\.css)';/gm)].map(([,path])=>`import ${JSON.stringify(path.startsWith('.')?'./src/'+path.slice(2):path)};`).join('\n');
const bundle=await build({stdin:{resolveDir:process.cwd(),loader:'tsx',contents:`${styles}
import React,{useState} from 'react';import {createRoot} from 'react-dom/client';
import {FloatingResultsWindow} from './src/features/linked-results/FloatingResultsWindow';
function Example(){const [minimized,setMinimized]=useState(true);return <>{['SMBv1 is enabled on Domain Controllers','Unexpected principal with metadata edit permission on a GPO linked to a critical AD container '.repeat(8)].map((title,index)=><FloatingResultsWindow key={index} id={'w'+index} title={'Results ('+(index+1)+') – '+title} sheets={[]} color="" zIndex={1} offset={0} clusterPosition={{left:30,top:50+index*40}} clusterWidth={Number(new URLSearchParams(location.search).get('width'))} clusterIndex={index} minimized={minimized} onToggleMinimize={()=>setMinimized(value=>!value)} search="" onActivate={()=>{}} onClose={()=>{}} onToggleBookmark={()=>{}} onOpenRecord={()=>{}}/>)}</>};createRoot(document.getElementById('root')).render(<Example/>);`},jsx:'automatic',bundle:true,write:false,outdir:'synthetic',format:'iife',loader:{'.woff2':'dataurl','.woff':'dataurl','.png':'dataurl','.svg':'dataurl'}});
const server=createServer((req,res)=>{const file=bundle.outputFiles.find(file=>req.url==='/'+file.path.split(/[\\/]/).at(-1));res.setHeader('Content-Type',file?(req.url.endsWith('.css')?'text/css':'text/javascript'):'text/html');res.end(file?.contents??'<!doctype html><html><head><meta charset="utf-8"><link rel="stylesheet" href="/stdin.css"></head><body><div id="root"></div><script src="/stdin.js"></script></body></html>');});
await new Promise(resolve=>server.listen(0,'127.0.0.1',resolve));let browser;
try{
browser=await chromium.launch({headless:true,channel:'chrome'});const page=await browser.newPage();
for(const width of [760,440])for(const theme of ['dark','light']){
await page.setViewportSize({width:1000,height:900});await page.goto('http://127.0.0.1:'+server.address().port+'/?width='+width);await page.waitForSelector('.results-window');
await page.evaluate(theme=>{document.documentElement.dataset.theme=theme;
// A synthetic marker exercises the existing ring's CSS geometry; production group logic is covered separately.
const slot=document.querySelector('.results-window-related-slot');slot.innerHTML='<button class="related-finding-ring is-inline" aria-label="Synthetic group"><span></span></button>';
},theme);
const boxes=await page.locator('.results-window').evaluateAll(nodes=>nodes.map(node=>{
const head=node.querySelector('.results-window-head'),title=node.querySelector('.results-window-name'),actions=node.querySelector('.results-window-actions');
const rect=element=>{const r=element.getBoundingClientRect();return{left:r.left,right:r.right,top:r.top,bottom:r.bottom}};
return{head:rect(head),title:rect(title),actions:rect(actions),controls:[...actions.querySelectorAll('button:not(:disabled)')].map(button=>({name:button.getAttribute('aria-label')??button.textContent,rect:rect(button),hit:document.elementFromPoint(button.getBoundingClientRect().x+button.clientWidth/2,button.getBoundingClientRect().y+button.clientHeight/2)?.closest('button')===button})),clipped:title.scrollWidth>title.clientWidth,ellipsis:getComputedStyle(title).textOverflow,tooltip:title.title};
}));
assert.ok(Math.abs(boxes[0].actions.left-boxes[1].actions.left)<1);
assert.equal(await page.locator('.results-window-actions').evaluateAll(nodes=>nodes.every(node=>node.lastElementChild.classList.contains('results-window-close'))),true);
assert.equal(await page.locator('.results-window').evaluateAll(nodes=>nodes.every(node=>{
const close=node.querySelector('.results-window-close').getBoundingClientRect();
return [...node.querySelectorAll('.results-window-actions button')].every(button=>button.getBoundingClientRect().right<=close.right);
})),true);
for(const box of boxes){assert.ok(box.title.right<=box.actions.left);for(const control of box.controls){assert.ok(control.rect.right<=box.head.right);assert.equal(control.hit,true,JSON.stringify({width,theme,control,boxes}));}}
assert.equal(boxes[1].clipped,true);assert.equal(boxes[1].ellipsis,'ellipsis');assert.ok(boxes[1].tooltip.length>600);
await page.locator('.results-window-toggle').nth(1).click();assert.equal(await page.locator('.results-window.is-minimized').count(),0);
await page.locator('.results-window-toggle').nth(1).click();assert.equal(await page.locator('.results-window.is-minimized').count(),2);
}
console.log('Both themes at 760/440px Results widths: matching action positions, title ellipsis, full tooltip, all controls hit-testable, minimize/restore preserved.');
}finally{await browser?.close();await new Promise(resolve=>server.close(resolve));}