diff --git a/.gitignore b/.gitignore index d3277019a06f1..39a72e9b2da08 100644 --- a/.gitignore +++ b/.gitignore @@ -23,4 +23,5 @@ docs/js/examples docs/downloads examples/shared/*.js test/the-files-to-test.generated.js -sauce_connect.log* +*.log* +chrome-user-data diff --git a/Gruntfile.js b/Gruntfile.js index b7bb12257bd1a..3a905b4eaf0ef 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -70,6 +70,11 @@ module.exports = function(grunt) { grunt.registerTask('build:transformer', ['jsx:debug', 'browserify:transformer']); grunt.registerTask('build:min', ['jsx:release', 'version-check', 'browserify:min']); grunt.registerTask('build:addons-min', ['jsx:debug', 'browserify:addonsMin']); + grunt.registerTask('build:withCodeCoverageLogging', [ + 'jsx:debug', + 'version-check', + 'browserify:withCodeCoverageLogging' + ]); grunt.registerTask('build:test', [ 'jsx:test', 'version-check', @@ -78,11 +83,19 @@ module.exports = function(grunt) { grunt.registerTask('webdriver-phantomjs', webdriverPhantomJSTask); + grunt.registerTask('coverage:parse', require('./grunt/tasks/coverage-parse')); + grunt.registerTask('test:webdriver:phantomjs', [ 'connect', 'webdriver-phantomjs', 'webdriver-jasmine:local' ]); + grunt.registerTask('test:coverage', [ + 'build:test', + 'build:withCodeCoverageLogging', + 'test:webdriver:phantomjs', + 'coverage:parse' + ]); grunt.registerTask('test', ['build:test', 'build:basic', 'test:webdriver:phantomjs']); grunt.registerTask('npm:test', ['build', 'npm:pack']); diff --git a/grunt/config/browserify.js b/grunt/config/browserify.js index 953251d4c746f..5bf86631f1855 100644 --- a/grunt/config/browserify.js +++ b/grunt/config/browserify.js @@ -95,10 +95,23 @@ var addonsMin = grunt.util._.merge({}, addons, { after: [minify, bannerify] }); +var withCodeCoverageLogging = { + entries: [ + './build/modules/React.js' + ], + outfile: './build/react.js', + debug: true, + standalone: 'React', + transforms: [ + require('coverify') + ] +}; + module.exports = { basic: basic, min: min, transformer: transformer, addons: addons, - addonsMin: addonsMin + addonsMin: addonsMin, + withCodeCoverageLogging: withCodeCoverageLogging }; diff --git a/grunt/config/server.js b/grunt/config/server.js index 6227c8e714d45..4e2a323cbddd6 100644 --- a/grunt/config/server.js +++ b/grunt/config/server.js @@ -1,11 +1,62 @@ 'use strict'; -module.exports = function(grunt) { +module.exports = function(grunt){ + var coverageWriteStream; + + grunt.task.registerTask('finalize-coverage-stream', function(){ + if (!coverageWriteStream) { + return; + } + var done = this.async(); + coverageWriteStream.once('close', done); + coverageWriteStream.end(); + coverageWriteStream = null; + }); + + function consoleLoggerMiddleware(req, res, next) { + if (!(req.method == 'POST' && req._parsedUrl.pathname.replace(/\//g,'') == 'console' && Array.isArray(req.body))) { + return next(); + } + res.write(''); + res.end('Got it, thanks!'); + + req.body.forEach(function(log){ + if (log.message.indexOf('not ok ') === 0) { + log.type = 'error'; + } else if (log.message.indexOf('ok ') === 0) { + log.type = 'ok'; + } else if (log.message.indexOf('COVER') === 0) { + log.type = 'coverage'; + } else if (log.message.indexOf('DONE\t') === 0) { + log.type = 'coverage done'; + } + + if (log.type == 'error') { + grunt.log.error(log.message); + } else if (log.type == 'ok') { + grunt.log.ok(log.message); + } else if (log.type == 'log') { + grunt.log.writeln(log.message); + } else if (log.type == 'coverage') { + if (!coverageWriteStream) { + coverageWriteStream = require('fs').createWriteStream(__dirname + '/../../coverage.log'); + } + coverageWriteStream.write(log.message + '\n'); + } else if (log.type == 'coverage done') { + grunt.task.run('finalize-coverage-stream'); + } else { + grunt.verbose.writeln(log); + } + }); + } function testResultLoggerMiddleware(req, res, next) { if (!(req.method == 'POST' && req._parsedUrl.pathname.indexOf('/reportTestResults') === 0)) { return next(); } + res.write(''); + res.end('Got it, thanks!'); + var logType = 'writeln'; var message = req.body; @@ -23,8 +74,6 @@ module.exports = function(grunt) { message = JSON.stringify(message, null, 2); } grunt.log[logType]('[%s][%s]', req.headers['user-agent'], Date.now(), message); - res.write(''); - res.end('Got it, thanks!'); } return { @@ -40,13 +89,14 @@ module.exports = function(grunt) { return [ connect.json(), + consoleLoggerMiddleware, testResultLoggerMiddleware, connect.logger({format:'[:user-agent][:timestamp] :method :url', stream:grunt.verbose}), connect.static(options.base), connect.directory(options.base) ]; - }, + } } } }; diff --git a/grunt/tasks/browserify.js b/grunt/tasks/browserify.js index d43c0841f8619..c83ed7e65c4d1 100644 --- a/grunt/tasks/browserify.js +++ b/grunt/tasks/browserify.js @@ -46,7 +46,7 @@ module.exports = function() { }; // TODO: make sure this works, test with this too - config.transforms.forEach(bundle.transform, this); + config.transforms.forEach(bundle.transform, bundle); // Actually bundle it up var _this = this; diff --git a/grunt/tasks/coverage-parse.js b/grunt/tasks/coverage-parse.js new file mode 100644 index 0000000000000..223bed34bc551 --- /dev/null +++ b/grunt/tasks/coverage-parse.js @@ -0,0 +1,53 @@ +"use strict"; +var grunt = require('grunt'); + +module.exports = function(){ + var ROOT = require('path').normalize(__dirname + '/../..'); + var done = this.async(); + var uncoveredExpressionCount = 0; + var uncoveredLineCount = 0; + + require('fs').createReadStream(ROOT + '/coverage.log') + .pipe(require('coverify/parse')(function(error, results){ + if (error) { + grunt.fatal(error); + } + + Object.keys(results) + .sort(function(a, b){ + return results[a].length - results[b].length; + }) + .reverse() + .forEach(function(path){ + if (results[path].length === 0) { + return; + } + var relativePath = path.replace(ROOT, ''); + uncoveredExpressionCount += results[path].length; + grunt.log.error(results[path].length + ' expressions not covered ' + relativePath); + + results[path].forEach(function(c){ + uncoveredLineCount += c.code.split('\n').length; + console.log('txmt://open?url=' + encodeURIComponent('file://' + path) + '&line=' + (c.lineNum+1) + '&column=' + (c.column[0]+2)); + }); + console.log(''); + }) + ; + + Object.keys(results).sort().forEach(function(path){ + if (results[path].length > 0) { + return; + } + var relativePath = path.replace(ROOT, ''); + grunt.log.ok('100% coverage ' + relativePath); + }); + + if (uncoveredExpressionCount > 0) { + grunt.log.error(uncoveredExpressionCount + ' expressions not covered'); + } + if (uncoveredLineCount > 0) { + grunt.log.error(uncoveredLineCount + ' lines not covered'); + } + done(); + })); +}; diff --git a/grunt/tasks/populist.js b/grunt/tasks/populist.js index 034e97b3a6c63..6efa1e5132af6 100644 --- a/grunt/tasks/populist.js +++ b/grunt/tasks/populist.js @@ -34,6 +34,6 @@ module.exports = function() { }).then(function(output) { grunt.file.write(config.outfile, output); theFilesToTestScript.end(); - done(); + theFilesToTestScript.once('close', done); }); }; diff --git a/package.json b/package.json index 7a30d06e1c4e5..661fc7c044824 100644 --- a/package.json +++ b/package.json @@ -61,7 +61,8 @@ "grunt-contrib-connect": "~0.5.0", "es5-shim": "~2.1.0", "wd": "~0.2.2", - "sauce-tunnel": "~1.1.0" + "sauce-tunnel": "~1.1.0", + "coverify": "~0.1.1" }, "engines": { "node": ">=0.10.0" diff --git a/src/environment/__tests__/ReactWebWorker-test.js b/src/environment/__tests__/ReactWebWorker-test.js index 8703dbb2a1cf1..287163bb7d3b6 100644 --- a/src/environment/__tests__/ReactWebWorker-test.js +++ b/src/environment/__tests__/ReactWebWorker-test.js @@ -31,6 +31,8 @@ describe('ReactWebWorker', function() { var data = JSON.parse(e.data); if (data.type == 'error') { error = data.message + "\n" + data.stack; + } else if (data.type == 'log') { + console.log(data.message); } else { expect(data.type).toBe('done'); done = true; @@ -42,7 +44,7 @@ describe('ReactWebWorker', function() { }); runs(function() { if (error) { - console.log(error); + console.error(error); throw new Error(error); } }); diff --git a/src/test/worker.js b/src/test/worker.js index 7ba7e1ffc734c..10691fda0bbe0 100644 --- a/src/test/worker.js +++ b/src/test/worker.js @@ -1,6 +1,26 @@ /* jshint worker: true */ "use strict"; +if (typeof console == 'undefined') { + this.console = { + error: function(e){ + postMessage(JSON.stringify({ + type: 'error', + message: e.message, + stack: e.stack + })); + }, + log: function(message){ + postMessage(JSON.stringify({ + type: 'log', + message: message + })); + } + } +} + +console.log('worker BEGIN'); + // The UMD wrapper tries to store on `global` if `window` isn't available var global = {}; importScripts("phantomjs-shims.js"); @@ -8,13 +28,11 @@ importScripts("phantomjs-shims.js"); try { importScripts("../../build/react.js"); } catch (e) { - postMessage(JSON.stringify({ - type: 'error', - message: e.message, - stack: e.stack - })); + console.error(e); } postMessage(JSON.stringify({ type: 'done' })); + +console.log('worker END'); diff --git a/test/lib/reportTestResults.browser.js b/test/lib/reportTestResults.browser.js index 92010ecd7afaa..a521b735d458b 100644 --- a/test/lib/reportTestResults.browser.js +++ b/test/lib/reportTestResults.browser.js @@ -1,34 +1,44 @@ +var __DEBUG__ = location.search.substring(1).indexOf('debug') != -1; + if (typeof console == 'undefined') console = { log: function(){}, warn: function(){}, error: function(){} }; +var __consoleReport__ = []; + console._log = console.log; console.log = function(message){ console._log(message); - postDataToURL({type:'log', message:message}, '/reportTestResults'); + if (__DEBUG__) postDataToURL({type:'log', message:message}, '/reportTestResults'); + else __consoleReport__.push({type:'log', message:message}); } + console._error = console.error; console.error = function(message){ console._error(message); - postDataToURL({type:'error', message:message}, '/reportTestResults'); + if (__DEBUG__) postDataToURL({type:'error', message:message}, '/reportTestResults'); + else __consoleReport__.push({type:'error', message:message}); +} + +console._flush = function(){ + postDataToURL(__consoleReport__, '/console'); + __consoleReport__.length = 0; } ;(function(env){ env.addReporter(new jasmine.JSReporter()); - if (location.search.substring(1).indexOf('debug') != -1){ - env.addReporter(new TAPReporter(console.log.bind(console))); - } + env.addReporter(new TAPReporter(console.log.bind(console))); function report(){ if (typeof jasmine.getJSReport != 'function') { - console.log("typeof jasmine.getJSReport != 'function'"); return setTimeout(report, 100); } - postDataToURL(jasmine.getJSReport(), '/reportTestResults', function(error, results){ - if (error) return console.error(error); - }); + if (!__DEBUG__) { + console.log('DONE\t' + navigator.userAgent); + console._flush(); + } } var oldCallback = env.currentRunner().finishCallback;