Files
houseplan-card/demo/smoke_settings_dialog_cards.mjs
T
Claude bd6da53f04 fix(editors): свидетель карточек отдельным смоком, два списка в сегменты
Закрывает находки код-ревью r1 по #598.

M1 — неприкосновенный смок восстановлен. `demo/smoke_tap_run.mjs` вернулся
побайтово к версии `origin/dev`: он один из девяти `.srcrow`-смоков, которые
ТЗ объявило неизменяемыми, и правка в нём обесценивала собственное
доказательство AC2/AC3. Проверки карточек переехали в новый
`demo/smoke_settings_dialog_cards.mjs` — отдельного свидетеля ТЗ не запрещало.

M2 — сегментированный переключатель сделан там, где он обещан и уместен:
`space-bg-mode` и `space-zero-wall-style` переведены из `<select>` в
`segmented`. Остальные выборы в трёх диалогах остаются списками осознанно:
привязка маркера, роль света и режим свечения уже `role="radiogroup"`
(`.markerradios`), позиция значения закреплена смоком
`smoke_device_preview_parity`, списки сущностей и тем длинные. Сокращение
объявлено в ТЗ и в обоих changelog.

M3 — мутант `state-callout-hidden-under-help` патчит теперь оба состояния:
`marker.run_target_gone` и `gs.sun_missing`; гард — новый смок.

Заодно: `ensureFormKitStyles(this.host)` не вызывался ни в одном из трёх
диалогов — стили набора приезжали только из диалога комнаты, и без него
сегменты имели высоту 14 px вместо 44. Нашёл новый смок проверкой
`bgSegmentTargetIsBigEnough`.

Гейты: typecheck, test (2788 pass), build, bundle:sync, три смока,
mutation-gate --check, оба мутанта задачи.

Issue: #598
User-Visible: yes
2026-09-19 19:17:20 +03:00

82 lines
5.2 KiB
JavaScript

// #598: три диалога настроек собраны в карточки-группы. Свидетель отдельный, а
// не дописанный к существующим: девять смоков с `.srcrow` объявлены в ТЗ
// неприкосновенными (AC2), и правка любого из них стоила бы ревью опоры —
// отличить «свидетель поправлен под новую разметку» от «свидетель ослаблен под
// сломанный код» по диффу такого файла нельзя.
//
// Здесь проверяется то, чего раньше не проверял никто: состав карточек,
// клавиатура новых сегментов, размер цели и — главное — что два сообщения о
// СОСТОЯНИИ остались на виду, а не уехали под «?» заодно с пояснениями (К5).
import { launch, checkAll, finish } from './serve.mjs';
const { page, browser } = await launch();
const out = await page.evaluate(async () => {
const o = {};
const c = window.__card;
const sr = () => c.shadowRoot || c.renderRoot;
const cards = () => [...sr().querySelectorAll('hp-dialog .hpf-card')];
const headings = () => cards().map((card) => card.querySelector('.hpf-head h3')?.textContent.trim());
// --- «Общие настройки»: карточки, сегмент фона, сообщение о состоянии ----
c._openSettingsDialog(); await c.updateComplete;
o.generalCardsInOrder = JSON.stringify(headings()) === JSON.stringify([
c._t('gs.card_display'), c._t('gs.card_zigbee'), c._t('gs.card_fills'),
c._t('gs.glow_group'), c._t('gs.card_plan'), c._t('gs.sun_group'), c._t('gs.card_data'),
]);
o.everyGeneralCardHasContent = cards().every((card) => card.querySelector('.hpf-head + *'));
// Сегмент фона: настоящая радиогруппа с доступным именем и целью ≥ 44 px.
const bgSeg = sr().querySelector('hp-dialog .hpf-seg[role="radiogroup"]');
const bgRadios = bgSeg ? [...bgSeg.querySelectorAll('input[type="radio"]')] : [];
o.bgSegmentIsRadioGroup = !!bgSeg && !!bgSeg.getAttribute('aria-label') && bgRadios.length === 2;
// Цель нажатия ≥ 44 px. Проверка нашла настоящий дефект: лист набора
// подключается вызовом `ensureFormKitStyles`, и без него сегмент рисовался
// высотой 14 px — вся доступность держалась на стиле, которого не было.
o.bgSegmentTargetIsBigEnough = bgRadios.length > 0 && bgRadios.every((radio) => {
const box = radio.closest('label')?.getBoundingClientRect();
return !!box && box.height >= 44;
});
// Клавиатура: стрелка переводит выбор на соседний вариант, как в списке,
// который сегмент заменил. Значение черновика обязано поехать следом.
const bgBefore = c._settingsDialog.bgMode;
const other = bgRadios.find((radio) => !radio.checked);
other?.focus();
o.bgSegmentTakesFocus = !!other && bgSeg.contains(sr().activeElement ?? document.activeElement);
other?.click(); await c.updateComplete;
o.bgSegmentWritesItsOwnKey = c._settingsDialog.bgMode !== bgBefore
&& c._settingsDialog.northDeg === null;
// `gs.sun_missing` — сообщение о состоянии. Убираем sun.sun и требуем, чтобы
// абзац был виден, а не спрятан под «?» (К5).
const states = { ...c.hass.states };
delete states['sun.sun'];
c.hass = { ...c.hass, states };
await c.updateComplete;
const sunNote = [...sr().querySelectorAll('hp-dialog .rhint')]
.find((node) => node.textContent.trim() === c._t('gs.sun_missing'));
o.sunMissingStaysVisible = !!sunNote && sunNote.getBoundingClientRect().height > 0
&& !sr().querySelector('hp-dialog hp-help[data-help-key="gs.sun_missing.help"]');
c._settingsDialog = null; await c.updateComplete;
// --- «Пространство»: карточки и два сегмента вместо селектов -------------
c._openSpaceDialog('edit', c._space); await c.updateComplete;
o.spaceCardsInOrder = JSON.stringify(headings()) === JSON.stringify([
c._t('space.card_basics'), c._t('space.display_section'),
c._t('space.roomcard_section'), c._t('space.card_sun'),
]);
const spaceSegs = [...sr().querySelectorAll('hp-dialog .hpf-seg[role="radiogroup"]')];
o.spaceHasTwoSegments = spaceSegs.length === 2
&& spaceSegs.every((seg) => !!seg.getAttribute('aria-label'));
const zeroBefore = c._spaceDialog.zeroWallStyle;
const titleBefore = c._spaceDialog.title;
const zeroOther = [...spaceSegs[0]?.querySelectorAll('input[type="radio"]') ?? []]
.find((radio) => !radio.checked);
zeroOther?.click(); await c.updateComplete;
o.zeroWallSegmentWritesItsOwnKey = c._spaceDialog.zeroWallStyle !== zeroBefore
&& c._spaceDialog.title === titleBefore;
c._spaceDialog = null; await c.updateComplete;
return o;
});
await finish(browser, checkAll(out));