From 9cf10bdde7696e13c049fe83b4dc284622386127 Mon Sep 17 00:00:00 2001 From: Sergey Matyunin Date: Tue, 8 Sep 2026 20:53:22 +0300 Subject: [PATCH] docs: specify public data-hp test contract (#489) Issue: #489 User-Visible: no --- docs/specs/489-data-hp-contract.md | 395 +++++++++++++++++++++++++++++ docs/specs/README.md | 1 + 2 files changed, 396 insertions(+) create mode 100644 docs/specs/489-data-hp-contract.md diff --git a/docs/specs/489-data-hp-contract.md b/docs/specs/489-data-hp-contract.md new file mode 100644 index 00000000..b9069471 --- /dev/null +++ b/docs/specs/489-data-hp-contract.md @@ -0,0 +1,395 @@ +# #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`, `device`, `decor` | Публичное имя текущего режима; внутреннее `devices` отображается как `device` | + +Empty card после завершённой загрузки является `ready`, даже если в ней нет ни +одного пространства. Ошибка fixed-floor не возвращает карточку в `booting`. + +### 6.3 Публичные `data-hp` для шапки и состояния + +| `data-hp` | Существующий элемент | Условие существования | +|---|---|---| +| `settings` | кнопка общих настроек | как сейчас: writable normal mode | +| `pdf` | кнопка PDF | как сейчас | +| `support` | кнопка помощи/обратной связи | как сейчас | +| `zoom-in` | кнопка `+` zoom | когда отображается header | +| `zoom-out` | кнопка `−` zoom | когда отображается header | +| `zoom-fit` | кнопка «вписать всё» | когда отображается 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 + `npm run check:docs` (если такой script +доступен в текущем package) и обычный build. + +### AC8 — обязательные гейты + +Проходят `npm run typecheck`, `npm test`, `npm run build`, +`npm run bundle:sync`, `npm run bundle:budget` и локальный +`node demo/smoke_styling_hooks.mjs`. + +**Доказательство:** точные команды и результаты в комментарии #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`; +- без golden baseline, если visual diff действительно нулевой. + +## 15. Принятые предположения + +Эти технические решения приняты предположительно и могут быть свободно +изменены ревьюером без блокировки владельца: + +- JSON использует object map `hooks[value]`, чтобы внешний E2E мог проверять + селектор без линейного поиска; +- `audience` различает `styling` и `test`, но оба набора получают одинаковое + обещание стабильности; +- toast в этой задаче имеет один честный kind `message`; severity нельзя + достоверно восстановить из переведённого текста и она не нужна заявленному + E2E-сценарию; +- dialog kinds намеренно broad и не кодируют каждый внутренний подшаг; +- `device` — публичное имя режима при внутреннем `_mode === "devices"`; +- следующая запись `since` предварительно равна `1.73.0-beta.7` и уточняется + release commit, если версия линии изменится; +- внутренние исключения могут быть exact values сверх перечисленных в issue, + если они уже существуют на исходной вершине `dev`; это не делает их API; +- открытых продуктовых вопросов нет. diff --git a/docs/specs/README.md b/docs/specs/README.md index 1a59f4be..dc9d84cd 100644 --- a/docs/specs/README.md +++ b/docs/specs/README.md @@ -29,6 +29,7 @@ GitHub Issues и GitHub Projects (v2) остаются единственным | Issue | ТЗ | |---|---| | [#486](https://github.com/Matysh/houseplan-card/issues/486) Панель House Plan в боковом меню HA | [486-house-plan-panel.md](486-house-plan-panel.md) | +| [#489](https://github.com/Matysh/houseplan-card/issues/489) Объявленный `data-hp`-контракт для UI и E2E | [489-data-hp-contract.md](489-data-hp-contract.md) | | [#484](https://github.com/Matysh/houseplan-card/issues/484) Внешняя размерная цепь ступенчатого фасада в PDF | [484-pdf-exterior-dimension-chain.md](484-pdf-exterior-dimension-chain.md) | | [#482](https://github.com/Matysh/houseplan-card/issues/482) Доводка экспорта пространства в PDF | [482-pdf-export-polish.md](482-pdf-export-polish.md) | | [#471](https://github.com/Matysh/houseplan-card/issues/471) Убрать белые raised plates вокруг маркеров и названий комнат | [471-isometric-overlay-white-plates.md](471-isometric-overlay-white-plates.md) |