diff --git a/.azure-pipelines-steps.yml b/.azure-pipelines-steps.yml index d786455f6bff..7fd184534620 100644 --- a/.azure-pipelines-steps.yml +++ b/.azure-pipelines-steps.yml @@ -26,9 +26,15 @@ steps: displayName: 'Move source into jest folder' # Run yarn to install dependencies and build - - script: yarn --frozen-lockfile + - script: node scripts/remove-postinstall workingDirectory: $(JEST_DIR) - displayName: 'Install dependencies and build' + displayName: 'Remove postinstall script' + - script: yarn --no-progress --frozen-lockfile + workingDirectory: $(JEST_DIR) + displayName: 'Install dependencies' + - script: node scripts/build + workingDirectory: $(JEST_DIR) + displayName: 'Build' # Run test-ci-partial - script: yarn run test-ci-partial diff --git a/.circleci/config.yml b/.circleci/config.yml index 323151353c34..cf29628f4b33 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -14,6 +14,7 @@ aliases: - &filter-ignore-gh-pages branches: ignore: gh-pages + - &install node scripts/remove-postinstall && yarn --no-progress --frozen-lockfile --ignore-engines && node scripts/build version: 2 jobs: @@ -37,7 +38,7 @@ jobs: steps: - checkout - restore-cache: *restore-cache - - run: yarn --no-progress --frozen-lockfile --ignore-engines + - run: *install - save-cache: *save-cache - run: # react-native does not work with node 6 @@ -52,7 +53,7 @@ jobs: steps: - checkout - restore-cache: *restore-cache - - run: yarn --no-progress --frozen-lockfile + - run: *install - save-cache: *save-cache - run: command: yarn test-ci-partial @@ -66,7 +67,7 @@ jobs: steps: - checkout - restore-cache: *restore-cache - - run: yarn --no-progress --frozen-lockfile + - run: *install - save-cache: *save-cache - run: command: yarn test-ci @@ -80,7 +81,7 @@ jobs: steps: - checkout - restore-cache: *restore-cache - - run: yarn --no-progress --frozen-lockfile + - run: *install - save-cache: *save-cache - run: command: JEST_CIRCUS=1 yarn test-ci-partial @@ -94,7 +95,7 @@ jobs: steps: - checkout - restore-cache: *restore-cache - - run: yarn --no-progress --frozen-lockfile + - run: *install - save-cache: *save-cache - run: command: yarn test-ci-partial @@ -108,7 +109,7 @@ jobs: steps: - checkout - restore-cache: *restore-cache - - run: yarn --no-progress --frozen-lockfile + - run: *install - save-cache: *save-cache - run: yarn test-ci-es5-build-in-browser @@ -120,7 +121,7 @@ jobs: steps: - checkout - restore-cache: *restore-cache - - run: yarn --no-progress --frozen-lockfile + - run: *install - save-cache: *save-cache - run: name: Test or Deploy Jest Website diff --git a/.travis.yml b/.travis.yml index aa57b3e56377..6ab91e3060aa 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,7 +11,7 @@ before_install: - curl -o- -L https://yarnpkg.com/install.sh | bash - export PATH="$HOME/.yarn/bin:$PATH" -install: yarn --frozen-lockfile +install: node scripts/remove-postinstall && yarn --no-progress --frozen-lockfile --ignore-engines && node scripts/build cache: yarn: true diff --git a/CHANGELOG.md b/CHANGELOG.md index c908134351a4..dba7081f7d17 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,21 +3,32 @@ ### Features - `[jest-config]` Add `freezeCoreModules` configuration option to mitigate memory leaks described in the following issues: [#6399](https://github.com/facebook/jest/issues/6399), [#6814](https://github.com/facebook/jest/issues/6814) ([#8331](https://github.com/facebook/jest/pull/8331)) +- `[jest-circus]` Bind to Circus events via an optional event handler on any custom env ([#8344](https://github.com/facebook/jest/pull/8344) - `[expect]` Improve report when matcher fails, part 15 ([#8281](https://github.com/facebook/jest/pull/8281)) - `[jest-cli]` Update `--forceExit` and "did not exit for one second" message colors ([#8329](https://github.com/facebook/jest/pull/8329)) - `[expect]` Improve report when matcher fails, part 16 ([#8306](https://github.com/facebook/jest/pull/8306)) - `[jest-runner]` Pass docblock pragmas to TestEnvironment constructor ([#8320](https://github.com/facebook/jest/pull/8320)) +- `[docs]` Add DynamoDB guide ([#8319](https://github.com/facebook/jest/pull/8319)) +- `[expect]` Improve report when matcher fails, part 17 ([#8349](https://github.com/facebook/jest/pull/8349)) +- `[expect]` Improve report when matcher fails, part 18 ([#8356](https://github.com/facebook/jest/pull/8356)) ### Fixes +- `[jest-each]` Fix bug with placeholder values ([#8289](https://github.com/facebook/jest/pull/8289)) - `[jest-snapshot]` Inline snapshots: do not indent empty lines ([#8277](https://github.com/facebook/jest/pull/8277)) - `[@jest/runtime, @jest/transform]` Allow custom transforms for JSON dependencies ([#2578](https://github.com/facebook/jest/pull/2578)) - `[jest-core]` Make `detectOpenHandles` imply `runInBand` ([#8283](https://github.com/facebook/jest/pull/8283)) - `[jest-haste-map]` Fix the `mapper` option which was incorrectly ignored ([#8299](https://github.com/facebook/jest/pull/8299)) +- `[jest-jasmine2]` Fix describe return value warning being shown if the describe function throws ([#8335](https://github.com/facebook/jest/pull/8335)) +- `[jest-environment-jsdom]` Re-declare global prototype of JSDOMEnvironment ([#8352](https://github.com/facebook/jest/pull/8352)) +- `[jest-snapshot]` Handle arrays when merging snapshots ([#7089](https://github.com/facebook/jest/pull/7089)) +- `[expect]` Extract names of async and generator functions ([#8362](https://github.com/facebook/jest/pull/8362)) ### Chore & Maintenance - `[expect]` Fix label and add opposite assertion for toEqual tests ([#8288](https://github.com/facebook/jest/pull/8288)) +- `[docs]` Mention Jest MongoDB Preset ([#8318](https://github.com/facebook/jest/pull/8318)) +- `[@jest/reporters]` Migrate away from `istanbul-api` ([#8294](https://github.com/facebook/jest/pull/8294)) ### Performance diff --git a/docs/Configuration.md b/docs/Configuration.md index 4960cdd91d86..bfae755af03b 100644 --- a/docs/Configuration.md +++ b/docs/Configuration.md @@ -877,6 +877,8 @@ test('use jsdom in this test file', () => { You can create your own module that will be used for setting up the test environment. The module must export a class with `setup`, `teardown` and `runScript` methods. You can also pass variables from this module to your test suites by assigning them to `this.global` object – this will make them available in your test suites as global variables. +The class may optionally expose a `handleTestEvent` method to bind to events fired by [`jest-circus`](https://github.com/facebook/jest/tree/master/packages/jest-circus). + Any docblock pragmas in test files will be passed to the environment constructor and can be used for per-test configuration. If the pragma does not have a value, it will be present in the object with it's value set to an empty string. If the pragma is not present, it will not be present in the object. _Note: TestEnvironment is sandboxed. Each test suite will trigger setup/teardown in their own TestEnvironment._ @@ -914,6 +916,12 @@ class CustomEnvironment extends NodeEnvironment { runScript(script) { return super.runScript(script); } + + handleTestEvent(event, state) { + if (event.name === 'test_start') { + // ... + } + } } module.exports = CustomEnvironment; diff --git a/docs/DynamoDB.md b/docs/DynamoDB.md new file mode 100644 index 000000000000..ed3d36febae9 --- /dev/null +++ b/docs/DynamoDB.md @@ -0,0 +1,81 @@ +--- +id: dynamodb +title: Using with DynamoDB +--- + +With the [Global Setup/Teardown](Configuration.md#globalsetup-string) and [Async Test Environment](Configuration.md#testenvironment-string) APIs, Jest can work smoothly with [DynamoDB](https://aws.amazon.com/dynamodb/). + +## Use jest-dynamodb Preset + +[Jest DynamoDB](https://github.com/shelfio/jest-dynamodb) provides all required configuration to run your tests using DynamoDB. + +1. First install `@shelf/jest-dynamodb` + +``` +yarn add @shelf/jest-dynamodb --dev +``` + +2. Specify preset in your Jest configuration: + +```json +{ + "preset": "@shelf/jest-dynamodb" +} +``` + +3. Create `jest-dynamodb-config.js` and define DynamoDB tables + +See [Create Table API](https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/DynamoDB.html#createTable-property) + +```js +module.exports = { + tables: [ + { + TableName: `files`, + KeySchema: [{AttributeName: 'id', KeyType: 'HASH'}], + AttributeDefinitions: [{AttributeName: 'id', AttributeType: 'S'}], + ProvisionedThroughput: {ReadCapacityUnits: 1, WriteCapacityUnits: 1}, + }, + // etc + ], +}; +``` + +4. Configure DynamoDB client + +```js +const {DocumentClient} = require('aws-sdk/clients/dynamodb'); + +const isTest = process.env.JEST_WORKER_ID; +const config = { + convertEmptyValues: true, + ...(isTest && { + endpoint: 'localhost:8000', + sslEnabled: false, + region: 'local-env', + }), +}; + +const ddb = new DocumentClient(config); +``` + +5. Write tests + +```js +it('should insert item into table', async () => { + await ddb + .put({TableName: 'files', Item: {id: '1', hello: 'world'}}) + .promise(); + + const {Item} = await ddb.get({TableName: 'files', Key: {id: '1'}}).promise(); + + expect(Item).toEqual({ + id: '1', + hello: 'world', + }); +}); +``` + +There's no need to load any dependencies. + +See [documentation](https://github.com/shelfio/jest-dynamodb) for details. diff --git a/docs/MigrationGuide.md b/docs/MigrationGuide.md index c885e0c08e19..d475453466c2 100644 --- a/docs/MigrationGuide.md +++ b/docs/MigrationGuide.md @@ -13,16 +13,10 @@ If you'd like to try out Jest with an existing codebase, there are a number of w If you are using [AVA](https://github.com/avajs/ava), [Chai](https://github.com/chaijs/chai), [Expect.js (by Automattic)](https://github.com/Automattic/expect.js), [Jasmine](https://github.com/jasmine/jasmine), [Mocha](https://github.com/mochajs/mocha), [proxyquire](https://github.com/thlorenz/proxyquire), [Should.js](https://github.com/shouldjs/should.js) or [Tape](https://github.com/substack/tape) you can use the third-party [jest-codemods](https://github.com/skovhus/jest-codemods) to do most of the dirty migration work. It runs a code transformation on your codebase using [jscodeshift](https://github.com/facebook/jscodeshift). -Install Jest Codemods with `yarn` by running: - -```bash -yarn global add jest-codemods -``` - To transform your existing tests, navigate to the project containing the tests and run: ```bash -jest-codemods +npx jest-codemods ``` More information can be found at [https://github.com/skovhus/jest-codemods](https://github.com/skovhus/jest-codemods). diff --git a/docs/MongoDB.md b/docs/MongoDB.md index f12bdca10018..3610da622673 100644 --- a/docs/MongoDB.md +++ b/docs/MongoDB.md @@ -5,142 +5,57 @@ title: Using with MongoDB With the [Global Setup/Teardown](Configuration.md#globalsetup-string) and [Async Test Environment](Configuration.md#testenvironment-string) APIs, Jest can work smoothly with [MongoDB](https://www.mongodb.com/). -## A jest-mongodb example +## Use jest-mongodb Preset -The basic idea is to: +[Jest MongoDB](https://github.com/shelfio/jest-mongodb) provides all required configuration to run your tests using MongoDB. -1. Spin up in-memory mongodb server -2. Export a global variable with mongo URI -3. Write tests for queries / aggregations using a real database ✨ -4. Shut down mongodb server using Global Teardown +1. First install `@shelf/jest-mongodb` -Here's an example of the GlobalSetup script - -```js -// setup.js -const path = require('path'); - -const fs = require('fs'); - -const {MongoMemoryServer} = require('mongodb-memory-server'); - -const globalConfigPath = path.join(__dirname, 'globalConfig.json'); - -const mongod = new MongoMemoryServer({ - autoStart: false, -}); - -module.exports = async () => { - if (!mongod.isRunning) { - await mongod.start(); - } - - const mongoConfig = { - mongoDBName: 'jest', - mongoUri: await mongod.getConnectionString(), - }; - - // Write global config to disk because all tests run in different contexts. - fs.writeFileSync(globalConfigPath, JSON.stringify(mongoConfig)); - - // Set reference to mongod in order to close the server during teardown. - global.__MONGOD__ = mongod; -}; +``` +yarn add @shelf/jest-mongodb --dev ``` -Then we need a custom Test Environment for Mongo - -```js -// mongo-environment.js -const NodeEnvironment = require('jest-environment-node'); - -const path = require('path'); - -const fs = require('fs'); - -const globalConfigPath = path.join(__dirname, 'globalConfig.json'); - -class MongoEnvironment extends NodeEnvironment { - constructor(config) { - super(config); - } - - async setup() { - console.log('Setup MongoDB Test Environment'); - - const globalConfig = JSON.parse(fs.readFileSync(globalConfigPath, 'utf-8')); - - this.global.__MONGO_URI__ = globalConfig.mongoUri; - this.global.__MONGO_DB_NAME__ = globalConfig.mongoDBName; - - await super.setup(); - } - - async teardown() { - console.log('Teardown MongoDB Test Environment'); - - await super.teardown(); - } +2. Specify preset in your Jest configuration: - runScript(script) { - return super.runScript(script); - } +```json +{ + "preset": "@shelf/jest-mongodb" } - -module.exports = MongoEnvironment; ``` -Finally we can shut down mongodb server +3. Write your test ```js -// teardown.js -module.exports = async function() { - await global.__MONGOD__.stop(); -}; -``` +const {MongoClient} = require('mongodb'); -With all the things set up, we can now write our tests like this: +describe('insert', () => { + let connection; + let db; -```js -// test.js -const {MongoClient} = require('mongodb'); + beforeAll(async () => { + connection = await MongoClient.connect(global.__MONGO_URI__, { + useNewUrlParser: true, + }); + db = await connection.db(global.__MONGO_DB_NAME__); + }); -let connection; -let db; + afterAll(async () => { + await connection.close(); + await db.close(); + }); -beforeAll(async () => { - connection = await MongoClient.connect(global.__MONGO_URI__); - db = await connection.db(global.__MONGO_DB_NAME__); -}); + it('should insert a doc into collection', async () => { + const users = db.collection('users'); -afterAll(async () => { - await connection.close(); - await db.close(); -}); + const mockUser = {_id: 'some-user-id', name: 'John'}; + await users.insertOne(mockUser); -it('should aggregate docs from collection', async () => { - const files = db.collection('files'); - - await files.insertMany([ - {type: 'Document'}, - {type: 'Video'}, - {type: 'Image'}, - {type: 'Document'}, - {type: 'Image'}, - {type: 'Document'}, - ]); - - const topFiles = await files - .aggregate([ - {$group: {_id: '$type', count: {$sum: 1}}}, - {$sort: {count: -1}}, - ]) - .toArray(); - - expect(topFiles).toEqual([ - {_id: 'Document', count: 3}, - {_id: 'Image', count: 2}, - {_id: 'Video', count: 1}, - ]); + const insertedUser = await users.findOne({_id: 'some-user-id'}); + expect(insertedUser).toEqual(mockUser); + }); }); ``` + +There's no need to load any dependencies. + +See [documentation](https://github.com/shelfio/jest-mongodb) for details (configuring MongoDB version, etc). diff --git a/docs/TestingFrameworks.md b/docs/TestingFrameworks.md index d668e010667c..dac35b278588 100644 --- a/docs/TestingFrameworks.md +++ b/docs/TestingFrameworks.md @@ -30,3 +30,7 @@ Although Jest may be considered a React-specific test runner, in fact it is a un ## Express.js - [How to test Express.js with Jest and Supertest](http://www.albertgao.xyz/2017/05/24/how-to-test-expressjs-with-jest-and-supertest/) by Albert Gao ([@albertgao](https://twitter.com/albertgao)) + +## GatsbyJS + +- [Unit Testing](https://www.gatsbyjs.org/docs/unit-testing/) by GatsbyJS docs diff --git a/e2e/__tests__/__snapshots__/moduleNameMapper.test.ts.snap b/e2e/__tests__/__snapshots__/moduleNameMapper.test.ts.snap index 86fa29655c31..18ccc60fbe68 100644 --- a/e2e/__tests__/__snapshots__/moduleNameMapper.test.ts.snap +++ b/e2e/__tests__/__snapshots__/moduleNameMapper.test.ts.snap @@ -30,6 +30,6 @@ FAIL __tests__/index.js 12 | module.exports = () => 'test'; 13 | - at createNoMappedModuleFoundError (../../packages/jest-resolve/build/index.js:471:17) + at createNoMappedModuleFoundError (../../packages/jest-resolve/build/index.js:472:17) at Object.require (index.js:10:1) `; diff --git a/e2e/__tests__/__snapshots__/resolveNoFileExtensions.test.ts.snap b/e2e/__tests__/__snapshots__/resolveNoFileExtensions.test.ts.snap index 131e4b0ba283..f88d38ad9041 100644 --- a/e2e/__tests__/__snapshots__/resolveNoFileExtensions.test.ts.snap +++ b/e2e/__tests__/__snapshots__/resolveNoFileExtensions.test.ts.snap @@ -33,6 +33,6 @@ FAIL __tests__/test.js | ^ 4 | - at Resolver.resolveModule (../../packages/jest-resolve/build/index.js:229:17) + at Resolver.resolveModule (../../packages/jest-resolve/build/index.js:230:17) at Object.require (index.js:3:18) `; diff --git a/e2e/__tests__/declarationErrors.test.ts b/e2e/__tests__/declarationErrors.test.ts index fcd5050cfe6d..55004e6e3c3c 100644 --- a/e2e/__tests__/declarationErrors.test.ts +++ b/e2e/__tests__/declarationErrors.test.ts @@ -29,3 +29,11 @@ it('warns if describe returns something', () => { expect(result.status).toBe(0); expect(normalizeCircusJasmine(result.stdout)).toMatchSnapshot(); }); + +it('errors if describe throws', () => { + const result = runJest('declaration-errors', ['describeThrow.test.js']); + + expect(result.status).toBe(1); + expect(result.stdout).toBe(''); + expect(result.stderr).toContain('whoops'); +}); diff --git a/e2e/__tests__/testEnvironmentCircus.test.ts b/e2e/__tests__/testEnvironmentCircus.test.ts new file mode 100644 index 000000000000..12ff9e833ab8 --- /dev/null +++ b/e2e/__tests__/testEnvironmentCircus.test.ts @@ -0,0 +1,46 @@ +/** + * 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. + */ + +import {skipSuiteOnJasmine} from '@jest/test-utils'; +import runJest from '../runJest'; + +skipSuiteOnJasmine(); + +it('calls testEnvironment handleTestEvent', () => { + const result = runJest('test-environment-circus'); + expect(result.failed).toEqual(false); + expect(result.stdout.split('\n')).toMatchInlineSnapshot(` + Array [ + "setup", + "add_hook", + "add_hook", + "add_test", + "add_test", + "run_start", + "run_describe_start", + "test_start: test name here", + "hook_start", + "hook_success: test name here", + "hook_start", + "hook_success: test name here", + "test_fn_start: test name here", + "test_fn_success: test name here", + "test_done: test name here", + "test_start: second test name here", + "hook_start", + "hook_success: second test name here", + "hook_start", + "hook_success: second test name here", + "test_fn_start: second test name here", + "test_fn_success: second test name here", + "test_done: second test name here", + "run_describe_finish", + "run_finish", + "teardown", + ] + `); +}); diff --git a/e2e/declaration-errors/__tests__/describeThrow.test.js b/e2e/declaration-errors/__tests__/describeThrow.test.js new file mode 100644 index 000000000000..d139d73fddaf --- /dev/null +++ b/e2e/declaration-errors/__tests__/describeThrow.test.js @@ -0,0 +1,14 @@ +/** + * 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'; + +describe('describe throw does not warn', () => { + it('t', () => {}); + throw new Error('whoops'); +}); diff --git a/e2e/test-environment-circus/CircusHandleTestEventEnvironment.js b/e2e/test-environment-circus/CircusHandleTestEventEnvironment.js new file mode 100644 index 000000000000..f06dc57e368a --- /dev/null +++ b/e2e/test-environment-circus/CircusHandleTestEventEnvironment.js @@ -0,0 +1,13 @@ +// Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved. + +'use strict'; + +const JSDOMEnvironment = require('jest-environment-jsdom'); + +class TestEnvironment extends JSDOMEnvironment { + handleTestEvent(event) { + console.log(event.name + (event.test ? ': ' + event.test.name : '')); + } +} + +module.exports = TestEnvironment; diff --git a/e2e/test-environment-circus/__tests__/circusHandleTestEvent.test.js b/e2e/test-environment-circus/__tests__/circusHandleTestEvent.test.js new file mode 100644 index 000000000000..6f90232e4c55 --- /dev/null +++ b/e2e/test-environment-circus/__tests__/circusHandleTestEvent.test.js @@ -0,0 +1,15 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved. + * + * @jest-environment ./CircusHandleTestEventEnvironment.js + */ + +beforeEach(() => {}); + +test('test name here', () => { + expect(true).toBe(true); +}); + +test('second test name here', () => { + expect(true).toBe(true); +}); diff --git a/e2e/test-environment-circus/package.json b/e2e/test-environment-circus/package.json new file mode 100644 index 000000000000..148788b25446 --- /dev/null +++ b/e2e/test-environment-circus/package.json @@ -0,0 +1,5 @@ +{ + "jest": { + "testEnvironment": "node" + } +} diff --git a/package.json b/package.json index a0a3553274b1..1aac0f43063d 100644 --- a/package.json +++ b/package.json @@ -43,8 +43,9 @@ "glob": "^7.1.1", "graceful-fs": "^4.1.15", "isbinaryfile": "^4.0.0", - "istanbul-api": "^2.0.8", "istanbul-lib-coverage": "^2.0.2", + "istanbul-lib-report": "^2.0.4", + "istanbul-reports": "^2.1.1", "jest-junit": "^6.2.1", "jest-silent-reporter": "^0.1.2", "jest-snapshot-serializer-raw": "^1.1.0", diff --git a/packages/expect/src/__tests__/__snapshots__/matchers.test.js.snap b/packages/expect/src/__tests__/__snapshots__/matchers.test.js.snap index e74042085cc7..b3f8a191a0a4 100644 --- a/packages/expect/src/__tests__/__snapshots__/matchers.test.js.snap +++ b/packages/expect/src/__tests__/__snapshots__/matchers.test.js.snap @@ -2841,7 +2841,7 @@ Expected has value: -3" `; exports[`.toHaveProperty() {error} expect({"a": {"b": {}}}).toHaveProperty('1') 1`] = ` -"expect(received).toHaveProperty(path) +"expect(received).toHaveProperty(path) Matcher error: expected path must be a string or array @@ -2850,7 +2850,7 @@ Expected has value: 1" `; exports[`.toHaveProperty() {error} expect({"a": {"b": {}}}).toHaveProperty('null') 1`] = ` -"expect(received).toHaveProperty(path) +"expect(received).toHaveProperty(path) Matcher error: expected path must be a string or array @@ -2858,17 +2858,24 @@ Expected has value: null" `; exports[`.toHaveProperty() {error} expect({"a": {"b": {}}}).toHaveProperty('undefined') 1`] = ` -"expect(received).toHaveProperty(path) +"expect(received).toHaveProperty(path) Matcher error: expected path must be a string or array Expected has value: undefined" `; -exports[`.toHaveProperty() {error} expect({}).toHaveProperty('') 1`] = `"pass must be initialized"`; +exports[`.toHaveProperty() {error} expect({}).toHaveProperty('') 1`] = ` +"expect(received).toHaveProperty(path) + +Matcher error: expected path must not be an empty array + +Expected has type: array +Expected has value: []" +`; exports[`.toHaveProperty() {error} expect(null).toHaveProperty('a.b') 1`] = ` -"expect(received).toHaveProperty(path) +"expect(received).toHaveProperty(path) Matcher error: received value must not be null nor undefined @@ -2876,7 +2883,7 @@ Received has value: null" `; exports[`.toHaveProperty() {error} expect(undefined).toHaveProperty('a') 1`] = ` -"expect(received).toHaveProperty(path) +"expect(received).toHaveProperty(path) Matcher error: received value must not be null nor undefined @@ -2884,116 +2891,87 @@ Received has value: undefined" `; exports[`.toHaveProperty() {pass: false} expect("").toHaveProperty('key') 1`] = ` -"expect(object).toHaveProperty(path) +"expect(received).toHaveProperty(path) -Expected the object: - \\"\\" -To have a nested property: - \\"key\\" -" +Expected path: \\"key\\" +Received path: [] + +Received value: \\"\\"" `; exports[`.toHaveProperty() {pass: false} expect("abc").toHaveProperty('a.b.c') 1`] = ` -"expect(object).toHaveProperty(path) +"expect(received).toHaveProperty(path) -Expected the object: - \\"abc\\" -To have a nested property: - \\"a.b.c\\" -" +Expected path: \\"a.b.c\\" +Received path: [] + +Received value: \\"abc\\"" `; exports[`.toHaveProperty() {pass: false} expect("abc").toHaveProperty('a.b.c', {"a": 5}) 1`] = ` -"expect(object).toHaveProperty(path, value) - -Expected the object: - \\"abc\\" -To have a nested property: - \\"a.b.c\\" -With a value of: - {\\"a\\": 5} -" +"expect(received).toHaveProperty(path, value) + +Expected path: \\"a.b.c\\" +Received path: [] + +Expected value: {\\"a\\": 5} +Received value: \\"abc\\"" `; exports[`.toHaveProperty() {pass: false} expect({"a": {"b": {"c": {"d": 1}}}}).toHaveProperty('a,b,c,d', 2) 1`] = ` -"expect(object).toHaveProperty(path, value) +"expect(received).toHaveProperty(path, value) + +Expected path: [\\"a\\", \\"b\\", \\"c\\", \\"d\\"] -Expected the object: - {\\"a\\": {\\"b\\": {\\"c\\": {\\"d\\": 1}}}} -To have a nested property: - [\\"a\\", \\"b\\", \\"c\\", \\"d\\"] -With a value of: - 2 -Received: - 1" +Expected value: 2 +Received value: 1" `; exports[`.toHaveProperty() {pass: false} expect({"a": {"b": {"c": {"d": 1}}}}).toHaveProperty('a.b.c.d', 2) 1`] = ` -"expect(object).toHaveProperty(path, value) +"expect(received).toHaveProperty(path, value) + +Expected path: \\"a.b.c.d\\" -Expected the object: - {\\"a\\": {\\"b\\": {\\"c\\": {\\"d\\": 1}}}} -To have a nested property: - \\"a.b.c.d\\" -With a value of: - 2 -Received: - 1" +Expected value: 2 +Received value: 1" `; exports[`.toHaveProperty() {pass: false} expect({"a": {"b": {"c": {"d": 1}}}}).toHaveProperty('a.b.ttt.d', 1) 1`] = ` -"expect(object).toHaveProperty(path, value) +"expect(received).toHaveProperty(path, value) + +Expected path: \\"a.b.ttt.d\\" +Received path: \\"a.b\\" -Expected the object: - {\\"a\\": {\\"b\\": {\\"c\\": {\\"d\\": 1}}}} -To have a nested property: - \\"a.b.ttt.d\\" -With a value of: - 1 -Received: - object.a.b: {\\"c\\": {\\"d\\": 1}}" +Expected value: 1 +Received value: {\\"c\\": {\\"d\\": 1}}" `; exports[`.toHaveProperty() {pass: false} expect({"a": {"b": {"c": {}}}}).toHaveProperty('a.b.c.d') 1`] = ` -"expect(object).toHaveProperty(path) +"expect(received).toHaveProperty(path) + +Expected path: \\"a.b.c.d\\" +Received path: \\"a.b.c\\" -Expected the object: - {\\"a\\": {\\"b\\": {\\"c\\": {}}}} -To have a nested property: - \\"a.b.c.d\\" -Received: - object.a.b.c: {}" +Received value: {}" `; exports[`.toHaveProperty() {pass: false} expect({"a": {"b": {"c": {}}}}).toHaveProperty('a.b.c.d', 1) 1`] = ` -"expect(object).toHaveProperty(path, value) +"expect(received).toHaveProperty(path, value) + +Expected path: \\"a.b.c.d\\" +Received path: \\"a.b.c\\" -Expected the object: - {\\"a\\": {\\"b\\": {\\"c\\": {}}}} -To have a nested property: - \\"a.b.c.d\\" -With a value of: - 1 -Received: - object.a.b.c: {}" +Expected value: 1 +Received value: {}" `; exports[`.toHaveProperty() {pass: false} expect({"a": {"b": {"c": 5}}}).toHaveProperty('a.b', {"c": 4}) 1`] = ` -"expect(object).toHaveProperty(path, value) +"expect(received).toHaveProperty(path, value) -Expected the object: - {\\"a\\": {\\"b\\": {\\"c\\": 5}}} -To have a nested property: - \\"a.b\\" -With a value of: - {\\"c\\": 4} -Received: - {\\"c\\": 5} +Expected path: \\"a.b\\" -Difference: - -- Expected -+ Received +- Expected value ++ Received value Object { - \\"c\\": 4, @@ -3002,451 +2980,347 @@ Difference: `; exports[`.toHaveProperty() {pass: false} expect({"a": {"b": 3}}).toHaveProperty('a.b', undefined) 1`] = ` -"expect(object).toHaveProperty(path, value) +"expect(received).toHaveProperty(path, value) -Expected the object: - {\\"a\\": {\\"b\\": 3}} -To have a nested property: - \\"a.b\\" -With a value of: - undefined -Received: - 3 +Expected path: \\"a.b\\" -Difference: - - Comparing two different types of values. Expected undefined but received number." +Expected value: undefined +Received value: 3" `; exports[`.toHaveProperty() {pass: false} expect({"a": 1}).toHaveProperty('a.b.c.d') 1`] = ` -"expect(object).toHaveProperty(path) +"expect(received).toHaveProperty(path) + +Expected path: \\"a.b.c.d\\" +Received path: \\"a\\" -Expected the object: - {\\"a\\": 1} -To have a nested property: - \\"a.b.c.d\\" -Received: - object.a: 1" +Received value: 1" `; exports[`.toHaveProperty() {pass: false} expect({"a": 1}).toHaveProperty('a.b.c.d', 5) 1`] = ` -"expect(object).toHaveProperty(path, value) +"expect(received).toHaveProperty(path, value) + +Expected path: \\"a.b.c.d\\" +Received path: \\"a\\" -Expected the object: - {\\"a\\": 1} -To have a nested property: - \\"a.b.c.d\\" -With a value of: - 5 -Received: - object.a: 1" +Expected value: 5 +Received value: 1" `; exports[`.toHaveProperty() {pass: false} expect({"a.b.c.d": 1}).toHaveProperty('a.b.c.d', 2) 1`] = ` -"expect(object).toHaveProperty(path, value) - -Expected the object: - {\\"a.b.c.d\\": 1} -To have a nested property: - \\"a.b.c.d\\" -With a value of: - 2 -" +"expect(received).toHaveProperty(path, value) + +Expected path: \\"a.b.c.d\\" +Received path: [] + +Expected value: 2 +Received value: {\\"a.b.c.d\\": 1}" `; exports[`.toHaveProperty() {pass: false} expect({"a.b.c.d": 1}).toHaveProperty('a.b.c.d', 2) 2`] = ` -"expect(object).toHaveProperty(path, value) +"expect(received).toHaveProperty(path, value) + +Expected path: [\\"a.b.c.d\\"] -Expected the object: - {\\"a.b.c.d\\": 1} -To have a nested property: - [\\"a.b.c.d\\"] -With a value of: - 2 -Received: - 1" +Expected value: 2 +Received value: 1" `; exports[`.toHaveProperty() {pass: false} expect({"key": 1}).toHaveProperty('not') 1`] = ` -"expect(object).toHaveProperty(path) +"expect(received).toHaveProperty(path) -Expected the object: - {\\"key\\": 1} -To have a nested property: - \\"not\\" -" +Expected path: \\"not\\" +Received path: [] + +Received value: {\\"key\\": 1}" `; exports[`.toHaveProperty() {pass: false} expect({}).toHaveProperty('a') 1`] = ` -"expect(object).toHaveProperty(path) +"expect(received).toHaveProperty(path) -Expected the object: - {} -To have a nested property: - \\"a\\" -" +Expected path: \\"a\\" +Received path: [] + +Received value: {}" `; exports[`.toHaveProperty() {pass: false} expect({}).toHaveProperty('a', "a") 1`] = ` -"expect(object).toHaveProperty(path, value) - -Expected the object: - {} -To have a nested property: - \\"a\\" -With a value of: - \\"a\\" -Received: - undefined +"expect(received).toHaveProperty(path, value) -Difference: +Expected path: \\"a\\" - Comparing two different types of values. Expected string but received undefined." +Expected value: \\"a\\" +Received value: undefined" `; exports[`.toHaveProperty() {pass: false} expect({}).toHaveProperty('a', "test") 1`] = ` -"expect(object).toHaveProperty(path, value) - -Expected the object: - {} -To have a nested property: - \\"a\\" -With a value of: - \\"test\\" -" +"expect(received).toHaveProperty(path, value) + +Expected path: \\"a\\" +Received path: [] + +Expected value: \\"test\\" +Received value: {}" `; exports[`.toHaveProperty() {pass: false} expect({}).toHaveProperty('b', undefined) 1`] = ` -"expect(object).toHaveProperty(path, value) +"expect(received).toHaveProperty(path, value) -Expected the object: - {} -To have a nested property: - \\"b\\" -With a value of: - undefined -Received: - \\"b\\" +Expected path: \\"b\\" -Difference: - - Comparing two different types of values. Expected undefined but received string." +Expected value: undefined +Received value: \\"b\\"" `; exports[`.toHaveProperty() {pass: false} expect(0).toHaveProperty('key') 1`] = ` -"expect(object).toHaveProperty(path) +"expect(received).toHaveProperty(path) -Expected the object: - 0 -To have a nested property: - \\"key\\" -" +Expected path: \\"key\\" +Received path: [] + +Received value: 0" `; exports[`.toHaveProperty() {pass: false} expect(1).toHaveProperty('a.b.c') 1`] = ` -"expect(object).toHaveProperty(path) +"expect(received).toHaveProperty(path) -Expected the object: - 1 -To have a nested property: - \\"a.b.c\\" -" +Expected path: \\"a.b.c\\" +Received path: [] + +Received value: 1" `; exports[`.toHaveProperty() {pass: false} expect(1).toHaveProperty('a.b.c', "test") 1`] = ` -"expect(object).toHaveProperty(path, value) - -Expected the object: - 1 -To have a nested property: - \\"a.b.c\\" -With a value of: - \\"test\\" -" +"expect(received).toHaveProperty(path, value) + +Expected path: \\"a.b.c\\" +Received path: [] + +Expected value: \\"test\\" +Received value: 1" `; exports[`.toHaveProperty() {pass: false} expect(Symbol()).toHaveProperty('key') 1`] = ` -"expect(object).toHaveProperty(path) +"expect(received).toHaveProperty(path) -Expected the object: - Symbol() -To have a nested property: - \\"key\\" -" +Expected path: \\"key\\" +Received path: [] + +Received value: Symbol()" `; exports[`.toHaveProperty() {pass: false} expect(false).toHaveProperty('key') 1`] = ` -"expect(object).toHaveProperty(path) +"expect(received).toHaveProperty(path) -Expected the object: - false -To have a nested property: - \\"key\\" -" +Expected path: \\"key\\" +Received path: [] + +Received value: false" `; exports[`.toHaveProperty() {pass: true} expect("").toHaveProperty('length', 0) 1`] = ` -"expect(object).not.toHaveProperty(path, value) - -Expected the object: - \\"\\" -Not to have a nested property: - \\"length\\" -With a value of: - 0 -" +"expect(received).not.toHaveProperty(path, value) + +Expected path: \\"length\\" + +Expected value: not 0" `; exports[`.toHaveProperty() {pass: true} expect([Function memoized]).toHaveProperty('memo', []) 1`] = ` -"expect(object).not.toHaveProperty(path, value) - -Expected the object: - [Function memoized] -Not to have a nested property: - \\"memo\\" -With a value of: - [] -" +"expect(received).not.toHaveProperty(path, value) + +Expected path: \\"memo\\" + +Expected value: not []" `; exports[`.toHaveProperty() {pass: true} expect({"a": {"b": [1, 2, 3]}}).toHaveProperty('a,b,1') 1`] = ` -"expect(object).not.toHaveProperty(path) +"expect(received).not.toHaveProperty(path) -Expected the object: - {\\"a\\": {\\"b\\": [1, 2, 3]}} -Not to have a nested property: - [\\"a\\", \\"b\\", 1] -" +Expected path: not [\\"a\\", \\"b\\", 1] + +Received value: 2" `; exports[`.toHaveProperty() {pass: true} expect({"a": {"b": [1, 2, 3]}}).toHaveProperty('a,b,1', 2) 1`] = ` -"expect(object).not.toHaveProperty(path, value) - -Expected the object: - {\\"a\\": {\\"b\\": [1, 2, 3]}} -Not to have a nested property: - [\\"a\\", \\"b\\", 1] -With a value of: - 2 -" +"expect(received).not.toHaveProperty(path, value) + +Expected path: [\\"a\\", \\"b\\", 1] + +Expected value: not 2" +`; + +exports[`.toHaveProperty() {pass: true} expect({"a": {"b": [1, 2, 3]}}).toHaveProperty('a,b,1', Any) 1`] = ` +"expect(received).not.toHaveProperty(path, value) + +Expected path: [\\"a\\", \\"b\\", 1] + +Expected value: not Any +Received value: 2" `; exports[`.toHaveProperty() {pass: true} expect({"a": {"b": {"c": {"d": 1}}}}).toHaveProperty('a,b,c,d') 1`] = ` -"expect(object).not.toHaveProperty(path) +"expect(received).not.toHaveProperty(path) -Expected the object: - {\\"a\\": {\\"b\\": {\\"c\\": {\\"d\\": 1}}}} -Not to have a nested property: - [\\"a\\", \\"b\\", \\"c\\", \\"d\\"] -" +Expected path: not [\\"a\\", \\"b\\", \\"c\\", \\"d\\"] + +Received value: 1" `; exports[`.toHaveProperty() {pass: true} expect({"a": {"b": {"c": {"d": 1}}}}).toHaveProperty('a,b,c,d', 1) 1`] = ` -"expect(object).not.toHaveProperty(path, value) - -Expected the object: - {\\"a\\": {\\"b\\": {\\"c\\": {\\"d\\": 1}}}} -Not to have a nested property: - [\\"a\\", \\"b\\", \\"c\\", \\"d\\"] -With a value of: - 1 -" +"expect(received).not.toHaveProperty(path, value) + +Expected path: [\\"a\\", \\"b\\", \\"c\\", \\"d\\"] + +Expected value: not 1" `; exports[`.toHaveProperty() {pass: true} expect({"a": {"b": {"c": {"d": 1}}}}).toHaveProperty('a.b.c.d') 1`] = ` -"expect(object).not.toHaveProperty(path) +"expect(received).not.toHaveProperty(path) -Expected the object: - {\\"a\\": {\\"b\\": {\\"c\\": {\\"d\\": 1}}}} -Not to have a nested property: - \\"a.b.c.d\\" -" +Expected path: not \\"a.b.c.d\\" + +Received value: 1" `; exports[`.toHaveProperty() {pass: true} expect({"a": {"b": {"c": {"d": 1}}}}).toHaveProperty('a.b.c.d', 1) 1`] = ` -"expect(object).not.toHaveProperty(path, value) - -Expected the object: - {\\"a\\": {\\"b\\": {\\"c\\": {\\"d\\": 1}}}} -Not to have a nested property: - \\"a.b.c.d\\" -With a value of: - 1 -" +"expect(received).not.toHaveProperty(path, value) + +Expected path: \\"a.b.c.d\\" + +Expected value: not 1" `; exports[`.toHaveProperty() {pass: true} expect({"a": {"b": {"c": 5}}}).toHaveProperty('a.b', {"c": 5}) 1`] = ` -"expect(object).not.toHaveProperty(path, value) - -Expected the object: - {\\"a\\": {\\"b\\": {\\"c\\": 5}}} -Not to have a nested property: - \\"a.b\\" -With a value of: - {\\"c\\": 5} -" +"expect(received).not.toHaveProperty(path, value) + +Expected path: \\"a.b\\" + +Expected value: not {\\"c\\": 5}" `; exports[`.toHaveProperty() {pass: true} expect({"a": {"b": undefined}}).toHaveProperty('a.b') 1`] = ` -"expect(object).not.toHaveProperty(path) +"expect(received).not.toHaveProperty(path) -Expected the object: - {\\"a\\": {\\"b\\": undefined}} -Not to have a nested property: - \\"a.b\\" -" +Expected path: not \\"a.b\\" + +Received value: undefined" `; exports[`.toHaveProperty() {pass: true} expect({"a": {"b": undefined}}).toHaveProperty('a.b', undefined) 1`] = ` -"expect(object).not.toHaveProperty(path, value) - -Expected the object: - {\\"a\\": {\\"b\\": undefined}} -Not to have a nested property: - \\"a.b\\" -With a value of: - undefined -" +"expect(received).not.toHaveProperty(path, value) + +Expected path: \\"a.b\\" + +Expected value: not undefined" +`; + +exports[`.toHaveProperty() {pass: true} expect({"a": {}}).toHaveProperty('a.b', undefined) 1`] = ` +"expect(received).not.toHaveProperty(path, value) + +Expected path: \\"a.b\\" +Received path: \\"a\\" + +Expected value: not undefined +Received value: {} + +Because a positive assertion passes for expected value undefined if the property does not exist, this negative assertion fails unless the property does exist and has a defined value" `; exports[`.toHaveProperty() {pass: true} expect({"a": 0}).toHaveProperty('a') 1`] = ` -"expect(object).not.toHaveProperty(path) +"expect(received).not.toHaveProperty(path) -Expected the object: - {\\"a\\": 0} -Not to have a nested property: - \\"a\\" -" +Expected path: not \\"a\\" + +Received value: 0" `; exports[`.toHaveProperty() {pass: true} expect({"a": 0}).toHaveProperty('a', 0) 1`] = ` -"expect(object).not.toHaveProperty(path, value) - -Expected the object: - {\\"a\\": 0} -Not to have a nested property: - \\"a\\" -With a value of: - 0 -" +"expect(received).not.toHaveProperty(path, value) + +Expected path: \\"a\\" + +Expected value: not 0" `; exports[`.toHaveProperty() {pass: true} expect({"a.b.c.d": 1}).toHaveProperty('a.b.c.d') 1`] = ` -"expect(object).not.toHaveProperty(path) +"expect(received).not.toHaveProperty(path) -Expected the object: - {\\"a.b.c.d\\": 1} -Not to have a nested property: - [\\"a.b.c.d\\"] -" +Expected path: not [\\"a.b.c.d\\"] + +Received value: 1" `; exports[`.toHaveProperty() {pass: true} expect({"a.b.c.d": 1}).toHaveProperty('a.b.c.d', 1) 1`] = ` -"expect(object).not.toHaveProperty(path, value) - -Expected the object: - {\\"a.b.c.d\\": 1} -Not to have a nested property: - [\\"a.b.c.d\\"] -With a value of: - 1 -" +"expect(received).not.toHaveProperty(path, value) + +Expected path: [\\"a.b.c.d\\"] + +Expected value: not 1" `; exports[`.toHaveProperty() {pass: true} expect({"nodeName": "DIV"}).toHaveProperty('nodeType', 1) 1`] = ` -"expect(object).not.toHaveProperty(path, value) - -Expected the object: - {\\"nodeName\\": \\"DIV\\"} -Not to have a nested property: - \\"nodeType\\" -With a value of: - 1 -" +"expect(received).not.toHaveProperty(path, value) + +Expected path: \\"nodeType\\" + +Expected value: not 1" `; exports[`.toHaveProperty() {pass: true} expect({"property": 1}).toHaveProperty('property', 1) 1`] = ` -"expect(object).not.toHaveProperty(path, value) - -Expected the object: - {\\"property\\": 1} -Not to have a nested property: - \\"property\\" -With a value of: - 1 -" +"expect(received).not.toHaveProperty(path, value) + +Expected path: \\"property\\" + +Expected value: not 1" `; exports[`.toHaveProperty() {pass: true} expect({"val": true}).toHaveProperty('a', undefined) 1`] = ` -"expect(object).not.toHaveProperty(path, value) - -Expected the object: - {\\"val\\": true} -Not to have a nested property: - \\"a\\" -With a value of: - undefined -" +"expect(received).not.toHaveProperty(path, value) + +Expected path: \\"a\\" + +Expected value: not undefined" `; exports[`.toHaveProperty() {pass: true} expect({"val": true}).toHaveProperty('c', "c") 1`] = ` -"expect(object).not.toHaveProperty(path, value) - -Expected the object: - {\\"val\\": true} -Not to have a nested property: - \\"c\\" -With a value of: - \\"c\\" -" +"expect(received).not.toHaveProperty(path, value) + +Expected path: \\"c\\" + +Expected value: not \\"c\\"" `; exports[`.toHaveProperty() {pass: true} expect({"val": true}).toHaveProperty('val', true) 1`] = ` -"expect(object).not.toHaveProperty(path, value) - -Expected the object: - {\\"val\\": true} -Not to have a nested property: - \\"val\\" -With a value of: - true -" +"expect(received).not.toHaveProperty(path, value) + +Expected path: \\"val\\" + +Expected value: not true" `; exports[`.toHaveProperty() {pass: true} expect({}).toHaveProperty('a', undefined) 1`] = ` -"expect(object).not.toHaveProperty(path, value) - -Expected the object: - {} -Not to have a nested property: - \\"a\\" -With a value of: - undefined -" +"expect(received).not.toHaveProperty(path, value) + +Expected path: \\"a\\" + +Expected value: not undefined" `; exports[`.toHaveProperty() {pass: true} expect({}).toHaveProperty('b', "b") 1`] = ` -"expect(object).not.toHaveProperty(path, value) - -Expected the object: - {} -Not to have a nested property: - \\"b\\" -With a value of: - \\"b\\" -" +"expect(received).not.toHaveProperty(path, value) + +Expected path: \\"b\\" + +Expected value: not \\"b\\"" `; exports[`.toHaveProperty() {pass: true} expect({}).toHaveProperty('setter', undefined) 1`] = ` -"expect(object).not.toHaveProperty(path, value) - -Expected the object: - {} -Not to have a nested property: - \\"setter\\" -With a value of: - undefined -" +"expect(received).not.toHaveProperty(path, value) + +Expected path: \\"setter\\" + +Expected value: not undefined" `; exports[`.toMatch() {pass: true} expect(Foo bar).toMatch(/^foo/i) 1`] = ` @@ -3615,13 +3489,8 @@ Expected: not {\\"test\\": {\\"a\\": 1, \\"b\\": 2}} `; exports[`toMatchObject() {pass: false} expect([0]).toMatchObject([-0]) 1`] = ` -"expect(received).toMatchObject(expected) +"expect(received).toMatchObject(expected) -Expected value to match object: - [-0] -Received: - [0] -Difference: - Expected + Received @@ -3632,13 +3501,8 @@ Difference: `; exports[`toMatchObject() {pass: false} expect([1, 2, 3]).toMatchObject([1, 2, 2]) 1`] = ` -"expect(received).toMatchObject(expected) +"expect(received).toMatchObject(expected) -Expected value to match object: - [1, 2, 2] -Received: - [1, 2, 3] -Difference: - Expected + Received @@ -3651,13 +3515,8 @@ Difference: `; exports[`toMatchObject() {pass: false} expect([1, 2, 3]).toMatchObject([2, 3, 1]) 1`] = ` -"expect(received).toMatchObject(expected) +"expect(received).toMatchObject(expected) -Expected value to match object: - [2, 3, 1] -Received: - [1, 2, 3] -Difference: - Expected + Received @@ -3670,13 +3529,8 @@ Difference: `; exports[`toMatchObject() {pass: false} expect([1, 2]).toMatchObject([1, 3]) 1`] = ` -"expect(received).toMatchObject(expected) +"expect(received).toMatchObject(expected) -Expected value to match object: - [1, 3] -Received: - [1, 2] -Difference: - Expected + Received @@ -3688,28 +3542,15 @@ Difference: `; exports[`toMatchObject() {pass: false} expect([Error: foo]).toMatchObject([Error: bar]) 1`] = ` -"expect(received).toMatchObject(expected) - -Expected value to match object: - [Error: bar] -Received: - [Error: foo] -Difference: -- Expected -+ Received +"expect(received).toMatchObject(expected) -- [Error: bar] -+ [Error: foo]" +Expected: [Error: bar] +Received: [Error: foo]" `; exports[`toMatchObject() {pass: false} expect({"a": "a", "c": "d"}).toMatchObject({"a": Any}) 1`] = ` -"expect(received).toMatchObject(expected) +"expect(received).toMatchObject(expected) -Expected value to match object: - {\\"a\\": Any} -Received: - {\\"a\\": \\"a\\", \\"c\\": \\"d\\"} -Difference: - Expected + Received @@ -3720,13 +3561,8 @@ Difference: `; exports[`toMatchObject() {pass: false} expect({"a": "b", "c": "d"}).toMatchObject({"a": "b!", "c": "d"}) 1`] = ` -"expect(received).toMatchObject(expected) +"expect(received).toMatchObject(expected) -Expected value to match object: - {\\"a\\": \\"b!\\", \\"c\\": \\"d\\"} -Received: - {\\"a\\": \\"b\\", \\"c\\": \\"d\\"} -Difference: - Expected + Received @@ -3738,13 +3574,8 @@ Difference: `; exports[`toMatchObject() {pass: false} expect({"a": "b", "c": "d"}).toMatchObject({"e": "b"}) 1`] = ` -"expect(received).toMatchObject(expected) +"expect(received).toMatchObject(expected) -Expected value to match object: - {\\"e\\": \\"b\\"} -Received: - {\\"a\\": \\"b\\", \\"c\\": \\"d\\"} -Difference: - Expected + Received @@ -3756,13 +3587,8 @@ Difference: `; exports[`toMatchObject() {pass: false} expect({"a": "b", "t": {"x": {"r": "r"}, "z": "z"}}).toMatchObject({"a": "b", "t": {"z": [3]}}) 1`] = ` -"expect(received).toMatchObject(expected) +"expect(received).toMatchObject(expected) -Expected value to match object: - {\\"a\\": \\"b\\", \\"t\\": {\\"z\\": [3]}} -Received: - {\\"a\\": \\"b\\", \\"t\\": {\\"x\\": {\\"r\\": \\"r\\"}, \\"z\\": \\"z\\"}} -Difference: - Expected + Received @@ -3778,13 +3604,8 @@ Difference: `; exports[`toMatchObject() {pass: false} expect({"a": "b", "t": {"x": {"r": "r"}, "z": "z"}}).toMatchObject({"t": {"l": {"r": "r"}}}) 1`] = ` -"expect(received).toMatchObject(expected) +"expect(received).toMatchObject(expected) -Expected value to match object: - {\\"t\\": {\\"l\\": {\\"r\\": \\"r\\"}}} -Received: - {\\"a\\": \\"b\\", \\"t\\": {\\"x\\": {\\"r\\": \\"r\\"}, \\"z\\": \\"z\\"}} -Difference: - Expected + Received @@ -3800,13 +3621,8 @@ Difference: `; exports[`toMatchObject() {pass: false} expect({"a": "b"}).toMatchObject({"c": "d"}) 1`] = ` -"expect(received).toMatchObject(expected) +"expect(received).toMatchObject(expected) -Expected value to match object: - {\\"c\\": \\"d\\"} -Received: - {\\"a\\": \\"b\\"} -Difference: - Expected + Received @@ -3817,13 +3633,8 @@ Difference: `; exports[`toMatchObject() {pass: false} expect({"a": [{"a": "a", "b": "b"}]}).toMatchObject({"a": [{"a": "c"}]}) 1`] = ` -"expect(received).toMatchObject(expected) +"expect(received).toMatchObject(expected) -Expected value to match object: - {\\"a\\": [{\\"a\\": \\"c\\"}]} -Received: - {\\"a\\": [{\\"a\\": \\"a\\", \\"b\\": \\"b\\"}]} -Difference: - Expected + Received @@ -3838,13 +3649,8 @@ Difference: `; exports[`toMatchObject() {pass: false} expect({"a": [3, 4, "v"], "b": "b"}).toMatchObject({"a": ["v"]}) 1`] = ` -"expect(received).toMatchObject(expected) +"expect(received).toMatchObject(expected) -Expected value to match object: - {\\"a\\": [\\"v\\"]} -Received: - {\\"a\\": [3, 4, \\"v\\"], \\"b\\": \\"b\\"} -Difference: - Expected + Received @@ -3858,13 +3664,8 @@ Difference: `; exports[`toMatchObject() {pass: false} expect({"a": [3, 4, 5], "b": "b"}).toMatchObject({"a": [3, 4, 5, 6]}) 1`] = ` -"expect(received).toMatchObject(expected) +"expect(received).toMatchObject(expected) -Expected value to match object: - {\\"a\\": [3, 4, 5, 6]} -Received: - {\\"a\\": [3, 4, 5], \\"b\\": \\"b\\"} -Difference: - Expected + Received @@ -3879,13 +3680,8 @@ Difference: `; exports[`toMatchObject() {pass: false} expect({"a": [3, 4, 5], "b": "b"}).toMatchObject({"a": [3, 4]}) 1`] = ` -"expect(received).toMatchObject(expected) +"expect(received).toMatchObject(expected) -Expected value to match object: - {\\"a\\": [3, 4]} -Received: - {\\"a\\": [3, 4, 5], \\"b\\": \\"b\\"} -Difference: - Expected + Received @@ -3899,13 +3695,8 @@ Difference: `; exports[`toMatchObject() {pass: false} expect({"a": [3, 4, 5], "b": "b"}).toMatchObject({"a": {"b": 4}}) 1`] = ` -"expect(received).toMatchObject(expected) +"expect(received).toMatchObject(expected) -Expected value to match object: - {\\"a\\": {\\"b\\": 4}} -Received: - {\\"a\\": [3, 4, 5], \\"b\\": \\"b\\"} -Difference: - Expected + Received @@ -3922,13 +3713,8 @@ Difference: `; exports[`toMatchObject() {pass: false} expect({"a": [3, 4, 5], "b": "b"}).toMatchObject({"a": {"b": Any}}) 1`] = ` -"expect(received).toMatchObject(expected) +"expect(received).toMatchObject(expected) -Expected value to match object: - {\\"a\\": {\\"b\\": Any}} -Received: - {\\"a\\": [3, 4, 5], \\"b\\": \\"b\\"} -Difference: - Expected + Received @@ -3945,13 +3731,8 @@ Difference: `; exports[`toMatchObject() {pass: false} expect({"a": 1, "b": 1, "c": 1, "d": {"e": {"f": 555}}}).toMatchObject({"d": {"e": {"f": 222}}}) 1`] = ` -"expect(received).toMatchObject(expected) +"expect(received).toMatchObject(expected) -Expected value to match object: - {\\"d\\": {\\"e\\": {\\"f\\": 222}}} -Received: - {\\"a\\": 1, \\"b\\": 1, \\"c\\": 1, \\"d\\": {\\"e\\": {\\"f\\": 555}}} -Difference: - Expected + Received @@ -3966,13 +3747,8 @@ Difference: `; exports[`toMatchObject() {pass: false} expect({"a": 2015-11-30T00:00:00.000Z, "b": "b"}).toMatchObject({"a": 2015-10-10T00:00:00.000Z}) 1`] = ` -"expect(received).toMatchObject(expected) +"expect(received).toMatchObject(expected) -Expected value to match object: - {\\"a\\": 2015-10-10T00:00:00.000Z} -Received: - {\\"a\\": 2015-11-30T00:00:00.000Z, \\"b\\": \\"b\\"} -Difference: - Expected + Received @@ -3983,13 +3759,8 @@ Difference: `; exports[`toMatchObject() {pass: false} expect({"a": null, "b": "b"}).toMatchObject({"a": "4"}) 1`] = ` -"expect(received).toMatchObject(expected) +"expect(received).toMatchObject(expected) -Expected value to match object: - {\\"a\\": \\"4\\"} -Received: - {\\"a\\": null, \\"b\\": \\"b\\"} -Difference: - Expected + Received @@ -4000,13 +3771,8 @@ Difference: `; exports[`toMatchObject() {pass: false} expect({"a": null, "b": "b"}).toMatchObject({"a": undefined}) 1`] = ` -"expect(received).toMatchObject(expected) +"expect(received).toMatchObject(expected) -Expected value to match object: - {\\"a\\": undefined} -Received: - {\\"a\\": null, \\"b\\": \\"b\\"} -Difference: - Expected + Received @@ -4017,13 +3783,8 @@ Difference: `; exports[`toMatchObject() {pass: false} expect({"a": undefined}).toMatchObject({"a": null}) 1`] = ` -"expect(received).toMatchObject(expected) +"expect(received).toMatchObject(expected) -Expected value to match object: - {\\"a\\": null} -Received: - {\\"a\\": undefined} -Difference: - Expected + Received @@ -4034,13 +3795,8 @@ Difference: `; exports[`toMatchObject() {pass: false} expect({}).toMatchObject({"a": undefined}) 1`] = ` -"expect(received).toMatchObject(expected) +"expect(received).toMatchObject(expected) -Expected value to match object: - {\\"a\\": undefined} -Received: - {} -Difference: - Expected + Received @@ -4051,28 +3807,15 @@ Difference: `; exports[`toMatchObject() {pass: false} expect(2015-11-30T00:00:00.000Z).toMatchObject(2015-10-10T00:00:00.000Z) 1`] = ` -"expect(received).toMatchObject(expected) - -Expected value to match object: - 2015-10-10T00:00:00.000Z -Received: - 2015-11-30T00:00:00.000Z -Difference: -- Expected -+ Received +"expect(received).toMatchObject(expected) -- 2015-10-10T00:00:00.000Z -+ 2015-11-30T00:00:00.000Z" +Expected: 2015-10-10T00:00:00.000Z +Received: 2015-11-30T00:00:00.000Z" `; exports[`toMatchObject() {pass: false} expect(Set {1, 2}).toMatchObject(Set {2}) 1`] = ` -"expect(received).toMatchObject(expected) +"expect(received).toMatchObject(expected) -Expected value to match object: - Set {2} -Received: - Set {1, 2} -Difference: - Expected + Received @@ -4083,205 +3826,153 @@ Difference: `; exports[`toMatchObject() {pass: true} expect([]).toMatchObject([]) 1`] = ` -"expect(received).not.toMatchObject(expected) +"expect(received).not.toMatchObject(expected) -Expected value not to match object: - [] -Received: - []" +Expected: not []" `; exports[`toMatchObject() {pass: true} expect([1, 2]).toMatchObject([1, 2]) 1`] = ` -"expect(received).not.toMatchObject(expected) +"expect(received).not.toMatchObject(expected) -Expected value not to match object: - [1, 2] -Received: - [1, 2]" +Expected: not [1, 2]" `; exports[`toMatchObject() {pass: true} expect([Error: bar]).toMatchObject({"message": "bar"}) 1`] = ` -"expect(received).not.toMatchObject(expected) +"expect(received).not.toMatchObject(expected) -Expected value not to match object: - {\\"message\\": \\"bar\\"} -Received: - [Error: bar]" +Expected: not {\\"message\\": \\"bar\\"} +Received: [Error: bar]" `; exports[`toMatchObject() {pass: true} expect([Error: foo]).toMatchObject([Error: foo]) 1`] = ` -"expect(received).not.toMatchObject(expected) +"expect(received).not.toMatchObject(expected) -Expected value not to match object: - [Error: foo] -Received: - [Error: foo]" +Expected: not [Error: foo]" `; exports[`toMatchObject() {pass: true} expect({"a": "b", "c": "d"}).toMatchObject({"a": "b", "c": "d"}) 1`] = ` -"expect(received).not.toMatchObject(expected) +"expect(received).not.toMatchObject(expected) -Expected value not to match object: - {\\"a\\": \\"b\\", \\"c\\": \\"d\\"} -Received: - {\\"a\\": \\"b\\", \\"c\\": \\"d\\"}" +Expected: not {\\"a\\": \\"b\\", \\"c\\": \\"d\\"}" `; exports[`toMatchObject() {pass: true} expect({"a": "b", "c": "d"}).toMatchObject({"a": "b"}) 1`] = ` -"expect(received).not.toMatchObject(expected) +"expect(received).not.toMatchObject(expected) -Expected value not to match object: - {\\"a\\": \\"b\\"} -Received: - {\\"a\\": \\"b\\", \\"c\\": \\"d\\"}" +Expected: not {\\"a\\": \\"b\\"} +Received: {\\"a\\": \\"b\\", \\"c\\": \\"d\\"}" `; exports[`toMatchObject() {pass: true} expect({"a": "b", "t": {"x": {"r": "r"}, "z": "z"}}).toMatchObject({"a": "b", "t": {"z": "z"}}) 1`] = ` -"expect(received).not.toMatchObject(expected) +"expect(received).not.toMatchObject(expected) -Expected value not to match object: - {\\"a\\": \\"b\\", \\"t\\": {\\"z\\": \\"z\\"}} -Received: - {\\"a\\": \\"b\\", \\"t\\": {\\"x\\": {\\"r\\": \\"r\\"}, \\"z\\": \\"z\\"}}" +Expected: not {\\"a\\": \\"b\\", \\"t\\": {\\"z\\": \\"z\\"}} +Received: {\\"a\\": \\"b\\", \\"t\\": {\\"x\\": {\\"r\\": \\"r\\"}, \\"z\\": \\"z\\"}}" `; exports[`toMatchObject() {pass: true} expect({"a": "b", "t": {"x": {"r": "r"}, "z": "z"}}).toMatchObject({"t": {"x": {"r": "r"}}}) 1`] = ` -"expect(received).not.toMatchObject(expected) +"expect(received).not.toMatchObject(expected) -Expected value not to match object: - {\\"t\\": {\\"x\\": {\\"r\\": \\"r\\"}}} -Received: - {\\"a\\": \\"b\\", \\"t\\": {\\"x\\": {\\"r\\": \\"r\\"}, \\"z\\": \\"z\\"}}" +Expected: not {\\"t\\": {\\"x\\": {\\"r\\": \\"r\\"}}} +Received: {\\"a\\": \\"b\\", \\"t\\": {\\"x\\": {\\"r\\": \\"r\\"}, \\"z\\": \\"z\\"}}" `; exports[`toMatchObject() {pass: true} expect({"a": "b"}).toMatchObject({"a": "b"}) 1`] = ` -"expect(received).not.toMatchObject(expected) +"expect(received).not.toMatchObject(expected) -Expected value not to match object: - {\\"a\\": \\"b\\"} -Received: - {\\"a\\": \\"b\\"}" +Expected: not {\\"a\\": \\"b\\"}" `; exports[`toMatchObject() {pass: true} expect({"a": [{"a": "a", "b": "b"}]}).toMatchObject({"a": [{"a": "a"}]}) 1`] = ` -"expect(received).not.toMatchObject(expected) +"expect(received).not.toMatchObject(expected) -Expected value not to match object: - {\\"a\\": [{\\"a\\": \\"a\\"}]} -Received: - {\\"a\\": [{\\"a\\": \\"a\\", \\"b\\": \\"b\\"}]}" +Expected: not {\\"a\\": [{\\"a\\": \\"a\\"}]} +Received: {\\"a\\": [{\\"a\\": \\"a\\", \\"b\\": \\"b\\"}]}" `; exports[`toMatchObject() {pass: true} expect({"a": [3, 4, 5, "v"], "b": "b"}).toMatchObject({"a": [3, 4, 5, "v"]}) 1`] = ` -"expect(received).not.toMatchObject(expected) +"expect(received).not.toMatchObject(expected) -Expected value not to match object: - {\\"a\\": [3, 4, 5, \\"v\\"]} -Received: - {\\"a\\": [3, 4, 5, \\"v\\"], \\"b\\": \\"b\\"}" +Expected: not {\\"a\\": [3, 4, 5, \\"v\\"]} +Received: {\\"a\\": [3, 4, 5, \\"v\\"], \\"b\\": \\"b\\"}" `; exports[`toMatchObject() {pass: true} expect({"a": [3, 4, 5], "b": "b"}).toMatchObject({"a": [3, 4, 5]}) 1`] = ` -"expect(received).not.toMatchObject(expected) +"expect(received).not.toMatchObject(expected) -Expected value not to match object: - {\\"a\\": [3, 4, 5]} -Received: - {\\"a\\": [3, 4, 5], \\"b\\": \\"b\\"}" +Expected: not {\\"a\\": [3, 4, 5]} +Received: {\\"a\\": [3, 4, 5], \\"b\\": \\"b\\"}" `; exports[`toMatchObject() {pass: true} expect({"a": {"x": "x", "y": "y"}}).toMatchObject({"a": {"x": Any}}) 1`] = ` -"expect(received).not.toMatchObject(expected) +"expect(received).not.toMatchObject(expected) -Expected value not to match object: - {\\"a\\": {\\"x\\": Any}} -Received: - {\\"a\\": {\\"x\\": \\"x\\", \\"y\\": \\"y\\"}}" +Expected: not {\\"a\\": {\\"x\\": Any}} +Received: {\\"a\\": {\\"x\\": \\"x\\", \\"y\\": \\"y\\"}}" `; exports[`toMatchObject() {pass: true} expect({"a": 1, "c": 2}).toMatchObject({"a": Any}) 1`] = ` -"expect(received).not.toMatchObject(expected) +"expect(received).not.toMatchObject(expected) -Expected value not to match object: - {\\"a\\": Any} -Received: - {\\"a\\": 1, \\"c\\": 2}" +Expected: not {\\"a\\": Any} +Received: {\\"a\\": 1, \\"c\\": 2}" `; exports[`toMatchObject() {pass: true} expect({"a": 2015-11-30T00:00:00.000Z, "b": "b"}).toMatchObject({"a": 2015-11-30T00:00:00.000Z}) 1`] = ` -"expect(received).not.toMatchObject(expected) +"expect(received).not.toMatchObject(expected) -Expected value not to match object: - {\\"a\\": 2015-11-30T00:00:00.000Z} -Received: - {\\"a\\": 2015-11-30T00:00:00.000Z, \\"b\\": \\"b\\"}" +Expected: not {\\"a\\": 2015-11-30T00:00:00.000Z} +Received: {\\"a\\": 2015-11-30T00:00:00.000Z, \\"b\\": \\"b\\"}" `; exports[`toMatchObject() {pass: true} expect({"a": null, "b": "b"}).toMatchObject({"a": null}) 1`] = ` -"expect(received).not.toMatchObject(expected) +"expect(received).not.toMatchObject(expected) -Expected value not to match object: - {\\"a\\": null} -Received: - {\\"a\\": null, \\"b\\": \\"b\\"}" +Expected: not {\\"a\\": null} +Received: {\\"a\\": null, \\"b\\": \\"b\\"}" `; exports[`toMatchObject() {pass: true} expect({"a": undefined, "b": "b"}).toMatchObject({"a": undefined}) 1`] = ` -"expect(received).not.toMatchObject(expected) +"expect(received).not.toMatchObject(expected) -Expected value not to match object: - {\\"a\\": undefined} -Received: - {\\"a\\": undefined, \\"b\\": \\"b\\"}" +Expected: not {\\"a\\": undefined} +Received: {\\"a\\": undefined, \\"b\\": \\"b\\"}" `; exports[`toMatchObject() {pass: true} expect({"a": undefined}).toMatchObject({"a": undefined}) 1`] = ` -"expect(received).not.toMatchObject(expected) +"expect(received).not.toMatchObject(expected) -Expected value not to match object: - {\\"a\\": undefined} -Received: - {\\"a\\": undefined}" +Expected: not {\\"a\\": undefined}" `; exports[`toMatchObject() {pass: true} expect({}).toMatchObject({"a": undefined, "b": "b"}) 1`] = ` -"expect(received).not.toMatchObject(expected) +"expect(received).not.toMatchObject(expected) -Expected value not to match object: - {\\"a\\": undefined, \\"b\\": \\"b\\"} -Received: - {}" +Expected: not {\\"a\\": undefined, \\"b\\": \\"b\\"} +Received: {}" `; exports[`toMatchObject() {pass: true} expect(2015-11-30T00:00:00.000Z).toMatchObject(2015-11-30T00:00:00.000Z) 1`] = ` -"expect(received).not.toMatchObject(expected) +"expect(received).not.toMatchObject(expected) -Expected value not to match object: - 2015-11-30T00:00:00.000Z -Received: - 2015-11-30T00:00:00.000Z" +Expected: not 2015-11-30T00:00:00.000Z" `; exports[`toMatchObject() {pass: true} expect(Set {1, 2}).toMatchObject(Set {1, 2}) 1`] = ` -"expect(received).not.toMatchObject(expected) +"expect(received).not.toMatchObject(expected) -Expected value not to match object: - Set {1, 2} -Received: - Set {1, 2}" +Expected: not Set {1, 2}" `; exports[`toMatchObject() {pass: true} expect(Set {1, 2}).toMatchObject(Set {2, 1}) 1`] = ` -"expect(received).not.toMatchObject(expected) +"expect(received).not.toMatchObject(expected) -Expected value not to match object: - Set {2, 1} -Received: - Set {1, 2}" +Expected: not Set {2, 1} +Received: Set {1, 2}" `; exports[`toMatchObject() throws expect("44").toMatchObject({}) 1`] = ` -"expect(received).toMatchObject(expected) +"expect(received).toMatchObject(expected) Matcher error: received value must be a non-null object @@ -4290,7 +3981,7 @@ Received has value: \\"44\\"" `; exports[`toMatchObject() throws expect({}).toMatchObject("some string") 1`] = ` -"expect(received).toMatchObject(expected) +"expect(received).toMatchObject(expected) Matcher error: expected value must be a non-null object @@ -4299,7 +3990,7 @@ Expected has value: \\"some string\\"" `; exports[`toMatchObject() throws expect({}).toMatchObject(4) 1`] = ` -"expect(received).toMatchObject(expected) +"expect(received).toMatchObject(expected) Matcher error: expected value must be a non-null object @@ -4308,7 +3999,7 @@ Expected has value: 4" `; exports[`toMatchObject() throws expect({}).toMatchObject(null) 1`] = ` -"expect(received).toMatchObject(expected) +"expect(received).toMatchObject(expected) Matcher error: expected value must be a non-null object @@ -4316,7 +4007,7 @@ Expected has value: null" `; exports[`toMatchObject() throws expect({}).toMatchObject(true) 1`] = ` -"expect(received).toMatchObject(expected) +"expect(received).toMatchObject(expected) Matcher error: expected value must be a non-null object @@ -4325,7 +4016,7 @@ Expected has value: true" `; exports[`toMatchObject() throws expect({}).toMatchObject(undefined) 1`] = ` -"expect(received).toMatchObject(expected) +"expect(received).toMatchObject(expected) Matcher error: expected value must be a non-null object @@ -4333,7 +4024,7 @@ Expected has value: undefined" `; exports[`toMatchObject() throws expect(4).toMatchObject({}) 1`] = ` -"expect(received).toMatchObject(expected) +"expect(received).toMatchObject(expected) Matcher error: received value must be a non-null object @@ -4342,7 +4033,7 @@ Received has value: 4" `; exports[`toMatchObject() throws expect(null).toMatchObject({}) 1`] = ` -"expect(received).toMatchObject(expected) +"expect(received).toMatchObject(expected) Matcher error: received value must be a non-null object @@ -4350,7 +4041,7 @@ Received has value: null" `; exports[`toMatchObject() throws expect(true).toMatchObject({}) 1`] = ` -"expect(received).toMatchObject(expected) +"expect(received).toMatchObject(expected) Matcher error: received value must be a non-null object @@ -4359,7 +4050,7 @@ Received has value: true" `; exports[`toMatchObject() throws expect(undefined).toMatchObject({}) 1`] = ` -"expect(received).toMatchObject(expected) +"expect(received).toMatchObject(expected) Matcher error: received value must be a non-null object diff --git a/packages/expect/src/__tests__/asymmetricMatchers.test.js b/packages/expect/src/__tests__/asymmetricMatchers.test.js index 9b1d55074cad..f0166e38d04b 100644 --- a/packages/expect/src/__tests__/asymmetricMatchers.test.js +++ b/packages/expect/src/__tests__/asymmetricMatchers.test.js @@ -42,6 +42,47 @@ test('Any.toAsymmetricMatcher()', () => { jestExpect(any(Number).toAsymmetricMatcher()).toBe('Any'); }); +test('Any.toAsymmetricMatcher() with function name', () => { + [ + ['someFunc', function someFunc() {}], + ['$someFunc', function $someFunc() {}], + [ + '$someFunc2', + (function() { + function $someFunc2() {} + Object.defineProperty($someFunc2, 'name', {value: ''}); + return $someFunc2; + })(), + ], + [ + '$someAsyncFunc', + (function() { + async function $someAsyncFunc() {} + Object.defineProperty($someAsyncFunc, 'name', {value: ''}); + return $someAsyncFunc; + })(), + ], + [ + '$someGeneratorFunc', + (function() { + function* $someGeneratorFunc() {} + Object.defineProperty($someGeneratorFunc, 'name', {value: ''}); + return $someGeneratorFunc; + })(), + ], + [ + '$someFuncWithFakeToString', + (function() { + function $someFuncWithFakeToString() {} + $someFuncWithFakeToString.toString = () => 'Fake to string'; + return $someFuncWithFakeToString; + })(), + ], + ].forEach(([name, fn]: [string, any]) => { + jestExpect(any(fn).toAsymmetricMatcher()).toBe(`Any<${name}>`); + }); +}); + test('Any throws when called with empty constructor', () => { jestExpect(() => any()).toThrow(); }); diff --git a/packages/expect/src/__tests__/matchers.test.js b/packages/expect/src/__tests__/matchers.test.js index 8c82661a6987..ad143ffa0437 100644 --- a/packages/expect/src/__tests__/matchers.test.js +++ b/packages/expect/src/__tests__/matchers.test.js @@ -1340,8 +1340,10 @@ describe('.toHaveProperty()', () => { [{a: {b: {c: {d: 1}}}}, ['a', 'b', 'c', 'd'], 1], [{'a.b.c.d': 1}, ['a.b.c.d'], 1], [{a: {b: [1, 2, 3]}}, ['a', 'b', 1], 2], + [{a: {b: [1, 2, 3]}}, ['a', 'b', 1], expect.any(Number)], [{a: 0}, 'a', 0], [{a: {b: undefined}}, 'a.b', undefined], + [{a: {}}, 'a.b', undefined], // delete for breaking change in future major [{a: {b: {c: 5}}}, 'a.b', {c: 5}], [Object.assign(Object.create(null), {property: 1}), 'property', 1], [new Foo(), 'a', undefined], @@ -1379,7 +1381,7 @@ describe('.toHaveProperty()', () => { [{a: {b: {c: 5}}}, 'a.b', {c: 4}], [new Foo(), 'a', 'a'], [new Foo(), 'b', undefined], - // [{a: {}}, 'a.b', undefined], // wait until Jest 25 + // [{a: {}}, 'a.b', undefined], // add for breaking change in future major ].forEach(([obj, keyPath, value]) => { test(`{pass: false} expect(${stringify( obj, diff --git a/packages/expect/src/jasmineUtils.ts b/packages/expect/src/jasmineUtils.ts index 8c98416fbd66..3bf94b3cc3c8 100644 --- a/packages/expect/src/jasmineUtils.ts +++ b/packages/expect/src/jasmineUtils.ts @@ -37,6 +37,8 @@ export function equals( return eq(a, b, [], [], customTesters, strictCheck ? hasKey : hasDefinedKey); } +const functionToString = Function.prototype.toString; + function isAsymmetric(obj: any) { return !!obj && isA('Function', obj.asymmetricMatch); } @@ -258,7 +260,9 @@ export function fnNameFor(func: Function) { return func.name; } - const matches = func.toString().match(/^\s*function\s*(\w*)\s*\(/); + const matches = functionToString + .call(func) + .match(/^(?:async)?\s*function\s*\*?\s*([\w$]+)\s*\(/); return matches ? matches[1] : ''; } diff --git a/packages/expect/src/matchers.ts b/packages/expect/src/matchers.ts index a26fe2a7ebbe..2309facee19a 100644 --- a/packages/expect/src/matchers.ts +++ b/packages/expect/src/matchers.ts @@ -12,7 +12,6 @@ import { EXPECTED_COLOR, RECEIVED_COLOR, SUGGEST_TO_CONTAIN_EQUAL, - diff, ensureExpectedIsNonNegativeInteger, ensureNoExpected, ensureNumbers, @@ -42,9 +41,11 @@ import { } from './utils'; import {equals} from './jasmineUtils'; -// Include colon and one or more spaces, same as returned by getLabelPrinter. -const EXPECTED_LABEL = 'Expected: '; -const RECEIVED_LABEL = 'Received: '; +// Omit colon and one or more spaces, so can call getLabelPrinter. +const EXPECTED_LABEL = 'Expected'; +const RECEIVED_LABEL = 'Received'; +const EXPECTED_VALUE_LABEL = 'Expected value'; +const RECEIVED_VALUE_LABEL = 'Received value'; const toStrictEqualTesters = [ iterableEquality, @@ -652,87 +653,123 @@ const matchers: MatchersObject = { toHaveProperty( this: MatcherState, - object: object, - keyPath: string | Array, - value?: unknown, + received: object, + expectedPath: string | Array, + expectedValue?: unknown, ) { - const matcherName = '.toHaveProperty'; - const valuePassed = arguments.length === 3; - const secondArgument = valuePassed ? 'value' : ''; + const matcherName = 'toHaveProperty'; + const expectedArgument = 'path'; + const hasValue = arguments.length === 3; const options: MatcherHintOptions = { isNot: this.isNot, - secondArgument, + promise: this.promise, + secondArgument: hasValue ? 'value' : '', }; - if (object === null || object === undefined) { + if (received === null || received === undefined) { throw new Error( matcherErrorMessage( - matcherHint(matcherName, undefined, 'path', options), + matcherHint(matcherName, undefined, expectedArgument, options), `${RECEIVED_COLOR('received')} value must not be null nor undefined`, - printWithType('Received', object, printReceived), + printWithType('Received', received, printReceived), ), ); } - const keyPathType = getType(keyPath); + const expectedPathType = getType(expectedPath); - if (keyPathType !== 'string' && keyPathType !== 'array') { + if (expectedPathType !== 'string' && expectedPathType !== 'array') { throw new Error( matcherErrorMessage( - matcherHint(matcherName, undefined, 'path', options), + matcherHint(matcherName, undefined, expectedArgument, options), `${EXPECTED_COLOR('expected')} path must be a string or array`, - printWithType('Expected', keyPath, printExpected), + printWithType('Expected', expectedPath, printExpected), ), ); } - const result = getPath(object, keyPath); - const {lastTraversedObject, hasEndProp} = result; + const expectedPathLength = + typeof expectedPath === 'string' + ? expectedPath.split('.').length + : expectedPath.length; - const pass = valuePassed - ? equals(result.value, value, [iterableEquality]) - : hasEndProp; + if (expectedPathType === 'array' && expectedPathLength === 0) { + throw new Error( + matcherErrorMessage( + matcherHint(matcherName, undefined, expectedArgument, options), + `${EXPECTED_COLOR('expected')} path must not be an empty array`, + printWithType('Expected', expectedPath, printExpected), + ), + ); + } - const traversedPath = result.traversedPath.join('.'); + const result = getPath(received, expectedPath); + const {lastTraversedObject, hasEndProp} = result; + const receivedPath = result.traversedPath; + const hasCompletePath = receivedPath.length === expectedPathLength; + const receivedValue = hasCompletePath ? result.value : lastTraversedObject; + + const pass = hasValue + ? equals(result.value, expectedValue, [iterableEquality]) + : Boolean(hasEndProp); // theoretically undefined if empty path + // Remove type cast if we rewrite getPath as iterative algorithm. + + // Delete this unique report if future breaking change + // removes the edge case that expected value undefined + // also matches absence of a property with the key path. + if (pass && !hasCompletePath) { + const message = () => + matcherHint(matcherName, undefined, expectedArgument, options) + + '\n\n' + + `Expected path: ${printExpected(expectedPath)}\n` + + `Received path: ${printReceived( + expectedPathType === 'array' || receivedPath.length === 0 + ? receivedPath + : receivedPath.join('.'), + )}\n\n` + + `Expected value: not ${printExpected(expectedValue)}\n` + + `Received value: ${printReceived(receivedValue)}\n\n` + + DIM_COLOR( + 'Because a positive assertion passes for expected value undefined if the property does not exist, this negative assertion fails unless the property does exist and has a defined value', + ); + + return {message, pass}; + } const message = pass ? () => - matcherHint(matcherName, 'object', 'path', options) + + matcherHint(matcherName, undefined, expectedArgument, options) + '\n\n' + - `Expected the object:\n` + - ` ${printReceived(object)}\n` + - `Not to have a nested property:\n` + - ` ${printExpected(keyPath)}\n` + - (valuePassed ? `With a value of:\n ${printExpected(value)}\n` : '') - : () => { - const difference = - valuePassed && hasEndProp - ? diff(value, result.value, {expand: this.expand}) - : ''; - return ( - matcherHint(matcherName, 'object', 'path', options) + - '\n\n' + - `Expected the object:\n` + - ` ${printReceived(object)}\n` + - `To have a nested property:\n` + - ` ${printExpected(keyPath)}\n` + - (valuePassed - ? `With a value of:\n ${printExpected(value)}\n` - : '') + - (hasEndProp - ? `Received:\n` + - ` ${printReceived(result.value)}` + - (difference ? `\n\nDifference:\n\n${difference}` : '') - : traversedPath - ? `Received:\n ${RECEIVED_COLOR( - 'object', - )}.${traversedPath}: ${printReceived(lastTraversedObject)}` - : '') - ); - }; - if (pass === undefined) { - throw new Error('pass must be initialized'); - } + (hasValue + ? `Expected path: ${printExpected(expectedPath)}\n\n` + + `Expected value: not ${printExpected(expectedValue)}` + + (stringify(expectedValue) !== stringify(receivedValue) + ? `\nReceived value: ${printReceived(receivedValue)}` + : '') + : `Expected path: not ${printExpected(expectedPath)}\n\n` + + `Received value: ${printReceived(receivedValue)}`) + : () => + matcherHint(matcherName, undefined, expectedArgument, options) + + '\n\n' + + `Expected path: ${printExpected(expectedPath)}\n` + + (hasCompletePath + ? '\n' + + printDiffOrStringify( + expectedValue, + receivedValue, + EXPECTED_VALUE_LABEL, + RECEIVED_VALUE_LABEL, + this.expand, + ) + : `Received path: ${printReceived( + expectedPathType === 'array' || receivedPath.length === 0 + ? receivedPath + : receivedPath.join('.'), + )}\n\n` + + (hasValue + ? `Expected value: ${printExpected(expectedValue)}\n` + : '') + + `Received value: ${printReceived(receivedValue)}`); return {message, pass}; }, @@ -812,65 +849,53 @@ const matchers: MatchersObject = { return {message, pass}; }, - toMatchObject( - this: MatcherState, - receivedObject: object, - expectedObject: object, - ) { - const matcherName = '.toMatchObject'; + toMatchObject(this: MatcherState, received: object, expected: object) { + const matcherName = 'toMatchObject'; const options: MatcherHintOptions = { isNot: this.isNot, + promise: this.promise, }; - if (typeof receivedObject !== 'object' || receivedObject === null) { + if (typeof received !== 'object' || received === null) { throw new Error( matcherErrorMessage( matcherHint(matcherName, undefined, undefined, options), `${RECEIVED_COLOR('received')} value must be a non-null object`, - printWithType('Received', receivedObject, printReceived), + printWithType('Received', received, printReceived), ), ); } - if (typeof expectedObject !== 'object' || expectedObject === null) { + if (typeof expected !== 'object' || expected === null) { throw new Error( matcherErrorMessage( matcherHint(matcherName, undefined, undefined, options), `${EXPECTED_COLOR('expected')} value must be a non-null object`, - printWithType('Expected', expectedObject, printExpected), + printWithType('Expected', expected, printExpected), ), ); } - const pass = equals(receivedObject, expectedObject, [ - iterableEquality, - subsetEquality, - ]); + const pass = equals(received, expected, [iterableEquality, subsetEquality]); const message = pass ? () => matcherHint(matcherName, undefined, undefined, options) + - `\n\nExpected value not to match object:\n` + - ` ${printExpected(expectedObject)}` + - `\nReceived:\n` + - ` ${printReceived(receivedObject)}` - : () => { - const difference = diff( - expectedObject, - getObjectSubset(receivedObject, expectedObject), - { - expand: this.expand, - }, - ); - return ( - matcherHint(matcherName, undefined, undefined, options) + - `\n\nExpected value to match object:\n` + - ` ${printExpected(expectedObject)}` + - `\nReceived:\n` + - ` ${printReceived(receivedObject)}` + - (difference ? `\nDifference:\n${difference}` : '') + '\n\n' + + `Expected: not ${printExpected(expected)}` + + (stringify(expected) !== stringify(received) + ? `\nReceived: ${printReceived(received)}` + : '') + : () => + matcherHint(matcherName, undefined, undefined, options) + + '\n\n' + + printDiffOrStringify( + expected, + getObjectSubset(received, expected), + EXPECTED_LABEL, + RECEIVED_LABEL, + this.expand, ); - }; return {message, pass}; }, diff --git a/packages/expect/src/print.ts b/packages/expect/src/print.ts index 725a16ddbfac..e5687941b481 100644 --- a/packages/expect/src/print.ts +++ b/packages/expect/src/print.ts @@ -11,6 +11,7 @@ import { INVERTED_COLOR, RECEIVED_COLOR, diff, + getLabelPrinter, printExpected, printReceived, stringify, @@ -103,18 +104,31 @@ export const printDiffOrStringify = ( // because stringify (that is, pretty-format with min option) // omits constructor name for array or object, too bad so sad :( const difference = shouldPrintDiff(expected, received) - ? diff(expected, received, {expand}) // string | null + ? diff(expected, received, { + aAnnotation: expectedLabel, + bAnnotation: receivedLabel, + expand, + }) // string | null : null; // Cannot reuse value of stringify(received) in report string, // because printReceived does inverse highlight space at end of line, // but RECEIVED_COLOR does not (it refers to a plain chalk method). - return typeof difference === 'string' && difference.includes('- Expected') - ? difference - : `${expectedLabel}${printExpected(expected)}\n` + - `${receivedLabel}${ - stringify(expected) === stringify(received) - ? 'serializes to the same string' - : printReceived(received) - }`; + if ( + typeof difference === 'string' && + difference.includes('- ' + expectedLabel) && + difference.includes('+ ' + receivedLabel) + ) { + return difference; + } + + const printLabel = getLabelPrinter(expectedLabel, receivedLabel); + return ( + `${printLabel(expectedLabel)}${printExpected(expected)}\n` + + `${printLabel(receivedLabel)}${ + stringify(expected) === stringify(received) + ? 'serializes to the same string' + : printReceived(received) + }` + ); }; diff --git a/packages/jest-circus/README.md b/packages/jest-circus/README.md index af963748aa87..0c5e282ffc3e 100644 --- a/packages/jest-circus/README.md +++ b/packages/jest-circus/README.md @@ -9,6 +9,25 @@ Circus is a flux-based test runner for Jest that is fast, easy to maintain, and simple to extend. +Circus allows you to bind to events via an optional event handler on any [custom environment](https://jestjs.io/docs/en/configuration#testenvironment-string). See the [type definitions](https://github.com/facebook/jest/blob/master/packages/jest-circus/src/types.ts) for more information on the events and state data currently available. + +```js +import {NodeEnvironment} from 'jest-environment-node'; +import {Event, State} from 'jest-circus'; + +class MyCustomEnvironment extends NodeEnvironment { + //... + + handleTestEvent(event: Event, state: State) { + if (event.name === 'test_start') { + // ... + } + } +} +``` + +Mutating event or state data is currently unsupported and may cause unexpected behavior or break in a future release without warning. New events, event data, and/or state data will not be considered a breaking change and may be added in any minor release. + ## Installation Install `jest-circus` using yarn: diff --git a/packages/jest-circus/src/__mocks__/testEventHandler.ts b/packages/jest-circus/src/__mocks__/testEventHandler.ts index 1bc877b075f9..6ae7ab7fa2be 100644 --- a/packages/jest-circus/src/__mocks__/testEventHandler.ts +++ b/packages/jest-circus/src/__mocks__/testEventHandler.ts @@ -6,9 +6,9 @@ * of patent rights can be found in the PATENTS file in the same directory. */ -import {EventHandler} from '../types'; +import {Circus} from '@jest/types'; -const testEventHandler: EventHandler = (event, state) => { +const testEventHandler: Circus.EventHandler = (event, state) => { switch (event.name) { case 'start_describe_definition': case 'finish_describe_definition': { diff --git a/packages/jest-circus/src/eventHandler.ts b/packages/jest-circus/src/eventHandler.ts index a29b69828d7b..1accee9c36a0 100644 --- a/packages/jest-circus/src/eventHandler.ts +++ b/packages/jest-circus/src/eventHandler.ts @@ -5,7 +5,8 @@ * LICENSE file in the root directory of this source tree. */ -import {EventHandler, TEST_TIMEOUT_SYMBOL} from './types'; +import {Circus} from '@jest/types'; +import {TEST_TIMEOUT_SYMBOL} from './types'; import { addErrorToEachTestUnderDescribe, @@ -20,7 +21,7 @@ import { restoreGlobalErrorHandlers, } from './globalErrorHandlers'; -const eventHandler: EventHandler = (event, state): void => { +const eventHandler: Circus.EventHandler = (event, state): void => { switch (event.name) { case 'include_test_location_in_result': { state.includeTestLocationInResult = true; diff --git a/packages/jest-circus/src/formatNodeAssertErrors.ts b/packages/jest-circus/src/formatNodeAssertErrors.ts index 9fa8f0031dd2..3ad52ac72117 100644 --- a/packages/jest-circus/src/formatNodeAssertErrors.ts +++ b/packages/jest-circus/src/formatNodeAssertErrors.ts @@ -6,6 +6,7 @@ */ import {AssertionError} from 'assert'; +import {Circus} from '@jest/types'; import { diff, printExpected, @@ -14,7 +15,6 @@ import { } from 'jest-matcher-utils'; import chalk from 'chalk'; import prettyFormat from 'pretty-format'; -import {Event, State, TestError} from './types'; interface AssertionErrorWithStack extends AssertionError { stack: string; @@ -38,10 +38,10 @@ const humanReadableOperators: {[key: string]: string} = { strictEqual: 'to strictly be equal', }; -const formatNodeAssertErrors = (event: Event, state: State) => { +const formatNodeAssertErrors = (event: Circus.Event, state: Circus.State) => { switch (event.name) { case 'test_done': { - event.test.errors = event.test.errors.map((errors: TestError) => { + event.test.errors = event.test.errors.map((errors: Circus.TestError) => { let error; if (Array.isArray(errors)) { const [originalError, asyncError] = errors; diff --git a/packages/jest-circus/src/globalErrorHandlers.ts b/packages/jest-circus/src/globalErrorHandlers.ts index 0bf47f870d70..a48763f8f24c 100644 --- a/packages/jest-circus/src/globalErrorHandlers.ts +++ b/packages/jest-circus/src/globalErrorHandlers.ts @@ -5,8 +5,8 @@ * LICENSE file in the root directory of this source tree. */ +import {Circus} from '@jest/types'; import {dispatch} from './state'; -import {GlobalErrorHandlers} from './types'; const uncaught: NodeJS.UncaughtExceptionListener & NodeJS.UnhandledRejectionListener = (error: unknown) => { @@ -15,7 +15,7 @@ const uncaught: NodeJS.UncaughtExceptionListener & export const injectGlobalErrorHandlers = ( parentProcess: NodeJS.Process, -): GlobalErrorHandlers => { +): Circus.GlobalErrorHandlers => { const uncaughtException = process.listeners('uncaughtException').slice(); const unhandledRejection = process.listeners('unhandledRejection').slice(); parentProcess.removeAllListeners('uncaughtException'); @@ -27,7 +27,7 @@ export const injectGlobalErrorHandlers = ( export const restoreGlobalErrorHandlers = ( parentProcess: NodeJS.Process, - originalErrorHandlers: GlobalErrorHandlers, + originalErrorHandlers: Circus.GlobalErrorHandlers, ) => { parentProcess.removeListener('uncaughtException', uncaught); parentProcess.removeListener('unhandledRejection', uncaught); diff --git a/packages/jest-circus/src/index.ts b/packages/jest-circus/src/index.ts index b7e93f3da0f4..f7dc64781f81 100644 --- a/packages/jest-circus/src/index.ts +++ b/packages/jest-circus/src/index.ts @@ -9,28 +9,21 @@ import chalk from 'chalk'; import {bind as bindEach} from 'jest-each'; import {formatExecError} from 'jest-message-util'; import {ErrorWithStack, isPromise} from 'jest-util'; -import {Global} from '@jest/types'; -import { - BlockFn, - HookFn, - HookType, - TestFn, - BlockMode, - BlockName, - TestName, - TestMode, -} from './types'; +import {Circus, Global} from '@jest/types'; import {dispatch} from './state'; -type THook = (fn: HookFn, timeout?: number) => void; -type DescribeFn = (blockName: BlockName, blockFn: BlockFn) => void; +type THook = (fn: Circus.HookFn, timeout?: number) => void; +type DescribeFn = ( + blockName: Circus.BlockName, + blockFn: Circus.BlockFn, +) => void; const describe = (() => { - const describe = (blockName: BlockName, blockFn: BlockFn) => + const describe = (blockName: Circus.BlockName, blockFn: Circus.BlockFn) => _dispatchDescribe(blockFn, blockName, describe); - const only = (blockName: BlockName, blockFn: BlockFn) => + const only = (blockName: Circus.BlockName, blockFn: Circus.BlockFn) => _dispatchDescribe(blockFn, blockName, only, 'only'); - const skip = (blockName: BlockName, blockFn: BlockFn) => + const skip = (blockName: Circus.BlockName, blockFn: Circus.BlockFn) => _dispatchDescribe(blockFn, blockName, skip, 'skip'); describe.each = bindEach(describe, false); @@ -45,10 +38,10 @@ const describe = (() => { })(); const _dispatchDescribe = ( - blockFn: BlockFn, - blockName: BlockName, + blockFn: Circus.BlockFn, + blockName: Circus.BlockName, describeFn: DescribeFn, - mode?: BlockMode, + mode?: Circus.BlockMode, ) => { const asyncError = new ErrorWithStack(undefined, describeFn); if (blockFn === undefined) { @@ -102,8 +95,8 @@ const _dispatchDescribe = ( }; const _addHook = ( - fn: HookFn, - hookType: HookType, + fn: Circus.HookFn, + hookType: Circus.HookType, hookFn: THook, timeout?: number, ) => { @@ -130,14 +123,23 @@ const afterAll: THook = (fn, timeout) => _addHook(fn, 'afterAll', afterAll, timeout); const test: Global.It = (() => { - const test = (testName: TestName, fn: TestFn, timeout?: number): void => - _addTest(testName, undefined, fn, test, timeout); - const skip = (testName: TestName, fn?: TestFn, timeout?: number): void => - _addTest(testName, 'skip', fn, skip, timeout); - const only = (testName: TestName, fn: TestFn, timeout?: number): void => - _addTest(testName, 'only', fn, test.only, timeout); - - test.todo = (testName: TestName, ...rest: Array): void => { + const test = ( + testName: Circus.TestName, + fn: Circus.TestFn, + timeout?: number, + ): void => _addTest(testName, undefined, fn, test, timeout); + const skip = ( + testName: Circus.TestName, + fn?: Circus.TestFn, + timeout?: number, + ): void => _addTest(testName, 'skip', fn, skip, timeout); + const only = ( + testName: Circus.TestName, + fn: Circus.TestFn, + timeout?: number, + ): void => _addTest(testName, 'only', fn, test.only, timeout); + + test.todo = (testName: Circus.TestName, ...rest: Array): void => { if (rest.length > 0 || typeof testName !== 'string') { throw new ErrorWithStack( 'Todo must be called with only a description.', @@ -148,10 +150,14 @@ const test: Global.It = (() => { }; const _addTest = ( - testName: TestName, - mode: TestMode, - fn: TestFn | undefined, - testFn: (testName: TestName, fn: TestFn, timeout?: number) => void, + testName: Circus.TestName, + mode: Circus.TestMode, + fn: Circus.TestFn | undefined, + testFn: ( + testName: Circus.TestName, + fn: Circus.TestFn, + timeout?: number, + ) => void, timeout?: number, ) => { const asyncError = new ErrorWithStack(undefined, testFn); @@ -195,7 +201,10 @@ const test: Global.It = (() => { const it: Global.It = test; -export = { +export type Event = Circus.Event; +export type State = Circus.State; +export {afterAll, afterEach, beforeAll, beforeEach, describe, it, test}; +export default { afterAll, afterEach, beforeAll, diff --git a/packages/jest-circus/src/legacy-code-todo-rewrite/jestAdapter.ts b/packages/jest-circus/src/legacy-code-todo-rewrite/jestAdapter.ts index d255c9b4661c..6ee6a44c9b39 100644 --- a/packages/jest-circus/src/legacy-code-todo-rewrite/jestAdapter.ts +++ b/packages/jest-circus/src/legacy-code-todo-rewrite/jestAdapter.ts @@ -39,6 +39,7 @@ const jestAdapter = async ( const {globals, snapshotState} = initialize({ config, + environment, getBabelTraverse, getPrettier, globalConfig, diff --git a/packages/jest-circus/src/legacy-code-todo-rewrite/jestAdapterInit.ts b/packages/jest-circus/src/legacy-code-todo-rewrite/jestAdapterInit.ts index 40f93dbf3c0b..434dd63675f1 100644 --- a/packages/jest-circus/src/legacy-code-todo-rewrite/jestAdapterInit.ts +++ b/packages/jest-circus/src/legacy-code-todo-rewrite/jestAdapterInit.ts @@ -5,7 +5,8 @@ * LICENSE file in the root directory of this source tree. */ -import {Config} from '@jest/types'; +import {Circus, Config} from '@jest/types'; +import {JestEnvironment} from '@jest/environment'; import {AssertionResult, Status, TestResult} from '@jest/test-result'; import {extractExpectedAssertionsErrors, getState, setState} from 'expect'; import {formatExecError, formatResultsErrors} from 'jest-message-util'; @@ -19,12 +20,12 @@ import {addEventHandler, dispatch, ROOT_DESCRIBE_BLOCK_NAME} from '../state'; import {getTestID} from '../utils'; import run from '../run'; import globals from '..'; -import {Event, RunResult, TestEntry} from '../types'; type Process = NodeJS.Process; export const initialize = ({ config, + environment, getPrettier, getBabelTraverse, globalConfig, @@ -33,6 +34,7 @@ export const initialize = ({ testPath, }: { config: Config.ProjectConfig; + environment: JestEnvironment; getPrettier: () => null | any; getBabelTraverse: () => Function; globalConfig: Config.GlobalConfig; @@ -83,6 +85,10 @@ export const initialize = ({ addEventHandler(eventHandler); + if (environment.handleTestEvent) { + addEventHandler(environment.handleTestEvent.bind(environment)); + } + dispatch({ name: 'setup', parentProcess, @@ -128,7 +134,7 @@ export const runAndTransformResultsToJestFormat = async ({ globalConfig: Config.GlobalConfig; testPath: string; }): Promise => { - const runResult: RunResult = await run(); + const runResult: Circus.RunResult = await run(); let numFailingTests = 0; let numPassingTests = 0; @@ -227,7 +233,7 @@ export const runAndTransformResultsToJestFormat = async ({ }; }; -const eventHandler = (event: Event) => { +const eventHandler = (event: Circus.Event) => { switch (event.name) { case 'test_start': { setState({currentTestName: getTestID(event.test)}); @@ -241,7 +247,7 @@ const eventHandler = (event: Event) => { } }; -const _addExpectedAssertionErrors = (test: TestEntry) => { +const _addExpectedAssertionErrors = (test: Circus.TestEntry) => { const failures = extractExpectedAssertionsErrors(); const errors = failures.map(failure => failure.error); test.errors = test.errors.concat(errors); @@ -250,7 +256,7 @@ const _addExpectedAssertionErrors = (test: TestEntry) => { // 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. -const _addSuppressedErrors = (test: TestEntry) => { +const _addSuppressedErrors = (test: Circus.TestEntry) => { const {suppressedErrors} = getState(); setState({suppressedErrors: []}); if (suppressedErrors.length) { diff --git a/packages/jest-circus/src/run.ts b/packages/jest-circus/src/run.ts index fcff37cd826a..f289a7501d24 100644 --- a/packages/jest-circus/src/run.ts +++ b/packages/jest-circus/src/run.ts @@ -5,14 +5,8 @@ * LICENSE file in the root directory of this source tree. */ -import { - RunResult, - TestEntry, - TestContext, - Hook, - DescribeBlock, - RETRY_TIMES, -} from './types'; +import {Circus} from '@jest/types'; +import {RETRY_TIMES} from './types'; import {getState, dispatch} from './state'; import { @@ -24,7 +18,7 @@ import { makeRunResult, } from './utils'; -const run = async (): Promise => { +const run = async (): Promise => { const {rootDescribeBlock} = getState(); dispatch({name: 'run_start'}); await _runTestsForDescribeBlock(rootDescribeBlock); @@ -35,7 +29,9 @@ const run = async (): Promise => { ); }; -const _runTestsForDescribeBlock = async (describeBlock: DescribeBlock) => { +const _runTestsForDescribeBlock = async ( + describeBlock: Circus.DescribeBlock, +) => { dispatch({describeBlock, name: 'run_describe_start'}); const {beforeAll, afterAll} = getAllHooksForDescribe(describeBlock); @@ -83,7 +79,7 @@ const _runTestsForDescribeBlock = async (describeBlock: DescribeBlock) => { dispatch({describeBlock, name: 'run_describe_finish'}); }; -const _runTest = async (test: TestEntry): Promise => { +const _runTest = async (test: Circus.TestEntry): Promise => { dispatch({name: 'test_start', test}); const testContext = Object.create(null); const {hasFocusedTests, testNamePattern} = getState(); @@ -132,10 +128,10 @@ const _callCircusHook = ({ describeBlock, testContext, }: { - hook: Hook; - describeBlock?: DescribeBlock; - test?: TestEntry; - testContext?: TestContext; + hook: Circus.Hook; + describeBlock?: Circus.DescribeBlock; + test?: Circus.TestEntry; + testContext?: Circus.TestContext; }): Promise => { dispatch({hook, name: 'hook_start'}); const timeout = hook.timeout || getState().testTimeout; @@ -147,8 +143,8 @@ const _callCircusHook = ({ }; const _callCircusTest = ( - test: TestEntry, - testContext: TestContext, + test: Circus.TestEntry, + testContext: Circus.TestContext, ): Promise => { dispatch({name: 'test_fn_start', test}); const timeout = test.timeout || getState().testTimeout; diff --git a/packages/jest-circus/src/state.ts b/packages/jest-circus/src/state.ts index 8f684d254d16..d42928e90bfa 100644 --- a/packages/jest-circus/src/state.ts +++ b/packages/jest-circus/src/state.ts @@ -5,13 +5,14 @@ * LICENSE file in the root directory of this source tree. */ -import {Event, State, EventHandler, STATE_SYM} from './types'; +import {Circus} from '@jest/types'; +import {STATE_SYM} from './types'; import {makeDescribe} from './utils'; import eventHandler from './eventHandler'; import formatNodeAssertErrors from './formatNodeAssertErrors'; -const eventHandlers: Array = [ +const eventHandlers: Array = [ eventHandler, formatNodeAssertErrors, ]; @@ -19,7 +20,7 @@ const eventHandlers: Array = [ export const ROOT_DESCRIBE_BLOCK_NAME = 'ROOT_DESCRIBE_BLOCK'; const ROOT_DESCRIBE_BLOCK = makeDescribe(ROOT_DESCRIBE_BLOCK_NAME); -const INITIAL_STATE: State = { +const INITIAL_STATE: Circus.State = { currentDescribeBlock: ROOT_DESCRIBE_BLOCK, currentlyRunningTest: null, expand: undefined, @@ -34,15 +35,16 @@ const INITIAL_STATE: State = { global[STATE_SYM] = INITIAL_STATE; -export const getState = (): State => global[STATE_SYM]; -export const setState = (state: State): State => (global[STATE_SYM] = state); +export const getState = (): Circus.State => global[STATE_SYM]; +export const setState = (state: Circus.State): Circus.State => + (global[STATE_SYM] = state); -export const dispatch = (event: Event): void => { +export const dispatch = (event: Circus.Event): void => { for (const handler of eventHandlers) { handler(event, getState()); } }; -export const addEventHandler = (handler: EventHandler): void => { +export const addEventHandler = (handler: Circus.EventHandler): void => { eventHandlers.push(handler); }; diff --git a/packages/jest-circus/src/types.ts b/packages/jest-circus/src/types.ts index 87bf25bce902..623dcd3c6ed9 100644 --- a/packages/jest-circus/src/types.ts +++ b/packages/jest-circus/src/types.ts @@ -8,217 +8,8 @@ // Used as type // eslint-disable-next-line @typescript-eslint/no-unused-vars import expect from 'expect'; -import {Global} from '@jest/types'; - -type Process = NodeJS.Process; - -export type DoneFn = Global.DoneFn; -export type BlockFn = Global.BlockFn; -export type BlockName = Global.BlockName; -export type BlockMode = void | 'skip' | 'only' | 'todo'; -export type TestMode = BlockMode; -export type TestName = Global.TestName; -export type TestFn = Global.TestFn; -export type HookFn = (done?: DoneFn) => Promise | null | undefined; -export type AsyncFn = TestFn | HookFn; -export type SharedHookType = 'afterAll' | 'beforeAll'; -export type HookType = SharedHookType | 'afterEach' | 'beforeEach'; -export type TestContext = Record; -export type Exception = any; // Since in JS anything can be thrown as an error. -export type FormattedError = string; // String representation of error. -export type Hook = { - asyncError: Error; - fn: HookFn; - type: HookType; - parent: DescribeBlock; - timeout: number | undefined | null; -}; - -export type EventHandler = (event: Event, state: State) => void; - -export type Event = - | { - name: 'include_test_location_in_result'; - } - | { - asyncError: Error; - mode: BlockMode; - name: 'start_describe_definition'; - blockName: BlockName; - } - | { - mode: BlockMode; - name: 'finish_describe_definition'; - blockName: BlockName; - } - | { - asyncError: Error; - name: 'add_hook'; - hookType: HookType; - fn: HookFn; - timeout: number | undefined; - } - | { - asyncError: Error; - name: 'add_test'; - testName: TestName; - fn?: TestFn; - mode?: TestMode; - timeout: number | undefined; - } - | { - name: 'hook_start'; - hook: Hook; - } - | { - name: 'hook_success'; - describeBlock: DescribeBlock | undefined | null; - test: TestEntry | undefined | null; - hook: Hook; - } - | { - name: 'hook_failure'; - error: string | Exception; - describeBlock: DescribeBlock | undefined | null; - test: TestEntry | undefined | null; - hook: Hook; - } - | { - name: 'test_fn_start'; - test: TestEntry; - } - | { - name: 'test_fn_success'; - test: TestEntry; - } - | { - name: 'test_fn_failure'; - error: Exception; - test: TestEntry; - } - | { - name: 'test_retry'; - test: TestEntry; - } - | { - // the `test` in this case is all hooks + it/test function, not just the - // function passed to `it/test` - name: 'test_start'; - test: TestEntry; - } - | { - name: 'test_skip'; - test: TestEntry; - } - | { - name: 'test_todo'; - test: TestEntry; - } - | { - // test failure is defined by presence of errors in `test.errors`, - // `test_done` indicates that the test and all its hooks were run, - // and nothing else will change it's state in the future. (except third - // party extentions/plugins) - name: 'test_done'; - test: TestEntry; - } - | { - name: 'run_describe_start'; - describeBlock: DescribeBlock; - } - | { - name: 'run_describe_finish'; - describeBlock: DescribeBlock; - } - | { - name: 'run_start'; - } - | { - name: 'run_finish'; - } - | { - // Any unhandled error that happened outside of test/hooks (unless it is - // an `afterAll` hook) - name: 'error'; - error: Exception; - } - | { - // first action to dispatch. Good time to initialize all settings - name: 'setup'; - testNamePattern?: string; - parentProcess: Process; - } - | { - // Action dispatched after everything is finished and we're about to wrap - // things up and return test results to the parent process (caller). - name: 'teardown'; - }; - -export type TestStatus = 'skip' | 'done' | 'todo'; -export type TestResult = { - duration: number | null | undefined; - errors: Array; - invocations: number; - status: TestStatus; - location: {column: number; line: number} | null | undefined; - testPath: Array; -}; - -export type RunResult = { - unhandledErrors: Array; - testResults: TestResults; -}; - -export type TestResults = Array; - -export type GlobalErrorHandlers = { - uncaughtException: Array<(exception: Exception) => void>; - unhandledRejection: Array< - (exception: Exception, promise: Promise) => void - >; -}; - -export type State = { - currentDescribeBlock: DescribeBlock; - currentlyRunningTest: TestEntry | undefined | null; // including when hooks are being executed - expand?: boolean; // expand error messages - hasFocusedTests: boolean; // that are defined using test.only - // Store process error handlers. During the run we inject our own - // handlers (so we could fail tests on unhandled errors) and later restore - // the original ones. - originalGlobalErrorHandlers?: GlobalErrorHandlers; - parentProcess: Process | null; // process object from the outer scope - rootDescribeBlock: DescribeBlock; - testNamePattern: RegExp | undefined | null; - testTimeout: number; - unhandledErrors: Array; - includeTestLocationInResult: boolean; -}; - -export type DescribeBlock = { - children: Array; - hooks: Array; - mode: BlockMode; - name: BlockName; - parent: DescribeBlock | undefined | null; - tests: Array; -}; - -export type TestError = Exception | Array<[Exception | undefined, Exception]>; // the error from the test, as well as a backup error for async - -export type TestEntry = { - asyncError: Exception; // Used if the test failure contains no usable stack trace - errors: TestError; - fn: TestFn | undefined | null; - invocations: number; - mode: TestMode; - name: TestName; - parent: DescribeBlock; - startedAt: number | undefined | null; - duration: number | undefined | null; - status: TestStatus | undefined | null; // whether the test has been skipped or run already - timeout: number | undefined | null; -}; +// eslint-disable-next-line @typescript-eslint/no-unused-vars +import {Circus} from '@jest/types'; export const STATE_SYM = (Symbol( 'JEST_STATE_SYMBOL', @@ -234,7 +25,7 @@ export const TEST_TIMEOUT_SYMBOL = (Symbol.for( declare global { module NodeJS { interface Global { - STATE_SYM_SYMBOL: State; + STATE_SYM_SYMBOL: Circus.State; RETRY_TIMES_SYMBOL: string; TEST_TIMEOUT_SYMBOL: number; expect: typeof expect; diff --git a/packages/jest-circus/src/utils.ts b/packages/jest-circus/src/utils.ts index fbad7a725c77..433f0c59ab77 100644 --- a/packages/jest-circus/src/utils.ts +++ b/packages/jest-circus/src/utils.ts @@ -5,38 +5,21 @@ * LICENSE file in the root directory of this source tree. */ +import {Circus} from '@jest/types'; import {convertDescriptorToString} from 'jest-util'; import isGeneratorFn from 'is-generator-fn'; import co from 'co'; - import StackUtils from 'stack-utils'; - import prettyFormat from 'pretty-format'; - import {getState} from './state'; -import { - AsyncFn, - BlockMode, - BlockName, - DescribeBlock, - Exception, - Hook, - RunResult, - TestEntry, - TestContext, - TestFn, - TestMode, - TestName, - TestResults, -} from './types'; const stackUtils = new StackUtils({cwd: 'A path that does not exist'}); export const makeDescribe = ( - name: BlockName, - parent?: DescribeBlock, - mode?: BlockMode, -): DescribeBlock => { + name: Circus.BlockName, + parent?: Circus.DescribeBlock, + mode?: Circus.BlockMode, +): Circus.DescribeBlock => { let _mode = mode; if (parent && !mode) { // If not set explicitly, inherit from the parent describe. @@ -54,13 +37,13 @@ export const makeDescribe = ( }; export const makeTest = ( - fn: TestFn | undefined, - mode: TestMode, - name: TestName, - parent: DescribeBlock, + fn: Circus.TestFn | undefined, + mode: Circus.TestMode, + name: Circus.TestName, + parent: Circus.DescribeBlock, timeout: number | undefined, - asyncError: Exception, -): TestEntry => ({ + asyncError: Circus.Exception, +): Circus.TestEntry => ({ asyncError, duration: null, errors: [], @@ -76,7 +59,7 @@ export const makeTest = ( // Traverse the tree of describe blocks and return true if at least one describe // block has an enabled test. -const hasEnabledTest = (describeBlock: DescribeBlock): boolean => { +const hasEnabledTest = (describeBlock: Circus.DescribeBlock): boolean => { const {hasFocusedTests, testNamePattern} = getState(); const hasOwnEnabledTests = describeBlock.tests.some( test => @@ -90,10 +73,10 @@ const hasEnabledTest = (describeBlock: DescribeBlock): boolean => { return hasOwnEnabledTests || describeBlock.children.some(hasEnabledTest); }; -export const getAllHooksForDescribe = (describe: DescribeBlock) => { +export const getAllHooksForDescribe = (describe: Circus.DescribeBlock) => { const result: { - beforeAll: Array; - afterAll: Array; + beforeAll: Array; + afterAll: Array; } = { afterAll: [], beforeAll: [], @@ -115,12 +98,12 @@ export const getAllHooksForDescribe = (describe: DescribeBlock) => { return result; }; -export const getEachHooksForTest = (test: TestEntry) => { +export const getEachHooksForTest = (test: Circus.TestEntry) => { const result: { - beforeEach: Array; - afterEach: Array; + beforeEach: Array; + afterEach: Array; } = {afterEach: [], beforeEach: []}; - let block: DescribeBlock | undefined | null = test.parent; + let block: Circus.DescribeBlock | undefined | null = test.parent; do { const beforeEachForCurrentBlock = []; @@ -141,7 +124,9 @@ export const getEachHooksForTest = (test: TestEntry) => { return result; }; -export const describeBlockHasTests = (describe: DescribeBlock): boolean => +export const describeBlockHasTests = ( + describe: Circus.DescribeBlock, +): boolean => describe.tests.length > 0 || describe.children.some(describeBlockHasTests); const _makeTimeoutMessage = (timeout: number, isHook: boolean) => @@ -158,8 +143,8 @@ function checkIsError(error: any): error is Error { } export const callAsyncCircusFn = ( - fn: AsyncFn, - testContext: TestContext | undefined, + fn: Circus.AsyncFn, + testContext: Circus.TestContext | undefined, {isHook, timeout}: {isHook?: boolean | null; timeout: number}, ): Promise => { let timeoutID: NodeJS.Timeout; @@ -245,25 +230,27 @@ export const callAsyncCircusFn = ( }); }; -export const getTestDuration = (test: TestEntry): number | null => { +export const getTestDuration = (test: Circus.TestEntry): number | null => { const {startedAt} = test; return typeof startedAt === 'number' ? Date.now() - startedAt : null; }; export const makeRunResult = ( - describeBlock: DescribeBlock, + describeBlock: Circus.DescribeBlock, unhandledErrors: Array, -): RunResult => ({ +): Circus.RunResult => ({ testResults: makeTestResults(describeBlock), unhandledErrors: unhandledErrors.map(_formatError), }); -const makeTestResults = (describeBlock: DescribeBlock): TestResults => { +const makeTestResults = ( + describeBlock: Circus.DescribeBlock, +): Circus.TestResults => { const {includeTestLocationInResult} = getState(); - let testResults: TestResults = []; + let testResults: Circus.TestResults = []; for (const test of describeBlock.tests) { const testPath = []; - let parent: TestEntry | DescribeBlock = test; + let parent: Circus.TestEntry | Circus.DescribeBlock = test; do { testPath.unshift(parent.name); } while ((parent = parent.parent)); @@ -309,9 +296,9 @@ const makeTestResults = (describeBlock: DescribeBlock): TestResults => { // Return a string that identifies the test (concat of parent describe block // names + test title) -export const getTestID = (test: TestEntry) => { +export const getTestID = (test: Circus.TestEntry) => { const titles = []; - let parent: TestEntry | DescribeBlock = test; + let parent: Circus.TestEntry | Circus.DescribeBlock = test; do { titles.unshift(parent.name); } while ((parent = parent.parent)); @@ -321,7 +308,7 @@ export const getTestID = (test: TestEntry) => { }; const _formatError = ( - errors?: Exception | [Exception | undefined, Exception], + errors?: Circus.Exception | [Circus.Exception | undefined, Circus.Exception], ): string => { let error; let asyncError; @@ -349,9 +336,9 @@ const _formatError = ( }; export const addErrorToEachTestUnderDescribe = ( - describeBlock: DescribeBlock, - error: Exception, - asyncError: Exception, + describeBlock: Circus.DescribeBlock, + error: Circus.Exception, + asyncError: Circus.Exception, ) => { for (const test of describeBlock.tests) { test.errors.push([error, asyncError]); diff --git a/packages/jest-each/src/__tests__/array.test.ts b/packages/jest-each/src/__tests__/array.test.ts index ceeffe197a92..64bced0cee50 100644 --- a/packages/jest-each/src/__tests__/array.test.ts +++ b/packages/jest-each/src/__tests__/array.test.ts @@ -385,6 +385,29 @@ describe('jest-each', () => { undefined, ); }); + + test('calls global with title with placeholder values correctly interpolated', () => { + const globalTestMocks = getGlobalTestMocks(); + const eachObject = each.withGlobal(globalTestMocks)([ + ['hello', '%d', 10, '%s', {foo: 'bar'}], + ['world', '%i', 1991, '%p', {foo: 'bar'}], + ]); + const testFunction = get(eachObject, keyPath); + testFunction('expected string: %s %s %d %s %p', () => {}); + + const globalMock = get(globalTestMocks, keyPath); + expect(globalMock).toHaveBeenCalledTimes(2); + expect(globalMock).toHaveBeenCalledWith( + 'expected string: hello %d 10 %s {"foo": "bar"}', + expectFunction, + undefined, + ); + expect(globalMock).toHaveBeenCalledWith( + 'expected string: world %i 1991 %p {"foo": "bar"}', + expectFunction, + undefined, + ); + }); }); }); }); diff --git a/packages/jest-each/src/table/array.ts b/packages/jest-each/src/table/array.ts index de9be8c0c57b..292a9e5e26a0 100644 --- a/packages/jest-each/src/table/array.ts +++ b/packages/jest-each/src/table/array.ts @@ -15,6 +15,8 @@ import {EachTests} from '../bind'; const SUPPORTED_PLACEHOLDERS = /%[sdifjoOp%]/g; const PRETTY_PLACEHOLDER = '%p'; const INDEX_PLACEHOLDER = '%#'; +const PLACEHOLDER_PREFIX = '%'; +const JEST_EACH_PLACEHOLDER_ESCAPE = '@@__JEST_EACH_PLACEHOLDER_ESCAPE__@@'; export default (title: string, arrayTable: Global.ArrayTable): EachTests => normaliseTable(arrayTable).map((row, index) => ({ @@ -35,15 +37,23 @@ const formatTitle = ( row: Global.Row, rowIndex: number, ): string => - row.reduce((formattedTitle, value) => { - const [placeholder] = getMatchingPlaceholders(formattedTitle); - if (!placeholder) return formattedTitle; + row + .reduce((formattedTitle, value) => { + const [placeholder] = getMatchingPlaceholders(formattedTitle); + const normalisedValue = normalisePlaceholderValue(value); + if (!placeholder) return formattedTitle; - if (placeholder === PRETTY_PLACEHOLDER) - return interpolatePrettyPlaceholder(formattedTitle, value); + if (placeholder === PRETTY_PLACEHOLDER) + return interpolatePrettyPlaceholder(formattedTitle, normalisedValue); - return util.format(formattedTitle, value); - }, interpolateTitleIndex(title, rowIndex)); + return util.format(formattedTitle, normalisedValue); + }, interpolateTitleIndex(title, rowIndex)) + .replace(new RegExp(JEST_EACH_PLACEHOLDER_ESCAPE, 'g'), PLACEHOLDER_PREFIX); + +const normalisePlaceholderValue = (value: unknown) => + typeof value === 'string' && SUPPORTED_PLACEHOLDERS.test(value) + ? value.replace(PLACEHOLDER_PREFIX, JEST_EACH_PLACEHOLDER_ESCAPE) + : value; const getMatchingPlaceholders = (title: string) => title.match(SUPPORTED_PLACEHOLDERS) || []; diff --git a/packages/jest-environment-jsdom/src/index.ts b/packages/jest-environment-jsdom/src/index.ts index 8d0c33c1df7c..5e362f9244f6 100644 --- a/packages/jest-environment-jsdom/src/index.ts +++ b/packages/jest-environment-jsdom/src/index.ts @@ -15,24 +15,17 @@ import {JSDOM, VirtualConsole} from 'jsdom'; // The `Window` interface does not have an `Error.stackTraceLimit` property, but // `JSDOMEnvironment` assumes it is there. -interface Win extends Window { - Error: { - stackTraceLimit: number; +type Win = Window & + Global.Global & { + Error: { + stackTraceLimit: number; + }; }; -} - -function isWin(globals: Win | Global.Global): globals is Win { - return (globals as Win).document !== undefined; -} - -// A lot of the globals expected by other APIs are `NodeJS.Global` and not -// `Window`, so we need to cast here and there class JSDOMEnvironment implements JestEnvironment { dom: JSDOM | null; fakeTimers: FakeTimers | null; - // @ts-ignore - global: Global.Global | Win | null; + global: Win; errorEventListener: ((event: Event & {error: Error}) => void) | null; moduleMocker: ModuleMocker | null; @@ -109,16 +102,15 @@ class JSDOMEnvironment implements JestEnvironment { this.fakeTimers.dispose(); } if (this.global) { - if (this.errorEventListener && isWin(this.global)) { + if (this.errorEventListener) { this.global.removeEventListener('error', this.errorEventListener); } // Dispose "document" to prevent "load" event from triggering. Object.defineProperty(this.global, 'document', {value: null}); - if (isWin(this.global)) { - this.global.close(); - } + this.global.close(); } this.errorEventListener = null; + // @ts-ignore this.global = null; this.dom = null; this.fakeTimers = null; diff --git a/packages/jest-environment/src/index.ts b/packages/jest-environment/src/index.ts index 00fb37b8fab8..b49313c47d76 100644 --- a/packages/jest-environment/src/index.ts +++ b/packages/jest-environment/src/index.ts @@ -6,7 +6,7 @@ */ import {Script} from 'vm'; -import {Config, Global} from '@jest/types'; +import {Circus, Config, Global} from '@jest/types'; import jestMock, {ModuleMocker} from 'jest-mock'; import {ScriptTransformer} from '@jest/transform'; import {JestFakeTimers as FakeTimers} from '@jest/fake-timers'; @@ -35,6 +35,7 @@ export declare class JestEnvironment { ): {[ScriptTransformer.EVAL_RESULT_VARIABLE]: ModuleWrapper} | null; setup(): Promise; teardown(): Promise; + handleTestEvent?(event: Circus.Event, state: Circus.State): void; } export type Module = typeof module; diff --git a/packages/jest-jasmine2/src/jasmine/Env.ts b/packages/jest-jasmine2/src/jasmine/Env.ts index 14d3ea9587ee..494096d9882c 100644 --- a/packages/jest-jasmine2/src/jasmine/Env.ts +++ b/packages/jest-jasmine2/src/jasmine/Env.ts @@ -416,8 +416,8 @@ export default function(j$: Jasmine) { parentSuite.addChild(suite); currentDeclarationSuite = suite; - let declarationError: null | Error = null; - let describeReturnValue: null | Error = null; + let declarationError: undefined | Error = undefined; + let describeReturnValue: undefined | Error = undefined; try { describeReturnValue = specDefinitions.call(suite); } catch (e) { diff --git a/packages/jest-phabricator/README.md b/packages/jest-phabricator/README.md index 7ede53019c97..97a3fdf505be 100644 --- a/packages/jest-phabricator/README.md +++ b/packages/jest-phabricator/README.md @@ -4,7 +4,7 @@ This Repo contains the testResultsProcessor needed to create the coverage map ne ## How to use it -In `example/JestUnitTestEngine` you'll find an example of a Phabricator Jest UnitTestEngine reference implementation. +Below you'll find an example of a Phabricator Jest UnitTestEngine reference implementation. You need to add the jest unit engine to your .arcconfig: @@ -22,3 +22,188 @@ In `JestUnitTestEngine` there are a couple of constants you probably need to mod - `JEST_PATH` is the path to Jest If you need to pass to Jest a custom configuration you can either use `JEST_PATH` and point it to a bash/script file that will just jest with `--config=path/to/config` or alternatively you can add the config option in the `getJestOptions` php function. + +## Reference implementation + +```php +class JestUnitTestEngine extends ArcanistBaseUnitTestEngine { + const PROCESSOR = 'jest/packages/jest-phabricator/build/index.js'; + const JEST_PATH = 'jest/packages/jest/bin/jest.js'; + const TOO_MANY_FILES_TO_COVER = 100; + const GIGANTIC_DIFF_THRESHOLD = 200; + + private function getRoot() { + return $this->getWorkingCopy()->getProjectRoot(); + } + + private function getOutputJSON() { + return $this->getRoot() . '/output.json'; + } + + private function getFutureResults($future) { + list($stdout, $stderr) = $future->resolvex(); + $output_JSON = $this->getOutputJSON(); + $report_path_exists = file_exists($output_JSON); + $raw_results = null; + + if ($report_path_exists) { + $raw_results = json_decode( + Filesystem::readFile($output_JSON), + true + )['coverageMap']; + Filesystem::remove($output_JSON); + } else { + $raw_results = json_decode($stdout, true); + } + + if (!is_array($raw_results)) { + throw new Exception("Unit test script emitted invalid JSON: {$stdout}"); + } + + $results = array(); + foreach ($raw_results as $result) { + $test_result = new ArcanistUnitTestResult(); + $test_result->setName($result['name']); + $succeed = isset($result['status']) && $result['status'] == 'passed'; + $test_result->setResult( + $succeed ? + ArcanistUnitTestResult::RESULT_PASS : + ArcanistUnitTestResult::RESULT_FAIL + ); + + if (isset($result['coverage'])) { + $coverage = array(); + $root = $this->getRoot() . '/'; + foreach ($result['coverage'] as $file_path => $coverage_data) { + if (substr($file_path, 0, strlen($root)) == $root) { + $file_path = substr($file_path, strlen($root)); + } + $coverage[$file_path] = $coverage_data; + } + $test_result->setCoverage($coverage); + } + $test_result->setUserData($result['message']); + $results[] = $test_result; + } + + return $results; + } + + private function runCommands($commands) { + $futures = array(); + foreach ($commands as $command) { + $bin = $command['bin']; + $options = implode(' ', $command['options']); + $paths = $command['paths']; + $futures[] = new ExecFuture("{$bin} {$options} %Ls", $paths); + } + + $console = PhutilConsole::getConsole(); + + // Pass stderr through so we can give the user updates on test + // status as tests run. + $completed = array(); + $iterator = new FutureIterator($futures); + foreach ($iterator->setUpdateInterval(0.2) as $_) { + foreach ($futures as $key => $future) { + if (isset($completed[$key])) { + continue; + } + if ($future->isReady()) { + $completed[$key] = true; + } + list(, $stderr) = $future->read(); + $console->writeErr('%s', $stderr); + break; + } + } + // Finish printing output for remaining futures + foreach ($futures as $key => $future) { + if (!isset($completed[$key])) { + list(, $stderr) = $future->read(); + $console->writeErr('%s', $stderr); + } + } + $results = array(); + foreach ($futures as $future) { + $results[] = $this->getFutureResults($future); + } + + return call_user_func_array('array_merge', $results); + } + + private function runJSTests() { + $console = PhutilConsole::getConsole(); + $root = $this->getRoot(); + + $result_arrays = []; + $paths = $this->getPaths(); + $jest_paths = array(); + foreach ($paths as $path) { + $ext = idx(pathinfo($path), 'extension'); + if ($ext === 'js' || $ext === 'json') { + // Filter deleted modules because Jest can't do anything with them. + if (file_exists("$root/$path")) { + $jest_paths[] = "$root/$path"; + } + } + } + + $commands = []; + if (count($jest_paths) > self::GIGANTIC_DIFF_THRESHOLD) { + $console->writeOut("Too many files, skipping JavaScript tests.\n"); + $result_arrays[] = array(); + } else { + if (count($jest_paths) > 0) { + $console->writeOut("Running JavaScript tests.\n"); + $commands[] = array( + 'bin' => self::JEST_PATH, + 'options' => $this->getJestOptions($jest_paths), + 'paths' => $jest_paths, + ); + } + + try { + $result_arrays[] = $this->runCommands($commands); + } catch (Exception $e) { + // Ignore the exception in case of failing tests + // As Jest should have already printed the results. + $result = new ArcanistUnitTestResult(); + $result->setName('JavaScript tests'); + $result->setResult(ArcanistUnitTestResult::RESULT_FAIL); + $result->setDuration(0); + $result_arrays[] = array($result); + } + } + + $console->writeOut("Finished tests.\n"); + return call_user_func_array('array_merge', $result_arrays); + } + + private function getJestOptions($paths) { + $output_JSON = $this->getOutputJSON(); + $options = array( + '--colors', + '--findRelatedTests', + '--json', + '--outputFile=' . $output_JSON, + '--testResultsProcessor=' . self::PROCESSOR + ); + + // Checks for the number of files to cover, in case it's too big skips coverage + // A better solution would involve knowing what's the machine buffer size limit + // for exec and check if the command can stay within it. + if (count($paths) < self::TOO_MANY_FILES_TO_COVER) { + $options[] = '--coverage'; + $options[] = '--collectCoverageOnlyFrom '. join(' ', $paths); + } + + return $options; + } + + /** @Override */ + public function run() { + return self::runJSTests(); + } +} +``` diff --git a/packages/jest-phabricator/example/JestUnitTestEngine.php b/packages/jest-phabricator/example/JestUnitTestEngine.php deleted file mode 100644 index 0e308e97d285..000000000000 --- a/packages/jest-phabricator/example/JestUnitTestEngine.php +++ /dev/null @@ -1,186 +0,0 @@ -getWorkingCopy()->getProjectRoot(); - } - - private function getOutputJSON() { - return $this->getRoot() . '/output.json'; - } - - private function getFutureResults($future) { - list($stdout, $stderr) = $future->resolvex(); - $output_JSON = $this->getOutputJSON(); - $report_path_exists = file_exists($output_JSON); - $raw_results = null; - - if ($report_path_exists) { - $raw_results = json_decode( - Filesystem::readFile($output_JSON), - true - )['coverageMap']; - Filesystem::remove($output_JSON); - } else { - $raw_results = json_decode($stdout, true); - } - - if (!is_array($raw_results)) { - throw new Exception("Unit test script emitted invalid JSON: {$stdout}"); - } - - $results = array(); - foreach ($raw_results as $result) { - $test_result = new ArcanistUnitTestResult(); - $test_result->setName($result['name']); - $succeed = isset($result['status']) && $result['status'] == 'passed'; - $test_result->setResult( - $succeed ? - ArcanistUnitTestResult::RESULT_PASS : - ArcanistUnitTestResult::RESULT_FAIL - ); - - if (isset($result['coverage'])) { - $coverage = array(); - $root = $this->getRoot() . '/'; - foreach ($result['coverage'] as $file_path => $coverage_data) { - if (substr($file_path, 0, strlen($root)) == $root) { - $file_path = substr($file_path, strlen($root)); - } - $coverage[$file_path] = $coverage_data; - } - $test_result->setCoverage($coverage); - } - $test_result->setUserData($result['message']); - $results[] = $test_result; - } - - return $results; - } - - private function runCommands($commands) { - $futures = array(); - foreach ($commands as $command) { - $bin = $command['bin']; - $options = implode(' ', $command['options']); - $paths = $command['paths']; - $futures[] = new ExecFuture("{$bin} {$options} %Ls", $paths); - } - - $console = PhutilConsole::getConsole(); - - // Pass stderr through so we can give the user updates on test - // status as tests run. - $completed = array(); - $iterator = new FutureIterator($futures); - foreach ($iterator->setUpdateInterval(0.2) as $_) { - foreach ($futures as $key => $future) { - if (isset($completed[$key])) { - continue; - } - if ($future->isReady()) { - $completed[$key] = true; - } - list(, $stderr) = $future->read(); - $console->writeErr('%s', $stderr); - break; - } - } - // Finish printing output for remaining futures - foreach ($futures as $key => $future) { - if (!isset($completed[$key])) { - list(, $stderr) = $future->read(); - $console->writeErr('%s', $stderr); - } - } - $results = array(); - foreach ($futures as $future) { - $results[] = $this->getFutureResults($future); - } - - return call_user_func_array('array_merge', $results); - } - - private function runJSTests() { - $console = PhutilConsole::getConsole(); - $root = $this->getRoot(); - - $result_arrays = []; - $paths = $this->getPaths(); - $jest_paths = array(); - foreach ($paths as $path) { - $ext = idx(pathinfo($path), 'extension'); - if ($ext === 'js' || $ext === 'json') { - // Filter deleted modules because Jest can't do anything with them. - if (file_exists("$root/$path")) { - $jest_paths[] = "$root/$path"; - } - } - } - - $commands = []; - if (count($jest_paths) > self::GIGANTIC_DIFF_THRESHOLD) { - $console->writeOut("Too many files, skipping JavaScript tests.\n"); - $result_arrays[] = array(); - } else { - if (count($jest_paths) > 0) { - $console->writeOut("Running JavaScript tests.\n"); - $commands[] = array( - 'bin' => self::JEST_PATH, - 'options' => $this->getJestOptions($jest_paths), - 'paths' => $jest_paths, - ); - } - - try { - $result_arrays[] = $this->runCommands($commands); - } catch (Exception $e) { - // Ignore the exception in case of failing tests - // As Jest should have already printed the results. - $result = new ArcanistUnitTestResult(); - $result->setName('JavaScript tests'); - $result->setResult(ArcanistUnitTestResult::RESULT_FAIL); - $result->setDuration(0); - $result_arrays[] = array($result); - } - } - - $console->writeOut("Finished tests.\n"); - return call_user_func_array('array_merge', $result_arrays); - } - - private function getJestOptions($paths) { - $output_JSON = $this->getOutputJSON(); - $options = array( - '--colors', - '--findRelatedTests', - '--json', - '--outputFile=' . $output_JSON, - '--testResultsProcessor=' . self::PROCESSOR - ); - - // Checks for the number of files to cover, in case it's too big skips coverage - // A better solution would involve knowing what's the machine buffer size limit - // for exec and check if the command can stay within it. - if (count($paths) < self::TOO_MANY_FILES_TO_COVER) { - $options[] = '--coverage'; - $options[] = '--collectCoverageOnlyFrom '. join(' ', $paths); - } - - return $options; - } - - /** @Override */ - public function run() { - return self::runJSTests(); - } -} diff --git a/packages/jest-reporters/istanbul-api.d.ts b/packages/jest-reporters/istanbul-api.d.ts deleted file mode 100644 index d39fef7f7ee6..000000000000 --- a/packages/jest-reporters/istanbul-api.d.ts +++ /dev/null @@ -1,21 +0,0 @@ -/** - * 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. - */ - -declare module 'istanbul-api' { - class Reporter { - constructor(config?: object, options?: object); - add(format: string): void; - addAll(formats: Array): void; - write(coverageMap: object, options: object): void; - config: object; - dir: string; - reports: object; - summarizer: string; - } - - function createReporter(config?: object, options?: object): Reporter; -} diff --git a/packages/jest-reporters/package.json b/packages/jest-reporters/package.json index 19512b63dbc4..81147bb6f961 100644 --- a/packages/jest-reporters/package.json +++ b/packages/jest-reporters/package.json @@ -12,10 +12,11 @@ "chalk": "^2.0.1", "exit": "^0.1.2", "glob": "^7.1.2", - "istanbul-api": "^2.1.1", "istanbul-lib-coverage": "^2.0.2", "istanbul-lib-instrument": "^3.0.1", + "istanbul-lib-report": "^2.0.4", "istanbul-lib-source-maps": "^3.0.1", + "istanbul-reports": "^2.1.1", "jest-haste-map": "^24.7.1", "jest-resolve": "^24.7.1", "jest-runtime": "^24.7.1", @@ -31,7 +32,9 @@ "@types/glob": "^7.1.1", "@types/istanbul-lib-coverage": "^2.0.0", "@types/istanbul-lib-instrument": "^1.7.2", + "@types/istanbul-lib-report": "^1.1.0", "@types/istanbul-lib-source-maps": "^1.2.1", + "@types/istanbul-reports": "^1.1.0", "@types/node-notifier": "^5.4.0", "@types/slash": "^2.0.0", "@types/string-length": "^2.0.0", diff --git a/packages/jest-reporters/src/__tests__/coverage_reporter.test.js b/packages/jest-reporters/src/__tests__/coverage_reporter.test.js index 1cebec0afdfd..b7936e051c3b 100644 --- a/packages/jest-reporters/src/__tests__/coverage_reporter.test.js +++ b/packages/jest-reporters/src/__tests__/coverage_reporter.test.js @@ -5,23 +5,22 @@ * LICENSE file in the root directory of this source tree. */ -jest.mock('istanbul-lib-source-maps').mock('istanbul-api'); +jest + .mock('istanbul-lib-source-maps') + .mock('istanbul-lib-report', () => ({ + createContext: jest.fn(), + summarizers: {pkg: jest.fn(() => ({visit: jest.fn()}))}, + })) + .mock('istanbul-reports'); let libCoverage; let libSourceMaps; let CoverageReporter; -let istanbulApi; import path from 'path'; import mock from 'mock-fs'; beforeEach(() => { - istanbulApi = require('istanbul-api'); - istanbulApi.createReporter = jest.fn(() => ({ - addAll: jest.fn(), - write: jest.fn(), - })); - CoverageReporter = require('../coverage_reporter').default; libCoverage = require('istanbul-lib-coverage'); libSourceMaps = require('istanbul-lib-source-maps'); diff --git a/packages/jest-reporters/src/__tests__/summary_reporter.test.js b/packages/jest-reporters/src/__tests__/summary_reporter.test.js index be5144991d25..b4b56758a6c7 100644 --- a/packages/jest-reporters/src/__tests__/summary_reporter.test.js +++ b/packages/jest-reporters/src/__tests__/summary_reporter.test.js @@ -6,7 +6,7 @@ */ 'use strict'; -import SummaryReporter from '../summary_reporter'; +let SummaryReporter; const env = {...process.env}; const now = Date.now; @@ -18,6 +18,12 @@ const globalConfig = { let results = []; +function requireReporter() { + jest.isolateModules(() => { + SummaryReporter = require('../summary_reporter').default; + }); +} + beforeEach(() => { process.env.npm_lifecycle_event = 'test'; process.env.npm_lifecycle_script = 'jest'; @@ -50,6 +56,7 @@ test('snapshots needs update with npm test', () => { }; process.env.npm_config_user_agent = 'npm'; + requireReporter(); const testReporter = new SummaryReporter(globalConfig); testReporter.onRunComplete(new Set(), aggregatedResults); expect(results.join('')).toMatchSnapshot(); @@ -73,6 +80,7 @@ test('snapshots needs update with yarn test', () => { }; process.env.npm_config_user_agent = 'yarn'; + requireReporter(); const testReporter = new SummaryReporter(globalConfig); testReporter.onRunComplete(new Set(), aggregatedResults); expect(results.join('')).toMatchSnapshot(); @@ -108,6 +116,7 @@ test('snapshots all have results (no update)', () => { testResults: {}, }; + requireReporter(); const testReporter = new SummaryReporter(globalConfig); testReporter.onRunComplete(new Set(), aggregatedResults); expect(results.join('').replace(/\\/g, '/')).toMatchSnapshot(); @@ -143,6 +152,7 @@ test('snapshots all have results (after update)', () => { testResults: {}, }; + requireReporter(); const testReporter = new SummaryReporter(globalConfig); testReporter.onRunComplete(new Set(), aggregatedResults); expect(results.join('').replace(/\\/g, '/')).toMatchSnapshot(); diff --git a/packages/jest-reporters/src/coverage_reporter.ts b/packages/jest-reporters/src/coverage_reporter.ts index e53f9fff894e..584ce7121a67 100644 --- a/packages/jest-reporters/src/coverage_reporter.ts +++ b/packages/jest-reporters/src/coverage_reporter.ts @@ -5,14 +5,12 @@ * LICENSE file in the root directory of this source tree. */ -// TODO: Remove this -/// - import path from 'path'; import {Config} from '@jest/types'; import {AggregatedResult, TestResult} from '@jest/test-result'; import {clearLine, isInteractive} from 'jest-util'; -import {createReporter} from 'istanbul-api'; +import istanbulReport from 'istanbul-lib-report'; +import istanbulReports from 'istanbul-reports'; import chalk from 'chalk'; import istanbulCoverage, { CoverageMap, @@ -88,20 +86,21 @@ export default class CoverageReporter extends BaseReporter { this._coverageMap, ); - const reporter = createReporter(); try { - if (this._globalConfig.coverageDirectory) { - reporter.dir = this._globalConfig.coverageDirectory; - } - + const reportContext = istanbulReport.createContext({ + dir: this._globalConfig.coverageDirectory, + sourceFinder, + }); const coverageReporters = this._globalConfig.coverageReporters || []; if (!this._globalConfig.useStderr && coverageReporters.length < 1) { coverageReporters.push('text-summary'); } - reporter.addAll(coverageReporters); - reporter.write(map, sourceFinder && {sourceFinder}); + const tree = istanbulReport.summarizers.pkg(map); + coverageReporters.forEach(reporter => { + tree.visit(istanbulReports.create(reporter, {}), reportContext); + }); aggregatedResults.coverageMap = map; } catch (e) { console.error( diff --git a/packages/jest-reporters/src/summary_reporter.ts b/packages/jest-reporters/src/summary_reporter.ts index 6e60fde4d3ea..e94cffb2d0a5 100644 --- a/packages/jest-reporters/src/summary_reporter.ts +++ b/packages/jest-reporters/src/summary_reporter.ts @@ -44,6 +44,12 @@ const NPM_EVENTS = new Set([ 'postrestart', ]); +const { + npm_config_user_agent, + npm_lifecycle_event, + npm_lifecycle_script, +} = process.env; + export default class SummaryReporter extends BaseReporter { private _estimatedTime: number; private _globalConfig: Config.GlobalConfig; @@ -123,15 +129,15 @@ export default class SummaryReporter extends BaseReporter { snapshots.updated ) { let updateCommand; - const event = process.env.npm_lifecycle_event || ''; + const event = npm_lifecycle_event || ''; const prefix = NPM_EVENTS.has(event) ? '' : 'run '; const isYarn = - typeof process.env.npm_config_user_agent === 'string' && - process.env.npm_config_user_agent.match('yarn') !== null; + typeof npm_config_user_agent === 'string' && + npm_config_user_agent.includes('yarn'); const client = isYarn ? 'yarn' : 'npm'; const scriptUsesJest = - typeof process.env.npm_lifecycle_script === 'string' && - process.env.npm_lifecycle_script.indexOf('jest') !== -1; + typeof npm_lifecycle_script === 'string' && + npm_lifecycle_script.includes('jest'); if (globalConfig.watch || globalConfig.watchAll) { updateCommand = 'press `u`'; diff --git a/packages/jest-resolve/src/index.ts b/packages/jest-resolve/src/index.ts index 169cccb0cc33..77dcff06df4a 100644 --- a/packages/jest-resolve/src/index.ts +++ b/packages/jest-resolve/src/index.ts @@ -39,8 +39,9 @@ const NATIVE_PLATFORM = 'native'; // We might be inside a symlink. const cwd = process.cwd(); const resolvedCwd = realpath(cwd) || cwd; -const nodePaths = process.env.NODE_PATH - ? process.env.NODE_PATH.split(path.delimiter) +const {NODE_PATH} = process.env; +const nodePaths = NODE_PATH + ? NODE_PATH.split(path.delimiter) .filter(Boolean) // The resolver expects absolute paths. .map(p => path.resolve(resolvedCwd, p)) diff --git a/packages/jest-snapshot/src/__tests__/utils.test.ts b/packages/jest-snapshot/src/__tests__/utils.test.ts index 981070691777..f28efd12128d 100644 --- a/packages/jest-snapshot/src/__tests__/utils.test.ts +++ b/packages/jest-snapshot/src/__tests__/utils.test.ts @@ -9,6 +9,7 @@ jest.mock('fs'); import fs from 'fs'; import path from 'path'; +import assert from 'assert'; import chalk from 'chalk'; import { @@ -201,12 +202,85 @@ test('serialize handles \\r\\n', () => { describe('DeepMerge', () => { it('Correctly merges objects with property matchers', () => { - const target = {data: {bar: 'bar', foo: 'foo'}}; + /* eslint-disable sort-keys */ + // to keep keys in numerical order rather than alphabetical + const target = { + data: { + one: 'one', + two: 'two', + three: [ + { + four: 'four', + five: 'five', + }, + // Include an array element not present in the propertyMatchers + { + six: 'six', + seven: 'seven', + }, + ], + eight: [{nine: 'nine'}], + }, + }; + const matcher = expect.any(String); - const propertyMatchers = {data: {foo: matcher}}; + const propertyMatchers = { + data: { + two: matcher, + three: [ + { + four: matcher, + }, + ], + eight: [ + {nine: matcher}, + // Include an array element not present in the target + {ten: matcher}, + ], + }, + }; + const mergedOutput = deepMerge(target, propertyMatchers); - expect(mergedOutput).toStrictEqual({data: {bar: 'bar', foo: matcher}}); - expect(target).toStrictEqual({data: {bar: 'bar', foo: 'foo'}}); + // Use assert.deepStrictEqual() instead of expect().toStrictEqual() + // since we want to actually validate that we got the matcher + // rather than treat it specially the way that expect() does + assert.deepStrictEqual(mergedOutput, { + data: { + one: 'one', + two: matcher, + three: [ + { + four: matcher, + five: 'five', + }, + { + six: 'six', + seven: 'seven', + }, + ], + eight: [{nine: matcher}, {ten: matcher}], + }, + }); + + // Ensure original target is not modified + expect(target).toStrictEqual({ + data: { + one: 'one', + two: 'two', + three: [ + { + four: 'four', + five: 'five', + }, + { + six: 'six', + seven: 'seven', + }, + ], + eight: [{nine: 'nine'}], + }, + }); + /* eslint-enable sort-keys */ }); }); diff --git a/packages/jest-snapshot/src/utils.ts b/packages/jest-snapshot/src/utils.ts index 992fa72926d0..d0e4b20eb049 100644 --- a/packages/jest-snapshot/src/utils.ts +++ b/packages/jest-snapshot/src/utils.ts @@ -178,6 +178,21 @@ export const saveSnapshotFile = ( ); }; +const deepMergeArray = (target: Array, source: Array) => { + // Clone target + const mergedOutput = target.slice(); + + source.forEach((element, index) => { + if (typeof mergedOutput[index] === 'undefined') { + mergedOutput[index] = element; + } else { + mergedOutput[index] = deepMerge(target[index], element); + } + }); + + return mergedOutput; +}; + export const deepMerge = (target: any, source: any) => { const mergedOutput = {...target}; if (isObject(target) && isObject(source)) { @@ -185,6 +200,8 @@ export const deepMerge = (target: any, source: any) => { if (isObject(source[key]) && !source[key].$$typeof) { if (!(key in target)) Object.assign(mergedOutput, {[key]: source[key]}); else mergedOutput[key] = deepMerge(target[key], source[key]); + } else if (Array.isArray(source[key])) { + mergedOutput[key] = deepMergeArray(target[key], source[key]); } else { Object.assign(mergedOutput, {[key]: source[key]}); } diff --git a/packages/jest-types/package.json b/packages/jest-types/package.json index 732c6bbf2929..dd9993f57f93 100644 --- a/packages/jest-types/package.json +++ b/packages/jest-types/package.json @@ -14,6 +14,7 @@ "types": "build/index.d.ts", "dependencies": { "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^1.1.1", "@types/yargs": "^12.0.9" }, "publishConfig": { diff --git a/packages/jest-types/src/Circus.ts b/packages/jest-types/src/Circus.ts new file mode 100644 index 000000000000..0ca639632e8d --- /dev/null +++ b/packages/jest-types/src/Circus.ts @@ -0,0 +1,218 @@ +/** + * 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. + */ + +import * as Global from './Global'; + +type Process = NodeJS.Process; + +export type DoneFn = Global.DoneFn; +export type BlockFn = Global.BlockFn; +export type BlockName = Global.BlockName; +export type BlockMode = void | 'skip' | 'only' | 'todo'; +export type TestMode = BlockMode; +export type TestName = Global.TestName; +export type TestFn = Global.TestFn; +export type HookFn = (done?: DoneFn) => Promise | null | undefined; +export type AsyncFn = TestFn | HookFn; +export type SharedHookType = 'afterAll' | 'beforeAll'; +export type HookType = SharedHookType | 'afterEach' | 'beforeEach'; +export type TestContext = Record; +export type Exception = any; // Since in JS anything can be thrown as an error. +export type FormattedError = string; // String representation of error. +export type Hook = { + asyncError: Error; + fn: HookFn; + type: HookType; + parent: DescribeBlock; + timeout: number | undefined | null; +}; + +export type EventHandler = (event: Event, state: State) => void; + +export type Event = + | { + name: 'include_test_location_in_result'; + } + | { + asyncError: Error; + mode: BlockMode; + name: 'start_describe_definition'; + blockName: BlockName; + } + | { + mode: BlockMode; + name: 'finish_describe_definition'; + blockName: BlockName; + } + | { + asyncError: Error; + name: 'add_hook'; + hookType: HookType; + fn: HookFn; + timeout: number | undefined; + } + | { + asyncError: Error; + name: 'add_test'; + testName: TestName; + fn?: TestFn; + mode?: TestMode; + timeout: number | undefined; + } + | { + name: 'hook_start'; + hook: Hook; + } + | { + name: 'hook_success'; + describeBlock: DescribeBlock | undefined | null; + test: TestEntry | undefined | null; + hook: Hook; + } + | { + name: 'hook_failure'; + error: string | Exception; + describeBlock: DescribeBlock | undefined | null; + test: TestEntry | undefined | null; + hook: Hook; + } + | { + name: 'test_fn_start'; + test: TestEntry; + } + | { + name: 'test_fn_success'; + test: TestEntry; + } + | { + name: 'test_fn_failure'; + error: Exception; + test: TestEntry; + } + | { + name: 'test_retry'; + test: TestEntry; + } + | { + // the `test` in this case is all hooks + it/test function, not just the + // function passed to `it/test` + name: 'test_start'; + test: TestEntry; + } + | { + name: 'test_skip'; + test: TestEntry; + } + | { + name: 'test_todo'; + test: TestEntry; + } + | { + // test failure is defined by presence of errors in `test.errors`, + // `test_done` indicates that the test and all its hooks were run, + // and nothing else will change it's state in the future. (except third + // party extentions/plugins) + name: 'test_done'; + test: TestEntry; + } + | { + name: 'run_describe_start'; + describeBlock: DescribeBlock; + } + | { + name: 'run_describe_finish'; + describeBlock: DescribeBlock; + } + | { + name: 'run_start'; + } + | { + name: 'run_finish'; + } + | { + // Any unhandled error that happened outside of test/hooks (unless it is + // an `afterAll` hook) + name: 'error'; + error: Exception; + } + | { + // first action to dispatch. Good time to initialize all settings + name: 'setup'; + testNamePattern?: string; + parentProcess: Process; + } + | { + // Action dispatched after everything is finished and we're about to wrap + // things up and return test results to the parent process (caller). + name: 'teardown'; + }; + +export type TestStatus = 'skip' | 'done' | 'todo'; +export type TestResult = { + duration: number | null | undefined; + errors: Array; + invocations: number; + status: TestStatus; + location: {column: number; line: number} | null | undefined; + testPath: Array; +}; + +export type RunResult = { + unhandledErrors: Array; + testResults: TestResults; +}; + +export type TestResults = Array; + +export type GlobalErrorHandlers = { + uncaughtException: Array<(exception: Exception) => void>; + unhandledRejection: Array< + (exception: Exception, promise: Promise) => void + >; +}; + +export type State = { + currentDescribeBlock: DescribeBlock; + currentlyRunningTest: TestEntry | undefined | null; // including when hooks are being executed + expand?: boolean; // expand error messages + hasFocusedTests: boolean; // that are defined using test.only + // Store process error handlers. During the run we inject our own + // handlers (so we could fail tests on unhandled errors) and later restore + // the original ones. + originalGlobalErrorHandlers?: GlobalErrorHandlers; + parentProcess: Process | null; // process object from the outer scope + rootDescribeBlock: DescribeBlock; + testNamePattern: RegExp | undefined | null; + testTimeout: number; + unhandledErrors: Array; + includeTestLocationInResult: boolean; +}; + +export type DescribeBlock = { + children: Array; + hooks: Array; + mode: BlockMode; + name: BlockName; + parent: DescribeBlock | undefined | null; + tests: Array; +}; + +export type TestError = Exception | Array<[Exception | undefined, Exception]>; // the error from the test, as well as a backup error for async + +export type TestEntry = { + asyncError: Exception; // Used if the test failure contains no usable stack trace + errors: TestError; + fn: TestFn | undefined | null; + invocations: number; + mode: TestMode; + name: TestName; + parent: DescribeBlock; + startedAt: number | undefined | null; + duration: number | undefined | null; + status: TestStatus | undefined | null; // whether the test has been skipped or run already + timeout: number | undefined | null; +}; diff --git a/packages/jest-types/src/Config.ts b/packages/jest-types/src/Config.ts index e5c073936a4e..6a79718bc523 100644 --- a/packages/jest-types/src/Config.ts +++ b/packages/jest-types/src/Config.ts @@ -6,6 +6,7 @@ */ import {Arguments} from 'yargs'; +import {ReportOptions} from 'istanbul-reports'; export type Path = string; @@ -300,7 +301,7 @@ export type GlobalConfig = { | undefined; coverageDirectory: string; coveragePathIgnorePatterns?: Array; - coverageReporters: Array; + coverageReporters: Array; coverageThreshold: CoverageThreshold; detectLeaks: boolean; detectOpenHandles: boolean; diff --git a/packages/jest-types/src/index.ts b/packages/jest-types/src/index.ts index 119377a9b79d..e38f36ad9541 100644 --- a/packages/jest-types/src/index.ts +++ b/packages/jest-types/src/index.ts @@ -5,7 +5,8 @@ * LICENSE file in the root directory of this source tree. */ +import * as Circus from './Circus'; import * as Config from './Config'; import * as Global from './Global'; -export {Config, Global}; +export {Circus, Config, Global}; diff --git a/packages/jest-util/src/__tests__/createProcessObject.test.ts b/packages/jest-util/src/__tests__/createProcessObject.test.ts index 4eca00c977a2..baa29639c6c5 100644 --- a/packages/jest-util/src/__tests__/createProcessObject.test.ts +++ b/packages/jest-util/src/__tests__/createProcessObject.test.ts @@ -6,9 +6,17 @@ */ import EventEmitter from 'events'; -import createProcessObject from '../createProcessObject'; + +let createProcessObject; + +function requireCreateProcessObject() { + jest.isolateModules(() => { + createProcessObject = require('../createProcessObject').default; + }); +} it('creates a process object that looks like the original one', () => { + requireCreateProcessObject(); const fakeProcess = createProcessObject(); // "process" inherits from EventEmitter through the prototype chain. @@ -35,6 +43,7 @@ it('fakes require("process") so it is equal to "global.process"', () => { it('checks that process.env works as expected on Linux platforms', () => { Object.defineProperty(process, 'platform', {get: () => 'linux'}); + requireCreateProcessObject(); // Existing properties inside process.env are copied to the fake environment. process.env.PROP_STRING = 'foo'; @@ -73,6 +82,7 @@ it('checks that process.env works as expected on Linux platforms', () => { it('checks that process.env works as expected in Windows platforms', () => { Object.defineProperty(process, 'platform', {get: () => 'win32'}); + requireCreateProcessObject(); // Windows is not case sensitive when it comes to property names. process.env.PROP_STRING = 'foo'; diff --git a/packages/jest-util/src/createProcessObject.ts b/packages/jest-util/src/createProcessObject.ts index 903336636bdd..53d18dd43cb5 100644 --- a/packages/jest-util/src/createProcessObject.ts +++ b/packages/jest-util/src/createProcessObject.ts @@ -8,6 +8,8 @@ import deepCyclicCopy from './deepCyclicCopy'; const BLACKLIST = new Set(['env', 'mainModule', '_events']); +const isWin32 = process.platform === 'win32'; +const proto: Record = Object.getPrototypeOf(process.env); // The "process.env" object has a bunch of particularities: first, it does not // directly extend from Object; second, it converts any assigned value to a @@ -15,42 +17,51 @@ const BLACKLIST = new Set(['env', 'mainModule', '_events']); // mimic it (see https://nodejs.org/api/process.html#process_process_env). function createProcessEnv(): NodeJS.ProcessEnv { - if (typeof Proxy === 'undefined') { - return deepCyclicCopy(process.env); - } - - const proto: Record = Object.getPrototypeOf(process.env); const real = Object.create(proto); const lookup: typeof process.env = {}; - const proxy = new Proxy(real, { - deleteProperty(_target, key) { - for (const name in real) { - if (real.hasOwnProperty(name)) { - if (typeof key === 'string' && process.platform === 'win32') { - if (name.toLowerCase() === key.toLowerCase()) { - delete real[name]; - delete lookup[name.toLowerCase()]; - } - } else { - if (key === name) { - delete real[name]; - delete lookup[name]; - } + function deletePropertyWin32(_target: any, key: any) { + for (const name in real) { + if (real.hasOwnProperty(name)) { + if (typeof key === 'string') { + if (name.toLowerCase() === key.toLowerCase()) { + delete real[name]; + delete lookup[name.toLowerCase()]; + } + } else { + if (key === name) { + delete real[name]; + delete lookup[name]; } } } + } - return true; - }, + return true; + } - get(_target, key) { - if (typeof key === 'string' && process.platform === 'win32') { - return lookup[key in proto ? key : key.toLowerCase()]; - } else { - return real[key]; - } - }, + function deleteProperty(_target: any, key: any) { + delete real[key]; + delete lookup[key]; + + return true; + } + + function getProperty(_target: any, key: any) { + return real[key]; + } + + function getPropertyWin32(_target: any, key: any) { + if (typeof key === 'string') { + return lookup[key in proto ? key : key.toLowerCase()]; + } else { + return real[key]; + } + } + + const proxy = new Proxy(real, { + deleteProperty: isWin32 ? deletePropertyWin32 : deleteProperty, + get: isWin32 ? getPropertyWin32 : getProperty, set(_target, key, value) { const strValue = '' + value; diff --git a/scripts/mapCoverage.js b/scripts/mapCoverage.js index b93148e19709..af27ed9b5ef5 100644 --- a/scripts/mapCoverage.js +++ b/scripts/mapCoverage.js @@ -25,12 +25,14 @@ * produce a full coverage report. */ -const createReporter = require('istanbul-api').createReporter; +const istanbulReport = require('istanbul-lib-report'); +const istanbulReports = require('istanbul-reports'); const istanbulCoverage = require('istanbul-lib-coverage'); const coverage = require('../coverage/coverage-final.json'); const map = istanbulCoverage.createCoverageMap(); -const reporter = createReporter(); + +const context = istanbulReport.createContext(); const mapFileCoverage = fileCoverage => { fileCoverage.path = fileCoverage.path.replace( @@ -44,5 +46,7 @@ Object.keys(coverage).forEach(filename => map.addFileCoverage(mapFileCoverage(coverage[filename])) ); -reporter.addAll(['json', 'lcov', 'text']); -reporter.write(map); +const tree = istanbulReport.summarizers.pkg(map); +['json', 'lcov', 'text'].forEach(reporter => + tree.visit(istanbulReports.create(reporter, {}), context) +); diff --git a/scripts/remove-postinstall.js b/scripts/remove-postinstall.js new file mode 100644 index 000000000000..a4b10c50f6b5 --- /dev/null +++ b/scripts/remove-postinstall.js @@ -0,0 +1,13 @@ +// Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved. + +'use strict'; + +const {writeFileSync} = require('fs'); + +const pkgPath = require.resolve('../package.json'); + +const pkg = require('../package.json'); + +delete pkg.scripts.postinstall; + +writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`); diff --git a/website/blog/2016-12-15-2016-in-jest.md b/website/blog/2016-12-15-2016-in-jest.md index 587ebe7bb8fa..619e2ada75d8 100644 --- a/website/blog/2016-12-15-2016-in-jest.md +++ b/website/blog/2016-12-15-2016-in-jest.md @@ -13,7 +13,7 @@ The newly created [react-test-renderer](https://yarnpkg.com/en/package/react-tes The [pretty-format](https://github.com/facebook/jest/tree/master/packages/pretty-format) project was rewritten with performance in mind to drive Jest's snapshot feature, was recently merged into Jest's monorepo and is also helpful in other [test runners](https://github.com/avajs/ava/pull/1154). Nowadays Jest is much more about collecting different ideas and solutions to testing than it is about one specific implementation of a test framework. -I'd like to deeply thank all the people that have [contributed to Jest this year](https://github.com/facebook/jest/graphs/contributors?from=2016-01-01&to=2016-12-14&type=c), both from the open source community and at Facebook: Dmitrii Abramov, Cristian Carlesso, Dan Abramov, Daniel Lo Nigro, Maxim Derbin, Evan Scott, Forbes Lindesay, Keyan Zhang and 60 more people. We'd also like to welcome [Michał Pierzchała (@thymikee)](https://twitter.com/thymikee) as first official external contributor to Jest. He's been doing a great job managing the issues and PRs on the repo. If you'd like to start contributing to Jest, we have a bunch of [good first tasks](https://github.com/facebook/jest/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+bug%22) and we are always happy to help on our [discord channel](https://jestjs.io/support.html). +I'd like to deeply thank all the people that have [contributed to Jest this year](https://github.com/facebook/jest/graphs/contributors?from=2016-01-01&to=2016-12-14&type=c), both from the open source community and at Facebook: Dmitrii Abramov, Cristian Carlesso, Dan Abramov, Daniel Lo Nigro, Maxim Derbin, Evan Scott, Forbes Lindesay, Keyan Zhang and 60 more people. We'd also like to welcome [Michał Pierzchała (@thymikee)](https://twitter.com/thymikee) as first official external contributor to Jest. He's been doing a great job managing the issues and PRs on the repo. If you'd like to start contributing to Jest, we have a bunch of [good first tasks](https://github.com/facebook/jest/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+bug%22) and we are always happy to help on our [discord channel](https://jestjs.io/en/help). ## [repl.it](http://repl.it/) with Jest integration diff --git a/website/sidebars.json b/website/sidebars.json index c8fb58c869e7..de65d7d26007 100644 --- a/website/sidebars.json +++ b/website/sidebars.json @@ -20,6 +20,7 @@ "webpack", "puppeteer", "mongodb", + "dynamodb", "tutorial-jquery", "watch-plugins", "migration-guide", diff --git a/website/versioned_docs/version-22.x/MigrationGuide.md b/website/versioned_docs/version-22.x/MigrationGuide.md index 03006de8b04b..e1d7545b7ee5 100644 --- a/website/versioned_docs/version-22.x/MigrationGuide.md +++ b/website/versioned_docs/version-22.x/MigrationGuide.md @@ -14,16 +14,10 @@ If you'd like to try out Jest with an existing codebase, there are a number of w If you are using [AVA](https://github.com/avajs/ava), [Chai](https://github.com/chaijs/chai), [Expect.js (by Automattic)](https://github.com/Automattic/expect.js), [Jasmine](https://github.com/jasmine/jasmine), [Mocha](https://github.com/mochajs/mocha), [proxyquire](https://github.com/thlorenz/proxyquire), [Should.js](https://github.com/tj/should.js/) or [Tape](https://github.com/substack/tape) you can use the third-party [jest-codemods](https://github.com/skovhus/jest-codemods) to do most of the dirty migration work. It runs a code transformation on your codebase using [jscodeshift](https://github.com/facebook/jscodeshift). -Install Jest Codemods with `yarn` by running: - -```bash -yarn global add jest-codemods -``` - To transform your existing tests, navigate to the project containing the tests and run: ```bash -jest-codemods +npx jest-codemods ``` More information can be found at [https://github.com/skovhus/jest-codemods](https://github.com/skovhus/jest-codemods). diff --git a/website/versioned_docs/version-22.x/TestingFrameworks.md b/website/versioned_docs/version-22.x/TestingFrameworks.md index 6073c7c860bc..fb0cc270551b 100644 --- a/website/versioned_docs/version-22.x/TestingFrameworks.md +++ b/website/versioned_docs/version-22.x/TestingFrameworks.md @@ -27,3 +27,7 @@ Although Jest may be considered a React-specific test runner, in fact it is a un ## Redux - [Writing Tests](http://redux.js.org/docs/recipes/WritingTests.html) by Redux docs + +## GatsbyJS + +- [Unit Testing](https://www.gatsbyjs.org/docs/unit-testing/) by GatsbyJS docs diff --git a/website/versioned_docs/version-23.x/MongoDB.md b/website/versioned_docs/version-23.x/MongoDB.md index 9606b0f9eb3f..df779d2b6b2b 100644 --- a/website/versioned_docs/version-23.x/MongoDB.md +++ b/website/versioned_docs/version-23.x/MongoDB.md @@ -6,140 +6,57 @@ original_id: mongodb With the [Global Setup/Teardown](Configuration.md#globalsetup-string) and [Async Test Environment](Configuration.md#testenvironment-string) APIs, Jest can work smoothly with [MongoDB](https://www.mongodb.com/). -## A jest-mongodb example +## Use jest-mongodb Preset -The basic idea is to: +[Jest MongoDB](https://github.com/shelfio/jest-mongodb) provides all required configuration to run your tests using MongoDB. -1. Spin up in-memory mongodb server -2. Export a global variable with mongo URI -3. Write tests for queries / aggregations using a real database ✨ -4. Shut down mongodb server using Global Teardown +1. First install `@shelf/jest-mongodb` -Here's an example of the GlobalSetup script - -```js -// setup.js -const path = require('path'); - -const fs = require('fs'); - -const {MongoMemoryServer} = require('mongodb-memory-server'); - -const globalConfigPath = path.join(__dirname, 'globalConfig.json'); - -const mongod = new MongoMemoryServer({ - autoStart: false, -}); - -module.exports = async () => { - if (!mongod.isRunning) { - await mongod.start(); - } - - const mongoConfig = { - mongoDBName: 'jest', - mongoUri: await mongod.getConnectionString(), - }; - - // Write global config to disk because all tests run in different contexts. - fs.writeFileSync(globalConfigPath, JSON.stringify(mongoConfig)); - - // Set reference to mongod in order to close the server during teardown. - global.__MONGOD__ = mongod; -}; +``` +yarn add @shelf/jest-mongodb --dev ``` -Then we need a custom Test Environment for Mongo - -```js -// mongo-environment.js -const NodeEnvironment = require('jest-environment-node'); - -const path = require('path'); - -const fs = require('fs'); - -const globalConfigPath = path.join(__dirname, 'globalConfig.json'); - -class MongoEnvironment extends NodeEnvironment { - constructor(config) { - super(config); - } - - async setup() { - console.log('Setup MongoDB Test Environment'); - - const globalConfig = JSON.parse(fs.readFileSync(globalConfigPath, 'utf-8')); - - this.global.__MONGO_URI__ = globalConfig.mongoUri; - this.global.__MONGO_DB_NAME__ = globalConfig.mongoDBName; - - await super.setup(); - } - - async teardown() { - console.log('Teardown MongoDB Test Environment'); - - await super.teardown(); - } +2. Specify preset in your Jest configuration: - runScript(script) { - return super.runScript(script); - } +```json +{ + "preset": "@shelf/jest-mongodb" } ``` -Finally we can shut down mongodb server +3. Write your test ```js -// teardown.js -module.exports = async function() { - await global.__MONGOD__.stop(); -}; -``` +const {MongoClient} = require('mongodb'); -With all the things set up, we can now write our tests like this: +describe('insert', () => { + let connection; + let db; -```js -// test.js -const {MongoClient} = require('mongodb'); + beforeAll(async () => { + connection = await MongoClient.connect(global.__MONGO_URI__, { + useNewUrlParser: true, + }); + db = await connection.db(global.__MONGO_DB_NAME__); + }); -let connection; -let db; + afterAll(async () => { + await connection.close(); + await db.close(); + }); -beforeAll(async () => { - connection = await MongoClient.connect(global.__MONGO_URI__); - db = await connection.db(global.__MONGO_DB_NAME__); -}); + it('should insert a doc into collection', async () => { + const users = db.collection('users'); -afterAll(async () => { - await connection.close(); - await db.close(); -}); + const mockUser = {_id: 'some-user-id', name: 'John'}; + await users.insertOne(mockUser); -it('should aggregate docs from collection', async () => { - const files = db.collection('files'); - - await files.insertMany([ - {type: 'Document'}, - {type: 'Video'}, - {type: 'Image'}, - {type: 'Document'}, - {type: 'Image'}, - {type: 'Document'}, - ]); - - const topFiles = await files - .aggregate([ - {$group: {_id: '$type', count: {$sum: 1}}}, - {$sort: {count: -1}}, - ]) - .toArray(); - - expect(topFiles).toEqual([ - {_id: 'Document', count: 3}, - {_id: 'Image', count: 2}, - {_id: 'Video', count: 1}, - ]); + const insertedUser = await users.findOne({_id: 'some-user-id'}); + expect(insertedUser).toEqual(mockUser); + }); }); ``` + +There's no need to load any dependencies. + +See [documentation](https://github.com/shelfio/jest-mongodb) for details (configuring MongoDB version, etc). diff --git a/website/versioned_docs/version-23.x/TestingFrameworks.md b/website/versioned_docs/version-23.x/TestingFrameworks.md index 4fb235251995..7e77bad4f91b 100644 --- a/website/versioned_docs/version-23.x/TestingFrameworks.md +++ b/website/versioned_docs/version-23.x/TestingFrameworks.md @@ -31,3 +31,7 @@ Although Jest may be considered a React-specific test runner, in fact it is a un ## Express.js - [How to test Express.js with Jest and Supertest](http://www.albertgao.xyz/2017/05/24/how-to-test-expressjs-with-jest-and-supertest/) by Albert Gao ([@albertgao](https://twitter.com/albertgao)) + +## GatsbyJS + +- [Unit Testing](https://www.gatsbyjs.org/docs/unit-testing/) by GatsbyJS docs diff --git a/website/versioned_docs/version-24.0/MongoDB.md b/website/versioned_docs/version-24.0/MongoDB.md index dc1f02ff4c14..23ba1283162e 100644 --- a/website/versioned_docs/version-24.0/MongoDB.md +++ b/website/versioned_docs/version-24.0/MongoDB.md @@ -6,142 +6,57 @@ original_id: mongodb With the [Global Setup/Teardown](Configuration.md#globalsetup-string) and [Async Test Environment](Configuration.md#testenvironment-string) APIs, Jest can work smoothly with [MongoDB](https://www.mongodb.com/). -## A jest-mongodb example +## Use jest-mongodb Preset -The basic idea is to: +[Jest MongoDB](https://github.com/shelfio/jest-mongodb) provides all required configuration to run your tests using MongoDB. -1. Spin up in-memory mongodb server -2. Export a global variable with mongo URI -3. Write tests for queries / aggregations using a real database ✨ -4. Shut down mongodb server using Global Teardown +1. First install `@shelf/jest-mongodb` -Here's an example of the GlobalSetup script - -```js -// setup.js -const path = require('path'); - -const fs = require('fs'); - -const {MongoMemoryServer} = require('mongodb-memory-server'); - -const globalConfigPath = path.join(__dirname, 'globalConfig.json'); - -const mongod = new MongoMemoryServer({ - autoStart: false, -}); - -module.exports = async () => { - if (!mongod.isRunning) { - await mongod.start(); - } - - const mongoConfig = { - mongoDBName: 'jest', - mongoUri: await mongod.getConnectionString(), - }; - - // Write global config to disk because all tests run in different contexts. - fs.writeFileSync(globalConfigPath, JSON.stringify(mongoConfig)); - - // Set reference to mongod in order to close the server during teardown. - global.__MONGOD__ = mongod; -}; +``` +yarn add @shelf/jest-mongodb --dev ``` -Then we need a custom Test Environment for Mongo - -```js -// mongo-environment.js -const NodeEnvironment = require('jest-environment-node'); - -const path = require('path'); - -const fs = require('fs'); - -const globalConfigPath = path.join(__dirname, 'globalConfig.json'); - -class MongoEnvironment extends NodeEnvironment { - constructor(config) { - super(config); - } - - async setup() { - console.log('Setup MongoDB Test Environment'); - - const globalConfig = JSON.parse(fs.readFileSync(globalConfigPath, 'utf-8')); - - this.global.__MONGO_URI__ = globalConfig.mongoUri; - this.global.__MONGO_DB_NAME__ = globalConfig.mongoDBName; - - await super.setup(); - } - - async teardown() { - console.log('Teardown MongoDB Test Environment'); - - await super.teardown(); - } +2. Specify preset in your Jest configuration: - runScript(script) { - return super.runScript(script); - } +```json +{ + "preset": "@shelf/jest-mongodb" } - -module.exports = MongoEnvironment; ``` -Finally we can shut down mongodb server +3. Write your test ```js -// teardown.js -module.exports = async function() { - await global.__MONGOD__.stop(); -}; -``` +const {MongoClient} = require('mongodb'); -With all the things set up, we can now write our tests like this: +describe('insert', () => { + let connection; + let db; -```js -// test.js -const {MongoClient} = require('mongodb'); + beforeAll(async () => { + connection = await MongoClient.connect(global.__MONGO_URI__, { + useNewUrlParser: true, + }); + db = await connection.db(global.__MONGO_DB_NAME__); + }); -let connection; -let db; + afterAll(async () => { + await connection.close(); + await db.close(); + }); -beforeAll(async () => { - connection = await MongoClient.connect(global.__MONGO_URI__); - db = await connection.db(global.__MONGO_DB_NAME__); -}); + it('should insert a doc into collection', async () => { + const users = db.collection('users'); -afterAll(async () => { - await connection.close(); - await db.close(); -}); + const mockUser = {_id: 'some-user-id', name: 'John'}; + await users.insertOne(mockUser); -it('should aggregate docs from collection', async () => { - const files = db.collection('files'); - - await files.insertMany([ - {type: 'Document'}, - {type: 'Video'}, - {type: 'Image'}, - {type: 'Document'}, - {type: 'Image'}, - {type: 'Document'}, - ]); - - const topFiles = await files - .aggregate([ - {$group: {_id: '$type', count: {$sum: 1}}}, - {$sort: {count: -1}}, - ]) - .toArray(); - - expect(topFiles).toEqual([ - {_id: 'Document', count: 3}, - {_id: 'Image', count: 2}, - {_id: 'Video', count: 1}, - ]); + const insertedUser = await users.findOne({_id: 'some-user-id'}); + expect(insertedUser).toEqual(mockUser); + }); }); ``` + +There's no need to load any dependencies. + +See [documentation](https://github.com/shelfio/jest-mongodb) for details (configuring MongoDB version, etc). diff --git a/website/versioned_docs/version-24.6/MigrationGuide.md b/website/versioned_docs/version-24.6/MigrationGuide.md index b2e2cd03e352..1c05db3b6a5a 100644 --- a/website/versioned_docs/version-24.6/MigrationGuide.md +++ b/website/versioned_docs/version-24.6/MigrationGuide.md @@ -14,16 +14,10 @@ If you'd like to try out Jest with an existing codebase, there are a number of w If you are using [AVA](https://github.com/avajs/ava), [Chai](https://github.com/chaijs/chai), [Expect.js (by Automattic)](https://github.com/Automattic/expect.js), [Jasmine](https://github.com/jasmine/jasmine), [Mocha](https://github.com/mochajs/mocha), [proxyquire](https://github.com/thlorenz/proxyquire), [Should.js](https://github.com/shouldjs/should.js) or [Tape](https://github.com/substack/tape) you can use the third-party [jest-codemods](https://github.com/skovhus/jest-codemods) to do most of the dirty migration work. It runs a code transformation on your codebase using [jscodeshift](https://github.com/facebook/jscodeshift). -Install Jest Codemods with `yarn` by running: - -```bash -yarn global add jest-codemods -``` - To transform your existing tests, navigate to the project containing the tests and run: ```bash -jest-codemods +npx jest-codemods ``` More information can be found at [https://github.com/skovhus/jest-codemods](https://github.com/skovhus/jest-codemods). diff --git a/yarn.lock b/yarn.lock index 64f3a6c583e3..ea4b24ae21de 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1749,6 +1749,13 @@ "@types/istanbul-lib-coverage" "*" source-map "^0.6.1" +"@types/istanbul-lib-report@*", "@types/istanbul-lib-report@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@types/istanbul-lib-report/-/istanbul-lib-report-1.1.0.tgz#79e9b463f947e98dcc82272da51b908fc93e8aea" + integrity sha512-nW5QuzmMhr7fHPijtaGOemFFI8Ctrxb/dIXgouSlKmWT16RxWlGLEX/nGghIBOReKe9hPFZXoNh338nFQk2xcA== + dependencies: + "@types/istanbul-lib-coverage" "*" + "@types/istanbul-lib-source-maps@^1.2.1": version "1.2.1" resolved "https://registry.yarnpkg.com/@types/istanbul-lib-source-maps/-/istanbul-lib-source-maps-1.2.1.tgz#c6db98b8b9f0b5aea000f7a8922cc075a85eda9f" @@ -1757,6 +1764,14 @@ "@types/istanbul-lib-coverage" "*" source-map "^0.6.1" +"@types/istanbul-reports@^1.1.0", "@types/istanbul-reports@^1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@types/istanbul-reports/-/istanbul-reports-1.1.1.tgz#7a8cbf6a406f36c8add871625b278eaf0b0d255a" + integrity sha512-UpYjBi8xefVChsCoBpKShdxTllC9pwISirfoZsUa2AAdQg/Jd2KQGtSbw+ya7GPo7x/wAPlH6JBhKhAsXUEZNA== + dependencies: + "@types/istanbul-lib-coverage" "*" + "@types/istanbul-lib-report" "*" + "@types/jest@*", "@types/jest@24.0.2": version "24.0.2" resolved "https://registry.yarnpkg.com/@types/jest/-/jest-24.0.2.tgz#a10ad017ee020b2dfa97655323dbf1f38f14f588" @@ -2354,13 +2369,6 @@ anymatch@^2.0.0: micromatch "^3.1.4" normalize-path "^2.1.1" -append-transform@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/append-transform/-/append-transform-1.0.0.tgz#046a52ae582a228bd72f58acfbe2967c678759ab" - integrity sha512-P009oYkeHyU742iSZJzZZywj4QRJdnTWffaKuJQLablCZ1uz6/cW4yaRgcDaoQ+uwOxxnt0gRUcwfsNP2ri0gw== - dependencies: - default-require-extensions "^2.0.0" - aproba@^1.0.3, aproba@^1.1.1: version "1.2.0" resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a" @@ -3787,11 +3795,6 @@ compare-func@^1.3.1: array-ify "^1.0.0" dot-prop "^3.0.0" -compare-versions@^3.2.1: - version "3.4.0" - resolved "https://registry.yarnpkg.com/compare-versions/-/compare-versions-3.4.0.tgz#e0747df5c9cb7f054d6d3dc3e1dbc444f9e92b26" - integrity sha512-tK69D7oNXXqUW3ZNo/z7NXTEz22TCF0pTE+YF9cxvaAM9XnkLo1fV621xCLrRR6aevJlKxExkss0vWqUCUpqdg== - component-bind@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/component-bind/-/component-bind-1.0.0.tgz#00c608ab7dcd93897c0009651b1d3a8e1e73bbd1" @@ -4470,13 +4473,6 @@ deepmerge@^2.0.1, deepmerge@^2.1.1: resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-2.2.1.tgz#5d3ff22a01c00f645405a2fbc17d0778a1801170" integrity sha512-R9hc1Xa/NOBi9WRVUWg19rl1UB7Tt4kuPd+thNJgFZoxXsTz7ncaPaeIm+40oSGuP33DfMb4sZt1QIGiJzC4EA== -default-require-extensions@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/default-require-extensions/-/default-require-extensions-2.0.0.tgz#f5f8fbb18a7d6d50b21f641f649ebb522cfe24f7" - integrity sha1-9fj7sYp9bVCyH2QfZJ67Uiz+JPc= - dependencies: - strip-bom "^3.0.0" - defaults@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/defaults/-/defaults-1.0.3.tgz#c656051e9817d9ff08ed881477f3fe4019f3ef7d" @@ -5856,14 +5852,6 @@ filenamify@^2.0.0: strip-outer "^1.0.0" trim-repeated "^1.0.0" -fileset@^2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/fileset/-/fileset-2.0.3.tgz#8e7548a96d3cc2327ee5e674168723a333bba2a0" - integrity sha1-jnVIqW08wjJ+5eZ0FocjozO7oqA= - dependencies: - glob "^7.0.3" - minimatch "^3.0.3" - filesize@3.5.11: version "3.5.11" resolved "https://registry.yarnpkg.com/filesize/-/filesize-3.5.11.tgz#1919326749433bb3cf77368bd158caabcc19e9ee" @@ -7553,38 +7541,12 @@ isstream@~0.1.2: resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" integrity sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo= -istanbul-api@^2.0.8, istanbul-api@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/istanbul-api/-/istanbul-api-2.1.1.tgz#194b773f6d9cbc99a9258446848b0f988951c4d0" - integrity sha512-kVmYrehiwyeBAk/wE71tW6emzLiHGjYIiDrc8sfyty4F8M02/lrgXSm+R1kXysmF20zArvmZXjlE/mg24TVPJw== - dependencies: - async "^2.6.1" - compare-versions "^3.2.1" - fileset "^2.0.3" - istanbul-lib-coverage "^2.0.3" - istanbul-lib-hook "^2.0.3" - istanbul-lib-instrument "^3.1.0" - istanbul-lib-report "^2.0.4" - istanbul-lib-source-maps "^3.0.2" - istanbul-reports "^2.1.1" - js-yaml "^3.12.0" - make-dir "^1.3.0" - minimatch "^3.0.4" - once "^1.4.0" - istanbul-lib-coverage@^2.0.2, istanbul-lib-coverage@^2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.3.tgz#0b891e5ad42312c2b9488554f603795f9a2211ba" integrity sha512-dKWuzRGCs4G+67VfW9pBFFz2Jpi4vSp/k7zBcJ888ofV5Mi1g5CUML5GvMvV6u9Cjybftu+E8Cgp+k0dI1E5lw== -istanbul-lib-hook@^2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/istanbul-lib-hook/-/istanbul-lib-hook-2.0.3.tgz#e0e581e461c611be5d0e5ef31c5f0109759916fb" - integrity sha512-CLmEqwEhuCYtGcpNVJjLV1DQyVnIqavMLFHV/DP+np/g3qvdxu3gsPqYoJMXm15sN84xOlckFB3VNvRbf5yEgA== - dependencies: - append-transform "^1.0.0" - -istanbul-lib-instrument@^3.0.0, istanbul-lib-instrument@^3.0.1, istanbul-lib-instrument@^3.1.0: +istanbul-lib-instrument@^3.0.0, istanbul-lib-instrument@^3.0.1: version "3.1.0" resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-3.1.0.tgz#a2b5484a7d445f1f311e93190813fa56dfb62971" integrity sha512-ooVllVGT38HIk8MxDj/OIHXSYvH+1tq/Vb38s8ixt9GoJadXska4WkGY+0wkmtYCZNYtaARniH/DixUGGLZ0uA== @@ -7606,7 +7568,7 @@ istanbul-lib-report@^2.0.4: make-dir "^1.3.0" supports-color "^6.0.0" -istanbul-lib-source-maps@^3.0.1, istanbul-lib-source-maps@^3.0.2: +istanbul-lib-source-maps@^3.0.1: version "3.0.2" resolved "https://registry.yarnpkg.com/istanbul-lib-source-maps/-/istanbul-lib-source-maps-3.0.2.tgz#f1e817229a9146e8424a28e5d69ba220fda34156" integrity sha512-JX4v0CiKTGp9fZPmoxpu9YEkPbEqCqBbO3403VabKjH+NRXo72HafD5UgnjTEqHL2SAjaZK1XDuDOkn6I5QVfQ== @@ -8920,7 +8882,7 @@ minimatch@3.0.3: dependencies: brace-expansion "^1.0.0" -minimatch@3.0.4, minimatch@^3.0.0, minimatch@^3.0.2, minimatch@^3.0.3, minimatch@^3.0.4, minimatch@~3.0.2: +minimatch@3.0.4, minimatch@^3.0.0, minimatch@^3.0.2, minimatch@^3.0.4, minimatch@~3.0.2: version "3.0.4" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==