Frontend Unit Tests (card logic)¶
Pure-JS unit tests for the card's own computational logic β coordinate math, hit-testing, state accessors, validation engines, colour/theme resolution β run with Node's built-in test runner, no browser and no build.
- Run them all:
npm run test:units(node --test "src/**/*.test.mjs"). - Run one file:
node --test src/state/rooms-logic.test.mjs. - 524 cases across 38 files, currently all green. Node β₯ 21 (native glob).
This is one of three separate frontend test tracks β see the three tracks.
How this set got big (2026-07-04)
It started as 13 files / 102 cases (mostly map/coordinate helpers). A
frontend-logic coverage audit then found that most of src/ is genuinely
plumbing (renderers, event-binding, hass-orchestration β correctly not
unit-tested), but a dense set of pure-derivation engines was untested:
the room access-graph, the reorder engine, start-block reasons, rule
validation, resolvedTheme, the learning summary/progress, map compose
geometry, floor-scope, and a few computations that had leaked inline into
renderers. Those were closed in three waves; the later floor-texture map view
then added the compositor + material-resolver engines (src/textures/),
taking the suite to 524 cases.
The map¶
Grouped by src/ area. Each file's test names carry inline [XX-N] ids (the
same convention as the Python suite; the check_legend_drift.py legend gate is
Python-only, so these .mjs ids are not legend-checked).
src/state/ β panel state accessors, coordinate math, and derivation engines¶
The card's state lives in applyXState(proto) mixins. These tests build a stub
proto (Object.create over the mixin'd prototype) and assert the pure
derivations β no DOM, no hass.
| File | Cases | What it guards |
|---|---|---|
rooms-logic.test.mjs |
28 | The room access-graph validator (validateRoomAccessUpdate + DFS cycle / self-reference / duplicate-edge / missing-ref / single-inbound rules β issue codes) and the start-button readiness reason-code precedence (no_rooms_included > already_cleaning > returning_to_dock > vacuum_error); orphanedRooms. |
order-engine.test.mjs |
24 | The reorder engine β _sortOrderedItems (numeric order + id tiebreak; the Number(null)===0 sort-to-front footgun is pinned), move-to-position clamp+splice, swap-by-id, 1-based reindex, and the scope-preview wrappers. Drives room drag-and-drop β backend number entities. |
room-rules-logic.test.mjs |
28 | ruleEntityDescriptor (domain β category + allowed-operator set), roomRulesDraftIsValid (Save-enabled gate incl. the clean_passes 1|2 rule), scored entity search tiers, operator-group filtering. |
theme-resolve.test.mjs |
27 | resolvedTheme β the deterministic 4-layer merge (room-fill palette defaults + floor-texture material defaults seeded from FLOOR_TEXTURE_REGISTRY, incl. the map-rotate token; computed -eff layers excluded β active theme β working draft), plus filteredThemeTokens / filteredPresetIds (facet AND/OR + search filtering). |
map-compose-and-viewport.test.mjs |
25 | composeToSegments (custom-segment draft β save payload: subtract-ordering, group/room_id resolution, rotated-rect β polygon trig), clampMapTransform (off-screen-recovery clamp), applyMapZoom (focal-point), loadComposeDraftFromSegments (id-counter advance). |
learning-derive.test.mjs |
33 | endLearningJob (actual-vs-predicted summary with the >0 guards), _dashboardJobIsActive (terminal-status gate for the whole live-job UI), room count / timeline / banner fallbacks, estimate keying. |
room-editor-matching.test.mjs |
19 | Preset snap-back matcher (_editorFieldsMatchProfile), option-list builder (omit β hide picker), clean-mode / intensity canonicalization, carpet gate. |
saved-zones-group.test.mjs |
17 | savedZonesGrouped (group under room, live map order, trailing "Unassigned" bucket) and selectedSavedZoneIds. |
external-jobs-group.test.mjs |
12 | externalWizardGroups (v1/v2 segment grouping) + wizard split-map / default-room seeding. |
run-profiles-normalize.test.mjs |
12 | _normalizeRunProfilesPayload (unwrap bare array / profiles / saved_run_profiles fallback; library guard). |
zone-draft.test.mjs |
12 | _rectToNormalized zone coord conversion + multi-zone draft list, and canDrawZone β draw-gate lights up over the live backdrop or an active VA raster (Roborock cv-mode path), still gated by supportsZoneClean. |
room-profiles-name.test.mjs |
10 | makeRoomProfileName (slug + custom_ prefix + _2/_3 collision suffix) + sorted profile lists. |
maintenance-logic.test.mjs |
9 | findUpkeepItem (case-insensitive kind+component match) and canInvokeMaintenanceReset. |
metrics-logic.test.mjs |
9 | findMetricsSaveCandidate (match on profile_key AND room_slug across the selected source). |
room-access-logic.test.mjs |
9 | accessEditableRooms (exclude self/dock, hide rooms claimed by another under the single-inbound rule). |
live-map-url.test.mjs |
7 | _liveMapImageUrl β backdrop URL + camera cache-bust. |
live-pose-overlay.test.mjs |
7 | Live-pose merge β fresh pose overrides only the moving fields; static segmentation preserved. |
room-hit-test.test.mjs |
6 | roomIdAtContentPct β pixel-exact room hit-test (Y-flip + contain letterbox). |
area-label-anchor.test.mjs |
5 | Per-room draggable mΒ² chip anchor. |
map-rotation.test.mjs |
5 | unrotatePct β pointer β content frame on a rotated map. |
mascot-dwell.test.mjs |
5 | Dwell-debounced mascot room tracker (state machine). |
core-battery.test.mjs |
5 | batteryState β 5-band classifier with charging override + ordered thresholds. |
hidden-regions.test.mjs |
4 | Per-map user-drawn masks β accessor, draw gate, rect conversion. |
map-room-labels.test.mjs |
2 | Per-vacuum map room-label visibility toggle. |
src/cards/ β standalone-card logic¶
| File | Cases | What it guards |
|---|---|---|
dashboard-dispatch.test.mjs |
23 | The dashboard card's pure run-launcher logic. |
zone-geometry.test.mjs |
14 | The card's pure zone-clean geometry (verified vs the real X10 camera). |
map-room-color.test.mjs |
9 | Map room-fill colour resolution (roomFillTokenName / palette / override, normalizeHex) β the frontend mirror of the backend _hex_color_or_none. |
src/theme-tokens/ β token-shape + floor-scoping logic¶
| File | Cases | What it guards |
|---|---|---|
helpers.test.mjs |
22 | makeTokenLabel (key β title-case label) + makeGroupedToken / makeTypedGroupToken (type-validated, finite-range-merge token factories). |
floor-scope.test.mjs |
14 | detectFloorScope / sliceThemeByTypes / clampThemeScalars β targeted theme export/import (longest-name-wins scoping, out-of-range scalar clamp). |
animals.test.mjs |
4 | Per-animal theme-token shape (editor lists only the tokens an animal themes). |
src/textures/ β floor-material render math¶
Pure math + config resolution behind the map's floor-texture view (mask Γ colour Γ opacity compositing and the roomβmaterial mapping). No canvas, no DOM.
| File | Cases | What it guards |
|---|---|---|
floor-texture-compositor.test.mjs |
7 | compositeFloorTexture(w, h, baseColor, layers) β the maskΓcolourΓopacity blend the floor view samples: white mask reveals the layer colour over the opaque base, opacity scales the reveal, mid-grey blends halfway, layers stack bottomβtop, and degenerate inputs (short luma / opacity β€ 0 / missing) are skipped safely. Output stays fully opaque (a floor is a solid surface). |
floor-texture-resolver.test.mjs |
10 | resolveFloorType(room) (spec {floor_type, carpet_type} β carpet_low/carpet_high; legacy combined forms; hardwood/laminateβwood, graniteβgranite_light aliases; direct registry keys; case/whitespace-insensitive; unknownβdefault) and normalizeFloorRotationDeg(deg) (non-finiteβ0, quantise to 0.1Β° so getComputedStyle noise can't churn the cache, wrap to [-180, 180)). |
src/renderers/ β pure math extracted from render methods¶
These computations previously lived inline in renderX(ctx) methods; they were
extracted into exported pure functions (behaviour-preserving; the render methods
now delegate) so they could be unit-tested off the render path.
| File | Cases | What it guards |
|---|---|---|
theme-parsers.test.mjs |
28 | _parseColorMix / parseScalarThemeValue / alphaPercentFromHex (+ serialize/clamp) β the CSS color-mix + scalar + alpha-hex parsers behind the theme editor. |
map-geometry.test.mjs |
20 | _polygonCentroid (signed-area + degenerate fallback), _savedZoneBbox, _overlayTransform (object-fit:contain letterbox). |
maintenance-derive.test.mjs |
18 | maintenanceDueInBucket(item, now, t) (due-in projection with 3-day / 0.1-h-per-day guards; now/t injected), needs-attention verdict, remaining-percent branch. |
mapping-review-outlier.test.mjs |
7 | computeJobBoundsOutlier β leave-one-out bounds-outlier detection (union bbox + 10% per-axis tolerance). |
src/bindings/ and src/controllers/¶
| File | Cases | What it guards |
|---|---|---|
controllers/learning-controller-progress.test.mjs |
19 | getRoomProgressSnapshot (per-room progress flags/percent) and _computeProgressPercent. |
bindings/room-rules-payload.test.mjs |
9 | _buildRulePayload β rule draft β persisted payload (modifier/blocker action, clean_passes 1|2 gate, fan-out filter). |
How it's tested¶
Every file uses Node's built-in runner and assertions β no jsdom, no DOM shim:
Two patterns:
- Mixin accessors (
src/state, controllers) β build a stub proto and set only the fields the method reads: - Module functions (
src/cards,src/theme-tokens, the extracted renderer helpers) β import and call directly, likesrc/cards/map-room-color.test.mjs.
Anything that needs a rendered card (DOM, computed styles, visual output) is a render-harness spec instead, not a unit test.
CI¶
npm run test:units runs in
node-tests.yml
on any src/** change, in the pinned Playwright image. So a src/state/ or
src/renderers/ refactor that breaks a derivation fails CI on push. The gate
authority is npm run test:units returning non-zero on any failure β mirroring
pytest tests --no-cov for the Python suite.
The three tracks¶
The frontend has three separate test tracks, none of them pytest:
- Unit tests (this doc) β
npm run test:units, pure logic at the module boundary. - Render harness β
npm run test:harness, the rendered card headless: smoke, visual regression, CVD, shape marks, intake. - i18n / sanitiser node tests β
npm run check:i18n(scripts/check-i18n.mjs, the translate/escape trust-model incl. the double-escape case) andscripts/sanitize-locale.test.mjs(the locale-sanitiser browser gates). Both run innode-tests.yml.