diff --git a/integration_tests/__tests__/__snapshots__/failures-test.js.snap b/integration_tests/__tests__/__snapshots__/failures-test.js.snap index f48f7ff0a8c0..c45d6dc788da 100644 --- a/integration_tests/__tests__/__snapshots__/failures-test.js.snap +++ b/integration_tests/__tests__/__snapshots__/failures-test.js.snap @@ -31,54 +31,30 @@ exports[`not throwing Error objects 3`] = ` exports[`not throwing Error objects 4`] = ` " FAIL __tests__/assertion-count-test.js ● .assertions() › throws - expect(received).toBeTruthy() - Expected value to be truthy, instead received false - at __tests__/assertion-count-test.js:14:17 - ● .assertions() › throws - expect.assertions(2) - Expected two assertions to be called but only received one assertion call. - - at ../../packages/jest-jasmine2/build/setup-jest-globals.js:68:21 - ● .assertions() › throws on redeclare of assertion count - expect(received).toBeTruthy() - Expected value to be truthy, instead received false - at __tests__/assertion-count-test.js:18:17 - ● .assertions() › throws on assertion - expect.assertions(0) - Expected zero assertions to be called but only received one assertion call. - - at ../../packages/jest-jasmine2/build/setup-jest-globals.js:68:21 - ● .hasAssertions() › throws when there are not assertions - expect.hasAssertions() - Expected at least one assertion to be called but received none. - - at ../../packages/jest-jasmine2/build/setup-jest-globals.js:88:21 - .assertions() ✕ throws ✕ throws on redeclare of assertion count ✕ throws on assertion .hasAssertions() ✕ throws when there are not assertions - " `; diff --git a/integration_tests/__tests__/failures-test.js b/integration_tests/__tests__/failures-test.js index 4cc811315fda..46049519b11b 100644 --- a/integration_tests/__tests__/failures-test.js +++ b/integration_tests/__tests__/failures-test.js @@ -17,6 +17,30 @@ const normalizeDots = text => text.replace(/\.{1,}$/gm, '.'); skipOnWindows.suite(); +const cleanupStackTrace = stderr => { + const STACK_REGEXP = /^.*at.*(setup-jest-globals|extractExpectedAssertionsErrors).*\n/gm; + if (!STACK_REGEXP.test(stderr)) { + throw new Error( + ` + This function is used to remove inconsistent stack traces between + jest-jasmine2 and jest-circus. If you see this error, that means the + stack traces are no longer inconsistent and this function can be + safely removed. + + output: + ${stderr} + `, + ); + } + + return ( + stderr + .replace(STACK_REGEXP, '') + // Also remove trailing whitespace. + .replace(/\s+$/gm, '') + ); +}; + test('not throwing Error objects', () => { let stderr; stderr = runJest(dir, ['throw-number-test.js']).stderr; @@ -26,7 +50,7 @@ test('not throwing Error objects', () => { stderr = runJest(dir, ['throw-object-test.js']).stderr; expect(extractSummary(stderr).rest).toMatchSnapshot(); stderr = runJest(dir, ['assertion-count-test.js']).stderr; - expect(extractSummary(stderr).rest).toMatchSnapshot(); + expect(extractSummary(cleanupStackTrace(stderr)).rest).toMatchSnapshot(); }); test('works with node assert', () => { diff --git a/integration_tests/utils.js b/integration_tests/utils.js index d9dc3795959c..112c3e1bf68d 100644 --- a/integration_tests/utils.js +++ b/integration_tests/utils.js @@ -137,6 +137,7 @@ const cleanupStackTrace = (output: string) => { .replace(/\n.*at.*next_tick\.js.*$/gm, '') .replace(/\n.*at Promise \(\).*$/gm, '') .replace(/\n.*at .*$/gm, '') + .replace(/\n.*at Generator.next \(\).*$/gm, '') .replace(/^.*at.*[\s][\(]?(\S*\:\d*\:\d*).*$/gm, ' at $1'); }; diff --git a/packages/jest-circus/package.json b/packages/jest-circus/package.json index ed0243eed0ff..618423bf8924 100644 --- a/packages/jest-circus/package.json +++ b/packages/jest-circus/package.json @@ -10,7 +10,8 @@ "dependencies": { "jest-snapshot": "^20.0.3", "jest-matchers": "^20.0.3", - "jest-message-util": "^20.0.3" + "jest-message-util": "^20.0.3", + "jest-diff": "^20.0.3" }, "devDependencies": { "jest-runtime": "^20.0.3" diff --git a/packages/jest-circus/src/formatNodeAssertErrors.js b/packages/jest-circus/src/formatNodeAssertErrors.js new file mode 100644 index 000000000000..ed4d022afb02 --- /dev/null +++ b/packages/jest-circus/src/formatNodeAssertErrors.js @@ -0,0 +1,149 @@ +/** + * Copyright (c) 2014-present, Facebook, Inc. All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @flow + */ + +import type {DiffOptions} from 'jest-diff/src/diffStrings'; +import type {Event, State} from '../types'; + +const {printReceived, printExpected} = require('jest-matcher-utils'); +const chalk = require('chalk'); +const diff = require('jest-diff'); + +type AssertionError = {| + actual: ?string, + expected: ?string, + generatedMessage: boolean, + message: string, + name: string, + operator: ?string, + stack: string, +|}; + +const assertOperatorsMap = { + '!=': 'notEqual', + '!==': 'notStrictEqual', + '==': 'equal', + '===': 'strictEqual', +}; + +const humanReadableOperators = { + deepEqual: 'to deeply equal', + deepStrictEqual: 'to deeply and strictly equal', + notDeepEqual: 'not to deeply equal', + notDeepStrictEqual: 'not to deeply and strictly equal', +}; + +module.exports = (event: Event, state: State) => { + switch (event.name) { + case 'test_failure': + case 'test_success': { + event.test.errors = event.test.errors.map(error => { + return error instanceof require('assert').AssertionError + ? assertionErrorMessage(error, {expand: state.expand}) + : error; + }); + break; + } + } +}; + +const getOperatorName = (operator: ?string, stack: string) => { + if (typeof operator === 'string') { + return assertOperatorsMap[operator] || operator; + } + if (stack.match('.doesNotThrow')) { + return 'doesNotThrow'; + } + if (stack.match('.throws')) { + return 'throws'; + } + return ''; +}; + +const operatorMessage = (operator: ?string, negator: boolean) => + typeof operator === 'string' + ? operator.startsWith('!') || operator.startsWith('=') + ? `${negator ? 'not ' : ''}to be (operator: ${operator}):\n` + : `${humanReadableOperators[operator] || operator} to:\n` + : ''; + +const assertThrowingMatcherHint = (operatorName: string) => { + return ( + chalk.dim('assert') + + chalk.dim('.' + operatorName + '(') + + chalk.red('function') + + chalk.dim(')') + ); +}; + +const assertMatcherHint = (operator: ?string, operatorName: string) => { + let message = + chalk.dim('assert') + + chalk.dim('.' + operatorName + '(') + + chalk.red('received') + + chalk.dim(', ') + + chalk.green('expected') + + chalk.dim(')'); + + if (operator === '==') { + message += + ' or ' + + chalk.dim('assert') + + chalk.dim('(') + + chalk.red('received') + + chalk.dim(') '); + } + + return message; +}; + +function assertionErrorMessage(error: AssertionError, options: DiffOptions) { + const {expected, actual, message, operator, stack} = error; + const diffString = diff(expected, actual, options); + const negator = + typeof operator === 'string' && + (operator.startsWith('!') || operator.startsWith('not')); + const hasCustomMessage = !error.generatedMessage; + const operatorName = getOperatorName(operator, stack); + + if (operatorName === 'doesNotThrow') { + return ( + assertThrowingMatcherHint(operatorName) + + '\n\n' + + chalk.reset(`Expected the function not to throw an error.\n`) + + chalk.reset(`Instead, it threw:\n`) + + ` ${printReceived(actual)}` + + chalk.reset(hasCustomMessage ? '\n\nMessage:\n ' + message : '') + + stack.replace(/AssertionError(.*)/g, '') + ); + } + + if (operatorName === 'throws') { + return ( + assertThrowingMatcherHint(operatorName) + + '\n\n' + + chalk.reset(`Expected the function to throw an error.\n`) + + chalk.reset(`But it didn't throw anything.`) + + chalk.reset(hasCustomMessage ? '\n\nMessage:\n ' + message : '') + + stack.replace(/AssertionError(.*)/g, '') + ); + } + + return ( + assertMatcherHint(operator, operatorName) + + '\n\n' + + chalk.reset(`Expected value ${operatorMessage(operator, negator)}`) + + ` ${printExpected(expected)}\n` + + chalk.reset(`Received:\n`) + + ` ${printReceived(actual)}` + + chalk.reset(hasCustomMessage ? '\n\nMessage:\n ' + message : '') + + (diffString ? `\n\nDifference:\n\n${diffString}` : '') + + stack.replace(/AssertionError(.*)/g, '') + ); +} diff --git a/packages/jest-circus/src/legacy_code_todo_rewrite/jest-adapter-init.js b/packages/jest-circus/src/legacy_code_todo_rewrite/jest-adapter-init.js index 864cd6e29b3d..3df550157496 100644 --- a/packages/jest-circus/src/legacy_code_todo_rewrite/jest-adapter-init.js +++ b/packages/jest-circus/src/legacy_code_todo_rewrite/jest-adapter-init.js @@ -12,7 +12,11 @@ import type {TestResult, Status} from 'types/TestResult'; import type {GlobalConfig, Path, ProjectConfig} from 'types/Config'; import type {Event, TestEntry} from '../../types'; -import {getState, setState} from 'jest-matchers'; +import { + extractExpectedAssertionsErrors, + getState, + setState, +} from 'jest-matchers'; import {formatResultsErrors} from 'jest-message-util'; import {SnapshotState, addSerializer} from 'jest-snapshot'; import {addEventHandler, ROOT_DESCRIBE_BLOCK_NAME} from '../state'; @@ -157,11 +161,18 @@ const eventHandler = (event: Event) => { case 'test_success': case 'test_failure': { _addSuppressedErrors(event.test); + _addExpectedAssertionErrors(event.test); break; } } }; +const _addExpectedAssertionErrors = (test: TestEntry) => { + const errors = extractExpectedAssertionsErrors(); + errors.length && (test.status = 'fail'); + test.errors = test.errors.concat(errors); +}; + // Get suppressed errors from ``jest-matchers`` that weren't throw during // test execution and add them to the test result, potentially failing // a passing test. diff --git a/packages/jest-circus/src/state.js b/packages/jest-circus/src/state.js index 350b6e93a7b1..20b0dbe272fa 100644 --- a/packages/jest-circus/src/state.js +++ b/packages/jest-circus/src/state.js @@ -12,8 +12,12 @@ import type {Event, State, EventHandler} from '../types'; import {makeDescribe} from './utils'; import eventHandler from './eventHandler'; +import formatNodeAssertErrors from './formatNodeAssertErrors'; -const eventHandlers: Array = [eventHandler]; +const eventHandlers: Array = [ + eventHandler, + formatNodeAssertErrors, +]; const ROOT_DESCRIBE_BLOCK_NAME = 'ROOT_DESCRIBE_BLOCK'; const STATE_SYM = Symbol('JEST_STATE_SYMBOL'); @@ -21,6 +25,7 @@ const STATE_SYM = Symbol('JEST_STATE_SYMBOL'); const ROOT_DESCRIBE_BLOCK = makeDescribe(ROOT_DESCRIBE_BLOCK_NAME); const INITIAL_STATE: State = { currentDescribeBlock: ROOT_DESCRIBE_BLOCK, + expand: undefined, hasFocusedTests: false, rootDescribeBlock: ROOT_DESCRIBE_BLOCK, testTimeout: 5000, diff --git a/packages/jest-circus/src/utils.js b/packages/jest-circus/src/utils.js index 5e92612c3070..94f10c3e4fee 100644 --- a/packages/jest-circus/src/utils.js +++ b/packages/jest-circus/src/utils.js @@ -240,7 +240,7 @@ const _formatError = (error: ?Exception): string => { } else if (error.message) { return error.message; } else { - return String(error); + return `${String(error)} thrown`; } }; diff --git a/packages/jest-circus/types.js b/packages/jest-circus/types.js index 98415abaf5eb..91c006f51c44 100644 --- a/packages/jest-circus/types.js +++ b/packages/jest-circus/types.js @@ -106,6 +106,7 @@ export type State = {| hasFocusedTests: boolean, // that are defined using test.only rootDescribeBlock: DescribeBlock, testTimeout: number, + expand?: boolean, // expand error messages |}; export type DescribeBlock = {| diff --git a/packages/jest-matchers/src/extractExpectedAssertionsErrors.js b/packages/jest-matchers/src/extractExpectedAssertionsErrors.js new file mode 100644 index 000000000000..7db7602d9905 --- /dev/null +++ b/packages/jest-matchers/src/extractExpectedAssertionsErrors.js @@ -0,0 +1,64 @@ +/** + * Copyright (c) 2014, Facebook, Inc. All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @flow + */ + +import { + EXPECTED_COLOR, + RECEIVED_COLOR, + matcherHint, + pluralize, +} from 'jest-matcher-utils'; + +import {getState, setState} from './jest-matchers-object'; + +// Create and format all errors related to the mismatched number of `expect` +// calls and reset the matchers state. +const extractExpectedAssertionsErrors = () => { + const result = []; + const { + assertionCalls, + expectedAssertionsNumber, + isExpectingAssertions, + } = getState(); + setState({assertionCalls: 0, expectedAssertionsNumber: null}); + if ( + typeof expectedAssertionsNumber === 'number' && + assertionCalls !== expectedAssertionsNumber + ) { + const numOfAssertionsExpected = EXPECTED_COLOR( + pluralize('assertion', expectedAssertionsNumber), + ); + const error = new Error( + matcherHint('.assertions', '', String(expectedAssertionsNumber), { + isDirectExpectCall: true, + }) + + '\n\n' + + `Expected ${numOfAssertionsExpected} to be called but only received ` + + RECEIVED_COLOR(pluralize('assertion call', assertionCalls || 0)) + + '.', + ); + result.push(error); + } + if (isExpectingAssertions && assertionCalls === 0) { + const expected = EXPECTED_COLOR('at least one assertion'); + const received = RECEIVED_COLOR('received none'); + const error = new Error( + matcherHint('.hasAssertions', '', '', { + isDirectExpectCall: true, + }) + + '\n\n' + + `Expected ${expected} to be called but ${received}.`, + ); + result.push(error); + } + + return result; +}; + +module.exports = extractExpectedAssertionsErrors; diff --git a/packages/jest-matchers/src/index.js b/packages/jest-matchers/src/index.js index 5ab860407f2f..7f4f12f225f3 100644 --- a/packages/jest-matchers/src/index.js +++ b/packages/jest-matchers/src/index.js @@ -32,8 +32,13 @@ import { stringContaining, stringMatching, } from './asymmetric-matchers'; - -const GLOBAL_STATE = Symbol.for('$$jest-matchers-object'); +import { + getState, + setState, + getMatchers, + setMatchers, +} from './jest-matchers-object'; +import extractExpectedAssertionsErrors from './extractExpectedAssertionsErrors'; class JestAssertionError extends Error { matcherResult: any; @@ -47,22 +52,8 @@ const isPromise = obj => { ); }; -if (!global[GLOBAL_STATE]) { - Object.defineProperty(global, GLOBAL_STATE, { - value: { - matchers: Object.create(null), - state: { - assertionCalls: 0, - expectedAssertionsNumber: null, - isExpectingAssertions: false, - suppressedErrors: [], - }, - }, - }); -} - const expect = (actual: any): ExpectationObject => { - const allMatchers = global[GLOBAL_STATE].matchers; + const allMatchers = getMatchers(); const expectation = { not: {}, rejects: {not: {}}, @@ -199,7 +190,7 @@ const makeThrowingMatcher = ( // snapshot matcher uses it because we want to log all snapshot // failures in a test. {dontThrow: () => (throws = false)}, - global[GLOBAL_STATE].state, + getState(), { equals, isNot, @@ -218,7 +209,7 @@ const makeThrowingMatcher = ( _validateResult(result); - global[GLOBAL_STATE].state.assertionCalls++; + getState().assertionCalls++; if ((result.pass && isNot) || (!result.pass && !isNot)) { // XOR @@ -234,15 +225,13 @@ const makeThrowingMatcher = ( if (throws) { throw error; } else { - global[GLOBAL_STATE].state.suppressedErrors.push(error); + getState().suppressedErrors.push(error); } } }; }; -expect.extend = (matchers: MatchersObject): void => { - Object.assign(global[GLOBAL_STATE].matchers, matchers); -}; +expect.extend = (matchers: MatchersObject): void => setMatchers(matchers); expect.anything = anything; expect.any = any; @@ -276,15 +265,14 @@ expect.extend(toThrowMatchers); expect.addSnapshotSerializer = () => void 0; expect.assertions = (expected: number) => { - global[GLOBAL_STATE].state.expectedAssertionsNumber = expected; + getState().expectedAssertionsNumber = expected; }; expect.hasAssertions = expected => { utils.ensureNoExpected(expected, '.hasAssertions'); - global[GLOBAL_STATE].state.isExpectingAssertions = true; -}; -expect.setState = (state: Object) => { - Object.assign(global[GLOBAL_STATE].state, state); + getState().isExpectingAssertions = true; }; -expect.getState = () => global[GLOBAL_STATE].state; +expect.getState = getState; +expect.setState = setState; +expect.extractExpectedAssertionsErrors = extractExpectedAssertionsErrors; module.exports = (expect: Expect); diff --git a/packages/jest-matchers/src/jest-matchers-object.js b/packages/jest-matchers/src/jest-matchers-object.js new file mode 100644 index 000000000000..c2a392f3cfb4 --- /dev/null +++ b/packages/jest-matchers/src/jest-matchers-object.js @@ -0,0 +1,48 @@ +/** + * Copyright (c) 2014, Facebook, Inc. All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @flow + */ + +import type {MatchersObject} from 'types/Matchers'; + +// Global matchers object holds the list of available matchers and +// the state, that can hold matcher specific values that change over time. +const JEST_MATCHERS_OBJECT = Symbol.for('$$jest-matchers-object'); + +if (!global[JEST_MATCHERS_OBJECT]) { + Object.defineProperty(global, JEST_MATCHERS_OBJECT, { + value: { + matchers: Object.create(null), + state: { + assertionCalls: 0, + expectedAssertionsNumber: null, + isExpectingAssertions: false, + suppressedErrors: [], // errors that are not thrown immediately. + }, + }, + }); +} + +const getState = () => global[JEST_MATCHERS_OBJECT].state; + +const setState = (state: Object) => { + Object.assign(global[JEST_MATCHERS_OBJECT].state, state); +}; + +const getMatchers = () => global[JEST_MATCHERS_OBJECT].matchers; + +const setMatchers = (matchers: MatchersObject) => { + Object.assign(global[JEST_MATCHERS_OBJECT].matchers, matchers); +}; + +module.exports = { + getMatchers, + getState, + setMatchers, + setState, +}; diff --git a/types/Matchers.js b/types/Matchers.js index a9faa9430906..94396f2baff8 100644 --- a/types/Matchers.js +++ b/types/Matchers.js @@ -45,6 +45,7 @@ export type Expect = { addSnapshotSerializer(any): void, assertions(number): void, extend(any): void, + extractExpectedAssertionsErrors(): Array, getState(): MatcherState, hasAssertions(): void, setState(Object): void,