diff --git a/integration_tests/snapshot/__tests__/snapshot-test.js b/integration_tests/snapshot/__tests__/snapshot-test.js index 8444c55bb9e1..7762167e5851 100644 --- a/integration_tests/snapshot/__tests__/snapshot-test.js +++ b/integration_tests/snapshot/__tests__/snapshot-test.js @@ -33,7 +33,7 @@ describe('snapshot', () => { it('cannot be used with .not', () => { expect(() => expect('').not.toMatchSnapshot()).toThrow( - new Error('Jest: `.not` can not be used with `.toMatchSnapShot()`.') + 'Jest: `.not` can not be used with `.toMatchSnapshot()`.' ); }); }); diff --git a/packages/jest-jasmine2/src/extendJasmineExpect.js b/packages/jest-jasmine2/src/extendJasmineExpect.js index 18806d26804e..b9493d8c0c9a 100644 --- a/packages/jest-jasmine2/src/extendJasmineExpect.js +++ b/packages/jest-jasmine2/src/extendJasmineExpect.js @@ -8,13 +8,18 @@ 'use strict'; const jestExpect = require('jest-matchers').expect; +const {addMatchers} = require('jest-matchers'); +const {toMatchSnapshot} = require('jest-snapshot'); const jasmineExpect = global.expect; // extend jasmine matchers with `jest-matchers` -global.expect = actual => { - const jasmineMatchers = jasmineExpect(actual); - const jestMatchers = jestExpect(actual); - const not = Object.assign(jasmineMatchers.not, jestMatchers.not); - return Object.assign(jasmineMatchers, jestMatchers, {not}); +module.exports = () => { + addMatchers({toMatchSnapshot}); + global.expect = actual => { + const jasmineMatchers = jasmineExpect(actual); + const jestMatchers = jestExpect(actual); + const not = Object.assign(jasmineMatchers.not, jestMatchers.not); + return Object.assign(jasmineMatchers, jestMatchers, {not}); + }; }; diff --git a/packages/jest-jasmine2/src/index.js b/packages/jest-jasmine2/src/index.js index 2692df67cedb..65209689e8bb 100644 --- a/packages/jest-jasmine2/src/index.js +++ b/packages/jest-jasmine2/src/index.js @@ -17,7 +17,6 @@ import type Runtime from 'jest-runtime'; const JasmineReporter = require('./reporter'); const jasmineAsync = require('./jasmine-async'); -const snapshot = require('jest-snapshot'); const fs = require('graceful-fs'); const path = require('path'); const vm = require('vm'); @@ -101,14 +100,6 @@ function jasmine2( ); env.beforeEach(() => { - jasmine.addMatchers({ - toMatchSnapshot: snapshot.matcher( - testPath, - config, - snapshotState, - ), - }); - if (config.resetModules) { runtime.resetModules(); } @@ -118,8 +109,6 @@ function jasmine2( } }); - const snapshotState = snapshot.getSnapshotState(jasmine, testPath); - env.addReporter(reporter); // `jest-matchers` should be required inside test environment (vm). @@ -127,7 +116,12 @@ function jasmine2( // class of the test and `error instanceof Error` will return `false`. runtime.requireInternalModule( path.resolve(__dirname, './extendJasmineExpect.js'), - ); + )(); + + const snapshotState = runtime.requireInternalModule( + path.resolve(__dirname, './setup-jest-globals.js'), + )({testPath, config}); + if (config.setupTestFrameworkScriptFile) { runtime.requireModule(config.setupTestFrameworkScriptFile); diff --git a/packages/jest-jasmine2/src/setup-jest-globals.js b/packages/jest-jasmine2/src/setup-jest-globals.js new file mode 100644 index 000000000000..7bf23c449793 --- /dev/null +++ b/packages/jest-jasmine2/src/setup-jest-globals.js @@ -0,0 +1,79 @@ +/** + * 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 + */ + +'use strict'; + +import type {Config, Path} from 'types/Config'; + +const {getState, setState} = require('jest-matchers'); +const {initializeSnapshotState} = require('jest-snapshot'); + +// Get suppressed errors form jest-matchers that weren't throw during +// test execution and add them to the test result, potentially failing +// a passing test. +const addSuppressedErrors = result => { + const {suppressedErrors} = getState(); + setState({suppressedErrors: []}); + if (suppressedErrors.length) { + result.status = 'failed'; + + result.failedExpectations = suppressedErrors.map(error => ({ + message: error.message, + stack: error.stack, + passed: false, + expected: '', + actual: '', + })); + } +}; + +const patchJasmine = () => { + global.jasmine.Spec = (realSpec => { + const Spec = function Spec(attr) { + const resultCallback = attr.resultCallback; + attr.resultCallback = function(result) { + addSuppressedErrors(result); + resultCallback.call(attr, result); + }; + + const onStart = attr.onStart; + attr.onStart = context => { + setState({currentTestName: context.getFullName()}); + onStart && onStart.call(attr, context); + }; + + realSpec.call(this, attr); + }; + + Spec.prototype = realSpec.prototype; + for (const statics in realSpec) { + if (Object.prototype.hasOwnProperty.call(realSpec, statics)) { + Spec[statics] = realSpec[statics]; + } + } + return Spec; + + })(global.jasmine.Spec); +}; + +type Options = { + testPath: Path, + config: Config, +}; + +module.exports = ({testPath, config}: Options) => { + setState({testPath}); + patchJasmine(); + const snapshotState + = initializeSnapshotState(testPath, config.updateSnapshot); + setState({snapshotState}); + // Return it back to the outer scope (test runner outside the VM). + return snapshotState; +}; diff --git a/packages/jest-matchers/src/__tests__/__snapshots__/matchers-test.js.snap b/packages/jest-matchers/src/__tests__/__snapshots__/matchers-test.js.snap index 3560a977f911..ad6fc7206570 100644 --- a/packages/jest-matchers/src/__tests__/__snapshots__/matchers-test.js.snap +++ b/packages/jest-matchers/src/__tests__/__snapshots__/matchers-test.js.snap @@ -35,49 +35,49 @@ Received: \"undefined\"" `; -exports[`.toBe() fails for '1' with '.not' 1`] = ` +exports[`.toBe() fails for '[]' with '.not' 1`] = ` "expect(received).not.toBe(expected) Expected value to not be (using ===): - 1 + [] Received: - 1" + []" `; -exports[`.toBe() fails for '[]' with '.not' 1`] = ` +exports[`.toBe() fails for '{}' with '.not' 1`] = ` "expect(received).not.toBe(expected) Expected value to not be (using ===): - [] + {} Received: - []" + {}" `; -exports[`.toBe() fails for 'false' with '.not' 1`] = ` +exports[`.toBe() fails for '1' with '.not' 1`] = ` "expect(received).not.toBe(expected) Expected value to not be (using ===): - false + 1 Received: - false" + 1" `; -exports[`.toBe() fails for 'null' with '.not' 1`] = ` +exports[`.toBe() fails for 'false' with '.not' 1`] = ` "expect(received).not.toBe(expected) Expected value to not be (using ===): - null + false Received: - null" + false" `; -exports[`.toBe() fails for '{}' with '.not' 1`] = ` +exports[`.toBe() fails for 'null' with '.not' 1`] = ` "expect(received).not.toBe(expected) Expected value to not be (using ===): - {} + null Received: - {}" + null" `; exports[`.toBe() fails for: "abc" and "cde" 1`] = ` @@ -89,15 +89,6 @@ Received: \"abc\"" `; -exports[`.toBe() fails for: 1 and 2 1`] = ` -"expect(received).toBe(expected) - -Expected value to be (using ===): - 2 -Received: - 1" -`; - exports[`.toBe() fails for: [] and [] 1`] = ` "expect(received).toBe(expected) @@ -111,30 +102,6 @@ Difference: Compared values have no visual difference" `; -exports[`.toBe() fails for: null and "undefined" 1`] = ` -"expect(received).toBe(expected) - -Expected value to be (using ===): - \"undefined\" -Received: - null - -Difference: - -Comparing two different types of values: - Expected: undefined - Received: null" -`; - -exports[`.toBe() fails for: true and false 1`] = ` -"expect(received).toBe(expected) - -Expected value to be (using ===): - false -Received: - true" -`; - exports[`.toBe() fails for: {"a":1} and {"a":1} 1`] = ` "expect(received).toBe(expected) @@ -180,6 +147,39 @@ Difference: Compared values have no visual difference" `; +exports[`.toBe() fails for: 1 and 2 1`] = ` +"expect(received).toBe(expected) + +Expected value to be (using ===): + 2 +Received: + 1" +`; + +exports[`.toBe() fails for: null and "undefined" 1`] = ` +"expect(received).toBe(expected) + +Expected value to be (using ===): + \"undefined\" +Received: + null + +Difference: + +Comparing two different types of values: + Expected: undefined + Received: null" +`; + +exports[`.toBe() fails for: true and false 1`] = ` +"expect(received).toBe(expected) + +Expected value to be (using ===): + false +Received: + true" +`; + exports[`.toBeCloseTo() accepts an optional precision argument: [0, 0.000004, 5] 1`] = ` "expect(received).not.toBeCloseTo(expected, precision) @@ -330,74 +330,88 @@ Expected value to be undefined, instead received \"a\"" `; -exports[`.toBeDefined(), .toBeUndefined() '0.5' is defined 1`] = ` +exports[`.toBeDefined(), .toBeUndefined() '[]' is defined 1`] = ` "expect(received).not.toBeDefined() Expected value not to be defined, instead received - 0.5" + []" `; -exports[`.toBeDefined(), .toBeUndefined() '0.5' is defined 2`] = ` +exports[`.toBeDefined(), .toBeUndefined() '[]' is defined 2`] = ` "expect(received).toBeUndefined() Expected value to be undefined, instead received - 0.5" + []" `; -exports[`.toBeDefined(), .toBeUndefined() '1' is defined 1`] = ` +exports[`.toBeDefined(), .toBeUndefined() '{}' is defined 1`] = ` "expect(received).not.toBeDefined() Expected value not to be defined, instead received - 1" + {}" `; -exports[`.toBeDefined(), .toBeUndefined() '1' is defined 2`] = ` +exports[`.toBeDefined(), .toBeUndefined() '{}' is defined 2`] = ` "expect(received).toBeUndefined() Expected value to be undefined, instead received - 1" + {}" `; -exports[`.toBeDefined(), .toBeUndefined() '[]' is defined 1`] = ` +exports[`.toBeDefined(), .toBeUndefined() '{}' is defined 3`] = ` "expect(received).not.toBeDefined() Expected value not to be defined, instead received - []" + {}" `; -exports[`.toBeDefined(), .toBeUndefined() '[]' is defined 2`] = ` +exports[`.toBeDefined(), .toBeUndefined() '{}' is defined 4`] = ` "expect(received).toBeUndefined() Expected value to be undefined, instead received - []" + {}" `; -exports[`.toBeDefined(), .toBeUndefined() 'true' is defined 1`] = ` +exports[`.toBeDefined(), .toBeUndefined() '0.5' is defined 1`] = ` "expect(received).not.toBeDefined() Expected value not to be defined, instead received - true" + 0.5" `; -exports[`.toBeDefined(), .toBeUndefined() 'true' is defined 2`] = ` +exports[`.toBeDefined(), .toBeUndefined() '0.5' is defined 2`] = ` "expect(received).toBeUndefined() Expected value to be undefined, instead received - true" + 0.5" `; -exports[`.toBeDefined(), .toBeUndefined() '{}' is defined 1`] = ` +exports[`.toBeDefined(), .toBeUndefined() '1' is defined 1`] = ` "expect(received).not.toBeDefined() Expected value not to be defined, instead received - {}" + 1" `; -exports[`.toBeDefined(), .toBeUndefined() '{}' is defined 2`] = ` +exports[`.toBeDefined(), .toBeUndefined() '1' is defined 2`] = ` "expect(received).toBeUndefined() Expected value to be undefined, instead received - {}" + 1" +`; + +exports[`.toBeDefined(), .toBeUndefined() 'true' is defined 1`] = ` +"expect(received).not.toBeDefined() + +Expected value not to be defined, instead received + true" +`; + +exports[`.toBeDefined(), .toBeUndefined() 'true' is defined 2`] = ` +"expect(received).toBeUndefined() + +Expected value to be undefined, instead received + true" `; exports[`.toBeDefined(), .toBeUndefined() undefined is undefined 1`] = ` @@ -720,78 +734,6 @@ Received: 2" `; -exports[`.toBeGreaterThan(), .toBeLessThan(), .toBeGreaterThanOrEqual(), .toBeLessThanOrEqual() throws: [17, 34] 1`] = ` -"expect(received).toBeGreaterThan(expected) - -Expected value to be greater than: - 34 -Received: - 17" -`; - -exports[`.toBeGreaterThan(), .toBeLessThan(), .toBeGreaterThanOrEqual(), .toBeLessThanOrEqual() throws: [17, 34] 2`] = ` -"expect(received).not.toBeLessThan(expected) - -Expected value not to be less than: - 34 -Received: - 17" -`; - -exports[`.toBeGreaterThan(), .toBeLessThan(), .toBeGreaterThanOrEqual(), .toBeLessThanOrEqual() throws: [17, 34] 3`] = ` -"expect(received).not.toBeGreaterThan(expected) - -Expected value not to be greater than: - 17 -Received: - 34" -`; - -exports[`.toBeGreaterThan(), .toBeLessThan(), .toBeGreaterThanOrEqual(), .toBeLessThanOrEqual() throws: [17, 34] 4`] = ` -"expect(received).toBeLessThan(expected) - -Expected value to be less than: - 17 -Received: - 34" -`; - -exports[`.toBeGreaterThan(), .toBeLessThan(), .toBeGreaterThanOrEqual(), .toBeLessThanOrEqual() throws: [17, 34] 5`] = ` -"expect(received).toBeGreaterThanOrEqual(expected) - -Expected value to be greater than or equal: - 34 -Received: - 17" -`; - -exports[`.toBeGreaterThan(), .toBeLessThan(), .toBeGreaterThanOrEqual(), .toBeLessThanOrEqual() throws: [17, 34] 6`] = ` -"expect(received).not.toBeLessThanOrEqual(expected) - -Expected value not to be less than or equal: - 34 -Received: - 17" -`; - -exports[`.toBeGreaterThan(), .toBeLessThan(), .toBeGreaterThanOrEqual(), .toBeLessThanOrEqual() throws: [17, 34] 7`] = ` -"expect(received).not.toBeGreaterThanOrEqual(expected) - -Expected value not to be greater than or equal: - 17 -Received: - 34" -`; - -exports[`.toBeGreaterThan(), .toBeLessThan(), .toBeGreaterThanOrEqual(), .toBeLessThanOrEqual() throws: [17, 34] 8`] = ` -"expect(received).toBeLessThanOrEqual(expected) - -Expected value to be less than or equal: - 17 -Received: - 34" -`; - exports[`.toBeGreaterThan(), .toBeLessThan(), .toBeGreaterThanOrEqual(), .toBeLessThanOrEqual() throws: [3, 7] 1`] = ` "expect(received).toBeGreaterThan(expected) @@ -1008,37 +950,87 @@ Received: 18" `; -exports[`.toBeInstanceOf() failing "a" and "function String() { [native code] }" 1`] = ` -"expect(value).toBeInstanceOf(constructor) +exports[`.toBeGreaterThan(), .toBeLessThan(), .toBeGreaterThanOrEqual(), .toBeLessThanOrEqual() throws: [17, 34] 1`] = ` +"expect(received).toBeGreaterThan(expected) -Expected value to be an instance of: - \"String\" +Expected value to be greater than: + 34 Received: - \"a\" -Constructor: - \"String\"" + 17" `; -exports[`.toBeInstanceOf() failing 1 and "function Number() { [native code] }" 1`] = ` -"expect(value).toBeInstanceOf(constructor) +exports[`.toBeGreaterThan(), .toBeLessThan(), .toBeGreaterThanOrEqual(), .toBeLessThanOrEqual() throws: [17, 34] 2`] = ` +"expect(received).not.toBeLessThan(expected) -Expected value to be an instance of: - \"Number\" +Expected value not to be less than: + 34 Received: - 1 -Constructor: - \"Number\"" + 17" `; -exports[`.toBeInstanceOf() failing true and "function Boolean() { [native code] }" 1`] = ` -"expect(value).toBeInstanceOf(constructor) +exports[`.toBeGreaterThan(), .toBeLessThan(), .toBeGreaterThanOrEqual(), .toBeLessThanOrEqual() throws: [17, 34] 3`] = ` +"expect(received).not.toBeGreaterThan(expected) -Expected value to be an instance of: - \"Boolean\" +Expected value not to be greater than: + 17 Received: - true -Constructor: - \"Boolean\"" + 34" +`; + +exports[`.toBeGreaterThan(), .toBeLessThan(), .toBeGreaterThanOrEqual(), .toBeLessThanOrEqual() throws: [17, 34] 4`] = ` +"expect(received).toBeLessThan(expected) + +Expected value to be less than: + 17 +Received: + 34" +`; + +exports[`.toBeGreaterThan(), .toBeLessThan(), .toBeGreaterThanOrEqual(), .toBeLessThanOrEqual() throws: [17, 34] 5`] = ` +"expect(received).toBeGreaterThanOrEqual(expected) + +Expected value to be greater than or equal: + 34 +Received: + 17" +`; + +exports[`.toBeGreaterThan(), .toBeLessThan(), .toBeGreaterThanOrEqual(), .toBeLessThanOrEqual() throws: [17, 34] 6`] = ` +"expect(received).not.toBeLessThanOrEqual(expected) + +Expected value not to be less than or equal: + 34 +Received: + 17" +`; + +exports[`.toBeGreaterThan(), .toBeLessThan(), .toBeGreaterThanOrEqual(), .toBeLessThanOrEqual() throws: [17, 34] 7`] = ` +"expect(received).not.toBeGreaterThanOrEqual(expected) + +Expected value not to be greater than or equal: + 17 +Received: + 34" +`; + +exports[`.toBeGreaterThan(), .toBeLessThan(), .toBeGreaterThanOrEqual(), .toBeLessThanOrEqual() throws: [17, 34] 8`] = ` +"expect(received).toBeLessThanOrEqual(expected) + +Expected value to be less than or equal: + 17 +Received: + 34" +`; + +exports[`.toBeInstanceOf() failing "a" and "function String() { [native code] }" 1`] = ` +"expect(value).toBeInstanceOf(constructor) + +Expected value to be an instance of: + \"String\" +Received: + \"a\" +Constructor: + \"String\"" `; exports[`.toBeInstanceOf() failing {} and "class A {}" 1`] = ` @@ -1063,6 +1055,28 @@ Constructor: \"A\"" `; +exports[`.toBeInstanceOf() failing 1 and "function Number() { [native code] }" 1`] = ` +"expect(value).toBeInstanceOf(constructor) + +Expected value to be an instance of: + \"Number\" +Received: + 1 +Constructor: + \"Number\"" +`; + +exports[`.toBeInstanceOf() failing true and "function Boolean() { [native code] }" 1`] = ` +"expect(value).toBeInstanceOf(constructor) + +Expected value to be an instance of: + \"Boolean\" +Received: + true +Constructor: + \"Boolean\"" +`; + exports[`.toBeInstanceOf() passing [] and "function Array() { [native code] }" 1`] = ` "expect(value).not.toBeInstanceOf(constructor) @@ -1135,13 +1149,6 @@ Expected value to be NaN, instead received 1" `; -exports[`.toBeNaN() throws 10`] = ` -"expect(received).toBeNaN() - -Expected value to be NaN, instead received - \"-Infinity\"" -`; - exports[`.toBeNaN() throws 2`] = ` "expect(received).toBeNaN() @@ -1198,6 +1205,13 @@ Expected value to be NaN, instead received \"Infinity\"" `; +exports[`.toBeNaN() throws 10`] = ` +"expect(received).toBeNaN() + +Expected value to be NaN, instead received + \"-Infinity\"" +`; + exports[`.toBeNull() fails for '"() => {}"' with .not 1`] = ` "expect(received).toBeNull() @@ -1219,39 +1233,46 @@ Expected value to be null, instead received \"a\"" `; -exports[`.toBeNull() fails for '0.5' with .not 1`] = ` +exports[`.toBeNull() fails for '[]' with .not 1`] = ` "expect(received).toBeNull() Expected value to be null, instead received - 0.5" + []" `; -exports[`.toBeNull() fails for '1' with .not 1`] = ` +exports[`.toBeNull() fails for '{}' with .not 1`] = ` "expect(received).toBeNull() Expected value to be null, instead received - 1" + {}" `; -exports[`.toBeNull() fails for '[]' with .not 1`] = ` +exports[`.toBeNull() fails for '{}' with .not 2`] = ` "expect(received).toBeNull() Expected value to be null, instead received - []" + {}" `; -exports[`.toBeNull() fails for 'true' with .not 1`] = ` +exports[`.toBeNull() fails for '0.5' with .not 1`] = ` "expect(received).toBeNull() Expected value to be null, instead received - true" + 0.5" `; -exports[`.toBeNull() fails for '{}' with .not 1`] = ` +exports[`.toBeNull() fails for '1' with .not 1`] = ` "expect(received).toBeNull() Expected value to be null, instead received - {}" + 1" +`; + +exports[`.toBeNull() fails for 'true' with .not 1`] = ` +"expect(received).toBeNull() + +Expected value to be null, instead received + true" `; exports[`.toBeNull() pass for null 1`] = ` @@ -1345,6 +1366,48 @@ Expected value not to be falsy, instead received \"undefined\"" `; +exports[`.toBeTruthy(), .toBeFalsy() '[]' is truthy 1`] = ` +"expect(received).not.toBeTruthy() + +Expected value not to be truthy, instead received + []" +`; + +exports[`.toBeTruthy(), .toBeFalsy() '[]' is truthy 2`] = ` +"expect(received).toBeFalsy() + +Expected value to be falsy, instead received + []" +`; + +exports[`.toBeTruthy(), .toBeFalsy() '{}' is truthy 1`] = ` +"expect(received).not.toBeTruthy() + +Expected value not to be truthy, instead received + {}" +`; + +exports[`.toBeTruthy(), .toBeFalsy() '{}' is truthy 2`] = ` +"expect(received).toBeFalsy() + +Expected value to be falsy, instead received + {}" +`; + +exports[`.toBeTruthy(), .toBeFalsy() '{}' is truthy 3`] = ` +"expect(received).not.toBeTruthy() + +Expected value not to be truthy, instead received + {}" +`; + +exports[`.toBeTruthy(), .toBeFalsy() '{}' is truthy 4`] = ` +"expect(received).toBeFalsy() + +Expected value to be falsy, instead received + {}" +`; + exports[`.toBeTruthy(), .toBeFalsy() '0' is falsy 1`] = ` "expect(received).toBeTruthy() @@ -1387,20 +1450,6 @@ Expected value to be falsy, instead received 1" `; -exports[`.toBeTruthy(), .toBeFalsy() '[]' is truthy 1`] = ` -"expect(received).not.toBeTruthy() - -Expected value not to be truthy, instead received - []" -`; - -exports[`.toBeTruthy(), .toBeFalsy() '[]' is truthy 2`] = ` -"expect(received).toBeFalsy() - -Expected value to be falsy, instead received - []" -`; - exports[`.toBeTruthy(), .toBeFalsy() 'false' is falsy 1`] = ` "expect(received).toBeTruthy() @@ -1443,20 +1492,6 @@ Expected value to be falsy, instead received true" `; -exports[`.toBeTruthy(), .toBeFalsy() '{}' is truthy 1`] = ` -"expect(received).not.toBeTruthy() - -Expected value not to be truthy, instead received - {}" -`; - -exports[`.toBeTruthy(), .toBeFalsy() '{}' is truthy 2`] = ` -"expect(received).toBeFalsy() - -Expected value to be falsy, instead received - {}" -`; - exports[`.toBeTruthy(), .toBeFalsy() does not accept arguments 1`] = ` "expect(received)[.not].toBeTruthy() @@ -1531,6 +1566,24 @@ Not to contain value: " `; +exports[`.toContain() '[{},[]]' does not contain '[]' 1`] = ` +"expect(array).toContain(value) + +Expected array: + [{},[]] +To contain value: + []" +`; + +exports[`.toContain() '[{},[]]' does not contain '{}' 1`] = ` +"expect(array).toContain(value) + +Expected array: + [{},[]] +To contain value: + {}" +`; + exports[`.toContain() '[1,2,3,4]' contains '1' 1`] = ` "expect(array).not.toContain(value) @@ -1559,24 +1612,6 @@ To contain value: 1" `; -exports[`.toContain() '[{},[]]' does not contain '[]' 1`] = ` -"expect(array).toContain(value) - -Expected array: - [{},[]] -To contain value: - []" -`; - -exports[`.toContain() '[{},[]]' does not contain '{}' 1`] = ` -"expect(array).toContain(value) - -Expected array: - [{},[]] -To contain value: - {}" -`; - exports[`.toContainEqual() '[{"a":"b"},{"a":"c"}]' does not contain a value equal to'{"a":"d"}' 1`] = ` "expect(array).toContainEqual(value) @@ -1604,6 +1639,34 @@ Received: \"banana\"" `; +exports[`.toEqual() expect({"a":5}).toEqual({"b":6}) 1`] = ` +"expect(received).toEqual(expected) + +Expected value to equal: + {\"b\":6} +Received: + {\"a\":5} + +Difference: + +- Expected ++ Received + +  Object { +-  \"b\": 6, ++  \"a\": 5, +  }" +`; + +exports[`.toEqual() expect({"a":99}).not.toEqual({"a":99}) 1`] = ` +"expect(received).not.toEqual(expected) + +Expected value to not equal: + {\"a\":99} +Received: + {\"a\":99}" +`; + exports[`.toEqual() expect(1).not.toEqual(1) 1`] = ` "expect(received).not.toEqual(expected) @@ -1655,34 +1718,6 @@ Received: true" `; -exports[`.toEqual() expect({"a":5}).toEqual({"b":6}) 1`] = ` -"expect(received).toEqual(expected) - -Expected value to equal: - {\"b\":6} -Received: - {\"a\":5} - -Difference: - -- Expected -+ Received - -  Object { --  \"b\": 6, -+  \"a\": 5, -  }" -`; - -exports[`.toEqual() expect({"a":99}).not.toEqual({"a":99}) 1`] = ` -"expect(received).not.toEqual(expected) - -Expected value to not equal: - {\"a\":99} -Received: - {\"a\":99}" -`; - exports[`.toMatch() passes: [Foo bar, /^foo/i] 1`] = ` "expect(received).not.toMatch(expected) @@ -1724,36 +1759,36 @@ exports[`.toMatch() throws if non String actual value passed: ["undefined", "foo Received: \"undefined\"" `; -exports[`.toMatch() throws if non String actual value passed: [1, "foo"] 1`] = ` +exports[`.toMatch() throws if non String actual value passed: [[], "foo"] 1`] = ` "expect(string)[.not].toMatch(expected) string value must be a string. Received: - number: 1" + array: []" `; -exports[`.toMatch() throws if non String actual value passed: [[], "foo"] 1`] = ` +exports[`.toMatch() throws if non String actual value passed: [{}, "foo"] 1`] = ` "expect(string)[.not].toMatch(expected) string value must be a string. Received: - array: []" + object: {}" `; -exports[`.toMatch() throws if non String actual value passed: [true, "foo"] 1`] = ` +exports[`.toMatch() throws if non String actual value passed: [1, "foo"] 1`] = ` "expect(string)[.not].toMatch(expected) string value must be a string. Received: - boolean: true" + number: 1" `; -exports[`.toMatch() throws if non String actual value passed: [{}, "foo"] 1`] = ` +exports[`.toMatch() throws if non String actual value passed: [true, "foo"] 1`] = ` "expect(string)[.not].toMatch(expected) string value must be a string. Received: - object: {}" + boolean: true" `; exports[`.toMatch() throws if non String/RegExp expected value passed: ["foo", "() => {}"] 1`] = ` @@ -1771,36 +1806,36 @@ exports[`.toMatch() throws if non String/RegExp expected value passed: ["foo", " Expected: \"undefined\"" `; -exports[`.toMatch() throws if non String/RegExp expected value passed: ["foo", 1] 1`] = ` +exports[`.toMatch() throws if non String/RegExp expected value passed: ["foo", []] 1`] = ` "expect(string)[.not].toMatch(expected) expected value must be a string or a regular expression. Expected: - number: 1" + array: []" `; -exports[`.toMatch() throws if non String/RegExp expected value passed: ["foo", []] 1`] = ` +exports[`.toMatch() throws if non String/RegExp expected value passed: ["foo", {}] 1`] = ` "expect(string)[.not].toMatch(expected) expected value must be a string or a regular expression. Expected: - array: []" + object: {}" `; -exports[`.toMatch() throws if non String/RegExp expected value passed: ["foo", true] 1`] = ` +exports[`.toMatch() throws if non String/RegExp expected value passed: ["foo", 1] 1`] = ` "expect(string)[.not].toMatch(expected) expected value must be a string or a regular expression. Expected: - boolean: true" + number: 1" `; -exports[`.toMatch() throws if non String/RegExp expected value passed: ["foo", {}] 1`] = ` +exports[`.toMatch() throws if non String/RegExp expected value passed: ["foo", true] 1`] = ` "expect(string)[.not].toMatch(expected) expected value must be a string or a regular expression. Expected: - object: {}" + boolean: true" `; exports[`.toMatch() throws: [bar, /foo/] 1`] = ` diff --git a/packages/jest-matchers/src/index.js b/packages/jest-matchers/src/index.js index 1e306cc3efc6..26e30382e740 100644 --- a/packages/jest-matchers/src/index.js +++ b/packages/jest-matchers/src/index.js @@ -15,6 +15,7 @@ import type { ExpectationObject, ExpectationResult, MatcherContext, + MatcherState, MatchersObject, RawMatcherFn, ThrowingMatcherFn, @@ -26,20 +27,20 @@ const toThrowMatchers = require('./toThrowMatchers'); const {stringify} = require('jest-matcher-utils'); -const GLOBAL_MATCHERS_OBJECT_SYMBOL = Symbol.for('$$jest-matchers-object'); +const GLOBAL_STATE = Symbol.for('$$jest-matchers-object'); class JestAssertionError extends Error {} -if (!global[GLOBAL_MATCHERS_OBJECT_SYMBOL]) { +if (!global[GLOBAL_STATE]) { Object.defineProperty( global, - GLOBAL_MATCHERS_OBJECT_SYMBOL, - {value: Object.create(null)}, + GLOBAL_STATE, + {value: {matchers: Object.create(null), state: {suppressedErrors: []}}}, ); } const expect: Expect = (actual: any): ExpectationObject => { - const allMatchers = global[GLOBAL_MATCHERS_OBJECT_SYMBOL]; + const allMatchers = global[GLOBAL_STATE].matchers; const expectation = {not: {}}; Object.keys(allMatchers).forEach(name => { expectation[name] = @@ -57,7 +58,17 @@ const makeThrowingMatcher = ( actual: any, ): ThrowingMatcherFn => { return function throwingMatcher(expected, ...rest) { - const matcherContext: MatcherContext = {isNot}; + let throws = true; + const matcherContext: MatcherContext = Object.assign( + // When throws is disabled, the matcher will not throw errors during test + // execution but instead add them to the global matcher state. If a + // matcher throws, test execution is normally stopped immediately. The + // snapshot matcher uses it because we want to log all snapshot + // failures in a test. + {dontThrow: () => throws = false}, + global[GLOBAL_STATE].state, + {isNot}, + ); let result: ExpectationResult; try { @@ -85,13 +96,18 @@ const makeThrowingMatcher = ( const error = new JestAssertionError(message); // Remove this function from the stack trace frame. Error.captureStackTrace(error, throwingMatcher); - throw error; + + if (throws) { + throw error; + } else { + global[GLOBAL_STATE].state.suppressedErrors.push(error); + } } }; }; const addMatchers = (matchersObj: MatchersObject): void => { - Object.assign(global[GLOBAL_MATCHERS_OBJECT_SYMBOL], matchersObj); + Object.assign(global[GLOBAL_STATE].matchers, matchersObj); }; const _validateResult = result => { @@ -113,6 +129,12 @@ const _validateResult = result => { } }; +const setState = (state: MatcherState) => { + Object.assign(global[GLOBAL_STATE].state, state); +}; + +const getState = () => global[GLOBAL_STATE].state; + // add default jest matchers addMatchers(matchers); addMatchers(spyMatchers); @@ -121,4 +143,6 @@ addMatchers(toThrowMatchers); module.exports = { addMatchers, expect, + getState, + setState, }; diff --git a/packages/jest-matchers/src/matchers.js b/packages/jest-matchers/src/matchers.js index ff646a2cdf85..9706e1eef4c9 100644 --- a/packages/jest-matchers/src/matchers.js +++ b/packages/jest-matchers/src/matchers.js @@ -11,7 +11,9 @@ 'use strict'; -import type {MatchersObject} from './types'; +import type { + MatchersObject, +} from './types'; const diff = require('jest-diff'); const {escapeStrForRegex} = require('jest-util'); @@ -338,7 +340,11 @@ const matchers: MatchersObject = { return {message, pass}; }, - toBeCloseTo(actual: number, expected: number, precision?: number = 2) { + toBeCloseTo( + actual: number, + expected: number, + precision?: number = 2, + ) { ensureNumbers(actual, expected, '.toBeCloseTo'); const pass = Math.abs(expected - actual) < (Math.pow(10, -precision) / 2); const message = pass diff --git a/packages/jest-matchers/src/types.js b/packages/jest-matchers/src/types.js index bc44721f9a02..ffcfe5457d58 100644 --- a/packages/jest-matchers/src/types.js +++ b/packages/jest-matchers/src/types.js @@ -9,6 +9,8 @@ */ 'use strict'; +import type {Path} from 'types/Config'; + export type ExpectationResult = { pass: boolean, message: string | () => string, @@ -22,6 +24,10 @@ export type RawMatcherFn = ( export type ThrowingMatcherFn = (actual: any) => void; export type MatcherContext = {isNot: boolean}; +export type MatcherState = { + currentTestName?: string, + testPath?: Path, +}; export type MatchersObject = {[id:string]: RawMatcherFn}; export type Expect = (expected: any) => ExpectationObject; export type ExpectationObject = { diff --git a/packages/jest-snapshot/src/State.js b/packages/jest-snapshot/src/State.js new file mode 100644 index 000000000000..d2c122339565 --- /dev/null +++ b/packages/jest-snapshot/src/State.js @@ -0,0 +1,90 @@ +/** + * 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 + */ + +'use strict'; + +import type {Path} from 'types/Config'; + +const {forFile} = require('./SnapshotFile'); +import type {SnapshotFile} from './SnapshotFile'; + +class SnapshotState { + _counters: Map; + _index: number; + added: number; + matched: number; + snapshot: SnapshotFile; + unmatched: number; + update: boolean; + updated: number; + + constructor(testPath: Path, update: boolean) { + this._counters = new Map(); + this._index = 0; + this.added = 0; + this.matched = 0; + this.snapshot = forFile(testPath); + this.unmatched = 0; + this.update = update; + this.updated = 0; + } + + match(testName: string, received: any, key?: string) { + this._counters.set(testName, (this._counters.get(testName) || 0) + 1); + const count = Number(this._counters.get(testName)); + + if (!key) { + key = testName + ' ' + count; + } + + const hasSnapshot = this.snapshot.has(key); + + if ( + !this.snapshot.fileExists() || + (hasSnapshot && this.update) || + !hasSnapshot + ) { + if (this.update) { + if (!this.snapshot.matches(key, received).pass) { + if (hasSnapshot) { + this.updated++; + } else { + this.added++; + } + this.snapshot.add(key, received); + } else { + this.matched++; + } + } else { + this.snapshot.add(key, received); + this.added++; + } + + return {pass: true}; + } else { + const matches = this.snapshot.matches(key, received); + const {pass} = matches; + if (!pass) { + this.unmatched++; + return { + count, + pass: false, + expected: matches.expected, + actual: matches.actual, + }; + } else { + this.matched++; + return {pass: true}; + } + } + } +} + +module.exports = SnapshotState; diff --git a/packages/jest-snapshot/src/index.js b/packages/jest-snapshot/src/index.js index e4cd8e79cd41..ee02a069d8e1 100644 --- a/packages/jest-snapshot/src/index.js +++ b/packages/jest-snapshot/src/index.js @@ -10,96 +10,105 @@ 'use strict'; import type {HasteFS} from 'types/HasteMap'; -import type {Jasmine} from 'types/Jasmine'; import type {Path} from 'types/Config'; -import type {SnapshotState} from './SnapshotState'; - -const SnapshotFile = require('./SnapshotFile'); +const SnapshotState = require('./State'); +const {SnapshotFile, SNAPSHOT_EXTENSION} = require('./SnapshotFile'); +const diff = require('jest-diff'); const fileExists = require('jest-file-exists'); const fs = require('fs'); -const matcher = require('./matcher'); const path = require('path'); +const { + EXPECTED_COLOR, + matcherHint, + RECEIVED_COLOR, +} = require('jest-matcher-utils'); -const EXTENSION = SnapshotFile.SNAPSHOT_EXTENSION; +const EXTENSION = SNAPSHOT_EXTENSION; -const patchAttr = (attr, state) => { - attr.onStart = function(onStart) { - return function(context) { - state.setSpecName(context.getFullName()); - state.setCounter(0); - if (onStart) { - onStart(context); +const cleanup = (hasteFS: HasteFS, update: boolean) => { + const pattern = '\\.' + EXTENSION + '$'; + const files = hasteFS.matchFiles(pattern); + const filesRemoved = files + .filter(snapshotFile => !fileExists( + path.resolve( + path.dirname(snapshotFile), + '..', + path.basename(snapshotFile, '.' + EXTENSION), + ), + hasteFS, + )) + .map(snapshotFile => { + if (update) { + fs.unlinkSync(snapshotFile); } - }; - }(attr.onStart); + }) + .length; + + return { + filesRemoved, + }; }; -const patchJasmine = (jasmine, state) => { - jasmine.Spec = (realSpec => { - const Spec = function Spec(attr) { - patchAttr(attr, state); - realSpec.call(this, attr); - }; - Spec.prototype = realSpec.prototype; - for (const statics in realSpec) { - if (Object.prototype.hasOwnProperty.call(realSpec, statics)) { - Spec[statics] = realSpec[statics]; - } - } - return Spec; - })(jasmine.Spec); +let snapshotState; + +const initializeSnapshotState + = (testFile: Path, update: boolean) => new SnapshotState(testFile, update); + +const getSnapshotState = () => snapshotState; + +const matcher = function(received: any) { + this.dontThrow(); + const {currentTestName, isNot, snapshotState} = this; + + if (isNot) { + throw new Error( + 'Jest: `.not` can not be used with `.toMatchSnapshot()`.', + ); + } + + if (!snapshotState) { + throw new Error('Jest: snapshot state must be initialized.'); + } + + const result = snapshotState.match(currentTestName, received); + const {pass} = result; + + if (pass) { + return {pass: true, message: ''}; + } else { + const {count, expected, actual} = result; + + + const expectedString = expected.trim(); + const actualString = actual.trim(); + const diffMessage = diff( + expectedString, + actualString, + { + aAnnotation: 'Snapshot', + bAnnotation: 'Received', + }, + ); + + const message = + matcherHint('.toMatchSnapshot', 'value', '') + '\n\n' + + `${RECEIVED_COLOR('Received value')} does not match ` + + `${EXPECTED_COLOR('stored snapshot ' + count)}.\n\n` + + (diffMessage || ( + RECEIVED_COLOR('- ' + expectedString) + '\n' + + EXPECTED_COLOR('+ ' + actualString) + )); + + return {pass: false, message}; + } }; module.exports = { EXTENSION, - SnapshotFile: SnapshotFile.SnapshotFile, - cleanup(hasteFS: HasteFS, update: boolean) { - const pattern = '\\.' + EXTENSION + '$'; - const files = hasteFS.matchFiles(pattern); - const filesRemoved = files - .filter(snapshotFile => !fileExists( - path.resolve( - path.dirname(snapshotFile), - '..', - path.basename(snapshotFile, '.' + EXTENSION), - ), - hasteFS, - )) - .map(snapshotFile => { - if (update) { - fs.unlinkSync(snapshotFile); - } - }) - .length; - - return { - filesRemoved, - }; - }, - matcher, - getSnapshotState: (jasmine: Jasmine, filePath: Path): SnapshotState => { - let _index = 0; - let _name = ''; - /* $FlowFixMe */ - const state = Object.assign(Object.create(null), { - getCounter: () => _index, - getSpecName: () => _name, - incrementCounter: () => ++_index, - setCounter(index) { - _index = index; - }, - setSpecName(name) { - _name = name; - }, - snapshot: SnapshotFile.forFile(filePath), - added: 0, - updated: 0, - matched: 0, - unmatched: 0, - }); - - patchJasmine(jasmine, state); - return state; - }, + cleanup, + getSnapshotState, + initializeSnapshotState, + toMatchSnapshot: matcher, + SnapshotFile, }; diff --git a/packages/jest-snapshot/src/matcher.js b/packages/jest-snapshot/src/matcher.js deleted file mode 100644 index 1b6bbc3bc0ef..000000000000 --- a/packages/jest-snapshot/src/matcher.js +++ /dev/null @@ -1,107 +0,0 @@ -/** - * 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 - */ -'use strict'; - -import type {Path} from 'types/Config'; -import type {SnapshotState} from './SnapshotState'; - -const diff = require('jest-diff'); -const { - EXPECTED_COLOR, - matcherHint, - RECEIVED_COLOR, -} = require('jest-matcher-utils'); - -type CompareResult = { - pass: boolean, - message: ?string, -}; - -module.exports = ( - filePath: Path, - options: Object, - snapshotState: SnapshotState, -) => (util: any, customEquality: any) => { - return { - negativeCompare() { - throw new Error( - 'Jest: `.not` can not be used with `.toMatchSnapshot()`.', - ); - }, - compare(actual: any, expected: any): CompareResult { - if (expected !== undefined) { - throw new Error( - 'Jest: toMatchSnapshot() does not accept parameters.', - ); - } - - const snapshot = snapshotState.snapshot; - const count = snapshotState.incrementCounter(); - const key = snapshotState.getSpecName() + ' ' + count; - const hasSnapshot = snapshot.has(key); - let pass = false; - let message; - - if ( - !snapshot.fileExists() || - (hasSnapshot && options.updateSnapshot) || - !hasSnapshot - ) { - if (options.updateSnapshot) { - if (!snapshot.matches(key, actual).pass) { - if (hasSnapshot) { - snapshotState.updated++; - } else { - snapshotState.added++; - } - snapshot.add(key, actual); - } else { - snapshotState.matched++; - } - } else { - snapshot.add(key, actual); - snapshotState.added++; - } - pass = true; - } else { - const matches = snapshot.matches(key, actual); - pass = matches.pass; - if (!pass) { - snapshotState.unmatched++; - const expectedString = matches.expected.trim(); - const actualString = matches.actual.trim(); - const diffMessage = diff( - expectedString, - actualString, - { - aAnnotation: 'Snapshot', - bAnnotation: 'Received', - }, - ); - message = - matcherHint('.toMatchSnapshot', 'value', '') + '\n\n' + - `${RECEIVED_COLOR('Received value')} does not match ` + - `${EXPECTED_COLOR('stored snapshot ' + count)}.\n\n` + - (diffMessage || ( - RECEIVED_COLOR('- ' + expectedString) + '\n' + - EXPECTED_COLOR('+ ' + actualString) - )); - } else { - snapshotState.matched++; - } - } - - return { - pass, - message, - }; - }, - }; -};