Files
houseplan-card/docs/testing-notes/devices.md
Claudeandclaude[bot] 7dc7597260 docs(process): ролевые конспекты, замер входа, Snapshot генерируется, TESTING.md разделён
Вход агента до первого файла кода стоил ≈ 26 700 слов (аудит 22.09).

- docs/process/AUTHOR.md и REVIEWER.md — выжимки PROCESS.md: каждый пункт
  ссылается на раздел канона, ключевые формулировки дословные;
  test/process-digests.test.mjs сверяет якоря, ссылки и правила.
- scripts/entry-cost.mjs — маршрут чтения по роли и бюджет (автор ≤ 12 000
  слов, AC1); AGENTS.md «Read this first» называет те же маршруты.
- docs/STATUS.md: блок Snapshot генерирует scripts/status-snapshot.mjs
  (версии — release-contract, счётчики — inventory, теги — git); feature
  surface и ранние milestones перенесены дословно в docs/STATUS-FEATURES.md.
- docs/TESTING.md — действующая инструкция (684 строки, AC3); ручные
  чек-листы и приложения по issue перенесены дословно в docs/testing-notes/
  с индексом и тестом на полноту.
- Промпт ревьюера в process.yml читает конспект вместо пересказа правил;
  машинные требования (строка вердикта, REVIEW_DOC, запрет fetch, таблица
  «чем краснеет», разделы повторного раунда) сохранены и закреплены тестом.
- PROCESS.md: правила не менялись; добавлены ссылка на конспекты в шапке и
  уточнение в §10.4, что ревьюер конвейера читает конспект.
- 7 мутантов в реестре.

Issue: #634
User-Visible: no
2026-09-24 02:33:08 +00:00

24 KiB
Raw Permalink Blame History

Устройства и маркеры

Приложение к docs/TESTING.md: перенесено оттуда дословно (#634). Индекс всех приложений — README.md.

Device icon design package (#179)

  • Pure presentation tests cover lock/unlock, exact marker-only LQI bands 0/40/41/179/180, unchanged room gradient, package pulse defaults, semantic colors and reduced motion [unit: device-presentation.test.mjs, device-pulse.test.mjs].
  • Shared face tests cover shell/core DOM, four Double positions, a third legacy section, deterministic font fitting, full text and safe CSS color variables [unit: device-face.test.mjs].
  • The browser renders the exact shell ratio and shadow color, Light/Dark cores without backdrop blur, state/LQI colors, 3.6 s presence pulse, unavailable no-hover, full Text/Double values, 44×44 target, View and Device-editor keyboard paths, and no Plan tab stop [auto: smoke_device_icon_design.mjs].
  • Full plan, preview and static card preserve the same face after the DOM redesign; static mode, state/value and disabled-device contracts stay green [auto: smoke_device_preview_parity.mjs, smoke_static_icon.mjs, smoke_state_value.mjs, smoke_disabled_device.mjs].
  • Restoring unavailable hover, shifting the LQI boundary, restoring value ellipsis or bypassing _clickDevice() makes its guard red [mutation: device-unavailable-hover-restored, device-marker-lqi-low-boundary-shifted, device-long-value-ellipsis-restored, device-keyboard-bypasses-click-path].
  • Pre-beta golden reviews desktop/mobile Light/Dark states, combo states, Text, four Double positions, long and legacy values, LQI, reduced motion, sizes 32/56/96 and colored backgrounds. Full smoke/golden/performance remains a Linux release gate.

Dense device-marker hit ownership (#564)

  • Pure geometry covers visible capsule priority, invisible 44 px-floor overlap, nearest-core selection, stable exact ties, stadium corners and spatial-index locality [unit: device-hit-owner.test.mjs].
  • Source contracts keep painted shells globally above transparent floors, route hover/click/pointer lifecycle through one semantic owner and forbid layout reads from the pointer-move path [unit: device-hit-owner-contract.test.mjs].
  • Household J7 checks the five-marker dense column at tablet and phone widths: every visible centre has the same native and semantic owner, its click opens that marker, and a pointer sequence remains latched through its terminal click [auto: smoke_household_journeys.mjs].
  • Real browser geometry covers Icon, Text, Double and untouched legacy faces at all four cardinal sides/directions. In every case a painted point overlaps only the neighbour's invisible 44 px floor and still owns the native hit, semantic owner and click [auto: smoke_device_hit_capsules.mjs].
  • Replacing nearest-screen-core selection with first-input/DOM order makes both the pure geometry guard and the real rendered-face browser guard red [mutations: dense-device-hit-falls-back-to-input-order, dense-device-hit-browser-skips-painted-priority].

Device marker polish and pointer modality (#212)

  • Shared icon geometry applies one 0.9 visual factor after card/per-marker sizing, keeps the saved centre and 44×44 hit floor unchanged, and gives wide Text cores a radius equal to half their height [unit: device-marker-polish-contract.test.mjs; auto: smoke_device_icon_design.mjs].
  • Only a genuinely dispatched toggle/run action produces one 1 → .95 → 1 feedback cycle lasting 200 ms. Info, editor, confirmation before acceptance, unavailable/secure/no-target and cancelled gestures do not; reduced motion has no scale tween [auto: smoke_device_icon_design.mjs].
  • Pointer authority is isolated per card. Touch/pen and compatibility mouse input clear JS/CSS hover, while a later real mouse restores it only on fine/hover hardware; mode/space/visibility/disconnect cleanup remains bounded [unit: pointer-modality.test.mjs; auto: smoke_feedback_v2.mjs].
  • Pre-beta visual review covers Light/Dark desktop and touch matrices for ordinary, Text, Double, unavailable and vacuum markers; full golden and performance gates remain release work.

Device value badge (#90)

  • An untouched legacy thermometer/humidity marker remains pixel-identical; saving another field does not materialize value_badge. [auto: device-presentation]
  • Explicit on/off overrides the legacy temperature gate; zero, false and off remain visible, while missing/unknown/unavailable render a stable —. [auto: device-presentation]
  • State, every allowlisted attribute, derived LQI and marker:<id> light state resolve identically on the full plan, static space card and preview. [auto: smoke_device_preview_parity]
  • Opening the editor explicitly selects the persisted source and position, even when they are not the first dynamic options, without touching config. [auto: smoke_device_preview_parity]
  • Right, bottom, left and top update live in the editor; bottom stacks above system LQI and derived LQI suppresses the duplicate system row. [auto: device-presentation]
  • Browser bounding boxes stay inside .previewstage with a safe gap for all positions, a long value, scale ×3 and the maximum activity ring. [auto: smoke_device_preview_parity]
  • Rebind resets the source, delete leaves a missing diagnostic reference, and space import remaps internal refs or disables/counts external refs. [auto: test_ha_import_export]
  • static_icon suppresses but preserves the setting; live-state and room label toggles do not suppress an explicit badge. [auto: device-presentation]

Policy: this checklist is updated in the same commit as any functional change (like CHANGELOG.md). For a pre-release, build the production bundle and run the smallest unit/smoke subset that covers its changed surfaces. Run the complete local frontend, backend and smoke gates only before a stable release. The exact-SHA GitHub Validate remains mandatory for publication. Items marked [manual] are covered by unit tests or the headless smokes in demo/ — they still deserve an occasional eyeball. File every failure as a GitHub issue before fixing.

What [manual] means (since 2026-07-27). A named check exists that FAILS when the behaviour breaks — in npm test or in the smoke suite, both of which run in CI on every push. Where the check lives is written next to the marker ([auto: smoke_modes]). Before this date the marker described an intention: the smoke suite printed values and always exited 0, so 96 markers guarded nothing (external audit T1/T3). If you add a checklist line marked [manual], add the failing check in the same commit.

⚠ Rule: a new scrollable list inside a dialog is tested by GEOMETRY, never by the DOM. Any new scrolling box or overflow container added to a dialog MUST get a smoke that measures the container's own height and the visible position of its first item (getBoundingClientRect, and the item's rect against the box's rect) — counting rendered rows, or asserting that the nodes exist, proves nothing. The failure mode is always the same and always invisible to a DOM check: a scrolling box is a flex item whose automatic minimum size is zero (min-height: auto → 0 for an overflow child), and a dialog body is a flex column with a height cap, so the box is the one child that can be squeezed to a sliver while every row inside it renders happily. It has bitten us twice already: the target search results in the tap action dialog (v1.53.1 — 26 matching automations rendered into a 1 px stripe; the smoke counted rows and passed) and the «Already uploaded» plan picker (dev, unreleased — rows present, box 14 px tall, same story). Both smokes measure heights now; write the third one that way from the start.

HA-disabled binding gate

The source-of-truth matrix is docs/superpowers/specs/2026-08-08-ha-disabled-devices-design.md §17. test/ha-binding-status.test.mjs covers full/limited registry decisions and the active-only state projection. The standalone demo exposes complete disabled_by rows through both registry list WS commands plus window.__setRegistryDisabled(kind, id, disabledBy) and window.__setRegistryAccess(mode) for browser scenarios.

  • A saved device/entity marker disappears from View, room data, Glow, controls, live text, openings and vacuum overlays after its registry row becomes disabled; config/layout remain byte-for-byte unchanged.
  • Device editor → Hidden and disabled shows a labelled grey ghost. Show is refused, metadata/Delete/Open in HA remain available, and the ghost is not draggable.
  • Reactivating the same ID restores its metadata/layout without a false new-device event; an explicitly user-hidden marker stays hidden.
  • A never-seen auto device disabled before discovery appears as new only after its first activation.
  • All disabled child entities make an otherwise active device disabled; one disabled auxiliary entity never suppresses active functional rows.
  • If full registry WS access is denied, positive live evidence stays active, an unknown binding is unverified, and no false disabled/orphaned ghost or service call is produced.
  • Two full cards plus a static space card share one registry fetch and one subscription pair per HA connection; registry events invalidate all of them without a reload.
  • houseplanDiagnostics() reports only redacted registry access/age/error and binding-status counts; it contains no names, states or marker data.

HA Area marker relocation (#126)

  • test/device-area-relocation.test.mjs covers direct-binding authority, same/cross-space transitions, conservative legacy backfill, explicit and composite exclusions, rebind, malformed metadata and delete-first provenance.
  • test/space-geometry.test.mjs proves both marker-position paths can suppress one stale saved point without changing the stored layout input.
  • demo/smoke_area_relocation.mjs changes an authoritative registry Area against the production bundle, checks one serialized layout delete plus config/attention persistence, and proves the read-only hosted card moves immediately without writes.
  • Area-provenance cleanup ignores the filtered display roster, preserves empty Device/Entity Registry namespaces, accepts exact live entity states as existence evidence, requests only one confirmation refresh and removes an orphan only after two distinct non-empty authoritative revisions.
  • A rejected confirmed-cleanup config write remains retryable on the next rebuild; the same revision, state ticks and repeated empty frames never become extra confirmation or a registry reload loop.
  • Backend validation and import/export tests cover the 20,000-entry bound, exact entry schema, same-source preservation and cross-source removal.

Device display preview and face parity

The behaviour matrix is defined in docs/superpowers/specs/2026-08-08-device-display-preview-design.md §22. Pure source/value/presentation rules live in test/device-presentation.test.mjs. demo/smoke_device_preview_parity.mjs compares the same live fixture across the interactive plan, hp-device-preview and houseplan-space-card, including semantic classes, icon/value/badges, scale variables, provider text and the public binding-status hook.

  • Every binding/display/icon/size/angle/control/temperature draft change updates the preview before Save; Cancel writes neither config nor layout.
  • Working, open, cover, presence, short event, transition, alarm, static, unavailable, media-neutral, composite-Power and live_states: false explanations match the actual face.
  • The local short-activity demo lasts 3.3 seconds and the continuous demo runs until stopped. Neither sends a service call; reduced motion uses a compact dot, and both reset immediately on binding change, real activity or alarm.
  • Provider metadata is cached between dialog openings and refreshed after registry/config-entry changes; source integrations remain separate from the binding provider.
  • Long provider/source/state text wraps without horizontal scroll; maximum marker/ripple size fits the stage and reports its preview scale.
  • Derived temperature/humidity values keep the compact plan form (22.4°, 48%), while a direct entity value continues to use HA localization and units.

Device icon package parity (#211)

The independent reference subset under demo/srv/reference/device-icons/ comes directly from designer package 1.1.1; it is not generated from production CSS. The package archive hash and the owner's #219 red/green Lock/Unlock paint override are recorded in that directory's README.

  • node demo/smoke_device_icon_design.mjs reads the SVG colors and stroke widths, then compares them with fresh computed styles. It also measures circular core/shell geometry, the real mdi:lightbulb-spot painted path, value-pill radius and 44×44 hit area at 32/56/96 px.
  • node demo/capture_device_icon_reference.mjs writes a two-column Reference SVG / Runtime matrix for both themes to artifacts/device-icon-reference/. Code review must inspect this artifact visually; a green historical golden is not proof of package parity.
  • Preview/static parity and unavailable keyboard/tap behavior remain covered by smoke_device_preview_parity, smoke_static_icon and smoke_disabled_device after a fresh production build.

Device marker geometry and input polish (#213)

  • node demo/smoke_device_icon_pixel_alignment.mjs covers core bases 24…112 CSS px in quarter-pixel steps at DPR 1/1.25/1.5/2. DOM centres, isolated painted centroids/support and a deliberate 1 CSS px mutant must distinguish browser raster parity from a persistent offset.
  • node demo/smoke_device_icon_design.mjs keeps the effective 32/56/96 geometry, uses the direct 0.55 MDI/core ratio and proves hover plus the configured action from the far value-capsule end at right/bottom/left/top.
  • Opening binding/registry-less/lock-action smokes preserve the secure no-toggle-on-plan invariant while checking compact Light/Dark locked/unlocked/unknown shell/core presentation.
  • node demo/smoke_opening_entity_search.mjs checks the real opening dialog: contact and lock search by friendly name/entity ID, preserved contact priority, visible IDs, persistent none option and unchanged opening.contact/opening.lock storage.
  • Unit presentation coverage compares marker LQI colour with the shared continuous lqiColor() across former 40/41 and 179/180 boundaries; bands remain semantic metadata only.

Text marker shell shape (#217)

  • node demo/smoke_device_icon_design.mjs checks the external Text frame, not only its core: a long value keeps a saturating capsule radius at 24/32/56/96/112 px, while Icon-only remains circular and Double remains a capsule. The runtime mutation to border-radius: 50% must be rejected.
  • device-text-shell-long-light and device-text-shell-long-dark isolate a large 498 ppm Text marker. Golden review must visibly confirm straight upper/lower middle sections rather than an ellipse.
  • node demo/capture_device_icon_reference.mjs includes an additional 96 px Text row beside the normative Light/Dark Text Default.svg.

Device lock and orange foreground palette (#219)

  • Closed/locked is green #66D17A; open/unlocked is red #F0410C. The same core/stroke palette is used by ordinary lock markers and compact door/gate lock badges; glyph shape remains closed/open/question.
  • Every device glyph on an orange core (on/working and physical open) is white in Light and #252525 in Dark. device-icon-state-table-light and device-icon-state-table-dark show on and open together, plus both lock states [unit: device-marker-polish-contract; auto: smoke_device_icon_design; golden: device-icon-state-table-*].
  • Alarm, hover, focus, selected, unavailable, virtual, press feedback, pulse, hit-area and lock actions retain their existing priority and behaviour [unit: device presentation/polish/pointer; visual source review].

Unified device status and pulse activity (#98)

  • The Display list contains exactly Icon + state, Icon + state and activity, Value + state, Always static icon, in that order. Legacy display: ripple reads and saves back as icon_ripple
  • Icon + dynamic plate shows state plate/morph but no ordinary activity effect; Icon + activity adds the semantic effect; Value keeps the state-coloured plate and hides ordinary activity; Always static icon keeps one neutral base icon and suppresses all state-driven visuals
  • Motion/vibration/sound/contact rising edges render exactly three waves for about 3.3 s; initial load and recovery from unknown/unavailable do not fake an event; a rapid retrigger restarts it
  • Occupancy/presence is one calm continuous pulse for the whole active state
  • Cover/lock/valve movement continuously pulses until the travelling state ends; direct terminal closed ↔ open / locked ↔ unlocked without an intermediate state breathes for about 3.3 s
  • Actual work (light/switch/fan/humidifier on, active climate action, vacuum cleaning, script running) is yellow and slowly breathes in Icon + activity; automation = on is merely enabled and remains neutral
  • Every media_player is neutral and has no running activity for on, idle, playing, paused and other transport states; explicit off uses the same faded treatment as unknown/unavailable. Several resolved media entities fade only when none is available and powered
  • Controls aggregate their targets: any working target drives both the yellow plate and the running effect
  • Open contact/open valve are orange; unlocked lock is red and locked lock is green; an open cover stays neutral because its icon morph carries that state
  • Unavailable suppresses ordinary activity. Alarm outranks all dynamic presentation, including when ordinary live states are off; static_icon deliberately hides alarm paint without suppressing service-call errors
  • static_icon hides temperature/humidity/LQI, RGB, value, icon morph, activity and live vacuum puck/trails/room highlight on the full and static cards; preview still names the real HA state/source and explains the static result
  • Switching a static vacuum back to a dynamic display restores applicable live/server trails; choosing static never deletes stored trail history
  • value_static_icon (#588) shows the same value as value — including on the plan itself and the space card, where sources are resolved lazily — while state, alarm, unavailability, RGB and activity never paint it; no pulse, no °/%/LQI, value badge suppressed with its setting kept, and the live vacuum puck, trail and route warning stay hidden [auto: smoke_static_icon.mjs, golden device-icon-state-table-*]
  • Activity colour and size (×2..×8) apply per device; alarm ignores them
  • Icon size ×0.5..×3 and rotation 0..355° apply per device; the temp/humidity badges scale with the icon
  • With OS "reduce motion" enabled, ordinary activity becomes a compact solid dot; alarm keeps the red plate and accessible alarm description, without an animated or static ring

Climate temperature opt-in (dev)

  • «Use the device's temperature sensor» (marker dialog, climate devices only, default OFF): current_temperature shows as the standard .tval badge and joins the room average like a thermometer; unavailable / missing attribute = no badge, no vote; hidden devices keep voting (registry-wide climate, like hidden thermometers); the tick survives dialog recreation [auto: smoke_climate_temp; units: test/devices.test.mjs; backend: tests_backend/test_validation.py (use_climate_temp)]

Styling hooks and HA-formatted values (docs/STYLING-HOOKS.md, dev, unreleased)

  • The hooks are there and they are the config's ids: open the plan's DOM (devtools → the card's shadow root) and check that a device marker carries data-hp="device", data-id, data-entity and data-area; a room data-hp="room" + data-id + data-area; a door/window/gate data-hp="opening" + data-kind; a decor shape data-hp="decor" + data-kind; a visible room card data-hp="room-label"; a floor tab data-hp="space-tab". The ids are the ones in your config, not DOM positions — they survive a reload [auto: smoke_styling_hooks]

  • Absent is absent: a virtual marker has NO data-entity at all, and a sub-area room (no HA area) has NO data-area — never the string «undefined» [auto: smoke_styling_hooks]

  • A card-mod rule actually applies: with card-mod installed, add ha-card [data-hp="device"] .lqi { display: none; } to the card — the signal badges disappear and nothing else moves. Then target one marker by [data-entity="…"] and confirm it is the only one affected [manual]

  • The static card carries the same hooks: a houseplan-space-card has data-hp on its rooms, visible room labels and markers (it draws no openings and no decor, so those are simply absent), and it needs its OWN card-mod block — it is a different card with its own shadow root [auto: smoke_styling_hooks]

  • ha-icon internals stay out of reach: a rule may style the icon HOST (colour, transform) but cannot reach the <svg> inside it. That is a browser rule, and it is why the hooks sit on our wrappers [auto: smoke_styling_hooks]

  • A value badge is formatted by HA: set a numeric sensor's display precision in HA (Settings → the entity → Display precision) to 1 and put it on the plan as «value instead of icon». The badge shows the rounded number with YOUR decimal separator and the entity's unit — once, not twice — and matches what more-info shows [auto: smoke_value_format]

  • A live decor label is formatted by HA: the same sensor in a text shape reads identically; a switch shows «Включено», not on; an attribute goes through the attribute formatter (a climate's current_temperature is a number, not the climate's state) [auto: smoke_value_format + unit logic.test]

  • A literal suffix stays literal: write an attribute token followed by проц. — the suffix is part of the text, with no hidden unit override and no duplicate appended by the label renderer [auto: smoke_live_text]

  • An older Home Assistant is unchanged: on an HA without formatEntityState the badge and the label print the raw state with the entity's unit appended, exactly as before — nothing is blank and nothing throws [auto: smoke_value_format + unit logic.test]

  • The °/% plates are untouched: the small temperature/humidity badges next to an icon (and the same numbers in a room card and the tooltip) still read «21.5°» / «48%». They are a derived reading, not an entity state — deliberately ours [auto: smoke_value_format]

  • The text block's handles are small but still catchable: select a label in the Background editor. The four corner circles and the rotate handle are a quarter of their old size — beads, not buttons — and the dashed frame no longer hides the text. Now grab one on a TABLET with a finger, aiming roughly at it rather than exactly: it is caught, because the invisible hit circle is still the old finger-sized one. Same at any zoom [auto: smoke_decor_text]