Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(browser): implement several userEvent methods, add fill and dragAndDrop events #5882

Merged
merged 28 commits into from
Jun 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
00ae8a7
feat: implement some userEvent methods: type/clear, add fill event
sheremet-va Jun 13, 2024
439ab2c
docs: more docs
sheremet-va Jun 13, 2024
51bc642
chore: remove only
sheremet-va Jun 13, 2024
108bc4b
feat: support selectall
sheremet-va Jun 13, 2024
8617102
feat: support tab method
sheremet-va Jun 13, 2024
b20b4d2
chore: logic closer to user-event
sheremet-va Jun 13, 2024
8112c45
chore: rename keyboard to sendKeys
sheremet-va Jun 13, 2024
ad4808a
chore: remove tester/body from playwright context, add only iframe, i…
sheremet-va Jun 13, 2024
43ae913
chore: update docs
sheremet-va Jun 13, 2024
6b75e7c
chore: remove sendKeys API
sheremet-va Jun 13, 2024
0a6f966
chore: fix keyboard event on playwright
sheremet-va Jun 13, 2024
fb8f6cd
fix: webdriverio correctly focuses the body
sheremet-va Jun 13, 2024
6462774
feat: add hover/unhover, expose option types for playwright
sheremet-va Jun 13, 2024
a5ba26f
chore: add more events!
sheremet-va Jun 14, 2024
06e1f58
docs: add documentation for userEvent commands
sheremet-va Jun 17, 2024
37b8e5f
chore: cleanup
sheremet-va Jun 17, 2024
f1e0716
feat: add dblClick and selectOptions
sheremet-va Jun 17, 2024
c19af2e
feat: add dragAndDrop method
sheremet-va Jun 17, 2024
00f15bf
chore: delete screenshot
sheremet-va Jun 17, 2024
9f3052a
chrore: ignore screenshot folders
sheremet-va Jun 17, 2024
9aed6c0
test: add test for hover
sheremet-va Jun 17, 2024
07eeff3
chore: stability
sheremet-va Jun 17, 2024
6c3ceae
chore: rewrite assertion
sheremet-va Jun 17, 2024
6656c8d
fix: patch dragAndDrop in webdriverio
sheremet-va Jun 17, 2024
52a073a
test: add tests for userEvent
sheremet-va Jun 18, 2024
5c72d71
fix: use action('key') API for webdriverio
sheremet-va Jun 18, 2024
d367570
chore: don't reload the page on keyboard interaction
sheremet-va Jun 18, 2024
563aa63
chore: cleanup
sheremet-va Jun 18, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,5 @@ docs/public/sponsors
.eslintcache
docs/.vitepress/cache/
!test/cli/fixtures/dotted-files/**/.cache
.vitest-reports
test/browser/test/__screenshots__/**/*
.vitest-reports
4 changes: 2 additions & 2 deletions docs/config/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -923,15 +923,15 @@ Minimum number of workers to run tests in. `poolOptions.{threads,vmThreads}.minT
### testTimeout

- **Type:** `number`
- **Default:** `5000`
- **Default:** `5_000` in Node.js, `15_000` if `browser.enabled` is `true`
- **CLI:** `--test-timeout=5000`, `--testTimeout=5000`

Default timeout of a test in milliseconds

### hookTimeout

- **Type:** `number`
- **Default:** `10000`
- **Default:** `10_000` in Node.js, `30_000` if `browser.enabled` is `true`
- **CLI:** `--hook-timeout=10000`, `--hookTimeout=10000`

Default timeout of a hook in milliseconds
Expand Down
483 changes: 454 additions & 29 deletions docs/guide/browser.md

Large diffs are not rendered by default.

142 changes: 120 additions & 22 deletions packages/browser/context.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,25 +19,6 @@ export interface FsOptions {
flag?: string | number
}

export interface TypePayload {
type: string
}
export interface PressPayload {
press: string
}
export interface DownPayload {
down: string
}
export interface UpPayload {
up: string
}

export type SendKeysPayload =
| TypePayload
| PressPayload
| DownPayload
| UpPayload

export interface ScreenshotOptions {
element?: Element
/**
Expand All @@ -57,21 +38,138 @@ export interface BrowserCommands {
options?: BufferEncoding | (FsOptions & { mode?: number | string })
) => Promise<void>
removeFile: (path: string) => Promise<void>
sendKeys: (payload: SendKeysPayload) => Promise<void>
}

export interface UserEvent {
setup: () => UserEvent
/**
* Click on an element. Uses provider's API under the hood and supports all its options.
* @see {@link https://playwright.dev/docs/api/class-locator#locator-click} Playwright API
* @see {@link https://webdriver.io/docs/api/element/click/} WebdriverIO API
* @see {@link https://testing-library.com/docs/user-event/convenience/#click} testing-library API
*/
click: (element: Element, options?: UserEventClickOptions) => Promise<void>
/**
* Triggers a double click event on an element. Uses provider's API under the hood.
* @see {@link https://playwright.dev/docs/api/class-locator#locator-dblclick} Playwright API
* @see {@link https://webdriver.io/docs/api/element/doubleClick/} WebdriverIO API
* @see {@link https://testing-library.com/docs/user-event/convenience/#dblClick} testing-library API
*/
dblClick: (element: Element, options?: UserEventDoubleClickOptions) => Promise<void>
/**
* Choose one or more values from a select element. Uses provider's API under the hood.
* If select doesn't have `multiple` attribute, only the first value will be selected.
* @example
* await userEvent.selectOptions(select, 'Option 1')
* expect(select).toHaveValue('option-1')
*
* await userEvent.selectOptions(select, 'option-1')
* expect(select).toHaveValue('option-1')
*
* await userEvent.selectOptions(select, [
* screen.getByRole('option', { name: 'Option 1' }),
* screen.getByRole('option', { name: 'Option 2' }),
* ])
* expect(select).toHaveValue(['option-1', 'option-2'])
* @see {@link https://playwright.dev/docs/api/class-locator#locator-select-option} Playwright API
* @see {@link https://webdriver.io/docs/api/element/doubleClick/} WebdriverIO API
* @see {@link https://testing-library.com/docs/user-event/utility/#-selectoptions-deselectoptions} testing-library API
*/
selectOptions: (
element: Element,
values: HTMLElement | HTMLElement[] | string | string[],
options?: UserEventSelectOptions,
) => Promise<void>
/**
* Type text on the keyboard. If any input is focused, it will receive the text,
* otherwise it will be typed on the document. Uses provider's API under the hood.
* **Supports** [user-event `keyboard` syntax](https://testing-library.com/docs/user-event/keyboard) (e.g., `{Shift}`) even with `playwright` and `webdriverio` providers.
* @example
* await userEvent.keyboard('foo') // translates to: f, o, o
* await userEvent.keyboard('{{a[[') // translates to: {, a, [
* await userEvent.keyboard('{Shift}{f}{o}{o}') // translates to: Shift, f, o, o
* @see {@link https://playwright.dev/docs/api/class-locator#locator-press} Playwright API
* @see {@link https://webdriver.io/docs/api/browser/keys} WebdriverIO API
* @see {@link https://testing-library.com/docs/user-event/keyboard} testing-library API
*/
keyboard: (text: string) => Promise<void>
/**
* Types text into an element. Uses provider's API under the hood.
* **Supports** [user-event `keyboard` syntax](https://testing-library.com/docs/user-event/keyboard) (e.g., `{Shift}`) even with `playwright` and `webdriverio` providers.
* @example
* await userEvent.type(input, 'foo') // translates to: f, o, o
* await userEvent.type(input, '{{a[[') // translates to: {, a, [
* await userEvent.type(input, '{Shift}{f}{o}{o}') // translates to: Shift, f, o, o
* @see {@link https://playwright.dev/docs/api/class-locator#locator-press} Playwright API
* @see {@link https://webdriver.io/docs/api/browser/action#key-input-source} WebdriverIO API
* @see {@link https://testing-library.com/docs/user-event/utility/#type} testing-library API
*/
type: (element: Element, text: string, options?: UserEventTypeOptions) => Promise<void>
/**
* Removes all text from an element. Uses provider's API under the hood.
* @see {@link https://playwright.dev/docs/api/class-locator#locator-clear} Playwright API
* @see {@link https://webdriver.io/docs/api/element/clearValue} WebdriverIO API
* @see {@link https://testing-library.com/docs/user-event/utility/#clear} testing-library API
*/
clear: (element: Element) => Promise<void>
/**
* Sends a `Tab` key event. Uses provider's API under the hood.
* @see {@link https://playwright.dev/docs/api/class-locator#locator-press} Playwright API
* @see {@link https://webdriver.io/docs/api/element/keys} WebdriverIO API
* @see {@link https://testing-library.com/docs/user-event/convenience/#tab} testing-library API
*/
tab: (options?: UserEventTabOptions) => Promise<void>
/**
* Hovers over an element. Uses provider's API under the hood.
* @see {@link https://playwright.dev/docs/api/class-locator#locator-hover} Playwright API
* @see {@link https://webdriver.io/docs/api/element/moveTo/} WebdriverIO API
* @see {@link https://testing-library.com/docs/user-event/convenience/#hover} testing-library API
*/
hover: (element: Element, options?: UserEventHoverOptions) => Promise<void>
/**
* Moves cursor position to the body element. Uses provider's API under the hood.
* By default, the cursor position is in the center (in webdriverio) or in some visible place (in playwright)
* of the body element, so if the current element is already there, this will have no effect.
* @see {@link https://playwright.dev/docs/api/class-locator#locator-hover} Playwright API
* @see {@link https://webdriver.io/docs/api/element/moveTo/} WebdriverIO API
* @see {@link https://testing-library.com/docs/user-event/convenience/#hover} testing-library API
*/
unhover: (element: Element, options?: UserEventHoverOptions) => Promise<void>
/**
* Fills an input element with text. This will remove any existing text in the input before typing the new text.
* Uses provider's API under the hood.
* This API is faster than using `userEvent.type` or `userEvent.keyboard`, but it **doesn't support** [user-event `keyboard` syntax](https://testing-library.com/docs/user-event/keyboard) (e.g., `{Shift}`).
* @example
* await userEvent.fill(input, 'foo') // translates to: f, o, o
* await userEvent.fill(input, '{{a[[') // translates to: {, {, a, [, [
* await userEvent.fill(input, '{Shift}') // translates to: {, S, h, i, f, t, }
* @see {@link https://playwright.dev/docs/api/class-locator#locator-fill} Playwright API
* @see {@link https://webdriver.io/docs/api/element/setValue} WebdriverIO API
* @see {@link https://testing-library.com/docs/user-event/utility/#type} testing-library API
*/
fill: (element: Element, text: string, options?: UserEventFillOptions) => Promise<void>
/**
* Drags a source element on top of the target element. This API is not supported by "preview" provider.
* @see {@link https://playwright.dev/docs/api/class-frame#frame-drag-and-drop} Playwright API
* @see {@link https://webdriver.io/docs/api/element/dragAndDrop/} WebdriverIO API
*/
dragAndDrop: (source: Element, target: Element, options?: UserEventDragAndDropOptions) => Promise<void>
}

export interface UserEventFillOptions {}
export interface UserEventHoverOptions {}
export interface UserEventSelectOptions {}
export interface UserEventClickOptions {}
export interface UserEventDoubleClickOptions {}
export interface UserEventDragAndDropOptions {}

export interface UserEventTabOptions {
shift?: boolean
}

export interface UserEventClickOptions {
[key: string]: any
export interface UserEventTypeOptions {
skipClick?: boolean
skipAutoClose?: boolean
}

type Platform =
Expand Down
27 changes: 23 additions & 4 deletions packages/browser/providers/playwright.d.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import type {
BrowserContext,
BrowserContextOptions,
FrameLocator,
Frame,
LaunchOptions,
Locator,
Page,
} from 'playwright'

Expand All @@ -17,7 +17,26 @@ declare module 'vitest/node' {

export interface BrowserCommandContext {
page: Page
tester: FrameLocator
body: Locator
frame: Frame
context: BrowserContext
}
}

type PWHoverOptions = Parameters<Page['hover']>[1]
type PWClickOptions = Parameters<Page['click']>[1]
type PWDoubleClickOptions = Parameters<Page['dblclick']>[1]
type PWFillOptions = Parameters<Page['fill']>[2]
type PWScreenshotOptions = Parameters<Page['screenshot']>[0]
type PWSelectOptions = Parameters<Page['selectOption']>[2]
type PWDragAndDropOptions = Parameters<Page['dragAndDrop']>[2]

declare module '@vitest/browser/context' {
export interface UserEventHoverOptions extends PWHoverOptions {}
export interface UserEventClickOptions extends PWClickOptions {}
export interface UserEventDoubleClickOptions extends PWDoubleClickOptions {}
export interface UserEventFillOptions extends PWFillOptions {}
export interface UserEventSelectOptions extends PWSelectOptions {}
export interface UserEventDragOptions extends UserEventDragAndDropOptions {}

export interface ScreenshotOptions extends PWScreenshotOptions {}
}
102 changes: 97 additions & 5 deletions packages/browser/src/client/context.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
import type { Task, WorkerGlobalState } from 'vitest'
import type {
BrowserPage,
UserEvent,
UserEventClickOptions,
} from '../../context'
import type { BrowserPage, UserEvent, UserEventClickOptions, UserEventTabOptions, UserEventTypeOptions } from '../../context'
import type { BrowserRPC } from './client'
import type { BrowserRunnerState } from './utils'

Expand Down Expand Up @@ -62,11 +58,107 @@ function triggerCommand<T>(command: string, ...args: any[]) {
return rpc().triggerCommand<T>(contextId, command, filepath(), args)
}

const provider = runner().provider

export const userEvent: UserEvent = {
// TODO: actually setup userEvent with config options
setup() {
return userEvent
},
click(element: Element, options: UserEventClickOptions = {}) {
const xpath = convertElementToXPath(element)
return triggerCommand('__vitest_click', xpath, options)
},
dblClick(element: Element, options: UserEventClickOptions = {}) {
const xpath = convertElementToXPath(element)
return triggerCommand('__vitest_dblClick', xpath, options)
},
selectOptions(element, value) {
const values = provider === 'webdriverio'
? getWebdriverioSelectOptions(element, value)
: getSimpleSelectOptions(element, value)
return triggerCommand('__vitest_selectOptions', convertElementToXPath(element), values)
},
type(element: Element, text: string, options: UserEventTypeOptions = {}) {
const xpath = convertElementToXPath(element)
return triggerCommand('__vitest_type', xpath, text, options)
},
clear(element: Element) {
const xpath = convertElementToXPath(element)
return triggerCommand('__vitest_clear', xpath)
},
tab(options: UserEventTabOptions = {}) {
return triggerCommand('__vitest_tab', options)
},
keyboard(text: string) {
return triggerCommand('__vitest_keyboard', text)
},
hover(element: Element) {
const xpath = convertElementToXPath(element)
return triggerCommand('__vitest_hover', xpath)
},
unhover(element: Element) {
const xpath = convertElementToXPath(element.ownerDocument.body)
return triggerCommand('__vitest_hover', xpath)
},

// non userEvent events, but still useful
fill(element: Element, text: string, options) {
const xpath = convertElementToXPath(element)
return triggerCommand('__vitest_fill', xpath, text, options)
},
dragAndDrop(source: Element, target: Element, options = {}) {
const sourceXpath = convertElementToXPath(source)
const targetXpath = convertElementToXPath(target)
return triggerCommand('__vitest_dragAndDrop', sourceXpath, targetXpath, options)
},
}

function getWebdriverioSelectOptions(element: Element, value: string | string[] | HTMLElement[] | HTMLElement) {
const options = [...element.querySelectorAll('option')] as HTMLOptionElement[]

const arrayValues = Array.isArray(value) ? value : [value]

if (!arrayValues.length) {
return []
}

if (arrayValues.length > 1) {
throw new Error('Provider "webdriverio" doesn\'t support selecting multiple values at once')
}

const optionValue = arrayValues[0]

if (typeof optionValue !== 'string') {
const index = options.indexOf(optionValue as HTMLOptionElement)
if (index === -1) {
throw new Error(`The element ${convertElementToXPath(optionValue)} was not found in the "select" options.`)
}

return [{ index }]
}

const valueIndex = options.findIndex(option => option.value === optionValue)
if (valueIndex !== -1) {
return [{ index: valueIndex }]
}

const labelIndex = options.findIndex(option => option.textContent?.trim() === optionValue || option.ariaLabel === optionValue)

if (labelIndex === -1) {
throw new Error(`The option "${optionValue}" was not found in the "select" options.`)
}

return [{ index: labelIndex }]
}

function getSimpleSelectOptions(element: Element, value: string | string[] | HTMLElement[] | HTMLElement) {
return (Array.isArray(value) ? value : [value]).map((v) => {
if (typeof v !== 'string') {
return { element: convertElementToXPath(v) }
}
return v
})
}

const screenshotIds: Record<string, Record<string, string>> = {}
Expand Down
1 change: 1 addition & 0 deletions packages/browser/src/client/orchestrator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ function createIframe(container: HTMLDivElement, file: string) {
iframe.style.position = 'relative'
iframe.setAttribute('allowfullscreen', 'true')
iframe.setAttribute('allow', 'clipboard-write;')
iframe.setAttribute('name', 'vitest-iframe')

iframes.set(file, iframe)
container.appendChild(iframe)
Expand Down
1 change: 1 addition & 0 deletions packages/browser/src/client/public/esm-client-injector.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ window.__vitest_browser_runner__ = {
files: { __VITEST_FILES__ },
type: { __VITEST_TYPE__ },
contextId: { __VITEST_CONTEXT_ID__ },
provider: { __VITEST_PROVIDER__ },
};

const config = __vitest_browser_runner__.config;
Expand Down
1 change: 1 addition & 0 deletions packages/browser/src/client/tester.html
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
{__VITEST_SCRIPTS__}
</head>
<body
data-vitest-body
style="
width: 100%;
height: 100%;
Expand Down
1 change: 1 addition & 0 deletions packages/browser/src/client/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export interface BrowserRunnerState {
runningFiles: string[]
moduleCache: WorkerGlobalState['moduleCache']
config: ResolvedConfig
provider: string
viteConfig: {
root: string
}
Expand Down
Loading
Loading