fix(workspace): match the concept brand name and compare shell hover states

This commit is contained in:
Vito Sansevero
2026-10-19 22:48:39 -07:00
parent 720231f7ba
commit cc271838c9
3 changed files with 172 additions and 75 deletions
+88 -62
View File
@@ -46,6 +46,16 @@ export async function focusRingOf(locator) {
outlineColor:style.outlineColor,outlineOffset:style.outlineOffset};});
}
/** Computed colors of an element while the pointer rests on it; the pointer is parked again afterwards. */
export async function hoverStateOf(page,locator) {
await locator.hover();
const state=await locator.evaluate(node=>{const style=getComputedStyle(node);return {hovered:node.matches(':hover'),
color:style.color,backgroundColor:style.backgroundColor,borderTopColor:style.borderTopColor,
textDecorationLine:style.textDecorationLine,boxShadow:style.boxShadow};});
await page.mouse.move(0,0);
return state;
}
/**
* Inventory sections: {chrome, heading, toolbar, filterRow, caption, grid, cards, panel, library, history,
* noPageScroll}. A section is null when its element is absent. Sections whose page PR has not landed yet
@@ -96,72 +106,88 @@ export function measureInventory(scope) {
gapAfter,icon:iconOf(link),
count:count?{text:count.textContent,shown:shown(count),...font(count),...css(count,'marginInlineStart')}:null};
}
function chromeOf() {
if(!sidebar||!workspace)return null;
const shell=document.querySelector('.app-shell');
const brand=sidebar.querySelector('.brand'),mark=brand.querySelector('.brand-mark'),small=brand.querySelector('small');
const bottom=sidebar.querySelector('.sidebar-bottom'),connection=sidebar.querySelector('.connection');
const topbar=workspace.querySelector('.topbar'),crumbs=topbar.querySelector('.breadcrumbs'),actions=topbar.querySelector('.top-actions');
const footer=workspace.querySelector('.app-footer');
// Every subsection is null when its element is missing, so a removed node is reported by its path
// (for example `chrome.breadcrumbs`) instead of failing the measurement.
const when=(node,measure)=>node?measure(node):null;
// The accessible text of a node: its text without aria-hidden descendants.
const accessibleText=node=>textOf(node,'[aria-hidden="true"]');
function itemOf(node,actions) {
const base={tag:node.tagName.toLowerCase(),className:node.className,shown:shown(node)};
if(!base.shown)return base;
const box=relative(node,actions);
return {...base,text:textOf(node,'select, .count'),...font(node),...edges(node),...layout(node),
left:box.left,top:box.top,width:box.width,height:box.height,
count:node.querySelector('.count')?.textContent??null,
select:when(node.querySelector('select'),select=>({options:[...select.options].map(option=>[option.value,option.textContent]),
label:select.getAttribute('aria-label'),labelled:select.labels?.[0]===node,...font(select),...edges(select),
minHeight:getComputedStyle(select).minHeight,width:px(rect(select).width),height:px(rect(select).height)})),
hiddenSuffix:[...node.querySelectorAll('[aria-hidden="true"]')].map(child=>child.textContent).join('')||null,
whiteSpace:getComputedStyle(node).whiteSpace};
}
function sidebarOf() {
return when(sidebar,()=>({...layout(sidebar),...edges(sidebar),
...css(sidebar,'top','overflowY','borderInlineEndWidth'),width:px(rect(sidebar).width),
fillsViewport:Math.abs(rect(sidebar).height-innerHeight)<=1}));
}
function bottomOf(bottom) {
// In the column sidebar the bottom group is pinned to the end by `margin-top: auto`, whose used value is only
// the free space above it, so the pin (its inset from the sidebar's padding edge) is recorded instead.
const sidebarStyle=getComputedStyle(sidebar);
const columns=getComputedStyle(shell).gridTemplateColumns.split(' ');
const item=node=>{
const base={tag:node.tagName.toLowerCase(),className:node.className,shown:shown(node)};
if(!base.shown)return base;
const box=relative(node,actions);
const select=node.querySelector('select');
return {...base,text:textOf(node,'select, .count'),...font(node),...edges(node),...layout(node),width:box.width,height:box.height,top:box.top,
count:node.querySelector('.count')?.textContent??null,
select:select?{options:[...select.options].map(option=>[option.value,option.textContent]),label:select.getAttribute('aria-label'),
labelled:select.labels?.[0]===node,...font(select),...edges(select),minHeight:getComputedStyle(select).minHeight,
width:px(rect(select).width),height:px(rect(select).height)}:null,
hiddenSuffix:[...node.querySelectorAll('[aria-hidden="true"]')].map(child=>child.textContent).join('')||null,
whiteSpace:getComputedStyle(node).whiteSpace};
};
const pinned=sidebarStyle.display==='flex';
const {marginTop,...rest}=edges(bottom);
return {...rest,...css(bottom,'display'),marginTop:pinned?'auto':marginTop,
bottomInset:pinned?px(rect(sidebar).bottom-rect(bottom).bottom-parseFloat(sidebarStyle.paddingBottom)):null};
}
function topActionsOf(actions,topbar) {
const style=getComputedStyle(topbar);
return {...layout(actions),...edges(actions),width:px(rect(actions).width),height:px(rect(actions).height),
fullWidth:Math.abs(rect(actions).width-(rect(topbar).width-parseFloat(style.paddingLeft)-parseFloat(style.paddingRight)))<=1,
rightInset:px(rect(topbar).right-rect(actions).right),
items:[...actions.children].map(node=>itemOf(node,actions))};
}
function chromeOf() {
if(!sidebar&&!workspace)return null;
const shell=document.querySelector('.app-shell');
const brand=sidebar?.querySelector('.brand');
const topbar=workspace?.querySelector('.topbar');
const footer=workspace?.querySelector('.app-footer');
return {
title:document.title.replace(/ · Prompt Manager.*$/,''),
skip:{text:document.querySelector('a.skip')?.textContent.trim()??null,...css(document.querySelector('a.skip'),'position','top','left')},
title:document.title,
skip:when(document.querySelector('a.skip'),skip=>({text:skip.textContent.trim(),...css(skip,'position','top','left')})),
body:{...font(document.body),backgroundColor:getComputedStyle(document.body).backgroundColor,
tokens:Object.fromEntries(['--ca-text','--ca-muted','--ca-focus','--ca-accent','--ca-field','--ca-surface','--ca-line',
'--line','--page','--sidebar','--soft-blue','--green'].map(name=>[name,getComputedStyle(document.body).getPropertyValue(name).trim()]))},
shell:{display:getComputedStyle(shell).display,columns:columns.length,sidebarColumn:columns.length>1?columns[0]:null,
minHeight:getComputedStyle(shell).minHeight},
sidebar:{...layout(sidebar),...edges(sidebar),...css(sidebar,'top','overflowY','borderInlineEndWidth'),width:px(rect(sidebar).width),
fillsViewport:Math.abs(rect(sidebar).height-innerHeight)<=1},
brand:{text:textOf(brand),href:brand.getAttribute('href')?.replace(/^#\/?/,''),...relative(brand,sidebar),...font(brand),...layout(brand),...edges(brand),
mark:{text:textOf(mark),...font(mark),width:px(rect(mark).width),height:px(rect(mark).height)},
small:{text:small.textContent,...font(small),...css(small,'display','marginTop')}},
labels:[...sidebar.querySelectorAll('.sidebar-label')].map(node=>({text:textOf(node),shown:shown(node),...font(node),...layout(node),...edges(node)})),
navs:[...sidebar.querySelectorAll('nav')].map(nav=>{const links=[...nav.querySelectorAll(':scope > a')];return {
shell:when(shell,()=>{const columns=getComputedStyle(shell).gridTemplateColumns.split(' ');
return {display:getComputedStyle(shell).display,columns:columns.length,sidebarColumn:columns.length>1?columns[0]:null,
minHeight:getComputedStyle(shell).minHeight};}),
sidebar:sidebarOf(),
brand:when(brand,()=>({text:textOf(brand),name:accessibleText(brand),href:brand.getAttribute('href')?.replace(/^#\/?/,''),
...relative(brand,sidebar),...font(brand),...layout(brand),...edges(brand),
mark:when(brand.querySelector('.brand-mark'),mark=>({text:textOf(mark),hidden:mark.getAttribute('aria-hidden'),...font(mark),
width:px(rect(mark).width),height:px(rect(mark).height)})),
small:when(brand.querySelector('small'),small=>({text:small.textContent,...font(small),...css(small,'display','marginTop')}))})),
labels:sidebar?[...sidebar.querySelectorAll('.sidebar-label')].map(node=>({text:textOf(node),shown:shown(node),...font(node),...layout(node),...edges(node)})):null,
navs:sidebar?[...sidebar.querySelectorAll('nav')].map(nav=>{const links=[...nav.querySelectorAll(':scope > a')];return {
id:nav.id,label:nav.getAttribute('aria-label'),shown:shown(nav),...layout(nav),...edges(nav),
width:px(rect(nav).width),rows:links.map(navRow)};}),
bottom:(()=>{
// In the column sidebar the bottom group is pinned to the end by `margin-top: auto`, whose used value is
// only the free space above it, so the pin (its inset from the sidebar's padding edge) is recorded instead.
const pinned=sidebarStyle.display==='flex';
const {marginTop,...rest}=edges(bottom);
return {...rest,...css(bottom,'display'),marginTop:pinned?'auto':marginTop,
bottomInset:pinned?px(rect(sidebar).bottom-rect(bottom).bottom-parseFloat(sidebarStyle.paddingBottom)):null};
})(),
connection:{shown:shown(connection),text:textOf(connection,'small'),small:connection.querySelector('small')?.textContent??null,
...layout(connection),...edges(connection),...font(connection),
smallFont:font(connection.querySelector('small')),
dot:(dot=>({...css(dot,'width','height','borderTopLeftRadius','backgroundColor','marginTop')}))(connection.querySelector('i'))},
topbar:{...layout(topbar),...edges(topbar),...css(topbar,'height','minHeight'),...relative(topbar,workspace),
fullWidth:px(rect(topbar).width)===px(rect(workspace).width)},
breadcrumbs:{shown:shown(crumbs),text:textOf(crumbs),...layout(crumbs),...font(crumbs),
width:px(rect(nav).width),rows:links.map(navRow)};}):null,
bottom:when(sidebar?.querySelector('.sidebar-bottom'),bottomOf),
connection:when(sidebar?.querySelector('.connection'),connection=>({shown:shown(connection),text:textOf(connection,'small'),
small:connection.querySelector('small')?.textContent??null,...layout(connection),...edges(connection),...font(connection),
smallFont:when(connection.querySelector('small'),font),
dot:when(connection.querySelector('i'),dot=>css(dot,'width','height','borderTopLeftRadius','backgroundColor','marginTop'))})),
topbar:when(topbar,()=>({...layout(topbar),...edges(topbar),...css(topbar,'height','minHeight'),...relative(topbar,workspace),
fullWidth:px(rect(topbar).width)===px(rect(workspace).width)})),
breadcrumbs:when(topbar?.querySelector('.breadcrumbs'),crumbs=>({shown:shown(crumbs),text:textOf(crumbs),...layout(crumbs),...font(crumbs),
parts:[...crumbs.querySelectorAll('span')].map(span=>({text:span.textContent,...font(span)})),
...(shown(crumbs)?{left:relative(crumbs,topbar).left,middle:px(rect(crumbs).top+rect(crumbs).height/2-rect(topbar).top)}:{})},
topActions:{...layout(actions),...edges(actions),width:px(rect(actions).width),height:px(rect(actions).height),
fullWidth:Math.abs(rect(actions).width-(rect(topbar).width-parseFloat(getComputedStyle(topbar).paddingLeft)-parseFloat(getComputedStyle(topbar).paddingRight)))<=1,
rightInset:px(rect(topbar).right-rect(actions).right),
items:[...actions.children].map(item)},
main:{tag:main.tagName.toLowerCase(),tabIndex:main.tabIndex,...css(main,'paddingTop','paddingRight','paddingBottom','paddingLeft','maxWidth','marginLeft','marginRight')},
footer:footer?{...layout(footer),...edges(footer),...font(footer),...css(footer,'height'),width:px(rect(footer).width),
...(shown(crumbs)?{left:relative(crumbs,topbar).left,middle:px(rect(crumbs).top+rect(crumbs).height/2-rect(topbar).top)}:{})})),
topActions:when(topbar?.querySelector('.top-actions'),actions=>topActionsOf(actions,topbar)),
main:when(main,()=>({tag:main.tagName.toLowerCase(),tabIndex:main.tabIndex,
...css(main,'paddingTop','paddingRight','paddingBottom','paddingLeft','maxWidth','marginLeft','marginRight')})),
footer:when(footer,()=>({...layout(footer),...edges(footer),...font(footer),...css(footer,'height'),width:px(rect(footer).width),
fullWidth:px(rect(footer).width)===px(rect(workspace).width),
spans:[...footer.querySelectorAll(':scope > span')].map(span=>({text:span.textContent.replace(/\s+/g,' ').trim(),...font(span),height:px(rect(span).height)})),
buttons:[...footer.querySelectorAll('button')].map(button=>({text:button.textContent.trim(),...font(button),...edges(button),
minHeight:getComputedStyle(button).minHeight,height:px(rect(button).height)}))}:null,
minHeight:getComputedStyle(button).minHeight,height:px(rect(button).height)}))})),
};
}
function headingOf() {
@@ -169,13 +195,13 @@ export function measureInventory(scope) {
if(!heading)return null;
const copy=heading.firstElementChild,h1=copy?.querySelector('h1'),description=copy?.querySelector('p'),actions=heading.querySelector('.heading-actions');
return {...layout(heading),...edges(heading),height:px(rect(heading).height),
copy:{tag:copy.tagName.toLowerCase(),...layout(copy)},
h1:{id:h1.id,tabIndex:h1.tabIndex,text:h1.textContent,...font(h1),...edges(h1)},
description:description?{className:description.className,text:description.textContent,...font(description),...edges(description)}:null,
actions:actions?{...layout(actions),...edges(actions),
fullWidth:px(rect(actions).width)===px(rect(heading).width),stacked:rect(actions).top>=rect(copy).bottom-0.5,
copy:when(copy,()=>({tag:copy.tagName.toLowerCase(),...layout(copy)})),
h1:when(h1,()=>({id:h1.id,tabIndex:h1.tabIndex,text:h1.textContent,...font(h1),...edges(h1)})),
description:when(description,()=>({className:description.className,text:description.textContent,...font(description),...edges(description)})),
actions:when(actions,()=>({...layout(actions),...edges(actions),
fullWidth:px(rect(actions).width)===px(rect(heading).width),stacked:copy?rect(actions).top>=rect(copy).bottom-0.5:null,
buttons:[...actions.querySelectorAll('button')].map(node=>({text:node.textContent.trim(),primary:node.classList.contains('ca-primary'),
...font(node),...edges(node),minHeight:getComputedStyle(node).minHeight,height:px(rect(node).height)}))}:null};
...font(node),...edges(node),minHeight:getComputedStyle(node).minHeight,height:px(rect(node).height)}))}))};
}
return {chrome:chromeOf(),heading:headingOf(),toolbar:null,filterRow:null,caption:null,grid:null,cards:null,panel:null,
library:null,history:null,noPageScroll:document.documentElement.scrollWidth<=innerWidth+1};
+83 -12
View File
@@ -1,7 +1,7 @@
import {writeFile} from 'node:fs/promises';
import {test,expect} from '../../tools/vendor/node_modules/@playwright/test/index.mjs';
import {captureCanonicalState} from './history-canonical-comparison.mjs';
import {SIZES,THEMES,MOTIONS,seedConcept,openConcept,openProduction,inventoryOf,diffInventory,focusRingOf} from './concept-inventory.mjs';
import {SIZES,THEMES,MOTIONS,seedConcept,openConcept,openProduction,inventoryOf,diffInventory,focusRingOf,hoverStateOf} from './concept-inventory.mjs';
// Task A6 of #249: the production shell (sidebar, topbar, heading, footer) against the immutable concept, with the
// concept-shaped real fixture so the sidebar counts are the concept's own (24 images, 6 prompts, 15 tags). Both themes,
@@ -18,17 +18,30 @@ const rowNamed=(nav,text)=>nav.rows.find(row=>row.text===text);
const removedItem=item=>String(item.className).includes('concept-badge')||(item.tag==='button'&&item.text.startsWith('Activity'));
/** Left offsets of flex items packed along a row of `width` by its `justify-content` and `gap`. */
function packedLefts(items,width,justify,gap) {
const total=items.reduce((sum,item)=>sum+item.width,0)+gap*(items.length-1);
let start=0,spacing=gap;
if(justify==='flex-end'||justify==='end')start=width-total;
else if(justify==='center')start=(width-total)/2;
else if(justify==='space-between'&&items.length>1)spacing=gap+(width-total)/(items.length-1);
let x=start;
return items.map(item=>{const left=Math.round(x);x+=item.width+spacing;return left;});
}
/** The top-actions row once removed items are gone: a centered flex row, so its size and item offsets follow. */
function remainingTopActions(actions) {
const kept=actions.items.filter(item=>!removedItem(item));
const shown=kept.filter(item=>item.shown);
const height=Math.max(...shown.map(item=>item.height));
const gap=parseFloat(actions.columnGap);
const width=shown.reduce((sum,item)=>sum+item.width,0)+gap*(shown.length-1);
return {...actions,height,
// A full-width row (<=700px) keeps its width; otherwise it shrinks to its remaining items.
width:actions.fullWidth?actions.width:width,
items:kept.map(item=>item.shown?{...item,top:Math.round((height-item.height)/2)}:item)};
// A full-width row (<=700px) keeps its width; otherwise it shrinks to its remaining items.
const width=actions.fullWidth?actions.width:shown.reduce((sum,item)=>sum+item.width,0)+gap*(shown.length-1);
// At <=700px the concept spreads three items with `space-between`, so its Theme select sits in the middle; with
// Activity removed (D5) the same rule puts Theme at the row's start. Offsets come from the concept's own sizes.
const lefts=new Map(shown.map((item,index)=>[item,packedLefts(shown,width,actions.justifyContent,gap)[index]]));
return {...actions,height,width,
items:kept.map(item=>item.shown?{...item,top:Math.round((height-item.height)/2),left:lefts.get(item)}:item)};
}
/** The footer once its button is gone: paddings plus the remaining spans along the footer's flex direction. */
@@ -57,6 +70,8 @@ function mapShellConcept(reference) {
]};
});
return {...reference,chrome:{...chrome,
// D29: no prototype suffix in the document title.
title:chrome.title.replace(/ · Prompt Manager concept$/,' · Prompt Manager'),
// D28: only the WORKSPACE group label remains.
labels:chrome.labels.filter(label=>label.text==='WORKSPACE'),
navs,
@@ -80,14 +95,13 @@ function mapShellConcept(reference) {
buttons:reference.heading.actions.buttons.filter(item=>item.text!=='Auto-tag images')}}};
}
/** Layout and typography of the heading without its page-owned copy and actions. */
/** The heading without its page-owned description copy and actions; the h1 text is the shell's and stays compared. */
function headingFrame(heading) {
if(!heading)return heading;
const {height,h1,description,actions,...frame}=heading;
const {text,...h1Tokens}=h1;
const {height,description,actions,...frame}=heading;
const {text:copy,...descriptionTokens}=description??{};
const {buttons,stacked,...actionTokens}=actions??{};
return {...frame,h1:h1Tokens,description:description?descriptionTokens:null,actions:actions?actionTokens:null};
return {...frame,description:description?descriptionTokens:null,actions:actions?actionTokens:null};
}
function compare(route,reference,actual) {
@@ -113,6 +127,22 @@ async function ringsOf(page,mainSelector) {
return {navLink,headingButton};
}
/** Hover colors of the first non-current WORKSPACE row, the Theme select and the ComfyUI link. */
async function hoversOf(page) {
return {
navRow:await hoverStateOf(page,page.locator('#primary-nav a:not([aria-current])').first()),
theme:await hoverStateOf(page,page.getByRole('combobox',{name:'Theme',exact:true})),
canvasLink:await hoverStateOf(page,page.locator('.topbar .canvas-link')),
};
}
/** A viewport capture with the footer (and, at 1600, the sidebar's bottom group) in view. */
async function captureFooter(page,testInfo,name) {
await page.locator('.app-footer').scrollIntoViewIfNeeded();
await page.evaluate(()=>new Promise(resolve=>requestAnimationFrame(()=>requestAnimationFrame(resolve))));
await captureCanonicalState(page,testInfo,name);
}
test.beforeEach(async({request})=>{await seedConcept(request);});
for(const [width,height] of SIZES)for(const theme of THEMES)test(`shell geometry, tokens and controls match the concept ${width}x${height} ${theme}`,async({page},testInfo)=>{
@@ -122,21 +152,32 @@ for(const [width,height] of SIZES)for(const theme of THEMES)test(`shell geometry
const reference=await inventoryOf(page,'#main');
if(motion==='no-preference')await captureCanonicalState(page,testInfo,`concept-shell-${route}-${width}-${theme}`);
const referenceRings=FULL_HEADING.has(route)?await ringsOf(page,'#main'):null;
const referenceHovers=await hoversOf(page);
if(motion==='no-preference')await captureFooter(page,testInfo,`concept-footer-${route}-${width}-${theme}`);
await openProduction(page,route,theme,motion);await page.mouse.move(0,0);
const actual=await inventoryOf(page,'#workspace-main');
if(motion==='no-preference')await captureCanonicalState(page,testInfo,`production-shell-${route}-${width}-${theme}`);
const actualRings=FULL_HEADING.has(route)?await ringsOf(page,'#workspace-main'):null;
result[`${motion} ${route}`]={breadcrumb,reference,actual,diff:compare(route,reference,actual),referenceRings,actualRings};
const actualHovers=await hoversOf(page);
if(motion==='no-preference')await captureFooter(page,testInfo,`production-footer-${route}-${width}-${theme}`);
result[`${motion} ${route}`]={route,breadcrumb,reference,actual,diff:compare(route,reference,actual),referenceRings,actualRings,
referenceHovers,actualHovers};
}
const path=testInfo.outputPath('shell-geometry.json');await writeFile(path,JSON.stringify(result,null,2));
await testInfo.attach('shell-geometry',{path,contentType:'application/json'});
for(const [key,{breadcrumb,reference,actual,diff,referenceRings,actualRings}] of Object.entries(result)) {
for(const [key,{route,breadcrumb,reference,actual,diff,referenceRings,actualRings,referenceHovers,actualHovers}] of Object.entries(result)) {
// The concept itself is what this comparison assumes: its sample counts and its shell controls.
expect.soft(reference.chrome.navs[0].rows.map(row=>[row.text,row.count?.text??null]),`${key} concept WORKSPACE rows`)
.toEqual([['Images','24'],['Prompt library','6'],['Tags & categories','15'],['Inspect a file',null]]);
expect.soft(reference.chrome.topActions.items.map(item=>item.className||item.tag),`${key} concept top actions`)
.toEqual(['concept-badge','button','theme-label','canvas-link']);
expect.soft(reference.chrome.breadcrumbs.parts.at(-1).text,`${key} concept breadcrumb`).toBe(breadcrumb);
expect.soft(reference.chrome.title,`${key} concept title`).toBe(`${breadcrumb} · Prompt Manager concept`);
expect.soft(actual.chrome.title,`${key} production title`).toBe(`${breadcrumb} · Prompt Manager`);
expect.soft(reference.heading.h1.text,`${key} concept page title`).toBe(route==='tags'?'Tags & categories':breadcrumb);
// The concept's own nav-row hover (style.css:183-186) is what production is held to.
expect.soft(referenceHovers.navRow,`${key} concept nav-row hover`).toMatchObject({hovered:true,color:reference.chrome.body.color});
expect.soft(actualHovers,`${key} hover states`).toEqual(referenceHovers);
expect.soft(diff,`${key} differences after the recorded deviations`).toEqual([]);
expect.soft(actual.noPageScroll,`${key} no page-level horizontal scroll`).toBe(true);
if(referenceRings) {
@@ -147,3 +188,33 @@ for(const [width,height] of SIZES)for(const theme of THEMES)test(`shell geometry
}
}
});
test('the inventory reports a missing shell element by its path instead of failing',async({page})=>{
await page.setViewportSize({width:1600,height:1100});
await openConcept(page,'tags','dark');await page.mouse.move(0,0);
const reference=mapShellConcept(await inventoryOf(page,'#main'));
await openProduction(page,'tags','dark');await page.mouse.move(0,0);
expect(diffInventory(reference.chrome,(await inventoryOf(page,'#workspace-main')).chrome)).toEqual([]);
await page.locator('.breadcrumbs').evaluate(node=>node.remove());
await page.locator('.connection small').evaluate(node=>node.remove());
const actual=await inventoryOf(page,'#workspace-main');
expect(actual.chrome.breadcrumbs).toBeNull();
expect(actual.chrome.connection.smallFont).toBeNull();
const paths=diffInventory(reference.chrome,actual.chrome).map(item=>item.path);
expect(paths).toContain('breadcrumbs');
expect(paths).toContain('connection.smallFont');
expect(paths).toContain('connection.small');
});
test('the concept fixture renders the concept inspector facts',async({page})=>{
await page.setViewportSize({width:1600,height:1100});
await openProduction(page,'images','dark');
// The first image is the concept's first sample, selected into the panel on first load (D19).
const facts=page.locator('#workspace-main .generation-facts');
await expect(facts).toBeVisible();
const pairs=await facts.evaluate(list=>Object.fromEntries([...list.querySelectorAll('dt')].map(term=>[term.textContent.trim(),term.nextElementSibling.textContent.trim()])));
// app.js:404-408: the concept's facts for alpine_00001.png.
expect(pairs).toEqual({Seed:'42001',Steps:'28',CFG:'6.5',Sampler:'dpmpp_2m',Scheduler:'karras',Model:'illustrative-model.safetensors'});
// D21 maps execution evidence to "Execution recorded" in PR D; today's panel names the same source this way.
await expect(page.locator('#workspace-main').getByText('Execution prompt evidence',{exact:true})).toBeVisible();
});
+1 -1
View File
@@ -21,7 +21,7 @@
<a class="skip" href="#workspace-main">Skip to workspace</a>
<div class="app-shell">
<aside class="sidebar">
<a class="brand" href="#/images"><span class="brand-mark" aria-hidden="true">P<span>±</span></span><span>Prompt Manager<small>COMFYASSETS</small></span></a>
<a class="brand" href="#/images"><span class="brand-mark">P<span>±</span></span><span>Prompt Manager<small>COMFYASSETS</small></span></a>
<div class="sidebar-label">WORKSPACE</div>
<nav id="primary-nav" aria-label="Workspace"></nav>
<div class="sidebar-bottom">