mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-29 03:09:36 +00:00
- ru/de/fr трёх словарей пространств — девять ленивых чанков (по одному на пару «пространство × язык»), английский статически как слой отката; рантайм — тот же LanguageRuntime (две попытки, отпечаток сборки, fallback, общий тост отказа), src/i18n/namespace-language.ts. - Загрузчики рантаймов онбординга и редактора отдают поверхность в рендер только после того, как словарь текущего языка осел; гейт хоста учитывает словари загруженной на нём поверхности (смена языка на лету — ветка warm). Оверлей Zigbee ждёт topology до первой отрисовки. - bundle-manifest: граф lazyNamespaceLocaleFiles, девять retry-токенов со строгим счётом «ровно один». bundle-budget: потолок онбординга 28 500 (замер 27 470), редактор 245 400 → 228 900 (замер 227 869), проверки ленивости и владения по содержимому. - Смок smoke_lazy_admin_locale (AC4–AC7); регресс-смоки, переключавшие язык синхронно, ждут гейт вместо ослабления проверок; пять мутантов. - monolith-baseline bundleBytes 2 500 387 → 2 508 676: +10 чанков (rollup intro и export на каждый) и записи манифеста; gzip каждой поверхности при этом уменьшился. Issue: #627 User-Visible: no
177 lines
7.0 KiB
JavaScript
177 lines
7.0 KiB
JavaScript
// #103: Toggle confirmation explains the current snapshot and the exact
|
|
// nextEffect while execution still resolves the current intent again.
|
|
import { launch, checkAll, finish } from './serve.mjs';
|
|
|
|
const { page, browser } = await launch({ width: 390, height: 760 });
|
|
const out = await page.evaluate(async () => {
|
|
const result = {};
|
|
const card = window.__card;
|
|
const root = () => card.shadowRoot || card.renderRoot;
|
|
const calls = [];
|
|
const coverState = (state, entityId = 'cover.gate', deviceClass = 'curtain') => ({
|
|
entity_id: entityId,
|
|
state,
|
|
attributes: {
|
|
friendly_name: entityId === 'cover.gate'
|
|
? 'Very long living-room curtain name that must wrap on a phone'
|
|
: 'Other curtain',
|
|
device_class: deviceClass,
|
|
supported_features: 11,
|
|
},
|
|
});
|
|
const setCover = async (state, extraStates = {}, deviceClass = 'curtain') => {
|
|
card.hass = {
|
|
...card.hass,
|
|
states: { ...card.hass.states, 'cover.gate': coverState(state, 'cover.gate', deviceClass), ...extraStates },
|
|
services: {
|
|
...card.hass.services,
|
|
cover: {
|
|
...card.hass.services?.cover,
|
|
open_cover: {}, close_cover: {}, stop_cover: {}, toggle: {},
|
|
},
|
|
},
|
|
// The demo fallback returns raw states; #103 must still localize the
|
|
// known vocabulary instead of exposing those tokens in confirmation.
|
|
formatEntityState: (entity) => entity.state,
|
|
callService: async (domain, service, data) => {
|
|
calls.push([domain, service, data]);
|
|
return {};
|
|
},
|
|
};
|
|
card.requestUpdate();
|
|
await card.updateComplete;
|
|
};
|
|
const setLanguage = async (language) => {
|
|
card._config = { ...card._config, language };
|
|
card.requestUpdate();
|
|
await card.updateComplete;
|
|
// #627: the live switch holds the previous frame until the lazy editor
|
|
// dictionaries of this language settle (inert + aria-busy).
|
|
for (let i = 0; i < 250 && card.hasAttribute('aria-busy'); i++) await new Promise((r) => setTimeout(r, 20));
|
|
await card.updateComplete;
|
|
};
|
|
const rebuildGate = async () => {
|
|
card._serverCfg = {
|
|
...card._serverCfg,
|
|
markers: [{
|
|
id: 'm_gate', binding: 'device:d_gate', tap_action: 'cover',
|
|
tap_confirm: true, display: 'icon_ripple',
|
|
}],
|
|
};
|
|
card._cfgEpoch++;
|
|
card._regSignature = '';
|
|
card._maybeRebuildDevices();
|
|
card._space = 'garden';
|
|
card._setMode('view');
|
|
card.requestUpdate();
|
|
await card.updateComplete;
|
|
};
|
|
const gate = () => card._devices.find((item) => item.bindingRef === 'd_gate');
|
|
const tap = async () => {
|
|
card._clickDevice({ stopPropagation() {} }, gate());
|
|
await card.updateComplete;
|
|
return card._tapConfirm;
|
|
};
|
|
const renderedLines = () => [...root().querySelectorAll('.tapconfirm-line')]
|
|
.map((node) => node.textContent.trim());
|
|
|
|
await setLanguage('en');
|
|
await setCover('closed');
|
|
await rebuildGate();
|
|
|
|
const english = await tap();
|
|
result.englishSnapshotIsStructured = english?.kind === 'toggle'
|
|
&& english.initialIntent?.nextEffect === 'open'
|
|
&& english.deviceId === gate()?.id;
|
|
result.englishCurrentExpected = JSON.stringify(renderedLines()) === JSON.stringify([
|
|
'Current state: Closed', 'After switching: Open',
|
|
]);
|
|
const groupLines = card._toggleConfirmationLines({
|
|
origin: 'explicit-toggle', kind: 'group', semantics: 'group-power',
|
|
targets: [
|
|
{ entityId: 'switch.one', name: 'One', state: 'on', via: 'control-entity' },
|
|
{ entityId: 'light.two', name: 'Two', state: 'off', via: 'control-entity' },
|
|
],
|
|
skippedTargets: [{
|
|
ref: 'switch.missing', entityId: 'switch.missing', name: 'Missing', reason: 'unavailable',
|
|
}],
|
|
noneReason: null, nextEffect: 'turn-off',
|
|
command: {
|
|
domain: 'homeassistant', service: 'turn_off',
|
|
data: { entity_id: ['switch.one', 'light.two'] },
|
|
},
|
|
});
|
|
const virtualLines = card._toggleConfirmationLines({
|
|
origin: 'explicit-toggle', kind: 'single', semantics: 'power',
|
|
targets: [{ entityId: '', name: 'Virtual lamp', state: 'on', via: 'virtual-light' }],
|
|
skippedTargets: [], noneReason: null, nextEffect: 'turn-off', command: null,
|
|
operation: { kind: 'virtual-light', markerId: 'virtual-lamp' },
|
|
});
|
|
result.englishGroupAndVirtualCopy = JSON.stringify(groupLines) === JSON.stringify([
|
|
'Current state: on 1 of 2', 'After switching: all are off', 'Unavailable: 1',
|
|
]) && JSON.stringify(virtualLines) === JSON.stringify([
|
|
'Current state: On', 'After switching: Off',
|
|
]);
|
|
const body = root().querySelector('.tapconfirm-body');
|
|
const dialog = root().querySelector('hp-dialog');
|
|
const titleText = dialog?.shadowRoot?.querySelector('.title-text');
|
|
const footer = root().querySelector('hp-dialog [slot="footer"]');
|
|
const buttons = [...(footer?.querySelectorAll('button') || [])];
|
|
result.accessibleDomOrder = dialog?.title === english.text
|
|
&& body?.children[0]?.getAttribute('data-line') === '0'
|
|
&& body?.children[1]?.getAttribute('data-line') === '1'
|
|
&& buttons.length === 2;
|
|
result.narrowDialogDoesNotScrollHorizontally = !!body
|
|
&& body.scrollWidth <= body.clientWidth + 1
|
|
&& !!titleText && titleText.scrollWidth <= titleText.clientWidth + 1
|
|
&& buttons.every((button) => {
|
|
const rect = button.getBoundingClientRect();
|
|
return rect.left >= 0 && rect.right <= innerWidth + 1;
|
|
});
|
|
|
|
// Same target, changed state: execute the newly resolved direction.
|
|
calls.length = 0;
|
|
await setCover('open');
|
|
english.exec();
|
|
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
result.sameTargetUsesCurrentDirection = calls.length === 1
|
|
&& calls[0][0] === 'cover' && calls[0][1] === 'close_cover';
|
|
|
|
// Different operation target: preserve #94's cancellation/toast contract.
|
|
await setCover('closed');
|
|
const changed = await tap();
|
|
const beforeChange = gate();
|
|
const other = coverState('closed', 'cover.other');
|
|
card.hass = { ...card.hass, states: { ...card.hass.states, 'cover.other': other } };
|
|
card._devices = card._devices.map((item) => item.id === beforeChange.id ? {
|
|
...item,
|
|
primary: 'cover.other', entities: ['cover.other'], allEntities: ['cover.other'],
|
|
bindingKind: 'entity', bindingRef: 'cover.other',
|
|
marker: { ...item.marker, binding: 'entity:cover.other' },
|
|
} : item);
|
|
calls.length = 0;
|
|
changed.exec();
|
|
await card.updateComplete;
|
|
result.changedTargetCancels = calls.length === 0
|
|
&& card._toast === card._t('toast.tap_target_changed');
|
|
|
|
await setLanguage('ru');
|
|
await setCover('closed');
|
|
await rebuildGate();
|
|
await tap();
|
|
result.russianCurrentExpected = JSON.stringify(renderedLines()) === JSON.stringify([
|
|
'Текущее состояние: Закрыто', 'После переключения: Открыто',
|
|
]);
|
|
|
|
// A secure/no-operation resolver result must not open a confirmation.
|
|
card._tapConfirm = null;
|
|
await setCover('closed', {}, 'garage');
|
|
await tap();
|
|
result.noOperationDoesNotOpen = card._tapConfirm == null && calls.length === 0;
|
|
|
|
return result;
|
|
});
|
|
|
|
checkAll(out);
|
|
await finish(browser, out);
|