From 5d1ef7de9944fcad34c90afede9c8b645fdec9bf Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Tue, 30 Jun 2026 19:53:19 +0100 Subject: [PATCH 1/3] chore: consolidate more stack trace utils into stackTrace.ts (#41542) --- packages/isomorphic/stackTrace.ts | 37 ++++++++++++++----- .../src/client/clientStackTrace.ts | 11 ++---- packages/playwright/src/common/fixtures.ts | 3 +- packages/playwright/src/common/process.ts | 3 ++ packages/playwright/src/index.ts | 4 -- packages/playwright/src/matchers/expect.ts | 8 ++-- packages/playwright/src/program.ts | 4 +- packages/playwright/src/reporters/base.ts | 2 +- packages/playwright/src/util.ts | 33 ++--------------- .../playwright/src/worker/fixtureRunner.ts | 3 +- packages/playwright/src/worker/testInfo.ts | 6 +-- packages/playwright/src/worker/testTracing.ts | 5 +-- packages/playwright/src/worker/workerMain.ts | 3 +- 13 files changed, 55 insertions(+), 67 deletions(-) diff --git a/packages/isomorphic/stackTrace.ts b/packages/isomorphic/stackTrace.ts index f6c906e0613bf..a973a0a465573 100644 --- a/packages/isomorphic/stackTrace.ts +++ b/packages/isomorphic/stackTrace.ts @@ -37,7 +37,7 @@ export function captureRawStack(): RawStack { return stack.split('\n'); } -export function parseStackFrame(text: string, pathSeparator: string, showInternalStackFrames: boolean): StackFrame | null { +export function parseStackFrame(text: string, pathSeparator: string): StackFrame | null { const match = text && text.match(re); if (!match) return null; @@ -46,7 +46,7 @@ export function parseStackFrame(text: string, pathSeparator: string, showInterna let file = match[7]; if (!file) return null; - if (!showInternalStackFrames && (file.startsWith('internal') || file.startsWith('node:'))) + if (!_showInternalStackFrames && (file.startsWith('internal') || file.startsWith('node:'))) return null; const line = match[8]; @@ -134,7 +134,7 @@ export function splitErrorMessage(message: string): { name: string, message: str }; } -export function parseErrorStack(stack: string, pathSeparator: string, showInternalStackFrames: boolean = false): { +export function parseErrorStack(stack: string, pathSeparator: string): { message: string; stackLines: string[]; location?: StackFrame; @@ -147,7 +147,7 @@ export function parseErrorStack(stack: string, pathSeparator: string, showIntern const stackLines = lines.slice(firstStackLine); let location: StackFrame | undefined; for (const line of stackLines) { - const frame = parseStackFrame(line, pathSeparator, showInternalStackFrames); + const frame = parseStackFrame(line, pathSeparator); if (!frame || !frame.file) continue; if (belongsToNodeModules(frame.file, pathSeparator)) @@ -162,6 +162,29 @@ function belongsToNodeModules(file: string, pathSeparator: string) { return file.includes(`${pathSeparator}node_modules${pathSeparator}`); } +export function filterStackFile(file: string) { + if (_showInternalStackFrames) + return true; + if (!!_coreDir && file.startsWith(_coreDir)) + return false; + if (_boxedStackPrefixes.some(prefix => file.startsWith(prefix))) + return false; + return true; +} + +export function filteredStackTrace(rawStack: RawStack, pathSeparator: string): StackFrame[] { + const frames: StackFrame[] = []; + for (const line of rawStack) { + const frame = parseStackFrame(line, pathSeparator); + if (!frame || !frame.file) + continue; + if (!filterStackFile(frame.file)) + continue; + frames.push(frame); + } + return frames; +} + const re = new RegExp('^' + // Sometimes we strip out the ' at' because it's noisy '(?:\\s*at )?' + @@ -216,12 +239,6 @@ export function setBoxedStackPrefixes(prefixes: string[]) { _boxedStackPrefixes = prefixes; } -export function boxedStackPrefixes(): string[] { - if (_showInternalStackFrames) - return []; - return _coreDir ? [_coreDir, ..._boxedStackPrefixes] : _boxedStackPrefixes.slice(); -} - export function setShowInternalStackFrames(value: boolean) { _showInternalStackFrames = value; } diff --git a/packages/playwright-core/src/client/clientStackTrace.ts b/packages/playwright-core/src/client/clientStackTrace.ts index 3f2e8fcd73219..7e55614ec1c95 100644 --- a/packages/playwright-core/src/client/clientStackTrace.ts +++ b/packages/playwright-core/src/client/clientStackTrace.ts @@ -16,7 +16,7 @@ import path from 'path'; -import { boxedStackPrefixes, captureRawStack, coreDir, parseStackFrame, showInternalStackFrames } from '@isomorphic/stackTrace'; +import { captureRawStack, coreDir, filterStackFile, parseStackFrame } from '@isomorphic/stackTrace'; import type { StackFrame } from '@isomorphic/stackTrace'; @@ -30,7 +30,7 @@ export function captureLibraryStackTrace(): { frames: StackFrame[], apiName: str isPlaywrightLibrary: boolean; }; let parsedFrames = stack.map(line => { - const frame = parseStackFrame(line, path.sep, showInternalStackFrames()); + const frame = parseStackFrame(line, path.sep); if (!frame || !frame.file) return null; const isPlaywrightLibrary = !!playwrightCoreDir && frame.file.startsWith(playwrightCoreDir); @@ -65,12 +65,7 @@ export function captureLibraryStackTrace(): { frames: StackFrame[], apiName: str } // This is for the inspector so that it did not include the test runner stack frames. - const filterPrefixes = boxedStackPrefixes(); - parsedFrames = parsedFrames.filter(f => { - if (filterPrefixes.some(prefix => f.frame.file.startsWith(prefix))) - return false; - return true; - }); + parsedFrames = parsedFrames.filter(f => filterStackFile(f.frame.file)); return { frames: parsedFrames.map(p => p.frame), diff --git a/packages/playwright/src/common/fixtures.ts b/packages/playwright/src/common/fixtures.ts index b47c3f7d6a01c..64c95426fbaba 100644 --- a/packages/playwright/src/common/fixtures.ts +++ b/packages/playwright/src/common/fixtures.ts @@ -15,8 +15,9 @@ */ import crypto from 'crypto'; +import { filterStackFile } from '@isomorphic/stackTrace'; -import { filterStackFile, formatLocation } from '../util'; +import { formatLocation } from '../util'; import type { FixturesWithLocation } from './config'; import type { Fixtures } from '../../types/test'; diff --git a/packages/playwright/src/common/process.ts b/packages/playwright/src/common/process.ts index 49ab3805d5cb3..ce722bdb234be 100644 --- a/packages/playwright/src/common/process.ts +++ b/packages/playwright/src/common/process.ts @@ -19,7 +19,9 @@ import 'playwright-core/lib/bootstrap'; import { ManualPromise } from '@isomorphic/manualPromise'; import { setTimeOrigin } from '@isomorphic/time'; import { startProfiling, stopProfiling } from '@utils/profiler'; +import { setBoxedStackPrefixes } from '@isomorphic/stackTrace'; +import { packageRoot } from '../package'; import { serializeError } from '../util'; import type { EnvProducedPayload, ProcessInitParams, TestInfoErrorPayload } from './ipc'; @@ -61,6 +63,7 @@ let forceExitInitiated = false; let processRunner: ProcessRunner | undefined; let processName: string | undefined; const startingEnv = { ...process.env }; +setBoxedStackPrefixes([packageRoot]); export function startProcessRunner(create: (params: any) => ProcessRunner) { sendMessageToParent({ method: 'ready' }); diff --git a/packages/playwright/src/index.ts b/packages/playwright/src/index.ts index c53f7a29e284f..d9be54325effd 100644 --- a/packages/playwright/src/index.ts +++ b/packages/playwright/src/index.ts @@ -20,7 +20,6 @@ import path from 'path'; import * as playwrightLibrary from 'playwright-core'; import { asLocatorDescription } from '@isomorphic/locatorGenerators'; import { getActionGroup, renderTitleForCall } from '@isomorphic/protocolFormatter'; -import { setBoxedStackPrefixes } from '@isomorphic/stackTrace'; import { escapeHTML } from '@isomorphic/stringUtils'; import { jsonStringifyForceASCII } from '@utils/ascii'; import { createGuid } from '@utils/crypto'; @@ -29,7 +28,6 @@ import { currentZone } from '@utils/zones'; import { buildErrorContext } from './errorContext'; import { config, testType } from './common'; import * as globals from './globals'; -import { packageRoot } from './package'; import { createCustomMessageHandler, runDaemonForContext } from './mcp/test/browserBackend'; import type { Fixtures, PlaywrightTestArgs, PlaywrightTestOptions, PlaywrightWorkerArgs, PlaywrightWorkerOptions, ScreenshotMode, TestInfo, TestType, VideoMode } from '../types/test'; @@ -46,8 +44,6 @@ import type { BrowserContext, BrowserContextOptions, LaunchOptions, Page, Tracin export { expect } from './matchers/expect'; export const _baseTest: TestType<{}, {}> = testType.rootTestType.test; -setBoxedStackPrefixes([packageRoot]); - if ((process as any)['__pw_initiator__']) { const originalStackTraceLimit = Error.stackTraceLimit; Error.stackTraceLimit = 200; diff --git a/packages/playwright/src/matchers/expect.ts b/packages/playwright/src/matchers/expect.ts index 412196154e0bf..79255f2227b4c 100644 --- a/packages/playwright/src/matchers/expect.ts +++ b/packages/playwright/src/matchers/expect.ts @@ -115,7 +115,7 @@ export interface ExpectTestInfo { export type ExpectConfig = { testInfo: ExpectTestInfo | null; - filteredStackTrace: (rawStack: string[]) => StackFrame[]; + filteredStackTrace: (rawStack: string[], pathSeparator: string) => StackFrame[]; ignoreSnapshots: boolean; updateSnapshots: 'all' | 'changed' | 'missing' | 'none'; timeout?: number; @@ -143,8 +143,8 @@ export type ExpectConfig = { toPass?: { timeout?: number; intervals?: number[] }; }; -function unfilteredStackTrace(rawStack: string[]): StackFrame[] { - return rawStack.map(frame => parseStackFrame(frame, path.sep, !!process.env.PWDEBUGIMPL)).filter(f => !!f); +function unfilteredStackTrace(rawStack: string[], pathSeparator: string): StackFrame[] { + return rawStack.map(frame => parseStackFrame(frame, pathSeparator)).filter(f => !!f); } let _expectConfig: ExpectConfig = { testInfo: null, filteredStackTrace: unfilteredStackTrace, ignoreSnapshots: false, updateSnapshots: 'missing' }; @@ -339,7 +339,7 @@ function callMatcherAsStep(matcherName: string, info: ExpectMetaInfo, actual: un // This looks like it is unnecessary, but it isn't - we need to filter // out all the frames that belong to the test runner from caught runtime errors. - const stackFrames = expectConfig().filteredStackTrace(captureRawStack()); + const stackFrames = expectConfig().filteredStackTrace(captureRawStack(), path.sep); const stepData = { category: 'expect' as const, apiName, diff --git a/packages/playwright/src/program.ts b/packages/playwright/src/program.ts index 242f7eee8b7e5..109aa1e54ae30 100644 --- a/packages/playwright/src/program.ts +++ b/packages/playwright/src/program.ts @@ -20,19 +20,21 @@ import 'playwright-core/lib/bootstrap'; import { libCli, tools } from 'playwright-core/lib/coreBundle'; import { program } from 'commander'; +import { setBoxedStackPrefixes } from '@isomorphic/stackTrace'; import { gracefullyProcessExitDoNotHang } from '@utils/processLauncher'; import { builtInReporters, config, configLoader } from './common'; import { runTests, clearCache, runTestServerAction } from './cli/testActions'; import { showReport, mergeReports } from './cli/reportActions'; import { TestServerBackend, testServerBackendTools } from './mcp/test/testBackend'; import { ClaudeGenerator, CodexGenerator, OpencodeGenerator, VSCodeGenerator, CopilotGenerator } from './agents/generateAgents'; -import { packageJSON } from './package'; +import { packageRoot, packageJSON } from './package'; export { program }; import type { TraceMode } from '../types/test'; import type { Command } from 'commander'; +setBoxedStackPrefixes([packageRoot]); libCli.decorateProgram(program); function addTestCommand(program: Command) { diff --git a/packages/playwright/src/reporters/base.ts b/packages/playwright/src/reporters/base.ts index e1d0737af599b..cc1886d7f98fc 100644 --- a/packages/playwright/src/reporters/base.ts +++ b/packages/playwright/src/reporters/base.ts @@ -631,7 +631,7 @@ export function prepareErrorStack(stack: string): { stackLines: string[]; location?: Location; } { - return parseErrorStack(stack, path.sep, !!process.env.PWDEBUGIMPL); + return parseErrorStack(stack, path.sep); } function resolveFromEnv(name: string): string | undefined { diff --git a/packages/playwright/src/util.ts b/packages/playwright/src/util.ts index bac08b1eaa43f..6f3cf17348995 100644 --- a/packages/playwright/src/util.ts +++ b/packages/playwright/src/util.ts @@ -24,24 +24,20 @@ import { minimatch } from 'minimatch'; import { calculateSha1 } from '@utils/crypto'; import { sanitizeForFilePath } from '@utils/fileUtils'; import { isRegExp } from '@isomorphic/rtti'; -import { parseStackFrame, stringifyStackFrames } from '@isomorphic/stackTrace'; +import { stringifyStackFrames, filteredStackTrace } from '@isomorphic/stackTrace'; import { ansiRegex, isString, stripAnsiEscapes } from '@isomorphic/stringUtils'; -import type { RawStack, StackFrame } from '@isomorphic/stackTrace'; import type { Location } from './../types/testReporter'; import type { TestInfoError } from './../types/test'; import type { TestCase } from './common/test'; -const PLAYWRIGHT_TEST_PATH = path.join(__dirname, '..'); -const PLAYWRIGHT_CORE_PATH = path.dirname(require.resolve('playwright-core/package.json')); - -export function filterStackTrace(e: Error): { message: string, stack: string, cause?: ReturnType } { +export function filterStackTrace(e: Error): TestInfoError { const name = e.name ? e.name + ': ' : ''; const cause = e.cause instanceof Error ? filterStackTrace(e.cause) : undefined; if (process.env.PWDEBUGIMPL) return { message: name + e.message, stack: e.stack || '', cause }; - const stackLines = stringifyStackFrames(filteredStackTrace(e.stack?.split('\n') || [])); + const stackLines = stringifyStackFrames(filteredStackTrace(e.stack?.split('\n') || [], path.sep)); return { message: name + e.message, stack: `${name}${e.message}${stackLines.map(line => '\n' + line).join('')}`, @@ -49,29 +45,6 @@ export function filterStackTrace(e: Error): { message: string, stack: string, ca }; } -export function filterStackFile(file: string) { - if (process.env.PWDEBUGIMPL) - return true; - if (file.startsWith(PLAYWRIGHT_TEST_PATH)) - return false; - if (file.startsWith(PLAYWRIGHT_CORE_PATH)) - return false; - return true; -} - -export function filteredStackTrace(rawStack: RawStack): StackFrame[] { - const frames: StackFrame[] = []; - for (const line of rawStack) { - const frame = parseStackFrame(line, path.sep, !!process.env.PWDEBUGIMPL); - if (!frame || !frame.file) - continue; - if (!filterStackFile(frame.file)) - continue; - frames.push(frame); - } - return frames; -} - export function serializeError(error: Error | any): TestInfoError { if (error instanceof Error) return filterStackTrace(error); diff --git a/packages/playwright/src/worker/fixtureRunner.ts b/packages/playwright/src/worker/fixtureRunner.ts index acba16fa278d2..b13fd9feaf499 100644 --- a/packages/playwright/src/worker/fixtureRunner.ts +++ b/packages/playwright/src/worker/fixtureRunner.ts @@ -16,9 +16,10 @@ import { ManualPromise } from '@isomorphic/manualPromise'; import { escapeWithQuotes } from '@isomorphic/stringUtils'; +import { filterStackFile } from '@isomorphic/stackTrace'; import { fixtures } from '../common'; -import { filterStackFile, formatLocation } from '../util'; +import { formatLocation } from '../util'; import type { TestInfoImpl } from './testInfo'; import type { FixtureDescription, RunnableDescription } from './timeoutManager'; diff --git a/packages/playwright/src/worker/testInfo.ts b/packages/playwright/src/worker/testInfo.ts index 58ee006bee26a..546c63cfd9a2a 100644 --- a/packages/playwright/src/worker/testInfo.ts +++ b/packages/playwright/src/worker/testInfo.ts @@ -18,7 +18,7 @@ import fs from 'fs'; import path from 'path'; import { ManualPromise } from '@isomorphic/manualPromise'; -import { captureRawStack, stringifyStackFrames } from '@isomorphic/stackTrace'; +import { captureRawStack, stringifyStackFrames, filteredStackTrace } from '@isomorphic/stackTrace'; import { escapeWithQuotes } from '@isomorphic/stringUtils'; import { monotonicTime } from '@isomorphic/time'; import { createGuid } from '@utils/crypto'; @@ -26,7 +26,7 @@ import { sanitizeForFilePath, trimLongString } from '@utils/fileUtils'; import { currentZone } from '@utils/zones'; import { TimeoutManager, TimeoutManagerError } from './timeoutManager'; -import { addSuffixToFilePath, filteredStackTrace, getContainedPath, normalizeAndSaveAttachment, sanitizeFilePathBeforeExtension, windowsFilesystemFriendlyLength } from '../util'; +import { addSuffixToFilePath, getContainedPath, normalizeAndSaveAttachment, sanitizeFilePathBeforeExtension, windowsFilesystemFriendlyLength } from '../util'; import { TestTracing } from './testTracing'; import { testInfoError } from './util'; import { ipc, transform } from '../common'; @@ -295,7 +295,7 @@ export class TestInfoImpl implements TestInfo { parentStep = this._parentStep(); } - const filteredStack = filteredStackTrace(captureRawStack()); + const filteredStack = filteredStackTrace(captureRawStack(), path.sep); let boxedStack = parentStep?.boxedStack; let location = data.location; if (!boxedStack && data.box) { diff --git a/packages/playwright/src/worker/testTracing.ts b/packages/playwright/src/worker/testTracing.ts index d6373496f55ba..049eae8bfbfd9 100644 --- a/packages/playwright/src/worker/testTracing.ts +++ b/packages/playwright/src/worker/testTracing.ts @@ -24,8 +24,7 @@ import { monotonicTime } from '@isomorphic/time'; import { calculateSha1, createGuid } from '@utils/crypto'; import { SerializedFS } from '@utils/serializedFS'; import { getPlaywrightVersion } from 'playwright-core/lib/coreBundle'; - -import { filteredStackTrace } from '../util'; +import { filteredStackTrace } from '@isomorphic/stackTrace'; import type { TestStepCategory, TestInfoImpl } from './testInfo'; import type { PlaywrightWorkerOptions, TestInfo, TestInfoError, TraceMode } from '../../types/test'; @@ -251,7 +250,7 @@ export class TestTracing { appendForError(error: TestInfoError) { const rawStack = error.stack?.split('\n') || []; - const stack = rawStack ? filteredStackTrace(rawStack) : []; + const stack = rawStack ? filteredStackTrace(rawStack, path.sep) : []; this._appendTraceEvent({ type: 'error', message: this._formatError(error), diff --git a/packages/playwright/src/worker/workerMain.ts b/packages/playwright/src/worker/workerMain.ts index 22e8f46451639..a566e37f217b9 100644 --- a/packages/playwright/src/worker/workerMain.ts +++ b/packages/playwright/src/worker/workerMain.ts @@ -18,11 +18,12 @@ import colors from 'colors/safe'; import { ManualPromise } from '@isomorphic/manualPromise'; import { removeFolders } from '@utils/fileUtils'; import { gracefullyCloseAll } from '@utils/processLauncher'; +import { filteredStackTrace } from '@isomorphic/stackTrace'; import { configLoader, fixtures, ipc, poolBuilder, ProcessRunner, suiteUtils, testLoader } from '../common'; import * as globals from '../globals'; import { setExpectConfig } from '../matchers/expect'; -import { debugTest, filteredStackTrace, relativeFilePath } from '../util'; +import { debugTest, relativeFilePath } from '../util'; import { FixtureRunner } from './fixtureRunner'; import { TestSkipError, TestInfoImpl, emtpyTestInfoCallbacks } from './testInfo'; import { testInfoError } from './util'; From 07b5acd9e4cf5c3bfd0839442c431720514c1014 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Tue, 30 Jun 2026 15:37:47 -0700 Subject: [PATCH 2/3] fix(cli): don't garbage-collect browsers during CLI daemon install (#41553) --- packages/playwright-core/src/server/registry/index.ts | 4 ++-- packages/playwright-core/src/tools/cli-daemon/program.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/playwright-core/src/server/registry/index.ts b/packages/playwright-core/src/server/registry/index.ts index 55b59d722fdd5..6cd606894ee2a 100644 --- a/packages/playwright-core/src/server/registry/index.ts +++ b/packages/playwright-core/src/server/registry/index.ts @@ -1101,7 +1101,7 @@ export class Registry { return await installDependenciesLinux(targets, dryRun); } - async install(executablesToInstall: Executable[], options?: { force?: boolean }) { + async install(executablesToInstall: Executable[], options?: { force?: boolean, gc?: boolean }) { const executables = this._dedupe(executablesToInstall); await fs.promises.mkdir(registryDirectory, { recursive: true }); const lockfilePath = path.join(registryDirectory, '__dirlock'); @@ -1127,7 +1127,7 @@ export class Registry { await fs.promises.writeFile(path.join(linksDir, calculateSha1(PACKAGE_PATH)), PACKAGE_PATH); // Remove stale browsers. - if (!getAsBooleanFromENV('PLAYWRIGHT_SKIP_BROWSER_GC')) + if (options?.gc !== false && !getAsBooleanFromENV('PLAYWRIGHT_SKIP_BROWSER_GC')) await this._validateInstallationCache(linksDir); // Install browsers for this package. diff --git a/packages/playwright-core/src/tools/cli-daemon/program.ts b/packages/playwright-core/src/tools/cli-daemon/program.ts index 1d8877330b8ad..fdb3e7a05a823 100644 --- a/packages/playwright-core/src/tools/cli-daemon/program.ts +++ b/packages/playwright-core/src/tools/cli-daemon/program.ts @@ -139,7 +139,7 @@ async function findOrInstallDefaultBrowser() { async function resolveAndInstall(nameOrChannel: string) { const executables = browserRegistry.resolveBrowsers([nameOrChannel], { shell: 'no' }); - await browserRegistry.install(executables); + await browserRegistry.install(executables, { gc: false }); } async function createDefaultConfig(channel: string) { From 5eab6461d214f5d7a17a6a5045184379ea2531db Mon Sep 17 00:00:00 2001 From: Devin Rousso Date: Tue, 30 Jun 2026 17:12:55 -0600 Subject: [PATCH 3/3] test(webview): add a timeout for `clearCookies` in teardown so a busy page can't stall it (#41416) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `tests/page/page-set-content.spec.ts:153:3 › should handle timeout properly` will infinitely loop the page as a result, none of the WebKit inspector protocol messages are handled `context.clearCookies` performs multiple round-trips using the WebKit inspector protocol (`Page.getCookies` and then `Page.deleteCookie` for each) as such, give the page fixture a `timeout` so that if `clearCookies` hangs then the worker will be killed and restarted instead of waiting forever --- tests/webview/expectations/webkit-webview-page.txt | 8 ++++++++ tests/webview/webviewTest.ts | 9 +++++++-- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/tests/webview/expectations/webkit-webview-page.txt b/tests/webview/expectations/webkit-webview-page.txt index 89512c7e1977d..8383ec40ecfee 100644 --- a/tests/webview/expectations/webkit-webview-page.txt +++ b/tests/webview/expectations/webkit-webview-page.txt @@ -709,3 +709,11 @@ page/page-wait-for-url.spec.ts › should work with history.replaceState() [fail page/page-wait-for-url.spec.ts › should work with url match for same document navigations [fail] page/retarget.spec.ts › should check the box outside shadow dom label [fail] +# ============================================================================ +# busy-main-thread (1 test) +# This test deliberately infinite loops the WebProcess, which the shared MobileSafari +# can't recover from without a full process restart. Left to run, its teardown would +# exhaust the page fixture timeout and expensively recycle the worker on every shard. +# ============================================================================ +page/page-set-content.spec.ts › should handle timeout properly 2 [fail] + diff --git a/tests/webview/webviewTest.ts b/tests/webview/webviewTest.ts index b5e0e1c21d939..c206ac8588054 100644 --- a/tests/webview/webviewTest.ts +++ b/tests/webview/webviewTest.ts @@ -25,6 +25,11 @@ export { expect } from '@playwright/test'; const PROXY_BASE = process.env.PW_WEBVIEW_PROXY_BASE || 'http://localhost:9222'; +// Limit the page fixture's setup and teardown in case the test infinitely loops. +// All tests use a single shared MobileSafari, so we cannot recover if its busy. +// As such, kill the worker to ensure that the next test has a reachable page. +const WEBVIEW_PAGE_TIMEOUT = 20000; + type WebViewWorkerFixtures = PageWorkerFixtures & { webviewExpectations: Map; webviewEndpoint: string; @@ -138,7 +143,7 @@ export const webviewTest = baseTest.extend { + page: [async ({ playwright, webviewEndpoint }, run) => { const browser = await playwright.webkit.connectOverCDP(webviewEndpoint); const [context] = browser.contexts(); const pages = context.pages(); @@ -152,7 +157,7 @@ export const webviewTest = baseTest.extend {}); await browser.close(); - }, + }, { scope: 'test', timeout: WEBVIEW_PAGE_TIMEOUT }], _autoSkipWebView: [async ({ webviewExpectations }, run, testInfo) => { if (process.env.PWTEST_DISABLE_WEBVIEW_EXPECTATIONS !== undefined) {