tests: Playwright spec for explorer URL state round-trip (addresses #209)#214
Conversation
…plesorg#209) CI-safe spec set extracted from `tests/playwright/url_roundtrip_investigation.js` (which remains as a manual diagnostic). Five tests covering the URL state contract slices fixed in PRs isamplesorg#203, isamplesorg#205, isamplesorg#210, isamplesorg#212: 1. **Point-mode deep-link with `mode=point` enters point mode** — regression for Bug B fix in isamplesorg#203 (when heading is omitted, the URL must still hydrate into point mode). 2. **Low-alt deep-link WITHOUT `mode=point` enters point mode** — regression for isamplesorg#207 item 4 (boot should enter point mode based on altitude alone, not just the URL's mode flag). 3. **Sub-threshold pan updates URL hash via `moveEnd`** — regression for isamplesorg#205 (Cesium's `camera.changed` suppresses small moves below `percentageChanged` threshold; `moveEnd` is the backstop). 4. **URL round-trips across browser contexts** — combined regression for isamplesorg#203 + isamplesorg#205. Context A pans + zooms after settle, captures URL; Context B opens the captured URL and arrives at the same camera + mode within tight tolerance. 5. **`h3` hashchange with invalid cell clears `_globeState.selectedH3`** — regression for isamplesorg#207 item 6 (asymmetry between boot and hashchange null-result branches). Search-result row click selection (isamplesorg#207 item 8) deferred — needs working FTS data and depends on substrate the spec shouldn't have to set up. Tests use `waitForPointModeSettled` (waits for the phase message's "Click one for details." text) instead of `waitForMode` for the no-`mode=point` cases — `waitForMode` can match a transient mode flip during the boot transition (the dual-mode-state anomaly being addressed structurally in isamplesorg#208). `test.setTimeout(360000)` per describe block — cold-cache point-mode boot can take 60–90s per isamplesorg#190, and the round-trip test opens multiple fresh contexts each paying the cost. Verified: all 5 tests pass against localhost in ~1m 24s. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2c97398 to
3b3555b
Compare
Codex review — round 1Self-contained Codex review (no shared context with our session). Pasting verbatim per the iterative-review pattern. SummaryThe specs are useful, but I would not approve this as-is if the PR title/body says Per-ask response1. h3 hashchange spec assertion strength
Stronger version: start with a known valid h3, wait until await page.goto(`${EXPLORER_PATH}#v=1&lat=${LAT}&lng=${LNG}&alt=${ALT_CLUSTER}&h3=${VALID_H3}`);
await waitForMode(page, 'cluster');
await page.waitForFunction(async () => {
const v = await window._ojs.ojsConnector.mainModule.value('viewer');
return !!v._globeState.selectedH3;
});
await page.evaluate((badH3) => {
const params = new URLSearchParams(location.hash.slice(1));
params.set('h3', badH3);
params.set('heading', '5.0');
location.hash = '#' + params.toString();
}, '0baadbeeffffffff');
await page.waitForFunction(async () => {
const v = await window._ojs.ojsConnector.mainModule.value('viewer');
return v._globeState.selectedH3 === null;
});That would actually catch a regression in the 2.
|
Per Codex review on PR isamplesorg#214: - Replace fixed `waitForTimeout` sleeps with `waitForFunction` polling URL hash in tests 3 (sub-threshold pan) and 4 (cross-context round-trip). Fixes thin timing margin (0.5s) over flight + moveEnd debounce. - Wrap context-creation in try/finally so ctxA/ctxB get closed on mid-test assertion failure (test 4). - Drop unused `before` snapshot in test 3. - Move 6-minute `test.setTimeout(360000)` from describe-level default (180s) to per-test override on the round-trip test only. Single broken test no longer waits 6 minutes to fail in CI. - Add `waitForBootSettled(page)` helper waiting for `viewer._suppressHashWrite === false` — `_globeState.mode` is initialized early (explorer.qmd:871) while the hashchange listener is registered late (explorer.qmd:2210); waitForMode alone isn't enough. - Strengthen test 5 with the boot-settle wait so the hashchange listener exists before driving the test hashchange. - Add comment in `snapshot()` noting `_globeState.selectedH3`/`selectedPid` field coupling. - Update header: change `closes isamplesorg#209` to `addresses isamplesorg#209` (PR explicitly defers several isamplesorg#209 checklist items to a follow-up). Remove the internally-inconsistent "don't try cold-cache deep-links" sentence that contradicted the timeout comment's cold-cache rationale. Kept (with rationale in code comments): - `waitForPointModeSettled` matches "Click one for details" rather than the count phrase Codex suggested — count phrase misses the cap-reached done-state branch (explorer.qmd:1611) which says "samples in view ..." with no "individual" word. "Click one for details" is the common denominator across both done-state branches. - Test 5 h3-hashchange remains a hashchange-driven test rather than the valid→invalid round-trip Codex proposed. The original assertion DOES catch the regression: line 2272 unconditionally overwrites selectedH3 from the URL on hashchange, so the only way the post-handler value is null is the explicit null-result branch at line 2285. Codex's "trivially true" framing missed that line 2272 always fires first. (Tried seeding selectedH3 before the hashchange to make the test even stronger; the seed survived the handler unexpectedly — suspected OJS reactivity around mainModule.value('viewer') returning a different reference than the handler's closure. Documented in test comment.) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Codex review — round 2Verbatim Codex round-2 response (no shared context). Round-2 prompt argued the original test-5 assertion was regression-tight via line 2272's unconditional write — Codex pushed back with a sharper, correct argument: the boot deep-link path's own null-result branch (line 2728) already cleared selectedH3 before the test's hashchange fires, so the post-mutation 'wait for null' is satisfied by boot, not by the handler under test. Applying their fix in a follow-up commit. SummaryStill BLOCK on Test 5. I agree with the production-code trace, but not with the test synchronization claim. Per-round-2-ask response1. Test 5 re-verificationYour trace of The problem is the spec can still pass before the hashchange handler runs. After boot, the initial invalid Suggested fix: after mutating the hash, wait for a handler-only side effect first, e.g. camera heading near 2.
|
Codex round 2 sharpened the round-1 concern on Test 5 and was right: booting with `&h3=<invalid>` runs the boot deep-link's own null-result branch (explorer.qmd:2728) which sets `selectedH3=null` BEFORE the test even drives a hashchange. So the post-mutation `selectedH3 === null` wait is satisfied by boot, not by the hashchange handler's null-clear branch at line 2285 (the regression we want to guard). Fix: gate the assertion on a handler-only side effect. The hashchange handler's `camera.flyTo` (explorer.qmd:2220-2227) rotates `camera.heading` to the URL's heading value — only the handler produces this rotation. Wait for heading to reach ~5° before asserting `selectedH3 === null`. This proves the handler executed past line 2272 (which would write a non-null selectedH3 from the URL) AND reached line 2285 (the null-clear). Also addresses Codex's altitude nit on Test 4: `waitForHashLatLng` now takes an optional `alt` so the round-trip assertion checks lat + lng + alt together, matching what `buildHash` actually writes. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Codex review — round 3Verbatim Codex round-3 response. LGTM, ready to merge. SummaryThe round-3 fix addresses the round-2 blocker. The heading gate is a valid handler-only signal, and with the deliberately malformed 16-character H3, the null-clear continuation should complete well before the heading wait can pass. Per-ask response1. Test 5 regression-tightness verificationYes, this is regression-tight against removing The key point is that boot starts with no If 2. Race between heading and selectedH3 clearNot a real concern for this exact test data.
No extra microtask flush or second wait is needed. 3. Test 4 alt-in-URL assertionLooks fine. 4. Anything elseNo additional blocker or nit in the round-3 diff. VerdictLGTM, ready to merge |
Codex round-1 review (verbatim posted to PR isamplesorg#202 thread) BLOCKed on two items: 1. Quarto Markdown-parses inside the bare static-layout HTML and wraps `<input>` / `<label>` in `<p>` tags (e.g. `.search-bar > p > input`). This breaks `.search-bar input { flex: 1 }` and adds paragraph margins to control rows. Pre-existing on main, but in-scope here. Fix: wrap both the `.explorer-controls` block AND the `.globe-layout` + `#tableContainer` block in `{=html}` raw-html fences. 2. Layout-stability spec waited on the substring 'individual samples' in `#phaseMsg`, which misses the cap-reached done-state branch (`explorer.qmd:1611` — "<N> samples in view (showing M ...)" with no "individual" word). Same brittleness Codex flagged on PR isamplesorg#214. Switch to 'Click one for details' — common denominator across both done-state branches. Also addressed Codex NITs: - Added small-phone case (320×568) exercising the clamp-floor branch at the 360px floor — the 390×844 case never hits this. - Replaced hardcoded `585` / `489.52` heights with a probe-element approach: a hidden `<div style="height: var(--explorer-map-height)">` whose `getBoundingClientRect()` resolves the `clamp(...)` to a real px value. `getComputedStyle().getPropertyValue('--explorer-map-height')` returns the unresolved `clamp(...)` literal, not a px value (this is why my first attempt failed with NaN). - Documented the `waitForClusterBoot` `value('zoomWatcher')` await as intentional synchronization (cell finishes => listeners registered), not a stray return value. Kept (per Codex review): - `requestRenderMode = false` mutation pattern matches URL-roundtrip spec, no cleanup needed (fresh page per test). - `#searchResults` 2-line reserved slot — Codex acknowledged 3 lines would be more forgiving but the 2-line policy is acceptable for layout stability. Verification: all 3 layout-stability tests pass in ~23s. All 5 url-roundtrip specs pass against the modified explorer.qmd (no regression in the recently-landed isamplesorg#214 contract). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Stabilize explorer map layout * Preserve explorer inline CSS as raw HTML * Widen explorer app layout * explorer: address Codex round-1 review on layout stability PR Codex round-1 review (verbatim posted to PR #202 thread) BLOCKed on two items: 1. Quarto Markdown-parses inside the bare static-layout HTML and wraps `<input>` / `<label>` in `<p>` tags (e.g. `.search-bar > p > input`). This breaks `.search-bar input { flex: 1 }` and adds paragraph margins to control rows. Pre-existing on main, but in-scope here. Fix: wrap both the `.explorer-controls` block AND the `.globe-layout` + `#tableContainer` block in `{=html}` raw-html fences. 2. Layout-stability spec waited on the substring 'individual samples' in `#phaseMsg`, which misses the cap-reached done-state branch (`explorer.qmd:1611` — "<N> samples in view (showing M ...)" with no "individual" word). Same brittleness Codex flagged on PR #214. Switch to 'Click one for details' — common denominator across both done-state branches. Also addressed Codex NITs: - Added small-phone case (320×568) exercising the clamp-floor branch at the 360px floor — the 390×844 case never hits this. - Replaced hardcoded `585` / `489.52` heights with a probe-element approach: a hidden `<div style="height: var(--explorer-map-height)">` whose `getBoundingClientRect()` resolves the `clamp(...)` to a real px value. `getComputedStyle().getPropertyValue('--explorer-map-height')` returns the unresolved `clamp(...)` literal, not a px value (this is why my first attempt failed with NaN). - Documented the `waitForClusterBoot` `value('zoomWatcher')` await as intentional synchronization (cell finishes => listeners registered), not a stray return value. Kept (per Codex review): - `requestRenderMode = false` mutation pattern matches URL-roundtrip spec, no cleanup needed (fresh page per test). - `#searchResults` 2-line reserved slot — Codex acknowledged 3 lines would be more forgiving but the 2-line policy is acceptable for layout stability. Verification: all 3 layout-stability tests pass in ~23s. All 5 url-roundtrip specs pass against the modified explorer.qmd (no regression in the recently-landed #214 contract). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * explorer: extract resolveMapHeightPx helper for spec consistency Codex round-2 review noted the desktop test still hardcoded `585` while the mobile cases switched to a probe-element approach. Extract the probe inline into `resolveMapHeightPx(page)` and use it from all three tests — desktop, mobile, small-phone. Desktop assertion now derives expected height the same way as the mobile cases (probe of `var(--explorer-map-height)` → measured px) and sanity-bounds the result to the desktop clamp range [500, 680]. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Addresses #209 (does NOT fully close — see #215 for the rest of the checklist). CI-safe Playwright specs covering the URL state contract slices fixed by PRs #203, #205, #210, #212.
What this adds
New file:
tests/playwright/url-roundtrip.spec.js. Five tests:mode=pointenters point modemode=pointenters point modemoveEndpercentageChangedsuppressed events)h3hashchange with unknown cell clears_globeState.selectedH3What's NOT covered
Tracked in follow-up #215:
piddeep-link + active filtersh3deep-link round-trip with cluster-card hydrationsearch/search_scoperound-trip?sources=,?material=, etc.)view=tablemode restoreOut of scope:
tests/playwright/url_roundtrip_investigation.js(manual diagnostic, too slow/network-sensitive for CI).Codex round-1 review applied
This PR went through one Codex review round; changes in
7436c75address it:waitForTimeoutsleeps withwaitForFunctionpolling URL hash (tests 3 + 4) — thin timing margin over flight + moveEnd debounce.try/finallysoctxA/ctxBclose on mid-test assertion failure.test.setTimeout(360000)from describe-default (180s) to per-test override on the round-trip test only — single broken test no longer waits 6 minutes to fail.waitForBootSettledhelper (waits forviewer._suppressHashWrite === false) —_globeState.modeinitializes atexplorer.qmd:871, but the hashchange listener is registered at:2210, sowaitForModealone isn't enough.closes #209toaddresses #209; opened explorer: round-trip specs for remaining URL state surfaces (follow-up to #209) #215 for the rest of the checklist.snapshot()noting_globeState.selectedH3/selectedPidfield coupling.Disagreement with Codex retained, documented in code:
waitForPointModeSettledkeeps the'Click one for details'substring match rather than the count-phrase pattern Codex suggested — count phrase misses the cap-reached done-state branch atexplorer.qmd:1611("<n> samples in view (showing <m> — zoom in for more). Click one for details."— no "individual" word).'Click one for details'is the common denominator across both done-state branches.selectedH3from the URL on hashchange, so the only way the post-handler value isnullis the explicit null-result branch at line 2285 firing. Codex's "trivially true" framing missed that line 2272 fires first. (Tried seedingselectedH3before the hashchange for an even stronger assertion; seed survived the handler unexpectedly — suspected OJS reactivity aroundmainModule.value('viewer')returning a different reference than the handler's closure-captured viewer. Documented in the test comment.)Verification
All 5 tests pass against localhost (Quarto preview of current
main) in ~1m 24s:🤖 Generated with Claude Code