From 95672765bcf95c7cfb372c10d0fbc3694d0328e1 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Tue, 31 May 2022 15:59:36 -0700 Subject: [PATCH] fix(ct): isolate component tests when recording video / trace (#14531) --- packages/playwright-test/src/index.ts | 38 ++++-- packages/playwright-test/src/mount.ts | 115 +++++++++++------- .../playwright.ct-reuse.spec.ts | 113 +++++++++++++++++ 3 files changed, 209 insertions(+), 57 deletions(-) create mode 100644 tests/playwright-test/playwright.ct-reuse.spec.ts diff --git a/packages/playwright-test/src/index.ts b/packages/playwright-test/src/index.ts index 083df68454..d605966763 100644 --- a/packages/playwright-test/src/index.ts +++ b/packages/playwright-test/src/index.ts @@ -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({ 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(); @@ -432,11 +429,8 @@ export const test = _baseTest.extend({ }, { 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(); 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; diff --git a/packages/playwright-test/src/mount.ts b/packages/playwright-test/src/mount.ts index cad29dcd7d..393359ecd8 100644 --- a/packages/playwright-test/src/mount.ts +++ b/packages/playwright-test/src/mount.ts @@ -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 Promise }, 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 }, + PlaywrightWorkerArgs & PlaywrightWorkerOptions & { _ctWorker: { page: Page | undefined, context: BrowserContext | undefined, hash: string, isolateTests: boolean } }, + { _contextFactory: (options?: BrowserContextOptions) => Promise }> = { - 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 { let component; @@ -137,3 +148,15 @@ function contextHash(context: BrowserContextOptions): string { }; return JSON.stringify(hash); } + +function createPage(context: BrowserContext): Promise { + 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); +} diff --git a/tests/playwright-test/playwright.ct-reuse.spec.ts b/tests/playwright-test/playwright.ct-reuse.spec.ts new file mode 100644 index 0000000000..7c1721bac8 --- /dev/null +++ b/tests/playwright-test/playwright.ct-reuse.spec.ts @@ -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': ``, + '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': ``, + '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': ``, + '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); +});