playwright/tests/expect/toThrowMatchers.test.ts

592 lines
18 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, expect } from './fixtures';
import { expect as expectUnderTest } from '../../packages/playwright/bundles/expect/src/expectBundleImpl';
const expectUnderTestAsAny = expectUnderTest as any;
// Custom Error class because node versions have different stack trace strings.
class CustomError extends Error {
constructor(message?: string) {
super(message);
this.name = 'Error';
this.stack =
'Error\n' +
' at expectUnderTest' +
' (packages/expect/src/__tests__/toThrowMatchers-test.js:24:74)';
}
}
for (const toThrow of ['toThrowError', 'toThrow'] as const) {
test.describe(toThrow, () => {
class Err extends CustomError {}
class Err2 extends CustomError {}
test('to throw or not to throw', () => {
expectUnderTest(() => {
throw new CustomError('apple');
})[toThrow]();
expectUnderTest(() => {}).not[toThrow]();
});
test.describe('substring', () => {
test('passes', () => {
expectUnderTest(() => {
throw new CustomError('apple');
})[toThrow]('apple');
expectUnderTest(() => {
throw new CustomError('banana');
}).not[toThrow]('apple');
expectUnderTest(() => {}).not[toThrow]('apple');
});
test('did not throw at all', () => {
expect(() =>
expectUnderTest(() => {})[toThrow]('apple'),
).toThrowErrorMatchingSnapshot();
});
test('threw, but message did not match (error)', () => {
expect(() => {
expectUnderTest(() => {
throw new CustomError('apple');
})[toThrow]('banana');
}).toThrowErrorMatchingSnapshot();
});
test('threw, but message did not match (non-error falsey)', () => {
expect(() => {
expectUnderTest(() => {
throw '';
})[toThrow]('Server Error');
}).toThrowErrorMatchingSnapshot();
});
test('properly escapes strings when matching against errors', () => {
expectUnderTest(() => {
throw new TypeError('"this"? throws.');
})[toThrow]('"this"? throws.');
});
test('threw, but message should not match (error)', () => {
expect(() => {
expectUnderTest(() => {
throw new CustomError('Invalid array length');
}).not[toThrow]('array');
}).toThrowErrorMatchingSnapshot();
});
test('threw, but message should not match (non-error truthy)', () => {
expect(() => {
expectUnderTest(() => {
throw 'Internal Server Error';
}).not[toThrow]('Server Error');
}).toThrowErrorMatchingSnapshot();
});
});
test.describe('regexp', () => {
test('passes', () => {
expectUnderTest(() => {
throw new CustomError('apple');
})[toThrow](/apple/);
expectUnderTest(() => {
throw new CustomError('banana');
}).not[toThrow](/apple/);
expectUnderTest(() => {}).not[toThrow](/apple/);
});
test('did not throw at all', () => {
expect(() =>
expectUnderTest(() => {})[toThrow](/apple/),
).toThrowErrorMatchingSnapshot();
});
test('threw, but message did not match (error)', () => {
expect(() => {
expectUnderTest(() => {
throw new CustomError('apple');
})[toThrow](/banana/);
}).toThrowErrorMatchingSnapshot();
});
test('threw, but message did not match (non-error falsey)', () => {
expect(() => {
expectUnderTest(() => {
throw 0;
})[toThrow](/^[123456789]\d*/);
}).toThrowErrorMatchingSnapshot();
});
test('threw, but message should not match (error)', () => {
expect(() => {
expectUnderTest(() => {
throw new CustomError('Invalid array length');
}).not[toThrow](/ array /);
}).toThrowErrorMatchingSnapshot();
});
test('threw, but message should not match (non-error truthy)', () => {
expect(() => {
expectUnderTest(() => {
throw 404;
}).not[toThrow](/^[123456789]\d*/);
}).toThrowErrorMatchingSnapshot();
});
});
test.describe('error class', () => {
class SubErr extends Err {
constructor(message?: string) {
super(message);
// In a carefully written error subclass,
// name property is equal to constructor name.
this.name = this.constructor.name;
}
}
class SubSubErr extends SubErr {
constructor(message?: string) {
super(message);
// In a carefully written error subclass,
// name property is equal to constructor name.
this.name = this.constructor.name;
}
}
test('passes', () => {
expectUnderTest(() => {
throw new Err();
})[toThrow](Err);
expectUnderTest(() => {
throw new Err();
})[toThrow](CustomError);
expectUnderTest(() => {
throw new Err();
}).not[toThrow](Err2);
expectUnderTest(() => {}).not[toThrow](Err);
});
test('did not throw at all', () => {
expect(() =>
expect(() => {})[toThrow](Err),
).toThrowErrorMatchingSnapshot();
});
test('threw, but class did not match (error)', () => {
expect(() => {
expectUnderTest(() => {
throw new Err('apple');
})[toThrow](Err2);
}).toThrowErrorMatchingSnapshot();
});
test('threw, but class did not match (non-error falsey)', () => {
expect(() => {
expectUnderTest(() => {
throw undefined;
})[toThrow](Err2);
}).toThrowErrorMatchingSnapshot();
});
test('threw, but class should not match (error)', () => {
expect(() => {
expectUnderTest(() => {
throw new Err('apple');
}).not[toThrow](Err);
}).toThrowErrorMatchingSnapshot();
});
test('threw, but class should not match (error subclass)', () => {
expect(() => {
expectUnderTest(() => {
throw new SubErr('apple');
}).not[toThrow](Err);
}).toThrowErrorMatchingSnapshot();
});
test('threw, but class should not match (error subsubclass)', () => {
expect(() => {
expectUnderTest(() => {
throw new SubSubErr('apple');
}).not[toThrow](Err);
}).toThrowErrorMatchingSnapshot();
});
});
test.describe('error-message', () => {
// Received message in report if object has message property.
class ErrorMessage {
// not extending Error!
constructor(public message: string) {}
}
const expected = new ErrorMessage('apple');
test.describe('pass', () => {
test('isNot false', () => {
expectUnderTest(() => {
throw new ErrorMessage('apple');
})[toThrow](expected);
});
test('isNot true', () => {
expectUnderTest(() => {
throw new ErrorMessage('banana');
}).not[toThrow](expected);
});
});
test.describe('fail', () => {
test('isNot false', () => {
expect(() =>
expectUnderTest(() => {
throw new ErrorMessage('banana');
})[toThrow](expected),
).toThrowErrorMatchingSnapshot();
});
test('isNot true', () => {
const message = 'Invalid array length';
expect(() =>
expectUnderTest(() => {
throw new ErrorMessage(message);
}).not[toThrow]({ message }),
).toThrowErrorMatchingSnapshot();
});
test('multiline diff highlight incorrect expected space', () => {
// jest/issues/2673
const a =
"There is no route defined for key Settings. \nMust be one of: 'Home'";
const b =
"There is no route defined for key Settings.\nMust be one of: 'Home'";
expect(() =>
expectUnderTest(() => {
throw new ErrorMessage(b);
})[toThrow]({ message: a }),
).toThrowErrorMatchingSnapshot();
});
});
});
test.describe('error message and cause', () => {
const errorA = new Error('A');
const errorB = new Error('B', { cause: errorA });
const expected = new Error('good', { cause: errorB });
test.describe('pass', () => {
test('isNot false', () => {
expectUnderTest(() => {
throw new Error('good', { cause: errorB });
})[toThrow](expected);
});
test('isNot true, incorrect message', () => {
expectUnderTest(() => {
throw new Error('bad', { cause: errorB });
}).not[toThrow](expected);
});
test('isNot true, incorrect cause', () => {
expectUnderTest(() => {
throw new Error('good', { cause: errorA });
}).not[toThrow](expected);
});
});
test.describe('fail', () => {
test('isNot false, incorrect message', () => {
expect(() =>
expectUnderTest(() => {
throw new Error('bad', { cause: errorB });
})[toThrow](expected),
).toThrow(
/^(?=.*Expected message and cause: ).*Received message and cause: /s,
);
});
test('isNot true, incorrect cause', () => {
expect(() =>
expectUnderTest(() => {
throw new Error('good', { cause: errorA });
})[toThrow](expected),
).toThrow(
/^(?=.*Expected message and cause: ).*Received message and cause: /s,
);
});
});
});
test.describe('asymmetric', () => {
test.describe('any-Class', () => {
test.describe('pass', () => {
test('isNot false', () => {
expectUnderTest(() => {
throw new Err('apple');
})[toThrow](expect.any(Err));
});
test('isNot true', () => {
expectUnderTest(() => {
throw new Err('apple');
}).not[toThrow](expect.any(Err2));
});
});
test.describe('fail', () => {
test('isNot false', () => {
expect(() =>
expectUnderTest(() => {
throw new Err('apple');
})[toThrow](expect.any(Err2)),
).toThrowErrorMatchingSnapshot();
});
test('isNot true', () => {
expect(() =>
expectUnderTest(() => {
throw new Err('apple');
}).not[toThrow](expect.any(Err)),
).toThrowErrorMatchingSnapshot();
});
});
});
test.describe('anything', () => {
test.describe('pass', () => {
test('isNot false', () => {
expectUnderTest(() => {
throw new CustomError('apple');
})[toThrow](expect.anything());
});
test('isNot true', () => {
expectUnderTest(() => {}).not[toThrow](expect.anything());
expectUnderTest(() => {
throw null;
}).not[toThrow](expect.anything());
});
});
test.describe('fail', () => {
test('isNot false', () => {
expect(() =>
expectUnderTest(() => {
throw null;
})[toThrow](expect.anything()),
).toThrowErrorMatchingSnapshot();
});
test('isNot true', () => {
expect(() =>
expectUnderTest(() => {
throw new CustomError('apple');
}).not[toThrow](expect.anything()),
).toThrowErrorMatchingSnapshot();
});
});
});
test.describe('no-symbol', () => {
// Test serialization of asymmetric matcher which has no property:
// this.$$typeof = Symbol.for('jest.asymmetricMatcher')
const matchError = {
asymmetricMatch(received: Error | null | undefined) {
return (
received !== null &&
received !== undefined &&
received.name === 'Error'
);
},
};
const matchNotError = {
asymmetricMatch(received: Error | null | undefined) {
return (
received !== null &&
received !== undefined &&
received.name !== 'Error'
);
},
};
test.describe('pass', () => {
test('isNot false', () => {
expectUnderTest(() => {
throw new CustomError('apple');
})[toThrow](matchError);
});
test('isNot true', () => {
expectUnderTest(() => {
throw new CustomError('apple');
}).not[toThrow](matchNotError);
});
});
test.describe('fail', () => {
test('isNot false', () => {
expect(() =>
expectUnderTest(() => {
throw new CustomError('apple');
})[toThrow](matchNotError),
).toThrowErrorMatchingSnapshot();
});
test('isNot true', () => {
expect(() =>
expectUnderTest(() => {
throw new CustomError('apple');
}).not[toThrow](matchError),
).toThrowErrorMatchingSnapshot();
});
});
});
test.describe('objectContaining', () => {
const matchError = expect.objectContaining({
name: 'Error',
});
const matchNotError = expect.objectContaining({
name: 'NotError',
});
test.describe('pass', () => {
test('isNot false', () => {
expectUnderTest(() => {
throw new CustomError('apple');
})[toThrow](matchError);
});
test('isNot true', () => {
expectUnderTest(() => {
throw new CustomError('apple');
}).not[toThrow](matchNotError);
});
});
test.describe('fail', () => {
test('isNot false', () => {
expect(() =>
expectUnderTest(() => {
throw new CustomError('apple');
})[toThrow](matchNotError),
).toThrowErrorMatchingSnapshot();
});
test('isNot true', () => {
expect(() =>
expectUnderTest(() => {
throw new CustomError('apple');
}).not[toThrow](matchError),
).toThrowErrorMatchingSnapshot();
});
});
});
});
test.describe('promise/async throws if Error-like object is returned', () => {
const asyncFn = async (shouldThrow?: boolean, resolve?: boolean) => {
let err;
if (shouldThrow)
err = new Err('async apple');
if (resolve)
return Promise.resolve(err || 'apple');
else
return Promise.reject(err || 'apple');
};
test('passes', async () => {
await expectUnderTest(Promise.reject(new Error())).rejects[toThrow]();
await expectUnderTest(asyncFn(true)).rejects[toThrow]();
await expectUnderTest(asyncFn(true)).rejects[toThrow](Err);
await expectUnderTest(asyncFn(true)).rejects[toThrow](Error);
await expectUnderTest(asyncFn(true)).rejects[toThrow]('apple');
await expectUnderTest(asyncFn(true)).rejects[toThrow](/app/);
await expectUnderTest(asyncFn(true)).rejects.not[toThrow](Err2);
await expectUnderTest(asyncFn(true)).rejects.not[toThrow]('banana');
await expectUnderTest(asyncFn(true)).rejects.not[toThrow](/banana/);
await expectUnderTest(asyncFn(true, true)).resolves[toThrow]();
await expectUnderTest(asyncFn(false, true)).resolves.not[toThrow]();
await expectUnderTest(asyncFn(false, true)).resolves.not[toThrow](Error);
await expectUnderTest(asyncFn(false, true)).resolves.not[toThrow]('apple');
await expectUnderTest(asyncFn(false, true)).resolves.not[toThrow](/apple/);
await expectUnderTest(asyncFn(false, true)).resolves.not[toThrow]('banana');
await expectUnderTest(asyncFn(false, true)).resolves.not[toThrow](/banana/);
await expectUnderTest(asyncFn()).rejects.not[toThrow]();
await expectUnderTest(asyncFn()).rejects.not[toThrow](Error);
await expectUnderTest(asyncFn()).rejects.not[toThrow]('apple');
await expectUnderTest(asyncFn()).rejects.not[toThrow](/apple/);
await expectUnderTest(asyncFn()).rejects.not[toThrow]('banana');
await expectUnderTest(asyncFn()).rejects.not[toThrow](/banana/);
// Works with nested functions inside promises
await expectUnderTest(
Promise.reject(() => {
throw new Error();
}),
).rejects[toThrow]();
await expectUnderTest(Promise.reject(() => {})).rejects.not[toThrow]();
});
test('did not throw at all', async () => {
await expectUnderTestAsAny(
expectUnderTest(asyncFn()).rejects[toThrow](),
).rejects.toThrowErrorMatchingSnapshot();
});
test('threw, but class did not match', async () => {
await expectUnderTestAsAny(
expectUnderTest(asyncFn(true)).rejects[toThrow](Err2),
).rejects.toThrowErrorMatchingSnapshot();
});
test('threw, but should not have', async () => {
await expectUnderTestAsAny(
expectUnderTest(asyncFn(true)).rejects.not[toThrow](),
).rejects.toThrowErrorMatchingSnapshot();
});
});
test.describe('expected is undefined', () => {
test('threw, but should not have (non-error falsey)', () => {
expect(() => {
expectUnderTest(() => {
throw null;
}).not[toThrow]();
}).toThrowErrorMatchingSnapshot();
});
});
test('invalid arguments', () => {
expect(() =>
expectUnderTest(() => {}).not[toThrow](111),
).toThrowErrorMatchingSnapshot();
});
test('invalid actual', () => {
expect(() =>
expectUnderTest('a string')[toThrow](),
).toThrowErrorMatchingSnapshot();
});
});
}