From b1e23fa8bb8f77636fbb49d7a86996efeb2c18f4 Mon Sep 17 00:00:00 2001 From: Todd Wolfson Date: Mon, 14 Mar 2016 00:48:42 -0500 Subject: [PATCH] refactor(client): Transferred lots of client logic to context, adding Electron support via postMessage --- .gitignore | 1 + .travis.yml | 10 +- client/karma.js | 120 +++++++-------------- client/main.js | 2 +- {client => common}/stringify.js | 0 {client => common}/util.js | 0 context/karma.js | 140 ++++++++++++++++++++++++ context/main.js | 24 +++++ gruntfile.js | 7 +- lib/middleware/karma.js | 8 +- static/context.html | 11 +- static/debug.html | 2 + test.sh | 1 + test/client/karma.conf.js | 8 +- test/client/karma.spec.js | 142 +++++++++++++++---------- test/client/stringify.spec.js | 2 +- test/client/util.spec.js | 2 +- test/e2e/basic.feature | 34 ------ test/e2e/support/context/context2.html | 11 +- 19 files changed, 324 insertions(+), 201 deletions(-) rename {client => common}/stringify.js (100%) rename {client => common}/util.js (100%) create mode 100644 context/karma.js create mode 100644 context/main.js create mode 100755 test.sh diff --git a/.gitignore b/.gitignore index 88230e825..72ab8b789 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ node_modules npm-debug.log +static/context.js static/karma.js .idea/* *.iml diff --git a/.travis.yml b/.travis.yml index 7678b5cf7..28a30a6d2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,9 +1,6 @@ sudo: false language: node_js node_js: - - "0.10" - - "0.12" - - "4" - "5" env: @@ -37,3 +34,10 @@ before_script: script: - npm run travis + +notifications: + email: + recipients: + - todd@twolfson.com + on_success: change + on_failure: change diff --git a/client/karma.js b/client/karma.js index 6c59bda4d..7a541135a 100644 --- a/client/karma.js +++ b/client/karma.js @@ -1,9 +1,8 @@ -var stringify = require('./stringify') +var stringify = require('../common/stringify') var constant = require('./constants') -var util = require('./util') +var util = require('../common/util') var Karma = function (socket, iframe, opener, navigator, location) { - var hasError = false var startEmitted = false var reloadingContext = false var store = {} @@ -23,73 +22,47 @@ var Karma = function (socket, iframe, opener, navigator, location) { // registry anymore. this.socket = socket + // Set up postMessage bindings for current window + // DEV: These are to allow windows in separate processes execute local tasks + // Electron is one of these environments + if (window.addEventListener) { + window.addEventListener('message', function handleMessage (evt) { + // Resolve the origin of our message + var origin = evt.origin || evt.originalEvent.origin + + // If the message isn't from our host, then reject it + if (origin !== window.location.origin) { + return + } + + // Take action based on the message type + var method = evt.data.method + if (!self[method]) { + self.error('Received `postMessage` for "' + method + '" but the method doesn\'t exist') + return + } + self[method].apply(self, evt.data.arguments) + }, false) + } + var childWindow = null var navigateContextTo = function (url) { if (self.config.useIframe === false) { - if (childWindow === null || childWindow.closed === true) { - // If this is the first time we are opening the window, or the window is closed - childWindow = opener('about:blank') + // If there is a window already open, then close it + // DEV: In some environments (e.g. Electron), we don't have setter access for location + if (childWindow !== null && childWindow.closed !== true) { + childWindow.close() } - childWindow.location = url + childWindow = opener(url) } else { iframe.src = url } } - this.setupContext = function (contextWindow) { - if (self.config.clearContext && hasError) { - return - } - - var getConsole = function (currentWindow) { - return currentWindow.console || { - log: function () {}, - info: function () {}, - warn: function () {}, - error: function () {}, - debug: function () {} - } - } - - contextWindow.__karma__ = this - - // This causes memory leak in Chrome (17.0.963.66) - contextWindow.onerror = function () { - return contextWindow.__karma__.error.apply(contextWindow.__karma__, arguments) - } - - contextWindow.onbeforeunload = function (e, b) { - if (!reloadingContext) { - // TODO(vojta): show what test (with explanation about jasmine.UPDATE_INTERVAL) - contextWindow.__karma__.error('Some of your tests did a full page reload!') - } - } - - if (self.config.captureConsole) { - // patch the console - var localConsole = contextWindow.console = getConsole(contextWindow) - var logMethods = ['log', 'info', 'warn', 'error', 'debug'] - var patchConsoleMethod = function (method) { - var orig = localConsole[method] - if (!orig) { - return - } - localConsole[method] = function () { - self.log(method, arguments) - return Function.prototype.apply.call(orig, localConsole, arguments) - } - } - for (var i = 0; i < logMethods.length; i++) { - patchConsoleMethod(logMethods[i]) - } - } - - contextWindow.dump = function () { - self.log('dump', arguments) - } - - contextWindow.alert = function (msg) { - self.log('alert', [msg]) + this.onbeforeunload = function () { + if (!reloadingContext) { + // TODO(vojta): show what test (with explanation about jasmine.UPDATE_INTERVAL) + self.error('Some of your tests did a full page reload!') } } @@ -114,7 +87,6 @@ var Karma = function (socket, iframe, opener, navigator, location) { // error during js file loading (most likely syntax error) // we are not going to execute at all this.error = function (msg, url, line) { - hasError = true var message = msg if (url) { @@ -175,21 +147,8 @@ var Karma = function (socket, iframe, opener, navigator, location) { } } - var UNIMPLEMENTED_START = function () { - this.error('You need to include some adapter that implements __karma__.start method!') - } - - // all files loaded, let's start the execution - this.loaded = function () { - // has error -> cancel - if (!hasError) { - this.start(this.config) - } - - // remove reference to child iframe - this.start = UNIMPLEMENTED_START - } - + // TODO: Does anyone use `this.store`??? + // https://gitter.im/karma-runner/karma?at=56e48e07618c335373eb497f this.store = function (key, value) { if (util.isUndefined(value)) { return store[key] @@ -206,13 +165,8 @@ var Karma = function (socket, iframe, opener, navigator, location) { } } - // supposed to be overriden by the context - // TODO(vojta): support multiple callbacks (queue) - this.start = UNIMPLEMENTED_START - socket.on('execute', function (cfg) { - // reset hasError and reload the iframe - hasError = false + // reset startEmitted and reload the iframe startEmitted = false self.config = cfg // if not clearing context, reloadingContext always true to prevent beforeUnload error diff --git a/client/main.js b/client/main.js index 93406c4be..ed9200013 100644 --- a/client/main.js +++ b/client/main.js @@ -4,7 +4,7 @@ require('core-js/es5') var Karma = require('./karma') var StatusUpdater = require('./updater') -var util = require('./util') +var util = require('../common/util') var KARMA_URL_ROOT = require('./constants').KARMA_URL_ROOT diff --git a/client/stringify.js b/common/stringify.js similarity index 100% rename from client/stringify.js rename to common/stringify.js diff --git a/client/util.js b/common/util.js similarity index 100% rename from client/util.js rename to common/util.js diff --git a/context/karma.js b/context/karma.js new file mode 100644 index 000000000..5d13a5a47 --- /dev/null +++ b/context/karma.js @@ -0,0 +1,140 @@ +// Load our dependencies +var stringify = require('../common/stringify') + +// Define our context Karma constructor +// TODO: We prob don't need a class, do we...? +var ContextKarma = function (callParentKarmaMethod) { + // Define local variables + var hasError = false + var self = this + + // Define our loggers + // DEV: These are intentionally repeated in client and context + this.log = function (type, args) { + var values = [] + + for (var i = 0; i < args.length; i++) { + values.push(this.stringify(args[i], 3)) + } + + this.info({log: values.join(', '), type: type}) + } + + this.stringify = stringify + + // Define our proxy error handler + // DEV: We require one in our context to track `hasError` + this.error = function () { + hasError = true + callParentKarmaMethod('error', [].slice.call(arguments)) + return false + } + + // Define our start handler + var UNIMPLEMENTED_START = function () { + this.error('You need to include some adapter that implements __karma__.start method!') + } + // all files loaded, let's start the execution + this.loaded = function () { + // has error -> cancel + if (!hasError) { + this.start(this.config) + } + + // remove reference to child iframe + this.start = UNIMPLEMENTED_START + } + // supposed to be overriden by the context + // TODO(vojta): support multiple callbacks (queue) + this.start = UNIMPLEMENTED_START + + // Define proxy methods + // DEV: This is a closured `for` loop (same as a `forEach`) for IE support + var proxyMethods = ['complete', 'info', 'result'] + for (var i = 0; i < proxyMethods.length; i++) { + (function bindProxyMethod (methodName) { + self[methodName] = function boundProxyMethod () { + callParentKarmaMethod(methodName, [].slice.call(arguments)) + } + }(proxyMethods[i])) + } + + // Define bindings for context window + this.setupContext = function (contextWindow) { + // If we clear the context after every run and we already had an error + // then stop now. Otherwise, carry on. + if (self.config.clearContext && hasError) { + return + } + + // Perform window level bindings + // DEV: We return `self.error` since we want to `return false` to ignore errors + contextWindow.onerror = function () { + return self.error.apply(self, arguments) + } + // DEV: We must defined a function since we don't want to pass the event object + contextWindow.onbeforeunload = function (e, b) { + callParentKarmaMethod('onbeforeunload', []) + } + + contextWindow.dump = function () { + self.log('dump', arguments) + } + + contextWindow.alert = function (msg) { + self.log('alert', [msg]) + } + + // If we want to overload our console, then do it + var getConsole = function (currentWindow) { + return currentWindow.console || { + log: function () {}, + info: function () {}, + warn: function () {}, + error: function () {}, + debug: function () {} + } + } + if (self.config.captureConsole) { + // patch the console + var localConsole = contextWindow.console = getConsole(contextWindow) + var logMethods = ['log', 'info', 'warn', 'error', 'debug'] + var patchConsoleMethod = function (method) { + var orig = localConsole[method] + if (!orig) { + return + } + localConsole[method] = function () { + self.log(method, arguments) + return Function.prototype.apply.call(orig, localConsole, arguments) + } + } + for (var i = 0; i < logMethods.length; i++) { + patchConsoleMethod(logMethods[i]) + } + } + } +} + +// Define call/proxy methods +ContextKarma.getDirectCallParentKarmaMethod = function (parentWindow) { + return function directCallParentKarmaMethod (method, args) { + // If the method doesn't exist, then error out + if (!parentWindow.karma[method]) { + parentWindow.karma.error('Expected Karma method "' + method + '" to exist but it doesn\'t') + return + } + + // Otherwise, run our method + parentWindow.karma[method].apply(parentWindow.karma, args) + } +} +ContextKarma.getPostMessageCallParentKarmaMethod = function (parentWindow) { + // TODO: The postMessage implementation of `callParentKarmaMethod` is untested. Please test it + return function postMessageCallParentKarmaMethod (method, args) { + parentWindow.postMessage({method: method, arguments: args}, window.location.origin) + } +} + +// Export our module +module.exports = ContextKarma diff --git a/context/main.js b/context/main.js new file mode 100644 index 000000000..c27e3dffc --- /dev/null +++ b/context/main.js @@ -0,0 +1,24 @@ +// TODO: Be sure that we lint this... +// Load in our dependencies +var ContextKarma = require('./karma') + +// Resolve our parent window +var parentWindow = window.opener || window.parent + +// Define a remote call method for Karma +var callParentKarmaMethod = ContextKarma.getDirectCallParentKarmaMethod(parentWindow) + +// If we don't have access to the window, then use `postMessage` +// DEV: In Electron, we don't have access to the parent window due to it being in a separate process +// DEV: We avoid using this in Internet Explorer as they only support strings +// http://caniuse.com/#search=postmessage +var haveParentAccess = false +try { haveParentAccess = !!parentWindow.window } catch (err) { /* Ignore errors (likely permisison errors) */ } +if (!haveParentAccess) { + // TODO: In PhantomJS, we had to use `window.parent` not `window.opener`. + // If we run into issues, try moving to `window.parent` + callParentKarmaMethod = ContextKarma.getPostMessageCallParentKarmaMethod(parentWindow) +} + +// Define a window-scoped Karma +window.__karma__ = new ContextKarma(callParentKarmaMethod) diff --git a/gruntfile.js b/gruntfile.js index 5c793b8cb..9c2967cba 100644 --- a/gruntfile.js +++ b/gruntfile.js @@ -5,13 +5,16 @@ module.exports = function (grunt) { files: { server: ['lib/**/*.js'], client: ['client/**/*.js'], + common: ['common/**/*.js'], + context: ['context/**/*.js'], grunt: ['grunt.js', 'tasks/*.js'], scripts: ['scripts/init-dev-env.js'] }, browserify: { client: { files: { - 'static/karma.js': ['client/main.js'] + 'static/karma.js': ['client/main.js'], + 'static/context.js': ['context/main.js'] } } }, @@ -76,6 +79,8 @@ module.exports = function (grunt) { '<%= files.grunt %>', '<%= files.scripts %>', '<%= files.client %>', + '<%= files.common %>', + '<%= files.context %>', 'test/**/*.js', 'gruntfile.js' ] diff --git a/lib/middleware/karma.js b/lib/middleware/karma.js index 0c965a521..75047feb2 100644 --- a/lib/middleware/karma.js +++ b/lib/middleware/karma.js @@ -102,7 +102,7 @@ var createKarmaMiddleware = function ( } // serve karma.js - if (requestUrl === '/karma.js') { + if (requestUrl === '/karma.js' || requestUrl === '/context.js') { return serveStaticFile(requestUrl, response, function (data) { return data.replace('%KARMA_URL_ROOT%', urlRoot) .replace('%KARMA_VERSION%', VERSION) @@ -174,11 +174,7 @@ var createKarmaMiddleware = function ( return util.format(" '%s': '%s'", filePath, file.sha) }) - var clientConfig = '' - - if (requestUrl === '/debug.html') { - clientConfig = 'window.__karma__.config = ' + JSON.stringify(client) + ';\n' - } + var clientConfig = 'window.__karma__.config = ' + JSON.stringify(client) + ';\n' mappings = 'window.__karma__.files = {\n' + mappings.join(',\n') + '\n};\n' diff --git a/static/context.html b/static/context.html index 1269eea7c..2da2d0d52 100644 --- a/static/context.html +++ b/static/context.html @@ -15,14 +15,11 @@ to have already been created so they can insert their magic into it. For example, if loaded before body, Angular Scenario test framework fails to find the body and crashes and burns in an epic manner. --> +