From c7569128be79e599ba44375ff7367c657665f705 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Thu, 13 Aug 2026 10:50:02 -0700 Subject: [PATCH 1/9] feat(trace-viewer): render action point and target box in aria mode (#42230) --- packages/isomorphic/trace/traceModernizer.ts | 1 + packages/playwright-core/src/server/dom.ts | 20 +++---- packages/playwright-core/src/server/frames.ts | 19 +++---- .../src/server/trace/recorder/tracing.ts | 7 +-- packages/trace-viewer/src/ui/ariaModeView.css | 15 ++++++ packages/trace-viewer/src/ui/ariaModeView.tsx | 46 +++++++++++----- packages/trace-viewer/src/ui/snapshotTab.tsx | 2 + packages/trace/src/trace.ts | 3 +- tests/library/trace-viewer.spec.ts | 54 ++++++++++++++++++- 9 files changed, 129 insertions(+), 38 deletions(-) diff --git a/packages/isomorphic/trace/traceModernizer.ts b/packages/isomorphic/trace/traceModernizer.ts index fc28e2dc93d13..48012f9b0d57f 100644 --- a/packages/isomorphic/trace/traceModernizer.ts +++ b/packages/isomorphic/trace/traceModernizer.ts @@ -122,6 +122,7 @@ export class TraceModernizer { const existing = this._actionMap.get(event.callId); existing!.inputSnapshot = event.inputSnapshot; existing!.point = event.point; + existing!.box = event.box; break; } case 'log': { diff --git a/packages/playwright-core/src/server/dom.ts b/packages/playwright-core/src/server/dom.ts index 9d1bc1d73b202..0beae8a69f70a 100644 --- a/packages/playwright-core/src/server/dom.ts +++ b/packages/playwright-core/src/server/dom.ts @@ -574,10 +574,10 @@ export class ElementHandle extends js.JSHandle { return throwRetargetableDOMError(result); } - async _selectOption(progress: Progress, elements: ElementHandle[], values: types.SelectOption[], options: types.CommonActionOptions): Promise { + async _selectOption(progress: Progress, elements: ElementHandle[], values: types.SelectOption[], options: types.CommonActionOptions, box?: types.Rect): Promise { let resultingOptions: string[] = []; const result = await this._retryAction(progress, 'select option', async progress => { - await this.instrumentation.onBeforeInputAction(progress, this); + await this.instrumentation.onBeforeInputAction(progress, this, undefined, box); if (!options.force) progress.log(` waiting for element to be visible and enabled`); const optionsToSelect = [...elements, ...values]; @@ -607,10 +607,10 @@ export class ElementHandle extends js.JSHandle { assertDone(throwRetargetableDOMError(result)); } - async _fill(progress: Progress, value: string, options: types.CommonActionOptions): Promise<'error:notconnected' | 'done'> { + async _fill(progress: Progress, value: string, options: types.CommonActionOptions, box?: types.Rect): Promise<'error:notconnected' | 'done'> { progress.log(` fill("${value}")`); return await this._retryAction(progress, 'fill', async progress => { - await this.instrumentation.onBeforeInputAction(progress, this); + await this.instrumentation.onBeforeInputAction(progress, this, undefined, box); if (!options.force) progress.log(' waiting for element to be visible, enabled and editable'); const result = await progress.race(this.evaluateInUtility(async ([injected, node, { value, force }]) => { @@ -723,7 +723,7 @@ export class ElementHandle extends js.JSHandle { }, { ...options, waitAfter: 'disabled' }); } - async _setInputFiles(progress: Progress, items: InputFilesItems): Promise<'error:notconnected' | 'done'> { + async _setInputFiles(progress: Progress, items: InputFilesItems, box?: types.Rect): Promise<'error:notconnected' | 'done'> { const { filePayloads, localPaths, localDirectory } = items; const multiple = filePayloads && filePayloads.length > 1 || localPaths && localPaths.length > 1; const result = await progress.race(this._evaluateHandleInUtility(([injected, node, { multiple, directoryUpload }]): Element | undefined => { @@ -744,7 +744,7 @@ export class ElementHandle extends js.JSHandle { if (result === 'error:notconnected' || !result.asElement()) return 'error:notconnected'; const retargeted = result.asElement() as ElementHandle; - await this.instrumentation.onBeforeInputAction(progress, this); + await this.instrumentation.onBeforeInputAction(progress, this, undefined, box); if (localPaths || localDirectory) { const localPathsOrDirectory = localDirectory ? [localDirectory] : localPaths!; await progress.race(Promise.all((localPathsOrDirectory).map(localPath => ( @@ -783,9 +783,9 @@ export class ElementHandle extends js.JSHandle { return assertDone(throwRetargetableDOMError(result)); } - async _type(progress: Progress, text: string, options: { delay?: number } & types.StrictOptions): Promise<'error:notconnected' | 'done'> { + async _type(progress: Progress, text: string, options: { delay?: number } & types.StrictOptions, box?: types.Rect): Promise<'error:notconnected' | 'done'> { progress.log(`elementHandle.type("${text}")`); - await this.instrumentation.onBeforeInputAction(progress, this); + await this.instrumentation.onBeforeInputAction(progress, this, undefined, box); const result = await this._focus(progress, true /* resetSelectionIfNotFocused */); if (result !== 'done') return result; @@ -799,9 +799,9 @@ export class ElementHandle extends js.JSHandle { return assertDone(throwRetargetableDOMError(result)); } - async _press(progress: Progress, key: string, options: { delay?: number, noWaitAfter?: boolean } & types.StrictOptions): Promise<'error:notconnected' | 'done'> { + async _press(progress: Progress, key: string, options: { delay?: number, noWaitAfter?: boolean } & types.StrictOptions, box?: types.Rect): Promise<'error:notconnected' | 'done'> { progress.log(`elementHandle.press("${key}")`); - await this.instrumentation.onBeforeInputAction(progress, this); + await this.instrumentation.onBeforeInputAction(progress, this, undefined, box); return this._page.frameManager.waitForSignalsCreatedBy(progress, !options.noWaitAfter, async progress => { const result = await this._focus(progress, true /* resetSelectionIfNotFocused */); if (result !== 'done') diff --git a/packages/playwright-core/src/server/frames.ts b/packages/playwright-core/src/server/frames.ts index bad833ee84e94..6657843634b52 100644 --- a/packages/playwright-core/src/server/frames.ts +++ b/packages/playwright-core/src/server/frames.ts @@ -1202,7 +1202,7 @@ export class Frame extends SdkObject { progress: Progress, selector: string, options: { strict?: boolean, noAutoWaiting?: boolean, force?: boolean, performActionPreChecks?: boolean }, - action: (progress: Progress, handle: dom.ElementHandle) => Promise): Promise { + action: (progress: Progress, handle: dom.ElementHandle, box?: types.Rect) => Promise): Promise { progress.log(`waiting for ${this._asLocator(selector)}`); const noAutoWaiting = (options as any).__testHookNoAutoWaiting ?? options.noAutoWaiting; const performActionPreChecks = (options.performActionPreChecks ?? !options.force) && !noAutoWaiting; @@ -1217,7 +1217,8 @@ export class Frame extends SdkObject { log = ` locator resolved to ${elements.length} elements. Proceeding with the first one: ${injected.previewNode(elements[0])}`; else if (element) log = ` locator resolved to ${injected.previewNode(element)}`; - return { log, success: !!element, element }; + const rect = element?.getBoundingClientRect(); + return { log, success: !!element, element, box: rect ? { x: rect.x, y: rect.y, width: rect.width, height: rect.height } : undefined }; }, {})); if (!resolved) { if (noAutoWaiting) @@ -1225,7 +1226,7 @@ export class Frame extends SdkObject { return continuePolling; } const result = resolved.result; - const { log, success } = await progress.race(result.evaluate(r => ({ log: r.log, success: r.success }))); + const { log, success, box } = await progress.race(result.evaluate(r => ({ log: r.log, success: r.success, box: r.box }))); if (log) progress.log(log); if (!success) { @@ -1237,7 +1238,7 @@ export class Frame extends SdkObject { const element = await progress.race(result.evaluateHandle(r => r.element)) as dom.ElementHandle; result.dispose(); try { - const result = await action(progress, element); + const result = await action(progress, element, box); if (result === 'error:notconnected') { if (noAutoWaiting) throw new dom.NonRecoverableDOMError('Element is not attached to the DOM'); @@ -1297,7 +1298,7 @@ export class Frame extends SdkObject { } async fill(progress: Progress, selector: string, value: string, options: types.CommonActionOptions) { - return dom.assertDone(await this._retryWithProgressIfNotConnected(progress, selector, options, (progress, handle) => handle._fill(progress, value, options))); + return dom.assertDone(await this._retryWithProgressIfNotConnected(progress, selector, options, (progress, handle, box) => handle._fill(progress, value, options, box))); } async focus(progress: Progress, selector: string, options: types.StrictOptions & { noAutoWaiting?: boolean }) { @@ -1446,12 +1447,12 @@ export class Frame extends SdkObject { } async selectOption(progress: Progress, selector: string, elements: dom.ElementHandle[], values: types.SelectOption[], options: types.CommonActionOptions): Promise { - return await this._retryWithProgressIfNotConnected(progress, selector, options, (progress, handle) => handle._selectOption(progress, elements, values, options)); + return await this._retryWithProgressIfNotConnected(progress, selector, options, (progress, handle, box) => handle._selectOption(progress, elements, values, options, box)); } async setInputFiles(progress: Progress, selector: string, params: Omit & { noAutoWaiting?: boolean }): Promise { const inputFileItems = await progress.race(prepareFilesForUpload(this, params)); - return dom.assertDone(await this._retryWithProgressIfNotConnected(progress, selector, params, (progress, handle) => handle._setInputFiles(progress, inputFileItems))); + return dom.assertDone(await this._retryWithProgressIfNotConnected(progress, selector, params, (progress, handle, box) => handle._setInputFiles(progress, inputFileItems, box))); } async drop(progress: Progress, selector: string, params: Omit, options: types.PointerActionWaitOptions): Promise { @@ -1465,11 +1466,11 @@ export class Frame extends SdkObject { } async type(progress: Progress, selector: string, text: string, options: { delay?: number, noAutoWaiting?: boolean } & types.StrictOptions) { - return dom.assertDone(await this._retryWithProgressIfNotConnected(progress, selector, options, (progress, handle) => handle._type(progress, text, options))); + return dom.assertDone(await this._retryWithProgressIfNotConnected(progress, selector, options, (progress, handle, box) => handle._type(progress, text, options, box))); } async press(progress: Progress, selector: string, key: string, options: { delay?: number, noWaitAfter?: boolean, noAutoWaiting?: boolean } & types.StrictOptions) { - return dom.assertDone(await this._retryWithProgressIfNotConnected(progress, selector, options, (progress, handle) => handle._press(progress, key, options))); + return dom.assertDone(await this._retryWithProgressIfNotConnected(progress, selector, options, (progress, handle, box) => handle._press(progress, key, options, box))); } async check(progress: Progress, selector: string, options: types.PointerActionWaitOptions) { diff --git a/packages/playwright-core/src/server/trace/recorder/tracing.ts b/packages/playwright-core/src/server/trace/recorder/tracing.ts index 8a8509f984f8f..0575ebb978064 100644 --- a/packages/playwright-core/src/server/trace/recorder/tracing.ts +++ b/packages/playwright-core/src/server/trace/recorder/tracing.ts @@ -544,12 +544,12 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps return this._captureSnapshot(progress, sdkObject, 'before', event.beforeSnapshot); } - onBeforeInputAction(progress: Progress, sdkObject: SdkObject, point?: types.Point) { + onBeforeInputAction(progress: Progress, sdkObject: SdkObject, point?: types.Point, box?: types.Rect) { const { metadata } = progress; // IMPORTANT: no awaits in this method, this._appendTraceEvent must be called synchronously. if (!this._state?.callsInProgress.has(metadata.id)) return Promise.resolve(); - const event = createInputActionTraceEvent(metadata, point); + const event = createInputActionTraceEvent(metadata, point, box); if (!event) return Promise.resolve(); this._temporarilyDisableThrottling(sdkObject.attribution.page); @@ -802,13 +802,14 @@ function createBeforeActionTraceEvent(metadata: CallMetadata, parentId?: string) return event; } -function createInputActionTraceEvent(metadata: CallMetadata, point: types.Point | undefined): trace.InputActionTraceEvent | null { +function createInputActionTraceEvent(metadata: CallMetadata, point: types.Point | undefined, box: types.Rect | undefined): trace.InputActionTraceEvent | null { if (metadata.internal || metadata.method.startsWith('tracing')) return null; return { type: 'input', callId: metadata.id, point, + box, }; } diff --git a/packages/trace-viewer/src/ui/ariaModeView.css b/packages/trace-viewer/src/ui/ariaModeView.css index 45a1cae73b381..852e6f8de8bf9 100644 --- a/packages/trace-viewer/src/ui/ariaModeView.css +++ b/packages/trace-viewer/src/ui/ariaModeView.css @@ -42,6 +42,21 @@ outline: 1px solid #6fa8dc; } +.aria-mode-action-highlight { + position: absolute; + pointer-events: none; + background-color: #f443361f; + outline: 1px solid #f44336; +} + +.aria-mode-action-point { + position: absolute; + pointer-events: none; + transform: translate(-50%, -50%); + border-radius: 50%; + background-color: #f44336; +} + .aria-mode-snapshot { flex: 1 1 0; border-left: 1px solid var(--vscode-panel-border); diff --git a/packages/trace-viewer/src/ui/ariaModeView.tsx b/packages/trace-viewer/src/ui/ariaModeView.tsx index 9e34e37c4438c..f9ed801ccf015 100644 --- a/packages/trace-viewer/src/ui/ariaModeView.tsx +++ b/packages/trace-viewer/src/ui/ariaModeView.tsx @@ -73,6 +73,7 @@ export function collectAriaModeTargets(model: TraceModel, action: ActionTraceEve return { action: actionTarget, before, after }; } +type Point = { x: number, y: number }; type Box = { x: number, y: number, width: number, height: number }; type AriaSnapshotLine = { @@ -133,7 +134,9 @@ function renderLineTokens(text: string): React.ReactNode[] { export const AriaModeView: React.FunctionComponent<{ model: TraceModel | undefined, target: AriaModeTarget | undefined, -}> = ({ model, target }) => { + point?: Point, + box?: Box, +}> = ({ model, target, point, box }) => { const screenshot = model && target ? model.screenshotForCall(target.callId, target.phase) : undefined; const ariaSnapshot = model && target ? model.ariaSnapshotForCall(target.callId, target.phase) : undefined; const [lines, setLines] = React.useState([]); @@ -165,7 +168,7 @@ export const AriaModeView: React.FunctionComponent<{ return ; return
- +
{ariaSnapshot &&
setHighlightedBox(undefined)}> {lines.map((line, index) =>
= ({ model, screenshot, highlightedBox }) => { + point: Point | undefined, + box: Box | undefined, +}> = ({ model, screenshot, highlightedBox, point, box }) => { const [measure, ref] = useMeasure(); const [naturalSize, setNaturalSize] = React.useState<{ width: number, height: number } | undefined>(); - // Trace screenshots are taken with css scale, so image pixels match the aria box viewport coordinates. - let highlightStyle: React.CSSProperties | undefined; - if (highlightedBox && naturalSize && measure.width) { + // Trace screenshots are taken with css scale, so image pixels match the aria box viewport + // coordinates. The image is scaled to fit into the available area, scale the boxes and + // the action point to match the rendered image. + let overlays: React.ReactNode; + if (screenshot && naturalSize && measure.width) { const padding = 10; const availableWidth = measure.width - 2 * padding; const availableHeight = measure.height - 2 * padding; const scale = Math.min(availableWidth / naturalSize.width, availableHeight / naturalSize.height, 1); - highlightStyle = { - left: padding + (availableWidth - naturalSize.width * scale) / 2 + highlightedBox.x * scale + 'px', - top: padding + (availableHeight - naturalSize.height * scale) / 2 + highlightedBox.y * scale + 'px', - width: highlightedBox.width * scale + 'px', - height: highlightedBox.height * scale + 'px', - }; + const offsetX = padding + (availableWidth - naturalSize.width * scale) / 2; + const offsetY = padding + (availableHeight - naturalSize.height * scale) / 2; + const boxStyle = (b: Box): React.CSSProperties => ({ + left: offsetX + b.x * scale + 'px', + top: offsetY + b.y * scale + 'px', + width: b.width * scale + 'px', + height: b.height * scale + 'px', + }); + const pointStyle = (p: Point): React.CSSProperties => ({ + left: offsetX + p.x * scale + 'px', + top: offsetY + p.y * scale + 'px', + width: 20 * scale + 'px', + height: 20 * scale + 'px', + }); + overlays = <> + {box &&
} + {point &&
} + {highlightedBox &&
} + ; } return
@@ -210,6 +230,6 @@ const AriaModeScreenshot: React.FunctionComponent<{ onLoad={event => setNaturalSize({ width: event.currentTarget.naturalWidth, height: event.currentTarget.naturalHeight })} />} {!screenshot && } - {screenshot && highlightStyle &&
} + {overlays}
; }; diff --git a/packages/trace-viewer/src/ui/snapshotTab.tsx b/packages/trace-viewer/src/ui/snapshotTab.tsx index 828daec4f922a..4b3933720dfd1 100644 --- a/packages/trace-viewer/src/ui/snapshotTab.tsx +++ b/packages/trace-viewer/src/ui/snapshotTab.tsx @@ -104,6 +104,8 @@ export const SnapshotTabsView: React.FunctionComponent<{ {displayAriaMode && } {!displayAriaMode && { }); test('should display aria mode', async ({ runAndTrace, page }) => { + let buttonBox: { x: number, y: number, width: number, height: number }; const traceViewer = await runAndTrace(async () => { await page.setContent(''); + buttonBox = (await page.locator('button').boundingBox())!; await page.locator('button').click(); + await page.locator('button').press('Enter'); }, { snapshots: { dom: true, aria: true, screen: true } }); await traceViewer.showSettings(); @@ -2123,18 +2126,65 @@ test('should display aria mode', async ({ runAndTrace, page }) => { await expect(ariaModeView.locator('img')).toBeVisible(); await expect(ariaModeView).toContainText('button "Click me"'); - // Hovering an aria node highlights its box on the screenshot. + // The screenshot is scaled to fit into the available area. + const imgBox = (await ariaModeView.locator('img').boundingBox())!; + const scale = imgBox.width / page.viewportSize()!.width; + const toImage = (point: { x: number, y: number }) => ({ x: imgBox.x + point.x * scale, y: imgBox.y + point.y * scale }); + + // Hovering an aria node highlights its box on the screenshot, scaled to match the image. const highlight = ariaModeView.locator('.aria-mode-highlight'); await expect(highlight).not.toBeVisible(); await ariaModeView.locator('.aria-mode-line', { hasText: 'button "Click me"' }).hover(); await expect(highlight).toBeVisible(); + { + const highlightBox = (await highlight.boundingBox())!; + const expected = toImage(buttonBox!); + expect(Math.abs(highlightBox.x - expected.x)).toBeLessThan(2); + expect(Math.abs(highlightBox.y - expected.y)).toBeLessThan(2); + expect(Math.abs(highlightBox.width - buttonBox!.width * scale)).toBeLessThan(2); + expect(Math.abs(highlightBox.height - buttonBox!.height * scale)).toBeLessThan(2); + } await ariaModeView.locator('img').hover(); await expect(highlight).not.toBeVisible(); - // The "Before" tab shows the state before the click. + // The action point and the target box are rendered on the "Action" screenshot. + const actionPoint = ariaModeView.locator('.aria-mode-action-point'); + const actionHighlight = ariaModeView.locator('.aria-mode-action-highlight'); + await expect(actionPoint).toBeVisible(); + await expect(actionHighlight).toBeVisible(); + { + const pointBox = (await actionPoint.boundingBox())!; + const expected = toImage({ x: buttonBox!.x + buttonBox!.width / 2, y: buttonBox!.y + buttonBox!.height / 2 }); + expect(Math.abs(pointBox.x + pointBox.width / 2 - expected.x)).toBeLessThan(2); + expect(Math.abs(pointBox.y + pointBox.height / 2 - expected.y)).toBeLessThan(2); + const actionHighlightBox = (await actionHighlight.boundingBox())!; + const expectedBox = toImage(buttonBox!); + expect(Math.abs(actionHighlightBox.x - expectedBox.x)).toBeLessThan(2); + expect(Math.abs(actionHighlightBox.y - expectedBox.y)).toBeLessThan(2); + expect(Math.abs(actionHighlightBox.width - buttonBox!.width * scale)).toBeLessThan(2); + expect(Math.abs(actionHighlightBox.height - buttonBox!.height * scale)).toBeLessThan(2); + } + + // Keyboard actions have no point, but still highlight the target box. + await traceViewer.selectAction('Press'); + await expect(actionHighlight).toBeVisible(); + await expect(actionPoint).not.toBeVisible(); + { + const actionHighlightBox = (await actionHighlight.boundingBox())!; + const expectedBox = toImage(buttonBox!); + expect(Math.abs(actionHighlightBox.x - expectedBox.x)).toBeLessThan(2); + expect(Math.abs(actionHighlightBox.y - expectedBox.y)).toBeLessThan(2); + expect(Math.abs(actionHighlightBox.width - buttonBox!.width * scale)).toBeLessThan(2); + expect(Math.abs(actionHighlightBox.height - buttonBox!.height * scale)).toBeLessThan(2); + } + await traceViewer.selectAction('Click'); + + // The "Before" tab shows the state before the click, without the action point. await traceViewer.selectSnapshot('Before'); await expect(ariaModeView.locator('img')).toBeVisible(); await expect(ariaModeView).toContainText('button "Click me"'); + await expect(actionPoint).not.toBeVisible(); + await expect(actionHighlight).not.toBeVisible(); // Toggling the setting off restores the DOM snapshot. await traceViewer.showSettings(); From ffbfb2746e301d0432479ec99c05eb1ad32000fe Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Thu, 13 Aug 2026 11:20:29 -0700 Subject: [PATCH 2/9] fix(extension): recover the debugger after an involuntary detach (#42221) --- packages/extension/src/relayConnection.ts | 59 +++++++++++- tests/extension/extension-fixtures.ts | 13 +++ tests/extension/scheme-detach.spec.ts | 106 ++++++++++++++++++++++ 3 files changed, 177 insertions(+), 1 deletion(-) create mode 100644 tests/extension/scheme-detach.spec.ts diff --git a/packages/extension/src/relayConnection.ts b/packages/extension/src/relayConnection.ts index 7ef2646d3fb1d..928820ef973a8 100644 --- a/packages/extension/src/relayConnection.ts +++ b/packages/extension/src/relayConnection.ts @@ -54,6 +54,10 @@ const CHROME_EVENT_METHODS = [ 'chrome.tabs.onRemoved', ]; +const REATTACH_DELAY_MS = 150; +const REATTACH_VERIFY_MS = 2500; +const REATTACH_COOLDOWN_MS = 3000; + export class RelayConnection { private _ws: WebSocket; // Tabs whose debugger we have explicitly attached for this connection. @@ -62,6 +66,8 @@ export class RelayConnection { private _hasEverAttached = false; private _eventListeners: Array<{ remove: () => void }> = []; private _closed = false; + private _pendingReattach = new Set(); + private _recentReattach = new Set(); onclose?: () => void; ontabattached?: (tabId: number) => void; @@ -125,6 +131,7 @@ export class RelayConnection { private _notifyTabAttached(tabId: number): void { this._attachedTabs.add(tabId); this._hasEverAttached = true; + this._pendingReattach.delete(tabId); this.ontabattached?.(tabId); } @@ -148,6 +155,8 @@ export class RelayConnection { if (this._closed) return; this._closed = true; + this._pendingReattach.clear(); + this._recentReattach.clear(); for (const l of this._eventListeners) l.remove(); this._eventListeners = []; @@ -159,7 +168,7 @@ export class RelayConnection { } private _checkLastTabDetached(): void { - if (this._hasEverAttached && this._attachedTabs.size === 0) + if (this._hasEverAttached && this._attachedTabs.size === 0 && this._pendingReattach.size === 0) this.close('All controlled tabs detached'); } @@ -172,11 +181,59 @@ export class RelayConnection { this._sendMessage({ method: fullMethod, params: args }); // chrome.debugger.onDetach is the single source of truth for detach bookkeeping. if (fullMethod === 'chrome.debugger.onDetach') { + const reason = args[1] as string | undefined; this._notifyTabDetached(tabId); + if (reason === 'target_closed' && this._maybeScheduleReattach(tabId)) + return; this._checkLastTabDetached(); } } + private _maybeScheduleReattach(tabId: number): boolean { + if (this._closed) + return false; + if (this._recentReattach.has(tabId)) { + debugLog(`Not re-attaching tab ${tabId}: re-detached within ${REATTACH_COOLDOWN_MS}ms`); + return false; + } + this._recentReattach.add(tabId); + setTimeout(() => this._recentReattach.delete(tabId), REATTACH_COOLDOWN_MS); + this._pendingReattach.add(tabId); + setTimeout(() => void this._tryReattach(tabId), REATTACH_DELAY_MS); + return true; + } + + private _reattachAborted(tabId: number): boolean { + return this._closed || !this._pendingReattach.has(tabId); + } + + private async _tryReattach(tabId: number): Promise { + if (this._reattachAborted(tabId)) + return; + let tab: chrome.tabs.Tab | undefined; + try { + tab = await chrome.tabs.get(tabId); + } catch { + this._pendingReattach.delete(tabId); + this._checkLastTabDetached(); + return; + } + if (this._reattachAborted(tabId)) + return; + if (this._attachedTabs.has(tabId)) { + this._pendingReattach.delete(tabId); + return; + } + this.attachTab(tab); + setTimeout(() => { + if (this._reattachAborted(tabId)) + return; + this._pendingReattach.delete(tabId); + if (!this._attachedTabs.has(tabId)) + this._checkLastTabDetached(); + }, REATTACH_VERIFY_MS); + } + // Returns the tabId an event refers to, for filtering by _attachedTabs. private _tabIdForEventArgs(fullMethod: string, args: any[]): number | undefined { switch (fullMethod) { diff --git a/tests/extension/extension-fixtures.ts b/tests/extension/extension-fixtures.ts index 8594364908baa..760c78fdaf8f3 100644 --- a/tests/extension/extension-fixtures.ts +++ b/tests/extension/extension-fixtures.ts @@ -206,6 +206,19 @@ export async function readExtensionToken(browserContext: BrowserContext): Promis return value; } +export async function connectWithToken(browserContext: BrowserContext, startClient: StartClient, userDataDir: string): Promise<{ client: Client, stderr: () => string }> { + const token = await readExtensionToken(browserContext); + const { client, stderr } = await startClient({ + args: ['--extension'], + env: { + DEBUG: 'pw:mcp:backend', + PLAYWRIGHT_MCP_EXTENSION_TOKEN: token, + PWTEST_EXTENSION_USER_DATA_DIR: userDataDir, + }, + }); + return { client, stderr }; +} + // The connect page closes itself once a different tab is selected, which races // with the click — the request reaches the background while the page is being // torn down. Swallow the resulting "Target closed" error. diff --git a/tests/extension/scheme-detach.spec.ts b/tests/extension/scheme-detach.spec.ts new file mode 100644 index 0000000000000..f541afad84a62 --- /dev/null +++ b/tests/extension/scheme-detach.spec.ts @@ -0,0 +1,106 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { test, expect, extensionId, connectWithToken } from './extension-fixtures'; + +import type { BrowserContext } from 'playwright'; + +async function watchDetach(browserContext: BrowserContext) { + const serviceWorker = browserContext.serviceWorkers().find(w => w.url().includes(extensionId))!; + await serviceWorker.evaluate(() => { + (globalThis as any).__detach = []; + (globalThis as any).chrome.debugger.onDetach.addListener((source: any, reason: string) => { + (globalThis as any).__detach.push({ source, reason }); + }); + }); + return () => serviceWorker.evaluate(() => (globalThis as any).__detach); +} + +test('debugger detaches from the whole tab when a subframe navigates to an unknown scheme', { + annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/42089' }, +}, async ({ browserWithExtension, startClient, server }) => { + const browserContext = await browserWithExtension.launch(); + const { client } = await connectWithToken(browserContext, startClient, browserWithExtension.userDataDir); + + server.setContent('/signin', `SignIn
QR CODE HERE
`, 'text/html'); + await client.callTool({ name: 'browser_navigate', arguments: { url: server.PREFIX + '/signin' } }); + + const getDetach = await watchDetach(browserContext); + const page = browserContext.pages().find(p => p.url().endsWith('/signin'))!; + await page.evaluate(() => { + const iframe = document.createElement('iframe'); + iframe.src = 'customscheme://cc/'; + document.body.appendChild(iframe); + }).catch(() => {}); + + await expect.poll(async () => (await getDetach()).length, { timeout: 5000 }).toBeGreaterThan(0); + const detach = await getDetach(); + expect(detach[0].reason).toBe('target_closed'); + expect(detach[0].source.frameId).toBeUndefined(); + + expect(page.url()).toContain('/signin'); + expect(await page.title()).toBe('SignIn'); +}); + +test('recovers and can screenshot after the debugger is detached', { + annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/42089' }, +}, async ({ browserWithExtension, startClient, server }) => { + const browserContext = await browserWithExtension.launch(); + const { client } = await connectWithToken(browserContext, startClient, browserWithExtension.userDataDir); + + server.setContent('/signin', ` + SignIn +
QR CODE HERE
+ + `, 'text/html'); + + const getDetach = await watchDetach(browserContext); + expect(await client.callTool({ + name: 'browser_navigate', + arguments: { url: server.PREFIX + '/signin' }, + })).toHaveResponse({ snapshot: expect.stringContaining('QR CODE HERE') }); + + await expect.poll(async () => (await getDetach()).length, { timeout: 5000 }).toBeGreaterThan(0); + expect((await getDetach())[0].reason).toBe('target_closed'); + + expect(browserContext.pages().find(p => p.url().endsWith('/signin'))).toBeTruthy(); + + await expect.poll(async () => { + const list = await client.callTool({ name: 'browser_tabs', arguments: { action: 'list' } }); + return (list as any).content?.[0]?.text ?? ''; + }, { timeout: 15000 }).toContain('/signin'); + + const list = await client.callTool({ name: 'browser_tabs', arguments: { action: 'list' } }); + const index = /- (\d+):[^\n]*\/signin/.exec((list as any).content[0].text)?.[1]; + expect(index).toBeTruthy(); + await client.callTool({ name: 'browser_tabs', arguments: { action: 'select', index: Number(index) } }); + + expect(await client.callTool({ name: 'browser_snapshot', arguments: {} })).toHaveResponse({ + inlineSnapshot: expect.stringContaining('QR CODE HERE'), + }); + expect(await client.callTool({ name: 'browser_take_screenshot', arguments: {} })).toHaveResponse({ + code: expect.stringContaining('page.screenshot'), + }); +}); From 290c16d9c95a5a287c3d62ad4aff4d6237dd21fd Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Thu, 13 Aug 2026 11:38:29 -0700 Subject: [PATCH 3/9] fix(mcp): report the actual remote browser name in browserInfo (#42228) --- packages/playwright-core/src/tools/index.ts | 2 +- .../src/tools/mcp/browserFactory.ts | 2 +- tests/mcp/remote-endpoint.spec.ts | 16 ++++++++++++++++ 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/packages/playwright-core/src/tools/index.ts b/packages/playwright-core/src/tools/index.ts index 5fdad4331b2ac..1b42c015a4990 100644 --- a/packages/playwright-core/src/tools/index.ts +++ b/packages/playwright-core/src/tools/index.ts @@ -25,7 +25,7 @@ export { createConnection } from './mcp/index'; export { resolveCLIConfigForCLI, resolveCLIConfigForMCP } from './mcp/config'; export { outputDir } from './backend/context'; export { isSystemDirectory } from '@utils/fileUtils'; -export { isProfileLocked } from './mcp/browserFactory'; +export { createBrowserWithInfo, isProfileLocked } from './mcp/browserFactory'; export { compareSemver } from './utils/socketConnection'; export { extractTrace, DirTraceLoaderBackend } from './trace/traceParser'; export { decorateMCPCommand } from './mcp/program'; diff --git a/packages/playwright-core/src/tools/mcp/browserFactory.ts b/packages/playwright-core/src/tools/mcp/browserFactory.ts index 8cd9656dd5ce4..5ca1c20b7ae5b 100644 --- a/packages/playwright-core/src/tools/mcp/browserFactory.ts +++ b/packages/playwright-core/src/tools/mcp/browserFactory.ts @@ -152,7 +152,7 @@ async function createRemoteBrowser(config: FullConfig): Promise // created, so create one when attaching to such a server. if (!browser.contexts().length) await browser.newContext(config.browser.contextOptions); - return { browser, browserInfo: browserInfo(browser, config), canBind: false, ownership: 'attached' }; + return { browser, browserInfo: { ...browserInfo(browser, config), browserName: browser._browserName }, canBind: false, ownership: 'attached' }; } async function createPersistentBrowser(config: FullConfig, clientInfo: ClientInfo): Promise { diff --git a/tests/mcp/remote-endpoint.spec.ts b/tests/mcp/remote-endpoint.spec.ts index 42521074bbfe8..4b0ce13b78b89 100644 --- a/tests/mcp/remote-endpoint.spec.ts +++ b/tests/mcp/remote-endpoint.spec.ts @@ -16,6 +16,10 @@ import { test, expect } from './fixtures'; +import { tools } from '../../packages/playwright-core/lib/coreBundle'; + +const { resolveCLIConfigForMCP, createBrowserWithInfo } = tools; + test.skip(({ mcpBrowser }) => mcpBrowser !== 'chromium', 'Run only on the chromium project; the remote server connection is browser-agnostic.'); test('connect without headers fails on run-server endpoint', async ({ startClient, server, runServerEndpoint }) => { @@ -60,6 +64,18 @@ test('remoteEndpoint accepts ConnectOptions object with headers', async ({ start }); }); +test('browserInfo reports the browser running on the remote endpoint, not the configured one', async ({ wsEndpoint }, testInfo) => { + // Empty env to isolate the test from the host environment. + const config = await resolveCLIConfigForMCP({ browser: 'firefox', endpoint: wsEndpoint }, {}); + const { browser, browserInfo } = await createBrowserWithInfo(config, { clientName: 'test-client', cwd: testInfo.outputPath() }, {}); + try { + expect(config.browser.browserName).toBe('firefox'); + expect(browserInfo.browserName).toBe('chromium'); + } finally { + await browser.close(); + } +}); + test('back-compat: remoteHeaders config still selects the browser on run-server endpoint', async ({ startClient, server, runServerEndpoint }) => { const { client } = await startClient({ config: { From b3739bb8fe24e62d09ee061ce6140e6eaa27669e Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Thu, 13 Aug 2026 11:38:36 -0700 Subject: [PATCH 4/9] fix(tests): remove stale devtools path alias (#42236) --- tests/tsconfig.json | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/tsconfig.json b/tests/tsconfig.json index ad46142a25302..b2e0e14ddd899 100644 --- a/tests/tsconfig.json +++ b/tests/tsconfig.json @@ -11,7 +11,6 @@ "baseUrl": "..", "paths": { "@dashboard/*": ["packages/dashboard/src/*"], - "@dvtools/*": ["packages/devtools/src/*"], "@injected/*": ["packages/injected/src/*"], "@isomorphic/*": ["packages/isomorphic/*"], "@utils/*": ["packages/utils/*"], From 553c76e6b45514372d8280bb48bc9b3de637c70f Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Thu, 13 Aug 2026 11:38:43 -0700 Subject: [PATCH 5/9] chore(deps): roll chokidar to 4.0.3, drop fsevents optional dependency (#42238) --- package-lock.json | 113 ++++++++++-------- package.json | 2 +- .../playwright-core/src/serverRegistry.ts | 3 +- packages/playwright/package.json | 3 - packages/playwright/src/runner/fsWatcher.ts | 12 +- tests/installation/bundle-licenses.spec.ts | 4 +- utils/build/build.js | 65 ++++++++-- 7 files changed, 132 insertions(+), 70 deletions(-) diff --git a/package-lock.json b/package-lock.json index 264c8012e6136..1775edeeed384 100644 --- a/package-lock.json +++ b/package-lock.json @@ -72,7 +72,7 @@ "@vitejs/plugin-basic-ssl": "2.3.0", "@vitejs/plugin-react": "6.0.3", "@zip.js/zip.js": "2.7.73", - "chokidar": "3.6.0", + "chokidar": "4.0.3", "chromium-bidi": "12.1.0", "colors": "1.4.0", "commander": "15.0.0", @@ -4142,28 +4142,19 @@ } }, "node_modules/chokidar": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", "dev": true, "license": "MIT", "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" + "readdirp": "^4.0.1" }, "engines": { - "node": ">= 8.10.0" + "node": ">= 14.16.0" }, "funding": { "url": "https://paulmillr.com/funding/" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" } }, "node_modules/chromium-bidi": { @@ -8092,29 +8083,17 @@ "license": "MIT" }, "node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dev": true, - "license": "MIT", - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" - } - }, - "node_modules/readdirp/node_modules/picomatch": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", - "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", "dev": true, "license": "MIT", "engines": { - "node": ">=8.6" + "node": ">= 14.18.0" }, "funding": { - "url": "https://github.com/sponsors/jonschlinkert" + "type": "individual", + "url": "https://paulmillr.com/funding/" } }, "node_modules/recorder": { @@ -9341,6 +9320,57 @@ "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/vite-plugin-static-copy/node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/vite-plugin-static-copy/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/vite-plugin-static-copy/node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, "node_modules/web": { "resolved": "packages/web", "link": true @@ -9646,9 +9676,6 @@ }, "engines": { "node": ">=20" - }, - "optionalDependencies": { - "fsevents": "2.3.2" } }, "packages/playwright-browser-chromium": { @@ -9764,20 +9791,6 @@ "node": ">=20" } }, - "packages/playwright/node_modules/fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, "packages/recorder": { "version": "0.0.0", "dependencies": { diff --git a/package.json b/package.json index 90a2210955c5a..5cd510efbbe49 100644 --- a/package.json +++ b/package.json @@ -115,7 +115,7 @@ "@vitejs/plugin-basic-ssl": "2.3.0", "@vitejs/plugin-react": "6.0.3", "@zip.js/zip.js": "2.7.73", - "chokidar": "3.6.0", + "chokidar": "4.0.3", "chromium-bidi": "12.1.0", "colors": "1.4.0", "commander": "15.0.0", diff --git a/packages/playwright-core/src/serverRegistry.ts b/packages/playwright-core/src/serverRegistry.ts index 3f43ad8d79494..f5ee5ec1cbbdb 100644 --- a/packages/playwright-core/src/serverRegistry.ts +++ b/packages/playwright-core/src/serverRegistry.ts @@ -23,6 +23,7 @@ import chokidar from 'chokidar'; import { packageJSON, packageRoot } from './package'; +import type { FSWatcher } from 'chokidar'; // Only client depenencies with backward compatibility guarantees should be imported here. import type { LaunchOptions } from '../types/types'; @@ -58,7 +59,7 @@ export interface ServerRegistryEvents { class ServerRegistry extends EventEmitter { private _descriptors = new Map(); - private _watcher: chokidar.FSWatcher | undefined; + private _watcher: FSWatcher | undefined; private _watcherRefs = 0; private _ready: Promise | undefined; diff --git a/packages/playwright/package.json b/packages/playwright/package.json index e82604319f86f..e601747eddac6 100644 --- a/packages/playwright/package.json +++ b/packages/playwright/package.json @@ -51,8 +51,5 @@ "license": "Apache-2.0", "dependencies": { "playwright-core": "1.63.0-next" - }, - "optionalDependencies": { - "fsevents": "2.3.2" } } diff --git a/packages/playwright/src/runner/fsWatcher.ts b/packages/playwright/src/runner/fsWatcher.ts index dbb47654c9bdf..04167de56e087 100644 --- a/packages/playwright/src/runner/fsWatcher.ts +++ b/packages/playwright/src/runner/fsWatcher.ts @@ -49,15 +49,23 @@ export class FSWatcher { if (!this._watchedPaths.length) return; - const ignored = [...this._ignoredFolders, '**/node_modules/**']; + // Chokidar calls the matcher with paths normalized to forward slashes. + const ignoredPrefixes = this._ignoredFolders.map(folder => folder.replace(/\\/g, '/') + '/'); + const ignored = (file: string) => { + if (file.split('/').includes('node_modules')) + return true; + return ignoredPrefixes.some(prefix => (file + '/').startsWith(prefix)); + }; this._fsWatcher = chokidar.watch(watchedPaths, { ignoreInitial: true, ignored }).on('all', async (event, file) => { + if (event !== 'add' && event !== 'addDir' && event !== 'change' && event !== 'unlink' && event !== 'unlinkDir') + return; if (this._throttleTimer) clearTimeout(this._throttleTimer); this._collector.push({ event, file }); this._throttleTimer = setTimeout(() => this._reportEventsIfAny(), 250); }); - await new Promise((resolve, reject) => this._fsWatcher!.once('ready', resolve).once('error', reject)); + await new Promise((resolve, reject) => this._fsWatcher!.once('ready', () => resolve()).once('error', reject)); } async close() { diff --git a/tests/installation/bundle-licenses.spec.ts b/tests/installation/bundle-licenses.spec.ts index 9f2462a3fe1b8..f9d8dd6fc0014 100644 --- a/tests/installation/bundle-licenses.spec.ts +++ b/tests/installation/bundle-licenses.spec.ts @@ -24,8 +24,8 @@ import { test, expect } from './npmTest'; // summary and is only checked for presence. const EXPECTED: Record> = { 'playwright-core': { - 'lib/serverRegistry.js.LICENSE': 10, - 'lib/utilsBundle.js.LICENSE': 80, + 'lib/serverRegistry.js.LICENSE': 2, + 'lib/utilsBundle.js.LICENSE': 75, // Vendored WebP codec (libwebp compiled to WASM), not a bundle sidecar. 'lib/webp_codec.LICENSE': 0, }, diff --git a/utils/build/build.js b/utils/build/build.js index 9a68a7725fd9f..75c247284e174 100644 --- a/utils/build/build.js +++ b/utils/build/build.js @@ -22,6 +22,7 @@ const chokidar = require('chokidar'); const fs = require('fs'); const { workspace } = require('../workspace'); const { build, context } = require('esbuild'); +const { minimatch } = require('minimatch'); /** * @typedef {{ @@ -79,6 +80,43 @@ function filePath(relative) { return path.join(ROOT, ...relative.split('/')); } +/** + * @param {string} p + * @returns {string} + */ +function toPosixPath(p) { + return p.split(path.sep).join('/'); +} + +/** + * Chokidar v4 dropped glob support: watch the static directory prefix of a + * glob instead, and filter emitted paths with `pathMatcher`. + * @param {string} pattern + * @returns {string} + */ +function globBase(pattern) { + const magicIndex = pattern.search(/[*?{[]/); + if (magicIndex === -1) + return pattern; + return pattern.slice(0, pattern.lastIndexOf(path.sep, magicIndex)); +} + +/** + * @param {string[]} patterns absolute files, directories or globs + * @returns {(file: string) => boolean} + */ +function pathMatcher(patterns) { + const posixPatterns = patterns.map(toPosixPath); + return file => { + const posixFile = toPosixPath(file); + return posixPatterns.some(pattern => { + if (pattern.search(/[*?{[]/) === -1) + return posixFile === pattern || posixFile.startsWith(pattern + '/'); + return minimatch(posixFile, pattern, { dot: true }); + }); + }; +} + /** * Resolve a CLI shipped by a node_modules package to an absolute path, so we * can spawn it via `node` directly instead of going through `npx`/`npm exec` @@ -185,14 +223,15 @@ async function runWatch() { clearTimeout(timeout); timeout = setTimeout(callback, 500); }; - chokidar.watch([...paths, ...mustExist, onChange.script].filter(Boolean).map(filePath)).on('all', reschedule); + chokidar.watch([...paths, ...mustExist, onChange.script].filter(Boolean).map(filePath).map(globBase)).on('all', reschedule); callback(); } for (const { files, from, to, ignored } of copyFiles) { - const watcher = chokidar.watch([filePath(files)], { ignored }); + const matches = pathMatcher([filePath(files)]); + const watcher = chokidar.watch(globBase(filePath(files)), { ignored: pathMatcher(ignored || []) }); watcher.on('all', (event, file) => { - if (event === 'add' || event === 'change') + if ((event === 'add' || event === 'change') && matches(file)) copyFile(file, from, to); }); } @@ -212,11 +251,11 @@ async function runWatch() { async function runBuild() { for (const { files, from, to, ignored } of copyFiles) { - const watcher = chokidar.watch([filePath(files)], { - ignored - }); + const matches = pathMatcher([filePath(files)]); + const watcher = chokidar.watch(globBase(filePath(files)), { ignored: pathMatcher(ignored || []) }); watcher.on('add', file => { - copyFile(file, from, to); + if (matches(file)) + copyFile(file, from, to); }); await new Promise(x => watcher.once('ready', x)); watcher.close(); @@ -330,9 +369,14 @@ class EsbuildStep extends Step { this._context = await context(this._options); disposables.push(() => this._context?.dispose()); - const watcher = chokidar.watch([...this._options.entryPoints, ...(this._watchPaths || [])]); + const watchPaths = [...this._options.entryPoints, ...(this._watchPaths || [])]; + const matches = pathMatcher(watchPaths); + const watcher = chokidar.watch([...new Set(watchPaths.map(globBase))]); await new Promise(x => watcher.once('ready', x)); - watcher.on('all', () => this._rebuild()); + watcher.on('all', (event, file) => { + if (matches(file)) + this._rebuild(); + }); await this._rebuild(); console.log('==== Esbuild watching:', this._relativeEntryPoints().join(', '), `(started in ${Date.now() - start}ms)`); @@ -623,7 +667,6 @@ steps.push(new EsbuildStep({ bundle: true, entryPoints: [filePath('packages/playwright-core/src/serverRegistry.js')], outfile: filePath('packages/playwright-core/lib/serverRegistry.js'), - external: ['fsevents'], }, [filePath('packages/playwright-core/src/*')])); const playwrightCoreSrc = filePath('packages/playwright-core/src'); @@ -634,7 +677,7 @@ steps.push(new EsbuildStep({ bundle: true, entryPoints: [filePath('packages/playwright-core/src/utilsBundle.ts')], outfile: filePath('packages/playwright-core/lib/utilsBundle.js'), - external: ['fsevents', 'express', '@anthropic-ai/sdk'], + external: ['express', '@anthropic-ai/sdk'], alias: { 'raw-body': filePath('utils/build/raw-body.ts'), }, From 47b5772c05fa24b87cdb2f099ee767b59e50bf0e Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Thu, 13 Aug 2026 11:38:59 -0700 Subject: [PATCH 6/9] fix(types): remove stale screencast annotate option (#42239) --- packages/playwright-client/types/types.d.ts | 5 ----- packages/playwright-core/types/types.d.ts | 5 ----- utils/generate_types/overrides.d.ts | 5 ----- utils/generate_types/test/test.ts | 4 ++++ 4 files changed, 4 insertions(+), 15 deletions(-) diff --git a/packages/playwright-client/types/types.d.ts b/packages/playwright-client/types/types.d.ts index da0a75d1bf7a5..19310a9c322b2 100644 --- a/packages/playwright-client/types/types.d.ts +++ b/packages/playwright-client/types/types.d.ts @@ -18761,11 +18761,6 @@ export interface Screencast { height: number; }; quality?: number; - annotate?: { - duration?: number; - position?: 'top-left' | 'top' | 'top-right' | 'bottom-left' | 'bottom' | 'bottom-right'; - fontSize?: number; - }; }): Promise; /** * Removes action decorations. diff --git a/packages/playwright-core/types/types.d.ts b/packages/playwright-core/types/types.d.ts index da0a75d1bf7a5..19310a9c322b2 100644 --- a/packages/playwright-core/types/types.d.ts +++ b/packages/playwright-core/types/types.d.ts @@ -18761,11 +18761,6 @@ export interface Screencast { height: number; }; quality?: number; - annotate?: { - duration?: number; - position?: 'top-left' | 'top' | 'top-right' | 'bottom-left' | 'bottom' | 'bottom-right'; - fontSize?: number; - }; }): Promise; /** * Removes action decorations. diff --git a/utils/generate_types/overrides.d.ts b/utils/generate_types/overrides.d.ts index 162db504ce6d8..a329924e5ad0f 100644 --- a/utils/generate_types/overrides.d.ts +++ b/utils/generate_types/overrides.d.ts @@ -256,11 +256,6 @@ export interface Screencast { height: number; }; quality?: number; - annotate?: { - duration?: number; - position?: 'top-left' | 'top' | 'top-right' | 'bottom-left' | 'bottom' | 'bottom-right'; - fontSize?: number; - }; }): Promise; } diff --git a/utils/generate_types/test/test.ts b/utils/generate_types/test/test.ts index dd340a20c7a8f..3d098fc0f4931 100644 --- a/utils/generate_types/test/test.ts +++ b/utils/generate_types/test/test.ts @@ -19,6 +19,10 @@ import * as playwright from 'playwright'; type AssertType = S extends T ? AssertNotAny : false; type AssertNotAny = {notRealProperty: number} extends S ? false : true; +declare const page: playwright.Page; +// @ts-expect-error annotate is not a Screencast.start option. +page.screencast.start({ annotate: { position: 'top-left' } }); + // Examples taken from README (async () => { const browser = await playwright.chromium.launch(); From 961a59985be9c482d545b14c596975b0877aef51 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Thu, 13 Aug 2026 12:29:05 -0700 Subject: [PATCH 7/9] fix(har): do not stall context close when saving the har fails (#42242) --- .../playwright-core/src/client/browserContext.ts | 4 +++- .../playwright-core/src/server/browserContext.ts | 7 +++++-- .../playwright-core/src/server/har/harRecorder.ts | 8 ++------ .../src/server/trace/recorder/tracing.ts | 6 +++--- packages/playwright/src/worker/testTracing.ts | 4 +--- packages/utils/serializedFS.ts | 8 ++++++-- tests/library/har.spec.ts | 13 +++++++++++++ 7 files changed, 33 insertions(+), 17 deletions(-) diff --git a/packages/playwright-core/src/client/browserContext.ts b/packages/playwright-core/src/client/browserContext.ts index 3cd2f2df642bc..5b79e670d975a 100644 --- a/packages/playwright-core/src/client/browserContext.ts +++ b/packages/playwright-core/src/client/browserContext.ts @@ -528,9 +528,11 @@ export class BrowserContext extends ChannelOwner this._closingStatus = 'closing'; await this.request.dispose(options); await this._instrumentation.runBeforeCloseBrowserContext(this); - await this.tracing._exportAllHars(); + const harError = await this.tracing._exportAllHars().catch(e => e); await this._channel.close(options, kNoTimeout); await this._closedPromise; + if (harError) + throw harError; } async _enableRecorder(params: channels.BrowserContextEnableRecorderParams, eventSink?: RecorderEventSink) { diff --git a/packages/playwright-core/src/server/browserContext.ts b/packages/playwright-core/src/server/browserContext.ts index 2dc2b84222073..36bb305670f57 100644 --- a/packages/playwright-core/src/server/browserContext.ts +++ b/packages/playwright-core/src/server/browserContext.ts @@ -554,6 +554,7 @@ export abstract class BrowserContext extends Sdk } async close(progress: Progress, options: { reason?: string }) { + let flushError: Error | undefined; if (this._closedStatus === 'open') { if (options.reason) this._closeReason = options.reason; @@ -561,8 +562,8 @@ export abstract class BrowserContext extends Sdk this._closedStatus = 'closing'; await progress.race(Promise.all([ - this.tracing.flush(), - this.fetchRequest.tracing().flush(), + this.tracing.flush().catch(e => flushError = flushError ?? e), + this.fetchRequest.tracing().flush().catch(e => flushError = flushError ?? e), ])); await progress.race(Promise.all(this.pages().map(page => page.screencast.handlePageOrContextClose()))); @@ -587,6 +588,8 @@ export abstract class BrowserContext extends Sdk this._didCloseInternal(); } await this._closePromise; + if (flushError) + throw flushError; } async newPage(progress: Progress, forStorageState?: boolean): Promise { diff --git a/packages/playwright-core/src/server/har/harRecorder.ts b/packages/playwright-core/src/server/har/harRecorder.ts index 360cfd34df6de..b8b8c80774fc4 100644 --- a/packages/playwright-core/src/server/har/harRecorder.ts +++ b/packages/playwright-core/src/server/har/harRecorder.ts @@ -141,9 +141,7 @@ export class HarRecorder implements HarTracerDelegate { async flush() { await this._flush(); - const error = await this._fs.syncAndGetError(); - if (error) - throw error; + await this._fs.sync(); } async export(mode: 'archive' | 'entries'): Promise<{ entries?: NameValue[], artifact?: Artifact }> { @@ -154,9 +152,7 @@ export class HarRecorder implements HarTracerDelegate { const zipPath = this._harFilePath + '.zip'; if (mode === 'archive') this._fs.zip(entries, zipPath); - const error = await this._fs.syncAndGetError(); - if (error) - throw error; + await this._fs.sync(); if (mode === 'entries') return { entries }; const artifact = new Artifact(this._context, zipPath); diff --git a/packages/playwright-core/src/server/trace/recorder/tracing.ts b/packages/playwright-core/src/server/trace/recorder/tracing.ts index 0575ebb978064..5df9d1cf42d4b 100644 --- a/packages/playwright-core/src/server/trace/recorder/tracing.ts +++ b/packages/playwright-core/src/server/trace/recorder/tracing.ts @@ -337,7 +337,7 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps this._closeAllGroups(); this._harTracer.stop(); this.flushHarEntries(); - await this._fs.syncAndGetError().finally(() => { + await this._fs.sync().finally(() => { this._state = undefined; }); } @@ -363,7 +363,7 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps this.abort(); for (const harRecorder of this.harRecorders.values()) await harRecorder.flush(); - await this._fs.syncAndGetError(); + await this._fs.sync(); } harStart(page: Page | null, options: RecordHarOptions): string { @@ -442,7 +442,7 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps // Make sure all file operations complete. let error: Error | undefined; try { - await progress.race(this._fs.syncAndGetError()); + await progress.race(this._fs.sync()); } catch (e) { error = e as Error; } diff --git a/packages/playwright/src/worker/testTracing.ts b/packages/playwright/src/worker/testTracing.ts index e13b8ef7cb56c..52487a80aab4e 100644 --- a/packages/playwright/src/worker/testTracing.ts +++ b/packages/playwright/src/worker/testTracing.ts @@ -177,9 +177,7 @@ export class TestTracing { if (!this._options) return; - const error = await this._liveTraceFile?.fs.syncAndGetError(); - if (error) - throw error; + await this._liveTraceFile?.fs.sync(); if (this._shouldAbandonTrace()) { for (const file of this._temporaryTraceFiles) diff --git a/packages/utils/serializedFS.ts b/packages/utils/serializedFS.ts index 994ebf22e192e..ed5f1099afec1 100644 --- a/packages/utils/serializedFS.ts +++ b/packages/utils/serializedFS.ts @@ -86,11 +86,15 @@ export class SerializedFS { this._appendOperation({ op: 'copyFile', from, to }); } - async syncAndGetError() { + async sync() { for (const file of this._buffers.keys()) this._flushFile(file); await this._operationsDone; - return this._error; + if (this._error) { + const e = this._error; + this._error = undefined; + throw e; + } } zip(entries: NameValue[], zipFileName: string) { diff --git a/tests/library/har.spec.ts b/tests/library/har.spec.ts index 8d7cb41f3f7c5..4857202ccdc5e 100644 --- a/tests/library/har.spec.ts +++ b/tests/library/har.spec.ts @@ -940,6 +940,19 @@ it('should not hang on slow chunked response', async ({ browserName, browser, co expect(log.browser!.version).toBe(browser.version()); }); +it('should close the context when saving the har fails', async ({ contextFactory, server }, testInfo) => { + it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/42231' }); + const filePath = testInfo.outputPath('not-a-directory'); + fs.writeFileSync(filePath, 'data'); + const context = await contextFactory({ recordHar: { path: path.join(filePath, 'test.har') } }); + const page = await context.newPage(); + await page.goto(server.EMPTY_PAGE); + const closed = new Promise(f => context.on('close', f)); + await expect(context.close()).rejects.toThrow(/ENOTDIR|ENOENT|EEXIST/); + await closed; + await context.close(); +}); + it('should support HAR larger than 512MB', async ({ contextFactory, server, browserName }, testInfo) => { it.skip(browserName !== 'chromium', 'serializer is browser-agnostic; one browser is enough'); it.slow(); From 4f25184efa4970fcf58f2538b135d1d3a6b9122e Mon Sep 17 00:00:00 2001 From: "microsoft-playwright-automation[bot]" <203992400+microsoft-playwright-automation[bot]@users.noreply.github.com> Date: Thu, 13 Aug 2026 12:40:29 -0700 Subject: [PATCH 8/9] feat(webkit): roll to r2349 (#42241) --- packages/playwright-core/browsers.json | 2 +- .../src/server/webkit/protocol.d.ts | 64 +++++++++++-------- 2 files changed, 39 insertions(+), 27 deletions(-) diff --git a/packages/playwright-core/browsers.json b/packages/playwright-core/browsers.json index e2d18c60b4859..e402d597f8b61 100644 --- a/packages/playwright-core/browsers.json +++ b/packages/playwright-core/browsers.json @@ -24,7 +24,7 @@ }, { "name": "webkit", - "revision": "2346", + "revision": "2349", "installByDefault": true, "revisionOverrides": { "mac14": "2251", diff --git a/packages/playwright-core/src/server/webkit/protocol.d.ts b/packages/playwright-core/src/server/webkit/protocol.d.ts index 5955ff74828c0..a1ef30279396e 100644 --- a/packages/playwright-core/src/server/webkit/protocol.d.ts +++ b/packages/playwright-core/src/server/webkit/protocol.d.ts @@ -1173,10 +1173,6 @@ export namespace Protocol { */ contextType: ContextType; sizes?: GenericTypes.Size[]; - /** - * The corresponding DOM node id. - */ - nodeId?: DOM.NodeId; /** * The CSS canvas identifiers, for canvases created with document.getCSSCanvasContext. */ @@ -1190,7 +1186,7 @@ export namespace Protocol { */ features?: string[]; /** - * Memory usage of the canvas in bytes. + * Estimated memory usage of the graphics context and its associated resources, in bytes. */ memoryCost?: number; /** @@ -1238,7 +1234,7 @@ export namespace Protocol { */ canvasId: CanvasId; /** - * New memory cost value for the canvas in bytes. + * New estimated memory cost of the graphics context and its associated resources, in bytes. */ memoryCost: number; } @@ -1249,11 +1245,27 @@ export namespace Protocol { */ extension: string; } - export type clientNodesChangedPayload = { + export type nodesChangedPayload = { + /** + * Identifier of canvas that changed. + */ + canvasId: CanvasId; + } + export type cssCanvasClientNodesChangedPayload = { + /** + * Identifier of canvas that changed. + */ + canvasId: CanvasId; + } + export type cssCanvasNamesChangedPayload = { /** * Identifier of canvas that changed. */ canvasId: CanvasId; + /** + * The CSS canvas identifiers, for canvases created with document.getCSSCanvasContext. + */ + cssCanvasNames: string[]; } export type recordingStartedPayload = { canvasId: CanvasId; @@ -1293,19 +1305,19 @@ export namespace Protocol { export type disableReturnValue = { } /** - * Gets the NodeId for the canvas node with the given CanvasId. + * Gets the NodeIds for the canvas nodes with the given CanvasId. */ - export type requestNodeParameters = { + export type requestNodesParameters = { /** * Canvas identifier. */ canvasId: CanvasId; } - export type requestNodeReturnValue = { + export type requestNodesReturnValue = { /** - * Node identifier for given canvas. + * Node identifiers for the given canvas. */ - nodeId: DOM.NodeId; + nodeIds: DOM.NodeId[]; } /** * Gets the data for the canvas node with the given CanvasId. @@ -1323,17 +1335,13 @@ export namespace Protocol { content: string; } /** - * Gets all -webkit-canvas nodes or active HTMLCanvasElement for a WebGPUDevice. + * Gets all nodes using a -webkit-canvas image associated with the given CanvasId. */ - export type requestClientNodesParameters = { + export type requestCSSCanvasClientNodesParameters = { canvasId: CanvasId; } - export type requestClientNodesReturnValue = { + export type requestCSSCanvasClientNodesReturnValue = { clientNodeIds: DOM.NodeId[]; - /** - * The CSS canvas identifiers, for canvases created with document.getCSSCanvasContext. - */ - cssCanvasNames: string[]; } /** * Resolves JavaScript canvas/device context object for given canvasId. @@ -7675,7 +7683,7 @@ the top of the viewport and Y increases as it proceeds towards the bottom of the */ export interface Frame { /** - * Information about an action made to the recorded object. Follows the structure [name, parameters, swizzleTypes, stackTrace, receiver, snapshot], where name is a string, parameters is an array, swizzleTypes is an array, stackTrace is a Console.StackTrace, receiver follows the structure [identifier, swizzleType] for the object that received the action, and snapshot is a data URL image of the current contents after this action. + * Information about an action made to the recorded object. Follows the structure [name, parameters, swizzleTypes, stackTrace, result, receiver, snapshot], where name is a string, parameters is an array, swizzleTypes is an array, stackTrace is a Console.StackTrace, result and receiver follow the structure [identifier, swizzleType] for the object returned by the action and the object that received it, respectively, and snapshot is a data URL image of the current contents after this action. */ actions: any[]; /** @@ -9199,7 +9207,9 @@ the top of the viewport and Y increases as it proceeds towards the bottom of the "Canvas.canvasSizeChanged": Canvas.canvasSizeChangedPayload; "Canvas.canvasMemoryChanged": Canvas.canvasMemoryChangedPayload; "Canvas.extensionEnabled": Canvas.extensionEnabledPayload; - "Canvas.clientNodesChanged": Canvas.clientNodesChangedPayload; + "Canvas.nodesChanged": Canvas.nodesChangedPayload; + "Canvas.cssCanvasClientNodesChanged": Canvas.cssCanvasClientNodesChangedPayload; + "Canvas.cssCanvasNamesChanged": Canvas.cssCanvasNamesChangedPayload; "Canvas.recordingStarted": Canvas.recordingStartedPayload; "Canvas.recordingProgress": Canvas.recordingProgressPayload; "Canvas.recordingFinished": Canvas.recordingFinishedPayload; @@ -9327,7 +9337,9 @@ the top of the viewport and Y increases as it proceeds towards the bottom of the ["Canvas.canvasSizeChanged"]: [Canvas.canvasSizeChangedPayload]; ["Canvas.canvasMemoryChanged"]: [Canvas.canvasMemoryChangedPayload]; ["Canvas.extensionEnabled"]: [Canvas.extensionEnabledPayload]; - ["Canvas.clientNodesChanged"]: [Canvas.clientNodesChangedPayload]; + ["Canvas.nodesChanged"]: [Canvas.nodesChangedPayload]; + ["Canvas.cssCanvasClientNodesChanged"]: [Canvas.cssCanvasClientNodesChangedPayload]; + ["Canvas.cssCanvasNamesChanged"]: [Canvas.cssCanvasNamesChangedPayload]; ["Canvas.recordingStarted"]: [Canvas.recordingStartedPayload]; ["Canvas.recordingProgress"]: [Canvas.recordingProgressPayload]; ["Canvas.recordingFinished"]: [Canvas.recordingFinishedPayload]; @@ -9467,9 +9479,9 @@ the top of the viewport and Y increases as it proceeds towards the bottom of the "CSS.setLayoutContextTypeChangedMode": CSS.setLayoutContextTypeChangedModeParameters; "Canvas.enable": Canvas.enableParameters; "Canvas.disable": Canvas.disableParameters; - "Canvas.requestNode": Canvas.requestNodeParameters; + "Canvas.requestNodes": Canvas.requestNodesParameters; "Canvas.requestContent": Canvas.requestContentParameters; - "Canvas.requestClientNodes": Canvas.requestClientNodesParameters; + "Canvas.requestCSSCanvasClientNodes": Canvas.requestCSSCanvasClientNodesParameters; "Canvas.resolveContext": Canvas.resolveContextParameters; "Canvas.setRecordingAutoCaptureFrameCount": Canvas.setRecordingAutoCaptureFrameCountParameters; "Canvas.startRecording": Canvas.startRecordingParameters; @@ -9777,9 +9789,9 @@ the top of the viewport and Y increases as it proceeds towards the bottom of the "CSS.setLayoutContextTypeChangedMode": CSS.setLayoutContextTypeChangedModeReturnValue; "Canvas.enable": Canvas.enableReturnValue; "Canvas.disable": Canvas.disableReturnValue; - "Canvas.requestNode": Canvas.requestNodeReturnValue; + "Canvas.requestNodes": Canvas.requestNodesReturnValue; "Canvas.requestContent": Canvas.requestContentReturnValue; - "Canvas.requestClientNodes": Canvas.requestClientNodesReturnValue; + "Canvas.requestCSSCanvasClientNodes": Canvas.requestCSSCanvasClientNodesReturnValue; "Canvas.resolveContext": Canvas.resolveContextReturnValue; "Canvas.setRecordingAutoCaptureFrameCount": Canvas.setRecordingAutoCaptureFrameCountReturnValue; "Canvas.startRecording": Canvas.startRecordingReturnValue; From 535d8f10cd5ab465260f331c180b773f5600d51a Mon Sep 17 00:00:00 2001 From: "microsoft-playwright-automation[bot]" <203992400+microsoft-playwright-automation[bot]@users.noreply.github.com> Date: Thu, 13 Aug 2026 12:41:14 -0700 Subject: [PATCH 9/9] feat(firefox): roll to r1540 (#42229) --- browser_patches/firefox/juggler/content/Runtime.js | 4 ++++ browser_patches/firefox/juggler/content/WorkerMain.js | 4 ++++ packages/playwright-core/browsers.json | 2 +- 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/browser_patches/firefox/juggler/content/Runtime.js b/browser_patches/firefox/juggler/content/Runtime.js index a29af41b2038c..46cc4514b9b4a 100644 --- a/browser_patches/firefox/juggler/content/Runtime.js +++ b/browser_patches/firefox/juggler/content/Runtime.js @@ -56,6 +56,10 @@ const disallowedMessageCategories = new Set([ class Runtime { constructor(isWorker = false) { this._debugger = new Debugger(); + // A debuggee global with these flags unset is pinned to the debuggable wasm/asm.js + // baseline tier, with the optimizing compiler disabled entirely. + this._debugger.allowUnobservedWasm = true; + this._debugger.allowUnobservedAsmJS = true; this._pendingPromises = new Map(); this._executionContexts = new Map(); this._windowToExecutionContext = new Map(); diff --git a/browser_patches/firefox/juggler/content/WorkerMain.js b/browser_patches/firefox/juggler/content/WorkerMain.js index 99a6623e7623f..555a97a874e02 100644 --- a/browser_patches/firefox/juggler/content/WorkerMain.js +++ b/browser_patches/firefox/juggler/content/WorkerMain.js @@ -22,6 +22,10 @@ const runtime = new Runtime(true /* isWorker */); // Create execution context in the runtime only when the script // source was actually evaluated in it. const dbg = new Debugger(global); + // A debuggee global with these flags unset is pinned to the debuggable wasm/asm.js + // baseline tier, with the optimizing compiler disabled entirely. + dbg.allowUnobservedWasm = true; + dbg.allowUnobservedAsmJS = true; if (dbg.findScripts({global}).length) { runtime.createExecutionContext(null /* domWindow */, global, {}); } else { diff --git a/packages/playwright-core/browsers.json b/packages/playwright-core/browsers.json index e402d597f8b61..a4981d493dfdd 100644 --- a/packages/playwright-core/browsers.json +++ b/packages/playwright-core/browsers.json @@ -17,7 +17,7 @@ }, { "name": "firefox", - "revision": "1539", + "revision": "1540", "installByDefault": true, "browserVersion": "153.0", "title": "Firefox"