feat(text matchers): add ignoreOrder option

This commit is contained in:
Max Schmitt 2024-04-07 13:21:32 +02:00
parent a6827772a5
commit d6dffeb759
8 changed files with 88 additions and 10 deletions

View file

@ -217,6 +217,12 @@ Expected substring or RegExp or a list of those.
Whether to perform case-insensitive match. [`option: ignoreCase`] option takes precedence over the corresponding regular expression flag if specified.
### option: LocatorAssertions.NotToContainText.ignoreOrder
* since: v1.44
- `ignoreOrder` <[boolean]>
Weather to ignore the order of the elements when comparing the text. This option is only available when the expected value is an array.
### option: LocatorAssertions.NotToContainText.useInnerText
* since: v1.18
- `useInnerText` <[boolean]>
@ -358,6 +364,12 @@ Expected string or RegExp or a list of those.
Whether to perform case-insensitive match. [`option: ignoreCase`] option takes precedence over the corresponding regular expression flag if specified.
### option: LocatorAssertions.NotToHaveText.ignoreOrder
* since: v1.44
- `ignoreOrder` <[boolean]>
Weather to ignore the order of the elements when comparing the text. This option is only available when the expected value is an array.
### option: LocatorAssertions.NotToHaveText.useInnerText
* since: v1.18
- `useInnerText` <[boolean]>
@ -1095,6 +1107,12 @@ Expected substring or RegExp or a list of those.
Whether to perform case-insensitive match. [`option: ignoreCase`] option takes precedence over the corresponding regular expression flag if specified.
### option: LocatorAssertions.toContainText.ignoreOrder
* since: v1.44
- `ignoreOrder` <[boolean]>
Weather to ignore the order of the elements when comparing the text. This option is only available when the expected value is an array.
### option: LocatorAssertions.toContainText.useInnerText
* since: v1.18
- `useInnerText` <[boolean]>
@ -1775,6 +1793,12 @@ Expected string or RegExp or a list of those.
Whether to perform case-insensitive match. [`option: ignoreCase`] option takes precedence over the corresponding regular expression flag if specified.
### option: LocatorAssertions.toHaveText.ignoreOrder
* since: v1.44
- `ignoreOrder` <[boolean]>
Weather to ignore the order of the elements when comparing the text. This option is only available when the expected value is an array.
### option: LocatorAssertions.toHaveText.useInnerText
* since: v1.18
- `useInnerText` <[boolean]>

View file

@ -1660,6 +1660,7 @@ scheme.FrameExpectParams = tObject({
expectedNumber: tOptional(tNumber),
expectedValue: tOptional(tType('SerializedArgument')),
useInnerText: tOptional(tBoolean),
ignoreOrder: tOptional(tBoolean),
isNot: tBoolean,
timeout: tOptional(tNumber),
});

View file

@ -1268,13 +1268,26 @@ export class InjectedScript {
// Each matcher should get a "received" that matches it, in order.
const matchers = options.expectedText.map(e => new ExpectedTextMatcher(e));
let mIndex = 0, rIndex = 0;
while (mIndex < matchers.length && rIndex < received.length) {
if (matchers[mIndex].matches(received[rIndex]))
++mIndex;
++rIndex;
if (options.ignoreOrder) {
const mReceivedIndexes = new Array<boolean>(received.length).fill(false);
let mIndex = 0;
while (mIndex < matchers.length) {
for (let rIndex = 0; rIndex < received.length && mReceivedIndexes[mIndex]; rIndex++) {
if (matchers[mIndex].matches(received[rIndex]))
mReceivedIndexes[rIndex] = true;
}
mIndex++;
}
return { received, matches: mIndex === matchers.length };
} else {
let mIndex = 0, rIndex = 0;
while (mIndex < matchers.length && rIndex < received.length) {
if (matchers[mIndex].matches(received[rIndex]))
++mIndex;
++rIndex;
}
return { received, matches: mIndex === matchers.length };
}
return { received, matches: mIndex === matchers.length };
}
throw this.createStacklessError('Unknown expect matcher: ' + expression);
}

View file

@ -159,12 +159,12 @@ export function toContainText(
this: ExpectMatcherContext,
locator: LocatorEx,
expected: string | RegExp | (string | RegExp)[],
options: { timeout?: number, useInnerText?: boolean, ignoreCase?: boolean } = {},
options: { timeout?: number, useInnerText?: boolean, ignoreCase?: boolean, ignoreOrder?: boolean } = {},
) {
if (Array.isArray(expected)) {
return toEqual.call(this, 'toContainText', locator, 'Locator', async (isNot, timeout) => {
const expectedText = toExpectedTextValues(expected, { matchSubstring: true, normalizeWhiteSpace: true, ignoreCase: options.ignoreCase });
return await locator._expect('to.contain.text.array', { expectedText, isNot, useInnerText: options.useInnerText, timeout });
return await locator._expect('to.contain.text.array', { expectedText, isNot, useInnerText: options.useInnerText, ignoreOrder: options.ignoreOrder, timeout });
}, expected, { ...options, contains: true });
} else {
return toMatchText.call(this, 'toContainText', locator, 'Locator', async (isNot, timeout) => {
@ -270,12 +270,12 @@ export function toHaveText(
this: ExpectMatcherContext,
locator: LocatorEx,
expected: string | RegExp | (string | RegExp)[],
options: { timeout?: number, useInnerText?: boolean, ignoreCase?: boolean } = {},
options: { timeout?: number, useInnerText?: boolean, ignoreCase?: boolean, ignoreOrder?: boolean } = {},
) {
if (Array.isArray(expected)) {
return toEqual.call(this, 'toHaveText', locator, 'Locator', async (isNot, timeout) => {
const expectedText = toExpectedTextValues(expected, { normalizeWhiteSpace: true, ignoreCase: options.ignoreCase });
return await locator._expect('to.have.text.array', { expectedText, isNot, useInnerText: options?.useInnerText, timeout });
return await locator._expect('to.have.text.array', { expectedText, isNot, useInnerText: options?.useInnerText, ignoreOrder: options.ignoreOrder, timeout });
}, expected, options);
} else {
return toMatchText.call(this, 'toHaveText', locator, 'Locator', async (isNot, timeout) => {

View file

@ -6869,6 +6869,12 @@ interface LocatorAssertions {
*/
ignoreCase?: boolean;
/**
* Weather to ignore the order of the elements when comparing the text. This option is only available when the
* expected value is an array.
*/
ignoreOrder?: boolean;
/**
* Time to retry the assertion for in milliseconds. Defaults to `timeout` in `TestConfig.expect`.
*/
@ -7282,6 +7288,12 @@ interface LocatorAssertions {
*/
ignoreCase?: boolean;
/**
* Weather to ignore the order of the elements when comparing the text. This option is only available when the
* expected value is an array.
*/
ignoreOrder?: boolean;
/**
* Time to retry the assertion for in milliseconds. Defaults to `timeout` in `TestConfig.expect`.
*/

View file

@ -2968,6 +2968,7 @@ export type FrameExpectParams = {
expectedNumber?: number,
expectedValue?: SerializedArgument,
useInnerText?: boolean,
ignoreOrder?: boolean,
isNot: boolean,
timeout?: number,
};
@ -2977,6 +2978,7 @@ export type FrameExpectOptions = {
expectedNumber?: number,
expectedValue?: SerializedArgument,
useInnerText?: boolean,
ignoreOrder?: boolean,
timeout?: number,
};
export type FrameExpectResult = {

View file

@ -2257,6 +2257,7 @@ Frame:
expectedNumber: number?
expectedValue: SerializedArgument?
useInnerText: boolean?
ignoreOrder: boolean?
isNot: boolean
timeout: number?
returns:

View file

@ -234,6 +234,19 @@ test.describe('toHaveText with array', () => {
const error = await expect(locator).toContainText(['KekFoo', 'KekFoo', 'KekFoo'], { timeout: 1000 }).catch(e => e);
expect(error.message).toContain('locator resolved to 1 element');
});
test('ignoreOrder should work with strings', async ({ page }) => {
await page.setContent('<div>Text 1</div><div>Text 2</div>');
const locator = page.locator('div');
await expect(locator).toHaveText(['Text 2', 'Text 1'], { ignoreOrder: true });
});
test('ignoreOrder should work with regex', async ({ page }) => {
await page.setContent('<div>Text 1</div><div>Text 2</div>');
const locator = page.locator('div');
await expect(locator).toHaveText([/Text 2/, /Text 1/], { ignoreOrder: true });
await expect(expect(locator).toHaveText([/Text 2/, /Text 1/, /Text 43/], { ignoreOrder: true, timeout: 10 })).rejects.toThrow('Timed out 10ms waiting for');
});
});
test.describe('toContainText with array', () => {
@ -251,4 +264,16 @@ test.describe('toContainText with array', () => {
const error = await expect(locator).toContainText(['Text 2'], { timeout: 1000 }).catch(e => e);
expect(stripAnsi(error.message)).toContain('- "Text 2"');
});
test('ignoreOrder should work with strings', async ({ page }) => {
await page.setContent('<div>Text 1</div><div>Text 2</div><div>Text 3</div>');
const locator = page.locator('div');
await expect(locator).toContainText(['ext 2', 'ext 1'], { ignoreOrder: true });
});
test('ignoreOrder should work with regex', async ({ page }) => {
await page.setContent('<div>Text 1</div><div>Text 2</div><div>Text 3</div>');
const locator = page.locator('div');
await expect(locator).toContainText([/ext 2/, /ext 1/], { ignoreOrder: true });
});
});