mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-29 03:09:36 +00:00
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
193 lines
7.8 KiB
JavaScript
193 lines
7.8 KiB
JavaScript
// 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);
|