playwright/tests/expect/asymmetricMatchers.test.ts

520 lines
17 KiB
TypeScript

/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
import { test } from '../playwright-test/stable-test-runner';
import { expect as expectUnderTest, asymmetricMatchers } from '../../packages/playwright/bundles/expect/src/expectBundleImpl';
const {
any,
anything,
arrayContaining,
arrayNotContaining,
closeTo,
notCloseTo,
objectContaining,
objectNotContaining,
stringContaining,
stringMatching,
stringNotContaining,
stringNotMatching,
} = asymmetricMatchers;
test('Any.asymmetricMatch()', () => {
class Thing {}
[
any(String).asymmetricMatch('jest'),
any(Number).asymmetricMatch(1),
any(Function).asymmetricMatch(() => {}),
any(Boolean).asymmetricMatch(true),
any(BigInt).asymmetricMatch(1n),
any(Symbol).asymmetricMatch(Symbol()),
any(Object).asymmetricMatch({}),
any(Object).asymmetricMatch(null),
any(Array).asymmetricMatch([]),
any(Thing).asymmetricMatch(new Thing()),
].forEach(test => {
expectUnderTest(test).toBe(true);
});
});
test('Any.asymmetricMatch() on primitive wrapper classes', () => {
[
any(String).asymmetricMatch(new String('jest')),
any(Number).asymmetricMatch(new Number(1)),
any(Function).asymmetricMatch(new Function('() => {}')),
any(Boolean).asymmetricMatch(new Boolean(true)),
any(BigInt).asymmetricMatch(Object(1n)),
any(Symbol).asymmetricMatch(Object(Symbol())),
].forEach(test => {
expectUnderTest(test).toBe(true);
});
});
test('Any.toAsymmetricMatcher()', () => {
expectUnderTest(any(Number).toAsymmetricMatcher()).toBe('Any<Number>');
});
test('Any.toAsymmetricMatcher() with function name', () => {
[
['someFunc', function someFunc() {}],
['$someFunc', function $someFunc() {}],
[
'$someFunc2',
(function() {
function $someFunc2() {}
Object.defineProperty($someFunc2, 'name', { value: '' });
return $someFunc2;
})(),
],
[
'$someAsyncFunc',
(function() {
async function $someAsyncFunc() {}
Object.defineProperty($someAsyncFunc, 'name', { value: '' });
return $someAsyncFunc;
})(),
],
[
'$someGeneratorFunc',
(function() {
function* $someGeneratorFunc() {}
Object.defineProperty($someGeneratorFunc, 'name', { value: '' });
return $someGeneratorFunc;
})(),
],
[
'$someFuncWithFakeToString',
(function() {
function $someFuncWithFakeToString() {}
$someFuncWithFakeToString.toString = () => 'Fake to string';
return $someFuncWithFakeToString;
})(),
],
].forEach(([name, fn]) => {
expectUnderTest(any(fn).toAsymmetricMatcher()).toBe(`Any<${name}>`);
});
});
test('Any throws when called with empty constructor', () => {
// @ts-expect-error: Testing runtime error
expectUnderTest(() => any()).toThrow(
'any() expects to be passed a constructor function. Please pass one or use anything() to match any object.',
);
});
test('Anything matches any type', () => {
[
anything().asymmetricMatch('jest'),
anything().asymmetricMatch(1),
anything().asymmetricMatch(() => {}),
anything().asymmetricMatch(true),
anything().asymmetricMatch({}),
anything().asymmetricMatch([]),
].forEach(test => {
expectUnderTest(test).toBe(true);
});
});
test('Anything does not match null and undefined', () => {
[
anything().asymmetricMatch(null),
anything().asymmetricMatch(undefined),
].forEach(test => {
expectUnderTest(test).toBe(false);
});
});
test('Anything.toAsymmetricMatcher()', () => {
expectUnderTest(anything().toAsymmetricMatcher()).toBe('Anything');
});
test('ArrayContaining matches', () => {
[
arrayContaining([]).asymmetricMatch('jest'),
arrayContaining(['foo']).asymmetricMatch(['foo']),
arrayContaining(['foo']).asymmetricMatch(['foo', 'bar']),
arrayContaining([]).asymmetricMatch({}),
].forEach(test => {
expectUnderTest(test).toEqual(true);
});
});
test('ArrayContaining does not match', () => {
expectUnderTest(arrayContaining(['foo']).asymmetricMatch(['bar'])).toBe(false);
});
test('ArrayContaining throws for non-arrays', () => {
expectUnderTest(() => {
// @ts-expect-error: Testing runtime error
arrayContaining('foo').asymmetricMatch([]);
}).toThrow("You must provide an array to ArrayContaining, not 'string'.");
});
test('ArrayNotContaining matches', () => {
expectUnderTest(arrayNotContaining(['foo']).asymmetricMatch(['bar'])).toBe(true);
});
test('ArrayNotContaining does not match', () => {
[
arrayNotContaining([]).asymmetricMatch('jest'),
arrayNotContaining(['foo']).asymmetricMatch(['foo']),
arrayNotContaining(['foo']).asymmetricMatch(['foo', 'bar']),
arrayNotContaining([]).asymmetricMatch({}),
].forEach(test => {
expectUnderTest(test).toEqual(false);
});
});
test('ArrayNotContaining throws for non-arrays', () => {
expectUnderTest(() => {
// @ts-expect-error: Testing runtime error
arrayNotContaining('foo').asymmetricMatch([]);
}).toThrow("You must provide an array to ArrayNotContaining, not 'string'.");
});
test('ObjectContaining matches', () => {
const foo = Symbol('foo');
[
objectContaining({}).asymmetricMatch('jest'),
objectContaining({ foo: 'foo' }).asymmetricMatch({ foo: 'foo', jest: 'jest' }),
objectContaining({ foo: undefined }).asymmetricMatch({ foo: undefined }),
objectContaining({ first: objectContaining({ second: {} }) }).asymmetricMatch({
first: { second: {} },
}),
objectContaining({ foo: Buffer.from('foo') }).asymmetricMatch({
foo: Buffer.from('foo'),
jest: 'jest',
}),
objectContaining({ [foo]: 'foo' }).asymmetricMatch({ [foo]: 'foo' }),
].forEach(test => {
expectUnderTest(test).toEqual(true);
});
});
test('ObjectContaining does not match', () => {
const foo = Symbol('foo');
const bar = Symbol('bar');
[
objectContaining({ foo: 'foo' }).asymmetricMatch({ bar: 'bar' }),
objectContaining({ foo: 'foo' }).asymmetricMatch({ foo: 'foox' }),
objectContaining({ foo: undefined }).asymmetricMatch({}),
objectContaining({
answer: 42,
foo: { bar: 'baz', foobar: 'qux' },
}).asymmetricMatch({ foo: { bar: 'baz' } }),
objectContaining({ [foo]: 'foo' }).asymmetricMatch({ [bar]: 'bar' }),
].forEach(test => {
expectUnderTest(test).toEqual(false);
});
});
test('ObjectContaining matches defined properties', () => {
const definedPropertyObject = {};
Object.defineProperty(definedPropertyObject, 'foo', { get: () => 'bar' });
expectUnderTest(
objectContaining({ foo: 'bar' }).asymmetricMatch(definedPropertyObject),
).toBe(true);
});
test('ObjectContaining matches prototype properties', () => {
const prototypeObject = { foo: 'bar' };
let obj;
if (Object.create) {
obj = Object.create(prototypeObject);
} else {
function Foo() {}
Foo.prototype = prototypeObject;
Foo.prototype.constructor = Foo;
obj = new (Foo as any)();
}
expectUnderTest(objectContaining({ foo: 'bar' }).asymmetricMatch(obj)).toBe(true);
});
test('ObjectContaining throws for non-objects', () => {
// @ts-expect-error: Testing runtime error
expectUnderTest(() => objectContaining(1337).asymmetricMatch()).toThrow(
"You must provide an object to ObjectContaining, not 'number'.",
);
});
test('ObjectContaining does not mutate the sample', () => {
const sample = { foo: { bar: {} } };
const sample_json = JSON.stringify(sample);
expectUnderTest({ foo: { bar: {} } }).toEqual(expectUnderTest.objectContaining(sample));
expectUnderTest(JSON.stringify(sample)).toEqual(sample_json);
});
test('ObjectNotContaining matches', () => {
const foo = Symbol('foo');
const bar = Symbol('bar');
[
objectContaining({}).asymmetricMatch(null),
objectContaining({}).asymmetricMatch(undefined),
objectNotContaining({ [foo]: 'foo' }).asymmetricMatch({ [bar]: 'bar' }),
objectNotContaining({ foo: 'foo' }).asymmetricMatch({ bar: 'bar' }),
objectNotContaining({ foo: 'foo' }).asymmetricMatch({ foo: 'foox' }),
objectNotContaining({ foo: undefined }).asymmetricMatch({}),
objectNotContaining({
first: objectNotContaining({ second: {} }),
}).asymmetricMatch({ first: { second: {} } }),
objectNotContaining({ first: { second: {}, third: {} } }).asymmetricMatch({
first: { second: {} },
}),
objectNotContaining({ first: { second: {} } }).asymmetricMatch({
first: { second: {}, third: {} },
}),
objectNotContaining({ foo: 'foo', jest: 'jest' }).asymmetricMatch({
foo: 'foo',
}),
].forEach(test => {
expectUnderTest(test).toEqual(true);
});
});
test('ObjectNotContaining does not match', () => {
[
objectNotContaining({}).asymmetricMatch('jest'),
objectNotContaining({ foo: 'foo' }).asymmetricMatch({
foo: 'foo',
jest: 'jest',
}),
objectNotContaining({ foo: undefined }).asymmetricMatch({ foo: undefined }),
objectNotContaining({ first: { second: {} } }).asymmetricMatch({
first: { second: {} },
}),
objectNotContaining({
first: objectContaining({ second: {} }),
}).asymmetricMatch({ first: { second: {} } }),
objectNotContaining({}).asymmetricMatch(null),
objectNotContaining({}).asymmetricMatch(undefined),
objectNotContaining({}).asymmetricMatch({}),
].forEach(test => {
expectUnderTest(test).toEqual(false);
});
});
test('ObjectNotContaining inverts ObjectContaining', () => {
(
[
[{}, null],
[{ foo: 'foo' }, { foo: 'foo', jest: 'jest' }],
[{ foo: 'foo', jest: 'jest' }, { foo: 'foo' }],
[{ foo: undefined }, { foo: undefined }],
[{ foo: undefined }, {}],
[{ first: { second: {} } }, { first: { second: {} } }],
[{ first: objectContaining({ second: {} }) }, { first: { second: {} } }],
[{ first: objectNotContaining({ second: {} }) }, { first: { second: {} } }],
[{}, { foo: undefined }],
] as const
).forEach(([sample, received]) => {
expectUnderTest(objectNotContaining(sample).asymmetricMatch(received)).toEqual(
!objectContaining(sample).asymmetricMatch(received),
);
});
});
test('ObjectNotContaining throws for non-objects', () => {
expectUnderTest(() => {
// @ts-expect-error: Testing runtime error
objectNotContaining(1337).asymmetricMatch();
}).toThrow(
"You must provide an object to ObjectNotContaining, not 'number'.",
);
});
test('StringContaining matches string against string', () => {
expectUnderTest(stringContaining('en*').asymmetricMatch('queen*')).toBe(true);
expectUnderTest(stringContaining('en').asymmetricMatch('queue')).toBe(false);
});
test('StringContaining throws if expected value is not string', () => {
expectUnderTest(() => {
// @ts-expect-error: Testing runtime error
stringContaining([1]).asymmetricMatch('queen');
}).toThrow('Expected is not a string');
});
test('StringContaining returns false if received value is not string', () => {
expectUnderTest(stringContaining('en*').asymmetricMatch(1)).toBe(false);
});
test('StringNotContaining matches string against string', () => {
expectUnderTest(stringNotContaining('en*').asymmetricMatch('queen*')).toBe(false);
expectUnderTest(stringNotContaining('en').asymmetricMatch('queue')).toBe(true);
});
test('StringNotContaining throws if expected value is not string', () => {
expectUnderTest(() => {
// @ts-expect-error: Testing runtime error
stringNotContaining([1]).asymmetricMatch('queen');
}).toThrow('Expected is not a string');
});
test('StringNotContaining returns true if received value is not string', () => {
expectUnderTest(stringNotContaining('en*').asymmetricMatch(1)).toBe(true);
});
test('StringMatching matches string against regexp', () => {
expectUnderTest(stringMatching(/en/).asymmetricMatch('queen')).toBe(true);
expectUnderTest(stringMatching(/en/).asymmetricMatch('queue')).toBe(false);
});
test('StringMatching matches string against string', () => {
expectUnderTest(stringMatching('en').asymmetricMatch('queen')).toBe(true);
expectUnderTest(stringMatching('en').asymmetricMatch('queue')).toBe(false);
});
test('StringMatching throws if expected value is neither string nor regexp', () => {
expectUnderTest(() => {
// @ts-expect-error: Testing runtime error
stringMatching([1]).asymmetricMatch('queen');
}).toThrow('Expected is not a String or a RegExp');
});
test('StringMatching returns false if received value is not string', () => {
expectUnderTest(stringMatching('en').asymmetricMatch(1)).toBe(false);
});
test('StringMatching returns false even if coerced non-string received value matches pattern', () => {
expectUnderTest(stringMatching('null').asymmetricMatch(null)).toBe(false);
});
test('StringNotMatching matches string against regexp', () => {
expectUnderTest(stringNotMatching(/en/).asymmetricMatch('queen')).toBe(false);
expectUnderTest(stringNotMatching(/en/).asymmetricMatch('queue')).toBe(true);
});
test('StringNotMatching matches string against string', () => {
expectUnderTest(stringNotMatching('en').asymmetricMatch('queen')).toBe(false);
expectUnderTest(stringNotMatching('en').asymmetricMatch('queue')).toBe(true);
});
test('StringNotMatching throws if expected value is neither string nor regexp', () => {
expectUnderTest(() => {
// @ts-expect-error: Testing runtime error
stringNotMatching([1]).asymmetricMatch('queen');
}).toThrow('Expected is not a String or a RegExp');
});
test('StringNotMatching returns true if received value is not string', () => {
expectUnderTest(stringNotMatching('en').asymmetricMatch(1)).toBe(true);
});
test.describe('closeTo', () => {
[
[0, 0],
[0, 0.001],
[1.23, 1.229],
[1.23, 1.226],
[1.23, 1.225],
[1.23, 1.234],
[Infinity, Infinity],
[-Infinity, -Infinity],
].forEach(([expected, received]) => {
test(`${expected} closeTo ${received} return true`, () => {
expectUnderTest(closeTo(expected).asymmetricMatch(received)).toBe(true);
});
test(`${expected} notCloseTo ${received} return false`, () => {
expectUnderTest(notCloseTo(expected).asymmetricMatch(received)).toBe(false);
});
});
[
[0, 0.01],
[1, 1.23],
[1.23, 1.2249999],
[Infinity, -Infinity],
[Infinity, 1.23],
[-Infinity, -1.23],
].forEach(([expected, received]) => {
test(`${expected} closeTo ${received} return false`, () => {
expectUnderTest(closeTo(expected).asymmetricMatch(received)).toBe(false);
});
test(`${expected} notCloseTo ${received} return true`, () => {
expectUnderTest(notCloseTo(expected).asymmetricMatch(received)).toBe(true);
});
});
[
[0, 0.1, 0],
[0, 0.0001, 3],
[0, 0.000004, 5],
[2.0000002, 2, 5],
].forEach(([expected, received, precision]) => {
test(`${expected} closeTo ${received} with precision ${precision} return true`, () => {
expectUnderTest(closeTo(expected, precision).asymmetricMatch(received)).toBe(
true,
);
});
test(`${expected} notCloseTo ${received} with precision ${precision} return false`, () => {
expectUnderTest(
notCloseTo(expected, precision).asymmetricMatch(received),
).toBe(false);
});
});
[
[3.141592e-7, 3e-7, 8],
[56789, 51234, -4],
].forEach(([expected, received, precision]) => {
test(`${expected} closeTo ${received} with precision ${precision} return false`, () => {
expectUnderTest(closeTo(expected, precision).asymmetricMatch(received)).toBe(
false,
);
});
test(`${expected} notCloseTo ${received} with precision ${precision} return true`, () => {
expectUnderTest(
notCloseTo(expected, precision).asymmetricMatch(received),
).toBe(true);
});
});
test('closeTo throw if expected is not number', () => {
expectUnderTest(() => {
// @ts-expect-error: Testing runtime error
closeTo('a');
}).toThrow('Expected is not a Number');
});
test('notCloseTo throw if expected is not number', () => {
expectUnderTest(() => {
// @ts-expect-error: Testing runtime error
notCloseTo('a');
}).toThrow('Expected is not a Number');
});
test('closeTo throw if precision is not number', () => {
expectUnderTest(() => {
// @ts-expect-error: Testing runtime error
closeTo(1, 'a');
}).toThrow('Precision is not a Number');
});
test('notCloseTo throw if precision is not number', () => {
expectUnderTest(() => {
// @ts-expect-error: Testing runtime error
notCloseTo(1, 'a');
}).toThrow('Precision is not a Number');
});
test('closeTo return false if received is not number', () => {
expectUnderTest(closeTo(1).asymmetricMatch('a')).toBe(false);
});
test('notCloseTo return false if received is not number', () => {
expectUnderTest(notCloseTo(1).asymmetricMatch('a')).toBe(false);
});
});