diff --git a/docs/src/test-fixtures-js.md b/docs/src/test-fixtures-js.md index 16afe6f25ead8..0646574ab7d40 100644 --- a/docs/src/test-fixtures-js.md +++ b/docs/src/test-fixtures-js.md @@ -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'; @@ -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'; @@ -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 diff --git a/packages/playwright-core/src/server/trace/recorder/snapshotterInjected.ts b/packages/playwright-core/src/server/trace/recorder/snapshotterInjected.ts index 86fa015eb3d9e..1a6effdceb058 100644 --- a/packages/playwright-core/src/server/trace/recorder/snapshotterInjected.ts +++ b/packages/playwright-core/src/server/trace/recorder/snapshotterInjected.ts @@ -86,6 +86,7 @@ export function frameSnapshotStreamer(snapshotStreamer: string, removeNoScript: class Streamer { private _lastSnapshotNumber = 0; private _staleStyleSheets = new Set(); + private _modifiedStyleSheets = new Set(); private _readingStyleSheet = false; // To avoid invalidating due to our own reads. private _fakeBase: HTMLBaseElement; private _observer: MutationObserver; @@ -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'); @@ -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; @@ -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 { @@ -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); @@ -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); diff --git a/packages/playwright-core/src/utils/isomorphic/trace/snapshotRenderer.ts b/packages/playwright-core/src/utils/isomorphic/trace/snapshotRenderer.ts index 64efaebb36281..fc8d420efb368 100644 --- a/packages/playwright-core/src/utils/isomorphic/trace/snapshotRenderer.ts +++ b/packages/playwright-core/src/utils/isomorphic/trace/snapshotRenderer.ts @@ -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, + } + }, + }; } } diff --git a/packages/playwright/src/worker/fixtureRunner.ts b/packages/playwright/src/worker/fixtureRunner.ts index 6329afaff7fb6..a8d43206cbb7c 100644 --- a/packages/playwright/src/worker/fixtureRunner.ts +++ b/packages/playwright/src/worker/fixtureRunner.ts @@ -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' }; } @@ -179,6 +182,7 @@ export class FixtureRunner { private testScopeClean = true; pool: FixturePool | undefined; instanceForId = new Map(); + workerFixtureTimeout = 0; setPool(pool: FixturePool) { if (!this.testScopeClean) diff --git a/packages/playwright/src/worker/workerMain.ts b/packages/playwright/src/worker/workerMain.ts index ea70b028a7638..d1289a46e5aec 100644 --- a/packages/playwright/src/worker/workerMain.ts +++ b/packages/playwright/src/worker/workerMain.ts @@ -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) { diff --git a/tests/library/trace-viewer.spec.ts b/tests/library/trace-viewer.spec.ts index 0dee62a814a7b..0e03f7fb1d094 100644 --- a/tests/library/trace-viewer.spec.ts +++ b/tests/library/trace-viewer.spec.ts @@ -2178,6 +2178,8 @@ test('should respect CSSOM changes', async ({ runAndTrace, page, server }) => { await page.goto(server.EMPTY_PAGE); await page.setContent(''); 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); @@ -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 }) => { diff --git a/tests/playwright-test/fixture-errors.spec.ts b/tests/playwright-test/fixture-errors.spec.ts index 51c928f156760..ca1c6d7177ec8 100644 --- a/tests/playwright-test/fixture-errors.spec.ts +++ b/tests/playwright-test/fixture-errors.spec.ts @@ -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 }) => { @@ -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', diff --git a/tests/playwright-test/reporter-html.spec.ts b/tests/playwright-test/reporter-html.spec.ts index 128e3c417e4c2..c3014f1102304 100644 --- a/tests/playwright-test/reporter-html.spec.ts +++ b/tests/playwright-test/reporter-html.spec.ts @@ -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+/ `); }); }); diff --git a/tests/playwright-test/timeout.spec.ts b/tests/playwright-test/timeout.spec.ts index 9d6001078d0c2..e1d41ef330fb7 100644 --- a/tests/playwright-test/timeout.spec.ts +++ b/tests/playwright-test/timeout.spec.ts @@ -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 }) => { @@ -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', + ]); +});