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/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); diff --git a/tests/library/tracing.spec.ts b/tests/library/tracing.spec.ts index 184bb34c0d8a0..970aa5436d8d3 100644 --- a/tests/library/tracing.spec.ts +++ b/tests/library/tracing.spec.ts @@ -468,6 +468,23 @@ 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); + }); + // 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(); +}); + 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..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); @@ -156,7 +158,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' && @@ -195,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, },