Assessor_2.0/tests/finding-reader-actions-brow...

82 lines
5.4 KiB
JavaScript

// Synthetic-only geometry acceptance. PLAYWRIGHT_MODULE points to installed Playwright.
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: { contents: `${styles}
import React from 'react';
import { createRoot } from 'react-dom/client';
import { FindingReader } from './src/features/finding-reader/FindingReader';
import { makeSheet } from './src/importers/sheet-factory';
const record = makeSheet('Synthetic', [['Name','Result','Description','ShortName','MITRE ATT&CK','MITRE D3FEND'],
['Synthetic LDAP signing finding','IOE Found','Synthetic evidence','SI000160','Privilege Escalation','Harden']], '', 'synthetic.csv', {toolId:'purple-knight'}).analysis.records[0];
const root = createRoot(document.getElementById('root'));
let include = false; let resultsOpen = false;
function render() { root.render(<FindingReader record={record} title={record.title} review={{include}}
linkedSheets={[]} resultReferences={[]} search="" position={0} recordCount={1} toolId="purple-knight"
onPrevious={()=>{}} onNext={()=>{}} onReview={patch=>{include=patch.include;render();}}
onResults={()=>{resultsOpen=true;render();}} resultsOpen={resultsOpen}/>); }
render();`, resolveDir: process.cwd(), loader: 'tsx' },
jsx: 'automatic', bundle: true, write: false, outdir: 'synthetic', format: 'iife',
loader: { '.woff2': 'dataurl', '.woff': 'dataurl', '.png': 'dataurl', '.svg': 'dataurl' }
});
const server = createServer((request, response) => {
const file = bundle.outputFiles.find(file => request.url === '/' + file.path.split(/[\\/]/).at(-1));
response.setHeader('Content-Type', file ? (request.url.endsWith('.css') ? 'text/css' : 'text/javascript') : 'text/html');
response.end(file?.contents ?? '<!doctype html><html><head><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();
await page.goto(`http://127.0.0.1:${server.address().port}/`);
await page.locator('.reader-primary-actions').waitFor();
for (const theme of ['dark', 'light']) {
await page.evaluate(theme => { document.documentElement.dataset.theme = theme; }, theme);
const disclosure = page.getByRole('button', { name: 'MITRE ATT&CK and D3FEND information', exact: true });
assert.equal(await disclosure.getAttribute('aria-expanded'), 'false');
assert.equal(await page.locator('.mitre-badge').count(), 0);
await disclosure.focus();
await page.keyboard.press('Enter');
await page.locator('.mitre-badge').first().waitFor();
assert.equal(await page.locator('.mitre-badge').count(), 2);
const surface = await page.locator('.mitre-disclosure').evaluate(element => {
const css = getComputedStyle(element); const button = getComputedStyle(element.querySelector('button'));
return { background: css.backgroundColor, border: css.borderTopWidth, shadow: css.boxShadow,
buttonBackground: button.backgroundColor, buttonBorder: button.borderTopWidth };
});
assert.deepEqual(surface, { background: 'rgba(0, 0, 0, 0)', border: '0px', shadow: 'none',
buttonBackground: 'rgba(0, 0, 0, 0)', buttonBorder: '0px' });
await page.keyboard.press('Space');
await page.waitForFunction(() => !document.querySelector('.mitre-badge'));
for (const width of [1200, 620, 320]) {
await page.setViewportSize({ width, height: 900 });
const geometry = await page.locator('.reader-primary-actions').evaluate(group => [...group.children].map(action => {
const rect = action.getBoundingClientRect();
return { x: rect.x, y: rect.y, right: rect.right, height: rect.height, text: action.textContent };
}));
assert.deepEqual(geometry.map(action => action.text), ['AI search', '', 'Results']);
assert.ok(geometry.every(action => Math.abs(action.y - geometry[0].y) < 1));
assert.ok(geometry[0].right < geometry[1].x && geometry[1].right < geometry[2].x);
assert.ok(geometry[0].x >= 0 && geometry[2].right <= width);
}
const bookmark = page.getByRole('button', { name: 'Bookmark', exact: true });
const previous = await bookmark.getAttribute('aria-pressed');
await bookmark.click();
await page.waitForFunction(previous => document.querySelector('.bookmark-btn').getAttribute('aria-pressed') !== previous, previous);
await page.getByRole('button', { name: 'Results', exact: true }).click();
assert.ok(await page.locator('.results-detail-btn').evaluate(button => button.classList.contains('is-open')));
}
console.log('Finding actions: both themes, 1200/620/320px, icon-only bookmark, Results and frameless MITRE disclosure with Enter/Space passed.');
} finally {
await browser?.close(); await new Promise(resolve => server.close(resolve));
}