# Playwright Coverage Fill — Design

**Date:** 2026-06-06
**Status:** Approved (brainstorming complete) → ready for writing-plans
**Component:** #9 Central UI — `tests/ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests`
**Predecessor:** [2026-06-05 Playwright Coverage Expansion](2026-06-05-playwright-coverage-expansion-design.md) (the first wave; this is the close-out)

## Goal

Close the remaining functional and edge-case gaps found in the 2026-06-06 coverage
re-audit — every untested Tier 1–3 page plus a cross-cutting edge-case sweep on the
already-covered pages — delivered as **4 risk-tiered waves** inside the existing
xunit + `PlaywrightFixture` harness, against the live 8-node docker cluster.

## Background — the re-audit

After the first expansion (suite at 83 passing), a re-audit mapped all 38 routable pages
against the test catalog. Findings:

- ~14 of 38 pages have real functional coverage; the rest are nav-only (h4 heading) or
  untested.
- **Biggest blind spot:** `InstanceConfigure` (`/deployment/instances/{id}/configure`) —
  the most complex mutating page (5 override subsystems, browse dialog, test-bindings) —
  has zero coverage.
- Edge cases are systematically uncovered across *all* pages: no duplicate-name validation,
  no cancel flows, no empty states, no pagination, no filter-combination, no error/500
  paths, no wrong-passphrase import.
- The suite is happy-path + outcome-tolerant by design (a deliberate, defensible choice
  that matches the relay reality) — but a page that renders-but-misbehaves on a non-default
  input can still stay green.

## Decisions (settled during brainstorming)

| # | Decision | Choice | Rationale |
|---|----------|--------|-----------|
| D1 | How much to take on | **Everything** — Tier 1–3 functional gaps + the edge sweep | User chose full close-out; phased so it stays tractable. |
| D2 | Structure | **Risk-tiered phased waves** (4) | Front-loads highest-risk surfaces; each wave is a shippable, green, zero-residue increment; mirrors the structure that worked for wave 1. |
| D3 | Test selectors on hook-poor pages | **Add `data-test` hooks as needed** (minimal, additive, non-functional) | Deep assertions on the complex forms (esp. `InstanceConfigure`) are otherwise brittle; matches the convention the audited Audit/SiteCalls/Transport pages already follow. |
| D4 | Real-time / streaming tests | **Drive real behavior, outcome-tolerant, generous timeouts, `SkippableFact`** | Highest value; managed flake risk on a handful of tests, with a documented downgrade-to-render-guard fallback. |

## Key enabler — the CLI mirrors the whole surface

The `scadabridge` CLI exposes create/list/delete (and read-back) for **every** entity the
new suites need, including the entire `InstanceConfigure` surface:

- `instance set-bindings | set-overrides | alarm-override set/delete/list |
  native-alarm-source set/clear | set-area | diff | get`
- `security api-key create/list/delete/update/set-methods`
- `data-connection`, `db-connection`, `external-system` (+ `method`), `api-method`,
  `shared-script`, `notification` (list/create/delete + `smtp`) create/list/delete

→ New functional suites need **no DB seeders**. Fixtures are CLI-provisioned and
persistence is verified by CLI read-back (`instance get`, `security api-key list`). DB
seeding remains only in the existing Audit/SiteCalls/Notification *report* edge tests that
require a `Parked` row (site SQLite isn't reachable, so those stay direct-SQL).

## Wave structure

| Wave | Theme | New suites | New fixtures |
|------|-------|-----------|--------------|
| **1** | Tier 1 — deepest mutating surfaces | `InstanceConfigureTests`, `ApiKeyCrudTests`, `TransportExportTests` (+ wrong-passphrase import negative) | `InstanceConfigureFixture`, `ApiSurfaceFixture` |
| **2** | Tier 2 — real-time / relay | `DeploymentsRealtimeTests`, `TopologyAreaTests`, `DebugViewTests`, `ParkedMessagesActionTests`, Discard click-throughs (SiteCalls + Notification) | reuse `DeploymentFixture` / `InstanceConfigureFixture` |
| **3** | Tier 3 — config CRUD breadth | `NotificationListCrudTests`, `SmtpConfigTests`, `NotificationKpisTests`, `DataConnectionCrudTests`, `ExternalSystemCrudTests`, `SharedScriptCrudTests`, `ApiMethodFormTests`, `EventLogsTests`, `AuditConfigurationTests` | small per-suite CLI helpers |
| **4** | Cross-cutting edge sweep | extend `SiteCrudTests`, `TemplateCrudTests`, `LdapMappingCrudTests`, `AuditLogPageTests`, `SiteCallsPageTests`, `NotificationActionTests` | none |

Each wave ends green with zero residue — a clean stop/ship point. Inside a wave, suites
are independent (disjoint fixtures/files) → parallel-dispatchable. Rough size: W1 ≈ 3 files
/~12 tests, W2 ≈ 5 files/~14, W3 ≈ 9 files/~20, W4 ≈ 6 extended files/~20. Total ≈
**23 files, ~65 new cases.**

## Shared infrastructure

**CLI helper extensions** (`CliRunner.Helpers.cs`, same throw-vs-swallow split):
- *Provision (throw):* `CreateDataConnectionAsync`, `CreateExternalSystemAsync`,
  `CreateApiMethodAsync`, `CreateNotificationListAsync`, `CreateSharedScriptAsync`.
- *Verify (read-back, throw):* `GetInstanceAsync(id)`, `ListApiKeysAsync`.
- *Teardown (best-effort):* `DeleteApiKeyAsync`, `DeleteDataConnectionAsync`,
  `DeleteExternalSystemAsync`, `DeleteNotificationListAsync`, `DeleteSharedScriptAsync` —
  all keyed on `zztest-*`.

**New fixtures** (`IAsyncLifetime` + `IClassFixture`, partial-init guard + `Available`
flag, mirroring `DeploymentFixture`):
- **`InstanceConfigureFixture`** — site-a: `zztest` template + attribute(s) + `zztest`
  data-connection + an instance, **deployed**. Disposes instance → connection → template.
- **`ApiSurfaceFixture`** — `zztest` external-system + one api-method, so the API-key form
  renders method checkboxes. Disposes api-method + external-system; created keys deleted
  per-test via `security api-key delete`.

**`data-test` hooks** (minimal, additive, each its own tiny commit so the app diff is
auditable/revertable): `InstanceConfigure.razor` subsystem anchors; row anchors on
`EventLogs.razor` / `ConfigurationAuditLog.razor`; save/result anchors on a few Design
forms. Only where no stable existing selector (id/aria-label/text) exists.

**Reused as-is:** `ClusterAvailability` skip gate + `SkipSummaryReporter`;
`PlaywrightFixture`; toast = `.toast` web-first `ToHaveCountAsync(1)`; confirm =
`.modal-footer .btn-danger` (Delete) / `.btn-primary` (Confirm); `zztest-<kind>-<8hex>`
naming; `PlaywrightDbConnection` (only Wave-4 report-page Parked-row edge tests).

## Per-wave test shapes

### Wave 1 — Tier 1 mutating surfaces

**`InstanceConfigureTests`** (`InstanceConfigureFixture`):
- *Bindings round-trip* — bulk "Assign all to" the zztest connection → Save Bindings →
  toast; verify persisted via `GetInstanceAsync` (not just a toast).
- *Attribute-override round-trip* — type text override → Save Overrides → toast →
  `GetInstanceAsync` shows it.
- *Area reassignment* — select area → Set Area → toast → `GetInstanceAsync` confirms.
- *(⚠ feasibility)* Alarm-override Edit→Save→badge→Clear — needs a template attribute with
  an alarm; resolve via `instance alarm-override set` precondition or downgrade to a
  section-renders assertion. Other three tests don't depend on alarms.
- *Edge* — `/deployment/instances/999999/configure` → `alert-danger` not-found.

**`ApiKeyCrudTests`** (`ApiSurfaceFixture`; teardown `security api-key delete`):
- Create→token reveal (`data-test="created-token"`, Copy works); Enable/Disable toast +
  badge; Delete confirm `.btn-danger` → row gone; Edit (name disabled, toggle method).
- Edges: empty name → validation; uncheck all methods → "select at least one".

**`TransportExportTests`** + negative import:
- Export — CLI-create zztest template → `/design/transport/export` → select → passphrase →
  Export → assert the bundle download (filename/size).
- Wrong-passphrase import — feed the exported bundle to the import wizard with a bad
  passphrase → Unlock error state (passphrase input stays, error shown), no `diff-summary`.

### Wave 2 — Tier 2 real-time / relay (drive behavior, tolerant, generous timeouts, `SkippableFact`)

- **`DeploymentsRealtimeTests`** — on `/deployment/deployments`, CLI `instance deploy` a
  fixture instance → status row appears within ~20s (SignalR push). Second: Pause → deploy
  → row absent → Refresh → row present.
- **`TopologyAreaTests`** — create area (toolbar + site context-menu); inline rename
  (Enter commits + toast; Escape reverts); move area; move instance; *(⚠)* Diff dialog
  opens for a deployed instance (tolerant).
- **`DebugViewTests`** — select site-a + enabled instance → Connect → badge "Live" +
  snapshot/table region renders (tolerant: rows *or* "Waiting for snapshot"). Disconnect →
  tables gone, selects re-enabled.
- **`ParkedMessagesActionTests`** — render+controls guard on the page's own Retry/Discard
  button presence/enablement (site SQLite not seedable → no click-through here).
- **Discard click-throughs** — add the symmetric Discard-clicked test to
  `SiteCallsPageTests` and `NotificationActionTests` (Retry is already click-through-tested).

### Wave 3 — Tier 3 config CRUD (breadth; CLI teardown by zztest name)

- **`NotificationListCrudTests`** — create → add recipient → delete recipient (no confirm)
  → delete list (confirm + "Deleted." toast).
- **`SmtpConfigTests`** (RequireAdmin) — add (host+from required) → saved toast →
  credentials "(stored)"; edit cancel/save.
- **`NotificationKpisTests`** — load → 5 tiles resolve (or "—") → Refresh spinner.
- **`DataConnectionCrudTests` / `ExternalSystemCrudTests` / `SharedScriptCrudTests`** —
  create→(edit)→delete round-trips.
- **`ApiMethodFormTests`** — create method (name+timeout, minimal Monaco script, default
  schema) → Save → appears under external systems (Monaco interaction minimal/tolerant).
- **`EventLogsTests`** — Search disabled until site selected; select site → Search; filter
  by Severity; row expand/collapse; Load more appends.
- **`AuditConfigurationTests`** — load `/audit/configuration`; search narrows; Prev disabled
  on page 1; large-state modal open/close; copy-entity-id toast; `?bundleImportId=` chip
  drill-in.

### Wave 4 — cross-cutting edge sweep (⚠ verify each page's real validation first)

- **Sites** — duplicate-identifier (assert whatever the app surfaces), cancel-from-edit,
  invalid Akka/gRPC URL *if validated*.
- **Templates** — duplicate-name, cancel, edit existing attribute, delete-when-instances-
  exist (blocking).
- **LDAP** — duplicate group name, missing-field validation.
- **Audit Log** — filter combination (channel+site+time), empty-results-after-Apply, drawer
  close (X/Escape), non-API row has no cURL button, pagination.
- **Site Calls** — filter by status, empty state, keyset pagination (`site-calls-prev/next`
  hooks exist, unused today).
- **Notification Report** — filter combos, "Stuck only", detail modal open/close, pagination.

## Error handling & verification strategy

**Validation-behavior protocol (the ⚠ items).** Before asserting a specific failure mode,
the implementer reads the page code-behind to learn what it actually does — inline
`_formError` vs error toast vs DB-constraint bubble vs silent success — and asserts that
reality. Where the app doesn't validate (relies on a DB constraint with no friendly
message), the test asserts the real surfaced behavior and a code comment notes the gap.
Same protocol resolves the alarm-override and Diff-dialog feasibility ⚠s.

**Flake management (real-time/streaming).** Web-first assertions only (`Expect(...)` with
explicit timeouts), never `WaitForTimeout` + read. Generous ceilings (~20s). Outcome-
tolerant where the cluster's response isn't deterministic. All `SkippableFact` +
`ClusterAvailability` gated. Documented fallback: if a real-time test is irreducibly flaky,
downgrade *that test only* to a render+controls guard.

**Teardown & residue.** Best-effort cleanup in `DisposeAsync`/`finally`, keyed on
`zztest-*`. Each wave ends with the no-residue check (`site/template/instance/
data-connection/api-key/notification list` via CLI + DB marker scan) → zero. Mutating tests
that target real site-a leave it as found.

**Per-wave gate.** Wave is "done" only when: new tests pass against the live cluster, the
full suite stays at **0 failed** (skips logged), zero residue, and `dotnet build` is clean
(`TreatWarningsAsErrors=true`). `data-test` additions verified not to alter rendered
behavior.

## Scope guard (YAGNI)

No new page-object framework, no CI wiring, no parallelization/runner changes, no
visual-regression/screenshot testing, no perf testing. New `data-test` attributes are the
only app-code change and are purely additive. Everything slots into the existing structure.

## Success criteria

All 4 waves merged; ~23 files / ~65 new cases; every Tier 1–3 page from the audit has
functional coverage; the edge sweep adds duplicate/cancel/empty/filter-combo/pagination
assertions to the previously happy-path-only pages; suite green with logged skips; zero
residue.

## Native tasks

Brainstorming checklist tasks #73–#78 track this design through to writing-plans. The
implementation plan (produced next by the writing-plans skill) carries its own task set,
one wave at a time.
