feat: key value store backed by filesystem (#20932)
Keys are used as a relative file path without any sanitization assuming that the underlying fs will throw on error.
This commit is contained in:
parent
eadcab6b9f
commit
09be9d6425
|
|
@ -749,6 +749,34 @@ export default defineConfig({
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## property: TestConfig.storeDir
|
||||||
|
* since: v1.32
|
||||||
|
- type: ?<[string]>
|
||||||
|
|
||||||
|
Directory where the values accessible via [TestStore] are persisted. All pahts in [TestStore] are relative to `storeDir`. Defaults to `./playwright`.
|
||||||
|
|
||||||
|
**Usage**
|
||||||
|
|
||||||
|
```js tab=js-js
|
||||||
|
// playwright.config.js
|
||||||
|
// @ts-check
|
||||||
|
|
||||||
|
const { defineConfig } = require('@playwright/test');
|
||||||
|
|
||||||
|
module.exports = defineConfig({
|
||||||
|
storeDir: './playwright-store',
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
```js tab=js-ts
|
||||||
|
// playwright.config.ts
|
||||||
|
import { defineConfig } from '@playwright/test';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
storeDir: './playwright-store',
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
## property: TestConfig.testDir
|
## property: TestConfig.testDir
|
||||||
* since: v1.10
|
* since: v1.10
|
||||||
- type: ?<[string]>
|
- type: ?<[string]>
|
||||||
|
|
|
||||||
76
docs/src/test-api/class-teststore.md
Normal file
76
docs/src/test-api/class-teststore.md
Normal file
|
|
@ -0,0 +1,76 @@
|
||||||
|
# class: TestStore
|
||||||
|
* since: v1.32
|
||||||
|
* langs: js
|
||||||
|
|
||||||
|
Playwright Test provides a global `store` object that can be used to read/write values on the filesystem. Each value is stored in its own file inside './playwright' directory, configurable with [`property: TestConfig.storeDir`].
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { test, store } from '@playwright/test';
|
||||||
|
|
||||||
|
test('get user name', async ({ page, context }) => {
|
||||||
|
await page.goto('/');
|
||||||
|
// Return mock user info from the store.
|
||||||
|
await page.route('**/info/user', route => route.fulfill({ path: store.path('mocks/user.json')}))
|
||||||
|
await page.getByText('My Profile');
|
||||||
|
// Check that the name matches mock data.
|
||||||
|
await expect(page.getByLabel('Name')).toHaveText('John');
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## async method: TestStore.delete
|
||||||
|
* since: v1.32
|
||||||
|
|
||||||
|
Delete named item from the store. Does nothing if the path is not in the store.
|
||||||
|
|
||||||
|
### param: TestStore.delete.path
|
||||||
|
* since: v1.32
|
||||||
|
- `path` <[string]>
|
||||||
|
|
||||||
|
Item path.
|
||||||
|
|
||||||
|
## async method: TestStore.get
|
||||||
|
* since: v1.32
|
||||||
|
- returns: <[any]>
|
||||||
|
|
||||||
|
Get named item from the store. Returns undefined if there is no value with given path.
|
||||||
|
|
||||||
|
### param: TestStore.get.path
|
||||||
|
* since: v1.32
|
||||||
|
- `path` <[string]>
|
||||||
|
|
||||||
|
Item path.
|
||||||
|
|
||||||
|
## method: TestStore.path
|
||||||
|
* since: v1.32
|
||||||
|
- returns: <[string]>
|
||||||
|
|
||||||
|
Returns absolute path of the corresponding store entry on the file system.
|
||||||
|
|
||||||
|
### param: TestStore.path.path
|
||||||
|
* since: v1.32
|
||||||
|
- `path` <[string]>
|
||||||
|
|
||||||
|
Path of the item in the store.
|
||||||
|
|
||||||
|
## method: TestStore.root
|
||||||
|
* since: v1.32
|
||||||
|
- returns: <[string]>
|
||||||
|
|
||||||
|
Returns absolute path of the store root directory.
|
||||||
|
|
||||||
|
## async method: TestStore.set
|
||||||
|
* since: v1.32
|
||||||
|
|
||||||
|
Set value to the store.
|
||||||
|
|
||||||
|
### param: TestStore.set.path
|
||||||
|
* since: v1.32
|
||||||
|
- `path` <[string]>
|
||||||
|
|
||||||
|
Item path.
|
||||||
|
|
||||||
|
### param: TestStore.set.value
|
||||||
|
* since: v1.32
|
||||||
|
- `value` <[any]>
|
||||||
|
|
||||||
|
Item value. The value must be serializable to JSON. Passing `undefined` deletes the entry with given path.
|
||||||
|
|
@ -22,6 +22,7 @@ import type { ConfigCLIOverrides, SerializedConfig } from './ipc';
|
||||||
import { requireOrImport } from './transform';
|
import { requireOrImport } from './transform';
|
||||||
import type { Config, FullConfigInternal, FullProjectInternal, Project, ReporterDescription } from './types';
|
import type { Config, FullConfigInternal, FullProjectInternal, Project, ReporterDescription } from './types';
|
||||||
import { errorWithFile, getPackageJsonPath, mergeObjects } from '../util';
|
import { errorWithFile, getPackageJsonPath, mergeObjects } from '../util';
|
||||||
|
import { setCurrentConfig } from './globals';
|
||||||
|
|
||||||
export const defaultTimeout = 30000;
|
export const defaultTimeout = 30000;
|
||||||
|
|
||||||
|
|
@ -47,11 +48,13 @@ export class ConfigLoader {
|
||||||
throw new Error('Cannot load two config files');
|
throw new Error('Cannot load two config files');
|
||||||
const config = await requireOrImportDefaultObject(file) as Config;
|
const config = await requireOrImportDefaultObject(file) as Config;
|
||||||
await this._processConfigObject(config, path.dirname(file), file);
|
await this._processConfigObject(config, path.dirname(file), file);
|
||||||
|
setCurrentConfig(this._fullConfig);
|
||||||
return this._fullConfig;
|
return this._fullConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadEmptyConfig(configDir: string): Promise<Config> {
|
async loadEmptyConfig(configDir: string): Promise<Config> {
|
||||||
await this._processConfigObject({}, configDir);
|
await this._processConfigObject({}, configDir);
|
||||||
|
setCurrentConfig(this._fullConfig);
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -102,7 +105,7 @@ export class ConfigLoader {
|
||||||
config.snapshotDir = path.resolve(configDir, config.snapshotDir);
|
config.snapshotDir = path.resolve(configDir, config.snapshotDir);
|
||||||
|
|
||||||
this._fullConfig._internal.configDir = configDir;
|
this._fullConfig._internal.configDir = configDir;
|
||||||
this._fullConfig._internal.storeDir = path.resolve(configDir, '.playwright-store');
|
this._fullConfig._internal.storeDir = path.resolve(configDir, config.storeDir || 'playwright');
|
||||||
this._fullConfig.configFile = configFile;
|
this._fullConfig.configFile = configFile;
|
||||||
this._fullConfig.rootDir = config.testDir || configDir;
|
this._fullConfig.rootDir = config.testDir || configDir;
|
||||||
this._fullConfig._internal.globalOutputDir = takeFirst(config.outputDir, throwawayArtifactsPath, baseFullConfig._internal.globalOutputDir);
|
this._fullConfig._internal.globalOutputDir = takeFirst(config.outputDir, throwawayArtifactsPath, baseFullConfig._internal.globalOutputDir);
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@
|
||||||
|
|
||||||
import type { TestInfoImpl } from '../worker/testInfo';
|
import type { TestInfoImpl } from '../worker/testInfo';
|
||||||
import type { Suite } from './test';
|
import type { Suite } from './test';
|
||||||
|
import type { FullConfigInternal } from './types';
|
||||||
|
|
||||||
let currentTestInfoValue: TestInfoImpl | null = null;
|
let currentTestInfoValue: TestInfoImpl | null = null;
|
||||||
export function setCurrentTestInfo(testInfo: TestInfoImpl | null) {
|
export function setCurrentTestInfo(testInfo: TestInfoImpl | null) {
|
||||||
|
|
@ -52,3 +53,11 @@ export function setIsWorkerProcess() {
|
||||||
export function isWorkerProcess() {
|
export function isWorkerProcess() {
|
||||||
return _isWorkerProcess;
|
return _isWorkerProcess;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let currentConfigValue: FullConfigInternal | null = null;
|
||||||
|
export function setCurrentConfig(config: FullConfigInternal | null) {
|
||||||
|
currentConfigValue = config;
|
||||||
|
}
|
||||||
|
export function currentConfig(): FullConfigInternal | null {
|
||||||
|
return currentConfigValue;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ import type { TestInfoImpl } from './worker/testInfo';
|
||||||
import { rootTestType } from './common/testType';
|
import { rootTestType } from './common/testType';
|
||||||
import { type ContextReuseMode } from './common/types';
|
import { type ContextReuseMode } from './common/types';
|
||||||
export { expect } from './matchers/expect';
|
export { expect } from './matchers/expect';
|
||||||
|
export { store } from './store';
|
||||||
export const _baseTest: TestType<{}, {}> = rootTestType.test;
|
export const _baseTest: TestType<{}, {}> = rootTestType.test;
|
||||||
|
|
||||||
addStackIgnoreFilter((frame: StackFrame) => frame.file.startsWith(path.dirname(require.resolve('../package.json'))));
|
addStackIgnoreFilter((frame: StackFrame) => frame.file.startsWith(path.dirname(require.resolve('../package.json'))));
|
||||||
|
|
|
||||||
61
packages/playwright-test/src/store.ts
Normal file
61
packages/playwright-test/src/store.ts
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
/**
|
||||||
|
* 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 { TestStore } from '../types/test';
|
||||||
|
import { currentConfig } from './common/globals';
|
||||||
|
|
||||||
|
class JsonStore implements TestStore {
|
||||||
|
async delete(name: string) {
|
||||||
|
const file = this.path(name);
|
||||||
|
await fs.promises.rm(file, { force: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
async get<T>(name: string) {
|
||||||
|
const file = this.path(name);
|
||||||
|
try {
|
||||||
|
const data = await fs.promises.readFile(file, 'utf-8');
|
||||||
|
return JSON.parse(data) as T;
|
||||||
|
} catch (e) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
path(name: string): string {
|
||||||
|
return path.join(this.root(), name);
|
||||||
|
}
|
||||||
|
|
||||||
|
root(): string {
|
||||||
|
const config = currentConfig();
|
||||||
|
if (!config)
|
||||||
|
throw new Error('Cannot access store before config is loaded');
|
||||||
|
return config._internal.storeDir;
|
||||||
|
}
|
||||||
|
|
||||||
|
async set<T>(name: string, value: T | undefined) {
|
||||||
|
const file = this.path(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 store = new JsonStore();
|
||||||
55
packages/playwright-test/types/test.d.ts
vendored
55
packages/playwright-test/types/test.d.ts
vendored
|
|
@ -1149,6 +1149,24 @@ interface TestConfig {
|
||||||
*/
|
*/
|
||||||
snapshotPathTemplate?: string;
|
snapshotPathTemplate?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Directory where the values accessible via [TestStore] are persisted. All pahts in [TestStore] are relative to
|
||||||
|
* `storeDir`. Defaults to `./playwright`.
|
||||||
|
*
|
||||||
|
* **Usage**
|
||||||
|
*
|
||||||
|
* ```js
|
||||||
|
* // playwright.config.ts
|
||||||
|
* import { defineConfig } from '@playwright/test';
|
||||||
|
*
|
||||||
|
* export default defineConfig({
|
||||||
|
* storeDir: './playwright-store',
|
||||||
|
* });
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
storeDir?: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Directory that will be recursively scanned for test files. Defaults to the directory of the configuration file.
|
* Directory that will be recursively scanned for test files. Defaults to the directory of the configuration file.
|
||||||
*
|
*
|
||||||
|
|
@ -3304,6 +3322,42 @@ type ConnectOptions = {
|
||||||
timeout?: number;
|
timeout?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Playwright Test provides a global `store` object that can be used to read/write values on the filesystem. Each
|
||||||
|
* value is stored in its own file inside './playwright' directory, configurable with
|
||||||
|
* [testConfig.storeDir](https://playwright.dev/docs/api/class-testconfig#test-config-store-dir).
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export interface TestStore {
|
||||||
|
/**
|
||||||
|
* Get named item from the store. Returns undefined if there is no value with given path.
|
||||||
|
* @param path Item path.
|
||||||
|
*/
|
||||||
|
get<T>(path: string): Promise<T | undefined>;
|
||||||
|
/**
|
||||||
|
* Set value to the store.
|
||||||
|
* @param path Item path.
|
||||||
|
* @param value Item value. The value must be serializable to JSON. Passing `undefined` deletes the entry with given path.
|
||||||
|
*/
|
||||||
|
set<T>(path: string, value: T | undefined): Promise<void>;
|
||||||
|
/**
|
||||||
|
* Delete named item from the store. Does nothing if the path is not in the store.
|
||||||
|
* @param path Item path.
|
||||||
|
*/
|
||||||
|
delete(path: string): Promise<void>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns absolute path of the corresponding store entry on the file system.
|
||||||
|
* @param path Path of the item in the store.
|
||||||
|
*/
|
||||||
|
path(path: string): string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns absolute path of the store root directory.
|
||||||
|
*/
|
||||||
|
root(): string;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Playwright Test provides many options to configure test environment, [Browser], [BrowserContext] and more.
|
* Playwright Test provides many options to configure test environment, [Browser], [BrowserContext] and more.
|
||||||
*
|
*
|
||||||
|
|
@ -4254,6 +4308,7 @@ export default test;
|
||||||
|
|
||||||
export const _baseTest: TestType<{}, {}>;
|
export const _baseTest: TestType<{}, {}>;
|
||||||
export const expect: Expect;
|
export const expect: Expect;
|
||||||
|
export const store: TestStore;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Defines Playwright config
|
* Defines Playwright config
|
||||||
|
|
|
||||||
|
|
@ -178,7 +178,7 @@ test('should override use:browserName with --browser', async ({ runInlineTest })
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should respect context options in various contexts', async ({ runInlineTest }, testInfo) => {
|
test('should respect context options in various contexts', async ({ runInlineTest }) => {
|
||||||
const result = await runInlineTest({
|
const result = await runInlineTest({
|
||||||
'playwright.config.ts': `
|
'playwright.config.ts': `
|
||||||
module.exports = { use: { viewport: { width: 500, height: 500 } } };
|
module.exports = { use: { viewport: { width: 500, height: 500 } } };
|
||||||
|
|
@ -294,7 +294,7 @@ test('should respect headless in modifiers that run before tests', async ({ runI
|
||||||
expect(result.passed).toBe(1);
|
expect(result.passed).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should call logger from launchOptions config', async ({ runInlineTest }, testInfo) => {
|
test('should call logger from launchOptions config', async ({ runInlineTest }) => {
|
||||||
const result = await runInlineTest({
|
const result = await runInlineTest({
|
||||||
'a.test.ts': `
|
'a.test.ts': `
|
||||||
import { test, expect } from '@playwright/test';
|
import { test, expect } from '@playwright/test';
|
||||||
|
|
@ -322,7 +322,7 @@ test('should call logger from launchOptions config', async ({ runInlineTest }, t
|
||||||
expect(result.passed).toBe(1);
|
expect(result.passed).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should report error and pending operations on timeout', async ({ runInlineTest }, testInfo) => {
|
test('should report error and pending operations on timeout', async ({ runInlineTest }) => {
|
||||||
const result = await runInlineTest({
|
const result = await runInlineTest({
|
||||||
'a.test.ts': `
|
'a.test.ts': `
|
||||||
import { test, expect } from '@playwright/test';
|
import { test, expect } from '@playwright/test';
|
||||||
|
|
@ -346,7 +346,7 @@ test('should report error and pending operations on timeout', async ({ runInline
|
||||||
expect(result.output).toContain(`7 | page.getByText('More missing').textContent(),`);
|
expect(result.output).toContain(`7 | page.getByText('More missing').textContent(),`);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should report error on timeout with shared page', async ({ runInlineTest }, testInfo) => {
|
test('should report error on timeout with shared page', async ({ runInlineTest }) => {
|
||||||
const result = await runInlineTest({
|
const result = await runInlineTest({
|
||||||
'a.test.ts': `
|
'a.test.ts': `
|
||||||
import { test, expect } from '@playwright/test';
|
import { test, expect } from '@playwright/test';
|
||||||
|
|
@ -370,7 +370,7 @@ test('should report error on timeout with shared page', async ({ runInlineTest }
|
||||||
expect(result.output).toContain(`11 | await page.getByText('Missing').click();`);
|
expect(result.output).toContain(`11 | await page.getByText('Missing').click();`);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should report error from beforeAll timeout', async ({ runInlineTest }, testInfo) => {
|
test('should report error from beforeAll timeout', async ({ runInlineTest }) => {
|
||||||
const result = await runInlineTest({
|
const result = await runInlineTest({
|
||||||
'a.test.ts': `
|
'a.test.ts': `
|
||||||
import { test, expect } from '@playwright/test';
|
import { test, expect } from '@playwright/test';
|
||||||
|
|
@ -394,7 +394,7 @@ test('should report error from beforeAll timeout', async ({ runInlineTest }, tes
|
||||||
expect(result.output).toContain(`8 | page.getByText('More missing').textContent(),`);
|
expect(result.output).toContain(`8 | page.getByText('More missing').textContent(),`);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should not report waitForEventInfo as pending', async ({ runInlineTest }, testInfo) => {
|
test('should not report waitForEventInfo as pending', async ({ runInlineTest }) => {
|
||||||
const result = await runInlineTest({
|
const result = await runInlineTest({
|
||||||
'a.test.ts': `
|
'a.test.ts': `
|
||||||
import { test, expect } from '@playwright/test';
|
import { test, expect } from '@playwright/test';
|
||||||
|
|
@ -414,7 +414,7 @@ test('should not report waitForEventInfo as pending', async ({ runInlineTest },
|
||||||
expect(result.output).not.toContain('- page.waitForLoadState');
|
expect(result.output).not.toContain('- page.waitForLoadState');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should throw when using page in beforeAll', async ({ runInlineTest }, testInfo) => {
|
test('should throw when using page in beforeAll', async ({ runInlineTest }) => {
|
||||||
const result = await runInlineTest({
|
const result = await runInlineTest({
|
||||||
'a.test.ts': `
|
'a.test.ts': `
|
||||||
import { test, expect } from '@playwright/test';
|
import { test, expect } from '@playwright/test';
|
||||||
|
|
@ -454,7 +454,7 @@ test('should report click error on sigint', async ({ runInlineTest }) => {
|
||||||
expect(result.output).toContain(`5 | const promise = page.click('text=Missing');`);
|
expect(result.output).toContain(`5 | const promise = page.click('text=Missing');`);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should work with video: retain-on-failure', async ({ runInlineTest }, testInfo) => {
|
test('should work with video: retain-on-failure', async ({ runInlineTest }) => {
|
||||||
const result = await runInlineTest({
|
const result = await runInlineTest({
|
||||||
'playwright.config.ts': `
|
'playwright.config.ts': `
|
||||||
module.exports = { use: { video: 'retain-on-failure' }, name: 'chromium' };
|
module.exports = { use: { video: 'retain-on-failure' }, name: 'chromium' };
|
||||||
|
|
@ -478,15 +478,15 @@ test('should work with video: retain-on-failure', async ({ runInlineTest }, test
|
||||||
expect(result.passed).toBe(1);
|
expect(result.passed).toBe(1);
|
||||||
expect(result.failed).toBe(1);
|
expect(result.failed).toBe(1);
|
||||||
|
|
||||||
const dirPass = testInfo.outputPath('test-results', 'a-pass-chromium');
|
const dirPass = test.info().outputPath('test-results', 'a-pass-chromium');
|
||||||
const videoPass = fs.existsSync(dirPass) ? fs.readdirSync(dirPass).find(file => file.endsWith('webm')) : undefined;
|
const videoPass = fs.existsSync(dirPass) ? fs.readdirSync(dirPass).find(file => file.endsWith('webm')) : undefined;
|
||||||
expect(videoPass).toBeFalsy();
|
expect(videoPass).toBeFalsy();
|
||||||
|
|
||||||
const videoFail = fs.readdirSync(testInfo.outputPath('test-results', 'a-fail-chromium')).find(file => file.endsWith('webm'));
|
const videoFail = fs.readdirSync(test.info().outputPath('test-results', 'a-fail-chromium')).find(file => file.endsWith('webm'));
|
||||||
expect(videoFail).toBeTruthy();
|
expect(videoFail).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should work with video: on-first-retry', async ({ runInlineTest }, testInfo) => {
|
test('should work with video: on-first-retry', async ({ runInlineTest }) => {
|
||||||
const result = await runInlineTest({
|
const result = await runInlineTest({
|
||||||
'playwright.config.ts': `
|
'playwright.config.ts': `
|
||||||
module.exports = { use: { video: 'on-first-retry' }, retries: 1, name: 'chromium' };
|
module.exports = { use: { video: 'on-first-retry' }, retries: 1, name: 'chromium' };
|
||||||
|
|
@ -510,13 +510,13 @@ test('should work with video: on-first-retry', async ({ runInlineTest }, testInf
|
||||||
expect(result.passed).toBe(1);
|
expect(result.passed).toBe(1);
|
||||||
expect(result.failed).toBe(1);
|
expect(result.failed).toBe(1);
|
||||||
|
|
||||||
const dirPass = testInfo.outputPath('test-results', 'a-pass-chromium');
|
const dirPass = test.info().outputPath('test-results', 'a-pass-chromium');
|
||||||
expect(fs.existsSync(dirPass)).toBeFalsy();
|
expect(fs.existsSync(dirPass)).toBeFalsy();
|
||||||
|
|
||||||
const dirFail = testInfo.outputPath('test-results', 'a-fail-chromium');
|
const dirFail = test.info().outputPath('test-results', 'a-fail-chromium');
|
||||||
expect(fs.existsSync(dirFail)).toBeFalsy();
|
expect(fs.existsSync(dirFail)).toBeFalsy();
|
||||||
|
|
||||||
const dirRetry = testInfo.outputPath('test-results', 'a-fail-chromium-retry1');
|
const dirRetry = test.info().outputPath('test-results', 'a-fail-chromium-retry1');
|
||||||
const videoFailRetry = fs.readdirSync(dirRetry).find(file => file.endsWith('webm'));
|
const videoFailRetry = fs.readdirSync(dirRetry).find(file => file.endsWith('webm'));
|
||||||
expect(videoFailRetry).toBeTruthy();
|
expect(videoFailRetry).toBeTruthy();
|
||||||
|
|
||||||
|
|
@ -528,7 +528,7 @@ test('should work with video: on-first-retry', async ({ runInlineTest }, testInf
|
||||||
}]);
|
}]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should work with video size', async ({ runInlineTest }, testInfo) => {
|
test('should work with video size', async ({ runInlineTest }) => {
|
||||||
const result = await runInlineTest({
|
const result = await runInlineTest({
|
||||||
'playwright.config.js': `
|
'playwright.config.js': `
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
|
@ -548,7 +548,7 @@ test('should work with video size', async ({ runInlineTest }, testInfo) => {
|
||||||
}, { workers: 1 });
|
}, { workers: 1 });
|
||||||
expect(result.exitCode).toBe(0);
|
expect(result.exitCode).toBe(0);
|
||||||
expect(result.passed).toBe(1);
|
expect(result.passed).toBe(1);
|
||||||
const folder = testInfo.outputPath(`test-results/a-pass-chromium/`);
|
const folder = test.info().outputPath(`test-results/a-pass-chromium/`);
|
||||||
const [file] = fs.readdirSync(folder);
|
const [file] = fs.readdirSync(folder);
|
||||||
const videoPlayer = new VideoPlayer(path.join(folder, file));
|
const videoPlayer = new VideoPlayer(path.join(folder, file));
|
||||||
expect(videoPlayer.videoWidth).toBe(220);
|
expect(videoPlayer.videoWidth).toBe(220);
|
||||||
|
|
@ -601,7 +601,7 @@ test('should pass fixture defaults to tests', async ({ runInlineTest }) => {
|
||||||
expect(result.passed).toBe(1);
|
expect(result.passed).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should not throw with many fixtures set to undefined', async ({ runInlineTest }, testInfo) => {
|
test('should not throw with many fixtures set to undefined', async ({ runInlineTest }) => {
|
||||||
const result = await runInlineTest({
|
const result = await runInlineTest({
|
||||||
'playwright.config.ts': `
|
'playwright.config.ts': `
|
||||||
module.exports = { use: {
|
module.exports = { use: {
|
||||||
|
|
@ -748,3 +748,22 @@ test('should skip on mobile', async ({ runInlineTest }) => {
|
||||||
expect(result.skipped).toBe(1);
|
expect(result.skipped).toBe(1);
|
||||||
expect(result.passed).toBe(1);
|
expect(result.passed).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('fulfill with return path of the entry', async ({ runInlineTest }) => {
|
||||||
|
const storeDir = path.join(test.info().outputPath(), 'playwright');
|
||||||
|
const file = path.join(storeDir, 'foo/body.json');
|
||||||
|
await fs.promises.mkdir(path.dirname(file), { recursive: true });
|
||||||
|
await fs.promises.writeFile(file, JSON.stringify({ 'a': 2023 }));
|
||||||
|
const result = await runInlineTest({
|
||||||
|
'a.test.ts': `
|
||||||
|
import { test, store, expect } from '@playwright/test';
|
||||||
|
test('should read value from path', async ({ page }) => {
|
||||||
|
await page.route('**/*', route => route.fulfill({ path: store.path('foo/body.json')}))
|
||||||
|
await page.goto('http://example.com');
|
||||||
|
expect(await page.textContent('body')).toBe(JSON.stringify({ 'a': 2023 }))
|
||||||
|
});
|
||||||
|
`,
|
||||||
|
}, { workers: 1 });
|
||||||
|
expect(result.exitCode).toBe(0);
|
||||||
|
expect(result.passed).toBe(1);
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -14,10 +14,10 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
import { expect, test } from './playwright-test-fixtures';
|
import { expect, test } from './playwright-test-fixtures';
|
||||||
|
|
||||||
test.fixme(true, 'Restore this');
|
|
||||||
|
|
||||||
test('should provide store fixture', async ({ runInlineTest }) => {
|
test('should provide store fixture', async ({ runInlineTest }) => {
|
||||||
const result = await runInlineTest({
|
const result = await runInlineTest({
|
||||||
'playwright.config.js': `
|
'playwright.config.js': `
|
||||||
|
|
@ -50,14 +50,19 @@ test('should share store state between project setup and tests', async ({ runInl
|
||||||
projects: [
|
projects: [
|
||||||
{
|
{
|
||||||
name: 'p1',
|
name: 'p1',
|
||||||
setupMatch: /.*store.setup.ts/
|
testMatch: /.*store.setup.ts/
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'p2',
|
||||||
|
dependencies: ['p1'],
|
||||||
|
testMatch: /.*.test.ts/
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
`,
|
`,
|
||||||
'store.setup.ts': `
|
'store.setup.ts': `
|
||||||
import { test, store, expect } from '@playwright/test';
|
import { test, store, expect } from '@playwright/test';
|
||||||
test.projectSetup('should initialize store', async ({ }) => {
|
test('should initialize store', async ({ }) => {
|
||||||
expect(await store.get('number')).toBe(undefined);
|
expect(await store.get('number')).toBe(undefined);
|
||||||
await store.set('number', 2022)
|
await store.set('number', 2022)
|
||||||
expect(await store.get('number')).toBe(2022);
|
expect(await store.get('number')).toBe(2022);
|
||||||
|
|
@ -120,53 +125,6 @@ test('should persist store state between project runs', async ({ runInlineTest }
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should isolate store state between projects', async ({ runInlineTest }) => {
|
|
||||||
const result = await runInlineTest({
|
|
||||||
'playwright.config.js': `
|
|
||||||
module.exports = {
|
|
||||||
projects: [
|
|
||||||
{
|
|
||||||
name: 'p1',
|
|
||||||
setupMatch: /.*store.setup.ts/
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'p2',
|
|
||||||
setupMatch: /.*store.setup.ts/
|
|
||||||
}
|
|
||||||
]
|
|
||||||
};
|
|
||||||
`,
|
|
||||||
'store.setup.ts': `
|
|
||||||
import { test, store, expect } from '@playwright/test';
|
|
||||||
test.projectSetup('should initialize store', async ({ }) => {
|
|
||||||
expect(await store.get('number')).toBe(undefined);
|
|
||||||
await store.set('number', 2022)
|
|
||||||
expect(await store.get('number')).toBe(2022);
|
|
||||||
|
|
||||||
expect(await store.get('name')).toBe(undefined);
|
|
||||||
await store.set('name', 'str-' + test.info().project.name)
|
|
||||||
expect(await store.get('name')).toBe('str-' + test.info().project.name);
|
|
||||||
});
|
|
||||||
`,
|
|
||||||
'a.test.ts': `
|
|
||||||
import { test, store, expect } from '@playwright/test';
|
|
||||||
test('should get data from setup', async ({ }) => {
|
|
||||||
expect(await store.get('number')).toBe(2022);
|
|
||||||
expect(await store.get('name')).toBe('str-' + test.info().project.name);
|
|
||||||
});
|
|
||||||
`,
|
|
||||||
'b.test.ts': `
|
|
||||||
import { test, store, expect } from '@playwright/test';
|
|
||||||
test('should get data from setup', async ({ }) => {
|
|
||||||
expect(await store.get('number')).toBe(2022);
|
|
||||||
expect(await store.get('name')).toBe('str-' + test.info().project.name);
|
|
||||||
});
|
|
||||||
`,
|
|
||||||
}, { workers: 2 });
|
|
||||||
expect(result.exitCode).toBe(0);
|
|
||||||
expect(result.passed).toBe(6);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should load context storageState from store', async ({ runInlineTest, server }) => {
|
test('should load context storageState from store', async ({ runInlineTest, server }) => {
|
||||||
server.setRoute('/setcookie.html', (req, res) => {
|
server.setRoute('/setcookie.html', (req, res) => {
|
||||||
res.setHeader('Set-Cookie', ['a=v1']);
|
res.setHeader('Set-Cookie', ['a=v1']);
|
||||||
|
|
@ -177,15 +135,20 @@ test('should load context storageState from store', async ({ runInlineTest, serv
|
||||||
module.exports = {
|
module.exports = {
|
||||||
projects: [
|
projects: [
|
||||||
{
|
{
|
||||||
name: 'p1',
|
name: 'setup',
|
||||||
setupMatch: /.*store.setup.ts/
|
testMatch: /.*store.setup.ts/
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'p2',
|
||||||
|
dependencies: ['setup'],
|
||||||
|
testMatch: /.*.test.ts/
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
`,
|
`,
|
||||||
'store.setup.ts': `
|
'store.setup.ts': `
|
||||||
import { test, store, expect } from '@playwright/test';
|
import { test, store, expect } from '@playwright/test';
|
||||||
test.projectSetup('should save storageState', async ({ page, context }) => {
|
test('should save storageState', async ({ page, context }) => {
|
||||||
expect(await store.get('user')).toBe(undefined);
|
expect(await store.get('user')).toBe(undefined);
|
||||||
await page.goto('${server.PREFIX}/setcookie.html');
|
await page.goto('${server.PREFIX}/setcookie.html');
|
||||||
const state = await page.context().storageState();
|
const state = await page.context().storageState();
|
||||||
|
|
@ -193,9 +156,9 @@ test('should load context storageState from store', async ({ runInlineTest, serv
|
||||||
});
|
});
|
||||||
`,
|
`,
|
||||||
'a.test.ts': `
|
'a.test.ts': `
|
||||||
import { test, expect } from '@playwright/test';
|
import { test, store, expect } from '@playwright/test';
|
||||||
test.use({
|
test.use({
|
||||||
storageStateName: 'user'
|
storageState: async ({}, use) => use(store.get('user'))
|
||||||
})
|
})
|
||||||
test('should get data from setup', async ({ page }) => {
|
test('should get data from setup', async ({ page }) => {
|
||||||
await page.goto('${server.EMPTY_PAGE}');
|
await page.goto('${server.EMPTY_PAGE}');
|
||||||
|
|
@ -216,115 +179,114 @@ test('should load context storageState from store', async ({ runInlineTest, serv
|
||||||
expect(result.passed).toBe(3);
|
expect(result.passed).toBe(3);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should load storageStateName specified in the project config from store', async ({ runInlineTest, server }) => {
|
test('should load value from filesystem', async ({ runInlineTest }) => {
|
||||||
server.setRoute('/setcookie.html', (req, res) => {
|
const storeDir = test.info().outputPath('playwright');
|
||||||
res.setHeader('Set-Cookie', ['a=v1']);
|
const file = path.join(storeDir, 'foo/bar.json');
|
||||||
res.end();
|
await fs.promises.mkdir(path.dirname(file), { recursive: true });
|
||||||
});
|
await fs.promises.writeFile(file, JSON.stringify({ 'a': 2023 }));
|
||||||
const result = await runInlineTest({
|
const result = await runInlineTest({
|
||||||
'playwright.config.js': `
|
'playwright.config.js': `
|
||||||
module.exports = {
|
module.exports = {};
|
||||||
projects: [
|
|
||||||
{
|
|
||||||
name: 'p1',
|
|
||||||
setupMatch: /.*store.setup.ts/,
|
|
||||||
use: {
|
|
||||||
storageStateName: 'stateInStorage',
|
|
||||||
},
|
|
||||||
}
|
|
||||||
]
|
|
||||||
};
|
|
||||||
`,
|
|
||||||
'store.setup.ts': `
|
|
||||||
import { test, store, expect } from '@playwright/test';
|
|
||||||
test.use({
|
|
||||||
storageStateName: ({}, use) => use(undefined),
|
|
||||||
})
|
|
||||||
test.projectSetup('should save storageState', async ({ page, context }) => {
|
|
||||||
expect(await store.get('stateInStorage')).toBe(undefined);
|
|
||||||
await page.goto('${server.PREFIX}/setcookie.html');
|
|
||||||
const state = await page.context().storageState();
|
|
||||||
await store.set('stateInStorage', state);
|
|
||||||
});
|
|
||||||
`,
|
`,
|
||||||
'a.test.ts': `
|
'a.test.ts': `
|
||||||
import { test, expect } from '@playwright/test';
|
import { test, store, expect } from '@playwright/test';
|
||||||
test('should get data from setup', async ({ page }) => {
|
test('should store number', async ({ }) => {
|
||||||
await page.goto('${server.EMPTY_PAGE}');
|
expect(await store.get('foo/bar.json')).toEqual({ 'a': 2023 });
|
||||||
const cookies = await page.evaluate(() => document.cookie);
|
});
|
||||||
expect(cookies).toBe('a=v1');
|
`,
|
||||||
|
}, { workers: 1 });
|
||||||
|
expect(result.exitCode).toBe(0);
|
||||||
|
expect(result.passed).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return root path', async ({ runInlineTest }) => {
|
||||||
|
const storeDir = test.info().outputPath('playwright');
|
||||||
|
const result = await runInlineTest({
|
||||||
|
'playwright.config.js': `
|
||||||
|
module.exports = {};
|
||||||
|
`,
|
||||||
|
'a.test.ts': `
|
||||||
|
import { test, store, expect } from '@playwright/test';
|
||||||
|
test('should store number', async ({ }) => {
|
||||||
|
expect(store.root()).toBe('${storeDir.replace(/\\/g, '\\\\')}');
|
||||||
|
});
|
||||||
|
`,
|
||||||
|
}, { workers: 1 });
|
||||||
|
expect(result.exitCode).toBe(0);
|
||||||
|
expect(result.passed).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should work in global setup and teardown', async ({ runInlineTest }) => {
|
||||||
|
const result = await runInlineTest({
|
||||||
|
'playwright.config.ts': `
|
||||||
|
import * as path from 'path';
|
||||||
|
module.exports = {
|
||||||
|
globalSetup: 'globalSetup.ts',
|
||||||
|
globalTeardown: 'globalTeardown.ts',
|
||||||
|
};
|
||||||
|
`,
|
||||||
|
'globalSetup.ts': `
|
||||||
|
import { store, expect } from '@playwright/test';
|
||||||
|
module.exports = async () => {
|
||||||
|
expect(store).toBeTruthy();
|
||||||
|
await store.set('foo/bar.json', {'a': 2023});
|
||||||
|
};
|
||||||
|
`,
|
||||||
|
'globalTeardown.ts': `
|
||||||
|
import { store, expect } from '@playwright/test';
|
||||||
|
module.exports = async () => {
|
||||||
|
const val = await store.get('foo/bar.json');
|
||||||
|
console.log('teardown=' + val);
|
||||||
|
};
|
||||||
|
`,
|
||||||
|
'a.test.ts': `
|
||||||
|
import { test, store, expect } from '@playwright/test';
|
||||||
|
test('should read value from global setup', async ({ }) => {
|
||||||
|
expect(await store.get('foo/bar.json')).toEqual({ 'a': 2023 });
|
||||||
|
await store.set('foo/bar.json', 'from test');
|
||||||
|
});
|
||||||
|
`,
|
||||||
|
}, { workers: 1 });
|
||||||
|
expect(result.exitCode).toBe(0);
|
||||||
|
expect(result.passed).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('store root can be changed with TestConfig.storeDir', async ({ runInlineTest }) => {
|
||||||
|
const result = await runInlineTest({
|
||||||
|
'playwright.config.ts': `
|
||||||
|
import * as path from 'path';
|
||||||
|
module.exports = {
|
||||||
|
storeDir: 'my/store/dir',
|
||||||
|
};
|
||||||
|
`,
|
||||||
|
'a.test.ts': `
|
||||||
|
import { test, store, expect } from '@playwright/test';
|
||||||
|
test('should store value', async ({ }) => {
|
||||||
|
await store.set('foo/bar.json', {'a': 2023});
|
||||||
|
});
|
||||||
|
test('should read value', async ({ }) => {
|
||||||
|
expect(await store.get('foo/bar.json')).toEqual({ 'a': 2023 });
|
||||||
});
|
});
|
||||||
`,
|
`,
|
||||||
}, { workers: 1 });
|
}, { workers: 1 });
|
||||||
expect(result.exitCode).toBe(0);
|
expect(result.exitCode).toBe(0);
|
||||||
expect(result.passed).toBe(2);
|
expect(result.passed).toBe(2);
|
||||||
|
const file = path.join(test.info().outputPath(), 'my/store/dir/foo/bar.json');
|
||||||
|
expect(JSON.parse(await fs.promises.readFile(file, 'utf-8'))).toEqual({ 'a': 2023 });
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should load storageStateName specified in the global config from store', async ({ runInlineTest, server }) => {
|
test('should delete value', async ({ runInlineTest }) => {
|
||||||
server.setRoute('/setcookie.html', (req, res) => {
|
|
||||||
res.setHeader('Set-Cookie', ['a=v1']);
|
|
||||||
res.end();
|
|
||||||
});
|
|
||||||
const result = await runInlineTest({
|
const result = await runInlineTest({
|
||||||
'playwright.config.js': `
|
|
||||||
module.exports = {
|
|
||||||
use: {
|
|
||||||
storageStateName: 'stateInStorage',
|
|
||||||
},
|
|
||||||
projects: [
|
|
||||||
{
|
|
||||||
name: 'p1',
|
|
||||||
setupMatch: /.*store.setup.ts/,
|
|
||||||
}
|
|
||||||
]
|
|
||||||
};
|
|
||||||
`,
|
|
||||||
'store.setup.ts': `
|
|
||||||
import { test, store, expect } from '@playwright/test';
|
|
||||||
test.use({
|
|
||||||
storageStateName: ({}, use) => use(undefined),
|
|
||||||
})
|
|
||||||
test.projectSetup('should save storageStateName', async ({ page, context }) => {
|
|
||||||
expect(await store.get('stateInStorage')).toBe(undefined);
|
|
||||||
await page.goto('${server.PREFIX}/setcookie.html');
|
|
||||||
const state = await page.context().storageState();
|
|
||||||
await store.set('stateInStorage', state);
|
|
||||||
});
|
|
||||||
`,
|
|
||||||
'a.test.ts': `
|
'a.test.ts': `
|
||||||
import { test, expect } from '@playwright/test';
|
import { test, store, expect } from '@playwright/test';
|
||||||
test('should get data from setup', async ({ page }) => {
|
test('should store value', async ({ }) => {
|
||||||
await page.goto('${server.EMPTY_PAGE}');
|
await store.set('foo/bar.json', {'a': 2023});
|
||||||
const cookies = await page.evaluate(() => document.cookie);
|
expect(await store.get('foo/bar.json')).toEqual({ 'a': 2023 });
|
||||||
expect(cookies).toBe('a=v1');
|
await store.delete('foo/bar.json');
|
||||||
|
expect(await store.get('foo/bar.json')).toBe(undefined);
|
||||||
});
|
});
|
||||||
`,
|
`,
|
||||||
}, { workers: 1 });
|
}, { workers: 1 });
|
||||||
expect(result.exitCode).toBe(0);
|
expect(result.exitCode).toBe(0);
|
||||||
expect(result.passed).toBe(2);
|
expect(result.passed).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should throw on unknown storageStateName value', async ({ runInlineTest, server }) => {
|
|
||||||
const result = await runInlineTest({
|
|
||||||
'playwright.config.js': `
|
|
||||||
module.exports = {
|
|
||||||
projects: [
|
|
||||||
{
|
|
||||||
name: 'p1',
|
|
||||||
use: {
|
|
||||||
storageStateName: 'stateInStorage',
|
|
||||||
},
|
|
||||||
}
|
|
||||||
]
|
|
||||||
};
|
|
||||||
`,
|
|
||||||
'a.test.ts': `
|
|
||||||
import { test, expect } from '@playwright/test';
|
|
||||||
test('should fail to initialize page', async ({ page }) => {
|
|
||||||
});
|
|
||||||
`,
|
|
||||||
}, { workers: 1 });
|
|
||||||
expect(result.exitCode).toBe(1);
|
|
||||||
expect(result.passed).toBe(0);
|
|
||||||
expect(result.output).toContain('Error: Cannot find value in the store for storageStateName: "stateInStorage"');
|
|
||||||
});
|
|
||||||
6
utils/generate_types/overrides-test.d.ts
vendored
6
utils/generate_types/overrides-test.d.ts
vendored
|
|
@ -197,6 +197,11 @@ type ConnectOptions = {
|
||||||
timeout?: number;
|
timeout?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export interface TestStore {
|
||||||
|
get<T>(path: string): Promise<T | undefined>;
|
||||||
|
set<T>(path: string, value: T | undefined): Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
export interface PlaywrightWorkerOptions {
|
export interface PlaywrightWorkerOptions {
|
||||||
browserName: BrowserName;
|
browserName: BrowserName;
|
||||||
defaultBrowserType: BrowserName;
|
defaultBrowserType: BrowserName;
|
||||||
|
|
@ -371,6 +376,7 @@ export default test;
|
||||||
|
|
||||||
export const _baseTest: TestType<{}, {}>;
|
export const _baseTest: TestType<{}, {}>;
|
||||||
export const expect: Expect;
|
export const expect: Expect;
|
||||||
|
export const store: TestStore;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Defines Playwright config
|
* Defines Playwright config
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue