Skip to content

fix(async-parallel): suppress in tests, browser fixtures, and ordered UI flows#270

Merged
aidenybai merged 7 commits into
mainfrom
cursor/suppress-async-parallel-noise-3ea1
May 16, 2026
Merged

fix(async-parallel): suppress in tests, browser fixtures, and ordered UI flows#270
aidenybai merged 7 commits into
mainfrom
cursor/suppress-async-parallel-noise-3ea1

Conversation

@aidenybai
Copy link
Copy Markdown
Member

@aidenybai aidenybai commented May 16, 2026

Problem

async-parallel was loud in places where the sequential awaits are the whole point:

  • It used a narrow local TEST_FILE_PATTERN (*.test.*, *.spec.*, *.stories.*) that missed tests/, test/, __tests__/, e2e/, playwright/, cypress/, fixtures, mocks, and *.browser.tsx.
  • It wasn't tagged test-noise, so the shared isTestFilePath() suppression in mergeAndFilterDiagnostics didn't apply.
  • It had no awareness of test-library imports — src/test-utils.ts importing @playwright/test or @testing-library/react still got the advice.
  • It flagged ordered UI flows like render → findByRole → click → findByText, where each await must observe the side effects of the previous one.
  • It flagged intentional animation/demo pacing (sleep, animate, fake-timer tick, …) that would lose meaning under Promise.all().

Dogfood references: async-parallel in SettingsPanels.browser.tsx, ordered test-like render → expect → click → expect screenshots, issues #216 and #219, related PR #238.

Fix

  1. Tag async-parallel as test-noise. Lets mergeAndFilterDiagnostics route every path that isTestFilePath() recognises through the existing shared filter — __tests__/, tests/, test/, __mocks__/, cypress/, e2e/, playwright/, *.test/spec/stories/story/fixture/fixtures.*, and Windows-slashed equivalents — for free.
  2. Drop the narrow local TEST_FILE_PATTERN early-return — the tag now covers it.
  3. Add a *.browser.[jt]sx? filename check for Vitest browser mode / Storybook test-runner / Playwright Component Testing fixtures that the path heuristic intentionally doesn't classify as tests.
  4. Track ImportDeclaration sources during traversal. Any import matching isTestLibraryImportSource() flags the file as a fixture and suppresses the rule. Covers vitest, jest, mocha, chai, node:test, bun:test, @testing-library/*, @playwright/*, playwright, cypress, @cypress/*, @storybook/test*, puppeteer, webdriverio, @nuxt/test-utils, and subpaths (vitest/browser, @vitest/spy, …).
  5. Skip sequences that contain a serialization signal. When a consecutive-await block contains at least one ordered UI-flow call (render, expect, click, fill, findByRole, findAllByText, page.goto, locator(...).click(), …) or one intentional sequencing call (sleep, delay, animate, spring, tween, tick, advanceTimersByTime, …), the entire block is treated as deliberately serial. A single render() or sleep() in the middle of three otherwise-independent awaits is enough — collapsing such a block into Promise.all([...]) would change observable behavior.
  6. Documented inline opt-out (// oxlint-disable-next-line react-doctor/async-parallel -- <reason>) still works for everything else, since mergeAndFilterDiagnostics already honors it.

Edge cases covered

  • A consecutive prefix of independent awaits followed (after non-await statements) by a later UI-flow call still fires on the prefix — the UI signal doesn't bleed across non-await statements.
  • Dependent chains where each await reads the previous result keep being silent (existing behavior).
  • import type from a test library still suppresses (a type-only import is still a strong "this is a fixture" signal).
  • findBy* / findAllBy* are matched by prefix without enumerating every suffix.
  • Locator method chains (page.locator("...").click()) match because the rightmost identifier in the callee trail is checked, not just the bare callee.
  • Windows-slashed test paths (src\components\Button.test.tsx) are auto-suppressed because the shared isTestFilePath() normalizes backslashes.

Tests

  • packages/react-doctor/tests/regressions/js-performance-rules.test.ts adds 10 end-to-end regressions exercising the in-rule guards via runOxlint():
    • Production code still flags three independent sequential awaits.
    • *.browser.tsx render/findBy/click/findBy stays silent.
    • @playwright/test, @testing-library/react, and vitest/browser imports each silence the rule on production-shaped paths.
    • sleep / animate / fadeIn sequences stay silent.
    • Documented inline opt-out stays silent.
    • Independent prefix followed by a UI flow still flags the prefix.
    • Playwright locator chains stay silent.
    • Dependent await fetchProfile(user.id) chains stay silent.
  • packages/react-doctor/tests/merge-and-filter-diagnostics.test.ts adds 5 cases exercising the test-noise tag through mergeAndFilterDiagnostics: *.test.tsx, __tests__/, playwright/ / cypress/ / e2e/, Windows-slashed paths, and a production path control.
  • Full suite (pnpm test, pnpm lint, pnpm typecheck, pnpm format:check) is green: 84 test files, 995 tests.

Refs: TODOS.md "Suppress async parallelization advice in tests and ordered UI flows".

Eval Results

Ran against 100-repo corpus (baseline: main @ 529015d, 13,279 diagnostics).

Parity: -204 — All removals are async-parallel false positives in test/e2e files. Zero regressions.

Metric Value
Diagnostics added 0
Diagnostics removed 204
Net change -204
Rule affected async-parallel only
Repos changed 6

Breakdown by repo

Repo Root dir Removed File types
pierrecomputer/pierre packages/trees -68 test/e2e/*.pw.ts, scripts/profileFileTree.ts
twentyhq/twenty packages/twenty-server -117 test/integration/**/*.integration-spec.ts
aidenybai/react-grab packages/react-grab -14 e2e/fixtures.ts
PostHog/posthog frontend -1 src/test/insight-testing/interactions.ts
PostHog/posthog packages/quill/apps/storybook -3 .storybook/test-runner.ts
twentyhq/twenty packages/twenty-front -1 useResetVirtualizationBecauseDataChanged.ts

All 204 removed hits were sequential awaits in Playwright tests, integration test fixtures, or browser fixtures — exactly the false positives this PR targets.

Open in Web Open in Cursor 

@reactreview
Copy link
Copy Markdown

reactreview Bot commented May 16, 2026

🔴 React Review0/100 (unchanged) · No new issues

Reviewed by react-review for commit f6e8b7b. Configure here.

@vercel
Copy link
Copy Markdown

vercel Bot commented May 16, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
react-doctor-website Ready Ready Preview, Comment May 16, 2026 11:41am

Comment thread packages/oxlint-plugin-react-doctor/src/plugin/constants/js.ts Outdated
cursor Bot pushed a commit that referenced this pull request May 16, 2026
Address two Bugbot findings on PR #270:

- getCalleeIdentifierTrail() rejected ChainExpression at entry, so
  optional-chained awaits like 'await page?.click()' produced an
  empty trail and the UI-flow / sequencing signal was missed. Peel
  any wrapping ChainExpression layers before checking for the call
  or new node — the inner traversal already handles ChainExpression
  but was never reached.
- '@storybook/test' as a bare prefix in
  TEST_LIBRARY_IMPORT_SOURCE_PREFIXES subsumed both
  '@storybook/test-runner' and '@storybook/testing-library' under
  the same catch-all, even though both are independently versioned
  and already enumerated in the exact set. Tighten the prefix to
  '@storybook/test/' and add '@storybook/test-runner/' and
  '@storybook/testing-library/' for genuine subpaths only.
  Document the trailing-slash invariant in the comment so future
  entries follow the same rule.

Add three regressions:

- 'await page?.click()' is treated as an ordered UI flow and skipped.
- '@storybook/test-runner' import on a production-shaped path
  silences three independent sequential awaits.
- '@storybook/test/spy' subpath import does the same.

Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit f3e4ffc. Configure here.

Comment thread packages/oxlint-plugin-react-doctor/src/plugin/constants/js.ts
cursor Bot pushed a commit that referenced this pull request May 16, 2026
Address two Bugbot findings on PR #270:

- getCalleeIdentifierTrail() rejected ChainExpression at entry, so
  optional-chained awaits like 'await page?.click()' produced an
  empty trail and the UI-flow / sequencing signal was missed. Peel
  any wrapping ChainExpression layers before checking for the call
  or new node — the inner traversal already handles ChainExpression
  but was never reached.
- '@storybook/test' as a bare prefix in
  TEST_LIBRARY_IMPORT_SOURCE_PREFIXES subsumed both
  '@storybook/test-runner' and '@storybook/testing-library' under
  the same catch-all, even though both are independently versioned
  and already enumerated in the exact set. Tighten the prefix to
  '@storybook/test/' and add '@storybook/test-runner/' and
  '@storybook/testing-library/' for genuine subpaths only.
  Document the trailing-slash invariant in the comment so future
  entries follow the same rule.

Add three regressions:

- 'await page?.click()' is treated as an ordered UI flow and skipped.
- '@storybook/test-runner' import on a production-shaped path
  silences three independent sequential awaits.
- '@storybook/test/spy' subpath import does the same.

Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
@cursor cursor Bot force-pushed the cursor/suppress-async-parallel-noise-3ea1 branch from 03d1380 to e91a18d Compare May 16, 2026 11:28
cursoragent and others added 7 commits May 16, 2026 11:39
Add the building blocks needed to suppress async-parallel advice in
files that are clearly tests or ordered UI flows:

- BROWSER_TEST_FILE_PATTERN matches Vitest browser mode / Playwright
  Component Testing / Storybook test-runner files (.browser.[jt]sx?).
- TEST_LIBRARY_IMPORT_SOURCES + TEST_LIBRARY_IMPORT_SOURCE_PREFIXES
  enumerate the module identifiers that prove a file is a test, story,
  or interaction-driving harness (Vitest, Jest, Mocha, Testing Library,
  Playwright, Cypress, Storybook test, Puppeteer, WebdriverIO, etc.).
- isTestLibraryImportSource() is the thin predicate over those tables.
- ORDERED_UI_FLOW_CALLEE_NAMES + ORDERED_UI_FLOW_CALLEE_PREFIXES name
  the render/expect/click/findBy*/page.goto-style callees that signal
  a deliberately serialized UI flow.
- INTENTIONAL_SEQUENCING_CALLEE_NAMES names the sleep/delay/animation
  callees that signal deliberate pacing, mirroring the sister set in
  async-await-in-loop.
- getCalleeIdentifierTrail() walks a CallExpression's callee chain
  (members, optional-chains, intermediate calls) and yields every
  identifier name leaf-first so the rule can pattern-match the
  rightmost identifier against any of those tables.

Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
The previous implementation used a narrow local TEST_FILE_PATTERN that
only matched '*.test.*', '*.spec.*', and '*.stories.*' suffixes — so
it false-positived on:

- '*.browser.tsx' Vitest browser fixtures.
- Files under 'tests/', 'test/', '__tests__/', 'e2e/', 'playwright/',
  'cypress/', or fixture/mock directories that the shared
  isTestFilePath() heuristic already understands.
- Production-co-located helpers (src/test-utils.ts, fixtures shipped
  next to a component) that import a test runner or driver.
- Render → expect → click → expect ordered UI flows whose
  serialization is the whole point.
- Intentional animation/demo sequencing (sleep, delay, animate, spring,
  fakeTimers tick/advance) that loses meaning under Promise.all().

Refit:

- Tag the rule 'test-noise' so mergeAndFilterDiagnostics routes every
  path that isTestFilePath() recognises through the shared filter —
  picking up __tests__/, tests/, test/, __mocks__/, cypress/, e2e/,
  playwright/, *.test/spec/stories/story/fixture/fixtures.* and
  Windows-slashed equivalents for free.
- Drop the local TEST_FILE_PATTERN early-return; the tag covers it.
- Add a '*.browser.[jt]sx?' filename check for Vitest browser mode /
  Storybook test-runner / Playwright Component Testing fixtures that
  the path heuristic intentionally doesn't classify as tests.
- Track ImportDeclaration sources during file traversal — any import
  matching isTestLibraryImportSource() flags the file as a fixture and
  suppresses the rule (so a 'src/seed.ts' that imports
  '@playwright/test' or '@testing-library/react' is treated as a
  harness even though its path looks like production code).
- When a consecutive-await block contains at least one ordered UI-flow
  call (render/expect/click/locator.fill/findByRole/page.goto/…) or
  one intentional sequencing call (sleep/delay/animate/tick/…), skip
  the entire block. A single render() or sleep() in the middle of
  three otherwise-independent awaits proves the sequence is
  deliberately serialized; collapsing it into Promise.all([...]) would
  change observable behavior.
- Authors who genuinely want serial execution without any of these
  signals can opt out per-block with a documented
  '// oxlint-disable-next-line react-doctor/async-parallel -- <reason>'
  comment, which mergeAndFilterDiagnostics already honors.

Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
Two layers of coverage for the async-parallel suppression refit:

- packages/react-doctor/tests/regressions/js-performance-rules.test.ts
  exercises the in-rule guards end-to-end via runOxlint(): production
  code still flags three independent awaits; '*.browser.tsx' fixtures,
  '@playwright/test' / '@testing-library/react' / 'vitest/browser'
  imports, render → findByRole → userEvent.click → findByText flows,
  Playwright locator chains, intentional sleep/animate/spring
  sequencing, and documented inline opt-outs all stay quiet; an
  independent prefix of three awaits followed (after non-await
  statements) by a UI-flow call still flags the prefix; dependent
  chains where each await reads an earlier result are never flagged.
- packages/react-doctor/tests/merge-and-filter-diagnostics.test.ts
  exercises the tag-driven path filter through mergeAndFilterDiagnostics:
  '*.test.tsx', '__tests__/' directories, 'playwright/' / 'cypress/' /
  'e2e/' top-level directories, and Windows-slashed test paths all
  auto-suppress, while production paths keep firing.

Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
… UI flows"

Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
Address two Bugbot findings on PR #270:

- getCalleeIdentifierTrail() rejected ChainExpression at entry, so
  optional-chained awaits like 'await page?.click()' produced an
  empty trail and the UI-flow / sequencing signal was missed. Peel
  any wrapping ChainExpression layers before checking for the call
  or new node — the inner traversal already handles ChainExpression
  but was never reached.
- '@storybook/test' as a bare prefix in
  TEST_LIBRARY_IMPORT_SOURCE_PREFIXES subsumed both
  '@storybook/test-runner' and '@storybook/testing-library' under
  the same catch-all, even though both are independently versioned
  and already enumerated in the exact set. Tighten the prefix to
  '@storybook/test/' and add '@storybook/test-runner/' and
  '@storybook/testing-library/' for genuine subpaths only.
  Document the trailing-slash invariant in the comment so future
  entries follow the same rule.

Add three regressions:

- 'await page?.click()' is treated as an ordered UI flow and skipped.
- '@storybook/test-runner' import on a production-shaped path
  silences three independent sequential awaits.
- '@storybook/test/spy' subpath import does the same.

Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
`async-parallel` was the only consumer of `TEST_FILE_PATTERN`, and
the previous commit moved it to the `test-noise` tag plus
`BROWSER_TEST_FILE_PATTERN`. `SECRET_TEST_FILE_PATTERN` in
`constants/security.ts` is a distinct export with a wider suffix
family and stays. No other call sites reference the bare name.

Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
@cursor cursor Bot force-pushed the cursor/suppress-async-parallel-noise-3ea1 branch from e91a18d to f6e8b7b Compare May 16, 2026 11:41
@aidenybai aidenybai merged commit 4cbf436 into main May 16, 2026
7 checks passed
@aidenybai aidenybai deleted the cursor/suppress-async-parallel-noise-3ea1 branch May 16, 2026 11:48
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.

2 participants