mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-29 03:09:36 +00:00
Release v1.62.0-beta.6 candidate
This commit is contained in:
@@ -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
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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 }]),
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
Vendored
+3
-3
File diff suppressed because one or more lines are too long
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
- Все виды активности устройств сведены в одну предсказуемую систему:
|
||||
|
||||
@@ -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
@@ -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. |
|
||||
|
||||
@@ -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
|
||||
|
||||
Generated
+2
-2
@@ -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
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 },
|
||||
|
||||
Reference in New Issue
Block a user