fix: stop an escaped browser-test click from aborting the whole run - #1276
fix: stop an escaped browser-test click from aborting the whole run#1276vivek7405 wants to merge 4 commits into
Conversation
6e4e435 to
f540698
Compare
vivek7405
left a comment
There was a problem hiding this comment.
Read the guard against the router's own click handling and re-ran the sweep. The window-bubble phase reasoning holds up and the dedicated test for it is the right shape, but three things needed fixing.
The real bug is that the guard was narrower than the thing it backstops. It resolved the anchor with e.target.closest('a[href]'), and since the listener sits on window, a click inside an open shadow root arrives retargeted to the host, so closest() walks the light tree and never sees the link. The router deliberately uses composedPath() for exactly this case. Confirmed by reverting it: a shadow link navigates the runner page away and takes the session down, which is the failure this whole PR is about. Now walks the composed path, with a test that reds on the revert.
The sweep for suites needing the guard also missed files, because the shell glob was not recursing. Two more are genuinely exposed and now guarded. The ui-a11y suite came up as a candidate but its only anchors are pure-fragment hrefs, which never navigate the page away, so it is correctly left alone.
Last, the guard is opt-in per suite, so a comment claiming every browser suite gets it was simply false and now says a new suite has to install it. Also added the docs-site section, since the scaffold copies the repo-root skill verbatim into every generated app and that surface has its own page.
f540698 to
c28bbac
Compare
vivek7405
left a comment
There was a problem hiding this comment.
Second pass, scoped to the composed-path fix and its blast radius. Nothing left to change in the code.
The guard now matches the router's own findAnchorInPath in both traversal and the instanceof HTMLAnchorElement test, and the extra hasAttribute('href') narrowing is harmless since an hrefless anchor has no activation behaviour and the router bails on it anyway. Worth writing down that the submit half needs no path walk at all: submit is composed: false, so it never crosses a shadow boundary, and the guard is blind to a shadow-root form in exactly the same way the router is. The backstop is not narrower than the router anywhere now, which was the whole complaint.
The sweep was re-run independently across every browser file the two WTR configs actually glob, checking each unguarded file for a real activation rather than just the presence of an anchor or form. Several render links or forms and never activate them, so they need nothing. No missed suite this time. The ui-a11y exclusion holds for a stronger reason than I gave: its fragment anchors are not merely harmless, they are never clicked at all.
One stale claim in the PR body, which said the scaffold skill has no references/testing.md. It does: the generator copies the repo-root skill wholesale into every generated app, and a scaffold test reads that exact file back out. That is precisely why the wording was changed to hand the app author a snippet instead of pointing at a framework-repo module, so the body was contradicting the commit it described. Body corrected, and it now also lists the docs-site page.
66a90c2 to
e5881c2
Compare
a77418c to
77c4efa
Compare
77c4efa to
d82f0f6
Compare
…abort the run web-test-runner aborts the whole SESSION, not one file, when the page navigates, so any browser test that clicks a real anchor or submits a real form is a single point of failure for all 60+ browser test files. When the router loses the race to intercept, the run dies with 0 failed plus exit 1, which reads as an infrastructure blip rather than a test problem. The guard listens on window in the BUBBLE phase, which is the load-bearing detail. Window bubble is the last step of the propagation path, so it runs after the router's document-level listeners and preventDefault still cancels the default action. A capture-phase guard would set defaultPrevented before the router ever saw the event, and the router returns early on that flag, so every guarded router test would pass while testing nothing. Forms are blocked on submit rather than on the submit control's click, since cancelling that click would stop the form from ever submitting and the router would never see it.
…real link Nine router suites click real anchors or submit real forms, so each was a single point of failure for the whole browser run. They now install the shared guard per test. form-action-submit had hand-rolled the identical window-bubble listener with the identical reasoning, which is the duplication the shared module replaces. router-js-handled now reads its degradation events off the guard rather than registering a second listener for the same thing. The two website suites keep their capture-phase blockNav and gain a comment saying why: capture suppresses the router on purpose there, because those tests exercise a menu rather than navigation, and a live router would issue real page fetches.
…er two missed suites
Three real gaps from review.
The guard resolved the anchor with e.target.closest('a[href]'). The listener
is on window, so a click inside an open shadow root arrives retargeted to the
host and closest() walks only light-tree ancestors, never finding the link.
The guard failed open for exactly the case the router itself handles via
composedPath, so it was narrower than the thing it backstops. It now walks the
composed path the same way, with a test that navigates the runner page away
when reverted.
The first sweep for suites needing the guard missed files because the shell
glob did not recurse. view-transitions-permanent drives eight real anchor
clicks and stream-action calls requestSubmit on a real form; both are now
guarded. A pure-fragment href needs no guard, so the ui-a11y suite is
correctly left alone.
The guard is opt-in per suite, so the comment claiming every browser suite
gets it was wrong and now says a new suite has to install it.
d82f0f6 to
109a5fa
Compare
Closes #1135
Follows #1270, which merged first per the order both issues specify.
Summary
web-test-runner aborts the whole SESSION, not one file, when the page navigates. That made every browser test that clicks a real
<a href>or submits a real<form>a single point of failure for all 67 browser files: whenever the router lost the race to intercept, the browser did the real navigation and the run died reporting0 failedand then exiting 1, which reads as an infrastructure blip rather than a test problem.One shared
test/browser-nav-guard.jsnow cancels the default activation, so an interception gap fails ONE test on its own assertion.installNavGuard()returns{ fallbacks, remove }, and it is opt-in per suite, so a new suite that clicks a real link has to install it.The phase is the load-bearing detail, and it is the opposite of the obvious choice. The listener is on
windowin the BUBBLE phase, the last step of the propagation path, so it runs after the router's document-level listeners andpreventDefaultstill cancels the default action. A CAPTURE-phase guard would setdefaultPreventedbefore the router ever saw the event, and the router returns immediately on that flag, so every guarded router test would pass while testing nothing at all.nav-guard.test.jsexists to red on exactly that.The anchor is resolved from
e.composedPath(), note.target.closest('a[href]'). The listener sits onwindow, so a click inside an open shadow root arrives retargeted to the host andclosest()walks only the light tree, never finding the link. That failed open for precisely the case the router itself handles throughfindAnchorInPath, making the backstop narrower than the thing it backstops.Forms are cancelled on
submit, not on the submit control'sclick: the form's default action fires on submit, so cancelling the click would stop the form from ever submitting and the router would never see it.form-action-submit.test.jshad already hand-rolled this identical listener with identical reasoning, which is the duplication the shared module replaces.Test plan
npm run test:browsergreen, 67/67 files on Chromium, Firefox, and WebKit. Newpackages/core/test/routing/browser/nav-guard.test.js, 5/5 on all three. A 20x Firefox loop over the four touched suites had zero failures.closest()navigates the runner page away on the shadow-root test and aborts the session, which is the failure this PR exists to prevent.router-js-handled,frame-missing,frame-targeting(both suites),view-transition-head-and-suspense(both),view-transitions-permanent,stream-action(all three),fetch-revalidates,navigation-error,form-action-submit,submit-state,query-params.packages/ui/test/components/browser/ui-a11y.test.jsis deliberately NOT guarded: its only anchors are pure-fragmenthref="#..."links, which never navigate the page away.blockNavand gain a comment explaining that suppressing the router is the intent there, so a future agent does not "unify" them onto the shared helper.browser/and collects only*.test.*, so it cannot reach this diff.packages/core/srcis untouched, so no framework behaviour changed. The website was booted anyway because this PR edits one of its docs pages: 200 on/,/docs/testing,/ui, and/ui/buttonin prod mode, with no broken modulepreload hints.webjs checkreports the same 60 findings as themainbaseline..agents/skills/webjs/references/testing.md(the rule, the phase reason, the composed-path reason, and a copy-pasteable snippet, sincepackages/cli/lib/create.jscopies this skill verbatim into every scaffolded app and such an app has no shared helper),website/app/docs/testing/page.ts(same rule on the docs site),packages/core/AGENTS.md(Tests section).