Skip to content

[Feature]: Custom AsymmetricMatchers #32562

Open
@amberUCP

Description

@amberUCP

🚀 Feature Request

The current custom matcher extend function does not seem to allow custom asymmetric matchers.

E.g. fixtures.ts

import { expect as baseExpect } from '@playwright/test';
export { test } from '@playwright/test';

export const expect =  baseExpect.extend({
  async toBeNumberOrNull(received) {
    const assertionName = `toBeNullOrType`;
    let pass: boolean;
    let matcherResult: any;
    const expected = 'Null or Number';
    let isNumber = false;
    let isNull = false;

    try {
      await baseExpect(received).toEqual(baseExpect.any(Number));
      
      isNumber = true;
    } catch (e: any) {
      matcherResult = e.matcherResult;
      isNumber = false;
    }

    try {
      await baseExpect(received).toEqual(null);
      isNull = true;
    } catch (e: any) {
      matcherResult = e.matcherResult;
      isNull = false;
    }
    
    pass = isNull || isNumber;

    const message = pass
      ? () => this.utils.matcherHint(assertionName, undefined, undefined, { isNot: this.isNot }) +
          '\n\n' +
          `Expected: ${this.isNot ? 'not' : ''}${this.utils.printExpected(expected)}\n` +
          (matcherResult ? `Received: ${this.utils.printReceived(matcherResult.actual)}` : '')
      : () =>  this.utils.matcherHint(assertionName, undefined, undefined, { isNot: this.isNot }) +
          '\n\n' +
          `Expected: ${this.utils.printExpected(expected)}\n` +
          (matcherResult ? `Received: ${this.utils.printReceived(matcherResult.actual)}` : '');

    return {
      message,
      pass,
      name: assertionName,
      expected,
      actual: matcherResult?.actual,
    };
  }
});

This works when writing an symmetric assertion such as:

  test('Test matcher', async () => {
   await expect(1).toBeNumberOrNull();
  });

This does not work when writing an asymmetric assertion such as:

  test('Gets order', async ({ request }) => {
    const response = await request.get('/order');

    expect(response.status()).toBe(200);

    const response = await response.json();

    expect(response.data).toMatchObject({
      id: expect.any(String),
      name: expect.any(String),
      item: {
        id: expect.any(Number),
        price: expect.any(Number),
        discount: expect.toBeNumberOrNull(), // this custom asymmetric matcher is not currently supported
      },
    });
  });

Example

No response

Motivation

This will allow custom matchers to be used asymmetrically, e.g. where expected values could be a few different values such as the example above.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions