@quickpickle/vitest-browser 0.0.3 → 0.1.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 +41 -0
- package/README.md +70 -0
- package/dist/VitestBrowserWorld.cjs +36 -17
- package/dist/VitestBrowserWorld.cjs.map +1 -1
- package/dist/VitestBrowserWorld.d.ts +36 -10
- package/dist/VitestBrowserWorld.mjs +36 -17
- package/dist/VitestBrowserWorld.mjs.map +1 -1
- package/dist/actions.steps.cjs +24 -0
- package/dist/actions.steps.cjs.map +1 -1
- package/dist/actions.steps.mjs +25 -1
- package/dist/actions.steps.mjs.map +1 -1
- package/dist/frameworks/ReactBrowserWorld.cjs +17 -6
- package/dist/frameworks/ReactBrowserWorld.cjs.map +1 -1
- package/dist/frameworks/ReactBrowserWorld.d.ts +9 -0
- package/dist/frameworks/ReactBrowserWorld.mjs +17 -7
- package/dist/frameworks/ReactBrowserWorld.mjs.map +1 -1
- package/dist/frameworks/SvelteBrowserWorld.cjs +11 -2
- package/dist/frameworks/SvelteBrowserWorld.cjs.map +1 -1
- package/dist/frameworks/SvelteBrowserWorld.d.ts +2 -3
- package/dist/frameworks/SvelteBrowserWorld.mjs +11 -2
- package/dist/frameworks/SvelteBrowserWorld.mjs.map +1 -1
- package/dist/frameworks/VueBrowserWorld.cjs +6 -4
- package/dist/frameworks/VueBrowserWorld.cjs.map +1 -1
- package/dist/frameworks/VueBrowserWorld.d.ts +1 -3
- package/dist/frameworks/VueBrowserWorld.mjs +6 -4
- package/dist/frameworks/VueBrowserWorld.mjs.map +1 -1
- package/dist/outcomes.steps.cjs +74 -0
- package/dist/outcomes.steps.cjs.map +1 -1
- package/dist/outcomes.steps.mjs +74 -0
- package/dist/outcomes.steps.mjs.map +1 -1
- package/package.json +2 -2
- package/src/VitestBrowserWorld.ts +60 -26
- package/src/actions.steps.ts +26 -0
- 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 +75 -0
- package/tests/generic/browser-actions.feature +193 -0
- package/tests/generic/browser-generic.feature +79 -0
- package/tests/generic/browser-outcomes.feature +138 -0
- package/tests/generic/generic.steps.ts +23 -0
- package/tests/svelte/Example.svelte +153 -0
- package/vite.config.ts +8 -0
- package/vitest.workspace.ts +5 -9
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@quickpickle/vitest-browser",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.1.0",
|
|
4
4
|
"description": "Support files for running Gherkin/Cucumber tests in Vitest Browser mode, for testing components.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"BDD",
|
|
@@ -98,7 +98,7 @@
|
|
|
98
98
|
"vitest-browser-react": "^0.1.1",
|
|
99
99
|
"vitest-browser-svelte": "^0.1.0",
|
|
100
100
|
"vitest-browser-vue": "^0.2.0",
|
|
101
|
-
"quickpickle": "^1.
|
|
101
|
+
"quickpickle": "^1.8.0"
|
|
102
102
|
},
|
|
103
103
|
"scripts": {
|
|
104
104
|
"build": "rollup -c && FORMAT=cjs rollup -c",
|
|
@@ -2,7 +2,7 @@ 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/dist/world';
|
|
5
|
+
import type { InfoConstructor } from 'quickpickle/dist/world';
|
|
6
6
|
|
|
7
7
|
/// <reference types="@vitest/browser/providers/playwright" />
|
|
8
8
|
|
|
@@ -18,21 +18,48 @@ export const defaultVitestWorldConfig:VitestWorldConfig = {
|
|
|
18
18
|
screenshotOptions: {}, // options for the default screenshot comparisons
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
export type ActionsInterface = {
|
|
22
|
+
clicks: any[];
|
|
23
|
+
doubleClicks: any[];
|
|
24
|
+
}
|
|
25
|
+
|
|
21
26
|
export type VitestBrowserWorldInterface = QuickPickleWorldInterface & {
|
|
27
|
+
/**
|
|
28
|
+
* The `render` function must be provided by the World Constructor
|
|
29
|
+
* and must be tailored for the framework being used. It should render
|
|
30
|
+
* the component, and then use the parent element to set the `page` property
|
|
31
|
+
* of the World.
|
|
32
|
+
*
|
|
33
|
+
* @param name string|any The compoenent to render
|
|
34
|
+
* @param props any The properties to use when rendering the component
|
|
35
|
+
* @param renderOptions any Options to pass to the render function
|
|
36
|
+
* @returns Promise<void>
|
|
37
|
+
*/
|
|
22
38
|
render: (name:string|any, props?:any, renderOptions?:any)=>Promise<void>;
|
|
23
|
-
|
|
39
|
+
/**
|
|
40
|
+
* The `cleanup` function must be provided by the World Constructor
|
|
41
|
+
* and must be tailored for the framework being used.
|
|
42
|
+
*
|
|
43
|
+
* @returns void
|
|
44
|
+
*/
|
|
24
45
|
cleanup: ()=>Promise<void>;
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
46
|
+
actions: ActionsInterface
|
|
47
|
+
browserPage: BrowserPage;
|
|
48
|
+
page: Locator;
|
|
49
|
+
userEvent: UserEvent;
|
|
28
50
|
}
|
|
29
51
|
|
|
30
52
|
export class VitestBrowserWorld extends QuickPickleWorld implements VitestBrowserWorldInterface {
|
|
31
53
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
54
|
+
actions:ActionsInterface = {
|
|
55
|
+
clicks: [],
|
|
56
|
+
doubleClicks: [],
|
|
57
|
+
};
|
|
58
|
+
browserPage!: BrowserPage;
|
|
35
59
|
userEvent!: UserEvent;
|
|
60
|
+
async render(name:string|any,props?:any,renderOptions?:any){};
|
|
61
|
+
async cleanup(){};
|
|
62
|
+
private _page:Locator|null = null;
|
|
36
63
|
|
|
37
64
|
constructor(context:TestContext, info:InfoConstructor) {
|
|
38
65
|
info = defaultsDeep(info || {}, { config: { worldConfig: defaultVitestWorldConfig } } )
|
|
@@ -40,39 +67,45 @@ export class VitestBrowserWorld extends QuickPickleWorld implements VitestBrowse
|
|
|
40
67
|
if (!info.config.worldConfig.screenshotDir && info.config.worldConfig?.screenshotOptions?.customSnapshotsDir) {
|
|
41
68
|
this.info.config.worldConfig.screenshotDir = info.config.worldConfig.screenshotOptions.customSnapshotsDir
|
|
42
69
|
}
|
|
43
|
-
this.renderFn = ()=>{};
|
|
44
|
-
this.cleanupFn = ()=>{};
|
|
45
70
|
}
|
|
46
71
|
|
|
47
72
|
async init() {
|
|
48
73
|
let browserContext = await import('@vitest/browser/context')
|
|
49
|
-
this.
|
|
74
|
+
this.browserPage = browserContext.page;
|
|
50
75
|
this.userEvent = browserContext.userEvent;
|
|
51
76
|
}
|
|
52
77
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
? await import(`${this.projectRoot}/${this.worldConfig.componentDir}/${name}`.replace(/\/+/g, '/') /* @vite-ignore */ )
|
|
56
|
-
: name
|
|
57
|
-
await this.renderFn(component, props, renderOptions)
|
|
58
|
-
};
|
|
59
|
-
|
|
60
|
-
async cleanup() {
|
|
61
|
-
await this.cleanupFn()
|
|
78
|
+
get screenshotDir() {
|
|
79
|
+
return this.sanitizePath(this.worldConfig.screenshotDir)
|
|
62
80
|
}
|
|
63
81
|
|
|
64
|
-
|
|
65
|
-
return
|
|
82
|
+
get screenshotFilename() {
|
|
83
|
+
return `${this.toString().replace(/^.+?Feature: /, '').replace(' ' + this.info.step, '')}.png`
|
|
66
84
|
}
|
|
67
85
|
|
|
68
|
-
|
|
69
|
-
|
|
86
|
+
async screenshot(options:{name?:string, locator?:Locator} = {}) {
|
|
87
|
+
let locator = options.locator || this.page
|
|
88
|
+
let path = options.name
|
|
89
|
+
? this.fullPath(`${this.screenshotDir}/${options.name}${(this.info.explodedIdx ? ` (${this.info.tags.join(',')})` : '')}.png`.replace(/\.png\.png$/i, '.png'))
|
|
90
|
+
: this.fullPath(`${this.screenshotDir}/${this.screenshotFilename}`)
|
|
91
|
+
await locator.screenshot({ path })
|
|
70
92
|
}
|
|
71
93
|
|
|
72
|
-
get
|
|
73
|
-
|
|
94
|
+
get page():Locator {
|
|
95
|
+
if (!this._page) throw new Error('You must render a component before running tests.')
|
|
96
|
+
return this._page
|
|
74
97
|
}
|
|
75
98
|
|
|
99
|
+
set page(value:HTMLElement) {
|
|
100
|
+
while (value.parentNode !== null && value.nodeName !== 'BODY') value = value.parentNode as HTMLBodyElement
|
|
101
|
+
this._page = this.browserPage.elementLocator(value)
|
|
102
|
+
value.addEventListener('click', (e)=>{
|
|
103
|
+
this.actions.clicks.push(e.target)
|
|
104
|
+
})
|
|
105
|
+
value.addEventListener('dblclick', (e)=>{
|
|
106
|
+
this.actions.doubleClicks.push(e.target)
|
|
107
|
+
})
|
|
108
|
+
}
|
|
76
109
|
|
|
77
110
|
/**
|
|
78
111
|
* Gets a locator based on a certain logic
|
|
@@ -138,6 +171,7 @@ export class VitestBrowserWorld extends QuickPickleWorld implements VitestBrowse
|
|
|
138
171
|
async scroll(locator:Locator, direction:"up"|"down"|"left"|"right", px = 100) {
|
|
139
172
|
let horiz = direction.includes('t')
|
|
140
173
|
let el = await locator.element()
|
|
174
|
+
if (el.nodeName === 'BODY' && el.parentElement) el = el.parentElement
|
|
141
175
|
if (horiz) await el.scrollBy(direction === 'right' ? px : -px, 0)
|
|
142
176
|
else await el.scrollBy(0, direction === 'down' ? px : -px)
|
|
143
177
|
}
|
package/src/actions.steps.ts
CHANGED
|
@@ -157,6 +157,14 @@ When('I wait (for ){float} second(s)', async function (world:VitestBrowserWorld,
|
|
|
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,6 +213,23 @@ 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
235
|
await expect(world.page).toMatchImageSnapshot({
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
Feature: Actions step definitions in Vitest Browser
|
|
2
|
+
|
|
3
|
+
As a developer or tester
|
|
4
|
+
I need to be sure that the step definitions work as promised
|
|
5
|
+
|
|
6
|
+
Background: Load the example HTML page
|
|
7
|
+
Given I render the "Example" component
|
|
8
|
+
|
|
9
|
+
Rule: Interaction: Clicking must be supported
|
|
10
|
+
|
|
11
|
+
Scenario: clicking on exact text
|
|
12
|
+
When I click on "Text"
|
|
13
|
+
Then "Text" should have been clicked
|
|
14
|
+
When I click on "Do CSS transitions affect Playwright awaits?"
|
|
15
|
+
And I wait for 1000ms
|
|
16
|
+
Then I should see "AI says:"
|
|
17
|
+
|
|
18
|
+
@should-fail
|
|
19
|
+
Scenario: clicking on inexact text FAILS
|
|
20
|
+
When I click on "Do CSS transitions"
|
|
21
|
+
|
|
22
|
+
@should-fail
|
|
23
|
+
Scenario: clicking on hidden elements FAILS
|
|
24
|
+
When I click on "Hidden item"
|
|
25
|
+
|
|
26
|
+
@should-fail
|
|
27
|
+
Scenario: clicking on invisible elements FAILS
|
|
28
|
+
When I click on "Invisible item"
|
|
29
|
+
|
|
30
|
+
@todo
|
|
31
|
+
Scenario: clicking on elements by css selector
|
|
32
|
+
When I click the 'a[href="#faq"]' element
|
|
33
|
+
Then the url should contain "faq"
|
|
34
|
+
|
|
35
|
+
Scenario: clicking on elements by role
|
|
36
|
+
When I click the "Image" link
|
|
37
|
+
Then the "Image" link should have been clicked
|
|
38
|
+
|
|
39
|
+
Rule: Interaction: Focusing must be supported
|
|
40
|
+
|
|
41
|
+
Scenario: focusing on exact text
|
|
42
|
+
When I focus on "Text"
|
|
43
|
+
Then the "Text" link should be focused
|
|
44
|
+
|
|
45
|
+
@should-fail
|
|
46
|
+
Scenario: focusing on hidden elements fails
|
|
47
|
+
When I focus on "Do CSS transitions affect Playwright awaits?"
|
|
48
|
+
|
|
49
|
+
@should-fail
|
|
50
|
+
Scenario: focusing on non-selectable elements FAILS
|
|
51
|
+
When I focus on "Item 1"
|
|
52
|
+
|
|
53
|
+
@should-fail
|
|
54
|
+
Scenario: focusing on inexact text FAILS
|
|
55
|
+
When I focus on "List"
|
|
56
|
+
|
|
57
|
+
@todo
|
|
58
|
+
Scenario: Focusing on elements by css selector should work
|
|
59
|
+
When I focus the 'a[href="#faq"]' element
|
|
60
|
+
Then the "FAQ" link should be focused
|
|
61
|
+
|
|
62
|
+
Scenario: Focusing on elements by role should work
|
|
63
|
+
When I focus the "Image" link
|
|
64
|
+
Then the "Image" link should be focused
|
|
65
|
+
|
|
66
|
+
Rule: Typing must be supported
|
|
67
|
+
|
|
68
|
+
Scenario: Typing text into a textbox
|
|
69
|
+
When for "name" I type "David Hunt"
|
|
70
|
+
Then the value of "name" should be "David Hunt"
|
|
71
|
+
When for the "name" textbox I type " Sam Ziegler"
|
|
72
|
+
Then the value of the "name" textbox should be "David Hunt Sam Ziegler"
|
|
73
|
+
|
|
74
|
+
Scenario: Navigating through links
|
|
75
|
+
When I focus the "Lists" link
|
|
76
|
+
Then the "Lists" link should be focused
|
|
77
|
+
When I type the following keys: Tab Tab
|
|
78
|
+
Then the "Form" link should be focused
|
|
79
|
+
When I type the following keys: {Shift>} Tab {/Shift}
|
|
80
|
+
Then the "Table" link should be focused
|
|
81
|
+
|
|
82
|
+
Scenario: Navigating through form elements
|
|
83
|
+
When I activate the "Name" textbox
|
|
84
|
+
And I type the following keys: Tab Tab
|
|
85
|
+
Then the "Message" textbox should be focused
|
|
86
|
+
When I type the following keys: {Shift>} Tab {Shift}
|
|
87
|
+
Then the "Email" textbox should be focused
|
|
88
|
+
|
|
89
|
+
Rule: Form entry must be supported
|
|
90
|
+
|
|
91
|
+
@should-fail
|
|
92
|
+
Scenario: filling a disabled field FAILS
|
|
93
|
+
When for "From" I enter "anything"
|
|
94
|
+
|
|
95
|
+
Scenario: Filling one textbox
|
|
96
|
+
When for "Name" I fill in "David Hunt"
|
|
97
|
+
Then the value of "Name" should be "David Hunt"
|
|
98
|
+
When for "Name" I enter "Sam Ziegler"
|
|
99
|
+
Then the value of "Name" should be "Sam Ziegler"
|
|
100
|
+
When for "Name" I enter ""
|
|
101
|
+
Then the value of "Name" should be ""
|
|
102
|
+
|
|
103
|
+
Scenario: Filling a date field
|
|
104
|
+
When for "Date" I fill in "2024-10-21"
|
|
105
|
+
Then the value of "Date" should be "2024-10-21"
|
|
106
|
+
|
|
107
|
+
Scenario: Filling a number field
|
|
108
|
+
When for "Number" I enter "9"
|
|
109
|
+
Then the value of "Number" should be 9
|
|
110
|
+
|
|
111
|
+
Scenario: Selecting an option
|
|
112
|
+
When for "Color" I select "Red"
|
|
113
|
+
Then the value of "Color" should be "red"
|
|
114
|
+
When for "Color" I enter "- none -"
|
|
115
|
+
Then the value of "Color" should be ""
|
|
116
|
+
|
|
117
|
+
Scenario: Checking and unchecking a checkbox
|
|
118
|
+
When I check "I agree"
|
|
119
|
+
Then the "I agree" checkbox should be checked
|
|
120
|
+
|
|
121
|
+
Scenario: Checking a radio button
|
|
122
|
+
When I check "later"
|
|
123
|
+
Then the "later" radio should be checked
|
|
124
|
+
And the "now" radio should be unchecked
|
|
125
|
+
|
|
126
|
+
Scenario: Filling a whole form
|
|
127
|
+
When I fill in the following fields:
|
|
128
|
+
| Name | David Hunt |
|
|
129
|
+
| Email | git@github.com |
|
|
130
|
+
| Message | Hope this works! |
|
|
131
|
+
| Date | 2024-11-01 |
|
|
132
|
+
| Number | 493 |
|
|
133
|
+
| Color: | blue |
|
|
134
|
+
| later | true |
|
|
135
|
+
| I agree | no |
|
|
136
|
+
Then the value of "Name" should be "David Hunt"
|
|
137
|
+
And the value of "Email" should contain "git"
|
|
138
|
+
And the value of "Message" should be "Hope this works!"
|
|
139
|
+
And the value of "Date" should be "2024-11-01"
|
|
140
|
+
And the value of "Number" should be 493
|
|
141
|
+
And the value of "Color" should be "blue"
|
|
142
|
+
And the "later" radio should be checked
|
|
143
|
+
And the "I agree" checkbox should be unchecked
|
|
144
|
+
|
|
145
|
+
Rule: Waiting must be supported
|
|
146
|
+
|
|
147
|
+
Scenario: waiting for visible text works ONLY if the animation starts immediately
|
|
148
|
+
When I click on "Do CSS transitions affect Playwright awaits?"
|
|
149
|
+
Then I should see "AI says:"
|
|
150
|
+
|
|
151
|
+
@fails
|
|
152
|
+
Scenario: waiting for invisible text DOES NOT WORK
|
|
153
|
+
When I click on "Do CSS transitions affect Playwright awaits?"
|
|
154
|
+
Then I should NOT see "AI says:"
|
|
155
|
+
|
|
156
|
+
Rule: Scrolling must be supported
|
|
157
|
+
|
|
158
|
+
Scenario: Scrolling the full page
|
|
159
|
+
When I scroll down
|
|
160
|
+
Then the "HTML Test Page" heading should be outside the viewport
|
|
161
|
+
When I scroll up
|
|
162
|
+
Then the "HTML Test Page" heading should be inside the viewport
|
|
163
|
+
|
|
164
|
+
@todo
|
|
165
|
+
Scenario: Scrolling a particular div
|
|
166
|
+
|
|
167
|
+
Rule: Screenshots must be supported
|
|
168
|
+
|
|
169
|
+
Scenario: Taking a default screenshot
|
|
170
|
+
When I take a screenshot
|
|
171
|
+
Then the screenshot "Actions step definitions in Vitest Browser_Taking a default screenshot_01.png" should exist--delete it
|
|
172
|
+
|
|
173
|
+
Scenario: Taking a named screenshot
|
|
174
|
+
When I take a screenshot named "pickles"
|
|
175
|
+
Then the screenshot "pickles.png" should exist--delete it
|
|
176
|
+
|
|
177
|
+
@tag1 @tag2 @sequential
|
|
178
|
+
Scenario: Taking a default screenshot with exploded tags
|
|
179
|
+
When I take a screenshot
|
|
180
|
+
|
|
181
|
+
@tag1 @tag2 @sequential
|
|
182
|
+
Scenario: Taking a named screenshot with exploded tags
|
|
183
|
+
When I take a screenshot named "temp"
|
|
184
|
+
|
|
185
|
+
@sequential
|
|
186
|
+
Scenario: Cleaning up the screenshots with exploded tags
|
|
187
|
+
Then the screenshot "Actions step definitions in Vitest Browser_Taking a default screenshot with exploded tags (sequential,tag1)_01.png" should exist--delete it
|
|
188
|
+
And the screenshot "Actions step definitions in Vitest Browser_Taking a default screenshot with exploded tags (sequential,tag2)_01.png" should exist--delete it
|
|
189
|
+
|
|
190
|
+
@sequential @skip-ci
|
|
191
|
+
Scenario: Cleaning up the screenshots with exploded tags
|
|
192
|
+
And the screenshot "temp (sequential,tag1).png" should exist--delete it
|
|
193
|
+
And the screenshot "temp (sequential,tag2).png" should exist--delete it
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
@todo
|
|
2
|
+
Feature: Basic tests of Vitest Browser Mode and steps
|
|
3
|
+
|
|
4
|
+
@skip-ci
|
|
5
|
+
Rule: Visual regression testing must be supported
|
|
6
|
+
|
|
7
|
+
Example: Passing visual regression test
|
|
8
|
+
When I render the "Example" component
|
|
9
|
+
Then the screenshot should match
|
|
10
|
+
|
|
11
|
+
Example: Passing named visual regression test
|
|
12
|
+
When I render the "Example" component
|
|
13
|
+
Then the screenshot "visual-regression-example-page" should match
|
|
14
|
+
|
|
15
|
+
Example: Passing visual regression test of an element
|
|
16
|
+
When I render the "Example" component
|
|
17
|
+
Then the screenshot of the "XKCD Comic" img should match
|
|
18
|
+
|
|
19
|
+
Example: Passing named visual regression test of an element
|
|
20
|
+
When I render the "Example" component
|
|
21
|
+
Then the screenshot "visual-regression-faq-section" of the "#faq" element should match
|
|
22
|
+
|
|
23
|
+
@fails
|
|
24
|
+
Example: Failing visual regression test
|
|
25
|
+
When I render the "Example" component
|
|
26
|
+
Then the screenshot "visual-regression-simple-page.png.diff.png" should not exist
|
|
27
|
+
And the screenshot "visual-regression-simple-page.png.actual.png" should not exist
|
|
28
|
+
And the screenshot "visual-regression-simple-page" should match
|
|
29
|
+
|
|
30
|
+
Scenario: Delete the visual regression failure file
|
|
31
|
+
Then the screenshot "visual-regression-simple-page.png.diff.png" should not exist
|
|
32
|
+
And the screenshot "visual-regression-simple-page.png.actual.png" should exist--delete it
|
|
33
|
+
|
|
34
|
+
@todo
|
|
35
|
+
Rule: Setting screenshot options must be supported
|
|
36
|
+
|
|
37
|
+
Scenario: Setting a screenshot mask
|
|
38
|
+
Given I render the "Example" component
|
|
39
|
+
And the following world config:
|
|
40
|
+
```yaml
|
|
41
|
+
screenshotOptions:
|
|
42
|
+
mask:
|
|
43
|
+
- form
|
|
44
|
+
```
|
|
45
|
+
Then the screenshot should match
|
|
46
|
+
|
|
47
|
+
Scenario: Setting a clip area
|
|
48
|
+
Given I render the "Example" component
|
|
49
|
+
And the following world config:
|
|
50
|
+
```yaml
|
|
51
|
+
screenshotOptions:
|
|
52
|
+
clip:
|
|
53
|
+
x: 0
|
|
54
|
+
y: 60
|
|
55
|
+
width: 300
|
|
56
|
+
height: 180
|
|
57
|
+
```
|
|
58
|
+
Then the screenshot should match
|
|
59
|
+
|
|
60
|
+
Rule: Screenshots should be placed in the proper directory
|
|
61
|
+
|
|
62
|
+
Scenario: Taking a screenshot
|
|
63
|
+
When I take a screenshot
|
|
64
|
+
Then the screenshot "Feature Basic tests of Vitest Browser Mode and steps_Taking a screenshot_01.png" should exist--delete it
|
|
65
|
+
|
|
66
|
+
Scenario: Taking a named screenshot
|
|
67
|
+
When I take a screenshot named "test"
|
|
68
|
+
Then the screenshot "test.png" should exist--delete it
|
|
69
|
+
|
|
70
|
+
Scenario: Taking a screenshot of an element
|
|
71
|
+
Given I load the file "tests/examples/example.html"
|
|
72
|
+
When I take a screenshot of the "Image" link
|
|
73
|
+
Then the screenshot "Feature Basic tests of Vitest Browser Mode and steps_Taking a screenshot of an element_02.png" should exist--delete it
|
|
74
|
+
|
|
75
|
+
Scenario: Taking a named screenshot of an element
|
|
76
|
+
Given I load the file "tests/examples/example.html"
|
|
77
|
+
When I take a screenshot of the "XKCD Comic" img named "test2"
|
|
78
|
+
Then the screenshot "test2.png" should exist--delete it
|
|
79
|
+
|