From 5d6c829de2b199f092c25115ac504e28626b0070 Mon Sep 17 00:00:00 2001 From: Simen Bekkhus Date: Fri, 15 Dec 2017 11:33:17 +0100 Subject: [PATCH] feat(jest-message-util): render codeframe on failure --- CHANGELOG.md | 2 + docs/Configuration.md | 8 +- docs/TimerMocks.md | 10 +- .../__snapshots__/failures.test.js.snap | 116 ++++++++++++++++++ integration_tests/__tests__/failures.test.js | 7 ++ packages/jest-message-util/package.json | 1 + packages/jest-message-util/src/index.js | 34 ++++- yarn.lock | 8 ++ 8 files changed, 178 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a4065c478427..6d864b74095d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -69,6 +69,8 @@ ### Features +* `[jest-message-util]` Add codeframe to test assertion failures + ([#5087](https://github.com/facebook/jest/pull/5087)) * `[jest-config]` Add Global Setup/Teardown options ([#4716](https://github.com/facebook/jest/pull/4716)) * `[jest-config]` Add `testEnvironmentOptions` to apply to jsdom options or node diff --git a/docs/Configuration.md b/docs/Configuration.md index ed2bd92478b7..e3e8037681fc 100644 --- a/docs/Configuration.md +++ b/docs/Configuration.md @@ -262,14 +262,18 @@ here, and some code mutates that value in the midst of running a test, that mutation will _not_ be persisted across test runs for other test files. ### `globalSetup` [string] + Default: `undefined` -This option allows the use of a custom global setup module which exports an async function that is triggered once before all test suites. +This option allows the use of a custom global setup module which exports an +async function that is triggered once before all test suites. ### `globalTeardown` [string] + Default: `undefined` -This option allows the use of a custom global teardown module which exports an async function that is triggered once after all test suites. +This option allows the use of a custom global teardown module which exports an +async function that is triggered once after all test suites. ### `mapCoverage` [boolean] diff --git a/docs/TimerMocks.md b/docs/TimerMocks.md index b0e529983980..531543d779a1 100644 --- a/docs/TimerMocks.md +++ b/docs/TimerMocks.md @@ -34,7 +34,7 @@ test('waits 1 second before ending the game', () => { const timerGame = require('../timerGame'); timerGame(); - expect(setTimeout).toHaveBeenCalledTimes(1) + expect(setTimeout).toHaveBeenCalledTimes(1); expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), 1000); }); ``` @@ -63,7 +63,7 @@ test('calls the callback after 1 second', () => { // Now our callback should have been called! expect(callback).toBeCalled(); - expect(callback).toHaveBeenCalledTimes(1) + expect(callback).toHaveBeenCalledTimes(1); }); ``` @@ -110,7 +110,7 @@ describe('infiniteTimerGame', () => { // At this point in time, there should have been a single call to // setTimeout to schedule the end of the game in 1 second. - expect(setTimeout).toHaveBeenCalledTimes(1) + expect(setTimeout).toHaveBeenCalledTimes(1); expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), 1000); // Fast forward and exhaust only currently pending timers @@ -122,7 +122,7 @@ describe('infiniteTimerGame', () => { // And it should have created a new timer to start the game over in // 10 seconds - expect(setTimeout).toHaveBeenCalledTimes(2) + expect(setTimeout).toHaveBeenCalledTimes(2); expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), 10000); }); }); @@ -170,7 +170,7 @@ it('calls the callback after 1 second via advanceTimersByTime', () => { // Now our callback should have been called! expect(callback).toBeCalled(); - expect(callback).toHaveBeenCalledTimes(1) + expect(callback).toHaveBeenCalledTimes(1); }); ``` diff --git a/integration_tests/__tests__/__snapshots__/failures.test.js.snap b/integration_tests/__tests__/__snapshots__/failures.test.js.snap index f484a661fb30..c0775600863a 100644 --- a/integration_tests/__tests__/__snapshots__/failures.test.js.snap +++ b/integration_tests/__tests__/__snapshots__/failures.test.js.snap @@ -40,6 +40,12 @@ exports[`not throwing Error objects 4`] = ` expect(received).toBeTruthy() Expected value to be truthy, instead received false + 11 | const throws = () => { + 12 | expect.assertions(2); + > 13 | expect(false).toBeTruthy(); + 14 | }; + 15 | const redeclare = () => { + 16 | expect.assertions(1); at __tests__/assertion_count.test.js:13:17 ● .assertions() › throws expect.assertions(2) @@ -48,6 +54,12 @@ exports[`not throwing Error objects 4`] = ` expect(received).toBeTruthy() Expected value to be truthy, instead received false + 15 | const redeclare = () => { + 16 | expect.assertions(1); + > 17 | expect(false).toBeTruthy(); + 18 | expect.assertions(2); + 19 | }; + 20 | at __tests__/assertion_count.test.js:17:17 ● .assertions() › throws on assertion expect.assertions(0) @@ -85,6 +97,13 @@ exports[`works with node assert 1`] = ` true Received: false + + 13 | + 14 | test('assert', () => { + > 15 | assert(false); + 16 | }); + 17 | + 18 | test('assert with a message', () => { at __tests__/node_assertion_error.test.js:15:3 @@ -99,6 +118,13 @@ exports[`works with node assert 1`] = ` Message: this is a message + + 17 | + 18 | test('assert with a message', () => { + > 19 | assert(false, 'this is a message'); + 20 | }); + 21 | + 22 | test('assert.ok', () => { at __tests__/node_assertion_error.test.js:19:3 @@ -110,6 +136,13 @@ exports[`works with node assert 1`] = ` true Received: false + + 21 | + 22 | test('assert.ok', () => { + > 23 | assert.ok(false); + 24 | }); + 25 | + 26 | test('assert.ok with a message', () => { at __tests__/node_assertion_error.test.js:23:10 @@ -124,6 +157,13 @@ exports[`works with node assert 1`] = ` Message: this is a message + + 25 | + 26 | test('assert.ok with a message', () => { + > 27 | assert.ok(false, 'this is a message'); + 28 | }); + 29 | + 30 | test('assert.equal', () => { at __tests__/node_assertion_error.test.js:27:10 @@ -135,6 +175,13 @@ exports[`works with node assert 1`] = ` 2 Received: 1 + + 29 | + 30 | test('assert.equal', () => { + > 31 | assert.equal(1, 2); + 32 | }); + 33 | + 34 | test('assert.notEqual', () => { at __tests__/node_assertion_error.test.js:31:10 @@ -150,6 +197,13 @@ exports[`works with node assert 1`] = ` Difference: Compared values have no visual difference. + + 33 | + 34 | test('assert.notEqual', () => { + > 35 | assert.notEqual(1, 1); + 36 | }); + 37 | + 38 | test('assert.deepEqual', () => { at __tests__/node_assertion_error.test.js:35:10 @@ -175,6 +229,13 @@ exports[`works with node assert 1`] = ` }, }, } + + 37 | + 38 | test('assert.deepEqual', () => { + > 39 | assert.deepEqual({a: {b: {c: 5}}}, {a: {b: {c: 6}}}); + 40 | }); + 41 | + 42 | test('assert.deepEqual with a message', () => { at __tests__/node_assertion_error.test.js:39:10 @@ -203,6 +264,13 @@ exports[`works with node assert 1`] = ` }, }, } + + 41 | + 42 | test('assert.deepEqual with a message', () => { + > 43 | assert.deepEqual({a: {b: {c: 5}}}, {a: {b: {c: 7}}}, 'this is a message'); + 44 | }); + 45 | + 46 | test('assert.notDeepEqual', () => { at __tests__/node_assertion_error.test.js:43:10 @@ -218,6 +286,13 @@ exports[`works with node assert 1`] = ` Difference: Compared values have no visual difference. + + 45 | + 46 | test('assert.notDeepEqual', () => { + > 47 | assert.notDeepEqual({a: 1}, {a: 1}); + 48 | }); + 49 | + 50 | test('assert.strictEqual', () => { at __tests__/node_assertion_error.test.js:47:10 @@ -229,6 +304,13 @@ exports[`works with node assert 1`] = ` NaN Received: 1 + + 49 | + 50 | test('assert.strictEqual', () => { + > 51 | assert.strictEqual(1, NaN); + 52 | }); + 53 | + 54 | test('assert.notStrictEqual', () => { at __tests__/node_assertion_error.test.js:51:10 @@ -247,6 +329,13 @@ exports[`works with node assert 1`] = ` Difference: Compared values have no visual difference. + + 53 | + 54 | test('assert.notStrictEqual', () => { + > 55 | assert.notStrictEqual(1, 1, 'My custom error message'); + 56 | }); + 57 | + 58 | test('assert.deepStrictEqual', () => { at __tests__/node_assertion_error.test.js:55:10 @@ -268,6 +357,13 @@ exports[`works with node assert 1`] = ` - \\"a\\": 2, + \\"a\\": 1, } + + 57 | + 58 | test('assert.deepStrictEqual', () => { + > 59 | assert.deepStrictEqual({a: 1}, {a: 2}); + 60 | }); + 61 | + 62 | test('assert.notDeepStrictEqual', () => { at __tests__/node_assertion_error.test.js:59:10 @@ -283,6 +379,13 @@ exports[`works with node assert 1`] = ` Difference: Compared values have no visual difference. + + 61 | + 62 | test('assert.notDeepStrictEqual', () => { + > 63 | assert.notDeepStrictEqual({a: 1}, {a: 1}); + 64 | }); + 65 | + 66 | test('assert.ifError', () => { at __tests__/node_assertion_error.test.js:63:10 @@ -301,6 +404,13 @@ exports[`works with node assert 1`] = ` Message: Got unwanted exception. + + 69 | + 70 | test('assert.doesNotThrow', () => { + > 71 | assert.doesNotThrow(() => { + 72 | throw Error('err!'); + 73 | }); + 74 | }); at __tests__/node_assertion_error.test.js:71:10 @@ -313,6 +423,12 @@ exports[`works with node assert 1`] = ` Message: Missing expected exception. + + 75 | + 76 | test('assert.throws', () => { + > 77 | assert.throws(() => {}); + 78 | }); + 79 | at __tests__/node_assertion_error.test.js:77:10 diff --git a/integration_tests/__tests__/failures.test.js b/integration_tests/__tests__/failures.test.js index c163b00f297e..2224d48030c5 100644 --- a/integration_tests/__tests__/failures.test.js +++ b/integration_tests/__tests__/failures.test.js @@ -72,6 +72,13 @@ test('works with node assert', () => { Got unwanted exception. err! err! + + 69 | + 70 | test('assert.doesNotThrow', () => { + > 71 | assert.doesNotThrow(() => { + 72 | throw Error('err!'); + 73 | }); + 74 | }); at __tests__/node_assertion_error.test.js:71:10 `); diff --git a/packages/jest-message-util/package.json b/packages/jest-message-util/package.json index ff5a7c7018ab..05168fe7ee3a 100644 --- a/packages/jest-message-util/package.json +++ b/packages/jest-message-util/package.json @@ -8,6 +8,7 @@ "license": "MIT", "main": "build/index.js", "dependencies": { + "@babel/code-frame": "^7.0.0-beta.35", "chalk": "^2.0.1", "micromatch": "^2.3.11", "slash": "^1.0.0", diff --git a/packages/jest-message-util/src/index.js b/packages/jest-message-util/src/index.js index 3ed544fdffbb..7f871b13ec21 100644 --- a/packages/jest-message-util/src/index.js +++ b/packages/jest-message-util/src/index.js @@ -10,10 +10,12 @@ import type {Glob, Path} from 'types/Config'; import type {AssertionResult, TestResult} from 'types/TestResult'; +import fs from 'fs'; import path from 'path'; import chalk from 'chalk'; import micromatch from 'micromatch'; import slash from 'slash'; +import {codeFrameColumns} from '@babel/code-frame'; import StackUtils from 'stack-utils'; let nodeInternals = []; @@ -194,15 +196,45 @@ export const formatStackTrace = ( testPath: ?Path, ) => { let lines = stack.split(/\n/); + let renderedCallsite = ''; const relativeTestPath = testPath ? slash(path.relative(config.rootDir, testPath)) : null; lines = removeInternalStackEntries(lines, options); - return lines + + if (testPath) { + const topFrame = lines + .join('\n') + .trim() + .split('\n')[0]; + + const parsedFrame = StackUtils.parseLine(topFrame); + + if (parsedFrame) { + renderedCallsite = codeFrameColumns( + fs.readFileSync(testPath, 'utf8'), + { + start: {line: parsedFrame.line}, + }, + {highlightCode: true}, + ); + + renderedCallsite = renderedCallsite + .split('\n') + .map(line => MESSAGE_INDENT + line) + .join('\n'); + + renderedCallsite = `\n${renderedCallsite}\n`; + } + } + + const stacktrace = lines .map(trimPaths) .map(formatPaths.bind(null, config, options, relativeTestPath)) .map(line => STACK_INDENT + line) .join('\n'); + + return renderedCallsite + stacktrace; }; export const formatResultsErrors = ( diff --git a/yarn.lock b/yarn.lock index ad7e65cf58d6..a59d9e7ab28b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,6 +2,14 @@ # yarn lockfile v1 +"@babel/code-frame@^7.0.0-beta.35": + version "7.0.0-beta.35" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.0.0-beta.35.tgz#04eeb6dca7efef8f65776a4c214157303b85ad51" + dependencies: + chalk "^2.0.0" + esutils "^2.0.2" + js-tokens "^3.0.0" + "@types/node@*": version "8.0.53" resolved "https://registry.yarnpkg.com/@types/node/-/node-8.0.53.tgz#396b35af826fa66aad472c8cb7b8d5e277f4e6d8"