From 8d39c44b69cb565d3cb951b6614b757545b8b2b1 Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Wed, 15 Jan 2025 14:38:34 -0800 Subject: [PATCH 01/19] docs: improve toHaveClass paramter description (#34345) --- docs/src/api/class-locatorassertions.md | 29 ++++++++++++------------- packages/playwright/types/test.d.ts | 13 ++++++----- tests/page/page-check.spec.ts | 7 +++--- 3 files changed, 26 insertions(+), 23 deletions(-) diff --git a/docs/src/api/class-locatorassertions.md b/docs/src/api/class-locatorassertions.md index cb5f56027c..8630051b8b 100644 --- a/docs/src/api/class-locatorassertions.md +++ b/docs/src/api/class-locatorassertions.md @@ -1413,49 +1413,48 @@ Attribute name. * langs: - alias-java: hasClass -Ensures the [Locator] points to an element with given CSS classes. This needs to be a full match -or using a relaxed regular expression. +Ensures the [Locator] points to an element with given CSS classes. When a string is provided, it must fully match the element's `class` attribute. To match individual classes or perform partial matches, use a regular expression: **Usage** ```html -
+
``` ```js const locator = page.locator('#component'); -await expect(locator).toHaveClass(/selected/); -await expect(locator).toHaveClass('selected row'); +await expect(locator).toHaveClass('middle selected row'); +await expect(locator).toHaveClass(/(^|\s)selected(\s|$)/); ``` ```java -assertThat(page.locator("#component")).hasClass(Pattern.compile("selected")); -assertThat(page.locator("#component")).hasClass("selected row"); +assertThat(page.locator("#component")).hasClass(Pattern.compile("(^|\\s)selected(\\s|$)")); +assertThat(page.locator("#component")).hasClass("middle selected row"); ``` ```python async from playwright.async_api import expect locator = page.locator("#component") -await expect(locator).to_have_class(re.compile(r"selected")) -await expect(locator).to_have_class("selected row") +await expect(locator).to_have_class(re.compile(r"(^|\\s)selected(\\s|$)")) +await expect(locator).to_have_class("middle selected row") ``` ```python sync from playwright.sync_api import expect locator = page.locator("#component") -expect(locator).to_have_class(re.compile(r"selected")) -expect(locator).to_have_class("selected row") +expect(locator).to_have_class(re.compile(r"(^|\\s)selected(\\s|$)")) +expect(locator).to_have_class("middle selected row") ``` ```csharp var locator = Page.Locator("#component"); -await Expect(locator).ToHaveClassAsync(new Regex("selected")); -await Expect(locator).ToHaveClassAsync("selected row"); +await Expect(locator).ToHaveClassAsync(new Regex("(^|\\s)selected(\\s|$)")); +await Expect(locator).ToHaveClassAsync("middle selected row"); ``` -Note that if array is passed as an expected value, entire lists of elements can be asserted: +When an array is passed, the method asserts that the list of elements located matches the corresponding list of expected class values. Each element's class attribute is matched against the corresponding string or regular expression in the array: ```js const locator = page.locator('list > .component'); @@ -2264,7 +2263,7 @@ expect(page.locator('body')).to_match_aria_snapshot(path='/path/to/snapshot.yml' ``` ```csharp -await Expect(page.Locator("body")).ToMatchAriaSnapshotAsync(new { Path = "/path/to/snapshot.yml" }); +await Expect(page.Locator("body")).ToMatchAriaSnapshotAsync(new { Path = "/path/to/snapshot.yml" }); ``` ```java diff --git a/packages/playwright/types/test.d.ts b/packages/playwright/types/test.d.ts index 501d1bcdb1..83306e932b 100644 --- a/packages/playwright/types/test.d.ts +++ b/packages/playwright/types/test.d.ts @@ -8207,21 +8207,24 @@ interface LocatorAssertions { /** * Ensures the [Locator](https://playwright.dev/docs/api/class-locator) points to an element with given CSS classes. - * This needs to be a full match or using a relaxed regular expression. + * When a string is provided, it must fully match the element's `class` attribute. To match individual classes or + * perform partial matches, use a regular expression: * * **Usage** * * ```html - *
+ *
* ``` * * ```js * const locator = page.locator('#component'); - * await expect(locator).toHaveClass(/selected/); - * await expect(locator).toHaveClass('selected row'); + * await expect(locator).toHaveClass('middle selected row'); + * await expect(locator).toHaveClass(/(^|\s)selected(\s|$)/); * ``` * - * Note that if array is passed as an expected value, entire lists of elements can be asserted: + * When an array is passed, the method asserts that the list of elements located matches the corresponding list of + * expected class values. Each element's class attribute is matched against the corresponding string or regular + * expression in the array: * * ```js * const locator = page.locator('list > .component'); diff --git a/tests/page/page-check.spec.ts b/tests/page/page-check.spec.ts index 01b00ddc55..42946ce555 100644 --- a/tests/page/page-check.spec.ts +++ b/tests/page/page-check.spec.ts @@ -18,9 +18,10 @@ import { test as it, expect } from './pageTest'; it('should check the box @smoke', async ({ page }) => { - await page.setContent(``); - await page.check('input'); - expect(await page.evaluate(() => window['checkbox'].checked)).toBe(true); + await page.setContent(`
`); + const locator = page.locator('#component'); + await expect(locator).toHaveClass(/(^|\s)selected(\s|$)/); + await expect(locator).toHaveClass('middle selected row'); }); it('should not check the checked box', async ({ page }) => { From 00bb17751b9c67f41d99d242ca0dc131f7413ba9 Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Wed, 15 Jan 2025 17:37:33 -0800 Subject: [PATCH 02/19] chore: delete recorder in traceviewer experiment (#34347) --- packages/playwright-core/src/cli/program.ts | 1 - .../playwright-core/src/protocol/validator.ts | 1 - .../src/server/browserContext.ts | 2 +- .../dispatchers/browserContextDispatcher.ts | 13 +- .../playwright-core/src/server/recorder.ts | 14 +- .../src/server/recorder/DEPS.list | 3 - .../src/server/recorder/contextRecorder.ts | 10 +- .../server/recorder/recorderInTraceViewer.ts | 126 -------- packages/protocol/src/channels.d.ts | 2 - packages/protocol/src/protocol.yml | 5 - packages/trace-viewer/recorder.html | 28 -- packages/trace-viewer/src/DEPS.list | 4 - packages/trace-viewer/src/recorder.tsx | 41 --- .../trace-viewer/src/ui/recorder/DEPS.list | 5 - .../src/ui/recorder/actionListView.tsx | 62 ---- .../src/ui/recorder/backendContext.tsx | 118 ------- .../src/ui/recorder/modelContext.tsx | 71 ----- .../src/ui/recorder/recorderView.css | 15 - .../src/ui/recorder/recorderView.tsx | 299 ------------------ packages/trace-viewer/vite.config.ts | 1 - tests/config/testModeFixtures.ts | 2 - tests/library/inspector/cli-codegen-1.spec.ts | 3 +- tests/library/inspector/cli-codegen-2.spec.ts | 10 +- tests/library/inspector/cli-codegen-3.spec.ts | 1 - .../inspector/cli-codegen-aria.spec.ts | 1 - .../cli-codegen-pick-locator.spec.ts | 1 - tests/library/inspector/inspectorTest.ts | 9 +- tests/library/playwright.config.ts | 12 - 28 files changed, 17 insertions(+), 843 deletions(-) delete mode 100644 packages/playwright-core/src/server/recorder/recorderInTraceViewer.ts delete mode 100644 packages/trace-viewer/recorder.html delete mode 100644 packages/trace-viewer/src/recorder.tsx delete mode 100644 packages/trace-viewer/src/ui/recorder/DEPS.list delete mode 100644 packages/trace-viewer/src/ui/recorder/actionListView.tsx delete mode 100644 packages/trace-viewer/src/ui/recorder/backendContext.tsx delete mode 100644 packages/trace-viewer/src/ui/recorder/modelContext.tsx delete mode 100644 packages/trace-viewer/src/ui/recorder/recorderView.css delete mode 100644 packages/trace-viewer/src/ui/recorder/recorderView.tsx diff --git a/packages/playwright-core/src/cli/program.ts b/packages/playwright-core/src/cli/program.ts index 5cd941d5d4..a69414f2b2 100644 --- a/packages/playwright-core/src/cli/program.ts +++ b/packages/playwright-core/src/cli/program.ts @@ -595,7 +595,6 @@ async function codegen(options: Options & { target: string, output?: string, tes device: options.device, saveStorage: options.saveStorage, mode: 'recording', - codegenMode: process.env.PW_RECORDER_IS_TRACE_VIEWER ? 'trace-events' : 'actions', testIdAttributeName, outputFile: outputFile ? path.resolve(outputFile) : undefined, handleSIGINT: false, diff --git a/packages/playwright-core/src/protocol/validator.ts b/packages/playwright-core/src/protocol/validator.ts index 9b14551fb8..50e8b4f02a 100644 --- a/packages/playwright-core/src/protocol/validator.ts +++ b/packages/playwright-core/src/protocol/validator.ts @@ -970,7 +970,6 @@ scheme.BrowserContextPauseResult = tOptional(tObject({})); scheme.BrowserContextEnableRecorderParams = tObject({ language: tOptional(tString), mode: tOptional(tEnum(['inspecting', 'recording'])), - codegenMode: tOptional(tEnum(['actions', 'trace-events'])), pauseOnNextStatement: tOptional(tBoolean), testIdAttributeName: tOptional(tString), launchOptions: tOptional(tAny), diff --git a/packages/playwright-core/src/server/browserContext.ts b/packages/playwright-core/src/server/browserContext.ts index 8a835d3726..ce10daf013 100644 --- a/packages/playwright-core/src/server/browserContext.ts +++ b/packages/playwright-core/src/server/browserContext.ts @@ -130,7 +130,7 @@ export abstract class BrowserContext extends SdkObject { // When PWDEBUG=1, show inspector for each context. if (debugMode() === 'inspector') - await Recorder.show('actions', this, RecorderApp.factory(this), { pauseOnNextStatement: true }); + await Recorder.show(this, RecorderApp.factory(this), { pauseOnNextStatement: true }); // When paused, show inspector. if (this._debugger.isPaused()) diff --git a/packages/playwright-core/src/server/dispatchers/browserContextDispatcher.ts b/packages/playwright-core/src/server/dispatchers/browserContextDispatcher.ts index 3579f4b3bb..0dcfb23e0a 100644 --- a/packages/playwright-core/src/server/dispatchers/browserContextDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/browserContextDispatcher.ts @@ -39,7 +39,6 @@ import type { Dialog } from '../dialog'; import type { ConsoleMessage } from '../console'; import { serializeError } from '../errors'; import { ElementHandleDispatcher } from './elementHandlerDispatcher'; -import { RecorderInTraceViewer } from '../recorder/recorderInTraceViewer'; import { RecorderApp } from '../recorder/recorderApp'; import { WebSocketRouteDispatcher } from './webSocketRouteDispatcher'; @@ -301,17 +300,7 @@ export class BrowserContextDispatcher extends Dispatcher { - if (params.codegenMode === 'trace-events') { - await this._context.tracing.start({ - name: 'trace', - snapshots: true, - screenshots: true, - live: true, - }); - await Recorder.show('trace-events', this._context, RecorderInTraceViewer.factory(this._context), params); - } else { - await Recorder.show('actions', this._context, RecorderApp.factory(this._context), params); - } + await Recorder.show(this._context, RecorderApp.factory(this._context), params); } async pause(params: channels.BrowserContextPauseParams, metadata: CallMetadata) { diff --git a/packages/playwright-core/src/server/recorder.ts b/packages/playwright-core/src/server/recorder.ts index 115639223c..f763d5491b 100644 --- a/packages/playwright-core/src/server/recorder.ts +++ b/packages/playwright-core/src/server/recorder.ts @@ -54,33 +54,33 @@ export class Recorder implements InstrumentationListener, IRecorder { static async showInspector(context: BrowserContext, params: channels.BrowserContextEnableRecorderParams, recorderAppFactory: IRecorderAppFactory) { if (isUnderTest()) params.language = process.env.TEST_INSPECTOR_LANGUAGE; - return await Recorder.show('actions', context, recorderAppFactory, params); + return await Recorder.show(context, recorderAppFactory, params); } static showInspectorNoReply(context: BrowserContext, recorderAppFactory: IRecorderAppFactory) { Recorder.showInspector(context, {}, recorderAppFactory).catch(() => {}); } - static show(codegenMode: 'actions' | 'trace-events', context: BrowserContext, recorderAppFactory: IRecorderAppFactory, params: channels.BrowserContextEnableRecorderParams): Promise { + static show(context: BrowserContext, recorderAppFactory: IRecorderAppFactory, params: channels.BrowserContextEnableRecorderParams): Promise { let recorderPromise = (context as any)[recorderSymbol] as Promise; if (!recorderPromise) { - recorderPromise = Recorder._create(codegenMode, context, recorderAppFactory, params); + recorderPromise = Recorder._create(context, recorderAppFactory, params); (context as any)[recorderSymbol] = recorderPromise; } return recorderPromise; } - private static async _create(codegenMode: 'actions' | 'trace-events', context: BrowserContext, recorderAppFactory: IRecorderAppFactory, params: channels.BrowserContextEnableRecorderParams = {}): Promise { - const recorder = new Recorder(codegenMode, context, params); + private static async _create(context: BrowserContext, recorderAppFactory: IRecorderAppFactory, params: channels.BrowserContextEnableRecorderParams = {}): Promise { + const recorder = new Recorder(context, params); const recorderApp = await recorderAppFactory(recorder); await recorder._install(recorderApp); return recorder; } - constructor(codegenMode: 'actions' | 'trace-events', context: BrowserContext, params: channels.BrowserContextEnableRecorderParams) { + constructor(context: BrowserContext, params: channels.BrowserContextEnableRecorderParams) { this._mode = params.mode || 'none'; this.handleSIGINT = params.handleSIGINT; - this._contextRecorder = new ContextRecorder(codegenMode, context, params, {}); + this._contextRecorder = new ContextRecorder(context, params, {}); this._context = context; this._omitCallTracking = !!params.omitCallTracking; this._debugger = context.debugger(); diff --git a/packages/playwright-core/src/server/recorder/DEPS.list b/packages/playwright-core/src/server/recorder/DEPS.list index 85ae7c9152..b130c181dc 100644 --- a/packages/playwright-core/src/server/recorder/DEPS.list +++ b/packages/playwright-core/src/server/recorder/DEPS.list @@ -10,6 +10,3 @@ ../../utils/** ../../utilsBundle.ts ../../zipBundle.ts - -[recorderInTraceViewer.ts] -../trace/viewer/traceViewer.ts diff --git a/packages/playwright-core/src/server/recorder/contextRecorder.ts b/packages/playwright-core/src/server/recorder/contextRecorder.ts index 9b641e96f8..292271acd5 100644 --- a/packages/playwright-core/src/server/recorder/contextRecorder.ts +++ b/packages/playwright-core/src/server/recorder/contextRecorder.ts @@ -54,11 +54,9 @@ export class ContextRecorder extends EventEmitter { private _throttledOutputFile: ThrottledFile | null = null; private _orderedLanguages: LanguageGenerator[] = []; private _listeners: RegisteredListener[] = []; - private _codegenMode: 'actions' | 'trace-events'; - constructor(codegenMode: 'actions' | 'trace-events', context: BrowserContext, params: channels.BrowserContextEnableRecorderParams, delegate: ContextRecorderDelegate) { + constructor(context: BrowserContext, params: channels.BrowserContextEnableRecorderParams, delegate: ContextRecorderDelegate) { super(); - this._codegenMode = codegenMode; this._context = context; this._params = params; this._delegate = delegate; @@ -150,12 +148,6 @@ export class ContextRecorder extends EventEmitter { setEnabled(enabled: boolean) { this._collection.setEnabled(enabled); - if (this._codegenMode === 'trace-events') { - if (enabled) - this._context.tracing.startChunk({ name: 'trace', title: 'trace' }).catch(() => {}); - else - this._context.tracing.stopChunk({ mode: 'discard' }).catch(() => {}); - } } dispose() { diff --git a/packages/playwright-core/src/server/recorder/recorderInTraceViewer.ts b/packages/playwright-core/src/server/recorder/recorderInTraceViewer.ts deleted file mode 100644 index fcfd0a36c5..0000000000 --- a/packages/playwright-core/src/server/recorder/recorderInTraceViewer.ts +++ /dev/null @@ -1,126 +0,0 @@ -/** - * 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 path from 'path'; -import type { CallLog, ElementInfo, Mode, Source } from '@recorder/recorderTypes'; -import { EventEmitter } from 'events'; -import type { IRecorder, IRecorderApp, IRecorderAppFactory } from './recorderFrontend'; -import { installRootRedirect, openTraceViewerApp, startTraceViewerServer } from '../trace/viewer/traceViewer'; -import type { TraceViewerServerOptions } from '../trace/viewer/traceViewer'; -import type { BrowserContext } from '../browserContext'; -import type { HttpServer, Transport } from '../../utils/httpServer'; -import type { Page } from '../page'; -import { ManualPromise } from '../../utils/manualPromise'; -import type * as actions from '@recorder/actions'; - -export class RecorderInTraceViewer extends EventEmitter implements IRecorderApp { - readonly wsEndpointForTest: string | undefined; - private _transport: RecorderTransport; - private _tracePage: Page; - private _traceServer: HttpServer; - - static factory(context: BrowserContext): IRecorderAppFactory { - return async (recorder: IRecorder) => { - const transport = new RecorderTransport(); - const trace = path.join(context._browser.options.tracesDir, 'trace'); - const { wsEndpointForTest, tracePage, traceServer } = await openApp(trace, { transport, headless: !context._browser.options.headful }); - return new RecorderInTraceViewer(transport, tracePage, traceServer, wsEndpointForTest); - }; - } - - constructor(transport: RecorderTransport, tracePage: Page, traceServer: HttpServer, wsEndpointForTest: string | undefined) { - super(); - this._transport = transport; - this._transport.eventSink.resolve(this); - this._tracePage = tracePage; - this._traceServer = traceServer; - this.wsEndpointForTest = wsEndpointForTest; - this._tracePage.once('close', () => { - this.close(); - }); - } - - async close(): Promise { - await this._tracePage.context().close({ reason: 'Recorder window closed' }); - await this._traceServer.stop(); - } - - async setPaused(paused: boolean): Promise { - this._transport.deliverEvent('setPaused', { paused }); - } - - async setMode(mode: Mode): Promise { - this._transport.deliverEvent('setMode', { mode }); - } - - async setRunningFile(file: string | undefined): Promise { - this._transport.deliverEvent('setRunningFile', { file }); - } - - async elementPicked(elementInfo: ElementInfo, userGesture?: boolean): Promise { - this._transport.deliverEvent('elementPicked', { elementInfo, userGesture }); - } - - async updateCallLogs(callLogs: CallLog[]): Promise { - this._transport.deliverEvent('updateCallLogs', { callLogs }); - } - - async setSources(sources: Source[]): Promise { - this._transport.deliverEvent('setSources', { sources }); - if (process.env.PWTEST_CLI_IS_UNDER_TEST && sources.length) { - if ((process as any)._didSetSourcesForTest(sources[0].text)) - this.close(); - } - } - - async setActions(actions: actions.ActionInContext[], sources: Source[]): Promise { - this._transport.deliverEvent('setActions', { actions, sources }); - } -} - -async function openApp(trace: string, options?: TraceViewerServerOptions & { headless?: boolean }): Promise<{ wsEndpointForTest: string | undefined, tracePage: Page, traceServer: HttpServer }> { - const traceServer = await startTraceViewerServer(options); - await installRootRedirect(traceServer, [trace], { ...options, webApp: 'recorder.html' }); - const page = await openTraceViewerApp(traceServer.urlPrefix('precise'), 'chromium', options); - return { wsEndpointForTest: page.context()._browser.options.wsEndpoint, tracePage: page, traceServer }; -} - -class RecorderTransport implements Transport { - private _connected = new ManualPromise(); - readonly eventSink = new ManualPromise(); - - constructor() { - } - - onconnect() { - this._connected.resolve(); - } - - async dispatch(method: string, params: any): Promise { - const eventSink = await this.eventSink; - eventSink.emit('event', { event: method, params }); - } - - onclose() { - } - - deliverEvent(method: string, params: any) { - this._connected.then(() => this.sendEvent?.(method, params)); - } - - sendEvent?: (method: string, params: any) => void; - close?: () => void; -} diff --git a/packages/protocol/src/channels.d.ts b/packages/protocol/src/channels.d.ts index 6f9e36f0c3..526cc599ab 100644 --- a/packages/protocol/src/channels.d.ts +++ b/packages/protocol/src/channels.d.ts @@ -1772,7 +1772,6 @@ export type BrowserContextPauseResult = void; export type BrowserContextEnableRecorderParams = { language?: string, mode?: 'inspecting' | 'recording', - codegenMode?: 'actions' | 'trace-events', pauseOnNextStatement?: boolean, testIdAttributeName?: string, launchOptions?: any, @@ -1786,7 +1785,6 @@ export type BrowserContextEnableRecorderParams = { export type BrowserContextEnableRecorderOptions = { language?: string, mode?: 'inspecting' | 'recording', - codegenMode?: 'actions' | 'trace-events', pauseOnNextStatement?: boolean, testIdAttributeName?: string, launchOptions?: any, diff --git a/packages/protocol/src/protocol.yml b/packages/protocol/src/protocol.yml index e8b9746b41..df54dcbe1c 100644 --- a/packages/protocol/src/protocol.yml +++ b/packages/protocol/src/protocol.yml @@ -1198,11 +1198,6 @@ BrowserContext: literals: - inspecting - recording - codegenMode: - type: enum? - literals: - - actions - - trace-events pauseOnNextStatement: boolean? testIdAttributeName: string? launchOptions: json? diff --git a/packages/trace-viewer/recorder.html b/packages/trace-viewer/recorder.html deleted file mode 100644 index c33d6586e5..0000000000 --- a/packages/trace-viewer/recorder.html +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - - - Playwright Recorder - - -
- - - diff --git a/packages/trace-viewer/src/DEPS.list b/packages/trace-viewer/src/DEPS.list index 3d486b5452..f52c0a024e 100644 --- a/packages/trace-viewer/src/DEPS.list +++ b/packages/trace-viewer/src/DEPS.list @@ -6,7 +6,3 @@ ui/ [sw-main.ts] sw/** - - -[recorder.tsx] -ui/recorder/** diff --git a/packages/trace-viewer/src/recorder.tsx b/packages/trace-viewer/src/recorder.tsx deleted file mode 100644 index 5e6b9764e3..0000000000 --- a/packages/trace-viewer/src/recorder.tsx +++ /dev/null @@ -1,41 +0,0 @@ -/** - * 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 '@web/common.css'; -import { applyTheme } from '@web/theme'; -import '@web/third_party/vscode/codicon.css'; -import * as ReactDOM from 'react-dom/client'; -import { RecorderView } from './ui/recorder/recorderView'; - -(async () => { - applyTheme(); - - if (window.location.protocol !== 'file:') { - if (!navigator.serviceWorker) - throw new Error(`Service workers are not supported.\nMake sure to serve the Recorder (${window.location}) via HTTPS or localhost.`); - navigator.serviceWorker.register('sw.bundle.js'); - if (!navigator.serviceWorker.controller) { - await new Promise(f => { - navigator.serviceWorker.oncontrollerchange = () => f(); - }); - } - - // Keep SW running. - setInterval(function() { fetch('ping'); }, 10000); - } - - ReactDOM.createRoot(document.querySelector('#root')!).render(); -})(); diff --git a/packages/trace-viewer/src/ui/recorder/DEPS.list b/packages/trace-viewer/src/ui/recorder/DEPS.list deleted file mode 100644 index a504a7dba1..0000000000 --- a/packages/trace-viewer/src/ui/recorder/DEPS.list +++ /dev/null @@ -1,5 +0,0 @@ -[*] -@isomorphic/** -@trace/** -@web/** -../** diff --git a/packages/trace-viewer/src/ui/recorder/actionListView.tsx b/packages/trace-viewer/src/ui/recorder/actionListView.tsx deleted file mode 100644 index 8e9fa0df45..0000000000 --- a/packages/trace-viewer/src/ui/recorder/actionListView.tsx +++ /dev/null @@ -1,62 +0,0 @@ -/* - 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 type * as actionTypes from '@recorder/actions'; -import { ListView } from '@web/components/listView'; -import * as React from 'react'; -import '../actionList.css'; -import { traceParamsForAction } from '@isomorphic/recorderUtils'; -import { asLocator } from '@isomorphic/locatorGenerators'; -import type { Language } from '@isomorphic/locatorGenerators'; - -const ActionList = ListView; - -export const ActionListView: React.FC<{ - sdkLanguage: Language, - actions: actionTypes.ActionInContext[], - selectedAction: actionTypes.ActionInContext | undefined, - onSelectedAction: (action: actionTypes.ActionInContext | undefined) => void, -}> = ({ - sdkLanguage, - actions, - selectedAction, - onSelectedAction, -}) => { - const render = React.useCallback((action: actionTypes.ActionInContext) => { - return renderAction(sdkLanguage, action); - }, [sdkLanguage]); - return
- -
; -}; - -export const renderAction = (sdkLanguage: Language, action: actionTypes.ActionInContext) => { - const { method, apiName, params } = traceParamsForAction(action); - const locator = params.selector ? asLocator(sdkLanguage || 'javascript', params.selector) : undefined; - - return <> -
- {apiName} - {locator &&
{locator}
} - {method === 'goto' && params.url &&
{params.url}
} -
- ; -}; diff --git a/packages/trace-viewer/src/ui/recorder/backendContext.tsx b/packages/trace-viewer/src/ui/recorder/backendContext.tsx deleted file mode 100644 index 312281001e..0000000000 --- a/packages/trace-viewer/src/ui/recorder/backendContext.tsx +++ /dev/null @@ -1,118 +0,0 @@ -/* - 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 type * as actionTypes from '@recorder/actions'; -import type { Mode, Source } from '@recorder/recorderTypes'; -import * as React from 'react'; - -export const BackendContext = React.createContext(undefined); - -export const BackendProvider: React.FunctionComponent> = ({ guid, children }) => { - const [connection, setConnection] = React.useState(undefined); - const [mode, setMode] = React.useState('none'); - const [actions, setActions] = React.useState<{ actions: actionTypes.ActionInContext[], sources: Source[] }>({ actions: [], sources: [] }); - const callbacks = React.useRef({ setMode, setActions }); - - React.useEffect(() => { - const wsURL = new URL(`../${guid}`, window.location.toString()); - wsURL.protocol = (window.location.protocol === 'https:' ? 'wss:' : 'ws:'); - const webSocket = new WebSocket(wsURL.toString()); - setConnection(new Connection(webSocket, callbacks.current)); - return () => { - webSocket.close(); - }; - }, [guid]); - - const backend = React.useMemo(() => { - return connection ? { mode, actions: actions.actions, sources: actions.sources, connection } : undefined; - }, [actions, mode, connection]); - - return - {children} - ; -}; - -export type Backend = { - actions: actionTypes.ActionInContext[], - sources: Source[], - connection: Connection, -}; - -type ConnectionCallbacks = { - setMode: (mode: Mode) => void; - setActions: (data: { actions: actionTypes.ActionInContext[], sources: Source[] }) => void; -}; - -class Connection { - private _lastId = 0; - private _webSocket: WebSocket; - private _callbacks = new Map void, reject: (arg: Error) => void }>(); - private _options: ConnectionCallbacks; - - constructor(webSocket: WebSocket, options: ConnectionCallbacks) { - this._webSocket = webSocket; - this._callbacks = new Map(); - this._options = options; - - this._webSocket.addEventListener('message', event => { - const message = JSON.parse(event.data); - const { id, result, error, method, params } = message; - if (id) { - const callback = this._callbacks.get(id); - if (!callback) - return; - this._callbacks.delete(id); - if (error) - callback.reject(new Error(error)); - else - callback.resolve(result); - } else { - this._dispatchEvent(method, params); - } - }); - } - - setMode(mode: Mode) { - this._sendMessageNoReply('setMode', { mode }); - } - - private async _sendMessage(method: string, params?: any): Promise { - const id = ++this._lastId; - const message = { id, method, params }; - this._webSocket.send(JSON.stringify(message)); - return new Promise((resolve, reject) => { - this._callbacks.set(id, { resolve, reject }); - }); - } - - private _sendMessageNoReply(method: string, params?: any) { - this._sendMessage(method, params).catch(() => { }); - } - - private _dispatchEvent(method: string, params?: any) { - if (method === 'setMode') { - const { mode } = params as { mode: Mode }; - this._options.setMode(mode); - } - if (method === 'setActions') { - const { actions, sources } = params as { actions: actionTypes.ActionInContext[], sources: Source[] }; - this._options.setActions({ actions: actions.filter(a => a.action.name !== 'openPage' && a.action.name !== 'closePage'), sources }); - (window as any).playwrightSourcesEchoForTest = sources; - } - } -} diff --git a/packages/trace-viewer/src/ui/recorder/modelContext.tsx b/packages/trace-viewer/src/ui/recorder/modelContext.tsx deleted file mode 100644 index 98f450361b..0000000000 --- a/packages/trace-viewer/src/ui/recorder/modelContext.tsx +++ /dev/null @@ -1,71 +0,0 @@ -/* - 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 { sha1 } from '@web/uiUtils'; -import * as React from 'react'; -import type { ContextEntry } from '../../types/entries'; -import { MultiTraceModel } from '../modelUtil'; - -export const ModelContext = React.createContext(undefined); - -export const ModelProvider: React.FunctionComponent> = ({ trace, children }) => { - const [model, setModel] = React.useState<{ model: MultiTraceModel, sha1: string } | undefined>(); - const [counter, setCounter] = React.useState(0); - const pollTimer = React.useRef(null); - - React.useEffect(() => { - if (pollTimer.current) - clearTimeout(pollTimer.current); - - // Start polling running test. - pollTimer.current = setTimeout(async () => { - try { - const result = await loadSingleTraceFile(trace); - if (result.sha1 !== model?.sha1) - setModel(result); - } catch { - setModel(undefined); - } finally { - setCounter(counter + 1); - } - }, 500); - return () => { - if (pollTimer.current) - clearTimeout(pollTimer.current); - }; - }, [counter, model, trace]); - - return - {children} - ; -}; - -async function loadSingleTraceFile(url: string): Promise<{ model: MultiTraceModel, sha1: string }> { - const params = new URLSearchParams(); - params.set('trace', url); - params.set('limit', '1'); - const response = await fetch(`contexts?${params.toString()}`); - const contextEntries = await response.json() as ContextEntry[]; - - const tokens: string[] = []; - for (const entry of contextEntries) { - entry.actions.forEach(a => tokens.push(a.type + '@' + a.startTime + '-' + a.endTime)); - entry.events.forEach(e => tokens.push(e.type + '@' + e.time)); - } - return { model: new MultiTraceModel(contextEntries), sha1: await sha1(tokens.join('|')) }; -} diff --git a/packages/trace-viewer/src/ui/recorder/recorderView.css b/packages/trace-viewer/src/ui/recorder/recorderView.css deleted file mode 100644 index ad03e78e7d..0000000000 --- a/packages/trace-viewer/src/ui/recorder/recorderView.css +++ /dev/null @@ -1,15 +0,0 @@ -/* - 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. -*/ diff --git a/packages/trace-viewer/src/ui/recorder/recorderView.tsx b/packages/trace-viewer/src/ui/recorder/recorderView.tsx deleted file mode 100644 index 93db2b917d..0000000000 --- a/packages/trace-viewer/src/ui/recorder/recorderView.tsx +++ /dev/null @@ -1,299 +0,0 @@ -/* - 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 type * as actionTypes from '@recorder/actions'; -import { SourceChooser } from '@web/components/sourceChooser'; -import { SplitView } from '@web/components/splitView'; -import type { TabbedPaneTabModel } from '@web/components/tabbedPane'; -import { TabbedPane } from '@web/components/tabbedPane'; -import { Toolbar } from '@web/components/toolbar'; -import { ToolbarButton, ToolbarSeparator } from '@web/components/toolbarButton'; -import { copy, useSetting } from '@web/uiUtils'; -import * as React from 'react'; -import { ConsoleTab, useConsoleTabModel } from '../consoleTab'; -import type { Boundaries } from '../geometry'; -import { InspectorTab } from '../inspectorTab'; -import type * as modelUtil from '../modelUtil'; -import type { SourceLocation } from '../modelUtil'; -import { NetworkTab, useNetworkTabModel } from '../networkTab'; -import { collectSnapshots, extendSnapshot, SnapshotView } from '../snapshotTab'; -import { SourceTab } from '../sourceTab'; -import { ModelContext, ModelProvider } from './modelContext'; -import './recorderView.css'; -import { ActionListView } from './actionListView'; -import { BackendContext, BackendProvider } from './backendContext'; -import type { Language } from '@isomorphic/locatorGenerators'; -import { SettingsToolbarButton } from '../settingsToolbarButton'; -import type { HighlightedElement } from '../snapshotTab'; - -export const RecorderView: React.FunctionComponent = () => { - const searchParams = new URLSearchParams(window.location.search); - const guid = searchParams.get('ws')!; - const trace = searchParams.get('trace') + '.json'; - - return - - - - ; -}; - -export const Workbench: React.FunctionComponent = () => { - const backend = React.useContext(BackendContext); - const model = React.useContext(ModelContext); - const [fileId, setFileId] = React.useState(); - const [selectedStartTime, setSelectedStartTime] = React.useState(undefined); - const [isInspecting, setIsInspecting] = React.useState(false); - const [highlightedElementInProperties, setHighlightedElementInProperties] = React.useState({ lastEdited: 'none' }); - const [highlightedElementInTrace, setHighlightedElementInTrace] = React.useState({ lastEdited: 'none' }); - const [traceCallId, setTraceCallId] = React.useState(); - - const setSelectedAction = React.useCallback((action: actionTypes.ActionInContext | undefined) => { - setSelectedStartTime(action?.startTime); - }, []); - - const selectedAction = React.useMemo(() => { - return backend?.actions.find(a => a.startTime === selectedStartTime); - }, [backend?.actions, selectedStartTime]); - - React.useEffect(() => { - const callId = model?.actions.find(a => a.endTime && a.endTime === selectedAction?.endTime)?.callId; - if (callId) - setTraceCallId(callId); - }, [model, selectedAction]); - - const source = React.useMemo(() => backend?.sources.find(s => s.id === fileId) || backend?.sources[0], [backend?.sources, fileId]); - const sourceLocation = React.useMemo(() => { - if (!source) - return undefined; - const sourceLocation: SourceLocation = { - file: '', - line: 0, - column: 0, - source: { - errors: [], - content: source.text - } - }; - return sourceLocation; - }, [source]); - - const sdkLanguage: Language = source?.language || 'javascript'; - - const { boundaries } = React.useMemo(() => { - const boundaries = { minimum: model?.startTime || 0, maximum: model?.endTime || 30000 }; - if (boundaries.minimum > boundaries.maximum) { - boundaries.minimum = 0; - boundaries.maximum = 30000; - } - // Leave some nice free space on the right hand side. - boundaries.maximum += (boundaries.maximum - boundaries.minimum) / 20; - return { boundaries }; - }, [model]); - - const elementPickedInTrace = React.useCallback((element: HighlightedElement) => { - setHighlightedElementInProperties(element); - setHighlightedElementInTrace({ lastEdited: 'none' }); - setIsInspecting(false); - }, []); - - const elementTypedInProperties = React.useCallback((element: HighlightedElement) => { - setHighlightedElementInTrace(element); - setHighlightedElementInProperties(element); - }, []); - - const actionList = ; - - const actionsTab: TabbedPaneTabModel = { - id: 'actions', - title: 'Actions', - component: actionList, - }; - - const toolbar = -
- { - setIsInspecting(!isInspecting); - }} /> - { - }} /> - { - }} /> - { - }} /> - - { - if (source?.text) - copy(source.text); - }}> -
-
Target:
- { - setFileId(fileId); - }} /> - -
; - - const sidebarTabbedPane = ; - const traceView = ; - const propertiesView = ; - - return
- - {toolbar} - {traceView} -
} - sidebar={propertiesView} - />} - sidebar={sidebarTabbedPane} - /> - ; -}; - -const PropertiesView: React.FunctionComponent<{ - sdkLanguage: Language, - boundaries: Boundaries, - setIsInspecting: (value: boolean) => void, - highlightedElement: HighlightedElement, - setHighlightedElement: (element: HighlightedElement) => void, - sourceLocation: modelUtil.SourceLocation | undefined, -}> = ({ - sdkLanguage, - boundaries, - setIsInspecting, - highlightedElement, - setHighlightedElement, - sourceLocation, -}) => { - const model = React.useContext(ModelContext); - const consoleModel = useConsoleTabModel(model, boundaries); - const networkModel = useNetworkTabModel(model, boundaries); - const sourceModel = React.useRef(new Map()); - const [selectedPropertiesTab, setSelectedPropertiesTab] = useSetting('recorderPropertiesTab', 'source'); - - const inspectorTab: TabbedPaneTabModel = { - id: 'inspector', - title: 'Locator', - render: () => , - }; - - const sourceTab: TabbedPaneTabModel = { - id: 'source', - title: 'Source', - render: () => - }; - const consoleTab: TabbedPaneTabModel = { - id: 'console', - title: 'Console', - count: consoleModel.entries.length, - render: () => - }; - const networkTab: TabbedPaneTabModel = { - id: 'network', - title: 'Network', - count: networkModel.resources.length, - render: () => - }; - - const tabs: TabbedPaneTabModel[] = [ - sourceTab, - inspectorTab, - consoleTab, - networkTab, - ]; - - return ; -}; - -const TraceView: React.FunctionComponent<{ - sdkLanguage: Language, - callId: string | undefined, - isInspecting: boolean; - setIsInspecting: (value: boolean) => void; - highlightedElement: HighlightedElement; - setHighlightedElement: (element: HighlightedElement) => void; -}> = ({ - sdkLanguage, - callId, - isInspecting, - setIsInspecting, - highlightedElement, - setHighlightedElement, -}) => { - const model = React.useContext(ModelContext); - - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const [shouldPopulateCanvasFromScreenshot, _] = useSetting('shouldPopulateCanvasFromScreenshot', false); - - const action = React.useMemo(() => { - return model?.actions.find(a => a.callId === callId); - }, [model, callId]); - - const snapshot = React.useMemo(() => { - const snapshot = collectSnapshots(action); - return snapshot.action || snapshot.after || snapshot.before; - }, [action]); - const snapshotUrls = React.useMemo(() => { - return snapshot ? extendSnapshot(snapshot, shouldPopulateCanvasFromScreenshot) : undefined; - }, [snapshot, shouldPopulateCanvasFromScreenshot]); - - return ; -}; diff --git a/packages/trace-viewer/vite.config.ts b/packages/trace-viewer/vite.config.ts index c26e020fde..00b367bbc8 100644 --- a/packages/trace-viewer/vite.config.ts +++ b/packages/trace-viewer/vite.config.ts @@ -46,7 +46,6 @@ export default defineConfig({ input: { index: path.resolve(__dirname, 'index.html'), uiMode: path.resolve(__dirname, 'uiMode.html'), - recorder: path.resolve(__dirname, 'recorder.html'), snapshot: path.resolve(__dirname, 'snapshot.html'), }, output: { diff --git a/tests/config/testModeFixtures.ts b/tests/config/testModeFixtures.ts index 1231a78260..6b6feff7c2 100644 --- a/tests/config/testModeFixtures.ts +++ b/tests/config/testModeFixtures.ts @@ -21,7 +21,6 @@ import * as playwrightLibrary from 'playwright-core'; export type TestModeWorkerOptions = { mode: TestModeName; - codegenMode: 'trace-events' | 'actions'; }; export type TestModeTestFixtures = { @@ -49,7 +48,6 @@ export const testModeTest = test.extend { await use((playwright as any)._toImpl); diff --git a/tests/library/inspector/cli-codegen-1.spec.ts b/tests/library/inspector/cli-codegen-1.spec.ts index 0fd5f69d0b..6936aeee41 100644 --- a/tests/library/inspector/cli-codegen-1.spec.ts +++ b/tests/library/inspector/cli-codegen-1.spec.ts @@ -19,7 +19,6 @@ import type { ConsoleMessage } from 'playwright'; test.describe('cli codegen', () => { test.skip(({ mode }) => mode !== 'default'); - test.skip(({ trace, codegenMode }) => trace === 'on' && codegenMode === 'trace-events'); test('should click', async ({ openRecorder }) => { const { page, recorder } = await openRecorder(); @@ -413,7 +412,7 @@ await page.GetByRole(AriaRole.Textbox).PressAsync("Shift+Enter");`); expect(messages[0].text()).toBe('press'); }); - test('should update selected element after pressing Tab', async ({ openRecorder, browserName, codegenMode }) => { + test('should update selected element after pressing Tab', async ({ openRecorder }) => { const { page, recorder } = await openRecorder(); await recorder.setContentAndWait(` diff --git a/tests/library/inspector/cli-codegen-2.spec.ts b/tests/library/inspector/cli-codegen-2.spec.ts index 47afcc2fab..920ecbdf76 100644 --- a/tests/library/inspector/cli-codegen-2.spec.ts +++ b/tests/library/inspector/cli-codegen-2.spec.ts @@ -20,7 +20,6 @@ import fs from 'fs'; test.describe('cli codegen', () => { test.skip(({ mode }) => mode !== 'default'); - test.skip(({ trace, codegenMode }) => trace === 'on' && codegenMode === 'trace-events'); test('should contain open page', async ({ openRecorder }) => { const { recorder } = await openRecorder(); @@ -310,8 +309,7 @@ await page.GetByRole(AriaRole.Button, new() { Name = "click me" }).ClickAsync(); } }); - test('should record open in a new tab with url', async ({ openRecorder, browserName, codegenMode }) => { - test.skip(codegenMode === 'trace-events'); + test('should record open in a new tab with url', async ({ openRecorder, browserName }) => { const { page, recorder } = await openRecorder(); await recorder.setContentAndWait(`link`); @@ -453,8 +451,7 @@ await page1.GotoAsync("about:blank?foo");`); await recorder.waitForOutput('JavaScript', `await page.goto('${server.PREFIX}/page2.html');`); }); - test('should --save-trace', async ({ runCLI, codegenMode }, testInfo) => { - test.skip(codegenMode === 'trace-events'); + test('should --save-trace', async ({ runCLI }, testInfo) => { const traceFileName = testInfo.outputPath('trace.zip'); const cli = runCLI([`--save-trace=${traceFileName}`], { autoExitWhen: ' ', @@ -463,8 +460,7 @@ await page1.GotoAsync("about:blank?foo");`); expect(fs.existsSync(traceFileName)).toBeTruthy(); }); - test('should save assets via SIGINT', async ({ runCLI, platform, codegenMode }, testInfo) => { - test.skip(codegenMode === 'trace-events'); + test('should save assets via SIGINT', async ({ runCLI, platform }, testInfo) => { test.skip(platform === 'win32', 'SIGINT not supported on Windows'); const traceFileName = testInfo.outputPath('trace.zip'); diff --git a/tests/library/inspector/cli-codegen-3.spec.ts b/tests/library/inspector/cli-codegen-3.spec.ts index 87c7e7bfec..8af5a76472 100644 --- a/tests/library/inspector/cli-codegen-3.spec.ts +++ b/tests/library/inspector/cli-codegen-3.spec.ts @@ -21,7 +21,6 @@ import type { Page } from '@playwright/test'; test.describe('cli codegen', () => { test.skip(({ mode }) => mode !== 'default'); - test.skip(({ trace, codegenMode }) => trace === 'on' && codegenMode === 'trace-events'); test('should click locator.first', async ({ openRecorder }) => { const { page, recorder } = await openRecorder(); diff --git a/tests/library/inspector/cli-codegen-aria.spec.ts b/tests/library/inspector/cli-codegen-aria.spec.ts index 354ca6495a..a11cf33342 100644 --- a/tests/library/inspector/cli-codegen-aria.spec.ts +++ b/tests/library/inspector/cli-codegen-aria.spec.ts @@ -19,7 +19,6 @@ import { roundBox } from '../../page/pageTest'; test.describe(() => { test.skip(({ mode }) => mode !== 'default'); - test.skip(({ trace, codegenMode }) => trace === 'on' && codegenMode === 'trace-events'); test('should generate aria snapshot', async ({ openRecorder }) => { const { recorder } = await openRecorder(); diff --git a/tests/library/inspector/cli-codegen-pick-locator.spec.ts b/tests/library/inspector/cli-codegen-pick-locator.spec.ts index 53d106b4c9..bd1a612842 100644 --- a/tests/library/inspector/cli-codegen-pick-locator.spec.ts +++ b/tests/library/inspector/cli-codegen-pick-locator.spec.ts @@ -19,7 +19,6 @@ import { roundBox } from '../../page/pageTest'; test.describe(() => { test.skip(({ mode }) => mode !== 'default'); - test.skip(({ trace, codegenMode }) => trace === 'on' && codegenMode === 'trace-events'); test('should inspect locator', async ({ openRecorder }) => { const { recorder } = await openRecorder(); diff --git a/tests/library/inspector/inspectorTest.ts b/tests/library/inspector/inspectorTest.ts index a90f73fcdf..b94bfc09a0 100644 --- a/tests/library/inspector/inspectorTest.ts +++ b/tests/library/inspector/inspectorTest.ts @@ -67,7 +67,7 @@ export const test = contextTest.extend({ }); }, - runCLI: async ({ childProcess, browserName, channel, headless, mode, launchOptions, codegenMode }, run, testInfo) => { + runCLI: async ({ childProcess, browserName, channel, headless, mode, launchOptions }, run, testInfo) => { testInfo.skip(mode.startsWith('service')); await run((cliArgs, { autoExitWhen } = {}) => { @@ -78,17 +78,15 @@ export const test = contextTest.extend({ args: cliArgs, executablePath: launchOptions.executablePath, autoExitWhen, - codegenMode }); }); }, - openRecorder: async ({ context, recorderPageGetter, codegenMode }, run) => { + openRecorder: async ({ context, recorderPageGetter }, run) => { await run(async (options?: { testIdAttributeName?: string }) => { await (context as any)._enableRecorder({ language: 'javascript', mode: 'recording', - codegenMode, ...options }); const page = await context.newPage(); @@ -235,7 +233,7 @@ export class Recorder { class CLIMock { process: TestChildProcess; - constructor(childProcess: CommonFixtures['childProcess'], options: { browserName: string, channel: string | undefined, headless: boolean | undefined, args: string[], executablePath: string | undefined, autoExitWhen: string | undefined, codegenMode?: 'trace-events' | 'actions'}) { + constructor(childProcess: CommonFixtures['childProcess'], options: { browserName: string, channel: string | undefined, headless: boolean | undefined, args: string[], executablePath: string | undefined, autoExitWhen: string | undefined}) { const nodeArgs = [ 'node', path.join(__dirname, '..', '..', '..', 'packages', 'playwright-core', 'cli.js'), @@ -248,7 +246,6 @@ class CLIMock { this.process = childProcess({ command: nodeArgs, env: { - PW_RECORDER_IS_TRACE_VIEWER: options.codegenMode === 'trace-events' ? '1' : undefined, PWTEST_CLI_AUTO_EXIT_WHEN: options.autoExitWhen, PWTEST_CLI_IS_UNDER_TEST: '1', PWTEST_CLI_HEADLESS: options.headless ? '1' : undefined, diff --git a/tests/library/playwright.config.ts b/tests/library/playwright.config.ts index 399eec1b6b..f588ee2f45 100644 --- a/tests/library/playwright.config.ts +++ b/tests/library/playwright.config.ts @@ -147,18 +147,6 @@ for (const browserName of browserNames) { testDir: path.join(testDir, 'page'), ...projectTemplate, }); - - // TODO: figure out reporting to flakiness dashboard (Problem: they get merged, we want to keep them separate) - // config.projects.push({ - // name: `${browserName}-codegen-mode-trace`, - // testDir: path.join(testDir, 'library'), - // testMatch: '**/cli-codegen-*.spec.ts', - // ...projectTemplate, - // use: { - // ...projectTemplate.use, - // codegenMode: 'trace-events', - // } - // }); } export default config; From 08af3a2f06d69839803f686f9eecad275eacc7ba Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Thu, 16 Jan 2025 08:27:48 -0800 Subject: [PATCH 03/19] chore: do not cache highlight agressively (#34349) --- .../src/server/injected/recorder/recorder.ts | 21 +++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/packages/playwright-core/src/server/injected/recorder/recorder.ts b/packages/playwright-core/src/server/injected/recorder/recorder.ts index 5a3f3d9a14..3df43f751e 100644 --- a/packages/playwright-core/src/server/injected/recorder/recorder.ts +++ b/packages/playwright-core/src/server/injected/recorder/recorder.ts @@ -1138,25 +1138,28 @@ export class Recorder { let highlight: HighlightModel | 'clear' | 'noop' = 'noop'; if (state.actionSelector !== this._lastHighlightedSelector) { - this._lastHighlightedSelector = state.actionSelector; const model = state.actionSelector ? querySelector(this.injectedScript, state.actionSelector, this.document) : null; highlight = model?.elements.length ? model : 'clear'; + this._lastHighlightedSelector = model?.elements.length ? state.actionSelector : undefined; } const ariaTemplateJSON = JSON.stringify(state.ariaTemplate); if (this._lastHighlightedAriaTemplateJSON !== ariaTemplateJSON) { - this._lastHighlightedAriaTemplateJSON = ariaTemplateJSON; const elements = state.ariaTemplate ? this.injectedScript.getAllByAria(this.document, state.ariaTemplate) : []; - if (elements.length) + if (elements.length) { highlight = { elements }; - else - highlight = 'clear'; + this._lastHighlightedAriaTemplateJSON = ariaTemplateJSON; + } else { + if (!this._lastHighlightedSelector) + highlight = 'clear'; + this._lastHighlightedAriaTemplateJSON = 'undefined'; + } } if (highlight === 'clear') this.clearHighlight(); else if (highlight !== 'noop') - this.updateHighlight(highlight, false); + this._updateHighlight(highlight, false); } clearHighlight() { @@ -1299,6 +1302,12 @@ export class Recorder { } updateHighlight(model: HighlightModel | null, userGesture: boolean) { + this._lastHighlightedSelector = undefined; + this._lastHighlightedAriaTemplateJSON = 'undefined'; + this._updateHighlight(model, userGesture); + } + + private _updateHighlight(model: HighlightModel | null, userGesture: boolean) { let tooltipText = model?.tooltipText; if (tooltipText === undefined && !model?.tooltipList && model?.selector) tooltipText = this.injectedScript.utils.asLocator(this.state.language, model.selector); From be6caed8df159ad15c6a9ed0eb23f6c1b9e65d79 Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Thu, 16 Jan 2025 18:53:32 +0000 Subject: [PATCH 04/19] chore: remove expectZone (#34312) --- .../playwright-core/src/utils/stackTrace.ts | 5 -- packages/playwright-core/src/utils/zones.ts | 2 +- packages/playwright/src/index.ts | 9 +-- packages/playwright/src/matchers/expect.ts | 12 +-- packages/playwright/src/worker/testInfo.ts | 9 +-- tests/playwright-test/test-step.spec.ts | 77 +++++++++++++++++-- 6 files changed, 81 insertions(+), 33 deletions(-) diff --git a/packages/playwright-core/src/utils/stackTrace.ts b/packages/playwright-core/src/utils/stackTrace.ts index 2e40968ebc..eba52a30f2 100644 --- a/packages/playwright-core/src/utils/stackTrace.ts +++ b/packages/playwright-core/src/utils/stackTrace.ts @@ -156,8 +156,3 @@ export function compressCallLog(log: string[]): string[] { } return lines; } - -export type ExpectZone = { - title: string; - stepId: string; -}; diff --git a/packages/playwright-core/src/utils/zones.ts b/packages/playwright-core/src/utils/zones.ts index 75612c8938..32664c3898 100644 --- a/packages/playwright-core/src/utils/zones.ts +++ b/packages/playwright-core/src/utils/zones.ts @@ -16,7 +16,7 @@ import { AsyncLocalStorage } from 'async_hooks'; -export type ZoneType = 'apiZone' | 'expectZone' | 'stepZone'; +export type ZoneType = 'apiZone' | 'stepZone'; class ZoneManager { private readonly _asyncLocalStorage = new AsyncLocalStorage(); diff --git a/packages/playwright/src/index.ts b/packages/playwright/src/index.ts index 29fabb22e8..83913c18dc 100644 --- a/packages/playwright/src/index.ts +++ b/packages/playwright/src/index.ts @@ -19,7 +19,6 @@ import * as path from 'path'; import type { APIRequestContext, BrowserContext, Browser, BrowserContextOptions, LaunchOptions, Page, Tracing, Video } from 'playwright-core'; import * as playwrightLibrary from 'playwright-core'; import { createGuid, debugMode, addInternalStackPrefix, isString, asLocator, jsonStringifyForceASCII, zones } from 'playwright-core/lib/utils'; -import type { ExpectZone } from 'playwright-core/lib/utils'; import type { Fixtures, PlaywrightTestArgs, PlaywrightTestOptions, PlaywrightWorkerArgs, PlaywrightWorkerOptions, ScreenshotMode, TestInfo, TestType, VideoMode } from '../types/test'; import type { TestInfoImpl, TestStepInternal } from './worker/testInfo'; import { rootTestType } from './common/testType'; @@ -264,12 +263,12 @@ const playwrightFixtures: Fixtures = ({ // Some special calls do not get into steps. if (!testInfo || data.apiName.includes('setTestIdAttribute') || data.apiName === 'tracing.groupEnd') return; - const expectZone = zones.zoneData('expectZone'); - if (expectZone) { + const zone = zones.zoneData('stepZone'); + if (zone && zone.category === 'expect') { // Display the internal locator._expect call under the name of the enclosing expect call, // and connect it to the existing expect step. - data.apiName = expectZone.title; - data.stepId = expectZone.stepId; + data.apiName = zone.title; + data.stepId = zone.stepId; return; } // In the general case, create a step for each api call and connect them through the stepId. diff --git a/packages/playwright/src/matchers/expect.ts b/packages/playwright/src/matchers/expect.ts index d4c3287d33..d382a4dbfd 100644 --- a/packages/playwright/src/matchers/expect.ts +++ b/packages/playwright/src/matchers/expect.ts @@ -19,7 +19,6 @@ import { createGuid, isString, pollAgainstDeadline } from 'playwright-core/lib/utils'; -import type { ExpectZone } from 'playwright-core/lib/utils'; import { toBeAttached, toBeChecked, @@ -315,9 +314,10 @@ class ExpectMetaInfoProxyHandler implements ProxyHandler { // out all the frames that belong to the test runner from caught runtime errors. const stackFrames = filteredStackTrace(captureRawStack()); - // Enclose toPass in a step to maintain async stacks, toPass matcher is always async. + // toPass and poll matchers can contain other steps, expects and API calls, + // so they behave like a retriable step. const stepInfo = { - category: 'expect', + category: (matcherName === 'toPass' || this._info.poll) ? 'step' : 'expect', title: trimLongString(title, 1024), params: args[0] ? { expected: args[0] } : undefined, infectParentStepsWithError: this._info.isSoft, @@ -345,11 +345,7 @@ class ExpectMetaInfoProxyHandler implements ProxyHandler { try { const callback = () => matcher.call(target, ...args); - // toPass and poll matchers can contain other steps, expects and API calls, - // so they behave like a retriable step. - const result = (matcherName === 'toPass' || this._info.poll) ? - zones.run('stepZone', step, callback) : - zones.run('expectZone', { title, stepId: step.stepId }, callback); + const result = zones.run('stepZone', step, callback); if (result instanceof Promise) return result.then(finalizer).catch(reportStepError); finalizer(); diff --git a/packages/playwright/src/worker/testInfo.ts b/packages/playwright/src/worker/testInfo.ts index 569e72c5dd..3eba797853 100644 --- a/packages/playwright/src/worker/testInfo.ts +++ b/packages/playwright/src/worker/testInfo.ts @@ -17,7 +17,6 @@ import fs from 'fs'; import path from 'path'; import { captureRawStack, monotonicTime, zones, sanitizeForFilePath, stringifyStackFrames } from 'playwright-core/lib/utils'; -import type { ExpectZone } from 'playwright-core/lib/utils'; import type { TestInfo, TestStatus, FullProject } from '../../types/test'; import type { AttachmentPayload, StepBeginPayload, StepEndPayload, TestInfoErrorImpl, WorkerInitParams } from '../common/ipc'; import type { TestCase } from '../common/test'; @@ -35,7 +34,7 @@ export interface TestStepInternal { attachmentIndices: number[]; stepId: string; title: string; - category: 'hook' | 'fixture' | 'test.step' | 'test.step.skip' | 'expect' | 'attach' | string; + category: string; location?: Location; boxedStack?: StackFrame[]; steps: TestStepInternal[]; @@ -195,7 +194,7 @@ export class TestInfoImpl implements TestInfo { this._attachmentsPush = this.attachments.push.bind(this.attachments); this.attachments.push = (...attachments: TestInfo['attachments']) => { for (const a of attachments) - this._attach(a, this._expectStepId() ?? this._parentStep()?.stepId); + this._attach(a, this._parentStep()?.stepId); return this.attachments.length; }; @@ -245,10 +244,6 @@ export class TestInfoImpl implements TestInfo { ?? this._findLastStageStep(this._steps); // If no parent step on stack, assume the current stage as parent. } - private _expectStepId() { - return zones.zoneData('expectZone')?.stepId; - } - _addStep(data: Omit, parentStep?: TestStepInternal): TestStepInternal { const stepId = `${data.category}@${++this._lastStepId}`; diff --git a/tests/playwright-test/test-step.spec.ts b/tests/playwright-test/test-step.spec.ts index ec04ef19e8..648cbeb52e 100644 --- a/tests/playwright-test/test-step.spec.ts +++ b/tests/playwright-test/test-step.spec.ts @@ -616,7 +616,7 @@ test('should not propagate errors from within toPass', async ({ runInlineTest }) expect(result.exitCode).toBe(0); expect(result.output).toBe(` hook |Before Hooks -expect |expect.toPass @ a.test.ts:7 +step |expect.toPass @ a.test.ts:7 expect | expect.toBe @ a.test.ts:6 expect | ↪ error: Error: expect(received).toBe(expected) // Object.is equality expect | expect.toBe @ a.test.ts:6 @@ -643,8 +643,8 @@ test('should show final toPass error', async ({ runInlineTest }) => { expect(result.exitCode).toBe(1); expect(stripAnsi(result.output)).toBe(` hook |Before Hooks -expect |expect.toPass @ a.test.ts:6 -expect |↪ error: Error: expect(received).toBe(expected) // Object.is equality +step |expect.toPass @ a.test.ts:6 +step |↪ error: Error: expect(received).toBe(expected) // Object.is equality expect | expect.toBe @ a.test.ts:5 expect | ↪ error: Error: expect(received).toBe(expected) // Object.is equality hook |After Hooks @@ -909,7 +909,7 @@ test('step inside expect.toPass', async ({ runInlineTest }) => { expect(stripAnsi(result.output)).toBe(` hook |Before Hooks test.step |step 1 @ a.test.ts:4 -expect | expect.toPass @ a.test.ts:11 +step | expect.toPass @ a.test.ts:11 test.step | step 2, attempt: 0 @ a.test.ts:7 test.step | ↪ error: Error: expect(received).toBe(expected) // Object.is equality expect | expect.toBe @ a.test.ts:9 @@ -956,7 +956,7 @@ fixture | fixture: context pw:api | browser.newContext fixture | fixture: page pw:api | browserContext.newPage -expect |expect.toPass @ a.test.ts:11 +step |expect.toPass @ a.test.ts:11 pw:api | page.goto(about:blank) @ a.test.ts:6 test.step | inner step attempt: 0 @ a.test.ts:7 test.step | ↪ error: Error: expect(received).toBe(expected) // Object.is equality @@ -1007,7 +1007,7 @@ fixture | fixture: context pw:api | browser.newContext fixture | fixture: page pw:api | browserContext.newPage -expect |expect.poll.toHaveLength @ a.test.ts:14 +step |expect.poll.toHaveLength @ a.test.ts:14 pw:api | page.goto(about:blank) @ a.test.ts:7 test.step | inner step attempt: 0 @ a.test.ts:8 expect | expect.toBe @ a.test.ts:10 @@ -1059,7 +1059,7 @@ pw:api | browser.newContext fixture | fixture: page pw:api | browserContext.newPage pw:api |page.setContent @ a.test.ts:4 -expect |expect.poll.toBe @ a.test.ts:13 +step |expect.poll.toBe @ a.test.ts:13 expect | expect.toHaveText @ a.test.ts:7 test.step | iteration 1 @ a.test.ts:9 expect | expect.toBeVisible @ a.test.ts:10 @@ -1565,3 +1565,66 @@ expect |expect.toBe @ a.test.ts:10 hook |After Hooks `); }); + +test('show api calls inside expects', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'reporter.ts': stepIndentReporter, + 'playwright.config.ts': `module.exports = { reporter: './reporter' };`, + 'a.test.ts': ` + import { test, expect as baseExpect } from '@playwright/test'; + + const expect = baseExpect.extend({ + async toBeInvisible(locator: Locator) { + try { + await expect.poll(() => locator.isVisible()).toBe(false); + return { name: 'toBeInvisible', pass: true, message: '' }; + } catch (e) { + return { name: 'toBeInvisible', pass: false, message: () => 'Expected to be invisible, got visible!' }; + } + }, + }); + + test('test', async ({ page }) => { + await page.setContent('
hello
'); + const promise = expect(page.locator('div')).toBeInvisible(); + await page.waitForTimeout(1100); + await page.setContent('
hello
'); + await promise; + }); + ` + }, { reporter: '' }); + + expect(result.exitCode).toBe(0); + expect(result.report.stats.expected).toBe(1); + expect(stripAnsi(result.output)).toBe(` +hook |Before Hooks +fixture | fixture: browser +pw:api | browserType.launch +fixture | fixture: context +pw:api | browser.newContext +fixture | fixture: page +pw:api | browserContext.newPage +pw:api |page.setContent @ a.test.ts:16 +expect |expect.toBeInvisible @ a.test.ts:17 +step | expect.poll.toBe @ a.test.ts:7 +pw:api | locator.isVisible(div) @ a.test.ts:7 +expect | expect.toBe @ a.test.ts:7 +expect | ↪ error: Error: expect(received).toBe(expected) // Object.is equality +pw:api | locator.isVisible(div) @ a.test.ts:7 +expect | expect.toBe @ a.test.ts:7 +expect | ↪ error: Error: expect(received).toBe(expected) // Object.is equality +pw:api | locator.isVisible(div) @ a.test.ts:7 +expect | expect.toBe @ a.test.ts:7 +expect | ↪ error: Error: expect(received).toBe(expected) // Object.is equality +pw:api | locator.isVisible(div) @ a.test.ts:7 +expect | expect.toBe @ a.test.ts:7 +expect | ↪ error: Error: expect(received).toBe(expected) // Object.is equality +pw:api | locator.isVisible(div) @ a.test.ts:7 +expect | expect.toBe @ a.test.ts:7 +pw:api |page.waitForTimeout @ a.test.ts:18 +pw:api |page.setContent @ a.test.ts:19 +hook |After Hooks +fixture | fixture: page +fixture | fixture: context +`); +}); From 84bbc5fd35b822e2dc0d94952846ca5922146628 Mon Sep 17 00:00:00 2001 From: Adam Gastineau Date: Thu, 16 Jan 2025 11:48:34 -0800 Subject: [PATCH 05/19] feat(trace-viewer): Render context string for most actions (#34292) --- packages/trace-viewer/src/ui/actionList.css | 9 +- packages/trace-viewer/src/ui/actionList.tsx | 175 +++++++++++++++++++- tests/library/trace-viewer.spec.ts | 55 ++++++ 3 files changed, 234 insertions(+), 5 deletions(-) diff --git a/packages/trace-viewer/src/ui/actionList.css b/packages/trace-viewer/src/ui/actionList.css index 10e3c39f98..0cb3cb5f54 100644 --- a/packages/trace-viewer/src/ui/actionList.css +++ b/packages/trace-viewer/src/ui/actionList.css @@ -70,13 +70,20 @@ flex: none; } -.action-selector { +.action-parameter { display: inline; flex: none; padding-left: 5px; +} + +.action-locator-parameter { color: var(--vscode-charts-orange); } +.action-generic-parameter { + color: var(--vscode-charts-purple); +} + .action-url { display: inline; flex: none; diff --git a/packages/trace-viewer/src/ui/actionList.tsx b/packages/trace-viewer/src/ui/actionList.tsx index 1deb8ecd88..87e78416b0 100644 --- a/packages/trace-viewer/src/ui/actionList.tsx +++ b/packages/trace-viewer/src/ui/actionList.tsx @@ -19,8 +19,7 @@ import { msToString } from '@web/uiUtils'; import * as React from 'react'; import './actionList.css'; import * as modelUtil from './modelUtil'; -import { asLocator } from '@isomorphic/locatorGenerators'; -import type { Language } from '@isomorphic/locatorGenerators'; +import { asLocator, type Language } from '@isomorphic/locatorGenerators'; import type { TreeState } from '@web/components/treeView'; import { TreeView } from '@web/components/treeView'; import type { ActionTraceEventInContext, ActionTreeItem } from './modelUtil'; @@ -116,9 +115,10 @@ export const renderAction = ( }) => { const { sdkLanguage, revealConsole, revealAttachment, isLive, showDuration, showBadges } = options; const { errors, warnings } = modelUtil.stats(action); - const locator = action.params.selector ? asLocator(sdkLanguage || 'javascript', action.params.selector) : undefined; const showAttachments = !!action.attachments?.length && !!revealAttachment; + const parameterString = actionParameterDisplayString(action, sdkLanguage || 'javascript'); + let time: string = ''; if (action.endTime) time = msToString(action.endTime - action.startTime); @@ -129,7 +129,23 @@ export const renderAction = ( return <>
{action.apiName} - {locator &&
{locator}
} + {parameterString && + (parameterString.type === 'locator' ? ( + <> + + {parameterString.value} + + {parameterString.childDisplayString && ( + + {parameterString.childDisplayString.value} + + )} + + ) : ( + + {parameterString.value} + + ))} {action.method === 'goto' && action.params.url &&
{action.params.url}
} {action.class === 'APIRequestContext' && action.params.url &&
{excludeOrigin(action.params.url)}
}
@@ -151,3 +167,154 @@ function excludeOrigin(url: string): string { return url; } } + +type ActionParameterDisplayString = + | { + type: 'generic'; + value: string; + } + | { + type: 'locator'; + value: string; + childDisplayString?: ActionParameterDisplayString; + }; + +const clockDisplayString = ( + action: ActionTraceEvent, +): ActionParameterDisplayString | undefined => { + switch (action.method) { + case 'clockPauseAt': + case 'clockSetFixedTime': + case 'clockSetSystemTime': { + if ( + action.params.timeString === undefined && + action.params.timeNumber === undefined + ) + return undefined; + return { + type: 'generic', + value: new Date( + action.params.timeString ?? action.params.timeNumber, + ).toLocaleString(undefined, { timeZone: 'UTC' }), + }; + } + case 'clockFastForward': + case 'clockRunFor': { + if ( + action.params.ticksNumber === undefined && + action.params.ticksString === undefined + ) + return undefined; + return { + type: 'generic', + value: action.params.ticksString ?? `${action.params.ticksNumber}ms`, + }; + } + } + + return undefined; +}; + +const keyboardDisplayString = ( + action: ActionTraceEvent, +): ActionParameterDisplayString | undefined => { + switch (action.method) { + case 'press': + case 'keyboardPress': + case 'keyboardDown': + case 'keyboardUp': { + if (action.params.key === undefined) + return undefined; + return { type: 'generic', value: action.params.key }; + } + case 'type': + case 'fill': + case 'keyboardType': + case 'keyboardInsertText': { + const string = action.params.text ?? action.params.value; + if (string === undefined) + return undefined; + return { type: 'generic', value: `"${string}"` }; + } + } +}; + +const mouseDisplayString = ( + action: ActionTraceEvent, +): ActionParameterDisplayString | undefined => { + switch (action.method) { + case 'click': + case 'dblclick': + case 'mouseClick': + case 'mouseMove': { + if (action.params.x === undefined || action.params.y === undefined) + return undefined; + return { + type: 'generic', + value: `(${action.params.x}, ${action.params.y})`, + }; + } + case 'mouseWheel': { + if ( + action.params.deltaX === undefined || + action.params.deltaY === undefined + ) + return undefined; + return { + type: 'generic', + value: `(${action.params.deltaX}, ${action.params.deltaY})`, + }; + } + } +}; + +const touchscreenDisplayString = ( + action: ActionTraceEvent, +): ActionParameterDisplayString | undefined => { + switch (action.method) { + case 'tap': { + if (action.params.x === undefined || action.params.y === undefined) + return undefined; + return { + type: 'generic', + value: `(${action.params.x}, ${action.params.y})`, + }; + } + } +}; + +const actionParameterDisplayString = ( + action: ActionTraceEvent, + sdkLanguage: Language, + ignoreLocator: boolean = false, +): ActionParameterDisplayString | undefined => { + const params = action.params; + + // Locators have many possible classes, so follow existing logic and use `selector` presence + if (!ignoreLocator && params.selector !== undefined) { + return { + type: 'locator', + value: asLocator(sdkLanguage, params.selector), + childDisplayString: actionParameterDisplayString( + action, + sdkLanguage, + true, + ), + }; + } + + switch (action.class.toLowerCase()) { + case 'browsercontext': + return clockDisplayString(action); + case 'page': + case 'frame': + case 'elementhandle': + return ( + keyboardDisplayString(action) ?? + mouseDisplayString(action) ?? + touchscreenDisplayString(action) + ); + } + + return undefined; +}; diff --git a/tests/library/trace-viewer.spec.ts b/tests/library/trace-viewer.spec.ts index 71ecfe0764..9bb67884f5 100644 --- a/tests/library/trace-viewer.spec.ts +++ b/tests/library/trace-viewer.spec.ts @@ -166,6 +166,61 @@ test('should open simple trace viewer', async ({ showTraceViewer }) => { ]); }); +test('should show action context on locators and other common actions', async ({ + runAndTrace, + page, +}) => { + const traceViewer = await runAndTrace(async () => { + await page.setContent(''); + await page.locator('input').click({ button: 'right' }); + await page.getByRole('textbox').click(); + await expect(page.locator('input')).toHaveText(''); + await page.locator('input').press('Enter'); + await page.keyboard.type( + 'Hello world this is a very long string what happens when it overflows?', + ); + await page.keyboard.press('Control+c'); + await page.keyboard.down('Shift'); + await page.keyboard.insertText('Hello world'); + await page.keyboard.up('Shift'); + await page.mouse.move(0, 0); + await page.mouse.down(); + await page.mouse.move(100, 200); + await page.mouse.wheel(5, 7); + await page.mouse.up(); + await page.clock.fastForward(1000); + await page.clock.fastForward('30:00'); + await page.clock.pauseAt(new Date('2020-02-02T00:00:00Z')); + await page.clock.runFor(10); + await page.clock.setFixedTime(new Date('2020-02-02T00:00:00Z')); + await page.clock.setSystemTime(new Date('2020-02-02T00:00:00Z')); + }); + + await expect(traceViewer.actionTitles).toHaveText([ + /page.setContent/, + /locator.clicklocator\('input'\)/, + /locator.clickgetByRole\('textbox'\)/, + /expect.toHaveTextlocator\('input'\)/, + /locator.presslocator\('input'\)Enter/, + /keyboard.type\"Hello world this is a very long string what happens when it overflows\?\"/, + /keyboard.pressControl\+c/, + /keyboard.downShift/, + /keyboard.insertText\"Hello world\"/, + /keyboard.upShift/, + /mouse.move\(0, 0\)/, + /mouse.down/, + /mouse.move\(100, 200\)/, + /mouse.wheel\(5, 7\)/, + /mouse.up/, + /clock.fastForward1000ms/, + /clock.fastForward30:00/, + /clock.pauseAt2\/2\/2020, 12:00:00 AM/, + /clock.runFor10ms/, + /clock.setFixedTime2\/2\/2020, 12:00:00 AM/, + /clock.setSystemTime2\/2\/2020, 12:00:00 AM/, + ]); +}); + test('should complain about newer version of trace in old viewer', async ({ showTraceViewer, asset }, testInfo) => { const traceViewer = await showTraceViewer([asset('trace-from-the-future.zip')]); await expect(traceViewer.page.getByText('The trace was created by a newer version of Playwright and is not supported by this version of the viewer.')).toBeVisible(); From 3cf0461a1a3614f000784182bb509dceb78c2145 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Thu, 16 Jan 2025 13:20:20 -0800 Subject: [PATCH 06/19] chore: fix the scrollable locator tab (#34358) --- packages/trace-viewer/src/ui/inspectorTab.tsx | 26 ++++++++++++------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/packages/trace-viewer/src/ui/inspectorTab.tsx b/packages/trace-viewer/src/ui/inspectorTab.tsx index b6882a6a04..4278cdbc28 100644 --- a/packages/trace-viewer/src/ui/inspectorTab.tsx +++ b/packages/trace-viewer/src/ui/inspectorTab.tsx @@ -47,17 +47,28 @@ export const InspectorTab: React.FunctionComponent<{ setIsInspecting(false); }, [highlightedElement, setHighlightedElement, setIsInspecting]); - return
-
Locator
-
+ return
+
+
Locator
+ { + copy(highlightedElement.locator || ''); + }}> +
+
{ // Updating text needs to go first - react can squeeze a render between the state updates. setHighlightedElement({ ...highlightedElement, locator: text, lastEdited: 'locator' }); setIsInspecting(false); }} />
-
Aria
-
+ +
+
Aria snapshot
+ { + copy(highlightedElement.ariaSnapshot || ''); + }}> +
+
-
- { - copy(highlightedElement.locator || ''); - }}> -
; }; From 587e778a5aa91d9b5481276c8b0dd19c34534d67 Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Thu, 16 Jan 2025 15:04:56 -0800 Subject: [PATCH 07/19] =?UTF-8?q?Revert=20"feat(aria):=20extend=20toHaveAc?= =?UTF-8?q?cessibleName()=20to=20accept=20an=20array=20=E2=80=A6=20(#34361?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/src/api/class-locatorassertions.md | 2 +- .../src/server/injected/injectedScript.ts | 2 - packages/playwright/src/matchers/matchers.ts | 19 +++------- packages/playwright/types/test.d.ts | 2 +- tests/page/expect-misc.spec.ts | 37 ------------------- 5 files changed, 8 insertions(+), 54 deletions(-) diff --git a/docs/src/api/class-locatorassertions.md b/docs/src/api/class-locatorassertions.md index 8630051b8b..1f0d0d9990 100644 --- a/docs/src/api/class-locatorassertions.md +++ b/docs/src/api/class-locatorassertions.md @@ -1313,7 +1313,7 @@ await Expect(locator).ToHaveAccessibleNameAsync("Save to disk"); ### param: LocatorAssertions.toHaveAccessibleName.name * since: v1.44 -- `name` <[string]|[RegExp]|[Array]<[string]|[RegExp]>> +- `name` <[string]|[RegExp]> Expected accessible name. diff --git a/packages/playwright-core/src/server/injected/injectedScript.ts b/packages/playwright-core/src/server/injected/injectedScript.ts index 4ffb8ba2d8..17380db170 100644 --- a/packages/playwright-core/src/server/injected/injectedScript.ts +++ b/packages/playwright-core/src/server/injected/injectedScript.ts @@ -1437,8 +1437,6 @@ export class InjectedScript { received = elements.map(e => options.useInnerText ? (e as HTMLElement).innerText : elementText(new Map(), e).full); else if (expression === 'to.have.class.array') received = elements.map(e => e.classList.toString()); - else if (expression === 'to.have.accessible.name.array') - received = elements.map(e => getElementAccessibleName(e, false)); if (received && options.expectedText) { // "To match an array" is "to contain an array" + "equal length" diff --git a/packages/playwright/src/matchers/matchers.ts b/packages/playwright/src/matchers/matchers.ts index c942fef246..9b43999cf9 100644 --- a/packages/playwright/src/matchers/matchers.ts +++ b/packages/playwright/src/matchers/matchers.ts @@ -196,20 +196,13 @@ export function toHaveAccessibleDescription( export function toHaveAccessibleName( this: ExpectMatcherState, locator: LocatorEx, - expected: string | RegExp | (string | RegExp)[], - options: { timeout?: number, ignoreCase?: boolean, normalizeWhiteSpace?: boolean } = {} + expected: string | RegExp, + options?: { timeout?: number, ignoreCase?: boolean }, ) { - if (Array.isArray(expected)) { - return toEqual.call(this, 'toHaveAccessibleName', locator, 'Locator', async (isNot, timeout) => { - const expectedText = serializeExpectedTextValues(expected, { ignoreCase: options?.ignoreCase, normalizeWhiteSpace: true }); - return await locator._expect('to.have.accessible.name.array', { expectedText, isNot, timeout }); - }, expected, options); - } else { - return toMatchText.call(this, 'toHaveAccessibleName', locator, 'Locator', async (isNot, timeout) => { - const expectedText = serializeExpectedTextValues([expected], { ignoreCase: options?.ignoreCase, normalizeWhiteSpace: true }); - return await locator._expect('to.have.accessible.name', { expectedText, isNot, timeout }); - }, expected, options); - } + return toMatchText.call(this, 'toHaveAccessibleName', locator, 'Locator', async (isNot, timeout) => { + const expectedText = serializeExpectedTextValues([expected], { ignoreCase: options?.ignoreCase, normalizeWhiteSpace: true }); + return await locator._expect('to.have.accessible.name', { expectedText, isNot, timeout }); + }, expected, options); } export function toHaveAccessibleErrorMessage( diff --git a/packages/playwright/types/test.d.ts b/packages/playwright/types/test.d.ts index 83306e932b..e01271ceec 100644 --- a/packages/playwright/types/test.d.ts +++ b/packages/playwright/types/test.d.ts @@ -8142,7 +8142,7 @@ interface LocatorAssertions { * @param name Expected accessible name. * @param options */ - toHaveAccessibleName(name: string|RegExp|ReadonlyArray, options?: { + toHaveAccessibleName(name: string|RegExp, options?: { /** * Whether to perform case-insensitive match. * [`ignoreCase`](https://playwright.dev/docs/api/class-locatorassertions#locator-assertions-to-have-accessible-name-option-ignore-case) diff --git a/tests/page/expect-misc.spec.ts b/tests/page/expect-misc.spec.ts index a1fb6637b1..8bd668dfd7 100644 --- a/tests/page/expect-misc.spec.ts +++ b/tests/page/expect-misc.spec.ts @@ -436,43 +436,6 @@ test('toHaveAccessibleName', async ({ page }) => { await expect(page.locator('button')).toHaveAccessibleName('foo bar baz'); }); -test('toHaveAccessibleName should accept array of names for multiple elements', async ({ page }) => { - await page.setContent(` - - - - - - - - - - - - - - - - -
Cell A1Cell B1Cell C1
Cell A2Cell B2Cell C2
Cell A3Cell B3Cell C3
- `); - await expect(page.getByRole('row')).toHaveAccessibleName([ - 'Cell A1 Cell B1 Cell C1', - 'Cell A2 Cell B2 Cell C2', - 'Cell A3 Cell B3 Cell C3', - ]); - await expect(page.getByRole('row')).toHaveAccessibleName(['cell a1 cell b1 cell C1', - 'cell A2 Cell b2 Cell c2', - 'Cell a3 Cell b3 cell C3',], { ignoreCase: true }); - - await expect(page.getByRole('row')).not.toHaveAccessibleName([ - 'Cel A4 Cell B4 Cell C4', - 'Cell A5 Cell B5 Cell C5', - 'Cell A6 Cell B6 Cell C6', - ]); -}); - - test('toHaveAccessibleDescription', async ({ page }) => { await page.setContent(`
From 5438814975ea0374088ddd4ebead482a4a5fd6ca Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Thu, 16 Jan 2025 15:20:40 -0800 Subject: [PATCH 08/19] chore: do not fall back to previous LTS release deps for new Ubuntu LTS (#34360) --- .../src/server/registry/dependencies.ts | 2 +- .../playwright-core/src/utils/hostPlatform.ts | 18 +++++++++++------- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/packages/playwright-core/src/server/registry/dependencies.ts b/packages/playwright-core/src/server/registry/dependencies.ts index 60ef80d846..4aa568a6f9 100644 --- a/packages/playwright-core/src/server/registry/dependencies.ts +++ b/packages/playwright-core/src/server/registry/dependencies.ts @@ -95,7 +95,7 @@ export async function installDependenciesLinux(targets: Set, dr for (const target of targets) { const info = deps[platform]; if (!info) { - console.warn(`Cannot install dependencies for ${platform}!`); // eslint-disable-line no-console + console.warn(`Cannot install dependencies for ${platform} with Playwright ${getPlaywrightVersion()}!`); // eslint-disable-line no-console return; } libraries.push(...info[target]); diff --git a/packages/playwright-core/src/utils/hostPlatform.ts b/packages/playwright-core/src/utils/hostPlatform.ts index 7d81f39da3..0563e4ab29 100644 --- a/packages/playwright-core/src/utils/hostPlatform.ts +++ b/packages/playwright-core/src/utils/hostPlatform.ts @@ -74,14 +74,18 @@ function calculatePlatform(): { hostPlatform: HostPlatform, isOfficiallySupporte // KDE Neon is ubuntu-based and has the same versions. // TUXEDO OS is ubuntu-based and has the same versions. if (distroInfo?.id === 'ubuntu' || distroInfo?.id === 'pop' || distroInfo?.id === 'neon' || distroInfo?.id === 'tuxedo') { - const isOfficiallySupportedPlatform = distroInfo?.id === 'ubuntu'; - if (parseInt(distroInfo.version, 10) <= 19) + const isUbuntu = distroInfo?.id === 'ubuntu'; + const version = distroInfo?.version; + const major = parseInt(distroInfo.version, 10); + if (major < 20) return { hostPlatform: ('ubuntu18.04' + archSuffix) as HostPlatform, isOfficiallySupportedPlatform: false }; - if (parseInt(distroInfo.version, 10) <= 21) - return { hostPlatform: ('ubuntu20.04' + archSuffix) as HostPlatform, isOfficiallySupportedPlatform }; - if (parseInt(distroInfo.version, 10) <= 22) - return { hostPlatform: ('ubuntu22.04' + archSuffix) as HostPlatform, isOfficiallySupportedPlatform }; - return { hostPlatform: ('ubuntu24.04' + archSuffix) as HostPlatform, isOfficiallySupportedPlatform }; + if (major < 22) + return { hostPlatform: ('ubuntu20.04' + archSuffix) as HostPlatform, isOfficiallySupportedPlatform: isUbuntu && version === '20.04' }; + if (major < 24) + return { hostPlatform: ('ubuntu22.04' + archSuffix) as HostPlatform, isOfficiallySupportedPlatform: isUbuntu && version === '22.04' }; + if (major < 26) + return { hostPlatform: ('ubuntu24.04' + archSuffix) as HostPlatform, isOfficiallySupportedPlatform: isUbuntu && version === '24.04' }; + return { hostPlatform: ('ubuntu' + distroInfo.version + archSuffix) as HostPlatform, isOfficiallySupportedPlatform: false }; } // Linux Mint is ubuntu-based but does not have the same versions if (distroInfo?.id === 'linuxmint') { From 40b92eaf2a0d2dbb1ea40fa0719608a92481051b Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Thu, 16 Jan 2025 16:56:13 -0800 Subject: [PATCH 09/19] docs: remove toMatchAriaSnapshot({path}) from language ports (#34363) --- docs/src/api/class-locatorassertions.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/src/api/class-locatorassertions.md b/docs/src/api/class-locatorassertions.md index 1f0d0d9990..63b487208f 100644 --- a/docs/src/api/class-locatorassertions.md +++ b/docs/src/api/class-locatorassertions.md @@ -2241,8 +2241,7 @@ assertThat(page.locator("body")).matchesAriaSnapshot(""" ## async method: LocatorAssertions.toMatchAriaSnapshot#2 * since: v1.50 -* langs: - - alias-java: matchesAriaSnapshot +* langs: js Asserts that the target element matches the given [accessibility snapshot](../aria-snapshots.md). From 07bcbcd1b497e0bb1e75bb984c624c9fc2a84ba9 Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Fri, 17 Jan 2025 10:45:58 +0000 Subject: [PATCH 10/19] feat(chromium): roll to r1155 (#34371) --- README.md | 4 +- packages/playwright-core/browsers.json | 4 +- .../src/server/chromium/protocol.d.ts | 213 +++++++++++++++++- .../src/server/deviceDescriptorsSource.json | 96 ++++---- packages/playwright-core/types/protocol.d.ts | 213 +++++++++++++++++- 5 files changed, 460 insertions(+), 70 deletions(-) diff --git a/README.md b/README.md index ca5c10d519..013ac177cf 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # 🎭 Playwright -[![npm version](https://img.shields.io/npm/v/playwright.svg)](https://www.npmjs.com/package/playwright) [![Chromium version](https://img.shields.io/badge/chromium-132.0.6834.57-blue.svg?logo=google-chrome)](https://www.chromium.org/Home) [![Firefox version](https://img.shields.io/badge/firefox-134.0-blue.svg?logo=firefoxbrowser)](https://www.mozilla.org/en-US/firefox/new/) [![WebKit version](https://img.shields.io/badge/webkit-18.2-blue.svg?logo=safari)](https://webkit.org/) [![Join Discord](https://img.shields.io/badge/join-discord-infomational)](https://aka.ms/playwright/discord) +[![npm version](https://img.shields.io/npm/v/playwright.svg)](https://www.npmjs.com/package/playwright) [![Chromium version](https://img.shields.io/badge/chromium-133.0.6943.16-blue.svg?logo=google-chrome)](https://www.chromium.org/Home) [![Firefox version](https://img.shields.io/badge/firefox-134.0-blue.svg?logo=firefoxbrowser)](https://www.mozilla.org/en-US/firefox/new/) [![WebKit version](https://img.shields.io/badge/webkit-18.2-blue.svg?logo=safari)](https://webkit.org/) [![Join Discord](https://img.shields.io/badge/join-discord-infomational)](https://aka.ms/playwright/discord) ## [Documentation](https://playwright.dev) | [API reference](https://playwright.dev/docs/api/class-playwright) @@ -8,7 +8,7 @@ Playwright is a framework for Web Testing and Automation. It allows testing [Chr | | Linux | macOS | Windows | | :--- | :---: | :---: | :---: | -| Chromium 132.0.6834.57 | :white_check_mark: | :white_check_mark: | :white_check_mark: | +| Chromium 133.0.6943.16 | :white_check_mark: | :white_check_mark: | :white_check_mark: | | WebKit 18.2 | :white_check_mark: | :white_check_mark: | :white_check_mark: | | Firefox 134.0 | :white_check_mark: | :white_check_mark: | :white_check_mark: | diff --git a/packages/playwright-core/browsers.json b/packages/playwright-core/browsers.json index 0803da3ff1..0895f005b1 100644 --- a/packages/playwright-core/browsers.json +++ b/packages/playwright-core/browsers.json @@ -3,9 +3,9 @@ "browsers": [ { "name": "chromium", - "revision": "1153", + "revision": "1155", "installByDefault": true, - "browserVersion": "132.0.6834.57" + "browserVersion": "133.0.6943.16" }, { "name": "chromium-tip-of-tree", diff --git a/packages/playwright-core/src/server/chromium/protocol.d.ts b/packages/playwright-core/src/server/chromium/protocol.d.ts index 3a61fbca59..fa1d6121f9 100644 --- a/packages/playwright-core/src/server/chromium/protocol.d.ts +++ b/packages/playwright-core/src/server/chromium/protocol.d.ts @@ -685,8 +685,8 @@ percentage [0 - 100] for scroll driven animations /** * The unique request id. */ - requestId: Network.RequestId; - url?: string; + requestId?: Network.RequestId; + url: string; } /** * Information about the frame affected by an inspector issue. @@ -697,6 +697,20 @@ percentage [0 - 100] for scroll driven animations export type CookieExclusionReason = "ExcludeSameSiteUnspecifiedTreatedAsLax"|"ExcludeSameSiteNoneInsecure"|"ExcludeSameSiteLax"|"ExcludeSameSiteStrict"|"ExcludeInvalidSameParty"|"ExcludeSamePartyCrossPartyContext"|"ExcludeDomainNonASCII"|"ExcludeThirdPartyCookieBlockedInFirstPartySet"|"ExcludeThirdPartyPhaseout"|"ExcludePortMismatch"|"ExcludeSchemeMismatch"; export type CookieWarningReason = "WarnSameSiteUnspecifiedCrossSiteContext"|"WarnSameSiteNoneInsecure"|"WarnSameSiteUnspecifiedLaxAllowUnsafe"|"WarnSameSiteStrictLaxDowngradeStrict"|"WarnSameSiteStrictCrossDowngradeStrict"|"WarnSameSiteStrictCrossDowngradeLax"|"WarnSameSiteLaxCrossDowngradeStrict"|"WarnSameSiteLaxCrossDowngradeLax"|"WarnAttributeValueExceedsMaxSize"|"WarnDomainNonASCII"|"WarnThirdPartyPhaseout"|"WarnCrossSiteRedirectDowngradeChangesInclusion"|"WarnDeprecationTrialMetadata"|"WarnThirdPartyCookieHeuristic"; export type CookieOperation = "SetCookie"|"ReadCookie"; + /** + * Represents the category of insight that a cookie issue falls under. + */ + export type InsightType = "GitHubResource"|"GracePeriod"|"Heuristics"; + /** + * Information about the suggested solution to a cookie issue. + */ + export interface CookieIssueInsight { + type: InsightType; + /** + * Link to table entry in third-party cookie migration readiness list. + */ + tableEntryUrl?: string; + } /** * This information is currently necessary, as the front-end has a difficult time finding a specific cookie. With this, we can convey specific error @@ -721,6 +735,10 @@ may be used by the front-end as additional context. siteForCookies?: string; cookieUrl?: string; request?: AffectedRequest; + /** + * The recommended solution to the issue. + */ + insight?: CookieIssueInsight; } export type MixedContentResolutionStatus = "MixedContentBlocked"|"MixedContentAutomaticallyUpgraded"|"MixedContentWarning"; export type MixedContentResourceType = "AttributionSrc"|"Audio"|"Beacon"|"CSPReport"|"Download"|"EventSource"|"Favicon"|"Font"|"Form"|"Frame"|"Image"|"Import"|"JSON"|"Manifest"|"Ping"|"PluginData"|"PluginResource"|"Prefetch"|"Resource"|"Script"|"ServiceWorker"|"SharedWorker"|"SpeculationRules"|"Stylesheet"|"Track"|"Video"|"Worker"|"XMLHttpRequest"|"XSLT"; @@ -758,7 +776,7 @@ Does not always exist (e.g. for unsafe form submission urls). * Enum indicating the reason a response has been blocked. These reasons are refinements of the net error BLOCKED_BY_RESPONSE. */ - export type BlockedByResponseReason = "CoepFrameResourceNeedsCoepHeader"|"CoopSandboxedIFrameCannotNavigateToCoopPage"|"CorpNotSameOrigin"|"CorpNotSameOriginAfterDefaultedToSameOriginByCoep"|"CorpNotSameOriginAfterDefaultedToSameOriginByDip"|"CorpNotSameOriginAfterDefaultedToSameOriginByCoepAndDip"|"CorpNotSameSite"; + export type BlockedByResponseReason = "CoepFrameResourceNeedsCoepHeader"|"CoopSandboxedIFrameCannotNavigateToCoopPage"|"CorpNotSameOrigin"|"CorpNotSameOriginAfterDefaultedToSameOriginByCoep"|"CorpNotSameOriginAfterDefaultedToSameOriginByDip"|"CorpNotSameOriginAfterDefaultedToSameOriginByCoepAndDip"|"CorpNotSameSite"|"SRIMessageSignatureMismatch"; /** * Details for a request that has been blocked with the BLOCKED_BY_RESPONSE code. Currently only used for COEP/COOP, but may be extended to include @@ -963,6 +981,15 @@ features, encourage the use of new ones, and provide general guidance. failureMessage: string; requestId?: Network.RequestId; } + export type SelectElementAccessibilityIssueReason = "DisallowedSelectChild"|"DisallowedOptGroupChild"|"NonPhrasingContentOptionChild"|"InteractiveContentOptionChild"|"InteractiveContentLegendChild"; + /** + * This isue warns about errors in the select element content model. + */ + export interface SelectElementAccessibilityIssueDetails { + nodeId: DOM.BackendNodeId; + selectElementAccessibilityIssueReason: SelectElementAccessibilityIssueReason; + hasDisallowedAttributes: boolean; + } export type StyleSheetLoadingIssueReason = "LateImportRule"|"RequestFailed"; /** * This issue warns when a referenced stylesheet couldn't be loaded. @@ -1005,7 +1032,7 @@ registrations being ignored. optional fields in InspectorIssueDetails to convey more specific information about the kind of issue. */ - export type InspectorIssueCode = "CookieIssue"|"MixedContentIssue"|"BlockedByResponseIssue"|"HeavyAdIssue"|"ContentSecurityPolicyIssue"|"SharedArrayBufferIssue"|"LowTextContrastIssue"|"CorsIssue"|"AttributionReportingIssue"|"QuirksModeIssue"|"NavigatorUserAgentIssue"|"GenericIssue"|"DeprecationIssue"|"ClientHintIssue"|"FederatedAuthRequestIssue"|"BounceTrackingIssue"|"CookieDeprecationMetadataIssue"|"StylesheetLoadingIssue"|"FederatedAuthUserInfoRequestIssue"|"PropertyRuleIssue"|"SharedDictionaryIssue"; + export type InspectorIssueCode = "CookieIssue"|"MixedContentIssue"|"BlockedByResponseIssue"|"HeavyAdIssue"|"ContentSecurityPolicyIssue"|"SharedArrayBufferIssue"|"LowTextContrastIssue"|"CorsIssue"|"AttributionReportingIssue"|"QuirksModeIssue"|"NavigatorUserAgentIssue"|"GenericIssue"|"DeprecationIssue"|"ClientHintIssue"|"FederatedAuthRequestIssue"|"BounceTrackingIssue"|"CookieDeprecationMetadataIssue"|"StylesheetLoadingIssue"|"FederatedAuthUserInfoRequestIssue"|"PropertyRuleIssue"|"SharedDictionaryIssue"|"SelectElementAccessibilityIssue"; /** * This struct holds a list of optional fields with additional information specific to the kind of issue. When adding a new issue code, please also @@ -1033,6 +1060,7 @@ add a new optional field to this type. propertyRuleIssueDetails?: PropertyRuleIssueDetails; federatedAuthUserInfoRequestIssueDetails?: FederatedAuthUserInfoRequestIssueDetails; sharedDictionaryIssueDetails?: SharedDictionaryIssueDetails; + selectElementAccessibilityIssueDetails?: SelectElementAccessibilityIssueDetails; } /** * A unique id for a DevTools inspector issue. Allows other entities (e.g. @@ -1534,7 +1562,7 @@ events afterwards if enabled and recording. */ windowState?: WindowState; } - export type PermissionType = "accessibilityEvents"|"audioCapture"|"backgroundSync"|"backgroundFetch"|"capturedSurfaceControl"|"clipboardReadWrite"|"clipboardSanitizedWrite"|"displayCapture"|"durableStorage"|"flash"|"geolocation"|"idleDetection"|"localFonts"|"midi"|"midiSysex"|"nfc"|"notifications"|"paymentHandler"|"periodicBackgroundSync"|"protectedMediaIdentifier"|"sensors"|"storageAccess"|"speakerSelection"|"topLevelStorageAccess"|"videoCapture"|"videoCapturePanTiltZoom"|"wakeLockScreen"|"wakeLockSystem"|"webAppInstallation"|"windowManagement"; + export type PermissionType = "ar"|"audioCapture"|"automaticFullscreen"|"backgroundFetch"|"backgroundSync"|"cameraPanTiltZoom"|"capturedSurfaceControl"|"clipboardReadWrite"|"clipboardSanitizedWrite"|"displayCapture"|"durableStorage"|"geolocation"|"handTracking"|"idleDetection"|"keyboardLock"|"localFonts"|"midi"|"midiSysex"|"nfc"|"notifications"|"paymentHandler"|"periodicBackgroundSync"|"pointerLock"|"protectedMediaIdentifier"|"sensors"|"smartCard"|"speakerSelection"|"storageAccess"|"topLevelStorageAccess"|"videoCapture"|"vr"|"wakeLockScreen"|"wakeLockSystem"|"webAppInstallation"|"webPrinting"|"windowManagement"; export type PermissionSetting = "granted"|"denied"|"prompt"; /** * Definition of PermissionDescriptor defined in the Permissions API: @@ -1961,6 +1989,19 @@ inspector" rules), "regular" for regular stylesheets. */ matches: RuleMatch[]; } + /** + * CSS style coming from animations with the name of the animation. + */ + export interface CSSAnimationStyle { + /** + * The name of the animation. + */ + name?: string; + /** + * The style coming from the animation. + */ + style: CSSStyle; + } /** * Inherited CSS rule collection from ancestor node. */ @@ -1974,6 +2015,19 @@ inspector" rules), "regular" for regular stylesheets. */ matchedCSSRules: RuleMatch[]; } + /** + * Inherited CSS style collection for animated styles from ancestor node. + */ + export interface InheritedAnimatedStyleEntry { + /** + * Styles coming from the animations of the ancestor, if any, in the style inheritance chain. + */ + animationStyles?: CSSAnimationStyle[]; + /** + * The style coming from the transitions of the ancestor, if any, in the style inheritance chain. + */ + transitionsStyle?: CSSStyle; + } /** * Inherited pseudo element matches from pseudos of an ancestor node. */ @@ -2897,6 +2951,21 @@ the browser. } export type forcePseudoStateReturnValue = { } + /** + * Ensures that the given node is in its starting-style state. + */ + export type forceStartingStyleParameters = { + /** + * The element id for which to force the starting-style state. + */ + nodeId: DOM.NodeId; + /** + * Boolean indicating if this is on or off. + */ + forced: boolean; + } + export type forceStartingStyleReturnValue = { + } export type getBackgroundColorsParameters = { /** * Id of the node to get background colors for. @@ -2934,6 +3003,46 @@ be ignored (as if the image had failed to load). */ computedStyle: CSSComputedStyleProperty[]; } + /** + * Resolve the specified values in the context of the provided element. +For example, a value of '1em' is evaluated according to the computed +'font-size' of the element and a value 'calc(1px + 2px)' will be +resolved to '3px'. + */ + export type resolveValuesParameters = { + /** + * Substitution functions (var()/env()/attr()) and cascade-dependent +keywords (revert/revert-layer) do not work. + */ + values: string[]; + /** + * Id of the node in whose context the expression is evaluated + */ + nodeId: DOM.NodeId; + /** + * Only longhands and custom property names are accepted. + */ + propertyName?: string; + /** + * Pseudo element type, only works for pseudo elements that generate +elements in the tree, such as ::before and ::after. + */ + pseudoType?: DOM.PseudoType; + /** + * Pseudo element custom ident. + */ + pseudoIdentifier?: string; + } + export type resolveValuesReturnValue = { + results: string[]; + } + export type getLonghandPropertiesParameters = { + shorthandName: string; + value: string; + } + export type getLonghandPropertiesReturnValue = { + longhandProperties: CSSProperty[]; + } /** * Returns the styles defined inline (explicitly in the "style" attribute and implicitly, using DOM attributes) for a DOM node identified by `nodeId`. @@ -2951,6 +3060,28 @@ attributes) for a DOM node identified by `nodeId`. */ attributesStyle?: CSSStyle; } + /** + * Returns the styles coming from animations & transitions +including the animation & transition styles coming from inheritance chain. + */ + export type getAnimatedStylesForNodeParameters = { + nodeId: DOM.NodeId; + } + export type getAnimatedStylesForNodeReturnValue = { + /** + * Styles coming from animations. + */ + animationStyles?: CSSAnimationStyle[]; + /** + * Style coming from transitions. + */ + transitionsStyle?: CSSStyle; + /** + * Inherited style entries for animationsStyle and transitionsStyle from +the inheritance chain of the element. + */ + inherited?: InheritedAnimatedStyleEntry[]; + } /** * Returns requested styles for a DOM node identified by `nodeId`. */ @@ -3603,7 +3734,7 @@ front-end. /** * Pseudo element type. */ - export type PseudoType = "first-line"|"first-letter"|"check"|"before"|"after"|"select-arrow"|"marker"|"backdrop"|"column"|"selection"|"search-text"|"target-text"|"spelling-error"|"grammar-error"|"highlight"|"first-line-inherited"|"scroll-marker"|"scroll-marker-group"|"scroll-next-button"|"scroll-prev-button"|"scrollbar"|"scrollbar-thumb"|"scrollbar-button"|"scrollbar-track"|"scrollbar-track-piece"|"scrollbar-corner"|"resizer"|"input-list-button"|"view-transition"|"view-transition-group"|"view-transition-image-pair"|"view-transition-old"|"view-transition-new"|"placeholder"|"file-selector-button"|"details-content"|"picker"; + export type PseudoType = "first-line"|"first-letter"|"checkmark"|"before"|"after"|"picker-icon"|"marker"|"backdrop"|"column"|"selection"|"search-text"|"target-text"|"spelling-error"|"grammar-error"|"highlight"|"first-line-inherited"|"scroll-marker"|"scroll-marker-group"|"scroll-button"|"scrollbar"|"scrollbar-thumb"|"scrollbar-button"|"scrollbar-track"|"scrollbar-track-piece"|"scrollbar-corner"|"resizer"|"input-list-button"|"view-transition"|"view-transition-group"|"view-transition-image-pair"|"view-transition-old"|"view-transition-new"|"placeholder"|"file-selector-button"|"details-content"|"picker"; /** * Shadow root type. */ @@ -8616,7 +8747,7 @@ applicable or not known. /** * The reason why request was blocked. */ - export type BlockedReason = "other"|"csp"|"mixed-content"|"origin"|"inspector"|"subresource-filter"|"content-type"|"coep-frame-resource-needs-coep-header"|"coop-sandboxed-iframe-cannot-navigate-to-coop-page"|"corp-not-same-origin"|"corp-not-same-origin-after-defaulted-to-same-origin-by-coep"|"corp-not-same-origin-after-defaulted-to-same-origin-by-dip"|"corp-not-same-origin-after-defaulted-to-same-origin-by-coep-and-dip"|"corp-not-same-site"; + export type BlockedReason = "other"|"csp"|"mixed-content"|"origin"|"inspector"|"subresource-filter"|"content-type"|"coep-frame-resource-needs-coep-header"|"coop-sandboxed-iframe-cannot-navigate-to-coop-page"|"corp-not-same-origin"|"corp-not-same-origin-after-defaulted-to-same-origin-by-coep"|"corp-not-same-origin-after-defaulted-to-same-origin-by-dip"|"corp-not-same-origin-after-defaulted-to-same-origin-by-coep-and-dip"|"corp-not-same-site"|"sri-message-signature-mismatch"; /** * The reason why request was blocked. */ @@ -9917,6 +10048,9 @@ are represented by the invalid cookie line string instead of a proper cookie. blockedCookies: BlockedSetCookieWithReason[]; /** * Raw response headers as they were received over the wire. +Duplicate headers in the response are represented as a single key with their values +concatentated using `\n` as the separator. +See also `headersText` that contains verbatim text for HTTP/1.*. */ headers: Headers; /** @@ -9962,6 +10096,9 @@ Only one responseReceivedEarlyHints may be fired for eached responseReceived eve requestId: RequestId; /** * Raw response headers as they were received over the wire. +Duplicate headers in the response are represented as a single key with their values +concatentated using `\n` as the separator. +See also `headersText` that contains verbatim text for HTTP/1.*. */ headers: Headers; } @@ -9978,7 +10115,7 @@ or after the response was received. of the operation already exists und thus, the operation was abort preemptively (e.g. a cache hit). */ - status: "Ok"|"InvalidArgument"|"MissingIssuerKeys"|"FailedPrecondition"|"ResourceExhausted"|"AlreadyExists"|"ResourceLimited"|"Unauthorized"|"BadResponse"|"InternalError"|"UnknownError"|"FulfilledLocally"; + status: "Ok"|"InvalidArgument"|"MissingIssuerKeys"|"FailedPrecondition"|"ResourceExhausted"|"AlreadyExists"|"ResourceLimited"|"Unauthorized"|"BadResponse"|"InternalError"|"UnknownError"|"FulfilledLocally"|"SiteIssuerLimit"; type: TrustTokenOperationType; requestId: RequestId; /** @@ -10672,6 +10809,26 @@ should be omitted for worker targets. export type loadNetworkResourceReturnValue = { resource: LoadNetworkResourcePageResult; } + /** + * Sets Controls for third-party cookie access +Page reload is required before the new cookie bahavior will be observed + */ + export type setCookieControlsParameters = { + /** + * Whether 3pc restriction is enabled. + */ + enableThirdPartyCookieRestriction: boolean; + /** + * Whether 3pc grace period exception should be enabled; false by default. + */ + disableThirdPartyCookieMetadata: boolean; + /** + * Whether 3pc heuristics exceptions should be enabled; false by default. + */ + disableThirdPartyCookieHeuristics: boolean; + } + export type setCookieControlsReturnValue = { + } } /** @@ -11545,7 +11702,7 @@ as an ad. * All Permissions Policy features. This enum should match the one defined in third_party/blink/renderer/core/permissions_policy/permissions_policy_features.json5. */ - export type PermissionsPolicyFeature = "accelerometer"|"all-screens-capture"|"ambient-light-sensor"|"attribution-reporting"|"autoplay"|"bluetooth"|"browsing-topics"|"camera"|"captured-surface-control"|"ch-dpr"|"ch-device-memory"|"ch-downlink"|"ch-ect"|"ch-prefers-color-scheme"|"ch-prefers-reduced-motion"|"ch-prefers-reduced-transparency"|"ch-rtt"|"ch-save-data"|"ch-ua"|"ch-ua-arch"|"ch-ua-bitness"|"ch-ua-platform"|"ch-ua-model"|"ch-ua-mobile"|"ch-ua-form-factors"|"ch-ua-full-version"|"ch-ua-full-version-list"|"ch-ua-platform-version"|"ch-ua-wow64"|"ch-viewport-height"|"ch-viewport-width"|"ch-width"|"clipboard-read"|"clipboard-write"|"compute-pressure"|"controlled-frame"|"cross-origin-isolated"|"deferred-fetch"|"digital-credentials-get"|"direct-sockets"|"direct-sockets-private"|"display-capture"|"document-domain"|"encrypted-media"|"execution-while-out-of-viewport"|"execution-while-not-rendered"|"fenced-unpartitioned-storage-read"|"focus-without-user-activation"|"fullscreen"|"frobulate"|"gamepad"|"geolocation"|"gyroscope"|"hid"|"identity-credentials-get"|"idle-detection"|"interest-cohort"|"join-ad-interest-group"|"keyboard-map"|"local-fonts"|"magnetometer"|"media-playback-while-not-visible"|"microphone"|"midi"|"otp-credentials"|"payment"|"picture-in-picture"|"popins"|"private-aggregation"|"private-state-token-issuance"|"private-state-token-redemption"|"publickey-credentials-create"|"publickey-credentials-get"|"run-ad-auction"|"screen-wake-lock"|"serial"|"shared-autofill"|"shared-storage"|"shared-storage-select-url"|"smart-card"|"speaker-selection"|"storage-access"|"sub-apps"|"sync-xhr"|"unload"|"usb"|"usb-unrestricted"|"vertical-scroll"|"web-app-installation"|"web-printing"|"web-share"|"window-management"|"xr-spatial-tracking"; + export type PermissionsPolicyFeature = "accelerometer"|"all-screens-capture"|"ambient-light-sensor"|"attribution-reporting"|"autoplay"|"bluetooth"|"browsing-topics"|"camera"|"captured-surface-control"|"ch-dpr"|"ch-device-memory"|"ch-downlink"|"ch-ect"|"ch-prefers-color-scheme"|"ch-prefers-reduced-motion"|"ch-prefers-reduced-transparency"|"ch-rtt"|"ch-save-data"|"ch-ua"|"ch-ua-arch"|"ch-ua-bitness"|"ch-ua-platform"|"ch-ua-model"|"ch-ua-mobile"|"ch-ua-form-factors"|"ch-ua-full-version"|"ch-ua-full-version-list"|"ch-ua-platform-version"|"ch-ua-wow64"|"ch-viewport-height"|"ch-viewport-width"|"ch-width"|"clipboard-read"|"clipboard-write"|"compute-pressure"|"controlled-frame"|"cross-origin-isolated"|"deferred-fetch"|"deferred-fetch-minimal"|"digital-credentials-get"|"direct-sockets"|"direct-sockets-private"|"display-capture"|"document-domain"|"encrypted-media"|"execution-while-out-of-viewport"|"execution-while-not-rendered"|"fenced-unpartitioned-storage-read"|"focus-without-user-activation"|"fullscreen"|"frobulate"|"gamepad"|"geolocation"|"gyroscope"|"hid"|"identity-credentials-get"|"idle-detection"|"interest-cohort"|"join-ad-interest-group"|"keyboard-map"|"local-fonts"|"magnetometer"|"media-playback-while-not-visible"|"microphone"|"midi"|"otp-credentials"|"payment"|"picture-in-picture"|"popins"|"private-aggregation"|"private-state-token-issuance"|"private-state-token-redemption"|"publickey-credentials-create"|"publickey-credentials-get"|"run-ad-auction"|"screen-wake-lock"|"serial"|"shared-autofill"|"shared-storage"|"shared-storage-select-url"|"smart-card"|"speaker-selection"|"storage-access"|"sub-apps"|"sync-xhr"|"unload"|"usb"|"usb-unrestricted"|"vertical-scroll"|"web-app-installation"|"web-printing"|"web-share"|"window-management"|"xr-spatial-tracking"; /** * Reason for a permissions policy feature to be disabled. */ @@ -15519,6 +15676,14 @@ Parts of the URL other than those constituting origin are ignored. * The initial URL the page will be navigated to. An empty string indicates about:blank. */ url: string; + /** + * Frame left origin in DIP (headless chrome only). + */ + left?: number; + /** + * Frame top origin in DIP (headless chrome only). + */ + top?: number; /** * Frame width in DIP (headless chrome only). */ @@ -17151,6 +17316,16 @@ possible for multiple rule sets and links to trigger a single attempt. ruleSetIds: RuleSetId[]; nodeIds: DOM.BackendNodeId[]; } + /** + * Chrome manages different types of preloads together using a +concept of preloading pipeline. For example, if a site uses a +SpeculationRules for prerender, Chrome first starts a prefetch and +then upgrades it to prerender. + +CDP events for them are emitted separately but they share +`PreloadPipelineId`. + */ + export type PreloadPipelineId = string; /** * List of FinalStatus reasons for Prerender2. */ @@ -17198,6 +17373,7 @@ filter out the ones that aren't necessary to the developers. */ export type prefetchStatusUpdatedPayload = { key: PreloadingAttemptKey; + pipelineId: PreloadPipelineId; /** * The frame id of the frame initiating prefetch. */ @@ -17212,6 +17388,7 @@ filter out the ones that aren't necessary to the developers. */ export type prerenderStatusUpdatedPayload = { key: PreloadingAttemptKey; + pipelineId: PreloadPipelineId; status: PreloadingStatus; prerenderStatus?: PrerenderFinalStatus; /** @@ -17922,6 +18099,10 @@ variables as its properties. * Content hash of the script, SHA-256. */ hash: string; + /** + * For Wasm modules, the content of the `build_id` custom section. + */ + buildId: string; /** * Embedder-specific auxiliary data likely matching {isDefault: boolean, type: 'default'|'isolated'|'worker', frameId: string} */ @@ -17996,6 +18177,10 @@ scripts upon enabling debugger. * Content hash of the script, SHA-256. */ hash: string; + /** + * For Wasm modules, the content of the `build_id` custom section. + */ + buildId: string; /** * Embedder-specific auxiliary data likely matching {isDefault: boolean, type: 'default'|'isolated'|'worker', frameId: string} */ @@ -20507,9 +20692,13 @@ Error was thrown. "CSS.disable": CSS.disableParameters; "CSS.enable": CSS.enableParameters; "CSS.forcePseudoState": CSS.forcePseudoStateParameters; + "CSS.forceStartingStyle": CSS.forceStartingStyleParameters; "CSS.getBackgroundColors": CSS.getBackgroundColorsParameters; "CSS.getComputedStyleForNode": CSS.getComputedStyleForNodeParameters; + "CSS.resolveValues": CSS.resolveValuesParameters; + "CSS.getLonghandProperties": CSS.getLonghandPropertiesParameters; "CSS.getInlineStylesForNode": CSS.getInlineStylesForNodeParameters; + "CSS.getAnimatedStylesForNode": CSS.getAnimatedStylesForNodeParameters; "CSS.getMatchedStylesForNode": CSS.getMatchedStylesForNodeParameters; "CSS.getMediaQueries": CSS.getMediaQueriesParameters; "CSS.getPlatformFontsForNode": CSS.getPlatformFontsForNodeParameters; @@ -20751,6 +20940,7 @@ Error was thrown. "Network.getSecurityIsolationStatus": Network.getSecurityIsolationStatusParameters; "Network.enableReportingApi": Network.enableReportingApiParameters; "Network.loadNetworkResource": Network.loadNetworkResourceParameters; + "Network.setCookieControls": Network.setCookieControlsParameters; "Overlay.disable": Overlay.disableParameters; "Overlay.enable": Overlay.enableParameters; "Overlay.getHighlightObjectForTest": Overlay.getHighlightObjectForTestParameters; @@ -21119,9 +21309,13 @@ Error was thrown. "CSS.disable": CSS.disableReturnValue; "CSS.enable": CSS.enableReturnValue; "CSS.forcePseudoState": CSS.forcePseudoStateReturnValue; + "CSS.forceStartingStyle": CSS.forceStartingStyleReturnValue; "CSS.getBackgroundColors": CSS.getBackgroundColorsReturnValue; "CSS.getComputedStyleForNode": CSS.getComputedStyleForNodeReturnValue; + "CSS.resolveValues": CSS.resolveValuesReturnValue; + "CSS.getLonghandProperties": CSS.getLonghandPropertiesReturnValue; "CSS.getInlineStylesForNode": CSS.getInlineStylesForNodeReturnValue; + "CSS.getAnimatedStylesForNode": CSS.getAnimatedStylesForNodeReturnValue; "CSS.getMatchedStylesForNode": CSS.getMatchedStylesForNodeReturnValue; "CSS.getMediaQueries": CSS.getMediaQueriesReturnValue; "CSS.getPlatformFontsForNode": CSS.getPlatformFontsForNodeReturnValue; @@ -21363,6 +21557,7 @@ Error was thrown. "Network.getSecurityIsolationStatus": Network.getSecurityIsolationStatusReturnValue; "Network.enableReportingApi": Network.enableReportingApiReturnValue; "Network.loadNetworkResource": Network.loadNetworkResourceReturnValue; + "Network.setCookieControls": Network.setCookieControlsReturnValue; "Overlay.disable": Overlay.disableReturnValue; "Overlay.enable": Overlay.enableReturnValue; "Overlay.getHighlightObjectForTest": Overlay.getHighlightObjectForTestReturnValue; diff --git a/packages/playwright-core/src/server/deviceDescriptorsSource.json b/packages/playwright-core/src/server/deviceDescriptorsSource.json index 3380be8ac7..4a08f9d687 100644 --- a/packages/playwright-core/src/server/deviceDescriptorsSource.json +++ b/packages/playwright-core/src/server/deviceDescriptorsSource.json @@ -110,7 +110,7 @@ "defaultBrowserType": "webkit" }, "Galaxy S5": { - "userAgent": "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.57 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.16 Mobile Safari/537.36", "viewport": { "width": 360, "height": 640 @@ -121,7 +121,7 @@ "defaultBrowserType": "chromium" }, "Galaxy S5 landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.57 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.16 Mobile Safari/537.36", "viewport": { "width": 640, "height": 360 @@ -132,7 +132,7 @@ "defaultBrowserType": "chromium" }, "Galaxy S8": { - "userAgent": "Mozilla/5.0 (Linux; Android 7.0; SM-G950U Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.57 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 7.0; SM-G950U Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.16 Mobile Safari/537.36", "viewport": { "width": 360, "height": 740 @@ -143,7 +143,7 @@ "defaultBrowserType": "chromium" }, "Galaxy S8 landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 7.0; SM-G950U Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.57 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 7.0; SM-G950U Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.16 Mobile Safari/537.36", "viewport": { "width": 740, "height": 360 @@ -154,7 +154,7 @@ "defaultBrowserType": "chromium" }, "Galaxy S9+": { - "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G965U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.57 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G965U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.16 Mobile Safari/537.36", "viewport": { "width": 320, "height": 658 @@ -165,7 +165,7 @@ "defaultBrowserType": "chromium" }, "Galaxy S9+ landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G965U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.57 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G965U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.16 Mobile Safari/537.36", "viewport": { "width": 658, "height": 320 @@ -176,7 +176,7 @@ "defaultBrowserType": "chromium" }, "Galaxy Tab S4": { - "userAgent": "Mozilla/5.0 (Linux; Android 8.1.0; SM-T837A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.57 Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 8.1.0; SM-T837A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.16 Safari/537.36", "viewport": { "width": 712, "height": 1138 @@ -187,7 +187,7 @@ "defaultBrowserType": "chromium" }, "Galaxy Tab S4 landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 8.1.0; SM-T837A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.57 Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 8.1.0; SM-T837A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.16 Safari/537.36", "viewport": { "width": 1138, "height": 712 @@ -1098,7 +1098,7 @@ "defaultBrowserType": "webkit" }, "LG Optimus L70": { - "userAgent": "Mozilla/5.0 (Linux; U; Android 4.4.2; en-us; LGMS323 Build/KOT49I.MS32310c) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/132.0.6834.57 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; U; Android 4.4.2; en-us; LGMS323 Build/KOT49I.MS32310c) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/133.0.6943.16 Mobile Safari/537.36", "viewport": { "width": 384, "height": 640 @@ -1109,7 +1109,7 @@ "defaultBrowserType": "chromium" }, "LG Optimus L70 landscape": { - "userAgent": "Mozilla/5.0 (Linux; U; Android 4.4.2; en-us; LGMS323 Build/KOT49I.MS32310c) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/132.0.6834.57 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; U; Android 4.4.2; en-us; LGMS323 Build/KOT49I.MS32310c) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/133.0.6943.16 Mobile Safari/537.36", "viewport": { "width": 640, "height": 384 @@ -1120,7 +1120,7 @@ "defaultBrowserType": "chromium" }, "Microsoft Lumia 550": { - "userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 550) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.57 Mobile Safari/537.36 Edge/14.14263", + "userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 550) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.16 Mobile Safari/537.36 Edge/14.14263", "viewport": { "width": 640, "height": 360 @@ -1131,7 +1131,7 @@ "defaultBrowserType": "chromium" }, "Microsoft Lumia 550 landscape": { - "userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 550) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.57 Mobile Safari/537.36 Edge/14.14263", + "userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 550) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.16 Mobile Safari/537.36 Edge/14.14263", "viewport": { "width": 360, "height": 640 @@ -1142,7 +1142,7 @@ "defaultBrowserType": "chromium" }, "Microsoft Lumia 950": { - "userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 950) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.57 Mobile Safari/537.36 Edge/14.14263", + "userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 950) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.16 Mobile Safari/537.36 Edge/14.14263", "viewport": { "width": 360, "height": 640 @@ -1153,7 +1153,7 @@ "defaultBrowserType": "chromium" }, "Microsoft Lumia 950 landscape": { - "userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 950) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.57 Mobile Safari/537.36 Edge/14.14263", + "userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 950) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.16 Mobile Safari/537.36 Edge/14.14263", "viewport": { "width": 640, "height": 360 @@ -1164,7 +1164,7 @@ "defaultBrowserType": "chromium" }, "Nexus 10": { - "userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.57 Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.16 Safari/537.36", "viewport": { "width": 800, "height": 1280 @@ -1175,7 +1175,7 @@ "defaultBrowserType": "chromium" }, "Nexus 10 landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.57 Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.16 Safari/537.36", "viewport": { "width": 1280, "height": 800 @@ -1186,7 +1186,7 @@ "defaultBrowserType": "chromium" }, "Nexus 4": { - "userAgent": "Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.57 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.16 Mobile Safari/537.36", "viewport": { "width": 384, "height": 640 @@ -1197,7 +1197,7 @@ "defaultBrowserType": "chromium" }, "Nexus 4 landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.57 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.16 Mobile Safari/537.36", "viewport": { "width": 640, "height": 384 @@ -1208,7 +1208,7 @@ "defaultBrowserType": "chromium" }, "Nexus 5": { - "userAgent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.57 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.16 Mobile Safari/537.36", "viewport": { "width": 360, "height": 640 @@ -1219,7 +1219,7 @@ "defaultBrowserType": "chromium" }, "Nexus 5 landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.57 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.16 Mobile Safari/537.36", "viewport": { "width": 640, "height": 360 @@ -1230,7 +1230,7 @@ "defaultBrowserType": "chromium" }, "Nexus 5X": { - "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 5X Build/OPR4.170623.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.57 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 5X Build/OPR4.170623.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.16 Mobile Safari/537.36", "viewport": { "width": 412, "height": 732 @@ -1241,7 +1241,7 @@ "defaultBrowserType": "chromium" }, "Nexus 5X landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 5X Build/OPR4.170623.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.57 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 5X Build/OPR4.170623.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.16 Mobile Safari/537.36", "viewport": { "width": 732, "height": 412 @@ -1252,7 +1252,7 @@ "defaultBrowserType": "chromium" }, "Nexus 6": { - "userAgent": "Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.57 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.16 Mobile Safari/537.36", "viewport": { "width": 412, "height": 732 @@ -1263,7 +1263,7 @@ "defaultBrowserType": "chromium" }, "Nexus 6 landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.57 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.16 Mobile Safari/537.36", "viewport": { "width": 732, "height": 412 @@ -1274,7 +1274,7 @@ "defaultBrowserType": "chromium" }, "Nexus 6P": { - "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 6P Build/OPP3.170518.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.57 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 6P Build/OPP3.170518.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.16 Mobile Safari/537.36", "viewport": { "width": 412, "height": 732 @@ -1285,7 +1285,7 @@ "defaultBrowserType": "chromium" }, "Nexus 6P landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 6P Build/OPP3.170518.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.57 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 6P Build/OPP3.170518.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.16 Mobile Safari/537.36", "viewport": { "width": 732, "height": 412 @@ -1296,7 +1296,7 @@ "defaultBrowserType": "chromium" }, "Nexus 7": { - "userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.57 Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.16 Safari/537.36", "viewport": { "width": 600, "height": 960 @@ -1307,7 +1307,7 @@ "defaultBrowserType": "chromium" }, "Nexus 7 landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.57 Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.16 Safari/537.36", "viewport": { "width": 960, "height": 600 @@ -1362,7 +1362,7 @@ "defaultBrowserType": "webkit" }, "Pixel 2": { - "userAgent": "Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.57 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.16 Mobile Safari/537.36", "viewport": { "width": 411, "height": 731 @@ -1373,7 +1373,7 @@ "defaultBrowserType": "chromium" }, "Pixel 2 landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.57 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.16 Mobile Safari/537.36", "viewport": { "width": 731, "height": 411 @@ -1384,7 +1384,7 @@ "defaultBrowserType": "chromium" }, "Pixel 2 XL": { - "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Pixel 2 XL Build/OPD1.170816.004) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.57 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Pixel 2 XL Build/OPD1.170816.004) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.16 Mobile Safari/537.36", "viewport": { "width": 411, "height": 823 @@ -1395,7 +1395,7 @@ "defaultBrowserType": "chromium" }, "Pixel 2 XL landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Pixel 2 XL Build/OPD1.170816.004) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.57 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Pixel 2 XL Build/OPD1.170816.004) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.16 Mobile Safari/537.36", "viewport": { "width": 823, "height": 411 @@ -1406,7 +1406,7 @@ "defaultBrowserType": "chromium" }, "Pixel 3": { - "userAgent": "Mozilla/5.0 (Linux; Android 9; Pixel 3 Build/PQ1A.181105.017.A1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.57 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 9; Pixel 3 Build/PQ1A.181105.017.A1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.16 Mobile Safari/537.36", "viewport": { "width": 393, "height": 786 @@ -1417,7 +1417,7 @@ "defaultBrowserType": "chromium" }, "Pixel 3 landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 9; Pixel 3 Build/PQ1A.181105.017.A1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.57 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 9; Pixel 3 Build/PQ1A.181105.017.A1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.16 Mobile Safari/537.36", "viewport": { "width": 786, "height": 393 @@ -1428,7 +1428,7 @@ "defaultBrowserType": "chromium" }, "Pixel 4": { - "userAgent": "Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.57 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.16 Mobile Safari/537.36", "viewport": { "width": 353, "height": 745 @@ -1439,7 +1439,7 @@ "defaultBrowserType": "chromium" }, "Pixel 4 landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.57 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.16 Mobile Safari/537.36", "viewport": { "width": 745, "height": 353 @@ -1450,7 +1450,7 @@ "defaultBrowserType": "chromium" }, "Pixel 4a (5G)": { - "userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 4a (5G)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.57 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 4a (5G)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.16 Mobile Safari/537.36", "screen": { "width": 412, "height": 892 @@ -1465,7 +1465,7 @@ "defaultBrowserType": "chromium" }, "Pixel 4a (5G) landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 4a (5G)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.57 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 4a (5G)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.16 Mobile Safari/537.36", "screen": { "height": 892, "width": 412 @@ -1480,7 +1480,7 @@ "defaultBrowserType": "chromium" }, "Pixel 5": { - "userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.57 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.16 Mobile Safari/537.36", "screen": { "width": 393, "height": 851 @@ -1495,7 +1495,7 @@ "defaultBrowserType": "chromium" }, "Pixel 5 landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.57 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.16 Mobile Safari/537.36", "screen": { "width": 851, "height": 393 @@ -1510,7 +1510,7 @@ "defaultBrowserType": "chromium" }, "Pixel 7": { - "userAgent": "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.57 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.16 Mobile Safari/537.36", "screen": { "width": 412, "height": 915 @@ -1525,7 +1525,7 @@ "defaultBrowserType": "chromium" }, "Pixel 7 landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.57 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.16 Mobile Safari/537.36", "screen": { "width": 915, "height": 412 @@ -1540,7 +1540,7 @@ "defaultBrowserType": "chromium" }, "Moto G4": { - "userAgent": "Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.57 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.16 Mobile Safari/537.36", "viewport": { "width": 360, "height": 640 @@ -1551,7 +1551,7 @@ "defaultBrowserType": "chromium" }, "Moto G4 landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.57 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.16 Mobile Safari/537.36", "viewport": { "width": 640, "height": 360 @@ -1562,7 +1562,7 @@ "defaultBrowserType": "chromium" }, "Desktop Chrome HiDPI": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.57 Safari/537.36", + "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.16 Safari/537.36", "screen": { "width": 1792, "height": 1120 @@ -1577,7 +1577,7 @@ "defaultBrowserType": "chromium" }, "Desktop Edge HiDPI": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.57 Safari/537.36 Edg/132.0.6834.57", + "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.16 Safari/537.36 Edg/133.0.6943.16", "screen": { "width": 1792, "height": 1120 @@ -1622,7 +1622,7 @@ "defaultBrowserType": "webkit" }, "Desktop Chrome": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.57 Safari/537.36", + "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.16 Safari/537.36", "screen": { "width": 1920, "height": 1080 @@ -1637,7 +1637,7 @@ "defaultBrowserType": "chromium" }, "Desktop Edge": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.57 Safari/537.36 Edg/132.0.6834.57", + "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.16 Safari/537.36 Edg/133.0.6943.16", "screen": { "width": 1920, "height": 1080 diff --git a/packages/playwright-core/types/protocol.d.ts b/packages/playwright-core/types/protocol.d.ts index 3a61fbca59..fa1d6121f9 100644 --- a/packages/playwright-core/types/protocol.d.ts +++ b/packages/playwright-core/types/protocol.d.ts @@ -685,8 +685,8 @@ percentage [0 - 100] for scroll driven animations /** * The unique request id. */ - requestId: Network.RequestId; - url?: string; + requestId?: Network.RequestId; + url: string; } /** * Information about the frame affected by an inspector issue. @@ -697,6 +697,20 @@ percentage [0 - 100] for scroll driven animations export type CookieExclusionReason = "ExcludeSameSiteUnspecifiedTreatedAsLax"|"ExcludeSameSiteNoneInsecure"|"ExcludeSameSiteLax"|"ExcludeSameSiteStrict"|"ExcludeInvalidSameParty"|"ExcludeSamePartyCrossPartyContext"|"ExcludeDomainNonASCII"|"ExcludeThirdPartyCookieBlockedInFirstPartySet"|"ExcludeThirdPartyPhaseout"|"ExcludePortMismatch"|"ExcludeSchemeMismatch"; export type CookieWarningReason = "WarnSameSiteUnspecifiedCrossSiteContext"|"WarnSameSiteNoneInsecure"|"WarnSameSiteUnspecifiedLaxAllowUnsafe"|"WarnSameSiteStrictLaxDowngradeStrict"|"WarnSameSiteStrictCrossDowngradeStrict"|"WarnSameSiteStrictCrossDowngradeLax"|"WarnSameSiteLaxCrossDowngradeStrict"|"WarnSameSiteLaxCrossDowngradeLax"|"WarnAttributeValueExceedsMaxSize"|"WarnDomainNonASCII"|"WarnThirdPartyPhaseout"|"WarnCrossSiteRedirectDowngradeChangesInclusion"|"WarnDeprecationTrialMetadata"|"WarnThirdPartyCookieHeuristic"; export type CookieOperation = "SetCookie"|"ReadCookie"; + /** + * Represents the category of insight that a cookie issue falls under. + */ + export type InsightType = "GitHubResource"|"GracePeriod"|"Heuristics"; + /** + * Information about the suggested solution to a cookie issue. + */ + export interface CookieIssueInsight { + type: InsightType; + /** + * Link to table entry in third-party cookie migration readiness list. + */ + tableEntryUrl?: string; + } /** * This information is currently necessary, as the front-end has a difficult time finding a specific cookie. With this, we can convey specific error @@ -721,6 +735,10 @@ may be used by the front-end as additional context. siteForCookies?: string; cookieUrl?: string; request?: AffectedRequest; + /** + * The recommended solution to the issue. + */ + insight?: CookieIssueInsight; } export type MixedContentResolutionStatus = "MixedContentBlocked"|"MixedContentAutomaticallyUpgraded"|"MixedContentWarning"; export type MixedContentResourceType = "AttributionSrc"|"Audio"|"Beacon"|"CSPReport"|"Download"|"EventSource"|"Favicon"|"Font"|"Form"|"Frame"|"Image"|"Import"|"JSON"|"Manifest"|"Ping"|"PluginData"|"PluginResource"|"Prefetch"|"Resource"|"Script"|"ServiceWorker"|"SharedWorker"|"SpeculationRules"|"Stylesheet"|"Track"|"Video"|"Worker"|"XMLHttpRequest"|"XSLT"; @@ -758,7 +776,7 @@ Does not always exist (e.g. for unsafe form submission urls). * Enum indicating the reason a response has been blocked. These reasons are refinements of the net error BLOCKED_BY_RESPONSE. */ - export type BlockedByResponseReason = "CoepFrameResourceNeedsCoepHeader"|"CoopSandboxedIFrameCannotNavigateToCoopPage"|"CorpNotSameOrigin"|"CorpNotSameOriginAfterDefaultedToSameOriginByCoep"|"CorpNotSameOriginAfterDefaultedToSameOriginByDip"|"CorpNotSameOriginAfterDefaultedToSameOriginByCoepAndDip"|"CorpNotSameSite"; + export type BlockedByResponseReason = "CoepFrameResourceNeedsCoepHeader"|"CoopSandboxedIFrameCannotNavigateToCoopPage"|"CorpNotSameOrigin"|"CorpNotSameOriginAfterDefaultedToSameOriginByCoep"|"CorpNotSameOriginAfterDefaultedToSameOriginByDip"|"CorpNotSameOriginAfterDefaultedToSameOriginByCoepAndDip"|"CorpNotSameSite"|"SRIMessageSignatureMismatch"; /** * Details for a request that has been blocked with the BLOCKED_BY_RESPONSE code. Currently only used for COEP/COOP, but may be extended to include @@ -963,6 +981,15 @@ features, encourage the use of new ones, and provide general guidance. failureMessage: string; requestId?: Network.RequestId; } + export type SelectElementAccessibilityIssueReason = "DisallowedSelectChild"|"DisallowedOptGroupChild"|"NonPhrasingContentOptionChild"|"InteractiveContentOptionChild"|"InteractiveContentLegendChild"; + /** + * This isue warns about errors in the select element content model. + */ + export interface SelectElementAccessibilityIssueDetails { + nodeId: DOM.BackendNodeId; + selectElementAccessibilityIssueReason: SelectElementAccessibilityIssueReason; + hasDisallowedAttributes: boolean; + } export type StyleSheetLoadingIssueReason = "LateImportRule"|"RequestFailed"; /** * This issue warns when a referenced stylesheet couldn't be loaded. @@ -1005,7 +1032,7 @@ registrations being ignored. optional fields in InspectorIssueDetails to convey more specific information about the kind of issue. */ - export type InspectorIssueCode = "CookieIssue"|"MixedContentIssue"|"BlockedByResponseIssue"|"HeavyAdIssue"|"ContentSecurityPolicyIssue"|"SharedArrayBufferIssue"|"LowTextContrastIssue"|"CorsIssue"|"AttributionReportingIssue"|"QuirksModeIssue"|"NavigatorUserAgentIssue"|"GenericIssue"|"DeprecationIssue"|"ClientHintIssue"|"FederatedAuthRequestIssue"|"BounceTrackingIssue"|"CookieDeprecationMetadataIssue"|"StylesheetLoadingIssue"|"FederatedAuthUserInfoRequestIssue"|"PropertyRuleIssue"|"SharedDictionaryIssue"; + export type InspectorIssueCode = "CookieIssue"|"MixedContentIssue"|"BlockedByResponseIssue"|"HeavyAdIssue"|"ContentSecurityPolicyIssue"|"SharedArrayBufferIssue"|"LowTextContrastIssue"|"CorsIssue"|"AttributionReportingIssue"|"QuirksModeIssue"|"NavigatorUserAgentIssue"|"GenericIssue"|"DeprecationIssue"|"ClientHintIssue"|"FederatedAuthRequestIssue"|"BounceTrackingIssue"|"CookieDeprecationMetadataIssue"|"StylesheetLoadingIssue"|"FederatedAuthUserInfoRequestIssue"|"PropertyRuleIssue"|"SharedDictionaryIssue"|"SelectElementAccessibilityIssue"; /** * This struct holds a list of optional fields with additional information specific to the kind of issue. When adding a new issue code, please also @@ -1033,6 +1060,7 @@ add a new optional field to this type. propertyRuleIssueDetails?: PropertyRuleIssueDetails; federatedAuthUserInfoRequestIssueDetails?: FederatedAuthUserInfoRequestIssueDetails; sharedDictionaryIssueDetails?: SharedDictionaryIssueDetails; + selectElementAccessibilityIssueDetails?: SelectElementAccessibilityIssueDetails; } /** * A unique id for a DevTools inspector issue. Allows other entities (e.g. @@ -1534,7 +1562,7 @@ events afterwards if enabled and recording. */ windowState?: WindowState; } - export type PermissionType = "accessibilityEvents"|"audioCapture"|"backgroundSync"|"backgroundFetch"|"capturedSurfaceControl"|"clipboardReadWrite"|"clipboardSanitizedWrite"|"displayCapture"|"durableStorage"|"flash"|"geolocation"|"idleDetection"|"localFonts"|"midi"|"midiSysex"|"nfc"|"notifications"|"paymentHandler"|"periodicBackgroundSync"|"protectedMediaIdentifier"|"sensors"|"storageAccess"|"speakerSelection"|"topLevelStorageAccess"|"videoCapture"|"videoCapturePanTiltZoom"|"wakeLockScreen"|"wakeLockSystem"|"webAppInstallation"|"windowManagement"; + export type PermissionType = "ar"|"audioCapture"|"automaticFullscreen"|"backgroundFetch"|"backgroundSync"|"cameraPanTiltZoom"|"capturedSurfaceControl"|"clipboardReadWrite"|"clipboardSanitizedWrite"|"displayCapture"|"durableStorage"|"geolocation"|"handTracking"|"idleDetection"|"keyboardLock"|"localFonts"|"midi"|"midiSysex"|"nfc"|"notifications"|"paymentHandler"|"periodicBackgroundSync"|"pointerLock"|"protectedMediaIdentifier"|"sensors"|"smartCard"|"speakerSelection"|"storageAccess"|"topLevelStorageAccess"|"videoCapture"|"vr"|"wakeLockScreen"|"wakeLockSystem"|"webAppInstallation"|"webPrinting"|"windowManagement"; export type PermissionSetting = "granted"|"denied"|"prompt"; /** * Definition of PermissionDescriptor defined in the Permissions API: @@ -1961,6 +1989,19 @@ inspector" rules), "regular" for regular stylesheets. */ matches: RuleMatch[]; } + /** + * CSS style coming from animations with the name of the animation. + */ + export interface CSSAnimationStyle { + /** + * The name of the animation. + */ + name?: string; + /** + * The style coming from the animation. + */ + style: CSSStyle; + } /** * Inherited CSS rule collection from ancestor node. */ @@ -1974,6 +2015,19 @@ inspector" rules), "regular" for regular stylesheets. */ matchedCSSRules: RuleMatch[]; } + /** + * Inherited CSS style collection for animated styles from ancestor node. + */ + export interface InheritedAnimatedStyleEntry { + /** + * Styles coming from the animations of the ancestor, if any, in the style inheritance chain. + */ + animationStyles?: CSSAnimationStyle[]; + /** + * The style coming from the transitions of the ancestor, if any, in the style inheritance chain. + */ + transitionsStyle?: CSSStyle; + } /** * Inherited pseudo element matches from pseudos of an ancestor node. */ @@ -2897,6 +2951,21 @@ the browser. } export type forcePseudoStateReturnValue = { } + /** + * Ensures that the given node is in its starting-style state. + */ + export type forceStartingStyleParameters = { + /** + * The element id for which to force the starting-style state. + */ + nodeId: DOM.NodeId; + /** + * Boolean indicating if this is on or off. + */ + forced: boolean; + } + export type forceStartingStyleReturnValue = { + } export type getBackgroundColorsParameters = { /** * Id of the node to get background colors for. @@ -2934,6 +3003,46 @@ be ignored (as if the image had failed to load). */ computedStyle: CSSComputedStyleProperty[]; } + /** + * Resolve the specified values in the context of the provided element. +For example, a value of '1em' is evaluated according to the computed +'font-size' of the element and a value 'calc(1px + 2px)' will be +resolved to '3px'. + */ + export type resolveValuesParameters = { + /** + * Substitution functions (var()/env()/attr()) and cascade-dependent +keywords (revert/revert-layer) do not work. + */ + values: string[]; + /** + * Id of the node in whose context the expression is evaluated + */ + nodeId: DOM.NodeId; + /** + * Only longhands and custom property names are accepted. + */ + propertyName?: string; + /** + * Pseudo element type, only works for pseudo elements that generate +elements in the tree, such as ::before and ::after. + */ + pseudoType?: DOM.PseudoType; + /** + * Pseudo element custom ident. + */ + pseudoIdentifier?: string; + } + export type resolveValuesReturnValue = { + results: string[]; + } + export type getLonghandPropertiesParameters = { + shorthandName: string; + value: string; + } + export type getLonghandPropertiesReturnValue = { + longhandProperties: CSSProperty[]; + } /** * Returns the styles defined inline (explicitly in the "style" attribute and implicitly, using DOM attributes) for a DOM node identified by `nodeId`. @@ -2951,6 +3060,28 @@ attributes) for a DOM node identified by `nodeId`. */ attributesStyle?: CSSStyle; } + /** + * Returns the styles coming from animations & transitions +including the animation & transition styles coming from inheritance chain. + */ + export type getAnimatedStylesForNodeParameters = { + nodeId: DOM.NodeId; + } + export type getAnimatedStylesForNodeReturnValue = { + /** + * Styles coming from animations. + */ + animationStyles?: CSSAnimationStyle[]; + /** + * Style coming from transitions. + */ + transitionsStyle?: CSSStyle; + /** + * Inherited style entries for animationsStyle and transitionsStyle from +the inheritance chain of the element. + */ + inherited?: InheritedAnimatedStyleEntry[]; + } /** * Returns requested styles for a DOM node identified by `nodeId`. */ @@ -3603,7 +3734,7 @@ front-end. /** * Pseudo element type. */ - export type PseudoType = "first-line"|"first-letter"|"check"|"before"|"after"|"select-arrow"|"marker"|"backdrop"|"column"|"selection"|"search-text"|"target-text"|"spelling-error"|"grammar-error"|"highlight"|"first-line-inherited"|"scroll-marker"|"scroll-marker-group"|"scroll-next-button"|"scroll-prev-button"|"scrollbar"|"scrollbar-thumb"|"scrollbar-button"|"scrollbar-track"|"scrollbar-track-piece"|"scrollbar-corner"|"resizer"|"input-list-button"|"view-transition"|"view-transition-group"|"view-transition-image-pair"|"view-transition-old"|"view-transition-new"|"placeholder"|"file-selector-button"|"details-content"|"picker"; + export type PseudoType = "first-line"|"first-letter"|"checkmark"|"before"|"after"|"picker-icon"|"marker"|"backdrop"|"column"|"selection"|"search-text"|"target-text"|"spelling-error"|"grammar-error"|"highlight"|"first-line-inherited"|"scroll-marker"|"scroll-marker-group"|"scroll-button"|"scrollbar"|"scrollbar-thumb"|"scrollbar-button"|"scrollbar-track"|"scrollbar-track-piece"|"scrollbar-corner"|"resizer"|"input-list-button"|"view-transition"|"view-transition-group"|"view-transition-image-pair"|"view-transition-old"|"view-transition-new"|"placeholder"|"file-selector-button"|"details-content"|"picker"; /** * Shadow root type. */ @@ -8616,7 +8747,7 @@ applicable or not known. /** * The reason why request was blocked. */ - export type BlockedReason = "other"|"csp"|"mixed-content"|"origin"|"inspector"|"subresource-filter"|"content-type"|"coep-frame-resource-needs-coep-header"|"coop-sandboxed-iframe-cannot-navigate-to-coop-page"|"corp-not-same-origin"|"corp-not-same-origin-after-defaulted-to-same-origin-by-coep"|"corp-not-same-origin-after-defaulted-to-same-origin-by-dip"|"corp-not-same-origin-after-defaulted-to-same-origin-by-coep-and-dip"|"corp-not-same-site"; + export type BlockedReason = "other"|"csp"|"mixed-content"|"origin"|"inspector"|"subresource-filter"|"content-type"|"coep-frame-resource-needs-coep-header"|"coop-sandboxed-iframe-cannot-navigate-to-coop-page"|"corp-not-same-origin"|"corp-not-same-origin-after-defaulted-to-same-origin-by-coep"|"corp-not-same-origin-after-defaulted-to-same-origin-by-dip"|"corp-not-same-origin-after-defaulted-to-same-origin-by-coep-and-dip"|"corp-not-same-site"|"sri-message-signature-mismatch"; /** * The reason why request was blocked. */ @@ -9917,6 +10048,9 @@ are represented by the invalid cookie line string instead of a proper cookie. blockedCookies: BlockedSetCookieWithReason[]; /** * Raw response headers as they were received over the wire. +Duplicate headers in the response are represented as a single key with their values +concatentated using `\n` as the separator. +See also `headersText` that contains verbatim text for HTTP/1.*. */ headers: Headers; /** @@ -9962,6 +10096,9 @@ Only one responseReceivedEarlyHints may be fired for eached responseReceived eve requestId: RequestId; /** * Raw response headers as they were received over the wire. +Duplicate headers in the response are represented as a single key with their values +concatentated using `\n` as the separator. +See also `headersText` that contains verbatim text for HTTP/1.*. */ headers: Headers; } @@ -9978,7 +10115,7 @@ or after the response was received. of the operation already exists und thus, the operation was abort preemptively (e.g. a cache hit). */ - status: "Ok"|"InvalidArgument"|"MissingIssuerKeys"|"FailedPrecondition"|"ResourceExhausted"|"AlreadyExists"|"ResourceLimited"|"Unauthorized"|"BadResponse"|"InternalError"|"UnknownError"|"FulfilledLocally"; + status: "Ok"|"InvalidArgument"|"MissingIssuerKeys"|"FailedPrecondition"|"ResourceExhausted"|"AlreadyExists"|"ResourceLimited"|"Unauthorized"|"BadResponse"|"InternalError"|"UnknownError"|"FulfilledLocally"|"SiteIssuerLimit"; type: TrustTokenOperationType; requestId: RequestId; /** @@ -10672,6 +10809,26 @@ should be omitted for worker targets. export type loadNetworkResourceReturnValue = { resource: LoadNetworkResourcePageResult; } + /** + * Sets Controls for third-party cookie access +Page reload is required before the new cookie bahavior will be observed + */ + export type setCookieControlsParameters = { + /** + * Whether 3pc restriction is enabled. + */ + enableThirdPartyCookieRestriction: boolean; + /** + * Whether 3pc grace period exception should be enabled; false by default. + */ + disableThirdPartyCookieMetadata: boolean; + /** + * Whether 3pc heuristics exceptions should be enabled; false by default. + */ + disableThirdPartyCookieHeuristics: boolean; + } + export type setCookieControlsReturnValue = { + } } /** @@ -11545,7 +11702,7 @@ as an ad. * All Permissions Policy features. This enum should match the one defined in third_party/blink/renderer/core/permissions_policy/permissions_policy_features.json5. */ - export type PermissionsPolicyFeature = "accelerometer"|"all-screens-capture"|"ambient-light-sensor"|"attribution-reporting"|"autoplay"|"bluetooth"|"browsing-topics"|"camera"|"captured-surface-control"|"ch-dpr"|"ch-device-memory"|"ch-downlink"|"ch-ect"|"ch-prefers-color-scheme"|"ch-prefers-reduced-motion"|"ch-prefers-reduced-transparency"|"ch-rtt"|"ch-save-data"|"ch-ua"|"ch-ua-arch"|"ch-ua-bitness"|"ch-ua-platform"|"ch-ua-model"|"ch-ua-mobile"|"ch-ua-form-factors"|"ch-ua-full-version"|"ch-ua-full-version-list"|"ch-ua-platform-version"|"ch-ua-wow64"|"ch-viewport-height"|"ch-viewport-width"|"ch-width"|"clipboard-read"|"clipboard-write"|"compute-pressure"|"controlled-frame"|"cross-origin-isolated"|"deferred-fetch"|"digital-credentials-get"|"direct-sockets"|"direct-sockets-private"|"display-capture"|"document-domain"|"encrypted-media"|"execution-while-out-of-viewport"|"execution-while-not-rendered"|"fenced-unpartitioned-storage-read"|"focus-without-user-activation"|"fullscreen"|"frobulate"|"gamepad"|"geolocation"|"gyroscope"|"hid"|"identity-credentials-get"|"idle-detection"|"interest-cohort"|"join-ad-interest-group"|"keyboard-map"|"local-fonts"|"magnetometer"|"media-playback-while-not-visible"|"microphone"|"midi"|"otp-credentials"|"payment"|"picture-in-picture"|"popins"|"private-aggregation"|"private-state-token-issuance"|"private-state-token-redemption"|"publickey-credentials-create"|"publickey-credentials-get"|"run-ad-auction"|"screen-wake-lock"|"serial"|"shared-autofill"|"shared-storage"|"shared-storage-select-url"|"smart-card"|"speaker-selection"|"storage-access"|"sub-apps"|"sync-xhr"|"unload"|"usb"|"usb-unrestricted"|"vertical-scroll"|"web-app-installation"|"web-printing"|"web-share"|"window-management"|"xr-spatial-tracking"; + export type PermissionsPolicyFeature = "accelerometer"|"all-screens-capture"|"ambient-light-sensor"|"attribution-reporting"|"autoplay"|"bluetooth"|"browsing-topics"|"camera"|"captured-surface-control"|"ch-dpr"|"ch-device-memory"|"ch-downlink"|"ch-ect"|"ch-prefers-color-scheme"|"ch-prefers-reduced-motion"|"ch-prefers-reduced-transparency"|"ch-rtt"|"ch-save-data"|"ch-ua"|"ch-ua-arch"|"ch-ua-bitness"|"ch-ua-platform"|"ch-ua-model"|"ch-ua-mobile"|"ch-ua-form-factors"|"ch-ua-full-version"|"ch-ua-full-version-list"|"ch-ua-platform-version"|"ch-ua-wow64"|"ch-viewport-height"|"ch-viewport-width"|"ch-width"|"clipboard-read"|"clipboard-write"|"compute-pressure"|"controlled-frame"|"cross-origin-isolated"|"deferred-fetch"|"deferred-fetch-minimal"|"digital-credentials-get"|"direct-sockets"|"direct-sockets-private"|"display-capture"|"document-domain"|"encrypted-media"|"execution-while-out-of-viewport"|"execution-while-not-rendered"|"fenced-unpartitioned-storage-read"|"focus-without-user-activation"|"fullscreen"|"frobulate"|"gamepad"|"geolocation"|"gyroscope"|"hid"|"identity-credentials-get"|"idle-detection"|"interest-cohort"|"join-ad-interest-group"|"keyboard-map"|"local-fonts"|"magnetometer"|"media-playback-while-not-visible"|"microphone"|"midi"|"otp-credentials"|"payment"|"picture-in-picture"|"popins"|"private-aggregation"|"private-state-token-issuance"|"private-state-token-redemption"|"publickey-credentials-create"|"publickey-credentials-get"|"run-ad-auction"|"screen-wake-lock"|"serial"|"shared-autofill"|"shared-storage"|"shared-storage-select-url"|"smart-card"|"speaker-selection"|"storage-access"|"sub-apps"|"sync-xhr"|"unload"|"usb"|"usb-unrestricted"|"vertical-scroll"|"web-app-installation"|"web-printing"|"web-share"|"window-management"|"xr-spatial-tracking"; /** * Reason for a permissions policy feature to be disabled. */ @@ -15519,6 +15676,14 @@ Parts of the URL other than those constituting origin are ignored. * The initial URL the page will be navigated to. An empty string indicates about:blank. */ url: string; + /** + * Frame left origin in DIP (headless chrome only). + */ + left?: number; + /** + * Frame top origin in DIP (headless chrome only). + */ + top?: number; /** * Frame width in DIP (headless chrome only). */ @@ -17151,6 +17316,16 @@ possible for multiple rule sets and links to trigger a single attempt. ruleSetIds: RuleSetId[]; nodeIds: DOM.BackendNodeId[]; } + /** + * Chrome manages different types of preloads together using a +concept of preloading pipeline. For example, if a site uses a +SpeculationRules for prerender, Chrome first starts a prefetch and +then upgrades it to prerender. + +CDP events for them are emitted separately but they share +`PreloadPipelineId`. + */ + export type PreloadPipelineId = string; /** * List of FinalStatus reasons for Prerender2. */ @@ -17198,6 +17373,7 @@ filter out the ones that aren't necessary to the developers. */ export type prefetchStatusUpdatedPayload = { key: PreloadingAttemptKey; + pipelineId: PreloadPipelineId; /** * The frame id of the frame initiating prefetch. */ @@ -17212,6 +17388,7 @@ filter out the ones that aren't necessary to the developers. */ export type prerenderStatusUpdatedPayload = { key: PreloadingAttemptKey; + pipelineId: PreloadPipelineId; status: PreloadingStatus; prerenderStatus?: PrerenderFinalStatus; /** @@ -17922,6 +18099,10 @@ variables as its properties. * Content hash of the script, SHA-256. */ hash: string; + /** + * For Wasm modules, the content of the `build_id` custom section. + */ + buildId: string; /** * Embedder-specific auxiliary data likely matching {isDefault: boolean, type: 'default'|'isolated'|'worker', frameId: string} */ @@ -17996,6 +18177,10 @@ scripts upon enabling debugger. * Content hash of the script, SHA-256. */ hash: string; + /** + * For Wasm modules, the content of the `build_id` custom section. + */ + buildId: string; /** * Embedder-specific auxiliary data likely matching {isDefault: boolean, type: 'default'|'isolated'|'worker', frameId: string} */ @@ -20507,9 +20692,13 @@ Error was thrown. "CSS.disable": CSS.disableParameters; "CSS.enable": CSS.enableParameters; "CSS.forcePseudoState": CSS.forcePseudoStateParameters; + "CSS.forceStartingStyle": CSS.forceStartingStyleParameters; "CSS.getBackgroundColors": CSS.getBackgroundColorsParameters; "CSS.getComputedStyleForNode": CSS.getComputedStyleForNodeParameters; + "CSS.resolveValues": CSS.resolveValuesParameters; + "CSS.getLonghandProperties": CSS.getLonghandPropertiesParameters; "CSS.getInlineStylesForNode": CSS.getInlineStylesForNodeParameters; + "CSS.getAnimatedStylesForNode": CSS.getAnimatedStylesForNodeParameters; "CSS.getMatchedStylesForNode": CSS.getMatchedStylesForNodeParameters; "CSS.getMediaQueries": CSS.getMediaQueriesParameters; "CSS.getPlatformFontsForNode": CSS.getPlatformFontsForNodeParameters; @@ -20751,6 +20940,7 @@ Error was thrown. "Network.getSecurityIsolationStatus": Network.getSecurityIsolationStatusParameters; "Network.enableReportingApi": Network.enableReportingApiParameters; "Network.loadNetworkResource": Network.loadNetworkResourceParameters; + "Network.setCookieControls": Network.setCookieControlsParameters; "Overlay.disable": Overlay.disableParameters; "Overlay.enable": Overlay.enableParameters; "Overlay.getHighlightObjectForTest": Overlay.getHighlightObjectForTestParameters; @@ -21119,9 +21309,13 @@ Error was thrown. "CSS.disable": CSS.disableReturnValue; "CSS.enable": CSS.enableReturnValue; "CSS.forcePseudoState": CSS.forcePseudoStateReturnValue; + "CSS.forceStartingStyle": CSS.forceStartingStyleReturnValue; "CSS.getBackgroundColors": CSS.getBackgroundColorsReturnValue; "CSS.getComputedStyleForNode": CSS.getComputedStyleForNodeReturnValue; + "CSS.resolveValues": CSS.resolveValuesReturnValue; + "CSS.getLonghandProperties": CSS.getLonghandPropertiesReturnValue; "CSS.getInlineStylesForNode": CSS.getInlineStylesForNodeReturnValue; + "CSS.getAnimatedStylesForNode": CSS.getAnimatedStylesForNodeReturnValue; "CSS.getMatchedStylesForNode": CSS.getMatchedStylesForNodeReturnValue; "CSS.getMediaQueries": CSS.getMediaQueriesReturnValue; "CSS.getPlatformFontsForNode": CSS.getPlatformFontsForNodeReturnValue; @@ -21363,6 +21557,7 @@ Error was thrown. "Network.getSecurityIsolationStatus": Network.getSecurityIsolationStatusReturnValue; "Network.enableReportingApi": Network.enableReportingApiReturnValue; "Network.loadNetworkResource": Network.loadNetworkResourceReturnValue; + "Network.setCookieControls": Network.setCookieControlsReturnValue; "Overlay.disable": Overlay.disableReturnValue; "Overlay.enable": Overlay.enableReturnValue; "Overlay.getHighlightObjectForTest": Overlay.getHighlightObjectForTestReturnValue; From b339d457e3432d13d997a3787ae6c3e77bfaa0e5 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Fri, 17 Jan 2025 10:17:26 -0800 Subject: [PATCH 11/19] chore: dogfood jest-style aria snapshots (#34365) --- .../should-traverse-up-down-1.yml | 13 ++++++ .../should-traverse-up-down-2.yml | 13 ++++++ .../should-traverse-up-down-3.yml | 13 ++++++ .../stable-test-runner/package-lock.json | 46 +++++++++---------- .../stable-test-runner/package.json | 2 +- .../playwright-test/ui-mode-test-tree.spec.ts | 3 ++ 6 files changed, 66 insertions(+), 24 deletions(-) create mode 100644 tests/playwright-test/__screenshots__/ui-mode-test-tree.spec.ts/should-traverse-up-down-1.yml create mode 100644 tests/playwright-test/__screenshots__/ui-mode-test-tree.spec.ts/should-traverse-up-down-2.yml create mode 100644 tests/playwright-test/__screenshots__/ui-mode-test-tree.spec.ts/should-traverse-up-down-3.yml diff --git a/tests/playwright-test/__screenshots__/ui-mode-test-tree.spec.ts/should-traverse-up-down-1.yml b/tests/playwright-test/__screenshots__/ui-mode-test-tree.spec.ts/should-traverse-up-down-1.yml new file mode 100644 index 0000000000..2f556153ec --- /dev/null +++ b/tests/playwright-test/__screenshots__/ui-mode-test-tree.spec.ts/should-traverse-up-down-1.yml @@ -0,0 +1,13 @@ +- tree: + - treeitem "[icon-circle-outline] a.test.ts" [expanded]: + - group: + - treeitem "[icon-circle-outline] passes" [selected]: + - button "Run" + - button "Show source" + - button "Watch" + - treeitem "[icon-circle-outline] fails" + - treeitem "[icon-circle-outline] suite" + - treeitem "[icon-circle-outline] b.test.ts" [expanded]: + - group: + - treeitem "[icon-circle-outline] passes" + - treeitem "[icon-circle-outline] fails" \ No newline at end of file diff --git a/tests/playwright-test/__screenshots__/ui-mode-test-tree.spec.ts/should-traverse-up-down-2.yml b/tests/playwright-test/__screenshots__/ui-mode-test-tree.spec.ts/should-traverse-up-down-2.yml new file mode 100644 index 0000000000..7a9622befd --- /dev/null +++ b/tests/playwright-test/__screenshots__/ui-mode-test-tree.spec.ts/should-traverse-up-down-2.yml @@ -0,0 +1,13 @@ +- tree: + - treeitem "[icon-circle-outline] a.test.ts" [expanded]: + - group: + - treeitem "[icon-circle-outline] passes" + - treeitem "[icon-circle-outline] fails" [selected]: + - button "Run" + - button "Show source" + - button "Watch" + - treeitem "[icon-circle-outline] suite" + - treeitem "[icon-circle-outline] b.test.ts" [expanded]: + - group: + - treeitem "[icon-circle-outline] passes" + - treeitem "[icon-circle-outline] fails" \ No newline at end of file diff --git a/tests/playwright-test/__screenshots__/ui-mode-test-tree.spec.ts/should-traverse-up-down-3.yml b/tests/playwright-test/__screenshots__/ui-mode-test-tree.spec.ts/should-traverse-up-down-3.yml new file mode 100644 index 0000000000..2f556153ec --- /dev/null +++ b/tests/playwright-test/__screenshots__/ui-mode-test-tree.spec.ts/should-traverse-up-down-3.yml @@ -0,0 +1,13 @@ +- tree: + - treeitem "[icon-circle-outline] a.test.ts" [expanded]: + - group: + - treeitem "[icon-circle-outline] passes" [selected]: + - button "Run" + - button "Show source" + - button "Watch" + - treeitem "[icon-circle-outline] fails" + - treeitem "[icon-circle-outline] suite" + - treeitem "[icon-circle-outline] b.test.ts" [expanded]: + - group: + - treeitem "[icon-circle-outline] passes" + - treeitem "[icon-circle-outline] fails" \ No newline at end of file diff --git a/tests/playwright-test/stable-test-runner/package-lock.json b/tests/playwright-test/stable-test-runner/package-lock.json index 191deff43f..e3abdb32c5 100644 --- a/tests/playwright-test/stable-test-runner/package-lock.json +++ b/tests/playwright-test/stable-test-runner/package-lock.json @@ -5,16 +5,16 @@ "packages": { "": { "dependencies": { - "@playwright/test": "1.49.0-beta-1731772650000" + "@playwright/test": "1.50.0-alpha-2025-01-17" } }, "node_modules/@playwright/test": { - "version": "1.49.0-beta-1731772650000", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.49.0-beta-1731772650000.tgz", - "integrity": "sha512-0d7DBoGZ23lv1/EkNoFXj5fQ9k3qlYHRE7la68zXihtjTH1DdwEtgdMgXR4UEScF2r/YNXaGRZ7sK/DVu9f6Aw==", + "version": "1.50.0-alpha-2025-01-17", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.50.0-alpha-2025-01-17.tgz", + "integrity": "sha512-fMUwMcP0YE2knged9GJXqv3fpT2xoywTtqYaSzpZmjnNESF+CUUAGY2hHm9/fz/v9ijcjyd62hYFbqS5KeKuHQ==", "license": "Apache-2.0", "dependencies": { - "playwright": "1.49.0-beta-1731772650000" + "playwright": "1.50.0-alpha-2025-01-17" }, "bin": { "playwright": "cli.js" @@ -38,12 +38,12 @@ } }, "node_modules/playwright": { - "version": "1.49.0-beta-1731772650000", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.49.0-beta-1731772650000.tgz", - "integrity": "sha512-+LLjx+DMLjx1qiBtLuURTLV3LmFxvQOSaVp9EDMH/qYpclhsp/W41vNxxZEqf8CIsL0BKHIVQYU+6D3OLnJq8g==", + "version": "1.50.0-alpha-2025-01-17", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.50.0-alpha-2025-01-17.tgz", + "integrity": "sha512-LRavQ9Qu27nHvJ57f+7UDBTAEWhGKV+MS2qLAJpF8HXtfSMVlLK82W9Oba41lCNUzgLoAuFv0wCO/RcHqLz7yQ==", "license": "Apache-2.0", "dependencies": { - "playwright-core": "1.49.0-beta-1731772650000" + "playwright-core": "1.50.0-alpha-2025-01-17" }, "bin": { "playwright": "cli.js" @@ -56,9 +56,9 @@ } }, "node_modules/playwright-core": { - "version": "1.49.0-beta-1731772650000", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.49.0-beta-1731772650000.tgz", - "integrity": "sha512-W1HbioibWPPsazFzU/PL9QzGEGubxizQOyMON8/d7DjOpNBqfzuemNuAsNBXucUEVbUlOOzMuoAEX/iqXUOl6Q==", + "version": "1.50.0-alpha-2025-01-17", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.50.0-alpha-2025-01-17.tgz", + "integrity": "sha512-XkoLZ+7J5ybDq68xSlofPziH1Y8It9LpMisxtBfebjKWbVY8BzctlB1Da9udKDP0oWQPNq4tUnwW0hkeET3lUg==", "license": "Apache-2.0", "bin": { "playwright-core": "cli.js" @@ -70,11 +70,11 @@ }, "dependencies": { "@playwright/test": { - "version": "1.49.0-beta-1731772650000", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.49.0-beta-1731772650000.tgz", - "integrity": "sha512-0d7DBoGZ23lv1/EkNoFXj5fQ9k3qlYHRE7la68zXihtjTH1DdwEtgdMgXR4UEScF2r/YNXaGRZ7sK/DVu9f6Aw==", + "version": "1.50.0-alpha-2025-01-17", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.50.0-alpha-2025-01-17.tgz", + "integrity": "sha512-fMUwMcP0YE2knged9GJXqv3fpT2xoywTtqYaSzpZmjnNESF+CUUAGY2hHm9/fz/v9ijcjyd62hYFbqS5KeKuHQ==", "requires": { - "playwright": "1.49.0-beta-1731772650000" + "playwright": "1.50.0-alpha-2025-01-17" } }, "fsevents": { @@ -84,18 +84,18 @@ "optional": true }, "playwright": { - "version": "1.49.0-beta-1731772650000", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.49.0-beta-1731772650000.tgz", - "integrity": "sha512-+LLjx+DMLjx1qiBtLuURTLV3LmFxvQOSaVp9EDMH/qYpclhsp/W41vNxxZEqf8CIsL0BKHIVQYU+6D3OLnJq8g==", + "version": "1.50.0-alpha-2025-01-17", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.50.0-alpha-2025-01-17.tgz", + "integrity": "sha512-LRavQ9Qu27nHvJ57f+7UDBTAEWhGKV+MS2qLAJpF8HXtfSMVlLK82W9Oba41lCNUzgLoAuFv0wCO/RcHqLz7yQ==", "requires": { "fsevents": "2.3.2", - "playwright-core": "1.49.0-beta-1731772650000" + "playwright-core": "1.50.0-alpha-2025-01-17" } }, "playwright-core": { - "version": "1.49.0-beta-1731772650000", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.49.0-beta-1731772650000.tgz", - "integrity": "sha512-W1HbioibWPPsazFzU/PL9QzGEGubxizQOyMON8/d7DjOpNBqfzuemNuAsNBXucUEVbUlOOzMuoAEX/iqXUOl6Q==" + "version": "1.50.0-alpha-2025-01-17", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.50.0-alpha-2025-01-17.tgz", + "integrity": "sha512-XkoLZ+7J5ybDq68xSlofPziH1Y8It9LpMisxtBfebjKWbVY8BzctlB1Da9udKDP0oWQPNq4tUnwW0hkeET3lUg==" } } } diff --git a/tests/playwright-test/stable-test-runner/package.json b/tests/playwright-test/stable-test-runner/package.json index eb5df89830..559cfc10e1 100644 --- a/tests/playwright-test/stable-test-runner/package.json +++ b/tests/playwright-test/stable-test-runner/package.json @@ -1,6 +1,6 @@ { "private": true, "dependencies": { - "@playwright/test": "1.49.0-beta-1731772650000" + "@playwright/test": "1.50.0-alpha-2025-01-17" } } diff --git a/tests/playwright-test/ui-mode-test-tree.spec.ts b/tests/playwright-test/ui-mode-test-tree.spec.ts index 69abd60cfd..e663b986b6 100644 --- a/tests/playwright-test/ui-mode-test-tree.spec.ts +++ b/tests/playwright-test/ui-mode-test-tree.spec.ts @@ -164,6 +164,7 @@ test('should traverse up/down', async ({ runUITest }) => { - treeitem "[icon-circle-outline] fails" - treeitem "[icon-circle-outline] suite" [expanded=false] `); + await expect(page.getByTestId('test-tree')).toMatchAriaSnapshot(); await page.keyboard.press('ArrowDown'); await expect.poll(dumpTestTree(page)).toContain(` @@ -180,6 +181,7 @@ test('should traverse up/down', async ({ runUITest }) => { - treeitem "[icon-circle-outline] fails" [selected] - treeitem "[icon-circle-outline] suite" [expanded=false] `); + await expect(page.getByTestId('test-tree')).toMatchAriaSnapshot(); await page.keyboard.press('ArrowUp'); await expect.poll(dumpTestTree(page)).toContain(` @@ -196,6 +198,7 @@ test('should traverse up/down', async ({ runUITest }) => { - treeitem "[icon-circle-outline] fails" - treeitem "[icon-circle-outline] suite" [expanded=false] `); + await expect(page.getByTestId('test-tree')).toMatchAriaSnapshot(); }); test('should expand / collapse groups', async ({ runUITest }) => { From 1b21ec9cd84387abe1537d15862e3b9bf5f1f387 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Fri, 17 Jan 2025 10:17:49 -0800 Subject: [PATCH 12/19] chore: remove --save-trace codegen option (#34362) --- packages/playwright-core/src/cli/program.ts | 7 - .../playwright-core/src/server/recorder.ts | 3 +- .../src/server/recorder/recorderCollection.ts | 22 +-- .../src/server/recorder/recorderRunner.ts | 8 +- .../src/server/recorder/recorderUtils.ts | 26 +-- .../src/utils/isomorphic/recorderUtils.ts | 163 ------------------ tests/library/inspector/cli-codegen-2.spec.ts | 13 +- 7 files changed, 16 insertions(+), 226 deletions(-) delete mode 100644 packages/playwright-core/src/utils/isomorphic/recorderUtils.ts diff --git a/packages/playwright-core/src/cli/program.ts b/packages/playwright-core/src/cli/program.ts index a69414f2b2..ce568b8dd3 100644 --- a/packages/playwright-core/src/cli/program.ts +++ b/packages/playwright-core/src/cli/program.ts @@ -65,7 +65,6 @@ commandWithOpenOptions('codegen [url]', 'open page and generate code for user ac [ ['-o, --output ', 'saves the generated script to a file'], ['--target ', `language to generate, one of javascript, playwright-test, python, python-async, python-pytest, csharp, csharp-mstest, csharp-nunit, java, java-junit`, codegenId()], - ['--save-trace ', 'record a trace for the session and save it to a file'], ['--test-id-attribute ', 'use the specified attribute to generate data test ID selectors'], ]).action(function(url, options) { codegen(options, url).catch(logErrorAndExit); @@ -353,7 +352,6 @@ type Options = { saveHar?: string; saveHarGlob?: string; saveStorage?: string; - saveTrace?: string; timeout: string; timezone?: string; viewportSize?: string; @@ -508,8 +506,6 @@ async function launchContext(options: Options, extraOptions: LaunchOptions): Pro if (closingBrowser) return; closingBrowser = true; - if (options.saveTrace) - await context.tracing.stop({ path: options.saveTrace }); if (options.saveStorage) await context.storageState({ path: options.saveStorage }).catch(e => null); if (options.saveHar) @@ -536,9 +532,6 @@ async function launchContext(options: Options, extraOptions: LaunchOptions): Pro context.setDefaultTimeout(timeout); context.setDefaultNavigationTimeout(timeout); - if (options.saveTrace) - await context.tracing.start({ screenshots: true, snapshots: true }); - // Omit options that we add automatically for presentation purpose. delete launchOptions.headless; delete launchOptions.executablePath; diff --git a/packages/playwright-core/src/server/recorder.ts b/packages/playwright-core/src/server/recorder.ts index f763d5491b..59705c2eb4 100644 --- a/packages/playwright-core/src/server/recorder.ts +++ b/packages/playwright-core/src/server/recorder.ts @@ -27,9 +27,8 @@ import { Debugger } from './debugger'; import type { CallMetadata, InstrumentationListener, SdkObject } from './instrumentation'; import { ContextRecorder, generateFrameSelector } from './recorder/contextRecorder'; import type { IRecorderAppFactory, IRecorderApp, IRecorder } from './recorder/recorderFrontend'; -import { metadataToCallLog } from './recorder/recorderUtils'; +import { buildFullSelector, metadataToCallLog } from './recorder/recorderUtils'; import type * as actions from '@recorder/actions'; -import { buildFullSelector } from '../utils/isomorphic/recorderUtils'; import { stringifySelector } from '../utils/isomorphic/selectorParser'; import type { Frame } from './frames'; import type { AriaTemplateNode } from '@isomorphic/ariaSnapshot'; diff --git a/packages/playwright-core/src/server/recorder/recorderCollection.ts b/packages/playwright-core/src/server/recorder/recorderCollection.ts index 2643dece91..162d5d6813 100644 --- a/packages/playwright-core/src/server/recorder/recorderCollection.ts +++ b/packages/playwright-core/src/server/recorder/recorderCollection.ts @@ -20,10 +20,8 @@ import type { Page } from '../page'; import type { Signal } from '../../../../recorder/src/actions'; import type * as actions from '@recorder/actions'; import { monotonicTime } from '../../utils/time'; -import { callMetadataForAction, collapseActions } from './recorderUtils'; -import { serializeError } from '../errors'; +import { collapseActions } from './recorderUtils'; import { performAction } from './recorderRunner'; -import type { CallMetadata } from '@protocol/callMetadata'; import { isUnderTest } from '../../utils/debug'; export class RecorderCollection extends EventEmitter { @@ -46,8 +44,8 @@ export class RecorderCollection extends EventEmitter { } async performAction(actionInContext: actions.ActionInContext) { - await this._addAction(actionInContext, async callMetadata => { - await performAction(callMetadata, this._pageAliases, actionInContext); + await this._addAction(actionInContext, async () => { + await performAction(this._pageAliases, actionInContext); }); } @@ -60,7 +58,7 @@ export class RecorderCollection extends EventEmitter { this._addAction(actionInContext).catch(() => {}); } - private async _addAction(actionInContext: actions.ActionInContext, callback?: (callMetadata: CallMetadata) => Promise) { + private async _addAction(actionInContext: actions.ActionInContext, callback?: () => Promise) { if (!this._enabled) return; if (actionInContext.action.name === 'openPage' || actionInContext.action.name === 'closePage') { @@ -69,18 +67,10 @@ export class RecorderCollection extends EventEmitter { return; } - const { callMetadata, mainFrame } = callMetadataForAction(this._pageAliases, actionInContext); - await mainFrame.instrumentation.onBeforeCall(mainFrame, callMetadata); this._actions.push(actionInContext); this._fireChange(); - const error = await callback?.(callMetadata).catch((e: Error) => e); - callMetadata.endTime = monotonicTime(); - actionInContext.endTime = callMetadata.endTime; - callMetadata.error = error ? serializeError(error) : undefined; - // Do not wait for onAfterCall so that performAction returned immediately after the action. - mainFrame.instrumentation.onAfterCall(mainFrame, callMetadata).then(() => { - this._fireChange(); - }).catch(() => {}); + await callback?.().catch(); + actionInContext.endTime = monotonicTime(); } signal(pageAlias: string, frame: Frame, signal: Signal) { diff --git a/packages/playwright-core/src/server/recorder/recorderRunner.ts b/packages/playwright-core/src/server/recorder/recorderRunner.ts index 1b33895f98..bb225ec5ab 100644 --- a/packages/playwright-core/src/server/recorder/recorderRunner.ts +++ b/packages/playwright-core/src/server/recorder/recorderRunner.ts @@ -16,14 +16,14 @@ import { serializeExpectedTextValues } from '../../utils'; import { toKeyboardModifiers } from '../codegen/language'; -import type { CallMetadata } from '../instrumentation'; +import { serverSideCallMetadata } from '../instrumentation'; import type { Page } from '../page'; import type * as actions from '@recorder/actions'; import type * as types from '../types'; -import { mainFrameForAction } from './recorderUtils'; -import { buildFullSelector } from '../../utils/isomorphic/recorderUtils'; +import { buildFullSelector, mainFrameForAction } from './recorderUtils'; -export async function performAction(callMetadata: CallMetadata, pageAliases: Map, actionInContext: actions.ActionInContext) { +export async function performAction(pageAliases: Map, actionInContext: actions.ActionInContext) { + const callMetadata = serverSideCallMetadata(); const mainFrame = mainFrameForAction(pageAliases, actionInContext); const { action } = actionInContext; diff --git a/packages/playwright-core/src/server/recorder/recorderUtils.ts b/packages/playwright-core/src/server/recorder/recorderUtils.ts index 990ba959d6..77ef329d27 100644 --- a/packages/playwright-core/src/server/recorder/recorderUtils.ts +++ b/packages/playwright-core/src/server/recorder/recorderUtils.ts @@ -19,8 +19,10 @@ import type { CallLog, CallLogStatus } from '@recorder/recorderTypes'; import type { Page } from '../page'; import type { Frame } from '../frames'; import type * as actions from '@recorder/actions'; -import { createGuid } from '../../utils'; -import { buildFullSelector, traceParamsForAction } from '../../utils/isomorphic/recorderUtils'; + +export function buildFullSelector(framePath: string[], selector: string) { + return [...framePath, selector].join(' >> internal:control=enter-frame >> '); +} export function metadataToCallLog(metadata: CallMetadata, status: CallLogStatus): CallLog { let title = metadata.apiName || metadata.method; @@ -70,26 +72,6 @@ export async function frameForAction(pageAliases: Map, actionInCon return result.frame; } -export function callMetadataForAction(pageAliases: Map, actionInContext: actions.ActionInContext): { callMetadata: CallMetadata, mainFrame: Frame } { - const mainFrame = mainFrameForAction(pageAliases, actionInContext); - const { method, apiName, params } = traceParamsForAction(actionInContext); - - const callMetadata: CallMetadata = { - id: `call@${createGuid()}`, - apiName, - objectId: mainFrame.guid, - pageId: mainFrame._page.guid, - frameId: mainFrame.guid, - startTime: actionInContext.startTime, - endTime: 0, - type: 'Frame', - method, - params, - log: [], - }; - return { callMetadata, mainFrame }; -} - export function collapseActions(actions: actions.ActionInContext[]): actions.ActionInContext[] { const result: actions.ActionInContext[] = []; for (const action of actions) { diff --git a/packages/playwright-core/src/utils/isomorphic/recorderUtils.ts b/packages/playwright-core/src/utils/isomorphic/recorderUtils.ts deleted file mode 100644 index 7ef45e5ece..0000000000 --- a/packages/playwright-core/src/utils/isomorphic/recorderUtils.ts +++ /dev/null @@ -1,163 +0,0 @@ -/** - * 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 type * as recorderActions from '@recorder/actions'; -import type * as channels from '@protocol/channels'; -import type * as types from '../../server/types'; - -export function buildFullSelector(framePath: string[], selector: string) { - return [...framePath, selector].join(' >> internal:control=enter-frame >> '); -} - -const kDefaultTimeout = 5_000; - -export function traceParamsForAction(actionInContext: recorderActions.ActionInContext): { method: string, apiName: string, params: any } { - const { action } = actionInContext; - - switch (action.name) { - case 'navigate': { - const params: channels.FrameGotoParams = { - url: action.url, - }; - return { method: 'goto', apiName: 'page.goto', params }; - } - case 'openPage': - case 'closePage': - throw new Error('Not reached'); - } - - const selector = buildFullSelector(actionInContext.frame.framePath, action.selector); - switch (action.name) { - case 'click': { - const params: channels.FrameClickParams = { - selector, - strict: true, - modifiers: toKeyboardModifiers(action.modifiers), - button: action.button, - clickCount: action.clickCount, - position: action.position, - }; - return { method: 'click', apiName: 'locator.click', params }; - } - case 'press': { - const params: channels.FramePressParams = { - selector, - strict: true, - key: [...toKeyboardModifiers(action.modifiers), action.key].join('+'), - }; - return { method: 'press', apiName: 'locator.press', params }; - } - case 'fill': { - const params: channels.FrameFillParams = { - selector, - strict: true, - value: action.text, - }; - return { method: 'fill', apiName: 'locator.fill', params }; - } - case 'setInputFiles': { - const params: channels.FrameSetInputFilesParams = { - selector, - strict: true, - localPaths: action.files, - }; - return { method: 'setInputFiles', apiName: 'locator.setInputFiles', params }; - } - case 'check': { - const params: channels.FrameCheckParams = { - selector, - strict: true, - }; - return { method: 'check', apiName: 'locator.check', params }; - } - case 'uncheck': { - const params: channels.FrameUncheckParams = { - selector, - strict: true, - }; - return { method: 'uncheck', apiName: 'locator.uncheck', params }; - } - case 'select': { - const params: channels.FrameSelectOptionParams = { - selector, - strict: true, - options: action.options.map(option => ({ value: option })), - }; - return { method: 'selectOption', apiName: 'locator.selectOption', params }; - } - case 'assertChecked': { - const params: channels.FrameExpectParams = { - selector: action.selector, - expression: 'to.be.checked', - isNot: !action.checked, - timeout: kDefaultTimeout, - }; - return { method: 'expect', apiName: 'expect.toBeChecked', params }; - } - case 'assertText': { - const params: channels.FrameExpectParams = { - selector, - expression: 'to.have.text', - expectedText: [], - isNot: false, - timeout: kDefaultTimeout, - }; - return { method: 'expect', apiName: 'expect.toContainText', params }; - } - case 'assertValue': { - const params: channels.FrameExpectParams = { - selector, - expression: 'to.have.value', - expectedValue: undefined, - isNot: false, - timeout: kDefaultTimeout, - }; - return { method: 'expect', apiName: 'expect.toHaveValue', params }; - } - case 'assertVisible': { - const params: channels.FrameExpectParams = { - selector, - expression: 'to.be.visible', - isNot: false, - timeout: kDefaultTimeout, - }; - return { method: 'expect', apiName: 'expect.toBeVisible', params }; - } - case 'assertSnapshot': { - const params: channels.FrameExpectParams = { - selector, - expression: 'to.match.snapshot', - expectedText: [], - isNot: false, - timeout: kDefaultTimeout, - }; - return { method: 'expect', apiName: 'expect.toMatchAriaSnapshot', params }; - } - } -} - -export function toKeyboardModifiers(modifiers: number): types.SmartKeyboardModifier[] { - const result: types.SmartKeyboardModifier[] = []; - if (modifiers & 1) - result.push('Alt'); - if (modifiers & 2) - result.push('ControlOrMeta'); - if (modifiers & 4) - result.push('ControlOrMeta'); - if (modifiers & 8) - result.push('Shift'); - return result; -} diff --git a/tests/library/inspector/cli-codegen-2.spec.ts b/tests/library/inspector/cli-codegen-2.spec.ts index 920ecbdf76..205bbbae5e 100644 --- a/tests/library/inspector/cli-codegen-2.spec.ts +++ b/tests/library/inspector/cli-codegen-2.spec.ts @@ -451,27 +451,16 @@ await page1.GotoAsync("about:blank?foo");`); await recorder.waitForOutput('JavaScript', `await page.goto('${server.PREFIX}/page2.html');`); }); - test('should --save-trace', async ({ runCLI }, testInfo) => { - const traceFileName = testInfo.outputPath('trace.zip'); - const cli = runCLI([`--save-trace=${traceFileName}`], { - autoExitWhen: ' ', - }); - await cli.waitForCleanExit(); - expect(fs.existsSync(traceFileName)).toBeTruthy(); - }); - test('should save assets via SIGINT', async ({ runCLI, platform }, testInfo) => { test.skip(platform === 'win32', 'SIGINT not supported on Windows'); - const traceFileName = testInfo.outputPath('trace.zip'); const storageFileName = testInfo.outputPath('auth.json'); const harFileName = testInfo.outputPath('har.har'); - const cli = runCLI([`--save-trace=${traceFileName}`, `--save-storage=${storageFileName}`, `--save-har=${harFileName}`]); + const cli = runCLI([`--save-storage=${storageFileName}`, `--save-har=${harFileName}`]); await cli.waitFor(`import { test, expect } from '@playwright/test'`); await cli.process.kill('SIGINT'); const { exitCode } = await cli.process.exited; expect(exitCode).toBe(130); - expect(fs.existsSync(traceFileName)).toBeTruthy(); expect(fs.existsSync(storageFileName)).toBeTruthy(); expect(fs.existsSync(harFileName)).toBeTruthy(); }); From 3c160df06a31c01e20eeaf50af3abcd1d7f02a0b Mon Sep 17 00:00:00 2001 From: Adam Gastineau Date: Fri, 17 Jan 2025 12:34:59 -0800 Subject: [PATCH 13/19] chore: mark v1.51.0-next (#34382) --- package-lock.json | 60 +++++++++---------- package.json | 2 +- .../playwright-browser-chromium/package.json | 4 +- .../playwright-browser-firefox/package.json | 4 +- .../playwright-browser-webkit/package.json | 4 +- packages/playwright-chromium/package.json | 4 +- packages/playwright-core/package.json | 2 +- packages/playwright-ct-core/package.json | 6 +- packages/playwright-ct-react/package.json | 4 +- packages/playwright-ct-react17/package.json | 4 +- packages/playwright-ct-svelte/package.json | 4 +- packages/playwright-ct-vue/package.json | 4 +- packages/playwright-firefox/package.json | 4 +- packages/playwright-test/package.json | 4 +- packages/playwright-webkit/package.json | 4 +- packages/playwright/package.json | 4 +- 16 files changed, 59 insertions(+), 59 deletions(-) diff --git a/package-lock.json b/package-lock.json index 411ef13b62..8c242d4bb9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "playwright-internal", - "version": "1.50.0-next", + "version": "1.51.0-next", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "playwright-internal", - "version": "1.50.0-next", + "version": "1.51.0-next", "license": "Apache-2.0", "workspaces": [ "packages/*" @@ -7751,10 +7751,10 @@ "version": "0.0.0" }, "packages/playwright": { - "version": "1.50.0-next", + "version": "1.51.0-next", "license": "Apache-2.0", "dependencies": { - "playwright-core": "1.50.0-next" + "playwright-core": "1.51.0-next" }, "bin": { "playwright": "cli.js" @@ -7768,11 +7768,11 @@ }, "packages/playwright-browser-chromium": { "name": "@playwright/browser-chromium", - "version": "1.50.0-next", + "version": "1.51.0-next", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { - "playwright-core": "1.50.0-next" + "playwright-core": "1.51.0-next" }, "engines": { "node": ">=18" @@ -7780,11 +7780,11 @@ }, "packages/playwright-browser-firefox": { "name": "@playwright/browser-firefox", - "version": "1.50.0-next", + "version": "1.51.0-next", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { - "playwright-core": "1.50.0-next" + "playwright-core": "1.51.0-next" }, "engines": { "node": ">=18" @@ -7792,22 +7792,22 @@ }, "packages/playwright-browser-webkit": { "name": "@playwright/browser-webkit", - "version": "1.50.0-next", + "version": "1.51.0-next", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { - "playwright-core": "1.50.0-next" + "playwright-core": "1.51.0-next" }, "engines": { "node": ">=18" } }, "packages/playwright-chromium": { - "version": "1.50.0-next", + "version": "1.51.0-next", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { - "playwright-core": "1.50.0-next" + "playwright-core": "1.51.0-next" }, "bin": { "playwright": "cli.js" @@ -7817,7 +7817,7 @@ } }, "packages/playwright-core": { - "version": "1.50.0-next", + "version": "1.51.0-next", "license": "Apache-2.0", "bin": { "playwright-core": "cli.js" @@ -7828,11 +7828,11 @@ }, "packages/playwright-ct-core": { "name": "@playwright/experimental-ct-core", - "version": "1.50.0-next", + "version": "1.51.0-next", "license": "Apache-2.0", "dependencies": { - "playwright": "1.50.0-next", - "playwright-core": "1.50.0-next", + "playwright": "1.51.0-next", + "playwright-core": "1.51.0-next", "vite": "^5.2.8" }, "engines": { @@ -7841,10 +7841,10 @@ }, "packages/playwright-ct-react": { "name": "@playwright/experimental-ct-react", - "version": "1.50.0-next", + "version": "1.51.0-next", "license": "Apache-2.0", "dependencies": { - "@playwright/experimental-ct-core": "1.50.0-next", + "@playwright/experimental-ct-core": "1.51.0-next", "@vitejs/plugin-react": "^4.2.1" }, "bin": { @@ -7856,10 +7856,10 @@ }, "packages/playwright-ct-react17": { "name": "@playwright/experimental-ct-react17", - "version": "1.50.0-next", + "version": "1.51.0-next", "license": "Apache-2.0", "dependencies": { - "@playwright/experimental-ct-core": "1.50.0-next", + "@playwright/experimental-ct-core": "1.51.0-next", "@vitejs/plugin-react": "^4.2.1" }, "bin": { @@ -7871,10 +7871,10 @@ }, "packages/playwright-ct-svelte": { "name": "@playwright/experimental-ct-svelte", - "version": "1.50.0-next", + "version": "1.51.0-next", "license": "Apache-2.0", "dependencies": { - "@playwright/experimental-ct-core": "1.50.0-next", + "@playwright/experimental-ct-core": "1.51.0-next", "@sveltejs/vite-plugin-svelte": "^3.0.1" }, "bin": { @@ -7889,10 +7889,10 @@ }, "packages/playwright-ct-vue": { "name": "@playwright/experimental-ct-vue", - "version": "1.50.0-next", + "version": "1.51.0-next", "license": "Apache-2.0", "dependencies": { - "@playwright/experimental-ct-core": "1.50.0-next", + "@playwright/experimental-ct-core": "1.51.0-next", "@vitejs/plugin-vue": "^5.2.0" }, "bin": { @@ -7903,11 +7903,11 @@ } }, "packages/playwright-firefox": { - "version": "1.50.0-next", + "version": "1.51.0-next", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { - "playwright-core": "1.50.0-next" + "playwright-core": "1.51.0-next" }, "bin": { "playwright": "cli.js" @@ -7918,10 +7918,10 @@ }, "packages/playwright-test": { "name": "@playwright/test", - "version": "1.50.0-next", + "version": "1.51.0-next", "license": "Apache-2.0", "dependencies": { - "playwright": "1.50.0-next" + "playwright": "1.51.0-next" }, "bin": { "playwright": "cli.js" @@ -7931,11 +7931,11 @@ } }, "packages/playwright-webkit": { - "version": "1.50.0-next", + "version": "1.51.0-next", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { - "playwright-core": "1.50.0-next" + "playwright-core": "1.51.0-next" }, "bin": { "playwright": "cli.js" diff --git a/package.json b/package.json index 5eafd4d805..76a2b426f7 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "playwright-internal", "private": true, - "version": "1.50.0-next", + "version": "1.51.0-next", "description": "A high-level API to automate web browsers", "repository": { "type": "git", diff --git a/packages/playwright-browser-chromium/package.json b/packages/playwright-browser-chromium/package.json index 909d5d3261..6d3d2cd294 100644 --- a/packages/playwright-browser-chromium/package.json +++ b/packages/playwright-browser-chromium/package.json @@ -1,6 +1,6 @@ { "name": "@playwright/browser-chromium", - "version": "1.50.0-next", + "version": "1.51.0-next", "description": "Playwright package that automatically installs Chromium", "repository": { "type": "git", @@ -27,6 +27,6 @@ "install": "node install.js" }, "dependencies": { - "playwright-core": "1.50.0-next" + "playwright-core": "1.51.0-next" } } diff --git a/packages/playwright-browser-firefox/package.json b/packages/playwright-browser-firefox/package.json index 635cfd6811..3eb00e556d 100644 --- a/packages/playwright-browser-firefox/package.json +++ b/packages/playwright-browser-firefox/package.json @@ -1,6 +1,6 @@ { "name": "@playwright/browser-firefox", - "version": "1.50.0-next", + "version": "1.51.0-next", "description": "Playwright package that automatically installs Firefox", "repository": { "type": "git", @@ -27,6 +27,6 @@ "install": "node install.js" }, "dependencies": { - "playwright-core": "1.50.0-next" + "playwright-core": "1.51.0-next" } } diff --git a/packages/playwright-browser-webkit/package.json b/packages/playwright-browser-webkit/package.json index 61811e3684..1072b6b401 100644 --- a/packages/playwright-browser-webkit/package.json +++ b/packages/playwright-browser-webkit/package.json @@ -1,6 +1,6 @@ { "name": "@playwright/browser-webkit", - "version": "1.50.0-next", + "version": "1.51.0-next", "description": "Playwright package that automatically installs WebKit", "repository": { "type": "git", @@ -27,6 +27,6 @@ "install": "node install.js" }, "dependencies": { - "playwright-core": "1.50.0-next" + "playwright-core": "1.51.0-next" } } diff --git a/packages/playwright-chromium/package.json b/packages/playwright-chromium/package.json index 22cabe176c..32b966d476 100644 --- a/packages/playwright-chromium/package.json +++ b/packages/playwright-chromium/package.json @@ -1,6 +1,6 @@ { "name": "playwright-chromium", - "version": "1.50.0-next", + "version": "1.51.0-next", "description": "A high-level API to automate Chromium", "repository": { "type": "git", @@ -30,6 +30,6 @@ "install": "node install.js" }, "dependencies": { - "playwright-core": "1.50.0-next" + "playwright-core": "1.51.0-next" } } diff --git a/packages/playwright-core/package.json b/packages/playwright-core/package.json index 9b484085dd..d99b9e2d6a 100644 --- a/packages/playwright-core/package.json +++ b/packages/playwright-core/package.json @@ -1,6 +1,6 @@ { "name": "playwright-core", - "version": "1.50.0-next", + "version": "1.51.0-next", "description": "A high-level API to automate web browsers", "repository": { "type": "git", diff --git a/packages/playwright-ct-core/package.json b/packages/playwright-ct-core/package.json index 60144fa7c3..e9376d1963 100644 --- a/packages/playwright-ct-core/package.json +++ b/packages/playwright-ct-core/package.json @@ -1,6 +1,6 @@ { "name": "@playwright/experimental-ct-core", - "version": "1.50.0-next", + "version": "1.51.0-next", "description": "Playwright Component Testing Helpers", "repository": { "type": "git", @@ -26,8 +26,8 @@ } }, "dependencies": { - "playwright-core": "1.50.0-next", + "playwright-core": "1.51.0-next", "vite": "^5.2.8", - "playwright": "1.50.0-next" + "playwright": "1.51.0-next" } } diff --git a/packages/playwright-ct-react/package.json b/packages/playwright-ct-react/package.json index 39a8e98a15..dde3c05e54 100644 --- a/packages/playwright-ct-react/package.json +++ b/packages/playwright-ct-react/package.json @@ -1,6 +1,6 @@ { "name": "@playwright/experimental-ct-react", - "version": "1.50.0-next", + "version": "1.51.0-next", "description": "Playwright Component Testing for React", "repository": { "type": "git", @@ -30,7 +30,7 @@ "./package.json": "./package.json" }, "dependencies": { - "@playwright/experimental-ct-core": "1.50.0-next", + "@playwright/experimental-ct-core": "1.51.0-next", "@vitejs/plugin-react": "^4.2.1" }, "bin": { diff --git a/packages/playwright-ct-react17/package.json b/packages/playwright-ct-react17/package.json index 41665bfb9b..f8dce715b5 100644 --- a/packages/playwright-ct-react17/package.json +++ b/packages/playwright-ct-react17/package.json @@ -1,6 +1,6 @@ { "name": "@playwright/experimental-ct-react17", - "version": "1.50.0-next", + "version": "1.51.0-next", "description": "Playwright Component Testing for React", "repository": { "type": "git", @@ -30,7 +30,7 @@ "./package.json": "./package.json" }, "dependencies": { - "@playwright/experimental-ct-core": "1.50.0-next", + "@playwright/experimental-ct-core": "1.51.0-next", "@vitejs/plugin-react": "^4.2.1" }, "bin": { diff --git a/packages/playwright-ct-svelte/package.json b/packages/playwright-ct-svelte/package.json index 7afba03632..fa01d16d2e 100644 --- a/packages/playwright-ct-svelte/package.json +++ b/packages/playwright-ct-svelte/package.json @@ -1,6 +1,6 @@ { "name": "@playwright/experimental-ct-svelte", - "version": "1.50.0-next", + "version": "1.51.0-next", "description": "Playwright Component Testing for Svelte", "repository": { "type": "git", @@ -30,7 +30,7 @@ "./package.json": "./package.json" }, "dependencies": { - "@playwright/experimental-ct-core": "1.50.0-next", + "@playwright/experimental-ct-core": "1.51.0-next", "@sveltejs/vite-plugin-svelte": "^3.0.1" }, "devDependencies": { diff --git a/packages/playwright-ct-vue/package.json b/packages/playwright-ct-vue/package.json index fe48461394..fb35b79143 100644 --- a/packages/playwright-ct-vue/package.json +++ b/packages/playwright-ct-vue/package.json @@ -1,6 +1,6 @@ { "name": "@playwright/experimental-ct-vue", - "version": "1.50.0-next", + "version": "1.51.0-next", "description": "Playwright Component Testing for Vue", "repository": { "type": "git", @@ -30,7 +30,7 @@ "./package.json": "./package.json" }, "dependencies": { - "@playwright/experimental-ct-core": "1.50.0-next", + "@playwright/experimental-ct-core": "1.51.0-next", "@vitejs/plugin-vue": "^5.2.0" }, "bin": { diff --git a/packages/playwright-firefox/package.json b/packages/playwright-firefox/package.json index 44832c5532..2d34bcf3ee 100644 --- a/packages/playwright-firefox/package.json +++ b/packages/playwright-firefox/package.json @@ -1,6 +1,6 @@ { "name": "playwright-firefox", - "version": "1.50.0-next", + "version": "1.51.0-next", "description": "A high-level API to automate Firefox", "repository": { "type": "git", @@ -30,6 +30,6 @@ "install": "node install.js" }, "dependencies": { - "playwright-core": "1.50.0-next" + "playwright-core": "1.51.0-next" } } diff --git a/packages/playwright-test/package.json b/packages/playwright-test/package.json index dcf74e1111..82a134b3d4 100644 --- a/packages/playwright-test/package.json +++ b/packages/playwright-test/package.json @@ -1,6 +1,6 @@ { "name": "@playwright/test", - "version": "1.50.0-next", + "version": "1.51.0-next", "description": "A high-level API to automate web browsers", "repository": { "type": "git", @@ -30,6 +30,6 @@ }, "scripts": {}, "dependencies": { - "playwright": "1.50.0-next" + "playwright": "1.51.0-next" } } diff --git a/packages/playwright-webkit/package.json b/packages/playwright-webkit/package.json index 9fd72b1c84..c07d8d00ab 100644 --- a/packages/playwright-webkit/package.json +++ b/packages/playwright-webkit/package.json @@ -1,6 +1,6 @@ { "name": "playwright-webkit", - "version": "1.50.0-next", + "version": "1.51.0-next", "description": "A high-level API to automate WebKit", "repository": { "type": "git", @@ -30,6 +30,6 @@ "install": "node install.js" }, "dependencies": { - "playwright-core": "1.50.0-next" + "playwright-core": "1.51.0-next" } } diff --git a/packages/playwright/package.json b/packages/playwright/package.json index a525d5deee..90a21cb807 100644 --- a/packages/playwright/package.json +++ b/packages/playwright/package.json @@ -1,6 +1,6 @@ { "name": "playwright", - "version": "1.50.0-next", + "version": "1.51.0-next", "description": "A high-level API to automate web browsers", "repository": { "type": "git", @@ -56,7 +56,7 @@ }, "license": "Apache-2.0", "dependencies": { - "playwright-core": "1.50.0-next" + "playwright-core": "1.51.0-next" }, "optionalDependencies": { "fsevents": "2.3.2" From d082805ea97c502a67ecb92a39b434508e99a48f Mon Sep 17 00:00:00 2001 From: Henrik Skupin Date: Fri, 17 Jan 2025 23:29:26 +0100 Subject: [PATCH 14/19] chore(bidi): disable thottling of background tabs in Firefox (#34381) --- .../src/server/bidi/third_party/firefoxPrefs.ts | 9 +++++++++ tests/bidi/expectations/bidi-firefox-nightly-library.txt | 1 - 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/playwright-core/src/server/bidi/third_party/firefoxPrefs.ts b/packages/playwright-core/src/server/bidi/third_party/firefoxPrefs.ts index 7c08bebe6d..d59ac0bc13 100644 --- a/packages/playwright-core/src/server/bidi/third_party/firefoxPrefs.ts +++ b/packages/playwright-core/src/server/bidi/third_party/firefoxPrefs.ts @@ -133,6 +133,12 @@ function defaultProfilePreferences( 'dom.max_chrome_script_run_time': 0, 'dom.max_script_run_time': 0, + // Disable background timer throttling to allow tests to run in parallel + // without a decrease in performance. + 'dom.min_background_timeout_value': 0, + 'dom.min_background_timeout_value_without_budget_throttling': 0, + 'dom.timeout.enable_budget_timer_throttling': false, + // Only load extensions from the application and user profile // AddonManager.SCOPE_PROFILE + AddonManager.SCOPE_APPLICATION 'extensions.autoDisableScopes': 0, @@ -175,6 +181,9 @@ function defaultProfilePreferences( // Show chrome errors and warnings in the error console 'javascript.options.showInConsole': true, + // Do not throttle rendering (requestAnimationFrame) in background tabs + 'layout.testing.top-level-always-active': true, + // Disable download and usage of OpenH264: and Widevine plugins 'media.gmp-manager.updateEnabled': false, diff --git a/tests/bidi/expectations/bidi-firefox-nightly-library.txt b/tests/bidi/expectations/bidi-firefox-nightly-library.txt index 70ff33fb60..f527d5fb61 100644 --- a/tests/bidi/expectations/bidi-firefox-nightly-library.txt +++ b/tests/bidi/expectations/bidi-firefox-nightly-library.txt @@ -101,7 +101,6 @@ library/inspector/cli-codegen-3.spec.ts › cli codegen › should generate fram library/page-clock.spec.ts › popup › should run time before popup [timeout] library/page-clock.spec.ts › popup › should tick after popup [timeout] library/page-clock.spec.ts › popup › should tick before popup [timeout] -library/popup.spec.ts › should not throttle rAF in the opener page [timeout] library/popup.spec.ts › should not throw when click closes popup [timeout] library/popup.spec.ts › should use viewport size from window features [timeout] library/trace-viewer.spec.ts › should serve css without content-type [timeout] From 9970446f517e0562b543a032c62ce5fe8ae78cbb Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Fri, 17 Jan 2025 14:35:15 -0800 Subject: [PATCH 15/19] chore: remove toMatchAriaSnapshot.path (#34379) --- docs/src/api/class-locatorassertions.md | 9 ++------- .../src/matchers/toMatchAriaSnapshot.ts | 4 +--- packages/playwright/types/test.d.ts | 9 ++------- .../playwright-test/aria-snapshot-file.spec.ts | 18 ------------------ 4 files changed, 5 insertions(+), 35 deletions(-) diff --git a/docs/src/api/class-locatorassertions.md b/docs/src/api/class-locatorassertions.md index 63b487208f..35f64ea711 100644 --- a/docs/src/api/class-locatorassertions.md +++ b/docs/src/api/class-locatorassertions.md @@ -2274,13 +2274,8 @@ assertThat(page.locator("body")).matchesAriaSnapshot(new LocatorAssertions.Match * langs: js - `name` <[string]> -Name of the snapshot to store in the snapshot folder corresponding to this test. Generates ordinal name if not specified. - -### option: LocatorAssertions.toMatchAriaSnapshot#2.path -* since: v1.50 -- `path` <[string]> - -Path to the YAML snapshot file. +Name of the snapshot to store in the snapshot (screenshot) folder corresponding to this test. +Generates sequential names if not specified. ### option: LocatorAssertions.toMatchAriaSnapshot#2.timeout = %%-js-assertions-timeout-%% * since: v1.50 diff --git a/packages/playwright/src/matchers/toMatchAriaSnapshot.ts b/packages/playwright/src/matchers/toMatchAriaSnapshot.ts index 152ceb6ba9..c291770fd5 100644 --- a/packages/playwright/src/matchers/toMatchAriaSnapshot.ts +++ b/packages/playwright/src/matchers/toMatchAriaSnapshot.ts @@ -59,9 +59,7 @@ export async function toMatchAriaSnapshot( if (isString(expectedParam)) { expected = expectedParam; } else { - if (expectedParam?.path) { - expectedPath = expectedParam.path; - } else if (expectedParam?.name) { + if (expectedParam?.name) { expectedPath = testInfo.snapshotPath(sanitizeFilePathBeforeExtension(expectedParam.name)); } else { let snapshotNames = (testInfo as any)[snapshotNamesSymbol] as SnapshotNames; diff --git a/packages/playwright/types/test.d.ts b/packages/playwright/types/test.d.ts index e01271ceec..b67377eb6d 100644 --- a/packages/playwright/types/test.d.ts +++ b/packages/playwright/types/test.d.ts @@ -8697,16 +8697,11 @@ interface LocatorAssertions { */ toMatchAriaSnapshot(options?: { /** - * Name of the snapshot to store in the snapshot folder corresponding to this test. Generates ordinal name if not - * specified. + * Name of the snapshot to store in the snapshot (screenshot) folder corresponding to this test. Generates sequential + * names if not specified. */ name?: string; - /** - * Path to the YAML snapshot file. - */ - path?: string; - /** * Time to retry the assertion for in milliseconds. Defaults to `timeout` in `TestConfig.expect`. */ diff --git a/tests/playwright-test/aria-snapshot-file.spec.ts b/tests/playwright-test/aria-snapshot-file.spec.ts index c05ae3897d..18ebc072cc 100644 --- a/tests/playwright-test/aria-snapshot-file.spec.ts +++ b/tests/playwright-test/aria-snapshot-file.spec.ts @@ -42,24 +42,6 @@ test('should match snapshot with name', async ({ runInlineTest }, testInfo) => { expect(result.exitCode).toBe(0); }); -test('should match snapshot with path', async ({ runInlineTest }, testInfo) => { - const result = await runInlineTest({ - 'test.yml': ` - - heading "hello world" - `, - 'a.spec.ts': ` - import { test, expect } from '@playwright/test'; - import path from 'path'; - test('test', async ({ page }) => { - await page.setContent(\`

hello world

\`); - await expect(page.locator('body')).toMatchAriaSnapshot({ path: path.resolve(__dirname, 'test.yml') }); - }); - ` - }); - - expect(result.exitCode).toBe(0); -}); - test('should generate multiple missing', async ({ runInlineTest }, testInfo) => { const result = await runInlineTest({ 'playwright.config.ts': ` From 372d4196d7b92868d855d3737642f008a28df808 Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Fri, 17 Jan 2025 21:15:47 -0800 Subject: [PATCH 16/19] chore: step timeout improvements (#34386) --- docs/src/test-api/class-test.md | 2 +- packages/playwright/src/common/testType.ts | 15 ++++++++++++-- tests/playwright-test/test-step.spec.ts | 23 +++++++++++++++++++++- 3 files changed, 36 insertions(+), 4 deletions(-) diff --git a/docs/src/test-api/class-test.md b/docs/src/test-api/class-test.md index 00c7a5f0ee..77c0c5028c 100644 --- a/docs/src/test-api/class-test.md +++ b/docs/src/test-api/class-test.md @@ -1822,7 +1822,7 @@ Maximum time in milliseconds for the step to finish. Defaults to `0` (no timeout * since: v1.50 - `timeout` <[float]> -Maximum time in milliseconds for the step to finish. Defaults to `0` (no timeout). +The maximum time, in milliseconds, allowed for the step to complete. If the step does not complete within the specified timeout, the [`method: Test.step`] method will throw a [TimeoutError]. Defaults to `0` (no timeout). ## method: Test.use * since: v1.10 diff --git a/packages/playwright/src/common/testType.ts b/packages/playwright/src/common/testType.ts index 4c9e6d8f90..58d813a99e 100644 --- a/packages/playwright/src/common/testType.ts +++ b/packages/playwright/src/common/testType.ts @@ -270,9 +270,20 @@ export class TestTypeImpl { const step = testInfo._addStep({ category: 'test.step', title, location: options.location, box: options.box }); return await zones.run('stepZone', step, async () => { try { - const result = await raceAgainstDeadline(async () => body(), options.timeout ? monotonicTime() + options.timeout : 0); + let result: Awaited>> | undefined = undefined; + result = await raceAgainstDeadline(async () => { + try { + return await body(); + } catch (e) { + // If the step timed out, the test fixtures will tear down, which in turn + // will abort unfinished actions in the step body. Record such errors here. + if (result?.timedOut) + testInfo._failWithError(e); + throw e; + } + }, options.timeout ? monotonicTime() + options.timeout : 0); if (result.timedOut) - throw new errors.TimeoutError(`Step timeout ${options.timeout}ms exceeded.`); + throw new errors.TimeoutError(`Step timeout of ${options.timeout}ms exceeded.`); step.complete({}); return result.result; } catch (error) { diff --git a/tests/playwright-test/test-step.spec.ts b/tests/playwright-test/test-step.spec.ts index 648cbeb52e..7d509fda25 100644 --- a/tests/playwright-test/test-step.spec.ts +++ b/tests/playwright-test/test-step.spec.ts @@ -399,7 +399,7 @@ test('step timeout option', async ({ runInlineTest }) => { }, { reporter: '', workers: 1 }); expect(result.exitCode).toBe(1); expect(result.failed).toBe(1); - expect(result.output).toContain('Error: Step timeout 100ms exceeded.'); + expect(result.output).toContain('Error: Step timeout of 100ms exceeded.'); }); test('step timeout longer than test timeout', async ({ runInlineTest }) => { @@ -422,6 +422,27 @@ test('step timeout longer than test timeout', async ({ runInlineTest }) => { expect(result.output).toContain('Test timeout of 900ms exceeded.'); }); +test('step timeout includes interrupted action errors', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'a.test.ts': ` + import { test, expect } from '@playwright/test'; + test('step with timeout', async ({ page }) => { + await test.step('my step', async () => { + await page.waitForTimeout(100_000); + }, { timeout: 1000 }); + }); + ` + }, { reporter: '', workers: 1 }); + expect(result.exitCode).toBe(1); + expect(result.failed).toBe(1); + // Should include 2 errors, one for the step timeout and one for the aborted action. + expect.soft(result.output).toContain('TimeoutError: Step timeout of 1000ms exceeded.'); + expect.soft(result.output).toContain(`> 4 | await test.step('my step', async () => {`); + expect.soft(result.output).toContain('Error: page.waitForTimeout: Test ended.'); + expect.soft(result.output.split('Error: page.waitForTimeout: Test ended.').length).toBe(2); + expect.soft(result.output).toContain('> 5 | await page.waitForTimeout(100_000);'); +}); + test('step timeout is errors.TimeoutError', async ({ runInlineTest }) => { const result = await runInlineTest({ 'a.test.ts': ` From 86768b9ebcba37c19fda8e4bb5c6ac4c4d4bb582 Mon Sep 17 00:00:00 2001 From: Max Schmitt Date: Sat, 18 Jan 2025 19:04:56 +0300 Subject: [PATCH 17/19] test: add test for consistent hyphen rendering in headless/headed (#34159) --- tests/library/headful.spec.ts | 43 +++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/tests/library/headful.spec.ts b/tests/library/headful.spec.ts index b74563850a..13445b9ce4 100644 --- a/tests/library/headful.spec.ts +++ b/tests/library/headful.spec.ts @@ -313,3 +313,46 @@ it('headless and headful should use same default fonts', async ({ page, browserN } await headlessBrowser.close(); }); + +it('should have the same hyphen rendering on headless and headed', { + annotation: { + type: 'issue', + description: 'https://github.com/microsoft/playwright/issues/33590' + } +}, async ({ browserType, page, headless, server }) => { + const content = ` + + + + + + +
+ supercalifragilisticexpialidocious +
+ + + `; + server.setRoute('/hyphenated.html', (req, res) => { + res.writeHead(200, { 'Content-Type': 'text/html' }); + res.end(content); + }); + const oppositeBrowser = await browserType.launch({ headless: !headless }); + const oppositePage = await oppositeBrowser.newPage(); + await oppositePage.goto(server.PREFIX + '/hyphenated.html'); + await page.goto(server.PREFIX + '/hyphenated.html'); + + const [divHeight1, divHeight2] = await Promise.all([ + page.evaluate(() => document.querySelector('.hyphenated').getBoundingClientRect().height), + oppositePage.evaluate(() => document.querySelector('.hyphenated').getBoundingClientRect().height), + ]); + expect(divHeight1).toBe(divHeight2); + await oppositeBrowser.close(); +}); From 99fb188cb4c05ec12f9671e185ede5c520683641 Mon Sep 17 00:00:00 2001 From: Simon Knott Date: Mon, 20 Jan 2025 09:06:01 +0100 Subject: [PATCH 18/19] chore: move attachment link back to tree item, make it flash yellow (#34353) --- packages/html-reporter/src/links.css | 10 ------- packages/html-reporter/src/links.tsx | 11 +++---- packages/html-reporter/src/testResultView.tsx | 17 ++--------- packages/html-reporter/src/treeItem.css | 13 ++++---- packages/html-reporter/src/treeItem.tsx | 8 ++--- .../trace-viewer/src/ui/attachmentsTab.css | 8 +++++ .../trace-viewer/src/ui/attachmentsTab.tsx | 30 +++++++++---------- packages/trace-viewer/src/ui/workbench.tsx | 11 +++++-- packages/web/src/uiUtils.ts | 24 +++++++++++++++ tests/playwright-test/reporter-html.spec.ts | 10 +++---- 10 files changed, 79 insertions(+), 63 deletions(-) diff --git a/packages/html-reporter/src/links.css b/packages/html-reporter/src/links.css index eb9390844b..4abe8a6caa 100644 --- a/packages/html-reporter/src/links.css +++ b/packages/html-reporter/src/links.css @@ -60,11 +60,6 @@ color: var(--color-scale-orange-6); border: 1px solid var(--color-scale-orange-4); } - .label-color-gray { - background-color: var(--color-scale-gray-0); - color: var(--color-scale-gray-6); - border: 1px solid var(--color-scale-gray-4); - } } @media(prefers-color-scheme: dark) { @@ -98,11 +93,6 @@ color: var(--color-scale-orange-2); border: 1px solid var(--color-scale-orange-4); } - .label-color-gray { - background-color: var(--color-scale-gray-9); - color: var(--color-scale-gray-2); - border: 1px solid var(--color-scale-gray-4); - } } .attachment-body { diff --git a/packages/html-reporter/src/links.tsx b/packages/html-reporter/src/links.tsx index 5f199568b5..5b79102ffe 100644 --- a/packages/html-reporter/src/links.tsx +++ b/packages/html-reporter/src/links.tsx @@ -21,7 +21,7 @@ import { TreeItem } from './treeItem'; import { CopyToClipboard } from './copyToClipboard'; import './links.css'; import { linkifyText } from '@web/renderUtils'; -import { clsx } from '@web/uiUtils'; +import { clsx, useFlash } from '@web/uiUtils'; export function navigate(href: string | URL) { window.history.pushState({}, '', href); @@ -73,7 +73,8 @@ export const AttachmentLink: React.FunctionComponent<{ linkName?: string, openInNewTab?: boolean, }> = ({ attachment, result, href, linkName, openInNewTab }) => { - const isAnchored = useIsAnchored('attachment-' + result.attachments.indexOf(attachment)); + const [flash, triggerFlash] = useFlash(); + useAnchor('attachment-' + result.attachments.indexOf(attachment), triggerFlash); return {attachment.contentType === kMissingContentType ? icons.warning() : icons.attachment()} {attachment.path && {linkName || attachment.name}} @@ -84,7 +85,7 @@ export const AttachmentLink: React.FunctionComponent<{ )} } loadChildren={attachment.body ? () => { return [
{linkifyText(attachment.body!)}
]; - } : undefined} depth={0} style={{ lineHeight: '32px' }} selected={isAnchored}>
; + } : undefined} depth={0} style={{ lineHeight: '32px' }} flash={flash}>; }; export const SearchParamsContext = React.createContext(new URLSearchParams(window.location.hash.slice(1))); @@ -118,12 +119,12 @@ const kMissingContentType = 'x-playwright/missing'; export type AnchorID = string | string[] | ((id: string) => boolean) | undefined; -export function useAnchor(id: AnchorID, onReveal: () => void) { +export function useAnchor(id: AnchorID, onReveal: React.EffectCallback) { const searchParams = React.useContext(SearchParamsContext); const isAnchored = useIsAnchored(id); React.useEffect(() => { if (isAnchored) - onReveal(); + return onReveal(); }, [isAnchored, onReveal, searchParams]); } diff --git a/packages/html-reporter/src/testResultView.tsx b/packages/html-reporter/src/testResultView.tsx index 64497b3bcd..681f4b507a 100644 --- a/packages/html-reporter/src/testResultView.tsx +++ b/packages/html-reporter/src/testResultView.tsx @@ -176,6 +176,7 @@ const StepTreeItem: React.FC<{ }> = ({ test, step, result, depth }) => { return {msToString(step.duration)} + {step.attachments.length > 0 && { evt.stopPropagation(); }}>{icons.attachment()}} {statusIcon(step.error || step.duration === -1 ? 'failed' : (step.skipped ? 'skipped' : 'passed'))} {step.title} {step.count > 1 && <> ✕ {step.count}} @@ -183,20 +184,6 @@ const StepTreeItem: React.FC<{ } loadChildren={step.steps.length || step.snippet ? () => { const snippet = step.snippet ? [] : []; const steps = step.steps.map((s, i) => ); - const attachments = step.attachments.map(attachmentIndex => ( - - - {icons.attachment()}{result.attachments[attachmentIndex].name} - - - )); - return snippet.concat(steps, attachments); + return snippet.concat(steps); } : undefined} depth={depth}/>; }; diff --git a/packages/html-reporter/src/treeItem.css b/packages/html-reporter/src/treeItem.css index f37a759c2d..b957d1ec5c 100644 --- a/packages/html-reporter/src/treeItem.css +++ b/packages/html-reporter/src/treeItem.css @@ -25,11 +25,14 @@ cursor: pointer; } -.tree-item-title.selected { - text-decoration: underline var(--color-underlinenav-icon); - text-decoration-thickness: 1.5px; -} - .tree-item-body { min-height: 18px; } + +.yellow-flash { + animation: yellowflash-bg 2s; +} +@keyframes yellowflash-bg { + from { background: var(--color-attention-subtle); } + to { background: transparent; } +} diff --git a/packages/html-reporter/src/treeItem.tsx b/packages/html-reporter/src/treeItem.tsx index 926a398a05..7ae9b840f8 100644 --- a/packages/html-reporter/src/treeItem.tsx +++ b/packages/html-reporter/src/treeItem.tsx @@ -25,12 +25,12 @@ export const TreeItem: React.FunctionComponent<{ onClick?: () => void, expandByDefault?: boolean, depth: number, - selected?: boolean, style?: React.CSSProperties, -}> = ({ title, loadChildren, onClick, expandByDefault, depth, selected, style }) => { + flash?: boolean +}> = ({ title, loadChildren, onClick, expandByDefault, depth, style, flash }) => { const [expanded, setExpanded] = React.useState(expandByDefault || false); - return
- { onClick?.(); setExpanded(!expanded); }} > + return
+ { onClick?.(); setExpanded(!expanded); }} > {loadChildren && !!expanded && icons.downArrow()} {loadChildren && !expanded && icons.rightArrow()} {!loadChildren && {icons.rightArrow()}} diff --git a/packages/trace-viewer/src/ui/attachmentsTab.css b/packages/trace-viewer/src/ui/attachmentsTab.css index c2455fc3c5..7d487bb3f2 100644 --- a/packages/trace-viewer/src/ui/attachmentsTab.css +++ b/packages/trace-viewer/src/ui/attachmentsTab.css @@ -55,3 +55,11 @@ a.codicon-cloud-download:hover{ background-color: var(--vscode-list-inactiveSelectionBackground) } + +.yellow-flash { + animation: yellowflash-bg 2s; +} +@keyframes yellowflash-bg { + from { background: var(--vscode-peekViewEditor-matchHighlightBackground); } + to { background: transparent; } +} diff --git a/packages/trace-viewer/src/ui/attachmentsTab.tsx b/packages/trace-viewer/src/ui/attachmentsTab.tsx index cf9ed2e681..7a636a83b0 100644 --- a/packages/trace-viewer/src/ui/attachmentsTab.tsx +++ b/packages/trace-viewer/src/ui/attachmentsTab.tsx @@ -17,36 +17,38 @@ import * as React from 'react'; import './attachmentsTab.css'; import { ImageDiffView } from '@web/shared/imageDiffView'; -import type { ActionTraceEventInContext, MultiTraceModel } from './modelUtil'; +import type { MultiTraceModel } from './modelUtil'; import { PlaceholderPanel } from './placeholderPanel'; import type { AfterActionTraceEventAttachment } from '@trace/trace'; import { CodeMirrorWrapper, lineHeight } from '@web/components/codeMirrorWrapper'; import { isTextualMimeType } from '@isomorphic/mimeType'; import { Expandable } from '@web/components/expandable'; import { linkifyText } from '@web/renderUtils'; -import { clsx } from '@web/uiUtils'; +import { clsx, useFlash } from '@web/uiUtils'; type Attachment = AfterActionTraceEventAttachment & { traceUrl: string }; type ExpandableAttachmentProps = { attachment: Attachment; - reveal: boolean; - highlight: boolean; + reveal?: any; }; -const ExpandableAttachment: React.FunctionComponent = ({ attachment, reveal, highlight }) => { +const ExpandableAttachment: React.FunctionComponent = ({ attachment, reveal }) => { const [expanded, setExpanded] = React.useState(false); const [attachmentText, setAttachmentText] = React.useState(null); const [placeholder, setPlaceholder] = React.useState(null); + const [flash, triggerFlash] = useFlash(); const ref = React.useRef(null); const isTextAttachment = isTextualMimeType(attachment.contentType); const hasContent = !!attachment.sha1 || !!attachment.path; React.useEffect(() => { - if (reveal) + if (reveal) { ref.current?.scrollIntoView({ behavior: 'smooth' }); - }, [reveal]); + return triggerFlash(); + } + }, [reveal, triggerFlash]); React.useEffect(() => { if (expanded && attachmentText === null && placeholder === null) { @@ -66,14 +68,14 @@ const ExpandableAttachment: React.FunctionComponent = }, [attachmentText]); const title = - {linkifyText(attachment.name)} + {linkifyText(attachment.name)} {hasContent && download} ; if (!isTextAttachment || !hasContent) return
{title}
; - return <> + return
{placeholder && {placeholder}} @@ -87,14 +89,13 @@ const ExpandableAttachment: React.FunctionComponent = wrapLines={false}>
} - ; +
; }; export const AttachmentsTab: React.FunctionComponent<{ model: MultiTraceModel | undefined, - selectedAction: ActionTraceEventInContext | undefined, - revealedAttachment?: AfterActionTraceEventAttachment, -}> = ({ model, selectedAction, revealedAttachment }) => { + revealedAttachment?: [AfterActionTraceEventAttachment, number], +}> = ({ model, revealedAttachment }) => { const { diffMap, screenshots, attachments } = React.useMemo(() => { const attachments = new Set(); const screenshots = new Set(); @@ -153,8 +154,7 @@ export const AttachmentsTab: React.FunctionComponent<{ return
isEqualAttachment(a, selected)) ?? false} - reveal={!!revealedAttachment && isEqualAttachment(a, revealedAttachment)} + reveal={(!!revealedAttachment && isEqualAttachment(a, revealedAttachment[0])) ? revealedAttachment : undefined} />
; })} diff --git a/packages/trace-viewer/src/ui/workbench.tsx b/packages/trace-viewer/src/ui/workbench.tsx index c6fcd7e54c..de59892772 100644 --- a/packages/trace-viewer/src/ui/workbench.tsx +++ b/packages/trace-viewer/src/ui/workbench.tsx @@ -59,7 +59,7 @@ export const Workbench: React.FunctionComponent<{ }> = ({ model, showSourcesFirst, rootDir, fallbackLocation, isLive, hideTimeline, status, annotations, inert, onOpenExternally, revealSource }) => { const [selectedCallId, setSelectedCallId] = React.useState(undefined); const [revealedError, setRevealedError] = React.useState(undefined); - const [revealedAttachment, setRevealedAttachment] = React.useState(undefined); + const [revealedAttachment, setRevealedAttachment] = React.useState<[attachment: AfterActionTraceEventAttachment, renderCounter: number] | undefined>(undefined); const [highlightedCallId, setHighlightedCallId] = React.useState(); const [highlightedEntry, setHighlightedEntry] = React.useState(); const [highlightedConsoleMessage, setHighlightedConsoleMessage] = React.useState(); @@ -148,7 +148,12 @@ export const Workbench: React.FunctionComponent<{ const revealAttachment = React.useCallback((attachment: AfterActionTraceEventAttachment) => { selectPropertiesTab('attachments'); - setRevealedAttachment(attachment); + setRevealedAttachment(currentValue => { + if (!currentValue) + return [attachment, 0]; + const revealCounter = currentValue[1]; + return [attachment, revealCounter + 1]; + }); }, [selectPropertiesTab]); React.useEffect(() => { @@ -238,7 +243,7 @@ export const Workbench: React.FunctionComponent<{ id: 'attachments', title: 'Attachments', count: attachments.length, - render: () => + render: () => }; const tabs: TabbedPaneTabModel[] = [ diff --git a/packages/web/src/uiUtils.ts b/packages/web/src/uiUtils.ts index 3544ec4bdc..a0b7a59c36 100644 --- a/packages/web/src/uiUtils.ts +++ b/packages/web/src/uiUtils.ts @@ -14,6 +14,7 @@ limitations under the License. */ +import type { EffectCallback } from 'react'; import React from 'react'; // Recalculates the value when dependencies change. @@ -224,3 +225,26 @@ export function scrollIntoViewIfNeeded(element: Element | undefined) { const kControlCodesRe = '\\u0000-\\u0020\\u007f-\\u009f'; export const kWebLinkRe = new RegExp('(?:[a-zA-Z][a-zA-Z0-9+.-]{2,}:\\/\\/|www\\.)[^\\s' + kControlCodesRe + '"]{2,}[^\\s' + kControlCodesRe + '"\')}\\],:;.!?]', 'ug'); + +/** + * Manages flash animation state. + * Calling `trigger` will turn `flash` to true for a second, and then back to false. + * If `trigger` is called while a flash is ongoing, the ongoing flash will be cancelled and after 50ms a new flash is started. + * @returns [flash, trigger] + */ +export function useFlash(): [boolean, EffectCallback] { + const [flash, setFlash] = React.useState(false); + const trigger = React.useCallback(() => { + const timeouts: any[] = []; + setFlash(currentlyFlashing => { + timeouts.push(setTimeout(() => setFlash(false), 1000)); + if (!currentlyFlashing) + return true; + + timeouts.push(setTimeout(() => setFlash(true), 50)); + return false; + }); + return () => timeouts.forEach(clearTimeout); + }, [setFlash]); + return [flash, trigger]; +} diff --git a/tests/playwright-test/reporter-html.spec.ts b/tests/playwright-test/reporter-html.spec.ts index 2640cb61c9..57ef76a7ca 100644 --- a/tests/playwright-test/reporter-html.spec.ts +++ b/tests/playwright-test/reporter-html.spec.ts @@ -959,10 +959,9 @@ for (const useIntermediateMergeReport of [true, false] as const) { await showReport(); await page.getByRole('link', { name: 'passing' }).click(); - const attachment = page.getByTestId('attachments').getByText('foo-2', { exact: true }); + const attachment = page.getByText('foo-2', { exact: true }); await expect(attachment).not.toBeInViewport(); - await page.getByLabel('attach "foo-2"').click(); - await page.getByTitle('see "foo-2"').click(); + await page.getByLabel(`attach "foo-2"`).getByTitle('reveal attachment').click(); await expect(attachment).toBeInViewport(); await page.reload(); @@ -989,10 +988,9 @@ for (const useIntermediateMergeReport of [true, false] as const) { await showReport(); await page.getByRole('link', { name: 'passing' }).click(); - const attachment = page.getByTestId('attachments').getByText('attachment', { exact: true }); + const attachment = page.getByText('attachment', { exact: true }); await expect(attachment).not.toBeInViewport(); - await page.getByLabel('step').click(); - await page.getByTitle('see "attachment"').click(); + await page.getByLabel('step').getByTitle('reveal attachment').click(); await expect(attachment).toBeInViewport(); }); From e20b6d1617354191c635ece258b346290a25ce03 Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Mon, 20 Jan 2025 16:28:41 +0000 Subject: [PATCH 19/19] feat(chromium-tip-of-tree): roll to r1295 (#34372) --- packages/playwright-core/browsers.json | 4 ++-- .../playwright-core/src/server/chromium/chromiumSwitches.ts | 5 ++++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/playwright-core/browsers.json b/packages/playwright-core/browsers.json index 0895f005b1..54d42fe041 100644 --- a/packages/playwright-core/browsers.json +++ b/packages/playwright-core/browsers.json @@ -9,9 +9,9 @@ }, { "name": "chromium-tip-of-tree", - "revision": "1293", + "revision": "1295", "installByDefault": false, - "browserVersion": "133.0.6943.0" + "browserVersion": "134.0.6960.0" }, { "name": "firefox", diff --git a/packages/playwright-core/src/server/chromium/chromiumSwitches.ts b/packages/playwright-core/src/server/chromium/chromiumSwitches.ts index 4774c13ed7..9d64d58f1c 100644 --- a/packages/playwright-core/src/server/chromium/chromiumSwitches.ts +++ b/packages/playwright-core/src/server/chromium/chromiumSwitches.ts @@ -38,7 +38,10 @@ export const chromiumSwitches = [ // ThirdPartyStoragePartitioning - https://github.com/microsoft/playwright/issues/32230 // LensOverlay - Hides the Lens feature in the URL address bar. Its not working in unofficial builds. // PlzDedicatedWorker - https://github.com/microsoft/playwright/issues/31747 - '--disable-features=ImprovedCookieControls,LazyFrameLoading,GlobalMediaControls,DestroyProfileOnBrowserClose,MediaRouter,DialMediaRouteProvider,AcceptCHFrame,AutoExpandDetailsElement,CertificateTransparencyComponentUpdater,AvoidUnnecessaryBeforeUnloadCheckSync,Translate,HttpsUpgrades,PaintHolding,ThirdPartyStoragePartitioning,LensOverlay,PlzDedicatedWorker', + // DeferRendererTasksAfterInput - this makes Page.frameScheduledNavigation arrive much later after a click, + // making our navigation auto-wait after click not working. Can be removed once we deperecate noWaitAfter. + // See https://github.com/microsoft/playwright/pull/34372. + '--disable-features=ImprovedCookieControls,LazyFrameLoading,GlobalMediaControls,DestroyProfileOnBrowserClose,MediaRouter,DialMediaRouteProvider,AcceptCHFrame,AutoExpandDetailsElement,CertificateTransparencyComponentUpdater,AvoidUnnecessaryBeforeUnloadCheckSync,Translate,HttpsUpgrades,PaintHolding,ThirdPartyStoragePartitioning,LensOverlay,PlzDedicatedWorker,DeferRendererTasksAfterInput', '--allow-pre-commit-input', '--disable-hang-monitor', '--disable-ipc-flooding-protection',