@quickpickle/vitest-browser 0.0.3 → 0.2.0

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