Files
houseplan-card/demo/smoke_value_face_source.mjs
T
Codex e561e5fc5d fix: drop the unreachable virtual-radio guard, keep honest evidence (#385 r2-M1)
An already-checked radio input fires no change event, so the same-binding
guard on the virtual branch was dead code and its smoke assert vacuous.
Removed both; a comment documents why the reset there is always
legitimate. The candidate-list guard (the reachable path) stays tested.

User-Visible: no
Issue: #385
2026-08-30 11:44:57 +03:00

193 lines
7.8 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(а) r2-M1: no AC for the virtual radio — an already-checked radio
// input fires no change event, so the reset branch is unreachable by a
// real click (verified by review r2); the guard and its vacuous assert
// were removed rather than kept as false evidence.
c._closeMarkerDialog();
return {
candidatePresent: candidateValues.includes(sourceKey),
preview42, draftSourceExact, savedExact, actionSaved, reopenedExact,
cancelKeptSource, plan42, static42, actionUnchanged,
unavailableDash, unavailableStillValue, recovered55, bindingResetToAuto,
sameBindingKeepsSource,
};
});
checkAll(res);
await finish(browser, res);