├── .gitignore ├── Guardfile ├── app ├── templates │ └── main_page.handlebars ├── lib │ ├── store.js │ ├── core.js │ ├── routes.js │ ├── states │ │ └── start.js │ ├── main.js │ ├── state_manager.js │ └── ext.js ├── css │ └── main.css ├── static │ └── img │ │ ├── glyphicons-halflings.png │ │ └── glyphicons-halflings-white.png ├── tests │ └── core_tests.js ├── plugins │ └── loader.js └── vendor │ ├── sproutcore-routing.js │ └── ember-data.js ├── Gemfile ├── config.ru ├── index.html ├── Rakefile ├── LICENSE ├── tests ├── index.html └── qunit │ ├── run-qunit.js │ ├── qunit.css │ └── qunit.js ├── Gemfile.lock ├── README.md └── Assetfile /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | tmp/ 3 | assets/ 4 | -------------------------------------------------------------------------------- /Guardfile: -------------------------------------------------------------------------------- 1 | guard :rake, :task => :test do 2 | watch(%r{^app/.+\.js$}) 3 | end 4 | -------------------------------------------------------------------------------- /app/templates/main_page.handlebars: -------------------------------------------------------------------------------- 1 |

Ember Skeleton v{{App.VERSION}}

2 | -------------------------------------------------------------------------------- /app/lib/store.js: -------------------------------------------------------------------------------- 1 | require('ember-skeleton/core'); 2 | 3 | App.store = DS.Store.create(); 4 | -------------------------------------------------------------------------------- /app/css/main.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding-top: 60px; 3 | } 4 | 5 | a { 6 | cursor: pointer; 7 | } 8 | -------------------------------------------------------------------------------- /app/static/img/glyphicons-halflings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pangratz/ember-skeleton/master/app/static/img/glyphicons-halflings.png -------------------------------------------------------------------------------- /app/static/img/glyphicons-halflings-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pangratz/ember-skeleton/master/app/static/img/glyphicons-halflings-white.png -------------------------------------------------------------------------------- /app/tests/core_tests.js: -------------------------------------------------------------------------------- 1 | module("ember-skeleton"); 2 | 3 | test("App is defined", function () { 4 | ok(typeof App !== undefined, "App is undefined"); 5 | }); 6 | -------------------------------------------------------------------------------- /app/lib/core.js: -------------------------------------------------------------------------------- 1 | require('jquery'); 2 | require('ember'); 3 | require('ember-data'); 4 | require('ember-skeleton/ext'); 5 | 6 | App = Ember.Application.create({ 7 | VERSION: '0.1' 8 | }); 9 | -------------------------------------------------------------------------------- /app/lib/routes.js: -------------------------------------------------------------------------------- 1 | require('sproutcore-routing'); 2 | 3 | require('ember-skeleton/core'); 4 | 5 | App.routes = { 6 | 7 | mainRoute: function(params) { 8 | } 9 | 10 | }; 11 | -------------------------------------------------------------------------------- /app/lib/states/start.js: -------------------------------------------------------------------------------- 1 | require('ember-skeleton/core'); 2 | 3 | App.StartState = Ember.ViewState.extend({ 4 | 5 | view: Ember.View.extend({ 6 | templateName: 'ember-skeleton/~templates/main_page' 7 | }) 8 | 9 | }); 10 | -------------------------------------------------------------------------------- /app/lib/main.js: -------------------------------------------------------------------------------- 1 | require('ember-skeleton/core'); 2 | require('ember-skeleton/store'); 3 | require('ember-skeleton/state_manager'); 4 | require('ember-skeleton/routes'); 5 | 6 | // SC.routes.wantsHistory = true; 7 | SC.routes.add('', App, App.routes.mainRoute); 8 | -------------------------------------------------------------------------------- /app/lib/state_manager.js: -------------------------------------------------------------------------------- 1 | require('ember-skeleton/core'); 2 | require('ember-skeleton/states/start'); 3 | 4 | App.stateManager = Ember.StateManager.create({ 5 | 6 | rootElement: '#main', 7 | initialState: 'start', 8 | 9 | start: App.StartState 10 | 11 | }); 12 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source :rubygems 2 | 3 | gem 'colored' 4 | 5 | gem 'guard' 6 | gem 'guard-rake' 7 | 8 | gem 'rack' 9 | gem 'rack-rewrite' 10 | # gem 'rack-streaming-proxy' 11 | 12 | gem 'sass' 13 | gem 'compass' 14 | 15 | gem 'uglifier' 16 | gem 'yui-compressor' 17 | 18 | gem 'rake-pipeline', :git => 'https://github.com/livingsocial/rake-pipeline.git' 19 | gem 'rake-pipeline-web-filters', :git => 'https://github.com/wycats/rake-pipeline-web-filters.git' 20 | -------------------------------------------------------------------------------- /config.ru: -------------------------------------------------------------------------------- 1 | require 'rake-pipeline' 2 | require 'rake-pipeline/middleware' 3 | use Rake::Pipeline::Middleware, 'Assetfile' 4 | 5 | # require 'rack/streaming_proxy' 6 | # use Rack::StreamingProxy do |request| 7 | # if request.path.start_with?('/proxy') 8 | # "http://127.0.0.1:8080#{request.path}" 9 | # end 10 | # end 11 | 12 | require 'rack-rewrite' 13 | use Rack::Rewrite do 14 | rewrite %r{^(.*)\/$}, '$1/index.html' 15 | end 16 | 17 | run Rack::Directory.new('.') 18 | -------------------------------------------------------------------------------- /app/lib/ext.js: -------------------------------------------------------------------------------- 1 | var get = Ember.get; 2 | 3 | Ember.View.reopen({ 4 | templateForName: function(name, type) { 5 | if (!name) { return; } 6 | 7 | var templates = get(this, 'templates'), 8 | template = get(templates, name); 9 | 10 | if (!template) { 11 | template = require(name); 12 | if (!template) { 13 | throw new Ember.Error(fmt('%@ - Unable to find %@ "%@".', [this, type, name])); 14 | } 15 | } 16 | 17 | return template; 18 | } 19 | }); 20 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Ember Skeleton 5 | 6 | 7 | 8 |
9 | 16 |
17 | 18 | 19 | 20 | 23 | 24 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | APPNAME = 'ember-skeleton' 2 | 3 | require 'colored' 4 | require 'rake-pipeline' 5 | 6 | desc "Build #{APPNAME}" 7 | task :build do 8 | Rake::Pipeline::Project.new('Assetfile').invoke 9 | end 10 | 11 | desc "Run tests with PhantomJS" 12 | task :test => :build do 13 | unless system("which phantomjs > /dev/null 2>&1") 14 | abort "PhantomJS is not installed. Download from http://phantomjs.org/" 15 | end 16 | 17 | cmd = "phantomjs tests/qunit/run-qunit.js \"file://#{File.dirname(__FILE__)}/tests/index.html\"" 18 | 19 | # Run the tests 20 | puts "Running #{APPNAME} tests" 21 | success = system(cmd) 22 | 23 | if success 24 | puts "Tests Passed".green 25 | else 26 | puts "Tests Failed".red 27 | exit(1) 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) Your Name Here 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any 4 | purpose with or without fee is hereby granted, provided that the above 5 | copyright notice and this permission notice appear in all copies. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 8 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 9 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 10 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 11 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 12 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 13 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 14 | -------------------------------------------------------------------------------- /tests/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | QUnit Test Suite 6 | 7 | 8 | 9 | 10 |
11 |
test markup
12 | 13 | 14 | 15 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GIT 2 | remote: https://github.com/livingsocial/rake-pipeline.git 3 | revision: b70ca6cad7655e58d13031f3e24df7dfc74f9030 4 | specs: 5 | rake-pipeline (0.6.0) 6 | rake (~> 0.9.0) 7 | thor 8 | 9 | GIT 10 | remote: https://github.com/wycats/rake-pipeline-web-filters.git 11 | revision: ba0b8a00356b4c854930a8e849b5629d51ffd70f 12 | specs: 13 | rake-pipeline-web-filters (0.6.0) 14 | rack 15 | rake-pipeline (~> 0.6) 16 | 17 | GEM 18 | remote: http://rubygems.org/ 19 | specs: 20 | POpen4 (0.1.4) 21 | Platform (>= 0.4.0) 22 | open4 23 | Platform (0.4.0) 24 | chunky_png (1.2.5) 25 | colored (1.2) 26 | compass (0.12.1) 27 | chunky_png (~> 1.2) 28 | fssm (>= 0.2.7) 29 | sass (~> 3.1) 30 | execjs (1.3.0) 31 | multi_json (~> 1.0) 32 | ffi (1.0.11) 33 | fssm (0.2.8.1) 34 | guard (1.0.1) 35 | ffi (>= 0.5.0) 36 | thor (~> 0.14.6) 37 | guard-rake (0.0.5) 38 | guard 39 | rake 40 | multi_json (1.2.0) 41 | open4 (1.3.0) 42 | rack (1.4.1) 43 | rack-rewrite (1.2.1) 44 | rake (0.9.2.2) 45 | sass (3.1.15) 46 | thor (0.14.6) 47 | uglifier (1.2.4) 48 | execjs (>= 0.3.0) 49 | multi_json (>= 1.0.2) 50 | yui-compressor (0.9.6) 51 | POpen4 (>= 0.1.4) 52 | 53 | PLATFORMS 54 | ruby 55 | 56 | DEPENDENCIES 57 | colored 58 | compass 59 | guard 60 | guard-rake 61 | rack 62 | rack-rewrite 63 | rake-pipeline! 64 | rake-pipeline-web-filters! 65 | sass 66 | uglifier 67 | yui-compressor 68 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Ember Skeleton 2 | ============== 3 | 4 | A skeleton application framework using Ember.js and Rake Pipeline. 5 | 6 | Running 7 | ------- 8 | 9 | $ bundle install 10 | $ bundle exec rackup 11 | 12 | App Structure 13 | ------------- 14 | 15 | ember-skeleton 16 | ├── Assetfile - App build file 17 | ├── Gemfile - Package dependencies for rakep/rack 18 | ├── Gemfile.lock - Here be dragons: don't touch, always include 19 | ├── app - App specific code 20 | │ ├── css - App CSS or SCSS (.scss) 21 | │ ├── lib - App code, *modularized during build* 22 | │ ├── modules - Module code, *already modularized* 23 | │ ├── plugins - Plugins (e.g. jquery.jsonrpc.js) 24 | │ │ └── loader.js - JS module loader 25 | │ ├── static - Static files, never touched, copied over during build 26 | │ ├── templates - Handlebars templates, *modularized during build* 27 | │ ├── tests - App tests 28 | │ └── vendor - Vendor code, *modularized during build* 29 | ├── assets - Built out asset files, minified in production 30 | │ ├── app.css - Built out app CSS/SCSS 31 | │ ├── loader.js - Built out JS module loader 32 | │ └── app.js - Built out app JS 33 | ├── config.ru - Rack development web server configuration 34 | ├── index.html - The app entry point 35 | └── tmp - Temporary build files used by rakep 36 | 37 | Testing 38 | ------- 39 | 40 | You can test the app by invoking 41 | 42 | $ bundle exec rake test 43 | 44 | This executes the tests by using [Phantom.JS](http://www.phantomjs.org/), which you need to have installed. 45 | 46 | Or you can run the tests via 47 | 48 | $ bundle exec rackup 49 | $ open http://localhost:9292/tests/index.html -------------------------------------------------------------------------------- /app/plugins/loader.js: -------------------------------------------------------------------------------- 1 | (function(window) { 2 | function requireWrapper(self) { 3 | var require = function() { 4 | return self.require.apply(self, arguments); 5 | }; 6 | require.exists = function() { 7 | return self.exists.apply(self, arguments); 8 | }; 9 | return require; 10 | } 11 | 12 | var Context = function() { 13 | return this; 14 | }; 15 | 16 | var Loader = function() { 17 | this.modules = {}; 18 | this.loaded = {}; 19 | this.exports = {}; 20 | return this; 21 | }; 22 | 23 | Loader.prototype.require = function(name) { 24 | if (!this.loaded[name]) { 25 | var module = this.modules[name]; 26 | if (module) { 27 | var require = requireWrapper(this); 28 | try { 29 | this.exports[name] = module.call(new Context(), require); 30 | return this.exports[name]; 31 | } finally { 32 | this.loaded[name] = true; 33 | } 34 | } else { 35 | throw "The module '" + name + "' has not been registered"; 36 | } 37 | } 38 | return this.exports[name]; 39 | }; 40 | 41 | Loader.prototype.register = function(name, module) { 42 | if (this.exists(name)) { 43 | throw "The module '"+ "' has already been registered"; 44 | } 45 | this.modules[name] = module; 46 | return true; 47 | }; 48 | 49 | Loader.prototype.unregister = function(name) { 50 | var loaded = !!this.loaded[name]; 51 | if (loaded) { 52 | delete this.exports[name]; 53 | delete this.modules[name]; 54 | delete this.loaded[name]; 55 | } 56 | return loaded; 57 | }; 58 | 59 | Loader.prototype.exists = function(name) { 60 | return name in this.modules; 61 | }; 62 | 63 | window.loader = new Loader(); 64 | })(this); 65 | -------------------------------------------------------------------------------- /tests/qunit/run-qunit.js: -------------------------------------------------------------------------------- 1 | // PhantomJS QUnit Test Runner 2 | 3 | var args = phantom.args; 4 | if (args.length < 1 || args.length > 2) { 5 | console.log("Usage: " + phantom.scriptName + " "); 6 | phantom.exit(1); 7 | } 8 | 9 | var page = require('webpage').create(); 10 | 11 | var depRe = /^DEPRECATION:/; 12 | page.onConsoleMessage = function(msg) { 13 | if (!depRe.test(msg)) console.log(msg); 14 | }; 15 | 16 | page.open(args[0], function(status) { 17 | if (status !== 'success') { 18 | console.error("Unable to access network"); 19 | phantom.exit(1); 20 | } else { 21 | page.evaluate(addLogging); 22 | 23 | var timeout = parseInt(args[1] || 30000, 10); 24 | var start = Date.now(); 25 | var interval = setInterval(function() { 26 | if (Date.now() > start + timeout) { 27 | console.error("Tests timed out"); 28 | phantom.exit(1); 29 | } else { 30 | var qunitDone = page.evaluate(function() { 31 | return window.qunitDone; 32 | }); 33 | 34 | if (qunitDone) { 35 | clearInterval(interval); 36 | if (qunitDone.failed > 0) { 37 | phantom.exit(1); 38 | } else { 39 | phantom.exit(); 40 | } 41 | } 42 | } 43 | }, 500); 44 | } 45 | }); 46 | 47 | function addLogging() { 48 | var testErrors = []; 49 | var assertionErrors = []; 50 | 51 | QUnit.moduleDone(function(context) { 52 | if (context.failed) { 53 | var msg = "Module Failed: " + context.name + "\n" + testErrors.join("\n"); 54 | console.error(msg); 55 | testErrors = []; 56 | } 57 | }); 58 | 59 | QUnit.testDone(function(context) { 60 | if (context.failed) { 61 | var msg = " Test Failed: " + context.name + assertionErrors.join(" "); 62 | testErrors.push(msg); 63 | assertionErrors = []; 64 | } 65 | }); 66 | 67 | QUnit.log(function(context) { 68 | if (context.result) return; 69 | 70 | var msg = "\n Assertion Failed:"; 71 | if (context.message) { 72 | msg += " " + context.message; 73 | } 74 | 75 | if (context.expected) { 76 | msg += "\n Expected: " + context.expected + ", Actual: " + context.actual; 77 | } 78 | 79 | assertionErrors.push(msg); 80 | }); 81 | 82 | QUnit.done(function(context) { 83 | var stats = [ 84 | "Time: " + context.runtime + "ms", 85 | "Total: " + context.total, 86 | "Passed: " + context.passed, 87 | "Failed: " + context.failed 88 | ]; 89 | console.log(stats.join(", ")); 90 | window.qunitDone = context; 91 | }); 92 | } 93 | -------------------------------------------------------------------------------- /Assetfile: -------------------------------------------------------------------------------- 1 | APPNAME = 'ember-skeleton' 2 | 3 | require 'json' 4 | require 'rake-pipeline-web-filters' 5 | 6 | WebFilters = Rake::Pipeline::Web::Filters 7 | 8 | class LoaderFilter < WebFilters::MinispadeFilter 9 | def generate_output(inputs, output) 10 | inputs.each do |input| 11 | code = input.read 12 | module_id = @module_id_generator.call(input) 13 | contents = "function(require) {\n#{code}\n}" 14 | ret = "\nloader.register('#{module_id}', #{contents});\n" 15 | output.write ret 16 | end 17 | end 18 | end 19 | 20 | class EmberAssertFilter < Filter 21 | def generate_output(inputs, output) 22 | inputs.each do |input| 23 | result = input.read 24 | result.gsub!(/ember_assert\((.*)\);/, '') 25 | output.write(result) 26 | end 27 | end 28 | end 29 | 30 | class HandlebarsFilter < Filter 31 | def generate_output(inputs, output) 32 | inputs.each do |input| 33 | code = input.read.to_json 34 | name = File.basename(input.path, '.handlebars') 35 | output.write "\nreturn Ember.Handlebars.compile(#{code});\n" 36 | end 37 | end 38 | end 39 | 40 | output 'assets' 41 | 42 | input 'app' do 43 | match 'lib/**/*.js' do 44 | filter LoaderFilter, 45 | :module_id_generator => proc { |input| 46 | input.path.sub(/^lib\//, "#{APPNAME}/").sub(/\.js$/, '') 47 | } 48 | 49 | if ENV['RAKEP_MODE'] == 'production' 50 | filter EmberAssertFilter 51 | uglify {|input| input} 52 | end 53 | concat 'app.js' 54 | end 55 | 56 | match 'vendor/**/*.js' do 57 | filter LoaderFilter, 58 | :module_id_generator => proc { |input| 59 | input.path.sub(/^vendor\//, '').sub(/\.js$/, '') 60 | } 61 | 62 | if ENV['RAKEP_MODE'] == 'production' 63 | filter EmberAssertFilter 64 | uglify {|input| input} 65 | end 66 | concat %w[ 67 | vendor/jquery.js 68 | vendor/ember.js 69 | vendor/ember-data.js 70 | vendor/sproutcore-routing.js 71 | ], 'app.js' 72 | end 73 | 74 | match 'modules/**/*.js' do 75 | if ENV['RAKEP_MODE'] == 'production' 76 | filter EmberAssertFilter 77 | uglify {|input| input} 78 | end 79 | concat 'app.js' 80 | end 81 | 82 | match 'plugins/**/*.js' do 83 | if ENV['RAKEP_MODE'] == 'production' 84 | uglify {|input| input} 85 | end 86 | concat do |input| 87 | input.sub(/plugins\//, '') 88 | end 89 | end 90 | 91 | match 'templates/**/*.handlebars' do 92 | filter HandlebarsFilter 93 | filter LoaderFilter, 94 | :module_id_generator => proc { |input| 95 | input.path.sub(/^templates\//, "#{APPNAME}/~templates/").sub(/\.handlebars$/, '') 96 | } 97 | if ENV['RAKEP_MODE'] == 'production' 98 | uglify {|input| input} 99 | end 100 | concat 'app.js' 101 | end 102 | 103 | match 'tests/**/*.js' do 104 | filter LoaderFilter, 105 | :module_id_generator => proc { |input| 106 | input.path.sub(/^lib\//, "#{APPNAME}/").sub(/\.js$/, '') 107 | } 108 | concat 'app-tests.js' 109 | end 110 | 111 | match 'css/**/*.css' do 112 | if ENV['RAKEP_MODE'] == 'production' 113 | yui_css 114 | end 115 | concat ['bootstrap.css', 'main.css'], 'app.css' 116 | end 117 | 118 | match 'css/**/*.scss' do 119 | sass 120 | if ENV['RAKEP_MODE'] == 'production' 121 | yui_css 122 | end 123 | concat 'app.css' 124 | end 125 | 126 | match "static/**/*" do 127 | concat do |input| 128 | input.sub(/static\//, '') 129 | end 130 | end 131 | end 132 | 133 | # vim: filetype=ruby 134 | -------------------------------------------------------------------------------- /tests/qunit/qunit.css: -------------------------------------------------------------------------------- 1 | /** 2 | * QUnit v1.4.0 - A JavaScript Unit Testing Framework 3 | * 4 | * http://docs.jquery.com/QUnit 5 | * 6 | * Copyright (c) 2012 John Resig, Jörn Zaefferer 7 | * Dual licensed under the MIT (MIT-LICENSE.txt) 8 | * or GPL (GPL-LICENSE.txt) licenses. 9 | */ 10 | 11 | /** Font Family and Sizes */ 12 | 13 | #qunit-tests, #qunit-header, #qunit-banner, #qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult { 14 | font-family: "Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial, sans-serif; 15 | } 16 | 17 | #qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult, #qunit-tests li { font-size: small; } 18 | #qunit-tests { font-size: smaller; } 19 | 20 | 21 | /** Resets */ 22 | 23 | #qunit-tests, #qunit-tests ol, #qunit-header, #qunit-banner, #qunit-userAgent, #qunit-testresult { 24 | margin: 0; 25 | padding: 0; 26 | } 27 | 28 | 29 | /** Header */ 30 | 31 | #qunit-header { 32 | padding: 0.5em 0 0.5em 1em; 33 | 34 | color: #8699a4; 35 | background-color: #0d3349; 36 | 37 | font-size: 1.5em; 38 | line-height: 1em; 39 | font-weight: normal; 40 | 41 | border-radius: 15px 15px 0 0; 42 | -moz-border-radius: 15px 15px 0 0; 43 | -webkit-border-top-right-radius: 15px; 44 | -webkit-border-top-left-radius: 15px; 45 | } 46 | 47 | #qunit-header a { 48 | text-decoration: none; 49 | color: #c2ccd1; 50 | } 51 | 52 | #qunit-header a:hover, 53 | #qunit-header a:focus { 54 | color: #fff; 55 | } 56 | 57 | #qunit-header label { 58 | display: inline-block; 59 | } 60 | 61 | #qunit-banner { 62 | height: 5px; 63 | } 64 | 65 | #qunit-testrunner-toolbar { 66 | padding: 0.5em 0 0.5em 2em; 67 | color: #5E740B; 68 | background-color: #eee; 69 | } 70 | 71 | #qunit-userAgent { 72 | padding: 0.5em 0 0.5em 2.5em; 73 | background-color: #2b81af; 74 | color: #fff; 75 | text-shadow: rgba(0, 0, 0, 0.5) 2px 2px 1px; 76 | } 77 | 78 | 79 | /** Tests: Pass/Fail */ 80 | 81 | #qunit-tests { 82 | list-style-position: inside; 83 | } 84 | 85 | #qunit-tests li { 86 | padding: 0.4em 0.5em 0.4em 2.5em; 87 | border-bottom: 1px solid #fff; 88 | list-style-position: inside; 89 | } 90 | 91 | #qunit-tests.hidepass li.pass, #qunit-tests.hidepass li.running { 92 | display: none; 93 | } 94 | 95 | #qunit-tests li strong { 96 | cursor: pointer; 97 | } 98 | 99 | #qunit-tests li a { 100 | padding: 0.5em; 101 | color: #c2ccd1; 102 | text-decoration: none; 103 | } 104 | #qunit-tests li a:hover, 105 | #qunit-tests li a:focus { 106 | color: #000; 107 | } 108 | 109 | #qunit-tests ol { 110 | margin-top: 0.5em; 111 | padding: 0.5em; 112 | 113 | background-color: #fff; 114 | 115 | border-radius: 15px; 116 | -moz-border-radius: 15px; 117 | -webkit-border-radius: 15px; 118 | 119 | box-shadow: inset 0px 2px 13px #999; 120 | -moz-box-shadow: inset 0px 2px 13px #999; 121 | -webkit-box-shadow: inset 0px 2px 13px #999; 122 | } 123 | 124 | #qunit-tests table { 125 | border-collapse: collapse; 126 | margin-top: .2em; 127 | } 128 | 129 | #qunit-tests th { 130 | text-align: right; 131 | vertical-align: top; 132 | padding: 0 .5em 0 0; 133 | } 134 | 135 | #qunit-tests td { 136 | vertical-align: top; 137 | } 138 | 139 | #qunit-tests pre { 140 | margin: 0; 141 | white-space: pre-wrap; 142 | word-wrap: break-word; 143 | } 144 | 145 | #qunit-tests del { 146 | background-color: #e0f2be; 147 | color: #374e0c; 148 | text-decoration: none; 149 | } 150 | 151 | #qunit-tests ins { 152 | background-color: #ffcaca; 153 | color: #500; 154 | text-decoration: none; 155 | } 156 | 157 | /*** Test Counts */ 158 | 159 | #qunit-tests b.counts { color: black; } 160 | #qunit-tests b.passed { color: #5E740B; } 161 | #qunit-tests b.failed { color: #710909; } 162 | 163 | #qunit-tests li li { 164 | margin: 0.5em; 165 | padding: 0.4em 0.5em 0.4em 0.5em; 166 | background-color: #fff; 167 | border-bottom: none; 168 | list-style-position: inside; 169 | } 170 | 171 | /*** Passing Styles */ 172 | 173 | #qunit-tests li li.pass { 174 | color: #5E740B; 175 | background-color: #fff; 176 | border-left: 26px solid #C6E746; 177 | } 178 | 179 | #qunit-tests .pass { color: #528CE0; background-color: #D2E0E6; } 180 | #qunit-tests .pass .test-name { color: #366097; } 181 | 182 | #qunit-tests .pass .test-actual, 183 | #qunit-tests .pass .test-expected { color: #999999; } 184 | 185 | #qunit-banner.qunit-pass { background-color: #C6E746; } 186 | 187 | /*** Failing Styles */ 188 | 189 | #qunit-tests li li.fail { 190 | color: #710909; 191 | background-color: #fff; 192 | border-left: 26px solid #EE5757; 193 | white-space: pre; 194 | } 195 | 196 | #qunit-tests > li:last-child { 197 | border-radius: 0 0 15px 15px; 198 | -moz-border-radius: 0 0 15px 15px; 199 | -webkit-border-bottom-right-radius: 15px; 200 | -webkit-border-bottom-left-radius: 15px; 201 | } 202 | 203 | #qunit-tests .fail { color: #000000; background-color: #EE5757; } 204 | #qunit-tests .fail .test-name, 205 | #qunit-tests .fail .module-name { color: #000000; } 206 | 207 | #qunit-tests .fail .test-actual { color: #EE5757; } 208 | #qunit-tests .fail .test-expected { color: green; } 209 | 210 | #qunit-banner.qunit-fail { background-color: #EE5757; } 211 | 212 | 213 | /** Result */ 214 | 215 | #qunit-testresult { 216 | padding: 0.5em 0.5em 0.5em 2.5em; 217 | 218 | color: #2b81af; 219 | background-color: #D2E0E6; 220 | 221 | border-bottom: 1px solid white; 222 | } 223 | 224 | /** Fixture */ 225 | 226 | #qunit-fixture { 227 | position: absolute; 228 | top: -10000px; 229 | left: -10000px; 230 | width: 1000px; 231 | height: 1000px; 232 | } 233 | -------------------------------------------------------------------------------- /app/vendor/sproutcore-routing.js: -------------------------------------------------------------------------------- 1 | // ========================================================================== 2 | // Project: SproutCore - JavaScript Application Framework 3 | // Copyright: ©2006-2011 Strobe Inc. and contributors. 4 | // Portions ©2008-2011 Apple Inc. All rights reserved. 5 | // License: Licensed under MIT license (see license.js) 6 | // ========================================================================== 7 | 8 | // Ember no longer aliases SC 9 | window.SC = window.SC || Ember.Namespace.create(); 10 | 11 | var get = Ember.get, set = Ember.set; 12 | 13 | /** 14 | Wether the browser supports HTML5 history. 15 | */ 16 | var supportsHistory = !!(window.history && window.history.pushState); 17 | 18 | /** 19 | Wether the browser supports the hashchange event. 20 | */ 21 | var supportsHashChange = ('onhashchange' in window) && (document.documentMode === undefined || document.documentMode > 7); 22 | 23 | /** 24 | @class 25 | 26 | Route is a class used internally by SC.routes. The routes defined by your 27 | application are stored in a tree structure, and this is the class for the 28 | nodes. 29 | */ 30 | var Route = Ember.Object.extend( 31 | /** @scope Route.prototype */ { 32 | 33 | target: null, 34 | 35 | method: null, 36 | 37 | staticRoutes: null, 38 | 39 | dynamicRoutes: null, 40 | 41 | wildcardRoutes: null, 42 | 43 | add: function(parts, target, method) { 44 | var part, nextRoute; 45 | 46 | // clone the parts array because we are going to alter it 47 | parts = Ember.copy(parts); 48 | 49 | if (!parts || parts.length === 0) { 50 | this.target = target; 51 | this.method = method; 52 | 53 | } else { 54 | part = parts.shift(); 55 | 56 | // there are 3 types of routes 57 | switch (part.slice(0, 1)) { 58 | 59 | // 1. dynamic routes 60 | case ':': 61 | part = part.slice(1, part.length); 62 | if (!this.dynamicRoutes) this.dynamicRoutes = {}; 63 | if (!this.dynamicRoutes[part]) this.dynamicRoutes[part] = this.constructor.create(); 64 | nextRoute = this.dynamicRoutes[part]; 65 | break; 66 | 67 | // 2. wildcard routes 68 | case '*': 69 | part = part.slice(1, part.length); 70 | if (!this.wildcardRoutes) this.wildcardRoutes = {}; 71 | nextRoute = this.wildcardRoutes[part] = this.constructor.create(); 72 | break; 73 | 74 | // 3. static routes 75 | default: 76 | if (!this.staticRoutes) this.staticRoutes = {}; 77 | if (!this.staticRoutes[part]) this.staticRoutes[part] = this.constructor.create(); 78 | nextRoute = this.staticRoutes[part]; 79 | } 80 | 81 | // recursively add the rest of the route 82 | if (nextRoute) nextRoute.add(parts, target, method); 83 | } 84 | }, 85 | 86 | routeForParts: function(parts, params) { 87 | var part, key, route; 88 | 89 | // clone the parts array because we are going to alter it 90 | parts = Ember.copy(parts); 91 | 92 | // if parts is empty, we are done 93 | if (!parts || parts.length === 0) { 94 | return this.method ? this : null; 95 | 96 | } else { 97 | part = parts.shift(); 98 | 99 | // try to match a static route 100 | if (this.staticRoutes && this.staticRoutes[part]) { 101 | route = this.staticRoutes[part].routeForParts(parts, params); 102 | if (route) { 103 | return route; 104 | } 105 | } 106 | 107 | // else, try to match a dynamic route 108 | for (key in this.dynamicRoutes) { 109 | route = this.dynamicRoutes[key].routeForParts(parts, params); 110 | if (route) { 111 | params[key] = part; 112 | return route; 113 | } 114 | } 115 | 116 | // else, try to match a wilcard route 117 | for (key in this.wildcardRoutes) { 118 | parts.unshift(part); 119 | params[key] = parts.join('/'); 120 | return this.wildcardRoutes[key].routeForParts(null, params); 121 | } 122 | 123 | // if nothing was found, it means that there is no match 124 | return null; 125 | } 126 | } 127 | 128 | }); 129 | 130 | /** 131 | @class 132 | 133 | SC.routes manages the browser location. You can change the hash part of the 134 | current location. The following code 135 | 136 | SC.routes.set('location', 'notes/edit/4'); 137 | 138 | will change the location to http://domain.tld/my_app#notes/edit/4. Adding 139 | routes will register a handler that will be called whenever the location 140 | changes and matches the route: 141 | 142 | SC.routes.add(':controller/:action/:id', MyApp, MyApp.route); 143 | 144 | You can pass additional parameters in the location hash that will be relayed 145 | to the route handler: 146 | 147 | SC.routes.set('location', 'notes/show/4?format=xml&language=fr'); 148 | 149 | The syntax for the location hash is described in the location property 150 | documentation, and the syntax for adding handlers is described in the 151 | add method documentation. 152 | 153 | Browsers keep track of the locations in their history, so when the user 154 | presses the 'back' or 'forward' button, the location is changed, SC.route 155 | catches it and calls your handler. Except for Internet Explorer versions 7 156 | and earlier, which do not modify the history stack when the location hash 157 | changes. 158 | 159 | SC.routes also supports HTML5 history, which uses a '/' instead of a '#' 160 | in the URLs, so that all your website's URLs are consistent. 161 | */ 162 | var routes = SC.routes = Ember.Object.create( 163 | /** @scope SC.routes.prototype */{ 164 | 165 | /** 166 | Set this property to true if you want to use HTML5 history, if available on 167 | the browser, instead of the location hash. 168 | 169 | HTML 5 history uses the history.pushState method and the window's popstate 170 | event. 171 | 172 | By default it is false, so your URLs will look like: 173 | 174 | http://domain.tld/my_app#notes/edit/4 175 | 176 | If set to true and the browser supports pushState(), your URLs will look 177 | like: 178 | 179 | http://domain.tld/my_app/notes/edit/4 180 | 181 | You will also need to make sure that baseURI is properly configured, as 182 | well as your server so that your routes are properly pointing to your 183 | SproutCore application. 184 | 185 | @see http://dev.w3.org/html5/spec/history.html#the-history-interface 186 | @property 187 | @type {Boolean} 188 | */ 189 | wantsHistory: false, 190 | 191 | /** 192 | A read-only boolean indicating whether or not HTML5 history is used. Based 193 | on the value of wantsHistory and the browser's support for pushState. 194 | 195 | @see wantsHistory 196 | @property 197 | @type {Boolean} 198 | */ 199 | usesHistory: null, 200 | 201 | /** 202 | The base URI used to resolve routes (which are relative URLs). Only used 203 | when usesHistory is equal to true. 204 | 205 | The build tools automatically configure this value if you have the 206 | html5_history option activated in the Buildfile: 207 | 208 | config :my_app, :html5_history => true 209 | 210 | Alternatively, it uses by default the value of the href attribute of the 211 | tag of the HTML document. For example: 212 | 213 | 214 | 215 | The value can also be customized before or during the exectution of the 216 | main() method. 217 | 218 | @see http://www.w3.org/TR/html5/semantics.html#the-base-element 219 | @property 220 | @type {String} 221 | */ 222 | baseURI: document.baseURI, 223 | 224 | /** @private 225 | A boolean value indicating whether or not the ping method has been called 226 | to setup the SC.routes. 227 | 228 | @property 229 | @type {Boolean} 230 | */ 231 | _didSetup: false, 232 | 233 | /** @private 234 | Internal representation of the current location hash. 235 | 236 | @property 237 | @type {String} 238 | */ 239 | _location: null, 240 | 241 | /** @private 242 | Routes are stored in a tree structure, this is the root node. 243 | 244 | @property 245 | @type {Route} 246 | */ 247 | _firstRoute: null, 248 | 249 | /** @private 250 | An internal reference to the Route class. 251 | 252 | @property 253 | */ 254 | _Route: Route, 255 | 256 | /** @private 257 | Internal method used to extract and merge the parameters of a URL. 258 | 259 | @returns {Hash} 260 | */ 261 | _extractParametersAndRoute: function(obj) { 262 | var params = {}, 263 | route = obj.route || '', 264 | separator, parts, i, len, crumbs, key; 265 | 266 | separator = (route.indexOf('?') < 0 && route.indexOf('&') >= 0) ? '&' : '?'; 267 | parts = route.split(separator); 268 | route = parts[0]; 269 | if (parts.length === 1) { 270 | parts = []; 271 | } else if (parts.length === 2) { 272 | parts = parts[1].split('&'); 273 | } else if (parts.length > 2) { 274 | parts.shift(); 275 | } 276 | 277 | // extract the parameters from the route string 278 | len = parts.length; 279 | for (i = 0; i < len; ++i) { 280 | crumbs = parts[i].split('='); 281 | params[crumbs[0]] = crumbs[1]; 282 | } 283 | 284 | // overlay any parameter passed in obj 285 | for (key in obj) { 286 | if (obj.hasOwnProperty(key) && key !== 'route') { 287 | params[key] = '' + obj[key]; 288 | } 289 | } 290 | 291 | // build the route 292 | parts = []; 293 | for (key in params) { 294 | parts.push([key, params[key]].join('=')); 295 | } 296 | params.params = separator + parts.join('&'); 297 | params.route = route; 298 | 299 | return params; 300 | }, 301 | 302 | /** 303 | The current location hash. It is the part in the browser's location after 304 | the '#' mark. 305 | 306 | The following code 307 | 308 | SC.routes.set('location', 'notes/edit/4'); 309 | 310 | will change the location to http://domain.tld/my_app#notes/edit/4 and call 311 | the correct route handler if it has been registered with the add method. 312 | 313 | You can also pass additional parameters. They will be relayed to the route 314 | handler. For example, the following code 315 | 316 | SC.routes.add(':controller/:action/:id', MyApp, MyApp.route); 317 | SC.routes.set('location', 'notes/show/4?format=xml&language=fr'); 318 | 319 | will change the location to 320 | http://domain.tld/my_app#notes/show/4?format=xml&language=fr and call the 321 | MyApp.route method with the following argument: 322 | 323 | { route: 'notes/show/4', 324 | params: '?format=xml&language=fr', 325 | controller: 'notes', 326 | action: 'show', 327 | id: '4', 328 | format: 'xml', 329 | language: 'fr' } 330 | 331 | The location can also be set with a hash, the following code 332 | 333 | SC.routes.set('location', 334 | { route: 'notes/edit/4', format: 'xml', language: 'fr' }); 335 | 336 | will change the location to 337 | http://domain.tld/my_app#notes/show/4?format=xml&language=fr. 338 | 339 | The 'notes/show/4&format=xml&language=fr' syntax for passing parameters, 340 | using a '&' instead of a '?', as used in SproutCore 1.0 is still supported. 341 | 342 | @property 343 | @type {String} 344 | */ 345 | location: Ember.computed(function(key, value) { 346 | this._skipRoute = false; 347 | return this._extractLocation(key, value); 348 | }).property(), 349 | 350 | _extractLocation: function(key, value) { 351 | var crumbs, encodedValue; 352 | 353 | if (value !== undefined) { 354 | if (value === null) { 355 | value = ''; 356 | } 357 | 358 | if (typeof(value) === 'object') { 359 | crumbs = this._extractParametersAndRoute(value); 360 | value = crumbs.route + crumbs.params; 361 | } 362 | 363 | if (!this._skipPush && (!Ember.empty(value) || (this._location && this._location !== value))) { 364 | encodedValue = encodeURI(value); 365 | 366 | if (this.usesHistory) { 367 | if (encodedValue.length > 0) { 368 | encodedValue = '/' + encodedValue; 369 | } 370 | window.history.pushState(null, null, get(this, 'baseURI') + encodedValue); 371 | } else if (encodedValue.length > 0 || window.location.hash.length > 0) { 372 | window.location.hash = encodedValue; 373 | } 374 | } 375 | 376 | this._location = value; 377 | } 378 | 379 | return this._location; 380 | }, 381 | 382 | updateLocation: function(loc){ 383 | this._skipRoute = true; 384 | return this._extractLocation('location', loc); 385 | }, 386 | 387 | /** 388 | You usually don't need to call this method. It is done automatically after 389 | the application has been initialized. 390 | 391 | It registers for the hashchange event if available. If not, it creates a 392 | timer that looks for location changes every 150ms. 393 | */ 394 | ping: function() { 395 | if (!this._didSetup) { 396 | this._didSetup = true; 397 | var state; 398 | 399 | if (get(this, 'wantsHistory') && supportsHistory) { 400 | this.usesHistory = true; 401 | 402 | // Move any hash state to url state 403 | // TODO: Make sure we have a hash before adding slash 404 | state = window.location.hash.slice(1); 405 | if (state.length > 0) { 406 | state = '/' + state; 407 | window.history.replaceState(null, null, get(this, 'baseURI')+state); 408 | } 409 | 410 | popState(); 411 | jQuery(window).bind('popstate', popState); 412 | 413 | } else { 414 | this.usesHistory = false; 415 | 416 | if (get(this, 'wantsHistory')) { 417 | // Move any url state to hash 418 | var base = get(this, 'baseURI'); 419 | var loc = (base.charAt(0) === '/') ? document.location.pathname : document.location.href.replace(document.location.hash, ''); 420 | state = loc.slice(base.length+1); 421 | if (state.length > 0) { 422 | window.location.href = base+'#'+state; 423 | } 424 | } 425 | 426 | if (supportsHashChange) { 427 | hashChange(); 428 | jQuery(window).bind('hashchange', hashChange); 429 | 430 | } else { 431 | var invokeHashChange = function() { 432 | hashChange(); 433 | setTimeout(invokeHashChange, 100); 434 | }; 435 | invokeHashChange(); 436 | } 437 | } 438 | } 439 | }, 440 | 441 | /** 442 | Adds a route handler. Routes have the following format: 443 | 444 | - 'users/show/5' is a static route and only matches this exact string, 445 | - ':action/:controller/:id' is a dynamic route and the handler will be 446 | called with the 'action', 'controller' and 'id' parameters passed in a 447 | hash, 448 | - '*url' is a wildcard route, it matches the whole route and the handler 449 | will be called with the 'url' parameter passed in a hash. 450 | 451 | Route types can be combined, the following are valid routes: 452 | 453 | - 'users/:action/:id' 454 | - ':controller/show/:id' 455 | - ':controller/ *url' (ignore the space, because of jslint) 456 | 457 | @param {String} route the route to be registered 458 | @param {Object} target the object on which the method will be called, or 459 | directly the function to be called to handle the route 460 | @param {Function} method the method to be called on target to handle the 461 | route, can be a function or a string 462 | */ 463 | add: function(route, target, method) { 464 | if (!this._didSetup) { 465 | Ember.run.once(this, 'ping'); 466 | } 467 | 468 | if (method === undefined && Ember.typeOf(target) === 'function') { 469 | method = target; 470 | target = null; 471 | } else if (Ember.typeOf(method) === 'string') { 472 | method = target[method]; 473 | } 474 | 475 | if (!this._firstRoute) this._firstRoute = Route.create(); 476 | this._firstRoute.add(route.split('/'), target, method); 477 | 478 | return this; 479 | }, 480 | 481 | /** 482 | Observer of the 'location' property that calls the correct route handler 483 | when the location changes. 484 | */ 485 | locationDidChange: Ember.observer(function() { 486 | this.trigger(); 487 | }, 'location'), 488 | 489 | /** 490 | Triggers a route even if already in that route (does change the location, if it 491 | is not already changed, as well). 492 | 493 | If the location is not the same as the supplied location, this simply lets "location" 494 | handle it (which ends up coming back to here). 495 | */ 496 | trigger: function() { 497 | var location = get(this, 'location'), 498 | params, route; 499 | 500 | if (this._firstRoute) { 501 | params = this._extractParametersAndRoute({ route: location }); 502 | location = params.route; 503 | delete params.route; 504 | delete params.params; 505 | 506 | route = this.getRoute(location, params); 507 | if (route && route.method) { 508 | route.method.call(route.target || this, params); 509 | } 510 | } 511 | }, 512 | 513 | getRoute: function(route, params) { 514 | var firstRoute = this._firstRoute; 515 | if (firstRoute == null) { 516 | return null; 517 | } 518 | 519 | if (params == null) { 520 | params = {}; 521 | } 522 | 523 | return firstRoute.routeForParts(route.split('/'), params); 524 | }, 525 | 526 | exists: function(route, params) { 527 | route = this.getRoute(route, params); 528 | return route !== null && route.method !== null; 529 | } 530 | 531 | }); 532 | 533 | /** 534 | Event handler for the hashchange event. Called automatically by the browser 535 | if it supports the hashchange event, or by our timer if not. 536 | */ 537 | function hashChange(event) { 538 | var loc = window.location.hash; 539 | 540 | // Remove the '#' prefix 541 | loc = (loc && loc.length > 0) ? loc.slice(1, loc.length) : ''; 542 | 543 | if (!jQuery.browser.mozilla) { 544 | // because of bug https://bugzilla.mozilla.org/show_bug.cgi?id=483304 545 | loc = decodeURI(loc); 546 | } 547 | 548 | if (get(routes, 'location') !== loc && !routes._skipRoute) { 549 | Ember.run.once(function() { 550 | routes._skipPush = true; 551 | set(routes, 'location', loc); 552 | routes._skipPush = false; 553 | }); 554 | } 555 | routes._skipRoute = false; 556 | } 557 | 558 | function popState(event) { 559 | var base = get(routes, 'baseURI'), 560 | loc = (base.charAt(0) === '/') ? document.location.pathname : document.location.href; 561 | 562 | if (loc.slice(0, base.length) === base) { 563 | // Remove the base prefix and the extra '/' 564 | loc = loc.slice(base.length + 1, loc.length); 565 | 566 | if (get(routes, 'location') !== loc && !routes._skipRoute) { 567 | Ember.run.once(function() { 568 | routes._skipPush = true; 569 | set(routes, 'location', loc); 570 | routes._skipPush = false; 571 | }); 572 | } 573 | } 574 | routes._skipRoute = false; 575 | } -------------------------------------------------------------------------------- /tests/qunit/qunit.js: -------------------------------------------------------------------------------- 1 | /** 2 | * QUnit v1.4.0 - A JavaScript Unit Testing Framework 3 | * 4 | * http://docs.jquery.com/QUnit 5 | * 6 | * Copyright (c) 2012 John Resig, Jörn Zaefferer 7 | * Dual licensed under the MIT (MIT-LICENSE.txt) 8 | * or GPL (GPL-LICENSE.txt) licenses. 9 | */ 10 | 11 | (function(window) { 12 | 13 | var defined = { 14 | setTimeout: typeof window.setTimeout !== "undefined", 15 | sessionStorage: (function() { 16 | var x = "qunit-test-string"; 17 | try { 18 | sessionStorage.setItem(x, x); 19 | sessionStorage.removeItem(x); 20 | return true; 21 | } catch(e) { 22 | return false; 23 | } 24 | }()) 25 | }; 26 | 27 | var testId = 0, 28 | toString = Object.prototype.toString, 29 | hasOwn = Object.prototype.hasOwnProperty; 30 | 31 | var Test = function(name, testName, expected, async, callback) { 32 | this.name = name; 33 | this.testName = testName; 34 | this.expected = expected; 35 | this.async = async; 36 | this.callback = callback; 37 | this.assertions = []; 38 | }; 39 | Test.prototype = { 40 | init: function() { 41 | var tests = id("qunit-tests"); 42 | if (tests) { 43 | var b = document.createElement("strong"); 44 | b.innerHTML = "Running " + this.name; 45 | var li = document.createElement("li"); 46 | li.appendChild( b ); 47 | li.className = "running"; 48 | li.id = this.id = "test-output" + testId++; 49 | tests.appendChild( li ); 50 | } 51 | }, 52 | setup: function() { 53 | if (this.module != config.previousModule) { 54 | if ( config.previousModule ) { 55 | runLoggingCallbacks('moduleDone', QUnit, { 56 | name: config.previousModule, 57 | failed: config.moduleStats.bad, 58 | passed: config.moduleStats.all - config.moduleStats.bad, 59 | total: config.moduleStats.all 60 | } ); 61 | } 62 | config.previousModule = this.module; 63 | config.moduleStats = { all: 0, bad: 0 }; 64 | runLoggingCallbacks( 'moduleStart', QUnit, { 65 | name: this.module 66 | } ); 67 | } else if (config.autorun) { 68 | runLoggingCallbacks( 'moduleStart', QUnit, { 69 | name: this.module 70 | } ); 71 | } 72 | 73 | config.current = this; 74 | this.testEnvironment = extend({ 75 | setup: function() {}, 76 | teardown: function() {} 77 | }, this.moduleTestEnvironment); 78 | 79 | runLoggingCallbacks( 'testStart', QUnit, { 80 | name: this.testName, 81 | module: this.module 82 | }); 83 | 84 | // allow utility functions to access the current test environment 85 | // TODO why?? 86 | QUnit.current_testEnvironment = this.testEnvironment; 87 | 88 | if ( !config.pollution ) { 89 | saveGlobal(); 90 | } 91 | if ( config.notrycatch ) { 92 | this.testEnvironment.setup.call(this.testEnvironment); 93 | return; 94 | } 95 | try { 96 | this.testEnvironment.setup.call(this.testEnvironment); 97 | } catch(e) { 98 | QUnit.pushFailure( "Setup failed on " + this.testName + ": " + e.message, extractStacktrace( e, 1 ) ); 99 | } 100 | }, 101 | run: function() { 102 | config.current = this; 103 | if ( this.async ) { 104 | QUnit.stop(); 105 | } 106 | 107 | if ( config.notrycatch ) { 108 | this.callback.call(this.testEnvironment); 109 | return; 110 | } 111 | try { 112 | this.callback.call(this.testEnvironment); 113 | } catch(e) { 114 | QUnit.pushFailure( "Died on test #" + (this.assertions.length + 1) + ": " + e.message, extractStacktrace( e, 1 ) ); 115 | // else next test will carry the responsibility 116 | saveGlobal(); 117 | 118 | // Restart the tests if they're blocking 119 | if ( config.blocking ) { 120 | QUnit.start(); 121 | } 122 | } 123 | }, 124 | teardown: function() { 125 | config.current = this; 126 | if ( config.notrycatch ) { 127 | this.testEnvironment.teardown.call(this.testEnvironment); 128 | return; 129 | } else { 130 | try { 131 | this.testEnvironment.teardown.call(this.testEnvironment); 132 | } catch(e) { 133 | QUnit.pushFailure( "Teardown failed on " + this.testName + ": " + e.message, extractStacktrace( e, 1 ) ); 134 | } 135 | } 136 | checkPollution(); 137 | }, 138 | finish: function() { 139 | config.current = this; 140 | if ( this.expected != null && this.expected != this.assertions.length ) { 141 | QUnit.pushFailure( "Expected " + this.expected + " assertions, but " + this.assertions.length + " were run" ); 142 | } else if ( this.expected == null && !this.assertions.length ) { 143 | QUnit.pushFailure( "Expected at least one assertion, but none were run - call expect(0) to accept zero assertions." ); 144 | } 145 | 146 | var good = 0, bad = 0, 147 | li, i, 148 | tests = id("qunit-tests"); 149 | 150 | config.stats.all += this.assertions.length; 151 | config.moduleStats.all += this.assertions.length; 152 | 153 | if ( tests ) { 154 | var ol = document.createElement("ol"); 155 | 156 | for ( i = 0; i < this.assertions.length; i++ ) { 157 | var assertion = this.assertions[i]; 158 | 159 | li = document.createElement("li"); 160 | li.className = assertion.result ? "pass" : "fail"; 161 | li.innerHTML = assertion.message || (assertion.result ? "okay" : "failed"); 162 | ol.appendChild( li ); 163 | 164 | if ( assertion.result ) { 165 | good++; 166 | } else { 167 | bad++; 168 | config.stats.bad++; 169 | config.moduleStats.bad++; 170 | } 171 | } 172 | 173 | // store result when possible 174 | if ( QUnit.config.reorder && defined.sessionStorage ) { 175 | if (bad) { 176 | sessionStorage.setItem("qunit-test-" + this.module + "-" + this.testName, bad); 177 | } else { 178 | sessionStorage.removeItem("qunit-test-" + this.module + "-" + this.testName); 179 | } 180 | } 181 | 182 | if (bad === 0) { 183 | ol.style.display = "none"; 184 | } 185 | 186 | var b = document.createElement("strong"); 187 | b.innerHTML = this.name + " (" + bad + ", " + good + ", " + this.assertions.length + ")"; 188 | 189 | var a = document.createElement("a"); 190 | a.innerHTML = "Rerun"; 191 | a.href = QUnit.url({ filter: getText([b]).replace(/\([^)]+\)$/, "").replace(/(^\s*|\s*$)/g, "") }); 192 | 193 | addEvent(b, "click", function() { 194 | var next = b.nextSibling.nextSibling, 195 | display = next.style.display; 196 | next.style.display = display === "none" ? "block" : "none"; 197 | }); 198 | 199 | addEvent(b, "dblclick", function(e) { 200 | var target = e && e.target ? e.target : window.event.srcElement; 201 | if ( target.nodeName.toLowerCase() == "span" || target.nodeName.toLowerCase() == "b" ) { 202 | target = target.parentNode; 203 | } 204 | if ( window.location && target.nodeName.toLowerCase() === "strong" ) { 205 | window.location = QUnit.url({ filter: getText([target]).replace(/\([^)]+\)$/, "").replace(/(^\s*|\s*$)/g, "") }); 206 | } 207 | }); 208 | 209 | li = id(this.id); 210 | li.className = bad ? "fail" : "pass"; 211 | li.removeChild( li.firstChild ); 212 | li.appendChild( b ); 213 | li.appendChild( a ); 214 | li.appendChild( ol ); 215 | 216 | } else { 217 | for ( i = 0; i < this.assertions.length; i++ ) { 218 | if ( !this.assertions[i].result ) { 219 | bad++; 220 | config.stats.bad++; 221 | config.moduleStats.bad++; 222 | } 223 | } 224 | } 225 | 226 | QUnit.reset(); 227 | 228 | runLoggingCallbacks( 'testDone', QUnit, { 229 | name: this.testName, 230 | module: this.module, 231 | failed: bad, 232 | passed: this.assertions.length - bad, 233 | total: this.assertions.length 234 | } ); 235 | }, 236 | 237 | queue: function() { 238 | var test = this; 239 | synchronize(function() { 240 | test.init(); 241 | }); 242 | function run() { 243 | // each of these can by async 244 | synchronize(function() { 245 | test.setup(); 246 | }); 247 | synchronize(function() { 248 | test.run(); 249 | }); 250 | synchronize(function() { 251 | test.teardown(); 252 | }); 253 | synchronize(function() { 254 | test.finish(); 255 | }); 256 | } 257 | // defer when previous test run passed, if storage is available 258 | var bad = QUnit.config.reorder && defined.sessionStorage && +sessionStorage.getItem("qunit-test-" + this.module + "-" + this.testName); 259 | if (bad) { 260 | run(); 261 | } else { 262 | synchronize(run, true); 263 | } 264 | } 265 | 266 | }; 267 | 268 | var QUnit = { 269 | 270 | // call on start of module test to prepend name to all tests 271 | module: function(name, testEnvironment) { 272 | config.currentModule = name; 273 | config.currentModuleTestEnviroment = testEnvironment; 274 | }, 275 | 276 | asyncTest: function(testName, expected, callback) { 277 | if ( arguments.length === 2 ) { 278 | callback = expected; 279 | expected = null; 280 | } 281 | 282 | QUnit.test(testName, expected, callback, true); 283 | }, 284 | 285 | test: function(testName, expected, callback, async) { 286 | var name = '' + escapeInnerText(testName) + ''; 287 | 288 | if ( arguments.length === 2 ) { 289 | callback = expected; 290 | expected = null; 291 | } 292 | 293 | if ( config.currentModule ) { 294 | name = '' + config.currentModule + ": " + name; 295 | } 296 | 297 | if ( !validTest(config.currentModule + ": " + testName) ) { 298 | return; 299 | } 300 | 301 | var test = new Test(name, testName, expected, async, callback); 302 | test.module = config.currentModule; 303 | test.moduleTestEnvironment = config.currentModuleTestEnviroment; 304 | test.queue(); 305 | }, 306 | 307 | // Specify the number of expected assertions to gurantee that failed test (no assertions are run at all) don't slip through. 308 | expect: function(asserts) { 309 | config.current.expected = asserts; 310 | }, 311 | 312 | // Asserts true. 313 | // @example ok( "asdfasdf".length > 5, "There must be at least 5 chars" ); 314 | ok: function(result, msg) { 315 | if (!config.current) { 316 | throw new Error("ok() assertion outside test context, was " + sourceFromStacktrace(2)); 317 | } 318 | result = !!result; 319 | var details = { 320 | result: result, 321 | message: msg 322 | }; 323 | msg = escapeInnerText(msg || (result ? "okay" : "failed")); 324 | if ( !result ) { 325 | var source = sourceFromStacktrace(2); 326 | if (source) { 327 | details.source = source; 328 | msg += '
Source:
' + escapeInnerText(source) + '
'; 329 | } 330 | } 331 | runLoggingCallbacks( 'log', QUnit, details ); 332 | config.current.assertions.push({ 333 | result: result, 334 | message: msg 335 | }); 336 | }, 337 | 338 | // Checks that the first two arguments are equal, with an optional message. Prints out both actual and expected values. 339 | // @example equal( format("Received {0} bytes.", 2), "Received 2 bytes." ); 340 | equal: function(actual, expected, message) { 341 | QUnit.push(expected == actual, actual, expected, message); 342 | }, 343 | 344 | notEqual: function(actual, expected, message) { 345 | QUnit.push(expected != actual, actual, expected, message); 346 | }, 347 | 348 | deepEqual: function(actual, expected, message) { 349 | QUnit.push(QUnit.equiv(actual, expected), actual, expected, message); 350 | }, 351 | 352 | notDeepEqual: function(actual, expected, message) { 353 | QUnit.push(!QUnit.equiv(actual, expected), actual, expected, message); 354 | }, 355 | 356 | strictEqual: function(actual, expected, message) { 357 | QUnit.push(expected === actual, actual, expected, message); 358 | }, 359 | 360 | notStrictEqual: function(actual, expected, message) { 361 | QUnit.push(expected !== actual, actual, expected, message); 362 | }, 363 | 364 | raises: function(block, expected, message) { 365 | var actual, ok = false; 366 | 367 | if (typeof expected === 'string') { 368 | message = expected; 369 | expected = null; 370 | } 371 | 372 | try { 373 | block(); 374 | } catch (e) { 375 | actual = e; 376 | } 377 | 378 | if (actual) { 379 | // we don't want to validate thrown error 380 | if (!expected) { 381 | ok = true; 382 | // expected is a regexp 383 | } else if (QUnit.objectType(expected) === "regexp") { 384 | ok = expected.test(actual); 385 | // expected is a constructor 386 | } else if (actual instanceof expected) { 387 | ok = true; 388 | // expected is a validation function which returns true is validation passed 389 | } else if (expected.call({}, actual) === true) { 390 | ok = true; 391 | } 392 | } 393 | 394 | QUnit.ok(ok, message); 395 | }, 396 | 397 | start: function(count) { 398 | config.semaphore -= count || 1; 399 | if (config.semaphore > 0) { 400 | // don't start until equal number of stop-calls 401 | return; 402 | } 403 | if (config.semaphore < 0) { 404 | // ignore if start is called more often then stop 405 | config.semaphore = 0; 406 | } 407 | // A slight delay, to avoid any current callbacks 408 | if ( defined.setTimeout ) { 409 | window.setTimeout(function() { 410 | if (config.semaphore > 0) { 411 | return; 412 | } 413 | if ( config.timeout ) { 414 | clearTimeout(config.timeout); 415 | } 416 | 417 | config.blocking = false; 418 | process(true); 419 | }, 13); 420 | } else { 421 | config.blocking = false; 422 | process(true); 423 | } 424 | }, 425 | 426 | stop: function(count) { 427 | config.semaphore += count || 1; 428 | config.blocking = true; 429 | 430 | if ( config.testTimeout && defined.setTimeout ) { 431 | clearTimeout(config.timeout); 432 | config.timeout = window.setTimeout(function() { 433 | QUnit.ok( false, "Test timed out" ); 434 | config.semaphore = 1; 435 | QUnit.start(); 436 | }, config.testTimeout); 437 | } 438 | } 439 | }; 440 | 441 | //We want access to the constructor's prototype 442 | (function() { 443 | function F(){} 444 | F.prototype = QUnit; 445 | QUnit = new F(); 446 | //Make F QUnit's constructor so that we can add to the prototype later 447 | QUnit.constructor = F; 448 | }()); 449 | 450 | // deprecated; still export them to window to provide clear error messages 451 | // next step: remove entirely 452 | QUnit.equals = function() { 453 | QUnit.push(false, false, false, "QUnit.equals has been deprecated since 2009 (e88049a0), use QUnit.equal instead"); 454 | }; 455 | QUnit.same = function() { 456 | QUnit.push(false, false, false, "QUnit.same has been deprecated since 2009 (e88049a0), use QUnit.deepEqual instead"); 457 | }; 458 | 459 | // Maintain internal state 460 | var config = { 461 | // The queue of tests to run 462 | queue: [], 463 | 464 | // block until document ready 465 | blocking: true, 466 | 467 | // when enabled, show only failing tests 468 | // gets persisted through sessionStorage and can be changed in UI via checkbox 469 | hidepassed: false, 470 | 471 | // by default, run previously failed tests first 472 | // very useful in combination with "Hide passed tests" checked 473 | reorder: true, 474 | 475 | // by default, modify document.title when suite is done 476 | altertitle: true, 477 | 478 | urlConfig: ['noglobals', 'notrycatch'], 479 | 480 | //logging callback queues 481 | begin: [], 482 | done: [], 483 | log: [], 484 | testStart: [], 485 | testDone: [], 486 | moduleStart: [], 487 | moduleDone: [] 488 | }; 489 | 490 | // Load paramaters 491 | (function() { 492 | var location = window.location || { search: "", protocol: "file:" }, 493 | params = location.search.slice( 1 ).split( "&" ), 494 | length = params.length, 495 | urlParams = {}, 496 | current; 497 | 498 | if ( params[ 0 ] ) { 499 | for ( var i = 0; i < length; i++ ) { 500 | current = params[ i ].split( "=" ); 501 | current[ 0 ] = decodeURIComponent( current[ 0 ] ); 502 | // allow just a key to turn on a flag, e.g., test.html?noglobals 503 | current[ 1 ] = current[ 1 ] ? decodeURIComponent( current[ 1 ] ) : true; 504 | urlParams[ current[ 0 ] ] = current[ 1 ]; 505 | } 506 | } 507 | 508 | QUnit.urlParams = urlParams; 509 | config.filter = urlParams.filter; 510 | 511 | // Figure out if we're running the tests from a server or not 512 | QUnit.isLocal = location.protocol === 'file:'; 513 | }()); 514 | 515 | // Expose the API as global variables, unless an 'exports' 516 | // object exists, in that case we assume we're in CommonJS - export everything at the end 517 | if ( typeof exports === "undefined" || typeof require === "undefined" ) { 518 | extend(window, QUnit); 519 | window.QUnit = QUnit; 520 | } 521 | 522 | // define these after exposing globals to keep them in these QUnit namespace only 523 | extend(QUnit, { 524 | config: config, 525 | 526 | // Initialize the configuration options 527 | init: function() { 528 | extend(config, { 529 | stats: { all: 0, bad: 0 }, 530 | moduleStats: { all: 0, bad: 0 }, 531 | started: +new Date(), 532 | updateRate: 1000, 533 | blocking: false, 534 | autostart: true, 535 | autorun: false, 536 | filter: "", 537 | queue: [], 538 | semaphore: 0 539 | }); 540 | 541 | var qunit = id( "qunit" ); 542 | if ( qunit ) { 543 | qunit.innerHTML = 544 | '

' + escapeInnerText( document.title ) + '

' + 545 | '

' + 546 | '
' + 547 | '

' + 548 | '
    '; 549 | } 550 | 551 | var tests = id( "qunit-tests" ), 552 | banner = id( "qunit-banner" ), 553 | result = id( "qunit-testresult" ); 554 | 555 | if ( tests ) { 556 | tests.innerHTML = ""; 557 | } 558 | 559 | if ( banner ) { 560 | banner.className = ""; 561 | } 562 | 563 | if ( result ) { 564 | result.parentNode.removeChild( result ); 565 | } 566 | 567 | if ( tests ) { 568 | result = document.createElement( "p" ); 569 | result.id = "qunit-testresult"; 570 | result.className = "result"; 571 | tests.parentNode.insertBefore( result, tests ); 572 | result.innerHTML = 'Running...
     '; 573 | } 574 | }, 575 | 576 | // Resets the test setup. Useful for tests that modify the DOM. 577 | // If jQuery is available, uses jQuery's html(), otherwise just innerHTML. 578 | reset: function() { 579 | if ( window.jQuery ) { 580 | jQuery( "#qunit-fixture" ).html( config.fixture ); 581 | } else { 582 | var main = id( 'qunit-fixture' ); 583 | if ( main ) { 584 | main.innerHTML = config.fixture; 585 | } 586 | } 587 | }, 588 | 589 | // Trigger an event on an element. 590 | // @example triggerEvent( document.body, "click" ); 591 | triggerEvent: function( elem, type, event ) { 592 | if ( document.createEvent ) { 593 | event = document.createEvent("MouseEvents"); 594 | event.initMouseEvent(type, true, true, elem.ownerDocument.defaultView, 595 | 0, 0, 0, 0, 0, false, false, false, false, 0, null); 596 | elem.dispatchEvent( event ); 597 | 598 | } else if ( elem.fireEvent ) { 599 | elem.fireEvent("on"+type); 600 | } 601 | }, 602 | 603 | // Safe object type checking 604 | is: function( type, obj ) { 605 | return QUnit.objectType( obj ) == type; 606 | }, 607 | 608 | objectType: function( obj ) { 609 | if (typeof obj === "undefined") { 610 | return "undefined"; 611 | 612 | // consider: typeof null === object 613 | } 614 | if (obj === null) { 615 | return "null"; 616 | } 617 | 618 | var type = toString.call( obj ).match(/^\[object\s(.*)\]$/)[1] || ''; 619 | 620 | switch (type) { 621 | case 'Number': 622 | if (isNaN(obj)) { 623 | return "nan"; 624 | } 625 | return "number"; 626 | case 'String': 627 | case 'Boolean': 628 | case 'Array': 629 | case 'Date': 630 | case 'RegExp': 631 | case 'Function': 632 | return type.toLowerCase(); 633 | } 634 | if (typeof obj === "object") { 635 | return "object"; 636 | } 637 | return undefined; 638 | }, 639 | 640 | push: function(result, actual, expected, message) { 641 | if (!config.current) { 642 | throw new Error("assertion outside test context, was " + sourceFromStacktrace()); 643 | } 644 | var details = { 645 | result: result, 646 | message: message, 647 | actual: actual, 648 | expected: expected 649 | }; 650 | 651 | message = escapeInnerText(message) || (result ? "okay" : "failed"); 652 | message = '' + message + ""; 653 | var output = message; 654 | if (!result) { 655 | expected = escapeInnerText(QUnit.jsDump.parse(expected)); 656 | actual = escapeInnerText(QUnit.jsDump.parse(actual)); 657 | output += ''; 658 | if (actual != expected) { 659 | output += ''; 660 | output += ''; 661 | } 662 | var source = sourceFromStacktrace(); 663 | if (source) { 664 | details.source = source; 665 | output += ''; 666 | } 667 | output += "
    Expected:
    ' + expected + '
    Result:
    ' + actual + '
    Diff:
    ' + QUnit.diff(expected, actual) +'
    Source:
    ' + escapeInnerText(source) + '
    "; 668 | } 669 | 670 | runLoggingCallbacks( 'log', QUnit, details ); 671 | 672 | config.current.assertions.push({ 673 | result: !!result, 674 | message: output 675 | }); 676 | }, 677 | 678 | pushFailure: function(message, source) { 679 | var details = { 680 | result: false, 681 | message: message 682 | }; 683 | var output = escapeInnerText(message); 684 | if (source) { 685 | details.source = source; 686 | output += '
    Source:
    ' + escapeInnerText(source) + '
    '; 687 | } 688 | runLoggingCallbacks( 'log', QUnit, details ); 689 | config.current.assertions.push({ 690 | result: false, 691 | message: output 692 | }); 693 | }, 694 | 695 | url: function( params ) { 696 | params = extend( extend( {}, QUnit.urlParams ), params ); 697 | var querystring = "?", 698 | key; 699 | for ( key in params ) { 700 | if ( !hasOwn.call( params, key ) ) { 701 | continue; 702 | } 703 | querystring += encodeURIComponent( key ) + "=" + 704 | encodeURIComponent( params[ key ] ) + "&"; 705 | } 706 | return window.location.pathname + querystring.slice( 0, -1 ); 707 | }, 708 | 709 | extend: extend, 710 | id: id, 711 | addEvent: addEvent 712 | }); 713 | 714 | //QUnit.constructor is set to the empty F() above so that we can add to it's prototype later 715 | //Doing this allows us to tell if the following methods have been overwritten on the actual 716 | //QUnit object, which is a deprecated way of using the callbacks. 717 | extend(QUnit.constructor.prototype, { 718 | // Logging callbacks; all receive a single argument with the listed properties 719 | // run test/logs.html for any related changes 720 | begin: registerLoggingCallback('begin'), 721 | // done: { failed, passed, total, runtime } 722 | done: registerLoggingCallback('done'), 723 | // log: { result, actual, expected, message } 724 | log: registerLoggingCallback('log'), 725 | // testStart: { name } 726 | testStart: registerLoggingCallback('testStart'), 727 | // testDone: { name, failed, passed, total } 728 | testDone: registerLoggingCallback('testDone'), 729 | // moduleStart: { name } 730 | moduleStart: registerLoggingCallback('moduleStart'), 731 | // moduleDone: { name, failed, passed, total } 732 | moduleDone: registerLoggingCallback('moduleDone') 733 | }); 734 | 735 | if ( typeof document === "undefined" || document.readyState === "complete" ) { 736 | config.autorun = true; 737 | } 738 | 739 | QUnit.load = function() { 740 | runLoggingCallbacks( 'begin', QUnit, {} ); 741 | 742 | // Initialize the config, saving the execution queue 743 | var oldconfig = extend({}, config); 744 | QUnit.init(); 745 | extend(config, oldconfig); 746 | 747 | config.blocking = false; 748 | 749 | var urlConfigHtml = '', len = config.urlConfig.length; 750 | for ( var i = 0, val; i < len; i++ ) { 751 | val = config.urlConfig[i]; 752 | config[val] = QUnit.urlParams[val]; 753 | urlConfigHtml += ''; 754 | } 755 | 756 | var userAgent = id("qunit-userAgent"); 757 | if ( userAgent ) { 758 | userAgent.innerHTML = navigator.userAgent; 759 | } 760 | var banner = id("qunit-header"); 761 | if ( banner ) { 762 | banner.innerHTML = ' ' + banner.innerHTML + ' ' + urlConfigHtml; 763 | addEvent( banner, "change", function( event ) { 764 | var params = {}; 765 | params[ event.target.name ] = event.target.checked ? true : undefined; 766 | window.location = QUnit.url( params ); 767 | }); 768 | } 769 | 770 | var toolbar = id("qunit-testrunner-toolbar"); 771 | if ( toolbar ) { 772 | var filter = document.createElement("input"); 773 | filter.type = "checkbox"; 774 | filter.id = "qunit-filter-pass"; 775 | addEvent( filter, "click", function() { 776 | var ol = document.getElementById("qunit-tests"); 777 | if ( filter.checked ) { 778 | ol.className = ol.className + " hidepass"; 779 | } else { 780 | var tmp = " " + ol.className.replace( /[\n\t\r]/g, " " ) + " "; 781 | ol.className = tmp.replace(/ hidepass /, " "); 782 | } 783 | if ( defined.sessionStorage ) { 784 | if (filter.checked) { 785 | sessionStorage.setItem("qunit-filter-passed-tests", "true"); 786 | } else { 787 | sessionStorage.removeItem("qunit-filter-passed-tests"); 788 | } 789 | } 790 | }); 791 | if ( config.hidepassed || defined.sessionStorage && sessionStorage.getItem("qunit-filter-passed-tests") ) { 792 | filter.checked = true; 793 | var ol = document.getElementById("qunit-tests"); 794 | ol.className = ol.className + " hidepass"; 795 | } 796 | toolbar.appendChild( filter ); 797 | 798 | var label = document.createElement("label"); 799 | label.setAttribute("for", "qunit-filter-pass"); 800 | label.innerHTML = "Hide passed tests"; 801 | toolbar.appendChild( label ); 802 | } 803 | 804 | var main = id('qunit-fixture'); 805 | if ( main ) { 806 | config.fixture = main.innerHTML; 807 | } 808 | 809 | if (config.autostart) { 810 | QUnit.start(); 811 | } 812 | }; 813 | 814 | addEvent(window, "load", QUnit.load); 815 | 816 | // addEvent(window, "error") gives us a useless event object 817 | window.onerror = function( message, file, line ) { 818 | if ( QUnit.config.current ) { 819 | QUnit.pushFailure( message, file + ":" + line ); 820 | } else { 821 | QUnit.test( "global failure", function() { 822 | QUnit.pushFailure( message, file + ":" + line ); 823 | }); 824 | } 825 | }; 826 | 827 | function done() { 828 | config.autorun = true; 829 | 830 | // Log the last module results 831 | if ( config.currentModule ) { 832 | runLoggingCallbacks( 'moduleDone', QUnit, { 833 | name: config.currentModule, 834 | failed: config.moduleStats.bad, 835 | passed: config.moduleStats.all - config.moduleStats.bad, 836 | total: config.moduleStats.all 837 | } ); 838 | } 839 | 840 | var banner = id("qunit-banner"), 841 | tests = id("qunit-tests"), 842 | runtime = +new Date() - config.started, 843 | passed = config.stats.all - config.stats.bad, 844 | html = [ 845 | 'Tests completed in ', 846 | runtime, 847 | ' milliseconds.
    ', 848 | '', 849 | passed, 850 | ' tests of ', 851 | config.stats.all, 852 | ' passed, ', 853 | config.stats.bad, 854 | ' failed.' 855 | ].join(''); 856 | 857 | if ( banner ) { 858 | banner.className = (config.stats.bad ? "qunit-fail" : "qunit-pass"); 859 | } 860 | 861 | if ( tests ) { 862 | id( "qunit-testresult" ).innerHTML = html; 863 | } 864 | 865 | if ( config.altertitle && typeof document !== "undefined" && document.title ) { 866 | // show ✖ for good, ✔ for bad suite result in title 867 | // use escape sequences in case file gets loaded with non-utf-8-charset 868 | document.title = [ 869 | (config.stats.bad ? "\u2716" : "\u2714"), 870 | document.title.replace(/^[\u2714\u2716] /i, "") 871 | ].join(" "); 872 | } 873 | 874 | // clear own sessionStorage items if all tests passed 875 | if ( config.reorder && defined.sessionStorage && config.stats.bad === 0 ) { 876 | for (var key in sessionStorage) { 877 | if (sessionStorage.hasOwnProperty(key) && key.indexOf("qunit-test-") === 0 ) { 878 | sessionStorage.removeItem(key); 879 | } 880 | } 881 | } 882 | 883 | runLoggingCallbacks( 'done', QUnit, { 884 | failed: config.stats.bad, 885 | passed: passed, 886 | total: config.stats.all, 887 | runtime: runtime 888 | } ); 889 | } 890 | 891 | function validTest( name ) { 892 | var filter = config.filter, 893 | run = false; 894 | 895 | if ( !filter ) { 896 | return true; 897 | } 898 | 899 | var not = filter.charAt( 0 ) === "!"; 900 | if ( not ) { 901 | filter = filter.slice( 1 ); 902 | } 903 | 904 | if ( name.indexOf( filter ) !== -1 ) { 905 | return !not; 906 | } 907 | 908 | if ( not ) { 909 | run = true; 910 | } 911 | 912 | return run; 913 | } 914 | 915 | // so far supports only Firefox, Chrome and Opera (buggy) 916 | // could be extended in the future to use something like https://github.com/csnover/TraceKit 917 | function extractStacktrace( e, offset ) { 918 | offset = offset || 3; 919 | if (e.stacktrace) { 920 | // Opera 921 | return e.stacktrace.split("\n")[offset + 3]; 922 | } else if (e.stack) { 923 | // Firefox, Chrome 924 | var stack = e.stack.split("\n"); 925 | if (/^error$/i.test(stack[0])) { 926 | stack.shift(); 927 | } 928 | return stack[offset]; 929 | } else if (e.sourceURL) { 930 | // Safari, PhantomJS 931 | // hopefully one day Safari provides actual stacktraces 932 | // exclude useless self-reference for generated Error objects 933 | if ( /qunit.js$/.test( e.sourceURL ) ) { 934 | return; 935 | } 936 | // for actual exceptions, this is useful 937 | return e.sourceURL + ":" + e.line; 938 | } 939 | } 940 | function sourceFromStacktrace(offset) { 941 | try { 942 | throw new Error(); 943 | } catch ( e ) { 944 | return extractStacktrace( e, offset ); 945 | } 946 | } 947 | 948 | function escapeInnerText(s) { 949 | if (!s) { 950 | return ""; 951 | } 952 | s = s + ""; 953 | return s.replace(/[\&<>]/g, function(s) { 954 | switch(s) { 955 | case "&": return "&"; 956 | case "<": return "<"; 957 | case ">": return ">"; 958 | default: return s; 959 | } 960 | }); 961 | } 962 | 963 | function synchronize( callback, last ) { 964 | config.queue.push( callback ); 965 | 966 | if ( config.autorun && !config.blocking ) { 967 | process(last); 968 | } 969 | } 970 | 971 | function process( last ) { 972 | function next() { 973 | process( last ); 974 | } 975 | var start = new Date().getTime(); 976 | config.depth = config.depth ? config.depth + 1 : 1; 977 | 978 | while ( config.queue.length && !config.blocking ) { 979 | if ( !defined.setTimeout || config.updateRate <= 0 || ( ( new Date().getTime() - start ) < config.updateRate ) ) { 980 | config.queue.shift()(); 981 | } else { 982 | window.setTimeout( next, 13 ); 983 | break; 984 | } 985 | } 986 | config.depth--; 987 | if ( last && !config.blocking && !config.queue.length && config.depth === 0 ) { 988 | done(); 989 | } 990 | } 991 | 992 | function saveGlobal() { 993 | config.pollution = []; 994 | 995 | if ( config.noglobals ) { 996 | for ( var key in window ) { 997 | if ( !hasOwn.call( window, key ) ) { 998 | continue; 999 | } 1000 | config.pollution.push( key ); 1001 | } 1002 | } 1003 | } 1004 | 1005 | function checkPollution( name ) { 1006 | var old = config.pollution; 1007 | saveGlobal(); 1008 | 1009 | var newGlobals = diff( config.pollution, old ); 1010 | if ( newGlobals.length > 0 ) { 1011 | QUnit.pushFailure( "Introduced global variable(s): " + newGlobals.join(", ") ); 1012 | } 1013 | 1014 | var deletedGlobals = diff( old, config.pollution ); 1015 | if ( deletedGlobals.length > 0 ) { 1016 | QUnit.pushFailure( "Deleted global variable(s): " + deletedGlobals.join(", ") ); 1017 | } 1018 | } 1019 | 1020 | // returns a new Array with the elements that are in a but not in b 1021 | function diff( a, b ) { 1022 | var result = a.slice(); 1023 | for ( var i = 0; i < result.length; i++ ) { 1024 | for ( var j = 0; j < b.length; j++ ) { 1025 | if ( result[i] === b[j] ) { 1026 | result.splice(i, 1); 1027 | i--; 1028 | break; 1029 | } 1030 | } 1031 | } 1032 | return result; 1033 | } 1034 | 1035 | function extend(a, b) { 1036 | for ( var prop in b ) { 1037 | if ( b[prop] === undefined ) { 1038 | delete a[prop]; 1039 | 1040 | // Avoid "Member not found" error in IE8 caused by setting window.constructor 1041 | } else if ( prop !== "constructor" || a !== window ) { 1042 | a[prop] = b[prop]; 1043 | } 1044 | } 1045 | 1046 | return a; 1047 | } 1048 | 1049 | function addEvent(elem, type, fn) { 1050 | if ( elem.addEventListener ) { 1051 | elem.addEventListener( type, fn, false ); 1052 | } else if ( elem.attachEvent ) { 1053 | elem.attachEvent( "on" + type, fn ); 1054 | } else { 1055 | fn(); 1056 | } 1057 | } 1058 | 1059 | function id(name) { 1060 | return !!(typeof document !== "undefined" && document && document.getElementById) && 1061 | document.getElementById( name ); 1062 | } 1063 | 1064 | function registerLoggingCallback(key){ 1065 | return function(callback){ 1066 | config[key].push( callback ); 1067 | }; 1068 | } 1069 | 1070 | // Supports deprecated method of completely overwriting logging callbacks 1071 | function runLoggingCallbacks(key, scope, args) { 1072 | //debugger; 1073 | var callbacks; 1074 | if ( QUnit.hasOwnProperty(key) ) { 1075 | QUnit[key].call(scope, args); 1076 | } else { 1077 | callbacks = config[key]; 1078 | for( var i = 0; i < callbacks.length; i++ ) { 1079 | callbacks[i].call( scope, args ); 1080 | } 1081 | } 1082 | } 1083 | 1084 | // Test for equality any JavaScript type. 1085 | // Author: Philippe Rathé 1086 | QUnit.equiv = (function() { 1087 | 1088 | var innerEquiv; // the real equiv function 1089 | var callers = []; // stack to decide between skip/abort functions 1090 | var parents = []; // stack to avoiding loops from circular referencing 1091 | 1092 | // Call the o related callback with the given arguments. 1093 | function bindCallbacks(o, callbacks, args) { 1094 | var prop = QUnit.objectType(o); 1095 | if (prop) { 1096 | if (QUnit.objectType(callbacks[prop]) === "function") { 1097 | return callbacks[prop].apply(callbacks, args); 1098 | } else { 1099 | return callbacks[prop]; // or undefined 1100 | } 1101 | } 1102 | } 1103 | 1104 | var getProto = Object.getPrototypeOf || function (obj) { 1105 | return obj.__proto__; 1106 | }; 1107 | 1108 | var callbacks = (function () { 1109 | 1110 | // for string, boolean, number and null 1111 | function useStrictEquality(b, a) { 1112 | if (b instanceof a.constructor || a instanceof b.constructor) { 1113 | // to catch short annotaion VS 'new' annotation of a 1114 | // declaration 1115 | // e.g. var i = 1; 1116 | // var j = new Number(1); 1117 | return a == b; 1118 | } else { 1119 | return a === b; 1120 | } 1121 | } 1122 | 1123 | return { 1124 | "string" : useStrictEquality, 1125 | "boolean" : useStrictEquality, 1126 | "number" : useStrictEquality, 1127 | "null" : useStrictEquality, 1128 | "undefined" : useStrictEquality, 1129 | 1130 | "nan" : function(b) { 1131 | return isNaN(b); 1132 | }, 1133 | 1134 | "date" : function(b, a) { 1135 | return QUnit.objectType(b) === "date" && a.valueOf() === b.valueOf(); 1136 | }, 1137 | 1138 | "regexp" : function(b, a) { 1139 | return QUnit.objectType(b) === "regexp" && 1140 | // the regex itself 1141 | a.source === b.source && 1142 | // and its modifers 1143 | a.global === b.global && 1144 | // (gmi) ... 1145 | a.ignoreCase === b.ignoreCase && 1146 | a.multiline === b.multiline; 1147 | }, 1148 | 1149 | // - skip when the property is a method of an instance (OOP) 1150 | // - abort otherwise, 1151 | // initial === would have catch identical references anyway 1152 | "function" : function() { 1153 | var caller = callers[callers.length - 1]; 1154 | return caller !== Object && typeof caller !== "undefined"; 1155 | }, 1156 | 1157 | "array" : function(b, a) { 1158 | var i, j, loop; 1159 | var len; 1160 | 1161 | // b could be an object literal here 1162 | if (QUnit.objectType(b) !== "array") { 1163 | return false; 1164 | } 1165 | 1166 | len = a.length; 1167 | if (len !== b.length) { // safe and faster 1168 | return false; 1169 | } 1170 | 1171 | // track reference to avoid circular references 1172 | parents.push(a); 1173 | for (i = 0; i < len; i++) { 1174 | loop = false; 1175 | for (j = 0; j < parents.length; j++) { 1176 | if (parents[j] === a[i]) { 1177 | loop = true;// dont rewalk array 1178 | } 1179 | } 1180 | if (!loop && !innerEquiv(a[i], b[i])) { 1181 | parents.pop(); 1182 | return false; 1183 | } 1184 | } 1185 | parents.pop(); 1186 | return true; 1187 | }, 1188 | 1189 | "object" : function(b, a) { 1190 | var i, j, loop; 1191 | var eq = true; // unless we can proove it 1192 | var aProperties = [], bProperties = []; // collection of 1193 | // strings 1194 | 1195 | // comparing constructors is more strict than using 1196 | // instanceof 1197 | if (a.constructor !== b.constructor) { 1198 | // Allow objects with no prototype to be equivalent to 1199 | // objects with Object as their constructor. 1200 | if (!((getProto(a) === null && getProto(b) === Object.prototype) || 1201 | (getProto(b) === null && getProto(a) === Object.prototype))) 1202 | { 1203 | return false; 1204 | } 1205 | } 1206 | 1207 | // stack constructor before traversing properties 1208 | callers.push(a.constructor); 1209 | // track reference to avoid circular references 1210 | parents.push(a); 1211 | 1212 | for (i in a) { // be strict: don't ensures hasOwnProperty 1213 | // and go deep 1214 | loop = false; 1215 | for (j = 0; j < parents.length; j++) { 1216 | if (parents[j] === a[i]) { 1217 | // don't go down the same path twice 1218 | loop = true; 1219 | } 1220 | } 1221 | aProperties.push(i); // collect a's properties 1222 | 1223 | if (!loop && !innerEquiv(a[i], b[i])) { 1224 | eq = false; 1225 | break; 1226 | } 1227 | } 1228 | 1229 | callers.pop(); // unstack, we are done 1230 | parents.pop(); 1231 | 1232 | for (i in b) { 1233 | bProperties.push(i); // collect b's properties 1234 | } 1235 | 1236 | // Ensures identical properties name 1237 | return eq && innerEquiv(aProperties.sort(), bProperties.sort()); 1238 | } 1239 | }; 1240 | }()); 1241 | 1242 | innerEquiv = function() { // can take multiple arguments 1243 | var args = Array.prototype.slice.apply(arguments); 1244 | if (args.length < 2) { 1245 | return true; // end transition 1246 | } 1247 | 1248 | return (function(a, b) { 1249 | if (a === b) { 1250 | return true; // catch the most you can 1251 | } else if (a === null || b === null || typeof a === "undefined" || 1252 | typeof b === "undefined" || 1253 | QUnit.objectType(a) !== QUnit.objectType(b)) { 1254 | return false; // don't lose time with error prone cases 1255 | } else { 1256 | return bindCallbacks(a, callbacks, [ b, a ]); 1257 | } 1258 | 1259 | // apply transition with (1..n) arguments 1260 | }(args[0], args[1]) && arguments.callee.apply(this, args.splice(1, args.length - 1))); 1261 | }; 1262 | 1263 | return innerEquiv; 1264 | 1265 | }()); 1266 | 1267 | /** 1268 | * jsDump Copyright (c) 2008 Ariel Flesler - aflesler(at)gmail(dot)com | 1269 | * http://flesler.blogspot.com Licensed under BSD 1270 | * (http://www.opensource.org/licenses/bsd-license.php) Date: 5/15/2008 1271 | * 1272 | * @projectDescription Advanced and extensible data dumping for Javascript. 1273 | * @version 1.0.0 1274 | * @author Ariel Flesler 1275 | * @link {http://flesler.blogspot.com/2008/05/jsdump-pretty-dump-of-any-javascript.html} 1276 | */ 1277 | QUnit.jsDump = (function() { 1278 | function quote( str ) { 1279 | return '"' + str.toString().replace(/"/g, '\\"') + '"'; 1280 | } 1281 | function literal( o ) { 1282 | return o + ''; 1283 | } 1284 | function join( pre, arr, post ) { 1285 | var s = jsDump.separator(), 1286 | base = jsDump.indent(), 1287 | inner = jsDump.indent(1); 1288 | if ( arr.join ) { 1289 | arr = arr.join( ',' + s + inner ); 1290 | } 1291 | if ( !arr ) { 1292 | return pre + post; 1293 | } 1294 | return [ pre, inner + arr, base + post ].join(s); 1295 | } 1296 | function array( arr, stack ) { 1297 | var i = arr.length, ret = new Array(i); 1298 | this.up(); 1299 | while ( i-- ) { 1300 | ret[i] = this.parse( arr[i] , undefined , stack); 1301 | } 1302 | this.down(); 1303 | return join( '[', ret, ']' ); 1304 | } 1305 | 1306 | var reName = /^function (\w+)/; 1307 | 1308 | var jsDump = { 1309 | parse: function( obj, type, stack ) { //type is used mostly internally, you can fix a (custom)type in advance 1310 | stack = stack || [ ]; 1311 | var parser = this.parsers[ type || this.typeOf(obj) ]; 1312 | type = typeof parser; 1313 | var inStack = inArray(obj, stack); 1314 | if (inStack != -1) { 1315 | return 'recursion('+(inStack - stack.length)+')'; 1316 | } 1317 | //else 1318 | if (type == 'function') { 1319 | stack.push(obj); 1320 | var res = parser.call( this, obj, stack ); 1321 | stack.pop(); 1322 | return res; 1323 | } 1324 | // else 1325 | return (type == 'string') ? parser : this.parsers.error; 1326 | }, 1327 | typeOf: function( obj ) { 1328 | var type; 1329 | if ( obj === null ) { 1330 | type = "null"; 1331 | } else if (typeof obj === "undefined") { 1332 | type = "undefined"; 1333 | } else if (QUnit.is("RegExp", obj)) { 1334 | type = "regexp"; 1335 | } else if (QUnit.is("Date", obj)) { 1336 | type = "date"; 1337 | } else if (QUnit.is("Function", obj)) { 1338 | type = "function"; 1339 | } else if (typeof obj.setInterval !== undefined && typeof obj.document !== "undefined" && typeof obj.nodeType === "undefined") { 1340 | type = "window"; 1341 | } else if (obj.nodeType === 9) { 1342 | type = "document"; 1343 | } else if (obj.nodeType) { 1344 | type = "node"; 1345 | } else if ( 1346 | // native arrays 1347 | toString.call( obj ) === "[object Array]" || 1348 | // NodeList objects 1349 | ( typeof obj.length === "number" && typeof obj.item !== "undefined" && ( obj.length ? obj.item(0) === obj[0] : ( obj.item( 0 ) === null && typeof obj[0] === "undefined" ) ) ) 1350 | ) { 1351 | type = "array"; 1352 | } else { 1353 | type = typeof obj; 1354 | } 1355 | return type; 1356 | }, 1357 | separator: function() { 1358 | return this.multiline ? this.HTML ? '
    ' : '\n' : this.HTML ? ' ' : ' '; 1359 | }, 1360 | indent: function( extra ) {// extra can be a number, shortcut for increasing-calling-decreasing 1361 | if ( !this.multiline ) { 1362 | return ''; 1363 | } 1364 | var chr = this.indentChar; 1365 | if ( this.HTML ) { 1366 | chr = chr.replace(/\t/g,' ').replace(/ /g,' '); 1367 | } 1368 | return new Array( this._depth_ + (extra||0) ).join(chr); 1369 | }, 1370 | up: function( a ) { 1371 | this._depth_ += a || 1; 1372 | }, 1373 | down: function( a ) { 1374 | this._depth_ -= a || 1; 1375 | }, 1376 | setParser: function( name, parser ) { 1377 | this.parsers[name] = parser; 1378 | }, 1379 | // The next 3 are exposed so you can use them 1380 | quote: quote, 1381 | literal: literal, 1382 | join: join, 1383 | // 1384 | _depth_: 1, 1385 | // This is the list of parsers, to modify them, use jsDump.setParser 1386 | parsers: { 1387 | window: '[Window]', 1388 | document: '[Document]', 1389 | error: '[ERROR]', //when no parser is found, shouldn't happen 1390 | unknown: '[Unknown]', 1391 | 'null': 'null', 1392 | 'undefined': 'undefined', 1393 | 'function': function( fn ) { 1394 | var ret = 'function', 1395 | name = 'name' in fn ? fn.name : (reName.exec(fn)||[])[1];//functions never have name in IE 1396 | if ( name ) { 1397 | ret += ' ' + name; 1398 | } 1399 | ret += '('; 1400 | 1401 | ret = [ ret, QUnit.jsDump.parse( fn, 'functionArgs' ), '){'].join(''); 1402 | return join( ret, QUnit.jsDump.parse(fn,'functionCode'), '}' ); 1403 | }, 1404 | array: array, 1405 | nodelist: array, 1406 | 'arguments': array, 1407 | object: function( map, stack ) { 1408 | var ret = [ ], keys, key, val, i; 1409 | QUnit.jsDump.up(); 1410 | if (Object.keys) { 1411 | keys = Object.keys( map ); 1412 | } else { 1413 | keys = []; 1414 | for (key in map) { keys.push( key ); } 1415 | } 1416 | keys.sort(); 1417 | for (i = 0; i < keys.length; i++) { 1418 | key = keys[ i ]; 1419 | val = map[ key ]; 1420 | ret.push( QUnit.jsDump.parse( key, 'key' ) + ': ' + QUnit.jsDump.parse( val, undefined, stack ) ); 1421 | } 1422 | QUnit.jsDump.down(); 1423 | return join( '{', ret, '}' ); 1424 | }, 1425 | node: function( node ) { 1426 | var open = QUnit.jsDump.HTML ? '<' : '<', 1427 | close = QUnit.jsDump.HTML ? '>' : '>'; 1428 | 1429 | var tag = node.nodeName.toLowerCase(), 1430 | ret = open + tag; 1431 | 1432 | for ( var a in QUnit.jsDump.DOMAttrs ) { 1433 | var val = node[QUnit.jsDump.DOMAttrs[a]]; 1434 | if ( val ) { 1435 | ret += ' ' + a + '=' + QUnit.jsDump.parse( val, 'attribute' ); 1436 | } 1437 | } 1438 | return ret + close + open + '/' + tag + close; 1439 | }, 1440 | functionArgs: function( fn ) {//function calls it internally, it's the arguments part of the function 1441 | var l = fn.length; 1442 | if ( !l ) { 1443 | return ''; 1444 | } 1445 | 1446 | var args = new Array(l); 1447 | while ( l-- ) { 1448 | args[l] = String.fromCharCode(97+l);//97 is 'a' 1449 | } 1450 | return ' ' + args.join(', ') + ' '; 1451 | }, 1452 | key: quote, //object calls it internally, the key part of an item in a map 1453 | functionCode: '[code]', //function calls it internally, it's the content of the function 1454 | attribute: quote, //node calls it internally, it's an html attribute value 1455 | string: quote, 1456 | date: quote, 1457 | regexp: literal, //regex 1458 | number: literal, 1459 | 'boolean': literal 1460 | }, 1461 | DOMAttrs:{//attributes to dump from nodes, name=>realName 1462 | id:'id', 1463 | name:'name', 1464 | 'class':'className' 1465 | }, 1466 | HTML:false,//if true, entities are escaped ( <, >, \t, space and \n ) 1467 | indentChar:' ',//indentation unit 1468 | multiline:true //if true, items in a collection, are separated by a \n, else just a space. 1469 | }; 1470 | 1471 | return jsDump; 1472 | }()); 1473 | 1474 | // from Sizzle.js 1475 | function getText( elems ) { 1476 | var ret = "", elem; 1477 | 1478 | for ( var i = 0; elems[i]; i++ ) { 1479 | elem = elems[i]; 1480 | 1481 | // Get the text from text nodes and CDATA nodes 1482 | if ( elem.nodeType === 3 || elem.nodeType === 4 ) { 1483 | ret += elem.nodeValue; 1484 | 1485 | // Traverse everything else, except comment nodes 1486 | } else if ( elem.nodeType !== 8 ) { 1487 | ret += getText( elem.childNodes ); 1488 | } 1489 | } 1490 | 1491 | return ret; 1492 | } 1493 | 1494 | //from jquery.js 1495 | function inArray( elem, array ) { 1496 | if ( array.indexOf ) { 1497 | return array.indexOf( elem ); 1498 | } 1499 | 1500 | for ( var i = 0, length = array.length; i < length; i++ ) { 1501 | if ( array[ i ] === elem ) { 1502 | return i; 1503 | } 1504 | } 1505 | 1506 | return -1; 1507 | } 1508 | 1509 | /* 1510 | * Javascript Diff Algorithm 1511 | * By John Resig (http://ejohn.org/) 1512 | * Modified by Chu Alan "sprite" 1513 | * 1514 | * Released under the MIT license. 1515 | * 1516 | * More Info: 1517 | * http://ejohn.org/projects/javascript-diff-algorithm/ 1518 | * 1519 | * Usage: QUnit.diff(expected, actual) 1520 | * 1521 | * QUnit.diff("the quick brown fox jumped over", "the quick fox jumps over") == "the quick brown fox jumped jumps over" 1522 | */ 1523 | QUnit.diff = (function() { 1524 | function diff(o, n) { 1525 | var ns = {}; 1526 | var os = {}; 1527 | var i; 1528 | 1529 | for (i = 0; i < n.length; i++) { 1530 | if (ns[n[i]] == null) { 1531 | ns[n[i]] = { 1532 | rows: [], 1533 | o: null 1534 | }; 1535 | } 1536 | ns[n[i]].rows.push(i); 1537 | } 1538 | 1539 | for (i = 0; i < o.length; i++) { 1540 | if (os[o[i]] == null) { 1541 | os[o[i]] = { 1542 | rows: [], 1543 | n: null 1544 | }; 1545 | } 1546 | os[o[i]].rows.push(i); 1547 | } 1548 | 1549 | for (i in ns) { 1550 | if ( !hasOwn.call( ns, i ) ) { 1551 | continue; 1552 | } 1553 | if (ns[i].rows.length == 1 && typeof(os[i]) != "undefined" && os[i].rows.length == 1) { 1554 | n[ns[i].rows[0]] = { 1555 | text: n[ns[i].rows[0]], 1556 | row: os[i].rows[0] 1557 | }; 1558 | o[os[i].rows[0]] = { 1559 | text: o[os[i].rows[0]], 1560 | row: ns[i].rows[0] 1561 | }; 1562 | } 1563 | } 1564 | 1565 | for (i = 0; i < n.length - 1; i++) { 1566 | if (n[i].text != null && n[i + 1].text == null && n[i].row + 1 < o.length && o[n[i].row + 1].text == null && 1567 | n[i + 1] == o[n[i].row + 1]) { 1568 | n[i + 1] = { 1569 | text: n[i + 1], 1570 | row: n[i].row + 1 1571 | }; 1572 | o[n[i].row + 1] = { 1573 | text: o[n[i].row + 1], 1574 | row: i + 1 1575 | }; 1576 | } 1577 | } 1578 | 1579 | for (i = n.length - 1; i > 0; i--) { 1580 | if (n[i].text != null && n[i - 1].text == null && n[i].row > 0 && o[n[i].row - 1].text == null && 1581 | n[i - 1] == o[n[i].row - 1]) { 1582 | n[i - 1] = { 1583 | text: n[i - 1], 1584 | row: n[i].row - 1 1585 | }; 1586 | o[n[i].row - 1] = { 1587 | text: o[n[i].row - 1], 1588 | row: i - 1 1589 | }; 1590 | } 1591 | } 1592 | 1593 | return { 1594 | o: o, 1595 | n: n 1596 | }; 1597 | } 1598 | 1599 | return function(o, n) { 1600 | o = o.replace(/\s+$/, ''); 1601 | n = n.replace(/\s+$/, ''); 1602 | var out = diff(o === "" ? [] : o.split(/\s+/), n === "" ? [] : n.split(/\s+/)); 1603 | 1604 | var str = ""; 1605 | var i; 1606 | 1607 | var oSpace = o.match(/\s+/g); 1608 | if (oSpace == null) { 1609 | oSpace = [" "]; 1610 | } 1611 | else { 1612 | oSpace.push(" "); 1613 | } 1614 | var nSpace = n.match(/\s+/g); 1615 | if (nSpace == null) { 1616 | nSpace = [" "]; 1617 | } 1618 | else { 1619 | nSpace.push(" "); 1620 | } 1621 | 1622 | if (out.n.length === 0) { 1623 | for (i = 0; i < out.o.length; i++) { 1624 | str += '' + out.o[i] + oSpace[i] + ""; 1625 | } 1626 | } 1627 | else { 1628 | if (out.n[0].text == null) { 1629 | for (n = 0; n < out.o.length && out.o[n].text == null; n++) { 1630 | str += '' + out.o[n] + oSpace[n] + ""; 1631 | } 1632 | } 1633 | 1634 | for (i = 0; i < out.n.length; i++) { 1635 | if (out.n[i].text == null) { 1636 | str += '' + out.n[i] + nSpace[i] + ""; 1637 | } 1638 | else { 1639 | var pre = ""; 1640 | 1641 | for (n = out.n[i].row + 1; n < out.o.length && out.o[n].text == null; n++) { 1642 | pre += '' + out.o[n] + oSpace[n] + ""; 1643 | } 1644 | str += " " + out.n[i].text + nSpace[i] + pre; 1645 | } 1646 | } 1647 | } 1648 | 1649 | return str; 1650 | }; 1651 | }()); 1652 | 1653 | // for CommonJS enviroments, export everything 1654 | if ( typeof exports !== "undefined" || typeof require !== "undefined" ) { 1655 | extend(exports, QUnit); 1656 | } 1657 | 1658 | // get at whatever the global object is, like window in browsers 1659 | }( (function() {return this;}.call()) )); 1660 | -------------------------------------------------------------------------------- /app/vendor/ember-data.js: -------------------------------------------------------------------------------- 1 | 2 | (function(exports) { 3 | window.DS = Ember.Namespace.create(); 4 | 5 | })({}); 6 | 7 | 8 | (function(exports) { 9 | DS.Adapter = Ember.Object.extend({ 10 | commit: function(store, commitDetails) { 11 | commitDetails.updated.eachType(function(type, array) { 12 | this.updateRecords(store, type, array.slice()); 13 | }, this); 14 | 15 | commitDetails.created.eachType(function(type, array) { 16 | this.createRecords(store, type, array.slice()); 17 | }, this); 18 | 19 | commitDetails.deleted.eachType(function(type, array) { 20 | this.deleteRecords(store, type, array.slice()); 21 | }, this); 22 | }, 23 | 24 | createRecords: function(store, type, models) { 25 | models.forEach(function(model) { 26 | this.createRecord(store, type, model); 27 | }, this); 28 | }, 29 | 30 | updateRecords: function(store, type, models) { 31 | models.forEach(function(model) { 32 | this.updateRecord(store, type, model); 33 | }, this); 34 | }, 35 | 36 | deleteRecords: function(store, type, models) { 37 | models.forEach(function(model) { 38 | this.deleteRecord(store, type, model); 39 | }, this); 40 | }, 41 | 42 | findMany: function(store, type, ids) { 43 | ids.forEach(function(id) { 44 | this.find(store, type, id); 45 | }, this); 46 | } 47 | }); 48 | })({}); 49 | 50 | 51 | (function(exports) { 52 | DS.fixtureAdapter = DS.Adapter.create({ 53 | find: function(store, type, id) { 54 | var fixtures = type.FIXTURES; 55 | 56 | ember_assert("Unable to find fixtures for model type "+type.toString(), !!fixtures); 57 | if (fixtures.hasLoaded) { return; } 58 | 59 | setTimeout(function() { 60 | store.loadMany(type, fixtures); 61 | fixtures.hasLoaded = true; 62 | }, 300); 63 | }, 64 | 65 | findMany: function() { 66 | this.find.apply(this, arguments); 67 | }, 68 | 69 | findAll: function(store, type) { 70 | var fixtures = type.FIXTURES; 71 | 72 | ember_assert("Unable to find fixtures for model type "+type.toString(), !!fixtures); 73 | 74 | var ids = fixtures.map(function(item, index, self){ return item.id; }); 75 | store.loadMany(type, ids, fixtures); 76 | } 77 | 78 | }); 79 | 80 | })({}); 81 | 82 | 83 | (function(exports) { 84 | /*global jQuery*/ 85 | var get = Ember.get, set = Ember.set, getPath = Ember.getPath; 86 | 87 | DS.RESTAdapter = DS.Adapter.extend({ 88 | createRecord: function(store, type, model) { 89 | var root = this.rootForType(type); 90 | 91 | var data = {}; 92 | data[root] = get(model, 'data'); 93 | 94 | this.ajax("/" + this.pluralize(root), "POST", { 95 | data: data, 96 | success: function(json) { 97 | this.sideload(store, type, json, root); 98 | store.didCreateRecord(model, json[root]); 99 | } 100 | }); 101 | }, 102 | 103 | createRecords: function(store, type, models) { 104 | if (get(this, 'bulkCommit') === false) { 105 | return this._super(store, type, models); 106 | } 107 | 108 | var root = this.rootForType(type), 109 | plural = this.pluralize(root); 110 | 111 | var data = {}; 112 | data[plural] = models.map(function(model) { 113 | return get(model, 'data'); 114 | }); 115 | 116 | this.ajax("/" + this.pluralize(root), "POST", { 117 | data: data, 118 | 119 | success: function(json) { 120 | this.sideload(store, type, json, plural); 121 | store.didCreateRecords(type, models, json[plural]); 122 | } 123 | }); 124 | }, 125 | 126 | updateRecord: function(store, type, model) { 127 | var id = get(model, 'id'); 128 | var root = this.rootForType(type); 129 | 130 | var data = {}; 131 | data[root] = get(model, 'data'); 132 | 133 | var url = ["", this.pluralize(root), id].join("/"); 134 | 135 | this.ajax(url, "PUT", { 136 | data: data, 137 | success: function(json) { 138 | this.sideload(store, type, json, root); 139 | store.didUpdateRecord(model, json[root]); 140 | } 141 | }); 142 | }, 143 | 144 | updateRecords: function(store, type, models) { 145 | if (get(this, 'bulkCommit') === false) { 146 | return this._super(store, type, models); 147 | } 148 | 149 | var root = this.rootForType(type), 150 | plural = this.pluralize(root); 151 | 152 | var data = {}; 153 | data[plural] = models.map(function(model) { 154 | return get(model, 'data'); 155 | }); 156 | 157 | this.ajax("/" + this.pluralize(root) + "/bulk", "PUT", { 158 | data: data, 159 | success: function(json) { 160 | this.sideload(store, type, json, plural); 161 | store.didUpdateRecords(models, json[plural]); 162 | } 163 | }); 164 | }, 165 | 166 | deleteRecord: function(store, type, model) { 167 | var id = get(model, 'id'); 168 | var root = this.rootForType(type); 169 | 170 | var url = ["", this.pluralize(root), id].join("/"); 171 | 172 | this.ajax(url, "DELETE", { 173 | success: function(json) { 174 | if (json) { this.sideload(store, type, json); } 175 | store.didDeleteRecord(model); 176 | } 177 | }); 178 | }, 179 | 180 | deleteRecords: function(store, type, models) { 181 | if (get(this, 'bulkCommit') === false) { 182 | return this._super(store, type, models); 183 | } 184 | 185 | var root = this.rootForType(type), 186 | plural = this.pluralize(root); 187 | 188 | var data = {}; 189 | data[plural] = models.map(function(model) { 190 | return get(model, 'id'); 191 | }); 192 | 193 | this.ajax("/" + this.pluralize(root) + "/bulk", "DELETE", { 194 | data: data, 195 | success: function(json) { 196 | if (json) { this.sideload(store, type, json); } 197 | store.didDeleteRecords(models); 198 | } 199 | }); 200 | }, 201 | 202 | find: function(store, type, id) { 203 | var root = this.rootForType(type); 204 | 205 | var url = ["", this.pluralize(root), id].join("/"); 206 | 207 | this.ajax(url, "GET", { 208 | success: function(json) { 209 | store.load(type, json[root]); 210 | this.sideload(store, type, json, root); 211 | } 212 | }); 213 | }, 214 | 215 | findMany: function(store, type, ids) { 216 | var root = this.rootForType(type), plural = this.pluralize(root); 217 | 218 | this.ajax("/" + plural, "GET", { 219 | data: { ids: ids }, 220 | success: function(json) { 221 | store.loadMany(type, ids, json[plural]); 222 | this.sideload(store, type, json, plural); 223 | } 224 | }); 225 | }, 226 | 227 | findAll: function(store, type) { 228 | var root = this.rootForType(type), plural = this.pluralize(root); 229 | 230 | this.ajax("/" + plural, "GET", { 231 | success: function(json) { 232 | store.loadMany(type, json[plural]); 233 | this.sideload(store, type, json, plural); 234 | } 235 | }); 236 | }, 237 | 238 | findQuery: function(store, type, query, modelArray) { 239 | var root = this.rootForType(type), plural = this.pluralize(root); 240 | 241 | this.ajax("/" + plural, "GET", { 242 | data: query, 243 | success: function(json) { 244 | modelArray.load(json[plural]); 245 | this.sideload(store, type, json, plural); 246 | } 247 | }); 248 | }, 249 | 250 | // HELPERS 251 | 252 | plurals: {}, 253 | 254 | // define a plurals hash in your subclass to define 255 | // special-case pluralization 256 | pluralize: function(name) { 257 | return this.plurals[name] || name + "s"; 258 | }, 259 | 260 | rootForType: function(type) { 261 | if (type.url) { return type.url; } 262 | 263 | // use the last part of the name as the URL 264 | var parts = type.toString().split("."); 265 | var name = parts[parts.length - 1]; 266 | return name.replace(/([A-Z])/g, '_$1').toLowerCase().slice(1); 267 | }, 268 | 269 | ajax: function(url, type, hash) { 270 | hash.url = url; 271 | hash.type = type; 272 | hash.dataType = 'json'; 273 | hash.contentType = 'application/json'; 274 | hash.context = this; 275 | 276 | if (hash.data) { 277 | hash.data = JSON.stringify(hash.data); 278 | } 279 | 280 | jQuery.ajax(hash); 281 | }, 282 | 283 | sideload: function(store, type, json, root) { 284 | var sideloadedType, mappings; 285 | 286 | for (var prop in json) { 287 | if (!json.hasOwnProperty(prop)) { continue; } 288 | if (prop === root) { continue; } 289 | 290 | sideloadedType = type.typeForAssociation(prop); 291 | 292 | if (!sideloadedType) { 293 | mappings = get(this, 'mappings'); 294 | 295 | ember_assert("Your server returned a hash with the key " + prop + " but you have no mappings", !!mappings); 296 | 297 | sideloadedType = get(get(this, 'mappings'), prop); 298 | 299 | ember_assert("Your server returned a hash with the key " + prop + " but you have no mapping for it", !!sideloadedType); 300 | } 301 | 302 | this.loadValue(store, sideloadedType, json[prop]); 303 | } 304 | }, 305 | 306 | loadValue: function(store, type, value) { 307 | if (value instanceof Array) { 308 | store.loadMany(type, value); 309 | } else { 310 | store.load(type, value); 311 | } 312 | } 313 | }); 314 | 315 | 316 | })({}); 317 | 318 | 319 | (function(exports) { 320 | var get = Ember.get, set = Ember.set; 321 | 322 | /** 323 | A model array is an array that contains records of a certain type. The model 324 | array materializes records as needed when they are retrieved for the first 325 | time. You should not create model arrays yourself. Instead, an instance of 326 | DS.ModelArray or its subclasses will be returned by your application's store 327 | in response to queries. 328 | */ 329 | 330 | DS.ModelArray = Ember.ArrayProxy.extend({ 331 | 332 | /** 333 | The model type contained by this model array. 334 | 335 | @type DS.Model 336 | */ 337 | type: null, 338 | 339 | // The array of client ids backing the model array. When a 340 | // record is requested from the model array, the record 341 | // for the client id at the same index is materialized, if 342 | // necessary, by the store. 343 | content: null, 344 | 345 | // The store that created this model array. 346 | store: null, 347 | 348 | init: function() { 349 | set(this, 'modelCache', Ember.A([])); 350 | this._super(); 351 | }, 352 | 353 | arrayDidChange: function(array, index, removed, added) { 354 | var modelCache = get(this, 'modelCache'); 355 | modelCache.replace(index, 0, new Array(added)); 356 | 357 | this._super(array, index, removed, added); 358 | }, 359 | 360 | arrayWillChange: function(array, index, removed, added) { 361 | this._super(array, index, removed, added); 362 | 363 | var modelCache = get(this, 'modelCache'); 364 | modelCache.replace(index, removed); 365 | }, 366 | 367 | objectAtContent: function(index) { 368 | var modelCache = get(this, 'modelCache'); 369 | var model = modelCache.objectAt(index); 370 | 371 | if (!model) { 372 | var store = get(this, 'store'); 373 | var content = get(this, 'content'); 374 | 375 | var contentObject = content.objectAt(index); 376 | 377 | if (contentObject !== undefined) { 378 | model = store.findByClientId(get(this, 'type'), contentObject); 379 | modelCache.replace(index, 1, [model]); 380 | } 381 | } 382 | 383 | return model; 384 | } 385 | }); 386 | 387 | })({}); 388 | 389 | 390 | (function(exports) { 391 | var get = Ember.get; 392 | 393 | DS.FilteredModelArray = DS.ModelArray.extend({ 394 | filterFunction: null, 395 | 396 | replace: function() { 397 | var type = get(this, 'type').toString(); 398 | throw new Error("The result of a client-side filter (on " + type + ") is immutable."); 399 | }, 400 | 401 | updateFilter: Ember.observer(function() { 402 | var store = get(this, 'store'); 403 | store.updateModelArrayFilter(this, get(this, 'type'), get(this, 'filterFunction')); 404 | }, 'filterFunction') 405 | }); 406 | 407 | })({}); 408 | 409 | 410 | (function(exports) { 411 | var get = Ember.get, set = Ember.set; 412 | 413 | DS.AdapterPopulatedModelArray = DS.ModelArray.extend({ 414 | query: null, 415 | isLoaded: false, 416 | 417 | replace: function() { 418 | var type = get(this, 'type').toString(); 419 | throw new Error("The result of a server query (on " + type + ") is immutable."); 420 | }, 421 | 422 | load: function(array) { 423 | var store = get(this, 'store'), type = get(this, 'type'); 424 | 425 | var clientIds = store.loadMany(type, array).clientIds; 426 | 427 | this.beginPropertyChanges(); 428 | set(this, 'content', Ember.A(clientIds)); 429 | set(this, 'isLoaded', true); 430 | this.endPropertyChanges(); 431 | } 432 | }); 433 | 434 | 435 | })({}); 436 | 437 | 438 | (function(exports) { 439 | var get = Ember.get, set = Ember.set; 440 | 441 | DS.ManyArray = DS.ModelArray.extend({ 442 | parentRecord: null, 443 | 444 | // Overrides Ember.Array's replace method to implement 445 | replace: function(index, removed, added) { 446 | var parentRecord = get(this, 'parentRecord'); 447 | var pendingParent = parentRecord && !get(parentRecord, 'id'); 448 | 449 | added = added.map(function(item) { 450 | ember_assert("You can only add items of " + (get(this, 'type') && get(this, 'type').toString()) + " to this association.", !get(this, 'type') || (get(this, 'type') === item.constructor)); 451 | 452 | if (pendingParent) { item.send('waitingOn', parentRecord); } 453 | return item.get('clientId'); 454 | }); 455 | 456 | this._super(index, removed, added); 457 | } 458 | }); 459 | 460 | })({}); 461 | 462 | 463 | (function(exports) { 464 | })({}); 465 | 466 | 467 | (function(exports) { 468 | var get = Ember.get, set = Ember.set, getPath = Ember.getPath, fmt = Ember.String.fmt; 469 | 470 | DS.Transaction = Ember.Object.extend({ 471 | init: function() { 472 | set(this, 'buckets', { 473 | clean: Ember.Map.create(), 474 | created: Ember.Map.create(), 475 | updated: Ember.Map.create(), 476 | deleted: Ember.Map.create() 477 | }); 478 | }, 479 | 480 | createRecord: function(type, hash) { 481 | var store = get(this, 'store'); 482 | 483 | return store.createRecord(type, hash, this); 484 | }, 485 | 486 | add: function(record) { 487 | // we could probably make this work if someone has a valid use case. Do you? 488 | ember_assert("Once a record has changed, you cannot move it into a different transaction", !get(record, 'isDirty')); 489 | 490 | var modelTransaction = get(record, 'transaction'), 491 | defaultTransaction = getPath(this, 'store.defaultTransaction'); 492 | 493 | ember_assert("Models cannot belong to more than one transaction at a time.", modelTransaction === defaultTransaction); 494 | 495 | this.adoptRecord(record); 496 | }, 497 | 498 | remove: function(record) { 499 | var defaultTransaction = getPath(this, 'store.defaultTransaction'); 500 | 501 | defaultTransaction.adoptRecord(record); 502 | }, 503 | 504 | /** 505 | @private 506 | 507 | This method moves a record into a different transaction without the normal 508 | checks that ensure that the user is not doing something weird, like moving 509 | a dirty record into a new transaction. 510 | 511 | It is designed for internal use, such as when we are moving a clean record 512 | into a new transaction when the transaction is committed. 513 | 514 | This method must not be called unless the record is clean. 515 | */ 516 | adoptRecord: function(record) { 517 | var oldTransaction = get(record, 'transaction'); 518 | 519 | if (oldTransaction) { 520 | oldTransaction.removeFromBucket('clean', record); 521 | } 522 | 523 | this.addToBucket('clean', record); 524 | set(record, 'transaction', this); 525 | }, 526 | 527 | modelBecameDirty: function(kind, record) { 528 | this.removeFromBucket('clean', record); 529 | this.addToBucket(kind, record); 530 | }, 531 | 532 | /** @private */ 533 | addToBucket: function(kind, record) { 534 | var bucket = get(get(this, 'buckets'), kind), 535 | type = record.constructor; 536 | 537 | var records = bucket.get(type); 538 | 539 | if (!records) { 540 | records = Ember.OrderedSet.create(); 541 | bucket.set(type, records); 542 | } 543 | 544 | records.add(record); 545 | }, 546 | 547 | /** @private */ 548 | removeFromBucket: function(kind, record) { 549 | var bucket = get(get(this, 'buckets'), kind), 550 | type = record.constructor; 551 | 552 | var records = bucket.get(type); 553 | records.remove(record); 554 | }, 555 | 556 | modelBecameClean: function(kind, record) { 557 | this.removeFromBucket(kind, record); 558 | 559 | var defaultTransaction = getPath(this, 'store.defaultTransaction'); 560 | defaultTransaction.adoptRecord(record); 561 | }, 562 | 563 | commit: function() { 564 | var buckets = get(this, 'buckets'); 565 | 566 | var iterate = function(kind, fn, binding) { 567 | var dirty = get(buckets, kind); 568 | 569 | dirty.forEach(function(type, models) { 570 | if (models.isEmpty()) { return; } 571 | 572 | var array = []; 573 | 574 | models.forEach(function(model) { 575 | model.send('willCommit'); 576 | 577 | if (get(model, 'isPending') === false) { 578 | array.push(model); 579 | } 580 | }); 581 | 582 | fn.call(binding, type, array); 583 | }); 584 | }; 585 | 586 | var commitDetails = { 587 | updated: { 588 | eachType: function(fn, binding) { iterate('updated', fn, binding); } 589 | }, 590 | 591 | created: { 592 | eachType: function(fn, binding) { iterate('created', fn, binding); } 593 | }, 594 | 595 | deleted: { 596 | eachType: function(fn, binding) { iterate('deleted', fn, binding); } 597 | } 598 | }; 599 | 600 | var store = get(this, 'store'); 601 | var adapter = get(store, '_adapter'); 602 | 603 | var clean = get(buckets, 'clean'); 604 | var defaultTransaction = get(store, 'defaultTransaction'); 605 | 606 | clean.forEach(function(type, records) { 607 | records.forEach(function(record) { 608 | this.remove(record); 609 | }, this); 610 | }, this); 611 | 612 | if (adapter && adapter.commit) { adapter.commit(store, commitDetails); } 613 | else { throw fmt("Adapter is either null or do not implement `commit` method", this); } 614 | } 615 | }); 616 | 617 | })({}); 618 | 619 | 620 | (function(exports) { 621 | var get = Ember.get, set = Ember.set, getPath = Ember.getPath, fmt = Ember.String.fmt; 622 | 623 | // Implementors Note: 624 | // 625 | // The variables in this file are consistently named according to the following 626 | // scheme: 627 | // 628 | // * +id+ means an identifier managed by an external source, provided inside the 629 | // data hash provided by that source. 630 | // * +clientId+ means a transient numerical identifier generated at runtime by 631 | // the data store. It is important primarily because newly created objects may 632 | // not yet have an externally generated id. 633 | // * +type+ means a subclass of DS.Model. 634 | 635 | /** 636 | The store contains all of the hashes for data models loaded from the server. 637 | It is also responsible for creating instances of DS.Model when you request one 638 | of these data hashes, so that they can be bound to in your Handlebars templates. 639 | 640 | Create a new store like this: 641 | 642 | MyApp.store = DS.Store.create(); 643 | 644 | You can retrieve DS.Model instances from the store in several ways. To retrieve 645 | a model for a specific id, use the `find()` method: 646 | 647 | var model = MyApp.store.find(MyApp.Contact, 123); 648 | 649 | By default, the store will talk to your backend using a standard REST mechanism. 650 | You can customize how the store talks to your backend by specifying a custom adapter: 651 | 652 | MyApp.store = DS.Store.create({ 653 | adapter: 'MyApp.CustomAdapter' 654 | }); 655 | 656 | You can learn more about writing a custom adapter by reading the `DS.Adapter` 657 | documentation. 658 | */ 659 | DS.Store = Ember.Object.extend({ 660 | 661 | /** 662 | Many methods can be invoked without specifying which store should be used. 663 | In those cases, the first store created will be used as the default. If 664 | an application has multiple stores, it should specify which store to use 665 | when performing actions, such as finding records by id. 666 | 667 | The init method registers this store as the default if none is specified. 668 | */ 669 | init: function() { 670 | if (!get(DS, 'defaultStore') || get(this, 'isDefaultStore')) { 671 | set(DS, 'defaultStore', this); 672 | } 673 | 674 | set(this, 'data', []); 675 | set(this, '_typeMap', {}); 676 | set(this, 'recordCache', []); 677 | set(this, 'modelArrays', []); 678 | set(this, 'modelArraysByClientId', {}); 679 | set(this, 'defaultTransaction', this.transaction()); 680 | 681 | return this._super(); 682 | }, 683 | 684 | transaction: function() { 685 | return DS.Transaction.create({ store: this }); 686 | }, 687 | 688 | modelArraysForClientId: function(clientId) { 689 | var modelArrays = get(this, 'modelArraysByClientId'); 690 | var ret = modelArrays[clientId]; 691 | 692 | if (!ret) { 693 | ret = modelArrays[clientId] = Ember.OrderedSet.create(); 694 | } 695 | 696 | return ret; 697 | }, 698 | 699 | /** 700 | The adapter to use to communicate to a backend server or other persistence layer. 701 | 702 | This can be specified as an instance, a class, or a property path that specifies 703 | where the adapter can be located. 704 | 705 | @property {DS.Adapter|String} 706 | */ 707 | adapter: null, 708 | 709 | _adapter: Ember.computed(function() { 710 | var adapter = get(this, 'adapter'); 711 | if (typeof adapter === 'string') { 712 | return getPath(this, adapter, false) || getPath(window, adapter); 713 | } 714 | return adapter; 715 | }).property('adapter').cacheable(), 716 | 717 | clientIdCounter: -1, 718 | 719 | // .................... 720 | // . CREATE NEW MODEL . 721 | // .................... 722 | 723 | createRecord: function(type, properties, transaction) { 724 | properties = properties || {}; 725 | 726 | // Create a new instance of the model `type` and put it 727 | // into the specified `transaction`. If no transaction is 728 | // specified, the default transaction will be used. 729 | // 730 | // NOTE: A `transaction` is specified when the 731 | // `transaction.createRecord` API is used. 732 | var record = type._create({ 733 | store: this 734 | }); 735 | 736 | transaction = transaction || get(this, 'defaultTransaction'); 737 | transaction.adoptRecord(record); 738 | 739 | // Extract the primary key from the `properties` hash, 740 | // based on the `primaryKey` for the model type. 741 | var id = properties[get(record, 'primaryKey')] || null; 742 | 743 | var hash = {}, clientId; 744 | 745 | // Push the hash into the store. If present, associate the 746 | // extracted `id` with the hash. 747 | clientId = this.pushHash(hash, id, type); 748 | 749 | record.send('setData', hash); 750 | 751 | var recordCache = get(this, 'recordCache'); 752 | 753 | // Now that we have a clientId, attach it to the record we 754 | // just created. 755 | set(record, 'clientId', clientId); 756 | 757 | // Store the record we just created in the record cache for 758 | // this clientId. 759 | recordCache[clientId] = record; 760 | 761 | // Set the properties specified on the record. 762 | record.setProperties(properties); 763 | 764 | // Update any model arrays. Most notably, add this record to 765 | // the model arrays returned by `find(type)` and add it to 766 | // any filtered arrays for whom this model passes the filter. 767 | this.updateModelArrays(type, clientId, hash); 768 | 769 | return record; 770 | }, 771 | 772 | // ................ 773 | // . DELETE MODEL . 774 | // ................ 775 | 776 | deleteRecord: function(model) { 777 | model.send('deleteRecord'); 778 | }, 779 | 780 | // ............... 781 | // . FIND MODELS . 782 | // ............... 783 | 784 | /** 785 | Finds a model by its id. If the data for that model has already been 786 | loaded, an instance of DS.Model with that data will be returned 787 | immediately. Otherwise, an empty DS.Model instance will be returned in 788 | the loading state. As soon as the requested data is available, the model 789 | will be moved into the loaded state and all of the information will be 790 | available. 791 | 792 | Note that only one DS.Model instance is ever created per unique id for a 793 | given type. 794 | 795 | Example: 796 | 797 | var record = MyApp.store.find(MyApp.Person, 1234); 798 | 799 | @param {DS.Model} type 800 | @param {String|Number} id 801 | */ 802 | find: function(type, id, query) { 803 | if (id === undefined) { 804 | return this.findAll(type); 805 | } 806 | 807 | if (query !== undefined) { 808 | return this.findMany(type, id, query); 809 | } else if (Ember.typeOf(id) === 'object') { 810 | return this.findQuery(type, id); 811 | } 812 | 813 | if (Ember.isArray(id)) { 814 | return this.findMany(type, id); 815 | } 816 | 817 | var clientId = this.clientIdForId(type, id); 818 | 819 | return this.findByClientId(type, clientId, id); 820 | }, 821 | 822 | findByClientId: function(type, clientId, id) { 823 | var model; 824 | 825 | var recordCache = get(this, 'recordCache'); 826 | var data = this.clientIdToHashMap(type); 827 | 828 | // If there is already a clientId assigned for this 829 | // type/id combination, try to find an existing 830 | // model for that id and return. Otherwise, 831 | // materialize a new model and set its data to the 832 | // value we already have. 833 | if (clientId !== undefined) { 834 | model = recordCache[clientId]; 835 | 836 | if (!model) { 837 | // create a new instance of the model in the 838 | // 'isLoading' state 839 | model = this.materializeRecord(type, clientId); 840 | 841 | // immediately set its data 842 | model.send('setData', data[clientId] || null); 843 | } 844 | } else { 845 | clientId = this.pushHash(null, id, type); 846 | 847 | // create a new instance of the model in the 848 | // 'isLoading' state 849 | model = this.materializeRecord(type, clientId); 850 | 851 | // let the adapter set the data, possibly async 852 | var adapter = get(this, '_adapter'); 853 | if (adapter && adapter.find) { adapter.find(this, type, id); } 854 | else { throw fmt("Adapter is either null or does not implement `find` method", this); } 855 | } 856 | 857 | return model; 858 | }, 859 | 860 | /** @private 861 | */ 862 | findMany: function(type, ids, query) { 863 | var idToClientIdMap = this.idToClientIdMap(type); 864 | var data = this.clientIdToHashMap(type), needed; 865 | 866 | var clientIds = Ember.A([]); 867 | 868 | if (ids) { 869 | needed = []; 870 | 871 | ids.forEach(function(id) { 872 | var clientId = idToClientIdMap[id]; 873 | if (clientId === undefined || data[clientId] === undefined) { 874 | clientId = this.pushHash(null, id, type); 875 | needed.push(id); 876 | } 877 | 878 | clientIds.push(clientId); 879 | }, this); 880 | } else { 881 | needed = null; 882 | } 883 | 884 | if ((needed && get(needed, 'length') > 0) || query) { 885 | var adapter = get(this, '_adapter'); 886 | if (adapter && adapter.findMany) { adapter.findMany(this, type, needed, query); } 887 | else { throw fmt("Adapter is either null or does not implement `findMany` method", this); } 888 | } 889 | 890 | return this.createManyArray(type, clientIds); 891 | }, 892 | 893 | findQuery: function(type, query) { 894 | var array = DS.AdapterPopulatedModelArray.create({ type: type, content: Ember.A([]), store: this }); 895 | var adapter = get(this, '_adapter'); 896 | if (adapter && adapter.findQuery) { adapter.findQuery(this, type, query, array); } 897 | else { throw fmt("Adapter is either null or does not implement `findQuery` method", this); } 898 | return array; 899 | }, 900 | 901 | findAll: function(type) { 902 | 903 | var typeMap = this.typeMapFor(type), 904 | findAllCache = typeMap.findAllCache; 905 | 906 | if (findAllCache) { return findAllCache; } 907 | 908 | var array = DS.ModelArray.create({ type: type, content: Ember.A([]), store: this }); 909 | this.registerModelArray(array, type); 910 | 911 | var adapter = get(this, '_adapter'); 912 | if (adapter && adapter.findAll) { adapter.findAll(this, type); } 913 | 914 | typeMap.findAllCache = array; 915 | return array; 916 | }, 917 | 918 | filter: function(type, filter) { 919 | var array = DS.FilteredModelArray.create({ type: type, content: Ember.A([]), store: this, filterFunction: filter }); 920 | 921 | this.registerModelArray(array, type, filter); 922 | 923 | return array; 924 | }, 925 | 926 | // ............ 927 | // . UPDATING . 928 | // ............ 929 | 930 | hashWasUpdated: function(type, clientId) { 931 | var clientIdToHashMap = this.clientIdToHashMap(type); 932 | var hash = clientIdToHashMap[clientId]; 933 | 934 | this.updateModelArrays(type, clientId, hash); 935 | }, 936 | 937 | // .............. 938 | // . PERSISTING . 939 | // .............. 940 | 941 | commit: function() { 942 | var defaultTransaction = get(this, 'defaultTransaction'); 943 | set(this, 'defaultTransaction', this.transaction()); 944 | 945 | defaultTransaction.commit(); 946 | }, 947 | 948 | didUpdateRecords: function(array, hashes) { 949 | if (arguments.length === 2) { 950 | array.forEach(function(model, idx) { 951 | this.didUpdateRecord(model, hashes[idx]); 952 | }, this); 953 | } else { 954 | array.forEach(function(model) { 955 | this.didUpdateRecord(model); 956 | }, this); 957 | } 958 | }, 959 | 960 | didUpdateRecord: function(model, hash) { 961 | if (arguments.length === 2) { 962 | var clientId = get(model, 'clientId'); 963 | var data = this.clientIdToHashMap(model.constructor); 964 | 965 | data[clientId] = hash; 966 | model.send('setData', hash); 967 | } 968 | 969 | model.send('didCommit'); 970 | }, 971 | 972 | didDeleteRecords: function(array) { 973 | array.forEach(function(model) { 974 | model.send('didCommit'); 975 | }); 976 | }, 977 | 978 | didDeleteRecord: function(model) { 979 | model.send('didCommit'); 980 | }, 981 | 982 | didCreateRecords: function(type, array, hashes) { 983 | var id, clientId, primaryKey = getPath(type, 'proto.primaryKey'); 984 | 985 | var idToClientIdMap = this.idToClientIdMap(type); 986 | var data = this.clientIdToHashMap(type); 987 | var idList = this.idList(type); 988 | 989 | for (var i=0, l=get(array, 'length'); i