mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-30 11:49:16 +00:00
406 lines
28 KiB
Markdown
406 lines
28 KiB
Markdown
# #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="<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 + обязательный
|
||
`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 сканирует все `<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;
|
||
- открытых продуктовых вопросов нет.
|