aboutsummaryrefslogtreecommitdiff
path: root/src/js/tests
diff options
context:
space:
mode:
authorGravatar Trevor Elliott <trevor@galois.com>2013-06-07 11:59:57 -0700
committerGravatar Trevor Elliott <trevor@galois.com>2013-06-07 11:59:57 -0700
commit8690d56ff0bbd1031e8cc6788dd2159aac6b7adb (patch)
treed573336305dc756f357f20dd986f77cb3ceafb9b /src/js/tests
parentf42930c2226d07482725b03ad522c106c0bdec8b (diff)
Reorganize the build system
* Move all javascript source to src/js * Update the test runner, etc. * Do less javascript compilation
Diffstat (limited to 'src/js/tests')
-rw-r--r--src/js/tests/.gitignore1
-rw-r--r--src/js/tests/PhantomJSJasmineRunner.html49
-rw-r--r--src/js/tests/README.md30
-rw-r--r--src/js/tests/SpecRunner.html41
-rw-r--r--src/js/tests/build.mk18
-rw-r--r--src/js/tests/jasmine/MIT.LICENSE20
-rw-r--r--src/js/tests/jasmine/boot.js104
-rw-r--r--src/js/tests/jasmine/jasmine-html.js329
-rw-r--r--src/js/tests/jasmine/jasmine.css54
-rw-r--r--src/js/tests/jasmine/jasmine.js2052
-rw-r--r--src/js/tests/jasmine/jasmine_favicon.pngbin0 -> 905 bytes
-rw-r--r--src/js/tests/mock-storage.js20
-rw-r--r--src/js/tests/specs/messenger.js96
-rw-r--r--src/js/tests/specs/prelude.js157
-rw-r--r--src/js/tests/specs/rules.js64
-rw-r--r--src/js/tests/specs/set.js24
-rw-r--r--src/js/tests/specs/settings.js87
-rw-r--r--src/js/tests/specs/state.js29
-rw-r--r--src/js/tests/specs/utils.js20
19 files changed, 3195 insertions, 0 deletions
diff --git a/src/js/tests/.gitignore b/src/js/tests/.gitignore
new file mode 100644
index 0000000..a9a1bd3
--- /dev/null
+++ b/src/js/tests/.gitignore
@@ -0,0 +1 @@
+reports/
diff --git a/src/js/tests/PhantomJSJasmineRunner.html b/src/js/tests/PhantomJSJasmineRunner.html
new file mode 100644
index 0000000..e8e07e6
--- /dev/null
+++ b/src/js/tests/PhantomJSJasmineRunner.html
@@ -0,0 +1,49 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Jasmine Test Runner</title>
+ <link rel="stylesheet"
+ type="text/css" href="../lib/phantomjs_jasmine/jasmine-1.0.2/jasmine.css">
+ <script type="text/javascript"
+ src="../lib/phantomjs_jasmine/jasmine-1.0.2/jasmine.js"></script>
+ <script type="text/javascript"
+ src="../lib/phantomjs_jasmine/jasmine-1.0.2/jasmine-html.js"></script>
+ <script type="text/javascript"
+ src="../lib/phantomjs_jasmine/jasmine-reporters/jasmine.phantomjs-reporter.js"></script>
+
+ <!-- Dependencies -->
+ <script type="text/javascript" src="../lib/jquery/jquery-1.8.3.js"></script>
+ <script type="text/javascript" src="../lib/underscore.js"></script>
+ <script type="text/javascript" src="../lib/backbone.js"></script>
+ <script type="text/javascript" src="mock-storage.js"></script>
+
+ <!-- source files -->
+ <script type="text/javascript" src="../fiveui/js/set.js"></script>
+ <script type="text/javascript" src="../fiveui/js/utils.js"></script>
+ <script type="text/javascript" src="../fiveui/js/chan.js"></script>
+ <script type="text/javascript" src="../fiveui/js/rules.js"></script>
+ <script type="text/javascript" src="../fiveui/js/messenger.js"></script>
+ <script type="text/javascript" src="../fiveui/js/url-pat.js"></script>
+ <script type="text/javascript" src="../fiveui/js/settings.js"></script>
+ <script type="text/javascript" src="../fiveui/js/state.js"></script>
+ <script type="text/javascript" src="../fiveui/injected/prelude.js"></script>
+
+ <!-- spec files -->
+ <script type="text/javascript" src="specs/set.js"></script>
+ <script type="text/javascript" src="specs/utils.js"></script>
+ <script type="text/javascript" src="specs/messenger.js"></script>
+ <script type="text/javascript" src="specs/rules.js"></script>
+ <script type="text/javascript" src="specs/settings.js"></script>
+ <script type="text/javascript" src="specs/state.js"></script>
+ <script type="text/javascript" src="specs/prelude.js"></script>
+</head>
+<body>
+
+<script type="text/javascript">
+ jasmine.getEnv().addReporter(new jasmine.TrivialReporter());
+ jasmine.getEnv().addReporter(new jasmine.PhantomJSReporter());
+ jasmine.getEnv().execute();
+</script>
+
+</body>
+</html>
diff --git a/src/js/tests/README.md b/src/js/tests/README.md
new file mode 100644
index 0000000..dbd8435
--- /dev/null
+++ b/src/js/tests/README.md
@@ -0,0 +1,30 @@
+Running Tests
+=============
+
+The Javascript unit tests for FiveUI can be run in one of two ways, manually in
+your browser, or in a headless context (better for automation/continuous
+integration).
+
+In Browser
+----------
+
+Start a local webserver at the FiveUI project root ($FIVEUI_ROOT) and load
+`contexts/data/tests/SpecRunner.html`.
+
+Headless
+--------
+
+Install [phantomjs](http://phantomjs.org/) on your system.
+
+ - Debian/Ubuntu: apt-get install phantomjs
+ - Fedora: yum install phantomjs
+ - Mac OSX: brew install phantomjs
+
+Run:
+
+ $ cd contexts/data
+ $ phantomjs lib/phantomjs_jasmine/phantomjs_jasminexml_runner.js \
+ tests/PhantomJSJasmineRunner.html tests/reports/
+
+XML test reports will be generated in
+`$FIVEUI_ROOT/contexts/data/tests/reports/`.
diff --git a/src/js/tests/SpecRunner.html b/src/js/tests/SpecRunner.html
new file mode 100644
index 0000000..3469bfd
--- /dev/null
+++ b/src/js/tests/SpecRunner.html
@@ -0,0 +1,41 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
+ <title>Jasmine Spec Runner v2.0.0-alpha</title>
+
+ <link rel="shortcut icon" type="image/png" href="jasmine/jasmine_favicon.png">
+ <link rel="stylesheet" type="text/css" href="jasmine/jasmine.css">
+
+ <script type="text/javascript" src="jasmine/jasmine.js"></script>
+ <script type="text/javascript" src="jasmine/jasmine-html.js"></script>
+ <script type="text/javascript" src="jasmine/boot.js"></script>
+
+ <script type="text/javascript" src="../lib/jquery/jquery-1.8.3.js"></script>
+ <script type="text/javascript" src="../lib/underscore.js"></script>
+ <script type="text/javascript" src="../lib/backbone.js"></script>
+ <script type="text/javascript" src="mock-storage.js"></script>
+
+ <script type="text/javascript" src="../fiveui/js/set.js"></script>
+ <script type="text/javascript" src="../fiveui/js/utils.js"></script>
+ <script type="text/javascript" src="../fiveui/js/chan.js"></script>
+ <script type="text/javascript" src="../fiveui/js/rules.js"></script>
+ <script type="text/javascript" src="../fiveui/js/messenger.js"></script>
+ <script type="text/javascript" src="../fiveui/js/url-pat.js"></script>
+ <script type="text/javascript" src="../fiveui/js/settings.js"></script>
+ <script type="text/javascript" src="../fiveui/js/state.js"></script>
+ <script type="text/javascript" src="../fiveui/injected/prelude.js"></script>
+
+ <script type="text/javascript" src="specs/set.js"></script>
+ <script type="text/javascript" src="specs/utils.js"></script>
+ <script type="text/javascript" src="specs/messenger.js"></script>
+ <script type="text/javascript" src="specs/rules.js"></script>
+ <script type="text/javascript" src="specs/settings.js"></script>
+ <script type="text/javascript" src="specs/state.js"></script>
+ <script type="text/javascript" src="specs/prelude.js"></script>
+
+</head>
+
+<body>
+</body>
+</html>
diff --git a/src/js/tests/build.mk b/src/js/tests/build.mk
new file mode 100644
index 0000000..b49109d
--- /dev/null
+++ b/src/js/tests/build.mk
@@ -0,0 +1,18 @@
+
+js-test-dir := $(path)
+
+clean::
+ $(RM) -r $(js-test-dir)/reports
+
+
+# Jasmine Specs ###############################################################
+
+ifneq "$(PHANTOM_EXE)" ""
+
+test: test-js
+test-js:
+ cd $(topdir)/src/js && $(PHANTOM_EXE) \
+ lib/phantomjs_jasmine/phantomjs_jasminexml_runner.js \
+ tests/PhantomJSJasmineRunner.html tests/reports/
+
+endif
diff --git a/src/js/tests/jasmine/MIT.LICENSE b/src/js/tests/jasmine/MIT.LICENSE
new file mode 100644
index 0000000..7c435ba
--- /dev/null
+++ b/src/js/tests/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/src/js/tests/jasmine/boot.js b/src/js/tests/jasmine/boot.js
new file mode 100644
index 0000000..aea8e00
--- /dev/null
+++ b/src/js/tests/jasmine/boot.js
@@ -0,0 +1,104 @@
+// Jasmine boot.js for browser runners - exposes external/global interface, builds the Jasmine environment and executes it.
+(function() {
+ var env = jasmine.getEnv();
+
+ var jasmineInterface = {
+ describe: function(description, specDefinitions) {
+ return env.describe(description, specDefinitions);
+ },
+
+ xdescribe: function(description, specDefinitions) {
+ return env.xdescribe(description, specDefinitions);
+ },
+
+ it: function(desc, func) {
+ return env.it(desc, func);
+ },
+
+ xit: function(desc, func) {
+ return env.xit(desc, func);
+ },
+
+ beforeEach: function(beforeEachFunction) {
+ return env.beforeEach(beforeEachFunction);
+ },
+
+ afterEach: function(afterEachFunction) {
+ return env.afterEach(afterEachFunction);
+ },
+
+ expect: function(actual) {
+ return env.expect(actual);
+ },
+
+ pending: function() {
+ return env.pending();
+ },
+
+ addMatchers: function(matchers) {
+ return env.addMatchers(matchers);
+ },
+
+ spyOn: function(obj, methodName) {
+ return env.spyOn(obj, methodName);
+ },
+
+ clock: env.clock,
+ setTimeout: env.clock.setTimeout,
+ clearTimeout: env.clock.clearTimeout,
+ setInterval: env.clock.setInterval,
+ clearInterval: env.clock.clearInterval,
+
+ jsApiReporter: new jasmine.JsApiReporter(jasmine)
+ };
+
+ if (typeof window == "undefined" && typeof exports == "object") {
+ extend(exports, jasmineInterface);
+ } else {
+ extend(window, jasmineInterface);
+ }
+
+ var queryString = new jasmine.QueryString({
+ getWindowLocation: function() { return window.location; }
+ });
+
+ // TODO: move all of catching to raise so we don't break our brains
+ var catchingExceptions = queryString.getParam("catch");
+ env.catchExceptions(typeof catchingExceptions === "undefined" ? true : catchingExceptions);
+
+ var htmlReporter = new jasmine.HtmlReporter({
+ env: env,
+ queryString: queryString,
+ onRaiseExceptionsClick: function() { queryString.setParam("catch", !env.catchingExceptions()); },
+ getContainer: function() { return document.body; },
+ createElement: function() { return document.createElement.apply(document, arguments); },
+ createTextNode: function() { return document.createTextNode.apply(document, arguments); }
+ });
+
+ env.addReporter(jasmineInterface.jsApiReporter);
+ env.addReporter(htmlReporter);
+
+ var specFilter = new jasmine.HtmlSpecFilter({
+ filterString: function() { return queryString.getParam("spec"); }
+ });
+
+ env.specFilter = function(spec) {
+ return specFilter.matches(spec.getFullName());
+ };
+
+ var currentWindowOnload = window.onload;
+
+ window.onload = function() {
+ if (currentWindowOnload) {
+ currentWindowOnload();
+ }
+ htmlReporter.initialize();
+ env.execute();
+ };
+
+ function extend(destination, source) {
+ for (var property in source) destination[property] = source[property];
+ return destination;
+ }
+
+}());
diff --git a/src/js/tests/jasmine/jasmine-html.js b/src/js/tests/jasmine/jasmine-html.js
new file mode 100644
index 0000000..dcc8913
--- /dev/null
+++ b/src/js/tests/jasmine/jasmine-html.js
@@ -0,0 +1,329 @@
+/*
+Copyright (c) 2008-2013 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.
+*/
+jasmine.HtmlReporter = function(options) {
+ var env = options.env || {},
+ getContainer = options.getContainer,
+ now = options.now || function() { return new Date().getTime();},
+ createElement = options.createElement,
+ createTextNode = options.createTextNode,
+ results = [],
+ startTime,
+ specsExecuted = 0,
+ failureCount = 0,
+ pendingSpecCount = 0,
+ htmlReporterMain,
+ symbols;
+
+ this.initialize = function() {
+ htmlReporterMain = createDom("div", {className: "html-reporter"},
+ createDom("div", {className: "banner"},
+ createDom("span", {className: "title"}, "Jasmine"),
+ createDom("span", {className: "version"}, jasmine.version)
+ ),
+ createDom("ul", {className: "symbol-summary"}),
+ createDom("div", {className: "alert"}),
+ createDom("div", {className: "results"},
+ createDom("div", {className: "failures"})
+ )
+ );
+ getContainer().appendChild(htmlReporterMain);
+
+ symbols = find(".symbol-summary")[0];
+ };
+
+ var totalSpecsDefined;
+ this.jasmineStarted = function(options) {
+ totalSpecsDefined = options.totalSpecsDefined || 0;
+ startTime = now();
+ };
+
+ var summary = createDom("div", {className: "summary"});
+
+ var topResults = new jasmine.ResultsNode({}, "", null),
+ currentParent = topResults;
+
+ this.suiteStarted = function(result) {
+ currentParent.addChild(result, "suite");
+ currentParent = currentParent.last();
+ };
+
+ this.suiteDone = function(result) {
+ if (currentParent == topResults) {
+ return;
+ }
+
+ currentParent = currentParent.parent;
+ };
+
+ this.specStarted = function(result) {
+ currentParent.addChild(result, "spec");
+ };
+
+ var failures = [];
+ this.specDone = function(result) {
+ if (result.status != "disabled") {
+ specsExecuted++;
+ }
+
+ symbols.appendChild(createDom("li", {
+ className: result.status,
+ id: "spec_" + result.id}
+ ));
+
+ if (result.status == "failed") {
+ failureCount++;
+
+ var failure =
+ createDom("div", {className: "spec-detail failed"},
+ createDom("a", {className: "description", title: result.fullName, href: specHref(result)}, result.fullName),
+ createDom("div", {className: "messages"})
+ );
+ var messages = failure.childNodes[1];
+
+ for (var i = 0; i < result.failedExpectations.length; i++) {
+ var expectation = result.failedExpectations[i];
+ messages.appendChild(createDom("div", {className: "result-message"}, expectation.message));
+ messages.appendChild(createDom("div", {className: "stack-trace"}, expectation.stack));
+ }
+
+ failures.push(failure);
+ }
+
+ if(result.status == "pending") {
+ pendingSpecCount++;
+ }
+ };
+
+ this.jasmineDone = function() {
+ var elapsed = now() - startTime;
+
+ var banner = find(".banner")[0];
+ banner.appendChild(createDom("span", {className: "duration"}, "finished in " + elapsed / 1000 + "s"));
+
+ var alert = find(".alert")[0];
+
+ alert.appendChild(createDom("span", { className: "exceptions" },
+ createDom("label", { className: "label", 'for': "raise-exceptions" }, "raise exceptions"),
+ createDom("input", {
+ className: "raise",
+ id: "raise-exceptions",
+ type: "checkbox"
+ })
+ ));
+ var checkbox = find("input")[0];
+
+ checkbox.checked = !env.catchingExceptions();
+ checkbox.onclick = options.onRaiseExceptionsClick;
+
+ if (specsExecuted < totalSpecsDefined) {
+ var skippedMessage = "Ran " + specsExecuted + " of " + totalSpecsDefined + " specs - run all";
+ alert.appendChild(
+ createDom("span", {className: "bar skipped"},
+ createDom("a", {href: "?", title: "Run all specs"}, skippedMessage)
+ )
+ );
+ }
+ var statusBarMessage = "" + pluralize("spec", specsExecuted) + ", " + pluralize("failure", failureCount);
+ if(pendingSpecCount) { statusBarMessage += ", " + pluralize("pending spec", pendingSpecCount); }
+
+ var statusBarClassName = "bar " + ((failureCount > 0) ? "failed" : "passed");
+ alert.appendChild(createDom("span", {className: statusBarClassName}, statusBarMessage));
+
+ var results = find(".results")[0];
+ results.appendChild(summary);
+
+ summaryList(topResults, summary);
+
+ function summaryList(resultsTree, domParent) {
+ var specListNode;
+ for (var i = 0; i < resultsTree.children.length; i++) {
+ var resultNode = resultsTree.children[i];
+ if (resultNode.type == "suite") {
+ var suiteListNode = createDom("ul", {className: "suite", id: "suite-" + resultNode.result.id},
+ createDom("li", {className: "suite-detail"},
+ createDom("a", {href: specHref(resultNode.result)}, resultNode.result.description)
+ )
+ );
+
+ summaryList(resultNode, suiteListNode);
+ domParent.appendChild(suiteListNode);
+ }
+ if (resultNode.type == "spec") {
+ if (domParent.getAttribute("class") != "specs") {
+ specListNode = createDom("ul", {className: "specs"});
+ domParent.appendChild(specListNode);
+ }
+ specListNode.appendChild(
+ createDom("li", {
+ className: resultNode.result.status,
+ id: "spec-" + resultNode.result.id
+ },
+ createDom("a", {href: specHref(resultNode.result)}, resultNode.result.description)
+ )
+ );
+ }
+ }
+ }
+
+ if (failures.length) {
+ alert.appendChild(
+ createDom('span', {className: "menu bar spec-list"},
+ createDom("span", {}, "Spec List | "),
+ createDom('a', {className: "failures-menu", href: "#"}, "Failures")));
+ alert.appendChild(
+ createDom('span', {className: "menu bar failure-list"},
+ createDom('a', {className: "spec-list-menu", href: "#"}, "Spec List"),
+ createDom("span", {}, " | Failures ")));
+
+ find(".failures-menu")[0].onclick = function() {
+ setMenuModeTo('failure-list');
+ };
+ find(".spec-list-menu")[0].onclick = function() {
+ setMenuModeTo('spec-list');
+ };
+
+ setMenuModeTo('failure-list');
+
+ var failureNode = find(".failures")[0];
+ for (var i = 0; i < failures.length; i++) {
+ failureNode.appendChild(failures[i]);
+ }
+ }
+ };
+
+ return this;
+
+ function find(selector) {
+ if (selector.match(/^\./)) {
+ var className = selector.substring(1);
+ return getContainer().getElementsByClassName(className);
+ } else {
+ return getContainer().getElementsByTagName(selector);
+ }
+ }
+
+ function createDom(type, attrs, childrenVarArgs) {
+ var el = createElement(type);
+
+ for (var i = 2; i < arguments.length; i++) {
+ var child = arguments[i];
+
+ if (typeof child === 'string') {
+ el.appendChild(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;
+ }
+
+ function pluralize(singular, count) {
+ var word = (count == 1 ? singular : singular + "s");
+
+ return "" + count + " " + word;
+ }
+
+ function specHref(result) {
+ return "?spec=" + encodeURIComponent(result.fullName);
+ }
+
+ function setMenuModeTo(mode) {
+ htmlReporterMain.setAttribute("class", "html-reporter " + mode);
+ }
+};
+jasmine.HtmlSpecFilter = function(options) {
+ var filterPattern = new RegExp(options && options.filterString());
+
+ this.matches = function(specName) {
+ return filterPattern.test(specName);
+ };
+};
+jasmine.ResultsNode = function(result, type, parent) {
+ this.result = result;
+ this.type = type;
+ this.parent = parent;
+
+ this.children = [];
+
+ this.addChild = function(result, type) {
+ this.children.push(new jasmine.ResultsNode(result, type, this));
+ };
+
+ this.last = function() {
+ return this.children[this.children.length-1];
+ };
+};
+jasmine.QueryString = function(options) {
+
+ this.setParam = function(key, value) {
+ var paramMap = queryStringToParamMap();
+ paramMap[key] = value;
+ options.getWindowLocation().search = toQueryString(paramMap);
+ };
+
+ this.getParam = function(key) {
+ return queryStringToParamMap()[key];
+ };
+
+ return this;
+
+ function toQueryString(paramMap) {
+ var qStrPairs = [];
+ for (var prop in paramMap) {
+ qStrPairs.push(encodeURIComponent(prop) + "=" + encodeURIComponent(paramMap[prop]));
+ }
+ return "?" + qStrPairs.join('&');
+ }
+
+ function queryStringToParamMap() {
+ var paramStr = options.getWindowLocation().search.substring(1),
+ params = [],
+ paramMap = {};
+
+ if (paramStr.length > 0) {
+ params = paramStr.split('&');
+ for (var i = 0; i < params.length; i++) {
+ var p = params[i].split('=');
+ var value = decodeURIComponent(p[1]);
+ if (value === "true" || value === "false") {
+ value = JSON.parse(value);
+ }
+ paramMap[decodeURIComponent(p[0])] = value;
+ }
+ }
+
+ return paramMap;
+ }
+
+}; \ No newline at end of file
diff --git a/src/js/tests/jasmine/jasmine.css b/src/js/tests/jasmine/jasmine.css
new file mode 100644
index 0000000..0a0ce80
--- /dev/null
+++ b/src/js/tests/jasmine/jasmine.css
@@ -0,0 +1,54 @@
+body { background-color: #eeeeee; padding: 0; margin: 5px; overflow-y: scroll; }
+
+.html-reporter { font-size: 11px; font-family: Monaco, "Lucida Console", monospace; line-height: 14px; color: #333333; }
+.html-reporter a { text-decoration: none; }
+.html-reporter a:hover { text-decoration: underline; }
+.html-reporter p, .html-reporter h1, .html-reporter h2, .html-reporter h3, .html-reporter h4, .html-reporter h5, .html-reporter h6 { margin: 0; line-height: 14px; }
+.html-reporter .banner, .html-reporter .symbol-summary, .html-reporter .summary, .html-reporter .result-message, .html-reporter .spec .description, .html-reporter .spec-detail .description, .html-reporter .alert .bar, .html-reporter .stack-trace { padding-left: 9px; padding-right: 9px; }
+.html-reporter .banner .version { margin-left: 14px; }
+.html-reporter #jasmine_content { position: fixed; right: 100%; }
+.html-reporter .version { color: #aaaaaa; }
+.html-reporter .banner { margin-top: 14px; }
+.html-reporter .duration { color: #aaaaaa; float: right; }
+.html-reporter .symbol-summary { overflow: hidden; *zoom: 1; margin: 14px 0; }
+.html-reporter .symbol-summary li { display: block; float: left; height: 7px; width: 14px; margin-bottom: 7px; font-size: 16px; }
+.html-reporter .symbol-summary li.passed { font-size: 14px; }
+.html-reporter .symbol-summary li.passed:before { color: #5e7d00; content: "\02022"; }
+.html-reporter .symbol-summary li.failed { line-height: 9px; }
+.html-reporter .symbol-summary li.failed:before { color: #b03911; content: "x"; font-weight: bold; margin-left: -1px; }
+.html-reporter .symbol-summary li.disabled { font-size: 14px; }
+.html-reporter .symbol-summary li.disabled:before { color: #bababa; content: "\02022"; }
+.html-reporter .symbol-summary li.pending { line-height: 17px; }
+.html-reporter .symbol-summary li.pending:before { color: #ba9d37; content: "*"; }
+.html-reporter .exceptions { color: #fff; float: right; margin-top: 5px; margin-right: 5px; }
+.html-reporter .bar { line-height: 28px; font-size: 14px; display: block; color: #eee; }
+.html-reporter .bar.failed { background-color: #b03911; }
+.html-reporter .bar.passed { background-color: #a6b779; }
+.html-reporter .bar.skipped { background-color: #bababa; }
+.html-reporter .bar.menu { background-color: #fff; color: #aaaaaa; }
+.html-reporter .bar.menu a { color: #333333; }
+.html-reporter .bar a { color: white; }
+.html-reporter.spec-list .bar.menu.failure-list, .html-reporter.spec-list .results .failures { display: none; }
+.html-reporter.failure-list .bar.menu.spec-list, .html-reporter.failure-list .summary { display: none; }
+.html-reporter .running-alert { background-color: #666666; }
+.html-reporter .results { margin-top: 14px; }
+.html-reporter.showDetails .summaryMenuItem { font-weight: normal; text-decoration: inherit; }
+.html-reporter.showDetails .summaryMenuItem:hover { text-decoration: underline; }
+.html-reporter.showDetails .detailsMenuItem { font-weight: bold; text-decoration: underline; }
+.html-reporter.showDetails .summary { display: none; }
+.html-reporter.showDetails #details { display: block; }
+.html-reporter .summaryMenuItem { font-weight: bold; text-decoration: underline; }
+.html-reporter .summary { margin-top: 14px; }
+.html-reporter .summary ul { list-style-type: none; margin-left: 14px; padding-top: 0; padding-left: 0; }
+.html-reporter .summary ul.suite { margin-top: 7px; margin-bottom: 7px; }
+.html-reporter .summary li.passed a { color: #5e7d00; }
+.html-reporter .summary li.failed a { color: #b03911; }
+.html-reporter .summary li.pending a { color: #ba9d37; }
+.html-reporter .description + .suite { margin-top: 0; }
+.html-reporter .suite { margin-top: 14px; }
+.html-reporter .suite a { color: #333333; }
+.html-reporter .failures .spec-detail { margin-bottom: 28px; }
+.html-reporter .failures .spec-detail .description { display: block; color: white; background-color: #b03911; }
+.html-reporter .result-message { padding-top: 14px; color: #333333; white-space: pre; }
+.html-reporter .result-message span.result { display: block; }
+.html-reporter .stack-trace { margin: 5px 0 0 0; max-height: 224px; overflow: auto; line-height: 18px; color: #666666; border: 1px solid #dddddd; background: white; white-space: pre; }
diff --git a/src/js/tests/jasmine/jasmine.js b/src/js/tests/jasmine/jasmine.js
new file mode 100644
index 0000000..32c580e
--- /dev/null
+++ b/src/js/tests/jasmine/jasmine.js
@@ -0,0 +1,2052 @@
+/*
+Copyright (c) 2008-2013 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.
+*/
+
+/**
+ * Top level namespace for Jasmine, a lightweight JavaScript BDD/spec/testing framework.
+ *
+ * @namespace
+ */
+var jasmine = {};
+
+// TODO: do we need this now that we have boot.js?
+if (typeof window == "undefined" && typeof exports == "object") {
+ exports.jasmine = jasmine;
+}
+
+/**
+ * @private
+ */
+jasmine.unimplementedMethod_ = function() {
+ throw new Error("unimplemented method");
+};
+
+/**
+ * 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;
+
+/**
+ * Maximum levels of nesting that will be included when an object is pretty-printed
+ */
+jasmine.MAX_PRETTY_PRINT_DEPTH = 40;
+
+/**
+ * Default timeout interval in milliseconds for waitsFor() blocks.
+ */
+jasmine.DEFAULT_TIMEOUT_INTERVAL = 5000;
+
+jasmine.getGlobal = function() {
+ function getGlobal() {
+ return this;
+ }
+
+ return getGlobal();
+};
+
+/**
+ * Getter for the Jasmine environment. Ensures one gets created
+ */
+jasmine.getEnv = function(options) {
+ var env = jasmine.currentEnv_ = jasmine.currentEnv_ || new jasmine.Env(options);
+ //jasmine. singletons in here (setTimeout blah blah).
+ 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);
+};
+
+/**
+ * Returns a matchable subset of a JSON object. For use in expectations when you don't care about all of the
+ * attributes on the object.
+ *
+ * @example
+ * // don't care about any other attributes than foo.
+ * expect(mySpy).toHaveBeenCalledWith(jasmine.objectContaining({foo: "bar"});
+ *
+ * @param sample {Object} sample
+ * @returns matchable object for the sample
+ */
+jasmine.objectContaining = function (sample) {
+ return new jasmine.Matchers.ObjectContaining(sample);
+};
+
+/**
+ * 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 <b>not</b> 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;
+};
+
+jasmine.util = {};
+
+jasmine.util.inherit = function(childClass, parentClass) {
+ 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, '&amp;')
+ .replace(/</g, '&lt;')
+ .replace(/>/g, '&gt;');
+};
+
+jasmine.util.argsToArray = function(args) {
+ var arrayOfArgs = [];
+ for (var i = 0; i < args.length; i++) arrayOfArgs.push(args[i]);
+ return arrayOfArgs;
+};
+
+jasmine.util.isUndefined = function(obj) {
+ return obj === void 0;
+};
+
+jasmine.Spec = function(attrs) {
+ this.encounteredExpectations = false;
+ this.expectationFactory = attrs.expectationFactory;
+ this.resultCallback = attrs.resultCallback || function() {};
+ this.id = attrs.id;
+ this.description = attrs.description || '';
+ this.fn = attrs.fn;
+ this.beforeFns = attrs.beforeFns || function() {};
+ this.afterFns = attrs.afterFns || function() {};
+ this.catchingExceptions = attrs.catchingExceptions;
+ this.onStart = attrs.onStart || function() {};
+ this.exceptionFormatter = attrs.exceptionFormatter || function() {};
+ this.getSpecName = attrs.getSpecName || function() { return ''; };
+ this.expectationResultFactory = attrs.expectationResultFactory || function() { };
+ this.queueRunner = attrs.queueRunner || function() {};
+ this.catchingExceptions = attrs.catchingExceptions || function() { return true; };
+
+ if (!this.fn) {
+ this.pend();
+ }
+
+ this.result = {
+ id: this.id,
+ description: this.description,
+ fullName: this.getFullName(),
+ status: this.status(),
+ failedExpectations: []
+ };
+};
+
+jasmine.Spec.prototype.addExpectationResult = function(passed, data) {
+ this.encounteredExpectations = true;
+ if (passed) {
+ return;
+ }
+ this.result.failedExpectations.push(this.expectationResultFactory(data));
+};
+
+jasmine.Spec.prototype.expect = function(actual) {
+ return this.expectationFactory(actual, this);
+};
+
+jasmine.Spec.prototype.execute = function(onComplete) {
+ var self = this;
+
+ this.onStart(this);
+
+ if (this.markedPending || this.disabled) {
+ complete();
+ return;
+ }
+
+ var befores = this.beforeFns() || [],
+ afters = this.afterFns() || [];
+ var allFns = befores.concat(this.fn).concat(afters);
+
+ this.queueRunner({
+ fns: allFns,
+ onException: function(e) {
+ if (jasmine.Spec.isPendingSpecException(e)) {
+ self.pend();
+ return;
+ }
+
+ self.addExpectationResult(false, {
+ matcherName: "",
+ passed: false,
+ expected: "",
+ actual: "",
+ error: e
+ });
+ },
+ onComplete: complete
+ });
+
+ function complete() {
+ self.result.status = self.status();
+ self.resultCallback(self.result);
+
+ if (onComplete) {
+ onComplete();
+ }
+ }
+};
+
+jasmine.Spec.prototype.disable = function() {
+ this.disabled = true;
+};
+
+jasmine.Spec.prototype.pend = function() {
+ this.markedPending = true;
+};
+
+jasmine.Spec.prototype.status = function() {
+ if (this.disabled) {
+ return 'disabled';
+ }
+
+ if (this.markedPending || !this.encounteredExpectations) {
+ return 'pending';
+ }
+
+ if (this.result.failedExpectations.length > 0) {
+ return 'failed';
+ } else {
+ return 'passed';
+ }
+};
+
+jasmine.Spec.prototype.getFullName = function() {
+ return this.getSpecName(this);
+};
+
+jasmine.Spec.pendingSpecExceptionMessage = "=> marked Pending";
+
+jasmine.Spec.isPendingSpecException = function(e) {
+ return e.toString().indexOf(jasmine.Spec.pendingSpecExceptionMessage) !== -1;
+};
+(function() {
+ jasmine.Env = function(options) {
+ options = options || {};
+ var self = this;
+ var global = options.global || jasmine.getGlobal();
+
+ var catchExceptions = true;
+
+ this.clock = new jasmine.Clock(global, new jasmine.DelayedFunctionScheduler());
+
+ this.jasmine = jasmine;
+ this.spies_ = [];
+ this.currentSpec = null;
+
+ this.reporter = new jasmine.ReportDispatcher([
+ "jasmineStarted",
+ "jasmineDone",
+ "suiteStarted",
+ "suiteDone",
+ "specStarted",
+ "specDone"
+ ]);
+
+ 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);
+
+ var expectationFactory = function(actual, spec) {
+ var expect = new (self.matchersClass)(self, actual, spec);
+ expect.not = new (self.matchersClass)(self, actual, spec, true);
+ return expect;
+ };
+
+ var specStarted = function(spec) {
+ self.currentSpec = spec;
+ self.reporter.specStarted(spec.result);
+ };
+
+ var beforeFns = function(currentSuite) {
+ return function() {
+ var befores = [];
+ for (var suite = currentSuite; suite; suite = suite.parentSuite) {
+ befores = befores.concat(suite.beforeFns);
+ }
+ return befores.reverse();
+ };
+ };
+
+ var afterFns = function(currentSuite) {
+ return function() {
+ var afters = [];
+ for (var suite = currentSuite; suite; suite = suite.parentSuite) {
+ afters = afters.concat(suite.afterFns);
+ }
+ return afters;
+ };
+ };
+
+ var specConstructor = jasmine.Spec;
+
+ var getSpecName = function(spec, currentSuite) {
+ return currentSuite.getFullName() + ' ' + spec.description + '.';
+ };
+
+ // TODO: we may just be able to pass in the fn instead of wrapping here
+ var buildExpectationResult = jasmine.buildExpectationResult,
+ exceptionFormatter = new jasmine.ExceptionFormatter(),
+ expectationResultFactory = function(attrs) {
+ attrs.messageFormatter = exceptionFormatter.message;
+ attrs.stackFormatter = exceptionFormatter.stack;
+
+ return buildExpectationResult(attrs);
+ };
+
+ // TODO: fix this naming, and here's where the value comes in
+ this.catchExceptions = function(value) {
+ catchExceptions = !!value;
+ return catchExceptions;
+ };
+
+ this.catchingExceptions = function() {
+ return catchExceptions;
+ };
+
+ this.catchException = function(e){
+ return jasmine.Spec.isPendingSpecException(e) || catchExceptions;
+ };
+
+ var maximumSpecCallbackDepth = 100;
+ var currentSpecCallbackDepth = 0;
+
+ function encourageGarbageCollection(fn) {
+ currentSpecCallbackDepth++;
+ if (currentSpecCallbackDepth > maximumSpecCallbackDepth) {
+ currentSpecCallbackDepth = 0;
+ global.setTimeout(fn, 0);
+ } else {
+ fn();
+ }
+ }
+
+ var queueRunnerFactory = function(options) {
+ options.catchException = self.catchException;
+ options.encourageGC = options.encourageGarbageCollection || encourageGarbageCollection;
+
+ new jasmine.QueueRunner(options).run(options.fns, 0);
+ };
+
+ var totalSpecsDefined = 0;
+ this.specFactory = function(description, fn, suite) {
+ totalSpecsDefined++;
+
+ var spec = new specConstructor({
+ id: self.nextSpecId(),
+ beforeFns: beforeFns(suite),
+ afterFns: afterFns(suite),
+ expectationFactory: expectationFactory,
+ exceptionFormatter: exceptionFormatter,
+ resultCallback: specResultCallback,
+ getSpecName: function(spec) {
+ return getSpecName(spec, suite);
+ },
+ onStart: specStarted,
+ description: description,
+ expectationResultFactory: expectationResultFactory,
+ queueRunner: queueRunnerFactory,
+ fn: fn
+ });
+
+ if (!self.specFilter(spec)) {
+ spec.disable();
+ }
+
+ return spec;
+
+ function specResultCallback(result) {
+ self.removeAllSpies();
+ self.clock.uninstall();
+ self.currentSpec = null;
+ self.reporter.specDone(result);
+ }
+ };
+
+ var suiteStarted = function(suite) {
+ self.reporter.suiteStarted(suite.result);
+ };
+
+ var suiteConstructor = jasmine.Suite;
+
+ this.topSuite = new jasmine.Suite({
+ env: this,
+ id: this.nextSuiteId(),
+ description: 'Jasmine__TopLevel__Suite',
+ queueRunner: queueRunnerFactory,
+ completeCallback: function() {}, // TODO - hook this up
+ resultCallback: function() {} // TODO - hook this up
+ });
+ this.currentSuite = this.topSuite;
+
+ this.suiteFactory = function(description) {
+ return new suiteConstructor({
+ env: self,
+ id: self.nextSuiteId(),
+ description: description,
+ parentSuite: self.currentSuite,
+ queueRunner: queueRunnerFactory,
+ onStart: suiteStarted,
+ resultCallback: function(attrs) {
+ self.reporter.suiteDone(attrs);
+ }
+ });
+ };
+
+ this.execute = function() {
+ this.reporter.jasmineStarted({
+ totalSpecsDefined: totalSpecsDefined
+ });
+ this.topSuite.execute(this.reporter.jasmineDone);
+ };
+ };
+
+ //TODO: shim Spec addMatchers behavior into Env. Should be rewritten to remove globals, etc.
+ jasmine.Env.prototype.addMatchers = function(matchersPrototype) {
+ var parent = this.matchersClass;
+ var newMatchersClass = function() {
+ parent.apply(this, arguments);
+ };
+ jasmine.util.inherit(newMatchersClass, parent);
+ jasmine.Matchers.wrapInto_(matchersPrototype, newMatchersClass);
+ this.matchersClass = newMatchersClass;
+ };
+
+ jasmine.Env.prototype.version = function() {
+ return jasmine.version;
+ };
+
+ jasmine.Env.prototype.expect = function(actual) {
+ return this.currentSpec.expect(actual);
+ };
+
+ jasmine.Env.prototype.spyOn = function(obj, methodName) {
+ if (jasmine.util.isUndefined(obj)) {
+ throw "spyOn could not find an object to spy upon for " + methodName + "()";
+ }
+
+ if (jasmine.util.isUndefined(obj[methodName])) {
+ throw methodName + '() method does not exist';
+ }
+
+ if (obj[methodName] && obj[methodName].isSpy) {
+ //TODO?: should this return the current spy? Downside: may cause user confusion about spy state
+ 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;
+ };
+
+ // TODO: move this to closure
+ jasmine.Env.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_ = [];
+ };
+
+ // TODO: move this to closure
+ jasmine.Env.prototype.versionString = function() {
+ console.log("DEPRECATED == use jasmine.version");
+ return jasmine.version;
+ };
+
+ // TODO: move this to closure
+ jasmine.Env.prototype.nextSpecId = function() {
+ return this.nextSpecId_++;
+ };
+
+ // TODO: move this to closure
+ jasmine.Env.prototype.nextSuiteId = function() {
+ return this.nextSuiteId_++;
+ };
+
+ // TODO: move this to closure
+ jasmine.Env.prototype.addReporter = function(reporter) {
+ this.reporter.addReporter(reporter);
+ };
+
+ // TODO: move this to closure
+ jasmine.Env.prototype.describe = function(description, specDefinitions) {
+ var suite = this.suiteFactory(description, specDefinitions);
+
+ var parentSuite = this.currentSuite;
+ parentSuite.addSuite(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;
+ };
+
+ // TODO: move this to closure
+ jasmine.Env.prototype.xdescribe = function(description, specDefinitions) {
+ var suite = this.describe(description, specDefinitions);
+ suite.disable();
+ return suite;
+ };
+
+ // TODO: move this to closure
+ jasmine.Env.prototype.it = function(description, fn) {
+ var spec = this.specFactory(description, fn, this.currentSuite);
+ this.currentSuite.addSpec(spec);
+ return spec;
+ };
+
+ // TODO: move this to closure
+ jasmine.Env.prototype.xit = function(description, fn) {
+ var spec = this.it(description, fn);
+ spec.pend();
+ return spec;
+ };
+
+ // TODO: move this to closure
+ jasmine.Env.prototype.beforeEach = function(beforeEachFunction) {
+ this.currentSuite.beforeEach(beforeEachFunction);
+ };
+
+ // TODO: move this to closure
+ jasmine.Env.prototype.afterEach = function(afterEachFunction) {
+ this.currentSuite.afterEach(afterEachFunction);
+ };
+
+ // TODO: move this to closure
+ jasmine.Env.prototype.pending = function() {
+ throw new Error(jasmine.Spec.pendingSpecExceptionMessage);
+ };
+
+ // TODO: Still needed?
+ jasmine.Env.prototype.currentRunner = function() {
+ return this.topSuite;
+ };
+
+ jasmine.Env.prototype.compareRegExps_ = function(a, b, mismatchKeys, mismatchValues) {
+ if (a.source != b.source)
+ mismatchValues.push("expected pattern /" + b.source + "/ is not equal to the pattern /" + a.source + "/");
+
+ if (a.ignoreCase != b.ignoreCase)
+ mismatchValues.push("expected modifier i was" + (b.ignoreCase ? " " : " not ") + "set and does not equal the origin modifier");
+
+ if (a.global != b.global)
+ mismatchValues.push("expected modifier g was" + (b.global ? " " : " not ") + "set and does not equal the origin modifier");
+
+ if (a.multiline != b.multiline)
+ mismatchValues.push("expected modifier m was" + (b.multiline ? " " : " not ") + "set and does not equal the origin modifier");
+
+ if (a.sticky != b.sticky)
+ mismatchValues.push("expected modifier y was" + (b.sticky ? " " : " not ") + "set and does not equal the origin modifier");
+
+ return (mismatchValues.length === 0);
+ };
+
+ 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 && !jasmine.util.isUndefined(obj[keyName]);
+ };
+
+ 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 (!jasmine.util.isUndefined(result)) {
+ return result;
+ }
+ }
+
+ if (a === b) return true;
+
+ if (jasmine.util.isUndefined(a) || a === null || jasmine.util.isUndefined(b) || b === null) {
+ return (jasmine.util.isUndefined(a) && jasmine.util.isUndefined(b));
+ }
+
+ if (jasmine.isDomNode(a) && jasmine.isDomNode(b)) {
+ return a === b;
+ }
+
+ if (a instanceof Date && b instanceof Date) {
+ return a.getTime() == b.getTime();
+ }
+
+ if (a.jasmineMatches) {
+ return a.jasmineMatches(b);
+ }
+
+ if (b.jasmineMatches) {
+ return b.jasmineMatches(a);
+ }
+
+ if (a instanceof jasmine.Matchers.ObjectContaining) {
+ return a.matches(b);
+ }
+
+ if (b instanceof jasmine.Matchers.ObjectContaining) {
+ 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 (a instanceof RegExp && b instanceof RegExp) {
+ return this.compareRegExps_(a, b, mismatchKeys, mismatchValues);
+ }
+
+ 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);
+ };
+}());
+
+jasmine.JsApiReporter = function(jasmine) {
+ this.jasmine = jasmine || {};
+ this.started = false;
+ this.finished = false;
+
+ var status = 'loaded';
+
+ this.jasmineStarted = function() {
+ this.started = true;
+ status = 'started';
+ };
+
+ this.jasmineDone = function() {
+ this.finished = true;
+ status = 'done';
+ };
+
+ this.status = function() {
+ return status;
+ };
+
+ var suites = {};
+
+ this.suiteStarted = function(result) {
+ storeSuite(result);
+ };
+
+ this.suiteDone = function(result) {
+ storeSuite(result);
+ };
+
+ function storeSuite(result) {
+ suites[result.id] = result;
+ }
+
+ this.suites = function() {
+ return suites;
+ };
+
+ var specs = [];
+ this.specStarted = function(result) { };
+
+ this.specDone = function(result) {
+ specs.push(result);
+ };
+
+ this.specResults = function(index, length) {
+ return specs.slice(index, index + length);
+ };
+
+ this.specs = function() {
+ return specs;
+ };
+
+};
+jasmine.Clock = function(global, delayedFunctionScheduler) {
+ var self = this,
+ realTimingFunctions = {
+ setTimeout: global.setTimeout,
+ clearTimeout: global.clearTimeout,
+ setInterval: global.setInterval,
+ clearInterval: global.clearInterval
+ },
+ fakeTimingFunctions = {
+ setTimeout: setTimeout,
+ clearTimeout: clearTimeout,
+ setInterval: setInterval,
+ clearInterval: clearInterval
+ },
+ timer = realTimingFunctions,
+ installed = false;
+
+ self.install = function() {
+ installed = true;
+ timer = fakeTimingFunctions;
+ };
+
+ self.uninstall = function() {
+ delayedFunctionScheduler.reset();
+ installed = false;
+ timer = realTimingFunctions;
+ };
+
+ self.setTimeout = function(fn, delay, params) {
+ if (legacyIE()) {
+ if (arguments.length > 2) {
+ throw new Error("IE < 9 cannot support extra params to setTimeout without a polyfill");
+ }
+ return timer.setTimeout(fn, delay);
+ }
+ return timer.setTimeout.apply(null, arguments);
+ };
+
+ self.setInterval = function(fn, delay, params) {
+ if (legacyIE()) {
+ if (arguments.length > 2) {
+ throw new Error("IE < 9 cannot support extra params to setInterval without a polyfill");
+ }
+ return timer.setInterval(fn, delay);
+ }
+ return timer.setInterval.apply(null, arguments);
+ };
+
+ self.clearTimeout = function(id) {
+ return timer.clearTimeout(id);
+ };
+
+ self.clearInterval = function(id) {
+ return timer.clearInterval(id);
+ };
+
+ self.tick = function(millis) {
+ if (installed) {
+ delayedFunctionScheduler.tick(millis);
+ } else {
+ throw new Error("Mock clock is not installed, use jasmine.Clock.useMock()");
+ }
+ };
+
+ return self;
+
+ function legacyIE() {
+ //if these methods are polyfilled, apply will be present
+ //TODO: it may be difficult to load the polyfill before jasmine loads
+ //(env should be new-ed inside of onload)
+ return !(global.setTimeout || global.setInterval).apply;
+ }
+
+ function setTimeout(fn, delay) {
+ return delayedFunctionScheduler.scheduleFunction(fn, delay, argSlice(arguments, 2));
+ }
+
+ function clearTimeout(id) {
+ return delayedFunctionScheduler.removeFunctionWithId(id);
+ }
+
+ function setInterval(fn, interval) {
+ return delayedFunctionScheduler.scheduleFunction(fn, interval, argSlice(arguments, 2), true);
+ }
+
+ function clearInterval(id) {
+ return delayedFunctionScheduler.removeFunctionWithId(id);
+ }
+
+ function argSlice(argsObj, n) {
+ return Array.prototype.slice.call(argsObj, 2);
+ }
+};
+
+jasmine.DelayedFunctionScheduler = function() {
+ var self = this;
+ var scheduledFunctions = {};
+ var currentTime = 0;
+ var delayedFnCount = 0;
+
+ self.tick = function(millis) {
+ runFunctionsWithinRange(currentTime, currentTime + millis);
+ currentTime = currentTime + millis;
+ };
+
+ self.scheduleFunction = function(funcToCall, millis, params, recurring, timeoutKey, runAtMillis) {
+ timeoutKey = timeoutKey || ++delayedFnCount;
+ runAtMillis = runAtMillis || (currentTime + millis);
+ scheduledFunctions[timeoutKey] = {
+ runAtMillis: runAtMillis,
+ funcToCall: funcToCall,
+ recurring: recurring,
+ params: params,
+ timeoutKey: timeoutKey,
+ millis: millis
+ };
+ return timeoutKey;
+ };
+
+ self.removeFunctionWithId = function(timeoutKey) {
+ delete scheduledFunctions[timeoutKey];
+ };
+
+ self.reset = function() {
+ currentTime = 0;
+ scheduledFunctions = {};
+ delayedFnCount = 0;
+ };
+
+ return self;
+
+
+ //finds/dupes functions within range and removes them.
+ function functionsWithinRange(startMillis, endMillis) {
+ var fnsToRun = [];
+ for (var timeoutKey in scheduledFunctions) {
+ var scheduledFunc = scheduledFunctions[timeoutKey];
+ if (scheduledFunc &&
+ scheduledFunc.runAtMillis >= startMillis &&
+ scheduledFunc.runAtMillis <= endMillis) {
+ //remove fn -- we'll reschedule later if it is recurring.
+ self.removeFunctionWithId(timeoutKey);
+ if (!scheduledFunc.recurring) {
+ fnsToRun.push(scheduledFunc); // schedules each function only once
+ } else {
+ fnsToRun.push(buildNthInstanceOf(scheduledFunc, 0));
+ var additionalTimesFnRunsInRange =
+ Math.floor((endMillis - scheduledFunc.runAtMillis) / scheduledFunc.millis);
+ for (var i = 0; i < additionalTimesFnRunsInRange; i++) {
+ fnsToRun.push(buildNthInstanceOf(scheduledFunc, i + 1));
+ }
+ reschedule(buildNthInstanceOf(scheduledFunc, additionalTimesFnRunsInRange));
+ }
+ }
+ }
+
+ return fnsToRun;
+ }
+
+ function buildNthInstanceOf(scheduledFunc, n) {
+ return {
+ runAtMillis: scheduledFunc.runAtMillis + (scheduledFunc.millis * n),
+ funcToCall: scheduledFunc.funcToCall,
+ params: scheduledFunc.params,
+ millis: scheduledFunc.millis,
+ recurring: scheduledFunc.recurring,
+ timeoutKey: scheduledFunc.timeoutKey
+ };
+ }
+
+ function reschedule(scheduledFn) {
+ self.scheduleFunction(scheduledFn.funcToCall,
+ scheduledFn.millis,
+ scheduledFn.params,
+ true,
+ scheduledFn.timeoutKey,
+ scheduledFn.runAtMillis + scheduledFn.millis);
+ }
+
+
+ function runFunctionsWithinRange(startMillis, endMillis) {
+ var funcsToRun = functionsWithinRange(startMillis, endMillis);
+ if (funcsToRun.length === 0) {
+ return;
+ }
+
+ funcsToRun.sort(function(a, b) {
+ return a.runAtMillis - b.runAtMillis;
+ });
+
+ for (var i = 0; i < funcsToRun.length; ++i) {
+ var funcToRun = funcsToRun[i];
+ funcToRun.funcToCall.apply(null, funcToRun.params);
+ }
+ }
+};
+
+jasmine.ExceptionFormatter = function() {
+ this.message = function(error) {
+ var message = error.name +
+ ': ' +
+ error.message;
+
+ if (error.fileName || error.sourceURL) {
+ message += " in " + (error.fileName || error.sourceURL);
+ }
+
+ if (error.line || error.lineNumber) {
+ message += " (line " + (error.line || error.lineNumber) + ")";
+ }
+
+ return message;
+ };
+
+ this.stack = function(error) {
+ return error ? error.stack : null;
+ };
+};
+//TODO: expectation result may make more sense as a presentation of an expectation.
+jasmine.buildExpectationResult = function(options) {
+ var messageFormatter = options.messageFormatter || function() {},
+ stackFormatter = options.stackFormatter || function() {};
+
+ return {
+ matcherName: options.matcherName,
+ expected: options.expected,
+ actual: options.actual,
+ message: message(),
+ stack: stack(),
+ passed: options.passed
+ };
+
+ function message() {
+ if (options.passed) {
+ return "Passed.";
+ } else if (options.message) {
+ return options.message;
+ } else if (options.error) {
+ return messageFormatter(options.error);
+ }
+ return "";
+ }
+
+ function stack() {
+ if (options.passed) {
+ return "";
+ }
+
+ var error = options.error;
+ if (!error) {
+ try {
+ throw new Error(message());
+ } catch (e) {
+ error = e;
+ }
+ }
+ return stackFormatter(error);
+ }
+};
+
+/**
+ * @constructor
+ * @param {jasmine.Env} env
+ * @param actual
+ * @param {jasmine.Spec} spec
+ */
+jasmine.Matchers = function(env, actual, spec, opt_isNot) {
+ //TODO: true dependency: equals, contains
+ this.env = env;
+ this.actual = actual;
+ this.spec = spec;
+ this.isNot = opt_isNot || 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!");
+};
+
+
+jasmine.Matchers.wrapInto_ = function(prototype, matchersClass) {
+ for (var methodName in prototype) {
+ 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;
+ }
+
+ 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 += ".";
+ }
+ }
+
+ this.spec.addExpectationResult(result, {
+ matcherName: matcherName,
+ passed: result,
+ expected: matcherArgs.length > 1 ? matcherArgs : matcherArgs[0],
+ actual: this.actual,
+ message: message
+ });
+ return void 0;
+ };
+};
+
+
+/**
+ * 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.toEqual() 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 !jasmine.util.isUndefined(this.actual);
+};
+
+/**
+ * Matcher that compares the actual to jasmine.undefined.
+ */
+jasmine.Matchers.prototype.toBeUndefined = function() {
+ return jasmine.util.isUndefined(this.actual);
+};
+
+/**
+ * Matcher that compares the actual to null.
+ */
+jasmine.Matchers.prototype.toBeNull = function() {
+ return (this.actual === null);
+};
+
+/**
+ * Matcher that compares the actual to NaN.
+ */
+jasmine.Matchers.prototype.toBeNaN = function() {
+ this.message = function() {
+ return [ "Expected " + jasmine.pp(this.actual) + " to be NaN." ];
+ };
+
+ return (this.actual !== this.actual);
+};
+
+/**
+ * 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() {
+ var invertedMessage = "Expected spy " + this.actual.identity + " not to have been called with " + jasmine.pp(expectedArgs) + " but it was.";
+ var positiveMessage = "";
+ if (this.actual.callCount === 0) {
+ positiveMessage = "Expected spy " + this.actual.identity + " to have been called with " + jasmine.pp(expectedArgs) + " but it was never called.";
+ } else {
+ positiveMessage = "Expected spy " + this.actual.identity + " to have been called with " + jasmine.pp(expectedArgs) + " but actual calls were " + jasmine.pp(this.actual.argsForCall).replace(/^\[ | \]$/g, '');
+ }
+ return [positiveMessage, invertedMessage];
+ };
+
+ 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.toContain() 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, as number of decimal places
+ */
+jasmine.Matchers.prototype.toBeCloseTo = function(expected, precision) {
+ if (precision !== 0) {
+ precision = precision || 2;
+ }
+ return Math.abs(expected - this.actual) < (Math.pow(10, -precision) / 2);
+};
+
+/**
+ * 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 = (jasmine.util.isUndefined(expected) || this.env.equals_(exception.message || exception, expected.message || expected));
+ }
+
+ var not = this.isNot ? "not " : "";
+
+ this.message = function() {
+ if (exception && (jasmine.util.isUndefined(expected) || !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.jasmineMatches = 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.jasmineToString = function() {
+ return '<jasmine.any(' + this.expectedClass + ')>';
+};
+
+jasmine.Matchers.ObjectContaining = function(sample) {
+ this.sample = sample;
+};
+
+jasmine.Matchers.ObjectContaining.prototype.jasmineMatches = function(other, mismatchKeys, mismatchValues) {
+ mismatchKeys = mismatchKeys || [];
+ mismatchValues = mismatchValues || [];
+
+ var env = jasmine.getEnv();
+
+ var hasKey = function(obj, keyName) {
+ return obj !== null && !jasmine.util.isUndefined(obj[keyName]);
+ };
+
+ for (var property in this.sample) {
+ if (!hasKey(other, property) && hasKey(this.sample, property)) {
+ mismatchKeys.push("expected has key '" + property + "', but missing from actual.");
+ }
+ else if (!env.equals_(this.sample[property], other[property], mismatchKeys, mismatchValues)) {
+ mismatchValues.push("'" + property + "' was '" + (other[property] ? jasmine.util.htmlEscape(other[property].toString()) : other[property]) + "' in expected, but was '" + (this.sample[property] ? jasmine.util.htmlEscape(this.sample[property].toString()) : this.sample[property]) + "' in actual.");
+ }
+ }
+
+ return (mismatchKeys.length === 0 && mismatchValues.length === 0);
+};
+
+jasmine.Matchers.ObjectContaining.prototype.jasmineToString = function() {
+ return "<jasmine.objectContaining(" + jasmine.pp(this.sample) + ")>";
+};
+
+/**
+ * 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) {
+ this.ppNestLevel_++;
+ try {
+ if (jasmine.util.isUndefined(value)) {
+ this.emitScalar('undefined');
+ } else if (value === null) {
+ this.emitScalar('null');
+ } else if (value === jasmine.getGlobal()) {
+ this.emitScalar('<global>');
+ } else if (value.jasmineToString) {
+ this.emitScalar(value.jasmineToString());
+ } 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('<circular reference: ' + (jasmine.isArray_(value) ? 'Array' : 'Object') + '>');
+ } 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 (!obj.hasOwnProperty(property)) continue;
+ if (property == '__Jasmine_been_here_before__') continue;
+ fn(property, obj.__lookupGetter__ ? (!jasmine.util.isUndefined(obj.__lookupGetter__(property)) &&
+ 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) {
+ if (this.ppNestLevel_ > jasmine.MAX_PRETTY_PRINT_DEPTH) {
+ this.append("Array");
+ return;
+ }
+
+ 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) {
+ if (this.ppNestLevel_ > jasmine.MAX_PRETTY_PRINT_DEPTH) {
+ this.append("Object");
+ return;
+ }
+
+ 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('<getter>');
+ } else {
+ self.format(obj[property]);
+ }
+ });
+
+ this.append(' }');
+};
+
+jasmine.StringPrettyPrinter.prototype.append = function(value) {
+ this.string += value;
+};
+
+jasmine.QueueRunner = function(attrs) {
+ this.fns = attrs.fns || [];
+ this.onComplete = attrs.onComplete || function() {};
+ this.encourageGC = attrs.encourageGC || function(fn) {fn();};
+ this.onException = attrs.onException || function() {};
+ this.catchException = attrs.catchException || function() { return true; };
+};
+
+jasmine.QueueRunner.prototype.execute = function() {
+ this.run(this.fns, 0);
+};
+
+jasmine.QueueRunner.prototype.run = function(fns, index) {
+ if (index >= fns.length) {
+ this.encourageGC(this.onComplete);
+ return;
+ }
+
+ var fn = fns[index];
+ var self = this;
+ if (fn.length > 0) {
+ attempt(function() { fn.call(self, function() { self.run(fns, index + 1); }); });
+ } else {
+ attempt(function() { fn.call(self); });
+ self.run(fns, index + 1);
+ }
+
+ function attempt(fn) {
+ try {
+ fn();
+ } catch (e) {
+ self.onException(e);
+ if (!self.catchException(e)) {
+ //TODO: set a var when we catch an exception and
+ //use a finally block to close the loop in a nice way..
+ throw e;
+ }
+ }
+ }
+};
+
+jasmine.ReportDispatcher = function(methods) {
+
+ var dispatchedMethods = methods || [];
+
+ for (var i = 0; i < dispatchedMethods.length; i++) {
+ var method = dispatchedMethods[i];
+ this[method] = function(m) {
+ return function() {
+ dispatch(m, arguments);
+ };
+ }(method);
+ }
+
+ var reporters = [];
+
+ this.addReporter = function(reporter) {
+ reporters.push(reporter);
+ };
+
+ return this;
+
+ function dispatch(method, args) {
+ for (var i = 0; i < reporters.length; i++) {
+ var reporter = reporters[i];
+ if (reporter[method]) {
+ reporter[method].apply(reporter, args);
+ }
+ }
+ }
+};
+jasmine.Suite = function(attrs) {
+ this.env = attrs.env;
+ this.id = attrs.id;
+ this.parentSuite = attrs.parentSuite;
+ this.description = attrs.description;
+ this.onStart = attrs.onStart || function() {};
+ this.completeCallback = attrs.completeCallback || function() {};
+ this.resultCallback = attrs.resultCallback || function() {};
+ this.encourageGC = attrs.encourageGC || function(fn) {fn();};
+
+ this.beforeFns = [];
+ this.afterFns = [];
+ this.queueRunner = attrs.queueRunner || function() {};
+ this.disabled = false;
+
+ this.children_ = []; // TODO: rename
+ this.suites = []; // TODO: needed?
+ this.specs = []; // TODO: needed?
+
+ this.result = {
+ id: this.id,
+ status: this.disabled ? 'disabled' : '',
+ description: this.description,
+ fullName: this.getFullName()
+ };
+};
+
+jasmine.Suite.prototype.getFullName = function() {
+ var fullName = this.description;
+ for (var parentSuite = this.parentSuite; parentSuite; parentSuite = parentSuite.parentSuite) {
+ if (parentSuite.parentSuite) {
+ fullName = parentSuite.description + ' ' + fullName;
+ }
+ }
+ return fullName;
+};
+
+jasmine.Suite.prototype.disable = function() {
+ this.disabled = true;
+};
+
+jasmine.Suite.prototype.beforeEach = function(fn) {
+ this.beforeFns.unshift(fn);
+};
+
+jasmine.Suite.prototype.afterEach = function(fn) {
+ this.afterFns.unshift(fn);
+};
+
+jasmine.Suite.prototype.addSpec = function(spec) {
+ this.children_.push(spec);
+ this.specs.push(spec); // TODO: needed?
+};
+
+jasmine.Suite.prototype.addSuite = function(suite) {
+ suite.parentSuite = this;
+ this.children_.push(suite);
+ this.suites.push(suite); // TODO: needed?
+};
+
+jasmine.Suite.prototype.children = function() {
+ return this.children_;
+};
+
+jasmine.Suite.prototype.execute = function(onComplete) {
+ var self = this;
+ if (this.disabled) {
+ complete();
+ return;
+ }
+
+ var allFns = [],
+ children = this.children_;
+
+ for (var i = 0; i < children.length; i++) {
+ allFns.push(wrapChild(children[i]));
+ }
+
+ this.onStart(this);
+
+ this.queueRunner({
+ fns: allFns,
+ onComplete: complete
+ });
+
+ function complete() {
+ self.resultCallback(self.result);
+
+ if (onComplete) {
+ onComplete();
+ }
+ }
+
+ function wrapChild(child) {
+ return function (done) {
+ child.execute(done);
+ };
+ }
+};
+
+jasmine.version = "2.0.0-alpha"; \ No newline at end of file
diff --git a/src/js/tests/jasmine/jasmine_favicon.png b/src/js/tests/jasmine/jasmine_favicon.png
new file mode 100644
index 0000000..218f3b4
--- /dev/null
+++ b/src/js/tests/jasmine/jasmine_favicon.png
Binary files differ
diff --git a/src/js/tests/mock-storage.js b/src/js/tests/mock-storage.js
new file mode 100644
index 0000000..71c667f
--- /dev/null
+++ b/src/js/tests/mock-storage.js
@@ -0,0 +1,20 @@
+
+var MockStorage = function() {
+ this.store = {};
+};
+
+_.extend(MockStorage.prototype, {
+
+ getItem: function(key) {
+ return this.store[key];
+ },
+
+ setItem: function(key,value) {
+ this.store[key] = value;
+ },
+
+ removeItem: function(key,value) {
+ delete this.store[key];
+ },
+
+});
diff --git a/src/js/tests/specs/messenger.js b/src/js/tests/specs/messenger.js
new file mode 100644
index 0000000..12bd9e1
--- /dev/null
+++ b/src/js/tests/specs/messenger.js
@@ -0,0 +1,96 @@
+
+describe('fiveui.Messenger', function() {
+
+ var chan1 = null;
+ var chan2 = null;
+
+ var m1 = null;
+ var m2 = null;
+
+
+ beforeEach(function() {
+ chan1 = new fiveui.Chan();
+ chan2 = new fiveui.Chan();
+
+ chan2.chan = chan1;
+ chan1.chan = chan2
+
+ m1 = new fiveui.Messenger(chan1);
+ m2 = new fiveui.Messenger(chan2);
+ });
+
+
+ afterEach(function() {
+ delete m1;
+ delete chan1;
+
+ delete m2;
+ delete chan2;
+ });
+
+
+ it('registers a simple callback', function() {
+ var got = [];
+
+ m1.register('count', function(n) {
+ got.push(n);
+ });
+
+ m2.send('count', 1);
+ m2.send('count', 2);
+
+ expect(got).toEqual([1,2]);
+ });
+
+
+ it('supports respond callbacks when sending', function() {
+ var m1got = [];
+ var m2got = [];
+
+ m1.register('count', function(n, respond){
+ m1got.push(n);
+ respond(n);
+ });
+
+ m2.send('count', 1, function(n) {
+ m2got.push(n);
+ });
+
+ expect(m1got.length).toBe(m2got.length);
+ });
+
+
+ it('doesn\'t invoke callbacks for null data', function() {
+ var m1got = [];
+
+ m1.register('count', function(n) {
+ m1got.push(n);
+ });
+
+ m2.send('count', null);
+
+ expect(m1got[0]).toBe(null);
+ });
+
+
+ it('is able to send rules', function() {
+ var ruleIn = new fiveui.Rule(42, 'testRule',
+ 'see: http://test.description/',
+ 'function() { console.log("fail"); }');
+
+ var got = [];
+ m1.register('rule', function(r){
+ got.push(r);
+ });
+
+ m2.send('rule', ruleIn);
+ expect(got.length).toBe(1);
+
+ var ruleOut = got[0];
+ expect(ruleIn.id).toBe(ruleOut.id);
+ expect(ruleIn.name).toBe(ruleOut.name);
+ expect(ruleIn.description).toBe(ruleOut.description);
+ expect(ruleIn.ruleStr).toBe(ruleOut.ruleStr);
+ });
+
+});
diff --git a/src/js/tests/specs/prelude.js b/src/js/tests/specs/prelude.js
new file mode 100644
index 0000000..31510e7
--- /dev/null
+++ b/src/js/tests/specs/prelude.js
@@ -0,0 +1,157 @@
+
+describe('prelude', function() {
+
+ var addTestSet = function(fn, tests) {
+ _.each(tests, function(test) {
+ it(test[0], function() {
+ expect(fn(test[1])).toEqual(test[2]);
+ });
+ });
+ };
+
+ addTestSet(fiveui.isString,[
+ // name , input , oracle
+ ['isString: undefined', undefined, false],
+ ['isString: null' , null, false],
+ ['isString: a string' , 'str', true]
+ ]);
+
+ addTestSet(fiveui.string.trim, [
+ ['string.trim leading space' , ' str', 'str'],
+ ['string.trim on null' , null, null],
+ ['string.trim trailing space' , 'str ', 'str'],
+ ['string.trim trailing tab' , 'str \t ', 'str'],
+ ['string.trim mixed & interior', ' this is a str \t ', 'this is a str']
+ ]);
+
+ addTestSet(fiveui.word.capitalized, [
+ ['capitalize: empty string' , '', false],
+ ['capitalize: a space' , ' ', false],
+ ['capitalize: whitespace' , '\t ', false],
+ ['capitalize: lowercase' , 'test', false],
+ ['capitalize: N-token' , 'a test', false],
+ ['capitalize: leading space' , ' test', false],
+ ['capitalize: 1-token - leading cap' , 'Test', true],
+ ['capitalize: N-token - leading cap' , 'A test', true],
+ ['capitalize: N-token - one cap' , 'this is a Test', false],
+ ['capitalize: N-token - one cap, punc' , 'this, is a Test.', false],
+ ['capitalize: N-token - all leading caps' , 'This Test', true],
+ ['capitalize: all caps' , 'TEST', true],
+ ['capitalize: N-token - all caps' , 'A TEST', true]
+ ]);
+
+ addTestSet(fiveui.word.allCaps, [
+ ['allCaps: empty string' , '', false],
+ ['allCaps: a space' , ' ', false],
+ ['allCaps: whitespace' , '\t ', false],
+ ['allCaps: lowercase' , 'test', false],
+ ['allCaps: N-token' , 'a test', false],
+ ['allCaps: leading space' , ' test', false],
+ ['allCaps: all caps' , 'TEST', true],
+ ['allCaps: N-token - all caps' , 'A TEST', true],
+ ['allCaps: 1-token - leading cap' , 'Test', false],
+ ['allCaps: N-token - leading cap' , 'A test', false],
+ ['allCaps: N-token - one cap , punc' , 'this, is a Test.', false],
+ ['allCaps: N-token - all caps , punc' , 'THIS, IS A TEST.', true]
+ ]);
+
+ var testTokenize = function(spec) {
+ it(spec[0], function() {
+ var result = fiveui.string.tokens(spec[1]);
+ var oracle = spec[2];
+
+ expect(result.length).toBe(oracle.length);
+
+ _.each(result, function(r, i) {
+ expect(r).toEqual(oracle[i]);
+ })
+ });
+ };
+
+ _.each([
+ ['tokenize: empty string' , '', []],
+ ['tokenize: a space' , ' ', []],
+ ['tokenize: whitespace' , '\t ', []],
+ ['tokenize: lowercase' , 'test', ['test']],
+ ['tokenize: N-token' , 'a test', ['a', 'test']],
+ ['tokenize: N-token - more spaces', 'a test', ['a', 'test']],
+ ['tokenize: leading spaces' , ' test', ['test']],
+ ['tokenize: trailing spaces', 'test ', ['test']],
+ ['tokenize: N-token' , 'this is a test', ['this', 'is', 'a', 'test']],
+ ['tokenize: N-token - punc' , 'this, is a test.', ['this,', 'is', 'a', 'test.']]
+ ], testTokenize);
+
+ it('colorCheck returns a function', function() {
+ expect(typeof fiveui.color.colorCheck('', [])).toEqual('function');
+ });
+
+ addTestSet(fiveui.color.colorToHex, [
+ ['colorToHex: full white' , '#000000', '#000000'],
+ ['colorToHex: abreviated white 1' , '#0', '#000000'],
+ ['colorToHex: abreviated white 2' , '#00', '#000000'],
+ ['colorToHex: black' , '#FFFFFF', '#FFFFFF'],
+ ['colorToHex: abreviated black' , '#FF', '#FFFFFF'],
+ ['colorToHex: abreviated C7 grey' , '#C7', '#C7C7C7'],
+ ['colorToHex: rgb(0, 0, 0)' , 'rgb(0, 0, 0)', '#000000'],
+ ['colorToHex: rgb(255, 255, 255)' , 'rgb(255, 255, 255)', '#FFFFFF'],
+ ['colorToHex: rgb(222, 173, 190)' , 'rgb(222, 173, 190)', '#DEADBE'],
+ ['colorToHex: rgba(255, 255, 255, 100)', 'rgba(255, 255, 255, 100)', '#FFFFFF'] // alpha is ignored
+ ]);
+
+ var getFontTests = [
+ // CSS ID, Family, Weight, Size
+ ['#getFontTest1', 'Arial', 'normal', '12'],
+ ['#getFontTest2', 'Arial sans-serif', 'normal', '12'],
+ ['#getFontTest3', 'Arial', 'bold', '12'],
+ // this case deals with an unparsable font-size parameter, which yields an
+ // empty size field on the result structure.
+ ['#getFontTest4', 'Verdana', 'bold', '']
+ ];
+
+
+ var getFontTestsDom = jQuery(
+ '<div><p id="getFontTest1" style="font-family: Arial; font-weight: normal; font-size: 12px">FontTest1</p>'
+ +'<p id="getFontTest2" style="font-family: Arial sans-serif; font-weight: normal; font-size: 12px">FontTest2</p>'
+ +'<p id="getFontTest3" style="font-family: Arial; font-weight: bold; font-size: 12px">FontTest3</p>'
+ +'<p id="getFontTest4" style="font-family: Verdana; font-weight: bold; font-size: 12">FontTest4</p>'
+ +'</div>'
+ );
+
+ _.each(getFontTests, function (spec) {
+ it(spec[0], function() {
+ var jElt = getFontTestsDom.find(spec[0]);
+ var font = fiveui.font.getFont(jElt);
+ expect(font.family.indexOf(spec[1])).not.toBe(-1);
+ expect(font.weight.indexOf(spec[2])).not.toBe(-1);
+ expect(font.size.indexOf(spec[3])).not.toBe(-1);
+ });
+ });
+
+ var validateTests =
+ // name, allow, font, oracle
+ [ ['a:verdana-bold-10 f:verdana+sans-bold-10', {'Verdana':{'bold':[10]}},
+ {'family':'Verdana sans-serif', 'weight':'bold', 'size':'10'},
+ true ]
+ , ['a:verdana-normal-12 f:verdana+sans-bold-10', {'Verdana':{'normal':[12]}},
+ {'family':'Verdana sans-serif', 'weight':'bold', 'size':'10'},
+ false ]
+ , ['a:arial-normal-12 f:verdana+sans-bold-10', {'Arial':{'normal':[12]}},
+ {'family':'Verdana sans-serif', 'weight':'bold', 'size':'10'},
+ false ]
+ , ['a:verdana-normal-10,12,14 f:verdana-normal-14', {'Verdana':{'normal':[10, 12, 14]}},
+ {'family':'Verdana', 'weight':'normal', 'size':'14'},
+ true ]
+ , ['a:verdana-normal,bold-12 f:verdana-bold-12', {'Verdana':{'normal':[12], 'bold':[12]}},
+ {'family':'Verdana', 'weight':'bold', 'size':'12'},
+ true ]
+ , ['a:verdana,arial-normal-12 f:arial-normal-12', {'Verdana':{'normal':[12]}, 'Arial':{'normal':[12]}},
+ {'family':'Arial', 'weight':'normal', 'size':'12'},
+ false ]
+ ];
+ _.each(validateTests, function (spec) {
+ it(spec[0], function () {
+ expect(fiveui.font.validate(spec[1], spec[2])).toEqual(spec[3]);
+ });
+ });
+
+});
diff --git a/src/js/tests/specs/rules.js b/src/js/tests/specs/rules.js
new file mode 100644
index 0000000..b9db6b3
--- /dev/null
+++ b/src/js/tests/specs/rules.js
@@ -0,0 +1,64 @@
+
+describe('fiveui.Rules', function() {
+
+ it('round trips via JSON', function() {
+
+ var ruleIn = new fiveui.Rule(42, 'testRule',
+ 'see: http://test.description/',
+ 'function() { console.log("fail"); }');
+
+ var jsonRule = JSON.stringify(ruleIn);
+
+ var ruleOut = fiveui.Rule.fromJSON(JSON.parse(jsonRule));
+
+ expect(ruleOut.id).toBe(ruleIn.id);
+ expect(ruleOut.name).toBe(ruleIn.name);
+ expect(ruleOut.description).toBe(ruleIn.description);
+ expect(ruleOut.ruleStr).toBe(ruleIn.ruleStr);
+ });
+
+
+});
+
+
+describe('fiveui.RuleSet', function() {
+
+ it('round trips via JSON, without deps', function() {
+
+ var rule1 = new fiveui.Rule(42, 'r1', 'desc1', 'rule txt1');
+ var rule2 = new fiveui.Rule(43, 'r2', 'desc2', 'rule txt2');
+
+ var ruleSet = new fiveui.RuleSet(42, 'rule set', 'desc', '', [rule1, rule2]);
+
+ var jsonSet = JSON.stringify(ruleSet);
+ var restoredSet = fiveui.RuleSet.fromJSON(42, JSON.parse(jsonSet));
+
+ expect(restoredSet.id).toBe(ruleSet.id);
+ expect(restoredSet.name).toBe(ruleSet.name);
+ expect(restoredSet.description).toBe(ruleSet.description);
+ expect(restoredSet.source).toEqual(ruleSet.source);
+ expect(restoredSet.rules).toEqual(ruleSet.rules);
+ expect(restoredSet.dependencies.length).toBe(0);
+
+
+ });
+
+ it('round trips via JSON, with deps', function() {
+
+ var rule1 = new fiveui.Rule(42, 'r1', 'desc1', 'rule txt1');
+ var rule2 = new fiveui.Rule(43, 'r2', 'desc2', 'rule txt2');
+
+ var ruleSet = new fiveui.RuleSet(42, 'rule set', 'desc', [rule1, rule2],
+ '', ['dep1.js', 'dep2.js']);
+
+ var jsonSet = JSON.stringify(ruleSet);
+ var restoredSet = fiveui.RuleSet.fromJSON(42, JSON.parse(jsonSet));
+
+ expect(restoredSet.id).toBe(ruleSet.id);
+ expect(restoredSet.name).toBe(ruleSet.name);
+ expect(restoredSet.description).toBe(ruleSet.description);
+ expect(restoredSet.rules.length).toBe(ruleSet.rules.length);
+ expect(restoredSet.dependencies).toEqual(ruleSet.dependencies);
+ });
+
+});
diff --git a/src/js/tests/specs/set.js b/src/js/tests/specs/set.js
new file mode 100644
index 0000000..a0dfb71
--- /dev/null
+++ b/src/js/tests/specs/set.js
@@ -0,0 +1,24 @@
+
+describe('Set', function() {
+
+ it('tests membership', function() {
+ var set = new Set();
+ set.add(1);
+ expect(set.member(1)).toBe(true);
+ });
+
+ it('allows multiple inserts', function() {
+ var set = new Set();
+
+ set.add(1);
+ set.add(1);
+
+ expect(set.size()).toBe(1);
+ });
+
+ it('fails the membership test for things that aren\'t in the set', function() {
+ var set = new Set();
+ expect(set.member(1)).toBe(false);
+ });
+
+});
diff --git a/src/js/tests/specs/settings.js b/src/js/tests/specs/settings.js
new file mode 100644
index 0000000..a89b532
--- /dev/null
+++ b/src/js/tests/specs/settings.js
@@ -0,0 +1,87 @@
+
+describe('fiveui.Settings', function() {
+
+ var settings;
+
+ beforeEach(function() {
+ settings = new fiveui.Settings(new MockStorage());
+ });
+
+
+ it('doesn\'t hold on to removed keys', function() {
+ var key = 'key';
+ var value = 'value';
+ var id = settings.set(key, value);
+ settings.remove(key);
+
+ var result = settings.get(id);
+ expect(result).toBe(null);
+ });
+
+
+ it('round trips values through set/get', function() {
+ var key = 'key';
+ var value = 'value';
+
+ settings.set(key, value);
+ expect(settings.get(key)).toEqual(value);
+ });
+
+ it('round trips rules through addUrl', function() {
+ // somewhat random rule id
+ var ruleId = Math.floor(Math.random() * 101);
+ var urlPat = 'http://.*';
+ var urlId = settings.addUrl(urlPat, ruleId);
+ var result = settings.getUrlPat(urlId);
+
+ expect(result.regex).toBe(urlPat);
+ expect(result.rule_id).toBe(ruleId);
+ });
+
+ it('matches urls when there\s a valid pattern registered', function() {
+ var newId = 42;
+ settings.addUrl('http://.*', newId);
+ expect(settings.checkUrl('http://foo').rule_id).toBe(newId);
+ });
+
+ it('doesn\'t match urls when there are no patterns registered', function() {
+ expect(settings.checkUrl('http://foo')).toBe(null);
+ });
+
+ it('removes rules successfully', function() {
+ var obj = {
+ id: 17,
+ name: 'rs',
+ description: '',
+ rules: []
+ };
+
+ var rs = settings.addRuleSet(obj);
+ expect(rs).not.toBe(null);
+
+ var rsCount1 = settings.getRuleSets().length;
+ settings.remRuleSet(rs.id);
+ var rsCount2 = settings.getRuleSets().length;
+ expect(rsCount1).toEqual(rsCount2 + 1);
+
+ rs = settings.getRuleSet(rs.id);
+ expect(rs).toBe(null);
+ });
+
+ it('is unable to remove rules that are in use', function() {
+ var obj = {
+ id: 17,
+ name: 'rs',
+ description: '',
+ rules: []
+ };
+
+ var rs = settings.addRuleSet(obj);
+ expect(rs).not.toBe(null);
+
+ var urlPatId = settings.addUrl('*', rs.id);
+ rs = settings.getRuleSet(rs.id);
+ expect(rs).not.toBe(null);
+ });
+
+});
diff --git a/src/js/tests/specs/state.js b/src/js/tests/specs/state.js
new file mode 100644
index 0000000..65ff0b1
--- /dev/null
+++ b/src/js/tests/specs/state.js
@@ -0,0 +1,29 @@
+
+describe('fiveui.state', function() {
+
+ var storage, state;
+
+ beforeEach(function() {
+ storage = new MockStorage();
+ state = new fiveui.State(storage);
+ });
+
+ it('cannot retrieve a tab that doesn\'t exist', function() {
+ expect(storage.getItem(42)).toBe(undefined);
+ expect(state.getTabState(42)).toBe(null);
+ });
+
+ it('manages the window location', function() {
+ var testId = 42;
+ var winState = new fiveui.WinState(0,1,10,15);
+ var tabState = new fiveui.TabState(testId,winState);
+ var oracle = new fiveui.TabState(testId,new fiveui.WinState(0,1,10,15));
+
+ state.setTabState(tabState);
+ var result = state.getTabState(testId);
+ _.each(result,function(val,key) {
+ expect(val).toEqual(oracle[key]);
+ });
+ });
+
+});
diff --git a/src/js/tests/specs/utils.js b/src/js/tests/specs/utils.js
new file mode 100644
index 0000000..f4ed399
--- /dev/null
+++ b/src/js/tests/specs/utils.js
@@ -0,0 +1,20 @@
+
+describe('fiveui.utils', function() {
+
+ describe('fiveui.utils.getNewId', function() {
+
+ it('returns 0 when no elements are given', function() {
+ expect(fiveui.utils.getNewId([])).toBe(0);
+ });
+
+ it('returns 1, when 0 is given', function() {
+ expect(fiveui.utils.getNewId([0])).toBe(1);
+ });
+
+ it('ignores order when finding the maximum element', function() {
+ expect(fiveui.utils.getNewId([2,1])).toBe(3);
+ });
+
+ });
+
+});