Skip to content

Commit 2a5b161

Browse files
committed
chore: reduce dependencies from expect matchers to the rest
1 parent 31d901f commit 2a5b161

14 files changed

Lines changed: 114 additions & 74 deletions

File tree

packages/playwright/src/common/DEPS.list

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,5 @@
77
[testType.ts]
88
../matchers/expect.ts
99

10+
[globals.ts]
11+
../matchers/expect.ts

packages/playwright/src/common/globals.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,18 @@
1414
* limitations under the License.
1515
*/
1616

17+
import { setExpectConfig } from '../matchers/expect';
18+
1719
import type { Suite } from './test';
1820
import type { TestInfoImpl } from '../worker/testInfo';
1921

2022
let currentTestInfoValue: TestInfoImpl | null = null;
2123
export function setCurrentTestInfo(testInfo: TestInfoImpl | null) {
2224
currentTestInfoValue = testInfo;
25+
setExpectConfig({
26+
testInfo: testInfo ?? undefined,
27+
...testInfo?._projectInternal.expect,
28+
});
2329
}
2430
export function currentTestInfo(): TestInfoImpl | null {
2531
return currentTestInfoValue;
Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
11
[*]
2-
../common/
3-
../mcp/test/browserBackend.ts
2+
../common/expectBundle.ts
43
../util.ts
5-
../utilsBundle.ts
64
../worker/testInfo.ts

packages/playwright/src/matchers/expect.ts

Lines changed: 46 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import {
2525
import { ExpectError, isJestError } from './matcherHint';
2626
import {
2727
computeMatcherTitleSuffix,
28+
defaultDeadlineForMatcher,
2829
toBeAttached,
2930
toBeChecked,
3031
toBeDisabled,
@@ -60,13 +61,47 @@ import { toHaveScreenshot, toMatchSnapshot } from './toMatchSnapshot';
6061
import {
6162
expect as expectLibrary,
6263
} from '../common/expectBundle';
63-
import { currentTestInfo } from '../common/globals';
6464
import { filteredStackTrace } from '../util';
65-
import { TestInfoImpl } from '../worker/testInfo';
6665

6766
import type { ExpectMatcherStateInternal } from './matchers';
6867
import type { Expect } from '../../types/test';
69-
import type { TestStepInfoImpl } from '../worker/testInfo';
68+
import type { TestInfoImpl, TestStepInfoImpl } from '../worker/testInfo';
69+
70+
export type ExpectConfig = {
71+
testInfo?: TestInfoImpl;
72+
timeout?: number;
73+
toHaveScreenshot?: {
74+
threshold?: number;
75+
maxDiffPixels?: number;
76+
maxDiffPixelRatio?: number;
77+
animations?: 'allow'|'disabled';
78+
caret?: 'hide'|'initial';
79+
scale?: 'css'|'device';
80+
stylePath?: string|Array<string>;
81+
pathTemplate?: string;
82+
};
83+
toMatchAriaSnapshot?: {
84+
pathTemplate?: string;
85+
children?: 'contain'|'equal'|'deep-equal';
86+
};
87+
toMatchSnapshot?: {
88+
threshold?: number;
89+
maxDiffPixels?: number;
90+
maxDiffPixelRatio?: number;
91+
};
92+
toPass?: {
93+
timeout?: number;
94+
intervals?: Array<number>;
95+
};
96+
};
97+
98+
let currentConfig: ExpectConfig = {};
99+
export function setExpectConfig(config: ExpectConfig) {
100+
currentConfig = config;
101+
}
102+
export function expectConfig(): ExpectConfig {
103+
return currentConfig;
104+
}
70105

71106
type ExpectMessage = string | { message?: string };
72107

@@ -158,7 +193,6 @@ function createExpect(info: ExpectMetaInfo, prefix: string[], userMatchers: Reco
158193
// Rely on sync call sequence to seed each matcher call with the context.
159194
type MatcherCallContext = {
160195
expectInfo: ExpectMetaInfo;
161-
testInfo: TestInfoImpl | null;
162196
step?: TestStepInfoImpl;
163197
};
164198

@@ -184,7 +218,7 @@ function wrapPlaywrightMatcherToPassNiceThis(matcher: any) {
184218
return function(this: any, ...args: any[]) {
185219
const { isNot, promise, utils } = this;
186220
const context = takeMatcherCallContext();
187-
const timeout = context?.expectInfo.timeout ?? context?.testInfo?._projectInternal?.expect?.timeout ?? defaultExpectTimeout;
221+
const timeout = context?.expectInfo.timeout ?? expectConfig().timeout ?? defaultExpectTimeout;
188222
const newThis: ExpectMatcherStateInternal = {
189223
isNot,
190224
promise,
@@ -297,8 +331,8 @@ class ExpectMetaInfoProxyHandler implements ProxyHandler<any> {
297331
matcher = (...args: any[]) => pollMatcher(resolvedMatcherName, this._info, this._prefix, ...args);
298332
}
299333
return (...args: any[]) => {
300-
const testInfo = currentTestInfo();
301-
setMatcherCallContext({ expectInfo: this._info, testInfo });
334+
const testInfo = expectConfig().testInfo;
335+
setMatcherCallContext({ expectInfo: this._info });
302336
if (!testInfo)
303337
return matcher.call(target, ...args);
304338

@@ -350,7 +384,7 @@ class ExpectMetaInfoProxyHandler implements ProxyHandler<any> {
350384
};
351385

352386
try {
353-
setMatcherCallContext({ expectInfo: this._info, testInfo, step: step.info });
387+
setMatcherCallContext({ expectInfo: this._info, step: step.info });
354388
const callback = () => matcher.call(target, ...args);
355389
const result = currentZone().with('stepZone', step).run(callback);
356390
if (result instanceof Promise)
@@ -365,13 +399,13 @@ class ExpectMetaInfoProxyHandler implements ProxyHandler<any> {
365399
}
366400

367401
async function pollMatcher(qualifiedMatcherName: string, info: ExpectMetaInfo, prefix: string[], ...args: any[]) {
368-
const testInfo = currentTestInfo();
402+
const config = expectConfig();
369403
const poll = info.poll!;
370-
const timeout = poll.timeout ?? info.timeout ?? testInfo?._projectInternal?.expect?.timeout ?? defaultExpectTimeout;
371-
const { deadline, timeoutMessage } = testInfo ? testInfo._deadlineForMatcher(timeout) : TestInfoImpl._defaultDeadlineForMatcher(timeout);
404+
const timeout = poll.timeout ?? info.timeout ?? config.timeout ?? defaultExpectTimeout;
405+
const { deadline, timeoutMessage } = config.testInfo ? config.testInfo._deadlineForMatcher(timeout) : defaultDeadlineForMatcher(timeout);
372406

373407
const result = await pollAgainstDeadline<Error|undefined>(async () => {
374-
if (testInfo && currentTestInfo() !== testInfo)
408+
if (config && expectConfig() !== config)
375409
return { continuePolling: false, result: undefined };
376410

377411
const innerInfo: ExpectMetaInfo = {

packages/playwright/src/matchers/matcherHint.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,3 +56,12 @@ export class ExpectError extends Error {
5656
export function isJestError(e: unknown): e is JestError {
5757
return e instanceof Error && 'matcherResult' in e && !!e.matcherResult;
5858
}
59+
60+
export function expectTypes(receiver: any, types: string[], matcherName: string) {
61+
if (typeof receiver !== 'object' || !types.includes(receiver.constructor.name)) {
62+
const commaSeparated = types.slice();
63+
const lastType = commaSeparated.pop();
64+
const typesString = commaSeparated.length ? commaSeparated.join(', ') + ' or ' + lastType : lastType;
65+
throw new Error(`${matcherName} can be only used with ${typesString} object${types.length > 1 ? 's' : ''}`);
66+
}
67+
}

packages/playwright/src/matchers/matchers.ts

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,19 +14,16 @@
1414
* limitations under the License.
1515
*/
1616

17-
import { asLocatorDescription, constructURLBasedOnBaseURL, isRegExp, isString, isTextualMimeType, isURLPattern, pollAgainstDeadline, serializeExpectedTextValues, formatMatcherMessage } from 'playwright-core/lib/utils';
17+
import { asLocatorDescription, constructURLBasedOnBaseURL, isRegExp, isString, isTextualMimeType, isURLPattern, pollAgainstDeadline, serializeExpectedTextValues, formatMatcherMessage, monotonicTime } from 'playwright-core/lib/utils';
1818
import { colors } from 'playwright-core/lib/utils';
1919

20-
import { expectTypes } from '../util';
2120
import { toBeTruthy } from './toBeTruthy';
2221
import { toEqual } from './toEqual';
2322
import { toHaveURLWithPredicate } from './toHaveURL';
2423
import { toMatchText } from './toMatchText';
2524
import { toHaveScreenshotStepTitle } from './toMatchSnapshot';
26-
import { takeFirst } from '../common/config';
27-
import { currentTestInfo } from '../common/globals';
28-
import { TestInfoImpl } from '../worker/testInfo';
29-
import { MatcherResult } from './matcherHint';
25+
import { expectConfig } from './expect';
26+
import { expectTypes, MatcherResult } from './matcherHint';
3027

3128
import type { ExpectMatcherState } from '../../types/test';
3229
import type { TestStepInfoImpl } from '../worker/testInfo';
@@ -476,13 +473,13 @@ export async function toPass(
476473
timeout?: number,
477474
} = {},
478475
) {
479-
const testInfo = currentTestInfo();
480-
const timeout = takeFirst(options.timeout, testInfo?._projectInternal.expect?.toPass?.timeout, 0);
481-
const intervals = takeFirst(options.intervals, testInfo?._projectInternal.expect?.toPass?.intervals, [100, 250, 500, 1000]);
476+
const config = expectConfig();
477+
const timeout = options.timeout ?? config.toPass?.timeout ?? 0;
478+
const intervals = options.intervals ?? config.toPass?.intervals ?? [100, 250, 500, 1000];
482479

483-
const { deadline, timeoutMessage } = testInfo ? testInfo._deadlineForMatcher(timeout) : TestInfoImpl._defaultDeadlineForMatcher(timeout);
480+
const { deadline, timeoutMessage } = config.testInfo ? config.testInfo._deadlineForMatcher(timeout) : defaultDeadlineForMatcher(timeout);
484481
const result = await pollAgainstDeadline<Error|undefined>(async () => {
485-
if (testInfo && currentTestInfo() !== testInfo)
482+
if (config && expectConfig() !== config)
486483
return { continuePolling: false, result: undefined };
487484
try {
488485
await callback();
@@ -517,3 +514,7 @@ export function computeMatcherTitleSuffix(matcherName: string, receiver: any, ar
517514
}
518515
return {};
519516
}
517+
518+
export function defaultDeadlineForMatcher(timeout: number): { deadline: number; timeoutMessage: string } {
519+
return { deadline: (timeout ? monotonicTime() + timeout : 0), timeoutMessage: `Timeout ${timeout}ms exceeded while waiting on the predicate` };
520+
}

packages/playwright/src/matchers/toBeTruthy.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616

1717
import { formatMatcherMessage } from 'playwright-core/lib/utils';
1818

19-
import { expectTypes } from '../util';
19+
import { expectTypes } from './matcherHint';
2020

2121
import type { MatcherResult } from './matcherHint';
2222
import type { Locator } from 'playwright-core';

packages/playwright/src/matchers/toEqual.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616

1717
import { formatMatcherMessage, isRegExp } from 'playwright-core/lib/utils';
1818

19-
import { expectTypes } from '../util';
19+
import { expectTypes } from './matcherHint';
2020

2121
import type { MatcherResult } from './matcherHint';
2222
import type { Locator } from 'playwright-core';

packages/playwright/src/matchers/toMatchAriaSnapshot.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,9 @@
1717

1818
import fs from 'fs';
1919
import path from 'path';
20-
2120
import { formatMatcherMessage, escapeTemplateString, isString, printReceivedStringContainExpectedSubstring } from 'playwright-core/lib/utils';
2221

23-
import { fileExistsAsync } from '../util';
24-
import { currentTestInfo } from '../common/globals';
22+
import { expectConfig } from './expect';
2523

2624
import type { MatcherResult } from './matcherHint';
2725
import type { ExpectMatcherStateInternal, LocatorEx } from './matchers';
@@ -42,7 +40,7 @@ export async function toMatchAriaSnapshot(
4240
): Promise<MatcherResult<string | RegExp, string>> {
4341
const matcherName = 'toMatchAriaSnapshot';
4442

45-
const testInfo = currentTestInfo();
43+
const testInfo = expectConfig().testInfo;
4644
if (!testInfo)
4745
throw new Error(`toMatchAriaSnapshot() must be called during the test`);
4846

@@ -180,3 +178,12 @@ function unshift(snapshot: string): string {
180178
function indent(snapshot: string, indent: string): string {
181179
return snapshot.split('\n').map(line => indent + line).join('\n');
182180
}
181+
182+
async function fileExistsAsync(resolved: string) {
183+
try {
184+
const stat = await fs.promises.stat(resolved);
185+
return stat.isFile();
186+
} catch {
187+
return false;
188+
}
189+
}

packages/playwright/src/matchers/toMatchSnapshot.ts

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,12 @@ import { callLogText, formatMatcherMessage, compareBuffersOrStrings, getComparat
2121
import { colors } from 'playwright-core/lib/utils';
2222
import { mime } from 'playwright-core/lib/utilsBundle';
2323

24-
import { addSuffixToFilePath, expectTypes } from '../util';
25-
import { currentTestInfo } from '../common/globals';
24+
import { expectTypes } from './matcherHint';
25+
import { expectConfig } from './expect';
2626

2727
import type { MatcherResult } from './matcherHint';
2828
import type { ExpectMatcherStateInternal } from './matchers';
29-
import type { FullProjectInternal } from '../common/config';
29+
import type { ExpectConfig } from './expect';
3030
import type { TestInfoImpl, TestStepInfoImpl } from '../worker/testInfo';
3131
import type { Locator, Page } from 'playwright-core';
3232
import type { ExpectScreenshotOptions, Page as PageEx } from 'playwright-core/lib/client/page';
@@ -36,7 +36,7 @@ type NameOrSegments = string | string[];
3636

3737
type ImageMatcherResult = MatcherResult<string, string> & { diff?: string };
3838

39-
type ToHaveScreenshotConfigOptions = NonNullable<NonNullable<FullProjectInternal['expect']>['toHaveScreenshot']> & {
39+
type ToHaveScreenshotConfigOptions = NonNullable<ExpectConfig['toHaveScreenshot']> & {
4040
_comparator?: string;
4141
};
4242

@@ -254,7 +254,7 @@ export function toMatchSnapshot(
254254
nameOrOptions: NameOrSegments | { name?: NameOrSegments } & ImageComparatorOptions = {},
255255
optOptions: ImageComparatorOptions = {}
256256
): MatcherResult<NameOrSegments | { name?: NameOrSegments }, string> {
257-
const testInfo = currentTestInfo();
257+
const testInfo = expectConfig().testInfo;
258258
if (!testInfo)
259259
throw new Error(`toMatchSnapshot() must be called during the test`);
260260
if (received instanceof Promise)
@@ -325,7 +325,7 @@ export async function toHaveScreenshot(
325325
nameOrOptions: NameOrSegments | { name?: NameOrSegments } & ToHaveScreenshotOptions = {},
326326
optOptions: ToHaveScreenshotOptions = {}
327327
): Promise<MatcherResult<NameOrSegments | { name?: NameOrSegments }, string>> {
328-
const testInfo = currentTestInfo();
328+
const testInfo = expectConfig().testInfo;
329329
if (!testInfo)
330330
throw new Error(`toHaveScreenshot() must be called during the test`);
331331

@@ -461,3 +461,9 @@ async function loadScreenshotStyles(stylePath?: string | string[]): Promise<stri
461461
}));
462462
return styles.join('\n').trim() || undefined;
463463
}
464+
465+
function addSuffixToFilePath(filePath: string, suffix: string): string {
466+
const ext = path.extname(filePath);
467+
const base = filePath.substring(0, filePath.length - ext.length);
468+
return base + suffix + ext;
469+
}

0 commit comments

Comments
 (0)