@quickpickle/vitest-browser 0.0.2
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 +15 -0
- package/LICENSE +21 -0
- package/README.md +4 -0
- package/dist/VitestBrowserWorld.cjs +179 -0
- package/dist/VitestBrowserWorld.cjs.map +1 -0
- package/dist/VitestBrowserWorld.d.ts +103 -0
- package/dist/VitestBrowserWorld.mjs +176 -0
- package/dist/VitestBrowserWorld.mjs.map +1 -0
- package/dist/actions.steps.cjs +157 -0
- package/dist/actions.steps.cjs.map +1 -0
- package/dist/actions.steps.d.ts +1 -0
- package/dist/actions.steps.mjs +155 -0
- package/dist/actions.steps.mjs.map +1 -0
- package/dist/frameworks/ReactBrowserWorld.cjs +38 -0
- package/dist/frameworks/ReactBrowserWorld.cjs.map +1 -0
- package/dist/frameworks/ReactBrowserWorld.d.ts +10 -0
- package/dist/frameworks/ReactBrowserWorld.mjs +36 -0
- package/dist/frameworks/ReactBrowserWorld.mjs.map +1 -0
- package/dist/frameworks/SvelteBrowserWorld.cjs +17 -0
- package/dist/frameworks/SvelteBrowserWorld.cjs.map +1 -0
- package/dist/frameworks/SvelteBrowserWorld.d.ts +9 -0
- package/dist/frameworks/SvelteBrowserWorld.mjs +15 -0
- package/dist/frameworks/SvelteBrowserWorld.mjs.map +1 -0
- package/dist/frameworks/VueBrowserWorld.cjs +25 -0
- package/dist/frameworks/VueBrowserWorld.cjs.map +1 -0
- package/dist/frameworks/VueBrowserWorld.d.ts +10 -0
- package/dist/frameworks/VueBrowserWorld.mjs +23 -0
- package/dist/frameworks/VueBrowserWorld.mjs.map +1 -0
- package/dist/frameworks/react.cjs +11 -0
- package/dist/frameworks/react.cjs.map +1 -0
- package/dist/frameworks/react.d.ts +1 -0
- package/dist/frameworks/react.mjs +9 -0
- package/dist/frameworks/react.mjs.map +1 -0
- package/dist/frameworks/svelte.cjs +10 -0
- package/dist/frameworks/svelte.cjs.map +1 -0
- package/dist/frameworks/svelte.d.ts +1 -0
- package/dist/frameworks/svelte.mjs +8 -0
- package/dist/frameworks/svelte.mjs.map +1 -0
- package/dist/frameworks/vue.cjs +10 -0
- package/dist/frameworks/vue.cjs.map +1 -0
- package/dist/frameworks/vue.d.ts +1 -0
- package/dist/frameworks/vue.mjs +8 -0
- package/dist/frameworks/vue.mjs.map +1 -0
- package/dist/outcomes.steps.cjs +186 -0
- package/dist/outcomes.steps.cjs.map +1 -0
- package/dist/outcomes.steps.d.ts +1 -0
- package/dist/outcomes.steps.mjs +184 -0
- package/dist/outcomes.steps.mjs.map +1 -0
- package/package.json +103 -0
- package/rollup.config.js +58 -0
- package/src/VitestBrowserWorld.ts +205 -0
- package/src/actions.steps.ts +169 -0
- package/src/frameworks/ReactBrowserWorld.ts +41 -0
- package/src/frameworks/SvelteBrowserWorld.ts +15 -0
- package/src/frameworks/VueBrowserWorld.ts +23 -0
- package/src/frameworks/react.ts +4 -0
- package/src/frameworks/svelte.ts +4 -0
- package/src/frameworks/vue.ts +4 -0
- package/src/outcomes.steps.ts +190 -0
- package/tests/react/Hello.tsx +23 -0
- package/tests/react/react.feature +15 -0
- package/tests/svelte/Hello.svelte +31 -0
- package/tests/svelte/svelte.feature +16 -0
- package/tests/vue/Hello.vue +31 -0
- package/tests/vue/vue.feature +16 -0
- package/tsconfig.json +17 -0
- package/vite.config.ts +8 -0
- package/vitest.workspace.ts +95 -0
- package/world.ts +7 -0
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { Before, QuickPickleWorld, QuickPickleWorldInterface } from 'quickpickle';
|
|
2
|
+
import type { BrowserPage, Locator, UserEvent, ScreenshotOptions } from '@vitest/browser/context'
|
|
3
|
+
import { defaultsDeep } from 'lodash-es'
|
|
4
|
+
import type { TestContext } from 'vitest';
|
|
5
|
+
import { InfoConstructor } from 'quickpickle/dist/world';
|
|
6
|
+
|
|
7
|
+
/// <reference types="@vitest/browser/providers/playwright" />
|
|
8
|
+
|
|
9
|
+
export type VitestWorldConfig = {
|
|
10
|
+
componentDir?: string;
|
|
11
|
+
screenshotDir?: string;
|
|
12
|
+
screenshotOptions?: Partial<ScreenshotOptions>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const defaultVitestWorldConfig:VitestWorldConfig = {
|
|
16
|
+
componentDir: '', // directory in which components are kept, relative to project root
|
|
17
|
+
screenshotDir: 'screenshots', // directory in which to save screenshots, relative to project root (default: "screenshots")
|
|
18
|
+
screenshotOptions: {}, // options for the default screenshot comparisons
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type VitestBrowserWorldInterface = QuickPickleWorldInterface & {
|
|
22
|
+
render: (name:string|any, props?:any, renderOptions?:any)=>Promise<void>;
|
|
23
|
+
renderFn: (component:any, props?:any, renderOptions?:any)=>void|Promise<void>;
|
|
24
|
+
cleanup: ()=>Promise<void>;
|
|
25
|
+
cleanupFn: ()=>void|Promise<void>;
|
|
26
|
+
page: BrowserPage;
|
|
27
|
+
userEvent: UserEvent
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export class VitestBrowserWorld extends QuickPickleWorld implements VitestBrowserWorldInterface {
|
|
31
|
+
|
|
32
|
+
renderFn: (component:any, props:any, renderOptions:any)=>void;
|
|
33
|
+
cleanupFn: ()=>void;
|
|
34
|
+
page!: BrowserPage;
|
|
35
|
+
userEvent!: UserEvent;
|
|
36
|
+
|
|
37
|
+
constructor(context:TestContext, info:InfoConstructor) {
|
|
38
|
+
info = defaultsDeep(info || {}, { config: { worldConfig: defaultVitestWorldConfig } } )
|
|
39
|
+
super(context, info);
|
|
40
|
+
if (!info.config.worldConfig.screenshotDir && info.config.worldConfig?.screenshotOptions?.customSnapshotsDir) {
|
|
41
|
+
this.info.config.worldConfig.screenshotDir = info.config.worldConfig.screenshotOptions.customSnapshotsDir
|
|
42
|
+
}
|
|
43
|
+
this.renderFn = ()=>{};
|
|
44
|
+
this.cleanupFn = ()=>{};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async init() {
|
|
48
|
+
let browserContext = await import('@vitest/browser/context')
|
|
49
|
+
this.page = browserContext.page;
|
|
50
|
+
this.userEvent = browserContext.userEvent;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async render(name:string|any, props?:any, renderOptions?:any) {
|
|
54
|
+
let component = typeof name === 'string'
|
|
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()
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
sanitizeFilepath(filepath:string) {
|
|
65
|
+
return filepath.replace(/\/\/+/g, '/').replace(/\/[\.~]+\//g, '/')
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
get screenshotDir() {
|
|
69
|
+
return this.sanitizeFilepath(`${this.projectRoot}/${this.worldConfig.screenshotDir}`)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
get screenshotFilename() {
|
|
73
|
+
return `${this.toString().replace(/^.+?Feature: /, '').replace(' ' + this.info.step, '')}.png`
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Gets a locator based on a certain logic
|
|
79
|
+
* @example getLocator(page, 'Cancel', 'button') => page.getByRole('button', { name: 'Cancel' })
|
|
80
|
+
* @example getLocator(page, 'Search', 'input') => page.getByLabel('Search').or(page.getByPlaceholder('Search'))
|
|
81
|
+
* @example getLocator(page, 'ul.fourteen-points li', 'element', 'Open covenants of peace') => page.locator('ul.fourteen-points li').filter({ hasText: 'Open covenants of peace' })
|
|
82
|
+
*
|
|
83
|
+
* @param el The locator or page inside which to get a new locator
|
|
84
|
+
* @param identifier The value, label, placeholder, or css selector, depending on role
|
|
85
|
+
* @param role An ARIA role, "input", or "element"
|
|
86
|
+
* @param text Optional text to match inside the locator
|
|
87
|
+
* @returns Promise<void>
|
|
88
|
+
*/
|
|
89
|
+
getLocator(el:Locator|BrowserPage, identifier:string, role:string|'element'|'input', text:string|null=null) {
|
|
90
|
+
let locator:Locator
|
|
91
|
+
if (role === 'element') throw new Error('Using "element" for CSS selectors is not yet supported; use an aria role or "input" instead.')
|
|
92
|
+
else if (role === 'input') locator = el.getByLabelText(identifier).or(el.getByPlaceholder(identifier))
|
|
93
|
+
else locator = el.getByRole(role as any, { name: identifier })
|
|
94
|
+
if (text && role !== 'input') return locator.filter({ hasText: text })
|
|
95
|
+
return locator
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Sets a value on a form element based on its type (select, checkbox/radio, or other input)
|
|
100
|
+
* @example setValue(locator, "Option 1, Option 2") => Selects multiple options in a select element
|
|
101
|
+
* @example setValue(locator, "true") => Checks a checkbox/radio button
|
|
102
|
+
* @example setValue(locator, "false") => Unchecks a checkbox/radio button
|
|
103
|
+
* @example setValue(locator, "Some text") => Fills a text input with "Some text"
|
|
104
|
+
*
|
|
105
|
+
* @param locator The Playwright locator for the form element
|
|
106
|
+
* @param value The value to set - can be string or other value type
|
|
107
|
+
* @returns Promise<void>
|
|
108
|
+
*/
|
|
109
|
+
async setValue(locator:Locator, value:string|any) {
|
|
110
|
+
let el = await locator.element()
|
|
111
|
+
let tag = el.tagName.toLowerCase()
|
|
112
|
+
if (!tag) throw new Error(`Could not find element with locator: ${locator.toString()}`)
|
|
113
|
+
if (tag === 'select') {
|
|
114
|
+
let values = value.split(/\s*(?<!\\),\s*/).map((v:string) => v.replace(/\\,/g, ','))
|
|
115
|
+
await locator.selectOptions(values)
|
|
116
|
+
}
|
|
117
|
+
else if (isCheckboxOrRadio(el)) {
|
|
118
|
+
let check = !( ['false','no','unchecked','','null','undefined','0'].includes(value.toString().toLowerCase()) )
|
|
119
|
+
if (check) el.checked = true
|
|
120
|
+
else el.checked = false
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
await locator.fill(value)
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Scrolls the mouse wheel in a specified direction by a given number of pixels
|
|
129
|
+
* @example scroll("down", 100) => Scrolls down 100 pixels
|
|
130
|
+
* @example scroll("up", 50) => Scrolls up 50 pixels
|
|
131
|
+
* @example scroll("left", 200) => Scrolls left 200 pixels
|
|
132
|
+
* @example scroll("right") => Scrolls right using default 100 pixels
|
|
133
|
+
*
|
|
134
|
+
* @param direction The direction to scroll: "up", "down", "left", or "right"
|
|
135
|
+
* @param px The number of pixels to scroll (defaults to 100)
|
|
136
|
+
* @returns Promise<void>
|
|
137
|
+
*/
|
|
138
|
+
async scroll(locator:Locator, direction:"up"|"down"|"left"|"right", px = 100) {
|
|
139
|
+
let horiz = direction.includes('t')
|
|
140
|
+
let el = await locator.element()
|
|
141
|
+
if (horiz) await el.scrollBy(direction === 'right' ? px : -px, 0)
|
|
142
|
+
else await el.scrollBy(0, direction === 'down' ? px : -px)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* A helper function for parsing text on a page or in an element.
|
|
147
|
+
* Can be used to check for the presence OR absence of visible OR hidden text.
|
|
148
|
+
* Examples:
|
|
149
|
+
* @example expectText(locator, 'text', true, true) // expect that a locator with the text is visible (and there may be hidden ones)
|
|
150
|
+
* @example expectText(locator, 'text', false, true) // expect that NO locator with the text is visible (but there may be hidden ones)
|
|
151
|
+
* @example expectText(locator, 'text', true, false) // expect that a HIDDEN locator with the text IS FOUND on the page (but there may be visible ones)
|
|
152
|
+
* @example expectText(locator, 'text', false, false) // expect that NO hidden locator with the text is found on the page (but there may be visible ones)
|
|
153
|
+
*
|
|
154
|
+
* @param locator the locator to check
|
|
155
|
+
* @param text the text to be found
|
|
156
|
+
* @param toBePresent whether a locator with the text should be present
|
|
157
|
+
* @param toBeVisible whether the locator with the text should be visible
|
|
158
|
+
* @returns void
|
|
159
|
+
*/
|
|
160
|
+
async expectText(locator:Locator|BrowserPage, text:string, toBePresent:boolean=true, toBeVisible:boolean=true) {
|
|
161
|
+
try {
|
|
162
|
+
await this.expectElement(locator.getByText(text), toBePresent, toBeVisible)
|
|
163
|
+
}
|
|
164
|
+
catch(e) {
|
|
165
|
+
let message = `The${toBeVisible ? '' : ' hidden'} text "${text}" was unexpectedly ${toBePresent ? 'not present' : 'present'}.`
|
|
166
|
+
throw new Error(message)
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* A helper function for parsing elements on a page or in an element.
|
|
172
|
+
* Can be used to check for the presence OR absence of visible OR hidden elements.
|
|
173
|
+
* Examples:
|
|
174
|
+
* @example expectElement(locator, true) // expect that an element is visible (and there may be hidden ones)
|
|
175
|
+
* @example expectElement(locator, false) // expect that NO element is visible (but there may be hidden ones)
|
|
176
|
+
* @example expectElement(locator, true, false) // expect that a HIDDEN element IS FOUND on the page (but there may be visible ones)
|
|
177
|
+
* @example expectElement(locator, false, false) // expect that NO hidden element is found on the page (but there may be visible ones)
|
|
178
|
+
*
|
|
179
|
+
* @param locator the locator to check
|
|
180
|
+
* @param toBePresent whether an element should be present
|
|
181
|
+
* @param toBeVisible whether the element should be visible
|
|
182
|
+
*/
|
|
183
|
+
async expectElement(locator:Locator, toBePresent:boolean=true, toBeVisible:boolean=true) {
|
|
184
|
+
let allElements = await locator.elements()
|
|
185
|
+
let matchingElements = allElements.filter(el => toBeVisible === el.checkVisibility({ opacityProperty:true, visibilityProperty:true }))
|
|
186
|
+
if (toBePresent === (matchingElements.length === 0)) throw new Error(`The${toBeVisible ? '' : ' hidden'} element "${locator}" was unexpectedly ${toBePresent ? 'not present' : 'present'}.`)
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Waits for a certain amount of time
|
|
191
|
+
* @param ms number
|
|
192
|
+
*/
|
|
193
|
+
async waitForTimeout(ms:number) {
|
|
194
|
+
await new Promise(r => setTimeout(r, ms))
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function isCheckboxOrRadio(el:any):el is HTMLInputElement {
|
|
200
|
+
return el.type === 'checkbox' || el.type === 'radio'
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
Before(async (world:VitestBrowserWorld) => {
|
|
204
|
+
await world.cleanup()
|
|
205
|
+
})
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { Given, When, Then, DataTable } from "quickpickle";
|
|
2
|
+
import { type VitestBrowserWorld } from "./VitestBrowserWorld";
|
|
3
|
+
import { expect } from 'vitest';
|
|
4
|
+
|
|
5
|
+
/// <reference types="@vitest/browser/providers/playwright" />
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* RENDERING COMPONENTS
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
Given('I render (the ){string}( component)', async(world:VitestBrowserWorld, name:string) => {
|
|
12
|
+
await world.render(name);
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
Given('I render (the ){string}( component) with the following props/properties:', async(world:VitestBrowserWorld, name:string, props:DataTable) => {
|
|
16
|
+
let propsObj = props.raw().reduce((acc:{[key:string]:any}, row:string[]) => {
|
|
17
|
+
let value
|
|
18
|
+
try {
|
|
19
|
+
value = JSON.parse(row[1])
|
|
20
|
+
} catch (e) {
|
|
21
|
+
value = row[1]
|
|
22
|
+
}
|
|
23
|
+
acc[row[0]] = value
|
|
24
|
+
return acc
|
|
25
|
+
}, {})
|
|
26
|
+
await world.render(name, propsObj);
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
// ================
|
|
31
|
+
// Interaction
|
|
32
|
+
|
|
33
|
+
When('I click/press/tap/touch (on ){string}', async function (world:VitestBrowserWorld, identifier) {
|
|
34
|
+
let locator = world.page.getByText(identifier, { exact:true })
|
|
35
|
+
await locator.click({ timeout:world.worldConfig.stepTimeout })
|
|
36
|
+
})
|
|
37
|
+
When('I click/press/tap/touch (on )the {string} {word}', async function (world:VitestBrowserWorld, identifier, role) {
|
|
38
|
+
let locator = world.getLocator(world.page, identifier, role)
|
|
39
|
+
await locator.click({ timeout:world.worldConfig.stepTimeout })
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
When('I focus/select/activate (on ){string}', async function (world:VitestBrowserWorld, identifier) {
|
|
43
|
+
let locator = world.page.getByText(identifier, { exact:true });
|
|
44
|
+
(locator.element() as HTMLElement)?.focus()
|
|
45
|
+
await expect(locator).toHaveFocus()
|
|
46
|
+
})
|
|
47
|
+
When('I focus/select/activate (on )the {string} {word}', async function (world:VitestBrowserWorld, identifier, role) {
|
|
48
|
+
let locator = world.getLocator(world.page, identifier, role);
|
|
49
|
+
(locator.element() as HTMLElement).focus()
|
|
50
|
+
await expect(locator).toHaveFocus()
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
// ================
|
|
54
|
+
// Typing
|
|
55
|
+
|
|
56
|
+
When("for/in/on (the ){string} I type {string}", async function (world: VitestBrowserWorld, identifier: string, value: string) {
|
|
57
|
+
const locator = world.getLocator(world.page, identifier, 'input');
|
|
58
|
+
const element = locator.element() as HTMLElement;
|
|
59
|
+
element.focus();
|
|
60
|
+
await world.userEvent.keyboard(value);
|
|
61
|
+
})
|
|
62
|
+
When("for/in/on (the ){string} {word} I type {string}", async function (world: VitestBrowserWorld, identifier: string, role: string, value: string) {
|
|
63
|
+
const locator = world.getLocator(world.page, identifier, role);
|
|
64
|
+
const element = locator.element() as HTMLElement;
|
|
65
|
+
element.focus();
|
|
66
|
+
await world.userEvent.keyboard(value);
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
When('I type the following keys: {}', async function (world: VitestBrowserWorld, keys: string) {
|
|
70
|
+
for (let key of keys.split(' ')) {
|
|
71
|
+
if (key !== '{{' && key !== '[[' && !key.match(/^[\{\[].+[\}\]]$/)) key = `{${key}}`
|
|
72
|
+
await world.userEvent.keyboard(key);
|
|
73
|
+
}
|
|
74
|
+
})
|
|
75
|
+
When("for/in/on (the ){string} I type the following keys: {}", async function (world:VitestBrowserWorld, identifier: string, keys: string) {
|
|
76
|
+
const locator = world.getLocator(world.page, identifier, 'input');
|
|
77
|
+
const element = locator.element() as HTMLElement;
|
|
78
|
+
element.focus();
|
|
79
|
+
for (let key of keys.split(' ')) {
|
|
80
|
+
if (key !== '{{' && key !== '[[' && !key.match(/^[\{\[].+[\}\]]$/)) key = `{${key}}`
|
|
81
|
+
await world.userEvent.keyboard(key);
|
|
82
|
+
}
|
|
83
|
+
})
|
|
84
|
+
When("for/in/on (the ){string} {word} I type the following keys: {}", async function (world:VitestBrowserWorld, identifier: string, role: string, keys: string) {
|
|
85
|
+
const locator = world.getLocator(world.page, identifier, role);
|
|
86
|
+
const element = locator.element() as HTMLElement;
|
|
87
|
+
element.focus();
|
|
88
|
+
for (let key of keys.split(' ')) {
|
|
89
|
+
if (key !== '{{' && key !== '[[' && !key.match(/^[\{\[].+[\}\]]$/)) key = `{${key}}`
|
|
90
|
+
await world.userEvent.keyboard(key);
|
|
91
|
+
}
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
// ================
|
|
95
|
+
// Forms
|
|
96
|
+
|
|
97
|
+
When("for/in/on (the ){string} I enter/fill/select (in ){string}", async function (world:VitestBrowserWorld, identifier, value) {
|
|
98
|
+
let locator = world.getLocator(world.page, identifier, 'input')
|
|
99
|
+
await world.setValue(locator, value)
|
|
100
|
+
})
|
|
101
|
+
When("for/in/on (the ){string} {word} I enter/fill/select (in ){string}", async function (world:VitestBrowserWorld, identifier, role, value) {
|
|
102
|
+
let locator = world.getLocator(world.page, identifier, role)
|
|
103
|
+
await world.setValue(locator, value)
|
|
104
|
+
})
|
|
105
|
+
When("for/in/on (the ){string} I enter/fill/select (in )the following( text):", async function (world:VitestBrowserWorld, identifier, value) {
|
|
106
|
+
let locator = world.getLocator(world.page, identifier, 'input')
|
|
107
|
+
await world.setValue(locator, value.toString())
|
|
108
|
+
})
|
|
109
|
+
When("for/in/on (the ){string} {word} I enter/fill/select (in )the following( text):", async function (world:VitestBrowserWorld, identifier, role, value) {
|
|
110
|
+
let locator = world.getLocator(world.page, identifier, role)
|
|
111
|
+
await world.setValue(locator, value.toString())
|
|
112
|
+
})
|
|
113
|
+
When('I enter/fill (in )the following( fields):', async function (world:VitestBrowserWorld, table:DataTable) {
|
|
114
|
+
let rows = table.raw()
|
|
115
|
+
let hasRole = rows[0].length === 3
|
|
116
|
+
for (let row of table.raw()) {
|
|
117
|
+
let [identifier, role, value] = row
|
|
118
|
+
if (!hasRole) {
|
|
119
|
+
value = role
|
|
120
|
+
role = 'input'
|
|
121
|
+
}
|
|
122
|
+
let locator = world.getLocator(world.page, identifier, role)
|
|
123
|
+
await world.setValue(locator, value)
|
|
124
|
+
}
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
When('I check (the ){string}( radio)( checkbox)( box)', async function (world:VitestBrowserWorld, indentifier) {
|
|
128
|
+
let locator = world.getLocator(world.page, indentifier, 'input')
|
|
129
|
+
await world.setValue(locator, 'on')
|
|
130
|
+
})
|
|
131
|
+
When('I uncheck (the ){string}( checkbox)( box)', async function (world:VitestBrowserWorld, indentifier) {
|
|
132
|
+
let locator = world.getLocator(world.page, indentifier, 'input')
|
|
133
|
+
await world.setValue(locator, 'off')
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
// ================
|
|
137
|
+
// Waiting
|
|
138
|
+
|
|
139
|
+
// When('I wait for {string} to be attached/detatched/visible/hidden', async function (world:VitestBrowserWorld, text) {
|
|
140
|
+
// let state = world.info.step?.match(/(attached|detatched|visible|hidden)$/)![0] as 'attached'|'detached'|'visible'|'hidden'
|
|
141
|
+
// let locator = world.page.getByText(text)
|
|
142
|
+
// await locator.waitFor({ state, timeout:world.worldConfig.stepTimeout })
|
|
143
|
+
// })
|
|
144
|
+
// When('I wait for a/an/the {string} {word} to be attached/detatched/visible/hidden', async function (world:VitestBrowserWorld, identifier, role) {
|
|
145
|
+
// let state = world.info.step?.match(/(attached|detatched|visible|hidden)$/)![0] as 'attached'|'detached'|'visible'|'hidden'
|
|
146
|
+
// let locator = world.getLocator(world.page, identifier, role)
|
|
147
|
+
// await locator.waitFor({ state, timeout:world.worldConfig.stepTimeout })
|
|
148
|
+
// })
|
|
149
|
+
|
|
150
|
+
When('I wait (for ){int}ms', async function (world:VitestBrowserWorld, num) {
|
|
151
|
+
await world.waitForTimeout(num)
|
|
152
|
+
})
|
|
153
|
+
When('I wait (for ){float} second(s)', async function (world:VitestBrowserWorld, num) {
|
|
154
|
+
await world.waitForTimeout(num * 1000)
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
// ================
|
|
158
|
+
// Scrolling
|
|
159
|
+
|
|
160
|
+
When('I scroll (the ){string} {word} down/up/left/right', async function (world:VitestBrowserWorld, identifier:string, role:string) {
|
|
161
|
+
let locator = world.getLocator(world.page, identifier, role)
|
|
162
|
+
let direction = world.info.step?.match(/(down|up|left|right)$/)![0] as 'down'|'up'|'left'|'right'
|
|
163
|
+
await world.scroll(locator, direction)
|
|
164
|
+
})
|
|
165
|
+
When('I scroll (the ){string} {word} down/up/left/right {int}(px)( pixels)', async function (world:VitestBrowserWorld, identifier, role, num) {
|
|
166
|
+
let locator = world.getLocator(world.page, identifier, role)
|
|
167
|
+
let direction = world.info.step?.match(/(down|up|left|right)(?= \d)/)![0] as 'down'|'up'|'left'|'right'
|
|
168
|
+
await world.scroll(locator, direction, num)
|
|
169
|
+
})
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { VitestBrowserWorld } from "../VitestBrowserWorld";
|
|
2
|
+
import { render, cleanup } from 'vitest-browser-react'
|
|
3
|
+
import type { TestContext } from "vitest";
|
|
4
|
+
import type { InfoConstructor } from "quickpickle/dist/world";
|
|
5
|
+
import React from 'react';
|
|
6
|
+
|
|
7
|
+
export class ReactBrowserWorld extends VitestBrowserWorld {
|
|
8
|
+
|
|
9
|
+
renderFn = render
|
|
10
|
+
cleanupFn = cleanup
|
|
11
|
+
|
|
12
|
+
constructor(context:TestContext, info:InfoConstructor) {
|
|
13
|
+
super(context, info)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// override VitestBrowserWorld.render
|
|
17
|
+
async render(name: string | any, props?: any, renderOptions?: any) {
|
|
18
|
+
let Component: any;
|
|
19
|
+
|
|
20
|
+
if (typeof name === 'string') {
|
|
21
|
+
// dynamic import returns the module object
|
|
22
|
+
const mod = await import(
|
|
23
|
+
`${this.projectRoot}/${this.worldConfig.componentDir}/${name}`.replace(/\/+/g,'/')
|
|
24
|
+
/* @vite-ignore */
|
|
25
|
+
);
|
|
26
|
+
// try .default first, then fall back to any other export
|
|
27
|
+
Component = mod.default ?? Object.values(mod)[0];
|
|
28
|
+
if (!Component) {
|
|
29
|
+
throw new Error(
|
|
30
|
+
`Could not find a React component export in module "${name}".`
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
} else {
|
|
34
|
+
Component = name;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// now call reactRender with the actual component
|
|
38
|
+
await this.renderFn(React.createElement(Component, props), renderOptions);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { VitestBrowserWorld } from "../VitestBrowserWorld";
|
|
2
|
+
import { render, cleanup } from 'vitest-browser-svelte';
|
|
3
|
+
import type { TestContext } from "vitest";
|
|
4
|
+
import { InfoConstructor } from "quickpickle/dist/world";
|
|
5
|
+
|
|
6
|
+
export class SvelteBrowserWorld extends VitestBrowserWorld {
|
|
7
|
+
|
|
8
|
+
renderFn = render
|
|
9
|
+
cleanupFn = cleanup
|
|
10
|
+
|
|
11
|
+
constructor(context:TestContext, info:InfoConstructor) {
|
|
12
|
+
super(context, info)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { VitestBrowserWorld } from "../VitestBrowserWorld";
|
|
2
|
+
import { render, cleanup } from 'vitest-browser-vue'
|
|
3
|
+
import type { TestContext } from "vitest";
|
|
4
|
+
import type { InfoConstructor } from "quickpickle/dist/world";
|
|
5
|
+
|
|
6
|
+
export class VueBrowserWorld extends VitestBrowserWorld {
|
|
7
|
+
|
|
8
|
+
renderFn = render
|
|
9
|
+
cleanupFn = cleanup
|
|
10
|
+
|
|
11
|
+
constructor(context:TestContext, info:InfoConstructor) {
|
|
12
|
+
super(context, info)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async render(name:string|any, props?:any, renderOptions?:any) {
|
|
16
|
+
let mod = typeof name === 'string'
|
|
17
|
+
? await import(`${this.projectRoot}/${this.worldConfig.componentDir}/${name}`.replace(/\/+/g, '/') /* @vite-ignore */ )
|
|
18
|
+
: name;
|
|
19
|
+
let component = mod.default ?? mod;
|
|
20
|
+
await this.renderFn(component, { props, ...renderOptions })
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { Then } from "quickpickle";
|
|
2
|
+
import type { VitestBrowserWorld } from "./VitestBrowserWorld";
|
|
3
|
+
import { expect } from 'vitest'
|
|
4
|
+
|
|
5
|
+
/// <reference types="@vitest/browser/providers/playwright" />
|
|
6
|
+
|
|
7
|
+
// ================
|
|
8
|
+
// Text on page
|
|
9
|
+
|
|
10
|
+
Then('I should see {string}( on the page)', async function (world:VitestBrowserWorld, text) {
|
|
11
|
+
await world.expectText(world.page, text)
|
|
12
|
+
})
|
|
13
|
+
Then('I should not/NOT see {string}( on the page)', async function (world:VitestBrowserWorld, text) {
|
|
14
|
+
await world.expectText(world.page, text, false)
|
|
15
|
+
})
|
|
16
|
+
Then('the text {string} should be visible( on the page)', async function (world:VitestBrowserWorld, text) {
|
|
17
|
+
await world.expectText(world.page, text)
|
|
18
|
+
})
|
|
19
|
+
Then('the text {string} should not/NOT be visible( on the page)', async function (world:VitestBrowserWorld, text) {
|
|
20
|
+
await world.expectText(world.page, text, false)
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
// ================
|
|
24
|
+
// Elements on page
|
|
25
|
+
Then('I should see a/an/the {string} {word}', async function (world:VitestBrowserWorld, identifier, role) {
|
|
26
|
+
let locator = await world.getLocator(world.page, identifier, role)
|
|
27
|
+
await world.expectElement(locator)
|
|
28
|
+
})
|
|
29
|
+
Then('I should not/NOT see a/an/the {string} {word}', async function (world:VitestBrowserWorld, identifier, role) {
|
|
30
|
+
let locator = await world.getLocator(world.page, identifier, role)
|
|
31
|
+
await world.expectElement(locator, false)
|
|
32
|
+
})
|
|
33
|
+
Then('I should see a/an/the {string} (element )with (the )(text ){string}', async function (world:VitestBrowserWorld, identifier, text) {
|
|
34
|
+
let locator = await world.getLocator(world.page, identifier, 'element', text)
|
|
35
|
+
await world.expectElement(locator)
|
|
36
|
+
})
|
|
37
|
+
Then('I should not/NOT see a/an/the {string} (element )with (the )(text ){string}', async function (world:VitestBrowserWorld, identifier, text) {
|
|
38
|
+
let locator = await world.getLocator(world.page, identifier, 'element', text)
|
|
39
|
+
await world.expectElement(locator, false)
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
// ================
|
|
43
|
+
// Element state
|
|
44
|
+
Then('a/an/the {string} {word} should be visible/hidden/invisible', async function (world:VitestBrowserWorld, identifier, role) {
|
|
45
|
+
let state = world.info.step?.match(/(\w+)$/)![0]
|
|
46
|
+
let locator = world.getLocator(world.page, identifier, role)
|
|
47
|
+
await world.expectElement(locator, true, state === 'visible')
|
|
48
|
+
})
|
|
49
|
+
Then('a/an/the {string} (element )with (the )(text ){string} should be visible/hidden/invisible', async function (world:VitestBrowserWorld, identifier, text) {
|
|
50
|
+
let state = world.info.step?.match(/(\w+)$/)![0]
|
|
51
|
+
let locator = world.getLocator(world.page, identifier, 'element', text)
|
|
52
|
+
await world.expectElement(locator, true, state === 'visible')
|
|
53
|
+
})
|
|
54
|
+
// Then('a/an/the {string} {word} should be attached/detatched', async function (world:VitestBrowserWorld, identifier, role) {
|
|
55
|
+
// let state = world.info.step?.match(/(\w)$/)![0] as 'attached'|'detached'
|
|
56
|
+
// let locator = world.getLocator(world.page, identifier, role)
|
|
57
|
+
// await locator.waitFor({ state, timeout:world.worldConfig.stepTimeout })
|
|
58
|
+
// })
|
|
59
|
+
// Then('a/an/the {string} (element )with (the )(text ){string} should be attached/detatched', async function (world:VitestBrowserWorld, identifier, text) {
|
|
60
|
+
// let state = world.info.step?.match(/(\w)$/)![0] as 'attached'|'detached'
|
|
61
|
+
// let locator = world.getLocator(world.page, identifier, 'element', text)
|
|
62
|
+
// await locator.waitFor({ state, timeout:world.worldConfig.stepTimeout })
|
|
63
|
+
// })
|
|
64
|
+
|
|
65
|
+
// disabled / enabled
|
|
66
|
+
Then('a/an/the {string} {word} should be disabled', async function (world:VitestBrowserWorld, identifier, role) {
|
|
67
|
+
let locator = await world.getLocator(world.page, identifier, role)
|
|
68
|
+
await expect(locator).toBeDisabled()
|
|
69
|
+
})
|
|
70
|
+
Then('a/an/the {string} {word} should be enabled', async function (world:VitestBrowserWorld, identifier, role) {
|
|
71
|
+
let locator = await world.getLocator(world.page, identifier, role)
|
|
72
|
+
await expect(locator).toBeEnabled()
|
|
73
|
+
})
|
|
74
|
+
Then('a/an/the {string} (element )with (the )(text ){string} should be disabled', async function (world:VitestBrowserWorld, identifier, text) {
|
|
75
|
+
let locator = await world.getLocator(world.page, identifier, 'element', text)
|
|
76
|
+
await expect(locator).toBeDisabled()
|
|
77
|
+
})
|
|
78
|
+
Then('a/an/the {string} (element )with (the )(text ){string} should be enabled', async function (world:VitestBrowserWorld, identifier, text) {
|
|
79
|
+
let locator = await world.getLocator(world.page, identifier, 'element', text)
|
|
80
|
+
await expect(locator).toBeEnabled()
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
// checked / unchecked
|
|
84
|
+
Then('a/an/the {string} {word} should be checked', async function (world:VitestBrowserWorld, identifier, role) {
|
|
85
|
+
let locator = await world.getLocator(world.page, identifier, role)
|
|
86
|
+
await expect(locator).toBeChecked()
|
|
87
|
+
})
|
|
88
|
+
Then('a/an/the {string} {word} should be unchecked', async function (world:VitestBrowserWorld, identifier, role) {
|
|
89
|
+
let locator = await world.getLocator(world.page, identifier, role)
|
|
90
|
+
await expect(locator).not.toBeChecked()
|
|
91
|
+
})
|
|
92
|
+
Then('a/an/the {string} (element )with (the )(text ){string} should be checked', async function (world:VitestBrowserWorld, identifier, text) {
|
|
93
|
+
let locator = await world.getLocator(world.page, identifier, 'element', text)
|
|
94
|
+
await expect(locator).toBeChecked()
|
|
95
|
+
})
|
|
96
|
+
Then('a/an/the {string} (element )with (the )(text ){string} should be unchecked', async function (world:VitestBrowserWorld, identifier, text) {
|
|
97
|
+
let locator = await world.getLocator(world.page, identifier, 'element', text)
|
|
98
|
+
await expect(locator).not.toBeChecked()
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
// focused / unfocused
|
|
102
|
+
Then('a/an/the {string} {word} should be focused/active', async function (world:VitestBrowserWorld, identifier, role) {
|
|
103
|
+
let locator = await world.getLocator(world.page, identifier, role)
|
|
104
|
+
await expect(locator).toHaveFocus()
|
|
105
|
+
})
|
|
106
|
+
Then('a/an/the {string} {word} should be unfocused/blurred', async function (world:VitestBrowserWorld, identifier, role) {
|
|
107
|
+
let locator = await world.getLocator(world.page, identifier, role)
|
|
108
|
+
await expect(locator).not.toHaveFocus()
|
|
109
|
+
})
|
|
110
|
+
Then('a/an/the {string} (element )with (the )(text ){string} should be focused/active', async function (world:VitestBrowserWorld, identifier, text) {
|
|
111
|
+
let locator = await world.getLocator(world.page, identifier, 'element', text)
|
|
112
|
+
await expect(locator).toHaveFocus()
|
|
113
|
+
})
|
|
114
|
+
Then('a/an/the {string} (element )with (the )(text ){string} should be unfocused/blurred', async function (world:VitestBrowserWorld, identifier, text) {
|
|
115
|
+
let locator = await world.getLocator(world.page, identifier, 'element', text)
|
|
116
|
+
await expect(locator).not.toHaveFocus()
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
// Values
|
|
120
|
+
Then('a/an/the (value of ){string} should contain/include/be/equal {string}', async function (world:VitestBrowserWorld, identifier, expected) {
|
|
121
|
+
let exact = world.info.step?.match(/ should (?:be|equal) ['"]/) ? true : false
|
|
122
|
+
let locator = await world.getLocator(world.page, identifier, 'input')
|
|
123
|
+
if (exact) await expect(locator).toHaveValue(expected)
|
|
124
|
+
else {
|
|
125
|
+
let actual = (await locator.element() as HTMLInputElement)?.value ?? ''
|
|
126
|
+
await expect(actual).toContain(expected)
|
|
127
|
+
}
|
|
128
|
+
})
|
|
129
|
+
Then('a/an/the (value of )(the ){string} {word} should contain/include/be/equal {string}', async function (world:VitestBrowserWorld, identifier, role, expected) {
|
|
130
|
+
let exact = world.info.step?.match(/ should (?:be|equal) ['"]/) ? true : false
|
|
131
|
+
let locator = await world.getLocator(world.page, identifier, role)
|
|
132
|
+
if (exact) await expect(locator).toHaveValue(expected)
|
|
133
|
+
else {
|
|
134
|
+
let actual = (await locator.element() as HTMLInputElement)?.value ?? ''
|
|
135
|
+
await expect(actual).toContain(expected)
|
|
136
|
+
}
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
Then('a/an/the (value of )(the ){string} should not/NOT contain/include/be/equal {string}', async function (world:VitestBrowserWorld, identifier, expected) {
|
|
140
|
+
let exact = world.info.step?.match(/ should (?:not|NOT) (?:be|equal) ['"]/) ? true : false
|
|
141
|
+
let locator = await world.getLocator(world.page, identifier, 'input')
|
|
142
|
+
if (exact) await expect(locator).not.toHaveValue(expected)
|
|
143
|
+
else {
|
|
144
|
+
let actual = (await locator.element() as HTMLInputElement)?.value ?? ''
|
|
145
|
+
await expect(actual).not.toContain(expected)
|
|
146
|
+
}
|
|
147
|
+
})
|
|
148
|
+
Then('a/an/the (value of )(the ){string} {word} should not/NOT contain/include/be/equal {string}', async function (world:VitestBrowserWorld, identifier, role, expected) {
|
|
149
|
+
let exact = world.info.step?.match(/ should (?:not|NOT) (?:be|equal) ['"]/) ? true : false
|
|
150
|
+
let locator = await world.getLocator(world.page, identifier, role)
|
|
151
|
+
if (exact) await expect(locator).not.toHaveValue(expected)
|
|
152
|
+
else {
|
|
153
|
+
let actual = (await locator.element() as HTMLInputElement)?.value ?? ''
|
|
154
|
+
await expect(actual).not.toContain(expected)
|
|
155
|
+
}
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
// Visual regression testing
|
|
159
|
+
Then('(the )screenshot/snapshot should match', async function (world:VitestBrowserWorld) {
|
|
160
|
+
await expect(world.page).toMatchImageSnapshot({
|
|
161
|
+
...world.worldConfig.screenshotOptions,
|
|
162
|
+
customSnapshotsDir: world.screenshotDir,
|
|
163
|
+
customSnapshotIdentifier: world.screenshotFilename,
|
|
164
|
+
})
|
|
165
|
+
})
|
|
166
|
+
Then('(the )screenshot/snapshot {string} should match', async function (world:VitestBrowserWorld, name:string) {
|
|
167
|
+
let explodedTags = world.info.explodedIdx ? `_(${world.info.tags.join(',')})` : ''
|
|
168
|
+
await expect(world.page).toMatchImageSnapshot({
|
|
169
|
+
...world.worldConfig.screenshotOptions,
|
|
170
|
+
customSnapshotsDir: world.screenshotDir,
|
|
171
|
+
customSnapshotIdentifier: `${name}${explodedTags}`,
|
|
172
|
+
})
|
|
173
|
+
})
|
|
174
|
+
Then('(the )screenshot/snapshot of the {string} {word} should match', async function (world:VitestBrowserWorld, identifier, role) {
|
|
175
|
+
let locator = await world.getLocator(world.page, identifier, role)
|
|
176
|
+
await expect(locator).toMatchImageSnapshot({
|
|
177
|
+
...world.worldConfig.screenshotOptions,
|
|
178
|
+
customSnapshotsDir: world.screenshotDir,
|
|
179
|
+
customSnapshotIdentifier: world.screenshotFilename,
|
|
180
|
+
})
|
|
181
|
+
})
|
|
182
|
+
Then('(the )screenshot/snapshot {string} of the {string} {word} should match', async function (world:VitestBrowserWorld, name, identifier, role) {
|
|
183
|
+
let locator = await world.getLocator(world.page, identifier, role)
|
|
184
|
+
let explodedTags = world.info.explodedIdx ? `_(${world.info.tags.join(',')})` : ''
|
|
185
|
+
await expect(locator).toMatchImageSnapshot({
|
|
186
|
+
...world.worldConfig.screenshotOptions,
|
|
187
|
+
customSnapshotsDir: world.screenshotDir,
|
|
188
|
+
customSnapshotIdentifier: `${name}${explodedTags}`,
|
|
189
|
+
})
|
|
190
|
+
})
|