diff --git a/.travis.yml b/.travis.yml
index 527478ad7a..75aae891c6 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -26,6 +26,7 @@ env:
matrix:
- TEST_TYPE=test:full
- TEST_TYPE=lint
+ - TEST_TYPE=perf:full
- TEST_TYPE=test:coverage
- TEST_TYPE=test:webdriver:saucelabs BROWSER_NAME=ie11
- TEST_TYPE=test:webdriver:saucelabs BROWSER_NAME=ie10
@@ -43,6 +44,7 @@ matrix:
allow_failures:
- env: TEST_TYPE=lint
- env: TEST_TYPE=test:coverage
+ - env: TEST_TYPE=perf:full
- env: TEST_TYPE=test:webdriver:saucelabs BROWSER_NAME=ie11
- env: TEST_TYPE=test:webdriver:saucelabs BROWSER_NAME=ie10
- env: TEST_TYPE=test:webdriver:saucelabs BROWSER_NAME=ie9
diff --git a/Gruntfile.js b/Gruntfile.js
index a117066e05..0d554baa57 100644
--- a/Gruntfile.js
+++ b/Gruntfile.js
@@ -21,7 +21,8 @@ module.exports = function(grunt) {
browserify: require('./grunt/config/browserify'),
populist: require('./grunt/config/populist'),
connect: require('./grunt/config/server')(grunt),
- "webdriver-jasmine": require('./grunt/config/webdriver-jasmine.js'),
+ "webdriver-jasmine": require('./grunt/config/webdriver-jasmine'),
+ "webdriver-perf": require('./grunt/config/webdriver-perf'),
npm: require('./grunt/config/npm'),
clean: ['./build', './*.gem', './docs/_site', './examples/shared/*.js', '.module-cache'],
jshint: require('./grunt/config/jshint'),
@@ -39,6 +40,8 @@ module.exports = function(grunt) {
// Alias 'jshint' to 'lint' to better match the workflow we know
grunt.registerTask('lint', ['jshint']);
+ grunt.registerTask('download-previous-version', require('./grunt/tasks/download-previous-version.js'));
+
// Register jsx:debug and :release tasks.
grunt.registerMultiTask('jsx', jsxTask);
@@ -51,6 +54,8 @@ module.exports = function(grunt) {
grunt.registerMultiTask('webdriver-jasmine', webdriverJasmineTasks);
+ grunt.registerMultiTask('webdriver-perf', require('./grunt/tasks/webdriver-perf'));
+
grunt.registerMultiTask('npm', npmTask);
grunt.registerTask('npm-react:release', npmReactTasks.buildRelease);
@@ -67,6 +72,14 @@ module.exports = function(grunt) {
'version-check',
'browserify:withCodeCoverageLogging'
]);
+ grunt.registerTask('build:perf', [
+ 'jsx:release',
+ 'version-check',
+ 'browserify:transformer',
+ 'browserify:basic',
+ 'browserify:min',
+ 'download-previous-version'
+ ]);
grunt.registerTask('build:test', [
'jsx:test',
'version-check',
@@ -84,6 +97,12 @@ module.exports = function(grunt) {
'webdriver-jasmine:local'
]);
+ grunt.registerTask('perf:webdriver:phantomjs', [
+ 'connect',
+ 'webdriver-phantomjs',
+ 'webdriver-perf:local'
+ ]);
+
grunt.registerTask('test:full', [
'build:test',
'build:basic',
@@ -98,6 +117,20 @@ module.exports = function(grunt) {
'webdriver-jasmine:saucelabs_chrome'
]);
+ grunt.registerTask('perf:full', [
+ 'build:perf',
+
+ 'connect',
+ 'webdriver-phantomjs',
+ 'webdriver-perf:local',
+
+ 'sauce-tunnel',
+ 'webdriver-perf:saucelabs_firefox',
+ 'webdriver-perf:saucelabs_chrome',
+ 'webdriver-perf:saucelabs_ie11',
+ 'webdriver-perf:saucelabs_ie8',
+ ]);
+
grunt.registerTask('test:webdriver:saucelabs', [
'build:test',
'build:basic',
@@ -137,6 +170,7 @@ module.exports = function(grunt) {
'coverage:parse'
]);
grunt.registerTask('test', ['build:test', 'build:basic', 'test:webdriver:phantomjs']);
+ grunt.registerTask('perf', ['build:perf', 'perf:webdriver:phantomjs']);
grunt.registerTask('npm:test', ['build', 'npm:pack']);
// Optimized build task that does all of our builds. The subtasks will be run
diff --git a/grunt/config/server.js b/grunt/config/server.js
index 4e2a323cbd..241ac109e6 100644
--- a/grunt/config/server.js
+++ b/grunt/config/server.js
@@ -44,6 +44,8 @@ module.exports = function(grunt){
coverageWriteStream.write(log.message + '\n');
} else if (log.type == 'coverage done') {
grunt.task.run('finalize-coverage-stream');
+ } else if (log.type == 'perf') {
+ grunt.event.emit('perf results', log.message);
} else {
grunt.verbose.writeln(log);
}
diff --git a/grunt/config/webdriver-all.js b/grunt/config/webdriver-all.js
new file mode 100644
index 0000000000..d41c99501f
--- /dev/null
+++ b/grunt/config/webdriver-all.js
@@ -0,0 +1,113 @@
+'use strict';
+
+var grunt = require('grunt');
+
+module.exports = function(props){
+ if (typeof props.url != 'string') {
+ throw TypeError('expected url string');
+ }
+ if ('isDoneTimeout' in props && typeof props.isDoneTimeout != 'number') {
+ throw TypeError('expected isDoneTimeout to be a number');
+ }
+ if ('onStart' in props && typeof props.onStart != 'function') {
+ throw TypeError('expected onStart to be a function');
+ }
+ if ('onComplete' in props && typeof props.onComplete != 'function') {
+ throw TypeError('expected onComplete to be a function');
+ }
+ if ('onError' in props && typeof props.onError != 'function') {
+ throw TypeError('expected onError to be a function');
+ }
+
+ var exports = {};
+
+ exports.local = {
+ webdriver: {
+ remote: { protocol: 'http:', hostname: '127.0.0.1', port: 9515, path: '/' }
+ },
+ url: props.url,
+ onStart: props.onStart,
+ onComplete: props.onComplete,
+ onError: props.onError,
+ isDoneTimeout: props.isDoneTimeout
+ };
+
+ if (grunt.option('debug')) {
+ exports.local.url += (exports.local.url.indexOf('?') == -1 ? '?' : '&') + 'debug=' + grunt.option('debug');
+ }
+
+ exports.saucelabs = {
+ webdriver: {
+ remote: {
+ /* https://github.com/admc/wd/blob/master/README.md#named-parameters */
+ user: process.env.SAUCE_USERNAME || 'React',
+ pwd: process.env.SAUCE_ACCESS_KEY || '339d32ca-d594-4570-a3c2-94c50a91919b',
+ protocol: 'http:',
+ hostname: 'ondemand.saucelabs.com',
+ port: '80',
+ path: '/wd/hub'
+ }
+ },
+ desiredCapabilities: {
+ "build": process.env.TRAVIS_BUILD_NUMBER || 'dev' + Date.now(),
+ "tunnel-identifier": process.env.TRAVIS_JOB_NUMBER || 'my awesome tunnel',
+ "browserName": "chrome"
+ },
+ url: exports.local.url,
+ onStart: function(browser){
+ grunt.log.writeln("Starting WebDriver Test. Watch results here: http://saucelabs.com/tests/" + browser.sessionID);
+ if (props.onStart) {
+ return props.onStart(browser);
+ }
+ },
+ onComplete: exports.local.onComplete,
+ onError: exports.local.onError,
+ isDoneTimeout: exports.local.isDoneTimeout
+ };
+
+ /* https://saucelabs.com/platforms */
+ exports.saucelabs_ios =
+ exports.saucelabs_ios7 = sauceItUp({ browserName: 'iphone', version: '7', platform:'OS X 10.9' });
+ exports.saucelabs_ios6_1 = sauceItUp({ browserName: 'iphone', version: '6.1', platform:'OS X 10.8' });
+ exports.saucelabs_ios6 = sauceItUp({ browserName: 'iphone', version: '6', platform:'OS X 10.8' });
+ exports.saucelabs_ios5_1 = sauceItUp({ browserName: 'iphone', version: '5.1', platform:'OS X 10.8' });
+ exports.saucelabs_ios5 = sauceItUp({ browserName: 'iphone', version: '5', platform:'OS X 10.6' });
+ exports.saucelabs_ios4 = sauceItUp({ browserName: 'iphone', version: '4', platform:'OS X 10.6' });
+
+ exports.saucelabs_ipad =
+ exports.saucelabs_ipad7 = sauceItUp({ browserName: 'ipad', version: '7', platform:'OS X 10.9' });
+ exports.saucelabs_ipad6_1 = sauceItUp({ browserName: 'ipad', version: '6.1', platform:'OS X 10.8' });
+ exports.saucelabs_ipad6 = sauceItUp({ browserName: 'ipad', version: '6', platform:'OS X 10.8' });
+ exports.saucelabs_ipad5_1 = sauceItUp({ browserName: 'ipad', version: '5.1', platform:'OS X 10.8' });
+ exports.saucelabs_ipad5 = sauceItUp({ browserName: 'ipad', version: '5', platform:'OS X 10.6' });
+ exports.saucelabs_ipad4 = sauceItUp({ browserName: 'ipad', version: '4', platform:'OS X 10.6' });
+
+ exports.saucelabs_android = sauceItUp({ browserName: 'android', version: '4.0', platform:'Linux' });
+ exports.saucelabs_android_tablet = sauceItUp({ browserName: 'android', version: '4.0', platform:'Linux', 'device-type':'tablet' });
+
+ exports.saucelabs_safari = sauceItUp({ browserName: 'safari' });
+ exports.saucelabs_chrome = sauceItUp({ browserName: 'chrome' });
+ exports.saucelabs_firefox = sauceItUp({ browserName: 'firefox' });
+
+ exports.saucelabs_ie =
+ exports.saucelabs_ie8 = sauceItUp({ browserName: 'internet explorer', version: 8 });
+ exports.saucelabs_ie9 = sauceItUp({ browserName: 'internet explorer', version: 9 });
+ exports.saucelabs_ie10 = sauceItUp({ browserName: 'internet explorer', version: 10 });
+ exports.saucelabs_ie11 = sauceItUp({ browserName: 'internet explorer', version: 11, platform:'Windows 8.1' });
+
+ function sauceItUp(desiredCapabilities) {
+ desiredCapabilities["build"] = exports.saucelabs.desiredCapabilities["build"];
+ desiredCapabilities["tunnel-identifier"] = exports.saucelabs.desiredCapabilities["tunnel-identifier"];
+ return {
+ webdriver: exports.saucelabs.webdriver,
+ url: exports.saucelabs.url,
+ onStart: exports.saucelabs.onStart,
+ onComplete: exports.saucelabs.onComplete,
+ onError: exports.saucelabs.onError,
+ isDoneTimeout: exports.saucelabs.isDoneTimeout,
+ desiredCapabilities: desiredCapabilities,
+ };
+ }
+
+ return exports;
+}
diff --git a/grunt/config/webdriver-jasmine.js b/grunt/config/webdriver-jasmine.js
index b01660ddeb..3990c6271a 100644
--- a/grunt/config/webdriver-jasmine.js
+++ b/grunt/config/webdriver-jasmine.js
@@ -2,11 +2,7 @@
var grunt = require('grunt');
-
-exports.local = {
- webdriver: {
- remote: { protocol: 'http:', hostname: '127.0.0.1', port: 9515, path: '/' }
- },
+module.exports = require('./webdriver-all')({
url: "http://127.0.0.1:9999/test/index.html",
onComplete: function(passed){
if (!passed){
@@ -16,76 +12,4 @@ exports.local = {
onError: function(error){
grunt.fatal(error);
}
-};
-
-if (grunt.option('debug')) {
- exports.local.url += '?debug=' + grunt.option('debug');
-}
-
-
-exports.saucelabs = {
- webdriver: {
- remote: {
- /* https://github.com/admc/wd/blob/master/README.md#named-parameters */
- user: process.env.SAUCE_USERNAME || 'React',
- pwd: process.env.SAUCE_ACCESS_KEY || '339d32ca-d594-4570-a3c2-94c50a91919b',
- protocol: 'http:',
- hostname: 'ondemand.saucelabs.com',
- port: '80',
- path: '/wd/hub'
- }
- },
- desiredCapabilities: {
- "build": process.env.TRAVIS_BUILD_NUMBER || 'dev' + Date.now(),
- "tunnel-identifier": process.env.TRAVIS_JOB_NUMBER || 'my awesome tunnel',
- "browserName": "chrome"
- },
- url: exports.local.url,
- onStart: function(browser){
- grunt.log.writeln("Starting WebDriver Test. Watch results here: http://saucelabs.com/tests/" + browser.sessionID);
- },
- onComplete: exports.local.onComplete,
- onError: exports.local.onError
-};
-
-/* https://saucelabs.com/docs/platforms */
-exports.saucelabs_ios =
-exports.saucelabs_ios6_1 = sauceItUp({ browserName: 'iphone', version: '6.1', platform:'OS X 10.8' });
-exports.saucelabs_ios6 = sauceItUp({ browserName: 'iphone', version: '6', platform:'OS X 10.8' });
-exports.saucelabs_ios5_1 = sauceItUp({ browserName: 'iphone', version: '5.1', platform:'OS X 10.8' });
-exports.saucelabs_ios5 = sauceItUp({ browserName: 'iphone', version: '5', platform:'OS X 10.6' });
-exports.saucelabs_ios4 = sauceItUp({ browserName: 'iphone', version: '4', platform:'OS X 10.6' });
-
-exports.saucelabs_ipad =
-exports.saucelabs_ipad6_1 = sauceItUp({ browserName: 'ipad', version: '6.1', platform:'OS X 10.8' });
-exports.saucelabs_ipad6 = sauceItUp({ browserName: 'ipad', version: '6', platform:'OS X 10.8' });
-exports.saucelabs_ipad5_1 = sauceItUp({ browserName: 'ipad', version: '5.1', platform:'OS X 10.8' });
-exports.saucelabs_ipad5 = sauceItUp({ browserName: 'ipad', version: '5', platform:'OS X 10.6' });
-exports.saucelabs_ipad4 = sauceItUp({ browserName: 'ipad', version: '4', platform:'OS X 10.6' });
-
-exports.saucelabs_android = sauceItUp({ browserName: 'android', version: '4.0', platform:'Linux' });
-exports.saucelabs_android_tablet = sauceItUp({ browserName: 'android', version: '4.0', platform:'Linux', 'device-type':'tablet' });
-
-exports.saucelabs_safari = sauceItUp({ browserName: 'safari' });
-exports.saucelabs_chrome = sauceItUp({ browserName: 'chrome' });
-exports.saucelabs_firefox = sauceItUp({ browserName: 'firefox' });
-
-exports.saucelabs_ie =
-exports.saucelabs_ie8 = sauceItUp({ browserName: 'internet explorer', version: 8 });
-exports.saucelabs_ie9 = sauceItUp({ browserName: 'internet explorer', version: 9 });
-exports.saucelabs_ie10 = sauceItUp({ browserName: 'internet explorer', version: 10 });
-exports.saucelabs_ie11 = sauceItUp({ browserName: 'internet explorer', version: 11, platform:'Windows 8.1' });
-
-
-function sauceItUp(desiredCapabilities) {
- desiredCapabilities["build"] = exports.saucelabs.desiredCapabilities["build"];
- desiredCapabilities["tunnel-identifier"] = exports.saucelabs.desiredCapabilities["tunnel-identifier"];
- return {
- webdriver: exports.saucelabs.webdriver,
- url: exports.saucelabs.url,
- onStart: exports.saucelabs.onStart,
- onComplete: exports.saucelabs.onComplete,
- onError: exports.saucelabs.onError,
- desiredCapabilities: desiredCapabilities,
- };
-}
+});
diff --git a/grunt/config/webdriver-perf.js b/grunt/config/webdriver-perf.js
new file mode 100644
index 0000000000..b53dd5f7db
--- /dev/null
+++ b/grunt/config/webdriver-perf.js
@@ -0,0 +1,48 @@
+'use strict';
+
+var grunt = require('grunt');
+
+var tests = grunt.file.expand(__dirname + '/../../perf/tests/*');
+
+var maxTime = 5;
+
+var reactVersions = [
+ 'edge',
+ 'previous'
+];
+
+var params = []
+ .concat('headless=false')
+ .concat('maxTime=' + maxTime)
+ .concat(tests
+ .map(function(path){ return path.split(/tests./i).reverse()[0]; })
+ .map(encodeURIComponent)
+ .map(function(filename){ return 'test=' + filename; })
+ )
+ .concat(reactVersions
+ .map(encodeURIComponent)
+ .map(function(version){ return 'react=' + version }
+ )
+);
+
+module.exports = require('./webdriver-all')({
+
+ url: "http://127.0.0.1:9999/perf/index.html?" + params.join('&'),
+
+ isDoneTimeout: 15 * 60 * 1000,
+
+ onStart: function(){
+ grunt.event.on('perf results', function(results){
+ console.log(results);
+ });
+ },
+
+ onComplete: function(completedTestKeys){
+ grunt.verbose.writeln('onComplete ' + JSON.stringify(completedTestKeys));
+ },
+
+ onError: function(error){
+ grunt.fatal(error);
+ }
+
+});
diff --git a/grunt/tasks/download-previous-version.js b/grunt/tasks/download-previous-version.js
new file mode 100644
index 0000000000..fbb2c22d7d
--- /dev/null
+++ b/grunt/tasks/download-previous-version.js
@@ -0,0 +1,47 @@
+"use strict";
+
+var grunt = require('grunt');
+var http = require('http');
+var fs = require('fs');
+
+module.exports = function() {
+ var completedSuccessfully = this.async();
+ get(
+ "http://react.zpao.com/builds/master/latest/react.min.js",
+ __dirname + '/../../build/react-previous.min.js',
+ function(success){
+ if (!success) {
+ return completedSuccessfully(success);
+ }
+ get(
+ "http://react.zpao.com/builds/master/latest/JSXTransformer.js",
+ __dirname + '/../../build/JSXTransformer-previous.js',
+ completedSuccessfully
+ );
+ }
+ );
+
+ function get(url, targetFilePath, completedSuccessfully) {
+ grunt.verbose.writeln('getting url "' + url + '"');
+ http.get(url, function(response) {
+ grunt.verbose.writeln('Received status code ' + response.statusCode + ' for "' + url + '"');
+
+ if (response.statusCode != 200) {
+ if (response.headers.location) {
+ get(response.headers.location, targetFilePath);
+ return;
+ } else {
+ grunt.fatal('Nothing else to do.');
+ completedSuccessfully(false);
+ return;
+ }
+ }
+ grunt.verbose.writeln('Writing url to "' + targetFilePath + '"');
+ response.pipe(fs.createWriteStream(targetFilePath))
+ .on('close', function() {
+ completedSuccessfully(true);
+ })
+ ;
+ });
+ }
+};
diff --git a/grunt/tasks/webdriver-all.js b/grunt/tasks/webdriver-all.js
new file mode 100644
index 0000000000..4571f697c9
--- /dev/null
+++ b/grunt/tasks/webdriver-all.js
@@ -0,0 +1,73 @@
+/* jshint evil: true */
+
+'use strict';
+
+var grunt = require("grunt");
+var wd = require('wd');
+
+module.exports = function task(getJSReport){
+ var config = this.data;
+ var taskSucceeded = this.async();
+ getJSReport = getJSReport.bind(this, config, wd);
+
+ var desiredCapabilities = {};
+ if (config.desiredCapabilities) {
+ Object.keys(config.desiredCapabilities).forEach(function(key) {
+ if (config.desiredCapabilities[key] === undefined) {
+ return;
+ }
+ desiredCapabilities[key] = config.desiredCapabilities[key];
+ });
+ }
+ grunt.verbose.writeln("desiredCapabilities", JSON.stringify(desiredCapabilities));
+
+ var browser = wd.promiseChainRemote(config.webdriver.remote);
+
+ browser.on('status', function(info) {
+ grunt.verbose.writeln(info);
+ });
+
+ browser.on('command', function(meth, path, data) {
+ grunt.verbose.writeln(' > ' + meth, path, data || '');
+ });
+
+ var results = null;
+
+ // browser._debugPromise();
+ browser
+ .init(desiredCapabilities)
+ .then(config.onStart && config.onStart.bind(config, browser))
+ .get(config.url)
+ .then(function(){return browser;})
+ .then(getJSReport)
+ .then(function(data){ results = data; })
+ .fail(function(error){
+ grunt.log.error(error);
+ return browser
+ .eval('document.documentElement.innerText || document.documentElement.textContent')
+ .then(grunt.verbose.writeln.bind(grunt.verbose))
+ .then(function(){ throw error; })
+ ;
+ })
+ .finally(function(){
+ if (grunt.option('webdriver-keep-open')) {
+ return;
+ }
+ grunt.verbose.writeln('Closing the browser window. To keep it open, pass the --webdriver-keep-open flag to grunt.');
+ return browser.quit();
+ })
+ .done(
+ function() {
+ if (config.onComplete) {
+ config.onComplete(results);
+ }
+ taskSucceeded(true);
+ },
+ function(error) {
+ if (config.onError) {
+ config.onError(error);
+ }
+ taskSucceeded(false);
+ }
+ );
+};
diff --git a/grunt/tasks/webdriver-jasmine.js b/grunt/tasks/webdriver-jasmine.js
index 95a57305f9..613627dc92 100644
--- a/grunt/tasks/webdriver-jasmine.js
+++ b/grunt/tasks/webdriver-jasmine.js
@@ -2,82 +2,16 @@
'use strict';
-var grunt = require("grunt");
-var wd = require('wd');
-
module.exports = function(){
- var config = this.data;
- var taskSucceeded = this.async();
-
- var desiredCapabilities = {};
- if (config.desiredCapabilities) {
- Object.keys(config.desiredCapabilities).forEach(function(key) {
- if (config.desiredCapabilities[key] === undefined) {
- return;
- }
- desiredCapabilities[key] = config.desiredCapabilities[key];
- });
- }
- grunt.verbose.writeln("desiredCapabilities", JSON.stringify(desiredCapabilities));
-
- var browser = wd.promiseChainRemote(config.webdriver.remote);
-
- browser.on('status', function(info) {
- grunt.verbose.writeln(info);
+ return require('./webdriver-all').call(this, function(config, wd, browser){
+ return browser
+ .waitFor(wd.asserters.jsCondition("typeof window.jasmine != 'undefined'"), 5e3, 50)
+ .fail(function(error){
+ throw Error("The test page didn't load properly. " + error);
+ })
+ .waitFor(wd.asserters.jsCondition("typeof window.jasmine.getJSReport != 'undefined'"), 60e3, 100)
+ .waitFor(wd.asserters.jsCondition("window.postDataToURL.running <= 0"), 30e3, 500)
+ .eval("jasmine.getJSReport().passed")
+ ;
});
-
- browser.on('command', function(meth, path, data) {
- grunt.verbose.writeln(' > ' + meth, path, data || '');
- });
-
- var results = null;
-
- // browser._debugPromise();
- browser
- .init(desiredCapabilities)
- .then(config.onStart && config.onStart.bind(config, browser))
- .get(config.url)
- .then(function(){return browser;})
- .then(getJSReport)
- .then(function(data){ results = data; })
- .fail(function(error){
- grunt.log.error(error);
- return browser
- .eval('document.documentElement.innerText || document.documentElement.textContent')
- .then(grunt.verbose.writeln.bind(grunt.verbose))
- .then(function(){ throw error; })
- ;
- })
- .finally(function(){
- if (grunt.option('webdriver-keep-open')) {
- return;
- }
- grunt.verbose.writeln('Closing the browser window. To keep it open, pass the --webdriver-keep-open flag to grunt.');
- return browser.quit();
- })
- .done(
- function() {
- if (config.onComplete) {
- config.onComplete(results);
- }
- taskSucceeded(true);
- },
- function(error) {
- if (config.onError) {
- config.onError(error);
- }
- taskSucceeded(false);
- }
- );
};
-
-function getJSReport(browser){
- return browser
- .waitFor(wd.asserters.jsCondition("typeof window.jasmine != 'undefined'"), 5e3, 50)
- .fail(function(error){
- throw Error("The test page didn't load properly. " + error);
- })
- .waitFor(wd.asserters.jsCondition("typeof window.jasmine.getJSReport != 'undefined'"), 60e3, 100)
- .waitFor(wd.asserters.jsCondition("window.postDataToURL.running <= 0"), 30e3, 500)
- .eval("jasmine.getJSReport().passed");
-}
diff --git a/grunt/tasks/webdriver-perf.js b/grunt/tasks/webdriver-perf.js
new file mode 100644
index 0000000000..27143cf6a5
--- /dev/null
+++ b/grunt/tasks/webdriver-perf.js
@@ -0,0 +1,23 @@
+/* jshint evil: true */
+
+'use strict';
+
+var grunt = require('grunt');
+
+module.exports = function(){
+ return require('./webdriver-all').call(this, function(config, wd, browser){
+ if (!config.isDoneTimeout) {
+ grunt.verbose.writeln('Expected isDoneTimeout config, using default value');
+ }
+ grunt.verbose.writeln('isDoneTimeout:' + config.isDoneTimeout);
+ return browser
+ .waitFor(wd.asserters.jsCondition("window.isDone === false"), 5e3, 50)
+ .fail(function(error){
+ throw Error("The test page didn't load properly. " + error);
+ })
+ .waitFor(wd.asserters.jsCondition("window.isDone === true"), config.isDoneTimeout || 30e3, 1e3)
+ .waitFor(wd.asserters.jsCondition("window.postDataToURL.running <= 0"), 30e3, 500)
+ .eval("window.completedTestKeys || window._unhandledError")
+ ;
+ });
+};
diff --git a/package.json b/package.json
index 9edf1d9dd0..efea7a6a52 100644
--- a/package.json
+++ b/package.json
@@ -38,6 +38,7 @@
"jstransform": "~2.0.2"
},
"devDependencies": {
+ "benchmark": "~1.0.0",
"browserify": "~2.36.1",
"coverify": "~0.1.1",
"envify": "~1.0.1",
@@ -53,8 +54,10 @@
"grunt-contrib-jshint": "~0.7.2",
"gzip-js": "~0.3.2",
"jasmine-tapreporter": "~0.2.2",
+ "microtime": "~0.5.1",
"optimist": "~0.6.0",
"phantomjs": "~1.9",
+ "platform": "~1.0.0",
"populist": "~0.1.6",
"recast": "~0.5.6",
"sauce-tunnel": "~1.1.0",
diff --git a/perf/index.html b/perf/index.html
new file mode 100644
index 0000000000..4b91dda47c
--- /dev/null
+++ b/perf/index.html
@@ -0,0 +1,66 @@
+
+
+
Perf Tests
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/perf/lib/BrowserPerfRunnerApp.react.js b/perf/lib/BrowserPerfRunnerApp.react.js
new file mode 100644
index 0000000000..5b34f3bcaa
--- /dev/null
+++ b/perf/lib/BrowserPerfRunnerApp.react.js
@@ -0,0 +1,205 @@
+var BrowserPerfRunnerApp = React.createClass({
+
+ propTypes: {
+ tests: React.PropTypes.array.isRequired,
+ react: React.PropTypes.array.isRequired,
+ maxTime: React.PropTypes.number,
+ onCompleteEach: React.PropTypes.func,
+ onComplete: React.PropTypes.func,
+ onError: React.PropTypes.func,
+ headless: React.PropTypes.bool
+ },
+
+ getInitialState: function(){
+ var queue = [];
+ this.props.tests.forEach(function(testName){
+ this.props.react.forEach(function(version){
+ queue.push({
+ test: testName,
+ react: version
+ });
+ },this);
+ },this);
+ return {
+ queue: queue,
+ results: {}
+ };
+ },
+
+ handleResults: function(results){
+ this.state.results[results.test + '@' + results.react] = results;
+ this.replaceState(this.state);
+ },
+
+ handleComplete: function(queueItem){
+ queueItem.completed = true;
+
+ if (!this.props.onCompleteEach) {
+ return;
+ }
+ // Can't get the resultsForAllVersions if there are still some queued
+ var incompleteCount = 0;
+ for (var index = this.state.queue.length; --index >= 0;){
+ if (this.state.queue[index].completed) {
+ continue;
+ }
+ if (this.state.queue[index].test === queueItem.test) {
+ return;
+ }
+ incompleteCount ++;
+ }
+ var resultsForAllVersions = Object.keys(this.state.results)
+ .filter(function(key){return key.indexOf(queueItem.test) === 0;})
+ .map(function(key){return this.state.results[key];}, this)
+ ;
+ this.props.onCompleteEach(resultsForAllVersions);
+
+ if (this.props.onComplete && incompleteCount === 0) {
+ this.props.onComplete(this.state.results);
+ }
+ },
+
+ render: function(){
+ var grid = null;
+
+ if (!this.props.headless) {
+ grid = GridViewTable({
+ rows: this.props.tests,
+ cols: this.props.react,
+ renderCell: BrowserPerfRunnerApp.renderBenchmarkCell,
+ value: this.state.results
+ });
+ }
+
+ return React.DOM.div(null,
+ BenchmarkQueue({
+ initialQueue: this.state.queue,
+ onChange: this.handleResults,
+ maxTime: this.props.maxTime,
+ onCompleteEach: this.handleComplete,
+ onError: this.props.onError
+ }),
+ grid
+ );
+ }
+});
+
+BrowserPerfRunnerApp.renderBenchmarkCell = function(props, row, col){
+ if (col == null && row == null) return React.DOM.th(null);
+ if (row == null) return React.DOM.th({style:{verticalAlign:'top', textAlign:'center'}}, col);
+
+ var benchmarks = Object.keys(props.value)
+ .filter(function(key){
+ return key.indexOf(row) === 0;
+ })
+ .map(function(key){
+ return props.value[key];
+ })
+ .filter(function(benchmark){
+ return benchmark && !benchmark.isRunning && benchmark.stats;
+ })
+ ;
+
+ if (col == null) return React.DOM.th({style:{verticalAlign:'top', textAlign:'right'}},
+ React.DOM.a({href:'?test=' + row}, benchmarks[0] && benchmarks[0].name || row)
+ );
+
+ var key = row + '@' + col;
+ var benchmark = props.value[key];
+ if (!(benchmark && benchmark.stats)) return React.DOM.td({key:key});
+
+
+ var colors = [
+ '000000',
+ 'AA0000',
+ '00AA00',
+ 'AA5500',
+ '0000AA',
+ 'AA00AA',
+ '00AAAA',
+ 'AAAAAA',
+
+ '555555',
+ 'FF5555',
+ '55FF55',
+ 'FFFF55',
+ '5555FF',
+ 'FF55FF',
+ '55FFFF',
+ 'FFFFFF'
+ ];
+
+ function chartValue(value){
+ return Math.round(valueFromRangeToRange(value, chartValue.min, chartValue.max, 0, 100));
+ }
+ chartValue.min = Math.min.apply(Math, benchmarks.map(function(benchmark){return Math.min.apply(Math, benchmark.stats.sample);}));
+ chartValue.max = Math.max.apply(Math, benchmarks.map(function(benchmark){return Math.max.apply(Math, benchmark.stats.sample);}));
+
+ var means = benchmarks.map(function(benchmark){
+ return benchmark.stats.mean;
+ });
+ benchmarks.forEach(function(benchmark){
+ benchmark.isTheWinner = benchmark.stats.mean <= Math.min.apply(Math, means);
+ });
+
+ var chartValues = benchmarks.map(function(benchmark){
+ // benchmark.stats.sample.sort(function(a,b){return b - a;});
+ return benchmark.stats.sample.map(chartValue).join(',');
+ }).join('|');
+
+ return (
+ React.DOM.td({key:key, style:{textAlign:'center', width:234, verticalAlign:'top'}},
+ benchmark.error && benchmark.error.message || '',
+ React.DOM.div({style: benchmark.isTheWinner ? { backgroundColor:'#0A5', color:'#AFA' } : {backgroundColor:'transparent', color:'inherit'}},
+ Math.round(1 / benchmark.stats.mean * 100) / 100, " op/s ",
+ React.DOM.strong(null, Math.round(benchmark.stats.mean * 1000 * 100) / 100, " ms/op "),
+ React.DOM.small(null, "(±" + (Math.round(benchmark.stats.rme * 10) / 10) + "%)")
+ ),
+ benchmark.isRunning && 'Running' || React.DOM.img({
+ style: {
+ borderWidth: 2,
+ borderStyle: 'solid',
+ color: '#' + colors[benchmarks.indexOf(benchmark)]
+ },
+ width: 230,
+ height: 50,
+ src: 'https://chart.googleapis.com/chart?cht=ls&chs=460x100&chd=t:' + chartValues + '&chco=' + colors.join(',')
+ })
+ )
+ );
+}
+
+function valueFromRangeToRange(value, fromMin, fromMax, toMin, toMax){
+ var fromRange = fromMax - fromMin;
+ var toRange = toMax - toMin;
+ return (((value - fromMin) * toRange) / fromRange) + toMin;
+}
+
+var GridViewTable = React.createClass({
+
+ propTypes: {
+ rows: React.PropTypes.array.isRequired,
+ cols: React.PropTypes.array.isRequired,
+ renderCell: React.PropTypes.func.isRequired
+ },
+
+ _renderCell: function(col){
+ return this.props.renderCell({ value:this.props.value }, this._row, col);
+ },
+
+ _renderRow: function(row){
+ this._row = row;
+ return React.DOM.tr({key:row},
+ this._renderCell(null, 0),
+ this.props.cols.map(this._renderCell, this)
+ );
+ },
+
+ render: function(){
+ return React.DOM.table(null,
+ this._renderRow(null, 0),
+ this.props.rows.map(this._renderRow, this)
+ );
+ }
+
+});
diff --git a/perf/lib/BrowserPerfRunnerContext.react.js b/perf/lib/BrowserPerfRunnerContext.react.js
new file mode 100644
index 0000000000..0fdbca8390
--- /dev/null
+++ b/perf/lib/BrowserPerfRunnerContext.react.js
@@ -0,0 +1,195 @@
+var BenchmarkQueue = React.createClass({
+ propTypes: {
+ debug: React.PropTypes.bool,
+ onChange: React.PropTypes.func.isRequired,
+ initialQueue: React.PropTypes.array.isRequired,
+ maxTime: React.PropTypes.number,
+ onCompleteEach: React.PropTypes.func,
+ onError: React.PropTypes.func
+ },
+
+ getDefaultProps: function(){
+ return {
+ maxTime: 5
+ };
+ },
+
+ getInitialState: function(){
+ return {
+ queue: this.props.initialQueue.slice()
+ };
+ },
+
+ setItemState: function(state){
+ state.test = this.state.queue[0].test;
+ state.react = this.state.queue[0].react;
+ this.props.onChange(state);
+ },
+
+ handleContextReady: function(window){
+ var benchmark = window.Benchmark(window.exports);
+ benchmark.options.maxTime = this.props.maxTime; //DEBUG
+
+ var itemState = {
+ testRunnerURL: window.location.href,
+
+ name: window.exports.name,
+ platform: window.Benchmark.platform.description,
+ reactVersion: window.React.version,
+
+ isMinified: (function(){
+ var code = window.React.renderComponent.toString();
+ return code.indexOf(',') - code.indexOf('(') <= 2;
+ }())
+ };
+
+ this.setItemState(itemState);
+
+ var self = this;
+ benchmark.on('start error cycle complete', function(){
+ var stats = JSON.parse(JSON.stringify(benchmark.stats));
+ itemState.stats = stats;
+ itemState.isRunning = benchmark.running;
+ itemState.error = benchmark.error;
+ self.setItemState(itemState);
+ });
+ if (this.props.onError) {
+ benchmark.on('error', this.props.onError);
+ }
+ benchmark.on('complete', function(){
+ var queue = self.state.queue.slice();
+ var queueItem = queue.shift();
+ if (self.props.onCompleteEach) {
+ self.props.onCompleteEach(queueItem);
+ }
+ self.setState({ queue:queue });
+ });
+ benchmark.run({async:true});
+ },
+
+ shouldComponentUpdate: function(nextProps, nextState){
+ return nextState.queue.length < this.state.queue.length;
+ },
+
+ render: function(){
+ if (!(this.state.queue && this.state.queue.length > 0)){
+ return React.DOM.div({style:{display:'none'}});
+ }
+ return BrowserPerfRunnerContext({
+ debug: this.props.debug,
+ test: this.state.queue[0].test,
+ react: this.state.queue[0].react,
+ onReady: this.handleContextReady
+ });
+ }
+});
+
+var BrowserPerfRunnerContext = React.createClass({
+
+ propTypes: {
+ debug: React.PropTypes.bool,
+ test: function(object, key){
+ React.PropTypes.string.isRequired(object, key);
+ if (/\.jsx?$/i.test(object[key])) return;
+ throw Error('Expected `' + key + '` to be a test file name with extension `.js` or `.jsx`');
+ },
+ react: function(object, key){
+ React.PropTypes.string.isRequired(object, key);
+ if (/^(?:builds\/.+|edge|previous|(?:\d+\.){2}\d+)$/.test(object[key])) return;
+ throw Error('Expected `' + key + '` prop to be a valid react version string, build string or "edge" or "previous"');
+ },
+ onReady: React.PropTypes.func.isRequired
+ },
+
+ getInitialState: function(){
+ return {
+ testRunnerURL:'about:blank'
+ };
+ },
+
+ // _handleFrameError: function(error){
+ // console.error('BrowserPerfRunnerContext', error);
+ // },
+ //
+ // _handleFrameLoad: function(event){
+ // console.log('BrowserPerfRunnerContext', event);
+ // },
+ //
+ _handleMessage: function(event){
+ if (location.href.indexOf(event.origin) !== 0)
+ return console.debug('BrowserPerfRunnerContext#_handleMessage ignored message from ' + event.origin);
+ if (event.source.location.href.indexOf(this.state.testRunnerURL) === -1)
+ return console.debug('BrowserPerfRunnerContext#_handleMessage ignored message from ' + event.source.location.href);
+ if (event.data !== 'Ready!')
+ return console.debug('BrowserPerfRunnerContext#_handleMessage ignored message ' + JSON.stringify(event.data));
+
+ this.props.onReady(event.source);
+ },
+
+ _getTestRunnerURL: function(props){
+ return 'runner.html' +
+ '?' +
+ 'debug=' + (props.debug ? 1 : 0) +
+ '&' +
+ 'react=' + encodeURIComponent(props.react) +
+ '&' +
+ 'test=' + encodeURIComponent(props.test)
+ },
+
+ _renderState: function(props){
+ return {
+ testRunnerURL: this._getTestRunnerURL(props)
+ };
+ },
+
+ componentDidMount: function(){
+ var node = this.refs.iframe.getDOMNode();
+ // node.onload = this._handleFrameLoad;
+ // node.onerror = this._handleFrameError;
+ if (window.addEventListener) {
+ window.addEventListener('message', this._handleMessage, false);
+ } else if (window.attachEvent) {
+ window.attachEvent('onmessage', this._handleMessage);
+ } else {
+ throw Error('cannot attach onmessage listener');
+ }
+ this.setState(this._renderState(this.props));
+ },
+
+ componentWillUnmount: function(){
+ if (window.removeEventListener) {
+ window.removeEventListener('message', this._handleMessage);
+ } else if (window.detachEvent) {
+ window.detachEvent('onmessage', this._handleMessage);
+ } else {
+ throw Error('cannot detach onmessage listener');
+ }
+ this.refs.iframe.getDOMNode().src = '';
+ },
+
+ componentWillReceiveProps: function(nextProps){
+ this.setState(this._renderState(nextProps));
+ },
+
+ shouldComponentUpdate: function(nextProps, nextState){
+ return nextState.testRunnerURL != this.state.testRunnerURL;
+ },
+
+ render: function(){
+ return (
+ React.DOM.iframe({
+ ref: 'iframe',
+ name: "BrowserPerfRunnerContextFrame",
+ style: this.style,
+ src: this.state.testRunnerURL
+ })
+ );
+ },
+
+ style: {
+ position: 'absolute',
+ right: '100%',
+ bottom: '100%'
+ }
+
+});
diff --git a/perf/lib/perf-test-runner.browser.js b/perf/lib/perf-test-runner.browser.js
new file mode 100644
index 0000000000..26e059f8c3
--- /dev/null
+++ b/perf/lib/perf-test-runner.browser.js
@@ -0,0 +1,204 @@
+if (typeof console == 'undefined') console = {
+ log: function(){},
+ warn: function(){},
+ error: function(){},
+ debug: function(){}
+};
+
+var perfRunner;
+if (typeof exports == 'object') {
+ perfRunner = exports;
+} else {
+ perfRunner = {};
+}
+
+perfRunner.assert = function(test, message){
+ if (typeof test == 'function') test = test();
+ if (test) return;
+ throw Error(message);
+}
+
+perfRunner.WriteScript = function(props){
+ var type = '';
+ if (props.jsx) {
+ type = ' type="text/jsx"';
+ }
+ var src = props.src;
+ if (!props.cache) {
+ src += src.indexOf('?') === -1 ? '?_' : '&_';
+ src += perfRunner.WriteScript.cacheBust;
+ }
+ document.write('
+
+
+
+
+
diff --git a/perf/runner.html b/perf/runner.html
new file mode 100644
index 0000000000..976841ff37
--- /dev/null
+++ b/perf/runner.html
@@ -0,0 +1,59 @@
+
+
+Perf Test Runner
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/perf/tests/basic-div.js b/perf/tests/basic-div.js
new file mode 100644
index 0000000000..b72d6ded6d
--- /dev/null
+++ b/perf/tests/basic-div.js
@@ -0,0 +1,16 @@
+/* jshint undef: true, unused: true */
+
+/* global document */
+/* global window */
+/* global Benchmark */
+/* global React */
+
+if (typeof exports == 'undefined') exports = {};
+
+/*http://benchmarkjs.com/docs#options*/
+
+exports.name = 'React.DOM.div, no props';
+
+exports.fn = function(){
+ React.DOM.div(null, 'lol, perf testing ', this.count);
+};
diff --git a/perf/tests/basic-unmount.js b/perf/tests/basic-unmount.js
new file mode 100644
index 0000000000..25be4a1a9f
--- /dev/null
+++ b/perf/tests/basic-unmount.js
@@ -0,0 +1,23 @@
+/* jshint undef: true, unused: true */
+
+/* global document */
+/* global window */
+/* global Benchmark */
+/* global React */
+
+if (typeof exports == 'undefined') exports = {};
+
+/*http://benchmarkjs.com/docs#options*/
+
+exports.name = 'unmountComponentAtNode';
+
+exports.setup = function(){
+ /*global*/_rootNode = document.createElement('div');
+ document.body.appendChild(_rootNode);
+ var _firstChild = React.DOM.div(null, 'lol, perf testing ', this.count);
+ React.renderComponent(_firstChild, _rootNode);
+};
+exports.fn = function(){
+ if (React.unmountAndReleaseReactRootNode) React.unmountAndReleaseReactRootNode(_rootNode);
+ else React.unmountComponentAtNode(_rootNode);
+};
diff --git a/perf/tests/renderComponent-basic.js b/perf/tests/renderComponent-basic.js
new file mode 100644
index 0000000000..36ad367419
--- /dev/null
+++ b/perf/tests/renderComponent-basic.js
@@ -0,0 +1,24 @@
+/* jshint undef: true, unused: true */
+
+/* global document */
+/* global window */
+/* global Benchmark */
+/* global React */
+
+if (typeof exports == 'undefined') exports = {};
+
+/*http://benchmarkjs.com/docs#options*/
+
+exports.name = 'React.renderComponent single div';
+
+exports.setup = function(){
+ /*global*/_rootNode = document.createElement('div');
+ document.body.appendChild(_rootNode);
+};
+exports.fn = function(){
+ React.renderComponent(React.DOM.div(null, 'lol, perf testing ', this.count), _rootNode);
+};
+exports.teardown = function(){
+ if (React.unmountAndReleaseReactRootNode) React.unmountAndReleaseReactRootNode(_rootNode);
+ else React.unmountComponentAtNode(_rootNode);
+};
diff --git a/perf/tests/sanity.js b/perf/tests/sanity.js
new file mode 100644
index 0000000000..0150114f16
--- /dev/null
+++ b/perf/tests/sanity.js
@@ -0,0 +1,15 @@
+if (typeof exports == 'undefined') exports = {};
+
+/*http://benchmarkjs.com/docs#options*/
+
+exports.name = 'Trivial benchmark to verify that everything works';
+
+exports.setup = function(){
+ var foo;
+};
+exports.fn = function(){
+ foo = Array(999).join('Howdy!\n');
+};
+exports.teardown = function(){
+ foo = null;
+};
diff --git a/perf/tests/setState-callback-5.js b/perf/tests/setState-callback-5.js
new file mode 100644
index 0000000000..5868baaab5
--- /dev/null
+++ b/perf/tests/setState-callback-5.js
@@ -0,0 +1,41 @@
+if (typeof exports == 'undefined') exports = {};
+
+/*http://benchmarkjs.com/docs#options*/
+
+exports.name = 'From setState to callback (x5)';
+
+exports.defer = true;
+
+exports.setup = function(){
+ /*global*/_rootNode = document.createElement('div');
+ document.body.appendChild(_rootNode);
+ /*global*/setState = null;
+
+ var AwesomeComponent = React.createClass({
+ getInitialState: function(){
+ return { random:null };
+ },
+ render: function(){
+ if (!setState) setState = this.setState.bind(this);
+ return React.DOM.div(null, this.state.random);
+ }
+ });
+
+ React.renderComponent(AwesomeComponent(null), _rootNode);
+};
+exports.fn = function(deferred){
+ setState({random: Date.now() + Math.random()}, function(){
+ setState({random: Date.now() + Math.random()}, function(){
+ setState({random: Date.now() + Math.random()}, function(){
+ setState({random: Date.now() + Math.random()}, function(){
+ setState({random: Date.now() + Math.random()}, function(){
+ deferred.resolve();
+ });
+ });
+ });
+ });
+ });
+};
+exports.teardown = function(){
+ React.unmountComponentAtNode(_rootNode);
+};
diff --git a/perf/tests/setState-callback.js b/perf/tests/setState-callback.js
new file mode 100644
index 0000000000..c5a66ac174
--- /dev/null
+++ b/perf/tests/setState-callback.js
@@ -0,0 +1,33 @@
+if (typeof exports == 'undefined') exports = {};
+
+/*http://benchmarkjs.com/docs#options*/
+
+exports.name = 'From setState to callback';
+
+exports.defer = true;
+
+exports.setup = function(){
+ /*global*/_rootNode = document.createElement('div');
+ document.body.appendChild(_rootNode);
+ /*global*/setState = null;
+
+ var AwesomeComponent = React.createClass({
+ getInitialState: function(){
+ return { random:null };
+ },
+ render: function(){
+ if (!setState) setState = this.setState.bind(this);
+ return React.DOM.div(null, this.state.random);
+ }
+ });
+
+ React.renderComponent(AwesomeComponent(null), _rootNode);
+};
+exports.fn = function(deferred){
+ setState({random: Date.now() + Math.random()}, function(){
+ deferred.resolve();
+ });
+};
+exports.teardown = function(){
+ React.unmountComponentAtNode(_rootNode);
+};
diff --git a/perf/tests/shouldComponentUpdate.js b/perf/tests/shouldComponentUpdate.js
new file mode 100644
index 0000000000..3bdb4e70b2
--- /dev/null
+++ b/perf/tests/shouldComponentUpdate.js
@@ -0,0 +1,25 @@
+if (typeof exports == 'undefined') exports = {};
+
+/*http://benchmarkjs.com/docs#options*/
+
+exports.name = 'shouldComponentUpdate';
+
+exports.setup = function(){
+ var AwesomeComponent = React.createClass({
+ shouldComponentUpdate: function(){
+ return false;
+ },
+ render: function(){
+ return React.DOM.div({});
+ }
+ });
+
+ var _rootNode = document.createElement('div');
+ document.body.appendChild(_rootNode);
+};
+exports.fn = function(){
+ React.renderComponent(AwesomeComponent(null), _rootNode);
+};
+exports.teardown = function(){
+ React.unmountComponentAtNode(_rootNode);
+};
diff --git a/perf/tests/todolist-add.js b/perf/tests/todolist-add.js
new file mode 100644
index 0000000000..7cccbbd6ac
--- /dev/null
+++ b/perf/tests/todolist-add.js
@@ -0,0 +1,27 @@
+if (typeof exports == 'undefined') exports = {};
+
+/*http://benchmarkjs.com/docs#options*/
+
+exports.name = 'todolist from addItem to callback';
+
+exports.defer = true;
+
+exports.setup = function(){
+ /*global*/_rootNode = document.createElement('div');
+ document.body.appendChild(_rootNode);
+ /*global*/_app = todolist.App({ fakeDataCount: 333 });
+ React.renderComponent(_app, _rootNode);
+};
+exports.fn = function(deferred){
+ var liCount = document.getElementsByTagName('li').length;
+ _app.addItem(Math.random(), function(){
+ if (document.getElementsByTagName('li').length <= liCount) throw Error('expected a list item to be added to the dom');
+ deferred.resolve();
+ });
+};
+exports.teardown = function(){
+ React.unmountComponentAtNode(_rootNode);
+ _rootNode.parentNode.removeChild(_rootNode);
+ _rootNode = null;
+ _app = null;
+};
diff --git a/perf/tests/todolist-do-stuff.js b/perf/tests/todolist-do-stuff.js
new file mode 100644
index 0000000000..aee008c7b8
--- /dev/null
+++ b/perf/tests/todolist-do-stuff.js
@@ -0,0 +1,54 @@
+if (typeof exports == 'undefined') exports = {};
+
+/*http://benchmarkjs.com/docs#options*/
+
+/*global*/_timesToRun = 2;
+
+exports.name = 'todolist add, complete, remove (x' + _timesToRun + ')';
+
+exports.defer = true;
+
+exports.setup = function(){
+ /*global*/_rootNode = document.createElement('div');
+ document.body.appendChild(_rootNode);
+ /*global*/_app = todolist.App({ fakeDataCount: 333 });
+ React.renderComponent(_app, _rootNode);
+};
+
+exports.fn = function(deferred){
+ var originalLiCount = document.getElementsByTagName('li').length;
+
+ var todos = [];
+ var times = _timesToRun;
+ while (times-- >= 0){
+ todos.push(_app.addItem(times+1));
+ }
+
+ todos.forEach(function(todo){
+ _app.setItemCompleted(todo.id);
+ });
+
+ todos.forEach(function(todo){
+ _app.deleteItemById(todo.id);
+ });
+
+ todos = null;
+
+ _app.addItem(Math.random(), function(todo){
+ if (document.getElementsByTagName('li').length <= originalLiCount)
+ throw Error('expected a list item to be added to the dom');
+
+ _app.deleteItemById(todo.id, function(){
+ if (document.getElementsByTagName('li').length != originalLiCount)
+ throw Error('expected everything to be done by now');
+ deferred.resolve();
+ });
+ });
+};
+
+exports.teardown = function(){
+ React.unmountComponentAtNode(_rootNode);
+ _rootNode.parentNode.removeChild(_rootNode);
+ _rootNode = null;
+ _app = null;
+};
diff --git a/perf/tests/todolist-edit.js b/perf/tests/todolist-edit.js
new file mode 100644
index 0000000000..bd12898001
--- /dev/null
+++ b/perf/tests/todolist-edit.js
@@ -0,0 +1,32 @@
+if (typeof exports == 'undefined') exports = {};
+
+/*http://benchmarkjs.com/docs#options*/
+
+exports.name = 'todolist setItemCompleted';
+
+exports.defer = true;
+
+exports.setup = function(){
+ /*global*/_rootNode = document.createElement('div');
+ document.body.appendChild(_rootNode);
+ /*global*/_app = todolist.App({ fakeDataCount: 333 });
+ React.renderComponent(_app, _rootNode);
+ /*global*/_todo1 = _app.addItem("Howdy 1!");
+ /*global*/_todo2 = _app.addItem("Howdy 2!");
+ /*global*/_todo3 = _app.addItem("Howdy 3!");
+};
+
+exports.fn = function(deferred){
+ _app.setItemCompleted(_todo1.id, !_todo1.completed);
+ _app.setItemCompleted(_todo2.id, !_todo2.completed);
+ _app.setItemCompleted(_todo3.id, !_todo3.completed, function(){
+ deferred.resolve();
+ });
+};
+
+exports.teardown = function(){
+ React.unmountComponentAtNode(_rootNode);
+ _rootNode.parentNode.removeChild(_rootNode);
+ _rootNode = null;
+ _app = null;
+};
diff --git a/perf/tests/todolist-mount.js b/perf/tests/todolist-mount.js
new file mode 100644
index 0000000000..71bdc91dbe
--- /dev/null
+++ b/perf/tests/todolist-mount.js
@@ -0,0 +1,23 @@
+if (typeof exports == 'undefined') exports = {};
+
+/*http://benchmarkjs.com/docs#options*/
+
+exports.name = 'todolist from renderComponent to renderComponent callback (333 rows)';
+
+exports.defer = true;
+
+exports.setup = function(){
+ if (typeof _rootNode != 'undefined') throw Error("should teardown before running setup again");
+ /*global*/_rootNode = document.createElement('div');
+ document.body.appendChild(_rootNode);
+};
+
+exports.fn = function(deferred){
+ React.renderComponent(todolist.App({ fakeDataCount: 333 }), _rootNode, function(){ deferred.resolve(); });
+};
+
+exports.teardown = function(){
+ React.unmountComponentAtNode(_rootNode);
+ _rootNode.parentNode.removeChild(_rootNode);
+ _rootNode = undefined;
+};
diff --git a/test/index.html b/test/index.html
index 5370e23824..31621b1903 100644
--- a/test/index.html
+++ b/test/index.html
@@ -11,6 +11,7 @@
'../node_modules/jasmine-tapreporter/src/tapreporter.js',
'../vendor/jasmine-jsreporter/jasmine-jsreporter.js',
+ 'lib/postDataToURL.browser.js',
'lib/reportTestResults.browser.js',
'../build/react.js',
diff --git a/test/lib/postDataToURL.browser.js b/test/lib/postDataToURL.browser.js
new file mode 100644
index 0000000000..5b54345921
--- /dev/null
+++ b/test/lib/postDataToURL.browser.js
@@ -0,0 +1,34 @@
+function createXMLHttpRequest(){
+ try{return new XMLHttpRequest();}
+ catch(e){}
+ try {return new ActiveXObject("Msxml2.XMLHTTP");}
+ catch (e) {}
+ try {return new ActiveXObject("Microsoft.XMLHTTP");}
+ catch (e) {}
+}
+
+function getURLSync(url){
+ var request = createXMLHttpRequest();
+ request.open('GET', url, /*asynchronous?*/false);
+ return request.responseText;
+}
+
+function postDataToURL(data, url, callback) {
+ if (!callback) callback = postDataToURL.defaultCallback;
+ var request = createXMLHttpRequest();
+ if (!request) return callback(Error('XMLHttpRequest is unsupported'));
+ postDataToURL.running = (postDataToURL.running||0) + 1;
+ request.onreadystatechange = function(){
+ if (request.readyState != 4) return;
+ request.onreadystatechange = null;
+ postDataToURL.running = (postDataToURL.running||0) - 1;
+ callback(request.status == 200 ? null : request.status, request.responseText);
+ };
+ request.open('POST', url);
+ request.setRequestHeader('Content-Type', 'application/json');
+ request.send(JSON.stringify(data));
+}
+
+postDataToURL.defaultCallback = function(error){
+ // console.log('postDataToURL.defaultCallback', arguments)
+}
diff --git a/test/lib/reportTestResults.browser.js b/test/lib/reportTestResults.browser.js
index a521b735d4..983a427270 100644
--- a/test/lib/reportTestResults.browser.js
+++ b/test/lib/reportTestResults.browser.js
@@ -48,30 +48,3 @@ console._flush = function(){
};
}(window.jasmine.getEnv()));
-
-function createXMLHttpRequest(){
- try{return new XMLHttpRequest();}
- catch(e){}
- try {return new ActiveXObject("Msxml2.XMLHTTP");}
- catch (e) {}
- try {return new ActiveXObject("Microsoft.XMLHTTP");}
- catch (e) {}
-}
-function postDataToURL(data, url, callback) {
- if (!callback) callback = postDataToURL.defaultCallback;
- var request = createXMLHttpRequest();
- if (!request) return callback(Error('XMLHttpRequest is unsupported'));
- postDataToURL.running = (postDataToURL.running||0) + 1;
- request.onreadystatechange = function(){
- if (request.readyState != 4) return;
- request.onreadystatechange = null;
- postDataToURL.running = (postDataToURL.running||0) - 1;
- callback(request.status == 200 ? null : request.status, request.responseText);
- };
- request.open('POST', url);
- request.setRequestHeader('Content-Type', 'application/json');
- request.send(JSON.stringify(data));
-}
-postDataToURL.defaultCallback = function(error){
- // console.log('postDataToURL.defaultCallback', arguments)
-}