diff --git a/_site/assets/application.css b/_site/assets/application.css new file mode 100644 index 0000000..61268e6 --- /dev/null +++ b/_site/assets/application.css @@ -0,0 +1,149 @@ +/* line 4, /Users/john/Projects/attentive/lib/assets/stylesheets/attentive.css.scss */ +body, html { + margin: 0; + padding: 0; +} + +/* line 9, /Users/john/Projects/attentive/lib/assets/stylesheets/attentive.css.scss */ +.timer { + position: absolute; + top: 10px; + left: 10px; + opacity: 0.5; + font-size: 30px; + font-family: Courier New, monospace; +} +/* line 19, /Users/john/Projects/attentive/lib/assets/stylesheets/attentive.css.scss */ +.timer.hide { + display: none; +} +/* line 23, /Users/john/Projects/attentive/lib/assets/stylesheets/attentive.css.scss */ +.timer.running { + color: #c00; +} + +/* line 28, /Users/john/Projects/attentive/lib/assets/stylesheets/attentive.css.scss */ +#slides-container { + position: absolute; + width: 100%; + height: 100%; + overflow: hidden; + *zoom: 1; +} + +/* line 36, /Users/john/Projects/attentive/lib/assets/stylesheets/attentive.css.scss */ +body.loading .slide { + opacity: 0 !important; +} + +/* line 41, /Users/john/Projects/attentive/lib/assets/stylesheets/attentive.css.scss */ +#slides { + position: absolute; + top: 0; + height: 100%; +} +/* line 46, /Users/john/Projects/attentive/lib/assets/stylesheets/attentive.css.scss */ +#slides .slide { + display: inline; + float: left; + position: relative; + height: 100%; +} +/* line 51, /Users/john/Projects/attentive/lib/assets/stylesheets/attentive.css.scss */ +#slides .slide .content { + text-align: center; + position: absolute; + left: 5%; + width: 90%; +} +/* line 58, /Users/john/Projects/attentive/lib/assets/stylesheets/attentive.css.scss */ +#slides .slide .content * { + line-height: 105%; +} +/* line 62, /Users/john/Projects/attentive/lib/assets/stylesheets/attentive.css.scss */ +#slides .slide .content h1, #slides .slide .content h2, #slides .slide .content h3 { + margin: 0; +} +/* line 66, /Users/john/Projects/attentive/lib/assets/stylesheets/attentive.css.scss */ +#slides .slide .content h1 { + font-size: 6.5em; +} +/* line 70, /Users/john/Projects/attentive/lib/assets/stylesheets/attentive.css.scss */ +#slides .slide .content h2 { + font-size: 4em; +} +/* line 74, /Users/john/Projects/attentive/lib/assets/stylesheets/attentive.css.scss */ +#slides .slide .content h3 { + font-size: 2em; +} +/* line 78, /Users/john/Projects/attentive/lib/assets/stylesheets/attentive.css.scss */ +#slides .slide .content div.highlight { + text-align: left; + padding: 1em; + font-size: 160%; +} +/* line 85, /Users/john/Projects/attentive/lib/assets/stylesheets/attentive.css.scss */ +#slides .slide .content div.highlight pre { + margin: 0; +} +/* line 92, /Users/john/Projects/attentive/lib/assets/stylesheets/attentive.css.scss */ +#slides .slide.style-smaller div.highlight { + font-size: 125%; +} +/* line 98, /Users/john/Projects/attentive/lib/assets/stylesheets/attentive.css.scss */ +#slides .slide.style-larger div.highlight { + font-size: 200%; +} +/* line 104, /Users/john/Projects/attentive/lib/assets/stylesheets/attentive.css.scss */ +#slides .slide.style-even-larger div.highlight { + font-size: 250%; +} + +/* line 5, /Users/john/Projects/tea-time-beginners-talk-on-jasmine/assets/stylesheets/application.css.scss */ +body, html { + background-color: #d4d2bf; +} + +/* line 9, /Users/john/Projects/tea-time-beginners-talk-on-jasmine/assets/stylesheets/application.css.scss */ +h1, h2, h3 { + font-family: Acme, sans-serif; +} + +/* line 13, /Users/john/Projects/tea-time-beginners-talk-on-jasmine/assets/stylesheets/application.css.scss */ +div.highlight { + background-color: #c0bda0; +} + +/* line 18, /Users/john/Projects/tea-time-beginners-talk-on-jasmine/assets/stylesheets/application.css.scss */ +.style-image-80-percent img { + height: 80%; +} + +/* line 23, /Users/john/Projects/tea-time-beginners-talk-on-jasmine/assets/stylesheets/application.css.scss */ +#slides { + -webkit-transition-duration: 0.3s; + -moz-transition-duration: 0.3s; + -ms-transition-duration: 0.3s; + -o-transition-duration: 0.3s; + transition-duration: 0.3s; +} + +/* line 27, /Users/john/Projects/tea-time-beginners-talk-on-jasmine/assets/stylesheets/application.css.scss */ +#intro { + width: 90%; + overflow: hidden; + *zoom: 1; + position: relative; +} +/* line 33, /Users/john/Projects/tea-time-beginners-talk-on-jasmine/assets/stylesheets/application.css.scss */ +#intro img { + width: 30%; + display: inline; + float: left; +} +/* line 38, /Users/john/Projects/tea-time-beginners-talk-on-jasmine/assets/stylesheets/application.css.scss */ +#intro div { + width: 70%; + display: inline; + float: right; +} diff --git a/_site/assets/application.js b/_site/assets/application.js new file mode 100644 index 0000000..6333349 --- /dev/null +++ b/_site/assets/application.js @@ -0,0 +1,1322 @@ +(function() { + var Attentive, + __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; + + if (!(typeof Attentive !== "undefined" && Attentive !== null)) Attentive = {}; + + Attentive.Presentation = (function() { + + Presentation.setup = function(identifier) { + var starter; + starter = function() { + return setTimeout(function() { + return (new Attentive.Presentation(identifier)).start(); + }, 250); + }; + return window.addEventListener('DOMContentLoaded', starter, false); + }; + + function Presentation(identifier) { + this.identifier = identifier; + this.align = __bind(this.align, this); + this.getCurrentSlide = __bind(this.getCurrentSlide, this); + this.calculate = __bind(this.calculate, this); + this.advanceTo = __bind(this.advanceTo, this); + this.isFile = __bind(this.isFile, this); + this.advance = __bind(this.advance, this); + this.handleKeyDown = __bind(this.handleKeyDown, this); + this.handleClick = __bind(this.handleClick, this); + this.handlePopState = __bind(this.handlePopState, this); + this.length = this.allSlides().length; + this.priorSlide = null; + this.initialRender = true; + this.timer = new Attentive.PresentationTimer(); + this.timer.hide(); + this.currentWindowHeight = null; + document.querySelector('body').appendChild(this.timer.el); + } + + Presentation.prototype.bodyClassList = function() { + return this._bodyClassList || (this._bodyClassList = document.querySelector('body').classList); + }; + + Presentation.prototype.allSlides = function() { + return this._allSlides || (this._allSlides = Attentive.Slide.fromList(this.slidesViewer().querySelectorAll('.slide'))); + }; + + Presentation.prototype.slidesViewer = function() { + return this._slidesViewer || (this._slidesViewer = document.querySelector(this.identifier)); + }; + + Presentation.prototype.start = function() { + var imageWait, + _this = this; + if (!this.isFile()) { + window.addEventListener('popstate', this.handlePopState, false); + } + this.timer.render(); + document.addEventListener('click', this.handleClick, false); + document.addEventListener('keydown', this.handleKeyDown, false); + window.addEventListener('resize', _.throttle(this.calculate, 500), false); + imageWait = null; + imageWait = function() { + var img, slide, wait, _i, _j, _len, _len2, _ref, _ref2; + wait = false; + _ref = _this.allSlides(); + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + slide = _ref[_i]; + _ref2 = slide.dom.getElementsByTagName('img'); + for (_j = 0, _len2 = _ref2.length; _j < _len2; _j++) { + img = _ref2[_j]; + if (!img.complete) wait = true; + } + } + if (wait) { + return setTimeout(imageWait, 100); + } else { + return _this.advanceTo(_this.slideFromLocation()); + } + }; + return imageWait(); + }; + + Presentation.prototype.slideFromLocation = function() { + var value; + value = this.isFile() ? location.hash : location.pathname; + return Number(value.substr(1)); + }; + + Presentation.prototype.handlePopState = function(e) { + return this.advanceTo(e.state ? e.state.index : this.slideFromLocation()); + }; + + Presentation.prototype.handleClick = function(e) { + if (e.target.tagName !== 'A') return this.advance(); + }; + + Presentation.prototype.handleKeyDown = function(e) { + switch (e.keyCode) { + case 72: + return this.advanceTo(0); + case 37: + return this.advance(-1); + case 39: + case 32: + return this.advance(); + case 220: + return this.timer.reset(); + case 84: + if (e.shiftKey) { + return this.timer.toggleVisible(); + } else { + if (this.timer.isVisible()) return this.timer.toggle(); + } + } + }; + + Presentation.prototype.advance = function(offset) { + if (offset == null) offset = 1; + return this.advanceTo(Math.max(Math.min(this.currentSlide + offset, this.length - 1), 0)); + }; + + Presentation.prototype.isFile = function() { + return location.href.slice(0, 4) === 'file'; + }; + + Presentation.prototype.advanceTo = function(index) { + this.priorSlide = this.currentSlide; + this.currentSlide = index || 0; + this.calculate(); + if (this.isFile()) { + return location.hash = this.currentSlide; + } else { + return history.pushState({ + index: this.currentSlide + }, '', this.currentSlide); + } + }; + + Presentation.prototype.calculate = function() { + var recalculate, slide, times, _i, _len, _ref; + if (this.currentWindowHeight !== window.innerHeight) { + recalculate = true; + times = 3; + while (recalculate && times > 0) { + recalculate = false; + times -= 1; + _ref = this.allSlides(); + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + slide = _ref[_i]; + if (slide.recalculate()) recalculate = true; + } + } + this.currentWindowHeight = window.innerHeight; + this.slidesViewer().style['width'] = "" + (window.innerWidth * this.allSlides().length) + "px"; + } + return this.align(); + }; + + Presentation.prototype.getCurrentSlide = function() { + return this.allSlides()[this.currentSlide]; + }; + + Presentation.prototype.align = function() { + if (this.priorSlide) this.allSlides()[this.priorSlide].deactivate(); + this.getCurrentSlide().activate(); + this.slidesViewer().style['left'] = "-" + (this.currentSlide * window.innerWidth) + "px"; + if (this.initialRender) { + this.bodyClassList().remove('loading'); + this.initialRender = false; + this.currentWindowHeight = null; + return this.calculate(); + } + }; + + return Presentation; + + })(); + +}).call(this); +(function() { + var Attentive, + __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; + + if (!(typeof Attentive !== "undefined" && Attentive !== null)) Attentive = {}; + + Attentive.Slide = (function() { + + Slide.fromList = function(list) { + var result, slide; + return result = (function() { + var _i, _len, _results; + _results = []; + for (_i = 0, _len = list.length; _i < _len; _i++) { + slide = list[_i]; + _results.push(new Attentive.Slide(slide)); + } + return _results; + })(); + }; + + function Slide(dom) { + this.dom = dom; + this.deactivate = __bind(this.deactivate, this); + this.activate = __bind(this.activate, this); + this.recalculate = __bind(this.recalculate, this); + } + + Slide.prototype.recalculate = function() { + var currentMarginTop, height; + this.dom.style['width'] = "" + window.innerWidth + "px"; + currentMarginTop = Number(this.dom.style['marginTop'].replace(/[^\d\.]/g, '')); + height = (window.innerHeight - this.dom.querySelector('.content').clientHeight) / 2; + if (height !== currentMarginTop) { + this.dom.style['marginTop'] = "" + height + "px"; + return true; + } + }; + + Slide.prototype.activate = function() { + return this.dom.classList.add('active'); + }; + + Slide.prototype.deactivate = function() { + return this.dom.classList.remove('active'); + }; + + return Slide; + + })(); + +}).call(this); +(function() { + var Attentive; + + if (!(typeof Attentive !== "undefined" && Attentive !== null)) Attentive = {}; + + Attentive.PresentationTimer = (function() { + + function PresentationTimer() { + this.time = 0; + this.el = null; + } + + PresentationTimer.prototype.render = function() { + return this.ensureEl().innerHTML = this.formattedTime(); + }; + + PresentationTimer.prototype.ensureEl = function() { + if (!this.el) { + this.el = document.createElement('div'); + this.el.classList.add('timer'); + } + return this.el; + }; + + PresentationTimer.prototype.start = function() { + this._runner = this.runner(); + return this.ensureEl().classList.add('running'); + }; + + PresentationTimer.prototype.runner = function() { + var _this = this; + return setTimeout(function() { + _this.render(); + _this.time += 1; + if (_this._runner != null) return _this.runner(); + }, 1000); + }; + + PresentationTimer.prototype.stop = function() { + clearTimeout(this._runner); + this.ensureEl().classList.remove('running'); + return this._runner = null; + }; + + PresentationTimer.prototype.reset = function() { + this.stop(); + this.time = 0; + return this.render(); + }; + + PresentationTimer.prototype.toggle = function() { + if (this._runner != null) { + return this.stop(); + } else { + return this.start(); + } + }; + + PresentationTimer.prototype.toggleVisible = function() { + return this.ensureEl().classList.toggle('hide'); + }; + + PresentationTimer.prototype.isVisible = function() { + return !this.ensureEl().classList.contains('hide'); + }; + + PresentationTimer.prototype.hide = function() { + return this.ensureEl().classList.add('hide'); + }; + + PresentationTimer.prototype.formattedTime = function() { + var minute, second; + minute = ("00" + (Math.floor(this.time / 60))).slice(-2); + second = ("00" + (this.time % 60)).slice(-2); + return "" + minute + ":" + second; + }; + + return PresentationTimer; + + })(); + +}).call(this); +// Underscore.js 1.3.1 +// (c) 2009-2012 Jeremy Ashkenas, DocumentCloud Inc. +// Underscore is freely distributable under the MIT license. +// Portions of Underscore are inspired or borrowed from Prototype, +// Oliver Steele's Functional, and John Resig's Micro-Templating. +// For all details and documentation: +// http://documentcloud.github.com/underscore + +(function() { + + // Baseline setup + // -------------- + + // Establish the root object, `window` in the browser, or `global` on the server. + var root = this; + + // Save the previous value of the `_` variable. + var previousUnderscore = root._; + + // Establish the object that gets returned to break out of a loop iteration. + var breaker = {}; + + // Save bytes in the minified (but not gzipped) version: + var ArrayProto = Array.prototype, ObjProto = Object.prototype, FuncProto = Function.prototype; + + // Create quick reference variables for speed access to core prototypes. + var slice = ArrayProto.slice, + unshift = ArrayProto.unshift, + toString = ObjProto.toString, + hasOwnProperty = ObjProto.hasOwnProperty; + + // All **ECMAScript 5** native function implementations that we hope to use + // are declared here. + var + nativeForEach = ArrayProto.forEach, + nativeMap = ArrayProto.map, + nativeReduce = ArrayProto.reduce, + nativeReduceRight = ArrayProto.reduceRight, + nativeFilter = ArrayProto.filter, + nativeEvery = ArrayProto.every, + nativeSome = ArrayProto.some, + nativeIndexOf = ArrayProto.indexOf, + nativeLastIndexOf = ArrayProto.lastIndexOf, + nativeIsArray = Array.isArray, + nativeKeys = Object.keys, + nativeBind = FuncProto.bind; + + // Create a safe reference to the Underscore object for use below. + var _ = function(obj) { return new wrapper(obj); }; + + // Export the Underscore object for **Node.js**, with + // backwards-compatibility for the old `require()` API. If we're in + // the browser, add `_` as a global object via a string identifier, + // for Closure Compiler "advanced" mode. + if (typeof exports !== 'undefined') { + if (typeof module !== 'undefined' && module.exports) { + exports = module.exports = _; + } + exports._ = _; + } else { + root['_'] = _; + } + + // Current version. + _.VERSION = '1.3.1'; + + // Collection Functions + // -------------------- + + // The cornerstone, an `each` implementation, aka `forEach`. + // Handles objects with the built-in `forEach`, arrays, and raw objects. + // Delegates to **ECMAScript 5**'s native `forEach` if available. + var each = _.each = _.forEach = function(obj, iterator, context) { + if (obj == null) return; + if (nativeForEach && obj.forEach === nativeForEach) { + obj.forEach(iterator, context); + } else if (obj.length === +obj.length) { + for (var i = 0, l = obj.length; i < l; i++) { + if (i in obj && iterator.call(context, obj[i], i, obj) === breaker) return; + } + } else { + for (var key in obj) { + if (_.has(obj, key)) { + if (iterator.call(context, obj[key], key, obj) === breaker) return; + } + } + } + }; + + // Return the results of applying the iterator to each element. + // Delegates to **ECMAScript 5**'s native `map` if available. + _.map = _.collect = function(obj, iterator, context) { + var results = []; + if (obj == null) return results; + if (nativeMap && obj.map === nativeMap) return obj.map(iterator, context); + each(obj, function(value, index, list) { + results[results.length] = iterator.call(context, value, index, list); + }); + if (obj.length === +obj.length) results.length = obj.length; + return results; + }; + + // **Reduce** builds up a single result from a list of values, aka `inject`, + // or `foldl`. Delegates to **ECMAScript 5**'s native `reduce` if available. + _.reduce = _.foldl = _.inject = function(obj, iterator, memo, context) { + var initial = arguments.length > 2; + if (obj == null) obj = []; + if (nativeReduce && obj.reduce === nativeReduce) { + if (context) iterator = _.bind(iterator, context); + return initial ? obj.reduce(iterator, memo) : obj.reduce(iterator); + } + each(obj, function(value, index, list) { + if (!initial) { + memo = value; + initial = true; + } else { + memo = iterator.call(context, memo, value, index, list); + } + }); + if (!initial) throw new TypeError('Reduce of empty array with no initial value'); + return memo; + }; + + // The right-associative version of reduce, also known as `foldr`. + // Delegates to **ECMAScript 5**'s native `reduceRight` if available. + _.reduceRight = _.foldr = function(obj, iterator, memo, context) { + var initial = arguments.length > 2; + if (obj == null) obj = []; + if (nativeReduceRight && obj.reduceRight === nativeReduceRight) { + if (context) iterator = _.bind(iterator, context); + return initial ? obj.reduceRight(iterator, memo) : obj.reduceRight(iterator); + } + var reversed = _.toArray(obj).reverse(); + if (context && !initial) iterator = _.bind(iterator, context); + return initial ? _.reduce(reversed, iterator, memo, context) : _.reduce(reversed, iterator); + }; + + // Return the first value which passes a truth test. Aliased as `detect`. + _.find = _.detect = function(obj, iterator, context) { + var result; + any(obj, function(value, index, list) { + if (iterator.call(context, value, index, list)) { + result = value; + return true; + } + }); + return result; + }; + + // Return all the elements that pass a truth test. + // Delegates to **ECMAScript 5**'s native `filter` if available. + // Aliased as `select`. + _.filter = _.select = function(obj, iterator, context) { + var results = []; + if (obj == null) return results; + if (nativeFilter && obj.filter === nativeFilter) return obj.filter(iterator, context); + each(obj, function(value, index, list) { + if (iterator.call(context, value, index, list)) results[results.length] = value; + }); + return results; + }; + + // Return all the elements for which a truth test fails. + _.reject = function(obj, iterator, context) { + var results = []; + if (obj == null) return results; + each(obj, function(value, index, list) { + if (!iterator.call(context, value, index, list)) results[results.length] = value; + }); + return results; + }; + + // Determine whether all of the elements match a truth test. + // Delegates to **ECMAScript 5**'s native `every` if available. + // Aliased as `all`. + _.every = _.all = function(obj, iterator, context) { + var result = true; + if (obj == null) return result; + if (nativeEvery && obj.every === nativeEvery) return obj.every(iterator, context); + each(obj, function(value, index, list) { + if (!(result = result && iterator.call(context, value, index, list))) return breaker; + }); + return result; + }; + + // Determine if at least one element in the object matches a truth test. + // Delegates to **ECMAScript 5**'s native `some` if available. + // Aliased as `any`. + var any = _.some = _.any = function(obj, iterator, context) { + iterator || (iterator = _.identity); + var result = false; + if (obj == null) return result; + if (nativeSome && obj.some === nativeSome) return obj.some(iterator, context); + each(obj, function(value, index, list) { + if (result || (result = iterator.call(context, value, index, list))) return breaker; + }); + return !!result; + }; + + // Determine if a given value is included in the array or object using `===`. + // Aliased as `contains`. + _.include = _.contains = function(obj, target) { + var found = false; + if (obj == null) return found; + if (nativeIndexOf && obj.indexOf === nativeIndexOf) return obj.indexOf(target) != -1; + found = any(obj, function(value) { + return value === target; + }); + return found; + }; + + // Invoke a method (with arguments) on every item in a collection. + _.invoke = function(obj, method) { + var args = slice.call(arguments, 2); + return _.map(obj, function(value) { + return (_.isFunction(method) ? method || value : value[method]).apply(value, args); + }); + }; + + // Convenience version of a common use case of `map`: fetching a property. + _.pluck = function(obj, key) { + return _.map(obj, function(value){ return value[key]; }); + }; + + // Return the maximum element or (element-based computation). + _.max = function(obj, iterator, context) { + if (!iterator && _.isArray(obj)) return Math.max.apply(Math, obj); + if (!iterator && _.isEmpty(obj)) return -Infinity; + var result = {computed : -Infinity}; + each(obj, function(value, index, list) { + var computed = iterator ? iterator.call(context, value, index, list) : value; + computed >= result.computed && (result = {value : value, computed : computed}); + }); + return result.value; + }; + + // Return the minimum element (or element-based computation). + _.min = function(obj, iterator, context) { + if (!iterator && _.isArray(obj)) return Math.min.apply(Math, obj); + if (!iterator && _.isEmpty(obj)) return Infinity; + var result = {computed : Infinity}; + each(obj, function(value, index, list) { + var computed = iterator ? iterator.call(context, value, index, list) : value; + computed < result.computed && (result = {value : value, computed : computed}); + }); + return result.value; + }; + + // Shuffle an array. + _.shuffle = function(obj) { + var shuffled = [], rand; + each(obj, function(value, index, list) { + if (index == 0) { + shuffled[0] = value; + } else { + rand = Math.floor(Math.random() * (index + 1)); + shuffled[index] = shuffled[rand]; + shuffled[rand] = value; + } + }); + return shuffled; + }; + + // Sort the object's values by a criterion produced by an iterator. + _.sortBy = function(obj, iterator, context) { + return _.pluck(_.map(obj, function(value, index, list) { + return { + value : value, + criteria : iterator.call(context, value, index, list) + }; + }).sort(function(left, right) { + var a = left.criteria, b = right.criteria; + return a < b ? -1 : a > b ? 1 : 0; + }), 'value'); + }; + + // Groups the object's values by a criterion. Pass either a string attribute + // to group by, or a function that returns the criterion. + _.groupBy = function(obj, val) { + var result = {}; + var iterator = _.isFunction(val) ? val : function(obj) { return obj[val]; }; + each(obj, function(value, index) { + var key = iterator(value, index); + (result[key] || (result[key] = [])).push(value); + }); + return result; + }; + + // Use a comparator function to figure out at what index an object should + // be inserted so as to maintain order. Uses binary search. + _.sortedIndex = function(array, obj, iterator) { + iterator || (iterator = _.identity); + var low = 0, high = array.length; + while (low < high) { + var mid = (low + high) >> 1; + iterator(array[mid]) < iterator(obj) ? low = mid + 1 : high = mid; + } + return low; + }; + + // Safely convert anything iterable into a real, live array. + _.toArray = function(iterable) { + if (!iterable) return []; + if (iterable.toArray) return iterable.toArray(); + if (_.isArray(iterable)) return slice.call(iterable); + if (_.isArguments(iterable)) return slice.call(iterable); + return _.values(iterable); + }; + + // Return the number of elements in an object. + _.size = function(obj) { + return _.toArray(obj).length; + }; + + // Array Functions + // --------------- + + // Get the first element of an array. Passing **n** will return the first N + // values in the array. Aliased as `head`. The **guard** check allows it to work + // with `_.map`. + _.first = _.head = function(array, n, guard) { + return (n != null) && !guard ? slice.call(array, 0, n) : array[0]; + }; + + // Returns everything but the last entry of the array. Especcialy useful on + // the arguments object. Passing **n** will return all the values in + // the array, excluding the last N. The **guard** check allows it to work with + // `_.map`. + _.initial = function(array, n, guard) { + return slice.call(array, 0, array.length - ((n == null) || guard ? 1 : n)); + }; + + // Get the last element of an array. Passing **n** will return the last N + // values in the array. The **guard** check allows it to work with `_.map`. + _.last = function(array, n, guard) { + if ((n != null) && !guard) { + return slice.call(array, Math.max(array.length - n, 0)); + } else { + return array[array.length - 1]; + } + }; + + // Returns everything but the first entry of the array. Aliased as `tail`. + // Especially useful on the arguments object. Passing an **index** will return + // the rest of the values in the array from that index onward. The **guard** + // check allows it to work with `_.map`. + _.rest = _.tail = function(array, index, guard) { + return slice.call(array, (index == null) || guard ? 1 : index); + }; + + // Trim out all falsy values from an array. + _.compact = function(array) { + return _.filter(array, function(value){ return !!value; }); + }; + + // Return a completely flattened version of an array. + _.flatten = function(array, shallow) { + return _.reduce(array, function(memo, value) { + if (_.isArray(value)) return memo.concat(shallow ? value : _.flatten(value)); + memo[memo.length] = value; + return memo; + }, []); + }; + + // Return a version of the array that does not contain the specified value(s). + _.without = function(array) { + return _.difference(array, slice.call(arguments, 1)); + }; + + // Produce a duplicate-free version of the array. If the array has already + // been sorted, you have the option of using a faster algorithm. + // Aliased as `unique`. + _.uniq = _.unique = function(array, isSorted, iterator) { + var initial = iterator ? _.map(array, iterator) : array; + var result = []; + _.reduce(initial, function(memo, el, i) { + if (0 == i || (isSorted === true ? _.last(memo) != el : !_.include(memo, el))) { + memo[memo.length] = el; + result[result.length] = array[i]; + } + return memo; + }, []); + return result; + }; + + // Produce an array that contains the union: each distinct element from all of + // the passed-in arrays. + _.union = function() { + return _.uniq(_.flatten(arguments, true)); + }; + + // Produce an array that contains every item shared between all the + // passed-in arrays. (Aliased as "intersect" for back-compat.) + _.intersection = _.intersect = function(array) { + var rest = slice.call(arguments, 1); + return _.filter(_.uniq(array), function(item) { + return _.every(rest, function(other) { + return _.indexOf(other, item) >= 0; + }); + }); + }; + + // Take the difference between one array and a number of other arrays. + // Only the elements present in just the first array will remain. + _.difference = function(array) { + var rest = _.flatten(slice.call(arguments, 1)); + return _.filter(array, function(value){ return !_.include(rest, value); }); + }; + + // Zip together multiple lists into a single array -- elements that share + // an index go together. + _.zip = function() { + var args = slice.call(arguments); + var length = _.max(_.pluck(args, 'length')); + var results = new Array(length); + for (var i = 0; i < length; i++) results[i] = _.pluck(args, "" + i); + return results; + }; + + // If the browser doesn't supply us with indexOf (I'm looking at you, **MSIE**), + // we need this function. Return the position of the first occurrence of an + // item in an array, or -1 if the item is not included in the array. + // Delegates to **ECMAScript 5**'s native `indexOf` if available. + // If the array is large and already in sort order, pass `true` + // for **isSorted** to use binary search. + _.indexOf = function(array, item, isSorted) { + if (array == null) return -1; + var i, l; + if (isSorted) { + i = _.sortedIndex(array, item); + return array[i] === item ? i : -1; + } + if (nativeIndexOf && array.indexOf === nativeIndexOf) return array.indexOf(item); + for (i = 0, l = array.length; i < l; i++) if (i in array && array[i] === item) return i; + return -1; + }; + + // Delegates to **ECMAScript 5**'s native `lastIndexOf` if available. + _.lastIndexOf = function(array, item) { + if (array == null) return -1; + if (nativeLastIndexOf && array.lastIndexOf === nativeLastIndexOf) return array.lastIndexOf(item); + var i = array.length; + while (i--) if (i in array && array[i] === item) return i; + return -1; + }; + + // Generate an integer Array containing an arithmetic progression. A port of + // the native Python `range()` function. See + // [the Python documentation](http://docs.python.org/library/functions.html#range). + _.range = function(start, stop, step) { + if (arguments.length <= 1) { + stop = start || 0; + start = 0; + } + step = arguments[2] || 1; + + var len = Math.max(Math.ceil((stop - start) / step), 0); + var idx = 0; + var range = new Array(len); + + while(idx < len) { + range[idx++] = start; + start += step; + } + + return range; + }; + + // Function (ahem) Functions + // ------------------ + + // Reusable constructor function for prototype setting. + var ctor = function(){}; + + // Create a function bound to a given object (assigning `this`, and arguments, + // optionally). Binding with arguments is also known as `curry`. + // Delegates to **ECMAScript 5**'s native `Function.bind` if available. + // We check for `func.bind` first, to fail fast when `func` is undefined. + _.bind = function bind(func, context) { + var bound, args; + if (func.bind === nativeBind && nativeBind) return nativeBind.apply(func, slice.call(arguments, 1)); + if (!_.isFunction(func)) throw new TypeError; + args = slice.call(arguments, 2); + return bound = function() { + if (!(this instanceof bound)) return func.apply(context, args.concat(slice.call(arguments))); + ctor.prototype = func.prototype; + var self = new ctor; + var result = func.apply(self, args.concat(slice.call(arguments))); + if (Object(result) === result) return result; + return self; + }; + }; + + // Bind all of an object's methods to that object. Useful for ensuring that + // all callbacks defined on an object belong to it. + _.bindAll = function(obj) { + var funcs = slice.call(arguments, 1); + if (funcs.length == 0) funcs = _.functions(obj); + each(funcs, function(f) { obj[f] = _.bind(obj[f], obj); }); + return obj; + }; + + // Memoize an expensive function by storing its results. + _.memoize = function(func, hasher) { + var memo = {}; + hasher || (hasher = _.identity); + return function() { + var key = hasher.apply(this, arguments); + return _.has(memo, key) ? memo[key] : (memo[key] = func.apply(this, arguments)); + }; + }; + + // Delays a function for the given number of milliseconds, and then calls + // it with the arguments supplied. + _.delay = function(func, wait) { + var args = slice.call(arguments, 2); + return setTimeout(function(){ return func.apply(func, args); }, wait); + }; + + // Defers a function, scheduling it to run after the current call stack has + // cleared. + _.defer = function(func) { + return _.delay.apply(_, [func, 1].concat(slice.call(arguments, 1))); + }; + + // Returns a function, that, when invoked, will only be triggered at most once + // during a given window of time. + _.throttle = function(func, wait) { + var context, args, timeout, throttling, more; + var whenDone = _.debounce(function(){ more = throttling = false; }, wait); + return function() { + context = this; args = arguments; + var later = function() { + timeout = null; + if (more) func.apply(context, args); + whenDone(); + }; + if (!timeout) timeout = setTimeout(later, wait); + if (throttling) { + more = true; + } else { + func.apply(context, args); + } + whenDone(); + throttling = true; + }; + }; + + // Returns a function, that, as long as it continues to be invoked, will not + // be triggered. The function will be called after it stops being called for + // N milliseconds. + _.debounce = function(func, wait) { + var timeout; + return function() { + var context = this, args = arguments; + var later = function() { + timeout = null; + func.apply(context, args); + }; + clearTimeout(timeout); + timeout = setTimeout(later, wait); + }; + }; + + // Returns a function that will be executed at most one time, no matter how + // often you call it. Useful for lazy initialization. + _.once = function(func) { + var ran = false, memo; + return function() { + if (ran) return memo; + ran = true; + return memo = func.apply(this, arguments); + }; + }; + + // Returns the first function passed as an argument to the second, + // allowing you to adjust arguments, run code before and after, and + // conditionally execute the original function. + _.wrap = function(func, wrapper) { + return function() { + var args = [func].concat(slice.call(arguments, 0)); + return wrapper.apply(this, args); + }; + }; + + // Returns a function that is the composition of a list of functions, each + // consuming the return value of the function that follows. + _.compose = function() { + var funcs = arguments; + return function() { + var args = arguments; + for (var i = funcs.length - 1; i >= 0; i--) { + args = [funcs[i].apply(this, args)]; + } + return args[0]; + }; + }; + + // Returns a function that will only be executed after being called N times. + _.after = function(times, func) { + if (times <= 0) return func(); + return function() { + if (--times < 1) { return func.apply(this, arguments); } + }; + }; + + // Object Functions + // ---------------- + + // Retrieve the names of an object's properties. + // Delegates to **ECMAScript 5**'s native `Object.keys` + _.keys = nativeKeys || function(obj) { + if (obj !== Object(obj)) throw new TypeError('Invalid object'); + var keys = []; + for (var key in obj) if (_.has(obj, key)) keys[keys.length] = key; + return keys; + }; + + // Retrieve the values of an object's properties. + _.values = function(obj) { + return _.map(obj, _.identity); + }; + + // Return a sorted list of the function names available on the object. + // Aliased as `methods` + _.functions = _.methods = function(obj) { + var names = []; + for (var key in obj) { + if (_.isFunction(obj[key])) names.push(key); + } + return names.sort(); + }; + + // Extend a given object with all the properties in passed-in object(s). + _.extend = function(obj) { + each(slice.call(arguments, 1), function(source) { + for (var prop in source) { + obj[prop] = source[prop]; + } + }); + return obj; + }; + + // Fill in a given object with default properties. + _.defaults = function(obj) { + each(slice.call(arguments, 1), function(source) { + for (var prop in source) { + if (obj[prop] == null) obj[prop] = source[prop]; + } + }); + return obj; + }; + + // Create a (shallow-cloned) duplicate of an object. + _.clone = function(obj) { + if (!_.isObject(obj)) return obj; + return _.isArray(obj) ? obj.slice() : _.extend({}, obj); + }; + + // Invokes interceptor with the obj, and then returns obj. + // The primary purpose of this method is to "tap into" a method chain, in + // order to perform operations on intermediate results within the chain. + _.tap = function(obj, interceptor) { + interceptor(obj); + return obj; + }; + + // Internal recursive comparison function. + function eq(a, b, stack) { + // Identical objects are equal. `0 === -0`, but they aren't identical. + // See the Harmony `egal` proposal: http://wiki.ecmascript.org/doku.php?id=harmony:egal. + if (a === b) return a !== 0 || 1 / a == 1 / b; + // A strict comparison is necessary because `null == undefined`. + if (a == null || b == null) return a === b; + // Unwrap any wrapped objects. + if (a._chain) a = a._wrapped; + if (b._chain) b = b._wrapped; + // Invoke a custom `isEqual` method if one is provided. + if (a.isEqual && _.isFunction(a.isEqual)) return a.isEqual(b); + if (b.isEqual && _.isFunction(b.isEqual)) return b.isEqual(a); + // Compare `[[Class]]` names. + var className = toString.call(a); + if (className != toString.call(b)) return false; + switch (className) { + // Strings, numbers, dates, and booleans are compared by value. + case '[object String]': + // Primitives and their corresponding object wrappers are equivalent; thus, `"5"` is + // equivalent to `new String("5")`. + return a == String(b); + case '[object Number]': + // `NaN`s are equivalent, but non-reflexive. An `egal` comparison is performed for + // other numeric values. + return a != +a ? b != +b : (a == 0 ? 1 / a == 1 / b : a == +b); + case '[object Date]': + case '[object Boolean]': + // Coerce dates and booleans to numeric primitive values. Dates are compared by their + // millisecond representations. Note that invalid dates with millisecond representations + // of `NaN` are not equivalent. + return +a == +b; + // RegExps are compared by their source patterns and flags. + case '[object RegExp]': + return a.source == b.source && + a.global == b.global && + a.multiline == b.multiline && + a.ignoreCase == b.ignoreCase; + } + if (typeof a != 'object' || typeof b != 'object') return false; + // Assume equality for cyclic structures. The algorithm for detecting cyclic + // structures is adapted from ES 5.1 section 15.12.3, abstract operation `JO`. + var length = stack.length; + while (length--) { + // Linear search. Performance is inversely proportional to the number of + // unique nested structures. + if (stack[length] == a) return true; + } + // Add the first object to the stack of traversed objects. + stack.push(a); + var size = 0, result = true; + // Recursively compare objects and arrays. + if (className == '[object Array]') { + // Compare array lengths to determine if a deep comparison is necessary. + size = a.length; + result = size == b.length; + if (result) { + // Deep compare the contents, ignoring non-numeric properties. + while (size--) { + // Ensure commutative equality for sparse arrays. + if (!(result = size in a == size in b && eq(a[size], b[size], stack))) break; + } + } + } else { + // Objects with different constructors are not equivalent. + if ('constructor' in a != 'constructor' in b || a.constructor != b.constructor) return false; + // Deep compare objects. + for (var key in a) { + if (_.has(a, key)) { + // Count the expected number of properties. + size++; + // Deep compare each member. + if (!(result = _.has(b, key) && eq(a[key], b[key], stack))) break; + } + } + // Ensure that both objects contain the same number of properties. + if (result) { + for (key in b) { + if (_.has(b, key) && !(size--)) break; + } + result = !size; + } + } + // Remove the first object from the stack of traversed objects. + stack.pop(); + return result; + } + + // Perform a deep comparison to check if two objects are equal. + _.isEqual = function(a, b) { + return eq(a, b, []); + }; + + // Is a given array, string, or object empty? + // An "empty" object has no enumerable own-properties. + _.isEmpty = function(obj) { + if (_.isArray(obj) || _.isString(obj)) return obj.length === 0; + for (var key in obj) if (_.has(obj, key)) return false; + return true; + }; + + // Is a given value a DOM element? + _.isElement = function(obj) { + return !!(obj && obj.nodeType == 1); + }; + + // Is a given value an array? + // Delegates to ECMA5's native Array.isArray + _.isArray = nativeIsArray || function(obj) { + return toString.call(obj) == '[object Array]'; + }; + + // Is a given variable an object? + _.isObject = function(obj) { + return obj === Object(obj); + }; + + // Is a given variable an arguments object? + _.isArguments = function(obj) { + return toString.call(obj) == '[object Arguments]'; + }; + if (!_.isArguments(arguments)) { + _.isArguments = function(obj) { + return !!(obj && _.has(obj, 'callee')); + }; + } + + // Is a given value a function? + _.isFunction = function(obj) { + return toString.call(obj) == '[object Function]'; + }; + + // Is a given value a string? + _.isString = function(obj) { + return toString.call(obj) == '[object String]'; + }; + + // Is a given value a number? + _.isNumber = function(obj) { + return toString.call(obj) == '[object Number]'; + }; + + // Is the given value `NaN`? + _.isNaN = function(obj) { + // `NaN` is the only value for which `===` is not reflexive. + return obj !== obj; + }; + + // Is a given value a boolean? + _.isBoolean = function(obj) { + return obj === true || obj === false || toString.call(obj) == '[object Boolean]'; + }; + + // Is a given value a date? + _.isDate = function(obj) { + return toString.call(obj) == '[object Date]'; + }; + + // Is the given value a regular expression? + _.isRegExp = function(obj) { + return toString.call(obj) == '[object RegExp]'; + }; + + // Is a given value equal to null? + _.isNull = function(obj) { + return obj === null; + }; + + // Is a given variable undefined? + _.isUndefined = function(obj) { + return obj === void 0; + }; + + // Has own property? + _.has = function(obj, key) { + return hasOwnProperty.call(obj, key); + }; + + // Utility Functions + // ----------------- + + // Run Underscore.js in *noConflict* mode, returning the `_` variable to its + // previous owner. Returns a reference to the Underscore object. + _.noConflict = function() { + root._ = previousUnderscore; + return this; + }; + + // Keep the identity function around for default iterators. + _.identity = function(value) { + return value; + }; + + // Run a function **n** times. + _.times = function (n, iterator, context) { + for (var i = 0; i < n; i++) iterator.call(context, i); + }; + + // Escape a string for HTML interpolation. + _.escape = function(string) { + return (''+string).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"').replace(/'/g, ''').replace(/\//g,'/'); + }; + + // Add your own custom functions to the Underscore object, ensuring that + // they're correctly added to the OOP wrapper as well. + _.mixin = function(obj) { + each(_.functions(obj), function(name){ + addToWrapper(name, _[name] = obj[name]); + }); + }; + + // Generate a unique integer id (unique within the entire client session). + // Useful for temporary DOM ids. + var idCounter = 0; + _.uniqueId = function(prefix) { + var id = idCounter++; + return prefix ? prefix + id : id; + }; + + // By default, Underscore uses ERB-style template delimiters, change the + // following template settings to use alternative delimiters. + _.templateSettings = { + evaluate : /<%([\s\S]+?)%>/g, + interpolate : /<%=([\s\S]+?)%>/g, + escape : /<%-([\s\S]+?)%>/g + }; + + // When customizing `templateSettings`, if you don't want to define an + // interpolation, evaluation or escaping regex, we need one that is + // guaranteed not to match. + var noMatch = /.^/; + + // Within an interpolation, evaluation, or escaping, remove HTML escaping + // that had been previously added. + var unescape = function(code) { + return code.replace(/\\\\/g, '\\').replace(/\\'/g, "'"); + }; + + // JavaScript micro-templating, similar to John Resig's implementation. + // Underscore templating handles arbitrary delimiters, preserves whitespace, + // and correctly escapes quotes within interpolated code. + _.template = function(str, data) { + var c = _.templateSettings; + var tmpl = 'var __p=[],print=function(){__p.push.apply(__p,arguments);};' + + 'with(obj||{}){__p.push(\'' + + str.replace(/\\/g, '\\\\') + .replace(/'/g, "\\'") + .replace(c.escape || noMatch, function(match, code) { + return "',_.escape(" + unescape(code) + "),'"; + }) + .replace(c.interpolate || noMatch, function(match, code) { + return "'," + unescape(code) + ",'"; + }) + .replace(c.evaluate || noMatch, function(match, code) { + return "');" + unescape(code).replace(/[\r\n\t]/g, ' ') + ";__p.push('"; + }) + .replace(/\r/g, '\\r') + .replace(/\n/g, '\\n') + .replace(/\t/g, '\\t') + + "');}return __p.join('');"; + var func = new Function('obj', '_', tmpl); + if (data) return func(data, _); + return function(data) { + return func.call(this, data, _); + }; + }; + + // Add a "chain" function, which will delegate to the wrapper. + _.chain = function(obj) { + return _(obj).chain(); + }; + + // The OOP Wrapper + // --------------- + + // If Underscore is called as a function, it returns a wrapped object that + // can be used OO-style. This wrapper holds altered versions of all the + // underscore functions. Wrapped objects may be chained. + var wrapper = function(obj) { this._wrapped = obj; }; + + // Expose `wrapper.prototype` as `_.prototype` + _.prototype = wrapper.prototype; + + // Helper function to continue chaining intermediate results. + var result = function(obj, chain) { + return chain ? _(obj).chain() : obj; + }; + + // A method to easily add functions to the OOP wrapper. + var addToWrapper = function(name, func) { + wrapper.prototype[name] = function() { + var args = slice.call(arguments); + unshift.call(args, this._wrapped); + return result(func.apply(_, args), this._chain); + }; + }; + + // Add all of the Underscore functions to the wrapper object. + _.mixin(_); + + // Add all mutator Array functions to the wrapper. + each(['pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift'], function(name) { + var method = ArrayProto[name]; + wrapper.prototype[name] = function() { + var wrapped = this._wrapped; + method.apply(wrapped, arguments); + var length = wrapped.length; + if ((name == 'shift' || name == 'splice') && length === 0) delete wrapped[0]; + return result(wrapped, this._chain); + }; + }); + + // Add all accessor Array functions to the wrapper. + each(['concat', 'join', 'slice'], function(name) { + var method = ArrayProto[name]; + wrapper.prototype[name] = function() { + return result(method.apply(this._wrapped, arguments), this._chain); + }; + }); + + // Start chaining a wrapped Underscore object. + wrapper.prototype.chain = function() { + this._chain = true; + return this; + }; + + // Extracts the result from a wrapped and chained object. + wrapper.prototype.value = function() { + return this._wrapped; + }; + +}).call(this); +(function() { + + + +}).call(this); +(function() { + + Attentive.Presentation.setup('#slides'); + +}).call(this); diff --git a/_site/assets/balancing-cat.jpg b/_site/assets/balancing-cat.jpg new file mode 100644 index 0000000..cf6cf15 Binary files /dev/null and b/_site/assets/balancing-cat.jpg differ diff --git a/_site/assets/beer-cat.jpg b/_site/assets/beer-cat.jpg new file mode 100644 index 0000000..30e1623 Binary files /dev/null and b/_site/assets/beer-cat.jpg differ diff --git a/_site/assets/cat-carrier.jpg b/_site/assets/cat-carrier.jpg new file mode 100644 index 0000000..9a5351e Binary files /dev/null and b/_site/assets/cat-carrier.jpg differ diff --git a/_site/assets/cat-meow.jpg b/_site/assets/cat-meow.jpg new file mode 100644 index 0000000..ea61bf5 Binary files /dev/null and b/_site/assets/cat-meow.jpg differ diff --git a/_site/assets/checklist.png b/_site/assets/checklist.png new file mode 100644 index 0000000..12db52f Binary files /dev/null and b/_site/assets/checklist.png differ diff --git a/_site/assets/dark-side-nyan-cat.jpg b/_site/assets/dark-side-nyan-cat.jpg new file mode 100644 index 0000000..2c16175 Binary files /dev/null and b/_site/assets/dark-side-nyan-cat.jpg differ diff --git a/_site/assets/flying-cat.jpg b/_site/assets/flying-cat.jpg new file mode 100644 index 0000000..929975e Binary files /dev/null and b/_site/assets/flying-cat.jpg differ diff --git a/_site/assets/hungry-cat.jpg b/_site/assets/hungry-cat.jpg new file mode 100644 index 0000000..09a2160 Binary files /dev/null and b/_site/assets/hungry-cat.jpg differ diff --git a/_site/assets/john-drinking-tea.png b/_site/assets/john-drinking-tea.png new file mode 100644 index 0000000..bc25166 Binary files /dev/null and b/_site/assets/john-drinking-tea.png differ diff --git a/_site/assets/relevant-cat.jpg b/_site/assets/relevant-cat.jpg new file mode 100644 index 0000000..6184c7e Binary files /dev/null and b/_site/assets/relevant-cat.jpg differ diff --git a/_site/assets/spaghetti.jpg b/_site/assets/spaghetti.jpg new file mode 100644 index 0000000..9ef5e45 Binary files /dev/null and b/_site/assets/spaghetti.jpg differ diff --git a/_site/assets/spy-cat.jpg b/_site/assets/spy-cat.jpg new file mode 100644 index 0000000..f52f252 Binary files /dev/null and b/_site/assets/spy-cat.jpg differ diff --git a/_site/assets/synergize.gif b/_site/assets/synergize.gif new file mode 100644 index 0000000..35d42e8 Binary files /dev/null and b/_site/assets/synergize.gif differ diff --git a/_site/assets/synergize.jpg b/_site/assets/synergize.jpg new file mode 100644 index 0000000..36e8b51 Binary files /dev/null and b/_site/assets/synergize.jpg differ diff --git a/_site/assets/wet-cat.jpg b/_site/assets/wet-cat.jpg new file mode 100644 index 0000000..466f69b Binary files /dev/null and b/_site/assets/wet-cat.jpg differ diff --git a/_site/index.html b/_site/index.html new file mode 100644 index 0000000..0ca4b67 --- /dev/null +++ b/_site/index.html @@ -0,0 +1,1605 @@ + + + +Tea Time: A Beginner's Guide to Jasmine + + + + + + + +
+
+
+ +
+

Tea Time

+

A Beginner's Guide to JavaScript Testing using Jasmine

+

By John Bintz

+
+
+ + + + +
+

Automated testing is important

+ +
+

+ +
+

Fortunately, we're beyond that nowadays

+ +
+
require 'spec_helper'
+
+describe MyCoolWebsite do
+  let(:website) { described_class.new }
+
+  describe '#cool_method' do
+    subject { website.cool_method }
+
+    let(:oh_yeah) { [ double_cool ] }
+    let(:double_cool) { 'double cool' }
+
+    before do
+      website.stubs(:whoa_cool).returns(oh_yeah)
+    end
+
+    it { should == double_cool }
+  end
+end
+
+
+ + + + +
+

But there's more to web apps than Ruby nowadays...

+ +
+
<img src="normal.gif"
+   onmouseover="this.src='hover.gif'"
+   onmouseout="this.src='normal.gif'" />
+
+
+ + + + +
+
<script type="text/javascript">
+function showMyCoolTitle(title, length) {
+  if (length == null) { length = 0; }
+
+  if (length < title.length) {
+    document.title = title.substr(0, length);
+    length++;
+
+    setTimeout(function() { showMyCoolTitle(title, length); }, 75);
+  }
+}
+
+window.onload = function() { showMyCoolTitle("My cool website! Whoaaaaa!"); }
+</script>
+
+
+ + + + +
+

jQuery

+ +
+

Backbone

+ +
+

Sprockets and RequireJS

+ +
+

Automated testing is important

+ +
+
require 'spec_helper'
+
+describe MyCoolWebsite do
+  let(:website) { described_class.new }
+
+  describe '#cool_method' do
+    subject { website.cool_method }
+
+    let(:oh_yeah) { [ double_cool ] }
+    let(:double_cool) { 'double cool' }
+
+    before do
+      website.stubs(:whoa_cool).returns(oh_yeah)
+    end
+
+    it { should == double_cool }
+  end
+end
+
+
+ + + + +
+
describe 'MyCoolWebsiteView', ->
+  website = null
+
+  beforeEach ->
+    website = new MyCoolWebsiteView()
+
+  describe '#coolMethod', ->
+    doubleCool = 'double cool'
+    ohYeah = [ doubleCool ]
+
+    beforeEach ->
+      website.whoaCool = -> ohYeah
+
+    it 'should be double cool', ->
+      expect(website.coolMethod()).toEqual(doubleCool)
+
+
+ + + + +
+

Jasmine

+ +
+

BDD unit testing framework for JavaScript

+ +
+

Platform independent

+ +
+

Easily extended

+ +
+

Very easy to learn!

+ +
+

Follow along!

+ +
+

No need to install anything right now

+ +
+

Specs on the left

+ +
+

Code under test on the right

+ +
+

Write code in CoffeeScript

+ +
+

Ready?

+ +
+

Let's go!

+ +
+

describe

+ +
+

Describes a thing or a behavior of a thing

+ +
+

Let's describe...

+ +
+

+ +
+
describe 'Cat', ->
+  # cat behavior descriptions go here
+
+
+ + + + +
+

Something that cats do...

+ +
+

+ +
+
describe 'Cat', ->
+  describe '#meow', ->
+    # description of the meow behavior goes here
+
+
+ + + + +
+

John behavior #1

+ +

Use Ruby-style indicators for instance- and class-level methods, even in Jasmine

+ +
describe 'John', ->
+  describe 'spec definitions', ->
+    it 'should look like you did it in RSpec', ->
+
+
+ + + + +
+

Describe how we expect a cat to meow

+ +
+

it

+ +
+
describe 'Cat', ->
+  describe '#meow', ->
+    it 'should meow correctly', ->
+      # expectation of a cat meowing
+
+
+ + + + +
+

We have the description...

+ +
+

Now let's add the expectations!

+ +
+

expect

+ +
+

What should we get as an output?

+ +
+
describe 'Cat', ->
+  describe '#meow', ->
+    it 'should meow correctly', ->
+      expect(cat.meow()).toEqual('meow')
+
+
+ + + + +
+

Wait, we need a cat.

+ +
+
describe 'Cat', ->
+  describe '#meow', ->
+    it 'should meow correctly', ->
+      cat = new Cat()
+
+      expect(cat.meow()).toEqual('meow')
+
+
+ + + + +
+
# code-under-test
+
+class this.Cat
+  meow: ->
+
+
+ + + + +
+
// safety wrapper to prevent global pollution
+(function() {
+  // ...but we want to pollute the Cat class
+  this.Cat = (function() {
+    function Cat() {}
+    Cat.prototype.meow = function() {};
+    return Cat;
+  })();
+})(this) // this is window in a browser
+
+
+ + + + +
+

Run it!

+ +
+
1 spec, 1 failure
+
+Expected undefined to equal 'meow'.
+
+
+ + + + +
+

Make it meow!

+ +
+
class this.Cat
+  meow: -> "meow"
+
+
+ + + + +
+
1 spec, 0 failures
+
+
+ + + + +
+

Here's what you should have meow...

+ +
+
# spec
+
+describe 'Cat', ->
+  describe '#meow', ->
+    it 'should meow correctly', ->
+      expect(cat.meow()).toEqual('meow')
+
+
+ + + + +
+
# code-under-test
+
+class this.Cat
+  meow: -> "meow"
+
+
+ + +
+

What if the cat meows differently based on certain states?

+ +
+

+ +
+

+ +
+

Nested describe

+ +
+
describe 'Cat', ->
+  describe '#meow', ->
+    describe 'hungry', ->
+      # Cat#meow expectation for when the cat is hungry
+
+    describe 'going to the vet', ->
+      # Cat#meow expectation for when the cat knows it's vet time
+
+
+ + + + +
+
describe 'Cat', ->
+  describe '#meow', ->
+    describe 'hungry', ->
+      it 'should be a mournful meow', ->
+        cat = new Cat()
+        cat.state = -> Cat.HUNGRY   # ...just like cat.stubs(:state)
+
+        expect(cat.meow()).toEqual("meeeyaow")
+
+    describe 'going to the vet', ->
+      it 'should be an evil meow', ->
+        cat = new Cat()
+        cat.state = -> Cat.VET_PSYCHIC   # ...just like the one above
+
+        expect(cat.meow()).toEqual("raowwww")
+
+
+ + + + +
+

+ +
+
cat = new Cat()
+
+
+ + + + +
+
before do
+  @cat = Cat.new
+end
+
+it 'should be a mournful meow' do
+  @cat.stubs(:state).returns(Cat::HUNGRY)
+
+  @cat.meow.should == "meeyaow"
+end
+
+
+ + + + +
+
before -> it -> after
+
+
+ + + + +
+
before do
+  @instance_variable = "yes"
+end
+
+it "should be in the same context as the before block" do
+  @instance_variable.should == "yes"
+end
+
+
+ + + + +
+
beforeEach -> it -> afterEach
+
+
+ + + + +
+
beforeEach ->
+  @instanceVariable = "yes"
+
+it "should be in the same context", ->
+  expect(@instanceVariable).toEqual("yes")
+
+
+ + + + +
+
describe 'Cat', ->
+  describe '#meow', ->
+    beforeEach ->
+      @cat = new Cat()
+
+    describe 'hungry', ->
+      it 'should be a mournful meow', ->
+        @cat.state = -> Cat.HUNGRY
+
+        expect(@cat.meow()).toEqual("meeeyaow")
+
+    describe 'going to the vet', ->
+      it 'should be an evil meow', ->
+        @cat.state = -> Cat.VET_PSYCHIC
+
+        expect(@cat.meow()).toEqual("raowwww")
+
+
+ + + + +
+

A little semantics game...

+ +
+
describe 'Cat', ->
+  describe '#meow', ->
+    describe 'hungry', ->
+      # cat codes
+
+    describe 'going to the vet', ->
+      # moar cat codes
+
+
+ + + + +
+

This works, but it can be clearer

+ +
+
describe Cat do
+  describe '#meow' do
+    describe 'hungry' do
+      # cat codes
+    end
+
+    describe 'going to the vet' do
+      # moar cat codes
+    end
+  end
+end
+
+
+ + + + +
+

context

+ +
+

Description of different states for a test

+ +
+
alias :context :describe
+
+
+ + + + +
+
describe Cat do
+  let(:cat) { described_class.new }
+
+  # save describe for things or behaviors...
+  describe '#meow' do
+    subject { cat.meow }
+
+    # use context to describe states
+    context 'hungry' do
+      # cat codes
+    end
+
+    context 'going to the vet' do
+      # moar cat codes
+    end
+  end
+end
+
+
+ + + + +
+

Jasmine doesn't have context

+ +
+

However...

+ +
+
this.context = this.describe
+
+
+ + + + +
+
this.context = this.describe
+
+describe 'Cat', ->
+  describe '#meow', ->
+    context 'hungry', ->
+      # cat codes
+
+    context 'going to the vet', ->
+      # moar cat codes
+
+
+ + + + +
+
this.context = this.describe
+
+describe 'Cat', ->
+  describe '#meow', ->
+    beforeEach ->
+      @cat = new Cat()
+
+    context 'hungry', ->
+      it 'should be a mournful meow', ->
+        @cat.state = -> Cat.HUNGRY
+
+        expect(@cat.meow()).toEqual("meeeyaow")
+
+    context 'going to the vet', ->
+      it 'should be an evil meow', ->
+        @cat.state = -> Cat.VET_PSYCHIC
+
+        expect(@cat.meow()).toEqual("raowwww")
+
+
+ + + + +
+
class this.Cat
+  @HUNGRY = 'hungry'
+  @VET_PSYCHIC = 'vet psychic'
+
+  meow: ->
+    switch this.state()
+      when Cat.HUNGRY
+        "meeeyaow"
+      when Cat.VET_PSYCHIC
+        "raowwww"
+
+
+ + + + +
+
2 spec, 0 failures
+
+
+ + + + +
+

Matchers

+ +
+
cat.meow.should == "meow"
+cat.should be_a_kind_of(Cat)
+cat.should_not be_hungry #=> cat.hungry?.should == false
+
+
+ + + + +
+
expect(cat.meow()).toEqual("meow")
+expect(cat.prototype).toEqual(Cat.prototype)
+expect(cat.isHungry()).not.toBeTruthy()
+
+
+ + + + +
+

Lots of built in matchers

+ +
toEqual(object)
+toBeTruthy()
+toBeFalsy()
+toBeGreaterThan()
+toBeLessThan()
+toBeUndefined()
+toContain()
+toMatch()
+
+
+ + + + +
+
expect(cat.isHungry()).not.toBeTruthy()
+
+
+ + + + +
+

Create your own matchers!

+ +
+
MyMatchers =
+  toBeHungry: ->
+    return @actual.isHungry() == true
+
+beforeEach ->
+  this.addMatchers(MyMatchers)
+
+describe 'Cat', ->
+  beforeEach ->
+    @cat = new Cat()
+
+  it 'should not be hungry', ->
+    expect(@cat).not.toBeHungry()
+
+
+ + + + +
+
describe
+it
+expect
+toSomething()
+beforeEach
+afterEach
+
+
+ + + + +
+

Jasmine == unit testing

+ +
+

+ +
+

No, this isn't a talk about integration testing

+ +
+

Testing the right things in your JavaScript unit tests

+ +
+

+ +
+

John behavior #2

+ +

Mock, stub, and spy on anything that should be handled in an integration test

+ +
describe 'John', ->
+  describe 'spec definitions', ->
+    it 'should keep unit tests as focused as possible', ->
+
+
+ + + + +
+

+ +
+
Feature: Cat Behaviors
+  Scenario: Hungry cats meow a particular way
+    Given I have a cat
+      And the cat is hungry
+    When the cat meows
+    Then the meow should sound like "meeyaow"
+
+
+ + + + +
+
class this.Cat
+  @FOOD_THRESHOLD = 20
+  @HUNGRY = 'hungry'
+
+  constructor: (@foodLevel = 30) ->
+
+  meow: ->
+    switch this.state()
+      when Cat.HUNGRY
+        "meeyaow"
+
+  state: ->
+    if @foodLevel < Cat.FOOD_THRESHOLD
+      Cat.HUNGRY
+
+
+ + + + +
+
describe 'Cat', ->
+  describe '#meow', ->
+    context 'hungry', ->
+      it 'should be a mournful meow', ->
+        cat = new Cat()
+        cat.foodLevel = 15
+
+        expect(cat.meow()).toEqual("meeeyaow")
+
+
+ + + + +
+

A perfectly cromulent test

+ +
+
class this.Cat
+  meow: ->
+    switch this.state() # <= dependent code executed
+      when Cat.HUNGRY
+        "meeyaow"
+
+
+ + + + +
+

Why make your unit tests fragile?

+ +
+
cat.foodLevel = 15 # do we care about food level in this test?
+                   # all we care about is that the cat is hungry
+
+
+ + + + +
+
describe 'Cat', ->
+  describe '#meow', ->
+    describe 'hungry', ->
+      it 'should be a mournful meow', ->
+        cat = new Cat()
+        cat.state = -> Cat.HUNGRY # <= we don't care how state works,
+                                  #    we just want a hungry cat
+
+        expect(cat.meow()).toEqual("meeeyaow")
+
+
+ + + + +
+

Instance Stubs in JavaScript

+ +

Just replace the method on the instance

+ +
class this.Cat
+  state: ->
+    # cat codes
+
+cat = new Cat()
+cat.state = -> "whatever"
+
+
+ + + + +
+

Stubs just return something when called

+ +
+

Mocks expect to be called

+ +
+

Test fails if all mocks are not called

+ +
+

Jasmine blurs the line a little

+ +
+

+ +
+

Spies work like mocks, but with additional abilities

+ +
+

+ +
+
class this.Cat
+  vocalProcessor: (speech) =>
+    if this.isAirborne()
+      this.modifyForAirborne(speech)
+    else
+      this.modifyForGround(speech)
+
+
+ + + + +
+
describe 'Cat#vocalProcessor', ->
+  speech = "speech"
+
+  beforeEach ->
+    @cat = new Cat()
+
+  context 'airborne', ->
+    beforeEach ->
+      spyOn(@cat, 'modifyForAirborne')
+      @cat.isAirborne = -> true
+
+    it 'should be modified for flight', ->
+      @cat.vocalProcessor(speech)
+      expect(@cat.modifyForAirborne).toHaveBeenCalledWith(speech)
+
+
+ + + + +
+

spyOn replaces a method on an instance with a spy method

+ +
spyOn(@cat, 'modifyForAirborne')
+
+
+ + + + +
+

Can return a value, run code, run the original code, or just wait to be called

+ +
+

Two basic ways to make sure a spy is called

+ +
+

toHaveBeenCalledWith()

+ +

Called least once with the given parameters

+ +
expect(@cat.modifyForAirborne).toHaveBeenCalledWith(speech)
+
+
+ + + + +
+

toHaveBeenCalled()

+ +

Just called, no parameter check

+ +
expect(@cat.modifyForAirborne).toHaveBeenCalled()
+
+
+ + + + +
+

Instance Mocks/Spies in JavaScript

+ +

Use spyOn/toHaveBeenCalled matchers

+ +
class this.Cat
+  state: ->
+    # cat codes
+
+cat = new Cat()
+spyOn(cat, 'state')
+expect(cat.state).toHaveBeenCalled()
+
+
+ + + + +
+

spyOn works great with class-level stubs and mocks, too

+ +
+
class this.Cat
+  @generateFurColor: (base) ->
+    # magicks to make a fur color given a base
+
+  regrowFur: (damagedHairs) ->
+    for follicle in damagedHairs
+      follicle.regrow(Cat.generateFurColor(this.baseColor))
+
+
+ + + + +
+
Cat.generateFurColor = ->
+  "whoops i nuked this method for every other test"
+
+
+ + + + +
+
describe 'Cat#regrowFur', ->
+  color = 'color'
+
+  beforeEach ->
+    @cat = new Cat()
+    @follicle =
+      regrow: ->
+
+    @follicles = [ follicle ]
+
+    spyOn(Cat, 'generateFurColor').andReturn(color)
+    #           ^^^ original is replaced when done
+    spyOn(@follicle, 'regrow')
+
+  it 'should regrow', ->
+    @cat.regrowFur(@follicles)
+
+    expect(@follicle.regrow).toHaveBeenCalledWith(color)
+
+
+ + + + +
+

Class Stubs in JavaScript

+ +

Use spyOn to generate stubs so that the original code is replaced after the test

+ +
class this.Cat
+  @injectPsychicPowers: (cat) ->
+    # cat codes
+
+spyOn(Cat, 'injectPsychicPowers').andReturn(psychicCat)
+
+
+ + + + +
+

John behavior #3

+ +

If you have too many mocks/stubs/contexts, your code is too complex

+ +
describe 'John', ->
+  describe 'spec definitions', ->
+    it 'should obey the Law of Demeter as much as possible', ->
+    it 'should not smell too funny', ->
+
+
+ + + + +
+
describe 'Cat#fetch', ->
+  object = null
+
+  context 'a mouse', ->
+    beforeEach ->
+      object = new Mouse()
+
+    context 'fast mouse', ->
+      it 'should wear down the mouse', ->
+        # who
+
+    context 'slow mouse', ->
+      it 'should deliver a present to you', ->
+        # cares
+
+  context 'a ball', ->
+    beforeEach ->
+      object = new Ball()
+
+    context 'ball is bouncing', ->
+      it 'should cause the cat to leap', ->
+        # this
+
+    context 'ball is rolling', ->
+      it 'should cause the cat to slide on the floor', ->
+        # test
+
+  context 'a red dot', ->
+    laser = null
+
+    beforeEach ->
+      laser = new Laser()
+
+    context 'laser out of batteries', ->
+      it 'should not activate', ->
+        # is
+
+    context 'laser functioning', ->
+      it 'should activate, driving the cat insane', ->
+        # huge and unmaintainable and silly
+
+
+ + + + +
+

Sometimes you just need a big blob of unit tests

+ +
+
# fast and focused!
+
+describe 'Cat#respondsTo', ->
+  beforeEach ->
+    @cat = new Cat()
+
+  context 'successes', ->
+    it 'should respond', ->
+      for request in [ 'kitty kitty', 'pookums', 'hisshead' ]
+        expect(@cat.respondsTo(request)).toBeTruthy()
+
+
+ + + + +
+
# slow and synergistic!
+
+Scenario Outline: Successful responsiveness
+  Given I have a cat
+  When I call it with "<request>"
+  Then the cat should respond
+
+  Examples:
+    | request     |
+    | kitty kitty |
+    | pookums     |
+    | hisshead    |
+
+
+ + + + +
+

+ +
+

Find what works best for you and stick with it

+ +
+

...until you get sick of it, of course...

+ +
+

Using it in your project

+ +
+

Starts a Rack server for running Jasmine against your code

+ +
+

Really easy to plug into an existing Rails project

+ +
+

Want to make that run fast?

+ +
+

Use PhantomJS or jasmine-headless-webkit

+ +
+

Fast code running in a real browser

+ +
+

Evergreen

+ +
+

Jasminerice

+ +
+

Node.js

+ +
+

Pick your favorite!

+ +
+

Some miscellaneous hints and tips

+ +
+

Testing jQuery

+ +
+

Mocking and stubbing $.fn calls

+ +
+
this.containerWaiter = ->
+  $('#container').addClass('wait').append('<div class="waiting" />')
+
+
+ + + + +
+
$.fn.makeWait = ->
+  $(this).addClass('wait').append('<div class="waiting" />')
+  this
+
+
+ + + + +
+
this.containerWaiter = ->
+  $('#container').makeWait()
+
+
+ + + + +
+

jquery-jasmine

+ +
+
describe 'container', ->
+  beforeEach ->
+    setFixtures('<div id="container" />')
+
+  it 'should make it wait', ->
+    containerWaiter()
+    expect($('#container')).toHaveClass('wait')
+    expect($('#container')).toContain('div.waiting')
+
+
+ + + + +
+

+ +
+

+ +
+
describe '$.fn.makeWait', ->
+  it 'should make wait', ->
+    $div = $('<div />')
+    $div.makeWait()
+
+    expect($div).toHaveClass('wait')
+    expect($div).toContain('div.waiting')
+
+
+ + + + +
+
describe 'container', ->
+  beforeEach ->
+    setFixtures('<div id="container" />')
+    spyOn($.fn, 'makeWait')
+
+  it 'should make it wait', ->
+    containerWaiter()
+    expect($.fn.makeWait).toHaveBeenCalled()
+
+
+ + + + +
+

No longer testing jQuery, just testing for our code

+ +
+

Animations and other time-dependent things

+ +
+
class Cat
+  constructor: ->
+    @mood = "happy"
+
+  pet: ->
+    setTimeout(
+      -> @mood = "angry"
+      , 500
+    )
+
+
+ + + + +
+

Do you really need to test the setTimeout?

+ +
+
class Cat
+  constructor: ->
+    @mood = "happy"
+
+  pet: -> setTimeout(@makeAngry, 500)
+
+  makeAngry: => @mood = "angry"
+
+
+ + + + +
+

Use Jasmine's waitsFor and runs

+ +
+
describe 'cat moods', ->
+  it 'should change moods', ->
+    cat = new Cat()
+
+    # we want to know the cat's current mood
+    currentMood = cat.mood
+
+    #  start petting the cat
+    runs -> cat.pet()
+
+    # wait one second for the cat's mood to change
+    waitsFor(
+      ->
+        cat.mood != currentMood
+      , "Cat changed its mood",
+      1000
+    )
+
+    # expect the inevitable
+    runs ->
+      expect(cat.mood).toEqual('angry')
+
+
+ + + + +
+

Underscore.js mixins

+ +

and other prototype mixin-style extensions

+ +
+
CatLike =
+  catify: (name) ->
+    "meow meow #{name}"
+
+# mix in to the Underscore object
+_.mixin(CatLike)
+
+# use it
+_.catify("john") # => "meow meow john"
+
+
+ + + + +
+
CatLike =
+  catify: (name) -> "meow meow #{name}"
+
+class Cat
+  hiss: -> "hiss"
+
+# like Ruby include, add code to instances
+for method, code of CatLike
+  Cat.prototype[method] = code
+
+cat = new Cat()
+cat.catify("john") # => "meow meow #{name}"
+
+
+ + + + +
+
CatLike =
+  catify: (name) -> "meow meow #{name}"
+
+class Cat
+  hiss: -> "hiss"
+
+# like Ruby extend, add code to class
+for method, code of CatLike
+  Cat[method] = code
+
+Cat.catify("john") # => "meow meow john"
+
+
+ + + + +
+
describe '_.catify', ->
+  it 'should catify', ->
+    expect(_.catify("hiss")).toEqual("meow meow hiss")
+
+
+ + + + +
+

Eliminate the Underscore.js dependency

+ +
+
describe 'CatLike', ->
+  beforeEach ->
+    @helper = {}
+
+    for method, code of CatLike
+      @helper[method] = code
+
+  describe '#catify', ->
+    it 'should catify', ->
+      expect(@helper.catify("hiss")).toEqual("meow meow hiss")
+
+
+ + + + +
+

So that's pretty much it.

+ +
+

Basic parts of Jasmine unit tests

+ +
describe
+it
+expect
+toSomething()
+beforeEach
+afterEach
+
+
+ + + + +
+

Mocking and stubbing

+ +
direct method replacement
+spyOn()
+toHaveBeenCalled()
+toHaveBeenCalledWith()
+
+
+ + + + +
+

Running Jasmine in your project

+ +
+

Hints and tips for JavaScript testing

+ +
waitsFor()
+runs()
+
+
+ + + + +
+

Any questions?

+ +
+

Thank you!

+ +

@johnbintz

+ +

GitHub

+ +
+ +
+ + +