chore(test-runner): vendor the expect library (#40301)

This commit is contained in:
Dmitry Gozman
2026-04-21 15:27:15 +01:00
committed by GitHub
parent ef3238b8a4
commit 396a39b147
17 changed files with 3038 additions and 481 deletions
+3 -76
View File
@@ -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"
+3 -2
View File
@@ -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"
+5 -1
View File
@@ -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
+253 -266
View File
@@ -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
+13 -17
View File
@@ -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);
+1 -2
View File
@@ -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();
}
+14 -10
View File
@@ -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[] {
+1 -1
View File
@@ -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,
},
+922
View File
@@ -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)));
});
});
+1
View File
@@ -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'),
+26
View File
@@ -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);
});
+10 -64
View File
@@ -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',