diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..3080ecd --- /dev/null +++ b/LICENSE @@ -0,0 +1,6 @@ +session.js 0.4.1 +(c) 2012 Iain, CodeJoust +session.js is freely distributable under the MIT license. +Portions of session.js are inspired or borrowed from Underscore.js, and quirksmode.org demo javascript. +This version uses google's jsapi library for location services. +For details, see: https://github.com/codejoust/session.js diff --git a/README.md b/README.md index a47c8a5..5408329 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ To use: include the file session.js, then access the visitor object. It uses the google javascript loader to get location data. For async loading, use the window.session_loaded callback. -[Live Demo](http://go.iain.in/sessionjslivedemo01) +[Live API Demo](http://go.iain.in/sessionjslivedemo01) | [Example Usage Page](http://go.iain.in/sessionjslivedemo02) Configurable options are below. @@ -16,13 +16,42 @@ Configurable options are below. Include `session.js` in the head or footer. #### Download/Linking: +Recommended: +[Api v0.4 Uncompressed](http://codejoust.github.com/session.js/session-0.4.js) + +Quick Example: + +```html + + +``` + +#### Other Source Options: +Lock version to v0.4 (current stable): +[uncompressed](http://codejoust.github.com/session.js/session-0.4.js), +[compressed](http://codejoust.github.com/session.js/session-0.4.min.js). + Edge: -[uncompressed](https://raw.github.com/codejoust/session.js/master/session.js), -[compressed](https://raw.github.com/codejoust/session.js/master/session.min.js) -Lock version to v0.3 (last stable version): -[uncompressed](https://raw.github.com/codejoust/session.js/v0.4/session.js) [compressed](https://raw.github.com/codejoust/session.js/v0.4/session.min.js) +[uncompressed](http://codejoust.github.com/session.js/session.js), +[compressed](http://codejoust.github.com/session.js/session.min.js) + -If used in the footer (before the `` tag), you can use the `window.session_loaded = function(session){}` callback). +If used in the footer (before the `` tag), you can use the `window.session = {start: function(sess){ /* loaded session data */ }}` callback, before including the session.js. This is recommended when using session.js with location data. ### API demo dump of `window.session`: @@ -35,16 +64,16 @@ If used in the footer (before the `` tag), you can use the `window.sessio }, "current_session": { "visits": 1, - "start": 1325991619228, - "last_visit": 1325991619228, - "url": "http://localhost:8000/demo.html", - "path": "/demo.html", - "referrer": "http://localhost:8000/", + "start": 1326170811877, + "last_visit": 1326170811877, + "url": "http://codejoust.github.com/session.js/", + "path": "/session.js/", + "referrer": "", "referrer_info": { - "host": "localhost:8000", - "path": "/", + "host": "codejoust.github.com", + "path": "/session.js/", "protocol": "http:", - "port": "8000", + "port": 80, "search": "", "query": {} }, @@ -54,19 +83,19 @@ If used in the footer (before the `` tag), you can use the `window.sessio } }, "original_session": { - "visits": 41, - "start": 1325990490065, - "last_visit": 1325991619229, - "url": "http://localhost:8000/demo.html", - "path": "/demo.html", + "visits": 29, + "start": 1326032481755, + "last_visit": 1326170811879, + "url": "http://codejoust.github.com/session.js/", + "path": "/session.js/", "referrer": "", "referrer_info": { - "host": "localhost:8000", - "path": "/demo.html", + "host": "codejoust.github.com", + "path": "/session.js/", "protocol": "http:", - "port": "8000", + "port": 80, "search": "", - "query": "" + "query": {} }, "search": { "engine": null, @@ -84,14 +113,18 @@ If used in the footer (before the `` tag), you can use the `window.sessio "java": true, "quicktime": true }, + "time": { + "tz_offset": -5, + "observes_dst": true + }, "device": { "screen": { "width": 1280, "height": 1024 }, "viewport": { - "width": 1063, - "height": 860 + "width": 1230, + "height": 952 }, "is_tablet": false, "is_phone": false, @@ -117,9 +150,13 @@ Default options are shown below. ipinfodb.com location [demo](http://codejoust.github.com/session.js/ipinfodb_demo.html). +Synchronous information (everything but location not cached in a cookie), +is available immediately after including session.js. + ```js window.session = { options: { + // Default Settings Example // Use the HTML5 Geolocation API // this ONLY returns lat & long, no city/address use_html5_location: false, @@ -129,21 +166,22 @@ window.session = { // Leaving true allows for fallback for both // the HTML5 location and the IPInfoDB gapi_location: true, - // Name of the location cookie + // Name of the location cookie (set blank to disable cookie) // - WARNING: different providers use the same cookie // - if switching providers, remember to use another cookie or provide checks for old cookies location_cookie: "location", // Location cookie expiration in hours - location_cookie_timeout: 2, + location_cookie_timeout: 5, // Session expiration in days session_timeout: 32, - // Session cookie name + // Session cookie name (set blank to disable cookie) session_cookie: "first_session" + }; }, - start: { - // Session location loaded. + start: function(session){ + // Session location loaded into window.session and first argument. } } ``` - \ No newline at end of file + diff --git a/debug.html b/debug.html new file mode 100644 index 0000000..928e9d4 --- /dev/null +++ b/debug.html @@ -0,0 +1,240 @@ + + + + Session.js example usage + + + + + + + +

Session.js Debug / Testing Page

+

Script Output:

+
+    loading...
+  
+ + + + + +

+
+
+

Session.JS on GitHub by Iain

+ + + diff --git a/demo.html b/demo.html index d7f1118..fabe432 100644 --- a/demo.html +++ b/demo.html @@ -1,153 +1,213 @@ - - Demo for Session.js - + + Demo for Session.js + + -

Session.js Demo Page

-

Demo output:

-
+

Session.js Demo Page

+

Demo output:

+
     loading...
   
- -

Session.JS on GitHub by Iain

- +

Session.JS on GitHub by Iain

+ - + + diff --git a/package.json b/package.json new file mode 100644 index 0000000..9158349 --- /dev/null +++ b/package.json @@ -0,0 +1,25 @@ +{ + "name": "session.js", + "version": "0.4.1", + "description": "Gives information about the current session.", + "main": "session.js", + "directories": { + "test": "test" + }, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/codejoust/session.js.git" + }, + "keywords": [ + "session" + ], + "author": "codejoust", + "license": "MIT", + "bugs": { + "url": "https://github.com/codejoust/session.js/issues" + }, + "homepage": "https://github.com/codejoust/session.js#readme" +} diff --git a/session.js b/session.js index 5229b2b..2253ca9 100644 --- a/session.js +++ b/session.js @@ -6,397 +6,590 @@ * This version uses google's jsapi library for location services. * For details, see: https://github.com/codejoust/session.js */ -(function(win, doc){ - // Changing the API Version invalidates olde cookies with previous api version tags. - var API_VERSION = 0.4; - - // Settings: defaults - var options = { - // Use the HTML5 Geolocation API - // this ONLY returns lat & long, no city/address - use_html5_location: false, - // Attempts to use IPInfoDB if provided a valid key - // Get a key at http://ipinfodb.com/register.php - ipinfodb_key: false, - // Leaving true allows for fallback for both - // the HTML5 location and the IPInfoDB - gapi_location: true, - // Name of the location cookie - // - WARNING: different providers use the same cookie - // - if switching providers, remember to use another cookie or provide checks for old cookies - location_cookie: "location", - // Location cookie expiration in hours - location_cookie_timeout: 2, - // Session expiration in days - session_timeout: 32, - // Session cookie name - session_cookie: "first_session" - }; - - // Session object - var SessionRunner = function(){ - // Merge options - if(win.session && win.session.options) { - for (option in win.session.options){ - options[option] = win.session.options[option]; } - } - // Modules to run - this.modules = { - api_version: API_VERSION, - locale: modules.locale(), - current_session: modules.session(), - original_session: modules.session( - options.session_cookie, - options.session_timeout * 24 * 60 * 60 * 1000), - browser: modules.browser(), - plugins: modules.plugins(), - device: modules.device() - }; - // Location switch - if (options.use_html5_location){ - this.modules.location = modules.html5_location(); - } else if (options.ipinfodb_key){ - this.modules.location = modules.ipinfodb_location(options.ipinfodb_key); - } else if (options.gapi_location){ - this.modules.location = modules.gapi_location(); - } - // Cache win.session.start - if (win.session && win.session.start){ - var start = win.session.start; - } - // Set up checking, if all modules are ready - var asynchs = 0, module, result, self = this, - check_asynch = function(){ - if (asynchs === 0){ - // Map over results - win.session = self.modules; - // Run start calback - if (start){ start(self.modules); } - } +let session_fetch = (function (win, doc, nav) { + 'use strict'; + // Changing the API Version invalidates old cookies with previous api version tags. + let API_VERSION = 0.4; + // Settings: defaults + let options = { + // Use the HTML5 Geolocation API + // this ONLY returns lat & long, no city/address + use_html5_location: false, + // Attempts to use IPInfoDB if provided a valid key + // Get a key at https://ipinfodb.com/register.php + ipinfodb_key: false, + // Leaving true allows for fallback for both + // the HTML5 location and the IPInfoDB + gapi_location: true, + // Name of the location cookie (set blank to disable cookie) + // - WARNING: different providers use the same cookie + // - if switching providers, remember to use another cookie or provide checks for old cookies + location_cookie: 'location', + // Location cookie expiration in hours + location_cookie_timeout: 5, + // Session expiration in days + session_timeout: 32, + // Session cookie name (set blank to disable cookie) + session_cookie: 'session-js', + get_object: null, set_object: null, // used for cookie session adaptors + // if null, will be reset to use cookies by default. + // Tracker ID: initialize with a random string so that repeated visits are + // easily tracked + tracker_id: null, + // Set to true to record extra client data + extra: false, }; - // Run asynchronous methods - for (var name in this.modules){ - module = self.modules[name]; - if(typeof module === "function"){ - //try { - asynchs++; - module(function(data){ - self.modules[name] = data; - asynchs--; - check_asynch(); - }); - /*} catch(err){ - if (win.console && typeof(console.log) === "function"){ - console.log(err); } - }*/ - } else { - self.modules[name] = module; - } - } - check_asynch(); - }; - - - // Browser (and OS) detection - var browser = { - detect: function(){ - return { - browser: this.search(this.data.browser), - version: this.search(navigator.userAgent) || this.search(navigator.appVersion), - os: this.search(this.data.os) - } }, - search: function(data) { - if (typeof data === "object"){ - // search for string match - for(var i = 0; i < data.length; i++) { - var dataString = data[i].string, - dataProp = data[i].prop; - this.version_string = data[i].versionSearch || data[i].identity; - if (dataString){ - if (dataString.indexOf(data[i].subString) != -1){ - return data[i].identity; + + // Session object + let SessionRunner = function () { + win.session = win.session || {}; + // Helper for querying. + // Usage: session.current_session.referrer_info.hostname.contains(['github.com','news.ycombinator.com']) + win.session.contains = function (other_str) { + if (typeof (other_str) === 'string') { + return (this.indexOf(other_str) !== -1); } - } else if (dataProp){ - return data[i].identity; - } + for (let i = 0; i < other_str.length; i++) { + if (this.indexOf(other_str[i]) !== -1) { + return true; + } + } + return false; + }; + // Merge options + if (win.session && win.session.options) { + for (let option in win.session.options) { + options[option] = win.session.options[option]; + } + } + // Modules to run + // If the module has arguments, + // it _needs_ to return a callback function. + let unloaded_modules = { + api_version: API_VERSION, + locale: modules.locale(), + current_session: modules.session( + null, + null, + options.tracker_id, + ), + original_session: modules.session( + options.session_cookie, + options.session_timeout * 24 * 60 * 60 * 1000, + options.tracker_id, + ), + browser: modules.browser(), + plugins: modules.plugins(), + time: modules.time(), + device: modules.device(), + architecture: modules.architecture(), + }; + // Location switch + if (options.use_html5_location) { + unloaded_modules.location = modules.html5_location(); + } else if (options.ipinfodb_key) { + unloaded_modules.location = modules.ipinfodb_location(options.ipinfodb_key); + } else if (options.gapi_location) { + unloaded_modules.location = modules.gapi_location(); + } + // Extra switch + if (options.extra) { + unloaded_modules.extra = modules.extra_data() } - } else { - // search for version number - var index = data.indexOf(this.version_string); - if (index == -1) return; - return parseFloat(data.substr(index + this.version_string.length + 1)); - } - }, - data: { - browser: [ - { string: navigator.userAgent, subString: "Chrome", identity: "Chrome" }, - { string: navigator.userAgent, subString: "OmniWeb", versionSearch: "OmniWeb/", identity: "OmniWeb" }, - { string: navigator.vendor, subString: "Apple", identity: "Safari", versionSearch: "Version" }, - { prop: win.opera, identity: "Opera", versionSearch: "Version" }, - { string: navigator.vendor, subString: "iCab",identity: "iCab" }, - { string: navigator.vendor, subString: "KDE", identity: "Konqueror" }, - { string: navigator.userAgent, subString: "Firefox", identity: "Firefox" }, - { string: navigator.vendor, subString: "Camino", identity: "Camino" }, - { string: navigator.userAgent, subString: "Netscape", identity: "Netscape" }, - { string: navigator.userAgent, subString: "MSIE", identity: "Explorer", versionSearch: "MSIE" }, - { string: navigator.userAgent, subString: "Gecko", identity: "Mozilla", versionSearch: "rv" }, - { string: navigator.userAgent, subString: "Mozilla", identity: "Netscape", versionSearch: "Mozilla" } - ], - os: [ - { string: navigator.platform, subString: "Win", identity: "Windows" }, - { string: navigator.platform, subString: "Mac", identity: "Mac" }, - { string: navigator.userAgent, subString: "iPhone", identity: "iPhone/iPod" }, - { string: navigator.userAgent, subString: "iPad", identitiy: "iPad" }, - { string: navigator.platform, subString: "Linux", identity: "Linux" }, - { string: navigator.userAgent, subString: "Android", identity: "Android" } - ]} - }; - - var modules = { - browser: function() { - return browser.detect(); - }, - locale: function() { - var lang = ( - navigator.language || - navigator.browserLanguage || - navigator.systemLanguage || - navigator.userLanguage - ).split("-"); - return { - country: lang[1].toLowerCase(), - lang: lang[0].toLowerCase() - }; - }, - device: function() { - var device = { - screen: { - width: screen.width, - height: screen.height + // Cache win.session.start + let start; + if (win.session && win.session.start) { + start = win.session.start; } - }; - var html = doc.documentElement, - body = doc.getElementsByTagName("body")[0]; - device.viewport = { - width: win.innerWidth || html.clientWidth || body.clientWidth, - height: win.innerHeight || html.clientHeight || body.clientHeight - }; - device.is_tablet = !!navigator.userAgent.match(/(iPad|SCH-I800|xoom|kindle)/i); - device.is_phone = !device.isTablet && !!navigator.userAgent.match(/(iPhone|iPod|blackberry|android 0.5|htc|lg|midp|mmp|mobile|nokia|opera mini|palm|pocket|psp|sgh|smartphone|symbian|treo mini|Playstation Portable|SonyEricsson|Samsung|MobileExplorer|PalmSource|Benq|Windows Phone|Windows Mobile|IEMobile|Windows CE|Nintendo Wii)/i); - device.is_mobile = (device.is_tablet || device.is_phone); - return device; - }, - plugins: function(){ - var check_plugin = function(name){ - if (navigator.plugins){ - var plugin, i = 0, length = navigator.plugins.length; - for (; i < length; i++ ){ - plugin = navigator.plugins[i]; - if (plugin && plugin.name && plugin.name.toLowerCase().indexOf(name) !== -1){ - return true; - } } - return false; - } return false; - } - return { - flash: check_plugin("flash"), - silverlight: check_plugin("silverlight"), - java: check_plugin("java"), - quicktime: check_plugin("quicktime") - }; - }, - session: function (cookie, expires){ - var session = util.get_obj(cookie); - if (session == null){ - session = { - visits: 1, - start: new Date().getTime(), last_visit: new Date().getTime(), - url: win.location.href, path: win.location.pathname, - referrer: doc.referrer, referrer_info: util.parse_url(doc.referrer), - search: { engine: null, query: null } + // Set up checking, if all modules are ready + let asynchs = 0; + let module; + let check_asynch = function (deinc) { + if (deinc) { + asynchs--; + } + if (asynchs === 0) { + // Run start calback + if (start) { + start(win.session); + } + } }; - var search_engines = [ - { name: "Google", host: "google", query: "q" }, - { name: "Bing", host: "bing.com", query: "q" }, - { name: "Yahoo", host: "search.yahoo", query: "p" }, - { name: "AOL", host: "search.aol", query: "q" }, - { name: "Ask", host: "ask.com", query: "q" }, - { name: "Baidu", host: "baidu.com", query: "wd" } - ], length = search_engines.length, - engine, match, i = 0, - fallbacks = 'q query term p wd query text'.split(' '); - for (i = 0; i < length; i++){ - engine = search_engines[i]; - if (session.referrer_info.host.indexOf(engine.host) !== -1){ - session.search.engine = engine.name; - session.search.query = session.referrer_info.query[engine.query]; - session.search.terms = session.search.query ? session.search.query.split(" ") : null; - break; - } + win.session = {}; + // Run asynchronous methods + for (let name in unloaded_modules) { + module = unloaded_modules[name]; + if (typeof module === 'function') { + try { + module(function (data) { + win.session[name] = data; + check_asynch(true); + }); + asynchs++; + } catch (err) { + if (win.console && typeof (console.log) === 'function') { + console.log(err); + check_asynch(true); + } + } + } else { + win.session[name] = module; + } + } + check_asynch(); + }; + + + // Browser (and OS) detection + // noinspection JSUnresolvedVariable + let browser = { + detect: function () { + let ret = { + browser: this.search(this.data.browser), + version: this.search(nav.userAgent) || this.search(nav.appVersion), + os: this.search(this.data.os) + }; + if (ret.os === 'Linux') { + let distros = ['CentOS', 'Debian', 'Fedora', 'Gentoo', 'Mandriva', 'Mageia', 'Red Hat', 'Slackware', 'SUSE', 'Turbolinux', 'Ubuntu']; + for (let i = 0; i < distros.length; i++) { + if (nav.userAgent.toLowerCase().match(distros[i].toLowerCase())) { + ret.distro = distros[i]; + break; + } + } + } + return ret; + }, + search: function (data) { + if (typeof data === 'object') { + // search for string match + for (let i = 0; i < data.length; i++) { + let dataString = data[i].string, + dataProp = data[i].prop; + this.version_string = data[i].versionSearch || data[i].identity; + if (dataString) { + if (dataString.indexOf(data[i].subString) !== -1) { + return data[i].identity; + } + } else if (dataProp) { + return data[i].identity; + } + } + } else { + // search for version number + let index = data.indexOf(this.version_string); + if (index === -1) return; + return parseFloat(data.substr(index + this.version_string.length + 1)); + } + }, + data: { + browser: [ + {string: nav.userAgent, subString: 'Edge', identity: 'Edge'}, + {string: nav.userAgent, subString: 'Chrome', identity: 'Chrome'}, + {string: nav.userAgent, subString: 'OmniWeb', versionSearch: 'OmniWeb/', identity: 'OmniWeb'}, + {string: nav.vendor, subString: 'Apple', identity: 'Safari', versionSearch: 'Version'}, + {prop: win.opera, identity: 'Opera', versionSearch: 'Version'}, + {string: nav.vendor, subString: 'iCab', identity: 'iCab'}, + {string: nav.vendor, subString: 'KDE', identity: 'Konqueror'}, + {string: nav.userAgent, subString: 'Firefox', identity: 'Firefox'}, + {string: nav.vendor, subString: 'Camino', identity: 'Camino'}, + {string: nav.userAgent, subString: 'Netscape', identity: 'Netscape'}, + {string: nav.userAgent, subString: 'MSIE', identity: 'Explorer', versionSearch: 'MSIE'}, + {string: nav.userAgent, subString: 'Trident', identity: 'Explorer', versionSearch: 'rv'}, + {string: nav.userAgent, subString: 'Gecko', identity: 'Mozilla', versionSearch: 'rv'}, + {string: nav.userAgent, subString: 'Mozilla', identity: 'Netscape', versionSearch: 'Mozilla'}, + ], + os: [ + {string: nav.platform, subString: 'Win', identity: 'Windows'}, + {string: nav.platform, subString: 'Mac', identity: 'Mac'}, + {string: nav.userAgent, subString: 'iPhone', identity: 'iPhone/iPod'}, + {string: nav.userAgent, subString: 'iPad', identity: 'iPad'}, + {string: nav.userAgent, subString: 'Android', identity: 'Android'}, + {string: nav.platform, subString: 'Linux', identity: 'Linux'}, + ] } - if (session.search.engine === null && session.referrer_info.search.length > 1){ - for (i = 0; i < fallbacks.length; i++){ - var terms = session.referrer_info.query[fallbacks[i]]; - if (terms){ - session.search.engine = "Unknown"; - session.search.query = terms; session.search.terms = terms.split(" "); - break; + }; + + let modules = { + browser: function () { + return browser.detect(); + }, + time: function () { + // split date and grab timezone estimation. + // timezone estimation: https://www.onlineaspect.com/2007/06/08/auto-detect-a-time-zone-with-javascript/ + let d1 = new Date(), d2 = new Date(); + d1.setMonth(0); + d1.setDate(1); + d2.setMonth(6); + d2.setDate(1); + return ({ + tz_offset: -(new Date().getTimezoneOffset()) / 60, + observes_dst: (d1.getTimezoneOffset() !== d2.getTimezoneOffset()) + }); + // Gives a browser estimation, not guaranteed to be correct. + }, + locale: function () { + let lang = (( + nav.language || + (nav.hasOwnProperty('browserLanguage') + ? nav.browserLanguage + : nav.systemLanguage) + ) || '').split('-'); + if (lang.length === 2) { + return {country: lang[1].toLowerCase(), lang: lang[0].toLowerCase()}; + } else if (lang) { + return {lang: lang[0].toLowerCase(), country: null}; + } else { + return {lang: null, country: null}; + } + }, + device: function () { + let device = { + screen: { + width: win.screen.width, + height: win.screen.height + } + }; + let width, height; + try { + width = win.innerWidth || doc.documentElement.clientWidth || doc.body.clientWidth; + } catch (e) { + width = 0; + } + try { + height = win.innerHeight || doc.documentElement.clientHeight || doc.body.clientHeight; + } catch (e) { + height = 0; + } + device.viewport = { + width: width, + height: height + }; + device.is_tablet = !!nav.userAgent.match(/(iPad|SCH-I800|xoom|kindle)/i); + device.is_phone = !device.is_tablet && !!nav.userAgent.match(/(iPhone|iPod|blackberry|android|htc|lg|midp|mmp|mobile|nokia|opera mini|palm|pocket|psp|sgh|smartphone|symbian|treo mini|Playstation Portable|SonyEricsson|Samsung|MobileExplorer|PalmSource|Benq|Windows Phone|Windows Mobile|IEMobile|Windows CE|Nintendo Wii)/i); + device.is_mobile = device.is_tablet || device.is_phone; + return device; + }, + plugins: function () { + let check_plugin = function (name) { + if (nav.plugins) { + let plugin, i = 0, length = nav.plugins.length; + for (; i < length; i++) { + plugin = nav.plugins[i]; + if (plugin && plugin.name && plugin.name.toLowerCase().indexOf(name) !== -1) { + return true; + } + } + return false; + } + return false; + }; + let check_activex_flash = function (versions) { + let found = false; + for (let i = 0; i < versions.length; i++) { + try { + new ActiveXObject('ShockwaveFlash.ShockwaveFlash' + versions[i]); + found = true; + break; + } catch (e) { /* nil */ + } + } + return found; + }; + return { + flash: check_plugin('flash') || check_activex_flash(['.7', '.6', '']), + silverlight: check_plugin('silverlight'), + java: check_plugin('java'), + quicktime: check_plugin('quicktime') + }; + }, + session: function (cookie, expires, tracker = null) { + let session = util.get_obj(cookie); + if (session == null) { + session = { + tracker: tracker, + visits: 1, + start: new Date().getTime(), last_visit: new Date().getTime(), + url: win.location.href, path: win.location.pathname, + referrer: doc.referrer, referrer_info: util.parse_url(doc.referrer), + search: {engine: null, query: null}, + }; + let search_engines = [ + {name: 'Google', host: 'google', query: 'q'}, + {name: 'Bing', host: 'bing.com', query: 'q'}, + {name: 'Yahoo', host: 'search.yahoo', query: 'p'}, + {name: 'AOL', host: 'search.aol', query: 'q'}, + {name: 'Ask', host: 'ask.com', query: 'q'}, + {name: 'Baidu', host: 'baidu.com', query: 'wd'}, + ]; + let length = search_engines.length; + let engine; + let fallbacks = 'q query term p wd query text'.split(' '); + for (let i = 0; i < length; i++) { + engine = search_engines[i]; + if (session.referrer_info.host.indexOf(engine.host) !== -1) { + session.search.engine = engine.name; + session.search.query = session.referrer_info.query[engine.query]; + session.search.terms = session.search.query ? session.search.query.split(' ') : null; + break; + } + } + if (session.search.engine === null && session.referrer_info.search.length > 1) { + for (let i = 0; i < fallbacks.length; i++) { + let terms = session.referrer_info.query[fallbacks[i]]; + if (terms) { + session.search.engine = 'Unknown'; + session.search.query = terms; + session.search.terms = terms.split(' '); + break; + } + } + } + } else { + // Could've been initialized w/o id, so check and set + if (tracker && !session.tracker) { + session.tracker = tracker; + } + session.prev_visit = session.last_visit; + session.last_visit = new Date().getTime(); + session.visits++; + session.time_since_last_visit = session.last_visit - session.prev_visit; + } + util.set_obj(cookie, session, expires); + return session; + }, + html5_location: function () { + return function (callback) { + nav.geolocation.getCurrentPosition(function (pos) { + pos.source = 'html5'; + callback(pos); + }, function (err) { + if (options.gapi_location) { + modules.gapi_location()(callback); + } else { + callback({ + error: true, + source: 'html5', + message: err.message, + }); + } + }); + }; + }, + gapi_location: function () { + return function (callback) { + let location = util.get_obj(options.location_cookie); + if (!location || location.source !== 'google') { + win.gloader_ready = function () { + if ('google' in win + && win.google.hasOwnProperty('loader') + && win.google.loader.hasOwnProperty('ClientLocation') + ) { + if (win.google.loader.ClientLocation) { + win.google.loader.ClientLocation.source = 'google'; + callback(win.google.loader.ClientLocation); + } else { + callback({error: true, source: 'google'}); + } + // noinspection JSUnresolvedVariable + util.set_obj( + options.location_cookie, + win.google.loader.ClientLocation, + options.location_cookie_timeout * 60 * 60 * 1000, + ); + } + }; + util.embed_script('https://www.google.com/jsapi?callback=gloader_ready'); + } else { + callback(location); + } + }; + }, + ipinfodb_location: function (api_key) { + return function (callback) { + let location_cookie = util.get_obj(options.location_cookie); + if (!location_cookie && location_cookie.source === 'ipinfodb') { + win.ipinfocb = function (data) { + if (data.statusCode === 'OK') { + data.source = 'ipinfodb'; + util.set_obj( + options.location_cookie, + data, + options.location_cookie * 60 * 60 * 1000); + callback(data); + } else { + if (options.gapi_location) { + return modules.gapi_location()(callback); + } else { + callback({error: true, source: 'ipinfodb', message: data.statusMessage}); + } + } + }; + util.embed_script('https://api.ipinfodb.com/v3/ip-city/?key=' + api_key + '&format=json&callback=ipinfocb'); + } else { + callback(location_cookie); + } + } + }, + architecture: function () { + let arch = nav.userAgent.match(/x86_64|Win64|WOW64|x86-64|x64;|AMD64|amd64/) || + (nav.hasOwnProperty('cpuClass') && nav.cpuClass === 'x64') ? 'x64' : 'x86'; + return { + arch: arch, + is_x64: arch === 'x64', + is_x86: arch === 'x68', + } + }, + extra_data: function () { + let nav = {}; + for (let obj in navigator) { + if (typeof (navigator[obj]) === 'string') { + nav[obj] = navigator[obj]; + } + } + return { + navigator: nav, + document: { + referrer: document.referrer, + clientWidth: document.documentElement.clientWidth, + clientHeight: document.documentElement.clientHeight, + }, + window: { + innerWidth: window.innerWidth, + innerHeight: window.innerHeight, + location: window.location.href, + }, } - } } - } else { - session.last_visit = new Date().getTime(); - session.visits++; - } - util.set_cookie(cookie, util.package_obj(session), expires); - return session; - }, - html5_location: function() { - return function(callback){ - navigator.geolocation.getCurrentPosition(function(pos){ - pos.source = 'html5'; - callback(pos); - }, function(err) { - if (options.gapi_location){ - modules.gapi_location()(callback); - } else { - callback({error: true, source: 'html5'}); } - }); - }; - }, - gapi_location: function() { - return function(callback){ - var location = util.get_obj(options.location_cookie); - if (!location || location.source !== 'google'){ - win.gloaderReady = function() { - if ("google" in win){ - if (win.google.loader.ClientLocation){ - win.google.loader.ClientLocation.source = "google"; - callback(win.google.loader.ClientLocation); - } else { - callback({error: true, source: "google"}); - } - util.set_cookie( - options.location_cookie, - util.package_obj(win.google.loader.ClientLocation), - options.location_cookie_timeout * 60 * 60 * 1000); - }} - util.embed_script("https://www.google.com/jsapi?callback=gloaderReady"); - } else { - callback(location); - }} - }, - ipinfodb_location: function(apiKey){ - return function (callback){ - var location_cookie = util.get_obj(options.location_cookie); - if (location_cookie && location_cookie.source === 'ipinfodb'){ callback(location_cookie); } - win.ipinfocb = function(data){ - if (data.statusCode === "OK"){ - data.source = "ipinfodb"; - util.set_cookie( - options.location_cookie, - util.package_obj(data), - options.location_cookie * 60 * 60 * 1000); - callback(data); - } else { - if (options.gapi_location){ return modules.gapi_location()(callback); } - else { callback({error: true, source: "ipinfodb", message: data.statusMessage}); } - }} - util.embed_script("http://api.ipinfodb.com/v3/ip-city/?key=" + apiKey + "&format=json&callback=ipinfocb"); - }} - }; - - // Utilities - var util = win.util = { - parse_url: function(url_str){ - var a = doc.createElement("a"), query = {}; - a.href = url_str; query_str = a.search.substr(1); - // Disassemble query string - if (query_str != ''){ - var pairs = query_str.split("&"), i = 0, - length = pairs.length, parts; - for (; i < length; i++){ - parts = pairs[i].split("="); - if (parts.length === 2){ - query[parts[0]] = decodeURI(parts[1]); } + }; + + // Utilities + let util = { + parse_url: function (url_str) { + let a = doc.createElement('a'), query = {}; + a.href = url_str; + let query_str = a.search.substr(1); + // Disassemble query string + if (query_str !== '') { + let pairs = query_str.split('&'), i = 0, + length = pairs.length, parts; + for (; i < length; i++) { + parts = pairs[i].split('='); + if (parts.length === 2) { + query[parts[0]] = decodeURI(parts[1]); + } + } + } + return { + host: a.host, + path: a.pathname, + protocol: a.protocol, + port: a.port === '' ? 80 : a.port, + search: a.search, + query: query + } + }, + set_cookie: function (cname, value, expires, options) { // from jquery.cookie.js + if (!cname) { + return null; + } + if (!options) { + options = {}; + } + if (value === null || value === undefined) { + expires = -1; + } + if (expires) { + options.expires = (new Date().getTime()) + expires; + } + return (doc.cookie = [ + encodeURIComponent(cname), '=', + encodeURIComponent(String(value)), + options.expires ? '; expires=' + new Date(options.expires).toUTCString() : '', // use expires attribute, max-age is not supported by IE + '; path=' + (options.path ? options.path : '/'), + options.domain ? '; domain=' + options.domain : '', + (win.location && win.location.protocol === 'https:') ? '; secure' : '' + ].join('')); + }, + get_cookie: function (cookie_name, result) { // from jquery.cookie.js + return (result = new RegExp('(?:^|; )' + encodeURIComponent(cookie_name) + '=([^;]*)').exec(doc.cookie)) ? decodeURIComponent(result[1]) : null; + }, + embed_script: function (url) { + let element = doc.createElement('script'); + element.type = 'text/javascript'; + element.src = url; + (doc.body || doc.getElementsByTagName('body')[0] || doc.head).appendChild(element); + }, + package_obj: function (obj) { + if (obj) { + obj.version = API_VERSION; + let ret = JSON.stringify(obj); + delete obj.version; + return ret; + } + }, + set_obj: function (cname, value, expires, options) { + util.set_cookie(cname, util.package_obj(value), expires, options); + }, + get_obj: function (cookie_name) { + let obj; + try { + obj = JSON.parse(util.get_cookie(cookie_name)); + } catch (e) { + console.log(e); + } + if (obj && obj.version === API_VERSION) { + delete obj.version; + return obj; + } } - } - return { - host: a.host, - path: a.pathname, - protocol: a.protocol, - port: a.port === '' ? 80 : a.port, - search: a.search, - query: query } - }, - set_cookie: function(name, value, expires, path){ - if (!doc.cookie || !name || !value){ return null; } - // set path - path = path ? "; path=" + path : "; path=/"; - // calculate expiration date - if (expires) { - var date = new Date(); - date.setTime(date.getTime() + expires); - expires = "; expires=" + date.toGMTString(); - } else { expires = ""; } - // set cookie - return (doc.cookie = (name + "=" + value + expires + path)); - }, - get_cookie: function(cookie_name){ // from quirksmode.org - var nameEQ = cookie_name + "="; - var ca = doc.cookie.split(';'); - for (var i = 0; i < ca.length; i++){ - var c = ca[i]; - while (c.charAt(0)==' '){ c = c.substring(1, c.length); } - if (c.indexOf(nameEQ) == 0){ return c.substring(nameEQ.length, c.length); } - } return null; - }, - embed_script: function(url){ - var element = doc.createElement("script"); - element.type = "text/javascript"; - element.src = url; - doc.getElementsByTagName("body")[0].appendChild(element); - }, - package_obj: function (obj){ - obj.version = API_VERSION; - var ret = JSON.stringify(obj); - delete obj.version; return ret; - }, - get_obj: function(cookie_name){ - var obj = JSON.parse(util.get_cookie(cookie_name)); - if (obj && obj.version == API_VERSION){ - delete obj.version; return obj; - } + }; + + // cookie options override + if (options.get_object != null) { + util.get_obj = options['get_object']; + } + if (options.set_object != null) { + util.set_obj = options['set_object']; } - }; - - // JSON - var JSON = { - parse: (win.JSON && win.JSON.parse) || function(data) { - try { - if (typeof data !== "string" || !data){ return null; } - return (new Function("return " + data))(); - } catch (e){ return null; } - }, - stringify: (win.JSON && win.JSON.stringify) || function(object) { - var type = typeof object; - if (type !== "object" || object === null) { - if (type === "string"){ return '"' + object + '"'; } - } else { - var k, v, json = [], - isArray = (object && object.constructor === Array); - for (k in object ) { - v = object[k]; type = typeof v; - if (type === "string") - v = '"' + v + '"'; - else if (type === "object" && v !== null) - v = this.stringify(v); - json.push((isArray ? "" : '"' + k + '":') + v); + + // JSON + let JSON = { + parse: (win.hasOwnProperty('JSON') && win.JSON && win.JSON.parse) || function (data) { + if (typeof data !== 'string' || !data) { + return null; + } + return (new Function('return ' + data))(); + }, + stringify: (win.hasOwnProperty('JSON') && win.JSON && win.JSON.stringify) || function (object) { + let type = typeof object; + if (type !== 'object' || object === null) { + if (type === 'string') { + return '"' + object + '"'; + } + } else { + let k, v, json = [], isArray = (object && object.constructor === Array); + for (k in object) { + v = object[k]; + type = typeof v; + if (type === 'string') + v = '"' + v + '"'; + else if (type === 'object' && v !== null) + v = this.stringify(v); + json.push((isArray ? "" : '"' + k + '":') + v); + } + return (isArray ? '[' : '{') + json.join(',') + (isArray ? ']' : '}'); + } } - return (isArray ? "[" : "{") + json.join(",") + (isArray ? "]" : "}"); - } } }; + }; - // Initialize SessionRunner - SessionRunner(); + // Initialize SessionRunner + SessionRunner(); -})(window, document); \ No newline at end of file +}); +// Switch for testing purposes. +if (typeof (window.exports) === 'undefined') { + session_fetch(window, document, navigator); +} else { + window.exports.session = session_fetch; +} diff --git a/session.min.js b/session.min.js index 9eb68d8..8899030 100644 --- a/session.min.js +++ b/session.min.js @@ -1,8 +1 @@ -/** - * session.js 0.4.1 - * (c) 2012 Iain, CodeJoust - * session.js is freely distributable under the MIT license. - * Portions of session.js are inspired or borrowed from Underscore.js, and quirksmode.org demo javascript. - * This version uses google's jsapi library for location services. - * For details, see: https://github.com/codejoust/session.js - */(function(a,b){var c=.4,d={use_html5_location:!1,ipinfodb_key:!1,gapi_location:!0,location_cookie:"location",location_cookie_timeout:2,session_timeout:32,session_cookie:"first_session"},e=function(){if(a.session&&a.session.options)for(option in a.session.options)d[option]=a.session.options[option];this.modules={api_version:c,locale:g.locale(),current_session:g.session(),original_session:g.session(d.session_cookie,d.session_timeout*24*60*60*1e3),browser:g.browser(),plugins:g.plugins(),device:g.device()},d.use_html5_location?this.modules.location=g.html5_location():d.ipinfodb_key?this.modules.location=g.ipinfodb_location(d.ipinfodb_key):d.gapi_location&&(this.modules.location=g.gapi_location());if(a.session&&a.session.start)var b=a.session.start;var e=0,f,h,i=this,j=function(){e===0&&(a.session=i.modules,b&&b(i.modules))};for(var k in this.modules)f=i.modules[k],typeof f=="function"?(e++,f(function(a){i.modules[k]=a,e--,j()})):i.modules[k]=f;j()},f={detect:function(){return{browser:this.search(this.data.browser),version:this.search(navigator.userAgent)||this.search(navigator.appVersion),os:this.search(this.data.os)}},search:function(a){if(typeof a!="object"){var e=a.indexOf(this.version_string);if(e==-1)return;return parseFloat(a.substr(e+this.version_string.length+1))}for(var b=0;b1)for(k=0;k1){for(let i=0;i + + + Jasmine Spec Runner for Session.js + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

Session.js test runner

+ + diff --git a/test/spec/browser-spec.js b/test/spec/browser-spec.js new file mode 100644 index 0000000..b4e9d86 --- /dev/null +++ b/test/spec/browser-spec.js @@ -0,0 +1,92 @@ +// @todo - can tests be dynamically created in Jasmine for this to work? + +var mock = create_mock(); +var _getTestHandler = function(testData, expectedResult) { + return function() { + mock.nav.userAgent = testData[3]; + mock.nav.appVersion = testData[2]; + mock.nav.platform = testData[1]; + mock.run_sess(); + + var browser_data = mock.win.session.browser; + expect(browser_data.browser).toEqual(expectedResult.browser); + expect(browser_data.version).toEqual(expectedResult.version); + expect(browser_data.os).toEqual(expectedResult.os); + }; +}; + +describe('IE Browser Names', function(){ + var ie_tests = [ + ['msie 11', 'Win', '11.0', 'Mozilla/5.0 (Windows NT 6.3; Trident/7.0; rv 11.0) like Gecko'], + ['msie 10', 'Win', '10.0', 'Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; WOW64; Trident/6.0)'], + ['msie 9', 'Win', '9.0', 'Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0)'], + ['msie 8 .net', 'Win', '8.0', 'Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.1; Trident/4.0; .NET CLR 1.1.4322; .NET CLR 2.0.50727; .NET CLR 3.0.4506.2152; .NET CLR 3.5.30729; .NET4.0C; .NET4.0E)'], + ['msie 9', 'Win', '9.0', 'Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; WOW64; Trident/5.0)'], + ['msie 8 .net', 'Win', '8.0', 'Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.1; Trident/4.0; .NET CLR 2.0.50727; .NET CLR 3.0.4506.2152; .NET CLR 3.5.30729)'], + ['msie 8 .net 2', 'Win', '8.0', 'Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.1; Trident/4.0; .NET CLR 2.0.50727; .NET CLR 3.0.4506.2152; .NET CLR 3.5.30729)'] + ]; + var ie_tests_expected_result = [ + { browser: 'Explorer', version: 11, os: 'Windows' }, + { browser: 'Explorer', version: 10, os: 'Windows' }, + { browser: 'Explorer', version: 9, os: 'Windows' }, + { browser: 'Explorer', version: 8, os: 'Windows' }, + { browser: 'Explorer', version: 9, os: 'Windows' }, + { browser: 'Explorer', version: 8, os: 'Windows' }, + { browser: 'Explorer', version: 8, os: 'Windows' }, + ]; + for (var i = 0; i < ie_tests.length; i++) { + var ie_test = ie_tests[i]; + var expected_result = ie_tests_expected_result[i]; + it(ie_test[0], _getTestHandler(ie_test, expected_result)); + } +}); + +describe('Edge Browser Names', function (){ + var edge_tests = [ + ['edge', 'Win', '12', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.135 Safari/537.36 Edge/12.10240'] + ]; + var edge_tests_expected_result = [ + { browser: 'Edge', version: 12.1024, os: 'Windows' } + ]; + for (var i = 0; i < edge_tests.length; i++) { + var edge_test = edge_tests[i]; + var expected_result = edge_tests_expected_result[i]; + it(edge_test[0], _getTestHandler(edge_test, expected_result)); + } + +}); + +describe('Firefox Browser Names', function(){ + var firefox_tests = [ + ['firefox 9.0.1', '9.0.1', 'Mozilla/5.0 (Windows NT 5.1; rv:9.0.1) Gecko/20100101 Firefox/9.0.1'], + ['firefox 8.0.1', '8.0.1', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:8.0.1) Gecko/20100101 Firefox/8.0.1'], + ['firefox 3.6', '3.6.24', 'Mozilla/5.0 (X11; U; Linux i686; sk; rv:1.9.2.24) Gecko/20111107 Ubuntu/10.04 (lucid) Firefox/3.6.24'], + ['firefox beta', '11.0a2', 'Mozilla/5.0 (X11; Linux x86_64; rv:11.0a2) Gecko/20120107 Firefox/11.0a2'] + ]; +}); + +describe('Safari Browser Names', function(){ + var safari_tests = [ + ['ipad test', '5.1', 'Mozilla/5.0 (iPad; CPU OS 5_0_1 like Mac OS X) AppleWebKit/534.46 (KHTML, like Gecko) Version/5.1 Mobile/9A405 Safari/7534.48.3'], + ['intel mac test', '5.1.2', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_8) AppleWebKit/534.52.7 (KHTML, like Gecko) Version/5.1.2 Safari/534.52.7'], + ['5.0 intel mac', '5.0', 'Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_3; en-us) AppleWebKit/533.16 (KHTML, like Gecko) Version/5.0 Safari/533.16'], + ['5.1 ipad', '5.1', 'Mozilla/5.0 (iPad; CPU OS 5_0_1 like Mac OS X) AppleWebKit/534.46 (KHTML, like Gecko) Version/5.1 Mobile/9A405 Safari/7534.48.3'], + ['4.1 iphone', '5.1', 'Mozilla/5.0 (iPhone; CPU iPhone OS 5_0_1 like Mac OS X) AppleWebKit/534.46 (KHTML, like Gecko) Version/5.1 Mobile/9A405 Safari/7534.48.3'] + ]; + +}); + +describe('Chrome Browser Names', function(){ + var chrome_tests = [ + ['chrome 44', 'Win', '44', 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.125 Safari/537.36'], + ]; + + var chrome_tests_expected_result = [ + { browser: 'Chrome', version: 44, os: 'Windows' } + ]; + for (var i = 0; i < chrome_tests.length; i++) { + var chrome_test = chrome_tests[i]; + var expected_result = chrome_tests_expected_result[i]; + it(chrome_test[0], _getTestHandler(chrome_test, expected_result)); + } +}); diff --git a/test/spec/device-spec.js b/test/spec/device-spec.js new file mode 100644 index 0000000..8ccd3fe --- /dev/null +++ b/test/spec/device-spec.js @@ -0,0 +1,97 @@ +describe('screen size', function(){ + var mock = create_mock() + beforeEach(function(){ + mock.win = create_mock_window() + mock.doc = create_mock_document() + delete mock.win.innerWidth + delete mock.win.innerHeight + }) + it('gets proper screen size', function(){ + mock.win.screen.width = 670 + mock.win.screen.height = 770 + mock.run_sess() + expect(mock.win.session.device.screen.height).toEqual(770) + expect(mock.win.session.device.screen.width).toEqual(670) + }) + it('gets proper inner size by win.inner*', function(){ + mock.win.innerWidth = 270 + mock.win.innerHeight = 220 + mock.run_sess() + expect(mock.win.session.device.viewport).toBeTruthy() + expect(mock.win.session.device.viewport.width).toEqual(270) + expect(mock.win.session.device.viewport.height).toEqual(220) + }) + it('gets proper inner size by doc.documentElement', function(){ + mock.doc.documentElement = {} + mock.doc.documentElement.clientWidth = 520 + mock.doc.documentElement.clientHeight = 340 + mock.run_sess() + expect(mock.win.session.device.viewport).toBeTruthy() + expect(mock.win.session.device.viewport.width).toEqual(520) + expect(mock.win.session.device.viewport.height).toEqual(340) + }) + it('gets proper size by doc.body.clientHeight / doc.body.clientWidth', function(){ + mock.doc.body = {} + mock.doc.body.clientWidth = 342 + mock.doc.body.clientHeight = 434 + mock.run_sess() + expect(mock.win.session.device.viewport).toBeTruthy() + expect(mock.win.session.device.viewport.width).toEqual(342) + expect(mock.win.session.device.viewport.height).toEqual(434) + }) + it('gets 0 size when inside iframe', function(){ + mock.doc.documentElement = {} + mock.doc.documentElement.clientWidth = 0 + mock.doc.documentElement.clientHeight = 0 + mock.doc.body = undefined + mock.run_sess() + expect(mock.win.session.device.viewport).toBeTruthy() + expect(mock.win.session.device.viewport.width).toEqual(0) + expect(mock.win.session.device.viewport.height).toEqual(0) + }) +}) + +describe('mobile / tablet device checks', function(){ + var mock = create_mock() + it("shouldn't blow up with a garbled useragent", function(){ + mock.nav.userAgent = 'baser'; + mock.run_sess() + expect(mock.win.session).toBeDefined() + expect(mock.win.session.device).toBeDefined() + }) + it('should see an ipad as a mobile tablet', function(){ + mock.nav.userAgent = 'Mozilla/5.0 (iPad; CPU OS 5_0_1 like Mac OS X) AppleWebKit/534.46 (KHTML, like Gecko) Version/5.1 Mobile/9A405 Safari/7534.48.3'; + mock.run_sess() + expect(mock.win.session.device.is_tablet).toBe(true) + expect(mock.win.session.device.is_mobile).toBe(true) + expect(mock.win.session.device.is_phone).toBe(false) + }) + it('should see an iphone as a phone', function(){ + mock.nav.userAgent = 'Mozilla/5.0 (iPhone; CPU iPhone OS 5_0_1 like Mac OS X) AppleWebKit/534.46 (KHTML, like Gecko) Version/5.1 Mobile/9A405 Safari/7534.48.3'; + mock.run_sess() + expect(mock.win.session.device.is_tablet).toBe(false) + expect(mock.win.session.device.is_mobile).toBe(true) + expect(mock.win.session.device.is_phone).toBe(true) + }) + it('should see a android phone as a phone', function(){ + mock.nav.userAgent = 'Mozilla/5.0 (Linux; U; Android 4.0.2; en-us; Galaxy Nexus Build/ICL53F) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30'; + mock.run_sess() + expect(mock.win.session.device.is_tablet).toBe(false) + expect(mock.win.session.device.is_mobile).toBe(true) + expect(mock.win.session.device.is_phone).toBe(true) + }) + it('should see a chrome windows desktop as not a tablet or a phone', function(){ + mock.nav.userAgent = 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/535.7 (KHTML, like Gecko) Chrome/16.0.912.75 Safari/535.7'; + mock.run_sess() + expect(mock.win.session.device.is_tablet).toBe(false) + expect(mock.win.session.device.is_mobile).toBe(false) + expect(mock.win.session.device.is_phone).toBe(false) + }) + it('should see chrome on mac as not a tablet or a phone', function(){ + mock.nav.userAgent = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_2) AppleWebKit/535.7 (KHTML, like Gecko) Chrome/16.0.912.75 Safari/535.7'; + mock.run_sess() + expect(mock.win.session.device.is_tablet).toBe(false) + expect(mock.win.session.device.is_mobile).toBe(false) + expect(mock.win.session.device.is_phone).toBe(false) + }) +}) \ No newline at end of file diff --git a/test/spec/extension-spec.js b/test/spec/extension-spec.js new file mode 100644 index 0000000..1643ddf --- /dev/null +++ b/test/spec/extension-spec.js @@ -0,0 +1,40 @@ +/*** +* Extension Spec - Test Session modules + Session cookie functionality of session.js. +* Author: Iain, CodeJoust 2012. MIT License +***/ + +var mock = create_mock() +var session = mock.run_sess().win.session + +describe("String contains string", function(){ + it('should work with a string', function(){ + expect(session.contains('asdf', 'as')).toBe(true) + expect(session.contains('asdf', 'asdf')).toBe(true) + expect(session.contains('ASDF', 'asdf')).toBe(false) + }) + it('should not fail with a blank string', function(){ + expect(session.contains('', '')).toBe(true) + expect(session.contains('', 'foo')).toBe(false) + expect(session.contains('asdf', 'b')).toBe(false) + }) + it('needs to work with wrong types', function(){ + expect(session.contains('true', true)).toBe(false) + expect(session.contains('false', false)).toBe(false) + expect(session.contains('asdf', 43)).toBe(false) + expect(session.contains('asdf', 'z')).toBe(false) + }) +}) + +describe('String contains in array', function(){ + it('should use an array', function(){ + expect(session.contains('asdf', ['a','s','d','f'])).toBe(true) + expect(session.contains('asdf', ['ASDF','A','a'])).toBe(true) + expect(session.contains('aSdF', ['as','df','asdf'])).toBe(false) + expect(session.contains('adfs', ['asd','asb','ad','bbb','b'])).toBe(true) + }) + it('should handle bad types in an array', function(){ + expect(session.contains('asdf', [4,3,23])).toBe(false) + expect(session.contains('asdf', [true, false])).toBe(false) + expect(session.contains('true', [true,'trua'])).toBe(true) + }) +}) diff --git a/test/spec/helpers/cookiejar.js b/test/spec/helpers/cookiejar.js new file mode 100644 index 0000000..71181d5 --- /dev/null +++ b/test/spec/helpers/cookiejar.js @@ -0,0 +1,216 @@ +// Code modified for client-side use by CodeJoust +// originally from: +// bmeck/node-cookiejar - GitHub +// https://github.com/bmeck/node-cookiejar + + +var cookiejar = {} + +var CookieAccessInfo = cookiejar.CookieAccessInfo = function CookieAccessInfo(domain, path, secure, script) { + if (this instanceof CookieAccessInfo) { + this.domain = domain || undefined; + this.path = path || "/"; + this.secure = !! secure; + this.script = !! script; + return this; + } else { + return new CookieAccessInfo(domain, path, secure, script) + } +} + +var Cookie = cookiejar.Cookie = function Cookie(cookiestr) { + if (cookiestr instanceof Cookie) { + return cookiestr; + } else { + if (this instanceof Cookie) { + this.name = null; + this.value = null; + this.expiration_date = Infinity; + this.path = "/"; + this.domain = null; + this.secure = false; //how to define? + this.noscript = false; //httponly + if (cookiestr) { + this.parse(cookiestr) + } + return this; + } + return new Cookie(cookiestr) + } +} + +Cookie.prototype.toString = function toString() { + var str = [this.name + "=" + this.value]; + if (this.expiration_date !== Infinity) { + str.push("expires=" + (new Date(this.expiration_date)).toGMTString()); + } + if (this.domain) { + str.push("domain=" + this.domain); + } + if (this.path) { + str.push("path=" + this.path); + } + if (this.secure) { + str.push("secure"); + } + if (this.noscript) { + str.push("httponly"); + } + return str.join("; "); +} + +Cookie.prototype.toValueString = function toValueString() { + return this.name + "=" + this.value; +} + +var cookie_str_splitter = /[:](?=\s*[a-zA-Z0-9_\-]+\s*[=])/g +Cookie.prototype.parse = function parse(str){ + if (this instanceof Cookie) { + var parts = str.split(";"), + pair = parts[0].match(/([^=]+)=((?:.|\n)*)/), + key = pair[1], + value = pair[2]; + this.name = key; + this.value = value; + for (var i = 1; i < parts.length; i++) { + pair = parts[i].match(/([^=]+)(?:=((?:.|\n)*))?/), key = pair[1].trim().toLowerCase(), value = pair[2]; + switch (key) { + case "httponly": + this.noscript = true; + break; + case "expires": + this.expiration_date = value ? Number(Date.parse(value)) : Infinity; + break; + case "path": + this.path = value ? value.trim() : ""; + break; + case "domain": + this.domain = value ? value.trim() : ""; + break; + case "secure": + this.secure = true; + break + } + } + + return this; + } + return new Cookie().parse(str) +} + +Cookie.prototype.matches = function matches(access_info) { + if (this.noscript && access_info.script || this.secure && !access_info.secure || !this.collidesWith(access_info)) { + return false + } + return true; +} + +Cookie.prototype.collidesWith = function collidesWith(access_info) { + if ((this.path && !access_info.path) || (this.domain && !access_info.domain)) { + return false + } + if (this.path && access_info.path.indexOf(this.path) !== 0) { + return false; + } + if (this.domain === access_info.domain) { + return true; + } else if (this.domain && this.domain.charAt(0) === ".") { + var wildcard = access_info.domain.indexOf(this.domain.slice(1)) + if (wildcard === -1 || wildcard !== access_info.domain.length - this.domain.length + 1) { + return false; + } + } else if (this.domain) { + return false + } + return true; +} + +var CookieJar = cookiejar.CookieJar = function CookieJar() { + if (this instanceof CookieJar) { + var cookies = {} //name: [Cookie] + this.setCookie = function setCookie(cookie){ + cookie = Cookie(cookie); + //Delete the cookie if the set is past the current time + var remove = cookie.expiration_date <= Date.now(); + if (cookie.name in cookies) { + var cookies_list = cookies[cookie.name]; + for (var i = 0; i < cookies_list.length; i++) { + var collidable_cookie = cookies_list[i]; + if (collidable_cookie.collidesWith(cookie)) { + if (remove) { + cookies_list.splice(i, 1); + if (cookies_list.length === 0) { + delete cookies[cookie.name] + } + return false; + } else { + return cookies_list[i] = cookie; + } + } + } + if (remove) { + return false; + } + cookies_list.push(cookie); + return cookie; + } else if (remove) { + return false; + } else { + return cookies[cookie.name] = [cookie]; + } + } + //returns a cookie + this.getCookie = function getCookie(cookie_name, access_info) { + var cookies_list = cookies[cookie_name]; + for (var i = 0; i < cookies_list.length; i++) { + var cookie = cookies_list[i]; + if (cookie.expiration_date <= Date.now()) { + if (cookies_list.length === 0) { + delete cookies[cookie.name] + } + continue; + } + if (cookie.matches(access_info)) { + return cookie; + } + } + } + //returns a list of cookies + this.getCookies = function getCookies(access_info) { + var matches = []; + for (var cookie_name in cookies) { + var cookie = this.getCookie(cookie_name, access_info); + if (cookie) { + matches.push(cookie); + } + } + matches.toString = function toString() { + return matches.join(":"); + } + matches.toValueString = function() { + return matches.map(function(c) { + return c.toValueString(); + }).join(';'); + } + return matches; + } + + return this; + } + + return new CookieJar() +} + + +//returns list of cookies that were set correctly. Cookies that are expired and removed are not returned. +CookieJar.prototype.setCookies = function setCookies(cookies) { + cookies = Array.isArray(cookies) ? cookies : cookies.split(cookie_str_splitter); + var successful = [] + for (var i = 0; i < cookies.length; i++) { + var cookie = Cookie(cookies[i]); + if (this.setCookie(cookie)) { + successful.push(cookie); + } + } + return successful; +} \ No newline at end of file diff --git a/test/spec/helpers/date.format.js b/test/spec/helpers/date.format.js new file mode 100644 index 0000000..3992c50 --- /dev/null +++ b/test/spec/helpers/date.format.js @@ -0,0 +1,126 @@ +/* + * Date Format 1.2.3 + * (c) 2007-2009 Steven Levithan + * MIT license + * + * Includes enhancements by Scott Trenda + * and Kris Kowal + * + * Accepts a date, a mask, or a date and a mask. + * Returns a formatted version of the given date. + * The date defaults to the current date/time. + * The mask defaults to dateFormat.masks.default. + */ + +var dateFormat = function () { + var token = /d{1,4}|m{1,4}|yy(?:yy)?|([HhMsTt])\1?|[LloSZ]|"[^"]*"|'[^']*'/g, + timezone = /\b(?:[PMCEA][SDP]T|(?:Pacific|Mountain|Central|Eastern|Atlantic) (?:Standard|Daylight|Prevailing) Time|(?:GMT|UTC)(?:[-+]\d{4})?)\b/g, + timezoneClip = /[^-+\dA-Z]/g, + pad = function (val, len) { + val = String(val); + len = len || 2; + while (val.length < len) val = "0" + val; + return val; + }; + + // Regexes and supporting functions are cached through closure + return function (date, mask, utc) { + var dF = dateFormat; + + // You can't provide utc if you skip other args (use the "UTC:" mask prefix) + if (arguments.length == 1 && Object.prototype.toString.call(date) == "[object String]" && !/\d/.test(date)) { + mask = date; + date = undefined; + } + + // Passing date through Date applies Date.parse, if necessary + date = date ? new Date(date) : new Date; + if (isNaN(date)) throw SyntaxError("invalid date"); + + mask = String(dF.masks[mask] || mask || dF.masks["default"]); + + // Allow setting the utc argument via the mask + if (mask.slice(0, 4) == "UTC:") { + mask = mask.slice(4); + utc = true; + } + + var _ = utc ? "getUTC" : "get", + d = date[_ + "Date"](), + D = date[_ + "Day"](), + m = date[_ + "Month"](), + y = date[_ + "FullYear"](), + H = date[_ + "Hours"](), + M = date[_ + "Minutes"](), + s = date[_ + "Seconds"](), + L = date[_ + "Milliseconds"](), + o = utc ? 0 : date.getTimezoneOffset(), + flags = { + d: d, + dd: pad(d), + ddd: dF.i18n.dayNames[D], + dddd: dF.i18n.dayNames[D + 7], + m: m + 1, + mm: pad(m + 1), + mmm: dF.i18n.monthNames[m], + mmmm: dF.i18n.monthNames[m + 12], + yy: String(y).slice(2), + yyyy: y, + h: H % 12 || 12, + hh: pad(H % 12 || 12), + H: H, + HH: pad(H), + M: M, + MM: pad(M), + s: s, + ss: pad(s), + l: pad(L, 3), + L: pad(L > 99 ? Math.round(L / 10) : L), + t: H < 12 ? "a" : "p", + tt: H < 12 ? "am" : "pm", + T: H < 12 ? "A" : "P", + TT: H < 12 ? "AM" : "PM", + Z: utc ? "UTC" : (String(date).match(timezone) || [""]).pop().replace(timezoneClip, ""), + o: (o > 0 ? "-" : "+") + pad(Math.floor(Math.abs(o) / 60) * 100 + Math.abs(o) % 60, 4), + S: ["th", "st", "nd", "rd"][d % 10 > 3 ? 0 : (d % 100 - d % 10 != 10) * d % 10] + }; + + return mask.replace(token, function ($0) { + return $0 in flags ? flags[$0] : $0.slice(1, $0.length - 1); + }); + }; +}(); + +// Some common format strings +dateFormat.masks = { + "default": "ddd mmm dd yyyy HH:MM:ss", + shortDate: "m/d/yy", + mediumDate: "mmm d, yyyy", + longDate: "mmmm d, yyyy", + fullDate: "dddd, mmmm d, yyyy", + shortTime: "h:MM TT", + mediumTime: "h:MM:ss TT", + longTime: "h:MM:ss TT Z", + isoDate: "yyyy-mm-dd", + isoTime: "HH:MM:ss", + isoDateTime: "yyyy-mm-dd'T'HH:MM:ss", + isoUtcDateTime: "UTC:yyyy-mm-dd'T'HH:MM:ss'Z'" +}; + +// Internationalization strings +dateFormat.i18n = { + dayNames: [ + "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", + "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday" + ], + monthNames: [ + "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec", + "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December" + ] +}; + +// For convenience... +Date.prototype.format = function (mask, utc) { + return dateFormat(this, mask, utc); +}; + diff --git a/test/spec/helpers/session-mocker.js b/test/spec/helpers/session-mocker.js new file mode 100644 index 0000000..3beb8fa --- /dev/null +++ b/test/spec/helpers/session-mocker.js @@ -0,0 +1,59 @@ +/*** +* Test Mocker for Session.js +* Author: Iain, CodeJoust 2012. MIT License +***/ + +function parse_url(url){ + var d = document.createElement('a'); + d.href = url; d.raw = url; + return d; +} + +function create_mock_document(){ + var doc = { + get cookie(){ return this.cookiejar.getCookies(cookiejar.CookieAccessInfo()).toString(); }, + set cookie(cookie_in){ this.cookiejar.setCookie(cookie_in); return cookie_in; }, + reset_jar: function(){ this.cookiejar = new cookiejar.CookieJar(); }, + cookiejar: new cookiejar.CookieJar(), + referrer: 'http://codejoust.com/asdf', + body: {}, + documentElement: {}, + cleanup_cookies: function(){ this.cookiejar = new cookiejar.CookieJar(); } + }; + doc.createElement = function(a){ return document.createElement(a) }; + doc.getElementsByTagName = function(nm){ return document.getElementsByTagName(nm) }; + return doc; +} + +function create_mock_navigator(){ + return { + userAgent: navigator.userAgent, + appVersion: '42', + vendor: 'testing', + platform: 'linux', + language: 'en-US' + } +} + +function create_mock_window(sess_obj){ + + return {innerWidth: 200, innerHeight: 200, location: window.location, + session: sess_obj, screen:{width: 10, height: 10}}; +} + +function create_mock(sess_obj){ + if (!sess_obj){ sess_obj = {options: { gapi_location: false }}}; + var mock = { + sess: null // sets initial state + , win: create_mock_window() + , nav: create_mock_navigator(sess_obj) + , get_cookie_obj: function(cookie_name){ + return JSON.parse(unescape(this.doc.cookiejar.getCookie(cookie_name, cookiejar.CookieAccessInfo()).value)); + }, doc: create_mock_document() + , run_sess: function(){ + mock.win.session = sess_obj; + sess = exports.session(mock.win, mock.doc, mock.nav) + return sess; + }}; + return mock; +} diff --git a/test/spec/jasmine/MIT.LICENSE b/test/spec/jasmine/MIT.LICENSE new file mode 100644 index 0000000..7c435ba --- /dev/null +++ b/test/spec/jasmine/MIT.LICENSE @@ -0,0 +1,20 @@ +Copyright (c) 2008-2011 Pivotal Labs + +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. diff --git a/test/spec/jasmine/jasmine-html.js b/test/spec/jasmine/jasmine-html.js new file mode 100644 index 0000000..7383401 --- /dev/null +++ b/test/spec/jasmine/jasmine-html.js @@ -0,0 +1,190 @@ +jasmine.TrivialReporter = function(doc) { + this.document = doc || document; + this.suiteDivs = {}; + this.logRunningSpecs = false; +}; + +jasmine.TrivialReporter.prototype.createDom = function(type, attrs, childrenVarArgs) { + var el = document.createElement(type); + + for (var i = 2; i < arguments.length; i++) { + var child = arguments[i]; + + if (typeof child === 'string') { + el.appendChild(document.createTextNode(child)); + } else { + if (child) { el.appendChild(child); } + } + } + + for (var attr in attrs) { + if (attr == "className") { + el[attr] = attrs[attr]; + } else { + el.setAttribute(attr, attrs[attr]); + } + } + + return el; +}; + +jasmine.TrivialReporter.prototype.reportRunnerStarting = function(runner) { + var showPassed, showSkipped; + + this.outerDiv = this.createDom('div', { className: 'jasmine_reporter' }, + this.createDom('div', { className: 'banner' }, + this.createDom('div', { className: 'logo' }, + this.createDom('span', { className: 'title' }, "Jasmine"), + this.createDom('span', { className: 'version' }, runner.env.versionString())), + this.createDom('div', { className: 'options' }, + "Show ", + showPassed = this.createDom('input', { id: "__jasmine_TrivialReporter_showPassed__", type: 'checkbox' }), + this.createDom('label', { "for": "__jasmine_TrivialReporter_showPassed__" }, " passed "), + showSkipped = this.createDom('input', { id: "__jasmine_TrivialReporter_showSkipped__", type: 'checkbox' }), + this.createDom('label', { "for": "__jasmine_TrivialReporter_showSkipped__" }, " skipped") + ) + ), + + this.runnerDiv = this.createDom('div', { className: 'runner running' }, + this.createDom('a', { className: 'run_spec', href: '?' }, "run all"), + this.runnerMessageSpan = this.createDom('span', {}, "Running..."), + this.finishedAtSpan = this.createDom('span', { className: 'finished-at' }, "")) + ); + + this.document.body.appendChild(this.outerDiv); + + var suites = runner.suites(); + for (var i = 0; i < suites.length; i++) { + var suite = suites[i]; + var suiteDiv = this.createDom('div', { className: 'suite' }, + this.createDom('a', { className: 'run_spec', href: '?spec=' + encodeURIComponent(suite.getFullName()) }, "run"), + this.createDom('a', { className: 'description', href: '?spec=' + encodeURIComponent(suite.getFullName()) }, suite.description)); + this.suiteDivs[suite.id] = suiteDiv; + var parentDiv = this.outerDiv; + if (suite.parentSuite) { + parentDiv = this.suiteDivs[suite.parentSuite.id]; + } + parentDiv.appendChild(suiteDiv); + } + + this.startedAt = new Date(); + + var self = this; + showPassed.onclick = function(evt) { + if (showPassed.checked) { + self.outerDiv.className += ' show-passed'; + } else { + self.outerDiv.className = self.outerDiv.className.replace(/ show-passed/, ''); + } + }; + + showSkipped.onclick = function(evt) { + if (showSkipped.checked) { + self.outerDiv.className += ' show-skipped'; + } else { + self.outerDiv.className = self.outerDiv.className.replace(/ show-skipped/, ''); + } + }; +}; + +jasmine.TrivialReporter.prototype.reportRunnerResults = function(runner) { + var results = runner.results(); + var className = (results.failedCount > 0) ? "runner failed" : "runner passed"; + this.runnerDiv.setAttribute("class", className); + //do it twice for IE + this.runnerDiv.setAttribute("className", className); + var specs = runner.specs(); + var specCount = 0; + for (var i = 0; i < specs.length; i++) { + if (this.specFilter(specs[i])) { + specCount++; + } + } + var message = "" + specCount + " spec" + (specCount == 1 ? "" : "s" ) + ", " + results.failedCount + " failure" + ((results.failedCount == 1) ? "" : "s"); + message += " in " + ((new Date().getTime() - this.startedAt.getTime()) / 1000) + "s"; + this.runnerMessageSpan.replaceChild(this.createDom('a', { className: 'description', href: '?'}, message), this.runnerMessageSpan.firstChild); + + this.finishedAtSpan.appendChild(document.createTextNode("Finished at " + new Date().toString())); +}; + +jasmine.TrivialReporter.prototype.reportSuiteResults = function(suite) { + var results = suite.results(); + var status = results.passed() ? 'passed' : 'failed'; + if (results.totalCount === 0) { // todo: change this to check results.skipped + status = 'skipped'; + } + this.suiteDivs[suite.id].className += " " + status; +}; + +jasmine.TrivialReporter.prototype.reportSpecStarting = function(spec) { + if (this.logRunningSpecs) { + this.log('>> Jasmine Running ' + spec.suite.description + ' ' + spec.description + '...'); + } +}; + +jasmine.TrivialReporter.prototype.reportSpecResults = function(spec) { + var results = spec.results(); + var status = results.passed() ? 'passed' : 'failed'; + if (results.skipped) { + status = 'skipped'; + } + var specDiv = this.createDom('div', { className: 'spec ' + status }, + this.createDom('a', { className: 'run_spec', href: '?spec=' + encodeURIComponent(spec.getFullName()) }, "run"), + this.createDom('a', { + className: 'description', + href: '?spec=' + encodeURIComponent(spec.getFullName()), + title: spec.getFullName() + }, spec.description)); + + + var resultItems = results.getItems(); + var messagesDiv = this.createDom('div', { className: 'messages' }); + for (var i = 0; i < resultItems.length; i++) { + var result = resultItems[i]; + + if (result.type == 'log') { + messagesDiv.appendChild(this.createDom('div', {className: 'resultMessage log'}, result.toString())); + } else if (result.type == 'expect' && result.passed && !result.passed()) { + messagesDiv.appendChild(this.createDom('div', {className: 'resultMessage fail'}, result.message)); + + if (result.trace.stack) { + messagesDiv.appendChild(this.createDom('div', {className: 'stackTrace'}, result.trace.stack)); + } + } + } + + if (messagesDiv.childNodes.length > 0) { + specDiv.appendChild(messagesDiv); + } + + this.suiteDivs[spec.suite.id].appendChild(specDiv); +}; + +jasmine.TrivialReporter.prototype.log = function() { + var console = jasmine.getGlobal().console; + if (console && console.log) { + if (console.log.apply) { + console.log.apply(console, arguments); + } else { + console.log(arguments); // ie fix: console.log.apply doesn't exist on ie + } + } +}; + +jasmine.TrivialReporter.prototype.getLocation = function() { + return this.document.location; +}; + +jasmine.TrivialReporter.prototype.specFilter = function(spec) { + var paramMap = {}; + var params = this.getLocation().search.substring(1).split('&'); + for (var i = 0; i < params.length; i++) { + var p = params[i].split('='); + paramMap[decodeURIComponent(p[0])] = decodeURIComponent(p[1]); + } + + if (!paramMap.spec) { + return true; + } + return spec.getFullName().indexOf(paramMap.spec) === 0; +}; diff --git a/test/spec/jasmine/jasmine.css b/test/spec/jasmine/jasmine.css new file mode 100644 index 0000000..6583fe7 --- /dev/null +++ b/test/spec/jasmine/jasmine.css @@ -0,0 +1,166 @@ +body { + font-family: "Helvetica Neue Light", "Lucida Grande", "Calibri", "Arial", sans-serif; +} + + +.jasmine_reporter a:visited, .jasmine_reporter a { + color: #303; +} + +.jasmine_reporter a:hover, .jasmine_reporter a:active { + color: blue; +} + +.run_spec { + float:right; + padding-right: 5px; + font-size: .8em; + text-decoration: none; +} + +.jasmine_reporter { + margin: 0 5px; +} + +.banner { + color: #303; + background-color: #fef; + padding: 5px; +} + +.logo { + float: left; + font-size: 1.1em; + padding-left: 5px; +} + +.logo .version { + font-size: .6em; + padding-left: 1em; +} + +.runner.running { + background-color: yellow; +} + + +.options { + text-align: right; + font-size: .8em; +} + + + + +.suite { + border: 1px outset gray; + margin: 5px 0; + padding-left: 1em; +} + +.suite .suite { + margin: 5px; +} + +.suite.passed { + background-color: #dfd; +} + +.suite.failed { + background-color: #fdd; +} + +.spec { + margin: 5px; + padding-left: 1em; + clear: both; +} + +.spec.failed, .spec.passed, .spec.skipped { + padding-bottom: 5px; + border: 1px solid gray; +} + +.spec.failed { + background-color: #fbb; + border-color: red; +} + +.spec.passed { + background-color: #bfb; + border-color: green; +} + +.spec.skipped { + background-color: #bbb; +} + +.messages { + border-left: 1px dashed gray; + padding-left: 1em; + padding-right: 1em; +} + +.passed { + background-color: #cfc; + display: none; +} + +.failed { + background-color: #fbb; +} + +.skipped { + color: #777; + background-color: #eee; + display: none; +} + + +/*.resultMessage {*/ + /*white-space: pre;*/ +/*}*/ + +.resultMessage span.result { + display: block; + line-height: 2em; + color: black; +} + +.resultMessage .mismatch { + color: black; +} + +.stackTrace { + white-space: pre; + font-size: .8em; + margin-left: 10px; + max-height: 5em; + overflow: auto; + border: 1px inset red; + padding: 1em; + background: #eef; +} + +.finished-at { + padding-left: 1em; + font-size: .6em; +} + +.show-passed .passed, +.show-skipped .skipped { + display: block; +} + + +#jasmine_content { + position:fixed; + right: 100%; +} + +.runner { + border: 1px solid gray; + display: block; + margin: 5px 0; + padding: 2px 0 2px 10px; +} diff --git a/test/spec/jasmine/jasmine.js b/test/spec/jasmine/jasmine.js new file mode 100644 index 0000000..b058ebd --- /dev/null +++ b/test/spec/jasmine/jasmine.js @@ -0,0 +1,2471 @@ +var isCommonJS = typeof window == "undefined"; + +/** + * Top level namespace for Jasmine, a lightweight JavaScript BDD/spec/testing framework. + * + * @namespace + */ +var jasmine = {}; +if (isCommonJS) exports.jasmine = jasmine; +/** + * @private + */ +jasmine.unimplementedMethod_ = function() { + throw new Error("unimplemented method"); +}; + +/** + * Use jasmine.undefined instead of undefined, since undefined is just + * a plain old variable and may be redefined by somebody else. + * + * @private + */ +jasmine.undefined = jasmine.___undefined___; + +/** + * Show diagnostic messages in the console if set to true + * + */ +jasmine.VERBOSE = false; + +/** + * Default interval in milliseconds for event loop yields (e.g. to allow network activity or to refresh the screen with the HTML-based runner). Small values here may result in slow test running. Zero means no updates until all tests have completed. + * + */ +jasmine.DEFAULT_UPDATE_INTERVAL = 250; + +/** + * Default timeout interval in milliseconds for waitsFor() blocks. + */ +jasmine.DEFAULT_TIMEOUT_INTERVAL = 5000; + +jasmine.getGlobal = function() { + function getGlobal() { + return this; + } + + return getGlobal(); +}; + +/** + * Allows for bound functions to be compared. Internal use only. + * + * @ignore + * @private + * @param base {Object} bound 'this' for the function + * @param name {Function} function to find + */ +jasmine.bindOriginal_ = function(base, name) { + var original = base[name]; + if (original.apply) { + return function() { + return original.apply(base, arguments); + }; + } else { + // IE support + return jasmine.getGlobal()[name]; + } +}; + +jasmine.setTimeout = jasmine.bindOriginal_(jasmine.getGlobal(), 'setTimeout'); +jasmine.clearTimeout = jasmine.bindOriginal_(jasmine.getGlobal(), 'clearTimeout'); +jasmine.setInterval = jasmine.bindOriginal_(jasmine.getGlobal(), 'setInterval'); +jasmine.clearInterval = jasmine.bindOriginal_(jasmine.getGlobal(), 'clearInterval'); + +jasmine.MessageResult = function(values) { + this.type = 'log'; + this.values = values; + this.trace = new Error(); // todo: test better +}; + +jasmine.MessageResult.prototype.toString = function() { + var text = ""; + for (var i = 0; i < this.values.length; i++) { + if (i > 0) text += " "; + if (jasmine.isString_(this.values[i])) { + text += this.values[i]; + } else { + text += jasmine.pp(this.values[i]); + } + } + return text; +}; + +jasmine.ExpectationResult = function(params) { + this.type = 'expect'; + this.matcherName = params.matcherName; + this.passed_ = params.passed; + this.expected = params.expected; + this.actual = params.actual; + this.message = this.passed_ ? 'Passed.' : params.message; + + var trace = (params.trace || new Error(this.message)); + this.trace = this.passed_ ? '' : trace; +}; + +jasmine.ExpectationResult.prototype.toString = function () { + return this.message; +}; + +jasmine.ExpectationResult.prototype.passed = function () { + return this.passed_; +}; + +/** + * Getter for the Jasmine environment. Ensures one gets created + */ +jasmine.getEnv = function() { + var env = jasmine.currentEnv_ = jasmine.currentEnv_ || new jasmine.Env(); + return env; +}; + +/** + * @ignore + * @private + * @param value + * @returns {Boolean} + */ +jasmine.isArray_ = function(value) { + return jasmine.isA_("Array", value); +}; + +/** + * @ignore + * @private + * @param value + * @returns {Boolean} + */ +jasmine.isString_ = function(value) { + return jasmine.isA_("String", value); +}; + +/** + * @ignore + * @private + * @param value + * @returns {Boolean} + */ +jasmine.isNumber_ = function(value) { + return jasmine.isA_("Number", value); +}; + +/** + * @ignore + * @private + * @param {String} typeName + * @param value + * @returns {Boolean} + */ +jasmine.isA_ = function(typeName, value) { + return Object.prototype.toString.apply(value) === '[object ' + typeName + ']'; +}; + +/** + * Pretty printer for expecations. Takes any object and turns it into a human-readable string. + * + * @param value {Object} an object to be outputted + * @returns {String} + */ +jasmine.pp = function(value) { + var stringPrettyPrinter = new jasmine.StringPrettyPrinter(); + stringPrettyPrinter.format(value); + return stringPrettyPrinter.string; +}; + +/** + * Returns true if the object is a DOM Node. + * + * @param {Object} obj object to check + * @returns {Boolean} + */ +jasmine.isDomNode = function(obj) { + return obj.nodeType > 0; +}; + +/** + * Returns a matchable 'generic' object of the class type. For use in expecations of type when values don't matter. + * + * @example + * // don't care about which function is passed in, as long as it's a function + * expect(mySpy).toHaveBeenCalledWith(jasmine.any(Function)); + * + * @param {Class} clazz + * @returns matchable object of the type clazz + */ +jasmine.any = function(clazz) { + return new jasmine.Matchers.Any(clazz); +}; + +/** + * Jasmine Spies are test doubles that can act as stubs, spies, fakes or when used in an expecation, mocks. + * + * Spies should be created in test setup, before expectations. They can then be checked, using the standard Jasmine + * expectation syntax. Spies can be checked if they were called or not and what the calling params were. + * + * A Spy has the following fields: wasCalled, callCount, mostRecentCall, and argsForCall (see docs). + * + * Spies are torn down at the end of every spec. + * + * Note: Do not call new jasmine.Spy() directly - a spy must be created using spyOn, jasmine.createSpy or jasmine.createSpyObj. + * + * @example + * // a stub + * var myStub = jasmine.createSpy('myStub'); // can be used anywhere + * + * // spy example + * var foo = { + * not: function(bool) { return !bool; } + * } + * + * // actual foo.not will not be called, execution stops + * spyOn(foo, 'not'); + + // foo.not spied upon, execution will continue to implementation + * spyOn(foo, 'not').andCallThrough(); + * + * // fake example + * var foo = { + * not: function(bool) { return !bool; } + * } + * + * // foo.not(val) will return val + * spyOn(foo, 'not').andCallFake(function(value) {return value;}); + * + * // mock example + * foo.not(7 == 7); + * expect(foo.not).toHaveBeenCalled(); + * expect(foo.not).toHaveBeenCalledWith(true); + * + * @constructor + * @see spyOn, jasmine.createSpy, jasmine.createSpyObj + * @param {String} name + */ +jasmine.Spy = function(name) { + /** + * The name of the spy, if provided. + */ + this.identity = name || 'unknown'; + /** + * Is this Object a spy? + */ + this.isSpy = true; + /** + * The actual function this spy stubs. + */ + this.plan = function() { + }; + /** + * Tracking of the most recent call to the spy. + * @example + * var mySpy = jasmine.createSpy('foo'); + * mySpy(1, 2); + * mySpy.mostRecentCall.args = [1, 2]; + */ + this.mostRecentCall = {}; + + /** + * Holds arguments for each call to the spy, indexed by call count + * @example + * var mySpy = jasmine.createSpy('foo'); + * mySpy(1, 2); + * mySpy(7, 8); + * mySpy.mostRecentCall.args = [7, 8]; + * mySpy.argsForCall[0] = [1, 2]; + * mySpy.argsForCall[1] = [7, 8]; + */ + this.argsForCall = []; + this.calls = []; +}; + +/** + * Tells a spy to call through to the actual implemenatation. + * + * @example + * var foo = { + * bar: function() { // do some stuff } + * } + * + * // defining a spy on an existing property: foo.bar + * spyOn(foo, 'bar').andCallThrough(); + */ +jasmine.Spy.prototype.andCallThrough = function() { + this.plan = this.originalValue; + return this; +}; + +/** + * For setting the return value of a spy. + * + * @example + * // defining a spy from scratch: foo() returns 'baz' + * var foo = jasmine.createSpy('spy on foo').andReturn('baz'); + * + * // defining a spy on an existing property: foo.bar() returns 'baz' + * spyOn(foo, 'bar').andReturn('baz'); + * + * @param {Object} value + */ +jasmine.Spy.prototype.andReturn = function(value) { + this.plan = function() { + return value; + }; + return this; +}; + +/** + * For throwing an exception when a spy is called. + * + * @example + * // defining a spy from scratch: foo() throws an exception w/ message 'ouch' + * var foo = jasmine.createSpy('spy on foo').andThrow('baz'); + * + * // defining a spy on an existing property: foo.bar() throws an exception w/ message 'ouch' + * spyOn(foo, 'bar').andThrow('baz'); + * + * @param {String} exceptionMsg + */ +jasmine.Spy.prototype.andThrow = function(exceptionMsg) { + this.plan = function() { + throw exceptionMsg; + }; + return this; +}; + +/** + * Calls an alternate implementation when a spy is called. + * + * @example + * var baz = function() { + * // do some stuff, return something + * } + * // defining a spy from scratch: foo() calls the function baz + * var foo = jasmine.createSpy('spy on foo').andCall(baz); + * + * // defining a spy on an existing property: foo.bar() calls an anonymnous function + * spyOn(foo, 'bar').andCall(function() { return 'baz';} ); + * + * @param {Function} fakeFunc + */ +jasmine.Spy.prototype.andCallFake = function(fakeFunc) { + this.plan = fakeFunc; + return this; +}; + +/** + * Resets all of a spy's the tracking variables so that it can be used again. + * + * @example + * spyOn(foo, 'bar'); + * + * foo.bar(); + * + * expect(foo.bar.callCount).toEqual(1); + * + * foo.bar.reset(); + * + * expect(foo.bar.callCount).toEqual(0); + */ +jasmine.Spy.prototype.reset = function() { + this.wasCalled = false; + this.callCount = 0; + this.argsForCall = []; + this.calls = []; + this.mostRecentCall = {}; +}; + +jasmine.createSpy = function(name) { + + var spyObj = function() { + spyObj.wasCalled = true; + spyObj.callCount++; + var args = jasmine.util.argsToArray(arguments); + spyObj.mostRecentCall.object = this; + spyObj.mostRecentCall.args = args; + spyObj.argsForCall.push(args); + spyObj.calls.push({object: this, args: args}); + return spyObj.plan.apply(this, arguments); + }; + + var spy = new jasmine.Spy(name); + + for (var prop in spy) { + spyObj[prop] = spy[prop]; + } + + spyObj.reset(); + + return spyObj; +}; + +/** + * Determines whether an object is a spy. + * + * @param {jasmine.Spy|Object} putativeSpy + * @returns {Boolean} + */ +jasmine.isSpy = function(putativeSpy) { + return putativeSpy && putativeSpy.isSpy; +}; + +/** + * Creates a more complicated spy: an Object that has every property a function that is a spy. Used for stubbing something + * large in one call. + * + * @param {String} baseName name of spy class + * @param {Array} methodNames array of names of methods to make spies + */ +jasmine.createSpyObj = function(baseName, methodNames) { + if (!jasmine.isArray_(methodNames) || methodNames.length === 0) { + throw new Error('createSpyObj requires a non-empty array of method names to create spies for'); + } + var obj = {}; + for (var i = 0; i < methodNames.length; i++) { + obj[methodNames[i]] = jasmine.createSpy(baseName + '.' + methodNames[i]); + } + return obj; +}; + +/** + * All parameters are pretty-printed and concatenated together, then written to the current spec's output. + * + * Be careful not to leave calls to jasmine.log in production code. + */ +jasmine.log = function() { + var spec = jasmine.getEnv().currentSpec; + spec.log.apply(spec, arguments); +}; + +/** + * Function that installs a spy on an existing object's method name. Used within a Spec to create a spy. + * + * @example + * // spy example + * var foo = { + * not: function(bool) { return !bool; } + * } + * spyOn(foo, 'not'); // actual foo.not will not be called, execution stops + * + * @see jasmine.createSpy + * @param obj + * @param methodName + * @returns a Jasmine spy that can be chained with all spy methods + */ +var spyOn = function(obj, methodName) { + return jasmine.getEnv().currentSpec.spyOn(obj, methodName); +}; +if (isCommonJS) exports.spyOn = spyOn; + +/** + * Creates a Jasmine spec that will be added to the current suite. + * + * // TODO: pending tests + * + * @example + * it('should be true', function() { + * expect(true).toEqual(true); + * }); + * + * @param {String} desc description of this specification + * @param {Function} func defines the preconditions and expectations of the spec + */ +var it = function(desc, func) { + return jasmine.getEnv().it(desc, func); +}; +if (isCommonJS) exports.it = it; + +/** + * Creates a disabled Jasmine spec. + * + * A convenience method that allows existing specs to be disabled temporarily during development. + * + * @param {String} desc description of this specification + * @param {Function} func defines the preconditions and expectations of the spec + */ +var xit = function(desc, func) { + return jasmine.getEnv().xit(desc, func); +}; +if (isCommonJS) exports.xit = xit; + +/** + * Starts a chain for a Jasmine expectation. + * + * It is passed an Object that is the actual value and should chain to one of the many + * jasmine.Matchers functions. + * + * @param {Object} actual Actual value to test against and expected value + */ +var expect = function(actual) { + return jasmine.getEnv().currentSpec.expect(actual); +}; +if (isCommonJS) exports.expect = expect; + +/** + * Defines part of a jasmine spec. Used in cominbination with waits or waitsFor in asynchrnous specs. + * + * @param {Function} func Function that defines part of a jasmine spec. + */ +var runs = function(func) { + jasmine.getEnv().currentSpec.runs(func); +}; +if (isCommonJS) exports.runs = runs; + +/** + * Waits a fixed time period before moving to the next block. + * + * @deprecated Use waitsFor() instead + * @param {Number} timeout milliseconds to wait + */ +var waits = function(timeout) { + jasmine.getEnv().currentSpec.waits(timeout); +}; +if (isCommonJS) exports.waits = waits; + +/** + * Waits for the latchFunction to return true before proceeding to the next block. + * + * @param {Function} latchFunction + * @param {String} optional_timeoutMessage + * @param {Number} optional_timeout + */ +var waitsFor = function(latchFunction, optional_timeoutMessage, optional_timeout) { + jasmine.getEnv().currentSpec.waitsFor.apply(jasmine.getEnv().currentSpec, arguments); +}; +if (isCommonJS) exports.waitsFor = waitsFor; + +/** + * A function that is called before each spec in a suite. + * + * Used for spec setup, including validating assumptions. + * + * @param {Function} beforeEachFunction + */ +var beforeEach = function(beforeEachFunction) { + jasmine.getEnv().beforeEach(beforeEachFunction); +}; +if (isCommonJS) exports.beforeEach = beforeEach; + +/** + * A function that is called after each spec in a suite. + * + * Used for restoring any state that is hijacked during spec execution. + * + * @param {Function} afterEachFunction + */ +var afterEach = function(afterEachFunction) { + jasmine.getEnv().afterEach(afterEachFunction); +}; +if (isCommonJS) exports.afterEach = afterEach; + +/** + * Defines a suite of specifications. + * + * Stores the description and all defined specs in the Jasmine environment as one suite of specs. Variables declared + * are accessible by calls to beforeEach, it, and afterEach. Describe blocks can be nested, allowing for specialization + * of setup in some tests. + * + * @example + * // TODO: a simple suite + * + * // TODO: a simple suite with a nested describe block + * + * @param {String} description A string, usually the class under test. + * @param {Function} specDefinitions function that defines several specs. + */ +var describe = function(description, specDefinitions) { + return jasmine.getEnv().describe(description, specDefinitions); +}; +if (isCommonJS) exports.describe = describe; + +/** + * Disables a suite of specifications. Used to disable some suites in a file, or files, temporarily during development. + * + * @param {String} description A string, usually the class under test. + * @param {Function} specDefinitions function that defines several specs. + */ +var xdescribe = function(description, specDefinitions) { + return jasmine.getEnv().xdescribe(description, specDefinitions); +}; +if (isCommonJS) exports.xdescribe = xdescribe; + + +// Provide the XMLHttpRequest class for IE 5.x-6.x: +jasmine.XmlHttpRequest = (typeof XMLHttpRequest == "undefined") ? function() { + function tryIt(f) { + try { + return f(); + } catch(e) { + } + return null; + } + + var xhr = tryIt(function() { + return new ActiveXObject("Msxml2.XMLHTTP.6.0"); + }) || + tryIt(function() { + return new ActiveXObject("Msxml2.XMLHTTP.3.0"); + }) || + tryIt(function() { + return new ActiveXObject("Msxml2.XMLHTTP"); + }) || + tryIt(function() { + return new ActiveXObject("Microsoft.XMLHTTP"); + }); + + if (!xhr) throw new Error("This browser does not support XMLHttpRequest."); + + return xhr; +} : XMLHttpRequest; +/** + * @namespace + */ +jasmine.util = {}; + +/** + * Declare that a child class inherit it's prototype from the parent class. + * + * @private + * @param {Function} childClass + * @param {Function} parentClass + */ +jasmine.util.inherit = function(childClass, parentClass) { + /** + * @private + */ + var subclass = function() { + }; + subclass.prototype = parentClass.prototype; + childClass.prototype = new subclass(); +}; + +jasmine.util.formatException = function(e) { + var lineNumber; + if (e.line) { + lineNumber = e.line; + } + else if (e.lineNumber) { + lineNumber = e.lineNumber; + } + + var file; + + if (e.sourceURL) { + file = e.sourceURL; + } + else if (e.fileName) { + file = e.fileName; + } + + var message = (e.name && e.message) ? (e.name + ': ' + e.message) : e.toString(); + + if (file && lineNumber) { + message += ' in ' + file + ' (line ' + lineNumber + ')'; + } + + return message; +}; + +jasmine.util.htmlEscape = function(str) { + if (!str) return str; + return str.replace(/&/g, '&') + .replace(//g, '>'); +}; + +jasmine.util.argsToArray = function(args) { + var arrayOfArgs = []; + for (var i = 0; i < args.length; i++) arrayOfArgs.push(args[i]); + return arrayOfArgs; +}; + +jasmine.util.extend = function(destination, source) { + for (var property in source) destination[property] = source[property]; + return destination; +}; + +/** + * Environment for Jasmine + * + * @constructor + */ +jasmine.Env = function() { + this.currentSpec = null; + this.currentSuite = null; + this.currentRunner_ = new jasmine.Runner(this); + + this.reporter = new jasmine.MultiReporter(); + + this.updateInterval = jasmine.DEFAULT_UPDATE_INTERVAL; + this.defaultTimeoutInterval = jasmine.DEFAULT_TIMEOUT_INTERVAL; + this.lastUpdate = 0; + this.specFilter = function() { + return true; + }; + + this.nextSpecId_ = 0; + this.nextSuiteId_ = 0; + this.equalityTesters_ = []; + + // wrap matchers + this.matchersClass = function() { + jasmine.Matchers.apply(this, arguments); + }; + jasmine.util.inherit(this.matchersClass, jasmine.Matchers); + + jasmine.Matchers.wrapInto_(jasmine.Matchers.prototype, this.matchersClass); +}; + + +jasmine.Env.prototype.setTimeout = jasmine.setTimeout; +jasmine.Env.prototype.clearTimeout = jasmine.clearTimeout; +jasmine.Env.prototype.setInterval = jasmine.setInterval; +jasmine.Env.prototype.clearInterval = jasmine.clearInterval; + +/** + * @returns an object containing jasmine version build info, if set. + */ +jasmine.Env.prototype.version = function () { + if (jasmine.version_) { + return jasmine.version_; + } else { + throw new Error('Version not set'); + } +}; + +/** + * @returns string containing jasmine version build info, if set. + */ +jasmine.Env.prototype.versionString = function() { + if (jasmine.version_) { + var version = this.version(); + return version.major + "." + version.minor + "." + version.build + " revision " + version.revision; + } else { + return "version unknown"; + } +}; + +/** + * @returns a sequential integer starting at 0 + */ +jasmine.Env.prototype.nextSpecId = function () { + return this.nextSpecId_++; +}; + +/** + * @returns a sequential integer starting at 0 + */ +jasmine.Env.prototype.nextSuiteId = function () { + return this.nextSuiteId_++; +}; + +/** + * Register a reporter to receive status updates from Jasmine. + * @param {jasmine.Reporter} reporter An object which will receive status updates. + */ +jasmine.Env.prototype.addReporter = function(reporter) { + this.reporter.addReporter(reporter); +}; + +jasmine.Env.prototype.execute = function() { + this.currentRunner_.execute(); +}; + +jasmine.Env.prototype.describe = function(description, specDefinitions) { + var suite = new jasmine.Suite(this, description, specDefinitions, this.currentSuite); + + var parentSuite = this.currentSuite; + if (parentSuite) { + parentSuite.add(suite); + } else { + this.currentRunner_.add(suite); + } + + this.currentSuite = suite; + + var declarationError = null; + try { + specDefinitions.call(suite); + } catch(e) { + declarationError = e; + } + + if (declarationError) { + this.it("encountered a declaration exception", function() { + throw declarationError; + }); + } + + this.currentSuite = parentSuite; + + return suite; +}; + +jasmine.Env.prototype.beforeEach = function(beforeEachFunction) { + if (this.currentSuite) { + this.currentSuite.beforeEach(beforeEachFunction); + } else { + this.currentRunner_.beforeEach(beforeEachFunction); + } +}; + +jasmine.Env.prototype.currentRunner = function () { + return this.currentRunner_; +}; + +jasmine.Env.prototype.afterEach = function(afterEachFunction) { + if (this.currentSuite) { + this.currentSuite.afterEach(afterEachFunction); + } else { + this.currentRunner_.afterEach(afterEachFunction); + } + +}; + +jasmine.Env.prototype.xdescribe = function(desc, specDefinitions) { + return { + execute: function() { + } + }; +}; + +jasmine.Env.prototype.it = function(description, func) { + var spec = new jasmine.Spec(this, this.currentSuite, description); + this.currentSuite.add(spec); + this.currentSpec = spec; + + if (func) { + spec.runs(func); + } + + return spec; +}; + +jasmine.Env.prototype.xit = function(desc, func) { + return { + id: this.nextSpecId(), + runs: function() { + } + }; +}; + +jasmine.Env.prototype.compareObjects_ = function(a, b, mismatchKeys, mismatchValues) { + if (a.__Jasmine_been_here_before__ === b && b.__Jasmine_been_here_before__ === a) { + return true; + } + + a.__Jasmine_been_here_before__ = b; + b.__Jasmine_been_here_before__ = a; + + var hasKey = function(obj, keyName) { + return obj !== null && obj[keyName] !== jasmine.undefined; + }; + + for (var property in b) { + if (!hasKey(a, property) && hasKey(b, property)) { + mismatchKeys.push("expected has key '" + property + "', but missing from actual."); + } + } + for (property in a) { + if (!hasKey(b, property) && hasKey(a, property)) { + mismatchKeys.push("expected missing key '" + property + "', but present in actual."); + } + } + for (property in b) { + if (property == '__Jasmine_been_here_before__') continue; + if (!this.equals_(a[property], b[property], mismatchKeys, mismatchValues)) { + mismatchValues.push("'" + property + "' was '" + (b[property] ? jasmine.util.htmlEscape(b[property].toString()) : b[property]) + "' in expected, but was '" + (a[property] ? jasmine.util.htmlEscape(a[property].toString()) : a[property]) + "' in actual."); + } + } + + if (jasmine.isArray_(a) && jasmine.isArray_(b) && a.length != b.length) { + mismatchValues.push("arrays were not the same length"); + } + + delete a.__Jasmine_been_here_before__; + delete b.__Jasmine_been_here_before__; + return (mismatchKeys.length === 0 && mismatchValues.length === 0); +}; + +jasmine.Env.prototype.equals_ = function(a, b, mismatchKeys, mismatchValues) { + mismatchKeys = mismatchKeys || []; + mismatchValues = mismatchValues || []; + + for (var i = 0; i < this.equalityTesters_.length; i++) { + var equalityTester = this.equalityTesters_[i]; + var result = equalityTester(a, b, this, mismatchKeys, mismatchValues); + if (result !== jasmine.undefined) return result; + } + + if (a === b) return true; + + if (a === jasmine.undefined || a === null || b === jasmine.undefined || b === null) { + return (a == jasmine.undefined && b == jasmine.undefined); + } + + if (jasmine.isDomNode(a) && jasmine.isDomNode(b)) { + return a === b; + } + + if (a instanceof Date && b instanceof Date) { + return a.getTime() == b.getTime(); + } + + if (a instanceof jasmine.Matchers.Any) { + return a.matches(b); + } + + if (b instanceof jasmine.Matchers.Any) { + return b.matches(a); + } + + if (jasmine.isString_(a) && jasmine.isString_(b)) { + return (a == b); + } + + if (jasmine.isNumber_(a) && jasmine.isNumber_(b)) { + return (a == b); + } + + if (typeof a === "object" && typeof b === "object") { + return this.compareObjects_(a, b, mismatchKeys, mismatchValues); + } + + //Straight check + return (a === b); +}; + +jasmine.Env.prototype.contains_ = function(haystack, needle) { + if (jasmine.isArray_(haystack)) { + for (var i = 0; i < haystack.length; i++) { + if (this.equals_(haystack[i], needle)) return true; + } + return false; + } + return haystack.indexOf(needle) >= 0; +}; + +jasmine.Env.prototype.addEqualityTester = function(equalityTester) { + this.equalityTesters_.push(equalityTester); +}; +/** No-op base class for Jasmine reporters. + * + * @constructor + */ +jasmine.Reporter = function() { +}; + +//noinspection JSUnusedLocalSymbols +jasmine.Reporter.prototype.reportRunnerStarting = function(runner) { +}; + +//noinspection JSUnusedLocalSymbols +jasmine.Reporter.prototype.reportRunnerResults = function(runner) { +}; + +//noinspection JSUnusedLocalSymbols +jasmine.Reporter.prototype.reportSuiteResults = function(suite) { +}; + +//noinspection JSUnusedLocalSymbols +jasmine.Reporter.prototype.reportSpecStarting = function(spec) { +}; + +//noinspection JSUnusedLocalSymbols +jasmine.Reporter.prototype.reportSpecResults = function(spec) { +}; + +//noinspection JSUnusedLocalSymbols +jasmine.Reporter.prototype.log = function(str) { +}; + +/** + * Blocks are functions with executable code that make up a spec. + * + * @constructor + * @param {jasmine.Env} env + * @param {Function} func + * @param {jasmine.Spec} spec + */ +jasmine.Block = function(env, func, spec) { + this.env = env; + this.func = func; + this.spec = spec; +}; + +jasmine.Block.prototype.execute = function(onComplete) { + try { + this.func.apply(this.spec); + } catch (e) { + this.spec.fail(e); + } + onComplete(); +}; +/** JavaScript API reporter. + * + * @constructor + */ +jasmine.JsApiReporter = function() { + this.started = false; + this.finished = false; + this.suites_ = []; + this.results_ = {}; +}; + +jasmine.JsApiReporter.prototype.reportRunnerStarting = function(runner) { + this.started = true; + var suites = runner.topLevelSuites(); + for (var i = 0; i < suites.length; i++) { + var suite = suites[i]; + this.suites_.push(this.summarize_(suite)); + } +}; + +jasmine.JsApiReporter.prototype.suites = function() { + return this.suites_; +}; + +jasmine.JsApiReporter.prototype.summarize_ = function(suiteOrSpec) { + var isSuite = suiteOrSpec instanceof jasmine.Suite; + var summary = { + id: suiteOrSpec.id, + name: suiteOrSpec.description, + type: isSuite ? 'suite' : 'spec', + children: [] + }; + + if (isSuite) { + var children = suiteOrSpec.children(); + for (var i = 0; i < children.length; i++) { + summary.children.push(this.summarize_(children[i])); + } + } + return summary; +}; + +jasmine.JsApiReporter.prototype.results = function() { + return this.results_; +}; + +jasmine.JsApiReporter.prototype.resultsForSpec = function(specId) { + return this.results_[specId]; +}; + +//noinspection JSUnusedLocalSymbols +jasmine.JsApiReporter.prototype.reportRunnerResults = function(runner) { + this.finished = true; +}; + +//noinspection JSUnusedLocalSymbols +jasmine.JsApiReporter.prototype.reportSuiteResults = function(suite) { +}; + +//noinspection JSUnusedLocalSymbols +jasmine.JsApiReporter.prototype.reportSpecResults = function(spec) { + this.results_[spec.id] = { + messages: spec.results().getItems(), + result: spec.results().failedCount > 0 ? "failed" : "passed" + }; +}; + +//noinspection JSUnusedLocalSymbols +jasmine.JsApiReporter.prototype.log = function(str) { +}; + +jasmine.JsApiReporter.prototype.resultsForSpecs = function(specIds){ + var results = {}; + for (var i = 0; i < specIds.length; i++) { + var specId = specIds[i]; + results[specId] = this.summarizeResult_(this.results_[specId]); + } + return results; +}; + +jasmine.JsApiReporter.prototype.summarizeResult_ = function(result){ + var summaryMessages = []; + var messagesLength = result.messages.length; + for (var messageIndex = 0; messageIndex < messagesLength; messageIndex++) { + var resultMessage = result.messages[messageIndex]; + summaryMessages.push({ + text: resultMessage.type == 'log' ? resultMessage.toString() : jasmine.undefined, + passed: resultMessage.passed ? resultMessage.passed() : true, + type: resultMessage.type, + message: resultMessage.message, + trace: { + stack: resultMessage.passed && !resultMessage.passed() ? resultMessage.trace.stack : jasmine.undefined + } + }); + } + + return { + result : result.result, + messages : summaryMessages + }; +}; + +/** + * @constructor + * @param {jasmine.Env} env + * @param actual + * @param {jasmine.Spec} spec + */ +jasmine.Matchers = function(env, actual, spec, opt_isNot) { + this.env = env; + this.actual = actual; + this.spec = spec; + this.isNot = opt_isNot || false; + this.reportWasCalled_ = false; +}; + +// todo: @deprecated as of Jasmine 0.11, remove soon [xw] +jasmine.Matchers.pp = function(str) { + throw new Error("jasmine.Matchers.pp() is no longer supported, please use jasmine.pp() instead!"); +}; + +// todo: @deprecated Deprecated as of Jasmine 0.10. Rewrite your custom matchers to return true or false. [xw] +jasmine.Matchers.prototype.report = function(result, failing_message, details) { + throw new Error("As of jasmine 0.11, custom matchers must be implemented differently -- please see jasmine docs"); +}; + +jasmine.Matchers.wrapInto_ = function(prototype, matchersClass) { + for (var methodName in prototype) { + if (methodName == 'report') continue; + var orig = prototype[methodName]; + matchersClass.prototype[methodName] = jasmine.Matchers.matcherFn_(methodName, orig); + } +}; + +jasmine.Matchers.matcherFn_ = function(matcherName, matcherFunction) { + return function() { + var matcherArgs = jasmine.util.argsToArray(arguments); + var result = matcherFunction.apply(this, arguments); + + if (this.isNot) { + result = !result; + } + + if (this.reportWasCalled_) return result; + + var message; + if (!result) { + if (this.message) { + message = this.message.apply(this, arguments); + if (jasmine.isArray_(message)) { + message = message[this.isNot ? 1 : 0]; + } + } else { + var englishyPredicate = matcherName.replace(/[A-Z]/g, function(s) { return ' ' + s.toLowerCase(); }); + message = "Expected " + jasmine.pp(this.actual) + (this.isNot ? " not " : " ") + englishyPredicate; + if (matcherArgs.length > 0) { + for (var i = 0; i < matcherArgs.length; i++) { + if (i > 0) message += ","; + message += " " + jasmine.pp(matcherArgs[i]); + } + } + message += "."; + } + } + var expectationResult = new jasmine.ExpectationResult({ + matcherName: matcherName, + passed: result, + expected: matcherArgs.length > 1 ? matcherArgs : matcherArgs[0], + actual: this.actual, + message: message + }); + this.spec.addMatcherResult(expectationResult); + return jasmine.undefined; + }; +}; + + + + +/** + * toBe: compares the actual to the expected using === + * @param expected + */ +jasmine.Matchers.prototype.toBe = function(expected) { + return this.actual === expected; +}; + +/** + * toNotBe: compares the actual to the expected using !== + * @param expected + * @deprecated as of 1.0. Use not.toBe() instead. + */ +jasmine.Matchers.prototype.toNotBe = function(expected) { + return this.actual !== expected; +}; + +/** + * toEqual: compares the actual to the expected using common sense equality. Handles Objects, Arrays, etc. + * + * @param expected + */ +jasmine.Matchers.prototype.toEqual = function(expected) { + return this.env.equals_(this.actual, expected); +}; + +/** + * toNotEqual: compares the actual to the expected using the ! of jasmine.Matchers.toEqual + * @param expected + * @deprecated as of 1.0. Use not.toNotEqual() instead. + */ +jasmine.Matchers.prototype.toNotEqual = function(expected) { + return !this.env.equals_(this.actual, expected); +}; + +/** + * Matcher that compares the actual to the expected using a regular expression. Constructs a RegExp, so takes + * a pattern or a String. + * + * @param expected + */ +jasmine.Matchers.prototype.toMatch = function(expected) { + return new RegExp(expected).test(this.actual); +}; + +/** + * Matcher that compares the actual to the expected using the boolean inverse of jasmine.Matchers.toMatch + * @param expected + * @deprecated as of 1.0. Use not.toMatch() instead. + */ +jasmine.Matchers.prototype.toNotMatch = function(expected) { + return !(new RegExp(expected).test(this.actual)); +}; + +/** + * Matcher that compares the actual to jasmine.undefined. + */ +jasmine.Matchers.prototype.toBeDefined = function() { + return (this.actual !== jasmine.undefined); +}; + +/** + * Matcher that compares the actual to jasmine.undefined. + */ +jasmine.Matchers.prototype.toBeUndefined = function() { + return (this.actual === jasmine.undefined); +}; + +/** + * Matcher that compares the actual to null. + */ +jasmine.Matchers.prototype.toBeNull = function() { + return (this.actual === null); +}; + +/** + * Matcher that boolean not-nots the actual. + */ +jasmine.Matchers.prototype.toBeTruthy = function() { + return !!this.actual; +}; + + +/** + * Matcher that boolean nots the actual. + */ +jasmine.Matchers.prototype.toBeFalsy = function() { + return !this.actual; +}; + + +/** + * Matcher that checks to see if the actual, a Jasmine spy, was called. + */ +jasmine.Matchers.prototype.toHaveBeenCalled = function() { + if (arguments.length > 0) { + throw new Error('toHaveBeenCalled does not take arguments, use toHaveBeenCalledWith'); + } + + if (!jasmine.isSpy(this.actual)) { + throw new Error('Expected a spy, but got ' + jasmine.pp(this.actual) + '.'); + } + + this.message = function() { + return [ + "Expected spy " + this.actual.identity + " to have been called.", + "Expected spy " + this.actual.identity + " not to have been called." + ]; + }; + + return this.actual.wasCalled; +}; + +/** @deprecated Use expect(xxx).toHaveBeenCalled() instead */ +jasmine.Matchers.prototype.wasCalled = jasmine.Matchers.prototype.toHaveBeenCalled; + +/** + * Matcher that checks to see if the actual, a Jasmine spy, was not called. + * + * @deprecated Use expect(xxx).not.toHaveBeenCalled() instead + */ +jasmine.Matchers.prototype.wasNotCalled = function() { + if (arguments.length > 0) { + throw new Error('wasNotCalled does not take arguments'); + } + + if (!jasmine.isSpy(this.actual)) { + throw new Error('Expected a spy, but got ' + jasmine.pp(this.actual) + '.'); + } + + this.message = function() { + return [ + "Expected spy " + this.actual.identity + " to not have been called.", + "Expected spy " + this.actual.identity + " to have been called." + ]; + }; + + return !this.actual.wasCalled; +}; + +/** + * Matcher that checks to see if the actual, a Jasmine spy, was called with a set of parameters. + * + * @example + * + */ +jasmine.Matchers.prototype.toHaveBeenCalledWith = function() { + var expectedArgs = jasmine.util.argsToArray(arguments); + if (!jasmine.isSpy(this.actual)) { + throw new Error('Expected a spy, but got ' + jasmine.pp(this.actual) + '.'); + } + this.message = function() { + if (this.actual.callCount === 0) { + // todo: what should the failure message for .not.toHaveBeenCalledWith() be? is this right? test better. [xw] + return [ + "Expected spy " + this.actual.identity + " to have been called with " + jasmine.pp(expectedArgs) + " but it was never called.", + "Expected spy " + this.actual.identity + " not to have been called with " + jasmine.pp(expectedArgs) + " but it was." + ]; + } else { + return [ + "Expected spy " + this.actual.identity + " to have been called with " + jasmine.pp(expectedArgs) + " but was called with " + jasmine.pp(this.actual.argsForCall), + "Expected spy " + this.actual.identity + " not to have been called with " + jasmine.pp(expectedArgs) + " but was called with " + jasmine.pp(this.actual.argsForCall) + ]; + } + }; + + return this.env.contains_(this.actual.argsForCall, expectedArgs); +}; + +/** @deprecated Use expect(xxx).toHaveBeenCalledWith() instead */ +jasmine.Matchers.prototype.wasCalledWith = jasmine.Matchers.prototype.toHaveBeenCalledWith; + +/** @deprecated Use expect(xxx).not.toHaveBeenCalledWith() instead */ +jasmine.Matchers.prototype.wasNotCalledWith = function() { + var expectedArgs = jasmine.util.argsToArray(arguments); + if (!jasmine.isSpy(this.actual)) { + throw new Error('Expected a spy, but got ' + jasmine.pp(this.actual) + '.'); + } + + this.message = function() { + return [ + "Expected spy not to have been called with " + jasmine.pp(expectedArgs) + " but it was", + "Expected spy to have been called with " + jasmine.pp(expectedArgs) + " but it was" + ]; + }; + + return !this.env.contains_(this.actual.argsForCall, expectedArgs); +}; + +/** + * Matcher that checks that the expected item is an element in the actual Array. + * + * @param {Object} expected + */ +jasmine.Matchers.prototype.toContain = function(expected) { + return this.env.contains_(this.actual, expected); +}; + +/** + * Matcher that checks that the expected item is NOT an element in the actual Array. + * + * @param {Object} expected + * @deprecated as of 1.0. Use not.toNotContain() instead. + */ +jasmine.Matchers.prototype.toNotContain = function(expected) { + return !this.env.contains_(this.actual, expected); +}; + +jasmine.Matchers.prototype.toBeLessThan = function(expected) { + return this.actual < expected; +}; + +jasmine.Matchers.prototype.toBeGreaterThan = function(expected) { + return this.actual > expected; +}; + +/** + * Matcher that checks that the expected item is equal to the actual item + * up to a given level of decimal precision (default 2). + * + * @param {Number} expected + * @param {Number} precision + */ +jasmine.Matchers.prototype.toBeCloseTo = function(expected, precision) { + if (!(precision === 0)) { + precision = precision || 2; + } + var multiplier = Math.pow(10, precision); + var actual = Math.round(this.actual * multiplier); + expected = Math.round(expected * multiplier); + return expected == actual; +}; + +/** + * Matcher that checks that the expected exception was thrown by the actual. + * + * @param {String} expected + */ +jasmine.Matchers.prototype.toThrow = function(expected) { + var result = false; + var exception; + if (typeof this.actual != 'function') { + throw new Error('Actual is not a function'); + } + try { + this.actual(); + } catch (e) { + exception = e; + } + if (exception) { + result = (expected === jasmine.undefined || this.env.equals_(exception.message || exception, expected.message || expected)); + } + + var not = this.isNot ? "not " : ""; + + this.message = function() { + if (exception && (expected === jasmine.undefined || !this.env.equals_(exception.message || exception, expected.message || expected))) { + return ["Expected function " + not + "to throw", expected ? expected.message || expected : "an exception", ", but it threw", exception.message || exception].join(' '); + } else { + return "Expected function to throw an exception."; + } + }; + + return result; +}; + +jasmine.Matchers.Any = function(expectedClass) { + this.expectedClass = expectedClass; +}; + +jasmine.Matchers.Any.prototype.matches = function(other) { + if (this.expectedClass == String) { + return typeof other == 'string' || other instanceof String; + } + + if (this.expectedClass == Number) { + return typeof other == 'number' || other instanceof Number; + } + + if (this.expectedClass == Function) { + return typeof other == 'function' || other instanceof Function; + } + + if (this.expectedClass == Object) { + return typeof other == 'object'; + } + + return other instanceof this.expectedClass; +}; + +jasmine.Matchers.Any.prototype.toString = function() { + return ''; +}; + +/** + * @constructor + */ +jasmine.MultiReporter = function() { + this.subReporters_ = []; +}; +jasmine.util.inherit(jasmine.MultiReporter, jasmine.Reporter); + +jasmine.MultiReporter.prototype.addReporter = function(reporter) { + this.subReporters_.push(reporter); +}; + +(function() { + var functionNames = [ + "reportRunnerStarting", + "reportRunnerResults", + "reportSuiteResults", + "reportSpecStarting", + "reportSpecResults", + "log" + ]; + for (var i = 0; i < functionNames.length; i++) { + var functionName = functionNames[i]; + jasmine.MultiReporter.prototype[functionName] = (function(functionName) { + return function() { + for (var j = 0; j < this.subReporters_.length; j++) { + var subReporter = this.subReporters_[j]; + if (subReporter[functionName]) { + subReporter[functionName].apply(subReporter, arguments); + } + } + }; + })(functionName); + } +})(); +/** + * Holds results for a set of Jasmine spec. Allows for the results array to hold another jasmine.NestedResults + * + * @constructor + */ +jasmine.NestedResults = function() { + /** + * The total count of results + */ + this.totalCount = 0; + /** + * Number of passed results + */ + this.passedCount = 0; + /** + * Number of failed results + */ + this.failedCount = 0; + /** + * Was this suite/spec skipped? + */ + this.skipped = false; + /** + * @ignore + */ + this.items_ = []; +}; + +/** + * Roll up the result counts. + * + * @param result + */ +jasmine.NestedResults.prototype.rollupCounts = function(result) { + this.totalCount += result.totalCount; + this.passedCount += result.passedCount; + this.failedCount += result.failedCount; +}; + +/** + * Adds a log message. + * @param values Array of message parts which will be concatenated later. + */ +jasmine.NestedResults.prototype.log = function(values) { + this.items_.push(new jasmine.MessageResult(values)); +}; + +/** + * Getter for the results: message & results. + */ +jasmine.NestedResults.prototype.getItems = function() { + return this.items_; +}; + +/** + * Adds a result, tracking counts (total, passed, & failed) + * @param {jasmine.ExpectationResult|jasmine.NestedResults} result + */ +jasmine.NestedResults.prototype.addResult = function(result) { + if (result.type != 'log') { + if (result.items_) { + this.rollupCounts(result); + } else { + this.totalCount++; + if (result.passed()) { + this.passedCount++; + } else { + this.failedCount++; + } + } + } + this.items_.push(result); +}; + +/** + * @returns {Boolean} True if everything below passed + */ +jasmine.NestedResults.prototype.passed = function() { + return this.passedCount === this.totalCount; +}; +/** + * Base class for pretty printing for expectation results. + */ +jasmine.PrettyPrinter = function() { + this.ppNestLevel_ = 0; +}; + +/** + * Formats a value in a nice, human-readable string. + * + * @param value + */ +jasmine.PrettyPrinter.prototype.format = function(value) { + if (this.ppNestLevel_ > 40) { + throw new Error('jasmine.PrettyPrinter: format() nested too deeply!'); + } + + this.ppNestLevel_++; + try { + if (value === jasmine.undefined) { + this.emitScalar('undefined'); + } else if (value === null) { + this.emitScalar('null'); + } else if (value === jasmine.getGlobal()) { + this.emitScalar(''); + } else if (value instanceof jasmine.Matchers.Any) { + this.emitScalar(value.toString()); + } else if (typeof value === 'string') { + this.emitString(value); + } else if (jasmine.isSpy(value)) { + this.emitScalar("spy on " + value.identity); + } else if (value instanceof RegExp) { + this.emitScalar(value.toString()); + } else if (typeof value === 'function') { + this.emitScalar('Function'); + } else if (typeof value.nodeType === 'number') { + this.emitScalar('HTMLNode'); + } else if (value instanceof Date) { + this.emitScalar('Date(' + value + ')'); + } else if (value.__Jasmine_been_here_before__) { + this.emitScalar(''); + } else if (jasmine.isArray_(value) || typeof value == 'object') { + value.__Jasmine_been_here_before__ = true; + if (jasmine.isArray_(value)) { + this.emitArray(value); + } else { + this.emitObject(value); + } + delete value.__Jasmine_been_here_before__; + } else { + this.emitScalar(value.toString()); + } + } finally { + this.ppNestLevel_--; + } +}; + +jasmine.PrettyPrinter.prototype.iterateObject = function(obj, fn) { + for (var property in obj) { + if (property == '__Jasmine_been_here_before__') continue; + fn(property, obj.__lookupGetter__ ? (obj.__lookupGetter__(property) !== jasmine.undefined && + obj.__lookupGetter__(property) !== null) : false); + } +}; + +jasmine.PrettyPrinter.prototype.emitArray = jasmine.unimplementedMethod_; +jasmine.PrettyPrinter.prototype.emitObject = jasmine.unimplementedMethod_; +jasmine.PrettyPrinter.prototype.emitScalar = jasmine.unimplementedMethod_; +jasmine.PrettyPrinter.prototype.emitString = jasmine.unimplementedMethod_; + +jasmine.StringPrettyPrinter = function() { + jasmine.PrettyPrinter.call(this); + + this.string = ''; +}; +jasmine.util.inherit(jasmine.StringPrettyPrinter, jasmine.PrettyPrinter); + +jasmine.StringPrettyPrinter.prototype.emitScalar = function(value) { + this.append(value); +}; + +jasmine.StringPrettyPrinter.prototype.emitString = function(value) { + this.append("'" + value + "'"); +}; + +jasmine.StringPrettyPrinter.prototype.emitArray = function(array) { + this.append('[ '); + for (var i = 0; i < array.length; i++) { + if (i > 0) { + this.append(', '); + } + this.format(array[i]); + } + this.append(' ]'); +}; + +jasmine.StringPrettyPrinter.prototype.emitObject = function(obj) { + var self = this; + this.append('{ '); + var first = true; + + this.iterateObject(obj, function(property, isGetter) { + if (first) { + first = false; + } else { + self.append(', '); + } + + self.append(property); + self.append(' : '); + if (isGetter) { + self.append(''); + } else { + self.format(obj[property]); + } + }); + + this.append(' }'); +}; + +jasmine.StringPrettyPrinter.prototype.append = function(value) { + this.string += value; +}; +jasmine.Queue = function(env) { + this.env = env; + this.blocks = []; + this.running = false; + this.index = 0; + this.offset = 0; + this.abort = false; +}; + +jasmine.Queue.prototype.addBefore = function(block) { + this.blocks.unshift(block); +}; + +jasmine.Queue.prototype.add = function(block) { + this.blocks.push(block); +}; + +jasmine.Queue.prototype.insertNext = function(block) { + this.blocks.splice((this.index + this.offset + 1), 0, block); + this.offset++; +}; + +jasmine.Queue.prototype.start = function(onComplete) { + this.running = true; + this.onComplete = onComplete; + this.next_(); +}; + +jasmine.Queue.prototype.isRunning = function() { + return this.running; +}; + +jasmine.Queue.LOOP_DONT_RECURSE = true; + +jasmine.Queue.prototype.next_ = function() { + var self = this; + var goAgain = true; + + while (goAgain) { + goAgain = false; + + if (self.index < self.blocks.length && !this.abort) { + var calledSynchronously = true; + var completedSynchronously = false; + + var onComplete = function () { + if (jasmine.Queue.LOOP_DONT_RECURSE && calledSynchronously) { + completedSynchronously = true; + return; + } + + if (self.blocks[self.index].abort) { + self.abort = true; + } + + self.offset = 0; + self.index++; + + var now = new Date().getTime(); + if (self.env.updateInterval && now - self.env.lastUpdate > self.env.updateInterval) { + self.env.lastUpdate = now; + self.env.setTimeout(function() { + self.next_(); + }, 0); + } else { + if (jasmine.Queue.LOOP_DONT_RECURSE && completedSynchronously) { + goAgain = true; + } else { + self.next_(); + } + } + }; + self.blocks[self.index].execute(onComplete); + + calledSynchronously = false; + if (completedSynchronously) { + onComplete(); + } + + } else { + self.running = false; + if (self.onComplete) { + self.onComplete(); + } + } + } +}; + +jasmine.Queue.prototype.results = function() { + var results = new jasmine.NestedResults(); + for (var i = 0; i < this.blocks.length; i++) { + if (this.blocks[i].results) { + results.addResult(this.blocks[i].results()); + } + } + return results; +}; + + +/** + * Runner + * + * @constructor + * @param {jasmine.Env} env + */ +jasmine.Runner = function(env) { + var self = this; + self.env = env; + self.queue = new jasmine.Queue(env); + self.before_ = []; + self.after_ = []; + self.suites_ = []; +}; + +jasmine.Runner.prototype.execute = function() { + var self = this; + if (self.env.reporter.reportRunnerStarting) { + self.env.reporter.reportRunnerStarting(this); + } + self.queue.start(function () { + self.finishCallback(); + }); +}; + +jasmine.Runner.prototype.beforeEach = function(beforeEachFunction) { + beforeEachFunction.typeName = 'beforeEach'; + this.before_.splice(0,0,beforeEachFunction); +}; + +jasmine.Runner.prototype.afterEach = function(afterEachFunction) { + afterEachFunction.typeName = 'afterEach'; + this.after_.splice(0,0,afterEachFunction); +}; + + +jasmine.Runner.prototype.finishCallback = function() { + this.env.reporter.reportRunnerResults(this); +}; + +jasmine.Runner.prototype.addSuite = function(suite) { + this.suites_.push(suite); +}; + +jasmine.Runner.prototype.add = function(block) { + if (block instanceof jasmine.Suite) { + this.addSuite(block); + } + this.queue.add(block); +}; + +jasmine.Runner.prototype.specs = function () { + var suites = this.suites(); + var specs = []; + for (var i = 0; i < suites.length; i++) { + specs = specs.concat(suites[i].specs()); + } + return specs; +}; + +jasmine.Runner.prototype.suites = function() { + return this.suites_; +}; + +jasmine.Runner.prototype.topLevelSuites = function() { + var topLevelSuites = []; + for (var i = 0; i < this.suites_.length; i++) { + if (!this.suites_[i].parentSuite) { + topLevelSuites.push(this.suites_[i]); + } + } + return topLevelSuites; +}; + +jasmine.Runner.prototype.results = function() { + return this.queue.results(); +}; +/** + * Internal representation of a Jasmine specification, or test. + * + * @constructor + * @param {jasmine.Env} env + * @param {jasmine.Suite} suite + * @param {String} description + */ +jasmine.Spec = function(env, suite, description) { + if (!env) { + throw new Error('jasmine.Env() required'); + } + if (!suite) { + throw new Error('jasmine.Suite() required'); + } + var spec = this; + spec.id = env.nextSpecId ? env.nextSpecId() : null; + spec.env = env; + spec.suite = suite; + spec.description = description; + spec.queue = new jasmine.Queue(env); + + spec.afterCallbacks = []; + spec.spies_ = []; + + spec.results_ = new jasmine.NestedResults(); + spec.results_.description = description; + spec.matchersClass = null; +}; + +jasmine.Spec.prototype.getFullName = function() { + return this.suite.getFullName() + ' ' + this.description + '.'; +}; + + +jasmine.Spec.prototype.results = function() { + return this.results_; +}; + +/** + * All parameters are pretty-printed and concatenated together, then written to the spec's output. + * + * Be careful not to leave calls to jasmine.log in production code. + */ +jasmine.Spec.prototype.log = function() { + return this.results_.log(arguments); +}; + +jasmine.Spec.prototype.runs = function (func) { + var block = new jasmine.Block(this.env, func, this); + this.addToQueue(block); + return this; +}; + +jasmine.Spec.prototype.addToQueue = function (block) { + if (this.queue.isRunning()) { + this.queue.insertNext(block); + } else { + this.queue.add(block); + } +}; + +/** + * @param {jasmine.ExpectationResult} result + */ +jasmine.Spec.prototype.addMatcherResult = function(result) { + this.results_.addResult(result); +}; + +jasmine.Spec.prototype.expect = function(actual) { + var positive = new (this.getMatchersClass_())(this.env, actual, this); + positive.not = new (this.getMatchersClass_())(this.env, actual, this, true); + return positive; +}; + +/** + * Waits a fixed time period before moving to the next block. + * + * @deprecated Use waitsFor() instead + * @param {Number} timeout milliseconds to wait + */ +jasmine.Spec.prototype.waits = function(timeout) { + var waitsFunc = new jasmine.WaitsBlock(this.env, timeout, this); + this.addToQueue(waitsFunc); + return this; +}; + +/** + * Waits for the latchFunction to return true before proceeding to the next block. + * + * @param {Function} latchFunction + * @param {String} optional_timeoutMessage + * @param {Number} optional_timeout + */ +jasmine.Spec.prototype.waitsFor = function(latchFunction, optional_timeoutMessage, optional_timeout) { + var latchFunction_ = null; + var optional_timeoutMessage_ = null; + var optional_timeout_ = null; + + for (var i = 0; i < arguments.length; i++) { + var arg = arguments[i]; + switch (typeof arg) { + case 'function': + latchFunction_ = arg; + break; + case 'string': + optional_timeoutMessage_ = arg; + break; + case 'number': + optional_timeout_ = arg; + break; + } + } + + var waitsForFunc = new jasmine.WaitsForBlock(this.env, optional_timeout_, latchFunction_, optional_timeoutMessage_, this); + this.addToQueue(waitsForFunc); + return this; +}; + +jasmine.Spec.prototype.fail = function (e) { + var expectationResult = new jasmine.ExpectationResult({ + passed: false, + message: e ? jasmine.util.formatException(e) : 'Exception', + trace: { stack: e.stack } + }); + this.results_.addResult(expectationResult); +}; + +jasmine.Spec.prototype.getMatchersClass_ = function() { + return this.matchersClass || this.env.matchersClass; +}; + +jasmine.Spec.prototype.addMatchers = function(matchersPrototype) { + var parent = this.getMatchersClass_(); + var newMatchersClass = function() { + parent.apply(this, arguments); + }; + jasmine.util.inherit(newMatchersClass, parent); + jasmine.Matchers.wrapInto_(matchersPrototype, newMatchersClass); + this.matchersClass = newMatchersClass; +}; + +jasmine.Spec.prototype.finishCallback = function() { + this.env.reporter.reportSpecResults(this); +}; + +jasmine.Spec.prototype.finish = function(onComplete) { + this.removeAllSpies(); + this.finishCallback(); + if (onComplete) { + onComplete(); + } +}; + +jasmine.Spec.prototype.after = function(doAfter) { + if (this.queue.isRunning()) { + this.queue.add(new jasmine.Block(this.env, doAfter, this)); + } else { + this.afterCallbacks.unshift(doAfter); + } +}; + +jasmine.Spec.prototype.execute = function(onComplete) { + var spec = this; + if (!spec.env.specFilter(spec)) { + spec.results_.skipped = true; + spec.finish(onComplete); + return; + } + + this.env.reporter.reportSpecStarting(this); + + spec.env.currentSpec = spec; + + spec.addBeforesAndAftersToQueue(); + + spec.queue.start(function () { + spec.finish(onComplete); + }); +}; + +jasmine.Spec.prototype.addBeforesAndAftersToQueue = function() { + var runner = this.env.currentRunner(); + var i; + + for (var suite = this.suite; suite; suite = suite.parentSuite) { + for (i = 0; i < suite.before_.length; i++) { + this.queue.addBefore(new jasmine.Block(this.env, suite.before_[i], this)); + } + } + for (i = 0; i < runner.before_.length; i++) { + this.queue.addBefore(new jasmine.Block(this.env, runner.before_[i], this)); + } + for (i = 0; i < this.afterCallbacks.length; i++) { + this.queue.add(new jasmine.Block(this.env, this.afterCallbacks[i], this)); + } + for (suite = this.suite; suite; suite = suite.parentSuite) { + for (i = 0; i < suite.after_.length; i++) { + this.queue.add(new jasmine.Block(this.env, suite.after_[i], this)); + } + } + for (i = 0; i < runner.after_.length; i++) { + this.queue.add(new jasmine.Block(this.env, runner.after_[i], this)); + } +}; + +jasmine.Spec.prototype.explodes = function() { + throw 'explodes function should not have been called'; +}; + +jasmine.Spec.prototype.spyOn = function(obj, methodName, ignoreMethodDoesntExist) { + if (obj == jasmine.undefined) { + throw "spyOn could not find an object to spy upon for " + methodName + "()"; + } + + if (!ignoreMethodDoesntExist && obj[methodName] === jasmine.undefined) { + throw methodName + '() method does not exist'; + } + + if (!ignoreMethodDoesntExist && obj[methodName] && obj[methodName].isSpy) { + throw new Error(methodName + ' has already been spied upon'); + } + + var spyObj = jasmine.createSpy(methodName); + + this.spies_.push(spyObj); + spyObj.baseObj = obj; + spyObj.methodName = methodName; + spyObj.originalValue = obj[methodName]; + + obj[methodName] = spyObj; + + return spyObj; +}; + +jasmine.Spec.prototype.removeAllSpies = function() { + for (var i = 0; i < this.spies_.length; i++) { + var spy = this.spies_[i]; + spy.baseObj[spy.methodName] = spy.originalValue; + } + this.spies_ = []; +}; + +/** + * Internal representation of a Jasmine suite. + * + * @constructor + * @param {jasmine.Env} env + * @param {String} description + * @param {Function} specDefinitions + * @param {jasmine.Suite} parentSuite + */ +jasmine.Suite = function(env, description, specDefinitions, parentSuite) { + var self = this; + self.id = env.nextSuiteId ? env.nextSuiteId() : null; + self.description = description; + self.queue = new jasmine.Queue(env); + self.parentSuite = parentSuite; + self.env = env; + self.before_ = []; + self.after_ = []; + self.children_ = []; + self.suites_ = []; + self.specs_ = []; +}; + +jasmine.Suite.prototype.getFullName = function() { + var fullName = this.description; + for (var parentSuite = this.parentSuite; parentSuite; parentSuite = parentSuite.parentSuite) { + fullName = parentSuite.description + ' ' + fullName; + } + return fullName; +}; + +jasmine.Suite.prototype.finish = function(onComplete) { + this.env.reporter.reportSuiteResults(this); + this.finished = true; + if (typeof(onComplete) == 'function') { + onComplete(); + } +}; + +jasmine.Suite.prototype.beforeEach = function(beforeEachFunction) { + beforeEachFunction.typeName = 'beforeEach'; + this.before_.unshift(beforeEachFunction); +}; + +jasmine.Suite.prototype.afterEach = function(afterEachFunction) { + afterEachFunction.typeName = 'afterEach'; + this.after_.unshift(afterEachFunction); +}; + +jasmine.Suite.prototype.results = function() { + return this.queue.results(); +}; + +jasmine.Suite.prototype.add = function(suiteOrSpec) { + this.children_.push(suiteOrSpec); + if (suiteOrSpec instanceof jasmine.Suite) { + this.suites_.push(suiteOrSpec); + this.env.currentRunner().addSuite(suiteOrSpec); + } else { + this.specs_.push(suiteOrSpec); + } + this.queue.add(suiteOrSpec); +}; + +jasmine.Suite.prototype.specs = function() { + return this.specs_; +}; + +jasmine.Suite.prototype.suites = function() { + return this.suites_; +}; + +jasmine.Suite.prototype.children = function() { + return this.children_; +}; + +jasmine.Suite.prototype.execute = function(onComplete) { + var self = this; + this.queue.start(function () { + self.finish(onComplete); + }); +}; +jasmine.WaitsBlock = function(env, timeout, spec) { + this.timeout = timeout; + jasmine.Block.call(this, env, null, spec); +}; + +jasmine.util.inherit(jasmine.WaitsBlock, jasmine.Block); + +jasmine.WaitsBlock.prototype.execute = function (onComplete) { + if (jasmine.VERBOSE) { + this.env.reporter.log('>> Jasmine waiting for ' + this.timeout + ' ms...'); + } + this.env.setTimeout(function () { + onComplete(); + }, this.timeout); +}; +/** + * A block which waits for some condition to become true, with timeout. + * + * @constructor + * @extends jasmine.Block + * @param {jasmine.Env} env The Jasmine environment. + * @param {Number} timeout The maximum time in milliseconds to wait for the condition to become true. + * @param {Function} latchFunction A function which returns true when the desired condition has been met. + * @param {String} message The message to display if the desired condition hasn't been met within the given time period. + * @param {jasmine.Spec} spec The Jasmine spec. + */ +jasmine.WaitsForBlock = function(env, timeout, latchFunction, message, spec) { + this.timeout = timeout || env.defaultTimeoutInterval; + this.latchFunction = latchFunction; + this.message = message; + this.totalTimeSpentWaitingForLatch = 0; + jasmine.Block.call(this, env, null, spec); +}; +jasmine.util.inherit(jasmine.WaitsForBlock, jasmine.Block); + +jasmine.WaitsForBlock.TIMEOUT_INCREMENT = 10; + +jasmine.WaitsForBlock.prototype.execute = function(onComplete) { + if (jasmine.VERBOSE) { + this.env.reporter.log('>> Jasmine waiting for ' + (this.message || 'something to happen')); + } + var latchFunctionResult; + try { + latchFunctionResult = this.latchFunction.apply(this.spec); + } catch (e) { + this.spec.fail(e); + onComplete(); + return; + } + + if (latchFunctionResult) { + onComplete(); + } else if (this.totalTimeSpentWaitingForLatch >= this.timeout) { + var message = 'timed out after ' + this.timeout + ' msec waiting for ' + (this.message || 'something to happen'); + this.spec.fail({ + name: 'timeout', + message: message + }); + + this.abort = true; + onComplete(); + } else { + this.totalTimeSpentWaitingForLatch += jasmine.WaitsForBlock.TIMEOUT_INCREMENT; + var self = this; + this.env.setTimeout(function() { + self.execute(onComplete); + }, jasmine.WaitsForBlock.TIMEOUT_INCREMENT); + } +}; +// Mock setTimeout, clearTimeout +// Contributed by Pivotal Computer Systems, www.pivotalsf.com + +jasmine.FakeTimer = function() { + this.reset(); + + var self = this; + self.setTimeout = function(funcToCall, millis) { + self.timeoutsMade++; + self.scheduleFunction(self.timeoutsMade, funcToCall, millis, false); + return self.timeoutsMade; + }; + + self.setInterval = function(funcToCall, millis) { + self.timeoutsMade++; + self.scheduleFunction(self.timeoutsMade, funcToCall, millis, true); + return self.timeoutsMade; + }; + + self.clearTimeout = function(timeoutKey) { + self.scheduledFunctions[timeoutKey] = jasmine.undefined; + }; + + self.clearInterval = function(timeoutKey) { + self.scheduledFunctions[timeoutKey] = jasmine.undefined; + }; + +}; + +jasmine.FakeTimer.prototype.reset = function() { + this.timeoutsMade = 0; + this.scheduledFunctions = {}; + this.nowMillis = 0; +}; + +jasmine.FakeTimer.prototype.tick = function(millis) { + var oldMillis = this.nowMillis; + var newMillis = oldMillis + millis; + this.runFunctionsWithinRange(oldMillis, newMillis); + this.nowMillis = newMillis; +}; + +jasmine.FakeTimer.prototype.runFunctionsWithinRange = function(oldMillis, nowMillis) { + var scheduledFunc; + var funcsToRun = []; + for (var timeoutKey in this.scheduledFunctions) { + scheduledFunc = this.scheduledFunctions[timeoutKey]; + if (scheduledFunc != jasmine.undefined && + scheduledFunc.runAtMillis >= oldMillis && + scheduledFunc.runAtMillis <= nowMillis) { + funcsToRun.push(scheduledFunc); + this.scheduledFunctions[timeoutKey] = jasmine.undefined; + } + } + + if (funcsToRun.length > 0) { + funcsToRun.sort(function(a, b) { + return a.runAtMillis - b.runAtMillis; + }); + for (var i = 0; i < funcsToRun.length; ++i) { + try { + var funcToRun = funcsToRun[i]; + this.nowMillis = funcToRun.runAtMillis; + funcToRun.funcToCall(); + if (funcToRun.recurring) { + this.scheduleFunction(funcToRun.timeoutKey, + funcToRun.funcToCall, + funcToRun.millis, + true); + } + } catch(e) { + } + } + this.runFunctionsWithinRange(oldMillis, nowMillis); + } +}; + +jasmine.FakeTimer.prototype.scheduleFunction = function(timeoutKey, funcToCall, millis, recurring) { + this.scheduledFunctions[timeoutKey] = { + runAtMillis: this.nowMillis + millis, + funcToCall: funcToCall, + recurring: recurring, + timeoutKey: timeoutKey, + millis: millis + }; +}; + +/** + * @namespace + */ +jasmine.Clock = { + defaultFakeTimer: new jasmine.FakeTimer(), + + reset: function() { + jasmine.Clock.assertInstalled(); + jasmine.Clock.defaultFakeTimer.reset(); + }, + + tick: function(millis) { + jasmine.Clock.assertInstalled(); + jasmine.Clock.defaultFakeTimer.tick(millis); + }, + + runFunctionsWithinRange: function(oldMillis, nowMillis) { + jasmine.Clock.defaultFakeTimer.runFunctionsWithinRange(oldMillis, nowMillis); + }, + + scheduleFunction: function(timeoutKey, funcToCall, millis, recurring) { + jasmine.Clock.defaultFakeTimer.scheduleFunction(timeoutKey, funcToCall, millis, recurring); + }, + + useMock: function() { + if (!jasmine.Clock.isInstalled()) { + var spec = jasmine.getEnv().currentSpec; + spec.after(jasmine.Clock.uninstallMock); + + jasmine.Clock.installMock(); + } + }, + + installMock: function() { + jasmine.Clock.installed = jasmine.Clock.defaultFakeTimer; + }, + + uninstallMock: function() { + jasmine.Clock.assertInstalled(); + jasmine.Clock.installed = jasmine.Clock.real; + }, + + real: { + setTimeout: jasmine.getGlobal().setTimeout, + clearTimeout: jasmine.getGlobal().clearTimeout, + setInterval: jasmine.getGlobal().setInterval, + clearInterval: jasmine.getGlobal().clearInterval + }, + + assertInstalled: function() { + if (!jasmine.Clock.isInstalled()) { + throw new Error("Mock clock is not installed, use jasmine.Clock.useMock()"); + } + }, + + isInstalled: function() { + return jasmine.Clock.installed == jasmine.Clock.defaultFakeTimer; + }, + + installed: null +}; +jasmine.Clock.installed = jasmine.Clock.real; + +//else for IE support +jasmine.getGlobal().setTimeout = function(funcToCall, millis) { + if (jasmine.Clock.installed.setTimeout.apply) { + return jasmine.Clock.installed.setTimeout.apply(this, arguments); + } else { + return jasmine.Clock.installed.setTimeout(funcToCall, millis); + } +}; + +jasmine.getGlobal().setInterval = function(funcToCall, millis) { + if (jasmine.Clock.installed.setInterval.apply) { + return jasmine.Clock.installed.setInterval.apply(this, arguments); + } else { + return jasmine.Clock.installed.setInterval(funcToCall, millis); + } +}; + +jasmine.getGlobal().clearTimeout = function(timeoutKey) { + if (jasmine.Clock.installed.clearTimeout.apply) { + return jasmine.Clock.installed.clearTimeout.apply(this, arguments); + } else { + return jasmine.Clock.installed.clearTimeout(timeoutKey); + } +}; + +jasmine.getGlobal().clearInterval = function(timeoutKey) { + if (jasmine.Clock.installed.clearTimeout.apply) { + return jasmine.Clock.installed.clearInterval.apply(this, arguments); + } else { + return jasmine.Clock.installed.clearInterval(timeoutKey); + } +}; + +jasmine.version_= { + "major": 1, + "minor": 1, + "build": 0, + "revision": 1308150691 +} diff --git a/test/spec/jasmine/jasmine_favicon.png b/test/spec/jasmine/jasmine_favicon.png new file mode 100644 index 0000000..218f3b4 Binary files /dev/null and b/test/spec/jasmine/jasmine_favicon.png differ diff --git a/test/spec/session-api-spec.js b/test/spec/session-api-spec.js new file mode 100644 index 0000000..1cc1d4c --- /dev/null +++ b/test/spec/session-api-spec.js @@ -0,0 +1,27 @@ +/*** +* Session API Spec - Check API version consistency of session.js. +* Author: Iain, CodeJoust 2012. MIT License +***/ + +describe('test api versions', function(){ + var mock = create_mock(); + it('should have the correct api version', function(){ + mock.run_sess() + expect(mock.win.session.api_version).toEqual(0.4) + }) +}) + +describe('test location api versions', function(){ + var mock = create_mock(); + it('should have the correct session cookie api version', function(){ + mock.run_sess() + expect(mock.get_cookie_obj('first_session').version).toEqual(0.4) + }) + it('should have the correct api version after a few runs', function(){ + mock.run_sess() + mock.run_sess() + mock.doc.cookiejar.setCookie('first_session=foo; path=/') + mock.run_sess() + expect(mock.get_cookie_obj('first_session').version).toEqual(0.4) + }) +}); \ No newline at end of file diff --git a/test/spec/session-info-spec.js b/test/spec/session-info-spec.js new file mode 100644 index 0000000..2036332 --- /dev/null +++ b/test/spec/session-info-spec.js @@ -0,0 +1,106 @@ +/*** +* Session Info Spec - Test Session modules + Session cookie functionality of session.js. +* Author: Iain, CodeJoust 2012. MIT License +***/ +describe("session url location", function(){ + var mock = create_mock() + it("switches location", function () { + var test_url = 'https://github.com/testing'; + mock.win.location = parse_url(test_url) + mock.run_sess(); + expect(mock.win.session.current_session.url).toEqual(test_url); + expect(mock.win.session.current_session.path).toEqual(mock.win.location.pathname); + }) + it("switches referrer", function () { + var test_url = parse_url('https://github.com/testing'); + mock.doc.referrer = test_url.raw; + mock.run_sess(); + expect(mock.win.session.current_session.url).toEqual(test_url.raw); + expect(mock.win.session.current_session.path).toEqual(test_url.pathname); + }) +}); + +describe("running search referrers", function(){ + var mock = create_mock() + it("runs google search", function(){ + mock.doc.referrer = 'http://google.com/go/url?q=asasd fasdf'; + var parsed_ref = parse_url(mock.doc.referrer); + mock.run_sess(); + expect(mock.win.session.current_session.referrer).toEqual(mock.doc.referrer); + expect(mock.win.session.current_session.referrer_info.path).toEqual('/go/url'); + expect(mock.win.session.current_session.referrer_info.search).toEqual('?q=asasd%20fasdf'); + expect(mock.win.session.current_session.search.query).toEqual('asasd fasdf'); + expect(mock.win.session.current_session.search.engine).toEqual('Google'); + }) + it("runs bing search", function(){ + mock.doc.referrer = 'http://search.yahoo.com/res?q=asdfasdf asdfasdf'; + var parsed_ref = parse_url(mock.doc.referrer); + mock.run_sess(); + expect(mock.win.session.current_session.referrer).toEqual(mock.doc.referrer); + expect(mock.win.session.current_session.referrer_info.path).toEqual('/res'); + expect(mock.win.session.current_session.referrer_info.search).toEqual('?q=asdfasdf%20asdfasdf'); + expect(mock.win.session.current_session.search.engine).toEqual('Yahoo'); + }) + it("presists search info", function(){ + mock.doc.reset_jar() + mock.doc.referrer = 'http://search.yahoo.com/res?p=asdfasdf asdfasdf'; + mock.run_sess() + mock.doc.referrer = 'http://google.com/go/url?q=asasd fasdf'; + mock.run_sess() + expect(mock.win.session.original_session.search.engine).toEqual('Yahoo'); + expect(mock.win.session.original_session.search.query).toEqual('asdfasdf asdfasdf'); + }) +}); + + +describe("counting visits", function(){ + var mock = create_mock() + mock.win.location = parse_url('http://codejoust.com/asdfasdf/asdfas') + beforeEach(function(){ + mock.doc.reset_jar() + }) + it('counts initial visits', function(){ + mock.run_sess() + expect(mock.win.session.original_session.visits).toEqual(1) + expect(mock.win.session.current_session.visits).toEqual(1) + }) + it('counts two visits', function(){ + mock.run_sess() + mock.run_sess() + expect(mock.win.session.original_session.visits).toEqual(2) + expect(mock.win.session.current_session.visits).toEqual(1) + }) + it('counts six visits', function(){ + for(var i = 0; i < 6; i++){ + mock.run_sess() + } + expect(mock.win.session.original_session.visits).toEqual(6) + expect(mock.win.session.current_session.visits).toEqual(1) + }) + it('presists across paths', function(){ + mock.run_sess() + mock.win.location.pathname = 'furballl' + mock.doc.cookiejar.getCookie('first_session', new cookiejar.CookieAccessInfo('codejoust.com', '/foopath/baar', false, false)) + }) + it('keeps start times', function(){ + runs(function(){ + mock.run_sess(); + var self = this; + self.start_time_1 = (new Date()).getTime(); + setTimeout(function(){ + mock.run_sess(); + self.start_time_2 = (new Date()).getTime(); + self.start_done_1 = mock.win.session.original_session.start; + self.start_done_2 = mock.win.session.current_session.start; + }, 50) + }) + waits(100); + runs(function(){ + var err_offset = 6; + expect(this.start_done_1).toBeLessThan(this.start_time_1 + err_offset) + expect(this.start_done_1).toBeGreaterThan(this.start_time_1 - err_offset); + expect(this.start_done_2).toBeLessThan(this.start_time_2 + err_offset) + expect(this.start_done_2).toBeGreaterThan(this.start_time_1 - err_offset); + }) + }) +}); diff --git a/test/spec/timezone-spec.js b/test/spec/timezone-spec.js new file mode 100644 index 0000000..56b7ac3 --- /dev/null +++ b/test/spec/timezone-spec.js @@ -0,0 +1,83 @@ +// drat. I have to mock the date object :/ + +describe('timezone / date tests', function(){ + var internal_date = new Date(); + // get IE date format + it('should expect IE date format', function(){ + + }) + // get firefox date format + it('should expect firefox date format', function(){ + + }) + // get chrome date format + it('should expect correct chrome date format', function(){ + + }) + // time offset + it("won't blow up with a no / wierd timeoffset", function(){ + + }) +}); + +describe('locale tests', function(){ + var mock = create_mock() + it('should take only one locale', function(){ + mock.nav.language = 'Es' + mock.run_sess() + expect(mock.win.session.locale.lang).toEqual('es') + expect(mock.win.session.locale.country).toBeFalsy() + }) + it('should take no locale', function(){ + delete mock.nav.language + mock.run_sess() + expect(mock.win.session.locale).toBeDefined() + expect(mock.win.session.locale.lang).toBeFalsy() + expect(mock.win.session.locale.country).toBeFalsy() + }) + it('should take both locale and country', function(){ + mock.nav.language = 'EN-US' + mock.run_sess() + expect(mock.win.session.locale.country).toEqual('us') + expect(mock.win.session.locale.lang).toEqual('en') + }) + it('should take blank string locale', function(){ + mock.nav.language = '' + mock.run_sess() + expect(mock.win.session.locale).toBeDefined() + expect(mock.win.session.locale.country).toBeFalsy() + expect(mock.win.session.locale.lang).toBeFalsy() + }) + it('should not fail on invalid string', function(){ + mock.nav.language = 'foO1-bar-langauge-----!' + mock.run_sess() + expect(mock.win.session.locale).toBeTruthy() + }) +}); + +describe('alternate locale sources', function(){ + var mock = create_mock() + + beforeEach(function(){ + mock.nav = create_mock_navigator() + delete mock.nav.langauge; + }) + + it('should take prop language', function(){ + mock.nav.language = 'en-US' + }) + it('should take prop browserLanguage', function(){ + mock.nav.browserLanguage = 'en-US' + }) + it('should take prop systemLanguage', function(){ + mock.nav.systemLanguage = 'en-US' + }) + it('should take prop userLanguage', function(){ + mock.nav.userLanguage = 'en-US' + }) + afterEach(function(){ + mock.run_sess() + expect(mock.win.session.locale.country).toEqual('us') + expect(mock.win.session.locale.lang).toEqual('en') + }) +}); \ No newline at end of file