Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 29s
Durable plan for the next step: drive the engine against the mock platform (spec/harness.sx :fetch + web/harness-web.sx simulate-click/DOM asserts), so fetch->swap->DOM behavior is tested without a browser — the same engine could drive a non-browser target. Phases: PoC (relate-delete), port the rest, trim Playwright to WASM-boot + content-addressed-load, stretch = non-browser renderer. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
111 lines
6.8 KiB
Markdown
111 lines
6.8 KiB
Markdown
# Plan: SX-native engine tests (browser-independent)
|
|
|
|
## Goal
|
|
|
|
Move the host's *interactive* test coverage from Playwright (`.spec.js`, drives a real
|
|
Chromium) into **SX harness tests** that drive the hypermedia engine against a **mock
|
|
platform** — no browser. Reserve Playwright for the one irreducible real-browser fact:
|
|
"the WASM kernel actually compiles, boots, and loads modules content-addressed."
|
|
|
|
**Why (the principle):** the SX engine (`web/engine.sx` + `web/orchestration.sx`) has no
|
|
hard browser dependency — it talks to a *platform* (fetch, DOM ops, timers) that is
|
|
injected. The harness supplies a mock platform, so engine behaviour (fetch → swap →
|
|
DOM mutation) is asserted with zero browser. The same engine could therefore drive
|
|
*something else* (a server-side DOM, a native UI) — the SX tests prove that
|
|
independence by running without one. This is consistent with
|
|
`[[project_zero_dependencies]]` and `[[feedback_runtime_control]]` (build IN the runtime).
|
|
|
|
## Current state (2026-06-29)
|
|
|
|
- **Already SX:** the 272 host conformance tests (`lib/host/tests/*.sx`, `spec/harness.sx`
|
|
mock-IO). The picker's *server contract* is SX too (`lib/host/tests/blog.sx`:
|
|
`picker form declaratively wired`, `load-more sentinel`, `no-sentinel-on-short-page`).
|
|
- **Still Playwright (`.spec.js`):** `lib/host/playwright/relate-picker.spec.js` (7 tests)
|
|
and `spa-check.spec.js` (4) — real-browser checks of populate / filter / paging /
|
|
relate-delete / remove-button / boosted-nav / error-retry / WASM boot.
|
|
|
|
## Infrastructure that already exists (the enabler — verified)
|
|
|
|
- `spec/harness.sx` — `make-harness`, `default-platform` with **`:fetch` overridable**
|
|
(`(fn (url &rest opts) {:status 200 :body "" :ok true})`), plus DOM ops, `:now`, etc.
|
|
- `web/harness-web.sx` — `(define-library (sx harness-web))` exports: `mock-element`,
|
|
`mock-set-attr!`, `mock-append-child!`, `mock-get-attr`, `mock-add-listener!`,
|
|
**`simulate-click` / `simulate-input` / `simulate-event`**, `assert-text`, `assert-attr`,
|
|
`assert-class`, `assert-no-class`, `assert-child-count`, `assert-event-fired`,
|
|
`make-web-harness`, render-audit helpers.
|
|
- `web/tests/` — existing SX engine tests: `test-orchestration.sx` (17 deftests),
|
|
`test-forms.sx` (25), `test-swap-integration.sx` (43, mock-response → swap → assert),
|
|
`test-engine.sx`, `test-handlers.sx`. **`test-swap-integration.sx` is the reference
|
|
pattern** (it sets `_mock-body`/`_mock-headers`/`_mock-content-type`, drives a swap,
|
|
asserts the result).
|
|
- Runner: `hosts/ocaml/bin/run_tests.ml` scans `spec/tests/`, `lib/tests/`, `web/tests/`
|
|
and loads `harness-web.sx` + `harness-reactive.sx`. Run via the `sx_test host="ocaml"`
|
|
MCP tool (or `./scripts/sx-build-all.sh`). JS runner: `hosts/javascript/run_tests.js`
|
|
also loads the web harnesses.
|
|
|
|
## Phases
|
|
|
|
### Phase 0 — Proof of concept (small): one behavior, SX
|
|
Port **relate → delete row** to an SX harness test (new `web/tests/test-relate-picker.sx`):
|
|
1. Build a mock DOM: a `.rp-results` `<ul>` containing one candidate `<li id="cand-related-x">`
|
|
with the relate `<form sx-post=/x/relate sx-target=#cand-related-x sx-swap=delete>`.
|
|
2. `process-elements` (or `bind-triggers`) the tree so the form's submit is bound.
|
|
3. Mock `:fetch` to return `{:status 200 :ok true :body ""}`.
|
|
4. `simulate-click` the button (or `simulate-event` "submit" on the form).
|
|
5. Assert the `<li>` is gone (`assert-child-count` results = 0).
|
|
This validates the **mock-DOM → execute-request → swap-dom-nodes** loop in SX end to end.
|
|
**If it reads cleanly, the rest is mechanical.**
|
|
|
|
### Phase 1 — Port the picker's interactive behaviors (medium)
|
|
Same file, more deftests, each = mock fetch + simulate + assert:
|
|
- **filter narrows**: `:fetch` returns N candidate rows for `q=...`; `simulate-input` the
|
|
filter; assert child-count == N.
|
|
- **sentinel paging**: `:fetch` returns rows + a `<li class=rp-more sx-trigger=revealed>`;
|
|
fire the revealed/intersect path; assert more rows appended, sentinel replaced.
|
|
- **load populate**: `load` trigger → fetch → assert results filled.
|
|
- **error/retry visible state**: `:fetch` rejects → assert `.sx-error` class added
|
|
(`assert-class`), then succeeds → assert cleared.
|
|
|
|
### Phase 2 — Trim Playwright to a boot smoke (small)
|
|
Keep ONLY what needs a real browser in `relate-picker.spec.js` / `spa-check.spec.js`:
|
|
- WASM kernel compiles + boots (`data-sx-ready`).
|
|
- modules load **content-addressed** (`/sx/h/` fetches, 0 path `.sxbc`).
|
|
- one boosted nav swaps `#content`.
|
|
Delete the per-behavior browser tests now covered by SX. Net: ~2 browser tests + an
|
|
SX suite.
|
|
|
|
### Phase 3 — Stretch: the engine drives a non-browser target ("something else")
|
|
The mock platform already *is* a non-browser target. This phase makes it a real
|
|
alternative renderer (e.g. server-side DOM string-builder, or a native UI shim) and runs
|
|
the same picker through it — concretely proving platform-independence. Optional / future.
|
|
|
|
## Gaps & risks to resolve during Phase 0
|
|
|
|
- **Mock-DOM completeness:** `swap-dom-nodes` uses `morph-children`, `dom-replace-child`,
|
|
`dom-insert-after/before/prepend/append`, `dom-remove-child`, `dom-parent`,
|
|
`dom-first-child`, `dom-clone`, `dom-is-fragment?`. Confirm `harness-web`'s mock DOM
|
|
implements (or can be extended for) these. `test-swap-integration.sx` already swaps, so
|
|
most exist; check `delete`/`outerHTML`/fragment paths specifically.
|
|
- **fetch callback shape:** the engine's `fetch-request` calls back
|
|
`(resp-ok status get-header text)`; the platform `:fetch` returns `{:status :body :ok}`.
|
|
Confirm/adapt the bridge (see how `test-swap-integration.sx` feeds `_mock-body` etc.).
|
|
- **trigger binding without a browser:** `simulate-click` fires bound listeners — the form
|
|
must be processed first (`process-elements` on the mock root, or bind directly).
|
|
- **component expansion:** `~relate-picker` need not be expanded for these tests — assert
|
|
on the *rendered* candidate rows / form markup directly (build the mock DOM from the
|
|
expanded HTML the server produces, which is already SX-testable server-side).
|
|
|
|
## Tracked loose ends (separate from this plan)
|
|
- **unrelate "clever" in-place delete** (just-the-row, no `#content` re-render): now that
|
|
`bind-boost-form` is fixed the remove button works via a boosted POST→swap; the
|
|
minimal-mutation version (sx-post + `sx-swap=delete` on the current-row) is a further
|
|
refinement — earlier attempt didn't fire, revisit with the binding now understood.
|
|
- **`hs-repeat-times`** bytecode test (architecture worktree): harness `host-new` stub bug
|
|
masks a pre-existing `beingTold` resume-env bug. See the diagnosis in this session.
|
|
|
|
## Done-when
|
|
- `web/tests/test-relate-picker.sx` covers populate / filter / paging / relate-delete /
|
|
error-retry in SX, green under `sx_test host="ocaml"`.
|
|
- Playwright trimmed to the boot smoke; suite still green.
|
|
- (Stretch) the picker runs through a non-browser platform.
|