350 lines
15 KiB
JavaScript
350 lines
15 KiB
JavaScript
"use strict";
|
|
|
|
Object.defineProperty(exports, "__esModule", {
|
|
value: true
|
|
});
|
|
exports.expect = void 0;
|
|
exports.mergeExpects = mergeExpects;
|
|
exports.printReceivedStringContainExpectedSubstring = exports.printReceivedStringContainExpectedResult = void 0;
|
|
var _utils = require("playwright-core/lib/utils");
|
|
var _matchers = require("./matchers");
|
|
var _toMatchSnapshot = require("./toMatchSnapshot");
|
|
var _globals = require("../common/globals");
|
|
var _util = require("../util");
|
|
var _expectBundle = require("../common/expectBundle");
|
|
var _testInfo = require("../worker/testInfo");
|
|
var _matcherHint = require("./matcherHint");
|
|
var _toMatchAriaSnapshot = require("./toMatchAriaSnapshot");
|
|
/**
|
|
* 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.
|
|
*/
|
|
|
|
// #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 => val.replace(/"|\\/g, '\\$&');
|
|
const printReceivedStringContainExpectedSubstring = (received, start, length // not end
|
|
) => (0, _expectBundle.RECEIVED_COLOR)('"' + printSubstring(received.slice(0, start)) + (0, _expectBundle.INVERTED_COLOR)(printSubstring(received.slice(start, start + length))) + printSubstring(received.slice(start + length)) + '"');
|
|
exports.printReceivedStringContainExpectedSubstring = printReceivedStringContainExpectedSubstring;
|
|
const printReceivedStringContainExpectedResult = (received, result) => result === null ? (0, _expectBundle.printReceived)(received) : printReceivedStringContainExpectedSubstring(received, result.index, result[0].length);
|
|
|
|
// #endregion
|
|
exports.printReceivedStringContainExpectedResult = printReceivedStringContainExpectedResult;
|
|
function createMatchers(actual, info, prefix) {
|
|
return new Proxy((0, _expectBundle.expect)(actual), new ExpectMetaInfoProxyHandler(info, prefix));
|
|
}
|
|
const getCustomMatchersSymbol = Symbol('get custom matchers');
|
|
function qualifiedMatcherName(qualifier, matcherName) {
|
|
return qualifier.join(':') + '$' + matcherName;
|
|
}
|
|
function createExpect(info, prefix, customMatchers) {
|
|
const expectInstance = new Proxy(_expectBundle.expect, {
|
|
apply: function (target, thisArg, argumentsList) {
|
|
const [actual, messageOrOptions] = argumentsList;
|
|
const message = (0, _utils.isString)(messageOrOptions) ? messageOrOptions : (messageOrOptions === null || messageOrOptions === void 0 ? void 0 : 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;
|
|
}
|
|
return createMatchers(actual, newInfo, prefix);
|
|
},
|
|
get: function (target, property) {
|
|
if (property === 'configure') return configure;
|
|
if (property === 'extend') {
|
|
return matchers => {
|
|
const qualifier = [...prefix, (0, _utils.createGuid)()];
|
|
const wrappedMatchers = {};
|
|
const extendedMatchers = {
|
|
...customMatchers
|
|
};
|
|
for (const [name, matcher] of Object.entries(matchers)) {
|
|
wrappedMatchers[name] = function (...args) {
|
|
const {
|
|
isNot,
|
|
promise,
|
|
utils
|
|
} = this;
|
|
const newThis = {
|
|
isNot,
|
|
promise,
|
|
utils,
|
|
timeout: currentExpectTimeout()
|
|
};
|
|
newThis.equals = throwUnsupportedExpectMatcherError;
|
|
return matcher.call(newThis, ...args);
|
|
};
|
|
const key = qualifiedMatcherName(qualifier, name);
|
|
wrappedMatchers[key] = wrappedMatchers[name];
|
|
Object.defineProperty(wrappedMatchers[key], 'name', {
|
|
value: name
|
|
});
|
|
extendedMatchers[name] = wrappedMatchers[key];
|
|
}
|
|
_expectBundle.expect.extend(wrappedMatchers);
|
|
return createExpect(info, qualifier, extendedMatchers);
|
|
};
|
|
}
|
|
if (property === 'soft') {
|
|
return (actual, messageOrOptions) => {
|
|
return configure({
|
|
soft: true
|
|
})(actual, messageOrOptions);
|
|
};
|
|
}
|
|
if (property === getCustomMatchersSymbol) return customMatchers;
|
|
if (property === 'poll') {
|
|
return (actual, messageOrOptions) => {
|
|
const poll = (0, _utils.isString)(messageOrOptions) ? {} : messageOrOptions || {};
|
|
return configure({
|
|
_poll: poll
|
|
})(actual, messageOrOptions);
|
|
};
|
|
}
|
|
return _expectBundle.expect[property];
|
|
}
|
|
});
|
|
const configure = configuration => {
|
|
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') {
|
|
var _configuration$_poll$, _configuration$_poll$2;
|
|
newInfo.poll.timeout = (_configuration$_poll$ = configuration._poll.timeout) !== null && _configuration$_poll$ !== void 0 ? _configuration$_poll$ : newInfo.poll.timeout;
|
|
newInfo.poll.intervals = (_configuration$_poll$2 = configuration._poll.intervals) !== null && _configuration$_poll$2 !== void 0 ? _configuration$_poll$2 : newInfo.poll.intervals;
|
|
}
|
|
}
|
|
return createExpect(newInfo, prefix, customMatchers);
|
|
};
|
|
return expectInstance;
|
|
}
|
|
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');
|
|
}
|
|
_expectBundle.expect.setState({
|
|
expand: false
|
|
});
|
|
const customAsyncMatchers = {
|
|
toBeAttached: _matchers.toBeAttached,
|
|
toBeChecked: _matchers.toBeChecked,
|
|
toBeDisabled: _matchers.toBeDisabled,
|
|
toBeEditable: _matchers.toBeEditable,
|
|
toBeEmpty: _matchers.toBeEmpty,
|
|
toBeEnabled: _matchers.toBeEnabled,
|
|
toBeFocused: _matchers.toBeFocused,
|
|
toBeHidden: _matchers.toBeHidden,
|
|
toBeInViewport: _matchers.toBeInViewport,
|
|
toBeOK: _matchers.toBeOK,
|
|
toBeVisible: _matchers.toBeVisible,
|
|
toContainText: _matchers.toContainText,
|
|
toHaveAccessibleDescription: _matchers.toHaveAccessibleDescription,
|
|
toHaveAccessibleName: _matchers.toHaveAccessibleName,
|
|
toHaveAttribute: _matchers.toHaveAttribute,
|
|
toHaveClass: _matchers.toHaveClass,
|
|
toHaveCount: _matchers.toHaveCount,
|
|
toHaveCSS: _matchers.toHaveCSS,
|
|
toHaveId: _matchers.toHaveId,
|
|
toHaveJSProperty: _matchers.toHaveJSProperty,
|
|
toHaveRole: _matchers.toHaveRole,
|
|
toHaveText: _matchers.toHaveText,
|
|
toHaveTitle: _matchers.toHaveTitle,
|
|
toHaveURL: _matchers.toHaveURL,
|
|
toHaveValue: _matchers.toHaveValue,
|
|
toHaveValues: _matchers.toHaveValues,
|
|
toHaveScreenshot: _toMatchSnapshot.toHaveScreenshot,
|
|
toMatchAriaSnapshot: _toMatchAriaSnapshot.toMatchAriaSnapshot,
|
|
toPass: _matchers.toPass
|
|
};
|
|
const customMatchers = {
|
|
...customAsyncMatchers,
|
|
toMatchSnapshot: _toMatchSnapshot.toMatchSnapshot
|
|
};
|
|
class ExpectMetaInfoProxyHandler {
|
|
constructor(info, prefix) {
|
|
this._info = void 0;
|
|
this._prefix = void 0;
|
|
this._info = {
|
|
...info
|
|
};
|
|
this._prefix = prefix;
|
|
}
|
|
get(target, matcherName, receiver) {
|
|
let matcher = Reflect.get(target, matcherName, receiver);
|
|
if (typeof matcherName !== 'string') return matcher;
|
|
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;
|
|
}
|
|
}
|
|
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);
|
|
}
|
|
if (this._info.poll) {
|
|
if (customAsyncMatchers[matcherName] || matcherName === 'resolves' || matcherName === 'rejects') throw new Error(`\`expect.poll()\` does not support "${matcherName}" matcher.`);
|
|
matcher = (...args) => pollMatcher(resolvedMatcherName, this._info, this._prefix, ...args);
|
|
}
|
|
return (...args) => {
|
|
const testInfo = (0, _globals.currentTestInfo)();
|
|
// We assume that the matcher will read the current expect timeout the first thing.
|
|
setCurrentExpectConfigureTimeout(this._info.timeout);
|
|
if (!testInfo) return matcher.call(target, ...args);
|
|
const customMessage = this._info.message || '';
|
|
const argsSuffix = computeArgsSuffix(matcherName, args);
|
|
const defaultTitle = `expect${this._info.poll ? '.poll' : ''}${this._info.isSoft ? '.soft' : ''}${this._info.isNot ? '.not' : ''}.${matcherName}${argsSuffix}`;
|
|
const title = customMessage || defaultTitle;
|
|
|
|
// 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 = (0, _util.filteredStackTrace)((0, _utils.captureRawStack)());
|
|
|
|
// Enclose toPass in a step to maintain async stacks, toPass matcher is always async.
|
|
const stepInfo = {
|
|
category: 'expect',
|
|
title: (0, _util.trimLongString)(title, 1024),
|
|
params: args[0] ? {
|
|
expected: args[0]
|
|
} : undefined,
|
|
infectParentStepsWithError: this._info.isSoft
|
|
};
|
|
const step = testInfo._addStep(stepInfo);
|
|
const reportStepError = e => {
|
|
const jestError = (0, _matcherHint.isJestError)(e) ? e : null;
|
|
const error = jestError ? new _matcherHint.ExpectError(jestError, customMessage, stackFrames) : e;
|
|
if (jestError !== null && jestError !== void 0 && jestError.matcherResult.suggestedRebaseline) {
|
|
step.complete({
|
|
suggestedRebaseline: jestError === null || jestError === void 0 ? void 0 : jestError.matcherResult.suggestedRebaseline
|
|
});
|
|
return;
|
|
}
|
|
step.complete({
|
|
error
|
|
});
|
|
if (this._info.isSoft) testInfo._failWithError(error);else throw error;
|
|
};
|
|
const finalizer = () => {
|
|
step.complete({});
|
|
};
|
|
try {
|
|
const callback = () => matcher.call(target, ...args);
|
|
// toPass and poll matchers can contain other steps, expects and API calls,
|
|
// so they behave like a retriable step.
|
|
const result = matcherName === 'toPass' || this._info.poll ? _utils.zones.run('stepZone', step, callback) : _utils.zones.run('expectZone', {
|
|
title,
|
|
stepId: step.stepId
|
|
}, callback);
|
|
if (result instanceof Promise) return result.then(finalizer).catch(reportStepError);
|
|
finalizer();
|
|
return result;
|
|
} catch (e) {
|
|
reportStepError(e);
|
|
}
|
|
};
|
|
}
|
|
}
|
|
async function pollMatcher(qualifiedMatcherName, info, prefix, ...args) {
|
|
var _poll$timeout, _poll$intervals;
|
|
const testInfo = (0, _globals.currentTestInfo)();
|
|
const poll = info.poll;
|
|
const timeout = (_poll$timeout = poll.timeout) !== null && _poll$timeout !== void 0 ? _poll$timeout : currentExpectTimeout();
|
|
const {
|
|
deadline,
|
|
timeoutMessage
|
|
} = testInfo ? testInfo._deadlineForMatcher(timeout) : _testInfo.TestInfoImpl._defaultDeadlineForMatcher(timeout);
|
|
const result = await (0, _utils.pollAgainstDeadline)(async () => {
|
|
if (testInfo && (0, _globals.currentTestInfo)() !== testInfo) return {
|
|
continuePolling: false,
|
|
result: undefined
|
|
};
|
|
const innerInfo = {
|
|
...info,
|
|
isSoft: false,
|
|
// soft is outside of poll, not inside
|
|
poll: undefined
|
|
};
|
|
const value = await poll.generator();
|
|
try {
|
|
let matchers = createMatchers(value, innerInfo, prefix);
|
|
if (info.isNot) matchers = matchers.not;
|
|
matchers[qualifiedMatcherName](...args);
|
|
return {
|
|
continuePolling: false,
|
|
result: undefined
|
|
};
|
|
} catch (error) {
|
|
return {
|
|
continuePolling: true,
|
|
result: error
|
|
};
|
|
}
|
|
}, deadline, (_poll$intervals = poll.intervals) !== null && _poll$intervals !== void 0 ? _poll$intervals : [100, 250, 500, 1000]);
|
|
if (result.timedOut) {
|
|
const message = result.result ? [result.result.message, '', `Call Log:`, `- ${timeoutMessage}`].join('\n') : timeoutMessage;
|
|
throw new Error(message);
|
|
}
|
|
}
|
|
let currentExpectConfigureTimeout;
|
|
function setCurrentExpectConfigureTimeout(timeout) {
|
|
currentExpectConfigureTimeout = timeout;
|
|
}
|
|
function currentExpectTimeout() {
|
|
var _testInfo$_projectInt;
|
|
if (currentExpectConfigureTimeout !== undefined) return currentExpectConfigureTimeout;
|
|
const testInfo = (0, _globals.currentTestInfo)();
|
|
let defaultExpectTimeout = testInfo === null || testInfo === void 0 || (_testInfo$_projectInt = testInfo._projectInternal) === null || _testInfo$_projectInt === void 0 || (_testInfo$_projectInt = _testInfo$_projectInt.expect) === null || _testInfo$_projectInt === void 0 ? void 0 : _testInfo$_projectInt.timeout;
|
|
if (typeof defaultExpectTimeout === 'undefined') defaultExpectTimeout = 5000;
|
|
return defaultExpectTimeout;
|
|
}
|
|
function computeArgsSuffix(matcherName, args) {
|
|
let value = '';
|
|
if (matcherName === 'toHaveScreenshot') value = (0, _toMatchSnapshot.toHaveScreenshotStepTitle)(...args);
|
|
return value ? `(${value})` : '';
|
|
}
|
|
const expect = exports.expect = createExpect({}, [], {}).extend(customMatchers);
|
|
function mergeExpects(...expects) {
|
|
let merged = expect;
|
|
for (const e of expects) {
|
|
const internals = e[getCustomMatchersSymbol];
|
|
if (!internals)
|
|
// non-playwright expects mutate the global expect, so we don't need to do anything special
|
|
continue;
|
|
merged = merged.extend(internals);
|
|
}
|
|
return merged;
|
|
} |