Skip to content

tests: shared browser-regression harness — six drifting copies become one (#515) - #518

Merged
richard-devbot merged 2 commits into
mainfrom
claude/browser-harness-515
Jul 31, 2026
Merged

tests: shared browser-regression harness — six drifting copies become one (#515)#518
richard-devbot merged 2 commits into
mainfrom
claude/browser-harness-515

Conversation

@richard-devbot

Copy link
Copy Markdown
Owner

Closes #515 · #502 hygiene lane · follow-up from PR #514's Qodo review.

What

Every browser suite carried its own ~90-line copy of the same plumbing — and the copies had already drifted: the spawn error handler existed only in the newest file (Qodo's #514 finding), named timeout constants in three of six, wrapper-level page-error assertions in four of six.

tests/browser/helpers/harness.js now owns it all: startServer (with the error handler + named constants), the one-browser-per-file lifecycle, the browserTest wrapper (skip-without-Chromium, temp fixture root, page-error collection + uniform assertion, teardown), gotoDashboard (optional STATE wait) and navigate (visible-nav-button click with strict/lenient visibility modes). Each suite keeps only its fixtures, selectors, and assertions — net −346 lines.

One deliberate tightening: dashboard-96's journeys previously lacked the wrapper-level no-page-errors gate (only journey 1 asserted explicitly); the shared wrapper applies the contract uniformly — all journeys pass under it.

Verification

Behavior-preserving refactor: browser suite 14/14 (same journey count), core 1817/1818 with the single failure being the known temp-dir teardown flake (dashboard-env-write, ENOTEMPTY, 8/8 in isolation — second sighting of the class, now filed as #517), lint 0 errors, typecheck 0, whitespace clean.

Merging after green checks + reviewer bodies read, per the current working protocol.

🤖 Generated with Claude Code

… one (#515)

Every browser suite carried its own ~90-line copy of the same plumbing
(startServer, browser lifecycle, browserTest wrapper, gotoDashboard), and
the copies had already drifted: the spawn 'error' handler existed only in
the newest file (Qodo, PR #514), named timeout constants in three of six,
page-error assertions in four of six.

tests/browser/helpers/harness.js now owns all of it — spawn error handler
and named constants everywhere, uniform page-error gating (dashboard-96's
journeys previously lacked the wrapper-level assert; the shared wrapper
applies the contract uniformly), consistent teardown. Each suite keeps
only its fixtures, selectors, and assertions. Behavior-preserving: the
14-journey suite is the parity net, same count, all green.

Closes #515.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@strix-security

strix-security Bot commented Jul 31, 2026

Copy link
Copy Markdown

Strix Security Review

No security issues found.

Updated for 51c5e9f.


Reviewed by Strix
Re-run review · Configure security review settings

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

tests: consolidate shared browser regression harness

🧪 Tests ✨ Enhancement 🕐 40+ Minutes

Grey Divider

AI Description

• Centralize Playwright browser-test plumbing into a single shared harness module.
• Remove drifted per-suite server/bootstrap/teardown code while keeping suite assertions intact.
• Enforce consistent uncaught page-error gating across all browser journeys.
Diagram

graph TD
  T["Browser test suites"] --> H["Shared harness"] --> S["Dashboard server process"]
  H --> P["Playwright Chromium"]
  H --> F["Temp fixture root"]
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Adopt Playwright Test runner + fixtures
  • ➕ First-class browser fixtures (browser/context/page) and better reporting
  • ➕ Built-in retries/trace collection can reduce CI flake debugging time
  • ➖ Larger migration away from node:test and existing suite conventions
  • ➖ Higher upfront churn than consolidating the existing harness
2. Keep per-suite harnesses but enforce a shared template
  • ➕ Avoids cross-suite coupling in a shared helper module
  • ➕ Allows suite-specific tweaks without option flags
  • ➖ Drift will recur without strict automation
  • ➖ Still repeats the same lifecycle/server plumbing across files

Recommendation: The shared harness module is the best incremental step: it removes duplicated lifecycle/server code (including the previously missing spawn 'error' handler) while keeping the existing node:test structure. The small, explicit option surface (seed/contextOptions/assertPageErrors/tmpPrefix plus gotoDashboard/navigate helpers) is sufficient for current suites without forcing a heavier migration to Playwright’s full test runner.

Files changed (7) +200 / -546

Tests (7) +200 / -546
dashboard-492-workspace-empty.test.jsSwitch suite to shared harness and standardized dashboard navigation +6/-88

Switch suite to shared harness and standardized dashboard navigation

• Replaces the local server/browser lifecycle and wrapper with setupBrowserSuite() from the shared harness. Uses harness gotoDashboard with STATE wait enabled and keeps suite-specific seeding and assertions.

tests/browser/dashboard-492-workspace-empty.test.js

dashboard-493-hero.test.jsAdopt shared harness for browser lifecycle, server start, and teardown +6/-88

Adopt shared harness for browser lifecycle, server start, and teardown

• Removes per-file Playwright/server boot code and delegates to the shared harness wrapper. Uses shared gotoDashboard with STATE wait to match prior behavior.

tests/browser/dashboard-493-hero.test.js

dashboard-494-approval-dedup.test.jsRefactor to shared harness with suite-specific temp prefix +5/-87

Refactor to shared harness with suite-specific temp prefix

• Eliminates duplicated spawn/server/bootstrap logic in favor of setupBrowserSuite(). Preserves the suite’s temp-dir prefix and uses shared gotoDashboard with STATE wait.

tests/browser/dashboard-494-approval-dedup.test.js

dashboard-495.test.jsUse shared harness and shared navigate helper for page routing checks +7/-102

Use shared harness and shared navigate helper for page routing checks

• Replaces the local browserTest wrapper and navigation helper with the shared harness implementation. Keeps suite-specific comments and uses gotoDashboard with STATE wait for interaction-contract correctness.

tests/browser/dashboard-495.test.js

dashboard-512-motion.test.jsWrap motion tests with shared harness while preserving context options +11/-80

Wrap motion tests with shared harness while preserving context options

• Moves server start and browser lifecycle to the shared harness while retaining per-test contextOptions (e.g., reduced motion settings). Adds a small adapter wrapper to call gotoDashboard before running existing assertions.

tests/browser/dashboard-512-motion.test.js

dashboard-96.test.jsMigrate multi-journey suite to shared harness and tighten page-error contract +9/-101

Migrate multi-journey suite to shared harness and tighten page-error contract

• Replaces bespoke lifecycle/server code with setupBrowserSuite() and shared gotoDashboard/navigate. Uses navigate with assertVisible=false to preserve prior lenient visibility behavior, while the shared wrapper now enforces uniform uncaught page-error assertions across journeys.

tests/browser/dashboard-96.test.js

harness.jsAdd shared browser-regression harness module +156/-0

Add shared browser-regression harness module

• Introduces a reusable harness that starts the dashboard server with named timeouts and a spawn error handler, manages a one-browser-per-file lifecycle, and provides a browserTest wrapper with per-test temp roots, teardown, and optional page-error assertions. Adds shared helpers for gotoDashboard (optional STATE wait) and navigate (visible-nav click with strict/lenient visibility modes).

tests/browser/helpers/harness.js

@coderabbitai

coderabbitai Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@richard-devbot, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 11 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 77fb0216-c1a0-4b5c-8d65-bc9549a1d908

📥 Commits

Reviewing files that changed from the base of the PR and between 8e5ef36 and 7351ea1.

📒 Files selected for processing (8)
  • tests/browser-harness-spawn-515.test.js
  • tests/browser/dashboard-492-workspace-empty.test.js
  • tests/browser/dashboard-493-hero.test.js
  • tests/browser/dashboard-494-approval-dedup.test.js
  • tests/browser/dashboard-495.test.js
  • tests/browser/dashboard-512-motion.test.js
  • tests/browser/dashboard-96.test.js
  • tests/browser/helpers/harness.js

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@qodo-code-review

qodo-code-review Bot commented Jul 31, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (1) 📜 Skill insights (1)

Context used
✅ Compliance rules (platform): 300 rules
✅ Skills: 17 invoked
  code-review-pr
  claude-api
  documentation-writing
  pptx
  docx
  performance-monitoring
  security-compliance
  cso
  plan-eng-review
  design-review
  prompt-engineering
  mcp-builder
  qa-testing
  code-patterns
  xlsx
  security-owasp
  testing-qa

Grey Divider


Remediation recommended

1. startServer spawn error untested 📜 Skill insight ▣ Testability
Description
The new child.on('error', ...) rejection path in startServer() has no corresponding test that
triggers a spawn failure and asserts the rejection behavior. This violates the requirement that
newly added error-handling branches must be covered by tests.
Code

tests/browser/helpers/harness.js[R84-87]

+    // A spawn failure must reject the promise, never raise an unhandled
+    // 'error' event (Qodo, PR #514).
+    child.on('error', (err) => { if (!settled) { settled = true; clearTimeout(timer); rejectPromise(err); } });
+    child.on('exit', () => { if (!settled) { settled = true; clearTimeout(timer); rejectPromise(new Error(`server exited\n${out}`)); } });
Relevance

●● Moderate

Mixed precedent: added error-path tests accepted (PR #305), but spawn error handler merged without
dedicated test (PR #514).

PR-#305
PR-#514

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 1400010 requires a test to exercise newly added error-handling branches. The
harness adds a spawn error handler that rejects the startServer() promise, but there is no
accompanying test in this PR that triggers a spawn failure and asserts the rejection behavior.

tests/browser/helpers/harness.js[84-87]
Skill: qa-testing

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`tests/browser/helpers/harness.js` adds a new error-handling branch for spawn failures (`child.on('error', ...)`), but there is no test that forces this branch and asserts that `startServer()` rejects as intended.

## Issue Context
PR Compliance ID 1400010 requires that when error-handling code is added, a test must trigger the error path and assert the behavior.

## Fix Focus Areas
- tests/browser/helpers/harness.js[52-88]
- tests/browser/helpers/harness.test.js[1-200]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Informational

2. Refactor includes new page-error gate 📘 Rule violation ⚙ Maintainability
Description
This PR combines a mechanical refactor (deduplicating browser harness logic) with a behavioral
change (dashboard-96 now uniformly fails on any uncaught pageerror via the shared wrapper). This
violates the requirement to keep mechanical refactors separate from behavior changes, making review
and rollback riskier.
Code

tests/browser/dashboard-96.test.js[R41-49]

+// Shared harness (#515): server boot, browser lifecycle, per-test teardown.
+// This suite historically did NOT gate every journey on zero page errors
+// in the wrapper (only journey 1 asserts explicitly) — the shared wrapper
+// now applies that contract uniformly.
+const suite = setupBrowserSuite();
+const browserTest = (name, seed, body) => suite.browserTest(name, { seed, tmpPrefix: 'rstack-browser-96-' }, body);

-// Wrap a browser test so it skips cleanly when the binary is unavailable, and
-// always tears down its server + page.
-function browserTest(name, seed, body) {
-  test(name, async (t) => {
-    if (!browser) { t.skip('Chromium not installed — run `npx playwright-core install chromium`'); return; }
-    const root = mkdtempSync(join(tmpdir(), 'rstack-browser-96-'));
-    let server = null;
-    const context = await browser.newContext();
-    const page = await context.newPage();
-    const pageErrors = [];
-    page.on('pageerror', (err) => pageErrors.push(String(err)));
-    try {
-      await seed(root);
-      server = await startServer(root);
-      await body({ page, server, pageErrors, t });
-    } finally {
-      await context.close();
-      if (server) server.stop();
-      rmSync(root, { recursive: true, force: true });
-    }
-  });
-}
-
-async function gotoDashboard(page, server) {
-  await page.goto(server.baseUrl, { waitUntil: 'networkidle' });
-}
-
-async function navigate(page, pageId) {
-  // Click the VISIBLE nav button for this page. The markup carries the same
-  // data-page in both the desktop sidebar and the (hidden) mobile panel, so a
-  // bare selector can match the hidden one — pick the one that is actually
-  // rendered, matching the real user path without selector ambiguity.
-  const clicked = await page.evaluate((id) => {
-    const buttons = [...document.querySelectorAll(`nav button[data-page="${id}"]`)];
-    const target = buttons.find((b) => b.offsetParent !== null) || buttons[0];
-    if (target) { target.click(); return true; }
-    return false;
-  }, pageId);
-  assert.ok(clicked, `nav button for ${pageId} exists`);
-  await page.waitForFunction((id) => {
-    const el = document.getElementById('page-' + id);
-    return el && !el.hidden && el.offsetParent !== null;
-  }, pageId, { timeout: 5000 }).catch(() => { /* assertions below report the real state */ });
-}
+const gotoDashboard = (page, server) => harnessGoto(page, server);
+const navigate = (page, pageId) => harnessNavigate(page, pageId, { assertVisible: false });
Relevance

● Weak

No evidence team enforces “refactor vs behavior separate”; similar compliance-policy suggestions
rejected (PR #504).

PR-#504

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 1587047 requires that mechanical refactors and behavioral changes not be bundled
together. The new shared harness introduces a default wrapper-level assertion on pageErrors, and
dashboard-96 explicitly opts into the shared wrapper and documents the newly-uniform contract,
which is a behavioral tightening alongside the refactor.

Rule 1587047: Keep mechanical refactors separate from behavior changes within the same code change
tests/browser/dashboard-96.test.js[41-49]
tests/browser/helpers/harness.js[103-118]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
This PR mixes a large mechanical refactor (shared harness extraction) with a behavioral tightening (enforcing the `no uncaught page errors` contract uniformly for `dashboard-96`). This should be separated so the refactor can be reviewed/rolled back independently of the behavior change.

## Issue Context
`tests/browser/dashboard-96.test.js` explicitly notes the behavioral tightening, and the new shared harness enforces `assert.deepEqual(pageErrors, [], ...)` by default.

## Fix Focus Areas
- tests/browser/dashboard-96.test.js[41-49]
- tests/browser/helpers/harness.js[103-118]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

To customize comments, go to the Qodo configuration screen, or learn more in the docs.

Qodo Logo

Comment on lines +84 to +87
// A spawn failure must reject the promise, never raise an unhandled
// 'error' event (Qodo, PR #514).
child.on('error', (err) => { if (!settled) { settled = true; clearTimeout(timer); rejectPromise(err); } });
child.on('exit', () => { if (!settled) { settled = true; clearTimeout(timer); rejectPromise(new Error(`server exited\n${out}`)); } });

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

1. startserver spawn error untested 📜 Skill insight ▣ Testability

The new child.on('error', ...) rejection path in startServer() has no corresponding test that
triggers a spawn failure and asserts the rejection behavior. This violates the requirement that
newly added error-handling branches must be covered by tests.
Agent Prompt
## Issue description
`tests/browser/helpers/harness.js` adds a new error-handling branch for spawn failures (`child.on('error', ...)`), but there is no test that forces this branch and asserts that `startServer()` rejects as intended.

## Issue Context
PR Compliance ID 1400010 requires that when error-handling code is added, a test must trigger the error path and assert the behavior.

## Fix Focus Areas
- tests/browser/helpers/harness.js[52-88]
- tests/browser/helpers/harness.test.js[1-200]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Qodo (testability, compliance 1400010): the harness's child 'error'
handler had no test triggering a real spawn failure. startServer gains an
injectable execPath seam; a browser-free core-suite test spawns a
nonexistent binary and asserts the clean ENOENT rejection. Mutation-
checked: with the handler removed, the test fails on the unhandled event.

Refs #515 (PR #518 review follow-up).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@richard-devbot

Copy link
Copy Markdown
Owner Author

Qodo's testability finding addressed: startServer gains an injectable execPath seam and a browser-free core-suite test that spawns a nonexistent binary and asserts the clean ENOENT rejection — mutation-checked (handler removed → the test fails on the unhandled event). — Claude Main

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Extract shared browser-test harness — six copies of startServer/browserTest drift independently (#502 hygiene)

2 participants