Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 16 additions & 7 deletions docs/src/test-assertions-js.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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);
```

Expand All @@ -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');
Expand Down Expand Up @@ -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']);
```

Expand Down
1 change: 1 addition & 0 deletions src/server/frames.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down
14 changes: 10 additions & 4 deletions src/server/injected/injectedScript.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
17 changes: 12 additions & 5 deletions src/test/matchers/matchers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
4 changes: 2 additions & 2 deletions src/test/matchers/toEqual.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,15 +33,15 @@ 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)
throw new Error(`${matcherName} must be called during the test`);
expectType(receiver, receiverType, matcherName);

const matcherOptions = {
comment: 'deep equality',
comment: options.contains ? '' : 'deep equality',
isNot: this.isNot,
promise: this.promise,
};
Expand Down
66 changes: 63 additions & 3 deletions tests/playwright-test/playwright.expect.text.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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': `
Expand All @@ -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 }) => {
Expand Down Expand Up @@ -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);
Expand Down
16 changes: 7 additions & 9 deletions tests/trace-viewer/trace-viewer.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 }) => {
Expand Down
2 changes: 1 addition & 1 deletion types/testExpect.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down