Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Transition from contextWindow to postMessage #1984

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
node_modules
npm-debug.log
static/context.js
static/karma.js
.idea/*
*.iml
Expand Down
118 changes: 35 additions & 83 deletions client/karma.js
Original file line number Diff line number Diff line change
@@ -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 self = this
Expand All @@ -22,73 +21,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!')
}
}

Expand All @@ -113,7 +86,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) {
Expand Down Expand Up @@ -174,28 +146,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
}

// 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
Expand Down
2 changes: 1 addition & 1 deletion client/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
File renamed without changes.
File renamed without changes.
138 changes: 138 additions & 0 deletions context/karma.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
// Load our dependencies
var stringify = require('../common/stringify')

// Define our context Karma constructor
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) {
return function postMessageCallParentKarmaMethod (method, args) {
parentWindow.postMessage({method: method, arguments: args}, window.location.origin)
}
}

// Export our module
module.exports = ContextKarma
21 changes: 21 additions & 0 deletions context/main.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// 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) {
callParentKarmaMethod = ContextKarma.getPostMessageCallParentKarmaMethod(parentWindow)
}

// Define a window-scoped Karma
window.__karma__ = new ContextKarma(callParentKarmaMethod)
8 changes: 6 additions & 2 deletions gruntfile.js
Original file line number Diff line number Diff line change
Expand Up @@ -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']
}
}
},
Expand Down Expand Up @@ -74,7 +77,8 @@ module.exports = function (grunt) {
'<%= files.grunt %>',
'<%= files.scripts %>',
'<%= files.client %>',
'static/context.js',
'<%= files.common %>',
'<%= files.context %>',
'static/debug.js',
'test/**/*.js',
'gruntfile.js'
Expand Down
13 changes: 0 additions & 13 deletions static/context.js

This file was deleted.

Loading