feat(expect): even more matchers (#7902)

This commit is contained in:
Pavel Feldman 2021-07-29 07:33:19 -07:00 committed by GitHub
parent 600d82b17c
commit 1807142eb7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 244 additions and 92 deletions

View file

@ -28,12 +28,14 @@ import {
toContainText, toContainText,
toHaveAttr, toHaveAttr,
toHaveClass, toHaveClass,
toHaveCount,
toHaveCSS, toHaveCSS,
toHaveData, toHaveData,
toHaveId, toHaveId,
toHaveLength,
toHaveProp, toHaveProp,
toHaveText, toHaveText,
toHaveTitle,
toHaveURL,
toHaveValue toHaveValue
} from './matchers/matchers'; } from './matchers/matchers';
import { toMatchSnapshot } from './matchers/toMatchSnapshot'; import { toMatchSnapshot } from './matchers/toMatchSnapshot';
@ -54,12 +56,14 @@ expectLibrary.extend({
toContainText, toContainText,
toHaveAttr, toHaveAttr,
toHaveClass, toHaveClass,
toHaveCount,
toHaveCSS, toHaveCSS,
toHaveData, toHaveData,
toHaveId, toHaveId,
toHaveLength,
toHaveProp, toHaveProp,
toHaveText, toHaveText,
toHaveTitle,
toHaveURL,
toHaveValue, toHaveValue,
toMatchSnapshot, toMatchSnapshot,
}); });

View file

@ -14,49 +14,48 @@
* limitations under the License. * limitations under the License.
*/ */
import matchers from 'expect/build/matchers'; import { Locator, Page } from '../../..';
import { Locator } from '../../..';
import type { Expect } from '../types'; import type { Expect } from '../types';
import { toBeTruthy } from './toBeTruthy'; import { toBeTruthy } from './toBeTruthy';
import { toEqual } from './toEqual'; import { toEqual } from './toEqual';
import { toMatchText } from './toMatchText'; import { toMatchText } from './toMatchText';
export async function toBeChecked( export function toBeChecked(
this: ReturnType<Expect['getState']>, this: ReturnType<Expect['getState']>,
locator: Locator, locator: Locator,
options?: { timeout?: number }, options?: { timeout?: number },
) { ) {
return toBeTruthy.call(this, 'toBeChecked', locator, async timeout => { return toBeTruthy.call(this, 'toBeChecked', locator, 'Locator', async timeout => {
return await locator.isChecked({ timeout }); return await locator.isChecked({ timeout });
}, options); }, options);
} }
export async function toBeDisabled( export function toBeDisabled(
this: ReturnType<Expect['getState']>, this: ReturnType<Expect['getState']>,
locator: Locator, locator: Locator,
options?: { timeout?: number }, options?: { timeout?: number },
) { ) {
return toBeTruthy.call(this, 'toBeDisabled', locator, async timeout => { return toBeTruthy.call(this, 'toBeDisabled', locator, 'Locator', async timeout => {
return await locator.isDisabled({ timeout }); return await locator.isDisabled({ timeout });
}, options); }, options);
} }
export async function toBeEditable( export function toBeEditable(
this: ReturnType<Expect['getState']>, this: ReturnType<Expect['getState']>,
locator: Locator, locator: Locator,
options?: { timeout?: number }, options?: { timeout?: number },
) { ) {
return toBeTruthy.call(this, 'toBeEditable', locator, async timeout => { return toBeTruthy.call(this, 'toBeEditable', locator, 'Locator', async timeout => {
return await locator.isEditable({ timeout }); return await locator.isEditable({ timeout });
}, options); }, options);
} }
export async function toBeEmpty( export function toBeEmpty(
this: ReturnType<Expect['getState']>, this: ReturnType<Expect['getState']>,
locator: Locator, locator: Locator,
options?: { timeout?: number }, options?: { timeout?: number },
) { ) {
return toBeTruthy.call(this, 'toBeEmpty', locator, async timeout => { return toBeTruthy.call(this, 'toBeEmpty', locator, 'Locator', async timeout => {
return await locator.evaluate(element => { return await locator.evaluate(element => {
if (element.nodeName === 'INPUT' || element.nodeName === 'TEXTAREA') if (element.nodeName === 'INPUT' || element.nodeName === 'TEXTAREA')
return !(element as HTMLInputElement).value; return !(element as HTMLInputElement).value;
@ -65,178 +64,212 @@ export async function toBeEmpty(
}, options); }, options);
} }
export async function toBeEnabled( export function toBeEnabled(
this: ReturnType<Expect['getState']>, this: ReturnType<Expect['getState']>,
locator: Locator, locator: Locator,
options?: { timeout?: number }, options?: { timeout?: number },
) { ) {
return toBeTruthy.call(this, 'toBeEnabled', locator, async timeout => { return toBeTruthy.call(this, 'toBeEnabled', locator, 'Locator', async timeout => {
return await locator.isEnabled({ timeout }); return await locator.isEnabled({ timeout });
}, options); }, options);
} }
export async function toBeFocused( export function toBeFocused(
this: ReturnType<Expect['getState']>, this: ReturnType<Expect['getState']>,
locator: Locator, locator: Locator,
options?: { timeout?: number }, options?: { timeout?: number },
) { ) {
return toBeTruthy.call(this, 'toBeFocused', locator, async timeout => { return toBeTruthy.call(this, 'toBeFocused', locator, 'Locator', async timeout => {
return await locator.evaluate(element => { return await locator.evaluate(element => {
return document.activeElement === element; return document.activeElement === element;
}, { timeout }); }, { timeout });
}, options); }, options);
} }
export async function toBeHidden( export function toBeHidden(
this: ReturnType<Expect['getState']>, this: ReturnType<Expect['getState']>,
locator: Locator, locator: Locator,
options?: { timeout?: number }, options?: { timeout?: number },
) { ) {
return toBeTruthy.call(this, 'toBeHidden', locator, async timeout => { return toBeTruthy.call(this, 'toBeHidden', locator, 'Locator', async timeout => {
return await locator.isHidden({ timeout }); return await locator.isHidden({ timeout });
}, options); }, options);
} }
export async function toBeSelected( export function toBeSelected(
this: ReturnType<Expect['getState']>, this: ReturnType<Expect['getState']>,
locator: Locator, locator: Locator,
options?: { timeout?: number }, options?: { timeout?: number },
) { ) {
return toBeTruthy.call(this, 'toBeSelected', locator, async timeout => { return toBeTruthy.call(this, 'toBeSelected', locator, 'Locator', async timeout => {
return await locator.evaluate(element => { return await locator.evaluate(element => {
return (element as HTMLOptionElement).selected; return (element as HTMLOptionElement).selected;
}, { timeout }); }, { timeout });
}, options); }, options);
} }
export async function toBeVisible( export function toBeVisible(
this: ReturnType<Expect['getState']>, this: ReturnType<Expect['getState']>,
locator: Locator, locator: Locator,
options?: { timeout?: number }, options?: { timeout?: number },
) { ) {
return toBeTruthy.call(this, 'toBeVisible', locator, async timeout => { return toBeTruthy.call(this, 'toBeVisible', locator, 'Locator', async timeout => {
return await locator.isVisible({ timeout }); return await locator.isVisible({ timeout });
}, options); }, options);
} }
export async function toContainText( export function toContainText(
this: ReturnType<Expect['getState']>, this: ReturnType<Expect['getState']>,
locator: Locator, locator: Locator,
expected: string, expected: string,
options?: { timeout?: number, useInnerText?: boolean }, options?: { timeout?: number, useInnerText?: boolean },
) { ) {
return toMatchText.call(this, 'toContainText', locator, async timeout => { return toMatchText.call(this, 'toContainText', locator, 'Locator', async timeout => {
if (options?.useInnerText) if (options?.useInnerText)
return await locator.innerText({ timeout }); return await locator.innerText({ timeout });
return await locator.textContent() || ''; return await locator.textContent() || '';
}, expected, { ...options, matchSubstring: true }); }, expected, { ...options, matchSubstring: true });
} }
export async function toHaveAttr( export function toHaveAttr(
this: ReturnType<Expect['getState']>, this: ReturnType<Expect['getState']>,
locator: Locator, locator: Locator,
name: string, name: string,
expected: string | RegExp, expected: string | RegExp,
options?: { timeout?: number }, options?: { timeout?: number },
) { ) {
return toMatchText.call(this, 'toHaveAttr', locator, async timeout => { return toMatchText.call(this, 'toHaveAttr', locator, 'Locator', async timeout => {
return await locator.getAttribute(name, { timeout }) || ''; return await locator.getAttribute(name, { timeout }) || '';
}, expected, options); }, expected, options);
} }
export async function toHaveClass( export function toHaveClass(
this: ReturnType<Expect['getState']>, this: ReturnType<Expect['getState']>,
locator: Locator, locator: Locator,
expected: string, expected: string | RegExp | string[],
options?: { timeout?: number }, options?: { timeout?: number },
) { ) {
return toMatchText.call(this, 'toHaveClass', locator, async timeout => { if (Array.isArray(expected)) {
return await locator.evaluate(element => element.className, { timeout }); return toEqual.call(this, 'toHaveClass', locator, 'Locator', async () => {
}, expected, { ...options, matchSubstring: true }); return await locator.evaluateAll(ee => ee.map(e => e.className));
}, expected, options);
} else {
return toMatchText.call(this, 'toHaveClass', locator, 'Locator', async timeout => {
return await locator.evaluate(element => element.className, { timeout });
}, expected, options);
}
} }
export async function toHaveCSS( export function toHaveCount(
this: ReturnType<Expect['getState']>,
locator: Locator,
expected: number,
options?: { timeout?: number },
) {
return toEqual.call(this, 'toHaveCount', locator, 'Locator', async timeout => {
return await locator.count();
}, expected, options);
}
export function toHaveCSS(
this: ReturnType<Expect['getState']>, this: ReturnType<Expect['getState']>,
locator: Locator, locator: Locator,
name: string, name: string,
expected: string | RegExp, expected: string | RegExp,
options?: { timeout?: number }, options?: { timeout?: number },
) { ) {
return toMatchText.call(this, 'toHaveCSS', locator, async timeout => { return toMatchText.call(this, 'toHaveCSS', locator, 'Locator', async timeout => {
return await locator.evaluate(async (element, name) => { return await locator.evaluate(async (element, name) => {
return (window.getComputedStyle(element) as any)[name]; return (window.getComputedStyle(element) as any)[name];
}, name, { timeout }); }, name, { timeout });
}, expected, options); }, expected, options);
} }
export async function toHaveData( export function toHaveData(
this: ReturnType<Expect['getState']>, this: ReturnType<Expect['getState']>,
locator: Locator, locator: Locator,
name: string, name: string,
expected: string | RegExp, expected: string | RegExp,
options?: { timeout?: number }, options?: { timeout?: number },
) { ) {
return toMatchText.call(this, 'toHaveData', locator, async timeout => { return toMatchText.call(this, 'toHaveData', locator, 'Locator', async timeout => {
return await locator.getAttribute('data-' + name, { timeout }) || ''; return await locator.getAttribute('data-' + name, { timeout }) || '';
}, expected, options); }, expected, options);
} }
export async function toHaveId( export function toHaveId(
this: ReturnType<Expect['getState']>, this: ReturnType<Expect['getState']>,
locator: Locator, locator: Locator,
expected: string | RegExp, expected: string | RegExp,
options?: { timeout?: number }, options?: { timeout?: number },
) { ) {
return toMatchText.call(this, 'toHaveId', locator, async timeout => { return toMatchText.call(this, 'toHaveId', locator, 'Locator', async timeout => {
return await locator.getAttribute('id', { timeout }) || ''; return await locator.getAttribute('id', { timeout }) || '';
}, expected, options); }, expected, options);
} }
export async function toHaveLength( export function toHaveProp(
this: ReturnType<Expect['getState']>,
locator: Locator,
expected: number,
options?: { timeout?: number },
) {
if (typeof locator !== 'object' || locator.constructor.name !== 'Locator')
return matchers.toHaveLength.call(this, locator, expected);
return toEqual.call(this, 'toHaveLength', locator, async timeout => {
return await locator.count();
}, expected, { expectedType: 'number', ...options });
}
export async function toHaveProp(
this: ReturnType<Expect['getState']>, this: ReturnType<Expect['getState']>,
locator: Locator, locator: Locator,
name: string, name: string,
expected: number, expected: number,
options?: { timeout?: number }, options?: { timeout?: number },
) { ) {
return toEqual.call(this, 'toHaveProp', locator, async timeout => { return toEqual.call(this, 'toHaveProp', locator, 'Locator', async timeout => {
return await locator.evaluate((element, name) => (element as any)[name], name, { timeout }); return await locator.evaluate((element, name) => (element as any)[name], name, { timeout });
}, expected, { expectedType: 'number', ...options });
}
export async function toHaveText(
this: ReturnType<Expect['getState']>,
locator: Locator,
expected: string | RegExp,
options?: { timeout?: number, useInnerText?: boolean },
) {
return toMatchText.call(this, 'toHaveText', locator, async timeout => {
if (options?.useInnerText)
return await locator.innerText({ timeout });
return await locator.textContent() || '';
}, expected, options); }, expected, options);
} }
export async function toHaveValue( export function toHaveText(
this: ReturnType<Expect['getState']>,
locator: Locator,
expected: string | RegExp | string[],
options?: { timeout?: number, useInnerText?: boolean },
) {
if (Array.isArray(expected)) {
return toEqual.call(this, 'toHaveText', locator, 'Locator', async () => {
return locator.evaluateAll((ee, useInnerText) => {
return ee.map(e => useInnerText ? (e as HTMLElement).innerText : e.textContent || '');
}, options?.useInnerText);
}, expected, options);
} else {
return toMatchText.call(this, 'toHaveText', locator, 'Locator', async timeout => {
if (options?.useInnerText)
return await locator.innerText({ timeout });
return await locator.textContent() || '';
}, expected, options);
}
}
export function toHaveTitle(
this: ReturnType<Expect['getState']>,
page: Page,
expected: string | RegExp,
options?: { timeout?: number },
) {
return toMatchText.call(this, 'toHaveTitle', page, 'Page', async () => {
return await page.title();
}, expected, options);
}
export function toHaveURL(
this: ReturnType<Expect['getState']>,
page: Page,
expected: string | RegExp,
options?: { timeout?: number },
) {
return toMatchText.call(this, 'toHaveURL', page, 'Page', async () => {
return page.url();
}, expected, options);
}
export function toHaveValue(
this: ReturnType<Expect['getState']>, this: ReturnType<Expect['getState']>,
locator: Locator, locator: Locator,
expected: string | RegExp, expected: string | RegExp,
options?: { timeout?: number }, options?: { timeout?: number },
) { ) {
return toMatchText.call(this, 'toHaveValue', locator, async timeout => { return toMatchText.call(this, 'toHaveValue', locator, 'Locator', async timeout => {
return await locator.inputValue({ timeout }); return await locator.inputValue({ timeout });
}, expected, options); }, expected, options);
} }

View file

@ -18,22 +18,22 @@ import {
matcherHint, matcherHint,
MatcherHintOptions MatcherHintOptions
} from 'jest-matcher-utils'; } from 'jest-matcher-utils';
import { Locator } from '../../..';
import { currentTestInfo } from '../globals'; import { currentTestInfo } from '../globals';
import type { Expect } from '../types'; import type { Expect } from '../types';
import { expectLocator, monotonicTime, pollUntilDeadline } from '../util'; import { expectType, monotonicTime, pollUntilDeadline } from '../util';
export async function toBeTruthy<T>( export async function toBeTruthy<T>(
this: ReturnType<Expect['getState']>, this: ReturnType<Expect['getState']>,
matcherName: string, matcherName: string,
locator: Locator, receiver: any,
receiverType: string,
query: (timeout: number) => Promise<T>, query: (timeout: number) => Promise<T>,
options: { timeout?: number } = {}, options: { timeout?: number } = {},
) { ) {
const testInfo = currentTestInfo(); const testInfo = currentTestInfo();
if (!testInfo) if (!testInfo)
throw new Error(`${matcherName} must be called during the test`); throw new Error(`${matcherName} must be called during the test`);
expectLocator(locator, matcherName); expectType(receiver, receiverType, matcherName);
const matcherOptions: MatcherHintOptions = { const matcherOptions: MatcherHintOptions = {
isNot: this.isNot, isNot: this.isNot,

View file

@ -25,10 +25,9 @@ import {
printReceived, printReceived,
stringify stringify
} from 'jest-matcher-utils'; } from 'jest-matcher-utils';
import { Locator } from '../../..';
import { currentTestInfo } from '../globals'; import { currentTestInfo } from '../globals';
import type { Expect } from '../types'; import type { Expect } from '../types';
import { expectLocator, monotonicTime, pollUntilDeadline } from '../util'; import { expectType, monotonicTime, pollUntilDeadline } from '../util';
// Omit colon and one or more spaces, so can call getLabelPrinter. // Omit colon and one or more spaces, so can call getLabelPrinter.
const EXPECTED_LABEL = 'Expected'; const EXPECTED_LABEL = 'Expected';
@ -40,7 +39,8 @@ const isExpand = (expand?: boolean): boolean => expand !== false;
export async function toEqual<T>( export async function toEqual<T>(
this: ReturnType<Expect['getState']>, this: ReturnType<Expect['getState']>,
matcherName: string, matcherName: string,
locator: Locator, receiver: any,
receiverType: string,
query: (timeout: number) => Promise<T>, query: (timeout: number) => Promise<T>,
expected: T, expected: T,
options: { timeout?: number } = {}, options: { timeout?: number } = {},
@ -48,7 +48,7 @@ export async function toEqual<T>(
const testInfo = currentTestInfo(); const testInfo = currentTestInfo();
if (!testInfo) if (!testInfo)
throw new Error(`${matcherName} must be called during the test`); throw new Error(`${matcherName} must be called during the test`);
expectLocator(locator, matcherName); expectType(receiver, receiverType, matcherName);
const matcherOptions: MatcherHintOptions = { const matcherOptions: MatcherHintOptions = {
comment: 'deep equality', comment: 'deep equality',

View file

@ -28,15 +28,15 @@ import {
printReceived, printReceived,
printWithType, printWithType,
} from 'jest-matcher-utils'; } from 'jest-matcher-utils';
import { Locator } from '../../..';
import { currentTestInfo } from '../globals'; import { currentTestInfo } from '../globals';
import type { Expect } from '../types'; import type { Expect } from '../types';
import { expectLocator, monotonicTime, pollUntilDeadline } from '../util'; import { expectType, monotonicTime, pollUntilDeadline } from '../util';
export async function toMatchText( export async function toMatchText(
this: ReturnType<Expect['getState']>, this: ReturnType<Expect['getState']>,
matcherName: string, matcherName: string,
locator: Locator, receiver: any,
receiverType: string,
query: (timeout: number) => Promise<string>, query: (timeout: number) => Promise<string>,
expected: string | RegExp, expected: string | RegExp,
options: { timeout?: number, matchSubstring?: boolean } = {}, options: { timeout?: number, matchSubstring?: boolean } = {},
@ -44,7 +44,7 @@ export async function toMatchText(
const testInfo = currentTestInfo(); const testInfo = currentTestInfo();
if (!testInfo) if (!testInfo)
throw new Error(`${matcherName} must be called during the test`); throw new Error(`${matcherName} must be called during the test`);
expectLocator(locator, matcherName); expectType(receiver, receiverType, matcherName);
const matcherOptions: MatcherHintOptions = { const matcherOptions: MatcherHintOptions = {
isNot: this.isNot, isNot: this.isNot,

View file

@ -187,7 +187,7 @@ export function errorWithLocation(location: Location, message: string) {
return new Error(`${formatLocation(location)}: ${message}`); return new Error(`${formatLocation(location)}: ${message}`);
} }
export function expectLocator(receiver: any, matcherName: string) { export function expectType(receiver: any, type: string, matcherName: string) {
if (typeof receiver !== 'object' || receiver.constructor.name !== 'Locator') if (typeof receiver !== 'object' || receiver.constructor.name !== type)
throw new Error(`${matcherName} can be only used with Locator object`); throw new Error(`${matcherName} can be only used with ${type} object`);
} }

View file

@ -16,7 +16,7 @@
import { test, expect, stripAscii } from './playwright-test-fixtures'; import { test, expect, stripAscii } from './playwright-test-fixtures';
test('should support toHaveLength', async ({ runInlineTest }) => { test('should support toHaveCount', async ({ runInlineTest }) => {
const result = await runInlineTest({ const result = await runInlineTest({
'a.test.ts': ` 'a.test.ts': `
const { test } = pwt; const { test } = pwt;
@ -24,8 +24,7 @@ test('should support toHaveLength', async ({ runInlineTest }) => {
test('pass', async ({ page }) => { test('pass', async ({ page }) => {
await page.setContent('<select><option>One</option><option>Two</option></select>'); await page.setContent('<select><option>One</option><option>Two</option></select>');
const locator = page.locator('option'); const locator = page.locator('option');
await expect(locator).toHaveLength(2); await expect(locator).toHaveCount(2);
await expect([1, 2]).toHaveLength(2);
}); });
`, `,
}, { workers: 1 }); }, { workers: 1 });
@ -68,19 +67,93 @@ test('should support toHaveClass', async ({ runInlineTest }) => {
test('pass', async ({ page }) => { test('pass', async ({ page }) => {
await page.setContent('<div class="foo bar baz"></div>'); await page.setContent('<div class="foo bar baz"></div>');
const locator = page.locator('div'); const locator = page.locator('div');
await expect(locator).toHaveClass('foo'); await expect(locator).toHaveClass('foo bar baz');
}); });
test('fail', async ({ page }) => { test('fail', async ({ page }) => {
await page.setContent('<div class="bar baz"></div>'); await page.setContent('<div class="bar baz"></div>');
const locator = page.locator('div'); const locator = page.locator('div');
await expect(locator).toHaveClass('foo', { timeout: 1000 }); await expect(locator).toHaveClass('foo bar baz', { timeout: 1000 });
}); });
`, `,
}, { workers: 1 }); }, { workers: 1 });
const output = stripAscii(result.output); const output = stripAscii(result.output);
expect(output).toContain('expect(locator).toHaveClass'); expect(output).toContain('expect(locator).toHaveClass');
expect(output).toContain('Expected substring: \"foo\"'); expect(output).toContain('Expected string: \"foo bar baz\"');
expect(result.passed).toBe(1);
expect(result.failed).toBe(1);
expect(result.exitCode).toBe(1);
});
test('should support toHaveClass w/ array', async ({ runInlineTest }) => {
const result = await runInlineTest({
'a.test.ts': `
const { test } = pwt;
test('pass', async ({ page }) => {
await page.setContent('<div class="foo"></div><div class="bar"></div><div class="baz"></div>');
const locator = page.locator('div');
await expect(locator).toHaveClass(['foo', 'bar', 'baz']);
});
test('fail', async ({ page }) => {
await page.setContent('<div class="foo"></div><div class="bar"></div><div class="bar"></div>');
const locator = page.locator('div');
await expect(locator).toHaveClass(['foo', 'bar', 'baz'], { timeout: 1000 });
});
`,
}, { workers: 1 });
const output = stripAscii(result.output);
expect(output).toContain('expect(received).toHaveClass(expected)');
expect(output).toContain('- \"baz\",');
expect(result.passed).toBe(1);
expect(result.failed).toBe(1);
expect(result.exitCode).toBe(1);
});
test('should support toHaveTitle', async ({ runInlineTest }) => {
const result = await runInlineTest({
'a.test.ts': `
const { test } = pwt;
test('pass', async ({ page }) => {
await page.setContent('<title>Hello</title>');
await expect(page).toHaveTitle('Hello');
});
test('fail', async ({ page }) => {
await page.setContent('<title>Bye</title>');
await expect(page).toHaveTitle('Hello', { timeout: 100 });
});
`,
}, { workers: 1 });
const output = stripAscii(result.output);
expect(output).toContain('expect(page).toHaveTitle');
expect(output).toContain('Expected string: \"Hello\"');
expect(result.passed).toBe(1);
expect(result.failed).toBe(1);
expect(result.exitCode).toBe(1);
});
test('should support toHaveURL', async ({ runInlineTest }) => {
const result = await runInlineTest({
'a.test.ts': `
const { test } = pwt;
test('pass', async ({ page }) => {
await page.goto('data:text/html,<div>A</div>');
await expect(page).toHaveURL('data:text/html,<div>A</div>');
});
test('fail', async ({ page }) => {
await page.goto('data:text/html,<div>B</div>');
await expect(page).toHaveURL('wrong', { timeout: 100 });
});
`,
}, { workers: 1 });
const output = stripAscii(result.output);
expect(output).toContain('expect(page).toHaveURL');
expect(output).toContain('Expected string: \"wrong\"');
expect(result.passed).toBe(1); expect(result.passed).toBe(1);
expect(result.failed).toBe(1); expect(result.failed).toBe(1);
expect(result.exitCode).toBe(1); expect(result.exitCode).toBe(1);

View file

@ -78,6 +78,33 @@ test('should support toHaveText w/ text', async ({ runInlineTest }) => {
expect(result.exitCode).toBe(1); expect(result.exitCode).toBe(1);
}); });
test('should support toHaveText w/ array', async ({ runInlineTest }) => {
const result = await runInlineTest({
'a.test.ts': `
const { test } = pwt;
test('pass', async ({ page }) => {
await page.setContent('<div>Text 1</div><div>Text 2</div>');
const locator = page.locator('div');
await expect(locator).toHaveText(['Text 1', 'Text 2']);
});
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 2'], { 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 2\"');
expect(result.passed).toBe(1);
expect(result.failed).toBe(1);
expect(result.exitCode).toBe(1);
});
test('should support toHaveText eventually', async ({ runInlineTest }) => { test('should support toHaveText eventually', async ({ runInlineTest }) => {
const result = await runInlineTest({ const result = await runInlineTest({
'a.test.ts': ` 'a.test.ts': `

23
types/testExpect.d.ts vendored
View file

@ -127,7 +127,12 @@ declare global {
/** /**
* Asserts that DOM node has a given CSS class. * Asserts that DOM node has a given CSS class.
*/ */
toHaveClass(className: string, options?: { timeout?: number }): Promise<R>; toHaveClass(className: string | RegExp | string[], options?: { timeout?: number }): Promise<R>;
/**
* Asserts number of DOM nodes matching given locator.
*/
toHaveCount(expected: number, options?: { timeout?: number }): Promise<R>;
/** /**
* Asserts element's computed CSS property `name` matches expected value. * Asserts element's computed CSS property `name` matches expected value.
@ -150,11 +155,21 @@ declare global {
toHaveProp(name: string, value: any, options?: { timeout?: number }): Promise<R>; toHaveProp(name: string, value: any, options?: { timeout?: number }): Promise<R>;
/** /**
* Asserts element's exact text content. * Asserts element's text content.
*/ */
toHaveText(expected: string | RegExp, options?: { timeout?: number, useInnerText?: boolean }): Promise<R>; toHaveText(expected: string | RegExp | string[], options?: { timeout?: number, useInnerText?: boolean }): Promise<R>;
/** /**
* Asserts page's title.
*/
toHaveTitle(expected: string | RegExp, options?: { timeout?: number }): Promise<R>;
/**
* Asserts page's title.
*/
toHaveURL(expected: string | RegExp, options?: { timeout?: number }): Promise<R>;
/**
* Asserts input element's value. * Asserts input element's value.
*/ */
toHaveValue(expected: string | RegExp, options?: { timeout?: number }): Promise<R>; toHaveValue(expected: string | RegExp, options?: { timeout?: number }): Promise<R>;