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
5 changes: 4 additions & 1 deletion docs/src/test-fixtures-js.md
Original file line number Diff line number Diff line change
Expand Up @@ -354,6 +354,8 @@ Playwright Test uses [worker processes](./test-parallel.md) to run test files. S

Below we'll create an `account` fixture that will be shared by all tests in the same worker, and override the `page` fixture to log in to this account for each test. To generate unique accounts, we'll use the [`property: WorkerInfo.workerIndex`] that is available to any test or fixture. Note the tuple-like syntax for the worker fixture - we have to pass `{scope: 'worker'}` so that test runner sets this fixture up once per worker.

In addition to only being run once per worker, worker-scoped fixtures also get a separate timeout equal to the default test timeout. You can change it by passing the `timeout` option. See [fixture timeout](#fixture-timeout) for more details.

```js title="my-test.ts"
import { test as base } from '@playwright/test';

Expand Down Expand Up @@ -434,7 +436,7 @@ export { expect } from '@playwright/test';

## Fixture timeout

By default, the fixture inherits the timeout value of the test. However, for slow fixtures, especially [worker-scoped](#worker-scoped-fixtures) ones, it is convenient to have a separate timeout. This way you can keep the overall test timeout small, and give the slow fixture more time.
Fixture is considered to be a part of a test, and so its setup and teardown running time counts towards the test timeout. Therefore, a slow fixture may cause test timeouts. You can set a separate larger timeout for such a fixture, and keep the overall test timeout small.

```js
import { test as base, expect } from '@playwright/test';
Expand All @@ -451,6 +453,7 @@ test('example test', async ({ slowFixture }) => {
});
```

Unlike regular test-scoped fixtures, each [worker-scoped](#worker-scoped-fixtures) fixture has its own timeout, equal to the test timeout. You can change the timeout for a worker-scoped fixture in the same way.

## Fixtures-options

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ export function frameSnapshotStreamer(snapshotStreamer: string, removeNoScript:
class Streamer {
private _lastSnapshotNumber = 0;
private _staleStyleSheets = new Set<CSSStyleSheet>();
private _modifiedStyleSheets = new Set<CSSStyleSheet>();
private _readingStyleSheet = false; // To avoid invalidating due to our own reads.
private _fakeBase: HTMLBaseElement;
private _observer: MutationObserver;
Expand All @@ -105,6 +106,10 @@ export function frameSnapshotStreamer(snapshotStreamer: string, removeNoScript:
this._interceptNativeMethod(window.CSSGroupingRule.prototype, 'insertRule', invalidateCSSGroupingRule);
this._interceptNativeMethod(window.CSSGroupingRule.prototype, 'deleteRule', invalidateCSSGroupingRule);
this._interceptNativeGetter(window.CSSGroupingRule.prototype, 'cssRules', invalidateCSSGroupingRule);
this._interceptNativeSetter(window.StyleSheet.prototype, 'disabled', (sheet: StyleSheet) => {
if (sheet instanceof CSSStyleSheet)
this._invalidateStyleSheet(sheet as CSSStyleSheet);
});
this._interceptNativeAsyncMethod(window.CSSStyleSheet.prototype, 'replace', (sheet: CSSStyleSheet) => this._invalidateStyleSheet(sheet));

this._fakeBase = document.createElement('base');
Expand Down Expand Up @@ -191,6 +196,18 @@ export function frameSnapshotStreamer(snapshotStreamer: string, removeNoScript:
});
}

private _interceptNativeSetter(obj: any, prop: string, cb: (thisObj: any, result: any) => void) {
const descriptor = Object.getOwnPropertyDescriptor(obj, prop)!;
Object.defineProperty(obj, prop, {
...descriptor,
set: function(value: any) {
const result = descriptor.set!.call(this, value);
cb(this, value);
return result;
},
});
}

private _handleMutations(list: MutationRecord[]) {
for (const mutation of list)
ensureCachedData(mutation.target).attributesCached = undefined;
Expand All @@ -200,6 +217,8 @@ export function frameSnapshotStreamer(snapshotStreamer: string, removeNoScript:
if (this._readingStyleSheet)
return;
this._staleStyleSheets.add(sheet);
if (sheet.href !== null)
this._modifiedStyleSheets.add(sheet);
}

private _updateStyleElementStyleSheetTextIfNeeded(sheet: CSSStyleSheet, forceText?: boolean): string | undefined {
Expand Down Expand Up @@ -309,6 +328,8 @@ export function frameSnapshotStreamer(snapshotStreamer: string, removeNoScript:
private _getSheetText(sheet: CSSStyleSheet): string {
this._readingStyleSheet = true;
try {
if (sheet.disabled)
return '';
const rules: string[] = [];
for (const rule of sheet.cssRules)
rules.push(rule.cssText);
Expand Down Expand Up @@ -614,7 +635,7 @@ export function frameSnapshotStreamer(snapshotStreamer: string, removeNoScript:
collectionTime: 0,
};

for (const sheet of this._staleStyleSheets) {
for (const sheet of this._modifiedStyleSheets) {
if (sheet.href === null)
continue;
const content = this._updateLinkStyleSheetTextIfNeeded(sheet, snapshotNumber);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -193,20 +193,24 @@ export class SnapshotRenderer {
let result = sameFrameResource ?? otherFrameResource;
if (result && method.toUpperCase() === 'GET') {
// Patch override if necessary.
for (const o of snapshot.resourceOverrides) {
if (url === o.url && o.sha1) {
result = {
...result,
response: {
...result.response,
content: {
...result.response.content,
_sha1: o.sha1,
}
},
};
break;
}
let override = snapshot.resourceOverrides.find(o => o.url === url);
if (override?.ref) {
// "ref" means use the same content as "ref" snapshots ago.
const index = this._index - override.ref;
if (index >= 0 && index < this._snapshots.length)
override = this._snapshots[index].resourceOverrides.find(o => o.url === url);
}
if (override?.sha1) {
result = {
...result,
response: {
...result.response,
content: {
...result.response.content,
_sha1: override.sha1,
}
},
};
}
}

Expand Down
8 changes: 6 additions & 2 deletions packages/playwright/src/worker/fixtureRunner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,10 +55,13 @@ class Fixture {
title,
phase: 'setup',
location,
slot: this.registration.timeout === undefined ? undefined : {
slot: this.registration.timeout !== undefined ? {
timeout: this.registration.timeout,
elapsed: 0,
}
} : this.registration.scope === 'worker' ? {
timeout: this.runner.workerFixtureTimeout,
elapsed: 0,
} : undefined,
};
this._teardownDescription = { ...this._setupDescription, phase: 'teardown' };
}
Expand Down Expand Up @@ -179,6 +182,7 @@ export class FixtureRunner {
private testScopeClean = true;
pool: FixturePool | undefined;
instanceForId = new Map<string, Fixture>();
workerFixtureTimeout = 0;

setPool(pool: FixturePool) {
if (!this.testScopeClean)
Expand Down
1 change: 1 addition & 0 deletions packages/playwright/src/worker/workerMain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,7 @@ export class WorkerMain extends ProcessRunner {
this._config = config;
this._project = project;
this._poolBuilder = PoolBuilder.createForWorker(this._project);
this._fixtureRunner.workerFixtureTimeout = this._project.project.timeout;
}

async runTestGroup(runPayload: ipc.RunPayload) {
Expand Down
7 changes: 7 additions & 0 deletions tests/library/trace-viewer.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2178,6 +2178,8 @@ test('should respect CSSOM changes', async ({ runAndTrace, page, server }) => {
await page.goto(server.EMPTY_PAGE);
await page.setContent('<link rel="stylesheet" href="style.css"><button>Hello</button>');
await page.evaluate(() => { (document.styleSheets[0].cssRules[0] as any).style.color = 'blue'; });
await page.evaluate(() => '1 + 1');
await page.evaluate(() => { document.styleSheets[0].disabled = true; });
});

const frame1 = await traceViewer.snapshotFrame('Set content', 0);
Expand All @@ -2196,6 +2198,11 @@ test('should respect CSSOM changes', async ({ runAndTrace, page, server }) => {
await expect(frame6.locator('button')).toHaveCSS('color', 'rgb(255, 0, 0)');
const frame7 = await traceViewer.snapshotFrame('Evaluate', 4);
await expect(frame7.locator('button')).toHaveCSS('color', 'rgb(0, 0, 255)');
const frame8 = await traceViewer.snapshotFrame('Evaluate', 5);
await traceViewer.page.waitForTimeout(1000); // give it time to render wrong color
await expect(frame8.locator('button')).toHaveCSS('color', 'rgb(0, 0, 255)');
const frame9 = await traceViewer.snapshotFrame('Evaluate', 6);
await expect(frame9.locator('button')).toHaveCSS('color', 'rgb(0, 0, 0)');
});

test('should preserve custom doctype', async ({ runAndTrace, page }) => {
Expand Down
8 changes: 4 additions & 4 deletions tests/playwright-test/fixture-errors.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,18 +46,18 @@ test('should handle worker fixture timeout', async ({ runInlineTest }) => {
'a.spec.ts': `
import { test as base, expect } from '@playwright/test';
const test = base.extend({
timeout: [async ({}, runTest) => {
slowFixture: [async ({}, runTest) => {
await runTest();
await new Promise(f => setTimeout(f, 100000));
}, { scope: 'worker' }]
});

test('fails', async ({timeout}) => {
test('fails', async ({ slowFixture }) => {
});
`
}, { timeout: 500 });
expect(result.exitCode).toBe(1);
expect(result.output).toContain('Worker teardown timeout of 500ms exceeded while tearing down "timeout".');
expect(result.output).toContain('Fixture "slowFixture" timeout of 500ms exceeded during teardown.');
});

test('should handle worker fixture error', async ({ runInlineTest }) => {
Expand Down Expand Up @@ -607,7 +607,7 @@ test('should report worker fixture teardown with debug info', async ({ runInline
expect(result.exitCode).toBe(1);
expect(result.passed).toBe(20);
expect(result.output).toContain([
'Worker teardown timeout of 1000ms exceeded while tearing down "fixture".',
'Fixture "fixture" timeout of 1000ms exceeded during teardown.',
'',
'Failed worker ran 20 tests, last 10 tests were:',
'a.spec.ts:10:9 › good10',
Expand Down
2 changes: 1 addition & 1 deletion tests/playwright-test/reporter-html.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3357,7 +3357,7 @@ for (const useIntermediateMergeReport of [true, false] as const) {
- text: Use shard weights to
- link "rebalance your shards":
- /url: https://playwright.dev/docs/test-sharding#rebalancing-shards
- text: ": @linux: npx playwright test --shard-weights=67:33:0 @mac: npx playwright test --shard-weights=67:33"
- text: /@linux. npx playwright test --shard-weights=\\d+:\\d+:\\d+ @mac. npx playwright test --shard-weights=\\d+:\\d+/
`);
});
});
Expand Down
38 changes: 37 additions & 1 deletion tests/playwright-test/timeout.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -532,7 +532,7 @@ test('should report up to 3 timeout errors', async ({ runInlineTest }) => {
expect(result.failed).toBe(1);
expect(result.output).toContain('Test timeout of 1000ms exceeded.');
expect(result.output).toContain('Test timeout of 1000ms exceeded while running "afterEach" hook.');
expect(result.output).toContain('Worker teardown timeout of 1000ms exceeded while tearing down "autoWorker".');
expect(result.output).toContain('Fixture "autoWorker" timeout of 1000ms exceeded during teardown.');
});

test('should complain when worker fixture times out during worker cleanup', async ({ runInlineTest }) => {
Expand Down Expand Up @@ -668,3 +668,39 @@ test('test.setTimeout should be able to change custom fixture timeout', async ({
expect(result.failed).toBe(1);
expect(result.output).toContain(`Fixture "foo" timeout of 100ms exceeded during setup`);
});

test('worker fixtures should each have a separate time slot', async ({ runInlineTest }) => {
const result = await runInlineTest({
'a.spec.ts': `
import { test as base, expect } from '@playwright/test';
const test = base.extend({
worker: [async ({}, use) => {
console.log('%%before worker');
await new Promise(f => setTimeout(f, 3000));
await use('hey');
console.log('%%after worker');
}, { scope: 'worker' }],
auto: [async ({}, use) => {
console.log('%%before auto');
await new Promise(f => setTimeout(f, 3000));
await use('hey');
console.log('%%after auto');
}, { scope: 'worker', auto: true }],
});
test('passes', async ({ worker }) => {
console.log('%%before test');
await new Promise(f => setTimeout(f, 3000));
console.log('%%after test');
});
`
}, { timeout: 5000 });
expect(result.exitCode).toBe(0);
expect(result.outputLines).toEqual([
'before auto',
'before worker',
'before test',
'after test',
'after worker',
'after auto',
]);
});
Loading