28 KiB
#489 — Объявленный data-hp-контракт для UI и E2E
- Issue: https://github.com/Matysh/houseplan-card/issues/489
- Тип / приоритет: feature + tests + docs / P2
- Трек: полный; задача охватывает несколько UI-поверхностей и вводит новый публичный DOM-контракт со стабильностью между релизами
- Оценка: пользовательская ценность 7/10; ценность для разработки 9/10; сложность 6/10; риск 6/10
- Связано: #486;
docs/STYLING-HOOKS.md,demo/smoke_styling_hooks.mjs
1. Сценарий
Персона — Home admin или разработчик интеграционного теста, который проверяет House Plan внутри настоящего Home Assistant. Поверхности — обычная dashboard card, отдельная sidebar panel #486, три редактора и общие диалоги. Момент — автоматическая проверка загрузки, навигации и основных действий перед выпуском, либо точечная настройка card-mod опытным пользователем.
Внешний тестовый репозиторий houseplan-e2e не имеет права читать приватные
поля экземпляра карточки, случайные CSS-классы или текущую вложенность DOM. Ему
нужны небольшие, объявленные и версионируемые точки привязки.
2. Что человек увидит до и после
До: интерфейс выглядит и работает как обычно, но внешний тест вынужден искать служебные элементы по нестабильным классам и не может честно определить, готова ли карточка.
После: интерфейс выглядит и работает ровно так же, а тесты и card-mod могут находить объявленные элементы и состояние через стабильные атрибуты.
3. Проблема
docs/STYLING-HOOKS.md уже обещает стабильность объектам самого плана:
устройствам, комнатам, проёмам, стенам, декору и вкладкам пространств. Но
служебные поверхности не покрыты единообразно:
- готовность читается из приватного
_booting, а режим — из_mode; - шапочные действия ищутся по
.settings-button,.pdf-button,.zbи другим классам; hp-dialogне сообщает назначение, а его основные действия не имеют общей семантической точки привязки;- тулбары редакторов, их инструменты, закрытие и вторичный трей не образуют объявленного контракта;
- empty state, toast и app bar sidebar panel не имеют стабильных селекторов;
- нет одного машиночитаемого источника, по которому внешний репозиторий может проверить допустимость своих селекторов;
- текущий smoke доказывает объектные styling hooks, но не новый test contract.
Одновременно в исходниках есть диагностические data-hp (iso-*,
zigbee-topology-*, resize-* и другие). Сам факт существования такого
атрибута не должен случайно превращать его в обещание совместимости.
4. Скоуп
- Расширить
docs/STYLING-HOOKS.mdотдельным разделом Test hooks на английском: назначение, селекторы, словари значений и политика стабильности. - Добавить
data-hpна уже существующие элементы шапки, редакторов, диалогов, empty state, toast и sidebar panel без добавления обёрток и без изменения CSS. - Публиковать на каждом корневом
ha-cardготовность и текущий режим. - Ввести машинный инвентарь
docs/data-hp-contract.jsonи unit-проверку его согласованности с исходниками. - Расширить
demo/smoke_styling_hooks.mjsдоказательствами DOM-состояний, которые нельзя подтвердить одним статическим поиском. - Там, где существующий smoke уже обращается к охваченному элементу по внутреннему классу, перевести его на объявленный hook; полный перенос всех остальных smoke не требуется.
- Добавить пользовательскую запись в оба changelog со ссылкой на #489.
5. Не входит
- изменение разметки, вложенности, визуальных стилей, размеров или расположения;
- новые кнопки, диалоги, тосты, действия или пользовательские настройки;
- изменение логики готовности, загрузки, редакторов или разрешений;
- backend seed/reset API и код внешнего
houseplan-e2e; - обещание стабильности для CSS-классов, shadow DOM-вложенности и внутренних
диагностических
data-hp; - обязательный перевод всей существующей smoke-suite на новые селекторы;
- добавление русскоязычного раздела в User Guide: это технический контракт, а не новый пользовательский сценарий.
6. Контракт поведения
6.1 Общие правила
data-hpостаётся единым атрибутом «что это за элемент».- Все значения из публичного инвентаря стабильны. Переименование или удаление требует записи в оба changelog и сохранения старого значения либо эквивалентного совместимого селектора на протяжении одной следующей стабильной версии. Добавление нового значения совместимо.
- Атрибут не создаёт поведения и не участвует в стилях продукта. Удаление атрибута через card-mod не должно ломать работу интерфейса.
- Когда элемент не существует по действующим permission/mode/state-правилам, соответствующий hook также отсутствует. Контракт не требует скрытой копии элемента.
- Тесты выбирают элементы внутри shadow root конкретного экземпляра House Plan. Уникальность между несколькими карточками на странице не обещается.
6.2 Корневое состояние
Каждый отрисованный корневой ha-card, включая boot/fixed-floor/empty/error
ветки, получает:
| Атрибут | Значения | Правило |
|---|---|---|
data-hp-state |
booting, ready |
booting, пока действует существующий _booting; после его завершения — ready |
data-hp-mode |
view, plan, devices, decor |
Публичное имя текущего режима; словарь совпадает с уже опубликованным mode-devices и существующим data-editor-navigation="devices" |
Empty card после завершённой загрузки является ready, даже если в ней нет ни
одного пространства. Ошибка fixed-floor не возвращает карточку в booting.
6.3 Публичные data-hp для шапки и состояния
data-hp |
Существующий элемент | Условие существования |
|---|---|---|
settings |
кнопка общих настроек | как сейчас: writable normal mode |
pdf |
кнопка PDF | как сейчас |
support |
кнопка помощи/обратной связи | как сейчас |
zoom-in |
кнопка + zoom |
как сейчас: в ordinary plan render; в kiosk остаётся в DOM внутри скрытого CSS header |
zoom-out |
кнопка − zoom |
как сейчас: в ordinary plan render; в kiosk остаётся в DOM внутри скрытого CSS header |
zoom-fit |
кнопка «вписать всё» | как сейчас: в ordinary plan render; в kiosk остаётся в DOM внутри скрытого CSS header |
space-add |
кнопка + рядом со вкладками |
по существующим permission/fixed-floor/kiosk правилам |
space-settings |
шестерёнка внутри вкладки | data-id равен id пространства |
empty |
существующий контейнер пустого состояния | нет пространств либо отображается fixed-floor pending/error |
create-space |
существующая CTA первого пространства | только если CTA уже разрешена |
toast |
существующий toast | дополнительно data-kind="message"; семантика severity в #489 не вводится |
space-settings несёт data-id, потому что на экране может быть несколько
шестерёнок. Остальные шапочные actions в пределах одной карточки единичны.
6.4 Редакторы
data-hp |
Дополнительный атрибут | Контракт |
|---|---|---|
toolbar |
`data-kind="plan | device |
tool |
data-tool="<stable-id>" |
существующая кнопка выбора инструмента/команды редактора |
editor-close |
— | существующая кнопка X в каждом toolbar и X активной mode-tab |
tray |
data-kind="<group-id>" при наличии группы |
корень видимой secondary/context surface редактора |
Минимальный словарь data-tool:
- Plan:
select,draw,column,merge,split,resize,wall-thickness,delete-roomи id существующих групповых launcher-кнопок; - Device:
add-device,device-inbox,icon-rulesи id существующих групповых launcher-кнопок; - Decor:
select,backdrop,line,rect,ellipse,text,furniture,image,eraseи id существующих групповых launcher-кнопок.
Undo/Redo, picker и отдельные save/cancel внутри трея не объявляются tool:
они не выбирают инструмент. Существующий data-editor-navigation сохраняется;
новый editor-close дополняет его, а не заменяет в переходный период.
6.5 Диалоги
Сам host каждого hp-dialog всегда получает data-hp="dialog". Каждый call
site задаёт один стабильный broad kind из закрытого стартового словаря:
space, room, marker, opening, physical, decor, settings, pdf,
support, confirm, onboarding, import, backup, kiosk, rules,
device-inbox, vacuum, summary, info.
Новые kind добавляются совместимо. Broad kind описывает пользовательскую задачу, а не имя приватного метода; несколько вариантов одного workflow могут иметь одинаковый kind.
На существующих главных действиях диалога:
data-hp="dialog-confirm"— Save/Create/Apply/Send/Import/Run и другое действие, которое подтверждает результат данного диалога;data-hp="dialog-cancel"— Cancel/Close/Back, которое закрывает диалог без принятия результата;- destructive secondary actions вроде Delete не получают
dialog-confirm, если открывают отдельный confirm workflow; - встроенная fallback-кнопка X
hp-dialogполучаетdialog-cancel; внутреннюю close-кнопку HA shadow tree контракт не охватывает.
У многошагового диалога hooks относятся к текущему шагу. Если у шага нет явного confirm/cancel button, синтетическая кнопка не добавляется.
6.6 Sidebar panel
В shadow root <houseplan-panel> существующие элементы получают:
data-hp="panel-menu"на кнопке, отправляющейhass-toggle-menu;data-hp="panel-title"на видимом заголовке House Plan.
Это не распространяет styling contract карточки через два shadow root и не обещает стабильность остальной panel-shell разметки.
7. UX
Визуальная и интерактивная дельта равна нулю:
- подписи, доступность, tab order и focus management не меняются;
- кнопки остаются видимыми и активными ровно в прежних условиях;
- режимы и readiness лишь проецируются в DOM, а не вычисляются заново;
- никаких тестовых индикаторов, debug-панелей или пользовательских настроек не появляется.
Для card-mod действует прежнее предупреждение: поддерживаются только явно объявленные hooks, но не произвольный CSS и не внутренняя структура.
8. Модель данных и миграция
Пользовательская конфигурация и backend storage не меняются; миграции нет.
docs/data-hp-contract.json имеет версионированную схему:
{
"schemaVersion": 1,
"attribute": "data-hp",
"hooks": {
"device": { "elements": ["div"], "since": "1.59.0-beta.3", "audience": ["styling", "test"] },
"settings": { "elements": ["button"], "since": "1.73.0-beta.7", "audience": ["test"] }
},
"rootAttributes": {
"data-hp-state": { "element": "ha-card", "values": ["booting", "ready"], "since": "1.73.0-beta.7" }
},
"internalPrefixes": ["iso-", "zigbee-topology-", "resize-", "plan-snap-", "hidden-wall-"]
}
Полный файл содержит все уже объявленные значения §3
STYLING-HOOKS.md, новые значения #489, data-hp-mode, связанные словари
data-kind/data-tool там, где они являются частью нового селектора, и
исчерпывающие исключения для существующих внутренних exact values/prefixes.
since существующих hooks восстанавливается по документации/истории, новых —
первая планируемая версия выпуска текущей линии (1.73.0-beta.7). Если до
релиза линия изменится, release commit обновляет только metadata since.
JSON не импортируется runtime-кодом и не попадает в initial bundle. Это документ и вход для тестов/внешнего E2E.
9. i18n
Новых пользовательских строк нет. EN/RU/DE/FR bundles не меняются. Раздел Test hooks пишется на английском как разработческий контракт; существующая русская User Guide лишь сохраняет ссылку на styling hooks, если такая ссылка уже есть.
10. Критерии приёмки
AC1 — корневые состояние и режим
Каждый render branch ha-card несёт допустимые data-hp-state и
data-hp-mode; переход View → каждый editor → View обновляет mode без чтения
приватных полей.
Доказательство: unit/source-contract test + расширенный
node demo/smoke_styling_hooks.mjs.
AC2 — шапка, empty и toast
Все доступные в текущем состоянии header actions находятся по селекторам из
§6.3; writable empty state имеет empty и create-space, read-only empty не
получает ложной CTA; toast находится как toast[data-kind="message"].
Доказательство: demo/smoke_styling_hooks.mjs на обычной, empty writable
и empty read-only fixtures.
AC3 — три редактора
В каждом editor существует один видимый toolbar с правильным data-kind,
каждая заявленная tool button имеет стабильный data-tool, закрытие находится
как editor-close, а открытая secondary surface — как tray.
Доказательство: browser smoke проходит Plan/Device/Decor и открывает по одному group/tray; unit test сверяет словари inventory с source call sites.
AC4 — диалоги
Каждый hp-dialog host имеет dialog и допустимый data-kind; все
существующие семантические confirm/cancel buttons размечены по §6.5, не меняя
действия и focus flow.
Доказательство: source-contract test запрещает call site без kind;
unit-тест hp-dialog проверяет host/fallback X; smoke открывает settings,
space/onboarding и confirm workflows и выполняет действия через hooks.
AC5 — sidebar panel
Menu и title существующей #486 panel находятся как panel-menu и
panel-title, при этом menu по-прежнему отправляет ровно одно
hass-toggle-menu.
Доказательство: unit test houseplan-panel.
AC6 — машинный инвентарь закрыт и сам себя защищает
docs/data-hp-contract.json валиден и содержит каждый публичный hook. Новый
test/data-hp-contract.test.mjs падает, если:
- объявленное значение больше не встречается в исходниках;
- в исходниках появилось статическое
data-hp, которого нет в публичном списке и которое не разрешено внутренним prefix/exact allowlist; hp-dialogcall site не имеет kind;- обязательное поле
elements,sinceилиaudienceотсутствует; - переименованный fixture-hook не совпадает с inventory.
Доказательство: npm test, включая локальный mutant/fixture case внутри
самого unit-теста.
AC7 — документация и совместимость
STYLING-HOOKS.md отличает styling и test audiences, перечисляет селекторы и
политику переходного периода. Оба changelog содержат понятную запись со ссылкой
на #489. Пользовательская config и видимый DOM/layout не меняются кроме новых
атрибутов.
Доказательство: review diff + обязательный
node scripts/check-docs.mjs после принятия канонического screenshot-артефакта
и обычный build.
AC8 — обязательные гейты
Проходят npm run typecheck, npm test, npm run build,
npm run bundle:sync, npm run bundle:budget и локальный
node demo/smoke_styling_hooks.mjs. Поскольку меняется src/**, дополнительно
обязателен node scripts/check-docs.mjs: до пересъёмки он должен честно
сообщить об устаревшем source fingerprint, после принятия нового комплекта —
пройти зелёным.
Доказательство: точные команды и результаты в комментарии #489 перед
S7-code-review.
11. План автотестов
- Новый Node test читает JSON, валидирует схему и сканирует
src/**/*.ts, включая статические и ограниченный набор динамических Lit assignments. - Тест содержит контролируемую строку-мутант: замена одного публичного
значения обязана дать ошибку
undeclared/missing, чтобы доказать, что gate не является формальным чтением JSON. - Unit
hp-dialogпроверяет автоматический host hook и fallback close hook; существующие focus/Escape тесты остаются зелёными. - Unit panel-shell проверяет два hooks и отсутствие дублированного menu event.
- Styling smoke последовательно проверяет ready/view, header, toast, empty, три editor toolbars, tray и репрезентативные dialog kinds/actions.
- Полный smoke/golden/performance остаются предрелизными гейтами; golden baseline меняться не должен, поскольку пиксельной дельты нет.
12. Риски и меры
- Пропущенный conditional branch. Root attributes выносятся в один
маленький resolver/helper и используются во всех ранних
ha-cardreturns; smoke отдельно проходит empty и обычный path. - Неполная классификация диалогов. Unit сканирует все
<hp-dialog ...>и запрещает неразмеченный call site; broad kinds не зависят от имени метода. - Ложное превращение diagnostics в API. Публичные hooks и внутренний allowlist лежат раздельно; docs прямо отрицает стабильность allowlist.
- Regex gate пропускает динамику. Динамические assignments допускаются только через явно проверенные expressions/fixtures; runtime smoke доказывает результирующий DOM.
- Сломанный внешний E2E при расширении.
schemaVersionменяется только при несовместимом изменении структуры JSON; добавление hook не меняет версию схемы. - Рост initial bundle. JSON не импортируется runtime-кодом; атрибуты дают только небольшую строковую дельту, проверяемую bundle budget.
13. Откат
До выпуска ветка откатывается обычным revert целиком. После выпуска удалять
или переименовывать опубликованные hooks немедленно нельзя: сначала changelog и
одна стабильная версия совместимого перехода. Runtime-атрибуты можно перестать
использовать внутри продукта в любой момент, пока публичные селекторы остаются
доступны. JSON schema v1 сохраняется, либо при несовместимой структуре получает
новую schemaVersion.
Пользовательские данные откатывать не требуется.
14. Release-артефакты
- source изменения в
src/**и тесты вtest/**,demo/**; docs/data-hp-contract.jsonи обновлённыйdocs/STYLING-HOOKS.md;- запись
docs/specs/README.mdи двусторонняя ссылка issue ↔ ТЗ; docs/CHANGELOG.mdиdocs/CHANGELOG.ru.mdв том же user-visible commit;- синхронные bundle trees после
npm run bundle:sync; - канонический комплект docs screenshots из workflow Docs screenshots на
точном implementation SHA, принятый командой
npm run docs:accept -- --reviewed --from=<распакованный-артефакт>; локальная самостоятельная пересъёмка PNG не принимается; - без golden baseline, если visual diff действительно нулевой.
15. Принятые предположения
Эти технические решения приняты предположительно и могут быть свободно изменены ревьюером без блокировки владельца:
- JSON использует object map
hooks[value], чтобы внешний E2E мог проверять селектор без линейного поиска; audienceразличаетstylingиtest, но оба набора получают одинаковое обещание стабильности;- toast в этой задаче имеет один честный kind
message; severity нельзя достоверно восстановить из переведённого текста и она не нужна заявленному E2E-сценарию; - dialog kinds намеренно broad и не кодируют каждый внутренний подшаг;
devicesсохраняет уже опубликованное имя.stage.mode-devicesи значениеdata-editor-navigation="devices"; отдельное третье имяdeviceдля того же режима не вводится;- следующая запись
sinceпредварительно равна1.73.0-beta.7и уточняется release commit, если версия линии изменится; - внутренние исключения могут быть exact values сверх перечисленных в issue,
если они уже существуют на исходной вершине
dev; это не делает их API; - открытых продуктовых вопросов нет.