test(e2e): pin explicit-submitter round-trip via chromedp#321
Conversation
Adds TestExplicitSubmitter_E2E covering Verification item 3 of docs/proposals/explicit-submitter.md: real-browser round-trip of the explicit "submitter" WS field that @livetemplate/client v0.9.0 emits. A form with two named submit buttons (save, delete) auto-intercepts onto the WebSocket; the test asserts the WS sent frame contains "submitter":"save" then "submitter":"delete", and that the controller dispatches to the matching method (counter increments on save, resets on delete). Two opposite-direction mutations rule out a stuck dispatch. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Code Review —
|
- Clear WS frame log between save and delete phases so the assertion only matches frames produced by the click under test. Removes the implicit dependency on "save" and "delete" being disjoint substrings (a future third button or repeated submit would have silently passed). - Trim textContent in WaitFor conditions so rendering or browser whitespace normalization cannot break the comparison. - Add symmetric action-field check after the delete click for parity with the save phase. - Drop redundant Counter: 0 zero-value field in the initial state. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Addressed items 1, 2, 3, 5 in f174af5 (frame-log clear between phases, .trim() on textContent, symmetric action assertion, dropped zero-value Counter: 0). Declining item 4 (singleton comment). In LiveTemplate's controller pattern, singleton is the framework term for "shared instance across sessions, never cloned" (see CLAUDE.md: Controllers hold dependencies (singleton, never cloned). State holds pure data (cloned per session)). The comment isn't claiming runtime enforcement — it's declaring the lifecycle contract, which is the same convention used by FocusTestController in the same file at line 1372 ("FocusTestController is a singleton that holds dependencies"). Changing it here would diverge from the established pattern. |
Code Review:
|
… assertions RecordWSFrames captures both browser→server and server→browser frames. WaitForMessage is direction-blind, which lets a substring check pass against a server frame that happens to echo the same content — a false positive that masks a missing client emit. Adds WaitForSentMessage (identical timeout loop, restricted to GetSent()) to testing/websocket.go and routes the two assertions in TestExplicitSubmitter_E2E through it. The new helper is generic and applies to any future WS round-trip test that needs to pin a client-side wire emission. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Round-2 fixes pushed in f27f824: Item 1 (direction filtering) — fixed. Added Item 2 ( Item 3 ("singleton" comment). Re-declined; this is the same suggestion as round-1 item 4, which I addressed in a prior reply. In LiveTemplate's controller pattern, singleton is the documented framework term for "shared instance across sessions, never cloned" (CLAUDE.md: Controllers hold dependencies (singleton, never cloned). State holds pure data (cloned per session)). |
Code Review —
|
| Location | Existing style | New code |
|---|---|---|
log.Printf inside goroutine |
"Server error: %v" |
"server error: %v" |
t.Fatalf for free port |
"Failed to get free port: %v" |
"get free port: %v" |
t.Logf for shutdown |
"Server shutdown warning: %v" |
consistent ✓ |
Potential JSON fragility
wsLog.WaitForSentMessage(`"submitter":"save"`, 3*time.Second)This substring match assumes the client serializes JSON with no space after the colon ("submitter":"save", not "submitter": "save"). If the JS runtime ever emits canonically-spaced JSON this silently breaks. Consider asserting only on the parsed field that is already checked two lines later:
// Wait for any sent frame, then assert on Parsed — avoids serialization-format coupling
msg, err := wsLog.WaitForSentMessage(`"submitter"`, 3*time.Second)
// existing: if got, _ := msg.Parsed["submitter"].(string); got != "save" { ... }Or alternatively encode the exact byte sequence expected as a constant comment so future maintainers know it's load-bearing.
SubmitterTestController package-level declaration
The comment says "singleton" but Go has no enforcement of that — it just means stateless. The word is slightly misleading; "stateless controller" would be more accurate. Not a functional issue.
state pointer vs. framework ownership
state := &SubmitterTestState{Status: "idle"}
mux.Handle("/", tmpl.Handle(controller, livetemplate.AsState(state)))Save and Delete operate on a by-value copy and return the new state, which is the correct pattern. This matches how other tests in this file use AsState. No issue, just flagging it was checked.
Missing assertion: server→browser update content
The test confirms the client sent the right frame and the controller ran the right method (via DOM text). It does not assert on the received (server→browser) patch frame. That's acceptable — the DOM check is an end-to-end proxy — but if the goal is pinning the wire contract in both directions, a wsLog.WaitForReceivedMessage assertion on the state diff would round it out.
wsLog.Clear() placement — correct ✓
Clear() is called before chromedp.Click, so frames from the click are guaranteed to land after the clear. Good.
Two-click ordering — correct ✓
The WaitFor DOM check after each click ensures the server has already processed the frame before WaitForSentMessage is called, so the 3-second wait should always be a near-instant hit. This is the right ordering.
Summary
| Area | Verdict |
|---|---|
| Correctness | ✅ |
| Race safety | ✅ |
| Style alignment | |
| JSON pattern matching | |
| Test coverage | ✅ Core contract is covered |
WaitForSentMessage API |
✅ Useful addition; minor consistency note |
Overall this is a well-reasoned test. The two issues worth addressing before merge are the JSON substring fragility and the capitalization inconsistencies. Everything else is low-priority cleanup.
- WaitForSentMessage now matches on the `"submitter"` field key only; the Parsed map carries the value-equality check. Decouples the assertion from JSON serialization spacing (`"submitter":"x"` vs `"submitter": "x"`). - Capitalize t.Fatalf / log.Printf strings to match the casing used by neighboring tests in livetemplate_core_test.go. - Trim WaitForSentMessage doc comment to one line. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Round-3 changes in d34354e: Substring fragility (✓ fixed). Capitalization (✓ partial). Aligned Doc-comment trim (✓ done). Declines:
|
Code Review —
|
The prior boot wait (`typeof window.liveTemplateClient !== 'undefined'`) fires once the client script evaluates, which can happen before the WS handshake completes. A click that races ahead of the open socket is swallowed and the subsequent `WaitFor(status === 'saved')` times out with no clear cause. Switch to the canonical framework signal: the [data-lvt-id] wrapper loses its data-lvt-loading attribute once the JS client confirms the socket is connected. Mirrors TestLoadingIndicator at livetemplate_core_test.go:1238. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Round-4 fixes in ef7b52d: Item 1 (WS readiness race) — ✓ fixed. Switched the boot wait from Items 2 & 4 — already addressed in earlier rounds. The review appears to be looking at a stale diff:
Item 3 (singleton comment) — 4th raise, declining. Per CLAUDE.md AI Code Review Workflow: Recognize the pattern and stop pushing; address remaining cosmetic nits by reply rather than by push. The term singleton is the project's documented framework terminology (controllers shared across sessions, never cloned — see CLAUDE.md Controller+State Pattern). |
Code ReviewOverviewThis PR adds IssuesMedium:
|
Restructures WaitForSentMessage to call a new FindSentMessage helper, in parallel with the existing FindMessage / WaitForMessage pair. Each poll tick now does one lock acquisition and one pass over wl.messages with early return, instead of GetSent's lock+copy+iterate. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Round-5 changes in 8d42d42: FindSentMessage refactor (✓ applied). Declines:
|
Code Review:
|
| Severity | Finding |
|---|---|
| Minor | WaitForSentMessage duplicates polling loop from WaitForMessage — risk of silent divergence |
| Minor | No Parsed == nil guard before field assertions — failure message is fine but root cause less obvious |
| Nit | SubmitterTestController "singleton" comment slightly misleading for a zero-field struct |
| Suggestion | Consider a follow-up issue for websocket_test.go unit tests covering direction filtering |
Approachable changes overall — the test design is sound and the implementation follows project conventions closely.
|
Round-6 reply (no push): Round 6 contains zero new functional issues. The three items are stylistic preferences, an explicit follow-up suggestion, and a sixth re-raise of the singleton comment. Per CLAUDE.md AI Code Review Workflow: The convergence signal is 'successive rounds aren't identifying any new functional issue' — only style, phrasing, or wording alternatives on content that was already accepted. We've crossed that line. 1. Polling-loop duplication between WaitForMessage and WaitForSentMessage — declining. The duplicated portion is six lines; CLAUDE.md's anti-DRY guidance applies: Three similar lines is better than a premature abstraction. A shared internal helper for two callers with identical bodies adds an indirection layer without removing meaningful duplication. 2. 3. "Singleton" comment (6th raise) — final decline. The bot's own round-6 framing acknowledges "the comment is consistent with the convention" while still suggesting a change. Reworking it here without also changing 4. PR is ready to merge once the Test workflow on 8d42d42 completes (currently ~6min in; Test runs typically take 10-11 min on this branch). |
Summary
Adds
TestExplicitSubmitter_E2Eine2e/livetemplate_core_test.go, covering Verification item 3 of the explicit-submitter proposal: a real-browser round-trip of the explicitsubmitterWS field that@livetemplate/clientv0.9.0 emits.A form with two named submit buttons (
save,delete) auto-intercepts onto the WebSocket; the test asserts the WS sent frame contains"submitter":"save"then"submitter":"delete", and that the controller dispatches to the matching method (counter increments on save, resets on delete). The two opposite-direction mutations rule out a stuck dispatch (always-"save" or always-"delete" would fail).This closes the cross-repo verification gap from livetemplate#237 Phase 2 (PR #396 server-side, client#120 client-side, released as
@livetemplate/client@0.9.0).Test plan
GOWORK=off go test -tags browser -run '^TestExplicitSubmitter_E2E$' -v ./e2e/passes locally (CDN-resolved@livetemplate/client@0.9.0viaunpkg/@latest)RecordWSFramesshow the explicit"submitter"JSON key on both clicksSave()thenDelete()(counter flips 0→1→0; status flips idle→saved→deleted)go vet -tags browser ./e2e/clean