Files
houseplan-card/demo/smoke_value_face_source.mjs
T
Codexandclaude[bot] 223f499e37 fix: audit-lows batch from the v1.69.0 audit (#385)
(a) a same-binding click in the marker dialog is a no-op: the value
source and badge reset only on an actual change of binding (#378 §1.6) —
both the candidate list and the virtual radio.
(b) rewriteMarkerControlReferences no longer plants value_badge /
value_source keys as undefined on markers that never had them.
(v) the expensive release diff proof (2 git-show per src file) runs only
for commits the SAME shared predicate classifies as release — both
disjuncts, including the Release: trailer.
(g) the paired neutralisation formats in space export are documented in
place and pinned by a combined badge+value_source pytest.

User-Visible: yes
Issue: #385
2026-08-30 08:24:40 +00:00

197 lines
7.9 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.
// Issue #378: an explicit Value + state source is selected in the real dialog,
// previewed, saved, reopened and projected by both renderers without changing
// the device action. Missing live data must keep the selection and render a dash.
import { launch, checkAll, finish } from './serve.mjs';
const { page, browser } = await launch({ width: 1100, height: 900 }, 1);
const res = await page.evaluate(async () => {
const c = window.__card;
const sr = () => c.shadowRoot || c.renderRoot;
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const entityId = 'cover.value_face_awning';
const deviceId = 'd_value_face_awning';
const source = { kind: 'entity_attribute', entity_id: entityId, attribute: 'current_position' };
const sourceKey = `attr:${entityId}:current_position`;
const calls = [];
const setState = async (state, position) => {
c.hass = {
...c.hass,
devices: {
...c.hass.devices,
[deviceId]: {
id: deviceId, name: 'Value face awning', model: 'Test cover', area_id: 'bedroom',
identifiers: [['demo', deviceId]], entry_type: null, via_device_id: null,
},
},
entities: {
...c.hass.entities,
[entityId]: { entity_id: entityId, device_id: deviceId, platform: 'demo' },
},
states: {
...c.hass.states,
[entityId]: {
entity_id: entityId, state,
attributes: {
friendly_name: 'Value face awning', device_class: 'awning',
supported_features: 15, current_position: position,
},
},
},
callService: async (domain, service, data) => {
calls.push([domain, service, data]);
return {};
},
};
c._regSignature = '';
c._maybeRebuildDevices();
const item = c._devices.find((candidate) => candidate.bindingRef === deviceId);
if (item) c._layout = { ...c._layout, [item.id]: { s: item.space, x: 0.12, y: 0.88 } };
c.requestUpdate();
await c.updateComplete;
};
const item = () => c._devices.find((candidate) => candidate.bindingRef === deviceId);
const faceValue = (root, selector) => root.querySelector(selector)
?.querySelector('.valtext')?.textContent?.trim() || '';
await setState('open', 42);
c._setMode('devices');
c._openMarkerDialog(item());
await c.updateComplete;
c._markerDialog = {
...c._markerDialog, display: 'value', tapAction: 'toggle', tapActionTouched: true,
};
c.requestUpdate();
await c.updateComplete;
const select = sr().querySelector('#marker-value-source');
const candidateValues = [...(select?.options || [])].map((option) => option.value);
select.value = sourceKey;
select.dispatchEvent(new Event('change', { bubbles: true, composed: true }));
await c.updateComplete;
const preview = sr().querySelector('hp-device-preview');
await preview?.updateComplete;
const preview42 = faceValue(preview?.renderRoot, '.dev') === '42 %';
const draftSourceExact = JSON.stringify(c._markerDialog?.valueSource) === JSON.stringify(source);
await c._saveMarker();
await c.updateComplete;
let saved = (c._serverCfg.markers || []).find((marker) => marker.binding === `device:${deviceId}`);
const savedExact = JSON.stringify(saved?.value_source) === JSON.stringify(source);
const actionSaved = saved?.tap_action === 'toggle';
c._openMarkerDialog(item());
await c.updateComplete;
const reopenedSelect = sr().querySelector('#marker-value-source');
const reopenedExact = reopenedSelect?.value === sourceKey
&& reopenedSelect?.selectedOptions?.[0]?.value === sourceKey;
// Cancel an unsaved switch to auto: the persisted source must survive.
reopenedSelect.value = '';
reopenedSelect.dispatchEvent(new Event('change', { bubbles: true, composed: true }));
await c.updateComplete;
c._closeMarkerDialog();
await c.updateComplete;
saved = (c._serverCfg.markers || []).find((marker) => marker.binding === `device:${deviceId}`);
const cancelKeptSource = JSON.stringify(saved?.value_source) === JSON.stringify(source);
c._setMode('view');
c._regSignature = '';
c._maybeRebuildDevices();
c.requestUpdate();
await c.updateComplete;
const plan42 = faceValue(sr(), `.dev[data-id="${item().id}"]`) === '42 %';
calls.length = 0;
c._clickDevice({ stopPropagation() {} }, item());
await wait(20);
const actionUnchanged = JSON.stringify(calls.at(-1)) === JSON.stringify([
'cover', 'close_cover', { entity_id: entityId },
]);
await customElements.whenDefined('houseplan-space-card');
const staticCard = document.createElement('houseplan-space-card');
staticCard.setConfig({ type: 'custom:houseplan-space-card', space: item().space });
staticCard._snap = {
config: structuredClone(c._serverCfg), rev: c._cfgRev,
configFingerprint: `fixture-${c._cfgRev}`,
layout: structuredClone(c._layout), layoutRev: c._layoutRev,
layoutFingerprint: `fixture-${c._layoutRev}`,
};
staticCard._loadedOnce = true;
staticCard.hass = c.hass;
document.body.appendChild(staticCard);
const started = Date.now();
while (!staticCard.renderRoot?.querySelector('.hp-static-stage') && Date.now() - started < 6000) {
await wait(60);
}
await staticCard.updateComplete;
const static42 = faceValue(staticCard.renderRoot, `.dev[data-id="${item().id}"]`) === '42 %';
await setState('unavailable', 42);
c._setMode('view');
c.requestUpdate();
await c.updateComplete;
await wait(120);
await c.updateComplete;
const unavailableDash = faceValue(sr(), `.dev[data-id="${item().id}"]`) === '—';
const unavailableStillValue = sr().querySelector(`.dev[data-id="${item().id}"]`)?.classList
.contains('valonly') === true;
await setState('open', 55);
c._setMode('view');
c.requestUpdate();
await c.updateComplete;
const recovered55 = faceValue(sr(), `.dev[data-id="${item().id}"]`) === '55 %';
// #385(а) AC1: a SAME-binding click is a no-op — the configured source and
// badge survive; only an actual change resets them (spec #378 §1.6).
c._setMode('devices');
c._openMarkerDialog(item());
await c.updateComplete;
c._markerDialog = { ...c._markerDialog, valueSource: source, valueSourceTouched: true, bindingOpen: true };
await c.updateComplete;
const badgeBefore = JSON.stringify({
e: c._markerDialog?.valueBadgeEnabled, s: c._markerDialog?.valueBadgeSource,
});
sr().querySelector('.cand.sel')?.click();
await c.updateComplete;
const sameBindingKeepsSource =
JSON.stringify(c._markerDialog?.valueSource) === JSON.stringify(source)
&& !c._markerDialog?.bindingOpen
&& JSON.stringify({
e: c._markerDialog?.valueBadgeEnabled, s: c._markerDialog?.valueBadgeSource,
}) === badgeBefore;
c._closeMarkerDialog();
// A real binding choice resets the old source to automatic before Save.
c._setMode('devices');
c._openMarkerDialog(item());
await c.updateComplete;
const virtualBinding = [...sr().querySelectorAll('input[name="bmode"]')]
.find((node) => node.parentElement?.textContent?.includes(c._t('marker.virtual_option')));
virtualBinding?.click();
await c.updateComplete;
const bindingResetToAuto = !!virtualBinding && c._markerDialog?.binding === 'virtual'
&& c._markerDialog?.valueSource === null
&& c._markerDialog?.valueSourceTouched === true;
// #385(а) AC2: re-clicking the ALREADY active virtual binding is a no-op too
c._markerDialog = { ...c._markerDialog, valueSource: { kind: 'sentinel-385' } };
await c.updateComplete;
virtualBinding?.click();
await c.updateComplete;
const sameVirtualKeepsSource =
c._markerDialog?.valueSource?.kind === 'sentinel-385'
&& c._markerDialog?.binding === 'virtual';
c._closeMarkerDialog();
return {
candidatePresent: candidateValues.includes(sourceKey),
preview42, draftSourceExact, savedExact, actionSaved, reopenedExact,
cancelKeptSource, plan42, static42, actionUnchanged,
unavailableDash, unavailableStillValue, recovered55, bindingResetToAuto,
sameBindingKeepsSource, sameVirtualKeepsSource,
};
});
checkAll(res);
await finish(browser, res);