2021-06-06 17:09:53 -07:00
|
|
|
/**
|
|
|
|
* Copyright Microsoft Corporation. All rights reserved.
|
|
|
|
*
|
|
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
* you may not use this file except in compliance with the License.
|
|
|
|
* You may obtain a copy of the License at
|
|
|
|
*
|
|
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
*
|
|
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
* See the License for the specific language governing permissions and
|
|
|
|
* limitations under the License.
|
|
|
|
*/
|
|
|
|
|
|
|
|
import expectLibrary from 'expect';
|
2021-10-18 20:06:18 -08:00
|
|
|
import path from 'path';
|
2021-10-08 08:01:31 -07:00
|
|
|
import {
|
|
|
|
INVERTED_COLOR,
|
|
|
|
RECEIVED_COLOR,
|
|
|
|
printReceived,
|
|
|
|
} from 'jest-matcher-utils';
|
2021-07-28 15:44:44 -07:00
|
|
|
import {
|
|
|
|
toBeChecked,
|
|
|
|
toBeDisabled,
|
|
|
|
toBeEditable,
|
|
|
|
toBeEmpty,
|
|
|
|
toBeEnabled,
|
|
|
|
toBeFocused,
|
|
|
|
toBeHidden,
|
2021-11-30 18:12:19 -08:00
|
|
|
toBeOK,
|
2021-07-28 22:30:37 -07:00
|
|
|
toBeVisible,
|
2021-07-28 15:44:44 -07:00
|
|
|
toContainText,
|
2021-08-06 16:58:42 -07:00
|
|
|
toHaveAttribute,
|
2021-07-28 15:44:44 -07:00
|
|
|
toHaveClass,
|
2021-07-29 07:33:19 -07:00
|
|
|
toHaveCount,
|
2021-07-28 22:30:37 -07:00
|
|
|
toHaveCSS,
|
2021-07-28 15:44:44 -07:00
|
|
|
toHaveId,
|
2021-08-06 16:58:42 -07:00
|
|
|
toHaveJSProperty,
|
2021-07-28 15:44:44 -07:00
|
|
|
toHaveText,
|
2021-07-29 07:33:19 -07:00
|
|
|
toHaveTitle,
|
|
|
|
toHaveURL,
|
2021-07-28 15:44:44 -07:00
|
|
|
toHaveValue
|
2021-07-28 22:30:37 -07:00
|
|
|
} from './matchers/matchers';
|
2022-02-28 13:25:59 -07:00
|
|
|
import { toMatchSnapshot, toHaveScreenshot } from './matchers/toMatchSnapshot';
|
2021-08-02 22:11:37 -07:00
|
|
|
import type { Expect, TestError } from './types';
|
2021-07-30 16:07:02 -07:00
|
|
|
import matchers from 'expect/build/matchers';
|
|
|
|
import { currentTestInfo } from './globals';
|
2021-08-02 22:11:37 -07:00
|
|
|
import { serializeError } from './util';
|
2021-10-18 20:06:18 -08:00
|
|
|
import StackUtils from 'stack-utils';
|
|
|
|
|
|
|
|
const stackUtils = new StackUtils();
|
2021-06-06 17:09:53 -07:00
|
|
|
|
2021-10-08 08:01:31 -07:00
|
|
|
// #region
|
|
|
|
// Mirrored from https://github.com/facebook/jest/blob/f13abff8df9a0e1148baf3584bcde6d1b479edc7/packages/expect/src/print.ts
|
|
|
|
/**
|
|
|
|
* Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.
|
|
|
|
*
|
|
|
|
* This source code is licensed under the MIT license found here
|
|
|
|
* https://github.com/facebook/jest/blob/1547740bbc26400d69f4576bf35645163e942829/LICENSE
|
|
|
|
*/
|
|
|
|
|
|
|
|
// Format substring but do not enclose in double quote marks.
|
|
|
|
// The replacement is compatible with pretty-format package.
|
|
|
|
const printSubstring = (val: string): string => val.replace(/"|\\/g, '\\$&');
|
|
|
|
|
|
|
|
export const printReceivedStringContainExpectedSubstring = (
|
|
|
|
received: string,
|
|
|
|
start: number,
|
|
|
|
length: number, // not end
|
|
|
|
): string =>
|
|
|
|
RECEIVED_COLOR(
|
|
|
|
'"' +
|
|
|
|
printSubstring(received.slice(0, start)) +
|
|
|
|
INVERTED_COLOR(printSubstring(received.slice(start, start + length))) +
|
|
|
|
printSubstring(received.slice(start + length)) +
|
|
|
|
'"',
|
|
|
|
);
|
|
|
|
|
|
|
|
export const printReceivedStringContainExpectedResult = (
|
|
|
|
received: string,
|
|
|
|
result: RegExpExecArray | null,
|
|
|
|
): string =>
|
|
|
|
result === null
|
|
|
|
? printReceived(received)
|
|
|
|
: printReceivedStringContainExpectedSubstring(
|
|
|
|
received,
|
|
|
|
result.index,
|
|
|
|
result[0].length,
|
|
|
|
);
|
|
|
|
|
|
|
|
// #endregion
|
|
|
|
|
2022-02-02 19:33:51 -07:00
|
|
|
function createExpect(actual: unknown, message: string|undefined, isSoft: boolean) {
|
|
|
|
if (message !== undefined && typeof message !== 'string')
|
|
|
|
throw new Error('expect(actual, optionalErrorMessage): optional error message must be a string.');
|
|
|
|
return new Proxy(expectLibrary(actual), new ExpectMetaInfoProxyHandler(message || '', isSoft));
|
|
|
|
}
|
|
|
|
|
2022-01-31 18:14:59 -07:00
|
|
|
export const expect: Expect = new Proxy(expectLibrary as any, {
|
|
|
|
apply: function(target: any, thisArg: any, argumentsList: [actual: unknown, message: string|undefined]) {
|
2022-02-02 19:33:51 -07:00
|
|
|
const [actual, message] = argumentsList;
|
|
|
|
return createExpect(actual, message, false /* isSoft */);
|
2022-01-31 18:14:59 -07:00
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2022-02-02 19:33:51 -07:00
|
|
|
expect.soft = (actual: unknown, message: string|undefined) => {
|
|
|
|
return createExpect(actual, message, true /* isSoft */);
|
|
|
|
};
|
|
|
|
|
2021-07-27 20:26:12 -07:00
|
|
|
expectLibrary.setState({ expand: false });
|
2021-07-30 16:07:02 -07:00
|
|
|
const customMatchers = {
|
2021-07-28 12:07:11 -07:00
|
|
|
toBeChecked,
|
|
|
|
toBeDisabled,
|
|
|
|
toBeEditable,
|
|
|
|
toBeEmpty,
|
|
|
|
toBeEnabled,
|
|
|
|
toBeFocused,
|
|
|
|
toBeHidden,
|
2021-11-30 18:12:19 -08:00
|
|
|
toBeOK,
|
2021-07-28 12:07:11 -07:00
|
|
|
toBeVisible,
|
|
|
|
toContainText,
|
2021-08-06 16:58:42 -07:00
|
|
|
toHaveAttribute,
|
2021-07-28 15:44:44 -07:00
|
|
|
toHaveClass,
|
2021-07-29 07:33:19 -07:00
|
|
|
toHaveCount,
|
2021-07-28 22:30:37 -07:00
|
|
|
toHaveCSS,
|
2021-07-28 12:07:11 -07:00
|
|
|
toHaveId,
|
2021-08-06 16:58:42 -07:00
|
|
|
toHaveJSProperty,
|
2021-07-28 12:07:11 -07:00
|
|
|
toHaveText,
|
2021-07-29 07:33:19 -07:00
|
|
|
toHaveTitle,
|
|
|
|
toHaveURL,
|
2021-07-28 12:07:11 -07:00
|
|
|
toHaveValue,
|
|
|
|
toMatchSnapshot,
|
2022-02-28 13:25:59 -07:00
|
|
|
toHaveScreenshot,
|
2021-07-30 16:07:02 -07:00
|
|
|
};
|
|
|
|
|
2022-01-31 18:14:59 -07:00
|
|
|
type ExpectMetaInfo = {
|
|
|
|
message: string;
|
2022-02-02 19:33:51 -07:00
|
|
|
isSoft: boolean;
|
2022-01-31 18:14:59 -07:00
|
|
|
};
|
|
|
|
|
|
|
|
let expectCallMetaInfo: undefined|ExpectMetaInfo = undefined;
|
|
|
|
|
|
|
|
class ExpectMetaInfoProxyHandler {
|
|
|
|
private _message: string;
|
2022-02-02 19:33:51 -07:00
|
|
|
private _isSoft: boolean;
|
2022-01-31 18:14:59 -07:00
|
|
|
|
2022-02-02 19:33:51 -07:00
|
|
|
constructor(message: string, isSoft: boolean) {
|
2022-01-31 18:14:59 -07:00
|
|
|
this._message = message;
|
2022-02-02 19:33:51 -07:00
|
|
|
this._isSoft = isSoft;
|
2022-01-31 18:14:59 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
get(target: any, prop: any, receiver: any): any {
|
|
|
|
const value = Reflect.get(target, prop, receiver);
|
|
|
|
if (typeof value !== 'function')
|
|
|
|
return new Proxy(value, this);
|
|
|
|
return (...args: any[]) => {
|
2022-02-02 19:33:51 -07:00
|
|
|
const testInfo = currentTestInfo();
|
|
|
|
if (!testInfo)
|
|
|
|
return value.call(target, ...args);
|
|
|
|
const handleError = (e: Error) => {
|
|
|
|
if (this._isSoft)
|
|
|
|
testInfo._failWithError(serializeError(e), false /* isHardError */);
|
|
|
|
else
|
|
|
|
throw e;
|
|
|
|
};
|
2022-01-31 18:14:59 -07:00
|
|
|
try {
|
|
|
|
expectCallMetaInfo = {
|
|
|
|
message: this._message,
|
2022-02-02 19:33:51 -07:00
|
|
|
isSoft: this._isSoft,
|
2022-01-31 18:14:59 -07:00
|
|
|
};
|
2022-02-02 19:33:51 -07:00
|
|
|
let result = value.call(target, ...args);
|
|
|
|
if ((result instanceof Promise))
|
|
|
|
result = result.catch(handleError);
|
2022-01-31 18:14:59 -07:00
|
|
|
return result;
|
2022-02-02 19:33:51 -07:00
|
|
|
} catch (e) {
|
|
|
|
handleError(e);
|
2022-01-31 18:14:59 -07:00
|
|
|
} finally {
|
|
|
|
expectCallMetaInfo = undefined;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-07-30 16:07:02 -07:00
|
|
|
function wrap(matcherName: string, matcher: any) {
|
2021-09-17 15:24:15 -07:00
|
|
|
const result = function(this: any, ...args: any[]) {
|
2021-07-30 16:07:02 -07:00
|
|
|
const testInfo = currentTestInfo();
|
|
|
|
if (!testInfo)
|
|
|
|
return matcher.call(this, ...args);
|
|
|
|
|
2022-01-31 18:14:59 -07:00
|
|
|
const INTERNAL_STACK_LENGTH = 4;
|
2021-09-28 16:02:34 -07:00
|
|
|
// at Object.__PWTRAP__[expect.toHaveText] (...)
|
|
|
|
// at __EXTERNAL_MATCHER_TRAP__ (...)
|
|
|
|
// at Object.throwingMatcher [as toHaveText] (...)
|
2022-01-31 18:14:59 -07:00
|
|
|
// at Proxy.<anonymous>
|
2021-09-28 16:02:34 -07:00
|
|
|
// at <test function> (...)
|
2021-08-31 16:34:52 -07:00
|
|
|
const stackLines = new Error().stack!.split('\n').slice(INTERNAL_STACK_LENGTH + 1);
|
2021-10-18 20:06:18 -08:00
|
|
|
const frame = stackLines[0] ? stackUtils.parseLine(stackLines[0]) : undefined;
|
2022-01-31 18:14:59 -07:00
|
|
|
const customMessage = expectCallMetaInfo?.message ?? '';
|
2022-02-02 19:33:51 -07:00
|
|
|
const isSoft = expectCallMetaInfo?.isSoft ?? false;
|
2021-09-16 15:51:27 -07:00
|
|
|
const step = testInfo._addStep({
|
2021-10-18 20:06:18 -08:00
|
|
|
location: frame && frame.file ? { file: path.resolve(process.cwd(), frame.file), line: frame.line || 0, column: frame.column || 0 } : undefined,
|
2021-09-16 15:51:27 -07:00
|
|
|
category: 'expect',
|
2022-02-02 19:33:51 -07:00
|
|
|
title: customMessage || `expect${isSoft ? '.soft' : ''}${this.isNot ? '.not' : ''}.${matcherName}`,
|
2021-09-16 15:51:27 -07:00
|
|
|
canHaveChildren: true,
|
|
|
|
forceNoParent: false
|
|
|
|
});
|
2021-08-02 17:17:20 -07:00
|
|
|
|
|
|
|
const reportStepEnd = (result: any) => {
|
2021-08-02 22:11:37 -07:00
|
|
|
const success = result.pass !== this.isNot;
|
|
|
|
let error: TestError | undefined;
|
2021-08-31 16:34:52 -07:00
|
|
|
if (!success) {
|
|
|
|
const message = result.message();
|
|
|
|
error = { message, stack: message + '\n' + stackLines.join('\n') };
|
2022-01-31 18:14:59 -07:00
|
|
|
if (customMessage) {
|
|
|
|
const messageLines = message.split('\n');
|
|
|
|
// Jest adds something like the following error to all errors:
|
|
|
|
// expect(received).toBe(expected); // Object.is equality
|
|
|
|
const uselessMatcherLineIndex = messageLines.findIndex((line: string) => /expect.*\(.*received.*\)/.test(line));
|
|
|
|
if (uselessMatcherLineIndex !== -1) {
|
|
|
|
// if there's a newline after the matcher text, then remove it as well.
|
|
|
|
if (uselessMatcherLineIndex + 1 < messageLines.length && messageLines[uselessMatcherLineIndex + 1].trim() === '')
|
|
|
|
messageLines.splice(uselessMatcherLineIndex, 2);
|
|
|
|
else
|
|
|
|
messageLines.splice(uselessMatcherLineIndex, 1);
|
|
|
|
}
|
|
|
|
const newMessage = [
|
|
|
|
customMessage,
|
|
|
|
'',
|
|
|
|
...messageLines,
|
|
|
|
].join('\n');
|
|
|
|
result.message = () => newMessage;
|
|
|
|
}
|
2021-08-31 16:34:52 -07:00
|
|
|
}
|
2021-09-03 13:08:17 -07:00
|
|
|
step.complete(error);
|
2021-08-02 17:17:20 -07:00
|
|
|
return result;
|
|
|
|
};
|
|
|
|
|
|
|
|
const reportStepError = (error: Error) => {
|
2021-09-03 13:08:17 -07:00
|
|
|
step.complete(serializeError(error));
|
2021-08-02 17:17:20 -07:00
|
|
|
throw error;
|
|
|
|
};
|
|
|
|
|
2021-07-30 16:07:02 -07:00
|
|
|
try {
|
|
|
|
const result = matcher.call(this, ...args);
|
2021-08-02 17:17:20 -07:00
|
|
|
if (result instanceof Promise)
|
|
|
|
return result.then(reportStepEnd).catch(reportStepError);
|
|
|
|
return reportStepEnd(result);
|
2021-07-30 16:07:02 -07:00
|
|
|
} catch (e) {
|
2021-08-02 17:17:20 -07:00
|
|
|
reportStepError(e);
|
2021-07-30 16:07:02 -07:00
|
|
|
}
|
|
|
|
};
|
2021-10-14 19:23:45 -07:00
|
|
|
Object.defineProperty(result, 'name', { value: '__PWTRAP__[expect.' + matcherName + ']' });
|
2021-09-17 15:24:15 -07:00
|
|
|
return result;
|
2021-07-30 16:07:02 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
const wrappedMatchers: any = {};
|
|
|
|
for (const matcherName in matchers)
|
|
|
|
wrappedMatchers[matcherName] = wrap(matcherName, matchers[matcherName]);
|
|
|
|
for (const matcherName in customMatchers)
|
|
|
|
wrappedMatchers[matcherName] = wrap(matcherName, (customMatchers as any)[matcherName]);
|
|
|
|
|
|
|
|
expectLibrary.extend(wrappedMatchers);
|