Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 4 additions & 6 deletions docs/src/test-api/class-testoptions.md
Original file line number Diff line number Diff line change
Expand Up @@ -584,8 +584,8 @@ export default defineConfig({

## property: TestOptions.trace
* since: v1.10
- type: <[Object]|[TraceMode]<"off"|"on"|"retain-on-failure"|"on-first-retry"|"retain-on-first-failure"|"retain-on-failure-and-retries"|"retain-all-failures">>
- `mode` <[TraceMode]<"off"|"on"|"retain-on-failure"|"on-first-retry"|"on-all-retries"|"retain-on-first-failure"|"retain-on-failure-and-retries"|"retain-all-failures">> Trace recording mode.
- type: <[Object]|[TraceMode]<"off"|"on"|"retain-on-failure"|"on-first-retry"|"retain-on-first-failure"|"retain-on-failure-and-retries">>
- `mode` <[TraceMode]<"off"|"on"|"retain-on-failure"|"on-first-retry"|"on-all-retries"|"retain-on-first-failure"|"retain-on-failure-and-retries">> Trace recording mode.
- `attachments` ?<[boolean]> Whether to include test attachments. Defaults to true. Optional.
- `screenshots` ?<[boolean]> Whether to capture screenshots during tracing. Screenshots are used to build a timeline preview. Defaults to true. Optional.
- `snapshots` ?<[boolean]> Whether to capture DOM snapshot on every action. Defaults to true. Optional.
Expand All @@ -599,7 +599,6 @@ Whether to record trace for each test. Defaults to `'off'`.
* `'retain-on-failure'`: Record trace for each test. When test run passes, remove the recorded trace.
* `'retain-on-first-failure'`: Record trace for the first run of each test, but not for retries. When test run passes, remove the recorded trace.
* `'retain-on-failure-and-retries'`: Record trace for each test run. Retains all traces when an attempt fails.
* `'retain-all-failures'`: Record trace for each test run. Retains the trace only for attempts that failed, regardless of the final test outcome.

For more control, pass an object that specifies `mode` and trace features to enable.

Expand Down Expand Up @@ -634,8 +633,8 @@ export default defineConfig({

## property: TestOptions.video
* since: v1.10
- type: <[Object]|[VideoMode]<"off"|"on"|"retain-on-failure"|"on-first-retry"|"on-all-retries"|"retain-on-first-failure"|"retain-on-failure-and-retries"|"retain-all-failures">>
- `mode` <[VideoMode]<"off"|"on"|"retain-on-failure"|"on-first-retry"|"on-all-retries"|"retain-on-first-failure"|"retain-on-failure-and-retries"|"retain-all-failures">> Video recording mode.
- type: <[Object]|[VideoMode]<"off"|"on"|"retain-on-failure"|"on-first-retry"|"on-all-retries"|"retain-on-first-failure"|"retain-on-failure-and-retries">>
- `mode` <[VideoMode]<"off"|"on"|"retain-on-failure"|"on-first-retry"|"on-all-retries"|"retain-on-first-failure"|"retain-on-failure-and-retries">> Video recording mode.
- `size` ?<[Object]> Size of the recorded video. Optional.
- `width` <[int]>
- `height` <[int]>
Expand All @@ -657,7 +656,6 @@ Whether to record video for each test. Defaults to `'off'`.
* `'retain-on-failure'`: Record video for each test. When test run passes, remove the recorded video.
* `'retain-on-first-failure'`: Record video for the first run of each test, but not for retries. When test run passes, remove the recorded video.
* `'retain-on-failure-and-retries'`: Record video for each test run. Retains all videos when an attempt fails.
* `'retain-all-failures'`: Record video for each test run. Retains the video only for attempts that failed, regardless of the final test outcome.

To control video size, pass an object with `mode` and `size` properties. If video size is not specified, it will be equal to [`property: TestOptions.viewport`] scaled down to fit into 800x800. If `viewport` is not configured explicitly the video size defaults to 800x450. Actual picture of each page will be scaled down if necessary to fit the specified size.

Expand Down
2 changes: 1 addition & 1 deletion docs/src/test-cli-js.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ npx playwright test --ui
| `--test-list <file>` | Path to a file containing a list of tests to run. See [test list](#test-list) for details. |
| `--test-list-invert <file>` | Path to a file containing a list of tests to skip. See [test list](#test-list) for details. |
| `--timeout <timeout>` | Specify test timeout threshold in milliseconds, zero for unlimited (default: 30 seconds). |
| `--trace <mode>` | Force tracing mode, can be `on`, `off`, `on-first-retry`, `on-all-retries`, `retain-on-failure`, `retain-on-first-failure`, `retain-on-failure-and-retries`, `retain-all-failures`. |
| `--trace <mode>` | Force tracing mode, can be `on`, `off`, `on-first-retry`, `on-all-retries`, `retain-on-failure`, `retain-on-first-failure`, `retain-on-failure-and-retries`. |
| `--tsconfig <path>` | Path to a single tsconfig applicable to all imported files (default: look up tsconfig for each imported file separately). |
| `--ui` | Run tests in interactive UI mode. |
| `--ui-host <host>` | Host to serve UI on; specifying this option opens UI in a browser tab. |
Expand Down
2 changes: 0 additions & 2 deletions docs/src/webview2.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,6 @@ By default, the WebView2 control will use the same user data directory for all i

Using the following, Playwright will run your WebView2 application as a sub-process, assign a unique user data directory to it and provide the [Page] instance to your test:

<!-- source code is available here to verify that the examples are working https://github.com/mxschmitt/playwright-webview2-demo -->

```js title="webView2Test.ts"
import { test as base } from '@playwright/test';
import fs from 'fs';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@ export function frameSnapshotStreamer(snapshotStreamer: string, removeNoScript:
return obj[kCachedData];
}

const kObserverConfig: MutationObserverInit = { attributes: true, subtree: true };

function removeHash(url: string) {
try {
const u = new URL(url);
Expand All @@ -90,6 +92,7 @@ export function frameSnapshotStreamer(snapshotStreamer: string, removeNoScript:
private _readingStyleSheet = false; // To avoid invalidating due to our own reads.
private _fakeBase: HTMLBaseElement;
private _observer: MutationObserver;
private _observedDocument: Document | undefined;
private _targetGeneration = 0;

constructor() {
Expand All @@ -116,9 +119,7 @@ export function frameSnapshotStreamer(snapshotStreamer: string, removeNoScript:
this._fakeBase = document.createElement('base');

this._observer = new MutationObserver(list => this._handleMutations(list));
const observerConfig = { attributes: true, subtree: true };
this._observer.observe(document, observerConfig);
this._refreshListenersWhenNeeded();
this._ensureObservingCurrentDocument();
}

private _refreshListenersWhenNeeded() {
Expand Down Expand Up @@ -210,6 +211,19 @@ export function frameSnapshotStreamer(snapshotStreamer: string, removeNoScript:
ensureCachedData(mutation.target).attributesCached = undefined;
}

private _ensureObservingCurrentDocument() {
// A window can swap its document without re-running the init script (e.g.
// a popup reusing its initial about:blank document), leaving our observers
// and listeners bound to the stale one. Re-attach on swap.
// https://github.com/microsoft/playwright/issues/40895
if (this._observedDocument === document)
return;
this._observedDocument = document;
this._observer.disconnect();
this._observer.observe(document, kObserverConfig);
this._refreshListenersWhenNeeded();
}

private _invalidateStyleSheet(sheet: CSSStyleSheet) {
if (this._readingStyleSheet)
return;
Expand Down Expand Up @@ -347,6 +361,7 @@ export function frameSnapshotStreamer(snapshotStreamer: string, removeNoScript:
let shadowDomNesting = 0;
let headNesting = 0;

this._ensureObservingCurrentDocument();
// Ensure we are up to date.
this._handleMutations(this._observer.takeRecords());

Expand Down
14 changes: 0 additions & 14 deletions packages/playwright-core/src/tools/backend/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,6 @@ export type ContextConfig = {
outputMaxSize?: number;
outputMode?: 'file' | 'stdout';
saveSession?: boolean;
saveTrace?: boolean;
secrets?: Record<string, string>;
snapshot?: {
mode?: 'full' | 'none';
Expand Down Expand Up @@ -327,19 +326,6 @@ export class Context {
const browserContext = this._rawBrowserContext;
await this._setupRequestInterception(browserContext);

if (this.config.saveTrace) {
await browserContext.tracing.start({
name: 'trace-' + Date.now(),
screenshots: true,
snapshots: true,
live: true,
});
this._disposables.push({
dispose: async () => {
await browserContext.tracing.stop();
},
});
}
for (const initScript of this.config.browser?.initScript || [])
this._disposables.push(await browserContext.addInitScript({ path: path.resolve(this.options.cwd, initScript) }));

Expand Down
1 change: 0 additions & 1 deletion packages/playwright-core/src/tools/mcp/configIni.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,6 @@ const longhandTypes: Record<string, LonghandType> = {
'extension': 'boolean',
'capabilities': 'string[]',
'saveSession': 'boolean',
'saveTrace': 'boolean',
'saveVideo': 'size',
'sharedBrowserContext': 'boolean',
'outputDir': 'string',
Expand Down
2 changes: 0 additions & 2 deletions packages/playwright/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -523,7 +523,6 @@ function shouldCaptureVideo(videoMode: VideoMode, testInfo: TestInfo) {
return videoMode === 'on'
|| videoMode === 'retain-on-failure'
|| videoMode === 'retain-on-failure-and-retries'
|| videoMode === 'retain-all-failures'
|| (videoMode === 'on-first-retry' && testInfo.retry === 1)
|| (videoMode === 'on-all-retries' && testInfo.retry > 0)
|| (videoMode === 'retain-on-first-failure' && testInfo.retry === 0);
Expand All @@ -538,7 +537,6 @@ function shouldPreserveVideo(videoMode: VideoMode, testInfo: TestInfo) {
return true;
case 'retain-on-failure':
case 'retain-on-first-failure':
case 'retain-all-failures':
return testFailed;
case 'retain-on-failure-and-retries':
return testFailed || testInfo.retry > 0;
Expand Down
2 changes: 1 addition & 1 deletion packages/playwright/src/program.ts
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ function addInitAgentsCommand(program: Command) {
});
}

const kTraceModes: TraceMode[] = ['on', 'off', 'on-first-retry', 'on-all-retries', 'retain-on-failure', 'retain-on-first-failure', 'retain-on-failure-and-retries', 'retain-all-failures'];
const kTraceModes: TraceMode[] = ['on', 'off', 'on-first-retry', 'on-all-retries', 'retain-on-failure', 'retain-on-first-failure', 'retain-on-failure-and-retries'];

// Note: update docs/src/test-cli-js.md when you update this, program is the source of truth.

Expand Down
5 changes: 0 additions & 5 deletions packages/playwright/src/worker/testTracing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,9 +90,6 @@ export class TestTracing {
if (this._options?.mode === 'retain-on-failure-and-retries')
return true;

if (this._options?.mode === 'retain-all-failures')
return true;

return false;
}

Expand Down Expand Up @@ -171,8 +168,6 @@ export class TestTracing {
const testFailed = this._testInfo.status !== this._testInfo.expectedStatus;
if (this._options.mode === 'retain-on-failure-and-retries')
return !testFailed && this._testInfo.retry === 0;
if (this._options.mode === 'retain-all-failures')
return !testFailed;
return !testFailed && (this._options.mode === 'retain-on-failure' || this._options.mode === 'retain-on-first-failure');
}

Expand Down
8 changes: 2 additions & 6 deletions packages/playwright/types/test.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6999,8 +6999,6 @@ export interface PlaywrightWorkerOptions {
* - `'retain-on-first-failure'`: Record trace for the first run of each test, but not for retries. When test run
* passes, remove the recorded trace.
* - `'retain-on-failure-and-retries'`: Record trace for each test run. Retains all traces when an attempt fails.
* - `'retain-all-failures'`: Record trace for each test run. Retains the trace only for attempts that failed,
* regardless of the final test outcome.
*
* For more control, pass an object that specifies `mode` and trace features to enable.
*
Expand Down Expand Up @@ -7030,8 +7028,6 @@ export interface PlaywrightWorkerOptions {
* - `'retain-on-first-failure'`: Record video for the first run of each test, but not for retries. When test run
* passes, remove the recorded video.
* - `'retain-on-failure-and-retries'`: Record video for each test run. Retains all videos when an attempt fails.
* - `'retain-all-failures'`: Record video for each test run. Retains the video only for attempts that failed,
* regardless of the final test outcome.
*
* To control video size, pass an object with `mode` and `size` properties. If video size is not specified, it will be
* equal to [testOptions.viewport](https://playwright.dev/docs/api/class-testoptions#test-options-viewport) scaled
Expand Down Expand Up @@ -7061,8 +7057,8 @@ export interface PlaywrightWorkerOptions {
}

export type ScreenshotMode = 'off' | 'on' | 'only-on-failure' | 'on-first-failure';
export type TraceMode = 'off' | 'on' | 'retain-on-failure' | 'on-first-retry' | 'on-all-retries' | 'retain-on-first-failure' | 'retain-on-failure-and-retries' | 'retain-all-failures';
export type VideoMode = 'off' | 'on' | 'retain-on-failure' | 'on-first-retry' | 'on-all-retries' | 'retain-on-first-failure' | 'retain-on-failure-and-retries' | 'retain-all-failures';
export type TraceMode = 'off' | 'on' | 'retain-on-failure' | 'on-first-retry' | 'on-all-retries' | 'retain-on-first-failure' | 'retain-on-failure-and-retries';
export type VideoMode = 'off' | 'on' | 'retain-on-failure' | 'on-first-retry' | 'on-all-retries' | 'retain-on-first-failure' | 'retain-on-failure-and-retries';
/**
* Playwright Test provides many options to configure test environment,
* [Browser](https://playwright.dev/docs/api/class-browser),
Expand Down
26 changes: 26 additions & 0 deletions tests/library/trace-viewer.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -681,6 +681,32 @@ test('should popup snapshot', async ({ page, runAndTrace, server }) => {
await expect(popup.frameLocator('iframe').getByText('hello äöü 🙂')).toBeVisible();
});

test('should capture attribute mutations inside a popup window', {
annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/40895' }
}, async ({ page, server, runAndTrace }) => {
server.setRoute('/popup.html', (req, res) => {
res.end(`<!DOCTYPE html><html><head>
<style>.no-display { display: none !important; }</style>
</head><body>
<div id="overlay">overlay</div>
<button id="hide" type="button" onclick="document.getElementById('overlay').classList.add('no-display')">Hide</button>
</body></html>`);
});

const traceViewer = await runAndTrace(async () => {
await page.goto(server.EMPTY_PAGE);
const [popup] = await Promise.all([
page.waitForEvent('popup'),
page.evaluate(url => window.open(url, 'popup', 'popup'), server.PREFIX + '/popup.html'),
]);
await popup.getByRole('button', { name: 'Hide' }).click();
await expect(popup.locator('#overlay')).toBeHidden();
});

const frame = await traceViewer.snapshotFrame('Click');
await expect(frame.locator('#overlay')).toHaveClass('no-display');
});

test('should capture iframe with sandbox attribute', async ({ page, server, runAndTrace }) => {
await page.route('**/empty.html', route => {
void route.fulfill({
Expand Down
4 changes: 2 additions & 2 deletions tests/mcp/config.ini.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,15 +105,15 @@ test('ini config boolean values', async ({ startClient }) => {
const { client } = await startClient({
config: `
capabilities = config
saveTrace = true
saveSession = true
browser.contextOptions.bypassCSP = true
browser.contextOptions.javaScriptEnabled = false
`,
});

const result = await client.callTool({ name: 'browser_get_config' });
const config = JSON.parse(parseResponse(result).result);
expect(config.saveTrace).toBe(true);
expect(config.saveSession).toBe(true);
expect(config.browser.contextOptions.bypassCSP).toBe(true);
expect(config.browser.contextOptions.javaScriptEnabled).toBe(false);
});
28 changes: 0 additions & 28 deletions tests/playwright-test/playwright.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -617,34 +617,6 @@ test('should work with video: retain-on-failure-and-retries', async ({ runInline
expect(fs.readdirSync(dirRetry).find(file => file.endsWith('webm'))).toBeTruthy();
});

test('should work with video: retain-all-failures', async ({ runInlineTest }) => {
const result = await runInlineTest({
'playwright.config.ts': `
module.exports = { use: { video: 'retain-all-failures' }, retries: 1, name: 'chromium' };
`,
'a.test.ts': `
import { test, expect } from '@playwright/test';
test('flaky', async ({ page }) => {
await page.setContent('<div>FLAKY</div>');
await page.waitForTimeout(1000);
test.expect(test.info().retry).toBe(1);
});
`,
}, { workers: 1 });

expect(result.exitCode).toBe(0);
expect(result.flaky).toBe(1);

// First attempt failed, video retained.
const dirFail = test.info().outputPath('test-results', 'a-flaky-chromium');
expect(fs.readdirSync(dirFail).find(file => file.endsWith('webm'))).toBeTruthy();

// Retry passed, so its video is removed.
const dirRetry = test.info().outputPath('test-results', 'a-flaky-chromium-retry1');
const videoRetry = fs.existsSync(dirRetry) ? fs.readdirSync(dirRetry).find(file => file.endsWith('webm')) : undefined;
expect(videoRetry).toBeFalsy();
});

test('should work with video size', async ({ runInlineTest }) => {
const result = await runInlineTest({
'playwright.config.js': `
Expand Down
44 changes: 1 addition & 43 deletions tests/playwright-test/playwright.trace.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -386,7 +386,7 @@ test('should respect --trace', async ({ runInlineTest }, testInfo) => {
expect(fs.existsSync(testInfo.outputPath('test-results', 'a-test-1', 'trace.zip'))).toBeTruthy();
});

for (const mode of ['off', 'retain-on-failure', 'on-first-retry', 'on-all-retries', 'retain-on-first-failure', 'retain-on-failure-and-retries', 'retain-all-failures']) {
for (const mode of ['off', 'retain-on-failure', 'on-first-retry', 'on-all-retries', 'retain-on-first-failure', 'retain-on-failure-and-retries']) {
test(`trace:${mode} should not create trace zip artifact if page test passed`, async ({ runInlineTest }) => {
const result = await runInlineTest({
'a.spec.ts': `
Expand Down Expand Up @@ -1196,48 +1196,6 @@ test('trace:retain-on-failure-and-retries should keep all traces when test fails
expect(fs.existsSync(retryTracePath)).toBeTruthy();
});

test('trace:retain-all-failures should keep traces only for failed attempts when test is flaky', async ({ runInlineTest }, testInfo) => {
const result = await runInlineTest({
'a.spec.ts': `
import { test, expect } from '@playwright/test';
test('flaky', async ({ page }) => {
await page.goto('about:blank');
expect(test.info().retry).toBe(2);
});
`,
}, { trace: 'retain-all-failures', retries: 2 });

expect(result.exitCode).toBe(0);
expect(result.flaky).toBe(1);

const firstRunTracePath = testInfo.outputPath('test-results', 'a-flaky', 'trace.zip');
expect(fs.existsSync(firstRunTracePath)).toBeTruthy();
const retry1TracePath = testInfo.outputPath('test-results', 'a-flaky-retry1', 'trace.zip');
expect(fs.existsSync(retry1TracePath)).toBeTruthy();
const retry2TracePath = testInfo.outputPath('test-results', 'a-flaky-retry2', 'trace.zip');
expect(fs.existsSync(retry2TracePath)).toBeFalsy();
});

test('trace:retain-all-failures should keep all traces when test fails on every attempt', async ({ runInlineTest }, testInfo) => {
const result = await runInlineTest({
'a.spec.ts': `
import { test, expect } from '@playwright/test';
test('fail', async ({ page }) => {
await page.goto('about:blank');
expect(true).toBe(false);
});
`,
}, { trace: 'retain-all-failures', retries: 1 });

expect(result.exitCode).toBe(1);
expect(result.failed).toBe(1);

const firstRunTracePath = testInfo.outputPath('test-results', 'a-fail', 'trace.zip');
expect(fs.existsSync(firstRunTracePath)).toBeTruthy();
const retryTracePath = testInfo.outputPath('test-results', 'a-fail-retry1', 'trace.zip');
expect(fs.existsSync(retryTracePath)).toBeTruthy();
});

test('should not corrupt actions when no library trace is present', async ({ runInlineTest }) => {
const result = await runInlineTest({
'a.spec.ts': `
Expand Down
Loading
Loading