Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Desktop App Testing

Status: Current Last updated: 2026-10-27 (commit c0e233d0)

This document covers the testing strategy for the Chatter desktop app (apps/chatter-desktop/). Testing is split into three tiers by speed and scope.

Testing Tiers

┌─────────────────────────────────────────────────────────┐
│  Tier 3: E2E (WebdriverIO + tauri-driver)               │
│  Real app, real DOM, real IPC. Slow (~5-10s/test).       │
│  Catches: rendering bugs, IPC wiring, platform quirks.   │
│  Run: manually before releases, optionally in CI.        │
├─────────────────────────────────────────────────────────┤
│  Tier 2: Rust integration tests                          │
│  Real validation pipeline, real event bridge, no GUI.    │
│  Catches: serialization mismatches, event ordering,      │
│  stats consistency, single-file handling.                 │
│  Use: affected behavior and final candidate checks.      │
├─────────────────────────────────────────────────────────┤
│  Tier 1: Unit tests (Rust + TypeScript)                  │
│  Pure functions and thin runtime seams in isolation.     │
│  Catches: protocol drift, reducer bugs, CLAN math.       │
│  Use: affected behavior and final candidate checks.      │
└─────────────────────────────────────────────────────────┘

Tier 2 exercises the shared validation runner and event bridge, not the entire native app. Command-context tests must enter tauri::async_runtime::block_on: a plain-thread call cannot detect nested-runtime failures at the Tauri boundary. Use isolated cache directories, never the real user cache. Passing these tests does not establish that native menus, dialogs, drag/drop, signing or installation work; those require their respective runtime or release checks.

Choose checks deliberately for the changed behavior and retain final candidate evidence. These tiers describe scope, not automatic per-commit gates. Follow the repository’s contributor and publication workflow.

Release-facing behavioral contracts

The desktop links the same validation engine as the CLI. Keep CHAT semantics in the shared Rust implementation; use canonical specs/reference files for desktop boundary tests rather than inventing a second fixture oracle.

Source revisions and Open in CLAN

Every available diagnostic source context carries a Rust-issued SourceRevision of the exact source used for presentation. The frontend forwards that identity alongside the admitted coordinates. Opening in CLAN reads the file again and rejects a changed revision with an instruction to revalidate. Missing files remain read failures; missing or malformed revisions fail request deserialization.

The resolved target owns its checked path, coordinates and message, and dispatch consumes that target. This removes the separate unchecked path at dispatch and prevents callers from constructing resolved coordinates directly. The bridge controls cover unchanged bytes, shifted lines, same-length edits, restored bytes and deleted files; the frontend control checks unchanged revision forwarding. These checks do not establish native Apple Event delivery or prevent another process from editing the file after admission and before CLAN reads it.

File outcomes and completion

validationState.ts::fileOutcome projects a streamed FileEntry into a discriminated union: pending, valid, or problem with an explanation. Both the file tree and the detail panel use it, and fileStatusLabel words it for the detail panel and the text export. In particular, readError, internalFailure, roundtripFailed and cached invalid statuses must remain visible when there is no diagnostic array. Do not infer success from an empty array.

shouldShowAllFilesValid requires a finished phase whose passed is true (the runner’s own verdict, the one the CLI’s exit status reads) and no file with anything to show. A cancelled run is the separate stopped phase, and a target with no transcript is nothingFound, so neither can reach this check. An internal failure increments internalFailures, not the valid or invalid counter: CHAT validity remains undetermined. finishedRunSummary is shared by the window title, notification and status bar. Regression tests exercise failure statuses without diagnostics, warning visibility, cancellation, empty populations and success. When adding a new wire status, update the exhaustive projection, not a second component-local classification.

Update lifecycle

updates.ts owns an idle/checking state. A checking state carries the single in-flight result and feedback mode. Launch, six-hour background and manual menu requests join that operation; a manual join promotes it to visible feedback. There can be only one prompt/install sequence for overlapping requests. Completion or failure returns the capability to idle so a later retry can start.

The update seam tests cover accepted/declined installs, no update, network failure, overlapping requests, a manual join, retry, and native error-dialog failure. The latter must not reject a fire-and-forget menu callback. These tests use transport doubles: they do not verify a real signature, actual installation, or relaunch. Installer and updater artifacts require separate release evidence.

Stored Unicode identity

Reveal-in-file-manager travels through the validation-target capability and the typed command protocol, not a component-local Tauri import. Transport-double tests retain the exact Unicode path and propagate native failures. They do not launch or certify the operating system’s file manager.

The runtime bridge test copies the canonical W109 media specimen into an owned temporary directory, then validates it through stored NFD, NFC alias (where the filesystem supports it), and directory targets from inside Tauri’s runtime. It requires the same both-side W109, no E531 mismatch, and nonempty HTML/plain renderings. Production identity comes from the shared stored-name resolver; the desktop must not independently normalize paths or downgrade failed name resolution to anonymous validation.

Export admission and failure preservation

The async command tests exercise text export with each file status, including failures without diagnostic cards and pending results. Text export deserializes typed file records and the same status enum used by the event bridge; it no longer navigates arbitrary JSON with question-mark fallbacks. Required paths and rendered diagnostic text must be present. An unknown status or malformed record refuses before filesystem writing, preserving an existing report.

Status and diagnostics are separate facts: a roundtrip failure can coexist with diagnostic text, and neither may suppress the other. Tests preserve rendered text verbatim. JSON export retains its existing wire representation. Neither per-file export is a whole-run coverage certificate; cancellation/session metadata is not presently included, as documented in the user guide.

Regular report export shares staged publication with CLI audit output. Source identity admission precedes staging; missing sources and aliases refuse export. ReportWriter::finish must flush successfully before it yields publication ownership. Failed writes discard staging, preserve existing reports and leave new destinations absent. Unix runtime tests impose a child-only file-size limit to exercise real write failures in both export formats. Other runtime controls cover symlink target selection, retained permissions and unchanged sibling hard links. Shared owner tests cover abandonment, destination replacement and dangling symlinks. Non-regular outputs remain streams; concurrent hostile filesystem mutation and native save-dialog behavior are outside these tests.

Streaming delivery and reducer cost

Each run owns an ordered, nonempty event buffer. File results flush on the next animation frame or at 50 completed files. Lifecycle and terminal events flush immediately; terminal delivery closes the buffer. Disposal discards unpublished updates, and a cancelled frame cannot flush a newer buffer.

The reducer exposes a read-only file map and copies it only on the first file update in a delivery. A counted-iteration test checks that 100 updates to an existing 100-file population visit those 100 entries once, while preserving the previous snapshot. This measures copying work, not wall-clock responsiveness or whole-run linear complexity. Capability tests cover frame/threshold delivery, terminal flushing, disposal and replacement-run isolation. Native observation is still required to qualify responsiveness and cancellation in the webview.

Before calling a desktop candidate ready

Run the focused frontend seam tests, compile the production frontend, and run the Rust bridge tests on the final sources. Inspect the actual app on supported platforms: open a canonical valid file, a diagnostic fixture and a failing target; verify cancellation/revalidation, copy/export, menus and update feedback. Do not call the app ready solely because TypeScript compiled or the headless bridge passed. Use the coordinated release process for signed installers and updater manifests, with versions tied to the exact release commit.

Tier 1 & 2: Unit and integration tests

Candidate evidence matrix

An implemented test is not a passing receipt, and a headless receipt is not native interaction evidence. Record candidate identity, platform, actual result and remaining gaps in the release record; do not copy historical success into a new candidate’s acceptance.

WorkflowExisting automated boundaryRequired native observation
Valid and invalid filesReference corpus and canonical spec bridge testsSelect each; verify visible verdict, diagnostic navigation and completion
Unreadable or unsupported targetTyped outcome projection and path-contract testsSurface failure visibly; do not show an all-valid result
Directory selectionNested discovery and event/statistics bridge testsSelect a directory; inspect relative paths and final totals
Unicode pathsStored-identity W109 runtime bridge testOpen an accented path through the native chooser and inspect media warnings
Cancel and revalidateRunner lifecycle and cancelled-state seam testsCancel an active run, change input, revalidate; no stale result may claim success
Copy and exportTyped export admission and refusal testsCheck clipboard and saved output, including failure without diagnostic cards
Menus and updatesSingle-flight update seam testsExercise menu feedback; test signed installation/update separately
Settings preservationNo complete native proof supplied by bridge testsVerify preferences survive relaunch and upgrade without touching unrelated settings

Declare platform support and installation/update acceptance explicitly. A configured CI target or available installer is not evidence that every workflow above has been exercised on that platform.

Running

# TypeScript capability/seam tests
cd apps/chatter-desktop && npm run test:unit

# Rust contract/integration tests
cargo test -p chatter-desktop --test validation_bridge

# Run ownership across startup, replacement and cancellation
cargo test -p chatter-desktop --lib validation_session

What they cover

TestWhat it verifies
apps/chatter-desktop/tests/unit/validationRunner.test.cjsRun-scoped events and cancellation, pending listener/startup disposal, startup ordering and retry, and exactly-once listener cleanup
validation_session::testsRuntime-entered pending/running ownership, stale cancellation/completion, scoped event envelopes and required nonempty run identity; these are headless boundaries, not native UI observations
apps/chatter-desktop/tests/unit/validationState.test.cjsValidation reducer computes relative file names and merges diagnostics/status immutably
reference_corpus_no_hard_errorsevery file under corpus/reference/ produces zero Severity::Error (warnings allowed)
event_lifecycle_has_correct_sequenceDiscovering → Started → FileComplete×N → Finished ordering
frontend_events_serialize_to_expected_json_shapeInner validation events retain their type field and camelCase payload; diagnostics include renderedText. The session owner adds the outer run identity.
protocol_contracts_serialize_to_expected_json_shapeRust command/event constants and request payloads stay aligned with the TypeScript protocol module
single_file_validationSingle-file path validates exactly the selected file
finished_stats_match_file_eventsTyped valid, invalid, parse-error and internal-failure counts account for the population; FileComplete count matches
rendered_html_present_for_errorsEvery diagnostic carries non-empty miette HTML with box-drawing characters and style= attributes (ANSI colors converted to HTML)

Adding new tests

Test file: apps/chatter-desktop/src-tauri/tests/validation_bridge.rs

The tests use collect_events() which runs the real validation pipeline and collects all FrontendEvent values. To test a specific scenario:

#![allow(unused)]
fn main() {
#[test]
fn my_scenario() {
    let target = workspace_root().join("path/to/corpus");
    let events = collect_events(&target);
    let summary = summarize(&events);
    // assert on summary fields or individual events
}
}

Miette rendering pipeline

Error rendering is server-side. Each FrontendDiagnostic carries two renderings:

  • rendered_html: render_error_with_miette_with_source_colored() produces ANSI-colored text, ansi-to-html converts it to HTML <span style="...">. The frontend displays it in a <pre> block via dangerouslySetInnerHTML. This guarantees identical output to the CLI.
  • rendered_text: render_error_with_miette_with_source() produces plain text (no ANSI codes) for clean clipboard copy-paste.

The rendered_html_present_for_errors integration test verifies that every error diagnostic includes non-empty HTML containing miette box-drawing characters and style= attributes from ANSI color conversion.

TypeScript seam tests

The TypeScript unit tests compile a focused subset of apps/chatter-desktop/src/ to a temporary CommonJS directory, then run Node’s built-in test runner against the compiled output. This keeps the test toolchain small while still exercising the runtime seam as real JavaScript.

  • Runner script: apps/chatter-desktop/scripts/run-unit-tests.mjs
  • Compile config: apps/chatter-desktop/tsconfig.unit.json
  • Test files: apps/chatter-desktop/tests/unit/*.test.cjs

tests/unit/renderedResults.test.ts is compiled in the same deck and uses the installed react-dom/server renderer. Typed production events travel through applyValidationEvent into the actual FileTree and ErrorPanel: no-card failure messages, pending versus valid results, warning retention, and the runner-owned all-valid verdict are checked in their rendered markup. FileTree imports its phase type directly from the canonical state owner; no native transport is loaded by this deck. DOM types are compile-time only.

These checks add no browser, DOM emulator or runtime dependency. Static markup does not prove clicks, effects, clipboard, native IPC, styling, installation or updates. Cancellation/incomplete events test refusal of an all-valid claim, not a live cancellation interaction.

TypeScript ↔ Rust contract

The Rust integration tests verify that serialized JSON matches what the TypeScript frontend expects. If you change a field name or event structure in events.rs, the frontend_events_serialize_to_expected_json_shape test will catch the mismatch before you discover it at runtime.

The key serde attributes:

  • #[serde(tag = "type", rename_all = "camelCase")] on enums, variant names become camelCase tag values (fileComplete, not FileComplete)
  • #[serde(rename_all = "camelCase")] on individual variants, field names become camelCase (totalFiles, not total_files)
  • Both must be present: the enum-level rename_all only affects tag names, not field names within variants

Tier 3: E2E Tests (WebdriverIO)

Prerequisites

cargo install tauri-driver    # WebDriver backend for Tauri (Linux/Windows only)
cargo tauri build --debug     # Build the app binary

Note: the checked-in configuration drives standalone tauri-driver, which supports Linux and Windows, not macOS. This is a limitation of that route, not of every available Tauri automation option; see the evaluation below.

Running

# Terminal 1: start tauri-driver (WebDriver server on :4444)
tauri-driver

# Terminal 2: run the tests
cd apps/chatter-desktop
npm run test:e2e

What they cover

The smoke tests in tests/e2e/smoke.spec.ts verify that the app launches and renders the expected UI elements:

  • Drop zone with Choose File / Choose Folder buttons
  • Empty file tree (“No files loaded”)
  • Empty error panel (“Select a file to view errors”)
  • Status bar showing “Ready”

Binary discovery

The ESM config derives the workspace root from its own import.meta.url, not the shell’s current directory. It admits target/debug/chatter-desktop on Linux and target/debug/chatter-desktop.exe on Windows, matching the Cargo binary declaration rather than the packaged macOS Chatter.app name.

For a custom Cargo target directory, set CHATTER_DESKTOP_TARGET_DIR to its absolute path; discovery adds debug and the native binary name. For release, cross-target, or other build layouts, set CHATTER_DESKTOP_BINARY to the exact absolute executable path instead. Set only one override. Relative paths, empty values, missing/non-file binaries, and unsupported platforms refuse before WebDriver connection. Linux also checks execute permission. Cargo configuration and CARGO_TARGET_DIR are not inferred: an explicit override must match the actual build output. Windows file existence is not executable-format evidence.

tests/unit/desktopSmokeBinary.test.mjs exercises this admission through the existing Node deck, with owned temporary files, including the old apps/target decoy and refusal cases. A simulated platform checks suffix policy on the host filesystem, not another OS. No binary or driver is launched by these controls. Retain a successful supported-platform native run separately before treating this launch-only deck as usable release evidence.

Limitations

The native file picker is outside this suite’s WebDriver-controlled DOM. The checked-in smoke suite verifies launch UI only: it does not yet automate file selection, a validation run, cancellation, export, or native menus. The Rust integration tests cover the shared validation pipeline and bridge, not those native interactions.

Do not copy a direct window.__TAURI__.core.invoke("validate", { path }) example: this app does not enable the global Tauri API, and validate takes one request argument (a ValidateRequest: path, roundtrip, parser kind, strict linkers and jobs), not a bare path. Production calls use the typed runtime capability and transport. A future automated native validation test must use that contract, subscribe before starting the run, await its terminal event with a bounded timeout, and isolate cache/settings. A fixed sleep is not proof of completion. Do not add a production test-only command or bypass admission just to drive a test.

Adding E2E tests

Test file: apps/chatter-desktop/tests/e2e/*.spec.ts

Follow the existing selector-based launch checks for layout assertions. New validation-flow coverage must meet the lifecycle and isolation requirements above; passing a DOM assertion after a delay is not a validation receipt.

When to run E2E tests

  • Before releases: manual run to verify the built app works end-to-end
  • Optionally in CI: requires tauri-driver and a display server (Xvfb on Linux). Slow, so consider running only on release branches.
  • Not on every commit: the Rust integration tests are fast and cover more ground

Platform-Specific Considerations

PlatformWebView engineE2E support
macOSWKWebViewNot supported by our current standalone-driver configuration; service-based alternatives exist
WindowsWebView2 (Chromium)Full support via tauri-driver
LinuxWebKitGTKFull support via tauri-driver; requires Xvfb for headless

Current macOS coverage: use the Rust integration tests (Tier 2) and explicit native smoke evidence. Do not infer native coverage from browser or transport doubles. The service-based option below has not been adopted or verified here.

Automation follow-up: evaluation, not implementation

The Tauri WebDriver guide documents @wdio/tauri-service with an embedded WebDriver plugin supporting macOS as well as Linux and Windows. It also documents renderer-only browser mode and external-driver alternatives. See the WebdriverIO Tauri documentation. These capabilities are upstream claims, not passing Chatter test receipts.

The least disruptive next experiment is rendered-component testing through the existing DesktopRuntimeProvider injection seam: valid, invalid and internal failure results; cancel/revalidate; export failure; and overlapping update requests. Keep typed event sequences and bounded completion assertions. This would complement the existing Rust bridge tests, not replace real IPC evidence.

Before adding a native automation dependency, review its permissions, lifecycle, dependency cost and release exclusion. An embedded control server must never ship in production artifacts; require an explicit test-build configuration and an automated release-exclusion check. Do not add a second validator, test-only production command, or global IPC escape hatch. Keep settings/cache isolated.

The native smoke deck only checks launch UI. Binary discovery has finite Node controls, not a successful native WebDriver receipt. Correct path admission alone does not certify that the executable launches or its workflows work.

CSS rendering differs slightly between WebKit (Linux) and Chromium (Windows). Visual regressions are possible, consider screenshot comparison tests if this becomes a problem.

Test Data

All tests use the reference corpus at corpus/reference/. This corpus is checked into the repo and must always pass validation with zero hard errors (warnings are allowed). The exact set of files and the current warning-emitting files are whatever rg --files corpus/reference -g '*.cha' and the validator report, do not hard-code those lists here.

Do not create ad-hoc .cha test files. Use existing reference corpus files or ask the user to provide test data.

CI Integration

Add to the existing CI workflow:

# Rust integration tests (fast, always run)
- name: Desktop integration tests
  run: cargo test -p chatter-desktop --test validation_bridge

# E2E tests (slow, release branches only)
- name: Build desktop app
  if: startsWith(github.ref, 'refs/heads/release')
  run: cargo tauri build --debug
- name: E2E smoke tests
  if: startsWith(github.ref, 'refs/heads/release')
  run: |
    tauri-driver &
    sleep 2
    cd apps/chatter-desktop && npm run test:e2e

This page last changed: 2026-10-27 (commit c0e233d0). The whole book last changed: 2026-10-27 (commit 5bfee2f9).