Skip to content

Commit

Permalink
feat(expect): add ignoreCase option to toHaveText and toContainText (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
dgozman authored Jun 2, 2022
1 parent 66fc04c commit d00efa0
Show file tree
Hide file tree
Showing 9 changed files with 83 additions and 16 deletions.
20 changes: 20 additions & 0 deletions docs/src/api/class-locatorassertions.md
Original file line number Diff line number Diff line change
Expand Up @@ -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]>

Expand Down Expand Up @@ -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]>

Expand Down Expand Up @@ -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]>

Expand Down Expand Up @@ -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]>

Expand Down
1 change: 1 addition & 0 deletions packages/playwright-core/src/protocol/channels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@ export type ExpectedTextValue = {
regexSource?: string,
regexFlags?: string,
matchSubstring?: boolean,
ignoreCase?: boolean,
normalizeWhiteSpace?: boolean,
};

Expand Down
1 change: 1 addition & 0 deletions packages/playwright-core/src/protocol/protocol.yml
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ ExpectedTextValue:
regexSource: string?
regexFlags: string?
matchSubstring: boolean?
ignoreCase: boolean?
normalizeWhiteSpace: boolean?


Expand Down
1 change: 1 addition & 0 deletions packages/playwright-core/src/protocol/validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
27 changes: 20 additions & 7 deletions packages/playwright-core/src/server/injected/injectedScript.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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;
}
}

Expand Down
16 changes: 8 additions & 8 deletions packages/playwright-test/src/matchers/matchers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
Expand Down Expand Up @@ -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);
}
Expand Down
3 changes: 2 additions & 1 deletion packages/playwright-test/src/matchers/toMatchText.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}));
}
12 changes: 12 additions & 0 deletions packages/playwright-test/types/test.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
*/
Expand Down Expand Up @@ -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`.
*/
Expand Down
18 changes: 18 additions & 0 deletions tests/playwright-test/playwright.expect.text.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 }) => {
Expand Down Expand Up @@ -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 }) => {
Expand All @@ -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 }) => {
Expand Down Expand Up @@ -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 }) => {
Expand Down Expand Up @@ -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 }) => {
Expand Down Expand Up @@ -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 }) => {
Expand Down

0 comments on commit d00efa0

Please sign in to comment.