fix(ct): isolate component tests when recording video / trace (#14531)
This commit is contained in:
parent
a7500c18d6
commit
95672765bc
|
|
@ -17,7 +17,7 @@
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import type { LaunchOptions, BrowserContextOptions, Page, Browser, BrowserContext, Video, APIRequestContext, Tracing } from 'playwright-core';
|
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 { rootTestType } from './testType';
|
||||||
import { createGuid, debugMode } from 'playwright-core/lib/utils';
|
import { createGuid, debugMode } from 'playwright-core/lib/utils';
|
||||||
import { removeFolders } from 'playwright-core/lib/utils/fileUtils';
|
import { removeFolders } from 'playwright-core/lib/utils/fileUtils';
|
||||||
|
|
@ -238,13 +238,10 @@ export const test = _baseTest.extend<TestFixtures, WorkerFixtures>({
|
||||||
if (debugMode())
|
if (debugMode())
|
||||||
testInfo.setTimeout(0);
|
testInfo.setTimeout(0);
|
||||||
|
|
||||||
let traceMode = typeof trace === 'string' ? trace : trace.mode;
|
const traceMode = normalizeTraceMode(trace);
|
||||||
if (traceMode as any === 'retry-with-trace')
|
|
||||||
traceMode = 'on-first-retry';
|
|
||||||
const defaultTraceOptions = { screenshots: true, snapshots: true, sources: true };
|
const defaultTraceOptions = { screenshots: true, snapshots: true, sources: true };
|
||||||
const traceOptions = typeof trace === 'string' ? defaultTraceOptions : { ...defaultTraceOptions, ...trace, mode: undefined };
|
const traceOptions = typeof trace === 'string' ? defaultTraceOptions : { ...defaultTraceOptions, ...trace, mode: undefined };
|
||||||
|
const captureTrace = shouldCaptureTrace(traceMode, testInfo);
|
||||||
const captureTrace = (traceMode === 'on' || traceMode === 'retain-on-failure' || (traceMode === 'on-first-retry' && testInfo.retry === 1));
|
|
||||||
const temporaryTraceFiles: string[] = [];
|
const temporaryTraceFiles: string[] = [];
|
||||||
const temporaryScreenshots: string[] = [];
|
const temporaryScreenshots: string[] = [];
|
||||||
const createdContexts = new Set<BrowserContext>();
|
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],
|
}, { auto: 'all-hooks-included', _title: 'built-in playwright configuration' } as any],
|
||||||
|
|
||||||
_contextFactory: [async ({ browser, video, _artifactsDir }, use, testInfo) => {
|
_contextFactory: [async ({ browser, video, _artifactsDir }, use, testInfo) => {
|
||||||
let videoMode = typeof video === 'string' ? video : video.mode;
|
const videoMode = normalizeVideoMode(video);
|
||||||
if (videoMode === 'retry-with-video')
|
const captureVideo = shouldCaptureVideo(videoMode, testInfo);
|
||||||
videoMode = 'on-first-retry';
|
|
||||||
|
|
||||||
const captureVideo = (videoMode === 'on' || videoMode === 'retain-on-failure' || (videoMode === 'on-first-retry' && testInfo.retry === 1));
|
|
||||||
const contexts = new Map<BrowserContext, { pages: Page[] }>();
|
const contexts = new Map<BrowserContext, { pages: Page[] }>();
|
||||||
|
|
||||||
await use(async options => {
|
await use(async options => {
|
||||||
|
|
@ -537,6 +531,28 @@ type ParsedStackTrace = {
|
||||||
apiName: string;
|
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');
|
const kTracingStarted = Symbol('kTracingStarted');
|
||||||
|
|
||||||
export default test;
|
export default test;
|
||||||
|
|
|
||||||
|
|
@ -14,59 +14,70 @@
|
||||||
* limitations under the License.
|
* 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[] = [];
|
let boundCallbacksForMount: Function[] = [];
|
||||||
|
|
||||||
export const fixtures: Fixtures<PlaywrightTestArgs & PlaywrightTestOptions & { mount: (component: any, options: any) => Promise<Locator> }, PlaywrightWorkerArgs & { _ctPage: { page: Page | undefined, hash: string } }> = {
|
export const fixtures: Fixtures<
|
||||||
_ctPage: [{ page: undefined, hash: '' }, { scope: 'worker' }],
|
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) => {
|
_ctWorker: [{ page: undefined, context: undefined, hash: '', isolateTests: false }, { scope: 'worker' }],
|
||||||
await use(page.context());
|
|
||||||
},
|
|
||||||
|
|
||||||
page: async ({ _ctPage, browser, viewport, playwright }, use) => {
|
context: async ({ _contextFactory, playwright, browser, _ctWorker, video, trace, viewport }, use, testInfo) => {
|
||||||
const defaultContextOptions = (playwright.chromium as any)._defaultContextOptions as BrowserContextOptions;
|
_ctWorker.isolateTests = shouldCaptureVideo(normalizeVideoMode(video), testInfo) || shouldCaptureTrace(normalizeTraceMode(trace), testInfo);
|
||||||
const hash = contextHash(defaultContextOptions);
|
if (_ctWorker.isolateTests) {
|
||||||
|
await use(await _contextFactory());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!_ctPage.page || _ctPage.hash !== hash) {
|
const defaultContextOptions = (playwright.chromium as any)._defaultContextOptions as BrowserContextOptions;
|
||||||
if (_ctPage.page)
|
const hash = contextHash(defaultContextOptions);
|
||||||
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);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
mount: async ({ page }, use) => {
|
if (!_ctWorker.page || _ctWorker.hash !== hash) {
|
||||||
await use(async (component, options) => {
|
if (_ctWorker.context)
|
||||||
const selector = await (page as any)._wrapApiCall(async () => {
|
await _ctWorker.context.close();
|
||||||
return await innerMount(page, component, options);
|
|
||||||
}, true);
|
const context = await browser.newContext();
|
||||||
return page.locator(selector);
|
const page = await createPage(context);
|
||||||
});
|
_ctWorker.context = context;
|
||||||
boundCallbacksForMount = [];
|
_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> {
|
async function innerMount(page: Page, jsxOrType: any, options: any): Promise<string> {
|
||||||
let component;
|
let component;
|
||||||
|
|
@ -137,3 +148,15 @@ function contextHash(context: BrowserContextOptions): string {
|
||||||
};
|
};
|
||||||
return JSON.stringify(hash);
|
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);
|
||||||
|
}
|
||||||
|
|
|
||||||
113
tests/playwright-test/playwright.ct-reuse.spec.ts
Normal file
113
tests/playwright-test/playwright.ct-reuse.spec.ts
Normal 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);
|
||||||
|
});
|
||||||
Loading…
Reference in a new issue