feat: make storage a global variable (#19283)
This commit is contained in:
Родитель
f9ef18912d
Коммит
3c1b26b9f1
100
docs/src/auth.md
100
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);
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
@ -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]>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
@ -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<TestFixtures, WorkerFixtures> = ({
|
|||
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;
|
||||
|
|
|
@ -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<T>(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<T>(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();
|
|
@ -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<T>(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<T>(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 {
|
||||
|
|
|
@ -1889,11 +1889,6 @@ export interface TestInfo {
|
|||
*/
|
||||
stdout: Array<string|Buffer>;
|
||||
|
||||
/**
|
||||
* 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 {};
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
`
|
||||
});
|
||||
|
|
|
@ -196,7 +196,7 @@ type ConnectOptions = {
|
|||
timeout?: number;
|
||||
};
|
||||
|
||||
export interface Storage {
|
||||
export interface TestStorage {
|
||||
get<T>(name: string): Promise<T | undefined>;
|
||||
set<T>(name: string, value: T | undefined): Promise<void>;
|
||||
}
|
||||
|
@ -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 {};
|
||||
|
|
Загрузка…
Ссылка в новой задаче