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'}); if (width === 1440) assert.equal(await page.locator('.theme-doc-sidebar-menu .menu__link').first().innerText(), 'Target'); const iframe = page.locator(`.archify-panel--story > .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.leftbox.right||b.topbox.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(!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, []); 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;});