From 753118380e47f9cc1056e07e40d17d71d6a5f682 Mon Sep 17 00:00:00 2001 From: Vito Sansevero Date: Tue, 27 Oct 2026 19:07:34 -0700 Subject: [PATCH] test(images): compare the category chip's visible origin against the concept (#332) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit D146: the inspector comparison measures each chip's span.chip-source and lays the concept's own chip out with the recorded rule and origin added at runtime (withChipSource), in the side panel and the viewer's details. The D39 recipe case asserts the visible " · prompt"; the mutation check removes the origin. --- tests/browser/concept-inventory.mjs | 32 ++++++++++++++++++++++- tests/browser/inspector-concept.spec.mjs | 33 +++++++++++++++++++----- 2 files changed, 58 insertions(+), 7 deletions(-) diff --git a/tests/browser/concept-inventory.mjs b/tests/browser/concept-inventory.mjs index d04d2fd3..3a3ed3ee 100644 --- a/tests/browser/concept-inventory.mjs +++ b/tests/browser/concept-inventory.mjs @@ -187,6 +187,33 @@ export async function mutate(page,{css=null,remove=[],clone=[]}={}) { },token); } +/** + * #332 D146: the category chip names its origin after the category, in a muted `span.chip-source` that never splits + * (workspace.css `.chip .chip-source`). The concept's chip shows the bare name (app.js a402), so the recorded rule and + * the span are added to the concept page at runtime and the browser lays the chip out: the first `.tags` row's + * non-badge chip under `root`. `category` also replaces the chip's text (an own category the concept never shows). + * Returns the undo. + */ +export const D146_CHIP_SOURCE='.chip .chip-source { color: var(--ca-muted); white-space: nowrap; }'; +export async function withChipSource(page,root,origin,category=null) { + const restoreRule=await mutate(page,{css:D146_CHIP_SOURCE}); + const token=await page.evaluate(({root,origin,category})=>{ + const chip=document.querySelector(root)?.querySelector('.tags')?.querySelector(':scope > .chip:not(.blue)'); + if(!chip)throw new Error(`withChipSource: no category chip under ${root}`); + const text=chip.textContent,source=document.createElement('span'); + source.className='chip-source';source.textContent=` · ${origin}`; + if(category!==null)chip.textContent=category; + chip.append(source); + const token=`chip-source-${Math.random().toString(36).slice(2)}`; + (window.__chipSources??=new Map()).set(token,()=>{chip.textContent=text;}); + return token; + },{root,origin,category}); + return async()=>{ + await page.evaluate(token=>{window.__chipSources.get(token)();window.__chipSources.delete(token);},token); + await restoreRule(); + }; +} + /** * Inventory sections: {chrome, heading, toolbar, filterRow, bulkbar, caption, grid, cards, continuation, panel, library, * history, noPageScroll}. A section is null when its element is absent. The Images sections (toolbar … panel, #249 C7) @@ -615,9 +642,12 @@ export function measureInspector(selector) { primary:node.classList.contains('ca-primary'),...box(node),...font(node),...edges(node),...layout(node), minHeight:style.minHeight,minWidth:style.minWidth,icon:iconOf(node)}; } + // #332 D146: a category chip's visible origin (`span.chip-source`), null on every other chip. function chipOf(node) { + const source=node.querySelector(':scope > .chip-source'); return {tag:node.tagName.toLowerCase(),className:classes(node),text:textOf(node),hidden:hiddenOf(node),...box(node),...font(node),...edges(node), - minHeight:getComputedStyle(node).minHeight}; + minHeight:getComputedStyle(node).minHeight, + source:when(source,()=>({text:source.textContent,...font(source),...css(source,'whiteSpace'),...box(source)}))}; } function rowOf(row) { return {...layout(row),...edges(row),...box(row),chips:[...row.children].map(chipOf)}; diff --git a/tests/browser/inspector-concept.spec.mjs b/tests/browser/inspector-concept.spec.mjs index 0a43d7f1..4244166d 100644 --- a/tests/browser/inspector-concept.spec.mjs +++ b/tests/browser/inspector-concept.spec.mjs @@ -1,7 +1,7 @@ import {writeFile} from 'node:fs/promises'; import {test,expect} from './fixtures.mjs'; import {captureCanonicalState} from './history-canonical-comparison.mjs'; -import {SIZES,THEMES,seedConcept,openConcept,openProduction,inspectorOf,diffInventory,mutate,countCopy,saveImageTags} from './concept-inventory.mjs'; +import {SIZES,THEMES,seedConcept,openConcept,openProduction,inspectorOf,diffInventory,mutate,countCopy,saveImageTags,withChipSource} from './concept-inventory.mjs'; // Task D4 of #249: the Images side panel (app.js imageDetails a400–414) and the viewer's full details (imageDetails(i, // true)) against the immutable concept, on the concept-shaped real fixture, at 1600x1100 and 390x844 in both themes. @@ -13,6 +13,8 @@ test.use({locale:'en-US',timezoneId:'UTC'}); const CARDS='.gallery-body > .image-grid > .media-card'; const TITLE='Alpine observatory'; +// D146 (#332): alpine_00001.png's recipe has no category, so its chip shows the positive's, " · prompt" (D39). +const ORIGIN='prompt'; const FIXTURE_IMAGES=24; // The concept's sticky panel never scrolls; production's scrolls inside the window above 700px (D20/D21 side panel). const PANEL_INSET=40; @@ -38,14 +40,18 @@ function withFocusablePrompts(inventory) { return {...inventory,controls,sections:inventory.sections.map(section=>({...section,text:{...section.text,tabIndex:0}}))}; } -/** D21: the category chip names its scope in a visually hidden prefix; here the linked positive's (D39 fallback). */ +/** + * D21: the category chip names its scope in a visually hidden prefix; here the linked positive's (D39 fallback). Its + * visible origin (D146, #332: " · prompt") is the concept page's own chip with `span.chip-source` added at runtime + * (`withChipSource`), so `reference.panel` and `viewerPlain` already carry it. + */ function withCategoryScope(inventory) { const [badge,category,...rest]=inventory.badges.chips; return {...inventory,badges:{...inventory.badges,chips:[badge,{...category,hidden:'Positive prompt category: '},...rest]}}; } /** - * Recorded inspector deviations only (spec §8, TESTING.md D3, D20–D22, D34, D38 and D117); everything else must + * Recorded inspector deviations only (spec §8, TESTING.md D3, D20–D22, D34, D38, D117 and D146); everything else must * equal production. `reference.viewerPlain` is the concept's own full details * with its tag chips made plain (D20), D117's summary button added and, under a coarse pointer, D34's targets, all at * runtime: the browser lays out what follows them, as the Images spec's removed toolbar items do. @@ -112,6 +118,7 @@ async function openProductionPanel(page,theme) { */ async function plainViewerChips(page) { const restoreTargets=await mutate(page,{css:D34_COARSE_TARGETS}); + const restoreSource=await withChipSource(page,'#viewer-details',ORIGIN); await page.evaluate(()=>{ window.__plainChips=[...document.querySelectorAll('#viewer-details .tags > button.chip')].map(button=>{ const span=document.createElement('span');span.className='chip';span.textContent=button.textContent; @@ -126,7 +133,7 @@ async function plainViewerChips(page) { return async()=>{ await page.evaluate(()=>{for(const [span,button] of window.__plainChips)span.replaceWith(button);delete window.__plainChips; window.__summaryButton.remove();delete window.__summaryButton;}); - await restoreTargets(); + await restoreSource();await restoreTargets(); }; } @@ -162,7 +169,11 @@ async function viewerOf(page,concept,mutation=null) { return {viewerDetails,viewerPlain,fileControls}; } async function inventory(page,concept=false) { + // D146: the concept panel's category chip with its visible origin, laid out by the concept page itself. + const restoreSource=concept?await withChipSource(page,'#image-inspector',ORIGIN):null; + if(concept)await settle(page); const {panel}=await inspectorOf(page); + await restoreSource?.(); const {viewerDetails,viewerPlain,fileControls}=await viewerOf(page,concept); return concept?{panel,viewerDetails,viewerPlain,fileControls}:{panel,viewerDetails,fileControls}; } @@ -287,7 +298,8 @@ for(const [width,height] of SIZES)for(const theme of THEMES)test(`inspector pane expect.soft(panel.sections.map(section=>section.label.color)[0],'concept role colors').not.toBe(panel.sections[1].label.color); expect.soft(panel.facts,'concept facts').toMatchObject({columns:2,columnGap:'12px',rowGap:'12px',fontSize:width>700?'11px':'12px'}); expect.soft(panel.footer,'concept footer').toMatchObject({paddingTop:'13px',paddingRight:'17px',paddingBottom:'13px',paddingLeft:'17px'}); - expect.soft(panel.badges.chips.map(chip=>chip.text),'concept badges').toEqual(['Execution recorded','Landscapes']); + // The concept's bare "Landscapes" with the D146 origin added at runtime (`withChipSource`). + expect.soft(panel.badges.chips.map(chip=>chip.text),'concept badges').toEqual(['Execution recorded','Landscapes · prompt']); expect.soft(reference.viewerDetails.structure.inner.slice(-2),'concept full details').toEqual(['details','details']); expect.soft(diff,'differences after the recorded deviations').toEqual([]); @@ -321,8 +333,10 @@ test('a recipe-linked image without recipe metadata shows its positive prompt me await expect(panelOf(page).locator('.panel-heading h2')).toHaveText('Quiet geometry'); const rows=panelOf(page).locator('.detail-inner > .tags'); await expect(rows).toHaveCount(2); - await expect(rows.first().locator('.chip')).toHaveText(['Execution recorded','Positive prompt category: Architecture']); + await expect(rows.first().locator('.chip')).toHaveText(['Execution recorded','Positive prompt category: Architecture · prompt']); await expect(rows.first().locator('.chip .ca-visually-hidden')).toHaveText('Positive prompt category: '); + // D146 (#332): the visible origin names where the value came from. + await expect(rows.first().locator('.chip .chip-source')).toHaveText(' · prompt'); await expect(rows.nth(1).locator('button.chip')).toHaveText(['minimal','warm_light','editorial']); await expect(rows.nth(1).locator('span.chip')).toHaveCount(0); await expect(panelOf(page).locator('.detail-footer button')).toHaveText(['Send to ComfyUI','Prompt history']); @@ -380,6 +394,13 @@ test('inspector comparison detects mutations',async({page})=>{ expect(()=>assertInspectorMatch(reference,withoutSummary,viewport)).toThrow(/viewerDetails\.structure\.inner/); assertInspectorMatch(reference,await viewerMutated(),viewport); + // D146: the visible origin is part of the mapped concept, so losing it is a difference. + const restoreSource=await mutate(page,{remove:['#image-inspector .chip-source']}); + const sourceMutated=await measure(); + expect(()=>assertInspectorMatch(reference,sourceMutated,viewport)).toThrow(/panel\.badges\.chips\.1\.source/); + await restoreSource();await settle(page); + assertInspectorMatch(reference,await measure(),viewport); + await mutate(page,{remove:['#image-inspector > .detail-footer']}); const domMutated=await measure(); expect(()=>assertInspectorMatch(reference,domMutated,viewport)).toThrow(/panel\.footer/);