@augment-vir/test 31.49.0 → 31.50.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/augments/test-playwright.d.ts +56 -0
- package/dist/augments/test-playwright.js +67 -0
- package/dist/augments/universal-testing-suite/it-cases-with-context.d.ts +1 -1
- package/dist/augments/universal-testing-suite/universal-describe.js +19 -5
- package/dist/augments/universal-testing-suite/universal-it.d.ts +1 -1
- package/dist/augments/universal-testing-suite/universal-it.js +105 -2
- package/dist/augments/universal-testing-suite/universal-snapshot.d.ts +1 -1
- package/dist/augments/universal-testing-suite/universal-snapshot.js +10 -5
- package/dist/augments/universal-testing-suite/universal-test-context.d.ts +65 -15
- package/dist/augments/universal-testing-suite/universal-test-context.js +40 -6
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/test-playwright/all-visible.d.ts +8 -0
- package/dist/test-playwright/all-visible.js +18 -0
- package/dist/test-playwright/enter-text.d.ts +10 -0
- package/dist/test-playwright/enter-text.js +11 -0
- package/dist/test-playwright/get-option.d.ts +16 -0
- package/dist/test-playwright/get-option.js +16 -0
- package/dist/test-playwright/has-class.d.ts +7 -0
- package/dist/test-playwright/has-class.js +12 -0
- package/dist/test-playwright/local-storage.d.ts +7 -0
- package/dist/test-playwright/local-storage.js +12 -0
- package/dist/test-playwright/nav.d.ts +34 -0
- package/dist/test-playwright/nav.js +48 -0
- package/dist/test-playwright/new-page-or-download.d.ts +14 -0
- package/dist/test-playwright/new-page-or-download.js +25 -0
- package/dist/test-playwright/screenshot.d.ts +87 -0
- package/dist/test-playwright/screenshot.js +159 -0
- package/package.json +20 -7
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { RuntimeEnvError } from '@augment-vir/core';
|
|
2
|
+
export { type MenuOptionOptions } from '../test-playwright/get-option.js';
|
|
3
|
+
export { playwrightTeatNameUrlParam, type NavPath } from '../test-playwright/nav.js';
|
|
4
|
+
export { type LocatorScreenshotOptions, type SaveScreenshotOptions, type TakeScreenshotOptions, } from '../test-playwright/screenshot.js';
|
|
5
|
+
declare function importPlaywrightTestApi(this: void): Promise<RuntimeEnvError | {
|
|
6
|
+
navigation: {
|
|
7
|
+
/** Navigate to a URL in Playwright via given paths. */
|
|
8
|
+
navigateTo: typeof import("../test-playwright/nav.js").navigateTo;
|
|
9
|
+
extractNavUrl: typeof import("../test-playwright/nav.js").extractNavUrl;
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Expects that all matches for the given locator are either visible or hidden (controlled
|
|
13
|
+
* by `isVisible`).
|
|
14
|
+
*/
|
|
15
|
+
expectAllVisible: typeof import("../test-playwright/all-visible.js").expectAllVisible;
|
|
16
|
+
/** Clicks a label to select its input and then types the given text. */
|
|
17
|
+
enterTextByLabel: typeof import("../test-playwright/enter-text").enterTextByLabel;
|
|
18
|
+
/** Find the matching (or first) element with the "option" role. */
|
|
19
|
+
getMenuOption: typeof import("../test-playwright/get-option.js").getMenuOption;
|
|
20
|
+
/** Checks if a locator contains the given class. */
|
|
21
|
+
checkHasClass: typeof import("../test-playwright/has-class").checkHasClass;
|
|
22
|
+
/**
|
|
23
|
+
* Run the trigger and catch a new page _or_ a new download (sometimes Playwright
|
|
24
|
+
* inconsistently chooses on or the other).
|
|
25
|
+
*/
|
|
26
|
+
handleNewPageOrDownload: typeof import("../test-playwright/new-page-or-download").handleNewPageOrDownload;
|
|
27
|
+
/** Read from a page's local storage (using `page.evaluate`). */
|
|
28
|
+
readLocalStorage: typeof import("../test-playwright/local-storage.js").readLocalStorage;
|
|
29
|
+
/** Screenshot methods. */
|
|
30
|
+
screenshot: {
|
|
31
|
+
/**
|
|
32
|
+
* Similar to Playwright's `expect().toHaveScreenshot` but allows images to have
|
|
33
|
+
* different sizes and has default comparison threshold options that are wide enough to
|
|
34
|
+
* allow testing between different operating systems without failure (usually).
|
|
35
|
+
*/
|
|
36
|
+
expectScreenshot: typeof import("../test-playwright/screenshot.js").expectScreenshot;
|
|
37
|
+
/** Get the path to save the given screenshot file name to. */
|
|
38
|
+
getScreenshotPath: typeof import("../test-playwright/screenshot.js").getScreenshotPath;
|
|
39
|
+
/**
|
|
40
|
+
* Take and immediately save a screenshot.
|
|
41
|
+
*
|
|
42
|
+
* @returns The path that the screenshot was saved to.
|
|
43
|
+
*/
|
|
44
|
+
takeScreenshot: typeof import("../test-playwright/screenshot.js").takeScreenshot;
|
|
45
|
+
};
|
|
46
|
+
}>;
|
|
47
|
+
/**
|
|
48
|
+
* A suite of Playwright test helpers. This is only accessible within a Playwright test runtime. If
|
|
49
|
+
* accessed outside of a Playwright runtime, it'll be an Error instead of a collection of test
|
|
50
|
+
* helpers.
|
|
51
|
+
*
|
|
52
|
+
* @category Test
|
|
53
|
+
* @category Package : @augment-vir/test
|
|
54
|
+
* @package [`@augment-vir/test`](https://www.npmjs.com/package/@augment-vir/test)
|
|
55
|
+
*/
|
|
56
|
+
export declare const testPlaywright: Exclude<Awaited<ReturnType<typeof importPlaywrightTestApi>>, Error>;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { isInsidePlaywrightTest, RuntimeEnvError } from '@augment-vir/core';
|
|
2
|
+
export { playwrightTeatNameUrlParam } from '../test-playwright/nav.js';
|
|
3
|
+
async function importPlaywrightTestApi() {
|
|
4
|
+
if (!isInsidePlaywrightTest()) {
|
|
5
|
+
return new RuntimeEnvError("The 'testPlaywright' api cannot be used outside of a Playwright test context.");
|
|
6
|
+
}
|
|
7
|
+
const { checkHasClass } = await import('../test-playwright/has-class');
|
|
8
|
+
const { enterTextByLabel } = await import('../test-playwright/enter-text');
|
|
9
|
+
const { expectAllVisible } = await import('../test-playwright/all-visible.js');
|
|
10
|
+
const { getMenuOption } = await import('../test-playwright/get-option');
|
|
11
|
+
const { getScreenshotPath, takeScreenshot, expectScreenshot } = await import('../test-playwright/screenshot.js');
|
|
12
|
+
const { handleNewPageOrDownload } = await import('../test-playwright/new-page-or-download');
|
|
13
|
+
const { navigateTo, extractNavUrl } = await import('../test-playwright/nav.js');
|
|
14
|
+
const { readLocalStorage } = await import('../test-playwright/local-storage.js');
|
|
15
|
+
return {
|
|
16
|
+
navigation: {
|
|
17
|
+
/** Navigate to a URL in Playwright via given paths. */
|
|
18
|
+
navigateTo,
|
|
19
|
+
extractNavUrl,
|
|
20
|
+
},
|
|
21
|
+
/**
|
|
22
|
+
* Expects that all matches for the given locator are either visible or hidden (controlled
|
|
23
|
+
* by `isVisible`).
|
|
24
|
+
*/
|
|
25
|
+
expectAllVisible,
|
|
26
|
+
/** Clicks a label to select its input and then types the given text. */
|
|
27
|
+
enterTextByLabel,
|
|
28
|
+
/** Find the matching (or first) element with the "option" role. */
|
|
29
|
+
getMenuOption,
|
|
30
|
+
/** Checks if a locator contains the given class. */
|
|
31
|
+
checkHasClass,
|
|
32
|
+
/**
|
|
33
|
+
* Run the trigger and catch a new page _or_ a new download (sometimes Playwright
|
|
34
|
+
* inconsistently chooses on or the other).
|
|
35
|
+
*/
|
|
36
|
+
handleNewPageOrDownload,
|
|
37
|
+
/** Read from a page's local storage (using `page.evaluate`). */
|
|
38
|
+
readLocalStorage,
|
|
39
|
+
/** Screenshot methods. */
|
|
40
|
+
screenshot: {
|
|
41
|
+
/**
|
|
42
|
+
* Similar to Playwright's `expect().toHaveScreenshot` but allows images to have
|
|
43
|
+
* different sizes and has default comparison threshold options that are wide enough to
|
|
44
|
+
* allow testing between different operating systems without failure (usually).
|
|
45
|
+
*/
|
|
46
|
+
expectScreenshot,
|
|
47
|
+
/** Get the path to save the given screenshot file name to. */
|
|
48
|
+
getScreenshotPath,
|
|
49
|
+
/**
|
|
50
|
+
* Take and immediately save a screenshot.
|
|
51
|
+
*
|
|
52
|
+
* @returns The path that the screenshot was saved to.
|
|
53
|
+
*/
|
|
54
|
+
takeScreenshot,
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* A suite of Playwright test helpers. This is only accessible within a Playwright test runtime. If
|
|
60
|
+
* accessed outside of a Playwright runtime, it'll be an Error instead of a collection of test
|
|
61
|
+
* helpers.
|
|
62
|
+
*
|
|
63
|
+
* @category Test
|
|
64
|
+
* @category Package : @augment-vir/test
|
|
65
|
+
* @package [`@augment-vir/test`](https://www.npmjs.com/package/@augment-vir/test)
|
|
66
|
+
*/
|
|
67
|
+
export const testPlaywright = (await importPlaywrightTestApi());
|
|
@@ -9,7 +9,7 @@ import { type UniversalTestContext } from './universal-test-context.js';
|
|
|
9
9
|
* @category Package : @augment-vir/test
|
|
10
10
|
* @package [`@augment-vir/test`](https://www.npmjs.com/package/@augment-vir/test)
|
|
11
11
|
*/
|
|
12
|
-
export type BaseFunctionWithContext = (testContext: UniversalTestContext
|
|
12
|
+
export type BaseFunctionWithContext = (testContext: Readonly<UniversalTestContext>, ...args: any[]) => any;
|
|
13
13
|
/**
|
|
14
14
|
* Input for a test function with context that only has a single input.
|
|
15
15
|
*
|
|
@@ -1,8 +1,22 @@
|
|
|
1
|
-
import { isRuntimeEnv, RuntimeEnv } from '@augment-vir/core';
|
|
1
|
+
import { isInsidePlaywrightTest, isRuntimeEnv, RuntimeEnv } from '@augment-vir/core';
|
|
2
2
|
const describes = isRuntimeEnv(RuntimeEnv.Node)
|
|
3
|
-
?
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
? isInsidePlaywrightTest()
|
|
4
|
+
? {
|
|
5
|
+
playwright: await (async () => {
|
|
6
|
+
const playwrightImport = await import('@playwright/test');
|
|
7
|
+
/** `as any` cast to prevent type guarding {@link playwrightImport}. */
|
|
8
|
+
if ('default' in playwrightImport) {
|
|
9
|
+
return playwrightImport.default.describe;
|
|
10
|
+
}
|
|
11
|
+
else {
|
|
12
|
+
return playwrightImport
|
|
13
|
+
.describe;
|
|
14
|
+
}
|
|
15
|
+
})(),
|
|
16
|
+
}
|
|
17
|
+
: {
|
|
18
|
+
node: (await import('node:test')).describe,
|
|
19
|
+
}
|
|
6
20
|
: {
|
|
7
21
|
mocha: globalThis.describe,
|
|
8
22
|
};
|
|
@@ -33,4 +47,4 @@ const describes = isRuntimeEnv(RuntimeEnv.Node)
|
|
|
33
47
|
*
|
|
34
48
|
* @package [`@augment-vir/test`](https://www.npmjs.com/package/@augment-vir/test)
|
|
35
49
|
*/
|
|
36
|
-
export const describe = describes.mocha || describes.node;
|
|
50
|
+
export const describe = describes.mocha || describes.playwright || describes.node;
|
|
@@ -10,7 +10,7 @@ import { type UniversalTestContext } from './universal-test-context.js';
|
|
|
10
10
|
* @category Package : @augment-vir/test
|
|
11
11
|
* @package [`@augment-vir/test`](https://www.npmjs.com/package/@augment-vir/test)
|
|
12
12
|
*/
|
|
13
|
-
export type UniversalItCallback = (this: void, context: UniversalTestContext) => Promise<void> | void;
|
|
13
|
+
export type UniversalItCallback = (this: void, context: Readonly<UniversalTestContext>) => Promise<void> | void;
|
|
14
14
|
/**
|
|
15
15
|
* A minimal interface for {@link it}. This is used in {@link UniversalIt}.
|
|
16
16
|
*
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { randomString } from '@augment-vir/common';
|
|
2
|
+
import { isInsidePlaywrightTest, isRuntimeEnv, RuntimeEnv } from '@augment-vir/core';
|
|
2
3
|
function createWebIt() {
|
|
3
4
|
const webIt = Object.assign((doesThis, callback) => {
|
|
4
5
|
return globalThis.it(doesThis, async function () {
|
|
@@ -21,6 +22,106 @@ function createWebIt() {
|
|
|
21
22
|
});
|
|
22
23
|
return webIt;
|
|
23
24
|
}
|
|
25
|
+
async function createPlaywrightIt() {
|
|
26
|
+
const rawPlaywrightImport = await import('@playwright/test');
|
|
27
|
+
const originalPlaywrightIt = 'default' in rawPlaywrightImport
|
|
28
|
+
? rawPlaywrightImport.default
|
|
29
|
+
: rawPlaywrightImport;
|
|
30
|
+
/**
|
|
31
|
+
* Right now this wrapper nukes Playwright's file detection. See
|
|
32
|
+
* https://github.com/microsoft/playwright/issues/23157#issuecomment-1574955057 for possible
|
|
33
|
+
* help.
|
|
34
|
+
*/
|
|
35
|
+
const playwrightIt = Object.assign((doesThis, callback) => {
|
|
36
|
+
return originalPlaywrightIt(doesThis, async ({ page, baseURL, browser, context, extraHTTPHeaders, viewport, video, userAgent, timezoneId, serviceWorkers, screenshot, isMobile, headless, hasTouch, }, testInfo) => {
|
|
37
|
+
const playwrightTestContext = {
|
|
38
|
+
page,
|
|
39
|
+
baseURL,
|
|
40
|
+
browser,
|
|
41
|
+
context,
|
|
42
|
+
extraHTTPHeaders,
|
|
43
|
+
viewport,
|
|
44
|
+
video,
|
|
45
|
+
userAgent,
|
|
46
|
+
timezoneId,
|
|
47
|
+
serviceWorkers,
|
|
48
|
+
screenshot,
|
|
49
|
+
isMobile,
|
|
50
|
+
headless,
|
|
51
|
+
hasTouch,
|
|
52
|
+
testInfo,
|
|
53
|
+
testName: {
|
|
54
|
+
clean: testInfo.titlePath.join(' > '),
|
|
55
|
+
unique: [
|
|
56
|
+
...testInfo.titlePath,
|
|
57
|
+
randomString(),
|
|
58
|
+
].join(' > '),
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
await callback(playwrightTestContext);
|
|
62
|
+
});
|
|
63
|
+
}, {
|
|
64
|
+
skip: (doesThis, callback) => {
|
|
65
|
+
return originalPlaywrightIt.skip(doesThis, async ({ page, baseURL, browser, context, extraHTTPHeaders, viewport, video, userAgent, timezoneId, serviceWorkers, screenshot, isMobile, headless, hasTouch, }, testInfo) => {
|
|
66
|
+
const playwrightTestContext = {
|
|
67
|
+
page,
|
|
68
|
+
baseURL,
|
|
69
|
+
browser,
|
|
70
|
+
context,
|
|
71
|
+
extraHTTPHeaders,
|
|
72
|
+
viewport,
|
|
73
|
+
video,
|
|
74
|
+
userAgent,
|
|
75
|
+
timezoneId,
|
|
76
|
+
serviceWorkers,
|
|
77
|
+
screenshot,
|
|
78
|
+
isMobile,
|
|
79
|
+
headless,
|
|
80
|
+
hasTouch,
|
|
81
|
+
testInfo,
|
|
82
|
+
testName: {
|
|
83
|
+
clean: testInfo.titlePath.join(' > '),
|
|
84
|
+
unique: [
|
|
85
|
+
...testInfo.titlePath,
|
|
86
|
+
randomString(),
|
|
87
|
+
].join(' > '),
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
await callback(playwrightTestContext);
|
|
91
|
+
});
|
|
92
|
+
},
|
|
93
|
+
only: (doesThis, callback) => {
|
|
94
|
+
return originalPlaywrightIt.only(doesThis, async ({ page, baseURL, browser, context, extraHTTPHeaders, viewport, video, userAgent, timezoneId, serviceWorkers, screenshot, isMobile, headless, hasTouch, }, testInfo) => {
|
|
95
|
+
const playwrightTestContext = {
|
|
96
|
+
page,
|
|
97
|
+
baseURL,
|
|
98
|
+
browser,
|
|
99
|
+
context,
|
|
100
|
+
extraHTTPHeaders,
|
|
101
|
+
viewport,
|
|
102
|
+
video,
|
|
103
|
+
userAgent,
|
|
104
|
+
timezoneId,
|
|
105
|
+
serviceWorkers,
|
|
106
|
+
screenshot,
|
|
107
|
+
isMobile,
|
|
108
|
+
headless,
|
|
109
|
+
hasTouch,
|
|
110
|
+
testInfo,
|
|
111
|
+
testName: {
|
|
112
|
+
clean: testInfo.titlePath.join(' > '),
|
|
113
|
+
unique: [
|
|
114
|
+
...testInfo.titlePath,
|
|
115
|
+
randomString(),
|
|
116
|
+
].join(' > '),
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
await callback(playwrightTestContext);
|
|
120
|
+
});
|
|
121
|
+
},
|
|
122
|
+
});
|
|
123
|
+
return playwrightIt;
|
|
124
|
+
}
|
|
24
125
|
/**
|
|
25
126
|
* A single test declaration. This can be used in both web tests _and_ node tests, so you only have
|
|
26
127
|
* import from a single place and learn a single interface.
|
|
@@ -50,5 +151,7 @@ function createWebIt() {
|
|
|
50
151
|
* @package [`@augment-vir/test`](https://www.npmjs.com/package/@augment-vir/test)
|
|
51
152
|
*/
|
|
52
153
|
export const it = isRuntimeEnv(RuntimeEnv.Node)
|
|
53
|
-
? (
|
|
154
|
+
? isInsidePlaywrightTest()
|
|
155
|
+
? await createPlaywrightIt()
|
|
156
|
+
: (await import('node:test')).it
|
|
54
157
|
: createWebIt();
|
|
@@ -22,4 +22,4 @@ export declare class SnapshotFileMissingError extends Error {
|
|
|
22
22
|
* @category Package : @augment-vir/test
|
|
23
23
|
* @package [`@augment-vir/test`](https://www.npmjs.com/package/@augment-vir/test)
|
|
24
24
|
*/
|
|
25
|
-
export declare function assertSnapshot(this: void, testContext: UniversalTestContext
|
|
25
|
+
export declare function assertSnapshot(this: void, testContext: Readonly<UniversalTestContext>, data: unknown): Promise<void>;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { check } from '@augment-vir/assert';
|
|
2
|
-
import { extractErrorMessage, getOrSet
|
|
3
|
-
import { extractTestName, isTestContext, } from './universal-test-context.js';
|
|
2
|
+
import { extractErrorMessage, getOrSet } from '@augment-vir/common';
|
|
3
|
+
import { determineTestContextEnv, extractTestName, isTestContext, TestEnv, } from './universal-test-context.js';
|
|
4
4
|
/**
|
|
5
5
|
* An error that is thrown from {@link assertSnapshot} when the snapshot comparison fails due to the
|
|
6
6
|
* snapshot expectation file simply not existing.
|
|
@@ -27,9 +27,9 @@ export class SnapshotFileMissingError extends Error {
|
|
|
27
27
|
* @package [`@augment-vir/test`](https://www.npmjs.com/package/@augment-vir/test)
|
|
28
28
|
*/
|
|
29
29
|
export async function assertSnapshot(testContext, data) {
|
|
30
|
-
const { snapshotName, testName } = getTestName(testContext);
|
|
31
30
|
const serializedData = check.isString(data) ? data : JSON.stringify(data);
|
|
32
|
-
if (isTestContext(testContext,
|
|
31
|
+
if (isTestContext(testContext, TestEnv.Node)) {
|
|
32
|
+
const { testName } = getTestName(testContext);
|
|
33
33
|
try {
|
|
34
34
|
testContext.assert.snapshot(serializedData);
|
|
35
35
|
}
|
|
@@ -42,7 +42,8 @@ export async function assertSnapshot(testContext, data) {
|
|
|
42
42
|
}
|
|
43
43
|
}
|
|
44
44
|
}
|
|
45
|
-
else {
|
|
45
|
+
else if (isTestContext(testContext, TestEnv.Web)) {
|
|
46
|
+
const { snapshotName, testName } = getTestName(testContext);
|
|
46
47
|
const { SnapshotCommand } = await import('@virmator/test/dist/web-snapshot-plugin/snapshot-payload.js');
|
|
47
48
|
const { executeServerCommand } = await import('@web/test-runner-commands');
|
|
48
49
|
const result = await executeServerCommand(SnapshotCommand.CompareSnapshot, {
|
|
@@ -58,6 +59,10 @@ export async function assertSnapshot(testContext, data) {
|
|
|
58
59
|
}
|
|
59
60
|
}
|
|
60
61
|
}
|
|
62
|
+
else {
|
|
63
|
+
const testEnv = determineTestContextEnv(testContext);
|
|
64
|
+
throw new Error(`assertSnapshot not supported for test env '${testEnv}'.`);
|
|
65
|
+
}
|
|
61
66
|
}
|
|
62
67
|
function getTestName(testContext) {
|
|
63
68
|
const testName = extractTestName(testContext);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { type SelectFrom } from '@augment-vir/common';
|
|
2
|
+
import { type PlaywrightTestArgs, type PlaywrightTestOptions, type PlaywrightWorkerArgs, type PlaywrightWorkerOptions, type TestInfo } from '@playwright/test';
|
|
2
3
|
import { type TestContext as NodeTestContextImport } from 'node:test';
|
|
3
4
|
import { type MochaTestContext } from './mocha-types.js';
|
|
4
|
-
export { RuntimeEnv } from '@augment-vir/core';
|
|
5
5
|
/**
|
|
6
6
|
* The test context for [Node.js's test runner](https://nodejs.org/api/test.html).
|
|
7
7
|
*
|
|
@@ -15,30 +15,72 @@ export type NodeTestContext = Readonly<NodeTestContextImport> & {
|
|
|
15
15
|
[TestName in string]: number;
|
|
16
16
|
};
|
|
17
17
|
};
|
|
18
|
+
/**
|
|
19
|
+
* The test context for Playwright tests.
|
|
20
|
+
*
|
|
21
|
+
* @category Test : Util
|
|
22
|
+
* @category Package : @augment-vir/test
|
|
23
|
+
* @package [`@augment-vir/test`](https://www.npmjs.com/package/@augment-vir/test)
|
|
24
|
+
*/
|
|
25
|
+
export type PlaywrightTestContext = SelectFrom<PlaywrightTestArgs & PlaywrightTestOptions & PlaywrightWorkerArgs & PlaywrightWorkerOptions, {
|
|
26
|
+
page: true;
|
|
27
|
+
baseURL: true;
|
|
28
|
+
browser: true;
|
|
29
|
+
context: true;
|
|
30
|
+
extraHTTPHeaders: true;
|
|
31
|
+
viewport: true;
|
|
32
|
+
video: true;
|
|
33
|
+
userAgent: true;
|
|
34
|
+
timezoneId: true;
|
|
35
|
+
serviceWorkers: true;
|
|
36
|
+
screenshot: true;
|
|
37
|
+
isMobile: true;
|
|
38
|
+
headless: true;
|
|
39
|
+
hasTouch: true;
|
|
40
|
+
}> & {
|
|
41
|
+
testInfo: TestInfo;
|
|
42
|
+
testName: {
|
|
43
|
+
/** Clean, easily readable for humans. */
|
|
44
|
+
clean: string;
|
|
45
|
+
/** Unique with a random slug appended. */
|
|
46
|
+
unique: string;
|
|
47
|
+
};
|
|
48
|
+
};
|
|
18
49
|
/**
|
|
19
50
|
* Test context provided by `it`'s callback.
|
|
20
51
|
*
|
|
21
|
-
* Compatible with both [Node.js's test runner](https://nodejs.org/api/test.html)
|
|
52
|
+
* Compatible with both [Node.js's test runner](https://nodejs.org/api/test.html),
|
|
22
53
|
* [web-test-runner](https://modern-web.dev/docs/test-runner/overview/) or other Mocha-style test
|
|
23
|
-
* runners.
|
|
54
|
+
* runners, and Playwright's test runner.
|
|
55
|
+
*
|
|
56
|
+
* @category Test : Util
|
|
57
|
+
* @category Package : @augment-vir/test
|
|
58
|
+
* @package [`@augment-vir/test`](https://www.npmjs.com/package/@augment-vir/test)
|
|
59
|
+
*/
|
|
60
|
+
export type UniversalTestContext = NodeTestContext | MochaTestContext | PlaywrightTestContext;
|
|
61
|
+
/**
|
|
62
|
+
* Used to determine which test context is in use.
|
|
24
63
|
*
|
|
25
64
|
* @category Test : Util
|
|
26
65
|
* @category Package : @augment-vir/test
|
|
27
66
|
* @package [`@augment-vir/test`](https://www.npmjs.com/package/@augment-vir/test)
|
|
28
67
|
*/
|
|
29
|
-
export
|
|
68
|
+
export declare enum TestEnv {
|
|
69
|
+
Node = "node",
|
|
70
|
+
Web = "web",
|
|
71
|
+
Playwright = "playwright"
|
|
72
|
+
}
|
|
30
73
|
/**
|
|
31
|
-
* Test context by
|
|
32
|
-
* used for Node tests and [web-test-runner](https://modern-web.dev/docs/test-runner/overview/) is
|
|
33
|
-
* used for web tests.
|
|
74
|
+
* Test context by the env they run in.
|
|
34
75
|
*
|
|
35
76
|
* @category Test : Util
|
|
36
77
|
* @category Package : @augment-vir/test
|
|
37
78
|
* @package [`@augment-vir/test`](https://www.npmjs.com/package/@augment-vir/test)
|
|
38
79
|
*/
|
|
39
|
-
export type
|
|
40
|
-
[
|
|
41
|
-
[
|
|
80
|
+
export type TestContextByEnv = {
|
|
81
|
+
[TestEnv.Node]: NodeTestContext;
|
|
82
|
+
[TestEnv.Web]: MochaTestContext;
|
|
83
|
+
[TestEnv.Playwright]: PlaywrightTestContext;
|
|
42
84
|
};
|
|
43
85
|
/**
|
|
44
86
|
* Extracts the full test name (including parent describes) of a given test context. Whether the
|
|
@@ -58,6 +100,14 @@ export declare function extractTestName(testContext: UniversalTestContext): stri
|
|
|
58
100
|
* @package [`@augment-vir/test`](https://www.npmjs.com/package/@augment-vir/test)
|
|
59
101
|
*/
|
|
60
102
|
export declare function extractTestNameAsDir(testContext: UniversalTestContext): string;
|
|
103
|
+
/**
|
|
104
|
+
* Same as {@link extractTestNameAsDir} but sanitizes any input in the same way.
|
|
105
|
+
*
|
|
106
|
+
* @category Test : Util
|
|
107
|
+
* @category Package : @augment-vir/test
|
|
108
|
+
* @package [`@augment-vir/test`](https://www.npmjs.com/package/@augment-vir/test)
|
|
109
|
+
*/
|
|
110
|
+
export declare function cleanTestNameAsDir(testName: string): string;
|
|
61
111
|
/**
|
|
62
112
|
* Asserts that the given context is for the given env and returns that context.
|
|
63
113
|
*
|
|
@@ -66,7 +116,7 @@ export declare function extractTestNameAsDir(testContext: UniversalTestContext):
|
|
|
66
116
|
* @throws `TypeError` if the context does not match the env.
|
|
67
117
|
* @package [`@augment-vir/test`](https://www.npmjs.com/package/@augment-vir/test)
|
|
68
118
|
*/
|
|
69
|
-
export declare function assertWrapTestContext<const SpecificEnv extends
|
|
119
|
+
export declare function assertWrapTestContext<const SpecificEnv extends TestEnv>(this: void, context: Readonly<UniversalTestContext>, env: SpecificEnv): TestContextByEnv[SpecificEnv];
|
|
70
120
|
/**
|
|
71
121
|
* Asserts that the given context is for the given env, otherwise throws an Error.
|
|
72
122
|
*
|
|
@@ -74,7 +124,7 @@ export declare function assertWrapTestContext<const SpecificEnv extends RuntimeE
|
|
|
74
124
|
* @category Package : @augment-vir/test
|
|
75
125
|
* @package [`@augment-vir/test`](https://www.npmjs.com/package/@augment-vir/test)
|
|
76
126
|
*/
|
|
77
|
-
export declare function assertTestContext<const SpecificEnv extends
|
|
127
|
+
export declare function assertTestContext<const SpecificEnv extends TestEnv>(this: void, context: Readonly<UniversalTestContext>, env: SpecificEnv): asserts context is TestContextByEnv[SpecificEnv];
|
|
78
128
|
/**
|
|
79
129
|
* Checks that the given context is for the given env.
|
|
80
130
|
*
|
|
@@ -82,7 +132,7 @@ export declare function assertTestContext<const SpecificEnv extends RuntimeEnv>(
|
|
|
82
132
|
* @category Package : @augment-vir/test
|
|
83
133
|
* @package [`@augment-vir/test`](https://www.npmjs.com/package/@augment-vir/test)
|
|
84
134
|
*/
|
|
85
|
-
export declare function isTestContext<const SpecificEnv extends
|
|
135
|
+
export declare function isTestContext<const SpecificEnv extends TestEnv>(this: void, context: Readonly<UniversalTestContext>, env: SpecificEnv): context is TestContextByEnv[SpecificEnv];
|
|
86
136
|
/**
|
|
87
137
|
* Determine the env for the given test context.
|
|
88
138
|
*
|
|
@@ -90,4 +140,4 @@ export declare function isTestContext<const SpecificEnv extends RuntimeEnv>(this
|
|
|
90
140
|
* @category Package : @augment-vir/test
|
|
91
141
|
* @package [`@augment-vir/test`](https://www.npmjs.com/package/@augment-vir/test)
|
|
92
142
|
*/
|
|
93
|
-
export declare function determineTestContextEnv(this: void, context: UniversalTestContext):
|
|
143
|
+
export declare function determineTestContextEnv(this: void, context: UniversalTestContext): TestEnv;
|
|
@@ -1,6 +1,18 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
|
|
1
|
+
import { assertWrap } from '@augment-vir/assert';
|
|
2
|
+
import { camelCaseToKebabCase, sanitizeFilePath } from '@augment-vir/common';
|
|
3
|
+
/**
|
|
4
|
+
* Used to determine which test context is in use.
|
|
5
|
+
*
|
|
6
|
+
* @category Test : Util
|
|
7
|
+
* @category Package : @augment-vir/test
|
|
8
|
+
* @package [`@augment-vir/test`](https://www.npmjs.com/package/@augment-vir/test)
|
|
9
|
+
*/
|
|
10
|
+
export var TestEnv;
|
|
11
|
+
(function (TestEnv) {
|
|
12
|
+
TestEnv["Node"] = "node";
|
|
13
|
+
TestEnv["Web"] = "web";
|
|
14
|
+
TestEnv["Playwright"] = "playwright";
|
|
15
|
+
})(TestEnv || (TestEnv = {}));
|
|
4
16
|
/**
|
|
5
17
|
* Extracts the full test name (including parent describes) of a given test context. Whether the
|
|
6
18
|
* test be run in web or node tests, the name will be the same.
|
|
@@ -10,9 +22,12 @@ export { RuntimeEnv } from '@augment-vir/core';
|
|
|
10
22
|
* @package [`@augment-vir/test`](https://www.npmjs.com/package/@augment-vir/test)
|
|
11
23
|
*/
|
|
12
24
|
export function extractTestName(testContext) {
|
|
13
|
-
if (isTestContext(testContext,
|
|
25
|
+
if (isTestContext(testContext, TestEnv.Node)) {
|
|
14
26
|
return testContext.fullName;
|
|
15
27
|
}
|
|
28
|
+
else if (isTestContext(testContext, TestEnv.Playwright)) {
|
|
29
|
+
return testContext.testName.clean;
|
|
30
|
+
}
|
|
16
31
|
else {
|
|
17
32
|
return flattenMochaParentTitles(testContext.test).join(' > ');
|
|
18
33
|
}
|
|
@@ -26,7 +41,17 @@ export function extractTestName(testContext) {
|
|
|
26
41
|
* @package [`@augment-vir/test`](https://www.npmjs.com/package/@augment-vir/test)
|
|
27
42
|
*/
|
|
28
43
|
export function extractTestNameAsDir(testContext) {
|
|
29
|
-
return
|
|
44
|
+
return assertWrap.isTruthy(cleanTestNameAsDir(extractTestName(testContext)));
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Same as {@link extractTestNameAsDir} but sanitizes any input in the same way.
|
|
48
|
+
*
|
|
49
|
+
* @category Test : Util
|
|
50
|
+
* @category Package : @augment-vir/test
|
|
51
|
+
* @package [`@augment-vir/test`](https://www.npmjs.com/package/@augment-vir/test)
|
|
52
|
+
*/
|
|
53
|
+
export function cleanTestNameAsDir(testName) {
|
|
54
|
+
return assertWrap.isTruthy(sanitizeFilePath(camelCaseToKebabCase(testName).replaceAll(/[<>:"/\-\\|?*_\s]+/g, '_')));
|
|
30
55
|
}
|
|
31
56
|
function flattenMochaParentTitles(node) {
|
|
32
57
|
if (node.root) {
|
|
@@ -81,6 +106,7 @@ export function isTestContext(context, env) {
|
|
|
81
106
|
}
|
|
82
107
|
}
|
|
83
108
|
const nodeOnlyCheckKey = 'diagnostic';
|
|
109
|
+
const playwrightOnlyCheckKey = 'browser';
|
|
84
110
|
/**
|
|
85
111
|
* Determine the env for the given test context.
|
|
86
112
|
*
|
|
@@ -89,5 +115,13 @@ const nodeOnlyCheckKey = 'diagnostic';
|
|
|
89
115
|
* @package [`@augment-vir/test`](https://www.npmjs.com/package/@augment-vir/test)
|
|
90
116
|
*/
|
|
91
117
|
export function determineTestContextEnv(context) {
|
|
92
|
-
|
|
118
|
+
if (playwrightOnlyCheckKey in context) {
|
|
119
|
+
return TestEnv.Playwright;
|
|
120
|
+
}
|
|
121
|
+
else if (nodeOnlyCheckKey in context) {
|
|
122
|
+
return TestEnv.Node;
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
return TestEnv.Web;
|
|
126
|
+
}
|
|
93
127
|
}
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type Locator } from '@playwright/test';
|
|
2
|
+
/**
|
|
3
|
+
* Expects that all matches for the given locator are either visible or hidden (controlled by
|
|
4
|
+
* `isVisible`).
|
|
5
|
+
*
|
|
6
|
+
* @category Internal
|
|
7
|
+
*/
|
|
8
|
+
export declare function expectAllVisible(locator: Readonly<Locator>, isVisible: boolean): Promise<void>;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { expect } from '@playwright/test';
|
|
2
|
+
/**
|
|
3
|
+
* Expects that all matches for the given locator are either visible or hidden (controlled by
|
|
4
|
+
* `isVisible`).
|
|
5
|
+
*
|
|
6
|
+
* @category Internal
|
|
7
|
+
*/
|
|
8
|
+
export async function expectAllVisible(locator, isVisible) {
|
|
9
|
+
const count = await locator.count();
|
|
10
|
+
for (let i = 0; i < count; i++) {
|
|
11
|
+
if (isVisible) {
|
|
12
|
+
await expect(locator.nth(i)).toBeVisible();
|
|
13
|
+
}
|
|
14
|
+
else {
|
|
15
|
+
await expect(locator.nth(i)).toBeHidden();
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type UniversalTestContext } from '../augments/universal-testing-suite/universal-test-context.js';
|
|
2
|
+
/**
|
|
3
|
+
* Clicks a label to select its input and then types the given text.
|
|
4
|
+
*
|
|
5
|
+
* @category Internal
|
|
6
|
+
*/
|
|
7
|
+
export declare function enterTextByLabel(testContext: Readonly<UniversalTestContext>, { label, text }: {
|
|
8
|
+
label: string;
|
|
9
|
+
text: string;
|
|
10
|
+
}): Promise<void>;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { assertTestContext, TestEnv, } from '../augments/universal-testing-suite/universal-test-context.js';
|
|
2
|
+
/**
|
|
3
|
+
* Clicks a label to select its input and then types the given text.
|
|
4
|
+
*
|
|
5
|
+
* @category Internal
|
|
6
|
+
*/
|
|
7
|
+
export async function enterTextByLabel(testContext, { label, text }) {
|
|
8
|
+
assertTestContext(testContext, TestEnv.Playwright);
|
|
9
|
+
await testContext.page.getByLabel(label).first().click();
|
|
10
|
+
await testContext.page.keyboard.type(text);
|
|
11
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type Page } from '@playwright/test';
|
|
2
|
+
import { type UniversalTestContext } from '../augments/universal-testing-suite/universal-test-context.js';
|
|
3
|
+
/**
|
|
4
|
+
* Options for `testPlaywright.getMenuOption`.
|
|
5
|
+
*
|
|
6
|
+
* @category Internal
|
|
7
|
+
*/
|
|
8
|
+
export type MenuOptionOptions = Parameters<Page['getByRole']>[1] & Partial<{
|
|
9
|
+
nth: number;
|
|
10
|
+
}>;
|
|
11
|
+
/**
|
|
12
|
+
* Find the matching (or first) "option" element.
|
|
13
|
+
*
|
|
14
|
+
* @category Internal
|
|
15
|
+
*/
|
|
16
|
+
export declare function getMenuOption(testContext: Readonly<UniversalTestContext>, options?: MenuOptionOptions | undefined): import("playwright-core").Locator;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { assertTestContext, TestEnv, } from '../augments/universal-testing-suite/universal-test-context.js';
|
|
2
|
+
/**
|
|
3
|
+
* Find the matching (or first) "option" element.
|
|
4
|
+
*
|
|
5
|
+
* @category Internal
|
|
6
|
+
*/
|
|
7
|
+
export function getMenuOption(testContext, options) {
|
|
8
|
+
assertTestContext(testContext, TestEnv.Playwright);
|
|
9
|
+
const baseLocator = testContext.page.getByRole('option', options);
|
|
10
|
+
if (options && 'nth' in options) {
|
|
11
|
+
return baseLocator.nth(options.nth);
|
|
12
|
+
}
|
|
13
|
+
else {
|
|
14
|
+
return baseLocator.first();
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Checks if a locator contains the given class.
|
|
3
|
+
*
|
|
4
|
+
* @category Internal
|
|
5
|
+
*/
|
|
6
|
+
export async function checkHasClass(locator, className) {
|
|
7
|
+
if (!className) {
|
|
8
|
+
return false;
|
|
9
|
+
}
|
|
10
|
+
const currentClassValue = (await locator.getAttribute('class')) || '';
|
|
11
|
+
return currentClassValue.includes(className);
|
|
12
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type UniversalTestContext } from '../augments/universal-testing-suite/universal-test-context.js';
|
|
2
|
+
/**
|
|
3
|
+
* Read from a page's local storage.
|
|
4
|
+
*
|
|
5
|
+
* @category Internal
|
|
6
|
+
*/
|
|
7
|
+
export declare function readLocalStorage(testContext: Readonly<UniversalTestContext>, storageKey: string): Promise<string | undefined>;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { assertTestContext, TestEnv, } from '../augments/universal-testing-suite/universal-test-context.js';
|
|
2
|
+
/**
|
|
3
|
+
* Read from a page's local storage.
|
|
4
|
+
*
|
|
5
|
+
* @category Internal
|
|
6
|
+
*/
|
|
7
|
+
export async function readLocalStorage(testContext, storageKey) {
|
|
8
|
+
assertTestContext(testContext, TestEnv.Playwright);
|
|
9
|
+
return ((await testContext.page.evaluate((keyToRead) => {
|
|
10
|
+
return localStorage.getItem(keyToRead);
|
|
11
|
+
}, storageKey)) || undefined);
|
|
12
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { type GenericTreePaths } from 'spa-router-vir';
|
|
2
|
+
import { type UniversalTestContext } from '../augments/universal-testing-suite/universal-test-context.js';
|
|
3
|
+
/**
|
|
4
|
+
* Converts {@link NavPath} into an actionable URL string.
|
|
5
|
+
*
|
|
6
|
+
* @category Internal
|
|
7
|
+
*/
|
|
8
|
+
export declare function extractNavUrl(frontendUrl: string, path: NavPath): string;
|
|
9
|
+
/**
|
|
10
|
+
* Used for the `path` argument of `testPlaywright.nav`.
|
|
11
|
+
*
|
|
12
|
+
* @category Internal
|
|
13
|
+
*/
|
|
14
|
+
export type NavPath = /** A full URL to load, will not be appended to the auto detected frontend url. */ string
|
|
15
|
+
/** Path array to append to the auto detected frontend url. */
|
|
16
|
+
| string[]
|
|
17
|
+
/** Prefer using tree paths with `frontendPathTree` */
|
|
18
|
+
| GenericTreePaths;
|
|
19
|
+
/**
|
|
20
|
+
* The test name appended to the frontend when `testPlaywright.nav` is used.
|
|
21
|
+
*
|
|
22
|
+
* @category Internal
|
|
23
|
+
*/
|
|
24
|
+
export declare const playwrightTeatNameUrlParam = "test-name";
|
|
25
|
+
/**
|
|
26
|
+
* Navigate to a URL in Playwright via given paths.
|
|
27
|
+
*
|
|
28
|
+
* @category Internal
|
|
29
|
+
*/
|
|
30
|
+
export declare function navigateTo(testContext: Readonly<UniversalTestContext>, { path, baseFrontendUrl, }: {
|
|
31
|
+
path: NavPath;
|
|
32
|
+
/** If not provided, the page's current URL will be used. */
|
|
33
|
+
baseFrontendUrl?: string | undefined;
|
|
34
|
+
}): Promise<void>;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { check } from '@augment-vir/assert';
|
|
2
|
+
import { buildUrl } from 'url-vir';
|
|
3
|
+
import { assertTestContext, extractTestNameAsDir, TestEnv, } from '../augments/universal-testing-suite/universal-test-context.js';
|
|
4
|
+
/**
|
|
5
|
+
* Converts {@link NavPath} into an actionable URL string.
|
|
6
|
+
*
|
|
7
|
+
* @category Internal
|
|
8
|
+
*/
|
|
9
|
+
export function extractNavUrl(frontendUrl, path) {
|
|
10
|
+
return check.isString(path)
|
|
11
|
+
? path
|
|
12
|
+
: check.isArray(path)
|
|
13
|
+
? buildUrl(frontendUrl, {
|
|
14
|
+
paths: path,
|
|
15
|
+
}).href
|
|
16
|
+
: buildUrl(frontendUrl, {
|
|
17
|
+
paths: path.fullPaths,
|
|
18
|
+
}).href;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* The test name appended to the frontend when `testPlaywright.nav` is used.
|
|
22
|
+
*
|
|
23
|
+
* @category Internal
|
|
24
|
+
*/
|
|
25
|
+
export const playwrightTeatNameUrlParam = 'test-name';
|
|
26
|
+
/**
|
|
27
|
+
* Navigate to a URL in Playwright via given paths.
|
|
28
|
+
*
|
|
29
|
+
* @category Internal
|
|
30
|
+
*/
|
|
31
|
+
export async function navigateTo(testContext, { path, baseFrontendUrl, }) {
|
|
32
|
+
assertTestContext(testContext, TestEnv.Playwright);
|
|
33
|
+
const page = testContext.page;
|
|
34
|
+
const testName = extractTestNameAsDir(testContext);
|
|
35
|
+
const finalPath = buildUrl(extractNavUrl(baseFrontendUrl || page.url(), path), {
|
|
36
|
+
search: {
|
|
37
|
+
[playwrightTeatNameUrlParam]: [testName],
|
|
38
|
+
},
|
|
39
|
+
}).href;
|
|
40
|
+
if (page.url() === finalPath) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
await page.goto(finalPath, {
|
|
45
|
+
waitUntil: 'domcontentloaded',
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type MaybePromise } from '@augment-vir/common';
|
|
2
|
+
import { type Download, type Page } from '@playwright/test';
|
|
3
|
+
import { type RequireExactlyOne } from 'type-fest';
|
|
4
|
+
import { type UniversalTestContext } from '../augments/universal-testing-suite/universal-test-context.js';
|
|
5
|
+
/**
|
|
6
|
+
* Run the trigger and catch a new page _or_ a new download (sometimes Playwright inconsistently
|
|
7
|
+
* chooses on or the other).
|
|
8
|
+
*
|
|
9
|
+
* @category Internal
|
|
10
|
+
*/
|
|
11
|
+
export declare function handleNewPageOrDownload(testContext: Readonly<UniversalTestContext>, trigger: () => MaybePromise<void>): Promise<RequireExactlyOne<{
|
|
12
|
+
newPage: Page;
|
|
13
|
+
download: Download;
|
|
14
|
+
}>>;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { assertTestContext, TestEnv, } from '../augments/universal-testing-suite/universal-test-context.js';
|
|
2
|
+
/**
|
|
3
|
+
* Run the trigger and catch a new page _or_ a new download (sometimes Playwright inconsistently
|
|
4
|
+
* chooses on or the other).
|
|
5
|
+
*
|
|
6
|
+
* @category Internal
|
|
7
|
+
*/
|
|
8
|
+
export async function handleNewPageOrDownload(testContext, trigger) {
|
|
9
|
+
assertTestContext(testContext, TestEnv.Playwright);
|
|
10
|
+
const openOrDownload = Promise.race([
|
|
11
|
+
testContext.page
|
|
12
|
+
.context()
|
|
13
|
+
.waitForEvent('page', async (newPage) => {
|
|
14
|
+
return (await newPage.opener()) === testContext.page;
|
|
15
|
+
})
|
|
16
|
+
.then((result) => {
|
|
17
|
+
return { page: result };
|
|
18
|
+
}),
|
|
19
|
+
testContext.page.waitForEvent('download').then((result) => {
|
|
20
|
+
return { download: result };
|
|
21
|
+
}),
|
|
22
|
+
]);
|
|
23
|
+
await trigger();
|
|
24
|
+
return (await openOrDownload);
|
|
25
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { type PartialWithUndefined } from '@augment-vir/common';
|
|
2
|
+
import { type Locator } from '@playwright/test';
|
|
3
|
+
import { type UniversalTestContext } from '../augments/universal-testing-suite/universal-test-context.js';
|
|
4
|
+
/** This is used for type extraction because Playwright does not export the types we need. */
|
|
5
|
+
declare function extractScreenshotMethod(): {
|
|
6
|
+
(name: string | ReadonlyArray<string>, options?: {
|
|
7
|
+
animations?: "disabled" | "allow";
|
|
8
|
+
caret?: "hide" | "initial";
|
|
9
|
+
mask?: Array<Locator>;
|
|
10
|
+
maskColor?: string;
|
|
11
|
+
maxDiffPixelRatio?: number;
|
|
12
|
+
maxDiffPixels?: number;
|
|
13
|
+
omitBackground?: boolean;
|
|
14
|
+
scale?: "css" | "device";
|
|
15
|
+
stylePath?: string | Array<string>;
|
|
16
|
+
threshold?: number;
|
|
17
|
+
timeout?: number;
|
|
18
|
+
}): Promise<void>;
|
|
19
|
+
(options?: {
|
|
20
|
+
animations?: "disabled" | "allow";
|
|
21
|
+
caret?: "hide" | "initial";
|
|
22
|
+
mask?: Array<Locator>;
|
|
23
|
+
maskColor?: string;
|
|
24
|
+
maxDiffPixelRatio?: number;
|
|
25
|
+
maxDiffPixels?: number;
|
|
26
|
+
omitBackground?: boolean;
|
|
27
|
+
scale?: "css" | "device";
|
|
28
|
+
stylePath?: string | Array<string>;
|
|
29
|
+
threshold?: number;
|
|
30
|
+
timeout?: number;
|
|
31
|
+
}): Promise<void>;
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* Correct options for `locator.screenshot`. (Playwright's `LocatorScreenshotOptions` export is
|
|
35
|
+
* wrong.)
|
|
36
|
+
*
|
|
37
|
+
* @category Internal
|
|
38
|
+
* @default defaultScreenshotOptions
|
|
39
|
+
*/
|
|
40
|
+
export type LocatorScreenshotOptions = NonNullable<Parameters<ReturnType<typeof extractScreenshotMethod>>[0]>;
|
|
41
|
+
/**
|
|
42
|
+
* Default internal options for {@link LocatorScreenshotOptions}, used in {@link expectScreenshot}
|
|
43
|
+
*
|
|
44
|
+
* @category Internal
|
|
45
|
+
*/
|
|
46
|
+
export declare const defaultScreenshotOptions: {
|
|
47
|
+
animations: "disabled";
|
|
48
|
+
caret: "hide";
|
|
49
|
+
timeout: number;
|
|
50
|
+
scale: "css";
|
|
51
|
+
threshold: number;
|
|
52
|
+
maxDiffPixelRatio: number;
|
|
53
|
+
};
|
|
54
|
+
export type TakeScreenshotOptions = PartialWithUndefined<{
|
|
55
|
+
/** If no locator is provided then the whole page is use. */
|
|
56
|
+
locator: Readonly<Locator>;
|
|
57
|
+
}> & Partial<LocatorScreenshotOptions>;
|
|
58
|
+
/**
|
|
59
|
+
* Get the path to save the given screenshot file name to.
|
|
60
|
+
*
|
|
61
|
+
* @category Internal
|
|
62
|
+
*/
|
|
63
|
+
export declare function getScreenshotPath(testContext: Readonly<UniversalTestContext>, screenshotBaseName: string): string;
|
|
64
|
+
/**
|
|
65
|
+
* Options for taking _and_ saving a screenshot.
|
|
66
|
+
*
|
|
67
|
+
* @category Internal
|
|
68
|
+
*/
|
|
69
|
+
export type SaveScreenshotOptions = TakeScreenshotOptions & {
|
|
70
|
+
screenshotBaseName: string;
|
|
71
|
+
};
|
|
72
|
+
/**
|
|
73
|
+
* Take and immediately save a screenshot.
|
|
74
|
+
*
|
|
75
|
+
* @category Internal
|
|
76
|
+
* @returns The path that the screenshot was saved to.
|
|
77
|
+
*/
|
|
78
|
+
export declare function takeScreenshot(testContext: Readonly<UniversalTestContext>, options: Readonly<SaveScreenshotOptions>): Promise<string>;
|
|
79
|
+
/**
|
|
80
|
+
* Similar to Playwright's `expect().toHaveScreenshot` but allows images to have different sizes and
|
|
81
|
+
* has default comparison threshold options that are wide enough to allow testing between different
|
|
82
|
+
* operating systems without failure (usually).
|
|
83
|
+
*
|
|
84
|
+
* @category Internal
|
|
85
|
+
*/
|
|
86
|
+
export declare function expectScreenshot(testContext: Readonly<UniversalTestContext>, options: Readonly<SaveScreenshotOptions>): Promise<void>;
|
|
87
|
+
export {};
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { assert } from '@augment-vir/assert';
|
|
2
|
+
import { addSuffix, log } from '@augment-vir/common';
|
|
3
|
+
import { writeFileAndDir } from '@augment-vir/node';
|
|
4
|
+
import { expect } from '@playwright/test';
|
|
5
|
+
import { existsSync } from 'node:fs';
|
|
6
|
+
import { readFile } from 'node:fs/promises';
|
|
7
|
+
import { relative } from 'node:path';
|
|
8
|
+
import pixelmatch from 'pixelmatch';
|
|
9
|
+
import { PNG } from 'pngjs';
|
|
10
|
+
import sharp from 'sharp';
|
|
11
|
+
import { assertTestContext, assertWrapTestContext, TestEnv, } from '../augments/universal-testing-suite/universal-test-context.js';
|
|
12
|
+
/** This is used for type extraction because Playwright does not export the types we need. */
|
|
13
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
14
|
+
function extractScreenshotMethod() {
|
|
15
|
+
assert.never('this function should not be executed, it is only used for types');
|
|
16
|
+
// eslint-disable-next-line @typescript-eslint/unbound-method
|
|
17
|
+
return expect({}).toHaveScreenshot;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Default internal options for {@link LocatorScreenshotOptions}, used in {@link expectScreenshot}
|
|
21
|
+
*
|
|
22
|
+
* @category Internal
|
|
23
|
+
*/
|
|
24
|
+
export const defaultScreenshotOptions = {
|
|
25
|
+
animations: 'disabled',
|
|
26
|
+
caret: 'hide',
|
|
27
|
+
timeout: 10_000,
|
|
28
|
+
scale: 'css',
|
|
29
|
+
threshold: 0.1,
|
|
30
|
+
maxDiffPixelRatio: 0.08,
|
|
31
|
+
};
|
|
32
|
+
async function padImage(image, { height, width }) {
|
|
33
|
+
return await sharp({
|
|
34
|
+
create: { width, height, channels: 4, background: { r: 0, g: 0, b: 0, alpha: 0 } },
|
|
35
|
+
})
|
|
36
|
+
/** Top-left align. */
|
|
37
|
+
.composite([{ input: image, left: 0, top: 0 }])
|
|
38
|
+
.png()
|
|
39
|
+
.toBuffer();
|
|
40
|
+
}
|
|
41
|
+
/** Pads both images to the same canvas (max width/height) without scaling. */
|
|
42
|
+
async function padToSameCanvas(aBuf, bBuf) {
|
|
43
|
+
const [aMeta, bMeta,] = await Promise.all([
|
|
44
|
+
sharp(aBuf).metadata(),
|
|
45
|
+
sharp(bBuf).metadata(),
|
|
46
|
+
]);
|
|
47
|
+
if (!aMeta.width || !aMeta.height || !bMeta.width || !bMeta.height) {
|
|
48
|
+
throw new Error('Unable to read image dimensions.');
|
|
49
|
+
}
|
|
50
|
+
const dimensions = {
|
|
51
|
+
width: Math.max(aMeta.width, bMeta.width),
|
|
52
|
+
height: Math.max(aMeta.height, bMeta.height),
|
|
53
|
+
};
|
|
54
|
+
const [aPadded, bPadded,] = await Promise.all([
|
|
55
|
+
padImage(aBuf, dimensions),
|
|
56
|
+
padImage(bBuf, dimensions),
|
|
57
|
+
]);
|
|
58
|
+
const aPng = PNG.sync.read(aPadded);
|
|
59
|
+
const bPng = PNG.sync.read(bPadded);
|
|
60
|
+
return {
|
|
61
|
+
aPng,
|
|
62
|
+
bPng,
|
|
63
|
+
dimensions,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
async function takeScreenshotBuffer(testContext, options = {}) {
|
|
67
|
+
if (options.locator) {
|
|
68
|
+
/** The locator expectation has different options than the page expectation. */
|
|
69
|
+
return await options.locator.screenshot({
|
|
70
|
+
...defaultScreenshotOptions,
|
|
71
|
+
...options,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
assertTestContext(testContext, TestEnv.Playwright);
|
|
76
|
+
return await testContext.page.screenshot({
|
|
77
|
+
...defaultScreenshotOptions,
|
|
78
|
+
...options,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
/** @returns The path that the screenshot was saved to. */
|
|
83
|
+
async function saveScreenshotBuffer(testContext, screenshotBuffer, screenshotBaseName) {
|
|
84
|
+
assertTestContext(testContext, TestEnv.Playwright);
|
|
85
|
+
const screenshotPath = getScreenshotPath(testContext, screenshotBaseName);
|
|
86
|
+
await writeFileAndDir(screenshotPath, screenshotBuffer);
|
|
87
|
+
return screenshotPath;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Get the path to save the given screenshot file name to.
|
|
91
|
+
*
|
|
92
|
+
* @category Internal
|
|
93
|
+
*/
|
|
94
|
+
export function getScreenshotPath(testContext, screenshotBaseName) {
|
|
95
|
+
assertTestContext(testContext, TestEnv.Playwright);
|
|
96
|
+
const screenshotFileName = addSuffix({ value: screenshotBaseName, suffix: '.png' });
|
|
97
|
+
return testContext.testInfo.snapshotPath(screenshotFileName);
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Take and immediately save a screenshot.
|
|
101
|
+
*
|
|
102
|
+
* @category Internal
|
|
103
|
+
* @returns The path that the screenshot was saved to.
|
|
104
|
+
*/
|
|
105
|
+
export async function takeScreenshot(testContext, options) {
|
|
106
|
+
return await saveScreenshotBuffer(testContext, await takeScreenshotBuffer(testContext, options), options.screenshotBaseName);
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Similar to Playwright's `expect().toHaveScreenshot` but allows images to have different sizes and
|
|
110
|
+
* has default comparison threshold options that are wide enough to allow testing between different
|
|
111
|
+
* operating systems without failure (usually).
|
|
112
|
+
*
|
|
113
|
+
* @category Internal
|
|
114
|
+
*/
|
|
115
|
+
export async function expectScreenshot(testContext, options) {
|
|
116
|
+
assertTestContext(testContext, TestEnv.Playwright);
|
|
117
|
+
const currentScreenshotBuffer = await takeScreenshotBuffer(testContext, options);
|
|
118
|
+
const screenshotFilePath = getScreenshotPath(testContext, options.screenshotBaseName);
|
|
119
|
+
async function writeNewScreenshot() {
|
|
120
|
+
log.mutate(`Updated screenshot: ${relative(process.cwd(), screenshotFilePath)}`);
|
|
121
|
+
await saveScreenshotBuffer(testContext, currentScreenshotBuffer, screenshotFilePath);
|
|
122
|
+
}
|
|
123
|
+
async function writeExpectationScreenshot(contents, fileName) {
|
|
124
|
+
const filePath = assertWrapTestContext(testContext, TestEnv.Playwright).testInfo.outputPath(addSuffix({ value: fileName, suffix: '.png' }));
|
|
125
|
+
await writeFileAndDir(filePath, contents);
|
|
126
|
+
}
|
|
127
|
+
if (existsSync(screenshotFilePath)) {
|
|
128
|
+
if (testContext.testInfo.config.updateSnapshots === 'changed') {
|
|
129
|
+
await writeNewScreenshot();
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
if (testContext.testInfo.config.updateSnapshots !== 'none') {
|
|
134
|
+
await writeNewScreenshot();
|
|
135
|
+
}
|
|
136
|
+
await writeExpectationScreenshot(currentScreenshotBuffer, 'actual');
|
|
137
|
+
throw new Error(`Baseline screenshot not found: ${screenshotFilePath}. Re-run with --update-snapshots to create it.`);
|
|
138
|
+
}
|
|
139
|
+
const baseScreenshotBuffer = await readFile(screenshotFilePath);
|
|
140
|
+
const { aPng: baseScreenshotPng, bPng: currentScreenshotPng, dimensions, } = await padToSameCanvas(baseScreenshotBuffer, currentScreenshotBuffer);
|
|
141
|
+
const diffPng = new PNG(dimensions);
|
|
142
|
+
const diffPixelCount = pixelmatch(baseScreenshotPng.data, currentScreenshotPng.data, diffPng.data, dimensions.width, dimensions.height, {
|
|
143
|
+
threshold: defaultScreenshotOptions.threshold,
|
|
144
|
+
});
|
|
145
|
+
const totalPixels = dimensions.width * dimensions.height;
|
|
146
|
+
const diffRatio = diffPixelCount / totalPixels;
|
|
147
|
+
const ratioOk = diffRatio <= defaultScreenshotOptions.maxDiffPixelRatio;
|
|
148
|
+
if (!ratioOk) {
|
|
149
|
+
if (process.env.CI) {
|
|
150
|
+
await writeNewScreenshot();
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
await writeExpectationScreenshot(PNG.sync.write(baseScreenshotPng), 'expected');
|
|
154
|
+
await writeExpectationScreenshot(PNG.sync.write(currentScreenshotPng), 'actual');
|
|
155
|
+
await writeExpectationScreenshot(PNG.sync.write(diffPng), 'diff');
|
|
156
|
+
throw new Error(`Screenshot mismatch: ${screenshotFilePath}\n diff=${diffPixelCount}px (${(diffRatio * 100).toFixed(3)}%) (limit: ${(defaultScreenshotOptions.maxDiffPixelRatio * 100).toFixed(3)}%). Run with --update-snapshots to update screenshot.`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@augment-vir/test",
|
|
3
|
-
"version": "31.
|
|
3
|
+
"version": "31.50.1",
|
|
4
4
|
"description": "A universal testing suite that works with Mocha style test runners _and_ Node.js's built-in test runner.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"test",
|
|
@@ -36,20 +36,22 @@
|
|
|
36
36
|
"scripts": {
|
|
37
37
|
"compile": "virmator compile",
|
|
38
38
|
"start": "tsx src/index.ts",
|
|
39
|
-
"test": "runstorm --names node,web \"npm run test:node\" \"npm run test:web\"",
|
|
39
|
+
"test": "runstorm --names node,web,playwright \"npm run test:node\" \"npm run test:web\" \"npm run test:playwright\"",
|
|
40
40
|
"test:coverage": "npm run test",
|
|
41
41
|
"test:node": "virmator test --no-deps node 'src/augments/universal-testing-suite/**/*.test.ts'",
|
|
42
|
+
"test:playwright": "playwright test --config configs/playwright.config.ts",
|
|
42
43
|
"test:update": "npm test",
|
|
43
44
|
"test:web": "virmator test --no-deps web 'src/test-web/**/*.test.ts' 'src/augments/universal-testing-suite/**/*.test.ts'"
|
|
44
45
|
},
|
|
45
46
|
"dependencies": {
|
|
46
|
-
"@augment-vir/assert": "^31.
|
|
47
|
-
"@augment-vir/common": "^31.
|
|
47
|
+
"@augment-vir/assert": "^31.50.1",
|
|
48
|
+
"@augment-vir/common": "^31.50.1",
|
|
48
49
|
"@open-wc/testing-helpers": "^3.0.1",
|
|
49
|
-
"@virmator/test": "^14.2.
|
|
50
|
+
"@virmator/test": "^14.2.2",
|
|
50
51
|
"type-fest": "^5.2.0"
|
|
51
52
|
},
|
|
52
53
|
"devDependencies": {
|
|
54
|
+
"@playwright/test": "^1.56.1",
|
|
53
55
|
"@types/node": "^24.10.0",
|
|
54
56
|
"@web/dev-server-esbuild": "^1.0.4",
|
|
55
57
|
"@web/test-runner": "^0.20.2",
|
|
@@ -57,11 +59,22 @@
|
|
|
57
59
|
"@web/test-runner-playwright": "^0.11.1",
|
|
58
60
|
"element-vir": "^26.11.1",
|
|
59
61
|
"istanbul-smart-text-reporter": "^1.1.5",
|
|
62
|
+
"pixelmatch": "^5.3.0",
|
|
63
|
+
"pngjs": "^7.0.0",
|
|
60
64
|
"runstorm": "^0.6.2",
|
|
61
|
-
"
|
|
65
|
+
"sharp": "^0.34.5",
|
|
66
|
+
"spa-router-vir": "^6.3.1",
|
|
67
|
+
"typescript": "^5.9.3",
|
|
68
|
+
"url-vir": "^2.1.6"
|
|
62
69
|
},
|
|
63
70
|
"peerDependencies": {
|
|
64
|
-
"
|
|
71
|
+
"@playwright/test": "*",
|
|
72
|
+
"element-vir": "*",
|
|
73
|
+
"pixelmatch": ">=5",
|
|
74
|
+
"pngjs": ">=7",
|
|
75
|
+
"sharp": ">=0.34",
|
|
76
|
+
"spa-router-vir": ">=6",
|
|
77
|
+
"url-vir": ">=2"
|
|
65
78
|
},
|
|
66
79
|
"engines": {
|
|
67
80
|
"node": ">=22"
|