Skip to content

tests: Playwright spec for tryEnterPointModeIfNeeded short-circuits (closes #192)#197

Open
rdhyee wants to merge 2 commits intoisamplesorg:mainfrom
rdhyee:fix/192-playwright-tryenterpointmode
Open

tests: Playwright spec for tryEnterPointModeIfNeeded short-circuits (closes #192)#197
rdhyee wants to merge 2 commits intoisamplesorg:mainfrom
rdhyee:fix/192-playwright-tryenterpointmode

Conversation

@rdhyee
Copy link
Copy Markdown
Contributor

@rdhyee rdhyee commented May 10, 2026

Summary

Closes #192. Adds tests/playwright/explorer-helper.spec.js covering the deterministically observable short-circuit invariants of tryEnterPointModeIfNeeded() (introduced in #191).

Coverage

Test Status What it asserts
boot to cluster mode reaches a done message ✓ passing Phase-1 cluster load reaches "${n} clusters, ${m} samples." done state.
source-filter toggle at world altitude does NOT trigger Fetching sample index ✓ passing Invariant (b) altitude short-circuit. The source-filter handler chases the helper after its own loadRes settles, and at world altitude the chase MUST bail at the altitude check without painting "Fetching sample index…".
deep-link to point altitude reaches a sample-points done message ⊘ skipped Documented future work — see below.
source-filter toggle in point mode does NOT trigger Fetching sample index ⊘ skipped Documented future work — see below.

Why two tests are skipped

Both require the Cesium camera at point altitude (alt < 120 km). In headless Chromium the scene.postRender event that applies the deep-link camera position doesn't drive the camera-changed handler the same way as a real WebGL display: the camera stays at world altitude and the point-mode logic never fires. (Confirmed during development — visited the alt=62054 deep-link in a headless browser, observed phase-message progression stopped at the cluster-mode done state and never advanced.)

The tests are kept (with detailed .skip() reasons inline) as living documentation. Two paths forward for a follow-up:

  1. Run them in headed mode (npm run test:headed against the same preview server). Slower, requires display in CI, but the camera flies correctly.
  2. Add a small test-only hook that exposes viewer on window (closure-private currently), so the spec can drive viewer.camera.flyTo() directly and the camera handler fires deterministically. This is a one-line change to explorer.qmd.

Either is out of scope for this PR — the issue's acceptance asks for "at least one Playwright spec covering items 1, 3, and 4," and the altitude-short-circuit test alone already covers an item-1 short-circuit invariant deterministically.

Approach

Cell-internal state (mode, currentRes, loading, viewer) is closure-private inside an OJS cell and is NOT exposed on window. The deterministically observable signal from outside the closure is #phaseMsg's textContent, which updatePhaseMsg(...) rewrites at every state transition. Tests install a MutationObserver on #phaseMsg, capture history during the action, then assert presence/absence of expected loading-message phrases.

Test plan

# Terminal 1: preview server
quarto preview explorer.qmd --no-browser --port 5860

# Terminal 2: tests
npm run test -- tests/playwright/explorer-helper.spec.js

Expected output:

✓ boot to cluster mode reaches a done message with cluster counts (2.1s)
✓ source-filter toggle at world altitude does NOT trigger Fetching sample index (4.0s)
- deep-link to point altitude reaches a sample-points done message
- source-filter toggle while in point mode does NOT trigger Fetching sample index
2 skipped, 2 passed (~7s)
  • Both passing tests run in <10s combined locally
  • Skipped tests have inline .skip() reason explaining why and how to enable
  • Spec is purely additive — no production-code changes

Refs

🤖 Generated with Claude Code

rdhyee and others added 2 commits May 10, 2026 08:31
…loses isamplesorg#192)

Adds tests/playwright/explorer-helper.spec.js with two passing tests
that exercise the helper's deterministically observable invariants
plus two `.skip()`d tests documenting paths that need a hookable
refactor or headed-mode execution.

Passing tests:
1. **boot to cluster mode** — explorer at world altitude reaches the
   "${n} clusters, ${m} samples." done state. Smoke test for the
   normal Phase-1 boot path.
2. **source-filter toggle at world altitude does NOT trigger
   "Fetching sample index…"** — verifies the helper's altitude
   short-circuit (invariant b). The source-filter handler chases the
   helper after its own loadRes settles, and at world altitude the
   chase MUST bail at the altitude check without painting the
   helper's own loadingMsg.

Skipped tests (documented inline why):
3. **deep-link to point altitude reaches a sample-points done message**
4. **source-filter toggle in point mode does NOT trigger "Fetching
   sample index…"**

Both require the Cesium camera at point altitude, which doesn't
reproduce reliably in headless chromium — the postRender event that
applies the deep-link camera position doesn't drive the camera-
changed handler the same way as a real WebGL display, so the camera
stays at world altitude and the point-mode logic never fires. The
tests are kept (skipped) as living documentation; a future PR can
either run them in headed mode (`npm run test:headed`) or expose a
small test-only hook on `window` that lets the spec drive
`viewer.camera.flyTo()` or set `_globeState.mode` directly.

Both passing tests run in <10s combined against `quarto preview
explorer.qmd --port 5860`. Local run:

  ✓ boot to cluster mode reaches a done message with cluster counts (2.1s)
  ✓ source-filter toggle at world altitude does NOT trigger Fetching sample index (4.0s)
  - deep-link to point altitude reaches a sample-points done message
  - source-filter toggle while in point mode does NOT trigger Fetching sample index
  2 skipped, 2 passed (6.5s)

Closes isamplesorg#192.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… tests (review round 2)

Address Codex review of isamplesorg#197:

1. The two `.skip()`d point-altitude tests are now executable. Codex
   pointed out that Quarto's OJS runtime exposes evaluated cells via
   `window._ojs.ojsConnector.mainModule.value('viewer')`, which lets
   tests reach the closure-private `viewer` Cesium widget without
   adding a production hook. From there, `viewer.camera.flyTo(...)`
   drives the cluster→point-mode transition deterministically.

2. Fix the headless render-loop suspension that was the *real* reason
   the deep-link approach didn't work: Cesium's default
   `requestRenderMode = true` only renders on demand, and a single
   `setView` + `requestRender` doesn't reliably trigger
   `camera.changed` in headless chromium. Setting
   `scene.requestRenderMode = false` keeps the loop active, and an
   animated `flyTo` produces a stream of `postRender` events that
   exceed the percentageChanged threshold and fire the
   camera-changed handler.

3. Tighten the point-mode done-message assertion. The earlier
   substring match on `"individual samples"` was satisfied by the
   intermediate `"Loading individual samples..."` loading state.
   Wait for the count-prefixed terminal form via a regex instead.

Coverage matrix is now:

  ✓ boot to cluster mode reaches a done message with cluster counts
  ✓ source-filter toggle at world altitude does NOT trigger Fetching sample index   (invariant b)
  ✓ flying camera into point altitude triggers helper and enters point mode         (camera-handler call site, end-to-end)
  ✓ source-filter toggle while in point mode does NOT trigger Fetching sample index (invariant a)

  4 passed (30.4s)

Closes isamplesorg#192.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@rdhyee
Copy link
Copy Markdown
Contributor Author

rdhyee commented May 10, 2026

Codex review round 1 — addressed in 11a06cd

Both blocking findings addressed.

Codex's pointer to the OJS runtime works

"A better available hook may be Quarto's OJS runtime: window._ojs.ojsConnector.mainModule.value('viewer') can likely retrieve viewer without production changes…"

Confirmed locally — the runtime does expose the viewer cell value. From there, viewer.camera.flyTo(...) drives Cesium directly. This eliminates the need for a production-side test hook.

Why the previous attempt didn't work (the real root cause)

Cesium's default requestRenderMode = true renders on demand only. A single setView + requestRender doesn't reliably fire camera.changed in headless chromium because the render loop suspends after idle. Setting scene.requestRenderMode = false keeps the loop running, and an animated flyTo produces a stream of postRender events that exceed percentageChanged and trigger the camera-changed handler.

(My earlier debug script "worked" because it ran during the still-warming-up render window. By the time the spec waited for cluster boot, the render loop had suspended.)

Result

All four tests pass:

✓ boot to cluster mode reaches a done message with cluster counts (2.0s)
✓ source-filter toggle at world altitude does NOT trigger Fetching sample index (5.6s)    [invariant b]
✓ flying camera into point altitude triggers helper and enters point mode (12.1s)         [camera-handler call site]
✓ source-filter toggle while in point mode does NOT trigger Fetching sample index (10.3s) [invariant a]

4 passed (30.4s)

Coverage now meets #192's acceptance: invariants (a) and (b) are deterministically exercised, plus an end-to-end smoke test of the camera-handler call site that drives the helper into a real point-mode entry. Invariant (c) (!loading short-circuit) still requires timing injection and is left for a future PR.

Also fixed

  • Tightened the point-mode done-message assertion. The earlier substring match on "individual samples" was satisfied by the intermediate "Loading individual samples..." loading state. Now waits for the count-prefixed terminal form via regex.

Re-requesting review.

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.

explorer: Playwright coverage for tryEnterPointModeIfNeeded() invariants

1 participant