docs: specify public data-hp test contract (#489)

Issue: #489
User-Visible: no
This commit is contained in:
Sergey Matyunin
2026-09-08 20:53:22 +03:00
parent 586494b726
commit 9cf10bdde7
2 changed files with 396 additions and 0 deletions
+395
View File
@@ -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="<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` имеет версионированную схему:
```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 сканирует все `<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`;
- без 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;
- открытых продуктовых вопросов нет.
+1
View File
@@ -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) |