mirror of
https://github.com/hanzoai/playwright.git
synced 2026-09-24 17:01:53 +00:00
chore(test-runner): vendor the expect library (#40301)
This commit is contained in:
Generated
+3
-76
@@ -3963,22 +3963,6 @@
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
},
|
||||
"node_modules/ci-info": {
|
||||
"version": "4.4.0",
|
||||
"resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz",
|
||||
"integrity": "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/sibiraj-s"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/cliui": {
|
||||
"version": "7.0.4",
|
||||
"resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz",
|
||||
@@ -5161,24 +5145,6 @@
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/expect": {
|
||||
"version": "30.2.0",
|
||||
"resolved": "https://registry.npmjs.org/expect/-/expect-30.2.0.tgz",
|
||||
"integrity": "sha512-u/feCi0GPsI+988gU2FLcsHyAHTU0MX1Wg68NhAnN7z/+C5wqG+CY8J53N9ioe8RXgaoz0nBR/TYMf3AycUuPw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jest/expect-utils": "30.2.0",
|
||||
"@jest/get-type": "30.1.0",
|
||||
"jest-matcher-utils": "30.2.0",
|
||||
"jest-message-util": "30.2.0",
|
||||
"jest-mock": "30.2.0",
|
||||
"jest-util": "30.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/express": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz",
|
||||
@@ -6591,21 +6557,6 @@
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/jest-mock": {
|
||||
"version": "30.2.0",
|
||||
"resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.2.0.tgz",
|
||||
"integrity": "sha512-JNNNl2rj4b5ICpmAcq+WbLH83XswjPbjH4T7yvGzfAGCPh1rw+xVNbtk+FnRslvt9lkCcdn9i1oAoKUuFsOxRw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jest/types": "30.2.0",
|
||||
"@types/node": "*",
|
||||
"jest-util": "30.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/jest-regex-util": {
|
||||
"version": "30.0.1",
|
||||
"resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.0.1.tgz",
|
||||
@@ -6616,31 +6567,6 @@
|
||||
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/jest-util": {
|
||||
"version": "30.2.0",
|
||||
"resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.2.0.tgz",
|
||||
"integrity": "sha512-QKNsM0o3Xe6ISQU869e+DhG+4CK/48aHYdJZGlFQVTjnbvgpcKyxpzk29fGiO7i/J8VENZ+d2iGnSsvmuHywlA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jest/types": "30.2.0",
|
||||
"@types/node": "*",
|
||||
"chalk": "^4.1.2",
|
||||
"ci-info": "^4.2.0",
|
||||
"graceful-fs": "^4.2.11",
|
||||
"picomatch": "^4.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/jest-util/node_modules/graceful-fs": {
|
||||
"version": "4.2.11",
|
||||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
|
||||
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/jose": {
|
||||
"version": "6.1.3",
|
||||
"resolved": "https://registry.npmjs.org/jose/-/jose-6.1.3.tgz",
|
||||
@@ -9726,12 +9652,13 @@
|
||||
"@babel/plugin-transform-private-property-in-object": "^7.27.1",
|
||||
"@babel/plugin-transform-react-jsx": "^7.27.1",
|
||||
"@babel/preset-typescript": "^7.27.1",
|
||||
"@jest/expect-utils": "30.2.0",
|
||||
"@types/babel__code-frame": "^7.0.6",
|
||||
"@types/babel__core": "^7.20.5",
|
||||
"@types/babel__helper-plugin-utils": "^7.10.3",
|
||||
"@types/babel__traverse": "^7.20.6",
|
||||
"expect": "30.2.0",
|
||||
"jest-matcher-utils": "30.2.0"
|
||||
"jest-matcher-utils": "30.2.0",
|
||||
"jest-message-util": "30.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
|
||||
@@ -83,8 +83,9 @@
|
||||
"@types/babel__core": "^7.20.5",
|
||||
"@types/babel__helper-plugin-utils": "^7.10.3",
|
||||
"@types/babel__traverse": "^7.20.6",
|
||||
"expect": "30.2.0",
|
||||
"jest-matcher-utils": "30.2.0"
|
||||
"@jest/expect-utils": "30.2.0",
|
||||
"jest-matcher-utils": "30.2.0",
|
||||
"jest-message-util": "30.2.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "2.3.2"
|
||||
|
||||
@@ -2,4 +2,8 @@
|
||||
@isomorphic/**
|
||||
@utils/**
|
||||
node_modules/colors/safe
|
||||
node_modules/expect
|
||||
|
||||
[expectLibrary.ts]
|
||||
node_modules/@jest/expect-utils
|
||||
node_modules/jest-matcher-utils
|
||||
node_modules/jest-message-util
|
||||
|
||||
@@ -16,15 +16,38 @@
|
||||
|
||||
import path from 'path';
|
||||
|
||||
import {
|
||||
expect as expectLibrary,
|
||||
} from 'expect';
|
||||
import { parseStackFrame, captureRawStack } from '@isomorphic/stackTrace';
|
||||
import { escapeWithQuotes, isString } from '@isomorphic/stringUtils';
|
||||
import { pollAgainstDeadline } from '@isomorphic/timeoutRunner';
|
||||
import { currentZone } from '@utils/zones';
|
||||
|
||||
import { ExpectError, isJestError } from './matcherHint';
|
||||
import {
|
||||
any,
|
||||
anything,
|
||||
arrayContaining,
|
||||
arrayNotContaining,
|
||||
arrayOf,
|
||||
buildCustomAsymmetricMatcher,
|
||||
closeTo,
|
||||
createThrowMatcher,
|
||||
getMessage,
|
||||
validateMatcherResult,
|
||||
isPromise,
|
||||
matchers as expectMatchers,
|
||||
notArrayOf,
|
||||
notCloseTo,
|
||||
objectContaining,
|
||||
objectNotContaining,
|
||||
stringContaining,
|
||||
stringMatching,
|
||||
stringNotContaining,
|
||||
stringNotMatching,
|
||||
utils,
|
||||
createExpectedPromiseMessage,
|
||||
createExpectedToResolveMessage,
|
||||
createExpectedToRejectMessage,
|
||||
} from './expectLibrary';
|
||||
import { ExpectError } from './matcherHint';
|
||||
import {
|
||||
computeMatcherTitleSuffix,
|
||||
deadlineForMatcher,
|
||||
@@ -61,17 +84,20 @@ import {
|
||||
import { toMatchAriaSnapshot } from './toMatchAriaSnapshot';
|
||||
import { toHaveScreenshot, toMatchSnapshot } from './toMatchSnapshot';
|
||||
|
||||
import type { MatcherContext, MatchersObject, RawMatcherFn } from './expectLibrary';
|
||||
import type { MatcherAttachment, MatcherResult } from './matcherHint';
|
||||
import type { ExpectMatcherStateInternal } from './matchers';
|
||||
import type { Expect } from '../../types/test';
|
||||
import type { StackFrame } from '@protocol/channels';
|
||||
|
||||
export interface ExpectStepInfo {
|
||||
_attachToStep(attachment: { name: string; contentType: string; path?: string; body?: string | Buffer }): void;
|
||||
}
|
||||
|
||||
export interface ExpectStep {
|
||||
complete(result: { error?: Error | unknown, suggestedRebaseline?: string }): void;
|
||||
info: ExpectStepInfo;
|
||||
interface ExpectStep {
|
||||
complete(result: {
|
||||
error?: Error | unknown,
|
||||
softError?: Error | unknown,
|
||||
shouldNotRetryTest?: boolean,
|
||||
suggestedRebaseline?: string,
|
||||
attachments?: MatcherAttachment[],
|
||||
}): void;
|
||||
}
|
||||
|
||||
export interface ExpectTestInfo {
|
||||
@@ -81,10 +107,8 @@ export interface ExpectTestInfo {
|
||||
title: string;
|
||||
shortTitle: string;
|
||||
params?: Record<string, any>;
|
||||
infectParentStepsWithError?: boolean;
|
||||
}): ExpectStep;
|
||||
_deadline(): { deadline: number; timeout: number };
|
||||
_failWithError(error: Error | unknown, shouldNotRetry?: 'shouldNotRetry'): void;
|
||||
_resolveSnapshotPaths(kind: 'snapshot' | 'screenshot' | 'aria', name: string | string[] | undefined, updateSnapshotIndex: 'updateSnapshotIndex' | 'dontUpdateSnapshotIndex', anonymousExtension?: string): { absoluteSnapshotPath: string; relativeOutputPath: string };
|
||||
_getOutputPath(...pathSegments: string[]): string;
|
||||
}
|
||||
@@ -134,141 +158,26 @@ export function expectConfig(): ExpectConfig {
|
||||
|
||||
type ExpectMessage = string | { message?: string };
|
||||
|
||||
function createMatchers(actual: unknown, info: ExpectMetaInfo, prefix: string[]): any {
|
||||
return new Proxy(expectLibrary(actual), new ExpectMetaInfoProxyHandler(actual, info, prefix));
|
||||
}
|
||||
|
||||
const userMatchersSymbol = Symbol('userMatchers');
|
||||
|
||||
function qualifiedMatcherName(qualifier: string[], matcherName: string) {
|
||||
return qualifier.join(':') + '$' + matcherName;
|
||||
}
|
||||
|
||||
let lastExtendId = 0;
|
||||
|
||||
function createExpect(info: ExpectMetaInfo, prefix: string[], userMatchers: Record<string, Function>) {
|
||||
const expectInstance: Expect<{}> = new Proxy(expectLibrary, {
|
||||
apply: function(target: any, thisArg: any, argumentsList: [unknown, ExpectMessage?]) {
|
||||
const [actual, messageOrOptions] = argumentsList;
|
||||
const message = isString(messageOrOptions) ? messageOrOptions : messageOrOptions?.message || info.message;
|
||||
const newInfo = { ...info, message };
|
||||
if (newInfo.poll) {
|
||||
if (typeof actual !== 'function')
|
||||
throw new Error('`expect.poll()` accepts only function as a first argument');
|
||||
newInfo.poll.generator = actual as any;
|
||||
}
|
||||
return createMatchers(actual, newInfo, prefix);
|
||||
},
|
||||
|
||||
get: function(target: any, property: string | typeof userMatchersSymbol) {
|
||||
if (property === 'configure')
|
||||
return configure;
|
||||
|
||||
if (property === 'extend') {
|
||||
return (matchers: any) => {
|
||||
const qualifier = [...prefix, String(++lastExtendId)];
|
||||
|
||||
const wrappedMatchers: any = {};
|
||||
for (const [name, matcher] of Object.entries(matchers)) {
|
||||
wrappedMatchers[name] = wrapPlaywrightMatcherToPassNiceThis(matcher);
|
||||
const key = qualifiedMatcherName(qualifier, name);
|
||||
wrappedMatchers[key] = wrappedMatchers[name];
|
||||
Object.defineProperty(wrappedMatchers[key], 'name', { value: name });
|
||||
}
|
||||
expectLibrary.extend(wrappedMatchers);
|
||||
return createExpect(info, qualifier, { ...userMatchers, ...matchers });
|
||||
};
|
||||
}
|
||||
|
||||
if (property === 'soft') {
|
||||
return (actual: unknown, messageOrOptions?: ExpectMessage) => {
|
||||
return configure({ soft: true })(actual, messageOrOptions) as any;
|
||||
};
|
||||
}
|
||||
|
||||
if (property === userMatchersSymbol)
|
||||
return userMatchers;
|
||||
|
||||
if (property === 'poll') {
|
||||
return (actual: unknown, messageOrOptions?: ExpectMessage & { timeout?: number, intervals?: number[] }) => {
|
||||
const poll = isString(messageOrOptions) ? {} : messageOrOptions || {};
|
||||
return configure({ _poll: poll })(actual, messageOrOptions) as any;
|
||||
};
|
||||
}
|
||||
return (expectLibrary as any)[property];
|
||||
},
|
||||
});
|
||||
|
||||
const configure = (configuration: { message?: string, timeout?: number, soft?: boolean, _poll?: boolean | { timeout?: number, intervals?: number[] } }) => {
|
||||
const newInfo = { ...info };
|
||||
if ('message' in configuration)
|
||||
newInfo.message = configuration.message;
|
||||
if ('timeout' in configuration)
|
||||
newInfo.timeout = configuration.timeout;
|
||||
if ('soft' in configuration)
|
||||
newInfo.isSoft = configuration.soft;
|
||||
if ('_poll' in configuration) {
|
||||
newInfo.poll = configuration._poll ? { ...info.poll, generator: () => {} } : undefined;
|
||||
if (typeof configuration._poll === 'object') {
|
||||
newInfo.poll!.timeout = configuration._poll.timeout ?? newInfo.poll!.timeout;
|
||||
newInfo.poll!.intervals = configuration._poll.intervals ?? newInfo.poll!.intervals;
|
||||
}
|
||||
}
|
||||
return createExpect(newInfo, prefix, userMatchers);
|
||||
type ExpectMetaInfo = {
|
||||
message?: string;
|
||||
isNot?: boolean;
|
||||
isSoft?: boolean;
|
||||
poll?: {
|
||||
timeout?: number;
|
||||
intervals?: number[];
|
||||
};
|
||||
|
||||
return expectInstance;
|
||||
}
|
||||
|
||||
// Expect wraps matchers, so there is no way to pass this information to the raw Playwright matcher.
|
||||
// Rely on sync call sequence to seed each matcher call with the context.
|
||||
type MatcherCallContext = {
|
||||
expectInfo: ExpectMetaInfo;
|
||||
testInfo: ExpectTestInfo | null;
|
||||
step?: ExpectStepInfo;
|
||||
timeout?: number;
|
||||
userMatchers: MatchersObject;
|
||||
};
|
||||
|
||||
let matcherCallContext: MatcherCallContext | undefined;
|
||||
|
||||
function setMatcherCallContext(context: MatcherCallContext) {
|
||||
matcherCallContext = context;
|
||||
}
|
||||
|
||||
function takeMatcherCallContext(): MatcherCallContext | undefined {
|
||||
try {
|
||||
return matcherCallContext;
|
||||
} finally {
|
||||
// Any subsequent matcher following the first is assumed to be an unsupported legacy asymmetric matcher.
|
||||
// Lacking call context in these scenarios is not particularly important.
|
||||
matcherCallContext = undefined;
|
||||
}
|
||||
}
|
||||
const META_INFO = Symbol('expectMetaInfo');
|
||||
|
||||
const defaultExpectTimeout = 5000;
|
||||
|
||||
function wrapPlaywrightMatcherToPassNiceThis(matcher: any) {
|
||||
return function(this: any, ...args: any[]) {
|
||||
const { isNot, promise, utils } = this;
|
||||
const context = takeMatcherCallContext();
|
||||
const timeout = context?.expectInfo.timeout ?? expectConfig().timeout ?? defaultExpectTimeout;
|
||||
const newThis: ExpectMatcherStateInternal = {
|
||||
isNot,
|
||||
promise,
|
||||
utils,
|
||||
timeout,
|
||||
_stepInfo: context?.step,
|
||||
};
|
||||
(newThis as any).equals = throwUnsupportedExpectMatcherError;
|
||||
return matcher.call(newThis, ...args);
|
||||
};
|
||||
}
|
||||
|
||||
function throwUnsupportedExpectMatcherError() {
|
||||
throw new Error('It looks like you are using custom expect matchers that are not compatible with Playwright. See https://aka.ms/playwright/expect-compatibility');
|
||||
}
|
||||
|
||||
expectLibrary.setState({ expand: false });
|
||||
|
||||
const customAsyncMatchers = {
|
||||
toBeAttached,
|
||||
toBeChecked,
|
||||
@@ -303,160 +212,238 @@ const customAsyncMatchers = {
|
||||
toPass,
|
||||
};
|
||||
|
||||
const customMatchers = {
|
||||
const allBuiltinMatchers: MatchersObject = {
|
||||
...expectMatchers,
|
||||
toThrow: createThrowMatcher('toThrow'),
|
||||
toThrowError: createThrowMatcher('toThrowError'),
|
||||
...customAsyncMatchers,
|
||||
toMatchSnapshot,
|
||||
} as any;
|
||||
|
||||
const promiseThrowMatchers: MatchersObject = {
|
||||
toThrow: createThrowMatcher('toThrow', true),
|
||||
toThrowError: createThrowMatcher('toThrowError', true),
|
||||
};
|
||||
|
||||
type Generator = () => any;
|
||||
function createExpect(info: ExpectMetaInfo): Expect<{}> {
|
||||
const expectFn: any = (actual: unknown, messageOrOptions?: ExpectMessage) => createMatchers(actual, info, messageOrOptions);
|
||||
Object.defineProperty(expectFn, META_INFO, { value: info });
|
||||
|
||||
type ExpectMetaInfo = {
|
||||
message?: string;
|
||||
isNot?: boolean;
|
||||
isSoft?: boolean;
|
||||
poll?: {
|
||||
timeout?: number;
|
||||
intervals?: number[];
|
||||
generator: Generator;
|
||||
expectFn.any = any;
|
||||
expectFn.anything = anything;
|
||||
expectFn.arrayContaining = arrayContaining;
|
||||
expectFn.arrayOf = arrayOf;
|
||||
expectFn.closeTo = closeTo;
|
||||
expectFn.objectContaining = objectContaining;
|
||||
expectFn.stringContaining = stringContaining;
|
||||
expectFn.stringMatching = stringMatching;
|
||||
|
||||
const notAsymmetric: any = {
|
||||
arrayContaining: arrayNotContaining,
|
||||
arrayOf: notArrayOf,
|
||||
closeTo: notCloseTo,
|
||||
objectContaining: objectNotContaining,
|
||||
stringContaining: stringNotContaining,
|
||||
stringMatching: stringNotMatching,
|
||||
};
|
||||
timeout?: number;
|
||||
};
|
||||
expectFn.not = notAsymmetric;
|
||||
|
||||
class ExpectMetaInfoProxyHandler implements ProxyHandler<any> {
|
||||
private _actual: any;
|
||||
private _info: ExpectMetaInfo;
|
||||
private _prefix: string[];
|
||||
|
||||
constructor(actual: any, info: ExpectMetaInfo, prefix: string[]) {
|
||||
this._actual = actual;
|
||||
this._info = { ...info };
|
||||
this._prefix = prefix;
|
||||
for (const [name, matcher] of Object.entries(info.userMatchers)) {
|
||||
const { positive, inverse } = buildCustomAsymmetricMatcher(name, matcher);
|
||||
expectFn[name] = positive;
|
||||
notAsymmetric[name] = inverse;
|
||||
}
|
||||
|
||||
get(target: Object, matcherName: string | symbol, receiver: any): any {
|
||||
if (matcherName === 'toThrowError')
|
||||
matcherName = 'toThrow';
|
||||
let matcher = Reflect.get(target, matcherName, receiver);
|
||||
if (typeof matcherName !== 'string')
|
||||
return matcher;
|
||||
expectFn.getState = () => ({});
|
||||
|
||||
let resolvedMatcherName = matcherName;
|
||||
for (let i = this._prefix.length; i > 0; i--) {
|
||||
const qualifiedName = qualifiedMatcherName(this._prefix.slice(0, i), matcherName);
|
||||
if (Reflect.has(target, qualifiedName)) {
|
||||
matcher = Reflect.get(target, qualifiedName, receiver);
|
||||
resolvedMatcherName = qualifiedName;
|
||||
break;
|
||||
}
|
||||
expectFn.configure = (configuration: { message?: string, timeout?: number, soft?: boolean }) => {
|
||||
const newInfo: ExpectMetaInfo = { ...info };
|
||||
if ('message' in configuration)
|
||||
newInfo.message = configuration.message;
|
||||
if ('timeout' in configuration)
|
||||
newInfo.timeout = configuration.timeout;
|
||||
if ('soft' in configuration)
|
||||
newInfo.isSoft = configuration.soft;
|
||||
return createExpect(newInfo);
|
||||
};
|
||||
|
||||
expectFn.soft = (actual: unknown, messageOrOptions?: ExpectMessage) => {
|
||||
return createMatchers(actual, { ... info, isSoft: true }, messageOrOptions);
|
||||
};
|
||||
|
||||
expectFn.poll = (actual: unknown, messageOrOptions?: ExpectMessage & { timeout?: number, intervals?: number[] }) => {
|
||||
const poll = isString(messageOrOptions) ? {} : messageOrOptions || {};
|
||||
return createMatchers(actual, { ...info, poll: { timeout: poll.timeout, intervals: poll.intervals } }, messageOrOptions);
|
||||
};
|
||||
|
||||
expectFn.extend = (matchers: MatchersObject) => {
|
||||
for (const [name, m] of Object.entries(matchers)) {
|
||||
if (typeof m !== 'function')
|
||||
throw new TypeError(`expect.extend: \`${name}\` is not a valid matcher. Must be a function, is "${typeof m}"`);
|
||||
}
|
||||
|
||||
if (matcher === undefined)
|
||||
throw new Error(`expect: Property '${matcherName}' not found.`);
|
||||
if (typeof matcher !== 'function') {
|
||||
if (matcherName === 'not')
|
||||
this._info.isNot = !this._info.isNot;
|
||||
return new Proxy(matcher, this);
|
||||
// Legacy behavior: `expect.extend({...})` without capturing the return value
|
||||
// must make the new matchers available on the same expect instance.
|
||||
Object.assign(info.userMatchers, matchers);
|
||||
for (const [name, matcher] of Object.entries(matchers)) {
|
||||
const { positive, inverse } = buildCustomAsymmetricMatcher(name, matcher);
|
||||
expectFn[name] = positive;
|
||||
notAsymmetric[name] = inverse;
|
||||
}
|
||||
if (this._info.poll) {
|
||||
if ((customAsyncMatchers as any)[matcherName] || matcherName === 'resolves' || matcherName === 'rejects')
|
||||
throw new Error(`\`expect.poll()\` does not support "${matcherName}" matcher.`);
|
||||
matcher = (...args: any[]) => pollMatcher(resolvedMatcherName, this._info, this._prefix, ...args);
|
||||
// End of legacy behavior.
|
||||
|
||||
return createExpect({
|
||||
...info,
|
||||
userMatchers: { ...info.userMatchers, ...matchers },
|
||||
});
|
||||
};
|
||||
|
||||
return expectFn as Expect<{}>;
|
||||
}
|
||||
|
||||
function createMatchers(actual: unknown, originalInfo: ExpectMetaInfo, messageOrOptions?: ExpectMessage): any {
|
||||
const message = isString(messageOrOptions) ? messageOrOptions : messageOrOptions?.message || originalInfo.message;
|
||||
const info = { ...originalInfo, message };
|
||||
const result: any = { not: {}, resolves: { not: {} }, rejects: { not: {} } };
|
||||
const notInfo: ExpectMetaInfo = { ...info, isNot: !info.isNot };
|
||||
for (const [name, matcher] of Object.entries({ ...allBuiltinMatchers, ...info.userMatchers })) {
|
||||
result[name] = createMatcher(name, info, actual, matcher);
|
||||
result.not[name] = createMatcher(name, notInfo, actual, matcher);
|
||||
const promiseMatcher = promiseThrowMatchers[name] ?? matcher;
|
||||
result.resolves[name] = createMatcher(name, info, actual, promiseMatcher, 'resolves');
|
||||
result.resolves.not[name] = createMatcher(name, notInfo, actual, promiseMatcher, 'resolves');
|
||||
result.rejects[name] = createMatcher(name, info, actual, promiseMatcher, 'rejects');
|
||||
result.rejects.not[name] = createMatcher(name, notInfo, actual, promiseMatcher, 'rejects');
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function createMatcher(matcherName: string, info: ExpectMetaInfo, actual: unknown, matcher: RawMatcherFn, promise?: 'resolves' | 'rejects') {
|
||||
return (...args: any[]) => callMatcherAsStep(matcherName, info, actual, matcher, args, promise);
|
||||
}
|
||||
|
||||
function callMatcherAsStep(matcherName: string, info: ExpectMetaInfo, actual: unknown, matcher: RawMatcherFn, args: any[], promise?: 'resolves' | 'rejects') {
|
||||
const testInfo = expectConfig().testInfo;
|
||||
const customMessage = info.message || '';
|
||||
const suffixes = computeMatcherTitleSuffix(matcherName, actual, args);
|
||||
const defaultTitle = `${info.poll ? 'poll ' : ''}${info.isSoft ? 'soft ' : ''}${info.isNot ? 'not ' : ''}${matcherName}${suffixes.short || ''}`;
|
||||
const shortTitle = customMessage || `Expect ${escapeWithQuotes(defaultTitle, '"')}`;
|
||||
const longTitle = shortTitle + (suffixes.long || '');
|
||||
const apiName = `expect${info.poll ? '.poll ' : ''}${info.isSoft ? '.soft ' : ''}${info.isNot ? '.not' : ''}.${matcherName}${suffixes.short || ''}`;
|
||||
|
||||
// This looks like it is unnecessary, but it isn't - we need to filter
|
||||
// out all the frames that belong to the test runner from caught runtime errors.
|
||||
const stackFrames = expectConfig().filteredStackTrace(captureRawStack());
|
||||
const stepData = {
|
||||
category: 'expect' as const,
|
||||
apiName,
|
||||
title: longTitle,
|
||||
shortTitle,
|
||||
params: args[0] ? { expected: args[0] } : undefined,
|
||||
};
|
||||
const step = testInfo?._addStep(stepData);
|
||||
|
||||
const handleError = (error: Error, result?: MatcherResult) => {
|
||||
if (info.isSoft && step) {
|
||||
step.complete({ ...result, error, softError: error });
|
||||
} else {
|
||||
step?.complete({ ...result, error });
|
||||
throw error;
|
||||
}
|
||||
return (...args: any[]) => {
|
||||
const testInfo = expectConfig().testInfo;
|
||||
setMatcherCallContext({ expectInfo: this._info, testInfo });
|
||||
if (!testInfo)
|
||||
return matcher.call(target, ...args);
|
||||
};
|
||||
|
||||
const customMessage = this._info.message || '';
|
||||
const suffixes = computeMatcherTitleSuffix(matcherName, this._actual, args);
|
||||
const defaultTitle = `${this._info.poll ? 'poll ' : ''}${this._info.isSoft ? 'soft ' : ''}${this._info.isNot ? 'not ' : ''}${matcherName}${suffixes.short || ''}`;
|
||||
const shortTitle = customMessage || `Expect ${escapeWithQuotes(defaultTitle, '"')}`;
|
||||
const longTitle = shortTitle + (suffixes.long || '');
|
||||
const apiName = `expect${this._info.poll ? '.poll ' : ''}${this._info.isSoft ? '.soft ' : ''}${this._info.isNot ? '.not' : ''}.${matcherName}${suffixes.short || ''}`;
|
||||
const finalizer = (result: MatcherResult) => {
|
||||
validateMatcherResult(result);
|
||||
if (result.pass === !!info.isNot) {
|
||||
const error = new ExpectError({ ...result, name: matcherName, message: getMessage(result.message) }, customMessage, stackFrames);
|
||||
handleError(error, result);
|
||||
} else {
|
||||
step?.complete(result);
|
||||
}
|
||||
};
|
||||
|
||||
// This looks like it is unnecessary, but it isn't - we need to filter
|
||||
// out all the frames that belong to the test runner from caught runtime errors.
|
||||
const stackFrames = expectConfig().filteredStackTrace(captureRawStack());
|
||||
|
||||
// toPass and poll matchers can contain other steps, expects and API calls,
|
||||
// so they behave like a retriable step.
|
||||
const stepInfo = {
|
||||
category: 'expect' as const,
|
||||
apiName,
|
||||
title: longTitle,
|
||||
shortTitle,
|
||||
params: args[0] ? { expected: args[0] } : undefined,
|
||||
infectParentStepsWithError: this._info.isSoft,
|
||||
};
|
||||
|
||||
const step = testInfo._addStep(stepInfo);
|
||||
|
||||
const reportStepError = (e: Error | unknown) => {
|
||||
const jestError = isJestError(e) ? e : null;
|
||||
const expectError = jestError ? new ExpectError(jestError, customMessage, stackFrames) : undefined;
|
||||
if (jestError?.matcherResult.suggestedRebaseline) {
|
||||
// NOTE: this is a workaround for the fact that we can't pass the suggested rebaseline
|
||||
// for passing matchers. See toMatchAriaSnapshot for a counterpart.
|
||||
step.complete({ suggestedRebaseline: jestError?.matcherResult.suggestedRebaseline });
|
||||
return;
|
||||
}
|
||||
|
||||
const error = expectError ?? e;
|
||||
step.complete({ error });
|
||||
|
||||
if (this._info.isSoft)
|
||||
testInfo._failWithError(error);
|
||||
else
|
||||
throw error;
|
||||
};
|
||||
|
||||
const finalizer = () => {
|
||||
step.complete({});
|
||||
};
|
||||
|
||||
try {
|
||||
setMatcherCallContext({ expectInfo: this._info, testInfo, step: step.info });
|
||||
const callback = () => matcher.call(target, ...args);
|
||||
const result = currentZone().with('stepZone', step).run(callback);
|
||||
if (result instanceof Promise)
|
||||
return result.then(finalizer).catch(reportStepError);
|
||||
finalizer();
|
||||
return result;
|
||||
} catch (e) {
|
||||
void reportStepError(e);
|
||||
}
|
||||
};
|
||||
try {
|
||||
const invoke = () => info.poll
|
||||
? invokePollMatcher(matcherName, info, matcher, actual, args, promise)
|
||||
: invokeMatcher(matcherName, info, matcher, actual, args, promise);
|
||||
const result = step ? currentZone().with('stepZone', step).run(invoke) : invoke();
|
||||
if (result instanceof Promise)
|
||||
return result.then(finalizer, handleError);
|
||||
finalizer(result);
|
||||
} catch (error) {
|
||||
handleError(error);
|
||||
}
|
||||
}
|
||||
|
||||
async function pollMatcher(qualifiedMatcherName: string, info: ExpectMetaInfo, prefix: string[], ...args: any[]) {
|
||||
function invokeMatcher(
|
||||
matcherName: string,
|
||||
info: ExpectMetaInfo,
|
||||
matcher: RawMatcherFn,
|
||||
actual: unknown,
|
||||
args: any[],
|
||||
promise: 'resolves' | 'rejects' | undefined,
|
||||
): MatcherResult | Promise<MatcherResult> {
|
||||
const isNot = !!info.isNot;
|
||||
const timeout = info.timeout ?? expectConfig().timeout ?? defaultExpectTimeout;
|
||||
const matcherContext: MatcherContext & ExpectMatcherStateInternal = {
|
||||
customTesters: [],
|
||||
isNot,
|
||||
promise: promise ?? '',
|
||||
utils,
|
||||
timeout,
|
||||
equals: throwUnsupportedExpectMatcherError as any,
|
||||
};
|
||||
|
||||
if (!promise)
|
||||
return matcher.call(matcherContext, actual, ...args) as MatcherResult | Promise<MatcherResult>;
|
||||
|
||||
if (typeof actual === 'function')
|
||||
actual = actual();
|
||||
if (!isPromise(actual))
|
||||
return { pass: false, message: createExpectedPromiseMessage(matcherName, isNot, promise, actual) };
|
||||
|
||||
if (promise === 'resolves') {
|
||||
return actual.then(
|
||||
result => matcher.call(matcherContext, result, ...args) as MatcherResult | Promise<MatcherResult>,
|
||||
error => ({ pass: false, message: createExpectedToResolveMessage(matcherName, isNot, promise, error) }),
|
||||
);
|
||||
} else {
|
||||
return actual.then(
|
||||
result => ({ pass: false, message: createExpectedToRejectMessage(matcherName, isNot, promise, result) }),
|
||||
error => matcher.call(matcherContext, error, ...args) as MatcherResult | Promise<MatcherResult>,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function invokePollMatcher(
|
||||
matcherName: string,
|
||||
info: ExpectMetaInfo,
|
||||
matcher: RawMatcherFn,
|
||||
actual: unknown,
|
||||
args: any[],
|
||||
promise: 'resolves' | 'rejects' | undefined,
|
||||
): Promise<MatcherResult> {
|
||||
if (typeof actual !== 'function')
|
||||
throw new Error('`expect.poll()` accepts only function as a first argument');
|
||||
if (promise || (customAsyncMatchers as any)[matcherName])
|
||||
throw new Error(`\`expect.poll()\` does not support "${promise ?? matcherName}" matcher.`);
|
||||
|
||||
const testInfo = expectConfig().testInfo;
|
||||
const poll = info.poll!;
|
||||
const timeout = poll.timeout ?? info.timeout ?? expectConfig().timeout ?? defaultExpectTimeout;
|
||||
const { deadline, timeoutMessage } = deadlineForMatcher(testInfo, timeout);
|
||||
|
||||
const result = await pollAgainstDeadline<Error|undefined>(async () => {
|
||||
const result = await pollAgainstDeadline<Error | undefined>(async () => {
|
||||
if (testInfo && expectConfig().testInfo !== testInfo)
|
||||
return { continuePolling: false, result: undefined };
|
||||
|
||||
const innerInfo: ExpectMetaInfo = {
|
||||
...info,
|
||||
isSoft: false, // soft is outside of poll, not inside
|
||||
poll: undefined,
|
||||
};
|
||||
const value = await poll.generator();
|
||||
const value = await actual();
|
||||
try {
|
||||
let matchers = createMatchers(value, innerInfo, prefix);
|
||||
if (info.isNot)
|
||||
matchers = matchers.not;
|
||||
matchers[qualifiedMatcherName](...args);
|
||||
await callMatcherAsStep(matcherName, { ...info, poll: undefined, isSoft: false }, value, matcher, args);
|
||||
return { continuePolling: false, result: undefined };
|
||||
} catch (error) {
|
||||
return { continuePolling: true, result: error };
|
||||
}
|
||||
}, deadline, poll.intervals ?? [100, 250, 500, 1000]);
|
||||
|
||||
if (result.timedOut) {
|
||||
const message = result.result ? [
|
||||
result.result.message,
|
||||
@@ -464,20 +451,20 @@ async function pollMatcher(qualifiedMatcherName: string, info: ExpectMetaInfo, p
|
||||
`Call Log:`,
|
||||
`- ${timeoutMessage}`,
|
||||
].join('\n') : timeoutMessage;
|
||||
|
||||
throw new Error(message);
|
||||
return { pass: !!info.isNot, message: () => message };
|
||||
}
|
||||
return { pass: !info.isNot, message: () => '' };
|
||||
}
|
||||
|
||||
export const expect: Expect<{}> = createExpect({}, [], {}).extend(customMatchers as any);
|
||||
export const expect: Expect<{}> = createExpect({ userMatchers: {} });
|
||||
|
||||
export function mergeExpects(...expects: any[]) {
|
||||
let merged = expect;
|
||||
for (const e of expects) {
|
||||
const internals = e[userMatchersSymbol];
|
||||
if (!internals) // non-playwright expects mutate the global expect, so we don't need to do anything special
|
||||
const info: ExpectMetaInfo | undefined = e[META_INFO];
|
||||
if (!info) // non-playwright expects mutate the global expect, so we don't need to do anything special
|
||||
continue;
|
||||
merged = merged.extend(internals);
|
||||
merged = merged.extend(info.userMatchers as any) as typeof merged;
|
||||
}
|
||||
return merged;
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -20,45 +20,41 @@ import { stringifyStackFrames } from '@isomorphic/stackTrace';
|
||||
|
||||
import type { StackFrame } from '@protocol/channels';
|
||||
|
||||
export type MatcherResult<E, A> = {
|
||||
name: string;
|
||||
export type MatcherAttachment = { name: string; contentType: string; path?: string; body?: string | Buffer };
|
||||
|
||||
export type MatcherResult<E = unknown, A = unknown> = {
|
||||
name?: string;
|
||||
expected?: E;
|
||||
message: () => string;
|
||||
pass: boolean;
|
||||
actual?: A;
|
||||
diff?: string;
|
||||
log?: string[];
|
||||
timeout?: number;
|
||||
suggestedRebaseline?: string;
|
||||
attachments?: MatcherAttachment[];
|
||||
softError?: Error | unknown;
|
||||
shouldNotRetryTest?: boolean;
|
||||
};
|
||||
|
||||
export type MatcherResultProperty = Omit<MatcherResult<unknown, unknown>, 'message'> & {
|
||||
export type MatcherResultProperty = Omit<MatcherResult, 'message' | 'name' | 'shouldNotRetryTest'> & {
|
||||
name: string;
|
||||
message: string;
|
||||
};
|
||||
|
||||
type JestError = Error & {
|
||||
matcherResult: MatcherResultProperty;
|
||||
};
|
||||
|
||||
export class ExpectError extends Error {
|
||||
matcherResult: MatcherResultProperty;
|
||||
|
||||
constructor(jestError: JestError, customMessage: string, stackFrames: StackFrame[]) {
|
||||
constructor(matcherResult: MatcherResultProperty, customMessage: string, stackFrames: StackFrame[]) {
|
||||
super('');
|
||||
// Copy to erase the JestMatcherError constructor name from the console.log(error).
|
||||
this.name = jestError.name;
|
||||
this.message = jestError.message;
|
||||
this.matcherResult = jestError.matcherResult;
|
||||
|
||||
this.message = matcherResult.message;
|
||||
this.matcherResult = matcherResult;
|
||||
if (customMessage)
|
||||
this.message = customMessage + '\n\n' + this.message;
|
||||
this.stack = this.name + ': ' + this.message + '\n' + stringifyStackFrames(stackFrames).join('\n');
|
||||
}
|
||||
}
|
||||
|
||||
export function isJestError(e: unknown): e is JestError {
|
||||
return e instanceof Error && 'matcherResult' in e && !!e.matcherResult;
|
||||
}
|
||||
|
||||
export function expectTypes(receiver: any, types: ('APIResponse' | 'Page' | 'Locator')[], matcherName: string) {
|
||||
if (typeof receiver !== 'object' || !types.includes(receiver._apiName)) {
|
||||
const receiverString = typeof receiver === 'object' && receiver !== null ? `${receiver.constructor.name} ${util.inspect(receiver)}` : String(receiver);
|
||||
|
||||
@@ -33,7 +33,7 @@ import { toHaveScreenshotStepTitle } from './toMatchSnapshot';
|
||||
import { expectConfig } from './expect';
|
||||
|
||||
import type { ExpectMatcherState } from '../../types/test';
|
||||
import type { ExpectStepInfo, ExpectTestInfo } from './expect';
|
||||
import type { ExpectTestInfo } from './expect';
|
||||
import type { InternalMatcherUtils } from './matcherHint';
|
||||
import type { APIResponse, Locator, Frame, Page } from 'playwright-core';
|
||||
import type { FrameExpectParams } from 'playwright-core/lib/client/types';
|
||||
@@ -41,7 +41,6 @@ import type { ExpectMatcherUtils } from '../../types/test';
|
||||
import type { URLPattern } from '@isomorphic/urlMatch';
|
||||
|
||||
export type ExpectMatcherStateInternal = Omit<ExpectMatcherState, 'utils'> & {
|
||||
_stepInfo?: ExpectStepInfo;
|
||||
utils: ExpectMatcherUtils & InternalMatcherUtils;
|
||||
};
|
||||
|
||||
|
||||
@@ -137,23 +137,19 @@ export async function toMatchAriaSnapshot(
|
||||
const relativePath = path.relative(process.cwd(), expectedPath);
|
||||
if (updateSnapshots === 'missing') {
|
||||
const message = `A snapshot doesn't exist at ${relativePath}, writing actual.`;
|
||||
testInfo._failWithError(new Error(message), 'shouldNotRetry');
|
||||
} else {
|
||||
const message = `A snapshot is generated at ${relativePath}.`;
|
||||
/* eslint-disable no-console */
|
||||
console.log(message);
|
||||
return { pass: true, message: () => '', name: 'toMatchAriaSnapshot', softError: new Error(message), shouldNotRetryTest: true };
|
||||
}
|
||||
const message = `A snapshot is generated at ${relativePath}.`;
|
||||
/* eslint-disable no-console */
|
||||
console.log(message);
|
||||
return { pass: true, message: () => '', name: 'toMatchAriaSnapshot' };
|
||||
} else {
|
||||
const suggestedRebaseline = `\`\n${escapeTemplateString(indent(typedReceived.regex, '{indent} '))}\n{indent}\``;
|
||||
if (updateSnapshots === 'missing') {
|
||||
const message = 'A snapshot is not provided, generating new baseline.';
|
||||
testInfo._failWithError(new Error(message), 'shouldNotRetry');
|
||||
return { pass: true, message: () => '', name: 'toMatchAriaSnapshot', suggestedRebaseline, softError: new Error(message), shouldNotRetryTest: true };
|
||||
}
|
||||
// TODO: ideally, we should return "pass: true" here because this matcher passes
|
||||
// when regenerating baselines. However, we can only access suggestedRebaseline in case
|
||||
// of an error, so we fail here and workaround it in the expect implementation.
|
||||
return { pass: false, message: () => '', name: 'toMatchAriaSnapshot', suggestedRebaseline };
|
||||
return { pass: true, message: () => '', name: 'toMatchAriaSnapshot', suggestedRebaseline };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,17 +26,15 @@ import { addSuffixToFilePath } from '@utils/fileUtils';
|
||||
import { callLogText, expectTypes, formatMatcherMessage } from './matcherHint';
|
||||
import { expectConfig } from './expect';
|
||||
|
||||
import type { MatcherResult } from './matcherHint';
|
||||
import type { MatcherAttachment, MatcherResult } from './matcherHint';
|
||||
import type { ExpectMatcherStateInternal } from './matchers';
|
||||
import type { ExpectTestInfo, ExpectStepInfo } from './expect';
|
||||
import type { ExpectTestInfo } from './expect';
|
||||
import type { Locator, Page } from 'playwright-core';
|
||||
import type { ExpectScreenshotOptions, Page as PageEx } from 'playwright-core/lib/client/page';
|
||||
import type { Comparator, ImageComparatorOptions } from '@utils/comparators';
|
||||
|
||||
type NameOrSegments = string | string[];
|
||||
|
||||
type ImageMatcherResult = MatcherResult<string, string> & { diff?: string };
|
||||
|
||||
type ToHaveScreenshotConfigOptions = ImageComparatorOptions & {
|
||||
animations?: 'allow' | 'disabled';
|
||||
caret?: 'hide' | 'initial';
|
||||
@@ -151,8 +149,8 @@ class SnapshotHelper {
|
||||
this.kind = this.mimeType.startsWith('image/') ? 'Screenshot' : 'Snapshot';
|
||||
}
|
||||
|
||||
createMatcherResult(message: string, pass: boolean, log?: string[]): ImageMatcherResult {
|
||||
const unfiltered: ImageMatcherResult = {
|
||||
createMatcherResult(message: string, pass: boolean, log?: string[], attachments?: MatcherAttachment[]): MatcherResult<string, string> {
|
||||
const unfiltered: MatcherResult<string, string> = {
|
||||
name: this.matcherName,
|
||||
expected: this.expectedPath,
|
||||
actual: this.actualPath,
|
||||
@@ -160,23 +158,24 @@ class SnapshotHelper {
|
||||
pass,
|
||||
message: () => message,
|
||||
log,
|
||||
attachments,
|
||||
};
|
||||
return Object.fromEntries(Object.entries(unfiltered).filter(([_, v]) => v !== undefined)) as ImageMatcherResult;
|
||||
return Object.fromEntries(Object.entries(unfiltered).filter(([_, v]) => v !== undefined)) as MatcherResult<string, string>;
|
||||
}
|
||||
|
||||
handleMissingNegated(): ImageMatcherResult {
|
||||
handleMissingNegated(): MatcherResult<string, string> {
|
||||
const isWriteMissingMode = this.updateSnapshots !== 'none';
|
||||
const message = `A snapshot doesn't exist at ${this.expectedPath}${isWriteMissingMode ? ', matchers using ".not" won\'t write them automatically.' : '.'}`;
|
||||
// NOTE: 'isNot' matcher implies inversed value.
|
||||
return this.createMatcherResult(message, true);
|
||||
}
|
||||
|
||||
handleDifferentNegated(): ImageMatcherResult {
|
||||
handleDifferentNegated(): MatcherResult<string, string> {
|
||||
// NOTE: 'isNot' matcher implies inversed value.
|
||||
return this.createMatcherResult('', false);
|
||||
}
|
||||
|
||||
handleMatchingNegated(): ImageMatcherResult {
|
||||
handleMatchingNegated(): MatcherResult<string, string> {
|
||||
const message = [
|
||||
colors.red(`${this.kind} comparison failed:`),
|
||||
'',
|
||||
@@ -186,24 +185,28 @@ class SnapshotHelper {
|
||||
return this.createMatcherResult(message, true);
|
||||
}
|
||||
|
||||
handleMissing(actual: Buffer | string, step: ExpectStepInfo | undefined): ImageMatcherResult {
|
||||
handleMissing(actual: Buffer | string): MatcherResult<string, string> {
|
||||
const attachments: MatcherAttachment[] = [];
|
||||
const isWriteMissingMode = this.updateSnapshots !== 'none';
|
||||
if (isWriteMissingMode)
|
||||
writeFileSync(this.expectedPath, actual);
|
||||
step?._attachToStep({ name: addSuffixToFilePath(this.attachmentBaseName, '-expected'), contentType: this.mimeType, path: this.expectedPath });
|
||||
attachments.push({ name: addSuffixToFilePath(this.attachmentBaseName, '-expected'), contentType: this.mimeType, path: this.expectedPath });
|
||||
writeFileSync(this.actualPath, actual);
|
||||
step?._attachToStep({ name: addSuffixToFilePath(this.attachmentBaseName, '-actual'), contentType: this.mimeType, path: this.actualPath });
|
||||
attachments.push({ name: addSuffixToFilePath(this.attachmentBaseName, '-actual'), contentType: this.mimeType, path: this.actualPath });
|
||||
const message = `A snapshot doesn't exist at ${this.expectedPath}${isWriteMissingMode ? ', writing actual.' : '.'}`;
|
||||
if (this.updateSnapshots === 'all' || this.updateSnapshots === 'changed') {
|
||||
/* eslint-disable no-console */
|
||||
console.log(message);
|
||||
return this.createMatcherResult(message, true);
|
||||
return this.createMatcherResult(message, true, undefined, attachments);
|
||||
}
|
||||
if (this.updateSnapshots === 'missing') {
|
||||
this.testInfo._failWithError(new Error(message), 'shouldNotRetry');
|
||||
return this.createMatcherResult('', true);
|
||||
return {
|
||||
...this.createMatcherResult('', true, undefined, attachments),
|
||||
softError: new Error(message),
|
||||
shouldNotRetryTest: true,
|
||||
};
|
||||
}
|
||||
return this.createMatcherResult(message, false);
|
||||
return this.createMatcherResult(message, false, undefined, attachments);
|
||||
}
|
||||
|
||||
handleDifferent(
|
||||
@@ -213,30 +216,30 @@ class SnapshotHelper {
|
||||
diff: Buffer | string | undefined,
|
||||
header: string,
|
||||
diffError: string,
|
||||
log: string[] | undefined,
|
||||
step: ExpectStepInfo | undefined): ImageMatcherResult {
|
||||
log: string[] | undefined): MatcherResult<string, string> {
|
||||
const output = [`${header}${indent(diffError, ' ')}`];
|
||||
if (this.name) {
|
||||
output.push('');
|
||||
output.push(` Snapshot: ${this.name}`);
|
||||
}
|
||||
const attachments: MatcherAttachment[] = [];
|
||||
if (expected !== undefined) {
|
||||
// Copy the expectation inside the `test-results/` folder for backwards compatibility,
|
||||
// so that one can upload `test-results/` directory and have all the data inside.
|
||||
writeFileSync(this.legacyExpectedPath, expected);
|
||||
step?._attachToStep({ name: addSuffixToFilePath(this.attachmentBaseName, '-expected'), contentType: this.mimeType, path: this.expectedPath });
|
||||
attachments.push({ name: addSuffixToFilePath(this.attachmentBaseName, '-expected'), contentType: this.mimeType, path: this.expectedPath });
|
||||
}
|
||||
if (previous !== undefined) {
|
||||
writeFileSync(this.previousPath, previous);
|
||||
step?._attachToStep({ name: addSuffixToFilePath(this.attachmentBaseName, '-previous'), contentType: this.mimeType, path: this.previousPath });
|
||||
attachments.push({ name: addSuffixToFilePath(this.attachmentBaseName, '-previous'), contentType: this.mimeType, path: this.previousPath });
|
||||
}
|
||||
if (actual !== undefined) {
|
||||
writeFileSync(this.actualPath, actual);
|
||||
step?._attachToStep({ name: addSuffixToFilePath(this.attachmentBaseName, '-actual'), contentType: this.mimeType, path: this.actualPath });
|
||||
attachments.push({ name: addSuffixToFilePath(this.attachmentBaseName, '-actual'), contentType: this.mimeType, path: this.actualPath });
|
||||
}
|
||||
if (diff !== undefined) {
|
||||
writeFileSync(this.diffPath, diff);
|
||||
step?._attachToStep({ name: addSuffixToFilePath(this.attachmentBaseName, '-diff'), contentType: this.mimeType, path: this.diffPath });
|
||||
attachments.push({ name: addSuffixToFilePath(this.attachmentBaseName, '-diff'), contentType: this.mimeType, path: this.diffPath });
|
||||
}
|
||||
|
||||
if (log?.length)
|
||||
@@ -244,10 +247,10 @@ class SnapshotHelper {
|
||||
else
|
||||
output.push('');
|
||||
|
||||
return this.createMatcherResult(output.join('\n'), false, log);
|
||||
return this.createMatcherResult(output.join('\n'), false, log, attachments);
|
||||
}
|
||||
|
||||
handleMatching(): ImageMatcherResult {
|
||||
handleMatching(): MatcherResult<string, string> {
|
||||
return this.createMatcherResult('', true);
|
||||
}
|
||||
}
|
||||
@@ -280,7 +283,7 @@ export function toMatchSnapshot(
|
||||
}
|
||||
|
||||
if (!fs.existsSync(helper.expectedPath))
|
||||
return helper.handleMissing(received, this._stepInfo);
|
||||
return helper.handleMissing(received);
|
||||
|
||||
const expected = fs.readFileSync(helper.expectedPath);
|
||||
|
||||
@@ -308,7 +311,7 @@ export function toMatchSnapshot(
|
||||
return helper.handleMatching();
|
||||
|
||||
const header = formatMatcherMessage(this.utils, { promise: this.promise, isNot: this.isNot, matcherName: 'toMatchSnapshot', receiver: isString(received) ? 'string' : 'Buffer', expectation: 'expected' });
|
||||
return helper.handleDifferent(received, expected, undefined, result.diff, header, result.errorMessage, undefined, this._stepInfo);
|
||||
return helper.handleDifferent(received, expected, undefined, result.diff, header, result.errorMessage, undefined);
|
||||
}
|
||||
|
||||
export function toHaveScreenshotStepTitle(
|
||||
@@ -393,11 +396,11 @@ export async function toHaveScreenshot(
|
||||
// This can be due to e.g. spinning animation, so we want to show it as a diff.
|
||||
if (errorMessage) {
|
||||
const header = formatMatcherMessage(this.utils, { promise: this.promise, isNot: this.isNot, matcherName: 'toHaveScreenshot', locator: locator?.toString(), expectation: 'expected', timeout, timedOut });
|
||||
return helper.handleDifferent(actual, undefined, previous, diff, header, errorMessage, log, this._stepInfo);
|
||||
return helper.handleDifferent(actual, undefined, previous, diff, header, errorMessage, log);
|
||||
}
|
||||
|
||||
// We successfully generated new screenshot.
|
||||
return helper.handleMissing(actual!, this._stepInfo);
|
||||
return helper.handleMissing(actual!);
|
||||
}
|
||||
|
||||
// General case:
|
||||
@@ -429,11 +432,11 @@ export async function toHaveScreenshot(
|
||||
return writeFiles(actual);
|
||||
let header = formatMatcherMessage(this.utils, { promise: this.promise, isNot: this.isNot, matcherName: 'toHaveScreenshot', locator: locator?.toString(), expectation: 'expected', timeout, timedOut });
|
||||
header += ' Failed to re-generate expected.\n';
|
||||
return helper.handleDifferent(actual, expectScreenshotOptions.expected, previous, diff, header, errorMessage, log, this._stepInfo);
|
||||
return helper.handleDifferent(actual, expectScreenshotOptions.expected, previous, diff, header, errorMessage, log);
|
||||
}
|
||||
|
||||
const header = formatMatcherMessage(this.utils, { promise: this.promise, isNot: this.isNot, matcherName: 'toHaveScreenshot', locator: locator?.toString(), expectation: 'expected', timeout, timedOut });
|
||||
return helper.handleDifferent(actual, expectScreenshotOptions.expected, previous, diff, header, errorMessage, log, this._stepInfo);
|
||||
return helper.handleDifferent(actual, expectScreenshotOptions.expected, previous, diff, header, errorMessage, log);
|
||||
} finally {
|
||||
await page.screencast.showOverlays();
|
||||
}
|
||||
|
||||
@@ -46,7 +46,6 @@ interface TestStepData {
|
||||
location?: Location;
|
||||
apiName?: string;
|
||||
params?: Record<string, any>;
|
||||
infectParentStepsWithError?: boolean;
|
||||
box?: boolean;
|
||||
// steps with any defined group are hidden from the report
|
||||
// 'internal' steps are hidden from the trace
|
||||
@@ -54,7 +53,7 @@ interface TestStepData {
|
||||
}
|
||||
|
||||
export interface TestStepInternal extends TestStepData {
|
||||
complete(result: { error?: Error | unknown, suggestedRebaseline?: string }): void;
|
||||
complete(result: { error?: Error | unknown, softError?: Error | unknown, shouldNotRetryTest?: boolean, suggestedRebaseline?: string, attachments?: TestInfo['attachments'] }): void;
|
||||
info: TestStepInfoImpl;
|
||||
attachmentIndices: number[];
|
||||
stepId: string;
|
||||
@@ -62,6 +61,7 @@ export interface TestStepInternal extends TestStepData {
|
||||
steps: TestStepInternal[];
|
||||
endWallTime?: number;
|
||||
error?: ipc.TestInfoErrorImpl;
|
||||
infectParentStepsWithError?: boolean;
|
||||
}
|
||||
|
||||
type SnapshotNames = {
|
||||
@@ -318,6 +318,10 @@ export class TestInfoImpl implements TestInfo {
|
||||
return;
|
||||
|
||||
step.endWallTime = Date.now();
|
||||
if (result.attachments) {
|
||||
for (const attachment of result.attachments)
|
||||
this._attach(attachment, stepId);
|
||||
}
|
||||
if (result.error) {
|
||||
if (typeof result.error === 'object' && !(result.error as any)?.[stepSymbol])
|
||||
(result.error as any)[stepSymbol] = step;
|
||||
@@ -326,6 +330,12 @@ export class TestInfoImpl implements TestInfo {
|
||||
error.stack = `${error.message}\n${stringifyStackFrames(step.boxedStack).join('\n')}`;
|
||||
step.error = error;
|
||||
}
|
||||
if (result.softError) {
|
||||
step.infectParentStepsWithError = true;
|
||||
this._failWithError(result.softError);
|
||||
}
|
||||
if (result.shouldNotRetryTest)
|
||||
this._hasNonRetriableError = true;
|
||||
|
||||
if (!step.error) {
|
||||
// Soft errors inside try/catch will make the test fail.
|
||||
@@ -403,9 +413,7 @@ export class TestInfoImpl implements TestInfo {
|
||||
this.status = 'interrupted';
|
||||
}
|
||||
|
||||
_failWithError(error: Error | unknown, shouldNotRetry?: 'shouldNotRetry') {
|
||||
if (shouldNotRetry)
|
||||
this._hasNonRetriableError = true;
|
||||
_failWithError(error: Error | unknown) {
|
||||
if (this.status === 'passed' || this.status === 'skipped')
|
||||
this.status = error instanceof TimeoutManagerError ? 'timedOut' : 'failed';
|
||||
const serialized = testInfoError(error);
|
||||
@@ -694,12 +702,8 @@ export class TestStepInfoImpl implements TestStepInfo {
|
||||
}
|
||||
}
|
||||
|
||||
_attachToStep(attachment: TestInfo['attachments'][0]): void {
|
||||
this._testInfo._attach(attachment, this._stepId);
|
||||
}
|
||||
|
||||
async attach(name: string, options?: { body?: string | Buffer; contentType?: string; path?: string; }): Promise<void> {
|
||||
this._attachToStep(await normalizeAndSaveAttachment(this._testInfo.outputPath(), name, options));
|
||||
this._testInfo._attach(await normalizeAndSaveAttachment(this._testInfo.outputPath(), name, options), this._stepId);
|
||||
}
|
||||
|
||||
get titlePath(): string[] {
|
||||
|
||||
@@ -25,7 +25,7 @@ const EXPECTED: Record<string, Record<string, number>> = {
|
||||
'lib/utilsBundle.js.LICENSE': 80,
|
||||
},
|
||||
'playwright': {
|
||||
'lib/matchers/expect.js.LICENSE': 30,
|
||||
'lib/matchers/expect.js.LICENSE': 20,
|
||||
'lib/transform/esmLoader.js.LICENSE': 10,
|
||||
'lib/transform/babelBundle.js.LICENSE': 65,
|
||||
},
|
||||
|
||||
@@ -0,0 +1,922 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates. All Rights Reserved.
|
||||
* Modifications copyright (c) Microsoft Corporation.
|
||||
*
|
||||
* This source code is licensed under the MIT license found here
|
||||
* https://github.com/jestjs/jest/blob/v30.2.0/LICENSE
|
||||
*/
|
||||
|
||||
// Adapted from jest/packages/expect/src/__tests__/matchers.test.js,
|
||||
// asymmetricMatchers.test.ts and toThrowMatchers.test.ts.
|
||||
|
||||
import { test, expect } from './pageTest';
|
||||
|
||||
test.describe('.toBe()', () => {
|
||||
test('does not throw', () => {
|
||||
expect('a').not.toBe('b');
|
||||
expect('a').toBe('a');
|
||||
expect(1).not.toBe(2);
|
||||
expect(1).toBe(1);
|
||||
expect(null).not.toBe(undefined);
|
||||
expect(null).toBe(null);
|
||||
expect(undefined).toBe(undefined);
|
||||
expect(NaN).toBe(NaN);
|
||||
expect(BigInt(1)).not.toBe(BigInt(2));
|
||||
expect(BigInt(1)).not.toBe(1);
|
||||
expect(BigInt(1)).toBe(BigInt(1));
|
||||
});
|
||||
|
||||
for (const [i, [a, b]] of [
|
||||
[1, 2],
|
||||
[true, false],
|
||||
[{}, {}],
|
||||
[{ a: 1 }, { a: 1 }],
|
||||
[{ a: 1 }, { a: 5 }],
|
||||
[{ a: undefined, b: 2 }, { b: 2 }],
|
||||
[new Date('2020-02-20'), new Date('2020-02-20')],
|
||||
[/received/, /expected/],
|
||||
[['abc'], ['cde']],
|
||||
[[], []],
|
||||
[null, undefined],
|
||||
[-0, +0],
|
||||
].entries()) {
|
||||
test(`fails for: ${JSON.stringify(a)} and ${JSON.stringify(b)} #${i}`, () => {
|
||||
expect(() => expect(a).toBe(b)).toThrow();
|
||||
});
|
||||
}
|
||||
|
||||
for (const v of [false, 1, 'a', undefined, null, {}, []]) {
|
||||
test(`fails for '${JSON.stringify(v)}' with '.not'`, () => {
|
||||
expect(() => expect(v).not.toBe(v)).toThrow();
|
||||
});
|
||||
}
|
||||
|
||||
test('does not crash on circular references', () => {
|
||||
const obj: any = {};
|
||||
obj.circular = obj;
|
||||
expect(() => expect(obj).toBe({})).toThrow();
|
||||
});
|
||||
|
||||
test('matcherResult property contains matcher name, expected and actual values', () => {
|
||||
const actual = { a: 1 };
|
||||
const expected = { a: 2 };
|
||||
try {
|
||||
expect(actual).toBe(expected);
|
||||
} catch (error) {
|
||||
expect(error.matcherResult).toEqual(expect.objectContaining({
|
||||
actual,
|
||||
expected,
|
||||
name: 'toBe',
|
||||
}));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('.toStrictEqual()', () => {
|
||||
class TestClassA {
|
||||
a: number;
|
||||
b: number;
|
||||
constructor(a: number, b: number) {
|
||||
this.a = a;
|
||||
this.b = b;
|
||||
}
|
||||
}
|
||||
|
||||
class TestClassB {
|
||||
a: number;
|
||||
b: number;
|
||||
constructor(a: number, b: number) {
|
||||
this.a = a;
|
||||
this.b = b;
|
||||
}
|
||||
}
|
||||
|
||||
test('does not ignore keys with undefined values', () => {
|
||||
expect({ a: undefined, b: 2 }).not.toStrictEqual({ b: 2 });
|
||||
});
|
||||
|
||||
test('does not ignore keys with undefined values inside an array', () => {
|
||||
expect([{ a: undefined }]).not.toStrictEqual([{}]);
|
||||
});
|
||||
|
||||
test('passes when comparing same type', () => {
|
||||
expect({ test: new TestClassA(1, 2) }).toStrictEqual({ test: new TestClassA(1, 2) });
|
||||
});
|
||||
|
||||
test('does not pass for different types', () => {
|
||||
expect({ test: new TestClassA(1, 2) }).not.toStrictEqual({ test: new TestClassB(1, 2) });
|
||||
});
|
||||
|
||||
test('passes for matching buffers', () => {
|
||||
expect(Uint8Array.from([1]).buffer).toStrictEqual(Uint8Array.from([1]).buffer);
|
||||
expect(Uint8Array.from([]).buffer).toStrictEqual(Uint8Array.from([]).buffer);
|
||||
expect(Uint8Array.from([9, 3]).buffer).toStrictEqual(Uint8Array.from([9, 3]).buffer);
|
||||
});
|
||||
|
||||
test('does not pass when ArrayBuffers are not equal', () => {
|
||||
expect(Uint8Array.from([1, 2]).buffer).not.toStrictEqual(Uint8Array.from([0, 0]).buffer);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('.toEqual()', () => {
|
||||
for (const [a, b] of [
|
||||
[true, false],
|
||||
[1, 2],
|
||||
[0, Number.MIN_VALUE],
|
||||
[{ a: 1 }, { a: 2 }],
|
||||
['banana', 'apple'],
|
||||
[null, undefined],
|
||||
[[1], [2]],
|
||||
[new Set([1, 2]), new Set()],
|
||||
[new Set([1, 2]), new Set([1, 2, 3])],
|
||||
[new Map([['a', 0]]), new Map([['b', 0]])],
|
||||
[new Uint8Array([97, 98, 99]), new Uint8Array([97, 98, 100])],
|
||||
[{ a: 1, b: 2 }, expect.objectContaining({ a: 2 })],
|
||||
[[1, 3], expect.arrayContaining([1, 2])],
|
||||
['abd', expect.stringContaining('bc')],
|
||||
['abd', expect.stringMatching(/bc/i)],
|
||||
[undefined, expect.anything()],
|
||||
[undefined, expect.any(Function)],
|
||||
].entries()) {
|
||||
test(`{pass: false} expect().toEqual() #${a}`, () => {
|
||||
expect(() => expect(b[0]).toEqual(b[1])).toThrow();
|
||||
expect(b[0]).not.toEqual(b[1]);
|
||||
});
|
||||
}
|
||||
|
||||
for (const [a, b] of [
|
||||
[true, true],
|
||||
[1, 1],
|
||||
[NaN, NaN],
|
||||
['abc', 'abc'],
|
||||
[[1], [1]],
|
||||
[{}, {}],
|
||||
[{ a: 99 }, { a: 99 }],
|
||||
[new Set(), new Set()],
|
||||
[new Set([1, 2]), new Set([1, 2])],
|
||||
[new Set([1, 2]), new Set([2, 1])],
|
||||
[new Map(), new Map()],
|
||||
[new Uint8Array([97, 98, 99]), new Uint8Array([97, 98, 99])],
|
||||
[{ a: 1, b: 2 }, expect.objectContaining({ a: 1 })],
|
||||
[[1, 2, 3], expect.arrayContaining([2, 3])],
|
||||
['abcd', expect.stringContaining('bc')],
|
||||
['abcd', expect.stringMatching('bc')],
|
||||
[true, expect.anything()],
|
||||
[() => {}, expect.any(Function)],
|
||||
].entries()) {
|
||||
test(`{pass: true} expect().toEqual() #${a}`, () => {
|
||||
expect(b[0]).toEqual(b[1]);
|
||||
expect(() => expect(b[0]).not.toEqual(b[1])).toThrow();
|
||||
});
|
||||
}
|
||||
|
||||
test('assertion error matcherResult property contains matcher name, expected and actual values', () => {
|
||||
const actual = { a: 1 };
|
||||
const expected = { a: 2 };
|
||||
try {
|
||||
expect(actual).toEqual(expected);
|
||||
} catch (error) {
|
||||
expect(error.matcherResult).toEqual(expect.objectContaining({
|
||||
actual,
|
||||
expected,
|
||||
name: 'toEqual',
|
||||
}));
|
||||
}
|
||||
});
|
||||
|
||||
test('symbol based keys in arrays are processed correctly', () => {
|
||||
const mySymbol = Symbol('test');
|
||||
const actual1: any[] = [];
|
||||
actual1[mySymbol as any] = 3;
|
||||
const actual2: any[] = [];
|
||||
actual2[mySymbol as any] = 4;
|
||||
const expected: any[] = [];
|
||||
expected[mySymbol as any] = 3;
|
||||
|
||||
expect(actual1).toEqual(expected);
|
||||
expect(actual2).not.toEqual(expected);
|
||||
});
|
||||
|
||||
test('non-enumerable members should be skipped during equal', () => {
|
||||
const actual: any = { x: 3 };
|
||||
Object.defineProperty(actual, 'test', { enumerable: false, value: 5 });
|
||||
expect(actual).toEqual({ x: 3 });
|
||||
});
|
||||
|
||||
test('objectContaining sample can be used multiple times', () => {
|
||||
const expected = expect.objectContaining({ b: 7 });
|
||||
expect({ a: 1, b: 2 }).not.toEqual(expected);
|
||||
expect({ a: 3, b: 7 }).toEqual(expected);
|
||||
});
|
||||
|
||||
test.describe('cyclic object equality', () => {
|
||||
test('properties with the same circularity are equal', () => {
|
||||
const a: any = {};
|
||||
a.x = a;
|
||||
const b: any = {};
|
||||
b.x = b;
|
||||
expect(a).toEqual(b);
|
||||
expect(b).toEqual(a);
|
||||
});
|
||||
|
||||
test('properties with different circularity are not equal', () => {
|
||||
const a: any = {};
|
||||
a.x = { y: a };
|
||||
const b: any = {};
|
||||
const bx: any = {};
|
||||
b.x = bx;
|
||||
bx.y = bx;
|
||||
expect(a).not.toEqual(b);
|
||||
expect(b).not.toEqual(a);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('.toBeInstanceOf()', () => {
|
||||
class A {}
|
||||
class B {}
|
||||
class C extends B {}
|
||||
class E extends C {}
|
||||
|
||||
for (const [i, [a, b]] of ([
|
||||
[new Map(), Map],
|
||||
[[], Array],
|
||||
[new A(), A],
|
||||
[new C(), B],
|
||||
[new E(), B],
|
||||
] as const).entries()) {
|
||||
test(`passing instanceof #${i}`, () => {
|
||||
expect(() => expect(a).not.toBeInstanceOf(b)).toThrow();
|
||||
expect(a).toBeInstanceOf(b);
|
||||
});
|
||||
}
|
||||
|
||||
for (const [i, [a, b]] of ([
|
||||
['a', String],
|
||||
[1, Number],
|
||||
[true, Boolean],
|
||||
[new A(), B],
|
||||
] as const).entries()) {
|
||||
test(`failing instanceof #${i}`, () => {
|
||||
expect(() => expect(a).toBeInstanceOf(b)).toThrow();
|
||||
expect(a).not.toBeInstanceOf(b);
|
||||
});
|
||||
}
|
||||
|
||||
test('throws if constructor is not a function', () => {
|
||||
expect(() => expect({}).toBeInstanceOf(4 as any)).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('.toBeTruthy(), .toBeFalsy()', () => {
|
||||
for (const v of [{}, [], true, 1, 'a', 0.5, new Map(), () => {}, Infinity]) {
|
||||
test(`'${String(v)}' is truthy`, () => {
|
||||
expect(v).toBeTruthy();
|
||||
expect(v).not.toBeFalsy();
|
||||
expect(() => expect(v).not.toBeTruthy()).toThrow();
|
||||
expect(() => expect(v).toBeFalsy()).toThrow();
|
||||
});
|
||||
}
|
||||
|
||||
for (const v of [false, null, NaN, 0, '', undefined]) {
|
||||
test(`'${String(v)}' is falsy`, () => {
|
||||
expect(v).toBeFalsy();
|
||||
expect(v).not.toBeTruthy();
|
||||
expect(() => expect(v).toBeTruthy()).toThrow();
|
||||
expect(() => expect(v).not.toBeFalsy()).toThrow();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
test.describe('.toBeNaN()', () => {
|
||||
test('{pass: true}', () => {
|
||||
for (const v of [NaN, Math.sqrt(-1), Infinity - Infinity, 0 / 0]) {
|
||||
expect(v).toBeNaN();
|
||||
expect(() => expect(v).not.toBeNaN()).toThrow();
|
||||
}
|
||||
});
|
||||
|
||||
test('throws', () => {
|
||||
for (const v of [1, '', null, undefined, {}, [], 0.2, 0, Infinity, -Infinity]) {
|
||||
expect(() => expect(v).toBeNaN()).toThrow();
|
||||
expect(v).not.toBeNaN();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('.toBeNull()', () => {
|
||||
for (const v of [{}, [], true, 1, 'a', 0.5, new Map(), () => {}, Infinity]) {
|
||||
test(`fails for '${String(v)}'`, () => {
|
||||
expect(v).not.toBeNull();
|
||||
expect(() => expect(v).toBeNull()).toThrow();
|
||||
});
|
||||
}
|
||||
|
||||
test('fails for null with .not', () => {
|
||||
expect(() => expect(null).not.toBeNull()).toThrow();
|
||||
});
|
||||
|
||||
test('pass for null', () => {
|
||||
expect(null).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('.toBeDefined(), .toBeUndefined()', () => {
|
||||
for (const v of [{}, [], true, 1, 'a', 0.5, new Map(), () => {}, Infinity]) {
|
||||
test(`'${String(v)}' is defined`, () => {
|
||||
expect(v).toBeDefined();
|
||||
expect(v).not.toBeUndefined();
|
||||
expect(() => expect(v).not.toBeDefined()).toThrow();
|
||||
expect(() => expect(v).toBeUndefined()).toThrow();
|
||||
});
|
||||
}
|
||||
|
||||
test('undefined is undefined', () => {
|
||||
expect(undefined).toBeUndefined();
|
||||
expect(undefined).not.toBeDefined();
|
||||
expect(() => expect(undefined).toBeDefined()).toThrow();
|
||||
expect(() => expect(undefined).not.toBeUndefined()).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('.toBeGreaterThan(), .toBeLessThan(), .toBeGreaterThanOrEqual(), .toBeLessThanOrEqual()', () => {
|
||||
for (const [small, big] of [
|
||||
[1, 2],
|
||||
[-Infinity, Infinity],
|
||||
[Number.MIN_VALUE, Number.MAX_VALUE],
|
||||
[0.1, 0.2],
|
||||
] as const) {
|
||||
test(`comparing ${small} and ${big}`, () => {
|
||||
expect(small).toBeLessThan(big);
|
||||
expect(big).not.toBeLessThan(small);
|
||||
expect(big).toBeGreaterThan(small);
|
||||
expect(small).not.toBeGreaterThan(big);
|
||||
expect(small).toBeLessThanOrEqual(big);
|
||||
expect(big).not.toBeLessThanOrEqual(small);
|
||||
expect(big).toBeGreaterThanOrEqual(small);
|
||||
expect(small).not.toBeGreaterThanOrEqual(big);
|
||||
|
||||
expect(() => expect(small).toBeGreaterThan(big)).toThrow();
|
||||
expect(() => expect(small).not.toBeLessThan(big)).toThrow();
|
||||
expect(() => expect(big).not.toBeGreaterThan(small)).toThrow();
|
||||
expect(() => expect(big).toBeLessThan(small)).toThrow();
|
||||
expect(() => expect(small).toBeGreaterThanOrEqual(big)).toThrow();
|
||||
expect(() => expect(small).not.toBeLessThanOrEqual(big)).toThrow();
|
||||
expect(() => expect(big).not.toBeGreaterThanOrEqual(small)).toThrow();
|
||||
expect(() => expect(big).toBeLessThanOrEqual(small)).toThrow();
|
||||
});
|
||||
}
|
||||
|
||||
test('can compare BigInt to Numbers', () => {
|
||||
const a = BigInt(2);
|
||||
expect(a).toBeGreaterThan(1);
|
||||
expect(a).toBeGreaterThanOrEqual(2);
|
||||
expect(2).toBeLessThanOrEqual(a);
|
||||
expect(a).toBeLessThan(3);
|
||||
expect(a).toBeLessThanOrEqual(2);
|
||||
});
|
||||
|
||||
for (const [n1, n2] of [
|
||||
[1, 1],
|
||||
[Number.MIN_VALUE, Number.MIN_VALUE],
|
||||
[Infinity, Infinity],
|
||||
[-Infinity, -Infinity],
|
||||
]) {
|
||||
test(`equal numbers: [${n1}, ${n2}]`, () => {
|
||||
expect(n1).toBeGreaterThanOrEqual(n2);
|
||||
expect(n1).toBeLessThanOrEqual(n2);
|
||||
expect(() => expect(n1).not.toBeGreaterThanOrEqual(n2)).toThrow();
|
||||
expect(() => expect(n1).not.toBeLessThanOrEqual(n2)).toThrow();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
test.describe('.toContain(), .toContainEqual()', () => {
|
||||
const typedArray = new Int8Array(2);
|
||||
typedArray[0] = 0;
|
||||
typedArray[1] = 1;
|
||||
|
||||
test('iterable', () => {
|
||||
const iterable = {
|
||||
*[Symbol.iterator]() {
|
||||
yield 1;
|
||||
yield 2;
|
||||
yield 3;
|
||||
},
|
||||
};
|
||||
expect(iterable).toContain(2);
|
||||
expect(iterable).toContainEqual(2);
|
||||
expect(() => expect(iterable).not.toContain(1)).toThrow();
|
||||
expect(() => expect(iterable).not.toContainEqual(1)).toThrow();
|
||||
});
|
||||
|
||||
for (const [i, [list, v]] of ([
|
||||
[[1, 2, 3, 4], 1],
|
||||
[['a', 'b', 'c', 'd'], 'a'],
|
||||
[[undefined, null], null],
|
||||
[[undefined, null], undefined],
|
||||
['abcdef', 'abc'],
|
||||
['11112111', '2'],
|
||||
[new Set(['abc', 'def']), 'abc'],
|
||||
[typedArray, 1],
|
||||
] as const).entries()) {
|
||||
test(`contains #${i}`, () => {
|
||||
expect(list).toContain(v);
|
||||
expect(() => expect(list).not.toContain(v)).toThrow();
|
||||
});
|
||||
}
|
||||
|
||||
for (const [i, [list, v]] of ([
|
||||
[[1, 2, 3], 4],
|
||||
[[null, undefined], 1],
|
||||
[[{}, []], []],
|
||||
[[{}, []], {}],
|
||||
] as const).entries()) {
|
||||
test(`does not contain #${i}`, () => {
|
||||
expect(list).not.toContain(v);
|
||||
expect(() => expect(list).toContain(v)).toThrow();
|
||||
});
|
||||
}
|
||||
|
||||
test('error cases', () => {
|
||||
expect(() => expect(null).toContain(1)).toThrow();
|
||||
});
|
||||
|
||||
for (const [i, [list, v]] of ([
|
||||
[[1, 2, 3, 4], 1],
|
||||
[['a', 'b', 'c', 'd'], 'a'],
|
||||
[[undefined, null], null],
|
||||
[[{ a: 'b' }, { a: 'c' }], { a: 'b' }],
|
||||
[new Set([1, 2, 3, 4]), 1],
|
||||
] as const).entries()) {
|
||||
test(`contains a value equal #${i}`, () => {
|
||||
expect(list).toContainEqual(v);
|
||||
expect(() => expect(list).not.toContainEqual(v)).toThrow();
|
||||
});
|
||||
}
|
||||
|
||||
test('error cases for toContainEqual', () => {
|
||||
expect(() => expect(null).toContainEqual(1)).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('.toBeCloseTo', () => {
|
||||
for (const [n1, n2] of [
|
||||
[0, 0],
|
||||
[0, 0.001],
|
||||
[1.23, 1.229],
|
||||
[1.23, 1.226],
|
||||
[1.23, 1.234],
|
||||
[Infinity, Infinity],
|
||||
[-Infinity, -Infinity],
|
||||
]) {
|
||||
test(`{pass: true} expect(${n1}).toBeCloseTo(${n2})`, () => {
|
||||
expect(n1).toBeCloseTo(n2);
|
||||
expect(() => expect(n1).not.toBeCloseTo(n2)).toThrow();
|
||||
});
|
||||
}
|
||||
|
||||
for (const [n1, n2] of [
|
||||
[0, 0.01],
|
||||
[1, 1.23],
|
||||
[Infinity, -Infinity],
|
||||
[Infinity, 1.23],
|
||||
[-Infinity, -1.23],
|
||||
]) {
|
||||
test(`{pass: false} expect(${n1}).toBeCloseTo(${n2})`, () => {
|
||||
expect(n1).not.toBeCloseTo(n2);
|
||||
expect(() => expect(n1).toBeCloseTo(n2)).toThrow();
|
||||
});
|
||||
}
|
||||
|
||||
for (const [n1, n2, p] of [
|
||||
[0, 0.1, 0],
|
||||
[0, 0.0001, 3],
|
||||
[0, 0.000_004, 5],
|
||||
[2.000_000_2, 2, 5],
|
||||
]) {
|
||||
test(`{pass: true} expect(${n1}).toBeCloseTo(${n2}, ${p})`, () => {
|
||||
expect(n1).toBeCloseTo(n2, p);
|
||||
expect(() => expect(n1).not.toBeCloseTo(n2, p)).toThrow();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
test.describe('.toMatch()', () => {
|
||||
for (const [n1, n2] of [
|
||||
['foo', 'foo'],
|
||||
['Foo bar', /^foo/i],
|
||||
] as const) {
|
||||
test(`{pass: true} expect(${n1}).toMatch(${n2})`, () => {
|
||||
expect(n1).toMatch(n2);
|
||||
expect(() => expect(n1).not.toMatch(n2)).toThrow();
|
||||
});
|
||||
}
|
||||
|
||||
for (const [n1, n2] of [
|
||||
['bar', 'foo'],
|
||||
['bar', /foo/],
|
||||
] as const) {
|
||||
test(`throws: [${n1}, ${n2}]`, () => {
|
||||
expect(() => expect(n1).toMatch(n2)).toThrow();
|
||||
});
|
||||
}
|
||||
|
||||
for (const [i, [n1, n2]] of ([
|
||||
[1, 'foo'],
|
||||
[{}, 'foo'],
|
||||
[[], 'foo'],
|
||||
[true, 'foo'],
|
||||
[/foo/i, 'foo'],
|
||||
[() => {}, 'foo'],
|
||||
[undefined, 'foo'],
|
||||
] as const).entries()) {
|
||||
test(`throws if non String actual value passed #${i}`, () => {
|
||||
expect(() => expect(n1).toMatch(n2)).toThrow();
|
||||
});
|
||||
}
|
||||
|
||||
for (const [i, [n1, n2]] of ([
|
||||
['foo', 1],
|
||||
['foo', {}],
|
||||
['foo', []],
|
||||
['foo', true],
|
||||
['foo', () => {}],
|
||||
['foo', undefined],
|
||||
] as const).entries()) {
|
||||
test(`throws if non String/RegExp expected value passed #${i}`, () => {
|
||||
expect(() => expect(n1).toMatch(n2 as any)).toThrow();
|
||||
});
|
||||
}
|
||||
|
||||
test('escapes strings properly', () => {
|
||||
expect('this?: throws').toMatch('this?: throws');
|
||||
});
|
||||
|
||||
test('does not maintain RegExp state between calls', () => {
|
||||
const regex = /f\d+/gi;
|
||||
expect('f123').toMatch(regex);
|
||||
expect('F456').toMatch(regex);
|
||||
expect(regex.lastIndex).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('.toHaveLength', () => {
|
||||
for (const [i, [received, length]] of ([
|
||||
[[1, 2], 2],
|
||||
[[], 0],
|
||||
[['a', 'b'], 2],
|
||||
['abc', 3],
|
||||
['', 0],
|
||||
[() => {}, 0],
|
||||
] as const).entries()) {
|
||||
test(`{pass: true} toHaveLength(${length}) #${i}`, () => {
|
||||
expect(received).toHaveLength(length);
|
||||
expect(() => expect(received).not.toHaveLength(length)).toThrow();
|
||||
});
|
||||
}
|
||||
|
||||
for (const [i, [received, length]] of ([
|
||||
[[1, 2], 3],
|
||||
[[], 1],
|
||||
[['a', 'b'], 99],
|
||||
['abc', 66],
|
||||
['', 1],
|
||||
] as const).entries()) {
|
||||
test(`{pass: false} toHaveLength(${length}) #${i}`, () => {
|
||||
expect(received).not.toHaveLength(length);
|
||||
expect(() => expect(received).toHaveLength(length)).toThrow();
|
||||
});
|
||||
}
|
||||
|
||||
test('error cases', () => {
|
||||
expect(() => expect({ a: 9 }).toHaveLength(1)).toThrow();
|
||||
expect(() => expect(0).toHaveLength(1)).toThrow();
|
||||
expect(() => expect(undefined).not.toHaveLength(1)).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('.toHaveProperty()', () => {
|
||||
for (const [i, [obj, keyPath, value]] of ([
|
||||
[{ a: { b: { c: { d: 1 } } } }, 'a.b.c.d', 1],
|
||||
[{ a: { b: { c: { d: 1 } } } }, ['a', 'b', 'c', 'd'], 1],
|
||||
[{ 'a.b.c.d': 1 }, ['a.b.c.d'], 1],
|
||||
[{ a: { b: [1, 2, 3] } }, ['a', 'b', 1], 2],
|
||||
[{ a: { b: [1, 2, 3] } }, ['a', 'b', 1], expect.any(Number)],
|
||||
[{ a: 0 }, 'a', 0],
|
||||
[{ a: { b: undefined } }, 'a.b', undefined],
|
||||
[{ a: { b: { c: 5 } } }, 'a.b', { c: 5 }],
|
||||
[{ a: { b: [{ c: [{ d: 1 }] }] } }, 'a.b[0].c[0].d', 1],
|
||||
[{ '': 1 }, '', 1],
|
||||
] as const).entries()) {
|
||||
test(`{pass: true} toHaveProperty with value #${i}`, () => {
|
||||
expect(obj).toHaveProperty(keyPath as any, value);
|
||||
expect(() => expect(obj).not.toHaveProperty(keyPath as any, value)).toThrow();
|
||||
});
|
||||
}
|
||||
|
||||
for (const [i, [obj, keyPath, value]] of ([
|
||||
[{ a: { b: { c: { d: 1 } } } }, 'a.b.ttt.d', 1],
|
||||
[{ a: { b: { c: { d: 1 } } } }, 'a.b.c.d', 2],
|
||||
[{ 'a.b.c.d': 1 }, 'a.b.c.d', 2],
|
||||
[{ a: { b: { c: {} } } }, 'a.b.c.d', 1],
|
||||
[{ a: 1 }, 'a.b.c.d', 5],
|
||||
[{}, 'a', 'test'],
|
||||
[{ a: { b: 3 } }, 'a.b', undefined],
|
||||
] as const).entries()) {
|
||||
test(`{pass: false} toHaveProperty with value #${i}`, () => {
|
||||
expect(() => expect(obj).toHaveProperty(keyPath, value)).toThrow();
|
||||
expect(obj).not.toHaveProperty(keyPath, value);
|
||||
});
|
||||
}
|
||||
|
||||
for (const [i, [obj, keyPath]] of ([
|
||||
[{ a: { b: { c: { d: 1 } } } }, 'a.b.c.d'],
|
||||
[{ a: { b: { c: { d: 1 } } } }, ['a', 'b', 'c', 'd']],
|
||||
[{ 'a.b.c.d': 1 }, ['a.b.c.d']],
|
||||
[{ a: 0 }, 'a'],
|
||||
[{ a: { b: undefined } }, 'a.b'],
|
||||
] as const).entries()) {
|
||||
test(`{pass: true} toHaveProperty without value #${i}`, () => {
|
||||
expect(obj).toHaveProperty(keyPath as any);
|
||||
expect(() => expect(obj).not.toHaveProperty(keyPath as any)).toThrow();
|
||||
});
|
||||
}
|
||||
|
||||
for (const [i, [obj, keyPath]] of ([
|
||||
[{ a: { b: { c: {} } } }, 'a.b.c.d'],
|
||||
[{ a: 1 }, 'a.b.c.d'],
|
||||
[{}, 'a'],
|
||||
] as const).entries()) {
|
||||
test(`{pass: false} toHaveProperty without value #${i}`, () => {
|
||||
expect(() => expect(obj).toHaveProperty(keyPath)).toThrow();
|
||||
expect(obj).not.toHaveProperty(keyPath);
|
||||
});
|
||||
}
|
||||
|
||||
for (const [i, [obj, keyPath]] of ([
|
||||
[null, 'a.b'],
|
||||
[undefined, 'a'],
|
||||
[{ a: { b: {} } }, undefined],
|
||||
[{ a: { b: {} } }, null],
|
||||
[{ a: { b: {} } }, 1],
|
||||
] as const).entries()) {
|
||||
test(`{error} toHaveProperty #${i}`, () => {
|
||||
expect(() => expect(obj).toHaveProperty(keyPath as any)).toThrow();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
test.describe('.toMatchObject()', () => {
|
||||
for (const [i, [a, b]] of ([
|
||||
[{ a: 'b', c: 'd' }, { a: 'b' }],
|
||||
[{ a: 'b', c: 'd' }, { a: 'b', c: 'd' }],
|
||||
[{ a: 'b', t: { x: { r: 'r' }, z: 'z' } }, { a: 'b', t: { z: 'z' } }],
|
||||
[{ a: [3, 4, 5], b: 'b' }, { a: [3, 4, 5] }],
|
||||
[{ a: 1, c: 2 }, { a: expect.any(Number) }],
|
||||
[new Set([1, 2]), new Set([1, 2])],
|
||||
[new Date('2015-11-30'), new Date('2015-11-30')],
|
||||
[{ a: null, b: 'b' }, { a: null }],
|
||||
[{ a: undefined, b: 'b' }, { a: undefined }],
|
||||
[[1, 2], [1, 2]],
|
||||
[new Error('foo'), new Error('foo')],
|
||||
[new Error('bar'), { message: 'bar' }],
|
||||
] as const).entries()) {
|
||||
test(`{pass: true} toMatchObject #${i}`, () => {
|
||||
expect(a).toMatchObject(b as any);
|
||||
expect(() => expect(a).not.toMatchObject(b as any)).toThrow();
|
||||
});
|
||||
}
|
||||
|
||||
for (const [i, [a, b]] of ([
|
||||
[{ a: 'b', c: 'd' }, { e: 'b' }],
|
||||
[{ a: 'b', c: 'd' }, { a: 'b!', c: 'd' }],
|
||||
[{ a: 'a', c: 'd' }, { a: expect.any(Number) }],
|
||||
[{ a: [3, 4, 5], b: 'b' }, { a: [3, 4, 5, 6] }],
|
||||
[[1, 2], [1, 3]],
|
||||
[new Set([1, 2]), new Set([2])],
|
||||
[new Error('foo'), new Error('bar')],
|
||||
] as const).entries()) {
|
||||
test(`{pass: false} toMatchObject #${i}`, () => {
|
||||
expect(a).not.toMatchObject(b as any);
|
||||
expect(() => expect(a).toMatchObject(b as any)).toThrow();
|
||||
});
|
||||
}
|
||||
|
||||
for (const [i, [a, b]] of ([
|
||||
[null, {}],
|
||||
[4, {}],
|
||||
['44', {}],
|
||||
[true, {}],
|
||||
[undefined, {}],
|
||||
[{}, null],
|
||||
[{}, 4],
|
||||
] as const).entries()) {
|
||||
test(`throws toMatchObject #${i}`, () => {
|
||||
expect(() => expect(a).toMatchObject(b as any)).toThrow();
|
||||
});
|
||||
}
|
||||
|
||||
test('does not match properties up in the prototype chain', () => {
|
||||
const a: any = {};
|
||||
a.ref = a;
|
||||
const b = Object.create(a);
|
||||
b.other = 'child';
|
||||
const matcher: any = { other: 'child' };
|
||||
matcher.ref = matcher;
|
||||
expect(b).not.toMatchObject(matcher);
|
||||
expect(() => expect(b).toMatchObject(matcher)).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('.toThrow()', () => {
|
||||
class CustomError extends Error {}
|
||||
|
||||
test('to throw or not to throw', () => {
|
||||
expect(() => {
|
||||
throw new CustomError('apple');
|
||||
}).toThrow();
|
||||
expect(() => {}).not.toThrow();
|
||||
});
|
||||
|
||||
test('substring passes', () => {
|
||||
expect(() => {
|
||||
throw new CustomError('apple');
|
||||
}).toThrow('apple');
|
||||
expect(() => {
|
||||
throw new CustomError('banana');
|
||||
}).not.toThrow('apple');
|
||||
expect(() => {}).not.toThrow('apple');
|
||||
});
|
||||
|
||||
test('substring fails when did not throw', () => {
|
||||
expect(() => expect(() => {}).toThrow('apple')).toThrow();
|
||||
});
|
||||
|
||||
test('substring fails when message did not match', () => {
|
||||
expect(() => expect(() => {
|
||||
throw new CustomError('apple');
|
||||
}).toThrow('banana')).toThrow();
|
||||
});
|
||||
|
||||
test('regexp passes', () => {
|
||||
expect(() => {
|
||||
throw new CustomError('apple');
|
||||
}).toThrow(/apple/);
|
||||
expect(() => {
|
||||
throw new CustomError('banana');
|
||||
}).not.toThrow(/apple/);
|
||||
expect(() => {}).not.toThrow(/apple/);
|
||||
});
|
||||
|
||||
test('regexp fails when did not throw', () => {
|
||||
expect(() => expect(() => {}).toThrow(/apple/)).toThrow();
|
||||
});
|
||||
|
||||
test('error class passes', () => {
|
||||
class Err extends CustomError {}
|
||||
class Err2 extends CustomError {}
|
||||
|
||||
expect(() => {
|
||||
throw new Err('apple');
|
||||
}).toThrow(Err);
|
||||
expect(() => {
|
||||
throw new Err('apple');
|
||||
}).toThrow(CustomError);
|
||||
|
||||
expect(() => {
|
||||
throw new Err('apple');
|
||||
}).not.toThrow(Err2);
|
||||
});
|
||||
|
||||
test('object with message passes', () => {
|
||||
expect(() => {
|
||||
throw new CustomError('apple');
|
||||
}).toThrow({ message: 'apple' });
|
||||
expect(() => {
|
||||
throw new CustomError('banana');
|
||||
}).not.toThrow({ message: 'apple' });
|
||||
});
|
||||
|
||||
test('properly escapes strings when matching against errors', () => {
|
||||
expect(() => {
|
||||
throw new TypeError('"this"? throws.');
|
||||
}).toThrow('"this"? throws.');
|
||||
});
|
||||
|
||||
test('error message and cause properties', () => {
|
||||
const errorCause = new Error('cause');
|
||||
const error = new Error('message', { cause: errorCause });
|
||||
expect(() => {
|
||||
throw error;
|
||||
}).toThrow({ message: 'message', cause: errorCause });
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('asymmetric matchers', () => {
|
||||
test('Any matches primitives', () => {
|
||||
expect('jest').toEqual(expect.any(String));
|
||||
expect(1).toEqual(expect.any(Number));
|
||||
expect(() => {}).toEqual(expect.any(Function));
|
||||
expect(true).toEqual(expect.any(Boolean));
|
||||
expect({}).toEqual(expect.any(Object));
|
||||
expect([]).toEqual(expect.any(Array));
|
||||
});
|
||||
|
||||
test('Any throws when called with empty constructor', () => {
|
||||
expect(() => (expect.any as any)()).toThrow(
|
||||
'any() expects to be passed a constructor function. Please pass one or use anything() to match any object.');
|
||||
});
|
||||
|
||||
test('Anything matches any type', () => {
|
||||
expect('jest').toEqual(expect.anything());
|
||||
expect(1).toEqual(expect.anything());
|
||||
expect(() => {}).toEqual(expect.anything());
|
||||
expect(true).toEqual(expect.anything());
|
||||
expect({}).toEqual(expect.anything());
|
||||
expect([]).toEqual(expect.anything());
|
||||
});
|
||||
|
||||
test('Anything does not match null and undefined', () => {
|
||||
expect(null).not.toEqual(expect.anything());
|
||||
expect(undefined).not.toEqual(expect.anything());
|
||||
});
|
||||
|
||||
test('ArrayContaining matches', () => {
|
||||
expect(['foo']).toEqual(expect.arrayContaining(['foo']));
|
||||
expect(['foo', 'bar']).toEqual(expect.arrayContaining(['foo']));
|
||||
});
|
||||
|
||||
test('ArrayContaining does not match', () => {
|
||||
expect(['bar']).not.toEqual(expect.arrayContaining(['foo']));
|
||||
});
|
||||
|
||||
test('ObjectContaining matches', () => {
|
||||
expect({ foo: 'foo', jest: 'jest' }).toEqual(expect.objectContaining({ foo: 'foo' }));
|
||||
expect({ foo: undefined }).toEqual(expect.objectContaining({ foo: undefined }));
|
||||
});
|
||||
|
||||
test('ObjectContaining does not match', () => {
|
||||
expect({ bar: 'bar' }).not.toEqual(expect.objectContaining({ foo: 'foo' }));
|
||||
expect({ foo: 'foox' }).not.toEqual(expect.objectContaining({ foo: 'foo' }));
|
||||
expect({}).not.toEqual(expect.objectContaining({ foo: undefined }));
|
||||
});
|
||||
|
||||
test('ObjectContaining throws for non-objects', () => {
|
||||
expect(() => expect({}).toEqual(expect.objectContaining(1337 as any))).toThrow(
|
||||
"You must provide an object to ObjectContaining, not 'number'.");
|
||||
});
|
||||
|
||||
test('StringContaining matches string against string', () => {
|
||||
expect('queen*').toEqual(expect.stringContaining('en*'));
|
||||
expect('queue').not.toEqual(expect.stringContaining('en*'));
|
||||
});
|
||||
|
||||
test('StringMatching matches string against regexp', () => {
|
||||
expect('queen').toEqual(expect.stringMatching(/en/));
|
||||
expect('queue').not.toEqual(expect.stringMatching(/en/));
|
||||
});
|
||||
|
||||
test('StringMatching matches string against string', () => {
|
||||
expect('queen').toEqual(expect.stringMatching('en'));
|
||||
expect('queue').not.toEqual(expect.stringMatching('en'));
|
||||
});
|
||||
|
||||
test('closeTo matches', () => {
|
||||
expect(0).toEqual(expect.closeTo(0));
|
||||
expect(0.001).toEqual(expect.closeTo(0));
|
||||
expect(1.229).toEqual(expect.closeTo(1.23));
|
||||
expect(Infinity).toEqual(expect.closeTo(Infinity));
|
||||
});
|
||||
|
||||
test('closeTo does not match', () => {
|
||||
expect(0.01).not.toEqual(expect.closeTo(0));
|
||||
expect(1.23).not.toEqual(expect.closeTo(1));
|
||||
expect(Infinity).not.toEqual(expect.closeTo(-Infinity));
|
||||
});
|
||||
|
||||
test('closeTo with precision', () => {
|
||||
expect(0.1).toEqual(expect.closeTo(0, 0));
|
||||
expect(0.0001).toEqual(expect.closeTo(0, 3));
|
||||
expect(0.000_004).toEqual(expect.closeTo(0, 5));
|
||||
});
|
||||
|
||||
test('closeTo throws if expected is not number', () => {
|
||||
expect(() => (expect.closeTo as any)('a')).toThrow('Expected is not a Number');
|
||||
});
|
||||
|
||||
test('arrayOf matches', () => {
|
||||
expect([1]).toEqual(expect.arrayOf(1));
|
||||
expect([1, 1, 1]).toEqual(expect.arrayOf(1));
|
||||
expect([{ a: 1 }, { a: 1 }]).toEqual(expect.arrayOf({ a: 1 }));
|
||||
expect(['a', 'b', 'c']).toEqual(expect.arrayOf(expect.any(String)));
|
||||
});
|
||||
|
||||
test('arrayOf does not match', () => {
|
||||
expect([2]).not.toEqual(expect.arrayOf(1));
|
||||
expect([1, 2]).not.toEqual(expect.arrayOf(1));
|
||||
expect('not an array').not.toEqual(expect.arrayOf(1));
|
||||
expect({}).not.toEqual(expect.arrayOf(1));
|
||||
expect([1, 2]).not.toEqual(expect.arrayOf(expect.any(String)));
|
||||
});
|
||||
});
|
||||
@@ -308,6 +308,7 @@ test('toHaveScreenshot should populate matcherResult', async ({ page, server, is
|
||||
e.matcherResult.message = stripAnsi(e.matcherResult.message);
|
||||
|
||||
expect.soft(e.matcherResult).toEqual({
|
||||
attachments: [expect.any(Object), expect.any(Object), expect.any(Object)],
|
||||
actual: expect.stringContaining('screenshot-sanity-actual'),
|
||||
expected: expect.stringContaining('screenshot-sanity-'),
|
||||
diff: expect.stringContaining('screenshot-sanity-diff'),
|
||||
|
||||
@@ -279,3 +279,29 @@ test('should show custom message', {
|
||||
expect(result.output).toContain('Expected: 2');
|
||||
expect(result.output).toContain('Received: 1');
|
||||
});
|
||||
|
||||
test('should swallow matcher errors and predicate errors', async ({ runInlineTest }) => {
|
||||
const result = await runInlineTest({
|
||||
'a.spec.ts': `
|
||||
import { test, expect } from '@playwright/test';
|
||||
test('should poll async predicate', async () => {
|
||||
let i = 0;
|
||||
await test.expect.poll(async () => {
|
||||
if (++i === 2)
|
||||
return 'some foo text';
|
||||
return undefined;
|
||||
}).toContain('foo');
|
||||
});
|
||||
test('should poll async predicate with not', async () => {
|
||||
let i = 0;
|
||||
await test.expect.poll(async () => {
|
||||
if (++i === 2)
|
||||
return 'some bar text';
|
||||
return undefined;
|
||||
}).not.toContain('foo');
|
||||
});
|
||||
`,
|
||||
});
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.passed).toBe(2);
|
||||
});
|
||||
|
||||
@@ -412,72 +412,18 @@ test('should return void/Promise when appropriate', async ({ runTSC }) => {
|
||||
expect(result.exitCode).toBe(0);
|
||||
});
|
||||
|
||||
test.describe('helpful expect errors', () => {
|
||||
test('top-level', async ({ runInlineTest }) => {
|
||||
const result = await runInlineTest({
|
||||
'a.spec.ts': `
|
||||
import { test, expect } from '@playwright/test';
|
||||
test('explodes', () => {
|
||||
expect(1).nope();
|
||||
});
|
||||
`
|
||||
});
|
||||
|
||||
expect(result.output).toContain(`expect: Property 'nope' not found.`);
|
||||
test('bare expect call should not throw', async ({ runInlineTest }) => {
|
||||
const result = await runInlineTest({
|
||||
'a.spec.ts': `
|
||||
import { test, expect } from '@playwright/test';
|
||||
test('explodes', () => {
|
||||
expect('');
|
||||
});
|
||||
`
|
||||
});
|
||||
|
||||
test('soft', async ({ runInlineTest }) => {
|
||||
const result = await runInlineTest({
|
||||
'a.spec.ts': `
|
||||
import { test, expect } from '@playwright/test';
|
||||
test('explodes', () => {
|
||||
expect.soft(1).nope();
|
||||
});
|
||||
`
|
||||
});
|
||||
|
||||
expect(result.output).toContain(`expect: Property 'nope' not found.`);
|
||||
});
|
||||
|
||||
test('poll', async ({ runInlineTest }) => {
|
||||
const result = await runInlineTest({
|
||||
'a.spec.ts': `
|
||||
import { test, expect } from '@playwright/test';
|
||||
test('explodes', () => {
|
||||
expect.poll(() => {}).nope();
|
||||
});
|
||||
`
|
||||
});
|
||||
|
||||
expect(result.output).toContain(`expect: Property 'nope' not found.`);
|
||||
});
|
||||
|
||||
test('not', async ({ runInlineTest }) => {
|
||||
const result = await runInlineTest({
|
||||
'a.spec.ts': `
|
||||
import { test, expect } from '@playwright/test';
|
||||
test('explodes', () => {
|
||||
expect(1).not.nope();
|
||||
});
|
||||
`
|
||||
});
|
||||
|
||||
expect(result.output).toContain(`expect: Property 'nope' not found.`);
|
||||
});
|
||||
|
||||
test('bare', async ({ runInlineTest }) => {
|
||||
const result = await runInlineTest({
|
||||
'a.spec.ts': `
|
||||
import { test, expect } from '@playwright/test';
|
||||
test('explodes', () => {
|
||||
expect('');
|
||||
});
|
||||
`
|
||||
});
|
||||
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.passed).toBe(1);
|
||||
});
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.passed).toBe(1);
|
||||
});
|
||||
|
||||
test('should reasonably work in global setup', async ({ runInlineTest }) => {
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
import { config as loadEnv } from 'dotenv';
|
||||
loadEnv({ path: path.join(__dirname, '..', '..', '.env') });
|
||||
process.env.PWTEST_UNDER_TEST = '1';
|
||||
|
||||
import { defineConfig, type ReporterDescription } from './stable-test-runner';
|
||||
import * as path from 'path';
|
||||
|
||||
@@ -102,7 +102,6 @@ export const test = base
|
||||
command: ['node', cliEntrypoint, 'test', (options.useWeb ? '--ui-host=127.0.0.1' : '--ui'), '--workers=1', ...(options.additionalArgs || [])],
|
||||
env: inheritAndCleanEnv({
|
||||
...env,
|
||||
PWTEST_UNDER_TEST: '1',
|
||||
PWTEST_CACHE_DIR: cacheDir,
|
||||
PWTEST_HEADED_FOR_TEST: headless ? '0' : '1',
|
||||
PWTEST_PRINT_WS_ENDPOINT: '1',
|
||||
|
||||
Reference in New Issue
Block a user