Release v1.62.0-beta.9 candidate

Issue: #108
User-Visible: yes
This commit is contained in:
Matysh
2026-08-12 21:49:29 +03:00
parent 9bb5f7c5a8
commit 8e2973fa7a
40 changed files with 779 additions and 393 deletions
+29
View File
@@ -0,0 +1,29 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import {
terminalTrailers,
validateCommitMessage,
} from '../scripts/validate-commit-provenance.mjs';
test('provenance accepts positive issues and one visibility trailer at the end', () => {
const message = `Fix relay\n\nIssue: #94\nIssue: #98\nUser-Visible: yes\n`;
assert.deepEqual(validateCommitMessage(message), []);
assert.deepEqual(terminalTrailers(message).get('Issue'), ['#94', '#98']);
});
test('provenance ignores trailer-like prose and rejects zero or duplicate visibility', () => {
assert.notDeepEqual(validateCommitMessage('Issue: #12\n\nExplanation after it'), []);
assert.notDeepEqual(validateCommitMessage('Fix\n\nIssue: #0\nUser-Visible: no'), []);
assert.notDeepEqual(validateCommitMessage(
'Fix\n\nIssue: #12\nUser-Visible: no\nUser-Visible: yes',
), []);
});
test('golden files require exact release-review provenance', () => {
const changed = ['demo/golden/baselines/example.png'];
const base = 'Update baseline\n\nIssue: #75\nUser-Visible: no';
assert.equal(validateCommitMessage(base, changed).length, 2);
assert.deepEqual(validateCommitMessage(
`${base}\nRelease: v1.2.3-beta.1\nBaseline-Reviewed: https://example.test/run`, changed,
), []);
});
+15 -1
View File
@@ -1,7 +1,8 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import {
deviceFaceStyle, legacySupplementalMetrics, lqiClassName, valueBadgeClassName,
deviceFaceStyle, legacySupplementalMetrics, lqiClassName, renderDeviceFace,
valueBadgeClassName,
} from '../test-build/device-face.js';
const face = (rippleColor) => ({
@@ -59,3 +60,16 @@ test('value badge classes cover all four positions and only bottom displaces LQI
}
assert.equal(lqiClassName(null), 'lqi');
});
test('rendered pulse retains the documented activity-ring compatibility hook', () => {
const presentation = {
...face('#ff9800'), classes: [], icon: 'mdi:lightbulb', angle: 0,
valueText: null, valueFullText: null, valueBadge: null,
lqiText: null, lqiColor: null, haDisabled: false,
tempText: null, humText: null,
};
const root = renderDeviceFace(presentation, { surface: 'preview' });
const nested = root.values.find((value) => value?.strings);
assert.ok(nested);
assert.match(nested.strings.join(''), /device-pulse activity-ring/);
});
+35 -2
View File
@@ -102,6 +102,21 @@ test('passive sensor source keeps its normal scalar value and never probes marke
assert.deepEqual(hits, []);
});
test('secondary humidity diagnostics do not create a legacy humidity satellite', () => {
const h = hass({
'switch.relay': state('switch.relay', 'on'),
'sensor.relay_humidity': state('sensor.relay_humidity', '49', {
device_class: 'humidity', unit_of_measurement: '%',
}),
});
const relay = device({
entities: ['switch.relay', 'sensor.relay_humidity'],
primary: 'switch.relay',
});
const presentation = resolveDevicePresentation(h, relay, options);
assert.equal(presentation.humText, null);
});
test('derived marker-state badge follows a stateful Always source', () => {
const h = hass({ 'light.bulb': state('light.bulb', 'on') });
const bulb = device({
@@ -443,7 +458,7 @@ test('derived temperature and humidity retain compact plan formatting', () => {
assert.equal(humidity.valueText, '48%');
});
test('one legacy marker keeps both temperature and humidity for face compatibility', () => {
test('temperature-led composite marker does not add a legacy humidity satellite', () => {
const h = hass({
'sensor.temp': state('sensor.temp', '22.4', { device_class: 'temperature', unit_of_measurement: '°C' }),
'sensor.humidity': state('sensor.humidity', '47.7', { device_class: 'humidity', unit_of_measurement: '%' }),
@@ -457,7 +472,7 @@ test('one legacy marker keeps both temperature and humidity for face compatibili
}), options);
assert.equal(result.valueBadge?.text, '22.4°');
assert.equal(result.tempText, '22.4');
assert.equal(result.humText, '48');
assert.equal(result.humText, null);
});
test('preview explanations distinguish activity display and composite Power source', () => {
@@ -658,3 +673,21 @@ test('an incidental cover does not hijack the light face of a mixed device', ()
assert.equal(explicitCover.sourceKind, 'cover');
assert.equal(explicitCover.visualSources[0].eid, 'cover.mixed');
});
test('an incidental cover does not hijack another whole-device role on info tap', () => {
const h = hass({
'media_player.screen': state('media_player.screen', 'playing'),
'cover.screen': state('cover.screen', 'opening', {
device_class: 'curtain', supported_features: 15,
}),
}, {
'media_player.screen': { entity_id: 'media_player.screen', device_id: 'd1', platform: 'demo' },
'cover.screen': { entity_id: 'cover.screen', device_id: 'd1', platform: 'demo' },
});
const result = resolvePresentationSources(h, device({
entities: ['cover.screen', 'media_player.screen'],
primary: 'media_player.screen', tapAction: 'info',
}));
assert.equal(result.sourceKind, 'device_role');
assert.deepEqual(result.visualSources.map((source) => source.eid), ['media_player.screen']);
});
+19
View File
@@ -160,6 +160,25 @@ test('passive forced-light marker is driven by the controller own relay and de-d
assert.equal(intent.targets[0].via, 'control-marker-driver');
});
test('passive marker driver matches the light graph without explicit light flags', () => {
const h = hass({
'switch.wall': state('switch.wall', 'off'),
'light.indicator': state('light.indicator', 'on'),
});
const lamp = device({
id: 'lamp', name: 'Dumb lamp', tapAction: null, primary: undefined, entities: [],
marker: { id: 'lamp', binding: 'virtual', is_light: true, controls: [] },
});
const controller = device({
id: 'wall', bindingKind: 'device', bindingRef: 'wall', primary: 'switch.wall',
entities: ['switch.wall', 'light.indicator'], controls: ['marker:lamp'],
marker: { id: 'wall', binding: 'device:wall', controls: ['marker:lamp'] },
});
const intent = resolveToggleIntent({ hass: h, devices: [controller, lamp], device: controller });
assert.deepEqual(toggleCommandEntityIds(intent.command), ['switch.wall']);
assert.equal(intent.targets[0].via, 'control-marker-driver');
});
test('legacy cover keeps cover priority, ignores controls and uses open/close/stop semantics', () => {
const h = hass({
'light.mixed': state('light.mixed', 'on'),
+18
View File
@@ -999,6 +999,24 @@ test('resolvedLightSources: marker room_id is more precise than a shared HA area
);
});
test('room-scoped light graph keeps a source controlled from another room', () => {
const hass = { states: { 'light.bed': { state: 'on' } } };
const source = {
id: 'bed-light', area: 'bedroom', primary: 'light.bed', entities: ['light.bed'],
marker: { id: 'bed-light', room_id: 'bedroom-room', is_light: true },
};
const remote = {
id: 'hall-switch', area: 'hall', primary: 'switch.hall', entities: ['switch.hall'],
controls: ['light.bed'], marker: { id: 'hall-switch', room_id: 'hall-room', controls: ['light.bed'] },
};
const roomSources = resolvedLightSources(
hass, [remote, source], { id: 'bedroom-room', area: 'bedroom' },
);
assert.deepEqual(roomSources.map((item) => ({ eid: item.eid, owner: item.device.id })), [
{ eid: 'light.bed', owner: 'bed-light' },
]);
});
test('self-control is not external and only explicit is_light makes the switch shine', () => {
assert.deepEqual(
persistedExternalControls('entity:switch.hood', [
+19 -1
View File
@@ -36,6 +36,11 @@ test('golden matrix has stable unique ids and bounded comparison thresholds', ()
&& scenario.openingPreviewPixels.minChannelDelta > 0
&& scenario.openingPreviewPixels.minChannelDelta <= 32, 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.tunnelContinuity) {
assert.match(scenario.tunnelContinuity.openingId, /^[a-z0-9-]+$/, scenario.id);
assert.equal(Number.isInteger(scenario.tunnelContinuity.insetPx)
@@ -145,10 +150,23 @@ test('opening placement golden requires browser-painted preview pixels', () => {
assert.equal(scenario.mode, 'plan');
assert.equal(scenario.openingPreview?.type, 'door');
assert.deepEqual(scenario.openingPreview?.pointer, [0.48, 0.65]);
assert.equal(scenario.openingPreviewPixels.minPixels >= 40, true);
assert.equal(scenario.openingPreviewPixels.minPixels >= 150, 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('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);
+19
View File
@@ -0,0 +1,19 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import {
helpHasContent,
helpScrollShouldDismiss,
} from '../test-build/help-behavior.js';
test('help is absent unless both visible copy and an accessible name exist', () => {
assert.equal(helpHasContent('Explanation', 'More information'), true);
assert.equal(helpHasContent(' ', 'More information'), false);
assert.equal(helpHasContent('Explanation', ''), false);
assert.equal(helpHasContent(null, 'More information'), false);
});
test('only scrolling the owning dialog outside the floating surface dismisses help', () => {
assert.equal(helpScrollShouldDismiss(true, true), false);
assert.equal(helpScrollShouldDismiss(false, true), true);
assert.equal(helpScrollShouldDismiss(false, false), false);
});
-32
View File
@@ -1,32 +0,0 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
const cardSource = readFileSync(new URL('../src/houseplan-card.ts', import.meta.url), 'utf8');
const secondarySource = readFileSync(new URL('../src/editor-secondary.ts', import.meta.url), 'utf8');
test('split commit materialises the old wall profile before replacing room outlines', () => {
const materialiseAt = cardSource.indexOf('const splitWalls = wasSplit');
const mutateAt = cardSource.indexOf('main.poly = this._pendingSplit.mainPoly');
const normaliseAt = cardSource.indexOf('this._normalizeWalls(splitWalls');
assert.ok(materialiseAt >= 0 && mutateAt > materialiseAt && normaliseAt > mutateAt);
assert.match(cardSource.slice(materialiseAt, mutateAt), /materializeWallIntervals\([\s\S]*pendingSplit/);
});
test('spatial glow rejects both wall masonry and independent physical bodies', () => {
assert.match(
cardSource,
/pointInOpaquePlanBody\(sourcePoint, masonryGeometry, physical\)/,
);
});
test('late editor permission is adopted through the mode state machine', () => {
assert.doesNotMatch(cardSource, /this\._mode\s*=\s*this\._pendingNavMode/);
assert.match(cardSource, /this\._pendingNavMode = null;\s*this\._setMode\(pendingMode, false\)/);
});
test('editor navigation stays actionable while transient editor surfaces dismiss', () => {
assert.match(cardSource, /data-editor-navigation="view"/);
assert.match(secondarySource, /hasAttribute\('data-editor-navigation'\)/);
assert.match(secondarySource, /if \(onNavigation\)[\s\S]*return false;/);
});
-23
View File
@@ -1,23 +0,0 @@
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import test from 'node:test';
const source = readFileSync(new URL('../src/hp-help.ts', import.meta.url), 'utf8');
test('empty help renders no trigger and cannot create dialog overflow geometry', () => {
assert.match(source, /if \(!this\._hasContent\(\)\) return nothing/);
assert.match(source, /\.sr-only\s*\{[\s\S]*?position:\s*fixed/);
assert.doesNotMatch(source, /class="sr-only"[^>]*\?hidden/);
});
test('help owns global listeners only while its surface is open', () => {
assert.match(source, /private _subscribeOpenListeners\(\)[\s\S]*?ownerDocument\.addEventListener\('keydown'/);
assert.match(source, /private _unsubscribeOpenListeners\(\)[\s\S]*?ownerDocument\.removeEventListener\('keydown'/);
assert.match(source, /this\._open = true;\s*this\._subscribeOpenListeners\(\)/);
assert.match(source, /this\._open = false;\s*this\._unsubscribeOpenListeners\(\)/);
});
test('help surface is keyboard focusable and its own scroll does not dismiss it', () => {
assert.match(source, /role="tooltip" aria-hidden="true" tabindex="0"/);
assert.match(source, /if \(this\._floating\.containsPath\(event\.composedPath\(\)\)\) return/);
});
+18
View File
@@ -124,6 +124,24 @@ test('Grid snap and soft centre magnet are deterministic', () => {
assert.equal(resolve({ pointer: [34, 0] }).measure.guide, null);
});
test('centre magnet wins over grid on a wall whose midpoint is off-grid', () => {
const offGridWall = interval({ b: [105, 0] });
const centred = resolve({ pointer: [50, 0], intervals: [offGridWall] });
assert.equal(centred.x, 52.5);
assert.deepEqual(centred.measure.guide, { x: 52.5, y: 0, angle: 0 });
const awayFromCentre = resolve({ pointer: [43, 0], intervals: [offGridWall] });
assert.equal(awayFromCentre.x, 40);
assert.equal(awayFromCentre.measure.guide, null);
});
test('non-wall geometry cannot become an opening placement target', () => {
assert.deepEqual(openingPlacementTargets([
interval({ key: 'partition-shape', kind: null, open: false }),
interval({ key: 'column-shape', kind: null, open: false, a: [40, 40], b: [60, 40] }),
]), []);
});
test('Junction tie-break does not depend on interval input order', () => {
const horizontal = interval({ key: 'a-horizontal' });
const vertical = interval({ key: 'b-vertical', a: [50, -50], b: [50, 50] });
+41
View File
@@ -0,0 +1,41 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import {
openingVisibleMetrics,
renderOpeningVisibleGeometry,
} from '../test-build/render/opening-symbol.js';
const spec = (patch = {}) => ({
type: 'door', length: 100, angle: 0, amount: 0,
flipH: false, flipV: false, base: '#000', tone: '#000',
cellCm: 10, gridPitch: 20,
face: { ox: 0, oy: 0, cm: 0, side: -1 },
...patch,
});
const templateText = (value) => {
if (!value || !value.strings) return '';
return value.strings.join('') + value.values.map(templateText).join('');
};
test('opening metrics expand hit and outline zones with a thick wall face', () => {
assert.deepEqual(openingVisibleMetrics(spec()), {
half: 50, jambHalf: 4, gateDepth: 0, outlineHalf: 16, hitHalf: 20,
});
assert.deepEqual(openingVisibleMetrics(spec({ face: { ox: 0, oy: 20, cm: 20, side: 1 } })), {
half: 50, jambHalf: 20, gateDepth: 0, outlineHalf: 28, hitHalf: 30,
});
});
test('shared renderer emits the expected visible symbol for every opening type', () => {
const windowText = templateText(renderOpeningVisibleGeometry(spec({
type: 'window', face: { ox: 0, oy: 20, cm: 20, side: 1 },
})));
const doorText = templateText(renderOpeningVisibleGeometry(spec({ type: 'door' })));
const gateText = templateText(renderOpeningVisibleGeometry(spec({ type: 'gate' })));
assert.match(windowText, /op-glass/);
assert.match(doorText, /op-leaf/);
assert.match(doorText, /op-arc/);
assert.equal((gateText.match(/op-leaf/g) || []).length, 2);
assert.ok(openingVisibleMetrics(spec({ type: 'gate' })).gateDepth > 0);
});
+1 -1
View File
@@ -770,7 +770,7 @@ test('split materialisation cuts a partial shared interval at the new divider',
key: wallKey([0, 0], [6, 0], pitch), a: [0, 0], b: [6, 0], cm: 15,
}];
const preserved = materializeWallIntervals(
original, walls, [], pitch, cellCm, GRID_PITCH, 1, [[4, 0], [4, 10]],
original, walls, [], pitch, cellCm, GRID_PITCH,
);
const split = [
{ id: 'source', poly: [[4, 0], [10, 0], [10, 10], [4, 10]] },