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

fix: don't assume stack is always a string #10697

Merged
merged 17 commits into from Oct 27, 2020
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -8,6 +8,7 @@

- `[jest-config]` Fix bug introduced in watch mode by PR[#10678](https://github.com/facebook/jest/pull/10678/files#r511037803) ([#10692](https://github.com/facebook/jest/pull/10692))
- `[expect]` Stop modifying the sample in `expect.objectContaining()` ([#10711](https://github.com/facebook/jest/pull/10711))
- `[jest-jasmine2]` fix: don't assume stack to be a string always ([#10697](https://github.com/facebook/jest/pull/10697))
- `[jest-resolve-dependencies]` Resolve mocks as dependencies ([#10713](https://github.com/facebook/jest/pull/10713))

### Chore & Maintenance
Expand Down
76 changes: 50 additions & 26 deletions e2e/__tests__/__snapshots__/failures.test.ts.snap
Expand Up @@ -117,6 +117,7 @@ FAIL __tests__/duringTests.test.js
✕ Boolean thrown during test
✕ undefined thrown during test
✕ Object thrown during test
✕ Object with stack prop thrown during test
✕ Error during test
✕ done(Error)
✕ done(non-error)
Expand Down Expand Up @@ -185,33 +186,49 @@ FAIL __tests__/duringTests.test.js

at Object.test (__tests__/duringTests.test.js:28:1)

● Object with stack prop thrown during test

thrown: Object {
"stack": 42,
}

30 | });
31 |
> 32 | test('Object with stack prop thrown during test', () => {
| ^
33 | // eslint-disable-next-line no-throw-literal
34 | throw {stack: 42};
35 | });

at Object.test (__tests__/duringTests.test.js:32:1)

● Error during test

ReferenceError: doesNotExist is not defined

32 | test('Error during test', () => {
33 | // eslint-disable-next-line no-undef
> 34 | doesNotExist.alsoThisNot;
37 | test('Error during test', () => {
38 | // eslint-disable-next-line no-undef
> 39 | doesNotExist.alsoThisNot;
| ^
35 | });
36 |
37 | test('done(Error)', done => {
40 | });
41 |
42 | test('done(Error)', done => {

at Object.doesNotExist (__tests__/duringTests.test.js:34:3)
at Object.doesNotExist (__tests__/duringTests.test.js:39:3)

● done(Error)

this is an error

36 |
37 | test('done(Error)', done => {
> 38 | done(new Error('this is an error'));
41 |
42 | test('done(Error)', done => {
> 43 | done(new Error('this is an error'));
| ^
39 | });
40 |
41 | test('done(non-error)', done => {
44 | });
45 |
46 | test('done(non-error)', done => {

at Object.<anonymous> (__tests__/duringTests.test.js:38:8)
at Object.<anonymous> (__tests__/duringTests.test.js:43:8)

● done(non-error)

Expand All @@ -224,15 +241,15 @@ FAIL __tests__/duringTests.test.js
],
}

40 |
41 | test('done(non-error)', done => {
> 42 | done(deepObject);
45 |
46 | test('done(non-error)', done => {
> 47 | done(deepObject);
| ^
43 | });
44 |
45 | test('returned promise rejection', () => Promise.reject(deepObject));
48 | });
49 |
50 | test('returned promise rejection', () => Promise.reject(deepObject));

at Object.done (__tests__/duringTests.test.js:42:3)
at Object.done (__tests__/duringTests.test.js:47:3)

● returned promise rejection

Expand All @@ -245,13 +262,20 @@ FAIL __tests__/duringTests.test.js
],
}

43 | });
44 |
> 45 | test('returned promise rejection', () => Promise.reject(deepObject));
48 | });
49 |
> 50 | test('returned promise rejection', () => Promise.reject(deepObject));
| ^
46 |
51 |

at Object.test (__tests__/duringTests.test.js:50:1)
`;

exports[`not throwing Error objects 6`] = `
FAIL __tests__/throwObjectWithStackProp.test.js
● Test suite failed to run

at Object.test (__tests__/duringTests.test.js:45:1)
42
SimenB marked this conversation as resolved.
Show resolved Hide resolved
`;

exports[`works with assertions in separate files 1`] = `
Expand Down
4 changes: 3 additions & 1 deletion e2e/__tests__/failures.test.ts
Expand Up @@ -40,7 +40,7 @@ test('not throwing Error objects', () => {
stderr = runJest(dir, ['duringTests.test.js']).stderr;

if (nodeMajorVersion < 12) {
const lineEntry = '(__tests__/duringTests.test.js:38:8)';
const lineEntry = '(__tests__/duringTests.test.js:43:8)';

expect(stderr).toContain(`at Object.<anonymous>.done ${lineEntry}`);

Expand All @@ -51,6 +51,8 @@ test('not throwing Error objects', () => {
}

expect(wrap(cleanStderr(stderr))).toMatchSnapshot();
stderr = runJest(dir, ['throwObjectWithStackProp.test.js']).stderr;
expect(wrap(cleanStderr(stderr))).toMatchSnapshot();
});

test('works with node assert', () => {
Expand Down
5 changes: 5 additions & 0 deletions e2e/failures/__tests__/duringTests.test.js
Expand Up @@ -29,6 +29,11 @@ test('Object thrown during test', () => {
throw deepObject;
});

test('Object with stack prop thrown during test', () => {
// eslint-disable-next-line no-throw-literal
throw {stack: 42};
});

test('Error during test', () => {
// eslint-disable-next-line no-undef
doesNotExist.alsoThisNot;
Expand Down
11 changes: 11 additions & 0 deletions e2e/failures/__tests__/throwObjectWithStackProp.test.js
@@ -0,0 +1,11 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

'use strict';

// eslint-disable-next-line no-throw-literal
throw {stack: 42};
@@ -1,5 +1,11 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`expectationResultFactory returns the result if failed (with \`error.stack\` not as a string). 1`] = `
"thrown: Object {
\\"stack\\": 42,
}"
`;

exports[`expectationResultFactory returns the result if passed. 1`] = `
Object {
"error": undefined,
Expand Down
Expand Up @@ -78,4 +78,16 @@ describe('expectationResultFactory', () => {
const result = expectationResultFactory(options);
expect(result.message).toEqual('Expected `Pass`, received `Fail`.');
});

it('returns the result if failed (with `error.stack` not as a string).', () => {
const options = {
actual: 'Fail',
error: {stack: 42},
expected: 'Pass',
matcherName: 'testMatcher',
passed: false,
};
const result = expectationResultFactory(options);
expect(result.message).toMatchSnapshot();
});
});
2 changes: 1 addition & 1 deletion packages/jest-jasmine2/src/expectationResultFactory.ts
Expand Up @@ -43,7 +43,7 @@ function stackFormatter(
}

if (options.error) {
if (options.error.stack) {
if (typeof options.error.stack === 'string') {
return options.error.stack;
}

Expand Down
12 changes: 8 additions & 4 deletions packages/jest-jasmine2/src/reporter.ts
Expand Up @@ -113,15 +113,19 @@ export default class Jasmine2Reporter implements Reporter {
return this._resultsPromise;
}

private _addMissingMessageToStack(stack: string, message?: string) {
private _addMissingMessageToStack(stack: unknown, message?: string) {
SimenB marked this conversation as resolved.
Show resolved Hide resolved
// Some errors (e.g. Angular injection error) don't prepend error.message
// to stack, instead the first line of the stack is just plain 'Error'
const ERROR_REGEX = /^Error:?\s*\n/;

if (stack && message && !stack.includes(message)) {
return message + stack.replace(ERROR_REGEX, '\n');
if (typeof stack === 'string') {
if (stack && message && !stack.includes(message)) {
return message + stack.replace(ERROR_REGEX, '\n');
}
return stack;
} else {
return `${stack}`;
}
return stack;
}

private _extractSpecResults(
Expand Down
10 changes: 6 additions & 4 deletions packages/jest-message-util/src/index.ts
Expand Up @@ -380,7 +380,7 @@ const removeBlankErrorLine = (str: string) =>
// Error object, so we have to regexp out the message from the stack string
// to format it.
export const separateMessageFromStack = (
content: string,
content: unknown,
): {message: string; stack: string} => {
if (!content) {
return {message: '', stack: ''};
Expand All @@ -390,9 +390,11 @@ export const separateMessageFromStack = (
// (maybe it's a code frame instead), just the first non-empty line.
// If the error is a plain "Error:" instead of a SyntaxError or TypeError we
// remove the prefix from the message because it is generally not useful.
const messageMatch = content.match(
/^(?:Error: )?([\s\S]*?(?=\n\s*at\s.*:\d*:\d*)|\s*.*)([\s\S]*)$/,
);
const ERROR_REGEXP = /^(?:Error: )?([\s\S]*?(?=\n\s*at\s.*:\d*:\d*)|\s*.*)([\s\S]*)$/;
const messageMatch =
typeof content !== 'string'
? `${content}`.match(ERROR_REGEXP)
SimenB marked this conversation as resolved.
Show resolved Hide resolved
: content.match(ERROR_REGEXP);
if (!messageMatch) {
// For typescript
throw new Error('If you hit this error, the regex above is buggy.');
Expand Down