mirror of
https://github.com/microsoft/playwright.git
synced 2024-12-12 20:03:03 +03:00
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 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;
|
||||
|
@ -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);
|
||||
}
|
||||
|
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 New Issue
Block a user