wire up matcher with parent step
This commit is contained in:
parent
5329341ef2
commit
1b8cd6dd5b
|
|
@ -60,7 +60,7 @@ import {
|
||||||
printReceived,
|
printReceived,
|
||||||
} from '../common/expectBundle';
|
} from '../common/expectBundle';
|
||||||
import { zones } from 'playwright-core/lib/utils';
|
import { zones } from 'playwright-core/lib/utils';
|
||||||
import { TestInfoImpl } from '../worker/testInfo';
|
import { TestInfoImpl, type TestStepInternal } from '../worker/testInfo';
|
||||||
import { ExpectError, isExpectError } from './matcherHint';
|
import { ExpectError, isExpectError } from './matcherHint';
|
||||||
import { toMatchAriaSnapshot } from './toMatchAriaSnapshot';
|
import { toMatchAriaSnapshot } from './toMatchAriaSnapshot';
|
||||||
|
|
||||||
|
|
@ -116,6 +116,8 @@ function qualifiedMatcherName(qualifier: string[], matcherName: string) {
|
||||||
return qualifier.join(':') + '$' + matcherName;
|
return qualifier.join(':') + '$' + matcherName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const testStepInternalSymbol = Symbol('TestStepInternal');
|
||||||
|
|
||||||
function createExpect(info: ExpectMetaInfo, prefix: string[], customMatchers: Record<string, Function>) {
|
function createExpect(info: ExpectMetaInfo, prefix: string[], customMatchers: Record<string, Function>) {
|
||||||
const expectInstance: Expect<{}> = new Proxy(expectLibrary, {
|
const expectInstance: Expect<{}> = new Proxy(expectLibrary, {
|
||||||
apply: function(target: any, thisArg: any, argumentsList: [unknown, ExpectMessage?]) {
|
apply: function(target: any, thisArg: any, argumentsList: [unknown, ExpectMessage?]) {
|
||||||
|
|
@ -141,7 +143,7 @@ function createExpect(info: ExpectMetaInfo, prefix: string[], customMatchers: Re
|
||||||
const wrappedMatchers: any = {};
|
const wrappedMatchers: any = {};
|
||||||
const extendedMatchers: any = { ...customMatchers };
|
const extendedMatchers: any = { ...customMatchers };
|
||||||
for (const [name, matcher] of Object.entries(matchers)) {
|
for (const [name, matcher] of Object.entries(matchers)) {
|
||||||
wrappedMatchers[name] = function(...args: any[]) {
|
wrappedMatchers[name] = function(actual: any, step: TestStepInternal, ...args: any[]) {
|
||||||
const { isNot, promise, utils } = this;
|
const { isNot, promise, utils } = this;
|
||||||
const newThis: ExpectMatcherState = {
|
const newThis: ExpectMatcherState = {
|
||||||
isNot,
|
isNot,
|
||||||
|
|
@ -149,8 +151,9 @@ function createExpect(info: ExpectMetaInfo, prefix: string[], customMatchers: Re
|
||||||
utils,
|
utils,
|
||||||
timeout: currentExpectTimeout()
|
timeout: currentExpectTimeout()
|
||||||
};
|
};
|
||||||
|
(newThis as any)[testStepInternalSymbol] = step;
|
||||||
(newThis as any).equals = throwUnsupportedExpectMatcherError;
|
(newThis as any).equals = throwUnsupportedExpectMatcherError;
|
||||||
return (matcher as any).call(newThis, ...args);
|
return (matcher as any).call(newThis, actual, ...args);
|
||||||
};
|
};
|
||||||
const key = qualifiedMatcherName(qualifier, name);
|
const key = qualifiedMatcherName(qualifier, name);
|
||||||
wrappedMatchers[key] = wrappedMatchers[name];
|
wrappedMatchers[key] = wrappedMatchers[name];
|
||||||
|
|
@ -301,7 +304,7 @@ class ExpectMetaInfoProxyHandler implements ProxyHandler<any> {
|
||||||
// We assume that the matcher will read the current expect timeout the first thing.
|
// We assume that the matcher will read the current expect timeout the first thing.
|
||||||
setCurrentExpectConfigureTimeout(this._info.timeout);
|
setCurrentExpectConfigureTimeout(this._info.timeout);
|
||||||
if (!testInfo)
|
if (!testInfo)
|
||||||
return matcher.call(target, ...args);
|
return matcher.call(target, undefined, ...args);
|
||||||
|
|
||||||
const customMessage = this._info.message || '';
|
const customMessage = this._info.message || '';
|
||||||
const argsSuffix = computeArgsSuffix(matcherName, args);
|
const argsSuffix = computeArgsSuffix(matcherName, args);
|
||||||
|
|
@ -337,7 +340,7 @@ class ExpectMetaInfoProxyHandler implements ProxyHandler<any> {
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const callback = () => matcher.call(target, ...args);
|
const callback = () => matcher.call(target, step, ...args);
|
||||||
// toPass and poll matchers can contain other steps, expects and API calls,
|
// toPass and poll matchers can contain other steps, expects and API calls,
|
||||||
// so they behave like a retriable step.
|
// so they behave like a retriable step.
|
||||||
const result = (matcherName === 'toPass' || this._info.poll) ?
|
const result = (matcherName === 'toPass' || this._info.poll) ?
|
||||||
|
|
|
||||||
|
|
@ -29,10 +29,11 @@ import { colors } from 'playwright-core/lib/utilsBundle';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { mime } from 'playwright-core/lib/utilsBundle';
|
import { mime } from 'playwright-core/lib/utilsBundle';
|
||||||
import type { TestInfoImpl } from '../worker/testInfo';
|
import type { TestInfoImpl, TestStepInternal } from '../worker/testInfo';
|
||||||
import type { ExpectMatcherState } from '../../types/test';
|
import type { ExpectMatcherState } from '../../types/test';
|
||||||
import type { MatcherResult } from './matcherHint';
|
import type { MatcherResult } from './matcherHint';
|
||||||
import type { FullProjectInternal } from '../common/config';
|
import type { FullProjectInternal } from '../common/config';
|
||||||
|
import { testStepInternalSymbol } from './expect';
|
||||||
|
|
||||||
type NameOrSegments = string | string[];
|
type NameOrSegments = string | string[];
|
||||||
const snapshotNamesSymbol = Symbol('snapshotNames');
|
const snapshotNamesSymbol = Symbol('snapshotNames');
|
||||||
|
|
@ -75,6 +76,7 @@ const NonConfigProperties: (keyof ToHaveScreenshotOptions)[] = [
|
||||||
|
|
||||||
class SnapshotHelper {
|
class SnapshotHelper {
|
||||||
readonly testInfo: TestInfoImpl;
|
readonly testInfo: TestInfoImpl;
|
||||||
|
readonly step: TestStepInternal;
|
||||||
readonly attachmentBaseName: string;
|
readonly attachmentBaseName: string;
|
||||||
readonly legacyExpectedPath: string;
|
readonly legacyExpectedPath: string;
|
||||||
readonly previousPath: string;
|
readonly previousPath: string;
|
||||||
|
|
@ -91,6 +93,7 @@ class SnapshotHelper {
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
testInfo: TestInfoImpl,
|
testInfo: TestInfoImpl,
|
||||||
|
step: TestStepInternal,
|
||||||
matcherName: string,
|
matcherName: string,
|
||||||
locator: Locator | undefined,
|
locator: Locator | undefined,
|
||||||
anonymousSnapshotExtension: string,
|
anonymousSnapshotExtension: string,
|
||||||
|
|
@ -182,6 +185,7 @@ class SnapshotHelper {
|
||||||
this.comparator = getComparator(this.mimeType);
|
this.comparator = getComparator(this.mimeType);
|
||||||
|
|
||||||
this.testInfo = testInfo;
|
this.testInfo = testInfo;
|
||||||
|
this.step = step;
|
||||||
this.kind = this.mimeType.startsWith('image/') ? 'Screenshot' : 'Snapshot';
|
this.kind = this.mimeType.startsWith('image/') ? 'Screenshot' : 'Snapshot';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -224,9 +228,9 @@ class SnapshotHelper {
|
||||||
const isWriteMissingMode = this.updateSnapshots === 'all' || this.updateSnapshots === 'missing';
|
const isWriteMissingMode = this.updateSnapshots === 'all' || this.updateSnapshots === 'missing';
|
||||||
if (isWriteMissingMode)
|
if (isWriteMissingMode)
|
||||||
writeFileSync(this.expectedPath, actual);
|
writeFileSync(this.expectedPath, actual);
|
||||||
this.testInfo.attachments.push({ name: addSuffixToFilePath(this.attachmentBaseName, '-expected'), contentType: this.mimeType, path: this.expectedPath });
|
this.step.attachments.push({ name: addSuffixToFilePath(this.attachmentBaseName, '-expected'), contentType: this.mimeType, path: this.expectedPath });
|
||||||
writeFileSync(this.actualPath, actual);
|
writeFileSync(this.actualPath, actual);
|
||||||
this.testInfo.attachments.push({ name: addSuffixToFilePath(this.attachmentBaseName, '-actual'), contentType: this.mimeType, path: this.actualPath });
|
this.step.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.' : '.'}`;
|
const message = `A snapshot doesn't exist at ${this.expectedPath}${isWriteMissingMode ? ', writing actual.' : '.'}`;
|
||||||
if (this.updateSnapshots === 'all') {
|
if (this.updateSnapshots === 'all') {
|
||||||
/* eslint-disable no-console */
|
/* eslint-disable no-console */
|
||||||
|
|
@ -260,22 +264,22 @@ class SnapshotHelper {
|
||||||
// Copy the expectation inside the `test-results/` folder for backwards compatibility,
|
// 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.
|
// so that one can upload `test-results/` directory and have all the data inside.
|
||||||
writeFileSync(this.legacyExpectedPath, expected);
|
writeFileSync(this.legacyExpectedPath, expected);
|
||||||
this.testInfo.attachments.push({ name: addSuffixToFilePath(this.attachmentBaseName, '-expected'), contentType: this.mimeType, path: this.expectedPath });
|
this.step.attachments.push({ name: addSuffixToFilePath(this.attachmentBaseName, '-expected'), contentType: this.mimeType, path: this.expectedPath });
|
||||||
output.push(`\nExpected: ${colors.yellow(this.expectedPath)}`);
|
output.push(`\nExpected: ${colors.yellow(this.expectedPath)}`);
|
||||||
}
|
}
|
||||||
if (previous !== undefined) {
|
if (previous !== undefined) {
|
||||||
writeFileSync(this.previousPath, previous);
|
writeFileSync(this.previousPath, previous);
|
||||||
this.testInfo.attachments.push({ name: addSuffixToFilePath(this.attachmentBaseName, '-previous'), contentType: this.mimeType, path: this.previousPath });
|
this.step.attachments.push({ name: addSuffixToFilePath(this.attachmentBaseName, '-previous'), contentType: this.mimeType, path: this.previousPath });
|
||||||
output.push(`Previous: ${colors.yellow(this.previousPath)}`);
|
output.push(`Previous: ${colors.yellow(this.previousPath)}`);
|
||||||
}
|
}
|
||||||
if (actual !== undefined) {
|
if (actual !== undefined) {
|
||||||
writeFileSync(this.actualPath, actual);
|
writeFileSync(this.actualPath, actual);
|
||||||
this.testInfo.attachments.push({ name: addSuffixToFilePath(this.attachmentBaseName, '-actual'), contentType: this.mimeType, path: this.actualPath });
|
this.step.attachments.push({ name: addSuffixToFilePath(this.attachmentBaseName, '-actual'), contentType: this.mimeType, path: this.actualPath });
|
||||||
output.push(`Received: ${colors.yellow(this.actualPath)}`);
|
output.push(`Received: ${colors.yellow(this.actualPath)}`);
|
||||||
}
|
}
|
||||||
if (diff !== undefined) {
|
if (diff !== undefined) {
|
||||||
writeFileSync(this.diffPath, diff);
|
writeFileSync(this.diffPath, diff);
|
||||||
this.testInfo.attachments.push({ name: addSuffixToFilePath(this.attachmentBaseName, '-diff'), contentType: this.mimeType, path: this.diffPath });
|
this.step.attachments.push({ name: addSuffixToFilePath(this.attachmentBaseName, '-diff'), contentType: this.mimeType, path: this.diffPath });
|
||||||
output.push(` Diff: ${colors.yellow(this.diffPath)}`);
|
output.push(` Diff: ${colors.yellow(this.diffPath)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -299,7 +303,8 @@ export function toMatchSnapshot(
|
||||||
optOptions: ImageComparatorOptions = {}
|
optOptions: ImageComparatorOptions = {}
|
||||||
): MatcherResult<NameOrSegments | { name?: NameOrSegments }, string> {
|
): MatcherResult<NameOrSegments | { name?: NameOrSegments }, string> {
|
||||||
const testInfo = currentTestInfo();
|
const testInfo = currentTestInfo();
|
||||||
if (!testInfo)
|
const step = (this as any)[testStepInternalSymbol] as TestStepInternal | undefined;
|
||||||
|
if (!testInfo || !step)
|
||||||
throw new Error(`toMatchSnapshot() must be called during the test`);
|
throw new Error(`toMatchSnapshot() must be called during the test`);
|
||||||
if (received instanceof Promise)
|
if (received instanceof Promise)
|
||||||
throw new Error('An unresolved Promise was passed to toMatchSnapshot(), make sure to resolve it by adding await to it.');
|
throw new Error('An unresolved Promise was passed to toMatchSnapshot(), make sure to resolve it by adding await to it.');
|
||||||
|
|
@ -309,7 +314,7 @@ export function toMatchSnapshot(
|
||||||
|
|
||||||
const configOptions = testInfo._projectInternal.expect?.toMatchSnapshot || {};
|
const configOptions = testInfo._projectInternal.expect?.toMatchSnapshot || {};
|
||||||
const helper = new SnapshotHelper(
|
const helper = new SnapshotHelper(
|
||||||
testInfo, 'toMatchSnapshot', undefined, determineFileExtension(received),
|
testInfo, step, 'toMatchSnapshot', undefined, determineFileExtension(received),
|
||||||
configOptions, nameOrOptions, optOptions);
|
configOptions, nameOrOptions, optOptions);
|
||||||
|
|
||||||
if (this.isNot) {
|
if (this.isNot) {
|
||||||
|
|
@ -356,7 +361,8 @@ export async function toHaveScreenshot(
|
||||||
optOptions: ToHaveScreenshotOptions = {}
|
optOptions: ToHaveScreenshotOptions = {}
|
||||||
): Promise<MatcherResult<NameOrSegments | { name?: NameOrSegments }, string>> {
|
): Promise<MatcherResult<NameOrSegments | { name?: NameOrSegments }, string>> {
|
||||||
const testInfo = currentTestInfo();
|
const testInfo = currentTestInfo();
|
||||||
if (!testInfo)
|
const step = (this as any)[testStepInternalSymbol] as TestStepInternal | undefined;
|
||||||
|
if (!testInfo || !step)
|
||||||
throw new Error(`toHaveScreenshot() must be called during the test`);
|
throw new Error(`toHaveScreenshot() must be called during the test`);
|
||||||
|
|
||||||
if (testInfo._projectInternal.ignoreSnapshots)
|
if (testInfo._projectInternal.ignoreSnapshots)
|
||||||
|
|
@ -365,7 +371,7 @@ export async function toHaveScreenshot(
|
||||||
expectTypes(pageOrLocator, ['Page', 'Locator'], 'toHaveScreenshot');
|
expectTypes(pageOrLocator, ['Page', 'Locator'], 'toHaveScreenshot');
|
||||||
const [page, locator] = pageOrLocator.constructor.name === 'Page' ? [(pageOrLocator as PageEx), undefined] : [(pageOrLocator as Locator).page() as PageEx, pageOrLocator as Locator];
|
const [page, locator] = pageOrLocator.constructor.name === 'Page' ? [(pageOrLocator as PageEx), undefined] : [(pageOrLocator as Locator).page() as PageEx, pageOrLocator as Locator];
|
||||||
const configOptions = testInfo._projectInternal.expect?.toHaveScreenshot || {};
|
const configOptions = testInfo._projectInternal.expect?.toHaveScreenshot || {};
|
||||||
const helper = new SnapshotHelper(testInfo, 'toHaveScreenshot', locator, 'png', configOptions, nameOrOptions, optOptions);
|
const helper = new SnapshotHelper(testInfo, step, 'toHaveScreenshot', locator, 'png', configOptions, nameOrOptions, optOptions);
|
||||||
if (!helper.expectedPath.toLowerCase().endsWith('.png'))
|
if (!helper.expectedPath.toLowerCase().endsWith('.png'))
|
||||||
throw new Error(`Screenshot name "${path.basename(helper.expectedPath)}" must have '.png' extension`);
|
throw new Error(`Screenshot name "${path.basename(helper.expectedPath)}" must have '.png' extension`);
|
||||||
expectTypes(pageOrLocator, ['Page', 'Locator'], 'toHaveScreenshot');
|
expectTypes(pageOrLocator, ['Page', 'Locator'], 'toHaveScreenshot');
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue