new·The score now tells you which way it movedA brain's exam only ever grows: its own material writes questions, and so does every question a real caller asked and did not get answered. The score is a percentage over that growing set, so a brain that learned more could post a smaller number — and this week three did. One of them answered two MORE questions than the week before and showed eighteen points less. Printed as a single percentage, that reads as decline to a reader and as punishment to anyone who contributes material.all news →
mozg.beta
Sign in

Vitest · Guide · all subjects

browser mode testing

144 notes in this subject, read out of this brain and free to use. This is page 1 of 3.

Custom browser commands security considerations

Custom browser commands run in the Vitest Node process. If a command exposes filesystem, process, network, database, or shell access based on browser-provided input, the input must be validated and restricted inside the command. Built-in file commands apply Vite server.fs checks and write-access checks, but custom commands are solely responsible for their own security protections.

browser.commands configuration property

The browser.commands configuration property is of type Record<string, BrowserCommand>. It is used to define custom commands that can be imported during browser tests from vitest/browser. The default value includes built-in commands such as readFile and writeFile.

Recording trace markers example

import type { BrowserCommand } from 'vitest/node' export const uploadFixture: BrowserCommand<[name: string]> = async ( context, name, ) => { await context.mark(`upload start: ${name}`, { kind: 'action' }) // ... do server-side work await context.mark(`upload done: ${name}`, { kind: 'action' }) }

Playwright-specific command context properties

Vitest exposes several playwright-specific properties on the command context: page (references the full page that contains the test iframe, the orchestrator HTML); frame (an async method that resolves to a Playwright Frame, has similar API to page but does not support certain methods); iframe (a FrameLocator that should be used to query other elements on the page, more stable and faster than frame for querying elements); context (refers to the unique BrowserContext).

CDP session access via vitest/browser

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. CDP session works only with playwright provider and only when using chromium browser. CDP is a privileged debugging API available only when browser API write and exec operations are enabled through api.allowWrite and api.allowExec configuration options.

Playwright custom command example with page and iframe

import { BrowserCommand } from 'vitest/node' export const myCommand: BrowserCommand<[string, number]> = async ( ctx, arg1: string, arg2: number ) => { if (ctx.provider.name === 'playwright') { const element = await ctx.iframe.findByRole('alert') const screenshot = await element.screenshot() // do something with the screenshot return difference } }

CDP session usage example

import { cdp } from 'vitest/browser' const input = document.createElement('input') document.body.appendChild(input) input.focus() await cdp().send('Input.dispatchKeyEvent', { type: 'keyDown', text: 'a', }) expect(input).toHaveValue('a')

Custom browser commands via browser.commands config

You can add custom commands via browser.commands config option. If you develop a library, you can provide them via a config hook inside a plugin. Custom commands are functions that run in the Vitest Node process and are callable from browser test code through Vitest's browser RPC connection. They have access to testPath and provider properties in their context.

readFile, writeFile, removeFile usage example

import { server } from 'vitest/browser' const { readFile, writeFile, removeFile } = server.commands it('handles files', async () => { const file = './test.txt' await writeFile(file, 'hello world') const content = await readFile(file) expect(content).toBe('hello world') await removeFile(file) })

Custom file-writing command with security checks example

import { mkdir, writeFile } from 'node:fs/promises' import { dirname, resolve } from 'node:path' import { normalizePath } from 'vite' import { isFileLoadingAllowed } from 'vitest/node' import type { BrowserCommand } from 'vitest/node' function assertFileAccess(path: string, project: any) { if ( !isFileLoadingAllowed(project.vite.config, path) && !isFileLoadingAllowed(project.vitest.vite.config, path) ) { throw new Error(`Access denied to "${path}".`) } } function assertWrite(project: any) { if (!project.config.browser.api.allowWrite || !project.vitest.config.api.allowWrite) { throw new Error('Writing files is disabled.') } } export const myWriteFileCommand: BrowserCommand<[path: string, content: string]> = async ( { project }, path, content, ) => { assertWrite(project) const file = resolve(project.config.root, path) assertFileAccess(normalizePath(file), project) await mkdir(dirname(file), { recursive: true }) await writeFile(file, content) }

Security considerations for custom browser commands

Custom commands run in the Vitest Node process and are callable from browser test code through Vitest's browser RPC connection. They can access local files, environment variables, network services, databases, shell commands, and other Node APIs. Custom commands do not automatically inherit the protections that Vitest's built-in file commands have (validation against Vite's server.fs restrictions). If a custom command accepts browser-provided input and uses it to read, write, delete, execute, or expose local resources, validate that input before using it. For file reads or fixture loading, use isFileLoadingAllowed from vitest/node or an explicit allowlist. For writes and deletes, also require an explicit mutation policy such as api.allowWrite and a command-specific allowed directory. For commands that execute code, shell commands, or project scripts, also check api.allowExec.

Recording trace markers in custom commands

Custom commands can record trace markers for the test that triggered them through context.mark. This is the server-side equivalent of page.mark and helps annotate the trace view with custom actions performed inside a command. context.mark is a no-op when browser tracing is not enabled or no test is currently running in the session. Unlike page.mark, it does not accept a callback form.

WebdriverIO-specific command context properties

Vitest exposes a browser property on the command context for webdriverio-specific commands. The browser property is the WebdriverIO.Browser API. Vitest automatically switches the webdriver context to the test iframe by calling browser.switchFrame before the command is called, so $ and $$ methods refer to elements inside the iframe, not in the orchestrator. However, non-webdriver APIs will still refer to the parent frame context.

Custom commands override built-in commands with same name

Custom functions will override built-in ones if they have the same name.

Custom browser command definition and usage example

import type { Plugin } from 'vitest/config' import type { BrowserCommand } from 'vitest/node' const myCustomCommand: BrowserCommand<[arg1: string, arg2: string]> = ({ testPath, provider }, arg1, arg2) => { if (provider.name === 'playwright') { console.log(testPath, arg1, arg2) return { someValue: true } } throw new Error(`provider ${provider.name} is not supported`) } export default function BrowserCommands(): Plugin { return { name: 'vitest:custom-commands', config() { return { test: { browser: { commands: { myCustomCommand, } } } } } } } // Usage in test: import { commands } from 'vitest/browser' import { expect, test } from 'vitest' test('custom command works correctly', async () => { const result = await commands.myCustomCommand('test1', 'test2') expect(result).toEqual({ someValue: true }) }) // TypeScript module augmentation: declare module 'vitest/browser' { interface BrowserCommands { myCustomCommand: (arg1: string, arg2: string) => Promise<{ someValue: true }> } }

readFile, writeFile, removeFile APIs in browser tests

Vitest exposes readFile, writeFile, and removeFile APIs to handle files in browser tests. Since Vitest 3.2, all paths are resolved relative to the project root (which is process.cwd(), unless overridden manually). Previously, paths were resolved relative to the test file. By default, Vitest uses utf-8 encoding but you can override it with options. These commands follow Vite's server.fs restrictions for security reasons. writeFile and removeFile require write access through api.allowWrite configuration.

toBeRequired assertion

toBeRequired() checks if a form element is currently required. An element is required if it has a 'required' or 'aria-required="true"' attribute.

toContainElement assertion

toContainElement(element: HTMLElement | SVGElement | Locator | null) asserts whether an element contains another element as a descendant or not. Can accept locators or null.

toBeInViewport assertion with ratio option

toBeInViewport(options: { ratio?: number }) checks if an element is currently in viewport using the IntersectionObserver API. The ratio argument (0~1) specifies the minimal ratio of the element that should be in viewport.

toBeVisible assertion

toBeVisible() checks if an element is currently visible to the user. An element is considered visible when it has a non-empty bounding box and does not have 'visibility:hidden' computed style. Elements of zero size are not visible. Elements with 'display:none' are not visible. Elements with 'opacity:0' are considered visible.

pixelmatch comparator options for toMatchScreenshot

pixelmatch comparator options: allowedMismatchedPixelRatio (number, 0-1) - maximum allowed ratio of differing pixels; allowedMismatchedPixels (number) - maximum number of pixels allowed to differ; threshold (number, default 0.1, 0-1) - acceptable perceived color difference; includeAA (boolean, default false) - if true, disables detection of anti-aliased pixels; alpha (number, default 0.1, 0-1) - blending level of unchanged pixels in diff image; aaColor (RGB array, default [255, 255, 0]) - color for anti-aliased pixels; diffColor (RGB array, default [255, 0, 0]) - color for differing pixels; diffColorAlt (RGB array, optional) - alternative color for dark-on-light differences; diffMask (boolean, default false) - if true, shows only diff as mask on transparent background. When both allowedMismatchedPixels and allowedMismatchedPixelRatio are set, the more restrictive value is used.

toMatchScreenshot options

toMatchScreenshot options: comparatorName (string, default 'pixelmatch') - the algorithm used for comparing images; comparatorOptions (object) - options for the comparator; screenshotOptions (object) - same options as locator.screenshot() except for 'base64', 'path', 'save', 'type'; timeout (number, default 5000) - time to wait for a stable screenshot in milliseconds, setting to 0 disables the timeout.

toBeValid assertion

toBeValid() checks if the value of an element is currently valid. An element is valid if it has no aria-invalid attribute or an attribute value of 'false'. The result of checkValidity() must also be true if it's a form element.

TypeScript support for browser assertions

To get correct type hints for expect when using browser mode, ensure 'vitest/browser' is referenced. If you never import from there, add a reference comment in any file covered by tsconfig.json: /// <reference types="vitest/browser" />

Use expect.element() instead of regular expect() for locators

Always use expect.element() when working with page.getBy* locators to reduce test flakiness. Regular expect() will fail immediately if the assertion does not pass, while expect.element() retries until the condition is met or timeout is reached.

ExpectPollOptions configuration for expect.element()

expect.element() accepts an options object with: interval (number, optional) - the interval to retry the assertion for in milliseconds, defaults to the 'expect.poll.interval' config option; timeout (number, optional) - time to retry the assertion for in milliseconds, defaults to the 'expect.poll.timeout' config option; message (string, optional) - the message printed when the assertion fails.

toBeInvalid assertion

toBeInvalid() checks if an element is currently invalid. An 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.

toContainHTML assertion

toContainHTML(htmlText: string) asserts whether a string representing an HTML element is contained in another element. The string must contain valid HTML, not incomplete HTML. This matcher tests DOM structure and is not recommended; use toContainElement() instead for elements you control.

toHaveAccessibleDescription assertion

toHaveAccessibleDescription(description?: string | RegExp) asserts that an element has the expected accessible description. Can pass an exact string, regular expression, expect.stringContaining(), or expect.stringMatching() for partial matches.

toHaveAccessibleErrorMessage assertion

toHaveAccessibleErrorMessage(message?: string | RegExp) asserts that an element has the expected accessible error message. Can pass an exact string, regular expression, expect.stringContaining(), or expect.stringMatching() for partial matches.

toBePartiallyChecked assertion

toBePartiallyChecked() checks whether an element is partially checked. Accepts input of type checkbox with aria-checked="mixed" or indeterminate set to true, and elements with role checkbox with aria-checked="mixed".

toBeInTheDocument assertion

toBeInTheDocument() asserts whether an element is present in the document or not. This matcher does not find detached elements; the element must be added to the document to be found. If you need to search in a detached element, use toContainElement() instead.

toHaveDisplayValue assertion

toHaveDisplayValue(value: string | RegExp | (string | RegExp)[]) checks whether a form element has the specified displayed value (what the user sees). Accepts input, select, and textarea elements (except input type="checkbox" and type="radio"). Can use strings, regular expressions, or arrays of mixed strings and regular expressions.

toHaveValue assertion

toHaveValue(value: string | string[] | number | null) checks whether a form element has the specified value. Accepts input, select, and textarea elements (except input type="checkbox" and type="radio", which use toBeChecked() or toHaveFormValues()). Also accepts elements with roles meter, progressbar, slider, or spinbutton (checks aria-valuenow as number). Uses the same value matching algorithm as toHaveFormValues().

toMatchTextContent assertion

toMatchTextContent(text: string | number | RegExp, options?: { normalizeWhitespace: boolean }) checks whether an element has text content matching the provided value. Supports elements, text nodes, and fragments. With a string argument, performs partial case-sensitive match. Use RegExp with /i flag for case-insensitive matching. Use RegExp to match the whole content or toHaveTextContent() instead.

toBeEmptyDOMElement assertion

toBeEmptyDOMElement() asserts whether an element has no visible content for the user. It ignores comments but will fail if the element contains white-space.

toHaveTextContent assertion

toHaveTextContent(text: string | number, options?: { normalizeWhitespace: boolean }) validates that an element's text matches the provided string exactly. Supports elements, text nodes, and fragments. For partial checks or case-insensitive matching, use toMatchTextContent() instead.

toHaveFormValues assertion

toHaveFormValues(expectedValues: Record<string, unknown>) checks if a form or fieldset contains form controls for each given name with specified values. Must be invoked on a form or fieldset element to leverage the .elements property. Handles different form control types: input type="number" returns value as number; checkbox with single name returns boolean; multiple checkboxes with same name return array; radio elements return string; input type="text" returns string; select without multiple returns string or undefined; select multiple returns array; textarea returns string.

toHaveStyle assertion

toHaveStyle(css: string | Partial<CSSStyleDeclaration>) checks if an element has specific CSS properties with specific values applied. Matches only if the element has ALL the expected properties. Works with inline styles and rules applied via classes in active stylesheets.

toBeEnabled assertion

toBeEnabled() checks whether an element is not disabled from the user's perspective. It works like not.toBeDisabled() and should be used to avoid double negation in tests.

toHaveFocus assertion

toHaveFocus() asserts whether an element has focus or not.

toBeChecked assertion

toBeChecked() checks whether an element is checked. Accepts input of type checkbox or radio, and elements with role checkbox, radio, or switch with a valid aria-checked attribute of 'true' or 'false'.

toHaveClass assertion with exact option

toHaveClass(...classNames: string[], options?: { exact: boolean }) checks whether an element has certain classes. Accepts strings and regular expressions. Regular expressions are matched against each individual class, not the full class attribute. The exact: true option requires the element to have EXACTLY the specified set of classes in any order. Cannot use exact: true with only regular expressions.

toHaveRole assertion

toHaveRole(role: ARIARole) asserts that an element has the expected ARIA role. Matches either an explicit role via the role attribute or an implicit one via implicit ARIA semantics. Roles are matched literally by string equality without inheriting from the ARIA role hierarchy. Vitest ignores all custom roles except the first valid one, following Playwright's behaviour.

toHaveSelection assertion

toHaveSelection(selection?: string) asserts that an element has text selection. Works with input type="text", textarea, or any element containing text. The expected selection is a string and does not allow checking for selection range indices.

toMatchScreenshot assertion for visual regression testing

toMatchScreenshot(options?: ScreenshotMatcherOptions) or toMatchScreenshot(name?: string, options?: ScreenshotMatcherOptions) performs visual regression testing by comparing screenshots against stored reference images. When differences are detected beyond the configured threshold, the test fails and generates the actual screenshot, expected reference screenshot, and a diff image. The assertion automatically retries taking screenshots until two consecutive captures yield the same result.

toHaveAttribute assertion

toHaveAttribute(attribute: string, value?: unknown) checks whether the given element has an attribute. Can optionally check that the attribute has a specific expected value using expect.stringContaining(), expect.stringMatching(), or regular comparison.

toHaveAccessibleName assertion

toHaveAccessibleName(name?: string | RegExp) asserts that an element has the expected accessible name. Can pass an exact string, regular expression, expect.stringContaining(), or expect.stringMatching() for partial matches. Useful for asserting that form elements and buttons are properly labelled.

toBeDisabled assertion

toBeDisabled() checks whether an element is disabled from the user's perspective. It matches if the element is a form control and the 'disabled' attribute is specified on it, or if the element is a descendant of a form element with a 'disabled' attribute. Only native control elements such as HTML button, input, select, textarea, option, optgroup can be disabled by setting the 'disabled' attribute. The 'disabled' attribute on other elements is ignored unless it's a custom element.

Browser tests may fail inconsistently due to asynchronous nature

Tests in the browser might fail inconsistently due to their asynchronous nature (timeouts, network requests, animations). Use expect.element() and expect.poll() to guarantee assertions succeed even if conditions are delayed.

expect.element() for browser assertions with retry-ability

expect.element() is used for DOM assertions in browser mode with built-in retry-ability. It automatically retries assertions until they pass or timeout is reached. When receiving a locator, Vitest resolves it with locator.findElement() before running the DOM assertion. The timeout option applies to the whole retry operation, while the interval option controls how often failed DOM assertions are retried. Regular expect() without expect.element() will fail immediately if the assertion does not pass.

frameLocator cross-origin iframe limitations

By default, frameLocator does not support querying elements with expect.element() in cross-origin iframes. Interactive methods such as click() work fine. This is different from Playwright's behavior. To work with cross-origin iframes, pass args: ["--disable-web-security"] in launchOptions, or create a custom browser command that accesses the iframe on the server side where it's available.

page.frameLocator() for iframe element interaction

The page.frameLocator(iframeElement: Locator) method returns a FrameLocator instance that can be used to find and interact with elements inside an iframe. The frame locator refers to the iframe's document, not the iframe HTML element itself. It supports interactive methods like click() and fill(), but by default does not support querying elements with expect.element() in cross-origin iframes (unless --disable-web-security is passed in launchOptions). The frameLocator method is currently only supported by the playwright provider.

page.mark() usage example

import { page } from 'vitest/browser' await page.mark('before submit') await page.getByRole('button', { name: 'Submit' }).click() await page.mark('after submit') await page.mark('submit flow', async () => { await page.getByRole('textbox', { name: 'Email' }).fill('john@example.com') await page.getByRole('button', { name: 'Submit' }).click() }, { kind: 'action' })

page.mark() for trace markers

The page.mark() method adds a named marker to the trace timeline for the current test. It has two forms: mark(name: string, options?: { stack?: string; kind?: BrowserTraceEntryKind }) adds a simple marker, and mark<T>(name: string, body: () => T | Promise<T>, options?: { stack?: string; kind?: BrowserTraceEntryKind }) creates a trace group, runs the callback, and closes the group automatically. The options.stack parameter overrides the callsite location in trace metadata (useful for wrapper libraries), and options.kind categorizes the marker as a specific type like 'action'. This method is only useful when browser.trace is enabled.

page.screenshot() method options

The page.screenshot() method has two overloads: screenshot(options?: ScreenshotOptions) returns Promise<string> (path to the screenshot file), and screenshot(options: Omit<ScreenshotOptions, 'base64'> & { base64: true }) returns Promise<{ path: string; base64: string }>. As of version 3.2.0, if save is set to false, screenshot will always return a base64 string and the path is ignored in that case.

page.viewport() method for browser tests

The page.viewport(width: number, height: number) method changes the size of the iframe's viewport in browser tests and returns a Promise<void>.

page.frameLocator() usage example

const frame = page.frameLocator( page.getByTestId('iframe') ) await frame.getByText('Hello World').click() // ✅ await frame.click() // ❌ Not available

userEvent API methods for browser testing

The userEvent context API provides the following methods for simulating user interactions in browser tests: setup(), cleanup(), click(element, options?), dblClick(element, options?), tripleClick(element, options?), selectOptions(element, values, options?), keyboard(text), type(element, text, options?), clear(element), tab(options?), hover(element, options?), unhover(element, options?), fill(element, text, options?), and dragAndDrop(source, target, options?). All methods except setup() return Promise<void>. The API is marked as experimental and support is implemented by the browser provider (playwright or webdriverio), with fallback to simulated events via @testing-library/user-event when using the preview provider.

utils.getElementLocatorSelectors() for locator selectors

The utils.getElementLocatorSelectors(element: Element) function is similar to calling page.elementLocator but returns only locator selectors instead of a Locator object.

Give your agent this brain