diff --git a/.babelrc b/.babelrc new file mode 100644 index 0000000..6206925 --- /dev/null +++ b/.babelrc @@ -0,0 +1 @@ +{ "presets": ["latest", "react"] } diff --git a/actions.js b/actions.js index 3020159..56c0d14 100644 --- a/actions.js +++ b/actions.js @@ -19,7 +19,9 @@ module.exports = { if (activeElement && !$(activeElement).is(':focus') && notSillyBlankIEObject(activeElement)) { $(activeElement).trigger('blur'); } - document.activeElement = element; + if (document.toString() !== '[object HTMLDocument]'){ + document.activeElement = element; + } $(element).focus(); } }, diff --git a/karma.conf.js b/karma.conf.js index c937a3c..89b264a 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -40,7 +40,9 @@ module.exports = function(config) { ], browserify: { - debug: true + debug: true, + extensions: ['.jsx'], + transform: ['babelify'], }, client: { diff --git a/mount.js b/mount.js new file mode 100644 index 0000000..e0306cc --- /dev/null +++ b/mount.js @@ -0,0 +1,110 @@ +var runningInBrowser = !require('is-node'); +var createBrowser = require('./create'); +var VineHill = require('vinehill'); +var window = require('global'); +var document = window.document; + +function addRefreshButton() { + var refreshLink = document.createElement('a'); + refreshLink.href = window.location.href; + refreshLink.innerText = 'refresh'; + document.body.appendChild(refreshLink); + document.body.appendChild(document.createElement('hr')); +} + +var div; +function createTestDiv() { + if (div) { + div.parentNode.removeChild(div); + } + div = document.createElement('div'); + document.body.appendChild(div); + return div; +} + +if (runningInBrowser) { + if (/\/debug\.html$/.test(window.location.pathname)) { + localStorage['debug'] = 'browser-monkey'; + addRefreshButton(); + } +} else { + require('./stubBrowser'); +} + +function Mount(options) { + this.vinehill = new VineHill(); + this.startApp = options.startApp.bind(this); + this.stopApp = options.stopApp.bind(this); +} + +Mount.prototype.setOrigin = function(host) { + this.vinehill.setOrigin(host); + return this; +} + +Mount.prototype.withServer = function(host, app) { + this.vinehill.add(host, app); + return this; +} + +Mount.prototype.withApp = function(getApp) { + this.getApp = getApp; + return this; +} + +Mount.prototype.start = function() { + this.vinehill.start(); + this.app = this.getApp(); + this.startApp(); + return this; +} + +Mount.prototype.stop = function(){ + this.stopApp(); + this.vinehill.stop(); +} + +module.exports = { + angular: function() { + return new Mount({ + stopApp: function(){}, + startApp: function(){ + var app = this.app; + + var div = createTestDiv(); + div.setAttribute(app.directiveName, ''); + angular.bootstrap(div, [app.moduleName]); + + this.browser = createBrowser(document.body); + } + }); + }, + + hyperdom: function() { + var hyperdom = require('hyperdom'); + var router = require('hyperdom-router'); + var vquery = require('vdom-query'); + router.start(); + + return new Mount({ + stopApp: function(){ + router.clear(); + }, + startApp: function(){ + var app = this.app; + + if (runningInBrowser) { + this.browser = createBrowser(document.body); + hyperdom.append(createTestDiv(), app); + } else { + var vdom = hyperdom.html('body'); + + this.browser = createBrowser(vdom); + this.browser.set({$: vquery, visibleOnly: false, document: {}}); + + hyperdom.appendVDom(vdom, app, { requestRender: setTimeout, window: window }); + } + } + }); + } +} diff --git a/package.json b/package.json index 14b30a1..518c54d 100644 --- a/package.json +++ b/package.json @@ -4,16 +4,26 @@ "description": "reliable dom testing", "main": "index.js", "scripts": { - "test": "mocha && karma start --single-run" + "test": "mocha --compilers js:babel-register && karma start --single-run" }, "author": "Tim Macfarlane ", "license": "MIT", "devDependencies": { "2vdom": "^0.2.0", + "angular": "^1.6.0", + "babel-cli": "^6.18.0", + "babel-preset-latest": "^6.16.0", + "babel-preset-react": "^6.16.0", + "babel-register": "^6.18.0", + "babelify": "^7.3.0", "browserify": "^13.1.1", "chai-as-promised": "6.0.0", "detect-node": "^2.0.3", + "express": "^4.14.0", + "httpism": "^2.6.2", "hyperdom": "^0.2.0", + "hyperdom-router": "^2.18.0", + "is-node": "^1.0.2", "karma": "1.3.0", "karma-browserify": "5.1.0", "karma-browserstack-launcher": "1.1.1", @@ -27,6 +37,7 @@ "lie": "^3.0.1", "mocha": "3.1.2", "vdom-query": "https://github.com/featurist/vdom-query", + "vinehill": "^0.4.1", "virtual-dom": "^2.1.1", "watchify": "^3.7.0" }, diff --git a/readme.md b/readme.md index 2e2f2d2..9cf11dc 100644 --- a/readme.md +++ b/readme.md @@ -75,6 +75,45 @@ Or to be more specific: localStorage['debug'] = 'browser-monkey'; ``` +# mount + +Typically you will need to mount your application into the DOM before running your tests. + +Browser monkey comes with a handy way of doing this for popular web frameworks (currently hyperdom and angular are supported) + +```js +var mount = require('browser-monkey/mount'); + + +// for hyperdom +// where YourHyperdomApp is a class that has a render method. [see here](test/app/hyperdom.js) for an example + +var monkey = mount.hyperdom() + .withApp(() => new YourHyperdomApp()) + .start() + +// for angular +// where YourAngularApp is a class with fields 'directiveName' and 'moduleName' [see here](test/app/angular.js) for an example +var monkey = mount.hyperdom() + .withApp(() => new YourAngularApp()) + .start() + + +monkey.browser.find('h1').shouldHave({text: 'Hello World'}); +``` + +The mount functions (hyperdom/angular etc.) return a `Mount` object with the following chainable functions: + + * withApp - accepts a single function as a parameter that returns an object containing the application + * withServer - allows you to route http requests to an express server using [Vinehill](https://www.npmjs.com/package/vinehill) + * start - starts the application and returns a `monkey` + * stop - stops the application and performs any cleanup necessary + +The `monkey` has the following properties: + + * browser - a browser monkey object that scoped to your application + * app - the application passed to withApp + # api The API is made up of three concepts: scopes, actions and assertions. diff --git a/stubBrowser.js b/stubBrowser.js new file mode 100644 index 0000000..db955ca --- /dev/null +++ b/stubBrowser.js @@ -0,0 +1,27 @@ +var window = require('global'); + + +var registeredEvents = {}; +var pushState, replaceState; + +pushState = replaceState = function(state, title, url) { + window.location.pathname = url; + (registeredEvents['onpopstate'] || []).forEach(cb => cb({})); +}; + +window.location = window.location || {}; +window.location.pathname = window.location.pathname || '/'; +window.location.origin = window.location.origin || ''; +window.location.search = window.location.search || ''; +window.history = { + pushState, + replaceState, +}; + +window.addEventListener = function(eventName, cb) { + eventName = 'on'+eventName; + if (!registeredEvents[eventName]) { + registeredEvents[eventName] = []; + } + registeredEvents[eventName].push(cb); +}; diff --git a/test/actionsSpec.js b/test/actionsSpec.js index 93c0d82..71090b3 100644 --- a/test/actionsSpec.js +++ b/test/actionsSpec.js @@ -83,7 +83,7 @@ describe('actions', function(){ var clicked; var buttonState = 'disabled'; - button = dom.insert(''); + var button = dom.insert(''); button.on('click', function () { clicked = buttonState; }); diff --git a/test/app/angular.js b/test/app/angular.js new file mode 100644 index 0000000..2e3033f --- /dev/null +++ b/test/app/angular.js @@ -0,0 +1,29 @@ +var angular = require('angular'); +var httpism = require('httpism'); + +angular + .module('FrameworksApp', []) + .directive('bestFrameworks', function () { + return { + restrict: 'A', + controller: 'FrameworksController', + template: `
+ +
` + }; + }) + .controller('FrameworksController', function($scope){ + httpism.get('/api/frameworks').then(response => { + $scope.frameworks = response.body; + $scope.$digest(); + }); + }); + +module.exports = class WebApp { + constructor() { + this.directiveName = 'best-frameworks'; + this.moduleName = 'FrameworksApp'; + } +} diff --git a/test/app/hyperdom.jsx b/test/app/hyperdom.jsx new file mode 100644 index 0000000..83e5f00 --- /dev/null +++ b/test/app/hyperdom.jsx @@ -0,0 +1,27 @@ +/** @jsx hyperdom.jsx */ +var httpism = require('httpism/browser'); +var hyperdom = require('hyperdom'); +var html = hyperdom.html; + +module.exports = class WebApp { + constructor() { + var self = this; + this.model = { + frameworks: [] + }; + + httpism.get('/api/frameworks').then(response => { + self.model.frameworks = response.body; + self.model.refresh(); + }); + } + + + render() { + this.model.refresh = html.refresh; + + return + } +} diff --git a/test/app/server.js b/test/app/server.js new file mode 100644 index 0000000..a028279 --- /dev/null +++ b/test/app/server.js @@ -0,0 +1,10 @@ +var expressApp = (require('express'))(); +expressApp.get('/api/frameworks', (req, res) => { + res.json([ + 'browser-monkey', + 'hyperdom', + 'vinehill', + ]); +}); + +module.exports = expressApp; diff --git a/test/mountSpec.js b/test/mountSpec.js new file mode 100644 index 0000000..ac1afd3 --- /dev/null +++ b/test/mountSpec.js @@ -0,0 +1,45 @@ +var expect = require('chai').expect +var isBrowser = !require('is-node'); +if (isBrowser) { + var mount = require('../mount'); + var expressApp= require('./app/server'); + + require('./app/angular'); + require('./app/hyperdom'); + + [ + 'hyperdom', + 'angular' + ].forEach(appType => { + var WebApp = require('./app/'+appType); + var monkeyBuilder = mount[appType]; + + describe(`mount ${appType}`, () => { + var monkey, app; + + beforeEach(() => { + monkey = monkeyBuilder() + .withServer('http://localhost:1234', expressApp) + .withApp(() => { + app = new WebApp(); + return app; + }) + .start(); + }); + + afterEach(() => monkey.stop()); + + it('loads some data', () => { + return monkey.browser.find('li').shouldHave({text: [ + 'browser-monkey', + 'hyperdom', + 'vinehill', + ]}) + }); + + it('exposes the app', () => { + expect(monkey.app).to.equal(app); + }); + }); + }); +}