Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update toThrow() to be able to use Error.cause #13606

Merged
merged 18 commits into from Feb 15, 2023
Merged
54 changes: 54 additions & 0 deletions packages/expect/src/__tests__/toThrowMatchers.test.ts
Expand Up @@ -278,6 +278,60 @@ describe.each(['toThrowError', 'toThrow'] as const)('%s', toThrow => {
});
});

describe('error message and cause', () => {
const errorA = new Error('A');
const errorB = new Error('B', {cause: errorA});
const expected = new Error('good', {cause: errorB});

describe('pass', () => {
test('isNot false', () => {
jestExpect(() => {
throw new Error('good', {cause: errorB});
})[toThrow](expected);
});

test('isNot true, incorrect message', () => {
jestExpect(() => {
throw new Error('bad', {cause: errorB});
}).not[toThrow](expected);
});

test('isNot true, incorrect cause', () => {
// only v16 or higher support Error.cause
if (Number(process.version.split('.')[0].slice(1)) >= 16) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

jestExpect(() => {
throw new Error('good', {cause: errorA});
}).not[toThrow](expected);
}
});
});

describe('fail', () => {
// only v16 or higher support Error.cause
if (Number(process.version.split('.')[0].slice(1)) >= 16) {
test('isNot false, incorrect message', () => {
expect(() =>
jestExpect(() => {
throw new Error('bad', {cause: errorB});
})[toThrow](expected),
).toThrow(
/^(?=.*Expected message and cause: ).*Received message and cause: /s,
);
});

test('isNot true, incorrect cause', () => {
expect(() =>
jestExpect(() => {
throw new Error('good', {cause: errorA});
})[toThrow](expected),
).toThrow(
/^(?=.*Expected message and cause: ).*Received message and cause: /s,
);
});
}
});
});

describe('asymmetric', () => {
describe('any-Class', () => {
describe('pass', () => {
Expand Down
3 changes: 3 additions & 0 deletions packages/expect/src/__tests__/tsconfig.json
@@ -1,5 +1,8 @@
{
"extends": "../../../../tsconfig.test.json",
"compilerOptions": {
"lib": ["es2022.error"]
},
"include": ["./**/*"],
"references": [{"path": "../../"}]
}
51 changes: 42 additions & 9 deletions packages/expect/src/toThrowMatchers.ts
Expand Up @@ -225,14 +225,20 @@ const toThrowExpectedObject = (
thrown: Thrown | null,
expected: Error,
): SyncExpectationResult => {
const pass = thrown !== null && thrown.message === expected.message;
const pass =
thrown !== null &&
thrown.message === expected.message &&
createMessageAndCause(thrown.value) === createMessageAndCause(expected);

const message = pass
? () =>
// eslint-disable-next-line prefer-template
matcherHint(matcherName, undefined, undefined, options) +
'\n\n' +
formatExpected('Expected message: not ', expected.message) +
formatExpected(
`Expected ${messageAndCause(expected)}: not `,
createMessageAndCause(expected),
) +
(thrown !== null && thrown.hasMessage
? formatStack(thrown)
: formatReceived('Received value: ', thrown, 'value'))
Expand All @@ -242,22 +248,27 @@ const toThrowExpectedObject = (
'\n\n' +
(thrown === null
? // eslint-disable-next-line prefer-template
formatExpected('Expected message: ', expected.message) +
formatExpected(
`Expected ${messageAndCause(expected)}: `,
createMessageAndCause(expected),
) +
'\n' +
DID_NOT_THROW
: thrown.hasMessage
? // eslint-disable-next-line prefer-template
printDiffOrStringify(
expected.message,
thrown.message,
'Expected message',
'Received message',
createMessageAndCause(expected),
createMessageAndCause(thrown.value),
`Expected ${messageAndCause(expected)}`,
`Received ${messageAndCause(thrown.value)}`,
true,
) +
'\n' +
formatStack(thrown)
: formatExpected('Expected message: ', expected.message) +
formatReceived('Received value: ', thrown, 'value'));
: formatExpected(
`Expected ${messageAndCause(expected)}: `,
createMessageAndCause(expected),
) + formatReceived('Received value: ', thrown, 'value'));

return {message, pass};
};
Expand Down Expand Up @@ -447,4 +458,26 @@ const formatStack = (thrown: Thrown | null) =>
},
);

const _createMessageAndCause = (error: Error): string => {
if (error.cause instanceof Error) {
return `{ message: ${error.message}, cause: ${_createMessageAndCause(
error.cause,
)}}`;
} else {
return `{ message: ${error.message} }`;
}
};

const createMessageAndCause = (error: Error) => {
if (error.cause instanceof Error) {
return _createMessageAndCause(error);
} else {
return error.message;
}
};
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why are these 2 functions?

Copy link
Contributor Author

@ibuibu ibuibu Nov 15, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently, if { message: 'foo' } is given, the standard output is "Expected message: foo".

To maintain this current behavior, I separated the functions so that only error.message is returned in the first loop.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_createMessageAndCause is only called from within itself or createMessageAndCause, can't they be combined?


const messageAndCause = (error: Error) => {
return error.cause === undefined ? 'message' : 'message and cause';
};

export default matchers;
2 changes: 1 addition & 1 deletion packages/expect/tsconfig.json
@@ -1,7 +1,7 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"lib": ["es2020", "dom"],
"lib": ["es2020", "es2021.promise", "es2022.error", "dom"],
"rootDir": "src",
"outDir": "build"
},
Expand Down