feat(expect): add ignoreCase option to toHaveText and toContainText (#14534)

This commit is contained in:
Dmitry Gozman 2022-06-02 05:52:53 -07:00 committed by GitHub
parent 66fc04cdb3
commit d00efa0dfe
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 83 additions and 16 deletions

View File

@ -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]>

View File

@ -187,6 +187,7 @@ export type ExpectedTextValue = {
regexSource?: string,
regexFlags?: string,
matchSubstring?: boolean,
ignoreCase?: boolean,
normalizeWhiteSpace?: boolean,
};

View File

@ -108,6 +108,7 @@ ExpectedTextValue:
regexSource: string?
regexFlags: string?
matchSubstring: boolean?
ignoreCase: boolean?
normalizeWhiteSpace: boolean?

View File

@ -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({

View File

@ -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;
}
}

View File

@ -117,17 +117,17 @@ export function toContainText(
this: ReturnType<Expect['getState']>,
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<Expect['getState']>,
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);
}

View File

@ -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,
}));
}

View File

@ -3197,6 +3197,12 @@ interface LocatorAssertions {
* @param options
*/
toContainText(expected: string|RegExp|Array<string|RegExp>, 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<string|RegExp>, 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`.
*/

View File

@ -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('<div id=node>Text content</div>');
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('<div>Text \\n1</div><div>Text2</div><div>Text3</div>');
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 }) => {