+ 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 || '');
- }}>
-
;
};
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/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/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/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/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]
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/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();
+});
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..205bbbae5e 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,29 +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, codegenMode }, testInfo) => {
- test.skip(codegenMode === 'trace-events');
- 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, 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');
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();
});
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;
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();
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 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',
- ]);
- 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(`
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 }) => {
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/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': `
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();
});
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/test-step.spec.ts b/tests/playwright-test/test-step.spec.ts
index ec04ef19e8..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': `
@@ -616,7 +637,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 +664,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 +930,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 +977,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 +1028,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 +1080,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 +1586,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
+`);
+});
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 }) => {