Release v1.62.0-beta.6 candidate

This commit is contained in:
Matysh
2026-08-12 14:07:09 +03:00
parent 6d0f97ef82
commit 36e81e9fb1
22 changed files with 214 additions and 40 deletions
+1 -1
View File
@@ -45,7 +45,7 @@ PLAN_ORPHAN_TTL_S = 3600
SCHEDULED_GRACE_S = 30 * 24 * 3600
FILES_DIR = "houseplan/files"
CONF_ADMIN_ONLY = "admin_only"
VERSION = "1.62.0-beta.5"
VERSION = "1.62.0-beta.6"
# Portable backup format. This is deliberately independent from the Home
# Assistant Store version above: storage migrations and files exported by a
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -16,5 +16,5 @@
"issue_tracker": "https://github.com/Matysh/houseplan-card/issues",
"requirements": [],
"single_config_entry": true,
"version": "1.62.0-beta.5"
"version": "1.62.0-beta.6"
}
+3 -3
View File
@@ -131,9 +131,9 @@ const runtime = () => {
attributes: { azimuth: 180, elevation: 24 },
},
};
entities['sun.sun'] = {
entity_id: 'sun.sun', device_id: null, platform: 'sun', disabled_by: null,
};
// Keep sun.sun state-only on purpose. Core/runtime entities and YAML
// entities without unique_id may have a live state without a registry row.
// The production projection must preserve them.
const layout = {};
const areas = Object.fromEntries(
[...geometryRooms, ...lightingRooms].map((room) => [room.area, { area_id: room.area, name: room.name }]),
+4 -3
View File
@@ -1,8 +1,8 @@
{
"schema": 1,
"matrixVersion": 12,
"acceptedAt": "2026-08-12T10:10:45.897Z",
"sourceFingerprint": "117e675d4314f3ef3d16b7d94885f953bbc856da645e488f37045ef5a04716eb",
"matrixVersion": 13,
"acceptedAt": "2026-08-12T10:53:18.675Z",
"sourceFingerprint": "0581cf065ba06734ce9bd00277f3fbe18483ac09914323070e321cba319cdc68",
"chromium": "151.0.7922.34",
"scenarios": {
"geometry-view-dark-fit": "a538deed6141b98b3e396d7da1024b18aee345312edd7954ad458751f08f48a3",
@@ -21,6 +21,7 @@
"openings-filled-tunnel-dark": "074fb859d2ae1dc2ccd2045e2d6fb6d8389e430cbef20287419b8840cbbd5094",
"openings-hidden-view-dark": "c85cc04d1d8622b98215e2bb83f5bb233a7cfb0ac684c912475ef7bc44245897",
"lighting-glow-sun-dark": "a98eee332f25a43c8d9d126c118c8cfea4ee73752b1060227548efedaf0efcdd",
"lighting-sun-window-state-only-dark": "3bd581a23a2e0ebba58530db5182adea5cba6ec10bc032bee024415c19108a17",
"lighting-fill-light-axis-split-dark": "4f867528aeb9124229f81659876b03ff297a3a7a92bc57ffb32d5c24913c4938",
"lighting-fill-temp-axis-split-dark": "e0535b70701c9fe6753f74c943b9f288a0fb1a23a9cfc8590d4945e0a1724eb5",
"lighting-fill-lqi-axis-split-dark": "485ab183144913569ddc11154553ab4ac7db522ab188de74d652b717dc786d9c",
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

+22 -2
View File
@@ -411,22 +411,42 @@ export async function prepareGoldenScenario(page, scenario) {
.some((help) => help.renderRoot?.querySelector('.trigger')?.getAttribute('aria-expanded') === 'true'),
editorTray: card.renderRoot.querySelector('.editor-secondary-host.open .editor-secondary')
?.className || '',
...(scenario.sunRayPixels ? { sun: {
raw: card.hass?.states?.['sun.sun']?.attributes || null,
plan: card._planHass?.states?.['sun.sun']?.attributes || null,
render: card._renderPlanHass?.states?.['sun.sun']?.attributes || null,
north: card._effNorth(),
enabled: card._effSunRays(),
editing: card._editing,
cachedRays: card._sunRaysCache?.rays?.length || 0,
} } : {}),
};
}, { fixture, scenario });
}
export async function goldenClip(page, capture) {
if (capture === 'page') return null;
return page.evaluate(() => {
return page.evaluate((captureKind) => {
const card = window.__goldenCard;
const target = card?.renderRoot?.querySelector('.stage');
if (!target) throw new Error('golden stage capture target missing');
const rect = target.getBoundingClientRect();
if (captureKind === 'sun-window') {
// Stable crop around the exterior window and the first part of its ray:
// deliberately excludes room labels and device markers, whose font/icon
// rasterisation would add noise unrelated to the visual contract.
return {
x: Math.max(0, Math.floor(rect.left + rect.width * 0.10)),
y: Math.max(0, Math.floor(rect.top + rect.height * 0.02)),
width: Math.max(1, Math.ceil(rect.width * 0.35)),
height: Math.max(1, Math.ceil(rect.height * 0.25)),
};
}
const pad = 2;
const x = Math.max(0, Math.floor(rect.left - pad));
const y = Math.max(0, Math.floor(rect.top - pad));
const right = Math.min(window.innerWidth, Math.ceil(rect.right + pad));
const bottom = Math.min(window.innerHeight, Math.ceil(rect.bottom + pad));
return { x, y, width: Math.max(1, right - x), height: Math.max(1, bottom - y) };
});
}, capture);
}
+10 -1
View File
@@ -1,10 +1,11 @@
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 = 12;
export const GOLDEN_MATRIX_VERSION = 13;
const stage = { capture: 'stage', threshold: { maxChannelDelta: 10, maxDiffRatio: 0.0005 } };
const page = { capture: 'page', threshold: { maxChannelDelta: 10, maxDiffRatio: 0.0008 } };
const sunWindow = { capture: 'sun-window', threshold: { maxChannelDelta: 10, maxDiffRatio: 0.001 } };
export const GOLDEN_SCENARIOS = Object.freeze([
{ id: 'geometry-view-dark-fit', fixture: 'visual', space: 'golden-geometry', mode: 'view',
@@ -57,6 +58,14 @@ export const GOLDEN_SCENARIOS = Object.freeze([
fillMode: 'none', glowEnabled: false, hideOpenings: true, theme: 'dark', viewport: { width: 1000, height: 900 }, ...stage },
{ id: 'lighting-glow-sun-dark', fixture: 'visual', space: 'golden-lighting', mode: 'view',
theme: 'dark', viewport: { width: 1000, height: 900 }, ...stage },
{ id: 'lighting-sun-window-state-only-dark', fixture: 'visual', space: 'golden-lighting', mode: 'view',
// The golden screenshot is backed by a second, sun-layer-hidden capture.
// A real painted ray must account for enough changed pixels; DOM-only
// presence or an accidentally accepted empty baseline is not sufficient.
glowEnabled: false, allLightsOff: true,
stateOverrides: { 'sun.sun': { attributes: { azimuth: 0, elevation: 24 } } },
sunRayPixels: { minPixels: 500, minChannelDelta: 4 },
theme: 'dark', viewport: { width: 1000, height: 900 }, ...sunWindow },
{ id: 'lighting-fill-light-axis-split-dark', fixture: 'visual', space: 'golden-lighting', mode: 'view',
fillMode: 'light', glowEnabled: false, theme: 'dark', viewport: { width: 1000, height: 900 }, ...stage },
{ id: 'lighting-fill-temp-axis-split-dark', fixture: 'visual', space: 'golden-lighting', mode: 'view',
+79 -2
View File
@@ -110,6 +110,69 @@ async function comparePng(page, actual, baseline, threshold) {
});
}
/** Capture the identical visual frame with only the rendered sun-ray SVG
* hidden. Comparing this control frame with the reviewed golden proves that
* the layer changes real browser pixels, not merely that its DOM exists. */
async function captureWithoutSunRays(page, screenshotOptions) {
const layerState = await page.evaluate(async () => {
const layer = window.__goldenCard?.renderRoot?.querySelector('.sunlayer');
if (!layer) throw new Error('semantic golden sun layer is missing');
const shapes = layer.querySelectorAll('path, polygon').length;
if (!shapes) throw new Error('semantic golden sun layer has no painted shapes');
const previous = layer.style.visibility;
layer.style.visibility = 'hidden';
await new Promise((done) => requestAnimationFrame(() => requestAnimationFrame(done)));
return { previous, shapes };
});
try {
return { png: await page.screenshot(screenshotOptions), shapes: layerState.shapes };
} finally {
await page.evaluate(async (previous) => {
const layer = window.__goldenCard?.renderRoot?.querySelector('.sunlayer');
if (layer) layer.style.visibility = previous;
await new Promise((done) => requestAnimationFrame(() => requestAnimationFrame(done)));
}, layerState.previous);
}
}
async function countChangedPixels(page, actual, control, spec) {
return page.evaluate(async ({ actual64, control64, minChannelDelta }) => {
const decode = async (base64) => {
const bytes = Uint8Array.from(atob(base64), (char) => char.charCodeAt(0));
return createImageBitmap(new Blob([bytes], { type: 'image/png' }));
};
const [actualImage, controlImage] = await Promise.all([decode(actual64), decode(control64)]);
if (actualImage.width !== controlImage.width || actualImage.height !== controlImage.height)
throw new Error('semantic golden sun control has different dimensions');
const canvas = document.createElement('canvas');
const controlCanvas = document.createElement('canvas');
canvas.width = controlCanvas.width = actualImage.width;
canvas.height = controlCanvas.height = actualImage.height;
const context = canvas.getContext('2d', { willReadFrequently: true });
const controlContext = controlCanvas.getContext('2d', { willReadFrequently: true });
context.drawImage(actualImage, 0, 0);
controlContext.drawImage(controlImage, 0, 0);
const pixels = context.getImageData(0, 0, canvas.width, canvas.height).data;
const controlPixels = controlContext.getImageData(0, 0, canvas.width, canvas.height).data;
let changed = 0;
let maxDelta = 0;
for (let offset = 0; offset < pixels.length; offset += 4) {
const delta = Math.max(
Math.abs(pixels[offset] - controlPixels[offset]),
Math.abs(pixels[offset + 1] - controlPixels[offset + 1]),
Math.abs(pixels[offset + 2] - controlPixels[offset + 2]),
);
maxDelta = Math.max(maxDelta, delta);
if (delta >= minChannelDelta) changed++;
}
return { changed, maxDelta, size: [canvas.width, canvas.height] };
}, {
actual64: actual.toString('base64'),
control64: control.toString('base64'),
minChannelDelta: spec.minChannelDelta,
});
}
/** Assert scenario semantics against the actual capture, not only its data.
* This protects a reviewed-but-empty baseline from becoming the reference. */
async function countWarmPixels(page, png, region) {
@@ -251,16 +314,30 @@ try {
result.runtime = await prepareGoldenScenario(page, scenario);
if (pageErrors.length) throw new Error(`browser exception: ${pageErrors.join(' | ')}`);
const clip = await goldenClip(page, scenario.capture);
const actual = await page.screenshot({
const screenshotOptions = {
...(clip ? { clip } : {}),
animations: 'disabled',
caret: 'hide',
scale: 'css',
});
};
const actual = await page.screenshot(screenshotOptions);
const actualPath = resolve(actualRoot, `${scenario.id}.png`);
writeFileSync(actualPath, actual);
result.actualSha256 = sha256(actual);
result.actual = actualPath;
if (scenario.sunRayPixels) {
const control = await captureWithoutSunRays(page, screenshotOptions);
const sample = await countChangedPixels(page, actual, control.png, scenario.sunRayPixels);
result.sunRayShapes = control.shapes;
result.sunRayChangedPixels = sample.changed;
result.sunRayMaxChannelDelta = sample.maxDelta;
if (sample.changed < scenario.sunRayPixels.minPixels) {
throw new Error(
`semantic golden assertion failed: sun rays paint ${sample.changed} pixels, expected at least `
+ `${scenario.sunRayPixels.minPixels}`,
);
}
}
if (scenario.warmPixelRegion) {
const sample = await countWarmPixels(page, actual, scenario.warmPixelRegion);
result.warmPixels = sample.warm;
File diff suppressed because one or more lines are too long
+3 -3
View File
File diff suppressed because one or more lines are too long
+12
View File
@@ -2,6 +2,18 @@
## Unreleased
## v1.62.0-beta.6 — 2026-08-12
- Fixed the remaining cause of invisible window sun rays in real Home Assistant
installations. Core/runtime entities such as `sun.sun` may have a live state
without an Entity Registry row; House Plan now preserves that state while
continuing to exclude explicitly disabled entities and devices
([#102](https://github.com/Matysh/houseplan-card/issues/102)).
- Added a deterministic visual regression crop of an exterior window and its
ray. The browser now compares the reviewed image with the same frame after
hiding only the sun layer and fails unless the ray paints a material number
of pixels, so an empty screenshot cannot be accepted as success.
## v1.62.0-beta.5 — 2026-08-12
- Unified all device activity around one predictable pulse system. Alerts use
+13
View File
@@ -8,6 +8,19 @@
## Unreleased
## v1.62.0-beta.6 — 2026-08-12
- Исправлена оставшаяся причина невидимых солнечных лучей из окон в реальных
установках Home Assistant. У системных/runtime-сущностей вроде `sun.sun`
может быть актуальное состояние без строки в Entity Registry; House Plan
теперь сохраняет такое состояние, продолжая исключать явно деактивированные
сущности и устройства
([#102](https://github.com/Matysh/houseplan-card/issues/102)).
- Добавлен детерминированный визуальный эталон внешнего окна и солнечного луча.
Браузер сравнивает проверенный кадр с тем же кадром, где скрыт только слой
солнца, и требует заметного количества реально нарисованных пикселей — пустой
скриншот больше нельзя принять как успешный результат.
## v1.62.0-beta.5 — 2026-08-12
- Все виды активности устройств сведены в одну предсказуемую систему:
+5 -9
View File
@@ -1,18 +1,14 @@
<!-- release: v1.62.0-beta.5 -->
<!-- release: v1.62.0-beta.6 -->
## Основное
- Все виды активности устройств объединены в понятную систему пульсаций без статичных колец.
- Переходы между просмотром и редакторами теперь плавно меняют панель, рабочую область, масштаб и фон.
- Восстановлено отображение солнечных лучей из окон.
- Исправлено отображение солнечных лучей из окон в реальных установках Home Assistant ([#102](https://github.com/Matysh/houseplan-card/issues/102)).
- Мелкие исправления и улучшения.
## Highlights
- All device activity now uses one predictable pulse system without static rings.
- View/editor transitions now animate the toolbar, workspace, camera and background together.
- Restored window sun rays after a transition regression.
- Fixed window sun rays in real Home Assistant installations ([#102](https://github.com/Matysh/houseplan-card/issues/102)).
- Small fixes and improvements.
[Полный список изменений на русском](https://github.com/Matysh/houseplan-card/blob/v1.62.0-beta.5/docs/CHANGELOG.ru.md)
· [Full changelog in English](https://github.com/Matysh/houseplan-card/blob/v1.62.0-beta.5/docs/CHANGELOG.md)
[Полный список изменений на русском](https://github.com/Matysh/houseplan-card/blob/v1.62.0-beta.6/docs/CHANGELOG.ru.md)
· [Full changelog in English](https://github.com/Matysh/houseplan-card/blob/v1.62.0-beta.6/docs/CHANGELOG.md)
+2 -2
View File
@@ -21,8 +21,8 @@ metadata). Only an explicit owner-approved emergency hotfix may skip this gate.
| Item | State |
|---|---|
| Version | **v1.62.0-beta.5** everywhere (manifest, const.py, package.json, CARD_VERSION) — prerelease candidate for issues #98–#102 |
| Current local cycle | v1.62.0-beta.5 unifies device activity pulses, adds one coordinated View/editor transition, fixes the help popover and value-badge selector regressions, and restores window sun rays after the transition-layer regression. Issue #89 remains specification-only. |
| Version | **v1.62.0-beta.6** everywhere (manifest, const.py, package.json, CARD_VERSION) — prerelease candidate for issue #102 |
| Current local cycle | v1.62.0-beta.6 fixes the remaining real-HA `sun.sun` projection regression and adds a browser-painted golden crop that fails when a window ray is absent. Issues #89 and #103 remain specification/backlog-only. |
| Workflow | Owner's rule since 2026-08-07: ordinary fixes/features are made **locally, without tests and without commits**. A requested pre-release gets a production build plus the smallest targeted unit/smoke set covering the changed surfaces, one tested `dev` commit/tag and a GitHub Release with `prerelease=true`; `main` stays untouched. The complete local frontend/backend/smoke gate runs only before a stable release, after which `main` is fast-forwarded to the exact tested `dev` SHA and the GitHub Release uses `prerelease=false`. Release bodies are short and bilingual (Russian first): only significant user changes get individual bullets, while minor/code-only work is grouped as `Мелкие исправления и улучшения` / `Small fixes and improvements`; every body ends with separate links to the Russian and English changelogs. Detailed RU/EN changelog bullets may link the corresponding closed GitHub Issues; open or partially delivered issues are never presented as shipped. Telegram announcements are sent only for stable releases; beta and RC publication is silent. `docs/RELEASE-NOTES.md` is the current canonical body instance; `npm run release:prerelease -- <tag> --issues=… --yes` is the primary local publication path and the manual `Publish prerelease` workflow is its GitHub-only equivalent once present on `main`. Nothing is copied to the home instance by hand |
| GitHub | https://github.com/Matysh/houseplan-card — [Issues](https://github.com/Matysh/houseplan-card/issues) are the canonical task records and the linked [Project v2](https://github.com/users/Matysh/projects/1) is the canonical priority/status view; both must stay current. `main` carries stable releases; pre-release tags may point directly at `dev`. Work lands on `dev` and is merged into `main` for a stable release, so `dev` is normally equal to or ahead of `main`, never behind. Push via SSH key `ha_jb` (remote git@github.com:…); API releases via the fine-grained PAT in `~/.git-credentials` (Contents R/W, issued 2026-07-23) |
| CI | Prerelease publication requires a green exact-SHA Validate: frontend/backend, smoke (including the #73 rAF frame sampler), golden, HACS/Hassfest and a short absolute-ceiling performance smoke. Obsolete same-ref Validate runs are cancelled. Full seven-sample base/candidate performance moved to `performance.yml` (`main` push, weekly, manual); stable release assets fail closed unless Validate and Full Performance are green for the exact tagged SHA and the stable-only CDP compositor screencast finds no empty/black presented frame. |
+4
View File
@@ -21,6 +21,10 @@ only — no entities are created, no services are called.
clockwise; `attributes.elevation` in degrees, negative below the
horizon). No `sun.sun` in the install → the whole feature stays
silent and the settings dialog says why.
- `sun.sun` is consumed from the live HA state machine. It is valid for
this core/runtime entity to have no Entity Registry row; registry
projection must preserve its live state while still excluding rows
that are explicitly disabled.
- Sun attributes update rarely (~30–120 s). Sun geometry is recomputed
ONLY when (azimuth, elevation) or the config change — never on every
`hass` tick. The wedge layer memoises on
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "houseplan-card",
"version": "1.62.0-beta.5",
"version": "1.62.0-beta.6",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "houseplan-card",
"version": "1.62.0-beta.5",
"version": "1.62.0-beta.6",
"license": "MIT",
"dependencies": {
"lit": "^3.1.3",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "houseplan-card",
"version": "1.62.0-beta.5",
"version": "1.62.0-beta.6",
"description": "Interactive house plan Lovelace card for Home Assistant",
"license": "MIT",
"type": "module",
+5 -1
View File
@@ -339,7 +339,11 @@ export function activeRegistryHass(hass: any, snapshot = haRegistrySnapshot(hass
}
for (const [eid, state] of Object.entries<any>(hass?.states || {})) {
const entity = snapshot.entities?.[eid];
if (snapshot.authoritative && !entity) continue;
// A complete Entity Registry is not a complete list of HA states: YAML
// entities without unique_id and core runtime entities such as sun.sun can
// legitimately have a live state without any registry row. Absence is
// therefore not evidence of disablement. Explicit disabled entity/device
// rows below remain authoritative and are still removed.
if (entity && !isRegistryEntryEnabled(entity)) continue;
const parent = entity?.device_id ? snapshot.devices?.[entity.device_id] : null;
if (snapshot.authoritative && entity?.device_id && !parent) continue;
+1 -1
View File
@@ -183,7 +183,7 @@ import {
import { renderOpeningTunnelFills } from './render/opening-tunnels';
import { safeStoredColor } from './color';
const CARD_VERSION = '1.62.0-beta.5';
const CARD_VERSION = '1.62.0-beta.6';
/** Keeps every previously valid scale at the maximum 20 cm grid scale lossless. */
const DECOR_TEXT_CM_MAX = 2000;
const CELL_CM_MIN = 0.1;
+26 -2
View File
@@ -10,7 +10,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'].includes(scenario.capture), 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);
@@ -21,6 +21,13 @@ test('golden matrix has stable unique ids and bounded comparison thresholds', ()
assert.equal(Number.isInteger(region.minPixels) && region.minPixels > 0, true, scenario.id);
assert.equal(region.minRedBlueDelta > 0, true, 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 (scenario.tunnelContinuity) {
assert.match(scenario.tunnelContinuity.openingId, /^[a-z0-9-]+$/, scenario.id);
assert.equal(Number.isInteger(scenario.tunnelContinuity.insetPx)
@@ -41,7 +48,7 @@ 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',
'warm-remount', 'dialog-mobile', 'color-popover', 'tray-wide', 'tray-medium', 'sun-window',
'tray-narrow', 'opaque-glow-two-doorways', 'filled-tunnel', 'backup-full', 'backup-space'])
assert.equal(ids.includes(token), true, token);
assert.equal(new Set(GOLDEN_SCENARIOS.map((scenario) => scenario.mode)).has('plan'), true);
@@ -84,6 +91,23 @@ test('doorway spill golden exposes the opaque-fill failure mode from issue 71',
assert.equal(scenario.warmPixelRegion?.x >= 0.5, 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(space.settings.sun_rays, true);
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);
});
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);
+14
View File
@@ -98,6 +98,20 @@ test('active projection removes stale states of disabled rows', () => {
assert.deepEqual(Object.keys(projected.states), ['switch.good']);
});
test('authoritative active projection keeps live states without registry rows', () => {
const state = {
entity_id: 'sun.sun', state: 'above_horizon',
attributes: { azimuth: 180, elevation: 35 },
};
const projected = activeRegistryHass(
{ devices: {}, entities: {}, states: { 'sun.sun': state } },
full({}, {}),
);
assert.equal(projected.states['sun.sun'], state);
assert.equal(projected.entities['sun.sun'], undefined);
});
test('limited active projection keeps a live entity when its parent row is unavailable', () => {
const entities = {
'switch.live': { entity_id: 'switch.live', device_id: 'not_exposed', disabled_by: null },