mirror of
https://github.com/vitest-dev/vitest.git
synced 2025-12-08 18:26:03 +00:00
feat: introduce separate packages for browser mode providers (#8629)
This commit is contained in:
parent
88e62c7586
commit
0dc93ea988
@ -253,6 +253,11 @@ export default ({ mode }: { mode: string }) => {
|
||||
link: '/guide/browser/webdriverio',
|
||||
docFooterText: 'Configuring WebdriverIO | Browser Mode',
|
||||
},
|
||||
{
|
||||
text: 'Configuring Preview',
|
||||
link: '/guide/browser/preview',
|
||||
docFooterText: 'Configuring Preview | Browser Mode',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@ -7,10 +7,10 @@ title: Assertion API | Browser Mode
|
||||
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.
|
||||
|
||||
::: tip TypeScript Support
|
||||
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`:
|
||||
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`:
|
||||
|
||||
```ts
|
||||
/// <reference types="@vitest/browser/context" />
|
||||
/// <reference types="vitest/browser" />
|
||||
```
|
||||
:::
|
||||
|
||||
@ -18,7 +18,7 @@ Tests in the browser might fail inconsistently due to their asynchronous nature.
|
||||
|
||||
```ts
|
||||
import { expect, test } from 'vitest'
|
||||
import { page } from '@vitest/browser/context'
|
||||
import { page } from 'vitest/browser'
|
||||
|
||||
test('error banner is rendered', async () => {
|
||||
triggerError()
|
||||
|
||||
@ -20,7 +20,7 @@ This API follows [`server.fs`](https://vitejs.dev/config/server-options.html#ser
|
||||
:::
|
||||
|
||||
```ts
|
||||
import { server } from '@vitest/browser/context'
|
||||
import { server } from 'vitest/browser'
|
||||
|
||||
const { readFile, writeFile, removeFile } = server.commands
|
||||
|
||||
@ -38,10 +38,10 @@ it('handles files', async () => {
|
||||
|
||||
## CDP Session
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
```ts
|
||||
import { cdp } from '@vitest/browser/context'
|
||||
import { cdp } from 'vitest/browser'
|
||||
|
||||
const input = document.createElement('input')
|
||||
document.body.appendChild(input)
|
||||
@ -97,10 +97,10 @@ export default function BrowserCommands(): Plugin {
|
||||
}
|
||||
```
|
||||
|
||||
Then you can call it inside your test by importing it from `@vitest/browser/context`:
|
||||
Then you can call it inside your test by importing it from `vitest/browser`:
|
||||
|
||||
```ts
|
||||
import { commands } from '@vitest/browser/context'
|
||||
import { commands } from 'vitest/browser'
|
||||
import { expect, test } from 'vitest'
|
||||
|
||||
test('custom command works correctly', async () => {
|
||||
@ -109,7 +109,7 @@ test('custom command works correctly', async () => {
|
||||
})
|
||||
|
||||
// if you are using TypeScript, you can augment the module
|
||||
declare module '@vitest/browser/context' {
|
||||
declare module 'vitest/browser' {
|
||||
interface BrowserCommands {
|
||||
myCustomCommand: (arg1: string, arg2: string) => Promise<{
|
||||
someValue: true
|
||||
|
||||
@ -138,7 +138,7 @@ The key is using `page.elementLocator()` to bridge Testing Library's DOM output
|
||||
```jsx
|
||||
// For Solid.js components
|
||||
import { render } from '@testing-library/solid'
|
||||
import { page } from '@vitest/browser/context'
|
||||
import { page } from 'vitest/browser'
|
||||
|
||||
test('Solid component handles user interaction', async () => {
|
||||
// Use Testing Library to render the component
|
||||
@ -564,7 +564,7 @@ import { render } from 'vitest-browser-react' // [!code ++]
|
||||
### Key Differences
|
||||
|
||||
- Use `await expect.element()` instead of `expect()` for DOM assertions
|
||||
- Use `@vitest/browser/context` for user interactions instead of `@testing-library/user-event`
|
||||
- Use `vitest/browser` for user interactions instead of `@testing-library/user-event`
|
||||
- Browser Mode provides real browser environment for accurate testing
|
||||
|
||||
## Learn More
|
||||
|
||||
@ -4,7 +4,7 @@ You can change the browser configuration by updating the `test.browser` field in
|
||||
|
||||
```ts [vitest.config.ts]
|
||||
import { defineConfig } from 'vitest/config'
|
||||
import { playwright } from '@vitest/browser/providers/playwright'
|
||||
import { playwright } from '@vitest/browser-playwright'
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
@ -47,14 +47,11 @@ Run all tests inside a browser by default. Note that `--browser` only works if y
|
||||
## browser.instances
|
||||
|
||||
- **Type:** `BrowserConfig`
|
||||
- **Default:** `[{ browser: name }]`
|
||||
- **Default:** `[]`
|
||||
|
||||
Defines multiple browser setups. Every config has to have at least a `browser` field. The config supports your providers configurations:
|
||||
Defines multiple browser setups. Every config has to have at least a `browser` field.
|
||||
|
||||
- [Configuring Playwright](/guide/browser/playwright)
|
||||
- [Configuring WebdriverIO](/guide/browser/webdriverio)
|
||||
|
||||
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`.
|
||||
You can specify most of the [project options](/config/) (not marked with a <NonProjectOption /> icon) and some of the `browser` options like `browser.testerHtmlPath`.
|
||||
|
||||
::: warning
|
||||
Every browser config inherits options from the root config:
|
||||
@ -79,8 +76,6 @@ export default defineConfig({
|
||||
})
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
For more examples, refer to the ["Multiple Setups" guide](/guide/browser/multiple-setups).
|
||||
:::
|
||||
|
||||
@ -94,8 +89,6 @@ List of available `browser` options:
|
||||
- [`browser.screenshotFailures`](#browser-screenshotfailures)
|
||||
- [`browser.provider`](#browser-provider)
|
||||
|
||||
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.
|
||||
|
||||
Under the hood, Vitest transforms these instances into separate [test projects](/advanced/api/test-project) sharing a single Vite server for better caching performance.
|
||||
|
||||
## browser.headless
|
||||
@ -134,12 +127,12 @@ Configure options for Vite server that serves code in the browser. Does not affe
|
||||
- **Default:** `'preview'`
|
||||
- **CLI:** `--browser.provider=playwright`
|
||||
|
||||
The return value of the provider factory. You can import the factory from `@vitest/browser/providers/<provider-name>` or make your own provider:
|
||||
The return value of the provider factory. You can import the factory from `@vitest/browser-<provider-name>` or make your own provider:
|
||||
|
||||
```ts{8-10}
|
||||
import { playwright } from '@vitest/browser/providers/playwright'
|
||||
import { webdriverio } from '@vitest/browser/providers/webdriverio'
|
||||
import { preview } from '@vitest/browser/providers/preview'
|
||||
import { playwright } from '@vitest/browser-playwright'
|
||||
import { webdriverio } from '@vitest/browser-webdriverio'
|
||||
import { preview } from '@vitest/browser-preview'
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
@ -155,7 +148,7 @@ export default defineConfig({
|
||||
To configure how provider initializes the browser, you can pass down options to the factory function:
|
||||
|
||||
```ts{7-13,20-26}
|
||||
import { playwright } from '@vitest/browser/providers/playwright'
|
||||
import { playwright } from '@vitest/browser-playwright'
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
@ -294,7 +287,7 @@ export interface BrowserScript {
|
||||
- **Type:** `Record<string, BrowserCommand>`
|
||||
- **Default:** `{ readFile, writeFile, ... }`
|
||||
|
||||
Custom [commands](/guide/browser/commands) that can be imported during browser tests from `@vitest/browser/commands`.
|
||||
Custom [commands](/guide/browser/commands) that can be imported during browser tests from `vitest/browser`.
|
||||
|
||||
## browser.connectTimeout
|
||||
|
||||
|
||||
@ -4,7 +4,7 @@ title: Context API | Browser Mode
|
||||
|
||||
# Context API
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
## `userEvent`
|
||||
|
||||
|
||||
@ -99,7 +99,7 @@ To activate browser mode in your Vitest configuration, set the `browser.enabled`
|
||||
|
||||
```ts [vitest.config.ts]
|
||||
import { defineConfig } from 'vitest/config'
|
||||
import { playwright } from '@vitest/browser/providers/playwright'
|
||||
import { playwright } from '@vitest/browser-playwright'
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
@ -127,7 +127,7 @@ If you have not used Vite before, make sure you have your framework's plugin ins
|
||||
```ts [react]
|
||||
import { defineConfig } from 'vitest/config'
|
||||
import react from '@vitejs/plugin-react'
|
||||
import { playwright } from '@vitest/browser/providers/playwright'
|
||||
import { playwright } from '@vitest/browser-playwright'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
@ -144,7 +144,7 @@ export default defineConfig({
|
||||
```
|
||||
```ts [vue]
|
||||
import { defineConfig } from 'vitest/config'
|
||||
import { playwright } from '@vitest/browser/providers/playwright'
|
||||
import { playwright } from '@vitest/browser-playwright'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
|
||||
export default defineConfig({
|
||||
@ -163,7 +163,7 @@ export default defineConfig({
|
||||
```ts [svelte]
|
||||
import { defineConfig } from 'vitest/config'
|
||||
import { svelte } from '@sveltejs/vite-plugin-svelte'
|
||||
import { playwright } from '@vitest/browser/providers/playwright'
|
||||
import { playwright } from '@vitest/browser-playwright'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [svelte()],
|
||||
@ -181,7 +181,7 @@ export default defineConfig({
|
||||
```ts [solid]
|
||||
import { defineConfig } from 'vitest/config'
|
||||
import solidPlugin from 'vite-plugin-solid'
|
||||
import { playwright } from '@vitest/browser/providers/playwright'
|
||||
import { playwright } from '@vitest/browser-playwright'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [solidPlugin()],
|
||||
@ -199,7 +199,7 @@ export default defineConfig({
|
||||
```ts [marko]
|
||||
import { defineConfig } from 'vitest/config'
|
||||
import marko from '@marko/vite'
|
||||
import { playwright } from '@vitest/browser/providers/playwright'
|
||||
import { playwright } from '@vitest/browser-playwright'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [marko()],
|
||||
@ -217,7 +217,7 @@ export default defineConfig({
|
||||
```ts [qwik]
|
||||
import { defineConfig } from 'vitest/config'
|
||||
import { qwikVite } from '@builder.io/qwik/optimizer'
|
||||
import { playwright } from '@vitest/browser/providers/playwright'
|
||||
import { playwright } from '@vitest/browser-playwright'
|
||||
|
||||
// optional, run the tests in SSR mode
|
||||
import { testSSR } from 'vitest-browser-qwik/ssr-plugin'
|
||||
@ -241,7 +241,7 @@ If you need to run some tests using Node-based runner, you can define a [`projec
|
||||
|
||||
```ts [vitest.config.ts]
|
||||
import { defineConfig } from 'vitest/config'
|
||||
import { playwright } from '@vitest/browser/providers/playwright'
|
||||
import { playwright } from '@vitest/browser-playwright'
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
@ -338,7 +338,7 @@ Here's an example configuration enabling headless mode:
|
||||
|
||||
```ts [vitest.config.ts]
|
||||
import { defineConfig } from 'vitest/config'
|
||||
import { playwright } from '@vitest/browser/providers/playwright'
|
||||
import { playwright } from '@vitest/browser-playwright'
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
@ -369,7 +369,7 @@ By default, you don't need any external packages to work with the Browser Mode:
|
||||
|
||||
```js [example.test.js]
|
||||
import { expect, test } from 'vitest'
|
||||
import { page } from '@vitest/browser/context'
|
||||
import { page } from 'vitest/browser'
|
||||
import { render } from './my-render-function.js'
|
||||
|
||||
test('properly handles form inputs', async () => {
|
||||
@ -407,15 +407,15 @@ Besides rendering components and locating elements, you will also need to make a
|
||||
|
||||
```ts
|
||||
import { expect } from 'vitest'
|
||||
import { page } from '@vitest/browser/context'
|
||||
import { page } from 'vitest/browser'
|
||||
// element is rendered correctly
|
||||
await expect.element(page.getByText('Hello World')).toBeInTheDocument()
|
||||
```
|
||||
|
||||
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).
|
||||
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).
|
||||
|
||||
```ts
|
||||
import { page, userEvent } from '@vitest/browser/context'
|
||||
import { page, userEvent } from 'vitest/browser'
|
||||
await userEvent.fill(page.getByLabelText(/username/i), 'Alice')
|
||||
// or just locator.fill
|
||||
await page.getByLabelText(/username/i).fill('Alice')
|
||||
@ -532,7 +532,7 @@ For unsupported frameworks, we recommend using `testing-library` packages:
|
||||
You can also see more examples in [`browser-examples`](https://github.com/vitest-tests/browser-examples) repository.
|
||||
|
||||
::: warning
|
||||
`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.
|
||||
`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.
|
||||
:::
|
||||
|
||||
::: code-group
|
||||
|
||||
@ -7,7 +7,7 @@ title: Interactivity API | Browser Mode
|
||||
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.
|
||||
|
||||
```ts
|
||||
import { userEvent } from '@vitest/browser/context'
|
||||
import { userEvent } from 'vitest/browser'
|
||||
|
||||
await userEvent.click(document.querySelector('.button'))
|
||||
```
|
||||
@ -23,10 +23,10 @@ function setup(): UserEvent
|
||||
Creates a new user event instance. This is useful if you need to keep the state of keyboard to press and release buttons correctly.
|
||||
|
||||
::: warning
|
||||
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:
|
||||
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:
|
||||
|
||||
```ts
|
||||
import { userEvent as vitestUserEvent } from '@vitest/browser/context'
|
||||
import { userEvent as vitestUserEvent } from 'vitest/browser'
|
||||
import { userEvent as originalUserEvent } from '@testing-library/user-event'
|
||||
|
||||
await vitestUserEvent.keyboard('{Shift}') // press shift without releasing
|
||||
@ -51,7 +51,7 @@ function click(
|
||||
Click on an element. Inherits provider's options. Please refer to your provider's documentation for detailed explanation about how this method works.
|
||||
|
||||
```ts
|
||||
import { page, userEvent } from '@vitest/browser/context'
|
||||
import { page, userEvent } from 'vitest/browser'
|
||||
|
||||
test('clicks on an element', async () => {
|
||||
const logo = page.getByRole('img', { name: /logo/ })
|
||||
@ -82,7 +82,7 @@ Triggers a double click event on an element.
|
||||
Please refer to your provider's documentation for detailed explanation about how this method works.
|
||||
|
||||
```ts
|
||||
import { page, userEvent } from '@vitest/browser/context'
|
||||
import { page, userEvent } from 'vitest/browser'
|
||||
|
||||
test('triggers a double click on an element', async () => {
|
||||
const logo = page.getByRole('img', { name: /logo/ })
|
||||
@ -113,7 +113,7 @@ Triggers a triple click event on an element. Since there is no `tripleclick` in
|
||||
Please refer to your provider's documentation for detailed explanation about how this method works.
|
||||
|
||||
```ts
|
||||
import { page, userEvent } from '@vitest/browser/context'
|
||||
import { page, userEvent } from 'vitest/browser'
|
||||
|
||||
test('triggers a triple click on an element', async () => {
|
||||
const logo = page.getByRole('img', { name: /logo/ })
|
||||
@ -150,7 +150,7 @@ function fill(
|
||||
Set a value to the `input`/`textarea`/`contenteditable` field. This will remove any existing text in the input before setting the new value.
|
||||
|
||||
```ts
|
||||
import { page, userEvent } from '@vitest/browser/context'
|
||||
import { page, userEvent } from 'vitest/browser'
|
||||
|
||||
test('update input', async () => {
|
||||
const input = page.getByRole('input')
|
||||
@ -189,7 +189,7 @@ The `userEvent.keyboard` allows you to trigger keyboard strokes. If any input ha
|
||||
This API supports [user-event `keyboard` syntax](https://testing-library.com/docs/user-event/keyboard).
|
||||
|
||||
```ts
|
||||
import { userEvent } from '@vitest/browser/context'
|
||||
import { userEvent } from 'vitest/browser'
|
||||
|
||||
test('trigger keystrokes', async () => {
|
||||
await userEvent.keyboard('foo') // translates to: f, o, o
|
||||
@ -215,7 +215,7 @@ function tab(options?: UserEventTabOptions): Promise<void>
|
||||
Sends a `Tab` key event. This is a shorthand for `userEvent.keyboard('{tab}')`.
|
||||
|
||||
```ts
|
||||
import { page, userEvent } from '@vitest/browser/context'
|
||||
import { page, userEvent } from 'vitest/browser'
|
||||
|
||||
test('tab works', async () => {
|
||||
const [input1, input2] = page.getByRole('input').elements()
|
||||
@ -259,7 +259,7 @@ This function allows you to type characters into an `input`/`textarea`/`contente
|
||||
If you just need to press characters without an input, use [`userEvent.keyboard`](#userevent-keyboard) API.
|
||||
|
||||
```ts
|
||||
import { page, userEvent } from '@vitest/browser/context'
|
||||
import { page, userEvent } from 'vitest/browser'
|
||||
|
||||
test('update input', async () => {
|
||||
const input = page.getByRole('input')
|
||||
@ -289,7 +289,7 @@ function clear(element: Element | Locator, options?: UserEventClearOptions): Pro
|
||||
This method clears the input element content.
|
||||
|
||||
```ts
|
||||
import { page, userEvent } from '@vitest/browser/context'
|
||||
import { page, userEvent } from 'vitest/browser'
|
||||
|
||||
test('clears input', async () => {
|
||||
const input = page.getByRole('input')
|
||||
@ -336,7 +336,7 @@ Unlike `@testing-library`, Vitest doesn't support [listbox](https://developer.mo
|
||||
:::
|
||||
|
||||
```ts
|
||||
import { page, userEvent } from '@vitest/browser/context'
|
||||
import { page, userEvent } from 'vitest/browser'
|
||||
|
||||
test('clears input', async () => {
|
||||
const select = page.getByRole('select')
|
||||
@ -386,7 +386,7 @@ If you are using `playwright` provider, the cursor moves to "some" visible point
|
||||
:::
|
||||
|
||||
```ts
|
||||
import { page, userEvent } from '@vitest/browser/context'
|
||||
import { page, userEvent } from 'vitest/browser'
|
||||
|
||||
test('hovers logo element', async () => {
|
||||
const logo = page.getByRole('img', { name: /logo/ })
|
||||
@ -419,7 +419,7 @@ By default, the cursor position is in "some" visible place (in `playwright` prov
|
||||
:::
|
||||
|
||||
```ts
|
||||
import { page, userEvent } from '@vitest/browser/context'
|
||||
import { page, userEvent } from 'vitest/browser'
|
||||
|
||||
test('unhover logo element', async () => {
|
||||
const logo = page.getByRole('img', { name: /logo/ })
|
||||
@ -449,7 +449,7 @@ function upload(
|
||||
Change a file input element to have the specified files.
|
||||
|
||||
```ts
|
||||
import { page, userEvent } from '@vitest/browser/context'
|
||||
import { page, userEvent } from 'vitest/browser'
|
||||
|
||||
test('can upload a file', async () => {
|
||||
const input = page.getByRole('button', { name: /Upload files/ })
|
||||
@ -488,7 +488,7 @@ function dragAndDrop(
|
||||
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`.
|
||||
|
||||
```ts
|
||||
import { page, userEvent } from '@vitest/browser/context'
|
||||
import { page, userEvent } from 'vitest/browser'
|
||||
|
||||
test('drag and drop works', async () => {
|
||||
const source = page.getByRole('img', { name: /logo/ })
|
||||
@ -520,7 +520,7 @@ function copy(): Promise<void>
|
||||
Copy the selected text to the clipboard.
|
||||
|
||||
```js
|
||||
import { page, userEvent } from '@vitest/browser/context'
|
||||
import { page, userEvent } from 'vitest/browser'
|
||||
|
||||
test('copy and paste', async () => {
|
||||
// write to 'source'
|
||||
@ -553,7 +553,7 @@ function cut(): Promise<void>
|
||||
Cut the selected text to the clipboard.
|
||||
|
||||
```js
|
||||
import { page, userEvent } from '@vitest/browser/context'
|
||||
import { page, userEvent } from 'vitest/browser'
|
||||
|
||||
test('copy and paste', async () => {
|
||||
// write to 'source'
|
||||
|
||||
@ -609,7 +609,7 @@ function click(options?: UserEventClickOptions): Promise<void>
|
||||
Click on an element. You can use the options to set the cursor position.
|
||||
|
||||
```ts
|
||||
import { page } from '@vitest/browser/context'
|
||||
import { page } from 'vitest/browser'
|
||||
|
||||
await page.getByRole('img', { name: 'Rose' }).click()
|
||||
```
|
||||
@ -625,7 +625,7 @@ function dblClick(options?: UserEventDoubleClickOptions): Promise<void>
|
||||
Triggers a double click event on an element. You can use the options to set the cursor position.
|
||||
|
||||
```ts
|
||||
import { page } from '@vitest/browser/context'
|
||||
import { page } from 'vitest/browser'
|
||||
|
||||
await page.getByRole('img', { name: 'Rose' }).dblClick()
|
||||
```
|
||||
@ -641,7 +641,7 @@ function tripleClick(options?: UserEventTripleClickOptions): Promise<void>
|
||||
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.
|
||||
|
||||
```ts
|
||||
import { page } from '@vitest/browser/context'
|
||||
import { page } from 'vitest/browser'
|
||||
|
||||
await page.getByRole('img', { name: 'Rose' }).tripleClick()
|
||||
```
|
||||
@ -657,7 +657,7 @@ function clear(options?: UserEventClearOptions): Promise<void>
|
||||
Clears the input element content.
|
||||
|
||||
```ts
|
||||
import { page } from '@vitest/browser/context'
|
||||
import { page } from 'vitest/browser'
|
||||
|
||||
await page.getByRole('textbox', { name: 'Full Name' }).clear()
|
||||
```
|
||||
@ -673,7 +673,7 @@ function hover(options?: UserEventHoverOptions): Promise<void>
|
||||
Moves the cursor position to the selected element.
|
||||
|
||||
```ts
|
||||
import { page } from '@vitest/browser/context'
|
||||
import { page } from 'vitest/browser'
|
||||
|
||||
await page.getByRole('img', { name: 'Rose' }).hover()
|
||||
```
|
||||
@ -689,7 +689,7 @@ function unhover(options?: UserEventHoverOptions): Promise<void>
|
||||
This works the same as [`locator.hover`](#hover), but moves the cursor to the `document.body` element instead.
|
||||
|
||||
```ts
|
||||
import { page } from '@vitest/browser/context'
|
||||
import { page } from 'vitest/browser'
|
||||
|
||||
await page.getByRole('img', { name: 'Rose' }).unhover()
|
||||
```
|
||||
@ -705,7 +705,7 @@ function fill(text: string, options?: UserEventFillOptions): Promise<void>
|
||||
Sets the value of the current `input`, `textarea` or `contenteditable` element.
|
||||
|
||||
```ts
|
||||
import { page } from '@vitest/browser/context'
|
||||
import { page } from 'vitest/browser'
|
||||
|
||||
await page.getByRole('input', { name: 'Full Name' }).fill('Mr. Bean')
|
||||
```
|
||||
@ -724,7 +724,7 @@ function dropTo(
|
||||
Drags the current element to the target location.
|
||||
|
||||
```ts
|
||||
import { page } from '@vitest/browser/context'
|
||||
import { page } from 'vitest/browser'
|
||||
|
||||
const paris = page.getByText('Paris')
|
||||
const france = page.getByText('France')
|
||||
@ -752,7 +752,7 @@ function selectOptions(
|
||||
Choose one or more values from a `<select>` element.
|
||||
|
||||
```ts
|
||||
import { page } from '@vitest/browser/context'
|
||||
import { page } from 'vitest/browser'
|
||||
|
||||
const languages = page.getByRole('select', { name: 'Languages' })
|
||||
|
||||
@ -784,7 +784,7 @@ You can specify the save location for the screenshot using the `path` option, wh
|
||||
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.
|
||||
|
||||
```ts
|
||||
import { page } from '@vitest/browser/context'
|
||||
import { page } from 'vitest/browser'
|
||||
|
||||
const button = page.getByRole('button', { name: 'Click Me!' })
|
||||
|
||||
@ -946,7 +946,7 @@ const test: BrowserCommand<string> = function test(context, selector) {
|
||||
|
||||
```ts [example.test.ts]
|
||||
import { test } from 'vitest'
|
||||
import { commands, page } from '@vitest/browser/context'
|
||||
import { commands, page } from 'vitest/browser'
|
||||
|
||||
test('works correctly', async () => {
|
||||
await commands.test(page.getByText('Hello').selector) // ✅
|
||||
@ -988,7 +988,7 @@ The selector syntax is identical to Playwright locators. Please, read [their gui
|
||||
:::
|
||||
|
||||
```ts
|
||||
import { locators } from '@vitest/browser/context'
|
||||
import { locators } from 'vitest/browser'
|
||||
|
||||
locators.extend({
|
||||
getByArticleTitle(title) {
|
||||
@ -1010,7 +1010,7 @@ locators.extend({
|
||||
|
||||
// if you are using typescript, you can extend LocatorSelectors interface
|
||||
// to have the autocompletion in locators.extend, page.* and locator.* methods
|
||||
declare module '@vitest/browser/context' {
|
||||
declare module 'vitest/browser' {
|
||||
interface LocatorSelectors {
|
||||
// if the custom method returns a string, it will be converted into a locator
|
||||
// if it returns anything else, then it will be returned as usual
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
# Multiple Setups
|
||||
|
||||
Since Vitest 3, you can specify several different browser setups using the new [`browser.instances`](/guide/browser/config#browser-instances) option.
|
||||
You can specify several different browser setups using the [`browser.instances`](/guide/browser/config#browser-instances) option.
|
||||
|
||||
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.
|
||||
|
||||
@ -10,7 +10,7 @@ You can use the `browser.instances` field to specify options for different brows
|
||||
|
||||
```ts [vitest.config.ts]
|
||||
import { defineConfig } from 'vitest/config'
|
||||
import { playwright } from '@vitest/browser/providers/playwright'
|
||||
import { playwright } from '@vitest/browser-playwright'
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
@ -35,7 +35,7 @@ You can also specify different config options independently from the browser (al
|
||||
::: code-group
|
||||
```ts [vitest.config.ts]
|
||||
import { defineConfig } from 'vitest/config'
|
||||
import { playwright } from '@vitest/browser/providers/playwright'
|
||||
import { playwright } from '@vitest/browser-playwright'
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
@ -50,14 +50,14 @@ export default defineConfig({
|
||||
setupFiles: ['./ratio-setup.ts'],
|
||||
provide: {
|
||||
ratio: 1,
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
browser: 'chromium',
|
||||
name: 'chromium-2',
|
||||
provide: {
|
||||
ratio: 2,
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
@ -119,20 +119,3 @@ export default defineConfig({
|
||||
})
|
||||
```
|
||||
:::
|
||||
|
||||
::: warning
|
||||
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:
|
||||
|
||||
```shell
|
||||
? Found multiple projects that run browser tests in headed mode: "chromium", "firefox".
|
||||
Vitest cannot run multiple headed browsers at the same time. 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. › - Use arrow-keys. Return to submit.
|
||||
❯ chromium
|
||||
firefox
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
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`.
|
||||
:::
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
# Configuring Playwright
|
||||
|
||||
To run tests using playwright, you need to specify it in the `test.browser.provider` property in your config:
|
||||
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:
|
||||
|
||||
```ts [vitest.config.js]
|
||||
import { playwright } from '@vitest/browser/providers/playwright'
|
||||
import { playwright } from '@vitest/browser-playwright'
|
||||
import { defineConfig } from 'vitest/config'
|
||||
|
||||
export default defineConfig({
|
||||
@ -16,10 +16,10 @@ export default defineConfig({
|
||||
})
|
||||
```
|
||||
|
||||
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:
|
||||
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:
|
||||
|
||||
```ts{7-14,21-26} [vitest.config.js]
|
||||
import { playwright } from '@vitest/browser/providers/playwright'
|
||||
import { playwright } from '@vitest/browser-playwright'
|
||||
import { defineConfig } from 'vitest/config'
|
||||
|
||||
export default defineConfig({
|
||||
@ -53,6 +53,10 @@ export default defineConfig({
|
||||
})
|
||||
```
|
||||
|
||||
::: warning
|
||||
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.
|
||||
:::
|
||||
|
||||
## launchOptions
|
||||
|
||||
These options are directly passed down to `playwright[browser].launch` command. You can read more about the command and available arguments in the [Playwright documentation](https://playwright.dev/docs/api/class-browsertype#browser-type-launch).
|
||||
@ -94,7 +98,7 @@ This value configures the default timeout it takes for Playwright to wait until
|
||||
You can also configure the action timeout per-action:
|
||||
|
||||
```ts
|
||||
import { page, userEvent } from '@vitest/browser/context'
|
||||
import { page, userEvent } from 'vitest/browser'
|
||||
|
||||
await userEvent.click(page.getByRole('button'), {
|
||||
timeout: 1_000,
|
||||
|
||||
32
docs/guide/browser/preview.md
Normal file
32
docs/guide/browser/preview.md
Normal file
@ -0,0 +1,32 @@
|
||||
# Configuring Preview
|
||||
|
||||
::: warning
|
||||
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).
|
||||
:::
|
||||
|
||||
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:
|
||||
|
||||
```ts [vitest.config.js]
|
||||
import { preview } from '@vitest/browser-preview'
|
||||
import { defineConfig } from 'vitest/config'
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
browser: {
|
||||
provider: preview(),
|
||||
instances: [{ browser: 'chromium' }]
|
||||
},
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
## Differences with Other Providers
|
||||
|
||||
The preview provider has some limitations compared to other providers like [Playwright](/guide/browser/playwright) or [WebdriverIO](/guide/browser/webdriverio):
|
||||
|
||||
- It does not support headless mode; the browser window will always be visible.
|
||||
- It does not support multiple instances of the same browser; each instance must use a different browser.
|
||||
- It does not support advanced browser capabilities or options; you can only specify the browser name.
|
||||
- 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.
|
||||
@ -9,7 +9,7 @@ Generating trace files is only available when using the [Playwright provider](/g
|
||||
::: code-group
|
||||
```ts [vitest.config.js]
|
||||
import { defineConfig } from 'vitest/config'
|
||||
import { playwright } from '@vitest/browser/providers/playwright'
|
||||
import { playwright } from '@vitest/browser-playwright'
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
@ -39,7 +39,7 @@ To change the output directory, you can set the `tracesDir` option in the `test.
|
||||
|
||||
```ts [vitest.config.js]
|
||||
import { defineConfig } from 'vitest/config'
|
||||
import { playwright } from '@vitest/browser/providers/playwright'
|
||||
import { playwright } from '@vitest/browser-playwright'
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
|
||||
@ -54,7 +54,7 @@ Visual regression testing in Vitest can be done through the
|
||||
|
||||
```ts
|
||||
import { expect, test } from 'vitest'
|
||||
import { page } from '@vitest/browser/context'
|
||||
import { page } from 'vitest/browser'
|
||||
|
||||
test('hero section looks correct', async () => {
|
||||
// ...the rest of the test
|
||||
@ -239,7 +239,7 @@ await page.viewport(1280, 720)
|
||||
```
|
||||
|
||||
```ts [vitest.config.ts]
|
||||
import { playwright } from '@vitest/browser/providers/playwright'
|
||||
import { playwright } from '@vitest/browser-playwright'
|
||||
import { defineConfig } from 'vitest/config'
|
||||
|
||||
export default defineConfig({
|
||||
@ -592,7 +592,7 @@ The cleanest approach is using [Test Projects](/guide/projects):
|
||||
```ts [vitest.config.ts]
|
||||
import { env } from 'node:process'
|
||||
import { defineConfig } from 'vitest/config'
|
||||
import { playwright } from '@vitest/browser/providers/playwright'
|
||||
import { playwright } from '@vitest/browser-playwright'
|
||||
|
||||
export default defineConfig({
|
||||
// ...global Vite config
|
||||
|
||||
@ -4,10 +4,10 @@
|
||||
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.
|
||||
:::
|
||||
|
||||
To run tests using WebdriverIO, you need to specify it in the `test.browser.provider` property in your config:
|
||||
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:
|
||||
|
||||
```ts [vitest.config.js]
|
||||
import { webdriverio } from '@vitest/browser/providers/webdriverio'
|
||||
import { webdriverio } from '@vitest/browser-webdriverio'
|
||||
import { defineConfig } from 'vitest/config'
|
||||
|
||||
export default defineConfig({
|
||||
@ -20,10 +20,10 @@ export default defineConfig({
|
||||
})
|
||||
```
|
||||
|
||||
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:
|
||||
You can configure all the parameters that [`remote`](https://webdriver.io/docs/api/modules/#remoteoptions-modifier) function accepts:
|
||||
|
||||
```ts{8-12,19-23} [vitest.config.js]
|
||||
import { webdriverio } from '@vitest/browser/providers/webdriverio'
|
||||
```ts{8-12,19-25} [vitest.config.js]
|
||||
import { webdriverio } from '@vitest/browser-webdriverio'
|
||||
import { defineConfig } from 'vitest/config'
|
||||
|
||||
export default defineConfig({
|
||||
@ -42,11 +42,13 @@ export default defineConfig({
|
||||
// overriding options only for a single instance
|
||||
// this will NOT merge options with the parent one
|
||||
provider: webdriverio({
|
||||
'moz:firefoxOptions': {
|
||||
args: ['--disable-gpu'],
|
||||
capabilities: {
|
||||
'moz:firefoxOptions': {
|
||||
args: ['--disable-gpu'],
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
@ -58,5 +60,5 @@ You can find most available options in the [WebdriverIO documentation](https://w
|
||||
::: tip
|
||||
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.
|
||||
|
||||
Note that Vitest will ignore `capabilities.browserName`. Use [`test.browser.instances.browser`](/guide/browser/config#browser-capabilities-name) instead.
|
||||
Note that Vitest will ignore `capabilities.browserName` — use [`test.browser.instances.browser`](/guide/browser/config#browser-capabilities-name) instead.
|
||||
:::
|
||||
|
||||
@ -52,7 +52,7 @@ vitest --inspect-brk --browser --no-file-parallelism
|
||||
```
|
||||
```ts [vitest.config.js]
|
||||
import { defineConfig } from 'vitest/config'
|
||||
import { playwright } from '@vitest/browser/providers/playwright'
|
||||
import { playwright } from '@vitest/browser-playwright'
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
|
||||
@ -215,12 +215,12 @@ export default defineWorkspace([ // [!code --]
|
||||
```
|
||||
:::
|
||||
|
||||
### Browser Provider Accepts an Object
|
||||
### Browser Provider Rework
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
```ts
|
||||
import { playwright } from '@vitest/browser/providers/playwright' // [!code ++]
|
||||
import { playwright } from '@vitest/browser-playwright' // [!code ++]
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
@ -246,6 +246,31 @@ export default defineConfig({
|
||||
|
||||
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.
|
||||
|
||||
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`:
|
||||
|
||||
```ts
|
||||
import { page } from '@vitest/browser/context' // [!code --]
|
||||
import { page } from 'vitest/browser' // [!code ++]
|
||||
|
||||
test('example', async () => {
|
||||
await page.getByRole('button').click()
|
||||
})
|
||||
```
|
||||
|
||||
The modules are identical, so doing a simple "Find and Replace" should be sufficient.
|
||||
|
||||
If you were using the `@vitest/browser/utils` module, you can now import those utilities from `vitest/browser` as well:
|
||||
|
||||
```ts
|
||||
import { getElementError } from '@vitest/browser/utils' // [!code --]
|
||||
import { utils } from 'vitest/browser' // [!code ++]
|
||||
const { getElementError } = utils // [!code ++]
|
||||
```
|
||||
|
||||
::: warning
|
||||
Both `@vitest/browser/context` and `@vitest/browser/utils` work at runtime during the transition period, but they will be removed in a future release.
|
||||
:::
|
||||
|
||||
### Reporter Updates
|
||||
|
||||
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,7 +302,7 @@ export default defineConfig({
|
||||
|
||||
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`.
|
||||
|
||||
```js
|
||||
```js{15-22}
|
||||
// before Vite 4.0
|
||||
exports[`custom element with shadow root 1`] = `
|
||||
"<body>
|
||||
|
||||
@ -89,7 +89,7 @@ export default antfu(
|
||||
},
|
||||
{
|
||||
// these files define vitest as peer dependency
|
||||
files: [`packages/{coverage-*,ui,browser,web-worker}/${GLOB_SRC}`],
|
||||
files: [`packages/{coverage-*,ui,browser,web-worker,browser-*}/${GLOB_SRC}`],
|
||||
rules: {
|
||||
'no-restricted-imports': [
|
||||
'error',
|
||||
|
||||
@ -17,7 +17,7 @@
|
||||
"lit": "^3.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitest/browser": "latest",
|
||||
"@vitest/browser-playwright": "latest",
|
||||
"jsdom": "latest",
|
||||
"playwright": "^1.55.0",
|
||||
"vite": "latest",
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { page } from '@vitest/browser/context'
|
||||
import { beforeEach, describe, expect, it } from 'vitest'
|
||||
import { page } from 'vitest/browser'
|
||||
|
||||
import '../src/my-button.js'
|
||||
|
||||
|
||||
@ -5,7 +5,6 @@
|
||||
"experimentalDecorators": true,
|
||||
"module": "node16",
|
||||
"moduleResolution": "Node16",
|
||||
"types": ["@vitest/browser/providers/playwright"],
|
||||
"verbatimModuleSyntax": true
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
/// <reference types="vitest/config" />
|
||||
|
||||
import { playwright } from '@vitest/browser/providers/playwright'
|
||||
import { playwright } from '@vitest/browser-playwright'
|
||||
import { defineConfig } from 'vite'
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
|
||||
@ -71,6 +71,9 @@
|
||||
"pnpm": {
|
||||
"overrides": {
|
||||
"@vitest/browser": "workspace:*",
|
||||
"@vitest/browser-playwright": "workspace:*",
|
||||
"@vitest/browser-preview": "workspace:*",
|
||||
"@vitest/browser-webdriverio": "workspace:*",
|
||||
"@vitest/ui": "workspace:*",
|
||||
"acorn": "8.11.3",
|
||||
"mlly": "^1.8.0",
|
||||
|
||||
48
packages/browser-playwright/README.md
Normal file
48
packages/browser-playwright/README.md
Normal file
@ -0,0 +1,48 @@
|
||||
# @vitest/browser-playwright
|
||||
|
||||
[](https://www.npmjs.com/package/@vitest/browser-playwright)
|
||||
|
||||
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.
|
||||
|
||||
We recommend using this package if you are already using playwright in your project or if you do not have any E2E tests yet.
|
||||
|
||||
## Installation
|
||||
|
||||
Install the package with your favorite package manager:
|
||||
|
||||
```sh
|
||||
npm install -D @vitest/browser-playwright
|
||||
# or
|
||||
yarn add -D @vitest/browser-playwright
|
||||
# or
|
||||
pnpm add -D @vitest/browser-playwright
|
||||
```
|
||||
|
||||
Then specify it in the `browser.provider` field of your Vitest configuration:
|
||||
|
||||
```ts
|
||||
// vitest.config.ts
|
||||
import { defineConfig } from 'vitest/config'
|
||||
import { playwright } from '@vitest/browser-playwright'
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
browser: {
|
||||
provider: playwright({
|
||||
// ...custom playwright options
|
||||
}),
|
||||
instances: [
|
||||
{ name: 'chromium' },
|
||||
],
|
||||
},
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
Then run Vitest in the browser mode:
|
||||
|
||||
```sh
|
||||
npx vitest --browser
|
||||
```
|
||||
|
||||
[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
vendored
Normal file
1
packages/browser-playwright/context.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
export * from '@vitest/browser/context'
|
||||
58
packages/browser-playwright/package.json
Normal file
58
packages/browser-playwright/package.json
Normal file
@ -0,0 +1,58 @@
|
||||
{
|
||||
"name": "@vitest/browser-playwright",
|
||||
"type": "module",
|
||||
"version": "4.0.0-beta.13",
|
||||
"description": "Browser running for Vitest using playwright",
|
||||
"license": "MIT",
|
||||
"funding": "https://opencollective.com/vitest",
|
||||
"homepage": "https://vitest.dev/guide/browser/playwright",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/vitest-dev/vitest.git",
|
||||
"directory": "packages/browser-playwright"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/vitest-dev/vitest/issues"
|
||||
},
|
||||
"sideEffects": false,
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"default": "./dist/index.js"
|
||||
},
|
||||
"./context": {
|
||||
"types": "./context.d.ts"
|
||||
},
|
||||
"./package.json": "./package.json"
|
||||
},
|
||||
"main": "./dist/index.js",
|
||||
"module": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"files": [
|
||||
"context.d.ts",
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "premove dist && pnpm rollup -c",
|
||||
"dev": "rollup -c --watch --watch.include 'src/**'"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"playwright": "*",
|
||||
"vitest": "workspace:*"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"playwright": {
|
||||
"optional": false
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@vitest/browser": "workspace:*",
|
||||
"@vitest/mocker": "workspace:*",
|
||||
"tinyrainbow": "catalog:"
|
||||
},
|
||||
"devDependencies": {
|
||||
"playwright": "^1.55.0",
|
||||
"playwright-core": "^1.55.0",
|
||||
"vitest": "workspace:*"
|
||||
}
|
||||
}
|
||||
62
packages/browser-playwright/rollup.config.js
Normal file
62
packages/browser-playwright/rollup.config.js
Normal file
@ -0,0 +1,62 @@
|
||||
import { createRequire } from 'node:module'
|
||||
import commonjs from '@rollup/plugin-commonjs'
|
||||
import json from '@rollup/plugin-json'
|
||||
import resolve from '@rollup/plugin-node-resolve'
|
||||
import { defineConfig } from 'rollup'
|
||||
import oxc from 'unplugin-oxc/rollup'
|
||||
import { createDtsUtils } from '../../scripts/build-utils.js'
|
||||
|
||||
const require = createRequire(import.meta.url)
|
||||
const pkg = require('./package.json')
|
||||
|
||||
const external = [
|
||||
...Object.keys(pkg.dependencies),
|
||||
...Object.keys(pkg.peerDependencies || {}),
|
||||
/^@?vitest(\/|$)/,
|
||||
'vite',
|
||||
'playwright-core/types/protocol',
|
||||
]
|
||||
|
||||
const dtsUtils = createDtsUtils()
|
||||
|
||||
const plugins = [
|
||||
resolve({
|
||||
preferBuiltins: true,
|
||||
}),
|
||||
json(),
|
||||
commonjs(),
|
||||
oxc({
|
||||
transform: { target: 'node18' },
|
||||
}),
|
||||
]
|
||||
|
||||
export default () =>
|
||||
defineConfig([
|
||||
{
|
||||
input: {
|
||||
index: './src/index.ts',
|
||||
locators: './src/locators.ts',
|
||||
},
|
||||
output: {
|
||||
dir: 'dist',
|
||||
format: 'esm',
|
||||
},
|
||||
external,
|
||||
context: 'null',
|
||||
plugins: [
|
||||
...dtsUtils.isolatedDecl(),
|
||||
...plugins,
|
||||
],
|
||||
},
|
||||
{
|
||||
input: dtsUtils.dtsInput('src/index.ts'),
|
||||
output: {
|
||||
dir: 'dist',
|
||||
entryFileNames: '[name].d.ts',
|
||||
format: 'esm',
|
||||
},
|
||||
watch: false,
|
||||
external,
|
||||
plugins: dtsUtils.dts(),
|
||||
},
|
||||
])
|
||||
11
packages/browser-playwright/src/commands/clear.ts
Normal file
11
packages/browser-playwright/src/commands/clear.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import type { UserEvent } from 'vitest/browser'
|
||||
import type { UserEventCommand } from './utils'
|
||||
|
||||
export const clear: UserEventCommand<UserEvent['clear']> = async (
|
||||
context,
|
||||
selector,
|
||||
) => {
|
||||
const { iframe } = context
|
||||
const element = iframe.locator(selector)
|
||||
await element.clear()
|
||||
}
|
||||
32
packages/browser-playwright/src/commands/click.ts
Normal file
32
packages/browser-playwright/src/commands/click.ts
Normal file
@ -0,0 +1,32 @@
|
||||
import type { UserEvent } from 'vitest/browser'
|
||||
import type { UserEventCommand } from './utils'
|
||||
|
||||
export const click: UserEventCommand<UserEvent['click']> = async (
|
||||
context,
|
||||
selector,
|
||||
options = {},
|
||||
) => {
|
||||
const tester = context.iframe
|
||||
await tester.locator(selector).click(options)
|
||||
}
|
||||
|
||||
export const dblClick: UserEventCommand<UserEvent['dblClick']> = async (
|
||||
context,
|
||||
selector,
|
||||
options = {},
|
||||
) => {
|
||||
const tester = context.iframe
|
||||
await tester.locator(selector).dblclick(options)
|
||||
}
|
||||
|
||||
export const tripleClick: UserEventCommand<UserEvent['tripleClick']> = async (
|
||||
context,
|
||||
selector,
|
||||
options = {},
|
||||
) => {
|
||||
const tester = context.iframe
|
||||
await tester.locator(selector).click({
|
||||
...options,
|
||||
clickCount: 3,
|
||||
})
|
||||
}
|
||||
16
packages/browser-playwright/src/commands/dragAndDrop.ts
Normal file
16
packages/browser-playwright/src/commands/dragAndDrop.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import type { UserEvent } from 'vitest/browser'
|
||||
import type { UserEventCommand } from './utils'
|
||||
|
||||
export const dragAndDrop: UserEventCommand<UserEvent['dragAndDrop']> = async (
|
||||
context,
|
||||
source,
|
||||
target,
|
||||
options_,
|
||||
) => {
|
||||
const frame = await context.frame()
|
||||
await frame.dragAndDrop(
|
||||
source,
|
||||
target,
|
||||
options_,
|
||||
)
|
||||
}
|
||||
13
packages/browser-playwright/src/commands/fill.ts
Normal file
13
packages/browser-playwright/src/commands/fill.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import type { UserEvent } from 'vitest/browser'
|
||||
import type { UserEventCommand } from './utils'
|
||||
|
||||
export const fill: UserEventCommand<UserEvent['fill']> = async (
|
||||
context,
|
||||
selector,
|
||||
text,
|
||||
options = {},
|
||||
) => {
|
||||
const { iframe } = context
|
||||
const element = iframe.locator(selector)
|
||||
await element.fill(text, options)
|
||||
}
|
||||
10
packages/browser-playwright/src/commands/hover.ts
Normal file
10
packages/browser-playwright/src/commands/hover.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import type { UserEvent } from 'vitest/browser'
|
||||
import type { UserEventCommand } from './utils'
|
||||
|
||||
export const hover: UserEventCommand<UserEvent['hover']> = async (
|
||||
context,
|
||||
selector,
|
||||
options = {},
|
||||
) => {
|
||||
await context.iframe.locator(selector).hover(options)
|
||||
}
|
||||
40
packages/browser-playwright/src/commands/index.ts
Normal file
40
packages/browser-playwright/src/commands/index.ts
Normal file
@ -0,0 +1,40 @@
|
||||
import { clear } from './clear'
|
||||
import { click, dblClick, tripleClick } from './click'
|
||||
import { dragAndDrop } from './dragAndDrop'
|
||||
import { fill } from './fill'
|
||||
import { hover } from './hover'
|
||||
import { keyboard, keyboardCleanup } from './keyboard'
|
||||
import { takeScreenshot } from './screenshot'
|
||||
import { selectOptions } from './select'
|
||||
import { tab } from './tab'
|
||||
import {
|
||||
annotateTraces,
|
||||
deleteTracing,
|
||||
startChunkTrace,
|
||||
startTracing,
|
||||
stopChunkTrace,
|
||||
} from './trace'
|
||||
import { type } from './type'
|
||||
import { upload } from './upload'
|
||||
|
||||
export default {
|
||||
__vitest_upload: upload as typeof upload,
|
||||
__vitest_click: click as typeof click,
|
||||
__vitest_dblClick: dblClick as typeof dblClick,
|
||||
__vitest_tripleClick: tripleClick as typeof tripleClick,
|
||||
__vitest_takeScreenshot: takeScreenshot as typeof takeScreenshot,
|
||||
__vitest_type: type as typeof type,
|
||||
__vitest_clear: clear as typeof clear,
|
||||
__vitest_fill: fill as typeof fill,
|
||||
__vitest_tab: tab as typeof tab,
|
||||
__vitest_keyboard: keyboard as typeof keyboard,
|
||||
__vitest_selectOptions: selectOptions as typeof selectOptions,
|
||||
__vitest_dragAndDrop: dragAndDrop as typeof dragAndDrop,
|
||||
__vitest_hover: hover as typeof hover,
|
||||
__vitest_cleanup: keyboardCleanup as typeof keyboardCleanup,
|
||||
__vitest_deleteTracing: deleteTracing as typeof deleteTracing,
|
||||
__vitest_startChunkTrace: startChunkTrace as typeof startChunkTrace,
|
||||
__vitest_startTracing: startTracing as typeof startTracing,
|
||||
__vitest_stopChunkTrace: stopChunkTrace as typeof stopChunkTrace,
|
||||
__vitest_annotateTraces: annotateTraces as typeof annotateTraces,
|
||||
}
|
||||
133
packages/browser-playwright/src/commands/keyboard.ts
Normal file
133
packages/browser-playwright/src/commands/keyboard.ts
Normal file
@ -0,0 +1,133 @@
|
||||
import type { BrowserProvider } from 'vitest/node'
|
||||
import type { PlaywrightBrowserProvider } from '../playwright'
|
||||
import type { UserEventCommand } from './utils'
|
||||
import { parseKeyDef } from '@vitest/browser'
|
||||
|
||||
export interface KeyboardState {
|
||||
unreleased: string[]
|
||||
}
|
||||
|
||||
export const keyboard: UserEventCommand<(text: string, state: KeyboardState) => Promise<{ unreleased: string[] }>> = async (
|
||||
context,
|
||||
text,
|
||||
state,
|
||||
) => {
|
||||
const frame = await context.frame()
|
||||
await frame.evaluate(focusIframe)
|
||||
|
||||
const pressed = new Set<string>(state.unreleased)
|
||||
|
||||
await keyboardImplementation(
|
||||
pressed,
|
||||
context.provider,
|
||||
context.sessionId,
|
||||
text,
|
||||
async () => {
|
||||
const frame = await context.frame()
|
||||
await frame.evaluate(selectAll)
|
||||
},
|
||||
true,
|
||||
)
|
||||
|
||||
return {
|
||||
unreleased: Array.from(pressed),
|
||||
}
|
||||
}
|
||||
|
||||
export const keyboardCleanup: UserEventCommand<(state: KeyboardState) => Promise<void>> = async (
|
||||
context,
|
||||
state,
|
||||
) => {
|
||||
const { provider, sessionId } = context
|
||||
if (!state.unreleased) {
|
||||
return
|
||||
}
|
||||
const page = (provider as PlaywrightBrowserProvider).getPage(sessionId)
|
||||
for (const key of state.unreleased) {
|
||||
await page.keyboard.up(key)
|
||||
}
|
||||
}
|
||||
|
||||
// fallback to insertText for non US key
|
||||
// https://github.com/microsoft/playwright/blob/50775698ae13642742f2a1e8983d1d686d7f192d/packages/playwright-core/src/server/input.ts#L95
|
||||
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'])
|
||||
|
||||
export async function keyboardImplementation(
|
||||
pressed: Set<string>,
|
||||
provider: BrowserProvider,
|
||||
sessionId: string,
|
||||
text: string,
|
||||
selectAll: () => Promise<void>,
|
||||
skipRelease: boolean,
|
||||
): Promise<{ pressed: Set<string> }> {
|
||||
const page = (provider as PlaywrightBrowserProvider).getPage(sessionId)
|
||||
const actions = parseKeyDef(text)
|
||||
|
||||
for (const { releasePrevious, releaseSelf, repeat, keyDef } of actions) {
|
||||
const key = keyDef.key!
|
||||
|
||||
// TODO: instead of calling down/up for each key, join non special
|
||||
// together, and call `type` once for all non special keys,
|
||||
// and then `press` for special keys
|
||||
if (pressed.has(key)) {
|
||||
if (VALID_KEYS.has(key)) {
|
||||
await page.keyboard.up(key)
|
||||
}
|
||||
pressed.delete(key)
|
||||
}
|
||||
|
||||
if (!releasePrevious) {
|
||||
if (key === 'selectall') {
|
||||
await selectAll()
|
||||
continue
|
||||
}
|
||||
|
||||
for (let i = 1; i <= repeat; i++) {
|
||||
if (VALID_KEYS.has(key)) {
|
||||
await page.keyboard.down(key)
|
||||
}
|
||||
else {
|
||||
await page.keyboard.insertText(key)
|
||||
}
|
||||
}
|
||||
|
||||
if (releaseSelf) {
|
||||
if (VALID_KEYS.has(key)) {
|
||||
await page.keyboard.up(key)
|
||||
}
|
||||
}
|
||||
else {
|
||||
pressed.add(key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!skipRelease && pressed.size) {
|
||||
for (const key of pressed) {
|
||||
if (VALID_KEYS.has(key)) {
|
||||
await page.keyboard.up(key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
pressed,
|
||||
}
|
||||
}
|
||||
|
||||
function focusIframe() {
|
||||
if (
|
||||
!document.activeElement
|
||||
|| document.activeElement.ownerDocument !== document
|
||||
|| document.activeElement === document.body
|
||||
) {
|
||||
window.focus()
|
||||
}
|
||||
}
|
||||
|
||||
function selectAll() {
|
||||
const element = document.activeElement as HTMLInputElement
|
||||
if (element && typeof element.select === 'function') {
|
||||
element.select()
|
||||
}
|
||||
}
|
||||
63
packages/browser-playwright/src/commands/screenshot.ts
Normal file
63
packages/browser-playwright/src/commands/screenshot.ts
Normal file
@ -0,0 +1,63 @@
|
||||
import type { ScreenshotOptions } from 'vitest/browser'
|
||||
import type { BrowserCommandContext } from 'vitest/node'
|
||||
import { mkdir } from 'node:fs/promises'
|
||||
import { resolveScreenshotPath } from '@vitest/browser'
|
||||
import { dirname, normalize } from 'pathe'
|
||||
|
||||
interface ScreenshotCommandOptions extends Omit<ScreenshotOptions, 'element' | 'mask'> {
|
||||
element?: string
|
||||
mask?: readonly string[]
|
||||
}
|
||||
/**
|
||||
* Takes a screenshot using the provided browser context and returns a buffer and the expected screenshot path.
|
||||
*
|
||||
* **Note**: the returned `path` indicates where the screenshot *might* be found.
|
||||
* It is not guaranteed to exist, especially if `options.save` is `false`.
|
||||
*
|
||||
* @throws {Error} If the function is not called within a test or if the browser provider does not support screenshots.
|
||||
*/
|
||||
export async function takeScreenshot(
|
||||
context: BrowserCommandContext,
|
||||
name: string,
|
||||
options: Omit<ScreenshotCommandOptions, 'base64'>,
|
||||
): Promise<{ buffer: Buffer<ArrayBufferLike>; path: string }> {
|
||||
if (!context.testPath) {
|
||||
throw new Error(`Cannot take a screenshot without a test path`)
|
||||
}
|
||||
|
||||
const path = resolveScreenshotPath(
|
||||
context.testPath,
|
||||
name,
|
||||
context.project.config,
|
||||
options.path,
|
||||
)
|
||||
|
||||
// playwright does not need a screenshot path if we don't intend to save it
|
||||
let savePath: string | undefined
|
||||
|
||||
if (options.save) {
|
||||
savePath = normalize(path)
|
||||
|
||||
await mkdir(dirname(savePath), { recursive: true })
|
||||
}
|
||||
|
||||
const mask = options.mask?.map(selector => context.iframe.locator(selector))
|
||||
|
||||
if (options.element) {
|
||||
const { element: selector, ...config } = options
|
||||
const element = context.iframe.locator(selector)
|
||||
const buffer = await element.screenshot({
|
||||
...config,
|
||||
mask,
|
||||
path: savePath,
|
||||
})
|
||||
return { buffer, path }
|
||||
}
|
||||
|
||||
const buffer = await context.iframe.locator('body').screenshot({
|
||||
...options,
|
||||
mask,
|
||||
path: savePath,
|
||||
})
|
||||
return { buffer, path }
|
||||
}
|
||||
27
packages/browser-playwright/src/commands/select.ts
Normal file
27
packages/browser-playwright/src/commands/select.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import type { ElementHandle } from 'playwright'
|
||||
import type { UserEvent } from 'vitest/browser'
|
||||
import type { UserEventCommand } from './utils'
|
||||
|
||||
export const selectOptions: UserEventCommand<UserEvent['selectOptions']> = async (
|
||||
context,
|
||||
selector,
|
||||
userValues,
|
||||
options = {},
|
||||
) => {
|
||||
const value = userValues as any as (string | { element: string })[]
|
||||
const { iframe } = context
|
||||
const selectElement = iframe.locator(selector)
|
||||
|
||||
const values = await Promise.all(value.map(async (v) => {
|
||||
if (typeof v === 'string') {
|
||||
return v
|
||||
}
|
||||
const elementHandler = await iframe.locator(v.element).elementHandle()
|
||||
if (!elementHandler) {
|
||||
throw new Error(`Element not found: ${v.element}`)
|
||||
}
|
||||
return elementHandler
|
||||
})) as (readonly string[]) | (readonly ElementHandle[])
|
||||
|
||||
await selectElement.selectOption(values, options)
|
||||
}
|
||||
10
packages/browser-playwright/src/commands/tab.ts
Normal file
10
packages/browser-playwright/src/commands/tab.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import type { UserEvent } from 'vitest/browser'
|
||||
import type { UserEventCommand } from './utils'
|
||||
|
||||
export const tab: UserEventCommand<UserEvent['tab']> = async (
|
||||
context,
|
||||
options = {},
|
||||
) => {
|
||||
const page = context.page
|
||||
await page.keyboard.press(options.shift === true ? 'Shift+Tab' : 'Tab')
|
||||
}
|
||||
@ -1,11 +1,10 @@
|
||||
import type { BrowserCommandContext } from 'vitest/node'
|
||||
import type { BrowserCommand } from '../plugin'
|
||||
import type { BrowserCommand, BrowserCommandContext, BrowserProvider } from 'vitest/node'
|
||||
import type { PlaywrightBrowserProvider } from '../playwright'
|
||||
import { unlink } from 'node:fs/promises'
|
||||
import { basename, dirname, relative, resolve } from 'pathe'
|
||||
import { PlaywrightBrowserProvider } from '../providers/playwright'
|
||||
|
||||
export const startTracing: BrowserCommand<[]> = async ({ context, project, provider, sessionId }) => {
|
||||
if (provider instanceof PlaywrightBrowserProvider) {
|
||||
if (isPlaywrightProvider(provider)) {
|
||||
if (provider.tracingContexts.has(sessionId)) {
|
||||
return
|
||||
}
|
||||
@ -33,7 +32,7 @@ export const startChunkTrace: BrowserCommand<[{ name: string; title: string }]>
|
||||
if (!testPath) {
|
||||
throw new Error(`stopChunkTrace cannot be called outside of the test file.`)
|
||||
}
|
||||
if (provider instanceof PlaywrightBrowserProvider) {
|
||||
if (isPlaywrightProvider(provider)) {
|
||||
if (!provider.tracingContexts.has(sessionId)) {
|
||||
await startTracing(command)
|
||||
}
|
||||
@ -49,7 +48,7 @@ export const stopChunkTrace: BrowserCommand<[{ name: string }]> = async (
|
||||
context,
|
||||
{ name },
|
||||
) => {
|
||||
if (context.provider instanceof PlaywrightBrowserProvider) {
|
||||
if (isPlaywrightProvider(context.provider)) {
|
||||
const path = resolveTracesPath(context, name)
|
||||
context.provider.pendingTraces.delete(path)
|
||||
await context.context.tracing.stopChunk({ path })
|
||||
@ -84,7 +83,7 @@ export const deleteTracing: BrowserCommand<[{ traces: string[] }]> = async (
|
||||
if (!context.testPath) {
|
||||
throw new Error(`stopChunkTrace cannot be called outside of the test file.`)
|
||||
}
|
||||
if (context.provider instanceof PlaywrightBrowserProvider) {
|
||||
if (isPlaywrightProvider(context.provider)) {
|
||||
return Promise.all(
|
||||
traces.map(trace => unlink(trace).catch((err) => {
|
||||
if (err.code === 'ENOENT') {
|
||||
@ -124,3 +123,7 @@ export const annotateTraces: BrowserCommand<[{ traces: string[]; testId: string
|
||||
})
|
||||
}))
|
||||
}
|
||||
|
||||
function isPlaywrightProvider(provider: BrowserProvider): provider is PlaywrightBrowserProvider {
|
||||
return provider.name === 'playwright'
|
||||
}
|
||||
33
packages/browser-playwright/src/commands/type.ts
Normal file
33
packages/browser-playwright/src/commands/type.ts
Normal file
@ -0,0 +1,33 @@
|
||||
import type { UserEvent } from 'vitest/browser'
|
||||
import type { UserEventCommand } from './utils'
|
||||
import { keyboardImplementation } from './keyboard'
|
||||
|
||||
export const type: UserEventCommand<UserEvent['type']> = async (
|
||||
context,
|
||||
selector,
|
||||
text,
|
||||
options = {},
|
||||
) => {
|
||||
const { skipClick = false, skipAutoClose = false } = options
|
||||
const unreleased = new Set(Reflect.get(options, 'unreleased') as string[] ?? [])
|
||||
|
||||
const { iframe } = context
|
||||
const element = iframe.locator(selector)
|
||||
|
||||
if (!skipClick) {
|
||||
await element.focus()
|
||||
}
|
||||
|
||||
await keyboardImplementation(
|
||||
unreleased,
|
||||
context.provider,
|
||||
context.sessionId,
|
||||
text,
|
||||
() => element.selectText(),
|
||||
skipAutoClose,
|
||||
)
|
||||
|
||||
return {
|
||||
unreleased: Array.from(unreleased),
|
||||
}
|
||||
}
|
||||
33
packages/browser-playwright/src/commands/upload.ts
Normal file
33
packages/browser-playwright/src/commands/upload.ts
Normal file
@ -0,0 +1,33 @@
|
||||
import type { UserEventUploadOptions } from 'vitest/browser'
|
||||
import type { UserEventCommand } from './utils'
|
||||
import { resolve } from 'pathe'
|
||||
|
||||
export const upload: UserEventCommand<(element: string, files: Array<string | {
|
||||
name: string
|
||||
mimeType: string
|
||||
base64: string
|
||||
}>, options: UserEventUploadOptions) => void> = async (
|
||||
context,
|
||||
selector,
|
||||
files,
|
||||
options,
|
||||
) => {
|
||||
const testPath = context.testPath
|
||||
if (!testPath) {
|
||||
throw new Error(`Cannot upload files outside of a test`)
|
||||
}
|
||||
const root = context.project.config.root
|
||||
|
||||
const { iframe } = context
|
||||
const playwrightFiles = files.map((file) => {
|
||||
if (typeof file === 'string') {
|
||||
return resolve(root, file)
|
||||
}
|
||||
return {
|
||||
name: file.name,
|
||||
mimeType: file.mimeType,
|
||||
buffer: Buffer.from(file.base64, 'base64'),
|
||||
}
|
||||
})
|
||||
await iframe.locator(selector).setInputFiles(playwrightFiles as string[], options)
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
import type { Locator } from '@vitest/browser/context'
|
||||
import type { Locator } from 'vitest/browser'
|
||||
import type { BrowserCommand } from 'vitest/node'
|
||||
|
||||
export type UserEventCommand<T extends (...args: any) => any> = BrowserCommand<
|
||||
5
packages/browser-playwright/src/constants.ts
Normal file
5
packages/browser-playwright/src/constants.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { resolve } from 'pathe'
|
||||
|
||||
const pkgRoot = resolve(fileURLToPath(import.meta.url), '../..')
|
||||
export const distRoot: string = resolve(pkgRoot, 'dist')
|
||||
6
packages/browser-playwright/src/index.ts
Normal file
6
packages/browser-playwright/src/index.ts
Normal file
@ -0,0 +1,6 @@
|
||||
export {
|
||||
playwright,
|
||||
PlaywrightBrowserProvider,
|
||||
type PlaywrightProviderOptions,
|
||||
} from './playwright'
|
||||
export { defineBrowserCommand } from '@vitest/browser'
|
||||
@ -6,8 +6,7 @@ import type {
|
||||
UserEventHoverOptions,
|
||||
UserEventSelectOptions,
|
||||
UserEventUploadOptions,
|
||||
} from '@vitest/browser/context'
|
||||
import { page, server } from '@vitest/browser/context'
|
||||
} from 'vitest/browser'
|
||||
import {
|
||||
getByAltTextSelector,
|
||||
getByLabelSelector,
|
||||
@ -16,48 +15,13 @@ import {
|
||||
getByTestIdSelector,
|
||||
getByTextSelector,
|
||||
getByTitleSelector,
|
||||
} from 'ivya'
|
||||
import { getIframeScale, processTimeoutOptions } from '../utils'
|
||||
import { Locator, selectorEngine } from './index'
|
||||
|
||||
page.extend({
|
||||
getByLabelText(text, options) {
|
||||
return new PlaywrightLocator(getByLabelSelector(text, options))
|
||||
},
|
||||
getByRole(role, options) {
|
||||
return new PlaywrightLocator(getByRoleSelector(role, options))
|
||||
},
|
||||
getByTestId(testId) {
|
||||
return new PlaywrightLocator(getByTestIdSelector(server.config.browser.locators.testIdAttribute, testId))
|
||||
},
|
||||
getByAltText(text, options) {
|
||||
return new PlaywrightLocator(getByAltTextSelector(text, options))
|
||||
},
|
||||
getByPlaceholder(text, options) {
|
||||
return new PlaywrightLocator(getByPlaceholderSelector(text, options))
|
||||
},
|
||||
getByText(text, options) {
|
||||
return new PlaywrightLocator(getByTextSelector(text, options))
|
||||
},
|
||||
getByTitle(title, options) {
|
||||
return new PlaywrightLocator(getByTitleSelector(title, options))
|
||||
},
|
||||
|
||||
_createLocator(selector: string) {
|
||||
return new PlaywrightLocator(selector)
|
||||
},
|
||||
elementLocator(element: Element) {
|
||||
return new PlaywrightLocator(
|
||||
selectorEngine.generateSelectorSimple(element),
|
||||
element,
|
||||
)
|
||||
},
|
||||
frameLocator(locator: Locator) {
|
||||
return new PlaywrightLocator(
|
||||
`${locator.selector} >> internal:control=enter-frame`,
|
||||
)
|
||||
},
|
||||
})
|
||||
getIframeScale,
|
||||
Locator,
|
||||
processTimeoutOptions,
|
||||
selectorEngine,
|
||||
} from '@vitest/browser/locators'
|
||||
import { page, server } from 'vitest/browser'
|
||||
import { __INTERNAL } from 'vitest/internal/browser'
|
||||
|
||||
class PlaywrightLocator extends Locator {
|
||||
constructor(public selector: string, protected _container?: Element) {
|
||||
@ -120,42 +84,71 @@ class PlaywrightLocator extends Locator {
|
||||
}
|
||||
}
|
||||
|
||||
function processDragAndDropOptions(options_?: UserEventDragAndDropOptions) {
|
||||
if (!options_) {
|
||||
return options_
|
||||
page.extend({
|
||||
getByLabelText(text, options) {
|
||||
return new PlaywrightLocator(getByLabelSelector(text, options))
|
||||
},
|
||||
getByRole(role, options) {
|
||||
return new PlaywrightLocator(getByRoleSelector(role, options))
|
||||
},
|
||||
getByTestId(testId) {
|
||||
return new PlaywrightLocator(getByTestIdSelector(server.config.browser.locators.testIdAttribute, testId))
|
||||
},
|
||||
getByAltText(text, options) {
|
||||
return new PlaywrightLocator(getByAltTextSelector(text, options))
|
||||
},
|
||||
getByPlaceholder(text, options) {
|
||||
return new PlaywrightLocator(getByPlaceholderSelector(text, options))
|
||||
},
|
||||
getByText(text, options) {
|
||||
return new PlaywrightLocator(getByTextSelector(text, options))
|
||||
},
|
||||
getByTitle(title, options) {
|
||||
return new PlaywrightLocator(getByTitleSelector(title, options))
|
||||
},
|
||||
|
||||
elementLocator(element: Element) {
|
||||
return new PlaywrightLocator(
|
||||
selectorEngine.generateSelectorSimple(element),
|
||||
element,
|
||||
)
|
||||
},
|
||||
frameLocator(locator: Locator) {
|
||||
return new PlaywrightLocator(
|
||||
`${locator.selector} >> internal:control=enter-frame`,
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
__INTERNAL._createLocator = selector => new PlaywrightLocator(selector)
|
||||
|
||||
function processDragAndDropOptions(options?: UserEventDragAndDropOptions) {
|
||||
if (!options) {
|
||||
return options
|
||||
}
|
||||
const options = options_ as NonNullable<
|
||||
Parameters<import('playwright').Page['dragAndDrop']>[2]
|
||||
>
|
||||
if (options.sourcePosition) {
|
||||
options.sourcePosition = processPlaywrightPosition(options.sourcePosition)
|
||||
}
|
||||
if (options.targetPosition) {
|
||||
options.targetPosition = processPlaywrightPosition(options.targetPosition)
|
||||
}
|
||||
return options_
|
||||
return options
|
||||
}
|
||||
|
||||
function processHoverOptions(options_?: UserEventHoverOptions) {
|
||||
if (!options_) {
|
||||
return options_
|
||||
function processHoverOptions(options?: UserEventHoverOptions) {
|
||||
if (!options) {
|
||||
return options
|
||||
}
|
||||
const options = options_ as NonNullable<
|
||||
Parameters<import('playwright').Page['hover']>[1]
|
||||
>
|
||||
if (options.position) {
|
||||
options.position = processPlaywrightPosition(options.position)
|
||||
}
|
||||
return options_
|
||||
return options
|
||||
}
|
||||
|
||||
function processClickOptions(options_?: UserEventClickOptions) {
|
||||
if (!options_) {
|
||||
return options_
|
||||
function processClickOptions(options?: UserEventClickOptions) {
|
||||
if (!options) {
|
||||
return options
|
||||
}
|
||||
const options = options_ as NonNullable<
|
||||
Parameters<import('playwright').Page['click']>[1]
|
||||
>
|
||||
if (options.position) {
|
||||
options.position = processPlaywrightPosition(options.position)
|
||||
}
|
||||
@ -1,9 +1,5 @@
|
||||
/* eslint-disable ts/method-signature-style */
|
||||
|
||||
import type {
|
||||
ScreenshotComparatorRegistry,
|
||||
ScreenshotMatcherOptions,
|
||||
} from '@vitest/browser/context'
|
||||
import type { MockedModule } from '@vitest/mocker'
|
||||
import type {
|
||||
Browser,
|
||||
@ -19,15 +15,25 @@ import type { Protocol } from 'playwright-core/types/protocol'
|
||||
import type { SourceMap } from 'rollup'
|
||||
import type { ResolvedConfig } from 'vite'
|
||||
import type {
|
||||
Locator,
|
||||
ScreenshotComparatorRegistry,
|
||||
ScreenshotMatcherOptions,
|
||||
} from 'vitest/browser'
|
||||
import type {
|
||||
BrowserCommand,
|
||||
BrowserModuleMocker,
|
||||
BrowserProvider,
|
||||
BrowserProviderOption,
|
||||
CDPSession,
|
||||
TestProject,
|
||||
} from 'vitest/node'
|
||||
import { defineBrowserProvider } from '@vitest/browser'
|
||||
import { createManualModuleSource } from '@vitest/mocker/node'
|
||||
import { resolve } from 'pathe'
|
||||
import c from 'tinyrainbow'
|
||||
import { createDebugger, isCSSRequest } from 'vitest/node'
|
||||
import commands from './commands'
|
||||
import { distRoot } from './constants'
|
||||
|
||||
const debug = createDebugger('vitest:browser:playwright')
|
||||
|
||||
@ -68,17 +74,14 @@ export interface PlaywrightProviderOptions {
|
||||
}
|
||||
|
||||
export function playwright(options: PlaywrightProviderOptions = {}): BrowserProviderOption<PlaywrightProviderOptions> {
|
||||
return {
|
||||
return defineBrowserProvider({
|
||||
name: 'playwright',
|
||||
supportedBrowser: playwrightBrowsers,
|
||||
options,
|
||||
factory(project) {
|
||||
providerFactory(project) {
|
||||
return new PlaywrightBrowserProvider(project, options)
|
||||
},
|
||||
// --browser.provider=playwright
|
||||
// @ts-expect-error hidden way to bypass importing playwright
|
||||
_cli: true,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export class PlaywrightBrowserProvider implements BrowserProvider {
|
||||
@ -98,6 +101,10 @@ export class PlaywrightBrowserProvider implements BrowserProvider {
|
||||
public tracingContexts: Set<string> = new Set()
|
||||
public pendingTraces: Map<string, string> = new Map()
|
||||
|
||||
public initScripts: string[] = [
|
||||
resolve(distRoot, 'locators.js'),
|
||||
]
|
||||
|
||||
constructor(
|
||||
private project: TestProject,
|
||||
private options: PlaywrightProviderOptions,
|
||||
@ -105,6 +112,10 @@ export class PlaywrightBrowserProvider implements BrowserProvider {
|
||||
this.browserName = project.config.browser.name as PlaywrightBrowser
|
||||
this.mocker = this.createMocker()
|
||||
|
||||
for (const [name, command] of Object.entries(commands)) {
|
||||
project.browser!.registerCommand(name as any, command as BrowserCommand)
|
||||
}
|
||||
|
||||
// make sure the traces are finished if the test hangs
|
||||
process.on('SIGTERM', () => {
|
||||
if (!this.browser) {
|
||||
@ -432,10 +443,9 @@ export class PlaywrightBrowserProvider implements BrowserProvider {
|
||||
return page
|
||||
}
|
||||
|
||||
async openPage(sessionId: string, url: string, beforeNavigate?: () => Promise<void>): Promise<void> {
|
||||
async openPage(sessionId: string, url: string): Promise<void> {
|
||||
debug?.('[%s][%s] creating the browser page for %s', sessionId, this.browserName, url)
|
||||
const browserPage = await this.openBrowserPage(sessionId)
|
||||
await beforeNavigate?.()
|
||||
debug?.('[%s][%s] browser page is created, opening %s', sessionId, this.browserName, url)
|
||||
await browserPage.goto(url, { timeout: 0 })
|
||||
await this._throwIfClosing(browserPage)
|
||||
@ -538,6 +548,10 @@ declare module 'vitest/node' {
|
||||
context: BrowserContext
|
||||
}
|
||||
|
||||
export interface _BrowserNames {
|
||||
playwright: PlaywrightBrowser
|
||||
}
|
||||
|
||||
export interface ToMatchScreenshotOptions
|
||||
extends Omit<
|
||||
ScreenshotMatcherOptions,
|
||||
@ -557,7 +571,7 @@ type PWSelectOptions = NonNullable<Parameters<Page['selectOption']>[2]>
|
||||
type PWDragAndDropOptions = NonNullable<Parameters<Page['dragAndDrop']>[2]>
|
||||
type PWSetInputFiles = NonNullable<Parameters<Page['setInputFiles']>[2]>
|
||||
|
||||
declare module '@vitest/browser/context' {
|
||||
declare module 'vitest/browser' {
|
||||
export interface UserEventHoverOptions extends PWHoverOptions {}
|
||||
export interface UserEventClickOptions extends PWClickOptions {}
|
||||
export interface UserEventDoubleClickOptions extends PWDoubleClickOptions {}
|
||||
13
packages/browser-playwright/tsconfig.json
Normal file
13
packages/browser-playwright/tsconfig.json
Normal file
@ -0,0 +1,13 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"types": ["node", "vite/client"],
|
||||
"isolatedDeclarations": true
|
||||
},
|
||||
"exclude": [
|
||||
"dist",
|
||||
"node_modules",
|
||||
"**/vite.config.ts",
|
||||
"src/client/**/*.ts"
|
||||
]
|
||||
}
|
||||
49
packages/browser-preview/README.md
Normal file
49
packages/browser-preview/README.md
Normal file
@ -0,0 +1,49 @@
|
||||
# @vitest/browser-preview
|
||||
|
||||
[](https://www.npmjs.com/package/@vitest/browser-preview)
|
||||
|
||||
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:
|
||||
|
||||
- [@vitest/browser-playwright](https://www.npmjs.com/package/@vitest/browser-playwright) - run tests using [playwright](https://playwright.dev/)
|
||||
- [@vitest/browser-webdriverio](https://www.npmjs.com/package/@vitest/browser-webdriverio) - run tests using [webdriverio](https://webdriver.io/)
|
||||
|
||||
## Installation
|
||||
|
||||
Install the package with your favorite package manager:
|
||||
|
||||
```sh
|
||||
npm install -D @vitest/browser-preview
|
||||
# or
|
||||
yarn add -D @vitest/browser-preview
|
||||
# or
|
||||
pnpm add -D @vitest/browser-preview
|
||||
```
|
||||
|
||||
Then specify it in the `browser.provider` field of your Vitest configuration:
|
||||
|
||||
```ts
|
||||
// vitest.config.ts
|
||||
import { defineConfig } from 'vitest/config'
|
||||
import { preview } from '@vitest/browser-preview'
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
browser: {
|
||||
provider: preview(),
|
||||
instances: [
|
||||
{ name: 'chromium' },
|
||||
],
|
||||
},
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
Then run Vitest in the browser mode:
|
||||
|
||||
```sh
|
||||
npx vitest --browser
|
||||
```
|
||||
|
||||
If browser didn't open automatically, follow the link in the terminal to open the browser preview.
|
||||
|
||||
[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
vendored
Normal file
1
packages/browser-preview/context.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
export * from '@vitest/browser/context'
|
||||
49
packages/browser-preview/package.json
Normal file
49
packages/browser-preview/package.json
Normal file
@ -0,0 +1,49 @@
|
||||
{
|
||||
"name": "@vitest/browser-preview",
|
||||
"type": "module",
|
||||
"version": "4.0.0-beta.13",
|
||||
"description": "Browser running for Vitest using your browser of choice",
|
||||
"license": "MIT",
|
||||
"funding": "https://opencollective.com/vitest",
|
||||
"homepage": "https://vitest.dev/guide/browser",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/vitest-dev/vitest.git",
|
||||
"directory": "packages/browser-preview"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/vitest-dev/vitest/issues"
|
||||
},
|
||||
"sideEffects": false,
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"default": "./dist/index.js"
|
||||
},
|
||||
"./context": {
|
||||
"types": "./context.d.ts"
|
||||
},
|
||||
"./package.json": "./package.json"
|
||||
},
|
||||
"main": "./dist/index.js",
|
||||
"module": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "premove dist && pnpm rollup -c",
|
||||
"dev": "rollup -c --watch --watch.include 'src/**'"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vitest": "workspace:*"
|
||||
},
|
||||
"dependencies": {
|
||||
"@testing-library/dom": "^10.4.1",
|
||||
"@testing-library/user-event": "^14.6.1",
|
||||
"@vitest/browser": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"vitest": "workspace:*"
|
||||
}
|
||||
}
|
||||
60
packages/browser-preview/rollup.config.js
Normal file
60
packages/browser-preview/rollup.config.js
Normal file
@ -0,0 +1,60 @@
|
||||
import { createRequire } from 'node:module'
|
||||
import commonjs from '@rollup/plugin-commonjs'
|
||||
import json from '@rollup/plugin-json'
|
||||
import resolve from '@rollup/plugin-node-resolve'
|
||||
import { defineConfig } from 'rollup'
|
||||
import oxc from 'unplugin-oxc/rollup'
|
||||
import { createDtsUtils } from '../../scripts/build-utils.js'
|
||||
|
||||
const require = createRequire(import.meta.url)
|
||||
const pkg = require('./package.json')
|
||||
|
||||
const external = [
|
||||
...Object.keys(pkg.dependencies),
|
||||
...Object.keys(pkg.peerDependencies || {}),
|
||||
/^@?vitest(\/|$)/,
|
||||
]
|
||||
|
||||
const dtsUtils = createDtsUtils()
|
||||
|
||||
const plugins = [
|
||||
resolve({
|
||||
preferBuiltins: true,
|
||||
}),
|
||||
json(),
|
||||
commonjs(),
|
||||
oxc({
|
||||
transform: { target: 'node18' },
|
||||
}),
|
||||
]
|
||||
|
||||
export default () =>
|
||||
defineConfig([
|
||||
{
|
||||
input: {
|
||||
index: './src/index.ts',
|
||||
locators: './src/locators.ts',
|
||||
},
|
||||
output: {
|
||||
dir: 'dist',
|
||||
format: 'esm',
|
||||
},
|
||||
external,
|
||||
context: 'null',
|
||||
plugins: [
|
||||
...dtsUtils.isolatedDecl(),
|
||||
...plugins,
|
||||
],
|
||||
},
|
||||
{
|
||||
input: dtsUtils.dtsInput('src/index.ts'),
|
||||
output: {
|
||||
dir: 'dist',
|
||||
entryFileNames: '[name].d.ts',
|
||||
format: 'esm',
|
||||
},
|
||||
watch: false,
|
||||
external,
|
||||
plugins: dtsUtils.dts(),
|
||||
},
|
||||
])
|
||||
5
packages/browser-preview/src/constants.ts
Normal file
5
packages/browser-preview/src/constants.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { resolve } from 'pathe'
|
||||
|
||||
const pkgRoot = resolve(fileURLToPath(import.meta.url), '../..')
|
||||
export const distRoot: string = resolve(pkgRoot, 'dist')
|
||||
5
packages/browser-preview/src/index.ts
Normal file
5
packages/browser-preview/src/index.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export {
|
||||
preview,
|
||||
PreviewBrowserProvider,
|
||||
} from './preview'
|
||||
export { defineBrowserCommand } from '@vitest/browser'
|
||||
@ -1,5 +1,5 @@
|
||||
import { page, server, userEvent } from '@vitest/browser/context'
|
||||
import {
|
||||
convertElementToCssSelector,
|
||||
getByAltTextSelector,
|
||||
getByLabelSelector,
|
||||
getByPlaceholderSelector,
|
||||
@ -7,44 +7,11 @@ import {
|
||||
getByTestIdSelector,
|
||||
getByTextSelector,
|
||||
getByTitleSelector,
|
||||
} from 'ivya'
|
||||
import { getElementError } from '../public-utils'
|
||||
import { convertElementToCssSelector } from '../utils'
|
||||
import { Locator, selectorEngine } from './index'
|
||||
|
||||
page.extend({
|
||||
getByLabelText(text, options) {
|
||||
return new PreviewLocator(getByLabelSelector(text, options))
|
||||
},
|
||||
getByRole(role, options) {
|
||||
return new PreviewLocator(getByRoleSelector(role, options))
|
||||
},
|
||||
getByTestId(testId) {
|
||||
return new PreviewLocator(getByTestIdSelector(server.config.browser.locators.testIdAttribute, testId))
|
||||
},
|
||||
getByAltText(text, options) {
|
||||
return new PreviewLocator(getByAltTextSelector(text, options))
|
||||
},
|
||||
getByPlaceholder(text, options) {
|
||||
return new PreviewLocator(getByPlaceholderSelector(text, options))
|
||||
},
|
||||
getByText(text, options) {
|
||||
return new PreviewLocator(getByTextSelector(text, options))
|
||||
},
|
||||
getByTitle(title, options) {
|
||||
return new PreviewLocator(getByTitleSelector(title, options))
|
||||
},
|
||||
|
||||
_createLocator(selector: string) {
|
||||
return new PreviewLocator(selector)
|
||||
},
|
||||
elementLocator(element: Element) {
|
||||
return new PreviewLocator(
|
||||
selectorEngine.generateSelectorSimple(element),
|
||||
element,
|
||||
)
|
||||
},
|
||||
})
|
||||
Locator,
|
||||
selectorEngine,
|
||||
} from '@vitest/browser/locators'
|
||||
import { page, server, userEvent, utils } from 'vitest/browser'
|
||||
import { __INTERNAL } from 'vitest/internal/browser'
|
||||
|
||||
class PreviewLocator extends Locator {
|
||||
constructor(protected _pwSelector: string, protected _container?: Element) {
|
||||
@ -54,7 +21,7 @@ class PreviewLocator extends Locator {
|
||||
override get selector() {
|
||||
const selectors = this.elements().map(element => convertElementToCssSelector(element))
|
||||
if (!selectors.length) {
|
||||
throw getElementError(this._pwSelector, this._container || document.body)
|
||||
throw utils.getElementError(this._pwSelector, this._container || document.body)
|
||||
}
|
||||
return selectors.join(', ')
|
||||
}
|
||||
@ -106,3 +73,36 @@ class PreviewLocator extends Locator {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
page.extend({
|
||||
getByLabelText(text, options) {
|
||||
return new PreviewLocator(getByLabelSelector(text, options))
|
||||
},
|
||||
getByRole(role, options) {
|
||||
return new PreviewLocator(getByRoleSelector(role, options))
|
||||
},
|
||||
getByTestId(testId) {
|
||||
return new PreviewLocator(getByTestIdSelector(server.config.browser.locators.testIdAttribute, testId))
|
||||
},
|
||||
getByAltText(text, options) {
|
||||
return new PreviewLocator(getByAltTextSelector(text, options))
|
||||
},
|
||||
getByPlaceholder(text, options) {
|
||||
return new PreviewLocator(getByPlaceholderSelector(text, options))
|
||||
},
|
||||
getByText(text, options) {
|
||||
return new PreviewLocator(getByTextSelector(text, options))
|
||||
},
|
||||
getByTitle(title, options) {
|
||||
return new PreviewLocator(getByTitleSelector(title, options))
|
||||
},
|
||||
|
||||
elementLocator(element: Element) {
|
||||
return new PreviewLocator(
|
||||
selectorEngine.generateSelectorSimple(element),
|
||||
element,
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
__INTERNAL._createLocator = selector => new PreviewLocator(selector)
|
||||
@ -1,16 +1,16 @@
|
||||
import type { BrowserProvider, BrowserProviderOption, TestProject } from 'vitest/node'
|
||||
import { nextTick } from 'node:process'
|
||||
import { defineBrowserProvider } from '@vitest/browser'
|
||||
import { resolve } from 'pathe'
|
||||
import { distRoot } from './constants'
|
||||
|
||||
export function preview(): BrowserProviderOption {
|
||||
return {
|
||||
return defineBrowserProvider({
|
||||
name: 'preview',
|
||||
options: {},
|
||||
factory(project) {
|
||||
providerFactory(project) {
|
||||
return new PreviewBrowserProvider(project)
|
||||
},
|
||||
// --browser.provider=preview
|
||||
// @ts-expect-error hidden way to bypass importing preview
|
||||
_cli: true,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export class PreviewBrowserProvider implements BrowserProvider {
|
||||
@ -19,6 +19,12 @@ export class PreviewBrowserProvider implements BrowserProvider {
|
||||
private project!: TestProject
|
||||
private open = false
|
||||
|
||||
public distRoot: string = distRoot
|
||||
|
||||
public initScripts: string[] = [
|
||||
resolve(distRoot, 'locators.js'),
|
||||
]
|
||||
|
||||
constructor(project: TestProject) {
|
||||
this.project = project
|
||||
this.open = false
|
||||
@ -27,7 +33,9 @@ export class PreviewBrowserProvider implements BrowserProvider {
|
||||
'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',
|
||||
)
|
||||
}
|
||||
project.vitest.logger.printBrowserBanner(project)
|
||||
nextTick(() => {
|
||||
project.vitest.logger.printBrowserBanner(project)
|
||||
})
|
||||
}
|
||||
|
||||
isOpen(): boolean {
|
||||
13
packages/browser-preview/tsconfig.json
Normal file
13
packages/browser-preview/tsconfig.json
Normal file
@ -0,0 +1,13 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"types": ["node", "vite/client"],
|
||||
"isolatedDeclarations": true
|
||||
},
|
||||
"exclude": [
|
||||
"dist",
|
||||
"node_modules",
|
||||
"**/vite.config.ts",
|
||||
"src/client/**/*.ts"
|
||||
]
|
||||
}
|
||||
48
packages/browser-webdriverio/README.md
Normal file
48
packages/browser-webdriverio/README.md
Normal file
@ -0,0 +1,48 @@
|
||||
# @vitest/browser-webdriverio
|
||||
|
||||
[](https://www.npmjs.com/package/@vitest/browser-webdriverio)
|
||||
|
||||
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.
|
||||
|
||||
We recommend using this package if you are already using webdriverio in your project.
|
||||
|
||||
## Installation
|
||||
|
||||
Install the package with your favorite package manager:
|
||||
|
||||
```sh
|
||||
npm install -D @vitest/browser-webdriverio
|
||||
# or
|
||||
yarn add -D @vitest/browser-webdriverio
|
||||
# or
|
||||
pnpm add -D @vitest/browser-webdriverio
|
||||
```
|
||||
|
||||
Then specify it in the `browser.provider` field of your Vitest configuration:
|
||||
|
||||
```ts
|
||||
// vitest.config.ts
|
||||
import { defineConfig } from 'vitest/config'
|
||||
import { webdriverio } from '@vitest/browser-webdriverio'
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
browser: {
|
||||
provider: webdriverio({
|
||||
// ...custom webdriverio options
|
||||
}),
|
||||
instances: [
|
||||
{ name: 'chromium' },
|
||||
],
|
||||
},
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
Then run Vitest in the browser mode:
|
||||
|
||||
```sh
|
||||
npx vitest --browser
|
||||
```
|
||||
|
||||
[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
vendored
Normal file
1
packages/browser-webdriverio/context.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
export * from '@vitest/browser/context'
|
||||
55
packages/browser-webdriverio/package.json
Normal file
55
packages/browser-webdriverio/package.json
Normal file
@ -0,0 +1,55 @@
|
||||
{
|
||||
"name": "@vitest/browser-webdriverio",
|
||||
"type": "module",
|
||||
"version": "4.0.0-beta.13",
|
||||
"description": "Browser running for Vitest using webdriverio",
|
||||
"license": "MIT",
|
||||
"funding": "https://opencollective.com/vitest",
|
||||
"homepage": "https://vitest.dev/guide/browser/webdriverio",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/vitest-dev/vitest.git",
|
||||
"directory": "packages/browser-webdriverio"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/vitest-dev/vitest/issues"
|
||||
},
|
||||
"sideEffects": false,
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"default": "./dist/index.js"
|
||||
},
|
||||
"./context": {
|
||||
"types": "./context.d.ts"
|
||||
},
|
||||
"./package.json": "./package.json"
|
||||
},
|
||||
"main": "./dist/index.js",
|
||||
"module": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "premove dist && pnpm rollup -c",
|
||||
"dev": "rollup -c --watch --watch.include 'src/**'"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vitest": "workspace:*",
|
||||
"webdriverio": "*"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"webdriverio": {
|
||||
"optional": false
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@vitest/browser": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@wdio/types": "^9.19.2",
|
||||
"vitest": "workspace:*",
|
||||
"webdriverio": "^9.19.2"
|
||||
}
|
||||
}
|
||||
60
packages/browser-webdriverio/rollup.config.js
Normal file
60
packages/browser-webdriverio/rollup.config.js
Normal file
@ -0,0 +1,60 @@
|
||||
import { createRequire } from 'node:module'
|
||||
import commonjs from '@rollup/plugin-commonjs'
|
||||
import json from '@rollup/plugin-json'
|
||||
import resolve from '@rollup/plugin-node-resolve'
|
||||
import { defineConfig } from 'rollup'
|
||||
import oxc from 'unplugin-oxc/rollup'
|
||||
import { createDtsUtils } from '../../scripts/build-utils.js'
|
||||
|
||||
const require = createRequire(import.meta.url)
|
||||
const pkg = require('./package.json')
|
||||
|
||||
const external = [
|
||||
...Object.keys(pkg.dependencies),
|
||||
...Object.keys(pkg.peerDependencies || {}),
|
||||
/^@?vitest(\/|$)/,
|
||||
]
|
||||
|
||||
const dtsUtils = createDtsUtils()
|
||||
|
||||
const plugins = [
|
||||
resolve({
|
||||
preferBuiltins: true,
|
||||
}),
|
||||
json(),
|
||||
commonjs(),
|
||||
oxc({
|
||||
transform: { target: 'node18' },
|
||||
}),
|
||||
]
|
||||
|
||||
export default () =>
|
||||
defineConfig([
|
||||
{
|
||||
input: {
|
||||
index: './src/index.ts',
|
||||
locators: './src/locators.ts',
|
||||
},
|
||||
output: {
|
||||
dir: 'dist',
|
||||
format: 'esm',
|
||||
},
|
||||
external,
|
||||
context: 'null',
|
||||
plugins: [
|
||||
...dtsUtils.isolatedDecl(),
|
||||
...plugins,
|
||||
],
|
||||
},
|
||||
{
|
||||
input: dtsUtils.dtsInput('src/index.ts'),
|
||||
output: {
|
||||
dir: 'dist',
|
||||
entryFileNames: '[name].d.ts',
|
||||
format: 'esm',
|
||||
},
|
||||
watch: false,
|
||||
external,
|
||||
plugins: dtsUtils.dts(),
|
||||
},
|
||||
])
|
||||
10
packages/browser-webdriverio/src/commands/clear.ts
Normal file
10
packages/browser-webdriverio/src/commands/clear.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import type { UserEvent } from 'vitest/browser'
|
||||
import type { UserEventCommand } from './utils'
|
||||
|
||||
export const clear: UserEventCommand<UserEvent['clear']> = async (
|
||||
context,
|
||||
selector,
|
||||
) => {
|
||||
const browser = context.browser
|
||||
await browser.$(selector).clearValue()
|
||||
}
|
||||
44
packages/browser-webdriverio/src/commands/click.ts
Normal file
44
packages/browser-webdriverio/src/commands/click.ts
Normal file
@ -0,0 +1,44 @@
|
||||
import type { UserEvent } from 'vitest/browser'
|
||||
import type { UserEventCommand } from './utils'
|
||||
|
||||
export const click: UserEventCommand<UserEvent['click']> = async (
|
||||
context,
|
||||
selector,
|
||||
options = {},
|
||||
) => {
|
||||
const browser = context.browser
|
||||
await browser.$(selector).click(options as any)
|
||||
}
|
||||
|
||||
export const dblClick: UserEventCommand<UserEvent['dblClick']> = async (
|
||||
context,
|
||||
selector,
|
||||
_options = {},
|
||||
) => {
|
||||
const browser = context.browser
|
||||
await browser.$(selector).doubleClick()
|
||||
}
|
||||
|
||||
export const tripleClick: UserEventCommand<UserEvent['tripleClick']> = async (
|
||||
context,
|
||||
selector,
|
||||
_options = {},
|
||||
) => {
|
||||
const browser = context.browser
|
||||
await browser
|
||||
.action('pointer', { parameters: { pointerType: 'mouse' } })
|
||||
// move the pointer over the button
|
||||
.move({ origin: browser.$(selector) })
|
||||
// simulate 3 clicks
|
||||
.down()
|
||||
.up()
|
||||
.pause(50)
|
||||
.down()
|
||||
.up()
|
||||
.pause(50)
|
||||
.down()
|
||||
.up()
|
||||
.pause(50)
|
||||
// run the sequence
|
||||
.perform()
|
||||
}
|
||||
27
packages/browser-webdriverio/src/commands/dragAndDrop.ts
Normal file
27
packages/browser-webdriverio/src/commands/dragAndDrop.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import type { UserEvent } from 'vitest/browser'
|
||||
import type { UserEventCommand } from './utils'
|
||||
|
||||
export const dragAndDrop: UserEventCommand<UserEvent['dragAndDrop']> = async (
|
||||
context,
|
||||
source,
|
||||
target,
|
||||
options_,
|
||||
) => {
|
||||
const $source = context.browser.$(source)
|
||||
const $target = context.browser.$(target)
|
||||
const options = (options_ || {}) as any
|
||||
const duration = options.duration ?? 10
|
||||
|
||||
// https://github.com/webdriverio/webdriverio/issues/8022#issuecomment-1700919670
|
||||
await context.browser
|
||||
.action('pointer')
|
||||
.move({ duration: 0, origin: $source, x: options.sourceX ?? 0, y: options.sourceY ?? 0 })
|
||||
.down({ button: 0 })
|
||||
.move({ duration: 0, origin: 'pointer', x: 0, y: 0 })
|
||||
.pause(duration)
|
||||
.move({ duration: 0, origin: $target, x: options.targetX ?? 0, y: options.targetY ?? 0 })
|
||||
.move({ duration: 0, origin: 'pointer', x: 1, y: 0 })
|
||||
.move({ duration: 0, origin: 'pointer', x: -1, y: 0 })
|
||||
.up({ button: 0 })
|
||||
.perform()
|
||||
}
|
||||
12
packages/browser-webdriverio/src/commands/fill.ts
Normal file
12
packages/browser-webdriverio/src/commands/fill.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import type { UserEvent } from 'vitest/browser'
|
||||
import type { UserEventCommand } from './utils'
|
||||
|
||||
export const fill: UserEventCommand<UserEvent['fill']> = async (
|
||||
context,
|
||||
selector,
|
||||
text,
|
||||
_options = {},
|
||||
) => {
|
||||
const browser = context.browser
|
||||
await browser.$(selector).setValue(text)
|
||||
}
|
||||
11
packages/browser-webdriverio/src/commands/hover.ts
Normal file
11
packages/browser-webdriverio/src/commands/hover.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import type { UserEvent } from 'vitest/browser'
|
||||
import type { UserEventCommand } from './utils'
|
||||
|
||||
export const hover: UserEventCommand<UserEvent['hover']> = async (
|
||||
context,
|
||||
selector,
|
||||
options = {},
|
||||
) => {
|
||||
const browser = context.browser
|
||||
await browser.$(selector).moveTo(options as any)
|
||||
}
|
||||
30
packages/browser-webdriverio/src/commands/index.ts
Normal file
30
packages/browser-webdriverio/src/commands/index.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import { clear } from './clear'
|
||||
import { click, dblClick, tripleClick } from './click'
|
||||
import { dragAndDrop } from './dragAndDrop'
|
||||
import { fill } from './fill'
|
||||
import { hover } from './hover'
|
||||
import { keyboard, keyboardCleanup } from './keyboard'
|
||||
import { takeScreenshot } from './screenshot'
|
||||
import { selectOptions } from './select'
|
||||
import { tab } from './tab'
|
||||
import { type } from './type'
|
||||
import { upload } from './upload'
|
||||
import { viewport } from './viewport'
|
||||
|
||||
export default {
|
||||
__vitest_upload: upload as typeof upload,
|
||||
__vitest_click: click as typeof click,
|
||||
__vitest_dblClick: dblClick as typeof dblClick,
|
||||
__vitest_tripleClick: tripleClick as typeof tripleClick,
|
||||
__vitest_takeScreenshot: takeScreenshot as typeof takeScreenshot,
|
||||
__vitest_type: type as typeof type,
|
||||
__vitest_clear: clear as typeof clear,
|
||||
__vitest_fill: fill as typeof fill,
|
||||
__vitest_tab: tab as typeof tab,
|
||||
__vitest_keyboard: keyboard as typeof keyboard,
|
||||
__vitest_selectOptions: selectOptions as typeof selectOptions,
|
||||
__vitest_dragAndDrop: dragAndDrop as typeof dragAndDrop,
|
||||
__vitest_hover: hover as typeof hover,
|
||||
__vitest_cleanup: keyboardCleanup as typeof keyboardCleanup,
|
||||
__vitest_viewport: viewport as typeof viewport,
|
||||
}
|
||||
125
packages/browser-webdriverio/src/commands/keyboard.ts
Normal file
125
packages/browser-webdriverio/src/commands/keyboard.ts
Normal file
@ -0,0 +1,125 @@
|
||||
import type { BrowserProvider } from 'vitest/node'
|
||||
import type { WebdriverBrowserProvider } from '../webdriverio'
|
||||
import type { UserEventCommand } from './utils'
|
||||
import { parseKeyDef } from '@vitest/browser'
|
||||
import { Key } from 'webdriverio'
|
||||
|
||||
export interface KeyboardState {
|
||||
unreleased: string[]
|
||||
}
|
||||
|
||||
export const keyboard: UserEventCommand<(
|
||||
text: string,
|
||||
state: KeyboardState
|
||||
) => Promise<{ unreleased: string[] }>> = async (
|
||||
context,
|
||||
text,
|
||||
state,
|
||||
) => {
|
||||
await context.browser.execute(focusIframe)
|
||||
|
||||
const pressed = new Set<string>(state.unreleased)
|
||||
|
||||
await keyboardImplementation(
|
||||
pressed,
|
||||
context.provider,
|
||||
context.sessionId,
|
||||
text,
|
||||
async () => {
|
||||
await context.browser.execute(selectAll)
|
||||
},
|
||||
true,
|
||||
)
|
||||
|
||||
return {
|
||||
unreleased: Array.from(pressed),
|
||||
}
|
||||
}
|
||||
|
||||
export const keyboardCleanup: UserEventCommand<(state: KeyboardState) => Promise<void>> = async (
|
||||
context,
|
||||
state,
|
||||
) => {
|
||||
if (!state.unreleased) {
|
||||
return
|
||||
}
|
||||
const keyboard = context.browser.action('key')
|
||||
for (const key of state.unreleased) {
|
||||
keyboard.up(key)
|
||||
}
|
||||
await keyboard.perform()
|
||||
}
|
||||
|
||||
export async function keyboardImplementation(
|
||||
pressed: Set<string>,
|
||||
provider: BrowserProvider,
|
||||
_sessionId: string,
|
||||
text: string,
|
||||
selectAll: () => Promise<void>,
|
||||
skipRelease: boolean,
|
||||
): Promise<{ pressed: Set<string> }> {
|
||||
const browser = (provider as WebdriverBrowserProvider).browser!
|
||||
const actions = parseKeyDef(text)
|
||||
|
||||
let keyboard = browser.action('key')
|
||||
|
||||
for (const { releasePrevious, releaseSelf, repeat, keyDef } of actions) {
|
||||
let key = keyDef.key!
|
||||
const special = Key[key as 'Shift']
|
||||
|
||||
if (special) {
|
||||
key = special
|
||||
}
|
||||
|
||||
if (pressed.has(key)) {
|
||||
keyboard.up(key)
|
||||
pressed.delete(key)
|
||||
}
|
||||
|
||||
if (!releasePrevious) {
|
||||
if (key === 'selectall') {
|
||||
await keyboard.perform()
|
||||
keyboard = browser.action('key')
|
||||
await selectAll()
|
||||
continue
|
||||
}
|
||||
|
||||
for (let i = 1; i <= repeat; i++) {
|
||||
keyboard.down(key)
|
||||
}
|
||||
|
||||
if (releaseSelf) {
|
||||
keyboard.up(key)
|
||||
}
|
||||
else {
|
||||
pressed.add(key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// seems like webdriverio doesn't release keys automatically if skipRelease is true and all events are keyUp
|
||||
const allRelease = keyboard.toJSON().actions.every(action => action.type === 'keyUp')
|
||||
|
||||
await keyboard.perform(allRelease ? false : skipRelease)
|
||||
|
||||
return {
|
||||
pressed,
|
||||
}
|
||||
}
|
||||
|
||||
function focusIframe() {
|
||||
if (
|
||||
!document.activeElement
|
||||
|| document.activeElement.ownerDocument !== document
|
||||
|| document.activeElement === document.body
|
||||
) {
|
||||
window.focus()
|
||||
}
|
||||
}
|
||||
|
||||
function selectAll() {
|
||||
const element = document.activeElement as HTMLInputElement
|
||||
if (element && typeof element.select === 'function') {
|
||||
element.select()
|
||||
}
|
||||
}
|
||||
70
packages/browser-webdriverio/src/commands/screenshot.ts
Normal file
70
packages/browser-webdriverio/src/commands/screenshot.ts
Normal file
@ -0,0 +1,70 @@
|
||||
import type { ScreenshotOptions } from 'vitest/browser'
|
||||
import type { BrowserCommandContext } from 'vitest/node'
|
||||
import crypto from 'node:crypto'
|
||||
import { mkdir, rm } from 'node:fs/promises'
|
||||
import { normalize as platformNormalize } from 'node:path'
|
||||
import { resolveScreenshotPath } from '@vitest/browser'
|
||||
import { dirname, normalize, resolve } from 'pathe'
|
||||
|
||||
interface ScreenshotCommandOptions extends Omit<ScreenshotOptions, 'element' | 'mask'> {
|
||||
element?: string
|
||||
mask?: readonly string[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes a screenshot using the provided browser context and returns a buffer and the expected screenshot path.
|
||||
*
|
||||
* **Note**: the returned `path` indicates where the screenshot *might* be found.
|
||||
* It is not guaranteed to exist, especially if `options.save` is `false`.
|
||||
*
|
||||
* @throws {Error} If the function is not called within a test or if the browser provider does not support screenshots.
|
||||
*/
|
||||
export async function takeScreenshot(
|
||||
context: BrowserCommandContext,
|
||||
name: string,
|
||||
options: Omit<ScreenshotCommandOptions, 'base64'>,
|
||||
): Promise<{ buffer: Buffer<ArrayBufferLike>; path: string }> {
|
||||
if (!context.testPath) {
|
||||
throw new Error(`Cannot take a screenshot without a test path`)
|
||||
}
|
||||
|
||||
const path = resolveScreenshotPath(
|
||||
context.testPath,
|
||||
name,
|
||||
context.project.config,
|
||||
options.path,
|
||||
)
|
||||
|
||||
// playwright does not need a screenshot path if we don't intend to save it
|
||||
let savePath: string | undefined
|
||||
|
||||
if (options.save) {
|
||||
savePath = normalize(path)
|
||||
|
||||
await mkdir(dirname(savePath), { recursive: true })
|
||||
}
|
||||
|
||||
// webdriverio needs a path, so if one is not already set we create a temporary one
|
||||
if (savePath === undefined) {
|
||||
savePath = resolve(context.project.tmpDir, crypto.randomUUID())
|
||||
|
||||
await mkdir(context.project.tmpDir, { recursive: true })
|
||||
}
|
||||
|
||||
const page = context.browser
|
||||
const element = !options.element
|
||||
? await page.$('body')
|
||||
: await page.$(`${options.element}`)
|
||||
|
||||
// webdriverio expects the path to contain the extension and only works with PNG files
|
||||
const savePathWithExtension = savePath.endsWith('.png') ? savePath : `${savePath}.png`
|
||||
|
||||
// there seems to be a bug in webdriverio, `X:/` gets appended to cwd, so we convert to `X:\`
|
||||
const buffer = await element.saveScreenshot(
|
||||
platformNormalize(savePathWithExtension),
|
||||
)
|
||||
if (!options.save) {
|
||||
await rm(savePathWithExtension, { force: true })
|
||||
}
|
||||
return { buffer, path }
|
||||
}
|
||||
25
packages/browser-webdriverio/src/commands/select.ts
Normal file
25
packages/browser-webdriverio/src/commands/select.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import type { UserEvent } from 'vitest/browser'
|
||||
import type { UserEventCommand } from './utils'
|
||||
|
||||
export const selectOptions: UserEventCommand<UserEvent['selectOptions']> = async (
|
||||
context,
|
||||
selector,
|
||||
userValues,
|
||||
_options = {},
|
||||
) => {
|
||||
const values = userValues as any as [({ index: number })]
|
||||
|
||||
if (!values.length) {
|
||||
return
|
||||
}
|
||||
|
||||
const browser = context.browser
|
||||
|
||||
if (values.length === 1 && 'index' in values[0]) {
|
||||
const selectElement = browser.$(selector)
|
||||
await selectElement.selectByIndex(values[0].index)
|
||||
}
|
||||
else {
|
||||
throw new Error('Provider "webdriverio" doesn\'t support selecting multiple values at once')
|
||||
}
|
||||
}
|
||||
11
packages/browser-webdriverio/src/commands/tab.ts
Normal file
11
packages/browser-webdriverio/src/commands/tab.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import type { UserEvent } from 'vitest/browser'
|
||||
import type { UserEventCommand } from './utils'
|
||||
import { Key } from 'webdriverio'
|
||||
|
||||
export const tab: UserEventCommand<UserEvent['tab']> = async (
|
||||
context,
|
||||
options = {},
|
||||
) => {
|
||||
const browser = context.browser
|
||||
await browser.keys(options.shift === true ? [Key.Shift, Key.Tab] : [Key.Tab])
|
||||
}
|
||||
38
packages/browser-webdriverio/src/commands/type.ts
Normal file
38
packages/browser-webdriverio/src/commands/type.ts
Normal file
@ -0,0 +1,38 @@
|
||||
import type { UserEvent } from 'vitest/browser'
|
||||
import type { UserEventCommand } from './utils'
|
||||
import { keyboardImplementation } from './keyboard'
|
||||
|
||||
export const type: UserEventCommand<UserEvent['type']> = async (
|
||||
context,
|
||||
selector,
|
||||
text,
|
||||
options = {},
|
||||
) => {
|
||||
const { skipClick = false, skipAutoClose = false } = options
|
||||
const unreleased = new Set(Reflect.get(options, 'unreleased') as string[] ?? [])
|
||||
|
||||
const browser = context.browser
|
||||
const element = browser.$(selector)
|
||||
|
||||
if (!skipClick && !await element.isFocused()) {
|
||||
await element.click()
|
||||
}
|
||||
|
||||
await keyboardImplementation(
|
||||
unreleased,
|
||||
context.provider,
|
||||
context.sessionId,
|
||||
text,
|
||||
() => browser.execute(() => {
|
||||
const element = document.activeElement as HTMLInputElement
|
||||
if (element && typeof element.select === 'function') {
|
||||
element.select()
|
||||
}
|
||||
}),
|
||||
skipAutoClose,
|
||||
)
|
||||
|
||||
return {
|
||||
unreleased: Array.from(unreleased),
|
||||
}
|
||||
}
|
||||
34
packages/browser-webdriverio/src/commands/upload.ts
Normal file
34
packages/browser-webdriverio/src/commands/upload.ts
Normal file
@ -0,0 +1,34 @@
|
||||
import type { UserEventUploadOptions } from 'vitest/browser'
|
||||
import type { UserEventCommand } from './utils'
|
||||
import { resolve } from 'pathe'
|
||||
|
||||
export const upload: UserEventCommand<(element: string, files: Array<string | {
|
||||
name: string
|
||||
mimeType: string
|
||||
base64: string
|
||||
}>, options: UserEventUploadOptions) => void> = async (
|
||||
context,
|
||||
selector,
|
||||
files,
|
||||
_options,
|
||||
) => {
|
||||
const testPath = context.testPath
|
||||
if (!testPath) {
|
||||
throw new Error(`Cannot upload files outside of a test`)
|
||||
}
|
||||
const root = context.project.config.root
|
||||
|
||||
for (const file of files) {
|
||||
if (typeof file !== 'string') {
|
||||
throw new TypeError(`The "${context.provider.name}" provider doesn't support uploading files objects. Provide a file path instead.`)
|
||||
}
|
||||
}
|
||||
|
||||
const element = context.browser.$(selector)
|
||||
|
||||
for (const file of files) {
|
||||
const filepath = resolve(root, file as string)
|
||||
const remoteFilePath = await context.browser.uploadFile(filepath)
|
||||
await element.addValue(remoteFilePath)
|
||||
}
|
||||
}
|
||||
11
packages/browser-webdriverio/src/commands/utils.ts
Normal file
11
packages/browser-webdriverio/src/commands/utils.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import type { Locator } from 'vitest/browser'
|
||||
import type { BrowserCommand } from 'vitest/node'
|
||||
|
||||
export type UserEventCommand<T extends (...args: any) => any> = BrowserCommand<
|
||||
ConvertUserEventParameters<Parameters<T>>
|
||||
>
|
||||
|
||||
type ConvertElementToLocator<T> = T extends Element | Locator ? string : T
|
||||
type ConvertUserEventParameters<T extends unknown[]> = {
|
||||
[K in keyof T]: ConvertElementToLocator<T[K]>;
|
||||
}
|
||||
9
packages/browser-webdriverio/src/commands/viewport.ts
Normal file
9
packages/browser-webdriverio/src/commands/viewport.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import type { WebdriverBrowserProvider } from '../webdriverio'
|
||||
import type { UserEventCommand } from './utils'
|
||||
|
||||
export const viewport: UserEventCommand<(options: {
|
||||
width: number
|
||||
height: number
|
||||
}) => void> = async (context, options) => {
|
||||
await (context.provider as WebdriverBrowserProvider).setViewport(options)
|
||||
}
|
||||
5
packages/browser-webdriverio/src/constants.ts
Normal file
5
packages/browser-webdriverio/src/constants.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { resolve } from 'pathe'
|
||||
|
||||
const pkgRoot = resolve(fileURLToPath(import.meta.url), '../..')
|
||||
export const distRoot: string = resolve(pkgRoot, 'dist')
|
||||
6
packages/browser-webdriverio/src/index.ts
Normal file
6
packages/browser-webdriverio/src/index.ts
Normal file
@ -0,0 +1,6 @@
|
||||
export {
|
||||
WebdriverBrowserProvider,
|
||||
webdriverio,
|
||||
type WebdriverProviderOptions,
|
||||
} from './webdriverio'
|
||||
export { defineBrowserCommand } from '@vitest/browser'
|
||||
@ -3,9 +3,9 @@ import type {
|
||||
UserEventDragAndDropOptions,
|
||||
UserEventHoverOptions,
|
||||
UserEventSelectOptions,
|
||||
} from '@vitest/browser/context'
|
||||
import { page, server } from '@vitest/browser/context'
|
||||
} from 'vitest/browser'
|
||||
import {
|
||||
convertElementToCssSelector,
|
||||
getByAltTextSelector,
|
||||
getByLabelSelector,
|
||||
getByPlaceholderSelector,
|
||||
@ -13,42 +13,12 @@ import {
|
||||
getByTestIdSelector,
|
||||
getByTextSelector,
|
||||
getByTitleSelector,
|
||||
} from 'ivya'
|
||||
import { getBrowserState } from '../../utils'
|
||||
import { getElementError } from '../public-utils'
|
||||
import { convertElementToCssSelector, getIframeScale } from '../utils'
|
||||
import { Locator, selectorEngine } from './index'
|
||||
|
||||
page.extend({
|
||||
getByLabelText(text, options) {
|
||||
return new WebdriverIOLocator(getByLabelSelector(text, options))
|
||||
},
|
||||
getByRole(role, options) {
|
||||
return new WebdriverIOLocator(getByRoleSelector(role, options))
|
||||
},
|
||||
getByTestId(testId) {
|
||||
return new WebdriverIOLocator(getByTestIdSelector(server.config.browser.locators.testIdAttribute, testId))
|
||||
},
|
||||
getByAltText(text, options) {
|
||||
return new WebdriverIOLocator(getByAltTextSelector(text, options))
|
||||
},
|
||||
getByPlaceholder(text, options) {
|
||||
return new WebdriverIOLocator(getByPlaceholderSelector(text, options))
|
||||
},
|
||||
getByText(text, options) {
|
||||
return new WebdriverIOLocator(getByTextSelector(text, options))
|
||||
},
|
||||
getByTitle(title, options) {
|
||||
return new WebdriverIOLocator(getByTitleSelector(title, options))
|
||||
},
|
||||
|
||||
_createLocator(selector: string) {
|
||||
return new WebdriverIOLocator(selector)
|
||||
},
|
||||
elementLocator(element: Element) {
|
||||
return new WebdriverIOLocator(selectorEngine.generateSelectorSimple(element))
|
||||
},
|
||||
})
|
||||
getIframeScale,
|
||||
Locator,
|
||||
selectorEngine,
|
||||
} from '@vitest/browser/locators'
|
||||
import { page, server, utils } from 'vitest/browser'
|
||||
import { __INTERNAL } from 'vitest/internal/browser'
|
||||
|
||||
class WebdriverIOLocator extends Locator {
|
||||
constructor(protected _pwSelector: string, protected _container?: Element) {
|
||||
@ -58,7 +28,7 @@ class WebdriverIOLocator extends Locator {
|
||||
override get selector(): string {
|
||||
const selectors = this.elements().map(element => convertElementToCssSelector(element))
|
||||
if (!selectors.length) {
|
||||
throw getElementError(this._pwSelector, this._container || document.body)
|
||||
throw utils.getElementError(this._pwSelector, this._container || document.body)
|
||||
}
|
||||
let hasShadowRoot = false
|
||||
const newSelectors = selectors.map((selector) => {
|
||||
@ -108,6 +78,36 @@ class WebdriverIOLocator extends Locator {
|
||||
}
|
||||
}
|
||||
|
||||
page.extend({
|
||||
getByLabelText(text, options) {
|
||||
return new WebdriverIOLocator(getByLabelSelector(text, options))
|
||||
},
|
||||
getByRole(role, options) {
|
||||
return new WebdriverIOLocator(getByRoleSelector(role, options))
|
||||
},
|
||||
getByTestId(testId) {
|
||||
return new WebdriverIOLocator(getByTestIdSelector(server.config.browser.locators.testIdAttribute, testId))
|
||||
},
|
||||
getByAltText(text, options) {
|
||||
return new WebdriverIOLocator(getByAltTextSelector(text, options))
|
||||
},
|
||||
getByPlaceholder(text, options) {
|
||||
return new WebdriverIOLocator(getByPlaceholderSelector(text, options))
|
||||
},
|
||||
getByText(text, options) {
|
||||
return new WebdriverIOLocator(getByTextSelector(text, options))
|
||||
},
|
||||
getByTitle(title, options) {
|
||||
return new WebdriverIOLocator(getByTitleSelector(title, options))
|
||||
},
|
||||
|
||||
elementLocator(element: Element) {
|
||||
return new WebdriverIOLocator(selectorEngine.generateSelectorSimple(element))
|
||||
},
|
||||
})
|
||||
|
||||
__INTERNAL._createLocator = selector => new WebdriverIOLocator(selector)
|
||||
|
||||
function getWebdriverioSelectOptions(element: Element, value: string | string[] | HTMLElement[] | HTMLElement | Locator | Locator[]) {
|
||||
const options = [...element.querySelectorAll('option')] as HTMLOptionElement[]
|
||||
|
||||
@ -149,12 +149,11 @@ function getWebdriverioSelectOptions(element: Element, value: string | string[]
|
||||
return [{ index: labelIndex }]
|
||||
}
|
||||
|
||||
function processClickOptions(options_?: UserEventClickOptions) {
|
||||
function processClickOptions(options?: UserEventClickOptions) {
|
||||
// only ui scales the iframe, so we need to adjust the position
|
||||
if (!options_ || !getBrowserState().config.browser.ui) {
|
||||
return options_
|
||||
if (!options || !server.config.browser.ui) {
|
||||
return options
|
||||
}
|
||||
const options = options_ as import('webdriverio').ClickOptions
|
||||
if (options.x != null || options.y != null) {
|
||||
const cache = {}
|
||||
if (options.x != null) {
|
||||
@ -164,15 +163,14 @@ function processClickOptions(options_?: UserEventClickOptions) {
|
||||
options.y = scaleCoordinate(options.y, cache)
|
||||
}
|
||||
}
|
||||
return options_
|
||||
return options
|
||||
}
|
||||
|
||||
function processHoverOptions(options_?: UserEventHoverOptions) {
|
||||
function processHoverOptions(options?: UserEventHoverOptions) {
|
||||
// only ui scales the iframe, so we need to adjust the position
|
||||
if (!options_ || !getBrowserState().config.browser.ui) {
|
||||
return options_
|
||||
if (!options || !server.config.browser.ui) {
|
||||
return options
|
||||
}
|
||||
const options = options_ as import('webdriverio').MoveToOptions
|
||||
const cache = {}
|
||||
if (options.xOffset != null) {
|
||||
options.xOffset = scaleCoordinate(options.xOffset, cache)
|
||||
@ -180,21 +178,15 @@ function processHoverOptions(options_?: UserEventHoverOptions) {
|
||||
if (options.yOffset != null) {
|
||||
options.yOffset = scaleCoordinate(options.yOffset, cache)
|
||||
}
|
||||
return options_
|
||||
return options
|
||||
}
|
||||
|
||||
function processDragAndDropOptions(options_?: UserEventDragAndDropOptions) {
|
||||
function processDragAndDropOptions(options?: UserEventDragAndDropOptions) {
|
||||
// only ui scales the iframe, so we need to adjust the position
|
||||
if (!options_ || !getBrowserState().config.browser.ui) {
|
||||
return options_
|
||||
if (!options || !server.config.browser.ui) {
|
||||
return options
|
||||
}
|
||||
const cache = {}
|
||||
const options = options_ as import('webdriverio').DragAndDropOptions & {
|
||||
targetX?: number
|
||||
targetY?: number
|
||||
sourceX?: number
|
||||
sourceY?: number
|
||||
}
|
||||
if (options.sourceX != null) {
|
||||
options.sourceX = scaleCoordinate(options.sourceX, cache)
|
||||
}
|
||||
@ -207,7 +199,7 @@ function processDragAndDropOptions(options_?: UserEventDragAndDropOptions) {
|
||||
if (options.targetY != null) {
|
||||
options.targetY = scaleCoordinate(options.targetY, cache)
|
||||
}
|
||||
return options_
|
||||
return options
|
||||
}
|
||||
|
||||
function scaleCoordinate(coordinate: number, cache: any) {
|
||||
@ -1,39 +1,41 @@
|
||||
import type { Capabilities } from '@wdio/types'
|
||||
import type {
|
||||
ScreenshotComparatorRegistry,
|
||||
ScreenshotMatcherOptions,
|
||||
} from '@vitest/browser/context'
|
||||
import type { Capabilities } from '@wdio/types'
|
||||
} from 'vitest/browser'
|
||||
import type {
|
||||
BrowserCommand,
|
||||
BrowserProvider,
|
||||
BrowserProviderOption,
|
||||
CDPSession,
|
||||
TestProject,
|
||||
} from 'vitest/node'
|
||||
import type { ClickOptions, DragAndDropOptions, remote } from 'webdriverio'
|
||||
import type { ClickOptions, DragAndDropOptions, MoveToOptions, remote } from 'webdriverio'
|
||||
import { defineBrowserProvider } from '@vitest/browser'
|
||||
|
||||
import { resolve } from 'pathe'
|
||||
import { createDebugger } from 'vitest/node'
|
||||
import commands from './commands'
|
||||
import { distRoot } from './constants'
|
||||
|
||||
const debug = createDebugger('vitest:browser:wdio')
|
||||
|
||||
const webdriverBrowsers = ['firefox', 'chrome', 'edge', 'safari'] as const
|
||||
type WebdriverBrowser = (typeof webdriverBrowsers)[number]
|
||||
|
||||
interface WebdriverProviderOptions extends Partial<
|
||||
export interface WebdriverProviderOptions extends Partial<
|
||||
Parameters<typeof remote>[0]
|
||||
> {}
|
||||
|
||||
export function webdriverio(options: WebdriverProviderOptions = {}): BrowserProviderOption<WebdriverProviderOptions> {
|
||||
return {
|
||||
return defineBrowserProvider({
|
||||
name: 'webdriverio',
|
||||
supportedBrowser: webdriverBrowsers,
|
||||
options,
|
||||
factory(project) {
|
||||
providerFactory(project) {
|
||||
return new WebdriverBrowserProvider(project, options)
|
||||
},
|
||||
// --browser.provider=webdriverio
|
||||
// @ts-expect-error hidden way to bypass importing webdriverio
|
||||
_cli: true,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export class WebdriverBrowserProvider implements BrowserProvider {
|
||||
@ -51,6 +53,10 @@ export class WebdriverBrowserProvider implements BrowserProvider {
|
||||
private iframeSwitched = false
|
||||
private topLevelContext: string | undefined
|
||||
|
||||
public initScripts: string[] = [
|
||||
resolve(distRoot, 'locators.js'),
|
||||
]
|
||||
|
||||
getSupportedBrowsers(): readonly string[] {
|
||||
return webdriverBrowsers
|
||||
}
|
||||
@ -69,6 +75,10 @@ export class WebdriverBrowserProvider implements BrowserProvider {
|
||||
this.project = project
|
||||
this.browserName = project.config.browser.name as WebdriverBrowser
|
||||
this.options = options
|
||||
|
||||
for (const [name, command] of Object.entries(commands)) {
|
||||
project.browser!.registerCommand(name as any, command as BrowserCommand)
|
||||
}
|
||||
}
|
||||
|
||||
isIframeSwitched(): boolean {
|
||||
@ -271,20 +281,27 @@ export class WebdriverBrowserProvider implements BrowserProvider {
|
||||
}
|
||||
}
|
||||
|
||||
declare module 'vitest/node' {
|
||||
export interface UserEventClickOptions extends ClickOptions {}
|
||||
declare module 'vitest/browser' {
|
||||
export interface UserEventClickOptions extends Partial<ClickOptions> {}
|
||||
export interface UserEventHoverOptions extends MoveToOptions {}
|
||||
|
||||
export interface UserEventDragOptions extends DragAndDropOptions {
|
||||
export interface UserEventDragAndDropOptions extends DragAndDropOptions {
|
||||
sourceX?: number
|
||||
sourceY?: number
|
||||
targetX?: number
|
||||
targetY?: number
|
||||
}
|
||||
}
|
||||
|
||||
declare module 'vitest/node' {
|
||||
export interface BrowserCommandContext {
|
||||
browser: WebdriverIO.Browser
|
||||
}
|
||||
|
||||
export interface _BrowserNames {
|
||||
webdriverio: WebdriverBrowser
|
||||
}
|
||||
|
||||
export interface ToMatchScreenshotOptions
|
||||
extends Omit<
|
||||
ScreenshotMatcherOptions,
|
||||
13
packages/browser-webdriverio/tsconfig.json
Normal file
13
packages/browser-webdriverio/tsconfig.json
Normal file
@ -0,0 +1,13 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"types": ["node", "vite/client"],
|
||||
"isolatedDeclarations": true
|
||||
},
|
||||
"exclude": [
|
||||
"dist",
|
||||
"node_modules",
|
||||
"**/vite.config.ts",
|
||||
"src/client/**/*.ts"
|
||||
]
|
||||
}
|
||||
59
packages/browser/context.d.ts
vendored
59
packages/browser/context.d.ts
vendored
@ -1,4 +1,5 @@
|
||||
import { SerializedConfig } from 'vitest'
|
||||
import { StringifyOptions } from 'vitest/internal/browser'
|
||||
import { ARIARole } from './aria-role.js'
|
||||
import {} from './matchers.js'
|
||||
|
||||
@ -186,7 +187,7 @@ export interface UserEvent {
|
||||
* state of keyboard to press and release buttons correctly.
|
||||
*
|
||||
* **Note:** Unlike `@testing-library/user-event`, the default `userEvent` instance
|
||||
* from `@vitest/browser/context` is created once, not every time its methods are called!
|
||||
* from `vitest/browser` is created once, not every time its methods are called!
|
||||
* @see {@link https://vitest.dev/guide/browser/interactivity-api.html#userevent-setup}
|
||||
*/
|
||||
setup: () => UserEvent
|
||||
@ -361,6 +362,10 @@ export interface LocatorOptions {
|
||||
* regular expression. Note that exact match still trims whitespace.
|
||||
*/
|
||||
exact?: boolean
|
||||
hasText?: string | RegExp
|
||||
hasNotText?: string | RegExp
|
||||
has?: Locator
|
||||
hasNot?: Locator
|
||||
}
|
||||
|
||||
export interface LocatorByRoleOptions extends LocatorOptions {
|
||||
@ -660,13 +665,6 @@ export const server: {
|
||||
config: SerializedConfig
|
||||
}
|
||||
|
||||
export interface LocatorOptions {
|
||||
hasText?: string | RegExp
|
||||
hasNotText?: string | RegExp
|
||||
has?: Locator
|
||||
hasNot?: Locator
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for user interactions. The support is provided by the browser provider (`playwright` or `webdriverio`).
|
||||
* If used with `preview` provider, fallbacks to simulated events via `@testing-library/user-event`.
|
||||
@ -732,11 +730,54 @@ export interface BrowserPage extends LocatorSelectors {
|
||||
|
||||
export interface BrowserLocators {
|
||||
createElementLocators(element: Element): LocatorSelectors
|
||||
// TODO: enhance docs
|
||||
/**
|
||||
* Extends `page.*` and `locator.*` interfaces.
|
||||
* @see {@link}
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* import { locators } from 'vitest/browser'
|
||||
*
|
||||
* declare module 'vitest/browser' {
|
||||
* interface LocatorSelectors {
|
||||
* getByCSS(css: string): Locator
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* locators.extend({
|
||||
* getByCSS(css: string) {
|
||||
* return `css=${css}`
|
||||
* }
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
extend(methods: {
|
||||
[K in keyof LocatorSelectors]?: (this: BrowserPage | Locator, ...args: Parameters<LocatorSelectors[K]>) => ReturnType<LocatorSelectors[K]> | string
|
||||
[K in keyof LocatorSelectors]?: (
|
||||
this: BrowserPage | Locator,
|
||||
...args: Parameters<LocatorSelectors[K]>
|
||||
) => ReturnType<LocatorSelectors[K]> | string
|
||||
}): void
|
||||
}
|
||||
|
||||
|
||||
export type PrettyDOMOptions = Omit<StringifyOptions, 'maxLength'>
|
||||
|
||||
export const utils: {
|
||||
getElementLocatorSelectors(element: Element): LocatorSelectors
|
||||
debug(
|
||||
el?: Element | Locator | null | (Element | Locator)[],
|
||||
maxLength?: number,
|
||||
options?: PrettyDOMOptions,
|
||||
): void
|
||||
prettyDOM(
|
||||
dom?: Element | Locator | undefined | null,
|
||||
maxLength?: number,
|
||||
prettyFormatOptions?: PrettyDOMOptions,
|
||||
): string
|
||||
getElementError(selector: string, container?: Element): Error
|
||||
}
|
||||
|
||||
export const locators: BrowserLocators
|
||||
|
||||
export const page: BrowserPage
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Vitest resolves "@vitest/browser/context" as a virtual module instead
|
||||
// Vitest resolves "vitest/browser" as a virtual module instead
|
||||
|
||||
// fake exports for static analysis
|
||||
export const page = null
|
||||
@ -7,12 +7,13 @@ export const userEvent = null
|
||||
export const cdp = null
|
||||
export const commands = null
|
||||
export const locators = null
|
||||
export const utils = null
|
||||
|
||||
const pool = globalThis.__vitest_worker__?.ctx?.pool
|
||||
|
||||
throw new Error(
|
||||
// eslint-disable-next-line prefer-template
|
||||
'@vitest/browser/context can be imported only inside the Browser Mode. '
|
||||
'vitest/browser can be imported only inside the Browser Mode. '
|
||||
+ (pool
|
||||
? `Your test is running in ${pool} pool. Make sure your regular tests are excluded from the "test.include" glob pattern.`
|
||||
: 'Instead, it was imported outside of Vitest.'),
|
||||
|
||||
10
packages/browser/jest-dom.d.ts
vendored
10
packages/browser/jest-dom.d.ts
vendored
@ -23,7 +23,7 @@ export interface TestingLibraryMatchers<E, R> {
|
||||
* This matcher calculates the intersection ratio between the element and the viewport, similar to the
|
||||
* IntersectionObserver API.
|
||||
*
|
||||
* The element must be in the document and have visible dimensions. Elements with display: none or
|
||||
* The element must be in the document and have visible dimensions. Elements with display: none or
|
||||
* visibility: hidden are considered not in viewport.
|
||||
* @example
|
||||
* <div
|
||||
@ -34,7 +34,7 @@ export interface TestingLibraryMatchers<E, R> {
|
||||
* </div>
|
||||
*
|
||||
* <div
|
||||
* data-testid="hidden-element"
|
||||
* data-testid="hidden-element"
|
||||
* style="position: fixed; top: -100px; left: 10px; width: 50px; height: 50px;"
|
||||
* >
|
||||
* Hidden Element
|
||||
@ -49,13 +49,13 @@ export interface TestingLibraryMatchers<E, R> {
|
||||
*
|
||||
* // Check if any part of element is in viewport
|
||||
* await expect.element(page.getByTestId('visible-element')).toBeInViewport()
|
||||
*
|
||||
*
|
||||
* // Check if element is outside viewport
|
||||
* await expect.element(page.getByTestId('hidden-element')).not.toBeInViewport()
|
||||
*
|
||||
*
|
||||
* // Check if at least 50% of element is visible
|
||||
* await expect.element(page.getByTestId('large-element')).toBeInViewport({ ratio: 0.5 })
|
||||
*
|
||||
*
|
||||
* // Check if element is completely visible
|
||||
* await expect.element(page.getByTestId('visible-element')).toBeInViewport({ ratio: 1 })
|
||||
* @see https://vitest.dev/guide/browser/assertion-api#tobeinviewport
|
||||
|
||||
2
packages/browser/matchers.d.ts
vendored
2
packages/browser/matchers.d.ts
vendored
@ -1,4 +1,4 @@
|
||||
import type { Locator } from '@vitest/browser/context'
|
||||
import type { Locator } from './context.js'
|
||||
import type { TestingLibraryMatchers } from './jest-dom.js'
|
||||
import type { Assertion, ExpectPollOptions } from 'vitest'
|
||||
|
||||
|
||||
@ -20,10 +20,6 @@
|
||||
"types": "./dist/index.d.ts",
|
||||
"default": "./dist/index.js"
|
||||
},
|
||||
"./providers/*": {
|
||||
"types": "./dist/providers/*.d.ts",
|
||||
"default": "./dist/providers/*.js"
|
||||
},
|
||||
"./context": {
|
||||
"types": "./context.d.ts",
|
||||
"default": "./context.js"
|
||||
@ -35,15 +31,15 @@
|
||||
"types": "./matchers.d.ts",
|
||||
"default": "./dummy.js"
|
||||
},
|
||||
"./locator": {
|
||||
"types": "./dist/locators/index.d.ts",
|
||||
"default": "./dist/locators/index.js"
|
||||
"./locators": {
|
||||
"types": "./dist/locators.d.ts",
|
||||
"default": "./dist/locators.js"
|
||||
},
|
||||
"./utils": {
|
||||
"types": "./utils.d.ts",
|
||||
"default": "./dist/utils.js"
|
||||
},
|
||||
"./*": "./*"
|
||||
"./package.json": "./package.json"
|
||||
},
|
||||
"main": "./dist/index.js",
|
||||
"module": "./dist/index.js",
|
||||
@ -66,24 +62,9 @@
|
||||
"dev": "premove dist && pnpm run --stream '/^dev:/'"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"playwright": "*",
|
||||
"vitest": "workspace:*",
|
||||
"webdriverio": "^7.0.0 || ^8.0.0 || ^9.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"playwright": {
|
||||
"optional": true
|
||||
},
|
||||
"safaridriver": {
|
||||
"optional": true
|
||||
},
|
||||
"webdriverio": {
|
||||
"optional": true
|
||||
}
|
||||
"vitest": "workspace:*"
|
||||
},
|
||||
"dependencies": {
|
||||
"@testing-library/dom": "^10.4.1",
|
||||
"@testing-library/user-event": "^14.6.1",
|
||||
"@vitest/mocker": "workspace:*",
|
||||
"@vitest/utils": "workspace:*",
|
||||
"magic-string": "catalog:",
|
||||
@ -94,18 +75,15 @@
|
||||
"ws": "catalog:"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@testing-library/user-event": "^14.6.1",
|
||||
"@types/pngjs": "^6.0.5",
|
||||
"@types/ws": "catalog:",
|
||||
"@vitest/runner": "workspace:*",
|
||||
"@wdio/types": "^9.19.2",
|
||||
"birpc": "catalog:",
|
||||
"flatted": "catalog:",
|
||||
"ivya": "^1.7.0",
|
||||
"mime": "^4.1.0",
|
||||
"pathe": "catalog:",
|
||||
"playwright": "^1.55.0",
|
||||
"playwright-core": "^1.55.0",
|
||||
"vitest": "workspace:*",
|
||||
"webdriverio": "^9.19.2"
|
||||
"vitest": "workspace:*"
|
||||
}
|
||||
}
|
||||
|
||||
7
packages/browser/providers.d.ts
vendored
7
packages/browser/providers.d.ts
vendored
@ -1,7 +0,0 @@
|
||||
import type { BrowserProviderModule } from 'vitest/node'
|
||||
|
||||
declare const webdriverio: BrowserProviderModule
|
||||
declare const playwright: BrowserProviderModule
|
||||
declare const preview: BrowserProviderModule
|
||||
|
||||
export { webdriverio, playwright, preview }
|
||||
@ -14,10 +14,13 @@ const external = [
|
||||
...Object.keys(pkg.peerDependencies || {}),
|
||||
/^@?vitest(\/|$)/,
|
||||
'@vitest/browser/utils',
|
||||
'@vitest/browser/context',
|
||||
'@vitest/browser/client',
|
||||
'vitest/browser',
|
||||
'worker_threads',
|
||||
'node:worker_threads',
|
||||
'vite',
|
||||
'playwright-core/types/protocol',
|
||||
'vitest/internal/browser',
|
||||
]
|
||||
|
||||
const dtsUtils = createDtsUtils()
|
||||
@ -39,10 +42,7 @@ const plugins = [
|
||||
]
|
||||
|
||||
const input = {
|
||||
'index': './src/node/index.ts',
|
||||
'providers/playwright': './src/node/providers/playwright.ts',
|
||||
'providers/webdriverio': './src/node/providers/webdriverio.ts',
|
||||
'providers/preview': './src/node/providers/preview.ts',
|
||||
index: './src/node/index.ts',
|
||||
}
|
||||
|
||||
export default () =>
|
||||
@ -75,12 +75,8 @@ export default () =>
|
||||
},
|
||||
{
|
||||
input: {
|
||||
'locators/playwright': './src/client/tester/locators/playwright.ts',
|
||||
'locators/webdriverio': './src/client/tester/locators/webdriverio.ts',
|
||||
'locators/preview': './src/client/tester/locators/preview.ts',
|
||||
'locators/index': './src/client/tester/locators/index.ts',
|
||||
'locators': './src/client/tester/locators/index.ts',
|
||||
'expect-element': './src/client/tester/expect-element.ts',
|
||||
'utils': './src/client/tester/public-utils.ts',
|
||||
},
|
||||
output: {
|
||||
dir: 'dist',
|
||||
@ -102,7 +98,7 @@ export default () =>
|
||||
file: 'dist/context.js',
|
||||
format: 'esm',
|
||||
},
|
||||
external: ['@vitest/browser/utils'],
|
||||
external: ['vitest/internal/browser'],
|
||||
plugins: [
|
||||
oxc({
|
||||
transform: { target: 'node18' },
|
||||
@ -150,7 +146,7 @@ export default () =>
|
||||
},
|
||||
{
|
||||
input: dtsUtilsClient.dtsInput({
|
||||
'locators/index': './src/client/tester/locators/index.ts',
|
||||
locators: './src/client/tester/locators/index.ts',
|
||||
}),
|
||||
output: {
|
||||
dir: 'dist',
|
||||
@ -161,17 +157,4 @@ export default () =>
|
||||
external,
|
||||
plugins: dtsUtilsClient.dts(),
|
||||
},
|
||||
// {
|
||||
// input: './src/client/tester/jest-dom.ts',
|
||||
// output: {
|
||||
// file: './jest-dom.d.ts',
|
||||
// format: 'esm',
|
||||
// },
|
||||
// external: [],
|
||||
// plugins: [
|
||||
// dts({
|
||||
// respectExternal: true,
|
||||
// }),
|
||||
// ],
|
||||
// },
|
||||
])
|
||||
|
||||
@ -143,7 +143,7 @@ function createClient() {
|
||||
`Cannot connect to the server in ${connectTimeout / 1000} seconds`,
|
||||
),
|
||||
)
|
||||
}, connectTimeout)?.unref?.()
|
||||
}, connectTimeout)
|
||||
if (ctx.ws.OPEN === ctx.ws.readyState) {
|
||||
resolve()
|
||||
}
|
||||
|
||||
@ -29,6 +29,8 @@ export class IframeOrchestrator {
|
||||
}
|
||||
|
||||
public async createTesters(options: BrowserTesterOptions): Promise<void> {
|
||||
const startTime = performance.now()
|
||||
|
||||
this.cancelled = false
|
||||
|
||||
const config = getConfig()
|
||||
@ -46,7 +48,7 @@ export class IframeOrchestrator {
|
||||
}
|
||||
|
||||
if (config.browser.isolate === false) {
|
||||
await this.runNonIsolatedTests(container, options)
|
||||
await this.runNonIsolatedTests(container, options, startTime)
|
||||
return
|
||||
}
|
||||
|
||||
@ -65,6 +67,7 @@ export class IframeOrchestrator {
|
||||
container,
|
||||
file,
|
||||
options,
|
||||
startTime,
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -95,7 +98,7 @@ export class IframeOrchestrator {
|
||||
this.recreateNonIsolatedIframe = true
|
||||
}
|
||||
|
||||
private async runNonIsolatedTests(container: HTMLDivElement, options: BrowserTesterOptions) {
|
||||
private async runNonIsolatedTests(container: HTMLDivElement, options: BrowserTesterOptions, startTime: number) {
|
||||
if (this.recreateNonIsolatedIframe) {
|
||||
// recreate a new non-isolated iframe during watcher reruns
|
||||
// because we called "cleanup" in the previous run
|
||||
@ -108,7 +111,7 @@ export class IframeOrchestrator {
|
||||
|
||||
if (!this.iframes.has(ID_ALL)) {
|
||||
debug('preparing non-isolated iframe')
|
||||
await this.prepareIframe(container, ID_ALL, options.startTime)
|
||||
await this.prepareIframe(container, ID_ALL, startTime)
|
||||
}
|
||||
|
||||
const config = getConfig()
|
||||
@ -133,6 +136,7 @@ export class IframeOrchestrator {
|
||||
container: HTMLDivElement,
|
||||
file: string,
|
||||
options: BrowserTesterOptions,
|
||||
startTime: number,
|
||||
) {
|
||||
const config = getConfig()
|
||||
const { width, height } = config.browser.viewport
|
||||
@ -142,7 +146,7 @@ export class IframeOrchestrator {
|
||||
this.iframes.delete(file)
|
||||
}
|
||||
|
||||
const iframe = await this.prepareIframe(container, file, options.startTime)
|
||||
const iframe = await this.prepareIframe(container, file, startTime)
|
||||
await setIframeViewport(iframe, width, height)
|
||||
// running tests after the "prepare" event
|
||||
await sendEventToIframe({
|
||||
|
||||
@ -2,19 +2,21 @@ import type {
|
||||
Options as TestingLibraryOptions,
|
||||
UserEvent as TestingLibraryUserEvent,
|
||||
} from '@testing-library/user-event'
|
||||
import type { RunnerTask } from 'vitest'
|
||||
import type {
|
||||
BrowserLocators,
|
||||
BrowserPage,
|
||||
Locator,
|
||||
LocatorSelectors,
|
||||
UserEvent,
|
||||
} from '@vitest/browser/context'
|
||||
import type { RunnerTask } from 'vitest'
|
||||
} from 'vitest/browser'
|
||||
import type { StringifyOptions } from 'vitest/internal/browser'
|
||||
import type { IframeViewportEvent } from '../client'
|
||||
import type { BrowserRunnerState } from '../utils'
|
||||
import type { Locator as LocatorAPI } from './locators/index'
|
||||
import { getElementLocatorSelectors } from '@vitest/browser/utils'
|
||||
import { __INTERNAL, stringify } from 'vitest/internal/browser'
|
||||
import { ensureAwaited, getBrowserState, getWorkerState } from '../utils'
|
||||
import { convertToSelector, processTimeoutOptions } from './utils'
|
||||
import { convertToSelector, processTimeoutOptions } from './tester-utils'
|
||||
|
||||
// this file should not import anything directly, only types and utils
|
||||
|
||||
@ -38,7 +40,7 @@ export function createUserEvent(__tl_user_event_base__?: TestingLibraryUserEvent
|
||||
|
||||
// https://playwright.dev/docs/api/class-keyboard
|
||||
// https://webdriver.io/docs/api/browser/keys/
|
||||
const modifier = provider === `playwright`
|
||||
const modifier = provider === 'playwright'
|
||||
? 'ControlOrMeta'
|
||||
: provider === 'webdriverio'
|
||||
? 'Ctrl'
|
||||
@ -340,9 +342,6 @@ export const page: BrowserPage = {
|
||||
frameLocator() {
|
||||
throw new Error(`Method "frameLocator" is not supported by the "${provider}" provider.`)
|
||||
},
|
||||
_createLocator() {
|
||||
throw new Error(`Method "_createLocator" is not supported by the "${provider}" provider.`)
|
||||
},
|
||||
extend(methods) {
|
||||
for (const key in methods) {
|
||||
(page as any)[key] = (methods as any)[key].bind(page)
|
||||
@ -365,9 +364,9 @@ function getTaskFullName(task: RunnerTask): string {
|
||||
export const locators: BrowserLocators = {
|
||||
createElementLocators: getElementLocatorSelectors,
|
||||
extend(methods) {
|
||||
const Locator = page._createLocator('css=body').constructor as typeof LocatorAPI
|
||||
const Locator = __INTERNAL._createLocator('css=body').constructor as typeof LocatorAPI
|
||||
for (const method in methods) {
|
||||
locators._extendedMethods.add(method)
|
||||
__INTERNAL._extendedMethods.add(method)
|
||||
const cb = (methods as any)[method] as (...args: any[]) => string | Locator
|
||||
// @ts-expect-error types are hard to make work
|
||||
Locator.prototype[method] = function (...args: any[]) {
|
||||
@ -380,23 +379,90 @@ export const locators: BrowserLocators = {
|
||||
page[method as 'getByRole'] = function (...args: any[]) {
|
||||
const selectorOrLocator = cb.call(this, ...args)
|
||||
if (typeof selectorOrLocator === 'string') {
|
||||
return page._createLocator(selectorOrLocator)
|
||||
return __INTERNAL._createLocator(selectorOrLocator)
|
||||
}
|
||||
return selectorOrLocator
|
||||
}
|
||||
}
|
||||
},
|
||||
_extendedMethods: new Set<string>(),
|
||||
}
|
||||
|
||||
declare module '@vitest/browser/context' {
|
||||
interface BrowserPage {
|
||||
/** @internal */
|
||||
_createLocator: (selector: string) => Locator
|
||||
}
|
||||
|
||||
interface BrowserLocators {
|
||||
/** @internal */
|
||||
_extendedMethods: Set<string>
|
||||
function getElementLocatorSelectors(element: Element): LocatorSelectors {
|
||||
const locator = page.elementLocator(element)
|
||||
return {
|
||||
getByAltText: (altText, options) => locator.getByAltText(altText, options),
|
||||
getByLabelText: (labelText, options) => locator.getByLabelText(labelText, options),
|
||||
getByPlaceholder: (placeholderText, options) => locator.getByPlaceholder(placeholderText, options),
|
||||
getByRole: (role, options) => locator.getByRole(role, options),
|
||||
getByTestId: testId => locator.getByTestId(testId),
|
||||
getByText: (text, options) => locator.getByText(text, options),
|
||||
getByTitle: (title, options) => locator.getByTitle(title, options),
|
||||
...Array.from(__INTERNAL._extendedMethods).reduce((methods, method) => {
|
||||
methods[method] = (...args: any[]) => (locator as any)[method](...args)
|
||||
return methods
|
||||
}, {} as any),
|
||||
}
|
||||
}
|
||||
|
||||
type PrettyDOMOptions = Omit<StringifyOptions, 'maxLength'>
|
||||
|
||||
function debug(
|
||||
el?: Element | Locator | null | (Element | Locator)[],
|
||||
maxLength?: number,
|
||||
options?: PrettyDOMOptions,
|
||||
): void {
|
||||
if (Array.isArray(el)) {
|
||||
// eslint-disable-next-line no-console
|
||||
el.forEach(e => console.log(prettyDOM(e, maxLength, options)))
|
||||
}
|
||||
else {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(prettyDOM(el, maxLength, options))
|
||||
}
|
||||
}
|
||||
|
||||
function prettyDOM(
|
||||
dom?: Element | Locator | undefined | null,
|
||||
maxLength: number = Number(import.meta.env.DEBUG_PRINT_LIMIT ?? 7000),
|
||||
prettyFormatOptions: PrettyDOMOptions = {},
|
||||
): string {
|
||||
if (maxLength === 0) {
|
||||
return ''
|
||||
}
|
||||
|
||||
if (!dom) {
|
||||
dom = document.body
|
||||
}
|
||||
|
||||
if ('element' in dom && 'all' in dom) {
|
||||
dom = dom.element()
|
||||
}
|
||||
|
||||
const type = typeof dom
|
||||
if (type !== 'object' || !dom.outerHTML) {
|
||||
const typeName = type === 'object' ? dom.constructor.name : type
|
||||
throw new TypeError(`Expecting a valid DOM element, but got ${typeName}.`)
|
||||
}
|
||||
|
||||
const pretty = stringify(dom, Number.POSITIVE_INFINITY, {
|
||||
maxLength,
|
||||
highlight: true,
|
||||
...prettyFormatOptions,
|
||||
})
|
||||
return dom.outerHTML.length > maxLength
|
||||
? `${pretty.slice(0, maxLength)}...`
|
||||
: pretty
|
||||
}
|
||||
|
||||
function getElementError(selector: string, container: Element): Error {
|
||||
const error = new Error(`Cannot find element with locator: ${__INTERNAL._asLocator('javascript', selector)}\n\n${prettyDOM(container)}`)
|
||||
error.name = 'VitestBrowserElementError'
|
||||
return error
|
||||
}
|
||||
|
||||
export const utils = {
|
||||
getElementError,
|
||||
prettyDOM,
|
||||
debug,
|
||||
getElementLocatorSelectors,
|
||||
}
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
import type { Locator } from '@vitest/browser/context'
|
||||
import type { ExpectPollOptions, PromisifyDomAssertion } from 'vitest'
|
||||
import type { Locator } from 'vitest/browser'
|
||||
import { chai, expect } from 'vitest'
|
||||
import { getType } from 'vitest/internal/browser'
|
||||
import { matchers } from './expect'
|
||||
import { processTimeoutOptions } from './utils'
|
||||
import { processTimeoutOptions } from './tester-utils'
|
||||
|
||||
const kLocator = Symbol.for('$$vitest:locator')
|
||||
|
||||
|
||||
@ -15,8 +15,8 @@
|
||||
|
||||
import type { ExpectationResult, MatcherState } from '@vitest/expect'
|
||||
import type { Locator } from '../locators'
|
||||
import { server } from '@vitest/browser/context'
|
||||
import { beginAriaCaches, endAriaCaches, isElementVisible as ivyaIsVisible } from 'ivya/utils'
|
||||
import { server } from 'vitest/browser'
|
||||
import { getElementFromUserInput } from './utils'
|
||||
|
||||
export default function toBeVisible(
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import type { ExpectationResult, MatcherState } from '@vitest/expect'
|
||||
import type { Locator } from '../locators'
|
||||
import { server } from '@vitest/browser/context'
|
||||
import { server } from 'vitest/browser'
|
||||
import { getElementFromUserInput } from './utils'
|
||||
|
||||
const browser = server.config.browser.name
|
||||
|
||||
@ -3,7 +3,7 @@ import type { ScreenshotMatcherOptions } from '../../../../context'
|
||||
import type { ScreenshotMatcherArguments, ScreenshotMatcherOutput } from '../../../shared/screenshotMatcher/types'
|
||||
import type { Locator } from '../locators'
|
||||
import { getBrowserState, getWorkerState } from '../../utils'
|
||||
import { convertToSelector } from '../utils'
|
||||
import { convertToSelector } from '../tester-utils'
|
||||
|
||||
const counters = new Map<string, { current: number }>([])
|
||||
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import type { ParsedSelector } from 'ivya'
|
||||
import type {
|
||||
LocatorByRoleOptions,
|
||||
LocatorOptions,
|
||||
@ -9,10 +10,9 @@ import type {
|
||||
UserEventHoverOptions,
|
||||
UserEventSelectOptions,
|
||||
UserEventUploadOptions,
|
||||
} from '@vitest/browser/context'
|
||||
import type { ParsedSelector } from 'ivya'
|
||||
import { page, server } from '@vitest/browser/context'
|
||||
} from 'vitest/browser'
|
||||
import {
|
||||
asLocator,
|
||||
getByAltTextSelector,
|
||||
getByLabelSelector,
|
||||
getByPlaceholderSelector,
|
||||
@ -21,11 +21,24 @@ import {
|
||||
getByTextSelector,
|
||||
getByTitleSelector,
|
||||
Ivya,
|
||||
|
||||
} from 'ivya'
|
||||
import { page, server, utils } from 'vitest/browser'
|
||||
import { __INTERNAL } from 'vitest/internal/browser'
|
||||
import { ensureAwaited, getBrowserState } from '../../utils'
|
||||
import { getElementError } from '../public-utils'
|
||||
import { escapeForTextSelector } from '../utils'
|
||||
import { escapeForTextSelector, isLocator } from '../tester-utils'
|
||||
|
||||
export { convertElementToCssSelector, getIframeScale, processTimeoutOptions } from '../tester-utils'
|
||||
export {
|
||||
getByAltTextSelector,
|
||||
getByLabelSelector,
|
||||
getByPlaceholderSelector,
|
||||
getByRoleSelector,
|
||||
getByTestIdSelector,
|
||||
getByTextSelector,
|
||||
getByTitleSelector,
|
||||
} from 'ivya'
|
||||
|
||||
__INTERNAL._asLocator = asLocator
|
||||
|
||||
// we prefer using playwright locators because they are more powerful and support Shadow DOM
|
||||
export const selectorEngine: Ivya = Ivya.create({
|
||||
@ -125,7 +138,7 @@ export abstract class Locator {
|
||||
): Promise<void> {
|
||||
const values = (Array.isArray(value) ? value : [value]).map((v) => {
|
||||
if (typeof v !== 'string') {
|
||||
const selector = 'element' in v ? v.selector : selectorEngine.generateSelectorSimple(v)
|
||||
const selector = isLocator(v) ? v.selector : selectorEngine.generateSelectorSimple(v)
|
||||
return { element: selector }
|
||||
}
|
||||
return v
|
||||
@ -223,7 +236,7 @@ export abstract class Locator {
|
||||
public element(): HTMLElement | SVGElement {
|
||||
const element = this.query()
|
||||
if (!element) {
|
||||
throw getElementError(this._pwSelector || this.selector, this._container || document.body)
|
||||
throw utils.getElementError(this._pwSelector || this.selector, this._container || document.body)
|
||||
}
|
||||
return element
|
||||
}
|
||||
|
||||
@ -1,78 +0,0 @@
|
||||
import type { Locator, LocatorSelectors } from '@vitest/browser/context'
|
||||
import type { StringifyOptions } from 'vitest/internal/browser'
|
||||
import { locators, page } from '@vitest/browser/context'
|
||||
import { asLocator } from 'ivya'
|
||||
import { stringify } from 'vitest/internal/browser'
|
||||
|
||||
export function getElementLocatorSelectors(element: Element): LocatorSelectors {
|
||||
const locator = page.elementLocator(element)
|
||||
return {
|
||||
getByAltText: (altText, options) => locator.getByAltText(altText, options),
|
||||
getByLabelText: (labelText, options) => locator.getByLabelText(labelText, options),
|
||||
getByPlaceholder: (placeholderText, options) => locator.getByPlaceholder(placeholderText, options),
|
||||
getByRole: (role, options) => locator.getByRole(role, options),
|
||||
getByTestId: testId => locator.getByTestId(testId),
|
||||
getByText: (text, options) => locator.getByText(text, options),
|
||||
getByTitle: (title, options) => locator.getByTitle(title, options),
|
||||
...Array.from(locators._extendedMethods).reduce((methods, method) => {
|
||||
methods[method] = (...args: any[]) => (locator as any)[method](...args)
|
||||
return methods
|
||||
}, {} as any),
|
||||
}
|
||||
}
|
||||
|
||||
type PrettyDOMOptions = Omit<StringifyOptions, 'maxLength'>
|
||||
|
||||
export function debug(
|
||||
el?: Element | Locator | null | (Element | Locator)[],
|
||||
maxLength?: number,
|
||||
options?: PrettyDOMOptions,
|
||||
): void {
|
||||
if (Array.isArray(el)) {
|
||||
// eslint-disable-next-line no-console
|
||||
el.forEach(e => console.log(prettyDOM(e, maxLength, options)))
|
||||
}
|
||||
else {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(prettyDOM(el, maxLength, options))
|
||||
}
|
||||
}
|
||||
|
||||
export function prettyDOM(
|
||||
dom?: Element | Locator | undefined | null,
|
||||
maxLength: number = Number(import.meta.env.DEBUG_PRINT_LIMIT ?? 7000),
|
||||
prettyFormatOptions: PrettyDOMOptions = {},
|
||||
): string {
|
||||
if (maxLength === 0) {
|
||||
return ''
|
||||
}
|
||||
|
||||
if (!dom) {
|
||||
dom = document.body
|
||||
}
|
||||
|
||||
if ('element' in dom && 'all' in dom) {
|
||||
dom = dom.element()
|
||||
}
|
||||
|
||||
const type = typeof dom
|
||||
if (type !== 'object' || !dom.outerHTML) {
|
||||
const typeName = type === 'object' ? dom.constructor.name : type
|
||||
throw new TypeError(`Expecting a valid DOM element, but got ${typeName}.`)
|
||||
}
|
||||
|
||||
const pretty = stringify(dom, Number.POSITIVE_INFINITY, {
|
||||
maxLength,
|
||||
highlight: true,
|
||||
...prettyFormatOptions,
|
||||
})
|
||||
return dom.outerHTML.length > maxLength
|
||||
? `${pretty.slice(0, maxLength)}...`
|
||||
: pretty
|
||||
}
|
||||
|
||||
export function getElementError(selector: string, container: Element): Error {
|
||||
const error = new Error(`Cannot find element with locator: ${asLocator('javascript', selector)}\n\n${prettyDOM(container)}`)
|
||||
error.name = 'VitestBrowserElementError'
|
||||
return error
|
||||
}
|
||||
@ -11,10 +11,10 @@ import type {
|
||||
} from '@vitest/runner'
|
||||
import type { SerializedConfig, TestExecutionMethod, WorkerGlobalState } from 'vitest'
|
||||
import type { VitestBrowserClientMocker } from './mocker'
|
||||
import type { CommandsManager } from './utils'
|
||||
import type { CommandsManager } from './tester-utils'
|
||||
import { globalChannel, onCancel } from '@vitest/browser/client'
|
||||
import { page, userEvent } from '@vitest/browser/context'
|
||||
import { getTestName } from '@vitest/runner/utils'
|
||||
import { page, userEvent } from 'vitest/browser'
|
||||
import {
|
||||
DecodedMap,
|
||||
getOriginalPosition,
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import type { Locator } from '@vitest/browser/context'
|
||||
import type { Locator } from 'vitest/browser'
|
||||
import type { BrowserRPC } from '../client'
|
||||
import { getBrowserState, getWorkerState } from '../utils'
|
||||
|
||||
@ -206,8 +206,14 @@ export function convertToSelector(elementOrLocator: Element | Locator): string {
|
||||
if (elementOrLocator instanceof Element) {
|
||||
return convertElementToCssSelector(elementOrLocator)
|
||||
}
|
||||
if ('selector' in elementOrLocator) {
|
||||
return (elementOrLocator as any).selector
|
||||
if (isLocator(elementOrLocator)) {
|
||||
return elementOrLocator.selector
|
||||
}
|
||||
throw new Error('Expected element or locator to be an instance of Element or Locator.')
|
||||
}
|
||||
|
||||
const kLocator = Symbol.for('$$vitest:locator')
|
||||
|
||||
export function isLocator(element: unknown): element is Locator {
|
||||
return (!!element && typeof element === 'object' && kLocator in element)
|
||||
}
|
||||
@ -1,7 +1,7 @@
|
||||
import type { BrowserRPC, IframeChannelEvent } from '@vitest/browser/client'
|
||||
import { channel, client, onCancel } from '@vitest/browser/client'
|
||||
import { page, server, userEvent } from '@vitest/browser/context'
|
||||
import { parse } from 'flatted'
|
||||
import { page, server, userEvent } from 'vitest/browser'
|
||||
import {
|
||||
collectTests,
|
||||
setupCommonEnv,
|
||||
@ -17,7 +17,7 @@ import { VitestBrowserClientMocker } from './mocker'
|
||||
import { createModuleMockerInterceptor } from './mocker-interceptor'
|
||||
import { createSafeRpc } from './rpc'
|
||||
import { browserHashMap, initiateRunner } from './runner'
|
||||
import { CommandsManager } from './utils'
|
||||
import { CommandsManager } from './tester-utils'
|
||||
|
||||
const debugVar = getConfig().env.VITEST_BROWSER_DEBUG
|
||||
const debug = debugVar && debugVar !== 'false'
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import type { VitestRunner } from '@vitest/runner'
|
||||
import type { EvaluatedModules, SerializedConfig, WorkerGlobalState } from 'vitest'
|
||||
import type { IframeOrchestrator } from './orchestrator'
|
||||
import type { CommandsManager } from './tester/utils'
|
||||
import type { CommandsManager } from './tester/tester-utils'
|
||||
|
||||
export async function importId(id: string): Promise<any> {
|
||||
const name = `/@id/${id}`.replace(/\\/g, '/')
|
||||
|
||||
@ -35,7 +35,7 @@ export default vite.defineConfig({
|
||||
/^vitest\//,
|
||||
'vitest',
|
||||
/^msw/,
|
||||
'@vitest/browser/context',
|
||||
'vitest/browser',
|
||||
'@vitest/browser/client',
|
||||
],
|
||||
},
|
||||
|
||||
@ -1,23 +0,0 @@
|
||||
import type { UserEvent } from '../../../context'
|
||||
import type { UserEventCommand } from './utils'
|
||||
import { PlaywrightBrowserProvider } from '../providers/playwright'
|
||||
import { WebdriverBrowserProvider } from '../providers/webdriverio'
|
||||
|
||||
export const clear: UserEventCommand<UserEvent['clear']> = async (
|
||||
context,
|
||||
selector,
|
||||
) => {
|
||||
if (context.provider instanceof PlaywrightBrowserProvider) {
|
||||
const { iframe } = context
|
||||
const element = iframe.locator(selector)
|
||||
await element.clear()
|
||||
}
|
||||
else if (context.provider instanceof WebdriverBrowserProvider) {
|
||||
const browser = context.browser
|
||||
const element = await browser.$(selector)
|
||||
await element.clearValue()
|
||||
}
|
||||
else {
|
||||
throw new TypeError(`Provider "${context.provider.name}" does not support clearing elements`)
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user