Skip to content

Implement stateful Z2M engine and enhance UI test coverage#1

Merged
tashda merged 9 commits into
mainfrom
dev
Apr 24, 2026
Merged

Implement stateful Z2M engine and enhance UI test coverage#1
tashda merged 9 commits into
mainfrom
dev

Conversation

@tashda
Copy link
Copy Markdown
Owner

@tashda tashda commented Apr 24, 2026

No description provided.

tashda and others added 9 commits April 23, 2026 21:27
The mock was a passive WS↔MQTT pipe, so every bridge/request/* from the
app silently dropped (no matching bridge/response/*) and <device>/set
never flowed back as a state update. That's the root cause of "can't
change states" in the dev stack.

- seeder.py: full rewrite as a stateful engine. Owns authoritative state
  for devices, groups, bridge info and health; merges /set payloads into
  state and republishes; answers every bridge/request/<path> with a
  matching bridge/response/<path> envelope; emits bridge/event for
  rename/leave/interview/permit_join; simulates OTA progress ticks.
  Header docstring enumerates what we faithfully simulate vs. stub.
- fixtures.py: 34 devices (up from 9) + 3 groups, covering lights, plugs,
  sensors, motion/contact/leak/smoke/vibration, switches/dimmers/buttons,
  curtain, siren, knob, locks, outdoor sensor. Retains the 9 original
  friendly_names UI tests rely on; renames Living Room Air Purifier to
  Bathroom Fan to match DeviceDetailUITests expectations.
- bridge.py: stop echoing client command topics (/set, /get,
  bridge/request/*) back to WS subscribers — real z2m never does, and
  the echo was being misrouted as a device-state update by the app.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
UI tests were hitting the bridge at ws://localhost:8080/api with no
?token= query string, so the bridge (AUTH_TOKEN enforced) closed every
connection with 1008 Invalid token. Tests that 'passed' were asserting
against the app's empty-state UI; any test that needed real data from
the bridge (groups, device state, logs) failed.

- UITestHelpers.launchForTesting now passes UI_TEST_Z2M_TOKEN.
- AppEnvironment.start reads UI_TEST_Z2M_TOKEN and forwards it as the
  ConnectionConfig authToken.
- bridge.py logs every WS connect path + reject reason so future auth
  issues are obvious in container logs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Add XCUIApplication.revealSearchField() helper for views that use
  .searchToolbarBehavior(.minimize); scrolling does not reveal the field.
- DeviceListUITests/DeviceDetailUITests: use the helper instead of the
  old swipeDown approach.
- GroupsUITests: AddGroupSheet and AddSceneSheet dismiss via drag
  indicator (they have no Cancel button by design). Update the three
  tests that assumed a Cancel button to dismiss the sheet the way the
  app actually wants users to.

Each touched test now has a Behavior: comment describing what it is
meant to verify.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Swipe-actions tests used `app.cells.firstMatch` which could land on a
section header (DeviceListView groups by category by default) and
produce no swipe response. Target a known device by name instead.

The default XCUIElement.swipeLeft() is too short on iOS 26 to reveal
trailing swipe actions when `allowsFullSwipe: false`. Added
swipeLeftFar() helper that does a far-travel press-drag gesture.

RenameDeviceSheet and RemoveDeviceSheet dismiss via the drag indicator
(no Cancel button by design) — update tests to dismiss via swipeDown.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Nested NavigationStacks (LogsView/MainSettingsView wrap themselves in
their own NavigationStack inside the SettingsView push) mean the
assertion 'navigationBars.element(boundBy: 1) exists' never holds —
both titles render into the same visible nav bar.

- Settings tests: assert on navigationBars[title] for each pane
  (Server, General, MQTT, Adapter, Home Assistant, Logs).
- testGeneralSettingsApplyAndCancel: Apply is always rendered in the
  confirmationAction slot but starts DISABLED; Cancel only appears
  once changes exist. Assert the correct enabled/existence state.
- testGeneralSettingsHasLogLevelPicker: match the staticText row label
  instead of the enclosing cell.
- seeder.py: emit a debug bridge-log line on every drift tick so the
  Bridge Log view has content when tests arrive (the startup log line
  is non-retained and races the app connect).
- SEED_INTERVAL default lowered to 10s so activity-log state diffs
  arrive within test timeouts.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Connection editor: SettingsTextField does not set an accessibility
  identifier — locate the Host and Token rows by their enclosing cell
  containing the label StaticText, then type into the nested text field
  or secure field. Supply the mock bridge's auth token so the Connect
  flow reaches Main tab bar.
- Protocol picker test: accept any of StaticText "Protocol", Button
  "Protocol", or a SegmentedControl — iOS 26 renders .automatic in a
  Form as a menu button, not a Picker.
- PermitJoin: duration options are inside a Menu-style Picker, not
  always visible. Assert the Duration section header + Preset label
  are present. Dismiss via repeated app swipeDown so medium-detent
  sheets collapse cleanly.
- Logs entry detail test falls back to Activity (drift-produced rows)
  and asserts the Logs back button appears after push.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two tests expected behaviour that pre-dates commit e631ccf (the
"device type specific configuration" boilerplate filter) and a
paragraphFallback: false decision in makePairingGuide.

- testNormalizesNestedPairingFromNotes: paragraphs in a Pairing
  subsection become the summary, not imitation steps. Only real
  stepList blocks populate primarySteps (avoids double-display).
  Added Behavior: comments spelling this out.
- testCollectsOptionsAndPreservesResidualBlocks: switched the residual
  paragraph to a non-boilerplate sentence so miscSections count stays 1.
- Added testOptionsBoilerplateIsStripped to explicitly cover the filter.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
New tests covering the recent Home redesign and OTA-aware notification
coalescing:

- HomeLayoutStoreTests: first-launch default (Groups hidden), hide/show
  persistence, IndexSet move, move-before drop semantics.
- HomeSnapshotTests: coordinator exclusion from topology counts, LQI
  averaging, low-battery / weak-signal counts, PAN ID formatting,
  permit-join countdown derivation, devicesWithUpdates counter.
- NotificationCoalescingTests: OTA occurrences track distinct device
  entries; displaying(occurrence:) swaps subtitle+device but keeps
  the aggregated count and log-ID list.
- HomeUITests: Recent Events "Show All" opens Logs; Mesh card chevron
  pushes MeshDetailView.

DeviceDocNormalizerTests: replace the skip-on-missing-local-corpus test
with a curated set of inline markdown samples covering lights, sensors,
covers, switches, buttons, and locks. Runs in every environment.
Every added test carries a `Behavior:` comment.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Default setUp timeouts: waitForMainTab 30s → 15s; initial cell waits
  20s → 10s; Connection flow 30s → 15s; Logs setup 30s → 10s.
- Remove the stray Thread.sleep in testClearLogsEmptiesList.
- CLAUDE.md (and AGENTS.md mirror): document the 2-worker parallel
  test invocation (~40% speedup, 1618s → 968s on a 2024 Mac) and the
  updated engine behaviour (SEED_INTERVAL=10s, stateful request
  handler, OTA progress ticks).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant