diff --git a/Gruntfile.js b/Gruntfile.js index dd04aa2d139c..e2fdacbe4868 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -4,6 +4,7 @@ var exec = require('child_process').exec; var jsxTask = require('./grunt/tasks/jsx'); var browserifyTask = require('./grunt/tasks/browserify'); var wrapupTask = require('./grunt/tasks/wrapup'); +var populistTask = require('./grunt/tasks/populist'); var phantomTask = require('./grunt/tasks/phantom'); var npmTask = require('./grunt/tasks/npm'); var releaseTasks = require('./grunt/tasks/release'); @@ -16,6 +17,7 @@ module.exports = function(grunt) { jsx: require('./grunt/config/jsx/jsx'), browserify: require('./grunt/config/browserify'), wrapup: require('./grunt/config/wrapup'), + populist: require('./grunt/config/populist'), phantom: require('./grunt/config/phantom'), npm: require('./grunt/config/npm'), clean: ['./build', './*.gem', './docs/_site', './examples/shared/*.js'], @@ -44,6 +46,8 @@ module.exports = function(grunt) { // defines global variables instead of using require. grunt.registerMultiTask('wrapup', wrapupTask); + grunt.registerMultiTask('populist', populistTask); + grunt.registerMultiTask('phantom', phantomTask); grunt.registerMultiTask('npm', npmTask); @@ -52,11 +56,10 @@ module.exports = function(grunt) { grunt.registerTask('build:transformer', ['jsx:debug', 'browserify:transformer']); grunt.registerTask('build:min', ['jsx:release', 'browserify:min']); grunt.registerTask('build:test', [ - 'jsx:debug', 'jsx:jasmine', 'jsx:test', 'browserify:jasmine', - 'browserify:test' + 'populist:test' ]); grunt.registerTask('test', ['build:test', 'phantom:run']); diff --git a/bin/jsx b/bin/jsx index 2a6437d535b3..5e269cda97e2 100755 --- a/bin/jsx +++ b/bin/jsx @@ -32,5 +32,23 @@ require("commoner").resolve(function(id) { // Constant propagation means removing any obviously dead code after // replacing constant expressions with literal (boolean) values. - return propagate(constants, source); + source = propagate(constants, source); + + if (context.config.mocking) { + // Make sure there is exactly one newline at the end of the module. + source = source.replace(/\s+$/m, "\n"); + + return context.getProvidedP().then(function(idToPath) { + if (id !== "mock-modules" && + id !== "mocks" && + idToPath.hasOwnProperty("mock-modules")) { + return source + '\nrequire("mock-modules").register(' + + JSON.stringify(id) + ', module);\n'; + } + + return source; + }); + } + + return source; }); diff --git a/grunt/config/jsx/jsx.js b/grunt/config/jsx/jsx.js index 34fe98c16630..b8a049da92f2 100644 --- a/grunt/config/jsx/jsx.js +++ b/grunt/config/jsx/jsx.js @@ -25,7 +25,7 @@ var test = { "test/all.js", "**/__tests__/*.js" ]), - configFile: debug.configFile, + configFile: "grunt/config/jsx/test.json", sourceDir: "src", outputDir: "build/modules" }; diff --git a/grunt/config/jsx/test.json b/grunt/config/jsx/test.json new file mode 100644 index 000000000000..e897023aabc2 --- /dev/null +++ b/grunt/config/jsx/test.json @@ -0,0 +1,7 @@ +{ + "debug": true, + "mocking": true, + "constants": { + "__DEV__": true + } +} diff --git a/grunt/config/populist.js b/grunt/config/populist.js new file mode 100644 index 000000000000..2f353ac3d1c3 --- /dev/null +++ b/grunt/config/populist.js @@ -0,0 +1,13 @@ +'use strict'; + +var test = { + args: ["test/all:"], + requires: [ + "**/__tests__/*-test.js" + ], + outfile: './build/react-test.js' +}; + +module.exports = { + test: test +}; diff --git a/grunt/tasks/populist.js b/grunt/tasks/populist.js new file mode 100644 index 000000000000..f89f45ccef5f --- /dev/null +++ b/grunt/tasks/populist.js @@ -0,0 +1,28 @@ +'use strict'; + +var grunt = require('grunt'); + +module.exports = function() { + var config = this.data; + var done = this.async(); + + // create the bundle we'll work with + var args = config.args; + + // Make sure the things that need to be exposed are. + var requires = config.requires || {}; + grunt.file.expand({ + nonull: true, // Keep IDs that don't expand to anything. + cwd: "src" + }, requires).forEach(function(name) { + args.push(name.replace(/\.js$/i, "")); + }); + + require("populist").buildP({ + rootDirectory: "build/modules", + args: args + }).then(function(output) { + grunt.file.write(config.outfile, output); + done(); + }); +}; diff --git a/package.json b/package.json index 802e3e82689e..d35a9af1b830 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,7 @@ "devDependencies": { "browserify": "~2.24.1", "wrapup": "~0.12.0", + "populist": "~0.1.2", "grunt-cli": "~0.1.9", "grunt": "~0.4.1", "grunt-contrib-copy": "~0.4.1", diff --git a/src/test/all.js b/src/test/all.js index b370c31b629f..3b3a3639a9de 100644 --- a/src/test/all.js +++ b/src/test/all.js @@ -6,6 +6,9 @@ var Ap = Array.prototype; var slice = Ap.slice; var Fp = Function.prototype; +var global = Function("return this")(); +global.require = require; + if (!Fp.bind) { // PhantomJS doesn't support Function.prototype.bind natively, so // polyfill it whenever this module is required. diff --git a/src/test/mock-modules.js b/src/test/mock-modules.js index 6c0efd95f870..d8bd57f81a48 100644 --- a/src/test/mock-modules.js +++ b/src/test/mock-modules.js @@ -16,18 +16,95 @@ * @providesModule mock-modules */ -var global = Function("return this")(); -require('test/mock-timers').installMockTimers(global); +var mocks = require("mocks"); +var exportsRegistry = {}; +var hasOwn = exportsRegistry.hasOwnProperty; +var explicitMockMap = {}; -exports.dumpCache = function() { - require("mocks").clear(); +function getMock(exports) { + try { + return mocks.generateFromMetadata(mocks.getMetadata(exports)); + } catch (err) { + console.warn(err); return exports; -}; + } +} -exports.dontMock = function() { - return exports; +// This function should be called at the bottom of any module that might +// need to be mocked, after the final value of module.exports is known. +exports.register = function(id, module) { + exportsRegistry[id] = { + module: module, + actual: module.exports, + mocked: null // Filled in lazily later. + }; + + // If doMock or doNotMock was called earlier, before the module was + // registered, then the choice should have been recorded in + // explicitMockMap. Now that the module is registered, we can finally + // fulfill the request. + if (hasOwn.call(explicitMockMap, id)) { + if (explicitMockMap[id]) { + doMock(id); + } else { + doNotMock(id); + } + } + + return exports; }; -exports.mock = function() { - return exports; +function resetEntry(id) { + if (hasOwn.call(exportsRegistry, id)) { + delete exportsRegistry[id].module.exports; + delete exportsRegistry[id]; + } +} + +exports.dumpCache = function() { + require("mocks").clear(); + + // Deleting module.exports will cause the module to be lazily + // reevaluated the next time it is required. + for (var id in exportsRegistry) { + resetEntry(id); + } + + return exports; }; + +// Call this function to ensure that require(id) returns the actual +// exports object created by the module. +function doNotMock(id) { + explicitMockMap[id] = false; + + var entry = exportsRegistry[id]; + if (entry && entry.module && entry.actual) { + entry.module.exports = entry.actual; + } + + return exports; +} + +// Call this function to ensure that require(id) returns a mock exports +// object based on the actual exports object created by the module. +function doMock(id) { + explicitMockMap[id] = true; + + var entry = exportsRegistry[id]; + if (entry && entry.module && entry.actual) { + // Because mocking can be expensive, create the mock exports object on + // demand, the first time doMock is called. + entry.mocked || (entry.mocked = getMock(entry.actual)); + entry.module.exports = entry.mocked; + } + + return exports; +} + +var global = Function("return this")(); +require('test/mock-timers').installMockTimers(global); + +// Exported names are different for backwards compatibility. +exports.dontMock = doNotMock; +exports.mock = doMock; diff --git a/src/test/mocks.js b/src/test/mocks.js index d0d60b0f398a..ea25b052c425 100644 --- a/src/test/mocks.js +++ b/src/test/mocks.js @@ -72,7 +72,7 @@ function makeComponent(metadata) { metadata.members.prototype.members) || {}; var f = function() { - dirtyMocks.push(f); + global.dirtyMocks.push(f); instances.push(this); calls.push(Array.prototype.slice.call(arguments)); @@ -305,7 +305,8 @@ function removeUnusedRefs(metadata) { }); } -var dirtyMocks = []; +var global = Function("return this")(); +global.dirtyMocks = global.dirtyMocks || []; module.exports = { /** @@ -313,8 +314,8 @@ module.exports = { * called since the last time clear was called. */ clear: function() { - var old = dirtyMocks; - dirtyMocks = []; + var old = global.dirtyMocks; + global.dirtyMocks = []; old.forEach(function(mock) { mock.mockClear(); });