# #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. Скоуп 1. Расширить `docs/STYLING-HOOKS.md` отдельным разделом **Test hooks** на английском: назначение, селекторы, словари значений и политика стабильности. 2. Добавить `data-hp` на уже существующие элементы шапки, редакторов, диалогов, empty state, toast и sidebar panel без добавления обёрток и без изменения CSS. 3. Публиковать на каждом корневом `ha-card` готовность и текущий режим. 4. Ввести машинный инвентарь `docs/data-hp-contract.json` и unit-проверку его согласованности с исходниками. 5. Расширить `demo/smoke_styling_hooks.mjs` доказательствами DOM-состояний, которые нельзя подтвердить одним статическим поиском. 6. Там, где существующий smoke уже обращается к охваченному элементу по внутреннему классу, перевести его на объявленный hook; полный перенос всех остальных smoke не требуется. 7. Добавить пользовательскую запись в оба changelog со ссылкой на #489. ## 5. Не входит - изменение разметки, вложенности, визуальных стилей, размеров или расположения; - новые кнопки, диалоги, тосты, действия или пользовательские настройки; - изменение логики готовности, загрузки, редакторов или разрешений; - backend seed/reset API и код внешнего `houseplan-e2e`; - обещание стабильности для CSS-классов, shadow DOM-вложенности и внутренних диагностических `data-hp`; - обязательный перевод всей существующей smoke-suite на новые селекторы; - добавление русскоязычного раздела в User Guide: это технический контракт, а не новый пользовательский сценарий. ## 6. Контракт поведения ### 6.1 Общие правила 1. `data-hp` остаётся единым атрибутом «что это за элемент». 2. Все значения из публичного инвентаря стабильны. Переименование или удаление требует записи в оба changelog и сохранения старого значения либо эквивалентного совместимого селектора на протяжении одной следующей **стабильной** версии. Добавление нового значения совместимо. 3. Атрибут не создаёт поведения и не участвует в стилях продукта. Удаление атрибута через card-mod не должно ломать работу интерфейса. 4. Когда элемент не существует по действующим permission/mode/state-правилам, соответствующий hook также отсутствует. Контракт не требует скрытой копии элемента. 5. Тесты выбирают элементы внутри 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|decor"` | корень видимого primary toolbar | | `tool` | `data-tool=""` | существующая кнопка выбора инструмента/команды редактора | | `editor-close` | — | существующая кнопка X в каждом toolbar и X активной mode-tab | | `tray` | `data-kind=""` при наличии группы | корень видимой 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 `` существующие элементы получают: - `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` имеет версионированную схему: ```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-dialog` call 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. План автотестов 1. Новый Node test читает JSON, валидирует схему и сканирует `src/**/*.ts`, включая статические и ограниченный набор динамических Lit assignments. 2. Тест содержит контролируемую строку-мутант: замена одного публичного значения обязана дать ошибку `undeclared/missing`, чтобы доказать, что gate не является формальным чтением JSON. 3. Unit `hp-dialog` проверяет автоматический host hook и fallback close hook; существующие focus/Escape тесты остаются зелёными. 4. Unit panel-shell проверяет два hooks и отсутствие дублированного menu event. 5. Styling smoke последовательно проверяет ready/view, header, toast, empty, три editor toolbars, tray и репрезентативные dialog kinds/actions. 6. Полный smoke/golden/performance остаются предрелизными гейтами; golden baseline меняться не должен, поскольку пиксельной дельты нет. ## 12. Риски и меры - **Пропущенный conditional branch.** Root attributes выносятся в один маленький resolver/helper и используются во всех ранних `ha-card` returns; smoke отдельно проходит empty и обычный path. - **Неполная классификация диалогов.** Unit сканирует все `` и запрещает неразмеченный 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; - открытых продуктовых вопросов нет.