fix(e2e): sweep blocking page.Element()-in-Eventually to non-blocking page.Elements() (systemic go-rod de-flake) (bookshelf-eu18m) #1331
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "bd-bookshelf-eu18m"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
page.Element(sel)/MustElement(sel)blocks internally up to the page timeout (e.g. 60s) when the element is absent, so using it insideEventually(budget, interval)is broken: the inner call blocks past the Eventually budget and the retry loop never fires. This is the systemic root of the recurring 'context deadline exceeded' / BeforeAll-timeout go-rod flakes.e2e/browsersuite to the non-blockingpage.Elements(sel)pattern (returns immediately with empty slice when absent; checklen(els) > 0before indexing intoels[0]). Same treatment forpage.Has/MustHasandpage.MustElementsinside Eventually loops.journey_oi1l2_screenshot,journey_library_counts_sidebar,journey_cover_card_series) are deliberately skipped.Test plan
go build -tags e2e ./e2e/browser/...compiles cleanmake e2e-policy-checkpasses (all Describes are Ordered journey containers)Closes bead bookshelf-eu18m on merge.
Security Review — PR #1331 (bookshelf-eu18m)
Scope: 53 mechanical conversions of blocking
page.Element()/page.MustHas()/page.MustElements()insideEventuallypoll loops to the non-blockingpage.Elements()equivalent, across 23e2e/browser/test files.File boundary check
All 23 changed files are under
e2e/browser/and carry//go:build e2e+package browser_test. Zero production code files are touched. Confirmed mechanically — no file outsidee2e/browser/appears in the diff.Security assertion integrity (CSP, auth, OIDC)
journey_read_book_test.go — two CSP checks (
Expect(cspReader.Violations()).To(BeEmpty())andExpect(cspDetail.Violations()).To(BeEmpty())) are both outside the convertedEventuallyblocks. The conversion only changes how the poll loop detects that the page has finished loading before the assertion fires. TheViolations()assertions themselves are unchanged context lines.journey_manage_library_test.go — same pattern. The
Expect(csp.Violations()).To(BeEmpty())assertion is an unchanged context line; only the precedingEventuallyDOM-ready poll was converted.journey_oidc_group_mapping_test.go — the converted blocks are purely DOM-visibility checks (panel open/closed, modal hidden attribute, mapping row presence/absence). The authorization logic under test is the server-side OIDC group → role mapping; the test exercises it by driving form submit and asserting DOM state changes, none of which is altered by the conversion.
All
Violations(),BeEmpty(),ContainSubstring,Equal,BeNumericallyGomega matchers are unchanged. The conversion is symmetric: every previouserr == nil && el != nilguard becomeserr == nil && len(els) > 0; everyel.SomeMethod()becomesels[0].SomeMethod(). No condition was weakened, inverted, or dropped.No secrets / PII
No credentials, tokens, API keys, or PII appear in the diff.
Summary
This is a pure flake-fix mechanical refactor. The pattern changed is exactly as described:
page.Element()blocks the poll loop for up to the page timeout when the element is absent;page.Elements()returns immediately with an empty slice, allowingEventuallyto retry on its own cadence. No security assertion is weakened, no production code is modified, no authz/scoping logic is touched.REVIEW VERDICT: 0 blocker, 0 major, 0 minor
Code Review — bookshelf-eu18m (PR #1331)
Reviewed per
.claude/rules/review-standard.md. All 23 changed files are ine2e/browser/— no production code touched.Methodology
Systematically verified all 53 conversions across three risk categories:
[0]access preceded byif err != nil || len(els) == 0 { return false/""/0 }Must*calls outsideEventuallyleft untouchedFindings
Guard completeness — PASS. Every
[0]access is inside alen > 0guard. The 12 conversions that useels, _ := page.Elements(...)(error discarded) never access[0]— they only calllen(els)— so no index-out-of-range risk there either.Semantic equivalence — PASS. All conversions preserve the original assertion:
page.Element(sel).MustText()→els[0].MustText()under guard (same text check)page.Has(sel)/page.MustHas(sel)→len(els) > 0(non-blocking existence check;page.Hasandpage.Elementsare both immediate DOM snapshots, semantically equivalent)!page.MustHas(sel)→len(els) == 0(negation preserved correctly, e.g.journey_oidc_group_mapping_test.go:203)page.MustElements(sel)insideEventually→page.Elements(sel)with_, _(no[0]access, onlylen)Over-conversion — PASS. Bare
Must*calls outsideEventuallyare not converted:e2e/browser/journey_books_list_test.go:231:cbs := page.MustElements(...)(post-Eventuallyaction, left as Must) — correct.page.MustElement(...).MustClick()action lines throughout — all correctly left alone.Variable naming — PASS. The
journey_duplicates_test.gohunk correctly renames the innerelstogroupsto avoid shadowing the outerelsused for the dismiss button in the same closure scope.MustHas-inside-Eventually conversions (OIDC, manage_library) — PASS. The two cases where
page.MustElementorpage.MustHaswas called bare insideEventually(would panic/propagate if element absent) are now safe:journey_oidc_group_mapping_test.go:121andjourney_manage_library_test.go:93both properly returnfalseon absence.File scope — PASS. All 23 files are under
e2e/browser/. Nointernal/,cmd/, templates, or static assets touched.No findings.
REVIEW VERDICT: 0 blocker, 0 major, 0 minor
fe556ad76c05be7d8b23