summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorBrainDoctor <github.account@chrigel.net>2017-07-17 15:25:11 +0200
committerBrainDoctor <github.account@chrigel.net>2017-07-17 18:04:59 +0200
commit1fe557fe3d06f79f2f1c2db1684eeedccf2dc8d8 (patch)
tree368dff21b7604fd09a67b945284ae95c0b0b47fb
parent4b4355e4c1f45f2d049d1c90d78b6c452213e5db (diff)
Setup and docs for unit testing javascript in browser and node.js
-rw-r--r--.gitignore4
-rw-r--r--package.json12
-rw-r--r--tests/README.md131
-rw-r--r--tests/node.d/fronius.spec.js58
-rw-r--r--tests/web/easypiechart.spec.js58
-rw-r--r--tests/web/fixtures/easypiechart.creation.fixture1.html6
-rw-r--r--tests/web/karma.conf.js110
-rw-r--r--tests/web/lib/jasmine-jquery.js841
8 files changed, 1220 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
index 00c7d6d68f..f8c55f3590 100644
--- a/.gitignore
+++ b/.gitignore
@@ -68,6 +68,10 @@ web/gadget.xml
web/index_new.html
web/version.txt
+# related to karma/javascript/node
+node_modules/
+coverage/
+
system/netdata-lsb
system/netdata-openrc
system/netdata-init-d
diff --git a/package.json b/package.json
new file mode 100644
index 0000000000..f04fc8d276
--- /dev/null
+++ b/package.json
@@ -0,0 +1,12 @@
+{
+ "devDependencies": {
+ "jasmine": "^2.6.0",
+ "jasmine-core": "^2.6.4",
+ "jasmine-node": "^1.14.5",
+ "karma": "^1.7.0",
+ "karma-chrome-launcher": "^2.2.0",
+ "karma-coverage": "^1.1.1",
+ "karma-firefox-launcher": "^1.0.1",
+ "karma-jasmine": "^1.1.0"
+ }
+}
diff --git a/tests/README.md b/tests/README.md
new file mode 100644
index 0000000000..2a38efc136
--- /dev/null
+++ b/tests/README.md
@@ -0,0 +1,131 @@
+This readme is a manual on how to get started with unit testing on javascript and nodejs
+
+Original author: BrainDoctor (github), July 2017
+
+# Installation
+
+Tested on Linux Mint 18.2 Sara (Ubuntu/debian derivative)
+
+Make sure you are the user who is developer (permissions, except sudo ofc)
+
+```sh
+sudo apt-get install nodejs npm chromium-browser
+
+cd /path/to/your/netdata
+
+npm install
+```
+
+That should install the necessary node modules.
+
+Other browsers work too (Chrome, Firefox). However, only the Chromium Browser 59 has been tested for headless unit testing.
+
+## Versions
+
+The commands above leave me with the following versions (July 2017):
+
+ - nodejs: v4.2.6
+ - npm: 3.5.2
+ - chromium-browser: 59.0.3071.109
+ - WebStorm (optional): 2017.1.4
+
+# Configuration
+
+## NPM
+
+The dependencies are installed in `netdata/package.json`. If you install a new NPM module, it gets added here. Future developers just need to execute `npm install` and every dep gets added automatically.
+
+## Karma
+
+Karma configuration is in `tests/web/karma.conf.js`. Documentation is provided via comments.
+
+## WebStorm
+
+If you use the JetBrains WebStorm IDE, you can integrate the karma runtime.
+
+### for Karma (Client side testing)
+
+Headless Chromium:
+1. Run > Edit Configurations
+2. "+" > Karma
+3. - Name: Karma Headless Chromium
+ - Configuration file: /path/to/your/netdata/tests/web/karma.conf.js
+ - Browsers to start: ChromiumHeadless
+ - Node interpreter: /usr/bin/nodejs (MUST be absolute, NVM works too)
+ - Karma package: /path/to/your/netdata/node_modules/karma
+
+GUI Chromium is similar:
+1. Run > Edit Configurations
+2. "+" > Karma
+3. - Name: Karma Chromium
+ - Configuration file: /path/to/your/netdata/tests/web/karma.conf.js
+ - Browsers to start: Chromium
+ - Node interpreter: /usr/bin/nodejs (MUST be absolute, NVM works too)
+ - Karma package: /path/to/your/netdata/node_modules/karma
+
+You may add other browsers too (comma separated). With the "Browsers to start" field you can override any settings in karma.conf.js.
+
+Also it is recommended to install WebStorm IDE Extension/Addon to Chrome/Chromium for awesome debugging.
+
+### for node.d plugins (nodejs)
+
+1. Run > Edit Configurations
+2. "+" > Node.js
+3. - Name: Node.d plugins
+ - Node interpreter: /usr/bin/nodejs (MUST be absolute, NVM works too)
+ - JavaScript file: node_modules/jasmine-node/bin/jasmine-node
+ - Application parameters: --captureExceptions tests/node.d
+
+**ATTENTION**
+
+The jasmine-node npm package includes an outdated jasmine on version 1.3.1.
+
+I haven't figured out yet how to switch to newer 2.6 versions. So it may contain bugs or lacks features.
+
+# Running
+
+## In WebStorm
+
+Just run the configured run configurations and they produce nice test trees:
+
+
+## From CLI
+
+### Karma
+
+```sh
+cd /path/to/your/netdata
+
+nodejs ./node_modules/karma/bin/karma start tests/web/karma.conf.js --single-run=true --browsers=ChromiumHeadless
+```
+will start the karma server, start chromium in headless mode and exit.
+
+If a test fails, it produces even a stack trace:
+
+
+### Node.d plugins
+
+```sh
+cd /path/to/your/netdata
+
+nodejs node_modules/jasmine-node/bin/jasmine-node --captureExceptions tests/node.d
+```
+
+will run the tests in `tests/node.d` and produce a stacktrace too on error:
+
+
+## Coverage
+
+### Karma
+
+A nice HTML is produced from Karma which shows which code paths were executed. It is located somewhere in `/path/to/your/netdata/coverage/`
+
+### Node.d
+
+Apparently, jasmine-node can produce a junit report with the `--junitreport` flag. But that output was not very useful. Maybe it's configurable?
+
+## CI
+
+The karma and node.d runners can be integrated in Travis (AFAIK), but that is outside my ability.
+
+Note: Karma is for browser-testing. On a build server, no GUI or browser might by available, unless browsers support headless mode. \ No newline at end of file
diff --git a/tests/node.d/fronius.spec.js b/tests/node.d/fronius.spec.js
new file mode 100644
index 0000000000..204f7f6fe4
--- /dev/null
+++ b/tests/node.d/fronius.spec.js
@@ -0,0 +1,58 @@
+"use strict";
+// delete these comments if not needed anymore.
+
+var netdata = require("../../node.d/node_modules/netdata");
+var fronius = require("../../node.d/fronius.node");
+
+describe("fronius chart creation", function () {
+
+ beforeEach(function () {
+ netdata.options.DEBUG = true;
+ });
+
+ it("should return a basic chart definition", function () {
+ // act
+ var result = fronius.createBasicDimension("id", "name", 2);
+ // assert
+ expect(result.divisor).toBe(2);
+ expect(result.id).toBe("id");
+ expect(result.algorithm).toEqual("absolute");
+ expect(result.multiplier).toBe(1);
+ });
+
+ it("will fail", function () {
+ netdata.debug("test");
+
+ throw new Error("expected failure to test unit test runner");
+ });
+
+});
+
+describe("fronius data parsing", function () {
+
+ var service = netdata.service({
+ name: "fronius",
+ module: this
+ });
+
+ beforeEach(function () {
+ // change this to enable debug log
+ netdata.options.DEBUG = false;
+ });
+
+ it("should return a parsed value", function () {
+ // arrange
+ netdata.send = jasmine.createSpy("send");
+ // act
+ fronius.processResponse(service, createFakeResponse());
+ var result = netdata.send.calls[0].args[0];
+ // assert
+ expect(result).toContain("SET p_grid = -3431");
+ });
+
+ function createFakeResponse() {
+ // this is a faked JSON response from the server.
+ // Used with freeformatter.com/json-escape.html to escape the json and turn it into a string.
+ return "{\r\n\t\"Head\" : {\r\n\t\t\"RequestArguments\" : {},\r\n\t\t\"Status\" : {\r\n\t\t\t\"Code\" : 0,\r\n\t\t\t\"Reason\" : \"\",\r\n\t\t\t\"UserMessage\" : \"\"\r\n\t\t},\r\n\t\t\"Timestamp\" : \"2017-07-17T16:01:04+02:00\"\r\n\t},\r\n\t\"Body\" : {\r\n\t\t\"Data\" : {\r\n\t\t\t\"Site\" : {\r\n\t\t\t\t\"Mode\" : \"meter\",\r\n\t\t\t\t\"P_Grid\" : -3430.729923,\r\n\t\t\t\t\"P_Load\" : -910.270077,\r\n\t\t\t\t\"P_Akku\" : null,\r\n\t\t\t\t\"P_PV\" : 4341,\r\n\t\t\t\t\"rel_SelfConsumption\" : 20.969133,\r\n\t\t\t\t\"rel_Autonomy\" : 100,\r\n\t\t\t\t\"E_Day\" : 57230,\r\n\t\t\t\t\"E_Year\" : 6425915.5,\r\n\t\t\t\t\"E_Total\" : 15388710,\r\n\t\t\t\t\"Meter_Location\" : \"grid\"\r\n\t\t\t},\r\n\t\t\t\"Inverters\" : {\r\n\t\t\t\t\"1\" : {\r\n\t\t\t\t\t\"DT\" : 123,\r\n\t\t\t\t\t\"P\" : 4341,\r\n\t\t\t\t\t\"E_Day\" : 57230,\r\n\t\t\t\t\t\"E_Year\" : 6425915.5,\r\n\t\t\t\t\t\"E_Total\" : 15388710\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n}"
+ }
+});
diff --git a/tests/web/easypiechart.spec.js b/tests/web/easypiechart.spec.js
new file mode 100644
index 0000000000..3888de27cd
--- /dev/null
+++ b/tests/web/easypiechart.spec.js
@@ -0,0 +1,58 @@
+// delete these comments if not needed anymore.
+
+describe("percentage calculations for easy pie charts", function () {
+
+ // some easy functions to test. incomplete yet.
+ it('should return 50 if positive value between min and max', function () {
+ // act
+ var result = NETDATA.easypiechartPercentFromValueMinMax(1, 0, 2);
+ // assert
+ expect(result).toBe(50);
+ });
+
+ it('should return 0.1 if value is zero', function () {
+ // act
+ var result = NETDATA.easypiechartPercentFromValueMinMax(0, 0, 2);
+ // assert
+ expect(result).toBe(0.1);
+ });
+
+});
+
+
+// with xdescribe, this is skipped.
+// Delete the x to enable again and let it fail to test the build
+describe('creation of easy pie charts', function () {
+
+ beforeAll(function () {
+ // karma stores the loaded files relative to "base/".
+ // This command is needed to load HTML fixtures
+ jasmine.getFixtures().fixturesPath = "base/tests/web/fixtures";
+ });
+
+ it('should create new chart', function () {
+ // arrange
+ // Theoretically we can load some html. What about jquery? could this work?
+ // https://stackoverflow.com/questions/5337481/spying-on-jquery-selectors-in-jasmine
+ loadFixtures("easypiechart.creation.fixture1.html");
+
+ // for easy pie chart, we can fake the data result:
+ var data = {
+ result: [5]
+ };
+ // act
+ var result = NETDATA.easypiechartChartCreate(createState(), data);
+ // assert
+ expect(result).toBe(true);
+ });
+
+ function createState() {
+ // create a fake state with only needed properties? Spying? not figured it out yet...
+ return {
+ tmp: {
+
+ }
+ };
+ }
+
+}); \ No newline at end of file
diff --git a/tests/web/fixtures/easypiechart.creation.fixture1.html b/tests/web/fixtures/easypiechart.creation.fixture1.html
new file mode 100644
index 0000000000..f0f4eb777d
--- /dev/null
+++ b/tests/web/fixtures/easypiechart.creation.fixture1.html
@@ -0,0 +1,6 @@
+<div data-netdata="system.cpu"
+ data-chart-library="easypiechart"
+ data-width="5%"
+ data-height="20"
+ data-after="-30"
+></div> \ No newline at end of file
diff --git a/tests/web/karma.conf.js b/tests/web/karma.conf.js
new file mode 100644
index 0000000000..b3ee0943dc
--- /dev/null
+++ b/tests/web/karma.conf.js
@@ -0,0 +1,110 @@
+// Karma configuration
+// Generated on Sun Jul 16 2017 02:28:05 GMT+0200 (CEST)
+
+module.exports = function (config) {
+ config.set({
+
+ // base path that will be used to resolve all patterns (eg. files, exclude)
+ // this path should always resolve so that "." is the "netdata" root folder.
+ basePath: '../../',
+
+ // frameworks to use
+ // available frameworks: https://npmjs.org/browse/keyword/karma-adapter
+ frameworks: ['jasmine'],
+
+
+ // list of files / patterns to load in the browser
+ files: [
+ // order matters! load jquery libraries first
+ 'web/lib/jquery*.js',
+ // our jasmine libs and fixtures
+ 'tests/web/lib/*.js',
+ 'tests/web/fixtures/*.html',
+ // then bootstrap
+ 'web/lib/bootstrap*.js',
+ // then the rest
+ 'web/lib/perfect-scrollbar*.js',
+ 'web/lib/dygraph*.js',
+ 'web/lib/gauge*.js',
+ 'web/lib/morris*.js',
+ 'web/lib/raphael*.js',
+ 'web/lib/tableExport*.js',
+ 'web/lib/d3*.js',
+ 'web/lib/c3*.js',
+ // some CSS
+ 'web/css/*.css',
+ 'web/dashboard.css',
+ // our dashboard
+ 'web/dashboard.js',
+ // finally our test specs
+ 'tests/web/*.spec.js',
+ ],
+
+
+ // list of files to exclude
+ exclude: [],
+
+
+ // preprocess matching files before serving them to the browser
+ // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor
+ preprocessors: {
+ 'web/dashboard.js': ['coverage']
+ },
+
+
+ // test results reporter to use
+ // possible values: 'dots', 'progress'
+ // available reporters: https://npmjs.org/browse/keyword/karma-reporter
+ reporters: ['progress', 'coverage'],
+
+ // optionally, configure the reporter
+ coverageReporter: {
+ type : 'html',
+ dir : 'coverage/'
+ },
+
+ // web server port
+ port: 9876,
+
+
+ // enable / disable colors in the output (reporters and logs)
+ colors: true,
+
+
+ // level of logging
+ // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
+ logLevel: config.LOG_INFO,
+
+
+ // enable / disable watching file and executing tests whenever any file changes
+ autoWatch: false,
+ // not needed with WebStorm. Just hit Alt+Shift+R to rerun.
+
+ // start these browsers
+ // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
+ browsers: ['Chromium', 'ChromiumHeadless'],
+
+ customLaunchers: {
+ // Headless browsers could be useful for CI integration, if installed.
+ ChromiumHeadless: {
+ // needs Chrome/Chromium version >= 59
+ // see https://chromium.googlesource.com/chromium/src/+/lkgr/headless/README.md
+ base: "Chromium",
+ flags: [
+ "--headless",
+ "--disable-gpu",
+ // Without a remote debugging port, Chromium exits immediately.
+ "--remote-debugging-port=9222"
+ ]
+ }
+ },
+
+ // Continuous Integration mode
+ // if true, Karma captures browsers, runs the tests and exits
+ singleRun: false,
+
+ // Concurrency level
+ // how many browser should be started simultaneous
+ concurrency: Infinity
+ })
+};
diff --git a/tests/web/lib/jasmine-jquery.js b/tests/web/lib/jasmine-jquery.js
new file mode 100644
index 0000000000..6e4611c191
--- /dev/null
+++ b/tests/web/lib/jasmine-jquery.js
@@ -0,0 +1,841 @@
+/*!
+ Jasmine-jQuery: a set of jQuery helpers for Jasmine tests.
+
+ Version 2.1.1
+
+ https://github.com/velesin/jasmine-jquery
+
+ Copyright (c) 2010-2014 Wojciech Zawistowski, Travis Jeffery
+
+ Permission is hereby granted, free of charge, to any person obtaining
+ a copy of this software and associated documentation files (the
+ "Software"), to deal in the Software without restriction, including
+ without limitation the rights to use, copy, modify, merge, publish,
+ distribute, sublicense, and/or sell copies of the Software, and to
+ permit persons to whom the Software is furnished to do so, subject to
+ the following conditions:
+
+ The above copyright notice and this permission notice shall be
+ included in all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+(function (root, factory) {
+ if (typeof module !== 'undefined' && module.exports && typeof exports !== 'undefined') {
+ factory(root, root.jasmine, require('jquery'));
+ } else {
+ factory(root, root.jasmine, root.jQuery);
+ }
+}((function() {return this; })(), function (window, jasmine, $) { "use strict";
+
+ jasmine.spiedEventsKey = function (selector, eventName) {
+ return [$(selector).selector, eventName].toString()
+ }
+
+ jasmine.getFixtures = function () {
+ return jasmine.currentFixtures_ = jasmine.currentFixtures_ || new jasmine.Fixtures()
+ }
+
+ jasmine.getStyleFixtures = function () {
+ return jasmine.currentStyleFixtures_ = jasmine.currentStyleFixtures_ || new jasmine.StyleFixtures()
+ }
+
+ jasmine.Fixtures = function () {
+ this.containerId = 'jasmine-fixtures'
+ this.fixturesCache_ = {}
+ this.fixturesPath = 'spec/javascripts/fixtures'
+ }
+
+ jasmine.Fixtures.prototype.set = function (html) {
+ this.cleanUp()
+ return this.createContainer_(html)
+ }
+
+ jasmine.Fixtures.prototype.appendSet= function (html) {
+ this.addToContainer_(html)
+ }
+
+ jasmine.Fixtures.prototype.preload = function () {
+ this.read.apply(this, arguments)
+ }
+
+ jasmine.Fixtures.prototype.load = function () {
+ this.cleanUp()
+ this.createContainer_(this.read.apply(this, arguments))
+ }
+
+ jasmine.Fixtures.prototype.appendLoad = function () {
+ this.addToContainer_(this.read.apply(this, arguments))
+ }
+
+ jasmine.Fixtures.prototype.read = function () {
+ var htmlChunks = []
+ , fixtureUrls = arguments
+
+ for(var urlCount = fixtureUrls.length, urlIndex = 0; urlIndex < urlCount; urlIndex++) {
+ htmlChunks.push(this.getFixtureHtml_(fixtureUrls[urlIndex]))
+ }
+
+ return htmlChunks.join('')
+ }
+
+ jasmine.Fixtures.prototype.clearCache = function () {
+ this.fixturesCache_ = {}
+ }
+
+ jasmine.Fixtures.prototype.cleanUp = function () {
+ $('#' + this.containerId).remove()
+ }
+
+ jasmine.Fixtures.prototype.sandbox = function (attributes) {
+ var attributesToSet = attributes || {}
+ return $('<div id="sandbox" />').attr(attributesToSet)
+ }
+
+ jasmine.Fixtures.prototype.createContainer_ = function (html) {
+ var container = $('<div>')
+ .attr('id', this.containerId)
+ .html(html)
+
+ $(document.body).append(container)
+ return container
+ }
+
+ jasmine.Fixtures.prototype.addToContainer_ = function (html){
+ var container = $(document.body).find('#'+this.containerId).append(html)
+
+ if (!container.length) {
+ this.createContainer_(html)
+ }
+ }
+
+ jasmine.Fixtures.prototype.getFixtureHtml_ = function (url) {
+ if (typeof this.fixturesCache_[url] === 'undefined') {
+ this.loadFixtureIntoCache_(url)
+ }
+ return this.fixturesCache_[url]
+ }
+
+ jasmine.Fixtures.prototype.loadFixtureIntoCache_ = function (relativeUrl) {
+ var self = this
+ , url = this.makeFixtureUrl_(relativeUrl)
+ , htmlText = ''
+ , request = $.ajax({
+ async: false, // must be synchronous to guarantee that no tests are run before fixture is loaded
+ cache: false,
+ url: url,
+ dataType: 'html',
+ success: function (data, status, $xhr) {
+ htmlText = $xhr.responseText
+ }
+ }).fail(function ($xhr, status, err) {
+ throw new Error('Fixture could not be loaded: ' + url + ' (status: ' + status + ', message: ' + err.message + ')')
+ })
+
+ var scripts = $($.parseHTML(htmlText, true)).find('script[src]') || [];
+
+ scripts.each(function(){
+ $.ajax({
+ async: false, // must be synchronous to guarantee that no tests are run before fixture is loaded
+ cache: false,
+ dataType: 'script',
+ url: $(this).attr('src'),
+ success: function (data, status, $xhr) {
+ htmlText += '<script>' + $xhr.responseText + '</script>'
+ },
+ error: function ($xhr, status, err) {
+ throw new Error('Script could not be loaded: ' + url + ' (status: ' + status + ', message: ' + err.message + ')')
+ }
+ });
+ })
+
+ self.fixturesCache_[relativeUrl] = htmlText;
+ }
+
+ jasmine.Fixtures.prototype.makeFixtureUrl_ = function (relativeUrl){
+ return this.fixturesPath.match('/$') ? this.fixturesPath + relativeUrl : this.fixturesPath + '/' + relativeUrl
+ }
+
+ jasmine.Fixtures.prototype.proxyCallTo_ = function (methodName, passedArguments) {
+ return this[methodName].apply(this, passedArguments)
+ }
+
+
+ jasmine.StyleFixtures = function () {
+ this.fixturesCache_ = {}
+ this.fixturesNodes_ = []
+ this.fixturesPath = 'spec/javascripts/fixtures'
+ }
+
+ jasmine.StyleFixtures.prototype.set = function (css) {
+ this.cleanUp()
+ this.createStyle_(css)
+ }
+
+ jasmine.StyleFixtures.prototype.appendSet = function (css) {
+ this.createStyle_(css)
+ }
+
+ jasmine.StyleFixtures.prototype.preload = function () {
+ this.read_.apply(this, arguments)
+ }
+
+ jasmine.StyleFixtures.prototype.load = function () {
+ this.cleanUp()
+ this.createStyle_(this.read_.apply(this, arguments))
+ }
+
+ jasmine.StyleFixtures.prototype.appendLoad = function () {
+ this.createStyle_(this.read_.apply(this, arguments))
+ }
+
+ jasmine.StyleFixtures.prototype.cleanUp = function () {
+ while(this.fixturesNodes_.length) {
+ this.fixturesNodes_.pop().remove()
+ }
+ }
+
+ jasmine.StyleFixtures.prototype.createStyle_ = function (html) {
+ var styleText = $('<div></div>').html(html).text()
+ , style = $('<style>' + styleText + '</style>')
+
+ this.fixturesNodes_.push(style)
+ $('head').append(style)
+ }
+
+ jasmine.StyleFixtures.prototype.clearCache = jasmine.Fixtures.prototype.clearCache
+ jasmine.StyleFixtures.prototype.read_ = jasmine.Fixtures.prototype.read
+ jasmine.StyleFixtures.prototype.getFixtureHtml_ = jasmine.Fixtures.prototype.getFixtureHtml_
+ jasmine.StyleFixtures.prototype.loadFixtureIntoCache_ = jasmine.Fixtures.prototype.loadFixtureIntoCache_
+ jasmine.StyleFixtures.prototype.makeFixtureUrl_ = jasmine.Fixtures.prototype.makeFixtureUrl_
+ jasmine.StyleFixtures.prototype.proxyCallTo_ = jasmine.Fixtures.prototype.proxyCallTo_
+
+ jasmine.getJSONFixtures = function () {
+ return jasmine.currentJSONFixtures_ = jasmine.currentJSONFixtures_ || new jasmine.JSONFixtures()
+ }
+
+ jasmine.JSONFixtures = function () {
+ this.fixturesCache_ = {}
+ this.fixturesPath = 'spec/javascripts/fixtures/json'
+ }
+
+ jasmine.JSONFixtures.prototype.load = function () {
+ this.read.apply(this, arguments)
+ return this.fixturesCache_
+ }
+
+ jasmine.JSONFixtures.prototype.read = function () {
+ var fixtureUrls = arguments
+
+ for(var urlCount = fixtureUrls.length, urlIndex = 0; urlIndex < urlCount; urlIndex++) {
+ this.getFixtureData_(fixtureUrls[urlIndex])
+ }
+
+ return this.fixturesCache_
+ }
+
+ jasmine.JSONFixtures.prototype.clearCache = function () {
+ this.fixturesCache_ = {}
+ }
+
+ jasmine.JSONFixtures.prototype.getFixtureData_ = function (url) {
+ if (!this.fixturesCache_[url]) this.loadFixtureIntoCache_(url)
+ return this.fixturesCache_[url]
+ }
+
+ jasmine.JSONFixtures.prototype.loadFixtureIntoCache_ = function (relativeUrl) {
+ var self = this
+ , url = this.fixturesPath.match('/$') ? this.fixturesPath + relativeUrl : this.fixturesPath + '/' + relativeUrl
+
+ $.ajax({
+ async: false, // must be synchronous to guarantee that no tests are run before fixture is loaded
+ cache: false,
+ dataType: 'json',
+ url: url,
+ success: function (data) {
+ self.fixturesCache_[relativeUrl] = data
+ },
+ error: function ($xhr, status, err) {
+ throw new Error('JSONFixture could not be loaded: ' + url + ' (status: ' + status + ', message: ' + err.message + ')')
+ }
+ })
+ }
+
+ jasmine.JSONFixtures.prototype.proxyCallTo_ = function (methodName, passedArguments) {
+ return this[methodName].apply(this, passedArguments)
+ }
+
+ jasmine.jQuery = function () {}
+
+ jasmine.jQuery.browserTagCaseIndependentHtml = function (html) {
+ return $('<div/>').append(html).html()
+ }
+
+ jasmine.jQuery.elementToString = function (element) {
+ return $(element).map(function () { return this.outerHTML; }).toArray().join(', ')
+ }
+
+ var data = {
+ spiedEvents: {}
+ , handlers: []
+ }
+
+ jasmine.jQuery.events = {
+ spyOn: function (selector, eventName) {
+ var handler = function (e) {
+ var calls = (typeof data.spiedEvents[jasmine.spiedEventsKey(selector, eventName)] !== 'undefined') ? data.spiedEvents[jasmine.spiedEventsKey(selector, eventName)].calls : 0
+ data.spiedEvents[jasmine.spiedEventsKey(selector, eventName)] = {
+ args: jasmine.util.argsToArray(arguments),
+ calls: ++calls
+ }
+ }
+
+ $(selector).on(eventName, handler)
+ data.handlers.push(handler)
+
+ return {
+ selector: selector,
+ eventName: eventName,
+ handler: handler,
+ reset: function (){
+ delete data.spiedEvents[jasmine.spiedEventsKey(selector, eventName)]
+ },
+ calls: {
+ count: function () {
+ return data.spiedEvents[jasmine.spiedEventsKey(selector, eventName)] ?
+ data.spiedEvents[jasmine.spiedEventsKey(selector, eventName)].calls : 0;
+ },
+ any: function () {
+ return data.spiedEvents[jasmine.spiedEventsKey(selector, eventName)] ?
+ !!data.spiedEvents[jasmine.spiedEventsKey(selector, eventName)].calls : false;
+ }
+ }
+ }
+ },
+
+ args: function (selector, eventName) {
+ var actualArgs = data.spiedEvents[jasmine.spiedEventsKey(selector, eventName)].args
+
+ if (!actualArgs) {
+ throw "There is no spy for " + eventName + " on " + selector.toString() + ". Make sure to create a spy using spyOnEvent."
+ }
+
+ return actualArgs
+ },
+
+ wasTriggered: function (selector, eventName) {
+ return !!(data.spiedEvents[jasmine.spiedEventsKey(selector, eventName)])
+ },
+
+ wasTriggeredWith: function (selector, eventName, expectedArgs, util, customEqualityTesters) {
+ var actualArgs = jasmine.jQuery.events.args(selector, eventName).slice(1)
+
+ if (Object.prototype.toString.call(expectedArgs) !== '[object Array]')
+ actualArgs = actualArgs[0]
+
+ return util.equals(actualArgs, expectedArgs, customEqualityTesters)
+ },
+
+ wasPrevented: function (selector, eventName) {
+ var spiedEvent = data.spiedEvents[jasmine.spiedEventsKey(selector, eventName)]
+ , args = (jasmine.util.isUndefined(spiedEvent)) ? {} : spiedEvent.args
+ , e = args ? args[0] : undefined
+
+ return e && e.isDefaultPrevented()
+ },
+
+ wasStopped: function (selector, eventName) {
+ var spiedEvent = data.spiedEvents[jasmine.spiedEventsKey(selector, eventName)]
+ , args = (jasmine.util.isUndefined(spiedEvent)) ? {} : spiedEvent.args
+ , e = args ? args[0] : undefined
+
+ return e && e.isPropagationStopped()
+ },
+
+ cleanUp: function () {
+ data.spiedEvents = {}
+ data.handlers = []
+ }
+ }
+
+ var hasProperty = function (actualValue, expectedValue) {
+ if (expectedValue === undefined)
+ return actualValue !== undefined
+
+ return actualValue === expectedValue
+ }
+
+ beforeEach(function () {
+ jasmine.addMatchers({
+ toHaveClass: function () {
+ return {
+ compare: function (actual, className) {
+ return { pass: $(actual).hasClass(className) }
+ }
+ }
+ },
+
+ toHaveCss: function () {
+ return {
+ compare: function (actual, css) {
+ var stripCharsRegex = /[\s;\"\']/g
+ for (var prop in css) {
+ var value = css[prop]
+ // see issue #147 on gh
+ ;if ((value === 'auto') && ($(actual).get(0).style[prop] === 'auto')) continue
+ var actualStripped = $(actual).css(prop).replace(stripCharsRegex, '')
+ var valueStripped = value.replace(stripCharsRegex, '')
+ if (actualStripped !== valueStripped) return { pass: false }
+ }
+ return { pass: true }
+ }
+ }
+ },
+
+ toBeVisible: function () {
+ return {
+ compare: function (actual) {
+ return { pass: $(actual).is(':visible') }
+ }
+ }
+ },
+
+ toBeHidden: function () {
+ return {
+ compare: function (actual) {
+ return { pass: $(actual).is(':hidden') }
+ }
+ }
+ },
+
+ toBeSelected: function () {
+ return {
+ compare: function (actual) {
+ return { pass: $(actual).is(':selected') }
+ }
+ }
+ },
+
+ toBeChecked: function () {
+ return {
+ compare: function (actual) {
+ return { pass: $(actual).is(':checked') }
+ }
+ }
+ },
+
+ toBeEmpty: function () {
+ return {
+ compare: function (actual) {
+ return { pass: $(actual).is(':empty') }
+ }
+ }
+ },
+
+ toBeInDOM: function () {
+ return {
+ compare: function (actual) {
+ return { pass: $.contains(document.documentElement, $(actual)[0]) }
+ }
+ }
+ },
+
+ toExist: function () {
+ return {
+ compare: function (actual) {
+ return { pass: $(actual).length }
+ }
+ }
+ },
+
+ toHaveLength: function () {
+ return {
+ compare: function (actual, length) {
+ return { pass: $(actual).length === length }
+ }
+ }
+ },
+
+ toHaveAttr: function () {
+ return {
+ compare: function (actual, attributeName, expectedAttributeValue) {
+ return { pass: hasProperty($(actual).attr(attributeName), expectedAttributeValue) }
+ }
+ }
+ },
+
+ toHaveProp: function () {
+ return {
+ compare: function (actual, propertyName, expectedPropertyValue) {
+ return { pass: hasProperty($(actual).prop(propertyName), expectedPropertyValue) }
+ }
+ }
+ },
+
+ toHaveId: function () {
+ return {
+ compare: function (actual, id) {
+ return { pass: $(actual).attr('id') == id }
+ }
+ }
+ },
+
+ toHaveHtml: function () {
+ return {
+ compare: function (actual, html) {
+ return { pass: $(actual).html() == jasmine.jQuery.browserTagCaseIndependentHtml(html) }
+ }
+ }
+ },
+
+ toContainHtml: function () {
+ return {
+ compare: function (actual, html) {
+ var actualHtml = $(actual).html()
+ , expectedHtml = jasmine.jQuery.browserTagCaseIndependentHtml(html)
+
+ return { pass: (actualHtml.indexOf(expectedHtml) >= 0) }
+ }
+ }
+ },
+
+ toHaveText: function () {