@quickpickle/vitest-browser 0.0.3 → 0.2.0
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/CHANGELOG.md +52 -0
- package/README.md +71 -0
- package/dist/VitestBrowserWorld.cjs +89 -22
- package/dist/VitestBrowserWorld.cjs.map +1 -1
- package/dist/VitestBrowserWorld.d.ts +45 -21
- package/dist/VitestBrowserWorld.mjs +90 -23
- package/dist/VitestBrowserWorld.mjs.map +1 -1
- package/dist/actions.steps.cjs +26 -2
- package/dist/actions.steps.cjs.map +1 -1
- package/dist/actions.steps.mjs +27 -3
- package/dist/actions.steps.mjs.map +1 -1
- package/dist/frameworks/ReactBrowserWorld.cjs +19 -6
- package/dist/frameworks/ReactBrowserWorld.cjs.map +1 -1
- package/dist/frameworks/ReactBrowserWorld.d.ts +9 -0
- package/dist/frameworks/ReactBrowserWorld.mjs +19 -7
- package/dist/frameworks/ReactBrowserWorld.mjs.map +1 -1
- package/dist/frameworks/SvelteBrowserWorld.cjs +13 -2
- package/dist/frameworks/SvelteBrowserWorld.cjs.map +1 -1
- package/dist/frameworks/SvelteBrowserWorld.d.ts +2 -3
- package/dist/frameworks/SvelteBrowserWorld.mjs +13 -2
- package/dist/frameworks/SvelteBrowserWorld.mjs.map +1 -1
- package/dist/frameworks/VueBrowserWorld.cjs +8 -4
- package/dist/frameworks/VueBrowserWorld.cjs.map +1 -1
- package/dist/frameworks/VueBrowserWorld.d.ts +1 -3
- package/dist/frameworks/VueBrowserWorld.mjs +8 -4
- package/dist/frameworks/VueBrowserWorld.mjs.map +1 -1
- package/dist/frameworks/react.cjs +2 -0
- package/dist/frameworks/react.cjs.map +1 -1
- package/dist/frameworks/react.mjs +2 -0
- package/dist/frameworks/react.mjs.map +1 -1
- package/dist/frameworks/svelte.cjs +2 -0
- package/dist/frameworks/svelte.cjs.map +1 -1
- package/dist/frameworks/svelte.mjs +2 -0
- package/dist/frameworks/svelte.mjs.map +1 -1
- package/dist/frameworks/vue.cjs +2 -0
- package/dist/frameworks/vue.cjs.map +1 -1
- package/dist/frameworks/vue.mjs +2 -0
- package/dist/frameworks/vue.mjs.map +1 -1
- package/dist/outcomes.steps.cjs +78 -22
- package/dist/outcomes.steps.cjs.map +1 -1
- package/dist/outcomes.steps.mjs +78 -22
- package/dist/outcomes.steps.mjs.map +1 -1
- package/package.json +5 -4
- package/rollup.config.js +1 -0
- package/src/VitestBrowserWorld.ts +127 -38
- package/src/actions.steps.ts +28 -2
- package/src/frameworks/ReactBrowserWorld.ts +22 -9
- package/src/frameworks/SvelteBrowserWorld.ts +11 -3
- package/src/frameworks/VueBrowserWorld.ts +6 -5
- package/src/outcomes.steps.ts +79 -22
- package/tests/generic/browser-actions.feature +193 -0
- package/tests/generic/browser-generic.feature +76 -0
- package/tests/generic/browser-outcomes.feature +138 -0
- package/tests/generic/generic.steps.ts +27 -0
- package/tests/svelte/Example.svelte +153 -0
- package/vite.config.ts +8 -0
- package/vitest.workspace.ts +34 -35
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,57 @@
|
|
|
1
1
|
# @quickpickle/vitest-browser
|
|
2
2
|
|
|
3
|
+
## 0.2.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 8273918: Switched to VisualWorld class for visual regression testing
|
|
8
|
+
|
|
9
|
+
### Patch Changes
|
|
10
|
+
|
|
11
|
+
- Updated dependencies [e617638]
|
|
12
|
+
- quickpickle@1.9.0
|
|
13
|
+
|
|
14
|
+
## 0.1.0
|
|
15
|
+
|
|
16
|
+
### Minor Changes
|
|
17
|
+
|
|
18
|
+
- 8a32a09: Add a true path sanitizer to the base World object
|
|
19
|
+
|
|
20
|
+
BREAKING CHANGE:
|
|
21
|
+
|
|
22
|
+
The base world object no longer has a public `projectRoot` property.
|
|
23
|
+
Instead of constructing paths that way, developers should use the
|
|
24
|
+
`world.fullPath()` method to get the full path to a file or subfolder.
|
|
25
|
+
This should ensure that only files below the projectRoot are accessed.
|
|
26
|
+
|
|
27
|
+
Playwright and Browser world constructors no longer have the
|
|
28
|
+
sanitizeFilepath method. Instead, there is a new "sanitizePath"
|
|
29
|
+
method on the base World, which should be relatively safe even
|
|
30
|
+
for user input, avoiding path traversal and the like.
|
|
31
|
+
|
|
32
|
+
- 15e0e06: Major refactor with breaking changes, including to the World Constructor APIs.
|
|
33
|
+
|
|
34
|
+
- Added tests for all step definitions (using Svelte) based on Playwright package tests
|
|
35
|
+
- Added basic instructions to README
|
|
36
|
+
- Added scrolling step definitions
|
|
37
|
+
- Added screenshot step definitions
|
|
38
|
+
- Added step definitions to test for integer values
|
|
39
|
+
- Added step definitions to check that an element is in the viewport
|
|
40
|
+
- Added step definitions to check for clicked items
|
|
41
|
+
- Changed the VitestBrowserWorld API to accommodate step definitions
|
|
42
|
+
- Changed the React, Vue and Svelte worlds to use the new API
|
|
43
|
+
|
|
44
|
+
BREAKING CHANGES:
|
|
45
|
+
|
|
46
|
+
- When extending the VitestBrowserWorld class, subclasses must implement the `render`
|
|
47
|
+
and `cleanup` methods. The `render` method must not only render the component
|
|
48
|
+
but also set the `page` property using RenderResult.container.
|
|
49
|
+
|
|
50
|
+
### Patch Changes
|
|
51
|
+
|
|
52
|
+
- Updated dependencies [8a32a09]
|
|
53
|
+
- quickpickle@1.8.0
|
|
54
|
+
|
|
3
55
|
## 0.0.3
|
|
4
56
|
|
|
5
57
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -1,4 +1,75 @@
|
|
|
1
1
|
# Vitest: Browser Mode
|
|
2
2
|
|
|
3
3
|
This is the library for using Vitest with browser mode for testing components.
|
|
4
|
+
It's still fairly early in development, but you can already use it for some purposes.
|
|
4
5
|
|
|
6
|
+
## Setup
|
|
7
|
+
|
|
8
|
+
The following is a pretty basic setup for testing components in Svelte, Vue, or React:
|
|
9
|
+
|
|
10
|
+
1. `pnpm i -D @quickpickle/vitest-browser@latest`
|
|
11
|
+
|
|
12
|
+
2. add plugins to the Vitest configuration, in one of the configuration files (`vite.config.ts`, `vitest.config.ts`, or `vitest.workspace.ts`). Here is a working example from a SvelteKit project:
|
|
13
|
+
|
|
14
|
+
```ts
|
|
15
|
+
// File: vitest.workspace.ts
|
|
16
|
+
import quickpickle from "quickpickle";
|
|
17
|
+
|
|
18
|
+
export default [
|
|
19
|
+
{
|
|
20
|
+
plugins: [quickpickle()],
|
|
21
|
+
extends: './vite.config.ts',
|
|
22
|
+
test: {
|
|
23
|
+
name: 'components',
|
|
24
|
+
environment: 'browser',
|
|
25
|
+
include: ['src/lib/**/*.feature'], // anticipates putting the .feature files next to components
|
|
26
|
+
setupFiles: ['./tests/components.steps.ts'], // this file must be created (see step 3)
|
|
27
|
+
// @ts-ignore
|
|
28
|
+
quickpickle: {
|
|
29
|
+
worldConfig: {
|
|
30
|
+
componentDir: 'src/lib', // The directory where the components are kept
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
browser: { // This is configuration for Vitest browser mode, and can be modified as appropriate
|
|
34
|
+
enabled: true,
|
|
35
|
+
screenshotFailures: true,
|
|
36
|
+
name: 'chromium',
|
|
37
|
+
provider: 'playwright',
|
|
38
|
+
ui: true,
|
|
39
|
+
instances: [
|
|
40
|
+
{ browser:'chromium' },
|
|
41
|
+
]
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
]
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
3. Create a step definition file for your component tests:
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
// File: tests/components.steps.ts
|
|
52
|
+
import '@quickpickle/vitest-browser/actions';
|
|
53
|
+
import '@quickpickle/vitest-browser/outcomes';
|
|
54
|
+
import '@quickpickle/vitest-browser/svelte'; // OR react or vue
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Known Issues:
|
|
58
|
+
|
|
59
|
+
* Reactivity is currently broken for Svelte and Vue tests.
|
|
60
|
+
* Selecting elements by css selector doesn't work yet.
|
|
61
|
+
* Performing screenshot comparisons may result in an extra screenshot
|
|
62
|
+
being created for @vitest/browser versions below 3.2.0
|
|
63
|
+
|
|
64
|
+
## Suspected Issues:
|
|
65
|
+
|
|
66
|
+
* I suspect that there will still be issues with the test code
|
|
67
|
+
not properly waiting for changes to propagate on the page when
|
|
68
|
+
there are delays for CSS transitions, async fetch calls, etc.
|
|
69
|
+
|
|
70
|
+
## Plans:
|
|
71
|
+
|
|
72
|
+
[x] basic actions and outcomes in English, to match @quickpickle/playwright
|
|
73
|
+
[x] basic tests for rendering Svelte, Vue, and React components
|
|
74
|
+
[x] full tests for all actions and outcomes, matching @quickpickle/playwright
|
|
75
|
+
[ ] some sort of Storybook-esque presentation using Vitest UI
|
|
@@ -2,45 +2,54 @@
|
|
|
2
2
|
|
|
3
3
|
var quickpickle = require('quickpickle');
|
|
4
4
|
var defaultsDeep = require('lodash/defaultsDeep');
|
|
5
|
+
var context = require('@vitest/browser/context');
|
|
6
|
+
var buffer = require('buffer');
|
|
5
7
|
|
|
6
8
|
const defaultVitestWorldConfig = {
|
|
7
9
|
componentDir: '', // directory in which components are kept, relative to project root
|
|
8
10
|
screenshotDir: 'screenshots', // directory in which to save screenshots, relative to project root (default: "screenshots")
|
|
9
|
-
|
|
11
|
+
screenshotOpts: {
|
|
12
|
+
threshold: 0.1,
|
|
13
|
+
alpha: 0.6,
|
|
14
|
+
maxDiffPercentage: .01,
|
|
15
|
+
},
|
|
10
16
|
};
|
|
11
|
-
class VitestBrowserWorld extends quickpickle.
|
|
17
|
+
class VitestBrowserWorld extends quickpickle.VisualWorld {
|
|
18
|
+
async render(name, props, renderOptions) { }
|
|
19
|
+
;
|
|
20
|
+
async cleanup() { }
|
|
21
|
+
;
|
|
12
22
|
constructor(context, info) {
|
|
13
23
|
info = defaultsDeep(info || {}, { config: { worldConfig: defaultVitestWorldConfig } });
|
|
14
24
|
super(context, info);
|
|
25
|
+
this.actions = {
|
|
26
|
+
clicks: [],
|
|
27
|
+
doubleClicks: [],
|
|
28
|
+
};
|
|
15
29
|
if (!info.config.worldConfig.screenshotDir && info.config.worldConfig?.screenshotOptions?.customSnapshotsDir) {
|
|
16
30
|
this.info.config.worldConfig.screenshotDir = info.config.worldConfig.screenshotOptions.customSnapshotsDir;
|
|
17
31
|
}
|
|
18
|
-
this.renderFn = () => { };
|
|
19
|
-
this.cleanupFn = () => { };
|
|
20
32
|
}
|
|
21
33
|
async init() {
|
|
22
34
|
let browserContext = await import('@vitest/browser/context');
|
|
23
|
-
this.
|
|
35
|
+
this.browserPage = browserContext.page;
|
|
24
36
|
this.userEvent = browserContext.userEvent;
|
|
25
37
|
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
await this.renderFn(component, props, renderOptions);
|
|
31
|
-
}
|
|
32
|
-
;
|
|
33
|
-
async cleanup() {
|
|
34
|
-
await this.cleanupFn();
|
|
35
|
-
}
|
|
36
|
-
sanitizeFilepath(filepath) {
|
|
37
|
-
return filepath.replace(/\/\/+/g, '/').replace(/\/[\.~]+\//g, '/');
|
|
38
|
-
}
|
|
39
|
-
get screenshotDir() {
|
|
40
|
-
return this.sanitizeFilepath(`${this.projectRoot}/${this.worldConfig.screenshotDir}`);
|
|
38
|
+
get page() {
|
|
39
|
+
if (!this._page)
|
|
40
|
+
throw new Error('You must render a component before running tests.');
|
|
41
|
+
return this._page;
|
|
41
42
|
}
|
|
42
|
-
|
|
43
|
-
|
|
43
|
+
set page(value) {
|
|
44
|
+
while (value.parentNode !== null && value.nodeName !== 'BODY')
|
|
45
|
+
value = value.parentNode;
|
|
46
|
+
this._page = this.browserPage.elementLocator(value);
|
|
47
|
+
value.addEventListener('click', (e) => {
|
|
48
|
+
this.actions.clicks.push(e.target);
|
|
49
|
+
});
|
|
50
|
+
value.addEventListener('dblclick', (e) => {
|
|
51
|
+
this.actions.doubleClicks.push(e.target);
|
|
52
|
+
});
|
|
44
53
|
}
|
|
45
54
|
/**
|
|
46
55
|
* Gets a locator based on a certain logic
|
|
@@ -111,6 +120,8 @@ class VitestBrowserWorld extends quickpickle.QuickPickleWorld {
|
|
|
111
120
|
async scroll(locator, direction, px = 100) {
|
|
112
121
|
let horiz = direction.includes('t');
|
|
113
122
|
let el = await locator.element();
|
|
123
|
+
if (el.nodeName === 'BODY' && el.parentElement)
|
|
124
|
+
el = el.parentElement;
|
|
114
125
|
if (horiz)
|
|
115
126
|
await el.scrollBy(direction === 'right' ? px : -px, 0);
|
|
116
127
|
else
|
|
@@ -159,9 +170,65 @@ class VitestBrowserWorld extends quickpickle.QuickPickleWorld {
|
|
|
159
170
|
if (toBePresent === (matchingElements.length === 0))
|
|
160
171
|
throw new Error(`The${toBeVisible ? '' : ' hidden'} element "${locator}" was unexpectedly ${toBePresent ? 'not present' : 'present'}.`);
|
|
161
172
|
}
|
|
173
|
+
async screenshot(opts) {
|
|
174
|
+
let path;
|
|
175
|
+
if (!opts?.bufferOnly)
|
|
176
|
+
path = this.getScreenshotPath(opts?.name);
|
|
177
|
+
let locator = opts?.locator ?? this.page;
|
|
178
|
+
return locator.screenshot({ path });
|
|
179
|
+
}
|
|
180
|
+
async expectScreenshotMatch(locator, filename, opts = {}) {
|
|
181
|
+
const filepath = this.getScreenshotPath(filename);
|
|
182
|
+
let expectedImg;
|
|
183
|
+
/**
|
|
184
|
+
* Load existing screenshot, or save it if it doesn't yet exist
|
|
185
|
+
*/
|
|
186
|
+
try {
|
|
187
|
+
expectedImg = await context.commands.readFile(this.getScreenshotPath(filename), 'base64');
|
|
188
|
+
}
|
|
189
|
+
catch (e) {
|
|
190
|
+
// If the screenshot doesn't exist, save it and pass the test
|
|
191
|
+
await locator.screenshot();
|
|
192
|
+
console.warn(`new visual regression test: ${this.screenshotDir}/${this.screenshotFilename}`);
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
let expected = buffer.Buffer.from(expectedImg, 'base64');
|
|
196
|
+
/**
|
|
197
|
+
* Get the screenshot
|
|
198
|
+
*/
|
|
199
|
+
// type does not include the "save" option in the docs: see https://vitest.dev/guide/browser/locators#screenshot
|
|
200
|
+
let screenshotOptions = { save: false, base64: true };
|
|
201
|
+
let actualImg = await locator.screenshot(screenshotOptions);
|
|
202
|
+
let actual = buffer.Buffer.from(typeof actualImg === 'string' ? actualImg : actualImg.base64, 'base64');
|
|
203
|
+
/**
|
|
204
|
+
* Compare the two screenshots
|
|
205
|
+
*/
|
|
206
|
+
let screenshotOpts = defaultsDeep(opts, this.worldConfig.screenshotOpts);
|
|
207
|
+
let matchResult = null;
|
|
208
|
+
try {
|
|
209
|
+
matchResult = await this.screenshotDiff(actual, expected, screenshotOpts);
|
|
210
|
+
}
|
|
211
|
+
catch (e) { }
|
|
212
|
+
console.log({
|
|
213
|
+
pass: matchResult?.pass,
|
|
214
|
+
diffPercentage: matchResult?.diffPercentage,
|
|
215
|
+
filename,
|
|
216
|
+
locator,
|
|
217
|
+
}.toString());
|
|
218
|
+
if (!matchResult?.pass) {
|
|
219
|
+
await context.commands.writeFile(`${filepath}.actual.png`, actual.toString('base64'), 'base64');
|
|
220
|
+
if (matchResult?.diff)
|
|
221
|
+
await context.commands.writeFile(`${filepath}.diff.png`, matchResult.diff.toString('base64'), 'base64');
|
|
222
|
+
throw new Error(`Screenshot does not match the snapshot.
|
|
223
|
+
Diff percentage: ${matchResult?.diffPercentage?.toFixed(2) ?? '100'}%
|
|
224
|
+
Max allowed: ${screenshotOpts.maxDiffPercentage}%
|
|
225
|
+
Diffs at: ${filepath}.(diff|actual).png`);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
162
228
|
/**
|
|
163
229
|
* Waits for a certain amount of time
|
|
164
230
|
* @param ms number
|
|
231
|
+
* @deprecated use `wait` method instead
|
|
165
232
|
*/
|
|
166
233
|
async waitForTimeout(ms) {
|
|
167
234
|
await new Promise(r => setTimeout(r, ms));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"VitestBrowserWorld.cjs","sources":["../src/VitestBrowserWorld.ts"],"sourcesContent":[null],"names":["
|
|
1
|
+
{"version":3,"file":"VitestBrowserWorld.cjs","sources":["../src/VitestBrowserWorld.ts"],"sourcesContent":[null],"names":["VisualWorld","commands","Buffer","Before"],"mappings":"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"}
|
|
@@ -1,33 +1,50 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import type { BrowserPage, Locator, UserEvent, ScreenshotOptions } from '@vitest/browser/context';
|
|
1
|
+
import type { BrowserPage, Locator, UserEvent } from '@vitest/browser/context';
|
|
3
2
|
import type { TestContext } from 'vitest';
|
|
4
|
-
import { InfoConstructor } from 'quickpickle
|
|
5
|
-
export
|
|
3
|
+
import { ScreenshotComparisonOptions, VisualConfigSetting, VisualWorld, VisualWorldInterface, type InfoConstructor } from 'quickpickle';
|
|
4
|
+
export interface VitestWorldConfigSetting extends VisualConfigSetting {
|
|
6
5
|
componentDir?: string;
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
}
|
|
7
|
+
export declare const defaultVitestWorldConfig: VitestWorldConfigSetting;
|
|
8
|
+
export type ActionsInterface = {
|
|
9
|
+
clicks: any[];
|
|
10
|
+
doubleClicks: any[];
|
|
9
11
|
};
|
|
10
|
-
export
|
|
11
|
-
|
|
12
|
+
export interface VitestBrowserWorldInterface extends VisualWorldInterface {
|
|
13
|
+
/**
|
|
14
|
+
* The `render` function must be provided by the World Constructor
|
|
15
|
+
* and must be tailored for the framework being used. It should render
|
|
16
|
+
* the component, and then use the parent element to set the `page` property
|
|
17
|
+
* of the World.
|
|
18
|
+
*
|
|
19
|
+
* @param name string|any The compoenent to render
|
|
20
|
+
* @param props any The properties to use when rendering the component
|
|
21
|
+
* @param renderOptions any Options to pass to the render function
|
|
22
|
+
* @returns Promise<void>
|
|
23
|
+
*/
|
|
12
24
|
render: (name: string | any, props?: any, renderOptions?: any) => Promise<void>;
|
|
13
|
-
|
|
25
|
+
/**
|
|
26
|
+
* The `cleanup` function must be provided by the World Constructor
|
|
27
|
+
* and must be tailored for the framework being used.
|
|
28
|
+
*
|
|
29
|
+
* @returns void
|
|
30
|
+
*/
|
|
14
31
|
cleanup: () => Promise<void>;
|
|
15
|
-
|
|
16
|
-
|
|
32
|
+
actions: ActionsInterface;
|
|
33
|
+
browserPage: BrowserPage;
|
|
34
|
+
page: Locator;
|
|
17
35
|
userEvent: UserEvent;
|
|
18
|
-
}
|
|
19
|
-
export declare class VitestBrowserWorld extends
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
page: BrowserPage;
|
|
36
|
+
}
|
|
37
|
+
export declare class VitestBrowserWorld extends VisualWorld implements VitestBrowserWorldInterface {
|
|
38
|
+
actions: ActionsInterface;
|
|
39
|
+
browserPage: BrowserPage;
|
|
23
40
|
userEvent: UserEvent;
|
|
24
|
-
constructor(context: TestContext, info: InfoConstructor);
|
|
25
|
-
init(): Promise<void>;
|
|
26
41
|
render(name: string | any, props?: any, renderOptions?: any): Promise<void>;
|
|
27
42
|
cleanup(): Promise<void>;
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
43
|
+
_page: Locator | null;
|
|
44
|
+
constructor(context: TestContext, info: InfoConstructor);
|
|
45
|
+
init(): Promise<void>;
|
|
46
|
+
get page(): Locator;
|
|
47
|
+
set page(value: HTMLElement);
|
|
31
48
|
/**
|
|
32
49
|
* Gets a locator based on a certain logic
|
|
33
50
|
* @example getLocator(page, 'Cancel', 'button') => page.getByRole('button', { name: 'Cancel' })
|
|
@@ -95,9 +112,16 @@ export declare class VitestBrowserWorld extends QuickPickleWorld implements Vite
|
|
|
95
112
|
* @param toBeVisible whether the element should be visible
|
|
96
113
|
*/
|
|
97
114
|
expectElement(locator: Locator, toBePresent?: boolean, toBeVisible?: boolean): Promise<void>;
|
|
115
|
+
screenshot(opts?: {
|
|
116
|
+
bufferOnly?: boolean;
|
|
117
|
+
name?: string;
|
|
118
|
+
locator?: Locator;
|
|
119
|
+
}): Promise<any>;
|
|
120
|
+
expectScreenshotMatch(locator: Locator, filename?: string, opts?: ScreenshotComparisonOptions): Promise<void>;
|
|
98
121
|
/**
|
|
99
122
|
* Waits for a certain amount of time
|
|
100
123
|
* @param ms number
|
|
124
|
+
* @deprecated use `wait` method instead
|
|
101
125
|
*/
|
|
102
126
|
waitForTimeout(ms: number): Promise<void>;
|
|
103
127
|
}
|
|
@@ -1,44 +1,53 @@
|
|
|
1
|
-
import { Before,
|
|
1
|
+
import { Before, VisualWorld } from 'quickpickle';
|
|
2
2
|
import { defaultsDeep } from 'lodash-es';
|
|
3
|
+
import { commands } from '@vitest/browser/context';
|
|
4
|
+
import { Buffer } from 'buffer';
|
|
3
5
|
|
|
4
6
|
const defaultVitestWorldConfig = {
|
|
5
7
|
componentDir: '', // directory in which components are kept, relative to project root
|
|
6
8
|
screenshotDir: 'screenshots', // directory in which to save screenshots, relative to project root (default: "screenshots")
|
|
7
|
-
|
|
9
|
+
screenshotOpts: {
|
|
10
|
+
threshold: 0.1,
|
|
11
|
+
alpha: 0.6,
|
|
12
|
+
maxDiffPercentage: .01,
|
|
13
|
+
},
|
|
8
14
|
};
|
|
9
|
-
class VitestBrowserWorld extends
|
|
15
|
+
class VitestBrowserWorld extends VisualWorld {
|
|
16
|
+
async render(name, props, renderOptions) { }
|
|
17
|
+
;
|
|
18
|
+
async cleanup() { }
|
|
19
|
+
;
|
|
10
20
|
constructor(context, info) {
|
|
11
21
|
info = defaultsDeep(info || {}, { config: { worldConfig: defaultVitestWorldConfig } });
|
|
12
22
|
super(context, info);
|
|
23
|
+
this.actions = {
|
|
24
|
+
clicks: [],
|
|
25
|
+
doubleClicks: [],
|
|
26
|
+
};
|
|
13
27
|
if (!info.config.worldConfig.screenshotDir && info.config.worldConfig?.screenshotOptions?.customSnapshotsDir) {
|
|
14
28
|
this.info.config.worldConfig.screenshotDir = info.config.worldConfig.screenshotOptions.customSnapshotsDir;
|
|
15
29
|
}
|
|
16
|
-
this.renderFn = () => { };
|
|
17
|
-
this.cleanupFn = () => { };
|
|
18
30
|
}
|
|
19
31
|
async init() {
|
|
20
32
|
let browserContext = await import('@vitest/browser/context');
|
|
21
|
-
this.
|
|
33
|
+
this.browserPage = browserContext.page;
|
|
22
34
|
this.userEvent = browserContext.userEvent;
|
|
23
35
|
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
await this.renderFn(component, props, renderOptions);
|
|
29
|
-
}
|
|
30
|
-
;
|
|
31
|
-
async cleanup() {
|
|
32
|
-
await this.cleanupFn();
|
|
33
|
-
}
|
|
34
|
-
sanitizeFilepath(filepath) {
|
|
35
|
-
return filepath.replace(/\/\/+/g, '/').replace(/\/[\.~]+\//g, '/');
|
|
36
|
-
}
|
|
37
|
-
get screenshotDir() {
|
|
38
|
-
return this.sanitizeFilepath(`${this.projectRoot}/${this.worldConfig.screenshotDir}`);
|
|
36
|
+
get page() {
|
|
37
|
+
if (!this._page)
|
|
38
|
+
throw new Error('You must render a component before running tests.');
|
|
39
|
+
return this._page;
|
|
39
40
|
}
|
|
40
|
-
|
|
41
|
-
|
|
41
|
+
set page(value) {
|
|
42
|
+
while (value.parentNode !== null && value.nodeName !== 'BODY')
|
|
43
|
+
value = value.parentNode;
|
|
44
|
+
this._page = this.browserPage.elementLocator(value);
|
|
45
|
+
value.addEventListener('click', (e) => {
|
|
46
|
+
this.actions.clicks.push(e.target);
|
|
47
|
+
});
|
|
48
|
+
value.addEventListener('dblclick', (e) => {
|
|
49
|
+
this.actions.doubleClicks.push(e.target);
|
|
50
|
+
});
|
|
42
51
|
}
|
|
43
52
|
/**
|
|
44
53
|
* Gets a locator based on a certain logic
|
|
@@ -109,6 +118,8 @@ class VitestBrowserWorld extends QuickPickleWorld {
|
|
|
109
118
|
async scroll(locator, direction, px = 100) {
|
|
110
119
|
let horiz = direction.includes('t');
|
|
111
120
|
let el = await locator.element();
|
|
121
|
+
if (el.nodeName === 'BODY' && el.parentElement)
|
|
122
|
+
el = el.parentElement;
|
|
112
123
|
if (horiz)
|
|
113
124
|
await el.scrollBy(direction === 'right' ? px : -px, 0);
|
|
114
125
|
else
|
|
@@ -157,9 +168,65 @@ class VitestBrowserWorld extends QuickPickleWorld {
|
|
|
157
168
|
if (toBePresent === (matchingElements.length === 0))
|
|
158
169
|
throw new Error(`The${toBeVisible ? '' : ' hidden'} element "${locator}" was unexpectedly ${toBePresent ? 'not present' : 'present'}.`);
|
|
159
170
|
}
|
|
171
|
+
async screenshot(opts) {
|
|
172
|
+
let path;
|
|
173
|
+
if (!opts?.bufferOnly)
|
|
174
|
+
path = this.getScreenshotPath(opts?.name);
|
|
175
|
+
let locator = opts?.locator ?? this.page;
|
|
176
|
+
return locator.screenshot({ path });
|
|
177
|
+
}
|
|
178
|
+
async expectScreenshotMatch(locator, filename, opts = {}) {
|
|
179
|
+
const filepath = this.getScreenshotPath(filename);
|
|
180
|
+
let expectedImg;
|
|
181
|
+
/**
|
|
182
|
+
* Load existing screenshot, or save it if it doesn't yet exist
|
|
183
|
+
*/
|
|
184
|
+
try {
|
|
185
|
+
expectedImg = await commands.readFile(this.getScreenshotPath(filename), 'base64');
|
|
186
|
+
}
|
|
187
|
+
catch (e) {
|
|
188
|
+
// If the screenshot doesn't exist, save it and pass the test
|
|
189
|
+
await locator.screenshot();
|
|
190
|
+
console.warn(`new visual regression test: ${this.screenshotDir}/${this.screenshotFilename}`);
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
let expected = Buffer.from(expectedImg, 'base64');
|
|
194
|
+
/**
|
|
195
|
+
* Get the screenshot
|
|
196
|
+
*/
|
|
197
|
+
// type does not include the "save" option in the docs: see https://vitest.dev/guide/browser/locators#screenshot
|
|
198
|
+
let screenshotOptions = { save: false, base64: true };
|
|
199
|
+
let actualImg = await locator.screenshot(screenshotOptions);
|
|
200
|
+
let actual = Buffer.from(typeof actualImg === 'string' ? actualImg : actualImg.base64, 'base64');
|
|
201
|
+
/**
|
|
202
|
+
* Compare the two screenshots
|
|
203
|
+
*/
|
|
204
|
+
let screenshotOpts = defaultsDeep(opts, this.worldConfig.screenshotOpts);
|
|
205
|
+
let matchResult = null;
|
|
206
|
+
try {
|
|
207
|
+
matchResult = await this.screenshotDiff(actual, expected, screenshotOpts);
|
|
208
|
+
}
|
|
209
|
+
catch (e) { }
|
|
210
|
+
console.log({
|
|
211
|
+
pass: matchResult?.pass,
|
|
212
|
+
diffPercentage: matchResult?.diffPercentage,
|
|
213
|
+
filename,
|
|
214
|
+
locator,
|
|
215
|
+
}.toString());
|
|
216
|
+
if (!matchResult?.pass) {
|
|
217
|
+
await commands.writeFile(`${filepath}.actual.png`, actual.toString('base64'), 'base64');
|
|
218
|
+
if (matchResult?.diff)
|
|
219
|
+
await commands.writeFile(`${filepath}.diff.png`, matchResult.diff.toString('base64'), 'base64');
|
|
220
|
+
throw new Error(`Screenshot does not match the snapshot.
|
|
221
|
+
Diff percentage: ${matchResult?.diffPercentage?.toFixed(2) ?? '100'}%
|
|
222
|
+
Max allowed: ${screenshotOpts.maxDiffPercentage}%
|
|
223
|
+
Diffs at: ${filepath}.(diff|actual).png`);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
160
226
|
/**
|
|
161
227
|
* Waits for a certain amount of time
|
|
162
228
|
* @param ms number
|
|
229
|
+
* @deprecated use `wait` method instead
|
|
163
230
|
*/
|
|
164
231
|
async waitForTimeout(ms) {
|
|
165
232
|
await new Promise(r => setTimeout(r, ms));
|