fix(ct): isolate component tests when recording video / trace (#14531)

This commit is contained in:
Pavel Feldman 2022-05-31 15:59:36 -07:00 committed by GitHub
parent a7500c18d6
commit 95672765bc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 209 additions and 57 deletions

View File

@ -17,7 +17,7 @@
import * as fs from 'fs';
import * as path from 'path';
import type { LaunchOptions, BrowserContextOptions, Page, Browser, BrowserContext, Video, APIRequestContext, Tracing } from 'playwright-core';
import type { TestType, PlaywrightTestArgs, PlaywrightTestOptions, PlaywrightWorkerArgs, PlaywrightWorkerOptions, TestInfo } from '../types/test';
import type { TestType, PlaywrightTestArgs, PlaywrightTestOptions, PlaywrightWorkerArgs, PlaywrightWorkerOptions, TestInfo, VideoMode, TraceMode } from '../types/test';
import { rootTestType } from './testType';
import { createGuid, debugMode } from 'playwright-core/lib/utils';
import { removeFolders } from 'playwright-core/lib/utils/fileUtils';
@ -238,13 +238,10 @@ export const test = _baseTest.extend<TestFixtures, WorkerFixtures>({
if (debugMode())
testInfo.setTimeout(0);
let traceMode = typeof trace === 'string' ? trace : trace.mode;
if (traceMode as any === 'retry-with-trace')
traceMode = 'on-first-retry';
const traceMode = normalizeTraceMode(trace);
const defaultTraceOptions = { screenshots: true, snapshots: true, sources: true };
const traceOptions = typeof trace === 'string' ? defaultTraceOptions : { ...defaultTraceOptions, ...trace, mode: undefined };
const captureTrace = (traceMode === 'on' || traceMode === 'retain-on-failure' || (traceMode === 'on-first-retry' && testInfo.retry === 1));
const captureTrace = shouldCaptureTrace(traceMode, testInfo);
const temporaryTraceFiles: string[] = [];
const temporaryScreenshots: string[] = [];
const createdContexts = new Set<BrowserContext>();
@ -432,11 +429,8 @@ export const test = _baseTest.extend<TestFixtures, WorkerFixtures>({
}, { auto: 'all-hooks-included', _title: 'built-in playwright configuration' } as any],
_contextFactory: [async ({ browser, video, _artifactsDir }, use, testInfo) => {
let videoMode = typeof video === 'string' ? video : video.mode;
if (videoMode === 'retry-with-video')
videoMode = 'on-first-retry';
const captureVideo = (videoMode === 'on' || videoMode === 'retain-on-failure' || (videoMode === 'on-first-retry' && testInfo.retry === 1));
const videoMode = normalizeVideoMode(video);
const captureVideo = shouldCaptureVideo(videoMode, testInfo);
const contexts = new Map<BrowserContext, { pages: Page[] }>();
await use(async options => {
@ -537,6 +531,28 @@ type ParsedStackTrace = {
apiName: string;
};
export function normalizeVideoMode(video: VideoMode | 'retry-with-video' | { mode: VideoMode }) {
let videoMode = typeof video === 'string' ? video : video.mode;
if (videoMode === 'retry-with-video')
videoMode = 'on-first-retry';
return videoMode;
}
export function shouldCaptureVideo(videoMode: VideoMode, testInfo: TestInfo) {
return (videoMode === 'on' || videoMode === 'retain-on-failure' || (videoMode === 'on-first-retry' && testInfo.retry === 1));
}
export function normalizeTraceMode(trace: TraceMode | 'retry-with-trace' | { mode: TraceMode }) {
let traceMode = typeof trace === 'string' ? trace : trace.mode;
if (traceMode === 'retry-with-trace')
traceMode = 'on-first-retry';
return traceMode;
}
export function shouldCaptureTrace(traceMode: TraceMode, testInfo: TestInfo) {
return traceMode === 'on' || traceMode === 'retain-on-failure' || (traceMode === 'on-first-retry' && testInfo.retry === 1);
}
const kTracingStarted = Symbol('kTracingStarted');
export default test;

View File

@ -14,59 +14,70 @@
* limitations under the License.
*/
import type { Fixtures, Locator, Page, BrowserContextOptions, PlaywrightTestArgs, PlaywrightTestOptions, PlaywrightWorkerArgs } from './types';
import { normalizeTraceMode, normalizeVideoMode, shouldCaptureTrace, shouldCaptureVideo } from './index';
import type { Fixtures, Locator, Page, BrowserContextOptions, PlaywrightTestArgs, PlaywrightTestOptions, PlaywrightWorkerArgs, PlaywrightWorkerOptions, BrowserContext } from './types';
let boundCallbacksForMount: Function[] = [];
export const fixtures: Fixtures<PlaywrightTestArgs & PlaywrightTestOptions & { mount: (component: any, options: any) => Promise<Locator> }, PlaywrightWorkerArgs & { _ctPage: { page: Page | undefined, hash: string } }> = {
_ctPage: [{ page: undefined, hash: '' }, { scope: 'worker' }],
export const fixtures: Fixtures<
PlaywrightTestArgs & PlaywrightTestOptions & { mount: (component: any, options: any) => Promise<Locator> },
PlaywrightWorkerArgs & PlaywrightWorkerOptions & { _ctWorker: { page: Page | undefined, context: BrowserContext | undefined, hash: string, isolateTests: boolean } },
{ _contextFactory: (options?: BrowserContextOptions) => Promise<BrowserContext> }> = {
context: async ({ page }, use) => {
await use(page.context());
},
_ctWorker: [{ page: undefined, context: undefined, hash: '', isolateTests: false }, { scope: 'worker' }],
page: async ({ _ctPage, browser, viewport, playwright }, use) => {
const defaultContextOptions = (playwright.chromium as any)._defaultContextOptions as BrowserContextOptions;
const hash = contextHash(defaultContextOptions);
context: async ({ _contextFactory, playwright, browser, _ctWorker, video, trace, viewport }, use, testInfo) => {
_ctWorker.isolateTests = shouldCaptureVideo(normalizeVideoMode(video), testInfo) || shouldCaptureTrace(normalizeTraceMode(trace), testInfo);
if (_ctWorker.isolateTests) {
await use(await _contextFactory());
return;
}
if (!_ctPage.page || _ctPage.hash !== hash) {
if (_ctPage.page)
await _ctPage.page.close();
const page = await (browser as any)._wrapApiCall(async () => {
const page = await browser.newPage();
await page.addInitScript('navigator.serviceWorker.register = () => {}');
await page.exposeFunction('__pw_dispatch', (ordinal: number, args: any[]) => {
boundCallbacksForMount[ordinal](...args);
});
await page.goto(process.env.PLAYWRIGHT_VITE_COMPONENTS_BASE_URL!);
return page;
}, true);
_ctPage.page = page;
_ctPage.hash = hash;
await use(page);
} else {
const page = _ctPage.page;
await (page as any)._wrapApiCall(async () => {
await (page as any)._resetForReuse();
await (page.context() as any)._resetForReuse();
await page.goto('about:blank');
await page.setViewportSize(viewport || { width: 1280, height: 800 });
await page.goto(process.env.PLAYWRIGHT_VITE_COMPONENTS_BASE_URL!);
}, true);
await use(page);
}
},
const defaultContextOptions = (playwright.chromium as any)._defaultContextOptions as BrowserContextOptions;
const hash = contextHash(defaultContextOptions);
mount: async ({ page }, use) => {
await use(async (component, options) => {
const selector = await (page as any)._wrapApiCall(async () => {
return await innerMount(page, component, options);
}, true);
return page.locator(selector);
});
boundCallbacksForMount = [];
},
};
if (!_ctWorker.page || _ctWorker.hash !== hash) {
if (_ctWorker.context)
await _ctWorker.context.close();
const context = await browser.newContext();
const page = await createPage(context);
_ctWorker.context = context;
_ctWorker.page = page;
_ctWorker.hash = hash;
await use(page.context());
return;
} else {
const page = _ctWorker.page;
await (page as any)._wrapApiCall(async () => {
await (page as any)._resetForReuse();
await (page.context() as any)._resetForReuse();
await page.goto('about:blank');
await page.setViewportSize(viewport || { width: 1280, height: 800 });
await page.goto(process.env.PLAYWRIGHT_VITE_COMPONENTS_BASE_URL!);
}, true);
await use(page.context());
}
},
page: async ({ context, _ctWorker }, use) => {
if (_ctWorker.isolateTests) {
await use(await createPage(context));
return;
}
await use(_ctWorker.page!);
},
mount: async ({ page }, use) => {
await use(async (component, options) => {
const selector = await (page as any)._wrapApiCall(async () => {
return await innerMount(page, component, options);
}, true);
return page.locator(selector);
});
boundCallbacksForMount = [];
},
};
async function innerMount(page: Page, jsxOrType: any, options: any): Promise<string> {
let component;
@ -137,3 +148,15 @@ function contextHash(context: BrowserContextOptions): string {
};
return JSON.stringify(hash);
}
function createPage(context: BrowserContext): Promise<Page> {
return (context as any)._wrapApiCall(async () => {
const page = await context.newPage();
await page.addInitScript('navigator.serviceWorker.register = () => {}');
await page.exposeFunction('__pw_dispatch', (ordinal: number, args: any[]) => {
boundCallbacksForMount[ordinal](...args);
});
await page.goto(process.env.PLAYWRIGHT_VITE_COMPONENTS_BASE_URL!);
return page;
}, true);
}

View File

@ -0,0 +1,113 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { test, expect } from './playwright-test-fixtures';
test('should reuse context', async ({ runInlineTest }) => {
const result = await runInlineTest({
'playwright/index.html': `<script type="module" src="/playwright/index.ts"></script>`,
'playwright/index.ts': `
//@no-header
`,
'src/reuse.test.tsx': `
//@no-header
import { test, expect } from '@playwright/experimental-ct-react';
let lastContext;
test('one', async ({ context }) => {
lastContext = context;
});
test('two', async ({ context }) => {
expect(context).toBe(lastContext);
});
test.describe('Dark', () => {
test.use({ colorScheme: 'dark' });
test('three', async ({ context }) => {
expect(context).not.toBe(lastContext);
});
});
`,
}, { workers: 1 });
expect(result.exitCode).toBe(0);
expect(result.passed).toBe(3);
});
test('should not reuse context with video', async ({ runInlineTest }) => {
const result = await runInlineTest({
'playwright.config.ts': `
export default {
use: { video: 'on' },
};
`,
'playwright/index.html': `<script type="module" src="/playwright/index.ts"></script>`,
'playwright/index.ts': `
//@no-header
`,
'src/reuse.test.tsx': `
//@no-header
import { test, expect } from '@playwright/experimental-ct-react';
let lastContext;
test('one', async ({ context }) => {
lastContext = context;
});
test('two', async ({ context }) => {
expect(context).not.toBe(lastContext);
});
`,
}, { workers: 1 });
expect(result.exitCode).toBe(0);
expect(result.passed).toBe(2);
});
test('should not reuse context with trace', async ({ runInlineTest }) => {
const result = await runInlineTest({
'playwright.config.ts': `
export default {
use: { trace: 'on' },
};
`,
'playwright/index.html': `<script type="module" src="/playwright/index.ts"></script>`,
'playwright/index.ts': `
//@no-header
`,
'src/reuse.test.tsx': `
//@no-header
import { test, expect } from '@playwright/experimental-ct-react';
let lastContext;
test('one', async ({ context }) => {
lastContext = context;
});
test('two', async ({ context }) => {
expect(context).not.toBe(lastContext);
});
`,
}, { workers: 1 });
expect(result.exitCode).toBe(0);
expect(result.passed).toBe(2);
});