Revert "chore: move artifacts recording to TestLifecycleInstrumentation (#30935)" (#31686)

This reverts commit ba5b460444.
This commit is contained in:
Dmitry Gozman 2024-07-15 07:01:51 -07:00 committed by GitHub
parent 1686e5174d
commit 6ee8f1de2d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 91 additions and 244 deletions

View file

@ -34,8 +34,8 @@ export class Tracing extends ChannelOwner<channels.TracingChannel> implements ap
} }
async start(options: { name?: string, title?: string, snapshots?: boolean, screenshots?: boolean, sources?: boolean, _live?: boolean } = {}) { async start(options: { name?: string, title?: string, snapshots?: boolean, screenshots?: boolean, sources?: boolean, _live?: boolean } = {}) {
await this._wrapApiCall(async () => { this._includeSources = !!options.sources;
this._includeSources = !!options.sources; const traceName = await this._wrapApiCall(async () => {
await this._channel.tracingStart({ await this._channel.tracingStart({
name: options.name, name: options.name,
snapshots: options.snapshots, snapshots: options.snapshots,
@ -43,15 +43,14 @@ export class Tracing extends ChannelOwner<channels.TracingChannel> implements ap
live: options._live, live: options._live,
}); });
const response = await this._channel.tracingStartChunk({ name: options.name, title: options.title }); const response = await this._channel.tracingStartChunk({ name: options.name, title: options.title });
await this._startCollectingStacks(response.traceName); return response.traceName;
}, true); }, true);
await this._startCollectingStacks(traceName);
} }
async startChunk(options: { name?: string, title?: string } = {}) { async startChunk(options: { name?: string, title?: string } = {}) {
await this._wrapApiCall(async () => { const { traceName } = await this._channel.tracingStartChunk(options);
const { traceName } = await this._channel.tracingStartChunk(options); await this._startCollectingStacks(traceName);
await this._startCollectingStacks(traceName);
}, true);
} }
private async _startCollectingStacks(traceName: string) { private async _startCollectingStacks(traceName: string) {

View file

@ -42,19 +42,3 @@ export function setIsWorkerProcess() {
export function isWorkerProcess() { export function isWorkerProcess() {
return _isWorkerProcess; return _isWorkerProcess;
} }
export interface TestLifecycleInstrumentation {
onTestBegin?(): Promise<void>;
onTestFunctionEnd?(): Promise<void>;
onTestEnd?(): Promise<void>;
}
let _testLifecycleInstrumentation: TestLifecycleInstrumentation | undefined;
export function setTestLifecycleInstrumentation(instrumentation: TestLifecycleInstrumentation | undefined) {
_testLifecycleInstrumentation = instrumentation;
}
export function testLifecycleInstrumentation() {
return _testLifecycleInstrumentation;
}

View file

@ -16,14 +16,15 @@
import * as fs from 'fs'; import * as fs from 'fs';
import * as path from 'path'; import * as path from 'path';
import type { APIRequestContext, BrowserContext, Browser, BrowserContextOptions, LaunchOptions, Page, Tracing, Video, PageScreenshotOptions } from 'playwright-core'; import type { APIRequestContext, BrowserContext, Browser, BrowserContextOptions, LaunchOptions, Page, Tracing, Video } from 'playwright-core';
import * as playwrightLibrary from 'playwright-core';
import { createGuid, debugMode, addInternalStackPrefix, isString, asLocator, jsonStringifyForceASCII } from 'playwright-core/lib/utils'; import { createGuid, debugMode, addInternalStackPrefix, isString, asLocator, jsonStringifyForceASCII } from 'playwright-core/lib/utils';
import type { Fixtures, PlaywrightTestArgs, PlaywrightTestOptions, PlaywrightWorkerArgs, PlaywrightWorkerOptions, ScreenshotMode, TestInfo, TestType, VideoMode } from '../types/test'; import type { Fixtures, PlaywrightTestArgs, PlaywrightTestOptions, PlaywrightWorkerArgs, PlaywrightWorkerOptions, ScreenshotMode, TestInfo, TestType, VideoMode } from '../types/test';
import type { TestInfoImpl } from './worker/testInfo'; import type { TestInfoImpl } from './worker/testInfo';
import { rootTestType } from './common/testType'; import { rootTestType } from './common/testType';
import type { ContextReuseMode } from './common/config'; import type { ContextReuseMode } from './common/config';
import type { ClientInstrumentation, ClientInstrumentationListener } from '../../playwright-core/src/client/clientInstrumentation'; import type { ClientInstrumentation, ClientInstrumentationListener } from '../../playwright-core/src/client/clientInstrumentation';
import { currentTestInfo, setTestLifecycleInstrumentation, type TestLifecycleInstrumentation } from './common/globals'; import { currentTestInfo } from './common/globals';
export { expect } from './matchers/expect'; export { expect } from './matchers/expect';
export const _baseTest: TestType<{}, {}> = rootTestType.test; export const _baseTest: TestType<{}, {}> = rootTestType.test;
@ -44,12 +45,11 @@ if ((process as any)['__pw_initiator__']) {
type TestFixtures = PlaywrightTestArgs & PlaywrightTestOptions & { type TestFixtures = PlaywrightTestArgs & PlaywrightTestOptions & {
_combinedContextOptions: BrowserContextOptions, _combinedContextOptions: BrowserContextOptions,
_setupContextOptions: void; _setupContextOptions: void;
_setupArtifacts: void;
_contextFactory: (options?: BrowserContextOptions) => Promise<BrowserContext>; _contextFactory: (options?: BrowserContextOptions) => Promise<BrowserContext>;
}; };
type WorkerFixtures = PlaywrightWorkerArgs & PlaywrightWorkerOptions & { type WorkerFixtures = PlaywrightWorkerArgs & PlaywrightWorkerOptions & {
// Same as "playwright", but exposed so that our internal tests can override it.
_playwrightImpl: PlaywrightWorkerArgs['playwright'];
_browserOptions: LaunchOptions; _browserOptions: LaunchOptions;
_optionContextReuseMode: ContextReuseMode, _optionContextReuseMode: ContextReuseMode,
_optionConnectOptions: PlaywrightWorkerOptions['connectOptions'], _optionConnectOptions: PlaywrightWorkerOptions['connectOptions'],
@ -59,14 +59,9 @@ type WorkerFixtures = PlaywrightWorkerArgs & PlaywrightWorkerOptions & {
const playwrightFixtures: Fixtures<TestFixtures, WorkerFixtures> = ({ const playwrightFixtures: Fixtures<TestFixtures, WorkerFixtures> = ({
defaultBrowserType: ['chromium', { scope: 'worker', option: true }], defaultBrowserType: ['chromium', { scope: 'worker', option: true }],
browserName: [({ defaultBrowserType }, use) => use(defaultBrowserType), { scope: 'worker', option: true }], browserName: [({ defaultBrowserType }, use) => use(defaultBrowserType), { scope: 'worker', option: true }],
_playwrightImpl: [({}, use) => use(require('playwright-core')), { scope: 'worker', box: true }], playwright: [async ({}, use) => {
await use(require('playwright-core'));
playwright: [async ({ _playwrightImpl, screenshot }, use) => {
await connector.setPlaywright(_playwrightImpl, screenshot);
await use(_playwrightImpl);
await connector.setPlaywright(undefined, screenshot);
}, { scope: 'worker', box: true }], }, { scope: 'worker', box: true }],
headless: [({ launchOptions }, use) => use(launchOptions.headless ?? true), { scope: 'worker', option: true }], headless: [({ launchOptions }, use) => use(launchOptions.headless ?? true), { scope: 'worker', option: true }],
channel: [({ launchOptions }, use) => use(launchOptions.channel), { scope: 'worker', option: true }], channel: [({ launchOptions }, use) => use(launchOptions.channel), { scope: 'worker', option: true }],
launchOptions: [{}, { scope: 'worker', option: true }], launchOptions: [{}, { scope: 'worker', option: true }],
@ -231,7 +226,7 @@ const playwrightFixtures: Fixtures<TestFixtures, WorkerFixtures> = ({
_setupContextOptions: [async ({ playwright, _combinedContextOptions, actionTimeout, navigationTimeout, testIdAttribute }, use, testInfo) => { _setupContextOptions: [async ({ playwright, _combinedContextOptions, actionTimeout, navigationTimeout, testIdAttribute }, use, testInfo) => {
if (testIdAttribute) if (testIdAttribute)
playwright.selectors.setTestIdAttribute(testIdAttribute); playwrightLibrary.selectors.setTestIdAttribute(testIdAttribute);
testInfo.snapshotSuffix = process.platform; testInfo.snapshotSuffix = process.platform;
if (debugMode()) if (debugMode())
testInfo.setTimeout(0); testInfo.setTimeout(0);
@ -252,6 +247,58 @@ const playwrightFixtures: Fixtures<TestFixtures, WorkerFixtures> = ({
} }
}, { auto: 'all-hooks-included', title: 'context configuration', box: true } as any], }, { auto: 'all-hooks-included', title: 'context configuration', box: true } as any],
_setupArtifacts: [async ({ playwright, screenshot }, use, testInfo) => {
const artifactsRecorder = new ArtifactsRecorder(playwright, tracing().artifactsDir(), screenshot);
await artifactsRecorder.willStartTest(testInfo as TestInfoImpl);
const csiListener: ClientInstrumentationListener = {
onApiCallBegin: (apiName: string, params: Record<string, any>, frames: StackFrame[], userData: any, out: { stepId?: string }) => {
const testInfo = currentTestInfo();
if (!testInfo || apiName.includes('setTestIdAttribute'))
return { userObject: null };
const step = testInfo._addStep({
location: frames[0] as any,
category: 'pw:api',
title: renderApiCall(apiName, params),
apiName,
params,
});
userData.userObject = step;
out.stepId = step.stepId;
},
onApiCallEnd: (userData: any, error?: Error) => {
const step = userData.userObject;
step?.complete({ error });
},
onWillPause: () => {
currentTestInfo()?.setTimeout(0);
},
runAfterCreateBrowserContext: async (context: BrowserContext) => {
await artifactsRecorder?.didCreateBrowserContext(context);
const testInfo = currentTestInfo();
if (testInfo)
attachConnectedHeaderIfNeeded(testInfo, context.browser());
},
runAfterCreateRequestContext: async (context: APIRequestContext) => {
await artifactsRecorder?.didCreateRequestContext(context);
},
runBeforeCloseBrowserContext: async (context: BrowserContext) => {
await artifactsRecorder?.willCloseBrowserContext(context);
},
runBeforeCloseRequestContext: async (context: APIRequestContext) => {
await artifactsRecorder?.willCloseRequestContext(context);
},
};
const clientInstrumentation = (playwright as any)._instrumentation as ClientInstrumentation;
clientInstrumentation.addListener(csiListener);
await use();
clientInstrumentation.removeListener(csiListener);
await artifactsRecorder.didFinishTest();
}, { auto: 'all-hooks-included', title: 'trace recording', box: true } as any],
_contextFactory: [async ({ browser, video, _reuseContext, _combinedContextOptions /** mitigate dep-via-auto lack of traceability */ }, use, testInfo) => { _contextFactory: [async ({ browser, video, _reuseContext, _combinedContextOptions /** mitigate dep-via-auto lack of traceability */ }, use, testInfo) => {
const testInfoImpl = testInfo as TestInfoImpl; const testInfoImpl = testInfo as TestInfoImpl;
const videoMode = normalizeVideoMode(video); const videoMode = normalizeVideoMode(video);
@ -462,7 +509,7 @@ class ArtifactsRecorder {
private _playwright: Playwright; private _playwright: Playwright;
private _artifactsDir: string; private _artifactsDir: string;
private _screenshotMode: ScreenshotMode; private _screenshotMode: ScreenshotMode;
private _screenshotOptions: { mode: ScreenshotMode } & Pick<PageScreenshotOptions, 'fullPage' | 'omitBackground'> | undefined; private _screenshotOptions: { mode: ScreenshotMode } & Pick<playwrightLibrary.PageScreenshotOptions, 'fullPage' | 'omitBackground'> | undefined;
private _temporaryScreenshots: string[] = []; private _temporaryScreenshots: string[] = [];
private _temporaryArtifacts: string[] = []; private _temporaryArtifacts: string[] = [];
private _reusedContexts = new Set<BrowserContext>(); private _reusedContexts = new Set<BrowserContext>();
@ -487,6 +534,7 @@ class ArtifactsRecorder {
async willStartTest(testInfo: TestInfoImpl) { async willStartTest(testInfo: TestInfoImpl) {
this._testInfo = testInfo; this._testInfo = testInfo;
testInfo._onDidFinishTestFunction = () => this.didFinishTestFunction();
// Since beforeAll(s), test and afterAll(s) reuse the same TestInfo, make sure we do not // Since beforeAll(s), test and afterAll(s) reuse the same TestInfo, make sure we do not
// overwrite previous screenshots. // overwrite previous screenshots.
@ -668,101 +716,6 @@ function tracing() {
return (test.info() as TestInfoImpl)._tracing; return (test.info() as TestInfoImpl)._tracing;
} }
class InstrumentationConnector implements TestLifecycleInstrumentation, ClientInstrumentationListener {
private _playwright: PlaywrightWorkerArgs['playwright'] | undefined;
private _screenshot: ScreenshotOption = 'off';
private _artifactsRecorder: ArtifactsRecorder | undefined;
private _testIsRunning = false;
constructor() {
setTestLifecycleInstrumentation(this);
}
async setPlaywright(playwright: PlaywrightWorkerArgs['playwright'] | undefined, screenshot: ScreenshotOption) {
if (this._playwright) {
if (this._testIsRunning) {
// When "playwright" is destroyed during a test, collect artifacts immediately.
await this.onTestEnd();
}
const clientInstrumentation = (this._playwright as any)._instrumentation as ClientInstrumentation;
clientInstrumentation.removeListener(this);
}
this._playwright = playwright;
this._screenshot = screenshot;
if (this._playwright) {
const clientInstrumentation = (this._playwright as any)._instrumentation as ClientInstrumentation;
clientInstrumentation.addListener(this);
if (this._testIsRunning) {
// When "playwright" is created during a test, wire it up immediately.
await this.onTestBegin();
}
}
}
async onTestBegin() {
this._testIsRunning = true;
if (this._playwright) {
this._artifactsRecorder = new ArtifactsRecorder(this._playwright, tracing().artifactsDir(), this._screenshot);
await this._artifactsRecorder.willStartTest(currentTestInfo() as TestInfoImpl);
}
}
async onTestFunctionEnd() {
await this._artifactsRecorder?.didFinishTestFunction();
}
async onTestEnd() {
await this._artifactsRecorder?.didFinishTest();
this._artifactsRecorder = undefined;
this._testIsRunning = false;
}
onApiCallBegin(apiName: string, params: Record<string, any>, frames: StackFrame[], userData: any, out: { stepId?: string }) {
const testInfo = currentTestInfo();
if (!testInfo || apiName.includes('setTestIdAttribute'))
return { userObject: null };
const step = testInfo._addStep({
location: frames[0] as any,
category: 'pw:api',
title: renderApiCall(apiName, params),
apiName,
params,
});
userData.userObject = step;
out.stepId = step.stepId;
}
onApiCallEnd(userData: any, error?: Error) {
const step = userData.userObject;
step?.complete({ error });
}
onWillPause() {
currentTestInfo()?.setTimeout(0);
}
async runAfterCreateBrowserContext(context: BrowserContext) {
await this._artifactsRecorder?.didCreateBrowserContext(context);
const testInfo = currentTestInfo();
if (testInfo)
attachConnectedHeaderIfNeeded(testInfo, context.browser());
}
async runAfterCreateRequestContext(context: APIRequestContext) {
await this._artifactsRecorder?.didCreateRequestContext(context);
}
async runBeforeCloseBrowserContext(context: BrowserContext) {
await this._artifactsRecorder?.willCloseBrowserContext(context);
}
async runBeforeCloseRequestContext(context: APIRequestContext) {
await this._artifactsRecorder?.willCloseRequestContext(context);
}
}
const connector = new InstrumentationConnector();
export const test = _baseTest.extend<TestFixtures, WorkerFixtures>(playwrightFixtures); export const test = _baseTest.extend<TestFixtures, WorkerFixtures>(playwrightFixtures);
export { defineConfig } from './common/configLoader'; export { defineConfig } from './common/configLoader';

View file

@ -68,6 +68,7 @@ export class TestInfoImpl implements TestInfo {
readonly _projectInternal: FullProjectInternal; readonly _projectInternal: FullProjectInternal;
readonly _configInternal: FullConfigInternal; readonly _configInternal: FullConfigInternal;
private readonly _steps: TestStepInternal[] = []; private readonly _steps: TestStepInternal[] = [];
_onDidFinishTestFunction: (() => Promise<void>) | undefined;
private readonly _stages: TestStage[] = []; private readonly _stages: TestStage[] = [];
_hasNonRetriableError = false; _hasNonRetriableError = false;
_hasUnhandledError = false; _hasUnhandledError = false;

View file

@ -17,7 +17,7 @@
import { colors } from 'playwright-core/lib/utilsBundle'; import { colors } from 'playwright-core/lib/utilsBundle';
import { debugTest, relativeFilePath, serializeError } from '../util'; import { debugTest, relativeFilePath, serializeError } from '../util';
import { type TestBeginPayload, type TestEndPayload, type RunPayload, type DonePayload, type WorkerInitParams, type TeardownErrorsPayload, stdioChunkToParams } from '../common/ipc'; import { type TestBeginPayload, type TestEndPayload, type RunPayload, type DonePayload, type WorkerInitParams, type TeardownErrorsPayload, stdioChunkToParams } from '../common/ipc';
import { setCurrentTestInfo, setIsWorkerProcess, testLifecycleInstrumentation } from '../common/globals'; import { setCurrentTestInfo, setIsWorkerProcess } from '../common/globals';
import { deserializeConfig } from '../common/configLoader'; import { deserializeConfig } from '../common/configLoader';
import type { Suite, TestCase } from '../common/test'; import type { Suite, TestCase } from '../common/test';
import type { Annotation, FullConfigInternal, FullProjectInternal } from '../common/config'; import type { Annotation, FullConfigInternal, FullProjectInternal } from '../common/config';
@ -304,11 +304,10 @@ export class WorkerMain extends ProcessRunner {
if (this._lastRunningTests.length > 10) if (this._lastRunningTests.length > 10)
this._lastRunningTests.shift(); this._lastRunningTests.shift();
let shouldRunAfterEachHooks = false; let shouldRunAfterEachHooks = false;
const tracingSlot = { timeout: this._project.project.timeout, elapsed: 0 };
testInfo._allowSkips = true; testInfo._allowSkips = true;
await testInfo._runAsStage({ title: 'setup and test' }, async () => { await testInfo._runAsStage({ title: 'setup and test' }, async () => {
await testInfo._runAsStage({ title: 'start tracing', runnable: { type: 'test', slot: tracingSlot } }, async () => { await testInfo._runAsStage({ title: 'start tracing', runnable: { type: 'test' } }, async () => {
// Ideally, "trace" would be an config-level option belonging to the // Ideally, "trace" would be an config-level option belonging to the
// test runner instead of a fixture belonging to Playwright. // test runner instead of a fixture belonging to Playwright.
// However, for backwards compatibility, we have to read it from a fixture today. // However, for backwards compatibility, we have to read it from a fixture today.
@ -319,7 +318,6 @@ export class WorkerMain extends ProcessRunner {
if (typeof traceFixtureRegistration.fn === 'function') if (typeof traceFixtureRegistration.fn === 'function')
throw new Error(`"trace" option cannot be a function`); throw new Error(`"trace" option cannot be a function`);
await testInfo._tracing.startIfNeeded(traceFixtureRegistration.fn); await testInfo._tracing.startIfNeeded(traceFixtureRegistration.fn);
await testLifecycleInstrumentation()?.onTestBegin?.();
}); });
if (this._isStopped || isSkipped) { if (this._isStopped || isSkipped) {
@ -374,10 +372,10 @@ export class WorkerMain extends ProcessRunner {
try { try {
// Run "immediately upon test function finish" callback. // Run "immediately upon test function finish" callback.
await testInfo._runAsStage({ title: 'on-test-function-finish', runnable: { type: 'test', slot: tracingSlot } }, async () => { await testInfo._runAsStage({ title: 'on-test-function-finish', runnable: { type: 'test', slot: afterHooksSlot } }, async () => testInfo._onDidFinishTestFunction?.());
await testLifecycleInstrumentation()?.onTestFunctionEnd?.();
});
} catch (error) { } catch (error) {
if (error instanceof TimeoutManagerError)
didTimeoutInAfterHooks = true;
firstAfterHooksError = firstAfterHooksError ?? error; firstAfterHooksError = firstAfterHooksError ?? error;
} }
@ -460,8 +458,8 @@ export class WorkerMain extends ProcessRunner {
}).catch(() => {}); // Ignore the top-level error, it is already inside TestInfo.errors. }).catch(() => {}); // Ignore the top-level error, it is already inside TestInfo.errors.
} }
const tracingSlot = { timeout: this._project.project.timeout, elapsed: 0 };
await testInfo._runAsStage({ title: 'stop tracing', runnable: { type: 'test', slot: tracingSlot } }, async () => { await testInfo._runAsStage({ title: 'stop tracing', runnable: { type: 'test', slot: tracingSlot } }, async () => {
await testLifecycleInstrumentation()?.onTestEnd?.();
await testInfo._tracing.stopIfNeeded(); await testInfo._tracing.stopIfNeeded();
}).catch(() => {}); // Ignore the top-level error, it is already inside TestInfo.errors. }).catch(() => {}); // Ignore the top-level error, it is already inside TestInfo.errors.

View file

@ -30,12 +30,11 @@ export type TestModeTestFixtures = {
export type TestModeWorkerFixtures = { export type TestModeWorkerFixtures = {
toImplInWorkerScope: (rpcObject?: any) => any; toImplInWorkerScope: (rpcObject?: any) => any;
playwright: typeof import('@playwright/test'); playwright: typeof import('@playwright/test');
_playwrightImpl: typeof import('@playwright/test');
}; };
export const testModeTest = test.extend<TestModeTestFixtures, TestModeWorkerOptions & TestModeWorkerFixtures>({ export const testModeTest = test.extend<TestModeTestFixtures, TestModeWorkerOptions & TestModeWorkerFixtures>({
mode: ['default', { scope: 'worker', option: true }], mode: ['default', { scope: 'worker', option: true }],
_playwrightImpl: [async ({ mode }, run) => { playwright: [async ({ mode }, run) => {
const testMode = { const testMode = {
'default': new DefaultTestMode(), 'default': new DefaultTestMode(),
'service': new DefaultTestMode(), 'service': new DefaultTestMode(),

View file

@ -151,8 +151,10 @@ test('should work with screenshot: on', async ({ runInlineTest }, testInfo) => {
' test-finished-1.png', ' test-finished-1.png',
'artifacts-shared-shared-failing', 'artifacts-shared-shared-failing',
' test-failed-1.png', ' test-failed-1.png',
' test-failed-2.png',
'artifacts-shared-shared-passing', 'artifacts-shared-shared-passing',
' test-finished-1.png', ' test-finished-1.png',
' test-finished-2.png',
'artifacts-two-contexts', 'artifacts-two-contexts',
' test-finished-1.png', ' test-finished-1.png',
' test-finished-2.png', ' test-finished-2.png',
@ -183,6 +185,7 @@ test('should work with screenshot: only-on-failure', async ({ runInlineTest }, t
' test-failed-1.png', ' test-failed-1.png',
'artifacts-shared-shared-failing', 'artifacts-shared-shared-failing',
' test-failed-1.png', ' test-failed-1.png',
' test-failed-2.png',
'artifacts-two-contexts-failing', 'artifacts-two-contexts-failing',
' test-failed-1.png', ' test-failed-1.png',
' test-failed-2.png', ' test-failed-2.png',

View file

@ -569,19 +569,14 @@ test('should opt out of attachments', async ({ runInlineTest, server }, testInfo
expect([...trace.resources.keys()].filter(f => f.startsWith('resources/'))).toHaveLength(0); expect([...trace.resources.keys()].filter(f => f.startsWith('resources/'))).toHaveLength(0);
}); });
test('should record with custom page fixture that closes the context', async ({ runInlineTest }, testInfo) => { test('should record with custom page fixture', async ({ runInlineTest }, testInfo) => {
// Note that original issue did not close the context, but we do not support such usecase.
test.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/23220' });
const result = await runInlineTest({ const result = await runInlineTest({
'a.spec.ts': ` 'a.spec.ts': `
import { test as base, expect } from '@playwright/test'; import { test as base, expect } from '@playwright/test';
const test = base.extend({ const test = base.extend({
myPage: async ({ browser }, use) => { myPage: async ({ browser }, use) => {
const page = await browser.newPage(); await use(await browser.newPage());
await use(page);
await page.close();
}, },
}); });
@ -1151,120 +1146,35 @@ test('should not corrupt actions when no library trace is present', async ({ run
]); ]);
}); });
test('should record trace in workerStorageState', async ({ runInlineTest }) => { test('should record trace for manually created context in a failed test', async ({ runInlineTest }) => {
test.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/30287' }); test.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/31541' });
const result = await runInlineTest({ const result = await runInlineTest({
'a.spec.ts': ` 'a.spec.ts': `
import { test as base, expect } from '@playwright/test'; import { test, expect } from '@playwright/test';
const test = base.extend({ test('fail', async ({ browser }) => {
storageState: ({ workerStorageState }, use) => use(workerStorageState), const page = await browser.newPage();
workerStorageState: [async ({ browser }, use) => { await page.setContent('<script>console.log("from the page");</script>');
const page = await browser.newPage({ storageState: undefined }); expect(1).toBe(2);
await page.setContent('<div>hello</div>');
await page.close();
await use(undefined);
}, { scope: 'worker' }],
})
test('pass', async ({ page }) => {
await page.goto('data:text/html,<div>hi</div>');
}); });
`, `,
}, { trace: 'on' }); }, { trace: 'on' });
expect(result.exitCode).toBe(0);
expect(result.passed).toBe(1);
const tracePath = test.info().outputPath('test-results', 'a-pass', 'trace.zip');
const trace = await parseTrace(tracePath);
expect(trace.actionTree).toEqual([
'Before Hooks',
' fixture: browser',
' browserType.launch',
' fixture: workerStorageState',
' browser.newPage',
' page.setContent',
' page.close',
' fixture: context',
' browser.newContext',
' fixture: page',
' browserContext.newPage',
'page.goto',
'After Hooks',
' fixture: page',
' fixture: context',
]);
});
test('should record trace after fixture teardown timeout', async ({ runInlineTest }) => {
test.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/30718' });
const result = await runInlineTest({
'a.spec.ts': `
import { test as base, expect } from '@playwright/test';
const test = base.extend({
fixture: async ({}, use) => {
await use('foo');
await new Promise(() => {});
},
})
test('fails', async ({ fixture, page }) => {
await page.evaluate(() => console.log('from the page'));
});
`,
}, { trace: 'on', timeout: '4000' });
expect(result.exitCode).toBe(1); expect(result.exitCode).toBe(1);
expect(result.failed).toBe(1); expect(result.failed).toBe(1);
const tracePath = test.info().outputPath('test-results', 'a-fails', 'trace.zip'); const tracePath = test.info().outputPath('test-results', 'a-fail', 'trace.zip');
const trace = await parseTrace(tracePath); const trace = await parseTrace(tracePath);
expect(trace.actionTree).toEqual([ expect(trace.actionTree).toEqual([
'Before Hooks', 'Before Hooks',
' fixture: fixture',
' fixture: browser', ' fixture: browser',
' browserType.launch', ' browserType.launch',
' fixture: context', 'browser.newPage',
' browser.newContext', 'page.setContent',
' fixture: page', 'expect.toBe',
' browserContext.newPage',
'page.evaluate',
'After Hooks', 'After Hooks',
' fixture: page',
' fixture: context',
' fixture: fixture',
'Worker Cleanup', 'Worker Cleanup',
' fixture: browser', ' fixture: browser',
]); ]);
// Check console events to make sure that library trace is recorded. // Check console events to make sure that library trace is recorded.
expect(trace.events).toContainEqual(expect.objectContaining({ type: 'console', text: 'from the page' })); expect(trace.events).toContainEqual(expect.objectContaining({ type: 'console', text: 'from the page' }));
}); });
test('should take a screenshot-on-failure in workerStorageState', async ({ runInlineTest }) => {
test.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/30959' });
const result = await runInlineTest({
'playwright.config.ts': `
export default {
use: {
screenshot: 'only-on-failure',
},
};
`,
'a.spec.ts': `
import { test as base, expect } from '@playwright/test';
const test = base.extend({
storageState: ({ workerStorageState }, use) => use(workerStorageState),
workerStorageState: [async ({ browser }, use) => {
const page = await browser.newPage({ storageState: undefined });
await page.setContent('hello world!');
throw new Error('Failed!');
await use(undefined);
}, { scope: 'worker' }],
})
test('fail', async ({ page }) => {
});
`,
});
expect(result.exitCode).toBe(1);
expect(result.failed).toBe(1);
expect(fs.existsSync(test.info().outputPath('test-results', 'a-fail', 'test-failed-1.png'))).toBeTruthy();
});