From a1af2d87fb672bc02b1b1ddfa504610777554bf9 Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Thu, 24 Sep 2026 14:01:22 -0700 Subject: [PATCH 1/4] fix(tracing): do not hang on unresponsive page when capturing snapshots Fixes: https://github.com/microsoft/playwright/issues/42903 --- .../src/server/trace/recorder/tracing.ts | 5 ++++- tests/library/tracing.spec.ts | 16 ++++++++++++++++ utils/eslint-plugin-progress/index.js | 4 +++- 3 files changed, 23 insertions(+), 2 deletions(-) diff --git a/packages/playwright-core/src/server/trace/recorder/tracing.ts b/packages/playwright-core/src/server/trace/recorder/tracing.ts index 67b163b1fe5bf..ed938f231ebc9 100644 --- a/packages/playwright-core/src/server/trace/recorder/tracing.ts +++ b/packages/playwright-core/src/server/trace/recorder/tracing.ts @@ -515,7 +515,10 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps if (this._snapshotter?.started()) { // Node references are only reset by the first snapshot of the action. const resetTargets = phase === 'before'; - await this._snapshotter?.captureSnapshot(page, progress.metadata.id, phase, resetTargets).catch(() => {}); + try { + await progress.race(this._snapshotter.captureSnapshot(page, progress.metadata.id, phase, resetTargets)); + } catch { + } } const options = this._state?.options; diff --git a/tests/library/tracing.spec.ts b/tests/library/tracing.spec.ts index 184bb34c0d8a0..9d86d7259a7a3 100644 --- a/tests/library/tracing.spec.ts +++ b/tests/library/tracing.spec.ts @@ -468,6 +468,22 @@ test('should not crash when browser closes mid-trace', async ({ browserType, ser await new Promise(f => setTimeout(f, 1000)); // Give it some time to throw errors }); +test('should not hang when page is unresponsive', async ({ browserType, server }) => { + test.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/42903' }); + const browser = await browserType.launch(); + const page = await browser.newPage(); + await page.context().tracing.start({ snapshots: true }); + await page.goto(server.EMPTY_PAGE); + await page.evaluate(() => { + setTimeout(() => { + while (true) {} + }, 0); + }); + const error = await page.goto(server.EMPTY_PAGE, { timeout: 1000 }).catch(e => e); + expect(error.message).toContain('Timeout 1000ms exceeded'); + await browser.close(); +}); + test('should survive browser.close with auto-created traces dir', async ({ browserType }, testInfo) => { const oldTracesDir = (browserType as any)._playwright._defaultTracesDir; (browserType as any)._playwright._defaultTracesDir = undefined; diff --git a/utils/eslint-plugin-progress/index.js b/utils/eslint-plugin-progress/index.js index 394ade81b93be..7d7a78df43ed7 100644 --- a/utils/eslint-plugin-progress/index.js +++ b/utils/eslint-plugin-progress/index.js @@ -156,7 +156,9 @@ const rule = createRule({ if (!isInProgressFunction()) return; - const awaited = node.argument; + let awaited = node.argument; + if (awaited.type === 'ChainExpression') + awaited = awaited.expression; // await progress.anything(...) is always fine — calls on the progress object itself. if (awaited.type === 'CallExpression' && From d8b8d3bbff8d29b75018141d34fa6cc9828127eb Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Thu, 24 Sep 2026 15:01:35 -0700 Subject: [PATCH 2/4] chore(eslint): invalidate eslint cache when progress plugin changes --- utils/eslint-plugin-progress/index.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/utils/eslint-plugin-progress/index.js b/utils/eslint-plugin-progress/index.js index 7d7a78df43ed7..7a256116b7b0d 100644 --- a/utils/eslint-plugin-progress/index.js +++ b/utils/eslint-plugin-progress/index.js @@ -16,6 +16,8 @@ // @ts-check +const crypto = require('crypto'); +const fs = require('fs'); const { ESLintUtils } = require('@typescript-eslint/utils'); const createRule = ESLintUtils.RuleCreator(name => name); @@ -197,6 +199,11 @@ const rule = createRule({ }); module.exports = { + meta: { + name: 'eslint-plugin-progress', + // ESLint cache key includes plugin version, so edits to this file invalidate cached results. + version: crypto.createHash('sha1').update(fs.readFileSync(__filename)).digest('hex'), + }, rules: { 'await-must-use-progress': rule, }, From 54327c4ab285e0104e3cb7ba6125c898037a6dda Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Thu, 24 Sep 2026 15:06:46 -0700 Subject: [PATCH 3/4] chore(trace-viewer): fix exhaustive-deps warning in workbench --- packages/trace-viewer/src/ui/workbench.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/trace-viewer/src/ui/workbench.tsx b/packages/trace-viewer/src/ui/workbench.tsx index 12c47c5590c73..685ab431fc77a 100644 --- a/packages/trace-viewer/src/ui/workbench.tsx +++ b/packages/trace-viewer/src/ui/workbench.tsx @@ -168,6 +168,7 @@ const PartitionedWorkbench: React.FunctionComponent { setSelectedPropertiesTab(tab); @@ -180,9 +181,9 @@ const PartitionedWorkbench: React.FunctionComponent { setHighlightedElement(element); From a7c5f545f852f012dbb6284da2331076af595cc0 Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Thu, 24 Sep 2026 15:24:48 -0700 Subject: [PATCH 4/4] test(tracing): clarify unresponsive page test intent --- tests/library/tracing.spec.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/library/tracing.spec.ts b/tests/library/tracing.spec.ts index 9d86d7259a7a3..970aa5436d8d3 100644 --- a/tests/library/tracing.spec.ts +++ b/tests/library/tracing.spec.ts @@ -479,6 +479,7 @@ test('should not hang when page is unresponsive', async ({ browserType, server } while (true) {} }, 0); }); + // All bets are off with a tight loop in the page, but an explicit timeout should still fire. const error = await page.goto(server.EMPTY_PAGE, { timeout: 1000 }).catch(e => e); expect(error.message).toContain('Timeout 1000ms exceeded'); await browser.close();