From 80f07edf60ae7759d154884663561d74bdd7024e Mon Sep 17 00:00:00 2001 From: Vladimir Date: Tue, 28 Apr 2026 16:11:22 +0200 Subject: [PATCH] fix!: represent locator as an object instead of a string (#10212) --- docs/api/browser/locators.md | 59 ++- docs/guide/migration.md | 22 ++ eslint.config.js | 34 ++ .../src/commands/dragAndDrop.ts | 4 +- .../src/commands/screenshot.ts | 7 +- .../browser-playwright/src/commands/select.ts | 3 +- .../browser-playwright/src/commands/trace.ts | 9 +- .../browser-playwright/src/commands/upload.ts | 3 +- .../browser-playwright/src/commands/utils.ts | 14 +- .../browser-webdriverio/src/commands/clear.ts | 2 +- .../browser-webdriverio/src/commands/click.ts | 6 +- .../src/commands/dragAndDrop.ts | 4 +- .../browser-webdriverio/src/commands/fill.ts | 2 +- .../browser-webdriverio/src/commands/hover.ts | 2 +- .../src/commands/screenshot.ts | 7 +- .../src/commands/select.ts | 2 +- .../browser-webdriverio/src/commands/type.ts | 2 +- .../src/commands/upload.ts | 5 +- .../browser-webdriverio/src/commands/utils.ts | 3 +- .../browser-webdriverio/src/commands/wheel.ts | 2 +- packages/browser-webdriverio/src/locators.ts | 4 +- packages/browser/context.d.ts | 33 +- packages/browser/package.json | 3 + packages/browser/rollup.config.js | 8 +- packages/browser/src/client/channel.ts | 2 +- packages/browser/src/client/orchestrator.ts | 18 +- packages/browser/src/client/tester/context.ts | 16 +- .../src/client/tester/expect-element.ts | 8 +- .../client/tester/expect/toMatchScreenshot.ts | 6 +- .../tester/{locators/index.ts => locators.ts} | 68 ++-- packages/browser/src/client/tester/runner.ts | 4 +- .../browser/src/client/tester/tester-utils.ts | 16 +- packages/browser/src/client/tester/tester.ts | 2 +- packages/browser/src/client/tester/trace.ts | 9 +- packages/browser/src/client/utils.ts | 2 +- .../browser/src/node/commands/screenshot.ts | 6 +- .../node/commands/screenshotMatcher/index.ts | 3 +- .../node/commands/screenshotMatcher/utils.ts | 5 +- packages/browser/src/node/commands/trace.ts | 3 +- packages/browser/src/node/index.ts | 7 +- packages/browser/src/node/plugin.ts | 3 + .../src/shared/screenshotMatcher/types.ts | 6 +- packages/mocker/package.json | 8 + .../ui/client/components/trace/TraceView.vue | 12 +- packages/vitest/LICENSE.md | 7 + packages/vitest/package.json | 5 + packages/vitest/rollup.config.js | 5 +- packages/vitest/src/public/browser.ts | 1 - pnpm-lock.yaml | 3 + pnpm-workspace.yaml | 1 + .../specs/playwright-trace-mark.test.ts | 8 +- test/browser/specs/trace.test.ts | 370 ++++++++++++++---- test/unit/test/exports.test.ts | 4 +- tsconfig.base.json | 4 - 54 files changed, 638 insertions(+), 214 deletions(-) rename packages/browser/src/client/tester/{locators/index.ts => locators.ts} (87%) diff --git a/docs/api/browser/locators.md b/docs/api/browser/locators.md index a4da5a8cc..e4ae66f9c 100644 --- a/docs/api/browser/locators.md +++ b/docs/api/browser/locators.md @@ -1051,19 +1051,70 @@ Internally, this method calls `.elements` and wraps every element using [`page.e - [See `locator.elements()`](#elements) +### serialize + +```ts +function serialize(): SerializedLocator +``` + +Returns a JSON-serializable representation of the locator. The returned object has two fields: + +- [`selector`](#selector): the provider-specific selector string used to query the element at runtime. +- `locator`: a human-readable description of the locator (e.g. `getByRole('button')`), used for error messages and tracing. Equivalent to calling [`asLocator()`](#aslocator). + +This is primarily intended for forwarding a locator to a [browser command](/api/browser/commands), which runs in Node and cannot receive a live `Locator` instance: + +```ts +import { commands, page } from 'vitest/browser' + +await commands.myCommand(page.getByRole('button').serialize()) +``` + +::: tip +Vitest automatically serializes any `Locator` argument passed to a command, so calling `serialize()` explicitly is rarely necessary. You can also use `JSON.stringify(locator)` (it calls [`toJSON`](#tojson) internally), which produces the same result. +::: + +### toJSON + +```ts +function toJSON(): SerializedLocator +``` + +Alias of [`serialize`](#serialize). Defined so that `JSON.stringify(locator)` and structured-clone-based transports return a `SerializedLocator` object. + +### asLocator + +```ts +function asLocator(): string +``` + +Returns a human-readable description of the locator using the JavaScript locator syntax (e.g. `getByRole('button', { name: 'Submit' })`). This is the same string exposed as the `locator` field of [`serialize()`](#serialize) and is used in error messages and traces. + +```ts +import { page } from 'vitest/browser' + +const button = page.getByRole('button', { name: 'Submit' }) +button.asLocator() // "getByRole('button', { name: 'Submit' })" +``` + +::: tip +Use [`selector`](#selector) when you need the provider-specific string to forward to a [browser command](/api/browser/commands). Use `asLocator()` only for diagnostic output. The returned string is not meant to be re-used to query elements. +::: + ## Properties ### selector -The `selector` is a string that will be used to locate the element by the browser provider. Playwright will use a `playwright` locator syntax while `preview` and `webdriverio` will use CSS. +The `selector` is a string that will be used to locate the element by the browser provider. Playwright will use a `playwright` locator syntax, and `preview` and `webdriverio` will use CSS. ::: danger You should not use this string in your test code. The `selector` string should only be used when working with the Commands API: ```ts [commands.ts] import type { BrowserCommand } from 'vitest/node' +import type { SerializedLocator } from '@vitest/browser' -const test: BrowserCommand = function test(context, selector) { +const test: BrowserCommand = function test(context, { selector }) { // playwright await context.iframe.locator(selector).click() // webdriverio @@ -1076,8 +1127,8 @@ import { test } from 'vitest' import { commands, page } from 'vitest/browser' test('works correctly', async () => { - await commands.test(page.getByText('Hello').selector) // ✅ - // vitest will automatically unwrap it to a string + await commands.test(page.getByText('Hello').serialize()) // ✅ + // vitest will automatically unwrap it to a SerializedLocator await commands.test(page.getByText('Hello')) // ✅ }) ``` diff --git a/docs/guide/migration.md b/docs/guide/migration.md index 788cf0ab8..8d1dd57b5 100644 --- a/docs/guide/migration.md +++ b/docs/guide/migration.md @@ -34,6 +34,28 @@ test('example', { sequential: true }, async () => { /* ... */ }) // [!code --] test('example', { concurrent: false }, async () => { /* ... */ }) // [!code ++] ``` +### Locators in Commands are Serialized as Objects + +Locators forwarded to [browser commands](/api/browser/commands) are now serialized as a `SerializedLocator` object instead of a bare selector string. The object exposes two fields: + +- `selector`: the provider-specific selector string (the same value commands previously received). +- `locator`: a human-readable representation of the locator (e.g. `getByRole('button')`), used for error messages and tracing. + +Update any custom commands that accept a locator to destructure `selector` from the new object: + +```ts +import type { SerializedLocator } from '@vitest/browser' +import type { BrowserCommandContext } from 'vitest/node' + +export async function customClick( + context: BrowserCommandContext, + selector: string, // [!code --] + { selector }: SerializedLocator, // [!code ++] +) { + await context.page.locator(selector).click() +} +``` + ## Migrating to Vitest 4.0 {#vitest-4} ::: warning Prerequisites diff --git a/eslint.config.js b/eslint.config.js index 77446a508..0578c31a8 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -142,4 +142,38 @@ export default antfu( 'unicorn/consistent-function-scoping': 'off', }, }, + { + files: [`packages/browser/src/client/orchestrator.ts`], + rules: { + 'no-restricted-imports': [ + 'error', + { + paths: ['vitest/internal/browser', 'vitest/node'], + }, + ], + }, + }, + // ivya should be loaded only once in "ivya chunk" (see browser rollup config) + { + files: [`packages/browser/${GLOB_SRC}`], + ignores: [ + // aria snapshots + `packages/browser/src/vendor-types.ts`, + `packages/browser/src/client/tester/aria.ts`, + // primary use case - creates the engine + `packages/browser/src/client/tester/locators.ts`, + // uses utils from ivya to reuse locator syntax + `packages/browser/src/client/tester/expect/${GLOB_SRC}`, + // used as a type + `packages/browser/src/client/utils.ts`, + ], + rules: { + 'no-restricted-imports': [ + 'error', + { + paths: ['ivya', 'ivya/utils', 'ivya/aria'], + }, + ], + }, + }, ) diff --git a/packages/browser-playwright/src/commands/dragAndDrop.ts b/packages/browser-playwright/src/commands/dragAndDrop.ts index 2febe3704..117430290 100644 --- a/packages/browser-playwright/src/commands/dragAndDrop.ts +++ b/packages/browser-playwright/src/commands/dragAndDrop.ts @@ -9,8 +9,8 @@ export const dragAndDrop: UserEventCommand = async ( ) => { const frame = await context.frame() await frame.dragAndDrop( - source, - target, + source.selector, + target.selector, options_, ) } diff --git a/packages/browser-playwright/src/commands/screenshot.ts b/packages/browser-playwright/src/commands/screenshot.ts index e38686e6a..75aa396ac 100644 --- a/packages/browser-playwright/src/commands/screenshot.ts +++ b/packages/browser-playwright/src/commands/screenshot.ts @@ -1,3 +1,4 @@ +import type { SerializedLocator } from '@vitest/browser' import type { ScreenshotOptions } from 'vitest/browser' import type { BrowserCommandContext } from 'vitest/node' import { mkdir } from 'node:fs/promises' @@ -6,8 +7,8 @@ import { dirname, normalize } from 'pathe' import { getDescribedLocator } from './utils' interface ScreenshotCommandOptions extends Omit { - element?: string - mask?: readonly string[] + element?: SerializedLocator + mask?: readonly SerializedLocator[] } const SCREENSHOT_STYLES = /* css */` @@ -71,7 +72,7 @@ export async function takeScreenshot( return { buffer, path } } - const buffer = await getDescribedLocator(context, 'body').screenshot({ + const buffer = await getDescribedLocator(context, { selector: 'body', locator: 'locator(\'body\')' }).screenshot({ ...options, mask, path: savePath, diff --git a/packages/browser-playwright/src/commands/select.ts b/packages/browser-playwright/src/commands/select.ts index b18e17518..253c00962 100644 --- a/packages/browser-playwright/src/commands/select.ts +++ b/packages/browser-playwright/src/commands/select.ts @@ -1,3 +1,4 @@ +import type { SerializedLocator } from '@vitest/browser' import type { ElementHandle } from 'playwright' import type { UserEvent } from 'vitest/browser' import type { UserEventCommand } from './utils' @@ -9,7 +10,7 @@ export const selectOptions: UserEventCommand = async userValues, options = {}, ) => { - const value = userValues as any as (string | { element: string })[] + const value = userValues as any as (string | { element: SerializedLocator })[] const selectElement = getDescribedLocator(context, selector) const values = await Promise.all(value.map(async (v) => { diff --git a/packages/browser-playwright/src/commands/trace.ts b/packages/browser-playwright/src/commands/trace.ts index addb43f87..f92563a36 100644 --- a/packages/browser-playwright/src/commands/trace.ts +++ b/packages/browser-playwright/src/commands/trace.ts @@ -1,3 +1,4 @@ +import type { SerializedLocator } from '@vitest/browser' import type { ParsedStack } from 'vitest' import type { BrowserCommand, BrowserCommandContext, BrowserProvider } from 'vitest/node' import type { PlaywrightBrowserProvider } from '../playwright' @@ -58,7 +59,7 @@ export const stopChunkTrace: BrowserCommand<[{ name: string }]> = async ( throw new TypeError(`The ${context.provider.name} provider does not support tracing.`) } -export const markTrace: BrowserCommand<[payload: { name: string; selector?: string; stack?: string }]> = async ( +export const markTrace: BrowserCommand<[payload: { name: string; element?: SerializedLocator; stack?: string }]> = async ( context, payload, ) => { @@ -69,14 +70,14 @@ export const markTrace: BrowserCommand<[payload: { name: string; selector?: stri if (!context.provider.tracingContexts.has(context.sessionId)) { return } - const { name, selector, stack } = payload + const { name, element, stack } = payload const location = parseLocation(context, stack) // mark trace via group/groupEnd with dummy calls to force snapshot. // https://github.com/microsoft/playwright/issues/39308 await context.context.tracing.group(name, { location }) try { - if (selector) { - const locator = getDescribedLocator(context, selector) as any + if (element) { + const locator = getDescribedLocator(context, element) as any if (typeof locator._expect === 'function') { await locator._expect('to.be.attached', { isNot: false, diff --git a/packages/browser-playwright/src/commands/upload.ts b/packages/browser-playwright/src/commands/upload.ts index 2f2f9763d..26760ffdc 100644 --- a/packages/browser-playwright/src/commands/upload.ts +++ b/packages/browser-playwright/src/commands/upload.ts @@ -1,9 +1,10 @@ +import type { SerializedLocator } from '@vitest/browser' import type { UserEventUploadOptions } from 'vitest/browser' import type { UserEventCommand } from './utils' import { resolve } from 'pathe' import { getDescribedLocator } from './utils' -export const upload: UserEventCommand<(element: string, files: Array any> = BrowserCommand< ConvertUserEventParameters> > -type ConvertElementToLocator = T extends Element | Locator ? string : T +type ConvertElementToLocator = T extends Element | Locator ? SerializedLocator : T type ConvertUserEventParameters = { [K in keyof T]: ConvertElementToLocator; } @@ -23,10 +23,10 @@ export function defineBrowserCommand( // - getByRole('button') export function getDescribedLocator( context: BrowserCommandContext, - selector: string, + { locator, selector }: SerializedLocator, ): ReturnType { - const locator = context.iframe.locator(selector) - return typeof locator.describe === 'function' - ? locator.describe(asLocator('javascript', selector)) - : locator + const iframeLocator = context.iframe.locator(selector) + return typeof iframeLocator.describe === 'function' + ? iframeLocator.describe(locator) + : iframeLocator } diff --git a/packages/browser-webdriverio/src/commands/clear.ts b/packages/browser-webdriverio/src/commands/clear.ts index 214f72afa..e9ddc1bb1 100644 --- a/packages/browser-webdriverio/src/commands/clear.ts +++ b/packages/browser-webdriverio/src/commands/clear.ts @@ -3,7 +3,7 @@ import type { UserEventCommand } from './utils' export const clear: UserEventCommand = async ( context, - selector, + { selector }, ) => { const browser = context.browser await browser.$(selector).clearValue() diff --git a/packages/browser-webdriverio/src/commands/click.ts b/packages/browser-webdriverio/src/commands/click.ts index 997850a8d..088ddd63b 100644 --- a/packages/browser-webdriverio/src/commands/click.ts +++ b/packages/browser-webdriverio/src/commands/click.ts @@ -3,7 +3,7 @@ import type { UserEventCommand } from './utils' export const click: UserEventCommand = async ( context, - selector, + { selector }, options, ) => { const browser = context.browser @@ -12,7 +12,7 @@ export const click: UserEventCommand = async ( export const dblClick: UserEventCommand = async ( context, - selector, + { selector }, _options, ) => { const browser = context.browser @@ -21,7 +21,7 @@ export const dblClick: UserEventCommand = async ( export const tripleClick: UserEventCommand = async ( context, - selector, + { selector }, _options, ) => { const browser = context.browser diff --git a/packages/browser-webdriverio/src/commands/dragAndDrop.ts b/packages/browser-webdriverio/src/commands/dragAndDrop.ts index 549f4dbcc..f32830ad0 100644 --- a/packages/browser-webdriverio/src/commands/dragAndDrop.ts +++ b/packages/browser-webdriverio/src/commands/dragAndDrop.ts @@ -3,8 +3,8 @@ import type { UserEventCommand } from './utils' export const dragAndDrop: UserEventCommand = async ( context, - source, - target, + { selector: source }, + { selector: target }, options_, ) => { const $source = context.browser.$(source) diff --git a/packages/browser-webdriverio/src/commands/fill.ts b/packages/browser-webdriverio/src/commands/fill.ts index f3f0b1ebb..fd4a5e7d4 100644 --- a/packages/browser-webdriverio/src/commands/fill.ts +++ b/packages/browser-webdriverio/src/commands/fill.ts @@ -3,7 +3,7 @@ import type { UserEventCommand } from './utils' export const fill: UserEventCommand = async ( context, - selector, + { selector }, text, _options = {}, ) => { diff --git a/packages/browser-webdriverio/src/commands/hover.ts b/packages/browser-webdriverio/src/commands/hover.ts index 3a0f5d248..8731315b1 100644 --- a/packages/browser-webdriverio/src/commands/hover.ts +++ b/packages/browser-webdriverio/src/commands/hover.ts @@ -3,7 +3,7 @@ import type { UserEventCommand } from './utils' export const hover: UserEventCommand = async ( context, - selector, + { selector }, options = {}, ) => { const browser = context.browser diff --git a/packages/browser-webdriverio/src/commands/screenshot.ts b/packages/browser-webdriverio/src/commands/screenshot.ts index de74dede0..826000db5 100644 --- a/packages/browser-webdriverio/src/commands/screenshot.ts +++ b/packages/browser-webdriverio/src/commands/screenshot.ts @@ -1,3 +1,4 @@ +import type { SerializedLocator } from '@vitest/browser' import type { ScreenshotOptions } from 'vitest/browser' import type { BrowserCommandContext } from 'vitest/node' import crypto from 'node:crypto' @@ -7,8 +8,8 @@ import { resolveScreenshotPath } from '@vitest/browser' import { dirname, normalize, resolve } from 'pathe' interface ScreenshotCommandOptions extends Omit { - element?: string - mask?: readonly string[] + element?: SerializedLocator + mask?: readonly SerializedLocator[] } /** @@ -54,7 +55,7 @@ export async function takeScreenshot( const page = context.browser const element = !options.element ? await page.$('body') - : await page.$(`${options.element}`) + : await page.$(`${options.element.selector}`) // webdriverio expects the path to contain the extension and only works with PNG files const savePathWithExtension = savePath.endsWith('.png') ? savePath : `${savePath}.png` diff --git a/packages/browser-webdriverio/src/commands/select.ts b/packages/browser-webdriverio/src/commands/select.ts index 409633803..0eacc9718 100644 --- a/packages/browser-webdriverio/src/commands/select.ts +++ b/packages/browser-webdriverio/src/commands/select.ts @@ -3,7 +3,7 @@ import type { UserEventCommand } from './utils' export const selectOptions: UserEventCommand = async ( context, - selector, + { selector }, userValues, _options = {}, ) => { diff --git a/packages/browser-webdriverio/src/commands/type.ts b/packages/browser-webdriverio/src/commands/type.ts index 24b71aa58..935b57500 100644 --- a/packages/browser-webdriverio/src/commands/type.ts +++ b/packages/browser-webdriverio/src/commands/type.ts @@ -4,7 +4,7 @@ import { keyboardImplementation } from './keyboard' export const type: UserEventCommand = async ( context, - selector, + { selector }, text, options = {}, ) => { diff --git a/packages/browser-webdriverio/src/commands/upload.ts b/packages/browser-webdriverio/src/commands/upload.ts index 77647c693..30b7ec000 100644 --- a/packages/browser-webdriverio/src/commands/upload.ts +++ b/packages/browser-webdriverio/src/commands/upload.ts @@ -1,14 +1,15 @@ +import type { SerializedLocator } from '@vitest/browser' import type { UserEventUploadOptions } from 'vitest/browser' import type { UserEventCommand } from './utils' import { resolve } from 'pathe' -export const upload: UserEventCommand<(element: string, files: Array, options: UserEventUploadOptions) => void> = async ( context, - selector, + { selector }, files, _options, ) => { diff --git a/packages/browser-webdriverio/src/commands/utils.ts b/packages/browser-webdriverio/src/commands/utils.ts index 89afac85a..f42359767 100644 --- a/packages/browser-webdriverio/src/commands/utils.ts +++ b/packages/browser-webdriverio/src/commands/utils.ts @@ -1,3 +1,4 @@ +import type { SerializedLocator } from '@vitest/browser' import type { Locator } from 'vitest/browser' import type { BrowserCommand } from 'vitest/node' @@ -5,7 +6,7 @@ export type UserEventCommand any> = BrowserCommand< ConvertUserEventParameters> > -type ConvertElementToLocator = T extends Element | Locator ? string : T +type ConvertElementToLocator = T extends Element | Locator ? SerializedLocator : T type ConvertUserEventParameters = { [K in keyof T]: ConvertElementToLocator; } diff --git a/packages/browser-webdriverio/src/commands/wheel.ts b/packages/browser-webdriverio/src/commands/wheel.ts index 9a06417e8..ce5a45527 100644 --- a/packages/browser-webdriverio/src/commands/wheel.ts +++ b/packages/browser-webdriverio/src/commands/wheel.ts @@ -5,7 +5,7 @@ type WheelCommand = (element: Locator | Element, options: UserEventWheelDeltaOpt export const wheel: UserEventCommand = async ( context, - selector, + { selector }, options, ) => { const browser = context.browser diff --git a/packages/browser-webdriverio/src/locators.ts b/packages/browser-webdriverio/src/locators.ts index e7210d1b9..5ce960005 100644 --- a/packages/browser-webdriverio/src/locators.ts +++ b/packages/browser-webdriverio/src/locators.ts @@ -82,9 +82,11 @@ class WebdriverIOLocator extends Locator { return ensureAwaited(async (error) => { const element = await this.findElement(options) const values = getWebdriverioSelectOptions(element, value) + const selector = convertElementToCssSelector(element) + const locator = this.asLocator() return triggerCommandWithTrace({ name: '__vitest_selectOptions', - arguments: [convertElementToCssSelector(element), values, options], + arguments: [{ selector, locator }, values, options], errorSource: error, }) }) diff --git a/packages/browser/context.d.ts b/packages/browser/context.d.ts index f99c32e27..b3b801101 100644 --- a/packages/browser/context.d.ts +++ b/packages/browser/context.d.ts @@ -539,6 +539,11 @@ export interface LocatorSelectors { export interface FrameLocator extends LocatorSelectors {} +export interface SerializedLocator { + selector: string + locator: string +} + export interface SelectorOptions { /** * How long to wait until a single element is found. By default, this has the same timeout as the test. @@ -579,6 +584,32 @@ export interface Locator extends LocatorSelectors { */ readonly length: number + /** + * Returns a JSON-serializable representation of the locator with two fields: + * - `selector`: the provider-specific selector string used to query the element at runtime. + * - `locator`: a human-readable description of the locator (e.g. `getByRole('button')`), + * used for error messages and tracing. + * + * Use this to forward a locator to a [browser command](https://vitest.dev/api/browser/commands), + * which runs in Node and cannot receive a live `Locator` instance. Vitest also auto-serializes + * any `Locator` argument passed to a command, so calling `serialize()` explicitly is rarely necessary. + * + * @see {@link https://vitest.dev/api/browser/locators#serialize} + */ + serialize(): SerializedLocator + /** + * Alias of {@link serialize}. Defined so that `JSON.stringify(locator)` and + * structured-clone-based transports return a {@link SerializedLocator} object. + * + * @see {@link https://vitest.dev/api/browser/locators#tojson} + */ + toJSON(): SerializedLocator + /** + * A human-readable description of the locator (e.g. `getByRole('button')`). + * + * @see {@link https://vitest.dev/api/browser/locators#aslocator} + */ + asLocator(): string /** * Click on an element. You can use the options to set the cursor position. * @see {@link https://vitest.dev/api/browser/interactivity#userevent-click} @@ -934,7 +965,7 @@ export const utils: { /** * Creates "Cannot find element" error. Useful for custom locators. */ - getElementError(selector: string, container?: Element): Error + getElementError(selector: string | Locator, container?: Element): Error /** * Utilities for generating and working with ARIA trees and templates. diff --git a/packages/browser/package.json b/packages/browser/package.json index 3d7d1bd89..661f9214c 100644 --- a/packages/browser/package.json +++ b/packages/browser/package.json @@ -31,6 +31,7 @@ "default": "./context.js" }, "./client": { + "__vitest_source__": "./src/client/client.ts", "default": "./dist/client.js" }, "./matchers": { @@ -38,6 +39,7 @@ "default": "./dummy.js" }, "./locators": { + "__vitest_source__": "./src/client/tester/locators.ts", "types": "./dist/locators.d.ts", "default": "./dist/locators.js" }, @@ -86,6 +88,7 @@ "devDependencies": { "@opentelemetry/api": "^1.9.0", "@testing-library/user-event": "^14.6.1", + "@types/node": "catalog:", "@types/pngjs": "^6.0.5", "@types/ws": "catalog:", "@vitest/runner": "workspace:*", diff --git a/packages/browser/rollup.config.js b/packages/browser/rollup.config.js index b7ed904c7..3e2bcd8c9 100644 --- a/packages/browser/rollup.config.js +++ b/packages/browser/rollup.config.js @@ -12,7 +12,9 @@ const pkg = require('./package.json') const external = [ ...Object.keys(pkg.dependencies), ...Object.keys(pkg.peerDependencies || {}), - /^@?vitest(\/|$)/, + /^vitest(\/|$)/, + /^@vitest\/utils\//, + /^@vitest\/mocker\//, '@vitest/browser/utils', '@vitest/browser/context', '@vitest/browser/client', @@ -33,6 +35,7 @@ const dtsUtilsClient = createDtsUtils({ const plugins = [ resolve({ preferBuiltins: true, + exportConditions: ['__vitest_source__'], }), json(), commonjs(), @@ -73,9 +76,10 @@ export default () => ...plugins, ], }, + // ivya chunk { input: { - 'locators': './src/client/tester/locators/index.ts', + 'locators': './src/client/tester/locators.ts', 'expect-element': './src/client/tester/expect-element.ts', }, output: { diff --git a/packages/browser/src/client/channel.ts b/packages/browser/src/client/channel.ts index e8cff74da..687346eac 100644 --- a/packages/browser/src/client/channel.ts +++ b/packages/browser/src/client/channel.ts @@ -1,5 +1,5 @@ import type { CancelReason, FileSpecification } from '@vitest/runner' -import type { OTELCarrier } from 'vitest/internal/browser' +import type { OTELCarrier } from 'vitest/internal/traces' import { getBrowserState } from './utils' export interface IframeViewportEvent { diff --git a/packages/browser/src/client/orchestrator.ts b/packages/browser/src/client/orchestrator.ts index 4e16be08d..5fb35d1e4 100644 --- a/packages/browser/src/client/orchestrator.ts +++ b/packages/browser/src/client/orchestrator.ts @@ -3,9 +3,8 @@ import type { GlobalChannelIncomingEvent, IframeChannelIncomingEvent, IframeChan import type { FileSpecification } from '@vitest/runner' import type { BrowserTesterOptions, SerializedConfig } from 'vitest' import { channel, client, globalChannel } from '@vitest/browser/client' -import { generateFileHash } from '@vitest/runner/utils' import { relative } from 'pathe' -import { Traces } from 'vitest/internal/browser' +import { Traces } from 'vitest/internal/traces' import { getUiAPI } from './ui' import { getBrowserState, getConfig } from './utils' @@ -465,7 +464,20 @@ async function getContainer(config: SerializedConfig): Promise { function generateFileId(file: string) { const config = getConfig() const path = relative(config.root, file) - return generateFileHash(path, config.name) + return generateHash(`${path}${config.name || ''}`) +} + +function generateHash(str: string): string { + let hash = 0 + if (str.length === 0) { + return `${hash}` + } + for (let i = 0; i < str.length; i++) { + const char = str.charCodeAt(i) + hash = (hash << 5) - hash + char + hash = hash & hash // Convert to 32bit integer + } + return `${hash}` } async function setIframeViewport( diff --git a/packages/browser/src/client/tester/context.ts b/packages/browser/src/client/tester/context.ts index 76158be18..0bcd5b39b 100644 --- a/packages/browser/src/client/tester/context.ts +++ b/packages/browser/src/client/tester/context.ts @@ -15,12 +15,12 @@ import type { 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 type { Locator as LocatorAPI } from './locators' import type { BrowserTraceEntryStatus } from './trace' import { vi } from 'vitest' import { __INTERNAL, stringify } from 'vitest/internal/browser' import { ensureAwaited, getBrowserState, getWorkerState, now } from '../utils' -import { convertToSelector, isLocator, processTimeoutOptions, resolveUserEventWheelOptions } from './tester-utils' +import { isLocator, processTimeoutOptions, resolveUserEventWheelOptions, serializeElement } from './tester-utils' import { recordBrowserTraceEntry } from './trace' // this file should not import anything directly, only types and utils @@ -106,11 +106,11 @@ export function createUserEvent(__tl_user_event_base__?: TestingLibraryUserEvent // testing-library user-event type(element, text, options) { return ensureAwaited(async (error) => { - const selector = await convertToSelector(element, options) + const serializedElement = await serializeElement(element, options) const { unreleased } = await triggerCommand<{ unreleased: string[] }>( '__vitest_type', [ - selector, + serializedElement, text, { ...options, unreleased: keyboard.unreleased }, ], @@ -334,9 +334,9 @@ export const page: BrowserPage = { = options.path || `${taskName.replace(/[^a-z0-9]/gi, '-')}-${number}.png` const [element, ...mask] = await Promise.all([ - options.element ? convertToSelector(options.element, options) : undefined, + options.element ? serializeElement(options.element, options) : undefined, ...('mask' in options - ? (options.mask as Array).map(el => convertToSelector(el, options)) + ? (options.mask as Array).map(el => serializeElement(el, options)) : []), ]) @@ -572,8 +572,8 @@ function prettyDOM( : 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)}`) +function getElementError(selector: string | Locator, container: Element): Error { + const error = new Error(`Cannot find element with locator: ${typeof selector === 'string' ? __INTERNAL._asLocator('javascript', selector) : selector.asLocator()}\n\n${prettyDOM(container)}`) error.name = 'VitestBrowserElementError' return error } diff --git a/packages/browser/src/client/tester/expect-element.ts b/packages/browser/src/client/tester/expect-element.ts index 5241bdcf1..a2d93a66a 100644 --- a/packages/browser/src/client/tester/expect-element.ts +++ b/packages/browser/src/client/tester/expect-element.ts @@ -68,11 +68,11 @@ function element(elementOrL chai.util.flag(expectElement, '_poll.onSettled', async (meta: { assertion: Assertion; status: BrowserTraceEntryStatus }) => { const isNot = chai.util.flag(meta.assertion, 'negate') const name = chai.util.flag(meta.assertion, '_name') || '' - const baseName = `expect.element().${isNot ? 'not.' : ''}${name}` + const baseName = `${isNot ? 'not.' : ''}${name}` const traceName = meta.status === 'fail' ? `${baseName} [ERROR]` : baseName const selector = !elementOrLocator || elementOrLocator instanceof Element ? undefined - : elementOrLocator.selector + : elementOrLocator.serialize() if (hasActiveTraceView) { recordBrowserTraceEntry(currentTest, { name: traceName, @@ -80,7 +80,7 @@ function element(elementOrL status: meta.status, startTime, duration: now() - startTime, - selector, + element: selector, stack: sourceError.stack, }) } @@ -89,7 +89,7 @@ function element(elementOrL '__vitest_markTrace', [{ name: traceName, - selector, + element: selector, stack: sourceError.stack, }], sourceError, diff --git a/packages/browser/src/client/tester/expect/toMatchScreenshot.ts b/packages/browser/src/client/tester/expect/toMatchScreenshot.ts index 16f454daa..0281fe710 100644 --- a/packages/browser/src/client/tester/expect/toMatchScreenshot.ts +++ b/packages/browser/src/client/tester/expect/toMatchScreenshot.ts @@ -5,7 +5,7 @@ import type { ScreenshotMatcherArguments, ScreenshotMatcherOutput } from '../../ import type { Locator } from '../locators' import { recordArtifact } from 'vitest' import { getBrowserState } from '../../utils' -import { convertToSelector } from '../tester-utils' +import { serializeElement } from '../tester-utils' const counters = new Map([]) @@ -41,10 +41,10 @@ export default async function toMatchScreenshot( : `${this.currentTestName} ${counter.current}` const [element, ...mask] = await Promise.all([ - convertToSelector(actual, options), + serializeElement(actual, options), ...options.screenshotOptions && 'mask' in options.screenshotOptions ? (options.screenshotOptions.mask as Array) - .map(m => convertToSelector(m, options)) + .map(m => serializeElement(m, options)) : [], ]) diff --git a/packages/browser/src/client/tester/locators/index.ts b/packages/browser/src/client/tester/locators.ts similarity index 87% rename from packages/browser/src/client/tester/locators/index.ts rename to packages/browser/src/client/tester/locators.ts index 75ebaea16..4816f54ff 100644 --- a/packages/browser/src/client/tester/locators/index.ts +++ b/packages/browser/src/client/tester/locators.ts @@ -27,12 +27,12 @@ import { } from 'ivya' import { page, server, utils } from 'vitest/browser' import { __INTERNAL, getSafeTimers } from 'vitest/internal/browser' -import { ensureAwaited, getBrowserState, getWorkerState } from '../../utils' -import { escapeForTextSelector, isLocator, processTimeoutOptions, resolveUserEventWheelOptions } from '../tester-utils' -import { recordBrowserTraceEntry } from '../trace' +import { ensureAwaited, getBrowserState, getWorkerState } from '../utils' +import { convertElementToCssSelector, escapeForTextSelector, isLocator, processTimeoutOptions, resolveUserEventWheelOptions } from './tester-utils' +import { recordBrowserTraceEntry } from './trace' -export { ensureAwaited } from '../../utils' -export { convertElementToCssSelector, getIframeScale, processTimeoutOptions } from '../tester-utils' +export { ensureAwaited } from '../utils' +export { convertElementToCssSelector, getIframeScale, processTimeoutOptions } from './tester-utils' export { getByAltTextSelector, getByLabelSelector, @@ -79,6 +79,7 @@ export abstract class Locator { private _parsedSelector: ParsedSelector | undefined protected _container?: Element | undefined protected _pwSelector?: string | undefined + protected _pwLocator?: string | undefined protected _errorSource?: Error constructor() { @@ -91,22 +92,22 @@ export abstract class Locator { } public click(options?: UserEventClickOptions): Promise { - return this.triggerCommand('__vitest_click', this.selector, options) + return this.triggerCommand('__vitest_click', this.serialize(), options) } public dblClick(options?: UserEventClickOptions): Promise { - return this.triggerCommand('__vitest_dblClick', this.selector, options) + return this.triggerCommand('__vitest_dblClick', this.serialize(), options) } public tripleClick(options?: UserEventClickOptions): Promise { - return this.triggerCommand('__vitest_tripleClick', this.selector, options) + return this.triggerCommand('__vitest_tripleClick', this.serialize(), options) } public wheel(options: UserEventWheelOptions): Promise { return ensureAwaited(async (error) => { await getBrowserState().commands.triggerCommand( '__vitest_wheel', - [this.selector, resolveUserEventWheelOptions(options)], + [this.serialize(), resolveUserEventWheelOptions(options)], error, ) @@ -124,19 +125,19 @@ export abstract class Locator { } public clear(options?: UserEventClearOptions): Promise { - return this.triggerCommand('__vitest_clear', this.selector, options) + return this.triggerCommand('__vitest_clear', this.serialize(), options) } public hover(options?: UserEventHoverOptions): Promise { - return this.triggerCommand('__vitest_hover', this.selector, options) + return this.triggerCommand('__vitest_hover', this.serialize(), options) } public unhover(options?: UserEventHoverOptions): Promise { - return this.triggerCommand('__vitest_hover', 'html > body', options) + return this.triggerCommand('__vitest_hover', { selector: 'html > body', locator: 'locator(\'body\')' }, options) } public fill(text: string, options?: UserEventFillOptions): Promise { - return this.triggerCommand('__vitest_fill', this.selector, text, options) + return this.triggerCommand('__vitest_fill', this.serialize(), text, options) } public upload(files: string | string[] | File | File[], options?: UserEventUploadOptions): Promise { @@ -161,7 +162,7 @@ export abstract class Locator { }) return getBrowserState().commands.triggerCommand( '__vitest_upload', - [this.selector, await Promise.all(filesPromise), options], + [this.serialize(), await Promise.all(filesPromise), options], error, ) }) @@ -170,8 +171,8 @@ export abstract class Locator { public dropTo(target: Locator, options: UserEventDragAndDropOptions = {}): Promise { return this.triggerCommand( '__vitest_dragAndDrop', - this.selector, - target.selector, + this.toJSON(), + target.toJSON(), options, ) } @@ -182,12 +183,17 @@ export abstract class Locator { ): Promise { const values = (Array.isArray(value) ? value : [value]).map((v) => { if (typeof v !== 'string') { - const selector = isLocator(v) ? v.selector : selectorEngine.generateSelectorSimple(v) - return { element: selector } + const element: SerializedLocator = isLocator(v) + ? v.serialize() + : { + selector: convertElementToCssSelector(v), + locator: __INTERNAL._asLocator('javascript', selectorEngine.generateSelectorSimple(v)), + } + return { element } } return v }) - return this.triggerCommand('__vitest_selectOptions', this.selector, values, options) + return this.triggerCommand('__vitest_selectOptions', this.serialize(), values, options) } public screenshot(options: Omit & { base64: true }): Promise<{ @@ -217,7 +223,7 @@ export abstract class Locator { recordBrowserTraceEntry(currentTest, { name, kind: 'mark', - selector: this.selector, + element: this.serialize(), stack: options?.stack ?? error?.stack, }) } @@ -228,7 +234,7 @@ export abstract class Locator { '__vitest_markTrace', [{ name, - selector: this.selector, + element: this.serialize(), stack: options?.stack ?? error?.stack, }], error, @@ -345,8 +351,19 @@ export abstract class Locator { return this.selector } - public toJSON(): string { - return this.selector + public serialize(): SerializedLocator { + return { + selector: this.selector, + locator: this.asLocator(), + } + } + + public asLocator(): string { + return this._pwLocator || (this._pwLocator = asLocator('javascript', this._pwSelector || this.selector)) + } + + public toJSON(): SerializedLocator { + return this.serialize() } public async findElement(options_: SelectorOptions = {}): Promise { @@ -409,6 +426,11 @@ export function triggerCommandWithTrace( ) } +export interface SerializedLocator { + selector: string + locator: string +} + function createStrictModeViolationError( selector: string, matches: Element[], diff --git a/packages/browser/src/client/tester/runner.ts b/packages/browser/src/client/tester/runner.ts index 0b6e884a0..945d55d91 100644 --- a/packages/browser/src/client/tester/runner.ts +++ b/packages/browser/src/client/tester/runner.ts @@ -11,9 +11,7 @@ import type { VitestRunner, } from '@vitest/runner' import type { SerializedConfig, TestExecutionMethod, WorkerGlobalState } from 'vitest' -import type { - Traces, -} from 'vitest/internal/browser' +import type { Traces } from 'vitest/internal/traces' import type { VitestBrowserClientMocker } from './mocker' import type { CommandsManager } from './tester-utils' import { globalChannel, onCancel } from '@vitest/browser/client' diff --git a/packages/browser/src/client/tester/tester-utils.ts b/packages/browser/src/client/tester/tester-utils.ts index e9290476c..6a0fbc1d0 100644 --- a/packages/browser/src/client/tester/tester-utils.ts +++ b/packages/browser/src/client/tester/tester-utils.ts @@ -1,6 +1,7 @@ -import type { Locator, SelectorOptions, UserEventWheelDeltaOptions, UserEventWheelOptions } from 'vitest/browser' +import type { Locator, SelectorOptions, SerializedLocator, UserEventWheelDeltaOptions, UserEventWheelOptions } from 'vitest/browser' import type { BrowserRPC } from '../client' import type { BrowserTraceEntryStatus } from './trace' +import { __INTERNAL } from 'vitest/internal/browser' import { getBrowserState, getWorkerState, now } from '../utils' import { recordBrowserTraceEntry } from './trace' @@ -189,7 +190,7 @@ export class CommandsManager { status, startTime, duration: now() - startTime, - selector: typeof args[0] === 'string' ? args[0] : undefined, + element: typeof args[0] === 'object' && 'selector' in args[0] && 'locator' in args[0] ? args[0] : undefined, stack: clientError.stack, }) } @@ -276,19 +277,22 @@ export function escapeForTextSelector(text: string | RegExp, exact: boolean): st const provider = getBrowserState().provider const kElementLocator = Symbol.for('$$vitest:locator-resolved') -export async function convertToSelector(elementOrLocator: Element | Locator, options?: SelectorOptions): Promise { +export async function serializeElement(elementOrLocator: Element | Locator, options?: SelectorOptions): Promise { if (!elementOrLocator) { throw new Error('Expected element or locator to be defined.') } if (elementOrLocator instanceof Element) { - return convertElementToCssSelector(elementOrLocator) + const selector = convertElementToCssSelector(elementOrLocator) + return { selector, locator: __INTERNAL._asLocator('javascript', selector) } } if (isLocator(elementOrLocator)) { if (provider === 'playwright' || kElementLocator in elementOrLocator) { - return elementOrLocator.selector + return elementOrLocator.serialize() } const element = await elementOrLocator.findElement(options) - return convertElementToCssSelector(element) + const selector = convertElementToCssSelector(element) + const locator = __INTERNAL._asLocator('javascript', selector) + return { selector, locator } } throw new Error('Expected element or locator to be an instance of Element or Locator.') } diff --git a/packages/browser/src/client/tester/tester.ts b/packages/browser/src/client/tester/tester.ts index 691bd7225..c82152911 100644 --- a/packages/browser/src/client/tester/tester.ts +++ b/packages/browser/src/client/tester/tester.ts @@ -10,8 +10,8 @@ import { startCoverageInsideWorker, startTests, stopCoverageInsideWorker, - Traces, } from 'vitest/internal/browser' +import { Traces } from 'vitest/internal/traces' import { getBrowserState, getConfig, getWorkerState, moduleRunner } from '../utils' import { setupDialogsSpy } from './dialog' import { setupConsoleLogSpy } from './logger' diff --git a/packages/browser/src/client/tester/trace.ts b/packages/browser/src/client/tester/trace.ts index a679cc434..9ec15b8c9 100644 --- a/packages/browser/src/client/tester/trace.ts +++ b/packages/browser/src/client/tester/trace.ts @@ -1,4 +1,5 @@ import type { Task } from '@vitest/runner' +import type { SerializedLocator } from './locators' import { getBrowserState, now } from '../utils' export interface BrowserTraceData { @@ -22,7 +23,7 @@ export interface BrowserTraceEntry { stack?: string // resolved server-side from stack in __vitest_recordBrowserTrace command location?: { file: string; line: number; column: number } - selector?: string + element?: SerializedLocator snapshot: TraceSnapshot } @@ -91,12 +92,12 @@ export function recordBrowserTraceEntry( // For now, remove trivial `html >` prefix generated by convertElementToCssSelector. // this is also necessary to `engine.querySelector + document.documentElement` // to find an element on webdriverio - if (options.selector?.startsWith('html >')) { - options.selector = options.selector.slice(6) + if (options.element?.selector?.startsWith('html >')) { + options.element.selector = options.element.selector.slice(6) } const attemptInfo = getBrowserState().browserTraceAttempts.get(task.id)! const relativeStartTime = (options.startTime ?? now()) - attemptInfo.startTime - const snapshot = takeSnapshot(options.selector) + const snapshot = takeSnapshot(options.element?.selector) const entry: BrowserTraceEntry = { ...options, startTime: relativeStartTime, diff --git a/packages/browser/src/client/utils.ts b/packages/browser/src/client/utils.ts index 4439efa59..063930468 100644 --- a/packages/browser/src/client/utils.ts +++ b/packages/browser/src/client/utils.ts @@ -1,7 +1,7 @@ import type { VitestRunner } from '@vitest/runner' import type { Ivya } from 'ivya' import type { SerializedConfig, WorkerGlobalState } from 'vitest' -import type { OTELCarrier, Traces } from 'vitest/internal/browser' +import type { OTELCarrier, Traces } from 'vitest/internal/traces' import type { IframeOrchestrator } from './orchestrator' import type { CommandsManager } from './tester/tester-utils' import type { BrowserTraceAttempt, BrowserTraceState } from './tester/trace' diff --git a/packages/browser/src/node/commands/screenshot.ts b/packages/browser/src/node/commands/screenshot.ts index 7e5dce337..911499eaa 100644 --- a/packages/browser/src/node/commands/screenshot.ts +++ b/packages/browser/src/node/commands/screenshot.ts @@ -1,9 +1,9 @@ import type { BrowserCommand } from 'vitest/node' -import type { ScreenshotOptions } from '../../../context' +import type { ScreenshotOptions, SerializedLocator } from '../../../context' interface ScreenshotCommandOptions extends Omit { - element?: string - mask?: readonly string[] + element?: SerializedLocator + mask?: readonly SerializedLocator[] } declare module 'vitest/browser' { diff --git a/packages/browser/src/node/commands/screenshotMatcher/index.ts b/packages/browser/src/node/commands/screenshotMatcher/index.ts index df466112c..6e151f75e 100644 --- a/packages/browser/src/node/commands/screenshotMatcher/index.ts +++ b/packages/browser/src/node/commands/screenshotMatcher/index.ts @@ -1,3 +1,4 @@ +import type { SerializedLocator } from '@vitest/browser' import type { SnapshotUpdateState } from 'vitest' import type { ScreenshotMatcherOptions } from 'vitest/browser' import type { BrowserCommand, BrowserCommandContext } from 'vitest/node' @@ -352,7 +353,7 @@ interface StableScreenshotOptions { comparator: AnyComparator comparatorOptions: ScreenshotMatcherOptions['comparatorOptions'] context: BrowserCommandContext - element: string + element: SerializedLocator name: string reference: ReturnType | null screenshotOptions: ScreenshotMatcherArguments[2]['screenshotOptions'] diff --git a/packages/browser/src/node/commands/screenshotMatcher/utils.ts b/packages/browser/src/node/commands/screenshotMatcher/utils.ts index 981680557..1eb3cb7ef 100644 --- a/packages/browser/src/node/commands/screenshotMatcher/utils.ts +++ b/packages/browser/src/node/commands/screenshotMatcher/utils.ts @@ -1,6 +1,7 @@ +import type { SerializedLocator } from '@vitest/browser' + // Note: this augments `screenshotOptions` types import type {} from '@vitest/browser-playwright' - import type { BrowserCommandContext, BrowserConfigOptions } from 'vitest/node' import type { ScreenshotMatcherOptions } from '../../../../context' import type { ScreenshotMatcherArguments } from '../../../shared/screenshotMatcher/types' @@ -241,7 +242,7 @@ export function takeDecodedScreenshot({ }: { codec: AnyCodec context: BrowserCommandContext - element: string + element: SerializedLocator name: string screenshotOptions: ScreenshotMatcherArguments[2]['screenshotOptions'] }): ReturnType { diff --git a/packages/browser/src/node/commands/trace.ts b/packages/browser/src/node/commands/trace.ts index 6ba31c0fe..bf7b5fca5 100644 --- a/packages/browser/src/node/commands/trace.ts +++ b/packages/browser/src/node/commands/trace.ts @@ -1,10 +1,11 @@ import type { BrowserCommand } from 'vitest/node' +import type { SerializedLocator } from '..' import type { BrowserTraceData, BrowserTraceEntry } from '../../client/tester/trace' interface MarkTracePayload { name: string stack?: string - selector?: string + element?: SerializedLocator } interface GroupTracePayload { diff --git a/packages/browser/src/node/index.ts b/packages/browser/src/node/index.ts index d927fb561..68eaf1eaa 100644 --- a/packages/browser/src/node/index.ts +++ b/packages/browser/src/node/index.ts @@ -11,6 +11,11 @@ import { setupBrowserRpc } from './rpc' export type { CustomComparatorsRegistry } from './commands/screenshotMatcher/types' +export interface SerializedLocator { + selector: string + locator: string +} + export function defineBrowserCommand( fn: BrowserCommand, ): BrowserCommand { @@ -20,8 +25,6 @@ export function defineBrowserCommand( // export type { ProjectBrowser } from './project' export { parseKeyDef, resolveScreenshotPath } from './utils' -export { asLocator } from 'ivya' - export const createBrowserServer: BrowserServerFactory = async (options) => { const project = options.project const configFile = project.vite.config.configFile diff --git a/packages/browser/src/node/plugin.ts b/packages/browser/src/node/plugin.ts index d3e5cd5dc..ea7c05651 100644 --- a/packages/browser/src/node/plugin.ts +++ b/packages/browser/src/node/plugin.ts @@ -279,6 +279,9 @@ export default (parentServer: ParentBrowserProject, base = '/'): Plugin[] => { entries.push(otelConfig.browserSdkPath) include.push('@opentelemetry/api') } + else { + exclude.push('@opentelemetry/api') + } return { define, diff --git a/packages/browser/src/shared/screenshotMatcher/types.ts b/packages/browser/src/shared/screenshotMatcher/types.ts index e95d4e99c..f7ab57d84 100644 --- a/packages/browser/src/shared/screenshotMatcher/types.ts +++ b/packages/browser/src/shared/screenshotMatcher/types.ts @@ -1,4 +1,4 @@ -import type { ScreenshotComparatorRegistry, ScreenshotMatcherOptions } from '../../../context' +import type { ScreenshotComparatorRegistry, ScreenshotMatcherOptions, SerializedLocator } from '../../../context' export type ScreenshotMatcherArguments< ComparatorName extends keyof ScreenshotComparatorRegistry = keyof ScreenshotComparatorRegistry, @@ -7,8 +7,8 @@ export type ScreenshotMatcherArguments< testName: string, options: ScreenshotMatcherOptions & { - element: string - screenshotOptions?: ScreenshotMatcherOptions['screenshotOptions'] & { mask?: readonly string[] } + element: SerializedLocator + screenshotOptions?: ScreenshotMatcherOptions['screenshotOptions'] & { mask?: readonly SerializedLocator[] } }, ] diff --git a/packages/mocker/package.json b/packages/mocker/package.json index 2ff1f6ab5..194f583ed 100644 --- a/packages/mocker/package.json +++ b/packages/mocker/package.json @@ -22,34 +22,42 @@ "sideEffects": false, "exports": { ".": { + "__vitest_source__": "./src/index.ts", "types": "./dist/index.d.ts", "default": "./dist/index.js" }, "./node": { + "__vitest_source__": "./src/node/index.ts", "types": "./dist/node.d.ts", "default": "./dist/node.js" }, "./browser": { + "__vitest_source__": "./src/browser/index.ts", "types": "./dist/browser.d.ts", "default": "./dist/browser.js" }, "./redirect": { + "__vitest_source__": "./src/node/redirect.ts", "types": "./dist/redirect.d.ts", "default": "./dist/redirect.js" }, "./automock": { + "__vitest_source__": "./src/node/automock.ts", "types": "./dist/automock.d.ts", "default": "./dist/automock.js" }, "./register": { + "__vitest_source__": "./src/browser/register.ts", "types": "./dist/register.d.ts", "default": "./dist/register.js" }, "./auto-register": { + "__vitest_source__": "./src/browser/auto-register.ts", "types": "./dist/register.d.ts", "default": "./dist/register.js" }, "./transforms": { + "__vitest_source__": "./src/node/transforms.ts", "types": "./dist/transforms.d.ts", "default": "./dist/transforms.js" }, diff --git a/packages/ui/client/components/trace/TraceView.vue b/packages/ui/client/components/trace/TraceView.vue index f0ee6b72e..7b28113fe 100644 --- a/packages/ui/client/components/trace/TraceView.vue +++ b/packages/ui/client/components/trace/TraceView.vue @@ -6,7 +6,7 @@ import { createCache, createMirror, rebuild } from 'rrweb-snapshot' // @ts-expect-error missing types import { Pane, Splitpanes } from 'splitpanes' import { computed, ref, watch } from 'vue' -import { getLocationString, openLocation } from '~/composables/location' +import { openLocation } from '~/composables/location' // TODO: component test to demo trace view inside trace view @@ -172,16 +172,10 @@ function getStepMarkerClass(step: BrowserTraceEntry) { {{ formatTraceTiming(step) }}
- {{ step.selector }} -
-
- {{ getLocationString(step.location) }} + {{ step.element.locator }}
diff --git a/packages/vitest/LICENSE.md b/packages/vitest/LICENSE.md index d2883c9b3..10f97d9b5 100644 --- a/packages/vitest/LICENSE.md +++ b/packages/vitest/LICENSE.md @@ -500,6 +500,13 @@ Repository: unjs/mlly --------------------------------------- +## natural-compare +License: MIT +By: Lauri Rooden +Repository: git://github.com/litejs/natural-compare-lite.git + +--------------------------------------- + ## package-manager-detector License: MIT By: Anthony Fu diff --git a/packages/vitest/package.json b/packages/vitest/package.json index 0739cee77..c265e70f1 100644 --- a/packages/vitest/package.json +++ b/packages/vitest/package.json @@ -69,9 +69,14 @@ "default": "./dist/node.js" }, "./internal/browser": { + "__vitest_source__": "./src/public/browser.ts", "types": "./dist/browser.d.ts", "default": "./dist/browser.js" }, + "./internal/traces": { + "__vitest_source__": "./src/utils/traces.ts", + "default": "./dist/traces.js" + }, "./runners": { "types": "./dist/runners.d.ts", "default": "./dist/runners.js" diff --git a/packages/vitest/rollup.config.js b/packages/vitest/rollup.config.js index 2daf9b8dc..cb041e917 100644 --- a/packages/vitest/rollup.config.js +++ b/packages/vitest/rollup.config.js @@ -32,6 +32,7 @@ const entries = { 'worker': 'src/public/worker.ts', 'module-evaluator': 'src/runtime/moduleRunner/moduleEvaluator.ts', 'nodejs-worker-loader': 'src/runtime/nodejsWorkerLoader.ts', + 'traces': 'src/utils/traces.ts', // for performance reasons we bundle them separately so we don't import everything at once // 'worker': 'src/runtime/worker.ts', @@ -77,14 +78,13 @@ const external = [ 'vitest/optional-types.js', 'vitest/browser', 'vite/module-runner', - '@vitest/mocker', - /@vitest\/mocker\/\w+/, '@vitest/utils/diff', '@vitest/utils/error', '@vitest/utils/source-map', '@vitest/runner/utils', '@vitest/runner/types', /@vitest\/utils\/\w+/, + /@vitest\/mocker\/\w+/, '#module-evaluator', '@opentelemetry/api', @@ -97,6 +97,7 @@ const dtsUtils = createDtsUtils() const plugins = [ nodeResolve({ preferBuiltins: true, + exportConditions: ['__vitest_source__'], }), json(), commonjs(), diff --git a/packages/vitest/src/public/browser.ts b/packages/vitest/src/public/browser.ts index 3405142e6..8d14408fd 100644 --- a/packages/vitest/src/public/browser.ts +++ b/packages/vitest/src/public/browser.ts @@ -8,7 +8,6 @@ export { loadSnapshotSerializers, setupCommonEnv, } from '../runtime/setup-common' -export { type OTELCarrier, Traces } from '../utils/traces' export { collectTests, startTests } from '@vitest/runner' export * as SpyModule from '@vitest/spy' export type { ParsedStack, StringifyOptions } from '@vitest/utils' diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 78a6fd8a8..fad086f6a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -557,6 +557,9 @@ importers: '@testing-library/user-event': specifier: ^14.6.1 version: 14.6.1(@testing-library/dom@10.4.1) + '@types/node': + specifier: 24.12.0 + version: 24.12.0 '@types/pngjs': specifier: ^6.0.5 version: 6.0.5 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 82dcfdc77..e5b7925a2 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -61,6 +61,7 @@ catalog: '@types/istanbul-lib-report': ^3.0.3 '@types/istanbul-lib-source-maps': ^4.0.4 '@types/istanbul-reports': ^3.0.4 + '@types/node': 24.12.0 '@types/ws': ^8.18.1 '@types/yauzl': ^2.10.3 '@unocss/reset': ^66.6.6 diff --git a/test/browser/specs/playwright-trace-mark.test.ts b/test/browser/specs/playwright-trace-mark.test.ts index 887323801..a1f1f5e3e 100644 --- a/test/browser/specs/playwright-trace-mark.test.ts +++ b/test/browser/specs/playwright-trace-mark.test.ts @@ -141,7 +141,7 @@ describe.runIf(provider.name === 'playwright')('playwright trace marks', () => { expect.arrayContaining([ expect.objectContaining({ method: 'tracingGroup', - title: 'expect.element().toHaveTextContent', + title: 'toHaveTextContent', }), expect.objectContaining({ method: 'expect', @@ -151,7 +151,7 @@ describe.runIf(provider.name === 'playwright')('playwright trace marks', () => { }), ]), ) - const markerEvent = events.find(e => e.title === 'expect.element().toHaveTextContent') + const markerEvent = events.find(e => e.title === 'toHaveTextContent') const formattedFrame = formatStack(markerEvent) if (name === 'webkit') { expect(formattedFrame).toMatchInlineSnapshot(`"basic.test.ts:20:23"`) @@ -170,7 +170,7 @@ describe.runIf(provider.name === 'playwright')('playwright trace marks', () => { }), expect.objectContaining({ method: 'tracingGroup', - title: 'expect.element().toHaveTextContent [ERROR]', + title: 'toHaveTextContent [ERROR]', }), expect.objectContaining({ method: 'expect', @@ -180,7 +180,7 @@ describe.runIf(provider.name === 'playwright')('playwright trace marks', () => { }), ]), ) - const markerEvent = events.find(e => e.title === 'expect.element().toHaveTextContent [ERROR]') + const markerEvent = events.find(e => e.title === 'toHaveTextContent [ERROR]') const formattedFrame = formatStack(markerEvent) if (name === 'webkit') { expect(formattedFrame).toMatchInlineSnapshot(`"basic.test.ts:26:23"`) diff --git a/test/browser/specs/trace.test.ts b/test/browser/specs/trace.test.ts index 9a406187e..bedf8c415 100644 --- a/test/browser/specs/trace.test.ts +++ b/test/browser/specs/trace.test.ts @@ -9,12 +9,12 @@ test('trace view artifacts', async () => { const result = await runBrowserTests({ root: './fixtures/trace', }) - const root = result.ctx.config.root + const root = result.ctx!.config.root function formatLocation(location: BrowserTraceEntry['location']) { // columns can differ between browsers // return `${relative(root, location.file)}:${location.line}:${location.column}` - return `${relative(root, location.file)}:${location.line}` + return `${relative(root, location!.file)}:${location!.line}` } function formatEntry(raw: BrowserTraceEntry) { @@ -32,14 +32,15 @@ test('trace view artifacts', async () => { }, } // remove noisy undefined properties - for (const key of Object.keys(result)) { + for (const _key in result) { + const key = _key as keyof typeof result if (result[key] === undefined) { delete result[key] } if (result[key] && typeof result[key] === 'object') { - for (const subKey of Object.keys(result[key])) { - if (result[key][subKey] === undefined) { - delete result[key][subKey] + for (const subKey in result[key]) { + if ((result as any)[key][subKey] === undefined) { + delete (result as any)[key][subKey] } } } @@ -139,10 +140,13 @@ test('trace view artifacts', async () => { { "entries": [ { + "element": { + "locator": "getByRole('button')", + "selector": " body > button", + }, "kind": "mark", "location": "exotic.test.ts:58", "name": "button rendered with adopted stylesheet", - "selector": " body > button", "snapshot": { "selectorResolution": "matched", }, @@ -161,10 +165,13 @@ test('trace view artifacts', async () => { { "entries": [ { + "element": { + "locator": "getByTestId('trace-canvas')", + "selector": " body > canvas", + }, "kind": "mark", "location": "exotic.test.ts:17", "name": "canvas drawn before mark", - "selector": " body > canvas", "snapshot": { "selectorResolution": "matched", }, @@ -183,10 +190,13 @@ test('trace view artifacts', async () => { { "entries": [ { + "element": { + "locator": "getByRole('button', { name: 'Custom element button' })", + "selector": ">>>html > body > trace-widget > button", + }, "kind": "mark", "location": "exotic.test.ts:47", "name": "custom element rendered", - "selector": ">>>html > body > trace-widget > button", "snapshot": { "selectorError": "Unexpected token "" while parsing selector """, "selectorResolution": "error", @@ -206,10 +216,13 @@ test('trace view artifacts', async () => { { "entries": [ { + "element": { + "locator": "getByRole('button', { name: 'Shadow button' })", + "selector": ">>>html > body > section > button", + }, "kind": "mark", "location": "exotic.test.ts:28", "name": "shadow button rendered", - "selector": ">>>html > body > section > button", "snapshot": { "selectorError": "Unexpected token "" while parsing selector """, "selectorResolution": "error", @@ -231,10 +244,13 @@ test('trace view artifacts', async () => { { "entries": [ { + "element": { + "locator": "getByRole('button', { name: 'Hello' })", + "selector": " body > button", + }, "kind": "action", "location": "expect.test.ts:25", "name": "vitest:click", - "selector": " body > button", "snapshot": { "selectorResolution": "matched", }, @@ -267,10 +283,13 @@ test('trace view artifacts', async () => { { "entries": [ { + "element": { + "locator": "getByRole('button')", + "selector": " body > button", + }, "kind": "expect", "location": "expect.test.ts:15", - "name": "expect.element().toHaveTextContent [ERROR]", - "selector": " body > button", + "name": "toHaveTextContent [ERROR]", "snapshot": { "selectorResolution": "matched", }, @@ -290,10 +309,13 @@ test('trace view artifacts', async () => { { "entries": [ { + "element": { + "locator": "getByRole('button')", + "selector": " body > button", + }, "kind": "expect", "location": "expect.test.ts:10", - "name": "expect.element().toHaveTextContent", - "selector": " body > button", + "name": "toHaveTextContent", "snapshot": { "selectorResolution": "matched", }, @@ -328,10 +350,13 @@ test('trace view artifacts', async () => { { "entries": [ { + "element": { + "locator": "getByRole('button', { name: 'Hello' })", + "selector": " body > button", + }, "kind": "mark", "location": "mark.test.ts:24", "name": "render helper", - "selector": " body > button", "snapshot": { "selectorResolution": "matched", }, @@ -350,10 +375,13 @@ test('trace view artifacts', async () => { { "entries": [ { + "element": { + "locator": "getByRole('button')", + "selector": " body > button", + }, "kind": "mark", "location": "mark.test.ts:10", "name": "button rendered - locator", - "selector": " body > button", "snapshot": { "selectorResolution": "matched", }, @@ -411,10 +439,13 @@ test('trace view artifacts', async () => { { "entries": [ { + "element": { + "locator": "getByRole('button')", + "selector": " body > button", + }, "kind": "mark", "location": "mark.test.ts:29", "name": "button rendered - stack", - "selector": " body > button", "snapshot": { "selectorResolution": "matched", }, @@ -435,10 +466,13 @@ test('trace view artifacts', async () => { { "entries": [ { + "element": { + "locator": "getByAltText('external trace asset')", + "selector": " body > img", + }, "kind": "mark", "location": "resources.test.ts:20", "name": "image rendered from external url", - "selector": " body > img", "snapshot": { "selectorResolution": "matched", }, @@ -457,10 +491,13 @@ test('trace view artifacts', async () => { { "entries": [ { + "element": { + "locator": "getByAltText('local trace asset')", + "selector": " body > img", + }, "kind": "mark", "location": "resources.test.ts:12", "name": "image rendered from same-origin url", - "selector": " body > img", "snapshot": { "selectorResolution": "matched", }, @@ -481,10 +518,13 @@ test('trace view artifacts', async () => { { "entries": [ { + "element": { + "locator": "getByRole('list')", + "selector": " body > ul", + }, "kind": "mark", "location": "retry.test.ts:29", "name": "renderHelper", - "selector": " body > ul", "snapshot": { "selectorResolution": "matched", }, @@ -501,10 +541,13 @@ test('trace view artifacts', async () => { { "entries": [ { + "element": { + "locator": "getByRole('list')", + "selector": " body > ul", + }, "kind": "mark", "location": "retry.test.ts:29", "name": "renderHelper", - "selector": " body > ul", "snapshot": { "selectorResolution": "matched", }, @@ -522,10 +565,13 @@ test('trace view artifacts', async () => { { "entries": [ { + "element": { + "locator": "getByRole('list')", + "selector": " body > ul", + }, "kind": "mark", "location": "retry.test.ts:29", "name": "renderHelper", - "selector": " body > ul", "snapshot": { "selectorResolution": "matched", }, @@ -543,10 +589,13 @@ test('trace view artifacts', async () => { { "entries": [ { + "element": { + "locator": "getByRole('list')", + "selector": " body > ul", + }, "kind": "mark", "location": "retry.test.ts:29", "name": "renderHelper", - "selector": " body > ul", "snapshot": { "selectorResolution": "matched", }, @@ -564,10 +613,13 @@ test('trace view artifacts', async () => { { "entries": [ { + "element": { + "locator": "getByRole('list')", + "selector": " body > ul", + }, "kind": "mark", "location": "retry.test.ts:29", "name": "renderHelper", - "selector": " body > ul", "snapshot": { "selectorResolution": "matched", }, @@ -587,10 +639,13 @@ test('trace view artifacts', async () => { { "entries": [ { + "element": { + "locator": "getByRole('list')", + "selector": " body > ul", + }, "kind": "mark", "location": "retry.test.ts:18", "name": "renderHelper", - "selector": " body > ul", "snapshot": { "selectorResolution": "matched", }, @@ -607,10 +662,13 @@ test('trace view artifacts', async () => { { "entries": [ { + "element": { + "locator": "getByRole('list')", + "selector": " body > ul", + }, "kind": "mark", "location": "retry.test.ts:18", "name": "renderHelper", - "selector": " body > ul", "snapshot": { "selectorResolution": "matched", }, @@ -628,10 +686,13 @@ test('trace view artifacts', async () => { { "entries": [ { + "element": { + "locator": "getByRole('list')", + "selector": " body > ul", + }, "kind": "mark", "location": "retry.test.ts:18", "name": "renderHelper", - "selector": " body > ul", "snapshot": { "selectorResolution": "matched", }, @@ -651,10 +712,13 @@ test('trace view artifacts', async () => { { "entries": [ { + "element": { + "locator": "getByRole('list')", + "selector": " body > ul", + }, "kind": "mark", "location": "retry.test.ts:36", "name": "renderHelper", - "selector": " body > ul", "snapshot": { "selectorResolution": "matched", }, @@ -671,10 +735,13 @@ test('trace view artifacts', async () => { { "entries": [ { + "element": { + "locator": "getByRole('list')", + "selector": " body > ul", + }, "kind": "mark", "location": "retry.test.ts:36", "name": "renderHelper", - "selector": " body > ul", "snapshot": { "selectorResolution": "matched", }, @@ -692,10 +759,13 @@ test('trace view artifacts', async () => { { "entries": [ { + "element": { + "locator": "getByRole('list')", + "selector": " body > ul", + }, "kind": "mark", "location": "retry.test.ts:36", "name": "renderHelper", - "selector": " body > ul", "snapshot": { "selectorResolution": "matched", }, @@ -714,10 +784,13 @@ test('trace view artifacts', async () => { { "entries": [ { + "element": { + "locator": "getByRole('list')", + "selector": " body > ul", + }, "kind": "mark", "location": "retry.test.ts:36", "name": "renderHelper", - "selector": " body > ul", "snapshot": { "selectorResolution": "matched", }, @@ -737,10 +810,13 @@ test('trace view artifacts', async () => { { "entries": [ { + "element": { + "locator": "getByRole('list')", + "selector": " body > ul", + }, "kind": "mark", "location": "retry.test.ts:22", "name": "renderHelper", - "selector": " body > ul", "snapshot": { "selectorResolution": "matched", }, @@ -757,10 +833,13 @@ test('trace view artifacts', async () => { { "entries": [ { + "element": { + "locator": "getByRole('list')", + "selector": " body > ul", + }, "kind": "mark", "location": "retry.test.ts:22", "name": "renderHelper", - "selector": " body > ul", "snapshot": { "selectorResolution": "matched", }, @@ -778,10 +857,13 @@ test('trace view artifacts', async () => { { "entries": [ { + "element": { + "locator": "getByRole('list')", + "selector": " body > ul", + }, "kind": "mark", "location": "retry.test.ts:22", "name": "renderHelper", - "selector": " body > ul", "snapshot": { "selectorResolution": "matched", }, @@ -898,50 +980,65 @@ test('trace view artifacts', async () => { { "entries": [ { + "element": { + "locator": "getByRole('button', { name: 'First pseudo state' })", + "selector": " body > button:nth-child(1)", + }, "kind": "expect", "location": "styles.test.ts:101", - "name": "expect.element().toHaveStyle", - "selector": " body > button:nth-child(1)", + "name": "toHaveStyle", "snapshot": { "selectorResolution": "matched", }, "status": "pass", }, { + "element": { + "locator": "getByRole('button', { name: 'First pseudo state' })", + "selector": " body > button:nth-child(1)", + }, "kind": "action", "location": "styles.test.ts:104", "name": "vitest:hover", - "selector": " body > button:nth-child(1)", "snapshot": { "selectorResolution": "matched", }, "status": "pass", }, { + "element": { + "locator": "getByRole('button', { name: 'Second pseudo state' })", + "selector": " body > button:nth-child(2)", + }, "kind": "action", "location": "styles.test.ts:105", "name": "vitest:click", - "selector": " body > button:nth-child(2)", "snapshot": { "selectorResolution": "matched", }, "status": "pass", }, { + "element": { + "locator": "getByLabel('Focused pseudo state')", + "selector": " body > input", + }, "kind": "action", "location": "styles.test.ts:106", "name": "vitest:fill", - "selector": " body > input", "snapshot": { "selectorResolution": "matched", }, "status": "pass", }, { + "element": { + "locator": "getByLabel('Focus within pseudo state')", + "selector": " body > label > input", + }, "kind": "action", "location": "styles.test.ts:107", "name": "vitest:fill", - "selector": " body > label > input", "snapshot": { "selectorResolution": "matched", }, @@ -982,10 +1079,13 @@ test('trace view artifacts', async () => { { "entries": [ { + "element": { + "locator": "getByRole('button')", + "selector": " body > main > button", + }, "kind": "mark", "location": "viewport.test.ts:31", "name": "document scrolled before mark", - "selector": " body > main > button", "snapshot": { "selectorResolution": "matched", }, @@ -1004,10 +1104,13 @@ test('trace view artifacts', async () => { { "entries": [ { + "element": { + "locator": "getByTestId('scroll-box')", + "selector": " body > section", + }, "kind": "mark", "location": "viewport.test.ts:47", "name": "overflow container scrolled before mark", - "selector": " body > section", "snapshot": { "selectorResolution": "matched", }, @@ -1053,10 +1156,13 @@ test('trace view artifacts', async () => { { "entries": [ { + "element": { + "locator": "getByRole('button')", + "selector": "internal:role=button", + }, "kind": "mark", "location": "exotic.test.ts:58", "name": "button rendered with adopted stylesheet", - "selector": "internal:role=button", "snapshot": { "selectorResolution": "matched", }, @@ -1075,10 +1181,13 @@ test('trace view artifacts', async () => { { "entries": [ { + "element": { + "locator": "getByTestId('trace-canvas')", + "selector": "internal:testid=[data-testid="trace-canvas"s]", + }, "kind": "mark", "location": "exotic.test.ts:17", "name": "canvas drawn before mark", - "selector": "internal:testid=[data-testid="trace-canvas"s]", "snapshot": { "selectorResolution": "matched", }, @@ -1097,10 +1206,13 @@ test('trace view artifacts', async () => { { "entries": [ { + "element": { + "locator": "getByRole('button', { name: 'Custom element button' })", + "selector": "internal:role=button[name="Custom element button"i]", + }, "kind": "mark", "location": "exotic.test.ts:47", "name": "custom element rendered", - "selector": "internal:role=button[name="Custom element button"i]", "snapshot": { "selectorResolution": "matched", }, @@ -1119,10 +1231,13 @@ test('trace view artifacts', async () => { { "entries": [ { + "element": { + "locator": "getByRole('button', { name: 'Shadow button' })", + "selector": "internal:role=button[name="Shadow button"i]", + }, "kind": "mark", "location": "exotic.test.ts:28", "name": "shadow button rendered", - "selector": "internal:role=button[name="Shadow button"i]", "snapshot": { "selectorResolution": "matched", }, @@ -1143,10 +1258,13 @@ test('trace view artifacts', async () => { { "entries": [ { + "element": { + "locator": "getByRole('button')", + "selector": "internal:role=button", + }, "kind": "action", "location": "expect.test.ts:25", "name": "vitest:click", - "selector": "internal:role=button", "snapshot": { "selectorResolution": "matched", }, @@ -1166,10 +1284,13 @@ test('trace view artifacts', async () => { { "entries": [ { + "element": { + "locator": "getByRole('button', { name: 'Missing' })", + "selector": "internal:role=button[name="Missing"i]", + }, "kind": "action", "location": "expect.test.ts:33", "name": "vitest:click", - "selector": "internal:role=button[name="Missing"i]", "snapshot": { "selectorResolution": "missing", }, @@ -1189,10 +1310,13 @@ test('trace view artifacts', async () => { { "entries": [ { + "element": { + "locator": "getByRole('button')", + "selector": "internal:role=button", + }, "kind": "expect", "location": "expect.test.ts:15", - "name": "expect.element().toHaveTextContent [ERROR]", - "selector": "internal:role=button", + "name": "toHaveTextContent [ERROR]", "snapshot": { "selectorResolution": "matched", }, @@ -1212,10 +1336,13 @@ test('trace view artifacts', async () => { { "entries": [ { + "element": { + "locator": "getByRole('button')", + "selector": "internal:role=button", + }, "kind": "expect", "location": "expect.test.ts:10", - "name": "expect.element().toHaveTextContent", - "selector": "internal:role=button", + "name": "toHaveTextContent", "snapshot": { "selectorResolution": "matched", }, @@ -1250,10 +1377,13 @@ test('trace view artifacts', async () => { { "entries": [ { + "element": { + "locator": "getByRole('button', { name: 'Hello' })", + "selector": "internal:role=button[name="Hello"i]", + }, "kind": "mark", "location": "mark.test.ts:24", "name": "render helper", - "selector": "internal:role=button[name="Hello"i]", "snapshot": { "selectorResolution": "matched", }, @@ -1272,10 +1402,13 @@ test('trace view artifacts', async () => { { "entries": [ { + "element": { + "locator": "getByRole('button')", + "selector": "internal:role=button", + }, "kind": "mark", "location": "mark.test.ts:10", "name": "button rendered - locator", - "selector": "internal:role=button", "snapshot": { "selectorResolution": "matched", }, @@ -1333,10 +1466,13 @@ test('trace view artifacts', async () => { { "entries": [ { + "element": { + "locator": "getByRole('button')", + "selector": "internal:role=button", + }, "kind": "mark", "location": "mark.test.ts:29", "name": "button rendered - stack", - "selector": "internal:role=button", "snapshot": { "selectorResolution": "matched", }, @@ -1357,10 +1493,13 @@ test('trace view artifacts', async () => { { "entries": [ { + "element": { + "locator": "getByAltText('external trace asset')", + "selector": "internal:attr=[alt="external trace asset"i]", + }, "kind": "mark", "location": "resources.test.ts:20", "name": "image rendered from external url", - "selector": "internal:attr=[alt="external trace asset"i]", "snapshot": { "selectorResolution": "matched", }, @@ -1379,10 +1518,13 @@ test('trace view artifacts', async () => { { "entries": [ { + "element": { + "locator": "getByAltText('local trace asset')", + "selector": "internal:attr=[alt="local trace asset"i]", + }, "kind": "mark", "location": "resources.test.ts:12", "name": "image rendered from same-origin url", - "selector": "internal:attr=[alt="local trace asset"i]", "snapshot": { "selectorResolution": "matched", }, @@ -1403,10 +1545,13 @@ test('trace view artifacts', async () => { { "entries": [ { + "element": { + "locator": "getByRole('list')", + "selector": "internal:role=list", + }, "kind": "mark", "location": "retry.test.ts:29", "name": "renderHelper", - "selector": "internal:role=list", "snapshot": { "selectorResolution": "matched", }, @@ -1423,10 +1568,13 @@ test('trace view artifacts', async () => { { "entries": [ { + "element": { + "locator": "getByRole('list')", + "selector": "internal:role=list", + }, "kind": "mark", "location": "retry.test.ts:29", "name": "renderHelper", - "selector": "internal:role=list", "snapshot": { "selectorResolution": "matched", }, @@ -1444,10 +1592,13 @@ test('trace view artifacts', async () => { { "entries": [ { + "element": { + "locator": "getByRole('list')", + "selector": "internal:role=list", + }, "kind": "mark", "location": "retry.test.ts:29", "name": "renderHelper", - "selector": "internal:role=list", "snapshot": { "selectorResolution": "matched", }, @@ -1465,10 +1616,13 @@ test('trace view artifacts', async () => { { "entries": [ { + "element": { + "locator": "getByRole('list')", + "selector": "internal:role=list", + }, "kind": "mark", "location": "retry.test.ts:29", "name": "renderHelper", - "selector": "internal:role=list", "snapshot": { "selectorResolution": "matched", }, @@ -1486,10 +1640,13 @@ test('trace view artifacts', async () => { { "entries": [ { + "element": { + "locator": "getByRole('list')", + "selector": "internal:role=list", + }, "kind": "mark", "location": "retry.test.ts:29", "name": "renderHelper", - "selector": "internal:role=list", "snapshot": { "selectorResolution": "matched", }, @@ -1509,10 +1666,13 @@ test('trace view artifacts', async () => { { "entries": [ { + "element": { + "locator": "getByRole('list')", + "selector": "internal:role=list", + }, "kind": "mark", "location": "retry.test.ts:18", "name": "renderHelper", - "selector": "internal:role=list", "snapshot": { "selectorResolution": "matched", }, @@ -1529,10 +1689,13 @@ test('trace view artifacts', async () => { { "entries": [ { + "element": { + "locator": "getByRole('list')", + "selector": "internal:role=list", + }, "kind": "mark", "location": "retry.test.ts:18", "name": "renderHelper", - "selector": "internal:role=list", "snapshot": { "selectorResolution": "matched", }, @@ -1550,10 +1713,13 @@ test('trace view artifacts', async () => { { "entries": [ { + "element": { + "locator": "getByRole('list')", + "selector": "internal:role=list", + }, "kind": "mark", "location": "retry.test.ts:18", "name": "renderHelper", - "selector": "internal:role=list", "snapshot": { "selectorResolution": "matched", }, @@ -1573,10 +1739,13 @@ test('trace view artifacts', async () => { { "entries": [ { + "element": { + "locator": "getByRole('list')", + "selector": "internal:role=list", + }, "kind": "mark", "location": "retry.test.ts:36", "name": "renderHelper", - "selector": "internal:role=list", "snapshot": { "selectorResolution": "matched", }, @@ -1593,10 +1762,13 @@ test('trace view artifacts', async () => { { "entries": [ { + "element": { + "locator": "getByRole('list')", + "selector": "internal:role=list", + }, "kind": "mark", "location": "retry.test.ts:36", "name": "renderHelper", - "selector": "internal:role=list", "snapshot": { "selectorResolution": "matched", }, @@ -1614,10 +1786,13 @@ test('trace view artifacts', async () => { { "entries": [ { + "element": { + "locator": "getByRole('list')", + "selector": "internal:role=list", + }, "kind": "mark", "location": "retry.test.ts:36", "name": "renderHelper", - "selector": "internal:role=list", "snapshot": { "selectorResolution": "matched", }, @@ -1636,10 +1811,13 @@ test('trace view artifacts', async () => { { "entries": [ { + "element": { + "locator": "getByRole('list')", + "selector": "internal:role=list", + }, "kind": "mark", "location": "retry.test.ts:36", "name": "renderHelper", - "selector": "internal:role=list", "snapshot": { "selectorResolution": "matched", }, @@ -1659,10 +1837,13 @@ test('trace view artifacts', async () => { { "entries": [ { + "element": { + "locator": "getByRole('list')", + "selector": "internal:role=list", + }, "kind": "mark", "location": "retry.test.ts:22", "name": "renderHelper", - "selector": "internal:role=list", "snapshot": { "selectorResolution": "matched", }, @@ -1679,10 +1860,13 @@ test('trace view artifacts', async () => { { "entries": [ { + "element": { + "locator": "getByRole('list')", + "selector": "internal:role=list", + }, "kind": "mark", "location": "retry.test.ts:22", "name": "renderHelper", - "selector": "internal:role=list", "snapshot": { "selectorResolution": "matched", }, @@ -1700,10 +1884,13 @@ test('trace view artifacts', async () => { { "entries": [ { + "element": { + "locator": "getByRole('list')", + "selector": "internal:role=list", + }, "kind": "mark", "location": "retry.test.ts:22", "name": "renderHelper", - "selector": "internal:role=list", "snapshot": { "selectorResolution": "matched", }, @@ -1820,50 +2007,65 @@ test('trace view artifacts', async () => { { "entries": [ { + "element": { + "locator": "getByRole('button', { name: 'First pseudo state' })", + "selector": "internal:role=button[name="First pseudo state"i]", + }, "kind": "expect", "location": "styles.test.ts:101", - "name": "expect.element().toHaveStyle", - "selector": "internal:role=button[name="First pseudo state"i]", + "name": "toHaveStyle", "snapshot": { "selectorResolution": "matched", }, "status": "pass", }, { + "element": { + "locator": "getByRole('button', { name: 'First pseudo state' })", + "selector": "internal:role=button[name="First pseudo state"i]", + }, "kind": "action", "location": "styles.test.ts:104", "name": "vitest:hover", - "selector": "internal:role=button[name="First pseudo state"i]", "snapshot": { "selectorResolution": "matched", }, "status": "pass", }, { + "element": { + "locator": "getByRole('button', { name: 'Second pseudo state' })", + "selector": "internal:role=button[name="Second pseudo state"i]", + }, "kind": "action", "location": "styles.test.ts:105", "name": "vitest:click", - "selector": "internal:role=button[name="Second pseudo state"i]", "snapshot": { "selectorResolution": "matched", }, "status": "pass", }, { + "element": { + "locator": "getByRole('textbox', { name: 'Focused pseudo state' })", + "selector": "internal:role=textbox[name="Focused pseudo state"i]", + }, "kind": "action", "location": "styles.test.ts:106", "name": "vitest:fill", - "selector": "internal:role=textbox[name="Focused pseudo state"i]", "snapshot": { "selectorResolution": "matched", }, "status": "pass", }, { + "element": { + "locator": "getByRole('textbox', { name: 'Focus within pseudo state' })", + "selector": "internal:role=textbox[name="Focus within pseudo state"i]", + }, "kind": "action", "location": "styles.test.ts:107", "name": "vitest:fill", - "selector": "internal:role=textbox[name="Focus within pseudo state"i]", "snapshot": { "selectorResolution": "matched", }, @@ -1904,10 +2106,13 @@ test('trace view artifacts', async () => { { "entries": [ { + "element": { + "locator": "getByRole('button')", + "selector": "internal:role=button", + }, "kind": "mark", "location": "viewport.test.ts:31", "name": "document scrolled before mark", - "selector": "internal:role=button", "snapshot": { "selectorResolution": "matched", }, @@ -1926,10 +2131,13 @@ test('trace view artifacts', async () => { { "entries": [ { + "element": { + "locator": "getByTestId('scroll-box')", + "selector": "internal:testid=[data-testid="scroll-box"s]", + }, "kind": "mark", "location": "viewport.test.ts:47", "name": "overflow container scrolled before mark", - "selector": "internal:testid=[data-testid="scroll-box"s]", "snapshot": { "selectorResolution": "matched", }, diff --git a/test/unit/test/exports.test.ts b/test/unit/test/exports.test.ts index 99ba12e7c..51a67f555 100644 --- a/test/unit/test/exports.test.ts +++ b/test/unit/test/exports.test.ts @@ -66,7 +66,6 @@ it('exports snapshot', async ({ skip, task }) => { "./internal/browser": { "DecodedMap": "function", "SpyModule": "object", - "Traces": "function", "__INTERNAL": "object", "collectTests": "function", "format": "function", @@ -85,6 +84,9 @@ it('exports snapshot', async ({ skip, task }) => { "stringify": "function", "takeCoverageInsideWorker": "function", }, + "./internal/traces": { + "Traces": "function", + }, "./node": { "AgentReporter": "function", "BaseCoverageProvider": "function", diff --git a/tsconfig.base.json b/tsconfig.base.json index 2cdbbd41e..4e96b086e 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -19,12 +19,8 @@ "@vitest/runner": ["./packages/runner/src/index.ts"], "@vitest/runner/*": ["./packages/runner/src/*"], "@vitest/browser-playwright": ["./packages/browser-playwright/src/index.ts"], - "@vitest/browser": ["./packages/browser/src/node/index.ts"], - "@vitest/browser/client": ["./packages/browser/src/client/client.ts"], - "@vitest/browser/internal/vendor-types": ["./packages/browser/src/vendor-types.ts"], "~/*": ["./packages/ui/client/*"], "vitest": ["./packages/vitest/src/public/index.ts"], - "vitest/internal/browser": ["./packages/vitest/src/public/browser.ts"], "vitest/globals": ["./packages/vitest/globals.d.ts"], "vitest/browser": ["./packages/vitest/browser/context.d.ts"], "vitest/*": ["./packages/vitest/src/public/*"], -- 2.51.2