From 33e48f01a2fa7a99045dcc234acfba8184979eed Mon Sep 17 00:00:00 2001 From: Josiah Sprague Date: Thu, 4 Sep 2014 19:25:57 -0400 Subject: [PATCH 01/60] [#MmLRZ2E2] [#MmLRZ2E2] [#MmLRZ2E2] Make README less opinionated, add UMD definition - Make the README less opinionated about how to use the module - Add to README: DOM element, jQuery element or selector stings as ways to select the element. - Add information about default template to README - Add details about configuration options to README - Add UMD definition to module Branch: MmLRZ2E2-development Branch: MmLRZ2E2-development Branch: MmLRZ2E2-development --- README.md | 52 ++++++++++++++++++++---------------------- src/subscribe-email.js | 17 ++++++++++++++ 2 files changed, 42 insertions(+), 27 deletions(-) create mode 100644 src/subscribe-email.js diff --git a/README.md b/README.md index 711e19d..dad58ef 100644 --- a/README.md +++ b/README.md @@ -2,50 +2,48 @@ Subscribe Email is a UMD JavaScript module for rendering a mailing list sign up It allows developers to quickly include an email collection form on a page without being concerned with the implementation details of a specific mailing list platform. We're currently aiming to support mailing lists on SendGrid, MailChimp and Universe. -# Including the Module in Your Project -You can include the module any way that fits with your workflow; +# Getting Started -**If you use bower (recommended):** -`bower install subscribe-email --save` +## 1) Get the Module +If you're doing things manually, you can download the ZIP and extract the contents of the `/dist` directory into your project's assets folder. -**If you use npm:** -`npm install subscribe-email --save` +If you're using a package manager like [npm](https://www.npmjs.org/) or [Bower](http://bower.io/), you can install the module to your devDependencies. -**If you're not using a package manager:** -Just copy and paste the `/dist` directory to wherever you want it in your project. +## 2) Include the Module in Your Page +If you're doing things manually, just drop ` + + +

Here's a MailChimp subscribe form!

+
1
+

Here's a SendGrid subscribe form!

+
2
+

Here's a Universe subscribe form!

+
3
+ + + + + \ No newline at end of file diff --git a/gulp/browserify.js b/gulp/browserify.js new file mode 100644 index 0000000..7f82b4d --- /dev/null +++ b/gulp/browserify.js @@ -0,0 +1,38 @@ +var browserify = require('browserify'); +var bundleLogger = require('./util/bundleLogger'); +var gulp = require('gulp'); +var handleErrors = require('./util/handleErrors'); +var source = require('vinyl-source-stream'); + +gulp.task('browserify', function() { + var bundler = browserify({ + // Required watchify args + cache: {}, packageCache: {}, fullPaths: true, + // Specify the entry point of your app + entries: ['./src/subscribe-email.js'], + // Add file extentions to make optional in your requires + extensions: ['.hbs'], + // Enable source maps! + debug: true + }); + + var bundle = function() { + // Log when bundling starts + bundleLogger.start(); + + return bundler + .bundle() + // Report compile errors + .on('error', handleErrors) + // Use vinyl-source-stream to make the + // stream gulp compatible. Specifiy the + // desired output filename here. + .pipe(source('subscribe-email.js')) + // Specify the output destination + .pipe(gulp.dest('./build/')) + // Log when bundling completes! + .on('end', bundleLogger.end); + }; + + return bundle(); +}); \ No newline at end of file diff --git a/gulp/util/bundleLogger.js b/gulp/util/bundleLogger.js new file mode 100644 index 0000000..153f7ca --- /dev/null +++ b/gulp/util/bundleLogger.js @@ -0,0 +1,21 @@ +/* bundleLogger + ------------ + Provides gulp style logs to the bundle method in browserify.js +*/ + +var gutil = require('gulp-util'); +var prettyHrtime = require('pretty-hrtime'); +var startTime; + +module.exports = { + start: function() { + startTime = process.hrtime(); + gutil.log('Running', gutil.colors.green("'bundle'") + '...'); + }, + + end: function() { + var taskTime = process.hrtime(startTime); + var prettyTime = prettyHrtime(taskTime); + gutil.log('Finished', gutil.colors.green("'bundle'"), 'in', gutil.colors.magenta(prettyTime)); + } +}; \ No newline at end of file diff --git a/gulp/util/handleErrors.js b/gulp/util/handleErrors.js new file mode 100644 index 0000000..be53783 --- /dev/null +++ b/gulp/util/handleErrors.js @@ -0,0 +1,15 @@ +var notify = require("gulp-notify"); + +module.exports = function() { + + var args = Array.prototype.slice.call(arguments); + + // Send error to notification center with gulp-notify + notify.onError({ + title: "Compile Error", + message: "<%= error.message %>" + }).apply(this, args); + + // Keep gulp from hanging on this task + this.emit('end'); +}; \ No newline at end of file diff --git a/gulpfile.js b/gulpfile.js new file mode 100644 index 0000000..d725a26 --- /dev/null +++ b/gulpfile.js @@ -0,0 +1,19 @@ +/* + gulpfile.js + =========== + Rather than manage one giant configuration file responsible + for creating multiple tasks, each task has been broken out into + its own file in `/gulp`. Any file in that folder gets automatically + required below. + + To add a new task, simply add a new task file to the `/gulp` directory. +*/ +var gulp = require('gulp'); +var requireDir = require('require-dir'); + +// Require all tasks in gulp/tasks, including subfolders +requireDir('./gulp', { recurse: true }); + +// Task groups +gulp.task('default', ['build']); +gulp.task('build', ['browserify']); \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..c9bcbb9 --- /dev/null +++ b/package.json @@ -0,0 +1,22 @@ +{ + "name": "subscribe-email", + "version": "0.0.0", + "private": true, + "main": "subscribe-email.js", + "browserify": { + "transform": [ + "hbsfy" + ] + }, + "devDependencies": { + "browserify": "^5.11.1", + "gulp": "^3.8.7", + "gulp-notify": "^1.5.1", + "gulp-util": "^3.0.1", + "handlebars": "1.3.x", + "hbsfy": "^2.1.0", + "pretty-hrtime": "^0.2.1", + "require-dir": "^0.1.0", + "vinyl-source-stream": "^0.1.1" + } +} diff --git a/src/subscribe-email.js b/src/subscribe-email.js index 5cf5181..d9bf523 100644 --- a/src/subscribe-email.js +++ b/src/subscribe-email.js @@ -2,16 +2,44 @@ // found here: https://github.com/umdjs/umd/blob/master/returnExports.js (function (root, factory) { - if (typeof define === 'function' && define.amd) { - define(['b'], factory); - } else if (typeof exports === 'object') { - module.exports = factory(require('b')); - } else { - root.returnExports = factory(root.b); + if (typeof define === 'function' && define.amd) { + define(['handlebars'], factory); + } else if (typeof exports === 'object') { + module.exports = factory(require('./templates/BEM-with-messaging.hbs')); + } else { + root.SubscribeEmail = factory(root.handlebars); + } +}(this, function (template) { + + this.SubscribeEmail = function(options){ + var placeholder = document.querySelector(options.element); + var serviceConfig = {}; + switch (options.service) { + case 'mailchimp': + serviceConfig = { + formAction: 'http://mailchimp-api.com/route', + formMethod: 'POST', + emailName: 'EMAIL' + }; + break; + case 'sendgrid': + serviceConfig = { + formAction: 'http://sendgrid-api.com/route', + formMethod: 'POST' + }; + break; + default: + serviceConfig = { + formAction: '', + formMethod: '' + }; + break; } -}(this, function (b) { + //Merge the serviceConfig object into the template options + for (var attrname in serviceConfig) { options[attrname] = serviceConfig[attrname]; } + + placeholder.innerHTML = template(options); + }; - // Module code here - - return {}; + return this.SubscribeEmail; })); \ No newline at end of file diff --git a/src/templates/BEM-with-messaging.hbs b/src/templates/BEM-with-messaging.hbs new file mode 100644 index 0000000..752cc21 --- /dev/null +++ b/src/templates/BEM-with-messaging.hbs @@ -0,0 +1,4 @@ +
+ + +
\ No newline at end of file From 79c0cfbcccfb035a9138eb7ebf913374e30300b7 Mon Sep 17 00:00:00 2001 From: Josiah Sprague Date: Tue, 9 Sep 2014 10:36:48 -0400 Subject: [PATCH 03/60] [#MmLRZ2E2] Add Universe Support Add support for Universe API Branch: MmLRZ2E2-development Branch: MmLRZ2E2-development --- README.md | 7 ++- demo/index.html | 25 ++-------- gulp/startServer.js | 11 +++++ gulpfile.js | 2 +- package.json | 4 +- src/subscribe-email.js | 72 ++++++++++++++++++++-------- src/templates/BEM-with-messaging.hbs | 7 +-- 7 files changed, 81 insertions(+), 47 deletions(-) create mode 100644 gulp/startServer.js diff --git a/README.md b/README.md index 2eb5491..4beb13b 100644 --- a/README.md +++ b/README.md @@ -21,11 +21,12 @@ After you've gotten the module and included it in your page, you can start using `
` -2) Create a new `SubscribeEmail` instance somewhere in the page's JavaScript. The only parameters that are required are `element`, which is a DOM element, jQuery element, or selector string to refer to the placeholder element and `service` which is the name of mailing list platform you are using. +2) Create a new `SubscribeEmail` instance somewhere in the page's JavaScript. The only parameters that are required are `element`, which is a DOM element, jQuery element, or selector string to refer to the placeholder element and `service` which is the name of mailing list platform you are using. Depending on the service, you may need an API key, which you can define with the `key` parameter. ``` new SubscribeEmail({ element: '#subscribe-form', - service: 'mailchimp' + service: 'universe', + key: 'your-api-key-here' }); ``` @@ -38,6 +39,8 @@ The module can be configured with several optional parameters passed to it's con - `element`: **(Required)** A DOM element, jQuery element, or selector string to refer to the placeholder element. - `service`: **(Required)** The mailing list platform you are using. Available options are `mailchimp`, `sendgrid` and `universe`. +- `key`: An API key if required by your mailing list platform. +- `submitText`: A string to be used on the form's submit button. Defaults to "Subscribe". - `template`: A string of the name of the compiled handlebars template to use to render the form. Available templates are `'BEM-with-messaging'` or `'BEM-minimal'` (default). See "Customizing the Templates" below for more information. - `async`: Whether the form with be submitted asynchronously (defaults to false). diff --git a/demo/index.html b/demo/index.html index ae208b9..f9b1fe9 100644 --- a/demo/index.html +++ b/demo/index.html @@ -4,31 +4,14 @@ -

Here's a MailChimp subscribe form!

-
1
-

Here's a SendGrid subscribe form!

-
2

Here's a Universe subscribe form!

-
3
+
No-script fallback can go here.
- - diff --git a/gulp/startServer.js b/gulp/startServer.js new file mode 100644 index 0000000..f7b04e8 --- /dev/null +++ b/gulp/startServer.js @@ -0,0 +1,11 @@ +var gulp = require('gulp'); +var http = require('http'); +var ecstatic = require('ecstatic'); + +gulp.task('startServer', function() { + http.createServer( + ecstatic({ root: './' }) + ).listen(8080); + + console.log('Listening on :8080'); +}); \ No newline at end of file diff --git a/gulpfile.js b/gulpfile.js index d725a26..1f9f21b 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -15,5 +15,5 @@ var requireDir = require('require-dir'); requireDir('./gulp', { recurse: true }); // Task groups -gulp.task('default', ['build']); +gulp.task('default', ['build', 'startServer']); gulp.task('build', ['browserify']); \ No newline at end of file diff --git a/package.json b/package.json index c9bcbb9..f51b1bd 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "subscribe-email", "version": "0.0.0", "private": true, - "main": "subscribe-email.js", + "main": "src/subscribe-email.js", "browserify": { "transform": [ "hbsfy" @@ -10,6 +10,8 @@ }, "devDependencies": { "browserify": "^5.11.1", + "ecstatic": "^0.5.4", + "form-serialize": "^0.3.0", "gulp": "^3.8.7", "gulp-notify": "^1.5.1", "gulp-util": "^3.0.1", diff --git a/src/subscribe-email.js b/src/subscribe-email.js index d9bf523..b5f6e16 100644 --- a/src/subscribe-email.js +++ b/src/subscribe-email.js @@ -5,41 +5,75 @@ if (typeof define === 'function' && define.amd) { define(['handlebars'], factory); } else if (typeof exports === 'object') { - module.exports = factory(require('./templates/BEM-with-messaging.hbs')); + module.exports = factory(require('./templates/BEM-with-messaging.hbs'), require('form-serialize'), function(a){console.log(a);}); } else { root.SubscribeEmail = factory(root.handlebars); } -}(this, function (template) { +}(this, function (template, serialize) { this.SubscribeEmail = function(options){ + var placeholder = document.querySelector(options.element); + var serviceConfig = configureService(options.service); + + //Merge the serviceConfig object into the template options + for (var attrname in serviceConfig) { options[attrname] = serviceConfig[attrname]; } + + //Render Template + placeholder.innerHTML = template(options); + + //Over-ride Default Submit Action with CORS request + placeholder.querySelector('form').addEventListener("submit", function(e) { + e.preventDefault(); + var requestData = serialize(this) + "&key=" + options.key; + makeCorsRequest(serviceConfig.formAction, requestData); + }); + + }; + + function configureService(service) { var serviceConfig = {}; - switch (options.service) { - case 'mailchimp': + switch (service) { + case 'universe': serviceConfig = { - formAction: 'http://mailchimp-api.com/route', - formMethod: 'POST', - emailName: 'EMAIL' - }; - break; - case 'sendgrid': - serviceConfig = { - formAction: 'http://sendgrid-api.com/route', - formMethod: 'POST' + formAction: 'http://staging.services.sparkart.net/api/v1/contacts', + emailName: 'contact[email]' }; break; default: serviceConfig = { - formAction: '', - formMethod: '' + formAction: '' }; break; } - //Merge the serviceConfig object into the template options - for (var attrname in serviceConfig) { options[attrname] = serviceConfig[attrname]; } + return serviceConfig; + } - placeholder.innerHTML = template(options); - }; + function makeCorsRequest(url, data) { + var xhr = createCorsRequest('POST', url); + if (!xhr) { + console.log('CORS not supported'); + return; + } + xhr.onload = function() { + console.log(xhr.responseText); + }; + xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded"); + xhr.send(data); + } + + function createCorsRequest(method, url) { + var xhr = new XMLHttpRequest(); + if ("withCredentials" in xhr) { + xhr.open(method, url, true); + } else if (typeof XDomainRequest != "undefined") { + xhr = new XDomainRequest(); + xhr.open(method, url); + } else { + xhr = null; + } + return xhr; + } return this.SubscribeEmail; })); \ No newline at end of file diff --git a/src/templates/BEM-with-messaging.hbs b/src/templates/BEM-with-messaging.hbs index 752cc21..b2e6eec 100644 --- a/src/templates/BEM-with-messaging.hbs +++ b/src/templates/BEM-with-messaging.hbs @@ -1,4 +1,5 @@ -
- + + -
\ No newline at end of file + +
\ No newline at end of file From 5376e6f18c0ab95ac6c160409cf3dc88031e5000 Mon Sep 17 00:00:00 2001 From: Josiah Sprague Date: Tue, 9 Sep 2014 13:20:14 -0400 Subject: [PATCH 04/60] [#MmLRZ2E2] Add Messaging and Events Add an event container to template HTML and add 'subscribe-email-message' event Branch: MmLRZ2E2-development --- src/subscribe-email.js | 25 +++++++++++++++++++------ src/templates/BEM-with-messaging.hbs | 4 ++-- 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/src/subscribe-email.js b/src/subscribe-email.js index b5f6e16..51cd19a 100644 --- a/src/subscribe-email.js +++ b/src/subscribe-email.js @@ -22,13 +22,20 @@ //Render Template placeholder.innerHTML = template(options); + var messageHolder = document.querySelector('#subscribe-email-message'); + //Over-ride Default Submit Action with CORS request - placeholder.querySelector('form').addEventListener("submit", function(e) { + placeholder.querySelector('form').addEventListener('submit', function(e) { e.preventDefault(); - var requestData = serialize(this) + "&key=" + options.key; + var requestData = serialize(this) + '&key=' + options.key; + console.log(requestData); makeCorsRequest(serviceConfig.formAction, requestData); }); + document.addEventListener('subscribe-email-message', function (e) { + messageHolder.innerHTML = e.detail; + }); + }; function configureService(service) { @@ -56,17 +63,23 @@ return; } xhr.onload = function() { - console.log(xhr.responseText); + var response = JSON.parse(xhr.responseText); + + response.messages.forEach(function(message){ + var msgEvent = new CustomEvent('subscribe-email-message', { 'detail': message }); + document.dispatchEvent(msgEvent); + }); + }; - xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded"); + xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); xhr.send(data); } function createCorsRequest(method, url) { var xhr = new XMLHttpRequest(); - if ("withCredentials" in xhr) { + if ('withCredentials' in xhr) { xhr.open(method, url, true); - } else if (typeof XDomainRequest != "undefined") { + } else if (typeof XDomainRequest != 'undefined') { xhr = new XDomainRequest(); xhr.open(method, url); } else { diff --git a/src/templates/BEM-with-messaging.hbs b/src/templates/BEM-with-messaging.hbs index b2e6eec..c08d4a1 100644 --- a/src/templates/BEM-with-messaging.hbs +++ b/src/templates/BEM-with-messaging.hbs @@ -1,5 +1,5 @@
-
-
\ No newline at end of file +

+ \ No newline at end of file From e8b4c403269707b04a02b3dbac91614889160774 Mon Sep 17 00:00:00 2001 From: Josiah Sprague Date: Wed, 10 Sep 2014 13:57:40 -0400 Subject: [PATCH 05/60] [#MmLRZ2E2] Add option to override the template setting template to false will now override the template with the markup from the page. Branch: no-template Branch: MmLRZ2E2-development --- demo/index.html | 8 +++++-- src/subscribe-email.js | 36 +++++++++++++++------------- src/templates/BEM-with-messaging.hbs | 8 +++---- 3 files changed, 28 insertions(+), 24 deletions(-) diff --git a/demo/index.html b/demo/index.html index f9b1fe9..939fac4 100644 --- a/demo/index.html +++ b/demo/index.html @@ -5,11 +5,15 @@

Here's a Universe subscribe form!

-
No-script fallback can go here.
+
+ + +

+
+ + + +

Here's a SendGrid subscribe form!

+
+ + + + \ No newline at end of file diff --git a/src/subscribe-email.js b/src/subscribe-email.js index 9f866aa..3f8f379 100644 --- a/src/subscribe-email.js +++ b/src/subscribe-email.js @@ -29,7 +29,7 @@ //Override Default Submit Action with CORS request theForm.addEventListener('submit', function(e) { e.preventDefault(); - var requestData = serialize(this) + '&key=' + options.key; + var requestData = prepareData(this, options); makeCorsRequest(serviceConfig.formAction, requestData, theForm); }); @@ -40,6 +40,22 @@ }; + function prepareData(data, options) { + var requestData = ''; + switch (options.service) { + case 'universe': + requestData = serialize(data) + '&key=' + options.key; + break; + case 'sendgrid': + requestData = serialize(data) + + '&p=' + encodeURIComponent(options.key) + + '&r=' + window.location; + break; + } + //requestData = encodeURIComponent(requestData); + return requestData; + } + function configureService(service) { var serviceConfig = {}; switch (service) { @@ -49,6 +65,12 @@ emailName: 'contact[email]' }; break; + case 'sendgrid': + serviceConfig = { + formAction: 'https://sendgrid.com/newsletter/addRecipientFromWidget', + emailName: 'SG_widget[email]' + }; + break; default: serviceConfig = { formAction: '' @@ -67,10 +89,15 @@ xhr.onload = function() { var response = JSON.parse(xhr.responseText); - response.messages.forEach(function(message){ - var msgEvent = new CustomEvent('message', { 'detail': message }); + if (response.message) { + var msgEvent = new CustomEvent('message', { 'detail': response.message }); form.dispatchEvent(msgEvent); - }); + } else if (response.messages) { + response.messages.forEach(function(message){ + var msgEvent = new CustomEvent('message', { 'detail': message }); + form.dispatchEvent(msgEvent); + }); + } }; xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); From 84e90a5725d97c551f8e91aa2b5a4dd2e372e1d5 Mon Sep 17 00:00:00 2001 From: Josiah Sprague Date: Fri, 12 Sep 2014 09:53:09 -0400 Subject: [PATCH 07/60] [#MmLRZ2E2] refactor as node module Branch: MmLRZ2E2-development --- demo/bundle.js | 19 ++++++++++++++++++ demo/index.html | 21 +------------------- gulp/browserify.js | 6 ++---- src/subscribe-email.js | 45 +++++++++++++++++------------------------- 4 files changed, 40 insertions(+), 51 deletions(-) create mode 100644 demo/bundle.js diff --git a/demo/bundle.js b/demo/bundle.js new file mode 100644 index 0000000..8989f85 --- /dev/null +++ b/demo/bundle.js @@ -0,0 +1,19 @@ +var SubscribeEmail = require('../src/subscribe-email.js'); + +document.addEventListener('DOMContentLoaded', function(){ + + SubscribeEmail.init({ + element: '#subscribe-form', + template: false, + service: 'universe', + key: 'd54e8487-e44e-4c6f-bdd7-6ab9c2eae1e9' + }); + + SubscribeEmail.init({ + element: '#subscribe-form2', + template: true, + service: 'sendgrid', + key: 'SDA+fsU1Qw6S6JIXfgrPngHjsFrn2z8v7VWCgt+a0ln11bNnVF1tvSwDWEK/pRiO' + }); + +}); \ No newline at end of file diff --git a/demo/index.html b/demo/index.html index 0c85bc4..ea1376f 100644 --- a/demo/index.html +++ b/demo/index.html @@ -4,35 +4,16 @@ +

Here's a Universe subscribe form!

- - -

Here's a SendGrid subscribe form!

- - - \ No newline at end of file diff --git a/gulp/browserify.js b/gulp/browserify.js index 7f82b4d..35bc1f2 100644 --- a/gulp/browserify.js +++ b/gulp/browserify.js @@ -9,11 +9,9 @@ gulp.task('browserify', function() { // Required watchify args cache: {}, packageCache: {}, fullPaths: true, // Specify the entry point of your app - entries: ['./src/subscribe-email.js'], + entries: ['./demo/bundle.js'], // Add file extentions to make optional in your requires - extensions: ['.hbs'], - // Enable source maps! - debug: true + extensions: ['.hbs'] }); var bundle = function() { diff --git a/src/subscribe-email.js b/src/subscribe-email.js index 3f8f379..78a992b 100644 --- a/src/subscribe-email.js +++ b/src/subscribe-email.js @@ -1,20 +1,13 @@ -// This JavaScript module is exported as UMD following the pattern which can be -// found here: https://github.com/umdjs/umd/blob/master/returnExports.js +var template = require('./templates/BEM-with-messaging.hbs'); +var serialize = require('form-serialize'); -(function (root, factory) { - if (typeof define === 'function' && define.amd) { - define(['handlebars'], factory); - } else if (typeof exports === 'object') { - module.exports = factory(require('./templates/BEM-with-messaging.hbs'), require('form-serialize')); - } else { - root.SubscribeEmail = factory(root.handlebars); - } -}(this, function (template, serialize) { +module.exports = { - this.SubscribeEmail = function(options){ + init: function(options) { var theForm = document.querySelector(options.element); + var serviceConfig = this.configureService(options.service); + var instance = this; - var serviceConfig = configureService(options.service); //Merge the serviceConfig object into the options for (var attrname in serviceConfig) { options[attrname] = serviceConfig[attrname]; @@ -29,8 +22,8 @@ //Override Default Submit Action with CORS request theForm.addEventListener('submit', function(e) { e.preventDefault(); - var requestData = prepareData(this, options); - makeCorsRequest(serviceConfig.formAction, requestData, theForm); + var requestData = instance.prepareData(this, options); + instance.makeCorsRequest(serviceConfig.formAction, requestData, theForm); }); //Listen for Message Events triggered on the form. @@ -38,9 +31,9 @@ messageHolder.innerHTML = e.detail; }); - }; + }, - function prepareData(data, options) { + prepareData: function(data, options) { var requestData = ''; switch (options.service) { case 'universe': @@ -52,11 +45,10 @@ '&r=' + window.location; break; } - //requestData = encodeURIComponent(requestData); return requestData; - } + }, - function configureService(service) { + configureService: function(service) { var serviceConfig = {}; switch (service) { case 'universe': @@ -78,10 +70,10 @@ break; } return serviceConfig; - } + }, - function makeCorsRequest(url, data, form) { - var xhr = createCorsRequest('POST', url); + makeCorsRequest: function(url, data, form) { + var xhr = this.createCorsRequest('POST', url); if (!xhr) { console.log('CORS not supported'); return; @@ -102,9 +94,9 @@ }; xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); xhr.send(data); - } + }, - function createCorsRequest(method, url) { + createCorsRequest: function(method, url) { var xhr = new XMLHttpRequest(); if ('withCredentials' in xhr) { xhr.open(method, url, true); @@ -117,5 +109,4 @@ return xhr; } - return this.SubscribeEmail; -})); \ No newline at end of file +}; \ No newline at end of file From 4bc3e0f61aebc4d52f1f34621aedd0aa8502b203 Mon Sep 17 00:00:00 2001 From: Josiah Sprague Date: Fri, 12 Sep 2014 13:29:58 -0400 Subject: [PATCH 08/60] [#MmLRZ2E2] package as UMD MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit I think this works 🙈 Branch: MmLRZ2E2-development --- .gitignore | 3 +- build/subscribe-email.js | 787 +++++++++++++++++++++++++++++++++++++++ demo/bundle.js | 19 - demo/index.html | 20 +- gulp/browserify.js | 16 +- package.json | 2 +- 6 files changed, 812 insertions(+), 35 deletions(-) create mode 100755 build/subscribe-email.js delete mode 100644 demo/bundle.js diff --git a/.gitignore b/.gitignore index b7dab5e..ad056cf 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ node_modules -build \ No newline at end of file +build/* +!build/subscribe-email.js \ No newline at end of file diff --git a/build/subscribe-email.js b/build/subscribe-email.js new file mode 100755 index 0000000..96055db --- /dev/null +++ b/build/subscribe-email.js @@ -0,0 +1,787 @@ +!function(e){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=e();else if("function"==typeof define&&define.amd)define([],e);else{var f;"undefined"!=typeof window?f=window:"undefined"!=typeof global?f=global:"undefined"!=typeof self&&(f=self),f.SubscribeEmail=e()}}(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o= 1.0.0' +}; +exports.REVISION_CHANGES = REVISION_CHANGES; +var isArray = Utils.isArray, + isFunction = Utils.isFunction, + toString = Utils.toString, + objectType = '[object Object]'; + +function HandlebarsEnvironment(helpers, partials) { + this.helpers = helpers || {}; + this.partials = partials || {}; + + registerDefaultHelpers(this); +} + +exports.HandlebarsEnvironment = HandlebarsEnvironment;HandlebarsEnvironment.prototype = { + constructor: HandlebarsEnvironment, + + logger: logger, + log: log, + + registerHelper: function(name, fn, inverse) { + if (toString.call(name) === objectType) { + if (inverse || fn) { throw new Exception('Arg not supported with multiple helpers'); } + Utils.extend(this.helpers, name); + } else { + if (inverse) { fn.not = inverse; } + this.helpers[name] = fn; + } + }, + + registerPartial: function(name, str) { + if (toString.call(name) === objectType) { + Utils.extend(this.partials, name); + } else { + this.partials[name] = str; + } + } +}; + +function registerDefaultHelpers(instance) { + instance.registerHelper('helperMissing', function(arg) { + if(arguments.length === 2) { + return undefined; + } else { + throw new Exception("Missing helper: '" + arg + "'"); + } + }); + + instance.registerHelper('blockHelperMissing', function(context, options) { + var inverse = options.inverse || function() {}, fn = options.fn; + + if (isFunction(context)) { context = context.call(this); } + + if(context === true) { + return fn(this); + } else if(context === false || context == null) { + return inverse(this); + } else if (isArray(context)) { + if(context.length > 0) { + return instance.helpers.each(context, options); + } else { + return inverse(this); + } + } else { + return fn(context); + } + }); + + instance.registerHelper('each', function(context, options) { + var fn = options.fn, inverse = options.inverse; + var i = 0, ret = "", data; + + if (isFunction(context)) { context = context.call(this); } + + if (options.data) { + data = createFrame(options.data); + } + + if(context && typeof context === 'object') { + if (isArray(context)) { + for(var j = context.length; i": ">", + '"': """, + "'": "'", + "`": "`" +}; + +var badChars = /[&<>"'`]/g; +var possible = /[&<>"'`]/; + +function escapeChar(chr) { + return escape[chr] || "&"; +} + +function extend(obj, value) { + for(var key in value) { + if(Object.prototype.hasOwnProperty.call(value, key)) { + obj[key] = value[key]; + } + } +} + +exports.extend = extend;var toString = Object.prototype.toString; +exports.toString = toString; +// Sourced from lodash +// https://github.com/bestiejs/lodash/blob/master/LICENSE.txt +var isFunction = function(value) { + return typeof value === 'function'; +}; +// fallback for older versions of Chrome and Safari +if (isFunction(/x/)) { + isFunction = function(value) { + return typeof value === 'function' && toString.call(value) === '[object Function]'; + }; +} +var isFunction; +exports.isFunction = isFunction; +var isArray = Array.isArray || function(value) { + return (value && typeof value === 'object') ? toString.call(value) === '[object Array]' : false; +}; +exports.isArray = isArray; + +function escapeExpression(string) { + // don't escape SafeStrings, since they're already safe + if (string instanceof SafeString) { + return string.toString(); + } else if (!string && string !== 0) { + return ""; + } + + // Force a string conversion as this will be done by the append regardless and + // the regex test will do this transparently behind the scenes, causing issues if + // an object's to string has escaped characters in it. + string = "" + string; + + if(!possible.test(string)) { return string; } + return string.replace(badChars, escapeChar); +} + +exports.escapeExpression = escapeExpression;function isEmpty(value) { + if (!value && value !== 0) { + return true; + } else if (isArray(value) && value.length === 0) { + return true; + } else { + return false; + } +} + +exports.isEmpty = isEmpty; +},{"./safe-string":7}],9:[function(require,module,exports){ +// Create a simple path alias to allow browserify to resolve +// the runtime on a supported path. +module.exports = require('./dist/cjs/handlebars.runtime'); + +},{"./dist/cjs/handlebars.runtime":3}],10:[function(require,module,exports){ +module.exports = require("handlebars/runtime")["default"]; + +},{"handlebars/runtime":9}],11:[function(require,module,exports){ +// hbsfy compiled Handlebars template +var HandlebarsCompiler = require('hbsfy/runtime'); +module.exports = HandlebarsCompiler.template(function (Handlebars,depth0,helpers,partials,data) { + this.compilerInfo = [4,'>= 1.0.0']; +helpers = this.merge(helpers, Handlebars.helpers); data = data || {}; + var buffer = "", stack1, helper, functionType="function", escapeExpression=this.escapeExpression; + + + buffer += "\n\n

"; + return buffer; + }); + +},{"hbsfy/runtime":10}]},{},[1])(1) +}); \ No newline at end of file diff --git a/demo/bundle.js b/demo/bundle.js deleted file mode 100644 index 8989f85..0000000 --- a/demo/bundle.js +++ /dev/null @@ -1,19 +0,0 @@ -var SubscribeEmail = require('../src/subscribe-email.js'); - -document.addEventListener('DOMContentLoaded', function(){ - - SubscribeEmail.init({ - element: '#subscribe-form', - template: false, - service: 'universe', - key: 'd54e8487-e44e-4c6f-bdd7-6ab9c2eae1e9' - }); - - SubscribeEmail.init({ - element: '#subscribe-form2', - template: true, - service: 'sendgrid', - key: 'SDA+fsU1Qw6S6JIXfgrPngHjsFrn2z8v7VWCgt+a0ln11bNnVF1tvSwDWEK/pRiO' - }); - -}); \ No newline at end of file diff --git a/demo/index.html b/demo/index.html index ea1376f..ed70eec 100644 --- a/demo/index.html +++ b/demo/index.html @@ -4,7 +4,7 @@ - +

Here's a Universe subscribe form!

@@ -12,8 +12,26 @@

+ +

Here's a SendGrid subscribe form!

+ + \ No newline at end of file diff --git a/gulp/browserify.js b/gulp/browserify.js index 35bc1f2..1427697 100644 --- a/gulp/browserify.js +++ b/gulp/browserify.js @@ -6,29 +6,19 @@ var source = require('vinyl-source-stream'); gulp.task('browserify', function() { var bundler = browserify({ - // Required watchify args - cache: {}, packageCache: {}, fullPaths: true, - // Specify the entry point of your app - entries: ['./demo/bundle.js'], - // Add file extentions to make optional in your requires - extensions: ['.hbs'] + entries: ['./src/subscribe-email.js'], + extensions: ['.hbs'], + standalone: 'SubscribeEmail' }); var bundle = function() { - // Log when bundling starts bundleLogger.start(); return bundler .bundle() - // Report compile errors .on('error', handleErrors) - // Use vinyl-source-stream to make the - // stream gulp compatible. Specifiy the - // desired output filename here. .pipe(source('subscribe-email.js')) - // Specify the output destination .pipe(gulp.dest('./build/')) - // Log when bundling completes! .on('end', bundleLogger.end); }; diff --git a/package.json b/package.json index f51b1bd..a3b3482 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "subscribe-email", "version": "0.0.0", "private": true, - "main": "src/subscribe-email.js", + "main": "build/subscribe-email.js", "browserify": { "transform": [ "hbsfy" From 6cb8dfe80a2d71871a438bbdba7ab9a7d638afb1 Mon Sep 17 00:00:00 2001 From: Josiah Sprague Date: Fri, 12 Sep 2014 16:47:59 -0400 Subject: [PATCH 09/60] [#MmLRZ2E2] add derequire so browserified module can be browserified without errors Branch: MmLRZ2E2-development --- build/subscribe-email.js | 56 ++++++++++++++++++++-------------------- gulp/browserify.js | 2 ++ package.json | 1 + 3 files changed, 31 insertions(+), 28 deletions(-) diff --git a/build/subscribe-email.js b/build/subscribe-email.js index 96055db..3f9dc6d 100755 --- a/build/subscribe-email.js +++ b/build/subscribe-email.js @@ -1,6 +1,6 @@ -!function(e){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=e();else if("function"==typeof define&&define.amd)define([],e);else{var f;"undefined"!=typeof window?f=window:"undefined"!=typeof global?f=global:"undefined"!=typeof self&&(f=self),f.SubscribeEmail=e()}}(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o= 1.0.0']; helpers = this.merge(helpers, Handlebars.helpers); data = data || {}; diff --git a/gulp/browserify.js b/gulp/browserify.js index 1427697..372b715 100644 --- a/gulp/browserify.js +++ b/gulp/browserify.js @@ -3,6 +3,7 @@ var bundleLogger = require('./util/bundleLogger'); var gulp = require('gulp'); var handleErrors = require('./util/handleErrors'); var source = require('vinyl-source-stream'); +var derequire = require('gulp-derequire'); gulp.task('browserify', function() { var bundler = browserify({ @@ -18,6 +19,7 @@ gulp.task('browserify', function() { .bundle() .on('error', handleErrors) .pipe(source('subscribe-email.js')) + .pipe(derequire()) .pipe(gulp.dest('./build/')) .on('end', bundleLogger.end); }; diff --git a/package.json b/package.json index a3b3482..9003118 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "ecstatic": "^0.5.4", "form-serialize": "^0.3.0", "gulp": "^3.8.7", + "gulp-derequire": "^1.1.0", "gulp-notify": "^1.5.1", "gulp-util": "^3.0.1", "handlebars": "1.3.x", From 23d151ee438714539efe0b00d228e544bcf6e759 Mon Sep 17 00:00:00 2001 From: Josiah Sprague Date: Fri, 12 Sep 2014 18:02:53 -0400 Subject: [PATCH 10/60] [#MmLRZ2E2] move handlebars to dependencies instead of dev Branch: MmLRZ2E2-development --- package.json | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 9003118..e083ef3 100644 --- a/package.json +++ b/package.json @@ -16,10 +16,12 @@ "gulp-derequire": "^1.1.0", "gulp-notify": "^1.5.1", "gulp-util": "^3.0.1", - "handlebars": "1.3.x", - "hbsfy": "^2.1.0", "pretty-hrtime": "^0.2.1", "require-dir": "^0.1.0", "vinyl-source-stream": "^0.1.1" + }, + "dependencies": { + "handlebars": "1.3.x", + "hbsfy": "^2.1.0" } } From 8cc6ab621b847a3ce425867b12b80611aa8c0821 Mon Sep 17 00:00:00 2001 From: Josiah Sprague Date: Mon, 15 Sep 2014 11:10:26 -0400 Subject: [PATCH 11/60] [#MmLRZ2E2] add default options and make template BEM Branch: MmLRZ2E2-development --- build/subscribe-email.js | 26 ++++++++++++++++++++++---- demo/index.html | 1 + src/subscribe-email.js | 18 ++++++++++++++++-- src/templates/BEM-with-messaging.hbs | 6 +++--- 4 files changed, 42 insertions(+), 9 deletions(-) diff --git a/build/subscribe-email.js b/build/subscribe-email.js index 3f9dc6d..2693af4 100755 --- a/build/subscribe-email.js +++ b/build/subscribe-email.js @@ -5,6 +5,7 @@ var serialize = _dereq_('form-serialize'); module.exports = { init: function(options) { + options = this.setDefaults(options); var theForm = document.querySelector(options.element); var serviceConfig = this.configureService(options.service); var instance = this; @@ -17,8 +18,11 @@ module.exports = { if (options.template) { //Render the Default Template theForm.innerHTML = template(options); + //Add BEM Namespace Class to Form + theForm.classList.add('subscribe-email'); } - var messageHolder = theForm.querySelector('.message'); + + var messageHolder = theForm.querySelector(options.responseElement); //Override Default Submit Action with CORS request theForm.addEventListener('submit', function(e) { @@ -29,9 +33,19 @@ module.exports = { //Listen for Message Events triggered on the form. theForm.addEventListener('message', function (e) { - messageHolder.innerHTML = e.detail; + if (messageHolder) { + messageHolder.innerHTML = e.detail; + } else { + console.log(e.detail); + } }); + }, + setDefaults: function(options) { + options.template = options.template || false; + options.submitText = options.submitText || 'Subscribe'; + options.responseElement = options.responseElement || '.subscribe-email__response'; + return options; }, prepareData: function(data, options) { @@ -775,11 +789,15 @@ helpers = this.merge(helpers, Handlebars.helpers); data = data || {}; var buffer = "", stack1, helper, functionType="function", escapeExpression=this.escapeExpression; - buffer += "\n\n

"; + + "\" required>\n\n

"; return buffer; }); diff --git a/demo/index.html b/demo/index.html index ed70eec..0c94e40 100644 --- a/demo/index.html +++ b/demo/index.html @@ -16,6 +16,7 @@ SubscribeEmail.init({ element: '#subscribe-form', template: false, + responseElement: '.message', service: 'universe', key: 'd54e8487-e44e-4c6f-bdd7-6ab9c2eae1e9' }); diff --git a/src/subscribe-email.js b/src/subscribe-email.js index 78a992b..dad4bfd 100644 --- a/src/subscribe-email.js +++ b/src/subscribe-email.js @@ -4,6 +4,7 @@ var serialize = require('form-serialize'); module.exports = { init: function(options) { + options = this.setDefaults(options); var theForm = document.querySelector(options.element); var serviceConfig = this.configureService(options.service); var instance = this; @@ -16,8 +17,11 @@ module.exports = { if (options.template) { //Render the Default Template theForm.innerHTML = template(options); + //Add BEM Namespace Class to Form + theForm.classList.add('subscribe-email'); } - var messageHolder = theForm.querySelector('.message'); + + var messageHolder = theForm.querySelector(options.responseElement); //Override Default Submit Action with CORS request theForm.addEventListener('submit', function(e) { @@ -28,9 +32,19 @@ module.exports = { //Listen for Message Events triggered on the form. theForm.addEventListener('message', function (e) { - messageHolder.innerHTML = e.detail; + if (messageHolder) { + messageHolder.innerHTML = e.detail; + } else { + console.log(e.detail); + } }); + }, + setDefaults: function(options) { + options.template = options.template || false; + options.submitText = options.submitText || 'Subscribe'; + options.responseElement = options.responseElement || '.subscribe-email__response'; + return options; }, prepareData: function(data, options) { diff --git a/src/templates/BEM-with-messaging.hbs b/src/templates/BEM-with-messaging.hbs index 96fece4..b1a5364 100644 --- a/src/templates/BEM-with-messaging.hbs +++ b/src/templates/BEM-with-messaging.hbs @@ -1,3 +1,3 @@ - - -

\ No newline at end of file + + + \ No newline at end of file From 897ae333331ef208c4ee2e1db01938b4e4b04f61 Mon Sep 17 00:00:00 2001 From: Josiah Sprague Date: Mon, 15 Sep 2014 11:31:58 -0400 Subject: [PATCH 12/60] [#MmLRZ2E2] include instanceof check include a this instanceof SubscribeEmail check to let people consume the module with new SubscribeEmail or SubscribeEmail() Branch: MmLRZ2E2-development --- build/subscribe-email.js | 216 +++++++++++++++++++-------------------- demo/index.html | 4 +- src/subscribe-email.js | 214 +++++++++++++++++++------------------- 3 files changed, 215 insertions(+), 219 deletions(-) diff --git a/build/subscribe-email.js b/build/subscribe-email.js index 2693af4..dcce776 100755 --- a/build/subscribe-email.js +++ b/build/subscribe-email.js @@ -2,129 +2,127 @@ var template = _dereq_('./templates/BEM-with-messaging.hbs'); var serialize = _dereq_('form-serialize'); -module.exports = { +module.exports = SubscribeEmail; - init: function(options) { - options = this.setDefaults(options); - var theForm = document.querySelector(options.element); - var serviceConfig = this.configureService(options.service); - var instance = this; +function SubscribeEmail (options) { + if (!(this instanceof SubscribeEmail)) return new SubscribeEmail(options); + options = setDefaults(options); + var theForm = document.querySelector(options.element); + var serviceConfig = configureService(options.service); - //Merge the serviceConfig object into the options - for (var attrname in serviceConfig) { - options[attrname] = serviceConfig[attrname]; - } - - if (options.template) { - //Render the Default Template - theForm.innerHTML = template(options); - //Add BEM Namespace Class to Form - theForm.classList.add('subscribe-email'); - } - - var messageHolder = theForm.querySelector(options.responseElement); + //Merge the serviceConfig object into the options + for (var attrname in serviceConfig) { + options[attrname] = serviceConfig[attrname]; + } - //Override Default Submit Action with CORS request - theForm.addEventListener('submit', function(e) { - e.preventDefault(); - var requestData = instance.prepareData(this, options); - instance.makeCorsRequest(serviceConfig.formAction, requestData, theForm); - }); + if (options.template) { + //Render the Default Template + theForm.innerHTML = template(options); + //Add BEM Namespace Class to Form + theForm.classList.add('subscribe-email'); + } - //Listen for Message Events triggered on the form. - theForm.addEventListener('message', function (e) { - if (messageHolder) { - messageHolder.innerHTML = e.detail; - } else { - console.log(e.detail); - } - }); - }, + var messageHolder = theForm.querySelector(options.responseElement); - setDefaults: function(options) { - options.template = options.template || false; - options.submitText = options.submitText || 'Subscribe'; - options.responseElement = options.responseElement || '.subscribe-email__response'; - return options; - }, + //Override Default Submit Action with CORS request + theForm.addEventListener('submit', function(e) { + e.preventDefault(); + var requestData = prepareData(this, options); + makeCorsRequest(serviceConfig.formAction, requestData, theForm); + }); - prepareData: function(data, options) { - var requestData = ''; - switch (options.service) { - case 'universe': - requestData = serialize(data) + '&key=' + options.key; - break; - case 'sendgrid': - requestData = serialize(data) + - '&p=' + encodeURIComponent(options.key) + - '&r=' + window.location; - break; + //Listen for Message Events triggered on the form. + theForm.addEventListener('message', function (e) { + if (messageHolder) { + messageHolder.innerHTML = e.detail; + } else { + console.log(e.detail); } - return requestData; - }, + }); +} - configureService: function(service) { - var serviceConfig = {}; - switch (service) { - case 'universe': - serviceConfig = { - formAction: 'http://staging.services.sparkart.net/api/v1/contacts', - emailName: 'contact[email]' - }; - break; - case 'sendgrid': - serviceConfig = { - formAction: 'https://sendgrid.com/newsletter/addRecipientFromWidget', - emailName: 'SG_widget[email]' - }; - break; - default: - serviceConfig = { - formAction: '' - }; - break; - } - return serviceConfig; - }, +function setDefaults(options) { + options.template = options.template || false; + options.submitText = options.submitText || 'Subscribe'; + options.responseElement = options.responseElement || '.subscribe-email__response'; + return options; +} - makeCorsRequest: function(url, data, form) { - var xhr = this.createCorsRequest('POST', url); - if (!xhr) { - console.log('CORS not supported'); - return; - } - xhr.onload = function() { - var response = JSON.parse(xhr.responseText); +function prepareData(data, options) { + var requestData = ''; + switch (options.service) { + case 'universe': + requestData = serialize(data) + '&key=' + options.key; + break; + case 'sendgrid': + requestData = serialize(data) + + '&p=' + encodeURIComponent(options.key) + + '&r=' + window.location; + break; + } + return requestData; +} - if (response.message) { - var msgEvent = new CustomEvent('message', { 'detail': response.message }); +function configureService(service) { + var serviceConfig = {}; + switch (service) { + case 'universe': + serviceConfig = { + formAction: 'http://staging.services.sparkart.net/api/v1/contacts', + emailName: 'contact[email]' + }; + break; + case 'sendgrid': + serviceConfig = { + formAction: 'https://sendgrid.com/newsletter/addRecipientFromWidget', + emailName: 'SG_widget[email]' + }; + break; + default: + serviceConfig = { + formAction: '' + }; + break; + } + return serviceConfig; +} + +function makeCorsRequest(url, data, form) { + var xhr = createCorsRequest('POST', url); + if (!xhr) { + console.log('CORS not supported'); + return; + } + xhr.onload = function() { + var response = JSON.parse(xhr.responseText); + + if (response.message) { + var msgEvent = new CustomEvent('message', { 'detail': response.message }); + form.dispatchEvent(msgEvent); + } else if (response.messages) { + response.messages.forEach(function(message){ + var msgEvent = new CustomEvent('message', { 'detail': message }); form.dispatchEvent(msgEvent); - } else if (response.messages) { - response.messages.forEach(function(message){ - var msgEvent = new CustomEvent('message', { 'detail': message }); - form.dispatchEvent(msgEvent); - }); - } + }); + } - }; - xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); - xhr.send(data); - }, + }; + xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); + xhr.send(data); +} - createCorsRequest: function(method, url) { - var xhr = new XMLHttpRequest(); - if ('withCredentials' in xhr) { - xhr.open(method, url, true); - } else if (typeof XDomainRequest != 'undefined') { - xhr = new XDomainRequest(); - xhr.open(method, url); - } else { - xhr = null; - } - return xhr; +function createCorsRequest(method, url) { + var xhr = new XMLHttpRequest(); + if ('withCredentials' in xhr) { + xhr.open(method, url, true); + } else if (typeof XDomainRequest != 'undefined') { + xhr = new XDomainRequest(); + xhr.open(method, url); + } else { + xhr = null; } - -}; + return xhr; +} },{"./templates/BEM-with-messaging.hbs":11,"form-serialize":2}],2:[function(_dereq_,module,exports){ // get successful control from form and assemble into object // http://www.w3.org/TR/html401/interact/forms.html#h-17.13.2 diff --git a/demo/index.html b/demo/index.html index 0c94e40..ad64793 100644 --- a/demo/index.html +++ b/demo/index.html @@ -13,7 +13,7 @@ ``` -3) Profit? +At a minimum, you'll need to change the `service` and `key` parameters to match your needs. # Advanced Usage ## Options The module can be configured with several optional parameters passed to it's constructor. Here is the full list of options: -- `element`: **(Required)** A DOM element, jQuery element, or selector string to refer to the placeholder element. -- `service`: **(Required)** The mailing list platform you are using. Available options are `mailchimp`, `sendgrid` and `universe`. -- `key`: **(Required)** A string of the API key for your mailing list platform. -- `submitText`: A string to be used on the form's submit button. Defaults to "Subscribe". -- `overrideTemplate`: Set this to true to override the markup that is automatically generated by the plugin. See "Customizing the Template" below (defaults to `false`). -- `responseElement`: A selector string for the element you want to display response (validation, errors, confirmation) messages from your platform's API (defaults to `'.subscribe-email__response'`). -- `async`: Whether the form with be submitted asynchronously (defaults to `true`). +### `element` +**(Required)** A DOM element, jQuery element, or selector string to refer to the placeholder element. + +### `service` +**(Required)** The mailing list platform you are using. Available options are `mailchimp`, `sendgrid` and `universe`. + +### `key` +**(Required)** A string of the API key for your mailing list platform. + +### `submitText` +A string to be used on the form's submit button (defaults to "Subscribe"). + +### `overrideTemplate` +Set this to true to override the markup that is automatically generated by the plugin. See "Customizing the Template" below (defaults to `false`). + +### `responseElement` +A selector string for the element you want to display response (validation, errors, confirmation) messages from your platform's API (defaults to `'.subscribe-email__response'`). + +### `async` +Whether the form with be submitted asynchronously (defaults to `true`). ## Customizing the Template Out of the box, the Subscribe Email module will generate BEM markup with the namespace `subscribe-email` that contains all of the elements your form needs. If you want to customize the markup, set `overrideTemplate: true` when you initialize the module to use the markup from the target element instead. This gives you full control over the markup, but you'll have to make sure that your form contains all of the required fields. diff --git a/build/subscribe-email.js b/build/subscribe-email.js index d1509c7..259ca21 100755 --- a/build/subscribe-email.js +++ b/build/subscribe-email.js @@ -1,5 +1,5 @@ !function(e){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=e();else if("function"==typeof define&&define.amd)define([],e);else{var f;"undefined"!=typeof window?f=window:"undefined"!=typeof global?f=global:"undefined"!=typeof self&&(f=self),f.SubscribeEmail=e()}}(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o Date: Wed, 17 Sep 2014 14:01:45 -0400 Subject: [PATCH 16/60] [#MmLRZ2E2] Add tests Add a basic test with Tape and Testling Branch: MmLRZ2E2-development --- README.md | 3 +++ package.json | 17 +++++++++++++++++ {demo => tests}/index.html | 0 tests/tests.js | 23 +++++++++++++++++++++++ 4 files changed, 43 insertions(+) rename {demo => tests}/index.html (100%) create mode 100644 tests/tests.js diff --git a/README.md b/README.md index 4090d55..11fb1dc 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,9 @@ Subscribe Email is a UMD JavaScript module for rendering a mailing list sign up It allows developers to quickly include an email collection form on a page without being concerned with the implementation details of a specific mailing list platform. We're currently aiming to support mailing lists on SendGrid, MailChimp and Universe. +[![browser support](https://ci.testling.com/blocks/subscribe-email.png) +](https://ci.testling.com/blocks/subscribe-email) + # Getting the Module You can get the module in any one of the following ways; - Download [the latest release](https://github.com/blocks/subscribe-email/releases) from GitHub diff --git a/package.json b/package.json index 1dd01d6..2a8595b 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,23 @@ "hbsfy": "^2.1.0", "pretty-hrtime": "^0.2.1", "require-dir": "^0.1.0", + "tape": "^3.0.0", "vinyl-source-stream": "^0.1.1" + }, + "testling": { + "files": "tests/*.js", + "browsers": [ + "ie/6..latest", + "chrome/22..latest", + "firefox/16..latest", + "safari/latest", + "opera/11.0..latest", + "iphone/6", + "ipad/6", + "android-browser/latest" + ] + }, + "scripts": { + "test": "tape tests/*.js" } } diff --git a/demo/index.html b/tests/index.html similarity index 100% rename from demo/index.html rename to tests/index.html diff --git a/tests/tests.js b/tests/tests.js new file mode 100644 index 0000000..2a12ef5 --- /dev/null +++ b/tests/tests.js @@ -0,0 +1,23 @@ +var SubscribeEmail = require('../build/subscribe-email.js'); +var test = require('tape'); + +document.addEventListener('DOMContentLoaded', function(){ + + var theElement = document.createElement('form'); + document.body.appendChild(theElement); + theElement.id = 'subscribe'; + + new SubscribeEmail({ + element: '#subscribe', + service: 'universe', + template: true, + key: '2366edcf-805b-43bf-b043-9c2f527967d9' + }); + +}); + +test('SubscribeEmail exists and is a function', function (t) { + var objType = typeof SubscribeEmail; + t.equal(objType, 'function'); + t.end(); +}); \ No newline at end of file From a9127d304606f36ac1871e3f48bc9bd6c104a1f3 Mon Sep 17 00:00:00 2001 From: Josiah Sprague Date: Wed, 17 Sep 2014 18:22:02 -0400 Subject: [PATCH 17/60] [#MmLRZ2E2] Bind events to object instance Branch: MmLRZ2E2-development --- build/subscribe-email.js | 404 +++++++++++++++++++++++++++---- src/subscribe-email.js | 55 ++--- tests/{index.html => tests.html} | 18 +- 3 files changed, 401 insertions(+), 76 deletions(-) rename tests/{index.html => tests.html} (60%) diff --git a/build/subscribe-email.js b/build/subscribe-email.js index 259ca21..53ee3fd 100755 --- a/build/subscribe-email.js +++ b/build/subscribe-email.js @@ -1,11 +1,15 @@ !function(e){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=e();else if("function"==typeof define&&define.amd)define([],e);else{var f;"undefined"!=typeof window?f=window:"undefined"!=typeof global?f=global:"undefined"!=typeof self&&(f=self),f.SubscribeEmail=e()}}(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o'; - }); - } else { - console.log(e.detail); - } + messageHolder.innerHTML = message; } }); } @@ -96,40 +91,37 @@ function configureService(service) { return serviceConfig; } -function fireEvent(name, detail, el){ - var customEvent; - if (window.CustomEvent) { - customEvent = new CustomEvent(name, {'detail': detail }); - } else { - customEvent = document.createEvent('CustomEvent'); - customEvent.initCustomEvent(name, true, true, detail); - } - el.dispatchEvent(customEvent); -} - -function makeCorsRequest(url, data, form) { +SubscribeEmail.prototype.makeCorsRequest = function (url, data, form) { +//function makeCorsRequest(url, data, form) { + var instance = this; var xhr = createCorsRequest('POST', url); if (!xhr) { console.log('CORS not supported'); return; } + xhr.onload = function() { var response = JSON.parse(xhr.responseText); //Fire Message Event(s) - var payload = response.message || response.messages; - fireEvent('subscriptionMessage', payload, form); + if (response.message) { + instance.emit('subscriptionMessage', response.message); + } else if (response.messages) { + response.messages.forEach(function(message) { + instance.emit('subscriptionMessage', message); + }); + } //Fire Success or Error Event if (response.success || response.status === 'ok') { - fireEvent('subscriptionSuccess', response, form); + instance.emit('subscriptionSuccess', response); } else { - fireEvent('subscriptionError', response, form); + instance.emit('subscriptionError', response); } }; - + xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); xhr.send(data); } @@ -146,7 +138,310 @@ function createCorsRequest(method, url) { } return xhr; } -},{"./template.hbs":11,"form-serialize":2}],2:[function(_dereq_,module,exports){ +},{"./template.hbs":13,"events":2,"form-serialize":3,"inherits":12}],2:[function(_dereq_,module,exports){ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +function EventEmitter() { + this._events = this._events || {}; + this._maxListeners = this._maxListeners || undefined; +} +module.exports = EventEmitter; + +// Backwards-compat with node 0.10.x +EventEmitter.EventEmitter = EventEmitter; + +EventEmitter.prototype._events = undefined; +EventEmitter.prototype._maxListeners = undefined; + +// By default EventEmitters will print a warning if more than 10 listeners are +// added to it. This is a useful default which helps finding memory leaks. +EventEmitter.defaultMaxListeners = 10; + +// Obviously not all Emitters should be limited to 10. This function allows +// that to be increased. Set to zero for unlimited. +EventEmitter.prototype.setMaxListeners = function(n) { + if (!isNumber(n) || n < 0 || isNaN(n)) + throw TypeError('n must be a positive number'); + this._maxListeners = n; + return this; +}; + +EventEmitter.prototype.emit = function(type) { + var er, handler, len, args, i, listeners; + + if (!this._events) + this._events = {}; + + // If there is no 'error' event listener then throw. + if (type === 'error') { + if (!this._events.error || + (isObject(this._events.error) && !this._events.error.length)) { + er = arguments[1]; + if (er instanceof Error) { + throw er; // Unhandled 'error' event + } + throw TypeError('Uncaught, unspecified "error" event.'); + } + } + + handler = this._events[type]; + + if (isUndefined(handler)) + return false; + + if (isFunction(handler)) { + switch (arguments.length) { + // fast cases + case 1: + handler.call(this); + break; + case 2: + handler.call(this, arguments[1]); + break; + case 3: + handler.call(this, arguments[1], arguments[2]); + break; + // slower + default: + len = arguments.length; + args = new Array(len - 1); + for (i = 1; i < len; i++) + args[i - 1] = arguments[i]; + handler.apply(this, args); + } + } else if (isObject(handler)) { + len = arguments.length; + args = new Array(len - 1); + for (i = 1; i < len; i++) + args[i - 1] = arguments[i]; + + listeners = handler.slice(); + len = listeners.length; + for (i = 0; i < len; i++) + listeners[i].apply(this, args); + } + + return true; +}; + +EventEmitter.prototype.addListener = function(type, listener) { + var m; + + if (!isFunction(listener)) + throw TypeError('listener must be a function'); + + if (!this._events) + this._events = {}; + + // To avoid recursion in the case that type === "newListener"! Before + // adding it to the listeners, first emit "newListener". + if (this._events.newListener) + this.emit('newListener', type, + isFunction(listener.listener) ? + listener.listener : listener); + + if (!this._events[type]) + // Optimize the case of one listener. Don't need the extra array object. + this._events[type] = listener; + else if (isObject(this._events[type])) + // If we've already got an array, just append. + this._events[type].push(listener); + else + // Adding the second element, need to change to array. + this._events[type] = [this._events[type], listener]; + + // Check for listener leak + if (isObject(this._events[type]) && !this._events[type].warned) { + var m; + if (!isUndefined(this._maxListeners)) { + m = this._maxListeners; + } else { + m = EventEmitter.defaultMaxListeners; + } + + if (m && m > 0 && this._events[type].length > m) { + this._events[type].warned = true; + console.error('(node) warning: possible EventEmitter memory ' + + 'leak detected. %d listeners added. ' + + 'Use emitter.setMaxListeners() to increase limit.', + this._events[type].length); + if (typeof console.trace === 'function') { + // not supported in IE 10 + console.trace(); + } + } + } + + return this; +}; + +EventEmitter.prototype.on = EventEmitter.prototype.addListener; + +EventEmitter.prototype.once = function(type, listener) { + if (!isFunction(listener)) + throw TypeError('listener must be a function'); + + var fired = false; + + function g() { + this.removeListener(type, g); + + if (!fired) { + fired = true; + listener.apply(this, arguments); + } + } + + g.listener = listener; + this.on(type, g); + + return this; +}; + +// emits a 'removeListener' event iff the listener was removed +EventEmitter.prototype.removeListener = function(type, listener) { + var list, position, length, i; + + if (!isFunction(listener)) + throw TypeError('listener must be a function'); + + if (!this._events || !this._events[type]) + return this; + + list = this._events[type]; + length = list.length; + position = -1; + + if (list === listener || + (isFunction(list.listener) && list.listener === listener)) { + delete this._events[type]; + if (this._events.removeListener) + this.emit('removeListener', type, listener); + + } else if (isObject(list)) { + for (i = length; i-- > 0;) { + if (list[i] === listener || + (list[i].listener && list[i].listener === listener)) { + position = i; + break; + } + } + + if (position < 0) + return this; + + if (list.length === 1) { + list.length = 0; + delete this._events[type]; + } else { + list.splice(position, 1); + } + + if (this._events.removeListener) + this.emit('removeListener', type, listener); + } + + return this; +}; + +EventEmitter.prototype.removeAllListeners = function(type) { + var key, listeners; + + if (!this._events) + return this; + + // not listening for removeListener, no need to emit + if (!this._events.removeListener) { + if (arguments.length === 0) + this._events = {}; + else if (this._events[type]) + delete this._events[type]; + return this; + } + + // emit removeListener for all listeners on all events + if (arguments.length === 0) { + for (key in this._events) { + if (key === 'removeListener') continue; + this.removeAllListeners(key); + } + this.removeAllListeners('removeListener'); + this._events = {}; + return this; + } + + listeners = this._events[type]; + + if (isFunction(listeners)) { + this.removeListener(type, listeners); + } else { + // LIFO order + while (listeners.length) + this.removeListener(type, listeners[listeners.length - 1]); + } + delete this._events[type]; + + return this; +}; + +EventEmitter.prototype.listeners = function(type) { + var ret; + if (!this._events || !this._events[type]) + ret = []; + else if (isFunction(this._events[type])) + ret = [this._events[type]]; + else + ret = this._events[type].slice(); + return ret; +}; + +EventEmitter.listenerCount = function(emitter, type) { + var ret; + if (!emitter._events || !emitter._events[type]) + ret = 0; + else if (isFunction(emitter._events[type])) + ret = 1; + else + ret = emitter._events[type].length; + return ret; +}; + +function isFunction(arg) { + return typeof arg === 'function'; +} + +function isNumber(arg) { + return typeof arg === 'number'; +} + +function isObject(arg) { + return typeof arg === 'object' && arg !== null; +} + +function isUndefined(arg) { + return arg === void 0; +} + +},{}],3:[function(_dereq_,module,exports){ // get successful control from form and assemble into object // http://www.w3.org/TR/html401/interact/forms.html#h-17.13.2 @@ -323,7 +618,7 @@ function extract_from_brackets(result, key, value) { module.exports = serialize; -},{}],3:[function(_dereq_,module,exports){ +},{}],4:[function(_dereq_,module,exports){ "use strict"; /*globals Handlebars: true */ var base = _dereq_("./handlebars/base"); @@ -356,7 +651,7 @@ var Handlebars = create(); Handlebars.create = create; exports["default"] = Handlebars; -},{"./handlebars/base":4,"./handlebars/exception":5,"./handlebars/runtime":6,"./handlebars/safe-string":7,"./handlebars/utils":8}],4:[function(_dereq_,module,exports){ +},{"./handlebars/base":5,"./handlebars/exception":6,"./handlebars/runtime":7,"./handlebars/safe-string":8,"./handlebars/utils":9}],5:[function(_dereq_,module,exports){ "use strict"; var Utils = _dereq_("./utils"); var Exception = _dereq_("./exception")["default"]; @@ -537,7 +832,7 @@ exports.log = log;var createFrame = function(object) { return obj; }; exports.createFrame = createFrame; -},{"./exception":5,"./utils":8}],5:[function(_dereq_,module,exports){ +},{"./exception":6,"./utils":9}],6:[function(_dereq_,module,exports){ "use strict"; var errorProps = ['description', 'fileName', 'lineNumber', 'message', 'name', 'number', 'stack']; @@ -566,7 +861,7 @@ function Exception(message, node) { Exception.prototype = new Error(); exports["default"] = Exception; -},{}],6:[function(_dereq_,module,exports){ +},{}],7:[function(_dereq_,module,exports){ "use strict"; var Utils = _dereq_("./utils"); var Exception = _dereq_("./exception")["default"]; @@ -704,7 +999,7 @@ exports.program = program;function invokePartial(partial, name, context, helpers exports.invokePartial = invokePartial;function noop() { return ""; } exports.noop = noop; -},{"./base":4,"./exception":5,"./utils":8}],7:[function(_dereq_,module,exports){ +},{"./base":5,"./exception":6,"./utils":9}],8:[function(_dereq_,module,exports){ "use strict"; // Build out our basic SafeString type function SafeString(string) { @@ -716,7 +1011,7 @@ SafeString.prototype.toString = function() { }; exports["default"] = SafeString; -},{}],8:[function(_dereq_,module,exports){ +},{}],9:[function(_dereq_,module,exports){ "use strict"; /*jshint -W004 */ var SafeString = _dereq_("./safe-string")["default"]; @@ -793,15 +1088,40 @@ exports.escapeExpression = escapeExpression;function isEmpty(value) { } exports.isEmpty = isEmpty; -},{"./safe-string":7}],9:[function(_dereq_,module,exports){ +},{"./safe-string":8}],10:[function(_dereq_,module,exports){ // Create a simple path alias to allow browserify to resolve // the runtime on a supported path. module.exports = _dereq_('./dist/cjs/handlebars.runtime'); -},{"./dist/cjs/handlebars.runtime":3}],10:[function(_dereq_,module,exports){ +},{"./dist/cjs/handlebars.runtime":4}],11:[function(_dereq_,module,exports){ module.exports = _dereq_("handlebars/runtime")["default"]; -},{"handlebars/runtime":9}],11:[function(_dereq_,module,exports){ +},{"handlebars/runtime":10}],12:[function(_dereq_,module,exports){ +if (typeof Object.create === 'function') { + // implementation from standard node.js 'util' module + module.exports = function inherits(ctor, superCtor) { + ctor.super_ = superCtor + ctor.prototype = Object.create(superCtor.prototype, { + constructor: { + value: ctor, + enumerable: false, + writable: true, + configurable: true + } + }); + }; +} else { + // old school shim for old browsers + module.exports = function inherits(ctor, superCtor) { + ctor.super_ = superCtor + var TempCtor = function () {} + TempCtor.prototype = superCtor.prototype + ctor.prototype = new TempCtor() + ctor.prototype.constructor = ctor + } +} + +},{}],13:[function(_dereq_,module,exports){ // hbsfy compiled Handlebars template var HandlebarsCompiler = _dereq_('hbsfy/runtime'); module.exports = HandlebarsCompiler.template(function (Handlebars,depth0,helpers,partials,data) { @@ -822,5 +1142,5 @@ helpers = this.merge(helpers, Handlebars.helpers); data = data || {}; return buffer; }); -},{"hbsfy/runtime":10}]},{},[1])(1) +},{"hbsfy/runtime":11}]},{},[1])(1) }); \ No newline at end of file diff --git a/src/subscribe-email.js b/src/subscribe-email.js index d05309c..f77c163 100644 --- a/src/subscribe-email.js +++ b/src/subscribe-email.js @@ -1,10 +1,14 @@ var template = require('./template.hbs'); var serialize = require('form-serialize'); +var inherits = require('inherits'); +var EventEmitter = require('events').EventEmitter; +inherits(SubscribeEmail, EventEmitter); module.exports = SubscribeEmail; function SubscribeEmail (options) { if (!(this instanceof SubscribeEmail)) return new SubscribeEmail(options); + var instance = this; options = setDefaults(options); var theForm = document.querySelector(options.element); var serviceConfig = configureService(options.service); @@ -27,24 +31,15 @@ function SubscribeEmail (options) { theForm.addEventListener('submit', function(e) { e.preventDefault(); var requestData = prepareData(this, options); - makeCorsRequest(serviceConfig.formAction, requestData, theForm); + instance.makeCorsRequest(serviceConfig.formAction, requestData, theForm); }); - //Listen for Message Events triggered on the form. - theForm.addEventListener('subscriptionMessage', function (e) { + //Listen for Message Events + this.on('subscriptionMessage', function (message) { if (!messageHolder) { - console.log(e.detail); + console.log(message); } else { - if (typeof e.detail === 'string') { - messageHolder.innerHTML = e.detail; - } else if (Array.isArray(e.detail)) { - messageHolder.innerHTML = ''; - e.detail.forEach(function(message) { - messageHolder.innerHTML += message + '
'; - }); - } else { - console.log(e.detail); - } + messageHolder.innerHTML = message; } }); } @@ -95,43 +90,39 @@ function configureService(service) { return serviceConfig; } -function fireEvent(name, detail, el){ - var customEvent; - if (window.CustomEvent) { - customEvent = new CustomEvent(name, {'detail': detail }); - } else { - customEvent = document.createEvent('CustomEvent'); - customEvent.initCustomEvent(name, true, true, detail); - } - el.dispatchEvent(customEvent); -} - -function makeCorsRequest(url, data, form) { +SubscribeEmail.prototype.makeCorsRequest = function (url, data, form) { + var instance = this; var xhr = createCorsRequest('POST', url); if (!xhr) { console.log('CORS not supported'); return; } + xhr.onload = function() { var response = JSON.parse(xhr.responseText); //Fire Message Event(s) - var payload = response.message || response.messages; - fireEvent('subscriptionMessage', payload, form); + if (response.message) { + instance.emit('subscriptionMessage', response.message); + } else if (response.messages) { + response.messages.forEach(function(message) { + instance.emit('subscriptionMessage', message); + }); + } //Fire Success or Error Event if (response.success || response.status === 'ok') { - fireEvent('subscriptionSuccess', response, form); + instance.emit('subscriptionSuccess', response); } else { - fireEvent('subscriptionError', response, form); + instance.emit('subscriptionError', response); } }; - + xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); xhr.send(data); -} +}; function createCorsRequest(method, url) { var xhr = new XMLHttpRequest(); diff --git a/tests/index.html b/tests/tests.html similarity index 60% rename from tests/index.html rename to tests/tests.html index e39affa..1024be0 100644 --- a/tests/index.html +++ b/tests/tests.html @@ -13,24 +13,38 @@

Here's a SendGrid subscribe form!

From 83c083309e980477edce9263e813301deb1953dc Mon Sep 17 00:00:00 2001 From: Josiah Sprague Date: Wed, 17 Sep 2014 18:22:26 -0400 Subject: [PATCH 18/60] [#MmLRZ2E2] Update test configuration Branch: MmLRZ2E2-development --- package.json | 15 +++++++-------- tests/tests.js | 22 ++++++++++------------ 2 files changed, 17 insertions(+), 20 deletions(-) diff --git a/package.json b/package.json index 2a8595b..426988e 100644 --- a/package.json +++ b/package.json @@ -19,15 +19,14 @@ "vinyl-source-stream": "^0.1.1" }, "testling": { - "files": "tests/*.js", + "html": "tests/tests.html", "browsers": [ - "ie/6..latest", - "chrome/22..latest", - "firefox/16..latest", - "safari/latest", - "opera/11.0..latest", - "iphone/6", - "ipad/6", + "ie/9..latest", + "chrome/36..latest", + "ff/24..latest", + "safari/6..latest", + "iphone/6..latest", + "ipad/6..latest", "android-browser/latest" ] }, diff --git a/tests/tests.js b/tests/tests.js index 2a12ef5..d7d0718 100644 --- a/tests/tests.js +++ b/tests/tests.js @@ -1,21 +1,19 @@ var SubscribeEmail = require('../build/subscribe-email.js'); var test = require('tape'); -document.addEventListener('DOMContentLoaded', function(){ - - var theElement = document.createElement('form'); - document.body.appendChild(theElement); - theElement.id = 'subscribe'; - - new SubscribeEmail({ - element: '#subscribe', - service: 'universe', - template: true, - key: '2366edcf-805b-43bf-b043-9c2f527967d9' - }); +//Setup +var theElement = document.createElement('form'); +document.body.appendChild(theElement); +theElement.id = 'subscribe'; +new SubscribeEmail({ + element: '#subscribe', + service: 'universe', + template: true, + key: '2366edcf-805b-43bf-b043-9c2f527967d9' }); +//Tests test('SubscribeEmail exists and is a function', function (t) { var objType = typeof SubscribeEmail; t.equal(objType, 'function'); From 3938c81fdf119f2dfe75cfedcfe8da6e29905996 Mon Sep 17 00:00:00 2001 From: Josiah Sprague Date: Fri, 19 Sep 2014 10:54:13 -0400 Subject: [PATCH 19/60] [#MmLRZ2E2] add mailchimp support, jquery support, and events add support for mailchimp's jsonp api, allow a jquery object to be passed in as the element parameter, update events API to fire on SubscribeEmail instance. Branch: MmLRZ2E2-development --- README.md | 13 ++++-- build/subscribe-email.js | 91 +++++++++++++++++++++++++--------------- src/subscribe-email.js | 90 +++++++++++++++++++++++++-------------- tests/tests.html | 15 ++++++- 4 files changed, 139 insertions(+), 70 deletions(-) diff --git a/README.md b/README.md index 11fb1dc..6fd7cee 100644 --- a/README.md +++ b/README.md @@ -66,8 +66,13 @@ Whether the form with be submitted asynchronously (defaults to `true`). Out of the box, the Subscribe Email module will generate BEM markup with the namespace `subscribe-email` that contains all of the elements your form needs. If you want to customize the markup, set `overrideTemplate: true` when you initialize the module to use the markup from the target element instead. This gives you full control over the markup, but you'll have to make sure that your form contains all of the required fields. ## Events -Some mailing list platforms may send response messages for things like confirmation or validation errors. The default template will display these messages along-side the form, but alternatively you can easily integrate the messages into other parts of your page by listening for the following events to fire on the form element; +Some mailing list platforms may send response messages for things like confirmation or validation errors. The default template will display these messages along-side the form, but alternatively you can easily integrate the messages into other parts of your page by listening for the following events to be emitted from the SubscribeEmail instance; -- `subscriptionMessage`: Fires whenever the mailing list provider returns a response (both success and failure). -- `subscriptionError`: This event will fire if the mailing list provider returns an error. Specific details about the error will be included in a payload object when available. -- `subscriptionSuccess`: This event will fire if the mailing list provider returns a confirmation that the email address has been added to the list. Specific details will be included in a payload object when available. \ No newline at end of file +### `subscriptionMessage` +Fires whenever the mailing list provider returns a response (both success and failure). + +### `subscriptionError` +This event will fire if the mailing list provider returns an error. Specific details about the error will be included in a payload object when available. + +### `subscriptionSuccess` +This event will fire if the mailing list provider returns a confirmation that the email address has been added to the list. Specific details will be included in a payload object when available. \ No newline at end of file diff --git a/build/subscribe-email.js b/build/subscribe-email.js index 53ee3fd..a91473d 100755 --- a/build/subscribe-email.js +++ b/build/subscribe-email.js @@ -11,12 +11,11 @@ function SubscribeEmail (options) { if (!(this instanceof SubscribeEmail)) return new SubscribeEmail(options); var instance = this; options = setDefaults(options); - var theForm = document.querySelector(options.element); - var serviceConfig = configureService(options.service); - - //Merge the serviceConfig object into the options - for (var attrname in serviceConfig) { - options[attrname] = serviceConfig[attrname]; + var theForm; + if (options.element.jquery) { + theForm = options.element[0]; + } else { + theForm = document.querySelector(options.element); } if (!options.overrideTemplate) { @@ -32,7 +31,11 @@ function SubscribeEmail (options) { theForm.addEventListener('submit', function(e) { e.preventDefault(); var requestData = prepareData(this, options); - instance.makeCorsRequest(serviceConfig.formAction, requestData, theForm); + if (options.service === 'mailchimp') { + instance.makeJSONPRequest(options.formAction, requestData, theForm); + } else { + instance.makeCorsRequest(options.formAction, requestData, theForm); + } }); //Listen for Message Events @@ -49,6 +52,24 @@ function setDefaults(options) { options.overrideTemplate = options.overrideTemplate || false; options.submitText = options.submitText || 'Subscribe'; options.responseElement = options.responseElement || '.subscribe-email__response'; + + switch (options.service) { + case 'universe': + options.formAction = options.formAction || 'http://staging.services.sparkart.net/api/v1/contacts'; + options.emailName = options.emailName || 'contact[email]'; + break; + case 'sendgrid': + options.formAction = options.formAction || 'https://sendgrid.com/newsletter/addRecipientFromWidget'; + options.emailName = options.emailName || 'SG_widget[email]'; + break; + case 'mailchimp': + options.formAction = options.formAction || options.url.replace('/post?', '/post-json?'); + options.emailName = options.emailName || 'EMAIL'; + break; + default: + break; + } + return options; } @@ -63,36 +84,14 @@ function prepareData(data, options) { '&p=' + encodeURIComponent(options.key) + '&r=' + window.location; break; - } - return requestData; -} - -function configureService(service) { - var serviceConfig = {}; - switch (service) { - case 'universe': - serviceConfig = { - formAction: 'http://staging.services.sparkart.net/api/v1/contacts', - emailName: 'contact[email]' - }; - break; - case 'sendgrid': - serviceConfig = { - formAction: 'https://sendgrid.com/newsletter/addRecipientFromWidget', - emailName: 'SG_widget[email]' - }; - break; - default: - serviceConfig = { - formAction: '' - }; + case 'mailchimp': + requestData = '&' + serialize(data); break; } - return serviceConfig; + return requestData; } SubscribeEmail.prototype.makeCorsRequest = function (url, data, form) { -//function makeCorsRequest(url, data, form) { var instance = this; var xhr = createCorsRequest('POST', url); if (!xhr) { @@ -124,7 +123,7 @@ SubscribeEmail.prototype.makeCorsRequest = function (url, data, form) { xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); xhr.send(data); -} +}; function createCorsRequest(method, url) { var xhr = new XMLHttpRequest(); @@ -138,6 +137,32 @@ function createCorsRequest(method, url) { } return xhr; } + +SubscribeEmail.prototype.getId = function() { + for (var id in window) { + if (window[id] === this) { return id; } + } +}; + +SubscribeEmail.prototype.makeJSONPRequest = function (url, data, form) { + var scriptElement = document.createElement('script'); + scriptElement.src = url + data + '&c=' + this.getId() + '.processJSONP'; + form.appendChild(scriptElement); +}; + +SubscribeEmail.prototype.processJSONP = function(json) { + //Fire Message Event(s) + if (json.msg) { + this.emit('subscriptionMessage', json.msg); + } + + //Fire Success or Error Event + if (json.result === 'success') { + this.emit('subscriptionSuccess', json); + } else { + this.emit('subscriptionError', json); + } +}; },{"./template.hbs":13,"events":2,"form-serialize":3,"inherits":12}],2:[function(_dereq_,module,exports){ // Copyright Joyent, Inc. and other Node contributors. // diff --git a/src/subscribe-email.js b/src/subscribe-email.js index f77c163..f50cbbc 100644 --- a/src/subscribe-email.js +++ b/src/subscribe-email.js @@ -10,12 +10,11 @@ function SubscribeEmail (options) { if (!(this instanceof SubscribeEmail)) return new SubscribeEmail(options); var instance = this; options = setDefaults(options); - var theForm = document.querySelector(options.element); - var serviceConfig = configureService(options.service); - - //Merge the serviceConfig object into the options - for (var attrname in serviceConfig) { - options[attrname] = serviceConfig[attrname]; + var theForm; + if (options.element.jquery) { + theForm = options.element[0]; + } else { + theForm = document.querySelector(options.element); } if (!options.overrideTemplate) { @@ -31,7 +30,11 @@ function SubscribeEmail (options) { theForm.addEventListener('submit', function(e) { e.preventDefault(); var requestData = prepareData(this, options); - instance.makeCorsRequest(serviceConfig.formAction, requestData, theForm); + if (options.service === 'mailchimp') { + instance.makeJSONPRequest(options.formAction, requestData, theForm); + } else { + instance.makeCorsRequest(options.formAction, requestData, theForm); + } }); //Listen for Message Events @@ -48,6 +51,24 @@ function setDefaults(options) { options.overrideTemplate = options.overrideTemplate || false; options.submitText = options.submitText || 'Subscribe'; options.responseElement = options.responseElement || '.subscribe-email__response'; + + switch (options.service) { + case 'universe': + options.formAction = options.formAction || 'http://staging.services.sparkart.net/api/v1/contacts'; + options.emailName = options.emailName || 'contact[email]'; + break; + case 'sendgrid': + options.formAction = options.formAction || 'https://sendgrid.com/newsletter/addRecipientFromWidget'; + options.emailName = options.emailName || 'SG_widget[email]'; + break; + case 'mailchimp': + options.formAction = options.formAction || options.url.replace('/post?', '/post-json?'); + options.emailName = options.emailName || 'EMAIL'; + break; + default: + break; + } + return options; } @@ -62,32 +83,11 @@ function prepareData(data, options) { '&p=' + encodeURIComponent(options.key) + '&r=' + window.location; break; - } - return requestData; -} - -function configureService(service) { - var serviceConfig = {}; - switch (service) { - case 'universe': - serviceConfig = { - formAction: 'http://staging.services.sparkart.net/api/v1/contacts', - emailName: 'contact[email]' - }; - break; - case 'sendgrid': - serviceConfig = { - formAction: 'https://sendgrid.com/newsletter/addRecipientFromWidget', - emailName: 'SG_widget[email]' - }; - break; - default: - serviceConfig = { - formAction: '' - }; + case 'mailchimp': + requestData = '&' + serialize(data); break; } - return serviceConfig; + return requestData; } SubscribeEmail.prototype.makeCorsRequest = function (url, data, form) { @@ -135,4 +135,30 @@ function createCorsRequest(method, url) { xhr = null; } return xhr; -} \ No newline at end of file +} + +SubscribeEmail.prototype.getId = function() { + for (var id in window) { + if (window[id] === this) { return id; } + } +}; + +SubscribeEmail.prototype.makeJSONPRequest = function (url, data, form) { + var scriptElement = document.createElement('script'); + scriptElement.src = url + data + '&c=' + this.getId() + '.processJSONP'; + form.appendChild(scriptElement); +}; + +SubscribeEmail.prototype.processJSONP = function(json) { + //Fire Message Event(s) + if (json.msg) { + this.emit('subscriptionMessage', json.msg); + } + + //Fire Success or Error Event + if (json.result === 'success') { + this.emit('subscriptionSuccess', json); + } else { + this.emit('subscriptionError', json); + } +}; \ No newline at end of file diff --git a/tests/tests.html b/tests/tests.html index 1024be0..845a3a7 100644 --- a/tests/tests.html +++ b/tests/tests.html @@ -1,6 +1,7 @@ Subscribe Email Demo + @@ -13,8 +14,9 @@ +

Here's a MailChimp subscribe form!

+
+ + + \ No newline at end of file From a51f83ebebe9a030fdc025b4ae32aae262a6ef5e Mon Sep 17 00:00:00 2001 From: Josiah Sprague Date: Mon, 22 Sep 2014 13:23:43 -0400 Subject: [PATCH 20/60] [#MmLRZ2E2] add IE9 fallbacks for all services fall back to JSONP or XDomainRequest when necessary depending on the browser and mailing list service Branch: MmLRZ2E2-development --- build/subscribe-email.js | 81 ++++++++++++++++++++++++++-------------- src/subscribe-email.js | 81 ++++++++++++++++++++++++++-------------- tests/tests.html | 1 + 3 files changed, 105 insertions(+), 58 deletions(-) diff --git a/build/subscribe-email.js b/build/subscribe-email.js index a91473d..77d3bd0 100755 --- a/build/subscribe-email.js +++ b/build/subscribe-email.js @@ -22,7 +22,7 @@ function SubscribeEmail (options) { //Render the Default Template theForm.innerHTML = template(options); //Add BEM Namespace Class to Form - theForm.classList.add('subscribe-email'); + theForm.className += ' subscribe-email'; } var messageHolder = theForm.querySelector(options.responseElement); @@ -30,8 +30,8 @@ function SubscribeEmail (options) { //Override Default Submit Action with CORS request theForm.addEventListener('submit', function(e) { e.preventDefault(); - var requestData = prepareData(this, options); - if (options.service === 'mailchimp') { + var requestData = instance.prepareData(this, options); + if (options.jsonp) { instance.makeJSONPRequest(options.formAction, requestData, theForm); } else { instance.makeCorsRequest(options.formAction, requestData, theForm); @@ -57,14 +57,17 @@ function setDefaults(options) { case 'universe': options.formAction = options.formAction || 'http://staging.services.sparkart.net/api/v1/contacts'; options.emailName = options.emailName || 'contact[email]'; + options.jsonp = !('withCredentials' in new XMLHttpRequest()); break; case 'sendgrid': - options.formAction = options.formAction || 'https://sendgrid.com/newsletter/addRecipientFromWidget'; + options.formAction = options.formAction || 'http://sendgrid.com/newsletter/addRecipientFromWidget'; options.emailName = options.emailName || 'SG_widget[email]'; + options.jsonp = false; break; case 'mailchimp': options.formAction = options.formAction || options.url.replace('/post?', '/post-json?'); options.emailName = options.emailName || 'EMAIL'; + options.jsonp = true; break; default: break; @@ -73,31 +76,33 @@ function setDefaults(options) { return options; } -function prepareData(data, options) { +SubscribeEmail.prototype.prepareData = function(data, options) { var requestData = ''; switch (options.service) { case 'universe': requestData = serialize(data) + '&key=' + options.key; + if (options.jsonp) { + requestData = '?' + requestData + + '&_method=post&callback=' + this.getId() + '.processJSONP'; + } break; case 'sendgrid': - requestData = serialize(data) + - '&p=' + encodeURIComponent(options.key) + - '&r=' + window.location; + requestData = 'p=' + encodeURIComponent(options.key) + + '&r=' + encodeURIComponent(window.location) + '&' + + serialize(data); break; case 'mailchimp': - requestData = '&' + serialize(data); + requestData = '&_method=post&' + serialize(data) + + '&c=' + this.getId() + '.processJSONP'; break; } return requestData; -} +}; SubscribeEmail.prototype.makeCorsRequest = function (url, data, form) { var instance = this; - var xhr = createCorsRequest('POST', url); - if (!xhr) { - console.log('CORS not supported'); - return; - } + var xhr = createCorsRequest('POST', url, data); + if (!xhr) { return; } xhr.onload = function() { @@ -121,21 +126,32 @@ SubscribeEmail.prototype.makeCorsRequest = function (url, data, form) { }; - xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); + if(xhr instanceof XMLHttpRequest){ + // Request headers cannot be set on XDomainRequest in IE9 + xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); + } xhr.send(data); }; -function createCorsRequest(method, url) { - var xhr = new XMLHttpRequest(); - if ('withCredentials' in xhr) { - xhr.open(method, url, true); - } else if (typeof XDomainRequest != 'undefined') { - xhr = new XDomainRequest(); - xhr.open(method, url); - } else { - xhr = null; - } - return xhr; +function createCorsRequest(method, url, data) { + + var xhr; + if ('withCredentials' in new XMLHttpRequest()) { + xhr = new XMLHttpRequest(); + xhr.open(method, url, true); + } else if (typeof XDomainRequest != 'undefined') { + xhr = new XDomainRequest(); + //The next 6 lines must be defined here or IE9 will abort the request + xhr.timeout = 3000; + xhr.onload = function(){}; + xhr.onerror = function (){}; + xhr.ontimeout = function(){}; + xhr.onprogress = function(){}; + xhr.open('POST', url + '?' + data); + } else { + xhr = null; + } + return xhr; } SubscribeEmail.prototype.getId = function() { @@ -146,14 +162,21 @@ SubscribeEmail.prototype.getId = function() { SubscribeEmail.prototype.makeJSONPRequest = function (url, data, form) { var scriptElement = document.createElement('script'); - scriptElement.src = url + data + '&c=' + this.getId() + '.processJSONP'; + scriptElement.src = url + data; form.appendChild(scriptElement); }; SubscribeEmail.prototype.processJSONP = function(json) { + var instance = this; //Fire Message Event(s) - if (json.msg) { + if (json.message) { + this.emit('subscriptionMessage', json.message); + } else if (json.msg) { this.emit('subscriptionMessage', json.msg); + } else if (json.messages) { + json.messages.forEach(function(message) { + instance.emit('subscriptionMessage', message); + }); } //Fire Success or Error Event diff --git a/src/subscribe-email.js b/src/subscribe-email.js index f50cbbc..91b0abb 100644 --- a/src/subscribe-email.js +++ b/src/subscribe-email.js @@ -21,7 +21,7 @@ function SubscribeEmail (options) { //Render the Default Template theForm.innerHTML = template(options); //Add BEM Namespace Class to Form - theForm.classList.add('subscribe-email'); + theForm.className += ' subscribe-email'; } var messageHolder = theForm.querySelector(options.responseElement); @@ -29,8 +29,8 @@ function SubscribeEmail (options) { //Override Default Submit Action with CORS request theForm.addEventListener('submit', function(e) { e.preventDefault(); - var requestData = prepareData(this, options); - if (options.service === 'mailchimp') { + var requestData = instance.prepareData(this, options); + if (options.jsonp) { instance.makeJSONPRequest(options.formAction, requestData, theForm); } else { instance.makeCorsRequest(options.formAction, requestData, theForm); @@ -56,14 +56,17 @@ function setDefaults(options) { case 'universe': options.formAction = options.formAction || 'http://staging.services.sparkart.net/api/v1/contacts'; options.emailName = options.emailName || 'contact[email]'; + options.jsonp = !('withCredentials' in new XMLHttpRequest()); break; case 'sendgrid': - options.formAction = options.formAction || 'https://sendgrid.com/newsletter/addRecipientFromWidget'; + options.formAction = options.formAction || 'http://sendgrid.com/newsletter/addRecipientFromWidget'; options.emailName = options.emailName || 'SG_widget[email]'; + options.jsonp = false; break; case 'mailchimp': options.formAction = options.formAction || options.url.replace('/post?', '/post-json?'); options.emailName = options.emailName || 'EMAIL'; + options.jsonp = true; break; default: break; @@ -72,31 +75,33 @@ function setDefaults(options) { return options; } -function prepareData(data, options) { +SubscribeEmail.prototype.prepareData = function(data, options) { var requestData = ''; switch (options.service) { case 'universe': requestData = serialize(data) + '&key=' + options.key; + if (options.jsonp) { + requestData = '?' + requestData + + '&_method=post&callback=' + this.getId() + '.processJSONP'; + } break; case 'sendgrid': - requestData = serialize(data) + - '&p=' + encodeURIComponent(options.key) + - '&r=' + window.location; + requestData = 'p=' + encodeURIComponent(options.key) + + '&r=' + encodeURIComponent(window.location) + '&' + + serialize(data); break; case 'mailchimp': - requestData = '&' + serialize(data); + requestData = '&_method=post&' + serialize(data) + + '&c=' + this.getId() + '.processJSONP'; break; } return requestData; -} +}; SubscribeEmail.prototype.makeCorsRequest = function (url, data, form) { var instance = this; - var xhr = createCorsRequest('POST', url); - if (!xhr) { - console.log('CORS not supported'); - return; - } + var xhr = createCorsRequest('POST', url, data); + if (!xhr) { return; } xhr.onload = function() { @@ -120,21 +125,32 @@ SubscribeEmail.prototype.makeCorsRequest = function (url, data, form) { }; - xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); + if(xhr instanceof XMLHttpRequest){ + // Request headers cannot be set on XDomainRequest in IE9 + xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); + } xhr.send(data); }; -function createCorsRequest(method, url) { - var xhr = new XMLHttpRequest(); - if ('withCredentials' in xhr) { - xhr.open(method, url, true); - } else if (typeof XDomainRequest != 'undefined') { - xhr = new XDomainRequest(); - xhr.open(method, url); - } else { - xhr = null; - } - return xhr; +function createCorsRequest(method, url, data) { + + var xhr; + if ('withCredentials' in new XMLHttpRequest()) { + xhr = new XMLHttpRequest(); + xhr.open(method, url, true); + } else if (typeof XDomainRequest != 'undefined') { + xhr = new XDomainRequest(); + //The next 6 lines must be defined here or IE9 will abort the request + xhr.timeout = 3000; + xhr.onload = function(){}; + xhr.onerror = function (){}; + xhr.ontimeout = function(){}; + xhr.onprogress = function(){}; + xhr.open('POST', url + '?' + data); + } else { + xhr = null; + } + return xhr; } SubscribeEmail.prototype.getId = function() { @@ -145,14 +161,21 @@ SubscribeEmail.prototype.getId = function() { SubscribeEmail.prototype.makeJSONPRequest = function (url, data, form) { var scriptElement = document.createElement('script'); - scriptElement.src = url + data + '&c=' + this.getId() + '.processJSONP'; + scriptElement.src = url + data; form.appendChild(scriptElement); }; SubscribeEmail.prototype.processJSONP = function(json) { + var instance = this; //Fire Message Event(s) - if (json.msg) { + if (json.message) { + this.emit('subscriptionMessage', json.message); + } else if (json.msg) { this.emit('subscriptionMessage', json.msg); + } else if (json.messages) { + json.messages.forEach(function(message) { + instance.emit('subscriptionMessage', message); + }); } //Fire Success or Error Event diff --git a/tests/tests.html b/tests/tests.html index 845a3a7..59b25bd 100644 --- a/tests/tests.html +++ b/tests/tests.html @@ -1,3 +1,4 @@ + Subscribe Email Demo From 383d29532e8a43d359e02c4262296bd8e71d0fe6 Mon Sep 17 00:00:00 2001 From: Josiah Sprague Date: Tue, 23 Sep 2014 16:17:52 -0400 Subject: [PATCH 21/60] [#MmLRZ2E2] Add Automated Mocha Tests Add some basic mocha tests that run on BrowserStack with Selenium WebDriver for Node. Branch: MmLRZ2E2-development --- package.json | 21 +++----------- tests/mocha-test.js | 68 +++++++++++++++++++++++++++++++++++++++++++++ tests/tests.html | 2 +- 3 files changed, 73 insertions(+), 18 deletions(-) create mode 100644 tests/mocha-test.js diff --git a/package.json b/package.json index 426988e..e6f8967 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "main": "build/subscribe-email.js", "devDependencies": { "browserify": "^5.11.1", + "browserstack-webdriver": "^2.41.1", "ecstatic": "^0.5.4", "form-serialize": "^0.3.0", "gulp": "^3.8.7", @@ -13,24 +14,10 @@ "gulp-util": "^3.0.1", "handlebars": "1.3.x", "hbsfy": "^2.1.0", + "inherits": "^2.0.1", "pretty-hrtime": "^0.2.1", "require-dir": "^0.1.0", - "tape": "^3.0.0", - "vinyl-source-stream": "^0.1.1" - }, - "testling": { - "html": "tests/tests.html", - "browsers": [ - "ie/9..latest", - "chrome/36..latest", - "ff/24..latest", - "safari/6..latest", - "iphone/6..latest", - "ipad/6..latest", - "android-browser/latest" - ] - }, - "scripts": { - "test": "tape tests/*.js" + "vinyl-source-stream": "^0.1.1", + "assert": "~1.1.2" } } diff --git a/tests/mocha-test.js b/tests/mocha-test.js new file mode 100644 index 0000000..b0c17ab --- /dev/null +++ b/tests/mocha-test.js @@ -0,0 +1,68 @@ +var assert = require('assert'), + fs = require('fs'); + +var webdriver = require('browserstack-webdriver') + test = require('browserstack-webdriver/testing'); + +test.describe('Subscribe Email', function() { + //Give the BrowserStack VM enough time to boot up before running tests + this.timeout(25000); + var driver, server; + + test.before(function() { + var capabilities = { + 'browser' : 'IE', + 'browser_version' : '9.0', + 'os' : 'Windows', + 'os_version' : '7', + 'resolution' : '1024x768', + 'browserstack.local' : 'true', + 'browserstack.user' : 'sparkart', + 'browserstack.key' : '***REMOVED***' + } + driver = new webdriver.Builder(). + usingServer('http://hub.browserstack.com/wd/hub'). + withCapabilities(capabilities). + build(); + + driver.get('http://lvh.me:8080/tests/tests.html'); + }); + + test.it('universe form works', function() { + + driver.findElement(webdriver.By.css('#subscribe-form input[type="email"]')).sendKeys('test@test.com'); + driver.findElement(webdriver.By.css('#subscribe-form input[type="submit"]')).click(); + driver.wait(function() { + return driver.findElement(webdriver.By.css('#subscribe-form .message')).getText().then(function(text) { + return 'Please check your email for confirmation instructions' === text; + }); + }, 2000); + + }); + + test.it('sendgrid form works', function() { + + driver.findElement(webdriver.By.css('#subscribe-form2 input[type="email"]')).sendKeys('test@test.com'); + driver.findElement(webdriver.By.css('#subscribe-form2 input[type="submit"]')).click(); + driver.wait(function() { + return driver.findElement(webdriver.By.css('#subscribe-form2 .subscribe-email__response')).getText().then(function(text) { + return 'You have subscribed to this Marketing Email.' === text; + }); + }, 2000); + + }); + + test.it('mailchimp form works', function() { + + driver.findElement(webdriver.By.css('#subscribe-form3 input[type="email"]')).sendKeys('test@test.com'); + driver.findElement(webdriver.By.css('#subscribe-form3 input[type="submit"]')).click(); + driver.wait(function() { + return driver.findElement(webdriver.By.css('#subscribe-form3 .subscribe-email__response')).getText().then(function(text) { + return '0 - This email address looks fake or invalid. Please enter a real email address' === text; + }); + }, 2000); + + }); + + test.after(function() { driver.quit(); }); +}); \ No newline at end of file diff --git a/tests/tests.html b/tests/tests.html index 59b25bd..88a4058 100644 --- a/tests/tests.html +++ b/tests/tests.html @@ -9,7 +9,7 @@

Here's a Universe subscribe form!

- +

From e2f4c667d5ed94c97520251a41db998ad1b784f1 Mon Sep 17 00:00:00 2001 From: Josiah Sprague Date: Wed, 24 Sep 2014 12:57:31 -0400 Subject: [PATCH 22/60] [#MmLRZ2E2] Add mocha task to gulp and make tests more DRY Branch: MmLRZ2E2-development --- gulp/mocha.js | 7 +++ gulpfile.js | 3 +- package.json | 4 +- tests/mocha-test.js | 114 +++++++++++++++++++++++++++----------------- tests/tests.html | 12 ++--- 5 files changed, 89 insertions(+), 51 deletions(-) create mode 100644 gulp/mocha.js diff --git a/gulp/mocha.js b/gulp/mocha.js new file mode 100644 index 0000000..de8cdeb --- /dev/null +++ b/gulp/mocha.js @@ -0,0 +1,7 @@ +var gulp = require('gulp'); +var mocha = require('gulp-mocha'); + +gulp.task('mocha', function () { + return gulp.src('tests/mocha-test.js', {read: false}) + .pipe(mocha({timeout: 25000})); +}); \ No newline at end of file diff --git a/gulpfile.js b/gulpfile.js index 1f9f21b..daf0a80 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -15,5 +15,6 @@ var requireDir = require('require-dir'); requireDir('./gulp', { recurse: true }); // Task groups -gulp.task('default', ['build', 'startServer']); +gulp.task('default', ['build', 'startServer', 'test']); +gulp.task('test', ['mocha']); gulp.task('build', ['browserify']); \ No newline at end of file diff --git a/package.json b/package.json index e6f8967..4433cf1 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,8 @@ "pretty-hrtime": "^0.2.1", "require-dir": "^0.1.0", "vinyl-source-stream": "^0.1.1", - "assert": "~1.1.2" + "assert": "~1.1.2", + "gulp-mocha": "~1.1.0", + "object-merge": "~2.5.1" } } diff --git a/tests/mocha-test.js b/tests/mocha-test.js index b0c17ab..edeabd3 100644 --- a/tests/mocha-test.js +++ b/tests/mocha-test.js @@ -1,67 +1,95 @@ -var assert = require('assert'), - fs = require('fs'); +var assert = require('assert'); +var fs = require('fs'); +var webdriver = require('browserstack-webdriver'); +var test = require('browserstack-webdriver/testing'); +var objectMerge = require('object-merge'); -var webdriver = require('browserstack-webdriver') - test = require('browserstack-webdriver/testing'); +var browserStackConfig = { + 'browserstack.local' : 'true', + 'browserstack.user' : 'sparkart', + 'browserstack.key' : '***REMOVED***' +} -test.describe('Subscribe Email', function() { - //Give the BrowserStack VM enough time to boot up before running tests - this.timeout(25000); - var driver, server; - - test.before(function() { - var capabilities = { - 'browser' : 'IE', - 'browser_version' : '9.0', - 'os' : 'Windows', - 'os_version' : '7', - 'resolution' : '1024x768', - 'browserstack.local' : 'true', - 'browserstack.user' : 'sparkart', - 'browserstack.key' : '***REMOVED***' - } +function setupDriver(capabilities) { driver = new webdriver.Builder(). usingServer('http://hub.browserstack.com/wd/hub'). withCapabilities(capabilities). build(); driver.get('http://lvh.me:8080/tests/tests.html'); + return driver; +} + +function testForm(driver, formId, submission, responseElement) { + responseElement = responseElement || '.subscribe-email__response' + driver.findElement(webdriver.By.css(formId + ' input[type="email"]')).sendKeys(submission); + driver.findElement(webdriver.By.css(formId + ' input[type="submit"]')).click(); + driver.wait(function() { + return driver.findElement(webdriver.By.css(formId + ' ' + responseElement)).getText().then(function(text) { + return text; + }); + }, 2000); +} + +//Test in IE 9 +test.describe('Forms work in IE 9', function() { + var driver; + + test.before(function() { + var capabilities = objectMerge(browserStackConfig, { + 'browser' : 'IE', + 'browser_version' : '9.0', + 'os' : 'Windows', + 'os_version' : '7' + }); + driver = setupDriver(capabilities); }); test.it('universe form works', function() { + var result = testForm(driver, '#universe-form', 'test@test.com', '.message'); + return 'Please check your email for confirmation instructions' === result; + }); - driver.findElement(webdriver.By.css('#subscribe-form input[type="email"]')).sendKeys('test@test.com'); - driver.findElement(webdriver.By.css('#subscribe-form input[type="submit"]')).click(); - driver.wait(function() { - return driver.findElement(webdriver.By.css('#subscribe-form .message')).getText().then(function(text) { - return 'Please check your email for confirmation instructions' === text; - }); - }, 2000); + test.it('sendgrid form works', function() { + var result = testForm(driver, '#sendgrid-form', 'test@test.com'); + return 'You have subscribed to this Marketing Email.' === result; + }); + test.it('mailchimp form works', function() { + var result = testForm(driver, '#mailchimp-form', 'test@test.com'); + return '0 - This email address looks fake or invalid. Please enter a real email address' === result; }); - test.it('sendgrid form works', function() { + test.after(function() { driver.quit(); }); +}); - driver.findElement(webdriver.By.css('#subscribe-form2 input[type="email"]')).sendKeys('test@test.com'); - driver.findElement(webdriver.By.css('#subscribe-form2 input[type="submit"]')).click(); - driver.wait(function() { - return driver.findElement(webdriver.By.css('#subscribe-form2 .subscribe-email__response')).getText().then(function(text) { - return 'You have subscribed to this Marketing Email.' === text; - }); - }, 2000); +//Test in Safari 7 +test.describe('Forms work in Safari 7', function() { + var driver; + test.before(function() { + var capabilities = objectMerge(browserStackConfig, { + 'browser' : 'Safari', + 'browser_version' : '7.0', + 'os' : 'OS X', + 'os_version' : 'Mavericks' + }); + driver = setupDriver(capabilities); }); - test.it('mailchimp form works', function() { + test.it('universe form works', function() { + var result = testForm(driver, '#universe-form', 'test@test.com', '.message'); + return 'Please check your email for confirmation instructions' === result; + }); - driver.findElement(webdriver.By.css('#subscribe-form3 input[type="email"]')).sendKeys('test@test.com'); - driver.findElement(webdriver.By.css('#subscribe-form3 input[type="submit"]')).click(); - driver.wait(function() { - return driver.findElement(webdriver.By.css('#subscribe-form3 .subscribe-email__response')).getText().then(function(text) { - return '0 - This email address looks fake or invalid. Please enter a real email address' === text; - }); - }, 2000); + test.it('sendgrid form works', function() { + var result = testForm(driver, '#sendgrid-form', 'test@test.com'); + return 'You have subscribed to this Marketing Email.' === result; + }); + test.it('mailchimp form works', function() { + var result = testForm(driver, '#mailchimp-form', 'test@test.com'); + return '0 - This email address looks fake or invalid. Please enter a real email address' === result; }); test.after(function() { driver.quit(); }); diff --git a/tests/tests.html b/tests/tests.html index 88a4058..2972635 100644 --- a/tests/tests.html +++ b/tests/tests.html @@ -8,14 +8,14 @@

Here's a Universe subscribe form!

-
+

Here's a SendGrid subscribe form!

-
+

Here's a MailChimp subscribe form!

-
+
+ + @@ -18,8 +20,7 @@ var myForm = $('#universe-form'); var universeForm = new SubscribeEmail({ element: myForm, - overrideTemplate: true, - responseElement: '.message', + template: Handlebars.templates.customTemplate, service: 'universe', key: 'd54e8487-e44e-4c6f-bdd7-6ab9c2eae1e9' }); From 91aa867a825d52dde0b7cb6a1de98658986fcbbd Mon Sep 17 00:00:00 2001 From: Josiah Sprague Date: Fri, 17 Oct 2014 16:27:31 -0400 Subject: [PATCH 39/60] [#MmLRZ2E2] update default template markup add label, switch to button instead of submit input Branch: MmLRZ2E2-development --- src/subscribe-form.hbs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/subscribe-form.hbs b/src/subscribe-form.hbs index b1a5364..c2f1ca7 100644 --- a/src/subscribe-form.hbs +++ b/src/subscribe-form.hbs @@ -1,3 +1,4 @@ + - + \ No newline at end of file From ff4cd6f7939885c6c02cb6a1563216f7dfbd761f Mon Sep 17 00:00:00 2001 From: Josiah Sprague Date: Fri, 17 Oct 2014 16:33:32 -0400 Subject: [PATCH 40/60] [#MmLRZ2E2] remove responseElement from test add a separate test for this later, when tests are written for custom templates Branch: MmLRZ2E2-development --- tests/mocha-test.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/mocha-test.js b/tests/mocha-test.js index 8ea6143..b8ce2be 100644 --- a/tests/mocha-test.js +++ b/tests/mocha-test.js @@ -38,12 +38,11 @@ function setupDriver(capabilities) { return driver; } -function testForm(driver, formId, submission, responseElement) { - responseElement = responseElement || '.subscribe-email__response' +function testForm(driver, formId, submission) { driver.findElement(webdriver.By.css(formId + ' input[type="email"]')).sendKeys(submission); driver.findElement(webdriver.By.css(formId + ' .subscribe-email__button')).click(); driver.wait(function() { - return driver.findElement(webdriver.By.css(formId + ' ' + responseElement)).getText().then(function(text) { + return driver.findElement(webdriver.By.css(formId + ' .subscribe-email__response')).getText().then(function(text) { return text; }); }, 2000); From 6c4a3831a832dd698b32468262b12ce008d9542d Mon Sep 17 00:00:00 2001 From: Josiah Sprague Date: Mon, 20 Oct 2014 14:01:25 -0400 Subject: [PATCH 41/60] [#MmLRZ2E2] switch to alerter module for messaging Branch: MmLRZ2E2-development --- README.md | 3 +++ gulp/browserify.js | 2 +- package.json | 1 + src/subscribe-email.js | 16 ++++++++++------ tests/mocha-test.js | 8 +++++--- 5 files changed, 20 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 7a6deee..0b51be7 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,9 @@ The module can be configured with several optional parameters passed to it's con ### `element` **(Required)** A DOM element, jQuery element, or selector string to refer to the placeholder element. +### `prependMessagesTo` +A selector string that refers to the element that should receive response messages. Defaults to the same value set for `element`. + ### `service` **(Required)** The mailing list platform you are using. Available options are `mailchimp`, `sendgrid` and `universe`. diff --git a/gulp/browserify.js b/gulp/browserify.js index 7d7ee4e..4038465 100644 --- a/gulp/browserify.js +++ b/gulp/browserify.js @@ -17,7 +17,7 @@ gulp.task('browserify', function() { bundleLogger.start(); return bundler - .transform(hbsfy) + .transform({global: true}, hbsfy) .bundle() .on('error', handleErrors) .pipe(source('subscribe-email.js')) diff --git a/package.json b/package.json index 7fc5e9d..888e4d5 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "vinyl-source-stream": "^0.1.1" }, "dependencies": { + "alerter": "git://github.com/blocks/alerts#c5fe1dc", "form-serialize": "^0.3.0", "inherits": "^2.0.1" } diff --git a/src/subscribe-email.js b/src/subscribe-email.js index 7bd2da3..e56938e 100644 --- a/src/subscribe-email.js +++ b/src/subscribe-email.js @@ -1,6 +1,7 @@ var template = require('./subscribe-form.hbs'); var serialize = require('form-serialize'); var inherits = require('inherits'); +var Alerter = require('alerter'); var EventEmitter = require('events').EventEmitter; inherits(SubscribeEmail, EventEmitter); @@ -23,7 +24,9 @@ function SubscribeEmail (options) { //Add BEM Namespace Class to Form theForm.className += ' subscribe-email'; - var messageHolder = theForm.querySelector(options.responseElement); + var messageHolder = new Alerter({ + prependTo: options.prependMessagesTo + }); //Override Default Submit Action with CORS request theForm.addEventListener('submit', function(e) { @@ -42,15 +45,16 @@ function SubscribeEmail (options) { //Listen for Message Events this.on('subscriptionMessage', function (message) { - if (messageHolder) { - messageHolder.innerHTML = message; - } + messageHolder.create({ + message: message, + dismissable: true + }); }); } function setDefaults(options, instance) { options.submitText = options.submitText || 'Subscribe'; - options.responseElement = options.responseElement || '.subscribe-email__response'; + options.prependMessagesTo = options.prependMessagesTo || options.element; if (typeof options.template === 'function') { instance.template = options.template; @@ -61,7 +65,7 @@ function setDefaults(options, instance) { switch (options.service) { case 'universe': - options.formAction = options.formAction || 'http://services.sparkart.net/api/v1/contacts'; + options.formAction = options.formAction || 'http://staging.services.sparkart.net/api/v1/contacts'; options.emailName = options.emailName || 'contact[email]'; options.jsonp = !('withCredentials' in new XMLHttpRequest()); break; diff --git a/tests/mocha-test.js b/tests/mocha-test.js index b8ce2be..f55bc8a 100644 --- a/tests/mocha-test.js +++ b/tests/mocha-test.js @@ -40,12 +40,14 @@ function setupDriver(capabilities) { function testForm(driver, formId, submission) { driver.findElement(webdriver.By.css(formId + ' input[type="email"]')).sendKeys(submission); - driver.findElement(webdriver.By.css(formId + ' .subscribe-email__button')).click(); + driver.findElement(webdriver.By.css(formId + ' .subscribe-email__button')).click().then(function() { + driver.sleep(800); + }); driver.wait(function() { - return driver.findElement(webdriver.By.css(formId + ' .subscribe-email__response')).getText().then(function(text) { + return driver.findElement(webdriver.By.css(formId + ' .alert__message')).getText().then(function(text) { return text; }); - }, 2000); + }, 1000); } From 1725fb950280e6f0b55fd6c13b2db0990835cb7b Mon Sep 17 00:00:00 2001 From: Josiah Sprague Date: Mon, 20 Oct 2014 15:34:25 -0400 Subject: [PATCH 42/60] [#MmLRZ2E2] clear up private method definitions Branch: MmLRZ2E2-development --- src/subscribe-email.js | 51 +++++++++++++++++------------------------- 1 file changed, 21 insertions(+), 30 deletions(-) diff --git a/src/subscribe-email.js b/src/subscribe-email.js index e56938e..cf33184 100644 --- a/src/subscribe-email.js +++ b/src/subscribe-email.js @@ -10,7 +10,7 @@ module.exports = SubscribeEmail; function SubscribeEmail (options) { if (!(this instanceof SubscribeEmail)) return new SubscribeEmail(options); var instance = this; - options = setDefaults(options, instance); + options = _setDefaults(options, instance); var theForm; if (options.element.jquery) { @@ -32,11 +32,11 @@ function SubscribeEmail (options) { theForm.addEventListener('submit', function(e) { e.preventDefault(); if (serialize(this)) { //Only submit form if there is data - var requestData = instance.prepareData(this, options); + var requestData = _prepareData(this, options); if (options.jsonp) { - instance.makeJSONPRequest(options.formAction, requestData); + _makeJSONPRequest(options.formAction, requestData, instance); } else { - instance.makeCorsRequest(options.formAction, requestData, theForm); + _makeCorsRequest(options.formAction, requestData, instance); } } else { instance.emit('subscriptionError', 'An email address is required.'); @@ -52,7 +52,8 @@ function SubscribeEmail (options) { }); } -function setDefaults(options, instance) { +//Private Functions +function _setDefaults(options, instance) { options.submitText = options.submitText || 'Subscribe'; options.prependMessagesTo = options.prependMessagesTo || options.element; @@ -86,7 +87,7 @@ function setDefaults(options, instance) { return options; } -SubscribeEmail.prototype.prepareData = function(data, options) { +function _prepareData(data, options) { var requestData = ''; switch (options.service) { case 'universe': @@ -106,11 +107,10 @@ SubscribeEmail.prototype.prepareData = function(data, options) { break; } return requestData; -}; +} -SubscribeEmail.prototype.makeCorsRequest = function (url, data, form) { - var instance = this; - var xhr = createCorsRequest('POST', url, data); +function _makeCorsRequest(url, data, instance) { + var xhr = _createCorsRequest('POST', url, data); if (!xhr) { return; } xhr.onload = function() { @@ -144,9 +144,9 @@ SubscribeEmail.prototype.makeCorsRequest = function (url, data, form) { xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); } xhr.send(data); -}; +} -function createCorsRequest(method, url, data) { +function _createCorsRequest(method, url, data) { var xhr; if ('withCredentials' in new XMLHttpRequest()) { @@ -167,16 +167,8 @@ function createCorsRequest(method, url, data) { return xhr; } -SubscribeEmail.prototype.getId = function() { - for (var id in window) { - if (window[id] === this) { return id; } - } - return 'SubscribeEmail'; -}; - -SubscribeEmail.prototype.makeJSONPRequest = function (url, data) { +function _makeJSONPRequest(url, data, instance) { var callbackName, scriptElement; - var instance = this; callbackName = "cb_" + Math.floor(Math.random() * 10000); @@ -188,21 +180,20 @@ SubscribeEmail.prototype.makeJSONPRequest = function (url, data) { window[callbackName] = undefined; } - instance.processJSONP(json); + _processJSONP(json, instance); }; scriptElement = document.createElement('script'); scriptElement.src = url + data + callbackName; document.body.appendChild(scriptElement); -}; +} -SubscribeEmail.prototype.processJSONP = function(json) { - var instance = this; +function _processJSONP(json, instance) { //Fire Message Event(s) if (json.message) { - this.emit('subscriptionMessage', json.message); + instance.emit('subscriptionMessage', json.message); } else if (json.msg) { - this.emit('subscriptionMessage', json.msg); + instance.emit('subscriptionMessage', json.msg); } else if (json.messages) { json.messages.forEach(function(message) { instance.emit('subscriptionMessage', message); @@ -211,8 +202,8 @@ SubscribeEmail.prototype.processJSONP = function(json) { //Fire Success or Error Event if (json.result === 'success' || json.status === 'ok') { - this.emit('subscriptionSuccess', json); + instance.emit('subscriptionSuccess', json); } else { - this.emit('subscriptionError', json); + instance.emit('subscriptionError', json); } -}; \ No newline at end of file +} \ No newline at end of file From d20995083a7302e378a260d8a0ae975085b97c42 Mon Sep 17 00:00:00 2001 From: Josiah Sprague Date: Tue, 21 Oct 2014 17:16:08 -0400 Subject: [PATCH 43/60] [#MmLRZ2E2] add in page mocha tests Move all gulp tasks into gulpfile (easier to manage), add selenium test that runs in-page mocha tests Branch: MmLRZ2E2-development --- .gitignore | 3 +- gulp/browserify.js | 30 - gulp/mocha.js | 7 - gulp/startBrowserStackTunnel.js | 24 - gulp/startServer.js | 11 - gulp/util/bundleLogger.js | 21 - gulp/util/handleErrors.js | 15 - gulpfile.js | 104 +- package.json | 7 +- {tests => test/demo}/handlebars.runtime.js | 0 {tests => test/demo}/template.js | 0 {tests => test/demo}/tests.html | 10 +- test/mocha.opts | 1 + test/mocha/mocha.css | 270 + test/mocha/mocha.js | 5983 +++++++++++++++++ test/mocha/test.html | 43 + .../mocha-test.js => test/selenium-driver.js | 74 +- test/tests.js | 23 + 18 files changed, 6468 insertions(+), 158 deletions(-) delete mode 100644 gulp/browserify.js delete mode 100644 gulp/mocha.js delete mode 100644 gulp/startBrowserStackTunnel.js delete mode 100644 gulp/startServer.js delete mode 100644 gulp/util/bundleLogger.js delete mode 100644 gulp/util/handleErrors.js rename {tests => test/demo}/handlebars.runtime.js (100%) rename {tests => test/demo}/template.js (100%) rename {tests => test/demo}/tests.html (87%) create mode 100644 test/mocha.opts create mode 100644 test/mocha/mocha.css create mode 100644 test/mocha/mocha.js create mode 100644 test/mocha/test.html rename tests/mocha-test.js => test/selenium-driver.js (53%) create mode 100644 test/tests.js diff --git a/.gitignore b/.gitignore index bcd300b..3006a18 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ node_modules -build/* \ No newline at end of file +build/* +test/mocha/tests.js \ No newline at end of file diff --git a/gulp/browserify.js b/gulp/browserify.js deleted file mode 100644 index 4038465..0000000 --- a/gulp/browserify.js +++ /dev/null @@ -1,30 +0,0 @@ -var browserify = require('browserify'); -var bundleLogger = require('./util/bundleLogger'); -var gulp = require('gulp'); -var handleErrors = require('./util/handleErrors'); -var source = require('vinyl-source-stream'); -var derequire = require('gulp-derequire'); - -var hbsfy = require('hbsfy'); - -gulp.task('browserify', function() { - var bundler = browserify({ - entries: ['./src/subscribe-email.js'], - standalone: 'SubscribeEmail' - }); - - var bundle = function() { - bundleLogger.start(); - - return bundler - .transform({global: true}, hbsfy) - .bundle() - .on('error', handleErrors) - .pipe(source('subscribe-email.js')) - .pipe(derequire()) - .pipe(gulp.dest('./build/')) - .on('end', bundleLogger.end); - }; - - return bundle(); -}); \ No newline at end of file diff --git a/gulp/mocha.js b/gulp/mocha.js deleted file mode 100644 index 490dc84..0000000 --- a/gulp/mocha.js +++ /dev/null @@ -1,7 +0,0 @@ -var gulp = require('gulp'); -var mocha = require('gulp-mocha'); - -gulp.task('mocha', function () { - return gulp.src('tests/mocha-test.js', {read: false}) - .pipe(mocha({timeout: 55000})); -}); \ No newline at end of file diff --git a/gulp/startBrowserStackTunnel.js b/gulp/startBrowserStackTunnel.js deleted file mode 100644 index e0a596d..0000000 --- a/gulp/startBrowserStackTunnel.js +++ /dev/null @@ -1,24 +0,0 @@ -var gulp = require('gulp'); -var BrowserStackTunnel = require('browserstacktunnel-wrapper'); - -gulp.task('BrowserStackTunnel', function(cb) { - var browserStackTunnel = new BrowserStackTunnel({ - key: '', - hosts: [{ - name: 'localhost', - port: 3000, - sslFlag: 0 - }], - v: true - }); - - browserStackTunnel.start(function(error) { - if (error) { - console.log(error); - } else { - console.log('BrowserStack Tunnel Started'); - cb(); - } - }); - -}); \ No newline at end of file diff --git a/gulp/startServer.js b/gulp/startServer.js deleted file mode 100644 index ddf60e0..0000000 --- a/gulp/startServer.js +++ /dev/null @@ -1,11 +0,0 @@ -var gulp = require('gulp'); -var http = require('http'); -var ecstatic = require('ecstatic'); - -gulp.task('startServer', function(cb) { - http.createServer( - ecstatic({ root: './' }) - ).listen(8080); - console.log('Listening on :8080'); - cb(); -}); \ No newline at end of file diff --git a/gulp/util/bundleLogger.js b/gulp/util/bundleLogger.js deleted file mode 100644 index 153f7ca..0000000 --- a/gulp/util/bundleLogger.js +++ /dev/null @@ -1,21 +0,0 @@ -/* bundleLogger - ------------ - Provides gulp style logs to the bundle method in browserify.js -*/ - -var gutil = require('gulp-util'); -var prettyHrtime = require('pretty-hrtime'); -var startTime; - -module.exports = { - start: function() { - startTime = process.hrtime(); - gutil.log('Running', gutil.colors.green("'bundle'") + '...'); - }, - - end: function() { - var taskTime = process.hrtime(startTime); - var prettyTime = prettyHrtime(taskTime); - gutil.log('Finished', gutil.colors.green("'bundle'"), 'in', gutil.colors.magenta(prettyTime)); - } -}; \ No newline at end of file diff --git a/gulp/util/handleErrors.js b/gulp/util/handleErrors.js deleted file mode 100644 index be53783..0000000 --- a/gulp/util/handleErrors.js +++ /dev/null @@ -1,15 +0,0 @@ -var notify = require("gulp-notify"); - -module.exports = function() { - - var args = Array.prototype.slice.call(arguments); - - // Send error to notification center with gulp-notify - notify.onError({ - title: "Compile Error", - message: "<%= error.message %>" - }).apply(this, args); - - // Keep gulp from hanging on this task - this.emit('end'); -}; \ No newline at end of file diff --git a/gulpfile.js b/gulpfile.js index c7ed217..01f2d70 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -1,26 +1,92 @@ -/* - gulpfile.js - =========== - Rather than manage one giant configuration file responsible - for creating multiple tasks, each task has been broken out into - its own file in `/gulp`. Any file in that folder gets automatically - required below. - - To add a new task, simply add a new task file to the `/gulp` directory. -*/ var gulp = require('gulp'); -var requireDir = require('require-dir'); var runSequence = require('run-sequence'); +var browserify = require('browserify'); +var hbsfy = require('hbsfy'); +var source = require('vinyl-source-stream'); +var derequire = require('gulp-derequire'); +var http = require('http'); +var ecstatic = require('ecstatic'); +var BrowserStackTunnel = require('browserstacktunnel-wrapper'); +var mocha = require('gulp-spawn-mocha'); -// Require all tasks in gulp/tasks, including subfolders -requireDir('./gulp', { recurse: true }); +function handleError(err) { + console.log(err.toString()); + this.emit('end'); +} // Task groups -gulp.task('default', ['build', 'startServer']); +gulp.task('default', ['build', 'start-server']); + gulp.task('test', function(callback) { - runSequence('default', - 'BrowserStackTunnel', - 'mocha', - callback); + runSequence( + ['default', 'build-tests'], + 'start-browserstack-tunnel', + 'run-selenium', + callback + ); +}); + +gulp.task('build', function() { + var bundler = browserify({ + entries: ['./src/subscribe-email.js'], + standalone: 'SubscribeEmail' + }); + var bundle = function() { + return bundler + .transform({global: true}, hbsfy) + .bundle() + .pipe(source('subscribe-email.js')) + .pipe(derequire()) + .pipe(gulp.dest('./build/')); + }; + return bundle(); +}); + +gulp.task('build-tests', function() { + var bundler = browserify({ + entries: ['./test/tests.js'] + }); + var bundle = function() { + return bundler + .transform({global: true}, hbsfy) + .bundle() + .pipe(source('tests.js')) + .pipe(derequire()) + .pipe(gulp.dest('./test/mocha/')); + }; + return bundle(); }); -gulp.task('build', ['browserify']); \ No newline at end of file + +gulp.task('start-server', function(cb) { + http.createServer( + ecstatic({ root: './' }) + ).listen(8080); + console.log('Listening on :8080'); + cb(); +}); + +gulp.task('start-browserstack-tunnel', function(cb) { + var browserStackTunnel = new BrowserStackTunnel({ + key: '', + hosts: [{ + name: 'localhost', + port: 3000, + sslFlag: 0 + }], + v: true + }); + browserStackTunnel.start(function(error) { + if (error) { + console.log(error); + } else { + console.log('BrowserStack Tunnel Started'); + cb(); + } + }); +}); + +gulp.task('run-selenium', function () { + return gulp.src('test/selenium-driver.js', {read: false}) + .pipe(mocha({timeout: 55000})) + .on('error', handleError); +}); \ No newline at end of file diff --git a/package.json b/package.json index 888e4d5..9ea0616 100644 --- a/package.json +++ b/package.json @@ -16,18 +16,19 @@ "author": "Josiah Sprague ", "main": "src/subscribe-email.js", "devDependencies": { - "assert": "~1.1.2", "browserify": "^5.11.1", "browserstack-webdriver": "^2.41.1", "browserstacktunnel-wrapper": "~1.3.0", "ecstatic": "^0.5.4", "gulp": "^3.8.7", "gulp-derequire": "^1.1.0", - "gulp-mocha": "~1.1.0", "gulp-notify": "^1.5.1", - "gulp-util": "^3.0.1", + "gulp-spawn-mocha": "^0.4.1", "handlebars": "1.3.x", "hbsfy": "^2.1.0", + "istanbul": "^0.3.2", + "mocha": "^1.21.5", + "mocha-clean": "^0.3.0", "object-merge": "~2.5.1", "pretty-hrtime": "^0.2.1", "require-dir": "^0.1.0", diff --git a/tests/handlebars.runtime.js b/test/demo/handlebars.runtime.js similarity index 100% rename from tests/handlebars.runtime.js rename to test/demo/handlebars.runtime.js diff --git a/tests/template.js b/test/demo/template.js similarity index 100% rename from tests/template.js rename to test/demo/template.js diff --git a/tests/tests.html b/test/demo/tests.html similarity index 87% rename from tests/tests.html rename to test/demo/tests.html index 529a167..880a35f 100644 --- a/tests/tests.html +++ b/test/demo/tests.html @@ -3,7 +3,7 @@ Subscribe Email Demo - + @@ -26,13 +26,13 @@ }); universeForm.on('subscriptionMessage', function (payload) { - console.log("Message: ", payload); + console.log("Message: ", payload); }); universeForm.on('subscriptionError', function (payload) { - console.log("Error:", payload); + console.log("Error:", payload); }); universeForm.on('subscriptionSuccess', function (payload) { - console.log("Success:", payload); + console.log("Success:", payload); }); @@ -47,7 +47,7 @@ }); sendGridForm.on('subscriptionMessage', function (payload) { - console.log("SendGrid Message: ", payload); + console.log("SendGrid Message: ", payload); }); diff --git a/test/mocha.opts b/test/mocha.opts new file mode 100644 index 0000000..b325f7c --- /dev/null +++ b/test/mocha.opts @@ -0,0 +1 @@ +--require mocha-clean \ No newline at end of file diff --git a/test/mocha/mocha.css b/test/mocha/mocha.css new file mode 100644 index 0000000..42b9798 --- /dev/null +++ b/test/mocha/mocha.css @@ -0,0 +1,270 @@ +@charset "utf-8"; + +body { + margin:0; +} + +#mocha { + font: 20px/1.5 "Helvetica Neue", Helvetica, Arial, sans-serif; + margin: 60px 50px; +} + +#mocha ul, +#mocha li { + margin: 0; + padding: 0; +} + +#mocha ul { + list-style: none; +} + +#mocha h1, +#mocha h2 { + margin: 0; +} + +#mocha h1 { + margin-top: 15px; + font-size: 1em; + font-weight: 200; +} + +#mocha h1 a { + text-decoration: none; + color: inherit; +} + +#mocha h1 a:hover { + text-decoration: underline; +} + +#mocha .suite .suite h1 { + margin-top: 0; + font-size: .8em; +} + +#mocha .hidden { + display: none; +} + +#mocha h2 { + font-size: 12px; + font-weight: normal; + cursor: pointer; +} + +#mocha .suite { + margin-left: 15px; +} + +#mocha .test { + margin-left: 15px; + overflow: hidden; +} + +#mocha .test.pending:hover h2::after { + content: '(pending)'; + font-family: arial, sans-serif; +} + +#mocha .test.pass.medium .duration { + background: #c09853; +} + +#mocha .test.pass.slow .duration { + background: #b94a48; +} + +#mocha .test.pass::before { + content: '✓'; + font-size: 12px; + display: block; + float: left; + margin-right: 5px; + color: #00d6b2; +} + +#mocha .test.pass .duration { + font-size: 9px; + margin-left: 5px; + padding: 2px 5px; + color: #fff; + -webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.2); + -moz-box-shadow: inset 0 1px 1px rgba(0,0,0,.2); + box-shadow: inset 0 1px 1px rgba(0,0,0,.2); + -webkit-border-radius: 5px; + -moz-border-radius: 5px; + -ms-border-radius: 5px; + -o-border-radius: 5px; + border-radius: 5px; +} + +#mocha .test.pass.fast .duration { + display: none; +} + +#mocha .test.pending { + color: #0b97c4; +} + +#mocha .test.pending::before { + content: '◦'; + color: #0b97c4; +} + +#mocha .test.fail { + color: #c00; +} + +#mocha .test.fail pre { + color: black; +} + +#mocha .test.fail::before { + content: '✖'; + font-size: 12px; + display: block; + float: left; + margin-right: 5px; + color: #c00; +} + +#mocha .test pre.error { + color: #c00; + max-height: 300px; + overflow: auto; +} + +/** + * (1): approximate for browsers not supporting calc + * (2): 42 = 2*15 + 2*10 + 2*1 (padding + margin + border) + * ^^ seriously + */ +#mocha .test pre { + display: block; + float: left; + clear: left; + font: 12px/1.5 monaco, monospace; + margin: 5px; + padding: 15px; + border: 1px solid #eee; + max-width: 85%; /*(1)*/ + max-width: calc(100% - 42px); /*(2)*/ + word-wrap: break-word; + border-bottom-color: #ddd; + -webkit-border-radius: 3px; + -webkit-box-shadow: 0 1px 3px #eee; + -moz-border-radius: 3px; + -moz-box-shadow: 0 1px 3px #eee; + border-radius: 3px; +} + +#mocha .test h2 { + position: relative; +} + +#mocha .test a.replay { + position: absolute; + top: 3px; + right: 0; + text-decoration: none; + vertical-align: middle; + display: block; + width: 15px; + height: 15px; + line-height: 15px; + text-align: center; + background: #eee; + font-size: 15px; + -moz-border-radius: 15px; + border-radius: 15px; + -webkit-transition: opacity 200ms; + -moz-transition: opacity 200ms; + transition: opacity 200ms; + opacity: 0.3; + color: #888; +} + +#mocha .test:hover a.replay { + opacity: 1; +} + +#mocha-report.pass .test.fail { + display: none; +} + +#mocha-report.fail .test.pass { + display: none; +} + +#mocha-report.pending .test.pass, +#mocha-report.pending .test.fail { + display: none; +} +#mocha-report.pending .test.pass.pending { + display: block; +} + +#mocha-error { + color: #c00; + font-size: 1.5em; + font-weight: 100; + letter-spacing: 1px; +} + +#mocha-stats { + position: fixed; + top: 15px; + right: 10px; + font-size: 12px; + margin: 0; + color: #888; + z-index: 1; +} + +#mocha-stats .progress { + float: right; + padding-top: 0; +} + +#mocha-stats em { + color: black; +} + +#mocha-stats a { + text-decoration: none; + color: inherit; +} + +#mocha-stats a:hover { + border-bottom: 1px solid #eee; +} + +#mocha-stats li { + display: inline-block; + margin: 0 5px; + list-style: none; + padding-top: 11px; +} + +#mocha-stats canvas { + width: 40px; + height: 40px; +} + +#mocha code .comment { color: #ddd; } +#mocha code .init { color: #2f6fad; } +#mocha code .string { color: #5890ad; } +#mocha code .keyword { color: #8a6343; } +#mocha code .number { color: #2f6fad; } + +@media screen and (max-device-width: 480px) { + #mocha { + margin: 60px 0px; + } + + #mocha #stats { + position: absolute; + } +} diff --git a/test/mocha/mocha.js b/test/mocha/mocha.js new file mode 100644 index 0000000..d677329 --- /dev/null +++ b/test/mocha/mocha.js @@ -0,0 +1,5983 @@ +;(function(){ + +// CommonJS require() + +function require(p){ + var path = require.resolve(p) + , mod = require.modules[path]; + if (!mod) throw new Error('failed to require "' + p + '"'); + if (!mod.exports) { + mod.exports = {}; + mod.call(mod.exports, mod, mod.exports, require.relative(path)); + } + return mod.exports; + } + +require.modules = {}; + +require.resolve = function (path){ + var orig = path + , reg = path + '.js' + , index = path + '/index.js'; + return require.modules[reg] && reg + || require.modules[index] && index + || orig; + }; + +require.register = function (path, fn){ + require.modules[path] = fn; + }; + +require.relative = function (parent) { + return function(p){ + if ('.' != p.charAt(0)) return require(p); + + var path = parent.split('/') + , segs = p.split('/'); + path.pop(); + + for (var i = 0; i < segs.length; i++) { + var seg = segs[i]; + if ('..' == seg) path.pop(); + else if ('.' != seg) path.push(seg); + } + + return require(path.join('/')); + }; + }; + + +require.register("browser/debug.js", function(module, exports, require){ + +module.exports = function(type){ + return function(){ + } +}; + +}); // module: browser/debug.js + +require.register("browser/diff.js", function(module, exports, require){ +/* See LICENSE file for terms of use */ + +/* + * Text diff implementation. + * + * This library supports the following APIS: + * JsDiff.diffChars: Character by character diff + * JsDiff.diffWords: Word (as defined by \b regex) diff which ignores whitespace + * JsDiff.diffLines: Line based diff + * + * JsDiff.diffCss: Diff targeted at CSS content + * + * These methods are based on the implementation proposed in + * "An O(ND) Difference Algorithm and its Variations" (Myers, 1986). + * http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.4.6927 + */ +var JsDiff = (function() { + /*jshint maxparams: 5*/ + function clonePath(path) { + return { newPos: path.newPos, components: path.components.slice(0) }; + } + function removeEmpty(array) { + var ret = []; + for (var i = 0; i < array.length; i++) { + if (array[i]) { + ret.push(array[i]); + } + } + return ret; + } + function escapeHTML(s) { + var n = s; + n = n.replace(/&/g, '&'); + n = n.replace(//g, '>'); + n = n.replace(/"/g, '"'); + + return n; + } + + var Diff = function(ignoreWhitespace) { + this.ignoreWhitespace = ignoreWhitespace; + }; + Diff.prototype = { + diff: function(oldString, newString) { + // Handle the identity case (this is due to unrolling editLength == 0 + if (newString === oldString) { + return [{ value: newString }]; + } + if (!newString) { + return [{ value: oldString, removed: true }]; + } + if (!oldString) { + return [{ value: newString, added: true }]; + } + + newString = this.tokenize(newString); + oldString = this.tokenize(oldString); + + var newLen = newString.length, oldLen = oldString.length; + var maxEditLength = newLen + oldLen; + var bestPath = [{ newPos: -1, components: [] }]; + + // Seed editLength = 0 + var oldPos = this.extractCommon(bestPath[0], newString, oldString, 0); + if (bestPath[0].newPos+1 >= newLen && oldPos+1 >= oldLen) { + return bestPath[0].components; + } + + for (var editLength = 1; editLength <= maxEditLength; editLength++) { + for (var diagonalPath = -1*editLength; diagonalPath <= editLength; diagonalPath+=2) { + var basePath; + var addPath = bestPath[diagonalPath-1], + removePath = bestPath[diagonalPath+1]; + oldPos = (removePath ? removePath.newPos : 0) - diagonalPath; + if (addPath) { + // No one else is going to attempt to use this value, clear it + bestPath[diagonalPath-1] = undefined; + } + + var canAdd = addPath && addPath.newPos+1 < newLen; + var canRemove = removePath && 0 <= oldPos && oldPos < oldLen; + if (!canAdd && !canRemove) { + bestPath[diagonalPath] = undefined; + continue; + } + + // Select the diagonal that we want to branch from. We select the prior + // path whose position in the new string is the farthest from the origin + // and does not pass the bounds of the diff graph + if (!canAdd || (canRemove && addPath.newPos < removePath.newPos)) { + basePath = clonePath(removePath); + this.pushComponent(basePath.components, oldString[oldPos], undefined, true); + } else { + basePath = clonePath(addPath); + basePath.newPos++; + this.pushComponent(basePath.components, newString[basePath.newPos], true, undefined); + } + + var oldPos = this.extractCommon(basePath, newString, oldString, diagonalPath); + + if (basePath.newPos+1 >= newLen && oldPos+1 >= oldLen) { + return basePath.components; + } else { + bestPath[diagonalPath] = basePath; + } + } + } + }, + + pushComponent: function(components, value, added, removed) { + var last = components[components.length-1]; + if (last && last.added === added && last.removed === removed) { + // We need to clone here as the component clone operation is just + // as shallow array clone + components[components.length-1] = + {value: this.join(last.value, value), added: added, removed: removed }; + } else { + components.push({value: value, added: added, removed: removed }); + } + }, + extractCommon: function(basePath, newString, oldString, diagonalPath) { + var newLen = newString.length, + oldLen = oldString.length, + newPos = basePath.newPos, + oldPos = newPos - diagonalPath; + while (newPos+1 < newLen && oldPos+1 < oldLen && this.equals(newString[newPos+1], oldString[oldPos+1])) { + newPos++; + oldPos++; + + this.pushComponent(basePath.components, newString[newPos], undefined, undefined); + } + basePath.newPos = newPos; + return oldPos; + }, + + equals: function(left, right) { + var reWhitespace = /\S/; + if (this.ignoreWhitespace && !reWhitespace.test(left) && !reWhitespace.test(right)) { + return true; + } else { + return left === right; + } + }, + join: function(left, right) { + return left + right; + }, + tokenize: function(value) { + return value; + } + }; + + var CharDiff = new Diff(); + + var WordDiff = new Diff(true); + var WordWithSpaceDiff = new Diff(); + WordDiff.tokenize = WordWithSpaceDiff.tokenize = function(value) { + return removeEmpty(value.split(/(\s+|\b)/)); + }; + + var CssDiff = new Diff(true); + CssDiff.tokenize = function(value) { + return removeEmpty(value.split(/([{}:;,]|\s+)/)); + }; + + var LineDiff = new Diff(); + LineDiff.tokenize = function(value) { + return value.split(/^/m); + }; + + return { + Diff: Diff, + + diffChars: function(oldStr, newStr) { return CharDiff.diff(oldStr, newStr); }, + diffWords: function(oldStr, newStr) { return WordDiff.diff(oldStr, newStr); }, + diffWordsWithSpace: function(oldStr, newStr) { return WordWithSpaceDiff.diff(oldStr, newStr); }, + diffLines: function(oldStr, newStr) { return LineDiff.diff(oldStr, newStr); }, + + diffCss: function(oldStr, newStr) { return CssDiff.diff(oldStr, newStr); }, + + createPatch: function(fileName, oldStr, newStr, oldHeader, newHeader) { + var ret = []; + + ret.push('Index: ' + fileName); + ret.push('==================================================================='); + ret.push('--- ' + fileName + (typeof oldHeader === 'undefined' ? '' : '\t' + oldHeader)); + ret.push('+++ ' + fileName + (typeof newHeader === 'undefined' ? '' : '\t' + newHeader)); + + var diff = LineDiff.diff(oldStr, newStr); + if (!diff[diff.length-1].value) { + diff.pop(); // Remove trailing newline add + } + diff.push({value: '', lines: []}); // Append an empty value to make cleanup easier + + function contextLines(lines) { + return lines.map(function(entry) { return ' ' + entry; }); + } + function eofNL(curRange, i, current) { + var last = diff[diff.length-2], + isLast = i === diff.length-2, + isLastOfType = i === diff.length-3 && (current.added !== last.added || current.removed !== last.removed); + + // Figure out if this is the last line for the given file and missing NL + if (!/\n$/.test(current.value) && (isLast || isLastOfType)) { + curRange.push('\\ No newline at end of file'); + } + } + + var oldRangeStart = 0, newRangeStart = 0, curRange = [], + oldLine = 1, newLine = 1; + for (var i = 0; i < diff.length; i++) { + var current = diff[i], + lines = current.lines || current.value.replace(/\n$/, '').split('\n'); + current.lines = lines; + + if (current.added || current.removed) { + if (!oldRangeStart) { + var prev = diff[i-1]; + oldRangeStart = oldLine; + newRangeStart = newLine; + + if (prev) { + curRange = contextLines(prev.lines.slice(-4)); + oldRangeStart -= curRange.length; + newRangeStart -= curRange.length; + } + } + curRange.push.apply(curRange, lines.map(function(entry) { return (current.added?'+':'-') + entry; })); + eofNL(curRange, i, current); + + if (current.added) { + newLine += lines.length; + } else { + oldLine += lines.length; + } + } else { + if (oldRangeStart) { + // Close out any changes that have been output (or join overlapping) + if (lines.length <= 8 && i < diff.length-2) { + // Overlapping + curRange.push.apply(curRange, contextLines(lines)); + } else { + // end the range and output + var contextSize = Math.min(lines.length, 4); + ret.push( + '@@ -' + oldRangeStart + ',' + (oldLine-oldRangeStart+contextSize) + + ' +' + newRangeStart + ',' + (newLine-newRangeStart+contextSize) + + ' @@'); + ret.push.apply(ret, curRange); + ret.push.apply(ret, contextLines(lines.slice(0, contextSize))); + if (lines.length <= 4) { + eofNL(ret, i, current); + } + + oldRangeStart = 0; newRangeStart = 0; curRange = []; + } + } + oldLine += lines.length; + newLine += lines.length; + } + } + + return ret.join('\n') + '\n'; + }, + + applyPatch: function(oldStr, uniDiff) { + var diffstr = uniDiff.split('\n'); + var diff = []; + var remEOFNL = false, + addEOFNL = false; + + for (var i = (diffstr[0][0]==='I'?4:0); i < diffstr.length; i++) { + if(diffstr[i][0] === '@') { + var meh = diffstr[i].split(/@@ -(\d+),(\d+) \+(\d+),(\d+) @@/); + diff.unshift({ + start:meh[3], + oldlength:meh[2], + oldlines:[], + newlength:meh[4], + newlines:[] + }); + } else if(diffstr[i][0] === '+') { + diff[0].newlines.push(diffstr[i].substr(1)); + } else if(diffstr[i][0] === '-') { + diff[0].oldlines.push(diffstr[i].substr(1)); + } else if(diffstr[i][0] === ' ') { + diff[0].newlines.push(diffstr[i].substr(1)); + diff[0].oldlines.push(diffstr[i].substr(1)); + } else if(diffstr[i][0] === '\\') { + if (diffstr[i-1][0] === '+') { + remEOFNL = true; + } else if(diffstr[i-1][0] === '-') { + addEOFNL = true; + } + } + } + + var str = oldStr.split('\n'); + for (var i = diff.length - 1; i >= 0; i--) { + var d = diff[i]; + for (var j = 0; j < d.oldlength; j++) { + if(str[d.start-1+j] !== d.oldlines[j]) { + return false; + } + } + Array.prototype.splice.apply(str,[d.start-1,+d.oldlength].concat(d.newlines)); + } + + if (remEOFNL) { + while (!str[str.length-1]) { + str.pop(); + } + } else if (addEOFNL) { + str.push(''); + } + return str.join('\n'); + }, + + convertChangesToXML: function(changes){ + var ret = []; + for ( var i = 0; i < changes.length; i++) { + var change = changes[i]; + if (change.added) { + ret.push(''); + } else if (change.removed) { + ret.push(''); + } + + ret.push(escapeHTML(change.value)); + + if (change.added) { + ret.push(''); + } else if (change.removed) { + ret.push(''); + } + } + return ret.join(''); + }, + + // See: http://code.google.com/p/google-diff-match-patch/wiki/API + convertChangesToDMP: function(changes){ + var ret = [], change; + for ( var i = 0; i < changes.length; i++) { + change = changes[i]; + ret.push([(change.added ? 1 : change.removed ? -1 : 0), change.value]); + } + return ret; + } + }; +})(); + +if (typeof module !== 'undefined') { + module.exports = JsDiff; +} + +}); // module: browser/diff.js + +require.register("browser/events.js", function(module, exports, require){ + +/** + * Module exports. + */ + +exports.EventEmitter = EventEmitter; + +/** + * Check if `obj` is an array. + */ + +function isArray(obj) { + return '[object Array]' == {}.toString.call(obj); +} + +/** + * Event emitter constructor. + * + * @api public + */ + +function EventEmitter(){}; + +/** + * Adds a listener. + * + * @api public + */ + +EventEmitter.prototype.on = function (name, fn) { + if (!this.$events) { + this.$events = {}; + } + + if (!this.$events[name]) { + this.$events[name] = fn; + } else if (isArray(this.$events[name])) { + this.$events[name].push(fn); + } else { + this.$events[name] = [this.$events[name], fn]; + } + + return this; +}; + +EventEmitter.prototype.addListener = EventEmitter.prototype.on; + +/** + * Adds a volatile listener. + * + * @api public + */ + +EventEmitter.prototype.once = function (name, fn) { + var self = this; + + function on () { + self.removeListener(name, on); + fn.apply(this, arguments); + }; + + on.listener = fn; + this.on(name, on); + + return this; +}; + +/** + * Removes a listener. + * + * @api public + */ + +EventEmitter.prototype.removeListener = function (name, fn) { + if (this.$events && this.$events[name]) { + var list = this.$events[name]; + + if (isArray(list)) { + var pos = -1; + + for (var i = 0, l = list.length; i < l; i++) { + if (list[i] === fn || (list[i].listener && list[i].listener === fn)) { + pos = i; + break; + } + } + + if (pos < 0) { + return this; + } + + list.splice(pos, 1); + + if (!list.length) { + delete this.$events[name]; + } + } else if (list === fn || (list.listener && list.listener === fn)) { + delete this.$events[name]; + } + } + + return this; +}; + +/** + * Removes all listeners for an event. + * + * @api public + */ + +EventEmitter.prototype.removeAllListeners = function (name) { + if (name === undefined) { + this.$events = {}; + return this; + } + + if (this.$events && this.$events[name]) { + this.$events[name] = null; + } + + return this; +}; + +/** + * Gets all listeners for a certain event. + * + * @api public + */ + +EventEmitter.prototype.listeners = function (name) { + if (!this.$events) { + this.$events = {}; + } + + if (!this.$events[name]) { + this.$events[name] = []; + } + + if (!isArray(this.$events[name])) { + this.$events[name] = [this.$events[name]]; + } + + return this.$events[name]; +}; + +/** + * Emits an event. + * + * @api public + */ + +EventEmitter.prototype.emit = function (name) { + if (!this.$events) { + return false; + } + + var handler = this.$events[name]; + + if (!handler) { + return false; + } + + var args = [].slice.call(arguments, 1); + + if ('function' == typeof handler) { + handler.apply(this, args); + } else if (isArray(handler)) { + var listeners = handler.slice(); + + for (var i = 0, l = listeners.length; i < l; i++) { + listeners[i].apply(this, args); + } + } else { + return false; + } + + return true; +}; +}); // module: browser/events.js + +require.register("browser/fs.js", function(module, exports, require){ + +}); // module: browser/fs.js + +require.register("browser/path.js", function(module, exports, require){ + +}); // module: browser/path.js + +require.register("browser/progress.js", function(module, exports, require){ +/** + * Expose `Progress`. + */ + +module.exports = Progress; + +/** + * Initialize a new `Progress` indicator. + */ + +function Progress() { + this.percent = 0; + this.size(0); + this.fontSize(11); + this.font('helvetica, arial, sans-serif'); +} + +/** + * Set progress size to `n`. + * + * @param {Number} n + * @return {Progress} for chaining + * @api public + */ + +Progress.prototype.size = function(n){ + this._size = n; + return this; +}; + +/** + * Set text to `str`. + * + * @param {String} str + * @return {Progress} for chaining + * @api public + */ + +Progress.prototype.text = function(str){ + this._text = str; + return this; +}; + +/** + * Set font size to `n`. + * + * @param {Number} n + * @return {Progress} for chaining + * @api public + */ + +Progress.prototype.fontSize = function(n){ + this._fontSize = n; + return this; +}; + +/** + * Set font `family`. + * + * @param {String} family + * @return {Progress} for chaining + */ + +Progress.prototype.font = function(family){ + this._font = family; + return this; +}; + +/** + * Update percentage to `n`. + * + * @param {Number} n + * @return {Progress} for chaining + */ + +Progress.prototype.update = function(n){ + this.percent = n; + return this; +}; + +/** + * Draw on `ctx`. + * + * @param {CanvasRenderingContext2d} ctx + * @return {Progress} for chaining + */ + +Progress.prototype.draw = function(ctx){ + try { + var percent = Math.min(this.percent, 100) + , size = this._size + , half = size / 2 + , x = half + , y = half + , rad = half - 1 + , fontSize = this._fontSize; + + ctx.font = fontSize + 'px ' + this._font; + + var angle = Math.PI * 2 * (percent / 100); + ctx.clearRect(0, 0, size, size); + + // outer circle + ctx.strokeStyle = '#9f9f9f'; + ctx.beginPath(); + ctx.arc(x, y, rad, 0, angle, false); + ctx.stroke(); + + // inner circle + ctx.strokeStyle = '#eee'; + ctx.beginPath(); + ctx.arc(x, y, rad - 1, 0, angle, true); + ctx.stroke(); + + // text + var text = this._text || (percent | 0) + '%' + , w = ctx.measureText(text).width; + + ctx.fillText( + text + , x - w / 2 + 1 + , y + fontSize / 2 - 1); + } catch (ex) {} //don't fail if we can't render progress + return this; +}; + +}); // module: browser/progress.js + +require.register("browser/tty.js", function(module, exports, require){ + +exports.isatty = function(){ + return true; +}; + +exports.getWindowSize = function(){ + if ('innerHeight' in global) { + return [global.innerHeight, global.innerWidth]; + } else { + // In a Web Worker, the DOM Window is not available. + return [640, 480]; + } +}; + +}); // module: browser/tty.js + +require.register("context.js", function(module, exports, require){ + +/** + * Expose `Context`. + */ + +module.exports = Context; + +/** + * Initialize a new `Context`. + * + * @api private + */ + +function Context(){} + +/** + * Set or get the context `Runnable` to `runnable`. + * + * @param {Runnable} runnable + * @return {Context} + * @api private + */ + +Context.prototype.runnable = function(runnable){ + if (0 == arguments.length) return this._runnable; + this.test = this._runnable = runnable; + return this; +}; + +/** + * Set test timeout `ms`. + * + * @param {Number} ms + * @return {Context} self + * @api private + */ + +Context.prototype.timeout = function(ms){ + if (arguments.length === 0) return this.runnable().timeout(); + this.runnable().timeout(ms); + return this; +}; + +/** + * Set test timeout `enabled`. + * + * @param {Boolean} enabled + * @return {Context} self + * @api private + */ + +Context.prototype.enableTimeouts = function (enabled) { + this.runnable().enableTimeouts(enabled); + return this; +}; + + +/** + * Set test slowness threshold `ms`. + * + * @param {Number} ms + * @return {Context} self + * @api private + */ + +Context.prototype.slow = function(ms){ + this.runnable().slow(ms); + return this; +}; + +/** + * Inspect the context void of `._runnable`. + * + * @return {String} + * @api private + */ + +Context.prototype.inspect = function(){ + return JSON.stringify(this, function(key, val){ + if ('_runnable' == key) return; + if ('test' == key) return; + return val; + }, 2); +}; + +}); // module: context.js + +require.register("hook.js", function(module, exports, require){ + +/** + * Module dependencies. + */ + +var Runnable = require('./runnable'); + +/** + * Expose `Hook`. + */ + +module.exports = Hook; + +/** + * Initialize a new `Hook` with the given `title` and callback `fn`. + * + * @param {String} title + * @param {Function} fn + * @api private + */ + +function Hook(title, fn) { + Runnable.call(this, title, fn); + this.type = 'hook'; +} + +/** + * Inherit from `Runnable.prototype`. + */ + +function F(){}; +F.prototype = Runnable.prototype; +Hook.prototype = new F; +Hook.prototype.constructor = Hook; + + +/** + * Get or set the test `err`. + * + * @param {Error} err + * @return {Error} + * @api public + */ + +Hook.prototype.error = function(err){ + if (0 == arguments.length) { + var err = this._error; + this._error = null; + return err; + } + + this._error = err; +}; + +}); // module: hook.js + +require.register("interfaces/bdd.js", function(module, exports, require){ + +/** + * Module dependencies. + */ + +var Suite = require('../suite') + , Test = require('../test') + , utils = require('../utils'); + +/** + * BDD-style interface: + * + * describe('Array', function(){ + * describe('#indexOf()', function(){ + * it('should return -1 when not present', function(){ + * + * }); + * + * it('should return the index when present', function(){ + * + * }); + * }); + * }); + * + */ + +module.exports = function(suite){ + var suites = [suite]; + + suite.on('pre-require', function(context, file, mocha){ + + /** + * Execute before running tests. + */ + + context.before = function(name, fn){ + suites[0].beforeAll(name, fn); + }; + + /** + * Execute after running tests. + */ + + context.after = function(name, fn){ + suites[0].afterAll(name, fn); + }; + + /** + * Execute before each test case. + */ + + context.beforeEach = function(name, fn){ + suites[0].beforeEach(name, fn); + }; + + /** + * Execute after each test case. + */ + + context.afterEach = function(name, fn){ + suites[0].afterEach(name, fn); + }; + + /** + * Describe a "suite" with the given `title` + * and callback `fn` containing nested suites + * and/or tests. + */ + + context.describe = context.context = function(title, fn){ + var suite = Suite.create(suites[0], title); + suite.file = file; + suites.unshift(suite); + fn.call(suite); + suites.shift(); + return suite; + }; + + /** + * Pending describe. + */ + + context.xdescribe = + context.xcontext = + context.describe.skip = function(title, fn){ + var suite = Suite.create(suites[0], title); + suite.pending = true; + suites.unshift(suite); + fn.call(suite); + suites.shift(); + }; + + /** + * Exclusive suite. + */ + + context.describe.only = function(title, fn){ + var suite = context.describe(title, fn); + mocha.grep(suite.fullTitle()); + return suite; + }; + + /** + * Describe a specification or test-case + * with the given `title` and callback `fn` + * acting as a thunk. + */ + + context.it = context.specify = function(title, fn){ + var suite = suites[0]; + if (suite.pending) var fn = null; + var test = new Test(title, fn); + test.file = file; + suite.addTest(test); + return test; + }; + + /** + * Exclusive test-case. + */ + + context.it.only = function(title, fn){ + var test = context.it(title, fn); + var reString = '^' + utils.escapeRegexp(test.fullTitle()) + '$'; + mocha.grep(new RegExp(reString)); + return test; + }; + + /** + * Pending test case. + */ + + context.xit = + context.xspecify = + context.it.skip = function(title){ + context.it(title); + }; + }); +}; + +}); // module: interfaces/bdd.js + +require.register("interfaces/exports.js", function(module, exports, require){ + +/** + * Module dependencies. + */ + +var Suite = require('../suite') + , Test = require('../test'); + +/** + * TDD-style interface: + * + * exports.Array = { + * '#indexOf()': { + * 'should return -1 when the value is not present': function(){ + * + * }, + * + * 'should return the correct index when the value is present': function(){ + * + * } + * } + * }; + * + */ + +module.exports = function(suite){ + var suites = [suite]; + + suite.on('require', visit); + + function visit(obj, file) { + var suite; + for (var key in obj) { + if ('function' == typeof obj[key]) { + var fn = obj[key]; + switch (key) { + case 'before': + suites[0].beforeAll(fn); + break; + case 'after': + suites[0].afterAll(fn); + break; + case 'beforeEach': + suites[0].beforeEach(fn); + break; + case 'afterEach': + suites[0].afterEach(fn); + break; + default: + var test = new Test(key, fn); + test.file = file; + suites[0].addTest(test); + } + } else { + var suite = Suite.create(suites[0], key); + suites.unshift(suite); + visit(obj[key]); + suites.shift(); + } + } + } +}; + +}); // module: interfaces/exports.js + +require.register("interfaces/index.js", function(module, exports, require){ + +exports.bdd = require('./bdd'); +exports.tdd = require('./tdd'); +exports.qunit = require('./qunit'); +exports.exports = require('./exports'); + +}); // module: interfaces/index.js + +require.register("interfaces/qunit.js", function(module, exports, require){ + +/** + * Module dependencies. + */ + +var Suite = require('../suite') + , Test = require('../test') + , utils = require('../utils'); + +/** + * QUnit-style interface: + * + * suite('Array'); + * + * test('#length', function(){ + * var arr = [1,2,3]; + * ok(arr.length == 3); + * }); + * + * test('#indexOf()', function(){ + * var arr = [1,2,3]; + * ok(arr.indexOf(1) == 0); + * ok(arr.indexOf(2) == 1); + * ok(arr.indexOf(3) == 2); + * }); + * + * suite('String'); + * + * test('#length', function(){ + * ok('foo'.length == 3); + * }); + * + */ + +module.exports = function(suite){ + var suites = [suite]; + + suite.on('pre-require', function(context, file, mocha){ + + /** + * Execute before running tests. + */ + + context.before = function(name, fn){ + suites[0].beforeAll(name, fn); + }; + + /** + * Execute after running tests. + */ + + context.after = function(name, fn){ + suites[0].afterAll(name, fn); + }; + + /** + * Execute before each test case. + */ + + context.beforeEach = function(name, fn){ + suites[0].beforeEach(name, fn); + }; + + /** + * Execute after each test case. + */ + + context.afterEach = function(name, fn){ + suites[0].afterEach(name, fn); + }; + + /** + * Describe a "suite" with the given `title`. + */ + + context.suite = function(title){ + if (suites.length > 1) suites.shift(); + var suite = Suite.create(suites[0], title); + suite.file = file; + suites.unshift(suite); + return suite; + }; + + /** + * Exclusive test-case. + */ + + context.suite.only = function(title, fn){ + var suite = context.suite(title, fn); + mocha.grep(suite.fullTitle()); + }; + + /** + * Describe a specification or test-case + * with the given `title` and callback `fn` + * acting as a thunk. + */ + + context.test = function(title, fn){ + var test = new Test(title, fn); + test.file = file; + suites[0].addTest(test); + return test; + }; + + /** + * Exclusive test-case. + */ + + context.test.only = function(title, fn){ + var test = context.test(title, fn); + var reString = '^' + utils.escapeRegexp(test.fullTitle()) + '$'; + mocha.grep(new RegExp(reString)); + }; + + /** + * Pending test case. + */ + + context.test.skip = function(title){ + context.test(title); + }; + }); +}; + +}); // module: interfaces/qunit.js + +require.register("interfaces/tdd.js", function(module, exports, require){ + +/** + * Module dependencies. + */ + +var Suite = require('../suite') + , Test = require('../test') + , utils = require('../utils');; + +/** + * TDD-style interface: + * + * suite('Array', function(){ + * suite('#indexOf()', function(){ + * suiteSetup(function(){ + * + * }); + * + * test('should return -1 when not present', function(){ + * + * }); + * + * test('should return the index when present', function(){ + * + * }); + * + * suiteTeardown(function(){ + * + * }); + * }); + * }); + * + */ + +module.exports = function(suite){ + var suites = [suite]; + + suite.on('pre-require', function(context, file, mocha){ + + /** + * Execute before each test case. + */ + + context.setup = function(name, fn){ + suites[0].beforeEach(name, fn); + }; + + /** + * Execute after each test case. + */ + + context.teardown = function(name, fn){ + suites[0].afterEach(name, fn); + }; + + /** + * Execute before the suite. + */ + + context.suiteSetup = function(name, fn){ + suites[0].beforeAll(name, fn); + }; + + /** + * Execute after the suite. + */ + + context.suiteTeardown = function(name, fn){ + suites[0].afterAll(name, fn); + }; + + /** + * Describe a "suite" with the given `title` + * and callback `fn` containing nested suites + * and/or tests. + */ + + context.suite = function(title, fn){ + var suite = Suite.create(suites[0], title); + suite.file = file; + suites.unshift(suite); + fn.call(suite); + suites.shift(); + return suite; + }; + + /** + * Pending suite. + */ + context.suite.skip = function(title, fn) { + var suite = Suite.create(suites[0], title); + suite.pending = true; + suites.unshift(suite); + fn.call(suite); + suites.shift(); + }; + + /** + * Exclusive test-case. + */ + + context.suite.only = function(title, fn){ + var suite = context.suite(title, fn); + mocha.grep(suite.fullTitle()); + }; + + /** + * Describe a specification or test-case + * with the given `title` and callback `fn` + * acting as a thunk. + */ + + context.test = function(title, fn){ + var suite = suites[0]; + if (suite.pending) var fn = null; + var test = new Test(title, fn); + test.file = file; + suite.addTest(test); + return test; + }; + + /** + * Exclusive test-case. + */ + + context.test.only = function(title, fn){ + var test = context.test(title, fn); + var reString = '^' + utils.escapeRegexp(test.fullTitle()) + '$'; + mocha.grep(new RegExp(reString)); + }; + + /** + * Pending test case. + */ + + context.test.skip = function(title){ + context.test(title); + }; + }); +}; + +}); // module: interfaces/tdd.js + +require.register("mocha.js", function(module, exports, require){ +/*! + * mocha + * Copyright(c) 2011 TJ Holowaychuk + * MIT Licensed + */ + +/** + * Module dependencies. + */ + +var path = require('browser/path') + , utils = require('./utils'); + +/** + * Expose `Mocha`. + */ + +exports = module.exports = Mocha; + +/** + * To require local UIs and reporters when running in node. + */ + +if (typeof process !== 'undefined' && typeof process.cwd === 'function') { + var join = path.join + , cwd = process.cwd(); + module.paths.push(cwd, join(cwd, 'node_modules')); +} + +/** + * Expose internals. + */ + +exports.utils = utils; +exports.interfaces = require('./interfaces'); +exports.reporters = require('./reporters'); +exports.Runnable = require('./runnable'); +exports.Context = require('./context'); +exports.Runner = require('./runner'); +exports.Suite = require('./suite'); +exports.Hook = require('./hook'); +exports.Test = require('./test'); + +/** + * Return image `name` path. + * + * @param {String} name + * @return {String} + * @api private + */ + +function image(name) { + return __dirname + '/../images/' + name + '.png'; +} + +/** + * Setup mocha with `options`. + * + * Options: + * + * - `ui` name "bdd", "tdd", "exports" etc + * - `reporter` reporter instance, defaults to `mocha.reporters.spec` + * - `globals` array of accepted globals + * - `timeout` timeout in milliseconds + * - `bail` bail on the first test failure + * - `slow` milliseconds to wait before considering a test slow + * - `ignoreLeaks` ignore global leaks + * - `grep` string or regexp to filter tests with + * + * @param {Object} options + * @api public + */ + +function Mocha(options) { + options = options || {}; + this.files = []; + this.options = options; + this.grep(options.grep); + this.suite = new exports.Suite('', new exports.Context); + this.ui(options.ui); + this.bail(options.bail); + this.reporter(options.reporter); + if (null != options.timeout) this.timeout(options.timeout); + this.useColors(options.useColors) + if (options.enableTimeouts !== null) this.enableTimeouts(options.enableTimeouts); + if (options.slow) this.slow(options.slow); + + this.suite.on('pre-require', function (context) { + exports.afterEach = context.afterEach || context.teardown; + exports.after = context.after || context.suiteTeardown; + exports.beforeEach = context.beforeEach || context.setup; + exports.before = context.before || context.suiteSetup; + exports.describe = context.describe || context.suite; + exports.it = context.it || context.test; + exports.setup = context.setup || context.beforeEach; + exports.suiteSetup = context.suiteSetup || context.before; + exports.suiteTeardown = context.suiteTeardown || context.after; + exports.suite = context.suite || context.describe; + exports.teardown = context.teardown || context.afterEach; + exports.test = context.test || context.it; + }); +} + +/** + * Enable or disable bailing on the first failure. + * + * @param {Boolean} [bail] + * @api public + */ + +Mocha.prototype.bail = function(bail){ + if (0 == arguments.length) bail = true; + this.suite.bail(bail); + return this; +}; + +/** + * Add test `file`. + * + * @param {String} file + * @api public + */ + +Mocha.prototype.addFile = function(file){ + this.files.push(file); + return this; +}; + +/** + * Set reporter to `reporter`, defaults to "spec". + * + * @param {String|Function} reporter name or constructor + * @api public + */ + +Mocha.prototype.reporter = function(reporter){ + if ('function' == typeof reporter) { + this._reporter = reporter; + } else { + reporter = reporter || 'spec'; + var _reporter; + try { _reporter = require('./reporters/' + reporter); } catch (err) {}; + if (!_reporter) try { _reporter = require(reporter); } catch (err) {}; + if (!_reporter && reporter === 'teamcity') + console.warn('The Teamcity reporter was moved to a package named ' + + 'mocha-teamcity-reporter ' + + '(https://npmjs.org/package/mocha-teamcity-reporter).'); + if (!_reporter) throw new Error('invalid reporter "' + reporter + '"'); + this._reporter = _reporter; + } + return this; +}; + +/** + * Set test UI `name`, defaults to "bdd". + * + * @param {String} bdd + * @api public + */ + +Mocha.prototype.ui = function(name){ + name = name || 'bdd'; + this._ui = exports.interfaces[name]; + if (!this._ui) try { this._ui = require(name); } catch (err) {}; + if (!this._ui) throw new Error('invalid interface "' + name + '"'); + this._ui = this._ui(this.suite); + return this; +}; + +/** + * Load registered files. + * + * @api private + */ + +Mocha.prototype.loadFiles = function(fn){ + var self = this; + var suite = this.suite; + var pending = this.files.length; + this.files.forEach(function(file){ + file = path.resolve(file); + suite.emit('pre-require', global, file, self); + suite.emit('require', require(file), file, self); + suite.emit('post-require', global, file, self); + --pending || (fn && fn()); + }); +}; + +/** + * Enable growl support. + * + * @api private + */ + +Mocha.prototype._growl = function(runner, reporter) { + var notify = require('growl'); + + runner.on('end', function(){ + var stats = reporter.stats; + if (stats.failures) { + var msg = stats.failures + ' of ' + runner.total + ' tests failed'; + notify(msg, { name: 'mocha', title: 'Failed', image: image('error') }); + } else { + notify(stats.passes + ' tests passed in ' + stats.duration + 'ms', { + name: 'mocha' + , title: 'Passed' + , image: image('ok') + }); + } + }); +}; + +/** + * Add regexp to grep, if `re` is a string it is escaped. + * + * @param {RegExp|String} re + * @return {Mocha} + * @api public + */ + +Mocha.prototype.grep = function(re){ + this.options.grep = 'string' == typeof re + ? new RegExp(utils.escapeRegexp(re)) + : re; + return this; +}; + +/** + * Invert `.grep()` matches. + * + * @return {Mocha} + * @api public + */ + +Mocha.prototype.invert = function(){ + this.options.invert = true; + return this; +}; + +/** + * Ignore global leaks. + * + * @param {Boolean} ignore + * @return {Mocha} + * @api public + */ + +Mocha.prototype.ignoreLeaks = function(ignore){ + this.options.ignoreLeaks = !!ignore; + return this; +}; + +/** + * Enable global leak checking. + * + * @return {Mocha} + * @api public + */ + +Mocha.prototype.checkLeaks = function(){ + this.options.ignoreLeaks = false; + return this; +}; + +/** + * Enable growl support. + * + * @return {Mocha} + * @api public + */ + +Mocha.prototype.growl = function(){ + this.options.growl = true; + return this; +}; + +/** + * Ignore `globals` array or string. + * + * @param {Array|String} globals + * @return {Mocha} + * @api public + */ + +Mocha.prototype.globals = function(globals){ + this.options.globals = (this.options.globals || []).concat(globals); + return this; +}; + +/** + * Emit color output. + * + * @param {Boolean} colors + * @return {Mocha} + * @api public + */ + +Mocha.prototype.useColors = function(colors){ + this.options.useColors = arguments.length && colors != undefined + ? colors + : true; + return this; +}; + +/** + * Use inline diffs rather than +/-. + * + * @param {Boolean} inlineDiffs + * @return {Mocha} + * @api public + */ + +Mocha.prototype.useInlineDiffs = function(inlineDiffs) { + this.options.useInlineDiffs = arguments.length && inlineDiffs != undefined + ? inlineDiffs + : false; + return this; +}; + +/** + * Set the timeout in milliseconds. + * + * @param {Number} timeout + * @return {Mocha} + * @api public + */ + +Mocha.prototype.timeout = function(timeout){ + this.suite.timeout(timeout); + return this; +}; + +/** + * Set slowness threshold in milliseconds. + * + * @param {Number} slow + * @return {Mocha} + * @api public + */ + +Mocha.prototype.slow = function(slow){ + this.suite.slow(slow); + return this; +}; + +/** + * Enable timeouts. + * + * @param {Boolean} enabled + * @return {Mocha} + * @api public + */ + +Mocha.prototype.enableTimeouts = function(enabled) { + this.suite.enableTimeouts(arguments.length && enabled !== undefined + ? enabled + : true); + return this +}; + +/** + * Makes all tests async (accepting a callback) + * + * @return {Mocha} + * @api public + */ + +Mocha.prototype.asyncOnly = function(){ + this.options.asyncOnly = true; + return this; +}; + +/** + * Run tests and invoke `fn()` when complete. + * + * @param {Function} fn + * @return {Runner} + * @api public + */ + +Mocha.prototype.run = function(fn){ + if (this.files.length) this.loadFiles(); + var suite = this.suite; + var options = this.options; + options.files = this.files; + var runner = new exports.Runner(suite); + var reporter = new this._reporter(runner, options); + runner.ignoreLeaks = false !== options.ignoreLeaks; + runner.asyncOnly = options.asyncOnly; + if (options.grep) runner.grep(options.grep, options.invert); + if (options.globals) runner.globals(options.globals); + if (options.growl) this._growl(runner, reporter); + exports.reporters.Base.useColors = options.useColors; + exports.reporters.Base.inlineDiffs = options.useInlineDiffs; + return runner.run(fn); +}; + +}); // module: mocha.js + +require.register("ms.js", function(module, exports, require){ +/** + * Helpers. + */ + +var s = 1000; +var m = s * 60; +var h = m * 60; +var d = h * 24; +var y = d * 365.25; + +/** + * Parse or format the given `val`. + * + * Options: + * + * - `long` verbose formatting [false] + * + * @param {String|Number} val + * @param {Object} options + * @return {String|Number} + * @api public + */ + +module.exports = function(val, options){ + options = options || {}; + if ('string' == typeof val) return parse(val); + return options.long ? longFormat(val) : shortFormat(val); +}; + +/** + * Parse the given `str` and return milliseconds. + * + * @param {String} str + * @return {Number} + * @api private + */ + +function parse(str) { + var match = /^((?:\d+)?\.?\d+) *(ms|seconds?|s|minutes?|m|hours?|h|days?|d|years?|y)?$/i.exec(str); + if (!match) return; + var n = parseFloat(match[1]); + var type = (match[2] || 'ms').toLowerCase(); + switch (type) { + case 'years': + case 'year': + case 'y': + return n * y; + case 'days': + case 'day': + case 'd': + return n * d; + case 'hours': + case 'hour': + case 'h': + return n * h; + case 'minutes': + case 'minute': + case 'm': + return n * m; + case 'seconds': + case 'second': + case 's': + return n * s; + case 'ms': + return n; + } +} + +/** + * Short format for `ms`. + * + * @param {Number} ms + * @return {String} + * @api private + */ + +function shortFormat(ms) { + if (ms >= d) return Math.round(ms / d) + 'd'; + if (ms >= h) return Math.round(ms / h) + 'h'; + if (ms >= m) return Math.round(ms / m) + 'm'; + if (ms >= s) return Math.round(ms / s) + 's'; + return ms + 'ms'; +} + +/** + * Long format for `ms`. + * + * @param {Number} ms + * @return {String} + * @api private + */ + +function longFormat(ms) { + return plural(ms, d, 'day') + || plural(ms, h, 'hour') + || plural(ms, m, 'minute') + || plural(ms, s, 'second') + || ms + ' ms'; +} + +/** + * Pluralization helper. + */ + +function plural(ms, n, name) { + if (ms < n) return; + if (ms < n * 1.5) return Math.floor(ms / n) + ' ' + name; + return Math.ceil(ms / n) + ' ' + name + 's'; +} + +}); // module: ms.js + +require.register("reporters/base.js", function(module, exports, require){ + +/** + * Module dependencies. + */ + +var tty = require('browser/tty') + , diff = require('browser/diff') + , ms = require('../ms') + , utils = require('../utils'); + +/** + * Save timer references to avoid Sinon interfering (see GH-237). + */ + +var Date = global.Date + , setTimeout = global.setTimeout + , setInterval = global.setInterval + , clearTimeout = global.clearTimeout + , clearInterval = global.clearInterval; + +/** + * Check if both stdio streams are associated with a tty. + */ + +var isatty = tty.isatty(1) && tty.isatty(2); + +/** + * Expose `Base`. + */ + +exports = module.exports = Base; + +/** + * Enable coloring by default. + */ + +exports.useColors = isatty || (process.env.MOCHA_COLORS !== undefined); + +/** + * Inline diffs instead of +/- + */ + +exports.inlineDiffs = false; + +/** + * Default color map. + */ + +exports.colors = { + 'pass': 90 + , 'fail': 31 + , 'bright pass': 92 + , 'bright fail': 91 + , 'bright yellow': 93 + , 'pending': 36 + , 'suite': 0 + , 'error title': 0 + , 'error message': 31 + , 'error stack': 90 + , 'checkmark': 32 + , 'fast': 90 + , 'medium': 33 + , 'slow': 31 + , 'green': 32 + , 'light': 90 + , 'diff gutter': 90 + , 'diff added': 42 + , 'diff removed': 41 +}; + +/** + * Default symbol map. + */ + +exports.symbols = { + ok: '✓', + err: '✖', + dot: '․' +}; + +// With node.js on Windows: use symbols available in terminal default fonts +if ('win32' == process.platform) { + exports.symbols.ok = '\u221A'; + exports.symbols.err = '\u00D7'; + exports.symbols.dot = '.'; +} + +/** + * Color `str` with the given `type`, + * allowing colors to be disabled, + * as well as user-defined color + * schemes. + * + * @param {String} type + * @param {String} str + * @return {String} + * @api private + */ + +var color = exports.color = function(type, str) { + if (!exports.useColors) return str; + return '\u001b[' + exports.colors[type] + 'm' + str + '\u001b[0m'; +}; + +/** + * Expose term window size, with some + * defaults for when stderr is not a tty. + */ + +exports.window = { + width: isatty + ? process.stdout.getWindowSize + ? process.stdout.getWindowSize(1)[0] + : tty.getWindowSize()[1] + : 75 +}; + +/** + * Expose some basic cursor interactions + * that are common among reporters. + */ + +exports.cursor = { + hide: function(){ + isatty && process.stdout.write('\u001b[?25l'); + }, + + show: function(){ + isatty && process.stdout.write('\u001b[?25h'); + }, + + deleteLine: function(){ + isatty && process.stdout.write('\u001b[2K'); + }, + + beginningOfLine: function(){ + isatty && process.stdout.write('\u001b[0G'); + }, + + CR: function(){ + if (isatty) { + exports.cursor.deleteLine(); + exports.cursor.beginningOfLine(); + } else { + process.stdout.write('\r'); + } + } +}; + +/** + * Outut the given `failures` as a list. + * + * @param {Array} failures + * @api public + */ + +exports.list = function(failures){ + console.error(); + failures.forEach(function(test, i){ + // format + var fmt = color('error title', ' %s) %s:\n') + + color('error message', ' %s') + + color('error stack', '\n%s\n'); + + // msg + var err = test.err + , message = err.message || '' + , stack = err.stack || message + , index = stack.indexOf(message) + message.length + , msg = stack.slice(0, index) + , actual = err.actual + , expected = err.expected + , escape = true; + + // uncaught + if (err.uncaught) { + msg = 'Uncaught ' + msg; + } + + // explicitly show diff + if (err.showDiff && sameType(actual, expected)) { + escape = false; + err.actual = actual = utils.stringify(actual); + err.expected = expected = utils.stringify(expected); + } + + // actual / expected diff + if ('string' == typeof actual && 'string' == typeof expected) { + fmt = color('error title', ' %s) %s:\n%s') + color('error stack', '\n%s\n'); + var match = message.match(/^([^:]+): expected/); + msg = '\n ' + color('error message', match ? match[1] : msg); + + if (exports.inlineDiffs) { + msg += inlineDiff(err, escape); + } else { + msg += unifiedDiff(err, escape); + } + } + + // indent stack trace without msg + stack = stack.slice(index ? index + 1 : index) + .replace(/^/gm, ' '); + + console.error(fmt, (i + 1), test.fullTitle(), msg, stack); + }); +}; + +/** + * Initialize a new `Base` reporter. + * + * All other reporters generally + * inherit from this reporter, providing + * stats such as test duration, number + * of tests passed / failed etc. + * + * @param {Runner} runner + * @api public + */ + +function Base(runner) { + var self = this + , stats = this.stats = { suites: 0, tests: 0, passes: 0, pending: 0, failures: 0 } + , failures = this.failures = []; + + if (!runner) return; + this.runner = runner; + + runner.stats = stats; + + runner.on('start', function(){ + stats.start = new Date; + }); + + runner.on('suite', function(suite){ + stats.suites = stats.suites || 0; + suite.root || stats.suites++; + }); + + runner.on('test end', function(test){ + stats.tests = stats.tests || 0; + stats.tests++; + }); + + runner.on('pass', function(test){ + stats.passes = stats.passes || 0; + + var medium = test.slow() / 2; + test.speed = test.duration > test.slow() + ? 'slow' + : test.duration > medium + ? 'medium' + : 'fast'; + + stats.passes++; + }); + + runner.on('fail', function(test, err){ + stats.failures = stats.failures || 0; + stats.failures++; + test.err = err; + failures.push(test); + }); + + runner.on('end', function(){ + stats.end = new Date; + stats.duration = new Date - stats.start; + }); + + runner.on('pending', function(){ + stats.pending++; + }); +} + +/** + * Output common epilogue used by many of + * the bundled reporters. + * + * @api public + */ + +Base.prototype.epilogue = function(){ + var stats = this.stats; + var tests; + var fmt; + + console.log(); + + // passes + fmt = color('bright pass', ' ') + + color('green', ' %d passing') + + color('light', ' (%s)'); + + console.log(fmt, + stats.passes || 0, + ms(stats.duration)); + + // pending + if (stats.pending) { + fmt = color('pending', ' ') + + color('pending', ' %d pending'); + + console.log(fmt, stats.pending); + } + + // failures + if (stats.failures) { + fmt = color('fail', ' %d failing'); + + console.error(fmt, + stats.failures); + + Base.list(this.failures); + console.error(); + } + + console.log(); +}; + +/** + * Pad the given `str` to `len`. + * + * @param {String} str + * @param {String} len + * @return {String} + * @api private + */ + +function pad(str, len) { + str = String(str); + return Array(len - str.length + 1).join(' ') + str; +} + + +/** + * Returns an inline diff between 2 strings with coloured ANSI output + * + * @param {Error} Error with actual/expected + * @return {String} Diff + * @api private + */ + +function inlineDiff(err, escape) { + var msg = errorDiff(err, 'WordsWithSpace', escape); + + // linenos + var lines = msg.split('\n'); + if (lines.length > 4) { + var width = String(lines.length).length; + msg = lines.map(function(str, i){ + return pad(++i, width) + ' |' + ' ' + str; + }).join('\n'); + } + + // legend + msg = '\n' + + color('diff removed', 'actual') + + ' ' + + color('diff added', 'expected') + + '\n\n' + + msg + + '\n'; + + // indent + msg = msg.replace(/^/gm, ' '); + return msg; +} + +/** + * Returns a unified diff between 2 strings + * + * @param {Error} Error with actual/expected + * @return {String} Diff + * @api private + */ + +function unifiedDiff(err, escape) { + var indent = ' '; + function cleanUp(line) { + if (escape) { + line = escapeInvisibles(line); + } + if (line[0] === '+') return indent + colorLines('diff added', line); + if (line[0] === '-') return indent + colorLines('diff removed', line); + if (line.match(/\@\@/)) return null; + if (line.match(/\\ No newline/)) return null; + else return indent + line; + } + function notBlank(line) { + return line != null; + } + msg = diff.createPatch('string', err.actual, err.expected); + var lines = msg.split('\n').splice(4); + return '\n ' + + colorLines('diff added', '+ expected') + ' ' + + colorLines('diff removed', '- actual') + + '\n\n' + + lines.map(cleanUp).filter(notBlank).join('\n'); +} + +/** + * Return a character diff for `err`. + * + * @param {Error} err + * @return {String} + * @api private + */ + +function errorDiff(err, type, escape) { + var actual = escape ? escapeInvisibles(err.actual) : err.actual; + var expected = escape ? escapeInvisibles(err.expected) : err.expected; + return diff['diff' + type](actual, expected).map(function(str){ + if (str.added) return colorLines('diff added', str.value); + if (str.removed) return colorLines('diff removed', str.value); + return str.value; + }).join(''); +} + +/** + * Returns a string with all invisible characters in plain text + * + * @param {String} line + * @return {String} + * @api private + */ +function escapeInvisibles(line) { + return line.replace(/\t/g, '') + .replace(/\r/g, '') + .replace(/\n/g, '\n'); +} + +/** + * Color lines for `str`, using the color `name`. + * + * @param {String} name + * @param {String} str + * @return {String} + * @api private + */ + +function colorLines(name, str) { + return str.split('\n').map(function(str){ + return color(name, str); + }).join('\n'); +} + +/** + * Check that a / b have the same type. + * + * @param {Object} a + * @param {Object} b + * @return {Boolean} + * @api private + */ + +function sameType(a, b) { + a = Object.prototype.toString.call(a); + b = Object.prototype.toString.call(b); + return a == b; +} + +}); // module: reporters/base.js + +require.register("reporters/doc.js", function(module, exports, require){ + +/** + * Module dependencies. + */ + +var Base = require('./base') + , utils = require('../utils'); + +/** + * Expose `Doc`. + */ + +exports = module.exports = Doc; + +/** + * Initialize a new `Doc` reporter. + * + * @param {Runner} runner + * @api public + */ + +function Doc(runner) { + Base.call(this, runner); + + var self = this + , stats = this.stats + , total = runner.total + , indents = 2; + + function indent() { + return Array(indents).join(' '); + } + + runner.on('suite', function(suite){ + if (suite.root) return; + ++indents; + console.log('%s
', indent()); + ++indents; + console.log('%s

%s

', indent(), utils.escape(suite.title)); + console.log('%s
', indent()); + }); + + runner.on('suite end', function(suite){ + if (suite.root) return; + console.log('%s
', indent()); + --indents; + console.log('%s
', indent()); + --indents; + }); + + runner.on('pass', function(test){ + console.log('%s
%s
', indent(), utils.escape(test.title)); + var code = utils.escape(utils.clean(test.fn.toString())); + console.log('%s
%s
', indent(), code); + }); + + runner.on('fail', function(test, err){ + console.log('%s
%s
', indent(), utils.escape(test.title)); + var code = utils.escape(utils.clean(test.fn.toString())); + console.log('%s
%s
', indent(), code); + console.log('%s
%s
', indent(), utils.escape(err)); + }); +} + +}); // module: reporters/doc.js + +require.register("reporters/dot.js", function(module, exports, require){ + +/** + * Module dependencies. + */ + +var Base = require('./base') + , color = Base.color; + +/** + * Expose `Dot`. + */ + +exports = module.exports = Dot; + +/** + * Initialize a new `Dot` matrix test reporter. + * + * @param {Runner} runner + * @api public + */ + +function Dot(runner) { + Base.call(this, runner); + + var self = this + , stats = this.stats + , width = Base.window.width * .75 | 0 + , n = -1; + + runner.on('start', function(){ + process.stdout.write('\n '); + }); + + runner.on('pending', function(test){ + if (++n % width == 0) process.stdout.write('\n '); + process.stdout.write(color('pending', Base.symbols.dot)); + }); + + runner.on('pass', function(test){ + if (++n % width == 0) process.stdout.write('\n '); + if ('slow' == test.speed) { + process.stdout.write(color('bright yellow', Base.symbols.dot)); + } else { + process.stdout.write(color(test.speed, Base.symbols.dot)); + } + }); + + runner.on('fail', function(test, err){ + if (++n % width == 0) process.stdout.write('\n '); + process.stdout.write(color('fail', Base.symbols.dot)); + }); + + runner.on('end', function(){ + console.log(); + self.epilogue(); + }); +} + +/** + * Inherit from `Base.prototype`. + */ + +function F(){}; +F.prototype = Base.prototype; +Dot.prototype = new F; +Dot.prototype.constructor = Dot; + + +}); // module: reporters/dot.js + +require.register("reporters/html-cov.js", function(module, exports, require){ + +/** + * Module dependencies. + */ + +var JSONCov = require('./json-cov') + , fs = require('browser/fs'); + +/** + * Expose `HTMLCov`. + */ + +exports = module.exports = HTMLCov; + +/** + * Initialize a new `JsCoverage` reporter. + * + * @param {Runner} runner + * @api public + */ + +function HTMLCov(runner) { + var jade = require('jade') + , file = __dirname + '/templates/coverage.jade' + , str = fs.readFileSync(file, 'utf8') + , fn = jade.compile(str, { filename: file }) + , self = this; + + JSONCov.call(this, runner, false); + + runner.on('end', function(){ + process.stdout.write(fn({ + cov: self.cov + , coverageClass: coverageClass + })); + }); +} + +/** + * Return coverage class for `n`. + * + * @return {String} + * @api private + */ + +function coverageClass(n) { + if (n >= 75) return 'high'; + if (n >= 50) return 'medium'; + if (n >= 25) return 'low'; + return 'terrible'; +} +}); // module: reporters/html-cov.js + +require.register("reporters/html.js", function(module, exports, require){ + +/** + * Module dependencies. + */ + +var Base = require('./base') + , utils = require('../utils') + , Progress = require('../browser/progress') + , escape = utils.escape; + +/** + * Save timer references to avoid Sinon interfering (see GH-237). + */ + +var Date = global.Date + , setTimeout = global.setTimeout + , setInterval = global.setInterval + , clearTimeout = global.clearTimeout + , clearInterval = global.clearInterval; + +/** + * Expose `HTML`. + */ + +exports = module.exports = HTML; + +/** + * Stats template. + */ + +var statsTemplate = ''; + +/** + * Initialize a new `HTML` reporter. + * + * @param {Runner} runner + * @api public + */ + +function HTML(runner) { + Base.call(this, runner); + + var self = this + , stats = this.stats + , total = runner.total + , stat = fragment(statsTemplate) + , items = stat.getElementsByTagName('li') + , passes = items[1].getElementsByTagName('em')[0] + , passesLink = items[1].getElementsByTagName('a')[0] + , failures = items[2].getElementsByTagName('em')[0] + , failuresLink = items[2].getElementsByTagName('a')[0] + , duration = items[3].getElementsByTagName('em')[0] + , canvas = stat.getElementsByTagName('canvas')[0] + , report = fragment('
    ') + , stack = [report] + , progress + , ctx + , root = document.getElementById('mocha'); + + if (canvas.getContext) { + var ratio = window.devicePixelRatio || 1; + canvas.style.width = canvas.width; + canvas.style.height = canvas.height; + canvas.width *= ratio; + canvas.height *= ratio; + ctx = canvas.getContext('2d'); + ctx.scale(ratio, ratio); + progress = new Progress; + } + + if (!root) return error('#mocha div missing, add it to your document'); + + // pass toggle + on(passesLink, 'click', function(){ + unhide(); + var name = /pass/.test(report.className) ? '' : ' pass'; + report.className = report.className.replace(/fail|pass/g, '') + name; + if (report.className.trim()) hideSuitesWithout('test pass'); + }); + + // failure toggle + on(failuresLink, 'click', function(){ + unhide(); + var name = /fail/.test(report.className) ? '' : ' fail'; + report.className = report.className.replace(/fail|pass/g, '') + name; + if (report.className.trim()) hideSuitesWithout('test fail'); + }); + + root.appendChild(stat); + root.appendChild(report); + + if (progress) progress.size(40); + + runner.on('suite', function(suite){ + if (suite.root) return; + + // suite + var url = self.suiteURL(suite); + var el = fragment('
  • %s

  • ', url, escape(suite.title)); + + // container + stack[0].appendChild(el); + stack.unshift(document.createElement('ul')); + el.appendChild(stack[0]); + }); + + runner.on('suite end', function(suite){ + if (suite.root) return; + stack.shift(); + }); + + runner.on('fail', function(test, err){ + if ('hook' == test.type) runner.emit('test end', test); + }); + + runner.on('test end', function(test){ + // TODO: add to stats + var percent = stats.tests / this.total * 100 | 0; + if (progress) progress.update(percent).draw(ctx); + + // update stats + var ms = new Date - stats.start; + text(passes, stats.passes); + text(failures, stats.failures); + text(duration, (ms / 1000).toFixed(2)); + + // test + if ('passed' == test.state) { + var url = self.testURL(test); + var el = fragment('
  • %e%ems ‣

  • ', test.speed, test.title, test.duration, url); + } else if (test.pending) { + var el = fragment('
  • %e

  • ', test.title); + } else { + var el = fragment('
  • %e ‣

  • ', test.title, encodeURIComponent(test.fullTitle())); + var str = test.err.stack || test.err.toString(); + + // FF / Opera do not add the message + if (!~str.indexOf(test.err.message)) { + str = test.err.message + '\n' + str; + } + + // <=IE7 stringifies to [Object Error]. Since it can be overloaded, we + // check for the result of the stringifying. + if ('[object Error]' == str) str = test.err.message; + + // Safari doesn't give you a stack. Let's at least provide a source line. + if (!test.err.stack && test.err.sourceURL && test.err.line !== undefined) { + str += "\n(" + test.err.sourceURL + ":" + test.err.line + ")"; + } + + el.appendChild(fragment('
    %e
    ', str)); + } + + // toggle code + // TODO: defer + if (!test.pending) { + var h2 = el.getElementsByTagName('h2')[0]; + + on(h2, 'click', function(){ + pre.style.display = 'none' == pre.style.display + ? 'block' + : 'none'; + }); + + var pre = fragment('
    %e
    ', utils.clean(test.fn.toString())); + el.appendChild(pre); + pre.style.display = 'none'; + } + + // Don't call .appendChild if #mocha-report was already .shift()'ed off the stack. + if (stack[0]) stack[0].appendChild(el); + }); +} + +/** + * Provide suite URL + * + * @param {Object} [suite] + */ + +HTML.prototype.suiteURL = function(suite){ + return '?grep=' + encodeURIComponent(suite.fullTitle()); +}; + +/** + * Provide test URL + * + * @param {Object} [test] + */ + +HTML.prototype.testURL = function(test){ + return '?grep=' + encodeURIComponent(test.fullTitle()); +}; + +/** + * Display error `msg`. + */ + +function error(msg) { + document.body.appendChild(fragment('
    %s
    ', msg)); +} + +/** + * Return a DOM fragment from `html`. + */ + +function fragment(html) { + var args = arguments + , div = document.createElement('div') + , i = 1; + + div.innerHTML = html.replace(/%([se])/g, function(_, type){ + switch (type) { + case 's': return String(args[i++]); + case 'e': return escape(args[i++]); + } + }); + + return div.firstChild; +} + +/** + * Check for suites that do not have elements + * with `classname`, and hide them. + */ + +function hideSuitesWithout(classname) { + var suites = document.getElementsByClassName('suite'); + for (var i = 0; i < suites.length; i++) { + var els = suites[i].getElementsByClassName(classname); + if (0 == els.length) suites[i].className += ' hidden'; + } +} + +/** + * Unhide .hidden suites. + */ + +function unhide() { + var els = document.getElementsByClassName('suite hidden'); + for (var i = 0; i < els.length; ++i) { + els[i].className = els[i].className.replace('suite hidden', 'suite'); + } +} + +/** + * Set `el` text to `str`. + */ + +function text(el, str) { + if (el.textContent) { + el.textContent = str; + } else { + el.innerText = str; + } +} + +/** + * Listen on `event` with callback `fn`. + */ + +function on(el, event, fn) { + if (el.addEventListener) { + el.addEventListener(event, fn, false); + } else { + el.attachEvent('on' + event, fn); + } +} + +}); // module: reporters/html.js + +require.register("reporters/index.js", function(module, exports, require){ + +exports.Base = require('./base'); +exports.Dot = require('./dot'); +exports.Doc = require('./doc'); +exports.TAP = require('./tap'); +exports.JSON = require('./json'); +exports.HTML = require('./html'); +exports.List = require('./list'); +exports.Min = require('./min'); +exports.Spec = require('./spec'); +exports.Nyan = require('./nyan'); +exports.XUnit = require('./xunit'); +exports.Markdown = require('./markdown'); +exports.Progress = require('./progress'); +exports.Landing = require('./landing'); +exports.JSONCov = require('./json-cov'); +exports.HTMLCov = require('./html-cov'); +exports.JSONStream = require('./json-stream'); + +}); // module: reporters/index.js + +require.register("reporters/json-cov.js", function(module, exports, require){ + +/** + * Module dependencies. + */ + +var Base = require('./base'); + +/** + * Expose `JSONCov`. + */ + +exports = module.exports = JSONCov; + +/** + * Initialize a new `JsCoverage` reporter. + * + * @param {Runner} runner + * @param {Boolean} output + * @api public + */ + +function JSONCov(runner, output) { + var self = this + , output = 1 == arguments.length ? true : output; + + Base.call(this, runner); + + var tests = [] + , failures = [] + , passes = []; + + runner.on('test end', function(test){ + tests.push(test); + }); + + runner.on('pass', function(test){ + passes.push(test); + }); + + runner.on('fail', function(test){ + failures.push(test); + }); + + runner.on('end', function(){ + var cov = global._$jscoverage || {}; + var result = self.cov = map(cov); + result.stats = self.stats; + result.tests = tests.map(clean); + result.failures = failures.map(clean); + result.passes = passes.map(clean); + if (!output) return; + process.stdout.write(JSON.stringify(result, null, 2 )); + }); +} + +/** + * Map jscoverage data to a JSON structure + * suitable for reporting. + * + * @param {Object} cov + * @return {Object} + * @api private + */ + +function map(cov) { + var ret = { + instrumentation: 'node-jscoverage' + , sloc: 0 + , hits: 0 + , misses: 0 + , coverage: 0 + , files: [] + }; + + for (var filename in cov) { + var data = coverage(filename, cov[filename]); + ret.files.push(data); + ret.hits += data.hits; + ret.misses += data.misses; + ret.sloc += data.sloc; + } + + ret.files.sort(function(a, b) { + return a.filename.localeCompare(b.filename); + }); + + if (ret.sloc > 0) { + ret.coverage = (ret.hits / ret.sloc) * 100; + } + + return ret; +}; + +/** + * Map jscoverage data for a single source file + * to a JSON structure suitable for reporting. + * + * @param {String} filename name of the source file + * @param {Object} data jscoverage coverage data + * @return {Object} + * @api private + */ + +function coverage(filename, data) { + var ret = { + filename: filename, + coverage: 0, + hits: 0, + misses: 0, + sloc: 0, + source: {} + }; + + data.source.forEach(function(line, num){ + num++; + + if (data[num] === 0) { + ret.misses++; + ret.sloc++; + } else if (data[num] !== undefined) { + ret.hits++; + ret.sloc++; + } + + ret.source[num] = { + source: line + , coverage: data[num] === undefined + ? '' + : data[num] + }; + }); + + ret.coverage = ret.hits / ret.sloc * 100; + + return ret; +} + +/** + * Return a plain-object representation of `test` + * free of cyclic properties etc. + * + * @param {Object} test + * @return {Object} + * @api private + */ + +function clean(test) { + return { + title: test.title + , fullTitle: test.fullTitle() + , duration: test.duration + } +} + +}); // module: reporters/json-cov.js + +require.register("reporters/json-stream.js", function(module, exports, require){ + +/** + * Module dependencies. + */ + +var Base = require('./base') + , color = Base.color; + +/** + * Expose `List`. + */ + +exports = module.exports = List; + +/** + * Initialize a new `List` test reporter. + * + * @param {Runner} runner + * @api public + */ + +function List(runner) { + Base.call(this, runner); + + var self = this + , stats = this.stats + , total = runner.total; + + runner.on('start', function(){ + console.log(JSON.stringify(['start', { total: total }])); + }); + + runner.on('pass', function(test){ + console.log(JSON.stringify(['pass', clean(test)])); + }); + + runner.on('fail', function(test, err){ + console.log(JSON.stringify(['fail', clean(test)])); + }); + + runner.on('end', function(){ + process.stdout.write(JSON.stringify(['end', self.stats])); + }); +} + +/** + * Return a plain-object representation of `test` + * free of cyclic properties etc. + * + * @param {Object} test + * @return {Object} + * @api private + */ + +function clean(test) { + return { + title: test.title + , fullTitle: test.fullTitle() + , duration: test.duration + } +} +}); // module: reporters/json-stream.js + +require.register("reporters/json.js", function(module, exports, require){ + +/** + * Module dependencies. + */ + +var Base = require('./base') + , cursor = Base.cursor + , color = Base.color; + +/** + * Expose `JSON`. + */ + +exports = module.exports = JSONReporter; + +/** + * Initialize a new `JSON` reporter. + * + * @param {Runner} runner + * @api public + */ + +function JSONReporter(runner) { + var self = this; + Base.call(this, runner); + + var tests = [] + , failures = [] + , passes = []; + + runner.on('test end', function(test){ + tests.push(test); + }); + + runner.on('pass', function(test){ + passes.push(test); + }); + + runner.on('fail', function(test, err){ + failures.push(test); + if (err === Object(err)) { + test.errMsg = err.message; + test.errStack = err.stack; + } + }); + + runner.on('end', function(){ + var obj = { + stats: self.stats, + tests: tests.map(clean), + failures: failures.map(clean), + passes: passes.map(clean) + }; + runner.testResults = obj; + + process.stdout.write(JSON.stringify(obj, null, 2)); + }); +} + +/** + * Return a plain-object representation of `test` + * free of cyclic properties etc. + * + * @param {Object} test + * @return {Object} + * @api private + */ + +function clean(test) { + return { + title: test.title, + fullTitle: test.fullTitle(), + duration: test.duration, + err: test.err, + errStack: test.err.stack, + errMessage: test.err.message + } +} + +}); // module: reporters/json.js + +require.register("reporters/landing.js", function(module, exports, require){ + +/** + * Module dependencies. + */ + +var Base = require('./base') + , cursor = Base.cursor + , color = Base.color; + +/** + * Expose `Landing`. + */ + +exports = module.exports = Landing; + +/** + * Airplane color. + */ + +Base.colors.plane = 0; + +/** + * Airplane crash color. + */ + +Base.colors['plane crash'] = 31; + +/** + * Runway color. + */ + +Base.colors.runway = 90; + +/** + * Initialize a new `Landing` reporter. + * + * @param {Runner} runner + * @api public + */ + +function Landing(runner) { + Base.call(this, runner); + + var self = this + , stats = this.stats + , width = Base.window.width * .75 | 0 + , total = runner.total + , stream = process.stdout + , plane = color('plane', '✈') + , crashed = -1 + , n = 0; + + function runway() { + var buf = Array(width).join('-'); + return ' ' + color('runway', buf); + } + + runner.on('start', function(){ + stream.write('\n '); + cursor.hide(); + }); + + runner.on('test end', function(test){ + // check if the plane crashed + var col = -1 == crashed + ? width * ++n / total | 0 + : crashed; + + // show the crash + if ('failed' == test.state) { + plane = color('plane crash', '✈'); + crashed = col; + } + + // render landing strip + stream.write('\u001b[4F\n\n'); + stream.write(runway()); + stream.write('\n '); + stream.write(color('runway', Array(col).join('⋅'))); + stream.write(plane) + stream.write(color('runway', Array(width - col).join('⋅') + '\n')); + stream.write(runway()); + stream.write('\u001b[0m'); + }); + + runner.on('end', function(){ + cursor.show(); + console.log(); + self.epilogue(); + }); +} + +/** + * Inherit from `Base.prototype`. + */ + +function F(){}; +F.prototype = Base.prototype; +Landing.prototype = new F; +Landing.prototype.constructor = Landing; + +}); // module: reporters/landing.js + +require.register("reporters/list.js", function(module, exports, require){ + +/** + * Module dependencies. + */ + +var Base = require('./base') + , cursor = Base.cursor + , color = Base.color; + +/** + * Expose `List`. + */ + +exports = module.exports = List; + +/** + * Initialize a new `List` test reporter. + * + * @param {Runner} runner + * @api public + */ + +function List(runner) { + Base.call(this, runner); + + var self = this + , stats = this.stats + , n = 0; + + runner.on('start', function(){ + console.log(); + }); + + runner.on('test', function(test){ + process.stdout.write(color('pass', ' ' + test.fullTitle() + ': ')); + }); + + runner.on('pending', function(test){ + var fmt = color('checkmark', ' -') + + color('pending', ' %s'); + console.log(fmt, test.fullTitle()); + }); + + runner.on('pass', function(test){ + var fmt = color('checkmark', ' '+Base.symbols.dot) + + color('pass', ' %s: ') + + color(test.speed, '%dms'); + cursor.CR(); + console.log(fmt, test.fullTitle(), test.duration); + }); + + runner.on('fail', function(test, err){ + cursor.CR(); + console.log(color('fail', ' %d) %s'), ++n, test.fullTitle()); + }); + + runner.on('end', self.epilogue.bind(self)); +} + +/** + * Inherit from `Base.prototype`. + */ + +function F(){}; +F.prototype = Base.prototype; +List.prototype = new F; +List.prototype.constructor = List; + + +}); // module: reporters/list.js + +require.register("reporters/markdown.js", function(module, exports, require){ +/** + * Module dependencies. + */ + +var Base = require('./base') + , utils = require('../utils'); + +/** + * Expose `Markdown`. + */ + +exports = module.exports = Markdown; + +/** + * Initialize a new `Markdown` reporter. + * + * @param {Runner} runner + * @api public + */ + +function Markdown(runner) { + Base.call(this, runner); + + var self = this + , stats = this.stats + , level = 0 + , buf = ''; + + function title(str) { + return Array(level).join('#') + ' ' + str; + } + + function indent() { + return Array(level).join(' '); + } + + function mapTOC(suite, obj) { + var ret = obj; + obj = obj[suite.title] = obj[suite.title] || { suite: suite }; + suite.suites.forEach(function(suite){ + mapTOC(suite, obj); + }); + return ret; + } + + function stringifyTOC(obj, level) { + ++level; + var buf = ''; + var link; + for (var key in obj) { + if ('suite' == key) continue; + if (key) link = ' - [' + key + '](#' + utils.slug(obj[key].suite.fullTitle()) + ')\n'; + if (key) buf += Array(level).join(' ') + link; + buf += stringifyTOC(obj[key], level); + } + --level; + return buf; + } + + function generateTOC(suite) { + var obj = mapTOC(suite, {}); + return stringifyTOC(obj, 0); + } + + generateTOC(runner.suite); + + runner.on('suite', function(suite){ + ++level; + var slug = utils.slug(suite.fullTitle()); + buf += '' + '\n'; + buf += title(suite.title) + '\n'; + }); + + runner.on('suite end', function(suite){ + --level; + }); + + runner.on('pass', function(test){ + var code = utils.clean(test.fn.toString()); + buf += test.title + '.\n'; + buf += '\n```js\n'; + buf += code + '\n'; + buf += '```\n\n'; + }); + + runner.on('end', function(){ + process.stdout.write('# TOC\n'); + process.stdout.write(generateTOC(runner.suite)); + process.stdout.write(buf); + }); +} +}); // module: reporters/markdown.js + +require.register("reporters/min.js", function(module, exports, require){ + +/** + * Module dependencies. + */ + +var Base = require('./base'); + +/** + * Expose `Min`. + */ + +exports = module.exports = Min; + +/** + * Initialize a new `Min` minimal test reporter (best used with --watch). + * + * @param {Runner} runner + * @api public + */ + +function Min(runner) { + Base.call(this, runner); + + runner.on('start', function(){ + // clear screen + process.stdout.write('\u001b[2J'); + // set cursor position + process.stdout.write('\u001b[1;3H'); + }); + + runner.on('end', this.epilogue.bind(this)); +} + +/** + * Inherit from `Base.prototype`. + */ + +function F(){}; +F.prototype = Base.prototype; +Min.prototype = new F; +Min.prototype.constructor = Min; + + +}); // module: reporters/min.js + +require.register("reporters/nyan.js", function(module, exports, require){ +/** + * Module dependencies. + */ + +var Base = require('./base') + , color = Base.color; + +/** + * Expose `Dot`. + */ + +exports = module.exports = NyanCat; + +/** + * Initialize a new `Dot` matrix test reporter. + * + * @param {Runner} runner + * @api public + */ + +function NyanCat(runner) { + Base.call(this, runner); + var self = this + , stats = this.stats + , width = Base.window.width * .75 | 0 + , rainbowColors = this.rainbowColors = self.generateColors() + , colorIndex = this.colorIndex = 0 + , numerOfLines = this.numberOfLines = 4 + , trajectories = this.trajectories = [[], [], [], []] + , nyanCatWidth = this.nyanCatWidth = 11 + , trajectoryWidthMax = this.trajectoryWidthMax = (width - nyanCatWidth) + , scoreboardWidth = this.scoreboardWidth = 5 + , tick = this.tick = 0 + , n = 0; + + runner.on('start', function(){ + Base.cursor.hide(); + self.draw(); + }); + + runner.on('pending', function(test){ + self.draw(); + }); + + runner.on('pass', function(test){ + self.draw(); + }); + + runner.on('fail', function(test, err){ + self.draw(); + }); + + runner.on('end', function(){ + Base.cursor.show(); + for (var i = 0; i < self.numberOfLines; i++) write('\n'); + self.epilogue(); + }); +} + +/** + * Draw the nyan cat + * + * @api private + */ + +NyanCat.prototype.draw = function(){ + this.appendRainbow(); + this.drawScoreboard(); + this.drawRainbow(); + this.drawNyanCat(); + this.tick = !this.tick; +}; + +/** + * Draw the "scoreboard" showing the number + * of passes, failures and pending tests. + * + * @api private + */ + +NyanCat.prototype.drawScoreboard = function(){ + var stats = this.stats; + var colors = Base.colors; + + function draw(color, n) { + write(' '); + write('\u001b[' + color + 'm' + n + '\u001b[0m'); + write('\n'); + } + + draw(colors.green, stats.passes); + draw(colors.fail, stats.failures); + draw(colors.pending, stats.pending); + write('\n'); + + this.cursorUp(this.numberOfLines); +}; + +/** + * Append the rainbow. + * + * @api private + */ + +NyanCat.prototype.appendRainbow = function(){ + var segment = this.tick ? '_' : '-'; + var rainbowified = this.rainbowify(segment); + + for (var index = 0; index < this.numberOfLines; index++) { + var trajectory = this.trajectories[index]; + if (trajectory.length >= this.trajectoryWidthMax) trajectory.shift(); + trajectory.push(rainbowified); + } +}; + +/** + * Draw the rainbow. + * + * @api private + */ + +NyanCat.prototype.drawRainbow = function(){ + var self = this; + + this.trajectories.forEach(function(line, index) { + write('\u001b[' + self.scoreboardWidth + 'C'); + write(line.join('')); + write('\n'); + }); + + this.cursorUp(this.numberOfLines); +}; + +/** + * Draw the nyan cat + * + * @api private + */ + +NyanCat.prototype.drawNyanCat = function() { + var self = this; + var startWidth = this.scoreboardWidth + this.trajectories[0].length; + var color = '\u001b[' + startWidth + 'C'; + var padding = ''; + + write(color); + write('_,------,'); + write('\n'); + + write(color); + padding = self.tick ? ' ' : ' '; + write('_|' + padding + '/\\_/\\ '); + write('\n'); + + write(color); + padding = self.tick ? '_' : '__'; + var tail = self.tick ? '~' : '^'; + var face; + write(tail + '|' + padding + this.face() + ' '); + write('\n'); + + write(color); + padding = self.tick ? ' ' : ' '; + write(padding + '"" "" '); + write('\n'); + + this.cursorUp(this.numberOfLines); +}; + +/** + * Draw nyan cat face. + * + * @return {String} + * @api private + */ + +NyanCat.prototype.face = function() { + var stats = this.stats; + if (stats.failures) { + return '( x .x)'; + } else if (stats.pending) { + return '( o .o)'; + } else if(stats.passes) { + return '( ^ .^)'; + } else { + return '( - .-)'; + } +} + +/** + * Move cursor up `n`. + * + * @param {Number} n + * @api private + */ + +NyanCat.prototype.cursorUp = function(n) { + write('\u001b[' + n + 'A'); +}; + +/** + * Move cursor down `n`. + * + * @param {Number} n + * @api private + */ + +NyanCat.prototype.cursorDown = function(n) { + write('\u001b[' + n + 'B'); +}; + +/** + * Generate rainbow colors. + * + * @return {Array} + * @api private + */ + +NyanCat.prototype.generateColors = function(){ + var colors = []; + + for (var i = 0; i < (6 * 7); i++) { + var pi3 = Math.floor(Math.PI / 3); + var n = (i * (1.0 / 6)); + var r = Math.floor(3 * Math.sin(n) + 3); + var g = Math.floor(3 * Math.sin(n + 2 * pi3) + 3); + var b = Math.floor(3 * Math.sin(n + 4 * pi3) + 3); + colors.push(36 * r + 6 * g + b + 16); + } + + return colors; +}; + +/** + * Apply rainbow to the given `str`. + * + * @param {String} str + * @return {String} + * @api private + */ + +NyanCat.prototype.rainbowify = function(str){ + var color = this.rainbowColors[this.colorIndex % this.rainbowColors.length]; + this.colorIndex += 1; + return '\u001b[38;5;' + color + 'm' + str + '\u001b[0m'; +}; + +/** + * Stdout helper. + */ + +function write(string) { + process.stdout.write(string); +} + +/** + * Inherit from `Base.prototype`. + */ + +function F(){}; +F.prototype = Base.prototype; +NyanCat.prototype = new F; +NyanCat.prototype.constructor = NyanCat; + + +}); // module: reporters/nyan.js + +require.register("reporters/progress.js", function(module, exports, require){ +/** + * Module dependencies. + */ + +var Base = require('./base') + , cursor = Base.cursor + , color = Base.color; + +/** + * Expose `Progress`. + */ + +exports = module.exports = Progress; + +/** + * General progress bar color. + */ + +Base.colors.progress = 90; + +/** + * Initialize a new `Progress` bar test reporter. + * + * @param {Runner} runner + * @param {Object} options + * @api public + */ + +function Progress(runner, options) { + Base.call(this, runner); + + var self = this + , options = options || {} + , stats = this.stats + , width = Base.window.width * .50 | 0 + , total = runner.total + , complete = 0 + , max = Math.max + , lastN = -1; + + // default chars + options.open = options.open || '['; + options.complete = options.complete || '▬'; + options.incomplete = options.incomplete || Base.symbols.dot; + options.close = options.close || ']'; + options.verbose = false; + + // tests started + runner.on('start', function(){ + console.log(); + cursor.hide(); + }); + + // tests complete + runner.on('test end', function(){ + complete++; + var incomplete = total - complete + , percent = complete / total + , n = width * percent | 0 + , i = width - n; + + if (lastN === n && !options.verbose) { + // Don't re-render the line if it hasn't changed + return; + } + lastN = n; + + cursor.CR(); + process.stdout.write('\u001b[J'); + process.stdout.write(color('progress', ' ' + options.open)); + process.stdout.write(Array(n).join(options.complete)); + process.stdout.write(Array(i).join(options.incomplete)); + process.stdout.write(color('progress', options.close)); + if (options.verbose) { + process.stdout.write(color('progress', ' ' + complete + ' of ' + total)); + } + }); + + // tests are complete, output some stats + // and the failures if any + runner.on('end', function(){ + cursor.show(); + console.log(); + self.epilogue(); + }); +} + +/** + * Inherit from `Base.prototype`. + */ + +function F(){}; +F.prototype = Base.prototype; +Progress.prototype = new F; +Progress.prototype.constructor = Progress; + + +}); // module: reporters/progress.js + +require.register("reporters/spec.js", function(module, exports, require){ + +/** + * Module dependencies. + */ + +var Base = require('./base') + , cursor = Base.cursor + , color = Base.color; + +/** + * Expose `Spec`. + */ + +exports = module.exports = Spec; + +/** + * Initialize a new `Spec` test reporter. + * + * @param {Runner} runner + * @api public + */ + +function Spec(runner) { + Base.call(this, runner); + + var self = this + , stats = this.stats + , indents = 0 + , n = 0; + + function indent() { + return Array(indents).join(' ') + } + + runner.on('start', function(){ + console.log(); + }); + + runner.on('suite', function(suite){ + ++indents; + console.log(color('suite', '%s%s'), indent(), suite.title); + }); + + runner.on('suite end', function(suite){ + --indents; + if (1 == indents) console.log(); + }); + + runner.on('pending', function(test){ + var fmt = indent() + color('pending', ' - %s'); + console.log(fmt, test.title); + }); + + runner.on('pass', function(test){ + if ('fast' == test.speed) { + var fmt = indent() + + color('checkmark', ' ' + Base.symbols.ok) + + color('pass', ' %s '); + cursor.CR(); + console.log(fmt, test.title); + } else { + var fmt = indent() + + color('checkmark', ' ' + Base.symbols.ok) + + color('pass', ' %s ') + + color(test.speed, '(%dms)'); + cursor.CR(); + console.log(fmt, test.title, test.duration); + } + }); + + runner.on('fail', function(test, err){ + cursor.CR(); + console.log(indent() + color('fail', ' %d) %s'), ++n, test.title); + }); + + runner.on('end', self.epilogue.bind(self)); +} + +/** + * Inherit from `Base.prototype`. + */ + +function F(){}; +F.prototype = Base.prototype; +Spec.prototype = new F; +Spec.prototype.constructor = Spec; + + +}); // module: reporters/spec.js + +require.register("reporters/tap.js", function(module, exports, require){ + +/** + * Module dependencies. + */ + +var Base = require('./base') + , cursor = Base.cursor + , color = Base.color; + +/** + * Expose `TAP`. + */ + +exports = module.exports = TAP; + +/** + * Initialize a new `TAP` reporter. + * + * @param {Runner} runner + * @api public + */ + +function TAP(runner) { + Base.call(this, runner); + + var self = this + , stats = this.stats + , n = 1 + , passes = 0 + , failures = 0; + + runner.on('start', function(){ + var total = runner.grepTotal(runner.suite); + console.log('%d..%d', 1, total); + }); + + runner.on('test end', function(){ + ++n; + }); + + runner.on('pending', function(test){ + console.log('ok %d %s # SKIP -', n, title(test)); + }); + + runner.on('pass', function(test){ + passes++; + console.log('ok %d %s', n, title(test)); + }); + + runner.on('fail', function(test, err){ + failures++; + console.log('not ok %d %s', n, title(test)); + if (err.stack) console.log(err.stack.replace(/^/gm, ' ')); + }); + + runner.on('end', function(){ + console.log('# tests ' + (passes + failures)); + console.log('# pass ' + passes); + console.log('# fail ' + failures); + }); +} + +/** + * Return a TAP-safe title of `test` + * + * @param {Object} test + * @return {String} + * @api private + */ + +function title(test) { + return test.fullTitle().replace(/#/g, ''); +} + +}); // module: reporters/tap.js + +require.register("reporters/xunit.js", function(module, exports, require){ + +/** + * Module dependencies. + */ + +var Base = require('./base') + , utils = require('../utils') + , escape = utils.escape; + +/** + * Save timer references to avoid Sinon interfering (see GH-237). + */ + +var Date = global.Date + , setTimeout = global.setTimeout + , setInterval = global.setInterval + , clearTimeout = global.clearTimeout + , clearInterval = global.clearInterval; + +/** + * Expose `XUnit`. + */ + +exports = module.exports = XUnit; + +/** + * Initialize a new `XUnit` reporter. + * + * @param {Runner} runner + * @api public + */ + +function XUnit(runner) { + Base.call(this, runner); + var stats = this.stats + , tests = [] + , self = this; + + runner.on('pending', function(test){ + tests.push(test); + }); + + runner.on('pass', function(test){ + tests.push(test); + }); + + runner.on('fail', function(test){ + tests.push(test); + }); + + runner.on('end', function(){ + console.log(tag('testsuite', { + name: 'Mocha Tests' + , tests: stats.tests + , failures: stats.failures + , errors: stats.failures + , skipped: stats.tests - stats.failures - stats.passes + , timestamp: (new Date).toUTCString() + , time: (stats.duration / 1000) || 0 + }, false)); + + tests.forEach(test); + console.log(''); + }); +} + +/** + * Inherit from `Base.prototype`. + */ + +function F(){}; +F.prototype = Base.prototype; +XUnit.prototype = new F; +XUnit.prototype.constructor = XUnit; + + +/** + * Output tag for the given `test.` + */ + +function test(test) { + var attrs = { + classname: test.parent.fullTitle() + , name: test.title + , time: (test.duration / 1000) || 0 + }; + + if ('failed' == test.state) { + var err = test.err; + console.log(tag('testcase', attrs, false, tag('failure', {}, false, cdata(escape(err.message) + "\n" + err.stack)))); + } else if (test.pending) { + console.log(tag('testcase', attrs, false, tag('skipped', {}, true))); + } else { + console.log(tag('testcase', attrs, true) ); + } +} + +/** + * HTML tag helper. + */ + +function tag(name, attrs, close, content) { + var end = close ? '/>' : '>' + , pairs = [] + , tag; + + for (var key in attrs) { + pairs.push(key + '="' + escape(attrs[key]) + '"'); + } + + tag = '<' + name + (pairs.length ? ' ' + pairs.join(' ') : '') + end; + if (content) tag += content + ''; +} + +}); // module: reporters/xunit.js + +require.register("runnable.js", function(module, exports, require){ + +/** + * Module dependencies. + */ + +var EventEmitter = require('browser/events').EventEmitter + , debug = require('browser/debug')('mocha:runnable') + , milliseconds = require('./ms'); + +/** + * Save timer references to avoid Sinon interfering (see GH-237). + */ + +var Date = global.Date + , setTimeout = global.setTimeout + , setInterval = global.setInterval + , clearTimeout = global.clearTimeout + , clearInterval = global.clearInterval; + +/** + * Object#toString(). + */ + +var toString = Object.prototype.toString; + +/** + * Expose `Runnable`. + */ + +module.exports = Runnable; + +/** + * Initialize a new `Runnable` with the given `title` and callback `fn`. + * + * @param {String} title + * @param {Function} fn + * @api private + */ + +function Runnable(title, fn) { + this.title = title; + this.fn = fn; + this.async = fn && fn.length; + this.sync = ! this.async; + this._timeout = 2000; + this._slow = 75; + this._enableTimeouts = true; + this.timedOut = false; +} + +/** + * Inherit from `EventEmitter.prototype`. + */ + +function F(){}; +F.prototype = EventEmitter.prototype; +Runnable.prototype = new F; +Runnable.prototype.constructor = Runnable; + + +/** + * Set & get timeout `ms`. + * + * @param {Number|String} ms + * @return {Runnable|Number} ms or self + * @api private + */ + +Runnable.prototype.timeout = function(ms){ + if (0 == arguments.length) return this._timeout; + if ('string' == typeof ms) ms = milliseconds(ms); + debug('timeout %d', ms); + this._timeout = ms; + if (this.timer) this.resetTimeout(); + return this; +}; + +/** + * Set & get slow `ms`. + * + * @param {Number|String} ms + * @return {Runnable|Number} ms or self + * @api private + */ + +Runnable.prototype.slow = function(ms){ + if (0 === arguments.length) return this._slow; + if ('string' == typeof ms) ms = milliseconds(ms); + debug('timeout %d', ms); + this._slow = ms; + return this; +}; + +/** + * Set and & get timeout `enabled`. + * + * @param {Boolean} enabled + * @return {Runnable|Boolean} enabled or self + * @api private + */ + +Runnable.prototype.enableTimeouts = function(enabled){ + if (arguments.length === 0) return this._enableTimeouts; + debug('enableTimeouts %s', enabled); + this._enableTimeouts = enabled; + return this; +}; + +/** + * Return the full title generated by recursively + * concatenating the parent's full title. + * + * @return {String} + * @api public + */ + +Runnable.prototype.fullTitle = function(){ + return this.parent.fullTitle() + ' ' + this.title; +}; + +/** + * Clear the timeout. + * + * @api private + */ + +Runnable.prototype.clearTimeout = function(){ + clearTimeout(this.timer); +}; + +/** + * Inspect the runnable void of private properties. + * + * @return {String} + * @api private + */ + +Runnable.prototype.inspect = function(){ + return JSON.stringify(this, function(key, val){ + if ('_' == key[0]) return; + if ('parent' == key) return '#'; + if ('ctx' == key) return '#'; + return val; + }, 2); +}; + +/** + * Reset the timeout. + * + * @api private + */ + +Runnable.prototype.resetTimeout = function(){ + var self = this; + var ms = this.timeout() || 1e9; + + if (!this._enableTimeouts) return; + this.clearTimeout(); + this.timer = setTimeout(function(){ + self.callback(new Error('timeout of ' + ms + 'ms exceeded')); + self.timedOut = true; + }, ms); +}; + +/** + * Whitelist these globals for this test run + * + * @api private + */ +Runnable.prototype.globals = function(arr){ + var self = this; + this._allowedGlobals = arr; +}; + +/** + * Run the test and invoke `fn(err)`. + * + * @param {Function} fn + * @api private + */ + +Runnable.prototype.run = function(fn){ + var self = this + , start = new Date + , ctx = this.ctx + , finished + , emitted; + + // Some times the ctx exists but it is not runnable + if (ctx && ctx.runnable) ctx.runnable(this); + + // called multiple times + function multiple(err) { + if (emitted) return; + emitted = true; + self.emit('error', err || new Error('done() called multiple times')); + } + + // finished + function done(err) { + var ms = self.timeout(); + if (self.timedOut) return; + if (finished) return multiple(err); + self.clearTimeout(); + self.duration = new Date - start; + finished = true; + if (!err && self.duration > ms && self._enableTimeouts) err = new Error('timeout of ' + ms + 'ms exceeded'); + fn(err); + } + + // for .resetTimeout() + this.callback = done; + + // explicit async with `done` argument + if (this.async) { + this.resetTimeout(); + + try { + this.fn.call(ctx, function(err){ + if (err instanceof Error || toString.call(err) === "[object Error]") return done(err); + if (null != err) { + if (Object.prototype.toString.call(err) === '[object Object]') { + return done(new Error('done() invoked with non-Error: ' + JSON.stringify(err))); + } else { + return done(new Error('done() invoked with non-Error: ' + err)); + } + } + done(); + }); + } catch (err) { + done(err); + } + return; + } + + if (this.asyncOnly) { + return done(new Error('--async-only option in use without declaring `done()`')); + } + + // sync or promise-returning + try { + if (this.pending) { + done(); + } else { + callFn(this.fn); + } + } catch (err) { + done(err); + } + + function callFn(fn) { + var result = fn.call(ctx); + if (result && typeof result.then === 'function') { + self.resetTimeout(); + result + .then(function() { + done() + }, + function(reason) { + done(reason || new Error('Promise rejected with no or falsy reason')) + }); + } else { + done(); + } + } +}; + +}); // module: runnable.js + +require.register("runner.js", function(module, exports, require){ +/** + * Module dependencies. + */ + +var EventEmitter = require('browser/events').EventEmitter + , debug = require('browser/debug')('mocha:runner') + , Test = require('./test') + , utils = require('./utils') + , filter = utils.filter + , keys = utils.keys; + +/** + * Non-enumerable globals. + */ + +var globals = [ + 'setTimeout', + 'clearTimeout', + 'setInterval', + 'clearInterval', + 'XMLHttpRequest', + 'Date' +]; + +/** + * Expose `Runner`. + */ + +module.exports = Runner; + +/** + * Initialize a `Runner` for the given `suite`. + * + * Events: + * + * - `start` execution started + * - `end` execution complete + * - `suite` (suite) test suite execution started + * - `suite end` (suite) all tests (and sub-suites) have finished + * - `test` (test) test execution started + * - `test end` (test) test completed + * - `hook` (hook) hook execution started + * - `hook end` (hook) hook complete + * - `pass` (test) test passed + * - `fail` (test, err) test failed + * - `pending` (test) test pending + * + * @api public + */ + +function Runner(suite) { + var self = this; + this._globals = []; + this._abort = false; + this.suite = suite; + this.total = suite.total(); + this.failures = 0; + this.on('test end', function(test){ self.checkGlobals(test); }); + this.on('hook end', function(hook){ self.checkGlobals(hook); }); + this.grep(/.*/); + this.globals(this.globalProps().concat(extraGlobals())); +} + +/** + * Wrapper for setImmediate, process.nextTick, or browser polyfill. + * + * @param {Function} fn + * @api private + */ + +Runner.immediately = global.setImmediate || process.nextTick; + +/** + * Inherit from `EventEmitter.prototype`. + */ + +function F(){}; +F.prototype = EventEmitter.prototype; +Runner.prototype = new F; +Runner.prototype.constructor = Runner; + + +/** + * Run tests with full titles matching `re`. Updates runner.total + * with number of tests matched. + * + * @param {RegExp} re + * @param {Boolean} invert + * @return {Runner} for chaining + * @api public + */ + +Runner.prototype.grep = function(re, invert){ + debug('grep %s', re); + this._grep = re; + this._invert = invert; + this.total = this.grepTotal(this.suite); + return this; +}; + +/** + * Returns the number of tests matching the grep search for the + * given suite. + * + * @param {Suite} suite + * @return {Number} + * @api public + */ + +Runner.prototype.grepTotal = function(suite) { + var self = this; + var total = 0; + + suite.eachTest(function(test){ + var match = self._grep.test(test.fullTitle()); + if (self._invert) match = !match; + if (match) total++; + }); + + return total; +}; + +/** + * Return a list of global properties. + * + * @return {Array} + * @api private + */ + +Runner.prototype.globalProps = function() { + var props = utils.keys(global); + + // non-enumerables + for (var i = 0; i < globals.length; ++i) { + if (~utils.indexOf(props, globals[i])) continue; + props.push(globals[i]); + } + + return props; +}; + +/** + * Allow the given `arr` of globals. + * + * @param {Array} arr + * @return {Runner} for chaining + * @api public + */ + +Runner.prototype.globals = function(arr){ + if (0 == arguments.length) return this._globals; + debug('globals %j', arr); + this._globals = this._globals.concat(arr); + return this; +}; + +/** + * Check for global variable leaks. + * + * @api private + */ + +Runner.prototype.checkGlobals = function(test){ + if (this.ignoreLeaks) return; + var ok = this._globals; + + var globals = this.globalProps(); + var leaks; + + if (test) { + ok = ok.concat(test._allowedGlobals || []); + } + + if(this.prevGlobalsLength == globals.length) return; + this.prevGlobalsLength = globals.length; + + leaks = filterLeaks(ok, globals); + this._globals = this._globals.concat(leaks); + + if (leaks.length > 1) { + this.fail(test, new Error('global leaks detected: ' + leaks.join(', ') + '')); + } else if (leaks.length) { + this.fail(test, new Error('global leak detected: ' + leaks[0])); + } +}; + +/** + * Fail the given `test`. + * + * @param {Test} test + * @param {Error} err + * @api private + */ + +Runner.prototype.fail = function(test, err){ + ++this.failures; + test.state = 'failed'; + + if ('string' == typeof err) { + err = new Error('the string "' + err + '" was thrown, throw an Error :)'); + } + + this.emit('fail', test, err); +}; + +/** + * Fail the given `hook` with `err`. + * + * Hook failures work in the following pattern: + * - If bail, then exit + * - Failed `before` hook skips all tests in a suite and subsuites, + * but jumps to corresponding `after` hook + * - Failed `before each` hook skips remaining tests in a + * suite and jumps to corresponding `after each` hook, + * which is run only once + * - Failed `after` hook does not alter + * execution order + * - Failed `after each` hook skips remaining tests in a + * suite and subsuites, but executes other `after each` + * hooks + * + * @param {Hook} hook + * @param {Error} err + * @api private + */ + +Runner.prototype.failHook = function(hook, err){ + this.fail(hook, err); + if (this.suite.bail()) { + this.emit('end'); + } +}; + +/** + * Run hook `name` callbacks and then invoke `fn()`. + * + * @param {String} name + * @param {Function} function + * @api private + */ + +Runner.prototype.hook = function(name, fn){ + var suite = this.suite + , hooks = suite['_' + name] + , self = this + , timer; + + function next(i) { + var hook = hooks[i]; + if (!hook) return fn(); + if (self.failures && suite.bail()) return fn(); + self.currentRunnable = hook; + + hook.ctx.currentTest = self.test; + + self.emit('hook', hook); + + hook.on('error', function(err){ + self.failHook(hook, err); + }); + + hook.run(function(err){ + hook.removeAllListeners('error'); + var testError = hook.error(); + if (testError) self.fail(self.test, testError); + if (err) { + self.failHook(hook, err); + + // stop executing hooks, notify callee of hook err + return fn(err); + } + self.emit('hook end', hook); + delete hook.ctx.currentTest; + next(++i); + }); + } + + Runner.immediately(function(){ + next(0); + }); +}; + +/** + * Run hook `name` for the given array of `suites` + * in order, and callback `fn(err, errSuite)`. + * + * @param {String} name + * @param {Array} suites + * @param {Function} fn + * @api private + */ + +Runner.prototype.hooks = function(name, suites, fn){ + var self = this + , orig = this.suite; + + function next(suite) { + self.suite = suite; + + if (!suite) { + self.suite = orig; + return fn(); + } + + self.hook(name, function(err){ + if (err) { + var errSuite = self.suite; + self.suite = orig; + return fn(err, errSuite); + } + + next(suites.pop()); + }); + } + + next(suites.pop()); +}; + +/** + * Run hooks from the top level down. + * + * @param {String} name + * @param {Function} fn + * @api private + */ + +Runner.prototype.hookUp = function(name, fn){ + var suites = [this.suite].concat(this.parents()).reverse(); + this.hooks(name, suites, fn); +}; + +/** + * Run hooks from the bottom up. + * + * @param {String} name + * @param {Function} fn + * @api private + */ + +Runner.prototype.hookDown = function(name, fn){ + var suites = [this.suite].concat(this.parents()); + this.hooks(name, suites, fn); +}; + +/** + * Return an array of parent Suites from + * closest to furthest. + * + * @return {Array} + * @api private + */ + +Runner.prototype.parents = function(){ + var suite = this.suite + , suites = []; + while (suite = suite.parent) suites.push(suite); + return suites; +}; + +/** + * Run the current test and callback `fn(err)`. + * + * @param {Function} fn + * @api private + */ + +Runner.prototype.runTest = function(fn){ + var test = this.test + , self = this; + + if (this.asyncOnly) test.asyncOnly = true; + + try { + test.on('error', function(err){ + self.fail(test, err); + }); + test.run(fn); + } catch (err) { + fn(err); + } +}; + +/** + * Run tests in the given `suite` and invoke + * the callback `fn()` when complete. + * + * @param {Suite} suite + * @param {Function} fn + * @api private + */ + +Runner.prototype.runTests = function(suite, fn){ + var self = this + , tests = suite.tests.slice() + , test; + + + function hookErr(err, errSuite, after) { + // before/after Each hook for errSuite failed: + var orig = self.suite; + + // for failed 'after each' hook start from errSuite parent, + // otherwise start from errSuite itself + self.suite = after ? errSuite.parent : errSuite; + + if (self.suite) { + // call hookUp afterEach + self.hookUp('afterEach', function(err2, errSuite2) { + self.suite = orig; + // some hooks may fail even now + if (err2) return hookErr(err2, errSuite2, true); + // report error suite + fn(errSuite); + }); + } else { + // there is no need calling other 'after each' hooks + self.suite = orig; + fn(errSuite); + } + } + + function next(err, errSuite) { + // if we bail after first err + if (self.failures && suite._bail) return fn(); + + if (self._abort) return fn(); + + if (err) return hookErr(err, errSuite, true); + + // next test + test = tests.shift(); + + // all done + if (!test) return fn(); + + // grep + var match = self._grep.test(test.fullTitle()); + if (self._invert) match = !match; + if (!match) return next(); + + // pending + if (test.pending) { + self.emit('pending', test); + self.emit('test end', test); + return next(); + } + + // execute test and hook(s) + self.emit('test', self.test = test); + self.hookDown('beforeEach', function(err, errSuite){ + + if (err) return hookErr(err, errSuite, false); + + self.currentRunnable = self.test; + self.runTest(function(err){ + test = self.test; + + if (err) { + self.fail(test, err); + self.emit('test end', test); + return self.hookUp('afterEach', next); + } + + test.state = 'passed'; + self.emit('pass', test); + self.emit('test end', test); + self.hookUp('afterEach', next); + }); + }); + } + + this.next = next; + next(); +}; + +/** + * Run the given `suite` and invoke the + * callback `fn()` when complete. + * + * @param {Suite} suite + * @param {Function} fn + * @api private + */ + +Runner.prototype.runSuite = function(suite, fn){ + var total = this.grepTotal(suite) + , self = this + , i = 0; + + debug('run suite %s', suite.fullTitle()); + + if (!total) return fn(); + + this.emit('suite', this.suite = suite); + + function next(errSuite) { + if (errSuite) { + // current suite failed on a hook from errSuite + if (errSuite == suite) { + // if errSuite is current suite + // continue to the next sibling suite + return done(); + } else { + // errSuite is among the parents of current suite + // stop execution of errSuite and all sub-suites + return done(errSuite); + } + } + + if (self._abort) return done(); + + var curr = suite.suites[i++]; + if (!curr) return done(); + self.runSuite(curr, next); + } + + function done(errSuite) { + self.suite = suite; + self.hook('afterAll', function(){ + self.emit('suite end', suite); + fn(errSuite); + }); + } + + this.hook('beforeAll', function(err){ + if (err) return done(); + self.runTests(suite, next); + }); +}; + +/** + * Handle uncaught exceptions. + * + * @param {Error} err + * @api private + */ + +Runner.prototype.uncaught = function(err){ + if (err) { + debug('uncaught exception %s', err.message); + } else { + debug('uncaught undefined exception'); + err = new Error('Catched undefined error, did you throw without specifying what?'); + } + + var runnable = this.currentRunnable; + if (!runnable || 'failed' == runnable.state) return; + runnable.clearTimeout(); + err.uncaught = true; + this.fail(runnable, err); + + // recover from test + if ('test' == runnable.type) { + this.emit('test end', runnable); + this.hookUp('afterEach', this.next); + return; + } + + // bail on hooks + this.emit('end'); +}; + +/** + * Run the root suite and invoke `fn(failures)` + * on completion. + * + * @param {Function} fn + * @return {Runner} for chaining + * @api public + */ + +Runner.prototype.run = function(fn){ + var self = this + , fn = fn || function(){}; + + function uncaught(err){ + self.uncaught(err); + } + + debug('start'); + + // callback + this.on('end', function(){ + debug('end'); + process.removeListener('uncaughtException', uncaught); + fn(self.failures); + }); + + // run suites + this.emit('start'); + this.runSuite(this.suite, function(){ + debug('finished running'); + self.emit('end'); + }); + + // uncaught exception + process.on('uncaughtException', uncaught); + + return this; +}; + +/** + * Cleanly abort execution + * + * @return {Runner} for chaining + * @api public + */ +Runner.prototype.abort = function(){ + debug('aborting'); + this._abort = true; +} + +/** + * Filter leaks with the given globals flagged as `ok`. + * + * @param {Array} ok + * @param {Array} globals + * @return {Array} + * @api private + */ + +function filterLeaks(ok, globals) { + return filter(globals, function(key){ + // Firefox and Chrome exposes iframes as index inside the window object + if (/^d+/.test(key)) return false; + + // in firefox + // if runner runs in an iframe, this iframe's window.getInterface method not init at first + // it is assigned in some seconds + if (global.navigator && /^getInterface/.test(key)) return false; + + // an iframe could be approached by window[iframeIndex] + // in ie6,7,8 and opera, iframeIndex is enumerable, this could cause leak + if (global.navigator && /^\d+/.test(key)) return false; + + // Opera and IE expose global variables for HTML element IDs (issue #243) + if (/^mocha-/.test(key)) return false; + + var matched = filter(ok, function(ok){ + if (~ok.indexOf('*')) return 0 == key.indexOf(ok.split('*')[0]); + return key == ok; + }); + return matched.length == 0 && (!global.navigator || 'onerror' !== key); + }); +} + +/** + * Array of globals dependent on the environment. + * + * @return {Array} + * @api private + */ + + function extraGlobals() { + if (typeof(process) === 'object' && + typeof(process.version) === 'string') { + + var nodeVersion = process.version.split('.').reduce(function(a, v) { + return a << 8 | v; + }); + + // 'errno' was renamed to process._errno in v0.9.11. + + if (nodeVersion < 0x00090B) { + return ['errno']; + } + } + + return []; + } + +}); // module: runner.js + +require.register("suite.js", function(module, exports, require){ + +/** + * Module dependencies. + */ + +var EventEmitter = require('browser/events').EventEmitter + , debug = require('browser/debug')('mocha:suite') + , milliseconds = require('./ms') + , utils = require('./utils') + , Hook = require('./hook'); + +/** + * Expose `Suite`. + */ + +exports = module.exports = Suite; + +/** + * Create a new `Suite` with the given `title` + * and parent `Suite`. When a suite with the + * same title is already present, that suite + * is returned to provide nicer reporter + * and more flexible meta-testing. + * + * @param {Suite} parent + * @param {String} title + * @return {Suite} + * @api public + */ + +exports.create = function(parent, title){ + var suite = new Suite(title, parent.ctx); + suite.parent = parent; + if (parent.pending) suite.pending = true; + title = suite.fullTitle(); + parent.addSuite(suite); + return suite; +}; + +/** + * Initialize a new `Suite` with the given + * `title` and `ctx`. + * + * @param {String} title + * @param {Context} ctx + * @api private + */ + +function Suite(title, parentContext) { + this.title = title; + var context = function() {}; + context.prototype = parentContext; + this.ctx = new context(); + this.suites = []; + this.tests = []; + this.pending = false; + this._beforeEach = []; + this._beforeAll = []; + this._afterEach = []; + this._afterAll = []; + this.root = !title; + this._timeout = 2000; + this._enableTimeouts = true; + this._slow = 75; + this._bail = false; +} + +/** + * Inherit from `EventEmitter.prototype`. + */ + +function F(){}; +F.prototype = EventEmitter.prototype; +Suite.prototype = new F; +Suite.prototype.constructor = Suite; + + +/** + * Return a clone of this `Suite`. + * + * @return {Suite} + * @api private + */ + +Suite.prototype.clone = function(){ + var suite = new Suite(this.title); + debug('clone'); + suite.ctx = this.ctx; + suite.timeout(this.timeout()); + suite.enableTimeouts(this.enableTimeouts()); + suite.slow(this.slow()); + suite.bail(this.bail()); + return suite; +}; + +/** + * Set timeout `ms` or short-hand such as "2s". + * + * @param {Number|String} ms + * @return {Suite|Number} for chaining + * @api private + */ + +Suite.prototype.timeout = function(ms){ + if (0 == arguments.length) return this._timeout; + if ('string' == typeof ms) ms = milliseconds(ms); + debug('timeout %d', ms); + this._timeout = parseInt(ms, 10); + return this; +}; + +/** + * Set timeout `enabled`. + * + * @param {Boolean} enabled + * @return {Suite|Boolean} self or enabled + * @api private + */ + +Suite.prototype.enableTimeouts = function(enabled){ + if (arguments.length === 0) return this._enableTimeouts; + debug('enableTimeouts %s', enabled); + this._enableTimeouts = enabled; + return this; +} + +/** + * Set slow `ms` or short-hand such as "2s". + * + * @param {Number|String} ms + * @return {Suite|Number} for chaining + * @api private + */ + +Suite.prototype.slow = function(ms){ + if (0 === arguments.length) return this._slow; + if ('string' == typeof ms) ms = milliseconds(ms); + debug('slow %d', ms); + this._slow = ms; + return this; +}; + +/** + * Sets whether to bail after first error. + * + * @parma {Boolean} bail + * @return {Suite|Number} for chaining + * @api private + */ + +Suite.prototype.bail = function(bail){ + if (0 == arguments.length) return this._bail; + debug('bail %s', bail); + this._bail = bail; + return this; +}; + +/** + * Run `fn(test[, done])` before running tests. + * + * @param {Function} fn + * @return {Suite} for chaining + * @api private + */ + +Suite.prototype.beforeAll = function(title, fn){ + if (this.pending) return this; + if ('function' === typeof title) { + fn = title; + title = fn.name; + } + title = '"before all" hook' + (title ? ': ' + title : ''); + + var hook = new Hook(title, fn); + hook.parent = this; + hook.timeout(this.timeout()); + hook.enableTimeouts(this.enableTimeouts()); + hook.slow(this.slow()); + hook.ctx = this.ctx; + this._beforeAll.push(hook); + this.emit('beforeAll', hook); + return this; +}; + +/** + * Run `fn(test[, done])` after running tests. + * + * @param {Function} fn + * @return {Suite} for chaining + * @api private + */ + +Suite.prototype.afterAll = function(title, fn){ + if (this.pending) return this; + if ('function' === typeof title) { + fn = title; + title = fn.name; + } + title = '"after all" hook' + (title ? ': ' + title : ''); + + var hook = new Hook(title, fn); + hook.parent = this; + hook.timeout(this.timeout()); + hook.enableTimeouts(this.enableTimeouts()); + hook.slow(this.slow()); + hook.ctx = this.ctx; + this._afterAll.push(hook); + this.emit('afterAll', hook); + return this; +}; + +/** + * Run `fn(test[, done])` before each test case. + * + * @param {Function} fn + * @return {Suite} for chaining + * @api private + */ + +Suite.prototype.beforeEach = function(title, fn){ + if (this.pending) return this; + if ('function' === typeof title) { + fn = title; + title = fn.name; + } + title = '"before each" hook' + (title ? ': ' + title : ''); + + var hook = new Hook(title, fn); + hook.parent = this; + hook.timeout(this.timeout()); + hook.enableTimeouts(this.enableTimeouts()); + hook.slow(this.slow()); + hook.ctx = this.ctx; + this._beforeEach.push(hook); + this.emit('beforeEach', hook); + return this; +}; + +/** + * Run `fn(test[, done])` after each test case. + * + * @param {Function} fn + * @return {Suite} for chaining + * @api private + */ + +Suite.prototype.afterEach = function(title, fn){ + if (this.pending) return this; + if ('function' === typeof title) { + fn = title; + title = fn.name; + } + title = '"after each" hook' + (title ? ': ' + title : ''); + + var hook = new Hook(title, fn); + hook.parent = this; + hook.timeout(this.timeout()); + hook.enableTimeouts(this.enableTimeouts()); + hook.slow(this.slow()); + hook.ctx = this.ctx; + this._afterEach.push(hook); + this.emit('afterEach', hook); + return this; +}; + +/** + * Add a test `suite`. + * + * @param {Suite} suite + * @return {Suite} for chaining + * @api private + */ + +Suite.prototype.addSuite = function(suite){ + suite.parent = this; + suite.timeout(this.timeout()); + suite.enableTimeouts(this.enableTimeouts()); + suite.slow(this.slow()); + suite.bail(this.bail()); + this.suites.push(suite); + this.emit('suite', suite); + return this; +}; + +/** + * Add a `test` to this suite. + * + * @param {Test} test + * @return {Suite} for chaining + * @api private + */ + +Suite.prototype.addTest = function(test){ + test.parent = this; + test.timeout(this.timeout()); + test.enableTimeouts(this.enableTimeouts()); + test.slow(this.slow()); + test.ctx = this.ctx; + this.tests.push(test); + this.emit('test', test); + return this; +}; + +/** + * Return the full title generated by recursively + * concatenating the parent's full title. + * + * @return {String} + * @api public + */ + +Suite.prototype.fullTitle = function(){ + if (this.parent) { + var full = this.parent.fullTitle(); + if (full) return full + ' ' + this.title; + } + return this.title; +}; + +/** + * Return the total number of tests. + * + * @return {Number} + * @api public + */ + +Suite.prototype.total = function(){ + return utils.reduce(this.suites, function(sum, suite){ + return sum + suite.total(); + }, 0) + this.tests.length; +}; + +/** + * Iterates through each suite recursively to find + * all tests. Applies a function in the format + * `fn(test)`. + * + * @param {Function} fn + * @return {Suite} + * @api private + */ + +Suite.prototype.eachTest = function(fn){ + utils.forEach(this.tests, fn); + utils.forEach(this.suites, function(suite){ + suite.eachTest(fn); + }); + return this; +}; + +}); // module: suite.js + +require.register("test.js", function(module, exports, require){ + +/** + * Module dependencies. + */ + +var Runnable = require('./runnable'); + +/** + * Expose `Test`. + */ + +module.exports = Test; + +/** + * Initialize a new `Test` with the given `title` and callback `fn`. + * + * @param {String} title + * @param {Function} fn + * @api private + */ + +function Test(title, fn) { + Runnable.call(this, title, fn); + this.pending = !fn; + this.type = 'test'; +} + +/** + * Inherit from `Runnable.prototype`. + */ + +function F(){}; +F.prototype = Runnable.prototype; +Test.prototype = new F; +Test.prototype.constructor = Test; + + +}); // module: test.js + +require.register("utils.js", function(module, exports, require){ +/** + * Module dependencies. + */ + +var fs = require('browser/fs') + , path = require('browser/path') + , join = path.join + , debug = require('browser/debug')('mocha:watch'); + +/** + * Ignored directories. + */ + +var ignore = ['node_modules', '.git']; + +/** + * Escape special characters in the given string of html. + * + * @param {String} html + * @return {String} + * @api private + */ + +exports.escape = function(html){ + return String(html) + .replace(/&/g, '&') + .replace(/"/g, '"') + .replace(//g, '>'); +}; + +/** + * Array#forEach (<=IE8) + * + * @param {Array} array + * @param {Function} fn + * @param {Object} scope + * @api private + */ + +exports.forEach = function(arr, fn, scope){ + for (var i = 0, l = arr.length; i < l; i++) + fn.call(scope, arr[i], i); +}; + +/** + * Array#map (<=IE8) + * + * @param {Array} array + * @param {Function} fn + * @param {Object} scope + * @api private + */ + +exports.map = function(arr, fn, scope){ + var result = []; + for (var i = 0, l = arr.length; i < l; i++) + result.push(fn.call(scope, arr[i], i)); + return result; +}; + +/** + * Array#indexOf (<=IE8) + * + * @parma {Array} arr + * @param {Object} obj to find index of + * @param {Number} start + * @api private + */ + +exports.indexOf = function(arr, obj, start){ + for (var i = start || 0, l = arr.length; i < l; i++) { + if (arr[i] === obj) + return i; + } + return -1; +}; + +/** + * Array#reduce (<=IE8) + * + * @param {Array} array + * @param {Function} fn + * @param {Object} initial value + * @api private + */ + +exports.reduce = function(arr, fn, val){ + var rval = val; + + for (var i = 0, l = arr.length; i < l; i++) { + rval = fn(rval, arr[i], i, arr); + } + + return rval; +}; + +/** + * Array#filter (<=IE8) + * + * @param {Array} array + * @param {Function} fn + * @api private + */ + +exports.filter = function(arr, fn){ + var ret = []; + + for (var i = 0, l = arr.length; i < l; i++) { + var val = arr[i]; + if (fn(val, i, arr)) ret.push(val); + } + + return ret; +}; + +/** + * Object.keys (<=IE8) + * + * @param {Object} obj + * @return {Array} keys + * @api private + */ + +exports.keys = Object.keys || function(obj) { + var keys = [] + , has = Object.prototype.hasOwnProperty // for `window` on <=IE8 + + for (var key in obj) { + if (has.call(obj, key)) { + keys.push(key); + } + } + + return keys; +}; + +/** + * Watch the given `files` for changes + * and invoke `fn(file)` on modification. + * + * @param {Array} files + * @param {Function} fn + * @api private + */ + +exports.watch = function(files, fn){ + var options = { interval: 100 }; + files.forEach(function(file){ + debug('file %s', file); + fs.watchFile(file, options, function(curr, prev){ + if (prev.mtime < curr.mtime) fn(file); + }); + }); +}; + +/** + * Ignored files. + */ + +function ignored(path){ + return !~ignore.indexOf(path); +} + +/** + * Lookup files in the given `dir`. + * + * @return {Array} + * @api private + */ + +exports.files = function(dir, ext, ret){ + ret = ret || []; + ext = ext || ['js']; + + var re = new RegExp('\\.(' + ext.join('|') + ')$'); + + fs.readdirSync(dir) + .filter(ignored) + .forEach(function(path){ + path = join(dir, path); + if (fs.statSync(path).isDirectory()) { + exports.files(path, ext, ret); + } else if (path.match(re)) { + ret.push(path); + } + }); + + return ret; +}; + +/** + * Compute a slug from the given `str`. + * + * @param {String} str + * @return {String} + * @api private + */ + +exports.slug = function(str){ + return str + .toLowerCase() + .replace(/ +/g, '-') + .replace(/[^-\w]/g, ''); +}; + +/** + * Strip the function definition from `str`, + * and re-indent for pre whitespace. + */ + +exports.clean = function(str) { + str = str + .replace(/\r\n?|[\n\u2028\u2029]/g, "\n").replace(/^\uFEFF/, '') + .replace(/^function *\(.*\) *{|\(.*\) *=> *{?/, '') + .replace(/\s+\}$/, ''); + + var spaces = str.match(/^\n?( *)/)[1].length + , tabs = str.match(/^\n?(\t*)/)[1].length + , re = new RegExp('^\n?' + (tabs ? '\t' : ' ') + '{' + (tabs ? tabs : spaces) + '}', 'gm'); + + str = str.replace(re, ''); + + return exports.trim(str); +}; + +/** + * Escape regular expression characters in `str`. + * + * @param {String} str + * @return {String} + * @api private + */ + +exports.escapeRegexp = function(str){ + return str.replace(/[-\\^$*+?.()|[\]{}]/g, "\\$&"); +}; + +/** + * Trim the given `str`. + * + * @param {String} str + * @return {String} + * @api private + */ + +exports.trim = function(str){ + return str.replace(/^\s+|\s+$/g, ''); +}; + +/** + * Parse the given `qs`. + * + * @param {String} qs + * @return {Object} + * @api private + */ + +exports.parseQuery = function(qs){ + return exports.reduce(qs.replace('?', '').split('&'), function(obj, pair){ + var i = pair.indexOf('=') + , key = pair.slice(0, i) + , val = pair.slice(++i); + + obj[key] = decodeURIComponent(val); + return obj; + }, {}); +}; + +/** + * Highlight the given string of `js`. + * + * @param {String} js + * @return {String} + * @api private + */ + +function highlight(js) { + return js + .replace(//g, '>') + .replace(/\/\/(.*)/gm, '//$1') + .replace(/('.*?')/gm, '$1') + .replace(/(\d+\.\d+)/gm, '$1') + .replace(/(\d+)/gm, '$1') + .replace(/\bnew[ \t]+(\w+)/gm, 'new $1') + .replace(/\b(function|new|throw|return|var|if|else)\b/gm, '$1') +} + +/** + * Highlight the contents of tag `name`. + * + * @param {String} name + * @api private + */ + +exports.highlightTags = function(name) { + var code = document.getElementsByTagName(name); + for (var i = 0, len = code.length; i < len; ++i) { + code[i].innerHTML = highlight(code[i].innerHTML); + } +}; + + +/** + * Stringify `obj`. + * + * @param {Object} obj + * @return {String} + * @api private + */ + +exports.stringify = function(obj) { + if (obj instanceof RegExp) return obj.toString(); + return JSON.stringify(exports.canonicalize(obj), null, 2).replace(/,(\n|$)/g, '$1'); +} + +/** + * Return a new object that has the keys in sorted order. + * @param {Object} obj + * @return {Object} + * @api private + */ + +exports.canonicalize = function(obj, stack) { + stack = stack || []; + + if (exports.indexOf(stack, obj) !== -1) return '[Circular]'; + + var canonicalizedObj; + + if ({}.toString.call(obj) === '[object Array]') { + stack.push(obj); + canonicalizedObj = exports.map(obj, function(item) { + return exports.canonicalize(item, stack); + }); + stack.pop(); + } else if (typeof obj === 'object' && obj !== null) { + stack.push(obj); + canonicalizedObj = {}; + exports.forEach(exports.keys(obj).sort(), function(key) { + canonicalizedObj[key] = exports.canonicalize(obj[key], stack); + }); + stack.pop(); + } else { + canonicalizedObj = obj; + } + + return canonicalizedObj; + } + +}); // module: utils.js +// The global object is "self" in Web Workers. +var global = (function() { return this; })(); + +/** + * Save timer references to avoid Sinon interfering (see GH-237). + */ + +var Date = global.Date; +var setTimeout = global.setTimeout; +var setInterval = global.setInterval; +var clearTimeout = global.clearTimeout; +var clearInterval = global.clearInterval; + +/** + * Node shims. + * + * These are meant only to allow + * mocha.js to run untouched, not + * to allow running node code in + * the browser. + */ + +var process = {}; +process.exit = function(status){}; +process.stdout = {}; + +var uncaughtExceptionHandlers = []; + +var originalOnerrorHandler = global.onerror; + +/** + * Remove uncaughtException listener. + * Revert to original onerror handler if previously defined. + */ + +process.removeListener = function(e, fn){ + if ('uncaughtException' == e) { + if (originalOnerrorHandler) { + global.onerror = originalOnerrorHandler; + } else { + global.onerror = function() {}; + } + var i = Mocha.utils.indexOf(uncaughtExceptionHandlers, fn); + if (i != -1) { uncaughtExceptionHandlers.splice(i, 1); } + } +}; + +/** + * Implements uncaughtException listener. + */ + +process.on = function(e, fn){ + if ('uncaughtException' == e) { + global.onerror = function(err, url, line){ + fn(new Error(err + ' (' + url + ':' + line + ')')); + return true; + }; + uncaughtExceptionHandlers.push(fn); + } +}; + +/** + * Expose mocha. + */ + +var Mocha = global.Mocha = require('mocha'), + mocha = global.mocha = new Mocha({ reporter: 'html' }); + +// The BDD UI is registered by default, but no UI will be functional in the +// browser without an explicit call to the overridden `mocha.ui` (see below). +// Ensure that this default UI does not expose its methods to the global scope. +mocha.suite.removeAllListeners('pre-require'); + +var immediateQueue = [] + , immediateTimeout; + +function timeslice() { + var immediateStart = new Date().getTime(); + while (immediateQueue.length && (new Date().getTime() - immediateStart) < 100) { + immediateQueue.shift()(); + } + if (immediateQueue.length) { + immediateTimeout = setTimeout(timeslice, 0); + } else { + immediateTimeout = null; + } +} + +/** + * High-performance override of Runner.immediately. + */ + +Mocha.Runner.immediately = function(callback) { + immediateQueue.push(callback); + if (!immediateTimeout) { + immediateTimeout = setTimeout(timeslice, 0); + } +}; + +/** + * Function to allow assertion libraries to throw errors directly into mocha. + * This is useful when running tests in a browser because window.onerror will + * only receive the 'message' attribute of the Error. + */ +mocha.throwError = function(err) { + Mocha.utils.forEach(uncaughtExceptionHandlers, function (fn) { + fn(err); + }); + throw err; +}; + +/** + * Override ui to ensure that the ui functions are initialized. + * Normally this would happen in Mocha.prototype.loadFiles. + */ + +mocha.ui = function(ui){ + Mocha.prototype.ui.call(this, ui); + this.suite.emit('pre-require', global, null, this); + return this; +}; + +/** + * Setup mocha with the given setting options. + */ + +mocha.setup = function(opts){ + if ('string' == typeof opts) opts = { ui: opts }; + for (var opt in opts) this[opt](opts[opt]); + return this; +}; + +/** + * Run mocha, returning the Runner. + */ + +mocha.run = function(fn){ + var options = mocha.options; + mocha.globals('location'); + + var query = Mocha.utils.parseQuery(global.location.search || ''); + if (query.grep) mocha.grep(query.grep); + if (query.invert) mocha.invert(); + + return Mocha.prototype.run.call(mocha, function(err){ + // The DOM Document is not available in Web Workers. + if (global.document) { + Mocha.utils.highlightTags('code'); + } + if (fn) fn(err); + }); +}; + +/** + * Expose the process shim. + */ + +Mocha.process = process; +})(); \ No newline at end of file diff --git a/test/mocha/test.html b/test/mocha/test.html new file mode 100644 index 0000000..5100dbb --- /dev/null +++ b/test/mocha/test.html @@ -0,0 +1,43 @@ + + + + Mocha Tests + + + +
    +
    + + + + + + + + diff --git a/tests/mocha-test.js b/test/selenium-driver.js similarity index 53% rename from tests/mocha-test.js rename to test/selenium-driver.js index f55bc8a..3f9af7c 100644 --- a/tests/mocha-test.js +++ b/test/selenium-driver.js @@ -13,35 +13,33 @@ var browserStackConfig = { //If browser_version is not specified, the latest version is used var setups = [ - { browser: 'Chrome'}, - { browser: 'Chrome', browser_version: '35.0'}, - { browser: 'Safari'}, - { browser: 'Safari', browser_version: '6.1'}, - { browser: 'IE'}, - { browser: 'IE', browser_version: '10.0'}, - { browser: 'IE', browser_version: '9.0'}, - { browser: 'Firefox'}, - { browser: 'Firefox', browser_version: '30.0'}, - { device: 'iPhone 5S'}, - { device: 'iPhone 5'}, - { device: 'LG Nexus 4'}, - { device: 'Motorola Razr'} - ]; + {browser: 'Chrome'}, + {browser: 'Chrome', browser_version: '35.0'}, + {browser: 'Safari'}, + {browser: 'Safari', browser_version: '6.1'}, + {browser: 'IE'}, + {browser: 'IE', browser_version: '10.0'}, + {browser: 'IE', browser_version: '9.0'}, + {browser: 'Firefox'}, + {browser: 'Firefox', browser_version: '30.0'}, + {device: 'iPhone 5S'}, + {device: 'iPhone 5'}, + {device: 'LG Nexus 4'}, + {device: 'Motorola Razr'} +]; function setupDriver(capabilities) { driver = new webdriver.Builder(). usingServer('http://hub.browserstack.com/wd/hub'). withCapabilities(capabilities). build(); - - driver.get('http://lvh.me:8080/tests/tests.html'); return driver; } function testForm(driver, formId, submission) { driver.findElement(webdriver.By.css(formId + ' input[type="email"]')).sendKeys(submission); driver.findElement(webdriver.By.css(formId + ' .subscribe-email__button')).click().then(function() { - driver.sleep(800); + driver.sleep(1000); }); driver.wait(function() { return driver.findElement(webdriver.By.css(formId + ' .alert__message')).getText().then(function(text) { @@ -61,12 +59,44 @@ setups.forEach(function (setup) { setupDescription = ' on ' + setup.device; } - test.describe('Forms should work' + setupDescription, function() { - var driver; + var capabilities = objectMerge(browserStackConfig, setup); + var driver = setupDriver(capabilities); + + test.describe('Mocha tests' + setupDescription, function() { test.before(function() { - var capabilities = objectMerge(browserStackConfig, setup); - driver = setupDriver(capabilities); + driver.get('http://localhost:8080/test/mocha/test.html'); + }); + + test.it('all tests pass', function() { + driver.wait(function() { + return driver.executeScript('return mocha_finished;').then(function(finished) { + if (!finished) return false; + + return driver.executeScript('return mocha_stats;').then(function(stats) { + console.log(' Passes: ' + stats.passes + ', Failures: ' + stats.failures + ', Duration: ' + (stats.duration / 1000).toFixed(2) + 's'); + assert(stats.tests > 0, 'No mocha tests were run'); + assert(stats.failures <= 0, 'Some mocha tests failed'); + if (!stats.failures) return true; + + return driver.executeScript('return mocha_failures;').then(function(failures) { + for (var i = 0; i < failures.length; ++i) { + var prefix = ' ' + (i + 1) + '. '; + console.log(prefix + failures[i][0]); + console.log(Array(prefix.length + 1).join(' ') + failures[i][1]); + } + return true; + }); + }); + }); + }); + }); + }); + + test.describe('Forms work' + setupDescription, function() { + + test.before(function() { + driver.get('http://localhost:8080/test/demo/tests.html'); }); test.it('universe form with test@test.com', function() { @@ -88,6 +118,6 @@ setups.forEach(function (setup) { }); test.after(function() { driver.quit(); }); - }); + }); }); \ No newline at end of file diff --git a/test/tests.js b/test/tests.js new file mode 100644 index 0000000..92ca215 --- /dev/null +++ b/test/tests.js @@ -0,0 +1,23 @@ +var assert = require('assert'); +var SubscribeEmail = require('../build/subscribe-email.js'); + +describe('Subscribe Email', function() { + describe('SubscribeEmail()', function() { + it('works', function(done) { + var sendGridForm = SubscribeEmail({ + element: '#sendgrid-form', + service: 'sendgrid', + key: 'SDA+fsU1Qw6S6JIXfgrPngHjsFrn2z8v7VWCgt+a0ln11bNnVF1tvSwDWEK/pRiO' + }); + + assert.deepEqual(true, true); + done(); + }); + + it('fails', function(done) { + assert.deepEqual(true, false); + done(); + }); + }); + +}); From 09b07ea70f6092d62073a337ed3c8eb7dd587d5d Mon Sep 17 00:00:00 2001 From: Josiah Sprague Date: Wed, 22 Oct 2014 10:43:45 -0400 Subject: [PATCH 44/60] [#MmLRZ2E2] improve testing workflow and write pending tests Branch: MmLRZ2E2-development --- gulpfile.js | 24 +++++++++++++++------- test/mocha.opts | 2 +- test/selenium-driver.js | 2 +- test/tests.js | 45 +++++++++++++++++++++++++---------------- 4 files changed, 47 insertions(+), 26 deletions(-) diff --git a/gulpfile.js b/gulpfile.js index 01f2d70..525b040 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -10,19 +10,19 @@ var BrowserStackTunnel = require('browserstacktunnel-wrapper'); var mocha = require('gulp-spawn-mocha'); function handleError(err) { - console.log(err.toString()); + console.log(err.message); this.emit('end'); } // Task groups gulp.task('default', ['build', 'start-server']); -gulp.task('test', function(callback) { +gulp.task('test', function(cb) { runSequence( - ['default', 'build-tests'], - 'start-browserstack-tunnel', + ['build', 'build-tests', 'start-server', 'start-browserstack-tunnel'], 'run-selenium', - callback + ['stop-test-server', 'stop-browserstack-tunnel'], + cb ); }); @@ -57,16 +57,22 @@ gulp.task('build-tests', function() { return bundle(); }); +var devServer; gulp.task('start-server', function(cb) { - http.createServer( + devServer = http.createServer( ecstatic({ root: './' }) ).listen(8080); console.log('Listening on :8080'); cb(); }); +gulp.task('stop-test-server', function(cb) { + devServer.close(cb); +}); + +var browserStackTunnel; gulp.task('start-browserstack-tunnel', function(cb) { - var browserStackTunnel = new BrowserStackTunnel({ + browserStackTunnel = new BrowserStackTunnel({ key: '', hosts: [{ name: 'localhost', @@ -85,6 +91,10 @@ gulp.task('start-browserstack-tunnel', function(cb) { }); }); +gulp.task('stop-browserstack-tunnel', function(cb) { + browserStackTunnel.stop(cb); +}); + gulp.task('run-selenium', function () { return gulp.src('test/selenium-driver.js', {read: false}) .pipe(mocha({timeout: 55000})) diff --git a/test/mocha.opts b/test/mocha.opts index b325f7c..3465159 100644 --- a/test/mocha.opts +++ b/test/mocha.opts @@ -1 +1 @@ ---require mocha-clean \ No newline at end of file +--require mocha-clean/brief \ No newline at end of file diff --git a/test/selenium-driver.js b/test/selenium-driver.js index 3f9af7c..1257656 100644 --- a/test/selenium-driver.js +++ b/test/selenium-driver.js @@ -62,7 +62,7 @@ setups.forEach(function (setup) { var capabilities = objectMerge(browserStackConfig, setup); var driver = setupDriver(capabilities); - test.describe('Mocha tests' + setupDescription, function() { + test.describe('Mocha tests should pass' + setupDescription, function() { test.before(function() { driver.get('http://localhost:8080/test/mocha/test.html'); diff --git a/test/tests.js b/test/tests.js index 92ca215..2eac62e 100644 --- a/test/tests.js +++ b/test/tests.js @@ -1,23 +1,34 @@ var assert = require('assert'); var SubscribeEmail = require('../build/subscribe-email.js'); -describe('Subscribe Email', function() { - describe('SubscribeEmail()', function() { - it('works', function(done) { - var sendGridForm = SubscribeEmail({ - element: '#sendgrid-form', - service: 'sendgrid', - key: 'SDA+fsU1Qw6S6JIXfgrPngHjsFrn2z8v7VWCgt+a0ln11bNnVF1tvSwDWEK/pRiO' - }); - - assert.deepEqual(true, true); - done(); - }); - - it('fails', function(done) { - assert.deepEqual(true, false); - done(); - }); +describe('Subscribe Email Module', function() { + + var sendGridForm = SubscribeEmail({ + element: '#sendgrid-form', + service: 'sendgrid', + key: '' + }); + + it('is an EventEmitter', function(done){ + assert(sendGridForm.emit); + assert(sendGridForm._events); + done(); + }); + + describe('element parameter', function() { + + it('accepts a DOM selector string'); + + it('accepts a jQuery element'); + + }); + + describe('template parameter', function() { + + it('accepts a precompiled handlebars template'); + + it('overrides the default template'); + }); }); From 1295b0ea31c6b8eb0c96891fd5a033b18f681f58 Mon Sep 17 00:00:00 2001 From: Josiah Sprague Date: Wed, 22 Oct 2014 17:32:03 -0400 Subject: [PATCH 45/60] [#MmLRZ2E2] add browser tests write some basic tests, make browserstack run all test sequentially, clean up demo tests, Branch: MmLRZ2E2-development --- gulpfile.js | 2 +- package.json | 1 + test/demo/handlebars.runtime.js | 660 -------------------------------- test/demo/template.js | 13 - test/demo/tests.html | 27 +- test/mocha/test.html | 12 +- test/selenium-driver.js | 13 +- test/test-template.hbs | 1 + test/tests.js | 71 +++- 9 files changed, 72 insertions(+), 728 deletions(-) delete mode 100644 test/demo/handlebars.runtime.js delete mode 100644 test/demo/template.js create mode 100644 test/test-template.hbs diff --git a/gulpfile.js b/gulpfile.js index 525b040..7ee648a 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -15,7 +15,7 @@ function handleError(err) { } // Task groups -gulp.task('default', ['build', 'start-server']); +gulp.task('default', ['build', 'build-tests', 'start-server']); gulp.task('test', function(cb) { runSequence( diff --git a/package.json b/package.json index 9ea0616..fd08ed5 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "handlebars": "1.3.x", "hbsfy": "^2.1.0", "istanbul": "^0.3.2", + "jquery": "^2.1.1", "mocha": "^1.21.5", "mocha-clean": "^0.3.0", "object-merge": "~2.5.1", diff --git a/test/demo/handlebars.runtime.js b/test/demo/handlebars.runtime.js deleted file mode 100644 index 932fb7a..0000000 --- a/test/demo/handlebars.runtime.js +++ /dev/null @@ -1,660 +0,0 @@ -/*! - - handlebars v2.0.0 - -Copyright (C) 2011-2014 by Yehuda Katz - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -@license -*/ -/* exported Handlebars */ -(function (root, factory) { - if (typeof define === 'function' && define.amd) { - define([], factory); - } else if (typeof exports === 'object') { - module.exports = factory(); - } else { - root.Handlebars = root.Handlebars || factory(); - } -}(this, function () { -// handlebars/safe-string.js -var __module3__ = (function() { - "use strict"; - var __exports__; - // Build out our basic SafeString type - function SafeString(string) { - this.string = string; - } - - SafeString.prototype.toString = function() { - return "" + this.string; - }; - - __exports__ = SafeString; - return __exports__; -})(); - -// handlebars/utils.js -var __module2__ = (function(__dependency1__) { - "use strict"; - var __exports__ = {}; - /*jshint -W004 */ - var SafeString = __dependency1__; - - var escape = { - "&": "&", - "<": "<", - ">": ">", - '"': """, - "'": "'", - "`": "`" - }; - - var badChars = /[&<>"'`]/g; - var possible = /[&<>"'`]/; - - function escapeChar(chr) { - return escape[chr]; - } - - function extend(obj /* , ...source */) { - for (var i = 1; i < arguments.length; i++) { - for (var key in arguments[i]) { - if (Object.prototype.hasOwnProperty.call(arguments[i], key)) { - obj[key] = arguments[i][key]; - } - } - } - - return obj; - } - - __exports__.extend = extend;var toString = Object.prototype.toString; - __exports__.toString = toString; - // Sourced from lodash - // https://github.com/bestiejs/lodash/blob/master/LICENSE.txt - var isFunction = function(value) { - return typeof value === 'function'; - }; - // fallback for older versions of Chrome and Safari - /* istanbul ignore next */ - if (isFunction(/x/)) { - isFunction = function(value) { - return typeof value === 'function' && toString.call(value) === '[object Function]'; - }; - } - var isFunction; - __exports__.isFunction = isFunction; - /* istanbul ignore next */ - var isArray = Array.isArray || function(value) { - return (value && typeof value === 'object') ? toString.call(value) === '[object Array]' : false; - }; - __exports__.isArray = isArray; - - function escapeExpression(string) { - // don't escape SafeStrings, since they're already safe - if (string instanceof SafeString) { - return string.toString(); - } else if (string == null) { - return ""; - } else if (!string) { - return string + ''; - } - - // Force a string conversion as this will be done by the append regardless and - // the regex test will do this transparently behind the scenes, causing issues if - // an object's to string has escaped characters in it. - string = "" + string; - - if(!possible.test(string)) { return string; } - return string.replace(badChars, escapeChar); - } - - __exports__.escapeExpression = escapeExpression;function isEmpty(value) { - if (!value && value !== 0) { - return true; - } else if (isArray(value) && value.length === 0) { - return true; - } else { - return false; - } - } - - __exports__.isEmpty = isEmpty;function appendContextPath(contextPath, id) { - return (contextPath ? contextPath + '.' : '') + id; - } - - __exports__.appendContextPath = appendContextPath; - return __exports__; -})(__module3__); - -// handlebars/exception.js -var __module4__ = (function() { - "use strict"; - var __exports__; - - var errorProps = ['description', 'fileName', 'lineNumber', 'message', 'name', 'number', 'stack']; - - function Exception(message, node) { - var line; - if (node && node.firstLine) { - line = node.firstLine; - - message += ' - ' + line + ':' + node.firstColumn; - } - - var tmp = Error.prototype.constructor.call(this, message); - - // Unfortunately errors are not enumerable in Chrome (at least), so `for prop in tmp` doesn't work. - for (var idx = 0; idx < errorProps.length; idx++) { - this[errorProps[idx]] = tmp[errorProps[idx]]; - } - - if (line) { - this.lineNumber = line; - this.column = node.firstColumn; - } - } - - Exception.prototype = new Error(); - - __exports__ = Exception; - return __exports__; -})(); - -// handlebars/base.js -var __module1__ = (function(__dependency1__, __dependency2__) { - "use strict"; - var __exports__ = {}; - var Utils = __dependency1__; - var Exception = __dependency2__; - - var VERSION = "2.0.0"; - __exports__.VERSION = VERSION;var COMPILER_REVISION = 6; - __exports__.COMPILER_REVISION = COMPILER_REVISION; - var REVISION_CHANGES = { - 1: '<= 1.0.rc.2', // 1.0.rc.2 is actually rev2 but doesn't report it - 2: '== 1.0.0-rc.3', - 3: '== 1.0.0-rc.4', - 4: '== 1.x.x', - 5: '== 2.0.0-alpha.x', - 6: '>= 2.0.0-beta.1' - }; - __exports__.REVISION_CHANGES = REVISION_CHANGES; - var isArray = Utils.isArray, - isFunction = Utils.isFunction, - toString = Utils.toString, - objectType = '[object Object]'; - - function HandlebarsEnvironment(helpers, partials) { - this.helpers = helpers || {}; - this.partials = partials || {}; - - registerDefaultHelpers(this); - } - - __exports__.HandlebarsEnvironment = HandlebarsEnvironment;HandlebarsEnvironment.prototype = { - constructor: HandlebarsEnvironment, - - logger: logger, - log: log, - - registerHelper: function(name, fn) { - if (toString.call(name) === objectType) { - if (fn) { throw new Exception('Arg not supported with multiple helpers'); } - Utils.extend(this.helpers, name); - } else { - this.helpers[name] = fn; - } - }, - unregisterHelper: function(name) { - delete this.helpers[name]; - }, - - registerPartial: function(name, partial) { - if (toString.call(name) === objectType) { - Utils.extend(this.partials, name); - } else { - this.partials[name] = partial; - } - }, - unregisterPartial: function(name) { - delete this.partials[name]; - } - }; - - function registerDefaultHelpers(instance) { - instance.registerHelper('helperMissing', function(/* [args, ]options */) { - if(arguments.length === 1) { - // A missing field in a {{foo}} constuct. - return undefined; - } else { - // Someone is actually trying to call something, blow up. - throw new Exception("Missing helper: '" + arguments[arguments.length-1].name + "'"); - } - }); - - instance.registerHelper('blockHelperMissing', function(context, options) { - var inverse = options.inverse, - fn = options.fn; - - if(context === true) { - return fn(this); - } else if(context === false || context == null) { - return inverse(this); - } else if (isArray(context)) { - if(context.length > 0) { - if (options.ids) { - options.ids = [options.name]; - } - - return instance.helpers.each(context, options); - } else { - return inverse(this); - } - } else { - if (options.data && options.ids) { - var data = createFrame(options.data); - data.contextPath = Utils.appendContextPath(options.data.contextPath, options.name); - options = {data: data}; - } - - return fn(context, options); - } - }); - - instance.registerHelper('each', function(context, options) { - if (!options) { - throw new Exception('Must pass iterator to #each'); - } - - var fn = options.fn, inverse = options.inverse; - var i = 0, ret = "", data; - - var contextPath; - if (options.data && options.ids) { - contextPath = Utils.appendContextPath(options.data.contextPath, options.ids[0]) + '.'; - } - - if (isFunction(context)) { context = context.call(this); } - - if (options.data) { - data = createFrame(options.data); - } - - if(context && typeof context === 'object') { - if (isArray(context)) { - for(var j = context.length; i= 2.0.0-beta.1"],"main":function(depth0,helpers,partials,data) { - var helper, functionType="function", helperMissing=helpers.helperMissing, escapeExpression=this.escapeExpression; - return "\n\n\n

    "; -},"useData":true}); -})(); \ No newline at end of file diff --git a/test/demo/tests.html b/test/demo/tests.html index 880a35f..9ba2b25 100644 --- a/test/demo/tests.html +++ b/test/demo/tests.html @@ -2,38 +2,19 @@ Subscribe Email Demo - - -

    Here's a Universe subscribe form!

    -
    - - -

    -
    +

    Here's a SendGrid subscribe form!

    @@ -45,10 +26,6 @@ service: 'sendgrid', key: 'SDA+fsU1Qw6S6JIXfgrPngHjsFrn2z8v7VWCgt+a0ln11bNnVF1tvSwDWEK/pRiO' }); - - sendGridForm.on('subscriptionMessage', function (payload) { - console.log("SendGrid Message: ", payload); - });

    Here's a MailChimp subscribe form!

    diff --git a/test/mocha/test.html b/test/mocha/test.html index 5100dbb..c4b9887 100644 --- a/test/mocha/test.html +++ b/test/mocha/test.html @@ -3,17 +3,12 @@ Mocha Tests + +
    -
    - - - - + \ No newline at end of file diff --git a/test/selenium-driver.js b/test/selenium-driver.js index 1257656..193bbed 100644 --- a/test/selenium-driver.js +++ b/test/selenium-driver.js @@ -59,12 +59,11 @@ setups.forEach(function (setup) { setupDescription = ' on ' + setup.device; } - var capabilities = objectMerge(browserStackConfig, setup); - var driver = setupDriver(capabilities); - test.describe('Mocha tests should pass' + setupDescription, function() { test.before(function() { + var capabilities = objectMerge(browserStackConfig, setup); + var driver = setupDriver(capabilities); driver.get('http://localhost:8080/test/mocha/test.html'); }); @@ -93,25 +92,25 @@ setups.forEach(function (setup) { }); }); - test.describe('Forms work' + setupDescription, function() { + test.describe('Forms should work' + setupDescription, function() { test.before(function() { driver.get('http://localhost:8080/test/demo/tests.html'); }); - test.it('universe form with test@test.com', function() { + test.it('universe form works with test@test.com', function() { var result = testForm(driver, '#universe-form', 'test@test.com'); var expectedResult = new RegExp('Please check your email for confirmation','gi'); return expectedResult.test(result); }); - test.it('sendgrid form with test@test.com', function() { + test.it('sendgrid form works with test@test.com', function() { var result = testForm(driver, '#sendgrid-form', 'test@test.com'); var expectedResult = new RegExp('You have subscribed','gi'); return expectedResult.test(result); }); - test.it('mailchimp form with test@test.com', function() { + test.it('mailchimp form works with test@test.com', function() { var result = testForm(driver, '#mailchimp-form', 'test@test.com'); var expectedResult = new RegExp('0 - This email address looks fake','gi'); return expectedResult.test(result); diff --git a/test/test-template.hbs b/test/test-template.hbs new file mode 100644 index 0000000..3f681cf --- /dev/null +++ b/test/test-template.hbs @@ -0,0 +1 @@ +
    \ No newline at end of file diff --git a/test/tests.js b/test/tests.js index 2eac62e..4af99d7 100644 --- a/test/tests.js +++ b/test/tests.js @@ -1,33 +1,78 @@ var assert = require('assert'); +var $ = require('jquery'); var SubscribeEmail = require('../build/subscribe-email.js'); +var Handlebars = require('handlebars'); +var testTemplate = require('./test-template.hbs'); +var sinon = require('sinon'); describe('Subscribe Email Module', function() { - var sendGridForm = SubscribeEmail({ - element: '#sendgrid-form', - service: 'sendgrid', - key: '' + beforeEach(function(){ + var testElement = $('#test-element'); + if (testElement) { + testElement.remove(); + } + testElement = document.createElement('div'); + testElement.id = 'test-element'; + document.body.appendChild(testElement); }); - it('is an EventEmitter', function(done){ - assert(sendGridForm.emit); - assert(sendGridForm._events); - done(); + describe('Events', function() { + + it('Emits events', function(done){ + var subscribeInstance = SubscribeEmail({ + element: '#test-element', + service: 'universe' + }); + var spy = sinon.spy(subscribeInstance, 'emit'); + subscribeInstance.emit('subscriptionMessage', 'Test Message'); + assert(spy.called); + done(); + }); + + it('fires a subscriptionSuccess event on success'); + it('fires a subscriptionError event on fail'); + it('fires a subscriptionMessage event on both subscribe and fail'); + }); describe('element parameter', function() { - it('accepts a DOM selector string'); + it('accepts a DOM selector string as the element to render the template into', function(done){ + var subscribeInstance = SubscribeEmail({ + element: '#test-element', + service: 'universe' + }); + var emailInputs = $('#test-element input[type=email]'); + assert(emailInputs.length > 0); + done(); + }); - it('accepts a jQuery element'); + it('accepts a jQuery object as the element to render the template into', function(done){ + var jQueryForm = $('#test-element'); + var subscribeInstance = SubscribeEmail({ + element: jQueryForm, + service: 'universe' + }); + var emailInputs = $('#test-element input[type=email]'); + assert(emailInputs.length > 0); + done(); + }); }); describe('template parameter', function() { - it('accepts a precompiled handlebars template'); - - it('overrides the default template'); + it('accepts a precompiled handlebars template that overrides the default template', function(done){ + var subscribeInstance = SubscribeEmail({ + element: '#test-element', + service: 'universe', + template: testTemplate + }); + var testElement = $('#test-element .custom-template'); + assert(testElement.length > 0); + done(); + }); }); From eab4133bf017b1eae7d7a6b61c532097bc287fef Mon Sep 17 00:00:00 2001 From: Josiah Sprague Date: Thu, 23 Oct 2014 17:53:54 -0400 Subject: [PATCH 46/60] [#MmLRZ2E2] add sinon fake server for universe and sendgrid mock universe and sendgrid apis with sinon. sinon.js had to be committed, because the node version of sinon doesnt include fakeServer. Branch: MmLRZ2E2-development --- test/demo/sinon.js | 5073 ++++++++++++++++++++++++++++++++++++++++++ test/demo/tests.html | 67 + 2 files changed, 5140 insertions(+) create mode 100644 test/demo/sinon.js diff --git a/test/demo/sinon.js b/test/demo/sinon.js new file mode 100644 index 0000000..703414d --- /dev/null +++ b/test/demo/sinon.js @@ -0,0 +1,5073 @@ +/** + * Sinon.JS 1.10.3, 2014/07/11 + * + * @author Christian Johansen (christian@cjohansen.no) + * @author Contributors: https://github.com/cjohansen/Sinon.JS/blob/master/AUTHORS + * + * (The BSD License) + * + * Copyright (c) 2010-2014, Christian Johansen, christian@cjohansen.no + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * * Neither the name of Christian Johansen nor the names of his contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +this.sinon = (function () { +var samsam, formatio; +function define(mod, deps, fn) { if (mod == "samsam") { samsam = deps(); } else if (typeof fn === "function") { formatio = fn(samsam); } } +define.amd = {}; +((typeof define === "function" && define.amd && function (m) { define("samsam", m); }) || + (typeof module === "object" && + function (m) { module.exports = m(); }) || // Node + function (m) { this.samsam = m(); } // Browser globals +)(function () { + var o = Object.prototype; + var div = typeof document !== "undefined" && document.createElement("div"); + + function isNaN(value) { + // Unlike global isNaN, this avoids type coercion + // typeof check avoids IE host object issues, hat tip to + // lodash + var val = value; // JsLint thinks value !== value is "weird" + return typeof value === "number" && value !== val; + } + + function getClass(value) { + // Returns the internal [[Class]] by calling Object.prototype.toString + // with the provided value as this. Return value is a string, naming the + // internal class, e.g. "Array" + return o.toString.call(value).split(/[ \]]/)[1]; + } + + /** + * @name samsam.isArguments + * @param Object object + * + * Returns ``true`` if ``object`` is an ``arguments`` object, + * ``false`` otherwise. + */ + function isArguments(object) { + if (typeof object !== "object" || typeof object.length !== "number" || + getClass(object) === "Array") { + return false; + } + if (typeof object.callee == "function") { return true; } + try { + object[object.length] = 6; + delete object[object.length]; + } catch (e) { + return true; + } + return false; + } + + /** + * @name samsam.isElement + * @param Object object + * + * Returns ``true`` if ``object`` is a DOM element node. Unlike + * Underscore.js/lodash, this function will return ``false`` if ``object`` + * is an *element-like* object, i.e. a regular object with a ``nodeType`` + * property that holds the value ``1``. + */ + function isElement(object) { + if (!object || object.nodeType !== 1 || !div) { return false; } + try { + object.appendChild(div); + object.removeChild(div); + } catch (e) { + return false; + } + return true; + } + + /** + * @name samsam.keys + * @param Object object + * + * Return an array of own property names. + */ + function keys(object) { + var ks = [], prop; + for (prop in object) { + if (o.hasOwnProperty.call(object, prop)) { ks.push(prop); } + } + return ks; + } + + /** + * @name samsam.isDate + * @param Object value + * + * Returns true if the object is a ``Date``, or *date-like*. Duck typing + * of date objects work by checking that the object has a ``getTime`` + * function whose return value equals the return value from the object's + * ``valueOf``. + */ + function isDate(value) { + return typeof value.getTime == "function" && + value.getTime() == value.valueOf(); + } + + /** + * @name samsam.isNegZero + * @param Object value + * + * Returns ``true`` if ``value`` is ``-0``. + */ + function isNegZero(value) { + return value === 0 && 1 / value === -Infinity; + } + + /** + * @name samsam.equal + * @param Object obj1 + * @param Object obj2 + * + * Returns ``true`` if two objects are strictly equal. Compared to + * ``===`` there are two exceptions: + * + * - NaN is considered equal to NaN + * - -0 and +0 are not considered equal + */ + function identical(obj1, obj2) { + if (obj1 === obj2 || (isNaN(obj1) && isNaN(obj2))) { + return obj1 !== 0 || isNegZero(obj1) === isNegZero(obj2); + } + } + + + /** + * @name samsam.deepEqual + * @param Object obj1 + * @param Object obj2 + * + * Deep equal comparison. Two values are "deep equal" if: + * + * - They are equal, according to samsam.identical + * - They are both date objects representing the same time + * - They are both arrays containing elements that are all deepEqual + * - They are objects with the same set of properties, and each property + * in ``obj1`` is deepEqual to the corresponding property in ``obj2`` + * + * Supports cyclic objects. + */ + function deepEqualCyclic(obj1, obj2) { + + // used for cyclic comparison + // contain already visited objects + var objects1 = [], + objects2 = [], + // contain pathes (position in the object structure) + // of the already visited objects + // indexes same as in objects arrays + paths1 = [], + paths2 = [], + // contains combinations of already compared objects + // in the manner: { "$1['ref']$2['ref']": true } + compared = {}; + + /** + * used to check, if the value of a property is an object + * (cyclic logic is only needed for objects) + * only needed for cyclic logic + */ + function isObject(value) { + + if (typeof value === 'object' && value !== null && + !(value instanceof Boolean) && + !(value instanceof Date) && + !(value instanceof Number) && + !(value instanceof RegExp) && + !(value instanceof String)) { + + return true; + } + + return false; + } + + /** + * returns the index of the given object in the + * given objects array, -1 if not contained + * only needed for cyclic logic + */ + function getIndex(objects, obj) { + + var i; + for (i = 0; i < objects.length; i++) { + if (objects[i] === obj) { + return i; + } + } + + return -1; + } + + // does the recursion for the deep equal check + return (function deepEqual(obj1, obj2, path1, path2) { + var type1 = typeof obj1; + var type2 = typeof obj2; + + // == null also matches undefined + if (obj1 === obj2 || + isNaN(obj1) || isNaN(obj2) || + obj1 == null || obj2 == null || + type1 !== "object" || type2 !== "object") { + + return identical(obj1, obj2); + } + + // Elements are only equal if identical(expected, actual) + if (isElement(obj1) || isElement(obj2)) { return false; } + + var isDate1 = isDate(obj1), isDate2 = isDate(obj2); + if (isDate1 || isDate2) { + if (!isDate1 || !isDate2 || obj1.getTime() !== obj2.getTime()) { + return false; + } + } + + if (obj1 instanceof RegExp && obj2 instanceof RegExp) { + if (obj1.toString() !== obj2.toString()) { return false; } + } + + var class1 = getClass(obj1); + var class2 = getClass(obj2); + var keys1 = keys(obj1); + var keys2 = keys(obj2); + + if (isArguments(obj1) || isArguments(obj2)) { + if (obj1.length !== obj2.length) { return false; } + } else { + if (type1 !== type2 || class1 !== class2 || + keys1.length !== keys2.length) { + return false; + } + } + + var key, i, l, + // following vars are used for the cyclic logic + value1, value2, + isObject1, isObject2, + index1, index2, + newPath1, newPath2; + + for (i = 0, l = keys1.length; i < l; i++) { + key = keys1[i]; + if (!o.hasOwnProperty.call(obj2, key)) { + return false; + } + + // Start of the cyclic logic + + value1 = obj1[key]; + value2 = obj2[key]; + + isObject1 = isObject(value1); + isObject2 = isObject(value2); + + // determine, if the objects were already visited + // (it's faster to check for isObject first, than to + // get -1 from getIndex for non objects) + index1 = isObject1 ? getIndex(objects1, value1) : -1; + index2 = isObject2 ? getIndex(objects2, value2) : -1; + + // determine the new pathes of the objects + // - for non cyclic objects the current path will be extended + // by current property name + // - for cyclic objects the stored path is taken + newPath1 = index1 !== -1 + ? paths1[index1] + : path1 + '[' + JSON.stringify(key) + ']'; + newPath2 = index2 !== -1 + ? paths2[index2] + : path2 + '[' + JSON.stringify(key) + ']'; + + // stop recursion if current objects are already compared + if (compared[newPath1 + newPath2]) { + return true; + } + + // remember the current objects and their pathes + if (index1 === -1 && isObject1) { + objects1.push(value1); + paths1.push(newPath1); + } + if (index2 === -1 && isObject2) { + objects2.push(value2); + paths2.push(newPath2); + } + + // remember that the current objects are already compared + if (isObject1 && isObject2) { + compared[newPath1 + newPath2] = true; + } + + // End of cyclic logic + + // neither value1 nor value2 is a cycle + // continue with next level + if (!deepEqual(value1, value2, newPath1, newPath2)) { + return false; + } + } + + return true; + + }(obj1, obj2, '$1', '$2')); + } + + var match; + + function arrayContains(array, subset) { + if (subset.length === 0) { return true; } + var i, l, j, k; + for (i = 0, l = array.length; i < l; ++i) { + if (match(array[i], subset[0])) { + for (j = 0, k = subset.length; j < k; ++j) { + if (!match(array[i + j], subset[j])) { return false; } + } + return true; + } + } + return false; + } + + /** + * @name samsam.match + * @param Object object + * @param Object matcher + * + * Compare arbitrary value ``object`` with matcher. + */ + match = function match(object, matcher) { + if (matcher && typeof matcher.test === "function") { + return matcher.test(object); + } + + if (typeof matcher === "function") { + return matcher(object) === true; + } + + if (typeof matcher === "string") { + matcher = matcher.toLowerCase(); + var notNull = typeof object === "string" || !!object; + return notNull && + (String(object)).toLowerCase().indexOf(matcher) >= 0; + } + + if (typeof matcher === "number") { + return matcher === object; + } + + if (typeof matcher === "boolean") { + return matcher === object; + } + + if (getClass(object) === "Array" && getClass(matcher) === "Array") { + return arrayContains(object, matcher); + } + + if (matcher && typeof matcher === "object") { + var prop; + for (prop in matcher) { + if (!match(object[prop], matcher[prop])) { + return false; + } + } + return true; + } + + throw new Error("Matcher was not a string, a number, a " + + "function, a boolean or an object"); + }; + + return { + isArguments: isArguments, + isElement: isElement, + isDate: isDate, + isNegZero: isNegZero, + identical: identical, + deepEqual: deepEqualCyclic, + match: match, + keys: keys + }; +}); +((typeof define === "function" && define.amd && function (m) { + define("formatio", ["samsam"], m); +}) || (typeof module === "object" && function (m) { + module.exports = m(require("samsam")); +}) || function (m) { this.formatio = m(this.samsam); } +)(function (samsam) { + + var formatio = { + excludeConstructors: ["Object", /^.$/], + quoteStrings: true + }; + + var hasOwn = Object.prototype.hasOwnProperty; + + var specialObjects = []; + if (typeof global !== "undefined") { + specialObjects.push({ object: global, value: "[object global]" }); + } + if (typeof document !== "undefined") { + specialObjects.push({ + object: document, + value: "[object HTMLDocument]" + }); + } + if (typeof window !== "undefined") { + specialObjects.push({ object: window, value: "[object Window]" }); + } + + function functionName(func) { + if (!func) { return ""; } + if (func.displayName) { return func.displayName; } + if (func.name) { return func.name; } + var matches = func.toString().match(/function\s+([^\(]+)/m); + return (matches && matches[1]) || ""; + } + + function constructorName(f, object) { + var name = functionName(object && object.constructor); + var excludes = f.excludeConstructors || + formatio.excludeConstructors || []; + + var i, l; + for (i = 0, l = excludes.length; i < l; ++i) { + if (typeof excludes[i] === "string" && excludes[i] === name) { + return ""; + } else if (excludes[i].test && excludes[i].test(name)) { + return ""; + } + } + + return name; + } + + function isCircular(object, objects) { + if (typeof object !== "object") { return false; } + var i, l; + for (i = 0, l = objects.length; i < l; ++i) { + if (objects[i] === object) { return true; } + } + return false; + } + + function ascii(f, object, processed, indent) { + if (typeof object === "string") { + var qs = f.quoteStrings; + var quote = typeof qs !== "boolean" || qs; + return processed || quote ? '"' + object + '"' : object; + } + + if (typeof object === "function" && !(object instanceof RegExp)) { + return ascii.func(object); + } + + processed = processed || []; + + if (isCircular(object, processed)) { return "[Circular]"; } + + if (Object.prototype.toString.call(object) === "[object Array]") { + return ascii.array.call(f, object, processed); + } + + if (!object) { return String((1/object) === -Infinity ? "-0" : object); } + if (samsam.isElement(object)) { return ascii.element(object); } + + if (typeof object.toString === "function" && + object.toString !== Object.prototype.toString) { + return object.toString(); + } + + var i, l; + for (i = 0, l = specialObjects.length; i < l; i++) { + if (object === specialObjects[i].object) { + return specialObjects[i].value; + } + } + + return ascii.object.call(f, object, processed, indent); + } + + ascii.func = function (func) { + return "function " + functionName(func) + "() {}"; + }; + + ascii.array = function (array, processed) { + processed = processed || []; + processed.push(array); + var i, l, pieces = []; + for (i = 0, l = array.length; i < l; ++i) { + pieces.push(ascii(this, array[i], processed)); + } + return "[" + pieces.join(", ") + "]"; + }; + + ascii.object = function (object, processed, indent) { + processed = processed || []; + processed.push(object); + indent = indent || 0; + var pieces = [], properties = samsam.keys(object).sort(); + var length = 3; + var prop, str, obj, i, l; + + for (i = 0, l = properties.length; i < l; ++i) { + prop = properties[i]; + obj = object[prop]; + + if (isCircular(obj, processed)) { + str = "[Circular]"; + } else { + str = ascii(this, obj, processed, indent + 2); + } + + str = (/\s/.test(prop) ? '"' + prop + '"' : prop) + ": " + str; + length += str.length; + pieces.push(str); + } + + var cons = constructorName(this, object); + var prefix = cons ? "[" + cons + "] " : ""; + var is = ""; + for (i = 0, l = indent; i < l; ++i) { is += " "; } + + if (length + indent > 80) { + return prefix + "{\n " + is + pieces.join(",\n " + is) + "\n" + + is + "}"; + } + return prefix + "{ " + pieces.join(", ") + " }"; + }; + + ascii.element = function (element) { + var tagName = element.tagName.toLowerCase(); + var attrs = element.attributes, attr, pairs = [], attrName, i, l, val; + + for (i = 0, l = attrs.length; i < l; ++i) { + attr = attrs.item(i); + attrName = attr.nodeName.toLowerCase().replace("html:", ""); + val = attr.nodeValue; + if (attrName !== "contenteditable" || val !== "inherit") { + if (!!val) { pairs.push(attrName + "=\"" + val + "\""); } + } + } + + var formatted = "<" + tagName + (pairs.length > 0 ? " " : ""); + var content = element.innerHTML; + + if (content.length > 20) { + content = content.substr(0, 20) + "[...]"; + } + + var res = formatted + pairs.join(" ") + ">" + content + + ""; + + return res.replace(/ contentEditable="inherit"/, ""); + }; + + function Formatio(options) { + for (var opt in options) { + this[opt] = options[opt]; + } + } + + Formatio.prototype = { + functionName: functionName, + + configure: function (options) { + return new Formatio(options); + }, + + constructorName: function (object) { + return constructorName(this, object); + }, + + ascii: function (object, processed, indent) { + return ascii(this, object, processed, indent); + } + }; + + return Formatio.prototype; +}); +/*jslint eqeqeq: false, onevar: false, forin: true, nomen: false, regexp: false, plusplus: false*/ +/*global module, require, __dirname, document*/ +/** + * Sinon core utilities. For internal use only. + * + * @author Christian Johansen (christian@cjohansen.no) + * @license BSD + * + * Copyright (c) 2010-2013 Christian Johansen + */ + +var sinon = (function (formatio) { + var div = typeof document != "undefined" && document.createElement("div"); + var hasOwn = Object.prototype.hasOwnProperty; + + function isDOMNode(obj) { + var success = false; + + try { + obj.appendChild(div); + success = div.parentNode == obj; + } catch (e) { + return false; + } finally { + try { + obj.removeChild(div); + } catch (e) { + // Remove failed, not much we can do about that + } + } + + return success; + } + + function isElement(obj) { + return div && obj && obj.nodeType === 1 && isDOMNode(obj); + } + + function isFunction(obj) { + return typeof obj === "function" || !!(obj && obj.constructor && obj.call && obj.apply); + } + + function isReallyNaN(val) { + return typeof val === 'number' && isNaN(val); + } + + function mirrorProperties(target, source) { + for (var prop in source) { + if (!hasOwn.call(target, prop)) { + target[prop] = source[prop]; + } + } + } + + function isRestorable (obj) { + return typeof obj === "function" && typeof obj.restore === "function" && obj.restore.sinon; + } + + var sinon = { + wrapMethod: function wrapMethod(object, property, method) { + if (!object) { + throw new TypeError("Should wrap property of object"); + } + + if (typeof method != "function") { + throw new TypeError("Method wrapper should be function"); + } + + var wrappedMethod = object[property], + error; + + if (!isFunction(wrappedMethod)) { + error = new TypeError("Attempted to wrap " + (typeof wrappedMethod) + " property " + + property + " as function"); + } else if (wrappedMethod.restore && wrappedMethod.restore.sinon) { + error = new TypeError("Attempted to wrap " + property + " which is already wrapped"); + } else if (wrappedMethod.calledBefore) { + var verb = !!wrappedMethod.returns ? "stubbed" : "spied on"; + error = new TypeError("Attempted to wrap " + property + " which is already " + verb); + } + + if (error) { + if (wrappedMethod && wrappedMethod._stack) { + error.stack += '\n--------------\n' + wrappedMethod._stack; + } + throw error; + } + + // IE 8 does not support hasOwnProperty on the window object and Firefox has a problem + // when using hasOwn.call on objects from other frames. + var owned = object.hasOwnProperty ? object.hasOwnProperty(property) : hasOwn.call(object, property); + object[property] = method; + method.displayName = property; + // Set up a stack trace which can be used later to find what line of + // code the original method was created on. + method._stack = (new Error('Stack Trace for original')).stack; + + method.restore = function () { + // For prototype properties try to reset by delete first. + // If this fails (ex: localStorage on mobile safari) then force a reset + // via direct assignment. + if (!owned) { + delete object[property]; + } + if (object[property] === method) { + object[property] = wrappedMethod; + } + }; + + method.restore.sinon = true; + mirrorProperties(method, wrappedMethod); + + return method; + }, + + extend: function extend(target) { + for (var i = 1, l = arguments.length; i < l; i += 1) { + for (var prop in arguments[i]) { + if (arguments[i].hasOwnProperty(prop)) { + target[prop] = arguments[i][prop]; + } + + // DONT ENUM bug, only care about toString + if (arguments[i].hasOwnProperty("toString") && + arguments[i].toString != target.toString) { + target.toString = arguments[i].toString; + } + } + } + + return target; + }, + + create: function create(proto) { + var F = function () {}; + F.prototype = proto; + return new F(); + }, + + deepEqual: function deepEqual(a, b) { + if (sinon.match && sinon.match.isMatcher(a)) { + return a.test(b); + } + + if (typeof a != 'object' || typeof b != 'object') { + if (isReallyNaN(a) && isReallyNaN(b)) { + return true; + } else { + return a === b; + } + } + + if (isElement(a) || isElement(b)) { + return a === b; + } + + if (a === b) { + return true; + } + + if ((a === null && b !== null) || (a !== null && b === null)) { + return false; + } + + if (a instanceof RegExp && b instanceof RegExp) { + return (a.source === b.source) && (a.global === b.global) && + (a.ignoreCase === b.ignoreCase) && (a.multiline === b.multiline); + } + + var aString = Object.prototype.toString.call(a); + if (aString != Object.prototype.toString.call(b)) { + return false; + } + + if (aString == "[object Date]") { + return a.valueOf() === b.valueOf(); + } + + var prop, aLength = 0, bLength = 0; + + if (aString == "[object Array]" && a.length !== b.length) { + return false; + } + + for (prop in a) { + aLength += 1; + + if (!(prop in b)) { + return false; + } + + if (!deepEqual(a[prop], b[prop])) { + return false; + } + } + + for (prop in b) { + bLength += 1; + } + + return aLength == bLength; + }, + + functionName: function functionName(func) { + var name = func.displayName || func.name; + + // Use function decomposition as a last resort to get function + // name. Does not rely on function decomposition to work - if it + // doesn't debugging will be slightly less informative + // (i.e. toString will say 'spy' rather than 'myFunc'). + if (!name) { + var matches = func.toString().match(/function ([^\s\(]+)/); + name = matches && matches[1]; + } + + return name; + }, + + functionToString: function toString() { + if (this.getCall && this.callCount) { + var thisValue, prop, i = this.callCount; + + while (i--) { + thisValue = this.getCall(i).thisValue; + + for (prop in thisValue) { + if (thisValue[prop] === this) { + return prop; + } + } + } + } + + return this.displayName || "sinon fake"; + }, + + getConfig: function (custom) { + var config = {}; + custom = custom || {}; + var defaults = sinon.defaultConfig; + + for (var prop in defaults) { + if (defaults.hasOwnProperty(prop)) { + config[prop] = custom.hasOwnProperty(prop) ? custom[prop] : defaults[prop]; + } + } + + return config; + }, + + format: function (val) { + return "" + val; + }, + + defaultConfig: { + injectIntoThis: true, + injectInto: null, + properties: ["spy", "stub", "mock", "clock", "server", "requests"], + useFakeTimers: true, + useFakeServer: true + }, + + timesInWords: function timesInWords(count) { + return count == 1 && "once" || + count == 2 && "twice" || + count == 3 && "thrice" || + (count || 0) + " times"; + }, + + calledInOrder: function (spies) { + for (var i = 1, l = spies.length; i < l; i++) { + if (!spies[i - 1].calledBefore(spies[i]) || !spies[i].called) { + return false; + } + } + + return true; + }, + + orderByFirstCall: function (spies) { + return spies.sort(function (a, b) { + // uuid, won't ever be equal + var aCall = a.getCall(0); + var bCall = b.getCall(0); + var aId = aCall && aCall.callId || -1; + var bId = bCall && bCall.callId || -1; + + return aId < bId ? -1 : 1; + }); + }, + + log: function () {}, + + logError: function (label, err) { + var msg = label + " threw exception: "; + sinon.log(msg + "[" + err.name + "] " + err.message); + if (err.stack) { sinon.log(err.stack); } + + setTimeout(function () { + err.message = msg + err.message; + throw err; + }, 0); + }, + + typeOf: function (value) { + if (value === null) { + return "null"; + } + else if (value === undefined) { + return "undefined"; + } + var string = Object.prototype.toString.call(value); + return string.substring(8, string.length - 1).toLowerCase(); + }, + + createStubInstance: function (constructor) { + if (typeof constructor !== "function") { + throw new TypeError("The constructor should be a function."); + } + return sinon.stub(sinon.create(constructor.prototype)); + }, + + restore: function (object) { + if (object !== null && typeof object === "object") { + for (var prop in object) { + if (isRestorable(object[prop])) { + object[prop].restore(); + } + } + } + else if (isRestorable(object)) { + object.restore(); + } + } + }; + + var isNode = typeof module !== "undefined" && module.exports && typeof require == "function"; + var isAMD = typeof define === 'function' && typeof define.amd === 'object' && define.amd; + + function makePublicAPI(require, exports, module) { + module.exports = sinon; + sinon.spy = require("./sinon/spy"); + sinon.spyCall = require("./sinon/call"); + sinon.behavior = require("./sinon/behavior"); + sinon.stub = require("./sinon/stub"); + sinon.mock = require("./sinon/mock"); + sinon.collection = require("./sinon/collection"); + sinon.assert = require("./sinon/assert"); + sinon.sandbox = require("./sinon/sandbox"); + sinon.test = require("./sinon/test"); + sinon.testCase = require("./sinon/test_case"); + sinon.match = require("./sinon/match"); + } + + if (isAMD) { + define(makePublicAPI); + } else if (isNode) { + try { + formatio = require("formatio"); + } catch (e) {} + makePublicAPI(require, exports, module); + } + + if (formatio) { + var formatter = formatio.configure({ quoteStrings: false }); + sinon.format = function () { + return formatter.ascii.apply(formatter, arguments); + }; + } else if (isNode) { + try { + var util = require("util"); + sinon.format = function (value) { + return typeof value == "object" && value.toString === Object.prototype.toString ? util.inspect(value) : value; + }; + } catch (e) { + /* Node, but no util module - would be very old, but better safe than + sorry */ + } + } + + return sinon; +}(typeof formatio == "object" && formatio)); + +/* @depend ../sinon.js */ +/*jslint eqeqeq: false, onevar: false, plusplus: false*/ +/*global module, require, sinon*/ +/** + * Match functions + * + * @author Maximilian Antoni (mail@maxantoni.de) + * @license BSD + * + * Copyright (c) 2012 Maximilian Antoni + */ + +(function (sinon) { + var commonJSModule = typeof module !== "undefined" && module.exports && typeof require == "function"; + + if (!sinon && commonJSModule) { + sinon = require("../sinon"); + } + + if (!sinon) { + return; + } + + function assertType(value, type, name) { + var actual = sinon.typeOf(value); + if (actual !== type) { + throw new TypeError("Expected type of " + name + " to be " + + type + ", but was " + actual); + } + } + + var matcher = { + toString: function () { + return this.message; + } + }; + + function isMatcher(object) { + return matcher.isPrototypeOf(object); + } + + function matchObject(expectation, actual) { + if (actual === null || actual === undefined) { + return false; + } + for (var key in expectation) { + if (expectation.hasOwnProperty(key)) { + var exp = expectation[key]; + var act = actual[key]; + if (match.isMatcher(exp)) { + if (!exp.test(act)) { + return false; + } + } else if (sinon.typeOf(exp) === "object") { + if (!matchObject(exp, act)) { + return false; + } + } else if (!sinon.deepEqual(exp, act)) { + return false; + } + } + } + return true; + } + + matcher.or = function (m2) { + if (!arguments.length) { + throw new TypeError("Matcher expected"); + } else if (!isMatcher(m2)) { + m2 = match(m2); + } + var m1 = this; + var or = sinon.create(matcher); + or.test = function (actual) { + return m1.test(actual) || m2.test(actual); + }; + or.message = m1.message + ".or(" + m2.message + ")"; + return or; + }; + + matcher.and = function (m2) { + if (!arguments.length) { + throw new TypeError("Matcher expected"); + } else if (!isMatcher(m2)) { + m2 = match(m2); + } + var m1 = this; + var and = sinon.create(matcher); + and.test = function (actual) { + return m1.test(actual) && m2.test(actual); + }; + and.message = m1.message + ".and(" + m2.message + ")"; + return and; + }; + + var match = function (expectation, message) { + var m = sinon.create(matcher); + var type = sinon.typeOf(expectation); + switch (type) { + case "object": + if (typeof expectation.test === "function") { + m.test = function (actual) { + return expectation.test(actual) === true; + }; + m.message = "match(" + sinon.functionName(expectation.test) + ")"; + return m; + } + var str = []; + for (var key in expectation) { + if (expectation.hasOwnProperty(key)) { + str.push(key + ": " + expectation[key]); + } + } + m.test = function (actual) { + return matchObject(expectation, actual); + }; + m.message = "match(" + str.join(", ") + ")"; + break; + case "number": + m.test = function (actual) { + return expectation == actual; + }; + break; + case "string": + m.test = function (actual) { + if (typeof actual !== "string") { + return false; + } + return actual.indexOf(expectation) !== -1; + }; + m.message = "match(\"" + expectation + "\")"; + break; + case "regexp": + m.test = function (actual) { + if (typeof actual !== "string") { + return false; + } + return expectation.test(actual); + }; + break; + case "function": + m.test = expectation; + if (message) { + m.message = message; + } else { + m.message = "match(" + sinon.functionName(expectation) + ")"; + } + break; + default: + m.test = function (actual) { + return sinon.deepEqual(expectation, actual); + }; + } + if (!m.message) { + m.message = "match(" + expectation + ")"; + } + return m; + }; + + match.isMatcher = isMatcher; + + match.any = match(function () { + return true; + }, "any"); + + match.defined = match(function (actual) { + return actual !== null && actual !== undefined; + }, "defined"); + + match.truthy = match(function (actual) { + return !!actual; + }, "truthy"); + + match.falsy = match(function (actual) { + return !actual; + }, "falsy"); + + match.same = function (expectation) { + return match(function (actual) { + return expectation === actual; + }, "same(" + expectation + ")"); + }; + + match.typeOf = function (type) { + assertType(type, "string", "type"); + return match(function (actual) { + return sinon.typeOf(actual) === type; + }, "typeOf(\"" + type + "\")"); + }; + + match.instanceOf = function (type) { + assertType(type, "function", "type"); + return match(function (actual) { + return actual instanceof type; + }, "instanceOf(" + sinon.functionName(type) + ")"); + }; + + function createPropertyMatcher(propertyTest, messagePrefix) { + return function (property, value) { + assertType(property, "string", "property"); + var onlyProperty = arguments.length === 1; + var message = messagePrefix + "(\"" + property + "\""; + if (!onlyProperty) { + message += ", " + value; + } + message += ")"; + return match(function (actual) { + if (actual === undefined || actual === null || + !propertyTest(actual, property)) { + return false; + } + return onlyProperty || sinon.deepEqual(value, actual[property]); + }, message); + }; + } + + match.has = createPropertyMatcher(function (actual, property) { + if (typeof actual === "object") { + return property in actual; + } + return actual[property] !== undefined; + }, "has"); + + match.hasOwn = createPropertyMatcher(function (actual, property) { + return actual.hasOwnProperty(property); + }, "hasOwn"); + + match.bool = match.typeOf("boolean"); + match.number = match.typeOf("number"); + match.string = match.typeOf("string"); + match.object = match.typeOf("object"); + match.func = match.typeOf("function"); + match.array = match.typeOf("array"); + match.regexp = match.typeOf("regexp"); + match.date = match.typeOf("date"); + + sinon.match = match; + + if (typeof define === "function" && define.amd) { + define(["module"], function(module) { module.exports = match; }); + } else if (commonJSModule) { + module.exports = match; + } +}(typeof sinon == "object" && sinon || null)); + +/** + * @depend ../sinon.js + * @depend match.js + */ +/*jslint eqeqeq: false, onevar: false, plusplus: false*/ +/*global module, require, sinon*/ +/** + * Spy calls + * + * @author Christian Johansen (christian@cjohansen.no) + * @author Maximilian Antoni (mail@maxantoni.de) + * @license BSD + * + * Copyright (c) 2010-2013 Christian Johansen + * Copyright (c) 2013 Maximilian Antoni + */ + +(function (sinon) { + var commonJSModule = typeof module !== "undefined" && module.exports && typeof require == "function"; + if (!sinon && commonJSModule) { + sinon = require("../sinon"); + } + + if (!sinon) { + return; + } + + function throwYieldError(proxy, text, args) { + var msg = sinon.functionName(proxy) + text; + if (args.length) { + msg += " Received [" + slice.call(args).join(", ") + "]"; + } + throw new Error(msg); + } + + var slice = Array.prototype.slice; + + var callProto = { + calledOn: function calledOn(thisValue) { + if (sinon.match && sinon.match.isMatcher(thisValue)) { + return thisValue.test(this.thisValue); + } + return this.thisValue === thisValue; + }, + + calledWith: function calledWith() { + for (var i = 0, l = arguments.length; i < l; i += 1) { + if (!sinon.deepEqual(arguments[i], this.args[i])) { + return false; + } + } + + return true; + }, + + calledWithMatch: function calledWithMatch() { + for (var i = 0, l = arguments.length; i < l; i += 1) { + var actual = this.args[i]; + var expectation = arguments[i]; + if (!sinon.match || !sinon.match(expectation).test(actual)) { + return false; + } + } + return true; + }, + + calledWithExactly: function calledWithExactly() { + return arguments.length == this.args.length && + this.calledWith.apply(this, arguments); + }, + + notCalledWith: function notCalledWith() { + return !this.calledWith.apply(this, arguments); + }, + + notCalledWithMatch: function notCalledWithMatch() { + return !this.calledWithMatch.apply(this, arguments); + }, + + returned: function returned(value) { + return sinon.deepEqual(value, this.returnValue); + }, + + threw: function threw(error) { + if (typeof error === "undefined" || !this.exception) { + return !!this.exception; + } + + return this.exception === error || this.exception.name === error; + }, + + calledWithNew: function calledWithNew() { + return this.proxy.prototype && this.thisValue instanceof this.proxy; + }, + + calledBefore: function (other) { + return this.callId < other.callId; + }, + + calledAfter: function (other) { + return this.callId > other.callId; + }, + + callArg: function (pos) { + this.args[pos](); + }, + + callArgOn: function (pos, thisValue) { + this.args[pos].apply(thisValue); + }, + + callArgWith: function (pos) { + this.callArgOnWith.apply(this, [pos, null].concat(slice.call(arguments, 1))); + }, + + callArgOnWith: function (pos, thisValue) { + var args = slice.call(arguments, 2); + this.args[pos].apply(thisValue, args); + }, + + "yield": function () { + this.yieldOn.apply(this, [null].concat(slice.call(arguments, 0))); + }, + + yieldOn: function (thisValue) { + var args = this.args; + for (var i = 0, l = args.length; i < l; ++i) { + if (typeof args[i] === "function") { + args[i].apply(thisValue, slice.call(arguments, 1)); + return; + } + } + throwYieldError(this.proxy, " cannot yield since no callback was passed.", args); + }, + + yieldTo: function (prop) { + this.yieldToOn.apply(this, [prop, null].concat(slice.call(arguments, 1))); + }, + + yieldToOn: function (prop, thisValue) { + var args = this.args; + for (var i = 0, l = args.length; i < l; ++i) { + if (args[i] && typeof args[i][prop] === "function") { + args[i][prop].apply(thisValue, slice.call(arguments, 2)); + return; + } + } + throwYieldError(this.proxy, " cannot yield to '" + prop + + "' since no callback was passed.", args); + }, + + toString: function () { + var callStr = this.proxy.toString() + "("; + var args = []; + + for (var i = 0, l = this.args.length; i < l; ++i) { + args.push(sinon.format(this.args[i])); + } + + callStr = callStr + args.join(", ") + ")"; + + if (typeof this.returnValue != "undefined") { + callStr += " => " + sinon.format(this.returnValue); + } + + if (this.exception) { + callStr += " !" + this.exception.name; + + if (this.exception.message) { + callStr += "(" + this.exception.message + ")"; + } + } + + return callStr; + } + }; + + callProto.invokeCallback = callProto.yield; + + function createSpyCall(spy, thisValue, args, returnValue, exception, id) { + if (typeof id !== "number") { + throw new TypeError("Call id is not a number"); + } + var proxyCall = sinon.create(callProto); + proxyCall.proxy = spy; + proxyCall.thisValue = thisValue; + proxyCall.args = args; + proxyCall.returnValue = returnValue; + proxyCall.exception = exception; + proxyCall.callId = id; + + return proxyCall; + } + createSpyCall.toString = callProto.toString; // used by mocks + + sinon.spyCall = createSpyCall; + + if (typeof define === "function" && define.amd) { + define(["module"], function(module) { module.exports = createSpyCall; }); + } else if (commonJSModule) { + module.exports = createSpyCall; + } +}(typeof sinon == "object" && sinon || null)); + + +/** + * @depend ../sinon.js + * @depend call.js + */ +/*jslint eqeqeq: false, onevar: false, plusplus: false*/ +/*global module, require, sinon*/ +/** + * Spy functions + * + * @author Christian Johansen (christian@cjohansen.no) + * @license BSD + * + * Copyright (c) 2010-2013 Christian Johansen + */ + +(function (sinon) { + var commonJSModule = typeof module !== "undefined" && module.exports && typeof require == "function"; + var push = Array.prototype.push; + var slice = Array.prototype.slice; + var callId = 0; + + if (!sinon && commonJSModule) { + sinon = require("../sinon"); + } + + if (!sinon) { + return; + } + + function spy(object, property) { + if (!property && typeof object == "function") { + return spy.create(object); + } + + if (!object && !property) { + return spy.create(function () { }); + } + + var method = object[property]; + return sinon.wrapMethod(object, property, spy.create(method)); + } + + function matchingFake(fakes, args, strict) { + if (!fakes) { + return; + } + + for (var i = 0, l = fakes.length; i < l; i++) { + if (fakes[i].matches(args, strict)) { + return fakes[i]; + } + } + } + + function incrementCallCount() { + this.called = true; + this.callCount += 1; + this.notCalled = false; + this.calledOnce = this.callCount == 1; + this.calledTwice = this.callCount == 2; + this.calledThrice = this.callCount == 3; + } + + function createCallProperties() { + this.firstCall = this.getCall(0); + this.secondCall = this.getCall(1); + this.thirdCall = this.getCall(2); + this.lastCall = this.getCall(this.callCount - 1); + } + + var vars = "a,b,c,d,e,f,g,h,i,j,k,l"; + function createProxy(func) { + // Retain the function length: + var p; + if (func.length) { + eval("p = (function proxy(" + vars.substring(0, func.length * 2 - 1) + + ") { return p.invoke(func, this, slice.call(arguments)); });"); + } + else { + p = function proxy() { + return p.invoke(func, this, slice.call(arguments)); + }; + } + return p; + } + + var uuid = 0; + + // Public API + var spyApi = { + reset: function () { + this.called = false; + this.notCalled = true; + this.calledOnce = false; + this.calledTwice = false; + this.calledThrice = false; + this.callCount = 0; + this.firstCall = null; + this.secondCall = null; + this.thirdCall = null; + this.lastCall = null; + this.args = []; + this.returnValues = []; + this.thisValues = []; + this.exceptions = []; + this.callIds = []; + if (this.fakes) { + for (var i = 0; i < this.fakes.length; i++) { + this.fakes[i].reset(); + } + } + }, + + create: function create(func) { + var name; + + if (typeof func != "function") { + func = function () { }; + } else { + name = sinon.functionName(func); + } + + var proxy = createProxy(func); + + sinon.extend(proxy, spy); + delete proxy.create; + sinon.extend(proxy, func); + + proxy.reset(); + proxy.prototype = func.prototype; + proxy.displayName = name || "spy"; + proxy.toString = sinon.functionToString; + proxy._create = sinon.spy.create; + proxy.id = "spy#" + uuid++; + + return proxy; + }, + + invoke: function invoke(func, thisValue, args) { + var matching = matchingFake(this.fakes, args); + var exception, returnValue; + + incrementCallCount.call(this); + push.call(this.thisValues, thisValue); + push.call(this.args, args); + push.call(this.callIds, callId++); + + // Make call properties available from within the spied function: + createCallProperties.call(this); + + try { + if (matching) { + returnValue = matching.invoke(func, thisValue, args); + } else { + returnValue = (this.func || func).apply(thisValue, args); + } + + var thisCall = this.getCall(this.callCount - 1); + if (thisCall.calledWithNew() && typeof returnValue !== 'object') { + returnValue = thisValue; + } + } catch (e) { + exception = e; + } + + push.call(this.exceptions, exception); + push.call(this.returnValues, returnValue); + + // Make return value and exception available in the calls: + createCallProperties.call(this); + + if (exception !== undefined) { + throw exception; + } + + return returnValue; + }, + + named: function named(name) { + this.displayName = name; + return this; + }, + + getCall: function getCall(i) { + if (i < 0 || i >= this.callCount) { + return null; + } + + return sinon.spyCall(this, this.thisValues[i], this.args[i], + this.returnValues[i], this.exceptions[i], + this.callIds[i]); + }, + + getCalls: function () { + var calls = []; + var i; + + for (i = 0; i < this.callCount; i++) { + calls.push(this.getCall(i)); + } + + return calls; + }, + + calledBefore: function calledBefore(spyFn) { + if (!this.called) { + return false; + } + + if (!spyFn.called) { + return true; + } + + return this.callIds[0] < spyFn.callIds[spyFn.callIds.length - 1]; + }, + + calledAfter: function calledAfter(spyFn) { + if (!this.called || !spyFn.called) { + return false; + } + + return this.callIds[this.callCount - 1] > spyFn.callIds[spyFn.callCount - 1]; + }, + + withArgs: function () { + var args = slice.call(arguments); + + if (this.fakes) { + var match = matchingFake(this.fakes, args, true); + + if (match) { + return match; + } + } else { + this.fakes = []; + } + + var original = this; + var fake = this._create(); + fake.matchingAguments = args; + fake.parent = this; + push.call(this.fakes, fake); + + fake.withArgs = function () { + return original.withArgs.apply(original, arguments); + }; + + for (var i = 0; i < this.args.length; i++) { + if (fake.matches(this.args[i])) { + incrementCallCount.call(fake); + push.call(fake.thisValues, this.thisValues[i]); + push.call(fake.args, this.args[i]); + push.call(fake.returnValues, this.returnValues[i]); + push.call(fake.exceptions, this.exceptions[i]); + push.call(fake.callIds, this.callIds[i]); + } + } + createCallProperties.call(fake); + + return fake; + }, + + matches: function (args, strict) { + var margs = this.matchingAguments; + + if (margs.length <= args.length && + sinon.deepEqual(margs, args.slice(0, margs.length))) { + return !strict || margs.length == args.length; + } + }, + + printf: function (format) { + var spy = this; + var args = slice.call(arguments, 1); + var formatter; + + return (format || "").replace(/%(.)/g, function (match, specifyer) { + formatter = spyApi.formatters[specifyer]; + + if (typeof formatter == "function") { + return formatter.call(null, spy, args); + } else if (!isNaN(parseInt(specifyer, 10))) { + return sinon.format(args[specifyer - 1]); + } + + return "%" + specifyer; + }); + } + }; + + function delegateToCalls(method, matchAny, actual, notCalled) { + spyApi[method] = function () { + if (!this.called) { + if (notCalled) { + return notCalled.apply(this, arguments); + } + return false; + } + + var currentCall; + var matches = 0; + + for (var i = 0, l = this.callCount; i < l; i += 1) { + currentCall = this.getCall(i); + + if (currentCall[actual || method].apply(currentCall, arguments)) { + matches += 1; + + if (matchAny) { + return true; + } + } + } + + return matches === this.callCount; + }; + } + + delegateToCalls("calledOn", true); + delegateToCalls("alwaysCalledOn", false, "calledOn"); + delegateToCalls("calledWith", true); + delegateToCalls("calledWithMatch", true); + delegateToCalls("alwaysCalledWith", false, "calledWith"); + delegateToCalls("alwaysCalledWithMatch", false, "calledWithMatch"); + delegateToCalls("calledWithExactly", true); + delegateToCalls("alwaysCalledWithExactly", false, "calledWithExactly"); + delegateToCalls("neverCalledWith", false, "notCalledWith", + function () { return true; }); + delegateToCalls("neverCalledWithMatch", false, "notCalledWithMatch", + function () { return true; }); + delegateToCalls("threw", true); + delegateToCalls("alwaysThrew", false, "threw"); + delegateToCalls("returned", true); + delegateToCalls("alwaysReturned", false, "returned"); + delegateToCalls("calledWithNew", true); + delegateToCalls("alwaysCalledWithNew", false, "calledWithNew"); + delegateToCalls("callArg", false, "callArgWith", function () { + throw new Error(this.toString() + " cannot call arg since it was not yet invoked."); + }); + spyApi.callArgWith = spyApi.callArg; + delegateToCalls("callArgOn", false, "callArgOnWith", function () { + throw new Error(this.toString() + " cannot call arg since it was not yet invoked."); + }); + spyApi.callArgOnWith = spyApi.callArgOn; + delegateToCalls("yield", false, "yield", function () { + throw new Error(this.toString() + " cannot yield since it was not yet invoked."); + }); + // "invokeCallback" is an alias for "yield" since "yield" is invalid in strict mode. + spyApi.invokeCallback = spyApi.yield; + delegateToCalls("yieldOn", false, "yieldOn", function () { + throw new Error(this.toString() + " cannot yield since it was not yet invoked."); + }); + delegateToCalls("yieldTo", false, "yieldTo", function (property) { + throw new Error(this.toString() + " cannot yield to '" + property + + "' since it was not yet invoked."); + }); + delegateToCalls("yieldToOn", false, "yieldToOn", function (property) { + throw new Error(this.toString() + " cannot yield to '" + property + + "' since it was not yet invoked."); + }); + + spyApi.formatters = { + "c": function (spy) { + return sinon.timesInWords(spy.callCount); + }, + + "n": function (spy) { + return spy.toString(); + }, + + "C": function (spy) { + var calls = []; + + for (var i = 0, l = spy.callCount; i < l; ++i) { + var stringifiedCall = " " + spy.getCall(i).toString(); + if (/\n/.test(calls[i - 1])) { + stringifiedCall = "\n" + stringifiedCall; + } + push.call(calls, stringifiedCall); + } + + return calls.length > 0 ? "\n" + calls.join("\n") : ""; + }, + + "t": function (spy) { + var objects = []; + + for (var i = 0, l = spy.callCount; i < l; ++i) { + push.call(objects, sinon.format(spy.thisValues[i])); + } + + return objects.join(", "); + }, + + "*": function (spy, args) { + var formatted = []; + + for (var i = 0, l = args.length; i < l; ++i) { + push.call(formatted, sinon.format(args[i])); + } + + return formatted.join(", "); + } + }; + + sinon.extend(spy, spyApi); + + spy.spyCall = sinon.spyCall; + sinon.spy = spy; + + if (typeof define === "function" && define.amd) { + define(["module"], function(module) { module.exports = spy; }); + } else if (commonJSModule) { + module.exports = spy; + } +}(typeof sinon == "object" && sinon || null)); + +/** + * @depend ../sinon.js + */ +/*jslint eqeqeq: false, onevar: false*/ +/*global module, require, sinon, process, setImmediate, setTimeout*/ +/** + * Stub behavior + * + * @author Christian Johansen (christian@cjohansen.no) + * @author Tim Fischbach (mail@timfischbach.de) + * @license BSD + * + * Copyright (c) 2010-2013 Christian Johansen + */ + +(function (sinon) { + var commonJSModule = typeof module !== "undefined" && module.exports && typeof require == "function"; + + if (!sinon && commonJSModule) { + sinon = require("../sinon"); + } + + if (!sinon) { + return; + } + + var slice = Array.prototype.slice; + var join = Array.prototype.join; + var proto; + + var nextTick = (function () { + if (typeof process === "object" && typeof process.nextTick === "function") { + return process.nextTick; + } else if (typeof setImmediate === "function") { + return setImmediate; + } else { + return function (callback) { + setTimeout(callback, 0); + }; + } + })(); + + function throwsException(error, message) { + if (typeof error == "string") { + this.exception = new Error(message || ""); + this.exception.name = error; + } else if (!error) { + this.exception = new Error("Error"); + } else { + this.exception = error; + } + + return this; + } + + function getCallback(behavior, args) { + var callArgAt = behavior.callArgAt; + + if (callArgAt < 0) { + var callArgProp = behavior.callArgProp; + + for (var i = 0, l = args.length; i < l; ++i) { + if (!callArgProp && typeof args[i] == "function") { + return args[i]; + } + + if (callArgProp && args[i] && + typeof args[i][callArgProp] == "function") { + return args[i][callArgProp]; + } + } + + return null; + } + + return args[callArgAt]; + } + + function getCallbackError(behavior, func, args) { + if (behavior.callArgAt < 0) { + var msg; + + if (behavior.callArgProp) { + msg = sinon.functionName(behavior.stub) + + " expected to yield to '" + behavior.callArgProp + + "', but no object with such a property was passed."; + } else { + msg = sinon.functionName(behavior.stub) + + " expected to yield, but no callback was passed."; + } + + if (args.length > 0) { + msg += " Received [" + join.call(args, ", ") + "]"; + } + + return msg; + } + + return "argument at index " + behavior.callArgAt + " is not a function: " + func; + } + + function callCallback(behavior, args) { + if (typeof behavior.callArgAt == "number") { + var func = getCallback(behavior, args); + + if (typeof func != "function") { + throw new TypeError(getCallbackError(behavior, func, args)); + } + + if (behavior.callbackAsync) { + nextTick(function() { + func.apply(behavior.callbackContext, behavior.callbackArguments); + }); + } else { + func.apply(behavior.callbackContext, behavior.callbackArguments); + } + } + } + + proto = { + create: function(stub) { + var behavior = sinon.extend({}, sinon.behavior); + delete behavior.create; + behavior.stub = stub; + + return behavior; + }, + + isPresent: function() { + return (typeof this.callArgAt == 'number' || + this.exception || + typeof this.returnArgAt == 'number' || + this.returnThis || + this.returnValueDefined); + }, + + invoke: function(context, args) { + callCallback(this, args); + + if (this.exception) { + throw this.exception; + } else if (typeof this.returnArgAt == 'number') { + return args[this.returnArgAt]; + } else if (this.returnThis) { + return context; + } + + return this.returnValue; + }, + + onCall: function(index) { + return this.stub.onCall(index); + }, + + onFirstCall: function() { + return this.stub.onFirstCall(); + }, + + onSecondCall: function() { + return this.stub.onSecondCall(); + }, + + onThirdCall: function() { + return this.stub.onThirdCall(); + }, + + withArgs: function(/* arguments */) { + throw new Error('Defining a stub by invoking "stub.onCall(...).withArgs(...)" is not supported. ' + + 'Use "stub.withArgs(...).onCall(...)" to define sequential behavior for calls with certain arguments.'); + }, + + callsArg: function callsArg(pos) { + if (typeof pos != "number") { + throw new TypeError("argument index is not number"); + } + + this.callArgAt = pos; + this.callbackArguments = []; + this.callbackContext = undefined; + this.callArgProp = undefined; + this.callbackAsync = false; + + return this; + }, + + callsArgOn: function callsArgOn(pos, context) { + if (typeof pos != "number") { + throw new TypeError("argument index is not number"); + } + if (typeof context != "object") { + throw new TypeError("argument context is not an object"); + } + + this.callArgAt = pos; + this.callbackArguments = []; + this.callbackContext = context; + this.callArgProp = undefined; + this.callbackAsync = false; + + return this; + }, + + callsArgWith: function callsArgWith(pos) { + if (typeof pos != "number") { + throw new TypeError("argument index is not number"); + } + + this.callArgAt = pos; + this.callbackArguments = slice.call(arguments, 1); + this.callbackContext = undefined; + this.callArgProp = undefined; + this.callbackAsync = false; + + return this; + }, + + callsArgOnWith: function callsArgWith(pos, context) { + if (typeof pos != "number") { + throw new TypeError("argument index is not number"); + } + if (typeof context != "object") { + throw new TypeError("argument context is not an object"); + } + + this.callArgAt = pos; + this.callbackArguments = slice.call(arguments, 2); + this.callbackContext = context; + this.callArgProp = undefined; + this.callbackAsync = false; + + return this; + }, + + yields: function () { + this.callArgAt = -1; + this.callbackArguments = slice.call(arguments, 0); + this.callbackContext = undefined; + this.callArgProp = undefined; + this.callbackAsync = false; + + return this; + }, + + yieldsOn: function (context) { + if (typeof context != "object") { + throw new TypeError("argument context is not an object"); + } + + this.callArgAt = -1; + this.callbackArguments = slice.call(arguments, 1); + this.callbackContext = context; + this.callArgProp = undefined; + this.callbackAsync = false; + + return this; + }, + + yieldsTo: function (prop) { + this.callArgAt = -1; + this.callbackArguments = slice.call(arguments, 1); + this.callbackContext = undefined; + this.callArgProp = prop; + this.callbackAsync = false; + + return this; + }, + + yieldsToOn: function (prop, context) { + if (typeof context != "object") { + throw new TypeError("argument context is not an object"); + } + + this.callArgAt = -1; + this.callbackArguments = slice.call(arguments, 2); + this.callbackContext = context; + this.callArgProp = prop; + this.callbackAsync = false; + + return this; + }, + + + "throws": throwsException, + throwsException: throwsException, + + returns: function returns(value) { + this.returnValue = value; + this.returnValueDefined = true; + + return this; + }, + + returnsArg: function returnsArg(pos) { + if (typeof pos != "number") { + throw new TypeError("argument index is not number"); + } + + this.returnArgAt = pos; + + return this; + }, + + returnsThis: function returnsThis() { + this.returnThis = true; + + return this; + } + }; + + // create asynchronous versions of callsArg* and yields* methods + for (var method in proto) { + // need to avoid creating anotherasync versions of the newly added async methods + if (proto.hasOwnProperty(method) && + method.match(/^(callsArg|yields)/) && + !method.match(/Async/)) { + proto[method + 'Async'] = (function (syncFnName) { + return function () { + var result = this[syncFnName].apply(this, arguments); + this.callbackAsync = true; + return result; + }; + })(method); + } + } + + sinon.behavior = proto; + + if (typeof define === "function" && define.amd) { + define(["module"], function(module) { module.exports = proto; }); + } else if (commonJSModule) { + module.exports = proto; + } +}(typeof sinon == "object" && sinon || null)); + +/** + * @depend ../sinon.js + * @depend spy.js + * @depend behavior.js + */ +/*jslint eqeqeq: false, onevar: false*/ +/*global module, require, sinon*/ +/** + * Stub functions + * + * @author Christian Johansen (christian@cjohansen.no) + * @license BSD + * + * Copyright (c) 2010-2013 Christian Johansen + */ + +(function (sinon) { + var commonJSModule = typeof module !== "undefined" && module.exports && typeof require == "function"; + + if (!sinon && commonJSModule) { + sinon = require("../sinon"); + } + + if (!sinon) { + return; + } + + function stub(object, property, func) { + if (!!func && typeof func != "function") { + throw new TypeError("Custom stub should be function"); + } + + var wrapper; + + if (func) { + wrapper = sinon.spy && sinon.spy.create ? sinon.spy.create(func) : func; + } else { + wrapper = stub.create(); + } + + if (!object && typeof property === "undefined") { + return sinon.stub.create(); + } + + if (typeof property === "undefined" && typeof object == "object") { + for (var prop in object) { + if (typeof object[prop] === "function") { + stub(object, prop); + } + } + + return object; + } + + return sinon.wrapMethod(object, property, wrapper); + } + + function getDefaultBehavior(stub) { + return stub.defaultBehavior || getParentBehaviour(stub) || sinon.behavior.create(stub); + } + + function getParentBehaviour(stub) { + return (stub.parent && getCurrentBehavior(stub.parent)); + } + + function getCurrentBehavior(stub) { + var behavior = stub.behaviors[stub.callCount - 1]; + return behavior && behavior.isPresent() ? behavior : getDefaultBehavior(stub); + } + + var uuid = 0; + + sinon.extend(stub, (function () { + var proto = { + create: function create() { + var functionStub = function () { + return getCurrentBehavior(functionStub).invoke(this, arguments); + }; + + functionStub.id = "stub#" + uuid++; + var orig = functionStub; + functionStub = sinon.spy.create(functionStub); + functionStub.func = orig; + + sinon.extend(functionStub, stub); + functionStub._create = sinon.stub.create; + functionStub.displayName = "stub"; + functionStub.toString = sinon.functionToString; + + functionStub.defaultBehavior = null; + functionStub.behaviors = []; + + return functionStub; + }, + + resetBehavior: function () { + var i; + + this.defaultBehavior = null; + this.behaviors = []; + + delete this.returnValue; + delete this.returnArgAt; + this.returnThis = false; + + if (this.fakes) { + for (i = 0; i < this.fakes.length; i++) { + this.fakes[i].resetBehavior(); + } + } + }, + + onCall: function(index) { + if (!this.behaviors[index]) { + this.behaviors[index] = sinon.behavior.create(this); + } + + return this.behaviors[index]; + }, + + onFirstCall: function() { + return this.onCall(0); + }, + + onSecondCall: function() { + return this.onCall(1); + }, + + onThirdCall: function() { + return this.onCall(2); + } + }; + + for (var method in sinon.behavior) { + if (sinon.behavior.hasOwnProperty(method) && + !proto.hasOwnProperty(method) && + method != 'create' && + method != 'withArgs' && + method != 'invoke') { + proto[method] = (function(behaviorMethod) { + return function() { + this.defaultBehavior = this.defaultBehavior || sinon.behavior.create(this); + this.defaultBehavior[behaviorMethod].apply(this.defaultBehavior, arguments); + return this; + }; + }(method)); + } + } + + return proto; + }())); + + sinon.stub = stub; + + if (typeof define === "function" && define.amd) { + define(["module"], function(module) { module.exports = stub; }); + } else if (commonJSModule) { + module.exports = stub; + } +}(typeof sinon == "object" && sinon || null)); + +/** + * @depend ../sinon.js + * @depend stub.js + */ +/*jslint eqeqeq: false, onevar: false, nomen: false*/ +/*global module, require, sinon*/ +/** + * Mock functions. + * + * @author Christian Johansen (christian@cjohansen.no) + * @license BSD + * + * Copyright (c) 2010-2013 Christian Johansen + */ + +(function (sinon) { + var commonJSModule = typeof module !== "undefined" && module.exports && typeof require == "function"; + var push = [].push; + var match; + + if (!sinon && commonJSModule) { + sinon = require("../sinon"); + } + + if (!sinon) { + return; + } + + match = sinon.match; + + if (!match && commonJSModule) { + match = require("./match"); + } + + function mock(object) { + if (!object) { + return sinon.expectation.create("Anonymous mock"); + } + + return mock.create(object); + } + + sinon.mock = mock; + + sinon.extend(mock, (function () { + function each(collection, callback) { + if (!collection) { + return; + } + + for (var i = 0, l = collection.length; i < l; i += 1) { + callback(collection[i]); + } + } + + return { + create: function create(object) { + if (!object) { + throw new TypeError("object is null"); + } + + var mockObject = sinon.extend({}, mock); + mockObject.object = object; + delete mockObject.create; + + return mockObject; + }, + + expects: function expects(method) { + if (!method) { + throw new TypeError("method is falsy"); + } + + if (!this.expectations) { + this.expectations = {}; + this.proxies = []; + } + + if (!this.expectations[method]) { + this.expectations[method] = []; + var mockObject = this; + + sinon.wrapMethod(this.object, method, function () { + return mockObject.invokeMethod(method, this, arguments); + }); + + push.call(this.proxies, method); + } + + var expectation = sinon.expectation.create(method); + push.call(this.expectations[method], expectation); + + return expectation; + }, + + restore: function restore() { + var object = this.object; + + each(this.proxies, function (proxy) { + if (typeof object[proxy].restore == "function") { + object[proxy].restore(); + } + }); + }, + + verify: function verify() { + var expectations = this.expectations || {}; + var messages = [], met = []; + + each(this.proxies, function (proxy) { + each(expectations[proxy], function (expectation) { + if (!expectation.met()) { + push.call(messages, expectation.toString()); + } else { + push.call(met, expectation.toString()); + } + }); + }); + + this.restore(); + + if (messages.length > 0) { + sinon.expectation.fail(messages.concat(met).join("\n")); + } else { + sinon.expectation.pass(messages.concat(met).join("\n")); + } + + return true; + }, + + invokeMethod: function invokeMethod(method, thisValue, args) { + var expectations = this.expectations && this.expectations[method]; + var length = expectations && expectations.length || 0, i; + + for (i = 0; i < length; i += 1) { + if (!expectations[i].met() && + expectations[i].allowsCall(thisValue, args)) { + return expectations[i].apply(thisValue, args); + } + } + + var messages = [], available, exhausted = 0; + + for (i = 0; i < length; i += 1) { + if (expectations[i].allowsCall(thisValue, args)) { + available = available || expectations[i]; + } else { + exhausted += 1; + } + push.call(messages, " " + expectations[i].toString()); + } + + if (exhausted === 0) { + return available.apply(thisValue, args); + } + + messages.unshift("Unexpected call: " + sinon.spyCall.toString.call({ + proxy: method, + args: args + })); + + sinon.expectation.fail(messages.join("\n")); + } + }; + }())); + + var times = sinon.timesInWords; + + sinon.expectation = (function () { + var slice = Array.prototype.slice; + var _invoke = sinon.spy.invoke; + + function callCountInWords(callCount) { + if (callCount == 0) { + return "never called"; + } else { + return "called " + times(callCount); + } + } + + function expectedCallCountInWords(expectation) { + var min = expectation.minCalls; + var max = expectation.maxCalls; + + if (typeof min == "number" && typeof max == "number") { + var str = times(min); + + if (min != max) { + str = "at least " + str + " and at most " + times(max); + } + + return str; + } + + if (typeof min == "number") { + return "at least " + times(min); + } + + return "at most " + times(max); + } + + function receivedMinCalls(expectation) { + var hasMinLimit = typeof expectation.minCalls == "number"; + return !hasMinLimit || expectation.callCount >= expectation.minCalls; + } + + function receivedMaxCalls(expectation) { + if (typeof expectation.maxCalls != "number") { + return false; + } + + return expectation.callCount == expectation.maxCalls; + } + + function verifyMatcher(possibleMatcher, arg){ + if (match && match.isMatcher(possibleMatcher)) { + return possibleMatcher.test(arg); + } else { + return true; + } + } + + return { + minCalls: 1, + maxCalls: 1, + + create: function create(methodName) { + var expectation = sinon.extend(sinon.stub.create(), sinon.expectation); + delete expectation.create; + expectation.method = methodName; + + return expectation; + }, + + invoke: function invoke(func, thisValue, args) { + this.verifyCallAllowed(thisValue, args); + + return _invoke.apply(this, arguments); + }, + + atLeast: function atLeast(num) { + if (typeof num != "number") { + throw new TypeError("'" + num + "' is not number"); + } + + if (!this.limitsSet) { + this.maxCalls = null; + this.limitsSet = true; + } + + this.minCalls = num; + + return this; + }, + + atMost: function atMost(num) { + if (typeof num != "number") { + throw new TypeError("'" + num + "' is not number"); + } + + if (!this.limitsSet) { + this.minCalls = null; + this.limitsSet = true; + } + + this.maxCalls = num; + + return this; + }, + + never: function never() { + return this.exactly(0); + }, + + once: function once() { + return this.exactly(1); + }, + + twice: function twice() { + return this.exactly(2); + }, + + thrice: function thrice() { + return this.exactly(3); + }, + + exactly: function exactly(num) { + if (typeof num != "number") { + throw new TypeError("'" + num + "' is not a number"); + } + + this.atLeast(num); + return this.atMost(num); + }, + + met: function met() { + return !this.failed && receivedMinCalls(this); + }, + + verifyCallAllowed: function verifyCallAllowed(thisValue, args) { + if (receivedMaxCalls(this)) { + this.failed = true; + sinon.expectation.fail(this.method + " already called " + times(this.maxCalls)); + } + + if ("expectedThis" in this && this.expectedThis !== thisValue) { + sinon.expectation.fail(this.method + " called with " + thisValue + " as thisValue, expected " + + this.expectedThis); + } + + if (!("expectedArguments" in this)) { + return; + } + + if (!args) { + sinon.expectation.fail(this.method + " received no arguments, expected " + + sinon.format(this.expectedArguments)); + } + + if (args.length < this.expectedArguments.length) { + sinon.expectation.fail(this.method + " received too few arguments (" + sinon.format(args) + + "), expected " + sinon.format(this.expectedArguments)); + } + + if (this.expectsExactArgCount && + args.length != this.expectedArguments.length) { + sinon.expectation.fail(this.method + " received too many arguments (" + sinon.format(args) + + "), expected " + sinon.format(this.expectedArguments)); + } + + for (var i = 0, l = this.expectedArguments.length; i < l; i += 1) { + + if (!verifyMatcher(this.expectedArguments[i],args[i])) { + sinon.expectation.fail(this.method + " received wrong arguments " + sinon.format(args) + + ", didn't match " + this.expectedArguments.toString()); + } + + if (!sinon.deepEqual(this.expectedArguments[i], args[i])) { + sinon.expectation.fail(this.method + " received wrong arguments " + sinon.format(args) + + ", expected " + sinon.format(this.expectedArguments)); + } + } + }, + + allowsCall: function allowsCall(thisValue, args) { + if (this.met() && receivedMaxCalls(this)) { + return false; + } + + if ("expectedThis" in this && this.expectedThis !== thisValue) { + return false; + } + + if (!("expectedArguments" in this)) { + return true; + } + + args = args || []; + + if (args.length < this.expectedArguments.length) { + return false; + } + + if (this.expectsExactArgCount && + args.length != this.expectedArguments.length) { + return false; + } + + for (var i = 0, l = this.expectedArguments.length; i < l; i += 1) { + if (!verifyMatcher(this.expectedArguments[i],args[i])) { + return false; + } + + if (!sinon.deepEqual(this.expectedArguments[i], args[i])) { + return false; + } + } + + return true; + }, + + withArgs: function withArgs() { + this.expectedArguments = slice.call(arguments); + return this; + }, + + withExactArgs: function withExactArgs() { + this.withArgs.apply(this, arguments); + this.expectsExactArgCount = true; + return this; + }, + + on: function on(thisValue) { + this.expectedThis = thisValue; + return this; + }, + + toString: function () { + var args = (this.expectedArguments || []).slice(); + + if (!this.expectsExactArgCount) { + push.call(args, "[...]"); + } + + var callStr = sinon.spyCall.toString.call({ + proxy: this.method || "anonymous mock expectation", + args: args + }); + + var message = callStr.replace(", [...", "[, ...") + " " + + expectedCallCountInWords(this); + + if (this.met()) { + return "Expectation met: " + message; + } + + return "Expected " + message + " (" + + callCountInWords(this.callCount) + ")"; + }, + + verify: function verify() { + if (!this.met()) { + sinon.expectation.fail(this.toString()); + } else { + sinon.expectation.pass(this.toString()); + } + + return true; + }, + + pass: function(message) { + sinon.assert.pass(message); + }, + fail: function (message) { + var exception = new Error(message); + exception.name = "ExpectationError"; + + throw exception; + } + }; + }()); + + sinon.mock = mock; + + if (typeof define === "function" && define.amd) { + define(["module"], function(module) { module.exports = mock; }); + } else if (commonJSModule) { + module.exports = mock; + } +}(typeof sinon == "object" && sinon || null)); + +/** + * @depend ../sinon.js + * @depend stub.js + * @depend mock.js + */ +/*jslint eqeqeq: false, onevar: false, forin: true*/ +/*global module, require, sinon*/ +/** + * Collections of stubs, spies and mocks. + * + * @author Christian Johansen (christian@cjohansen.no) + * @license BSD + * + * Copyright (c) 2010-2013 Christian Johansen + */ + +(function (sinon) { + var commonJSModule = typeof module !== "undefined" && module.exports && typeof require == "function"; + var push = [].push; + var hasOwnProperty = Object.prototype.hasOwnProperty; + + if (!sinon && commonJSModule) { + sinon = require("../sinon"); + } + + if (!sinon) { + return; + } + + function getFakes(fakeCollection) { + if (!fakeCollection.fakes) { + fakeCollection.fakes = []; + } + + return fakeCollection.fakes; + } + + function each(fakeCollection, method) { + var fakes = getFakes(fakeCollection); + + for (var i = 0, l = fakes.length; i < l; i += 1) { + if (typeof fakes[i][method] == "function") { + fakes[i][method](); + } + } + } + + function compact(fakeCollection) { + var fakes = getFakes(fakeCollection); + var i = 0; + while (i < fakes.length) { + fakes.splice(i, 1); + } + } + + var collection = { + verify: function resolve() { + each(this, "verify"); + }, + + restore: function restore() { + each(this, "restore"); + compact(this); + }, + + verifyAndRestore: function verifyAndRestore() { + var exception; + + try { + this.verify(); + } catch (e) { + exception = e; + } + + this.restore(); + + if (exception) { + throw exception; + } + }, + + add: function add(fake) { + push.call(getFakes(this), fake); + return fake; + }, + + spy: function spy() { + return this.add(sinon.spy.apply(sinon, arguments)); + }, + + stub: function stub(object, property, value) { + if (property) { + var original = object[property]; + + if (typeof original != "function") { + if (!hasOwnProperty.call(object, property)) { + throw new TypeError("Cannot stub non-existent own property " + property); + } + + object[property] = value; + + return this.add({ + restore: function () { + object[property] = original; + } + }); + } + } + if (!property && !!object && typeof object == "object") { + var stubbedObj = sinon.stub.apply(sinon, arguments); + + for (var prop in stubbedObj) { + if (typeof stubbedObj[prop] === "function") { + this.add(stubbedObj[prop]); + } + } + + return stubbedObj; + } + + return this.add(sinon.stub.apply(sinon, arguments)); + }, + + mock: function mock() { + return this.add(sinon.mock.apply(sinon, arguments)); + }, + + inject: function inject(obj) { + var col = this; + + obj.spy = function () { + return col.spy.apply(col, arguments); + }; + + obj.stub = function () { + return col.stub.apply(col, arguments); + }; + + obj.mock = function () { + return col.mock.apply(col, arguments); + }; + + return obj; + } + }; + + sinon.collection = collection; + + if (typeof define === "function" && define.amd) { + define(["module"], function(module) { module.exports = collection; }); + } else if (commonJSModule) { + module.exports = collection; + } +}(typeof sinon == "object" && sinon || null)); + +/*jslint eqeqeq: false, plusplus: false, evil: true, onevar: false, browser: true, forin: false*/ +/*global module, require, window*/ +/** + * Fake timer API + * setTimeout + * setInterval + * clearTimeout + * clearInterval + * tick + * reset + * Date + * + * Inspired by jsUnitMockTimeOut from JsUnit + * + * @author Christian Johansen (christian@cjohansen.no) + * @license BSD + * + * Copyright (c) 2010-2013 Christian Johansen + */ + +if (typeof sinon == "undefined") { + var sinon = {}; +} + +(function (global) { + // node expects setTimeout/setInterval to return a fn object w/ .ref()/.unref() + // browsers, a number. + // see https://github.com/cjohansen/Sinon.JS/pull/436 + var timeoutResult = setTimeout(function() {}, 0); + var addTimerReturnsObject = typeof timeoutResult === 'object'; + clearTimeout(timeoutResult); + + var id = 1; + + function addTimer(args, recurring) { + if (args.length === 0) { + throw new Error("Function requires at least 1 parameter"); + } + + if (typeof args[0] === "undefined") { + throw new Error("Callback must be provided to timer calls"); + } + + var toId = id++; + var delay = args[1] || 0; + + if (!this.timeouts) { + this.timeouts = {}; + } + + this.timeouts[toId] = { + id: toId, + func: args[0], + callAt: this.now + delay, + invokeArgs: Array.prototype.slice.call(args, 2) + }; + + if (recurring === true) { + this.timeouts[toId].interval = delay; + } + + if (addTimerReturnsObject) { + return { + id: toId, + ref: function() {}, + unref: function() {} + }; + } + else { + return toId; + } + } + + function parseTime(str) { + if (!str) { + return 0; + } + + var strings = str.split(":"); + var l = strings.length, i = l; + var ms = 0, parsed; + + if (l > 3 || !/^(\d\d:){0,2}\d\d?$/.test(str)) { + throw new Error("tick only understands numbers and 'h:m:s'"); + } + + while (i--) { + parsed = parseInt(strings[i], 10); + + if (parsed >= 60) { + throw new Error("Invalid time " + str); + } + + ms += parsed * Math.pow(60, (l - i - 1)); + } + + return ms * 1000; + } + + function createObject(object) { + var newObject; + + if (Object.create) { + newObject = Object.create(object); + } else { + var F = function () {}; + F.prototype = object; + newObject = new F(); + } + + newObject.Date.clock = newObject; + return newObject; + } + + sinon.clock = { + now: 0, + + create: function create(now) { + var clock = createObject(this); + + if (typeof now == "number") { + clock.now = now; + } + + if (!!now && typeof now == "object") { + throw new TypeError("now should be milliseconds since UNIX epoch"); + } + + return clock; + }, + + setTimeout: function setTimeout(callback, timeout) { + return addTimer.call(this, arguments, false); + }, + + clearTimeout: function clearTimeout(timerId) { + if (!timerId) { + // null appears to be allowed in most browsers, and appears to be relied upon by some libraries, like Bootstrap carousel + return; + } + if (!this.timeouts) { + this.timeouts = []; + } + // in Node, timerId is an object with .ref()/.unref(), and + // its .id field is the actual timer id. + if (typeof timerId === 'object') { + timerId = timerId.id + } + if (timerId in this.timeouts) { + delete this.timeouts[timerId]; + } + }, + + setInterval: function setInterval(callback, timeout) { + return addTimer.call(this, arguments, true); + }, + + clearInterval: function clearInterval(timerId) { + this.clearTimeout(timerId); + }, + + setImmediate: function setImmediate(callback) { + var passThruArgs = Array.prototype.slice.call(arguments, 1); + + return addTimer.call(this, [callback, 0].concat(passThruArgs), false); + }, + + clearImmediate: function clearImmediate(timerId) { + this.clearTimeout(timerId); + }, + + tick: function tick(ms) { + ms = typeof ms == "number" ? ms : parseTime(ms); + var tickFrom = this.now, tickTo = this.now + ms, previous = this.now; + var timer = this.firstTimerInRange(tickFrom, tickTo); + + var firstException; + while (timer && tickFrom <= tickTo) { + if (this.timeouts[timer.id]) { + tickFrom = this.now = timer.callAt; + try { + this.callTimer(timer); + } catch (e) { + firstException = firstException || e; + } + } + + timer = this.firstTimerInRange(previous, tickTo); + previous = tickFrom; + } + + this.now = tickTo; + + if (firstException) { + throw firstException; + } + + return this.now; + }, + + firstTimerInRange: function (from, to) { + var timer, smallest = null, originalTimer; + + for (var id in this.timeouts) { + if (this.timeouts.hasOwnProperty(id)) { + if (this.timeouts[id].callAt < from || this.timeouts[id].callAt > to) { + continue; + } + + if (smallest === null || this.timeouts[id].callAt < smallest) { + originalTimer = this.timeouts[id]; + smallest = this.timeouts[id].callAt; + + timer = { + func: this.timeouts[id].func, + callAt: this.timeouts[id].callAt, + interval: this.timeouts[id].interval, + id: this.timeouts[id].id, + invokeArgs: this.timeouts[id].invokeArgs + }; + } + } + } + + return timer || null; + }, + + callTimer: function (timer) { + if (typeof timer.interval == "number") { + this.timeouts[timer.id].callAt += timer.interval; + } else { + delete this.timeouts[timer.id]; + } + + try { + if (typeof timer.func == "function") { + timer.func.apply(null, timer.invokeArgs); + } else { + eval(timer.func); + } + } catch (e) { + var exception = e; + } + + if (!this.timeouts[timer.id]) { + if (exception) { + throw exception; + } + return; + } + + if (exception) { + throw exception; + } + }, + + reset: function reset() { + this.timeouts = {}; + }, + + Date: (function () { + var NativeDate = Date; + + function ClockDate(year, month, date, hour, minute, second, ms) { + // Defensive and verbose to avoid potential harm in passing + // explicit undefined when user does not pass argument + switch (arguments.length) { + case 0: + return new NativeDate(ClockDate.clock.now); + case 1: + return new NativeDate(year); + case 2: + return new NativeDate(year, month); + case 3: + return new NativeDate(year, month, date); + case 4: + return new NativeDate(year, month, date, hour); + case 5: + return new NativeDate(year, month, date, hour, minute); + case 6: + return new NativeDate(year, month, date, hour, minute, second); + default: + return new NativeDate(year, month, date, hour, minute, second, ms); + } + } + + return mirrorDateProperties(ClockDate, NativeDate); + }()) + }; + + function mirrorDateProperties(target, source) { + if (source.now) { + target.now = function now() { + return target.clock.now; + }; + } else { + delete target.now; + } + + if (source.toSource) { + target.toSource = function toSource() { + return source.toSource(); + }; + } else { + delete target.toSource; + } + + target.toString = function toString() { + return source.toString(); + }; + + target.prototype = source.prototype; + target.parse = source.parse; + target.UTC = source.UTC; + target.prototype.toUTCString = source.prototype.toUTCString; + + for (var prop in source) { + if (source.hasOwnProperty(prop)) { + target[prop] = source[prop]; + } + } + + return target; + } + + var methods = ["Date", "setTimeout", "setInterval", + "clearTimeout", "clearInterval"]; + + if (typeof global.setImmediate !== "undefined") { + methods.push("setImmediate"); + } + + if (typeof global.clearImmediate !== "undefined") { + methods.push("clearImmediate"); + } + + function restore() { + var method; + + for (var i = 0, l = this.methods.length; i < l; i++) { + method = this.methods[i]; + + if (global[method].hadOwnProperty) { + global[method] = this["_" + method]; + } else { + try { + delete global[method]; + } catch (e) {} + } + } + + // Prevent multiple executions which will completely remove these props + this.methods = []; + } + + function stubGlobal(method, clock) { + clock[method].hadOwnProperty = Object.prototype.hasOwnProperty.call(global, method); + clock["_" + method] = global[method]; + + if (method == "Date") { + var date = mirrorDateProperties(clock[method], global[method]); + global[method] = date; + } else { + global[method] = function () { + return clock[method].apply(clock, arguments); + }; + + for (var prop in clock[method]) { + if (clock[method].hasOwnProperty(prop)) { + global[method][prop] = clock[method][prop]; + } + } + } + + global[method].clock = clock; + } + + sinon.useFakeTimers = function useFakeTimers(now) { + var clock = sinon.clock.create(now); + clock.restore = restore; + clock.methods = Array.prototype.slice.call(arguments, + typeof now == "number" ? 1 : 0); + + if (clock.methods.length === 0) { + clock.methods = methods; + } + + for (var i = 0, l = clock.methods.length; i < l; i++) { + stubGlobal(clock.methods[i], clock); + } + + return clock; + }; +}(typeof global != "undefined" && typeof global !== "function" ? global : this)); + +sinon.timers = { + setTimeout: setTimeout, + clearTimeout: clearTimeout, + setImmediate: (typeof setImmediate !== "undefined" ? setImmediate : undefined), + clearImmediate: (typeof clearImmediate !== "undefined" ? clearImmediate: undefined), + setInterval: setInterval, + clearInterval: clearInterval, + Date: Date +}; + +if (typeof module !== 'undefined' && module.exports) { + module.exports = sinon; +} + +/*jslint eqeqeq: false, onevar: false*/ +/*global sinon, module, require, ActiveXObject, XMLHttpRequest, DOMParser*/ +/** + * Minimal Event interface implementation + * + * Original implementation by Sven Fuchs: https://gist.github.com/995028 + * Modifications and tests by Christian Johansen. + * + * @author Sven Fuchs (svenfuchs@artweb-design.de) + * @author Christian Johansen (christian@cjohansen.no) + * @license BSD + * + * Copyright (c) 2011 Sven Fuchs, Christian Johansen + */ + +if (typeof sinon == "undefined") { + this.sinon = {}; +} + +(function () { + var push = [].push; + + sinon.Event = function Event(type, bubbles, cancelable, target) { + this.initEvent(type, bubbles, cancelable, target); + }; + + sinon.Event.prototype = { + initEvent: function(type, bubbles, cancelable, target) { + this.type = type; + this.bubbles = bubbles; + this.cancelable = cancelable; + this.target = target; + }, + + stopPropagation: function () {}, + + preventDefault: function () { + this.defaultPrevented = true; + } + }; + + sinon.ProgressEvent = function ProgressEvent(type, progressEventRaw, target) { + this.initEvent(type, false, false, target); + this.loaded = progressEventRaw.loaded || null; + this.total = progressEventRaw.total || null; + }; + + sinon.ProgressEvent.prototype = new sinon.Event(); + + sinon.ProgressEvent.prototype.constructor = sinon.ProgressEvent; + + sinon.CustomEvent = function CustomEvent(type, customData, target) { + this.initEvent(type, false, false, target); + this.detail = customData.detail || null; + }; + + sinon.CustomEvent.prototype = new sinon.Event(); + + sinon.CustomEvent.prototype.constructor = sinon.CustomEvent; + + sinon.EventTarget = { + addEventListener: function addEventListener(event, listener) { + this.eventListeners = this.eventListeners || {}; + this.eventListeners[event] = this.eventListeners[event] || []; + push.call(this.eventListeners[event], listener); + }, + + removeEventListener: function removeEventListener(event, listener) { + var listeners = this.eventListeners && this.eventListeners[event] || []; + + for (var i = 0, l = listeners.length; i < l; ++i) { + if (listeners[i] == listener) { + return listeners.splice(i, 1); + } + } + }, + + dispatchEvent: function dispatchEvent(event) { + var type = event.type; + var listeners = this.eventListeners && this.eventListeners[type] || []; + + for (var i = 0; i < listeners.length; i++) { + if (typeof listeners[i] == "function") { + listeners[i].call(this, event); + } else { + listeners[i].handleEvent(event); + } + } + + return !!event.defaultPrevented; + } + }; +}()); + +/** + * @depend ../../sinon.js + * @depend event.js + */ +/*jslint eqeqeq: false, onevar: false*/ +/*global sinon, module, require, ActiveXObject, XMLHttpRequest, DOMParser*/ +/** + * Fake XMLHttpRequest object + * + * @author Christian Johansen (christian@cjohansen.no) + * @license BSD + * + * Copyright (c) 2010-2013 Christian Johansen + */ + +// wrapper for global +(function(global) { + if (typeof sinon === "undefined") { + global.sinon = {}; + } + + var supportsProgress = typeof ProgressEvent !== "undefined"; + var supportsCustomEvent = typeof CustomEvent !== "undefined"; + sinon.xhr = { XMLHttpRequest: global.XMLHttpRequest }; + var xhr = sinon.xhr; + xhr.GlobalXMLHttpRequest = global.XMLHttpRequest; + xhr.GlobalActiveXObject = global.ActiveXObject; + xhr.supportsActiveX = typeof xhr.GlobalActiveXObject != "undefined"; + xhr.supportsXHR = typeof xhr.GlobalXMLHttpRequest != "undefined"; + xhr.workingXHR = xhr.supportsXHR ? xhr.GlobalXMLHttpRequest : xhr.supportsActiveX + ? function() { return new xhr.GlobalActiveXObject("MSXML2.XMLHTTP.3.0") } : false; + xhr.supportsCORS = xhr.supportsXHR && 'withCredentials' in (new sinon.xhr.GlobalXMLHttpRequest()); + + /*jsl:ignore*/ + var unsafeHeaders = { + "Accept-Charset": true, + "Accept-Encoding": true, + "Connection": true, + "Content-Length": true, + "Cookie": true, + "Cookie2": true, + "Content-Transfer-Encoding": true, + "Date": true, + "Expect": true, + "Host": true, + "Keep-Alive": true, + "Referer": true, + "TE": true, + "Trailer": true, + "Transfer-Encoding": true, + "Upgrade": true, + "User-Agent": true, + "Via": true + }; + /*jsl:end*/ + + function FakeXMLHttpRequest() { + this.readyState = FakeXMLHttpRequest.UNSENT; + this.requestHeaders = {}; + this.requestBody = null; + this.status = 0; + this.statusText = ""; + this.upload = new UploadProgress(); + if (sinon.xhr.supportsCORS) { + this.withCredentials = false; + } + + + var xhr = this; + var events = ["loadstart", "load", "abort", "loadend"]; + + function addEventListener(eventName) { + xhr.addEventListener(eventName, function (event) { + var listener = xhr["on" + eventName]; + + if (listener && typeof listener == "function") { + listener.call(this, event); + } + }); + } + + for (var i = events.length - 1; i >= 0; i--) { + addEventListener(events[i]); + } + + if (typeof FakeXMLHttpRequest.onCreate == "function") { + FakeXMLHttpRequest.onCreate(this); + } + } + + // An upload object is created for each + // FakeXMLHttpRequest and allows upload + // events to be simulated using uploadProgress + // and uploadError. + function UploadProgress() { + this.eventListeners = { + "progress": [], + "load": [], + "abort": [], + "error": [] + } + } + + UploadProgress.prototype.addEventListener = function(event, listener) { + this.eventListeners[event].push(listener); + }; + + UploadProgress.prototype.removeEventListener = function(event, listener) { + var listeners = this.eventListeners[event] || []; + + for (var i = 0, l = listeners.length; i < l; ++i) { + if (listeners[i] == listener) { + return listeners.splice(i, 1); + } + } + }; + + UploadProgress.prototype.dispatchEvent = function(event) { + var listeners = this.eventListeners[event.type] || []; + + for (var i = 0, listener; (listener = listeners[i]) != null; i++) { + listener(event); + } + }; + + function verifyState(xhr) { + if (xhr.readyState !== FakeXMLHttpRequest.OPENED) { + throw new Error("INVALID_STATE_ERR"); + } + + if (xhr.sendFlag) { + throw new Error("INVALID_STATE_ERR"); + } + } + + // filtering to enable a white-list version of Sinon FakeXhr, + // where whitelisted requests are passed through to real XHR + function each(collection, callback) { + if (!collection) return; + for (var i = 0, l = collection.length; i < l; i += 1) { + callback(collection[i]); + } + } + function some(collection, callback) { + for (var index = 0; index < collection.length; index++) { + if(callback(collection[index]) === true) return true; + } + return false; + } + // largest arity in XHR is 5 - XHR#open + var apply = function(obj,method,args) { + switch(args.length) { + case 0: return obj[method](); + case 1: return obj[method](args[0]); + case 2: return obj[method](args[0],args[1]); + case 3: return obj[method](args[0],args[1],args[2]); + case 4: return obj[method](args[0],args[1],args[2],args[3]); + case 5: return obj[method](args[0],args[1],args[2],args[3],args[4]); + } + }; + + FakeXMLHttpRequest.filters = []; + FakeXMLHttpRequest.addFilter = function(fn) { + this.filters.push(fn) + }; + var IE6Re = /MSIE 6/; + FakeXMLHttpRequest.defake = function(fakeXhr,xhrArgs) { + var xhr = new sinon.xhr.workingXHR(); + each(["open","setRequestHeader","send","abort","getResponseHeader", + "getAllResponseHeaders","addEventListener","overrideMimeType","removeEventListener"], + function(method) { + fakeXhr[method] = function() { + return apply(xhr,method,arguments); + }; + }); + + var copyAttrs = function(args) { + each(args, function(attr) { + try { + fakeXhr[attr] = xhr[attr] + } catch(e) { + if(!IE6Re.test(navigator.userAgent)) throw e; + } + }); + }; + + var stateChange = function() { + fakeXhr.readyState = xhr.readyState; + if(xhr.readyState >= FakeXMLHttpRequest.HEADERS_RECEIVED) { + copyAttrs(["status","statusText"]); + } + if(xhr.readyState >= FakeXMLHttpRequest.LOADING) { + copyAttrs(["responseText"]); + } + if(xhr.readyState === FakeXMLHttpRequest.DONE) { + copyAttrs(["responseXML"]); + } + if(fakeXhr.onreadystatechange) fakeXhr.onreadystatechange.call(fakeXhr, { target: fakeXhr }); + }; + if(xhr.addEventListener) { + for(var event in fakeXhr.eventListeners) { + if(fakeXhr.eventListeners.hasOwnProperty(event)) { + each(fakeXhr.eventListeners[event],function(handler) { + xhr.addEventListener(event, handler); + }); + } + } + xhr.addEventListener("readystatechange",stateChange); + } else { + xhr.onreadystatechange = stateChange; + } + apply(xhr,"open",xhrArgs); + }; + FakeXMLHttpRequest.useFilters = false; + + function verifyRequestOpened(xhr) { + if (xhr.readyState != FakeXMLHttpRequest.OPENED) { + throw new Error("INVALID_STATE_ERR - " + xhr.readyState); + } + } + + function verifyRequestSent(xhr) { + if (xhr.readyState == FakeXMLHttpRequest.DONE) { + throw new Error("Request done"); + } + } + + function verifyHeadersReceived(xhr) { + if (xhr.async && xhr.readyState != FakeXMLHttpRequest.HEADERS_RECEIVED) { + throw new Error("No headers received"); + } + } + + function verifyResponseBodyType(body) { + if (typeof body != "string") { + var error = new Error("Attempted to respond to fake XMLHttpRequest with " + + body + ", which is not a string."); + error.name = "InvalidBodyException"; + throw error; + } + } + + sinon.extend(FakeXMLHttpRequest.prototype, sinon.EventTarget, { + async: true, + + open: function open(method, url, async, username, password) { + this.method = method; + this.url = url; + this.async = typeof async == "boolean" ? async : true; + this.username = username; + this.password = password; + this.responseText = null; + this.responseXML = null; + this.requestHeaders = {}; + this.sendFlag = false; + if(sinon.FakeXMLHttpRequest.useFilters === true) { + var xhrArgs = arguments; + var defake = some(FakeXMLHttpRequest.filters,function(filter) { + return filter.apply(this,xhrArgs) + }); + if (defake) { + return sinon.FakeXMLHttpRequest.defake(this,arguments); + } + } + this.readyStateChange(FakeXMLHttpRequest.OPENED); + }, + + readyStateChange: function readyStateChange(state) { + this.readyState = state; + + if (typeof this.onreadystatechange == "function") { + try { + this.onreadystatechange(); + } catch (e) { + sinon.logError("Fake XHR onreadystatechange handler", e); + } + } + + this.dispatchEvent(new sinon.Event("readystatechange")); + + switch (this.readyState) { + case FakeXMLHttpRequest.DONE: + this.dispatchEvent(new sinon.Event("load", false, false, this)); + this.dispatchEvent(new sinon.Event("loadend", false, false, this)); + this.upload.dispatchEvent(new sinon.Event("load", false, false, this)); + if (supportsProgress) { + this.upload.dispatchEvent(new sinon.ProgressEvent('progress', {loaded: 100, total: 100})); + } + break; + } + }, + + setRequestHeader: function setRequestHeader(header, value) { + verifyState(this); + + if (unsafeHeaders[header] || /^(Sec-|Proxy-)/.test(header)) { + throw new Error("Refused to set unsafe header \"" + header + "\""); + } + + if (this.requestHeaders[header]) { + this.requestHeaders[header] += "," + value; + } else { + this.requestHeaders[header] = value; + } + }, + + // Helps testing + setResponseHeaders: function setResponseHeaders(headers) { + verifyRequestOpened(this); + this.responseHeaders = {}; + + for (var header in headers) { + if (headers.hasOwnProperty(header)) { + this.responseHeaders[header] = headers[header]; + } + } + + if (this.async) { + this.readyStateChange(FakeXMLHttpRequest.HEADERS_RECEIVED); + } else { + this.readyState = FakeXMLHttpRequest.HEADERS_RECEIVED; + } + }, + + // Currently treats ALL data as a DOMString (i.e. no Document) + send: function send(data) { + verifyState(this); + + if (!/^(get|head)$/i.test(this.method)) { + if (this.requestHeaders["Content-Type"]) { + var value = this.requestHeaders["Content-Type"].split(";"); + this.requestHeaders["Content-Type"] = value[0] + ";charset=utf-8"; + } else { + this.requestHeaders["Content-Type"] = "text/plain;charset=utf-8"; + } + + this.requestBody = data; + } + + this.errorFlag = false; + this.sendFlag = this.async; + this.readyStateChange(FakeXMLHttpRequest.OPENED); + + if (typeof this.onSend == "function") { + this.onSend(this); + } + + this.dispatchEvent(new sinon.Event("loadstart", false, false, this)); + }, + + abort: function abort() { + this.aborted = true; + this.responseText = null; + this.errorFlag = true; + this.requestHeaders = {}; + + if (this.readyState > sinon.FakeXMLHttpRequest.UNSENT && this.sendFlag) { + this.readyStateChange(sinon.FakeXMLHttpRequest.DONE); + this.sendFlag = false; + } + + this.readyState = sinon.FakeXMLHttpRequest.UNSENT; + + this.dispatchEvent(new sinon.Event("abort", false, false, this)); + + this.upload.dispatchEvent(new sinon.Event("abort", false, false, this)); + + if (typeof this.onerror === "function") { + this.onerror(); + } + }, + + getResponseHeader: function getResponseHeader(header) { + if (this.readyState < FakeXMLHttpRequest.HEADERS_RECEIVED) { + return null; + } + + if (/^Set-Cookie2?$/i.test(header)) { + return null; + } + + header = header.toLowerCase(); + + for (var h in this.responseHeaders) { + if (h.toLowerCase() == header) { + return this.responseHeaders[h]; + } + } + + return null; + }, + + getAllResponseHeaders: function getAllResponseHeaders() { + if (this.readyState < FakeXMLHttpRequest.HEADERS_RECEIVED) { + return ""; + } + + var headers = ""; + + for (var header in this.responseHeaders) { + if (this.responseHeaders.hasOwnProperty(header) && + !/^Set-Cookie2?$/i.test(header)) { + headers += header + ": " + this.responseHeaders[header] + "\r\n"; + } + } + + return headers; + }, + + setResponseBody: function setResponseBody(body) { + verifyRequestSent(this); + verifyHeadersReceived(this); + verifyResponseBodyType(body); + + var chunkSize = this.chunkSize || 10; + var index = 0; + this.responseText = ""; + + do { + if (this.async) { + this.readyStateChange(FakeXMLHttpRequest.LOADING); + } + + this.responseText += body.substring(index, index + chunkSize); + index += chunkSize; + } while (index < body.length); + + var type = this.getResponseHeader("Content-Type"); + + if (this.responseText && + (!type || /(text\/xml)|(application\/xml)|(\+xml)/.test(type))) { + try { + this.responseXML = FakeXMLHttpRequest.parseXML(this.responseText); + } catch (e) { + // Unable to parse XML - no biggie + } + } + + if (this.async) { + this.readyStateChange(FakeXMLHttpRequest.DONE); + } else { + this.readyState = FakeXMLHttpRequest.DONE; + } + }, + + respond: function respond(status, headers, body) { + this.status = typeof status == "number" ? status : 200; + this.statusText = FakeXMLHttpRequest.statusCodes[this.status]; + this.setResponseHeaders(headers || {}); + this.setResponseBody(body || ""); + }, + + uploadProgress: function uploadProgress(progressEventRaw) { + if (supportsProgress) { + this.upload.dispatchEvent(new sinon.ProgressEvent("progress", progressEventRaw)); + } + }, + + uploadError: function uploadError(error) { + if (supportsCustomEvent) { + this.upload.dispatchEvent(new sinon.CustomEvent("error", {"detail": error})); + } + } + }); + + sinon.extend(FakeXMLHttpRequest, { + UNSENT: 0, + OPENED: 1, + HEADERS_RECEIVED: 2, + LOADING: 3, + DONE: 4 + }); + + // Borrowed from JSpec + FakeXMLHttpRequest.parseXML = function parseXML(text) { + var xmlDoc; + + if (typeof DOMParser != "undefined") { + var parser = new DOMParser(); + xmlDoc = parser.parseFromString(text, "text/xml"); + } else { + xmlDoc = new ActiveXObject("Microsoft.XMLDOM"); + xmlDoc.async = "false"; + xmlDoc.loadXML(text); + } + + return xmlDoc; + }; + + FakeXMLHttpRequest.statusCodes = { + 100: "Continue", + 101: "Switching Protocols", + 200: "OK", + 201: "Created", + 202: "Accepted", + 203: "Non-Authoritative Information", + 204: "No Content", + 205: "Reset Content", + 206: "Partial Content", + 300: "Multiple Choice", + 301: "Moved Permanently", + 302: "Found", + 303: "See Other", + 304: "Not Modified", + 305: "Use Proxy", + 307: "Temporary Redirect", + 400: "Bad Request", + 401: "Unauthorized", + 402: "Payment Required", + 403: "Forbidden", + 404: "Not Found", + 405: "Method Not Allowed", + 406: "Not Acceptable", + 407: "Proxy Authentication Required", + 408: "Request Timeout", + 409: "Conflict", + 410: "Gone", + 411: "Length Required", + 412: "Precondition Failed", + 413: "Request Entity Too Large", + 414: "Request-URI Too Long", + 415: "Unsupported Media Type", + 416: "Requested Range Not Satisfiable", + 417: "Expectation Failed", + 422: "Unprocessable Entity", + 500: "Internal Server Error", + 501: "Not Implemented", + 502: "Bad Gateway", + 503: "Service Unavailable", + 504: "Gateway Timeout", + 505: "HTTP Version Not Supported" + }; + + sinon.useFakeXMLHttpRequest = function () { + sinon.FakeXMLHttpRequest.restore = function restore(keepOnCreate) { + if (xhr.supportsXHR) { + global.XMLHttpRequest = xhr.GlobalXMLHttpRequest; + } + + if (xhr.supportsActiveX) { + global.ActiveXObject = xhr.GlobalActiveXObject; + } + + delete sinon.FakeXMLHttpRequest.restore; + + if (keepOnCreate !== true) { + delete sinon.FakeXMLHttpRequest.onCreate; + } + }; + if (xhr.supportsXHR) { + global.XMLHttpRequest = sinon.FakeXMLHttpRequest; + } + + if (xhr.supportsActiveX) { + global.ActiveXObject = function ActiveXObject(objId) { + if (objId == "Microsoft.XMLHTTP" || /^Msxml2\.XMLHTTP/i.test(objId)) { + + return new sinon.FakeXMLHttpRequest(); + } + + return new xhr.GlobalActiveXObject(objId); + }; + } + + return sinon.FakeXMLHttpRequest; + }; + + sinon.FakeXMLHttpRequest = FakeXMLHttpRequest; + +})((function(){ return typeof global === "object" ? global : this; })()); + +if (typeof module !== 'undefined' && module.exports) { + module.exports = sinon; +} + +/** + * @depend fake_xml_http_request.js + */ +/*jslint eqeqeq: false, onevar: false, regexp: false, plusplus: false*/ +/*global module, require, window*/ +/** + * The Sinon "server" mimics a web server that receives requests from + * sinon.FakeXMLHttpRequest and provides an API to respond to those requests, + * both synchronously and asynchronously. To respond synchronuously, canned + * answers have to be provided upfront. + * + * @author Christian Johansen (christian@cjohansen.no) + * @license BSD + * + * Copyright (c) 2010-2013 Christian Johansen + */ + +if (typeof sinon == "undefined") { + var sinon = {}; +} + +sinon.fakeServer = (function () { + var push = [].push; + function F() {} + + function create(proto) { + F.prototype = proto; + return new F(); + } + + function responseArray(handler) { + var response = handler; + + if (Object.prototype.toString.call(handler) != "[object Array]") { + response = [200, {}, handler]; + } + + if (typeof response[2] != "string") { + throw new TypeError("Fake server response body should be string, but was " + + typeof response[2]); + } + + return response; + } + + var wloc = typeof window !== "undefined" ? window.location : {}; + var rCurrLoc = new RegExp("^" + wloc.protocol + "//" + wloc.host); + + function matchOne(response, reqMethod, reqUrl) { + var rmeth = response.method; + var matchMethod = !rmeth || rmeth.toLowerCase() == reqMethod.toLowerCase(); + var url = response.url; + var matchUrl = !url || url == reqUrl || (typeof url.test == "function" && url.test(reqUrl)); + + return matchMethod && matchUrl; + } + + function match(response, request) { + var requestUrl = request.url; + + if (!/^https?:\/\//.test(requestUrl) || rCurrLoc.test(requestUrl)) { + requestUrl = requestUrl.replace(rCurrLoc, ""); + } + + if (matchOne(response, this.getHTTPMethod(request), requestUrl)) { + if (typeof response.response == "function") { + var ru = response.url; + var args = [request].concat(ru && typeof ru.exec == "function" ? ru.exec(requestUrl).slice(1) : []); + return response.response.apply(response, args); + } + + return true; + } + + return false; + } + + return { + create: function () { + var server = create(this); + this.xhr = sinon.useFakeXMLHttpRequest(); + server.requests = []; + + this.xhr.onCreate = function (xhrObj) { + server.addRequest(xhrObj); + }; + + return server; + }, + + addRequest: function addRequest(xhrObj) { + var server = this; + push.call(this.requests, xhrObj); + + xhrObj.onSend = function () { + server.handleRequest(this); + + if (server.autoRespond && !server.responding) { + setTimeout(function () { + server.responding = false; + server.respond(); + }, server.autoRespondAfter || 10); + + server.responding = true; + } + }; + }, + + getHTTPMethod: function getHTTPMethod(request) { + if (this.fakeHTTPMethods && /post/i.test(request.method)) { + var matches = (request.requestBody || "").match(/_method=([^\b;]+)/); + return !!matches ? matches[1] : request.method; + } + + return request.method; + }, + + handleRequest: function handleRequest(xhr) { + if (xhr.async) { + if (!this.queue) { + this.queue = []; + } + + push.call(this.queue, xhr); + } else { + this.processRequest(xhr); + } + }, + + log: function(response, request) { + var str; + + str = "Request:\n" + sinon.format(request) + "\n\n"; + str += "Response:\n" + sinon.format(response) + "\n\n"; + + sinon.log(str); + }, + + respondWith: function respondWith(method, url, body) { + if (arguments.length == 1 && typeof method != "function") { + this.response = responseArray(method); + return; + } + + if (!this.responses) { this.responses = []; } + + if (arguments.length == 1) { + body = method; + url = method = null; + } + + if (arguments.length == 2) { + body = url; + url = method; + method = null; + } + + push.call(this.responses, { + method: method, + url: url, + response: typeof body == "function" ? body : responseArray(body) + }); + }, + + respond: function respond() { + if (arguments.length > 0) this.respondWith.apply(this, arguments); + var queue = this.queue || []; + var requests = queue.splice(0, queue.length); + var request; + + while(request = requests.shift()) { + this.processRequest(request); + } + }, + + processRequest: function processRequest(request) { + try { + if (request.aborted) { + return; + } + + var response = this.response || [404, {}, ""]; + + if (this.responses) { + for (var l = this.responses.length, i = l - 1; i >= 0; i--) { + if (match.call(this, this.responses[i], request)) { + response = this.responses[i].response; + break; + } + } + } + + if (request.readyState != 4) { + sinon.fakeServer.log(response, request); + + request.respond(response[0], response[1], response[2]); + } + } catch (e) { + sinon.logError("Fake server request processing", e); + } + }, + + restore: function restore() { + return this.xhr.restore && this.xhr.restore.apply(this.xhr, arguments); + } + }; +}()); + +if (typeof module !== 'undefined' && module.exports) { + module.exports = sinon; +} + +/** + * @depend fake_server.js + * @depend fake_timers.js + */ +/*jslint browser: true, eqeqeq: false, onevar: false*/ +/*global sinon*/ +/** + * Add-on for sinon.fakeServer that automatically handles a fake timer along with + * the FakeXMLHttpRequest. The direct inspiration for this add-on is jQuery + * 1.3.x, which does not use xhr object's onreadystatehandler at all - instead, + * it polls the object for completion with setInterval. Dispite the direct + * motivation, there is nothing jQuery-specific in this file, so it can be used + * in any environment where the ajax implementation depends on setInterval or + * setTimeout. + * + * @author Christian Johansen (christian@cjohansen.no) + * @license BSD + * + * Copyright (c) 2010-2013 Christian Johansen + */ + +(function () { + function Server() {} + Server.prototype = sinon.fakeServer; + + sinon.fakeServerWithClock = new Server(); + + sinon.fakeServerWithClock.addRequest = function addRequest(xhr) { + if (xhr.async) { + if (typeof setTimeout.clock == "object") { + this.clock = setTimeout.clock; + } else { + this.clock = sinon.useFakeTimers(); + this.resetClock = true; + } + + if (!this.longestTimeout) { + var clockSetTimeout = this.clock.setTimeout; + var clockSetInterval = this.clock.setInterval; + var server = this; + + this.clock.setTimeout = function (fn, timeout) { + server.longestTimeout = Math.max(timeout, server.longestTimeout || 0); + + return clockSetTimeout.apply(this, arguments); + }; + + this.clock.setInterval = function (fn, timeout) { + server.longestTimeout = Math.max(timeout, server.longestTimeout || 0); + + return clockSetInterval.apply(this, arguments); + }; + } + } + + return sinon.fakeServer.addRequest.call(this, xhr); + }; + + sinon.fakeServerWithClock.respond = function respond() { + var returnVal = sinon.fakeServer.respond.apply(this, arguments); + + if (this.clock) { + this.clock.tick(this.longestTimeout || 0); + this.longestTimeout = 0; + + if (this.resetClock) { + this.clock.restore(); + this.resetClock = false; + } + } + + return returnVal; + }; + + sinon.fakeServerWithClock.restore = function restore() { + if (this.clock) { + this.clock.restore(); + } + + return sinon.fakeServer.restore.apply(this, arguments); + }; +}()); + +/** + * @depend ../sinon.js + * @depend collection.js + * @depend util/fake_timers.js + * @depend util/fake_server_with_clock.js + */ +/*jslint eqeqeq: false, onevar: false, plusplus: false*/ +/*global require, module*/ +/** + * Manages fake collections as well as fake utilities such as Sinon's + * timers and fake XHR implementation in one convenient object. + * + * @author Christian Johansen (christian@cjohansen.no) + * @license BSD + * + * Copyright (c) 2010-2013 Christian Johansen + */ + +if (typeof module !== "undefined" && module.exports && typeof require == "function") { + var sinon = require("../sinon"); + sinon.extend(sinon, require("./util/fake_timers")); +} + +(function () { + var push = [].push; + + function exposeValue(sandbox, config, key, value) { + if (!value) { + return; + } + + if (config.injectInto && !(key in config.injectInto)) { + config.injectInto[key] = value; + sandbox.injectedKeys.push(key); + } else { + push.call(sandbox.args, value); + } + } + + function prepareSandboxFromConfig(config) { + var sandbox = sinon.create(sinon.sandbox); + + if (config.useFakeServer) { + if (typeof config.useFakeServer == "object") { + sandbox.serverPrototype = config.useFakeServer; + } + + sandbox.useFakeServer(); + } + + if (config.useFakeTimers) { + if (typeof config.useFakeTimers == "object") { + sandbox.useFakeTimers.apply(sandbox, config.useFakeTimers); + } else { + sandbox.useFakeTimers(); + } + } + + return sandbox; + } + + sinon.sandbox = sinon.extend(sinon.create(sinon.collection), { + useFakeTimers: function useFakeTimers() { + this.clock = sinon.useFakeTimers.apply(sinon, arguments); + + return this.add(this.clock); + }, + + serverPrototype: sinon.fakeServer, + + useFakeServer: function useFakeServer() { + var proto = this.serverPrototype || sinon.fakeServer; + + if (!proto || !proto.create) { + return null; + } + + this.server = proto.create(); + return this.add(this.server); + }, + + inject: function (obj) { + sinon.collection.inject.call(this, obj); + + if (this.clock) { + obj.clock = this.clock; + } + + if (this.server) { + obj.server = this.server; + obj.requests = this.server.requests; + } + + return obj; + }, + + restore: function () { + sinon.collection.restore.apply(this, arguments); + this.restoreContext(); + }, + + restoreContext: function () { + if (this.injectedKeys) { + for (var i = 0, j = this.injectedKeys.length; i < j; i++) { + delete this.injectInto[this.injectedKeys[i]]; + } + this.injectedKeys = []; + } + }, + + create: function (config) { + if (!config) { + return sinon.create(sinon.sandbox); + } + + var sandbox = prepareSandboxFromConfig(config); + sandbox.args = sandbox.args || []; + sandbox.injectedKeys = []; + sandbox.injectInto = config.injectInto; + var prop, value, exposed = sandbox.inject({}); + + if (config.properties) { + for (var i = 0, l = config.properties.length; i < l; i++) { + prop = config.properties[i]; + value = exposed[prop] || prop == "sandbox" && sandbox; + exposeValue(sandbox, config, prop, value); + } + } else { + exposeValue(sandbox, config, "sandbox", value); + } + + return sandbox; + } + }); + + sinon.sandbox.useFakeXMLHttpRequest = sinon.sandbox.useFakeServer; + + if (typeof define === "function" && define.amd) { + define(["module"], function(module) { module.exports = sinon.sandbox; }); + } else if (typeof module !== 'undefined' && module.exports) { + module.exports = sinon.sandbox; + } +}()); + +/** + * @depend ../sinon.js + * @depend stub.js + * @depend mock.js + * @depend sandbox.js + */ +/*jslint eqeqeq: false, onevar: false, forin: true, plusplus: false*/ +/*global module, require, sinon*/ +/** + * Test function, sandboxes fakes + * + * @author Christian Johansen (christian@cjohansen.no) + * @license BSD + * + * Copyright (c) 2010-2013 Christian Johansen + */ + +(function (sinon) { + var commonJSModule = typeof module !== "undefined" && module.exports && typeof require == "function"; + + if (!sinon && commonJSModule) { + sinon = require("../sinon"); + } + + if (!sinon) { + return; + } + + function test(callback) { + var type = typeof callback; + + if (type != "function") { + throw new TypeError("sinon.test needs to wrap a test function, got " + type); + } + + function sinonSandboxedTest() { + var config = sinon.getConfig(sinon.config); + config.injectInto = config.injectIntoThis && this || config.injectInto; + var sandbox = sinon.sandbox.create(config); + var exception, result; + var args = Array.prototype.slice.call(arguments).concat(sandbox.args); + + try { + result = callback.apply(this, args); + } catch (e) { + exception = e; + } + + if (typeof exception !== "undefined") { + sandbox.restore(); + throw exception; + } + else { + sandbox.verifyAndRestore(); + } + + return result; + }; + + if (callback.length) { + return function sinonAsyncSandboxedTest(callback) { + return sinonSandboxedTest.apply(this, arguments); + }; + } + + return sinonSandboxedTest; + } + + test.config = { + injectIntoThis: true, + injectInto: null, + properties: ["spy", "stub", "mock", "clock", "server", "requests"], + useFakeTimers: true, + useFakeServer: true + }; + + sinon.test = test; + + if (typeof define === "function" && define.amd) { + define(["module"], function(module) { module.exports = test; }); + } else if (commonJSModule) { + module.exports = test; + } +}(typeof sinon == "object" && sinon || null)); + +/** + * @depend ../sinon.js + * @depend test.js + */ +/*jslint eqeqeq: false, onevar: false, eqeqeq: false*/ +/*global module, require, sinon*/ +/** + * Test case, sandboxes all test functions + * + * @author Christian Johansen (christian@cjohansen.no) + * @license BSD + * + * Copyright (c) 2010-2013 Christian Johansen + */ + +(function (sinon) { + var commonJSModule = typeof module !== "undefined" && module.exports && typeof require == "function"; + + if (!sinon && commonJSModule) { + sinon = require("../sinon"); + } + + if (!sinon || !Object.prototype.hasOwnProperty) { + return; + } + + function createTest(property, setUp, tearDown) { + return function () { + if (setUp) { + setUp.apply(this, arguments); + } + + var exception, result; + + try { + result = property.apply(this, arguments); + } catch (e) { + exception = e; + } + + if (tearDown) { + tearDown.apply(this, arguments); + } + + if (exception) { + throw exception; + } + + return result; + }; + } + + function testCase(tests, prefix) { + /*jsl:ignore*/ + if (!tests || typeof tests != "object") { + throw new TypeError("sinon.testCase needs an object with test functions"); + } + /*jsl:end*/ + + prefix = prefix || "test"; + var rPrefix = new RegExp("^" + prefix); + var methods = {}, testName, property, method; + var setUp = tests.setUp; + var tearDown = tests.tearDown; + + for (testName in tests) { + if (tests.hasOwnProperty(testName)) { + property = tests[testName]; + + if (/^(setUp|tearDown)$/.test(testName)) { + continue; + } + + if (typeof property == "function" && rPrefix.test(testName)) { + method = property; + + if (setUp || tearDown) { + method = createTest(property, setUp, tearDown); + } + + methods[testName] = sinon.test(method); + } else { + methods[testName] = tests[testName]; + } + } + } + + return methods; + } + + sinon.testCase = testCase; + + if (typeof define === "function" && define.amd) { + define(["module"], function(module) { module.exports = testCase; }); + } else if (commonJSModule) { + module.exports = testCase; + } +}(typeof sinon == "object" && sinon || null)); + +/** + * @depend ../sinon.js + * @depend stub.js + */ +/*jslint eqeqeq: false, onevar: false, nomen: false, plusplus: false*/ +/*global module, require, sinon*/ +/** + * Assertions matching the test spy retrieval interface. + * + * @author Christian Johansen (christian@cjohansen.no) + * @license BSD + * + * Copyright (c) 2010-2013 Christian Johansen + */ + +(function (sinon, global) { + var commonJSModule = typeof module !== "undefined" && module.exports && typeof require == "function"; + var slice = Array.prototype.slice; + var assert; + + if (!sinon && commonJSModule) { + sinon = require("../sinon"); + } + + if (!sinon) { + return; + } + + function verifyIsStub() { + var method; + + for (var i = 0, l = arguments.length; i < l; ++i) { + method = arguments[i]; + + if (!method) { + assert.fail("fake is not a spy"); + } + + if (typeof method != "function") { + assert.fail(method + " is not a function"); + } + + if (typeof method.getCall != "function") { + assert.fail(method + " is not stubbed"); + } + } + } + + function failAssertion(object, msg) { + object = object || global; + var failMethod = object.fail || assert.fail; + failMethod.call(object, msg); + } + + function mirrorPropAsAssertion(name, method, message) { + if (arguments.length == 2) { + message = method; + method = name; + } + + assert[name] = function (fake) { + verifyIsStub(fake); + + var args = slice.call(arguments, 1); + var failed = false; + + if (typeof method == "function") { + failed = !method(fake); + } else { + failed = typeof fake[method] == "function" ? + !fake[method].apply(fake, args) : !fake[method]; + } + + if (failed) { + failAssertion(this, fake.printf.apply(fake, [message].concat(args))); + } else { + assert.pass(name); + } + }; + } + + function exposedName(prefix, prop) { + return !prefix || /^fail/.test(prop) ? prop : + prefix + prop.slice(0, 1).toUpperCase() + prop.slice(1); + } + + assert = { + failException: "AssertError", + + fail: function fail(message) { + var error = new Error(message); + error.name = this.failException || assert.failException; + + throw error; + }, + + pass: function pass(assertion) {}, + + callOrder: function assertCallOrder() { + verifyIsStub.apply(null, arguments); + var expected = "", actual = ""; + + if (!sinon.calledInOrder(arguments)) { + try { + expected = [].join.call(arguments, ", "); + var calls = slice.call(arguments); + var i = calls.length; + while (i) { + if (!calls[--i].called) { + calls.splice(i, 1); + } + } + actual = sinon.orderByFirstCall(calls).join(", "); + } catch (e) { + // If this fails, we'll just fall back to the blank string + } + + failAssertion(this, "expected " + expected + " to be " + + "called in order but were called as " + actual); + } else { + assert.pass("callOrder"); + } + }, + + callCount: function assertCallCount(method, count) { + verifyIsStub(method); + + if (method.callCount != count) { + var msg = "expected %n to be called " + sinon.timesInWords(count) + + " but was called %c%C"; + failAssertion(this, method.printf(msg)); + } else { + assert.pass("callCount"); + } + }, + + expose: function expose(target, options) { + if (!target) { + throw new TypeError("target is null or undefined"); + } + + var o = options || {}; + var prefix = typeof o.prefix == "undefined" && "assert" || o.prefix; + var includeFail = typeof o.includeFail == "undefined" || !!o.includeFail; + + for (var method in this) { + if (method != "export" && (includeFail || !/^(fail)/.test(method))) { + target[exposedName(prefix, method)] = this[method]; + } + } + + return target; + }, + + match: function match(actual, expectation) { + var matcher = sinon.match(expectation); + if (matcher.test(actual)) { + assert.pass("match"); + } else { + var formatted = [ + "expected value to match", + " expected = " + sinon.format(expectation), + " actual = " + sinon.format(actual) + ] + failAssertion(this, formatted.join("\n")); + } + } + }; + + mirrorPropAsAssertion("called", "expected %n to have been called at least once but was never called"); + mirrorPropAsAssertion("notCalled", function (spy) { return !spy.called; }, + "expected %n to not have been called but was called %c%C"); + mirrorPropAsAssertion("calledOnce", "expected %n to be called once but was called %c%C"); + mirrorPropAsAssertion("calledTwice", "expected %n to be called twice but was called %c%C"); + mirrorPropAsAssertion("calledThrice", "expected %n to be called thrice but was called %c%C"); + mirrorPropAsAssertion("calledOn", "expected %n to be called with %1 as this but was called with %t"); + mirrorPropAsAssertion("alwaysCalledOn", "expected %n to always be called with %1 as this but was called with %t"); + mirrorPropAsAssertion("calledWithNew", "expected %n to be called with new"); + mirrorPropAsAssertion("alwaysCalledWithNew", "expected %n to always be called with new"); + mirrorPropAsAssertion("calledWith", "expected %n to be called with arguments %*%C"); + mirrorPropAsAssertion("calledWithMatch", "expected %n to be called with match %*%C"); + mirrorPropAsAssertion("alwaysCalledWith", "expected %n to always be called with arguments %*%C"); + mirrorPropAsAssertion("alwaysCalledWithMatch", "expected %n to always be called with match %*%C"); + mirrorPropAsAssertion("calledWithExactly", "expected %n to be called with exact arguments %*%C"); + mirrorPropAsAssertion("alwaysCalledWithExactly", "expected %n to always be called with exact arguments %*%C"); + mirrorPropAsAssertion("neverCalledWith", "expected %n to never be called with arguments %*%C"); + mirrorPropAsAssertion("neverCalledWithMatch", "expected %n to never be called with match %*%C"); + mirrorPropAsAssertion("threw", "%n did not throw exception%C"); + mirrorPropAsAssertion("alwaysThrew", "%n did not always throw exception%C"); + + sinon.assert = assert; + + if (typeof define === "function" && define.amd) { + define(["module"], function(module) { module.exports = assert; }); + } else if (commonJSModule) { + module.exports = assert; + } +}(typeof sinon == "object" && sinon || null, typeof window != "undefined" ? window : (typeof self != "undefined") ? self : global)); + +/** + * @depend ../../sinon.js + * @depend event.js + */ +/*jslint eqeqeq: false, onevar: false*/ +/*global sinon, module, require, XDomainRequest*/ +/** + * Fake XDomainRequest object + */ + +if (typeof sinon == "undefined") { + this.sinon = {}; +} +sinon.xdr = { XDomainRequest: this.XDomainRequest }; + +// wrapper for global +(function (global) { + var xdr = sinon.xdr; + xdr.GlobalXDomainRequest = global.XDomainRequest; + xdr.supportsXDR = typeof xdr.GlobalXDomainRequest != "undefined"; + xdr.workingXDR = xdr.supportsXDR ? xdr.GlobalXDomainRequest : false; + + function FakeXDomainRequest() { + this.readyState = FakeXDomainRequest.UNSENT; + this.requestBody = null; + this.requestHeaders = {}; + this.status = 0; + this.timeout = null; + + if (typeof FakeXDomainRequest.onCreate == "function") { + FakeXDomainRequest.onCreate(this); + } + } + + function verifyState(xdr) { + if (xdr.readyState !== FakeXDomainRequest.OPENED) { + throw new Error("INVALID_STATE_ERR"); + } + + if (xdr.sendFlag) { + throw new Error("INVALID_STATE_ERR"); + } + } + + function verifyRequestSent(xdr) { + if (xdr.readyState == FakeXDomainRequest.UNSENT) { + throw new Error("Request not sent"); + } + if (xdr.readyState == FakeXDomainRequest.DONE) { + throw new Error("Request done"); + } + } + + function verifyResponseBodyType(body) { + if (typeof body != "string") { + var error = new Error("Attempted to respond to fake XDomainRequest with " + + body + ", which is not a string."); + error.name = "InvalidBodyException"; + throw error; + } + } + + sinon.extend(FakeXDomainRequest.prototype, sinon.EventTarget, { + open: function open(method, url) { + this.method = method; + this.url = url; + + this.responseText = null; + this.sendFlag = false; + + this.readyStateChange(FakeXDomainRequest.OPENED); + }, + + readyStateChange: function readyStateChange(state) { + this.readyState = state; + var eventName = ''; + switch (this.readyState) { + case FakeXDomainRequest.UNSENT: + break; + case FakeXDomainRequest.OPENED: + break; + case FakeXDomainRequest.LOADING: + if (this.sendFlag){ + //raise the progress event + eventName = 'onprogress'; + } + break; + case FakeXDomainRequest.DONE: + if (this.isTimeout){ + eventName = 'ontimeout' + } + else if (this.errorFlag || (this.status < 200 || this.status > 299)) { + eventName = 'onerror'; + } + else { + eventName = 'onload' + } + break; + } + + // raising event (if defined) + if (eventName) { + if (typeof this[eventName] == "function") { + try { + this[eventName](); + } catch (e) { + sinon.logError("Fake XHR " + eventName + " handler", e); + } + } + } + }, + + send: function send(data) { + verifyState(this); + + if (!/^(get|head)$/i.test(this.method)) { + this.requestBody = data; + } + this.requestHeaders["Content-Type"] = "text/plain;charset=utf-8"; + + this.errorFlag = false; + this.sendFlag = true; + this.readyStateChange(FakeXDomainRequest.OPENED); + + if (typeof this.onSend == "function") { + this.onSend(this); + } + }, + + abort: function abort() { + this.aborted = true; + this.responseText = null; + this.errorFlag = true; + + if (this.readyState > sinon.FakeXDomainRequest.UNSENT && this.sendFlag) { + this.readyStateChange(sinon.FakeXDomainRequest.DONE); + this.sendFlag = false; + } + }, + + setResponseBody: function setResponseBody(body) { + verifyRequestSent(this); + verifyResponseBodyType(body); + + var chunkSize = this.chunkSize || 10; + var index = 0; + this.responseText = ""; + + do { + this.readyStateChange(FakeXDomainRequest.LOADING); + this.responseText += body.substring(index, index + chunkSize); + index += chunkSize; + } while (index < body.length); + + this.readyStateChange(FakeXDomainRequest.DONE); + }, + + respond: function respond(status, contentType, body) { + // content-type ignored, since XDomainRequest does not carry this + // we keep the same syntax for respond(...) as for FakeXMLHttpRequest to ease + // test integration across browsers + this.status = typeof status == "number" ? status : 200; + this.setResponseBody(body || ""); + }, + + simulatetimeout: function(){ + this.status = 0; + this.isTimeout = true; + // Access to this should actually throw an error + this.responseText = undefined; + this.readyStateChange(FakeXDomainRequest.DONE); + } + }); + + sinon.extend(FakeXDomainRequest, { + UNSENT: 0, + OPENED: 1, + LOADING: 3, + DONE: 4 + }); + + sinon.useFakeXDomainRequest = function () { + sinon.FakeXDomainRequest.restore = function restore(keepOnCreate) { + if (xdr.supportsXDR) { + global.XDomainRequest = xdr.GlobalXDomainRequest; + } + + delete sinon.FakeXDomainRequest.restore; + + if (keepOnCreate !== true) { + delete sinon.FakeXDomainRequest.onCreate; + } + }; + if (xdr.supportsXDR) { + global.XDomainRequest = sinon.FakeXDomainRequest; + } + return sinon.FakeXDomainRequest; + }; + + sinon.FakeXDomainRequest = FakeXDomainRequest; +})(this); + +if (typeof module == "object" && typeof require == "function") { + module.exports = sinon; +} + +return sinon;}.call(typeof window != 'undefined' && window || {})); diff --git a/test/demo/tests.html b/test/demo/tests.html index 9ba2b25..0987a84 100644 --- a/test/demo/tests.html +++ b/test/demo/tests.html @@ -3,6 +3,7 @@ Subscribe Email Demo + @@ -39,5 +40,71 @@ }); + + \ No newline at end of file From 5b46c7e54fe9019c42c0680d6998fdd4de9a2fee Mon Sep 17 00:00:00 2001 From: Josiah Sprague Date: Fri, 24 Oct 2014 16:58:24 -0400 Subject: [PATCH 47/60] [#MmLRZ2E2] mock JSONP responses and more mock JSONP responses, add IE helper for Sinon, update BrowserStack webdriver tests to test mocked forms, fix Universe API URL that was incorrectly changed to staging in a previous commit, add gulp task that builds and opens browserstack tunnel for manual testing Branch: MmLRZ2E2-development --- gulpfile.js | 2 + src/subscribe-email.js | 81 +++++++++++++++----------------- test/demo/sinon-ie.js | 100 ++++++++++++++++++++++++++++++++++++++++ test/demo/tests.html | 74 ++++++++++++++++++++++------- test/selenium-driver.js | 50 ++++++++++++-------- 5 files changed, 230 insertions(+), 77 deletions(-) create mode 100644 test/demo/sinon-ie.js diff --git a/gulpfile.js b/gulpfile.js index 7ee648a..43a4d25 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -26,6 +26,8 @@ gulp.task('test', function(cb) { ); }); +gulp.task('manual-test', ['default', 'start-browserstack-tunnel']) + gulp.task('build', function() { var bundler = browserify({ entries: ['./src/subscribe-email.js'], diff --git a/src/subscribe-email.js b/src/subscribe-email.js index cf33184..511635c 100644 --- a/src/subscribe-email.js +++ b/src/subscribe-email.js @@ -34,7 +34,7 @@ function SubscribeEmail (options) { if (serialize(this)) { //Only submit form if there is data var requestData = _prepareData(this, options); if (options.jsonp) { - _makeJSONPRequest(options.formAction, requestData, instance); + instance.makeJSONPRequest(options.formAction, requestData, instance); } else { _makeCorsRequest(options.formAction, requestData, instance); } @@ -52,6 +52,42 @@ function SubscribeEmail (options) { }); } +SubscribeEmail.prototype.makeJSONPRequest = function(url, data, instance) { + var callbackName, scriptElement; + callbackName = "cb_" + Math.floor(Math.random() * 10000); + window[callbackName] = function(json) { + try { + delete window[callbackName]; + } catch (e) { + window[callbackName] = undefined; + } + instance.processJSONP(json, instance); + }; + scriptElement = document.createElement('script'); + scriptElement.src = url + data + callbackName; + document.body.appendChild(scriptElement); +} + +SubscribeEmail.prototype.processJSONP = function(json, instance) { + //Fire Message Event(s) + if (json.message) { + instance.emit('subscriptionMessage', json.message); + } else if (json.msg) { + instance.emit('subscriptionMessage', json.msg); + } else if (json.messages) { + json.messages.forEach(function(message) { + instance.emit('subscriptionMessage', message); + }); + } + + //Fire Success or Error Event + if (json.result === 'success' || json.status === 'ok') { + instance.emit('subscriptionSuccess', json); + } else { + instance.emit('subscriptionError', json); + } +} + //Private Functions function _setDefaults(options, instance) { options.submitText = options.submitText || 'Subscribe'; @@ -66,7 +102,7 @@ function _setDefaults(options, instance) { switch (options.service) { case 'universe': - options.formAction = options.formAction || 'http://staging.services.sparkart.net/api/v1/contacts'; + options.formAction = options.formAction || 'http://services.sparkart.net/api/v1/contacts'; options.emailName = options.emailName || 'contact[email]'; options.jsonp = !('withCredentials' in new XMLHttpRequest()); break; @@ -165,45 +201,4 @@ function _createCorsRequest(method, url, data) { xhr = null; } return xhr; -} - -function _makeJSONPRequest(url, data, instance) { - var callbackName, scriptElement; - - callbackName = "cb_" + Math.floor(Math.random() * 10000); - - window[callbackName] = function(json) { - try { - delete window[callbackName]; - } - catch (e) { - window[callbackName] = undefined; - } - - _processJSONP(json, instance); - }; - - scriptElement = document.createElement('script'); - scriptElement.src = url + data + callbackName; - document.body.appendChild(scriptElement); -} - -function _processJSONP(json, instance) { - //Fire Message Event(s) - if (json.message) { - instance.emit('subscriptionMessage', json.message); - } else if (json.msg) { - instance.emit('subscriptionMessage', json.msg); - } else if (json.messages) { - json.messages.forEach(function(message) { - instance.emit('subscriptionMessage', message); - }); - } - - //Fire Success or Error Event - if (json.result === 'success' || json.status === 'ok') { - instance.emit('subscriptionSuccess', json); - } else { - instance.emit('subscriptionError', json); - } } \ No newline at end of file diff --git a/test/demo/sinon-ie.js b/test/demo/sinon-ie.js new file mode 100644 index 0000000..8331dad --- /dev/null +++ b/test/demo/sinon-ie.js @@ -0,0 +1,100 @@ +/** + * Sinon.JS 1.10.3, 2014/07/11 + * + * @author Christian Johansen (christian@cjohansen.no) + * @author Contributors: https://github.com/cjohansen/Sinon.JS/blob/master/AUTHORS + * + * (The BSD License) + * + * Copyright (c) 2010-2014, Christian Johansen, christian@cjohansen.no + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * * Neither the name of Christian Johansen nor the names of his contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/*global sinon, setTimeout, setInterval, clearTimeout, clearInterval, Date*/ +/** + * Helps IE run the fake timers. By defining global functions, IE allows + * them to be overwritten at a later point. If these are not defined like + * this, overwriting them will result in anything from an exception to browser + * crash. + * + * If you don't require fake timers to work in IE, don't include this file. + * + * @author Christian Johansen (christian@cjohansen.no) + * @license BSD + * + * Copyright (c) 2010-2013 Christian Johansen + */ +function setTimeout() {} +function clearTimeout() {} +function setImmediate() {} +function clearImmediate() {} +function setInterval() {} +function clearInterval() {} +function Date() {} + +// Reassign the original functions. Now their writable attribute +// should be true. Hackish, I know, but it works. +setTimeout = sinon.timers.setTimeout; +clearTimeout = sinon.timers.clearTimeout; +setImmediate = sinon.timers.setImmediate; +clearImmediate = sinon.timers.clearImmediate; +setInterval = sinon.timers.setInterval; +clearInterval = sinon.timers.clearInterval; +Date = sinon.timers.Date; + +/*global sinon*/ +/** + * Helps IE run the fake XMLHttpRequest. By defining global functions, IE allows + * them to be overwritten at a later point. If these are not defined like + * this, overwriting them will result in anything from an exception to browser + * crash. + * + * If you don't require fake XHR to work in IE, don't include this file. + * + * @author Christian Johansen (christian@cjohansen.no) + * @license BSD + * + * Copyright (c) 2010-2013 Christian Johansen + */ +function XMLHttpRequest() {} + +// Reassign the original function. Now its writable attribute +// should be true. Hackish, I know, but it works. +XMLHttpRequest = sinon.xhr.XMLHttpRequest || undefined; +/*global sinon*/ +/** + * Helps IE run the fake XDomainRequest. By defining global functions, IE allows + * them to be overwritten at a later point. If these are not defined like + * this, overwriting them will result in anything from an exception to browser + * crash. + * + * If you don't require fake XDR to work in IE, don't include this file. + */ +function XDomainRequest() {} + +// Reassign the original function. Now its writable attribute +// should be true. Hackish, I know, but it works. +XDomainRequest = sinon.xdr.XDomainRequest || undefined; \ No newline at end of file diff --git a/test/demo/tests.html b/test/demo/tests.html index 0987a84..47f26a2 100644 --- a/test/demo/tests.html +++ b/test/demo/tests.html @@ -4,6 +4,7 @@ Subscribe Email Demo + @@ -43,28 +44,41 @@ ``` -At a minimum, you'll need to change the `service` and `key` parameters to match your needs. +At a minimum, you'll need to change the `service` and `key` parameters to match your needs. *(Note: MailChimp uses `url` instead of `key`).* # Advanced Usage @@ -48,7 +43,10 @@ A selector string that refers to the element that should receive response messag **(Required)** The mailing list platform you are using. Available options are `mailchimp`, `sendgrid` and `universe`. ### `key` -**(Required)** A string of the API key for your mailing list platform. +**(Required)** A string of the API key for your mailing list platform. *(This is not required for MailChimp. Instead, you'll have to use `url`).* + +### `url` +**(Required for MailChimp)** A string of the `
    ` attribute generated by MailChimp which contains MailChimp authentication information. You can get this from MailChimp under "Signup forms > Embedded form code > Naked" and copying just the value from the `` attribute. It should follow this format: `http://{username}.{data center}.list-manage.com/subscribe/post?u={user id}&id={list id}`. ### `submitText` A string to be used on the form's submit button (defaults to "Subscribe"). @@ -56,17 +54,17 @@ A string to be used on the form's submit button (defaults to "Subscribe"). ### `template` Out of the box, the module will generate BEM markup with the namespace `subscribe-email` that contains all of the markup needed to display the alert. If you want to customize the markup, you can pass in a *compiled* handlebars template using this option. (Defaults to `false`). -### `responseElement` -A selector string for the element you want to display response (validation, errors, confirmation) messages from your platform's API (defaults to `'.subscribe-email__response'`). +### `prependMessagesTo` +By default, responses from the different mailing list platforms will be prepended to the SubscribeEmail `element` as a dismissable alert, but you can use this option to override which element the alerts are prepended to. Accepts a query string or a jQuery object. ## Events -Some mailing list platforms may send response messages for things like confirmation or validation errors. The default template will display these messages along-side the form, but alternatively you can easily integrate the messages into other parts of your page by listening for the following events to be emitted from the SubscribeEmail instance; +You can easily integrate the messages into other parts of your page by listening for the following events to be emitted from the SubscribeEmail instance; ### `subscriptionMessage` -Fires whenever the mailing list provider returns a response (both success and failure). +Fires whenever the mailing list provider returns a response (both success and failure). The message will be passed to this event as a string. ### `subscriptionError` -This event will fire if the mailing list provider returns an error. Specific details about the error will be included in a payload object when available. +This event will fire if the mailing list provider returns an error. Specific details about the error will be passed to the event as a payload object. ### `subscriptionSuccess` -This event will fire if the mailing list provider returns a confirmation that the email address has been added to the list. Specific details will be included in a payload object when available. \ No newline at end of file +This event will fire if the mailing list provider returns a confirmation that the email address has been added to the list. Specific details will be passed to the event as a payload object. \ No newline at end of file diff --git a/test/demo/tests.html b/test/demo/tests.html index be9694e..97b4bf9 100644 --- a/test/demo/tests.html +++ b/test/demo/tests.html @@ -37,7 +37,7 @@ var mailchimpForm = SubscribeEmail({ element: '#mailchimp-form', service: 'mailchimp', - url: 'http://josiahsprague.us5.list-manage.com/subscribe/post?u=e91ffb6b8d4681eafd86fd883&id=596075069d' + url: 'list-manage.com' }); From 05bdb24af6d627326a3f6cb754025e65dc0df1b5 Mon Sep 17 00:00:00 2001 From: Josiah Sprague Date: Thu, 30 Oct 2014 10:23:45 -0400 Subject: [PATCH 54/60] [#MmLRZ2E2] replace placeholder with template rather than inserting the template inside of the placeholder, the placeholder is now completely replaced by the template, which means the template contains the entire markup of the subscription form including the form tag itself. Branch: MmLRZ2E2-development --- README.md | 5 ++++- src/subscribe-email.js | 18 ++++++++++-------- src/subscribe-form.hbs | 9 +++++---- test/selenium-driver.js | 2 +- test/test-template.hbs | 2 +- test/tests.js | 2 +- 6 files changed, 22 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index b9fabca..2434e81 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,10 @@ A selector string that refers to the element that should receive response messag A string to be used on the form's submit button (defaults to "Subscribe"). ### `template` -Out of the box, the module will generate BEM markup with the namespace `subscribe-email` that contains all of the markup needed to display the alert. If you want to customize the markup, you can pass in a *compiled* handlebars template using this option. (Defaults to `false`). +If you want to customize the markup, you can override the default markup by passing in a *compiled* handlebars template using this option. See the default template for a starting point to work from. A custom template will not work without a form tag that contains `id="{{id}}"` and an email input that contains `name="{{emailName}}"`. (Defaults to `false`). + +### `namespace` +Out of the box, the module will generate BEM markup with the namespace `subscribe-email`, but you can use this option to override the default without passing in a custom template. ### `prependMessagesTo` By default, responses from the different mailing list platforms will be prepended to the SubscribeEmail `element` as a dismissable alert, but you can use this option to override which element the alerts are prepended to. Accepts a query string or a jQuery object. diff --git a/src/subscribe-email.js b/src/subscribe-email.js index 50aa4f1..f823ab1 100644 --- a/src/subscribe-email.js +++ b/src/subscribe-email.js @@ -10,26 +10,25 @@ module.exports = SubscribeEmail; function SubscribeEmail (options) { if (!(this instanceof SubscribeEmail)) return new SubscribeEmail(options); var instance = this; - options = _setDefaults(options, instance); - var theForm; if (options.element.jquery) { - theForm = options.element[0]; + instance.theForm = options.element[0]; } else { - theForm = document.querySelector(options.element); + instance.theForm = document.querySelector(options.element); } + options = _setDefaults(options, instance); //Render the Default Template - theForm.innerHTML = instance.template(options); - //Add BEM Namespace Class to Form - theForm.className += ' subscribe-email'; + instance.theForm.outerHTML = instance.template(options); + //Select new DOM element after replacing original with rendered template + instance.theForm = document.getElementById(options.id); var messageHolder = new Alerter({ prependTo: options.prependMessagesTo }); //Override Default Submit Action with CORS request - theForm.addEventListener('submit', function(e) { + instance.theForm.addEventListener('submit', function(e) { e.preventDefault(); if (serialize(this)) { //Only submit form if there is data var requestData = _prepareData(this, options); @@ -90,8 +89,11 @@ SubscribeEmail.prototype.processJSONP = function(json, instance) { //Private Functions function _setDefaults(options, instance) { + options.namespace = options.namespace || 'subscribe-email'; options.submitText = options.submitText || 'Subscribe'; options.prependMessagesTo = options.prependMessagesTo || options.element; + //get/set the ID of the HTML element (may be different than the value of element) + options.id = instance.theForm.id || ('subscribe-email-' + options.service); if (typeof options.template === 'function') { instance.template = options.template; diff --git a/src/subscribe-form.hbs b/src/subscribe-form.hbs index c2f1ca7..2b8727a 100644 --- a/src/subscribe-form.hbs +++ b/src/subscribe-form.hbs @@ -1,4 +1,5 @@ - - - - \ No newline at end of file + + + + + \ No newline at end of file diff --git a/test/selenium-driver.js b/test/selenium-driver.js index 2d0761f..9983f74 100644 --- a/test/selenium-driver.js +++ b/test/selenium-driver.js @@ -63,7 +63,7 @@ setups.forEach(function (setup) { return driver.executeScript('return mocha_stats;').then(function(stats) { assert(stats.tests > 0, 'No mocha tests were run'); - assert(stats.failures <= 0, 'Some mocha tests failed'); + assert(stats.failures <= 0, 'Some mocha tests failed, run locally for details'); if (!stats.failures) return true; return driver.executeScript('return mocha_failures;').then(function(failures) { diff --git a/test/test-template.hbs b/test/test-template.hbs index 3f681cf..835703c 100644 --- a/test/test-template.hbs +++ b/test/test-template.hbs @@ -1 +1 @@ -
    \ No newline at end of file +
    \ No newline at end of file diff --git a/test/tests.js b/test/tests.js index ad96637..56a99e6 100644 --- a/test/tests.js +++ b/test/tests.js @@ -65,7 +65,7 @@ describe('Subscribe Email Module', function() { service: 'universe', template: testTemplate }); - var testElement = $('#test-element .custom-template'); + var testElement = $('#test-element'); assert(testElement.length > 0); done(); }); From eafad14095bc231fe29af06242612b1b36863865 Mon Sep 17 00:00:00 2001 From: Josiah Sprague Date: Thu, 30 Oct 2014 15:13:05 -0400 Subject: [PATCH 55/60] [#MmLRZ2E2] add friendlier message for mocha test errors Branch: MmLRZ2E2-development --- test/selenium-driver.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/selenium-driver.js b/test/selenium-driver.js index 9983f74..20130ed 100644 --- a/test/selenium-driver.js +++ b/test/selenium-driver.js @@ -63,7 +63,7 @@ setups.forEach(function (setup) { return driver.executeScript('return mocha_stats;').then(function(stats) { assert(stats.tests > 0, 'No mocha tests were run'); - assert(stats.failures <= 0, 'Some mocha tests failed, run locally for details'); + assert(stats.failures <= 0, 'Tests failed, run manually' + setupDescription + ' for details'); if (!stats.failures) return true; return driver.executeScript('return mocha_failures;').then(function(failures) { From 72b76575c6ebe2d54172e40a9f1c98339615bb0d Mon Sep 17 00:00:00 2001 From: Josiah Sprague Date: Wed, 12 Nov 2014 13:11:33 -0500 Subject: [PATCH 56/60] [#MmLRZ2E2] address review feedback namespace module name with "blocks-", update alerter dependency to npm repo instead of github, remove global option from hbsfy transform, remove unnecessary code, update Events test to just test if object has emit method since specific events are tested elsewhere, update custom template test to check that the custom template is used and not just the default, change test API keys to be fake keys, remove duplicate prependMessagesTo docs Branch: MmLRZ2E2-development --- README.md | 5 +---- gulpfile.js | 2 +- package.json | 4 ++-- src/subscribe-email.js | 3 +-- test/demo/tests.html | 4 ++-- test/tests.js | 10 ++++------ 6 files changed, 11 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 2434e81..2453099 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ The module can be configured with several optional parameters passed to it's con **(Required)** A DOM element, jQuery element, or selector string to refer to the placeholder element. ### `prependMessagesTo` -A selector string that refers to the element that should receive response messages. Defaults to the same value set for `element`. +By default, responses from the different mailing list platforms will be prepended to the SubscribeEmail `element` as a dismissable alert, but you can use this option to override which element the alerts are prepended to. Accepts a query string or a jQuery object. ### `service` **(Required)** The mailing list platform you are using. Available options are `mailchimp`, `sendgrid` and `universe`. @@ -57,9 +57,6 @@ If you want to customize the markup, you can override the default markup by pass ### `namespace` Out of the box, the module will generate BEM markup with the namespace `subscribe-email`, but you can use this option to override the default without passing in a custom template. -### `prependMessagesTo` -By default, responses from the different mailing list platforms will be prepended to the SubscribeEmail `element` as a dismissable alert, but you can use this option to override which element the alerts are prepended to. Accepts a query string or a jQuery object. - ## Events You can easily integrate the messages into other parts of your page by listening for the following events to be emitted from the SubscribeEmail instance; diff --git a/gulpfile.js b/gulpfile.js index 43a4d25..9c2fa1a 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -35,7 +35,7 @@ gulp.task('build', function() { }); var bundle = function() { return bundler - .transform({global: true}, hbsfy) + .transform(hbsfy) .bundle() .pipe(source('subscribe-email.js')) .pipe(derequire()) diff --git a/package.json b/package.json index fd08ed5..1388211 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "subscribe-email", + "name": "blocks-subscribe", "version": "1.0.0", "description": "Subscribes an email address to a list. Supports a selection of email marketing services.", "keywords": [ @@ -37,7 +37,7 @@ "vinyl-source-stream": "^0.1.1" }, "dependencies": { - "alerter": "git://github.com/blocks/alerts#c5fe1dc", + "blocks-alerter": "^1.0.2", "form-serialize": "^0.3.0", "inherits": "^2.0.1" } diff --git a/src/subscribe-email.js b/src/subscribe-email.js index f823ab1..4d2c7e9 100644 --- a/src/subscribe-email.js +++ b/src/subscribe-email.js @@ -1,7 +1,7 @@ var template = require('./subscribe-form.hbs'); var serialize = require('form-serialize'); var inherits = require('inherits'); -var Alerter = require('alerter'); +var Alerter = require('blocks-alerter'); var EventEmitter = require('events').EventEmitter; inherits(SubscribeEmail, EventEmitter); @@ -97,7 +97,6 @@ function _setDefaults(options, instance) { if (typeof options.template === 'function') { instance.template = options.template; - delete options.template; } else { instance.template = template; } diff --git a/test/demo/tests.html b/test/demo/tests.html index 97b4bf9..72dc345 100644 --- a/test/demo/tests.html +++ b/test/demo/tests.html @@ -15,7 +15,7 @@ var universeForm = new SubscribeEmail({ element: '#universe-form', service: 'universe', - key: 'd54e8487-e44e-4c6f-bdd7-6ab9c2eae1e9' + key: 'test-key' }); @@ -26,7 +26,7 @@ var sendgridForm = SubscribeEmail({ element: '#sendgrid-form', service: 'sendgrid', - key: 'SDA+fsU1Qw6S6JIXfgrPngHjsFrn2z8v7VWCgt+a0ln11bNnVF1tvSwDWEK/pRiO' + key: 'test-key' }); diff --git a/test/tests.js b/test/tests.js index 56a99e6..1fb0fcd 100644 --- a/test/tests.js +++ b/test/tests.js @@ -17,16 +17,14 @@ describe('Subscribe Email Module', function() { document.body.appendChild(testElement); }); - describe('Events', function() { + describe('Initialization', function() { - it('Emits events', function(done){ + it('Has an emit method', function(done){ var subscribeInstance = SubscribeEmail({ element: '#test-element', service: 'universe' }); - var spy = sinon.spy(subscribeInstance, 'emit'); - subscribeInstance.emit('subscriptionMessage', 'Test Message'); - assert(spy.called); + assert(typeof subscribeInstance.emit === 'function'); done(); }); @@ -65,7 +63,7 @@ describe('Subscribe Email Module', function() { service: 'universe', template: testTemplate }); - var testElement = $('#test-element'); + var testElement = $('#test-element.custom-template'); assert(testElement.length > 0); done(); }); From 9ba64cb1798a63237a43cd07aa4cfc7366b81a67 Mon Sep 17 00:00:00 2001 From: Josiah Sprague Date: Wed, 12 Nov 2014 13:11:33 -0500 Subject: [PATCH 57/60] [#MmLRZ2E2] address review feedback namespace module name with "blocks-", update alerter dependency to npm repo instead of github, remove global option from hbsfy transform, remove unnecessary code, update Events test to just test if object has emit method since specific events are tested elsewhere, update custom template test to check that the custom template is used and not just the default, change test API keys to be fake keys, update README by adding better examples and remove duplicate prependMessagesTo docs Branch: MmLRZ2E2-development --- README.md | 37 ++++++++++++++++++++++--------------- gulpfile.js | 2 +- package.json | 4 ++-- src/subscribe-email.js | 3 +-- test/demo/tests.html | 4 ++-- test/tests.js | 10 ++++------ 6 files changed, 32 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index 2434e81..e6b7f38 100644 --- a/README.md +++ b/README.md @@ -8,21 +8,23 @@ You can get the module in any one of the following ways; - Or install with npm; `npm install subscribe-email` - Or install with Bower; `bower install subscribe-email` -# Quick Start -To get started, you'll need to include the script on your page, create a `
    ` element, and initialize the module. After you include subscribe-email.js in your project, here's some minimal code you can use to get started quickly; +# Example Usage +To get started, you'll need to include the script on your page, create a placeholder element, and initialize the module. After you include subscribe-email.js in your project, here's some minimal code you can use to get started quickly; ``` -
    +
    ``` ``` ``` @@ -37,7 +39,7 @@ The module can be configured with several optional parameters passed to it's con **(Required)** A DOM element, jQuery element, or selector string to refer to the placeholder element. ### `prependMessagesTo` -A selector string that refers to the element that should receive response messages. Defaults to the same value set for `element`. +By default, responses from the different mailing list platforms will be prepended to the SubscribeEmail `element` as a dismissable alert, but you can use this option to override which element the alerts are prepended to. Accepts a query string or a jQuery object. ### `service` **(Required)** The mailing list platform you are using. Available options are `mailchimp`, `sendgrid` and `universe`. @@ -57,17 +59,22 @@ If you want to customize the markup, you can override the default markup by pass ### `namespace` Out of the box, the module will generate BEM markup with the namespace `subscribe-email`, but you can use this option to override the default without passing in a custom template. -### `prependMessagesTo` -By default, responses from the different mailing list platforms will be prepended to the SubscribeEmail `element` as a dismissable alert, but you can use this option to override which element the alerts are prepended to. Accepts a query string or a jQuery object. - ## Events -You can easily integrate the messages into other parts of your page by listening for the following events to be emitted from the SubscribeEmail instance; +You can easily integrate the messages into other parts of your page by listening for events being emitted from the SubscribeEmail instance; + +``` +mySubscribeForm.on('subscriptionMessage', function(payload){ + console.log(payload); +}); +``` + +You can listen for the following events; ### `subscriptionMessage` Fires whenever the mailing list provider returns a response (both success and failure). The message will be passed to this event as a string. ### `subscriptionError` -This event will fire if the mailing list provider returns an error. Specific details about the error will be passed to the event as a payload object. +This event will fire if the mailing list provider returns an error. Specific details about the error will be passed to the event as a payload object. Note: the payload object may vary depending on the service. ### `subscriptionSuccess` -This event will fire if the mailing list provider returns a confirmation that the email address has been added to the list. Specific details will be passed to the event as a payload object. \ No newline at end of file +This event will fire if the mailing list provider returns a confirmation that the email address has been added to the list. Specific details will be passed to the event as a payload object. Note: the payload object may vary depending on the service. \ No newline at end of file diff --git a/gulpfile.js b/gulpfile.js index 43a4d25..9c2fa1a 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -35,7 +35,7 @@ gulp.task('build', function() { }); var bundle = function() { return bundler - .transform({global: true}, hbsfy) + .transform(hbsfy) .bundle() .pipe(source('subscribe-email.js')) .pipe(derequire()) diff --git a/package.json b/package.json index fd08ed5..1388211 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "subscribe-email", + "name": "blocks-subscribe", "version": "1.0.0", "description": "Subscribes an email address to a list. Supports a selection of email marketing services.", "keywords": [ @@ -37,7 +37,7 @@ "vinyl-source-stream": "^0.1.1" }, "dependencies": { - "alerter": "git://github.com/blocks/alerts#c5fe1dc", + "blocks-alerter": "^1.0.2", "form-serialize": "^0.3.0", "inherits": "^2.0.1" } diff --git a/src/subscribe-email.js b/src/subscribe-email.js index f823ab1..4d2c7e9 100644 --- a/src/subscribe-email.js +++ b/src/subscribe-email.js @@ -1,7 +1,7 @@ var template = require('./subscribe-form.hbs'); var serialize = require('form-serialize'); var inherits = require('inherits'); -var Alerter = require('alerter'); +var Alerter = require('blocks-alerter'); var EventEmitter = require('events').EventEmitter; inherits(SubscribeEmail, EventEmitter); @@ -97,7 +97,6 @@ function _setDefaults(options, instance) { if (typeof options.template === 'function') { instance.template = options.template; - delete options.template; } else { instance.template = template; } diff --git a/test/demo/tests.html b/test/demo/tests.html index 97b4bf9..72dc345 100644 --- a/test/demo/tests.html +++ b/test/demo/tests.html @@ -15,7 +15,7 @@ var universeForm = new SubscribeEmail({ element: '#universe-form', service: 'universe', - key: 'd54e8487-e44e-4c6f-bdd7-6ab9c2eae1e9' + key: 'test-key' }); @@ -26,7 +26,7 @@ var sendgridForm = SubscribeEmail({ element: '#sendgrid-form', service: 'sendgrid', - key: 'SDA+fsU1Qw6S6JIXfgrPngHjsFrn2z8v7VWCgt+a0ln11bNnVF1tvSwDWEK/pRiO' + key: 'test-key' }); diff --git a/test/tests.js b/test/tests.js index 56a99e6..1fb0fcd 100644 --- a/test/tests.js +++ b/test/tests.js @@ -17,16 +17,14 @@ describe('Subscribe Email Module', function() { document.body.appendChild(testElement); }); - describe('Events', function() { + describe('Initialization', function() { - it('Emits events', function(done){ + it('Has an emit method', function(done){ var subscribeInstance = SubscribeEmail({ element: '#test-element', service: 'universe' }); - var spy = sinon.spy(subscribeInstance, 'emit'); - subscribeInstance.emit('subscriptionMessage', 'Test Message'); - assert(spy.called); + assert(typeof subscribeInstance.emit === 'function'); done(); }); @@ -65,7 +63,7 @@ describe('Subscribe Email Module', function() { service: 'universe', template: testTemplate }); - var testElement = $('#test-element'); + var testElement = $('#test-element.custom-template'); assert(testElement.length > 0); done(); }); From 86a23a8f2ed1bee32cf38c61636197a10ee8ecc6 Mon Sep 17 00:00:00 2001 From: Josiah Sprague Date: Wed, 12 Nov 2014 14:18:36 -0500 Subject: [PATCH 58/60] [#MmLRZ2E2] change api urls to https Branch: MmLRZ2E2-development --- src/subscribe-email.js | 4 ++-- test/demo/tests.html | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/subscribe-email.js b/src/subscribe-email.js index 4d2c7e9..0300f40 100644 --- a/src/subscribe-email.js +++ b/src/subscribe-email.js @@ -103,12 +103,12 @@ function _setDefaults(options, instance) { switch (options.service) { case 'universe': - options.formAction = options.formAction || 'http://services.sparkart.net/api/v1/contacts'; + options.formAction = options.formAction || 'https://services.sparkart.net/api/v1/contacts'; options.emailName = options.emailName || 'contact[email]'; options.jsonp = !('withCredentials' in new XMLHttpRequest()); break; case 'sendgrid': - options.formAction = options.formAction || 'http://sendgrid.com/newsletter/addRecipientFromWidget'; + options.formAction = options.formAction || 'https://sendgrid.com/newsletter/addRecipientFromWidget'; options.emailName = options.emailName || 'SG_widget[email]'; options.jsonp = false; break; diff --git a/test/demo/tests.html b/test/demo/tests.html index 72dc345..52636aa 100644 --- a/test/demo/tests.html +++ b/test/demo/tests.html @@ -46,7 +46,7 @@ var server; var fakeAPIs = [ { - route: 'http://services.sparkart.net/api/v1/contacts', + route: 'https://services.sparkart.net/api/v1/contacts', email: 'contact[email]', required: ['key'], callback: 'callback', @@ -54,7 +54,7 @@ fail: '{"status":"error", "messages":["Fail!"]}' }, { - route: 'http://sendgrid.com/newsletter/addRecipientFromWidget', + route: 'https://sendgrid.com/newsletter/addRecipientFromWidget', email: 'SG_widget[email]', required: ['p','r'], success: '{"success":true, "message":"Success!"}', From 4eaeffc5198d5bd37cb6de489c6979b8cc076e7c Mon Sep 17 00:00:00 2001 From: Josiah Sprague Date: Thu, 13 Nov 2014 14:11:27 -0500 Subject: [PATCH 59/60] [#MmLRZ2E2] move hbsfy to package.json instead of gulp task to avoid double transforms that cause the source code text to be rendered into the page Branch: MmLRZ2E2-development --- gulpfile.js | 3 --- package.json | 5 +++++ 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/gulpfile.js b/gulpfile.js index 9c2fa1a..3ba0a10 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -1,7 +1,6 @@ var gulp = require('gulp'); var runSequence = require('run-sequence'); var browserify = require('browserify'); -var hbsfy = require('hbsfy'); var source = require('vinyl-source-stream'); var derequire = require('gulp-derequire'); var http = require('http'); @@ -35,7 +34,6 @@ gulp.task('build', function() { }); var bundle = function() { return bundler - .transform(hbsfy) .bundle() .pipe(source('subscribe-email.js')) .pipe(derequire()) @@ -50,7 +48,6 @@ gulp.task('build-tests', function() { }); var bundle = function() { return bundler - .transform({global: true}, hbsfy) .bundle() .pipe(source('tests.js')) .pipe(derequire()) diff --git a/package.json b/package.json index 1388211..1bc4ce7 100644 --- a/package.json +++ b/package.json @@ -40,5 +40,10 @@ "blocks-alerter": "^1.0.2", "form-serialize": "^0.3.0", "inherits": "^2.0.1" + }, + "browserify": { + "transform": [ + "hbsfy" + ] } } From d7090d9295a3138493ff79304da8e9bda44a641d Mon Sep 17 00:00:00 2001 From: Josiah Sprague Date: Thu, 13 Nov 2014 15:19:05 -0500 Subject: [PATCH 60/60] [#MmLRZ2E2] update metadata add more descriptive name and better keywords Branch: MmLRZ2E2-development --- package.json | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 1bc4ce7..0cf07fd 100644 --- a/package.json +++ b/package.json @@ -1,10 +1,19 @@ { - "name": "blocks-subscribe", + "name": "blocks-subscribe-email", "version": "1.0.0", "description": "Subscribes an email address to a list. Supports a selection of email marketing services.", "keywords": [ "email", - "subscription" + "subscription", + "blocks", + "browser", + "browserify", + "mailchimp", + "sendgrid", + "universe", + "ecosystem:browser", + "ecosystem:solidus", + "ecosystem:blocks" ], "repository": { "type": "git",