diff --git a/.gitignore b/.gitignore index bace69973f..96810b174d 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ gmon.out v8.log node_modules +test/worker-bundle.js diff --git a/lib/jsdom.js b/lib/jsdom.js index 52ee71f498..84b05809d8 100644 --- a/lib/jsdom.js +++ b/lib/jsdom.js @@ -34,6 +34,13 @@ exports.debugMode = false; }); }); +exports.debugMode = false; + +defineGetter(exports, 'version', function() { + return pkg.version; +}); + +var level2Html = require('./jsdom/level2/html'); exports.level = function (level, feature) { if(!feature) { feature = 'core'; diff --git a/lib/jsdom/contextify-shim.js b/lib/jsdom/contextify-shim.js new file mode 100644 index 0000000000..feaea5621e --- /dev/null +++ b/lib/jsdom/contextify-shim.js @@ -0,0 +1,5 @@ +module.exports = function (o) { + o.getGlobal = function () { + return o; + }; +}; diff --git a/package.json b/package.json index 1da8ae7f49..9ad8af8930 100644 --- a/package.json +++ b/package.json @@ -67,9 +67,24 @@ "contextify": "~0.1.5" }, "devDependencies" : { + "browser-request": "~0.3.1", + "cssstyle-browserify": "git://github.com/TreehouseJS/CSSStyleDeclaration.git", "nodeunit": "~0.8.0", "optimist": "*", - "urlmaster": ">=0.2.15" + "urlmaster": ">=0.2.15", + "http-browserify": "git://github.com/kumavis/http-browserify.git#dc84f15eb15c58505c0dea29de7ee64ff56dfe4f", + "browserify": "~3.24.1", + "q": "^1.0.1", + "wd": "^0.2.21", + "selenium-standalone": "^2.42.0-2.9.0", + "http-server": "^0.6.1" + }, + "browser": { + "canvas": false, + "contextify": "./lib/jsdom/contextify-shim.js", + "cssstyle": "./node_modules/cssstyle-browserify/lib/CSSStyleDeclaration.js", + "http": "./node_modules/http-browserify/index.js", + "request": "./node_modules/browser-request/index.js" }, "scripts": { "test": "node ./test/runner" diff --git a/test/browser-main.js b/test/browser-main.js new file mode 100644 index 0000000000..ce662740fc --- /dev/null +++ b/test/browser-main.js @@ -0,0 +1,34 @@ +window._browserRunner = { + events: [] +}; + +var worker = new Worker('./worker-bundle.js'); +var consoleEl = document.querySelector('.console'); + +function fire(event, detail) { + window._browserRunner.events.push({ + event: event, + detail: detail + }); +} + +worker.onmessage = function (e) { + if (e.data.method) { + switch (e.data.method) { + case 'fire': + fire(e.data.params.event, e.data.params.data); + break; + case 'console': + fire('console', e.data.params); + console[e.data.params.level].apply(console, e.data.params.message); + break; + case 'ready': + fire('ready'); + worker.postMessage(location.search.slice(1)); + break; + default: + console.error('Unknown method', e.data.method); + } + } +}; + diff --git a/test/browser-runner.js b/test/browser-runner.js new file mode 100644 index 0000000000..d8b0242c77 --- /dev/null +++ b/test/browser-runner.js @@ -0,0 +1,183 @@ +require('colors'); +var EventEmitter = require('events').EventEmitter; +var wd = require('wd'); +var Q = require('q'); +var browser; + +var optimist = require('./runner-options'); + +optimist. + usage('Run the jsdom test suite in a browser via WebDriver'). + describe('http-port', 'port to run test server on (defaults to pid + 20000)'). + describe('web-driver-port', 'port to run Selenium on (defaults to pid + 20000)'). + describe('verbose-web-driver', 'print verbose output from wd to stdout'). + describe('verbose-browser-console', 'print browser console to stdout'); + +var argv = optimist.argv; + +if (argv.help) { + optimist.showHelp(); + process.exit(); +} + +var httpPort = argv['http-port'] || process.pid + 20000; +var wdPort = argv['web-driver-port'] || httpPort + 10000; + +/** + * Return the body of a function as a string + * + * wd should do this for us, but it doesn't + */ +function getFnBody(fn) { + var src = fn.toString(); + return src.slice(src.indexOf('{') + 1, src.lastIndexOf('}')); +} + +function run() { + var passed = false; + browser.init({ browserName: 'chrome' }). + then(function () { + return browser.setAsyncScriptTimeout(5000); + }). + then(function () { + return browser.get([ + 'http://localhost:', + httpPort, + '/test?', + require('querystring').stringify(argv) + ].join('')); + }). + then(function (result) { + function browserPoll() { + var events = window._browserRunner.events; + + return events.splice(0, events.length); + } + + var deferred = Q.defer(); + + var runner = new EventEmitter(); + require('./runner-display')(runner, argv, function (err) { + passed = !err; + deferred.resolve(); + }); + var nodeunitTypes = require('nodeunit').types; + + function poll() { + browser. + execute(getFnBody(browserPoll)). + then(function (events) { + var done = false; + + events.forEach(function (event) { + switch (event.event) { + case 'testDone': + case 'moduleDone': + runner.emit(event.event, + event.detail[0], + nodeunitTypes.assertionList( + event.detail[1].map(nodeunitTypes.assertion))); + break; + case 'log': + runner.emit(event.event, + nodeunitTypes.assertion(event.detail[0])); + break; + case 'done': + runner.emit(event.event, + nodeunitTypes.assertionList( + event.detail[0].map(nodeunitTypes.assertion))); + break; + case 'console': + case 'http': + case 'status': + case 'command': + browser. + emit.apply(browser, [event.event].concat(event.detail)); + break; + default: + runner.emit.apply(runner, [event.event].concat(event.detail)); + } + + if (event.detail && event.event === 'done') { + done = true; + } + }); + + if (!done) { + setTimeout(poll, 50); + } + }); + } + + poll(); + return deferred.promise; + }). + fin(function () { + return browser.quit(); + }). + fin(function () { + process.exit(passed ? 0 : 1); + }). + done(); +} + +// browserify and run the tests +require('child_process').exec( + 'node_modules/browserify/bin/cmd.js test/worker.js -o test/worker-bundle.js', + function (err, stdout, stderr) { + if (err) { + console.log(stdout.toString()); + console.log('Failed to browserify test/worker'); + console.log(err); + process.exit(1); + return; + } + + // start web server + var httpServer = require('http-server').createServer().listen(httpPort); + + // set up webdriver + browser = wd.promiseRemote({ + port: wdPort + }); + + if (argv['verbose-web-driver']) { + // really verbose wd logging + browser.on('status', function (info) { + console.log(info.cyan); + }); + browser.on('command', function (eventType, command, response) { + console.log(' > ' + eventType.cyan, command, (response || '').grey); + }); + browser.on('http', function (method, path, data) { + console.log(' > ' + method.magenta, path, (data || '').grey); + }); + } + + if (argv['verbose-browser-console']) { + browser.on('console', function (detail) { + console[detail.level].apply(console, detail.message); + }); + } + + // start selenium + var selenium = require('selenium-standalone'); + var wdServer = selenium({ + stdio: 'pipe' + }, ['-port', wdPort]); + + // time out after a default of 30 seconds + var h = setTimeout(function () { + console.log('Timed out waiting for selenium server to start'); + wdServer.kill(); + process.exit(1); + }, argv.wdTimeout || 30 * 1000); + + // Wait for selenium server to start. + wdServer.stdout.on('data', function (output) { + if (output.toString().indexOf('Started org.openqa.jetty.jetty.Server') >= 0) { + clearTimeout(h); + run(); + } + }); + }); diff --git a/test/index.html b/test/index.html new file mode 100644 index 0000000000..1aaf5e52de --- /dev/null +++ b/test/index.html @@ -0,0 +1,10 @@ + + + + Browserify in worker test + + +

+    
+  
+
diff --git a/test/level2/core/files/staff2.xml.js b/test/level2/core/files/staff2.xml.js
index 9e01382f2c..2c77e6bc24 100644
--- a/test/level2/core/files/staff2.xml.js
+++ b/test/level2/core/files/staff2.xml.js
@@ -1,4 +1,4 @@
-var dom = require(__dirname + "/../../../../lib/jsdom/level2/core").dom.level2.core;
+var dom = require("../../../../lib/jsdom/level2/core").dom.level2.core;
 
 exports.staff2 = function() {
 
diff --git a/test/level2/style.js b/test/level2/style.js
index 6f4ff2a3cc..75e97f7c3b 100644
--- a/test/level2/style.js
+++ b/test/level2/style.js
@@ -3,6 +3,7 @@ var assert = require('assert');
 var http = require('http');
 var fs = require('fs');
 var path = require('path');
+
 exports.tests = {
 
   HTMLStyleElement01 : function (test) {
diff --git a/test/worker-runner.js b/test/worker-runner.js
new file mode 100644
index 0000000000..9f366f6a14
--- /dev/null
+++ b/test/worker-runner.js
@@ -0,0 +1,134 @@
+function fire(event) {
+  var data = Array.prototype.slice.call(arguments, 1);
+  data = JSON.parse(JSON.stringify(data));
+
+  postMessage({
+    jsonrpc: '2.0',
+    method: 'fire',
+    params: {
+      event: event,
+      data: data
+    }
+  });
+}
+
+var runner = require('./runner-core');
+self.onmessage = function (e) {
+  var options = require('querystring').parse(e.data);
+  var fileFilter, testFilter;
+
+  var modules = {
+    "level1/core": require("../test/level1/core"), // ok
+    "level1/html": require("../test/level1/html"), // ok
+    "level1/svg": require("../test/level1/svg"), // ok
+    "level2/core": require("../test/level2/core"), // ok
+    "level2/html": require("../test/level2/html"), // 6/708
+    "level2/style": require("../test/level2/style"), // 0/15
+    "level2/extra": require("../test/level2/extra"), // ok
+    "level2/events": require("../test/level2/events"), // ok
+    //"level3/core": require("../test/level3/core"),
+    //"level3/ls": require("../test/level3/ls"),
+    "level3/textContent.js": require("../test/level3/textContent.js"), // ok
+    "level3/xpath": require("../test/level3/xpath"), // 0/93
+
+    "living-dom/attributes.js": require("../test/living-dom/attributes.js"), // 0/10
+    "living-dom/compare-document-position.js": require("../test/living-dom/compare-document-position.js"), // 0/20
+    "living-dom/node-contains.js": require("../test/living-dom/node-contains.js"), // 0/20
+    "living-dom/node-parent-element.js": require("../test/living-dom/node-parent-element.js"), // 0/11
+    "living-html/navigator.js": require("../test/living-html/navigator.js"), // 0/2
+
+    "window/index": require("../test/window/index"), // ok
+    "window/history": require("../test/window/history"), // 0/5
+    "window/script": require("../test/window/script"), // 0/10
+    "window/console": require("../test/window/console"), // 0/2
+    //"window/frame": require("../test/window/frame"), // fail
+    //"sizzle/index": require("../test/sizzle/index"), // fail
+    //"jsdom/index": require("../test/jsdom/index"), // fail
+    "jsdom/parsing": require("../test/jsdom/parsing"), // 0/11
+    "jsdom/env": require("../test/jsdom/env"), // 9/25
+    "jsdom/utils": require("../test/jsdom/utils"), // ok
+    "jsonp/jsonp": require("../test/jsonp/jsonp"), // 0/1
+    "browser/css": require("../test/browser/css"), // 0/1
+    "browser/index": require("../test/browser/index"), // 30/34
+    "w3c/index.js": require("../test/w3c/index"), // 0/2
+  };
+
+  var modulesToRun = {};
+
+  if (options.suites) {
+    fileFilter = options.suites.replace(/\s/g, '').split(',');
+  }
+  // default to only those test modules that pass
+  fileFilter = fileFilter || [
+    "level1/core",
+    "level1/html",
+    "level1/svg",
+    "level2/core",
+    "level2/extra",
+    "level2/events",
+    "level3/textContent.js",
+    "window/index",
+    "jsdom/utils",
+  ];
+
+  if (options.tests) {
+    testFilter = options.tests.replace(/\s/g, '').split(',');
+  }
+
+  Object.keys(modules).
+    // process file filters
+    filter(function (module) {
+      if (fileFilter) {
+        return fileFilter.some(function (filter) {
+          return module.indexOf(filter) >= 0;
+        });
+      }
+
+      return true;
+    }).
+    forEach(function (name) {
+      var required = modules[name];
+      var module = required.tests || required;
+
+      if (testFilter) {
+        // process test filters
+        module = Object.keys(module).
+          filter(function (test) {
+            return testFilter.some(function (filter) {
+              return test.indexOf(filter) >= 0;
+            });
+          }).
+          reduce(function (filteredModule, test) {
+            filteredModule[test] = module[test];
+
+            return filteredModule;
+          }, {});
+      }
+
+      if (module && Object.keys(module).length > 0) {
+        modulesToRun[name] = module;
+      }
+    });
+
+  if (options.parser) {
+    var browser = require("../lib/jsdom/browser/index");
+    console.log("Using parser " + options.parser);
+    browser.setDefaultParser(options.parser);
+  }
+
+  var runnerEmitter = runner(modulesToRun);
+  [
+    'moduleStart',
+    'moduleDone',
+    'testStart',
+    'testReady',
+    'testDone',
+    'log',
+    'done'
+  ].forEach(function (event) {
+    runnerEmitter.on(event, fire.bind(null, event));
+  });
+  require('./runner-display')(runnerEmitter, options, function () {
+    console.log('display finished...');
+  });
+};
diff --git a/test/worker.js b/test/worker.js
new file mode 100644
index 0000000000..c4cf245350
--- /dev/null
+++ b/test/worker.js
@@ -0,0 +1,45 @@
+function postConsole(level) {
+  var message = Array.prototype.slice.call(arguments, 1);
+  postMessage({
+    jsonrpc: '2.0',
+    method: 'console',
+    params: {
+      level: level,
+      message: message
+    }
+  });
+}
+
+self.console = {
+  log: postConsole.bind(self.console, 'log'),
+  warn: postConsole.bind(self.console, 'warn'),
+  error: postConsole.bind(self.console, 'error')
+};
+
+self.process = self.process || {};
+
+var stream = require('stream');
+process.stdout = new stream.Writable();
+process.stdout._write = function (chunk, encoding, cb) {
+  var s = '';
+  for (var i = 0; i < chunk.length; i++) {
+    s += String.fromCharCode(chunk[i]);
+  }
+  console.log('stdout: ', s);
+
+  cb();
+};
+
+process.on = function () {
+  console.log('process.on called with:',
+    JSON.stringify(Array.prototype.slice.call(arguments)));
+};
+
+//var jsdom = require('../lib/jsdom');
+var nodeunit = require('nodeunit');
+var runner = require('./worker-runner');
+
+postMessage({
+  jsonrpc: '2.0',
+  method: 'ready'
+});