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!') })