docs: [verified] connect skills-first Roadmap to autonomous Target
Build and publish Eldorado documentation / build (pull_request) Successful in 49s
Build and publish Eldorado documentation / build (pull_request) Successful in 49s
This commit is contained in:
1 parent
733d6f2ece
commit
e10b03dff4
17 files changed
+621
-331
No files matched your search
@@ -1,129 +1,136 @@
|
||||
const {chromium} = require('playwright');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const ids = ['assessment','readiness','pilot','expansion','sustain'];
|
||||
const views = {
|
||||
'strategy-roadmap': ['skills','autonomy'],
|
||||
'strategy-roadmap-adoption': ['delivery','remaining'],
|
||||
'strategy-roadmap-loop': ['understand','baseline','topology','use','collect','reassess'],
|
||||
};
|
||||
(async () => {
|
||||
const base = process.argv[2] || 'http://127.0.0.1:4173';
|
||||
const output = `.cache/roadmap-timeline-${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 = [];
|
||||
const inspect = async (frame, vertical) => {
|
||||
const inspect = async (frame, name, vertical) => {
|
||||
await frame.locator('svg[role="img"]').waitFor();
|
||||
const geometry = await frame.locator('svg[role="img"]').evaluate(svg => ({
|
||||
width:innerWidth, height:innerHeight,
|
||||
edges:[...svg.querySelectorAll('path[data-edge-id]')].map(p => ({id:p.dataset.edgeId,box:p.getBoundingClientRect().toJSON()})),
|
||||
cards:[...svg.querySelectorAll('[data-node-id]')].map(node => ({
|
||||
id:node.dataset.nodeId, box:node.querySelector('rect').getBoundingClientRect().toJSON(),
|
||||
number:node.querySelector('[data-stage-number]').textContent,
|
||||
numberColor:getComputedStyle(node.querySelector('[data-stage-number]')).fill,
|
||||
titleColor:getComputedStyle(node.querySelector('[data-node-label]')).fill,
|
||||
text:[...node.querySelectorAll('text')].map(text => ({value:text.textContent, box:text.getBoundingClientRect().toJSON(), size:parseFloat(getComputedStyle(text).fontSize)*text.getScreenCTM().a}))
|
||||
}))
|
||||
}));
|
||||
geometry.cards.sort((a,b) => a.box.y-b.box.y || a.box.x-b.box.x);
|
||||
const ids=views[name];
|
||||
geometry.cards.sort((a,b)=>ids.indexOf(a.id)-ids.indexOf(b.id));
|
||||
assert.deepEqual(geometry.cards.map(n => n.id), ids);
|
||||
assert.deepEqual(geometry.cards.map(n => n.number), ['1','2','3','4','5']);
|
||||
assert.deepEqual(geometry.cards.map(n => n.number), ids.length === 6 ? ids.map((_,i)=>`Step ${i+1}`) : name === 'strategy-roadmap' ? ['Stage 1','Stage 2'] : ['First','Then']);
|
||||
assert.equal(geometry.edges.length,ids.length===6?6:1);
|
||||
if(ids.length===6) assert(geometry.edges.some(e=>e.id==='repeat'));
|
||||
for(const edge of geometry.edges) assert(edge.box.left>=0 && edge.box.right<=geometry.width+1 && edge.box.top>=0 && edge.box.bottom<=geometry.height+1, `clipped edge ${name} ${edge.id}`);
|
||||
for (const [index, card] of geometry.cards.entries()) {
|
||||
assert.equal(card.text.length, 3, 'number, title and one outcome only');
|
||||
assert.equal(card.numberColor,card.titleColor,'stage number must inherit readable theme color');
|
||||
assert(card.box.left >= 0 && card.box.top >= 0 && card.box.right <= geometry.width + 1 && card.box.bottom <= geometry.height + 1, `outside iframe: ${card.id}`);
|
||||
for (const text of card.text) {
|
||||
assert(text.size >= 13, `small text: ${card.id} ${text.size}`);
|
||||
assert(!/OUTPUT|Exit:/.test(text.value));
|
||||
assert(text.box.left >= card.box.left && text.box.right <= card.box.right + 1 && text.box.top >= card.box.top && text.box.bottom <= card.box.bottom + 1, `clipped: ${card.id}`);
|
||||
}
|
||||
for (let t=1; t<card.text.length; t++) assert(card.text[t-1].box.bottom < card.text[t].box.top, `overlapping text: ${card.id}`);
|
||||
if (index) {
|
||||
const previous = geometry.cards[index-1].box;
|
||||
if (vertical) {assert.equal(previous.x,card.box.x); assert(previous.bottom < card.box.top);}
|
||||
else {assert.equal(previous.y,card.box.y); assert(previous.right < card.box.left);}
|
||||
assert.equal(card.text.length,3);
|
||||
assert(card.box.left >= 0 && card.box.top >= 0 && card.box.right <= geometry.width+1 && card.box.bottom <= geometry.height+1, `outside iframe ${name} ${card.id}`);
|
||||
for(const text of card.text) {
|
||||
assert(text.size>=13, `small text ${name} ${card.id}: ${text.size}`);
|
||||
assert(text.box.left>=card.box.left && text.box.right<=card.box.right+1 && text.box.top>=card.box.top && text.box.bottom<=card.box.bottom+1, `clipped text ${name} ${card.id}`);
|
||||
}
|
||||
for(let t=1;t<card.text.length;t++) assert(card.text[t-1].box.bottom<card.text[t].box.top,`text overlap ${card.id}`);
|
||||
if(index && vertical) {assert.equal(geometry.cards[index-1].box.x,card.box.x);assert(geometry.cards[index-1].box.bottom<card.box.top);}
|
||||
}
|
||||
return geometry;
|
||||
};
|
||||
try {
|
||||
for (const width of [1440,1024,390,320]) {
|
||||
const context = await browser.newContext({viewport:{width,height:1000}, colorScheme:'light'});
|
||||
for(const width of [1440,1024,390,320]) {
|
||||
const context=await browser.newContext({viewport:{width,height:1000},colorScheme:'light'});
|
||||
try {
|
||||
const page = await context.newPage();
|
||||
const errors = [];
|
||||
page.on('pageerror', error => errors.push(error.message));
|
||||
assert.equal((await page.goto(`${base}/eldorado-v1/documentation/strategy/roadmap/`, {waitUntil:'networkidle'})).status(),200);
|
||||
const panel = page.locator('[data-diagram="strategy-roadmap"]');
|
||||
const iframe = panel.locator(width < 1360 ? '.story-mobile' : '.story-desktop');
|
||||
const frame = await (await iframe.elementHandle()).contentFrame();
|
||||
await frame.locator('svg[role="img"]').waitFor();
|
||||
const opener = panel.getByRole('button', {name:/^Expand /});
|
||||
for (const theme of ['light','dark']) {
|
||||
if (await frame.locator('html').getAttribute('data-theme') !== theme) await frame.locator('#btn-theme').click();
|
||||
await page.waitForFunction(t => document.documentElement.dataset.theme === t, theme);
|
||||
const inline = await inspect(frame,width < 1360);
|
||||
await panel.screenshot({path:`${output}/${width}-${theme}-inline.png`});
|
||||
await opener.focus(); await page.keyboard.press('Enter');
|
||||
const close = panel.getByRole('button', {name:'Close full-screen diagram', exact:true});
|
||||
await close.waitFor({state:'visible'});
|
||||
const fullscreen = await (await panel.locator('dialog iframe').elementHandle()).contentFrame();
|
||||
await fullscreen.waitForFunction(t => document.documentElement.dataset.theme === t, theme);
|
||||
const expanded = await inspect(fullscreen,width < 1000);
|
||||
await page.screenshot({path:`${output}/${width}-${theme}-fullscreen.png`});
|
||||
await page.keyboard.press('Escape');
|
||||
await page.waitForFunction(() => !document.querySelector('[data-diagram="strategy-roadmap"] dialog').open);
|
||||
assert(await opener.evaluate(n => n === document.activeElement));
|
||||
assert(!await page.evaluate(() => document.documentElement.scrollWidth > innerWidth));
|
||||
results.push({width,theme,inline,expanded});
|
||||
fs.writeFileSync(`${output}/results.json`,JSON.stringify(results,null,2));
|
||||
const page=await context.newPage(); const errors=[]; page.on('pageerror',e=>errors.push(e.message));
|
||||
assert.equal((await page.goto(`${base}/eldorado-v1/documentation/strategy/roadmap/`,{waitUntil:'networkidle'})).status(),200);
|
||||
await page.addStyleTag({content:'html {scroll-behavior:auto !important;}'});
|
||||
assert.equal(await page.locator('.archify-panel--story').count(),3);
|
||||
for(const name of Object.keys(views)) {
|
||||
const panel=page.locator(`[data-diagram="${name}"]`);
|
||||
const iframe=panel.locator(width<1100?'.story-mobile':'.story-desktop');
|
||||
await iframe.scrollIntoViewIfNeeded();
|
||||
const frame=await (await iframe.elementHandle()).contentFrame();
|
||||
await frame.locator('svg[role="img"]').waitFor();
|
||||
const opener=panel.getByRole('button',{name:/^Expand /});
|
||||
for(const theme of ['light','dark']) {
|
||||
const frameTop=await iframe.evaluate(n=>n.getBoundingClientRect().top+scrollY);
|
||||
await page.evaluate(y=>scrollTo({top:y-120,behavior:'instant'}),frameTop);
|
||||
if(await frame.locator('html').getAttribute('data-theme')!==theme) await frame.locator('#btn-theme').click();
|
||||
await page.waitForFunction(t=>document.documentElement.dataset.theme===t,theme);
|
||||
const inline=await inspect(frame,name,width<1100);
|
||||
await frame.locator('#btn-preset').focus();await page.keyboard.press('Enter');
|
||||
await frame.locator('[data-preset-value="blueprint"]').focus();await page.keyboard.press('Enter');
|
||||
await frame.waitForFunction(()=>document.documentElement.dataset.preset==='blueprint');
|
||||
await inspect(frame,name,width<1100);
|
||||
await frame.locator('#btn-preset').focus();await page.keyboard.press('Enter');
|
||||
await frame.locator('[data-preset-value="classic"]').focus();await page.keyboard.press('Enter');
|
||||
await frame.waitForFunction(()=>document.documentElement.dataset.preset==='classic');
|
||||
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 exported=`${output}/${width}-${theme}-${name}.svg`;
|
||||
await download.saveAs(exported);
|
||||
const svg=fs.readFileSync(exported,'utf8');
|
||||
for(const id of views[name]) assert(svg.includes(`data-node-id="${id}"`));
|
||||
assert(svg.includes('eldorado-roadmap-typography'));
|
||||
await panel.screenshot({path:`${output}/${width}-${theme}-${name}-inline.png`});
|
||||
// Scroll each card into the real viewport: tall iframe captures alone
|
||||
// may contain compositor blanks even when DOM geometry is correct.
|
||||
for(const card of inline.cards) {
|
||||
const top=await iframe.evaluate(n=>n.getBoundingClientRect().top+scrollY);
|
||||
await page.evaluate(y=>scrollTo({top:y,behavior:'instant'}),top+card.box.top-100);
|
||||
await page.screenshot({path:`${output}/${width}-${theme}-${name}-${card.id}.png`});
|
||||
}
|
||||
await opener.focus();await page.keyboard.press('Enter');
|
||||
const close=panel.getByRole('button',{name:'Close full-screen diagram',exact:true});await close.waitFor({state:'visible'});
|
||||
const fullIframe=panel.locator('dialog iframe');
|
||||
const fullscreen=await (await fullIframe.elementHandle()).contentFrame();
|
||||
await fullscreen.waitForFunction(t=>document.documentElement.dataset.theme===t,theme);
|
||||
const expanded=await inspect(fullscreen,name,width<1000);
|
||||
await page.screenshot({path:`${output}/${width}-${theme}-${name}-fullscreen.png`});
|
||||
await close.focus();await page.keyboard.press('Escape');
|
||||
await fullIframe.waitFor({state:'detached'});assert(await opener.evaluate(n=>n===document.activeElement));
|
||||
results.push({width,theme,name,inline,expanded});fs.writeFileSync(`${output}/results.json`,JSON.stringify(results,null,2));
|
||||
}
|
||||
}
|
||||
assert.deepEqual(errors,[]);
|
||||
assert(!await page.evaluate(()=>document.documentElement.scrollWidth>innerWidth));assert.deepEqual(errors,[]);
|
||||
} finally {await context.close();}
|
||||
}
|
||||
assert.equal(results.length,8);
|
||||
for (const viewport of [{width:390,height:667},{width:320,height:568}]) {
|
||||
const context = await browser.newContext({viewport});
|
||||
assert.equal(results.length,24);
|
||||
for(const viewport of [{width:390,height:667},{width:320,height:568}]) {
|
||||
const context=await browser.newContext({viewport});
|
||||
try {
|
||||
const page = await context.newPage();
|
||||
await page.goto(`${base}/eldorado-v1/documentation/strategy/roadmap/`, {waitUntil:'networkidle'});
|
||||
const panel = page.locator('[data-diagram="strategy-roadmap"]');
|
||||
const opener = panel.getByRole('button', {name:/^Expand /});
|
||||
await opener.focus(); await page.keyboard.press('Enter');
|
||||
const modal = panel.locator('dialog');
|
||||
const close = modal.getByRole('button', {name:'Close full-screen diagram', exact:true});
|
||||
await close.waitFor({state:'visible'});
|
||||
const iframe = modal.locator('iframe');
|
||||
const frame = await (await iframe.elementHandle()).contentFrame();
|
||||
const geometry = await inspect(frame,true);
|
||||
assert(await modal.evaluate(n => n.scrollHeight > n.clientHeight), 'short fullscreen must scroll');
|
||||
for (const card of geometry.cards) {
|
||||
await modal.evaluate((n, y) => {n.scrollTop = y;}, card.box.top);
|
||||
const frameBox = await iframe.boundingBox();
|
||||
const toolbarBox = await modal.locator('.archify-modal-toolbar').boundingBox();
|
||||
const closeBox = await close.boundingBox();
|
||||
assert(closeBox.y >= 0 && closeBox.y + closeBox.height <= viewport.height, 'close remains visible');
|
||||
assert(frameBox.y + card.box.top >= toolbarBox.y + toolbarBox.height - 1 && frameBox.y + card.box.bottom <= viewport.height + 1, `stage unreachable: ${card.id}`);
|
||||
const page=await context.newPage();await page.goto(`${base}/eldorado-v1/documentation/strategy/roadmap/`,{waitUntil:'networkidle'});
|
||||
await page.addStyleTag({content:'html {scroll-behavior:auto !important;}'});
|
||||
for(const name of Object.keys(views)) {
|
||||
const panel=page.locator(`[data-diagram="${name}"]`), opener=panel.getByRole('button',{name:/^Expand /});
|
||||
await opener.focus();await page.keyboard.press('Enter');const modal=panel.locator('dialog'), close=modal.getByRole('button',{name:'Close full-screen diagram',exact:true});await close.waitFor({state:'visible'});
|
||||
const iframe=modal.locator('iframe'), frame=await (await iframe.elementHandle()).contentFrame();
|
||||
const geometry=await inspect(frame,name,true);
|
||||
assert(await modal.evaluate(n=>n.scrollHeight>n.clientHeight),'short fullscreen must scroll');
|
||||
for(const card of geometry.cards) {
|
||||
await modal.evaluate((n,y)=>{n.scrollTop=y;},card.box.top);
|
||||
const b=await iframe.boundingBox(), t=await modal.locator('.archify-modal-toolbar').boundingBox(), c=await close.boundingBox();
|
||||
assert(c.y>=0 && c.y+c.height<=viewport.height);
|
||||
assert(b.y+card.box.top>=t.y+t.height-1 && b.y+card.box.bottom<=viewport.height+1,`unreachable ${name} ${card.id}`);
|
||||
}
|
||||
await modal.evaluate(n=>{n.scrollTop=0;});
|
||||
for(const selector of ['#btn-theme','#btn-preset','#btn-export']) {
|
||||
const control=frame.locator(selector), b=await control.boundingBox();assert(b.x>=0 && b.x+b.width<=viewport.width && b.y>=0 && b.y+b.height<=viewport.height);await control.click();if(selector!=='#btn-theme')await control.click();
|
||||
}
|
||||
await close.focus();await page.keyboard.press('Escape');await iframe.waitFor({state:'detached'});assert(await opener.evaluate(n=>n===document.activeElement));
|
||||
await page.keyboard.press('Enter');await close.waitFor({state:'visible'});await close.click();await iframe.waitFor({state:'detached'});assert(await opener.evaluate(n=>n===document.activeElement));
|
||||
results.push({viewport,name,scrollable:geometry});fs.writeFileSync(`${output}/results.json`,JSON.stringify(results,null,2));
|
||||
}
|
||||
// Controls stay reachable at the beginning of the scrollable viewer.
|
||||
await modal.evaluate(n => {n.scrollTop = 0;});
|
||||
for (const selector of ['#btn-theme','#btn-preset','#btn-export']) {
|
||||
const control = frame.locator(selector);
|
||||
const box = await control.boundingBox();
|
||||
assert(box.x >= 0 && box.x + box.width <= viewport.width && box.y >= 0 && box.y + box.height <= viewport.height, `control outside viewport: ${selector}`);
|
||||
await control.click();
|
||||
if (selector !== '#btn-theme') await control.click();
|
||||
}
|
||||
await close.focus(); await page.keyboard.press('Escape');
|
||||
await page.waitForFunction(() => !document.querySelector('[data-diagram="strategy-roadmap"] dialog').open);
|
||||
await iframe.waitFor({state:'detached'});
|
||||
assert(await opener.evaluate(n => n === document.activeElement), 'Escape restores opener focus');
|
||||
await page.keyboard.press('Enter');
|
||||
await close.waitFor({state:'visible'});
|
||||
await close.focus(); await page.keyboard.press('Enter');
|
||||
await page.waitForFunction(() => !document.querySelector('[data-diagram="strategy-roadmap"] dialog').open);
|
||||
assert(await opener.evaluate(n => n === document.activeElement), 'close button restores opener focus');
|
||||
results.push({viewport,scrollable:geometry});
|
||||
fs.writeFileSync(`${output}/results.json`,JSON.stringify(results,null,2));
|
||||
} finally {await context.close();}
|
||||
}
|
||||
assert.equal(results.length,10);
|
||||
console.log('PASS 16 timeline contexts: 4 widths × 2 themes × inline/fullscreen; ordered readable text, no clipping, keyboard exit.');
|
||||
console.log('PASS short fullscreen 390×667 and 320×568: readable stages reachable by scroll, sticky close, accessible controls, keyboard close and focus restoration.');
|
||||
assert.equal(results.length,30);
|
||||
console.log('PASS 48 diagram contexts: three views × four widths × light/dark × inline/fullscreen; plus six short-phone scrolling/keyboard contexts.');
|
||||
} finally {await browser.close();}
|
||||
})().catch(error => {console.error(error); process.exitCode=1;});
|
||||
})().catch(error=>{console.error(error);process.exitCode=1;});
|
||||
+10
-10
@@ -8,8 +8,8 @@ const fs = require('node:fs');
|
||||
const browser = await chromium.launch({headless:true, executablePath:process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE, args:['--no-sandbox']});
|
||||
const results = [];
|
||||
try {
|
||||
// Intermediate widths must use the readable vertical Roadmap, not shrink five cards.
|
||||
for (const width of [768, 1024, 1280]) {
|
||||
// Intermediate widths must use the readable vertical Roadmap, not shrink the loop.
|
||||
for (const width of [768, 1024]) {
|
||||
const context = await browser.newContext({viewport:{width,height:1000}, colorScheme:'light'});
|
||||
try {
|
||||
const page = await context.newPage();
|
||||
@@ -31,7 +31,7 @@ const fs = require('node:fs');
|
||||
text:[...node.querySelectorAll('text')].map(t => ({text:t.textContent, box:t.getBoundingClientRect().toJSON(), size:parseFloat(getComputedStyle(t).fontSize)*t.getScreenCTM().a}))
|
||||
})).sort((a,b) => a.box.y-b.box.y || a.box.x-b.box.x)
|
||||
}));
|
||||
assert.deepEqual(geometry.nodes.map(n => n.id), ['assessment','readiness','pilot','expansion','sustain']);
|
||||
assert.deepEqual(geometry.nodes.map(n => n.id), ['skills','autonomy']);
|
||||
for (const [index, card] of geometry.nodes.entries()) {
|
||||
assert(card.text.length > 0, `${width}: missing text in ${card.id}`);
|
||||
assert(card.box.left >= 0 && card.box.top >= 0 && card.box.right <= geometry.width + 1 && card.box.bottom <= geometry.height + 1, `${width}: card outside iframe: ${card.id}`);
|
||||
@@ -61,16 +61,16 @@ const fs = require('node:fs');
|
||||
assert.equal(response.status(), 200);
|
||||
assert.equal(await page.locator('h1').innerText(), 'Roadmap');
|
||||
const headings = page.locator('.strategy-content > h2');
|
||||
assert.deepEqual((await headings.allTextContents()).map(s => s.replace(/\u200b/g, '')), ['1. Understand code quality and delivery','2. Fix critical readiness gaps','3. Run a bounded pilot','4. Expand only on evidence','5. Sustain and improve']);
|
||||
assert.deepEqual((await headings.allTextContents()).map(s => s.replace(/\u200b/g, '')), ['Two stages toward Target','Stage 1: Delivery first','Improve within each phase','Delivery in practice']);
|
||||
const text = await page.locator('.strategy-content').innerText();
|
||||
for (const marker of ['baseline findings', 'process map', 'prioritized gaps', 'alongside the pilot', 'Recommended sequence']) assert(text.includes(marker), marker);
|
||||
for (const marker of ['Humans direct, guide and review throughout', 'Delivery keeps improving', 'system-logic source', 'baseline', 'Target operating model']) assert(text.includes(marker), marker);
|
||||
assert(!text.includes('Supporting gate evidence and hold conditions\n\nThese are'));
|
||||
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()).slice(0,2), ['Target','Roadmap']);
|
||||
if (width < 700) await page.getByRole('button', {name:'Close navigation bar'}).click();
|
||||
const panel = page.locator('.archify-panel--story');
|
||||
const panel = page.locator('.archify-panel--story').first();
|
||||
const iframe = panel.locator(width > 700 ? '.story-desktop' : '.story-mobile');
|
||||
await iframe.scrollIntoViewIfNeeded();
|
||||
const frame = await (await iframe.elementHandle()).contentFrame();
|
||||
@@ -92,7 +92,7 @@ const fs = require('node:fs');
|
||||
width:innerWidth, height:innerHeight,
|
||||
minLabel:Math.min(...[...svg.querySelectorAll('text[data-node-label]')].map(n => parseFloat(getComputedStyle(n).fontSize)*n.getScreenCTM().a))
|
||||
}));
|
||||
assert.deepEqual(geometry.nodes.map(n => n.id), ['assessment','readiness','pilot','expansion','sustain']);
|
||||
assert.deepEqual(geometry.nodes.map(n => n.id), ['skills','autonomy']);
|
||||
for (const n of geometry.nodes) assert(n.x >= 0 && n.y >= 0 && n.right <= geometry.width + 1 && n.bottom <= geometry.height + 1);
|
||||
assert(geometry.minLabel >= 10, `readable node labels: ${geometry.minLabel}`);
|
||||
for (let i=1; i<geometry.nodes.length; i++) {
|
||||
@@ -125,9 +125,9 @@ const fs = require('node:fs');
|
||||
const [download] = await Promise.all([page.waitForEvent('download'), frame.locator('[data-format="svg"]').click()]);
|
||||
assert.equal(await download.failure(), null);
|
||||
await download.saveAs(`${output}/${width}.svg`);
|
||||
assert(fs.readFileSync(`${output}/${width}.svg`, 'utf8').includes('Understand today'));
|
||||
assert(fs.readFileSync(`${output}/${width}.svg`, 'utf8').includes('Adopt skills'));
|
||||
assert(fs.readFileSync(`${output}/${width}.svg`, 'utf8').includes('eldorado-roadmap-typography'));
|
||||
for (const id of ['assessment','readiness','pilot','expansion','sustain']) {
|
||||
for (const id of ['skills','autonomy']) {
|
||||
await frame.locator(`svg[role="img"] [data-node-id="${id}"]`).screenshot({path:`${output}/${width}-${id}-card.png`});
|
||||
}
|
||||
const opener = panel.getByRole('button', {name:/^Expand /});
|
||||
@@ -159,6 +159,6 @@ const fs = require('node:fs');
|
||||
fs.writeFileSync(`${output}/results.json`, JSON.stringify(results,null,2));
|
||||
await context.close();
|
||||
}
|
||||
console.log(`PASS ${results.length} Roadmap viewports: visible stages/outputs, sidebar, diagram geometry, controls, export and links`);
|
||||
console.log(`PASS ${results.length} Roadmap viewports: three-level story, sidebar, diagram geometry, controls, export and links`);
|
||||
} finally {await browser.close();}
|
||||
})().catch(error => {console.error(error); process.exitCode = 1;});
|
||||
@@ -12,14 +12,14 @@ const fs=require('node:fs');
|
||||
const page=await context.newPage();
|
||||
for(const id of ['index','target-operating-model','skills-governance','skills-quality','skills-adoption','measurement','skills-maintenance','roadmap']){
|
||||
const response=await page.goto(`${base}/eldorado-v1/documentation/strategy/${id==='index'?'':id+'/'}`,{waitUntil:'networkidle'});assert.equal(response.status(),200);
|
||||
const panel=page.locator('.archify-panel--story');
|
||||
const panel=page.locator('.archify-panel--story').first();
|
||||
const iframe=panel.locator(width===390?'.story-mobile':'.story-desktop');await iframe.scrollIntoViewIfNeeded();
|
||||
const frame=await (await iframe.elementHandle()).contentFrame();await frame.locator('svg[role="img"]').waitFor();
|
||||
const geometry=await frame.locator('svg[role="img"]').evaluate(svg=>{
|
||||
const nodes=[...svg.querySelectorAll('[data-node-id]')].map(n=>{const r=n.getBoundingClientRect();return {id:n.id,x:r.x,y:r.y,right:r.right,bottom:r.bottom};});
|
||||
return {width:innerWidth,height:innerHeight,nodes,svg:svg.getBoundingClientRect().toJSON(),minLabel:Math.min(...[...svg.querySelectorAll('text[data-node-label]')].map(n=>parseFloat(getComputedStyle(n).fontSize)*n.getScreenCTM().a))};
|
||||
});
|
||||
assert(geometry.nodes.length>=5 && geometry.nodes.length<=(['index','target-operating-model'].includes(id)?18:7),id);
|
||||
assert(geometry.nodes.length>=(id==='roadmap'?2:5) && geometry.nodes.length<=(['index','target-operating-model'].includes(id)?18:7),id);
|
||||
for(const n of geometry.nodes) assert(n.x>=0 && n.y>=0 && n.right<=geometry.width+1 && n.bottom<=geometry.height+1,`${id} ${width} clipped ${JSON.stringify(n)} / ${geometry.height}`);
|
||||
const data=await page.locator('.strategy-content').evaluate(el=>({words:el.innerText.trim().split(/\s+/).length,overflow:document.documentElement.scrollWidth>innerWidth,openDetails:el.querySelectorAll('details[open]').length}));
|
||||
assert(data.words<=(id==='measurement'?1100:id==='roadmap'?600:200),id);assert(!data.overflow,id);assert.equal(data.openDetails,0,id);
|
||||
|
||||
@@ -26,7 +26,7 @@ for (const file of readdirSync('diagrams').sort()) {
|
||||
mkdirSync(`static/diagrams/${name}`, {recursive: true});
|
||||
if (spec.diagram_type !== type || spec.meta.output !== output) throw new Error(`Diagram type/output mismatch: ${file}`);
|
||||
const run = args => execFileSync(process.execPath,[cli,...args],{stdio:'inherit',env:{...process.env,ARCHIFY_UPDATE_CHECK_DISABLED:'1'}});
|
||||
const roadmap = ['strategy-roadmap', 'strategy-roadmap-mobile'].includes(name);
|
||||
const roadmap = /^strategy-roadmap(?:-(?:adoption|loop))?(?:-mobile)?$/.test(name);
|
||||
if (roadmap) {
|
||||
// Native renderer validates schema and compiles layout. Check readability
|
||||
// after multiline typography, not upstream's single-line preview.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Native Workflow v2 owns node geometry, mainPath and routing. The portal adds
|
||||
// multiline typography: upstream's three single-line labels are too small for
|
||||
// a five-stage reading view. Do not change the pinned vendor or graph semantics.
|
||||
// a Roadmap reading view. Do not change the pinned vendor or graph semantics.
|
||||
const escape = text => text.replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>').replaceAll('"', '"');
|
||||
const wrap = (text, limit = 21) => {
|
||||
const lines = [];
|
||||
@@ -11,7 +11,7 @@ const wrap = (text, limit = 21) => {
|
||||
return lines;
|
||||
};
|
||||
export function roadmapViewer(html, spec) {
|
||||
if (spec.diagram_type !== 'workflow' || spec.schema_version !== 2 || spec.nodes.length !== 5) throw new Error('Roadmap workflow contract drift');
|
||||
if (spec.diagram_type !== 'workflow' || spec.schema_version !== 2 || ![2, 6].includes(spec.nodes.length)) throw new Error('Roadmap workflow contract drift');
|
||||
const boxes = [];
|
||||
for (const [index, node] of spec.nodes.entries()) {
|
||||
const start = html.indexOf(`<g id="node-${node.id}"`);
|
||||
@@ -30,7 +30,8 @@ export function roadmapViewer(html, spec) {
|
||||
return `<text x="${x+24}" y="${y+top}" font-size="${size}" class="${cls}" ${attrs}>${lines.map((line,i) => `<tspan x="${x+24}" dy="${i ? 34 : 0}">${escape(line)}</tspan>`).join('')}</text>`;
|
||||
};
|
||||
if (!node.sublabel || node.tag || /Output:|Exit:|\n/.test(node.sublabel)) throw new Error(`Roadmap content contract drift: ${node.id}`);
|
||||
const content = text(String(index+1), 40, 26, 't-primary', 'data-stage-number="" font-weight="700"')
|
||||
const number = spec.nodes.length === 6 ? `Step ${index+1}` : spec.nodes[0].id === 'skills' ? `Stage ${index+1}` : index === 0 ? 'First' : 'Then';
|
||||
const content = text(number, 40, 26, 't-primary', 'data-stage-number="" font-weight="700"')
|
||||
+ text(node.label, 88, 30, 't-primary', 'data-node-label="" font-weight="700"')
|
||||
+ text(node.sublabel, 160, 26, 't-muted');
|
||||
// Preserve native node title/metadata and rectangles; replace sigil/text only.
|
||||
@@ -41,7 +42,8 @@ export function roadmapViewer(html, spec) {
|
||||
const replacement = original.slice(0,prefixEnd).replaceAll('rx="6"', 'rx="0"') + content + '</g>';
|
||||
html = html.slice(0,start) + replacement + html.slice(end);
|
||||
}
|
||||
const left = Math.min(...boxes.map(b => b.x)) - 12;
|
||||
// The native mobile return channel sits outside the card column.
|
||||
const left = Math.min(...boxes.map(b => b.x)) - (spec.edges.some(e => e.route === 'return-left') ? 80 : 12);
|
||||
const top = Math.min(...boxes.map(b => b.y)) - 12;
|
||||
const width = Math.max(...boxes.map(b => b.x+b.w)) - left + 12;
|
||||
const height = Math.max(...boxes.map(b => b.y+b.h)) - top + 12;
|
||||
@@ -51,6 +53,7 @@ export function roadmapViewer(html, spec) {
|
||||
if (roots.length !== 1) throw new Error('Roadmap SVG contract drift');
|
||||
const root = roots[0][0].replace(/viewBox="[^"]*"/, `viewBox="${left} ${top} ${width} ${height}"`);
|
||||
const styles = `<style id="eldorado-roadmap-typography">
|
||||
html[data-embed="true"] .diagram-container > svg {max-height:calc(100vh - 84px);}
|
||||
[data-node-id] text {font-family: Arial, sans-serif;}
|
||||
svg[data-preset="classic"] [data-node-id] > rect.c-backend,
|
||||
svg[data-preset="editorial"] [data-node-id] > rect.c-backend {fill:var(--panel); stroke:var(--border);}
|
||||
|
||||
@@ -4,18 +4,18 @@ import {readFileSync, writeFileSync, mkdtempSync, rmSync, mkdirSync} from 'node:
|
||||
import {execFileSync} from 'node:child_process';
|
||||
import {roadmapViewer} from './roadmap-viewer.mjs';
|
||||
|
||||
for (const suffix of ['', '-mobile']) test(`native Workflow${suffix} retains graph and passes final composition`, () => {
|
||||
for (const name of ['strategy-roadmap', 'strategy-roadmap-adoption', 'strategy-roadmap-loop']) for (const suffix of ['', '-mobile']) test(`${name}${suffix} retains graph and passes final composition`, () => {
|
||||
mkdirSync('.cache', {recursive:true});
|
||||
const dir = mkdtempSync('.cache/roadmap-test-');
|
||||
try {
|
||||
const source = `diagrams/strategy-roadmap${suffix}.workflow.json`;
|
||||
const source = `diagrams/${name}${suffix}.workflow.json`;
|
||||
const spec = JSON.parse(readFileSync(source, 'utf8'));
|
||||
const output = `${dir}/index.html`;
|
||||
execFileSync(process.execPath, ['vendor/archify/renderers/workflow/render-workflow.mjs', source, output], {env:{...process.env, ARCHIFY_UPDATE_CHECK_DISABLED:'1'}});
|
||||
const native = readFileSync(output, 'utf8');
|
||||
const styled = roadmapViewer(native, spec);
|
||||
const graph = html => [...html.matchAll(/<path\b[^>]*data-edge-id="[^"]+"[^>]*>/g)].map(m => m[0]);
|
||||
assert.equal(graph(native).length, 4);
|
||||
assert.equal(graph(native).length, spec.edges.length);
|
||||
assert.deepEqual(graph(styled), graph(native), 'native paths must not be rewritten');
|
||||
for (const node of spec.nodes) assert(styled.includes(`data-node-id="${node.id}"`));
|
||||
assert.equal((styled.match(/<text data-node-label=/g) || []).length, 0, 'single-line preview replaced');
|
||||
|
||||
+24
-25
@@ -3,33 +3,32 @@ import assert from 'node:assert/strict';
|
||||
import {readFileSync} from 'node:fs';
|
||||
const read = path => readFileSync(path, 'utf8');
|
||||
|
||||
test('Roadmap visibly assesses code and real delivery before adoption', () => {
|
||||
export const views = {
|
||||
'strategy-roadmap': ['skills', 'autonomy'],
|
||||
'strategy-roadmap-adoption': ['delivery', 'remaining'],
|
||||
'strategy-roadmap-loop': ['understand', 'baseline', 'topology', 'use', 'collect', 'reassess'],
|
||||
};
|
||||
test('Roadmap has three visual levels and preserves human direction and Delivery discovery', () => {
|
||||
const page = read('docs/strategy/roadmap.mdx');
|
||||
const visible = page.split('<details>')[0];
|
||||
const headings = [...visible.matchAll(/^## (.+)$/gm)].map(match => match[1]);
|
||||
assert.deepEqual(headings, ['1. Understand code quality and delivery', '2. Fix critical readiness gaps', '3. Run a bounded pilot', '4. Expand only on evidence', '5. Sustain and improve']);
|
||||
assert(visible.indexOf(' story />') < visible.indexOf('## 1.'));
|
||||
for (const marker of ['representative repositories', 'tests and CI', 'maintainability', 'technical debt', 'security findings', 'actual SDLC', 'ownership', 'review and release', 'bottlenecks', 'baseline findings', 'process map', 'prioritized gaps', 'alongside the pilot', './measurement.mdx', './target-operating-model.mdx']) assert(visible.includes(marker), marker);
|
||||
assert.equal((visible.match(/\*\*Output:/g) || []).length, 5);
|
||||
assert.match(visible, /Recommended sequence/);
|
||||
assert(!page.includes('### Foundation'));
|
||||
for (const marker of ['revocation', 'exact-artifact approvals', 'idempotency', 'human takeover', 'sensitive decisions']) assert(page.includes(marker), marker);
|
||||
assert.deepEqual([...visible.matchAll(/<ArchifyDiagram name="([^"]+)"/g)].map(m => m[1]), Object.keys(views));
|
||||
assert.deepEqual([...visible.matchAll(/^## (.+)$/gm)].map(m => m[1]), ['Two stages toward Target', 'Stage 1: Delivery first', 'Improve within each phase', 'Delivery in practice']);
|
||||
for (const marker of ['Humans direct, guide and review throughout', 'standardizes skills', 'foundation for Stage 2', 'future Autonomous Agent', 'not deployed autonomy', 'order is not yet decided', 'Delivery keeps improving', 'not as six global roadmap stages', 'what skills exist, how they relate, when they are used and how they work together', 'codebase structure', 'existing principles', 'system-logic source', 'tests', 'builds and packaging', 'deployments', 'practices, tooling, controls and problems', 'metrics/KPIs and a baseline', 'friction and feedback', './measurement.mdx', './target-operating-model.mdx']) assert(visible.includes(marker), marker);
|
||||
for (const marker of ['revocation','exact-artifact approvals','idempotency','human takeover','sensitive decisions','security findings','reproducible baseline']) assert(page.includes(marker), marker);
|
||||
assert(!/## [1-5]\.|Prepare foundations|Sustain and improve/.test(page));
|
||||
});
|
||||
|
||||
test('both roadmap diagrams make assessment and readiness prerequisites to the pilot', () => {
|
||||
const ids = ['assessment', 'readiness', 'pilot', 'expansion', 'sustain'];
|
||||
for (const suffix of ['', '-mobile']) {
|
||||
const diagram = JSON.parse(read(`diagrams/strategy-roadmap${suffix}.workflow.json`));
|
||||
assert.equal(diagram.schema_version, 2);
|
||||
assert.equal(diagram.diagram_type, 'workflow');
|
||||
assert.deepEqual(diagram.mainPath, ids);
|
||||
assert.deepEqual(diagram.nodes.map(n => n.id), ids);
|
||||
assert.deepEqual(diagram.nodes.map(n => n.label), ['Understand today', 'Prepare foundations', 'Pilot', 'Expand', 'Sustain']);
|
||||
assert.deepEqual(diagram.edges.map(e => [e.from,e.to]), ids.slice(1).map((id, i) => [ids[i],id]));
|
||||
assert.equal(diagram.nodes[0].sublabel, 'Code quality and delivery processes understood.');
|
||||
assert.equal(diagram.nodes[1].sublabel, 'Critical pilot gaps closed.');
|
||||
assert(diagram.nodes.every(n => !n.sublabel.includes('Output:') && !n.tag && n.height === 240));
|
||||
assert.deepEqual(diagram.nodes.map(n => n.col), suffix ? [0,0,0,0,0] : [0,1,2,3,4]);
|
||||
assert.equal(diagram.lanes.length, 1);
|
||||
for (const [name, ids] of Object.entries(views)) test(`${name}: native graph, mobile parity and loop semantics`, () => {
|
||||
const variants = ['', '-mobile'].map(suffix => JSON.parse(read(`diagrams/${name}${suffix}.workflow.json`)));
|
||||
for (const [index, d] of variants.entries()) {
|
||||
assert.equal(d.schema_version, 2);
|
||||
assert.equal(d.diagram_type, 'workflow');
|
||||
assert.deepEqual(d.mainPath, ids.length === 6 ? ids.slice(0,3) : ids);
|
||||
assert.deepEqual(d.nodes.map(n => n.id), ids);
|
||||
assert.deepEqual(d.edges.filter(e => e.role === 'main').map(e => [e.from,e.to]), ids.slice(1).map((id,i) => [ids[i],id]));
|
||||
assert.deepEqual(d.edges.filter(e => e.role === 'return').map(e => [e.from,e.to]), ids.length === 6 ? [['reassess','understand']] : []);
|
||||
if (index) assert(d.nodes.every(n => n.col === 0));
|
||||
assert(d.nodes.every(n => n.sublabel && !n.tag && n.height === 240));
|
||||
}
|
||||
const semantics = d => ({nodes:d.nodes.map(({col,yOffset,...n}) => n), edges:d.edges.map(({fromSide,toSide,route,...e}) => e)});
|
||||
assert.deepEqual(semantics(variants[0]),semantics(variants[1]));
|
||||
});
|
||||
@@ -8,12 +8,12 @@ test('every strategy page leads with a compact diagram story and closes its refe
|
||||
for (const {id} of model.pages) {
|
||||
const page = read(`docs/strategy/${id}.mdx`);
|
||||
const narrative = page.split('<div className="strategy-content">')[1].replace(/<details>[\s\S]*?<\/details>/g, '');
|
||||
assert.equal((narrative.match(/<ArchifyDiagram /g) || []).length, 1, id);
|
||||
assert.equal((narrative.match(/<ArchifyDiagram /g) || []).length, id === 'roadmap' ? 3 : 1, id);
|
||||
assert(narrative.includes(' story />'), id);
|
||||
assert(!page.includes('<details>') || page.indexOf(' story />') < page.indexOf('<details>'), id);
|
||||
assert(!/<details\s+open/.test(page), id);
|
||||
assert.equal((page.match(/<details>/g) || []).length, (page.match(/<\/details>/g) || []).length, id);
|
||||
assert.equal((narrative.match(/^## /gm) || []).length, id === 'measurement' ? 4 : id === 'roadmap' ? 5 : 1, id);
|
||||
assert.equal((narrative.match(/^## /gm) || []).length, id === 'measurement' ? 4 : id === 'roadmap' ? 4 : 1, id);
|
||||
assert((narrative.match(/^- /gm) || []).length <= 3, id);
|
||||
assert(narrative.includes('**Next:**'), id);
|
||||
assert(!narrative.includes('| ---'), id);
|
||||
|
||||
Reference in new issue
Block a user