├── .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 |
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 | '' +
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 += 'Expected: ' + expected + ' ';
658 | if (actual != expected) {
659 | output += 'Result: ' + actual + ' ';
660 | output += 'Diff: ' + QUnit.diff(expected, actual) +' ';
661 | }
662 | var source = sourceFromStacktrace();
663 | if (source) {
664 | details.source = source;
665 | output += 'Source: ' + escapeInnerText(source) + ' ';
666 | }
667 | output += "
";
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 += ' ' + val + ' ';
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