Files
houseplan-card/test/golden-matrix.test.mjs
T

1233 lines
64 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import assert from 'node:assert/strict';
import { readFileSync, readdirSync } from 'node:fs';
import test from 'node:test';
import { prepareGoldenFixture } from '../demo/golden/harness.mjs';
import {
GOLDEN_MATRIX_VERSION, GOLDEN_SCENARIOS, OPENING_SYMBOL_EXISTING_GOLDEN_IMPACT,
STAGE6_ACCEPTANCE_SCENARIOS,
} from '../demo/golden/matrix.mjs';
import { fixtureWallKey } from '../demo/fixtures/visual-matrix.mjs';
import { readHouseplanProductionSource } from './houseplan-source.mjs';
test('golden matrix has stable unique ids and bounded comparison thresholds', () => {
assert.equal(Number.isInteger(GOLDEN_MATRIX_VERSION) && GOLDEN_MATRIX_VERSION > 0, true);
assert.equal(new Set(GOLDEN_SCENARIOS.map((scenario) => scenario.id)).size, GOLDEN_SCENARIOS.length);
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(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);
if (scenario.warmPixelRegion) {
const region = scenario.warmPixelRegion;
assert.equal(region.x >= 0 && region.y >= 0 && region.w > 0 && region.h > 0, true, scenario.id);
assert.equal(region.x + region.w <= 1 && region.y + region.h <= 1, true, scenario.id);
assert.equal(Number.isInteger(region.minPixels) && region.minPixels > 0, true, scenario.id);
assert.equal(region.minRedBlueDelta > 0, true, scenario.id);
}
// #649: 2.5D is enabled by settings.volumetric_view, never by the alpha switch.
assert.equal('alpha' in scenario, false, scenario.id);
if (scenario.projection === 'iso') assert.equal(scenario.mode, 'view', scenario.id);
assert.equal('labs' in scenario, false, scenario.id);
assert.equal('testOnlyLabsSnapshot' in scenario, false, scenario.id);
if (scenario.sunRayPixels) {
assert.equal(Number.isInteger(scenario.sunRayPixels.minPixels)
&& scenario.sunRayPixels.minPixels > 0, true, scenario.id);
assert.equal(Number.isInteger(scenario.sunRayPixels.minChannelDelta)
&& scenario.sunRayPixels.minChannelDelta > 0
&& scenario.sunRayPixels.minChannelDelta <= 32, true, scenario.id);
}
if (typeof scenario.northDeg === 'number') {
assert.equal(Number.isInteger(scenario.northDeg)
&& scenario.northDeg >= 0 && scenario.northDeg < 360, true, scenario.id);
}
if (scenario.openingPreviewPixels) {
assert.ok(scenario.openingPreview, scenario.id);
assert.equal(Number.isInteger(scenario.openingPreviewPixels.minPixels)
&& scenario.openingPreviewPixels.minPixels > 0, true, scenario.id);
assert.equal(Number.isInteger(scenario.openingPreviewPixels.minChannelDelta)
&& scenario.openingPreviewPixels.minChannelDelta > 0
&& scenario.openingPreviewPixels.minChannelDelta <= 32, true, scenario.id);
assert.equal(Number.isInteger(scenario.openingPreviewPixels.minInsideWallPixels)
&& scenario.openingPreviewPixels.minInsideWallPixels > 0, true, scenario.id);
}
if (scenario.openingGeometry) {
assert.match(scenario.openingGeometry.id, /^[a-z0-9-]+$/, scenario.id);
assert.equal(['door', 'window', 'gate'].includes(scenario.openingGeometry.type), true, scenario.id);
assert.equal(Number.isFinite(scenario.openingGeometry.angle), true, scenario.id);
}
if (scenario.openingSymbolContract) {
const contract = scenario.openingSymbolContract;
assert.equal(['room', 'partition'].includes(contract.kind), true, scenario.id);
assert.equal(['flat', 'iso'].includes(contract.surface), true, scenario.id);
assert.equal(contract.wallCm > 0, true, scenario.id);
assert.equal(contract.openings.length >= 3, true, scenario.id);
assert.equal(new Set(contract.openings.map((opening) => opening.id)).size,
contract.openings.length, scenario.id);
for (const opening of contract.openings) {
assert.match(opening.id, /^[a-z0-9-]+$/, scenario.id);
assert.equal(['door', 'window', 'gate'].includes(opening.type), true, scenario.id);
const expectedOffset = contract.surface === 'iso' && opening.type !== 'window'
? 'selected-face'
: 'center';
assert.equal(opening.offset, expectedOffset, scenario.id);
assert.equal(typeof opening.flipV, 'boolean', scenario.id);
assert.equal(Number.isFinite(opening.at) && opening.length > 0, true, scenario.id);
}
assert.equal(contract.surface === 'iso', scenario.projection === 'iso', scenario.id);
}
if (scenario.tunnelContinuity) {
assert.match(scenario.tunnelContinuity.openingId, /^[a-z0-9-]+$/, scenario.id);
assert.equal(Number.isInteger(scenario.tunnelContinuity.insetPx)
&& scenario.tunnelContinuity.insetPx >= 1, true, scenario.id);
assert.equal(scenario.tunnelContinuity.maxChannelJump >= 0
&& scenario.tunnelContinuity.maxChannelJump <= 32, true, scenario.id);
assert.equal(typeof scenario.tunnelContinuity.dpr2, 'boolean', scenario.id);
}
if (scenario.decorPixelProbes) {
assert.match(scenario.decorPixelProbes.color, /^#[0-9a-f]{6}$/i, scenario.id);
assert.equal(Number.isInteger(scenario.decorPixelProbes.radius)
&& scenario.decorPixelProbes.radius >= 0, true, scenario.id);
assert.equal(scenario.decorPixelProbes.minMatchingFraction > 0
&& scenario.decorPixelProbes.minMatchingFraction <= 1, true, scenario.id);
assert.equal(scenario.decorPixelProbes.points.length >= 1, true, scenario.id);
for (const point of scenario.decorPixelProbes.points) {
assert.match(point.id, /^[a-z0-9-]+$/, scenario.id);
assert.equal(point.x >= 0 && point.x <= 1 && point.y >= 0 && point.y <= 1,
true, scenario.id);
}
}
if (scenario.decorOverride) {
assert.equal(new Set(scenario.decorOverride.map((shape) => shape.id)).size,
scenario.decorOverride.length, scenario.id);
// #606 deliberately isolates four furniture drawings; other decor
// override scenes remain the five-kind integration fixtures.
const expectedKinds = scenario.id === 'furniture-corrected-art-light'
? new Set(['furniture'])
: new Set(['line', 'rect', 'ellipse', 'text', 'furniture']);
assert.deepEqual(new Set(scenario.decorOverride.map((shape) => shape.kind)),
expectedKinds, scenario.id);
}
if (scenario.helpTextRegion) {
assert.match(scenario.helpTextRegion.key, /^[a-z0-9_.-]+\.help$/, scenario.id);
assert.equal(Number.isInteger(scenario.helpTextRegion.minPixels)
&& scenario.helpTextRegion.minPixels > 0, true, scenario.id);
assert.equal(scenario.openHelp, scenario.helpTextRegion.key, scenario.id);
}
if (scenario.planSnap) {
assert.equal(scenario.planSnap.tool, 'draw', scenario.id);
assert.equal(['endpoint', 'line'].includes(scenario.planSnap.expectedKind), true, scenario.id);
assert.equal(scenario.mode, 'plan', scenario.id);
assert.equal(scenario.capture, 'page', scenario.id);
}
if (scenario.roomLabelParity) {
assert.equal(scenario.space, 'golden-lighting', scenario.id);
assert.equal(['view', 'plan'].includes(scenario.mode), true, scenario.id);
assert.equal(scenario.capture, 'page', scenario.id);
}
if (scenario.wallJunctionPreview) {
assert.equal(scenario.wallJunctions, true, scenario.id);
assert.equal(scenario.mode, 'plan', scenario.id);
assert.equal(scenario.capture, 'page', scenario.id);
assert.ok(scenario.wallJunctionPreview.path.length >= 1, scenario.id);
}
if (scenario.tabDrag) {
assert.equal(['before', 'after'].includes(scenario.tabDrag), true, scenario.id);
assert.equal(scenario.applianceLifecycle, true, scenario.id);
assert.equal(scenario.mode, 'plan', scenario.id);
assert.equal(scenario.capture, 'page', scenario.id);
}
}
});
test('issue 486 golden matrix has exactly the three focused panel-host surfaces', () => {
const scenarios = GOLDEN_SCENARIOS.filter((scenario) => scenario.panelHost);
assert.deepEqual(scenarios.map((scenario) => scenario.id), [
'panel-wide-view-light-en',
'panel-wide-plan-editor-dark-en',
'panel-narrow-read-only-empty-light-en',
]);
assert.deepEqual(scenarios.map((scenario) => scenario.viewport), [
{ width: 1280, height: 800 },
{ width: 1280, height: 800 },
{ width: 320, height: 720 },
]);
assert.equal(scenarios.every((scenario) => scenario.capture === 'page'), true);
assert.deepEqual(scenarios.map((scenario) => scenario.mode), ['view', 'plan', 'view']);
const readOnly = scenarios.at(-1);
assert.equal(readOnly.emptyPlan, true);
assert.equal(readOnly.canWrite, false);
assert.equal(readOnly.userIsAdmin, false);
assert.equal(readOnly.preloadEditorRuntime, false);
const fixture = prepareGoldenFixture(readOnly);
assert.deepEqual(fixture.config.spaces, []);
assert.deepEqual(fixture.config.markers, []);
assert.deepEqual(fixture.layout, {});
});
test('issue 462 has reviewed desktop, narrow-touch and attempted-kiosk notices', () => {
const scenarios = GOLDEN_SCENARIOS.filter((scenario) =>
scenario.id.startsWith('version-mismatch-'));
assert.deepEqual(scenarios.map((scenario) => scenario.id), [
'version-mismatch-desktop-dark-en',
'version-mismatch-touch-light-ru',
'version-mismatch-kiosk-attempted-dark-de',
]);
assert.deepEqual(new Set(scenarios.map((scenario) => scenario.language)),
new Set(['en', 'ru', 'de']));
assert.equal(scenarios.every((scenario) => scenario.mode === 'view'
&& scenario.capture === 'page'
&& scenario.integrationVersion === '0.0.0-golden-backend'), true);
const touch = scenarios.find((scenario) => scenario.touchViewport);
assert.deepEqual(touch?.viewport, { width: 390, height: 760 });
const kiosk = scenarios.find((scenario) => scenario.versionRecoveryAttempted);
assert.equal(kiosk?.kiosk, true);
assert.equal(kiosk?.theme, 'dark');
const harness = readFileSync(new URL('../demo/golden/harness.mjs', import.meta.url), 'utf8');
assert.match(harness, /sessionStorage\.setItem\(attemptKey, attemptedTarget\)/);
assert.match(harness, /integration_version: scenario\.integrationVersion/);
});
test('golden matrix covers required geometry, rendering and adaptive surfaces', () => {
const ids = GOLDEN_SCENARIOS.map((scenario) => scenario.id).join(' ');
for (const token of ['geometry', 'diagonal-45-opening', 'openings', 'openings-hidden',
'fill-light', 'fill-temp', 'fill-lqi', 'lighting', 'hover', 'zoom-040', 'zoom-250',
'warm-remount', 'dialog-mobile', 'color-popover', 'tray-wide', 'tray-medium', 'sun-window',
'tray-narrow', 'opaque-glow-two-doorways', 'filled-tunnel', 'opening-placement',
'backup-full', 'backup-space', 'backup-plan-only', 'value-badge-positions',
'value-face-cover-source', 'isometric-geometry',
'isometric-live-layers', 'isometric-no-borders', 'isometric-touch-kiosk',
'isometric-large-warm-remount', 'split-corner-wall', 'plan-snap-endpoint',
'plan-snap-line-gaps', 'wall-junctions', 'isometric-wall-junctions',
'wall-key-roundtrip',
'washer-active-cycle', 'washer-idle-cycle', 'space-tab-drop-before',
'space-tab-drop-after', 'decor-over-opaque-hover',
'decor-over-glow-base'])
assert.equal(ids.includes(token), true, token);
assert.equal(new Set(GOLDEN_SCENARIOS.map((scenario) => scenario.mode)).has('plan'), true);
assert.equal(new Set(GOLDEN_SCENARIOS.map((scenario) => scenario.mode)).has('devices'), true);
assert.equal(new Set(GOLDEN_SCENARIOS.map((scenario) => scenario.mode)).has('decor'), true);
assert.equal(new Set(GOLDEN_SCENARIOS.map((scenario) => scenario.theme)).has('light'), true);
assert.equal(new Set(GOLDEN_SCENARIOS.map((scenario) => scenario.theme)).has('dark'), true);
});
test('room-label parity goldens pair View and Plan in light and dark themes', () => {
const scenarios = GOLDEN_SCENARIOS.filter((scenario) => scenario.roomLabelParity);
assert.deepEqual(scenarios.map(({ mode, theme }) => `${mode}-${theme}`).sort(), [
'plan-dark', 'plan-light', 'view-dark', 'view-light',
]);
for (const scenario of scenarios) {
const fixture = prepareGoldenFixture(scenario);
const space = fixture.config.spaces.find((item) => item.id === scenario.space);
assert.equal(space.settings.show_names, true);
assert.equal(space.settings.label_temp, true);
assert.equal(space.settings.label_lqi, true);
assert.equal(space.settings.label_light, true);
assert.deepEqual(Object.keys(fixture.layout).filter((id) => id.startsWith('rl_')).sort(), [
'rl_light-left', 'rl_light-right',
]);
}
});
test('space-tab drop goldens hold both insertion sides in light and dark', () => {
const scenarios = GOLDEN_SCENARIOS.filter((scenario) => scenario.tabDrag);
assert.equal(scenarios.length, 2);
assert.deepEqual(new Set(scenarios.map((scenario) => scenario.tabDrag)),
new Set(['before', 'after']));
assert.deepEqual(new Set(scenarios.map((scenario) => scenario.theme)),
new Set(['light', 'dark']));
for (const scenario of scenarios) {
assert.equal(prepareGoldenFixture(scenario).config.spaces.length >= 3, true, scenario.id);
}
});
test('washer lifecycle goldens pair active and idle snapshots of one composite fixture', () => {
const active = GOLDEN_SCENARIOS.find((scenario) => scenario.id === 'washer-active-cycle-dark');
const idle = GOLDEN_SCENARIOS.find((scenario) => scenario.id === 'washer-idle-cycle-dark');
assert.ok(active);
assert.ok(idle);
assert.equal(active.space, 'golden-appliance');
assert.equal(idle.space, active.space);
assert.equal(active.applianceLifecycle, true);
assert.equal(idle.applianceLifecycle, true);
assert.equal(active.stateOverrides['sensor.golden_washer_status'].state, 'start');
assert.equal(idle.stateOverrides['sensor.golden_washer_status'].state, 'done');
for (const scenario of [active, idle]) {
const fixture = prepareGoldenFixture(scenario);
assert.equal(fixture.states['switch.golden_washer_power'].state, 'on');
assert.equal(fixture.states['switch.golden_washer_child_lock'].state, 'off');
assert.equal(fixture.entities['sensor.golden_washer_status'].translation_key, 'status');
assert.equal(fixture.layout['golden-washer'].s, 'golden-appliance');
}
});
test('wall junction goldens cover live L/T previews plus saved flat and isometric bodies', () => {
const scenarios = GOLDEN_SCENARIOS.filter((scenario) => scenario.wallJunctions);
assert.deepEqual(scenarios.map((scenario) => scenario.id), [
'wall-junctions-plan-preview-light',
'wall-junctions-plan-t-dark',
'wall-junctions-view-dark',
'isometric-wall-junctions-dark',
]);
for (const scenario of scenarios) {
const fixture = prepareGoldenFixture(scenario);
const space = fixture.config.spaces.find((item) => item.id === scenario.space);
assert.equal(space.partitions.length, 9);
assert.equal('room_drafts' in space, false);
assert.ok(space.partitions.some((item) => item.b[1] === 0.94), 'room-wall T fixture');
}
});
test('corner Split golden captures before, thin and thick facade states', () => {
const scenarios = GOLDEN_SCENARIOS.filter((scenario) => scenario.cornerSplitWall);
assert.deepEqual(
scenarios.map((scenario) => scenario.cornerSplitWall),
['before', 'thin', 'thick', 'zero-taper'],
);
for (const scenario of scenarios) {
const fixture = prepareGoldenFixture(scenario);
const space = fixture.config.spaces.find((item) => item.id === scenario.space);
assert.ok(space);
assert.equal(space.settings.show_borders, true);
if (scenario.cornerSplitWall === 'zero-taper') {
assert.equal(space.rooms.length, 2);
assert.equal(space.rooms[0].poly.some((point) => point[1] === 0.405), true);
assert.equal(
space.walls.some((wall) => (
wall.key === fixtureWallKey([0.60, 0.40], [0.90, 0.405])
)),
false,
);
continue;
}
assert.equal(space.rooms.length, scenario.cornerSplitWall === 'before' ? 1 : 2);
if (scenario.cornerSplitWall !== 'before') {
const divider = space.walls.find((wall) => (
wall.a?.[0] === 0.10 && wall.a?.[1] === 0.10
&& wall.b?.[0] === 0.90 && wall.b?.[1] === 0.50
));
assert.equal(divider?.cm, scenario.cornerSplitWall === 'thin' ? 15 : 100);
}
}
});
test('issue 276 golden captures 5 cm offsets and hosted door before/after 10/30/virtual', () => {
const scenarios = GOLDEN_SCENARIOS.filter((scenario) => (
scenario.coincidentPartition && !scenario.hiddenWallDiagnostics
));
assert.deepEqual(
scenarios.map((scenario) => scenario.coincidentPartition),
['before', 'thin', 'thick', 'virtual'],
);
for (const scenario of scenarios) {
const fixture = prepareGoldenFixture(scenario);
const space = fixture.config.spaces.find((item) => item.id === scenario.space);
assert.ok(space);
assert.equal(space.cell_cm, 5);
assert.ok(space.rooms[0].poly.some((point) => point[0] === 0.5
&& point[1] === 0.004166666666666667), 'short 5 cm offset must be visible');
assert.equal(space.openings.length, 1);
if (scenario.coincidentPartition === 'before') {
assert.equal(space.partitions.length, 1);
assert.equal(space.openings[0].host.id, 'redundant');
continue;
}
assert.equal(space.partitions, undefined);
assert.equal(space.openings[0].host, undefined);
assert.equal(space.openings[0].x, 0.504166667);
if (scenario.coincidentPartition === 'virtual') {
assert.equal(space.open_spans.length, 1);
} else {
assert.equal(space.walls[0].cm, scenario.coincidentPartition === 'thin' ? 10 : 30);
}
}
});
test('issue 296 golden shows hidden independent-wall diagnostics in both themes', () => {
const scenarios = GOLDEN_SCENARIOS.filter((scenario) => scenario.hiddenWallDiagnostics);
assert.deepEqual(scenarios.map((scenario) => scenario.theme).sort(), ['dark', 'light']);
for (const scenario of scenarios) {
assert.equal(scenario.mode, 'plan');
const fixture = prepareGoldenFixture(scenario);
const space = fixture.config.spaces.find((item) => item.id === scenario.space);
assert.equal(space.partitions.length, 2);
assert.equal('room_drafts' in space, false);
assert.ok(space.partitions.some((item) => item.id === 'hidden-saved-chain-edge'
&& item.a[0] === 0 && item.a[1] === 0 && item.b[0] === 0 && item.b[1] === 1));
}
});
test('filled opening golden has a pixel-level seam detector', () => {
const scenario = GOLDEN_SCENARIOS.find((item) => item.id === 'openings-filled-tunnel-dark');
assert.ok(scenario);
assert.equal(scenario.fillMode, 'custom');
assert.equal(scenario.customFill.a > 0 && scenario.customFill.a < 1, true);
assert.equal(scenario.glowEnabled, false);
assert.equal(scenario.hideOpenings, true, 'opening symbols must not mask the sampled tunnel pixels');
assert.equal(scenario.tunnelContinuity.openingId, 'light-door');
assert.equal(scenario.tunnelContinuity.maxChannelJump <= 3, true);
assert.equal(scenario.tunnelContinuity.dpr2, true);
assert.equal(scenario.wallReplacements[0].segments.length, 2);
assert.notEqual(
scenario.wallReplacements[0].segments[0].cm,
scenario.wallReplacements[0].segments[1].cm,
);
});
test('decor layer goldens pair opaque hover and Glow base with semantic pixels', () => {
const scenarios = GOLDEN_SCENARIOS.filter((item) => item.decorPixelProbes);
assert.deepEqual(scenarios.map((item) => item.id), [
'decor-over-opaque-hover-light',
'decor-over-glow-base-dark',
]);
assert.deepEqual(scenarios.map((item) => item.theme), ['light', 'dark']);
for (const scenario of scenarios) {
assert.equal(scenario.showBorders, false);
assert.equal(scenario.showNames, false);
assert.equal(scenario.hideOpenings, true);
assert.equal(scenario.sunRays, false);
assert.equal(scenario.decorPixelProbes.points.some((point) => point.id === 'opening-tunnel'), true);
const fixture = prepareGoldenFixture(scenario);
const space = fixture.config.spaces.find((item) => item.id === scenario.space);
assert.deepEqual(new Set(space.decor.map((shape) => shape.kind)),
new Set(['line', 'rect', 'ellipse', 'text', 'furniture']));
assert.equal(space.settings.show_borders, false);
assert.equal(space.settings.hide_openings, true);
}
assert.equal(scenarios[0].fillMode, 'custom');
assert.equal(scenarios[0].customFill.a, 1);
assert.equal(scenarios[0].hoverRoom, 'light-left');
assert.equal(scenarios[1].fillMode, 'glow');
assert.equal(scenarios[1].glowEnabled, true);
assert.equal(scenarios[1].allLightsOff, true);
});
test('doorway spill golden exposes the opaque-fill failure mode from issue 71', () => {
const scenario = GOLDEN_SCENARIOS.find((item) => item.id === 'lighting-opaque-glow-two-doorways-dark');
assert.ok(scenario);
assert.equal(scenario.theme, 'dark');
assert.equal(scenario.customFill?.a, 1);
assert.equal(scenario.glowEnabled, true);
assert.equal(scenario.sunRays, false);
assert.equal(scenario.allLightsOff, true);
assert.equal(scenario.extraOpenings?.filter((opening) => opening.type === 'door').length, 2);
assert.equal(scenario.extraOpenings?.find((opening) => opening.id === 'light-door-second')?.contact,
'cover.golden_door_closed');
assert.equal(scenario.extraOpenings?.find((opening) => opening.id === 'light-door-third')?.contact,
'cover.golden_door_half');
assert.equal(scenario.stateOverrides?.['light.golden_light_one']?.state, 'on');
assert.deepEqual(scenario.layoutOverrides?.['golden-light-one'], {
s: 'golden-lighting', x: 0.40, y: 0.48,
});
assert.equal(scenario.warmPixelRegion?.minPixels >= 2500, true);
assert.equal(scenario.warmPixelRegion?.x >= 0.5, true);
});
test('stepped exterior PDF golden pins the complete reconstructable dimension chain', () => {
const scenario = GOLDEN_SCENARIOS.find((item) =>
item.id === 'pdf-export-stepped-dimensions-light');
assert.ok(scenario);
assert.equal(scenario.pdfExport, true);
assert.equal(scenario.pdfSteppedExterior, true);
assert.deepEqual(scenario.pdfSemantic.requiredDimensionLabels,
['1.20 m', '2.40 m', '3.60 m', '3.75 m', '7.35 m', '9.75 m']);
assert.equal(scenario.pdfSemantic.maxDimensionAxisErrorDeg, 0.5);
const fixture = prepareGoldenFixture(scenario);
const space = fixture.config.spaces.find((item) => item.id === scenario.space);
assert.equal(space.rooms[0].poly.length, 8);
assert.equal(space.wall_segments.length, 8);
assert.equal(space.wall_segments.every((wall) => wall.cm === 15), true);
});
test('sun-ray golden requires browser-painted light from a state-only sun entity', () => {
const scenario = GOLDEN_SCENARIOS.find((item) => item.id === 'lighting-sun-window-state-only-dark');
assert.ok(scenario);
const fixture = prepareGoldenFixture(scenario);
const space = fixture.config.spaces.find((item) => item.id === scenario.space);
assert.equal(GOLDEN_MATRIX_VERSION, 66);
assert.equal(space.settings.sun_rays, true);
assert.equal(scenario.northDeg, 90,
'the sign-sensitive golden must keep a non-zero north direction');
assert.equal(space.settings.north_deg, 90);
assert.equal(fixture.states['sun.sun']?.attributes?.azimuth, 270,
'north=90 plus azimuth=270 points to the top window; subtraction would point down');
assert.equal(space.openings.some((opening) => opening.type === 'window'), true);
assert.equal(fixture.states['sun.sun']?.state, 'above_horizon');
assert.equal(fixture.entities['sun.sun'], undefined,
'sun.sun must exercise the real state-only HA path');
assert.equal(scenario.capture, 'sun-window');
assert.equal(scenario.glowEnabled, false);
assert.equal(scenario.allLightsOff, true);
assert.equal(scenario.sunRayPixels.minPixels >= 500, true);
assert.equal(scenario.sunRayPixels.minChannelDelta >= 4, true);
const outer = GOLDEN_SCENARIOS.find((item) =>
item.id === 'lighting-sun-window-outer-thick-dark');
assert.ok(outer);
const outerFixture = prepareGoldenFixture(outer);
const outerSpace = outerFixture.config.spaces.find((item) => item.id === outer.space);
const hostWall = outerSpace.walls.find((wall) =>
Math.abs(wall.a[1] - 0.10) < 1e-9 && Math.abs(wall.b[1] - 0.10) < 1e-9);
assert.equal(outerFixture.config.settings.sun_ray_origin, 'outer');
assert.equal(outerSpace.settings.sun_ray_origin, undefined,
'the selector is global-only and must not be hidden in a space override');
assert.equal(hostWall?.cm, 15, 'outer golden must retain a physical wall tunnel');
assert.equal(outer.capture, 'sun-window');
assert.equal(outer.sunRayPixels.minPixels >= 500, true);
const occlusion = GOLDEN_SCENARIOS.filter((item) =>
item.id.startsWith('lighting-sun-window-outer-occluded-dpr'));
assert.deepEqual(occlusion.map((item) => item.deviceScaleFactor), [1, 2]);
for (const item of occlusion) {
const prepared = prepareGoldenFixture(item);
assert.equal(prepared.config.settings.sun_ray_origin, 'outer');
assert.equal(prepared.states['sun.sun']?.attributes?.azimuth, 240,
'north=90 plus azimuth=240 keeps the top window lit at an oblique angle');
assert.equal(item.capture, 'sun-window');
assert.equal(item.sunRayPixels.minPixels >= 400, true);
}
});
test('vacuum smoothing golden covers current gaps and the previous run', () => {
const scenario = GOLDEN_SCENARIOS.find((item) => item.id === 'vacuum-trail-smoothing-dark');
assert.ok(scenario);
assert.equal(scenario.mode, 'view');
assert.equal(scenario.vacuumTrail?.maxDeviationCm, 17.5);
assert.equal(scenario.vacuumTrail?.current.length, 2);
assert.equal(scenario.vacuumTrail?.current.flat().length >= 8, true);
assert.equal(scenario.vacuumTrail?.previous.length >= 4, true);
const fixture = prepareGoldenFixture(scenario);
assert.equal(fixture.config.markers.some((marker) => marker.id === 'golden-vacuum-trail'), true);
assert.equal(fixture.states['vacuum.golden_vacuum_trail']?.state, 'cleaning');
});
test('issue #277 golden pairs safe/disabled handles and an opening clamp in both themes', () => {
const scenarios = GOLDEN_SCENARIOS.filter((item) => item.safeResizePreview);
assert.deepEqual(scenarios.map((item) => item.theme).sort(), ['dark', 'light']);
for (const scenario of scenarios) {
assert.equal(scenario.safeResizeFixture, true);
assert.equal(scenario.mode, 'plan');
const fixture = prepareGoldenFixture(scenario);
const space = fixture.config.spaces.find((item) => item.id === scenario.space);
assert.equal(space.rooms.length, 3);
assert.equal(space.rooms.some((room) => room.id === 'resize-diagonal'), true);
assert.equal(space.openings.some((opening) => opening.id === 'resize-side-door'), true);
}
});
test('issue 252 golden matrix covers owner-aware cleanup in both themes and languages', () => {
const optimize = GOLDEN_SCENARIOS.filter(
(item) => item.dialog === 'optimize-orphan-references',
);
assert.deepEqual(optimize.map((item) => item.theme).sort(), ['dark', 'light']);
assert.deepEqual(optimize.map((item) => item.language).sort(), ['en', 'ru']);
for (const scenario of optimize) {
const live = scenario.markerOverrides.find((marker) => marker.id === 'golden-light-two');
const removed = scenario.markerOverrides.find((marker) => marker.id === 'golden-presence');
assert.equal(live.space, 'removed-floor');
assert.equal(!!live.name, true);
assert.equal(removed.removed, true);
assert.equal(scenario.layoutOverrides['golden-light-one'].s, 'unresolved-floor');
assert.equal(scenario.layoutOverrides['golden-presence'].s, 'removed-floor');
}
// The existing #244 default-floor warning remains paired light/dark as well.
const editors = GOLDEN_SCENARIOS.filter(
(item) => item.cardEditorInvalidDefaultFloor === 'removed-floor',
);
assert.deepEqual(editors.map((item) => item.theme).sort(), ['dark', 'light']);
assert.deepEqual(editors.map((item) => item.language).sort(), ['en', 'ru']);
});
test('issue #197 golden keeps the complete junction fixture in Plan and View', () => {
const scenarios = GOLDEN_SCENARIOS.filter((item) =>
item.id.startsWith('junction-patch-resilience-'));
assert.deepEqual(scenarios.map((item) => item.mode).sort(), ['plan', 'view']);
for (const scenario of scenarios) {
assert.equal(scenario.theme, 'dark');
assert.equal(scenario.junctionPatchResilience, true);
assert.deepEqual(scenario.retainedWedgeProbe, [0.8955, 0.556]);
assert.deepEqual(
scenario.absentWallProbes,
[[0.420833333, 0.37625], [0.92, 0.348]],
);
const fixture = prepareGoldenFixture(scenario);
const space = fixture.config.spaces.find((item) => item.id === scenario.space);
assert.ok(space);
assert.deepEqual(
[space.rooms.length, space.walls.length, space.open_spans.length],
[8, 25, 3],
);
}
});
test('issues #278/#291 golden covers the stored wall-union fixture in both themes', () => {
const scenarios = GOLDEN_SCENARIOS.filter((item) => item.wallUnionIsolation);
assert.deepEqual(scenarios.map((item) => item.theme).sort(), ['dark', 'light']);
for (const scenario of scenarios) {
assert.equal(scenario.mode, 'view');
const fixture = prepareGoldenFixture(scenario);
const space = fixture.config.spaces.find((item) => item.id === scenario.space);
assert.ok(space);
assert.deepEqual([space.rooms.length, space.walls.length], [2, 3]);
}
});
test('issue #249 golden isolates a bounded physical three-ray bevel', () => {
const scenario = GOLDEN_SCENARIOS.find(
(item) => item.id === 'multiwall-junction-bevel-view-dark',
);
assert.ok(scenario);
assert.equal(scenario.mode, 'view');
assert.equal(scenario.theme, 'dark');
assert.equal(scenario.multiWallJunction.rays, 3);
assert.equal(scenario.multiWallJunction.node.length, 2);
assert.equal(scenario.multiWallJunction.retainedOverlapProbe.length, 2);
assert.equal(scenario.multiWallJunction.enclosedHoles, 0,
'the product contract must not preserve the temporary two-hole inventory');
const fixture = prepareGoldenFixture(scenario);
const space = fixture.config.spaces.find((item) => item.id === scenario.space);
assert.ok(space);
assert.deepEqual(space.node, scenario.multiWallJunction.node);
const endpointRays = space.walls.filter((wall) => [wall.a, wall.b].some((point) => (
Math.hypot(point[0] - space.node[0], point[1] - space.node[1]) < 1e-8
)));
assert.equal(endpointRays.length >= scenario.multiWallJunction.rays, true);
assert.equal(space.settings.show_borders, true);
});
test('every multi-wall golden scene declares its enclosed-hole inventory (#272)', () => {
const scenes = GOLDEN_SCENARIOS.filter((item) => item.multiWallJunction);
assert.ok(scenes.length >= 1, 'multi-wall semantic scenes disappeared');
for (const scene of scenes) {
assert.equal(Number.isInteger(scene.multiWallJunction.enclosedHoles), true,
`${scene.id}: enclosedHoles is missing`);
assert.ok(scene.multiWallJunction.enclosedHoles >= 0);
}
});
test('issue #275 golden preflight samples protected strips, not just enclosed holes', () => {
const scenarios = GOLDEN_SCENARIOS.filter((item) => item.orthogonalStripContainment);
assert.deepEqual(scenarios.map((item) => item.id), [
'orthogonal-strip-cell-5-view-dark',
'orthogonal-strip-cell-1-view-dark',
]);
assert.deepEqual(
scenarios.map((item) => item.orthogonalStripContainment.caseId),
['cell-5-mixed-depth-t', 'cell-1-thick-crossbar-t'],
);
for (const scenario of scenarios) {
assert.equal(scenario.mode, 'view');
assert.ok(scenario.orthogonalStripContainment.minSamples >= 300);
const fixture = prepareGoldenFixture(scenario);
const space = fixture.config.spaces.find((item) => item.id === scenario.space);
assert.ok(space);
assert.ok(space.nodes.length >= 1);
assert.ok(space.walls.length >= 3);
assert.equal(space.settings.show_borders, true);
}
});
test('issue #258 golden renders the affected persisted key at its T-junction', () => {
const scenario = GOLDEN_SCENARIOS.find(
(item) => item.id === 'wall-key-roundtrip-view-dark',
);
assert.ok(scenario);
assert.equal(scenario.mode, 'view');
assert.equal(scenario.theme, 'dark');
assert.equal(scenario.wallKeyRoundtrip.variant, 'affected');
const fixture = prepareGoldenFixture(scenario);
const space = fixture.config.spaces.find((item) => item.id === scenario.space);
assert.ok(space);
assert.equal(space.walls[0].key, '0.887500,0.195833@1.5706');
assert.deepEqual(scenario.wallKeyRoundtrip.node, [0.8875, 0.345833333]);
assert.equal(space.settings.show_borders, true);
});
test('the open color picker golden covers dark mobile and light desktop themes', () => {
const scenarios = GOLDEN_SCENARIOS.filter((item) => item.dialog === 'decor-color');
assert.deepEqual(scenarios.map(({ id, language, theme, viewport }) => ({
id, language, theme, width: viewport.width,
})), [
{ id: 'decor-color-popover-mobile-ru', language: 'ru', theme: 'dark', width: 390 },
{ id: 'decor-color-popover-desktop-en', language: 'en', theme: 'light', width: 760 },
]);
});
test('all newly unified color-picker dialog families have reviewed-golden candidates', () => {
const scenarios = GOLDEN_SCENARIOS.filter((item) => [
'general-color', 'device-ripple-color', 'space-room-color',
].includes(item.dialog));
assert.deepEqual(scenarios.map(({ id, dialog, theme, viewport }) => ({
id, dialog, theme, width: viewport.width,
})), [
{ id: 'general-color-popover-desktop-en', dialog: 'general-color', theme: 'light', width: 900 },
{ id: 'device-ripple-color-popover-mobile-ru', dialog: 'device-ripple-color', theme: 'dark', width: 390 },
{ id: 'space-room-color-popover-desktop-ru', dialog: 'space-room-color', theme: 'dark', width: 900 },
]);
});
test('value badge golden covers four positions and bottom badge with separate LQI', () => {
const scenario = GOLDEN_SCENARIOS.find((item) => item.id === 'device-value-badge-positions-dark');
assert.ok(scenario);
assert.deepEqual(
new Set(scenario.markerOverrides.map((marker) => marker.value_badge.position)),
new Set(['right', 'bottom', 'left', 'top']),
);
const bottom = scenario.markerOverrides.find((marker) => marker.value_badge.position === 'bottom');
assert.equal(bottom.id, 'golden-climate');
assert.notEqual(bottom.value_badge.source.kind, 'derived_lqi');
const fixture = prepareGoldenFixture(scenario);
assert.equal(fixture.states['climate.golden_climate'].attributes.lqi, 190);
assert.equal(
fixture.config.markers.find((marker) => marker.id === 'golden-climate').value_badge.position,
'bottom',
);
});
test('explicit cover value-source golden pins the 42 percent value face', () => {
const scenario = GOLDEN_SCENARIOS.find(
(item) => item.id === 'device-value-face-cover-source-dark',
);
assert.ok(scenario);
assert.equal(scenario.deviceOnly, 'golden-left-linkquality');
const fixture = prepareGoldenFixture(scenario);
const marker = fixture.config.markers.find((item) => item.id === scenario.deviceOnly);
assert.equal(marker.binding, 'entity:cover.golden_door_half');
assert.equal(marker.display, 'value');
assert.deepEqual(marker.value_source, {
kind: 'entity_attribute',
entity_id: 'cover.golden_door_half',
attribute: 'current_position',
});
assert.equal(fixture.states['cover.golden_door_half'].attributes.current_position, 42);
});
test('device icon state-table goldens cover both themes and design facets', () => {
const scenarios = GOLDEN_SCENARIOS.filter((item) => item.id.startsWith('device-icon-state-table-'));
assert.deepEqual(new Set(scenarios.map((scenario) => scenario.theme)), new Set(['light', 'dark']));
for (const scenario of scenarios) {
assert.equal(scenario.focusDevice, 'golden-climate');
assert.equal(scenario.hoverDevice, 'golden-presence');
assert.equal(scenario.hideHoverTooltip, true);
assert.deepEqual(scenario.deviceClassOverrides['golden-climate'], ['alarm', 'sel']);
assert.deepEqual(scenario.deviceClassOverrides['golden-presence'], ['virtual']);
assert.deepEqual(scenario.deviceClassOverrides['golden-light-two'], ['lock-locked']);
assert.deepEqual(scenario.deviceClassOverrides['golden-light-three'], ['lock-unlocked']);
assert.deepEqual(scenario.deviceClassOverrides['golden-right-linkquality'], ['open']);
const fixture = prepareGoldenFixture(scenario);
assert.equal(fixture.states['light.golden_light_one'].attributes.lqi, 40);
assert.equal(fixture.states['light.golden_light_two'].attributes.lqi, 41);
assert.equal(fixture.states['light.golden_light_three'].attributes.lqi, 180);
assert.equal(fixture.states['light.golden_light_three'].state, 'unavailable');
const targetAvailabilityController = fixture.config.markers.find(
(marker) => marker.id === 'golden-left-linkquality',
);
assert.equal(targetAvailabilityController.tap_action, 'toggle');
assert.deepEqual(targetAvailabilityController.controls, ['light.golden_light_three']);
assert.equal(
fixture.config.markers.find((marker) => marker.id === 'golden-left-temperature').display,
'value',
);
assert.equal(
fixture.config.markers.find((marker) => marker.id === 'golden-right-temperature').display,
'static_icon',
);
// #588. Пятый режим стоит на включённой RGB-лампе, и это существенно:
// «Значение + состояние» красит её маркер, новый режим обязан оставить его
// нейтральным. На нейтральном источнике (сенсор, крышка) обе стороны дали
// бы один и тот же кадр, и пересъёмка доказывала бы только сама себя.
assert.equal(
fixture.config.markers.find((marker) => marker.id === 'golden-light-one').display,
'value_static_icon',
);
assert.equal(fixture.states['light.golden_light_one'].state, 'on');
assert.deepEqual(fixture.states['light.golden_light_one'].attributes.rgb_color,
[255, 196, 112]);
}
});
test('Text shell regression goldens isolate a large long value in both themes', () => {
const scenarios = GOLDEN_SCENARIOS.filter((item) => item.id.startsWith('device-text-shell-long-'));
assert.deepEqual(new Set(scenarios.map((scenario) => scenario.theme)), new Set(['light', 'dark']));
for (const scenario of scenarios) {
assert.equal(scenario.deviceOnly, 'golden-left-linkquality');
const fixture = prepareGoldenFixture(scenario);
const marker = fixture.config.markers.find((item) => item.id === 'golden-left-linkquality');
assert.equal(marker.display, 'value');
assert.equal(marker.size, 3);
assert.equal(
fixture.config.spaces.find((item) => item.id === scenario.space).settings.show_names,
false,
);
assert.equal(fixture.states['sensor.golden_left_linkquality'].state, '498');
assert.equal(
fixture.states['sensor.golden_left_linkquality'].attributes.unit_of_measurement,
'ppm',
);
}
});
test('opening placement golden requires browser-painted preview pixels', () => {
const door = GOLDEN_SCENARIOS.find(
(item) => item.id === 'opening-placement-door-thick-wall-dark',
);
assert.ok(door);
assert.equal(door.mode, 'plan');
assert.equal(door.openingPreview?.type, 'door');
assert.deepEqual(door.openingPreview?.pointer, [0.48, 0.65]);
assert.equal(door.openingPreviewPixels.minPixels >= 150, true);
assert.equal(door.openingPreviewPixels.minChannelDelta >= 4, true);
const passages = GOLDEN_SCENARIOS.filter(
(item) => item.id.startsWith('opening-placement-passage-thick-wall-'),
);
assert.deepEqual(passages.map((scenario) => scenario.theme), ['dark', 'light']);
for (const scenario of passages) {
assert.equal(scenario.mode, 'plan');
assert.equal(scenario.openingPreview?.type, 'passage');
assert.deepEqual(scenario.openingPreview?.pointer, [0.48, 0.65]);
assert.equal(scenario.openingPreviewPixels.minPixels >= 150, true);
assert.equal(scenario.openingPreviewPixels.minInsideWallPixels >= 8, true);
assert.equal(scenario.openingPreviewPixels.minChannelDelta >= 4, true);
}
});
test('diagonal opening golden asserts a real painted 45 degree symbol', () => {
const scenario = GOLDEN_SCENARIOS.find(
(item) => item.id === 'geometry-diagonal-45-opening-dark',
);
assert.ok(scenario);
assert.deepEqual(scenario.openingGeometry, {
id: 'geo-diagonal-window', type: 'window', angle: 45,
});
const fixture = prepareGoldenFixture(scenario);
const space = fixture.config.spaces.find((item) => item.id === scenario.space);
assert.deepEqual(space.openings.map((opening) => opening.id), ['geo-diagonal-window']);
});
test('opening symbol goldens lock room, diagonal, flip-pair and hidden Iso contracts', () => {
const scenarios = GOLDEN_SCENARIOS.filter((item) => item.openingSymbolContract);
assert.deepEqual(scenarios.map((item) => item.id), [
'opening-symbol-room-wall-light',
'opening-symbol-diagonal-partition-dark',
'opening-symbol-flip-pairs-light',
'isometric-opening-symbol-parity-dark',
]);
for (const scenario of scenarios) {
const fixture = prepareGoldenFixture(scenario);
const space = fixture.config.spaces.find((item) => item.id === scenario.space);
assert.ok(space, scenario.id);
assert.equal(space.openings.length, scenario.openingSymbolContract.openings.length, scenario.id);
assert.equal(space.settings.show_borders, true, scenario.id);
assert.equal(space.openings.every((opening) => Number.isFinite(opening.x)
&& Number.isFinite(opening.y) && Number.isFinite(opening.angle)), true, scenario.id);
if (scenario.openingSymbolContract.kind === 'partition') {
assert.equal(space.partitions.length, 1, scenario.id);
assert.equal(space.openings.every((opening) => opening.host?.id === space.partitions[0].id),
true, scenario.id);
} else {
assert.equal(space.rooms.length, 2, scenario.id);
assert.equal(space.walls.some((wall) => wall.cm === scenario.openingSymbolContract.wallCm),
true, scenario.id);
}
}
const flipPair = scenarios.find((item) => item.id === 'opening-symbol-flip-pairs-light');
assert.deepEqual(flipPair.openingSymbolContract.openings.map((opening) => [
opening.type, opening.flipV, opening.offset,
]), [
['door', false, 'center'], ['door', true, 'center'],
['window', false, 'center'], ['window', true, 'center'],
['gate', false, 'center'], ['gate', true, 'center'],
]);
assert.equal(OPENING_SYMBOL_EXISTING_GOLDEN_IMPACT.length, 67);
assert.equal(new Set(OPENING_SYMBOL_EXISTING_GOLDEN_IMPACT).size, 67);
const scenarioIds = new Set(GOLDEN_SCENARIOS.map((scenario) => scenario.id));
// #634: чек-листы по issue перенесены в docs/testing-notes/ — документация
// тестирования это TESTING.md вместе с приложениями.
const notesDir = new URL('../docs/testing-notes/', import.meta.url);
const testingDoc = [new URL('../docs/TESTING.md', import.meta.url),
...readdirSync(notesDir).filter((name) => name.endsWith('.md')).sort().map((name) => new URL(name, notesDir))]
.map((url) => readFileSync(url, 'utf8')).join('\n');
for (const id of OPENING_SYMBOL_EXISTING_GOLDEN_IMPACT) {
assert.equal(scenarioIds.has(id), true, id);
assert.match(testingDoc, new RegExp(`\\b${id}\\b`), id);
}
});
test('issue 570 Stage 4 reuses the historical iso goldens for visual handoff coverage', () => {
const scenarios = GOLDEN_SCENARIOS.filter((item) => item.id.startsWith('isometric-stage3-'));
assert.deepEqual(scenarios.map((item) => item.id), [
'isometric-stage3-overlays-light',
'isometric-stage3-overlays-dark',
'isometric-stage3-openings-dark',
'isometric-stage3-forced-colors-dark',
'isometric-stage3-no-filter-dark',
]);
assert.equal(scenarios.every((item) => item.projection === 'iso'
&& item.mode === 'view' && item.capture === 'stage'), true);
const overlays = scenarios.filter((item) => item.id.includes('-overlays-'));
assert.deepEqual(new Set(overlays.map((item) => item.theme)), new Set(['light', 'dark']));
assert.equal(overlays.every((item) => item.stage3Golden.requireNudged
&& item.stage3Golden.requireDenseFacets
&& item.stage3Golden.requireNoTetherCues
&& item.stage3Golden.requireNoGrounding
&& item.stage3Golden.requiredKinds.includes('device')
&& item.stage3Golden.requiredKinds.includes('room-label')
&& item.stage3Golden.requiredKinds.includes('opening-lock')), true);
assert.equal(overlays.find((item) => item.theme === 'dark')?.stage3Golden.requireVacuumFloor,
true);
assert.equal(overlays.find((item) => item.theme === 'dark')?.stage3Golden.requireLiveLayers,
true);
assert.ok(overlays.find((item) => item.theme === 'dark')?.decorOverride?.length);
const combinedFixture = prepareGoldenFixture(overlays.find((item) => item.theme === 'dark'));
const combinedSpace = combinedFixture.config.spaces.find((item) => item.id === 'golden-lighting');
const combinedAreas = new Set(combinedSpace.rooms.map((room) => room.area));
assert.ok(combinedAreas.has(combinedFixture.devices['golden-vacuum-trail'].area_id));
assert.equal(combinedSpace.settings.north_deg, 90);
assert.equal(combinedFixture.states['sun.sun'].attributes.azimuth, 270);
const openingScenario = scenarios.find((item) => item.id === 'isometric-stage3-openings-dark');
const openingFixture = prepareGoldenFixture(openingScenario);
const openingSpace = openingFixture.config.spaces.find(
(item) => item.id === openingScenario.space,
);
assert.deepEqual(openingScenario.stage3Golden.openingKinds, ['door', 'window', 'gate']);
assert.equal(openingScenario.stage3Golden.requirePassage, true);
assert.equal(openingSpace.openings.some((item) => item.type === 'passage'), true);
const lockedOpening = openingSpace.openings.find((item) => item.id === 'light-door');
assert.equal(openingFixture.states[lockedOpening.lock].state, 'unlocked');
const denseFixture = prepareGoldenFixture(overlays[0]);
const denseSpace = denseFixture.config.spaces.find((item) => item.id === overlays[0].space);
assert.equal(denseSpace.settings.label_temp, true);
assert.equal(denseSpace.settings.label_lqi, true);
assert.deepEqual(denseFixture.config.settings.new_device_ids, ['golden-presence']);
assert.equal(denseFixture.config.markers.find((item) => item.id === 'golden-presence')?.display,
'icon_ripple');
assert.ok(denseFixture.layout['rl_light-left']);
const forced = scenarios.find((item) => item.forcedColors);
const noFilter = scenarios.find((item) => item.disableIsoFilters);
assert.equal(forced.stage3Golden.requireNoMaterialDefs, true);
assert.equal(noFilter.stage3Golden.requireNoMaterialDefs, true);
assert.equal(forced.stage3Golden.requireNoTetherCues, true);
assert.equal(noFilter.stage3Golden.requireNoTetherCues, true);
assert.equal(forced.stage3Golden.requireNoGrounding, true);
assert.equal(noFilter.stage3Golden.requireNoGrounding, true);
const noBorders = GOLDEN_SCENARIOS.find((item) => item.id === 'isometric-no-borders-dark');
assert.equal(noBorders.stage3Golden.noBorders, true);
const harness = readFileSync(new URL('../demo/golden/harness.mjs', import.meta.url), 'utf8');
assert.match(harness,
/\[data-hp-iso-overlay-kind\]\[data-hp-iso-floor\]\[data-hp-iso-visual\]/);
assert.match(harness, /forcedColors: scenario\.forcedColors \? 'active' : 'none'/);
assert.match(harness, /Stage 4 golden raised a floor-bound vacuum/);
assert.match(harness, /Stage 4 no-borders golden retained low overlays/);
assert.match(harness, /!scenario\.stage3Golden\?\.noBorders[\s\S]*?data-hp-iso-stage/,
'no-borders remains true Iso without requiring the intentionally absent volume stage');
assert.match(harness,
/\.\.\.\(scenario\.stage3Golden[\s\S]*?\? \{ themes: \{ darkMode: scenario\.theme === 'dark' \} \}[\s\S]*?: \{\}\)/,
'explicit HA darkMode must be scoped to isometric golden scenarios');
assert.doesNotMatch(harness,
/locale: \{ language: scenario\.language \|\| 'en' \},\s*themes:/,
'legacy Flat and Iso golden inputs must not gain an unconditional HA themes field');
assert.match(harness, /Stage 4 golden did not use HA darkMode/);
assert.match(harness, /Stage 4 dense golden lacks/);
assert.match(harness, /Stage 4 combined golden lacks/);
assert.ok(harness.lastIndexOf('Stage 4 combined golden lacks')
> harness.lastIndexOf("await until(() => !!card.renderRoot.querySelector('.vactrail > path.case'))"),
'combined live-layer preflight must run after the async vacuum fixture settles');
});
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, 66);
assert.deepEqual(ids, [
'isometric-stage6-light-lightfloor',
'isometric-stage6-light-darkfloor',
'isometric-stage6-dark-darkfloor',
'isometric-stage6-dark-lightfloor',
'isometric-stage6-hover-light',
]);
for (const scenario of STAGE6_ACCEPTANCE_SCENARIOS) {
assert.strictEqual(GOLDEN_SCENARIOS.find((item) => item.id === scenario.id), scenario,
`${scenario.id} must be the exact reviewed matrix object, not a drifting copy`);
assert.equal(scenario.projection, 'iso');
assert.equal(scenario.mode, 'view');
assert.equal(scenario.capture, 'stage');
assert.deepEqual(scenario.stage3Golden.requiredKinds,
['device', 'room-label', 'opening-lock']);
}
const themeFloor = STAGE6_ACCEPTANCE_SCENARIOS.slice(0, 4);
assert.deepEqual(themeFloor.map((scenario) => [scenario.theme, scenario.customFill.c]), [
['light', '#eee8de'], ['light', '#737777'],
['dark', '#737777'], ['dark', '#eee8de'],
]);
assert.equal(themeFloor.every((scenario) => scenario.sunRays === true
&& scenario.northDeg === 180
&& scenario.stateOverrides['sun.sun'].attributes.azimuth === 150
&& scenario.stateOverrides['sun.sun'].attributes.elevation === 52), true);
const hover = STAGE6_ACCEPTANCE_SCENARIOS[4];
assert.equal(hover.hoverDevice, 'golden-light-two');
assert.equal(hover.sunRays, false);
});
test('golden harness applies doorway, state and layout overrides to a cloned fixture', () => {
const scenario = GOLDEN_SCENARIOS.find((item) => item.id === 'lighting-opaque-glow-two-doorways-dark');
const fixture = prepareGoldenFixture(scenario);
const space = fixture.config.spaces.find((item) => item.id === scenario.space);
assert.equal(space.openings.filter((opening) => opening.type === 'door').length, 3);
assert.equal(fixture.states['cover.golden_door_closed'].attributes.current_position, 0);
assert.equal(fixture.states['cover.golden_door_half'].attributes.current_position, 50);
assert.equal(fixture.states['light.golden_light_one'].state, 'on');
assert.deepEqual(fixture.layout['golden-light-one'], {
s: 'golden-lighting', x: 0.40, y: 0.48,
});
assert.equal(space.settings.sun_rays, false);
assert.equal(space.settings.custom_fill.a, 1);
});
test('golden overrides fail closed on misspelled fixture references', () => {
const base = {
id: 'invalid', fixture: 'visual', space: 'golden-lighting', mode: 'view',
theme: 'dark', viewport: { width: 1000, height: 900 }, capture: 'stage',
threshold: { maxChannelDelta: 10, maxDiffRatio: 0.001 },
};
assert.throws(() => prepareGoldenFixture({
...base, stateOverrides: { 'light.golden_light_onee': { state: 'on' } },
}), /missing entity/);
assert.throws(() => prepareGoldenFixture({
...base, layoutOverrides: { 'golden-light-onee': { x: 0.4, y: 0.5 } },
}), /missing item/);
assert.throws(() => prepareGoldenFixture({
...base, roomGlow: { 'light-rightt': true },
}), /missing room/);
assert.throws(() => prepareGoldenFixture({
...base, decorOverride: [{ id: 'bad', kind: 'triangle' }],
}), /unknown kind/);
});
test('a light source paints exactly one region: the floor it can see', () => {
const source = readHouseplanProductionSource();
const glow = readFileSync(new URL('../src/glow-scene.ts', import.meta.url), 'utf8');
// One region per source, and it is the visibility polygon clipped to floor.
assert.match(glow,
/visibilityPolygon\(\s*\[input\.source\.x, input\.source\.y\], input\.radius/);
assert.match(glow,
/lit: seen\.length >= 3\s*\? intersectionPaths\(\[seen\], input\.scene\.floor, \{/);
assert.match(glow, /onBoundsFailure: \(\{ boundIndex, phase \}\) =>/,
'room-local boolean failures must be observable without restoring an unclipped fan');
assert.match(glow, /<circle class="glow-pool"/);
assert.match(source, /return renderGlowPools\(\{/,
'the full card must consume the shared pool renderer');
// No second layer of LIGHT may come back: a spill path or a shadow mask of
// its own is how the rendered result and the computed geometry got to
// disagree (#71, #73). The single blur below is the penumbra of the one
// region, measured in screen pixels and applied in one pass.
assert.doesNotMatch(glow, /glow-spill|glow-shadow|glow-blocker/);
// Exactly one blur, for the whole layer, sized in screen pixels.
assert.equal((glow.match(/<feGaussianBlur/g) || []).length, 1);
assert.doesNotMatch(glow, /<mask /);
assert.match(glow, /const next = GLOW_EDGE_FEATHER_PX \/ 2 \/ \(perUnit > 0 \? perUnit : 1\)/);
// #396: the freeze keys on "the camera is still", not on the two gesture
// flags. An animated transition sets neither, so every tween frame rebuilt
// the blur region — the cost this gate exists to avoid. Both the predicate
// and its use are pinned: a predicate that stops reading the transition is
// the regression, and so is a call that stops reading the predicate.
assert.match(source,
/const cameraStill = !this\._pinchStart && !this\._panStart\s*\n\s*&& !this\._cameraTransition\.active;/);
assert.match(source,
/resolveGlowFeather\(\s*this\._glowRuntimeState, perUnit, cameraStill,/);
assert.match(glow,
/filter=\$\{input\.featherEnabled \? 'url\(#hp-glowfeather\)' : nothing\}/,
'the expensive whole-layer filter must be bypassed during a viewport gesture/transition');
// Barriers are keyed by their own content: `_cfgEpoch` lags behind geometry
// edited in place, and a stale barrier set lights straight through a wall.
assert.doesNotMatch(glow, /_cfgEpoch/);
assert.match(glow,
/const geometryFingerprint = contentFingerprint\(\[\s*input\.rawSpaceConfig, input\.cellCm, input\.gridPitch/);
assert.match(glow,
/fingerprint: contentFingerprint\(\[geometryFingerprint, openingStateSignature\]\)/);
assert.match(glow,
/const cacheKey = `\$\{input\.space\.id\}\|\$\{revision\.fingerprint\}`/);
assert.match(glow, /sharedFingerprint === revision\.geometryFingerprint/,
'an in-place edit must never recut stale shared masonry');
});
test('all destructive editor dialogs use the medium shell and shared responsive footer groups', () => {
const source = readHouseplanProductionSource();
// #600: тело формы пространства живёт в общем модуле `space-form.ts`, а метод
// — тонкая обёртка с оболочкой. Футер ищем в обоих текстах.
const sharedSpaceForm = readFileSync(new URL('../src/editors/space-form.ts', import.meta.url), 'utf8');
for (const method of ['_renderOpeningDialog', '_renderPhysicalDialog', '_renderSpaceDialog']) {
const start = source.indexOf(`private ${method}`);
assert.notEqual(start, -1, method);
const end = source.indexOf('\n private ', start + 10);
const body = source.slice(start, end < 0 ? undefined : end)
+ (method === '_renderSpaceDialog' ? sharedSpaceForm : '');
const openTag = body.match(/<hp-dialog[\s\S]*?>/)?.[0] || '';
assert.match(openTag, /\bwide\b/,
`${method} must reserve the wide desktop shell (500 px; 560 px with form-shell, #600 Q7)`);
assert.match(body, /dialog-action-footer/, method);
assert.match(body, /dialog-action-danger/, method);
assert.match(body, /dialog-action-commit/, method);
}
});
test('editor tray golden contract covers every adaptive width in English and Russian', () => {
const tray = GOLDEN_SCENARIOS.filter((scenario) => scenario.editorTray);
assert.equal(tray.length, 6);
const widths = {
wide: (scenario) => scenario.viewport.width >= 1000,
medium: (scenario) => scenario.viewport.width >= 600 && scenario.viewport.width < 1000,
narrow: (scenario) => scenario.viewport.width < 600,
};
for (const [name, predicate] of Object.entries(widths)) {
const languages = new Set(tray.filter(predicate).map((scenario) => scenario.language));
assert.deepEqual(languages, new Set(['en', 'ru']), name);
}
assert.deepEqual(new Set(tray.map((scenario) => scenario.editorTray)), new Set([
'plan-selection', 'plan-tool', 'group', 'decor-selection',
'furniture-palette', 'decor-tool',
]));
});
test('issue 159 goldens cover both palette levels and every plan-art compatibility class', () => {
const furniture = GOLDEN_SCENARIOS.filter((scenario) => scenario.furniturePalette);
assert.deepEqual(new Set(furniture.map((scenario) => scenario.theme)), new Set(['dark', 'light']));
assert.deepEqual(new Set(furniture.map((scenario) => scenario.furniturePalette)),
new Set(['categories', 'variants']));
const plan = GOLDEN_SCENARIOS.find((scenario) => scenario.id === 'furniture-plan-art-dark');
assert.deepEqual(new Set(plan.decorOverride.filter((shape) => shape.kind === 'furniture')
.map((shape) => shape.symbol)),
new Set(['sofa', 'fridge', 'sofa_corner_right']));
});
test('issue 606 isolates both corrected drawings and the saved exercise alias', () => {
const scenario = GOLDEN_SCENARIOS.find((item) => item.id === 'furniture-corrected-art-light');
assert.equal(scenario?.mode, 'view');
assert.equal(scenario?.fixture, 'visual');
assert.deepEqual(scenario.decorOverride.map((shape) => shape.symbol),
['cactus', 'exercise', 'bookshelf', 'shelf_floor']);
assert.ok(scenario.decorOverride.every((shape) => shape.kind === 'furniture'));
assert.equal(scenario.decorOverride[0].flip_h, true);
assert.equal(scenario.decorOverride[2].flip_v, true);
});
test('issue 359 has one deterministic light-theme furniture placement preview golden', () => {
const scenario = GOLDEN_SCENARIOS.find((item) => item.id === 'furniture-placement-preview-light');
assert.ok(scenario);
assert.equal(scenario.mode, 'decor');
assert.equal(scenario.theme, 'light');
assert.equal(scenario.capture, 'stage');
assert.deepEqual(scenario.furniturePlacementPreview, {
symbol: 'sofa', widthCm: 180, depthCm: 90, pointer: [0.35, 0.90],
});
});
test('issue 383 goldens cover every furniture mirror orientation in light and dark themes', () => {
const scenarios = GOLDEN_SCENARIOS.filter((item) => item.id.startsWith('furniture-transform-'));
assert.equal(scenarios.length, 2);
assert.deepEqual(new Set(scenarios.map((item) => item.theme)), new Set(['light', 'dark']));
for (const scenario of scenarios) {
const furniture = scenario.decorOverride.filter((shape) => shape.kind === 'furniture');
const orientations = furniture.map((shape) =>
`${shape.flip_h ? 'h' : ''}${shape.flip_v ? 'v' : ''}` || 'normal');
assert.deepEqual(new Set(orientations), new Set(['normal', 'h', 'v', 'hv']), scenario.id);
assert.ok(furniture.every((shape) => shape.symbol === 'sofa_corner_right'));
}
const selected = scenarios.find((scenario) => scenario.decorSelection);
assert.equal(selected?.theme, 'light');
assert.equal(selected?.mode, 'decor');
assert.equal(selected?.decorSelection, 'furn-flip-hv');
});
test('device dialog goldens expose the complete light-source controls at desktop and mobile widths', () => {
const dialogs = GOLDEN_SCENARIOS.filter((scenario) => scenario.id.startsWith('device-dialog-'));
assert.equal(dialogs.length, 3);
assert.deepEqual(new Set(dialogs.map((scenario) => scenario.language)), new Set(['en', 'ru', 'de']));
for (const scenario of dialogs) {
assert.equal(scenario.deviceLightControls, true, scenario.id);
assert.equal(scenario.deviceId, 'golden-light-two', scenario.id);
assert.equal(scenario.capture, 'page', scenario.id);
assert.equal(scenario.viewport.height >= 1000, true, scenario.id);
}
const harness = readFileSync(new URL('../demo/golden/harness.mjs', import.meta.url), 'utf8');
assert.match(harness, /if \(scenario\.deviceLightControls\)/,
'the harness must activate the declared light-controls scenario flag');
});
test('issue #199 golden candidates cover the blocked Optimize dialog in both themes', () => {
const scenarios = GOLDEN_SCENARIOS.filter((scenario) =>
scenario.id.startsWith('optimize-preflight-dialog-'));
assert.equal(scenarios.length, 2);
assert.deepEqual(new Set(scenarios.map((scenario) => scenario.theme)), new Set(['dark', 'light']));
assert.deepEqual(new Set(scenarios.map((scenario) => scenario.language)), new Set(['en', 'ru']));
for (const scenario of scenarios) {
assert.equal(scenario.dialog, 'optimize-preflight');
assert.equal(scenario.capture, 'page');
}
});
test('golden harness neutralizes the shared pointer before every scenario', () => {
const harness = readFileSync(new URL('../demo/golden/harness.mjs', import.meta.url), 'utf8');
const reset = harness.indexOf('await page.mouse.move(0, 0);');
const fixture = harness.indexOf('const fixture = prepareGoldenFixture(scenario);', reset);
assert.equal(reset >= 0, true, 'the shared Playwright pointer must be reset');
assert.equal(fixture > reset, true, 'pointer reset must happen before fixture adoption');
});
test('toggle-entity dialog goldens cover selected and stale states across themes and widths', () => {
const dialogs = GOLDEN_SCENARIOS.filter((scenario) =>
scenario.id.startsWith('toggle-entity-dialog-'));
assert.equal(dialogs.length, 2);
assert.deepEqual(new Set(dialogs.map((scenario) => scenario.deviceToggleEntity)),
new Set(['selected', 'stale']));
assert.deepEqual(new Set(dialogs.map((scenario) => scenario.language)), new Set(['en', 'ru']));
assert.deepEqual(new Set(dialogs.map((scenario) => scenario.theme)), new Set(['light', 'dark']));
assert.equal(dialogs.some((scenario) => scenario.viewport.width < 600), true);
assert.equal(dialogs.some((scenario) => scenario.viewport.width >= 1000), true);
});
test('help affordance golden covers an open text-bearing surface in both themes', () => {
const help = GOLDEN_SCENARIOS.filter((scenario) => scenario.openHelp);
assert.equal(help.length >= 2, true);
assert.deepEqual(new Set(help.map((scenario) => scenario.theme)), new Set(['dark', 'light']));
for (const scenario of help) {
assert.equal(scenario.helpTextRegion?.key, scenario.openHelp, scenario.id);
assert.equal(scenario.helpTextRegion?.minPixels >= 30, true, scenario.id);
}
});
test('issue 86 help goldens render browser zoom 200% in both themes', () => {
const zoom = GOLDEN_SCENARIOS.filter((scenario) =>
scenario.id.startsWith('settings-help-zoom-200-'));
assert.equal(zoom.length, 2);
assert.deepEqual(new Set(zoom.map((scenario) => scenario.theme)), new Set(['light', 'dark']));
assert.deepEqual(new Set(zoom.map((scenario) => scenario.language)), new Set(['en', 'ru']));
for (const scenario of zoom) {
assert.equal(scenario.deviceScaleFactor, 2);
assert.deepEqual(scenario.viewport, { width: 390, height: 900 });
assert.equal(scenario.dialog, 'general-help');
assert.equal(scenario.openHelp, 'gs.bg_mode.help');
assert.equal(scenario.capture, 'page');
}
});
test('issue 43 support dialog has the six reviewed responsive states', () => {
const support = GOLDEN_SCENARIOS.filter((scenario) => scenario.dialog === 'support');
assert.equal(support.length, 6);
assert.deepEqual(new Set(support.map((scenario) => scenario.supportState)), new Set([
'empty', 'preview', 'validation', 'success', 'relay-error',
]));
assert.deepEqual(new Set(support.map((scenario) => scenario.theme)), new Set(['light', 'dark']));
assert.equal(support.some((scenario) => scenario.viewport.width === 320), true);
assert.equal(support.some((scenario) => scenario.viewport.width === 390), true);
assert.equal(support.some((scenario) => scenario.viewport.width === 768), true);
assert.equal(support.filter((scenario) => scenario.viewport.width >= 900).length, 3);
for (const scenario of support) assert.equal(scenario.capture, 'page', scenario.id);
});