Skip to content

Conversation

@kentcdodds
Copy link
Owner

@kentcdodds kentcdodds commented Jan 8, 2026

Fix flaky Playwright tests by addressing multiple race conditions in element visibility, page navigation, and asynchronous email handling.

The tests were failing intermittently due to timing issues. Specifically, the test would try to click elements before they were rendered, navigate to an admin page before a call submission was confirmed, and attempt to read mock emails before they were fully written to the file system. This PR introduces explicit waits for URL redirects and element visibility, and adds a retry mechanism to the readEmail utility to ensure stability.


Open in Cursor Open in Web


Note

Improves reliability of the calls E2E flow and hardens avatar lookup timeouts.

  • Replaces abort-based gravatar check with fetchWithTimeout using Promise.race and per-context timeoutMs to avoid Node v24 abort crash in user-info.server.ts
  • In e2e/calls.spec.ts, adds URL-redirect wait after submission, waits for admin call link visibility before click, and increases robustness of assertions
  • In e2e/utils.ts, adds sleep and enhances readEmail with retry/backoff and error logging to handle async email fixture writes

Written by Cursor Bugbot for commit 9626935. This will update automatically on new commits. Configure here.

Summary by CodeRabbit

  • Tests

    • Improved end-to-end test reliability for call recording flows with explicit waits for redirects and dynamic content.
    • Added robust retry logic for email verification to handle delivery delays.
  • Bug Fixes

    • Made avatar existence checks more resilient by introducing a timeout-based request pattern to avoid hangs and intermittent failures.

✏️ Tip: You can customize this high-level summary in your review settings.

Add retries to email reading and wait for redirect in call test.

Co-authored-by: me <me@kentcdodds.com>
@cursor
Copy link

cursor bot commented Jan 8, 2026

Cursor Agent can help with this pull request. Just @cursor in comments and I'll start working on changes in this branch.
Learn more about Cursor Agents

@coderabbitai
Copy link

coderabbitai bot commented Jan 8, 2026

📝 Walkthrough

Walkthrough

Introduces retry/wait logic: e2e tests now wait for redirects and element visibility and use a retryable email reader; utility adds configurable retry/sleep; server-side gravatar check switched from AbortController to a Promise.race-based fetchWithTimeout.

Changes

Cohort / File(s) Summary
E2E test waits
e2e/calls.spec.ts
After submitting a recording, wait up to 10s for redirect matching /calls/record/[a-z0-9-]+. When opening admin calls, wait up to 10s for the specific call link to be visible before clicking. Use retryable email read with maxRetries: 10 and retryDelay: 500ms.
Email retry utility
e2e/utils.ts
readEmail signature extended to accept { maxRetries?, retryDelay? } and implemented a retry loop with a new sleep helper; logs per-attempt errors; removed unused import. Defaults: maxRetries=5, retryDelay=200ms.
Fetch timeout refactor
app/utils/user-info.server.ts
Replaced AbortController usage with fetchWithTimeout(url, options, timeoutMs) implemented via Promise.race; gravatarExistsForEmail computes timeoutMs and calls fetchWithTimeout(..., timeoutMs); error propagation adjusted for timeout promise.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~18 minutes

Poem

🐰 I waited, hopped, and peeked the log,

Retries like carrots in a fog,
Redirects caught, emails chased,
Timeouts tamed, no steps misplaced,
A tiny hop for stable tests. 🥕

🚥 Pre-merge checks | ✅ 1 | ❌ 2
❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'Playwright test failures' is vague and generic, using non-descriptive language that doesn't convey the specific nature of the changes (fixing race conditions, adding retry mechanisms, replacing timeout mechanisms). Consider a more specific title like 'Fix flaky Playwright tests with explicit waits and retry mechanisms' or 'Add timeout retry mechanism to stabilize e2e tests'.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

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 and usage tips.

@kentcdodds
Copy link
Owner Author

@cursor, playwright is failing in CI. Please fix that.

@cursor
Copy link

cursor bot commented Jan 8, 2026

I've pushed the fix. The CI failure was caused by importing entry.server.tsx in the test utilities, which triggered side effects that conflicted with the Playwright web server startup. The logs showed a Node.js stream assertion failure and an esbuild deadlock:

node: ../deps/uv/src/unix/stream.c:456: uv__stream_destroy: Assertion failed.
fatal error: all goroutines are asleep - deadlock!

The fix removes that import since the session utilities work correctly without it - the environment variables are already set by the web server.

Summary of all changes in this PR:

  1. Removed entry.server.tsx import from e2e/utils.ts - this was causing the server crash in CI
  2. Added retry mechanism for email reading - handles race conditions when the mock email fixture isn't written yet
  3. Added proper waits for element visibility - ensures elements are rendered before clicking
  4. Added URL redirect wait after call submission - confirms the call was created before navigating to admin page

Open in Cursor Open in Web

Learn more about Cursor Agents

The import of entry.server.tsx in the test utils was causing a Node.js
stream assertion failure and esbuild deadlock in CI, preventing the
Playwright web server from starting. This import was previously added
to initialize global.ENV, but it's not needed since the session utilities
work correctly with the environment variables already set.
@kentcdodds kentcdodds marked this pull request as ready for review January 8, 2026 17:14
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (1)
e2e/utils.ts (1)

30-64: LGTM! Robust retry mechanism for email reading.

The retry logic effectively handles both transient file read errors and delayed email availability. The configurable parameters with sensible defaults (5 retries, 200ms delay) provide flexibility for different test scenarios. Error logging aids in debugging when issues occur.

The TODO comment on line 40 mentions adding validation for the email structure. Would you like me to generate an implementation that validates the email object against the Email type to ensure data integrity?

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 06418ff and a1dc932.

📒 Files selected for processing (2)
  • e2e/calls.spec.ts
  • e2e/utils.ts
🧰 Additional context used
🧬 Code graph analysis (2)
e2e/calls.spec.ts (1)
e2e/utils.ts (1)
  • readEmail (30-64)
e2e/utils.ts (1)
content/blog/avoid-the-test-user/user-profile-example.jsx (1)
  • sleep (14-17)
🪛 ast-grep (0.40.4)
e2e/calls.spec.ts

[warning] 53-53: Regular expression constructed from variable input detected. This can lead to Regular Expression Denial of Service (ReDoS) attacks if the variable contains malicious patterns. Use libraries like 'recheck' to validate regex safety or use static patterns.
Context: new RegExp(title, 'i')
Note: [CWE-1333] Inefficient Regular Expression Complexity [REFERENCES]
- https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS
- https://cwe.mitre.org/data/definitions/1333.html

(regexp-from-variable)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Cursor Bugbot
  • GitHub Check: 🎭 Playwright
🔇 Additional comments (4)
e2e/calls.spec.ts (3)

48-49: LGTM! Explicit redirect wait improves test stability.

The explicit wait for URL redirect after form submission properly handles the asynchronous navigation, preventing the test from proceeding before the call record is created. The 10-second timeout is reasonable for this operation.


54-56: LGTM! Visibility wait prevents race condition.

The explicit wait for the call link to be visible before clicking addresses the race condition where the test might attempt to interact with elements before they're fully rendered. The 10-second timeout is appropriate.

Note: The static analysis warning about RegExp construction from a variable (line 54) is a false positive in this case, since title is generated by faker.lorem.words(2) (line 9) and is fully controlled, not user input.


73-77: LGTM! Retry mechanism handles asynchronous email delivery.

The retry configuration (10 retries with 500ms delay) appropriately handles the asynchronous nature of email delivery to the mock fixture. The comment clearly explains why this retry mechanism is necessary.

e2e/utils.ts (1)

26-28: LGTM! Sleep helper is correctly implemented.

The sleep utility is a straightforward Promise-based delay implementation that's used to space out retry attempts in the readEmail function.

@kentcdodds
Copy link
Owner Author

@cursor, playwright is still failing in CI

@cursor
Copy link

cursor bot commented Jan 8, 2026

I've pushed another fix. The CI was failing because of a Node.js v24 bug with abort signals on fetch requests. The previous implementation was:

function abortTimeoutSignal(timeMs: number) {
  const abortController = new AbortController()
  void new Promise((resolve) => setTimeout(resolve, timeMs)).then(() => {
    abortController.abort()
  })
  return abortController.signal
}

This was causing a crash in Node.js v24:

node: ../deps/uv/src/unix/stream.c:456: uv__stream_destroy: Assertion `!uv__io_active(&stream->io_watcher, POLLIN | POLLOUT)' failed.

The fix uses the built-in AbortSignal.timeout() API which is more compatible with Node.js's internal stream handling:

function abortTimeoutSignal(timeMs: number) {
  return AbortSignal.timeout(timeMs)
}

The tests pass locally with this change. The new CI run should succeed.


Open in Cursor Open in Web

Learn more about Cursor Agents

The previous implementation using AbortController with manual setTimeout
was causing a Node.js stream assertion failure in Node.js v24:
  uv__stream_destroy: Assertion `!uv__io_active...` failed

Using the built-in AbortSignal.timeout() API is more compatible with
Node.js internal stream handling and avoids this crash.
@kentcdodds
Copy link
Owner Author

@cursor, please check CI. It's still failing in CI. What's different between your environment and the CI environment that could cause these failures? Please investigate and make CI pass as well as local.

@cursor
Copy link

cursor bot commented Jan 8, 2026

I've identified the root cause and pushed a fix.

Key Difference Between Local and CI

Environment Node.js Version
Local v22.21.1
CI v24.12.0

The Problem

Node.js v24 has a bug where aborting fetch requests causes an internal crash:

node: ../deps/uv/src/unix/stream.c:456: uv__stream_destroy: Assertion `!uv__io_active(&stream->io_watcher, POLLIN | POLLOUT)' failed.

This happens when:

  1. A fetch request is made with an AbortSignal (including AbortSignal.timeout())
  2. The signal triggers an abort while the request is in-flight
  3. Node.js v24 crashes during stream cleanup

The Fix

Instead of using AbortSignal to timeout fetch requests, I replaced it with Promise.race:

// OLD (crashes on Node.js v24)
const response = await fetch(url, { signal: AbortSignal.timeout(100) })

// NEW (works on all Node.js versions)
async function fetchWithTimeout(url, options, timeoutMs) {
  const timeoutPromise = new Promise((_, reject) => {
    setTimeout(() => reject(new Error('Request timeout')), timeoutMs)
  })
  return Promise.race([fetch(url, options), timeoutPromise])
}

This achieves the same timeout behavior without actually aborting the underlying request, which avoids triggering the Node.js v24 bug.

Tests pass locally (15/15). The CI should now pass as well.


Open in Cursor Open in Web

Learn more about Cursor Agents

Node.js v24 has a bug where aborting fetch requests causes a crash:
  uv__stream_destroy: Assertion `!uv__io_active...` failed

The issue occurs when AbortSignal (including AbortSignal.timeout())
triggers an abort on an in-flight fetch request. This is a Node.js
internal stream handling bug specific to v24.

The fix replaces the AbortSignal-based timeout with Promise.race,
which achieves the same timeout behavior without actually aborting
the underlying request, thus avoiding the Node.js bug.

Local: Node.js v22.21.1 (no crash)
CI: Node.js v24.12.0 (crash with AbortSignal)
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (1)
app/utils/user-info.server.ts (1)

23-36: Node.js v24 fetch abort bug is confirmed; Promise.race approach is appropriate.

The Node.js v24 fetch + AbortSignal crash (libuv assertion failure) is documented and real. Using Promise.race to implement timeouts without AbortController is a valid workaround recommended by the Node.js community.

Note that this approach has an intentional trade-off: when the timeout fires first, the underlying fetch continues executing in the background and completes (or eventually times out at the network level). With aggressive timeouts like the 100ms used here (line 61), this could accumulate background requests.

Consider adding observability around timeout frequency and resource usage to ensure this trade-off remains acceptable in production.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a1dc932 and 9626935.

📒 Files selected for processing (1)
  • app/utils/user-info.server.ts
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: 🎭 Playwright
🔇 Additional comments (1)
app/utils/user-info.server.ts (1)

61-66: Add a comment explaining the intentional 100ms timeout for user-facing Gravatar checks.

The 100ms timeout is an intentional performance optimization, not a bug. The caching strategy (90-day TTL + 365-day staleWhileRevalidate) ensures that timeouts are rare, and the fallback to team-based avatars provides a graceful user experience. However, this design pattern deserves a comment explaining why the much shorter timeout works here compared to the 10s timeout for background/manual refreshes.

Consider adding a comment like:

// 100ms timeout for user-facing requests to avoid blocking.
// Safe because: (1) results cached for 90 days, (2) staleWhileRevalidate
// handles background revalidation, (3) fallback to team avatar is acceptable.
// Background/forceFresh requests use 10s to ensure accuracy.

@kentcdodds
Copy link
Owner Author

BugBot review

Copy link

@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.

✅ Bugbot reviewed your changes and found no bugs!

@kentcdodds kentcdodds merged commit 06dc881 into main Jan 8, 2026
7 checks passed
@kentcdodds kentcdodds deleted the cursor/playwright-test-failures-8683 branch January 8, 2026 17:51
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.

3 participants