Skip to content

Commit 3ea0941

Browse files
committed
chore: reduce dependencies from expect matchers to the rest
1 parent a622c1c commit 3ea0941

14 files changed

Lines changed: 117 additions & 73 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
@@ -19,6 +19,7 @@ import { iso, serverUtils } from 'playwright-core/lib/coreBundle';
1919
import { ExpectError, isJestError } from './matcherHint';
2020
import {
2121
computeMatcherTitleSuffix,
22+
defaultDeadlineForMatcher,
2223
toBeAttached,
2324
toBeChecked,
2425
toBeDisabled,
@@ -54,13 +55,47 @@ import { toHaveScreenshot, toMatchSnapshot } from './toMatchSnapshot';
5455
import {
5556
expect as expectLibrary,
5657
} from '../common/expectBundle';
57-
import { currentTestInfo } from '../common/globals';
5858
import { filteredStackTrace } from '../util';
59-
import { TestInfoImpl } from '../worker/testInfo';
6059

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

65100
type ExpectMessage = string | { message?: string };
66101

@@ -152,7 +187,6 @@ function createExpect(info: ExpectMetaInfo, prefix: string[], userMatchers: Reco
152187
// Rely on sync call sequence to seed each matcher call with the context.
153188
type MatcherCallContext = {
154189
expectInfo: ExpectMetaInfo;
155-
testInfo: TestInfoImpl | null;
156190
step?: TestStepInfoImpl;
157191
};
158192

@@ -178,7 +212,7 @@ function wrapPlaywrightMatcherToPassNiceThis(matcher: any) {
178212
return function(this: any, ...args: any[]) {
179213
const { isNot, promise, utils } = this;
180214
const context = takeMatcherCallContext();
181-
const timeout = context?.expectInfo.timeout ?? context?.testInfo?._projectInternal?.expect?.timeout ?? defaultExpectTimeout;
215+
const timeout = context?.expectInfo.timeout ?? expectConfig().timeout ?? defaultExpectTimeout;
182216
const newThis: ExpectMatcherStateInternal = {
183217
isNot,
184218
promise,
@@ -291,8 +325,8 @@ class ExpectMetaInfoProxyHandler implements ProxyHandler<any> {
291325
matcher = (...args: any[]) => pollMatcher(resolvedMatcherName, this._info, this._prefix, ...args);
292326
}
293327
return (...args: any[]) => {
294-
const testInfo = currentTestInfo();
295-
setMatcherCallContext({ expectInfo: this._info, testInfo });
328+
const testInfo = expectConfig().testInfo;
329+
setMatcherCallContext({ expectInfo: this._info });
296330
if (!testInfo)
297331
return matcher.call(target, ...args);
298332

@@ -344,7 +378,7 @@ class ExpectMetaInfoProxyHandler implements ProxyHandler<any> {
344378
};
345379

346380
try {
347-
setMatcherCallContext({ expectInfo: this._info, testInfo, step: step.info });
381+
setMatcherCallContext({ expectInfo: this._info, step: step.info });
348382
const callback = () => matcher.call(target, ...args);
349383
const result = serverUtils.currentZone().with('stepZone', step).run(callback);
350384
if (result instanceof Promise)
@@ -359,13 +393,13 @@ class ExpectMetaInfoProxyHandler implements ProxyHandler<any> {
359393
}
360394

361395
async function pollMatcher(qualifiedMatcherName: string, info: ExpectMetaInfo, prefix: string[], ...args: any[]) {
362-
const testInfo = currentTestInfo();
396+
const config = expectConfig();
363397
const poll = info.poll!;
364-
const timeout = poll.timeout ?? info.timeout ?? testInfo?._projectInternal?.expect?.timeout ?? defaultExpectTimeout;
365-
const { deadline, timeoutMessage } = testInfo ? testInfo._deadlineForMatcher(timeout) : TestInfoImpl._defaultDeadlineForMatcher(timeout);
398+
const timeout = poll.timeout ?? info.timeout ?? config.timeout ?? defaultExpectTimeout;
399+
const { deadline, timeoutMessage } = config.testInfo ? config.testInfo._deadlineForMatcher(timeout) : defaultDeadlineForMatcher(timeout);
366400

367401
const result = await iso.pollAgainstDeadline<Error|undefined>(async () => {
368-
if (testInfo && currentTestInfo() !== testInfo)
402+
if (expectConfig() !== config)
369403
return { continuePolling: false, result: undefined };
370404

371405
const innerInfo: ExpectMetaInfo = {

packages/playwright/src/matchers/matcherHint.ts

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

17+
import util from 'util';
18+
1719
import { iso } from 'playwright-core/lib/coreBundle';
1820

1921
import type { StackFrame } from '@protocol/channels';
@@ -56,3 +58,13 @@ export class ExpectError extends Error {
5658
export function isJestError(e: unknown): e is JestError {
5759
return e instanceof Error && 'matcherResult' in e && !!e.matcherResult;
5860
}
61+
62+
export function expectTypes(receiver: any, types: ('APIResponse' | 'Page' | 'Locator')[], matcherName: string) {
63+
if (typeof receiver !== 'object' || !types.includes(receiver._apiName)) {
64+
const receiverString = typeof receiver === 'object' && receiver !== null ? `${receiver.constructor.name} ${util.inspect(receiver)}` : String(receiver);
65+
const commaSeparated = types.slice();
66+
const lastType = commaSeparated.pop();
67+
const typesString = commaSeparated.length ? commaSeparated.join(', ') + ' or ' + lastType : lastType;
68+
throw new Error(`${matcherName} can be only used with ${typesString} object${types.length > 1 ? 's' : ''}, was called with ${receiverString}`);
69+
}
70+
}

packages/playwright/src/matchers/matchers.ts

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,16 +17,13 @@
1717
import { iso, serverUtils } from 'playwright-core/lib/coreBundle';
1818
import { colors } from 'playwright-core/lib/utilsBundle';
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';
@@ -478,13 +475,13 @@ export async function toPass(
478475
timeout?: number,
479476
} = {},
480477
) {
481-
const testInfo = currentTestInfo();
482-
const timeout = takeFirst(options.timeout, testInfo?._projectInternal.expect?.toPass?.timeout, 0);
483-
const intervals = takeFirst(options.intervals, testInfo?._projectInternal.expect?.toPass?.intervals, [100, 250, 500, 1000]);
478+
const config = expectConfig();
479+
const timeout = options.timeout ?? config.toPass?.timeout ?? 0;
480+
const intervals = options.intervals ?? config.toPass?.intervals ?? [100, 250, 500, 1000];
484481

485-
const { deadline, timeoutMessage } = testInfo ? testInfo._deadlineForMatcher(timeout) : TestInfoImpl._defaultDeadlineForMatcher(timeout);
482+
const { deadline, timeoutMessage } = config.testInfo ? config.testInfo._deadlineForMatcher(timeout) : defaultDeadlineForMatcher(timeout);
486483
const result = await iso.pollAgainstDeadline<Error|undefined>(async () => {
487-
if (testInfo && currentTestInfo() !== testInfo)
484+
if (expectConfig() !== config)
488485
return { continuePolling: false, result: undefined };
489486
try {
490487
await callback();
@@ -519,3 +516,7 @@ export function computeMatcherTitleSuffix(matcherName: string, receiver: any, ar
519516
}
520517
return {};
521518
}
519+
520+
export function defaultDeadlineForMatcher(timeout: number): { deadline: number; timeoutMessage: string } {
521+
return { deadline: (timeout ? iso.monotonicTime() + timeout : 0), timeoutMessage: `Timeout ${timeout}ms exceeded while waiting on the predicate` };
522+
}

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 { serverUtils } from 'playwright-core/lib/coreBundle';
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 { iso, serverUtils } from 'playwright-core/lib/coreBundle';
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: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,8 @@ import path from 'path';
2020

2121
import { iso, serverUtils } from 'playwright-core/lib/coreBundle';
2222

23-
import { expectTypes, fileExistsAsync } from '../util';
24-
import { currentTestInfo } from '../common/globals';
23+
import { expectConfig } from './expect';
24+
import { expectTypes } from './matcherHint';
2525

2626
import type { MatcherResult } from './matcherHint';
2727
import type { ExpectMatcherStateInternal, FrameEx, LocatorEx } from './matchers';
@@ -45,7 +45,7 @@ export async function toMatchAriaSnapshot(
4545
expectTypes(receiver, ['Page', 'Locator'], matcherName);
4646
const locator = (receiver as any)._apiName === 'Page' ? undefined : receiver as LocatorEx;
4747

48-
const testInfo = currentTestInfo();
48+
const testInfo = expectConfig().testInfo;
4949
if (!testInfo)
5050
throw new Error(`${matcherName}() must be called during the test`);
5151

@@ -186,3 +186,12 @@ function unshift(snapshot: string): string {
186186
function indent(snapshot: string, indent: string): string {
187187
return snapshot.split('\n').map(line => indent + line).join('\n');
188188
}
189+
190+
async function fileExistsAsync(resolved: string) {
191+
try {
192+
const stat = await fs.promises.stat(resolved);
193+
return stat.isFile();
194+
} catch {
195+
return false;
196+
}
197+
}

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 { iso, serverUtils } from 'playwright-core/lib/coreBundle';
2121
import { colors } from 'playwright-core/lib/utilsBundle';
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';
@@ -37,7 +37,7 @@ type NameOrSegments = string | string[];
3737

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

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

@@ -255,7 +255,7 @@ export function toMatchSnapshot(
255255
nameOrOptions: NameOrSegments | { name?: NameOrSegments } & ImageComparatorOptions = {},
256256
optOptions: ImageComparatorOptions = {}
257257
): MatcherResult<NameOrSegments | { name?: NameOrSegments }, string> {
258-
const testInfo = currentTestInfo();
258+
const testInfo = expectConfig().testInfo;
259259
if (!testInfo)
260260
throw new Error(`toMatchSnapshot() must be called during the test`);
261261
if (received instanceof Promise)
@@ -326,7 +326,7 @@ export async function toHaveScreenshot(
326326
nameOrOptions: NameOrSegments | { name?: NameOrSegments } & ToHaveScreenshotOptions = {},
327327
optOptions: ToHaveScreenshotOptions = {}
328328
): Promise<MatcherResult<NameOrSegments | { name?: NameOrSegments }, string>> {
329-
const testInfo = currentTestInfo();
329+
const testInfo = expectConfig().testInfo;
330330
if (!testInfo)
331331
throw new Error(`toHaveScreenshot() must be called during the test`);
332332

@@ -470,3 +470,9 @@ async function loadScreenshotStyles(stylePath?: string | string[]): Promise<stri
470470
}));
471471
return styles.join('\n').trim() || undefined;
472472
}
473+
474+
function addSuffixToFilePath(filePath: string, suffix: string): string {
475+
const ext = path.extname(filePath);
476+
const base = filePath.substring(0, filePath.length - ext.length);
477+
return base + suffix + ext;
478+
}

0 commit comments

Comments
 (0)