@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
|
@@ -2,37 +2,69 @@ import { Before, QuickPickleWorld, QuickPickleWorldInterface } from 'quickpickle
|
|
|
2
2
|
import type { BrowserPage, Locator, UserEvent, ScreenshotOptions } from '@vitest/browser/context'
|
|
3
3
|
import { defaultsDeep } from 'lodash-es'
|
|
4
4
|
import type { TestContext } from 'vitest';
|
|
5
|
-
import { InfoConstructor } from 'quickpickle
|
|
5
|
+
import { ScreenshotComparisonOptions, VisualConfigSetting, VisualWorld, VisualWorldInterface, type InfoConstructor } from 'quickpickle';
|
|
6
|
+
import { commands } from '@vitest/browser/context';
|
|
7
|
+
import { Buffer } from 'buffer'
|
|
8
|
+
|
|
6
9
|
|
|
7
10
|
/// <reference types="@vitest/browser/providers/playwright" />
|
|
8
11
|
|
|
9
|
-
export
|
|
12
|
+
export interface VitestWorldConfigSetting extends VisualConfigSetting {
|
|
10
13
|
componentDir?: string;
|
|
11
|
-
screenshotDir?: string;
|
|
12
|
-
screenshotOptions?: Partial<ScreenshotOptions>;
|
|
13
14
|
}
|
|
14
15
|
|
|
15
|
-
export const defaultVitestWorldConfig:
|
|
16
|
+
export const defaultVitestWorldConfig:VitestWorldConfigSetting = {
|
|
16
17
|
componentDir: '', // directory in which components are kept, relative to project root
|
|
17
18
|
screenshotDir: 'screenshots', // directory in which to save screenshots, relative to project root (default: "screenshots")
|
|
18
|
-
|
|
19
|
+
screenshotOpts: { // options for the default screenshot comparisons
|
|
20
|
+
threshold: 0.1,
|
|
21
|
+
alpha: 0.6,
|
|
22
|
+
maxDiffPercentage: .01,
|
|
23
|
+
},
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export type ActionsInterface = {
|
|
27
|
+
clicks: any[];
|
|
28
|
+
doubleClicks: any[];
|
|
19
29
|
}
|
|
20
30
|
|
|
21
|
-
export
|
|
31
|
+
export interface VitestBrowserWorldInterface extends VisualWorldInterface {
|
|
32
|
+
/**
|
|
33
|
+
* The `render` function must be provided by the World Constructor
|
|
34
|
+
* and must be tailored for the framework being used. It should render
|
|
35
|
+
* the component, and then use the parent element to set the `page` property
|
|
36
|
+
* of the World.
|
|
37
|
+
*
|
|
38
|
+
* @param name string|any The compoenent to render
|
|
39
|
+
* @param props any The properties to use when rendering the component
|
|
40
|
+
* @param renderOptions any Options to pass to the render function
|
|
41
|
+
* @returns Promise<void>
|
|
42
|
+
*/
|
|
22
43
|
render: (name:string|any, props?:any, renderOptions?:any)=>Promise<void>;
|
|
23
|
-
|
|
44
|
+
/**
|
|
45
|
+
* The `cleanup` function must be provided by the World Constructor
|
|
46
|
+
* and must be tailored for the framework being used.
|
|
47
|
+
*
|
|
48
|
+
* @returns void
|
|
49
|
+
*/
|
|
24
50
|
cleanup: ()=>Promise<void>;
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
51
|
+
actions: ActionsInterface
|
|
52
|
+
browserPage: BrowserPage;
|
|
53
|
+
page: Locator;
|
|
54
|
+
userEvent: UserEvent;
|
|
28
55
|
}
|
|
29
56
|
|
|
30
|
-
export class VitestBrowserWorld extends
|
|
57
|
+
export class VitestBrowserWorld extends VisualWorld implements VitestBrowserWorldInterface {
|
|
31
58
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
59
|
+
actions:ActionsInterface = {
|
|
60
|
+
clicks: [],
|
|
61
|
+
doubleClicks: [],
|
|
62
|
+
};
|
|
63
|
+
browserPage!: BrowserPage;
|
|
35
64
|
userEvent!: UserEvent;
|
|
65
|
+
async render(name:string|any,props?:any,renderOptions?:any){};
|
|
66
|
+
async cleanup(){};
|
|
67
|
+
_page!:Locator|null;
|
|
36
68
|
|
|
37
69
|
constructor(context:TestContext, info:InfoConstructor) {
|
|
38
70
|
info = defaultsDeep(info || {}, { config: { worldConfig: defaultVitestWorldConfig } } )
|
|
@@ -40,40 +72,30 @@ export class VitestBrowserWorld extends QuickPickleWorld implements VitestBrowse
|
|
|
40
72
|
if (!info.config.worldConfig.screenshotDir && info.config.worldConfig?.screenshotOptions?.customSnapshotsDir) {
|
|
41
73
|
this.info.config.worldConfig.screenshotDir = info.config.worldConfig.screenshotOptions.customSnapshotsDir
|
|
42
74
|
}
|
|
43
|
-
this.renderFn = ()=>{};
|
|
44
|
-
this.cleanupFn = ()=>{};
|
|
45
75
|
}
|
|
46
76
|
|
|
47
77
|
async init() {
|
|
48
78
|
let browserContext = await import('@vitest/browser/context')
|
|
49
|
-
this.
|
|
79
|
+
this.browserPage = browserContext.page;
|
|
50
80
|
this.userEvent = browserContext.userEvent;
|
|
51
81
|
}
|
|
52
82
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
: name
|
|
57
|
-
await this.renderFn(component, props, renderOptions)
|
|
58
|
-
};
|
|
59
|
-
|
|
60
|
-
async cleanup() {
|
|
61
|
-
await this.cleanupFn()
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
sanitizeFilepath(filepath:string) {
|
|
65
|
-
return filepath.replace(/\/\/+/g, '/').replace(/\/[\.~]+\//g, '/')
|
|
83
|
+
get page():Locator {
|
|
84
|
+
if (!this._page) throw new Error('You must render a component before running tests.')
|
|
85
|
+
return this._page
|
|
66
86
|
}
|
|
67
87
|
|
|
68
|
-
|
|
69
|
-
|
|
88
|
+
set page(value:HTMLElement) {
|
|
89
|
+
while (value.parentNode !== null && value.nodeName !== 'BODY') value = value.parentNode as HTMLBodyElement
|
|
90
|
+
this._page = this.browserPage.elementLocator(value)
|
|
91
|
+
value.addEventListener('click', (e)=>{
|
|
92
|
+
this.actions.clicks.push(e.target)
|
|
93
|
+
})
|
|
94
|
+
value.addEventListener('dblclick', (e)=>{
|
|
95
|
+
this.actions.doubleClicks.push(e.target)
|
|
96
|
+
})
|
|
70
97
|
}
|
|
71
98
|
|
|
72
|
-
get screenshotFilename() {
|
|
73
|
-
return `${this.toString().replace(/^.+?Feature: /, '').replace(' ' + this.info.step, '')}.png`
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
|
|
77
99
|
/**
|
|
78
100
|
* Gets a locator based on a certain logic
|
|
79
101
|
* @example getLocator(page, 'Cancel', 'button') => page.getByRole('button', { name: 'Cancel' })
|
|
@@ -138,6 +160,7 @@ export class VitestBrowserWorld extends QuickPickleWorld implements VitestBrowse
|
|
|
138
160
|
async scroll(locator:Locator, direction:"up"|"down"|"left"|"right", px = 100) {
|
|
139
161
|
let horiz = direction.includes('t')
|
|
140
162
|
let el = await locator.element()
|
|
163
|
+
if (el.nodeName === 'BODY' && el.parentElement) el = el.parentElement
|
|
141
164
|
if (horiz) await el.scrollBy(direction === 'right' ? px : -px, 0)
|
|
142
165
|
else await el.scrollBy(0, direction === 'down' ? px : -px)
|
|
143
166
|
}
|
|
@@ -186,9 +209,75 @@ export class VitestBrowserWorld extends QuickPickleWorld implements VitestBrowse
|
|
|
186
209
|
if (toBePresent === (matchingElements.length === 0)) throw new Error(`The${toBeVisible ? '' : ' hidden'} element "${locator}" was unexpectedly ${toBePresent ? 'not present' : 'present'}.`)
|
|
187
210
|
}
|
|
188
211
|
|
|
212
|
+
async screenshot(opts?:{
|
|
213
|
+
bufferOnly?:boolean
|
|
214
|
+
name?:string
|
|
215
|
+
locator?:Locator
|
|
216
|
+
}):Promise<any> {
|
|
217
|
+
let path
|
|
218
|
+
if (!opts?.bufferOnly) path = this.getScreenshotPath(opts?.name)
|
|
219
|
+
let locator = opts?.locator ?? this.page
|
|
220
|
+
return locator.screenshot({ path })
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
async expectScreenshotMatch(locator:Locator, filename?:string, opts:ScreenshotComparisonOptions={}) {
|
|
224
|
+
|
|
225
|
+
const filepath = this.getScreenshotPath(filename)
|
|
226
|
+
let expectedImg:string
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Load existing screenshot, or save it if it doesn't yet exist
|
|
230
|
+
*/
|
|
231
|
+
try {
|
|
232
|
+
expectedImg = await commands.readFile(this.getScreenshotPath(filename), 'base64')
|
|
233
|
+
}
|
|
234
|
+
catch(e) {
|
|
235
|
+
// If the screenshot doesn't exist, save it and pass the test
|
|
236
|
+
await locator.screenshot()
|
|
237
|
+
console.warn(`new visual regression test: ${this.screenshotDir}/${this.screenshotFilename}`)
|
|
238
|
+
return
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
let expected = Buffer.from(expectedImg, 'base64')
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Get the screenshot
|
|
245
|
+
*/
|
|
246
|
+
// type does not include the "save" option in the docs: see https://vitest.dev/guide/browser/locators#screenshot
|
|
247
|
+
let screenshotOptions = { save:false, base64:true } as ScreenshotOptions
|
|
248
|
+
let actualImg = await locator.screenshot(screenshotOptions) as string|{ base64:string }
|
|
249
|
+
let actual = Buffer.from(typeof actualImg === 'string' ? actualImg : actualImg.base64, 'base64')
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Compare the two screenshots
|
|
253
|
+
*/
|
|
254
|
+
let screenshotOpts = defaultsDeep(opts, this.worldConfig.screenshotOpts)
|
|
255
|
+
let matchResult = null
|
|
256
|
+
try {
|
|
257
|
+
matchResult = await this.screenshotDiff(actual, expected, screenshotOpts)
|
|
258
|
+
}
|
|
259
|
+
catch(e) {}
|
|
260
|
+
console.log({
|
|
261
|
+
pass: matchResult?.pass,
|
|
262
|
+
diffPercentage: matchResult?.diffPercentage,
|
|
263
|
+
filename,
|
|
264
|
+
locator,
|
|
265
|
+
}.toString())
|
|
266
|
+
|
|
267
|
+
if (!matchResult?.pass) {
|
|
268
|
+
await commands.writeFile(`${filepath}.actual.png`, actual.toString('base64'), 'base64');
|
|
269
|
+
if (matchResult?.diff) await commands.writeFile(`${filepath}.diff.png`, matchResult.diff.toString('base64'), 'base64');
|
|
270
|
+
throw new Error(`Screenshot does not match the snapshot.
|
|
271
|
+
Diff percentage: ${matchResult?.diffPercentage?.toFixed(2) ?? '100'}%
|
|
272
|
+
Max allowed: ${screenshotOpts.maxDiffPercentage}%
|
|
273
|
+
Diffs at: ${filepath}.(diff|actual).png`)
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
189
277
|
/**
|
|
190
278
|
* Waits for a certain amount of time
|
|
191
279
|
* @param ms number
|
|
280
|
+
* @deprecated use `wait` method instead
|
|
192
281
|
*/
|
|
193
282
|
async waitForTimeout(ms:number) {
|
|
194
283
|
await new Promise(r => setTimeout(r, ms))
|
package/src/actions.steps.ts
CHANGED
|
@@ -148,15 +148,23 @@ When('I uncheck (the ){string}( checkbox)( box)', async function (world:VitestBr
|
|
|
148
148
|
// })
|
|
149
149
|
|
|
150
150
|
When('I wait (for ){int}ms', async function (world:VitestBrowserWorld, num) {
|
|
151
|
-
await world.
|
|
151
|
+
await world.wait(num)
|
|
152
152
|
})
|
|
153
153
|
When('I wait (for ){float} second(s)', async function (world:VitestBrowserWorld, num) {
|
|
154
|
-
await world.
|
|
154
|
+
await world.wait(num * 1000)
|
|
155
155
|
})
|
|
156
156
|
|
|
157
157
|
// ================
|
|
158
158
|
// Scrolling
|
|
159
159
|
|
|
160
|
+
When('I scroll down/up/left/right', async function (world:VitestBrowserWorld) {
|
|
161
|
+
let direction = world.info.step?.match(/(down|up|left|right)$/)![0] as 'down'|'up'|'left'|'right'
|
|
162
|
+
await world.scroll(world.page, direction)
|
|
163
|
+
})
|
|
164
|
+
When('I scroll down/up/left/right {int}(px)( pixels)', async function (world:VitestBrowserWorld, num) {
|
|
165
|
+
let direction = world.info.step?.match(/(down|up|left|right)(?= \d)/)![0] as 'down'|'up'|'left'|'right'
|
|
166
|
+
await world.scroll(world.page, direction, num)
|
|
167
|
+
})
|
|
160
168
|
When('I scroll (the ){string} {word} down/up/left/right', async function (world:VitestBrowserWorld, identifier:string, role:string) {
|
|
161
169
|
let locator = world.getLocator(world.page, identifier, role)
|
|
162
170
|
let direction = world.info.step?.match(/(down|up|left|right)$/)![0] as 'down'|'up'|'left'|'right'
|
|
@@ -167,3 +175,21 @@ When('I scroll (the ){string} {word} down/up/left/right {int}(px)( pixels)', asy
|
|
|
167
175
|
let direction = world.info.step?.match(/(down|up|left|right)(?= \d)/)![0] as 'down'|'up'|'left'|'right'
|
|
168
176
|
await world.scroll(locator, direction, num)
|
|
169
177
|
})
|
|
178
|
+
|
|
179
|
+
// ================
|
|
180
|
+
// Screenshots
|
|
181
|
+
|
|
182
|
+
Then('(I )take (a )screenshot', async function (world:VitestBrowserWorld) {
|
|
183
|
+
await world.screenshot()
|
|
184
|
+
})
|
|
185
|
+
Then('(I )take (a )screenshot named {string}', async function (world:VitestBrowserWorld, name:string) {
|
|
186
|
+
await world.screenshot({ name })
|
|
187
|
+
})
|
|
188
|
+
Then('(I )take (a )screenshot of the {string} {word}', async function (world:VitestBrowserWorld, identifier:string, role:string) {
|
|
189
|
+
let locator = world.getLocator(world.page, identifier, role)
|
|
190
|
+
await world.screenshot({ locator })
|
|
191
|
+
})
|
|
192
|
+
Then('(I )take (a )screenshot of the {string} {word} named {string}', async function (world:VitestBrowserWorld, identifier:string, role:string, name:string) {
|
|
193
|
+
let locator = world.getLocator(world.page, identifier, role)
|
|
194
|
+
await world.screenshot({ locator, name })
|
|
195
|
+
})
|
|
@@ -3,6 +3,17 @@ import { render, cleanup } from 'vitest-browser-react'
|
|
|
3
3
|
import type { TestContext } from "vitest";
|
|
4
4
|
import type { InfoConstructor } from "quickpickle/dist/world";
|
|
5
5
|
import React from 'react';
|
|
6
|
+
import { defaultsDeep } from 'lodash-es'
|
|
7
|
+
|
|
8
|
+
export const defaultReactOptions = {
|
|
9
|
+
/**
|
|
10
|
+
* The default extension to use when rendering components
|
|
11
|
+
* (if none is provided in the step)
|
|
12
|
+
*/
|
|
13
|
+
defaultComponentExtension: 'tsx',
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export type ReactWorldConfigSetting = Partial<typeof defaultReactOptions>
|
|
6
17
|
|
|
7
18
|
export class ReactBrowserWorld extends VitestBrowserWorld {
|
|
8
19
|
|
|
@@ -10,19 +21,18 @@ export class ReactBrowserWorld extends VitestBrowserWorld {
|
|
|
10
21
|
cleanupFn = cleanup
|
|
11
22
|
|
|
12
23
|
constructor(context:TestContext, info:InfoConstructor) {
|
|
24
|
+
info.config.worldConfig = defaultsDeep(info.config.worldConfig, defaultReactOptions)
|
|
13
25
|
super(context, info)
|
|
14
26
|
}
|
|
15
27
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
28
|
+
async render(name:string|any, props?:any, renderOptions?:any) {
|
|
29
|
+
let Component:any;
|
|
30
|
+
// Set the default extension if not provided
|
|
31
|
+
if (typeof name === 'string' && !name.match(/\.[jt]sx?$/))
|
|
32
|
+
name += '.' + this.worldConfig.defaultComponentExtension;
|
|
20
33
|
if (typeof name === 'string') {
|
|
21
34
|
// dynamic import returns the module object
|
|
22
|
-
|
|
23
|
-
`${this.projectRoot}/${this.worldConfig.componentDir}/${name}`.replace(/\/+/g,'/')
|
|
24
|
-
/* @vite-ignore */
|
|
25
|
-
);
|
|
35
|
+
let mod = await import(`${this.fullPath(`${this.worldConfig.componentDir}/${name}`)}` /* @vite-ignore */ )
|
|
26
36
|
// try .default first, then fall back to any other export
|
|
27
37
|
Component = mod.default ?? Object.values(mod)[0];
|
|
28
38
|
if (!Component) {
|
|
@@ -35,7 +45,10 @@ export class ReactBrowserWorld extends VitestBrowserWorld {
|
|
|
35
45
|
}
|
|
36
46
|
|
|
37
47
|
// now call reactRender with the actual component
|
|
38
|
-
await
|
|
48
|
+
let result = await render(React.createElement(Component, props), renderOptions);
|
|
49
|
+
this.page = result.container;
|
|
39
50
|
}
|
|
40
51
|
|
|
52
|
+
async cleanup() { await cleanup(); }
|
|
53
|
+
|
|
41
54
|
}
|
|
@@ -5,11 +5,19 @@ import { InfoConstructor } from "quickpickle/dist/world";
|
|
|
5
5
|
|
|
6
6
|
export class SvelteBrowserWorld extends VitestBrowserWorld {
|
|
7
7
|
|
|
8
|
-
renderFn = render
|
|
9
|
-
cleanupFn = cleanup
|
|
10
|
-
|
|
11
8
|
constructor(context:TestContext, info:InfoConstructor) {
|
|
12
9
|
super(context, info)
|
|
13
10
|
}
|
|
14
11
|
|
|
12
|
+
async render(name:string|any, props?:any, renderOptions?:any) {
|
|
13
|
+
if (typeof name === 'string' && !name.match(/\.svelte$/)) name += '.svelte'
|
|
14
|
+
let component = typeof name === 'string'
|
|
15
|
+
? await import(`${this.fullPath(`${this.worldConfig.componentDir}/${name}`)}` /* @vite-ignore */ )
|
|
16
|
+
: name
|
|
17
|
+
let result = await render(component, props, renderOptions)
|
|
18
|
+
this.page = result.container
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
async cleanup() { await cleanup(); }
|
|
22
|
+
|
|
15
23
|
}
|
|
@@ -5,19 +5,20 @@ import type { InfoConstructor } from "quickpickle/dist/world";
|
|
|
5
5
|
|
|
6
6
|
export class VueBrowserWorld extends VitestBrowserWorld {
|
|
7
7
|
|
|
8
|
-
renderFn = render
|
|
9
|
-
cleanupFn = cleanup
|
|
10
|
-
|
|
11
8
|
constructor(context:TestContext, info:InfoConstructor) {
|
|
12
9
|
super(context, info)
|
|
13
10
|
}
|
|
14
11
|
|
|
15
12
|
async render(name:string|any, props?:any, renderOptions?:any) {
|
|
13
|
+
if (typeof name === 'string' && !name.match(/\.vue$/)) name += '.vue'
|
|
16
14
|
let mod = typeof name === 'string'
|
|
17
|
-
? await import(`${this.
|
|
15
|
+
? await import(`${this.fullPath(`${this.worldConfig.componentDir}/${name}`)}` /* @vite-ignore */ )
|
|
18
16
|
: name;
|
|
19
17
|
let component = mod.default ?? mod;
|
|
20
|
-
await
|
|
18
|
+
let result = await render(component, { props, ...renderOptions })
|
|
19
|
+
this.page = result.container
|
|
21
20
|
};
|
|
22
21
|
|
|
22
|
+
async cleanup() { await cleanup(); }
|
|
23
|
+
|
|
23
24
|
}
|
package/src/outcomes.steps.ts
CHANGED
|
@@ -1,9 +1,33 @@
|
|
|
1
1
|
import { Then } from "quickpickle";
|
|
2
2
|
import type { VitestBrowserWorld } from "./VitestBrowserWorld";
|
|
3
3
|
import { expect } from 'vitest'
|
|
4
|
+
import { BrowserPage, Locator } from "@vitest/browser/context";
|
|
4
5
|
|
|
5
6
|
/// <reference types="@vitest/browser/providers/playwright" />
|
|
6
7
|
|
|
8
|
+
expect.extend({
|
|
9
|
+
async toBeInTheViewport(locator:Locator) {
|
|
10
|
+
let frame = await locator.element()
|
|
11
|
+
while (frame.parentElement && frame.tagName !== 'HTML') {
|
|
12
|
+
frame = frame.parentElement
|
|
13
|
+
}
|
|
14
|
+
const viewport = await frame.getBoundingClientRect();
|
|
15
|
+
const rect = await locator.element().getBoundingClientRect();
|
|
16
|
+
if (rect) {
|
|
17
|
+
const inViewport = (rect.right > 0 && rect.bottom > 0 && rect.top < viewport.height && rect.left < viewport.width);
|
|
18
|
+
return {
|
|
19
|
+
message: () => `expected ${locator.toString()} to be in viewport`,
|
|
20
|
+
pass: inViewport,
|
|
21
|
+
};
|
|
22
|
+
} else {
|
|
23
|
+
return {
|
|
24
|
+
message: () => `could not get bounding box for ${locator.toString()}`,
|
|
25
|
+
pass: false,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
})
|
|
30
|
+
|
|
7
31
|
// ================
|
|
8
32
|
// Text on page
|
|
9
33
|
|
|
@@ -39,6 +63,21 @@ Then('I should not/NOT see a/an/the {string} (element )with (the )(text ){string
|
|
|
39
63
|
await world.expectElement(locator, false)
|
|
40
64
|
})
|
|
41
65
|
|
|
66
|
+
// ================
|
|
67
|
+
// Actions
|
|
68
|
+
Then('{string} should have been clicked/doubleclicked/dblclicked', async function (world:VitestBrowserWorld, text) {
|
|
69
|
+
let single = world.info.step?.match(/ clicked$/)
|
|
70
|
+
if (single) expect(world.actions.clicks.find((el) => (el && el?.textContent === text))).not.toBeNull()
|
|
71
|
+
else expect(world.actions.doubleClicks.find((el) => (el && el?.textContent === text))).not.toBeNull()
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
Then('(the ){string} {word} should have been clicked/doubleclicked/dblclicked', async function (world:VitestBrowserWorld, identifier, role) {
|
|
75
|
+
let single = world.info.step?.match(/ clicked$/)
|
|
76
|
+
let element = world.getLocator(world.page, identifier, role)
|
|
77
|
+
if (single) expect(world.actions.clicks.find((el) => (el === element))).not.toBeNull()
|
|
78
|
+
else expect(world.actions.doubleClicks.find((el) => (el === element))).not.toBeNull()
|
|
79
|
+
})
|
|
80
|
+
|
|
42
81
|
// ================
|
|
43
82
|
// Element state
|
|
44
83
|
Then('a/an/the {string} {word} should be visible/hidden/invisible', async function (world:VitestBrowserWorld, identifier, role) {
|
|
@@ -116,6 +155,25 @@ Then('a/an/the {string} (element )with (the )(text ){string} should be unfocused
|
|
|
116
155
|
await expect(locator).not.toHaveFocus()
|
|
117
156
|
})
|
|
118
157
|
|
|
158
|
+
Then('a/an/the {string} {word} should be in(side) (of )the viewport', async function (world:VitestBrowserWorld, identifier, role) {
|
|
159
|
+
let locator = await world.getLocator(world.page, identifier, role)
|
|
160
|
+
// @ts-ignore
|
|
161
|
+
await expect(locator).toBeInTheViewport()
|
|
162
|
+
})
|
|
163
|
+
Then('a/an/the {string} {word} should be out(side) (of )the viewport', async function (world:VitestBrowserWorld, identifier, role) {
|
|
164
|
+
let locator = await world.getLocator(world.page, identifier, role)
|
|
165
|
+
// @ts-ignore
|
|
166
|
+
expect(locator).not.toBeInTheViewport()
|
|
167
|
+
})
|
|
168
|
+
// Then('a/an/the {string} (element )with (the )(text ){string} should be in(side) (of )the viewport', async function (world:VitestBrowserWorld, identifier, text) {
|
|
169
|
+
// let locator = await world.getLocator(world.page, identifier, 'element', text)
|
|
170
|
+
// await isInViewport(world, locator)
|
|
171
|
+
// })
|
|
172
|
+
// Then('a/an/the {string} (element )with (the )(text ){string} should be out(side) (of )the viewport', async function (world:VitestBrowserWorld, identifier, text) {
|
|
173
|
+
// let locator = await world.getLocator(world.page, identifier, 'element', text)
|
|
174
|
+
// await isInViewport(world, locator)
|
|
175
|
+
// })
|
|
176
|
+
|
|
119
177
|
// Values
|
|
120
178
|
Then('a/an/the (value of ){string} should contain/include/be/equal {string}', async function (world:VitestBrowserWorld, identifier, expected) {
|
|
121
179
|
let exact = world.info.step?.match(/ should (?:be|equal) ['"]/) ? true : false
|
|
@@ -155,36 +213,35 @@ Then('a/an/the (value of )(the ){string} {word} should not/NOT contain/include/b
|
|
|
155
213
|
}
|
|
156
214
|
})
|
|
157
215
|
|
|
216
|
+
Then('a/an/the (value of ){string} should be/equal {int}', async function (world:VitestBrowserWorld, identifier, expected) {
|
|
217
|
+
let locator = await world.getLocator(world.page, identifier, 'input')
|
|
218
|
+
await expect(locator).toHaveValue(expected)
|
|
219
|
+
})
|
|
220
|
+
Then('a/an/the (value of )(the ){string} {word} should be/equal {int}', async function (world:VitestBrowserWorld, identifier, role, expected) {
|
|
221
|
+
let locator = await world.getLocator(world.page, identifier, role)
|
|
222
|
+
await expect(locator).toHaveValue(expected)
|
|
223
|
+
})
|
|
224
|
+
Then('a/an/the (value of )(the ){string} should not/NOT be/equal {int}', async function (world:VitestBrowserWorld, identifier, expected) {
|
|
225
|
+
let locator = await world.getLocator(world.page, identifier, 'input')
|
|
226
|
+
await expect(locator).not.toHaveValue(expected)
|
|
227
|
+
})
|
|
228
|
+
Then('a/an/the (value of )(the ){string} {word} should not/NOT be/equal {int}', async function (world:VitestBrowserWorld, identifier, role, expected) {
|
|
229
|
+
let locator = await world.getLocator(world.page, identifier, role)
|
|
230
|
+
await expect(locator).not.toHaveValue(expected)
|
|
231
|
+
})
|
|
232
|
+
|
|
158
233
|
// Visual regression testing
|
|
159
234
|
Then('(the )screenshot/snapshot should match', async function (world:VitestBrowserWorld) {
|
|
160
|
-
await
|
|
161
|
-
...world.worldConfig.screenshotOptions,
|
|
162
|
-
customSnapshotsDir: world.screenshotDir,
|
|
163
|
-
customSnapshotIdentifier: world.screenshotFilename,
|
|
164
|
-
})
|
|
235
|
+
await world.expectScreenshotMatch(world.page)
|
|
165
236
|
})
|
|
166
237
|
Then('(the )screenshot/snapshot {string} should match', async function (world:VitestBrowserWorld, name:string) {
|
|
167
|
-
|
|
168
|
-
await expect(world.page).toMatchImageSnapshot({
|
|
169
|
-
...world.worldConfig.screenshotOptions,
|
|
170
|
-
customSnapshotsDir: world.screenshotDir,
|
|
171
|
-
customSnapshotIdentifier: `${name}${explodedTags}`,
|
|
172
|
-
})
|
|
238
|
+
await world.expectScreenshotMatch(world.page, name)
|
|
173
239
|
})
|
|
174
240
|
Then('(the )screenshot/snapshot of the {string} {word} should match', async function (world:VitestBrowserWorld, identifier, role) {
|
|
175
241
|
let locator = await world.getLocator(world.page, identifier, role)
|
|
176
|
-
await
|
|
177
|
-
...world.worldConfig.screenshotOptions,
|
|
178
|
-
customSnapshotsDir: world.screenshotDir,
|
|
179
|
-
customSnapshotIdentifier: world.screenshotFilename,
|
|
180
|
-
})
|
|
242
|
+
await world.expectScreenshotMatch(locator)
|
|
181
243
|
})
|
|
182
244
|
Then('(the )screenshot/snapshot {string} of the {string} {word} should match', async function (world:VitestBrowserWorld, name, identifier, role) {
|
|
183
245
|
let locator = await world.getLocator(world.page, identifier, role)
|
|
184
|
-
|
|
185
|
-
await expect(locator).toMatchImageSnapshot({
|
|
186
|
-
...world.worldConfig.screenshotOptions,
|
|
187
|
-
customSnapshotsDir: world.screenshotDir,
|
|
188
|
-
customSnapshotIdentifier: `${name}${explodedTags}`,
|
|
189
|
-
})
|
|
246
|
+
await world.expectScreenshotMatch(locator, name)
|
|
190
247
|
})
|