67 lines
5.4 KiB
JavaScript
67 lines
5.4 KiB
JavaScript
// Synthetic browser acceptance. Run with PLAYWRIGHT_MODULE pointing to Playwright's index.mjs.
|
|
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);
|
|
assert.ok(!(await readFile('src/app/App.tsx', 'utf8')).includes('context-layer-btn'));
|
|
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 {ObsidianGraphView} from './src/features/mitre-graph/ObsidianGraphView';
|
|
import {EnginesControl} from './src/components/EnginesControl';
|
|
import {makeSheet} from './src/importers/sheet-factory';
|
|
import {genericProfile} from './src/profiles/generic/generic-profile';
|
|
const sheet=makeSheet('Synthetic',[['Title','Severity','Category','Status','MITRE ATT&CK','Source Rule ID'],['Synthetic finding','High','Synthetic category','Failed','T1001','S-Synthetic']], '', 'synthetic.csv');
|
|
const noop=()=>{};
|
|
window.fixtureApp={dataset:{sheets:[sheet],profileId:'generic'},activeSheet:sheet,activeSheetId:sheet.id,activeProfile:genericProfile,titleColumn:'Title',reviews:{},actionOnly:false,facetFilters:{},pathFindingIds:[],search:'',statusFilter:'all',setFacetFilter:noop,setSelectedRecord:noop,setView:noop,toggleWorkspace:noop,updateReview:noop,resetFilters:noop,setActionOnly:noop,setStatusFilter:noop,setSearch:noop};
|
|
createRoot(document.getElementById('root')).render(<div className="app-shell app-shell-graph"><header className="topbar"><div className="app-header-panel"><div className="area-topbar"><span className="area-topbar-heading"><strong>Pathfinder</strong><EnginesControl/></span></div><div id="pathfinder-controls"/></div></header><div style={{padding:20,minHeight:0}}><ObsidianGraphView active onOpenFinding={noop}/></div></div>);
|
|
`,resolveDir:process.cwd(),loader:'tsx'},
|
|
plugins:[{name:'synthetic',setup(build){
|
|
build.onResolve({filter:/(?:AppProvider|use-filtered-records|use-context-cluster-index|work-facets|ViewBookmarks)$/},args=>({path:args.path,namespace:'fixture'}));
|
|
build.onLoad({filter:/.*/,namespace:'fixture'},args=>({contents:
|
|
args.path.endsWith('AppProvider')?'export const useApp=()=>window.fixtureApp; export const useOptionalApp=useApp;':
|
|
args.path.endsWith('use-filtered-records')?'export const useFilteredRecords=()=>window.fixtureApp.activeSheet.analysis.records;':
|
|
args.path.endsWith('work-facets')?`export const selectWorkFacets=()=>['Security Domain','CIS Category','Severity','Category','MITRE ATT&CK','MITRE D3FEND','Target','Context Cluster','Source: Scan'].map((label,i)=>({id:'synthetic-'+i,label,kind:'category',values:[{value:'example',label:'Example',count:1}]}));`:
|
|
args.path.endsWith('use-context-cluster-index')?'export const useContextClusterIndex=()=>undefined;':
|
|
'export const ViewBookmarks=()=> <button className="btn">Path Snapshot</button>;',loader:'tsx',resolveDir:process.cwd()}));
|
|
}}],define:{'import.meta.env.DEV':'false'},jsx:'automatic',bundle:true,write:false,outdir:'synthetic',format:'iife',loader:{'.woff2':'dataurl','.woff':'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:'msedge'});
|
|
const page=await browser.newPage({viewport:{width:1440,height:900}});
|
|
const errors=[]; page.on('pageerror',e=>errors.push(e.message));
|
|
await page.goto(`http://127.0.0.1:${server.address().port}/`);
|
|
for(const theme of ['dark','light']) {
|
|
await page.setViewportSize({width:theme==='dark'?1440:1000,height:900});
|
|
await page.evaluate(t=>document.documentElement.dataset.theme=t,theme);
|
|
await page.locator('.obsidian-node-finding').press('Enter');
|
|
assert.ok((await page.locator('.obsidian-graph-inspector').textContent()).includes('Synthetic finding'));
|
|
const details=await page.locator('.obsidian-graph-inspector').boundingBox();
|
|
const graph=await page.locator('.obsidian-graph-stage').boundingBox();
|
|
const filters=await page.locator('.pathfinder-controls').boundingBox();
|
|
assert.ok(details.x+details.width<=graph.x+1);
|
|
assert.ok(filters.y+filters.height<=graph.y);
|
|
assert.ok(graph.height>400);
|
|
await page.getByRole('button',{name:'Engines',exact:true}).click();
|
|
assert.equal(await page.getByRole('checkbox',{name:'Core Engine Always active'}).isDisabled(),true);
|
|
await page.keyboard.press('Escape');
|
|
await page.screenshot({path:`.pathfinder-${theme}.png`});
|
|
await page.locator('.obsidian-node-finding').press('Enter');
|
|
}
|
|
assert.deepEqual(errors,[]);
|
|
console.log('Pathfinder layout and Engines: both themes passed');
|
|
} finally {await browser?.close(); await new Promise(resolve=>server.close(resolve));}
|