diff --git a/docs/config/browser/locators.md b/docs/config/browser/locators.md
index 29ecdd039..f914256d4 100644
--- a/docs/config/browser/locators.md
+++ b/docs/config/browser/locators.md
@@ -14,16 +14,16 @@ Options for built-in [browser locators](/api/browser/locators).
Attribute used to find elements with `getByTestId` locator.
-## browser.locators.exact 4.1.3 {#browser-locators-exact}
+## browser.locators.exact
- **Type:** `boolean`
-- **Default:** `false`
+- **Default:** `true`
-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.
+When set to `true`, [locators](/api/browser/locators) 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.
+// With exact: true (default), this only matches the string "Hello, World" exactly.
+// With exact: false, this matches "Hello, World!", "Say Hello, World", etc.
const locator = page.getByText('Hello, World', { exact: true })
await locator.click()
```
diff --git a/docs/guide/cli-generated.md b/docs/guide/cli-generated.md
index 93cb69c96..32484b6b3 100644
--- a/docs/guide/cli-generated.md
+++ b/docs/guide/cli-generated.md
@@ -462,7 +462,7 @@ Inline loaded image pixels in trace-view snapshots (default: `false`)
- **CLI:** `--browser.locators.exact`
- **Config:** [browser.locators.exact](/config/browser/locators#locators-exact)
-Should locators match the text exactly by default (default: `false`)
+Should locators match the text exactly by default (default: `true`)
### pool
diff --git a/docs/guide/migration.md b/docs/guide/migration.md
index abc017851..2943981a1 100644
--- a/docs/guide/migration.md
+++ b/docs/guide/migration.md
@@ -56,6 +56,17 @@ export async function customClick(
}
```
+### Locators are Strict by Default
+
+Browser locators now match the text exactly by default, requiring a full, case-sensitive match. To keep the previous behaviour, you can set [`browser.locators.exact`](/config/browser/locators#browser-locators-exact) to `false`.
+
+```ts
+// With exact: true (default), this only matches the string "Hello, World" exactly.
+// With exact: false, this matches "Hello, World!", "Say Hello, World", etc.
+const locator = page.getByText('Hello, World', { exact: true })
+await locator.click()
+```
+
### Removed Deprecated Entrypoints
Several entry points were marked as deprecated in Vitest 4.1. This release removes them entirely.
diff --git a/packages/browser-webdriverio/src/locators.ts b/packages/browser-webdriverio/src/locators.ts
index 5ce960005..534bc1e07 100644
--- a/packages/browser-webdriverio/src/locators.ts
+++ b/packages/browser-webdriverio/src/locators.ts
@@ -32,8 +32,11 @@ class WebdriverIOLocator extends Locator {
}
// This exists to avoid calling `this.elements` in `this.selector`'s getter in interactive actions
- private withElement(element: Element, error: Error | undefined) {
- const pwSelector = selectorEngine.generateSelectorSimple(element)
+ private withElement(
+ element: Element,
+ error: Error | undefined,
+ pwSelector: string,
+ ) {
const cssSelector = convertElementToCssSelector(element)
return new ElementWebdriverIOLocator(cssSelector, error, pwSelector, element)
}
@@ -57,21 +60,21 @@ class WebdriverIOLocator extends Locator {
public override click(options?: UserEventClickOptions): Promise {
return ensureAwaited(async (error) => {
const element = await this.findElement(options)
- return this.withElement(element, error).click(processClickOptions(options))
+ return this.withElement(element, error, this._pwSelector).click(processClickOptions(options))
})
}
public override dblClick(options?: UserEventClickOptions): Promise {
return ensureAwaited(async (error) => {
const element = await this.findElement(options)
- return this.withElement(element, error).dblClick(processClickOptions(options))
+ return this.withElement(element, error, this._pwSelector).dblClick(processClickOptions(options))
})
}
public override tripleClick(options?: UserEventClickOptions): Promise {
return ensureAwaited(async (error) => {
const element = await this.findElement(options)
- return this.withElement(element, error).tripleClick(processClickOptions(options))
+ return this.withElement(element, error, this._pwSelector).tripleClick(processClickOptions(options))
})
}
@@ -95,7 +98,7 @@ class WebdriverIOLocator extends Locator {
public override hover(options?: UserEventHoverOptions): Promise {
return ensureAwaited(async (error) => {
const element = await this.findElement(options)
- return this.withElement(element, error).hover(processHoverOptions(options))
+ return this.withElement(element, error, this._pwSelector).hover(processHoverOptions(options))
})
}
@@ -108,28 +111,28 @@ class WebdriverIOLocator extends Locator {
public override wheel(options: UserEventWheelOptions): Promise {
return ensureAwaited(async (error) => {
const element = await this.findElement(options)
- return this.withElement(element, error).wheel(options)
+ return this.withElement(element, error, this._pwSelector).wheel(options)
})
}
public override clear(options?: UserEventClearOptions): Promise {
return ensureAwaited(async (error) => {
const element = await this.findElement(options)
- return this.withElement(element, error).clear(options)
+ return this.withElement(element, error, this._pwSelector).clear(options)
})
}
public override fill(text: string, options?: UserEventFillOptions): Promise {
return ensureAwaited(async (error) => {
const element = await this.findElement(options)
- return this.withElement(element, error).fill(text, options)
+ return this.withElement(element, error, this._pwSelector).fill(text, options)
})
}
public override screenshot(options?: LocatorScreenshotOptions): Promise {
return ensureAwaited(async (error) => {
const element = await this.findElement(options)
- return this.withElement(element, error).screenshot(options)
+ return this.withElement(element, error, this._pwSelector).screenshot(options)
})
}
diff --git a/packages/browser/package.json b/packages/browser/package.json
index 303c13a57..aae379f4f 100644
--- a/packages/browser/package.json
+++ b/packages/browser/package.json
@@ -94,7 +94,7 @@
"@vitest/runner": "workspace:*",
"birpc": "catalog:",
"flatted": "catalog:",
- "ivya": "^1.8.1",
+ "ivya": "^1.8.2",
"mime": "^4.1.0",
"pathe": "catalog:",
"rrweb-snapshot": "2.0.0-alpha.20",
diff --git a/packages/vitest/src/node/cli/cli-config.ts b/packages/vitest/src/node/cli/cli-config.ts
index 9878b64e3..5f2becbbe 100644
--- a/packages/vitest/src/node/cli/cli-config.ts
+++ b/packages/vitest/src/node/cli/cli-config.ts
@@ -443,7 +443,7 @@ export const cliOptionsConfig: VitestCLIOptions = {
subcommands: {
testIdAttribute: null,
exact: {
- description: 'Should locators match the text exactly by default (default: `false`)',
+ description: 'Should locators match the text exactly by default (default: `true`)',
},
errorFormat: null,
},
diff --git a/packages/vitest/src/node/config/resolveConfig.ts b/packages/vitest/src/node/config/resolveConfig.ts
index f38c4e4a1..6ed5822cc 100644
--- a/packages/vitest/src/node/config/resolveConfig.ts
+++ b/packages/vitest/src/node/config/resolveConfig.ts
@@ -859,7 +859,7 @@ export function resolveConfig(
resolved.browser.locators ??= {} as any
resolved.browser.locators.testIdAttribute ??= 'data-testid'
- resolved.browser.locators.exact ??= false
+ resolved.browser.locators.exact ??= true
resolved.browser.locators.errorFormat ??= 'all'
if (typeof resolved.browser.provider === 'string') {
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index de00a7a64..27f216d7f 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -567,8 +567,8 @@ importers:
specifier: 'catalog:'
version: 3.4.2
ivya:
- specifier: ^1.8.1
- version: 1.8.1
+ specifier: ^1.8.2
+ version: 1.8.2
mime:
specifier: ^4.1.0
version: 4.1.0
@@ -7662,8 +7662,8 @@ packages:
resolution: {integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==}
engines: {node: '>=8'}
- ivya@1.8.1:
- resolution: {integrity: sha512-/0JJuIGSfzsNxIyoFQEPsZ/pGl5Chso3s0LHGdzcR2xD2CexQFjxYWaMrzs7LOYnNO2GONvDvEMadJcBxznGSg==}
+ ivya@1.8.2:
+ resolution: {integrity: sha512-9pGI1dtJbK2U+fpUoV95SL9slfrF2EM4p0Sf6EddYJwmMttBKijGOxCXibSCg8UKhmVwMx5CaDZ2PXinrcGfbA==}
jackspeak@3.4.0:
resolution: {integrity: sha512-JVYhQnN59LVPFCEcVa2C3CrEKYacvjRfqIQl+h8oi91aLYQVWRYbxjPcv1bUiUy/kLmQaANrYfNMCO3kuEDHfw==}
@@ -16530,7 +16530,7 @@ snapshots:
html-escaper: 2.0.2
istanbul-lib-report: 3.0.1
- ivya@1.8.1: {}
+ ivya@1.8.2: {}
jackspeak@3.4.0:
dependencies:
diff --git a/test/browser/specs/trace.test.ts b/test/browser/specs/trace.test.ts
index af6323848..d0fdee2f3 100644
--- a/test/browser/specs/trace.test.ts
+++ b/test/browser/specs/trace.test.ts
@@ -213,7 +213,7 @@ test('trace view artifacts', async () => {
"entries": [
{
"element": {
- "_pwSelector": "internal:role=button[name="Custom element button"i]",
+ "_pwSelector": "internal:role=button[name="Custom element button"s]",
"locator": "getByRole('button', { name: 'Custom element button' })",
"selector": ">>>html > body > trace-widget > button",
},
@@ -243,7 +243,7 @@ test('trace view artifacts', async () => {
"entries": [
{
"element": {
- "_pwSelector": "internal:role=button[name="Shadow button"i]",
+ "_pwSelector": "internal:role=button[name="Shadow button"s]",
"locator": "getByRole('button', { name: 'Shadow button' })",
"selector": ">>>html > body > section > button",
},
@@ -275,8 +275,8 @@ test('trace view artifacts', async () => {
"entries": [
{
"element": {
- "_pwSelector": "internal:role=button[name="Hello"i]",
- "locator": "getByRole('button', { name: 'Hello' })",
+ "_pwSelector": "internal:role=button",
+ "locator": "getByRole('button')",
"selector": "html > body > button",
},
"kind": "action",
@@ -295,8 +295,8 @@ test('trace view artifacts', async () => {
"entries": [
{
"element": {
- "_pwSelector": "internal:role=button[name="Hello"i]",
- "locator": "getByRole('button', { name: 'Hello' })",
+ "_pwSelector": "internal:role=button",
+ "locator": "getByRole('button')",
"selector": "html > body > button",
},
"kind": "action",
@@ -488,7 +488,7 @@ test('trace view artifacts', async () => {
{
"element": {
"_pwSelector": "internal:role=button[name="Hello"i]",
- "locator": "getByRole('button', { name: 'Hello' })",
+ "locator": "getByRole('button', { name: 'Hello', exact: false })",
"selector": "html > body > button",
},
"kind": "mark",
@@ -735,7 +735,7 @@ test('trace view artifacts', async () => {
"entries": [
{
"element": {
- "_pwSelector": "internal:attr=[alt="external trace asset"i]",
+ "_pwSelector": "internal:attr=[alt="external trace asset"s]",
"locator": "getByAltText('external trace asset')",
"selector": "html > body > img",
},
@@ -765,7 +765,7 @@ test('trace view artifacts', async () => {
"entries": [
{
"element": {
- "_pwSelector": "internal:attr=[alt="local trace asset"i]",
+ "_pwSelector": "internal:attr=[alt="local trace asset"s]",
"locator": "getByAltText('local trace asset')",
"selector": "html > body > img",
},
@@ -1366,7 +1366,7 @@ test('trace view artifacts', async () => {
"entries": [
{
"element": {
- "_pwSelector": "internal:role=button[name="First pseudo state"i]",
+ "_pwSelector": "internal:role=button[name="First pseudo state"s]",
"locator": "getByRole('button', { name: 'First pseudo state' })",
"selector": "html > body > button:nth-child(1)",
},
@@ -1386,7 +1386,7 @@ test('trace view artifacts', async () => {
"entries": [
{
"element": {
- "_pwSelector": "internal:role=button[name="First pseudo state"i]",
+ "_pwSelector": "internal:role=button[name="First pseudo state"s]",
"locator": "getByRole('button', { name: 'First pseudo state' })",
"selector": "html > body > button:nth-child(1)",
},
@@ -1407,7 +1407,7 @@ test('trace view artifacts', async () => {
"entries": [
{
"element": {
- "_pwSelector": "internal:role=button[name="First pseudo state"i]",
+ "_pwSelector": "internal:role=button[name="First pseudo state"s]",
"locator": "getByRole('button', { name: 'First pseudo state' })",
"selector": "html > body > button:nth-child(1)",
},
@@ -1427,7 +1427,7 @@ test('trace view artifacts', async () => {
"entries": [
{
"element": {
- "_pwSelector": "internal:role=button[name="First pseudo state"i]",
+ "_pwSelector": "internal:role=button[name="First pseudo state"s]",
"locator": "getByRole('button', { name: 'First pseudo state' })",
"selector": "html > body > button:nth-child(1)",
},
@@ -1448,7 +1448,7 @@ test('trace view artifacts', async () => {
"entries": [
{
"element": {
- "_pwSelector": "internal:role=button[name="Second pseudo state"i]",
+ "_pwSelector": "internal:role=button[name="Second pseudo state"s]",
"locator": "getByRole('button', { name: 'Second pseudo state' })",
"selector": "html > body > button:nth-child(2)",
},
@@ -1468,7 +1468,7 @@ test('trace view artifacts', async () => {
"entries": [
{
"element": {
- "_pwSelector": "internal:role=button[name="Second pseudo state"i]",
+ "_pwSelector": "internal:role=button[name="Second pseudo state"s]",
"locator": "getByRole('button', { name: 'Second pseudo state' })",
"selector": "html > body > button:nth-child(2)",
},
@@ -1489,8 +1489,8 @@ test('trace view artifacts', async () => {
"entries": [
{
"element": {
- "_pwSelector": "internal:label="Focused pseudo state"i",
- "locator": "getByLabel('Focused pseudo state')",
+ "_pwSelector": "internal:role=textbox[name="Focused pseudo state"s]",
+ "locator": "getByRole('textbox', { name: 'Focused pseudo state' })",
"selector": "html > body > input",
},
"kind": "action",
@@ -1509,8 +1509,8 @@ test('trace view artifacts', async () => {
"entries": [
{
"element": {
- "_pwSelector": "internal:label="Focused pseudo state"i",
- "locator": "getByLabel('Focused pseudo state')",
+ "_pwSelector": "internal:role=textbox[name="Focused pseudo state"s]",
+ "locator": "getByRole('textbox', { name: 'Focused pseudo state' })",
"selector": "html > body > input",
},
"kind": "action",
@@ -1530,8 +1530,8 @@ test('trace view artifacts', async () => {
"entries": [
{
"element": {
- "_pwSelector": "internal:label="Focus within pseudo state"i",
- "locator": "getByLabel('Focus within pseudo state')",
+ "_pwSelector": "internal:role=textbox[name="Focus within pseudo state"s]",
+ "locator": "getByRole('textbox', { name: 'Focus within pseudo state' })",
"selector": "html > body > label > input",
},
"kind": "action",
@@ -1550,8 +1550,8 @@ test('trace view artifacts', async () => {
"entries": [
{
"element": {
- "_pwSelector": "internal:label="Focus within pseudo state"i",
- "locator": "getByLabel('Focus within pseudo state')",
+ "_pwSelector": "internal:role=textbox[name="Focus within pseudo state"s]",
+ "locator": "getByRole('textbox', { name: 'Focus within pseudo state' })",
"selector": "html > body > label > input",
},
"kind": "action",
@@ -1759,7 +1759,7 @@ test('trace view artifacts', async () => {
{
"element": {
"locator": "getByRole('button', { name: 'Custom element button' })",
- "selector": "internal:role=button[name="Custom element button"i]",
+ "selector": "internal:role=button[name="Custom element button"s]",
},
"kind": "mark",
"location": "exotic.test.ts:47",
@@ -1788,7 +1788,7 @@ test('trace view artifacts', async () => {
{
"element": {
"locator": "getByRole('button', { name: 'Shadow button' })",
- "selector": "internal:role=button[name="Shadow button"i]",
+ "selector": "internal:role=button[name="Shadow button"s]",
},
"kind": "mark",
"location": "exotic.test.ts:28",
@@ -1871,7 +1871,7 @@ test('trace view artifacts', async () => {
{
"element": {
"locator": "getByRole('button', { name: 'Missing' })",
- "selector": "internal:role=button[name="Missing"i]",
+ "selector": "internal:role=button[name="Missing"s]",
},
"kind": "action",
"location": "expect.test.ts:33",
@@ -1890,7 +1890,7 @@ test('trace view artifacts', async () => {
{
"element": {
"locator": "getByRole('button', { name: 'Missing' })",
- "selector": "internal:role=button[name="Missing"i]",
+ "selector": "internal:role=button[name="Missing"s]",
},
"kind": "action",
"location": "expect.test.ts:33",
@@ -2063,7 +2063,7 @@ test('trace view artifacts', async () => {
"entries": [
{
"element": {
- "locator": "getByRole('button', { name: 'Hello' })",
+ "locator": "getByRole('button', { name: 'Hello', exact: false })",
"selector": "internal:role=button[name="Hello"i]",
},
"kind": "mark",
@@ -2309,7 +2309,7 @@ test('trace view artifacts', async () => {
{
"element": {
"locator": "getByAltText('external trace asset')",
- "selector": "internal:attr=[alt="external trace asset"i]",
+ "selector": "internal:attr=[alt="external trace asset"s]",
},
"kind": "mark",
"location": "resources.test.ts:20",
@@ -2338,7 +2338,7 @@ test('trace view artifacts', async () => {
{
"element": {
"locator": "getByAltText('local trace asset')",
- "selector": "internal:attr=[alt="local trace asset"i]",
+ "selector": "internal:attr=[alt="local trace asset"s]",
},
"kind": "mark",
"location": "resources.test.ts:12",
@@ -2923,7 +2923,7 @@ test('trace view artifacts', async () => {
{
"element": {
"locator": "getByRole('button', { name: 'First pseudo state' })",
- "selector": "internal:role=button[name="First pseudo state"i]",
+ "selector": "internal:role=button[name="First pseudo state"s]",
},
"kind": "expect",
"location": "styles.test.ts:101",
@@ -2942,7 +2942,7 @@ test('trace view artifacts', async () => {
{
"element": {
"locator": "getByRole('button', { name: 'First pseudo state' })",
- "selector": "internal:role=button[name="First pseudo state"i]",
+ "selector": "internal:role=button[name="First pseudo state"s]",
},
"kind": "expect",
"location": "styles.test.ts:101",
@@ -2962,7 +2962,7 @@ test('trace view artifacts', async () => {
{
"element": {
"locator": "getByRole('button', { name: 'First pseudo state' })",
- "selector": "internal:role=button[name="First pseudo state"i]",
+ "selector": "internal:role=button[name="First pseudo state"s]",
},
"kind": "action",
"location": "styles.test.ts:104",
@@ -2981,7 +2981,7 @@ test('trace view artifacts', async () => {
{
"element": {
"locator": "getByRole('button', { name: 'First pseudo state' })",
- "selector": "internal:role=button[name="First pseudo state"i]",
+ "selector": "internal:role=button[name="First pseudo state"s]",
},
"kind": "action",
"location": "styles.test.ts:104",
@@ -3001,7 +3001,7 @@ test('trace view artifacts', async () => {
{
"element": {
"locator": "getByRole('button', { name: 'Second pseudo state' })",
- "selector": "internal:role=button[name="Second pseudo state"i]",
+ "selector": "internal:role=button[name="Second pseudo state"s]",
},
"kind": "action",
"location": "styles.test.ts:105",
@@ -3020,7 +3020,7 @@ test('trace view artifacts', async () => {
{
"element": {
"locator": "getByRole('button', { name: 'Second pseudo state' })",
- "selector": "internal:role=button[name="Second pseudo state"i]",
+ "selector": "internal:role=button[name="Second pseudo state"s]",
},
"kind": "action",
"location": "styles.test.ts:105",
@@ -3040,7 +3040,7 @@ test('trace view artifacts', async () => {
{
"element": {
"locator": "getByRole('textbox', { name: 'Focused pseudo state' })",
- "selector": "internal:role=textbox[name="Focused pseudo state"i]",
+ "selector": "internal:role=textbox[name="Focused pseudo state"s]",
},
"kind": "action",
"location": "styles.test.ts:106",
@@ -3059,7 +3059,7 @@ test('trace view artifacts', async () => {
{
"element": {
"locator": "getByRole('textbox', { name: 'Focused pseudo state' })",
- "selector": "internal:role=textbox[name="Focused pseudo state"i]",
+ "selector": "internal:role=textbox[name="Focused pseudo state"s]",
},
"kind": "action",
"location": "styles.test.ts:106",
@@ -3079,7 +3079,7 @@ test('trace view artifacts', async () => {
{
"element": {
"locator": "getByRole('textbox', { name: 'Focus within pseudo state' })",
- "selector": "internal:role=textbox[name="Focus within pseudo state"i]",
+ "selector": "internal:role=textbox[name="Focus within pseudo state"s]",
},
"kind": "action",
"location": "styles.test.ts:107",
@@ -3098,7 +3098,7 @@ test('trace view artifacts', async () => {
{
"element": {
"locator": "getByRole('textbox', { name: 'Focus within pseudo state' })",
- "selector": "internal:role=textbox[name="Focus within pseudo state"i]",
+ "selector": "internal:role=textbox[name="Focus within pseudo state"s]",
},
"kind": "action",
"location": "styles.test.ts:107",
diff --git a/test/browser/test/iframe.test.ts b/test/browser/test/iframe.test.ts
index cea9145f6..bc28e65ff 100644
--- a/test/browser/test/iframe.test.ts
+++ b/test/browser/test/iframe.test.ts
@@ -10,6 +10,6 @@ test.runIf(server.provider === 'playwright')('locates an iframe', async () => {
page.getByTestId('iframe'),
)
- await frame.getByText('Hello World').click()
- await expect.element(frame.getByText('Hello World')).toHaveTextContent('Hello World')
+ await frame.getByText('Hello World!').click()
+ await expect.element(frame.getByText('Hello World!')).toHaveTextContent('Hello World!')
})