diff --git a/docs/src/test-api/class-fixtures.md b/docs/src/test-api/class-fixtures.md index b68c101b4e..23c5044ef4 100644 --- a/docs/src/test-api/class-fixtures.md +++ b/docs/src/test-api/class-fixtures.md @@ -128,3 +128,9 @@ test('basic test', async ({ request }) => { // ... }); ``` + +## property: Fixtures.storage +* since: v1.28 +- type: <[Storage]> + +[Storage] is shared between all tests in the same run. diff --git a/docs/src/test-api/class-storage.md b/docs/src/test-api/class-storage.md new file mode 100644 index 0000000000..8a438d4be6 --- /dev/null +++ b/docs/src/test-api/class-storage.md @@ -0,0 +1,36 @@ +# class: Storage +* since: v1.28 +* langs: js + +Playwright Test provides a `storage` fixture for passing values between project setup and tests. +TODO: examples + +## method: Storage.get +* since: v1.28 +- returns: <[any]> + +Get named item from the store. + +### param: Storage.get.name +* since: v1.28 +- `name` <[string]> + +Item name. + +## method: Storage.set +* since: v1.28 + +Set value to the store. + +### param: Storage.set.name +* since: v1.28 +- `name` <[string]> + +Item name. + +### param: Storage.set.value +* since: v1.28 +- `value` <[any]> + +Item value. The value must be serializable to JSON. + diff --git a/packages/playwright-test/src/index.ts b/packages/playwright-test/src/index.ts index a870e3e8f8..c19ed76896 100644 --- a/packages/playwright-test/src/index.ts +++ b/packages/playwright-test/src/index.ts @@ -16,17 +16,18 @@ import * as fs from 'fs'; import * as path from 'path'; -import type { LaunchOptions, BrowserContextOptions, Page, BrowserContext, Video, APIRequestContext, Tracing } from 'playwright-core'; -import type { TestType, PlaywrightTestArgs, PlaywrightTestOptions, PlaywrightWorkerArgs, PlaywrightWorkerOptions, TestInfo, VideoMode, TraceMode } from '../types/test'; -import { rootTestType } from './testType'; +import type { APIRequestContext, BrowserContext, BrowserContextOptions, LaunchOptions, Page, Tracing, Video } from 'playwright-core'; +import * as playwrightLibrary from 'playwright-core'; +import * as outOfProcess from 'playwright-core/lib/outofprocess'; import { createGuid, debugMode } from 'playwright-core/lib/utils'; import { removeFolders } from 'playwright-core/lib/utils/fileUtils'; -export { expect } from './expect'; -export const _baseTest: TestType<{}, {}> = rootTestType.test; -export { addRunnerPlugin as _addRunnerPlugin } from './plugins'; -import * as outOfProcess from 'playwright-core/lib/outofprocess'; -import * as playwrightLibrary from 'playwright-core'; +import type { PlaywrightTestArgs, PlaywrightTestOptions, PlaywrightWorkerArgs, PlaywrightWorkerOptions, TestInfo, TestType, TraceMode, VideoMode } from '../types/test'; import type { TestInfoImpl } from './testInfo'; +import { rootTestType } from './testType'; +import { sanitizeForFilePath, trimLongString } from './util'; +export { expect } from './expect'; +export { addRunnerPlugin as _addRunnerPlugin } from './plugins'; +export const _baseTest: TestType<{}, {}> = rootTestType.test; if ((process as any)['__pw_initiator__']) { const originalStackTraceLimit = Error.stackTraceLimit; @@ -127,7 +128,7 @@ export const test = _baseTest.extend({ } }, { scope: 'worker', auto: true }], - browser: [async ({ playwright, browserName }, use) => { + browser: [async ({ playwright, browserName }, use, testInfo) => { if (!['chromium', 'firefox', 'webkit'].includes(browserName)) throw new Error(`Unexpected browserName "${browserName}", must be one of "chromium", "firefox" or "webkit"`); const browser = await playwright[browserName].launch(); @@ -135,6 +136,35 @@ export const test = _baseTest.extend({ await browser.close(); }, { scope: 'worker', timeout: 0 }], + storage: [async ({ }, use, testInfo) => { + const toFilePath = (name: string) => { + const fileName = sanitizeForFilePath(trimLongString(name)) + '.json'; + return path.join(testInfo.project.outputDir, '.playwright-storage', (testInfo as TestInfoImpl).project._id, fileName); + }; + const storage = { + async get(name: string) { + const file = toFilePath(name); + try { + const data = (await fs.promises.readFile(file)).toString('utf-8'); + return JSON.parse(data) as T; + } catch (e) { + return undefined; + } + }, + async set(name: string, value: T | undefined) { + const file = toFilePath(name); + if (value === undefined) { + await fs.promises.rm(file, { force: true }); + return; + } + const data = JSON.stringify(value, undefined, 2); + await fs.promises.mkdir(path.dirname(file), { recursive: true }); + await fs.promises.writeFile(file, data); + } + }; + await use(storage); + }, { scope: 'worker' }], + acceptDownloads: [({ contextOptions }, use) => use(contextOptions.acceptDownloads ?? true), { option: true }], bypassCSP: [({ contextOptions }, use) => use(contextOptions.bypassCSP), { option: true }], colorScheme: [({ contextOptions }, use) => use(contextOptions.colorScheme), { option: true }], diff --git a/packages/playwright-test/types/test.d.ts b/packages/playwright-test/types/test.d.ts index 3d20fbe350..57a9bcbf2f 100644 --- a/packages/playwright-test/types/test.d.ts +++ b/packages/playwright-test/types/test.d.ts @@ -2668,6 +2668,23 @@ type ConnectOptions = { timeout?: number; }; +/** + * Playwright Test provides a `storage` fixture for passing values between project setup and tests. TODO: examples + */ +interface Storage { + /** + * Get named item from the store. + * @param name Item name. + */ + get(name: string): Promise; + /** + * Set value to the store. + * @param name Item name. + * @param value Item value. The value must be serializable to JSON. + */ + set(name: string, value: T | undefined): Promise; +} + /** * Playwright Test provides many options to configure test environment, [Browser], [BrowserContext] and more. * @@ -3004,6 +3021,10 @@ export interface PlaywrightWorkerArgs { * Learn how to [configure browser](https://playwright.dev/docs/test-configuration) and see [available options][TestOptions]. */ browser: Browser; + /** + * [Storage] is shared between all tests in the same run. + */ + storage: Storage; } /** diff --git a/tests/playwright-test/playwright.spec.ts b/tests/playwright-test/playwright.spec.ts index 831eb58959..d8e332b91a 100644 --- a/tests/playwright-test/playwright.spec.ts +++ b/tests/playwright-test/playwright.spec.ts @@ -600,3 +600,121 @@ test('should pass fixture defaults to tests', async ({ runInlineTest }) => { expect(result.exitCode).toBe(0); expect(result.passed).toBe(1); }); + +test('should provide storage fixture', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'playwright.config.js': ` + module.exports = {}; + `, + 'a.test.ts': ` + const { test } = pwt; + test('should store number', async ({ storage }) => { + expect(storage).toBeTruthy(); + expect(await storage.get('number')).toBe(undefined); + await storage.set('number', 2022) + expect(await storage.get('number')).toBe(2022); + }); + test('should store object', async ({ storage }) => { + expect(storage).toBeTruthy(); + expect(await storage.get('object')).toBe(undefined); + await storage.set('object', { 'a': 2022 }) + expect(await storage.get('object')).toEqual({ 'a': 2022 }); + }); + `, + }, { workers: 1 }); + expect(result.exitCode).toBe(0); + expect(result.passed).toBe(2); +}); + + +test('should share storage state between project setup and tests', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'playwright.config.js': ` + module.exports = { + projects: [ + { + name: 'p1', + setup: /.*storage.setup.ts/ + } + ] + }; + `, + 'storage.setup.ts': ` + const { test, expect } = pwt; + test('should initialize storage', async ({ storage }) => { + expect(await storage.get('number')).toBe(undefined); + await storage.set('number', 2022) + expect(await storage.get('number')).toBe(2022); + + expect(await storage.get('object')).toBe(undefined); + await storage.set('object', { 'a': 2022 }) + expect(await storage.get('object')).toEqual({ 'a': 2022 }); + }); + `, + 'a.test.ts': ` + const { test } = pwt; + test('should get data from setup', async ({ storage }) => { + expect(await storage.get('number')).toBe(2022); + expect(await storage.get('object')).toEqual({ 'a': 2022 }); + }); + `, + 'b.test.ts': ` + const { test } = pwt; + test('should get data from setup', async ({ storage }) => { + expect(await storage.get('number')).toBe(2022); + expect(await storage.get('object')).toEqual({ 'a': 2022 }); + }); + `, + }, { workers: 1 }); + expect(result.exitCode).toBe(0); + expect(result.passed).toBe(3); +}); + + +test('should isolate storage state between projects', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'playwright.config.js': ` + module.exports = { + projects: [ + { + name: 'p1', + setup: /.*storage.setup.ts/ + }, + { + name: 'p2', + setup: /.*storage.setup.ts/ + } + ] + }; + `, + 'storage.setup.ts': ` + const { test, expect } = pwt; + test('should initialize storage', async ({ storage }, testInfo) => { + expect(await storage.get('number')).toBe(undefined); + await storage.set('number', 2022) + expect(await storage.get('number')).toBe(2022); + + expect(await storage.get('name')).toBe(undefined); + await storage.set('name', 'str-' + testInfo.project.name) + expect(await storage.get('name')).toBe('str-' + testInfo.project.name); + }); + `, + 'a.test.ts': ` + const { test } = pwt; + test('should get data from setup', async ({ storage }, testInfo) => { + expect(await storage.get('number')).toBe(2022); + expect(await storage.get('name')).toBe('str-' + testInfo.project.name); + }); + `, + 'b.test.ts': ` + const { test } = pwt; + test('should get data from setup', async ({ storage }, testInfo) => { + expect(await storage.get('number')).toBe(2022); + expect(await storage.get('name')).toBe('str-' + testInfo.project.name); + }); + `, + }, { workers: 2 }); + expect(result.exitCode).toBe(0); + expect(result.passed).toBe(6); +}); + diff --git a/utils/generate_types/overrides-test.d.ts b/utils/generate_types/overrides-test.d.ts index 5a41fa4de3..982be88d1c 100644 --- a/utils/generate_types/overrides-test.d.ts +++ b/utils/generate_types/overrides-test.d.ts @@ -196,6 +196,11 @@ type ConnectOptions = { timeout?: number; }; +interface Storage { + get(name: string): Promise; + set(name: string, value: T | undefined): Promise; +} + export interface PlaywrightWorkerOptions { browserName: BrowserName; defaultBrowserType: BrowserName; @@ -243,6 +248,7 @@ export interface PlaywrightTestOptions { export interface PlaywrightWorkerArgs { playwright: typeof import('playwright-core'); browser: Browser; + storage: Storage; } export interface PlaywrightTestArgs {