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. -->
+