diff --git a/demo/golden/device-battery.mjs b/demo/golden/device-battery.mjs index 0b676aa3..29a816d4 100644 --- a/demo/golden/device-battery.mjs +++ b/demo/golden/device-battery.mjs @@ -206,3 +206,185 @@ export async function inspectBatteryBoardPixels(page, png, clip, board) { if (failures.length) throw new Error(`semantic battery golden ink missing: ${JSON.stringify(failures)}`); return samples; } + +export const BATTERY_ZIGBEE_MARKER = 'battery-56-normal'; + +/** Same natural unplaced-parent arrangement as smoke_device_battery_zigbee. */ +export function makeBatteryZigbeeFixture() { + const fixture = makeBatteryBoardFixture('desktop'); + fixture.config.markers = fixture.config.markers.filter((marker) => marker.id === BATTERY_ZIGBEE_MARKER); + fixture.config.markers[0].size = 1.4; + fixture.config.settings = { ...fixture.config.settings, volumetric_view: false, + show_device_battery: true, moon_enabled: false, zigbee_topology: { enabled: true }, + marker_area_snapshot: { [BATTERY_ZIGBEE_MARKER]: fixture.config.settings.marker_area_snapshot[BATTERY_ZIGBEE_MARKER] }, + }; + fixture.devices = { [BATTERY_ZIGBEE_MARKER]: fixture.devices[BATTERY_ZIGBEE_MARKER] }; + fixture.entities = Object.fromEntries(Object.entries(fixture.entities) + .filter(([, entity]) => entity.device_id === BATTERY_ZIGBEE_MARKER)); + fixture.states = Object.fromEntries(Object.entries(fixture.states).filter(([id]) => fixture.entities[id])); + fixture.layout = { [BATTERY_ZIGBEE_MARKER]: { s: SPACE, x: 0.34, y: 0.34 } }; + fixture.zhaDevices = [ + { ieee: '00124b0000000001', nwk: 1, device_reg_id: BATTERY_ZIGBEE_MARKER, device_type: 'EndDevice', + neighbors: [{ ieee: '00124b0000000002', relationship: 'Parent', lqi: 50 }] }, + { ieee: '00124b0000000002', nwk: 2, device_reg_id: 'not_on_plan', device_type: 'Router', + name: 'Upstairs parent relay', + neighbors: [{ ieee: '00124b0000000001', relationship: 'Child', lqi: 50 }] }, + ]; + return fixture; +} + +export async function batteryZigbeeProbe(page) { + return page.evaluate((id) => { + const root = window.__goldenCard.renderRoot; + const marker = root.querySelector(`[data-hp="device"][data-id="${CSS.escape(id)}"]`); + const overlay = root.querySelector('hp-zigbee-topology-overlay')?.shadowRoot; + const rect = (node) => { + if (!node) throw new Error('battery Zigbee golden missing rendered geometry'); + const r = node.getBoundingClientRect(); + return { x: r.x, y: r.y, right: r.right, bottom: r.bottom, width: r.width, height: r.height }; + }; + const battery = rect(marker?.querySelector('.device-battery')); + const caption = rect(overlay?.querySelector('[data-hp="zigbee-topology-parent-bubble"]')); + const core = rect(marker?.querySelector('.device-core')); + return { battery, caption, core, overlap: { + x: Math.max(battery.x, caption.x), y: Math.max(battery.y, caption.y), + right: Math.min(battery.right, caption.right), bottom: Math.min(battery.bottom, caption.bottom), + } }; + }, BATTERY_ZIGBEE_MARKER); +} + +/** Fixed, core-anchored focus: tiny layering changes cannot hide in a full page. */ +export function batteryZigbeeClip(probe, viewport) { + const clip = { x: Math.floor(probe.core.x - 16), + y: Math.floor(probe.core.y + probe.core.height / 2 - 70), width: 420, height: 140 }; + if (clip.x < 0 || clip.y < 0 || clip.x + clip.width > viewport.width + || clip.y + clip.height > viewport.height || [probe.core, probe.battery, probe.caption].some((rect) => + rect.x < clip.x || rect.y < clip.y || rect.right > clip.x + clip.width || rect.bottom > clip.y + clip.height)) + throw new Error(`battery Zigbee golden focused clip truncates the witness: ${JSON.stringify({ clip, probe, viewport })}`); + return clip; +} + +/** Executable pixel oracle shared with unit negative cases, not a z-index check. */ +export function inspectBatteryZigbeePixels(images, probe, clip) { + if (probe.overlap.right - probe.overlap.x < 4 || probe.overlap.bottom - probe.overlap.y < 6) + throw new Error('battery Zigbee golden caption does not overlap battery'); + const at = (image, x, y) => { + const ix = Math.floor(x - clip.x), iy = Math.floor(y - clip.y); + if (ix < 0 || iy < 0 || ix >= image.width || iy >= image.height) + throw new Error('battery Zigbee golden pixel outside capture'); + const index = (iy * image.width + ix) * 4; + return image.data.slice(index, index + 3); + }; + const changed = (a, b) => a.some((value, index) => Math.abs(value - b[index]) > 12); + const green = (pixel) => COLORS.normal.every((value, index) => Math.abs(value - pixel[index]) < 8); + let ink = 0, covered = 0, coreChanged = 0, exposedRoute = 0; + const centreY = probe.caption.y + probe.caption.height / 2; + for (let y = Math.ceil(Math.max(probe.overlap.y + 2, centreY - 5)); + y < Math.min(probe.overlap.bottom - 2, centreY + 5); y++) { + for (let x = Math.ceil(probe.overlap.x + 2); x < probe.overlap.right - 2; x++) { + const baseline = at(images.captionHidden, x, y), painted = at(images.active, x, y); + if (!green(baseline)) continue; + ink++; + if (changed(baseline, painted) && painted[1] - Math.max(painted[0], painted[2]) < 50) covered++; + } + } + const cx = probe.core.x + probe.core.width / 2, cy = probe.core.y + probe.core.height / 2; + for (let x = Math.ceil(cx); x < probe.caption.x - 3; x++) { + for (let dy = -2; dy <= 2; dy++) { + if (!changed(at(images.active, x, cy + dy), at(images.routesHidden, x, cy + dy))) continue; + if (x < probe.core.right - 5) coreChanged++; + else if (x > probe.core.right + 3) exposedRoute++; + } + } + const evidence = { ink, covered, coreChanged, exposedRoute }; + // Keep the smoke's differential thresholds; the focused zero-diff golden + // additionally catches a one-pixel edge leak that a ratio alone could miss. + if (ink < 3 || covered / ink < 0.9) + throw new Error(`battery Zigbee golden caption does not cover real battery ink: ${JSON.stringify(evidence)}`); + if (coreChanged > 2 || exposedRoute < 3) + throw new Error(`battery Zigbee golden route/core paint order is missing: ${JSON.stringify(evidence)}`); + return evidence; +} + +export async function prepareBatteryZigbeeOverlap(page) { + // Open/refresh/close the real General Settings UI; __hpTest still points at + // the old demo card after golden remounts and must not seed this scene. + await page.evaluate(() => window.__goldenCard.renderRoot.querySelector('[data-hp="settings"]').click()); + await page.waitForFunction(() => [...(window.__goldenCard.renderRoot + .querySelector('hp-zigbee-topology-settings')?.shadowRoot?.querySelectorAll('button') || [])] + .some((button) => button.querySelector('ha-icon[icon="mdi:access-point-network"]') && !button.disabled)); + await page.evaluate(() => [...window.__goldenCard.renderRoot.querySelector('hp-zigbee-topology-settings') + .shadowRoot.querySelectorAll('button')] + .find((button) => button.querySelector('ha-icon[icon="mdi:access-point-network"]')).click()); + await page.waitForFunction(() => /Received/i.test(window.__goldenCard.renderRoot + .querySelector('hp-zigbee-topology-settings')?.shadowRoot?.textContent || '')); + await page.evaluate(() => window.__goldenCard.renderRoot.querySelector('hp-dialog [data-hp="dialog-cancel"]').click()); + await page.waitForFunction(() => !window.__goldenCard.renderRoot.querySelector('hp-dialog')); + const face = await page.evaluate(async (id) => { + const marker = window.__goldenCard.renderRoot.querySelector(`[data-hp="device"][data-id="${CSS.escape(id)}"]`); + const battery = marker?.querySelector('.device-battery'); + const icon = battery?.querySelector('ha-icon.device-battery-icon'); + await icon?.updateComplete; + const svg = icon?.shadowRoot?.querySelector('svg'), path = svg?.querySelector('path'); + const rect = battery?.getBoundingClientRect(), core = marker?.querySelector('.device-core')?.getBoundingClientRect(); + if (battery?.dataset.state !== 'normal' || icon?.icon !== 'mdi:battery' + || !path || path.getAttribute('d') !== window.__ICONS?.['mdi:battery'] + || !(path.getBBox().width > 0) || !(path.getBBox().height > 0) + || svg.getAttribute('viewBox') !== '0 0 24 24' + || !(rect.width > 0) || Math.abs(svg.getBoundingClientRect().width - rect.width) > 0.5 + || Math.abs(svg.getBoundingClientRect().height - rect.height) > 0.5) + throw new Error('battery Zigbee golden lacks a full-size official MDI battery'); + return { x: core.x + core.width / 2, y: core.y + core.height / 2, + battery: { id, state: 'normal', color: [29, 194, 29], x: rect.x, y: rect.y, width: rect.width, height: rect.height } }; + }, BATTERY_ZIGBEE_MARKER); + // Before hover the very same MDI must paint. Empty glyphs cannot make the + // subsequent caption control vacuously green. + await inspectBatteryBoardPixels(page, await page.screenshot({ scale: 'css', animations: 'disabled' }), + null, { samples: [face.battery] }); + await page.mouse.move(face.x, face.y); + await page.waitForFunction(() => window.__goldenCard.renderRoot.querySelector('hp-zigbee-topology-overlay') + ?.shadowRoot?.querySelector('[data-hp="zigbee-topology-parent-bubble"]')?.getBoundingClientRect().width > 0); + await page.evaluate(() => { + const caption = window.__goldenCard.renderRoot.querySelector('hp-zigbee-topology-overlay').shadowRoot + .querySelector('[data-hp="zigbee-topology-parent-bubble"]'); + if (!caption.textContent.includes('Upstairs parent relay')) + throw new Error('battery Zigbee golden expected real parent caption text'); + }); + await page.evaluate(async () => { + await window.__goldenCard.updateComplete; + await new Promise((done) => requestAnimationFrame(() => requestAnimationFrame(done))); + }); + const probe = await batteryZigbeeProbe(page), clip = batteryZigbeeClip(probe, page.viewportSize()); + const screenshot = () => page.screenshot({ clip, scale: 'css', animations: 'disabled', caret: 'hide' }); + const controls = async (hidden) => page.evaluate((hidden) => { + const overlay = window.__goldenCard.renderRoot.querySelector('hp-zigbee-topology-overlay').shadowRoot; + for (const [name, selector] of [['caption', 'zigbee-topology-parent-bubble'], ['routes', 'zigbee-topology-lines']]) { + const node = overlay.querySelector(`[data-hp="${selector}"]`); + if (!node) throw new Error(`battery Zigbee golden missing ${name}`); + if (hidden === name) node.style.visibility = 'hidden'; + else node.style.removeProperty('visibility'); + } + }, hidden); + let active, captionHidden, routesHidden; + try { + active = await screenshot(); + await controls('caption'); + captionHidden = await screenshot(); + await controls('routes'); + routesHidden = await screenshot(); + } finally { + await controls(null); + } + const images = await page.evaluate(async (encoded) => Object.fromEntries(await Promise.all( + Object.entries(encoded).map(async ([name, base64]) => { + const bytes = Uint8Array.from(atob(base64), (char) => char.charCodeAt(0)); + const bitmap = await createImageBitmap(new Blob([bytes], { type: 'image/png' })); + const canvas = new OffscreenCanvas(bitmap.width, bitmap.height), context = canvas.getContext('2d'); + context.drawImage(bitmap, 0, 0); + const image = context.getImageData(0, 0, bitmap.width, bitmap.height); + bitmap.close(); + return [name, { width: image.width, height: image.height, data: Array.from(image.data) }]; + }))), { active: active.toString('base64'), captionHidden: captionHidden.toString('base64'), + routesHidden: routesHidden.toString('base64') }); + return { ...inspectBatteryZigbeePixels(images, probe, clip), clip, overlap: probe.overlap }; +} diff --git a/demo/golden/harness.mjs b/demo/golden/harness.mjs index 3abf5750..4c234bcd 100644 --- a/demo/golden/harness.mjs +++ b/demo/golden/harness.mjs @@ -1,7 +1,8 @@ import { makeLargeHouseFixture } from '../fixtures/large-house.mjs'; import { fixtureWallKey, makeVisualMatrixFixture } from '../fixtures/visual-matrix.mjs'; import { readFileSync } from 'node:fs'; -import { makeBatteryBoardFixture, prepareBatteryBoard } from './device-battery.mjs'; +import { makeBatteryBoardFixture, prepareBatteryBoard, makeBatteryZigbeeFixture, + prepareBatteryZigbeeOverlap, batteryZigbeeProbe, batteryZigbeeClip } from './device-battery.mjs'; const junctionArtifactsFixture = JSON.parse(readFileSync( new URL('../../test/fixtures/302-junction-artifacts.json', import.meta.url), 'utf8', @@ -43,6 +44,7 @@ const cardVersion = '0.0.0-golden'; const VERSION_RELOAD_ATTEMPT_KEY = 'houseplan-card:version-reload-target:v1'; const fixtureFor = (scenario) => scenario.batteryBoard ? makeBatteryBoardFixture(scenario.batteryBoard) + : scenario.batteryZigbeeOverlap ? makeBatteryZigbeeFixture() : scenario.fixture === 'large' ? makeLargeHouseFixture({ includeStairs: false }) : makeVisualMatrixFixture({ @@ -1013,6 +1015,7 @@ export async function prepareGoldenScenario(page, scenario) { three: { floor_id: 'three', name: 'Three', level: 2 }, }, callWS: async (message) => { + if (message.type === 'zha/devices' && fixture.zhaDevices) return structuredClone(fixture.zhaDevices); if (message.type === 'houseplan/config/get') return { config: structuredClone(fixture.config), rev: 1, can_write: scenario.canWrite !== false, @@ -2494,11 +2497,13 @@ export async function prepareGoldenScenario(page, scenario) { await page.mouse.move(point.x, point.y); } if (scenario.batteryBoard) result.batteryBoard = await prepareBatteryBoard(page, scenario); + if (scenario.batteryZigbeeOverlap) result.batteryZigbeeOverlap = await prepareBatteryZigbeeOverlap(page); return result; } export async function goldenClip(page, capture) { if (capture === 'page') return null; + if (capture === 'battery-zigbee-overlap') return batteryZigbeeClip(await batteryZigbeeProbe(page), page.viewportSize()); return page.evaluate((captureKind) => { const card = window.__goldenCard; const target = card?.renderRoot?.querySelector('.stage'); diff --git a/demo/golden/matrix.mjs b/demo/golden/matrix.mjs index 5866c9a7..16b7bde9 100644 --- a/demo/golden/matrix.mjs +++ b/demo/golden/matrix.mjs @@ -1,7 +1,7 @@ import { fixtureWallKey } from '../fixtures/visual-matrix.mjs'; /** Data-only HP-QA-01 capture matrix. Bump when framing or scenarios change. */ -export const GOLDEN_MATRIX_VERSION = 72; +export const GOLDEN_MATRIX_VERSION = 73; const stage = { capture: 'stage', threshold: { maxChannelDelta: 10, maxDiffRatio: 0.0005 } }; const page = { capture: 'page', threshold: { maxChannelDelta: 10, maxDiffRatio: 0.0008 } }; @@ -897,6 +897,11 @@ export const GOLDEN_SCENARIOS = Object.freeze([ })), { id: 'device-battery-mobile-dark', fixture: 'visual', space: 'golden-battery', mode: 'view', batteryBoard: 'mobile', theme: 'dark', viewport: { width: 390, height: 844 }, ...stage }, + // Real ZHA refresh + hover; differential raster controls prove natural + // caption/battery overlap. A focused zero-diff frame protects 1px edges. + { id: 'device-battery-zigbee-overlap-dark', fixture: 'visual', space: 'golden-battery', mode: 'view', + batteryZigbeeOverlap: true, iconSize: 5, theme: 'dark', viewport: { width: 1200, height: 900 }, + capture: 'battery-zigbee-overlap', threshold: { maxChannelDelta: 10, maxDiffRatio: 0 } }, { id: 'device-value-face-cover-source-dark', fixture: 'visual', space: 'golden-lighting', mode: 'view', glowEnabled: false, sunRays: false, showNames: false, diff --git a/docs/USER-GUIDE.md b/docs/USER-GUIDE.md index f83afc85..59342d02 100644 --- a/docs/USER-GUIDE.md +++ b/docs/USER-GUIDE.md @@ -1429,7 +1429,7 @@ have no indicator. The normal vacuum dock marker follows the ordinary rules. The indicator does not enlarge the device's click target; Zigbee hover information appears above it. -**General settings → Display → Show device battery charge** is on by default +**General settings → Display → Show device battery status** is on by default and applies installation-wide, including static space cards and the editor preview. Full configuration export/import preserves the setting. diff --git a/test/golden-battery.test.mjs b/test/golden-battery.test.mjs index 574e69a2..966f4ff7 100644 --- a/test/golden-battery.test.mjs +++ b/test/golden-battery.test.mjs @@ -1,6 +1,8 @@ import assert from 'node:assert/strict'; import test from 'node:test'; -import { BATTERY_BOARD_ICONS, batteryBoardSamples, makeBatteryBoardFixture } from '../demo/golden/device-battery.mjs'; +import { BATTERY_BOARD_ICONS, batteryBoardSamples, makeBatteryBoardFixture, + BATTERY_ZIGBEE_MARKER, makeBatteryZigbeeFixture, batteryZigbeeClip, + inspectBatteryZigbeePixels } from '../demo/golden/device-battery.mjs'; import { prepareGoldenFixture } from '../demo/golden/harness.mjs'; import { GOLDEN_SCENARIOS } from '../demo/golden/matrix.mjs'; @@ -61,3 +63,91 @@ test('#792 golden fixtures are fresh independent public config/HA data, without assert.ok(!Object.keys(existing.entities).some((entityId) => entityId.includes('battery_32_'))); assert.throws(() => batteryBoardSamples('unlisted'), /unknown battery board/); }); + +test('#792 fourth battery golden uses a real enabled ZHA parent and a focused zero-diff capture', () => { + const scenes = GOLDEN_SCENARIOS.filter((scenario) => scenario.batteryZigbeeOverlap); + assert.equal(scenes.length, 1); + const [scene] = scenes; + assert.equal(scene.id, 'device-battery-zigbee-overlap-dark'); + assert.equal(scene.mode, 'view'); + assert.equal(scene.theme, 'dark'); + assert.equal(scene.capture, 'battery-zigbee-overlap'); + assert.deepEqual(scene.threshold, { maxChannelDelta: 10, maxDiffRatio: 0 }); + const fixture = prepareGoldenFixture(scene); + assert.equal(fixture.config.settings.zigbee_topology.enabled, true); + assert.equal(fixture.config.settings.show_device_battery, true); + assert.equal(fixture.config.markers.length, 1); + assert.equal(fixture.config.markers[0].binding, `device:${BATTERY_ZIGBEE_MARKER}`); + assert.deepEqual(fixture.layout, { [BATTERY_ZIGBEE_MARKER]: { s: scene.space, x: 0.34, y: 0.34 } }); + assert.equal(Object.keys(fixture.entities).length, 2); // Measurement and its own registry battery only. + const batteryId = Object.keys(fixture.entities).find((id) => fixture.entities[id].original_device_class === 'battery'); + assert.equal(fixture.entities[batteryId].device_id, BATTERY_ZIGBEE_MARKER); + assert.equal(fixture.states[batteryId].state, '80'); + const [child, parent] = fixture.zhaDevices; + assert.equal(child.device_reg_id, BATTERY_ZIGBEE_MARKER); + assert.deepEqual(child.neighbors, [{ ieee: parent.ieee, relationship: 'Parent', lqi: 50 }]); + assert.deepEqual(parent.neighbors, [{ ieee: child.ieee, relationship: 'Child', lqi: 50 }]); + assert.equal(parent.name, 'Upstairs parent relay'); + assert.equal(fixture.devices[parent.device_reg_id], undefined); + assert.equal(fixture.layout[parent.device_reg_id], undefined); + fixture.zhaDevices[0].neighbors[0].lqi = 255; + assert.equal(makeBatteryZigbeeFixture().zhaDevices[0].neighbors[0].lqi, 50); + const oldScene = GOLDEN_SCENARIOS.find((scenario) => scenario.batteryBoard); + assert.equal(prepareGoldenFixture(oldScene).zhaDevices, undefined); +}); + +function pixelWitness() { + const rect = (x, y, width, height) => ({ x, y, width, height, right: x + width, bottom: y + height }); + const probe = { core: rect(4, 16, 20, 16), battery: rect(34, 14, 16, 20), caption: rect(36, 16, 24, 16), + overlap: { x: 36, y: 16, right: 50, bottom: 32 } }; + const image = () => ({ width: 64, height: 48, data: new Array(64 * 48 * 4).fill(32) }); + const images = { active: image(), captionHidden: image(), routesHidden: image() }; + const paint = (image, x, y, width, height, color) => { + for (let py = y; py < y + height; py++) for (let px = x; px < x + width; px++) { + const offset = (py * image.width + px) * 4; + image.data.splice(offset, 4, ...color, 255); + } + }; + paint(images.captionHidden, 34, 14, 16, 20, [29, 194, 29]); + paint(images.active, 28, 23, 5, 3, [220, 200, 50]); + return { images, probe, clip: { x: 0, y: 0 }, paint }; +} + +test('#792 Zigbee pixel oracle proves caption-over-battery and core-over-route from controls', () => { + const { images, probe, clip } = pixelWitness(); + assert.deepEqual(inspectBatteryZigbeePixels(images, probe, clip), + { ink: 100, covered: 100, coreChanged: 0, exposedRoute: 15 }); +}); + +test('#792 Zigbee pixel oracle rejects missing overlap, invisible MDI and reversed caption layer', () => { + const missing = pixelWitness(); + missing.probe.overlap.right = missing.probe.overlap.x; + assert.throws(() => inspectBatteryZigbeePixels(missing.images, missing.probe, missing.clip), /does not overlap/); + const empty = pixelWitness(); + empty.images.captionHidden = structuredClone(empty.images.active); + assert.throws(() => inspectBatteryZigbeePixels(empty.images, empty.probe, empty.clip), /real battery ink/); + const under = pixelWitness(); + under.paint(under.images.active, 34, 14, 16, 20, [29, 194, 29]); + assert.throws(() => inspectBatteryZigbeePixels(under.images, under.probe, under.clip), /real battery ink/); +}); + +test('#792 Zigbee pixel oracle rejects routes above core and an absent route control', () => { + const above = pixelWitness(); + above.paint(above.images.active, 14, 23, 3, 3, [220, 200, 50]); + assert.throws(() => inspectBatteryZigbeePixels(above.images, above.probe, above.clip), /route\/core paint order/); + const absent = pixelWitness(); + absent.images.routesHidden = structuredClone(absent.images.active); + assert.throws(() => inspectBatteryZigbeePixels(absent.images, absent.probe, absent.clip), /route\/core paint order/); +}); + +test('#792 Zigbee focus is fixed size, core-anchored and refuses a clipped witness', () => { + const { probe } = pixelWitness(); + const shifted = Object.fromEntries(Object.entries(probe).map(([key, rect]) => [key, { + ...rect, x: rect.x + 100, right: rect.right + 100, y: rect.y + 100, bottom: rect.bottom + 100, + }])); + assert.deepEqual(batteryZigbeeClip(shifted, { width: 1200, height: 900 }), + { x: 88, y: 54, width: 420, height: 140 }); + assert.throws(() => batteryZigbeeClip(probe, { width: 1200, height: 900 }), /truncates/); + shifted.caption.right = 540; + assert.throws(() => batteryZigbeeClip(shifted, { width: 1200, height: 900 }), /truncates/); +}); diff --git a/test/golden-matrix.test.mjs b/test/golden-matrix.test.mjs index e52df589..e5bf5e9c 100644 --- a/test/golden-matrix.test.mjs +++ b/test/golden-matrix.test.mjs @@ -15,7 +15,7 @@ test('golden matrix has stable unique ids and bounded comparison thresholds', () for (const scenario of GOLDEN_SCENARIOS) { assert.match(scenario.id, /^[a-z0-9-]+$/); assert.equal(['visual', 'large'].includes(scenario.fixture), true, scenario.id); - assert.equal(['page', 'stage', 'sun-window'].includes(scenario.capture), true, scenario.id); + assert.equal(['page', 'stage', 'sun-window', 'battery-zigbee-overlap'].includes(scenario.capture), true, scenario.id); assert.equal(scenario.viewport.width > 0 && scenario.viewport.height > 0, true, scenario.id); assert.equal(scenario.threshold.maxChannelDelta >= 0 && scenario.threshold.maxChannelDelta <= 32, true, scenario.id); assert.equal(scenario.threshold.maxDiffRatio >= 0 && scenario.threshold.maxDiffRatio <= 0.01, true, scenario.id); @@ -446,7 +446,7 @@ test('sun-ray golden requires browser-painted light from a state-only sun entity assert.ok(scenario); const fixture = prepareGoldenFixture(scenario); const space = fixture.config.spaces.find((item) => item.id === scenario.space); - assert.equal(GOLDEN_MATRIX_VERSION, 72); + assert.equal(GOLDEN_MATRIX_VERSION, 73); assert.equal(space.settings.sun_rays, true); assert.equal(scenario.northDeg, 90, 'the sign-sensitive golden must keep a non-zero north direction'); @@ -940,7 +940,7 @@ test('issue 570 Stage 4 reuses the historical iso goldens for visual handoff cov test('#673 Stage 6 designer acceptance scenes are canonical golden entries', () => { const ids = STAGE6_ACCEPTANCE_SCENARIOS.map((scenario) => scenario.id); - assert.equal(GOLDEN_MATRIX_VERSION, 72); + assert.equal(GOLDEN_MATRIX_VERSION, 73); assert.deepEqual(ids, [ 'isometric-stage6-light-lightfloor', 'isometric-stage6-light-darkfloor',