diff --git a/docs/visual-parity.md b/docs/visual-parity.md index 4b2173c..a573b2a 100644 --- a/docs/visual-parity.md +++ b/docs/visual-parity.md @@ -91,10 +91,10 @@ This baseline accepts neither icon presentation nor the overflow affordances and composition. Those remain part of the full navigation family and both application-adoption reviews. A matching text-tab measurement or passing functional/coverage check does not complete that family or the library. The first candidate exposed a different bottom track row and the native button label -0.796875px below the reference. Both were corrected in the shared renderer. Read-only decoding -of all16 final local samples finds zero changed pixels against their original references. -This is scoped to the pinned local Chromium/macOS captures; the hosted cross-platform gate -checks exact styles and geometry, rather than requiring platform-specific raster identity. +0.796875px below the reference. Both were corrected in the shared renderer. The compiled +catalog now checks exact agreement of every decoded pixel against the matching Chromium +reference on macOS and Linux. All 16 text-only candidate captures per platform, covering +link and panel navigation, must also match the measured styles and geometry. No full navigation or human UX acceptance is claimed. @@ -138,8 +138,9 @@ vertical bound. Original failure diagnostics remain evidence; no tolerance was i The compiled catalog asserts exact measured state/style values and less-than-one-pixel geometry, with a 0.1px vertical bound, against all three icon/text presentations. Original images are verified -by digest and dimensions before each comparison. Scoped raster inspection and current package -identities are recorded separately in the [implementation evidence](evidence/shared-ui-foundation.json). +by digest and dimensions before each comparison. The catalog also requires zero changed +pixels in the 48 icon/text captures per platform and records the pixel agreement for each +candidate screenshot. Custom font/theme breadth, overflow affordances, other component families and both application adoptions remain required under the unchanged 64 foundation and 6 delivery cases. diff --git a/tests/storybook/navigation-icons-visual.ts b/tests/storybook/navigation-icons-visual.ts index 8dc22ba..9024b31 100644 --- a/tests/storybook/navigation-icons-visual.ts +++ b/tests/storybook/navigation-icons-visual.ts @@ -1,6 +1,7 @@ import {captureSettledElement} from './screenshot.ts'; import {waitForCatalogStory} from './lifecycle.ts'; import {readOriginalRaster} from './reference-images.ts'; +import {compareVisualPng} from './raster-comparison.ts'; import assert from 'node:assert/strict'; import {readFile,writeFile} from 'node:fs/promises'; import {resolve} from 'node:path'; @@ -43,8 +44,11 @@ export async function exerciseNavigationIconsVisual(page:Page,origin:string,outp } if(row.state==='focus'&&n===0){assert.equal(item.focusVisible,true);assert.equal(item.base.outline,expected.base.outline);assert.equal(item.base.outlineOffset,expected.base.outlineOffset);} } - const raster=await captureSettledElement(page,'#icon-proof',resolve(output,'ha-icons-'+kind+'-'+theme+'-'+row.presentation+'-'+row.state+'.png')); - results.push({platform,kind,theme,presentation:row.presentation,state:row.state,geometryAndStateMatch:true,layoutToleranceCssPx:1,verticalToleranceCssPx:0.1,actual,referenceImage:row.image,raster}); + const path=resolve(output,`ha-icons-${kind}-${theme}-${row.presentation}-${row.state}.png`); + const raster=await captureSettledElement(page,'#icon-proof',path); + const {agreement}=await compareVisualPng(page,original,await readFile(path)); + assert.equal(agreement.changedPixels,0,`Original HA navigation icon pixels differ for ${platform} ${kind} ${theme} ${row.presentation} ${row.state}`); + results.push({platform,kind,theme,presentation:row.presentation,state:row.state,geometryAndStateMatch:true,layoutToleranceCssPx:1,verticalToleranceCssPx:0.1,actual,referenceImage:row.image,raster,rasterAgreement:agreement}); } } await writeFile(resolve(output,'ha-navigation-icons-parity.json'),JSON.stringify({reference:'docs/evidence/ha-navigation-icons-reference.json',scope:'Three icon/text presentations; overflow, other visual families and installed app/human acceptance remain pending.',results},null,2)+'\n');return results; diff --git a/tests/storybook/navigation-visual.ts b/tests/storybook/navigation-visual.ts index 4bc570d..f17ae03 100644 --- a/tests/storybook/navigation-visual.ts +++ b/tests/storybook/navigation-visual.ts @@ -1,6 +1,7 @@ import {captureSettledElement} from './screenshot.ts'; import {waitForCatalogStory} from './lifecycle.ts'; import {readOriginalRaster} from './reference-images.ts'; +import {compareVisualPng} from './raster-comparison.ts'; import assert from 'node:assert/strict'; import {readFile,writeFile} from 'node:fs/promises'; import {resolve} from 'node:path'; @@ -22,7 +23,7 @@ export async function exerciseNavigationVisual(page:Page,origin:string,output:st await page.evaluate(()=>{const holder=document.createElement('div');holder.id='navigation-proof';holder.style.cssText='position:fixed;left:32px;top:32px;width:600px;height:56px;overflow:hidden;z-index:9999';const host=document.createElement('vyp-ha-tabs');host.id='visual-navigation';holder.append(host);document.querySelector('main.catalog-frame')!.append(holder);}); const states:ReferenceState[]=reference.states.filter(row=>row.platform===platform&&row.theme===theme);assert.equal(states.length,4,'All measured platform states are required'); for(const row of states){ - const bytes=await readOriginalRaster(row.image);assert.equal(bytes.readUInt32BE(16),row.image.width);assert.equal(bytes.readUInt32BE(20),row.image.height); + const original=await readOriginalRaster(row.image);assert.equal(original.readUInt32BE(16),row.image.width);assert.equal(original.readUInt32BE(20),row.image.height); await page.locator('#visual-navigation').evaluate(async(el,{kind,state})=>{const host=el as HaTabs;host.state={kind,label:'Household sections',selected:'overview',items:['Overview','Tasks','Inventory'].map((label,n)=>({id:['overview','tasks','inventory'][n],label,href:kind==='navigation'?'#section-'+n:null,disabled:n===1&&state==='disabled'}))};await host.updateComplete;host.shadowRoot!.querySelector('[part=item]')!.blur();},{kind,state:row.state}); if(row.state==='hover')await page.locator('#visual-navigation a,#visual-navigation button').nth(1).hover();else await page.mouse.move(1100,800); if(row.state==='focus'){await page.locator('#visual-navigation a,#visual-navigation button').first().focus();await page.keyboard.press('Home');} @@ -33,8 +34,11 @@ export async function exerciseNavigationVisual(page:Page,origin:string,output:st for(const[key,value]of Object.entries(item.geometry))assert.ok(Math.abs(value-expected.geometry[key as keyof typeof expected.geometry])<(key==='textY'?0.1:1),kind+' '+theme+' '+row.state+' '+key); if(row.state==='focus'&&index===0){assert.equal(item.focusVisible,true);assert.equal(item.base.outline,expected.base.outline);assert.equal(item.base.outlineOffset,expected.base.outlineOffset);} } - const raster=await captureSettledElement(page,'#navigation-proof',resolve(output,'ha-navigation-'+kind+'-'+theme+'-'+row.state+'.png')); - results.push({platform,kind,theme,state:row.state,typographyStateAndGeometryMatch:true,layoutToleranceCssPx:1,textVerticalToleranceCssPx:0.1,actual,referenceImage:row.image,raster}); + const path=resolve(output,`ha-navigation-${kind}-${theme}-${row.state}.png`); + const raster=await captureSettledElement(page,'#navigation-proof',path); + const {agreement}=await compareVisualPng(page,original,await readFile(path)); + assert.equal(agreement.changedPixels,0,`Original HA navigation pixels differ for ${platform} ${kind} ${theme} ${row.state}`); + results.push({platform,kind,theme,state:row.state,typographyStateAndGeometryMatch:true,layoutToleranceCssPx:1,textVerticalToleranceCssPx:0.1,actual,referenceImage:row.image,raster,rasterAgreement:agreement}); } } await writeFile(resolve(output,'ha-navigation-parity.json'),JSON.stringify({reference:'docs/evidence/ha-navigation-reference.json',scope:'Measured text-only controls; icons, overflow affordances and installed-application/human acceptance remain required',results},null,2)+'\n');return results;