diff --git a/packages/playwright/src/matchers/expect.ts b/packages/playwright/src/matchers/expect.ts index 0fc3859834..265329e643 100644 --- a/packages/playwright/src/matchers/expect.ts +++ b/packages/playwright/src/matchers/expect.ts @@ -105,11 +105,13 @@ export const printReceivedStringContainExpectedResult = ( type ExpectMessage = string | { message?: string }; -function createMatchers(actual: unknown, info: ExpectMetaInfo, prefix: string[]): any { - return new Proxy(expectLibrary(actual), new ExpectMetaInfoProxyHandler(info, prefix)); +function createMatchers(actual: unknown, info: ExpectMetaInfo, prefix: string[], parentPrefixes: string[][]): any { + return new Proxy(expectLibrary(actual), new ExpectMetaInfoProxyHandler(info, [prefix, ...parentPrefixes])); } -function createExpect(info: ExpectMetaInfo, prefix: string[] = []) { +const getPrefixSymbol = Symbol('get prefix'); + +function createExpect(info: ExpectMetaInfo, prefix: string[] = [], parentPrefixes: string[][] = []) { const expectInstance: Expect<{}> = new Proxy(expectLibrary, { apply: function(target: any, thisArg: any, argumentsList: [unknown, ExpectMessage?]) { const [actual, messageOrOptions] = argumentsList; @@ -120,10 +122,10 @@ function createExpect(info: ExpectMetaInfo, prefix: string[] = []) { throw new Error('`expect.poll()` accepts only function as a first argument'); newInfo.generator = actual as any; } - return createMatchers(actual, newInfo, prefix); + return createMatchers(actual, newInfo, prefix, parentPrefixes); }, - get: function(target: any, property: string) { + get: function(target: any, property: string | typeof getPrefixSymbol) { if (property === 'configure') return configure; @@ -149,7 +151,7 @@ function createExpect(info: ExpectMetaInfo, prefix: string[] = []) { } expectLibrary.extend(wrappedMatchers); - return createExpect(info, qualifier); + return createExpect(info, qualifier, parentPrefixes); }; } @@ -159,6 +161,9 @@ function createExpect(info: ExpectMetaInfo, prefix: string[] = []) { }; } + if (property === getPrefixSymbol) + return { prefix, parentPrefixes }; + if (property === 'poll') { return (actual: unknown, messageOrOptions?: ExpectMessage & { timeout?: number, intervals?: number[] }) => { const poll = isString(messageOrOptions) ? {} : messageOrOptions || {}; @@ -247,22 +252,24 @@ type ExpectMetaInfo = { class ExpectMetaInfoProxyHandler implements ProxyHandler { private _info: ExpectMetaInfo; - private _prefix: string[]; + private _prefixes: string[][]; - constructor(info: ExpectMetaInfo, prefix: string[]) { + constructor(info: ExpectMetaInfo, prefixes: string[][]) { this._info = { ...info }; - this._prefix = prefix; + this._prefixes = prefixes; } get(target: Object, matcherName: string | symbol, receiver: any): any { let matcher = Reflect.get(target, matcherName, receiver); if (typeof matcherName === 'string') { - for (let i = this._prefix.length; i > 0; i--) { - const qualifiedName = `${this._prefix.slice(0, i).join(':')}$${matcherName}`; - if (Reflect.has(target, qualifiedName)) { - matcher = Reflect.get(target, qualifiedName, receiver); - break; + for (const prefix of this._prefixes) { + for (let i = prefix.length; i > 0; i--) { + const qualifiedName = `${prefix.slice(0, i).join(':')}$${matcherName}`; + if (Reflect.has(target, qualifiedName)) { + matcher = Reflect.get(target, qualifiedName, receiver); + break; + } } } } @@ -397,5 +404,11 @@ function computeArgsSuffix(matcherName: string, args: any[]) { export const expect: Expect<{}> = createExpect({}).extend(customMatchers); export function mergeExpects(...expects: any[]) { - return expect; + const parentPrefixes = expects.flatMap(e => { + const internals = e[getPrefixSymbol]; + if (!internals) // non-playwright expects mutate the global expect, so we don't need to do anything special + return []; + return [internals.prefix, ...internals.parentPrefixes]; + }); + return createExpect({}, undefined, parentPrefixes); } diff --git a/tests/playwright-test/expect.spec.ts b/tests/playwright-test/expect.spec.ts index 1f9bd72141..b7199e7433 100644 --- a/tests/playwright-test/expect.spec.ts +++ b/tests/playwright-test/expect.spec.ts @@ -18,41 +18,6 @@ import path from 'path'; import { test, expect, parseTestRunnerOutput, stripAnsi } from './playwright-test-fixtures'; const { spawnAsync } = require('../../packages/playwright-core/lib/utils'); -test('should be able to call expect.extend in config', async ({ runInlineTest }) => { - const result = await runInlineTest({ - 'helper.ts': ` - import { test as base, expect } from '@playwright/test'; - expect.extend({ - toBeWithinRange(received, floor, ceiling) { - const pass = received >= floor && received <= ceiling; - if (pass) { - return { - message: () => - 'passed', - pass: true, - }; - } else { - return { - message: () => 'failed', - pass: false, - }; - } - }, - }); - export const test = base; - `, - 'expect-test.spec.ts': ` - import { test } from './helper'; - test('numeric ranges', () => { - test.expect(100).toBeWithinRange(90, 110); - test.expect(101).not.toBeWithinRange(0, 100); - }); - ` - }); - expect(result.exitCode).toBe(0); - expect(result.passed).toBe(1); -}); - test('should not expand huge arrays', async ({ runInlineTest }) => { const result = await runInlineTest({ 'expect-test.spec.ts': `