mirror of
https://github.com/microsoft/playwright.git
synced 2024-12-12 11:50:22 +03:00
feat(expect): add more matchers (#7891)
This commit is contained in:
parent
3f0485486d
commit
3187ffdebf
@ -15,9 +15,29 @@
|
||||
*/
|
||||
|
||||
import expectLibrary from 'expect';
|
||||
import { toBeChecked, toBeDisabled, toBeEditable, toBeEmpty, toBeEnabled, toBeFocused, toBeHidden, toBeVisible } from './matchers/toBeTruthy';
|
||||
import {
|
||||
toBeChecked,
|
||||
toBeDisabled,
|
||||
toBeEditable,
|
||||
toBeEmpty,
|
||||
toBeEnabled,
|
||||
toBeFocused,
|
||||
toBeHidden,
|
||||
toBeSelected,
|
||||
toBeVisible
|
||||
} from './matchers/toBeTruthy';
|
||||
import { toHaveLength, toHaveProp } from './matchers/toEqual';
|
||||
import { toMatchSnapshot } from './matchers/toMatchSnapshot';
|
||||
import { toContainText, toHaveAttr, toHaveCSS, toHaveData, toHaveId, toHaveText, toHaveValue } from './matchers/toMatchText';
|
||||
import {
|
||||
toContainText,
|
||||
toHaveAttr,
|
||||
toHaveCSS,
|
||||
toHaveClass,
|
||||
toHaveData,
|
||||
toHaveId,
|
||||
toHaveText,
|
||||
toHaveValue
|
||||
} from './matchers/toMatchText';
|
||||
import type { Expect } from './types';
|
||||
|
||||
export const expect: Expect = expectLibrary as any;
|
||||
@ -30,12 +50,16 @@ expectLibrary.extend({
|
||||
toBeEnabled,
|
||||
toBeFocused,
|
||||
toBeHidden,
|
||||
toBeSelected,
|
||||
toBeVisible,
|
||||
toContainText,
|
||||
toHaveAttr,
|
||||
toHaveCSS,
|
||||
toHaveClass,
|
||||
toHaveData,
|
||||
toHaveId,
|
||||
toHaveLength,
|
||||
toHaveProp,
|
||||
toHaveText,
|
||||
toHaveValue,
|
||||
toMatchSnapshot,
|
||||
|
@ -16,50 +16,46 @@
|
||||
|
||||
import {
|
||||
matcherHint,
|
||||
MatcherHintOptions,
|
||||
printReceived
|
||||
MatcherHintOptions
|
||||
} from 'jest-matcher-utils';
|
||||
import { Locator } from '../../..';
|
||||
import { currentTestInfo } from '../globals';
|
||||
import type { Expect } from '../types';
|
||||
import { monotonicTime, pollUntilDeadline } from '../util';
|
||||
import { expectLocator, monotonicTime, pollUntilDeadline } from '../util';
|
||||
|
||||
|
||||
async function toBeTruthyImpl(
|
||||
async function toBeTruthyImpl<T>(
|
||||
this: ReturnType<Expect['getState']>,
|
||||
matcherName: string,
|
||||
query: (timeout: number) => Promise<boolean>,
|
||||
locator: Locator,
|
||||
query: (timeout: number) => Promise<T>,
|
||||
options: { timeout?: number } = {},
|
||||
) {
|
||||
const testInfo = currentTestInfo();
|
||||
if (!testInfo)
|
||||
throw new Error(`toMatchSnapshot() must be called during the test`);
|
||||
throw new Error(`${matcherName} must be called during the test`);
|
||||
expectLocator(locator, matcherName);
|
||||
|
||||
const matcherOptions: MatcherHintOptions = {
|
||||
isNot: this.isNot,
|
||||
promise: this.promise,
|
||||
};
|
||||
|
||||
let received: boolean;
|
||||
let received: T;
|
||||
let pass = false;
|
||||
const timeout = options.timeout === 0 ? 0 : options.timeout || testInfo.timeout;
|
||||
const deadline = timeout ? monotonicTime() + timeout : 0;
|
||||
|
||||
try {
|
||||
await pollUntilDeadline(async () => {
|
||||
const remainingTime = deadline ? deadline - monotonicTime() : 0;
|
||||
received = await query(remainingTime);
|
||||
pass = !!received;
|
||||
return pass === !matcherOptions.isNot;
|
||||
}, deadline, 100);
|
||||
} catch (e) {
|
||||
pass = false;
|
||||
}
|
||||
// TODO: interrupt on timeout for nice message.
|
||||
await pollUntilDeadline(async () => {
|
||||
const remainingTime = deadline ? deadline - monotonicTime() : 0;
|
||||
received = await query(remainingTime);
|
||||
pass = !!received;
|
||||
return pass === !matcherOptions.isNot;
|
||||
}, deadline, 100);
|
||||
|
||||
const message = () =>
|
||||
matcherHint(matcherName, undefined, '', matcherOptions) +
|
||||
'\n\n' +
|
||||
`Received: ${printReceived(received)}`;
|
||||
const message = () => {
|
||||
return matcherHint(matcherName, undefined, '', matcherOptions);
|
||||
};
|
||||
|
||||
return { message, pass };
|
||||
}
|
||||
@ -69,7 +65,7 @@ export async function toBeChecked(
|
||||
locator: Locator,
|
||||
options?: { timeout?: number },
|
||||
) {
|
||||
return toBeTruthyImpl.call(this, 'toBeChecked', async timeout => {
|
||||
return toBeTruthyImpl.call(this, 'toBeChecked', locator, async timeout => {
|
||||
return await locator.isChecked({ timeout });
|
||||
}, options);
|
||||
}
|
||||
@ -79,7 +75,7 @@ export async function toBeEditable(
|
||||
locator: Locator,
|
||||
options?: { timeout?: number },
|
||||
) {
|
||||
return toBeTruthyImpl.call(this, 'toBeEditable', async timeout => {
|
||||
return toBeTruthyImpl.call(this, 'toBeEditable', locator, async timeout => {
|
||||
return await locator.isEditable({ timeout });
|
||||
}, options);
|
||||
}
|
||||
@ -89,7 +85,7 @@ export async function toBeEnabled(
|
||||
locator: Locator,
|
||||
options?: { timeout?: number },
|
||||
) {
|
||||
return toBeTruthyImpl.call(this, 'toBeEnabled', async timeout => {
|
||||
return toBeTruthyImpl.call(this, 'toBeEnabled', locator, async timeout => {
|
||||
return await locator.isEnabled({ timeout });
|
||||
}, options);
|
||||
}
|
||||
@ -99,7 +95,7 @@ export async function toBeDisabled(
|
||||
locator: Locator,
|
||||
options?: { timeout?: number },
|
||||
) {
|
||||
return toBeTruthyImpl.call(this, 'toBeDisabled', async timeout => {
|
||||
return toBeTruthyImpl.call(this, 'toBeDisabled', locator, async timeout => {
|
||||
return await locator.isDisabled({ timeout });
|
||||
}, options);
|
||||
}
|
||||
@ -109,7 +105,7 @@ export async function toBeEmpty(
|
||||
locator: Locator,
|
||||
options?: { timeout?: number },
|
||||
) {
|
||||
return toBeTruthyImpl.call(this, 'toBeEmpty', async timeout => {
|
||||
return toBeTruthyImpl.call(this, 'toBeEmpty', locator, async timeout => {
|
||||
return await locator.evaluate(element => {
|
||||
if (element.nodeName === 'INPUT' || element.nodeName === 'TEXTAREA')
|
||||
return !(element as HTMLInputElement).value;
|
||||
@ -123,7 +119,7 @@ export async function toBeHidden(
|
||||
locator: Locator,
|
||||
options?: { timeout?: number },
|
||||
) {
|
||||
return toBeTruthyImpl.call(this, 'toBeHidden', async timeout => {
|
||||
return toBeTruthyImpl.call(this, 'toBeHidden', locator, async timeout => {
|
||||
return await locator.isHidden({ timeout });
|
||||
}, options);
|
||||
}
|
||||
@ -133,7 +129,7 @@ export async function toBeVisible(
|
||||
locator: Locator,
|
||||
options?: { timeout?: number },
|
||||
) {
|
||||
return toBeTruthyImpl.call(this, 'toBeVisible', async timeout => {
|
||||
return toBeTruthyImpl.call(this, 'toBeVisible', locator, async timeout => {
|
||||
return await locator.isVisible({ timeout });
|
||||
}, options);
|
||||
}
|
||||
@ -143,9 +139,21 @@ export async function toBeFocused(
|
||||
locator: Locator,
|
||||
options?: { timeout?: number },
|
||||
) {
|
||||
return toBeTruthyImpl.call(this, 'toBeFocused', async timeout => {
|
||||
return toBeTruthyImpl.call(this, 'toBeFocused', locator, async timeout => {
|
||||
return await locator.evaluate(element => {
|
||||
return document.activeElement === element;
|
||||
}, { timeout });
|
||||
}, options);
|
||||
}
|
||||
|
||||
export async function toBeSelected(
|
||||
this: ReturnType<Expect['getState']>,
|
||||
locator: Locator,
|
||||
options?: { timeout?: number },
|
||||
) {
|
||||
return toBeTruthyImpl.call(this, 'toBeSelected', locator, async timeout => {
|
||||
return await locator.evaluate(element => {
|
||||
return (element as HTMLOptionElement).selected;
|
||||
}, { timeout });
|
||||
}, options);
|
||||
}
|
||||
|
121
src/test/matchers/toEqual.ts
Normal file
121
src/test/matchers/toEqual.ts
Normal file
@ -0,0 +1,121 @@
|
||||
/**
|
||||
* Copyright Microsoft Corporation. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { equals } from 'expect/build/jasmineUtils';
|
||||
import matchers from 'expect/build/matchers';
|
||||
import {
|
||||
iterableEquality
|
||||
} from 'expect/build/utils';
|
||||
import {
|
||||
matcherHint, MatcherHintOptions,
|
||||
printDiffOrStringify,
|
||||
printExpected,
|
||||
printReceived,
|
||||
stringify
|
||||
} from 'jest-matcher-utils';
|
||||
import { Locator } from '../../..';
|
||||
import { currentTestInfo } from '../globals';
|
||||
import type { Expect } from '../types';
|
||||
import { expectLocator, monotonicTime, pollUntilDeadline } from '../util';
|
||||
|
||||
// Omit colon and one or more spaces, so can call getLabelPrinter.
|
||||
const EXPECTED_LABEL = 'Expected';
|
||||
const RECEIVED_LABEL = 'Received';
|
||||
|
||||
// The optional property of matcher context is true if undefined.
|
||||
const isExpand = (expand?: boolean): boolean => expand !== false;
|
||||
|
||||
async function toEqualImpl<T>(
|
||||
this: ReturnType<Expect['getState']>,
|
||||
matcherName: string,
|
||||
locator: Locator,
|
||||
query: (timeout: number) => Promise<T>,
|
||||
expected: T,
|
||||
options: { timeout?: number } = {},
|
||||
) {
|
||||
const testInfo = currentTestInfo();
|
||||
if (!testInfo)
|
||||
throw new Error(`${matcherName} must be called during the test`);
|
||||
expectLocator(locator, matcherName);
|
||||
|
||||
const matcherOptions: MatcherHintOptions = {
|
||||
comment: 'deep equality',
|
||||
isNot: this.isNot,
|
||||
promise: this.promise,
|
||||
};
|
||||
|
||||
let received: T | undefined = undefined;
|
||||
let pass = false;
|
||||
const timeout = options.timeout === 0 ? 0 : options.timeout || testInfo.timeout;
|
||||
const deadline = timeout ? monotonicTime() + timeout : 0;
|
||||
|
||||
// TODO: interrupt on timeout for nice message.
|
||||
await pollUntilDeadline(async () => {
|
||||
const remainingTime = deadline ? deadline - monotonicTime() : 0;
|
||||
received = await query(remainingTime);
|
||||
pass = equals(received, expected, [iterableEquality]);
|
||||
return pass === !matcherOptions.isNot;
|
||||
}, deadline, 100);
|
||||
|
||||
const message = pass
|
||||
? () =>
|
||||
matcherHint(matcherName, undefined, undefined, matcherOptions) +
|
||||
'\n\n' +
|
||||
`Expected: not ${printExpected(expected)}\n` +
|
||||
(stringify(expected) !== stringify(received)
|
||||
? `Received: ${printReceived(received)}`
|
||||
: '')
|
||||
: () =>
|
||||
matcherHint(matcherName, undefined, undefined, matcherOptions) +
|
||||
'\n\n' +
|
||||
printDiffOrStringify(
|
||||
expected,
|
||||
received,
|
||||
EXPECTED_LABEL,
|
||||
RECEIVED_LABEL,
|
||||
isExpand(this.expand),
|
||||
);
|
||||
|
||||
// Passing the actual and expected objects so that a custom reporter
|
||||
// could access them, for example in order to display a custom visual diff,
|
||||
// or create a different error message
|
||||
return { actual: received, expected, message, name: matcherName, pass };
|
||||
}
|
||||
|
||||
export async function toHaveLength(
|
||||
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 toEqualImpl.call(this, 'toHaveLength', locator, async timeout => {
|
||||
return await locator.count();
|
||||
}, expected, { expectedType: 'number', ...options });
|
||||
}
|
||||
|
||||
export async function toHaveProp(
|
||||
this: ReturnType<Expect['getState']>,
|
||||
locator: Locator,
|
||||
name: string,
|
||||
expected: number,
|
||||
options?: { timeout?: number },
|
||||
) {
|
||||
return toEqualImpl.call(this, 'toHaveProp', locator, async timeout => {
|
||||
return await locator.evaluate((element, name) => (element as any)[name], name, { timeout });
|
||||
}, expected, { expectedType: 'number', ...options });
|
||||
}
|
@ -31,18 +31,20 @@ import {
|
||||
import { Locator } from '../../..';
|
||||
import { currentTestInfo } from '../globals';
|
||||
import type { Expect } from '../types';
|
||||
import { monotonicTime, pollUntilDeadline } from '../util';
|
||||
import { expectLocator, monotonicTime, pollUntilDeadline } from '../util';
|
||||
|
||||
async function toMatchTextImpl(
|
||||
this: ReturnType<Expect['getState']>,
|
||||
matcherName: string,
|
||||
locator: Locator,
|
||||
query: (timeout: number) => Promise<string>,
|
||||
expected: string | RegExp,
|
||||
options: { timeout?: number, matchSubstring?: boolean } = {},
|
||||
) {
|
||||
const testInfo = currentTestInfo();
|
||||
if (!testInfo)
|
||||
throw new Error(`toMatchSnapshot() must be called during the test`);
|
||||
throw new Error(`${matcherName} must be called during the test`);
|
||||
expectLocator(locator, matcherName);
|
||||
|
||||
const matcherOptions: MatcherHintOptions = {
|
||||
isNot: this.isNot,
|
||||
@ -69,22 +71,19 @@ async function toMatchTextImpl(
|
||||
const timeout = options.timeout === 0 ? 0 : options.timeout || testInfo.timeout;
|
||||
const deadline = timeout ? monotonicTime() + timeout : 0;
|
||||
|
||||
try {
|
||||
await pollUntilDeadline(async () => {
|
||||
const remainingTime = deadline ? deadline - monotonicTime() : 0;
|
||||
received = await query(remainingTime);
|
||||
if (options.matchSubstring)
|
||||
pass = received.includes(expected as string);
|
||||
else if (typeof expected === 'string')
|
||||
pass = received === expected;
|
||||
else
|
||||
pass = expected.test(received);
|
||||
// TODO: interrupt on timeout for nice message.
|
||||
await pollUntilDeadline(async () => {
|
||||
const remainingTime = deadline ? deadline - monotonicTime() : 0;
|
||||
received = await query(remainingTime);
|
||||
if (options.matchSubstring)
|
||||
pass = received.includes(expected as string);
|
||||
else if (typeof expected === 'string')
|
||||
pass = received === expected;
|
||||
else
|
||||
pass = expected.test(received);
|
||||
|
||||
return pass === !matcherOptions.isNot;
|
||||
}, deadline, 100);
|
||||
} catch (e) {
|
||||
pass = false;
|
||||
}
|
||||
return pass === !matcherOptions.isNot;
|
||||
}, deadline, 100);
|
||||
|
||||
const stringSubstring = options.matchSubstring ? 'substring' : 'string';
|
||||
const message = pass
|
||||
@ -130,7 +129,7 @@ export async function toHaveText(
|
||||
expected: string | RegExp,
|
||||
options?: { timeout?: number, useInnerText?: boolean },
|
||||
) {
|
||||
return toMatchTextImpl.call(this, 'toHaveText', async timeout => {
|
||||
return toMatchTextImpl.call(this, 'toHaveText', locator, async timeout => {
|
||||
if (options?.useInnerText)
|
||||
return await locator.innerText({ timeout });
|
||||
return await locator.textContent() || '';
|
||||
@ -143,7 +142,7 @@ export async function toContainText(
|
||||
expected: string,
|
||||
options?: { timeout?: number, useInnerText?: boolean },
|
||||
) {
|
||||
return toMatchTextImpl.call(this, 'toContainText', async timeout => {
|
||||
return toMatchTextImpl.call(this, 'toContainText', locator, async timeout => {
|
||||
if (options?.useInnerText)
|
||||
return await locator.innerText({ timeout });
|
||||
return await locator.textContent() || '';
|
||||
@ -157,7 +156,7 @@ export async function toHaveAttr(
|
||||
expected: string | RegExp,
|
||||
options?: { timeout?: number },
|
||||
) {
|
||||
return toMatchTextImpl.call(this, 'toHaveAttr', async timeout => {
|
||||
return toMatchTextImpl.call(this, 'toHaveAttr', locator, async timeout => {
|
||||
return await locator.getAttribute(name, { timeout }) || '';
|
||||
}, expected, options);
|
||||
}
|
||||
@ -169,7 +168,7 @@ export async function toHaveData(
|
||||
expected: string | RegExp,
|
||||
options?: { timeout?: number },
|
||||
) {
|
||||
return toMatchTextImpl.call(this, 'toHaveData', async timeout => {
|
||||
return toMatchTextImpl.call(this, 'toHaveData', locator, async timeout => {
|
||||
return await locator.getAttribute('data-' + name, { timeout }) || '';
|
||||
}, expected, options);
|
||||
}
|
||||
@ -181,7 +180,7 @@ export async function toHaveCSS(
|
||||
expected: string | RegExp,
|
||||
options?: { timeout?: number },
|
||||
) {
|
||||
return toMatchTextImpl.call(this, 'toHaveCSS', async timeout => {
|
||||
return toMatchTextImpl.call(this, 'toHaveCSS', locator, async timeout => {
|
||||
return await locator.evaluate(async (element, name) => {
|
||||
return (window.getComputedStyle(element) as any)[name];
|
||||
}, name, { timeout });
|
||||
@ -194,7 +193,7 @@ export async function toHaveId(
|
||||
expected: string | RegExp,
|
||||
options?: { timeout?: number },
|
||||
) {
|
||||
return toMatchTextImpl.call(this, 'toHaveId', async timeout => {
|
||||
return toMatchTextImpl.call(this, 'toHaveId', locator, async timeout => {
|
||||
return await locator.getAttribute('id', { timeout }) || '';
|
||||
}, expected, options);
|
||||
}
|
||||
@ -205,7 +204,17 @@ export async function toHaveValue(
|
||||
expected: string | RegExp,
|
||||
options?: { timeout?: number },
|
||||
) {
|
||||
return toMatchTextImpl.call(this, 'toHaveValue', async timeout => {
|
||||
return toMatchTextImpl.call(this, 'toHaveValue', locator, async timeout => {
|
||||
return await locator.inputValue({ timeout });
|
||||
}, expected, options);
|
||||
}
|
||||
}
|
||||
export async function toHaveClass(
|
||||
this: ReturnType<Expect['getState']>,
|
||||
locator: Locator,
|
||||
expected: string,
|
||||
options?: { timeout?: number },
|
||||
) {
|
||||
return toMatchTextImpl.call(this, 'toHaveClass', locator, async timeout => {
|
||||
return await locator.evaluate(element => element.className, { timeout });
|
||||
}, expected, { ...options, matchSubstring: true });
|
||||
}
|
||||
|
@ -18,7 +18,7 @@ import util from 'util';
|
||||
import path from 'path';
|
||||
import type { TestError, Location } from './types';
|
||||
import { default as minimatch } from 'minimatch';
|
||||
import { TimeoutError } from '../utils/errors';
|
||||
import { errors } from '../..';
|
||||
|
||||
export class DeadlineRunner<T> {
|
||||
private _timer: NodeJS.Timer | undefined;
|
||||
@ -72,14 +72,19 @@ export async function raceAgainstDeadline<T>(promise: Promise<T>, deadline: numb
|
||||
|
||||
export async function pollUntilDeadline(func: () => Promise<boolean>, deadline: number, delay: number): Promise<void> {
|
||||
while (true) {
|
||||
if (await func())
|
||||
return;
|
||||
|
||||
const timeUntilDeadline = deadline ? deadline - monotonicTime() : Number.MAX_VALUE;
|
||||
if (timeUntilDeadline > 0)
|
||||
await new Promise(f => setTimeout(f, Math.min(timeUntilDeadline, delay)));
|
||||
else
|
||||
throw new TimeoutError('Timed out while waiting for condition to be met');
|
||||
if (timeUntilDeadline <= 0)
|
||||
break;
|
||||
|
||||
try {
|
||||
if (await func())
|
||||
return;
|
||||
} catch (e) {
|
||||
if (e instanceof errors.TimeoutError)
|
||||
return;
|
||||
throw e;
|
||||
}
|
||||
await new Promise(f => setTimeout(f, delay));
|
||||
}
|
||||
}
|
||||
|
||||
@ -181,3 +186,8 @@ export function errorWithFile(file: string, message: string) {
|
||||
export function errorWithLocation(location: Location, message: string) {
|
||||
return new Error(`${formatLocation(location)}: ${message}`);
|
||||
}
|
||||
|
||||
export function expectLocator(receiver: any, matcherName: string) {
|
||||
if (typeof receiver !== 'object' || receiver.constructor.name !== 'Locator')
|
||||
throw new Error(`${matcherName} can be only used with Locator object`);
|
||||
}
|
||||
|
@ -47,3 +47,15 @@ it('should throw on capture w/ nth()', async ({page}) => {
|
||||
const e = await page.locator('*css=div >> p').nth(0).click().catch(e => e);
|
||||
expect(e.message).toContain(`Can't query n-th element`);
|
||||
});
|
||||
|
||||
it('should throw on due to strictness', async ({page}) => {
|
||||
await page.setContent(`<div>A</div><div>B</div>`);
|
||||
const e = await page.locator('div').isVisible().catch(e => e);
|
||||
expect(e.message).toContain(`strict mode violation`);
|
||||
});
|
||||
|
||||
it('should throw on due to strictness 2', async ({page}) => {
|
||||
await page.setContent(`<select><option>One</option><option>Two</option></select>`);
|
||||
const e = await page.locator('option').evaluate(e => {}).catch(e => e);
|
||||
expect(e.message).toContain(`strict mode violation`);
|
||||
});
|
||||
|
87
tests/playwright-test/playwright.expect.misc.spec.ts
Normal file
87
tests/playwright-test/playwright.expect.misc.spec.ts
Normal file
@ -0,0 +1,87 @@
|
||||
/**
|
||||
* Copyright (c) Microsoft Corporation.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { test, expect, stripAscii } from './playwright-test-fixtures';
|
||||
|
||||
test('should support toHaveLength', async ({ runInlineTest }) => {
|
||||
const result = await runInlineTest({
|
||||
'a.test.ts': `
|
||||
const { test } = pwt;
|
||||
|
||||
test('pass', async ({ page }) => {
|
||||
await page.setContent('<select><option>One</option><option>Two</option></select>');
|
||||
const locator = page.locator('option');
|
||||
await expect(locator).toHaveLength(2);
|
||||
await expect([1, 2]).toHaveLength(2);
|
||||
});
|
||||
`,
|
||||
}, { workers: 1 });
|
||||
expect(result.passed).toBe(1);
|
||||
expect(result.exitCode).toBe(0);
|
||||
});
|
||||
|
||||
test('should support toHaveProp', async ({ runInlineTest }) => {
|
||||
const result = await runInlineTest({
|
||||
'a.test.ts': `
|
||||
const { test } = pwt;
|
||||
|
||||
test('pass', async ({ page }) => {
|
||||
await page.setContent('<div></div>');
|
||||
await page.$eval('div', e => e.foo = { a: 1, b: 'string', c: new Date(1627503992000) });
|
||||
const locator = page.locator('div');
|
||||
await expect(locator).toHaveProp('foo', { a: 1, b: 'string', c: new Date(1627503992000) });
|
||||
});
|
||||
|
||||
test('fail', async ({ page }) => {
|
||||
await page.setContent('<div></div>');
|
||||
await page.$eval('div', e => e.foo = { a: 1, b: 'string', c: new Date(1627503992000) });
|
||||
const locator = page.locator('div');
|
||||
await expect(locator).toHaveProp('foo', { a: 1, b: 'string', c: new Date(1627503992001) }, { timeout: 1000 });
|
||||
});
|
||||
`,
|
||||
}, { workers: 1 });
|
||||
const output = stripAscii(result.output);
|
||||
expect(output).toContain('- "c"');
|
||||
expect(result.passed).toBe(1);
|
||||
expect(result.failed).toBe(1);
|
||||
expect(result.exitCode).toBe(1);
|
||||
});
|
||||
|
||||
test('should support toHaveClass', async ({ runInlineTest }) => {
|
||||
const result = await runInlineTest({
|
||||
'a.test.ts': `
|
||||
const { test } = pwt;
|
||||
|
||||
test('pass', async ({ page }) => {
|
||||
await page.setContent('<div class="foo bar baz"></div>');
|
||||
const locator = page.locator('div');
|
||||
await expect(locator).toHaveClass('foo');
|
||||
});
|
||||
|
||||
test('fail', async ({ page }) => {
|
||||
await page.setContent('<div class="bar baz"></div>');
|
||||
const locator = page.locator('div');
|
||||
await expect(locator).toHaveClass('foo', { timeout: 1000 });
|
||||
});
|
||||
`,
|
||||
}, { workers: 1 });
|
||||
const output = stripAscii(result.output);
|
||||
expect(output).toContain('expect(locator).toHaveClass');
|
||||
expect(output).toContain('Expected substring: \"foo\"');
|
||||
expect(result.passed).toBe(1);
|
||||
expect(result.failed).toBe(1);
|
||||
expect(result.exitCode).toBe(1);
|
||||
});
|
@ -36,7 +36,7 @@ test('should support toBeChecked', async ({ runInlineTest }) => {
|
||||
test('fail', async ({ page }) => {
|
||||
await page.setContent('<input type=checkbox></input>');
|
||||
const locator = page.locator('input');
|
||||
await expect(locator).toBeChecked({ timeout: 100 });
|
||||
await expect(locator).toBeChecked({ timeout: 1000 });
|
||||
});
|
||||
`,
|
||||
}, { workers: 1 });
|
||||
@ -72,7 +72,7 @@ test('should support toBeEditable, toBeEnabled, toBeDisabled, toBeEmpty', async
|
||||
});
|
||||
|
||||
test('empty input', async ({ page }) => {
|
||||
await page.setContent('<input></inpput>');
|
||||
await page.setContent('<input></input>');
|
||||
const locator = page.locator('input');
|
||||
await expect(locator).toBeEmpty();
|
||||
});
|
||||
@ -128,7 +128,7 @@ test('should support toBeVisible, toBeHidden', async ({ runInlineTest }) => {
|
||||
expect(result.exitCode).toBe(0);
|
||||
});
|
||||
|
||||
test('should support toBeFocused', async ({ runInlineTest }) => {
|
||||
test('should support toBeFocused, toBeSelected', async ({ runInlineTest }) => {
|
||||
const result = await runInlineTest({
|
||||
'a.test.ts': `
|
||||
const { test } = pwt;
|
||||
@ -139,8 +139,23 @@ test('should support toBeFocused', async ({ runInlineTest }) => {
|
||||
await locator.focus();
|
||||
await expect(locator).toBeFocused({ timeout: 1000 });
|
||||
});
|
||||
|
||||
test('selected', async ({ page }) => {
|
||||
await page.setContent('<select><option>One</option></select>');
|
||||
const locator = page.locator('option');
|
||||
await expect(locator).toBeSelected();
|
||||
});
|
||||
|
||||
test('fail on strict option', async ({ page }) => {
|
||||
await page.setContent('<select><option>One</option><option>Two</option></select>');
|
||||
const locator = page.locator('option');
|
||||
await expect(locator).toBeSelected();
|
||||
});
|
||||
`,
|
||||
}, { workers: 1 });
|
||||
expect(result.passed).toBe(1);
|
||||
expect(result.exitCode).toBe(0);
|
||||
const output = stripAscii(result.output);
|
||||
expect(output).toContain('strict mode violation');
|
||||
expect(result.passed).toBe(2);
|
||||
expect(result.failed).toBe(1);
|
||||
expect(result.exitCode).toBe(1);
|
||||
});
|
||||
|
17
types/testExpect.d.ts
vendored
17
types/testExpect.d.ts
vendored
@ -143,7 +143,22 @@ declare global {
|
||||
* Asserts given DOM is a focused (active) in document.
|
||||
*/
|
||||
toBeFocused(options?: { timeout?: number }): Promise<R>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts given select option is selected
|
||||
*/
|
||||
toBeSelected(options?: { timeout?: number }): Promise<R>;
|
||||
|
||||
/**
|
||||
* Asserts JavaScript object that corresponds to the Node has a property with given value.
|
||||
*/
|
||||
toHaveProp(name: string, value: any, options?: { timeout?: number }): Promise<R>;
|
||||
|
||||
/**
|
||||
* Asserts that DOM node has a given CSS class.
|
||||
*/
|
||||
toHaveClass(className: string, options?: { timeout?: number }): Promise<R>;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user