@quickpickle/vitest-browser 0.0.3 → 0.1.0

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