Skip to content

Commit

Permalink
refactor(client): Transferred lots of client logic to context, adding…
Browse files Browse the repository at this point in the history
… Electron support via postMessage
  • Loading branch information
twolfson committed Mar 25, 2016
1 parent 6c62b87 commit 7efa81d
Show file tree
Hide file tree
Showing 12 changed files with 286 additions and 156 deletions.
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)
7 changes: 6 additions & 1 deletion 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 @@ -76,6 +79,8 @@ module.exports = function (grunt) {
'<%= files.grunt %>',
'<%= files.scripts %>',
'<%= files.client %>',
'<%= files.common %>',
'<%= files.context %>',
'test/**/*.js',
'gruntfile.js'
]
Expand Down
13 changes: 0 additions & 13 deletions static/context.js

This file was deleted.

0 comments on commit 7efa81d

Please sign in to comment.