Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions docs/config/browser/locators.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,17 @@ Options for built-in [browser locators](/api/browser/locators).
- **Default:** `data-testid`

Attribute used to find elements with `getByTestId` locator.

## browser.locators.exact <Version type="experimental">4.1.3</Version> {#browser-locators-exact}

- **Type:** `boolean`
- **Default:** `false`

When set to `true`, [locators](/api/browser/locators) will match text exactly by default, requiring a full, case-sensitive match. Individual locator calls can override this default via their own `exact` option.

```ts
// With exact: false (default), this matches "Hello, World!", "Say Hello, World", etc.
// With exact: true, this only matches the string "Hello, World" exactly.
const locator = page.getByText('Hello, World', { exact: true })
await locator.click()
```
7 changes: 7 additions & 0 deletions docs/guide/cli-generated.md
Original file line number Diff line number Diff line change
Expand Up @@ -429,6 +429,13 @@ Control if Vitest catches uncaught exceptions so they can be reported (default:

Enable trace view mode. Supported: "on", "off", "on-first-retry", "on-all-retries", "retain-on-failure".

### browser.locators.exact

- **CLI:** `--browser.locators.exact`
- **Config:** [browser.locators.exact](/config/browser/locators#locators-exact)

Should locators match the text exactly by default (default: `false`)

### pool

- **CLI:** `--pool <pool>`
Expand Down
2 changes: 1 addition & 1 deletion packages/browser/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@
"@vitest/runner": "workspace:*",
"birpc": "catalog:",
"flatted": "catalog:",
"ivya": "^1.7.1",
"ivya": "^1.8.0",
"mime": "^4.1.0",
"pathe": "catalog:",
"vitest": "workspace:*"
Expand Down
2 changes: 1 addition & 1 deletion packages/browser/src/client/tester/locators/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,8 @@ function sleep(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms))
}

// we prefer using playwright locators because they are more powerful and support Shadow DOM
export const selectorEngine: Ivya = Ivya.create({
exact: server.config.browser.locators.exact,
browser: ((name: string) => {
switch (name) {
case 'edge':
Expand Down
17 changes: 16 additions & 1 deletion packages/vitest/src/node/cli/cli-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -418,7 +418,22 @@ export const cliOptionsConfig: VitestCLIOptions = {
viewport: null,
screenshotDirectory: null,
screenshotFailures: null,
locators: null,
locators: {
description: 'Options for how locators should be handled by default',
argument: '<options>',
subcommands: {
testIdAttribute: null,
exact: {
Comment thread
AriPerkkio marked this conversation as resolved.
description: 'Should locators match the text exactly by default (default: `false`)',
},
},
transform(val) {
if (typeof val !== 'object' || val == null) {
return {}
}
return val
},
},
testerHtmlPath: null,
instances: null,
expect: null,
Expand Down
1 change: 1 addition & 0 deletions packages/vitest/src/node/config/resolveConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -835,6 +835,7 @@ export function resolveConfig(

resolved.browser.locators ??= {} as any
resolved.browser.locators.testIdAttribute ??= 'data-testid'
resolved.browser.locators.exact ??= false

if (typeof resolved.browser.provider === 'string') {
const source = `@vitest/browser-${resolved.browser.provider}`
Expand Down
1 change: 1 addition & 0 deletions packages/vitest/src/node/config/serializeConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ export function serializeConfig(project: TestProject): SerializedConfig {
screenshotFailures: browser.screenshotFailures,
locators: {
testIdAttribute: browser.locators.testIdAttribute,
exact: browser.locators.exact,
},
providerOptions: provider?.name === 'playwright'
? {
Expand Down
1 change: 1 addition & 0 deletions packages/vitest/src/node/projects/resolveProjects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,7 @@ function cloneConfig(project: TestProject, { browser, ...config }: BrowserInstan
locators: locators
? {
testIdAttribute: locators.testIdAttribute ?? currentConfig.locators.testIdAttribute,
exact: locators.exact ?? currentConfig.locators.exact,
}
: project.config.browser.locators,
viewport: viewport ?? currentConfig.viewport,
Expand Down
6 changes: 6 additions & 0 deletions packages/vitest/src/node/types/browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,11 @@ export interface BrowserConfigOptions {
* @default 'data-testid'
*/
testIdAttribute?: string
/**
* Should locators match the text exactly by default
* @default false
*/
exact?: boolean
}

/**
Expand Down Expand Up @@ -394,6 +399,7 @@ export interface ResolvedBrowserOptions extends BrowserConfigOptions {
screenshotFailures: boolean
locators: {
testIdAttribute: string
exact: boolean
}
trace: {
mode: BrowserTraceViewMode
Expand Down
1 change: 1 addition & 0 deletions packages/vitest/src/runtime/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ export interface SerializedConfig {
}
locators: {
testIdAttribute: string
exact: boolean
}
screenshotFailures: boolean
providerOptions: {
Expand Down
10 changes: 5 additions & 5 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

42 changes: 42 additions & 0 deletions test/browser/fixtures/locators-exact/basic.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { page } from 'vitest/browser'
import { afterEach, describe, expect, test } from 'vitest'

afterEach(() => {
document.body.innerHTML = ''
})

describe('exact: true global default', () => {
test('getByText finds an exact text match', () => {
document.body.innerHTML = '<span>Hello</span>'
const screen = page.elementLocator(document.body)
expect(screen.getByText('Hello').element()).toBe(document.querySelector('span'))
})

test('getByText does not find a substring match', () => {
document.body.innerHTML = '<span>Hello World</span>'
const screen = page.elementLocator(document.body)
expect(() => screen.getByText('Hello').element()).toThrow(
'Cannot find element',
)
})

test('getByText is case-sensitive', () => {
document.body.innerHTML = '<span>Hello</span>'
const screen = page.elementLocator(document.body)
expect(() => screen.getByText('hello').element()).toThrow(
'Cannot find element',
)
})

test('getByText finds a full text match with multiple words', () => {
document.body.innerHTML = '<span>Hello World</span>'
const screen = page.elementLocator(document.body)
expect(screen.getByText('Hello World').element()).toBe(document.querySelector('span'))
})

test('per-call exact: false overrides the global default', () => {
document.body.innerHTML = '<span>Hello</span>'
const screen = page.elementLocator(document.body)
expect(screen.getByText('hello', { exact: false }).element()).toBe(document.querySelector('span'))
})
})
18 changes: 18 additions & 0 deletions test/browser/fixtures/locators-exact/vitest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { fileURLToPath } from 'node:url'
import { defineConfig } from 'vitest/config'
import { instances, provider } from '../../settings'

export default defineConfig({
cacheDir: fileURLToPath(new URL("./node_modules/.vite", import.meta.url)),
test: {
browser: {
enabled: true,
provider,
headless: true,
instances,
locators: {
exact: true,
},
},
},
})
19 changes: 19 additions & 0 deletions test/browser/specs/locators.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,25 @@ test('locators work correctly', async () => {
expect(stdout).toReportSummaryTests({ passed: instances.length * COUNT_TESTS_OVERALL })
})

test('locators.exact option works', async () => {
const { stderr, stdout } = await runBrowserTests({
root: './fixtures/locators-exact',
reporters: [['verbose', { isTTY: false }]],
})

expect(stderr).toReportNoErrors()

instances.forEach(({ browser }) => {
expect(stdout).toReportPassedTest('basic.test.ts', browser)
})

const COUNT_TEST_FILES = 1
const COUNT_TESTS_OVERALL = 5

expect(stdout).toReportSummaryTestFiles({ passed: instances.length * COUNT_TEST_FILES })
expect(stdout).toReportSummaryTests({ passed: instances.length * COUNT_TESTS_OVERALL })
})

test('custom locators work', async () => {
const { stderr, stdout } = await runBrowserTests({
root: './fixtures/locators-custom',
Expand Down
Loading
Loading