From 3c1b26b9f1632e2782046bc87069fc97bb206d48 Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Mon, 5 Dec 2022 21:37:37 -0800 Subject: [PATCH] feat: make storage a global variable (#19283) --- docs/src/auth.md | 100 ++++++++++------------- docs/src/test-api/class-storage.md | 36 -------- docs/src/test-api/class-testinfo.md | 6 -- docs/src/test-api/class-testoptions.md | 6 +- docs/src/test-api/class-testproject.md | 2 +- docs/src/test-api/class-teststorage.md | 56 +++++++++++++ packages/playwright-test/src/index.ts | 4 +- packages/playwright-test/src/storage.ts | 54 ++++++++++++ packages/playwright-test/src/testInfo.ts | 39 +-------- packages/playwright-test/types/test.d.ts | 27 +++--- tests/playwright-test/storage.spec.ts | 37 +++------ tests/playwright-test/types.spec.ts | 8 +- utils/generate_types/overrides-test.d.ts | 3 +- 13 files changed, 198 insertions(+), 180 deletions(-) delete mode 100644 docs/src/test-api/class-storage.md create mode 100644 docs/src/test-api/class-teststorage.md create mode 100644 packages/playwright-test/src/storage.ts diff --git a/docs/src/auth.md b/docs/src/auth.md index 37cc729cc3..cb20ae521f 100644 --- a/docs/src/auth.md +++ b/docs/src/auth.md @@ -177,15 +177,15 @@ in only once per project and then skip the log in step for all of the tests. Web apps use cookie-based or token-based authentication, where authenticated state is stored as [cookies](https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies) or in [local storage](https://developer.mozilla.org/en-US/docs/Web/API/Storage). Playwright provides [browserContext.storageState([options])](https://playwright.dev/docs/api/class-browsercontext#browser-context-storage-state) method that can be used to retrieve storage state from authenticated contexts and then create new contexts with prepopulated state. -You can run authentication steps once during the project [`property: TestProject.setup`] phase and save the context state into [`method: TestInfo.storage`]. The stored value can later be reused to automatically restore authenticated context state in every test of the project. This way the login will run once per project before all tests. +You can run authentication steps once during the project [`property: TestProject.setup`] phase and save the context state into [TestStorage]. The stored value can later be reused to automatically restore authenticated context state in every test of the project. This way the login will run once per project before all tests. Create a setup test that performs login and saves the context state into project storage: ```js tab=js-js // github-login.setup.js -const { test } = require('@playwright/test'); +const { setup, storage } = require('@playwright/test'); -test('sign in', async ({ page, context }) => { +setup('sign in', async ({ page, context }) => { await page.goto('https://github.com/login'); await page.getByLabel('User Name').fill('user'); await page.getByLabel('Password').fill('password'); @@ -193,16 +193,15 @@ test('sign in', async ({ page, context }) => { // Save signed-in state to an entry named 'github-test-user'. const contextState = await context.storageState(); - const storage = test.info().storage(); await storage.set('github-test-user', contextState) }); ``` ```js tab=js-ts // github-login.setup.ts -import { test } from '@playwright/test'; +import { setup, storage } from '@playwright/setup'; -test('sign in', async ({ page, context }) => { +setup('sign in', async ({ page, context }) => { await page.goto('https://github.com/login'); await page.getByLabel('User Name').fill('user'); await page.getByLabel('Password').fill('password'); @@ -210,7 +209,6 @@ test('sign in', async ({ page, context }) => { // Save signed-in state to an entry named 'github-test-user'. const contextState = await context.storageState(); - const storage = test.info().storage(); await storage.set('github-test-user', contextState) }); ``` @@ -278,29 +276,29 @@ test('test', async ({ page }) => { ### Reusing signed in state between test runs * langs: js -When you set an entry on [`method: TestInfo.storage`] Playwright will store it in a separate file under `.playwright-storage/`. Playwright does not delete those files automatically. You can leverage this fact to persist storage state between test runs and only sign in if the entry is not in the storage yet. +When you set an entry on [TestStorage] Playwright will store it in a separate file under `.playwright-storage/`. Playwright does not delete those files automatically. You can leverage this fact to persist storage state between test runs and only sign in if the entry is not in the storage yet. ```js tab=js-js // github-login.setup.js -const { test } = require('@playwright/test'); +const { setup, storage } = require('@playwright/test'); -test('sign in', async ({ page, context }) => { - if (test.info().storage().get('github-test-user')) +setup('sign in', async ({ page, context }) => { + if (storage.get('github-test-user')) return; // ... login here ... - await test.info().storage().set('github-test-user', await context.storageState()); + await storage.set('github-test-user', await context.storageState()); }); ``` ```js tab=js-ts // github-login.setup.ts -import { test } from '@playwright/test'; +import { setup, storage } from '@playwright/test'; -test('sign in', async ({ page, context }) => { - if (test.info().storage().get('github-test-user')) +setup('sign in', async ({ page, context }) => { + if (storage.get('github-test-user')) return; // ... login here ... - await test.info().storage().set('github-test-user', await context.storageState()); + await storage.set('github-test-user', await context.storageState()); }); ``` @@ -313,9 +311,9 @@ If your web application supports signing in via API, you can use [APIRequestCont ```js tab=js-js // github-login.setup.js -const { test } = require('@playwright/test'); +const { setup, storage } = require('@playwright/test'); -test('sign in', async ({ request }) => { +setup('sign in', async ({ request }) => { await request.post('https://github.com/login', { form: { 'user': 'user', @@ -324,16 +322,15 @@ test('sign in', async ({ request }) => { }); // Save signed-in state to an entry named 'github-test-user'. const contextState = await request.storageState(); - const storage = test.info().storage(); await storage.set('github-test-user', contextState) }); ``` ```js tab=js-ts // github-login.setup.ts -import { test } from '@playwright/test'; +import { setup, storage } from '@playwright/test'; -test('sign in', async ({ request }) => { +setup('sign in', async ({ request }) => { await request.post('https://github.com/login', { form: { 'user': 'user', @@ -342,7 +339,6 @@ test('sign in', async ({ request }) => { }); // Save signed-in state to an entry named 'github-test-user'. const contextState = await request.storageState(); - const storage = test.info().storage(); await storage.set('github-test-user', contextState) }); ``` @@ -356,7 +352,7 @@ In this example we [override `storageState` fixture](./test-fixtures.md#overridi ```js tab=js-js // signin-all-users.setup.js -const { test } = require('@playwright/test'); +const { setup, storage } = require('@playwright/test'); const users = [ { username: 'user-1', password: 'password-1' }, @@ -365,21 +361,20 @@ const users = [ ]; // Run all logins in parallel. -test.describe.configure({ +setup.describe.configure({ mode: 'parallel' }); // Sign in all test users duing project setup and save their state // to be used in the tests. for (let i = 0; i < users.length; i++) { - test(`login user ${i}`, async ({ page }) => { + setup(`login user ${i}`, async ({ page }) => { await page.goto('https://github.com/login'); await page.getByLabel('User Name').fill(users[i].username); await page.getByLabel('Password').fill(users[i].password); await page.getByText('Sign in').click(); const contextState = await page.context().storageState(); - const storage = test.info().storage(); await storage.set(`test-user-${i}`, contextState); }); } @@ -399,7 +394,7 @@ test('test', async ({ page }) => { ```js tab=js-ts // signin-all-users.setup.ts -import { test } from '@playwright/test'; +import { setup, storage } from '@playwright/test'; const users = [ { username: 'user-1', password: 'password-1' }, @@ -408,14 +403,14 @@ const users = [ ]; // Run all logins in parallel. -test.describe.configure({ +setup.describe.configure({ mode: 'parallel' }); // Sign in all test users duing project setup and save their state // to be used in the tests. for (let i = 0; i < users.length; i++) { - test(`login user ${i}`, async ({ page }) => { + setup(`login user ${i}`, async ({ page }) => { await page.goto('https://github.com/login'); // Use a unique username for each worker. await page.getByLabel('User Name').fill(users[i].username); @@ -423,7 +418,6 @@ for (let i = 0; i < users.length; i++) { await page.getByText('Sign in').click(); const contextState = await page.context().storageState(); - const storage = test.info().storage(); await storage.set(`test-user-${i}`, contextState); }); } @@ -433,7 +427,7 @@ import { test } from '@playwright/test'; test.use({ // User different user for each worker. - storageStateName: `test-user-${test.info().parallelIndex}` + storageStateName: ({}, use) => use(`test-user-${test.info().parallelIndex}`) }); test('test', async ({ page }) => { @@ -448,29 +442,27 @@ Sometimes you have more than one signed-in user in your end to end tests. You ca ```js tab=js-js // login.setup.js -const { test } = require('@playwright/test'); +const { setup, storage } = require('@playwright/test'); // Run all logins in parallel. -test.describe.configure({ +setup.describe.configure({ mode: 'parallel' }); -test(`login as regular user`, async ({ page }) => { +setup(`login as regular user`, async ({ page }) => { await page.goto('https://github.com/login'); //... const contextState = await page.context().storageState(); - const storage = test.info().storage(); // Save the user state. await storage.set(`user`, contextState); }); -test(`login as admin`, async ({ page }) => { +setup(`login as admin`, async ({ page }) => { await page.goto('https://github.com/login'); //... const contextState = await page.context().storageState(); - const storage = test.info().storage(); // Save the admin state. await storage.set(`admin`, contextState); }); @@ -478,29 +470,27 @@ test(`login as admin`, async ({ page }) => { ```js tab=js-ts // login.setup.ts -import { test } from '@playwright/test'; +import { setup, storage } from '@playwright/test'; // Run all logins in parallel. -test.describe.configure({ +setup.describe.configure({ mode: 'parallel' }); -test(`login as regular user`, async ({ page }) => { +setup(`login as regular user`, async ({ page }) => { await page.goto('https://github.com/login'); //... const contextState = await page.context().storageState(); - const storage = test.info().storage(); // Save the user state. await storage.set(`user`, contextState); }); -test(`login as admin`, async ({ page }) => { +setup(`login as admin`, async ({ page }) => { await page.goto('https://github.com/login'); //... const contextState = await page.context().storageState(); - const storage = test.info().storage(); // Save the admin state. await storage.set(`admin`, contextState); }); @@ -550,15 +540,15 @@ test.describe(() => { If you need to test how multiple authenticated roles interact together, use multiple [BrowserContext]s and [Page]s with different storage states in the same test. Any of the methods above to create multiple storage state entries would work. ```js tab=js-ts -import { test } from '@playwright/test'; +import { test, storage } from '@playwright/test'; test('admin and user', async ({ browser }) => { // adminContext and all pages inside, including adminPage, are signed in as "admin". - const adminContext = await browser.newContext({ storageState: await test.info().storage().get('admin') }); + const adminContext = await browser.newContext({ storageState: await storage.get('admin') }); const adminPage = await adminContext.newPage(); // userContext and all pages inside, including userPage, are signed in as "user". - const userContext = await browser.newContext({ storageState: await test.info().storage().get('user') }); + const userContext = await browser.newContext({ storageState: await storage.get('user') }); const userPage = await userContext.newPage(); // ... interact with both adminPage and userPage ... @@ -566,15 +556,15 @@ test('admin and user', async ({ browser }) => { ``` ```js tab=js-js -const { test } = require('@playwright/test'); +const { test, storage } = require('@playwright/test'); test('admin and user', async ({ browser }) => { // adminContext and all pages inside, including adminPage, are signed in as "admin". - const adminContext = await browser.newContext({ storageState: await test.info().storage().get('admin') }); + const adminContext = await browser.newContext({ storageState: await storage.get('admin') }); const adminPage = await adminContext.newPage(); // userContext and all pages inside, including userPage, are signed in as "user". - const userContext = await browser.newContext({ storageState: await test.info().storage().get('user') }); + const userContext = await browser.newContext({ storageState: await storage.get('user') }); const userPage = await userContext.newPage(); // ... interact with both adminPage and userPage ... @@ -591,7 +581,7 @@ Below is an example that [creates fixtures](./test-fixtures.md#creating-a-fixtur ```js tab=js-ts // fixtures.ts import { test as base, Page, Browser, Locator } from '@playwright/test'; -export { expect } from '@playwright/test'; +export { expect, storage } from '@playwright/test'; // Page Object Model for the "admin" page. // Here you can add locators and helper methods specific to the admin page. @@ -604,7 +594,7 @@ class AdminPage { } static async create(browser: Browser) { - const context = await browser.newContext({ storageState: await test.info().storage().get('admin') }); + const context = await browser.newContext({ storageState: await storage.get('admin') }); const page = await context.newPage(); return new AdminPage(page); } @@ -625,7 +615,7 @@ class UserPage { } static async create(browser: Browser) { - const context = await browser.newContext({ storageState: await test.info().storage().get('user') }); + const context = await browser.newContext({ storageState: await storage.get('user') }); const page = await context.newPage(); return new UserPage(page); } @@ -662,7 +652,7 @@ test('admin and user', async ({ adminPage, userPage }) => { ```js tab=js-js // fixtures.js -const { test: base } = require('@playwright/test'); +const { test: base, storage } = require('@playwright/test'); // Page Object Model for the "admin" page. // Here you can add locators and helper methods specific to the admin page. @@ -673,7 +663,7 @@ class AdminPage { } static async create(browser) { - const context = await browser.newContext({ storageState: await test.info().storage().get('admin') }); + const context = await browser.newContext({ storageState: await storage.get('admin') }); const page = await context.newPage(); return new AdminPage(page); } @@ -690,7 +680,7 @@ class UserPage { } static async create(browser) { - const context = await browser.newContext({ storageState: await test.info().storage().get('user') }); + const context = await browser.newContext({ storageState: await storage.get('user') }); const page = await context.newPage(); return new UserPage(page); } diff --git a/docs/src/test-api/class-storage.md b/docs/src/test-api/class-storage.md deleted file mode 100644 index 366544a331..0000000000 --- a/docs/src/test-api/class-storage.md +++ /dev/null @@ -1,36 +0,0 @@ -# class: Storage -* since: v1.28 -* langs: js - -Playwright Test provides a [`method: TestInfo.storage`] object for passing values between project setup and tests. -TODO: examples - -## async method: Storage.get -* since: v1.28 -- returns: <[any]> - -Get named item from the storage. Returns undefined if there is no value with given name. - -### param: Storage.get.name -* since: v1.28 -- `name` <[string]> - -Item name. - -## async method: Storage.set -* since: v1.28 - -Set value to the storage. - -### 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. Passing `undefined` deletes the entry with given name. - diff --git a/docs/src/test-api/class-testinfo.md b/docs/src/test-api/class-testinfo.md index b545955203..4c9e6e6c18 100644 --- a/docs/src/test-api/class-testinfo.md +++ b/docs/src/test-api/class-testinfo.md @@ -505,12 +505,6 @@ Output written to `process.stderr` or `console.error` during the test execution. Output written to `process.stdout` or `console.log` during the test execution. -## method: TestInfo.storage -* since: v1.28 -- returns: <[Storage]> - -Returns a [Storage] instance for the currently running project. - ## property: TestInfo.timeout * since: v1.10 - type: <[int]> diff --git a/docs/src/test-api/class-testoptions.md b/docs/src/test-api/class-testoptions.md index 7181f15d62..8668f3ca15 100644 --- a/docs/src/test-api/class-testoptions.md +++ b/docs/src/test-api/class-testoptions.md @@ -203,11 +203,11 @@ Learn more about [automatic screenshots](../test-configuration.md#automatic-scre * since: v1.10 ## property: TestOptions.storageStateName -* since: v1.28 +* since: v1.29 - type: <[string]> -Name of the [Storage] entry that should be used to initialize [`property: TestOptions.storageState`]. The value must be -written to the storage before creatiion of a browser context that uses it (usually in [`property: TestProject.setup`]). If both +Name of the [TestStorage] entry that should be used to initialize [`property: TestOptions.storageState`]. The value must be +written to the test storage before creation of a browser context that uses it (usually in [`property: TestProject.setup`]). If both this property and [`property: TestOptions.storageState`] are specified, this property will always take precedence. ## property: TestOptions.testIdAttribute diff --git a/docs/src/test-api/class-testproject.md b/docs/src/test-api/class-testproject.md index ca1c99d511..eeba8c06ba 100644 --- a/docs/src/test-api/class-testproject.md +++ b/docs/src/test-api/class-testproject.md @@ -165,7 +165,7 @@ Metadata that will be put directly to the test report serialized as JSON. Project name is visible in the report and during test execution. ## property: TestProject.setup -* since: v1.28 +* since: v1.29 - type: ?<[string]|[RegExp]|[Array]<[string]|[RegExp]>> Project setup files that would be executed before all tests in the project. If project setup fails the tests in this project will be skipped. All project setup files will run in every shard if the project is sharded. [`property: TestProject.grep`] and [`property: TestProject.grepInvert`] and their command line counterparts also apply to the setup files. If such filters match only tests in the project Playwright will run all setup files before running the matching tests. diff --git a/docs/src/test-api/class-teststorage.md b/docs/src/test-api/class-teststorage.md new file mode 100644 index 0000000000..cd5d77d354 --- /dev/null +++ b/docs/src/test-api/class-teststorage.md @@ -0,0 +1,56 @@ +# class: TestStorage +* since: v1.29 +* langs: js + +Playwright Test provides a global `storage` object for passing values between project setup and tests. It is +an error to call storage methods outside of setup and tests. + +```js tab=js-js +const { setup, storage } = require('@playwright/test'); + +setup('sign in', async ({ page, context }) => { + // Save signed-in state to an entry named 'github-test-user'. + const contextState = await context.storageState(); + await storage.set('test-user', contextState) +}); +``` + +```js tab=js-ts +import { setup, storage } from '@playwright/test'; + +setup('sign in', async ({ page, context }) => { + // Save signed-in state to an entry named 'github-test-user'. + const contextState = await context.storageState(); + await storage.set('test-user', contextState) +}); +``` + +## async method: TestStorage.get +* since: v1.29 +- returns: <[any]> + +Get named item from the storage. Returns undefined if there is no value with given name. + +### param: TestStorage.get.name +* since: v1.29 +- `name` <[string]> + +Item name. + +## async method: TestStorage.set +* since: v1.29 + +Set value to the storage. + +### param: TestStorage.set.name +* since: v1.29 +- `name` <[string]> + +Item name. + +### param: TestStorage.set.value +* since: v1.29 +- `value` <[any]> + +Item value. The value must be serializable to JSON. Passing `undefined` deletes the entry with given name. + diff --git a/packages/playwright-test/src/index.ts b/packages/playwright-test/src/index.ts index 85f43ef7a4..1872140806 100644 --- a/packages/playwright-test/src/index.ts +++ b/packages/playwright-test/src/index.ts @@ -22,11 +22,13 @@ import * as outOfProcess from 'playwright-core/lib/outofprocess'; import { createGuid, debugMode } from 'playwright-core/lib/utils'; import { removeFolders } from 'playwright-core/lib/utils/fileUtils'; import type { Fixtures, PlaywrightTestArgs, PlaywrightTestOptions, PlaywrightWorkerArgs, PlaywrightWorkerOptions, TestInfo, TestType, TraceMode, VideoMode } from '../types/test'; +import { storage as _baseStorage } from './storage'; import type { TestInfoImpl } from './testInfo'; import { rootTestType, _setProjectSetup } from './testType'; export { expect } from './expect'; export { addRunnerPlugin as _addRunnerPlugin } from './plugins'; export const _baseTest: TestType<{}, {}> = rootTestType.test; +export const storage = _baseStorage; if ((process as any)['__pw_initiator__']) { const originalStackTraceLimit = Error.stackTraceLimit; @@ -221,7 +223,7 @@ const playwrightFixtures: Fixtures = ({ if (proxy !== undefined) options.proxy = proxy; if (storageStateName !== undefined) { - const value = await test.info().storage().get(storageStateName); + const value = await storage.get(storageStateName); if (!value) throw new Error(`Cannot find value in the storage for storageStateName: "${storageStateName}"`); options.storageState = value as any; diff --git a/packages/playwright-test/src/storage.ts b/packages/playwright-test/src/storage.ts new file mode 100644 index 0000000000..908072b9c0 --- /dev/null +++ b/packages/playwright-test/src/storage.ts @@ -0,0 +1,54 @@ +/** + * Copyright Microsoft Corporation. All rights reserved. + * + * 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 fs from 'fs'; +import path from 'path'; +import type { TestStorage } from '../types/test'; +import { currentTestInfo } from './globals'; +import { sanitizeForFilePath, trimLongString } from './util'; + +class JsonStorage implements TestStorage { + private _toFilePath(name: string) { + const testInfo = currentTestInfo(); + if (!testInfo) + throw new Error('storage can only be called while test is running'); + const fileName = sanitizeForFilePath(trimLongString(name)) + '.json'; + return path.join(testInfo.config._storageDir, testInfo.project._id, fileName); + } + + async get(name: string) { + const file = this._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 = this._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); + } +} + +export const storage = new JsonStorage(); diff --git a/packages/playwright-test/src/testInfo.ts b/packages/playwright-test/src/testInfo.ts index 35e24792bd..00e52f7f5d 100644 --- a/packages/playwright-test/src/testInfo.ts +++ b/packages/playwright-test/src/testInfo.ts @@ -17,7 +17,7 @@ import fs from 'fs'; import path from 'path'; import { monotonicTime } from 'playwright-core/lib/utils'; -import type { Storage, TestError, TestInfo, TestStatus } from '../types/test'; +import type { TestError, TestInfo, TestStatus } from '../types/test'; import type { WorkerInitParams } from './ipc'; import type { Loader } from './loader'; import type { TestCase } from './test'; @@ -60,7 +60,6 @@ export class TestInfoImpl implements TestInfo { readonly snapshotDir: string; errors: TestError[] = []; currentStep: TestStepInternal | undefined; - private readonly _storage: JsonStorage; get error(): TestError | undefined { return this.errors[0]; @@ -108,7 +107,6 @@ export class TestInfoImpl implements TestInfo { this.expectedStatus = test.expectedStatus; this._timeoutManager = new TimeoutManager(this.project.timeout); - this._storage = new JsonStorage(this); this.outputDir = (() => { const relativeTestFilePath = path.relative(this.project.testDir, test._requireFile.replace(/\.(spec|test)\.(js|ts|mjs)$/, '')); @@ -281,41 +279,6 @@ export class TestInfoImpl implements TestInfo { setTimeout(timeout: number) { this._timeoutManager.setTimeout(timeout); } - - storage() { - return this._storage; - } -} - -class JsonStorage implements Storage { - constructor(private _testInfo: TestInfoImpl) { - } - - private _toFilePath(name: string) { - const fileName = sanitizeForFilePath(trimLongString(name)) + '.json'; - return path.join(this._testInfo.config._storageDir, this._testInfo.project._id, fileName); - } - - async get(name: string) { - const file = this._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 = this._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); - } } class SkipError extends Error { diff --git a/packages/playwright-test/types/test.d.ts b/packages/playwright-test/types/test.d.ts index fdd1d0a709..ae07a12b94 100644 --- a/packages/playwright-test/types/test.d.ts +++ b/packages/playwright-test/types/test.d.ts @@ -1889,11 +1889,6 @@ export interface TestInfo { */ stdout: Array; - /** - * Returns a [Storage] instance for the currently running project. - */ - storage(): Storage; - /** * Timeout in milliseconds for the currently running test. Zero means no timeout. Learn more about * [various timeouts](https://playwright.dev/docs/test-timeouts). @@ -2849,10 +2844,21 @@ type ConnectOptions = { }; /** - * Playwright Test provides a [testInfo.storage()](https://playwright.dev/docs/api/class-testinfo#test-info-storage) - * object for passing values between project setup and tests. TODO: examples + * Playwright Test provides a global `storage` object for passing values between project setup and tests. It is an + * error to call storage methods outside of setup and tests. + * + * ```js + * import { setup, storage } from '@playwright/test'; + * + * setup('sign in', async ({ page, context }) => { + * // Save signed-in state to an entry named 'github-test-user'. + * const contextState = await context.storageState(); + * await storage.set('test-user', contextState) + * }); + * ``` + * */ -export interface Storage { +export interface TestStorage { /** * Get named item from the storage. Returns undefined if there is no value with given name. * @param name Item name. @@ -3102,9 +3108,9 @@ export interface PlaywrightTestOptions { */ storageState: StorageState | undefined; /** - * Name of the [Storage] entry that should be used to initialize + * Name of the [TestStorage] entry that should be used to initialize * [testOptions.storageState](https://playwright.dev/docs/api/class-testoptions#test-options-storage-state). The value - * must be written to the storage before creatiion of a browser context that uses it (usually in + * must be written to the test storage before creation of a browser context that uses it (usually in * [testProject.setup](https://playwright.dev/docs/api/class-testproject#test-project-setup)). If both this property * and [testOptions.storageState](https://playwright.dev/docs/api/class-testoptions#test-options-storage-state) are * specified, this property will always take precedence. @@ -3387,6 +3393,7 @@ export default test; export const _baseTest: TestType<{}, {}>; export const expect: Expect; +export const storage: TestStorage; // This is required to not export everything by default. See https://github.com/Microsoft/TypeScript/issues/19545#issuecomment-340490459 export {}; diff --git a/tests/playwright-test/storage.spec.ts b/tests/playwright-test/storage.spec.ts index caf5e95e13..000bdeb68a 100644 --- a/tests/playwright-test/storage.spec.ts +++ b/tests/playwright-test/storage.spec.ts @@ -22,16 +22,14 @@ test('should provide storage fixture', async ({ runInlineTest }) => { module.exports = {}; `, 'a.test.ts': ` - const { test } = pwt; + const { test, storage } = pwt; test('should store number', async ({ }) => { - const storage = test.info().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 ({ }) => { - const storage = test.info().storage(); expect(storage).toBeTruthy(); expect(await storage.get('object')).toBe(undefined); await storage.set('object', { 'a': 2022 }) @@ -56,9 +54,8 @@ test('should share storage state between project setup and tests', async ({ runI }; `, 'storage.setup.ts': ` - const { setup, expect } = pwt; + const { setup, expect, storage } = pwt; setup('should initialize storage', async ({ }) => { - const storage = setup.info().storage(); expect(await storage.get('number')).toBe(undefined); await storage.set('number', 2022) expect(await storage.get('number')).toBe(2022); @@ -69,17 +66,15 @@ test('should share storage state between project setup and tests', async ({ runI }); `, 'a.test.ts': ` - const { test } = pwt; + const { test, storage } = pwt; test('should get data from setup', async ({ }) => { - const storage = test.info().storage(); expect(await storage.get('number')).toBe(2022); expect(await storage.get('object')).toEqual({ 'a': 2022 }); }); `, 'b.test.ts': ` - const { test } = pwt; + const { test, storage } = pwt; test('should get data from setup', async ({ }) => { - const storage = test.info().storage(); expect(await storage.get('number')).toBe(2022); expect(await storage.get('object')).toEqual({ 'a': 2022 }); }); @@ -95,9 +90,8 @@ test('should persist storage state between project runs', async ({ runInlineTest module.exports = { }; `, 'a.test.ts': ` - const { test } = pwt; + const { test, storage } = pwt; test('should have no data on first run', async ({ }) => { - const storage = test.info().storage(); expect(await storage.get('number')).toBe(undefined); await storage.set('number', 2022) expect(await storage.get('object')).toBe(undefined); @@ -105,9 +99,8 @@ test('should persist storage state between project runs', async ({ runInlineTest }); `, 'b.test.ts': ` - const { test } = pwt; + const { test, storage } = pwt; test('should get data from previous run', async ({ }) => { - const storage = test.info().storage(); expect(await storage.get('number')).toBe(2022); expect(await storage.get('object')).toEqual({ 'a': 2022 }); }); @@ -142,9 +135,8 @@ test('should isolate storage state between projects', async ({ runInlineTest }) }; `, 'storage.setup.ts': ` - const { setup, expect } = pwt; + const { setup, expect, storage } = pwt; setup('should initialize storage', async ({ }) => { - const storage = setup.info().storage(); expect(await storage.get('number')).toBe(undefined); await storage.set('number', 2022) expect(await storage.get('number')).toBe(2022); @@ -155,17 +147,15 @@ test('should isolate storage state between projects', async ({ runInlineTest }) }); `, 'a.test.ts': ` - const { test } = pwt; + const { test, storage } = pwt; test('should get data from setup', async ({ }) => { - const storage = test.info().storage(); expect(await storage.get('number')).toBe(2022); expect(await storage.get('name')).toBe('str-' + test.info().project.name); }); `, 'b.test.ts': ` - const { test } = pwt; + const { test, storage } = pwt; test('should get data from setup', async ({ }) => { - const storage = test.info().storage(); expect(await storage.get('number')).toBe(2022); expect(await storage.get('name')).toBe('str-' + test.info().project.name); }); @@ -192,9 +182,8 @@ test('should load context storageState from storage', async ({ runInlineTest, se }; `, 'storage.setup.ts': ` - const { setup, expect } = pwt; + const { setup, expect, storage } = pwt; setup('should save storageState', async ({ page, context }) => { - const storage = setup.info().storage(); expect(await storage.get('user')).toBe(undefined); await page.goto('${server.PREFIX}/setcookie.html'); const state = await page.context().storageState(); @@ -245,12 +234,11 @@ test('should load storageStateName specified in the project config from storage' }; `, 'storage.setup.ts': ` - const { setup, expect } = pwt; + const { setup, expect, storage } = pwt; setup.use({ storageStateName: ({}, use) => use(undefined), }) setup('should save storageState', async ({ page, context }) => { - const storage = setup.info().storage(); expect(await storage.get('stateInStorage')).toBe(undefined); await page.goto('${server.PREFIX}/setcookie.html'); const state = await page.context().storageState(); @@ -290,12 +278,11 @@ test('should load storageStateName specified in the global config from storage', }; `, 'storage.setup.ts': ` - const { setup, expect } = pwt; + const { setup, expect, storage } = pwt; setup.use({ storageStateName: ({}, use) => use(undefined), }) setup('should save storageStateName', async ({ page, context }) => { - const storage = setup.info().storage(); expect(await storage.get('stateInStorage')).toBe(undefined); await page.goto('${server.PREFIX}/setcookie.html'); const state = await page.context().storageState(); diff --git a/tests/playwright-test/types.spec.ts b/tests/playwright-test/types.spec.ts index 1ef96945f9..3a328edfbc 100644 --- a/tests/playwright-test/types.spec.ts +++ b/tests/playwright-test/types.spec.ts @@ -192,12 +192,12 @@ test('config should allow void/empty options', async ({ runTSC }) => { test('should provide storage interface', async ({ runTSC }) => { const result = await runTSC({ 'a.spec.ts': ` - const { test } = pwt; + const { test, storage } = pwt; test('my test', async () => { - await test.info().storage().set('foo', 'bar'); - const val = await test.info().storage().get('foo'); + await storage.set('foo', 'bar'); + const val = await storage.get('foo'); // @ts-expect-error - await test.info().storage().unknown(); + await storage.unknown(); }); ` }); diff --git a/utils/generate_types/overrides-test.d.ts b/utils/generate_types/overrides-test.d.ts index 65813d0762..03ac7f121f 100644 --- a/utils/generate_types/overrides-test.d.ts +++ b/utils/generate_types/overrides-test.d.ts @@ -196,7 +196,7 @@ type ConnectOptions = { timeout?: number; }; -export interface Storage { +export interface TestStorage { get(name: string): Promise; set(name: string, value: T | undefined): Promise; } @@ -350,6 +350,7 @@ export default test; export const _baseTest: TestType<{}, {}>; export const expect: Expect; +export const storage: TestStorage; // This is required to not export everything by default. See https://github.com/Microsoft/TypeScript/issues/19545#issuecomment-340490459 export {};