Add synthetic focus/type/click: real DOM events, not attribute pokes - #91
Merged
Merged
Conversation
The actual "drive a login form" API on top of the LiveDocument foundation (#89) and hit-testing (#90). js/events.go: dispatch now bubbles — fires target-phase listeners on the node, then (when event.bubbles is true and nothing called stopPropagation) walks up the ancestor chain doing the same, updating target/currentTarget as it goes. Distinguishes stopPropagation (later listeners on the SAME node still run) from stopImmediatePropagation (they don't), which a single flag would have blurred. This is a behavior change to a function every existing page script's click()/ dispatchEvent() call already goes through — verified against the full existing test suite (unaffected: real content bubbles=false by default, matching spec) before adding anything on top. js/session.go: Session.Dispatch(n, typ, EventInit) is the new Go-facing seam — fires a synthetic event exactly as if the page's own script had called element.dispatchEvent(), and reports defaultPrevented so a caller (native form submission, a later phase) can honor it. synthetic.go (engine package): LiveDocument.Focus/Blur/Type/Click compose real event sequences through that seam — Type fires keydown->input->keyup PER CHARACTER (not one bulk value assignment), which is what lets a controlled React/Vue onChange see every keystroke; Focus/Blur track which control is focused and fire change only when its value actually differs at blur, matching real browser semantics; Click does mousedown->mouseup->click and reports defaultPrevented. Each interaction resettles (F0) so the live session keeps running, not restarting. TestSyntheticLoginFlow is the decisive end-to-end proof: a login form whose JS accumulates typed text in its OWN closure variables (not just DOM attributes) across many separate Focus/Type/Click calls, only succeeding once both fields match — exactly the shape a real login page's JS takes, and exactly what a naive re-render-per-keystroke would have silently corrupted. 100% coverage on every new/changed function, full existing suite + -race unaffected. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
5 tasks
tannevaled
added a commit
that referenced
this pull request
Sep 2, 2026
F4 of the interactive-form-input plan (#89-#91): the path for a plain server-rendered login form with no JS submit handler at all. LiveDocument.Submit(ctx, form) dispatches "submit" through the live session first, exactly like a real browser. If a listener calls preventDefault() (the common SPA case, already reachable via Focus/Type/Click plus the engine's existing fetch()/XHR support), nothing further happens and the current page stays open. Otherwise it gathers named form-control values (input/textarea/select, skipping disabled controls and unchecked checkboxes/radios, matching what a real browser submits) and performs the actual GET-with-querystring or POST-urlencoded request per the form's action/method, then opens the response as a new LiveDocument and closes the old one — a real navigation ending the previous page's JS, same as a browser. Caught two real gaps while writing this against the actual DOM model rather than assuming: <select>'s submitted value needs its own selected-option walk (no shared helper existed at the engine-package level); <textarea> has no dedicated JS value accessor (js/dom.go's generic "value" property just reads the "value" attribute, which a textarea doesn't carry until something explicitly sets it) — Submit's own value-gathering falls back to the element's text content for an untouched textarea, matching what a real browser's textarea.value defaults to, while documenting the JS-binding gap for anyone who hits it from script instead. 100% coverage on every function except postForm (83.3% — the remaining gaps are low-level I/O failure branches http.NewRequest/ ReadAll/decodeCharset/dom.Parse can each return, consistent with this codebase's own existing tolerance for that class of near-unreachable path: Fetch and decodeCharset sit at 82.4%/75% on main today for the same reason). Full existing suite + -race unaffected. Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
The actual "drive a login form" API, on top of the
LiveDocumentfoundation (#89) and hit-testing (#90).js/events.go:dispatchnow bubbles — target-phase listeners on the node, then (whenevent.bubblesand nothing calledstopPropagation) up the ancestor chain, updatingtarget/currentTargetas it goes. DistinguishesstopPropagation(later listeners on the SAME node still run) fromstopImmediatePropagation(they don't). This is a behavior change to a function every existing page script's.click()/dispatchEvent()already goes through — verified against the full existing suite before building anything on top (unaffected: real content defaultsbubbles=false, matching spec).js/session.go:Session.Dispatch(n, typ, EventInit)— the Go-facing seam, firing a synthetic event exactly as if the page's own script had calledelement.dispatchEvent(), reportingdefaultPrevented.synthetic.go:LiveDocument.Focus/Blur/Type/Click.Typefireskeydown→input→keyupper character (not one bulk value assignment) — what lets a controlled React/VueonChangesee every keystroke.Focus/Blurtrack the focused control and firechangeonly when its value actually differs at blur.Clickdoesmousedown→mouseup→clickand reportsdefaultPrevented.Test plan
-racepass unmodified,go vet/gofmtclean, 100% coverage on every new/changed functionTestSyntheticLoginFlow— the decisive proof: a login form whose JS accumulates typed text in its own closure variables (not just DOM attributes) across many separateFocus/Type/Clickcalls, only succeeding once both fields match. This is exactly what a naive re-render-per-keystroke would have silently corrupted, and exactly the shape a real login page's JS takes.TestEventBubbling/TestStopPropagationVsStopImmediate/TestEventCurrentTargetTracksBubblePhase— the bubbling mechanics in isolationTestFocusFiresBlurAndChangeOnThePreviousField— the blur/change fallout when focus moves🤖 Generated with Claude Code