mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-28 19:01:34 +00:00
§5 SPEC.md референса. Семь карточек #598 остались, менялось содержимое: - Display — две полные строки с иконкой, подписью и тумблером справа; пояснение радара по §5.2 стало подписью строки (текст прежний, ключ переехал в ленивый словарь как gs.radar_show_live_hint, «?» у строки больше нет); - Zigbee links — заголовок и «?» функции (topology.help) рисует карточка, а блок hp-zigbee-topology-settings получил режим embedded: строка-тумблер, callout «сохраните настройку», подзаголовки ZHA / Zigbee2MQTT, поле тем и строки действий контролами набора. Заголовок больше не удваивается (дефект 1). Лист набора вносится в теневой корень компонента; токены --hpf-* наследуются с хоста. Прежний render без embedded не тронут — его держит контракт #459; - Room fill colors — три подзаголовка (Lights / Temperature / Zigbee signal, Q6) и плитки цвета по три в ряд: название на свотче, hex, число прозрачности; свотч — trigger прежнего hp-color-opacity (Q5); - Light-source glow — две плитки и поле радиуса с единицей внутри рамки; - Plan — плашка цвета стен, сегмент фона, при Static — плашка фона с Reset и подсказкой (тема / свой цвет); - Sun — gs.sun_missing остаётся callout'ом (К5); север: поле градусов с «°», ссылка Clear, «N» и компас 44 px справа — индикатор набора, но тянуть его по-прежнему можно (smoke_sun гоняет настоящий drag; старый 120-px SVG и его CSS в plan.styles.ts удалены); лучи — строка-тумблер с подписью; источник лучей — сегмент (sun-settings-view.ts переписан с <select> на segmented, логика та же); - Data — подзаголовки с «?» и строки действий набора. К10 (Q1): Save только при изменениях и без ошибок (радиус > 0, север 0–359), статус в футере, вопрос при закрытии с изменениями; снимок при открытии — editors/general-form-state.ts поверх общего editors/dialog-baseline.ts (на него же переведён space-form-state.ts). _settingsDialog не получил новых ключей. Удалено мёртвое: _renderColorRow (рантайм и карточка), .sunrow/.suncol, ключи gs.wall_group, gs.bg_group, gs.bg_theme, gs.radar_show_live.help(.aria) в четырёх локалях. Девять новых ключей — в i18n/settings. Смоки переписаны осознанно, с пометкой #600: smoke_general_settings (инвентарь по классам набора, подзаголовки .hpf-sub h4, пояснения под «?» и подписью, ни одного .rhint), smoke_sun (сегмент источника лучей вместо select, поле #gs-north, radius drag 18 px), smoke_backup_transfer (подзаголовок и строка действий), smoke_settings_dialog_cards (callout вместо .rhint). Новый фокусный свидетель demo/smoke_general_settings_form.mjs: семь карточек и отсутствие старой разметки, один заголовок у Zigbee, десять плиток, компас 44, цели 44, запись плиток/плашки/радиуса/севера/сегментов в свои ключи, Save/dirty/вопрос при закрытии. Мутанты: general-save-enabled-without-changes, general-tile-opacity-writes-a-neighbour-key, general-north-clear-writes-zero, topology-embedded-draws-its-own-heading; якоря state-callout-hidden-under-help и dialog-card-loses-its-heading переставлены на новую разметку. Контрактные тесты: houseplan-source.mjs приводит `host.` к `this.` и в телах вынесенных диалогов; color-picker — три явных showOpacity=true (плитки, плашка стены, цвет комнаты); счётчик производных .aria 35 → 34. Бюджет: стартовый граф 290 711 — потолок ОПУЩЕН 292 500 → 292 200 (ушёл 120-px компас из eager-листа и мёртвые ключи); ленивый редакторский 237 929 — потолок 236 300 → 238 600 с разбором в bundle-budget.mjs. Гейты: typecheck, build, bundle:sync, npm test 2792 pass, mutation-gate --check, семь смоков «Общих», smoke_color_picker_consumers без правок. Issue: #600 User-Visible: no
141 lines
5.7 KiB
JavaScript
141 lines
5.7 KiB
JavaScript
import { launch, checkAll, finish } from './serve.mjs';
|
|
|
|
const { page, browser } = await launch();
|
|
const result = await page.evaluate(async () => {
|
|
const card = window.__card;
|
|
const root = () => card.shadowRoot || card.renderRoot;
|
|
card._openSettingsDialog();
|
|
await card.updateComplete;
|
|
// #600 §5.1: подзаголовок карточки «Данные» — `.hpf-sub h4`, кнопки — в
|
|
// строке действий набора под ним.
|
|
const groupNode = [...root().querySelectorAll('.hpf-sub h4')]
|
|
.find((node) => node.textContent.trim() === card._t('gs.backup_group'));
|
|
const group = !!groupNode;
|
|
const actions = groupNode?.closest('.hpf-sub')?.nextElementSibling?.querySelectorAll('.btn').length ?? 0;
|
|
const importTrigger = root().querySelector('.backupupload > button');
|
|
importTrigger?.focus();
|
|
const keyboardImport = importTrigger?.tagName === 'BUTTON'
|
|
&& importTrigger.tabIndex === 0
|
|
&& importTrigger.matches(':focus');
|
|
|
|
card._openBackupExport();
|
|
await card.updateComplete;
|
|
const exportRadios = root().querySelectorAll('input[name="backup-kind"]').length;
|
|
const exportWarning = !!root().querySelector('.backupwarn');
|
|
const planOnlyHiddenForFull = !root().querySelector('.backupplanonly');
|
|
root().querySelector('input[name="backup-kind"][value="space"]')?.click();
|
|
await card.updateComplete;
|
|
const planOnly = root().querySelector('.backupplanonly input[type="checkbox"]');
|
|
planOnly?.click();
|
|
await card.updateComplete;
|
|
const planOnlySpace = {
|
|
visible: !!planOnly,
|
|
checked: card._backupExportDialog?.planOnly === true,
|
|
labelled: root().querySelector('.backupplanonly')?.textContent
|
|
.includes(card._t('backup.plan_only')) === true,
|
|
};
|
|
root().querySelector('input[name="backup-kind"][value="full"]')?.click();
|
|
await card.updateComplete;
|
|
const planOnlyResetForFull = card._backupExportDialog?.planOnly === false
|
|
&& !root().querySelector('.backupplanonly');
|
|
|
|
let sentExport = null;
|
|
card.hass = {
|
|
...card.hass,
|
|
callWS: async (message) => {
|
|
sentExport = message;
|
|
return { document: { smoke: true }, filename: 'plan-only-smoke.json' };
|
|
},
|
|
};
|
|
card._backupExportDialog = {
|
|
kind: 'space', planOnly: true, busy: false, error: '',
|
|
};
|
|
await card._runBackupExport();
|
|
const planOnlyRequest = {
|
|
kind: sentExport?.kind,
|
|
planOnly: sentExport?.plan_only,
|
|
space: sentExport?.space_id,
|
|
};
|
|
|
|
card._backupExportDialog = null;
|
|
card._backupImportDialog = {
|
|
filename: 'backup.json', size: 1024, token: 'opaque',
|
|
preview: {
|
|
kind: 'full', source: 'foreign', created_at: '2026-08-11T00:00:00Z',
|
|
counts: { spaces: 1, rooms: 2, markers: 3, layout: 4 },
|
|
confirmation_required: true,
|
|
content: [{ url: '/api/houseplan/content/plans/_/floor.svg', state: 'detach_required' }],
|
|
},
|
|
expectedConfigRev: 1, expectedLayoutRev: 2, duplicatePolicy: 'skip',
|
|
confirmMissing: false, busy: false, error: '',
|
|
};
|
|
await card.updateComplete;
|
|
const apply = root().querySelector('hp-dialog .btn.danger');
|
|
const importSafe = {
|
|
danger: !!apply,
|
|
disabledUntilConfirmed: !!apply?.disabled,
|
|
noHorizontalOverflow: root().querySelector('hp-dialog').scrollWidth
|
|
<= root().querySelector('hp-dialog').clientWidth,
|
|
};
|
|
card._backupImportDialog = {
|
|
filename: 'houseplan-space-plan-only.json', size: 2048, token: 'plan-only',
|
|
preview: {
|
|
kind: 'space', plan_only: true, source: 'same',
|
|
created_at: '2026-08-17T00:00:00Z', space_title: 'Ground (2)',
|
|
counts: { spaces: 1, rooms: 2, markers: 0, layout: 2 },
|
|
bindings: { device: 0, entity: 0, virtual: 0, active: 0, disabled: 0, missing: 0 },
|
|
duplicates: 0, confirmation_required: false, content: [],
|
|
repaired_target_refs: 2, preserved_unresolved_refs: 1,
|
|
reference_report: {
|
|
remapped: {
|
|
incoming: { 'layout.space': 2 }, target: { 'marker.space': 2 },
|
|
},
|
|
collisions: {}, preservedUnresolved: { 'marker.room_id': 1 },
|
|
droppedIncomingLinks: {}, boundedLineages: 0,
|
|
examples: [{
|
|
bucket: 'preservedUnresolved', category: 'marker.room_id',
|
|
owner: 'legacy-marker', reference: 'room_old_deadbeef',
|
|
}],
|
|
},
|
|
},
|
|
expectedConfigRev: 1, expectedLayoutRev: 2, duplicatePolicy: 'skip',
|
|
confirmMissing: false, busy: false, error: '',
|
|
};
|
|
await card.updateComplete;
|
|
const planOnlyPreview = {
|
|
visible: root().querySelector('.backupplanonlystatus')?.textContent
|
|
=== card._t('backup.plan_only_preview'),
|
|
noDuplicatePolicy: !root().querySelector('.backupchoices'),
|
|
noHorizontalOverflow: root().querySelector('hp-dialog').scrollWidth
|
|
<= root().querySelector('hp-dialog').clientWidth,
|
|
hasReferenceReport: !!root().querySelector('.backupdetails')
|
|
&& root().querySelector('.backupdetails')?.textContent
|
|
.includes(card._t('backup.import_details')),
|
|
warnsWithoutDiscarding: root().querySelector('.backupwarn')?.textContent
|
|
.includes(card._t('backup.preserved_unresolved_hint')) === true,
|
|
};
|
|
return {
|
|
group, actions, keyboardImport, exportRadios, exportWarning,
|
|
planOnlyHiddenForFull, planOnlySpace, planOnlyResetForFull, planOnlyRequest,
|
|
importSafe, planOnlyPreview,
|
|
};
|
|
});
|
|
|
|
checkAll(result, {
|
|
group: true,
|
|
actions: 2,
|
|
keyboardImport: true,
|
|
exportRadios: 2,
|
|
exportWarning: true,
|
|
planOnlyHiddenForFull: true,
|
|
planOnlySpace: { visible: true, checked: true, labelled: true },
|
|
planOnlyResetForFull: true,
|
|
planOnlyRequest: { kind: 'space', planOnly: true, space: 'f1' },
|
|
importSafe: { danger: true, disabledUntilConfirmed: true, noHorizontalOverflow: true },
|
|
planOnlyPreview: {
|
|
visible: true, noDuplicatePolicy: true, noHorizontalOverflow: true,
|
|
hasReferenceReport: true, warnsWithoutDiscarding: true,
|
|
},
|
|
});
|
|
await finish(browser, result);
|