mirror of
https://github.com/microsoft/playwright.git
synced 2025-06-26 21:40:17 +00:00
chore: extract ArtifactsRecorder from built-in fixtures (#22717)
This commit is contained in:
parent
116fb349ce
commit
a1842cc708
@ -265,17 +265,8 @@ const playwrightFixtures: Fixtures<TestFixtures, WorkerFixtures> = ({
|
|||||||
}, { auto: 'all-hooks-included', _title: 'context configuration' } as any],
|
}, { auto: 'all-hooks-included', _title: 'context configuration' } as any],
|
||||||
|
|
||||||
_setupArtifacts: [async ({ playwright, _artifactsDir, trace, screenshot }, use, testInfo) => {
|
_setupArtifacts: [async ({ playwright, _artifactsDir, trace, screenshot }, use, testInfo) => {
|
||||||
const screenshotMode = normalizeScreenshotMode(screenshot);
|
const artifactsRecorder = new ArtifactsRecorder(playwright, _artifactsDir(), trace, screenshot);
|
||||||
const screenshotOptions = typeof screenshot === 'string' ? undefined : screenshot;
|
|
||||||
const traceMode = normalizeTraceMode(trace);
|
|
||||||
const defaultTraceOptions = { screenshots: true, snapshots: true, sources: true };
|
|
||||||
const traceOptions = typeof trace === 'string' ? defaultTraceOptions : { ...defaultTraceOptions, ...trace, mode: undefined };
|
|
||||||
const captureTrace = shouldCaptureTrace(traceMode, testInfo) && !process.env.PW_TEST_DISABLE_TRACING;
|
|
||||||
const temporaryTraceFiles: string[] = [];
|
|
||||||
const temporaryScreenshots: string[] = [];
|
|
||||||
const testInfoImpl = testInfo as TestInfoImpl;
|
const testInfoImpl = testInfo as TestInfoImpl;
|
||||||
const reusedContexts = new Set<BrowserContext>();
|
|
||||||
let traceOrdinal = 0;
|
|
||||||
|
|
||||||
const csiListener: ClientInstrumentationListener = {
|
const csiListener: ClientInstrumentationListener = {
|
||||||
onApiCallBegin: (apiCall: string, stackTrace: ParsedStackTrace | null, wallTime: number, userData: any) => {
|
onApiCallBegin: (apiCall: string, stackTrace: ParsedStackTrace | null, wallTime: number, userData: any) => {
|
||||||
@ -297,179 +288,31 @@ const playwrightFixtures: Fixtures<TestFixtures, WorkerFixtures> = ({
|
|||||||
testInfo.setTimeout(0);
|
testInfo.setTimeout(0);
|
||||||
},
|
},
|
||||||
onDidCreateBrowserContext: async (context: BrowserContext) => {
|
onDidCreateBrowserContext: async (context: BrowserContext) => {
|
||||||
await startTraceChunkOnContextCreation(context.tracing);
|
await artifactsRecorder.didCreateBrowserContext(context);
|
||||||
attachConnectedHeaderIfNeeded(testInfo, context.browser());
|
attachConnectedHeaderIfNeeded(testInfo, context.browser());
|
||||||
},
|
},
|
||||||
onDidCreateRequestContext: async (context: APIRequestContext) => {
|
onDidCreateRequestContext: async (context: APIRequestContext) => {
|
||||||
const tracing = (context as any)._tracing as Tracing;
|
await artifactsRecorder.didCreateRequestContext(context);
|
||||||
await startTraceChunkOnContextCreation(tracing);
|
|
||||||
},
|
},
|
||||||
onWillCloseBrowserContext: async (context: BrowserContext) => {
|
onWillCloseBrowserContext: async (context: BrowserContext) => {
|
||||||
// When reusing context, we get all previous contexts closed at the start of next test.
|
await artifactsRecorder.willCloseBrowserContext(context);
|
||||||
// Do not record empty traces and useless screenshots for them.
|
|
||||||
if (reusedContexts.has(context))
|
|
||||||
return;
|
|
||||||
await stopTracing(context.tracing, (context as any)[kStartedContextTearDown]);
|
|
||||||
if (screenshotMode === 'on' || screenshotMode === 'only-on-failure') {
|
|
||||||
// Capture screenshot for now. We'll know whether we have to preserve them
|
|
||||||
// after the test finishes.
|
|
||||||
await Promise.all(context.pages().map(screenshotPage));
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
onWillCloseRequestContext: async (context: APIRequestContext) => {
|
onWillCloseRequestContext: async (context: APIRequestContext) => {
|
||||||
const tracing = (context as any)._tracing as Tracing;
|
await artifactsRecorder.willCloseRequestContext(context);
|
||||||
await stopTracing(tracing, (context as any)[kStartedContextTearDown]);
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const startTraceChunkOnContextCreation = async (tracing: Tracing) => {
|
|
||||||
if (captureTrace) {
|
|
||||||
const title = [path.relative(testInfo.project.testDir, testInfo.file) + ':' + testInfo.line, ...testInfo.titlePath.slice(1)].join(' › ');
|
|
||||||
const ordinalSuffix = traceOrdinal ? `-${traceOrdinal}` : '';
|
|
||||||
++traceOrdinal;
|
|
||||||
const retrySuffix = testInfo.retry ? `-${testInfo.retry}` : '';
|
|
||||||
const name = `${testInfo.testId}${retrySuffix}${ordinalSuffix}`;
|
|
||||||
if (!(tracing as any)[kTracingStarted]) {
|
|
||||||
await tracing.start({ ...traceOptions, title, name });
|
|
||||||
(tracing as any)[kTracingStarted] = true;
|
|
||||||
} else {
|
|
||||||
await tracing.startChunk({ title, name });
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if ((tracing as any)[kTracingStarted]) {
|
|
||||||
(tracing as any)[kTracingStarted] = false;
|
|
||||||
await tracing.stop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const preserveTrace = () => {
|
|
||||||
const testFailed = testInfo.status !== testInfo.expectedStatus;
|
|
||||||
return captureTrace && (traceMode === 'on' || (testFailed && traceMode === 'retain-on-failure') || (traceMode === 'on-first-retry' && testInfo.retry === 1) || (traceMode === 'on-all-retries' && testInfo.retry > 0));
|
|
||||||
};
|
|
||||||
|
|
||||||
const startedCollectingArtifacts = Symbol('startedCollectingArtifacts');
|
|
||||||
const stopTracing = async (tracing: Tracing, contextTearDownStarted: boolean) => {
|
|
||||||
if ((tracing as any)[startedCollectingArtifacts])
|
|
||||||
return;
|
|
||||||
(tracing as any)[startedCollectingArtifacts] = true;
|
|
||||||
if (captureTrace) {
|
|
||||||
let tracePath;
|
|
||||||
// Create a trace file if we know that:
|
|
||||||
// - it is's going to be used due to the config setting and the test status or
|
|
||||||
// - we are inside a test or afterEach and the user manually closed the context.
|
|
||||||
if (preserveTrace() || !contextTearDownStarted) {
|
|
||||||
tracePath = path.join(_artifactsDir(), createGuid() + '.zip');
|
|
||||||
temporaryTraceFiles.push(tracePath);
|
|
||||||
}
|
|
||||||
await tracing.stopChunk({ path: tracePath });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const screenshottedSymbol = Symbol('screenshotted');
|
|
||||||
const screenshotPage = async (page: Page) => {
|
|
||||||
if ((page as any)[screenshottedSymbol])
|
|
||||||
return;
|
|
||||||
(page as any)[screenshottedSymbol] = true;
|
|
||||||
const screenshotPath = path.join(_artifactsDir(), createGuid() + '.png');
|
|
||||||
temporaryScreenshots.push(screenshotPath);
|
|
||||||
// Pass caret=initial to avoid any evaluations that might slow down the screenshot
|
|
||||||
// and let the page modify itself from the problematic state it had at the moment of failure.
|
|
||||||
await page.screenshot({ ...screenshotOptions, timeout: 5000, path: screenshotPath, caret: 'initial' }).catch(() => {});
|
|
||||||
};
|
|
||||||
|
|
||||||
const screenshotOnTestFailure = async () => {
|
|
||||||
const contexts: BrowserContext[] = [];
|
|
||||||
for (const browserType of [playwright.chromium, playwright.firefox, playwright.webkit])
|
|
||||||
contexts.push(...(browserType as any)._contexts);
|
|
||||||
await Promise.all(contexts.map(ctx => Promise.all(ctx.pages().map(screenshotPage))));
|
|
||||||
};
|
|
||||||
|
|
||||||
// 1. Setup instrumentation and process existing contexts.
|
// 1. Setup instrumentation and process existing contexts.
|
||||||
const instrumentation = (playwright as any)._instrumentation as ClientInstrumentation;
|
const instrumentation = (playwright as any)._instrumentation as ClientInstrumentation;
|
||||||
instrumentation.addListener(csiListener);
|
instrumentation.addListener(csiListener);
|
||||||
for (const browserType of [playwright.chromium, playwright.firefox, playwright.webkit]) {
|
await artifactsRecorder.testStarted(testInfoImpl);
|
||||||
const promises: (Promise<void> | undefined)[] = [];
|
|
||||||
const existingContexts = Array.from((browserType as any)._contexts) as BrowserContext[];
|
|
||||||
for (const context of existingContexts) {
|
|
||||||
if ((context as any)[kIsReusedContext])
|
|
||||||
reusedContexts.add(context);
|
|
||||||
else
|
|
||||||
promises.push(csiListener.onDidCreateBrowserContext?.(context as any));
|
|
||||||
}
|
|
||||||
await Promise.all(promises);
|
|
||||||
}
|
|
||||||
{
|
|
||||||
const existingApiRequests: APIRequestContext[] = Array.from((playwright.request as any)._contexts as Set<APIRequestContext>);
|
|
||||||
await Promise.all(existingApiRequests.map(c => csiListener.onDidCreateRequestContext?.(c as any)));
|
|
||||||
}
|
|
||||||
if (screenshotMode === 'on' || screenshotMode === 'only-on-failure')
|
|
||||||
testInfoImpl._onTestFailureImmediateCallbacks.set(screenshotOnTestFailure, 'Screenshot on failure');
|
|
||||||
|
|
||||||
// 2. Run the test.
|
// 2. Run the test.
|
||||||
await use();
|
await use();
|
||||||
|
|
||||||
// 3. Determine whether we need the artifacts.
|
// 3. Cleanup instrumentation.
|
||||||
const testFailed = testInfo.status !== testInfo.expectedStatus;
|
await artifactsRecorder.testFinished();
|
||||||
const captureScreenshots = screenshotMode === 'on' || (screenshotMode === 'only-on-failure' && testFailed);
|
|
||||||
|
|
||||||
const screenshotAttachments: string[] = [];
|
|
||||||
const addScreenshotAttachment = () => {
|
|
||||||
const screenshotPath = testInfo.outputPath(`test-${testFailed ? 'failed' : 'finished'}-${screenshotAttachments.length + 1}.png`);
|
|
||||||
screenshotAttachments.push(screenshotPath);
|
|
||||||
testInfo.attachments.push({ name: 'screenshot', path: screenshotPath, contentType: 'image/png' });
|
|
||||||
return screenshotPath;
|
|
||||||
};
|
|
||||||
|
|
||||||
// 4. Cleanup instrumentation.
|
|
||||||
instrumentation.removeListener(csiListener);
|
instrumentation.removeListener(csiListener);
|
||||||
|
|
||||||
const leftoverContexts: BrowserContext[] = [];
|
|
||||||
for (const browserType of [playwright.chromium, playwright.firefox, playwright.webkit])
|
|
||||||
leftoverContexts.push(...(browserType as any)._contexts);
|
|
||||||
const leftoverApiRequests: APIRequestContext[] = Array.from((playwright.request as any)._contexts as Set<APIRequestContext>);
|
|
||||||
testInfoImpl._onTestFailureImmediateCallbacks.delete(screenshotOnTestFailure);
|
|
||||||
|
|
||||||
// 5. Collect artifacts from any non-closed contexts.
|
|
||||||
await Promise.all(leftoverContexts.map(async context => {
|
|
||||||
await stopTracing(context.tracing, true);
|
|
||||||
if (captureScreenshots) {
|
|
||||||
await Promise.all(context.pages().map(async page => {
|
|
||||||
if ((page as any)[screenshottedSymbol])
|
|
||||||
return;
|
|
||||||
// Pass caret=initial to avoid any evaluations that might slow down the screenshot
|
|
||||||
// and let the page modify itself from the problematic state it had at the moment of failure.
|
|
||||||
await page.screenshot({ ...screenshotOptions, timeout: 5000, path: addScreenshotAttachment(), caret: 'initial' }).catch(() => {});
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}).concat(leftoverApiRequests.map(async context => {
|
|
||||||
const tracing = (context as any)._tracing as Tracing;
|
|
||||||
await stopTracing(tracing, true);
|
|
||||||
})));
|
|
||||||
|
|
||||||
// 6. Save test trace.
|
|
||||||
if (preserveTrace()) {
|
|
||||||
const events = (testInfo as any)._traceEvents;
|
|
||||||
if (events.length) {
|
|
||||||
const tracePath = path.join(_artifactsDir(), createGuid() + '.zip');
|
|
||||||
temporaryTraceFiles.push(tracePath);
|
|
||||||
await saveTraceFile(tracePath, events, traceOptions.sources);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 7. Either remove or attach temporary traces and screenshots for contexts closed
|
|
||||||
// before the test has finished.
|
|
||||||
if (preserveTrace() && temporaryTraceFiles.length) {
|
|
||||||
const tracePath = testInfo.outputPath(`trace.zip`);
|
|
||||||
await mergeTraceFiles(tracePath, temporaryTraceFiles);
|
|
||||||
testInfo.attachments.push({ name: 'trace', path: tracePath, contentType: 'application/zip' });
|
|
||||||
}
|
|
||||||
await Promise.all(temporaryScreenshots.map(async file => {
|
|
||||||
if (captureScreenshots)
|
|
||||||
await fs.promises.rename(file, addScreenshotAttachment()).catch(() => {});
|
|
||||||
else
|
|
||||||
await fs.promises.unlink(file).catch(() => {});
|
|
||||||
}));
|
|
||||||
}, { auto: 'all-hooks-included', _title: 'trace recording' } as any],
|
}, { auto: 'all-hooks-included', _title: 'trace recording' } as any],
|
||||||
|
|
||||||
_contextFactory: [async ({ browser, video, _artifactsDir, _reuseContext }, use, testInfo) => {
|
_contextFactory: [async ({ browser, video, _artifactsDir, _reuseContext }, use, testInfo) => {
|
||||||
@ -600,6 +443,10 @@ type StackFrame = {
|
|||||||
function?: string,
|
function?: string,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type ScreenshotOption = PlaywrightWorkerOptions['screenshot'] | undefined;
|
||||||
|
type TraceOption = PlaywrightWorkerOptions['trace'] | undefined;
|
||||||
|
type Playwright = PlaywrightWorkerArgs['playwright'];
|
||||||
|
|
||||||
function normalizeVideoMode(video: VideoMode | 'retry-with-video' | { mode: VideoMode } | undefined): VideoMode {
|
function normalizeVideoMode(video: VideoMode | 'retry-with-video' | { mode: VideoMode } | undefined): VideoMode {
|
||||||
if (!video)
|
if (!video)
|
||||||
return 'off';
|
return 'off';
|
||||||
@ -613,7 +460,7 @@ function shouldCaptureVideo(videoMode: VideoMode, testInfo: TestInfo) {
|
|||||||
return (videoMode === 'on' || videoMode === 'retain-on-failure' || (videoMode === 'on-first-retry' && testInfo.retry === 1));
|
return (videoMode === 'on' || videoMode === 'retain-on-failure' || (videoMode === 'on-first-retry' && testInfo.retry === 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeTraceMode(trace: TraceMode | 'retry-with-trace' | { mode: TraceMode } | undefined): TraceMode {
|
function normalizeTraceMode(trace: TraceOption): TraceMode {
|
||||||
if (!trace)
|
if (!trace)
|
||||||
return 'off';
|
return 'off';
|
||||||
let traceMode = typeof trace === 'string' ? trace : trace.mode;
|
let traceMode = typeof trace === 'string' ? trace : trace.mode;
|
||||||
@ -626,7 +473,7 @@ function shouldCaptureTrace(traceMode: TraceMode, testInfo: TestInfo) {
|
|||||||
return traceMode === 'on' || traceMode === 'retain-on-failure' || (traceMode === 'on-first-retry' && testInfo.retry === 1) || (traceMode === 'on-all-retries' && testInfo.retry > 0);
|
return traceMode === 'on' || traceMode === 'retain-on-failure' || (traceMode === 'on-first-retry' && testInfo.retry === 1) || (traceMode === 'on-all-retries' && testInfo.retry > 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeScreenshotMode(screenshot: PlaywrightWorkerOptions['screenshot'] | undefined): ScreenshotMode {
|
function normalizeScreenshotMode(screenshot: ScreenshotOption): ScreenshotMode {
|
||||||
if (!screenshot)
|
if (!screenshot)
|
||||||
return 'off';
|
return 'off';
|
||||||
return typeof screenshot === 'string' ? screenshot : screenshot.mode;
|
return typeof screenshot === 'string' ? screenshot : screenshot.mode;
|
||||||
@ -664,6 +511,210 @@ function connectOptionsFromEnv() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class ArtifactsRecorder {
|
||||||
|
private _testInfo!: TestInfoImpl;
|
||||||
|
private _playwright: Playwright;
|
||||||
|
private _artifactsDir: string;
|
||||||
|
private _screenshotMode: ScreenshotMode;
|
||||||
|
private _traceMode: TraceMode;
|
||||||
|
private _captureTrace = false;
|
||||||
|
private _screenshotOptions: { mode: ScreenshotMode } & Pick<playwrightLibrary.PageScreenshotOptions, 'fullPage' | 'omitBackground'> | undefined;
|
||||||
|
private _traceOptions: { screenshots: boolean, snapshots: boolean, sources: boolean, mode?: TraceMode };
|
||||||
|
private _temporaryTraceFiles: string[] = [];
|
||||||
|
private _temporaryScreenshots: string[] = [];
|
||||||
|
private _reusedContexts = new Set<BrowserContext>();
|
||||||
|
private _traceOrdinal = 0;
|
||||||
|
private _screenshottedSymbol: symbol;
|
||||||
|
private _startedCollectingArtifacts: symbol;
|
||||||
|
private _screenshotOnTestFailureBound: () => Promise<void>;
|
||||||
|
|
||||||
|
constructor(playwright: Playwright, artifactsDir: string, trace: TraceOption, screenshot: ScreenshotOption) {
|
||||||
|
this._playwright = playwright;
|
||||||
|
this._artifactsDir = artifactsDir;
|
||||||
|
this._screenshotMode = normalizeScreenshotMode(screenshot);
|
||||||
|
this._screenshotOptions = typeof screenshot === 'string' ? undefined : screenshot;
|
||||||
|
this._traceMode = normalizeTraceMode(trace);
|
||||||
|
const defaultTraceOptions = { screenshots: true, snapshots: true, sources: true };
|
||||||
|
this._traceOptions = typeof trace === 'string' ? defaultTraceOptions : { ...defaultTraceOptions, ...trace, mode: undefined };
|
||||||
|
this._screenshottedSymbol = Symbol('screenshotted');
|
||||||
|
this._startedCollectingArtifacts = Symbol('startedCollectingArtifacts');
|
||||||
|
this._screenshotOnTestFailureBound = this._screenshotOnTestFailure.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
async testStarted(testInfo: TestInfoImpl) {
|
||||||
|
this._testInfo = testInfo;
|
||||||
|
this._captureTrace = shouldCaptureTrace(this._traceMode, testInfo) && !process.env.PW_TEST_DISABLE_TRACING;
|
||||||
|
|
||||||
|
// Process existing contexts.
|
||||||
|
for (const browserType of [this._playwright.chromium, this._playwright.firefox, this._playwright.webkit]) {
|
||||||
|
const promises: (Promise<void> | undefined)[] = [];
|
||||||
|
const existingContexts = Array.from((browserType as any)._contexts) as BrowserContext[];
|
||||||
|
for (const context of existingContexts) {
|
||||||
|
if ((context as any)[kIsReusedContext])
|
||||||
|
this._reusedContexts.add(context);
|
||||||
|
else
|
||||||
|
promises.push(this.didCreateBrowserContext(context));
|
||||||
|
}
|
||||||
|
await Promise.all(promises);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
const existingApiRequests: APIRequestContext[] = Array.from((this._playwright.request as any)._contexts as Set<APIRequestContext>);
|
||||||
|
await Promise.all(existingApiRequests.map(c => this.didCreateRequestContext(c)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup "screenshot on failure" callback.
|
||||||
|
if (this._screenshotMode === 'on' || this._screenshotMode === 'only-on-failure')
|
||||||
|
testInfo._onTestFailureImmediateCallbacks.set(this._screenshotOnTestFailureBound, 'Screenshot on failure');
|
||||||
|
}
|
||||||
|
|
||||||
|
async didCreateBrowserContext(context: BrowserContext) {
|
||||||
|
await this._startTraceChunkOnContextCreation(context.tracing);
|
||||||
|
}
|
||||||
|
|
||||||
|
async willCloseBrowserContext(context: BrowserContext) {
|
||||||
|
// When reusing context, we get all previous contexts closed at the start of next test.
|
||||||
|
// Do not record empty traces and useless screenshots for them.
|
||||||
|
if (this._reusedContexts.has(context))
|
||||||
|
return;
|
||||||
|
await this._stopTracing(context.tracing, (context as any)[kStartedContextTearDown]);
|
||||||
|
if (this._screenshotMode === 'on' || this._screenshotMode === 'only-on-failure') {
|
||||||
|
// Capture screenshot for now. We'll know whether we have to preserve them
|
||||||
|
// after the test finishes.
|
||||||
|
await Promise.all(context.pages().map(page => this._screenshotPage(page)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async didCreateRequestContext(context: APIRequestContext) {
|
||||||
|
const tracing = (context as any)._tracing as Tracing;
|
||||||
|
await this._startTraceChunkOnContextCreation(tracing);
|
||||||
|
}
|
||||||
|
|
||||||
|
async willCloseRequestContext(context: APIRequestContext) {
|
||||||
|
const tracing = (context as any)._tracing as Tracing;
|
||||||
|
await this._stopTracing(tracing, (context as any)[kStartedContextTearDown]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async testFinished() {
|
||||||
|
const captureScreenshots = this._screenshotMode === 'on' || (this._screenshotMode === 'only-on-failure' && this._testInfo.status !== this._testInfo.expectedStatus);
|
||||||
|
|
||||||
|
const leftoverContexts: BrowserContext[] = [];
|
||||||
|
for (const browserType of [this._playwright.chromium, this._playwright.firefox, this._playwright.webkit])
|
||||||
|
leftoverContexts.push(...(browserType as any)._contexts);
|
||||||
|
const leftoverApiRequests: APIRequestContext[] = Array.from((this._playwright.request as any)._contexts as Set<APIRequestContext>);
|
||||||
|
this._testInfo._onTestFailureImmediateCallbacks.delete(this._screenshotOnTestFailureBound);
|
||||||
|
|
||||||
|
// Collect traces/screenshots for remaining contexts.
|
||||||
|
await Promise.all(leftoverContexts.map(async context => {
|
||||||
|
await this._stopTracing(context.tracing, true);
|
||||||
|
if (captureScreenshots) {
|
||||||
|
await Promise.all(context.pages().map(async page => {
|
||||||
|
if ((page as any)[this._screenshottedSymbol])
|
||||||
|
return;
|
||||||
|
// Pass caret=initial to avoid any evaluations that might slow down the screenshot
|
||||||
|
// and let the page modify itself from the problematic state it had at the moment of failure.
|
||||||
|
await page.screenshot({ ...this._screenshotOptions, timeout: 5000, path: this._addScreenshotAttachment(), caret: 'initial' }).catch(() => {});
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}).concat(leftoverApiRequests.map(async context => {
|
||||||
|
const tracing = (context as any)._tracing as Tracing;
|
||||||
|
await this._stopTracing(tracing, true);
|
||||||
|
})));
|
||||||
|
|
||||||
|
// Collect test trace.
|
||||||
|
if (this._preserveTrace()) {
|
||||||
|
const events = this._testInfo._traceEvents;
|
||||||
|
if (events.length) {
|
||||||
|
const tracePath = path.join(this._artifactsDir, createGuid() + '.zip');
|
||||||
|
this._temporaryTraceFiles.push(tracePath);
|
||||||
|
await saveTraceFile(tracePath, events, this._traceOptions.sources);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Either remove or attach temporary traces and screenshots for contexts closed
|
||||||
|
// before the test has finished.
|
||||||
|
if (this._preserveTrace() && this._temporaryTraceFiles.length) {
|
||||||
|
const tracePath = this._testInfo.outputPath(`trace.zip`);
|
||||||
|
await mergeTraceFiles(tracePath, this._temporaryTraceFiles);
|
||||||
|
this._testInfo.attachments.push({ name: 'trace', path: tracePath, contentType: 'application/zip' });
|
||||||
|
}
|
||||||
|
await Promise.all(this._temporaryScreenshots.map(async file => {
|
||||||
|
if (captureScreenshots)
|
||||||
|
await fs.promises.rename(file, this._addScreenshotAttachment()).catch(() => {});
|
||||||
|
else
|
||||||
|
await fs.promises.unlink(file).catch(() => {});
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
private _addScreenshotAttachment() {
|
||||||
|
const testFailed = this._testInfo.status !== this._testInfo.expectedStatus;
|
||||||
|
const index = this._testInfo.attachments.filter(a => a.name === 'screenshot').length + 1;
|
||||||
|
const screenshotPath = this._testInfo.outputPath(`test-${testFailed ? 'failed' : 'finished'}-${index}.png`);
|
||||||
|
this._testInfo.attachments.push({ name: 'screenshot', path: screenshotPath, contentType: 'image/png' });
|
||||||
|
return screenshotPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _screenshotPage(page: Page) {
|
||||||
|
if ((page as any)[this._screenshottedSymbol])
|
||||||
|
return;
|
||||||
|
(page as any)[this._screenshottedSymbol] = true;
|
||||||
|
const screenshotPath = path.join(this._artifactsDir, createGuid() + '.png');
|
||||||
|
this._temporaryScreenshots.push(screenshotPath);
|
||||||
|
// Pass caret=initial to avoid any evaluations that might slow down the screenshot
|
||||||
|
// and let the page modify itself from the problematic state it had at the moment of failure.
|
||||||
|
await page.screenshot({ ...this._screenshotOptions, timeout: 5000, path: screenshotPath, caret: 'initial' }).catch(() => {});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _screenshotOnTestFailure() {
|
||||||
|
const contexts: BrowserContext[] = [];
|
||||||
|
for (const browserType of [this._playwright.chromium, this._playwright.firefox, this._playwright.webkit])
|
||||||
|
contexts.push(...(browserType as any)._contexts);
|
||||||
|
await Promise.all(contexts.map(ctx => Promise.all(ctx.pages().map(page => this._screenshotPage(page)))));
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _startTraceChunkOnContextCreation(tracing: Tracing) {
|
||||||
|
if (this._captureTrace) {
|
||||||
|
const title = [path.relative(this._testInfo.project.testDir, this._testInfo.file) + ':' + this._testInfo.line, ...this._testInfo.titlePath.slice(1)].join(' › ');
|
||||||
|
const ordinalSuffix = this._traceOrdinal ? `-${this._traceOrdinal}` : '';
|
||||||
|
++this._traceOrdinal;
|
||||||
|
const retrySuffix = this._testInfo.retry ? `-${this._testInfo.retry}` : '';
|
||||||
|
const name = `${this._testInfo.testId}${retrySuffix}${ordinalSuffix}`;
|
||||||
|
if (!(tracing as any)[kTracingStarted]) {
|
||||||
|
await tracing.start({ ...this._traceOptions, title, name });
|
||||||
|
(tracing as any)[kTracingStarted] = true;
|
||||||
|
} else {
|
||||||
|
await tracing.startChunk({ title, name });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if ((tracing as any)[kTracingStarted]) {
|
||||||
|
(tracing as any)[kTracingStarted] = false;
|
||||||
|
await tracing.stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _preserveTrace() {
|
||||||
|
const testFailed = this._testInfo.status !== this._testInfo.expectedStatus;
|
||||||
|
return this._captureTrace && (this._traceMode === 'on' || (testFailed && this._traceMode === 'retain-on-failure') || (this._traceMode === 'on-first-retry' && this._testInfo.retry === 1) || (this._traceMode === 'on-all-retries' && this._testInfo.retry > 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _stopTracing(tracing: Tracing, contextTearDownStarted: boolean) {
|
||||||
|
if ((tracing as any)[this._startedCollectingArtifacts])
|
||||||
|
return;
|
||||||
|
(tracing as any)[this._startedCollectingArtifacts] = true;
|
||||||
|
if (this._captureTrace) {
|
||||||
|
let tracePath;
|
||||||
|
// Create a trace file if we know that:
|
||||||
|
// - it is's going to be used due to the config setting and the test status or
|
||||||
|
// - we are inside a test or afterEach and the user manually closed the context.
|
||||||
|
if (this._preserveTrace() || !contextTearDownStarted) {
|
||||||
|
tracePath = path.join(this._artifactsDir, createGuid() + '.zip');
|
||||||
|
this._temporaryTraceFiles.push(tracePath);
|
||||||
|
}
|
||||||
|
await tracing.stopChunk({ path: tracePath });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const test = _baseTest.extend<TestFixtures, WorkerFixtures>(playwrightFixtures);
|
export const test = _baseTest.extend<TestFixtures, WorkerFixtures>(playwrightFixtures);
|
||||||
|
|
||||||
export default test;
|
export default test;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user