[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: introduce separate packages for browser mode providers (#8629)

authored by

Vladimir and committed by
GitHub
(Oct 1, 2025, 3:58 PM +0200) 0dc93ea9 88e62c75

+2757 -1715
+5
docs/.vitepress/config.ts
··· 253 253 link: '/guide/browser/webdriverio', 254 254 docFooterText: 'Configuring WebdriverIO | Browser Mode', 255 255 }, 256 + { 257 + text: 'Configuring Preview', 258 + link: '/guide/browser/preview', 259 + docFooterText: 'Configuring Preview | Browser Mode', 260 + }, 256 261 ], 257 262 }, 258 263 {
+3 -3
docs/guide/browser/assertion-api.md
··· 7 7 Vitest provides a wide range of DOM assertions out of the box forked from [`@testing-library/jest-dom`](https://github.com/testing-library/jest-dom) library with the added support for locators and built-in retry-ability. 8 8 9 9 ::: tip TypeScript Support 10 - If you are using [TypeScript](/guide/browser/#typescript) or want to have correct type hints in `expect`, make sure you have `@vitest/browser/context` referenced somewhere. If you never imported from there, you can add a `reference` comment in any file that's covered by your `tsconfig.json`: 10 + If you are using [TypeScript](/guide/browser/#typescript) or want to have correct type hints in `expect`, make sure you have `vitest/browser` referenced somewhere. If you never imported from there, you can add a `reference` comment in any file that's covered by your `tsconfig.json`: 11 11 12 12 ```ts 13 - /// <reference types="@vitest/browser/context" /> 13 + /// <reference types="vitest/browser" /> 14 14 ``` 15 15 ::: 16 16 ··· 18 18 19 19 ```ts 20 20 import { expect, test } from 'vitest' 21 - import { page } from '@vitest/browser/context' 21 + import { page } from 'vitest/browser' 22 22 23 23 test('error banner is rendered', async () => { 24 24 triggerError()
+6 -6
docs/guide/browser/commands.md
··· 20 20 ::: 21 21 22 22 ```ts 23 - import { server } from '@vitest/browser/context' 23 + import { server } from 'vitest/browser' 24 24 25 25 const { readFile, writeFile, removeFile } = server.commands 26 26 ··· 38 38 39 39 ## CDP Session 40 40 41 - Vitest exposes access to raw Chrome Devtools Protocol via the `cdp` method exported from `@vitest/browser/context`. It is mostly useful to library authors to build tools on top of it. 41 + Vitest exposes access to raw Chrome Devtools Protocol via the `cdp` method exported from `vitest/browser`. It is mostly useful to library authors to build tools on top of it. 42 42 43 43 ```ts 44 - import { cdp } from '@vitest/browser/context' 44 + import { cdp } from 'vitest/browser' 45 45 46 46 const input = document.createElement('input') 47 47 document.body.appendChild(input) ··· 97 97 } 98 98 ``` 99 99 100 - Then you can call it inside your test by importing it from `@vitest/browser/context`: 100 + Then you can call it inside your test by importing it from `vitest/browser`: 101 101 102 102 ```ts 103 - import { commands } from '@vitest/browser/context' 103 + import { commands } from 'vitest/browser' 104 104 import { expect, test } from 'vitest' 105 105 106 106 test('custom command works correctly', async () => { ··· 109 109 }) 110 110 111 111 // if you are using TypeScript, you can augment the module 112 - declare module '@vitest/browser/context' { 112 + declare module 'vitest/browser' { 113 113 interface BrowserCommands { 114 114 myCustomCommand: (arg1: string, arg2: string) => Promise<{ 115 115 someValue: true
+2 -2
docs/guide/browser/component-testing.md
··· 138 138 ```jsx 139 139 // For Solid.js components 140 140 import { render } from '@testing-library/solid' 141 - import { page } from '@vitest/browser/context' 141 + import { page } from 'vitest/browser' 142 142 143 143 test('Solid component handles user interaction', async () => { 144 144 // Use Testing Library to render the component ··· 564 564 ### Key Differences 565 565 566 566 - Use `await expect.element()` instead of `expect()` for DOM assertions 567 - - Use `@vitest/browser/context` for user interactions instead of `@testing-library/user-event` 567 + - Use `vitest/browser` for user interactions instead of `@testing-library/user-event` 568 568 - Browser Mode provides real browser environment for accurate testing 569 569 570 570 ## Learn More
+10 -17
docs/guide/browser/config.md
··· 4 4 5 5 ```ts [vitest.config.ts] 6 6 import { defineConfig } from 'vitest/config' 7 - import { playwright } from '@vitest/browser/providers/playwright' 7 + import { playwright } from '@vitest/browser-playwright' 8 8 9 9 export default defineConfig({ 10 10 test: { ··· 47 47 ## browser.instances 48 48 49 49 - **Type:** `BrowserConfig` 50 - - **Default:** `[{ browser: name }]` 50 + - **Default:** `[]` 51 51 52 - Defines multiple browser setups. Every config has to have at least a `browser` field. The config supports your providers configurations: 53 - 54 - - [Configuring Playwright](/guide/browser/playwright) 55 - - [Configuring WebdriverIO](/guide/browser/webdriverio) 52 + Defines multiple browser setups. Every config has to have at least a `browser` field. 56 53 57 - In addition to that, you can also specify most of the [project options](/config/) (not marked with a <NonProjectOption /> icon) and some of the `browser` options like `browser.testerHtmlPath`. 54 + You can specify most of the [project options](/config/) (not marked with a <NonProjectOption /> icon) and some of the `browser` options like `browser.testerHtmlPath`. 58 55 59 56 ::: warning 60 57 Every browser config inherits options from the root config: ··· 79 76 }) 80 77 ``` 81 78 82 - During development, Vitest supports only one [non-headless](#browser-headless) configuration. You can limit the headed project yourself by specifying `headless: false` in the config, or by providing the `--browser.headless=false` flag, or by filtering projects with `--project=chromium` flag. 83 - 84 79 For more examples, refer to the ["Multiple Setups" guide](/guide/browser/multiple-setups). 85 80 ::: 86 81 ··· 94 89 - [`browser.screenshotFailures`](#browser-screenshotfailures) 95 90 - [`browser.provider`](#browser-provider) 96 91 97 - By default, Vitest creates an array with a single element which uses the [`browser.name`](#browser-name) field as a `browser`. Note that this behaviour will be removed with Vitest 4. 98 - 99 92 Under the hood, Vitest transforms these instances into separate [test projects](/advanced/api/test-project) sharing a single Vite server for better caching performance. 100 93 101 94 ## browser.headless ··· 134 127 - **Default:** `'preview'` 135 128 - **CLI:** `--browser.provider=playwright` 136 129 137 - The return value of the provider factory. You can import the factory from `@vitest/browser/providers/<provider-name>` or make your own provider: 130 + The return value of the provider factory. You can import the factory from `@vitest/browser-<provider-name>` or make your own provider: 138 131 139 132 ```ts{8-10} 140 - import { playwright } from '@vitest/browser/providers/playwright' 141 - import { webdriverio } from '@vitest/browser/providers/webdriverio' 142 - import { preview } from '@vitest/browser/providers/preview' 133 + import { playwright } from '@vitest/browser-playwright' 134 + import { webdriverio } from '@vitest/browser-webdriverio' 135 + import { preview } from '@vitest/browser-preview' 143 136 144 137 export default defineConfig({ 145 138 test: { ··· 155 148 To configure how provider initializes the browser, you can pass down options to the factory function: 156 149 157 150 ```ts{7-13,20-26} 158 - import { playwright } from '@vitest/browser/providers/playwright' 151 + import { playwright } from '@vitest/browser-playwright' 159 152 160 153 export default defineConfig({ 161 154 test: { ··· 294 287 - **Type:** `Record<string, BrowserCommand>` 295 288 - **Default:** `{ readFile, writeFile, ... }` 296 289 297 - Custom [commands](/guide/browser/commands) that can be imported during browser tests from `@vitest/browser/commands`. 290 + Custom [commands](/guide/browser/commands) that can be imported during browser tests from `vitest/browser`. 298 291 299 292 ## browser.connectTimeout 300 293
+1 -1
docs/guide/browser/context.md
··· 4 4 5 5 # Context API 6 6 7 - Vitest exposes a context module via `@vitest/browser/context` entry point. As of 2.0, it exposes a small set of utilities that might be useful to you in tests. 7 + Vitest exposes a context module via `vitest/browser` entry point. As of 2.0, it exposes a small set of utilities that might be useful to you in tests. 8 8 9 9 ## `userEvent` 10 10
+14 -14
docs/guide/browser/index.md
··· 99 99 100 100 ```ts [vitest.config.ts] 101 101 import { defineConfig } from 'vitest/config' 102 - import { playwright } from '@vitest/browser/providers/playwright' 102 + import { playwright } from '@vitest/browser-playwright' 103 103 104 104 export default defineConfig({ 105 105 test: { ··· 127 127 ```ts [react] 128 128 import { defineConfig } from 'vitest/config' 129 129 import react from '@vitejs/plugin-react' 130 - import { playwright } from '@vitest/browser/providers/playwright' 130 + import { playwright } from '@vitest/browser-playwright' 131 131 132 132 export default defineConfig({ 133 133 plugins: [react()], ··· 144 144 ``` 145 145 ```ts [vue] 146 146 import { defineConfig } from 'vitest/config' 147 - import { playwright } from '@vitest/browser/providers/playwright' 147 + import { playwright } from '@vitest/browser-playwright' 148 148 import vue from '@vitejs/plugin-vue' 149 149 150 150 export default defineConfig({ ··· 163 163 ```ts [svelte] 164 164 import { defineConfig } from 'vitest/config' 165 165 import { svelte } from '@sveltejs/vite-plugin-svelte' 166 - import { playwright } from '@vitest/browser/providers/playwright' 166 + import { playwright } from '@vitest/browser-playwright' 167 167 168 168 export default defineConfig({ 169 169 plugins: [svelte()], ··· 181 181 ```ts [solid] 182 182 import { defineConfig } from 'vitest/config' 183 183 import solidPlugin from 'vite-plugin-solid' 184 - import { playwright } from '@vitest/browser/providers/playwright' 184 + import { playwright } from '@vitest/browser-playwright' 185 185 186 186 export default defineConfig({ 187 187 plugins: [solidPlugin()], ··· 199 199 ```ts [marko] 200 200 import { defineConfig } from 'vitest/config' 201 201 import marko from '@marko/vite' 202 - import { playwright } from '@vitest/browser/providers/playwright' 202 + import { playwright } from '@vitest/browser-playwright' 203 203 204 204 export default defineConfig({ 205 205 plugins: [marko()], ··· 217 217 ```ts [qwik] 218 218 import { defineConfig } from 'vitest/config' 219 219 import { qwikVite } from '@builder.io/qwik/optimizer' 220 - import { playwright } from '@vitest/browser/providers/playwright' 220 + import { playwright } from '@vitest/browser-playwright' 221 221 222 222 // optional, run the tests in SSR mode 223 223 import { testSSR } from 'vitest-browser-qwik/ssr-plugin' ··· 241 241 242 242 ```ts [vitest.config.ts] 243 243 import { defineConfig } from 'vitest/config' 244 - import { playwright } from '@vitest/browser/providers/playwright' 244 + import { playwright } from '@vitest/browser-playwright' 245 245 246 246 export default defineConfig({ 247 247 test: { ··· 338 338 339 339 ```ts [vitest.config.ts] 340 340 import { defineConfig } from 'vitest/config' 341 - import { playwright } from '@vitest/browser/providers/playwright' 341 + import { playwright } from '@vitest/browser-playwright' 342 342 343 343 export default defineConfig({ 344 344 test: { ··· 369 369 370 370 ```js [example.test.js] 371 371 import { expect, test } from 'vitest' 372 - import { page } from '@vitest/browser/context' 372 + import { page } from 'vitest/browser' 373 373 import { render } from './my-render-function.js' 374 374 375 375 test('properly handles form inputs', async () => { ··· 407 407 408 408 ```ts 409 409 import { expect } from 'vitest' 410 - import { page } from '@vitest/browser/context' 410 + import { page } from 'vitest/browser' 411 411 // element is rendered correctly 412 412 await expect.element(page.getByText('Hello World')).toBeInTheDocument() 413 413 ``` 414 414 415 - Vitest exposes a [Context API](/guide/browser/context) with a small set of utilities that might be useful to you in tests. For example, if you need to make an interaction, like clicking an element or typing text into an input, you can use `userEvent` from `@vitest/browser/context`. Read more at the [Interactivity API](/guide/browser/interactivity-api). 415 + Vitest exposes a [Context API](/guide/browser/context) with a small set of utilities that might be useful to you in tests. For example, if you need to make an interaction, like clicking an element or typing text into an input, you can use `userEvent` from `vitest/browser`. Read more at the [Interactivity API](/guide/browser/interactivity-api). 416 416 417 417 ```ts 418 - import { page, userEvent } from '@vitest/browser/context' 418 + import { page, userEvent } from 'vitest/browser' 419 419 await userEvent.fill(page.getByLabelText(/username/i), 'Alice') 420 420 // or just locator.fill 421 421 await page.getByLabelText(/username/i).fill('Alice') ··· 532 532 You can also see more examples in [`browser-examples`](https://github.com/vitest-tests/browser-examples) repository. 533 533 534 534 ::: warning 535 - `testing-library` provides a package `@testing-library/user-event`. We do not recommend using it directly because it simulates events instead of actually triggering them - instead, use [`userEvent`](/guide/browser/interactivity-api) imported from `@vitest/browser/context` that uses Chrome DevTools Protocol or Webdriver (depending on the provider) under the hood. 535 + `testing-library` provides a package `@testing-library/user-event`. We do not recommend using it directly because it simulates events instead of actually triggering them - instead, use [`userEvent`](/guide/browser/interactivity-api) imported from `vitest/browser` that uses Chrome DevTools Protocol or Webdriver (depending on the provider) under the hood. 536 536 ::: 537 537 538 538 ::: code-group
+18 -18
docs/guide/browser/interactivity-api.md
··· 7 7 Vitest implements a subset of [`@testing-library/user-event`](https://testing-library.com/docs/user-event/intro) APIs using [Chrome DevTools Protocol](https://chromedevtools.github.io/devtools-protocol/) or [webdriver](https://www.w3.org/TR/webdriver/) instead of faking events which makes the browser behaviour more reliable and consistent with how users interact with a page. 8 8 9 9 ```ts 10 - import { userEvent } from '@vitest/browser/context' 10 + import { userEvent } from 'vitest/browser' 11 11 12 12 await userEvent.click(document.querySelector('.button')) 13 13 ``` ··· 23 23 Creates a new user event instance. This is useful if you need to keep the state of keyboard to press and release buttons correctly. 24 24 25 25 ::: warning 26 - Unlike `@testing-library/user-event`, the default `userEvent` instance from `@vitest/browser/context` is created once, not every time its methods are called! You can see the difference in how it works in this snippet: 26 + Unlike `@testing-library/user-event`, the default `userEvent` instance from `vitest/browser` is created once, not every time its methods are called! You can see the difference in how it works in this snippet: 27 27 28 28 ```ts 29 - import { userEvent as vitestUserEvent } from '@vitest/browser/context' 29 + import { userEvent as vitestUserEvent } from 'vitest/browser' 30 30 import { userEvent as originalUserEvent } from '@testing-library/user-event' 31 31 32 32 await vitestUserEvent.keyboard('{Shift}') // press shift without releasing ··· 51 51 Click on an element. Inherits provider's options. Please refer to your provider's documentation for detailed explanation about how this method works. 52 52 53 53 ```ts 54 - import { page, userEvent } from '@vitest/browser/context' 54 + import { page, userEvent } from 'vitest/browser' 55 55 56 56 test('clicks on an element', async () => { 57 57 const logo = page.getByRole('img', { name: /logo/ }) ··· 82 82 Please refer to your provider's documentation for detailed explanation about how this method works. 83 83 84 84 ```ts 85 - import { page, userEvent } from '@vitest/browser/context' 85 + import { page, userEvent } from 'vitest/browser' 86 86 87 87 test('triggers a double click on an element', async () => { 88 88 const logo = page.getByRole('img', { name: /logo/ }) ··· 113 113 Please refer to your provider's documentation for detailed explanation about how this method works. 114 114 115 115 ```ts 116 - import { page, userEvent } from '@vitest/browser/context' 116 + import { page, userEvent } from 'vitest/browser' 117 117 118 118 test('triggers a triple click on an element', async () => { 119 119 const logo = page.getByRole('img', { name: /logo/ }) ··· 150 150 Set a value to the `input`/`textarea`/`contenteditable` field. This will remove any existing text in the input before setting the new value. 151 151 152 152 ```ts 153 - import { page, userEvent } from '@vitest/browser/context' 153 + import { page, userEvent } from 'vitest/browser' 154 154 155 155 test('update input', async () => { 156 156 const input = page.getByRole('input') ··· 189 189 This API supports [user-event `keyboard` syntax](https://testing-library.com/docs/user-event/keyboard). 190 190 191 191 ```ts 192 - import { userEvent } from '@vitest/browser/context' 192 + import { userEvent } from 'vitest/browser' 193 193 194 194 test('trigger keystrokes', async () => { 195 195 await userEvent.keyboard('foo') // translates to: f, o, o ··· 215 215 Sends a `Tab` key event. This is a shorthand for `userEvent.keyboard('{tab}')`. 216 216 217 217 ```ts 218 - import { page, userEvent } from '@vitest/browser/context' 218 + import { page, userEvent } from 'vitest/browser' 219 219 220 220 test('tab works', async () => { 221 221 const [input1, input2] = page.getByRole('input').elements() ··· 259 259 If you just need to press characters without an input, use [`userEvent.keyboard`](#userevent-keyboard) API. 260 260 261 261 ```ts 262 - import { page, userEvent } from '@vitest/browser/context' 262 + import { page, userEvent } from 'vitest/browser' 263 263 264 264 test('update input', async () => { 265 265 const input = page.getByRole('input') ··· 289 289 This method clears the input element content. 290 290 291 291 ```ts 292 - import { page, userEvent } from '@vitest/browser/context' 292 + import { page, userEvent } from 'vitest/browser' 293 293 294 294 test('clears input', async () => { 295 295 const input = page.getByRole('input') ··· 336 336 ::: 337 337 338 338 ```ts 339 - import { page, userEvent } from '@vitest/browser/context' 339 + import { page, userEvent } from 'vitest/browser' 340 340 341 341 test('clears input', async () => { 342 342 const select = page.getByRole('select') ··· 386 386 ::: 387 387 388 388 ```ts 389 - import { page, userEvent } from '@vitest/browser/context' 389 + import { page, userEvent } from 'vitest/browser' 390 390 391 391 test('hovers logo element', async () => { 392 392 const logo = page.getByRole('img', { name: /logo/ }) ··· 419 419 ::: 420 420 421 421 ```ts 422 - import { page, userEvent } from '@vitest/browser/context' 422 + import { page, userEvent } from 'vitest/browser' 423 423 424 424 test('unhover logo element', async () => { 425 425 const logo = page.getByRole('img', { name: /logo/ }) ··· 449 449 Change a file input element to have the specified files. 450 450 451 451 ```ts 452 - import { page, userEvent } from '@vitest/browser/context' 452 + import { page, userEvent } from 'vitest/browser' 453 453 454 454 test('can upload a file', async () => { 455 455 const input = page.getByRole('button', { name: /Upload files/ }) ··· 488 488 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`. 489 489 490 490 ```ts 491 - import { page, userEvent } from '@vitest/browser/context' 491 + import { page, userEvent } from 'vitest/browser' 492 492 493 493 test('drag and drop works', async () => { 494 494 const source = page.getByRole('img', { name: /logo/ }) ··· 520 520 Copy the selected text to the clipboard. 521 521 522 522 ```js 523 - import { page, userEvent } from '@vitest/browser/context' 523 + import { page, userEvent } from 'vitest/browser' 524 524 525 525 test('copy and paste', async () => { 526 526 // write to 'source' ··· 553 553 Cut the selected text to the clipboard. 554 554 555 555 ```js 556 - import { page, userEvent } from '@vitest/browser/context' 556 + import { page, userEvent } from 'vitest/browser' 557 557 558 558 test('copy and paste', async () => { 559 559 // write to 'source'
+13 -13
docs/guide/browser/locators.md
··· 609 609 Click on an element. You can use the options to set the cursor position. 610 610 611 611 ```ts 612 - import { page } from '@vitest/browser/context' 612 + import { page } from 'vitest/browser' 613 613 614 614 await page.getByRole('img', { name: 'Rose' }).click() 615 615 ``` ··· 625 625 Triggers a double click event on an element. You can use the options to set the cursor position. 626 626 627 627 ```ts 628 - import { page } from '@vitest/browser/context' 628 + import { page } from 'vitest/browser' 629 629 630 630 await page.getByRole('img', { name: 'Rose' }).dblClick() 631 631 ``` ··· 641 641 Triggers a triple click event on an element. Since there is no `tripleclick` in browser api, this method will fire three click events in a row. 642 642 643 643 ```ts 644 - import { page } from '@vitest/browser/context' 644 + import { page } from 'vitest/browser' 645 645 646 646 await page.getByRole('img', { name: 'Rose' }).tripleClick() 647 647 ``` ··· 657 657 Clears the input element content. 658 658 659 659 ```ts 660 - import { page } from '@vitest/browser/context' 660 + import { page } from 'vitest/browser' 661 661 662 662 await page.getByRole('textbox', { name: 'Full Name' }).clear() 663 663 ``` ··· 673 673 Moves the cursor position to the selected element. 674 674 675 675 ```ts 676 - import { page } from '@vitest/browser/context' 676 + import { page } from 'vitest/browser' 677 677 678 678 await page.getByRole('img', { name: 'Rose' }).hover() 679 679 ``` ··· 689 689 This works the same as [`locator.hover`](#hover), but moves the cursor to the `document.body` element instead. 690 690 691 691 ```ts 692 - import { page } from '@vitest/browser/context' 692 + import { page } from 'vitest/browser' 693 693 694 694 await page.getByRole('img', { name: 'Rose' }).unhover() 695 695 ``` ··· 705 705 Sets the value of the current `input`, `textarea` or `contenteditable` element. 706 706 707 707 ```ts 708 - import { page } from '@vitest/browser/context' 708 + import { page } from 'vitest/browser' 709 709 710 710 await page.getByRole('input', { name: 'Full Name' }).fill('Mr. Bean') 711 711 ``` ··· 724 724 Drags the current element to the target location. 725 725 726 726 ```ts 727 - import { page } from '@vitest/browser/context' 727 + import { page } from 'vitest/browser' 728 728 729 729 const paris = page.getByText('Paris') 730 730 const france = page.getByText('France') ··· 752 752 Choose one or more values from a `<select>` element. 753 753 754 754 ```ts 755 - import { page } from '@vitest/browser/context' 755 + import { page } from 'vitest/browser' 756 756 757 757 const languages = page.getByRole('select', { name: 'Languages' }) 758 758 ··· 784 784 If you also need the content of the screenshot, you can specify `base64: true` to return it alongside the filepath where the screenshot is saved. 785 785 786 786 ```ts 787 - import { page } from '@vitest/browser/context' 787 + import { page } from 'vitest/browser' 788 788 789 789 const button = page.getByRole('button', { name: 'Click Me!' }) 790 790 ··· 946 946 947 947 ```ts [example.test.ts] 948 948 import { test } from 'vitest' 949 - import { commands, page } from '@vitest/browser/context' 949 + import { commands, page } from 'vitest/browser' 950 950 951 951 test('works correctly', async () => { 952 952 await commands.test(page.getByText('Hello').selector) // ✅ ··· 988 988 ::: 989 989 990 990 ```ts 991 - import { locators } from '@vitest/browser/context' 991 + import { locators } from 'vitest/browser' 992 992 993 993 locators.extend({ 994 994 getByArticleTitle(title) { ··· 1010 1010 1011 1011 // if you are using typescript, you can extend LocatorSelectors interface 1012 1012 // to have the autocompletion in locators.extend, page.* and locator.* methods 1013 - declare module '@vitest/browser/context' { 1013 + declare module 'vitest/browser' { 1014 1014 interface LocatorSelectors { 1015 1015 // if the custom method returns a string, it will be converted into a locator 1016 1016 // if it returns anything else, then it will be returned as usual
+5 -22
docs/guide/browser/multiple-setups.md
··· 1 1 # Multiple Setups 2 2 3 - Since Vitest 3, you can specify several different browser setups using the new [`browser.instances`](/guide/browser/config#browser-instances) option. 3 + You can specify several different browser setups using the [`browser.instances`](/guide/browser/config#browser-instances) option. 4 4 5 5 The main advantage of using the `browser.instances` over the [test projects](/guide/projects) is improved caching. Every project will use the same Vite server meaning the file transform and [dependency pre-bundling](https://vite.dev/guide/dep-pre-bundling.html) has to happen only once. 6 6 ··· 10 10 11 11 ```ts [vitest.config.ts] 12 12 import { defineConfig } from 'vitest/config' 13 - import { playwright } from '@vitest/browser/providers/playwright' 13 + import { playwright } from '@vitest/browser-playwright' 14 14 15 15 export default defineConfig({ 16 16 test: { ··· 35 35 ::: code-group 36 36 ```ts [vitest.config.ts] 37 37 import { defineConfig } from 'vitest/config' 38 - import { playwright } from '@vitest/browser/providers/playwright' 38 + import { playwright } from '@vitest/browser-playwright' 39 39 40 40 export default defineConfig({ 41 41 test: { ··· 50 50 setupFiles: ['./ratio-setup.ts'], 51 51 provide: { 52 52 ratio: 1, 53 - } 53 + }, 54 54 }, 55 55 { 56 56 browser: 'chromium', 57 57 name: 'chromium-2', 58 58 provide: { 59 59 ratio: 2, 60 - } 60 + }, 61 61 }, 62 62 ], 63 63 }, ··· 119 119 }) 120 120 ``` 121 121 ::: 122 - 123 - ::: warning 124 - Vitest cannot run multiple instances that have `headless` mode set to `false` (the default behaviour). During development, you can select what project to run in your terminal: 125 - 126 - ```shell 127 - ? Found multiple projects that run browser tests in headed mode: "chromium", "firefox". 128 - Vitest cannot run multiple headed browsers at the same time. Select a single project 129 - to run or cancel and run tests with "headless: true" option. Note that you can also 130 - start tests with --browser=name or --project=name flag. › - Use arrow-keys. Return to submit. 131 - ❯ chromium 132 - firefox 133 - ``` 134 - 135 - If you have several non-headless projects in CI (i.e. the `headless: false` is set manually in the config and not overridden in CI env), Vitest will fail the run and won't start any tests. 136 - 137 - The ability to run tests in headless mode is not affected by this. You can still run all instances in parallel as long as they don't have `headless: false`. 138 - :::
+9 -5
docs/guide/browser/playwright.md
··· 1 1 # Configuring Playwright 2 2 3 - To run tests using playwright, you need to specify it in the `test.browser.provider` property in your config: 3 + To run tests using playwright, you need to install the [`@vitest/browser-playwright`](https://www.npmjs.com/package/@vitest/browser-playwright) npm package and specify its `playwright` export in the `test.browser.provider` property of your config: 4 4 5 5 ```ts [vitest.config.js] 6 - import { playwright } from '@vitest/browser/providers/playwright' 6 + import { playwright } from '@vitest/browser-playwright' 7 7 import { defineConfig } from 'vitest/config' 8 8 9 9 export default defineConfig({ ··· 16 16 }) 17 17 ``` 18 18 19 - Vitest opens a single page to run all tests in the same file. You can configure the `launch`, `connect` and `context` when calling `playwright` at the top level or inside instances: 19 + You can configure the [`launchOptions`](https://playwright.dev/docs/api/class-browsertype#browser-type-launch), [`connectOptions`](https://playwright.dev/docs/api/class-browsertype#browser-type-connect) and [`contextOptions`](https://playwright.dev/docs/api/class-browser#browser-new-context) when calling `playwright` at the top level or inside instances: 20 20 21 21 ```ts{7-14,21-26} [vitest.config.js] 22 - import { playwright } from '@vitest/browser/providers/playwright' 22 + import { playwright } from '@vitest/browser-playwright' 23 23 import { defineConfig } from 'vitest/config' 24 24 25 25 export default defineConfig({ ··· 52 52 }, 53 53 }) 54 54 ``` 55 + 56 + ::: warning 57 + Unlike Playwright test runner, Vitest opens a _single_ page to run all tests that are defined in the same file. This means that isolation is restricted to a single test file, not to every individual test. 58 + ::: 55 59 56 60 ## launchOptions 57 61 ··· 94 98 You can also configure the action timeout per-action: 95 99 96 100 ```ts 97 - import { page, userEvent } from '@vitest/browser/context' 101 + import { page, userEvent } from 'vitest/browser' 98 102 99 103 await userEvent.click(page.getByRole('button'), { 100 104 timeout: 1_000,
+32
docs/guide/browser/preview.md
··· 1 + # Configuring Preview 2 + 3 + ::: warning 4 + The `preview` provider's main functionality is to show tests in a real browser environment. However, it does not support advanced browser automation features like multiple browser instances or headless mode. For more complex scenarios, consider using [Playwright](/guide/browser/playwright) or [WebdriverIO](/guide/browser/webdriverio). 5 + ::: 6 + 7 + To see your tests running in a real browser, you need to install the [`@vitest/browser-preview`](https://www.npmjs.com/package/@vitest/browser-preview) npm package and specify its `preview` export in the `test.browser.provider` property of your config: 8 + 9 + ```ts [vitest.config.js] 10 + import { preview } from '@vitest/browser-preview' 11 + import { defineConfig } from 'vitest/config' 12 + 13 + export default defineConfig({ 14 + test: { 15 + browser: { 16 + provider: preview(), 17 + instances: [{ browser: 'chromium' }] 18 + }, 19 + }, 20 + }) 21 + ``` 22 + 23 + This will open a new browser window using your default browser to run the tests. You can configure which browser to use by setting the `browser` property in the `instances` array. Vitest will try to open that browser automatically, but it might not work in some environments. In that case, you can manually open the provided URL in your desired browser. 24 + 25 + ## Differences with Other Providers 26 + 27 + The preview provider has some limitations compared to other providers like [Playwright](/guide/browser/playwright) or [WebdriverIO](/guide/browser/webdriverio): 28 + 29 + - It does not support headless mode; the browser window will always be visible. 30 + - It does not support multiple instances of the same browser; each instance must use a different browser. 31 + - It does not support advanced browser capabilities or options; you can only specify the browser name. 32 + - It does not support CDP (Chrome DevTools Protocol) commands or other low-level browser interactions. Unlike Playwright or WebdriverIO, the [`userEvent`](/guide/browser/interactivity-api) API is just re-exported from [`@testing-library/user-event`](https://www.npmjs.com/package/@testing-library/user-event) and does not have any special integration with the browser.
+2 -2
docs/guide/browser/trace-viewer.md
··· 9 9 ::: code-group 10 10 ```ts [vitest.config.js] 11 11 import { defineConfig } from 'vitest/config' 12 - import { playwright } from '@vitest/browser/providers/playwright' 12 + import { playwright } from '@vitest/browser-playwright' 13 13 14 14 export default defineConfig({ 15 15 test: { ··· 39 39 40 40 ```ts [vitest.config.js] 41 41 import { defineConfig } from 'vitest/config' 42 - import { playwright } from '@vitest/browser/providers/playwright' 42 + import { playwright } from '@vitest/browser-playwright' 43 43 44 44 export default defineConfig({ 45 45 test: {
+3 -3
docs/guide/browser/visual-regression-testing.md
··· 54 54 55 55 ```ts 56 56 import { expect, test } from 'vitest' 57 - import { page } from '@vitest/browser/context' 57 + import { page } from 'vitest/browser' 58 58 59 59 test('hero section looks correct', async () => { 60 60 // ...the rest of the test ··· 239 239 ``` 240 240 241 241 ```ts [vitest.config.ts] 242 - import { playwright } from '@vitest/browser/providers/playwright' 242 + import { playwright } from '@vitest/browser-playwright' 243 243 import { defineConfig } from 'vitest/config' 244 244 245 245 export default defineConfig({ ··· 592 592 ```ts [vitest.config.ts] 593 593 import { env } from 'node:process' 594 594 import { defineConfig } from 'vitest/config' 595 - import { playwright } from '@vitest/browser/providers/playwright' 595 + import { playwright } from '@vitest/browser-playwright' 596 596 597 597 export default defineConfig({ 598 598 // ...global Vite config
+11 -9
docs/guide/browser/webdriverio.md
··· 4 4 If you do not already use WebdriverIO in your project, we recommend starting with [Playwright](/guide/browser/playwright) as it is easier to configure and has more flexible API. 5 5 ::: 6 6 7 - To run tests using WebdriverIO, you need to specify it in the `test.browser.provider` property in your config: 7 + To run tests using WebdriverIO, you need to install the [`@vitest/browser-webdriverio`](https://www.npmjs.com/package/@vitest/browser-webdriverio) npm package and specify its `webdriverio` export in the `test.browser.provider` property of your config: 8 8 9 9 ```ts [vitest.config.js] 10 - import { webdriverio } from '@vitest/browser/providers/webdriverio' 10 + import { webdriverio } from '@vitest/browser-webdriverio' 11 11 import { defineConfig } from 'vitest/config' 12 12 13 13 export default defineConfig({ ··· 20 20 }) 21 21 ``` 22 22 23 - Vitest opens a single page to run all tests in the same file. You can configure all the parameters that [`remote`](https://webdriver.io/docs/api/modules/#remoteoptions-modifier) function accepts: 23 + You can configure all the parameters that [`remote`](https://webdriver.io/docs/api/modules/#remoteoptions-modifier) function accepts: 24 24 25 - ```ts{8-12,19-23} [vitest.config.js] 26 - import { webdriverio } from '@vitest/browser/providers/webdriverio' 25 + ```ts{8-12,19-25} [vitest.config.js] 26 + import { webdriverio } from '@vitest/browser-webdriverio' 27 27 import { defineConfig } from 'vitest/config' 28 28 29 29 export default defineConfig({ ··· 42 42 // overriding options only for a single instance 43 43 // this will NOT merge options with the parent one 44 44 provider: webdriverio({ 45 - 'moz:firefoxOptions': { 46 - args: ['--disable-gpu'], 45 + capabilities: { 46 + 'moz:firefoxOptions': { 47 + args: ['--disable-gpu'], 48 + }, 47 49 }, 48 50 }) 49 - } 51 + }, 50 52 ], 51 53 }, 52 54 }, ··· 58 60 ::: tip 59 61 Most useful options are located on `capabilities` object. WebdriverIO allows nested capabilities, but Vitest will ignore those options because we rely on a different mechanism to spawn several browsers. 60 62 61 - Note that Vitest will ignore `capabilities.browserName`. Use [`test.browser.instances.browser`](/guide/browser/config#browser-capabilities-name) instead. 63 + Note that Vitest will ignore `capabilities.browserName` — use [`test.browser.instances.browser`](/guide/browser/config#browser-capabilities-name) instead. 62 64 :::
+1 -1
docs/guide/debugging.md
··· 52 52 ``` 53 53 ```ts [vitest.config.js] 54 54 import { defineConfig } from 'vitest/config' 55 - import { playwright } from '@vitest/browser/providers/playwright' 55 + import { playwright } from '@vitest/browser-playwright' 56 56 57 57 export default defineConfig({ 58 58 test: {
+29 -4
docs/guide/migration.md
··· 215 215 ``` 216 216 ::: 217 217 218 - ### Browser Provider Accepts an Object 218 + ### Browser Provider Rework 219 219 220 - In Vitest 4.0, the browser provider now accepts an object instead of a string (`'playwright'`, `'webdriverio'`). This makes it simpler to work with custom options and doesn't require adding `/// <reference` comments anymore. 220 + In Vitest 4.0, the browser provider now accepts an object instead of a string (`'playwright'`, `'webdriverio'`). The `preview` is no longer a default. This makes it simpler to work with custom options and doesn't require adding `/// <reference` comments anymore. 221 221 222 222 ```ts 223 - import { playwright } from '@vitest/browser/providers/playwright' // [!code ++] 223 + import { playwright } from '@vitest/browser-playwright' // [!code ++] 224 224 225 225 export default defineConfig({ 226 226 test: { ··· 246 246 247 247 The naming of properties in `playwright` factory now also aligns with [Playwright documentation](https://playwright.dev/docs/api/class-testoptions#test-options-launch-options) making it easier to find. 248 248 249 + With this change, the `@vitest/browser` package is no longer needed, and you can remove it from your dependencies. To support the context import, you should update the `@vitest/browser/context` to `vitest/browser`: 250 + 251 + ```ts 252 + import { page } from '@vitest/browser/context' // [!code --] 253 + import { page } from 'vitest/browser' // [!code ++] 254 + 255 + test('example', async () => { 256 + await page.getByRole('button').click() 257 + }) 258 + ``` 259 + 260 + The modules are identical, so doing a simple "Find and Replace" should be sufficient. 261 + 262 + If you were using the `@vitest/browser/utils` module, you can now import those utilities from `vitest/browser` as well: 263 + 264 + ```ts 265 + import { getElementError } from '@vitest/browser/utils' // [!code --] 266 + import { utils } from 'vitest/browser' // [!code ++] 267 + const { getElementError } = utils // [!code ++] 268 + ``` 269 + 270 + ::: warning 271 + Both `@vitest/browser/context` and `@vitest/browser/utils` work at runtime during the transition period, but they will be removed in a future release. 272 + ::: 273 + 249 274 ### Reporter Updates 250 275 251 276 Reporter APIs `onCollected`, `onSpecsCollected`, `onPathsCollected`, `onTaskUpdate` and `onFinished` were removed. See [`Reporters API`](/advanced/api/reporters) for new alternatives. The new APIs were introduced in Vitest `v3.0.0`. ··· 277 302 278 303 In Vitest 4.0 snapshots that include custom elements will print the shadow root contents. To restore the previous behavior, set the [`printShadowRoot` option](/config/#snapshotformat) to `false`. 279 304 280 - ```js 305 + ```js{15-22} 281 306 // before Vite 4.0 282 307 exports[`custom element with shadow root 1`] = ` 283 308 "<body>
+1 -1
eslint.config.js
··· 89 89 }, 90 90 { 91 91 // these files define vitest as peer dependency 92 - files: [`packages/{coverage-*,ui,browser,web-worker}/${GLOB_SRC}`], 92 + files: [`packages/{coverage-*,ui,browser,web-worker,browser-*}/${GLOB_SRC}`], 93 93 rules: { 94 94 'no-restricted-imports': [ 95 95 'error',
+1 -1
examples/lit/package.json
··· 17 17 "lit": "^3.3.1" 18 18 }, 19 19 "devDependencies": { 20 - "@vitest/browser": "latest", 20 + "@vitest/browser-playwright": "latest", 21 21 "jsdom": "latest", 22 22 "playwright": "^1.55.0", 23 23 "vite": "latest",
+1 -1
examples/lit/test/basic.test.ts
··· 1 - import { page } from '@vitest/browser/context' 2 1 import { beforeEach, describe, expect, it } from 'vitest' 2 + import { page } from 'vitest/browser' 3 3 4 4 import '../src/my-button.js' 5 5
-1
examples/lit/tsconfig.json
··· 5 5 "experimentalDecorators": true, 6 6 "module": "node16", 7 7 "moduleResolution": "Node16", 8 - "types": ["@vitest/browser/providers/playwright"], 9 8 "verbatimModuleSyntax": true 10 9 } 11 10 }
+1 -1
examples/lit/vite.config.ts
··· 1 1 /// <reference types="vitest/config" /> 2 2 3 - import { playwright } from '@vitest/browser/providers/playwright' 3 + import { playwright } from '@vitest/browser-playwright' 4 4 import { defineConfig } from 'vite' 5 5 6 6 // https://vitejs.dev/config/
+3
package.json
··· 71 71 "pnpm": { 72 72 "overrides": { 73 73 "@vitest/browser": "workspace:*", 74 + "@vitest/browser-playwright": "workspace:*", 75 + "@vitest/browser-preview": "workspace:*", 76 + "@vitest/browser-webdriverio": "workspace:*", 74 77 "@vitest/ui": "workspace:*", 75 78 "acorn": "8.11.3", 76 79 "mlly": "^1.8.0",
+48
packages/browser-playwright/README.md
··· 1 + # @vitest/browser-playwright 2 + 3 + [![NPM version](https://img.shields.io/npm/v/@vitest/browser-playwright?color=a1b858&label=)](https://www.npmjs.com/package/@vitest/browser-playwright) 4 + 5 + Run your Vitest [browser tests](https://vitest.dev/guide/browser/) using [playwright](https://playwright.dev/docs/api/class-playwright) API. Note that Vitest does not use playwright as a test runner, but only as a browser provider. 6 + 7 + We recommend using this package if you are already using playwright in your project or if you do not have any E2E tests yet. 8 + 9 + ## Installation 10 + 11 + Install the package with your favorite package manager: 12 + 13 + ```sh 14 + npm install -D @vitest/browser-playwright 15 + # or 16 + yarn add -D @vitest/browser-playwright 17 + # or 18 + pnpm add -D @vitest/browser-playwright 19 + ``` 20 + 21 + Then specify it in the `browser.provider` field of your Vitest configuration: 22 + 23 + ```ts 24 + // vitest.config.ts 25 + import { defineConfig } from 'vitest/config' 26 + import { playwright } from '@vitest/browser-playwright' 27 + 28 + export default defineConfig({ 29 + test: { 30 + browser: { 31 + provider: playwright({ 32 + // ...custom playwright options 33 + }), 34 + instances: [ 35 + { name: 'chromium' }, 36 + ], 37 + }, 38 + }, 39 + }) 40 + ``` 41 + 42 + Then run Vitest in the browser mode: 43 + 44 + ```sh 45 + npx vitest --browser 46 + ``` 47 + 48 + [GitHub](https://github.com/vitest-dev/vitest/tree/main/packages/browser-playwright) | [Documentation](https://vitest.dev/guide/browser/playwright)
+1
packages/browser-playwright/context.d.ts
··· 1 + export * from '@vitest/browser/context'
+58
packages/browser-playwright/package.json
··· 1 + { 2 + "name": "@vitest/browser-playwright", 3 + "type": "module", 4 + "version": "4.0.0-beta.13", 5 + "description": "Browser running for Vitest using playwright", 6 + "license": "MIT", 7 + "funding": "https://opencollective.com/vitest", 8 + "homepage": "https://vitest.dev/guide/browser/playwright", 9 + "repository": { 10 + "type": "git", 11 + "url": "git+https://github.com/vitest-dev/vitest.git", 12 + "directory": "packages/browser-playwright" 13 + }, 14 + "bugs": { 15 + "url": "https://github.com/vitest-dev/vitest/issues" 16 + }, 17 + "sideEffects": false, 18 + "exports": { 19 + ".": { 20 + "types": "./dist/index.d.ts", 21 + "default": "./dist/index.js" 22 + }, 23 + "./context": { 24 + "types": "./context.d.ts" 25 + }, 26 + "./package.json": "./package.json" 27 + }, 28 + "main": "./dist/index.js", 29 + "module": "./dist/index.js", 30 + "types": "./dist/index.d.ts", 31 + "files": [ 32 + "context.d.ts", 33 + "dist" 34 + ], 35 + "scripts": { 36 + "build": "premove dist && pnpm rollup -c", 37 + "dev": "rollup -c --watch --watch.include 'src/**'" 38 + }, 39 + "peerDependencies": { 40 + "playwright": "*", 41 + "vitest": "workspace:*" 42 + }, 43 + "peerDependenciesMeta": { 44 + "playwright": { 45 + "optional": false 46 + } 47 + }, 48 + "dependencies": { 49 + "@vitest/browser": "workspace:*", 50 + "@vitest/mocker": "workspace:*", 51 + "tinyrainbow": "catalog:" 52 + }, 53 + "devDependencies": { 54 + "playwright": "^1.55.0", 55 + "playwright-core": "^1.55.0", 56 + "vitest": "workspace:*" 57 + } 58 + }
+62
packages/browser-playwright/rollup.config.js
··· 1 + import { createRequire } from 'node:module' 2 + import commonjs from '@rollup/plugin-commonjs' 3 + import json from '@rollup/plugin-json' 4 + import resolve from '@rollup/plugin-node-resolve' 5 + import { defineConfig } from 'rollup' 6 + import oxc from 'unplugin-oxc/rollup' 7 + import { createDtsUtils } from '../../scripts/build-utils.js' 8 + 9 + const require = createRequire(import.meta.url) 10 + const pkg = require('./package.json') 11 + 12 + const external = [ 13 + ...Object.keys(pkg.dependencies), 14 + ...Object.keys(pkg.peerDependencies || {}), 15 + /^@?vitest(\/|$)/, 16 + 'vite', 17 + 'playwright-core/types/protocol', 18 + ] 19 + 20 + const dtsUtils = createDtsUtils() 21 + 22 + const plugins = [ 23 + resolve({ 24 + preferBuiltins: true, 25 + }), 26 + json(), 27 + commonjs(), 28 + oxc({ 29 + transform: { target: 'node18' }, 30 + }), 31 + ] 32 + 33 + export default () => 34 + defineConfig([ 35 + { 36 + input: { 37 + index: './src/index.ts', 38 + locators: './src/locators.ts', 39 + }, 40 + output: { 41 + dir: 'dist', 42 + format: 'esm', 43 + }, 44 + external, 45 + context: 'null', 46 + plugins: [ 47 + ...dtsUtils.isolatedDecl(), 48 + ...plugins, 49 + ], 50 + }, 51 + { 52 + input: dtsUtils.dtsInput('src/index.ts'), 53 + output: { 54 + dir: 'dist', 55 + entryFileNames: '[name].d.ts', 56 + format: 'esm', 57 + }, 58 + watch: false, 59 + external, 60 + plugins: dtsUtils.dts(), 61 + }, 62 + ])
+11
packages/browser-playwright/src/commands/clear.ts
··· 1 + import type { UserEvent } from 'vitest/browser' 2 + import type { UserEventCommand } from './utils' 3 + 4 + export const clear: UserEventCommand<UserEvent['clear']> = async ( 5 + context, 6 + selector, 7 + ) => { 8 + const { iframe } = context 9 + const element = iframe.locator(selector) 10 + await element.clear() 11 + }
+32
packages/browser-playwright/src/commands/click.ts
··· 1 + import type { UserEvent } from 'vitest/browser' 2 + import type { UserEventCommand } from './utils' 3 + 4 + export const click: UserEventCommand<UserEvent['click']> = async ( 5 + context, 6 + selector, 7 + options = {}, 8 + ) => { 9 + const tester = context.iframe 10 + await tester.locator(selector).click(options) 11 + } 12 + 13 + export const dblClick: UserEventCommand<UserEvent['dblClick']> = async ( 14 + context, 15 + selector, 16 + options = {}, 17 + ) => { 18 + const tester = context.iframe 19 + await tester.locator(selector).dblclick(options) 20 + } 21 + 22 + export const tripleClick: UserEventCommand<UserEvent['tripleClick']> = async ( 23 + context, 24 + selector, 25 + options = {}, 26 + ) => { 27 + const tester = context.iframe 28 + await tester.locator(selector).click({ 29 + ...options, 30 + clickCount: 3, 31 + }) 32 + }
+16
packages/browser-playwright/src/commands/dragAndDrop.ts
··· 1 + import type { UserEvent } from 'vitest/browser' 2 + import type { UserEventCommand } from './utils' 3 + 4 + export const dragAndDrop: UserEventCommand<UserEvent['dragAndDrop']> = async ( 5 + context, 6 + source, 7 + target, 8 + options_, 9 + ) => { 10 + const frame = await context.frame() 11 + await frame.dragAndDrop( 12 + source, 13 + target, 14 + options_, 15 + ) 16 + }
+13
packages/browser-playwright/src/commands/fill.ts
··· 1 + import type { UserEvent } from 'vitest/browser' 2 + import type { UserEventCommand } from './utils' 3 + 4 + export const fill: UserEventCommand<UserEvent['fill']> = async ( 5 + context, 6 + selector, 7 + text, 8 + options = {}, 9 + ) => { 10 + const { iframe } = context 11 + const element = iframe.locator(selector) 12 + await element.fill(text, options) 13 + }
+10
packages/browser-playwright/src/commands/hover.ts
··· 1 + import type { UserEvent } from 'vitest/browser' 2 + import type { UserEventCommand } from './utils' 3 + 4 + export const hover: UserEventCommand<UserEvent['hover']> = async ( 5 + context, 6 + selector, 7 + options = {}, 8 + ) => { 9 + await context.iframe.locator(selector).hover(options) 10 + }
+40
packages/browser-playwright/src/commands/index.ts
··· 1 + import { clear } from './clear' 2 + import { click, dblClick, tripleClick } from './click' 3 + import { dragAndDrop } from './dragAndDrop' 4 + import { fill } from './fill' 5 + import { hover } from './hover' 6 + import { keyboard, keyboardCleanup } from './keyboard' 7 + import { takeScreenshot } from './screenshot' 8 + import { selectOptions } from './select' 9 + import { tab } from './tab' 10 + import { 11 + annotateTraces, 12 + deleteTracing, 13 + startChunkTrace, 14 + startTracing, 15 + stopChunkTrace, 16 + } from './trace' 17 + import { type } from './type' 18 + import { upload } from './upload' 19 + 20 + export default { 21 + __vitest_upload: upload as typeof upload, 22 + __vitest_click: click as typeof click, 23 + __vitest_dblClick: dblClick as typeof dblClick, 24 + __vitest_tripleClick: tripleClick as typeof tripleClick, 25 + __vitest_takeScreenshot: takeScreenshot as typeof takeScreenshot, 26 + __vitest_type: type as typeof type, 27 + __vitest_clear: clear as typeof clear, 28 + __vitest_fill: fill as typeof fill, 29 + __vitest_tab: tab as typeof tab, 30 + __vitest_keyboard: keyboard as typeof keyboard, 31 + __vitest_selectOptions: selectOptions as typeof selectOptions, 32 + __vitest_dragAndDrop: dragAndDrop as typeof dragAndDrop, 33 + __vitest_hover: hover as typeof hover, 34 + __vitest_cleanup: keyboardCleanup as typeof keyboardCleanup, 35 + __vitest_deleteTracing: deleteTracing as typeof deleteTracing, 36 + __vitest_startChunkTrace: startChunkTrace as typeof startChunkTrace, 37 + __vitest_startTracing: startTracing as typeof startTracing, 38 + __vitest_stopChunkTrace: stopChunkTrace as typeof stopChunkTrace, 39 + __vitest_annotateTraces: annotateTraces as typeof annotateTraces, 40 + }
+133
packages/browser-playwright/src/commands/keyboard.ts
··· 1 + import type { BrowserProvider } from 'vitest/node' 2 + import type { PlaywrightBrowserProvider } from '../playwright' 3 + import type { UserEventCommand } from './utils' 4 + import { parseKeyDef } from '@vitest/browser' 5 + 6 + export interface KeyboardState { 7 + unreleased: string[] 8 + } 9 + 10 + export const keyboard: UserEventCommand<(text: string, state: KeyboardState) => Promise<{ unreleased: string[] }>> = async ( 11 + context, 12 + text, 13 + state, 14 + ) => { 15 + const frame = await context.frame() 16 + await frame.evaluate(focusIframe) 17 + 18 + const pressed = new Set<string>(state.unreleased) 19 + 20 + await keyboardImplementation( 21 + pressed, 22 + context.provider, 23 + context.sessionId, 24 + text, 25 + async () => { 26 + const frame = await context.frame() 27 + await frame.evaluate(selectAll) 28 + }, 29 + true, 30 + ) 31 + 32 + return { 33 + unreleased: Array.from(pressed), 34 + } 35 + } 36 + 37 + export const keyboardCleanup: UserEventCommand<(state: KeyboardState) => Promise<void>> = async ( 38 + context, 39 + state, 40 + ) => { 41 + const { provider, sessionId } = context 42 + if (!state.unreleased) { 43 + return 44 + } 45 + const page = (provider as PlaywrightBrowserProvider).getPage(sessionId) 46 + for (const key of state.unreleased) { 47 + await page.keyboard.up(key) 48 + } 49 + } 50 + 51 + // fallback to insertText for non US key 52 + // https://github.com/microsoft/playwright/blob/50775698ae13642742f2a1e8983d1d686d7f192d/packages/playwright-core/src/server/input.ts#L95 53 + const VALID_KEYS = new Set(['Escape', 'F1', 'F2', 'F3', 'F4', 'F5', 'F6', 'F7', 'F8', 'F9', 'F10', 'F11', 'F12', 'Backquote', '`', '~', 'Digit1', '1', '!', 'Digit2', '2', '@', 'Digit3', '3', '#', 'Digit4', '4', '$', 'Digit5', '5', '%', 'Digit6', '6', '^', 'Digit7', '7', '&', 'Digit8', '8', '*', 'Digit9', '9', '(', 'Digit0', '0', ')', 'Minus', '-', '_', 'Equal', '=', '+', 'Backslash', '\\', '|', 'Backspace', 'Tab', 'KeyQ', 'q', 'Q', 'KeyW', 'w', 'W', 'KeyE', 'e', 'E', 'KeyR', 'r', 'R', 'KeyT', 't', 'T', 'KeyY', 'y', 'Y', 'KeyU', 'u', 'U', 'KeyI', 'i', 'I', 'KeyO', 'o', 'O', 'KeyP', 'p', 'P', 'BracketLeft', '[', '{', 'BracketRight', ']', '}', 'CapsLock', 'KeyA', 'a', 'A', 'KeyS', 's', 'S', 'KeyD', 'd', 'D', 'KeyF', 'f', 'F', 'KeyG', 'g', 'G', 'KeyH', 'h', 'H', 'KeyJ', 'j', 'J', 'KeyK', 'k', 'K', 'KeyL', 'l', 'L', 'Semicolon', ';', ':', 'Quote', '\'', '"', 'Enter', '\n', '\r', 'ShiftLeft', 'Shift', 'KeyZ', 'z', 'Z', 'KeyX', 'x', 'X', 'KeyC', 'c', 'C', 'KeyV', 'v', 'V', 'KeyB', 'b', 'B', 'KeyN', 'n', 'N', 'KeyM', 'm', 'M', 'Comma', ',', '<', 'Period', '.', '>', 'Slash', '/', '?', 'ShiftRight', 'ControlLeft', 'Control', 'MetaLeft', 'Meta', 'AltLeft', 'Alt', 'Space', ' ', 'AltRight', 'AltGraph', 'MetaRight', 'ContextMenu', 'ControlRight', 'PrintScreen', 'ScrollLock', 'Pause', 'PageUp', 'PageDown', 'Insert', 'Delete', 'Home', 'End', 'ArrowLeft', 'ArrowUp', 'ArrowRight', 'ArrowDown', 'NumLock', 'NumpadDivide', 'NumpadMultiply', 'NumpadSubtract', 'Numpad7', 'Numpad8', 'Numpad9', 'Numpad4', 'Numpad5', 'Numpad6', 'NumpadAdd', 'Numpad1', 'Numpad2', 'Numpad3', 'Numpad0', 'NumpadDecimal', 'NumpadEnter', 'ControlOrMeta']) 54 + 55 + export async function keyboardImplementation( 56 + pressed: Set<string>, 57 + provider: BrowserProvider, 58 + sessionId: string, 59 + text: string, 60 + selectAll: () => Promise<void>, 61 + skipRelease: boolean, 62 + ): Promise<{ pressed: Set<string> }> { 63 + const page = (provider as PlaywrightBrowserProvider).getPage(sessionId) 64 + const actions = parseKeyDef(text) 65 + 66 + for (const { releasePrevious, releaseSelf, repeat, keyDef } of actions) { 67 + const key = keyDef.key! 68 + 69 + // TODO: instead of calling down/up for each key, join non special 70 + // together, and call `type` once for all non special keys, 71 + // and then `press` for special keys 72 + if (pressed.has(key)) { 73 + if (VALID_KEYS.has(key)) { 74 + await page.keyboard.up(key) 75 + } 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 + if (VALID_KEYS.has(key)) { 87 + await page.keyboard.down(key) 88 + } 89 + else { 90 + await page.keyboard.insertText(key) 91 + } 92 + } 93 + 94 + if (releaseSelf) { 95 + if (VALID_KEYS.has(key)) { 96 + await page.keyboard.up(key) 97 + } 98 + } 99 + else { 100 + pressed.add(key) 101 + } 102 + } 103 + } 104 + 105 + if (!skipRelease && pressed.size) { 106 + for (const key of pressed) { 107 + if (VALID_KEYS.has(key)) { 108 + await page.keyboard.up(key) 109 + } 110 + } 111 + } 112 + 113 + return { 114 + pressed, 115 + } 116 + } 117 + 118 + function focusIframe() { 119 + if ( 120 + !document.activeElement 121 + || document.activeElement.ownerDocument !== document 122 + || document.activeElement === document.body 123 + ) { 124 + window.focus() 125 + } 126 + } 127 + 128 + function selectAll() { 129 + const element = document.activeElement as HTMLInputElement 130 + if (element && typeof element.select === 'function') { 131 + element.select() 132 + } 133 + }
+63
packages/browser-playwright/src/commands/screenshot.ts
··· 1 + import type { ScreenshotOptions } from 'vitest/browser' 2 + import type { BrowserCommandContext } from 'vitest/node' 3 + import { mkdir } from 'node:fs/promises' 4 + import { resolveScreenshotPath } from '@vitest/browser' 5 + import { dirname, normalize } from 'pathe' 6 + 7 + interface ScreenshotCommandOptions extends Omit<ScreenshotOptions, 'element' | 'mask'> { 8 + element?: string 9 + mask?: readonly string[] 10 + } 11 + /** 12 + * Takes a screenshot using the provided browser context and returns a buffer and the expected screenshot path. 13 + * 14 + * **Note**: the returned `path` indicates where the screenshot *might* be found. 15 + * It is not guaranteed to exist, especially if `options.save` is `false`. 16 + * 17 + * @throws {Error} If the function is not called within a test or if the browser provider does not support screenshots. 18 + */ 19 + export async function takeScreenshot( 20 + context: BrowserCommandContext, 21 + name: string, 22 + options: Omit<ScreenshotCommandOptions, 'base64'>, 23 + ): Promise<{ buffer: Buffer<ArrayBufferLike>; path: string }> { 24 + if (!context.testPath) { 25 + throw new Error(`Cannot take a screenshot without a test path`) 26 + } 27 + 28 + const path = resolveScreenshotPath( 29 + context.testPath, 30 + name, 31 + context.project.config, 32 + options.path, 33 + ) 34 + 35 + // playwright does not need a screenshot path if we don't intend to save it 36 + let savePath: string | undefined 37 + 38 + if (options.save) { 39 + savePath = normalize(path) 40 + 41 + await mkdir(dirname(savePath), { recursive: true }) 42 + } 43 + 44 + const mask = options.mask?.map(selector => context.iframe.locator(selector)) 45 + 46 + if (options.element) { 47 + const { element: selector, ...config } = options 48 + const element = context.iframe.locator(selector) 49 + const buffer = await element.screenshot({ 50 + ...config, 51 + mask, 52 + path: savePath, 53 + }) 54 + return { buffer, path } 55 + } 56 + 57 + const buffer = await context.iframe.locator('body').screenshot({ 58 + ...options, 59 + mask, 60 + path: savePath, 61 + }) 62 + return { buffer, path } 63 + }
+27
packages/browser-playwright/src/commands/select.ts
··· 1 + import type { ElementHandle } from 'playwright' 2 + import type { UserEvent } from 'vitest/browser' 3 + import type { UserEventCommand } from './utils' 4 + 5 + export const selectOptions: UserEventCommand<UserEvent['selectOptions']> = async ( 6 + context, 7 + selector, 8 + userValues, 9 + options = {}, 10 + ) => { 11 + const value = userValues as any as (string | { element: string })[] 12 + const { iframe } = context 13 + const selectElement = iframe.locator(selector) 14 + 15 + const values = await Promise.all(value.map(async (v) => { 16 + if (typeof v === 'string') { 17 + return v 18 + } 19 + const elementHandler = await iframe.locator(v.element).elementHandle() 20 + if (!elementHandler) { 21 + throw new Error(`Element not found: ${v.element}`) 22 + } 23 + return elementHandler 24 + })) as (readonly string[]) | (readonly ElementHandle[]) 25 + 26 + await selectElement.selectOption(values, options) 27 + }
+10
packages/browser-playwright/src/commands/tab.ts
··· 1 + import type { UserEvent } from 'vitest/browser' 2 + import type { UserEventCommand } from './utils' 3 + 4 + export const tab: UserEventCommand<UserEvent['tab']> = async ( 5 + context, 6 + options = {}, 7 + ) => { 8 + const page = context.page 9 + await page.keyboard.press(options.shift === true ? 'Shift+Tab' : 'Tab') 10 + }
+33
packages/browser-playwright/src/commands/type.ts
··· 1 + import type { UserEvent } from 'vitest/browser' 2 + import type { UserEventCommand } from './utils' 3 + import { keyboardImplementation } from './keyboard' 4 + 5 + export const type: UserEventCommand<UserEvent['type']> = async ( 6 + context, 7 + selector, 8 + text, 9 + options = {}, 10 + ) => { 11 + const { skipClick = false, skipAutoClose = false } = options 12 + const unreleased = new Set(Reflect.get(options, 'unreleased') as string[] ?? []) 13 + 14 + const { iframe } = context 15 + const element = iframe.locator(selector) 16 + 17 + if (!skipClick) { 18 + await element.focus() 19 + } 20 + 21 + await keyboardImplementation( 22 + unreleased, 23 + context.provider, 24 + context.sessionId, 25 + text, 26 + () => element.selectText(), 27 + skipAutoClose, 28 + ) 29 + 30 + return { 31 + unreleased: Array.from(unreleased), 32 + } 33 + }
+33
packages/browser-playwright/src/commands/upload.ts
··· 1 + import type { UserEventUploadOptions } from 'vitest/browser' 2 + import type { UserEventCommand } from './utils' 3 + import { resolve } from 'pathe' 4 + 5 + export const upload: UserEventCommand<(element: string, files: Array<string | { 6 + name: string 7 + mimeType: string 8 + base64: string 9 + }>, options: UserEventUploadOptions) => void> = async ( 10 + context, 11 + selector, 12 + files, 13 + options, 14 + ) => { 15 + const testPath = context.testPath 16 + if (!testPath) { 17 + throw new Error(`Cannot upload files outside of a test`) 18 + } 19 + const root = context.project.config.root 20 + 21 + const { iframe } = context 22 + const playwrightFiles = files.map((file) => { 23 + if (typeof file === 'string') { 24 + return resolve(root, file) 25 + } 26 + return { 27 + name: file.name, 28 + mimeType: file.mimeType, 29 + buffer: Buffer.from(file.base64, 'base64'), 30 + } 31 + }) 32 + await iframe.locator(selector).setInputFiles(playwrightFiles as string[], options) 33 + }
+5
packages/browser-playwright/src/constants.ts
··· 1 + import { fileURLToPath } from 'node:url' 2 + import { resolve } from 'pathe' 3 + 4 + const pkgRoot = resolve(fileURLToPath(import.meta.url), '../..') 5 + export const distRoot: string = resolve(pkgRoot, 'dist')
+6
packages/browser-playwright/src/index.ts
··· 1 + export { 2 + playwright, 3 + PlaywrightBrowserProvider, 4 + type PlaywrightProviderOptions, 5 + } from './playwright' 6 + export { defineBrowserCommand } from '@vitest/browser'
+13
packages/browser-playwright/tsconfig.json
··· 1 + { 2 + "extends": "../../tsconfig.base.json", 3 + "compilerOptions": { 4 + "types": ["node", "vite/client"], 5 + "isolatedDeclarations": true 6 + }, 7 + "exclude": [ 8 + "dist", 9 + "node_modules", 10 + "**/vite.config.ts", 11 + "src/client/**/*.ts" 12 + ] 13 + }
+49
packages/browser-preview/README.md
··· 1 + # @vitest/browser-preview 2 + 3 + [![NPM version](https://img.shields.io/npm/v/@vitest/browser-preview?color=a1b858&label=)](https://www.npmjs.com/package/@vitest/browser-preview) 4 + 5 + See how your tests look like in a real browser. For proper and stable browser testing, we recommend running tests in a headless browser in your CI instead. For this, you should use either: 6 + 7 + - [@vitest/browser-playwright](https://www.npmjs.com/package/@vitest/browser-playwright) - run tests using [playwright](https://playwright.dev/) 8 + - [@vitest/browser-webdriverio](https://www.npmjs.com/package/@vitest/browser-webdriverio) - run tests using [webdriverio](https://webdriver.io/) 9 + 10 + ## Installation 11 + 12 + Install the package with your favorite package manager: 13 + 14 + ```sh 15 + npm install -D @vitest/browser-preview 16 + # or 17 + yarn add -D @vitest/browser-preview 18 + # or 19 + pnpm add -D @vitest/browser-preview 20 + ``` 21 + 22 + Then specify it in the `browser.provider` field of your Vitest configuration: 23 + 24 + ```ts 25 + // vitest.config.ts 26 + import { defineConfig } from 'vitest/config' 27 + import { preview } from '@vitest/browser-preview' 28 + 29 + export default defineConfig({ 30 + test: { 31 + browser: { 32 + provider: preview(), 33 + instances: [ 34 + { name: 'chromium' }, 35 + ], 36 + }, 37 + }, 38 + }) 39 + ``` 40 + 41 + Then run Vitest in the browser mode: 42 + 43 + ```sh 44 + npx vitest --browser 45 + ``` 46 + 47 + If browser didn't open automatically, follow the link in the terminal to open the browser preview. 48 + 49 + [GitHub](https://github.com/vitest-dev/vitest/tree/main/packages/browser-preview) | [Documentation](https://vitest.dev/guide/browser/)
+1
packages/browser-preview/context.d.ts
··· 1 + export * from '@vitest/browser/context'
+49
packages/browser-preview/package.json
··· 1 + { 2 + "name": "@vitest/browser-preview", 3 + "type": "module", 4 + "version": "4.0.0-beta.13", 5 + "description": "Browser running for Vitest using your browser of choice", 6 + "license": "MIT", 7 + "funding": "https://opencollective.com/vitest", 8 + "homepage": "https://vitest.dev/guide/browser", 9 + "repository": { 10 + "type": "git", 11 + "url": "git+https://github.com/vitest-dev/vitest.git", 12 + "directory": "packages/browser-preview" 13 + }, 14 + "bugs": { 15 + "url": "https://github.com/vitest-dev/vitest/issues" 16 + }, 17 + "sideEffects": false, 18 + "exports": { 19 + ".": { 20 + "types": "./dist/index.d.ts", 21 + "default": "./dist/index.js" 22 + }, 23 + "./context": { 24 + "types": "./context.d.ts" 25 + }, 26 + "./package.json": "./package.json" 27 + }, 28 + "main": "./dist/index.js", 29 + "module": "./dist/index.js", 30 + "types": "./dist/index.d.ts", 31 + "files": [ 32 + "dist" 33 + ], 34 + "scripts": { 35 + "build": "premove dist && pnpm rollup -c", 36 + "dev": "rollup -c --watch --watch.include 'src/**'" 37 + }, 38 + "peerDependencies": { 39 + "vitest": "workspace:*" 40 + }, 41 + "dependencies": { 42 + "@testing-library/dom": "^10.4.1", 43 + "@testing-library/user-event": "^14.6.1", 44 + "@vitest/browser": "workspace:*" 45 + }, 46 + "devDependencies": { 47 + "vitest": "workspace:*" 48 + } 49 + }
+60
packages/browser-preview/rollup.config.js
··· 1 + import { createRequire } from 'node:module' 2 + import commonjs from '@rollup/plugin-commonjs' 3 + import json from '@rollup/plugin-json' 4 + import resolve from '@rollup/plugin-node-resolve' 5 + import { defineConfig } from 'rollup' 6 + import oxc from 'unplugin-oxc/rollup' 7 + import { createDtsUtils } from '../../scripts/build-utils.js' 8 + 9 + const require = createRequire(import.meta.url) 10 + const pkg = require('./package.json') 11 + 12 + const external = [ 13 + ...Object.keys(pkg.dependencies), 14 + ...Object.keys(pkg.peerDependencies || {}), 15 + /^@?vitest(\/|$)/, 16 + ] 17 + 18 + const dtsUtils = createDtsUtils() 19 + 20 + const plugins = [ 21 + resolve({ 22 + preferBuiltins: true, 23 + }), 24 + json(), 25 + commonjs(), 26 + oxc({ 27 + transform: { target: 'node18' }, 28 + }), 29 + ] 30 + 31 + export default () => 32 + defineConfig([ 33 + { 34 + input: { 35 + index: './src/index.ts', 36 + locators: './src/locators.ts', 37 + }, 38 + output: { 39 + dir: 'dist', 40 + format: 'esm', 41 + }, 42 + external, 43 + context: 'null', 44 + plugins: [ 45 + ...dtsUtils.isolatedDecl(), 46 + ...plugins, 47 + ], 48 + }, 49 + { 50 + input: dtsUtils.dtsInput('src/index.ts'), 51 + output: { 52 + dir: 'dist', 53 + entryFileNames: '[name].d.ts', 54 + format: 'esm', 55 + }, 56 + watch: false, 57 + external, 58 + plugins: dtsUtils.dts(), 59 + }, 60 + ])
+5
packages/browser-preview/src/constants.ts
··· 1 + import { fileURLToPath } from 'node:url' 2 + import { resolve } from 'pathe' 3 + 4 + const pkgRoot = resolve(fileURLToPath(import.meta.url), '../..') 5 + export const distRoot: string = resolve(pkgRoot, 'dist')
+5
packages/browser-preview/src/index.ts
··· 1 + export { 2 + preview, 3 + PreviewBrowserProvider, 4 + } from './preview' 5 + export { defineBrowserCommand } from '@vitest/browser'
+13
packages/browser-preview/tsconfig.json
··· 1 + { 2 + "extends": "../../tsconfig.base.json", 3 + "compilerOptions": { 4 + "types": ["node", "vite/client"], 5 + "isolatedDeclarations": true 6 + }, 7 + "exclude": [ 8 + "dist", 9 + "node_modules", 10 + "**/vite.config.ts", 11 + "src/client/**/*.ts" 12 + ] 13 + }
+48
packages/browser-webdriverio/README.md
··· 1 + # @vitest/browser-webdriverio 2 + 3 + [![NPM version](https://img.shields.io/npm/v/@vitest/browser-webdriverio?color=a1b858&label=)](https://www.npmjs.com/package/@vitest/browser-webdriverio) 4 + 5 + Run your Vitest [browser tests](https://vitest.dev/guide/browser/) using [webdriverio](https://webdriver.io/docs/api/browser) API. Note that Vitest does not use webdriverio as a test runner, but only as a browser provider. 6 + 7 + We recommend using this package if you are already using webdriverio in your project. 8 + 9 + ## Installation 10 + 11 + Install the package with your favorite package manager: 12 + 13 + ```sh 14 + npm install -D @vitest/browser-webdriverio 15 + # or 16 + yarn add -D @vitest/browser-webdriverio 17 + # or 18 + pnpm add -D @vitest/browser-webdriverio 19 + ``` 20 + 21 + Then specify it in the `browser.provider` field of your Vitest configuration: 22 + 23 + ```ts 24 + // vitest.config.ts 25 + import { defineConfig } from 'vitest/config' 26 + import { webdriverio } from '@vitest/browser-webdriverio' 27 + 28 + export default defineConfig({ 29 + test: { 30 + browser: { 31 + provider: webdriverio({ 32 + // ...custom webdriverio options 33 + }), 34 + instances: [ 35 + { name: 'chromium' }, 36 + ], 37 + }, 38 + }, 39 + }) 40 + ``` 41 + 42 + Then run Vitest in the browser mode: 43 + 44 + ```sh 45 + npx vitest --browser 46 + ``` 47 + 48 + [GitHub](https://github.com/vitest-dev/vitest/tree/main/packages/browser-webdriverio) | [Documentation](https://vitest.dev/guide/browser/webdriverio)
+1
packages/browser-webdriverio/context.d.ts
··· 1 + export * from '@vitest/browser/context'
+55
packages/browser-webdriverio/package.json
··· 1 + { 2 + "name": "@vitest/browser-webdriverio", 3 + "type": "module", 4 + "version": "4.0.0-beta.13", 5 + "description": "Browser running for Vitest using webdriverio", 6 + "license": "MIT", 7 + "funding": "https://opencollective.com/vitest", 8 + "homepage": "https://vitest.dev/guide/browser/webdriverio", 9 + "repository": { 10 + "type": "git", 11 + "url": "git+https://github.com/vitest-dev/vitest.git", 12 + "directory": "packages/browser-webdriverio" 13 + }, 14 + "bugs": { 15 + "url": "https://github.com/vitest-dev/vitest/issues" 16 + }, 17 + "sideEffects": false, 18 + "exports": { 19 + ".": { 20 + "types": "./dist/index.d.ts", 21 + "default": "./dist/index.js" 22 + }, 23 + "./context": { 24 + "types": "./context.d.ts" 25 + }, 26 + "./package.json": "./package.json" 27 + }, 28 + "main": "./dist/index.js", 29 + "module": "./dist/index.js", 30 + "types": "./dist/index.d.ts", 31 + "files": [ 32 + "dist" 33 + ], 34 + "scripts": { 35 + "build": "premove dist && pnpm rollup -c", 36 + "dev": "rollup -c --watch --watch.include 'src/**'" 37 + }, 38 + "peerDependencies": { 39 + "vitest": "workspace:*", 40 + "webdriverio": "*" 41 + }, 42 + "peerDependenciesMeta": { 43 + "webdriverio": { 44 + "optional": false 45 + } 46 + }, 47 + "dependencies": { 48 + "@vitest/browser": "workspace:*" 49 + }, 50 + "devDependencies": { 51 + "@wdio/types": "^9.19.2", 52 + "vitest": "workspace:*", 53 + "webdriverio": "^9.19.2" 54 + } 55 + }
+60
packages/browser-webdriverio/rollup.config.js
··· 1 + import { createRequire } from 'node:module' 2 + import commonjs from '@rollup/plugin-commonjs' 3 + import json from '@rollup/plugin-json' 4 + import resolve from '@rollup/plugin-node-resolve' 5 + import { defineConfig } from 'rollup' 6 + import oxc from 'unplugin-oxc/rollup' 7 + import { createDtsUtils } from '../../scripts/build-utils.js' 8 + 9 + const require = createRequire(import.meta.url) 10 + const pkg = require('./package.json') 11 + 12 + const external = [ 13 + ...Object.keys(pkg.dependencies), 14 + ...Object.keys(pkg.peerDependencies || {}), 15 + /^@?vitest(\/|$)/, 16 + ] 17 + 18 + const dtsUtils = createDtsUtils() 19 + 20 + const plugins = [ 21 + resolve({ 22 + preferBuiltins: true, 23 + }), 24 + json(), 25 + commonjs(), 26 + oxc({ 27 + transform: { target: 'node18' }, 28 + }), 29 + ] 30 + 31 + export default () => 32 + defineConfig([ 33 + { 34 + input: { 35 + index: './src/index.ts', 36 + locators: './src/locators.ts', 37 + }, 38 + output: { 39 + dir: 'dist', 40 + format: 'esm', 41 + }, 42 + external, 43 + context: 'null', 44 + plugins: [ 45 + ...dtsUtils.isolatedDecl(), 46 + ...plugins, 47 + ], 48 + }, 49 + { 50 + input: dtsUtils.dtsInput('src/index.ts'), 51 + output: { 52 + dir: 'dist', 53 + entryFileNames: '[name].d.ts', 54 + format: 'esm', 55 + }, 56 + watch: false, 57 + external, 58 + plugins: dtsUtils.dts(), 59 + }, 60 + ])
+10
packages/browser-webdriverio/src/commands/clear.ts
··· 1 + import type { UserEvent } from 'vitest/browser' 2 + import type { UserEventCommand } from './utils' 3 + 4 + export const clear: UserEventCommand<UserEvent['clear']> = async ( 5 + context, 6 + selector, 7 + ) => { 8 + const browser = context.browser 9 + await browser.$(selector).clearValue() 10 + }
+44
packages/browser-webdriverio/src/commands/click.ts
··· 1 + import type { UserEvent } from 'vitest/browser' 2 + import type { UserEventCommand } from './utils' 3 + 4 + export const click: UserEventCommand<UserEvent['click']> = async ( 5 + context, 6 + selector, 7 + options = {}, 8 + ) => { 9 + const browser = context.browser 10 + await browser.$(selector).click(options as any) 11 + } 12 + 13 + export const dblClick: UserEventCommand<UserEvent['dblClick']> = async ( 14 + context, 15 + selector, 16 + _options = {}, 17 + ) => { 18 + const browser = context.browser 19 + await browser.$(selector).doubleClick() 20 + } 21 + 22 + export const tripleClick: UserEventCommand<UserEvent['tripleClick']> = async ( 23 + context, 24 + selector, 25 + _options = {}, 26 + ) => { 27 + const browser = context.browser 28 + await browser 29 + .action('pointer', { parameters: { pointerType: 'mouse' } }) 30 + // move the pointer over the button 31 + .move({ origin: browser.$(selector) }) 32 + // simulate 3 clicks 33 + .down() 34 + .up() 35 + .pause(50) 36 + .down() 37 + .up() 38 + .pause(50) 39 + .down() 40 + .up() 41 + .pause(50) 42 + // run the sequence 43 + .perform() 44 + }
+27
packages/browser-webdriverio/src/commands/dragAndDrop.ts
··· 1 + import type { UserEvent } from 'vitest/browser' 2 + import type { UserEventCommand } from './utils' 3 + 4 + export const dragAndDrop: UserEventCommand<UserEvent['dragAndDrop']> = async ( 5 + context, 6 + source, 7 + target, 8 + options_, 9 + ) => { 10 + const $source = context.browser.$(source) 11 + const $target = context.browser.$(target) 12 + const options = (options_ || {}) as any 13 + const duration = options.duration ?? 10 14 + 15 + // https://github.com/webdriverio/webdriverio/issues/8022#issuecomment-1700919670 16 + await context.browser 17 + .action('pointer') 18 + .move({ duration: 0, origin: $source, x: options.sourceX ?? 0, y: options.sourceY ?? 0 }) 19 + .down({ button: 0 }) 20 + .move({ duration: 0, origin: 'pointer', x: 0, y: 0 }) 21 + .pause(duration) 22 + .move({ duration: 0, origin: $target, x: options.targetX ?? 0, y: options.targetY ?? 0 }) 23 + .move({ duration: 0, origin: 'pointer', x: 1, y: 0 }) 24 + .move({ duration: 0, origin: 'pointer', x: -1, y: 0 }) 25 + .up({ button: 0 }) 26 + .perform() 27 + }
+12
packages/browser-webdriverio/src/commands/fill.ts
··· 1 + import type { UserEvent } from 'vitest/browser' 2 + import type { UserEventCommand } from './utils' 3 + 4 + export const fill: UserEventCommand<UserEvent['fill']> = async ( 5 + context, 6 + selector, 7 + text, 8 + _options = {}, 9 + ) => { 10 + const browser = context.browser 11 + await browser.$(selector).setValue(text) 12 + }
+11
packages/browser-webdriverio/src/commands/hover.ts
··· 1 + import type { UserEvent } from 'vitest/browser' 2 + import type { UserEventCommand } from './utils' 3 + 4 + export const hover: UserEventCommand<UserEvent['hover']> = async ( 5 + context, 6 + selector, 7 + options = {}, 8 + ) => { 9 + const browser = context.browser 10 + await browser.$(selector).moveTo(options as any) 11 + }
+30
packages/browser-webdriverio/src/commands/index.ts
··· 1 + import { clear } from './clear' 2 + import { click, dblClick, tripleClick } from './click' 3 + import { dragAndDrop } from './dragAndDrop' 4 + import { fill } from './fill' 5 + import { hover } from './hover' 6 + import { keyboard, keyboardCleanup } from './keyboard' 7 + import { takeScreenshot } from './screenshot' 8 + import { selectOptions } from './select' 9 + import { tab } from './tab' 10 + import { type } from './type' 11 + import { upload } from './upload' 12 + import { viewport } from './viewport' 13 + 14 + export default { 15 + __vitest_upload: upload as typeof upload, 16 + __vitest_click: click as typeof click, 17 + __vitest_dblClick: dblClick as typeof dblClick, 18 + __vitest_tripleClick: tripleClick as typeof tripleClick, 19 + __vitest_takeScreenshot: takeScreenshot as typeof takeScreenshot, 20 + __vitest_type: type as typeof type, 21 + __vitest_clear: clear as typeof clear, 22 + __vitest_fill: fill as typeof fill, 23 + __vitest_tab: tab as typeof tab, 24 + __vitest_keyboard: keyboard as typeof keyboard, 25 + __vitest_selectOptions: selectOptions as typeof selectOptions, 26 + __vitest_dragAndDrop: dragAndDrop as typeof dragAndDrop, 27 + __vitest_hover: hover as typeof hover, 28 + __vitest_cleanup: keyboardCleanup as typeof keyboardCleanup, 29 + __vitest_viewport: viewport as typeof viewport, 30 + }
+125
packages/browser-webdriverio/src/commands/keyboard.ts
··· 1 + import type { BrowserProvider } from 'vitest/node' 2 + import type { WebdriverBrowserProvider } from '../webdriverio' 3 + import type { UserEventCommand } from './utils' 4 + import { parseKeyDef } from '@vitest/browser' 5 + import { Key } from 'webdriverio' 6 + 7 + export interface KeyboardState { 8 + unreleased: string[] 9 + } 10 + 11 + export const keyboard: UserEventCommand<( 12 + text: string, 13 + state: KeyboardState 14 + ) => Promise<{ unreleased: string[] }>> = async ( 15 + context, 16 + text, 17 + state, 18 + ) => { 19 + await context.browser.execute(focusIframe) 20 + 21 + const pressed = new Set<string>(state.unreleased) 22 + 23 + await keyboardImplementation( 24 + pressed, 25 + context.provider, 26 + context.sessionId, 27 + text, 28 + async () => { 29 + await context.browser.execute(selectAll) 30 + }, 31 + true, 32 + ) 33 + 34 + return { 35 + unreleased: Array.from(pressed), 36 + } 37 + } 38 + 39 + export const keyboardCleanup: UserEventCommand<(state: KeyboardState) => Promise<void>> = async ( 40 + context, 41 + state, 42 + ) => { 43 + if (!state.unreleased) { 44 + return 45 + } 46 + const keyboard = context.browser.action('key') 47 + for (const key of state.unreleased) { 48 + keyboard.up(key) 49 + } 50 + await keyboard.perform() 51 + } 52 + 53 + export async function keyboardImplementation( 54 + pressed: Set<string>, 55 + provider: BrowserProvider, 56 + _sessionId: string, 57 + text: string, 58 + selectAll: () => Promise<void>, 59 + skipRelease: boolean, 60 + ): Promise<{ pressed: Set<string> }> { 61 + const browser = (provider as WebdriverBrowserProvider).browser! 62 + const actions = parseKeyDef(text) 63 + 64 + let keyboard = browser.action('key') 65 + 66 + for (const { releasePrevious, releaseSelf, repeat, keyDef } of actions) { 67 + let key = keyDef.key! 68 + const special = Key[key as 'Shift'] 69 + 70 + if (special) { 71 + key = special 72 + } 73 + 74 + if (pressed.has(key)) { 75 + keyboard.up(key) 76 + pressed.delete(key) 77 + } 78 + 79 + if (!releasePrevious) { 80 + if (key === 'selectall') { 81 + await keyboard.perform() 82 + keyboard = browser.action('key') 83 + await selectAll() 84 + continue 85 + } 86 + 87 + for (let i = 1; i <= repeat; i++) { 88 + keyboard.down(key) 89 + } 90 + 91 + if (releaseSelf) { 92 + keyboard.up(key) 93 + } 94 + else { 95 + pressed.add(key) 96 + } 97 + } 98 + } 99 + 100 + // seems like webdriverio doesn't release keys automatically if skipRelease is true and all events are keyUp 101 + const allRelease = keyboard.toJSON().actions.every(action => action.type === 'keyUp') 102 + 103 + await keyboard.perform(allRelease ? false : skipRelease) 104 + 105 + return { 106 + pressed, 107 + } 108 + } 109 + 110 + function focusIframe() { 111 + if ( 112 + !document.activeElement 113 + || document.activeElement.ownerDocument !== document 114 + || document.activeElement === document.body 115 + ) { 116 + window.focus() 117 + } 118 + } 119 + 120 + function selectAll() { 121 + const element = document.activeElement as HTMLInputElement 122 + if (element && typeof element.select === 'function') { 123 + element.select() 124 + } 125 + }
+70
packages/browser-webdriverio/src/commands/screenshot.ts
··· 1 + import type { ScreenshotOptions } from 'vitest/browser' 2 + import type { BrowserCommandContext } from 'vitest/node' 3 + import crypto from 'node:crypto' 4 + import { mkdir, rm } from 'node:fs/promises' 5 + import { normalize as platformNormalize } from 'node:path' 6 + import { resolveScreenshotPath } from '@vitest/browser' 7 + import { dirname, normalize, resolve } from 'pathe' 8 + 9 + interface ScreenshotCommandOptions extends Omit<ScreenshotOptions, 'element' | 'mask'> { 10 + element?: string 11 + mask?: readonly string[] 12 + } 13 + 14 + /** 15 + * Takes a screenshot using the provided browser context and returns a buffer and the expected screenshot path. 16 + * 17 + * **Note**: the returned `path` indicates where the screenshot *might* be found. 18 + * It is not guaranteed to exist, especially if `options.save` is `false`. 19 + * 20 + * @throws {Error} If the function is not called within a test or if the browser provider does not support screenshots. 21 + */ 22 + export async function takeScreenshot( 23 + context: BrowserCommandContext, 24 + name: string, 25 + options: Omit<ScreenshotCommandOptions, 'base64'>, 26 + ): Promise<{ buffer: Buffer<ArrayBufferLike>; path: string }> { 27 + if (!context.testPath) { 28 + throw new Error(`Cannot take a screenshot without a test path`) 29 + } 30 + 31 + const path = resolveScreenshotPath( 32 + context.testPath, 33 + name, 34 + context.project.config, 35 + options.path, 36 + ) 37 + 38 + // playwright does not need a screenshot path if we don't intend to save it 39 + let savePath: string | undefined 40 + 41 + if (options.save) { 42 + savePath = normalize(path) 43 + 44 + await mkdir(dirname(savePath), { recursive: true }) 45 + } 46 + 47 + // webdriverio needs a path, so if one is not already set we create a temporary one 48 + if (savePath === undefined) { 49 + savePath = resolve(context.project.tmpDir, crypto.randomUUID()) 50 + 51 + await mkdir(context.project.tmpDir, { recursive: true }) 52 + } 53 + 54 + const page = context.browser 55 + const element = !options.element 56 + ? await page.$('body') 57 + : await page.$(`${options.element}`) 58 + 59 + // webdriverio expects the path to contain the extension and only works with PNG files 60 + const savePathWithExtension = savePath.endsWith('.png') ? savePath : `${savePath}.png` 61 + 62 + // there seems to be a bug in webdriverio, `X:/` gets appended to cwd, so we convert to `X:\` 63 + const buffer = await element.saveScreenshot( 64 + platformNormalize(savePathWithExtension), 65 + ) 66 + if (!options.save) { 67 + await rm(savePathWithExtension, { force: true }) 68 + } 69 + return { buffer, path } 70 + }
+25
packages/browser-webdriverio/src/commands/select.ts
··· 1 + import type { UserEvent } from 'vitest/browser' 2 + import type { UserEventCommand } from './utils' 3 + 4 + export const selectOptions: UserEventCommand<UserEvent['selectOptions']> = async ( 5 + context, 6 + selector, 7 + userValues, 8 + _options = {}, 9 + ) => { 10 + const values = userValues as any as [({ index: number })] 11 + 12 + if (!values.length) { 13 + return 14 + } 15 + 16 + const browser = context.browser 17 + 18 + if (values.length === 1 && 'index' in values[0]) { 19 + const selectElement = browser.$(selector) 20 + await selectElement.selectByIndex(values[0].index) 21 + } 22 + else { 23 + throw new Error('Provider "webdriverio" doesn\'t support selecting multiple values at once') 24 + } 25 + }
+11
packages/browser-webdriverio/src/commands/tab.ts
··· 1 + import type { UserEvent } from 'vitest/browser' 2 + import type { UserEventCommand } from './utils' 3 + import { Key } from 'webdriverio' 4 + 5 + export const tab: UserEventCommand<UserEvent['tab']> = async ( 6 + context, 7 + options = {}, 8 + ) => { 9 + const browser = context.browser 10 + await browser.keys(options.shift === true ? [Key.Shift, Key.Tab] : [Key.Tab]) 11 + }
+38
packages/browser-webdriverio/src/commands/type.ts
··· 1 + import type { UserEvent } from 'vitest/browser' 2 + import type { UserEventCommand } from './utils' 3 + import { keyboardImplementation } from './keyboard' 4 + 5 + export const type: UserEventCommand<UserEvent['type']> = async ( 6 + context, 7 + selector, 8 + text, 9 + options = {}, 10 + ) => { 11 + const { skipClick = false, skipAutoClose = false } = options 12 + const unreleased = new Set(Reflect.get(options, 'unreleased') as string[] ?? []) 13 + 14 + const browser = context.browser 15 + const element = browser.$(selector) 16 + 17 + if (!skipClick && !await element.isFocused()) { 18 + await element.click() 19 + } 20 + 21 + await keyboardImplementation( 22 + unreleased, 23 + context.provider, 24 + context.sessionId, 25 + text, 26 + () => browser.execute(() => { 27 + const element = document.activeElement as HTMLInputElement 28 + if (element && typeof element.select === 'function') { 29 + element.select() 30 + } 31 + }), 32 + skipAutoClose, 33 + ) 34 + 35 + return { 36 + unreleased: Array.from(unreleased), 37 + } 38 + }
+34
packages/browser-webdriverio/src/commands/upload.ts
··· 1 + import type { UserEventUploadOptions } from 'vitest/browser' 2 + import type { UserEventCommand } from './utils' 3 + import { resolve } from 'pathe' 4 + 5 + export const upload: UserEventCommand<(element: string, files: Array<string | { 6 + name: string 7 + mimeType: string 8 + base64: string 9 + }>, options: UserEventUploadOptions) => void> = async ( 10 + context, 11 + selector, 12 + files, 13 + _options, 14 + ) => { 15 + const testPath = context.testPath 16 + if (!testPath) { 17 + throw new Error(`Cannot upload files outside of a test`) 18 + } 19 + const root = context.project.config.root 20 + 21 + for (const file of files) { 22 + if (typeof file !== 'string') { 23 + throw new TypeError(`The "${context.provider.name}" provider doesn't support uploading files objects. Provide a file path instead.`) 24 + } 25 + } 26 + 27 + const element = context.browser.$(selector) 28 + 29 + for (const file of files) { 30 + const filepath = resolve(root, file as string) 31 + const remoteFilePath = await context.browser.uploadFile(filepath) 32 + await element.addValue(remoteFilePath) 33 + } 34 + }
+11
packages/browser-webdriverio/src/commands/utils.ts
··· 1 + import type { Locator } from 'vitest/browser' 2 + import type { BrowserCommand } from 'vitest/node' 3 + 4 + export type UserEventCommand<T extends (...args: any) => any> = BrowserCommand< 5 + ConvertUserEventParameters<Parameters<T>> 6 + > 7 + 8 + type ConvertElementToLocator<T> = T extends Element | Locator ? string : T 9 + type ConvertUserEventParameters<T extends unknown[]> = { 10 + [K in keyof T]: ConvertElementToLocator<T[K]>; 11 + }
+9
packages/browser-webdriverio/src/commands/viewport.ts
··· 1 + import type { WebdriverBrowserProvider } from '../webdriverio' 2 + import type { UserEventCommand } from './utils' 3 + 4 + export const viewport: UserEventCommand<(options: { 5 + width: number 6 + height: number 7 + }) => void> = async (context, options) => { 8 + await (context.provider as WebdriverBrowserProvider).setViewport(options) 9 + }
+5
packages/browser-webdriverio/src/constants.ts
··· 1 + import { fileURLToPath } from 'node:url' 2 + import { resolve } from 'pathe' 3 + 4 + const pkgRoot = resolve(fileURLToPath(import.meta.url), '../..') 5 + export const distRoot: string = resolve(pkgRoot, 'dist')
+6
packages/browser-webdriverio/src/index.ts
··· 1 + export { 2 + WebdriverBrowserProvider, 3 + webdriverio, 4 + type WebdriverProviderOptions, 5 + } from './webdriverio' 6 + export { defineBrowserCommand } from '@vitest/browser'
+13
packages/browser-webdriverio/tsconfig.json
··· 1 + { 2 + "extends": "../../tsconfig.base.json", 3 + "compilerOptions": { 4 + "types": ["node", "vite/client"], 5 + "isolatedDeclarations": true 6 + }, 7 + "exclude": [ 8 + "dist", 9 + "node_modules", 10 + "**/vite.config.ts", 11 + "src/client/**/*.ts" 12 + ] 13 + }
+50 -9
packages/browser/context.d.ts
··· 1 1 import { SerializedConfig } from 'vitest' 2 + import { StringifyOptions } from 'vitest/internal/browser' 2 3 import { ARIARole } from './aria-role.js' 3 4 import {} from './matchers.js' 4 5 ··· 186 187 * state of keyboard to press and release buttons correctly. 187 188 * 188 189 * **Note:** Unlike `@testing-library/user-event`, the default `userEvent` instance 189 - * from `@vitest/browser/context` is created once, not every time its methods are called! 190 + * from `vitest/browser` is created once, not every time its methods are called! 190 191 * @see {@link https://vitest.dev/guide/browser/interactivity-api.html#userevent-setup} 191 192 */ 192 193 setup: () => UserEvent ··· 361 362 * regular expression. Note that exact match still trims whitespace. 362 363 */ 363 364 exact?: boolean 365 + hasText?: string | RegExp 366 + hasNotText?: string | RegExp 367 + has?: Locator 368 + hasNot?: Locator 364 369 } 365 370 366 371 export interface LocatorByRoleOptions extends LocatorOptions { ··· 660 665 config: SerializedConfig 661 666 } 662 667 663 - export interface LocatorOptions { 664 - hasText?: string | RegExp 665 - hasNotText?: string | RegExp 666 - has?: Locator 667 - hasNot?: Locator 668 - } 669 - 670 668 /** 671 669 * Handler for user interactions. The support is provided by the browser provider (`playwright` or `webdriverio`). 672 670 * If used with `preview` provider, fallbacks to simulated events via `@testing-library/user-event`. ··· 732 730 733 731 export interface BrowserLocators { 734 732 createElementLocators(element: Element): LocatorSelectors 733 + // TODO: enhance docs 734 + /** 735 + * Extends `page.*` and `locator.*` interfaces. 736 + * @see {@link} 737 + * 738 + * @example 739 + * ```ts 740 + * import { locators } from 'vitest/browser' 741 + * 742 + * declare module 'vitest/browser' { 743 + * interface LocatorSelectors { 744 + * getByCSS(css: string): Locator 745 + * } 746 + * } 747 + * 748 + * locators.extend({ 749 + * getByCSS(css: string) { 750 + * return `css=${css}` 751 + * } 752 + * }) 753 + * ``` 754 + */ 735 755 extend(methods: { 736 - [K in keyof LocatorSelectors]?: (this: BrowserPage | Locator, ...args: Parameters<LocatorSelectors[K]>) => ReturnType<LocatorSelectors[K]> | string 756 + [K in keyof LocatorSelectors]?: ( 757 + this: BrowserPage | Locator, 758 + ...args: Parameters<LocatorSelectors[K]> 759 + ) => ReturnType<LocatorSelectors[K]> | string 737 760 }): void 761 + } 762 + 763 + 764 + export type PrettyDOMOptions = Omit<StringifyOptions, 'maxLength'> 765 + 766 + export const utils: { 767 + getElementLocatorSelectors(element: Element): LocatorSelectors 768 + debug( 769 + el?: Element | Locator | null | (Element | Locator)[], 770 + maxLength?: number, 771 + options?: PrettyDOMOptions, 772 + ): void 773 + prettyDOM( 774 + dom?: Element | Locator | undefined | null, 775 + maxLength?: number, 776 + prettyFormatOptions?: PrettyDOMOptions, 777 + ): string 778 + getElementError(selector: string, container?: Element): Error 738 779 } 739 780 740 781 export const locators: BrowserLocators
+3 -2
packages/browser/context.js
··· 1 - // Vitest resolves "@vitest/browser/context" as a virtual module instead 1 + // Vitest resolves "vitest/browser" as a virtual module instead 2 2 3 3 // fake exports for static analysis 4 4 export const page = null ··· 7 7 export const cdp = null 8 8 export const commands = null 9 9 export const locators = null 10 + export const utils = null 10 11 11 12 const pool = globalThis.__vitest_worker__?.ctx?.pool 12 13 13 14 throw new Error( 14 15 // eslint-disable-next-line prefer-template 15 - '@vitest/browser/context can be imported only inside the Browser Mode. ' 16 + 'vitest/browser can be imported only inside the Browser Mode. ' 16 17 + (pool 17 18 ? `Your test is running in ${pool} pool. Make sure your regular tests are excluded from the "test.include" glob pattern.` 18 19 : 'Instead, it was imported outside of Vitest.'),
+5 -5
packages/browser/jest-dom.d.ts
··· 23 23 * This matcher calculates the intersection ratio between the element and the viewport, similar to the 24 24 * IntersectionObserver API. 25 25 * 26 - * The element must be in the document and have visible dimensions. Elements with display: none or 26 + * The element must be in the document and have visible dimensions. Elements with display: none or 27 27 * visibility: hidden are considered not in viewport. 28 28 * @example 29 29 * <div ··· 34 34 * </div> 35 35 * 36 36 * <div 37 - * data-testid="hidden-element" 37 + * data-testid="hidden-element" 38 38 * style="position: fixed; top: -100px; left: 10px; width: 50px; height: 50px;" 39 39 * > 40 40 * Hidden Element ··· 49 49 * 50 50 * // Check if any part of element is in viewport 51 51 * await expect.element(page.getByTestId('visible-element')).toBeInViewport() 52 - * 52 + * 53 53 * // Check if element is outside viewport 54 54 * await expect.element(page.getByTestId('hidden-element')).not.toBeInViewport() 55 - * 55 + * 56 56 * // Check if at least 50% of element is visible 57 57 * await expect.element(page.getByTestId('large-element')).toBeInViewport({ ratio: 0.5 }) 58 - * 58 + * 59 59 * // Check if element is completely visible 60 60 * await expect.element(page.getByTestId('visible-element')).toBeInViewport({ ratio: 1 }) 61 61 * @see https://vitest.dev/guide/browser/assertion-api#tobeinviewport
+1 -1
packages/browser/matchers.d.ts
··· 1 - import type { Locator } from '@vitest/browser/context' 1 + import type { Locator } from './context.js' 2 2 import type { TestingLibraryMatchers } from './jest-dom.js' 3 3 import type { Assertion, ExpectPollOptions } from 'vitest' 4 4
+7 -29
packages/browser/package.json
··· 20 20 "types": "./dist/index.d.ts", 21 21 "default": "./dist/index.js" 22 22 }, 23 - "./providers/*": { 24 - "types": "./dist/providers/*.d.ts", 25 - "default": "./dist/providers/*.js" 26 - }, 27 23 "./context": { 28 24 "types": "./context.d.ts", 29 25 "default": "./context.js" ··· 35 31 "types": "./matchers.d.ts", 36 32 "default": "./dummy.js" 37 33 }, 38 - "./locator": { 39 - "types": "./dist/locators/index.d.ts", 40 - "default": "./dist/locators/index.js" 34 + "./locators": { 35 + "types": "./dist/locators.d.ts", 36 + "default": "./dist/locators.js" 41 37 }, 42 38 "./utils": { 43 39 "types": "./utils.d.ts", 44 40 "default": "./dist/utils.js" 45 41 }, 46 - "./*": "./*" 42 + "./package.json": "./package.json" 47 43 }, 48 44 "main": "./dist/index.js", 49 45 "module": "./dist/index.js", ··· 66 62 "dev": "premove dist && pnpm run --stream '/^dev:/'" 67 63 }, 68 64 "peerDependencies": { 69 - "playwright": "*", 70 - "vitest": "workspace:*", 71 - "webdriverio": "^7.0.0 || ^8.0.0 || ^9.0.0" 72 - }, 73 - "peerDependenciesMeta": { 74 - "playwright": { 75 - "optional": true 76 - }, 77 - "safaridriver": { 78 - "optional": true 79 - }, 80 - "webdriverio": { 81 - "optional": true 82 - } 65 + "vitest": "workspace:*" 83 66 }, 84 67 "dependencies": { 85 - "@testing-library/dom": "^10.4.1", 86 - "@testing-library/user-event": "^14.6.1", 87 68 "@vitest/mocker": "workspace:*", 88 69 "@vitest/utils": "workspace:*", 89 70 "magic-string": "catalog:", ··· 94 75 "ws": "catalog:" 95 76 }, 96 77 "devDependencies": { 78 + "@testing-library/user-event": "^14.6.1", 97 79 "@types/pngjs": "^6.0.5", 98 80 "@types/ws": "catalog:", 99 81 "@vitest/runner": "workspace:*", 100 - "@wdio/types": "^9.19.2", 101 82 "birpc": "catalog:", 102 83 "flatted": "catalog:", 103 84 "ivya": "^1.7.0", 104 85 "mime": "^4.1.0", 105 86 "pathe": "catalog:", 106 - "playwright": "^1.55.0", 107 - "playwright-core": "^1.55.0", 108 - "vitest": "workspace:*", 109 - "webdriverio": "^9.19.2" 87 + "vitest": "workspace:*" 110 88 } 111 89 }
-7
packages/browser/providers.d.ts
··· 1 - import type { BrowserProviderModule } from 'vitest/node' 2 - 3 - declare const webdriverio: BrowserProviderModule 4 - declare const playwright: BrowserProviderModule 5 - declare const preview: BrowserProviderModule 6 - 7 - export { webdriverio, playwright, preview }
+8 -25
packages/browser/rollup.config.js
··· 14 14 ...Object.keys(pkg.peerDependencies || {}), 15 15 /^@?vitest(\/|$)/, 16 16 '@vitest/browser/utils', 17 + '@vitest/browser/context', 18 + '@vitest/browser/client', 19 + 'vitest/browser', 17 20 'worker_threads', 18 21 'node:worker_threads', 19 22 'vite', 20 - 'playwright-core/types/protocol', 23 + 'vitest/internal/browser', 21 24 ] 22 25 23 26 const dtsUtils = createDtsUtils() ··· 39 42 ] 40 43 41 44 const input = { 42 - 'index': './src/node/index.ts', 43 - 'providers/playwright': './src/node/providers/playwright.ts', 44 - 'providers/webdriverio': './src/node/providers/webdriverio.ts', 45 - 'providers/preview': './src/node/providers/preview.ts', 45 + index: './src/node/index.ts', 46 46 } 47 47 48 48 export default () => ··· 75 75 }, 76 76 { 77 77 input: { 78 - 'locators/playwright': './src/client/tester/locators/playwright.ts', 79 - 'locators/webdriverio': './src/client/tester/locators/webdriverio.ts', 80 - 'locators/preview': './src/client/tester/locators/preview.ts', 81 - 'locators/index': './src/client/tester/locators/index.ts', 78 + 'locators': './src/client/tester/locators/index.ts', 82 79 'expect-element': './src/client/tester/expect-element.ts', 83 - 'utils': './src/client/tester/public-utils.ts', 84 80 }, 85 81 output: { 86 82 dir: 'dist', ··· 102 98 file: 'dist/context.js', 103 99 format: 'esm', 104 100 }, 105 - external: ['@vitest/browser/utils'], 101 + external: ['vitest/internal/browser'], 106 102 plugins: [ 107 103 oxc({ 108 104 transform: { target: 'node18' }, ··· 150 146 }, 151 147 { 152 148 input: dtsUtilsClient.dtsInput({ 153 - 'locators/index': './src/client/tester/locators/index.ts', 149 + locators: './src/client/tester/locators/index.ts', 154 150 }), 155 151 output: { 156 152 dir: 'dist', ··· 161 157 external, 162 158 plugins: dtsUtilsClient.dts(), 163 159 }, 164 - // { 165 - // input: './src/client/tester/jest-dom.ts', 166 - // output: { 167 - // file: './jest-dom.d.ts', 168 - // format: 'esm', 169 - // }, 170 - // external: [], 171 - // plugins: [ 172 - // dts({ 173 - // respectExternal: true, 174 - // }), 175 - // ], 176 - // }, 177 160 ])
+1 -1
packages/browser/src/client/client.ts
··· 143 143 `Cannot connect to the server in ${connectTimeout / 1000} seconds`, 144 144 ), 145 145 ) 146 - }, connectTimeout)?.unref?.() 146 + }, connectTimeout) 147 147 if (ctx.ws.OPEN === ctx.ws.readyState) { 148 148 resolve() 149 149 }
+8 -4
packages/browser/src/client/orchestrator.ts
··· 29 29 } 30 30 31 31 public async createTesters(options: BrowserTesterOptions): Promise<void> { 32 + const startTime = performance.now() 33 + 32 34 this.cancelled = false 33 35 34 36 const config = getConfig() ··· 46 48 } 47 49 48 50 if (config.browser.isolate === false) { 49 - await this.runNonIsolatedTests(container, options) 51 + await this.runNonIsolatedTests(container, options, startTime) 50 52 return 51 53 } 52 54 ··· 65 67 container, 66 68 file, 67 69 options, 70 + startTime, 68 71 ) 69 72 } 70 73 } ··· 95 98 this.recreateNonIsolatedIframe = true 96 99 } 97 100 98 - private async runNonIsolatedTests(container: HTMLDivElement, options: BrowserTesterOptions) { 101 + private async runNonIsolatedTests(container: HTMLDivElement, options: BrowserTesterOptions, startTime: number) { 99 102 if (this.recreateNonIsolatedIframe) { 100 103 // recreate a new non-isolated iframe during watcher reruns 101 104 // because we called "cleanup" in the previous run ··· 108 111 109 112 if (!this.iframes.has(ID_ALL)) { 110 113 debug('preparing non-isolated iframe') 111 - await this.prepareIframe(container, ID_ALL, options.startTime) 114 + await this.prepareIframe(container, ID_ALL, startTime) 112 115 } 113 116 114 117 const config = getConfig() ··· 133 136 container: HTMLDivElement, 134 137 file: string, 135 138 options: BrowserTesterOptions, 139 + startTime: number, 136 140 ) { 137 141 const config = getConfig() 138 142 const { width, height } = config.browser.viewport ··· 142 146 this.iframes.delete(file) 143 147 } 144 148 145 - const iframe = await this.prepareIframe(container, file, options.startTime) 149 + const iframe = await this.prepareIframe(container, file, startTime) 146 150 await setIframeViewport(iframe, width, height) 147 151 // running tests after the "prepare" event 148 152 await sendEventToIframe({
+85 -19
packages/browser/src/client/tester/context.ts
··· 2 2 Options as TestingLibraryOptions, 3 3 UserEvent as TestingLibraryUserEvent, 4 4 } from '@testing-library/user-event' 5 + import type { RunnerTask } from 'vitest' 5 6 import type { 6 7 BrowserLocators, 7 8 BrowserPage, 8 9 Locator, 10 + LocatorSelectors, 9 11 UserEvent, 10 - } from '@vitest/browser/context' 11 - import type { RunnerTask } from 'vitest' 12 + } from 'vitest/browser' 13 + import type { StringifyOptions } from 'vitest/internal/browser' 12 14 import type { IframeViewportEvent } from '../client' 13 15 import type { BrowserRunnerState } from '../utils' 14 16 import type { Locator as LocatorAPI } from './locators/index' 15 - import { getElementLocatorSelectors } from '@vitest/browser/utils' 17 + import { __INTERNAL, stringify } from 'vitest/internal/browser' 16 18 import { ensureAwaited, getBrowserState, getWorkerState } from '../utils' 17 - import { convertToSelector, processTimeoutOptions } from './utils' 19 + import { convertToSelector, processTimeoutOptions } from './tester-utils' 18 20 19 21 // this file should not import anything directly, only types and utils 20 22 ··· 38 40 39 41 // https://playwright.dev/docs/api/class-keyboard 40 42 // https://webdriver.io/docs/api/browser/keys/ 41 - const modifier = provider === `playwright` 43 + const modifier = provider === 'playwright' 42 44 ? 'ControlOrMeta' 43 45 : provider === 'webdriverio' 44 46 ? 'Ctrl' ··· 340 342 frameLocator() { 341 343 throw new Error(`Method "frameLocator" is not supported by the "${provider}" provider.`) 342 344 }, 343 - _createLocator() { 344 - throw new Error(`Method "_createLocator" is not supported by the "${provider}" provider.`) 345 - }, 346 345 extend(methods) { 347 346 for (const key in methods) { 348 347 (page as any)[key] = (methods as any)[key].bind(page) ··· 365 364 export const locators: BrowserLocators = { 366 365 createElementLocators: getElementLocatorSelectors, 367 366 extend(methods) { 368 - const Locator = page._createLocator('css=body').constructor as typeof LocatorAPI 367 + const Locator = __INTERNAL._createLocator('css=body').constructor as typeof LocatorAPI 369 368 for (const method in methods) { 370 - locators._extendedMethods.add(method) 369 + __INTERNAL._extendedMethods.add(method) 371 370 const cb = (methods as any)[method] as (...args: any[]) => string | Locator 372 371 // @ts-expect-error types are hard to make work 373 372 Locator.prototype[method] = function (...args: any[]) { ··· 380 379 page[method as 'getByRole'] = function (...args: any[]) { 381 380 const selectorOrLocator = cb.call(this, ...args) 382 381 if (typeof selectorOrLocator === 'string') { 383 - return page._createLocator(selectorOrLocator) 382 + return __INTERNAL._createLocator(selectorOrLocator) 384 383 } 385 384 return selectorOrLocator 386 385 } 387 386 } 388 387 }, 389 - _extendedMethods: new Set<string>(), 390 388 } 391 389 392 - declare module '@vitest/browser/context' { 393 - interface BrowserPage { 394 - /** @internal */ 395 - _createLocator: (selector: string) => Locator 390 + function getElementLocatorSelectors(element: Element): LocatorSelectors { 391 + const locator = page.elementLocator(element) 392 + return { 393 + getByAltText: (altText, options) => locator.getByAltText(altText, options), 394 + getByLabelText: (labelText, options) => locator.getByLabelText(labelText, options), 395 + getByPlaceholder: (placeholderText, options) => locator.getByPlaceholder(placeholderText, options), 396 + getByRole: (role, options) => locator.getByRole(role, options), 397 + getByTestId: testId => locator.getByTestId(testId), 398 + getByText: (text, options) => locator.getByText(text, options), 399 + getByTitle: (title, options) => locator.getByTitle(title, options), 400 + ...Array.from(__INTERNAL._extendedMethods).reduce((methods, method) => { 401 + methods[method] = (...args: any[]) => (locator as any)[method](...args) 402 + return methods 403 + }, {} as any), 396 404 } 405 + } 397 406 398 - interface BrowserLocators { 399 - /** @internal */ 400 - _extendedMethods: Set<string> 407 + type PrettyDOMOptions = Omit<StringifyOptions, 'maxLength'> 408 + 409 + function debug( 410 + el?: Element | Locator | null | (Element | Locator)[], 411 + maxLength?: number, 412 + options?: PrettyDOMOptions, 413 + ): void { 414 + if (Array.isArray(el)) { 415 + // eslint-disable-next-line no-console 416 + el.forEach(e => console.log(prettyDOM(e, maxLength, options))) 417 + } 418 + else { 419 + // eslint-disable-next-line no-console 420 + console.log(prettyDOM(el, maxLength, options)) 401 421 } 402 422 } 423 + 424 + function prettyDOM( 425 + dom?: Element | Locator | undefined | null, 426 + maxLength: number = Number(import.meta.env.DEBUG_PRINT_LIMIT ?? 7000), 427 + prettyFormatOptions: PrettyDOMOptions = {}, 428 + ): string { 429 + if (maxLength === 0) { 430 + return '' 431 + } 432 + 433 + if (!dom) { 434 + dom = document.body 435 + } 436 + 437 + if ('element' in dom && 'all' in dom) { 438 + dom = dom.element() 439 + } 440 + 441 + const type = typeof dom 442 + if (type !== 'object' || !dom.outerHTML) { 443 + const typeName = type === 'object' ? dom.constructor.name : type 444 + throw new TypeError(`Expecting a valid DOM element, but got ${typeName}.`) 445 + } 446 + 447 + const pretty = stringify(dom, Number.POSITIVE_INFINITY, { 448 + maxLength, 449 + highlight: true, 450 + ...prettyFormatOptions, 451 + }) 452 + return dom.outerHTML.length > maxLength 453 + ? `${pretty.slice(0, maxLength)}...` 454 + : pretty 455 + } 456 + 457 + function getElementError(selector: string, container: Element): Error { 458 + const error = new Error(`Cannot find element with locator: ${__INTERNAL._asLocator('javascript', selector)}\n\n${prettyDOM(container)}`) 459 + error.name = 'VitestBrowserElementError' 460 + return error 461 + } 462 + 463 + export const utils = { 464 + getElementError, 465 + prettyDOM, 466 + debug, 467 + getElementLocatorSelectors, 468 + }
+2 -2
packages/browser/src/client/tester/expect-element.ts
··· 1 - import type { Locator } from '@vitest/browser/context' 2 1 import type { ExpectPollOptions, PromisifyDomAssertion } from 'vitest' 2 + import type { Locator } from 'vitest/browser' 3 3 import { chai, expect } from 'vitest' 4 4 import { getType } from 'vitest/internal/browser' 5 5 import { matchers } from './expect' 6 - import { processTimeoutOptions } from './utils' 6 + import { processTimeoutOptions } from './tester-utils' 7 7 8 8 const kLocator = Symbol.for('$$vitest:locator') 9 9
+1 -1
packages/browser/src/client/tester/expect/toBeVisible.ts
··· 15 15 16 16 import type { ExpectationResult, MatcherState } from '@vitest/expect' 17 17 import type { Locator } from '../locators' 18 - import { server } from '@vitest/browser/context' 19 18 import { beginAriaCaches, endAriaCaches, isElementVisible as ivyaIsVisible } from 'ivya/utils' 19 + import { server } from 'vitest/browser' 20 20 import { getElementFromUserInput } from './utils' 21 21 22 22 export default function toBeVisible(
+1 -1
packages/browser/src/client/tester/expect/toHaveStyle.ts
··· 1 1 import type { ExpectationResult, MatcherState } from '@vitest/expect' 2 2 import type { Locator } from '../locators' 3 - import { server } from '@vitest/browser/context' 3 + import { server } from 'vitest/browser' 4 4 import { getElementFromUserInput } from './utils' 5 5 6 6 const browser = server.config.browser.name
+1 -1
packages/browser/src/client/tester/expect/toMatchScreenshot.ts
··· 3 3 import type { ScreenshotMatcherArguments, ScreenshotMatcherOutput } from '../../../shared/screenshotMatcher/types' 4 4 import type { Locator } from '../locators' 5 5 import { getBrowserState, getWorkerState } from '../../utils' 6 - import { convertToSelector } from '../utils' 6 + import { convertToSelector } from '../tester-utils' 7 7 8 8 const counters = new Map<string, { current: number }>([]) 9 9
+21 -8
packages/browser/src/client/tester/locators/index.ts
··· 1 + import type { ParsedSelector } from 'ivya' 1 2 import type { 2 3 LocatorByRoleOptions, 3 4 LocatorOptions, ··· 9 10 UserEventHoverOptions, 10 11 UserEventSelectOptions, 11 12 UserEventUploadOptions, 12 - } from '@vitest/browser/context' 13 - import type { ParsedSelector } from 'ivya' 14 - import { page, server } from '@vitest/browser/context' 13 + } from 'vitest/browser' 15 14 import { 15 + asLocator, 16 16 getByAltTextSelector, 17 17 getByLabelSelector, 18 18 getByPlaceholderSelector, ··· 21 21 getByTextSelector, 22 22 getByTitleSelector, 23 23 Ivya, 24 + } from 'ivya' 25 + import { page, server, utils } from 'vitest/browser' 26 + import { __INTERNAL } from 'vitest/internal/browser' 27 + import { ensureAwaited, getBrowserState } from '../../utils' 28 + import { escapeForTextSelector, isLocator } from '../tester-utils' 24 29 30 + export { convertElementToCssSelector, getIframeScale, processTimeoutOptions } from '../tester-utils' 31 + export { 32 + getByAltTextSelector, 33 + getByLabelSelector, 34 + getByPlaceholderSelector, 35 + getByRoleSelector, 36 + getByTestIdSelector, 37 + getByTextSelector, 38 + getByTitleSelector, 25 39 } from 'ivya' 26 - import { ensureAwaited, getBrowserState } from '../../utils' 27 - import { getElementError } from '../public-utils' 28 - import { escapeForTextSelector } from '../utils' 40 + 41 + __INTERNAL._asLocator = asLocator 29 42 30 43 // we prefer using playwright locators because they are more powerful and support Shadow DOM 31 44 export const selectorEngine: Ivya = Ivya.create({ ··· 125 138 ): Promise<void> { 126 139 const values = (Array.isArray(value) ? value : [value]).map((v) => { 127 140 if (typeof v !== 'string') { 128 - const selector = 'element' in v ? v.selector : selectorEngine.generateSelectorSimple(v) 141 + const selector = isLocator(v) ? v.selector : selectorEngine.generateSelectorSimple(v) 129 142 return { element: selector } 130 143 } 131 144 return v ··· 223 236 public element(): HTMLElement | SVGElement { 224 237 const element = this.query() 225 238 if (!element) { 226 - throw getElementError(this._pwSelector || this.selector, this._container || document.body) 239 + throw utils.getElementError(this._pwSelector || this.selector, this._container || document.body) 227 240 } 228 241 return element 229 242 }
+57 -64
packages/browser/src/client/tester/locators/playwright.ts packages/browser-playwright/src/locators.ts
··· 6 6 UserEventHoverOptions, 7 7 UserEventSelectOptions, 8 8 UserEventUploadOptions, 9 - } from '@vitest/browser/context' 10 - import { page, server } from '@vitest/browser/context' 9 + } from 'vitest/browser' 11 10 import { 12 11 getByAltTextSelector, 13 12 getByLabelSelector, ··· 16 15 getByTestIdSelector, 17 16 getByTextSelector, 18 17 getByTitleSelector, 19 - } from 'ivya' 20 - import { getIframeScale, processTimeoutOptions } from '../utils' 21 - import { Locator, selectorEngine } from './index' 22 - 23 - page.extend({ 24 - getByLabelText(text, options) { 25 - return new PlaywrightLocator(getByLabelSelector(text, options)) 26 - }, 27 - getByRole(role, options) { 28 - return new PlaywrightLocator(getByRoleSelector(role, options)) 29 - }, 30 - getByTestId(testId) { 31 - return new PlaywrightLocator(getByTestIdSelector(server.config.browser.locators.testIdAttribute, testId)) 32 - }, 33 - getByAltText(text, options) { 34 - return new PlaywrightLocator(getByAltTextSelector(text, options)) 35 - }, 36 - getByPlaceholder(text, options) { 37 - return new PlaywrightLocator(getByPlaceholderSelector(text, options)) 38 - }, 39 - getByText(text, options) { 40 - return new PlaywrightLocator(getByTextSelector(text, options)) 41 - }, 42 - getByTitle(title, options) { 43 - return new PlaywrightLocator(getByTitleSelector(title, options)) 44 - }, 45 - 46 - _createLocator(selector: string) { 47 - return new PlaywrightLocator(selector) 48 - }, 49 - elementLocator(element: Element) { 50 - return new PlaywrightLocator( 51 - selectorEngine.generateSelectorSimple(element), 52 - element, 53 - ) 54 - }, 55 - frameLocator(locator: Locator) { 56 - return new PlaywrightLocator( 57 - `${locator.selector} >> internal:control=enter-frame`, 58 - ) 59 - }, 60 - }) 18 + getIframeScale, 19 + Locator, 20 + processTimeoutOptions, 21 + selectorEngine, 22 + } from '@vitest/browser/locators' 23 + import { page, server } from 'vitest/browser' 24 + import { __INTERNAL } from 'vitest/internal/browser' 61 25 62 26 class PlaywrightLocator extends Locator { 63 27 constructor(public selector: string, protected _container?: Element) { ··· 120 84 } 121 85 } 122 86 123 - function processDragAndDropOptions(options_?: UserEventDragAndDropOptions) { 124 - if (!options_) { 125 - return options_ 87 + page.extend({ 88 + getByLabelText(text, options) { 89 + return new PlaywrightLocator(getByLabelSelector(text, options)) 90 + }, 91 + getByRole(role, options) { 92 + return new PlaywrightLocator(getByRoleSelector(role, options)) 93 + }, 94 + getByTestId(testId) { 95 + return new PlaywrightLocator(getByTestIdSelector(server.config.browser.locators.testIdAttribute, testId)) 96 + }, 97 + getByAltText(text, options) { 98 + return new PlaywrightLocator(getByAltTextSelector(text, options)) 99 + }, 100 + getByPlaceholder(text, options) { 101 + return new PlaywrightLocator(getByPlaceholderSelector(text, options)) 102 + }, 103 + getByText(text, options) { 104 + return new PlaywrightLocator(getByTextSelector(text, options)) 105 + }, 106 + getByTitle(title, options) { 107 + return new PlaywrightLocator(getByTitleSelector(title, options)) 108 + }, 109 + 110 + elementLocator(element: Element) { 111 + return new PlaywrightLocator( 112 + selectorEngine.generateSelectorSimple(element), 113 + element, 114 + ) 115 + }, 116 + frameLocator(locator: Locator) { 117 + return new PlaywrightLocator( 118 + `${locator.selector} >> internal:control=enter-frame`, 119 + ) 120 + }, 121 + }) 122 + 123 + __INTERNAL._createLocator = selector => new PlaywrightLocator(selector) 124 + 125 + function processDragAndDropOptions(options?: UserEventDragAndDropOptions) { 126 + if (!options) { 127 + return options 126 128 } 127 - const options = options_ as NonNullable< 128 - Parameters<import('playwright').Page['dragAndDrop']>[2] 129 - > 130 129 if (options.sourcePosition) { 131 130 options.sourcePosition = processPlaywrightPosition(options.sourcePosition) 132 131 } 133 132 if (options.targetPosition) { 134 133 options.targetPosition = processPlaywrightPosition(options.targetPosition) 135 134 } 136 - return options_ 135 + return options 137 136 } 138 137 139 - function processHoverOptions(options_?: UserEventHoverOptions) { 140 - if (!options_) { 141 - return options_ 138 + function processHoverOptions(options?: UserEventHoverOptions) { 139 + if (!options) { 140 + return options 142 141 } 143 - const options = options_ as NonNullable< 144 - Parameters<import('playwright').Page['hover']>[1] 145 - > 146 142 if (options.position) { 147 143 options.position = processPlaywrightPosition(options.position) 148 144 } 149 - return options_ 145 + return options 150 146 } 151 147 152 - function processClickOptions(options_?: UserEventClickOptions) { 153 - if (!options_) { 154 - return options_ 148 + function processClickOptions(options?: UserEventClickOptions) { 149 + if (!options) { 150 + return options 155 151 } 156 - const options = options_ as NonNullable< 157 - Parameters<import('playwright').Page['click']>[1] 158 - > 159 152 if (options.position) { 160 153 options.position = processPlaywrightPosition(options.position) 161 154 }
+40 -40
packages/browser/src/client/tester/locators/preview.ts packages/browser-preview/src/locators.ts
··· 1 - import { page, server, userEvent } from '@vitest/browser/context' 2 1 import { 2 + convertElementToCssSelector, 3 3 getByAltTextSelector, 4 4 getByLabelSelector, 5 5 getByPlaceholderSelector, ··· 7 7 getByTestIdSelector, 8 8 getByTextSelector, 9 9 getByTitleSelector, 10 - } from 'ivya' 11 - import { getElementError } from '../public-utils' 12 - import { convertElementToCssSelector } from '../utils' 13 - import { Locator, selectorEngine } from './index' 14 - 15 - page.extend({ 16 - getByLabelText(text, options) { 17 - return new PreviewLocator(getByLabelSelector(text, options)) 18 - }, 19 - getByRole(role, options) { 20 - return new PreviewLocator(getByRoleSelector(role, options)) 21 - }, 22 - getByTestId(testId) { 23 - return new PreviewLocator(getByTestIdSelector(server.config.browser.locators.testIdAttribute, testId)) 24 - }, 25 - getByAltText(text, options) { 26 - return new PreviewLocator(getByAltTextSelector(text, options)) 27 - }, 28 - getByPlaceholder(text, options) { 29 - return new PreviewLocator(getByPlaceholderSelector(text, options)) 30 - }, 31 - getByText(text, options) { 32 - return new PreviewLocator(getByTextSelector(text, options)) 33 - }, 34 - getByTitle(title, options) { 35 - return new PreviewLocator(getByTitleSelector(title, options)) 36 - }, 37 - 38 - _createLocator(selector: string) { 39 - return new PreviewLocator(selector) 40 - }, 41 - elementLocator(element: Element) { 42 - return new PreviewLocator( 43 - selectorEngine.generateSelectorSimple(element), 44 - element, 45 - ) 46 - }, 47 - }) 10 + Locator, 11 + selectorEngine, 12 + } from '@vitest/browser/locators' 13 + import { page, server, userEvent, utils } from 'vitest/browser' 14 + import { __INTERNAL } from 'vitest/internal/browser' 48 15 49 16 class PreviewLocator extends Locator { 50 17 constructor(protected _pwSelector: string, protected _container?: Element) { ··· 54 21 override get selector() { 55 22 const selectors = this.elements().map(element => convertElementToCssSelector(element)) 56 23 if (!selectors.length) { 57 - throw getElementError(this._pwSelector, this._container || document.body) 24 + throw utils.getElementError(this._pwSelector, this._container || document.body) 58 25 } 59 26 return selectors.join(', ') 60 27 } ··· 106 73 ) 107 74 } 108 75 } 76 + 77 + page.extend({ 78 + getByLabelText(text, options) { 79 + return new PreviewLocator(getByLabelSelector(text, options)) 80 + }, 81 + getByRole(role, options) { 82 + return new PreviewLocator(getByRoleSelector(role, options)) 83 + }, 84 + getByTestId(testId) { 85 + return new PreviewLocator(getByTestIdSelector(server.config.browser.locators.testIdAttribute, testId)) 86 + }, 87 + getByAltText(text, options) { 88 + return new PreviewLocator(getByAltTextSelector(text, options)) 89 + }, 90 + getByPlaceholder(text, options) { 91 + return new PreviewLocator(getByPlaceholderSelector(text, options)) 92 + }, 93 + getByText(text, options) { 94 + return new PreviewLocator(getByTextSelector(text, options)) 95 + }, 96 + getByTitle(title, options) { 97 + return new PreviewLocator(getByTitleSelector(title, options)) 98 + }, 99 + 100 + elementLocator(element: Element) { 101 + return new PreviewLocator( 102 + selectorEngine.generateSelectorSimple(element), 103 + element, 104 + ) 105 + }, 106 + }) 107 + 108 + __INTERNAL._createLocator = selector => new PreviewLocator(selector)
+51 -59
packages/browser/src/client/tester/locators/webdriverio.ts packages/browser-webdriverio/src/locators.ts
··· 3 3 UserEventDragAndDropOptions, 4 4 UserEventHoverOptions, 5 5 UserEventSelectOptions, 6 - } from '@vitest/browser/context' 7 - import { page, server } from '@vitest/browser/context' 6 + } from 'vitest/browser' 8 7 import { 8 + convertElementToCssSelector, 9 9 getByAltTextSelector, 10 10 getByLabelSelector, 11 11 getByPlaceholderSelector, ··· 13 13 getByTestIdSelector, 14 14 getByTextSelector, 15 15 getByTitleSelector, 16 - } from 'ivya' 17 - import { getBrowserState } from '../../utils' 18 - import { getElementError } from '../public-utils' 19 - import { convertElementToCssSelector, getIframeScale } from '../utils' 20 - import { Locator, selectorEngine } from './index' 21 - 22 - page.extend({ 23 - getByLabelText(text, options) { 24 - return new WebdriverIOLocator(getByLabelSelector(text, options)) 25 - }, 26 - getByRole(role, options) { 27 - return new WebdriverIOLocator(getByRoleSelector(role, options)) 28 - }, 29 - getByTestId(testId) { 30 - return new WebdriverIOLocator(getByTestIdSelector(server.config.browser.locators.testIdAttribute, testId)) 31 - }, 32 - getByAltText(text, options) { 33 - return new WebdriverIOLocator(getByAltTextSelector(text, options)) 34 - }, 35 - getByPlaceholder(text, options) { 36 - return new WebdriverIOLocator(getByPlaceholderSelector(text, options)) 37 - }, 38 - getByText(text, options) { 39 - return new WebdriverIOLocator(getByTextSelector(text, options)) 40 - }, 41 - getByTitle(title, options) { 42 - return new WebdriverIOLocator(getByTitleSelector(title, options)) 43 - }, 44 - 45 - _createLocator(selector: string) { 46 - return new WebdriverIOLocator(selector) 47 - }, 48 - elementLocator(element: Element) { 49 - return new WebdriverIOLocator(selectorEngine.generateSelectorSimple(element)) 50 - }, 51 - }) 16 + getIframeScale, 17 + Locator, 18 + selectorEngine, 19 + } from '@vitest/browser/locators' 20 + import { page, server, utils } from 'vitest/browser' 21 + import { __INTERNAL } from 'vitest/internal/browser' 52 22 53 23 class WebdriverIOLocator extends Locator { 54 24 constructor(protected _pwSelector: string, protected _container?: Element) { ··· 58 28 override get selector(): string { 59 29 const selectors = this.elements().map(element => convertElementToCssSelector(element)) 60 30 if (!selectors.length) { 61 - throw getElementError(this._pwSelector, this._container || document.body) 31 + throw utils.getElementError(this._pwSelector, this._container || document.body) 62 32 } 63 33 let hasShadowRoot = false 64 34 const newSelectors = selectors.map((selector) => { ··· 108 78 } 109 79 } 110 80 81 + page.extend({ 82 + getByLabelText(text, options) { 83 + return new WebdriverIOLocator(getByLabelSelector(text, options)) 84 + }, 85 + getByRole(role, options) { 86 + return new WebdriverIOLocator(getByRoleSelector(role, options)) 87 + }, 88 + getByTestId(testId) { 89 + return new WebdriverIOLocator(getByTestIdSelector(server.config.browser.locators.testIdAttribute, testId)) 90 + }, 91 + getByAltText(text, options) { 92 + return new WebdriverIOLocator(getByAltTextSelector(text, options)) 93 + }, 94 + getByPlaceholder(text, options) { 95 + return new WebdriverIOLocator(getByPlaceholderSelector(text, options)) 96 + }, 97 + getByText(text, options) { 98 + return new WebdriverIOLocator(getByTextSelector(text, options)) 99 + }, 100 + getByTitle(title, options) { 101 + return new WebdriverIOLocator(getByTitleSelector(title, options)) 102 + }, 103 + 104 + elementLocator(element: Element) { 105 + return new WebdriverIOLocator(selectorEngine.generateSelectorSimple(element)) 106 + }, 107 + }) 108 + 109 + __INTERNAL._createLocator = selector => new WebdriverIOLocator(selector) 110 + 111 111 function getWebdriverioSelectOptions(element: Element, value: string | string[] | HTMLElement[] | HTMLElement | Locator | Locator[]) { 112 112 const options = [...element.querySelectorAll('option')] as HTMLOptionElement[] 113 113 ··· 149 149 return [{ index: labelIndex }] 150 150 } 151 151 152 - function processClickOptions(options_?: UserEventClickOptions) { 152 + function processClickOptions(options?: UserEventClickOptions) { 153 153 // only ui scales the iframe, so we need to adjust the position 154 - if (!options_ || !getBrowserState().config.browser.ui) { 155 - return options_ 154 + if (!options || !server.config.browser.ui) { 155 + return options 156 156 } 157 - const options = options_ as import('webdriverio').ClickOptions 158 157 if (options.x != null || options.y != null) { 159 158 const cache = {} 160 159 if (options.x != null) { ··· 164 163 options.y = scaleCoordinate(options.y, cache) 165 164 } 166 165 } 167 - return options_ 166 + return options 168 167 } 169 168 170 - function processHoverOptions(options_?: UserEventHoverOptions) { 169 + function processHoverOptions(options?: UserEventHoverOptions) { 171 170 // only ui scales the iframe, so we need to adjust the position 172 - if (!options_ || !getBrowserState().config.browser.ui) { 173 - return options_ 171 + if (!options || !server.config.browser.ui) { 172 + return options 174 173 } 175 - const options = options_ as import('webdriverio').MoveToOptions 176 174 const cache = {} 177 175 if (options.xOffset != null) { 178 176 options.xOffset = scaleCoordinate(options.xOffset, cache) ··· 180 178 if (options.yOffset != null) { 181 179 options.yOffset = scaleCoordinate(options.yOffset, cache) 182 180 } 183 - return options_ 181 + return options 184 182 } 185 183 186 - function processDragAndDropOptions(options_?: UserEventDragAndDropOptions) { 184 + function processDragAndDropOptions(options?: UserEventDragAndDropOptions) { 187 185 // only ui scales the iframe, so we need to adjust the position 188 - if (!options_ || !getBrowserState().config.browser.ui) { 189 - return options_ 186 + if (!options || !server.config.browser.ui) { 187 + return options 190 188 } 191 189 const cache = {} 192 - const options = options_ as import('webdriverio').DragAndDropOptions & { 193 - targetX?: number 194 - targetY?: number 195 - sourceX?: number 196 - sourceY?: number 197 - } 198 190 if (options.sourceX != null) { 199 191 options.sourceX = scaleCoordinate(options.sourceX, cache) 200 192 } ··· 207 199 if (options.targetY != null) { 208 200 options.targetY = scaleCoordinate(options.targetY, cache) 209 201 } 210 - return options_ 202 + return options 211 203 } 212 204 213 205 function scaleCoordinate(coordinate: number, cache: any) {
-78
packages/browser/src/client/tester/public-utils.ts
··· 1 - import type { Locator, LocatorSelectors } from '@vitest/browser/context' 2 - import type { StringifyOptions } from 'vitest/internal/browser' 3 - import { locators, page } from '@vitest/browser/context' 4 - import { asLocator } from 'ivya' 5 - import { stringify } from 'vitest/internal/browser' 6 - 7 - export function getElementLocatorSelectors(element: Element): LocatorSelectors { 8 - const locator = page.elementLocator(element) 9 - return { 10 - getByAltText: (altText, options) => locator.getByAltText(altText, options), 11 - getByLabelText: (labelText, options) => locator.getByLabelText(labelText, options), 12 - getByPlaceholder: (placeholderText, options) => locator.getByPlaceholder(placeholderText, options), 13 - getByRole: (role, options) => locator.getByRole(role, options), 14 - getByTestId: testId => locator.getByTestId(testId), 15 - getByText: (text, options) => locator.getByText(text, options), 16 - getByTitle: (title, options) => locator.getByTitle(title, options), 17 - ...Array.from(locators._extendedMethods).reduce((methods, method) => { 18 - methods[method] = (...args: any[]) => (locator as any)[method](...args) 19 - return methods 20 - }, {} as any), 21 - } 22 - } 23 - 24 - type PrettyDOMOptions = Omit<StringifyOptions, 'maxLength'> 25 - 26 - export function debug( 27 - el?: Element | Locator | null | (Element | Locator)[], 28 - maxLength?: number, 29 - options?: PrettyDOMOptions, 30 - ): void { 31 - if (Array.isArray(el)) { 32 - // eslint-disable-next-line no-console 33 - el.forEach(e => console.log(prettyDOM(e, maxLength, options))) 34 - } 35 - else { 36 - // eslint-disable-next-line no-console 37 - console.log(prettyDOM(el, maxLength, options)) 38 - } 39 - } 40 - 41 - export function prettyDOM( 42 - dom?: Element | Locator | undefined | null, 43 - maxLength: number = Number(import.meta.env.DEBUG_PRINT_LIMIT ?? 7000), 44 - prettyFormatOptions: PrettyDOMOptions = {}, 45 - ): string { 46 - if (maxLength === 0) { 47 - return '' 48 - } 49 - 50 - if (!dom) { 51 - dom = document.body 52 - } 53 - 54 - if ('element' in dom && 'all' in dom) { 55 - dom = dom.element() 56 - } 57 - 58 - const type = typeof dom 59 - if (type !== 'object' || !dom.outerHTML) { 60 - const typeName = type === 'object' ? dom.constructor.name : type 61 - throw new TypeError(`Expecting a valid DOM element, but got ${typeName}.`) 62 - } 63 - 64 - const pretty = stringify(dom, Number.POSITIVE_INFINITY, { 65 - maxLength, 66 - highlight: true, 67 - ...prettyFormatOptions, 68 - }) 69 - return dom.outerHTML.length > maxLength 70 - ? `${pretty.slice(0, maxLength)}...` 71 - : pretty 72 - } 73 - 74 - export function getElementError(selector: string, container: Element): Error { 75 - const error = new Error(`Cannot find element with locator: ${asLocator('javascript', selector)}\n\n${prettyDOM(container)}`) 76 - error.name = 'VitestBrowserElementError' 77 - return error 78 - }
+2 -2
packages/browser/src/client/tester/runner.ts
··· 11 11 } from '@vitest/runner' 12 12 import type { SerializedConfig, TestExecutionMethod, WorkerGlobalState } from 'vitest' 13 13 import type { VitestBrowserClientMocker } from './mocker' 14 - import type { CommandsManager } from './utils' 14 + import type { CommandsManager } from './tester-utils' 15 15 import { globalChannel, onCancel } from '@vitest/browser/client' 16 - import { page, userEvent } from '@vitest/browser/context' 17 16 import { getTestName } from '@vitest/runner/utils' 17 + import { page, userEvent } from 'vitest/browser' 18 18 import { 19 19 DecodedMap, 20 20 getOriginalPosition,
+2 -2
packages/browser/src/client/tester/tester.ts
··· 1 1 import type { BrowserRPC, IframeChannelEvent } from '@vitest/browser/client' 2 2 import { channel, client, onCancel } from '@vitest/browser/client' 3 - import { page, server, userEvent } from '@vitest/browser/context' 4 3 import { parse } from 'flatted' 4 + import { page, server, userEvent } from 'vitest/browser' 5 5 import { 6 6 collectTests, 7 7 setupCommonEnv, ··· 17 17 import { createModuleMockerInterceptor } from './mocker-interceptor' 18 18 import { createSafeRpc } from './rpc' 19 19 import { browserHashMap, initiateRunner } from './runner' 20 - import { CommandsManager } from './utils' 20 + import { CommandsManager } from './tester-utils' 21 21 22 22 const debugVar = getConfig().env.VITEST_BROWSER_DEBUG 23 23 const debug = debugVar && debugVar !== 'false'
+9 -3
packages/browser/src/client/tester/utils.ts packages/browser/src/client/tester/tester-utils.ts
··· 1 - import type { Locator } from '@vitest/browser/context' 1 + import type { Locator } from 'vitest/browser' 2 2 import type { BrowserRPC } from '../client' 3 3 import { getBrowserState, getWorkerState } from '../utils' 4 4 ··· 206 206 if (elementOrLocator instanceof Element) { 207 207 return convertElementToCssSelector(elementOrLocator) 208 208 } 209 - if ('selector' in elementOrLocator) { 210 - return (elementOrLocator as any).selector 209 + if (isLocator(elementOrLocator)) { 210 + return elementOrLocator.selector 211 211 } 212 212 throw new Error('Expected element or locator to be an instance of Element or Locator.') 213 213 } 214 + 215 + const kLocator = Symbol.for('$$vitest:locator') 216 + 217 + export function isLocator(element: unknown): element is Locator { 218 + return (!!element && typeof element === 'object' && kLocator in element) 219 + }
+1 -1
packages/browser/src/client/utils.ts
··· 1 1 import type { VitestRunner } from '@vitest/runner' 2 2 import type { EvaluatedModules, SerializedConfig, WorkerGlobalState } from 'vitest' 3 3 import type { IframeOrchestrator } from './orchestrator' 4 - import type { CommandsManager } from './tester/utils' 4 + import type { CommandsManager } from './tester/tester-utils' 5 5 6 6 export async function importId(id: string): Promise<any> { 7 7 const name = `/@id/${id}`.replace(/\\/g, '/')
+1 -1
packages/browser/src/client/vite.config.ts
··· 35 35 /^vitest\//, 36 36 'vitest', 37 37 /^msw/, 38 - '@vitest/browser/context', 38 + 'vitest/browser', 39 39 '@vitest/browser/client', 40 40 ], 41 41 },
-23
packages/browser/src/node/commands/clear.ts
··· 1 - import type { UserEvent } from '../../../context' 2 - import type { UserEventCommand } from './utils' 3 - import { PlaywrightBrowserProvider } from '../providers/playwright' 4 - import { WebdriverBrowserProvider } from '../providers/webdriverio' 5 - 6 - export const clear: UserEventCommand<UserEvent['clear']> = async ( 7 - context, 8 - selector, 9 - ) => { 10 - if (context.provider instanceof PlaywrightBrowserProvider) { 11 - const { iframe } = context 12 - const element = iframe.locator(selector) 13 - await element.clear() 14 - } 15 - else if (context.provider instanceof WebdriverBrowserProvider) { 16 - const browser = context.browser 17 - const element = await browser.$(selector) 18 - await element.clearValue() 19 - } 20 - else { 21 - throw new TypeError(`Provider "${context.provider.name}" does not support clearing elements`) 22 - } 23 - }
-79
packages/browser/src/node/commands/click.ts
··· 1 - import type { UserEvent } from '../../../context' 2 - import type { UserEventCommand } from './utils' 3 - import { PlaywrightBrowserProvider } from '../providers/playwright' 4 - import { WebdriverBrowserProvider } from '../providers/webdriverio' 5 - 6 - export const click: UserEventCommand<UserEvent['click']> = async ( 7 - context, 8 - selector, 9 - options = {}, 10 - ) => { 11 - const provider = context.provider 12 - if (provider instanceof PlaywrightBrowserProvider) { 13 - const tester = context.iframe 14 - await tester.locator(selector).click(options) 15 - } 16 - else if (provider instanceof WebdriverBrowserProvider) { 17 - const browser = context.browser 18 - await browser.$(selector).click(options as any) 19 - } 20 - else { 21 - throw new TypeError(`Provider "${provider.name}" doesn't support click command`) 22 - } 23 - } 24 - 25 - export const dblClick: UserEventCommand<UserEvent['dblClick']> = async ( 26 - context, 27 - selector, 28 - options = {}, 29 - ) => { 30 - const provider = context.provider 31 - if (provider instanceof PlaywrightBrowserProvider) { 32 - const tester = context.iframe 33 - await tester.locator(selector).dblclick(options) 34 - } 35 - else if (provider instanceof WebdriverBrowserProvider) { 36 - const browser = context.browser 37 - await browser.$(selector).doubleClick() 38 - } 39 - else { 40 - throw new TypeError(`Provider "${provider.name}" doesn't support dblClick command`) 41 - } 42 - } 43 - 44 - export const tripleClick: UserEventCommand<UserEvent['tripleClick']> = async ( 45 - context, 46 - selector, 47 - options = {}, 48 - ) => { 49 - const provider = context.provider 50 - if (provider instanceof PlaywrightBrowserProvider) { 51 - const tester = context.iframe 52 - await tester.locator(selector).click({ 53 - ...options, 54 - clickCount: 3, 55 - }) 56 - } 57 - else if (provider instanceof WebdriverBrowserProvider) { 58 - const browser = context.browser 59 - await browser 60 - .action('pointer', { parameters: { pointerType: 'mouse' } }) 61 - // move the pointer over the button 62 - .move({ origin: browser.$(selector) }) 63 - // simulate 3 clicks 64 - .down() 65 - .up() 66 - .pause(50) 67 - .down() 68 - .up() 69 - .pause(50) 70 - .down() 71 - .up() 72 - .pause(50) 73 - // run the sequence 74 - .perform() 75 - } 76 - else { 77 - throw new TypeError(`Provider "${provider.name}" doesn't support tripleClick command`) 78 - } 79 - }
-42
packages/browser/src/node/commands/dragAndDrop.ts
··· 1 - import type { UserEvent } from '../../../context' 2 - import type { UserEventCommand } from './utils' 3 - import { PlaywrightBrowserProvider } from '../providers/playwright' 4 - import { WebdriverBrowserProvider } from '../providers/webdriverio' 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 - const frame = await context.frame() 14 - await frame.dragAndDrop( 15 - source, 16 - target, 17 - options_, 18 - ) 19 - } 20 - else if (context.provider instanceof WebdriverBrowserProvider) { 21 - const $source = context.browser.$(source) 22 - const $target = context.browser.$(target) 23 - const options = (options_ || {}) as any 24 - const duration = options.duration ?? 10 25 - 26 - // https://github.com/webdriverio/webdriverio/issues/8022#issuecomment-1700919670 27 - await context.browser 28 - .action('pointer') 29 - .move({ duration: 0, origin: $source, x: options.sourceX ?? 0, y: options.sourceY ?? 0 }) 30 - .down({ button: 0 }) 31 - .move({ duration: 0, origin: 'pointer', x: 0, y: 0 }) 32 - .pause(duration) 33 - .move({ duration: 0, origin: $target, x: options.targetX ?? 0, y: options.targetY ?? 0 }) 34 - .move({ duration: 0, origin: 'pointer', x: 1, y: 0 }) 35 - .move({ duration: 0, origin: 'pointer', x: -1, y: 0 }) 36 - .up({ button: 0 }) 37 - .perform() 38 - } 39 - else { 40 - throw new TypeError(`Provider "${context.provider.name}" does not support dragging elements`) 41 - } 42 - }
-24
packages/browser/src/node/commands/fill.ts
··· 1 - import type { UserEvent } from '../../../context' 2 - import type { UserEventCommand } from './utils' 3 - import { PlaywrightBrowserProvider } from '../providers/playwright' 4 - import { WebdriverBrowserProvider } from '../providers/webdriverio' 5 - 6 - export const fill: UserEventCommand<UserEvent['fill']> = async ( 7 - context, 8 - selector, 9 - text, 10 - options = {}, 11 - ) => { 12 - if (context.provider instanceof PlaywrightBrowserProvider) { 13 - const { iframe } = context 14 - const element = iframe.locator(selector) 15 - await element.fill(text, options) 16 - } 17 - else if (context.provider instanceof WebdriverBrowserProvider) { 18 - const browser = context.browser 19 - await browser.$(selector).setValue(text) 20 - } 21 - else { 22 - throw new TypeError(`Provider "${context.provider.name}" does not support filling inputs`) 23 - } 24 - }
-21
packages/browser/src/node/commands/hover.ts
··· 1 - import type { UserEvent } from '../../../context' 2 - import type { UserEventCommand } from './utils' 3 - import { PlaywrightBrowserProvider } from '../providers/playwright' 4 - import { WebdriverBrowserProvider } from '../providers/webdriverio' 5 - 6 - export const hover: UserEventCommand<UserEvent['hover']> = async ( 7 - context, 8 - selector, 9 - options = {}, 10 - ) => { 11 - if (context.provider instanceof PlaywrightBrowserProvider) { 12 - await context.iframe.locator(selector).hover(options) 13 - } 14 - else if (context.provider instanceof WebdriverBrowserProvider) { 15 - const browser = context.browser 16 - await browser.$(selector).moveTo(options as any) 17 - } 18 - else { 19 - throw new TypeError(`Provider "${context.provider.name}" does not support hover`) 20 - } 21 - }
-38
packages/browser/src/node/commands/index.ts
··· 1 - import { clear } from './clear' 2 - import { click, dblClick, tripleClick } from './click' 3 - import { dragAndDrop } from './dragAndDrop' 4 - import { fill } from './fill' 5 1 import { 6 2 _fileInfo, 7 3 readFile, 8 4 removeFile, 9 5 writeFile, 10 6 } from './fs' 11 - import { hover } from './hover' 12 - import { keyboard, keyboardCleanup } from './keyboard' 13 7 import { screenshot } from './screenshot' 14 8 import { screenshotMatcher } from './screenshotMatcher' 15 - import { selectOptions } from './select' 16 - import { tab } from './tab' 17 - import { 18 - annotateTraces, 19 - deleteTracing, 20 - startChunkTrace, 21 - startTracing, 22 - stopChunkTrace, 23 - } from './trace' 24 - import { type } from './type' 25 - import { upload } from './upload' 26 - import { viewport } from './viewport' 27 9 28 10 export default { 29 11 readFile: readFile as typeof readFile, 30 12 removeFile: removeFile as typeof removeFile, 31 13 writeFile: writeFile as typeof writeFile, 32 - 33 14 // private commands 34 15 __vitest_fileInfo: _fileInfo as typeof _fileInfo, 35 - __vitest_upload: upload as typeof upload, 36 - __vitest_click: click as typeof click, 37 - __vitest_dblClick: dblClick as typeof dblClick, 38 - __vitest_tripleClick: tripleClick as typeof tripleClick, 39 16 __vitest_screenshot: screenshot as typeof screenshot, 40 - __vitest_type: type as typeof type, 41 - __vitest_clear: clear as typeof clear, 42 - __vitest_fill: fill as typeof fill, 43 - __vitest_tab: tab as typeof tab, 44 - __vitest_keyboard: keyboard as typeof keyboard, 45 - __vitest_selectOptions: selectOptions as typeof selectOptions, 46 - __vitest_dragAndDrop: dragAndDrop as typeof dragAndDrop, 47 - __vitest_hover: hover as typeof hover, 48 - __vitest_cleanup: keyboardCleanup as typeof keyboardCleanup, 49 - __vitest_viewport: viewport as typeof viewport, 50 17 __vitest_screenshotMatcher: screenshotMatcher as typeof screenshotMatcher, 51 - __vitest_deleteTracing: deleteTracing as typeof deleteTracing, 52 - __vitest_startChunkTrace: startChunkTrace as typeof startChunkTrace, 53 - __vitest_startTracing: startTracing as typeof startTracing, 54 - __vitest_stopChunkTrace: stopChunkTrace as typeof stopChunkTrace, 55 - __vitest_annotateTraces: annotateTraces as typeof annotateTraces, 56 18 }
-208
packages/browser/src/node/commands/keyboard.ts
··· 1 - import type { BrowserProvider } from 'vitest/node' 2 - import type { UserEventCommand } from './utils' 3 - import { defaultKeyMap } from '@testing-library/user-event/dist/esm/keyboard/keyMap.js' 4 - import { parseKeyDef } from '@testing-library/user-event/dist/esm/keyboard/parseKeyDef.js' 5 - import { PlaywrightBrowserProvider } from '../providers/playwright' 6 - import { WebdriverBrowserProvider } from '../providers/webdriverio' 7 - 8 - export interface KeyboardState { 9 - unreleased: string[] 10 - } 11 - 12 - export const keyboard: UserEventCommand<(text: string, state: KeyboardState) => Promise<{ unreleased: string[] }>> = async ( 13 - context, 14 - text, 15 - state, 16 - ) => { 17 - if (context.provider instanceof PlaywrightBrowserProvider) { 18 - const frame = await context.frame() 19 - await frame.evaluate(focusIframe) 20 - } 21 - else if (context.provider instanceof WebdriverBrowserProvider) { 22 - await context.browser.execute(focusIframe) 23 - } 24 - 25 - const pressed = new Set<string>(state.unreleased) 26 - 27 - await keyboardImplementation( 28 - pressed, 29 - context.provider, 30 - context.sessionId, 31 - text, 32 - async () => { 33 - if (context.provider instanceof PlaywrightBrowserProvider) { 34 - const frame = await context.frame() 35 - await frame.evaluate(selectAll) 36 - } 37 - else if (context.provider instanceof WebdriverBrowserProvider) { 38 - await context.browser.execute(selectAll) 39 - } 40 - else { 41 - throw new TypeError(`Provider "${context.provider.name}" does not support selecting all text`) 42 - } 43 - }, 44 - true, 45 - ) 46 - 47 - return { 48 - unreleased: Array.from(pressed), 49 - } 50 - } 51 - 52 - export const keyboardCleanup: UserEventCommand<(state: KeyboardState) => Promise<void>> = async ( 53 - context, 54 - state, 55 - ) => { 56 - const { provider, sessionId } = context 57 - if (!state.unreleased) { 58 - return 59 - } 60 - if (provider instanceof PlaywrightBrowserProvider) { 61 - const page = provider.getPage(sessionId) 62 - for (const key of state.unreleased) { 63 - await page.keyboard.up(key) 64 - } 65 - } 66 - else if (provider instanceof WebdriverBrowserProvider) { 67 - const keyboard = provider.browser!.action('key') 68 - for (const key of state.unreleased) { 69 - keyboard.up(key) 70 - } 71 - await keyboard.perform() 72 - } 73 - else { 74 - throw new TypeError(`Provider "${context.provider.name}" does not support keyboard api`) 75 - } 76 - } 77 - 78 - // fallback to insertText for non US key 79 - // https://github.com/microsoft/playwright/blob/50775698ae13642742f2a1e8983d1d686d7f192d/packages/playwright-core/src/server/input.ts#L95 80 - const VALID_KEYS = new Set(['Escape', 'F1', 'F2', 'F3', 'F4', 'F5', 'F6', 'F7', 'F8', 'F9', 'F10', 'F11', 'F12', 'Backquote', '`', '~', 'Digit1', '1', '!', 'Digit2', '2', '@', 'Digit3', '3', '#', 'Digit4', '4', '$', 'Digit5', '5', '%', 'Digit6', '6', '^', 'Digit7', '7', '&', 'Digit8', '8', '*', 'Digit9', '9', '(', 'Digit0', '0', ')', 'Minus', '-', '_', 'Equal', '=', '+', 'Backslash', '\\', '|', 'Backspace', 'Tab', 'KeyQ', 'q', 'Q', 'KeyW', 'w', 'W', 'KeyE', 'e', 'E', 'KeyR', 'r', 'R', 'KeyT', 't', 'T', 'KeyY', 'y', 'Y', 'KeyU', 'u', 'U', 'KeyI', 'i', 'I', 'KeyO', 'o', 'O', 'KeyP', 'p', 'P', 'BracketLeft', '[', '{', 'BracketRight', ']', '}', 'CapsLock', 'KeyA', 'a', 'A', 'KeyS', 's', 'S', 'KeyD', 'd', 'D', 'KeyF', 'f', 'F', 'KeyG', 'g', 'G', 'KeyH', 'h', 'H', 'KeyJ', 'j', 'J', 'KeyK', 'k', 'K', 'KeyL', 'l', 'L', 'Semicolon', ';', ':', 'Quote', '\'', '"', 'Enter', '\n', '\r', 'ShiftLeft', 'Shift', 'KeyZ', 'z', 'Z', 'KeyX', 'x', 'X', 'KeyC', 'c', 'C', 'KeyV', 'v', 'V', 'KeyB', 'b', 'B', 'KeyN', 'n', 'N', 'KeyM', 'm', 'M', 'Comma', ',', '<', 'Period', '.', '>', 'Slash', '/', '?', 'ShiftRight', 'ControlLeft', 'Control', 'MetaLeft', 'Meta', 'AltLeft', 'Alt', 'Space', ' ', 'AltRight', 'AltGraph', 'MetaRight', 'ContextMenu', 'ControlRight', 'PrintScreen', 'ScrollLock', 'Pause', 'PageUp', 'PageDown', 'Insert', 'Delete', 'Home', 'End', 'ArrowLeft', 'ArrowUp', 'ArrowRight', 'ArrowDown', 'NumLock', 'NumpadDivide', 'NumpadMultiply', 'NumpadSubtract', 'Numpad7', 'Numpad8', 'Numpad9', 'Numpad4', 'Numpad5', 'Numpad6', 'NumpadAdd', 'Numpad1', 'Numpad2', 'Numpad3', 'Numpad0', 'NumpadDecimal', 'NumpadEnter', 'ControlOrMeta']) 81 - 82 - export async function keyboardImplementation( 83 - pressed: Set<string>, 84 - provider: BrowserProvider, 85 - sessionId: string, 86 - text: string, 87 - selectAll: () => Promise<void>, 88 - skipRelease: boolean, 89 - ): Promise<{ pressed: Set<string> }> { 90 - if (provider instanceof PlaywrightBrowserProvider) { 91 - const page = provider.getPage(sessionId) 92 - const actions = parseKeyDef(defaultKeyMap, text) 93 - 94 - for (const { releasePrevious, releaseSelf, repeat, keyDef } of actions) { 95 - const key = keyDef.key! 96 - 97 - // TODO: instead of calling down/up for each key, join non special 98 - // together, and call `type` once for all non special keys, 99 - // and then `press` for special keys 100 - if (pressed.has(key)) { 101 - if (VALID_KEYS.has(key)) { 102 - await page.keyboard.up(key) 103 - } 104 - pressed.delete(key) 105 - } 106 - 107 - if (!releasePrevious) { 108 - if (key === 'selectall') { 109 - await selectAll() 110 - continue 111 - } 112 - 113 - for (let i = 1; i <= repeat; i++) { 114 - if (VALID_KEYS.has(key)) { 115 - await page.keyboard.down(key) 116 - } 117 - else { 118 - await page.keyboard.insertText(key) 119 - } 120 - } 121 - 122 - if (releaseSelf) { 123 - if (VALID_KEYS.has(key)) { 124 - await page.keyboard.up(key) 125 - } 126 - } 127 - else { 128 - pressed.add(key) 129 - } 130 - } 131 - } 132 - 133 - if (!skipRelease && pressed.size) { 134 - for (const key of pressed) { 135 - if (VALID_KEYS.has(key)) { 136 - await page.keyboard.up(key) 137 - } 138 - } 139 - } 140 - } 141 - else if (provider instanceof WebdriverBrowserProvider) { 142 - const { Key } = await import('webdriverio') 143 - const browser = provider.browser! 144 - const actions = parseKeyDef(defaultKeyMap, text) 145 - 146 - let keyboard = browser.action('key') 147 - 148 - for (const { releasePrevious, releaseSelf, repeat, keyDef } of actions) { 149 - let key = keyDef.key! 150 - const special = Key[key as 'Shift'] 151 - 152 - if (special) { 153 - key = special 154 - } 155 - 156 - if (pressed.has(key)) { 157 - keyboard.up(key) 158 - pressed.delete(key) 159 - } 160 - 161 - if (!releasePrevious) { 162 - if (key === 'selectall') { 163 - await keyboard.perform() 164 - keyboard = browser.action('key') 165 - await selectAll() 166 - continue 167 - } 168 - 169 - for (let i = 1; i <= repeat; i++) { 170 - keyboard.down(key) 171 - } 172 - 173 - if (releaseSelf) { 174 - keyboard.up(key) 175 - } 176 - else { 177 - pressed.add(key) 178 - } 179 - } 180 - } 181 - 182 - // seems like webdriverio doesn't release keys automatically if skipRelease is true and all events are keyUp 183 - const allRelease = keyboard.toJSON().actions.every(action => action.type === 'keyUp') 184 - 185 - await keyboard.perform(allRelease ? false : skipRelease) 186 - } 187 - 188 - return { 189 - pressed, 190 - } 191 - } 192 - 193 - function focusIframe() { 194 - if ( 195 - !document.activeElement 196 - || document.activeElement.ownerDocument !== document 197 - || document.activeElement === document.body 198 - ) { 199 - window.focus() 200 - } 201 - } 202 - 203 - function selectAll() { 204 - const element = document.activeElement as HTMLInputElement 205 - if (element && typeof element.select === 'function') { 206 - element.select() 207 - } 208 - }
+14 -116
packages/browser/src/node/commands/screenshot.ts
··· 1 - import type { BrowserCommand, BrowserCommandContext, ResolvedConfig } from 'vitest/node' 1 + import type { BrowserCommand } from 'vitest/node' 2 2 import type { ScreenshotOptions } from '../../../context' 3 - import { mkdir, rm } from 'node:fs/promises' 4 - import { normalize as platformNormalize } from 'node:path' 5 - import { nanoid } from '@vitest/utils/helpers' 6 - import { basename, dirname, normalize, relative, resolve } from 'pathe' 7 - import { PlaywrightBrowserProvider } from '../providers/playwright' 8 - import { WebdriverBrowserProvider } from '../providers/webdriverio' 9 3 10 4 interface ScreenshotCommandOptions extends Omit<ScreenshotOptions, 'element' | 'mask'> { 11 5 element?: string 12 6 mask?: readonly string[] 13 7 } 14 8 9 + declare module 'vitest/browser' { 10 + interface BrowserCommands { 11 + /** 12 + * @internal 13 + */ 14 + __vitest_takeScreenshot: (name: string, options: ScreenshotCommandOptions) => Promise<{ 15 + buffer: Buffer 16 + path: string 17 + }> 18 + } 19 + } 20 + 15 21 export const screenshot: BrowserCommand<[string, ScreenshotCommandOptions]> = async ( 16 22 context, 17 23 name: string, ··· 23 29 options.base64 = true 24 30 } 25 31 26 - const { buffer, path } = await takeScreenshot(context, name, options) 32 + const { buffer, path } = await context.triggerCommand('__vitest_takeScreenshot', name, options) 27 33 28 34 return returnResult(options, path, buffer) 29 - } 30 - 31 - /** 32 - * Takes a screenshot using the provided browser context and returns a buffer and the expected screenshot path. 33 - * 34 - * **Note**: the returned `path` indicates where the screenshot *might* be found. 35 - * It is not guaranteed to exist, especially if `options.save` is `false`. 36 - * 37 - * @throws {Error} If the function is not called within a test or if the browser provider does not support screenshots. 38 - */ 39 - export async function takeScreenshot( 40 - context: BrowserCommandContext, 41 - name: string, 42 - options: Omit<ScreenshotCommandOptions, 'base64'>, 43 - ): Promise<{ buffer: Buffer<ArrayBufferLike>; path: string }> { 44 - if (!context.testPath) { 45 - throw new Error(`Cannot take a screenshot without a test path`) 46 - } 47 - 48 - const path = resolveScreenshotPath( 49 - context.testPath, 50 - name, 51 - context.project.config, 52 - options.path, 53 - ) 54 - 55 - // playwright does not need a screenshot path if we don't intend to save it 56 - let savePath: string | undefined 57 - 58 - if (options.save) { 59 - savePath = normalize(path) 60 - 61 - await mkdir(dirname(savePath), { recursive: true }) 62 - } 63 - 64 - if (context.provider instanceof PlaywrightBrowserProvider) { 65 - const mask = options.mask?.map(selector => context.iframe.locator(selector)) 66 - 67 - if (options.element) { 68 - const { element: selector, ...config } = options 69 - const element = context.iframe.locator(selector) 70 - const buffer = await element.screenshot({ 71 - ...config, 72 - mask, 73 - path: savePath, 74 - }) 75 - return { buffer, path } 76 - } 77 - 78 - const buffer = await context.iframe.locator('body').screenshot({ 79 - ...options, 80 - mask, 81 - path: savePath, 82 - }) 83 - return { buffer, path } 84 - } 85 - 86 - if (context.provider instanceof WebdriverBrowserProvider) { 87 - // webdriverio needs a path, so if one is not already set we create a temporary one 88 - if (savePath === undefined) { 89 - savePath = resolve(context.project.tmpDir, nanoid()) 90 - 91 - await mkdir(context.project.tmpDir, { recursive: true }) 92 - } 93 - 94 - const page = context.provider.browser! 95 - const element = !options.element 96 - ? await page.$('body') 97 - : await page.$(`${options.element}`) 98 - 99 - // webdriverio expects the path to contain the extension and only works with PNG files 100 - const savePathWithExtension = savePath.endsWith('.png') ? savePath : `${savePath}.png` 101 - 102 - // there seems to be a bug in webdriverio, `X:/` gets appended to cwd, so we convert to `X:\` 103 - const buffer = await element.saveScreenshot( 104 - platformNormalize(savePathWithExtension), 105 - ) 106 - if (!options.save) { 107 - await rm(savePathWithExtension, { force: true }) 108 - } 109 - return { buffer, path } 110 - } 111 - 112 - throw new Error( 113 - `Provider "${context.provider.name}" does not support screenshots`, 114 - ) 115 - } 116 - 117 - function resolveScreenshotPath( 118 - testPath: string, 119 - name: string, 120 - config: ResolvedConfig, 121 - customPath: string | undefined, 122 - ): string { 123 - if (customPath) { 124 - return resolve(dirname(testPath), customPath) 125 - } 126 - const dir = dirname(testPath) 127 - const base = basename(testPath) 128 - if (config.browser.screenshotDirectory) { 129 - return resolve( 130 - config.browser.screenshotDirectory, 131 - relative(config.root, dir), 132 - base, 133 - name, 134 - ) 135 - } 136 - return resolve(dir, '__screenshots__', base, name) 137 35 } 138 36 139 37 function returnResult(
+16
packages/browser/src/node/commands/screenshotMatcher/types.ts
··· 1 + import type { 2 + ScreenshotComparatorRegistry, 3 + ScreenshotMatcherOptions, 4 + } from '@vitest/browser/context' 5 + 1 6 interface BaseMetadata { height: number; width: number } 2 7 export type TypedArray 3 8 = | Buffer<ArrayBufferLike> ··· 41 46 createDiff: boolean 42 47 } & Options 43 48 ) => Promisable<{ pass: boolean; diff: TypedArray | null; message: string | null }> 49 + 50 + declare module 'vitest/node' { 51 + export interface ToMatchScreenshotOptions 52 + extends Omit< 53 + ScreenshotMatcherOptions, 54 + 'comparatorName' | 'comparatorOptions' 55 + > {} 56 + 57 + export interface ToMatchScreenshotComparators 58 + extends ScreenshotComparatorRegistry {} 59 + }
+2 -3
packages/browser/src/node/commands/screenshotMatcher/utils.ts
··· 5 5 import { platform } from 'node:os' 6 6 import { deepMerge } from '@vitest/utils/helpers' 7 7 import { basename, dirname, extname, join, relative, resolve } from 'pathe' 8 - import { takeScreenshot } from '../screenshot' 9 8 import { getCodec } from './codecs' 10 9 import { getComparator } from './comparators' 11 10 ··· 238 237 name: string 239 238 screenshotOptions: ScreenshotMatcherArguments[2]['screenshotOptions'] 240 239 }): ReturnType<AnyCodec['decode']> { 241 - return takeScreenshot( 242 - context, 240 + return context.triggerCommand( 241 + '__vitest_takeScreenshot', 243 242 name, 244 243 { ...screenshotOptions, save: false, element }, 245 244 ).then(
-51
packages/browser/src/node/commands/select.ts
··· 1 - import type { ElementHandle } from 'playwright' 2 - import type { UserEvent } from '../../../context' 3 - import type { UserEventCommand } from './utils' 4 - import { PlaywrightBrowserProvider } from '../providers/playwright' 5 - import { WebdriverBrowserProvider } from '../providers/webdriverio' 6 - 7 - export const selectOptions: UserEventCommand<UserEvent['selectOptions']> = async ( 8 - context, 9 - selector, 10 - userValues, 11 - options = {}, 12 - ) => { 13 - if (context.provider instanceof PlaywrightBrowserProvider) { 14 - const value = userValues as any as (string | { element: string })[] 15 - const { iframe } = context 16 - const selectElement = iframe.locator(selector) 17 - 18 - const values = await Promise.all(value.map(async (v) => { 19 - if (typeof v === 'string') { 20 - return v 21 - } 22 - const elementHandler = await iframe.locator(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, options) 30 - } 31 - else if (context.provider instanceof WebdriverBrowserProvider) { 32 - const values = userValues as any as [({ index: number })] 33 - 34 - if (!values.length) { 35 - return 36 - } 37 - 38 - const browser = context.browser 39 - 40 - if (values.length === 1 && 'index' in values[0]) { 41 - const selectElement = browser.$(selector) 42 - await selectElement.selectByIndex(values[0].index) 43 - } 44 - else { 45 - throw new Error('Provider "webdriverio" doesn\'t support selecting multiple values at once') 46 - } 47 - } 48 - else { 49 - throw new TypeError(`Provider "${context.provider.name}" doesn't support selectOptions command`) 50 - } 51 - }
-23
packages/browser/src/node/commands/tab.ts
··· 1 - import type { UserEvent } from '../../../context' 2 - import type { UserEventCommand } from './utils' 3 - import { PlaywrightBrowserProvider } from '../providers/playwright' 4 - import { WebdriverBrowserProvider } from '../providers/webdriverio' 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 - }
+10 -7
packages/browser/src/node/commands/trace.ts packages/browser-playwright/src/commands/trace.ts
··· 1 - import type { BrowserCommandContext } from 'vitest/node' 2 - import type { BrowserCommand } from '../plugin' 1 + import type { BrowserCommand, BrowserCommandContext, BrowserProvider } from 'vitest/node' 2 + import type { PlaywrightBrowserProvider } from '../playwright' 3 3 import { unlink } from 'node:fs/promises' 4 4 import { basename, dirname, relative, resolve } from 'pathe' 5 - import { PlaywrightBrowserProvider } from '../providers/playwright' 6 5 7 6 export const startTracing: BrowserCommand<[]> = async ({ context, project, provider, sessionId }) => { 8 - if (provider instanceof PlaywrightBrowserProvider) { 7 + if (isPlaywrightProvider(provider)) { 9 8 if (provider.tracingContexts.has(sessionId)) { 10 9 return 11 10 } ··· 33 32 if (!testPath) { 34 33 throw new Error(`stopChunkTrace cannot be called outside of the test file.`) 35 34 } 36 - if (provider instanceof PlaywrightBrowserProvider) { 35 + if (isPlaywrightProvider(provider)) { 37 36 if (!provider.tracingContexts.has(sessionId)) { 38 37 await startTracing(command) 39 38 } ··· 49 48 context, 50 49 { name }, 51 50 ) => { 52 - if (context.provider instanceof PlaywrightBrowserProvider) { 51 + if (isPlaywrightProvider(context.provider)) { 53 52 const path = resolveTracesPath(context, name) 54 53 context.provider.pendingTraces.delete(path) 55 54 await context.context.tracing.stopChunk({ path }) ··· 84 83 if (!context.testPath) { 85 84 throw new Error(`stopChunkTrace cannot be called outside of the test file.`) 86 85 } 87 - if (context.provider instanceof PlaywrightBrowserProvider) { 86 + if (isPlaywrightProvider(context.provider)) { 88 87 return Promise.all( 89 88 traces.map(trace => unlink(trace).catch((err) => { 90 89 if (err.code === 'ENOENT') { ··· 124 123 }) 125 124 })) 126 125 } 126 + 127 + function isPlaywrightProvider(provider: BrowserProvider): provider is PlaywrightBrowserProvider { 128 + return provider.name === 'playwright' 129 + }
-62
packages/browser/src/node/commands/type.ts
··· 1 - import type { UserEvent } from '../../../context' 2 - import type { UserEventCommand } from './utils' 3 - import { PlaywrightBrowserProvider } from '../providers/playwright' 4 - import { WebdriverBrowserProvider } from '../providers/webdriverio' 5 - import { keyboardImplementation } from './keyboard' 6 - 7 - export const type: UserEventCommand<UserEvent['type']> = async ( 8 - context, 9 - selector, 10 - text, 11 - options = {}, 12 - ) => { 13 - const { skipClick = false, skipAutoClose = false } = options 14 - const unreleased = new Set(Reflect.get(options, 'unreleased') as string[] ?? []) 15 - 16 - if (context.provider instanceof PlaywrightBrowserProvider) { 17 - const { iframe } = context 18 - const element = iframe.locator(selector) 19 - 20 - if (!skipClick) { 21 - await element.focus() 22 - } 23 - 24 - await keyboardImplementation( 25 - unreleased, 26 - context.provider, 27 - context.sessionId, 28 - text, 29 - () => element.selectText(), 30 - skipAutoClose, 31 - ) 32 - } 33 - else if (context.provider instanceof WebdriverBrowserProvider) { 34 - const browser = context.browser 35 - const element = browser.$(selector) 36 - 37 - if (!skipClick && !await element.isFocused()) { 38 - await element.click() 39 - } 40 - 41 - await keyboardImplementation( 42 - unreleased, 43 - context.provider, 44 - context.sessionId, 45 - text, 46 - () => browser.execute(() => { 47 - const element = document.activeElement as HTMLInputElement 48 - if (element && typeof element.select === 'function') { 49 - element.select() 50 - } 51 - }), 52 - skipAutoClose, 53 - ) 54 - } 55 - else { 56 - throw new TypeError(`Provider "${context.provider.name}" does not support typing`) 57 - } 58 - 59 - return { 60 - unreleased: Array.from(unreleased), 61 - } 62 - }
-55
packages/browser/src/node/commands/upload.ts
··· 1 - import type { UserEventUploadOptions } from '@vitest/browser/context' 2 - import type { UserEventCommand } from './utils' 3 - import { resolve } from 'pathe' 4 - import { PlaywrightBrowserProvider } from '../providers/playwright' 5 - import { WebdriverBrowserProvider } from '../providers/webdriverio' 6 - 7 - export const upload: UserEventCommand<(element: string, files: Array<string | { 8 - name: string 9 - mimeType: string 10 - base64: string 11 - }>, options: UserEventUploadOptions) => void> = async ( 12 - context, 13 - selector, 14 - files, 15 - options, 16 - ) => { 17 - const testPath = context.testPath 18 - if (!testPath) { 19 - throw new Error(`Cannot upload files outside of a test`) 20 - } 21 - const root = context.project.config.root 22 - 23 - if (context.provider instanceof PlaywrightBrowserProvider) { 24 - const { iframe } = context 25 - const playwrightFiles = files.map((file) => { 26 - if (typeof file === 'string') { 27 - return resolve(root, file) 28 - } 29 - return { 30 - name: file.name, 31 - mimeType: file.mimeType, 32 - buffer: Buffer.from(file.base64, 'base64'), 33 - } 34 - }) 35 - await iframe.locator(selector).setInputFiles(playwrightFiles as string[], options) 36 - } 37 - else if (context.provider instanceof WebdriverBrowserProvider) { 38 - for (const file of files) { 39 - if (typeof file !== 'string') { 40 - throw new TypeError(`The "${context.provider.name}" provider doesn't support uploading files objects. Provide a file path instead.`) 41 - } 42 - } 43 - 44 - const element = context.browser.$(selector) 45 - 46 - for (const file of files) { 47 - const filepath = resolve(root, file as string) 48 - const remoteFilePath = await context.browser.uploadFile(filepath) 49 - await element.addValue(remoteFilePath) 50 - } 51 - } 52 - else { 53 - throw new TypeError(`Provider "${context.provider.name}" does not support uploading files via userEvent.upload`) 54 - } 55 - }
+1 -1
packages/browser/src/node/commands/utils.ts packages/browser-playwright/src/commands/utils.ts
··· 1 - import type { Locator } from '@vitest/browser/context' 1 + import type { Locator } from 'vitest/browser' 2 2 import type { BrowserCommand } from 'vitest/node' 3 3 4 4 export type UserEventCommand<T extends (...args: any) => any> = BrowserCommand<
-14
packages/browser/src/node/commands/viewport.ts
··· 1 - import type { UserEventCommand } from './utils' 2 - import { WebdriverBrowserProvider } from '../providers/webdriverio' 3 - 4 - export const viewport: UserEventCommand<(options: { 5 - width: number 6 - height: number 7 - }) => void> = async (context, options) => { 8 - if (context.provider instanceof WebdriverBrowserProvider) { 9 - await context.provider.setViewport(options) 10 - } 11 - else { 12 - throw new TypeError(`Provider ${context.provider.name} doesn't support "viewport" command`) 13 - } 14 - }
+41 -13
packages/browser/src/node/index.ts
··· 1 - import type { Plugin } from 'vitest/config' 2 - import type { TestProject } from 'vitest/node' 1 + import type { BrowserCommand, BrowserProviderOption, BrowserServerFactory } from 'vitest/node' 3 2 import { MockerRegistry } from '@vitest/mocker' 4 3 import { interceptorPlugin } from '@vitest/mocker/node' 5 4 import c from 'tinyrainbow' 6 5 import { createViteLogger, createViteServer } from 'vitest/node' 7 6 import { version } from '../../package.json' 7 + import { distRoot } from './constants' 8 8 import BrowserPlugin from './plugin' 9 9 import { ParentBrowserProject } from './projectParent' 10 10 import { setupBrowserRpc } from './rpc' 11 11 12 - export { distRoot } from './constants' 13 - export { createBrowserPool } from './pool' 12 + export function defineBrowserCommand<T extends unknown[]>( 13 + fn: BrowserCommand<T>, 14 + ): BrowserCommand<T> { 15 + return fn 16 + } 17 + 18 + // export type { ProjectBrowser } from './project' 19 + export { parseKeyDef, resolveScreenshotPath } from './utils' 14 20 15 - export type { ProjectBrowser } from './project' 21 + export const createBrowserServer: BrowserServerFactory = async (options) => { 22 + const project = options.project 23 + const configFile = project.vite.config.configFile 16 24 17 - export async function createBrowserServer( 18 - project: TestProject, 19 - configFile: string | undefined, 20 - prePlugins: Plugin[] = [], 21 - postPlugins: Plugin[] = [], 22 - ): Promise<ParentBrowserProject> { 23 25 if (project.vitest.version !== version) { 24 26 project.vitest.logger.warn( 25 27 c.yellow( ··· 42 44 43 45 const mockerRegistry = new MockerRegistry() 44 46 47 + let cacheDir: string 45 48 const vite = await createViteServer({ 46 49 ...project.options, // spread project config inlined in root workspace config 47 50 base: '/', ··· 71 74 }, 72 75 cacheDir: project.vite.config.cacheDir, 73 76 plugins: [ 74 - ...prePlugins, 77 + { 78 + name: 'vitest-internal:browser-cacheDir', 79 + configResolved(config) { 80 + cacheDir = config.cacheDir 81 + }, 82 + }, 83 + ...options.mocksPlugins({ 84 + filter(id) { 85 + if (id.includes(distRoot) || id.includes(cacheDir)) { 86 + return false 87 + } 88 + return true 89 + }, 90 + }), 91 + options.metaEnvReplacer(), 75 92 ...(project.options?.plugins || []), 76 93 BrowserPlugin(server), 77 94 interceptorPlugin({ registry: mockerRegistry }), 78 - ...postPlugins, 95 + options.coveragePlugin(), 79 96 ], 80 97 }) 81 98 ··· 85 102 86 103 return server 87 104 } 105 + 106 + export function defineBrowserProvider<T extends object = object>(options: Omit< 107 + BrowserProviderOption<T>, 108 + 'serverFactory' | 'options' 109 + > & { options?: T }): BrowserProviderOption { 110 + return { 111 + ...options, 112 + options: options.options || {}, 113 + serverFactory: createBrowserServer, 114 + } 115 + }
+18 -15
packages/browser/src/node/plugin.ts
··· 8 8 import { dynamicImportPlugin } from '@vitest/mocker/node' 9 9 import { toArray } from '@vitest/utils/helpers' 10 10 import MagicString from 'magic-string' 11 - import { basename, dirname, extname, resolve } from 'pathe' 11 + import { basename, dirname, extname, join, resolve } from 'pathe' 12 12 import sirv from 'sirv' 13 13 import { coverageConfigDefaults } from 'vitest/config' 14 14 import { ··· 24 24 import { createTesterMiddleware } from './middlewares/testerMiddleware' 25 25 import BrowserContext from './plugins/pluginContext' 26 26 27 - export { defineBrowserCommand } from './commands/utils' 28 27 export type { BrowserCommand } from 'vitest/node' 29 28 30 29 const versionRegexp = /(?:\?|&)v=\w{8}/ ··· 232 231 233 232 const exclude = [ 234 233 'vitest', 234 + 'vitest/browser', 235 235 'vitest/internal/browser', 236 236 'vitest/runners', 237 - '@vitest/browser', 238 237 '@vitest/browser/client', 239 238 '@vitest/utils', 240 239 '@vitest/utils/source-map', ··· 282 281 'vitest > expect-type', 283 282 'vitest > @vitest/snapshot > magic-string', 284 283 'vitest > @vitest/expect > chai', 285 - '@vitest/browser > @testing-library/user-event', 286 - '@vitest/browser > @testing-library/dom', 287 284 ] 288 285 286 + const provider = parentServer.config.browser.provider || [...parentServer.children][0]?.provider 287 + if (provider?.name === 'preview') { 288 + include.push( 289 + '@vitest/browser-preview > @testing-library/user-event', 290 + '@vitest/browser-preview > @testing-library/dom', 291 + ) 292 + } 293 + 289 294 const fileRoot = browserTestFiles[0] ? dirname(browserTestFiles[0]) : project.config.root 290 295 291 296 const svelte = isPackageExists('vitest-browser-svelte', fileRoot) ··· 549 554 }, 550 555 injectTo: 'head' as const, 551 556 }, 552 - parentServer.locatorsUrl 553 - ? { 554 - tag: 'script', 555 - attrs: { 556 - type: 'module', 557 - src: parentServer.locatorsUrl, 558 - }, 559 - injectTo: 'head', 560 - } as const 561 - : null, 557 + ...parentServer.initScripts.map(script => ({ 558 + tag: 'script', 559 + attrs: { 560 + type: 'module', 561 + src: join('/@fs/', script), 562 + }, 563 + injectTo: 'head', 564 + } as const)), 562 565 ...testerTags, 563 566 ].filter(s => s != null) 564 567 },
+41 -12
packages/browser/src/node/plugins/pluginContext.ts
··· 1 1 import type { Rollup } from 'vite' 2 2 import type { Plugin } from 'vitest/config' 3 + import type { BrowserProvider } from 'vitest/node' 3 4 import type { ParentBrowserProject } from '../projectParent' 4 5 import { fileURLToPath } from 'node:url' 5 6 import { slash } from '@vitest/utils/helpers' 6 7 import { dirname, resolve } from 'pathe' 7 8 8 - const VIRTUAL_ID_CONTEXT = '\0@vitest/browser/context' 9 - const ID_CONTEXT = '@vitest/browser/context' 9 + const VIRTUAL_ID_CONTEXT = '\0vitest/browser' 10 + const ID_CONTEXT = 'vitest/browser' 11 + // for libraries that use an older import but are not type checked 12 + const DEPRECATED_ID_CONTEXT = '@vitest/browser/context' 13 + 14 + const DEPRECATED_VIRTUAL_ID_UTILS = '\0@vitest/browser/utils' 15 + const DEPRECATED_ID_UTILS = '@vitest/browser/utils' 10 16 11 17 const __dirname = dirname(fileURLToPath(import.meta.url)) 12 18 ··· 14 20 return { 15 21 name: 'vitest:browser:virtual-module:context', 16 22 enforce: 'pre', 17 - resolveId(id) { 23 + resolveId(id, importer) { 18 24 if (id === ID_CONTEXT) { 19 25 return VIRTUAL_ID_CONTEXT 20 26 } 27 + if (id === DEPRECATED_ID_CONTEXT) { 28 + if (importer) { 29 + globalServer.vitest.logger.deprecate( 30 + `${importer} tries to load a deprecated "${id}" module. ` 31 + + `This import will stop working in the next major version. ` 32 + + `Please, use "vitest/browser" instead.`, 33 + ) 34 + } 35 + return VIRTUAL_ID_CONTEXT 36 + } 37 + if (id === DEPRECATED_ID_UTILS) { 38 + return DEPRECATED_VIRTUAL_ID_UTILS 39 + } 21 40 }, 22 41 load(id) { 23 42 if (id === VIRTUAL_ID_CONTEXT) { 24 43 return generateContextFile.call(this, globalServer) 25 44 } 45 + if (id === DEPRECATED_VIRTUAL_ID_UTILS) { 46 + return ` 47 + import { utils } from 'vitest/browser' 48 + export const getElementLocatorSelectors = utils.getElementLocatorSelectors 49 + export const debug = utils.debug 50 + export const prettyDOM = utils.prettyDOM 51 + export const getElementError = utils.getElementError 52 + ` 53 + } 26 54 }, 27 55 } 28 56 } ··· 32 60 globalServer: ParentBrowserProject, 33 61 ) { 34 62 const commands = Object.keys(globalServer.commands) 35 - const provider = [...globalServer.children][0].provider || { name: 'preview' } 36 - const providerName = provider.name 63 + const provider = [...globalServer.children][0].provider 64 + const providerName = provider?.name || 'preview' 37 65 38 66 const commandsCode = commands 39 67 .filter(command => !command.startsWith('__vitest')) ··· 43 71 .join('\n') 44 72 45 73 const userEventNonProviderImport = await getUserEventImport( 46 - providerName, 74 + provider, 47 75 this.resolve.bind(this), 48 76 ) 49 77 const distContextPath = slash(`/@fs/${resolve(__dirname, 'context.js')}`) 50 78 51 79 return ` 52 - import { page, createUserEvent, cdp, locators } from '${distContextPath}' 80 + import { page, createUserEvent, cdp, locators, utils } from '${distContextPath}' 53 81 ${userEventNonProviderImport} 54 82 55 83 export const server = { ··· 64 92 } 65 93 export const commands = server.commands 66 94 export const userEvent = createUserEvent(_userEventSetup) 67 - export { page, cdp, locators } 95 + export { page, cdp, locators, utils } 68 96 ` 69 97 } 70 98 71 - async function getUserEventImport(provider: string, resolve: (id: string, importer: string) => Promise<null | { id: string }>) { 72 - if (provider !== 'preview') { 99 + async function getUserEventImport(provider: BrowserProvider | undefined, resolve: (id: string, importer: string) => Promise<null | { id: string }>) { 100 + if (!provider || provider.name !== 'preview') { 73 101 return 'const _userEventSetup = undefined' 74 102 } 75 - const resolved = await resolve('@testing-library/user-event', __dirname) 103 + const previewDistRoot = (provider as any).distRoot 104 + const resolved = await resolve('@testing-library/user-event', previewDistRoot) 76 105 if (!resolved) { 77 - throw new Error(`Failed to resolve user-event package from ${__dirname}`) 106 + throw new Error(`Failed to resolve user-event package from ${previewDistRoot}`) 78 107 } 79 108 return `\ 80 109 import { userEvent as __vitest_user_event__ } from '${slash(`/@fs/${resolved.id}`)}'
+6 -11
packages/browser/src/node/pool.ts packages/vitest/src/node/pools/browser.ts
··· 1 1 import type { DeferPromise } from '@vitest/utils/helpers' 2 - import type { 3 - BrowserProvider, 4 - ProcessPool, 5 - TestProject, 6 - TestSpecification, 7 - Vitest, 8 - } from 'vitest/node' 2 + import type { Vitest } from '../core' 3 + import type { ProcessPool } from '../pool' 4 + import type { TestProject } from '../project' 5 + import type { TestSpecification } from '../spec' 6 + import type { BrowserProvider } from '../types/browser' 9 7 import crypto from 'node:crypto' 10 8 import * as nodeos from 'node:os' 11 - import { performance } from 'node:perf_hooks' 12 9 import { createDefer } from '@vitest/utils/helpers' 13 10 import { stringify } from 'flatted' 14 - import { createDebugger } from 'vitest/node' 11 + import { createDebugger } from '../../utils/debugger' 15 12 16 13 const debug = createDebugger('vitest:browser:pool') 17 14 ··· 310 307 if (!this._promise) { 311 308 throw new Error(`Unexpected empty queue`) 312 309 } 313 - const startTime = performance.now() 314 310 315 311 const orchestrator = this.getOrchestrator(sessionId) 316 312 debug?.('[%s] run test %s', sessionId, file) ··· 324 320 // this will be parsed by the test iframe, not the orchestrator 325 321 // so we need to stringify it first to avoid double serialization 326 322 providedContext: this._providedContext || '[{}]', 327 - startTime, 328 323 }, 329 324 ) 330 325 .then(() => {
+44
packages/browser/src/node/project.ts
··· 2 2 import type { ViteDevServer } from 'vite' 3 3 import type { ParsedStack, SerializedConfig, TestError } from 'vitest' 4 4 import type { 5 + BrowserCommand, 6 + BrowserCommandContext, 5 7 BrowserProvider, 6 8 ProjectBrowser as IProjectBrowser, 7 9 ResolvedConfig, 8 10 TestProject, 9 11 Vitest, 10 12 } from 'vitest/node' 13 + import type { BrowserCommands } from '../../context' 11 14 import type { ParentBrowserProject } from './projectParent' 12 15 import { existsSync } from 'node:fs' 13 16 import { readFile } from 'node:fs/promises' ··· 57 60 return this.parent.vite 58 61 } 59 62 63 + private commands = {} as Record<string, BrowserCommand<any, any>> 64 + 65 + public registerCommand<K extends keyof BrowserCommands>( 66 + name: K, 67 + cb: BrowserCommand< 68 + Parameters<BrowserCommands[K]>, 69 + ReturnType<BrowserCommands[K]> 70 + >, 71 + ): void { 72 + if (!/^[a-z_$][\w$]*$/i.test(name)) { 73 + throw new Error( 74 + `Invalid command name "${name}". Only alphanumeric characters, $ and _ are allowed.`, 75 + ) 76 + } 77 + this.commands[name] = cb 78 + } 79 + 80 + public triggerCommand = (<K extends keyof BrowserCommand>( 81 + name: K, 82 + context: BrowserCommandContext, 83 + ...args: Parameters<BrowserCommands[K]> 84 + ): ReturnType<BrowserCommands[K]> => { 85 + if (name in this.commands) { 86 + return this.commands[name](context, ...args) 87 + } 88 + if (name in this.parent.commands) { 89 + return this.parent.commands[name](context, ...args) 90 + } 91 + throw new Error(`Provider ${this.provider.name} does not support command "${name}".`) 92 + }) as any 93 + 60 94 wrapSerializedConfig(): SerializedConfig { 61 95 const config = wrapConfig(this.project.serializedConfig) 62 96 config.env ??= {} ··· 69 103 return 70 104 } 71 105 this.provider = await getBrowserProvider(project.config.browser, project) 106 + if (this.provider.initScripts) { 107 + this.parent.initScripts = this.provider.initScripts 108 + // make sure the script can be imported 109 + this.provider.initScripts.forEach((script) => { 110 + const allow = this.parent.vite.config.server.fs.allow 111 + if (!allow.includes(script)) { 112 + allow.push(script) 113 + } 114 + }) 115 + } 72 116 } 73 117 74 118 public parseErrorStacktrace(
+2 -5
packages/browser/src/node/projectParent.ts
··· 38 38 public matchersUrl: string 39 39 public stateJs: Promise<string> | string 40 40 41 + public initScripts: string[] = [] 42 + 41 43 public commands: Record<string, BrowserCommand<any>> = {} 42 44 public children: Set<ProjectBrowser> = new Set() 43 45 public vitest: Vitest ··· 129 131 ).then(js => (this.injectorJs = js)) 130 132 this.errorCatcherUrl = join('/@fs/', resolve(distRoot, 'client/error-catcher.js')) 131 133 132 - const builtinProviders = ['playwright', 'webdriverio', 'preview'] 133 - const providerName = project.config.browser.provider?.name || 'preview' 134 - if (builtinProviders.includes(providerName)) { 135 - this.locatorsUrl = join('/@fs/', distRoot, 'locators', `${providerName}.js`) 136 - } 137 134 this.matchersUrl = join('/@fs/', distRoot, 'expect-element.js') 138 135 this.stateJs = readFile( 139 136 resolve(distRoot, 'state.js'),
-3
packages/browser/src/node/providers/index.ts
··· 1 - export { playwright } from './playwright' 2 - export { preview } from './preview' 3 - export { webdriverio } from './webdriverio'
+27 -13
packages/browser/src/node/providers/playwright.ts packages/browser-playwright/src/playwright.ts
··· 1 1 /* eslint-disable ts/method-signature-style */ 2 2 3 - import type { 4 - ScreenshotComparatorRegistry, 5 - ScreenshotMatcherOptions, 6 - } from '@vitest/browser/context' 7 3 import type { MockedModule } from '@vitest/mocker' 8 4 import type { 9 5 Browser, ··· 19 15 import type { SourceMap } from 'rollup' 20 16 import type { ResolvedConfig } from 'vite' 21 17 import type { 18 + Locator, 19 + ScreenshotComparatorRegistry, 20 + ScreenshotMatcherOptions, 21 + } from 'vitest/browser' 22 + import type { 23 + BrowserCommand, 22 24 BrowserModuleMocker, 23 25 BrowserProvider, 24 26 BrowserProviderOption, 25 27 CDPSession, 26 28 TestProject, 27 29 } from 'vitest/node' 30 + import { defineBrowserProvider } from '@vitest/browser' 28 31 import { createManualModuleSource } from '@vitest/mocker/node' 32 + import { resolve } from 'pathe' 29 33 import c from 'tinyrainbow' 30 34 import { createDebugger, isCSSRequest } from 'vitest/node' 35 + import commands from './commands' 36 + import { distRoot } from './constants' 31 37 32 38 const debug = createDebugger('vitest:browser:playwright') 33 39 ··· 68 74 } 69 75 70 76 export function playwright(options: PlaywrightProviderOptions = {}): BrowserProviderOption<PlaywrightProviderOptions> { 71 - return { 77 + return defineBrowserProvider({ 72 78 name: 'playwright', 73 79 supportedBrowser: playwrightBrowsers, 74 80 options, 75 - factory(project) { 81 + providerFactory(project) { 76 82 return new PlaywrightBrowserProvider(project, options) 77 83 }, 78 - // --browser.provider=playwright 79 - // @ts-expect-error hidden way to bypass importing playwright 80 - _cli: true, 81 - } 84 + }) 82 85 } 83 86 84 87 export class PlaywrightBrowserProvider implements BrowserProvider { ··· 98 101 public tracingContexts: Set<string> = new Set() 99 102 public pendingTraces: Map<string, string> = new Map() 100 103 104 + public initScripts: string[] = [ 105 + resolve(distRoot, 'locators.js'), 106 + ] 107 + 101 108 constructor( 102 109 private project: TestProject, 103 110 private options: PlaywrightProviderOptions, 104 111 ) { 105 112 this.browserName = project.config.browser.name as PlaywrightBrowser 106 113 this.mocker = this.createMocker() 114 + 115 + for (const [name, command] of Object.entries(commands)) { 116 + project.browser!.registerCommand(name as any, command as BrowserCommand) 117 + } 107 118 108 119 // make sure the traces are finished if the test hangs 109 120 process.on('SIGTERM', () => { ··· 432 443 return page 433 444 } 434 445 435 - async openPage(sessionId: string, url: string, beforeNavigate?: () => Promise<void>): Promise<void> { 446 + async openPage(sessionId: string, url: string): Promise<void> { 436 447 debug?.('[%s][%s] creating the browser page for %s', sessionId, this.browserName, url) 437 448 const browserPage = await this.openBrowserPage(sessionId) 438 - await beforeNavigate?.() 439 449 debug?.('[%s][%s] browser page is created, opening %s', sessionId, this.browserName, url) 440 450 await browserPage.goto(url, { timeout: 0 }) 441 451 await this._throwIfClosing(browserPage) ··· 538 548 context: BrowserContext 539 549 } 540 550 551 + export interface _BrowserNames { 552 + playwright: PlaywrightBrowser 553 + } 554 + 541 555 export interface ToMatchScreenshotOptions 542 556 extends Omit< 543 557 ScreenshotMatcherOptions, ··· 557 571 type PWDragAndDropOptions = NonNullable<Parameters<Page['dragAndDrop']>[2]> 558 572 type PWSetInputFiles = NonNullable<Parameters<Page['setInputFiles']>[2]> 559 573 560 - declare module '@vitest/browser/context' { 574 + declare module 'vitest/browser' { 561 575 export interface UserEventHoverOptions extends PWHoverOptions {} 562 576 export interface UserEventClickOptions extends PWClickOptions {} 563 577 export interface UserEventDoubleClickOptions extends PWDoubleClickOptions {}
+16 -8
packages/browser/src/node/providers/preview.ts packages/browser-preview/src/preview.ts
··· 1 1 import type { BrowserProvider, BrowserProviderOption, TestProject } from 'vitest/node' 2 + import { nextTick } from 'node:process' 3 + import { defineBrowserProvider } from '@vitest/browser' 4 + import { resolve } from 'pathe' 5 + import { distRoot } from './constants' 2 6 3 7 export function preview(): BrowserProviderOption { 4 - return { 8 + return defineBrowserProvider({ 5 9 name: 'preview', 6 - options: {}, 7 - factory(project) { 10 + providerFactory(project) { 8 11 return new PreviewBrowserProvider(project) 9 12 }, 10 - // --browser.provider=preview 11 - // @ts-expect-error hidden way to bypass importing preview 12 - _cli: true, 13 - } 13 + }) 14 14 } 15 15 16 16 export class PreviewBrowserProvider implements BrowserProvider { ··· 19 19 private project!: TestProject 20 20 private open = false 21 21 22 + public distRoot: string = distRoot 23 + 24 + public initScripts: string[] = [ 25 + resolve(distRoot, 'locators.js'), 26 + ] 27 + 22 28 constructor(project: TestProject) { 23 29 this.project = project 24 30 this.open = false ··· 27 33 'You\'ve enabled headless mode for "preview" provider but it doesn\'t support it. Use "playwright" or "webdriverio" instead: https://vitest.dev/guide/browser/#configuration', 28 34 ) 29 35 } 30 - project.vitest.logger.printBrowserBanner(project) 36 + nextTick(() => { 37 + project.vitest.logger.printBrowserBanner(project) 38 + }) 31 39 } 32 40 33 41 isOpen(): boolean {
+30 -13
packages/browser/src/node/providers/webdriverio.ts packages/browser-webdriverio/src/webdriverio.ts
··· 1 + import type { Capabilities } from '@wdio/types' 1 2 import type { 2 3 ScreenshotComparatorRegistry, 3 4 ScreenshotMatcherOptions, 4 - } from '@vitest/browser/context' 5 - import type { Capabilities } from '@wdio/types' 5 + } from 'vitest/browser' 6 6 import type { 7 + BrowserCommand, 7 8 BrowserProvider, 8 9 BrowserProviderOption, 9 10 CDPSession, 10 11 TestProject, 11 12 } from 'vitest/node' 12 - import type { ClickOptions, DragAndDropOptions, remote } from 'webdriverio' 13 + import type { ClickOptions, DragAndDropOptions, MoveToOptions, remote } from 'webdriverio' 14 + import { defineBrowserProvider } from '@vitest/browser' 13 15 16 + import { resolve } from 'pathe' 14 17 import { createDebugger } from 'vitest/node' 18 + import commands from './commands' 19 + import { distRoot } from './constants' 15 20 16 21 const debug = createDebugger('vitest:browser:wdio') 17 22 18 23 const webdriverBrowsers = ['firefox', 'chrome', 'edge', 'safari'] as const 19 24 type WebdriverBrowser = (typeof webdriverBrowsers)[number] 20 25 21 - interface WebdriverProviderOptions extends Partial< 26 + export interface WebdriverProviderOptions extends Partial< 22 27 Parameters<typeof remote>[0] 23 28 > {} 24 29 25 30 export function webdriverio(options: WebdriverProviderOptions = {}): BrowserProviderOption<WebdriverProviderOptions> { 26 - return { 31 + return defineBrowserProvider({ 27 32 name: 'webdriverio', 28 33 supportedBrowser: webdriverBrowsers, 29 34 options, 30 - factory(project) { 35 + providerFactory(project) { 31 36 return new WebdriverBrowserProvider(project, options) 32 37 }, 33 - // --browser.provider=webdriverio 34 - // @ts-expect-error hidden way to bypass importing webdriverio 35 - _cli: true, 36 - } 38 + }) 37 39 } 38 40 39 41 export class WebdriverBrowserProvider implements BrowserProvider { ··· 51 53 private iframeSwitched = false 52 54 private topLevelContext: string | undefined 53 55 56 + public initScripts: string[] = [ 57 + resolve(distRoot, 'locators.js'), 58 + ] 59 + 54 60 getSupportedBrowsers(): readonly string[] { 55 61 return webdriverBrowsers 56 62 } ··· 69 75 this.project = project 70 76 this.browserName = project.config.browser.name as WebdriverBrowser 71 77 this.options = options 78 + 79 + for (const [name, command] of Object.entries(commands)) { 80 + project.browser!.registerCommand(name as any, command as BrowserCommand) 81 + } 72 82 } 73 83 74 84 isIframeSwitched(): boolean { ··· 271 281 } 272 282 } 273 283 274 - declare module 'vitest/node' { 275 - export interface UserEventClickOptions extends ClickOptions {} 284 + declare module 'vitest/browser' { 285 + export interface UserEventClickOptions extends Partial<ClickOptions> {} 286 + export interface UserEventHoverOptions extends MoveToOptions {} 276 287 277 - export interface UserEventDragOptions extends DragAndDropOptions { 288 + export interface UserEventDragAndDropOptions extends DragAndDropOptions { 278 289 sourceX?: number 279 290 sourceY?: number 280 291 targetX?: number 281 292 targetY?: number 282 293 } 294 + } 283 295 296 + declare module 'vitest/node' { 284 297 export interface BrowserCommandContext { 285 298 browser: WebdriverIO.Browser 299 + } 300 + 301 + export interface _BrowserNames { 302 + webdriverio: WebdriverBrowser 286 303 } 287 304 288 305 export interface ToMatchScreenshotOptions
+15 -9
packages/browser/src/node/rpc.ts
··· 5 5 import type { WebSocket } from 'ws' 6 6 import type { WebSocketBrowserEvents, WebSocketBrowserHandlers } from '../types' 7 7 import type { ParentBrowserProject } from './projectParent' 8 - import type { WebdriverBrowserProvider } from './providers/webdriverio' 9 8 import type { BrowserServerState } from './state' 10 9 import { existsSync, promises as fs } from 'node:fs' 11 10 import { AutomockedModule, AutospiedModule, ManualMockedModule, RedirectedModule } from '@vitest/mocker' ··· 220 219 return vitest.state.getCountOfFailedTests() 221 220 }, 222 221 async wdioSwitchContext(direction) { 223 - const provider = project.browser!.provider as WebdriverBrowserProvider 222 + const provider = project.browser!.provider 224 223 if (!provider) { 225 224 throw new Error('Commands are only available for browser tests.') 226 225 } ··· 228 227 throw new Error('Switch context is only available for WebDriverIO provider.') 229 228 } 230 229 if (direction === 'iframe') { 231 - await provider.switchToTestFrame() 230 + await (provider as any).switchToTestFrame() 232 231 } 233 232 else { 234 - await provider.switchToMainFrame() 233 + await (provider as any).switchToMainFrame() 235 234 } 236 235 }, 237 236 async triggerCommand(sessionId, command, testPath, payload) { ··· 240 239 if (!provider) { 241 240 throw new Error('Commands are only available for browser tests.') 242 241 } 243 - const commands = globalServer.commands 244 - if (!commands || !commands[command]) { 245 - throw new Error(`Unknown command "${command}".`) 246 - } 247 242 const context = Object.assign( 248 243 { 249 244 testPath, ··· 251 246 provider, 252 247 contextId: sessionId, 253 248 sessionId, 249 + triggerCommand: (name: string, ...args: any[]) => { 250 + return project.browser!.triggerCommand( 251 + name as any, 252 + context, 253 + ...args, 254 + ) 255 + }, 254 256 }, 255 257 provider.getCommandsContext(sessionId), 256 258 ) as any as BrowserCommandContext 257 - return await commands[command](context, ...payload) 259 + return await project.browser!.triggerCommand( 260 + command as any, 261 + context, 262 + ...payload, 263 + ) 258 264 }, 259 265 resolveMock(rawId, importer, options) { 260 266 return mockResolver.resolveMock(rawId, importer, options)
+66 -17
packages/browser/src/node/utils.ts
··· 1 - import type { BrowserProvider, BrowserProviderOption, ResolvedBrowserOptions, TestProject } from 'vitest/node' 1 + import type { 2 + BrowserProvider, 3 + ResolvedBrowserOptions, 4 + ResolvedConfig, 5 + TestProject, 6 + } from 'vitest/node' 7 + 8 + import { defaultKeyMap } from '@testing-library/user-event/dist/esm/keyboard/keyMap.js' 9 + import { parseKeyDef as tlParse } from '@testing-library/user-event/dist/esm/keyboard/parseKeyDef.js' 10 + import { basename, dirname, relative, resolve } from 'pathe' 11 + 12 + declare enum DOM_KEY_LOCATION { 13 + STANDARD = 0, 14 + LEFT = 1, 15 + RIGHT = 2, 16 + NUMPAD = 3, 17 + } 18 + 19 + interface keyboardKey { 20 + /** Physical location on a keyboard */ 21 + code?: string 22 + /** Character or functional key descriptor */ 23 + key?: string 24 + /** Location on the keyboard for keys with multiple representation */ 25 + location?: DOM_KEY_LOCATION 26 + /** Does the character in `key` require/imply AltRight to be pressed? */ 27 + altGr?: boolean 28 + /** Does the character in `key` require/imply a shiftKey to be pressed? */ 29 + shift?: boolean 30 + } 31 + 32 + export function parseKeyDef(text: string): { 33 + keyDef: keyboardKey 34 + releasePrevious: boolean 35 + releaseSelf: boolean 36 + repeat: number 37 + }[] { 38 + return tlParse(defaultKeyMap, text) 39 + } 2 40 3 41 export function replacer(code: string, values: Record<string, string>): string { 4 42 return code.replace(/\{\s*(\w+)\s*\}/g, (_, key) => values[key] ?? _) 5 43 } 6 44 45 + export function resolveScreenshotPath( 46 + testPath: string, 47 + name: string, 48 + config: ResolvedConfig, 49 + customPath: string | undefined, 50 + ): string { 51 + if (customPath) { 52 + return resolve(dirname(testPath), customPath) 53 + } 54 + const dir = dirname(testPath) 55 + const base = basename(testPath) 56 + if (config.browser.screenshotDirectory) { 57 + return resolve( 58 + config.browser.screenshotDirectory, 59 + relative(config.root, dir), 60 + base, 61 + name, 62 + ) 63 + } 64 + return resolve(dir, '__screenshots__', base, name) 65 + } 66 + 7 67 export async function getBrowserProvider( 8 68 options: ResolvedBrowserOptions, 9 69 project: TestProject, ··· 15 75 `${name}Browser name is required. Please, set \`test.browser.instances[].browser\` option manually.`, 16 76 ) 17 77 } 18 - if ( 19 - // nothing is provided by default 20 - options.provider == null 21 - // the provider is provided via `--browser.provider=playwright` 22 - // or the config was serialized, but we can infer the factory by the name 23 - || ('_cli' in options.provider && typeof options.provider.factory !== 'function') 24 - ) { 25 - const providers = await import('./providers/index') 26 - const name = (options.provider?.name || 'preview') as 'preview' | 'webdriverio' | 'playwright' 27 - if (!(name in providers)) { 28 - throw new Error(`Unknown browser provider "${name}". Available providers: ${Object.keys(providers).join(', ')}.`) 29 - } 30 - return (providers[name] as (options?: object) => BrowserProviderOption)(options.provider?.options).factory(project) 78 + if (options.provider == null) { 79 + throw new Error(`Browser Mode requires the "provider" to always be specified.`) 31 80 } 32 81 const supportedBrowsers = options.provider.supportedBrowser || [] 33 82 if (supportedBrowsers.length && !supportedBrowsers.includes(browser)) { ··· 37 86 }". Supported browsers: ${supportedBrowsers.join(', ')}.`, 38 87 ) 39 88 } 40 - if (typeof options.provider.factory !== 'function') { 41 - throw new TypeError(`The "${name}" browser provider does not provide a "factory" function. Received ${typeof options.provider.factory}.`) 89 + if (typeof options.provider.providerFactory !== 'function') { 90 + throw new TypeError(`The "${name}" browser provider does not provide a "providerFactory" function. Received ${typeof options.provider.providerFactory}.`) 42 91 } 43 - return options.provider.factory(project) 92 + return options.provider.providerFactory(project) 44 93 } 45 94 46 95 export function slash(path: string): string {
+5 -5
packages/browser/utils.d.ts
··· 1 - // should be in sync with tester/public-utils.ts 2 - // we cannot bundle it because vitest depend on the @vitest/browser and vice versa 3 - // fortunately, the file is quite small 4 - 5 - import { LocatorSelectors, Locator } from '@vitest/browser/context' 1 + import { LocatorSelectors, Locator } from './context' 6 2 import { StringifyOptions } from 'vitest/internal/browser' 7 3 8 4 export type PrettyDOMOptions = Omit<StringifyOptions, 'maxLength'> 9 5 6 + /** @deprecated use `import('vitest/browser').utils.getElementLocatorSelectors` instead */ 10 7 export declare function getElementLocatorSelectors(element: Element): LocatorSelectors 8 + /** @deprecated use `import('vitest/browser').utils.debug` instead */ 11 9 export declare function debug( 12 10 el?: Element | Locator | null | (Element | Locator)[], 13 11 maxLength?: number, 14 12 options?: PrettyDOMOptions, 15 13 ): void 14 + /** @deprecated use `import('vitest/browser').utils.prettyDOM` instead */ 16 15 export declare function prettyDOM( 17 16 dom?: Element | Locator | undefined | null, 18 17 maxLength?: number, 19 18 prettyFormatOptions?: PrettyDOMOptions, 20 19 ): string 20 + /** @deprecated use `import('vitest/browser').utils.getElementError` instead */ 21 21 export declare function getElementError(selector: string, container?: Element): Error
+1 -1
packages/coverage-v8/src/browser.ts
··· 1 1 import type { CoverageProviderModule } from 'vitest/node' 2 2 import type { V8CoverageProvider } from './provider' 3 - import { cdp } from '@vitest/browser/context' 3 + import { cdp } from 'vitest/browser' 4 4 import { loadProvider } from './load-provider' 5 5 6 6 const session = cdp()
-1
packages/coverage-v8/tsconfig.json
··· 2 2 "extends": "../../tsconfig.base.json", 3 3 "compilerOptions": { 4 4 "moduleResolution": "Bundler", 5 - "types": ["@vitest/browser/providers/playwright"], 6 5 "isolatedDeclarations": true 7 6 }, 8 7 "include": ["./src/**/*.ts"],
+1
packages/ui/package.json
··· 66 66 "@types/ws": "catalog:", 67 67 "@unocss/reset": "catalog:", 68 68 "@vitejs/plugin-vue": "catalog:", 69 + "@vitest/browser-playwright": "workspace:*", 69 70 "@vitest/runner": "workspace:*", 70 71 "@vitest/ws-client": "workspace:*", 71 72 "@vue/test-utils": "^2.4.6",
+1 -1
packages/ui/vitest.config.ts
··· 1 - import { playwright } from '@vitest/browser/providers/playwright' 1 + import { playwright } from '@vitest/browser-playwright' 2 2 import { mergeConfig } from 'vite' 3 3 import { defineConfig } from 'vitest/config' 4 4 import viteConfig from './vite.config'
+3
packages/utils/src/source-map.ts
··· 32 32 '/deps/@vitest', 33 33 '/deps/loupe', 34 34 '/deps/chai', 35 + '/browser-playwright/dist/locators.js', 36 + '/browser-webdriverio/dist/locators.js', 37 + '/browser-preview/dist/locators.js', 35 38 /node:\w+/, 36 39 /__vitest_test__/, 37 40 /__vitest_browser__/,
+6
packages/vitest/browser/context.d.ts
··· 1 + // @ts-ignore -- @vitest/browser-playwright might not be installed 2 + export * from '@vitest/browser-playwright/context' 3 + // @ts-ignore -- @vitest/browser-webdriverio might not be installed 4 + export * from '@vitest/browser-webdriverio/context' 5 + // @ts-ignore -- @vitest/browser-preview might not be installed 6 + export * from '@vitest/browser-preview/context'
+20
packages/vitest/browser/context.js
··· 1 + // Vitest resolves "vitest/browser" as a virtual module instead 2 + 3 + // fake exports for static analysis 4 + export const page = null 5 + export const server = null 6 + export const userEvent = null 7 + export const cdp = null 8 + export const commands = null 9 + export const locators = null 10 + export const utils = null 11 + 12 + const pool = globalThis.__vitest_worker__?.ctx?.pool 13 + 14 + throw new Error( 15 + // eslint-disable-next-line prefer-template 16 + 'vitest/browser can be imported only inside the Browser Mode. ' 17 + + (pool 18 + ? `Your test is running in ${pool} pool. Make sure your regular tests are excluded from the "test.include" glob pattern.` 19 + : 'Instead, it was imported outside of Vitest.'), 20 + )
+16 -2
packages/vitest/package.json
··· 39 39 "default": "./index.cjs" 40 40 } 41 41 }, 42 + "./browser": { 43 + "types": "./browser/context.d.ts", 44 + "default": "./browser/context.js" 45 + }, 42 46 "./package.json": "./package.json", 43 47 "./optional-types.js": { 44 48 "types": "./optional-types.d.ts" ··· 114 118 "*.d.ts", 115 119 "*.mjs", 116 120 "bin", 121 + "browser", 117 122 "dist" 118 123 ], 119 124 "engines": { ··· 127 132 "@edge-runtime/vm": "*", 128 133 "@types/debug": "^4.1.12", 129 134 "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", 130 - "@vitest/browser": "workspace:*", 135 + "@vitest/browser-playwright": "workspace:*", 136 + "@vitest/browser-preview": "workspace:*", 137 + "@vitest/browser-webdriverio": "workspace:*", 131 138 "@vitest/ui": "workspace:*", 132 139 "happy-dom": "*", 133 140 "jsdom": "*" ··· 142 149 "@types/node": { 143 150 "optional": true 144 151 }, 145 - "@vitest/browser": { 152 + "@vitest/browser-playwright": { 153 + "optional": true 154 + }, 155 + "@vitest/browser-preview": { 156 + "optional": true 157 + }, 158 + "@vitest/browser-webdriverio": { 146 159 "optional": true 147 160 }, 148 161 "@vitest/ui": { ··· 192 205 "@types/picomatch": "^4.0.2", 193 206 "@types/prompts": "^2.4.9", 194 207 "@types/sinonjs__fake-timers": "^8.1.5", 208 + "@vitest/browser-playwright": "workspace:*", 195 209 "acorn-walk": "catalog:", 196 210 "birpc": "catalog:", 197 211 "cac": "catalog:",
+1
packages/vitest/rollup.config.js
··· 70 70 'node:console', 71 71 'inspector', 72 72 'vitest/optional-types.js', 73 + 'vitest/browser', 73 74 'vite/module-runner', 74 75 '@vitest/mocker', 75 76 '@vitest/mocker/node',
+2 -41
packages/vitest/src/create/browser/creator.ts
··· 41 41 } 42 42 } 43 43 44 - function getProviderPackageNames(provider: BrowserBuiltinProvider) { 45 - switch (provider) { 46 - case 'webdriverio': 47 - return { 48 - types: '@vitest/browser/providers/webdriverio', 49 - pkg: 'webdriverio', 50 - } 51 - case 'playwright': 52 - return { 53 - types: '@vitest/browser/providers/playwright', 54 - pkg: 'playwright', 55 - } 56 - case 'preview': 57 - return { 58 - types: '@vitest/browser/matchers', 59 - pkg: null, 60 - } 61 - } 62 - throw new Error(`Unsupported provider: ${provider}`) 63 - } 64 - 65 44 function getFramework(): prompt.Choice[] { 66 45 return [ 67 46 { ··· 156 135 return null 157 136 } 158 137 159 - async function updateTsConfig(type: string | undefined | null) { 160 - if (type == null) { 161 - return 162 - } 163 - const msg = `Add "${c.bold(type)}" to your tsconfig.json "${c.bold('compilerOptions.types')}" field to have better intellisense support.` 164 - log() 165 - log(c.yellow('◼'), c.yellow(msg)) 166 - } 167 - 168 138 function getLanguageOptions(): prompt.Choice[] { 169 139 return [ 170 140 { ··· 295 265 296 266 const configContent = [ 297 267 `import { defineConfig } from 'vitest/config'`, 298 - `import { ${options.provider} } from '@vitest/browser/providers/${options.provider}'`, 268 + `import { ${options.provider} } from '@vitest/browser-${options.provider}'`, 299 269 options.frameworkPlugin ? frameworkImport : null, 300 270 ``, 301 271 'export default defineConfig({', ··· 433 403 } 434 404 435 405 const dependenciesToInstall = [ 436 - '@vitest/browser', 406 + `@vitest/browser-${provider}`, 437 407 ] 438 408 439 409 const frameworkPackage = getFrameworkTestPackage(framework) ··· 441 411 dependenciesToInstall.push(frameworkPackage) 442 412 } 443 413 444 - const providerPkg = getProviderPackageNames(provider) 445 - if (providerPkg.pkg) { 446 - dependenciesToInstall.push(providerPkg.pkg) 447 - } 448 414 const frameworkPlugin = getFrameworkPluginPackage(framework) 449 415 if (frameworkPlugin) { 450 416 dependenciesToInstall.push(frameworkPlugin) ··· 512 478 stdio: ['pipe', 'inherit', 'inherit'], 513 479 }, 514 480 }) 515 - } 516 - 517 - // TODO: can we do this ourselves? 518 - if (lang === 'ts') { 519 - await updateTsConfig(providerPkg?.types) 520 481 } 521 482 522 483 log()
+24 -15
packages/vitest/src/node/config/resolveConfig.ts
··· 253 253 const browser = resolved.browser 254 254 255 255 if (browser.enabled) { 256 - if (!browser.name && !browser.instances) { 257 - throw new Error(`Vitest Browser Mode requires "browser.name" (deprecated) or "browser.instances" options, none were set.`) 256 + const instances = browser.instances 257 + if (!browser.instances) { 258 + browser.instances = [] 259 + } 260 + 261 + // use `chromium` by default when the preview provider is specified 262 + // for a smoother experience. if chromium is not available, it will 263 + // open the default browser anyway 264 + if (!browser.instances.length && browser.provider?.name === 'preview') { 265 + browser.instances = [{ browser: 'chromium' }] 258 266 } 259 267 260 - const instances = browser.instances 261 - if (browser.name && browser.instances) { 268 + if (browser.name && instances?.length) { 262 269 // --browser=chromium filters configs to a single one 263 270 browser.instances = browser.instances.filter(instance => instance.browser === browser.name) 264 - } 265 271 266 - if (browser.instances && !browser.instances.length) { 267 - throw new Error([ 268 - `"browser.instances" was set in the config, but the array is empty. Define at least one browser config.`, 269 - browser.name && instances?.length ? ` The "browser.name" was set to "${browser.name}" which filtered all configs (${instances.map(c => c.browser).join(', ')}). Did you mean to use another name?` : '', 270 - ].join('')) 272 + // if `instances` were defined, but now they are empty, 273 + // let's throw an error because the filter is invalid 274 + if (!browser.instances.length) { 275 + throw new Error([ 276 + `"browser.instances" was set in the config, but the array is empty. Define at least one browser config.`, 277 + ` The "browser.name" was set to "${browser.name}" which filtered all configs (${instances.map(c => c.browser).join(', ')}). Did you mean to use another name?`, 278 + ].join('')) 279 + } 271 280 } 272 281 } 273 282 ··· 750 759 resolved.browser.locators ??= {} as any 751 760 resolved.browser.locators.testIdAttribute ??= 'data-testid' 752 761 753 - if (resolved.browser.enabled && stdProvider === 'stackblitz') { 754 - resolved.browser.provider = undefined // reset to "preview" 755 - } 756 - 757 762 if (typeof resolved.browser.provider === 'string') { 758 - const source = `@vitest/browser/providers/${resolved.browser.provider}` 763 + const source = `@vitest/browser-${resolved.browser.provider}` 759 764 throw new TypeError( 760 765 'The `browser.provider` configuration was changed to accept a factory instead of a string. ' 761 766 + `Add an import of "${resolved.browser.provider}" from "${source}" instead. See: https://vitest.dev/guide/browser/config#provider`, ··· 763 768 } 764 769 765 770 const isPreview = resolved.browser.provider?.name === 'preview' 771 + 772 + if (!isPreview && resolved.browser.enabled && stdProvider === 'stackblitz') { 773 + throw new Error(`stackblitz environment does not support the ${resolved.browser.provider?.name} provider. Please, use "@vitest/browser-preview" instead.`) 774 + } 766 775 if (isPreview && resolved.browser.screenshotFailures === true) { 767 776 console.warn(c.yellow( 768 777 [
+5 -1
packages/vitest/src/node/core.ts
··· 273 273 throw new Error(`No projects matched the filter "${filter}".`) 274 274 } 275 275 else { 276 - throw new Error(`Vitest wasn't able to resolve any project.`) 276 + let error = `Vitest wasn't able to resolve any project.` 277 + if (this.config.browser.enabled && !this.config.browser.instances?.length) { 278 + error += ` Please, check that you specified the "browser.instances" option.` 279 + } 280 + throw new Error(error) 277 281 } 278 282 } 279 283
+1 -1
packages/vitest/src/node/logger.ts
··· 247 247 const output = project.isRootProject() 248 248 ? '' 249 249 : formatProjectName(project) 250 - const provider = project.browser.provider.name 250 + const provider = project.browser.provider?.name 251 251 const providerString = provider === 'preview' ? '' : ` by ${c.reset(c.bold(provider))}` 252 252 this.log( 253 253 c.dim(
+2 -12
packages/vitest/src/node/pool.ts
··· 8 8 import { version as viteVersion } from 'vite' 9 9 import { rootDir } from '../paths' 10 10 import { isWindows } from '../utils/env' 11 + import { createBrowserPool } from './pools/browser' 11 12 import { createForksPool } from './pools/forks' 12 13 import { createThreadsPool } from './pools/threads' 13 14 import { createTypecheckPool } from './pools/typecheck' ··· 175 176 return getConcurrentPool(pool, () => resolveCustomPool(pool)) 176 177 } 177 178 178 - function getBrowserPool() { 179 - return getConcurrentPool('browser', async () => { 180 - const { createBrowserPool } = await import('@vitest/browser') 181 - return createBrowserPool(ctx) 182 - }) 183 - } 184 - 185 179 const groupedSpecifications: Record<string, TestSpecification[]> = {} 186 180 const groups = new Set<number>() 187 181 ··· 191 185 threads: specs => createThreadsPool(ctx, options, specs), 192 186 forks: specs => createForksPool(ctx, options, specs), 193 187 typescript: () => createTypecheckPool(ctx), 188 + browser: () => createBrowserPool(ctx), 194 189 } 195 190 196 191 for (const spec of files) { ··· 253 248 const factory = factories[pool] 254 249 pools[pool] ??= factory(specs) 255 250 return pools[pool]![method](specs, invalidate) 256 - } 257 - 258 - if (pool === 'browser') { 259 - pools.browser ??= await getBrowserPool() 260 - return pools.browser[method](specs, invalidate) 261 251 } 262 252 263 253 const poolHandler = await getCustomPool(pool)
+15 -31
packages/vitest/src/node/project.ts
··· 449 449 /** @internal */ 450 450 public _parent?: TestProject 451 451 /** @internal */ 452 - _initParentBrowser = deduped(async () => { 452 + _initParentBrowser = deduped(async (childProject: TestProject) => { 453 453 if (!this.isBrowserEnabled() || this._parentBrowser) { 454 454 return 455 455 } 456 - await this.vitest.packageInstaller.ensureInstalled( 457 - '@vitest/browser', 458 - this.config.root, 459 - this.vitest.version, 460 - ) 461 - const { createBrowserServer, distRoot } = await import('@vitest/browser') 462 - let cacheDir: string 463 - const browser = await createBrowserServer( 464 - this, 465 - this.vite.config.configFile, 466 - [ 467 - { 468 - name: 'vitest:browser-cacheDir', 469 - configResolved(config) { 470 - cacheDir = config.cacheDir 471 - }, 472 - }, 473 - ...MocksPlugins({ 474 - filter(id) { 475 - if (id.includes(distRoot) || id.includes(cacheDir)) { 476 - return false 477 - } 478 - return true 479 - }, 480 - }), 481 - MetaEnvReplacerPlugin(), 482 - ], 483 - [CoverageTransform(this.vitest)], 484 - ) 456 + const provider = this.config.browser.provider || childProject.config.browser.provider 457 + if (provider == null) { 458 + throw new Error(`Proider was not specified in the "browser.provider" setting. Please, pass down playwright(), webdriverio() or preview() from "@vitest/browser-playwright", "@vitest/browser-webdriverio" or "@vitest/browser-preview" package respectively.`) 459 + } 460 + if (typeof provider.serverFactory !== 'function') { 461 + throw new TypeError(`The browser provider options do not return a "serverFactory" function. Are you using the latest "@vitest/browser-${provider.name}" package?`) 462 + } 463 + const browser = await provider.serverFactory({ 464 + project: this, 465 + mocksPlugins: options => MocksPlugins(options), 466 + metaEnvReplacer: () => MetaEnvReplacerPlugin(), 467 + coveragePlugin: () => CoverageTransform(this.vitest), 468 + }) 485 469 this._parentBrowser = browser 486 470 if (this.config.browser.ui) { 487 471 setup(this.vitest, browser.vite) ··· 490 474 491 475 /** @internal */ 492 476 _initBrowserServer = deduped(async () => { 493 - await this._parent?._initParentBrowser() 477 + await this._parent?._initParentBrowser(this) 494 478 495 479 if (!this.browser && this._parent?._parentBrowser) { 496 480 this.browser = this._parent._parentBrowser.spawn(this)
+7 -51
packages/vitest/src/node/projects/resolveProjects.ts
··· 15 15 import { glob, isDynamicPattern } from 'tinyglobby' 16 16 import { mergeConfig } from 'vite' 17 17 import { configFiles as defaultConfigFiles } from '../../constants' 18 - import { isTTY } from '../../utils/env' 19 18 import { VitestFilteredOutProjectError } from '../errors' 20 19 import { initializeProject, TestProject } from '../project' 21 - import { withLabel } from '../reporters/renderers/utils' 22 20 23 21 // vitest.config.* 24 22 // vite.config.* ··· 190 188 return 191 189 } 192 190 const instances = project.config.browser.instances || [] 193 - const browser = project.config.browser.name 194 - if (instances.length === 0 && browser) { 195 - instances.push({ 196 - browser, 197 - name: project.name ? `${project.name} (${browser})` : browser, 198 - }) 199 - vitest.logger.warn( 200 - withLabel( 201 - 'yellow', 202 - 'Vitest', 203 - [ 204 - `No browser "instances" were defined`, 205 - project.name ? ` for the "${project.name}" project. ` : '. ', 206 - `Running tests in "${project.config.browser.name}" browser. `, 207 - 'The "browser.name" field is deprecated since Vitest 3. ', 208 - 'Read more: https://vitest.dev/guide/browser/config#browser-instances', 209 - ].filter(Boolean).join(''), 210 - ), 211 - ) 191 + if (instances.length === 0) { 192 + removeProjects.add(project) 193 + return 212 194 } 213 195 const originalName = project.config.name 214 196 // if original name is in the --project=name filter, keep all instances ··· 237 219 if (name == null) { 238 220 throw new Error(`The browser configuration must have a "name" property. This is a bug in Vitest. Please, open a new issue with reproduction`) 239 221 } 222 + if (config.provider?.name != null && project.config.browser.provider?.name != null && config.provider?.name !== project.config.browser.provider?.name) { 223 + throw new Error(`The instance cannot have a different provider from its parent. The "${name}" instance specifies "${config.provider?.name}" provider, but its parent has a "${project.config.browser.provider?.name}" provider.`) 224 + } 240 225 241 226 if (names.has(name)) { 242 227 throw new Error( ··· 257 242 removeProjects.add(project) 258 243 }) 259 244 260 - resolvedProjects = resolvedProjects.filter(project => !removeProjects.has(project)) 261 - 262 - const headedBrowserProjects = resolvedProjects.filter((project) => { 263 - return project.config.browser.enabled && !project.config.browser.headless 264 - }) 265 - if (headedBrowserProjects.length > 1) { 266 - const message = [ 267 - `Found multiple projects that run browser tests in headed mode: "${headedBrowserProjects.map(p => p.name).join('", "')}".`, 268 - ` Vitest cannot run multiple headed browsers at the same time.`, 269 - ].join('') 270 - if (!isTTY) { 271 - throw new Error(`${message} Please, filter projects with --browser=name or --project=name flag or run tests with "headless: true" option.`) 272 - } 273 - const prompts = await import('prompts') 274 - const { projectName } = await prompts.default({ 275 - type: 'select', 276 - name: 'projectName', 277 - choices: headedBrowserProjects.map(project => ({ 278 - title: project.name, 279 - value: project.name, 280 - })), 281 - message: `${message} Select a single project to run or cancel and run tests with "headless: true" option. Note that you can also start tests with --browser=name or --project=name flag.`, 282 - }) 283 - if (!projectName) { 284 - throw new Error('The test run was aborted.') 285 - } 286 - return resolvedProjects.filter(project => project.name === projectName) 287 - } 288 - 289 - return resolvedProjects 245 + return resolvedProjects.filter(project => !removeProjects.has(project)) 290 246 } 291 247 292 248 function cloneConfig(project: TestProject, { browser, ...config }: BrowserInstanceOption) {
+53 -8
packages/vitest/src/node/types/browser.ts
··· 2 2 import type { CancelReason } from '@vitest/runner' 3 3 import type { Awaitable, ParsedStack, TestError } from '@vitest/utils' 4 4 import type { StackTraceParserOptions } from '@vitest/utils/source-map' 5 - import type { ViteDevServer } from 'vite' 5 + import type { Plugin, ViteDevServer } from 'vite' 6 + import type { BrowserCommands } from 'vitest/browser' 6 7 import type { BrowserTraceViewMode } from '../../runtime/config' 7 8 import type { BrowserTesterOptions } from '../../types/browser' 8 9 import type { TestProject } from '../project' ··· 25 26 name: string 26 27 supportedBrowser?: ReadonlyArray<string> 27 28 options: Options 28 - factory: (project: TestProject) => BrowserProvider 29 + providerFactory: (project: TestProject) => BrowserProvider 30 + serverFactory: BrowserServerFactory 31 + } 32 + 33 + export interface BrowserServerOptions { 34 + project: TestProject 35 + coveragePlugin: () => Plugin 36 + mocksPlugins: (options: { filter: (id: string) => boolean }) => Plugin[] 37 + metaEnvReplacer: () => Plugin 38 + } 39 + 40 + export interface BrowserServerFactory { 41 + (otpions: BrowserServerOptions): Promise<ParentProjectBrowser> 29 42 } 30 43 31 44 export interface BrowserProvider { 32 45 name: string 33 46 mocker?: BrowserModuleMocker 47 + readonly initScripts?: string[] 34 48 /** 35 49 * @experimental opt-in into file parallelisation 36 50 */ ··· 42 56 } 43 57 44 58 export type BrowserBuiltinProvider = 'webdriverio' | 'playwright' | 'preview' 59 + export interface _BrowserNames {} 45 60 46 61 type UnsupportedProperties 47 62 = | 'browser' ··· 72 87 | 'testerHtmlPath' 73 88 | 'screenshotDirectory' 74 89 | 'screenshotFailures' 75 - | 'provider' 76 90 > { 77 91 /** 78 92 * Name of the browser 79 93 */ 80 - browser: string 94 + browser: keyof _BrowserNames extends never 95 + ? string 96 + : _BrowserNames[keyof _BrowserNames] 81 97 82 98 name?: string 99 + provider?: BrowserProviderOption 83 100 } 84 101 85 102 export interface BrowserConfigOptions { ··· 100 117 /** 101 118 * Configurations for different browser setups 102 119 */ 103 - instances: BrowserInstanceOption[] 120 + instances?: BrowserInstanceOption[] 104 121 105 122 /** 106 123 * Browser provider 124 + * @example 125 + * ```ts 126 + * import { playwright } from '@vitest/browser-playwright' 127 + * export default defineConfig({ 128 + * test: { 129 + * browser: { 130 + * provider: playwright(), 131 + * }, 132 + * }, 133 + * }) 134 + * ``` 107 135 */ 108 136 provider?: BrowserProviderOption 109 137 ··· 222 250 223 251 /** 224 252 * Commands that will be executed on the server 225 - * via the browser `import("@vitest/browser/context").commands` API. 253 + * via the browser `import("vitest/browser").commands` API. 226 254 * @see {@link https://vitest.dev/guide/browser/commands} 227 255 */ 228 256 commands?: Record<string, BrowserCommand<any>> ··· 264 292 provider: BrowserProvider 265 293 project: TestProject 266 294 sessionId: string 295 + triggerCommand: <K extends keyof BrowserCommands>( 296 + name: K, 297 + ...args: Parameters<BrowserCommands[K]> 298 + ) => ReturnType<BrowserCommands[K]> 267 299 } 268 300 269 301 export interface BrowserServerStateSession { ··· 285 317 286 318 export interface ParentProjectBrowser { 287 319 spawn: (project: TestProject) => ProjectBrowser 320 + vite: ViteDevServer 288 321 } 289 322 290 323 export interface ProjectBrowser { ··· 295 328 initBrowserProvider: (project: TestProject) => Promise<void> 296 329 parseStacktrace: (stack: string) => ParsedStack[] 297 330 parseErrorStacktrace: (error: TestError, options?: StackTraceParserOptions) => ParsedStack[] 331 + registerCommand: <K extends keyof BrowserCommands>( 332 + name: K, 333 + cb: BrowserCommand< 334 + Parameters<BrowserCommands[K]>, 335 + ReturnType<BrowserCommands[K]> 336 + > 337 + ) => void 338 + triggerCommand: <K extends keyof BrowserCommands>( 339 + name: K, 340 + context: BrowserCommandContext, 341 + ...args: Parameters<BrowserCommands[K]> 342 + ) => ReturnType<BrowserCommands[K]> 298 343 } 299 344 300 - export interface BrowserCommand<Payload extends unknown[] = []> { 301 - (context: BrowserCommandContext, ...payload: Payload): Awaitable<any> 345 + export interface BrowserCommand<Payload extends unknown[] = [], ReturnValue = any> { 346 + (context: BrowserCommandContext, ...payload: Payload): Awaitable<ReturnValue> 302 347 } 303 348 304 349 export interface BrowserScript {
+10 -1
packages/vitest/src/public/browser.ts
··· 3 3 stopCoverageInsideWorker, 4 4 takeCoverageInsideWorker, 5 5 } from '../integrations/coverage' 6 - 7 6 export { 8 7 loadDiffConfig, 9 8 loadSnapshotSerializers, ··· 24 23 getOriginalPosition, 25 24 } from '@vitest/utils/source-map' 26 25 export { getSafeTimers, setSafeTimers } from '@vitest/utils/timers' 26 + /** 27 + * @internal 28 + */ 29 + export const __INTERNAL: { 30 + _asLocator: (lang: 'javascript', selector: string) => string 31 + _createLocator: (selector: string) => any 32 + _extendedMethods: Set<string> 33 + } = { 34 + _extendedMethods: new Set(), 35 + } as any
+3
packages/vitest/src/public/node.ts
··· 59 59 export type { BenchmarkUserOptions } from '../node/types/benchmark' 60 60 61 61 export type { 62 + _BrowserNames, 62 63 BrowserBuiltinProvider, 63 64 BrowserCommand, 64 65 BrowserCommandContext, ··· 69 70 BrowserProvider, 70 71 BrowserProviderOption, 71 72 BrowserScript, 73 + BrowserServerFactory, 74 + BrowserServerOptions, 72 75 BrowserServerState, 73 76 BrowserServerStateSession, 74 77 CDPSession,
-1
packages/vitest/src/types/browser.ts
··· 4 4 method: TestExecutionMethod 5 5 files: string[] 6 6 providedContext: string 7 - startTime: number 8 7 }
+1 -1
packages/vitest/src/utils/graph.ts
··· 18 18 if (!mod || !mod.id) { 19 19 return 20 20 } 21 - if (mod.id === '\0@vitest/browser/context') { 21 + if (mod.id === '\0vitest/browser') { 22 22 return 23 23 } 24 24 if (seen.has(mod)) {
+99 -21
pnpm-lock.yaml
··· 120 120 121 121 overrides: 122 122 '@vitest/browser': workspace:* 123 + '@vitest/browser-playwright': workspace:* 124 + '@vitest/browser-preview': workspace:* 125 + '@vitest/browser-webdriverio': workspace:* 123 126 '@vitest/ui': workspace:* 124 127 acorn: 8.11.3 125 128 mlly: ^1.8.0 ··· 351 354 specifier: ^3.3.1 352 355 version: 3.3.1 353 356 devDependencies: 354 - '@vitest/browser': 357 + '@vitest/browser-playwright': 355 358 specifier: workspace:* 356 - version: link:../../packages/browser 359 + version: link:../../packages/browser-playwright 357 360 jsdom: 358 361 specifier: latest 359 362 version: 27.0.0(postcss@8.5.6) ··· 438 441 439 442 packages/browser: 440 443 dependencies: 441 - '@testing-library/dom': 442 - specifier: ^10.4.1 443 - version: 10.4.1 444 - '@testing-library/user-event': 445 - specifier: ^14.6.1 446 - version: 14.6.1(@testing-library/dom@10.4.1) 447 444 '@vitest/mocker': 448 445 specifier: workspace:* 449 446 version: link:../mocker ··· 469 466 specifier: 'catalog:' 470 467 version: 8.18.3 471 468 devDependencies: 469 + '@testing-library/user-event': 470 + specifier: ^14.6.1 471 + version: 14.6.1(@testing-library/dom@10.4.1) 472 472 '@types/pngjs': 473 473 specifier: ^6.0.5 474 474 version: 6.0.5 ··· 478 478 '@vitest/runner': 479 479 specifier: workspace:* 480 480 version: link:../runner 481 - '@wdio/types': 482 - specifier: ^9.19.2 483 - version: 9.19.2 484 481 birpc: 485 482 specifier: 'catalog:' 486 483 version: 2.5.0 ··· 496 493 pathe: 497 494 specifier: 'catalog:' 498 495 version: 2.0.3 496 + vitest: 497 + specifier: workspace:* 498 + version: link:../vitest 499 + 500 + packages/browser-playwright: 501 + dependencies: 502 + '@vitest/browser': 503 + specifier: workspace:* 504 + version: link:../browser 505 + '@vitest/mocker': 506 + specifier: workspace:* 507 + version: link:../mocker 508 + tinyrainbow: 509 + specifier: 'catalog:' 510 + version: 3.0.3 511 + devDependencies: 499 512 playwright: 500 513 specifier: ^1.55.0 501 514 version: 1.55.0 ··· 505 518 vitest: 506 519 specifier: workspace:* 507 520 version: link:../vitest 521 + 522 + packages/browser-preview: 523 + dependencies: 524 + '@testing-library/dom': 525 + specifier: ^10.4.1 526 + version: 10.4.1 527 + '@testing-library/user-event': 528 + specifier: ^14.6.1 529 + version: 14.6.1(@testing-library/dom@10.4.1) 530 + '@vitest/browser': 531 + specifier: workspace:* 532 + version: link:../browser 533 + devDependencies: 534 + vitest: 535 + specifier: workspace:* 536 + version: link:../vitest 537 + 538 + packages/browser-webdriverio: 539 + dependencies: 540 + '@vitest/browser': 541 + specifier: workspace:* 542 + version: link:../browser 543 + devDependencies: 544 + '@wdio/types': 545 + specifier: ^9.19.2 546 + version: 9.19.2 547 + vitest: 548 + specifier: workspace:* 549 + version: link:../vitest 508 550 webdriverio: 509 551 specifier: ^9.19.2 510 552 version: 9.19.2 ··· 784 826 '@vitejs/plugin-vue': 785 827 specifier: 'catalog:' 786 828 version: 6.0.1(vite@7.1.5(@types/node@22.18.6)(jiti@2.5.1)(lightningcss@1.30.1)(sass-embedded@1.93.0)(sass@1.93.0)(terser@5.44.0)(tsx@4.20.5)(yaml@2.8.1))(vue@3.5.21(typescript@5.9.2)) 829 + '@vitest/browser-playwright': 830 + specifier: workspace:* 831 + version: link:../browser-playwright 787 832 '@vitest/runner': 788 833 specifier: workspace:* 789 834 version: link:../runner ··· 903 948 904 949 packages/vitest: 905 950 dependencies: 906 - '@vitest/browser': 951 + '@vitest/browser-preview': 952 + specifier: workspace:* 953 + version: link:../browser-preview 954 + '@vitest/browser-webdriverio': 907 955 specifier: workspace:* 908 - version: link:../browser 956 + version: link:../browser-webdriverio 909 957 '@vitest/expect': 910 958 specifier: workspace:* 911 959 version: link:../expect ··· 1012 1060 '@types/sinonjs__fake-timers': 1013 1061 specifier: ^8.1.5 1014 1062 version: 8.1.5(patch_hash=0218b33f433e26861380c2b90c757bde6fea871cb988083c0bd4a9a1f6c00252) 1063 + '@vitest/browser-playwright': 1064 + specifier: workspace:* 1065 + version: link:../browser-playwright 1015 1066 acorn-walk: 1016 1067 specifier: 'catalog:' 1017 1068 version: 8.3.4 ··· 1104 1155 '@vitest/browser': 1105 1156 specifier: workspace:* 1106 1157 version: link:../../packages/browser 1158 + '@vitest/browser-playwright': 1159 + specifier: workspace:* 1160 + version: link:../../packages/browser-playwright 1161 + '@vitest/browser-preview': 1162 + specifier: workspace:* 1163 + version: link:../../packages/browser-preview 1164 + '@vitest/browser-webdriverio': 1165 + specifier: workspace:* 1166 + version: link:../../packages/browser-webdriverio 1107 1167 '@vitest/bundled-lib': 1108 1168 specifier: link:./bundled-lib 1109 1169 version: link:bundled-lib ··· 1149 1209 '@vitejs/plugin-basic-ssl': 1150 1210 specifier: ^2.1.0 1151 1211 version: 2.1.0(vite@7.1.5(@types/node@22.18.6)(jiti@2.5.1)(lightningcss@1.30.1)(sass-embedded@1.93.0)(sass@1.93.0)(terser@5.44.0)(tsx@4.20.5)(yaml@2.8.1)) 1212 + '@vitest/browser-playwright': 1213 + specifier: workspace:* 1214 + version: link:../../packages/browser-playwright 1215 + '@vitest/browser-preview': 1216 + specifier: workspace:* 1217 + version: link:../../packages/browser-preview 1152 1218 '@vitest/runner': 1153 1219 specifier: workspace:^ 1154 1220 version: link:../../packages/runner ··· 1173 1239 1174 1240 test/config: 1175 1241 devDependencies: 1242 + '@vitest/browser-playwright': 1243 + specifier: workspace:* 1244 + version: link:../../packages/browser-playwright 1245 + '@vitest/browser-preview': 1246 + specifier: workspace:* 1247 + version: link:../../packages/browser-preview 1248 + '@vitest/browser-webdriverio': 1249 + specifier: workspace:* 1250 + version: link:../../packages/browser-webdriverio 1176 1251 '@vitest/test-dep-conditions': 1177 1252 specifier: file:./deps/test-dep-conditions 1178 1253 version: file:test/config/deps/test-dep-conditions ··· 1296 1371 '@vitejs/plugin-vue': 1297 1372 specifier: latest 1298 1373 version: 6.0.1(vite@7.1.5(@types/node@22.18.6)(jiti@2.5.1)(lightningcss@1.30.1)(sass-embedded@1.93.0)(sass@1.93.0)(terser@5.44.0)(tsx@4.20.5)(yaml@2.8.1))(vue@3.5.21(typescript@5.9.2)) 1299 - '@vitest/browser': 1374 + '@vitest/browser-playwright': 1300 1375 specifier: workspace:* 1301 - version: link:../../packages/browser 1376 + version: link:../../packages/browser-playwright 1302 1377 '@vitest/coverage-istanbul': 1303 1378 specifier: workspace:* 1304 1379 version: link:../../packages/coverage-istanbul ··· 1356 1431 1357 1432 test/dts-playwright: 1358 1433 devDependencies: 1359 - '@vitest/browser': 1434 + '@vitest/browser-playwright': 1360 1435 specifier: workspace:* 1361 - version: link:../../packages/browser 1436 + version: link:../../packages/browser-playwright 1362 1437 vitest: 1363 1438 specifier: workspace:* 1364 1439 version: link:../../packages/vitest ··· 1481 1556 1482 1557 test/watch: 1483 1558 devDependencies: 1484 - '@vitest/browser': 1559 + '@vitest/browser-playwright': 1560 + specifier: workspace:* 1561 + version: link:../../packages/browser-playwright 1562 + '@vitest/browser-webdriverio': 1485 1563 specifier: workspace:* 1486 - version: link:../../packages/browser 1564 + version: link:../../packages/browser-webdriverio 1487 1565 vite: 1488 1566 specifier: ^7.1.5 1489 1567 version: 7.1.5(@types/node@22.18.6)(jiti@2.5.1)(lightningcss@1.30.1)(sass-embedded@1.93.0)(sass@1.93.0)(terser@5.44.0)(tsx@4.20.5)(yaml@2.8.1) ··· 1511 1589 1512 1590 test/workspaces-browser: 1513 1591 devDependencies: 1514 - '@vitest/browser': 1592 + '@vitest/browser-playwright': 1515 1593 specifier: workspace:* 1516 - version: link:../../packages/browser 1594 + version: link:../../packages/browser-playwright 1517 1595 vitest: 1518 1596 specifier: workspace:* 1519 1597 version: link:../../packages/vitest
+1 -1
test/browser/fixtures/broken-iframe/submit-form.test.ts
··· 1 - import { userEvent } from '@vitest/browser/context'; 1 + import { userEvent } from 'vitest/browser'; 2 2 import { test } from 'vitest'; 3 3 4 4 test('submitting a form reloads the iframe with "?" query', async () => {
+2 -2
test/browser/fixtures/browser-crash/browser-crash.test.ts
··· 1 - import { commands } from '@vitest/browser/context' 1 + import { commands } from 'vitest/browser' 2 2 import { it } from 'vitest' 3 3 4 - declare module '@vitest/browser/context' { 4 + declare module 'vitest/browser' { 5 5 interface BrowserCommands { 6 6 forceCrash: () => Promise<void> 7 7 }
+1 -1
test/browser/fixtures/expect-dom/toHaveLength.test.ts
··· 1 1 import { describe, expect, test } from 'vitest'; 2 2 import { render } from './utils'; 3 - import { page } from '@vitest/browser/context'; 3 + import { page } from 'vitest/browser'; 4 4 5 5 describe('.toHaveLength', () => { 6 6 test('accepts locator', async () => {
+1 -1
test/browser/fixtures/expect-dom/toMatchScreenshot.test.ts
··· 1 1 import { afterEach, describe, expect, test } from 'vitest' 2 2 import { extractToMatchScreenshotPaths, render } from './utils' 3 - import { page, server } from '@vitest/browser/context' 3 + import { page, server } from 'vitest/browser' 4 4 import { join } from 'pathe' 5 5 6 6 const blockSize = 19
+1 -1
test/browser/fixtures/failing/failing.test.ts
··· 1 - import { page } from '@vitest/browser/context' 1 + import { page } from 'vitest/browser' 2 2 import { index } from '@vitest/bundled-lib' 3 3 import { expect, it } from 'vitest' 4 4 import { throwError } from './src/error'
+4 -4
test/browser/fixtures/locators-custom/basic.test.tsx
··· 1 - import { type Locator, locators, page } from '@vitest/browser/context'; 1 + import type { BrowserPage, Locator } from 'vitest/browser'; 2 + import { locators, page, utils } from 'vitest/browser'; 2 3 import { beforeEach, expect, test } from 'vitest'; 3 - import { getElementLocatorSelectors } from '@vitest/browser/utils' 4 4 5 - declare module '@vitest/browser/context' { 5 + declare module 'vitest/browser' { 6 6 interface LocatorSelectors { 7 7 getByCustomTitle: (title: string) => Locator 8 8 getByNestedTitle: (title: string) => Locator ··· 87 87 }) 88 88 89 89 test('locators are available from getElementLocatorSelectors', () => { 90 - const locators = getElementLocatorSelectors(document.body) 90 + const locators = utils.getElementLocatorSelectors(document.body) 91 91 92 92 expect(locators.updateHtml).toBeTypeOf('function') 93 93 expect(locators.getByCustomTitle).toBeTypeOf('function')
+1 -1
test/browser/fixtures/locators/blog.test.tsx
··· 1 1 import { expect, test } from 'vitest' 2 - import { page, userEvent } from '@vitest/browser/context' 2 + import { page, userEvent } from 'vitest/browser' 3 3 import Blog from '../../src/blog-app/blog' 4 4 5 5 test('renders blog posts', async () => {
+1 -1
test/browser/fixtures/locators/query.test.ts
··· 1 - import { page } from '@vitest/browser/context'; 1 + import { page } from 'vitest/browser'; 2 2 import { afterEach, describe, expect, test } from 'vitest'; 3 3 4 4 afterEach(() => {
+1 -1
test/browser/fixtures/multiple-different-configs/basic.test.js
··· 1 1 import { test as baseTest, expect, inject } from 'vitest'; 2 - import { server } from '@vitest/browser/context' 2 + import { server } from 'vitest/browser' 3 3 4 4 const test = baseTest.extend({ 5 5 // chromium should inject the value as "true"
+1 -1
test/browser/fixtures/timeout-hooks/hooks-timeout.test.ts
··· 1 - import { page, server } from '@vitest/browser/context'; 1 + import { page, server } from 'vitest/browser'; 2 2 import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, onTestFailed, onTestFinished } from 'vitest'; 3 3 4 4 describe.runIf(server.provider === 'playwright')('timeouts are failing correctly', () => {
+1 -1
test/browser/fixtures/timeout/timeout.test.ts
··· 1 - import { page } from '@vitest/browser/context'; 1 + import { page } from 'vitest/browser'; 2 2 import { afterEach, expect, test } from 'vitest'; 3 3 4 4 afterEach(() => {
+1 -1
test/browser/fixtures/trace-view/vitest.config.ts
··· 1 1 import { fileURLToPath } from 'node:url' 2 2 import { defineConfig } from 'vitest/config' 3 - import { playwright } from '@vitest/browser/providers/playwright' 3 + import { playwright } from '@vitest/browser-playwright' 4 4 5 5 export default defineConfig({ 6 6 cacheDir: fileURLToPath(new URL("./node_modules/.vite", import.meta.url)),
+1 -1
test/browser/fixtures/user-event/cleanup-retry.test.ts
··· 1 1 import { expect, onTestFinished, test } from 'vitest' 2 - import { userEvent } from '@vitest/browser/context' 2 + import { userEvent } from 'vitest/browser' 3 3 4 4 test('cleanup retry', { retry: 1 }, async (ctx) => { 5 5 let logs: any[] = [];
+1 -1
test/browser/fixtures/user-event/cleanup1.test.ts
··· 1 1 import { expect, onTestFinished, test } from 'vitest' 2 - import { userEvent } from '@vitest/browser/context' 2 + import { userEvent } from 'vitest/browser' 3 3 4 4 test('cleanup1', async () => { 5 5 let logs: any[] = [];
+1 -1
test/browser/fixtures/user-event/cleanup2.test.ts
··· 1 1 import { expect, onTestFinished, test } from 'vitest' 2 - import { userEvent } from '@vitest/browser/context' 2 + import { userEvent } from 'vitest/browser' 3 3 4 4 // test per-test-file cleanup just in case 5 5
+1 -1
test/browser/fixtures/user-event/clipboard.test.ts
··· 1 1 import { expect, test } from 'vitest'; 2 - import { page, userEvent } from '@vitest/browser/context'; 2 + import { page, userEvent } from 'vitest/browser'; 3 3 4 4 test('clipboard', async () => { 5 5 // make it smaller since webdriverio fails when scaled
+1 -1
test/browser/fixtures/user-event/keyboard.test.ts
··· 1 1 import { expect, test } from 'vitest' 2 - import { userEvent, page, server } from '@vitest/browser/context' 2 + import { userEvent, page, server } from 'vitest/browser' 3 3 4 4 test('non US keys', async () => { 5 5 document.body.innerHTML = `
+1 -1
test/browser/fixtures/viewport/basic.test.ts
··· 1 - import { page, userEvent, server } from "@vitest/browser/context"; 1 + import { page, userEvent, server } from "vitest/browser"; 2 2 import { expect, test } from "vitest"; 3 3 4 4 test("drag and drop over large viewport", async () => {
+3
test/browser/package.json
··· 33 33 "@types/react": "^19.1.13", 34 34 "@vitejs/plugin-basic-ssl": "^2.1.0", 35 35 "@vitest/browser": "workspace:*", 36 + "@vitest/browser-playwright": "workspace:*", 37 + "@vitest/browser-preview": "workspace:*", 38 + "@vitest/browser-webdriverio": "workspace:*", 36 39 "@vitest/bundled-lib": "link:./bundled-lib", 37 40 "@vitest/cjs-lib": "link:./cjs-lib", 38 41 "playwright": "^1.55.0",
+3 -3
test/browser/settings.ts
··· 1 1 import type { BrowserInstanceOption } from 'vitest/node' 2 - import { playwright } from '@vitest/browser/providers/playwright' 3 - import { preview } from '@vitest/browser/providers/preview' 4 - import { webdriverio } from '@vitest/browser/providers/webdriverio' 2 + import { playwright } from '@vitest/browser-playwright' 3 + import { preview } from '@vitest/browser-preview' 4 + import { webdriverio } from '@vitest/browser-webdriverio' 5 5 6 6 const providerName = (process.env.PROVIDER || 'playwright') as 'playwright' | 'webdriverio' | 'preview' 7 7 export const providers = {
+1 -2
test/browser/setup.unit.ts
··· 54 54 }) 55 55 56 56 declare module 'vitest' { 57 - // eslint-disable-next-line unused-imports/no-unused-vars 58 - interface Assertion<T = any> { 57 + interface Matchers { 59 58 // eslint-disable-next-line ts/method-signature-style 60 59 toReportPassedTest(testName: string, testProject?: string | BrowserInstanceOption[]): void 61 60 // eslint-disable-next-line ts/method-signature-style
+16 -2
test/browser/specs/locators.test.ts
··· 1 - import { expect, test } from 'vitest' 1 + import { expect, test, vi } from 'vitest' 2 2 import { instances, runBrowserTests } from './utils' 3 3 4 4 test('locators work correctly', async () => { 5 + const log = vi.fn() 5 6 const { stderr, stdout } = await runBrowserTests({ 6 7 root: './fixtures/locators', 7 - reporters: [['verbose', { isTTY: false }]], 8 + reporters: [ 9 + ['verbose', { isTTY: false }], 10 + { 11 + onInit(vitest) { 12 + vitest.logger.deprecate = log 13 + }, 14 + }, 15 + ], 8 16 }) 9 17 10 18 expect(stderr).toReportNoErrors() 19 + expect(log).toHaveBeenCalledWith( 20 + expect.stringContaining( 21 + `tries to load a deprecated "@vitest/browser/context" module. ` 22 + + `This import will stop working in the next major version. Please, use "vitest/browser" instead.`, 23 + ), 24 + ) 11 25 12 26 instances.forEach(({ browser }) => { 13 27 expect(stdout).toReportPassedTest('blog.test.tsx', browser)
+3 -3
test/browser/specs/playwright-connect.test.ts
··· 1 - import { playwright } from '@vitest/browser/providers/playwright' 1 + import { playwright } from '@vitest/browser-playwright' 2 2 import { chromium } from 'playwright' 3 3 import { expect, test } from 'vitest' 4 4 import { provider } from '../settings' ··· 27 27 28 28 await browserServer.close() 29 29 30 + expect(stderr).toBe('') 30 31 expect(stdout).toContain('Tests 2 passed') 31 32 expect(exitCode).toBe(0) 32 - expect(stderr).toBe('') 33 33 }) 34 34 35 35 test.runIf(provider.name === 'playwright')('[playwright] warns if both connect and launch mode are configured', async () => { ··· 56 56 57 57 await browserServer.close() 58 58 59 + expect(stderr).toContain('Found both connect and launch options in browser instance configuration.') 59 60 expect(stdout).toContain('Tests 2 passed') 60 61 expect(exitCode).toBe(0) 61 - expect(stderr).toContain('Found both connect and launch options in browser instance configuration.') 62 62 })
+9 -8
test/browser/specs/to-match-screenshot.test.ts
··· 1 - import type { ViteUserConfig } from 'vitest/config.js' 1 + import type { ViteUserConfig } from 'vitest/config' 2 2 import type { TestFsStructure } from '../../test-utils' 3 3 import { platform } from 'node:os' 4 4 import { resolve } from 'node:path' 5 - import { playwright } from '@vitest/browser/providers/playwright' 6 5 import { describe, expect, test } from 'vitest' 7 6 import { runVitestCli, useFS } from '../../test-utils' 8 7 import { extractToMatchScreenshotPaths } from '../fixtures/expect-dom/utils' ··· 13 12 const bgColor = '#fff' 14 13 15 14 const testContent = /* ts */` 16 - import { page, server } from '@vitest/browser/context' 15 + import { page, server } from 'vitest/browser' 17 16 import { describe, test } from 'vitest' 18 17 import { render } from './utils' 19 18 ··· 30 29 31 30 export async function runInlineTests( 32 31 structure: TestFsStructure, 33 - config?: ViteUserConfig['test'], 32 + config: ViteUserConfig['test'] = {}, 34 33 ) { 35 34 const root = resolve(process.cwd(), `vitest-test-${crypto.randomUUID()}`) 36 35 37 36 const fs = useFS(root, { 38 37 ...structure, 39 - 'vitest.config.ts': { 38 + 'vitest.config.ts': ` 39 + import { playwright } from '@vitest/browser-playwright' 40 + export default { 40 41 test: { 41 42 browser: { 42 43 enabled: true, 43 44 screenshotFailures: false, 44 45 provider: playwright(), 45 46 headless: true, 46 - instances: [{ browser }], 47 + instances: [{ browser: ${JSON.stringify(browser)} }], 47 48 }, 48 49 reporters: ['verbose'], 49 - ...config, 50 + ...${JSON.stringify(config)}, 50 51 }, 51 - }, 52 + }`, 52 53 }) 53 54 54 55 const vitest = await runVitestCli({
+1 -1
test/browser/test/cdp.test.ts
··· 1 - import { cdp, server } from '@vitest/browser/context' 2 1 import { describe, expect, it, onTestFinished, vi } from 'vitest' 2 + import { cdp, server } from 'vitest/browser' 3 3 4 4 describe.runIf( 5 5 server.provider === 'playwright' && server.browser === 'chromium',
+2 -2
test/browser/test/commands.test.ts
··· 1 - import { server } from '@vitest/browser/context' 2 1 import { expect, it } from 'vitest' 2 + import { server } from 'vitest/browser' 3 3 4 4 const { readFile, writeFile, removeFile, myCustomCommand } = server.commands 5 5 ··· 51 51 }) 52 52 }) 53 53 54 - declare module '@vitest/browser/context' { 54 + declare module 'vitest/browser' { 55 55 interface BrowserCommands { 56 56 myCustomCommand: (arg1: string, arg2: string) => Promise<{ 57 57 testPath: string
+1 -1
test/browser/test/dom.test.ts
··· 1 1 import { createNode } from '#src/createNode' 2 - import { page, server } from '@vitest/browser/context' 3 2 import { afterAll, beforeEach, describe, expect, test } from 'vitest' 3 + import { page, server } from 'vitest/browser' 4 4 import '../src/button.css' 5 5 6 6 afterAll(() => {
+1 -1
test/browser/test/expect-element.test.ts
··· 1 - import { page } from '@vitest/browser/context' 2 1 import { expect, test, vi } from 'vitest' 2 + import { page } from 'vitest/browser' 3 3 4 4 // element selector uses prettyDOM under the hood, which is an expensive call 5 5 // that should not be called on each failed locator attempt to avoid memory leak:
+1 -1
test/browser/test/iframe.test.ts
··· 1 - import { page, server } from '@vitest/browser/context' 2 1 import { expect, test } from 'vitest' 2 + import { page, server } from 'vitest/browser' 3 3 4 4 test.runIf(server.provider === 'playwright')('locates an iframe', async () => { 5 5 const iframe = document.createElement('iframe')
+2 -2
test/browser/test/userEvent.test.ts
··· 1 - import { userEvent as _uE, server } from '@vitest/browser/context' 2 1 import { beforeEach, describe, expect, test, vi } from 'vitest' 2 + import { userEvent as _uE, server } from 'vitest/browser' 3 3 import '../src/button.css' 4 4 5 5 beforeEach(() => { ··· 615 615 expect(value()).toBe('Another Value') 616 616 }) 617 617 618 - test('fill input in shadow root', async () => { 618 + test.skipIf(server.provider === 'preview')('fill input in shadow root', async () => { 619 619 const input = getInput() 620 620 const shadowRoot = createShadowRoot() 621 621 shadowRoot.appendChild(input)
+8 -9
test/browser/test/utils.test.ts
··· 1 - import { commands } from '@vitest/browser/context' 2 - import { prettyDOM } from '@vitest/browser/utils' 3 1 import { afterEach, expect, it, test } from 'vitest' 2 + import { commands, utils } from 'vitest/browser' 4 3 5 4 import { inspect } from 'vitest/internal/browser' 6 5 ··· 13 12 }) 14 13 15 14 test('prints default document', async () => { 16 - expect(await commands.stripVTControlCharacters(prettyDOM())).toMatchSnapshot() 15 + expect(await commands.stripVTControlCharacters(utils.prettyDOM())).toMatchSnapshot() 17 16 18 17 const div = document.createElement('div') 19 18 div.innerHTML = '<span>hello</span>' 20 19 document.body.append(div) 21 20 22 - expect(await commands.stripVTControlCharacters(prettyDOM())).toMatchSnapshot() 21 + expect(await commands.stripVTControlCharacters(utils.prettyDOM())).toMatchSnapshot() 23 22 }) 24 23 25 24 test('prints the element', async () => { ··· 27 26 div.innerHTML = '<span>hello</span>' 28 27 document.body.append(div) 29 28 30 - expect(await commands.stripVTControlCharacters(prettyDOM())).toMatchSnapshot() 29 + expect(await commands.stripVTControlCharacters(utils.prettyDOM())).toMatchSnapshot() 31 30 }) 32 31 33 32 test('prints the element with attributes', async () => { ··· 35 34 div.innerHTML = '<span class="some-name" data-test-id="33" id="5">hello</span>' 36 35 document.body.append(div) 37 36 38 - expect(await commands.stripVTControlCharacters(prettyDOM())).toMatchSnapshot() 37 + expect(await commands.stripVTControlCharacters(utils.prettyDOM())).toMatchSnapshot() 39 38 }) 40 39 41 40 test('should handle DOM content bigger than maxLength', async () => { ··· 50 49 parentDiv.innerHTML = domString 51 50 52 51 document.body.appendChild(parentDiv) 53 - expect(await commands.stripVTControlCharacters(prettyDOM(undefined, maxContent))).toMatchSnapshot() 52 + expect(await commands.stripVTControlCharacters(utils.prettyDOM(undefined, maxContent))).toMatchSnapshot() 54 53 }) 55 54 56 55 test('should handle shadow DOM content', async () => { ··· 71 70 div.innerHTML = '<custom-element></custom-element>' 72 71 document.body.append(div) 73 72 74 - expect(await commands.stripVTControlCharacters(prettyDOM())).toMatchSnapshot() 73 + expect(await commands.stripVTControlCharacters(utils.prettyDOM())).toMatchSnapshot() 75 74 }) 76 75 77 76 test('should be able to opt out of shadow DOM content', async () => { ··· 92 91 div.innerHTML = '<no-shadow-root></no-shadow-root>' 93 92 document.body.append(div) 94 93 95 - expect(await commands.stripVTControlCharacters(prettyDOM(undefined, undefined, { printShadowRoot: false }))).toMatchSnapshot() 94 + expect(await commands.stripVTControlCharacters(utils.prettyDOM(undefined, undefined, { printShadowRoot: false }))).toMatchSnapshot() 96 95 })
+1 -1
test/browser/test/viewport.test.ts
··· 1 - import { server } from '@vitest/browser/context' 2 1 import { describe, expect, it } from 'vitest' 2 + import { server } from 'vitest/browser' 3 3 4 4 describe.skipIf( 5 5 // preview cannot control viewport
-1
test/browser/tsconfig.json
··· 9 9 }, 10 10 "types": [ 11 11 "vite/client", 12 - "@vitest/browser/providers/playwright", 13 12 "vitest-browser-react", 14 13 "vitest/import-meta" 15 14 ],
+4 -4
test/browser/vitest.config.mts
··· 2 2 import { dirname, resolve } from 'node:path' 3 3 import { fileURLToPath } from 'node:url' 4 4 import * as util from 'node:util' 5 - import { playwright } from '@vitest/browser/providers/playwright' 6 - import { preview } from '@vitest/browser/providers/preview' 7 - import { webdriverio } from '@vitest/browser/providers/webdriverio' 5 + import { playwright } from '@vitest/browser-playwright' 6 + import { preview } from '@vitest/browser-preview' 7 + import { webdriverio } from '@vitest/browser-webdriverio' 8 8 import { defineConfig } from 'vitest/config' 9 9 10 10 const dir = dirname(fileURLToPath(import.meta.url)) 11 11 12 12 const providerName = process.env.PROVIDER || 'playwright' 13 - const browser = process.env.BROWSER || (providerName === 'playwright' ? 'chromium' : 'chrome') 13 + const browser = process.env.BROWSER as 'firefox' || (providerName === 'playwright' ? 'chromium' : 'chrome') 14 14 const provider = { 15 15 playwright, 16 16 preview,
+1 -1
test/cli/fixtures/browser-init/package.json
··· 6 6 "vitest": "latest" 7 7 }, 8 8 "devDependencies": { 9 - "@vitest/browser": "latest" 9 + "@vitest/browser-preview": "latest" 10 10 } 11 11 }
+1 -1
test/cli/fixtures/browser-multiple/vitest.config.ts
··· 1 - import { playwright } from '@vitest/browser/providers/playwright'; 1 + import { playwright } from '@vitest/browser-playwright'; 2 2 import { resolve } from 'pathe'; 3 3 import { defineConfig } from 'vitest/config'; 4 4
+1 -1
test/cli/fixtures/config-loader/browser/vitest.config.ts
··· 1 1 import { defineConfig } from "vitest/config" 2 2 import "@test/test-dep-linked/ts"; 3 - import { playwright } from '@vitest/browser/providers/playwright'; 3 + import { playwright } from '@vitest/browser-playwright'; 4 4 5 5 export default defineConfig({ 6 6 test: {
+1 -1
test/cli/fixtures/fails/node-browser-context.test.ts
··· 1 - import { page } from '@vitest/browser/context' 1 + import { page } from 'vitest/browser' 2 2 3 3 console.log(page)
+1 -1
test/cli/fixtures/list/vitest.config.ts
··· 1 - import { playwright } from '@vitest/browser/providers/playwright' 1 + import { playwright } from '@vitest/browser-playwright' 2 2 import { fileURLToPath } from 'node:url' 3 3 import { defineConfig } from 'vitest/config' 4 4
+1 -1
test/cli/fixtures/public-api/vitest.config.ts
··· 1 - import { playwright } from '@vitest/browser/providers/playwright' 1 + import { playwright } from '@vitest/browser-playwright' 2 2 import { defineConfig } from 'vitest/config' 3 3 4 4 export default defineConfig({
+2
test/cli/package.json
··· 12 12 "@types/debug": "catalog:", 13 13 "@types/ws": "catalog:", 14 14 "@vitejs/plugin-basic-ssl": "^2.1.0", 15 + "@vitest/browser-playwright": "workspace:*", 16 + "@vitest/browser-preview": "workspace:*", 15 17 "@vitest/runner": "workspace:^", 16 18 "@vitest/utils": "workspace:*", 17 19 "debug": "^4.4.3",
+1 -1
test/cli/test/__snapshots__/fails.test.ts.snap
··· 61 61 62 62 exports[`should fail no-assertions.test.ts 1`] = `"Error: expected any number of assertion, but got none"`; 63 63 64 - exports[`should fail node-browser-context.test.ts 1`] = `"Error: @vitest/browser/context can be imported only inside the Browser Mode. Your test is running in forks pool. Make sure your regular tests are excluded from the "test.include" glob pattern."`; 64 + exports[`should fail node-browser-context.test.ts 1`] = `"Error: vitest/browser can be imported only inside the Browser Mode. Your test is running in forks pool. Make sure your regular tests are excluded from the "test.include" glob pattern."`; 65 65 66 66 exports[`should fail poll-no-awaited.test.ts 1`] = ` 67 67 "Error: expect.poll(assertion).toBe() was not awaited. This assertion is asynchronous and must be awaited; otherwise, it is not executed to avoid unhandled rejections:
+2 -2
test/cli/test/annotations.test.ts
··· 1 1 import type { TestAnnotation } from 'vitest' 2 - import { playwright } from '@vitest/browser/providers/playwright' 2 + import { playwright } from '@vitest/browser-playwright' 3 3 import { describe, expect, test } from 'vitest' 4 4 import { runInlineTests } from '../../test-utils' 5 5 ··· 40 40 provider: playwright(), 41 41 headless: true, 42 42 instances: [ 43 - { browser: 'chromium' }, 43 + { browser: 'chromium' as const }, 44 44 ], 45 45 }, 46 46 },
+10 -11
test/cli/test/fails.test.ts
··· 1 1 import type { TestCase } from 'vitest/node' 2 - import { playwright } from '@vitest/browser/providers/playwright' 2 + import { playwright } from '@vitest/browser-playwright' 3 3 4 4 import { resolve } from 'pathe' 5 5 import { glob } from 'tinyglobby' ··· 109 109 110 110 it('prints a warning if the assertion is not awaited in the browser mode', async () => { 111 111 const { stderr } = await runInlineTests({ 112 - './vitest.config.js': { 113 - test: { 114 - browser: { 115 - enabled: true, 116 - instances: [{ browser: 'chromium' }], 117 - provider: playwright(), 118 - headless: true, 119 - }, 120 - }, 121 - }, 122 112 'base.test.js': ts` 123 113 import { expect, test } from 'vitest'; 124 114 ··· 126 116 expect(Promise.resolve(1)).resolves.toBe(1) 127 117 }) 128 118 `, 119 + }, {}, {}, { 120 + test: { 121 + browser: { 122 + enabled: true, 123 + instances: [{ browser: 'chromium' }], 124 + provider: playwright(), 125 + headless: true, 126 + }, 127 + }, 129 128 }) 130 129 expect(stderr).toContain('Promise returned by \`expect(actual).resolves.toBe(expected)\` was not awaited') 131 130 expect(stderr).toContain('base.test.js:5:33')
+1 -1
test/cli/test/init.test.ts
··· 54 54 55 55 expect(await getFileContent('/vitest.browser.config.ts')).toMatchInlineSnapshot(` 56 56 "import { defineConfig } from 'vitest/config' 57 - import { preview } from '@vitest/browser/providers/preview' 57 + import { preview } from '@vitest/browser-preview' 58 58 59 59 export default defineConfig({ 60 60 test: {
+2 -2
test/cli/test/scoped-fixtures.test.ts
··· 4 4 import type { ViteUserConfig } from 'vitest/config' 5 5 import type { TestSpecification, TestUserConfig } from 'vitest/node' 6 6 import type { TestFsStructure } from '../../test-utils' 7 - import { playwright } from '@vitest/browser/providers/playwright' 7 + import { playwright } from '@vitest/browser-playwright' 8 8 import { runInlineTests } from '../../test-utils' 9 9 10 10 interface TestContext { ··· 555 555 provider: playwright(), 556 556 headless: true, 557 557 instances: [ 558 - { browser: 'chromium', name: '' }, 558 + { browser: 'chromium' as const, name: '' }, 559 559 ], 560 560 }, 561 561 },
+1 -1
test/config/fixtures/browser-custom-html/vitest.config.correct.ts
··· 1 - import { playwright } from '@vitest/browser/providers/playwright'; 1 + import { playwright } from '@vitest/browser-playwright'; 2 2 import { defineConfig } from 'vitest/config'; 3 3 4 4 export default defineConfig({
+1 -1
test/config/fixtures/browser-custom-html/vitest.config.custom-transformIndexHtml.ts
··· 1 - import { playwright } from '@vitest/browser/providers/playwright'; 1 + import { playwright } from '@vitest/browser-playwright'; 2 2 import { defineConfig } from 'vitest/config'; 3 3 4 4 export default defineConfig({
+1 -1
test/config/fixtures/browser-custom-html/vitest.config.default-transformIndexHtml.ts
··· 1 - import { playwright } from '@vitest/browser/providers/playwright'; 1 + import { playwright } from '@vitest/browser-playwright'; 2 2 import { defineConfig } from 'vitest/config'; 3 3 4 4 export default defineConfig({
+1 -1
test/config/fixtures/browser-custom-html/vitest.config.error-hook.ts
··· 1 - import { playwright } from '@vitest/browser/providers/playwright'; 1 + import { playwright } from '@vitest/browser-playwright'; 2 2 import { defineConfig } from 'vitest/config'; 3 3 4 4 export default defineConfig({
+1 -1
test/config/fixtures/browser-custom-html/vitest.config.non-existing.ts
··· 1 - import { playwright } from '@vitest/browser/providers/playwright'; 1 + import { playwright } from '@vitest/browser-playwright'; 2 2 import { defineConfig } from 'vitest/config'; 3 3 4 4 export default defineConfig({
+2 -1
test/config/fixtures/browser-no-config/vitest.config.ts
··· 4 4 test: { 5 5 browser: { 6 6 headless: true, 7 - } as any, // testing that instances is required 7 + // testing that instances is required 8 + }, 8 9 }, 9 10 })
+3
test/config/package.json
··· 6 6 "test": "vitest --typecheck.enabled" 7 7 }, 8 8 "devDependencies": { 9 + "@vitest/browser-playwright": "workspace:*", 10 + "@vitest/browser-preview": "workspace:*", 11 + "@vitest/browser-webdriverio": "workspace:*", 9 12 "@vitest/test-dep-conditions": "file:./deps/test-dep-conditions", 10 13 "tinyexec": "^0.3.2", 11 14 "vite": "latest",
+1 -1
test/config/test/bail.test.ts
··· 1 1 import type { TestUserConfig } from 'vitest/node' 2 2 3 - import { playwright } from '@vitest/browser/providers/playwright' 3 + import { playwright } from '@vitest/browser-playwright' 4 4 import { expect, test } from 'vitest' 5 5 import { runVitest } from '../../test-utils' 6 6
+14 -15
test/config/test/browser-configs.test.ts
··· 2 2 import type { TestUserConfig, VitestOptions } from 'vitest/node' 3 3 import type { TestFsStructure } from '../../test-utils' 4 4 import crypto from 'node:crypto' 5 - import { playwright } from '@vitest/browser/providers/playwright' 6 - import { webdriverio } from '@vitest/browser/providers/webdriverio' 7 - import { preview } from '@vitest/browser/src/node/providers/preview.js' 5 + import { playwright } from '@vitest/browser-playwright' 6 + import { preview } from '@vitest/browser-preview' 7 + import { webdriverio } from '@vitest/browser-webdriverio' 8 8 import { resolve } from 'pathe' 9 9 import { describe, expect, onTestFinished, test } from 'vitest' 10 10 import { createVitest } from 'vitest/node' ··· 21 21 browser: { 22 22 enabled: true, 23 23 headless: true, 24 + provider: preview(), 24 25 instances: [ 25 26 { browser: 'chromium' }, 26 27 { browser: 'firefox' }, ··· 39 40 const { projects } = await vitest({ project: 'chromium' }, { 40 41 browser: { 41 42 enabled: true, 43 + provider: preview(), 42 44 instances: [ 43 45 { browser: 'chromium' }, 44 46 { browser: 'firefox' }, ··· 55 57 const { projects } = await vitest({ project: 'chrom*' }, { 56 58 browser: { 57 59 enabled: true, 60 + provider: preview(), 58 61 instances: [ 59 62 { browser: 'chromium' }, 60 63 { browser: 'firefox' }, ··· 76 79 browser: { 77 80 enabled: true, 78 81 headless: true, 82 + provider: preview(), 79 83 instances: [ 80 84 { browser: 'chromium' }, 81 85 { browser: 'firefox' }, ··· 103 107 } as any, 104 108 browser: { 105 109 enabled: true, 110 + provider: preview(), 106 111 headless: true, 107 112 screenshotFailures: false, 108 113 testerHtmlPath: '/custom-path.html', ··· 244 249 includeSource: ['src/**/*.{js,ts}'], 245 250 browser: { 246 251 enabled: true, 252 + provider: preview(), 247 253 headless: true, 248 254 instances: [ 249 255 { browser: 'chromium' }, ··· 299 305 include: ['**/*.test.{js,ts}'], 300 306 browser: { 301 307 enabled: true, 308 + provider: preview(), 302 309 headless: true, 303 310 instances: [ 304 311 { browser: 'chromium', include: [] }, ··· 349 356 { 350 357 browser: { 351 358 enabled: true, 359 + provider: preview(), 352 360 headless: true, 353 361 instances: [], 354 362 }, ··· 387 395 expect(projects[1].config.browser.headless).toBe(true) 388 396 }) 389 397 390 - test('core provider has no options if `provider` is not set', async () => { 391 - const v = await vitest({}, { 392 - browser: { 393 - enabled: true, 394 - instances: [{ 395 - browser: 'chromium', 396 - }], 397 - }, 398 - }) 399 - expect(v.config.browser.provider).toEqual(undefined) 400 - }) 401 - 402 398 test('core provider has options if `provider` is playwright', async () => { 403 399 const v = await vitest({}, { 404 400 browser: { ··· 512 508 }, { 513 509 browser: { 514 510 enabled: true, 511 + provider: preview(), 515 512 instances: [ 516 513 { browser: 'chromium' }, 517 514 ], ··· 785 782 name: 'browser', 786 783 browser: { 787 784 enabled: true, 785 + provider: preview(), 788 786 instances: [], 789 787 }, 790 788 }, ··· 839 837 name: 'browser', 840 838 browser: { 841 839 enabled: true, 840 + provider: preview(), 842 841 instances: [], 843 842 }, 844 843 },
+5 -23
test/config/test/failures.test.ts
··· 1 1 import type { UserConfig as ViteUserConfig } from 'vite' 2 2 import type { TestUserConfig } from 'vitest/node' 3 3 import type { VitestRunnerCLIOptions } from '../../test-utils' 4 - import { playwright } from '@vitest/browser/providers/playwright' 5 - import { preview } from '@vitest/browser/providers/preview' 6 - import { webdriverio } from '@vitest/browser/providers/webdriverio' 4 + import { playwright } from '@vitest/browser-playwright' 5 + import { preview } from '@vitest/browser-preview' 6 + import { webdriverio } from '@vitest/browser-webdriverio' 7 7 import { normalize, resolve } from 'pathe' 8 8 import { beforeEach, expect, test } from 'vitest' 9 9 import { version } from 'vitest/package.json' ··· 405 405 }, 406 406 }, 407 407 }) 408 - expect(stderr).toMatch('"browser.instances" was set in the config, but the array is empty. Define at least one browser config.') 408 + expect(stderr).toMatch(`Vitest wasn't able to resolve any project. Please, check that you specified the "browser.instances" option.`) 409 409 }) 410 410 411 411 test('browser.name or browser.instances are required', async () => { 412 412 const { stderr, exitCode } = await runVitestCli('--browser.enabled', '--root=./fixtures/browser-no-config') 413 413 expect(exitCode).toBe(1) 414 - expect(stderr).toMatch('Vitest Browser Mode requires "browser.name" (deprecated) or "browser.instances" options, none were set.') 414 + expect(stderr).toMatch('Vitest received --browser flag, but no project had a browser configuration.') 415 415 }) 416 416 417 417 test('--browser flag without browser configuration throws an error', async () => { ··· 495 495 }, 496 496 }) 497 497 expect(stderr).toMatch('Cannot define a nested project for a firefox browser. The project name "1 (firefox)" was already defined. If you have multiple instances for the same browser, make sure to define a custom "name". All projects should have unique names. Make sure your configuration is correct.') 498 - }) 499 - 500 - test('throws an error if several browsers are headed in nonTTY mode', async () => { 501 - const { stderr } = await runVitest({}, { 502 - test: { 503 - browser: { 504 - enabled: true, 505 - provider: playwright(), 506 - headless: false, 507 - instances: [ 508 - { browser: 'chromium' }, 509 - { browser: 'firefox' }, 510 - ], 511 - }, 512 - }, 513 - }) 514 - expect(stderr).toContain('Found multiple projects that run browser tests in headed mode: "chromium", "firefox"') 515 - expect(stderr).toContain('Please, filter projects with --browser=name or --project=name flag or run tests with "headless: true" option') 516 498 }) 517 499 518 500 test('non existing project name will throw', async () => {
+5
test/core/test/exports.test.ts
··· 9 9 const manifest = await getPackageExportsManifest({ 10 10 importMode: 'package', // or 'dist' or 'package' 11 11 cwd: resolve(import.meta.dirname, '../../../packages/vitest'), 12 + resolveExportEntries(entries) { 13 + return entries.filter(([key]) => key !== './browser') 14 + }, 12 15 }) 13 16 14 17 if (rolldownVersion) { ··· 59 62 "./internal/browser": { 60 63 "DecodedMap": "function", 61 64 "SpyModule": "object", 65 + "__INTERNAL": "object", 62 66 "collectTests": "function", 63 67 "format": "function", 64 68 "getOriginalPosition": "function", ··· 210 214 "./internal/browser": { 211 215 "DecodedMap": "function", 212 216 "SpyModule": "object", 217 + "__INTERNAL": "object", 213 218 "collectTests": "function", 214 219 "format": "function", 215 220 "getOriginalPosition": "function",
+1 -1
test/coverage-test/package.json
··· 10 10 "@types/istanbul-lib-coverage": "catalog:", 11 11 "@types/istanbul-lib-report": "catalog:", 12 12 "@vitejs/plugin-vue": "latest", 13 - "@vitest/browser": "workspace:*", 13 + "@vitest/browser-playwright": "workspace:*", 14 14 "@vitest/coverage-istanbul": "workspace:*", 15 15 "@vitest/coverage-v8": "workspace:*", 16 16 "@vitest/web-worker": "workspace:*",
+1 -1
test/coverage-test/utils.ts
··· 7 7 import { resolve } from 'node:path' 8 8 import { fileURLToPath } from 'node:url' 9 9 import { stripVTControlCharacters } from 'node:util' 10 - import { playwright } from '@vitest/browser/providers/playwright' 10 + import { playwright } from '@vitest/browser-playwright' 11 11 import libCoverage from 'istanbul-lib-coverage' 12 12 import { normalize } from 'pathe' 13 13 import { vi, describe as vitestDescribe, test as vitestTest } from 'vitest'
+1 -1
test/dts-playwright/package.json
··· 6 6 "test": "tsc -b" 7 7 }, 8 8 "devDependencies": { 9 - "@vitest/browser": "workspace:*", 9 + "@vitest/browser-playwright": "workspace:*", 10 10 "vitest": "workspace:*" 11 11 } 12 12 }
+1 -1
test/dts-playwright/src/basic.test.ts
··· 1 - import { page, userEvent } from '@vitest/browser/context' 2 1 import { test } from 'vitest' 2 + import { page, userEvent } from 'vitest/browser' 3 3 4 4 test('basic', async () => { 5 5 document.body.innerHTML = `<button>hello</button>`
-1
test/dts-playwright/tsconfig.json
··· 4 4 "target": "ESNext", 5 5 "module": "ESNext", 6 6 "moduleResolution": "Bundler", 7 - "types": ["@vitest/browser/providers/playwright"], 8 7 "strict": true, 9 8 "noEmit": true 10 9 },
+1 -1
test/dts-playwright/vite.config.ts
··· 1 - import { playwright } from '@vitest/browser/providers/playwright' 1 + import { playwright } from '@vitest/browser-playwright' 2 2 import { defineConfig } from 'vitest/config' 3 3 4 4 export default defineConfig({
+9
test/test-utils/index.ts
··· 298 298 ${(content[1].exports || []).map(e => `export const ${e} = results["${e}"]`)} 299 299 ` 300 300 } 301 + if ('test' in content && content.test?.browser?.enabled && content.test?.browser?.provider?.name) { 302 + const name = content.test.browser.provider.name 303 + return ` 304 + import { ${name} } from '@vitest/browser-${name}' 305 + const config = ${JSON.stringify(content)} 306 + config.test.browser.provider = ${name}(${JSON.stringify(content.test.browser.provider.options || {})}) 307 + export default config 308 + ` 309 + } 301 310 return `export default ${JSON.stringify(content)}` 302 311 } 303 312
+2 -1
test/watch/package.json
··· 6 6 "test": "vitest" 7 7 }, 8 8 "devDependencies": { 9 - "@vitest/browser": "workspace:*", 9 + "@vitest/browser-playwright": "workspace:*", 10 + "@vitest/browser-webdriverio": "workspace:*", 10 11 "vite": "latest", 11 12 "vitest": "workspace:*" 12 13 }
+1 -1
test/watch/test/config-watching.test.ts
··· 1 - import { playwright } from '@vitest/browser/providers/playwright' 1 + import { playwright } from '@vitest/browser-playwright' 2 2 import { expect, test } from 'vitest' 3 3 import { runInlineTests } from '../../test-utils' 4 4
+1 -1
test/watch/test/file-watching.test.ts
··· 1 1 import { existsSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs' 2 - import { webdriverio } from '@vitest/browser/providers/webdriverio' 2 + import { webdriverio } from '@vitest/browser-webdriverio' 3 3 4 4 import { afterEach, describe, expect, test } from 'vitest' 5 5 import * as testUtils from '../../test-utils'
+1 -1
test/workspaces-browser/package.json
··· 6 6 "test": "vitest run" 7 7 }, 8 8 "devDependencies": { 9 - "@vitest/browser": "workspace:^", 9 + "@vitest/browser-playwright": "workspace:^", 10 10 "vitest": "workspace:*" 11 11 } 12 12 }
+2 -2
test/workspaces-browser/space_browser/vitest.config.ts
··· 1 - import { playwright } from '@vitest/browser/providers/playwright' 1 + import { playwright } from '@vitest/browser-playwright' 2 2 import { defineProject } from 'vitest/config' 3 3 4 4 export default defineProject({ 5 5 test: { 6 6 browser: { 7 7 enabled: true, 8 - instances: [{ browser: process.env.BROWSER || 'chromium' }], 8 + instances: [{ browser: process.env.BROWSER as 'chromium' || 'chromium' }], 9 9 headless: true, 10 10 provider: playwright(), 11 11 },
+2 -2
test/workspaces-browser/vitest.config.ts
··· 1 - import { playwright } from '@vitest/browser/providers/playwright' 1 + import { playwright } from '@vitest/browser-playwright' 2 2 import { defineConfig } from 'vitest/config' 3 3 4 4 if (process.env.TEST_WATCH) { ··· 21 21 root: './space_browser_inline', 22 22 browser: { 23 23 enabled: true, 24 - instances: [{ browser: process.env.BROWSER || 'chromium' }], 24 + instances: [{ browser: process.env.BROWSER as 'chromium' || 'chromium' }], 25 25 headless: true, 26 26 provider: playwright(), 27 27 },
+3
tsconfig.base.json
··· 19 19 "@vitest/mocker/browser": ["./packages/mocker/src/browser/index.ts"], 20 20 "@vitest/runner": ["./packages/runner/src/index.ts"], 21 21 "@vitest/runner/*": ["./packages/runner/src/*"], 22 + "@vitest/browser-playwright": ["./packages/browser-playwright/src/index.ts"], 22 23 "@vitest/browser": ["./packages/browser/src/node/index.ts"], 23 24 "@vitest/browser/client": ["./packages/browser/src/client/client.ts"], 24 25 "~/*": ["./packages/ui/client/*"], 25 26 "vitest": ["./packages/vitest/src/public/index.ts"], 27 + "vitest/internal/browser": ["./packages/vitest/src/public/browser.ts"], 26 28 "vitest/internal/module-runner": ["./packages/vitest/src/public/module-runner.ts"], 27 29 "vitest/globals": ["./packages/vitest/globals.d.ts"], 30 + "vitest/browser": ["./packages/vitest/browser/context.d.ts"], 28 31 "vitest/*": ["./packages/vitest/src/public/*"], 29 32 "vite-node": ["./packages/vite-node/src/index.ts"], 30 33 "vite-node/*": ["./packages/vite-node/src/*"]