Files
documentation/scripts/browser/sdlc-cycle.cjs

104 lines
7.8 KiB
JavaScript

const {chromium} = require('playwright');
const assert = require('node:assert/strict');
const fs = require('node:fs');
(async () => {
const base = process.argv[2] || 'http://127.0.0.1:4173';
const output = `.cache/cycle-${process.argv[3] || 'local'}`;
fs.mkdirSync(output, {recursive:true});
const browser = await chromium.launch({headless:true, executablePath:process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE, args:['--no-sandbox']});
const results = [];
try {
for (const width of [1440, 390, 320]) {
const context = await browser.newContext({viewport:{width,height:1000}, colorScheme:'light'});
const page = await context.newPage();
await page.goto(`${base}/eldorado-v1/documentation/`, {waitUntil:'networkidle'});
if (width < 700) await page.getByRole('button', {name:'Toggle navigation bar'}).click();
const nav = page.getByRole('link', {name:'Strategy', exact:true}).filter({visible:true});
assert((await nav.getAttribute('href')).endsWith('/strategy/target-operating-model/'));
await nav.click();
await page.waitForURL('**/strategy/target-operating-model/');
for (const route of ['target-operating-model/', '']) {
await page.goto(`${base}/eldorado-v1/documentation/strategy/${route}`, {waitUntil:'networkidle'});
await page.waitForURL('**/strategy/target-operating-model/');
if (width < 700) await page.getByRole('button', {name:'Toggle navigation bar'}).click();
const menu = page.locator('.theme-doc-sidebar-menu .menu__link').filter({visible:true});
await menu.first().waitFor({state:'visible'});
assert.deepEqual(await menu.allTextContents(), ['Target', 'Roadmap', '6 Months Target', '6 Months Roadmap', 'Beyond 6 Months']);
assert((await menu.nth(0).getAttribute('href')).endsWith('/strategy/target-operating-model/'));
assert((await menu.nth(1).getAttribute('href')).endsWith('/strategy/roadmap/'));
assert.equal(await menu.filter({hasText:/^Target$/}).count(), 1);
await page.screenshot({path:`${output}/${width}-${route ? 'target' : 'landing'}-navigation.png`,fullPage:true,animations:'disabled'});
await menu.nth(1).click();
await page.waitForURL('**/strategy/roadmap/');
await page.getByRole('heading', {level:1,name:'Roadmap',exact:true}).waitFor();
await page.goBack({waitUntil:'domcontentloaded'});
await page.waitForURL('**/strategy/target-operating-model/');
const iframe = page.locator(`[data-diagram="strategy-target"] > .story-${width > 700 ? 'desktop' : 'mobile'}`);
await iframe.scrollIntoViewIfNeeded();
const frame = await (await iframe.elementHandle()).contentFrame();
await frame.locator('svg[role="img"]').waitFor();
const metrics = await frame.evaluate(() => {
const svg = document.querySelector('svg[role="img"]');
const labels = [...svg.querySelectorAll('text[data-node-label]')].map(n => ({text:n.textContent,px:parseFloat(getComputedStyle(n).fontSize)*n.getScreenCTM().a}));
const edges = [...svg.querySelectorAll('path[data-edge-id^="stage-"]')].map(n => [n.dataset.edgeFrom,n.dataset.edgeTo,n.getAttribute('marker-end')]);
const overflow = [...svg.querySelectorAll('[data-node-id]')].flatMap(n => {
const box = n.querySelector('rect').getBoundingClientRect();
return [...n.querySelectorAll('text')].filter(t => {const b=t.getBoundingClientRect();return b.left<box.left||b.right>box.right||b.top<box.top||b.bottom>box.bottom;}).map(t=>t.textContent);
});
const relationships = [...svg.querySelectorAll('path[data-edge-id]')].map(n => ({id:n.dataset.edgeId,from:n.dataset.edgeFrom,to:n.dataset.edgeTo,dash:getComputedStyle(n).strokeDasharray,arrow:n.getAttribute('marker-end')}));
const nodes = [...svg.querySelectorAll('[data-node-id]')].map(n => {const r=n.querySelector('rect').getBoundingClientRect();return {id:n.dataset.nodeId,x:r.x+r.width/2,y:r.y+r.height/2};});
return {labels,edges,overflow,relationships,nodes};
});
const ids = ['scope','architecture','ui-ux','planning','delivery','uat','security','releases'];
assert.deepEqual(metrics.edges.map(e=>e.slice(0,2)), ids.map((id,i)=>[id,ids[(i+1)%ids.length]]));
assert(metrics.edges.every(e=>e[2]?.startsWith('url(')), 'all stages must have arrowheads');
assert.equal(metrics.nodes.length, 17);
assert.equal(metrics.labels.filter(n => n.text === 'AI Agent').length, 8);
assert(!metrics.labels.some(n => /code[ -]agent/i.test(n.text)));
assert((await page.locator('.strategy-content').innerText()).includes('human-controlled AI Agent'));
assert(!metrics.nodes.some(n=>n.id==='humans'));
assert.equal(await frame.locator('[data-node-id="agent"] text').allTextContents().then(texts=>texts.includes('Human Controlled')), true);
assert.equal(metrics.relationships.length, 24);
const governs = metrics.relationships.filter(e=>e.id.startsWith('govern-'));
const contributions = metrics.relationships.filter(e=>e.id.startsWith('contribute-'));
assert.deepEqual(governs.map(e=>[e.from,e.to]), ids.map(id=>['agent',id]));
assert.deepEqual(contributions.map(e=>[e.from,e.to]), ids.map(id=>[`code-${id}`,id]));
assert(governs.every(e=>e.dash==='3px, 9px' && e.arrow?.startsWith('url(')));
assert(contributions.every(e=>e.dash==='12px, 8px' && e.arrow?.startsWith('url(')));
assert(!metrics.relationships.some(e=>e.id==='human-direction' || e.from==='humans' || e.to==='humans'));
if (width > 700) {
const agent = metrics.nodes.find(n=>n.id==='agent');
const phases = metrics.nodes.filter(n=>ids.includes(n.id));
for (const axis of ['x','y']) assert(Math.abs(agent[axis]-(Math.min(...phases.map(n=>n[axis]))+Math.max(...phases.map(n=>n[axis])))/2)<1);
for (const id of ids) {
const phase=phases.find(n=>n.id===id), code=metrics.nodes.find(n=>n.id===`code-${id}`);
assert(Math.hypot(code.x-agent.x,code.y-agent.y)>Math.hypot(phase.x-agent.x,phase.y-agent.y));
}
}
assert(metrics.labels.every(n=>n.px>=14), JSON.stringify(metrics.labels));
assert.deepEqual(metrics.overflow, []);
await frame.locator('#btn-export').focus(); await page.keyboard.press('Enter');
await frame.locator('[data-format="svg"]').focus();
const [download] = await Promise.all([page.waitForEvent('download'), page.keyboard.press('Enter')]);
assert.equal(await download.failure(), null);
const exportPath = `${output}/${width}-${route ? 'target' : 'landing'}.svg`;
await download.saveAs(exportPath);
const exported = fs.readFileSync(exportPath, 'utf8');
assert.equal((exported.match(/>AI Agent<\/text>/g) || []).length, 8);
assert(!/code[ -]agent/i.test(exported));
assert(exported.includes('Autonomous agent') && exported.includes('Human Controlled'));
assert(!await page.evaluate(()=>document.documentElement.scrollWidth>innerWidth));
await page.evaluate(()=>scrollTo({top:0,behavior:'instant'}));
await page.waitForFunction(()=>scrollY===0);
await page.evaluate(()=>new Promise(resolve=>requestAnimationFrame(()=>requestAnimationFrame(resolve))));
await page.screenshot({path:`${output}/${width}-${route ? 'target' : 'landing'}.png`,fullPage:true});
results.push({width,route,...metrics});
fs.writeFileSync(`${output}/results.json`,JSON.stringify(results,null,2));
}
await context.close();
}
assert.equal(results.length,6);
console.log('PASS 6 cycle routes/viewports: navigation, sidebar order, exact directed loop, arrowheads, >=14px labels, text bounds and no overflow');
} finally {await browser.close();}
})().catch(error=>{console.error(error);process.exitCode=1;});