@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.
Files changed (69) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/LICENSE +21 -0
  3. package/README.md +4 -0
  4. package/dist/VitestBrowserWorld.cjs +179 -0
  5. package/dist/VitestBrowserWorld.cjs.map +1 -0
  6. package/dist/VitestBrowserWorld.d.ts +103 -0
  7. package/dist/VitestBrowserWorld.mjs +176 -0
  8. package/dist/VitestBrowserWorld.mjs.map +1 -0
  9. package/dist/actions.steps.cjs +157 -0
  10. package/dist/actions.steps.cjs.map +1 -0
  11. package/dist/actions.steps.d.ts +1 -0
  12. package/dist/actions.steps.mjs +155 -0
  13. package/dist/actions.steps.mjs.map +1 -0
  14. package/dist/frameworks/ReactBrowserWorld.cjs +38 -0
  15. package/dist/frameworks/ReactBrowserWorld.cjs.map +1 -0
  16. package/dist/frameworks/ReactBrowserWorld.d.ts +10 -0
  17. package/dist/frameworks/ReactBrowserWorld.mjs +36 -0
  18. package/dist/frameworks/ReactBrowserWorld.mjs.map +1 -0
  19. package/dist/frameworks/SvelteBrowserWorld.cjs +17 -0
  20. package/dist/frameworks/SvelteBrowserWorld.cjs.map +1 -0
  21. package/dist/frameworks/SvelteBrowserWorld.d.ts +9 -0
  22. package/dist/frameworks/SvelteBrowserWorld.mjs +15 -0
  23. package/dist/frameworks/SvelteBrowserWorld.mjs.map +1 -0
  24. package/dist/frameworks/VueBrowserWorld.cjs +25 -0
  25. package/dist/frameworks/VueBrowserWorld.cjs.map +1 -0
  26. package/dist/frameworks/VueBrowserWorld.d.ts +10 -0
  27. package/dist/frameworks/VueBrowserWorld.mjs +23 -0
  28. package/dist/frameworks/VueBrowserWorld.mjs.map +1 -0
  29. package/dist/frameworks/react.cjs +11 -0
  30. package/dist/frameworks/react.cjs.map +1 -0
  31. package/dist/frameworks/react.d.ts +1 -0
  32. package/dist/frameworks/react.mjs +9 -0
  33. package/dist/frameworks/react.mjs.map +1 -0
  34. package/dist/frameworks/svelte.cjs +10 -0
  35. package/dist/frameworks/svelte.cjs.map +1 -0
  36. package/dist/frameworks/svelte.d.ts +1 -0
  37. package/dist/frameworks/svelte.mjs +8 -0
  38. package/dist/frameworks/svelte.mjs.map +1 -0
  39. package/dist/frameworks/vue.cjs +10 -0
  40. package/dist/frameworks/vue.cjs.map +1 -0
  41. package/dist/frameworks/vue.d.ts +1 -0
  42. package/dist/frameworks/vue.mjs +8 -0
  43. package/dist/frameworks/vue.mjs.map +1 -0
  44. package/dist/outcomes.steps.cjs +186 -0
  45. package/dist/outcomes.steps.cjs.map +1 -0
  46. package/dist/outcomes.steps.d.ts +1 -0
  47. package/dist/outcomes.steps.mjs +184 -0
  48. package/dist/outcomes.steps.mjs.map +1 -0
  49. package/package.json +103 -0
  50. package/rollup.config.js +58 -0
  51. package/src/VitestBrowserWorld.ts +205 -0
  52. package/src/actions.steps.ts +169 -0
  53. package/src/frameworks/ReactBrowserWorld.ts +41 -0
  54. package/src/frameworks/SvelteBrowserWorld.ts +15 -0
  55. package/src/frameworks/VueBrowserWorld.ts +23 -0
  56. package/src/frameworks/react.ts +4 -0
  57. package/src/frameworks/svelte.ts +4 -0
  58. package/src/frameworks/vue.ts +4 -0
  59. package/src/outcomes.steps.ts +190 -0
  60. package/tests/react/Hello.tsx +23 -0
  61. package/tests/react/react.feature +15 -0
  62. package/tests/svelte/Hello.svelte +31 -0
  63. package/tests/svelte/svelte.feature +16 -0
  64. package/tests/vue/Hello.vue +31 -0
  65. package/tests/vue/vue.feature +16 -0
  66. package/tsconfig.json +17 -0
  67. package/vite.config.ts +8 -0
  68. package/vitest.workspace.ts +95 -0
  69. 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,4 @@
1
+ import { setWorldConstructor } from "quickpickle";
2
+ import { ReactBrowserWorld } from "./ReactBrowserWorld";
3
+
4
+ setWorldConstructor(ReactBrowserWorld);
@@ -0,0 +1,4 @@
1
+ import { setWorldConstructor } from "quickpickle";
2
+ import { SvelteBrowserWorld } from "./SvelteBrowserWorld";
3
+
4
+ setWorldConstructor(SvelteBrowserWorld);
@@ -0,0 +1,4 @@
1
+ import { setWorldConstructor } from "quickpickle";
2
+ import { VueBrowserWorld } from "./VueBrowserWorld";
3
+
4
+ setWorldConstructor(VueBrowserWorld);
@@ -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
+ })