diff --git a/docs/config/index.md b/docs/config/index.md
index a092e3a28853..eee75fb48c71 100644
--- a/docs/config/index.md
+++ b/docs/config/index.md
@@ -407,6 +407,10 @@ browser-like environment through either [`jsdom`](https://github.com/jsdom/jsdom
or [`happy-dom`](https://github.com/capricorn86/happy-dom) instead.
If you are building edge functions, you can use [`edge-runtime`](https://edge-runtime.vercel.app/packages/vm) environment
+::: tip
+You can also use [Browser Mode](/guide/browser) to run integration or unit tests in the browser without mocking the environment.
+:::
+
By adding a `@vitest-environment` docblock or comment at the top of the file,
you can specify another environment to be used for all tests in that file:
diff --git a/docs/guide/browser.md b/docs/guide/browser.md
index 098759b91672..e7163737057a 100644
--- a/docs/guide/browser.md
+++ b/docs/guide/browser.md
@@ -79,6 +79,112 @@ export default defineConfig({
})
```
+If you have not used Vite before, make sure you have your framework's plugin installed and specified in the config. Some frameworks might require extra configuration to work - check their Vite related documentation to be sure.
+
+::: code-group
+```ts [vue]
+import { defineConfig } from 'vitest/config'
+import vue from '@vitejs/plugin-vue'
+
+export default defineConfig({
+ plugins: [vue()],
+ test: {
+ browser: {
+ enabled: true,
+ provider: 'playwright',
+ name: 'chrome',
+ }
+ }
+})
+```
+```ts [svelte]
+import { defineConfig } from 'vitest/config'
+import { svelte } from '@sveltejs/vite-plugin-svelte'
+
+export default defineConfig({
+ plugins: [svelte()],
+ test: {
+ browser: {
+ enabled: true,
+ provider: 'playwright',
+ name: 'chrome',
+ }
+ }
+})
+```
+```ts [solid]
+import { defineConfig } from 'vitest/config'
+import solidPlugin from 'vite-plugin-solid'
+
+export default defineConfig({
+ plugins: [solidPlugin()],
+ test: {
+ browser: {
+ enabled: true,
+ provider: 'playwright',
+ name: 'chrome',
+ }
+ }
+})
+```
+```ts [marko]
+import { defineConfig } from 'vitest/config'
+import marko from '@marko/vite'
+
+export default defineConfig({
+ plugins: [marko()],
+ test: {
+ browser: {
+ enabled: true,
+ provider: 'playwright',
+ name: 'chrome',
+ }
+ }
+})
+```
+:::
+
+::: tip
+`react` doesn't require a plugin to work, but `preact` requires [extra configuration](https://preactjs.com/guide/v10/getting-started/#create-a-vite-powered-preact-app) to make aliases work.
+:::
+
+If you need to run some tests using Node-based runner, you can define a [workspace](/guide/workspace) file with separate configurations for different testing strategies:
+
+```ts
+// vitest.workspace.ts
+import { defineWorkspace } from 'vitest/config'
+
+export default defineWorkspace([
+ {
+ test: {
+ // an example of file based convention,
+ // you don't have to follow it
+ include: [
+ 'tests/unit/**/*.{test,spec}.ts',
+ 'tests/**/*.unit.{test,spec}.ts',
+ ],
+ name: 'unit',
+ environment: 'node',
+ },
+ },
+ {
+ test: {
+ // an example of file based convention,
+ // you don't have to follow it
+ include: [
+ 'tests/browser/**/*.{test,spec}.ts',
+ 'tests/**/*.browser.{test,spec}.ts',
+ ],
+ name: 'browser',
+ browser: {
+ enabled: true,
+ name: 'chrome',
+ },
+ },
+ },
+])
+```
+
## Browser Option Types
The browser option in Vitest depends on the provider. Vitest will fail, if you pass `--browser` and don't specify its name in the config file. Available options:
@@ -176,6 +282,102 @@ In this case, Vitest will run in headless mode using the Chrome browser.
Headless mode is not available by default. You need to use either [`playwright`](https://npmjs.com/package/playwright) or [`webdriverio`](https://www.npmjs.com/package/webdriverio) providers to enable this feature.
:::
+## Assertion API
+
+Vitest bundles [`@testing-library/jest-dom`](https://github.com/testing-library/jest-dom) library to provide a wide range of DOM assertions out of the box. For detailed documentation, you can read the `jest-dom` readme:
+
+- [`toBeDisabled`](https://github.com/testing-library/jest-dom#toBeDisabled)
+- [`toBeEnabled`](https://github.com/testing-library/jest-dom#toBeEnabled)
+- [`toBeEmptyDOMElement`](https://github.com/testing-library/jest-dom#toBeEmptyDOMElement)
+- [`toBeInTheDocument`](https://github.com/testing-library/jest-dom#toBeInTheDocument)
+- [`toBeInvalid`](https://github.com/testing-library/jest-dom#toBeInvalid)
+- [`toBeRequired`](https://github.com/testing-library/jest-dom#toBeRequired)
+- [`toBeValid`](https://github.com/testing-library/jest-dom#toBeValid)
+- [`toBeVisible`](https://github.com/testing-library/jest-dom#toBeVisible)
+- [`toContainElement`](https://github.com/testing-library/jest-dom#toContainElement)
+- [`toContainHTML`](https://github.com/testing-library/jest-dom#toContainHTML)
+- [`toHaveAccessibleDescription`](https://github.com/testing-library/jest-dom#toHaveAccessibleDescription)
+- [`toHaveAccessibleErrorMessage`](https://github.com/testing-library/jest-dom#toHaveAccessibleErrorMessage)
+- [`toHaveAccessibleName`](https://github.com/testing-library/jest-dom#toHaveAccessibleName)
+- [`toHaveAttribute`](https://github.com/testing-library/jest-dom#toHaveAttribute)
+- [`toHaveClass`](https://github.com/testing-library/jest-dom#toHaveClass)
+- [`toHaveFocus`](https://github.com/testing-library/jest-dom#toHaveFocus)
+- [`toHaveFormValues`](https://github.com/testing-library/jest-dom#toHaveFormValues)
+- [`toHaveStyle`](https://github.com/testing-library/jest-dom#toHaveStyle)
+- [`toHaveTextContent`](https://github.com/testing-library/jest-dom#toHaveTextContent)
+- [`toHaveValue`](https://github.com/testing-library/jest-dom#toHaveValue)
+- [`toHaveDisplayValue`](https://github.com/testing-library/jest-dom#toHaveDisplayValue)
+- [`toBeChecked`](https://github.com/testing-library/jest-dom#toBeChecked)
+- [`toBePartiallyChecked`](https://github.com/testing-library/jest-dom#toBePartiallyChecked)
+- [`toHaveRole`](https://github.com/testing-library/jest-dom#toHaveRole)
+- [`toHaveErrorMessage`](https://github.com/testing-library/jest-dom#toHaveErrorMessage)
+
+If you are using TypeScript or want to have correct type hints in `expect`, make sure you have either `@vitest/browser/providers/playwright` or `@vitest/browser/providers/webdriverio` specified in your `tsconfig` depending on the provider you use. If you use the default `preview` provider, you can specify `@vitest/browser/matchers` instead.
+
+::: code-group
+```json [preview]
+{
+ "compilerOptions": {
+ "types": [
+ "@vitest/browser/matchers"
+ ]
+ }
+}
+```
+```json [playwright]
+{
+ "compilerOptions": {
+ "types": [
+ "@vitest/browser/providers/playwright"
+ ]
+ }
+}
+```
+```json [webdriverio]
+{
+ "compilerOptions": {
+ "types": [
+ "@vitest/browser/providers/webdriverio"
+ ]
+ }
+}
+```
+:::
+
+## Retry-ability
+
+Tests in the browser might fail inconsistently due to their asynchronous nature. Because of this, it is important to have a way to guarantee that assertions succeed even if the condition is delayed (by a timeout, network request, or animation, for example). For this purpose, Vitest provides retriable assertions out of the box via the [`expect.poll`](/api/expect#poll) and `expect.element` APIs:
+
+```ts
+import { expect, test } from 'vitest'
+import { screen } from '@testing-library/dom'
+
+test('error banner is rendered', async () => {
+ triggerError()
+
+ // @testing-library provides queries with built-in retry-ability
+ // It will try to find the banner until it's rendered
+ const banner = await screen.findByRole('alert', {
+ name: /error/i,
+ })
+
+ // Vitest provides `expect.element` with built-in retry-ability
+ // It will check `element.textContent` until it's equal to "Error!"
+ await expect.element(banner).toHaveTextContent('Error!')
+})
+```
+
+::: tip
+`expect.element` is a shorthand for `expect.poll(() => element)` and works in exactly the same way.
+
+`toHaveTextContent` and all other [`@testing-library/jest-dom`](https://github.com/testing-library/jest-dom) assertions are still available on a regular `expect` without a built-in retry-ability mechanism:
+
+```ts
+// will fail immediately if .textContent is not `'Error!'`
+expect(banner).toHaveTextContent('Error!')
+```
+:::
+
## Context
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.
@@ -207,111 +409,26 @@ export const server: {
}
/**
- * Handler for user interactions. The support is provided by the browser provider (`playwright` or `webdriverio`).
+ * Handler for user interactions. The support is implemented by the browser provider (`playwright` or `webdriverio`).
* If used with `preview` provider, fallbacks to simulated events via `@testing-library/user-event`.
* @experimental
*/
export const userEvent: {
setup: () => UserEvent
- /**
- * Click on an element. Uses provider's API under the hood and supports all its options.
- * @see {@link https://playwright.dev/docs/api/class-locator#locator-click} Playwright API
- * @see {@link https://webdriver.io/docs/api/element/click/} WebdriverIO API
- * @see {@link https://testing-library.com/docs/user-event/convenience/#click} testing-library API
- */
click: (element: Element, options?: UserEventClickOptions) => Promise
- /**
- * Triggers a double click event on an element. Uses provider's API under the hood.
- * @see {@link https://playwright.dev/docs/api/class-locator#locator-dblclick} Playwright API
- * @see {@link https://webdriver.io/docs/api/element/doubleClick/} WebdriverIO API
- * @see {@link https://testing-library.com/docs/user-event/convenience/#dblClick} testing-library API
- */
dblClick: (element: Element, options?: UserEventDoubleClickOptions) => Promise
- /**
- * Choose one or more values from a select element. Uses provider's API under the hood.
- * If select doesn't have `multiple` attribute, only the first value will be selected.
- * @see {@link https://playwright.dev/docs/api/class-locator#locator-select-option} Playwright API
- * @see {@link https://webdriver.io/docs/api/element/doubleClick/} WebdriverIO API
- * @see {@link https://testing-library.com/docs/user-event/utility/#-selectoptions-deselectoptions} testing-library API
- */
selectOptions: (
element: Element,
values: HTMLElement | HTMLElement[] | string | string[],
options?: UserEventSelectOptions,
) => Promise
- /**
- * Type text on the keyboard. If any input is focused, it will receive the text,
- * otherwise it will be typed on the document. Uses provider's API under the hood.
- * **Supports** [user-event `keyboard` syntax](https://testing-library.com/docs/user-event/keyboard) (e.g., `{Shift}`) even with `playwright` and `webdriverio` providers.
- * @example
- * await userEvent.keyboard('foo') // translates to: f, o, o
- * await userEvent.keyboard('{{a[[') // translates to: {, a, [
- * await userEvent.keyboard('{Shift}{f}{o}{o}') // translates to: Shift, f, o, o
- * @see {@link https://playwright.dev/docs/api/class-locator#locator-press} Playwright API
- * @see {@link https://webdriver.io/docs/api/browser/action#key-input-source} WebdriverIO API
- * @see {@link https://testing-library.com/docs/user-event/keyboard} testing-library API
- */
keyboard: (text: string) => Promise
- /**
- * Types text into an element. Uses provider's API under the hood.
- * **Supports** [user-event `keyboard` syntax](https://testing-library.com/docs/user-event/keyboard) (e.g., `{Shift}`) even with `playwright` and `webdriverio` providers.
- * @example
- * await userEvent.type(input, 'foo') // translates to: f, o, o
- * await userEvent.type(input, '{{a[[') // translates to: {, a, [
- * await userEvent.type(input, '{Shift}{f}{o}{o}') // translates to: Shift, f, o, o
- * @see {@link https://playwright.dev/docs/api/class-locator#locator-press} Playwright API
- * @see {@link https://webdriver.io/docs/api/browser/action#key-input-source} WebdriverIO API
- * @see {@link https://testing-library.com/docs/user-event/utility/#type} testing-library API
- */
type: (element: Element, text: string, options?: UserEventTypeOptions) => Promise
- /**
- * Removes all text from an element. Uses provider's API under the hood.
- * @see {@link https://playwright.dev/docs/api/class-locator#locator-clear} Playwright API
- * @see {@link https://webdriver.io/docs/api/element/clearValue} WebdriverIO API
- * @see {@link https://testing-library.com/docs/user-event/utility/#clear} testing-library API
- */
clear: (element: Element) => Promise
- /**
- * Sends a `Tab` key event. Uses provider's API under the hood.
- * @see {@link https://playwright.dev/docs/api/class-locator#locator-press} Playwright API
- * @see {@link https://webdriver.io/docs/api/element/keys} WebdriverIO API
- * @see {@link https://testing-library.com/docs/user-event/convenience/#tab} testing-library API
- */
tab: (options?: UserEventTabOptions) => Promise
- /**
- * Hovers over an element. Uses provider's API under the hood.
- * @see {@link https://playwright.dev/docs/api/class-locator#locator-hover} Playwright API
- * @see {@link https://webdriver.io/docs/api/element/moveTo/} WebdriverIO API
- * @see {@link https://testing-library.com/docs/user-event/convenience/#hover} testing-library API
- */
hover: (element: Element, options?: UserEventHoverOptions) => Promise
- /**
- * Moves cursor position to the body element. Uses provider's API under the hood.
- * By default, the cursor position is in the center (in webdriverio) or in some visible place (in playwright)
- * of the body element, so if the current element is already there, this will have no effect.
- * @see {@link https://playwright.dev/docs/api/class-locator#locator-hover} Playwright API
- * @see {@link https://webdriver.io/docs/api/element/moveTo/} WebdriverIO API
- * @see {@link https://testing-library.com/docs/user-event/convenience/#hover} testing-library API
- */
unhover: (element: Element, options?: UserEventHoverOptions) => Promise
- /**
- * Fills an input element with text. This will remove any existing text in the input before typing the new value.
- * Uses provider's API under the hood.
- * This API is faster than using `userEvent.type` or `userEvent.keyboard`, but it **doesn't support** [user-event `keyboard` syntax](https://testing-library.com/docs/user-event/keyboard) (e.g., `{Shift}`).
- * @example
- * await userEvent.fill(input, 'foo') // translates to: f, o, o
- * await userEvent.fill(input, '{{a[[') // translates to: {, {, a, [, [
- * await userEvent.fill(input, '{Shift}') // translates to: {, S, h, i, f, t, }
- * @see {@link https://playwright.dev/docs/api/class-locator#locator-fill} Playwright API
- * @see {@link https://webdriver.io/docs/api/element/setValue} WebdriverIO API
- * @see {@link https://testing-library.com/docs/user-event/utility/#type} testing-library API
- */
fill: (element: Element, text: string, options?: UserEventFillOptions) => Promise
- /**
- * Drags a source element on top of the target element. This API is not supported by "preview" provider.
- * @see {@link https://playwright.dev/docs/api/class-frame#frame-drag-and-drop} Playwright API
- * @see {@link https://webdriver.io/docs/api/element/dragAndDrop/} WebdriverIO API
- */
dragAndDrop: (source: Element, target: Element, options?: UserEventDragAndDropOptions) => Promise
}
@@ -480,9 +597,8 @@ Sends a `Tab` key event. This is a shorthand for `userEvent.keyboard('{tab}')`.
```ts
import { userEvent } from '@vitest/browser/context'
import { screen } from '@testing-library/dom'
-import '@testing-library/jest-dom' // adds support for "toHaveFocus"
-test('tab works', () => {
+test('tab works', async () => {
const [input1, input2] = screen.getAllByRole('input')
expect(input1).toHaveFocus()
@@ -545,7 +661,6 @@ This method clear the input element content.
```ts
import { userEvent } from '@vitest/browser/context'
import { screen } from '@testing-library/dom'
-import '@testing-library/jest-dom' // adds support for "toHaveValue"
test('clears input', () => {
const input = screen.getByRole('input')
@@ -579,7 +694,6 @@ Unlike `@testing-library`, Vitest doesn't support [listbox](https://developer.mo
```ts
import { userEvent } from '@vitest/browser/context'
import { screen } from '@testing-library/dom'
-import '@testing-library/jest-dom' // adds support for "toHaveValue"
test('clears input', () => {
const select = screen.getByRole('select')
@@ -847,6 +961,161 @@ If you are using TypeScript, don't forget to add `@vitest/browser/providers/webd
```
:::
+## Examples
+
+Browser Mode is framework agnostic so it doesn't provide any method to render your components. However, you should be able to use your framework's test utils packages.
+
+We recommend using `testing-library` packages depending on your framework:
+
+- [`@testing-library/dom`](https://testing-library.com/docs/dom-testing-library/intro) if you don't use a framework
+- [`@testing-library/vue`](https://testing-library.com/docs/vue-testing-library/intro) to render [vue](https://vuejs.org) components
+- [`@testing-library/svelte`](https://testing-library.com/docs/svelte-testing-library/intro) to render [svelte](https://svelte.dev) components
+- [`@testing-library/react`](https://testing-library.com/docs/react-testing-library/intro) to render [react](https://react.dev) components
+- [`@testing-library/preact`](https://testing-library.com/docs/preact-testing-library/intro) to render [preact](https://preactjs.com) components
+- [`@testing-library/solid`](https://testing-library.com/docs/solid-testing-library/intro) to render [solid](https://www.solidjs.com) components
+- [`@marko/testing-library`](https://testing-library.com/docs/marko-testing-library/intro) to render [marko](https://markojs.com) components
+
+::: 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`](#interactivity-api) imported from `@vitest/browser/context` that uses Chrome DevTools Protocol or Webdriver (depending on the provider) under the hood.
+:::
+
+::: code-group
+```ts [vue]
+// based on @testing-library/vue example
+// https://testing-library.com/docs/vue-testing-library/examples
+
+import { userEvent } from '@vitest/browser/context'
+import { render, screen } from '@testing-library/vue'
+import Component from './Component.vue'
+
+test('properly handles v-model', async () => {
+ render(Component)
+
+ // Asserts initial state.
+ expect(screen.getByText('Hi, my name is Alice')).toBeInTheDocument()
+
+ // Get the input DOM node by querying the associated label.
+ const usernameInput = await screen.findByLabelText(/username/i)
+
+ // Type the name into the input. This already validates that the input
+ // is filled correctly, no need to check the value manually.
+ await userEvent.fill(usernameInput, 'Bob')
+
+ expect(screen.getByText('Hi, my name is Alice')).toBeInTheDocument()
+})
+```
+```ts [svelte]
+// based on @testing-library/svelte
+// https://testing-library.com/docs/svelte-testing-library/example
+
+import { render, screen } from '@testing-library/svelte'
+import { userEvent } from '@vitest/browser/context'
+import { expect, test } from 'vitest'
+
+import Greeter from './greeter.svelte'
+
+test('greeting appears on click', async () => {
+ const user = userEvent.setup()
+ render(Greeter, { name: 'World' })
+
+ const button = screen.getByRole('button')
+ await user.click(button)
+ const greeting = await screen.findByText(/hello world/iu)
+
+ expect(greeting).toBeInTheDocument()
+})
+```
+```tsx [react]
+// based on @testing-library/react example
+// https://testing-library.com/docs/react-testing-library/example-intro
+
+import { userEvent } from '@vitest/browser/context'
+import { render, screen } from '@testing-library/react'
+import Fetch from './fetch'
+
+test('loads and displays greeting', async () => {
+ // Render a React element into the DOM
+ render()
+
+ await userEvent.click(screen.getByText('Load Greeting'))
+ // wait before throwing an error if it cannot find an element
+ const heading = await screen.findByRole('heading')
+
+ // assert that the alert message is correct
+ expect(heading).toHaveTextContent('hello there')
+ expect(screen.getByRole('button')).toBeDisabled()
+})
+```
+```tsx [preact]
+// based on @testing-library/preact example
+// https://testing-library.com/docs/preact-testing-library/example
+
+import { h } from 'preact'
+import { userEvent } from '@vitest/browser/context'
+import { render } from '@testing-library/preact'
+
+import HiddenMessage from '../hidden-message'
+
+test('shows the children when the checkbox is checked', async () => {
+ const testMessage = 'Test Message'
+
+ const { queryByText, getByLabelText, getByText } = render(
+ {testMessage},
+ )
+
+ // query* functions will return the element or null if it cannot be found.
+ // get* functions will return the element or throw an error if it cannot be found.
+ expect(queryByText(testMessage)).not.toBeInTheDocument()
+
+ // The queries can accept a regex to make your selectors more
+ // resilient to content tweaks and changes.
+ await userEvent.click(getByLabelText(/show/i))
+
+ expect(getByText(testMessage)).toBeInTheDocument()
+})
+```
+```tsx [solid]
+// baed on @testing-library/solid API
+// https://testing-library.com/docs/solid-testing-library/api
+
+import { render } from '@testing-library/solid'
+
+it('uses params', async () => {
+ const App = () => (
+ <>
+ (
+
+ `)
+})
+```
+:::
+
## Limitations
### Thread Blocking Dialogs
diff --git a/eslint.config.js b/eslint.config.js
index 87d66c1f18d1..1034d607f7e5 100644
--- a/eslint.config.js
+++ b/eslint.config.js
@@ -12,6 +12,7 @@ export default antfu(
'**/bench.json',
'**/fixtures',
'**/assets/**',
+ '**/*.d.ts',
'**/*.timestamp-*',
'test/core/src/self',
'test/cache/cache/.vitest-base/results.json',
diff --git a/packages/browser/jest-dom.d.ts b/packages/browser/jest-dom.d.ts
new file mode 100644
index 000000000000..9c75da5eef7f
--- /dev/null
+++ b/packages/browser/jest-dom.d.ts
@@ -0,0 +1,816 @@
+// Disable automatic exports.
+
+
+type ARIAWidgetRole =
+ | "button"
+ | "checkbox"
+ | "gridcell"
+ | "link"
+ | "menuitem"
+ | "menuitemcheckbox"
+ | "menuitemradio"
+ | "option"
+ | "progressbar"
+ | "radio"
+ | "scrollbar"
+ | "searchbox"
+ | "slider"
+ | "spinbutton"
+ | "switch"
+ | "tab"
+ | "tabpanel"
+ | "textbox"
+ | "treeitem";
+
+type ARIACompositeWidgetRole =
+ | "combobox"
+ | "grid"
+ | "listbox"
+ | "menu"
+ | "menubar"
+ | "radiogroup"
+ | "tablist"
+ | "tree"
+ | "treegrid";
+
+type ARIADocumentStructureRole =
+ | "application"
+ | "article"
+ | "blockquote"
+ | "caption"
+ | "cell"
+ | "columnheader"
+ | "definition"
+ | "deletion"
+ | "directory"
+ | "document"
+ | "emphasis"
+ | "feed"
+ | "figure"
+ | "generic"
+ | "group"
+ | "heading"
+ | "img"
+ | "insertion"
+ | "list"
+ | "listitem"
+ | "math"
+ | "meter"
+ | "none"
+ | "note"
+ | "paragraph"
+ | "presentation"
+ | "row"
+ | "rowgroup"
+ | "rowheader"
+ | "separator"
+ | "strong"
+ | "subscript"
+ | "superscript"
+ | "table"
+ | "term"
+ | "time"
+ | "toolbar"
+ | "tooltip";
+
+type ARIALandmarkRole =
+ | "banner"
+ | "complementary"
+ | "contentinfo"
+ | "form"
+ | "main"
+ | "navigation"
+ | "region"
+ | "search";
+
+type ARIALiveRegionRole = "alert" | "log" | "marquee" | "status" | "timer";
+
+type ARIAWindowRole = "alertdialog" | "dialog";
+
+type ARIAUncategorizedRole = "code";
+
+type ARIARole =
+ | ARIAWidgetRole
+ | ARIACompositeWidgetRole
+ | ARIADocumentStructureRole
+ | ARIALandmarkRole
+ | ARIALiveRegionRole
+ | ARIAWindowRole
+ | ARIAUncategorizedRole;
+
+declare namespace matchers {
+ interface TestingLibraryMatchers {
+ /**
+ * @deprecated
+ * since v1.9.0
+ * @description
+ * Assert whether a value is a DOM element, or not. Contrary to what its name implies, this matcher only checks
+ * that you passed to it a valid DOM element.
+ *
+ * It does not have a clear definition of what "the DOM" is. Therefore, it does not check whether that element
+ * is contained anywhere.
+ * @see
+ * [testing-library/jest-dom#toBeInTheDom](https://github.com/testing-library/jest-dom#toBeInTheDom)
+ */
+ toBeInTheDOM(container?: HTMLElement | SVGElement): R
+ /**
+ * @description
+ * Assert whether an element is present in the document or not.
+ * @example
+ *
+ *
+ * expect(queryByTestId('svg-element')).toBeInTheDocument()
+ * expect(queryByTestId('does-not-exist')).not.toBeInTheDocument()
+ * @see
+ * [testing-library/jest-dom#tobeinthedocument](https://github.com/testing-library/jest-dom#tobeinthedocument)
+ */
+ toBeInTheDocument(): R
+ /**
+ * @description
+ * This allows you to check if an element is currently visible to the user.
+ *
+ * An element is visible if **all** the following conditions are met:
+ * * it does not have its css property display set to none
+ * * it does not have its css property visibility set to either hidden or collapse
+ * * it does not have its css property opacity set to 0
+ * * its parent element is also visible (and so on up to the top of the DOM tree)
+ * * it does not have the hidden attribute
+ * * if `` it has the open attribute
+ * @example
+ *
+ * Zero Opacity
+ *
+ *
+ *
Visible Example
+ *
+ * expect(getByTestId('zero-opacity')).not.toBeVisible()
+ * expect(getByTestId('visible')).toBeVisible()
+ * @see
+ * [testing-library/jest-dom#tobevisible](https://github.com/testing-library/jest-dom#tobevisible)
+ */
+ toBeVisible(): R
+ /**
+ * @deprecated
+ * since v5.9.0
+ * @description
+ * Assert whether an element has content or not.
+ * @example
+ *
+ *
+ *
+ *
+ * expect(getByTestId('empty')).toBeEmpty()
+ * expect(getByTestId('not-empty')).not.toBeEmpty()
+ * @see
+ * [testing-library/jest-dom#tobeempty](https://github.com/testing-library/jest-dom#tobeempty)
+ */
+ toBeEmpty(): R
+ /**
+ * @description
+ * Assert whether an element has content or not.
+ * @example
+ *
+ *
+ *
+ *
+ * expect(getByTestId('empty')).toBeEmptyDOMElement()
+ * expect(getByTestId('not-empty')).not.toBeEmptyDOMElement()
+ * @see
+ * [testing-library/jest-dom#tobeemptydomelement](https://github.com/testing-library/jest-dom#tobeemptydomelement)
+ */
+ toBeEmptyDOMElement(): R
+ /**
+ * @description
+ * Allows you to check whether an element is disabled from the user's perspective.
+ *
+ * Matches if the element is a form control and the `disabled` attribute is specified on this element or the
+ * element is a descendant of a form element with a `disabled` attribute.
+ * @example
+ *
+ *
+ * expect(getByTestId('button')).toBeDisabled()
+ * @see
+ * [testing-library/jest-dom#tobedisabled](https://github.com/testing-library/jest-dom#tobedisabled)
+ */
+ toBeDisabled(): R
+ /**
+ * @description
+ * Allows you to check whether an element is not disabled from the user's perspective.
+ *
+ * Works like `not.toBeDisabled()`.
+ *
+ * Use this matcher to avoid double negation in your tests.
+ * @example
+ *
+ *
+ * expect(getByTestId('button')).toBeEnabled()
+ * @see
+ * [testing-library/jest-dom#tobeenabled](https://github.com/testing-library/jest-dom#tobeenabled)
+ */
+ toBeEnabled(): R
+ /**
+ * @description
+ * Check if a form element, or the entire `form`, is currently invalid.
+ *
+ * An `input`, `select`, `textarea`, or `form` element is invalid if it has an `aria-invalid` attribute with no
+ * value or a value of "true", or if the result of `checkValidity()` is false.
+ * @example
+ *
+ *
+ *
+ *
+ * expect(getByTestId('no-aria-invalid')).not.toBeInvalid()
+ * expect(getByTestId('invalid-form')).toBeInvalid()
+ * @see
+ * [testing-library/jest-dom#tobeinvalid](https://github.com/testing-library/jest-dom#tobeinvalid)
+ */
+ toBeInvalid(): R
+ /**
+ * @description
+ * This allows you to check if a form element is currently required.
+ *
+ * An element is required if it is having a `required` or `aria-required="true"` attribute.
+ * @example
+ *
+ *
+ *
+ * expect(getByTestId('required-input')).toBeRequired()
+ * expect(getByTestId('supported-role')).not.toBeRequired()
+ * @see
+ * [testing-library/jest-dom#toberequired](https://github.com/testing-library/jest-dom#toberequired)
+ */
+ toBeRequired(): R
+ /**
+ * @description
+ * Allows you to check if a form element is currently required.
+ *
+ * An `input`, `select`, `textarea`, or `form` element is invalid if it has an `aria-invalid` attribute with no
+ * value or a value of "false", or if the result of `checkValidity()` is true.
+ * @example
+ *
+ *
+ *
+ *
+ * expect(getByTestId('no-aria-invalid')).not.toBeValid()
+ * expect(getByTestId('invalid-form')).toBeInvalid()
+ * @see
+ * [testing-library/jest-dom#tobevalid](https://github.com/testing-library/jest-dom#tobevalid)
+ */
+ toBeValid(): R
+ /**
+ * @description
+ * Allows you to assert whether an element contains another element as a descendant or not.
+ * @example
+ *
+ *
+ *
+ *
+ * const ancestor = getByTestId('ancestor')
+ * const descendant = getByTestId('descendant')
+ * const nonExistantElement = getByTestId('does-not-exist')
+ * expect(ancestor).toContainElement(descendant)
+ * expect(descendant).not.toContainElement(ancestor)
+ * expect(ancestor).not.toContainElement(nonExistantElement)
+ * @see
+ * [testing-library/jest-dom#tocontainelement](https://github.com/testing-library/jest-dom#tocontainelement)
+ */
+ toContainElement(element: HTMLElement | SVGElement | null): R
+ /**
+ * @description
+ * Assert whether a string representing a HTML element is contained in another element.
+ * @example
+ *
+ *
+ * expect(getByTestId('parent')).toContainHTML('')
+ * @see
+ * [testing-library/jest-dom#tocontainhtml](https://github.com/testing-library/jest-dom#tocontainhtml)
+ */
+ toContainHTML(htmlText: string): R
+ /**
+ * @description
+ * Allows you to check if a given element has an attribute or not.
+ *
+ * You can also optionally check that the attribute has a specific expected value or partial match using
+ * [expect.stringContaining](https://jestjs.io/docs/en/expect.html#expectnotstringcontainingstring) or
+ * [expect.stringMatching](https://jestjs.io/docs/en/expect.html#expectstringmatchingstring-regexp).
+ * @example
+ *
+ *
+ * expect(button).toHaveAttribute('disabled')
+ * expect(button).toHaveAttribute('type', 'submit')
+ * expect(button).not.toHaveAttribute('type', 'button')
+ * @see
+ * [testing-library/jest-dom#tohaveattribute](https://github.com/testing-library/jest-dom#tohaveattribute)
+ */
+ toHaveAttribute(attr: string, value?: unknown): R
+ /**
+ * @description
+ * Check whether the given element has certain classes within its `class` attribute.
+ *
+ * You must provide at least one class, unless you are asserting that an element does not have any classes.
+ * @example
+ *
+ *
+ *
no classes
+ *
+ * const deleteButton = getByTestId('delete-button')
+ * const noClasses = getByTestId('no-classes')
+ * expect(deleteButton).toHaveClass('btn')
+ * expect(deleteButton).toHaveClass('btn-danger xs')
+ * expect(deleteButton).toHaveClass(/danger/, 'xs')
+ * expect(deleteButton).toHaveClass('btn xs btn-danger', {exact: true})
+ * expect(deleteButton).not.toHaveClass('btn xs btn-danger', {exact: true})
+ * expect(noClasses).not.toHaveClass()
+ * @see
+ * [testing-library/jest-dom#tohaveclass](https://github.com/testing-library/jest-dom#tohaveclass)
+ */
+ toHaveClass(...classNames: Array): R
+ toHaveClass(classNames: string, options?: {exact: boolean}): R
+ /**
+ * @description
+ * This allows you to check whether the given form element has the specified displayed value (the one the
+ * end user will see). It accepts ,
+ *
+ *
+ *
+ * expect(getByTestId('img-alt')).toHaveAccessibleName('Test alt')
+ * expect(getByTestId('img-empty-alt')).not.toHaveAccessibleName()
+ * expect(getByTestId('svg-title')).toHaveAccessibleName('Test title')
+ * expect(getByTestId('button-img-alt')).toHaveAccessibleName()
+ * expect(getByTestId('img-paragraph')).not.toHaveAccessibleName()
+ * expect(getByTestId('svg-button')).toHaveAccessibleName()
+ * expect(getByTestId('svg-without-title')).not.toHaveAccessibleName()
+ * expect(getByTestId('input-title')).toHaveAccessibleName()
+ * @see
+ * [testing-library/jest-dom#tohaveaccessiblename](https://github.com/testing-library/jest-dom#tohaveaccessiblename)
+ */
+ toHaveAccessibleName(text?: string | RegExp | E): R
+ /**
+ * @description
+ * This allows you to assert that an element has the expected
+ * [role](https://www.w3.org/TR/html-aria/#docconformance).
+ *
+ * This is useful in cases where you already have access to an element via
+ * some query other than the role itself, and want to make additional
+ * assertions regarding its accessibility.
+ *
+ * The role can match either an explicit role (via the `role` attribute), or
+ * an implicit one via the [implicit ARIA
+ * semantics](https://www.w3.org/TR/html-aria/).
+ *
+ * Note: roles are matched literally by string equality, without inheriting
+ * from the ARIA role hierarchy. As a result, querying a superclass role
+ * like 'checkbox' will not include elements with a subclass role like
+ * 'switch'.
+ *
+ * @example
+ *
+ *
Continue
+ *
+ * About
+ * Invalid link
+ *
+ * expect(getByTestId('button')).toHaveRole('button')
+ * expect(getByTestId('button-explicit')).toHaveRole('button')
+ * expect(getByTestId('button-explicit-multiple')).toHaveRole('button')
+ * expect(getByTestId('button-explicit-multiple')).toHaveRole('switch')
+ * expect(getByTestId('link')).toHaveRole('link')
+ * expect(getByTestId('link-invalid')).not.toHaveRole('link')
+ * expect(getByTestId('link-invalid')).toHaveRole('generic')
+ *
+ * @see
+ * [testing-library/jest-dom#tohaverole](https://github.com/testing-library/jest-dom#tohaverole)
+ */
+ toHaveRole(
+ // Get autocomplete for ARIARole union types, while still supporting another string
+ // Ref: https://github.com/microsoft/TypeScript/issues/29729#issuecomment-567871939
+ role: ARIARole | (string & {}),
+ ): R
+ /**
+ * @description
+ * This allows you to check whether the given element is partially checked.
+ * It accepts an input of type checkbox and elements with a role of checkbox
+ * with a aria-checked="mixed", or input of type checkbox with indeterminate
+ * set to true
+ *
+ * @example
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ * const ariaCheckboxMixed = getByTestId('aria-checkbox-mixed')
+ * const inputCheckboxChecked = getByTestId('input-checkbox-checked')
+ * const inputCheckboxUnchecked = getByTestId('input-checkbox-unchecked')
+ * const ariaCheckboxChecked = getByTestId('aria-checkbox-checked')
+ * const ariaCheckboxUnchecked = getByTestId('aria-checkbox-unchecked')
+ * const inputCheckboxIndeterminate = getByTestId('input-checkbox-indeterminate')
+ *
+ * expect(ariaCheckboxMixed).toBePartiallyChecked()
+ * expect(inputCheckboxChecked).not.toBePartiallyChecked()
+ * expect(inputCheckboxUnchecked).not.toBePartiallyChecked()
+ * expect(ariaCheckboxChecked).not.toBePartiallyChecked()
+ * expect(ariaCheckboxUnchecked).not.toBePartiallyChecked()
+ *
+ * inputCheckboxIndeterminate.indeterminate = true
+ * expect(inputCheckboxIndeterminate).toBePartiallyChecked()
+ * @see
+ * [testing-library/jest-dom#tobepartiallychecked](https://github.com/testing-library/jest-dom#tobepartiallychecked)
+ */
+ toBePartiallyChecked(): R
+ /**
+ * @deprecated
+ * since v5.17.0
+ *
+ * @description
+ * Check whether the given element has an [ARIA error message](https://www.w3.org/TR/wai-aria/#aria-errormessage) or not.
+ *
+ * Use the `aria-errormessage` attribute to reference another element that contains
+ * custom error message text. Multiple ids is **NOT** allowed. Authors MUST use
+ * `aria-invalid` in conjunction with `aria-errormessage`. Learn more from the
+ * [`aria-errormessage` spec](https://www.w3.org/TR/wai-aria/#aria-errormessage).
+ *
+ * Whitespace is normalized.
+ *
+ * When a `string` argument is passed through, it will perform a whole
+ * case-sensitive match to the error message text.
+ *
+ * To perform a case-insensitive match, you can use a `RegExp` with the `/i`
+ * modifier.
+ *
+ * To perform a partial match, you can pass a `RegExp` or use
+ * expect.stringContaining("partial string")`.
+ *
+ * @example
+ *
+ *
+ *
+ * Invalid time: the time must be between 9:00 AM and 5:00 PM"
+ *
+ *
+ *
+ * const timeInput = getByLabel('startTime')
+ *
+ * expect(timeInput).toHaveErrorMessage(
+ * 'Invalid time: the time must be between 9:00 AM and 5:00 PM',
+ * )
+ * expect(timeInput).toHaveErrorMessage(/invalid time/i) // to partially match
+ * expect(timeInput).toHaveErrorMessage(expect.stringContaining('Invalid time')) // to partially match
+ * expect(timeInput).not.toHaveErrorMessage('Pikachu!')
+ * @see
+ * [testing-library/jest-dom#tohaveerrormessage](https://github.com/testing-library/jest-dom#tohaveerrormessage)
+ */
+ toHaveErrorMessage(text?: string | RegExp | E): R
+ }
+}
+
+// Needs to extend Record to be accepted by expect.extend()
+// as it requires a string index signature.
+declare const matchers: matchers.TestingLibraryMatchers &
+ Record
+
+declare namespace matchers$1 {
+ export { matchers as default };
+}
+
+export { matchers$1 as default };
diff --git a/packages/browser/matchers.d.ts b/packages/browser/matchers.d.ts
new file mode 100644
index 000000000000..521383b869f0
--- /dev/null
+++ b/packages/browser/matchers.d.ts
@@ -0,0 +1,22 @@
+import type jsdomMatchers from './jest-dom.js'
+import type { Assertion } from 'vitest'
+
+declare module 'vitest' {
+ interface JestAssertion extends jsdomMatchers.default.TestingLibraryMatchers {}
+
+ type Promisify = {
+ [K in keyof O]: O[K] extends (...args: infer A) => infer R
+ ? O extends R
+ ? Promisify
+ : (...args: A) => Promise
+ : O[K];
+ }
+
+ type PromisifyDomAssertion = Promisify>
+
+ interface ExpectStatic {
+ element: (element: T, options?: ExpectPollOptions) => PromisifyDomAssertion>
+ }
+}
+
+export {}
diff --git a/packages/browser/package.json b/packages/browser/package.json
index ed4d30c019f6..a8f85c7f0013 100644
--- a/packages/browser/package.json
+++ b/packages/browser/package.json
@@ -28,6 +28,9 @@
"types": "./context.d.ts",
"default": "./context.js"
},
+ "./matchers": {
+ "types": "./matchers.d.ts"
+ },
"./providers/webdriverio": {
"types": "./providers/webdriverio.d.ts"
},
@@ -79,6 +82,7 @@
"ws": "^8.17.1"
},
"devDependencies": {
+ "@testing-library/jest-dom": "^6.4.6",
"@types/ws": "^8.5.10",
"@vitest/runner": "workspace:*",
"@vitest/ui": "workspace:*",
diff --git a/packages/browser/providers/playwright.d.ts b/packages/browser/providers/playwright.d.ts
index 4657179bcf62..c5ffd2f4f46b 100644
--- a/packages/browser/providers/playwright.d.ts
+++ b/packages/browser/providers/playwright.d.ts
@@ -5,6 +5,7 @@ import type {
LaunchOptions,
Page,
} from 'playwright'
+import '../matchers.js'
declare module 'vitest/node' {
interface BrowserProviderOptions {
diff --git a/packages/browser/providers/webdriverio.d.ts b/packages/browser/providers/webdriverio.d.ts
index 890a878c8e6e..30694789dfa2 100644
--- a/packages/browser/providers/webdriverio.d.ts
+++ b/packages/browser/providers/webdriverio.d.ts
@@ -1,4 +1,5 @@
import type { RemoteOptions } from 'webdriverio'
+import '../matchers.js'
declare module 'vitest/node' {
interface BrowserProviderOptions extends RemoteOptions {}
diff --git a/packages/browser/rollup.config.js b/packages/browser/rollup.config.js
index 3022e2ebfb64..28a585f18df3 100644
--- a/packages/browser/rollup.config.js
+++ b/packages/browser/rollup.config.js
@@ -78,8 +78,23 @@ export default () =>
format: 'esm',
},
external,
- plugins: [dts({
- respectExternal: true,
- })],
+ plugins: [
+ dts({
+ respectExternal: true,
+ }),
+ ],
+ },
+ {
+ input: './src/client/tester/jest-dom.ts',
+ output: {
+ file: './jest-dom.d.ts',
+ format: 'esm',
+ },
+ external: [],
+ plugins: [
+ dts({
+ respectExternal: true,
+ }),
+ ],
},
])
diff --git a/packages/browser/src/client/tester/expect-dom.ts b/packages/browser/src/client/tester/expect-dom.ts
new file mode 100644
index 000000000000..8ed810a4a12c
--- /dev/null
+++ b/packages/browser/src/client/tester/expect-dom.ts
@@ -0,0 +1,10 @@
+import * as matchers from '@testing-library/jest-dom/matchers'
+import type { ExpectPollOptions } from 'vitest'
+import { expect } from 'vitest'
+
+export async function setupExpectDom() {
+ expect.extend(matchers)
+ expect.element = (element: T, options?: ExpectPollOptions) => {
+ return expect.poll(() => element, options)
+ }
+}
diff --git a/packages/browser/src/client/tester/jest-dom.ts b/packages/browser/src/client/tester/jest-dom.ts
new file mode 100644
index 000000000000..178952e1ec77
--- /dev/null
+++ b/packages/browser/src/client/tester/jest-dom.ts
@@ -0,0 +1 @@
+export type { default } from '@testing-library/jest-dom/matchers'
diff --git a/packages/browser/src/client/tester/tester.ts b/packages/browser/src/client/tester/tester.ts
index 50d40ffab756..7c5188d3a4e9 100644
--- a/packages/browser/src/client/tester/tester.ts
+++ b/packages/browser/src/client/tester/tester.ts
@@ -10,6 +10,7 @@ import { setupConsoleLogSpy } from './logger'
import { createSafeRpc } from './rpc'
import { browserHashMap, initiateRunner } from './runner'
import { VitestBrowserClientMocker } from './mocker'
+import { setupExpectDom } from './expect-dom'
const url = new URL(location.href)
const reloadStart = url.searchParams.get('__reloadStart')
@@ -91,6 +92,7 @@ async function prepareTestEnvironment(files: string[]) {
setupConsoleLogSpy()
setupDialogsSpy()
+ setupExpectDom()
const runner = await initiateRunner(state, mocker, config)
diff --git a/packages/vitest/src/types/index.ts b/packages/vitest/src/types/index.ts
index 31196af13b9b..f7d52bd5465b 100644
--- a/packages/vitest/src/types/index.ts
+++ b/packages/vitest/src/types/index.ts
@@ -32,4 +32,5 @@ export type {
AsymmetricMatchersContaining,
JestAssertion,
Assertion,
+ ExpectPollOptions,
} from '@vitest/expect'
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 7bea584f80a8..0ac6b2be5aab 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -451,6 +451,9 @@ importers:
specifier: ^8.17.1
version: 8.17.1
devDependencies:
+ '@testing-library/jest-dom':
+ specifier: ^6.4.6
+ version: 6.4.6(vitest@packages+vitest)
'@types/ws':
specifier: ^8.5.10
version: 8.5.10
@@ -1402,6 +1405,10 @@ packages:
resolution: {integrity: sha512-rE0Pygv0sEZ4vBWHlAgJLGDU7Pm8xoO6p3wsEceb7GYAjScrOHpEo8KK/eVkAcnSM+slAEtXjA2JpdjLp4fJQQ==}
dev: true
+ /@adobe/css-tools@4.4.0:
+ resolution: {integrity: sha512-Ff9+ksdQQB3rMncgqDK78uLznstjyfIf2Arnh22pW8kBpLs6rpKDwgnZT46hin5Hl1WzazzK64DOrhSwYpS7bQ==}
+ dev: true
+
/@algolia/autocomplete-core@1.9.3(algoliasearch@4.20.0)(search-insights@2.9.0):
resolution: {integrity: sha512-009HdfugtGCdC4JdXUbVJClA0q0zh24yyePn+KUGk3rP7j8FEe/m5Yo/z65gn6nP/cM39PxpzqKrL7A6fP6PPw==}
dependencies:
@@ -5260,6 +5267,38 @@ packages:
vitest: link:packages/vitest
dev: true
+ /@testing-library/jest-dom@6.4.6(vitest@packages+vitest):
+ resolution: {integrity: sha512-8qpnGVincVDLEcQXWaHOf6zmlbwTKc6Us6PPu4CRnPXCzo2OGBS5cwgMMOWdxDpEz1mkbvXHpEy99M5Yvt682w==}
+ engines: {node: '>=14', npm: '>=6', yarn: '>=1'}
+ peerDependencies:
+ '@jest/globals': '>= 28'
+ '@types/bun': latest
+ '@types/jest': '>= 28'
+ jest: '>= 28'
+ vitest: workspace:*
+ peerDependenciesMeta:
+ '@jest/globals':
+ optional: true
+ '@types/bun':
+ optional: true
+ '@types/jest':
+ optional: true
+ jest:
+ optional: true
+ vitest:
+ optional: true
+ dependencies:
+ '@adobe/css-tools': 4.4.0
+ '@babel/runtime': 7.24.4
+ aria-query: 5.3.0
+ chalk: 3.0.0
+ css.escape: 1.5.1
+ dom-accessibility-api: 0.6.3
+ lodash: 4.17.21
+ redent: 3.0.0
+ vitest: link:packages/vitest
+ dev: true
+
/@testing-library/preact@3.2.3(preact@10.21.0):
resolution: {integrity: sha512-y6Kklp1XK3f1X2fWCbujmJyzkf+1BgLYXNgAx21j9+D4CoqMTz5qC4SQufb1L6q/jxLGACzrQ90ewVOTBvHOfg==}
engines: {node: '>= 12'}
diff --git a/test/browser/test/dom.test.ts b/test/browser/test/dom.test.ts
index 499d276375e8..d955883974bb 100644
--- a/test/browser/test/dom.test.ts
+++ b/test/browser/test/dom.test.ts
@@ -11,7 +11,7 @@ describe('dom related activity', () => {
document.body.appendChild(wrapper)
const div = createNode()
wrapper.appendChild(div)
- expect(div.textContent).toBe('Hello World!')
+ await expect.element(div).toHaveTextContent('Hello World!')
const screenshotPath = await page.screenshot({
element: wrapper,
})
diff --git a/test/browser/tsconfig.json b/test/browser/tsconfig.json
index dd26cb7f7263..5b66297693ad 100644
--- a/test/browser/tsconfig.json
+++ b/test/browser/tsconfig.json
@@ -5,7 +5,10 @@
"paths": {
"#src/*": ["./src/*"]
},
- "types": ["vite/client"],
+ "types": [
+ "vite/client",
+ "@vitest/browser/providers/playwright"
+ ],
"esModuleInterop": true
}
}