From d00efa0dfed58e4949d6f5574d8b0c75ad3afe12 Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Thu, 2 Jun 2022 05:52:53 -0700 Subject: [PATCH] feat(expect): add ignoreCase option to toHaveText and toContainText (#14534) --- docs/src/api/class-locatorassertions.md | 20 ++++++++++++++ .../playwright-core/src/protocol/channels.ts | 1 + .../playwright-core/src/protocol/protocol.yml | 1 + .../playwright-core/src/protocol/validator.ts | 1 + .../src/server/injected/injectedScript.ts | 27 ++++++++++++++----- .../playwright-test/src/matchers/matchers.ts | 16 +++++------ .../src/matchers/toMatchText.ts | 3 ++- packages/playwright-test/types/test.d.ts | 12 +++++++++ .../playwright.expect.text.spec.ts | 18 +++++++++++++ 9 files changed, 83 insertions(+), 16 deletions(-) diff --git a/docs/src/api/class-locatorassertions.md b/docs/src/api/class-locatorassertions.md index af9ab41464..7ba990214e 100644 --- a/docs/src/api/class-locatorassertions.md +++ b/docs/src/api/class-locatorassertions.md @@ -158,6 +158,11 @@ The opposite of [`method: LocatorAssertions.toContainText`]. Expected substring or RegExp or a list of those. +### option: LocatorAssertions.NotToContainText.ignoreCase +- `ignoreCase` <[boolean]> + +Whether to perform case-insensitive match. [`option: ignoreCase`] option takes precedence over the corresponding regular expression flag if specified. + ### option: LocatorAssertions.NotToContainText.useInnerText - `useInnerText` <[boolean]> @@ -269,6 +274,11 @@ The opposite of [`method: LocatorAssertions.toHaveText`]. Expected substring or RegExp or a list of those. +### option: LocatorAssertions.NotToHaveText.ignoreCase +- `ignoreCase` <[boolean]> + +Whether to perform case-insensitive match. [`option: ignoreCase`] option takes precedence over the corresponding regular expression flag if specified. + ### option: LocatorAssertions.NotToHaveText.useInnerText - `useInnerText` <[boolean]> @@ -685,6 +695,11 @@ Expected substring or RegExp or a list of those. Expected substring or RegExp or a list of those. +### option: LocatorAssertions.toContainText.ignoreCase +- `ignoreCase` <[boolean]> + +Whether to perform case-insensitive match. [`option: ignoreCase`] option takes precedence over the corresponding regular expression flag if specified. + ### option: LocatorAssertions.toContainText.useInnerText - `useInnerText` <[boolean]> @@ -1136,6 +1151,11 @@ Expected substring or RegExp or a list of those. Expected substring or RegExp or a list of those. +### option: LocatorAssertions.toHaveText.ignoreCase +- `ignoreCase` <[boolean]> + +Whether to perform case-insensitive match. [`option: ignoreCase`] option takes precedence over the corresponding regular expression flag if specified. + ### option: LocatorAssertions.toHaveText.useInnerText - `useInnerText` <[boolean]> diff --git a/packages/playwright-core/src/protocol/channels.ts b/packages/playwright-core/src/protocol/channels.ts index 3171e1e462..16e250f78e 100644 --- a/packages/playwright-core/src/protocol/channels.ts +++ b/packages/playwright-core/src/protocol/channels.ts @@ -187,6 +187,7 @@ export type ExpectedTextValue = { regexSource?: string, regexFlags?: string, matchSubstring?: boolean, + ignoreCase?: boolean, normalizeWhiteSpace?: boolean, }; diff --git a/packages/playwright-core/src/protocol/protocol.yml b/packages/playwright-core/src/protocol/protocol.yml index 18a445f0d1..1d37848e7f 100644 --- a/packages/playwright-core/src/protocol/protocol.yml +++ b/packages/playwright-core/src/protocol/protocol.yml @@ -108,6 +108,7 @@ ExpectedTextValue: regexSource: string? regexFlags: string? matchSubstring: boolean? + ignoreCase: boolean? normalizeWhiteSpace: boolean? diff --git a/packages/playwright-core/src/protocol/validator.ts b/packages/playwright-core/src/protocol/validator.ts index da82ca1815..dcffd61ee5 100644 --- a/packages/playwright-core/src/protocol/validator.ts +++ b/packages/playwright-core/src/protocol/validator.ts @@ -84,6 +84,7 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme { regexSource: tOptional(tString), regexFlags: tOptional(tString), matchSubstring: tOptional(tBoolean), + ignoreCase: tOptional(tBoolean), normalizeWhiteSpace: tOptional(tBoolean), }); scheme.AXNode = tObject({ diff --git a/packages/playwright-core/src/server/injected/injectedScript.ts b/packages/playwright-core/src/server/injected/injectedScript.ts index c9c5896b6c..913a80e824 100644 --- a/packages/playwright-core/src/server/injected/injectedScript.ts +++ b/packages/playwright-core/src/server/injected/injectedScript.ts @@ -1231,17 +1231,26 @@ class ExpectedTextMatcher { private _substring: string | undefined; private _regex: RegExp | undefined; private _normalizeWhiteSpace: boolean | undefined; + private _ignoreCase: boolean | undefined; constructor(expected: channels.ExpectedTextValue) { this._normalizeWhiteSpace = expected.normalizeWhiteSpace; - this._string = expected.matchSubstring ? undefined : this.normalizeWhiteSpace(expected.string); - this._substring = expected.matchSubstring ? this.normalizeWhiteSpace(expected.string) : undefined; - this._regex = expected.regexSource ? new RegExp(expected.regexSource, expected.regexFlags) : undefined; + this._ignoreCase = expected.ignoreCase; + this._string = expected.matchSubstring ? undefined : this.normalize(expected.string); + this._substring = expected.matchSubstring ? this.normalize(expected.string) : undefined; + if (expected.regexSource) { + const flags = new Set((expected.regexFlags || '').split('')); + if (expected.ignoreCase === false) + flags.delete('i'); + if (expected.ignoreCase === true) + flags.add('i'); + this._regex = new RegExp(expected.regexSource, [...flags].join('')); + } } matches(text: string): boolean { - if (this._normalizeWhiteSpace && !this._regex) - text = this.normalizeWhiteSpace(text)!; + if (!this._regex) + text = this.normalize(text)!; if (this._string !== undefined) return text === this._string; if (this._substring !== undefined) @@ -1251,10 +1260,14 @@ class ExpectedTextMatcher { return false; } - private normalizeWhiteSpace(s: string | undefined): string | undefined { + private normalize(s: string | undefined): string | undefined { if (!s) return s; - return this._normalizeWhiteSpace ? s.trim().replace(/\u200b/g, '').replace(/\s+/g, ' ') : s; + if (this._normalizeWhiteSpace) + s = s.trim().replace(/\u200b/g, '').replace(/\s+/g, ' '); + if (this._ignoreCase) + s = s.toLocaleLowerCase(); + return s; } } diff --git a/packages/playwright-test/src/matchers/matchers.ts b/packages/playwright-test/src/matchers/matchers.ts index 40c053f3d0..36c91a7c49 100644 --- a/packages/playwright-test/src/matchers/matchers.ts +++ b/packages/playwright-test/src/matchers/matchers.ts @@ -117,17 +117,17 @@ export function toContainText( this: ReturnType, locator: LocatorEx, expected: string | RegExp | (string | RegExp)[], - options?: { timeout?: number, useInnerText?: boolean }, + options: { timeout?: number, useInnerText?: boolean, ignoreCase?: boolean } = {}, ) { if (Array.isArray(expected)) { return toEqual.call(this, 'toContainText', locator, 'Locator', async (isNot, timeout, customStackTrace) => { - const expectedText = toExpectedTextValues(expected, { matchSubstring: true, normalizeWhiteSpace: true }); - return await locator._expect(customStackTrace, 'to.contain.text.array', { expectedText, isNot, useInnerText: options?.useInnerText, timeout }); + const expectedText = toExpectedTextValues(expected, { matchSubstring: true, normalizeWhiteSpace: true, ignoreCase: options.ignoreCase }); + return await locator._expect(customStackTrace, 'to.contain.text.array', { expectedText, isNot, useInnerText: options.useInnerText, timeout }); }, expected, { ...options, contains: true }); } else { return toMatchText.call(this, 'toContainText', locator, 'Locator', async (isNot, timeout, customStackTrace) => { - const expectedText = toExpectedTextValues([expected], { matchSubstring: true, normalizeWhiteSpace: true }); - return await locator._expect(customStackTrace, 'to.have.text', { expectedText, isNot, useInnerText: options?.useInnerText, timeout }); + const expectedText = toExpectedTextValues([expected], { matchSubstring: true, normalizeWhiteSpace: true, ignoreCase: options.ignoreCase }); + return await locator._expect(customStackTrace, 'to.have.text', { expectedText, isNot, useInnerText: options.useInnerText, timeout }); }, expected, options); } } @@ -216,16 +216,16 @@ export function toHaveText( this: ReturnType, locator: LocatorEx, expected: string | RegExp | (string | RegExp)[], - options: { timeout?: number, useInnerText?: boolean } = {}, + options: { timeout?: number, useInnerText?: boolean, ignoreCase?: boolean } = {}, ) { if (Array.isArray(expected)) { return toEqual.call(this, 'toHaveText', locator, 'Locator', async (isNot, timeout, customStackTrace) => { - const expectedText = toExpectedTextValues(expected, { normalizeWhiteSpace: true }); + const expectedText = toExpectedTextValues(expected, { normalizeWhiteSpace: true, ignoreCase: options.ignoreCase }); return await locator._expect(customStackTrace, 'to.have.text.array', { expectedText, isNot, useInnerText: options?.useInnerText, timeout }); }, expected, options); } else { return toMatchText.call(this, 'toHaveText', locator, 'Locator', async (isNot, timeout, customStackTrace) => { - const expectedText = toExpectedTextValues([expected], { normalizeWhiteSpace: true }); + const expectedText = toExpectedTextValues([expected], { normalizeWhiteSpace: true, ignoreCase: options.ignoreCase }); return await locator._expect(customStackTrace, 'to.have.text', { expectedText, isNot, useInnerText: options?.useInnerText, timeout }); }, expected, options); } diff --git a/packages/playwright-test/src/matchers/toMatchText.ts b/packages/playwright-test/src/matchers/toMatchText.ts index 00e5d9223e..d14ec2077f 100644 --- a/packages/playwright-test/src/matchers/toMatchText.ts +++ b/packages/playwright-test/src/matchers/toMatchText.ts @@ -101,12 +101,13 @@ export async function toMatchText( return { message, pass }; } -export function toExpectedTextValues(items: (string | RegExp)[], options: { matchSubstring?: boolean, normalizeWhiteSpace?: boolean } = {}): ExpectedTextValue[] { +export function toExpectedTextValues(items: (string | RegExp)[], options: { matchSubstring?: boolean, normalizeWhiteSpace?: boolean, ignoreCase?: boolean } = {}): ExpectedTextValue[] { return items.map(i => ({ string: isString(i) ? i : undefined, regexSource: isRegExp(i) ? i.source : undefined, regexFlags: isRegExp(i) ? i.flags : undefined, matchSubstring: options.matchSubstring, + ignoreCase: options.ignoreCase, normalizeWhiteSpace: options.normalizeWhiteSpace, })); } diff --git a/packages/playwright-test/types/test.d.ts b/packages/playwright-test/types/test.d.ts index 9d32ccfd43..b3918f9d74 100644 --- a/packages/playwright-test/types/test.d.ts +++ b/packages/playwright-test/types/test.d.ts @@ -3197,6 +3197,12 @@ interface LocatorAssertions { * @param options */ toContainText(expected: string|RegExp|Array, options?: { + /** + * Whether to perform case-insensitive match. `ignoreCase` option takes precedence over the corresponding regular + * expression flag if specified. + */ + ignoreCase?: boolean; + /** * Time to retry the assertion for. Defaults to `timeout` in `TestConfig.expect`. */ @@ -3496,6 +3502,12 @@ interface LocatorAssertions { * @param options */ toHaveText(expected: string|RegExp|Array, options?: { + /** + * Whether to perform case-insensitive match. `ignoreCase` option takes precedence over the corresponding regular + * expression flag if specified. + */ + ignoreCase?: boolean; + /** * Time to retry the assertion for. Defaults to `timeout` in `TestConfig.expect`. */ diff --git a/tests/playwright-test/playwright.expect.text.spec.ts b/tests/playwright-test/playwright.expect.text.spec.ts index c110becb69..07c6b91927 100644 --- a/tests/playwright-test/playwright.expect.text.spec.ts +++ b/tests/playwright-test/playwright.expect.text.spec.ts @@ -28,6 +28,10 @@ test('should support toHaveText w/ regex', async ({ runInlineTest }) => { // Should not normalize whitespace. await expect(locator).toHaveText(/Text content/); + // Should respect ignoreCase. + await expect(locator).toHaveText(/text content/, { ignoreCase: true }); + // Should override regex flag with ignoreCase. + await expect(locator).not.toHaveText(/text content/i, { ignoreCase: false }); }); test('fail', async ({ page }) => { @@ -90,6 +94,10 @@ test('should support toHaveText w/ text', async ({ runInlineTest }) => { await expect(locator).toHaveText('Text content'); // Should normalize zero width whitespace. await expect(locator).toHaveText('T\u200be\u200bx\u200bt content'); + // Should support ignoreCase. + await expect(locator).toHaveText('text CONTENT', { ignoreCase: true }); + // Should support falsy ignoreCase. + await expect(locator).not.toHaveText('TEXT', { ignoreCase: false }); }); test('pass contain', async ({ page }) => { @@ -98,6 +106,10 @@ test('should support toHaveText w/ text', async ({ runInlineTest }) => { await expect(locator).toContainText('Text'); // Should normalize whitespace. await expect(locator).toContainText(' ext cont\\n '); + // Should support ignoreCase. + await expect(locator).toContainText('EXT', { ignoreCase: true }); + // Should support falsy ignoreCase. + await expect(locator).not.toContainText('TEXT', { ignoreCase: false }); }); test('fail', async ({ page }) => { @@ -126,6 +138,8 @@ test('should support toHaveText w/ not', async ({ runInlineTest }) => { await page.setContent('
Text content
'); const locator = page.locator('#node'); await expect(locator).not.toHaveText('Text2'); + // Should be case-sensitive by default. + await expect(locator).not.toHaveText('TEXT'); }); test('fail', async ({ page }) => { @@ -155,6 +169,8 @@ test('should support toHaveText w/ array', async ({ runInlineTest }) => { const locator = page.locator('div'); // Should only normalize whitespace in the first item. await expect(locator).toHaveText(['Text 1', /Text \\d+a/]); + // Should support ignoreCase. + await expect(locator).toHaveText(['tEXT 1', 'TExt 2A'], { ignoreCase: true }); }); test('pass lazy', async ({ page }) => { @@ -228,6 +244,8 @@ test('should support toContainText w/ array', async ({ runInlineTest }) => { await page.setContent('
Text \\n1
Text2
Text3
'); const locator = page.locator('div'); await expect(locator).toContainText(['ext 1', /ext3/]); + // Should support ignoreCase. + await expect(locator).toContainText(['EXT 1', 'eXt3'], { ignoreCase: true }); }); test('fail', async ({ page }) => {