feat(runner): change storage fixture to TestInfo.storage() (#18584)

This commit is contained in:
Yury Semikhatsky 2022-11-04 14:28:25 -07:00 committed by GitHub
parent a9c15a25f8
commit 25dc0bfacb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 148 additions and 82 deletions

View File

@ -128,9 +128,3 @@ test('basic test', async ({ request }) => {
// ...
});
```
## property: Fixtures.storage
* since: v1.28
- type: <[Storage]>
[Storage] is shared between all tests in the same run.

View File

@ -2,14 +2,14 @@
* since: v1.28
* langs: js
Playwright Test provides a `storage` fixture for passing values between project setup and tests.
Playwright Test provides a [`method: TestInfo.storage`] object for passing values between project setup and tests.
TODO: examples
## method: Storage.get
## async method: Storage.get
* since: v1.28
- returns: <[any]>
Get named item from the store.
Get named item from the storage. Returns undefined if there is no value with given name.
### param: Storage.get.name
* since: v1.28
@ -17,10 +17,10 @@ Get named item from the store.
Item name.
## method: Storage.set
## async method: Storage.set
* since: v1.28
Set value to the store.
Set value to the storage.
### param: Storage.set.name
* since: v1.28
@ -32,5 +32,5 @@ Item name.
* since: v1.28
- `value` <[any]>
Item value. The value must be serializable to JSON.
Item value. The value must be serializable to JSON. Passing `undefined` deletes the entry with given name.

View File

@ -498,6 +498,12 @@ 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]>

View File

@ -24,7 +24,6 @@ import { removeFolders } from 'playwright-core/lib/utils/fileUtils';
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;
@ -136,35 +135,6 @@ export const test = _baseTest.extend<TestFixtures, WorkerFixtures>({
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<T>(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<T>(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 }],
@ -216,7 +186,6 @@ export const test = _baseTest.extend<TestFixtures, WorkerFixtures>({
baseURL,
contextOptions,
serviceWorkers,
storage,
}, use) => {
const options: BrowserContextOptions = {};
if (acceptDownloads !== undefined)
@ -252,7 +221,7 @@ export const test = _baseTest.extend<TestFixtures, WorkerFixtures>({
if (storageState !== undefined) {
options.storageState = storageState;
if (typeof storageState === 'string') {
const value = await storage.get(storageState);
const value = await test.info().storage().get(storageState);
if (value)
options.storageState = value as any;
}

View File

@ -121,6 +121,7 @@ export class Loader {
config.snapshotDir = path.resolve(configDir, config.snapshotDir);
this._fullConfig._configDir = configDir;
this._fullConfig._storageDir = path.resolve(configDir, '.playwright-storage');
this._fullConfig.configFile = this._configFile;
this._fullConfig.rootDir = config.testDir || this._configDir;
this._fullConfig._globalOutputDir = takeFirst(config.outputDir, throwawayArtifactsPath, baseFullConfig._globalOutputDir);
@ -669,6 +670,7 @@ export const baseFullConfig: FullConfigInternal = {
_webServers: [],
_globalOutputDir: path.resolve(process.cwd()),
_configDir: '',
_storageDir: '',
_maxConcurrentTestGroups: 0,
_ignoreSnapshots: false,
_workerIsolation: 'isolate-pools',

View File

@ -16,15 +16,14 @@
import fs from 'fs';
import path from 'path';
import type { TestError, TestInfo, TestStatus } from '../types/test';
import type { FullConfigInternal, FullProjectInternal } from './types';
import { monotonicTime } from 'playwright-core/lib/utils';
import type { Storage, TestError, TestInfo, TestStatus } from '../types/test';
import type { WorkerInitParams } from './ipc';
import type { Loader } from './loader';
import type { TestCase } from './test';
import { TimeoutManager } from './timeoutManager';
import type { Annotation, TestStepInternal } from './types';
import type { Annotation, FullConfigInternal, FullProjectInternal, TestStepInternal } from './types';
import { addSuffixToFilePath, getContainedPath, normalizeAndSaveAttachment, sanitizeForFilePath, serializeError, trimLongString } from './util';
import { monotonicTime } from 'playwright-core/lib/utils';
export class TestInfoImpl implements TestInfo {
private _addStepImpl: (data: Omit<TestStepInternal, 'complete'>) => TestStepInternal;
@ -62,6 +61,7 @@ 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];
@ -109,6 +109,7 @@ 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)$/, ''));
@ -279,6 +280,41 @@ 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 {

View File

@ -45,6 +45,7 @@ export interface TestStepInternal {
export interface FullConfigInternal extends FullConfigPublic {
_globalOutputDir: string;
_configDir: string;
_storageDir: string;
_maxConcurrentTestGroups: number;
_ignoreSnapshots: boolean;
_workerIsolation: WorkerIsolation;

View File

@ -1740,6 +1740,11 @@ 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).
@ -2693,18 +2698,19 @@ type ConnectOptions = {
};
/**
* Playwright Test provides a `storage` fixture for passing values between project setup and tests. TODO: examples
* 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
*/
interface Storage {
export interface Storage {
/**
* Get named item from the store.
* Get named item from the storage. Returns undefined if there is no value with given name.
* @param name Item name.
*/
get<T>(name: string): Promise<T | undefined>;
/**
* Set value to the store.
* Set value to the storage.
* @param name Item name.
* @param value Item value. The value must be serializable to JSON.
* @param value Item value. The value must be serializable to JSON. Passing `undefined` deletes the entry with given name.
*/
set<T>(name: string, value: T | undefined): Promise<void>;
}
@ -3045,10 +3051,6 @@ 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;
}
/**

View File

@ -23,13 +23,15 @@ test('should provide storage fixture', async ({ runInlineTest }) => {
`,
'a.test.ts': `
const { test } = pwt;
test('should store number', async ({ storage }) => {
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 ({ storage }) => {
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 })
@ -41,7 +43,6 @@ test('should provide storage fixture', async ({ runInlineTest }) => {
expect(result.passed).toBe(2);
});
test('should share storage state between project setup and tests', async ({ runInlineTest }) => {
const result = await runInlineTest({
'playwright.config.js': `
@ -56,7 +57,8 @@ test('should share storage state between project setup and tests', async ({ runI
`,
'storage.setup.ts': `
const { test, expect } = pwt;
test('should initialize storage', async ({ storage }) => {
test('should initialize storage', async ({ }) => {
const storage = test.info().storage();
expect(await storage.get('number')).toBe(undefined);
await storage.set('number', 2022)
expect(await storage.get('number')).toBe(2022);
@ -68,14 +70,16 @@ test('should share storage state between project setup and tests', async ({ runI
`,
'a.test.ts': `
const { test } = pwt;
test('should get data from setup', async ({ storage }) => {
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;
test('should get data from setup', async ({ storage }) => {
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 });
});
@ -85,6 +89,41 @@ test('should share storage state between project setup and tests', async ({ runI
expect(result.passed).toBe(3);
});
test('should persist storage state between project runs', async ({ runInlineTest }) => {
const files = {
'playwright.config.js': `
module.exports = { };
`,
'a.test.ts': `
const { test } = 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);
await storage.set('object', { 'a': 2022 })
});
`,
'b.test.ts': `
const { test } = 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 });
});
`,
};
{
const result = await runInlineTest(files, { grep: 'should have no data on first run' });
expect(result.exitCode).toBe(0);
expect(result.passed).toBe(1);
}
{
const result = await runInlineTest(files, { grep: 'should get data from previous run' });
expect(result.exitCode).toBe(0);
expect(result.passed).toBe(1);
}
});
test('should isolate storage state between projects', async ({ runInlineTest }) => {
const result = await runInlineTest({
@ -104,28 +143,31 @@ test('should isolate storage state between projects', async ({ runInlineTest })
`,
'storage.setup.ts': `
const { test, expect } = pwt;
test('should initialize storage', async ({ storage }, testInfo) => {
test('should initialize storage', async ({ }) => {
const storage = test.info().storage();
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);
await storage.set('name', 'str-' + test.info().project.name)
expect(await storage.get('name')).toBe('str-' + test.info().project.name);
});
`,
'a.test.ts': `
const { test } = pwt;
test('should get data from setup', async ({ storage }, testInfo) => {
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-' + testInfo.project.name);
expect(await storage.get('name')).toBe('str-' + test.info().project.name);
});
`,
'b.test.ts': `
const { test } = pwt;
test('should get data from setup', async ({ storage }, testInfo) => {
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-' + testInfo.project.name);
expect(await storage.get('name')).toBe('str-' + test.info().project.name);
});
`,
}, { workers: 2 });
@ -133,7 +175,6 @@ test('should isolate storage state between projects', async ({ runInlineTest })
expect(result.passed).toBe(6);
});
test('should load context storageState from storage', async ({ runInlineTest, server }) => {
server.setRoute('/setcookie.html', (req, res) => {
res.setHeader('Set-Cookie', ['a=v1']);
@ -152,7 +193,8 @@ test('should load context storageState from storage', async ({ runInlineTest, se
`,
'storage.setup.ts': `
const { test, expect } = pwt;
test('should save storageState', async ({ page, context, storage }, testInfo) => {
test('should save storageState', async ({ page, context }) => {
const storage = test.info().storage();
expect(await storage.get('user')).toBe(undefined);
await page.goto('${server.PREFIX}/setcookie.html');
const state = await page.context().storageState();
@ -164,7 +206,7 @@ test('should load context storageState from storage', async ({ runInlineTest, se
test.use({
storageState: 'user'
})
test('should get data from setup', async ({ page }, testInfo) => {
test('should get data from setup', async ({ page }) => {
await page.goto('${server.EMPTY_PAGE}');
const cookies = await page.evaluate(() => document.cookie);
expect(cookies).toBe('a=v1');
@ -172,7 +214,7 @@ test('should load context storageState from storage', async ({ runInlineTest, se
`,
'b.test.ts': `
const { test } = pwt;
test('should not get data from setup if not configured', async ({ page }, testInfo) => {
test('should not get data from setup if not configured', async ({ page }) => {
await page.goto('${server.EMPTY_PAGE}');
const cookies = await page.evaluate(() => document.cookie);
expect(cookies).toBe('');
@ -207,17 +249,17 @@ test('should load storageState specified in the project config from storage', as
test.reset({
storageState: 'default'
})
test('should save storageState', async ({ page, context, storage }, testInfo) => {
test('should save storageState', async ({ page, context }) => {
const storage = test.info().storage();
expect(await storage.get('stateInStorage')).toBe(undefined);
await page.goto('${server.PREFIX}/setcookie.html');
const state = await page.context().storageState();
await storage.set('stateInStorage', state);
console.log('project setup state = ' + state);
});
`,
'a.test.ts': `
const { test } = pwt;
test('should get data from setup', async ({ page, storage }, testInfo) => {
test('should get data from setup', async ({ page }) => {
await page.goto('${server.EMPTY_PAGE}');
const cookies = await page.evaluate(() => document.cookie);
expect(cookies).toBe('a=v1');
@ -252,17 +294,17 @@ test('should load storageState specified in the global config from storage', asy
test.reset({
storageState: 'default'
})
test('should save storageState', async ({ page, context, storage }, testInfo) => {
test('should save storageState', async ({ page, context }) => {
const storage = test.info().storage();
expect(await storage.get('stateInStorage')).toBe(undefined);
await page.goto('${server.PREFIX}/setcookie.html');
const state = await page.context().storageState();
await storage.set('stateInStorage', state);
console.log('project setup state = ' + state);
});
`,
'a.test.ts': `
const { test } = pwt;
test('should get data from setup', async ({ page, storage }, testInfo) => {
test('should get data from setup', async ({ page }) => {
await page.goto('${server.EMPTY_PAGE}');
const cookies = await page.evaluate(() => document.cookie);
expect(cookies).toBe('a=v1');

View File

@ -195,3 +195,18 @@ test('config should allow void/empty options', async ({ runTSC }) => {
});
expect(result.exitCode).toBe(0);
});
test('should provide storage interface', async ({ runTSC }) => {
const result = await runTSC({
'a.spec.ts': `
const { test } = pwt;
test('my test', async () => {
await test.info().storage().set('foo', 'bar');
const val = await test.info().storage().get('foo');
// @ts-expect-error
await test.info().storage().unknown();
});
`
});
expect(result.exitCode).toBe(0);
});

View File

@ -198,7 +198,7 @@ type ConnectOptions = {
timeout?: number;
};
interface Storage {
export interface Storage {
get<T>(name: string): Promise<T | undefined>;
set<T>(name: string, value: T | undefined): Promise<void>;
}
@ -250,7 +250,6 @@ export interface PlaywrightTestOptions {
export interface PlaywrightWorkerArgs {
playwright: typeof import('playwright-core');
browser: Browser;
storage: Storage;
}
export interface PlaywrightTestArgs {