[READ-ONLY] Mirror of https://github.com/vitest-dev/vitest. Next generation testing framework powered by Vite. vitest.dev
test testing-tools vite
12

Configure Feed

Select the types of activity you want to include in your feed.

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

authored by

Vladimir and committed by
GitHub
(Jun 18, 2024, 1:29 PM +0200) 4dbea4ae f969fb0f

+1743 -285
+2 -1
.gitignore
··· 22 22 .eslintcache 23 23 docs/.vitepress/cache/ 24 24 !test/cli/fixtures/dotted-files/**/.cache 25 - .vitest-reports 25 + test/browser/test/__screenshots__/**/* 26 + .vitest-reports
+2 -2
docs/config/index.md
··· 923 923 ### testTimeout 924 924 925 925 - **Type:** `number` 926 - - **Default:** `5000` 926 + - **Default:** `5_000` in Node.js, `15_000` if `browser.enabled` is `true` 927 927 - **CLI:** `--test-timeout=5000`, `--testTimeout=5000` 928 928 929 929 Default timeout of a test in milliseconds ··· 931 931 ### hookTimeout 932 932 933 933 - **Type:** `number` 934 - - **Default:** `10000` 934 + - **Default:** `10_000` in Node.js, `30_000` if `browser.enabled` is `true` 935 935 - **CLI:** `--hook-timeout=10000`, `--hookTimeout=10000` 936 936 937 937 Default timeout of a hook in milliseconds
+454 -29
docs/guide/browser.md
··· 212 212 * @experimental 213 213 */ 214 214 export const userEvent: { 215 + setup: () => UserEvent 215 216 /** 216 217 * Click on an element. Uses provider's API under the hood and supports all its options. 217 218 * @see {@link https://playwright.dev/docs/api/class-locator#locator-click} Playwright API ··· 219 220 * @see {@link https://testing-library.com/docs/user-event/convenience/#click} testing-library API 220 221 */ 221 222 click: (element: Element, options?: UserEventClickOptions) => Promise<void> 223 + /** 224 + * Triggers a double click event on an element. Uses provider's API under the hood. 225 + * @see {@link https://playwright.dev/docs/api/class-locator#locator-dblclick} Playwright API 226 + * @see {@link https://webdriver.io/docs/api/element/doubleClick/} WebdriverIO API 227 + * @see {@link https://testing-library.com/docs/user-event/convenience/#dblClick} testing-library API 228 + */ 229 + dblClick: (element: Element, options?: UserEventDoubleClickOptions) => Promise<void> 230 + /** 231 + * Choose one or more values from a select element. Uses provider's API under the hood. 232 + * If select doesn't have `multiple` attribute, only the first value will be selected. 233 + * @see {@link https://playwright.dev/docs/api/class-locator#locator-select-option} Playwright API 234 + * @see {@link https://webdriver.io/docs/api/element/doubleClick/} WebdriverIO API 235 + * @see {@link https://testing-library.com/docs/user-event/utility/#-selectoptions-deselectoptions} testing-library API 236 + */ 237 + selectOptions: ( 238 + element: Element, 239 + values: HTMLElement | HTMLElement[] | string | string[], 240 + options?: UserEventSelectOptions, 241 + ) => Promise<void> 242 + /** 243 + * Type text on the keyboard. If any input is focused, it will receive the text, 244 + * otherwise it will be typed on the document. Uses provider's API under the hood. 245 + * **Supports** [user-event `keyboard` syntax](https://testing-library.com/docs/user-event/keyboard) (e.g., `{Shift}`) even with `playwright` and `webdriverio` providers. 246 + * @example 247 + * await userEvent.keyboard('foo') // translates to: f, o, o 248 + * await userEvent.keyboard('{{a[[') // translates to: {, a, [ 249 + * await userEvent.keyboard('{Shift}{f}{o}{o}') // translates to: Shift, f, o, o 250 + * @see {@link https://playwright.dev/docs/api/class-locator#locator-press} Playwright API 251 + * @see {@link https://webdriver.io/docs/api/browser/action#key-input-source} WebdriverIO API 252 + * @see {@link https://testing-library.com/docs/user-event/keyboard} testing-library API 253 + */ 254 + keyboard: (text: string) => Promise<void> 255 + /** 256 + * Types text into an element. Uses provider's API under the hood. 257 + * **Supports** [user-event `keyboard` syntax](https://testing-library.com/docs/user-event/keyboard) (e.g., `{Shift}`) even with `playwright` and `webdriverio` providers. 258 + * @example 259 + * await userEvent.type(input, 'foo') // translates to: f, o, o 260 + * await userEvent.type(input, '{{a[[') // translates to: {, a, [ 261 + * await userEvent.type(input, '{Shift}{f}{o}{o}') // translates to: Shift, f, o, o 262 + * @see {@link https://playwright.dev/docs/api/class-locator#locator-press} Playwright API 263 + * @see {@link https://webdriver.io/docs/api/browser/action#key-input-source} WebdriverIO API 264 + * @see {@link https://testing-library.com/docs/user-event/utility/#type} testing-library API 265 + */ 266 + type: (element: Element, text: string, options?: UserEventTypeOptions) => Promise<void> 267 + /** 268 + * Removes all text from an element. Uses provider's API under the hood. 269 + * @see {@link https://playwright.dev/docs/api/class-locator#locator-clear} Playwright API 270 + * @see {@link https://webdriver.io/docs/api/element/clearValue} WebdriverIO API 271 + * @see {@link https://testing-library.com/docs/user-event/utility/#clear} testing-library API 272 + */ 273 + clear: (element: Element) => Promise<void> 274 + /** 275 + * Sends a `Tab` key event. Uses provider's API under the hood. 276 + * @see {@link https://playwright.dev/docs/api/class-locator#locator-press} Playwright API 277 + * @see {@link https://webdriver.io/docs/api/element/keys} WebdriverIO API 278 + * @see {@link https://testing-library.com/docs/user-event/convenience/#tab} testing-library API 279 + */ 280 + tab: (options?: UserEventTabOptions) => Promise<void> 281 + /** 282 + * Hovers over an element. Uses provider's API under the hood. 283 + * @see {@link https://playwright.dev/docs/api/class-locator#locator-hover} Playwright API 284 + * @see {@link https://webdriver.io/docs/api/element/moveTo/} WebdriverIO API 285 + * @see {@link https://testing-library.com/docs/user-event/convenience/#hover} testing-library API 286 + */ 287 + hover: (element: Element, options?: UserEventHoverOptions) => Promise<void> 288 + /** 289 + * Moves cursor position to the body element. Uses provider's API under the hood. 290 + * By default, the cursor position is in the center (in webdriverio) or in some visible place (in playwright) 291 + * of the body element, so if the current element is already there, this will have no effect. 292 + * @see {@link https://playwright.dev/docs/api/class-locator#locator-hover} Playwright API 293 + * @see {@link https://webdriver.io/docs/api/element/moveTo/} WebdriverIO API 294 + * @see {@link https://testing-library.com/docs/user-event/convenience/#hover} testing-library API 295 + */ 296 + unhover: (element: Element, options?: UserEventHoverOptions) => Promise<void> 297 + /** 298 + * Fills an input element with text. This will remove any existing text in the input before typing the new value. 299 + * Uses provider's API under the hood. 300 + * 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}`). 301 + * @example 302 + * await userEvent.fill(input, 'foo') // translates to: f, o, o 303 + * await userEvent.fill(input, '{{a[[') // translates to: {, {, a, [, [ 304 + * await userEvent.fill(input, '{Shift}') // translates to: {, S, h, i, f, t, } 305 + * @see {@link https://playwright.dev/docs/api/class-locator#locator-fill} Playwright API 306 + * @see {@link https://webdriver.io/docs/api/element/setValue} WebdriverIO API 307 + * @see {@link https://testing-library.com/docs/user-event/utility/#type} testing-library API 308 + */ 309 + fill: (element: Element, text: string, options?: UserEventFillOptions) => Promise<void> 310 + /** 311 + * Drags a source element on top of the target element. This API is not supported by "preview" provider. 312 + * @see {@link https://playwright.dev/docs/api/class-frame#frame-drag-and-drop} Playwright API 313 + * @see {@link https://webdriver.io/docs/api/element/dragAndDrop/} WebdriverIO API 314 + */ 315 + dragAndDrop: (source: Element, target: Element, options?: UserEventDragAndDropOptions) => Promise<void> 222 316 } 223 317 224 318 /** ··· 243 337 screenshot: (options?: ScreenshotOptions) => Promise<string> 244 338 } 245 339 ``` 340 + 341 + ## Interactivity API 342 + 343 + Vitest implements a subset of [`@testing-library/user-event`](https://testing-library.com/docs/user-event) APIs using [Chrome DevTools Protocol](https://chromedevtools.github.io/devtools-protocol/) or [webdriver](https://www.w3.org/TR/webdriver/) APIs instead of faking events which makes the browser behaviour more reliable and consistent. 344 + 345 + Almost every `userEvent` method inherits its provider options. To see all available options in your IDE, add `webdriver` or `playwright` types to your `tsconfig.json` file: 346 + 347 + ::: code-group 348 + ```json [playwright] 349 + { 350 + "compilerOptions": { 351 + "types": [ 352 + "@vitest/browser/providers/playwright" 353 + ] 354 + } 355 + } 356 + ``` 357 + ```json [webdriverio] 358 + { 359 + "compilerOptions": { 360 + "types": [ 361 + "@vitest/browser/providers/webdriverio" 362 + ] 363 + } 364 + } 365 + ``` 366 + ::: 367 + 368 + ### userEvent.click 369 + 370 + - **Type:** `(element: Element, options?: UserEventClickOptions) => Promise<void>` 371 + 372 + Clicks on an element. Inherits provider's options. Please refer to your provider's documentation for detailed explanaition about how this method works. 373 + 374 + ```ts 375 + import { userEvent } from '@vitest/browser/context' 376 + import { screen } from '@testing-library/dom' 377 + 378 + test('clicks on an element', () => { 379 + const logo = screen.getByRole('img', { name: /logo/ }) 380 + 381 + await userEvent.click(logo) 382 + }) 383 + ``` 384 + 385 + References: 386 + 387 + - [Playwright `locator.click` API](https://playwright.dev/docs/api/class-locator#locator-click) 388 + - [WebdriverIO `element.click` API](https://webdriver.io/docs/api/element/click/) 389 + - [testing-library `click` API](https://testing-library.com/docs/user-event/convenience/#click) 390 + 391 + ### userEvent.dblClick 392 + 393 + - **Type:** `(element: Element, options?: UserEventDoubleClickOptions) => Promise<void>` 394 + 395 + Triggers a double click event on an element 396 + 397 + Please refer to your provider's documentation for detailed explanaition about how this method works. 398 + 399 + ```ts 400 + import { userEvent } from '@vitest/browser/context' 401 + import { screen } from '@testing-library/dom' 402 + 403 + test('triggers a double click on an element', () => { 404 + const logo = screen.getByRole('img', { name: /logo/ }) 405 + 406 + await userEvent.dblClick(logo) 407 + }) 408 + ``` 409 + 410 + References: 411 + 412 + - [Playwright `locator.dblclick` API](https://playwright.dev/docs/api/class-locator#locator-dblclick) 413 + - [WebdriverIO `element.doubleClick` API](https://webdriver.io/docs/api/element/doubleClick/) 414 + - [testing-library `dblClick` API](https://testing-library.com/docs/user-event/convenience/#dblClick) 415 + 416 + ### userEvent.fill 417 + 418 + - **Type:** `(element: Element, text: string) => Promise<void>` 419 + 420 + Fills an input/textarea/conteneditable element with text. This will remove any existing text in the input before typing the new value. 421 + 422 + ```ts 423 + import { userEvent } from '@vitest/browser/context' 424 + import { screen } from '@testing-library/dom' 425 + 426 + test('update input', () => { 427 + const input = screen.getByRole('input') 428 + 429 + await userEvent.fill(input, 'foo') // input.value == foo 430 + await userEvent.fill(input, '{{a[[') // input.value == {{a[[ 431 + await userEvent.fill(input, '{Shift}') // input.value == {Shift} 432 + }) 433 + ``` 434 + 435 + ::: tip 436 + This API is faster than using [`userEvent.type`](#userevent-type) or [`userEvent.keyboard`](#userevent-keyboard), but it **doesn't support** [user-event `keyboard` syntax](https://testing-library.com/docs/user-event/keyboard) (e.g., `{Shift}{selectall}`). 437 + 438 + We recommend using this API over [`userEvent.type`](#userevent-type) in situations when you don't need to enter special characters. 439 + ::: 440 + 441 + References: 442 + 443 + - [Playwright `locator.fill` API](https://playwright.dev/docs/api/class-locator#locator-fill) 444 + - [WebdriverIO `element.setValue` API](https://webdriver.io/docs/api/element/setValue) 445 + - [testing-library `type` API](https://testing-library.com/docs/user-event/utility/#type) 446 + 447 + ### userEvent.keyboard 448 + 449 + - **Type:** `(text: string) => Promise<void>` 450 + 451 + The `userEvent.keyboard` allows you to trigger keyboard strokes. If any input has a focus, it will type characters into that input. Otherwise, it will trigger keyboard events on the currently focused element (`document.body` if there are no focused elements). 452 + 453 + This API supports [user-event `keyboard` syntax](https://testing-library.com/docs/user-event/keyboard). 454 + 455 + ```ts 456 + import { userEvent } from '@vitest/browser/context' 457 + import { screen } from '@testing-library/dom' 458 + 459 + test('trigger keystrokes', () => { 460 + await userEvent.keyboard('foo') // translates to: f, o, o 461 + await userEvent.keyboard('{{a[[') // translates to: {, a, [ 462 + await userEvent.keyboard('{Shift}{f}{o}{o}') // translates to: Shift, f, o, o 463 + await userEvent.keyboard('{a>5}') // press a without releasing it and trigger 5 keydown 464 + await userEvent.keyboard('{a>5/}') // press a for 5 keydown and then release it 465 + }) 466 + ``` 467 + 468 + References: 469 + 470 + - [Playwright `locator.press` API](https://playwright.dev/docs/api/class-locator#locator-press) 471 + - [WebdriverIO `action('key')` API](https://webdriver.io/docs/api/browser/action#key-input-source) 472 + - [testing-library `type` API](https://testing-library.com/docs/user-event/utility/#type) 473 + 474 + ### userEvent.tab 475 + 476 + - **Type:** `(options?: UserEventTabOptions) => Promise<void>` 477 + 478 + Sends a `Tab` key event. This is a shorthand for `userEvent.keyboard('{tab}')`. 479 + 480 + ```ts 481 + import { userEvent } from '@vitest/browser/context' 482 + import { screen } from '@testing-library/dom' 483 + import '@testing-library/jest-dom' // adds support for "toHaveFocus" 484 + 485 + test('tab works', () => { 486 + const [input1, input2] = screen.getAllByRole('input') 487 + 488 + expect(input1).toHaveFocus() 489 + 490 + await userEvent.tab() 491 + 492 + expect(input2).toHaveFocus() 493 + 494 + await userEvent.tab({ shift: true }) 495 + 496 + expect(input1).toHaveFocus() 497 + }) 498 + ``` 499 + 500 + References: 501 + 502 + - [Playwright `locator.press` API](https://playwright.dev/docs/api/class-locator#locator-press) 503 + - [WebdriverIO `action('key')` API](https://webdriver.io/docs/api/browser/action#key-input-source) 504 + - [testing-library `tab` API](https://testing-library.com/docs/user-event/convenience/#tab) 505 + 506 + ### userEvent.type 507 + 508 + - **Type:** `(element: Element, text: string, options?: UserEventTypeOptions) => Promise<void>` 509 + 510 + ::: warning 511 + If you don't rely on [special characters](https://testing-library.com/docs/user-event/keyboard) (e.g., `{shift}` or `{selectall}`), it is recommended to use [`userEvent.fill`](#userevent-fill) instead. 512 + ::: 513 + 514 + The `type` method implements `@testing-library/user-event`'s [`type`](https://testing-library.com/docs/user-event/utility/#type) utility built on top of [`keyboard`](https://testing-library.com/docs/user-event/keyboard) API. 515 + 516 + This function allows you to type characters into an input/textarea/conteneditable element. It supports [user-event `keyboard` syntax](https://testing-library.com/docs/user-event/keyboard). 517 + 518 + If you just need to press characters without an input, use [`userEvent.keyboard`](#userevent-keyboard) API. 519 + 520 + ```ts 521 + import { userEvent } from '@vitest/browser/context' 522 + import { screen } from '@testing-library/dom' 523 + 524 + test('update input', () => { 525 + const input = screen.getByRole('input') 526 + 527 + await userEvent.type(input, 'foo') // input.value == foo 528 + await userEvent.type(input, '{{a[[') // input.value == foo{a[ 529 + await userEvent.type(input, '{Shift}') // input.value == foo{a[ 530 + }) 531 + ``` 532 + 533 + References: 534 + 535 + - [Playwright `locator.press` API](https://playwright.dev/docs/api/class-locator#locator-press) 536 + - [WebdriverIO `action('key')` API](https://webdriver.io/docs/api/browser/action#key-input-source) 537 + - [testing-library `type` API](https://testing-library.com/docs/user-event/utility/#type) 538 + 539 + ### userEvent.clear 540 + 541 + - **Type:** `(element: Element) => Promise<void>` 542 + 543 + This method clear the input element content. 544 + 545 + ```ts 546 + import { userEvent } from '@vitest/browser/context' 547 + import { screen } from '@testing-library/dom' 548 + import '@testing-library/jest-dom' // adds support for "toHaveValue" 549 + 550 + test('clears input', () => { 551 + const input = screen.getByRole('input') 552 + 553 + await userEvent.fill(input, 'foo') 554 + expect(input).toHaveValue('foo') 555 + 556 + await userEvent.clear(input) 557 + expect(input).toHaveValue('') 558 + }) 559 + ``` 560 + 561 + References: 562 + 563 + - [Playwright `locator.clear` API](https://playwright.dev/docs/api/class-locator#locator-clear) 564 + - [WebdriverIO `element.clearValue` API](https://webdriver.io/docs/api/element/clearValue) 565 + - [testing-library `clear` API](https://testing-library.com/docs/user-event/utility/#clear) 566 + 567 + ### userEvent.selectOptions 568 + 569 + - **Type:** `(element: Element, values: HTMLElement | HTMLElement[] | string | string[], options?: UserEventSelectOptions) => Promise<void>` 570 + 571 + The `userEvent.selectOptions` allows selecting a value in a `<select>` element. 572 + 573 + ::: warning 574 + If select element doesn't have [`multiple`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/select#attr-multiple) attribute, Vitest will select only the first element in the array. 575 + 576 + Unlike `@testing-library`, Vitest doesn't support [listbox](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/listbox_role) at the moment, but we plan to add support for it in the future. 577 + ::: 578 + 579 + ```ts 580 + import { userEvent } from '@vitest/browser/context' 581 + import { screen } from '@testing-library/dom' 582 + import '@testing-library/jest-dom' // adds support for "toHaveValue" 583 + 584 + test('clears input', () => { 585 + const select = screen.getByRole('select') 586 + 587 + await userEvent.selectOptions(select, 'Option 1') 588 + expect(select).toHaveValue('option-1') 589 + 590 + await userEvent.selectOptions(select, 'option-1') 591 + expect(select).toHaveValue('option-1') 592 + 593 + await userEvent.selectOptions(select, [ 594 + screen.getByRole('option', { name: 'Option 1' }), 595 + screen.getByRole('option', { name: 'Option 2' }), 596 + ]) 597 + expect(select).toHaveValue(['option-1', 'option-2']) 598 + }) 599 + ``` 600 + 601 + ::: warning 602 + `webdriverio` provider doesn't support selecting multiple elements because it doesn't provide API to do so. 603 + ::: 604 + 605 + References: 606 + 607 + - [Playwright `locator.selectOption` API](https://playwright.dev/docs/api/class-locator#locator-select-option) 608 + - [WebdriverIO `element.selectByIndex` API](https://webdriver.io/docs/api/element/selectByIndex) 609 + - [testing-library `selectOptions` API](https://testing-library.com/docs/user-event/utility/#-selectoptions-deselectoptions) 610 + 611 + ### userEvent.hover 612 + 613 + - **Type:** `(element: Element, options?: UserEventHoverOptions) => Promise<void>` 614 + 615 + This method moves the cursor position to selected element. Please refer to your provider's documentation for detailed explanaition about how this method works. 616 + 617 + ::: warning 618 + If you are using `webdriverio` provider, the cursor will move to the center of the element by default. 619 + 620 + If you are using `playwright` provider, the cursor moves to "some" visible point of the element. 621 + ::: 622 + 623 + ```ts 624 + import { userEvent } from '@vitest/browser/context' 625 + import { screen } from '@testing-library/dom' 626 + 627 + test('hovers logo element', () => { 628 + const logo = screen.getByRole('img', { name: /logo/ }) 629 + 630 + await userEvent.hover(logo) 631 + }) 632 + ``` 633 + 634 + References: 635 + 636 + - [Playwright `locator.hover` API](https://playwright.dev/docs/api/class-locator#locator-hover) 637 + - [WebdriverIO `element.moveTo` API](https://webdriver.io/docs/api/element/moveTo/) 638 + - [testing-library `hover` API](https://testing-library.com/docs/user-event/convenience/#hover) 639 + 640 + ### userEvent.unhover 641 + 642 + - **Type:** `(element: Element, options?: UserEventHoverOptions) => Promise<void>` 643 + 644 + This works the same as [`userEvent.hover`](#userevent-hover), but moves the cursor to the `document.body` element instead. 645 + 646 + ::: warning 647 + By default, the cursor position is in the center (in `webdriverio` provider) or in "some" visible place (in `playwright` provider) of the body element, so if the currently hovered element is already in the same position, this method will have no effect. 648 + ::: 649 + 650 + ```ts 651 + import { userEvent } from '@vitest/browser/context' 652 + import { screen } from '@testing-library/dom' 653 + 654 + test('unhover logo element', () => { 655 + const logo = screen.getByRole('img', { name: /logo/ }) 656 + 657 + await userEvent.unhover(logo) 658 + }) 659 + ``` 660 + 661 + References: 662 + 663 + - [Playwright `locator.hover` API](https://playwright.dev/docs/api/class-locator#locator-hover) 664 + - [WebdriverIO `element.moveTo` API](https://webdriver.io/docs/api/element/moveTo/) 665 + - [testing-library `hover` API](https://testing-library.com/docs/user-event/convenience/#hover) 666 + 667 + ### userEvent.dragAndDrop 668 + 669 + - **Type:** `(source: Element, target: Element, options?: UserEventDragAndDropOptions) => Promise<void>` 670 + 671 + Drags the source element on top of the target element. Don't forget that the `source` element has to have the `draggable` attribute set to `true`. 672 + 673 + ```ts 674 + import { userEvent } from '@vitest/browser/context' 675 + import { screen } from '@testing-library/dom' 676 + import '@testing-library/jest-dom' // adds support for "toHaveTextContent" 677 + 678 + test('drag and drop works', async () => { 679 + const source = screen.getByRole('img', { name: /logo/ }) 680 + const target = screen.getByTestId('logo-target') 681 + 682 + await userEvent.dragAndDrop(source, target) 683 + 684 + expect(target).toHaveTextContent('Logo is processed') 685 + }) 686 + ``` 687 + 688 + ::: warning 689 + This API is not supported by the `preview` provider. 690 + ::: 691 + 692 + References: 693 + 694 + - [Playwright `frame.dragAndDrop` API](https://playwright.dev/docs/api/class-frame#frame-drag-and-drop) 695 + - [WebdriverIO `element.dragAndDrop` API](https://webdriver.io/docs/api/element/dragAndDrop/) 246 696 247 697 ## Commands 248 698 ··· 276 726 await removeFile(file) 277 727 }) 278 728 ``` 279 - 280 - ### Keyboard Interactions 281 - 282 - Vitest also implements Web Test Runner's [`sendKeys` API](https://modern-web.dev/docs/test-runner/commands/#send-keys). It accepts an object with a single property: 283 - 284 - - `type` - types a sequence of characters, this API _is not_ affected by modifier keys, so having `Shift` won't make letters uppercase 285 - - `press` - presses a single key, this API _is_ affected by modifier keys, so having `Shift` will make subsequent characters uppercase 286 - - `up` - holds down a key (supported only with `playwright` provider) 287 - - `down` - releases a key (supported only with `playwright` provider) 288 - 289 - ```ts 290 - interface TypePayload { type: string } 291 - interface PressPayload { press: string } 292 - interface DownPayload { down: string } 293 - interface UpPayload { up: string } 294 - 295 - type SendKeysPayload = TypePayload | PressPayload | DownPayload | UpPayload 296 - 297 - declare function sendKeys(payload: SendKeysPayload): Promise<void> 298 - ``` 299 - 300 - This is just a simple wrapper around providers APIs. Please refer to their respective documentations for details: 301 - 302 - - [Playwright Keyboard API](https://playwright.dev/docs/api/class-keyboard) 303 - - [Webdriver Keyboard API](https://webdriver.io/docs/api/browser/keys/) 304 729 305 730 ## Custom Commands 306 731 ··· 370 795 Vitest exposes several `playwright` specific properties on the command context. 371 796 372 797 - `page` references the full page that contains the test iframe. This is the orchestrator HTML and you most likely shouldn't touch it to not break things. 373 - - `tester` is the iframe locator. The API is pretty limited here, but you can chain it further to access your HTML elements. 374 - - `body` is the iframe's `body` locator that exposes more Playwright APIs. 798 + - `frame` is the tester [iframe instance](https://playwright.dev/docs/api/class-frame). It has a simillar API to the page, but it doesn't support certain methods. 799 + - `context` refers to the unique [BrowserContext](https://playwright.dev/docs/api/class-browsercontext). 375 800 376 801 ```ts 377 802 import { defineCommand } from '@vitest/browser' 378 803 379 804 export const myCommand = defineCommand(async (ctx, arg1, arg2) => { 380 805 if (ctx.provider.name === 'playwright') { 381 - const element = await ctx.tester.findByRole('alert') 806 + const element = await ctx.frame.findByRole('alert') 382 807 const screenshot = await element.screenshot() 383 808 // do something with the screenshot 384 809 return difference ··· 387 812 ``` 388 813 389 814 ::: tip 390 - If you are using TypeScript, don't forget to add `@vitest/browser/providers/playwright` to your `tsconfig` "compilerOptions.types" field to get autocompletion: 815 + If you are using TypeScript, don't forget to add `@vitest/browser/providers/playwright` to your `tsconfig` "compilerOptions.types" field to get autocompletion in the config and on `userEvent` and `page` options: 391 816 392 817 ```json 393 818 {
+120 -22
packages/browser/context.d.ts
··· 19 19 flag?: string | number 20 20 } 21 21 22 - export interface TypePayload { 23 - type: string 24 - } 25 - export interface PressPayload { 26 - press: string 27 - } 28 - export interface DownPayload { 29 - down: string 30 - } 31 - export interface UpPayload { 32 - up: string 33 - } 34 - 35 - export type SendKeysPayload = 36 - | TypePayload 37 - | PressPayload 38 - | DownPayload 39 - | UpPayload 40 - 41 22 export interface ScreenshotOptions { 42 23 element?: Element 43 24 /** ··· 57 38 options?: BufferEncoding | (FsOptions & { mode?: number | string }) 58 39 ) => Promise<void> 59 40 removeFile: (path: string) => Promise<void> 60 - sendKeys: (payload: SendKeysPayload) => Promise<void> 61 41 } 62 42 63 43 export interface UserEvent { 44 + setup: () => UserEvent 64 45 /** 65 46 * Click on an element. Uses provider's API under the hood and supports all its options. 66 47 * @see {@link https://playwright.dev/docs/api/class-locator#locator-click} Playwright API ··· 68 49 * @see {@link https://testing-library.com/docs/user-event/convenience/#click} testing-library API 69 50 */ 70 51 click: (element: Element, options?: UserEventClickOptions) => Promise<void> 52 + /** 53 + * Triggers a double click event on an element. Uses provider's API under the hood. 54 + * @see {@link https://playwright.dev/docs/api/class-locator#locator-dblclick} Playwright API 55 + * @see {@link https://webdriver.io/docs/api/element/doubleClick/} WebdriverIO API 56 + * @see {@link https://testing-library.com/docs/user-event/convenience/#dblClick} testing-library API 57 + */ 58 + dblClick: (element: Element, options?: UserEventDoubleClickOptions) => Promise<void> 59 + /** 60 + * Choose one or more values from a select element. Uses provider's API under the hood. 61 + * If select doesn't have `multiple` attribute, only the first value will be selected. 62 + * @example 63 + * await userEvent.selectOptions(select, 'Option 1') 64 + * expect(select).toHaveValue('option-1') 65 + * 66 + * await userEvent.selectOptions(select, 'option-1') 67 + * expect(select).toHaveValue('option-1') 68 + * 69 + * await userEvent.selectOptions(select, [ 70 + * screen.getByRole('option', { name: 'Option 1' }), 71 + * screen.getByRole('option', { name: 'Option 2' }), 72 + * ]) 73 + * expect(select).toHaveValue(['option-1', 'option-2']) 74 + * @see {@link https://playwright.dev/docs/api/class-locator#locator-select-option} Playwright API 75 + * @see {@link https://webdriver.io/docs/api/element/doubleClick/} WebdriverIO API 76 + * @see {@link https://testing-library.com/docs/user-event/utility/#-selectoptions-deselectoptions} testing-library API 77 + */ 78 + selectOptions: ( 79 + element: Element, 80 + values: HTMLElement | HTMLElement[] | string | string[], 81 + options?: UserEventSelectOptions, 82 + ) => Promise<void> 83 + /** 84 + * Type text on the keyboard. If any input is focused, it will receive the text, 85 + * otherwise it will be typed on the document. Uses provider's API under the hood. 86 + * **Supports** [user-event `keyboard` syntax](https://testing-library.com/docs/user-event/keyboard) (e.g., `{Shift}`) even with `playwright` and `webdriverio` providers. 87 + * @example 88 + * await userEvent.keyboard('foo') // translates to: f, o, o 89 + * await userEvent.keyboard('{{a[[') // translates to: {, a, [ 90 + * await userEvent.keyboard('{Shift}{f}{o}{o}') // translates to: Shift, f, o, o 91 + * @see {@link https://playwright.dev/docs/api/class-locator#locator-press} Playwright API 92 + * @see {@link https://webdriver.io/docs/api/browser/keys} WebdriverIO API 93 + * @see {@link https://testing-library.com/docs/user-event/keyboard} testing-library API 94 + */ 95 + keyboard: (text: string) => Promise<void> 96 + /** 97 + * Types text into an element. Uses provider's API under the hood. 98 + * **Supports** [user-event `keyboard` syntax](https://testing-library.com/docs/user-event/keyboard) (e.g., `{Shift}`) even with `playwright` and `webdriverio` providers. 99 + * @example 100 + * await userEvent.type(input, 'foo') // translates to: f, o, o 101 + * await userEvent.type(input, '{{a[[') // translates to: {, a, [ 102 + * await userEvent.type(input, '{Shift}{f}{o}{o}') // translates to: Shift, f, o, o 103 + * @see {@link https://playwright.dev/docs/api/class-locator#locator-press} Playwright API 104 + * @see {@link https://webdriver.io/docs/api/browser/action#key-input-source} WebdriverIO API 105 + * @see {@link https://testing-library.com/docs/user-event/utility/#type} testing-library API 106 + */ 107 + type: (element: Element, text: string, options?: UserEventTypeOptions) => Promise<void> 108 + /** 109 + * Removes all text from an element. Uses provider's API under the hood. 110 + * @see {@link https://playwright.dev/docs/api/class-locator#locator-clear} Playwright API 111 + * @see {@link https://webdriver.io/docs/api/element/clearValue} WebdriverIO API 112 + * @see {@link https://testing-library.com/docs/user-event/utility/#clear} testing-library API 113 + */ 114 + clear: (element: Element) => Promise<void> 115 + /** 116 + * Sends a `Tab` key event. Uses provider's API under the hood. 117 + * @see {@link https://playwright.dev/docs/api/class-locator#locator-press} Playwright API 118 + * @see {@link https://webdriver.io/docs/api/element/keys} WebdriverIO API 119 + * @see {@link https://testing-library.com/docs/user-event/convenience/#tab} testing-library API 120 + */ 121 + tab: (options?: UserEventTabOptions) => Promise<void> 122 + /** 123 + * Hovers over an element. Uses provider's API under the hood. 124 + * @see {@link https://playwright.dev/docs/api/class-locator#locator-hover} Playwright API 125 + * @see {@link https://webdriver.io/docs/api/element/moveTo/} WebdriverIO API 126 + * @see {@link https://testing-library.com/docs/user-event/convenience/#hover} testing-library API 127 + */ 128 + hover: (element: Element, options?: UserEventHoverOptions) => Promise<void> 129 + /** 130 + * Moves cursor position to the body element. Uses provider's API under the hood. 131 + * By default, the cursor position is in the center (in webdriverio) or in some visible place (in playwright) 132 + * of the body element, so if the current element is already there, this will have no effect. 133 + * @see {@link https://playwright.dev/docs/api/class-locator#locator-hover} Playwright API 134 + * @see {@link https://webdriver.io/docs/api/element/moveTo/} WebdriverIO API 135 + * @see {@link https://testing-library.com/docs/user-event/convenience/#hover} testing-library API 136 + */ 137 + unhover: (element: Element, options?: UserEventHoverOptions) => Promise<void> 138 + /** 139 + * Fills an input element with text. This will remove any existing text in the input before typing the new text. 140 + * Uses provider's API under the hood. 141 + * 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}`). 142 + * @example 143 + * await userEvent.fill(input, 'foo') // translates to: f, o, o 144 + * await userEvent.fill(input, '{{a[[') // translates to: {, {, a, [, [ 145 + * await userEvent.fill(input, '{Shift}') // translates to: {, S, h, i, f, t, } 146 + * @see {@link https://playwright.dev/docs/api/class-locator#locator-fill} Playwright API 147 + * @see {@link https://webdriver.io/docs/api/element/setValue} WebdriverIO API 148 + * @see {@link https://testing-library.com/docs/user-event/utility/#type} testing-library API 149 + */ 150 + fill: (element: Element, text: string, options?: UserEventFillOptions) => Promise<void> 151 + /** 152 + * Drags a source element on top of the target element. This API is not supported by "preview" provider. 153 + * @see {@link https://playwright.dev/docs/api/class-frame#frame-drag-and-drop} Playwright API 154 + * @see {@link https://webdriver.io/docs/api/element/dragAndDrop/} WebdriverIO API 155 + */ 156 + dragAndDrop: (source: Element, target: Element, options?: UserEventDragAndDropOptions) => Promise<void> 71 157 } 72 158 73 - export interface UserEventClickOptions { 74 - [key: string]: any 159 + export interface UserEventFillOptions {} 160 + export interface UserEventHoverOptions {} 161 + export interface UserEventSelectOptions {} 162 + export interface UserEventClickOptions {} 163 + export interface UserEventDoubleClickOptions {} 164 + export interface UserEventDragAndDropOptions {} 165 + 166 + export interface UserEventTabOptions { 167 + shift?: boolean 168 + } 169 + 170 + export interface UserEventTypeOptions { 171 + skipClick?: boolean 172 + skipAutoClose?: boolean 75 173 } 76 174 77 175 type Platform =
+23 -4
packages/browser/providers/playwright.d.ts
··· 1 1 import type { 2 + BrowserContext, 2 3 BrowserContextOptions, 3 - FrameLocator, 4 + Frame, 4 5 LaunchOptions, 5 - Locator, 6 6 Page, 7 7 } from 'playwright' 8 8 ··· 17 17 18 18 export interface BrowserCommandContext { 19 19 page: Page 20 - tester: FrameLocator 21 - body: Locator 20 + frame: Frame 21 + context: BrowserContext 22 22 } 23 + } 24 + 25 + type PWHoverOptions = Parameters<Page['hover']>[1] 26 + type PWClickOptions = Parameters<Page['click']>[1] 27 + type PWDoubleClickOptions = Parameters<Page['dblclick']>[1] 28 + type PWFillOptions = Parameters<Page['fill']>[2] 29 + type PWScreenshotOptions = Parameters<Page['screenshot']>[0] 30 + type PWSelectOptions = Parameters<Page['selectOption']>[2] 31 + type PWDragAndDropOptions = Parameters<Page['dragAndDrop']>[2] 32 + 33 + declare module '@vitest/browser/context' { 34 + export interface UserEventHoverOptions extends PWHoverOptions {} 35 + export interface UserEventClickOptions extends PWClickOptions {} 36 + export interface UserEventDoubleClickOptions extends PWDoubleClickOptions {} 37 + export interface UserEventFillOptions extends PWFillOptions {} 38 + export interface UserEventSelectOptions extends PWSelectOptions {} 39 + export interface UserEventDragOptions extends UserEventDragAndDropOptions {} 40 + 41 + export interface ScreenshotOptions extends PWScreenshotOptions {} 23 42 }
+1 -3
packages/utils/src/source-map.ts
··· 32 32 '/node_modules/tinypool/', 33 33 '/node_modules/tinyspy/', 34 34 // browser related deps 35 - '/deps/chai.js', 36 - '/deps/vitest___chai.js', 37 - '/deps/p-limit.js', 35 + '/deps/', 38 36 /node:\w+/, 39 37 /__vitest_test__/, 40 38 /__vitest_browser__/,
-2
packages/vitest/src/defaults.ts
··· 106 106 mockReset: false, 107 107 include: defaultInclude, 108 108 exclude: defaultExclude, 109 - testTimeout: 5000, 110 - hookTimeout: 10000, 111 109 teardownTimeout: 10000, 112 110 forceRerunTriggers: ['**/package.json/**', '**/{vitest,vite}.config.*/**'], 113 111 update: false,
+2 -2
test/browser/specs/runner.test.ts
··· 23 23 console.error(stderr) 24 24 }) 25 25 26 - expect(browserResultJson.testResults).toHaveLength(16) 27 - expect(passedTests).toHaveLength(14) 26 + expect(browserResultJson.testResults).toHaveLength(17) 27 + expect(passedTests).toHaveLength(15) 28 28 expect(failedTests).toHaveLength(2) 29 29 30 30 expect(stderr).not.toContain('has been externalized for browser compatibility')
+2 -91
test/browser/test/commands.test.ts
··· 1 - import { commands, server } from '@vitest/browser/context' 1 + import { server } from '@vitest/browser/context' 2 2 import { expect, it } from 'vitest' 3 3 4 - const { readFile, writeFile, removeFile, sendKeys, myCustomCommand } = server.commands 4 + const { readFile, writeFile, removeFile, myCustomCommand } = server.commands 5 5 6 6 it('can manipulate files', async () => { 7 7 const file = './test.txt' ··· 40 40 expect(err.message).toMatch('test/browser/test/test.txt') 41 41 } 42 42 } 43 - }) 44 - 45 - // Test Cases from https://modern-web.dev/docs/test-runner/commands/#writing-and-reading-files 46 - it('natively types into an input', async () => { 47 - const keys = 'abc123' 48 - const input = document.createElement('input') 49 - document.body.append(input) 50 - input.focus() 51 - 52 - await commands.sendKeys({ 53 - type: keys, 54 - }) 55 - 56 - expect(input.value).to.equal(keys) 57 - input.remove() 58 - }) 59 - 60 - it('natively presses `Tab`', async () => { 61 - const input1 = document.createElement('input') 62 - const input2 = document.createElement('input') 63 - document.body.append(input1, input2) 64 - input1.focus() 65 - expect(document.activeElement).to.equal(input1) 66 - 67 - await commands.sendKeys({ 68 - press: 'Tab', 69 - }) 70 - 71 - expect(document.activeElement).to.equal(input2) 72 - input1.remove() 73 - input2.remove() 74 - }) 75 - 76 - it.skipIf(server.provider === 'webdriverio')('natively presses `Shift+Tab`', async () => { 77 - const input1 = document.createElement('input') 78 - const input2 = document.createElement('input') 79 - document.body.append(input1, input2) 80 - input2.focus() 81 - expect(document.activeElement).to.equal(input2) 82 - 83 - await sendKeys({ 84 - down: 'Shift', 85 - }) 86 - await sendKeys({ 87 - press: 'Tab', 88 - }) 89 - await sendKeys({ 90 - up: 'Shift', 91 - }) 92 - 93 - expect(document.activeElement).to.equal(input1) 94 - input1.remove() 95 - input2.remove() 96 - }) 97 - 98 - it.skipIf(server.provider === 'webdriverio')('natively holds and then releases a key', async () => { 99 - const input = document.createElement('input') 100 - document.body.append(input) 101 - input.focus() 102 - 103 - await sendKeys({ 104 - down: 'Shift', 105 - }) 106 - // Note that pressed modifier keys are only respected when using `press` or 107 - // `down`, and only when using the `Key...` variants. 108 - await sendKeys({ 109 - press: 'KeyA', 110 - }) 111 - await sendKeys({ 112 - press: 'KeyB', 113 - }) 114 - await sendKeys({ 115 - press: 'KeyC', 116 - }) 117 - await sendKeys({ 118 - up: 'Shift', 119 - }) 120 - await sendKeys({ 121 - press: 'KeyA', 122 - }) 123 - await sendKeys({ 124 - press: 'KeyB', 125 - }) 126 - await sendKeys({ 127 - press: 'KeyC', 128 - }) 129 - 130 - expect(input.value).to.equal('ABCabc') 131 - input.remove() 132 43 }) 133 44 134 45 it('can run custom commands', async () => {
+3 -1
test/browser/test/dom.test.ts
··· 15 15 const screenshotPath = await page.screenshot({ 16 16 element: wrapper, 17 17 }) 18 - expect(screenshotPath).toMatch(/__screenshots__\/dom.test.ts\/dom-related-activity-renders-div-1.png/) 18 + expect(screenshotPath).toMatch( 19 + /__screenshots__\/dom.test.ts\/dom-related-activity-renders-div-1.png/, 20 + ) 19 21 }) 20 22 })
+556
test/browser/test/userEvent.test.ts
··· 1 + import { beforeEach, describe, expect, test, vi } from 'vitest' 2 + import { server, userEvent } from '@vitest/browser/context' 3 + import '../src/button.css' 4 + 5 + beforeEach(() => { 6 + // clear body 7 + document.body.replaceChildren() 8 + }) 9 + 10 + describe('userEvent.click', () => { 11 + test('correctly clicks a button', async () => { 12 + const button = document.createElement('button') 13 + button.textContent = 'Click me' 14 + document.body.appendChild(button) 15 + const onClick = vi.fn() 16 + const dblClick = vi.fn() 17 + button.addEventListener('click', onClick) 18 + 19 + await userEvent.click(button) 20 + 21 + expect(onClick).toHaveBeenCalled() 22 + expect(dblClick).not.toHaveBeenCalled() 23 + }) 24 + 25 + test('correctly doesn\'t click on a disabled button', async () => { 26 + const button = document.createElement('button') 27 + button.textContent = 'Click me' 28 + button.disabled = true 29 + document.body.appendChild(button) 30 + const onClick = vi.fn() 31 + button.addEventListener('click', onClick) 32 + 33 + await userEvent.click(button, { 34 + // playwright requires force: true to click on a disabled button 35 + force: true, 36 + }) 37 + 38 + expect(onClick).not.toHaveBeenCalled() 39 + }) 40 + }) 41 + 42 + describe('userEvent.dblClick', () => { 43 + test('correctly clicks a button', async () => { 44 + const button = document.createElement('button') 45 + button.textContent = 'Click me' 46 + document.body.appendChild(button) 47 + const onClick = vi.fn() 48 + const dblClick = vi.fn() 49 + button.addEventListener('click', onClick) 50 + button.addEventListener('dblclick', dblClick) 51 + 52 + await userEvent.dblClick(button) 53 + 54 + expect(onClick).toHaveBeenCalledTimes(2) 55 + expect(dblClick).toHaveBeenCalledTimes(1) 56 + }) 57 + 58 + test('correctly doesn\'t click on a disabled button', async () => { 59 + const button = document.createElement('button') 60 + button.textContent = 'Click me' 61 + button.disabled = true 62 + document.body.appendChild(button) 63 + const onClick = vi.fn() 64 + const dblClick = vi.fn() 65 + button.addEventListener('click', onClick) 66 + button.addEventListener('dblclick', dblClick) 67 + 68 + await userEvent.dblClick(button, { 69 + // playwright requires force: true to click on a disabled button 70 + force: true, 71 + }) 72 + 73 + expect(onClick).not.toHaveBeenCalled() 74 + expect(dblClick).not.toHaveBeenCalled() 75 + }) 76 + }) 77 + 78 + describe('userEvent.hover, userEvent.unhover', () => { 79 + test('hover works correctly', async () => { 80 + const target = document.createElement('div') 81 + target.style.width = '100px' 82 + target.style.height = '100px' 83 + 84 + let mouseEntered = false 85 + let pointerEntered = false 86 + target.addEventListener('mouseover', () => { 87 + mouseEntered = true 88 + }) 89 + target.addEventListener('pointerenter', () => { 90 + pointerEntered = true 91 + }) 92 + target.addEventListener('pointerleave', () => { 93 + pointerEntered = false 94 + }) 95 + target.addEventListener('mouseout', () => { 96 + mouseEntered = false 97 + }) 98 + 99 + document.body.appendChild(target) 100 + 101 + await userEvent.hover(target) 102 + 103 + expect(pointerEntered).toBe(true) 104 + expect(mouseEntered).toBe(true) 105 + 106 + await userEvent.unhover(target) 107 + 108 + expect(pointerEntered).toBe(false) 109 + expect(mouseEntered).toBe(false) 110 + }) 111 + }) 112 + 113 + const inputLike = [ 114 + () => { 115 + const input = document.createElement('input') 116 + input.type = 'text' 117 + input.placeholder = 'Type here' 118 + document.body.appendChild(input) 119 + return input 120 + }, 121 + () => { 122 + const input = document.createElement('textarea') 123 + input.placeholder = 'Type here' 124 + document.body.appendChild(input) 125 + return input 126 + }, 127 + () => { 128 + const contentEditable = document.createElement('div') 129 + contentEditable.contentEditable = 'true' 130 + document.body.appendChild(contentEditable) 131 + return contentEditable 132 + }, 133 + ] 134 + 135 + describe.each(inputLike)('userEvent.type', (getElement) => { 136 + test('types into an input', async () => { 137 + const { input, keydown, keyup, value } = createTextInput() 138 + 139 + await userEvent.type(input, 'Hello World!') 140 + expect(value()).toBe('Hello World!') 141 + expect(keydown).toEqual([ 142 + 'H', 143 + 'e', 144 + 'l', 145 + 'l', 146 + 'o', 147 + ' ', 148 + 'W', 149 + 'o', 150 + 'r', 151 + 'l', 152 + 'd', 153 + '!', 154 + ]) 155 + expect(keyup).toEqual([ 156 + 'H', 157 + 'e', 158 + 'l', 159 + 'l', 160 + 'o', 161 + ' ', 162 + 'W', 163 + 'o', 164 + 'r', 165 + 'l', 166 + 'd', 167 + '!', 168 + ]) 169 + keydown.length = 0 170 + keyup.length = 0 171 + 172 + await userEvent.type(input, '{a>3}4') 173 + expect(value()).toBe('Hello World!aaa4') 174 + 175 + await userEvent.type(input, '{backspace}') 176 + expect(value()).toBe('Hello World!aaa') 177 + 178 + // doesn't affect the input value 179 + await userEvent.type(input, '{/a}') 180 + expect(value()).toBe('Hello World!aaa') 181 + 182 + expect(keydown).toEqual([ 183 + 'a', 184 + 'a', 185 + 'a', 186 + '4', 187 + 'Backspace', 188 + ]) 189 + 190 + await userEvent.type(input, '{Shift}b{/Shift}') 191 + 192 + // this follow userEvent logic 193 + expect(value()).toBe('Hello World!aaab') 194 + 195 + await userEvent.clear(input) 196 + 197 + expect(value()).toBe('') 198 + }) 199 + 200 + test('repeating without manual up works correctly', async () => { 201 + const { input, keydown, keyup, value } = createTextInput() 202 + 203 + await userEvent.type(input, '{a>3}4') 204 + expect(value()).toBe('aaa4') 205 + 206 + expect(keydown).toEqual([ 207 + 'a', 208 + 'a', 209 + 'a', 210 + '4', 211 + ]) 212 + // keyup is released at the end by userEvent 213 + expect(keyup).toEqual([ 214 + '4', 215 + 'a', 216 + ]) 217 + }) 218 + 219 + test('repeating with manual up works correctly', async () => { 220 + const { input, keydown, keyup, value } = createTextInput() 221 + 222 + await userEvent.type(input, '{a>3/}4') 223 + expect(value()).toBe('aaa4') 224 + 225 + expect(keydown).toEqual([ 226 + 'a', 227 + 'a', 228 + 'a', 229 + '4', 230 + ]) 231 + // keyup is released with "/" syntax 232 + expect(keyup).toEqual([ 233 + 'a', 234 + '4', 235 + ]) 236 + }) 237 + 238 + test('repeating with disabled up works correctly', async () => { 239 + const { input, keydown, keyup, value } = createTextInput() 240 + 241 + await userEvent.type(input, '{a>3}4', { 242 + skipAutoClose: true, 243 + }) 244 + expect(value()).toBe('aaa4') 245 + 246 + expect(keydown).toEqual([ 247 + 'a', 248 + 'a', 249 + 'a', 250 + '4', 251 + ]) 252 + // keyup is not released at the end by userEvent 253 + expect(keyup).toEqual([ 254 + '4', 255 + ]) 256 + }) 257 + 258 + // strangly enough, original userEvent doesn't support this, 259 + // but we can implement it 260 + test.skipIf(server.provider === 'preview')('selectall works correctly', async () => { 261 + const input = document.createElement('input') 262 + input.type = 'text' 263 + input.placeholder = 'Type here' 264 + document.body.appendChild(input) 265 + await userEvent.type(input, 'Hello World!') 266 + await userEvent.type(input, '{selectall}') 267 + await userEvent.type(input, '{backspace}') 268 + expect(input.value).toBe('') 269 + }) 270 + 271 + function createTextInput() { 272 + const input = getElement() 273 + const keydown: string[] = [] 274 + const keyup: string[] = [] 275 + input.addEventListener('keydown', (event: KeyboardEvent) => { 276 + keydown.push(event.key) 277 + }) 278 + input.addEventListener('keyup', (event: KeyboardEvent) => { 279 + keyup.push(event.key) 280 + }) 281 + document.body.appendChild(input) 282 + return { 283 + input, 284 + keydown, 285 + keyup, 286 + value() { 287 + if ('value' in input) { 288 + return input.value 289 + } 290 + return input.textContent 291 + }, 292 + } 293 + } 294 + }) 295 + 296 + describe('userEvent.tab', () => { 297 + test('tab correctly switches focus', async () => { 298 + const input1 = document.createElement('input') 299 + input1.type = 'text' 300 + const input2 = document.createElement('input') 301 + input2.type = 'text' 302 + document.body.appendChild(input1) 303 + document.body.appendChild(input2) 304 + 305 + input1.focus() 306 + await userEvent.tab() 307 + 308 + expect(document.activeElement).toBe(input2) 309 + 310 + await userEvent.tab({ shift: true }) 311 + 312 + expect(document.activeElement).toBe(input1) 313 + }) 314 + }) 315 + 316 + describe.each(inputLike)('userEvent.fill', async (getInput) => { 317 + test('correctly fills the input value', async () => { 318 + const input = getInput() 319 + function value() { 320 + if ('value' in input) { 321 + return input.value 322 + } 323 + return input.textContent 324 + } 325 + 326 + await userEvent.fill(input, 'Hello World!') 327 + expect(value()).toBe('Hello World!') 328 + 329 + await userEvent.fill(input, 'Another Value') 330 + expect(value()).toBe('Another Value') 331 + }) 332 + }) 333 + 334 + describe('userEvent.keyboard', async () => { 335 + test('standalone keyboard works correctly with a body in focus', async () => { 336 + const pressed: string[] = [] 337 + document.addEventListener('keydown', (event) => { 338 + pressed.push(event.key) 339 + }) 340 + expect(document.activeElement).toBe(document.body) 341 + await userEvent.keyboard('Hello') 342 + expect(pressed).toEqual([ 343 + 'H', 344 + 'e', 345 + 'l', 346 + 'l', 347 + 'o', 348 + ]) 349 + }) 350 + 351 + test('standalone keyboard works correctly with an active non-input', async () => { 352 + const documentKeydown: string[] = [] 353 + const divKeydown: string[] = [] 354 + const div = document.createElement('div') 355 + div.style.width = '100px' 356 + div.style.height = '100px' 357 + div.tabIndex = 0 358 + div.addEventListener('keydown', (event) => { 359 + divKeydown.push(event.key) 360 + event.stopPropagation() 361 + event.preventDefault() 362 + }) 363 + document.body.appendChild(div) 364 + document.addEventListener('keydown', (event) => { 365 + documentKeydown.push(event.key) 366 + }) 367 + expect(document.activeElement).toBe(document.body) 368 + div.focus() 369 + expect(document.activeElement).toBe(div) 370 + await userEvent.keyboard('Hello{backspace}') 371 + expect(documentKeydown).toEqual([]) 372 + expect(divKeydown).toEqual([ 373 + 'H', 374 + 'e', 375 + 'l', 376 + 'l', 377 + 'o', 378 + 'Backspace', 379 + ]) 380 + }) 381 + 382 + test('standalone keyboard works correctly with active input', async () => { 383 + const documentKeydown: string[] = [] 384 + const inputKeydown: string[] = [] 385 + const input = document.createElement('input') 386 + input.addEventListener('keydown', (event) => { 387 + inputKeydown.push(event.key) 388 + event.stopPropagation() 389 + }) 390 + document.body.appendChild(input) 391 + document.addEventListener('keydown', (event) => { 392 + documentKeydown.push(event.key) 393 + }) 394 + expect(document.activeElement).toBe(document.body) 395 + input.focus() 396 + expect(document.activeElement).toBe(input) 397 + 398 + await userEvent.keyboard('Hello{backspace}') 399 + 400 + expect(input.value).toBe('Hell') 401 + expect(documentKeydown).toEqual([]) 402 + expect(inputKeydown).toEqual([ 403 + 'H', 404 + 'e', 405 + 'l', 406 + 'l', 407 + 'o', 408 + 'Backspace', 409 + ]) 410 + }) 411 + }) 412 + 413 + describe.skipIf(server.provider === 'preview')('userEvent.dragAndDrop', async () => { 414 + test('drag and drop works', async () => { 415 + const source = document.createElement('div') 416 + source.textContent = 'Drag me' 417 + source.style.width = '100px' 418 + source.style.height = '100px' 419 + source.style.background = 'red' 420 + source.draggable = true 421 + 422 + const dragstart = vi.fn() 423 + 424 + source.addEventListener('dragstart', dragstart) 425 + 426 + const target = document.createElement('div') 427 + target.style.width = '100px' 428 + target.style.height = '100px' 429 + target.style.background = 'blue' 430 + 431 + const dragover = vi.fn(() => { 432 + target.textContent = 'Dropped' 433 + }) 434 + 435 + target.addEventListener('dragover', dragover) 436 + 437 + document.body.appendChild(source) 438 + document.body.appendChild(target) 439 + 440 + expect(target.textContent).toBe('') 441 + 442 + await userEvent.dragAndDrop(source, target) 443 + 444 + await expect.poll(() => dragstart).toHaveBeenCalled() 445 + await expect.poll(() => dragover).toHaveBeenCalled() 446 + 447 + expect(target.textContent).toBe('Dropped') 448 + }) 449 + }) 450 + 451 + describe.each([ 452 + [ 453 + 'select', 454 + function createSelect() { 455 + const select = document.createElement('select') 456 + const option1 = document.createElement('option') 457 + option1.value = '1' 458 + option1.textContent = 'Option 1' 459 + const option2 = document.createElement('option') 460 + option2.value = '2' 461 + option2.textContent = 'Option 2' 462 + select.appendChild(option1) 463 + select.appendChild(option2) 464 + document.body.appendChild(select) 465 + 466 + return { select, options: [option1, option2] } 467 + }, 468 + ], 469 + // TODO: support listbox 470 + // [ 471 + // 'listbox', 472 + // function createSelect() { 473 + // const select = document.createElement('div') 474 + // select.setAttribute('role', 'listbox') 475 + // const option1 = document.createElement('div') 476 + // option1.setAttribute('role', 'option') 477 + // // option1.value = '1' 478 + // option1.textContent = 'Option 1' 479 + // const option2 = document.createElement('div') 480 + // option2.setAttribute('role', 'option') 481 + // // option2.value = '2' 482 + // option2.textContent = 'Option 2' 483 + // select.appendChild(option1) 484 + // select.appendChild(option2) 485 + // document.body.appendChild(select) 486 + 487 + // return { select, options: [option1, option2] } 488 + // }, 489 + // ], 490 + ])('selectOptions in "%s" works correctly', (_, createSelect) => { 491 + test('can select a single primitive value', async () => { 492 + const { select } = createSelect() 493 + 494 + await userEvent.selectOptions(select, '2') 495 + 496 + expect(select.value).toBe('2') 497 + }) 498 + 499 + test('can select a single primitive value by label', async () => { 500 + const { select } = createSelect() 501 + 502 + await userEvent.selectOptions(select, 'Option 2') 503 + 504 + expect(select.value).toBe('2') 505 + }) 506 + 507 + test('can select a single element value', async () => { 508 + const { select, options } = createSelect() 509 + 510 + await userEvent.selectOptions(select, options[1]) 511 + 512 + expect(select.value).toBe('2') 513 + }) 514 + 515 + // webdriverio doesn't support selecting multiple values 516 + describe.skipIf(server.provider === 'webdriverio')('multiple values', () => { 517 + test('can select multiple values', async () => { 518 + const { select, options } = createSelect() 519 + select.multiple = true 520 + 521 + await userEvent.selectOptions(select, ['1', '2']) 522 + 523 + const selected = document.querySelectorAll('option:checked') 524 + 525 + expect(selected).toHaveLength(2) 526 + expect(selected[0]).toBe(options[0]) 527 + expect(selected[1]).toBe(options[1]) 528 + }) 529 + 530 + test('can select multiple values by label', async () => { 531 + const { select, options } = createSelect() 532 + select.multiple = true 533 + 534 + await userEvent.selectOptions(select, ['Option 1', 'Option 2']) 535 + 536 + const selected = document.querySelectorAll('option:checked') 537 + 538 + expect(selected).toHaveLength(2) 539 + expect(selected[0]).toBe(options[0]) 540 + expect(selected[1]).toBe(options[1]) 541 + }) 542 + 543 + test('can select multiple element values', async () => { 544 + const { select, options } = createSelect() 545 + select.multiple = true 546 + 547 + await userEvent.selectOptions(select, options) 548 + 549 + const selected = document.querySelectorAll('option:checked') 550 + 551 + expect(selected).toHaveLength(2) 552 + expect(selected[0]).toBe(options[0]) 553 + expect(selected[1]).toBe(options[1]) 554 + }) 555 + }) 556 + })
+97 -5
packages/browser/src/client/context.ts
··· 1 1 import type { Task, WorkerGlobalState } from 'vitest' 2 - import type { 3 - BrowserPage, 4 - UserEvent, 5 - UserEventClickOptions, 6 - } from '../../context' 2 + import type { BrowserPage, UserEvent, UserEventClickOptions, UserEventTabOptions, UserEventTypeOptions } from '../../context' 7 3 import type { BrowserRPC } from './client' 8 4 import type { BrowserRunnerState } from './utils' 9 5 ··· 62 58 return rpc().triggerCommand<T>(contextId, command, filepath(), args) 63 59 } 64 60 61 + const provider = runner().provider 62 + 65 63 export const userEvent: UserEvent = { 64 + // TODO: actually setup userEvent with config options 65 + setup() { 66 + return userEvent 67 + }, 66 68 click(element: Element, options: UserEventClickOptions = {}) { 67 69 const xpath = convertElementToXPath(element) 68 70 return triggerCommand('__vitest_click', xpath, options) 69 71 }, 72 + dblClick(element: Element, options: UserEventClickOptions = {}) { 73 + const xpath = convertElementToXPath(element) 74 + return triggerCommand('__vitest_dblClick', xpath, options) 75 + }, 76 + selectOptions(element, value) { 77 + const values = provider === 'webdriverio' 78 + ? getWebdriverioSelectOptions(element, value) 79 + : getSimpleSelectOptions(element, value) 80 + return triggerCommand('__vitest_selectOptions', convertElementToXPath(element), values) 81 + }, 82 + type(element: Element, text: string, options: UserEventTypeOptions = {}) { 83 + const xpath = convertElementToXPath(element) 84 + return triggerCommand('__vitest_type', xpath, text, options) 85 + }, 86 + clear(element: Element) { 87 + const xpath = convertElementToXPath(element) 88 + return triggerCommand('__vitest_clear', xpath) 89 + }, 90 + tab(options: UserEventTabOptions = {}) { 91 + return triggerCommand('__vitest_tab', options) 92 + }, 93 + keyboard(text: string) { 94 + return triggerCommand('__vitest_keyboard', text) 95 + }, 96 + hover(element: Element) { 97 + const xpath = convertElementToXPath(element) 98 + return triggerCommand('__vitest_hover', xpath) 99 + }, 100 + unhover(element: Element) { 101 + const xpath = convertElementToXPath(element.ownerDocument.body) 102 + return triggerCommand('__vitest_hover', xpath) 103 + }, 104 + 105 + // non userEvent events, but still useful 106 + fill(element: Element, text: string, options) { 107 + const xpath = convertElementToXPath(element) 108 + return triggerCommand('__vitest_fill', xpath, text, options) 109 + }, 110 + dragAndDrop(source: Element, target: Element, options = {}) { 111 + const sourceXpath = convertElementToXPath(source) 112 + const targetXpath = convertElementToXPath(target) 113 + return triggerCommand('__vitest_dragAndDrop', sourceXpath, targetXpath, options) 114 + }, 115 + } 116 + 117 + function getWebdriverioSelectOptions(element: Element, value: string | string[] | HTMLElement[] | HTMLElement) { 118 + const options = [...element.querySelectorAll('option')] as HTMLOptionElement[] 119 + 120 + const arrayValues = Array.isArray(value) ? value : [value] 121 + 122 + if (!arrayValues.length) { 123 + return [] 124 + } 125 + 126 + if (arrayValues.length > 1) { 127 + throw new Error('Provider "webdriverio" doesn\'t support selecting multiple values at once') 128 + } 129 + 130 + const optionValue = arrayValues[0] 131 + 132 + if (typeof optionValue !== 'string') { 133 + const index = options.indexOf(optionValue as HTMLOptionElement) 134 + if (index === -1) { 135 + throw new Error(`The element ${convertElementToXPath(optionValue)} was not found in the "select" options.`) 136 + } 137 + 138 + return [{ index }] 139 + } 140 + 141 + const valueIndex = options.findIndex(option => option.value === optionValue) 142 + if (valueIndex !== -1) { 143 + return [{ index: valueIndex }] 144 + } 145 + 146 + const labelIndex = options.findIndex(option => option.textContent?.trim() === optionValue || option.ariaLabel === optionValue) 147 + 148 + if (labelIndex === -1) { 149 + throw new Error(`The option "${optionValue}" was not found in the "select" options.`) 150 + } 151 + 152 + return [{ index: labelIndex }] 153 + } 154 + 155 + function getSimpleSelectOptions(element: Element, value: string | string[] | HTMLElement[] | HTMLElement) { 156 + return (Array.isArray(value) ? value : [value]).map((v) => { 157 + if (typeof v !== 'string') { 158 + return { element: convertElementToXPath(v) } 159 + } 160 + return v 161 + }) 70 162 } 71 163 72 164 const screenshotIds: Record<string, Record<string, string>> = {}
+1
packages/browser/src/client/orchestrator.ts
··· 52 52 iframe.style.position = 'relative' 53 53 iframe.setAttribute('allowfullscreen', 'true') 54 54 iframe.setAttribute('allow', 'clipboard-write;') 55 + iframe.setAttribute('name', 'vitest-iframe') 55 56 56 57 iframes.set(file, iframe) 57 58 container.appendChild(iframe)
+1
packages/browser/src/client/tester.html
··· 20 20 {__VITEST_SCRIPTS__} 21 21 </head> 22 22 <body 23 + data-vitest-body 23 24 style=" 24 25 width: 100%; 25 26 height: 100%;
+1
packages/browser/src/client/utils.ts
··· 14 14 runningFiles: string[] 15 15 moduleCache: WorkerGlobalState['moduleCache'] 16 16 config: ResolvedConfig 17 + provider: string 17 18 viteConfig: { 18 19 root: string 19 20 }
+2
packages/browser/src/node/index.ts
··· 94 94 const files = project.browserState.get(contextId!)?.files ?? [] 95 95 96 96 const injector = replacer(await injectorJs, { 97 + __VITEST_PROVIDER__: JSON.stringify(project.browserProvider!.name), 97 98 __VITEST_CONFIG__: JSON.stringify(config), 98 99 __VITEST_VITE_CONFIG__: JSON.stringify({ 99 100 root: project.browser!.config.root, ··· 169 170 const files = project.browserState.get(contextId)?.files ?? [] 170 171 171 172 const injector = replacer(await injectorJs, { 173 + __VITEST_PROVIDER__: JSON.stringify(project.browserProvider!.name), 172 174 __VITEST_CONFIG__: JSON.stringify(config), 173 175 __VITEST_FILES__: JSON.stringify(files), 174 176 __VITEST_VITE_CONFIG__: JSON.stringify({
+3
packages/vitest/src/node/config.ts
··· 701 701 702 702 resolved.testTransformMode ??= {} 703 703 704 + resolved.testTimeout ??= resolved.browser.enabled ? 15000 : 5000 705 + resolved.hookTimeout ??= resolved.browser.enabled ? 30000 : 10000 706 + 704 707 return resolved 705 708 } 706 709
+1
packages/browser/src/client/public/esm-client-injector.js
··· 23 23 files: { __VITEST_FILES__ }, 24 24 type: { __VITEST_TYPE__ }, 25 25 contextId: { __VITEST_CONTEXT_ID__ }, 26 + provider: { __VITEST_PROVIDER__ }, 26 27 }; 27 28 28 29 const config = __vitest_browser_runner__.config;
+26
packages/browser/src/node/commands/clear.ts
··· 1 + import type { UserEvent } from '../../../context' 2 + import { PlaywrightBrowserProvider } from '../providers/playwright' 3 + import { WebdriverBrowserProvider } from '../providers/webdriver' 4 + import type { UserEventCommand } from './utils' 5 + 6 + export const clear: UserEventCommand<UserEvent['clear']> = async ( 7 + context, 8 + xpath, 9 + ) => { 10 + if (context.provider instanceof PlaywrightBrowserProvider) { 11 + const { frame } = context 12 + const element = frame.locator(`xpath=${xpath}`) 13 + await element.clear({ 14 + timeout: 1000, 15 + }) 16 + } 17 + else if (context.provider instanceof WebdriverBrowserProvider) { 18 + const browser = context.browser 19 + const markedXpath = `//${xpath}` 20 + const element = await browser.$(markedXpath) 21 + await element.clearValue() 22 + } 23 + else { 24 + throw new TypeError(`Provider "${context.provider.name}" does not support clearing elements`) 25 + } 26 + }
+31 -9
packages/browser/src/node/commands/click.ts
··· 10 10 ) => { 11 11 const provider = context.provider 12 12 if (provider instanceof PlaywrightBrowserProvider) { 13 - const tester = context.tester 14 - await tester.locator(`xpath=${xpath}`).click(options) 15 - return 13 + const tester = context.frame 14 + await tester.locator(`xpath=${xpath}`).click({ 15 + timeout: 1000, 16 + ...options, 17 + }) 16 18 } 17 - if (provider instanceof WebdriverBrowserProvider) { 18 - const page = provider.browser! 19 + else if (provider instanceof WebdriverBrowserProvider) { 20 + const browser = context.browser 19 21 const markedXpath = `//${xpath}` 20 - const element = await page.$(markedXpath) 21 - await element.click(options) 22 - return 22 + await browser.$(markedXpath).click(options as any) 23 23 } 24 - throw new Error(`Provider "${provider.name}" doesn't support click command`) 24 + else { 25 + throw new TypeError(`Provider "${provider.name}" doesn't support click command`) 26 + } 27 + } 28 + 29 + export const dblClick: UserEventCommand<UserEvent['dblClick']> = async ( 30 + context, 31 + xpath, 32 + options = {}, 33 + ) => { 34 + const provider = context.provider 35 + if (provider instanceof PlaywrightBrowserProvider) { 36 + const tester = context.frame 37 + await tester.locator(`xpath=${xpath}`).dblclick(options) 38 + } 39 + else if (provider instanceof WebdriverBrowserProvider) { 40 + const browser = context.browser 41 + const markedXpath = `//${xpath}` 42 + await browser.$(markedXpath).doubleClick() 43 + } 44 + else { 45 + throw new TypeError(`Provider "${provider.name}" doesn't support dblClick command`) 46 + } 25 47 }
+45
packages/browser/src/node/commands/dragAndDrop.ts
··· 1 + import type { UserEvent } from '../../../context' 2 + import { PlaywrightBrowserProvider } from '../providers/playwright' 3 + import { WebdriverBrowserProvider } from '../providers/webdriver' 4 + import type { UserEventCommand } from './utils' 5 + 6 + export const dragAndDrop: UserEventCommand<UserEvent['dragAndDrop']> = async ( 7 + context, 8 + source, 9 + target, 10 + options, 11 + ) => { 12 + if (context.provider instanceof PlaywrightBrowserProvider) { 13 + await context.frame.dragAndDrop( 14 + `xpath=${source}`, 15 + `xpath=${target}`, 16 + { 17 + timeout: 1000, 18 + ...options, 19 + }, 20 + ) 21 + } 22 + else if (context.provider instanceof WebdriverBrowserProvider) { 23 + const sourceXpath = `//${source}` 24 + const targetXpath = `//${target}` 25 + const $source = context.browser.$(sourceXpath) 26 + const $target = context.browser.$(targetXpath) 27 + const duration = (options as any)?.duration ?? 10 28 + 29 + // https://github.com/webdriverio/webdriverio/issues/8022#issuecomment-1700919670 30 + await context.browser 31 + .action('pointer') 32 + .move({ duration: 0, origin: $source, x: 0, y: 0 }) 33 + .down({ button: 0 }) 34 + .move({ duration: 0, origin: 'pointer', x: 0, y: 0 }) 35 + .pause(duration) 36 + .move({ duration: 0, origin: $target, x: 0, y: 0 }) 37 + .move({ duration: 0, origin: 'pointer', x: 1, y: 0 }) 38 + .move({ duration: 0, origin: 'pointer', x: -1, y: 0 }) 39 + .up({ button: 0 }) 40 + .perform() 41 + } 42 + else { 43 + throw new TypeError(`Provider "${context.provider.name}" does not support dragging elements`) 44 + } 45 + }
+25
packages/browser/src/node/commands/fill.ts
··· 1 + import type { UserEvent } from '../../../context' 2 + import { PlaywrightBrowserProvider } from '../providers/playwright' 3 + import { WebdriverBrowserProvider } from '../providers/webdriver' 4 + import type { UserEventCommand } from './utils' 5 + 6 + export const fill: UserEventCommand<UserEvent['fill']> = async ( 7 + context, 8 + xpath, 9 + text, 10 + options = {}, 11 + ) => { 12 + if (context.provider instanceof PlaywrightBrowserProvider) { 13 + const { frame } = context 14 + const element = frame.locator(`xpath=${xpath}`) 15 + await element.fill(text, { timeout: 1000, ...options }) 16 + } 17 + else if (context.provider instanceof WebdriverBrowserProvider) { 18 + const browser = context.browser 19 + const markedXpath = `//${xpath}` 20 + await browser.$(markedXpath).setValue(text) 21 + } 22 + else { 23 + throw new TypeError(`Provider "${context.provider.name}" does not support clearing elements`) 24 + } 25 + }
+25
packages/browser/src/node/commands/hover.ts
··· 1 + import type { UserEvent } from '../../../context' 2 + import { PlaywrightBrowserProvider } from '../providers/playwright' 3 + import { WebdriverBrowserProvider } from '../providers/webdriver' 4 + import type { UserEventCommand } from './utils' 5 + 6 + export const hover: UserEventCommand<UserEvent['hover']> = async ( 7 + context, 8 + xpath, 9 + options = {}, 10 + ) => { 11 + if (context.provider instanceof PlaywrightBrowserProvider) { 12 + await context.frame.locator(`xpath=${xpath}`).hover({ 13 + timeout: 1000, 14 + ...options, 15 + }) 16 + } 17 + else if (context.provider instanceof WebdriverBrowserProvider) { 18 + const browser = context.browser 19 + const markedXpath = `//${xpath}` 20 + await browser.$(markedXpath).moveTo(options) 21 + } 22 + else { 23 + throw new TypeError(`Provider "${context.provider.name}" does not support hover`) 24 + } 25 + }
+23 -4
packages/browser/src/node/commands/index.ts
··· 1 - import { click } from './click' 2 - import { readFile, removeFile, writeFile } from './fs' 3 - import { sendKeys } from './keyboard' 1 + import { click, dblClick } from './click' 2 + import { type } from './type' 3 + import { clear } from './clear' 4 + import { fill } from './fill' 5 + import { selectOptions } from './select' 6 + import { tab } from './tab' 7 + import { keyboard } from './keyboard' 8 + import { dragAndDrop } from './dragAndDrop' 9 + import { hover } from './hover' 10 + import { 11 + readFile, 12 + removeFile, 13 + writeFile, 14 + } from './fs' 4 15 import { screenshot } from './screenshot' 5 16 6 17 export default { 7 18 readFile, 8 19 removeFile, 9 20 writeFile, 10 - sendKeys, 11 21 __vitest_click: click, 22 + __vitest_dblClick: dblClick, 12 23 __vitest_screenshot: screenshot, 24 + __vitest_type: type, 25 + __vitest_clear: clear, 26 + __vitest_fill: fill, 27 + __vitest_tab: tab, 28 + __vitest_keyboard: keyboard, 29 + __vitest_selectOptions: selectOptions, 30 + __vitest_dragAndDrop: dragAndDrop, 31 + __vitest_hover: hover, 13 32 }
+132 -91
packages/browser/src/node/commands/keyboard.ts
··· 1 - // based on https://github.com/modernweb-dev/web/blob/f7fcf29cb79e82ad5622665d76da3f6b23d0ef43/packages/test-runner-commands/src/sendKeysPlugin.ts 2 - 3 - import type { BrowserCommand } from 'vitest/node' 4 - import type { 5 - BrowserCommands, 6 - DownPayload, 7 - PressPayload, 8 - SendKeysPayload, 9 - TypePayload, 10 - UpPayload, 11 - } from '../../../context' 1 + import { parseKeyDef } from '@testing-library/user-event/dist/esm/keyboard/parseKeyDef.js' 2 + import { defaultKeyMap } from '@testing-library/user-event/dist/esm/keyboard/keyMap.js' 3 + import type { BrowserProvider } from 'vitest/node' 12 4 import { PlaywrightBrowserProvider } from '../providers/playwright' 13 5 import { WebdriverBrowserProvider } from '../providers/webdriver' 6 + import type { UserEvent } from '../../../context' 7 + import type { UserEventCommand } from './utils' 14 8 15 - function isObject(payload: unknown): payload is Record<string, unknown> { 16 - return payload != null && typeof payload === 'object' 17 - } 18 - 19 - function isSendKeysPayload(payload: unknown): boolean { 20 - const validOptions = ['type', 'press', 'down', 'up'] 21 - 22 - if (!isObject(payload)) { 23 - throw new Error('You must provide a `SendKeysPayload` object') 9 + export const keyboard: UserEventCommand<UserEvent['keyboard']> = async ( 10 + context, 11 + text, 12 + ) => { 13 + function focusIframe() { 14 + if ( 15 + !document.activeElement 16 + || document.activeElement.ownerDocument !== document 17 + || document.activeElement === document.body 18 + ) { 19 + window.focus() 20 + } 24 21 } 25 22 26 - const numberOfValidOptions = Object.keys(payload).filter(key => 27 - validOptions.includes(key), 28 - ).length 29 - const unknownOptions = Object.keys(payload).filter( 30 - key => !validOptions.includes(key), 23 + if (context.provider instanceof PlaywrightBrowserProvider) { 24 + await context.frame.evaluate(focusIframe) 25 + } 26 + else if (context.provider instanceof WebdriverBrowserProvider) { 27 + await context.browser.execute(focusIframe) 28 + } 29 + 30 + await keyboardImplementation( 31 + context.provider, 32 + context.contextId, 33 + text, 34 + async () => { 35 + function selectAll() { 36 + const element = document.activeElement as HTMLInputElement 37 + if (element && element.select) { 38 + element.select() 39 + } 40 + } 41 + if (context.provider instanceof PlaywrightBrowserProvider) { 42 + await context.frame.evaluate(selectAll) 43 + } 44 + else if (context.provider instanceof WebdriverBrowserProvider) { 45 + await context.browser.execute(selectAll) 46 + } 47 + else { 48 + throw new TypeError(`Provider "${context.provider.name}" does not support selecting all text`) 49 + } 50 + }, 51 + false, 31 52 ) 32 - 33 - if (numberOfValidOptions > 1) { 34 - throw new Error( 35 - `You must provide ONLY one of the following properties to pass to the browser runner: ${validOptions.join( 36 - ', ', 37 - )}.`, 38 - ) 39 - } 40 - if (numberOfValidOptions === 0) { 41 - throw new Error( 42 - `You must provide one of the following properties to pass to the browser runner: ${validOptions.join( 43 - ', ', 44 - )}.`, 45 - ) 46 - } 47 - if (unknownOptions.length > 0) { 48 - throw new Error( 49 - `Unknown options \`${unknownOptions.join(', ')}\` present.`, 50 - ) 51 - } 52 - 53 - return true 54 53 } 55 54 56 - function isTypePayload(payload: SendKeysPayload): payload is TypePayload { 57 - return 'type' in payload 58 - } 59 - 60 - function isPressPayload(payload: SendKeysPayload): payload is PressPayload { 61 - return 'press' in payload 62 - } 63 - 64 - function isDownPayload(payload: SendKeysPayload): payload is DownPayload { 65 - return 'down' in payload 66 - } 67 - 68 - function isUpPayload(payload: SendKeysPayload): payload is UpPayload { 69 - return 'up' in payload 70 - } 71 - 72 - export const sendKeys: BrowserCommand< 73 - Parameters<BrowserCommands['sendKeys']> 74 - > = async ({ provider, contextId }, payload) => { 75 - if (!isSendKeysPayload(payload) || !payload) { 76 - throw new Error('You must provide a `SendKeysPayload` object') 77 - } 55 + export async function keyboardImplementation( 56 + provider: BrowserProvider, 57 + contextId: string, 58 + text: string, 59 + selectAll: () => Promise<void>, 60 + skipRelease: boolean, 61 + ) { 62 + const pressed = new Set<string>() 78 63 79 64 if (provider instanceof PlaywrightBrowserProvider) { 80 65 const page = provider.getPage(contextId) 81 - if (isTypePayload(payload)) { 82 - await page.keyboard.type(payload.type) 66 + const actions = parseKeyDef(defaultKeyMap, text) 67 + 68 + for (const { releasePrevious, releaseSelf, repeat, keyDef } of actions) { 69 + const key = keyDef.key! 70 + 71 + // TODO: instead of calling down/up for each key, join non special 72 + // together, and call `type` once for all non special keys, 73 + // and then `press` for special keys 74 + if (pressed.has(key)) { 75 + await page.keyboard.up(key) 76 + pressed.delete(key) 77 + } 78 + 79 + if (!releasePrevious) { 80 + if (key === 'selectall') { 81 + await selectAll() 82 + continue 83 + } 84 + 85 + for (let i = 1; i <= repeat; i++) { 86 + await page.keyboard.down(key) 87 + } 88 + 89 + if (releaseSelf) { 90 + await page.keyboard.up(key) 91 + } 92 + else { 93 + pressed.add(key) 94 + } 95 + } 83 96 } 84 - else if (isPressPayload(payload)) { 85 - await page.keyboard.press(payload.press) 86 - } 87 - else if (isDownPayload(payload)) { 88 - await page.keyboard.down(payload.down) 89 - } 90 - else if (isUpPayload(payload)) { 91 - await page.keyboard.up(payload.up) 97 + 98 + if (!skipRelease && pressed.size) { 99 + for (const key of pressed) { 100 + await page.keyboard.up(key) 101 + } 92 102 } 93 103 } 94 104 else if (provider instanceof WebdriverBrowserProvider) { 105 + const { Key } = await import('webdriverio') 95 106 const browser = provider.browser! 96 - if (isTypePayload(payload)) { 97 - await browser.keys(payload.type.split('')) 107 + const actions = parseKeyDef(defaultKeyMap, text) 108 + 109 + let keyboard = browser.action('key') 110 + 111 + for (const { releasePrevious, releaseSelf, repeat, keyDef } of actions) { 112 + let key = keyDef.key! 113 + const code = 'location' in keyDef ? keyDef.key! : keyDef.code! 114 + const special = Key[code as 'Shift'] 115 + 116 + if (special) { 117 + key = special 118 + } 119 + 120 + if (pressed.has(key)) { 121 + keyboard.up(key) 122 + pressed.delete(key) 123 + } 124 + 125 + if (!releasePrevious) { 126 + if (key === 'selectall') { 127 + await keyboard.perform() 128 + keyboard = browser.action('key') 129 + await selectAll() 130 + continue 131 + } 132 + 133 + for (let i = 1; i <= repeat; i++) { 134 + keyboard.down(key) 135 + } 136 + 137 + if (releaseSelf) { 138 + keyboard.up(key) 139 + } 140 + else { 141 + pressed.add(key) 142 + } 143 + } 98 144 } 99 - else if (isPressPayload(payload)) { 100 - await browser.keys([payload.press]) 101 - } 102 - else { 103 - throw new Error('Only "press" and "type" are supported by webdriverio.') 104 - } 145 + 146 + await keyboard.perform(skipRelease) 105 147 } 106 - else { 107 - throw new TypeError( 108 - `"sendKeys" is not supported for ${provider.name} browser provider.`, 109 - ) 148 + 149 + return { 150 + pressed, 110 151 } 111 152 }
+2 -2
packages/browser/src/node/commands/screenshot.ts
··· 28 28 if (context.provider instanceof PlaywrightBrowserProvider) { 29 29 if (options.element) { 30 30 const { element: elementXpath, ...config } = options 31 - const iframe = context.tester 31 + const iframe = context.frame 32 32 const element = iframe.locator(`xpath=${elementXpath}`) 33 33 await element.screenshot({ ...config, path: savePath }) 34 34 } 35 35 else { 36 - await context.body.screenshot({ ...options, path: savePath }) 36 + await context.frame.locator('body').screenshot({ ...options, path: savePath }) 37 37 } 38 38 return path 39 39 }
+55
packages/browser/src/node/commands/select.ts
··· 1 + import type { ElementHandle } from 'playwright' 2 + import type { UserEvent } from '../../../context' 3 + import { PlaywrightBrowserProvider } from '../providers/playwright' 4 + import { WebdriverBrowserProvider } from '../providers/webdriver' 5 + import type { UserEventCommand } from './utils' 6 + 7 + export const selectOptions: UserEventCommand<UserEvent['selectOptions']> = async ( 8 + context, 9 + xpath, 10 + userValues, 11 + options = {}, 12 + ) => { 13 + if (context.provider instanceof PlaywrightBrowserProvider) { 14 + const value = userValues as any as (string | { element: string })[] 15 + const { frame } = context 16 + const selectElement = frame.locator(`xpath=${xpath}`) 17 + 18 + const values = await Promise.all(value.map(async (v) => { 19 + if (typeof v === 'string') { 20 + return v 21 + } 22 + const elementHandler = await frame.locator(`xpath=${v.element}`).elementHandle() 23 + if (!elementHandler) { 24 + throw new Error(`Element not found: ${v.element}`) 25 + } 26 + return elementHandler 27 + })) as (readonly string[]) | (readonly ElementHandle[]) 28 + 29 + await selectElement.selectOption(values, { 30 + timeout: 1000, 31 + ...options, 32 + }) 33 + } 34 + else if (context.provider instanceof WebdriverBrowserProvider) { 35 + const values = userValues as any as [({ index: number })] 36 + 37 + if (!values.length) { 38 + return 39 + } 40 + 41 + const markedXpath = `//${xpath}` 42 + const browser = context.browser 43 + 44 + if (values.length === 1 && 'index' in values[0]) { 45 + const selectElement = browser.$(markedXpath) 46 + await selectElement.selectByIndex(values[0].index) 47 + } 48 + else { 49 + throw new Error('Provider "webdriverio" doesn\'t support selecting multiple values at once') 50 + } 51 + } 52 + else { 53 + throw new TypeError(`Provider "${context.provider.name}" doesn't support selectOptions command`) 54 + } 55 + }
+23
packages/browser/src/node/commands/tab.ts
··· 1 + import type { UserEvent } from '../../../context' 2 + import { PlaywrightBrowserProvider } from '../providers/playwright' 3 + import { WebdriverBrowserProvider } from '../providers/webdriver' 4 + import type { UserEventCommand } from './utils' 5 + 6 + export const tab: UserEventCommand<UserEvent['tab']> = async ( 7 + context, 8 + options = {}, 9 + ) => { 10 + const provider = context.provider 11 + if (provider instanceof PlaywrightBrowserProvider) { 12 + const page = context.page 13 + await page.keyboard.press(options.shift === true ? 'Shift+Tab' : 'Tab') 14 + return 15 + } 16 + if (provider instanceof WebdriverBrowserProvider) { 17 + const { Key } = await import('webdriverio') 18 + const browser = context.browser 19 + await browser.keys(options.shift === true ? [Key.Shift, Key.Tab] : [Key.Tab]) 20 + return 21 + } 22 + throw new Error(`Provider "${provider.name}" doesn't support tab command`) 23 + }
+56
packages/browser/src/node/commands/type.ts
··· 1 + import type { UserEvent } from '../../../context' 2 + import { PlaywrightBrowserProvider } from '../providers/playwright' 3 + import { WebdriverBrowserProvider } from '../providers/webdriver' 4 + import type { UserEventCommand } from './utils' 5 + import { keyboardImplementation } from './keyboard' 6 + 7 + export const type: UserEventCommand<UserEvent['type']> = async ( 8 + context, 9 + xpath, 10 + text, 11 + options = {}, 12 + ) => { 13 + const { skipClick = false, skipAutoClose = false } = options 14 + 15 + if (context.provider instanceof PlaywrightBrowserProvider) { 16 + const { frame } = context 17 + const element = frame.locator(`xpath=${xpath}`) 18 + 19 + if (!skipClick) { 20 + await element.focus() 21 + } 22 + 23 + await keyboardImplementation( 24 + context.provider, 25 + context.contextId, 26 + text, 27 + () => element.selectText(), 28 + skipAutoClose, 29 + ) 30 + } 31 + else if (context.provider instanceof WebdriverBrowserProvider) { 32 + const browser = context.browser 33 + const markedXpath = `//${xpath}` 34 + const element = browser.$(markedXpath) 35 + 36 + if (!skipClick && !await element.isFocused()) { 37 + await element.click() 38 + } 39 + 40 + await keyboardImplementation( 41 + context.provider, 42 + context.contextId, 43 + text, 44 + () => browser.execute(() => { 45 + const element = document.activeElement as HTMLInputElement 46 + if (element) { 47 + element.select() 48 + } 49 + }), 50 + skipAutoClose, 51 + ) 52 + } 53 + else { 54 + throw new TypeError(`Provider "${context.provider.name}" does not support typing`) 55 + } 56 + }
+19 -7
packages/browser/src/node/plugins/pluginContext.ts
··· 81 81 } 82 82 } 83 83 export const commands = server.commands 84 - export const userEvent = ${ 85 - provider.name === 'preview' ? '__vitest_user_event__' : '__userEvent_CDP__' 86 - } 84 + export const userEvent = ${getUserEvent(provider)} 87 85 export { page } 88 86 ` 89 87 } 90 88 91 - async function getUserEventImport( 92 - provider: BrowserProvider, 93 - resolve: (id: string, importer: string) => Promise<null | { id: string }>, 94 - ) { 89 + function getUserEvent(provider: BrowserProvider) { 90 + if (provider.name !== 'preview') { 91 + return '__userEvent_CDP__' 92 + } 93 + // TODO: have this in a separate file 94 + return `{ 95 + ...__vitest_user_event__, 96 + fill: async (element, text) => { 97 + await __vitest_user_event__.clear(element) 98 + await __vitest_user_event__.type(element, text) 99 + }, 100 + dragAndDrop: async () => { 101 + throw new Error('Provider "preview" does not support dragging elements') 102 + } 103 + }` 104 + } 105 + 106 + async function getUserEventImport(provider: BrowserProvider, resolve: (id: string, importer: string) => Promise<null | { id: string }>) { 95 107 if (provider.name !== 'preview') { 96 108 return '' 97 109 }
+3 -4
packages/browser/src/node/providers/playwright.ts
··· 102 102 103 103 public getCommandsContext(contextId: string) { 104 104 const page = this.getPage(contextId) 105 - const tester = page.frameLocator('iframe[data-vitest]') 106 105 return { 107 106 page, 108 - tester, 109 - get body() { 110 - return page.frameLocator('iframe[data-vitest]').locator('body') 107 + context: this.contexts.get(contextId)!, 108 + get frame() { 109 + return page.frame('vitest-iframe')! 111 110 }, 112 111 } 113 112 }
+7 -6
packages/vitest/src/integrations/chai/poll.ts
··· 40 40 }) as Assertion 41 41 const proxy: any = new Proxy(assertion, { 42 42 get(target, key, receiver) { 43 - const result = Reflect.get(target, key, receiver) 43 + const assertionFunction = Reflect.get(target, key, receiver) 44 44 45 - if (typeof result !== 'function') { 46 - return result instanceof chai.Assertion ? proxy : result 45 + if (typeof assertionFunction !== 'function') { 46 + return assertionFunction instanceof chai.Assertion ? proxy : assertionFunction 47 47 } 48 48 49 49 if (key === 'assert') { 50 - return result 50 + return assertionFunction 51 51 } 52 52 53 53 if (typeof key === 'string' && unsupported.includes(key)) { ··· 75 75 }, timeout) 76 76 const check = async () => { 77 77 try { 78 - chai.util.flag(this, 'object', await fn()) 79 - resolve(await result.call(this, ...args)) 78 + const obj = await fn() 79 + chai.util.flag(assertion, 'object', obj) 80 + resolve(await assertionFunction.call(assertion, ...args)) 80 81 clearTimeout(intervalId) 81 82 clearTimeout(timeoutId) 82 83 }