diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 000000000000..7734a2a81437 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,17 @@ +# http://editorconfig.org +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_size = 2 +indent_style = space +max_line_length = 80 +trim_trailing_whitespace = true + +[*.md] +max_line_length = 0 +trim_trailing_whitespace = false + +[COMMIT_EDITMSG] +max_line_length = 0 diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000000..80af036a1db9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +*~ +*.swp +.DS_STORE +.haste_cache_dir +node_modules +npm-debug.log +build +website/core/metadata.js +website/src/jest/docs diff --git a/.jshintrc b/.jshintrc new file mode 100644 index 000000000000..77d3b2a0cd96 --- /dev/null +++ b/.jshintrc @@ -0,0 +1,77 @@ +{ + "-W093": true, + "asi": false, + "bitwise": true, + "boss": false, + "browser": false, + "camelcase": true, + "couch": false, + "curly": true, + "debug": false, + "devel": true, + "dojo": false, + "eqeqeq": true, + "eqnull": false, + "esnext": true, + "evil": false, + "expr": true, + "forin": false, + "freeze": true, + "funcscope": true, + "gcl": false, + "globalstrict": true, + "immed": false, + "indent": 2, + "iterator": false, + "jquery": false, + "lastsemic": false, + "latedef": false, + "laxbreak": true, + "laxcomma": false, + "loopfunc": false, + "maxcomplexity": false, + "maxdepth": false, + "maxerr": 50, + "maxlen": 80, + "maxparams": false, + "maxstatements": false, + "mootools": false, + "moz": false, + "multistr": false, + "newcap": true, + "noarg": true, + "node": true, + "noempty": true, + "nonbsp": true, + "nonew": true, + "nonstandard": false, + "notypeof": false, + "noyield": false, + "phantom": false, + "plusplus": false, + "predef": [ + "describe", + "beforeEach", + "it", + "jest", + "pit", + "expect" + ], + "proto": false, + "prototypejs": false, + "quotmark": true, + "rhino": false, + "scripturl": false, + "shadow": false, + "smarttabs": false, + "strict": true, + "sub": false, + "supernew": false, + "trailing": true, + "undef": true, + "unused": true, + "validthis": false, + "worker": false, + "wsh": false, + "yui": false +} diff --git a/.npmignore b/.npmignore new file mode 100644 index 000000000000..efbda9ae1c72 --- /dev/null +++ b/.npmignore @@ -0,0 +1,3 @@ +examples +website +docs diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 000000000000..edf467fa459b --- /dev/null +++ b/.travis.yml @@ -0,0 +1,13 @@ +--- +language: node_js +node_js: +- '0.10' +- '0.8' +notifications: + irc: + use_notice: true + skip_join: true + on_success: change + on_failure: change + channels: + - chat.freenode.net#jestjs diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000000..f7ac2743851f --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,63 @@ +# Contributing to Jest + +Jest is one of Facebook's first open source projects that is both under very active development and is also being used to ship code to everybody on facebook.com. We're still working out the kinks to make contributing to this project as easy and transparent as possible, but we're not quite there yet. Hopefully this document makes the process for contributing clear and preempts some questions you may have. + +## Our Development Process + +Some of the core team will be working directly on GitHub. These changes will be public from the beginning. Other changesets will come via a bridge with Facebook's internal source control. This is a necessity as it allows engineers at Facebook outside of the core team to move fast and contribute from an environment they are comfortable in. + +### `master` is unsafe + +We will do our best to keep `master` in good shape, with tests passing at all times. But in order to move fast, we will make API changes that your application might not be compatible with. We will do our best to communicate these changes and always version appropriately so you can lock into a specific version if need be. + +### Pull Requests + +The core team will be monitoring for pull requests. When we get one, we'll run some Facebook-specific integration tests on it first. From here, we'll need to get another person to sign off on the changes and then merge the pull request. For API changes we may need to fix internal uses, which could cause some delay. We'll do our best to provide updates and feedback throughout the process. + +*Before* submitting a pull request, please make sure the following is done… + +1. Fork the repo and create your branch from `master`. +2. If you've added code that should be tested, add tests! +3. If you've changed APIs, update the documentation. +4. Ensure the test suite passes (`npm test`). +5. If you haven't already, complete the CLA. + +### Contributor License Agreement ("CLA") + +In order to accept your pull request, we need you to submit a CLA. You only need to do this once, so if you've done this for another Facebook open source project, you're good to go. If you are submitting a pull request for the first time, just let us know that you have completed the CLA and we can cross-check with your GitHub username. + +Complete your CLA here: + +## Bugs + +### Where to Find Known Issues + +We will be using GitHub Issues for our public bugs. We will keep a close eye on this and try to make it clear when we have an internal fix in progress. Before filing a new task, try to make sure your problem doesn't already exist. + +### Reporting New Issues + +The best way to get your bug fixed is to provide a reduced test case. jsFiddle, jsBin, and other sites provide a way to give live examples. + +### Security Bugs + +Facebook has a [bounty program](https://www.facebook.com/whitehat/) for the safe disclosure of security bugs. With that in mind, please do not file public issues and go through the process outlined on that page. + +## How to Get in Touch + +* IRC - [#jestjs on freenode](http://webchat.freenode.net/?channels=jestjs) +* Mailing list - [jestjs on Google Groups](http://groups.google.com/group/jestjs) + +## Coding Style + +* Use semicolons; +* Commas last, +* 2 spaces for indentation (no tabs) +* Prefer `'` over `"` +* `'use strict';` +* 80 character line length +* "Attractive" +* Do not use the optional parameters of `setTimeout` and `setInterval` + +## License + +By contributing to Jest, you agree that your contributions will be licensed under its BSD license. diff --git a/LICENSE b/LICENSE new file mode 100644 index 000000000000..6d3911c3ae1c --- /dev/null +++ b/LICENSE @@ -0,0 +1,30 @@ +BSD License + +For Jest software + +Copyright (c) 2014, Facebook, Inc. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + * Neither the name Facebook nor the names of its contributors may be used to + endorse or promote products derived from this software without specific + prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/PATENTS b/PATENTS new file mode 100644 index 000000000000..7ea76e64c528 --- /dev/null +++ b/PATENTS @@ -0,0 +1,23 @@ +Additional Grant of Patent Rights + +"Software" means the Jest software distributed by Facebook, Inc. + +Facebook hereby grants you a perpetual, worldwide, royalty-free, non-exclusive, +irrevocable (subject to the termination provision below) license under any +rights in any patent claims owned by Facebook, to make, have made, use, sell, +offer to sell, import, and otherwise transfer the Software. For avoidance of +doubt, no license is granted under Facebook’s rights in any patent claims that +are infringed by (i) modifications to the Software made by you or a third party, +or (ii) the Software in combination with any software or other technology +provided by you or a third party. + +The license granted hereunder will terminate, automatically and without notice, +for anyone that makes any claim (including by filing any lawsuit, assertion or +other action) alleging (a) direct, indirect, or contributory infringement or +inducement to infringe any patent: (i) by Facebook or any of its subsidiaries or +affiliates, whether or not such claim is related to the Software, (ii) by any +party if such claim arises in whole or in part from any software, product or +service of Facebook or any of its subsidiaries or affiliates, whether or not +such claim is related to the Software, or (iii) by any party relating to the +Software; or (b) that any right in any patent claim of Facebook is invalid or +unenforceable. diff --git a/README.md b/README.md new file mode 100644 index 000000000000..0300fee9bc41 --- /dev/null +++ b/README.md @@ -0,0 +1,90 @@ +# [Jest](http://facebook.github.io/jest/) + +Painless JavaScript Unit Testing + +- **Familiar Approach**: Built on top of Jasmine test framework, a familiar BDD testing environment + +- **Mock by Default**: Automatically mocks CommonJS modules returned by require(), making most existing code testable + +- **Short Feedback Loop**: Tests run in parallel and DOM apis are mocked so you can run tests on the command line + +## Getting Started + +Check out the [Getting Started](http://facebook.github.io/jest/docs/getting-started.html) tutorial. It's pretty simple! + +## API + + +#### The `jest` object + + - [`jest.autoMockOff()`](http://facebook.github.io/jest/docs/api.html#jest-automockoff) + - [`jest.autoMockOn()`](http://facebook.github.io/jest/docs/api.html#jest-automockon) + - [`jest.clearAllTimers()`](http://facebook.github.io/jest/docs/api.html#jest-clearalltimers) + - [`jest.dontMock(module)`](http://facebook.github.io/jest/docs/api.html#jest-dontmockmodulename) + - [`jest.genMockFromModule(moduleObj)`](http://facebook.github.io/jest/docs/api.html#jest-genmockfrommodule-moduleobj) + - [`jest.genMockFunction()`](http://facebook.github.io/jest/docs/api.html#jest-genmockfunction) + - [`jest.genMockFn()`](http://facebook.github.io/jest/docs/api.html#jest-genmockfn) + - [`jest.mock(moduleName)`](http://facebook.github.io/jest/docs/api.html#jest-mockmodule-name) + - [`jest.runAllTicks()`](http://facebook.github.io/jest/docs/api.html#jest-runallticks) + - [`jest.runAllTimers()`](http://facebook.github.io/jest/docs/api.html#jest-runalltimers) + - [`jest.runOnlyPendingTimers()`](http://facebook.github.io/jest/docs/api.html#jest-runonlypendingtimers) + - [`jest.setMock(moduleName, moduleExports)`](http://facebook.github.io/jest/docs/api.html#jest-setmock-modulename-moduleexports) + +#### Mock functions + + - [`mockFn.mock.calls`](http://facebook.github.io/jest/docs/api.html#mockfn-mock-calls) + - [`mockFn.mock.instances`](http://facebook.github.io/jest/docs/api.html#mockfn-mock-instances) + - [`mockFn.mockClear()`](http://facebook.github.io/jest/docs/api.html#mockfn-mockclear) + - [`mockFn.mockImplementation(fn)`](http://facebook.github.io/jest/docs/api.html#mockfn-mockimplementation-fn) + - [`mockFn.mockImpl(fn)`](http://facebook.github.io/jest/docs/api.html#mockfn-mockimpl-fn) + - [`mockFn.mockReturnThis()`](http://facebook.github.io/jest/docs/api.html#mockfn-mockreturnthis) + - [`mockFn.mockReturnValue(value)`](http://facebook.github.io/jest/docs/api.html#mockfn-mockreturnvalue-value) + - [`mockFn.mockReturnValueOnce(value)`](http://facebook.github.io/jest/docs/api.html#mockfn-mockreturnvalueonce-value) + +#### Config options + + - [`config.collectCoverage` [boolean]](http://facebook.github.io/jest/docs/api.html#config-collectcoverage-boolean) + - [`config.collectCoverageOnlyFrom` [object]](http://facebook.github.io/jest/docs/api.html#config-collectcoverageonlyfrom-object) + - [`config.modulePathIgnorePatterns` [array]](http://facebook.github.io/jest/docs/api.html#config-modulepathignorepatterns-array-string) + - [`config.rootDir` [string]](http://facebook.github.io/jest/docs/api.html#config-rootdir-string) + - [`config.scriptPreprocessor` [string]](http://facebook.github.io/jest/docs/api.html#config-scriptpreprocessor-string) + - [`config.setupEnvScriptFile` [string]](http://facebook.github.io/jest/docs/api.html#config-setupenvscriptfile-string) + - [`config.setupTestFrameworkScriptFile` [string]](http://facebook.github.io/jest/docs/api.html#config-setuptestframeworkscriptfile-string) + - [`config.testFileExtensions` [array]](http://facebook.github.io/jest/docs/api.html#config-testfileextensions-array-string) + - [`config.testPathDirs` [array]](http://facebook.github.io/jest/docs/api.html#config-testpathdirs-array-string) + - [`config.testPathIgnorePatterns` [array]](http://facebook.github.io/jest/docs/api.html#config-testpathignorepatterns-array-string) + - [`config.unmockedModulePathPatterns` [array]](http://facebook.github.io/jest/docs/api.html#config-unmockedmodulepathpatterns-array-string) + +#### Globally injected variables + + - [`jest`](http://facebook.github.io/jest/docs/api.html#the-jest-object) + - `require(module)` + - `require.requireActual(module)` + - `describe(name, fn)` + - `beforeEach(fn)` + - `afterEach(fn)` + - `it(name, fn)` + - `it.only(name, fn)` executes [only](https://github.com/davemo/jasmine-only) this test. Useful when investigating a failure + - `pit(name, fn)` [helper](https://www.npmjs.org/package/jasmine-pit) for promises + +#### `expect(value)` + + - `.not` inverse the next comparison + - `.toThrow(?message)` + - `.toBe(value)` comparison using `===` + - `.toEqual(value)` deep comparison. Use [`jasmine.any(type)`](http://jasmine.github.io/1.3/introduction.html#section-Matching_Anything_with_jasmine.any) to be softer + - `.toBeFalsy()` + - `.toBeTruthy()` + - `.toBeNull()` + - `.toBeUndefined()` + - `.toBeDefined()` + - `.toMatch(regexp)` + - `.toContain(string)` + - `.toBeCloseTo(number, delta)` + - `.toBeGreaterThan(number)` + - `.toBeLessThan(number)` + - `.toBeCalled()` + - `.toBeCalledWith(arg, um, ents)` + - `.lastCalledWith(arg, um, ents)` + + diff --git a/bin/jest.js b/bin/jest.js new file mode 100755 index 000000000000..22d0bc424447 --- /dev/null +++ b/bin/jest.js @@ -0,0 +1,261 @@ +#!/usr/bin/env node --harmony +/** + * Copyright (c) 2014, Facebook, Inc. All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +/* jshint node: true */ +"use strict"; + +var child_process = require('child_process'); +var defaultTestResultHandler = require('../src/defaultTestResultHandler'); +var fs = require('fs'); +var optimist = require('optimist'); +var path = require('path'); +var Q = require('q'); +var TestRunner = require('../src/TestRunner'); +var utils = require('../src/lib/utils'); + +function _findChangedFiles(dirPath) { + var deferred = Q.defer(); + + var args = + ['diff', '--name-only', '--diff-filter=ACMR']; + var child = child_process.spawn('git', args, {cwd: dirPath}); + + var stdout = ''; + child.stdout.on('data', function(data) { + stdout += data; + }); + + var stderr = ''; + child.stderr.on('data', function(data) { + stderr += data; + }); + + child.on('close', function(code) { + if (code === 0) { + stdout = stdout.trim(); + if (stdout === '') { + deferred.resolve([]); + } else { + deferred.resolve(stdout.split('\n').map(function(changedPath) { + return path.resolve(dirPath, changedPath); + })); + } + } else { + deferred.reject(code + ': ' + stderr); + } + }); + + return deferred.promise; +} + +function _onResultReady(config, result) { + return defaultTestResultHandler(config, result); +} + +function _onRunComplete(completionData) { + var numFailedTests = completionData.numFailedTests; + var numTotalTests = completionData.numTotalTests; + var startTime = completionData.startTime; + var endTime = completionData.endTime; + + console.log(numFailedTests + '/' + numTotalTests + ' tests failed'); + console.log('Run time: ' + ((endTime - startTime) / 1000) + 's'); +} + +function _verifyIsGitRepository(dirPath) { + var deferred = Q.defer(); + + child_process.spawn('git', ['rev-parse', '--git-dir'], {cwd: dirPath}) + .on('close', function(code) { + var isGitRepo = code === 0; + deferred.resolve(isGitRepo); + }); + + return deferred.promise; +} + +/** + * Takes a description string, puts it on the next line, indents it, and makes + * sure it wraps without exceeding 80chars + */ +function _wrapDesc(desc) { + var indent = '\n '; + return indent + desc.split(' ').reduce(function(wrappedDesc, word) { + var lastLineIdx = wrappedDesc.length - 1; + var lastLine = wrappedDesc[lastLineIdx]; + + var appendedLastLine = lastLine === '' ? word : (lastLine + ' ' + word); + + if (appendedLastLine.length > 80) { + wrappedDesc.push(word); + } else { + wrappedDesc[lastLineIdx] = appendedLastLine; + } + + return wrappedDesc; + }, ['']).join(indent); +} + +var argv = optimist + .usage('Usage: $0 [--config=] [TestPathRegExp]') + .options({ + config: { + alias: 'c', + description: _wrapDesc( + 'The path to a jest config file specifying how to find and execute ' + + 'tests.' + ), + type: 'string' + }, + coverage: { + description: _wrapDesc( + 'Indicates that test coverage information should be collected and ' + + 'reported in the output.' + ), + type: 'boolean' + }, + maxWorkers: { + alias: 'w', + description: _wrapDesc( + 'Specifies the maximum number of workers the worker-pool will spawn ' + + 'for running tests. This defaults to the number of the cores ' + + 'available on your machine. (its usually best not to override this ' + + 'default)' + ), + type: 'string' // no, optimist -- its a number.. :( + }, + onlyChanged: { + alias: 'o', + description: _wrapDesc( + 'Attempts to identify which tests to run based on which files have ' + + 'changed in the current repository. Only works if you\'re running ' + + 'tests in a git repository at the moment.' + ), + type: 'boolean' + }, + runInBand: { + alias: 'i', + description: _wrapDesc( + 'Run all tests serially in the current process (rather than creating ' + + 'a worker pool of child processes that run tests). This is sometimes ' + + 'useful for debugging, but such use cases are pretty rare.' + ), + type: 'boolean' + } + }) + .check(function(argv) { + if (argv.runInBand && argv.hasOwnProperty('maxWorkers')) { + throw ( + "Both --runInBand and --maxWorkers were specified, but these two " + + "options don't make sense together. Which is it?" + ); + } + + if (argv.onlyChanged && argv._.length > 0) { + throw ( + "Both --onlyChanged and a path pattern were specified, but these two " + + "options don't make sense together. Which is it? Do you want to run " + + "tests for changed files? Or for a specific set of files?" + ); + } + }) + .argv + +var config; +if (argv.config) { + config = utils.loadConfigFromFile(argv.config); +} else { + var cwd = process.cwd(); + + var pkgJsonPath = path.join(cwd, 'package.json'); + var pkgJson = fs.existsSync(pkgJsonPath) ? require(pkgJsonPath) : {}; + + // First look to see if there is a package.json file with a jest config in it + if (pkgJson.jest) { + if (!pkgJson.jest.hasOwnProperty('rootDir')) { + pkgJson.jest.rootDir = cwd; + } + config = utils.normalizeConfig(pkgJson.jest); + config.name = pkgJson.name; + config = Q(config); + + // If not, use a sane default config + } else { + config = Q(utils.normalizeConfig({ + name: cwd.replace(/[/\\]/g, '_'), + rootDir: cwd, + testPathDirs: [cwd], + testPathIgnorePatterns: ['/node_modules/.+'] + }, cwd)); + } +} + +config.done(function(config) { + var pathPattern = + argv._.length === 0 + ? /.*/ + : new RegExp(argv._.join('|')); + + var testRunnerOpts = {}; + if (argv.maxWorkers) { + testRunnerOpts.maxWorkers = argv.maxWorkers; + } + + if (argv.coverage) { + config.collectCoverage = true; + } + + var testRunner = new TestRunner(config, testRunnerOpts); + + function _runTestsOnPathPattern(pathPattern) { + return testRunner.findTestPathsMatching(pathPattern) + .then(function(matchingTestPaths) { + console.log('Found ' + matchingTestPaths.length + ' matching tests...'); + if (argv.runInBand) { + return testRunner.runTestsInBand(matchingTestPaths, _onResultReady); + } else { + return testRunner.runTestsParallel(matchingTestPaths, _onResultReady); + } + }) + .then(_onRunComplete); + } + + if (argv.onlyChanged) { + console.log('Looking for changed files...'); + + var testPathDirsAreGit = config.testPathDirs.map(_verifyIsGitRepository); + Q.all(testPathDirsAreGit).then(function(results) { + if (!results.every(function(result) { return result; })) { + console.error( + 'It appears that one of your testPathDirs does not exist ' + + 'with in a git repository. Currently --onlyChanged only works ' + + 'with git projects.\n' + ); + process.exit(1); + } + + return Q.all(config.testPathDirs.map(_findChangedFiles)); + }).then(function(changedPathSets) { + // Collapse changed files from each of the testPathDirs into a single list + // of changed file paths + var changedPaths = []; + changedPathSets.forEach(function(pathSet) { + changedPaths = changedPaths.concat(pathSet); + }); + return testRunner.findTestsRelatedTo(changedPaths); + }).done(function(affectedTestPaths) { + if (affectedTestPaths.length > 0) { + _runTestsOnPathPattern(new RegExp(affectedTestPaths.join('|'))).done(); + } else { + console.log('No tests to run!'); + } + }); + } else { + _runTestsOnPathPattern(pathPattern).done(); + } +}); diff --git a/docs/API.md b/docs/API.md new file mode 100644 index 000000000000..70f05ec8dce5 --- /dev/null +++ b/docs/API.md @@ -0,0 +1,304 @@ +--- +id: api +title: API Reference +layout: docs +category: Reference +permalink: docs/api.html +--- + +#### The `jest` object + + - [`jest.autoMockOff()`](#jest-automockoff) + - [`jest.autoMockOn()`](#jest-automockon) + - [`jest.clearAllTimers()`](#jest-clearalltimers) + - [`jest.dontMock(module)`](#jest-dontmockmodulename) + - [`jest.genMockFromModule(moduleObj)`](#jest-genmockfrommodule-moduleobj) + - [`jest.genMockFunction()`](#jest-genmockfunction) + - [`jest.genMockFn()`](#jest-genmockfn) + - [`jest.mock(moduleName)`](#jest-mockmodule-name) + - [`jest.runAllTicks()`](#jest-runallticks) + - [`jest.runAllTimers()`](#jest-runalltimers) + - [`jest.runOnlyPendingTimers()`](#jest-runonlypendingtimers) + - [`jest.setMock(moduleName, moduleExports)`](#jest-setmock-modulename-moduleexports) + +#### Mock functions + + - [`mockFn.mock.calls`](#mockfn-mock-calls) + - [`mockFn.mock.instances`](#mockfn-mock-instances) + - [`mockFn.mockClear()`](#mockfn-mockclear) + - [`mockFn.mockImplementation(fn)`](#mockfn-mockimplementation-fn) + - [`mockFn.mockImpl(fn)`](#mockfn-mockimpl-fn) + - [`mockFn.mockReturnThis()`](#mockfn-mockreturnthis) + - [`mockFn.mockReturnValue(value)`](#mockfn-mockreturnvalue-value) + - [`mockFn.mockReturnValueOnce(value)`](#mockfn-mockreturnvalueonce-value) + +#### Config options + + - [`config.collectCoverage` [boolean]](#config-collectcoverage-boolean) + - [`config.collectCoverageOnlyFrom` [object]](#config-collectcoverageonlyfrom-object) + - [`config.modulePathIgnorePatterns` [array]](#config-modulepathignorepatterns-array-string) + - [`config.rootDir` [string]](#config-rootdir-string) + - [`config.scriptPreprocessor` [string]](#config-scriptpreprocessor-string) + - [`config.setupEnvScriptFile` [string]](#config-setupenvscriptfile-string) + - [`config.setupTestFrameworkScriptFile` [string]](#config-setuptestframeworkscriptfile-string) + - [`config.testFileExtensions` [array]](#config-testfileextensions-array-string) + - [`config.testPathDirs` [array]](#config-testpathdirs-array-string) + - [`config.testPathIgnorePatterns` [array]](#config-testpathignorepatterns-array-string) + - [`config.unmockedModulePathPatterns` [array]](#config-unmockedmodulepathpatterns-array-string) + +#### Globally injected variables + + - [`jest`](#the-jest-object) + - `require(module)` + - `require.requireActual(module)` + - `describe(name, fn)` + - `beforeEach(fn)` + - `afterEach(fn)` + - `it(name, fn)` + - `it.only(name, fn)` executes [only](https://github.com/davemo/jasmine-only) this test. Useful when investigating a failure + - `pit(name, fn)` [helper](https://www.npmjs.org/package/jasmine-pit) for promises + +#### `expect(value)` + + - `.not` inverse the next comparison + - `.toThrow(?message)` + - `.toBe(value)` comparison using `===` + - `.toEqual(value)` deep comparison. Use [`jasmine.any(type)`](http://jasmine.github.io/1.3/introduction.html#section-Matching_Anything_with_jasmine.any) to be softer + - `.toBeFalsy()` + - `.toBeTruthy()` + - `.toBeNull()` + - `.toBeUndefined()` + - `.toBeDefined()` + - `.toMatch(regexp)` + - `.toContain(string)` + - `.toBeCloseTo(number, delta)` + - `.toBeGreaterThan(number)` + - `.toBeLessThan(number)` + - `.toBeCalled()` + - `.toBeCalledWith(arg, um, ents)` + - `.lastCalledWith(arg, um, ents)` + +----- + +### `jest.autoMockOff()` +Disables automatic mocking in the module loader. + +After this method is called, all `require()`s will return the real versions of each module (rather than a mocked version) + +This is usually useful when you have a scenario where the number of dependencies you want to mock is far less than the number of dependencies that you don't. For example, if you're writing a test for a module that uses a large number of dependencies that can be reasonably classified as "implementation details" of the module, then you likely do not want to mock them. + +Examples of dependencies that might be considered "implementation details" are things ranging from language built-ins (e.g. Array.prototype methods) to highly common utility methods (e.g. underscore/lo-dash, array utilities, class-builder libraries, etc) + +### `jest.autoMockOn()` +Re-enables automatic mocking in the module loader. + +It's worth noting that automatic mocking is on by default, so this method is only useful if that default has been changes (such as by previously calling [`jest.autoMockOff()`](#jest-automockoff)) + +### `jest.clearAllTimers()` +Removes any pending timers from the timer system. + +This means, if any timers have been scheduled (but have not yet executed), they will be cleared and will never have the opportunity to execute in the future. + +### `jest.dontMock(moduleName)` +Indicates that the module system should never return a mocked version of the specified module from `require()` (e.g. that it should always return the real module). + +The most common use of this API is for specifying the module a given test intends to be testing (and thus doesn't want automatically mocked). + +### `jest.genMockFromModule(moduleObj)` +Given a module exports object, use the automatic mocking system to generate a mocked version of the object for you. + +This is useful when you have an object that the module system does not know about, and you want to automatically generate a mock for it. + +### `jest.genMockFunction()` +Returns a freshly generated, unused [mock function](#mock-functions). + +### `jest.genMockFn()` +Shorthand alias for [`jest.genMockFunction`](#jest-genmockfunction). + +### `jest.mock(moduleName)` +Indicates that the module system should always return a mocked version of the specified module from `require()` (e.g. that it should never return the real module). + +This is normally useful under the circumstances where you have called [`jest.autoMockOff()`](#jest-automockoff), but still wish to specify that certain particular modules should be mocked by the module system. + +### `jest.runAllTicks()` +Exhausts the micro-task queue (usually interfaced in node via `process.nextTick`). + +When this API is called, all pending micro-tasks that have been queued via `process.nextTick` will be executed. Additionally, if those micro-tasks themselves schedule new micro-tasks, those will be continually exhausted until there are no more micro-tasks remaining in the queue. + +This is often useful for synchronously executing all pending promises in the system. + +### `jest.runAllTimers()` +Exhausts the macro-task queue (i.e., all tasks queued by `setTimeout()` and `setInterval()`). + +When this API is called, all pending "macro-tasks" that have been queued via `setTimeout()` or `setInterval()` will be executed. Additionally if those macro-tasks themselves schedule new macro-tasks, those will be continually exuasted until there are no more macro-tasks remaining in the queue. + +This is often useful for synchronously executing setTimeouts during a test in order to synchronously assert about some behavior that would only happen after the `setTimeout()` or `setInterval()` callbacks executed. See the [Timer mocks](/jest/docs/timer-mocks.html) doc for more information. + +### `jest.runOnlyPendingTimers()` +Executes only the macro-tasks that are currently pending (i.e., only the tasks that have been queued by `setTimeout()` or `setInterval()` up to this point). If any of the currently pending macro-tasks schedule new macro-tasks, those new tasks will not be executed by this call. + +This is useful for scenarios such as one where the module being tested schedules a `setTimeout()` whose callback scheduls another `setTimeout()` recursively (meaning the scheduling never stops). In these scenarios, it's useful to be able to run forward in time by a single step at a time. + +### `jest.setMock(moduleName, moduleExports)` +Explicitly supplies the mock object that the module system should return for the specified module. + +On occaison there are times where the automatically generated mock the module system would normally provide you isn't adequate enough for your testing needs. Normally under those circumstances you should write a [manual mock](/jest/docs/manual-mocks.html) that is more adequate for the module in question. However, on extremely rare occasions, even a manual mock isn't suitable for your purposes and you need to build the mock yourself inside your test. + +In these rare scenarios you can use this API to manually fill the slot in the module system's mock-module registry. + +### `mockFn.mock.calls` +An array that represents all calls that have been made into this mock function. Each call is represented by an array of arguments that were passed during the call. + +For example: A mock function `f` that has been called twice, with the arguments `f('arg1', 'arg2')`, and then with the arguments `f('arg3', 'arg4')` would have a `mock.calls` array that looks like this: + +```javascript +[ + ['arg1', 'arg2'], + ['arg3', 'arg4'] +] +``` + +### `mockFn.mock.instances` +An array that contains all the object instances that have been instantiated from this mock function. + +For example: A mock function that has been instantiated twice would have the following `mock.instances` array: + +```javascript +var mockFn = jest.genMockFunction(); + +var a = new mockFn(); +var b = new mockFn(); + +mockFn.mock.instances[0] === a; // true +mockFn.mock.instances[1] === b; // true +``` + +### `mockFn.mockClear()` +Resets all information stored in the [`mockFn.mock.calls`](#mockfn-mock-calls) and [`mockFn.mock.instances`](#mockfn-mock-instances) arrays. + +Often this is useful when you want to clean up a mock's usage data between two assertions. + +### `mockFn.mockImplementation(fn)` +Accepts a function that should be used as the implementation of the mock. The mock itself will still record all calls that go into and instances that come from itself – the only difference is that the implementation will also be executed when the mock is called. + +For example: + +```javascript +var mockFn = jest.genMockFunction().mockImplementation(function(scalar) { + return 42 + scalar; +}); + +var a = mockFn(0); +var b = mockFn(1); + +a === 42; // true +b === 43; // true + +mockFn.mock.calls[0][0] === 0; // true +mockFn.mock.calls[1][0] === 1; // true +``` + +### `mockFn.mockImpl(fn)` +Shorthand alias for [`mockFn.mockImplementation(fn)`](#mockfn-mockimplementation-fn). + +### `mockFn.mockReturnThis()` +Just a simple sugar function for: + +```javascript +.mockImplementation(function() { + return this; +}); +``` + +### `mockFn.mockReturnValue(value)` +Just a simple sugar function for: + +```javascript +.mockImplementation(function() { + return value; +}); +``` + +### `mockFn.mockReturnValueOnce(value)` +Just a simple sugar function for: + +```javascript +var valueReturned = false; +.mockImplementation(function() { + if (!valueReturned) { + valueReturned = true; + return value; + } +}); +``` + +### `config.collectCoverage` [boolean] +(default: `false`) + +Indicates whether the coverage information should be collected while executing the test. Because this retrofits all executed files with coverage collection statements, it may significantly slow down your tests. + +### `config.collectCoverageOnlyFrom` [object] +(default: `undefined`) + +An object that, when present, indicates a set of files for which coverage information should be collected. Any files not present in this set will not have coverage collected for them. Since there is a performance cost for each file that we collect coverage information from, this can help prune this cost down to only the files in which you care about coverage (such as the specific modules that you are testing). + +### `config.modulePathIgnorePatterns` [array] +(default: `["/node_modules/"]`) + +An array of regexp pattern strings that are matched against all module paths before those paths are to be considered 'visible' to the module loader. If a given module's path matches any of the patterns, it will not be `require()`-able in the test environment. + +### `config.rootDir` [string] +(default: The `pwd` the CLI is being executed from) + +The root directory that Jest should scan for tests and modules within. If you put your Jest config inside your `package.json` and want the root directory to be the root of your repo, the value for this config param will default to the directory of the `package.json`. + +Oftentimes, you'll want to set this to `'src'` or `'lib'`, corresponding to where in your repository the code is stored. + +### `config.scriptPreprocessor` [string] +(default: `undefined`) + +The path to a module that provides a synchronous function from pre-processing source files. For example, if you wanted to be able to use a new language feature in your modules or tests that isn't yet supported by node (like, for example, ES6 classes), you might plug in one of many transpilers that compile ES6 to ES5 here. + +Examples of such compilers include [jstransform](http://github.com/facebook/jstransform), [recast](http://github.com/facebook/recast), [regenerator](http://github.com/facebook/regenerator), and [traceur](https://github.com/google/traceur-compiler). + +### `config.setupEnvScriptFile` [string] +(default: `undefined`) + +The path to a module that runs some code to configure or set up the testing environment before each test. Since every test runs in it's own environment, this script will be executed in the testing environment immediately before executing the test code itself. + +It's worth noting that this code will execute *before* [`config.setupTestFrameworkScriptFile`](#config-setuptestframeworkscriptfile-string). + +### `config.setupTestFrameworkScriptFile` [string] +(default: `undefined`) + +The path to a module that runs some code to configure or set up the testing framework before each test. Since [`config.setupEnvScriptFile`](#config-setupenvscriptfile-string) executes before the test framework is installed in the environment, this script file presents you the opportunity of running some code immediately after the test framework has been installed in the environment. + +For example, Jest ships with several plug-ins to `jasmine` that work by monkey-patching the jasmine API. If you wanted to add even more jasmine plugins to the mix (or if you wanted some custom, project-wide matchers for example), you could do so in this module. + +### `config.testFileExtensions` [array] +(default: `['js']`) + +An array of file extensions that test files might have. Jest uses this when searching for tests to run. + +This is useful if, for example, you are writting test files using CoffeeScript with a `.coffee` file extension. In such a scenario, you can use `['js', 'coffee']` to make Jest find files that end in both `.js` and `.coffee`. (Don't for get to set up a coffeescript pre-processor using [`config.scriptPreprocessor`](#config-scriptpreprocessor-string) too!) + +### `config.testPathDirs` [array] +(default: The `pwd` the cli is being executed from) + +A list of paths to directories that Jest should use to search for tests in. + +There are times where you only want Jest to search in a single sub-directory (such as cases where you have a `src/` directory in your repo), but not the rest of the repo. + +### `config.testPathIgnorePatterns` [array] +(default: `["/node_modules/"]`) + +An array of regexp pattern strings that are matched against all test paths before executing the test. If the test path matches any of the patterns, it will be skipped. + +### `config.unmockedModulePathPatterns` [array] +(default: `[]`) + +An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them. If a module's path matches any of the patterns in this list, it will not be automatically mocked by the module loader. + +This is useful for some commonly used 'utility' modules that are almost always used as implementation details almost all the time (like underscore/lo-dash, etc). It's generally a best practice to keep this list as small as possible and always use explicit `jest.mock()`/`jest.dontMock()` calls in individual tests. Explicit per-test setup is far easier for other readers of the test to reason about the environment the test will run in. + +It is possible to override this setting in individual tests by explicitly calling `jest.mock()` at the top of the test file. diff --git a/docs/AutomaticMocking.md b/docs/AutomaticMocking.md new file mode 100644 index 000000000000..dab06c43aea7 --- /dev/null +++ b/docs/AutomaticMocking.md @@ -0,0 +1,119 @@ +--- +id: automatic-mocking +title: Automatic Mocking +layout: docs +category: Core Concepts +permalink: docs/automatic-mocking.html +next: mock-functions +--- + +In order to write an effective unit test, you want to be able to isolate a unit of code and test only that unit – nothing else. It is fairly common and good practice to consider a module such a unit, and this is where Jest excels. Jest makes isolating a module from its dependencies extremely easy by automatically generating mocks for each of the module's depenedencies and providing those mocks (rather than the real dependency modules) by default. + +Let's look at a concrete example: + +```javascript +// CurrentUser.js +var userID = 0; +module.exports = { + getID: function() { + return userID; + }, + setID: function(id) { + userID = id; + } +}; +``` + +```javascript +// login.js +var CurrentUser = require('./CurrentUser.js'); +``` + +If we run `login.js` with node, Jest will not become involved at all and the program will execute as you'd normally expect. However, if you run a unit test for the `login.js` file, Jest takes over and modifies `require()` such that the code behaves in the following way: + +```javascript +var CurrentUser = { + getID: jest.genMockFunction(), + setID: jest.genMockFunction() +}; +``` + +With this setup, you cannot accidentally rely on the implementation details of `CurrentUser.js` when testing `login.js` because all of the calls to the `CurrentUser` module are mocked. Additionally, testing becomes easier in practice because you don't have to write any boilerplate in your tests to setup mock objects for the dependencies you don't want to test. + +How does it work? +----------------- + +Jest actually implements its own version of the `require()` function in the testing environment. Jest's custom `require()` function loads the real module, inspects what it looks like, and then makes a mocked version based on what it saw and returns that. + +This means Jest is going to give you an object with the same shape as the real module, but with mocks for each of the exported values instead of the real values. + +```javascript +// Single function +jest.genMockFromModule(function() { /* ... */ }) + -> jest.genMockFunction(); + +// Object +jest.genMockFromModule({ + a: 1, + b: function() { /* ... */ }, + c: { + d: function() { /* ... */ } + } +}) -> { + a: 1, + b: jest.genMockFunction(), + c: { + d: jest.genMockFunction() + } +} +``` + +The automatic mocking system is also aware of classes/constructor functions with custom prototypes: + +```javascript +// User.js +function User() { + this.name = null; +} +User.prototype.setName = function(name) { + this.name = name; +}; + +// createCouple.js +var User = require('./User.js'); + +function createCouple(nameA, nameB) { + var userA = new User(); + userA.setName(nameA); + + var userB = new User(); + userB.setName(nameB); + + return [userA, userB]; +} +module.export = createCouple; +``` + +In this example, you can instantiate the mocked version of the constructor using `new` (just like you'd normally do), and all of the methods will also be mock functions as you would expect: + +``` +// __tests__/createCouple-test.js +jest.dontMock('../createCouple.js'); +var createCouple = require('../createCouple.js'); + +var couple = createCouple('userA', 'userB'); +expect(couple[0].setName.mock.calls.length).toEqual(1); +expect(couple[1].setName.mock.calls.length).toEqual(1); +``` + +An interesting detail to note is that while functions in the prototype are +normally shared across all instances, mock functions are not – they are +generated for each instance. + + +Conclusion +---------- + +A good goal to aim for when designing a system is to provide an API that is easy to use for 90% of use cases, while leaving the ability to accomplish the last 10% as well. In the case of Jest, automated mocking solves the rather uninteresting (but common) task of writing boilerplate to generate mocks for the unit you are testing. However, it is still possible to have complete control over what is mocked and what is not by providing `jest.mock()` and `jest.dontMock()` APIs for customization. + +Additionally, there are times where the automated mocking system isn't able to generate a mock that's sufficient enough for your needs. In these cases, you can [manually write a mock](/jest/docs/manual-mocks.html) that Jest should use (rather than trying to infer one itself). diff --git a/docs/CommonJSTesting.md b/docs/CommonJSTesting.md new file mode 100644 index 000000000000..9b6a97dd3a1c --- /dev/null +++ b/docs/CommonJSTesting.md @@ -0,0 +1,148 @@ +--- +id: common-js-testing +title: CommonJS Testing +layout: docs +category: Core Concepts +permalink: docs/common-js-testing.html +next: automatic-mocking +--- + +Dependency Injection was popularized in the JavaScript community by Angular as a +way to mock dependencies in order to make code testable. In this article, we're +going to see how Jest achieves the same result using a different approach. + +What is the problem? +-------------------- + +The [example](https://docs.angularjs.org/guide/unit-testing#dependency-injection) that Angular documentation uses to justify Dependency Injection is the following: + +```javascript +function doWork() { + var xhr = new XHR(); + xhr.open('POST', 'http://facebook.github.io/jest/'); + xhr.send(); +} +``` + +This function has a dependency on the `XHR` class and uses the global namespace +in order to get a reference to `XHR`. In order to mock this dependency, we have +to monkey patch the global object. + +```javascript +var oldXHR = XHR; +XHR = function MockXHR() {}; +doWork(); +// assert that MockXHR got called with the right arguments +XHR = oldXHR; // if you forget this bad things will happen +``` + +This small example shows two important concepts. We need a way to get a +reference to `XHR` and a way to provide two implementations: one for the normal +execution and one for testing. + +In this case, the solution to both concepts is to use the global object. It +works, but it's not ideal for reasons outlined in this article: [Brittle Global State & Singletons](http://misko.hevery.com/code-reviewers-guide/flaw-brittle-global-state-singletons/). + + +How does Angular solve this problem? +------------------------------------ + +In Angular, you write your code by passing dependencies as arguments: + +```javascript +function doWork(XHR) { + var xhr = new XHR(); + xhr.open('POST', 'http://facebook.github.io/jest/'); + xhr.send(); +} +``` + +It makes it very easy to write a test – you just pass your mocked version as +argument to your function: + +```javascript +var MockXHR = function() {}; +doWork(MockXHR); +// assert that MockXHR got called with the right arguments +``` + +But it's a pain to thread these constructor arguments throughout a real +application. So Angular uses an `injector` behind the scenes. This makes it +easy to create instances that automatically acquire their dependencies: + +``` +var injectedDoWork = injector.instantiate(doWork); + +// is the equivalent of writing + +function injectedDoWork() { + var XHR = injector.get('XHR'); + xhr.open('POST', 'http://facebook.github.io/jest/'); + xhr.send(); +} +``` + +Angular inspects the function and sees that it has one argument called `XHR`. +It then provides the value `injector.get('XHR')` for the variable `XHR`. + +In order to have a testable function in Angular, you must conform to this +specific pattern and pass it into Angular's DI framework before you can use it. + + +How does Jest solve this problem? +--------------------------------- + +Angular uses function arguments as a way to model dependencies and has to +implement its own module loader. Most large JavaScript applications already use +a module loader with the `require` function. In a CommonJS JavaScript app, the +example above would look more like this: + +``` +var XHR = require('XHR'); +function doWork() { + var xhr = new XHR(); + xhr.open('POST', 'http://facebook.github.io/jest/'); + xhr.send(); +} +``` + +The interesting aspect of this code is that the dependency on `XHR` is +marshalled by `require()`. The idea behind Jest is to use this as a seam for +inserting test doubles by implementing a special `require` in the testing +environment. + +``` +jest.mock('XHR'); +require('XHR'); // returns a mocked version of XHR + +jest.dontMock('XHR'); +require('XHR'); // returns the real XHR module +``` + +This allows you to write your tests like this: + +``` +jest.mock('XHR'); // note: by default, this is done automatically in Jest +doWork(); +var MockXHR = require('XHR'); +// assert that MockXHR got called with the right arguments +``` + +Conclusion +---------- + +Dependency Injection is a very powerful tool that lets you swap the +implementation of any module at any time. However, the vast majority of code +only deals with one implementation for production and one for testing. Jest is +designed to make this common case much simpler to test. + +Jest allows for mocking dependencies in the same way that Angular does, but +instead of building a proprietary module loader, it uses CommonJS. This enables +you to test any existing code that already uses CommonJS without having to +heavily refactor it to make it compatible with a another module system such as +Angular's. + +Fortunately, because Angular code has been designed for testing in any +environment, it is still possible to test Angular code using Jest. + + diff --git a/docs/GettingStarted.md b/docs/GettingStarted.md new file mode 100644 index 000000000000..1b9d59df895d --- /dev/null +++ b/docs/GettingStarted.md @@ -0,0 +1,54 @@ +--- +id: getting-started +title: Getting Started +layout: docs +category: Quick Start +permalink: docs/getting-started.html +next: tutorial +--- + +Consider a scenario where you want to test the following `sum.js` file: + +```javascript +// sum.js +function sum(value1, value2) { + return value1 + value2; +} +module.exports = sum; +``` + +We can get up and running with the following 4 steps: + +1. Create a directory `__tests__/` with a file `sum-test.js` + + ```javascript + // __tests__/sum-test.js + jest.dontMock('../sum'); + + describe('sum', function() { + it('adds 1 + 2 to equal 3', function() { + var sum = require('../sum'); + expect(sum(1, 2)).toBe(3); + }); + }); + ``` + +2. Run `npm install jest-cli --save-dev` + +3. Add the following to your `package.json` + + ```js + { + ... + "scripts": { + "test": "jest" + } + ... + } + ``` + +4. Run `npm test` + + ``` + [PASS] __tests__/sum-test.js (0.015s) + ``` diff --git a/docs/ManualMocks.md b/docs/ManualMocks.md new file mode 100644 index 000000000000..603887c145a0 --- /dev/null +++ b/docs/ManualMocks.md @@ -0,0 +1,108 @@ +--- +id: manual-mocks +title: Manual mocks +layout: docs +category: Reference +permalink: docs/manual-mocks.html +next: timer-mocks +--- + +Although autogeneration of mocks is convenient, there are behaviors it misses, such as [fluent interfaces](http://martinfowler.com/bliki/FluentInterface.html). Furthermore, providing useful helpers on mock versions of a module, especially a core module, promotes reuse and can help to hide implementation details. + +Manual mocks are defined by writing a module in a `__mocks__/` subdirectory immediately adjacent to the module. When a manual mock exists for a given module, Jest's module system will just use that instead of trying to automatically generating a mock. + +Here's a contrived example where we have a module that provides a summary of all the files in a given directory. + +```javascript +// FileSummarizer.js +var fs = require('fs'); + +function summarizeFilesInDirectorySync(directoryPath) { + return fs.readdirSync(directoryPath).map(function(fileName) { + return { + fileName: fileName, + directory: directoryPath + }; + }); + return directoryFileSummary; +} + +exports.summarizeFilesInDirectorySync = summarizeFilesInDirectorySync; +``` + +Since we'd like our tests to avoid actually hitting the disk (that's pretty slow and fragile), we create a manual mock for the `fs` module that implements custom versions of the `fs` APIs that we can build on for our tests: + +```javascript +// __mocks__/fs.js + +// Get the real (not mocked) version of the 'path' module +var path = require.requireActual('path'); + +// This is a custom function that our tests can use during setup to specify +// what the files on the "mock" filesystem should look like when any of the +// `fs` APIs are used. +var _mockFiles = {}; +function __setMockFiles(newMockFiles) { + _mockFiles = {}; + for (var file in newMockFiles) { + var dir = path.dirname(file); + + if (!_mockFiles[dir]) { + _mockFiles[dir] = []; + } + + _mockFiles[dir].push(path.basename(file)); + } +}; + +// A custom version of `readdirSync` that reads from the special mocked out +// file list set via __setMockFiles +function readdirSync(directoryPath) { + return _mockFiles[directoryPath] || []; +}; + +exports.__setMockFiles = __setMockFiles; +exports.readdirSync = readdirSync; +``` + +Now we write our test: + +```javascript +jest.dontMock('../FileSummarizer'); + +describe('FileSummarizer', function() { + describe('listFilesInDirectorySync', function() { + var MOCK_FILE_INFO = { + '/path/to/file1.js': + 'console.log("file1 contents");', + + '/path/to/file2.txt': + "file2 contents" + }; + + beforeEach(function() { + // Set up some mocked out file info before each test + require('fs').__setMockFiles(MOCK_FILE_INFO); + }); + + it('includes all files in the directory in the summary', function() { + var FileSummarizer = require('../FileSummarizer'); + var fileSummary = FileSummarizer.summarizeFilesInDirectorySync( + '/path/to' + ); + + expect(fileSummary.length).toBe(2); + }); + }); +}); +``` + +As you can see, it's sometimes useful to do more than what the automatic mocker is capable of doing for us. Of course, one downside to manual mocks is that they're manual – meaning you have to manually update them any time the module they are mocking changes. Because of this, it's best to use the automatic mocker when it works for your needs. + + +Testing manual mocks +------------- + +It's generally an anti-pattern to implement an elaborate, stateful mock for a module. Before going down this route, consider covering the real module completely with tests and then whitelisting it with [`config.unmockedModulePathPatterns`](/jest/docs/api.html#config-unmockedmodulepathpatterns-array-string), so that any tests that `require()` it will always get the real implementation (rather than a complicated mock version). + +In cases where this kind of elaborate mock is unavoidable, it's not necessarily a bad idea to write a test that ensures that the mock and the actual implementation are in sync. Luckily, this is relatively easy to do with the API provided by `jest`, which allows you to explicitly require both the real module (using `require.requireActual()`) and the manually mocked implementation of the module (using `require()`) in a single test! diff --git a/docs/MockFunctions.md b/docs/MockFunctions.md new file mode 100644 index 000000000000..f8f43cae1c91 --- /dev/null +++ b/docs/MockFunctions.md @@ -0,0 +1,185 @@ +--- +id: mock-functions +title: Mock functions +layout: docs +category: Reference +permalink: docs/mock-functions.html +next: manual-mocks +--- + +Mock functions make it easy to test the links between code by erasing the actual +implementation of a function, capturing calls to the function (and the +parameters passed in those calls), capturing instances of constructor functions +when instantiated with `new`, and allowing test-time configuration of return +values. + +There are two ways to get your hands on a mock functions: Either by +`require()`ing a mocked component (See [Automatic Mocking](/jest/docs/automatic-mocking.html)) +or by explicitly requesting one from `jest.genMockFunction()` in your test: + +```javascript +var myMock = jest.genMockFunction(); +myMock('1'); +myMock('a', 'b'); +console.log(myMock.mock.calls); +> [ [1], ['a', 'b'] ] +``` + +## `.mock` property + +All mock functions have this special `.mock` property, which is where data about +how the function has been called is kept. The `.mock` property also tracks the +value of `this` for each call, so it is possible to inspect this as well: + +```javascript +var myMock = jest.genMockFunction(); + +var a = new myMock(); +var b = {}; +var bound = myMock.bind(b); +bound(); + +console.log(myMock.mock.instances); +> [ , ] +``` + +These mock members are very useful in tests to assert how these functions get +called, or instantiated: + +```javascript +// The function was called exactly once +expect(someMockFunction.mock.calls.length).toBe(1); + +// The first arg of the first call to the function was 'first arg' +expect(someMockFunction.mock.calls[0][0]).toBe('first arg'); + +// The second arg of the first call to the function was 'second arg' +expect(someMockFunction.mock.calls[0][1]).toBe('second arg'); + +// This function was instantiated exactly twice +expect(someMockFunction.mock.instances.length).toBe(2); + +// The object returned by the first instantiation of this function +// had a `name` property whose value was set to 'test' +expect(someMockFunction.mock.instances[0].name).toEqual('test'); +``` + +## Mock Return Values + +Mock functions can also be used to inject test values into your code during a +test: + +```javascript +var myMock = jest.genMockFunction(); +console.log( myMock() ); +> undefined + +myMock.mockReturnValueOnce(10); + .mockReturnValueOnce('x'); + .mockReturnValue(true); + +console.log(myMock(), myMock(), myMock(), myMock()); +> 10, 'x', true, true +``` + +Mock functions are also very effective in code that uses a functional +continuation-passing style. Code written in this style helps avoid the need for +complicated stubs that recreate behavior of the real component they're standing +in for, in favor of injecting values directly into the test right before they're +used. + +```javascript +var Filter = require('Filter'); + +var filterTestFn = jest.genMockFunction(); + +// Make the mock return `true` for the first call, +// and `false` for the second call +filterTestFunction + .mockReturnValueOnce(true) + .mockReturnValueOnce(false); + +var result = [11, 12].filter(filterTestFn); + +console.log(result); +> [11] +console.log(filterTestFn.mock.calls); +> [ [11], [12] ] +``` + +Most real-world examples actually involve getting ahold of a mock function on a +dependent component and configuring that, but the technique is the same. In +these cases, try to avoid the temptation to implement logic inside of any +function that's not directly being tested. + +## Mock Implementations + +Still, there are cases where it's useful to go beyond the ability to specify +return values and full-on replace the implementation of a mock function. This +can be done with the `mockImplementation` method on mock functions: + +```javascript +var myObj = { + myMethod: jest.genMockFunction().mockImplementation(function() { + // do something stateful + return this; + }); +}; + +myObj.myMethod(1).myMethod(2); +``` + +For cases where we have methods that are typically chained (and thus always need +to return `this`), we have a sugary API to simplify this in the form of a +`.mockReturnThis()` function that also sits on all mocks: + +```javascript +var myObj = { + myMethod: jest.genMockFunction().mockReturnThis() +}; + +// is the same as + +var myObj = { + myMethod = jest.genMockFunction().mockImplementation(function() { + return this; + }); +}; +``` + +## Custom Matchers + +Finally, in order to make it simpler to assert how mock functions have been +called, we've added some custom matcher functions to jasmine for you: + +```javascript +// The mock function was called at least once +expect(mockFunc).toBeCalled(); + +// The mock function was called at least once with the specified args +expect(mockFunc).toBeCalledWith(arg1, arg2); + +// The last call to the mock function was called with the specified args +expect(mockFunc).lastCalledWith(arg1, arg2); +``` + +These matchers are really just sugar for common forms of inspecting the `.mock` +property. You can always do this manually yourself if that's more to your taste +or if you need to do something more specific: + +```jasmine +// The mock function was called at least once +expect(mockFunc.mock.calls.length).toBeGreaterThan(0); + +// The mock function was called at least once with the specified args +expect(mockFunc.mock.calls).toContain([arg1, arg2]); + +// The last call to the mock function was called with the specified args +expect(mockFunc.mock.calls[mockFunc.mock.calls.length - 1]).toEqual( + [arg1, arg2] +); + +// The first arg of the last call to the mock funciton was called with `42` +// (note that there is no sugar helper for this specific of an assertion) +expect(mockFunc.mock.calls[mockFunc.mock.calls.length - 1][0]).toBe(42); +``` diff --git a/docs/TimerMocks.md b/docs/TimerMocks.md new file mode 100644 index 000000000000..f536c891d6f2 --- /dev/null +++ b/docs/TimerMocks.md @@ -0,0 +1,126 @@ +--- +id: timer-mocks +title: Timer mocks +layout: docs +category: Reference +permalink: docs/timer-mocks.html +next: api +--- + +The native timer functions (i.e., `setTimeout`, `setInterval`, `clearTimeout`, +`clearInterval`) are less than ideal for a testing environment since they depend +on real time to elapse. To resolve this issue, Jest provides you with mocked +timer functions and APIs that allow you to control the passage of time. +[Great Scott!](https://www.youtube.com/watch?v=5gVv10J4nio) + +```javascript +// timerGame.js +function timerGame(callback) { + console.log('Ready....go!'); + + setTimeout(function() { + console.log("Time's up -- stop!"); + callback && callback(); + }, 1000); +} + +module.exports = timerGame; +``` +```javascript +// __tests__/timerGame-test.js +jest.dontMock('../timerGame'); + +describe('timerGame', function() { + it('waits 1 second before ending the game', function() { + var timerGame = require('../timerGame'); + timerGame(); + + expect(setTimeout.mock.calls.length).toBe(1); + expect(setTimeout.mock.calls[0][1]).toBe(1000); + }); +}); +``` + +## Run All Timers + +Another test we might want to write for this module is one that asserts that the +callback is called after 1 second. To do this, we're going to use Jest's timer +control APIs to fast-forward time right in the middle of the test: + +```javascript + it('calls the callback after 1 second', function() { + var timerGame = require('../timerGame'); + var callback = jest.genMockFunction(); + + timerGame(callback); + + // At this point in time, the callback should not have been called yet + expect(callback).not.toBeCalled(); + + // Fast-forward until all timers have been executed + jest.runAllTimers(); + + // Now our callback should have been called! + expect(callback).toBeCalled() + expect(callback.mock.calls.length).toBe(1); + }); +``` + +## Run Pending Timers + +There are also scenarios where you might have a recursive timer -- that is a +timer that sets a new timer in its own callback. For these, running all the +timers would be an endless loop… so something like `jest.runAllTimers()` is not +desirable. For these cases you might use `jest.runOnlyPendingTimers()`: + +```javascript +// infiniteTimerGame.js +function infiniteTimerGame(callback) { + console.log('Ready....go!'); + + setTimeout(function() { + console.log('Times up! 10 seconds before the next game starts...'); + callback && callback(); + + // Schedule the next game in 10 seconds + setTimeout(function() { + infiniteTimerGame(callback); + }, 10000); + + }, 1000); +} + +module.exports = infiniteTimerGame; +``` +```javascript +// __tests__/infiniteTimerGame-test.js +jest.dontMock('../infiniteTimerGame'); + +describe('infiniteTimerGame', function() { + it('schedules a 10-second timer after 1 second', function() { + var infiniteTimerGame = require('../infiniteTimerGame'); + var callback = jest.genMockFunction(); + + infiniteTimerGame(callback); + + // At this point in time, there should have been a single call to + // setTimeout to schedule the end of the game in 1 second. + expect(setTimeout.mock.calls.length).toBe(1); + expect(setTimeout.mock.calls[0][1]).toBe(1000); + + // Fast forward and exhaust only currently pending timers + // (but not any new timers that get created during that process) + jest.runOnlyPendingTimers(); + + // At this point, our 1-second timer should have fired it's callback + expect(callback).toBeCalled(); + + // And it should have created a new timer to start the game over in + // 10 seconds + expect(setTimeout.mock.calls.length).toBe(2); + expect(setTimeout.mock.calls[1][1]).toBe(10000); + }); +}); +``` +Lastly, it may occasionally be useful in some tests to be able to clear all of +the pending timers. For this, we have `jest.clearTimers()`. diff --git a/docs/Tutorial.md b/docs/Tutorial.md new file mode 100644 index 000000000000..b2b39a309776 --- /dev/null +++ b/docs/Tutorial.md @@ -0,0 +1,132 @@ +--- +id: tutorial +title: Tutorial +layout: docs +category: Quick Start +permalink: docs/tutorial.html +next: tutorial-jquery +--- + + +To begin, let's see how we might test the following function (borrowed from [this great article on testing asynchronous functions](http://martinfowler.com/articles/asyncJS.html)). It does an Ajax request to get the current user as JSON, transforms this JSON into a new object, and passes it to the callback. Very typical code: + +```javascript +// fetchCurrentUser.js +var $ = require('jquery'); + +function parseUserJson(userJson) { + return { + loggedIn: true, + fullName: userJson.firstName + ' ' + userJson.lastName + }; +} + +function fetchCurrentUser(callback) { + return $.ajax({ + type: 'GET', + url: 'http://example.com/currentUser', + done: function(userJson) { + callback(parseUserJson(userJson)); + } + }); +} + +module.exports = fetchCurrentUser; +``` + +In order to write a test for this module, we need to create a `__tests__/` +directory where the file `fetchCurrentUser.js` is. In this folder, we create a +file called `fetchCurrentUser-test.js` and we write our test in it: + +```javascript +// __tests__/fetchCurrentUser-test.js +jest.dontMock('../fetchCurrentUser.js'); + +describe('fetchCurrentUser', function() { + it('calls into $.ajax with the correct params', function() { + var $ = require('jquery'); + var fetchCurrentUser = require('../fetchCurrentUser'); + + // Call into the function we want to test + function dummyCallback() {} + fetchCurrentUser(dummyCallback); + + // Now make sure that $.ajax was properly called during the previous + // 2 lines + expect($.ajax).toBeCalledWith({ + type: 'GET', + url: 'http://example.com/currentUser', + done: jasmine.any(Function) + }); + }); +}); +``` + +When Jest runs, it runs any tests found in `__tests__` directories within the source tree. + +The first line is very important: `jest.dontMock('../fetchCurrentUser.js');`. +By default, Jest automatically makes all calls to `require()` return a mocked +version of the real module – so we need to tell Jest not to mock the file we +want to test or else `require('../fetchCurrentUser')` will return a mock. + +In our first test, we want to confirm that calling `fetchCurrentUser()` +properly incurs a call into `$.ajax()` with the parameters we expect. To do +this, we just call `fetchCurrentUser()` with a dummy callback function, and +then simply inspect the `$.ajax` mock to verify that it was called with the +correct parameters. + +Woohoo! We've written our first test. But we're not quite done: We would still +like to test that the callback we are passing in is indeed called back when the +`$.ajax` request has completed. To test this, we can do the following: + +```javascript + it('calls the callback when $.ajax requests are finished', function() { + var $ = require('jquery'); + var fetchCurrentUser = require('../fetchCurrentUser'); + + // Create a mock function for our callback + var callback = jest.genMockFunction(); + fetchCurrentUser(callback); + + // Now we emulate the process by which `$.ajax` would execute its own + // callback + $.ajax.mock.calls[0 /*first call*/][0 /*first argument*/].done({ + firstName: 'Bobby', + lastName: '");DROP TABLE Users;--' + }); + + // And finally we assert that this emulated call by `$.ajax` incurred a + // call back into the mock function we provided as a callback + expect(callback.mock.calls[0/*first call*/][0/*first arg*/]).toEqual({ + loggedIn: true, + fullName: 'Bobby ");DROP TABLE Users;--' + }); + }); +``` + +In order for `fetchCurrentUser` to compute the result to be passed in to the +callback, `fetchCurrentUser` will call in to one of it's dependencies: `$.ajax`. +Since Jest has mocked this dependency for us, it's easy to inspect all of the +interactions with `$.ajax` that occurred during our test. + +At this point, you might be wondering how Jest was able to decide what the mock for the `jQuery` module should look like. The answer is simple: Jest secretly requires the real module, inspects what it looks like, and then builds a mocked version of what it saw. This is how Jest knew that there should be a `$.ajax` property, and that that property should be a mock function. + +In Jest, all mock functions have a `.mock` property that stores all the +interactions with the function. In the above case, we are reading from +`mock.calls`, which is an array that contains information about each time the +function was called, and what arguments each of those calls had. + +Now it is time to see if it worked: + +``` +> npm test +[PASS] jest/examples/__tests__/fetchCurrentUser-test.js (0.075s) +``` + +Woohoo! That's it, we just tested this asynchronous function. One thing to +notice is that the code we've written is entirely synchronous. This is one of +the strengths of using mock functions in this way: the code you write in tests +is always straightfoward and imperative, no matter if the code being tested is +synchronous or asynchronous. + +The code for this example is available at [examples/tutorial/](https://github.com/facebook/jest/tree/master/examples/tutorial). diff --git a/docs/TutorialCoffeeScript.md b/docs/TutorialCoffeeScript.md new file mode 100644 index 000000000000..9562ba4821fe --- /dev/null +++ b/docs/TutorialCoffeeScript.md @@ -0,0 +1,58 @@ +--- +id: tutorial-coffeescript +title: Tutorial – CoffeeScript +layout: docs +category: Quick Start +permalink: docs/tutorial-coffeescript.html +next: tutorial-react +--- + +Jest doesn't come with builtin support for CoffeeScript but can easily be configured in order to make it work. To use CoffeeScript, you need to tell Jest to look for `*.coffee` files and to run them through the CoffeeScript compiler when requiring them. Here's how to set it up with Jest: + + +```javascript +// package.json + "dependencies": { + "coffee-script": "*" + }, + "jest": { + "scriptPreprocessor": "preprocessor.js", + "testFileExtensions": ["coffee", "js"] + } +``` + +``` +// preprocessor.js +var coffee = require('coffee-script'); + +module.exports = { + process: function(src, path) { + if (path.match(/\.coffee$/)) { + return coffee.compile(src, {'bare': true}); + } + return src; + } +}; +``` + +Once you have this, you can use CoffeeScript with Jest in any place you would have written JavaScript. You can write all of your tests and code in CoffeeScript or mix and match, using CoffeeScript in some files and plain JavaScript in others. + + +```javascript +// sum.coffee +sum = (a, b) -> + a + b +module.exports = sum +``` + +```javascript +// __tests__/sum-test.coffee +jest.dontMock '../sum.coffee' +describe 'sum', -> + it 'adds 1 + 2 to equal 3', -> + sum = require '../sum.coffee' + expect(sum 1, 2).toBe 3 +``` + +The code for this example is availabe at [examples/coffeescript/](https://github.com/facebook/jest/tree/master/examples/coffeescript). + diff --git a/docs/TutorialReact.md b/docs/TutorialReact.md new file mode 100644 index 000000000000..450dadbd44c6 --- /dev/null +++ b/docs/TutorialReact.md @@ -0,0 +1,104 @@ +--- +id: tutorial-react +title: Tutorial – React +layout: docs +category: Quick Start +permalink: docs/tutorial-react.html +next: common-js-testing +--- + +At Facebook, we use Jest to test [React](http://facebook.github.io/react/) applications. Let's implement a simple checkbox which swaps between two labels: + +```javascript +// CheckboxWithLabel.js + +/** @jsx React.DOM */ +var React = require('react/addons'); +var CheckboxWithLabel = React.createClass({ + getInitialState: function() { + return { isChecked: false }; + }, + onChange: function() { + this.setState({isChecked: !this.state.isChecked}); + }, + render: function() { + return ( + + ); + } +}); +module.exports = CheckboxWithLabel; +``` + +The test code is pretty straightforward; we use React's [TestUtils](http://facebook.github.io/react/docs/test-utils.html) in order to manipulate React components. + +```javascript +// __tests__/CheckboxWithLabel-test.js + +/** @jsx React.DOM */ +jest.dontMock('../CheckboxWithLabel.js'); +describe('CheckboxWithLabel', function() { + it('changes the text after click', function() { + var React = require('react/addons'); + var CheckboxWithLabel = require('../CheckboxWithLabel.js'); + var TestUtils = React.addons.TestUtils; + + // Render a checkbox with label in the document + var checkbox = ; + TestUtils.renderIntoDocument(checkbox); + + // Verify that it's Off by default + var label = TestUtils.findRenderedDOMComponentWithTag( + checkbox, 'label'); + expect(label.getDOMNode().textContent).toEqual('Off'); + + // Simulate a click and verify that it is now On + var input = TestUtils.findRenderedDOMComponentWithTag( + checkbox, 'input'); + TestUtils.Simulate.change(input); + expect(label.getDOMNode().textContent).toEqual('On'); + }); +}); +``` + +## Setup + +Since we are writing code using JSX, a bit of one-time setup is required to make the test working: + +```javascript +// package.json + "dependencies": { + "react": "*", + "react-tools": "*" + }, + "jest": { + "scriptPreprocessor": "/preprocessor.js", + "unmockedModulePathPatterns": ["/node_modules/react"], + "testPathIgnorePatterns": ["/node_modules"] + } +``` + +To enable the JSX transforms, we need to add a simple preprocessor file to run JSX over our source and test files using `react-tools` when they're required: + +```javascript +// preprocessor.js +var ReactTools = require('react-tools'); +module.exports = { + process: function(src) { + return ReactTools.transform(src); + } +}; +``` + +React is designed to be tested without being mocked and ships with `TestUtils` to help. Therefore, we use `unmockedModulePathPatterns` to prevent React from being mocked. + +Finally, React has a lot of tests written in Jest. Since you probably don't want to run React tests in your application, you can blacklist all the tests from `node_modules` using `testPathIgnorePatterns`. + +The code for this example is available at [examples/react/](https://github.com/facebook/jest/tree/master/examples/react). diff --git a/docs/TutorialjQuery.md b/docs/TutorialjQuery.md new file mode 100644 index 000000000000..2747afafb114 --- /dev/null +++ b/docs/TutorialjQuery.md @@ -0,0 +1,77 @@ +--- +id: tutorial-jquery +title: Tutorial – jQuery +layout: docs +category: Quick Start +permalink: docs/tutorial-jquery.html +next: tutorial-coffeescript +--- + +Another class of functions that is often considered difficult to test is code that directly manipulates the DOM. Let's see how we can test the following snippet of jQuery code that listens to a click event, fetches some data asynchronously and sets the content of a span. + +```javascript +// displayUser.js +var $ = require('jquery'); +var fetchCurrentUser = require('./fetchCurrentUser.js'); + +$('#button').click(function() { + fetchCurrentUser(function(user) { + var loggedText = 'Logged ' + (user.loggedIn ? 'In' : 'Out'); + $('#username').text(user.fullName + ' - ' + loggedText); + }); +}); +``` + +Again, we create a test file in the `__tests__/` folder: + +```javascript +// __tests__/displayUser-test.js +jest + .dontMock('../displayUser.js') + .dontMock('jquery'); + +describe('displayUser', function() { + it('displays a user after a click', function() { + // Set up our document body + document.body.innerHTML = + '
' + + ' ' + + '
'; + + var displayUser = require('../displayUser'); + var $ = require('jquery'); + var fetchCurrentUser = require('../fetchCurrentUser'); + + // Tell the fetchCurrentUser mock function to automatically it's + // callback with some data + fetchCurrentUser.mockImplementation(function(cb) { + cb({ + loggedIn: true, + fullName: 'Johnny Cash' + }); + }); + + // Use jquery to emulate a click on our button + $('#button').click(); + + // Assert that the fetchCurrentUser function was called, and that the + // #username span's innter text was updated as we'd it expect. + expect(fetchCurrentUser).toBeCalled(); + expect($('#username').text()).toEqual('Johnny Cash - Logged In'); + }); +}); +``` + +The function being tested adds an event listener on the `#button` DOM element, so +we need to setup our DOM correctly for the test. Jest ships with `jsdom` which +simulates a DOM environment as if you were in the browser. This means that every +DOM API that we call can be observed in the same way it would be observed in a +browser! + +Since we are interested in testing that `displayUser.js` makes specific changes +to the DOM, we tell Jest not to mock our `jquery` dependency. This lets +`displayUser.js` actually mutate the DOM, and it gives us an easy means of +querying the DOM in our test. + +The code for this example is available at [examples/jquery/](https://github.com/facebook/jest/tree/master/examples/jquery). diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 000000000000..45da88d6ef37 --- /dev/null +++ b/examples/README.md @@ -0,0 +1,6 @@ +In order to run the examples, go to the respective folder and run the following two commands + +``` +npm install +npm test +``` diff --git a/examples/coffeescript/__tests__/sum-test.coffee b/examples/coffeescript/__tests__/sum-test.coffee new file mode 100644 index 000000000000..aef85fbe331f --- /dev/null +++ b/examples/coffeescript/__tests__/sum-test.coffee @@ -0,0 +1,12 @@ +jest + .dontMock '../sum.coffee' + .dontMock '../sum.js' + +describe 'sum', -> + it 'adds 1 + 2 to equal 3 in CoffeeScript', -> + sum = require '../sum.coffee' + expect(sum 1, 2).toBe 3 + + it 'adds 1 + 2 to equal 3 in JavaScript', -> + sum = require '../sum.js' + expect(sum 1, 2).toBe 3 diff --git a/examples/coffeescript/__tests__/sum-test.js b/examples/coffeescript/__tests__/sum-test.js new file mode 100644 index 000000000000..2820da7821c2 --- /dev/null +++ b/examples/coffeescript/__tests__/sum-test.js @@ -0,0 +1,15 @@ +jest + .dontMock('../sum.coffee') + .dontMock('../sum.js'); + +describe('sum', function() { + it('adds 1 + 2 to equal 3 in CoffeeScript', function() { + var sum = require('../sum.coffee'); + expect(sum(1, 2)).toBe(3); + }); + + it('adds 1 + 2 to equal 3 in JavaScript', function() { + var sum = require('../sum.js'); + expect(sum(1, 2)).toBe(3); + }); +}); diff --git a/examples/coffeescript/package.json b/examples/coffeescript/package.json new file mode 100644 index 000000000000..af260ec447da --- /dev/null +++ b/examples/coffeescript/package.json @@ -0,0 +1,12 @@ +{ + "dependencies": { + "coffee-script": "*" + }, + "scripts": { + "test": "node ../../bin/jest.js" + }, + "jest": { + "scriptPreprocessor": "/preprocessor.js", + "testFileExtensions": ["coffee", "js"] + } +} diff --git a/examples/coffeescript/preprocessor.js b/examples/coffeescript/preprocessor.js new file mode 100644 index 000000000000..a7a0118ab13c --- /dev/null +++ b/examples/coffeescript/preprocessor.js @@ -0,0 +1,10 @@ +var coffee = require('coffee-script'); + +module.exports = { + process: function(src, path) { + if (path.match(/\.coffee$/)) { + return coffee.compile(src, {'bare': true}); + } + return src; + } +}; diff --git a/examples/coffeescript/sum.coffee b/examples/coffeescript/sum.coffee new file mode 100644 index 000000000000..f0a50f0b5d89 --- /dev/null +++ b/examples/coffeescript/sum.coffee @@ -0,0 +1,4 @@ +sum = (a, b) -> + a + b + +module.exports = sum diff --git a/examples/coffeescript/sum.js b/examples/coffeescript/sum.js new file mode 100644 index 000000000000..afa261cda592 --- /dev/null +++ b/examples/coffeescript/sum.js @@ -0,0 +1,5 @@ +function sum(a, b) { + return a + b; +} + +module.exports = sum; diff --git a/examples/getting_started/__tests__/sum-test.js b/examples/getting_started/__tests__/sum-test.js new file mode 100644 index 000000000000..ead8bcd7865e --- /dev/null +++ b/examples/getting_started/__tests__/sum-test.js @@ -0,0 +1,8 @@ +jest.dontMock('../sum'); + +describe('sum', function() { + it('adds 1 + 2 to equal 3', function() { + var sum = require('../sum'); + expect(sum(1, 2)).toBe(3); + }); +}); diff --git a/examples/getting_started/package.json b/examples/getting_started/package.json new file mode 100644 index 000000000000..0ea218e142f6 --- /dev/null +++ b/examples/getting_started/package.json @@ -0,0 +1,5 @@ +{ + "scripts": { + "test": "node ../../bin/jest.js" + } +} diff --git a/examples/getting_started/sum.js b/examples/getting_started/sum.js new file mode 100644 index 000000000000..5c1603904fd7 --- /dev/null +++ b/examples/getting_started/sum.js @@ -0,0 +1,5 @@ +function sum(value1, value2) { + return value1 + value2; +} + +module.exports = sum; diff --git a/examples/jquery/__tests__/displayUser-test.js b/examples/jquery/__tests__/displayUser-test.js new file mode 100644 index 000000000000..0f25286af94e --- /dev/null +++ b/examples/jquery/__tests__/displayUser-test.js @@ -0,0 +1,35 @@ +jest + .dontMock('../displayUser.js') + .dontMock('jquery'); + +describe('displayUser', function() { + it('displays a user after a click', function() { + // Set up our document body + document.body.innerHTML = + '
' + + ' ' + + '
'; + + var displayUser = require('../displayUser'); + var $ = require('jquery'); + var fetchCurrentUser = require('../fetchCurrentUser'); + + // Tell the fetchCurrentUser mock function to automatically it's + // callback with some data + fetchCurrentUser.mockImplementation(function(cb) { + cb({ + loggedIn: true, + fullName: 'Johnny Cash' + }); + }); + + // Use jquery to emulate a click on our button + $('#button').click(); + + // Assert that the fetchCurrentUser function was called, and that the + // #username span's innter text was updated as we'd it expect. + expect(fetchCurrentUser).toBeCalled(); + expect($('#username').text()).toEqual('Johnny Cash - Logged In'); + }); +}); diff --git a/examples/jquery/displayUser.js b/examples/jquery/displayUser.js new file mode 100644 index 000000000000..1de1e5f9254b --- /dev/null +++ b/examples/jquery/displayUser.js @@ -0,0 +1,9 @@ +var $ = require('jquery'); +var fetchCurrentUser = require('./fetchCurrentUser.js'); + +$('#button').click(function() { + fetchCurrentUser(function(user) { + var loggedText = 'Logged ' + (user.loggedIn ? 'In' : 'Out'); + $('#username').text(user.fullName + ' - ' + loggedText); + }); +}); diff --git a/examples/jquery/fetchCurrentUser.js b/examples/jquery/fetchCurrentUser.js new file mode 100644 index 000000000000..e599f4c14367 --- /dev/null +++ b/examples/jquery/fetchCurrentUser.js @@ -0,0 +1,20 @@ +var $ = require('jquery'); + +function parseUserJson(userJson) { + return { + loggedIn: true, + fullName: userJson.firstName + ' ' + userJson.lastName + }; +}; + +function fetchCurrentUser(callback) { + return $.ajax({ + type: 'GET', + url: 'http://example.com/currentUser', + done: function(userJson) { + callback(parseUserJson(userJson)); + } + }); +}; + +module.exports = fetchCurrentUser; diff --git a/examples/jquery/package.json b/examples/jquery/package.json new file mode 100644 index 000000000000..97622f90d97e --- /dev/null +++ b/examples/jquery/package.json @@ -0,0 +1,8 @@ +{ + "dependencies": { + "jquery": "*" + }, + "scripts": { + "test": "node ../../bin/jest.js" + } +} diff --git a/examples/manual_mocks/FileSummarizer.js b/examples/manual_mocks/FileSummarizer.js new file mode 100644 index 000000000000..f78ad40edc4c --- /dev/null +++ b/examples/manual_mocks/FileSummarizer.js @@ -0,0 +1,14 @@ +var fs = require('fs'); + +function summarizeFilesInDirectorySync(directoryPath) { + return fs.readdirSync(directoryPath).map(function(fileName) { + return { + fileName: fileName, + directory: directoryPath + }; + }); + return directoryFileSummary; +} + +exports.summarizeFilesInDirectorySync = summarizeFilesInDirectorySync; + diff --git a/examples/manual_mocks/__mocks__/fs.js b/examples/manual_mocks/__mocks__/fs.js new file mode 100644 index 000000000000..e8b5004ac07a --- /dev/null +++ b/examples/manual_mocks/__mocks__/fs.js @@ -0,0 +1,29 @@ +// Get the real (not mocked) version of the 'path' module +var path = require.requireActual('path'); + +// This is a custom function that our tests can use during setup to specify +// what the files on the "mock" filesystem should look like when any of the +// `fs` APIs are used. +var _mockFiles = {}; +function __setMockFiles(newMockFiles) { + _mockFiles = {}; + for (var file in newMockFiles) { + var dir = path.dirname(file); + + if (!_mockFiles[dir]) { + _mockFiles[dir] = []; + } + + _mockFiles[dir].push(path.basename(file)); + } +}; + +// A custom version of `readdirSync` that reads from the special mocked out +// file list set via __setMockFiles +function readdirSync(directoryPath) { + return _mockFiles[directoryPath] || []; +}; + +exports.__setMockFiles = __setMockFiles; +exports.readdirSync = readdirSync; + diff --git a/examples/manual_mocks/__tests__/FileSummarizer-test.js b/examples/manual_mocks/__tests__/FileSummarizer-test.js new file mode 100644 index 000000000000..042ae868dad2 --- /dev/null +++ b/examples/manual_mocks/__tests__/FileSummarizer-test.js @@ -0,0 +1,27 @@ +jest.dontMock('../FileSummarizer'); + +describe('FileSummarizer', function() { + describe('listFilesInDirectorySync', function() { + var MOCK_FILE_INFO = { + '/path/to/file1.js': + 'console.log("file1 contents");', + + '/path/to/file2.txt': + "file2 contents" + }; + + beforeEach(function() { + // Set up some mocked out file info before each test + require('fs').__setMockFiles(MOCK_FILE_INFO); + }); + + it('includes all files in the directory in the summary', function() { + var FileSummarizer = require('../FileSummarizer'); + var fileSummary = FileSummarizer.summarizeFilesInDirectorySync( + '/path/to' + ); + + expect(fileSummary.length).toBe(2); + }); + }); +}) diff --git a/examples/manual_mocks/package.json b/examples/manual_mocks/package.json new file mode 100644 index 000000000000..0ea218e142f6 --- /dev/null +++ b/examples/manual_mocks/package.json @@ -0,0 +1,5 @@ +{ + "scripts": { + "test": "node ../../bin/jest.js" + } +} diff --git a/examples/react/CheckboxWithLabel.js b/examples/react/CheckboxWithLabel.js new file mode 100644 index 000000000000..8e8686f49255 --- /dev/null +++ b/examples/react/CheckboxWithLabel.js @@ -0,0 +1,25 @@ +/** @jsx React.DOM */ +var React = require('react/addons'); + +var CheckboxWithLabel = React.createClass({ + getInitialState: function() { + return { isChecked: false }; + }, + onChange: function() { + this.setState({isChecked: !this.state.isChecked}); + }, + render: function() { + return ( + + ); + } +}); + +module.exports = CheckboxWithLabel; diff --git a/examples/react/__tests__/CheckboxWithLabel-test.js b/examples/react/__tests__/CheckboxWithLabel-test.js new file mode 100644 index 000000000000..f16e0d96d202 --- /dev/null +++ b/examples/react/__tests__/CheckboxWithLabel-test.js @@ -0,0 +1,24 @@ +/** @jsx React.DOM */ + +jest.dontMock('../CheckboxWithLabel.js'); + +describe('CheckboxWithLabel', function() { + it('changes the text after click', function() { + var React = require('react/addons'); + var CheckboxWithLabel = require('../CheckboxWithLabel.js'); + var TestUtils = React.addons.TestUtils; + + // Render a checkbox with label in the document + var checkbox = ; + TestUtils.renderIntoDocument(checkbox); + + // Verify that it's Off by default + var label = TestUtils.findRenderedDOMComponentWithTag(checkbox, 'label'); + expect(label.getDOMNode().textContent).toEqual('Off'); + + // Simulate a click and verify that it is now On + var input = TestUtils.findRenderedDOMComponentWithTag(checkbox, 'input'); + TestUtils.Simulate.change(input); + expect(label.getDOMNode().textContent).toEqual('On'); + }); +}); diff --git a/examples/react/package.json b/examples/react/package.json new file mode 100644 index 000000000000..533f0fda20d5 --- /dev/null +++ b/examples/react/package.json @@ -0,0 +1,14 @@ +{ + "dependencies": { + "react": "*", + "react-tools": "*" + }, + "scripts": { + "test": "node ../../bin/jest.js" + }, + "jest": { + "scriptPreprocessor": "/preprocessor.js", + "unmockedModulePathPatterns": ["/node_modules/react"], + "testPathIgnorePatterns": ["/node_modules"] + } +} diff --git a/examples/react/preprocessor.js b/examples/react/preprocessor.js new file mode 100644 index 000000000000..32f4a005c371 --- /dev/null +++ b/examples/react/preprocessor.js @@ -0,0 +1,7 @@ +var ReactTools = require('react-tools'); + +module.exports = { + process: function(src) { + return ReactTools.transform(src); + } +}; diff --git a/examples/timer/__tests__/infiniteTimerGame-test.js b/examples/timer/__tests__/infiniteTimerGame-test.js new file mode 100644 index 000000000000..a520d21d47be --- /dev/null +++ b/examples/timer/__tests__/infiniteTimerGame-test.js @@ -0,0 +1,28 @@ +jest.dontMock('../infiniteTimerGame'); + +describe('infiniteTimerGame', function() { + it('schedules a 10-second timer after 1 second', function() { + var infiniteTimerGame = require('../infiniteTimerGame'); + var callback = jest.genMockFunction(); + + infiniteTimerGame(callback); + + // At this point in time, there should have been a single call to + // setTimeout to schedule the end of the game in 1 second. + expect(setTimeout.mock.calls.length).toBe(1); + expect(setTimeout.mock.calls[0][1]).toBe(1000); + + // Fast forward and exhaust only currently pending timers + // (but not any new timers that get created during that process) + jest.runOnlyPendingTimers(); + + // At this point, our 1-second timer should have fired it's callback + expect(callback).toBeCalled(); + + // And it should have created a new timer to start the game over in + // 10 seconds + expect(setTimeout.mock.calls.length).toBe(2); + expect(setTimeout.mock.calls[1][1]).toBe(10000); + }); +}); + diff --git a/examples/timer/__tests__/timerGame-test.js b/examples/timer/__tests__/timerGame-test.js new file mode 100644 index 000000000000..e03765ef886b --- /dev/null +++ b/examples/timer/__tests__/timerGame-test.js @@ -0,0 +1,30 @@ +jest.dontMock('../timerGame'); + +describe('timerGame', function() { + it('waits 1 second before ending the game', function() { + var timerGame = require('../timerGame'); + timerGame(); + + expect(setTimeout.mock.calls.length).toBe(1); + expect(setTimeout.mock.calls[0][1]).toBe(1000); + }); + + it('calls the callback after 1 second', function() { + var timerGame = require('../timerGame'); + var callback = jest.genMockFunction(); + + timerGame(callback); + + // At this point in time, the callback should not have been called yet + expect(callback).not.toBeCalled(); + + // Fast-forward until all timers have been executed + jest.runAllTimers(); + + // Now our callback should have been called! + expect(callback).toBeCalled() + expect(callback.mock.calls.length).toBe(1); + }); + +}); + diff --git a/examples/timer/infiniteTimerGame.js b/examples/timer/infiniteTimerGame.js new file mode 100644 index 000000000000..1bd04408c7c9 --- /dev/null +++ b/examples/timer/infiniteTimerGame.js @@ -0,0 +1,17 @@ +function infiniteTimerGame(callback) { + console.log('Ready....go!'); + + setTimeout(function() { + console.log('Times up! 10 seconds before the next game starts...'); + callback && callback(); + + // Schedule the next game in 10 seconds + setTimeout(function() { + infiniteTimerGame(callback); + }, 10000); + + }, 1000); +} + +module.exports = infiniteTimerGame; + diff --git a/examples/timer/package.json b/examples/timer/package.json new file mode 100644 index 000000000000..0ea218e142f6 --- /dev/null +++ b/examples/timer/package.json @@ -0,0 +1,5 @@ +{ + "scripts": { + "test": "node ../../bin/jest.js" + } +} diff --git a/examples/timer/timerGame.js b/examples/timer/timerGame.js new file mode 100644 index 000000000000..8ded30dc16db --- /dev/null +++ b/examples/timer/timerGame.js @@ -0,0 +1,10 @@ +function timerGame(callback) { + console.log('Ready....go!'); + setTimeout(function() { + console.log('Times up -- stop!'); + + callback && callback(); + }, 1000); +} + +module.exports = timerGame; diff --git a/examples/tutorial/__tests__/fetchCurrentUser-test.js b/examples/tutorial/__tests__/fetchCurrentUser-test.js new file mode 100644 index 000000000000..a32d9c2adb46 --- /dev/null +++ b/examples/tutorial/__tests__/fetchCurrentUser-test.js @@ -0,0 +1,43 @@ +jest.dontMock('../fetchCurrentUser.js'); + +describe('fetchCurrentUser', function() { + it('calls into $.ajax with the correct params', function() { + var $ = require('jquery'); + var fetchCurrentUser = require('../fetchCurrentUser'); + + // Call into the function we want to test + function dummyCallback() {} + fetchCurrentUser(dummyCallback); + + // Now make sure that $.ajax was properly called during the previous + // 2 lines + expect($.ajax).toBeCalledWith({ + type: 'GET', + url: 'http://example.com/currentUser', + done: jasmine.any(Function) + }); + }); + + it('calls the callback when $.ajax requests are finished', function() { + var $ = require('jquery'); + var fetchCurrentUser = require('../fetchCurrentUser'); + + // Create a mock function for our callback + var callback = jest.genMockFunction(); + fetchCurrentUser(callback); + + // Now we emulate the process by which `$.ajax` would execute its own + // callback + $.ajax.mock.calls[0 /*first call*/][0 /*first argument*/].done({ + firstName: 'Bobby', + lastName: '");DROP TABLE Users;--' + }); + + // And finally we assert that this emulated call by `$.ajax` incurred a + // call back into the mock function we provided as a callback + expect(callback.mock.calls[0/*first call*/][0/*first arg*/]).toEqual({ + loggedIn: true, + fullName: 'Bobby ");DROP TABLE Users;--' + }); + }); +}); diff --git a/examples/tutorial/fetchCurrentUser.js b/examples/tutorial/fetchCurrentUser.js new file mode 100644 index 000000000000..51f84ff072ee --- /dev/null +++ b/examples/tutorial/fetchCurrentUser.js @@ -0,0 +1,20 @@ +var $ = require('jquery'); + +function parseUserJson(userJson) { + return { + loggedIn: true, + fullName: userJson.firstName + ' ' + userJson.lastName + }; +} + +function fetchCurrentUser(callback) { + return $.ajax({ + type: 'GET', + url: 'http://example.com/currentUser', + done: function(userJson) { + callback(parseUserJson(userJson)); + } + }); +} + +module.exports = fetchCurrentUser; diff --git a/examples/tutorial/package.json b/examples/tutorial/package.json new file mode 100644 index 000000000000..97622f90d97e --- /dev/null +++ b/examples/tutorial/package.json @@ -0,0 +1,8 @@ +{ + "dependencies": { + "jquery": "*" + }, + "scripts": { + "test": "node ../../bin/jest.js" + } +} diff --git a/licenseify_all_files.js b/licenseify_all_files.js new file mode 100644 index 000000000000..6f1d2a4a1d65 --- /dev/null +++ b/licenseify_all_files.js @@ -0,0 +1,119 @@ +/** + * Copyright (c) 2014, Facebook, Inc. All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +var FileFinder = require('node-find-files'); +var fs = require('fs'); +var path = require('path'); + +var EXTRACT_DOCBLOCK_REGEX = /^\s*(\/\*\*?(?:.|\r?\n)*?\*\/)/; +var IS_JSHINT_DOCBLOCK = /\/\*\*?\s*jshint\s+[a-zA-Z0-9]+\s*:\s*[a-zA-Z0-9]+/; + +var EXAMPLES_DIR = path.join(__dirname, 'examples'); +var COVERAGE_TEMPLATE_PATH = path.join(__dirname, 'src', 'coverageTemplate.js'); +var NODE_MODULES_DIR = path.join(__dirname, 'node_modules'); +var VENDOR_DIR = path.join(__dirname, 'vendor'); +var WEBSITE_DIR = path.join(__dirname, 'website'); + +var LICENSE_HEADER = [ + ' * Copyright (c) 2014, Facebook, Inc. All rights reserved.', + ' *', + ' * This source code is licensed under the BSD-style license found in the', + ' * LICENSE file in the root directory of this source tree. An additional ' + + 'grant', + ' * of patent rights can be found in the PATENTS file in the same directory.', +].join('\n'); + +function needsLicenseHeader(fileData) { + if (/^#!/.test(fileData)) { + fileData = fileData.split('\n'); + fileData.shift(); + fileData = fileData.join('\n'); + } + return fileData.indexOf(LICENSE_HEADER) === -1; +} + +function addLicenseHeader(fileData) { + var preamble = ''; + var epilogue = '\n */' + + if (/^#!/.test(fileData)) { + fileData = fileData.split('\n'); + preamble += fileData.shift() + '\n'; + fileData = fileData.join('\n'); + } + + preamble += '/**\n'; + + var startingDocblock = EXTRACT_DOCBLOCK_REGEX.exec(fileData); + if (startingDocblock && !IS_JSHINT_DOCBLOCK.test(startingDocblock)) { + startingDocblock = startingDocblock[0]; + + fileData = fileData.substr(startingDocblock.length); + + // Strip of starting/ending tokens + var strippedDocblock = + startingDocblock + .replace(/^\/\*\*?\s*/, '') + .replace(/\s*\*\/$/, ''); + + if (strippedDocblock.trim().charAt(0) !== '*') { + strippedDocblock = '* ' + strippedDocblock.trim(); + } + + epilogue = '\n *\n ' + strippedDocblock + epilogue; + } + + + return preamble + LICENSE_HEADER + epilogue + '\n' + fileData; +} + +function main() { + var finder = new FileFinder({ + rootFolder: __dirname, + filterFunction: function(pathStr) { + return ( + pathStr !== COVERAGE_TEMPLATE_PATH + && pathStr.substr(0, EXAMPLES_DIR.length) !== EXAMPLES_DIR + && pathStr.substr(0, NODE_MODULES_DIR.length) !== NODE_MODULES_DIR + && pathStr.substr(0, VENDOR_DIR.length) !== VENDOR_DIR + && pathStr.substr(0, WEBSITE_DIR.length) !== WEBSITE_DIR + && path.extname(pathStr) === '.js' + ); + } + }); + + finder.on('error', function(err) { + console.error('Error: ' + (err.stack || err.msg || err)); + process.exit(1); + }); + + finder.on('match', function(pathStr) { + console.log('Reading ' + pathStr + '...'); + fs.readFile(pathStr, 'utf8', function(err, fileData) { + if (err) { + throw err; + } + + if (needsLicenseHeader(fileData)) { + console.log('Adding license header to ' + pathStr + '...'); + addLicenseHeader(fileData); + fs.writeFile(pathStr, addLicenseHeader(fileData), function(err) { + if (err) { + throw err; + } + console.log('Successfully updated ' + pathStr + '!'); + }); + } + }); + }); + + finder.startSearch(); +} + +if (require.main === module) { + main(); +} diff --git a/package.json b/package.json new file mode 100644 index 000000000000..6a2cefdb16fb --- /dev/null +++ b/package.json @@ -0,0 +1,40 @@ +{ + "name": "jest-cli", + "version": "0.0.10-pre", + "dependencies": { + "coffee-script": "1.7.1", + "cover": "~0.2.8", + "diff": "~1.0.4", + "jasmine-only": "0.1.0", + "jasmine-pit": "~1.0.0", + "jsdom": "~0.10.3", + "node-find-files": "~0.0.2", + "node-haste": "~1.2.1", + "node-worker-pool": "~2.4.0", + "optimist": "~0.6.0", + "q": "~0.9.7", + "resolve": "~0.6.1", + "underscore": "1.2.4" + }, + "devDependencies": { + "jest-cli": "~0.0.0", + "jshint": "~2.5.0" + }, + "bin": { + "jest": "./bin/jest.js" + }, + "repository": { + "type": "git", + "url": "https://github.com/facebook/jest" + }, + "scripts": { + "prepublish": "jshint --config=.jshintrc --exclude=src/coverageTemplate.js src && jest", + "test": "jshint --config=.jshintrc --exclude=src/coverageTemplate.js src && jest" + }, + "jest": { + "rootDir": "src", + "testPathIgnorePatterns": [ + "/__tests__/[^/]*/.+" + ] + } +} diff --git a/src/CoverageCollector.js b/src/CoverageCollector.js new file mode 100644 index 000000000000..a104fb2f3659 --- /dev/null +++ b/src/CoverageCollector.js @@ -0,0 +1,74 @@ +/** + * Copyright (c) 2014, Facebook, Inc. All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +'use strict'; + +var CoverageInstrumentor = require('cover/instrument').Instrumentor; +var fs = require('fs'); + +var COVERAGE_TEMPLATE_PATH = require.resolve('./coverageTemplate'); + +var _memoizedCoverageTemplate = null; +function _getCoverageTemplate() { + if (_memoizedCoverageTemplate === null) { + _memoizedCoverageTemplate = require('underscore').template( + fs.readFileSync(COVERAGE_TEMPLATE_PATH, 'utf8') + ); + } + return _memoizedCoverageTemplate; +} + +function CoverageCollector(sourceText) { + this._coverageDataStore = {}; + this._instrumentedSourceText = null; + this._instrumentor = new CoverageInstrumentor(); + this._origSourceText = sourceText; +} + +CoverageCollector.prototype.getCoverageDataStore = function() { + return this._coverageDataStore; +}; + +CoverageCollector.prototype.getInstrumentedSource = function(storageVarName) { + if (this._instrumentedSourceText === null) { + this._instrumentedSourceText = _getCoverageTemplate()({ + instrumented: this._instrumentor, + coverageStorageVar: storageVarName, + source: this._instrumentor.instrument(this._origSourceText) + }); + } + return this._instrumentedSourceText; +}; + +CoverageCollector.prototype.extractRuntimeCoverageInfo = function() { + var instrumentationInfo = this._instrumentor.objectify(); + var coverageInfo = { + coveredSpans: [], + uncoveredSpans: [], + sourceText: this._origSourceText + }; + + var nodeIndex; + + // Find all covered spans + for (nodeIndex in this._coverageDataStore.nodes) { + coverageInfo.coveredSpans.push(instrumentationInfo.nodes[nodeIndex].loc); + } + + // Find all definitely uncovered spans + for (nodeIndex in instrumentationInfo.nodes) { + if (!this._coverageDataStore.nodes.hasOwnProperty(nodeIndex)) { + coverageInfo.uncoveredSpans.push( + instrumentationInfo.nodes[nodeIndex].loc + ); + } + } + + return coverageInfo; +}; + +module.exports = CoverageCollector; diff --git a/src/HasteModuleLoader/HasteModuleLoader.js b/src/HasteModuleLoader/HasteModuleLoader.js new file mode 100644 index 000000000000..1e88be0b85f4 --- /dev/null +++ b/src/HasteModuleLoader/HasteModuleLoader.js @@ -0,0 +1,1116 @@ +/** + * Copyright (c) 2014, Facebook, Inc. All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +'use strict'; + +/** + * TODO: This file has grown into a monster. It really needs to be refactored + * into smaller pieces. One of the best places to start would be to move a + * bunch of the logic that exists here into node-haste. + * + * Relatedly: It's time we vastly simplify node-haste. + */ + +var CoverageCollector = require('../CoverageCollector'); +var fs = require('fs'); +var hasteLoaders = require('node-haste/lib/loaders'); +var moduleMocker = require('../lib/moduleMocker'); +var NodeHaste = require('node-haste/lib/Haste'); +var os = require('os'); +var path = require('path'); +var Q = require('q'); +var resolve = require('resolve'); +var utils = require('../lib/utils'); + +var MAIN_DIR = path.resolve(__dirname + '/../'); +var CACHE_DIR_PATH = MAIN_DIR + '/.haste_cache_dir'; +var COVERAGE_STORAGE_VAR_NAME = '____JEST_COVERAGE_DATA____'; + +var IS_PATH_BASED_MODULE_NAME = /^(?:\.\.?\/|\/)/; + +var NODE_CORE_MODULES = { + assert: true, + buffer: true, + child_process: true, // jshint ignore:line + cluster: true, + console: true, + constants: true, + crypto: true, + dgram: true, + dns: true, + domain: true, + events: true, + freelist: true, + fs: true, + http: true, + https: true, + module: true, + net: true, + os: true, + path: true, + punycode: true, + querystring: true, + readline: true, + repl: true, + smalloc: true, + stream: true, + string_decoder: true, // jshint ignore:line + sys: true, + timers: true, + tls: true, + tty: true, + url: true, + util: true, + vm: true, + zlib: true +}; + +var _configUnmockListRegExpCache = null; + +function _buildLoadersList(config) { + return [ + new hasteLoaders.ProjectConfigurationLoader(), + new hasteLoaders.JSTestLoader(config.setupJSTestLoaderOptions), + new hasteLoaders.JSMockLoader(config.setupJSMockLoaderOptions), + new hasteLoaders.JSLoader(config.setupJSLoaderOptions), + new hasteLoaders.ResourceLoader() + ]; +} + +function _calculateCacheFilePath(config) { + return CACHE_DIR_PATH + '/cache-' + config.name; +} + +function _constructHasteInst(config, options) { + var HASTE_IGNORE_REGEX = new RegExp( + config.modulePathIgnorePatterns + ? config.modulePathIgnorePatterns.join('|') + : '__NOT_EXIST__' + ); + + if (!fs.existsSync(CACHE_DIR_PATH)) { + fs.mkdirSync(CACHE_DIR_PATH); + } + + return new NodeHaste( + _buildLoadersList(config), + (config.testPathDirs || []), + { + ignorePaths: function(path) { + return path.match(HASTE_IGNORE_REGEX); + }, + version: JSON.stringify(config), + useNativeFind: true, + maxProcesses: os.cpus().length, + maxOpenFiles: options.maxOpenFiles || 100 + } + ); +} + +function Loader(config, environment, resourceMap) { + this._config = config; + this._coverageCollectors = {}; + this._currentlyExecutingModulePath = ''; + this._environment = environment; + this._explicitShouldMock = {}; + this._explicitlySetMocks = {}; + this._isCurrentlyExecutingManualMock = null; + this._mockMetaDataCache = {}; + this._nodeModuleProjectConfigNameToResource = null; + this._resourceMap = resourceMap; + this._reverseDependencyMap = null; + this._shouldAutoMock = true; + this._configShouldMockModuleNames = {}; + + if (_configUnmockListRegExpCache === null) { + // Node must have been run with --harmony in order for WeakMap to be + // available + if (!process.execArgv.some(function(arg) { return arg === '--harmony'; })) { + throw new Error('Please run node with the --harmony flag!'); + } + + _configUnmockListRegExpCache = new WeakMap(); + } + + if (!config.unmockedModulePathPatterns + || config.unmockedModulePathPatterns.length === 0) { + this._unmockListRegExps = []; + } else { + this._unmockListRegExps = _configUnmockListRegExpCache.get(config); + if (!this._unmockListRegExps) { + this._unmockListRegExps = config.unmockedModulePathPatterns + .map(function(unmockPathRe) { + return new RegExp(unmockPathRe); + }); + _configUnmockListRegExpCache.set(config, this._unmockListRegExps); + } + } + + this.resetModuleRegistry(); +} + +Loader.loadResourceMap = function(config, options) { + options = options || {}; + + var deferred = Q.defer(); + try { + _constructHasteInst(config, options).update( + _calculateCacheFilePath(config), + function(resourceMap) { + deferred.resolve(resourceMap); + } + ); + } catch (e) { + deferred.reject(e); + } + + return deferred.promise; +}; + +Loader.loadResourceMapFromCacheFile = function(config, options) { + options = options || {}; + + var deferred = Q.defer(); + try { + var hasteInst = _constructHasteInst(config, options); + hasteInst.loadMap(_calculateCacheFilePath(config), function(err, map) { + if (err) { + deferred.reject(err); + } else { + deferred.resolve(map); + } + }); + } catch (e) { + deferred.reject(e); + } + + return deferred.promise; +}; + +/** + * Given the path to a module: Read it from disk (synchronously) and + * evaluate it's constructor function to generate the module and exports + * objects. + * + * @param string modulePath + * @return object + */ +Loader.prototype._execModule = function(moduleObj) { + var modulePath = moduleObj.__filename; + + var moduleContent = + utils.readAndPreprocessFileContent(modulePath, this._config); + + moduleObj.require = this.constructBoundRequire(modulePath); + + var moduleLocalBindings = { + 'module': moduleObj, + 'exports': moduleObj.exports, + 'require': moduleObj.require, + '__dirname': path.dirname(modulePath), + '__filename': modulePath, + 'global': this._environment.global, + 'jest': this._builtInModules['jest-runtime'](modulePath).exports + }; + + var onlyCollectFrom = this._config.collectCoverageOnlyFrom; + var shouldCollectCoverage = + this._config.collectCoverage === true && !onlyCollectFrom + || (onlyCollectFrom && onlyCollectFrom[modulePath] === true); + + if (shouldCollectCoverage) { + if (!this._coverageCollectors.hasOwnProperty(modulePath)) { + this._coverageCollectors[modulePath] = + new CoverageCollector(moduleContent); + } + var collector = this._coverageCollectors[modulePath]; + moduleLocalBindings[COVERAGE_STORAGE_VAR_NAME] = + collector.getCoverageDataStore(); + moduleContent = collector.getInstrumentedSource(COVERAGE_STORAGE_VAR_NAME); + } + + var lastExecutingModulePath = this._currentlyExecutingModulePath; + this._currentlyExecutingModulePath = modulePath; + + var origCurrExecutingManualMock = this._isCurrentlyExecutingManualMock; + this._isCurrentlyExecutingManualMock = modulePath; + + utils.runContentWithLocalBindings( + this._environment.runSourceText.bind(this._environment), + moduleContent, + modulePath, + moduleLocalBindings + ); + + this._isCurrentlyExecutingManualMock = origCurrExecutingManualMock; + this._currentlyExecutingModulePath = lastExecutingModulePath; +}; + +Loader.prototype._generateMock = function(currPath, moduleName) { + var modulePath = this._moduleNameToPath(currPath, moduleName); + + if (!this._mockMetaDataCache.hasOwnProperty(modulePath)) { + // This allows us to handle circular dependencies while generating an + // automock + this._mockMetaDataCache[modulePath] = moduleMocker.getMetadata({}); + + // In order to avoid it being possible for automocking to potentially cause + // side-effects within the module environment, we need to execute the module + // in isolation. This accomplishes that by temporarily clearing out the + // module and mock registries while the module being analyzed is executed. + // + // An example scenario where this could cause issue is if the module being + // mocked has calls into side-effectful APIs on another module. + var origMockRegistry = this._mockRegistry; + var origModuleRegistry = this._moduleRegistry; + this._mockRegistry = {}; + this._moduleRegistry = {}; + + var moduleExports = this.requireModule(currPath, moduleName); + + // Restore the "real" module/mock registries + this._mockRegistry = origMockRegistry; + this._moduleRegistry = origModuleRegistry; + + this._mockMetaDataCache[modulePath] = moduleMocker.getMetadata( + moduleExports + ); + } + + return moduleMocker.generateFromMetadata( + this._mockMetaDataCache[modulePath] + ); +}; + +Loader.prototype._getDependencyPathsFromResource = function(resource) { + var dependencyPaths = []; + for (var i = 0; i < resource.requiredModules.length; i++) { + var requiredModule = resource.requiredModules[i]; + + // *facepalm* node-haste is pretty clowny + if (resource.getModuleIDByOrigin) { + requiredModule = + resource.getModuleIDByOrigin(requiredModule) || requiredModule; + } + + dependencyPaths.push(this._getRealPathFromNormalizedModuleID( + this._getNormalizedModuleID(resource.path, requiredModule) + )); + } + return dependencyPaths; +}; + +Loader.prototype._getResource = function(resourceType, resourceName) { + var resource = this._resourceMap.getResource(resourceType, resourceName); + + // TODO: Fix this properly in node-haste, not here :( + if (resource === undefined && resourceType === 'JS' && /\//.test(resourceName) + && !/\.js$/.test(resourceName)) { + resource = this._resourceMap.getResource( + resourceType, + resourceName + '.js' + ); + } + + return resource; +}; + +Loader.prototype._getNormalizedModuleID = function(currPath, moduleName) { + var moduleType; + var mockAbsPath = null; + var realAbsPath = null; + + if (this._builtInModules.hasOwnProperty(moduleName)) { + moduleType = 'builtin'; + realAbsPath = moduleName; + } else if (NODE_CORE_MODULES.hasOwnProperty(moduleName)) { + moduleType = 'node'; + realAbsPath = moduleName; + } else { + moduleType = 'user'; + + // If this is a path-based module name, resolve it to an absolute path and + // then see if there's a node-haste resource for it (so that we can extract + // info from the resource, like whether its a mock, or a + if (IS_PATH_BASED_MODULE_NAME.test(moduleName)) { + var absolutePath = this._moduleNameToPath(currPath, moduleName); + if (absolutePath === undefined) { + throw new Error( + 'Cannot find module \'' + moduleName + '\' from \'' + currPath + '\'' + ); + } + + // See if node-haste is already aware of this resource. If so, we need to + // look up if it has an associated manual mock. + var resource = this._resourceMap.getResourceByPath(absolutePath); + if (resource) { + if (resource.type === 'JS') { + realAbsPath = absolutePath; + } else if (resource.type === 'JSMock') { + mockAbsPath = absolutePath; + } + moduleName = resource.id; + } + } + + if (realAbsPath === null) { + var moduleResource = this._getResource('JS', moduleName); + if (moduleResource) { + realAbsPath = moduleResource.path; + } + } + + if (mockAbsPath === null) { + var mockResource = this._getResource('JSMock', moduleName); + if (mockResource) { + mockAbsPath = mockResource.path; + } + } + } + + return [moduleType, realAbsPath, mockAbsPath].join(':'); +}; + +Loader.prototype._getRealPathFromNormalizedModuleID = function(moduleID) { + return moduleID.split(':')[1]; +}; + +/** + * Given a module name and the current file path, returns the normalized + * (absolute) module path for said module. Relative-path CommonJS require()s + * such as `require('./otherModule')` need to be looked up with context of + * the module that's calling require() + * + * Also contains special case logic for built-in modules, in which it just + * returns the module name. + * + * @param string currPath The path of the file that is attempting to + * resolve the module + * @param string moduleName The name of the module to be resolved + * @return string + */ +Loader.prototype._moduleNameToPath = function(currPath, moduleName) { + if (this._builtInModules.hasOwnProperty(moduleName)) { + return moduleName; + } + + // Relative-path CommonJS require()s such as `require('./otherModule')` + // need to be looked up with context of the module that's calling + // require(). + if (IS_PATH_BASED_MODULE_NAME.test(moduleName)) { + // Normalize the relative path to an absolute path + var modulePath = path.resolve(currPath, '..', moduleName); + var modulePathExtName = path.extname(modulePath); + + if (modulePathExtName !== '.js' + && fs.existsSync(modulePath + '.js') + && fs.statSync(modulePath + '.js').isFile()) { + return modulePath + '.js'; + } else if (fs.existsSync(modulePath)) { + if (fs.statSync(modulePath).isDirectory()) { + if (fs.existsSync(modulePath + '.js') + && fs.statSync(modulePath + '.js').isFile()) { + // The required path is a valid directory, but there's also a + // matching js file at the same path -- so the js file wins + return modulePath + '.js'; + } else { + // The required path is a valid directory, but there's no matching + // js file at the same path. So look in the directory for an + // index.js file. + var indexPath = path.join(modulePath, 'index.js'); + if (fs.existsSync(indexPath)) { + return indexPath; + } else { + throw new Error('Module(' + moduleName + ') does not exist!'); + } + } + } else { + // The required path is a file, so return this path + return modulePath; + } + } else if (fs.existsSync(modulePath + '.json') + && fs.statSync(modulePath + '.json').isFile()) { + // The required path doesn't exist, nor does a .js file at that path, + // but a .json file does -- so use that + return modulePath + '.json'; + } + } else { + var resource = this._getResource('JS', moduleName); + if (!resource) { + return this._nodeModuleNameToPath( + currPath, + moduleName + ); + } + return resource.path; + } +}; + +Loader.prototype._nodeModuleNameToPath = function(currPath, moduleName) { + // Handle module names like require('jest/lib/util') + var subModulePath = null; + var moduleProjectPart = moduleName; + if (/\//.test(moduleName)) { + var projectPathParts = moduleName.split('/'); + moduleProjectPart = projectPathParts.shift(); + subModulePath = projectPathParts.join('/'); + } + + // Memoize the project name -> package.json resource lookup map + if (this._nodeModuleProjectConfigNameToResource === null) { + this._nodeModuleProjectConfigNameToResource = {}; + var resources = + this._resourceMap.getAllResourcesByType('ProjectConfiguration'); + resources.forEach(function(res) { + this._nodeModuleProjectConfigNameToResource[res.data.name] = res; + }.bind(this)); + } + + // Get the resource for the package.json file + var resource = this._nodeModuleProjectConfigNameToResource[moduleProjectPart]; + if (!resource) { + if (NODE_CORE_MODULES[moduleName]) { + return null; + } + return resolve.sync(moduleName, {basedir: path.dirname(currPath)}); + } + + // Make sure the resource path is above the currPath in the fs path + // tree. If so, just use node's resolve + var resourceDirname = path.dirname(resource.path); + var currFileDirname = path.dirname(currPath); + if (resourceDirname.indexOf(currFileDirname) > 0) { + return resolve.sync(moduleName, {basedir: path.dirname(currPath)}); + } + + if (subModulePath === null) { + subModulePath = + resource.data.hasOwnProperty('main') + ? resource.data.main + : 'index.js'; + } + + return this._moduleNameToPath( + resource.path, + './' + subModulePath + ); +}; + +/** + * Indicates whether a given module is mocked per the current state of the + * module loader. When a module is "mocked", that means calling + * `requireModuleOrMock()` for the module will return the mock version + * rather than the real version. + * + * @param string currPath The path of the file that is attempting to + * resolve the module + * @param string moduleName The name of the module to be resolved + * @return bool + */ +Loader.prototype._shouldMock = function(currPath, moduleName) { + var moduleID = this._getNormalizedModuleID(currPath, moduleName); + if (this._builtInModules.hasOwnProperty(moduleName)) { + return false; + } else if (this._explicitShouldMock.hasOwnProperty(moduleID)) { + return this._explicitShouldMock[moduleID]; + } else if (this._shouldAutoMock) { + + // See if the module is specified in the config as a module that should + // never be mocked + if (this._configShouldMockModuleNames.hasOwnProperty(moduleName)) { + return this._configShouldMockModuleNames[moduleName]; + } else if (this._unmockListRegExps.length > 0) { + this._configShouldMockModuleNames[moduleName] = true; + + var manualMockResource = + this._getResource('JSMock', moduleName); + try { + var modulePath = this._moduleNameToPath(currPath, moduleName); + } catch(e) { + // If there isn't a real module, we don't have a path to match + // against the unmockList regexps. If there is also not a manual + // mock, then we throw because this module doesn't exist anywhere. + // + // However, it's possible that someone has a manual mock for a + // non-existant real module. In this case, we should mock the module + // (because we technically can). + // + // Ideally this should never happen, but we have some odd + // pre-existing edge-cases that rely on it so we need it for now. + // + // I'd like to eliminate this behavior in favor of requiring that + // all module environments are complete (meaning you can't just + // write a manual mock as a substitute for a real module). + if (manualMockResource) { + return true; + } + throw e; + } + var unmockRegExp; + + this._configShouldMockModuleNames[moduleName] = true; + for (var i = 0; i < this._unmockListRegExps.length; i++) { + unmockRegExp = this._unmockListRegExps[i]; + if (unmockRegExp.test(modulePath)) { + return this._configShouldMockModuleNames[moduleName] = false; + } + } + return this._configShouldMockModuleNames[moduleName]; + } + return true; + } else { + return false; + } +}; + +Loader.prototype.constructBoundRequire = function(sourceModulePath) { + var boundModuleRequire = this.requireModuleOrMock.bind( + this, + sourceModulePath + ); + + boundModuleRequire.resolve = function(moduleName) { + var ret = this._moduleNameToPath(sourceModulePath, moduleName); + if (!ret) { + throw new Error('Module(' + moduleName + ') not found!'); + } + return ret; + }.bind(this); + boundModuleRequire.generateMock = this._generateMock.bind( + this, + sourceModulePath + ); + boundModuleRequire.requireMock = this.requireMock.bind( + this, + sourceModulePath + ); + boundModuleRequire.requireActual = this.requireModule.bind( + this, + sourceModulePath + ); + + return boundModuleRequire; +}; + +/** + * Returns a map from modulePath -> coverageInfo, where coverageInfo is of the + * structure returned By CoverageCollector.extractRuntimeCoverageInfo() + */ +Loader.prototype.getAllCoverageInfo = function() { + if (!this._config.collectCoverage) { + throw new Error( + 'config.collectCoverage was not set, so no coverage info has been ' + + '(or will be) collected!' + ); + } + + var coverageInfo = {}; + for (var filePath in this._coverageCollectors) { + coverageInfo[filePath] = + this._coverageCollectors[filePath].extractRuntimeCoverageInfo(); + } + return coverageInfo; +}; + +Loader.prototype.getCoverageForFilePath = function(filePath) { + if (!this._config.collectCoverage) { + throw new Error( + 'config.collectCoverage was not set, so no coverage info has been ' + + '(or will be) collected!' + ); + } + + return ( + this._coverageCollectors.hasOwnProperty(filePath) + ? this._coverageCollectors[filePath].extractRuntimeCoverageInfo() + : null + ); +}; + +/** + * Given the path to some file, find the path to all other files that it + * *directly* depends on. + * + * @param {String} modulePath Absolute path to the module in question + * @return {Array} List of paths to files that the given module directly + * depends on. + */ +Loader.prototype.getDependenciesFromPath = function(modulePath) { + var resource = this._resourceMap.getResourceByPath(modulePath); + if (!resource) { + throw new Error('Unknown modulePath: ' + modulePath); + } + + if (resource.type === 'ProjectConfiguration' + || resource.type === 'Resource') { + throw new Error( + 'Could not extract dependency information from this type of file!' + ); + } + + return this._getDependencyPathsFromResource(resource); +}; + +/** + * Given the path to some module, find all other files that *directly* depend on + * it. + * + * @param {String} modulePath Absolute path to the module in question + * @return {Array} List of paths to files that directly depend on the + * given module path. + */ +Loader.prototype.getDependentsFromPath = function(modulePath) { + if (this._reverseDependencyMap === null) { + var resourceMap = this._resourceMap; + var reverseDepMap = this._reverseDependencyMap = {}; + var allResources = resourceMap.getAllResources(); + for (var resourceID in allResources) { + var resource = allResources[resourceID]; + if (resource.type === 'ProjectConfiguration' + || resource.type === 'Resource') { + continue; + } + + var dependencyPaths = this._getDependencyPathsFromResource(resource); + for (var i = 0; i < dependencyPaths.length; i++) { + var requiredModulePath = dependencyPaths[i]; + if (!reverseDepMap.hasOwnProperty(requiredModulePath)) { + reverseDepMap[requiredModulePath] = {}; + } + reverseDepMap[requiredModulePath][resource.path] = true; + } + } + } + + var reverseDeps = this._reverseDependencyMap[modulePath]; + return reverseDeps ? Object.keys(reverseDeps) : []; +}; + +/** + * Given a module name, return the mock version of said module. + * + * @param string currPath The path of the file that is attempting to + * resolve the module + * @param string moduleName The name of the module to be resolved + * @return object + */ +Loader.prototype.requireMock = function(currPath, moduleName) { + var moduleID = this._getNormalizedModuleID(currPath, moduleName); + + if (this._explicitlySetMocks.hasOwnProperty(moduleID)) { + return this._explicitlySetMocks[moduleID]; + } + + // Look in the node-haste resource map + var manualMockResource = this._getResource('JSMock', moduleName); + var modulePath; + if (manualMockResource) { + modulePath = manualMockResource.path; + } else { + modulePath = this._moduleNameToPath(currPath, moduleName); + + // If the actual module file has a __mocks__ dir sitting immediately next to + // it, look to see if there is a manual mock for this file in that dir. + // + // The reason why node-haste isn't good enough for this is because + // node-haste only handles manual mocks for @providesModules well. Otherwise + // it's not good enough to disambiguate something like the following + // scenario: + // + // subDir1/MyModule.js + // subDir1/__mocks__/MyModule.js + // subDir2/MyModule.js + // subDir2/__mocks__/MyModule.js + // + // Where some other module does a relative require into each of the + // respective subDir{1,2} directories and expects a manual mock + // corresponding to that particular MyModule.js file. + var moduleDir = path.dirname(modulePath); + var moduleFileName = path.basename(modulePath); + var potentialManualMock = path.join(moduleDir, '__mocks__', moduleFileName); + if (fs.existsSync(potentialManualMock)) { + manualMockResource = true; + modulePath = potentialManualMock; + } + } + + if (this._mockRegistry.hasOwnProperty(modulePath)) { + return this._mockRegistry[modulePath]; + } + + if (manualMockResource) { + var moduleObj = { + exports: {}, + __filename: modulePath + }; + this._execModule(moduleObj); + this._mockRegistry[modulePath] = moduleObj.exports; + } else { + // Look for a real module to generate an automock from + this._mockRegistry[modulePath] = this._generateMock( + currPath, + moduleName + ); + } + + return this._mockRegistry[modulePath]; +}; + +/** + * Given a module name, return the *real* (un-mocked) version of said + * module. + * + * @param string currPath The path of the file that is attempting to + * resolve the module + * @param string moduleName The name of the module to be resolved + * @param bool bypassRegistryCache Whether we should read from/write to the + * module registry. Fuck this arg. + * @return object + */ +Loader.prototype.requireModule = function(currPath, moduleName, + bypassRegistryCache) { + var modulePath; + var moduleID = this._getNormalizedModuleID(currPath, moduleName); + + // I don't like this behavior as it makes the module system's mocking + // rules harder to understand. Would much prefer that mock state were + // either "on" or "off" -- rather than "automock on", "automock off", + // "automock off -- but there's a manual mock, so you get that if you ask + // for the module and one doesnt exist", or "automock off -- but theres a + // useAutoMock: false entry in the package.json -- and theres a manual + // mock -- and the module is listed in the unMockList in the test config + // -- soooo...uhh...fuck I lost track". + // + // To simplify things I'd like to move to a system where tests must + // explicitly call .mock() on a module to recieve the mocked version if + // automocking is off. If a manual mock exists, that is used. Otherwise + // we fall back to the automocking system to generate one for you. + // + // The only reason we're supporting this in jest for now is because we + // have some tests that depend on this behavior. I'd like to clean this + // up at some point in the future. + var manualMockResource = null; + var moduleResource = null; + moduleResource = this._getResource('JS', moduleName); + manualMockResource = this._getResource('JSMock', moduleName); + if (!moduleResource + && manualMockResource + && manualMockResource.path !== this._isCurrentlyExecutingManualMock + && this._explicitShouldMock[moduleID] !== false) { + modulePath = manualMockResource.path; + } + + if (!modulePath) { + modulePath = this._moduleNameToPath(currPath, moduleName); + } + + if (!modulePath) { + if (NODE_CORE_MODULES[moduleName]) { + return require(moduleName); + } + + throw new Error( + 'Cannot find module \'' + moduleName + '\' from \'' + currPath + + '\'' + ); + } + + var moduleObj; + if (modulePath && this._builtInModules.hasOwnProperty(modulePath)) { + moduleObj = this._builtInModules[modulePath](currPath); + } + + if (!moduleObj && !bypassRegistryCache) { + moduleObj = this._moduleRegistry[modulePath]; + } + if (!moduleObj) { + // Good ole node... + if (path.extname(modulePath) === '.json') { + return this._moduleRegistry[modulePath] = JSON.parse(fs.readFileSync( + modulePath, + 'utf8' + )); + } + + // We must register the pre-allocated module object first so that any + // circular dependencies that may arise while evaluating the module can + // be satisfied. + moduleObj = { + __filename: modulePath, + exports: {} + }; + + if (!bypassRegistryCache) { + this._moduleRegistry[modulePath] = moduleObj; + } + + this._execModule(moduleObj); + } + + return moduleObj.exports; +}; + +/** + * Given a module name, return either the real module or the mock version of + * that module -- depending on the mocking state of the loader (and, perhaps + * the mocking state for the requested module). + * + * @param string currPath The path of the file that is attempting to + * resolve the module + * @param string moduleName The name of the module to be resolved + * @return object + */ +Loader.prototype.requireModuleOrMock = function(currPath, moduleName) { + if (this._shouldMock(currPath, moduleName)) { + return this.requireMock(currPath, moduleName); + } else { + return this.requireModule(currPath, moduleName); + } +}; + +/** + * Clears all cached module objects. This allows one to reset the state of + * all modules in the system. It will reset (read: clear) the export objects + * for all evaluated modules and mocks. + * + * @return void + */ +Loader.prototype.resetModuleRegistry = function() { + this._mockRegistry = {}; + this._moduleRegistry = {}; + this._builtInModules = { + 'jest-runtime': function(currPath) { + var jestRuntime = { + exports: { + autoMockOff: function() { + this._shouldAutoMock = false; + return jestRuntime.exports; + }.bind(this), + + autoMockOn: function() { + this._shouldAutoMock = true; + return jestRuntime.exports; + }.bind(this), + + clearAllTimers: function() { + this._environment.fakeTimers.clearAllTimers(); + }.bind(this), + + dontMock: function(moduleName) { + var moduleID = this._getNormalizedModuleID(currPath, moduleName); + this._explicitShouldMock[moduleID] = false; + return jestRuntime.exports; + }.bind(this), + + genMockFromModule: function(moduleName) { + return this._generateMock( + this._currentlyExecutingModulePath, + moduleName + ); + }.bind(this), + + genMockFunction: function() { + return moduleMocker.getMockFunction(); + }, + + mock: function(moduleName) { + var moduleID = this._getNormalizedModuleID(currPath, moduleName); + this._explicitShouldMock[moduleID] = true; + return jestRuntime.exports; + }.bind(this), + + resetModuleRegistry: function() { + var globalMock; + for (var key in this._environment.global) { + globalMock = this._environment.global[key]; + if ((typeof globalMock === 'object' && globalMock !== null) + || typeof globalMock === 'function') { + globalMock._isMockFunction && globalMock.mockClear(); + } + } + + if (this._environment.global.mockClearTimers) { + this._environment.global.mockClearTimers(); + } + + this.resetModuleRegistry(); + + return jestRuntime.exports; + }.bind(this), + + runAllTicks: function() { + this._environment.fakeTimers.runAllTicks(); + }.bind(this), + + runAllTimers: function() { + this._environment.fakeTimers.runAllTimers(); + }.bind(this), + + runOnlyPendingTimers: function() { + this._environment.fakeTimers.runOnlyPendingTimers(); + }.bind(this), + + setMock: function(moduleName, moduleExports) { + var moduleID = this._getNormalizedModuleID(currPath, moduleName); + this._explicitShouldMock[moduleID] = true; + this._explicitlySetMocks[moduleID] = moduleExports; + return jestRuntime.exports; + }.bind(this) + } + }; + + // This is a pretty common API to use in many tests, so this is just a + // shorter alias to make it less annoying to type out each time. + jestRuntime.exports.genMockFn = jestRuntime.exports.genMockFunction; + + return jestRuntime; + }.bind(this), + + // This is a legacy API that will soon be deprecated. + // Don't use it for new stuff as it will go away soon! + 'node-haste': function() { + return { + exports: { + // Do not use this API -- it is deprecated and will go away very soon! + getResourceMap: function() { + return this._resourceMap; + }.bind(this) + } + }; + }.bind(this), + + // This is a legacy API that will soon be deprecated. + // Don't use it for new stuff as it will go away soon! + 'mocks': function(currPath) { + var mocks = { + exports: { + generateFromMetadata: moduleMocker.generateFromMetadata, + getMetadata: moduleMocker.getMetadata, + getMockFunction: function() { + return this.requireModule( + currPath, + 'jest-runtime' + ).genMockFn(); + }.bind(this), + } + }; + mocks.exports.getMockFn = mocks.exports.getMockFunction; + return mocks; + }.bind(this), + + // This is a legacy API that will soon be deprecated. + // Don't use it for new stuff as it will go away soon! + 'mock-modules': function(currPath) { + var mockModules = { + exports: { + dontMock: function(moduleName) { + this.requireModule( + currPath, + 'jest-runtime' + ).dontMock(moduleName); + return mockModules.exports; + }.bind(this), + + mock: function(moduleName) { + this.requireModule( + currPath, + 'jest-runtime' + ).mock(moduleName); + return mockModules.exports; + }.bind(this), + + autoMockOff: function() { + this.requireModule( + currPath, + 'jest-runtime' + ).autoMockOff(); + return mockModules.exports; + }.bind(this), + + autoMockOn: function() { + this.requireModule( + currPath, + 'jest-runtime' + ).autoMockOn(); + return mockModules.exports; + }.bind(this), + + // TODO: This is such a bad name, we should rename it to + // `resetModuleRegistry()` -- or anything else, really + dumpCache: function() { + this.requireModule( + currPath, + 'jest-runtime' + ).resetModuleRegistry(); + return mockModules.exports; + }.bind(this), + + setMock: function(moduleName, moduleExports) { + this.requireModule( + currPath, + 'jest-runtime' + ).setMock(moduleName, moduleExports); + return mockModules.exports; + }.bind(this), + + // wtf is this shit? + hasDependency: function(moduleAName, moduleBName) { + var traversedModules = {}; + + var self = this; + function _recurse(moduleAName, moduleBName) { + traversedModules[moduleAName] = true; + if (moduleAName === moduleBName) { + return true; + } + var moduleAResource = self._getResource('JS', moduleAName); + return !!( + moduleAResource + && moduleAResource.requiredModules + && moduleAResource.requiredModules.some(function(dep) { + return !traversedModules[dep] && _recurse(dep, moduleBName); + }) + ); + } + + return _recurse(moduleAName, moduleBName); + }.bind(this), + + generateMock: function(moduleName) { + return this.requireModule( + currPath, + 'jest-runtime' + ).genMockFromModule(moduleName); + }.bind(this), + + useActualTimers: function() { + this.requireModule( + currPath, + 'jest-runtime' + ).useActualTimers(); + }.bind(this), + + /** + * Load actual module without reading from or writing to module + * exports registry. This method's name is devastatingly misleading. + * :( + */ + loadActualModule: function(moduleName) { + return this.requireModule( + this._currentlyExecutingModulePath, + moduleName, + true // yay boolean args! + ); + }.bind(this) + } + }; + return mockModules; + }.bind(this) + }; +}; + +module.exports = Loader; diff --git a/src/HasteModuleLoader/__tests__/HasteModuleLoader-genMockFromModule-test.js b/src/HasteModuleLoader/__tests__/HasteModuleLoader-genMockFromModule-test.js new file mode 100644 index 000000000000..480f5fd1b19d --- /dev/null +++ b/src/HasteModuleLoader/__tests__/HasteModuleLoader-genMockFromModule-test.js @@ -0,0 +1,70 @@ +/** + * Copyright (c) 2014, Facebook, Inc. All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +'use strict'; + +jest.autoMockOff(); + +var path = require('path'); +var q = require('q'); + +describe('nodeHasteModuleLoader', function() { + var HasteModuleLoader; + var mockEnvironment; + var resourceMap; + + var CONFIG = { + name: 'nodeHasteModuleLoader-tests', + testPathDirs: [path.resolve(__dirname, 'test_root')] + }; + + function buildLoader(config) { + config = config || CONFIG; + if (!resourceMap) { + return HasteModuleLoader.loadResourceMap(config).then(function(map) { + resourceMap = map; + return buildLoader(config); + }); + } else { + return q(new HasteModuleLoader(config, mockEnvironment, resourceMap)); + } + } + + beforeEach(function() { + HasteModuleLoader = require('../HasteModuleLoader'); + + mockEnvironment = { + global: { + console: {}, + mockClearTimers: jest.genMockFn() + }, + runSourceText: jest.genMockFn().mockImplementation(function(codeStr) { + /* jshint evil:true */ + return (new Function('return ' + codeStr))(); + }) + }; + }); + + describe('genMockFromModule', function() { + pit('does not cause side effects in the rest of the module system when ' + + 'generating a mock', function() { + return buildLoader().then(function(loader) { + var testRequire = loader.requireModule.bind(loader, __filename); + + var regularModule = testRequire('RegularModule'); + var origModuleStateValue = regularModule.getModuleStateValue(); + + testRequire('jest-runtime').dontMock('RegularModule'); + + // Generate a mock for a module with side effects + testRequire('jest-runtime').genMockFromModule('ModuleWithSideEffects'); + + expect(regularModule.getModuleStateValue()).toBe(origModuleStateValue); + }); + }); + }); +}); diff --git a/src/HasteModuleLoader/__tests__/HasteModuleLoader-hasDependency-test.js b/src/HasteModuleLoader/__tests__/HasteModuleLoader-hasDependency-test.js new file mode 100644 index 000000000000..bd78cef6ffb3 --- /dev/null +++ b/src/HasteModuleLoader/__tests__/HasteModuleLoader-hasDependency-test.js @@ -0,0 +1,99 @@ +/** + * Copyright (c) 2014, Facebook, Inc. All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +'use strict'; + +jest.autoMockOff(); + +describe('nodeHasteModuleLoader', function() { + var HasteModuleLoader; + var mockEnvironment; + var resources; + + var mockResourceMap = { + getResource: function(type, name) { + if (!resources.hasOwnProperty(name)) { + return undefined; + } + return resources[name]; + } + }; + + function _generateResource(name, deps) { + deps = deps || []; + var resource = { + path: '/path/to/' + name + '.js', + id: name, + _requiredModuleMap: deps.reduce(function(prev, next) { + return prev[next] = true; + }, {}) + }; + if (deps.length) { + resource.requiredModules = deps; + } + return resource; + } + + beforeEach(function() { + HasteModuleLoader = require('../HasteModuleLoader'); + + mockEnvironment = { + global: { + console: {}, + mockClearTimers: jest.genMockFn() + }, + runSourceText: jest.genMockFn().mockImplementation(function(codeStr) { + /* jshint evil: true */ + return (new Function('return ' + codeStr))(); + }) + }; + resources = {}; + }); + + describe('hasDependency', function() { + pit('properly calculates direct 1-way dependencies', function() { + resources.ModuleA = _generateResource('ModuleA', ['ModuleB']); + resources.ModuleB = _generateResource('ModuleB'); + + var loader = new HasteModuleLoader({}, mockEnvironment, mockResourceMap); + var mockModules = loader.requireModule(__filename, 'mock-modules'); + expect(mockModules.hasDependency('ModuleA', 'ModuleB')).toBe(true); + expect(mockModules.hasDependency('ModuleB', 'ModuleA')).toBe(false); + }); + + pit('properly calculates direct cyclic dependencies', function() { + resources.ModuleA = _generateResource('ModuleA', ['ModuleB']); + resources.ModuleB = _generateResource('ModuleB', ['ModuleA']); + + var loader = new HasteModuleLoader({}, mockEnvironment, mockResourceMap); + var mockModules = loader.requireModule(__filename, 'mock-modules'); + expect(mockModules.hasDependency('ModuleA', 'ModuleB')).toBe(true); + expect(mockModules.hasDependency('ModuleB', 'ModuleA')).toBe(true); + }); + + pit('properly calculates indirect 1-way dependencies', function() { + resources.ModuleA = _generateResource('ModuleA', ['ModuleB']); + resources.ModuleB = _generateResource('ModuleB', ['ModuleC']); + resources.ModuleC = _generateResource('ModuleC'); + + var loader = new HasteModuleLoader({}, mockEnvironment, mockResourceMap); + var mockModules = loader.requireModule(__filename, 'mock-modules'); + expect(mockModules.hasDependency('ModuleA', 'ModuleC')).toBe(true); + }); + + pit('properly calculates indirect cyclic dependencies', function() { + resources.ModuleA = _generateResource('ModuleA', ['ModuleB']); + resources.ModuleB = _generateResource('ModuleB', ['ModuleC']); + resources.ModuleC = _generateResource('ModuleC', ['ModuleA']); + + var loader = new HasteModuleLoader({}, mockEnvironment, mockResourceMap); + var mockModules = loader.requireModule(__filename, 'mock-modules'); + expect(mockModules.hasDependency('ModuleA', 'ModuleC')).toBe(true); + expect(mockModules.hasDependency('ModuleC', 'ModuleA')).toBe(true); + }); + }); +}); diff --git a/src/HasteModuleLoader/__tests__/HasteModuleLoader-requireMock-test.js b/src/HasteModuleLoader/__tests__/HasteModuleLoader-requireMock-test.js new file mode 100644 index 000000000000..446424696687 --- /dev/null +++ b/src/HasteModuleLoader/__tests__/HasteModuleLoader-requireMock-test.js @@ -0,0 +1,148 @@ +/** + * Copyright (c) 2014, Facebook, Inc. All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +'use strict'; + +jest.autoMockOff(); + +var path = require('path'); +var q = require('q'); + +describe('nodeHasteModuleLoader', function() { + var HasteModuleLoader; + var mockEnvironment; + var resourceMap; + + var CONFIG = { + name: 'nodeHasteModuleLoader-tests', + testPathDirs: [path.resolve(__dirname, 'test_root')] + }; + + function buildLoader(config) { + config = config || CONFIG; + if (!resourceMap) { + return HasteModuleLoader.loadResourceMap(config).then(function(map) { + resourceMap = map; + return buildLoader(config); + }); + } else { + return q(new HasteModuleLoader(config, mockEnvironment, resourceMap)); + } + } + + beforeEach(function() { + HasteModuleLoader = require('../HasteModuleLoader'); + + mockEnvironment = { + global: { + console: {}, + mockClearTimers: jest.genMockFn() + }, + runSourceText: jest.genMockFn().mockImplementation(function(codeStr) { + /* jshint evil:true */ + return (new Function('return ' + codeStr))(); + }) + }; + }); + + describe('requireMock', function() { + pit('uses manual mocks before attempting to automock', function() { + return buildLoader().then(function(loader) { + var exports = loader.requireMock(null, 'ManuallyMocked'); + expect(exports.isManualMockModule).toBe(true); + }); + }); + + pit('stores and re-uses manual mock exports', function() { + return buildLoader().then(function(loader) { + var exports = loader.requireMock(null, 'ManuallyMocked'); + exports.setModuleStateValue('test value'); + exports = loader.requireMock(null, 'ManuallyMocked'); + expect(exports.getModuleStateValue()).toBe('test value'); + }); + }); + + pit('automocks @providesModule modules without a manual mock', function() { + return buildLoader().then(function(loader) { + var exports = loader.requireMock(null, 'RegularModule'); + expect(exports.getModuleStateValue._isMockFunction).toBe(true); + }); + }); + + pit('automocks relative-path modules without a file extension', function() { + return buildLoader().then(function(loader) { + var exports = loader.requireMock( + __filename, + './test_root/RegularModule' + ); + expect(exports.getModuleStateValue._isMockFunction).toBe(true); + }); + }); + + pit('automocks relative-path modules with a file extension', function() { + return buildLoader().then(function(loader) { + var exports = loader.requireMock( + __filename, + './test_root/RegularModule.js' + ); + expect(exports.getModuleStateValue._isMockFunction).toBe(true); + }); + }); + + pit('stores and re-uses automocked @providesModule exports', function() { + return buildLoader().then(function(loader) { + var exports = loader.requireMock(null, 'RegularModule'); + exports.externalMutation = 'test value'; + exports = loader.requireMock(null, 'RegularModule'); + expect(exports.externalMutation).toBe('test value'); + }); + }); + + pit('stores and re-uses automocked relative-path modules', function() { + return buildLoader().then(function(loader) { + var exports = loader.requireMock( + __filename, + './test_root/RegularModule' + ); + exports.externalMutation = 'test value'; + exports = loader.requireMock( + __filename, + './test_root/RegularModule' + ); + expect(exports.externalMutation).toBe('test value'); + }); + }); + + pit('throws on non-existant @providesModule modules', function() { + return buildLoader().then(function(loader) { + expect(function() { + loader.requireMock(null, 'DoesntExist'); + }).toThrow(); + }); + }); + + pit('uses the closest manual mock when duplicates exist', function() { + return buildLoader().then(function(loader) { + var exports1 = loader.requireMock( + __dirname, + path.resolve(__dirname, './test_root/subdir1/MyModule') + ); + expect(exports1.modulePath).toEqual( + 'subdir1/__mocks__/MyModule.js' + ); + + var exports2 = loader.requireMock( + __dirname, + path.resolve(__dirname, './test_root/subdir2/MyModule') + ); + expect(exports2.modulePath).toEqual( + 'subdir2/__mocks__/MyModule.js' + ); + }); + }); + }); +}); diff --git a/src/HasteModuleLoader/__tests__/HasteModuleLoader-requireModule-test.js b/src/HasteModuleLoader/__tests__/HasteModuleLoader-requireModule-test.js new file mode 100644 index 000000000000..850e26a19b5d --- /dev/null +++ b/src/HasteModuleLoader/__tests__/HasteModuleLoader-requireModule-test.js @@ -0,0 +1,211 @@ +/** + * Copyright (c) 2014, Facebook, Inc. All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +'use strict'; + +jest.autoMockOff(); + +var path = require('path'); +var q = require('q'); + +describe('nodeHasteModuleLoader', function() { + var HasteModuleLoader; + var mockEnvironment; + var resourceMap; + + var CONFIG = { + name: 'nodeHasteModuleLoader-tests', + testPathDirs: [path.resolve(__dirname, 'test_root')] + }; + + function buildLoader(config) { + config = config || CONFIG; + if (!resourceMap) { + return HasteModuleLoader.loadResourceMap(config).then(function(map) { + resourceMap = map; + return buildLoader(config); + }); + } else { + return q(new HasteModuleLoader(config, mockEnvironment, resourceMap)); + } + } + + beforeEach(function() { + HasteModuleLoader = require('../HasteModuleLoader'); + + mockEnvironment = { + global: { + console: {}, + mockClearTimers: jest.genMockFn() + }, + runSourceText: jest.genMockFn().mockImplementation(function(codeStr) { + /* jshint evil:true */ + return (new Function('return ' + codeStr))(); + }) + }; + }); + + describe('requireModule', function() { + pit('finds @providesModule modules', function() { + return buildLoader().then(function(loader) { + var exports = loader.requireModule(null, 'RegularModule'); + expect(exports.isRealModule).toBe(true); + }); + }); + + pit('throws on non-existant @providesModule modules', function() { + return buildLoader().then(function(loader) { + expect(function() { + loader.requireModule(null, 'DoesntExist'); + }).toThrow('Cannot find module \'DoesntExist\' from \'.\''); + }); + }); + + pit('finds relative-path modules without file extension', function() { + return buildLoader().then(function(loader) { + var exports = loader.requireModule( + __filename, + './test_root/RegularModule' + ); + expect(exports.isRealModule).toBe(true); + }); + }); + + pit('finds relative-path modules with file extension', function() { + return buildLoader().then(function(loader) { + var exports = loader.requireModule( + __filename, + './test_root/RegularModule.js' + ); + expect(exports.isRealModule).toBe(true); + }); + }); + + pit('throws on non-existant relative-path modules', function() { + return buildLoader().then(function(loader) { + expect(function() { + loader.requireModule(__filename, './DoesntExist'); + }).toThrow( + 'Cannot find module \'./DoesntExist\' from \'' + __filename + '\'' + ); + }); + }); + + pit('finds node core built-in modules', function() { + return buildLoader().then(function(loader) { + expect(function() { + loader.requireModule(null, 'fs'); + }).not.toThrow(); + }); + }); + + pit('finds and loads JSON files without file extension', function() { + return buildLoader().then(function(loader) { + var exports = loader.requireModule(__filename, './test_root/JSONFile'); + expect(exports.isJSONModule).toBe(true); + }); + }); + + pit('finds and loads JSON files with file extension', function() { + return buildLoader().then(function(loader) { + var exports = loader.requireModule( + __filename, + './test_root/JSONFile.json' + ); + expect(exports.isJSONModule).toBe(true); + }); + }); + + describe('features I want to remove, but must exist for now', function() { + /** + * I'd like to kill this and make all tests use something more explicit + * when they want a manual mock, like: + * + * require.mock('MyManualMock'); + * var ManuallyMocked = require('ManuallyMocked'); + * + * --or-- + * + * var ManuallyMocked = require.manualMock('ManuallyMocked'); + * + * For now, however, this is built-in and many tests rely on it, so we + * must support it until we can do some cleanup. + */ + pit('provides manual mock when real module doesnt exist', function() { + return buildLoader().then(function(loader) { + var exports = loader.requireModule( + __filename, + 'ExclusivelyManualMock' + ); + expect(exports.isExclusivelyManualMockModule).toBe(true); + }); + }); + + /** + * requireModule() should *always* return the real module. Mocks should + * only be returned by requireMock(). + * + * See the 'overrides real modules with manual mock when one exists' test + * for more info on why I want to kill this feature. + */ + pit('doesnt override real modules with manual mocks when explicitly ' + + 'marked with .dontMock()', function() { + return buildLoader().then(function(loader) { + loader.requireModule(__filename, 'jest-runtime') + .dontMock('ManuallyMocked'); + + var exports = loader.requireModule(__filename, 'ManuallyMocked'); + expect(exports.isManualMockModule).toBe(false); + }); + }); + + /** + * This test is only in this section because it seems sketchy to be able + * to load up a module without pulling it from the registry. I need to do + * more investigation to understand the reasoning behind this before I + * declare it unnecessary and condemn it. + */ + pit('doesnt read from the module registry when bypassModuleRegistry is ' + + 'set', function() { + return buildLoader().then(function(loader) { + var registryExports = loader.requireModule( + __filename, + 'RegularModule' + ); + registryExports.setModuleStateValue('registry'); + + var bypassedExports = loader.requireModule( + __filename, + 'RegularModule', + true + ); + expect(bypassedExports.getModuleStateValue()).not.toBe('registry'); + }); + }); + + pit('doesnt write to the module registry when bypassModuleRegistry is ' + + 'set', function() { + return buildLoader().then(function(loader) { + var registryExports = loader.requireModule( + __filename, + 'RegularModule' + ); + registryExports.setModuleStateValue('registry'); + + var bypassedExports = loader.requireModule( + __filename, + 'RegularModule', + true + ); + bypassedExports.setModuleStateValue('bypassed'); + + expect(registryExports.getModuleStateValue()).toBe('registry'); + }); + }); + }); + }); +}); diff --git a/src/HasteModuleLoader/__tests__/HasteModuleLoader-requireModuleOrMock-test.js b/src/HasteModuleLoader/__tests__/HasteModuleLoader-requireModuleOrMock-test.js new file mode 100644 index 000000000000..b79edc6b48af --- /dev/null +++ b/src/HasteModuleLoader/__tests__/HasteModuleLoader-requireModuleOrMock-test.js @@ -0,0 +1,103 @@ +/** + * Copyright (c) 2014, Facebook, Inc. All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +'use strict'; + +jest.autoMockOff(); + +var path = require('path'); +var q = require('q'); + +describe('nodeHasteModuleLoader', function() { + var HasteModuleLoader; + var mockEnvironment; + var resourceMap; + + var CONFIG = { + name: 'nodeHasteModuleLoader-tests', + testPathDirs: [path.resolve(__dirname, 'test_root')] + }; + + function buildLoader(config) { + config = config || CONFIG; + if (!resourceMap) { + return HasteModuleLoader.loadResourceMap(config).then(function(map) { + resourceMap = map; + return buildLoader(config); + }); + } else { + return q(new HasteModuleLoader(config, mockEnvironment, resourceMap)); + } + } + + beforeEach(function() { + HasteModuleLoader = require('../HasteModuleLoader'); + + mockEnvironment = { + global: { + console: {}, + mockClearTimers: jest.genMockFn() + }, + runSourceText: jest.genMockFn().mockImplementation(function(codeStr) { + /* jshint evil:true */ + return (new Function('return ' + codeStr))(); + }) + }; + }); + + describe('requireModuleOrMock', function() { + pit('mocks modules by default', function() { + return buildLoader().then(function(loader) { + var exports = loader.requireModuleOrMock(null, 'RegularModule'); + expect(exports.setModuleStateValue._isMockFunction).toBe(true); + }); + }); + + pit('doesnt mock modules when explicitly dontMock()ed', function() { + return buildLoader().then(function(loader) { + loader.requireModuleOrMock(null, 'jest-runtime') + .dontMock('RegularModule'); + var exports = loader.requireModuleOrMock(null, 'RegularModule'); + expect(exports.isRealModule).toBe(true); + }); + }); + + pit('doesnt mock modules when explicitly dontMock()ed via a different ' + + 'denormalized module name', function() { + return buildLoader().then(function(loader) { + loader.requireModuleOrMock(__filename, 'jest-runtime') + .dontMock('./test_root/RegularModule'); + var exports = loader.requireModuleOrMock(__filename, 'RegularModule'); + expect(exports.isRealModule).toBe(true); + }); + }); + + pit('doesnt mock modules when autoMockOff() has been called', function() { + return buildLoader().then(function(loader) { + loader.requireModuleOrMock(null, 'jest-runtime').autoMockOff(); + var exports = loader.requireModuleOrMock(null, 'RegularModule'); + expect(exports.isRealModule).toBe(true); + }); + }); + + pit('uses manual mock when automocking on and mock is avail', function() { + return buildLoader().then(function(loader) { + var exports = loader.requireModuleOrMock(null, 'ManuallyMocked'); + expect(exports.isManualMockModule).toBe(true); + }); + }); + + pit('does not use manual mock when automocking is off and a real ' + + 'module is available', function() { + return buildLoader().then(function(loader) { + loader.requireModuleOrMock(__filename, 'jest-runtime').autoMockOff(); + var exports = loader.requireModuleOrMock(__filename, 'ManuallyMocked'); + expect(exports.isManualMockModule).toBe(false); + }); + }); + }); +}); diff --git a/src/HasteModuleLoader/__tests__/test_root/JSONFile.json b/src/HasteModuleLoader/__tests__/test_root/JSONFile.json new file mode 100644 index 000000000000..30ce67d73b0e --- /dev/null +++ b/src/HasteModuleLoader/__tests__/test_root/JSONFile.json @@ -0,0 +1 @@ +{"isJSONModule": true} diff --git a/src/HasteModuleLoader/__tests__/test_root/ManuallyMocked.js b/src/HasteModuleLoader/__tests__/test_root/ManuallyMocked.js new file mode 100644 index 000000000000..eee3178070be --- /dev/null +++ b/src/HasteModuleLoader/__tests__/test_root/ManuallyMocked.js @@ -0,0 +1,12 @@ +/** + * Copyright (c) 2014, Facebook, Inc. All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @providesModule ManuallyMocked + */ + + +exports.isManualMockModule = false; diff --git a/src/HasteModuleLoader/__tests__/test_root/ModuleWithSideEffects.js b/src/HasteModuleLoader/__tests__/test_root/ModuleWithSideEffects.js new file mode 100644 index 000000000000..558755386c8a --- /dev/null +++ b/src/HasteModuleLoader/__tests__/test_root/ModuleWithSideEffects.js @@ -0,0 +1,15 @@ +/** + * Copyright (c) 2014, Facebook, Inc. All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @providesModule ModuleWithSideEffects + */ + +'use strict'; + +var RegularModule = require('RegularModule'); + +RegularModule.setModuleStateValue('Side effect value'); diff --git a/src/HasteModuleLoader/__tests__/test_root/RegularModule.js b/src/HasteModuleLoader/__tests__/test_root/RegularModule.js new file mode 100644 index 000000000000..c2bf9bb50c3d --- /dev/null +++ b/src/HasteModuleLoader/__tests__/test_root/RegularModule.js @@ -0,0 +1,25 @@ +/** + * Copyright (c) 2014, Facebook, Inc. All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @providesModule RegularModule + */ + +'use strict'; + +var moduleStateValue = 'default'; + +function setModuleStateValue(value) { + moduleStateValue = value; +} + +function getModuleStateValue() { + return moduleStateValue; +} + +exports.getModuleStateValue = getModuleStateValue; +exports.isRealModule = true; +exports.setModuleStateValue = setModuleStateValue; diff --git a/src/HasteModuleLoader/__tests__/test_root/__mocks__/ExclusivelyManualMock.js b/src/HasteModuleLoader/__tests__/test_root/__mocks__/ExclusivelyManualMock.js new file mode 100644 index 000000000000..b27745e65b66 --- /dev/null +++ b/src/HasteModuleLoader/__tests__/test_root/__mocks__/ExclusivelyManualMock.js @@ -0,0 +1,8 @@ +/** + * Copyright (c) 2014, Facebook, Inc. All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +exports.isExclusivelyManualMockModule = true; diff --git a/src/HasteModuleLoader/__tests__/test_root/__mocks__/ManuallyMocked.js b/src/HasteModuleLoader/__tests__/test_root/__mocks__/ManuallyMocked.js new file mode 100644 index 000000000000..037f17c1567b --- /dev/null +++ b/src/HasteModuleLoader/__tests__/test_root/__mocks__/ManuallyMocked.js @@ -0,0 +1,22 @@ +/** + * Copyright (c) 2014, Facebook, Inc. All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +'use strict'; + +var moduleStateValue = 'default'; + +function setModuleStateValue(value) { + moduleStateValue = value; +} + +function getModuleStateValue() { + return moduleStateValue; +} + +exports.getModuleStateValue = getModuleStateValue; +exports.isManualMockModule = true; +exports.setModuleStateValue = setModuleStateValue; diff --git a/src/HasteModuleLoader/__tests__/test_root/subdir1/MyModule.js b/src/HasteModuleLoader/__tests__/test_root/subdir1/MyModule.js new file mode 100644 index 000000000000..cd082b077de1 --- /dev/null +++ b/src/HasteModuleLoader/__tests__/test_root/subdir1/MyModule.js @@ -0,0 +1,8 @@ +/** + * Copyright (c) 2014, Facebook, Inc. All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +exports.modulePath = 'subdir1/MyModule.js'; diff --git a/src/HasteModuleLoader/__tests__/test_root/subdir1/__mocks__/MyModule.js b/src/HasteModuleLoader/__tests__/test_root/subdir1/__mocks__/MyModule.js new file mode 100644 index 000000000000..a1eccfbbab92 --- /dev/null +++ b/src/HasteModuleLoader/__tests__/test_root/subdir1/__mocks__/MyModule.js @@ -0,0 +1,8 @@ +/** + * Copyright (c) 2014, Facebook, Inc. All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +exports.modulePath = 'subdir1/__mocks__/MyModule.js'; diff --git a/src/HasteModuleLoader/__tests__/test_root/subdir2/MyModule.js b/src/HasteModuleLoader/__tests__/test_root/subdir2/MyModule.js new file mode 100644 index 000000000000..65e1483978fd --- /dev/null +++ b/src/HasteModuleLoader/__tests__/test_root/subdir2/MyModule.js @@ -0,0 +1,8 @@ +/** + * Copyright (c) 2014, Facebook, Inc. All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +exports.modulePath = 'subdir2/MyModule.js'; diff --git a/src/HasteModuleLoader/__tests__/test_root/subdir2/__mocks__/MyModule.js b/src/HasteModuleLoader/__tests__/test_root/subdir2/__mocks__/MyModule.js new file mode 100644 index 000000000000..1aea2e8ed627 --- /dev/null +++ b/src/HasteModuleLoader/__tests__/test_root/subdir2/__mocks__/MyModule.js @@ -0,0 +1,8 @@ +/** + * Copyright (c) 2014, Facebook, Inc. All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +exports.modulePath = 'subdir2/__mocks__/MyModule.js'; diff --git a/src/JSDomEnvironment.js b/src/JSDomEnvironment.js new file mode 100644 index 000000000000..eb7bef43b345 --- /dev/null +++ b/src/JSDomEnvironment.js @@ -0,0 +1,97 @@ +/** + * Copyright (c) 2014, Facebook, Inc. All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +'use strict'; + +var FakeTimers = require('./lib/FakeTimers'); + +function JSDomEnvironment() { + // We lazily require jsdom because it takes a good ~.5s to load. + // + // Since this file may be require'd at the top of other files that may/may not + // use it (depending on the context -- such as TestRunner.js when operating as + // a workerpool parent), this is the best way to ensure we only spend time + // require()ing this when necessary. + this.global = require('./lib/jsdom-compat').jsdom().parentWindow; + + // Setup window.location + Object.defineProperty(this.global.location, 'hostname', { + value: '' + }); + Object.defineProperty(this.global.location, 'host', { + value: '' + }); + + // Setup defaults for navigator.onLine + // TODO: It's questionable as to whether this should go here + // It's a pretty rarely depended on feature, so maybe tests that care + // about it should just shim it themselves?) + this.global.navigator.onLine = true; + + // Pass through the node implementation of some needed APIs + this.global.ArrayBuffer = ArrayBuffer; + this.global.Float32Array = Float32Array; + this.global.Int16Array = Int16Array; + this.global.Int32Array = Int32Array; + this.global.Int8Array = Int8Array; + this.global.Uint8Array = Uint8Array; + this.global.Uint16Array = Uint16Array; + this.global.Uint32Array = Uint32Array; + this.global.DataView = DataView; + this.global.Buffer = Buffer; + this.global.process = process; + + this.fakeTimers = new FakeTimers(this.global); + + // I kinda wish tests just did this manually rather than relying on a + // helper function to do it, but I'm keeping it for backward compat reasons + // while we get jest deployed internally. Then we can look into removing it. + // + // #3376754 + if (!this.global.hasOwnProperty('mockSetReadOnlyProperty')) { + this.global.mockSetReadOnlyProperty = function(obj, property, value) { + obj.__defineGetter__(property, function() { + return value; + }); + }; + } + + // jsdom doesn't have support for window.Image, so we just replace it with a + // dummy constructor + try { + /* jshint nonew:false */ + new this.global.Image(); + } catch (e) { + this.global.Image = function Image() {}; + } + + // Pass through the node `process` global. + // TODO: Consider locking this down somehow so tests can't do crazy stuff to + // worker processes... + this.global.process = process; +} + +JSDomEnvironment.prototype.dispose = function() { + // TODO: In node 0.8.8 (at least), closing each jsdom context appears to + // reproducibly cause a worker to segfault after a large number of tests + // have run (repro cases had > 1200). + // + // Disabling this to solve the problem for now -- but we should come + // back and investigate this soon. There's a reasonable chance that not + // closing out our contexts is eating an absurd amount of memory... + //this.global.close(); +}; + +JSDomEnvironment.prototype.runSourceText = function(sourceText, fileName) { + return this.global.run(sourceText, fileName); +}; + +JSDomEnvironment.prototype.runWithRealTimers = function(cb) { + this._fakeTimers.runWithRealTimers(cb); +}; + +module.exports = JSDomEnvironment; diff --git a/src/TestRunner.js b/src/TestRunner.js new file mode 100644 index 000000000000..cacaf22ca709 --- /dev/null +++ b/src/TestRunner.js @@ -0,0 +1,494 @@ +/** + * Copyright (c) 2014, Facebook, Inc. All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +'use strict'; + +var FileFinder = require('node-find-files'); +var fs = require('fs'); +var os = require('os'); +var path = require('path'); +var q = require('q'); +var utils = require('./lib/utils'); +var WorkerPool = require('node-worker-pool'); + +var TEST_WORKER_PATH = require.resolve('./TestWorker'); + +var DEFAULT_OPTIONS = { + /** + * The maximum number of workers to run tests concurrently with. + * + * It's probably good to keep this at something close to the number of cores + * on the machine that's running the test. + */ + maxWorkers: os.cpus().length - 1, + + /** + * The path to the executable node binary. + * + * This is used in the process of booting each of the workers. + */ + nodePath: process.execPath, + + /** + * The args to be passed to the node binary executable. + * + * this is used in the process of booting each of the workers. + */ + nodeArgv: process.execArgv.filter(function(arg) { + // Passing --debug off to child processes can screw with socket connections + // of the parent process. + return arg !== '--debug'; + }) +}; + +var HIDDEN_FILE_RE = /\/\.[^\/]*$/; + +function _serializeConsoleArguments(type, args) { + return { + type: type, + args: Array.prototype.map.call(args, function(arg) { + return utils.serializeConsoleArgValue(arg); + }) + }; +} + +/** + * A class that takes a project's test config and provides various utilities for + * executing its tests. + * + * @param config The jest configuration + * @param options See DEFAULT_OPTIONS for descriptions on the various options + * and their defaults. + */ +function TestRunner(config, options) { + this._config = config; + this._configDeps = null; + this._moduleLoaderResourceMap = null; + this._testPathDirsRegExp = new RegExp( + config.testPathDirs + .map(function(dir) { + return dir.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&'); + }) + .join('|') + ); + this._nodeHasteTestRegExp = new RegExp( + '/__tests__/.*\\.(' + config.testFileExtensions.join('|') + ')$' + ); + + this._opts = Object.create(DEFAULT_OPTIONS); + if (options) { + for (var key in options) { + this._opts[key] = options[key]; + } + } +} + +TestRunner.prototype._constructModuleLoader = function(environment, customCfg) { + var config = customCfg || this._config; + var ModuleLoader = this._loadConfigDependencies().ModuleLoader; + return this._getModuleLoaderResourceMap().then(function(resourceMap) { + return new ModuleLoader(config, environment, resourceMap); + }); +}; + +TestRunner.prototype._getModuleLoaderResourceMap = function() { + var ModuleLoader = this._loadConfigDependencies().ModuleLoader; + if (this._moduleLoaderResourceMap === null) { + if (this._opts.useCachedModuleLoaderResourceMap) { + this._moduleLoaderResourceMap = + ModuleLoader.loadResourceMapFromCacheFile(this._config); + } else { + this._moduleLoaderResourceMap = + ModuleLoader.loadResourceMap(this._config); + } + } + return this._moduleLoaderResourceMap; +}; + +TestRunner.prototype._isTestFilePath = function(filePath) { + var testPathIgnorePattern = + this._config.testPathIgnorePatterns + ? new RegExp(this._config.testPathIgnorePatterns.join('|')) + : null; + + return ( + this._nodeHasteTestRegExp.test(filePath) + && !HIDDEN_FILE_RE.test(filePath) + && (!testPathIgnorePattern || !testPathIgnorePattern.test(filePath)) + && this._testPathDirsRegExp.test(filePath) + ); +}; + +TestRunner.prototype._loadConfigDependencies = function() { + var config = this._config; + if (this._configDeps === null) { + this._configDeps = { + ModuleLoader: require(config.moduleLoader), + testEnvironment: require(config.testEnvironment), + testRunner: require(config.testRunner).bind(null) + }; + } + return this._configDeps; +}; + +/** + * Given a list of paths to modules or tests, find all tests that are related to + * any of those paths. For a test to be considered "related" to a path, the test + * must depend on that path (either directly, or indirectly through one of its + * direct dependencies). + * + * @param {Array} paths A list of path strings to find related tests for + * @return {Promise>} Fulfilled with a list of testPaths once the + * search has completed. + */ +TestRunner.prototype.findTestsRelatedTo = function(paths) { + var testRunner = this; + return this._constructModuleLoader().then(function(moduleLoader) { + var discoveredModules = {}; + + // If a path to a test file is given, make sure we consider that test as + // related to itself... + // + // (If any of the supplied paths aren't tests, it's ok because we filter + // non-tests out at the end) + paths.forEach(function(path) { + discoveredModules[path] = true; + }); + + var modulesToSearch = [].concat(paths); + while (modulesToSearch.length > 0) { + var modulePath = modulesToSearch.shift(); + var depPaths = moduleLoader.getDependentsFromPath(modulePath); + + /* jshint loopfunc:true */ + depPaths.forEach(function(depPath) { + if (!discoveredModules.hasOwnProperty(depPath)) { + discoveredModules[depPath] = true; + modulesToSearch.push(depPath); + } + }); + } + + return Object.keys(discoveredModules).filter(function(path) { + return testRunner._isTestFilePath(path) && fs.existsSync(path); + }); + }); +}; + +/** + * Given a path pattern, find the absolute paths for all tests that match the + * pattern. + * + * @param {RegExp} pathPattern + * @param {Function} onTestFound Callback called immediately when a test is + * found. + * + * Ideally this function should return a + * stream, but I don't personally understand all + * the variations of "node streams" that exist in + * the world (and their various compatibilities + * with various node versions), so I've opted to + * forgo that for now. + * @return {Promise>} Fulfilled with a list of testPaths once the + * search has completed. + */ +TestRunner.prototype.findTestPathsMatching = function( + pathPattern, onTestFound) { + + var config = this._config; + var deferred = q.defer(); + + var foundPaths = []; + function _onMatcherMatch(pathStr) { + foundPaths.push(pathStr); + try { + onTestFound && onTestFound(pathStr); + } catch (e) { + deferred.reject(e); + } + } + + var numMatchers = config.testPathDirs.length; + function _onMatcherEnd() { + numMatchers--; + if (numMatchers === 0) { + deferred.resolve(foundPaths); + } + } + + function _onMatcherError(err) { + deferred.reject(err); + } + + config.testPathDirs.forEach(function(scanDir) { + var finder = new FileFinder({ + rootFolder: scanDir, + filterFunction: function(pathStr) { + return this._isTestFilePath(pathStr) && pathPattern.test(pathStr); + }.bind(this) + }); + finder.on('error', _onMatcherError); + finder.on('match', _onMatcherMatch); + finder.on('complete', _onMatcherEnd); + finder.startSearch(); + }, this); + + return deferred.promise; +}; + +/** + * For use by external users of TestRunner as a means of optimization. + * + * Imagine the following scenario executing in a child worker process: + * + * var runner = new TestRunner(config, { + * moduleLoaderResourceMap: serializedResourceMap + * }); + * someOtherAyncProcess.then(function() { + * runner.runTestsParallel(); + * }); + * + * Here we wouldn't start deserializing the resource map (passed to us from the + * parent) until runner.runTestsParallel() is called. At the time of this + * writing, resource map deserialization is slow and a bottleneck on running the + * first test in a child. + * + * So this API gives scenarios such as the one above an optimization path to + * potentially start deserializing the resource map while we wait on the + * someOtherAsyncProcess to resolve (rather that doing it after it's resolved). + */ +TestRunner.prototype.preloadResourceMap = function() { + this._getModuleLoaderResourceMap().done(); +}; + +TestRunner.prototype.preloadConfigDependencies = function() { + this._loadConfigDependencies(); +}; + +/** + * Run the given single test file path. + * This just contains logic for running a single test given it's file path. + * + * @param {String} testFilePath + * @return {Promise} Results of the test + */ +TestRunner.prototype.runTest = function(testFilePath) { + // Using Object.create() lets us adjust the config object locally without + // worrying about the external consequences of changing the config object for + // needs that are local to this particular function call + var config = Object.create(this._config); + var configDeps = this._loadConfigDependencies(); + + var env = new configDeps.testEnvironment(); + var testRunner = configDeps.testRunner; + + // Capture and serialize console.{log|warning|error}s so they can be passed + // around (such as through some channel back to a parent process) + var consoleMessages = []; + env.global.console = { + error: function() { + consoleMessages.push(_serializeConsoleArguments('error', arguments)); + }, + + group: function() { + // TODO + }, + + groupCollapsed: function() { + // TODO + }, + + groupEnd: function() { + // TODO + }, + + log: function() { + consoleMessages.push(_serializeConsoleArguments('log', arguments)); + }, + + table: function() { + // TODO + }, + + warn: function() { + consoleMessages.push(_serializeConsoleArguments('warn', arguments)); + } + }; + + return this._constructModuleLoader(env, config).then(function(moduleLoader) { + // This is a kind of janky way to ensure that we only collect coverage + // information on modules that are immediate dependencies of the test file. + // + // Collecting coverage info on more than that is often not useful as + // *usually*, when one is looking for coverage info, one is only looking + // for coverage info on the files under test. Since a test file is just a + // regular old module that can depend on whatever other modules it likes, + // it's usually pretty hard to tell which of those dependencies is/are the + // "module(s)" under test. + // + // I'm not super happy with having to inject stuff into the config object + // mid-stream here, but it gets the job done. + if (config.collectCoverage && !config.collectCoverageOnlyFrom) { + config.collectCoverageOnlyFrom = {}; + moduleLoader.getDependenciesFromPath(testFilePath) + .filter(function(depPath) { + // Skip over built-in and node modules + return /^\//.test(depPath); + }).forEach(function(depPath) { + config.collectCoverageOnlyFrom[depPath] = true; + }); + } + + if (config.setupEnvScriptFile) { + utils.runContentWithLocalBindings( + env.runSourceText.bind(env), + utils.readAndPreprocessFileContent(config.setupEnvScriptFile, config), + config.setupEnvScriptFile, + { + __dirname: path.dirname(config.setupEnvScriptFile), + __filename: config.setupEnvScriptFile, + require: moduleLoader.constructBoundRequire( + config.setupEnvScriptFile + ) + } + ); + } + + var testExecStats = {start: Date.now()}; + return testRunner(config, env, moduleLoader, testFilePath) + .then(function(results) { + testExecStats.end = Date.now(); + + results.logMessages = consoleMessages; + results.perfStats = testExecStats; + results.testFilePath = testFilePath; + results.coverage = + config.collectCoverage + ? moduleLoader.getAllCoverageInfo() + : {}; + + return results; + }); + }).finally(function() { + env.dispose(); + }); +}; + +/** + * Run all given test paths serially (in the current process). + * + * This is mostly useful for debugging issues with jest itself, but may also be + * useful for scenarios where you don't want jest to start up a worker pool of + * its own. + * + * @param {Array} testPaths Array of paths to test files + * @param {Function} onResult Callback called once for each test result + * @return {Promise} Fulfilled with aggregate pass/fail information + * about all tests that were run + */ +TestRunner.prototype.runTestsInBand = function(testPaths, onResult) { + var config = this._config; + + var aggregatedResults = { + numFailedTests: 0, + numTotalTests: testPaths.length, + startTime: Date.now(), + endTime: null + }; + + var testSequence = q(); + testPaths.forEach(function(testPath) { + testSequence = testSequence.then(this.runTest.bind(this, testPath)) + .then(function(testResult) { + if (testResult.numFailingTests > 0) { + aggregatedResults.numFailedTests++; + } + onResult && onResult(config, testResult); + }) + .catch(function(err) { + aggregatedResults.numFailedTests++; + onResult && onResult(config, { + testFilePath: testPath, + testExecError: err, + suites: {}, + tests: {}, + logMessages: [] + }); + }); + }, this); + + return testSequence.then(function() { + aggregatedResults.endTime = Date.now(); + return aggregatedResults; + }); +}; + +/** + * Run all given test paths in parallel using a worker pool. + * + * @param {Array} testPaths Array of paths to test files + * @param {Function} onResult Callback called once for each test result + * @return {Promise} Fulfilled with aggregate pass/fail information + * about all tests that were run + */ +TestRunner.prototype.runTestsParallel = function(testPaths, onResult) { + var config = this._config; + + var aggregatedResults = { + numFailedTests: 0, + numTotalTests: testPaths.length, + startTime: Date.now(), + endTime: null + }; + + var workerPool = new WorkerPool( + this._opts.maxWorkers, + this._opts.nodePath, + this._opts.nodeArgv.concat([ + '--harmony', + TEST_WORKER_PATH, + '--config=' + JSON.stringify(config) + ]) + ); + + return this._getModuleLoaderResourceMap() + .then(function() { + // Tell all workers that it's now safe to read the resource map from disk. + return workerPool.sendMessageToAllWorkers({ + resourceMapWrittenToDisk: true + }); + }) + .then(function() { + return q.all(testPaths.map(function(testPath) { + return workerPool.sendMessage({testFilePath: testPath}) + .then(function(testResult) { + if (testResult.numFailingTests > 0) { + aggregatedResults.numFailedTests++; + } + onResult && onResult(config, testResult); + }) + .catch(function(err) { + aggregatedResults.numFailedTests++; + onResult(config, { + testFilePath: testPath, + testExecError: err, + suites: {}, + tests: {}, + logMessages: [] + }); + }); + })); + }) + .then(function() { + return workerPool.destroy().then(function() { + aggregatedResults.endTime = Date.now(); + return aggregatedResults; + }); + }); +}; + +module.exports = TestRunner; diff --git a/src/TestWorker.js b/src/TestWorker.js new file mode 100644 index 000000000000..bd4ff857de2c --- /dev/null +++ b/src/TestWorker.js @@ -0,0 +1,69 @@ +/** + * Copyright (c) 2014, Facebook, Inc. All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +'use strict'; + +var optimist = require('optimist'); +var q = require('q'); +var TestRunner = require('./TestRunner'); +var workerUtils = require('node-worker-pool/nodeWorkerUtils'); + +if (require.main === module) { + try { + var argv = optimist.demand(['config']).argv; + var config = JSON.parse(argv.config); + + var testRunner = null; + /* jshint -W082:true */ + function onMessage(message) { + if (testRunner === null) { + // Quick sanity-check assertion that the first message is just an + // indicator that the resourceMap is ready to be read from disk + // (and not a "run-test" instruction") + if (!message.hasOwnProperty('resourceMapWrittenToDisk') + || message.resourceMapWrittenToDisk !== true) { + throw new Error( + 'Received an unexpected initialization message from worker ' + + 'coordinator: ' + JSON.stringify(message) + ); + } + + testRunner = new TestRunner(config, { + useCachedModuleLoaderResourceMap: true, + }); + + // Start require()ing config dependencies now. + // + // Config dependencies are entries in the config that are require()d (in + // order to be pluggable) such as 'moduleLoader' or + // 'testEnvironment'. + testRunner.preloadConfigDependencies(); + + // Start deserializing the resource map to get a potential head-start on + // that work before the first "run-test" message comes in. + // + // This is just a perf optimization -- and it is only an optimization + // some of the time (when the there is any significant idle time between + // this first initialization message and the first "run-rest" message). + // + // It is also only an optimization so long as deserialization of the + // resource map is a bottleneck (which is the case at the time of this + // writing). + testRunner.preloadResourceMap(); + + // Dummy response + return q({}); + } else { + return testRunner.runTest(message.testFilePath); + } + } + + workerUtils.startWorker(null, onMessage); + } catch (e) { + workerUtils.respondWithError(e); + } +} diff --git a/src/__tests__/TestRunner-test.js b/src/__tests__/TestRunner-test.js new file mode 100644 index 000000000000..cdccb0fcf9fb --- /dev/null +++ b/src/__tests__/TestRunner-test.js @@ -0,0 +1,142 @@ +/** + * Copyright (c) 2014, Facebook, Inc. All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +'use strict'; + +require('jest-runtime') + .autoMockOff() + .mock('fs'); + +var q = require('q'); + +describe('TestRunner', function() { + var TestRunner; + + beforeEach(function() { + TestRunner = require('../TestRunner'); + }); + + describe('findTestsRelatedTo', function() { + var fakeDepsFromPath; + var fs; + var runner; + var utils; + + beforeEach(function() { + fs = require('fs'); + utils = require('../lib/utils'); + runner = new TestRunner(utils.normalizeConfig({ + rootDir: '.', + testPathDirs: [] + })); + + fakeDepsFromPath = {}; + runner._constructModuleLoader = function() { + return q({ + getDependentsFromPath: function(modulePath) { + return fakeDepsFromPath[modulePath] || []; + } + }); + }; + }); + + pit('finds no tests when no tests depend on the path', function() { + var path = '/path/to/module/not/covered/by/any/tests.js'; + fakeDepsFromPath[path] = []; + + // Mock out existsSync to return true, since our test path isn't real + fs.existsSync = function() { return true; }; + + return runner.findTestsRelatedTo([path]).then(function(relatedTests) { + expect(relatedTests).toEqual([]); + }); + }); + + pit('finds tests that depend directly on the path', function() { + var path = '/path/to/module/covered/by/one/test.js'; + var dependentTestPath = '/path/to/test/__tests__/asdf-test.js'; + fakeDepsFromPath[path] = [dependentTestPath]; + + // Mock out existsSync to return true, since our test path isn't real + fs.existsSync = function() { return true; }; + + return runner.findTestsRelatedTo([path]).then(function(relatedTests) { + expect(relatedTests).toEqual([dependentTestPath]); + }); + }); + + pit('finds tests that depend indirectly on the path', function() { + var path = '/path/to/module/covered/by/module/covered/by/test.js'; + var dependentModulePath = '/path/to/dependent/module.js'; + var dependentTestPath = '/path/to/test/__tests__/asdf-test.js'; + fakeDepsFromPath[path] = [dependentModulePath]; + fakeDepsFromPath[dependentModulePath] = [dependentTestPath]; + + // Mock out existsSync to return true, since our test path isn't real + fs.existsSync = function() { return true; }; + + return runner.findTestsRelatedTo([path]).then(function(relatedTests) { + expect(relatedTests).toEqual([dependentTestPath]); + }); + }); + + pit('finds multiple tests that depend indirectly on the path', function() { + var path = '/path/to/module/covered/by/modules/covered/by/test.js'; + var dependentModulePath1 = '/path/to/dependent/module1.js'; + var dependentModulePath2 = '/path/to/dependent/module2.js'; + var dependentTestPath1 = '/path/to/test1/__tests__/asdf1-test.js'; + var dependentTestPath2 = '/path/to/test2/__tests__/asdf2-test.js'; + fakeDepsFromPath[path] = [dependentModulePath1, dependentModulePath2]; + fakeDepsFromPath[dependentModulePath1] = [dependentTestPath1]; + fakeDepsFromPath[dependentModulePath2] = [dependentTestPath2]; + + // Mock out existsSync to return true, since our test path isn't real + fs.existsSync = function() { return true; }; + + return runner.findTestsRelatedTo([path]).then(function(relatedTests) { + expect(relatedTests).toEqual([dependentTestPath1, dependentTestPath2]); + }); + }); + + pit('flattens circular dependencies', function() { + var path = '/path/to/module/covered/by/modules/covered/by/test.js'; + var directDependentModulePath = '/path/to/direct/dependent/module.js'; + var indirectDependentModulePath = '/path/to/indirect/dependent/module.js'; + var dependentTestPath = '/path/to/test/__tests__/asdf-test.js'; + fakeDepsFromPath[path] = [directDependentModulePath]; + fakeDepsFromPath[directDependentModulePath] = + [indirectDependentModulePath]; + fakeDepsFromPath[indirectDependentModulePath] = [ + directDependentModulePath, + dependentTestPath + ]; + + // Mock out existsSync to return true, since our test path isn't real + fs.existsSync = function() { return true; }; + + return runner.findTestsRelatedTo([path]).then(function(relatedTests) { + expect(relatedTests).toEqual([dependentTestPath]); + }); + }); + + pit('filters test paths that don\'t exist on the filesystem', function() { + var path = '/path/to/module/covered/by/one/test.js'; + var existingTestPath = '/path/to/test/__tests__/exists-test.js'; + var nonExistantTestPath = '/path/to/test/__tests__/doesnt-exist-test.js'; + fakeDepsFromPath[path] = [existingTestPath, nonExistantTestPath]; + + // Mock out existsSync to return true, since our test path isn't real + fs.existsSync = function(path) { + return path !== nonExistantTestPath; + }; + + return runner.findTestsRelatedTo([path]).then(function(relatedTests) { + expect(relatedTests).toEqual([existingTestPath]); + }); + }); + }); +}); diff --git a/src/coverageTemplate.js b/src/coverageTemplate.js new file mode 100644 index 000000000000..e35fddc42df1 --- /dev/null +++ b/src/coverageTemplate.js @@ -0,0 +1,28 @@ + +// Instrumentation Header +{ + var <%= instrumented.names.statement %>; + var <%= instrumented.names.expression %>; + var <%= instrumented.names.block %>; + var nodes = <%= coverageStorageVar %>.nodes = {}; + var blocks = <%= coverageStorageVar %>.blocks = {}; + + <%= instrumented.names.statement %> = function(i) { + var node = nodes[i] = (nodes[i] || {index: i, count:0}) + node.count++; + }; + + <%= instrumented.names.expression %> = function(i) { + var node = nodes[i] = (nodes[i] || {index: i, count:0}) + node.count++; + }; + + <%= instrumented.names.block %> = function(i) { + var block = blocks[i] = (blocks[i] || {index: i, count:0}) + block.count++; + }; +}; +//////////////////////// + +// Instrumented Code +<%= source %> diff --git a/src/defaultTestResultHandler.js b/src/defaultTestResultHandler.js new file mode 100644 index 000000000000..700219f2c148 --- /dev/null +++ b/src/defaultTestResultHandler.js @@ -0,0 +1,149 @@ +/** + * Copyright (c) 2014, Facebook, Inc. All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +'use strict'; + +var colors = require('./lib/colors'); +var path = require('path'); +var utils = require('./lib/utils'); + +var FAIL_COLOR = colors.RED_BG + colors.BOLD; +var PASS_COLOR = colors.GREEN_BG + colors.BOLD; +var TEST_NAME_COLOR = colors.BOLD; + +// A RegExp that matches paths that should not be included in error stack traces +// (mostly because these paths represent noisy/unhelpful libs) +var STACK_TRACE_LINE_IGNORE_RE = new RegExp('^(?:' + [ + path.resolve(__dirname, '..', 'node_modules', 'q'), + path.resolve(__dirname, '..', 'vendor', 'jasmine') +].join('|') + ')'); + +function _printConsoleMessage(msg) { + switch (msg.type) { + case 'error': + // TODO: jstest doesn't print console.error messages. + // This is a big WAT, and we should come back to this -- but + // right now the goal is jest/jstest feature parity, not test + // cleanup. + break; + + /* + console.error.apply(console, msg.args.map(function(arg) { + arg = utils.stringifySerializedConsoleArgValue(arg); + return colors.colorize(arg, colors.RED); + })); + break; + */ + case 'log': + console.log.apply(console, msg.args.map(function(arg) { + return utils.stringifySerializedConsoleArgValue(arg); + })); + break; + case 'warn': + // TODO: jstest doesn't print console.warn messages. + // Turning this on gets pretty noisy...but we should probably + // clean this up as warns are likely a sign of clownitude + break; + + /* + console.warn.apply(console, msg.args.map(function(arg) { + arg = utils.stringifySerializedConsoleArgValue(arg); + return colors.colorize(arg, colors.RED); + })); + break; + */ + default: + throw new Error('Unknown console message type!: ' + JSON.stringify(msg)); + } +} + +function _getResultHeader(passed, testName, columns) { + var passFailTag = passed + ? colors.colorize(' PASS ', PASS_COLOR) + : colors.colorize(' FAIL ', FAIL_COLOR); + + return [ + passFailTag, + colors.colorize(testName, TEST_NAME_COLOR) + ].concat(columns || []).join(' '); +} + +function defaultTestResultHandler(config, testResult) { + var pathStr = + config.rootDir + ? path.relative(config.rootDir, testResult.testFilePath) + : testResult.testFilePath; + + if (testResult.testExecError) { + console.log(_getResultHeader(false, pathStr)); + console.log(testResult.testExecError); + return false; + } + + var allTestsPassed = testResult.numFailingTests === 0; + + var testRunTime = + testResult.perfStats + ? (testResult.perfStats.end - testResult.perfStats.start) / 1000 + : null; + + var testRunTimeString = '(' + testRunTime + 's)'; + if (testRunTime > 2.5) { + testRunTimeString = colors.colorize(testRunTimeString, FAIL_COLOR); + } + + /* + if (config.collectCoverage) { + // TODO: Find a nice pretty way to print this out + } + */ + + console.log(_getResultHeader(allTestsPassed, pathStr, [ + testRunTimeString + ])); + + testResult.logMessages.forEach(_printConsoleMessage); + + if (!allTestsPassed) { + var ancestrySeparator = ' \u203A '; + var descBullet = colors.colorize('\u25cf ', colors.BOLD); + var msgBullet = ' - '; + var msgIndent = msgBullet.replace(/./g, ' '); + + testResult.testResults.forEach(function(result) { + if (result.failureMessages.length === 0) { + return; + } + + var testTitleAncestry = + result.ancestorTitles.map(function(title) { + return colors.colorize(title, colors.BOLD); + }).join(ancestrySeparator) + ancestrySeparator; + + console.log(descBullet + testTitleAncestry + result.title); + + result.failureMessages.forEach(function(errorMsg) { + // Filter out q and jasmine entries from the stack trace. + // They're super noisy and unhelpful + errorMsg = errorMsg.split('\n').filter(function(line) { + if (/^\s+at .*?/.test(line)) { + // Extract the file path from the trace line + var filePath = line.match(/(?:\(|at (?=\/))(.*):[0-9]+:[0-9]+\)?$/); + if (filePath + && STACK_TRACE_LINE_IGNORE_RE.test(filePath[1])) { + return false; + } + } + return true; + }).join('\n'); + console.log(msgBullet + errorMsg.replace(/\n/g, '\n' + msgIndent)); + }); + }); + } +} + +module.exports = defaultTestResultHandler; diff --git a/src/jasmineTestRunner/JasmineReporter.js b/src/jasmineTestRunner/JasmineReporter.js new file mode 100644 index 000000000000..59b8336f5e85 --- /dev/null +++ b/src/jasmineTestRunner/JasmineReporter.js @@ -0,0 +1,221 @@ +/** + * Copyright (c) 2014, Facebook, Inc. All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +'use strict'; + +var colors = require('../lib/colors'); +var diff = require('diff'); +var jasmine = require('../../vendor/jasmine/jasmine-1.3.0').jasmine; +var Q = require('q'); + +var colorize = colors.colorize; + +var ERROR_TITLE_COLOR = colors.RED + colors.BOLD + colors.UNDERLINE; +var DIFFABLE_MATCHERS = { + toBe: true, + toNotBe: true, + toEqual: true, + toNotEqual: true +}; + +function _highlightDifferences(a, b) { + var changes = diff.diffChars(a, b); + + var ret = {a: '', b: ''}; + var change; + for (var i = 0, il = changes.length; i < il; i++) { + change = changes[i]; + if (change.added) { + ret.b += colorize(change.value, colors.RED_BG); + } else if (change.removed) { + ret.a += colorize(change.value, colors.RED_BG); + } else { + ret.a += change.value; + ret.b += change.value; + } + } + return ret; +} + +function _prettyPrint(obj, indent) { + if (!indent) { + indent = ''; + } + + if (typeof obj === 'object' && obj !== null) { + if (jasmine.isDomNode(obj)) { + var attrStr = ''; + Array.prototype.forEach.call(obj.attributes, function(attr) { + var attrName = attr.nodeName.trim(); + var attrValue = attr.nodeValue.trim(); + attrStr += ' ' + attrName + '="' + attrValue + '"'; + }); + return 'HTMLNode(' + + '<' + obj.tagName + attrStr + '>[...]' + + ')'; + } + + /* jshint camelcase:false */ + if (obj.__jstest_pp_cycle__) { + return ''; + } + obj.__jstest_pp_cycle__ = true; + + var orderedKeys = Object.keys(obj).sort(); + var value; + var keysOutput = []; + var keyIndent = colorize('|', colors.GRAY) + ' '; + for (var i = 0; i < orderedKeys.length; i++) { + if (orderedKeys[i] === '__jstest_pp_cycle__') { + continue; + } + value = obj[orderedKeys[i]]; + keysOutput.push( + indent + keyIndent + orderedKeys[i] + ': ' + + _prettyPrint(value, indent + keyIndent) + ); + } + delete obj.__jstest_pp_cycle__; + return '{\n' + keysOutput.join(',\n') + '\n' + indent + '}'; + } else { + return jasmine.pp(obj); + } +} + +function _extractSuiteResults(container, ancestorTitles, suite) { + ancestorTitles = ancestorTitles.concat([suite.description]); + + suite.specs().forEach( + _extractSpecResults.bind(null, container, ancestorTitles) + ); + suite.suites().forEach( + _extractSuiteResults.bind(null, container, ancestorTitles) + ); +} + +function _extractSpecResults(container, ancestorTitles, spec) { + var results = { + title: 'it ' + spec.description, + ancestorTitles: ancestorTitles, + failureMessages: [], + logMessages: [], + numPassingAsserts: 0 + }; + + spec.results().getItems().forEach(function(result) { + switch (result.type) { + case 'log': + results.logMessages.push(result.toString()); + break; + case 'expect': + if (result.passed()) { + results.numPassingAsserts++; + + // Exception thrown + } else if (!result.matcherName && result.trace.stack) { + // jasmine doesn't give us access to the actual Error object, so we + // have to regexp out the message from the stack string in order to + // colorize the `message` value + result.trace.stack = result.trace.stack.replace( + /(^.*$(?=\n\s*at))/m, + colorize('$1', ERROR_TITLE_COLOR) + ); + + results.failureMessages.push(result.trace.stack); + } else { + var message; + if (DIFFABLE_MATCHERS[result.matcherName]) { + var ppActual = _prettyPrint(result.actual); + var ppExpected = _prettyPrint(result.expected); + var colorDiff = _highlightDifferences(ppActual, ppExpected); + + var matcherName = (result.isNot ? 'NOT ' : '') + result.matcherName; + + message = + colorize('Expected:', ERROR_TITLE_COLOR) + + ' ' + colorDiff.a + + ' ' + colorize(matcherName + ':', ERROR_TITLE_COLOR) + + ' ' + colorDiff.b; + } else { + message = colorize(result.message, ERROR_TITLE_COLOR); + } + + if (result.trace.stack) { + // Replace the error message with a colorized version of the error + message = result.trace.stack.replace(result.trace.message, message); + + // Remove the 'Error: ' prefix from the stack trace + message = message.replace(/^.*Error:\s*/, ''); + + // Remove jasmine jonx from the stack trace + message = message.split('\n').filter(function(line) { + return !/vendor\/jasmine\//.test(line); + }).join('\n'); + } + + results.failureMessages.push(message); + } + break; + default: + throw new Error( + 'Unexpected jasmine spec result type: ', result.type + ); + } + }); + + container.push(results); +} + +function JasmineReporter() { + jasmine.Reporter.call(this); + this._logs = []; + this._resultsDeferred = Q.defer(); +} +JasmineReporter.prototype = Object.create(jasmine.Reporter.prototype); + +// All describe() suites have finished +JasmineReporter.prototype.reportRunnerResults = function(runner) { + var testResults = []; + + // Find the top-level suite in order to flatten test results from there + if (runner.suites().length) { + var topLevelSuite; + runner.suites().forEach(function(suite) { + if (suite.parentSuite === null) { + topLevelSuite = suite; + } + }); + + _extractSuiteResults(testResults, [], topLevelSuite); + } + + var numFailingTests = 0; + var numPassingTests = 0; + testResults.forEach(function(testResult) { + if (testResult.failureMessages.length > 0) { + numFailingTests++; + } else { + numPassingTests++; + } + }); + + this._resultsDeferred.resolve({ + numFailingTests: numFailingTests, + numPassingTests: numPassingTests, + testResults: testResults + }); +}; + +JasmineReporter.prototype.getResults = function() { + return this._resultsDeferred.promise; +}; + +JasmineReporter.prototype.log = function(str) { + console.log('logging: ', str); +}; + +module.exports = JasmineReporter; diff --git a/src/jasmineTestRunner/jasmineTestRunner.js b/src/jasmineTestRunner/jasmineTestRunner.js new file mode 100644 index 000000000000..aaf8f2dad610 --- /dev/null +++ b/src/jasmineTestRunner/jasmineTestRunner.js @@ -0,0 +1,231 @@ +/** + * Copyright (c) 2014, Facebook, Inc. All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +'use strict'; + +var fs = require('fs'); +var jasminePit = require('jasmine-pit'); +var JasmineReporter = require('./JasmineReporter'); +var path = require('path'); +var utils = require('../lib/utils'); + +var JASMINE_PATH = require.resolve('../../vendor/jasmine/jasmine-1.3.0'); +var jasmineFileContent = + fs.readFileSync(require.resolve(JASMINE_PATH), 'utf8'); + +var JASMINE_ONLY_ROOT = path.dirname(require.resolve('jasmine-only')); +var POTENTIALLY_PRECOMPILED_FILE = path.join( + JASMINE_ONLY_ROOT, + 'app', + 'js', + 'jasmine_only.js' +); +var COFFEE_SCRIPT_FILE = path.join( + JASMINE_ONLY_ROOT, + 'app', + 'js', + 'jasmine_only.coffee' +); + +var jasmineOnlyContent = + fs.existsSync(POTENTIALLY_PRECOMPILED_FILE) + ? fs.readFileSync(POTENTIALLY_PRECOMPILED_FILE, 'utf8') + : require('coffee-script').compile( + fs.readFileSync(COFFEE_SCRIPT_FILE, 'utf8') + ); + +function jasmineTestRunner(config, environment, moduleLoader, testPath) { + // Jasmine does stuff with timers that affect running the tests. However, we + // also mock out all the timer APIs (to make them test-controllable). + // + // To account for this conflict, we set up jasmine in an environment with real + // timers (instead of mock timers). + environment.fakeTimers.runWithRealTimers(function() { + // Execute jasmine's main code + environment.runSourceText(jasmineFileContent, JASMINE_PATH); + + // Install jasmine-pit -- because it's amazing + jasminePit.install(environment.global); + + // Install jasmine-only + environment.runSourceText(jasmineOnlyContent); + + // Node must have been run with --harmony in order for WeakMap to be + // available + if (!process.execArgv.some(function(arg) { return arg === '--harmony'; })) { + throw new Error('Please run node with the --harmony flag!'); + } + + // Mainline Jasmine sets __Jasmine_been_here_before__ on each object to + // detect cycles, but that doesn't work on frozen objects so we use a + // WeakMap instead. + var _comparedObjects = new WeakMap(); + environment.global.jasmine.Env.prototype.compareObjects_ = + function(a, b, mismatchKeys, mismatchValues) { + if (_comparedObjects.get(a) === b && _comparedObjects.get(b) === a) { + return true; + } + var areArrays = + environment.global.jasmine.isArray_(a) + && environment.global.jasmine.isArray_(b); + + _comparedObjects.set(a, b); + _comparedObjects.set(b, a); + + var hasKey = function(obj, keyName) { + return ( + obj !== null + && obj !== undefined + && obj[keyName] !== environment.global.jasmine.undefined + ); + }; + + for (var property in b) { + if (areArrays && typeof b[property] === 'function') { + continue; + } + if (!hasKey(a, property) && hasKey(b, property)) { + mismatchKeys.push( + 'expected has key \'' + property + '\', but missing from actual.' + ); + } + } + for (property in a) { + if (areArrays && typeof a[property] === 'function') { + continue; + } + if (!hasKey(b, property) && hasKey(a, property)) { + mismatchKeys.push( + 'expected missing key \'' + property + '\', but present in ' + + 'actual.' + ); + } + } + for (property in b) { + // The only different implementation from the original jasmine + if (areArrays && + (typeof a[property] === 'function' || + typeof b[property] === 'function')) { + continue; + } + var areEqual = this.equals_( + a[property], + b[property], + mismatchKeys, + mismatchValues + ); + if (!areEqual) { + var aprop; + var bprop; + if (!a[property]) { + aprop = a[property]; + } else if (a[property].toString) { + aprop = environment.global.jasmine.util.htmlEscape( + a[property].toString() + ); + } else { + aprop = Object.prototype.toString.call(a[property]); + } + + if (!b[property]) { + bprop = b[property]; + } else if (b[property].toString) { + bprop = environment.global.jasmine.util.htmlEscape( + b[property].toString() + ); + } else { + bprop = Object.prototype.toString.call(b[property]); + } + + mismatchValues.push( + '\'' + property + '\' was \'' + bprop + + '\' in expected, but was \'' + aprop + + '\' in actual.' + ); + } + } + + if (areArrays && a.length !== b.length) { + mismatchValues.push('arrays were not the same length'); + } + + _comparedObjects.delete(a); + _comparedObjects.delete(b); + return (mismatchKeys.length === 0 && mismatchValues.length === 0); + }; + + if (config.setupTestFrameworkScriptFile) { + var setupScriptContent = utils.readAndPreprocessFileContent( + config.setupTestFrameworkScriptFile, + config + ); + + utils.runContentWithLocalBindings( + environment.runSourceText.bind(environment), + setupScriptContent, + config.setupTestFrameworkScriptFile, + { + __dirname: path.dirname(config.setupTestFrameworkScriptFile), + __filename: config.setupTestFrameworkScriptFile, + require: moduleLoader.constructBoundRequire( + config.setupTestFrameworkScriptFile + ) + } + ); + } + }); + + var jasmine = environment.global.jasmine; + + jasmine.getEnv().beforeEach(function() { + this.addMatchers({ + toBeCalled: function() { + if (this.actual.mock === undefined) { + throw Error('toBeCalled() should be used on a mock function'); + } + return this.actual.mock.calls.length !== 0; + }, + + lastCalledWith: function() { + if (this.actual.mock === undefined) { + throw Error('lastCalledWith() should be used on a mock function'); + } + var calls = this.actual.mock.calls; + var args = Array.prototype.slice.call(arguments); + return this.env.equals_(calls[calls.length - 1], args); + }, + + toBeCalledWith: function() { + if (this.actual.mock === undefined) { + throw Error('toBeCalledWith() should be used on a mock function'); + } + var args = Array.prototype.slice.call(arguments); + return this.actual.mock.calls.some(function(call) { + return this.env.equals_(call, args); + }.bind(this)); + } + }); + + if (!config.persistModuleRegistryBetweenSpecs) { + moduleLoader.requireModule( + __filename, + 'jest-runtime' + ).resetModuleRegistry(); + } + }); + + var jasmineReporter = new JasmineReporter(); + jasmine.getEnv().addReporter(jasmineReporter); + + // Run the test by require()ing it + moduleLoader.requireModule(testPath, './' + path.basename(testPath)); + + jasmine.getEnv().execute(); + return jasmineReporter.getResults(); +} + +module.exports = jasmineTestRunner; diff --git a/src/lib/FakeTimers.js b/src/lib/FakeTimers.js new file mode 100644 index 000000000000..73011cf60964 --- /dev/null +++ b/src/lib/FakeTimers.js @@ -0,0 +1,313 @@ +/** + * Copyright (c) 2014, Facebook, Inc. All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +'use strict'; + +var mocks = require('./moduleMocker'); + +var MS_IN_A_YEAR = 31536000000; + +function FakeTimers(global) { + this._global = global; + this._uuidCounter = 0; + + this.reset(); + + // Store original timer APIs for future reference + this._originalTimerAPIs = { + setTimeout: global.setTimeout, + clearTimeout: global.clearTimeout, + setInterval: global.setInterval, + clearInterval: global.clearInterval + }; + + // Install mocked versions of the timer APIs + global.setTimeout = mocks.getMockFn().mockImpl( + this._fakeSetTimeout.bind(this) + ); + global.clearTimeout = mocks.getMockFn().mockImpl( + this._fakeClearTimer.bind(this) + ); + global.setInterval = mocks.getMockFn().mockImpl( + this._fakeSetInterval.bind(this) + ); + global.clearInterval = mocks.getMockFn().mockImpl( + this._fakeClearTimer.bind(this) + ); + + // If there's a process.nextTick on the global, mock it out + // (only applicable to node/node-emulating environments) + if (typeof global.process === 'object' + && typeof global.process.nextTick === 'function') { + this._originalTimerAPIs.nextTick = global.process.nextTick; + global.process.nextTick = mocks.getMockFn().mockImpl( + this._fakeNextTick.bind(this) + ); + } + + // TODO: These globally-accessible function are now deprecated! + // They will go away very soon, so do not use them! + // Instead, use the versions available on the `jest` object + global.mockRunTicksRepeatedly = this.runAllTicks.bind(this); + global.mockRunTimersOnce = this.runOnlyPendingTimers.bind(this); + global.mockRunTimersToTime = this.runTimersToTime.bind(this); + global.mockRunTimersRepeatedly = this.runAllTimers.bind(this); + global.mockClearTimers = this.clearAllTimers.bind(this); + global.mockGetTimersCount = function() { + return Object.keys(this._timers).length; + }.bind(this); +} + +FakeTimers.prototype.clearAllTimers = function() { + for (var uuid in this._timers) { + delete this._timers[uuid]; + } +}; + +FakeTimers.prototype.reset = function() { + this._cancelledTicks = {}; + this._now = 0; + this._ticks = []; + this._timers = {}; +}; + +// Used to be called runTicksRepeatedly +FakeTimers.prototype.runAllTicks = function() { + // Only run a generous number of ticks and then bail. + // This is just to help avoid recursive loops + var maxTicksToRun = 100000; + + for (var i = 0; i < maxTicksToRun; i++) { + var tick = this._ticks.shift(); + + if (tick === undefined) { + break; + } + + if (!this._cancelledTicks.hasOwnProperty(tick.uuid)) { + tick.callback(); + this._cancelledTicks[tick.uuid] = true; + } + } + + if (i === maxTicksToRun) { + throw new Error( + 'Ran ' + maxTicksToRun + ' ticks, and there are still more! Assuming ' + + 'we\'ve hit an infinite recursion and bailing out...' + ); + } +}; + +// Used to be called runTimersRepeatedly +FakeTimers.prototype.runAllTimers = function() { + this.runAllTicks(); + + // Only run a generous number of timers and then bail. + // This is just to help avoid recursive loops + var maxTimersToRun = 100000; + + for (var i = 0; i < maxTimersToRun; i++) { + var nextTimerHandle = this._getNextTimerHandle(); + + // If there are no more timer handles, stop! + if (nextTimerHandle === null) { + break; + } + + this._runTimerHandle(nextTimerHandle); + } + + if (i === maxTimersToRun) { + throw new Error( + 'Ran ' + maxTimersToRun + ' timers, and there are still more! Assuming ' + + 'we\'ve hit an infinite recursion and bailing out...' + ); + } +}; + +// Used to be called runTimersOnce +FakeTimers.prototype.runOnlyPendingTimers = function() { + var timers = this._timers; + Object.keys(timers) + .sort(function(left, right) { + return timers[left].expiry - timers[right].expiry; + }) + .forEach(this._runTimerHandle, this); +}; + +// Use to be runTimersToTime +FakeTimers.prototype.runTimersToTime = function(msToRun) { + // Only run a generous number of timers and then bail. + // This is jsut to help avoid recursive loops + var maxTimersToRun = 100000; + + for (var i = 0; i < maxTimersToRun; i++) { + var timerHandle = this._getNextTimerHandle(); + + // If there are no more timer handles, stop! + if (timerHandle === null) { + break; + } + + var nextTimerExpiry = this._timers[timerHandle].expiry; + if (this._now + msToRun < nextTimerExpiry) { + // There are no timers between now and the target we're running to, so + // adjust our time cursor and quit + this._now += msToRun; + break; + } else { + msToRun -= (nextTimerExpiry - this._now); + this._now = nextTimerExpiry; + this._runTimerHandle(timerHandle); + } + } + + if (i === maxTimersToRun) { + throw new Error( + 'Ran ' + maxTimersToRun + ' timers, and there are still more! Assuming ' + + 'we\'ve hit an infinite recursion and bailing out...' + ); + } +}; + +FakeTimers.prototype.runWithRealTimers = function(cb) { + var hasNextTick = + typeof this._global.process === 'object' + && typeof this._global.process.nextTick === 'function'; + + var fakeSetTimeout = this._global.setTimeout; + var fakeSetInterval = this._global.setInterval; + var fakeClearTimeout = this._global.clearTimeout; + var fakeClearInterval = this._global.clearInterval; + if (hasNextTick) { + var fakeNextTick = this._global.process.nextTick; + } + + this._global.setTimeout = this._originalTimerAPIs.setTimeout; + this._global.setInterval = this._originalTimerAPIs.setInterval; + this._global.clearTimeout = this._originalTimerAPIs.clearTimeout; + this._global.clearInterval = this._originalTimerAPIs.clearInterval; + if (hasNextTick) { + this._global.process.nextTick = this._originalTimerAPIs.nextTick; + } + + var cbErr = null; + var errThrown = false; + try { + cb(); + } catch (e) { + errThrown = true; + cbErr = e; + } + + this._global.setTimeout = fakeSetTimeout; + this._global.setInterval = fakeSetInterval; + this._global.clearTimeout = fakeClearTimeout; + this._global.clearInterval = fakeClearInterval; + if (hasNextTick) { + this._global.process.nextTick = fakeNextTick; + } + + if (errThrown) { + throw cbErr; + } +}; + +FakeTimers.prototype._fakeClearTimer = function(uuid) { + if (this._timers.hasOwnProperty(uuid)) { + delete this._timers[uuid]; + } +}; + +FakeTimers.prototype._fakeNextTick = function(callback) { + var uuid = this._uuidCounter++; + this._ticks.push({ + uuid: uuid, + callback: callback + }); + + var cancelledTicks = this._cancelledTicks; + this._originalTimerAPIs.nextTick(function() { + if (!cancelledTicks.hasOwnProperty(uuid)) { + callback(); + cancelledTicks[uuid] = true; + } + }); +}; + +FakeTimers.prototype._fakeSetInterval = function(callback, intervalDelay) { + if (intervalDelay === undefined || intervalDelay === null) { + intervalDelay = 0; + } + + var uuid = this._uuidCounter++; + + this._timers[uuid] = { + type: 'interval', + callback: callback, + expiry: this._now + intervalDelay, + interval: intervalDelay + }; + + return uuid; +}; + +FakeTimers.prototype._fakeSetTimeout = function(callback, delay) { + if (delay === undefined || delay === null) { + delay = 0; + } + + var uuid = this._uuidCounter++; + + this._timers[uuid] = { + type: 'timeout', + callback: callback, + expiry: this._now + delay, + interval: null + }; + + return uuid; +}; + +FakeTimers.prototype._getNextTimerHandle = function() { + var nextTimerHandle = null; + var uuid; + var soonestTime = MS_IN_A_YEAR; + + var timer; + for (uuid in this._timers) { + timer = this._timers[uuid]; + if (timer.expiry < soonestTime) { + soonestTime = timer.expiry; + nextTimerHandle = uuid; + } + } + + return nextTimerHandle; +}; + +FakeTimers.prototype._runTimerHandle = function(timerHandle) { + var timer = this._timers[timerHandle]; + + switch (timer.type) { + case 'timeout': + var callback = timer.callback; + delete this._timers[timerHandle]; + callback(); + break; + + case 'interval': + timer.expiry = this._now + timer.interval; + timer.callback(); + break; + + default: + throw new Error('Unexepcted timer type: ' + timer.type); + } +}; + +module.exports = FakeTimers; diff --git a/src/lib/__tests__/FakeTimers-test.js b/src/lib/__tests__/FakeTimers-test.js new file mode 100644 index 000000000000..13ad678282b4 --- /dev/null +++ b/src/lib/__tests__/FakeTimers-test.js @@ -0,0 +1,587 @@ +/** + * Copyright (c) 2014, Facebook, Inc. All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +'use strict'; + +require('mock-modules').autoMockOff(); + +describe('FakeTimers', function() { + var FakeTimers; + + beforeEach(function() { + FakeTimers = require('../FakeTimers'); + }); + + describe('construction', function() { + /* jshint nonew:false */ + it('installs setTimeout mock', function() { + var global = {}; + new FakeTimers(global); + expect(global.setTimeout).not.toBe(undefined); + }); + + it('installs clearTimeout mock', function() { + var global = {}; + new FakeTimers(global); + expect(global.clearTimeout).not.toBe(undefined); + }); + + it('installs setInterval mock', function() { + var global = {}; + new FakeTimers(global); + expect(global.setInterval).not.toBe(undefined); + }); + + it('installs clearInterval mock', function() { + var global = {}; + new FakeTimers(global); + expect(global.clearInterval).not.toBe(undefined); + }); + + it('mocks process.nextTick if on exists on global', function() { + var origNextTick = function() {}; + var global = { + process: { + nextTick: origNextTick + } + }; + new FakeTimers(global); + expect(global.process.nextTick).not.toBe(origNextTick); + }); + + it('doesn\'t mock process.nextTick if real impl isnt present', function() { + var global = {}; + new FakeTimers(global); + expect(global.process).toBe(undefined); + }); + }); + + describe('runAllTicks', function() { + it('runs all ticks, in order', function() { + var global = { + process: { + nextTick: function() {} + } + }; + + var fakeTimers = new FakeTimers(global); + + var runOrder = []; + var mock1 = jest.genMockFn().mockImpl(function() { + runOrder.push('mock1'); + }); + var mock2 = jest.genMockFn().mockImpl(function() { + runOrder.push('mock2'); + }); + + global.process.nextTick(mock1); + global.process.nextTick(mock2); + + expect(mock1.mock.calls.length).toBe(0); + expect(mock2.mock.calls.length).toBe(0); + + fakeTimers.runAllTicks(); + + expect(mock1.mock.calls.length).toBe(1); + expect(mock2.mock.calls.length).toBe(1); + expect(runOrder).toEqual(['mock1', 'mock2']); + }); + + it('does nothing when no ticks have been scheduled', function() { + var nextTick = jest.genMockFn(); + var global = { + process: { + nextTick: nextTick + } + }; + + var fakeTimers = new FakeTimers(global); + fakeTimers.runAllTicks(); + + expect(nextTick.mock.calls.length).toBe(0); + }); + + it('only runs a scheduled callback once', function() { + var global = { + process: { + nextTick: function() {} + } + }; + + var fakeTimers = new FakeTimers(global); + + var mock1 = jest.genMockFn(); + global.process.nextTick(mock1); + expect(mock1.mock.calls.length).toBe(0); + + fakeTimers.runAllTicks(); + expect(mock1.mock.calls.length).toBe(1); + + fakeTimers.runAllTicks(); + expect(mock1.mock.calls.length).toBe(1); + }); + + it('cancels a callback even from native nextTick', function() { + var nativeNextTick = jest.genMockFn(); + + var global = { + process: { + nextTick: nativeNextTick + } + }; + + var fakeTimers = new FakeTimers(global); + + var mock1 = jest.genMockFn(); + global.process.nextTick(mock1); + fakeTimers.runAllTicks(); + expect(mock1.mock.calls.length).toBe(1); + expect(nativeNextTick.mock.calls.length).toBe(1); + + // Now imagine we fast forward to the next real tick. We need to be sure + // that native nextTick doesn't try to run the callback again + nativeNextTick.mock.calls[0][0](); + expect(mock1.mock.calls.length).toBe(1); + }); + + it('doesnt run a tick callback if native nextTick already did', function() { + var nativeNextTick = jest.genMockFn(); + + var global = { + process: { + nextTick: nativeNextTick + } + }; + + var fakeTimers = new FakeTimers(global); + + var mock1 = jest.genMockFn(); + global.process.nextTick(mock1); + + // Emulate native nextTick running... + nativeNextTick.mock.calls[0][0](); + expect(mock1.mock.calls.length).toBe(1); + + // Ensure runAllTicks() doesn't run the callback again + fakeTimers.runAllTicks(); + expect(mock1.mock.calls.length).toBe(1); + }); + + it('throws before allowing infinite recursion', function() { + var global = { + process: { + nextTick: function() {} + } + }; + + var fakeTimers = new FakeTimers(global); + + global.process.nextTick(function infinitelyRecursingCallback() { + global.process.nextTick(infinitelyRecursingCallback); + }); + + expect(function() { + fakeTimers.runAllTicks(); + }).toThrow( + 'Ran 100000 ticks, and there are still more! Assuming we\'ve hit an ' + + 'infinite recursion and bailing out...' + ); + }); + }); + + describe('runAllTimers', function() { + it('runs all timers in order', function() { + var global = {}; + var fakeTimers = new FakeTimers(global); + + var runOrder = []; + var mock1 = jest.genMockFn().mockImpl(function() { + runOrder.push('mock1'); + }); + var mock2 = jest.genMockFn().mockImpl(function() { + runOrder.push('mock2'); + }); + var mock3 = jest.genMockFn().mockImpl(function() { + runOrder.push('mock3'); + }); + var mock4 = jest.genMockFn().mockImpl(function() { + runOrder.push('mock4'); + }); + + global.setTimeout(mock1, 100); + global.setTimeout(mock2, 0); + global.setTimeout(mock3, 0); + var intervalHandler = global.setInterval(function() { + mock4(); + global.clearInterval(intervalHandler); + }, 200); + + fakeTimers.runAllTimers(); + expect(runOrder).toEqual(['mock2', 'mock3', 'mock1', 'mock4']); + }); + + it('does nothing when no timers have been scheduled', function() { + var nativeSetTimeout = jest.genMockFn(); + var global = { + setTimeout: nativeSetTimeout + }; + + var fakeTimers = new FakeTimers(global); + fakeTimers.runAllTimers(); + }); + + it('only runs a setTimeout callback once (ever)', function() { + var global = {}; + var fakeTimers = new FakeTimers(global); + + var fn = jest.genMockFn(); + global.setTimeout(fn, 0); + expect(fn.mock.calls.length).toBe(0); + + fakeTimers.runAllTimers(); + expect(fn.mock.calls.length).toBe(1); + + fakeTimers.runAllTimers(); + expect(fn.mock.calls.length).toBe(1); + }); + + it('doesnt pass the callback to native setTimeout', function() { + var nativeSetTimeout = jest.genMockFn(); + + var global = { + setTimeout: nativeSetTimeout + }; + + var fakeTimers = new FakeTimers(global); + + var mock1 = jest.genMockFn(); + global.setTimeout(mock1, 0); + + fakeTimers.runAllTimers(); + expect(mock1.mock.calls.length).toBe(1); + expect(nativeSetTimeout.mock.calls.length).toBe(0); + }); + + it('throws before allowing infinite recursion', function() { + var global = {}; + var fakeTimers = new FakeTimers(global); + + global.setTimeout(function infinitelyRecursingCallback() { + global.setTimeout(infinitelyRecursingCallback, 0); + }, 0); + + expect(function() { + fakeTimers.runAllTimers(); + }).toThrow( + 'Ran 100000 timers, and there are still more! Assuming we\'ve hit an ' + + 'infinite recursion and bailing out...' + ); + }); + }); + + describe('runTimersToTime', function() { + it('runs timers in order', function() { + var global = {}; + var fakeTimers = new FakeTimers(global); + + var runOrder = []; + var mock1 = jest.genMockFn().mockImpl(function() { + runOrder.push('mock1'); + }); + var mock2 = jest.genMockFn().mockImpl(function() { + runOrder.push('mock2'); + }); + var mock3 = jest.genMockFn().mockImpl(function() { + runOrder.push('mock3'); + }); + var mock4 = jest.genMockFn().mockImpl(function() { + runOrder.push('mock4'); + }); + + global.setTimeout(mock1, 100); + global.setTimeout(mock2, 0); + global.setTimeout(mock3, 0); + global.setInterval(function() { + mock4(); + }, 200); + + // Move forward to t=50 + fakeTimers.runTimersToTime(50); + expect(runOrder).toEqual(['mock2', 'mock3']); + + // Move forward to t=60 + fakeTimers.runTimersToTime(10); + expect(runOrder).toEqual(['mock2', 'mock3']); + + // Move forward to t=100 + fakeTimers.runTimersToTime(40); + expect(runOrder).toEqual(['mock2', 'mock3', 'mock1']); + + // Move forward to t=200 + fakeTimers.runTimersToTime(100); + expect(runOrder).toEqual(['mock2', 'mock3', 'mock1', 'mock4']); + + // Move forward to t=400 + fakeTimers.runTimersToTime(200); + expect(runOrder).toEqual(['mock2', 'mock3', 'mock1', 'mock4', 'mock4']); + }); + + it('does nothing when no timers have been scheduled', function() { + var global = {}; + var fakeTimers = new FakeTimers(global); + + fakeTimers.runTimersToTime(100); + }); + + it('throws before allowing infinite recursion', function() { + var global = {}; + var fakeTimers = new FakeTimers(global); + + global.setTimeout(function infinitelyRecursingCallback() { + global.setTimeout(infinitelyRecursingCallback, 0); + }, 0); + + expect(function() { + fakeTimers.runTimersToTime(50); + }).toThrow( + 'Ran 100000 timers, and there are still more! Assuming we\'ve hit an ' + + 'infinite recursion and bailing out...' + ); + }); + }); + + describe('reset', function() { + it('resets all pending setTimeouts', function() { + var global = {}; + var fakeTimers = new FakeTimers(global); + + var mock1 = jest.genMockFn(); + global.setTimeout(mock1, 100); + + fakeTimers.reset(); + fakeTimers.runAllTimers(); + expect(mock1.mock.calls.length).toBe(0); + }); + + it('resets all pending setIntervals', function() { + var global = {}; + var fakeTimers = new FakeTimers(global); + + var mock1 = jest.genMockFn(); + global.setInterval(mock1, 200); + + fakeTimers.reset(); + fakeTimers.runAllTimers(); + expect(mock1.mock.calls.length).toBe(0); + }); + + it('resets all pending ticks callbacks', function() { + var global = { + process: { + nextTick: function() {} + } + }; + var fakeTimers = new FakeTimers(global); + + var mock1 = jest.genMockFn(); + global.process.nextTick(mock1); + + fakeTimers.reset(); + fakeTimers.runAllTicks(); + expect(mock1.mock.calls.length).toBe(0); + }); + + it('resets current runTimersToTime time cursor', function() { + var global = {}; + var fakeTimers = new FakeTimers(global); + + var mock1 = jest.genMockFn(); + global.setTimeout(mock1, 100); + fakeTimers.runTimersToTime(50); + + fakeTimers.reset(); + global.setTimeout(mock1, 100); + + fakeTimers.runTimersToTime(50); + expect(mock1.mock.calls.length).toBe(0); + }); + }); + + describe('runOnlyPendingTimers', function() { + it('runs all timers in order', function() { + var global = {}; + var fakeTimers = new FakeTimers(global); + + var runOrder = []; + + global.setTimeout(function cb() { + runOrder.push('mock1'); + global.setTimeout(cb, 100); + }, 100); + + global.setTimeout(function cb() { + runOrder.push('mock2'); + global.setTimeout(cb, 0); + }, 0); + + global.setInterval(function() { + runOrder.push('mock3'); + }, 200); + + fakeTimers.runOnlyPendingTimers(); + expect(runOrder).toEqual([ + 'mock2', + 'mock1', + 'mock3' + ]); + + fakeTimers.runOnlyPendingTimers(); + expect(runOrder).toEqual([ + 'mock2', + 'mock1', + 'mock3', + + 'mock2', + 'mock1', + 'mock3' + ]); + }); + }); + + describe('runWithRealTimers', function() { + it('executes callback with native setTimeout', function() { + var nativeSetTimeout = jest.genMockFn(); + var global = {setTimeout: nativeSetTimeout}; + var fakeTimers = new FakeTimers(global); + + fakeTimers.runWithRealTimers(function() { + global.setTimeout(); + }); + expect(nativeSetTimeout.mock.calls.length).toBe(1); + expect(global.setTimeout.mock.calls.length).toBe(0); + }); + + it('executes callback with native setInterval', function() { + var nativeSetInterval = jest.genMockFn(); + var global = {setInterval: nativeSetInterval}; + var fakeTimers = new FakeTimers(global); + + fakeTimers.runWithRealTimers(function() { + global.setInterval(); + }); + expect(nativeSetInterval.mock.calls.length).toBe(1); + expect(global.setInterval.mock.calls.length).toBe(0); + }); + + it('executes callback with native clearTimeout', function() { + var nativeClearTimeout = jest.genMockFn(); + var global = {clearTimeout: nativeClearTimeout}; + var fakeTimers = new FakeTimers(global); + + fakeTimers.runWithRealTimers(function() { + global.clearTimeout(); + }); + expect(nativeClearTimeout.mock.calls.length).toBe(1); + expect(global.clearTimeout.mock.calls.length).toBe(0); + }); + + it('executes callback with native clearInterval', function() { + var nativeClearInterval = jest.genMockFn(); + var global = {clearInterval: nativeClearInterval}; + var fakeTimers = new FakeTimers(global); + + fakeTimers.runWithRealTimers(function() { + global.clearInterval(); + }); + expect(nativeClearInterval.mock.calls.length).toBe(1); + expect(global.clearInterval.mock.calls.length).toBe(0); + }); + + it('resets mock setTimeout after executing callback', function() { + var nativeSetTimeout = jest.genMockFn(); + var global = {setTimeout: nativeSetTimeout}; + var fakeTimers = new FakeTimers(global); + + fakeTimers.runWithRealTimers(function() { + global.setTimeout(); + }); + expect(nativeSetTimeout.mock.calls.length).toBe(1); + expect(global.setTimeout.mock.calls.length).toBe(0); + + global.setTimeout(); + expect(nativeSetTimeout.mock.calls.length).toBe(1); + expect(global.setTimeout.mock.calls.length).toBe(1); + }); + + it('resets mock setInterval after executing callback', function() { + var nativeSetInterval = jest.genMockFn(); + var global = {setInterval: nativeSetInterval}; + var fakeTimers = new FakeTimers(global); + + fakeTimers.runWithRealTimers(function() { + global.setInterval(); + }); + expect(nativeSetInterval.mock.calls.length).toBe(1); + expect(global.setInterval.mock.calls.length).toBe(0); + + global.setInterval(); + expect(nativeSetInterval.mock.calls.length).toBe(1); + expect(global.setInterval.mock.calls.length).toBe(1); + }); + + it('resets mock clearTimeout after executing callback', function() { + var nativeClearTimeout = jest.genMockFn(); + var global = {clearTimeout: nativeClearTimeout}; + var fakeTimers = new FakeTimers(global); + + fakeTimers.runWithRealTimers(function() { + global.clearTimeout(); + }); + expect(nativeClearTimeout.mock.calls.length).toBe(1); + expect(global.clearTimeout.mock.calls.length).toBe(0); + + global.clearTimeout(); + expect(nativeClearTimeout.mock.calls.length).toBe(1); + expect(global.clearTimeout.mock.calls.length).toBe(1); + }); + + it('resets mock clearInterval after executing callback', function() { + var nativeClearInterval = jest.genMockFn(); + var global = {clearInterval: nativeClearInterval}; + var fakeTimers = new FakeTimers(global); + + fakeTimers.runWithRealTimers(function() { + global.clearInterval(); + }); + expect(nativeClearInterval.mock.calls.length).toBe(1); + expect(global.clearInterval.mock.calls.length).toBe(0); + + global.clearInterval(); + expect(nativeClearInterval.mock.calls.length).toBe(1); + expect(global.clearInterval.mock.calls.length).toBe(1); + }); + + it('resets mock timer functions even if callback throws', function() { + var nativeSetTimeout = jest.genMockFn(); + var global = {setTimeout: nativeSetTimeout}; + var fakeTimers = new FakeTimers(global); + + expect(function() { + fakeTimers.runWithRealTimers(function() { + global.setTimeout(); + throw new Error('test'); + }); + }).toThrow('test'); + expect(nativeSetTimeout.mock.calls.length).toBe(1); + expect(global.setTimeout.mock.calls.length).toBe(0); + + global.setTimeout(); + expect(nativeSetTimeout.mock.calls.length).toBe(1); + expect(global.setTimeout.mock.calls.length).toBe(1); + }); + }); +}); diff --git a/src/lib/__tests__/utils-normalizeConfig-test.js b/src/lib/__tests__/utils-normalizeConfig-test.js new file mode 100644 index 000000000000..ea10f697c8c0 --- /dev/null +++ b/src/lib/__tests__/utils-normalizeConfig-test.js @@ -0,0 +1,316 @@ +/** + * Copyright (c) 2014, Facebook, Inc. All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +'use strict'; + +jest.autoMockOff(); + +describe('utils-normalizeConfig', function() { + var utils; + + beforeEach(function() { + utils = require('../utils'); + }); + + it('throws when an invalid config option is passed in', function() { + expect(function() { + utils.normalizeConfig({ + rootDir: '/root/path/foo', + thisIsAnInvalidConfigKey: 'with a value even!' + }); + }).toThrow('Unknown config option: thisIsAnInvalidConfigKey'); + }); + + describe('rootDir', function() { + it('throws if the config is missing a rootDir property', function() { + expect(function() { + utils.normalizeConfig({}); + }).toThrow('No rootDir config value found!'); + }); + }); + + describe('collectCoverageOnlyFrom', function() { + it('normalizes all paths relative to rootDir', function() { + var config = utils.normalizeConfig({ + rootDir: '/root/path/foo/', + collectCoverageOnlyFrom: { + 'bar/baz': true, + 'qux/quux/': true + } + }, '/root/path'); + + expect(config.collectCoverageOnlyFrom).toEqual({ + '/root/path/foo/bar/baz': true, + '/root/path/foo/qux/quux': true + }); + }); + + it('does not change absolute paths', function() { + var config = utils.normalizeConfig({ + rootDir: '/root/path/foo', + collectCoverageOnlyFrom: { + '/an/abs/path': true, + '/another/abs/path': true + } + }); + + expect(config.collectCoverageOnlyFrom).toEqual({ + '/an/abs/path': true, + '/another/abs/path': true + }); + }); + + it('substitutes tokens', function() { + var config = utils.normalizeConfig({ + rootDir: '/root/path/foo', + collectCoverageOnlyFrom: { + '/bar/baz': true + } + }); + + expect(config.collectCoverageOnlyFrom).toEqual({ + '/root/path/foo/bar/baz': true + }); + }); + }); + + describe('testPathDirs', function() { + it('normalizes all paths relative to rootDir', function() { + var config = utils.normalizeConfig({ + rootDir: '/root/path/foo', + testPathDirs: [ + 'bar/baz', + 'qux/quux/' + ] + }, '/root/path'); + + expect(config.testPathDirs).toEqual([ + '/root/path/foo/bar/baz', + '/root/path/foo/qux/quux' + ]); + }); + + it('does not change absolute paths', function() { + var config = utils.normalizeConfig({ + rootDir: '/root/path/foo', + testPathDirs: [ + '/an/abs/path', + '/another/abs/path' + ] + }); + + expect(config.testPathDirs).toEqual([ + '/an/abs/path', + '/another/abs/path' + ]); + }); + + it('substitutes tokens', function() { + var config = utils.normalizeConfig({ + rootDir: '/root/path/foo', + testPathDirs: [ + '/bar/baz' + ] + }); + + expect(config.testPathDirs).toEqual(['/root/path/foo/bar/baz']); + }); + }); + + describe('scriptPreprocessor', function() { + it('normalizes the path according to rootDir', function() { + var config = utils.normalizeConfig({ + rootDir: '/root/path/foo', + scriptPreprocessor: 'bar/baz' + }, '/root/path'); + + expect(config.scriptPreprocessor).toEqual('/root/path/foo/bar/baz'); + }); + + it('does not change absolute paths', function() { + var config = utils.normalizeConfig({ + rootDir: '/root/path/foo', + scriptPreprocessor: '/an/abs/path' + }); + + expect(config.scriptPreprocessor).toEqual('/an/abs/path'); + }); + + it('substitutes tokens', function() { + var config = utils.normalizeConfig({ + rootDir: '/root/path/foo', + scriptPreprocessor: '/bar/baz' + }); + + expect(config.scriptPreprocessor).toEqual('/root/path/foo/bar/baz'); + }); + }); + + describe('setupEnvScriptFile', function() { + it('normalizes the path according to rootDir', function() { + var config = utils.normalizeConfig({ + rootDir: '/root/path/foo', + setupEnvScriptFile: 'bar/baz' + }, '/root/path'); + + expect(config.setupEnvScriptFile).toEqual('/root/path/foo/bar/baz'); + }); + + it('does not change absolute paths', function() { + var config = utils.normalizeConfig({ + rootDir: '/root/path/foo', + setupEnvScriptFile: '/an/abs/path' + }); + + expect(config.setupEnvScriptFile).toEqual('/an/abs/path'); + }); + + it('substitutes tokens', function() { + var config = utils.normalizeConfig({ + rootDir: '/root/path/foo', + setupEnvScriptFile: '/bar/baz' + }); + + expect(config.setupEnvScriptFile).toEqual('/root/path/foo/bar/baz'); + }); + }); + + describe('setupTestFrameworkScriptFile', function() { + it('normalizes the path according to rootDir', function() { + var config = utils.normalizeConfig({ + rootDir: '/root/path/foo', + setupTestFrameworkScriptFile: 'bar/baz' + }, '/root/path'); + + expect(config.setupTestFrameworkScriptFile).toEqual( + '/root/path/foo/bar/baz' + ); + }); + + it('does not change absolute paths', function() { + var config = utils.normalizeConfig({ + rootDir: '/root/path/foo', + setupTestFrameworkScriptFile: '/an/abs/path' + }); + + expect(config.setupTestFrameworkScriptFile).toEqual('/an/abs/path'); + }); + + it('substitutes tokens', function() { + var config = utils.normalizeConfig({ + rootDir: '/root/path/foo', + setupTestFrameworkScriptFile: '/bar/baz' + }); + + expect(config.setupTestFrameworkScriptFile).toEqual( + '/root/path/foo/bar/baz' + ); + }); + }); + + describe('testPathIgnorePatterns', function() { + it('does not normalize paths relative to rootDir', function() { + // This is a list of patterns, so we can't assume any of them are + // directories + var config = utils.normalizeConfig({ + rootDir: '/root/path/foo', + testPathIgnorePatterns: [ + 'bar/baz', + 'qux/quux' + ] + }, '/root/path'); + + expect(config.testPathIgnorePatterns).toEqual([ + 'bar/baz', + 'qux/quux' + ]); + }); + + it('does not normalize trailing slashes', function() { + // This is a list of patterns, so we can't assume any of them are + // directories + var config = utils.normalizeConfig({ + rootDir: '/root/path/foo', + testPathIgnorePatterns: [ + 'bar/baz', + 'qux/quux/' + ] + }); + + expect(config.testPathIgnorePatterns).toEqual([ + 'bar/baz', + 'qux/quux/' + ]); + }); + + it('substitutes tokens', function() { + var config = utils.normalizeConfig({ + rootDir: '/root/path/foo', + testPathIgnorePatterns: [ + 'hasNoToken', + '/hasAToken' + ] + }); + + expect(config.testPathIgnorePatterns).toEqual([ + 'hasNoToken', + '/root/path/foo/hasAToken' + ]); + }); + }); + + describe('modulePathIgnorePatterns', function() { + it('does not normalize paths relative to rootDir', function() { + // This is a list of patterns, so we can't assume any of them are + // directories + var config = utils.normalizeConfig({ + rootDir: '/root/path/foo', + modulePathIgnorePatterns: [ + 'bar/baz', + 'qux/quux' + ] + }, '/root/path'); + + expect(config.modulePathIgnorePatterns).toEqual([ + 'bar/baz', + 'qux/quux' + ]); + }); + + it('does not normalize trailing slashes', function() { + // This is a list of patterns, so we can't assume any of them are + // directories + var config = utils.normalizeConfig({ + rootDir: '/root/path/foo', + modulePathIgnorePatterns: [ + 'bar/baz', + 'qux/quux/' + ] + }); + + expect(config.modulePathIgnorePatterns).toEqual([ + 'bar/baz', + 'qux/quux/' + ]); + }); + + it('substitutes tokens', function() { + var config = utils.normalizeConfig({ + rootDir: '/root/path/foo', + modulePathIgnorePatterns: [ + 'hasNoToken', + '/hasAToken' + ] + }); + + expect(config.modulePathIgnorePatterns).toEqual([ + 'hasNoToken', + '/root/path/foo/hasAToken' + ]); + }); + }); +}); diff --git a/src/lib/colors.js b/src/lib/colors.js new file mode 100644 index 000000000000..ffa94b62b2b0 --- /dev/null +++ b/src/lib/colors.js @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2014, Facebook, Inc. All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +'use strict'; + +var BOLD = '\x1B[1m'; +var GRAY = '\x1B[90m'; +var GREEN_BG = '\x1B[42m'; +var MAGENTA_BG = '\x1B[45m'; +var RED = '\x1B[31m'; +var RED_BG = '\x1B[41m'; +var RESET = '\x1B[0m'; +var UNDERLINE = '\x1B[4m'; + +function colorize(str, color) { + return color + str.toString().split(RESET).join(RESET + color) + RESET; +} + +exports.BOLD = BOLD; +exports.GRAY = GRAY; +exports.GREEN_BG = GREEN_BG; +exports.MAGENTA_BG = MAGENTA_BG; +exports.RED = RED; +exports.RED_BG = RED_BG; +exports.RESET = RESET; +exports.UNDERLINE = UNDERLINE; + +exports.colorize = colorize; diff --git a/src/lib/jsdom-compat.js b/src/lib/jsdom-compat.js new file mode 100644 index 000000000000..b052d4e890a5 --- /dev/null +++ b/src/lib/jsdom-compat.js @@ -0,0 +1,169 @@ +/** + * Copyright (c) 2014, Facebook, Inc. All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +'use strict'; + +/** + * This file contains various hacks and tweaks that were necessary at some + * point to get jsdom to behave correctly. + * + * TODO(benjamn) Periodically purge unnecessary stuff from this file + * and/or create upstream pull requests for obvious bugs. + */ + +// If this require starts failing in the future, it might be because +// cssstyle has matured enough that the hacks below are no longer +// necessary, so don't panic. +try { + var cssPropertyParsers = require('cssstyle/lib/parsers'); +} catch (err) { + // This error probably just means cssstyle is not installed yet, because + // we're still in the process of upgrading jsdom. Don't worry about it + // until jsdom has been updated to the latest version (v0.8.x). +} + +if (cssPropertyParsers) { + // The shorthandParser function should never return a string, but it + // does when given an empty string. Here we detect that case and make it + // return an empty object instead, to work around bugs in later code + // that assume the result of shorthandParser is always an object. + var shorthandParser = cssPropertyParsers.shorthandParser; + cssPropertyParsers.shorthandParser = function() { + var result = shorthandParser.apply(this, arguments); + return result === '' ? {} : result; + }; + + // Current versions of the cssstyle parseInteger function can't actually + // handle string inputs. + var badInt = cssPropertyParsers.parseInteger('5'); + if (badInt !== '5') { + cssPropertyParsers.parseInteger = function parseInteger(val) { + return String(parseInt(val, 10)); + }; + } + + // Current versions of the cssstyle parseNumber function can't actually + // handle string inputs. + var badNum = cssPropertyParsers.parseNumber('0.5'); + if (badNum !== '0.5') { + cssPropertyParsers.parseNumber = function parseNumber(val) { + return String(parseFloat(val, 10)); + }; + } +} + +// We can't require jsdom/lib/jsdom/browser/utils directly, because it +// requires jsdom, which requires utils circularly, so the utils module +// won't be fully populated when its (non-existent) NOT_IMPLEMENTED +// property is imported elsewhere. Instead, the only thing that seems to +// work is to override the utils module in require.cache, so that we never +// have to evaluate the original module. +try { + var utilsId = require.resolve('jsdom/lib/jsdom/browser/utils'); +} catch (err) { + // Leave utilsId undefined if require.resolve couldn't resolve it. +} + +if (utilsId) { + require.cache[utilsId] = { + id: utilsId, + exports: { + NOT_IMPLEMENTED: function(target, nameForErrorMessage) { + var message = 'NOT IMPLEMENTED' + ( + nameForErrorMessage ? ': ' + nameForErrorMessage : '' + ); + + return function() { + if (!jsdom.debugMode) { + // These two lines have been changed from the original + // NOT_IMPLEMENTED function to be more defensive about the + // presence/absence of .raise and raise.call. + var raise = (target && target.raise) || (this && this.raise); + if (raise && raise.call) { + raise.call(this, 'error', message); + } else { + // In case there was no suitable raise function to use, we + // still want to throw a meaningful Error (another + // improvement over the original NOT_IMPLEMENTED). + throw new Error(message); + } + } + }; + } + } + }; +} + +var jsdom = require('jsdom'); +var elements = jsdom.defaultLevel; +if (elements && elements.HTMLInputElement) { + var proto = elements.HTMLInputElement.prototype; + var desc = Object.getOwnPropertyDescriptor(proto, 'checked'); + if (desc) { + // Reimplement the .checked setter to require that two radio buttons + // have the same .form in order for their .checked values to be + // mutually exclusive. Except for the lines commented below, this code + // was borrowed directly from the jsdom implementation: + // https://github.com/tmpvar/jsdom/blob/0cf670d6eb/lib/jsdom/level2/html.js#L975-L990 + desc.set = function(checked) { + this._initDefaultChecked(); + + // Accept empty strings as truthy values for the .checked attribute. + if (checked || (checked === '')) { + this.setAttribute('checked', 'checked'); + + if (this.type === 'radio') { + var elements = this._ownerDocument.getElementsByName(this.name); + + for (var i = 0; i < elements.length; i++) { + var other = elements[i]; + if (other !== this && + other.tagName === 'INPUT' && + other.type === 'radio' && + // This is the condition that is missing from the default + // implementation of the .checked setter. + other.form === this.form) { + other.checked = false; + } + } + } + + } else { + this.removeAttribute('checked'); + } + }; + + Object.defineProperty(proto, 'checked', desc); + } +} + +// Make sure we unselect all but the first selected option when a