Skip to content

internal(browser): Add waitForTimeout to editor and debugger#1169

Merged
allansson merged 6 commits intomainfrom
feat/add-wait-for-timeout
Apr 16, 2026
Merged

internal(browser): Add waitForTimeout to editor and debugger#1169
allansson merged 6 commits intomainfrom
feat/add-wait-for-timeout

Conversation

@allansson
Copy link
Copy Markdown
Collaborator

@allansson allansson commented Apr 14, 2026

Description

This PR adds support for the page.waitForTimeout action to both the test editor and debugger.

When implementing session replay, I noticed that tests with only a goto action would finish before anything meaningful is displayed. The feedback loop is supposed to be: run test -> check replay -> add next action. But if nothing is displayed you have no idea what to do next.

So the idea is to add a timeout at the end of the test to make sure that content is loaded before the test ends.

Since I was going to have to implement all the codegen and everything I figured I'd just make it an editable action.

How to Test

  1. Open a browser test
  2. Add a "Wait for timeout" action
  3. Check the script
    • It should have a page.waitForTimeout
  4. Export the script
  5. Run the script in the debugger
    • The timeout action should be recorded

Checklist

  • I have performed a self-review of my code.
  • I have added tests for my changes.
  • I have commented on my code, particularly in hard-to-understand areas.

Related PR(s)/Issue(s)


Note

Medium Risk
Touches multiple layers (action schema parsing, runtime action tracking, and script codegen), so mismatches could break recording/replay or generated scripts. NaN/null coercion for timeout is subtle and may affect persisted action data if not handled consistently.

Overview
Adds end-to-end support for the page.waitForTimeout browser action so it can be recorded, edited, validated, and code-generated.

This introduces a new wait-for-timeout node in the browser test model/IR and emits await page.waitForTimeout(<ms>) in generated scripts (with a new snapshot test). The runner schema and page proxy now recognize/track the action, including special handling to persist invalid/empty timeouts via NaN.

The Browser Test Editor UI gains a “Wait for timeout” action with a dedicated TimeoutForm, registry entry, and display text in the validator.

Reviewed by Cursor Bugbot for commit 3c6a3c1. Bugbot is set up for automated code reviews on this repo. Configure here.

Copy link
Copy Markdown
Collaborator Author

@allansson allansson left a comment

Choose a reason for hiding this comment

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

Self-review

case 'number':
if (isNaN(value)) {
return identifier('NaN')
}
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Without this, the NaN values would be rendered as nan for some reason (maybe prettier does a toLowerCase on number literals?)

>
Wait for element
</DropdownMenu.Item>
<DropdownMenu.Item
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Thoughtfully placed after "Wait for element" because it should be your default.

@allansson
Copy link
Copy Markdown
Collaborator Author

bugbot run

Copy link
Copy Markdown
Contributor

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

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: NaN timeout can be saved, breaking file reload
    • Added NaN check in fromBrowserActionInstance to coerce NaN timeout values to 0 before serialization, preventing null in saved files.

Create PR

Or push these changes by commenting:

@cursor push 5b7b05f8d2
Preview (5b7b05f8d2)
diff --git a/package-lock.json b/package-lock.json
--- a/package-lock.json
+++ b/package-lock.json
@@ -317,6 +317,7 @@
       "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz",
       "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
       "license": "MIT",
+      "peer": true,
       "dependencies": {
         "@babel/code-frame": "^7.29.0",
         "@babel/generator": "^7.29.0",
@@ -625,6 +626,7 @@
       "resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz",
       "integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==",
       "license": "MIT",
+      "peer": true,
       "dependencies": {
         "@dnd-kit/accessibility": "^3.1.1",
         "@dnd-kit/utilities": "^3.2.2",
@@ -1150,6 +1152,7 @@
       "integrity": "sha512-zx0EIq78WlY/lBb1uXlziZmDZI4ubcCXIMJ4uGjXzZW0nS19TjSPeXPAjzzTmKQlJUZm0SbmZhPKP7tuQ1SsEw==",
       "dev": true,
       "license": "MIT",
+      "peer": true,
       "dependencies": {
         "chalk": "^4.1.1",
         "fs-extra": "^9.0.1",
@@ -1592,6 +1595,7 @@
       "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz",
       "integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==",
       "license": "MIT",
+      "peer": true,
       "dependencies": {
         "@babel/runtime": "^7.18.3",
         "@emotion/babel-plugin": "^11.13.5",
@@ -2554,6 +2558,7 @@
       "integrity": "sha512-yl43JD/86CIj3Mz5mvvLJqAOfIup7ncxfJ0Btnl0/v5TouVUyeEdcpknfgc+yMevS/48oH9WAkkw93m7otLb/A==",
       "dev": true,
       "license": "MIT",
+      "peer": true,
       "dependencies": {
         "@inquirer/checkbox": "^3.0.1",
         "@inquirer/confirm": "^4.0.1",
@@ -2891,6 +2896,7 @@
       "integrity": "sha512-/g2d4sW9nUDJOMz3mabVQvOGhVa4e/BN/Um7yca9Bb2XTzPPnfTWHWQg+IsEYO7M3Vx+EXvaM/I2pJWIMun1bg==",
       "dev": true,
       "license": "MIT",
+      "peer": true,
       "dependencies": {
         "@octokit/auth-token": "^4.0.0",
         "@octokit/graphql": "^7.1.0",
@@ -3201,6 +3207,7 @@
       "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz",
       "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==",
       "license": "Apache-2.0",
+      "peer": true,
       "engines": {
         "node": ">=8.0.0"
       }
@@ -3222,6 +3229,7 @@
       "resolved": "https://registry.npmjs.org/@opentelemetry/context-async-hooks/-/context-async-hooks-1.30.1.tgz",
       "integrity": "sha512-s5vvxXPVdjqS3kTLKMeBMvop9hbWkwzBpu+mUO2M7sZtlkyDJGwFe33wRKnbaYDo8ExRVBIIdwIGrqpxHuKttA==",
       "license": "Apache-2.0",
+      "peer": true,
       "engines": {
         "node": ">=14"
       },
@@ -3258,6 +3266,7 @@
       "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.57.2.tgz",
       "integrity": "sha512-BdBGhQBh8IjZ2oIIX6F2/Q3LKm/FDDKi6ccYKcBTeilh6SNdNKveDOLk73BkSJjQLJk6qe4Yh+hHw1UPhCDdrg==",
       "license": "Apache-2.0",
+      "peer": true,
       "dependencies": {
         "@opentelemetry/api-logs": "0.57.2",
         "@types/shimmer": "^1.2.0",
@@ -3788,6 +3797,7 @@
       "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.31.0.tgz",
       "integrity": "sha512-cYJeP+6qN0UnBv1r09hXl0YorB8kXHv61BC0NUlBA8vxrylZ4/C8lnva3gd1E8n33DNYSaiGW+DuGoSt0QQ7Dw==",
       "license": "Apache-2.0",
+      "peer": true,
       "engines": {
         "node": ">=14"
       }
@@ -6911,6 +6921,7 @@
       "resolved": "https://registry.npmjs.org/@types/node/-/node-22.18.0.tgz",
       "integrity": "sha512-m5ObIqwsUp6BZzyiy4RdZpzWGub9bqLJMvZDD0QMXhxjqMHMENlj+SqF5QxoUwaQNFe+8kz8XM8ZQhqkQPTgMQ==",
       "license": "MIT",
+      "peer": true,
       "dependencies": {
         "undici-types": "~6.21.0"
       }
@@ -6987,6 +6998,7 @@
       "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.10.tgz",
       "integrity": "sha512-WPigyYuGhgZ/cTPRXB2EwUw+XvsRA3GqHlsP4qteqrnnjDrApbS7MxcGr/hke5iUoeB7E/gQtrs9I37zAJ0Vjw==",
       "license": "MIT",
+      "peer": true,
       "dependencies": {
         "csstype": "^3.2.2"
       }
@@ -6997,6 +7009,7 @@
       "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
       "devOptional": true,
       "license": "MIT",
+      "peer": true,
       "peerDependencies": {
         "@types/react": "^19.2.0"
       }
@@ -7094,6 +7107,7 @@
       "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.16.1.tgz",
       "integrity": "sha512-SxdPak/5bO0EnGktV05+Hq8oatjAYVY3Zh2bye9pGZy6+jwyR3LG3YKkV4YatlsgqXP28BTeVm9pqwJM96vf2A==",
       "dev": true,
+      "peer": true,
       "dependencies": {
         "@eslint-community/regexpp": "^4.10.0",
         "@typescript-eslint/scope-manager": "7.16.1",
@@ -7962,6 +7976,7 @@
       "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
       "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
       "license": "MIT",
+      "peer": true,
       "bin": {
         "acorn": "bin/acorn"
       },
@@ -9016,6 +9031,7 @@
         }
       ],
       "license": "MIT",
+      "peer": true,
       "dependencies": {
         "baseline-browser-mapping": "^2.9.0",
         "caniuse-lite": "^1.0.30001759",
@@ -10869,16 +10885,6 @@
         "node": ">= 0.8"
       }
     },
-    "node_modules/encoding": {
-      "version": "0.1.13",
-      "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz",
-      "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==",
-      "license": "MIT",
-      "optional": true,
-      "dependencies": {
-        "iconv-lite": "^0.6.2"
-      }
-    },
     "node_modules/end-of-stream": {
       "version": "1.4.4",
       "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
@@ -11217,6 +11223,7 @@
       "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz",
       "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==",
       "dev": true,
+      "peer": true,
       "dependencies": {
         "@eslint-community/eslint-utils": "^4.2.0",
         "@eslint-community/regexpp": "^4.6.1",
@@ -11355,6 +11362,7 @@
       "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.29.1.tgz",
       "integrity": "sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw==",
       "dev": true,
+      "peer": true,
       "dependencies": {
         "array-includes": "^3.1.7",
         "array.prototype.findlastindex": "^1.2.3",
@@ -13068,6 +13076,7 @@
       "version": "10.1.1",
       "resolved": "https://registry.npmjs.org/immer/-/immer-10.1.1.tgz",
       "integrity": "sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==",
+      "peer": true,
       "funding": {
         "type": "opencollective",
         "url": "https://opencollective.com/immer"
@@ -14988,6 +14997,7 @@
       "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.55.1.tgz",
       "integrity": "sha512-jz4x+TJNFHwHtwuV9vA9rMujcZRb0CEilTEwG2rRSpe/A7Jdkuj8xPKttCgOh+v/lkHy7HsZ64oj+q3xoAFl9A==",
       "license": "MIT",
+      "peer": true,
       "dependencies": {
         "dompurify": "3.2.7",
         "marked": "14.0.0"
@@ -16557,6 +16567,7 @@
       "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz",
       "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==",
       "license": "MIT",
+      "peer": true,
       "engines": {
         "node": ">=0.10.0"
       }
@@ -16581,6 +16592,7 @@
       "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz",
       "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==",
       "license": "MIT",
+      "peer": true,
       "dependencies": {
         "scheduler": "^0.27.0"
       },
@@ -16592,6 +16604,7 @@
       "version": "7.53.0",
       "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.53.0.tgz",
       "integrity": "sha512-M1n3HhqCww6S2hxLxciEXy2oISPnAzxY7gvwVPrtlczTM/1dDadXgUxDpHMrMTblDOcm/AXtXxHwZ3jpg1mqKQ==",
+      "peer": true,
       "engines": {
         "node": ">=18.0.0"
       },
@@ -17511,6 +17524,7 @@
       "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
       "dev": true,
       "license": "MIT",
+      "peer": true,
       "dependencies": {
         "fast-deep-equal": "^3.1.3",
         "fast-uri": "^3.0.1",
@@ -18678,6 +18692,7 @@
       "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
       "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
       "license": "MIT",
+      "peer": true,
       "engines": {
         "node": ">=12"
       },
@@ -18932,7 +18947,8 @@
     "node_modules/tslib": {
       "version": "2.6.3",
       "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz",
-      "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ=="
+      "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==",
+      "peer": true
     },
     "node_modules/type-check": {
       "version": "0.4.0",
@@ -19070,6 +19086,7 @@
       "version": "5.5.3",
       "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.3.tgz",
       "integrity": "sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ==",
+      "peer": true,
       "bin": {
         "tsc": "bin/tsc",
         "tsserver": "bin/tsserver"
@@ -19602,6 +19619,7 @@
       "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.2.tgz",
       "integrity": "sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ==",
       "license": "MIT",
+      "peer": true,
       "dependencies": {
         "esbuild": "^0.25.0",
         "fdir": "^6.4.4",
@@ -19712,6 +19730,7 @@
       "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
       "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
       "license": "MIT",
+      "peer": true,
       "engines": {
         "node": ">=12"
       },
@@ -20347,6 +20366,7 @@
       "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
       "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
       "license": "MIT",
+      "peer": true,
       "funding": {
         "url": "https://github.com/sponsors/colinhacks"
       }

diff --git a/src/views/BrowserTestEditor/actionAdapters.ts b/src/views/BrowserTestEditor/actionAdapters.ts
--- a/src/views/BrowserTestEditor/actionAdapters.ts
+++ b/src/views/BrowserTestEditor/actionAdapters.ts
@@ -42,5 +42,12 @@
     }
   }
 
+  if (action.method === 'page.waitForTimeout' && isNaN(action.timeout)) {
+    return {
+      ...action,
+      timeout: 0,
+    }
+  }
+
   return action
 }

You can send follow-ups to the cloud agent here.

Comment thread src/views/BrowserTestEditor/ActionForms/forms/TimeoutForm.tsx
@allansson
Copy link
Copy Markdown
Collaborator Author

bugbot run

Copy link
Copy Markdown
Contributor

@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 new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit de1dfa2. Configure here.

Copy link
Copy Markdown
Collaborator Author

@allansson allansson left a comment

Choose a reason for hiding this comment

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

Self-review

Comment thread src/main/runner/schema.ts
Comment on lines +102 to +108
// NaN is converted to null by `JSON.stringify` so we type this
// as nullable and transform it back to NaN to allow invalid data
// to be saved.
timeout: z
.number()
.nullable()
.transform((value) => value ?? NaN),
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

This is up for debate. How do we want to handle invalid data and saving? Should we disable saving or make the schema tolerant to errors?

Personally, I would make the schema tolerant because it allows you to save unfinished work. It's more complex though than just disabling the save button.

WDYT?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I have a similar opinion - as long as we clearly communicate which values/actions are invalid, the use should be able to save/open invalid tests. This is already the case when you have an invalid URL in page.goto and in a few other places, though I didn't pay much attention to the schema - maybe it should have been adjusted as well.

@allansson allansson marked this pull request as ready for review April 15, 2026 06:36
@allansson allansson requested a review from a team as a code owner April 15, 2026 06:36
going-confetti
going-confetti previously approved these changes Apr 16, 2026
Copy link
Copy Markdown
Collaborator

@going-confetti going-confetti left a comment

Choose a reason for hiding this comment

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

LGTM!

Comment thread src/main/runner/schema.ts
Comment on lines +102 to +108
// NaN is converted to null by `JSON.stringify` so we type this
// as nullable and transform it back to NaN to allow invalid data
// to be saved.
timeout: z
.number()
.nullable()
.transform((value) => value ?? NaN),
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I have a similar opinion - as long as we clearly communicate which values/actions are invalid, the use should be able to save/open invalid tests. This is already the case when you have an invalid URL in page.goto and in a few other places, though I didn't pay much attention to the schema - maybe it should have been adjusted as well.

Comment on lines +51 to +52
value={isNaN(timeout) ? '' : timeout}
onChange={(e) => {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I'd add min={0} to the props.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

For some reason I can't comment on the first comment you made, but I think this is where you want two set of types: one for the document schema, which is fault tolerant, and another one that's a validated and strictly typed version.

And this is not the same as the Prefix‍TestActionSchema Prefix‍RunnerActionSchema.

@allansson allansson merged commit 96383c9 into main Apr 16, 2026
13 checks passed
@allansson allansson deleted the feat/add-wait-for-timeout branch April 16, 2026 10:02
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