feat(expect): toContainText(array) (#9160)
This matches when each expected item from the array is matched to one of the resolved elements, in order. Note this performs both "sub-array" and "substring" matching. Drive-by: documentation fixes. Drive-by: added "selector resolved to 3 elements" log line when expecting arrays.
This commit is contained in:
parent
cd22072685
commit
8dc8777ab4
|
|
@ -118,17 +118,25 @@ const locator = page.locator('.my-element');
|
|||
await expect(locator).toBeVisible();
|
||||
```
|
||||
|
||||
## expect(locator).toContainText(text, options?)
|
||||
- `text`: <[string]> Text to look for inside the element
|
||||
## expect(locator).toContainText(expected, options?)
|
||||
- `expected`: <[string] | [RegExp] | [Array]<[string]|[RegExp]>>
|
||||
- `options`
|
||||
- `timeout`: <[number]> Time to wait for, defaults to `timeout` in [`property: TestProject.expect`].
|
||||
- `timeout`: <[number]> Time to retry assertion for, defaults to `timeout` in [`property: TestProject.expect`].
|
||||
- `useInnerText`: <[boolean]> Whether to use `element.innerText` instead of `element.textContent` when retrieving DOM node text.
|
||||
|
||||
Ensures [Locator] points to a selected option.
|
||||
Ensures [Locator] points to an element that contains the given text. You can use regular expressions for the value as well.
|
||||
|
||||
```js
|
||||
const locator = page.locator('.title');
|
||||
await expect(locator).toContainText('substring');
|
||||
await expect(locator).toContainText(/\d messages/);
|
||||
```
|
||||
|
||||
Note that if array is passed as an expected value, entire lists can be asserted:
|
||||
|
||||
```js
|
||||
const locator = page.locator('list > .list-item');
|
||||
await expect(locator).toContainText(['Text 1', 'Text 4', 'Text 5']);
|
||||
```
|
||||
|
||||
## expect(locator).toHaveAttribute(name, value)
|
||||
|
|
@ -171,7 +179,7 @@ await expect(locator).toHaveClass(['component', 'component selected', 'component
|
|||
Ensures [Locator] resolves to an exact number of DOM nodes.
|
||||
|
||||
```js
|
||||
const list = page.locator('list > #component');
|
||||
const list = page.locator('list > .component');
|
||||
await expect(list).toHaveCount(3);
|
||||
```
|
||||
|
||||
|
|
@ -181,7 +189,7 @@ await expect(list).toHaveCount(3);
|
|||
- `options`
|
||||
- `timeout`: <[number]> Time to retry assertion for, defaults to `timeout` in [`property: TestProject.expect`].
|
||||
|
||||
Ensures [Locator] resolves to an element with the given computed CSS style
|
||||
Ensures [Locator] resolves to an element with the given computed CSS style.
|
||||
|
||||
```js
|
||||
const locator = page.locator('button');
|
||||
|
|
@ -224,13 +232,14 @@ Ensures [Locator] points to an element with the given text. You can use regular
|
|||
|
||||
```js
|
||||
const locator = page.locator('.title');
|
||||
await expect(locator).toHaveText(/Welcome, Test User/);
|
||||
await expect(locator).toHaveText(/Welcome, .*/);
|
||||
```
|
||||
|
||||
Note that if array is passed as an expected value, entire lists can be asserted:
|
||||
|
||||
```js
|
||||
const locator = page.locator('list > #component');
|
||||
const locator = page.locator('list > .component');
|
||||
await expect(locator).toHaveText(['Text 1', 'Text 2', 'Text 3']);
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -1256,6 +1256,7 @@ export class Frame extends SdkObject {
|
|||
return poller((progress, continuePolling) => {
|
||||
if (querySelectorAll) {
|
||||
const elements = injected.querySelectorAll(info.parsed, document);
|
||||
progress.logRepeating(` selector resolved to ${elements.length} element${elements.length === 1 ? '' : 's'}`);
|
||||
return callback(progress, elements[0], taskData as T, elements, continuePolling);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -882,20 +882,26 @@ export class InjectedScript {
|
|||
{
|
||||
// List of values.
|
||||
let received: string[] | undefined;
|
||||
if (expression === 'to.have.text.array')
|
||||
if (expression === 'to.have.text.array' || expression === 'to.contain.text.array')
|
||||
received = elements.map(e => options.useInnerText ? (e as HTMLElement).innerText : e.textContent || '');
|
||||
else if (expression === 'to.have.class.array')
|
||||
received = elements.map(e => e.className);
|
||||
|
||||
if (received && options.expectedText) {
|
||||
if (received.length !== options.expectedText.length) {
|
||||
// "To match an array" is "to contain an array" + "equal length"
|
||||
const lengthShouldMatch = expression !== 'to.contain.text.array';
|
||||
if (received.length !== options.expectedText.length && lengthShouldMatch) {
|
||||
progress.setIntermediateResult(received);
|
||||
return continuePolling;
|
||||
}
|
||||
|
||||
// Each matcher should get a "received" that matches it, in order.
|
||||
let i = 0;
|
||||
const matchers = options.expectedText.map(e => new ExpectedTextMatcher(e));
|
||||
for (let i = 0; i < received.length; ++i) {
|
||||
if (matchers[i].matches(received[i]) === options.isNot) {
|
||||
for (const matcher of matchers) {
|
||||
while (i < received.length && matcher.matches(received[i]) === options.isNot)
|
||||
i++;
|
||||
if (i === received.length) {
|
||||
progress.setIntermediateResult(received);
|
||||
return continuePolling;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -109,13 +109,20 @@ export function toBeVisible(
|
|||
export function toContainText(
|
||||
this: ReturnType<Expect['getState']>,
|
||||
locator: LocatorEx,
|
||||
expected: string,
|
||||
expected: string | RegExp | (string | RegExp)[],
|
||||
options?: { timeout?: number, useInnerText?: boolean },
|
||||
) {
|
||||
return toMatchText.call(this, 'toContainText', locator, 'Locator', async (isNot, timeout) => {
|
||||
const expectedText = toExpectedTextValues([expected], { matchSubstring: true, normalizeWhiteSpace: true });
|
||||
return await locator._expect('to.have.text', { expectedText, isNot, useInnerText: options?.useInnerText, timeout });
|
||||
}, expected, options);
|
||||
if (Array.isArray(expected)) {
|
||||
return toEqual.call(this, 'toContainText', locator, 'Locator', async (isNot, timeout) => {
|
||||
const expectedText = toExpectedTextValues(expected, { matchSubstring: true, normalizeWhiteSpace: true });
|
||||
return await locator._expect('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) => {
|
||||
const expectedText = toExpectedTextValues([expected], { matchSubstring: true, normalizeWhiteSpace: true });
|
||||
return await locator._expect('to.have.text', { expectedText, isNot, useInnerText: options?.useInnerText, timeout });
|
||||
}, expected, options);
|
||||
}
|
||||
}
|
||||
|
||||
export function toHaveAttribute(
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ export async function toEqual<T>(
|
|||
receiverType: string,
|
||||
query: (isNot: boolean, timeout: number) => Promise<{ pass: boolean, received?: any, log?: string[] }>,
|
||||
expected: T,
|
||||
options: { timeout?: number } = {},
|
||||
options: { timeout?: number, contains?: boolean } = {},
|
||||
) {
|
||||
const testInfo = currentTestInfo();
|
||||
if (!testInfo)
|
||||
|
|
@ -41,7 +41,7 @@ export async function toEqual<T>(
|
|||
expectType(receiver, receiverType, matcherName);
|
||||
|
||||
const matcherOptions = {
|
||||
comment: 'deep equality',
|
||||
comment: options.contains ? '' : 'deep equality',
|
||||
isNot: this.isNot,
|
||||
promise: this.promise,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -47,6 +47,37 @@ test('should support toHaveText w/ regex', async ({ runInlineTest }) => {
|
|||
expect(result.exitCode).toBe(1);
|
||||
});
|
||||
|
||||
test('should support toContainText w/ regex', async ({ runInlineTest }) => {
|
||||
const result = await runInlineTest({
|
||||
'a.test.ts': `
|
||||
const { test } = pwt;
|
||||
|
||||
test('pass', async ({ page }) => {
|
||||
await page.setContent('<div id=node>Text content</div>');
|
||||
const locator = page.locator('#node');
|
||||
await expect(locator).toContainText(/ex/);
|
||||
|
||||
// Should not normalize whitespace.
|
||||
await expect(locator).toContainText(/ext cont/);
|
||||
});
|
||||
|
||||
test('fail', async ({ page }) => {
|
||||
await page.setContent('<div id=node>Text content</div>');
|
||||
const locator = page.locator('#node');
|
||||
await expect(locator).toContainText(/ex2/, { timeout: 100 });
|
||||
});
|
||||
`,
|
||||
}, { workers: 1 });
|
||||
const output = stripAscii(result.output);
|
||||
expect(output).toContain('Error: expect(received).toContainText(expected)');
|
||||
expect(output).toContain('Expected pattern: /ex2/');
|
||||
expect(output).toContain('Received string: "Text content"');
|
||||
expect(output).toContain('expect(locator).toContainText');
|
||||
expect(result.passed).toBe(1);
|
||||
expect(result.failed).toBe(1);
|
||||
expect(result.exitCode).toBe(1);
|
||||
});
|
||||
|
||||
test('should support toHaveText w/ text', async ({ runInlineTest }) => {
|
||||
const result = await runInlineTest({
|
||||
'a.test.ts': `
|
||||
|
|
@ -64,7 +95,7 @@ test('should support toHaveText w/ text', async ({ runInlineTest }) => {
|
|||
const locator = page.locator('#node');
|
||||
await expect(locator).toContainText('Text');
|
||||
// Should normalize whitespace.
|
||||
await expect(locator).toContainText(' Text content\\n ');
|
||||
await expect(locator).toContainText(' ext cont\\n ');
|
||||
});
|
||||
|
||||
test('fail', async ({ page }) => {
|
||||
|
|
@ -127,14 +158,43 @@ test('should support toHaveText w/ array', async ({ runInlineTest }) => {
|
|||
test('fail', async ({ page }) => {
|
||||
await page.setContent('<div>Text 1</div><div>Text 3</div>');
|
||||
const locator = page.locator('div');
|
||||
await expect(locator).toHaveText(['Text 1', /Text \\d+a/], { timeout: 1000 });
|
||||
await expect(locator).toHaveText(['Text 1', /Text \\d/, 'Extra'], { timeout: 1000 });
|
||||
});
|
||||
`,
|
||||
}, { workers: 1 });
|
||||
const output = stripAscii(result.output);
|
||||
expect(output).toContain('Error: expect(received).toHaveText(expected) // deep equality');
|
||||
expect(output).toContain('await expect(locator).toHaveText');
|
||||
expect(output).toContain('- /Text \\d+a/');
|
||||
expect(output).toContain('- "Extra"');
|
||||
expect(output).toContain('waiting for selector "div"');
|
||||
expect(output).toContain('selector resolved to 2 elements');
|
||||
expect(result.passed).toBe(1);
|
||||
expect(result.failed).toBe(1);
|
||||
expect(result.exitCode).toBe(1);
|
||||
});
|
||||
|
||||
test('should support toContainText w/ array', async ({ runInlineTest }) => {
|
||||
const result = await runInlineTest({
|
||||
'a.test.ts': `
|
||||
const { test } = pwt;
|
||||
|
||||
test('pass', async ({ page }) => {
|
||||
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/]);
|
||||
});
|
||||
|
||||
test('fail', async ({ page }) => {
|
||||
await page.setContent('<div>Text 1</div><div>Text 3</div>');
|
||||
const locator = page.locator('div');
|
||||
await expect(locator).toContainText(['Text 2'], { timeout: 1000 });
|
||||
});
|
||||
`,
|
||||
}, { workers: 1 });
|
||||
const output = stripAscii(result.output);
|
||||
expect(output).toContain('Error: expect(received).toContainText(expected)');
|
||||
expect(output).toContain('await expect(locator).toContainText');
|
||||
expect(output).toContain('- "Text 2"');
|
||||
expect(result.passed).toBe(1);
|
||||
expect(result.failed).toBe(1);
|
||||
expect(result.exitCode).toBe(1);
|
||||
|
|
|
|||
|
|
@ -232,18 +232,16 @@ test('should have correct stack trace', async ({ showTraceViewer }) => {
|
|||
|
||||
await traceViewer.selectAction('page.click');
|
||||
await traceViewer.showSourceTab();
|
||||
const stack1 = (await traceViewer.stackFrames.allInnerTexts()).map(s => s.replace(/\s+/g, ' ').replace(/:[0-9]+/g, ':XXX'));
|
||||
expect(stack1.slice(0, 2)).toEqual([
|
||||
'doClick trace-viewer.spec.ts :XXX',
|
||||
'recordTrace trace-viewer.spec.ts :XXX',
|
||||
]);
|
||||
await expect(traceViewer.stackFrames).toContainText([
|
||||
/doClick\s+trace-viewer.spec.ts\s+:\d+/,
|
||||
/recordTrace\s+trace-viewer.spec.ts\s+:\d+/,
|
||||
], { useInnerText: true });
|
||||
|
||||
await traceViewer.selectAction('page.hover');
|
||||
await traceViewer.showSourceTab();
|
||||
const stack2 = (await traceViewer.stackFrames.allInnerTexts()).map(s => s.replace(/\s+/g, ' ').replace(/:[0-9]+/g, ':XXX'));
|
||||
expect(stack2.slice(0, 1)).toEqual([
|
||||
'BrowserType.browserType._onWillCloseContext trace-viewer.spec.ts :XXX',
|
||||
]);
|
||||
await expect(traceViewer.stackFrames).toContainText([
|
||||
/BrowserType.browserType._onWillCloseContext\s+trace-viewer.spec.ts\s+:\d+/,
|
||||
], { useInnerText: true });
|
||||
});
|
||||
|
||||
test('should have network requests', async ({ showTraceViewer }) => {
|
||||
|
|
|
|||
2
types/testExpect.d.ts
vendored
2
types/testExpect.d.ts
vendored
|
|
@ -111,7 +111,7 @@ declare global {
|
|||
/**
|
||||
* Asserts element's text content matches given pattern or contains given substring.
|
||||
*/
|
||||
toContainText(expected: string, options?: { timeout?: number, useInnerText?: boolean }): Promise<R>;
|
||||
toContainText(expected: string | RegExp | (string|RegExp)[], options?: { timeout?: number, useInnerText?: boolean }): Promise<R>;
|
||||
|
||||
/**
|
||||
* Asserts element's attributes `name` matches expected value.
|
||||
|
|
|
|||
Loading…
Reference in a new issue