diff --git a/.jshintrc b/.jshintrc
index f7f699b5ebd..a8d17196ecc 100644
--- a/.jshintrc
+++ b/.jshintrc
@@ -13,6 +13,7 @@
"module",
"moduleFor",
"moduleForComponent",
+ "Pretender",
"sandbox",
"integration",
"controllerFor",
diff --git a/app/assets/javascripts/discourse/controllers/login.js.es6 b/app/assets/javascripts/discourse/controllers/login.js.es6
index c96a836d188..2fbf865c5e2 100644
--- a/app/assets/javascripts/discourse/controllers/login.js.es6
+++ b/app/assets/javascripts/discourse/controllers/login.js.es6
@@ -11,10 +11,12 @@ export default Discourse.Controller.extend(Discourse.ModalFunctionality, {
needs: ['modal', 'createAccount'],
authenticate: null,
loggingIn: false,
+ loggedIn: false,
resetForm: function() {
this.set('authenticate', null);
this.set('loggingIn', false);
+ this.set('loggedIn', false);
},
site: function() {
@@ -32,9 +34,7 @@ export default Discourse.Controller.extend(Discourse.ModalFunctionality, {
return this.get('loggingIn') ? I18n.t('login.logging_in') : I18n.t('login.title');
}.property('loggingIn'),
- loginDisabled: function() {
- return this.get('loggingIn');
- }.property('loggingIn'),
+ loginDisabled: Em.computed.or('loggingIn', 'loggedIn'),
showSignupLink: function() {
return !Discourse.SiteSettings.invite_only &&
@@ -68,6 +68,7 @@ export default Discourse.Controller.extend(Discourse.ModalFunctionality, {
}
self.flash(result.error, 'error');
} else {
+ self.set('loggedIn', true);
// Trigger the browser's password manager using the hidden static login form:
var $hidden_login_form = $('#hidden-login-form');
$hidden_login_form.find('input[name=username]').val(self.get('loginName'));
@@ -76,7 +77,7 @@ export default Discourse.Controller.extend(Discourse.ModalFunctionality, {
$hidden_login_form.submit();
}
- }, function() {
+ }, function(e) {
// Failed to login
if (self.blank('loginName') || self.blank('loginPassword')) {
self.flash(I18n.t('login.blank_username_or_password'), 'error');
diff --git a/app/assets/javascripts/discourse/templates/modal/create_account.js.handlebars b/app/assets/javascripts/discourse/templates/modal/create_account.js.handlebars
index 43c07ef7631..7e675b3a339 100644
--- a/app/assets/javascripts/discourse/templates/modal/create_account.js.handlebars
+++ b/app/assets/javascripts/discourse/templates/modal/create_account.js.handlebars
@@ -31,7 +31,7 @@
|
{{input value=accountUsername id="new-account-username" maxlength=Discourse.SiteSettings.max_username_length}}
- {{input-tip validation=usernameValidation}}
+ {{input-tip validation=usernameValidation id="username-validation"}}
|
diff --git a/test/javascripts/fixtures/session_fixtures.js b/test/javascripts/fixtures/session_fixtures.js
deleted file mode 100644
index e7db28f8656..00000000000
--- a/test/javascripts/fixtures/session_fixtures.js
+++ /dev/null
@@ -1,2 +0,0 @@
-/*jshint maxlen:10000000 */
-Discourse.URL_FIXTURES["/session"] = [ { error: "Incorrect username, email or password" } ];
diff --git a/test/javascripts/fixtures/site_settings_fixtures.js b/test/javascripts/fixtures/site_settings_fixtures.js
index 50a4ba61362..d1ce7347d3e 100644
--- a/test/javascripts/fixtures/site_settings_fixtures.js
+++ b/test/javascripts/fixtures/site_settings_fixtures.js
@@ -1,3 +1,3 @@
/*jshint maxlen:10000000 */
-Discourse.SiteSettingsOriginal = {"title":"Discourse Meta","logo_url":"/assets/logo.png","logo_small_url":"/assets/logo-single.png","traditional_markdown_linebreaks":false,"top_menu":"latest|new|unread|read|starred|categories","post_menu":"like|edit|flag|delete|share|bookmark|admin|reply","share_links":"twitter|facebook|google+|email","track_external_right_clicks":false,"must_approve_users":false,"ga_tracking_code":"UA-33736483-2","ga_domain_name":"","enable_long_polling":true,"polling_interval":3000,"anon_polling_interval":30000,"min_post_length":20,"max_post_length":16000,"min_topic_title_length":15,"max_topic_title_length":255,"min_private_message_title_length":2,"allow_uncategorized_topics":true,"min_search_term_length":3,"flush_timings_secs":5,"suppress_reply_directly_below":true,"email_domains_blacklist":"mailinator.com","email_domains_whitelist":null,"version_checks":true,"min_title_similar_length":10,"min_body_similar_length":15,"category_colors":"BF1E2E|F1592A|F7941D|9EB83B|3AB54A|12A89D|25AAE2|0E76BD|652D90|92278F|ED207B|8C6238|231F20|808281|B3B5B4|283890","max_upload_size_kb":1024,"category_featured_topics":6,"favicon_url":"/assets/favicon.ico","dynamic_favicon":false,"uncategorized_name":"uncategorized","uncategorized_color":"AB9364","uncategorized_text_color":"FFFFFF","invite_only":false,"login_required":false,"min_password_length":8,"enable_local_logins":true,"enable_google_logins":false,"enable_google_oauth2_logins":false,"enable_yahoo_logins":false,"enable_twitter_logins":false,"enable_facebook_logins":false,"enable_cas_logins":false,"enable_github_logins":false,"educate_until_posts":2,"topic_views_heat_low":1000,"topic_views_heat_medium":2000,"topic_views_heat_high":5000,"min_private_message_post_length":5,"faq_url":"","tos_url":"","privacy_policy_url":"","authorized_extensions":".jpg|.jpeg|.png|.gif|.txt","relative_date_duration":14,"delete_removed_posts_after":24,"delete_user_max_post_age":7, "default_code_lang": "lang-auto", "suppress_uncategorized_badge": true, "min_username_length": 3, "max_username_length": 20};
+Discourse.SiteSettingsOriginal = {"title":"Discourse Meta","logo_url":"/assets/logo.png","logo_small_url":"/assets/logo-single.png","mobile_logo_url":"","favicon_url":"//meta.discourse.org/uploads/default/2499/79d53726406d87af.ico","allow_user_locale":false,"suggested_topics":7,"track_external_right_clicks":false,"ga_universal_tracking_code":"","ga_universal_domain_name":"auto","ga_tracking_code":"UA-33736483-2","ga_domain_name":"","top_menu":"latest|new|unread|starred|categories|top","post_menu":"like|share|flag|edit|bookmark|delete|admin|reply","post_menu_hidden_items":"edit|delete|admin","share_links":"twitter|facebook|google+|email","category_colors":"BF1E2E|F1592A|F7941D|9EB83B|3AB54A|12A89D|25AAE2|0E76BD|652D90|92278F|ED207B|8C6238|231F20|808281|B3B5B4|283890","enable_mobile_theme":true,"relative_date_duration":14,"category_featured_topics":4,"fixed_category_positions":false,"show_subcategory_list":false,"posts_per_page":20,"enable_badges":true,"invite_only":false,"login_required":false,"must_approve_users":false,"enable_local_logins":true,"allow_new_registrations":true,"enable_google_logins":true,"enable_google_oauth2_logins":false,"enable_yahoo_logins":true,"enable_twitter_logins":true,"enable_facebook_logins":true,"enable_github_logins":true,"enable_sso":false,"min_username_length":3,"max_username_length":20,"min_password_length":8,"enable_names":true,"invites_shown":30,"delete_user_max_post_age":60,"delete_all_posts_max":15,"min_post_length":20,"min_private_message_post_length":10,"max_post_length":32000,"min_topic_title_length":15,"max_topic_title_length":255,"min_private_message_title_length":2,"allow_uncategorized_topics":true,"min_title_similar_length":10,"min_body_similar_length":15,"edit_history_visible_to_public":true,"delete_removed_posts_after":24,"traditional_markdown_linebreaks":false,"suppress_reply_directly_below":true,"suppress_reply_directly_above":true,"newuser_max_images":0,"newuser_max_attachments":0,"display_name_on_posts":true,"short_progress_text_threshold":10000,"default_code_lang":"lang-auto","autohighlight_all_code":false,"email_in":false,"max_image_size_kb":3072,"max_attachment_size_kb":1024,"authorized_extensions":".jpg|.jpeg|.png|.gif|.svg|.txt|.ico|.yml","max_image_width":690,"max_image_height":500,"allow_profile_backgrounds":true,"allow_uploaded_avatars":true,"allow_animated_avatars":false,"basic_requires_read_posts":30,"enable_long_polling":true,"polling_interval":3000,"anon_polling_interval":30000,"flush_timings_secs":5,"tos_url":"","privacy_policy_url":"","tos_accept_required":false,"faq_url":"","allow_restore":false,"maximum_backups":7,"version_checks":true,"suppress_uncategorized_badge":true,"min_search_term_length":3,"topic_views_heat_low":1000,"topic_views_heat_medium":2000,"topic_views_heat_high":5000,"global_notice":"","show_create_topics_notice":true,"available_locales":"cs|da|de|en|es|fr|he|id|it|ja|ko|nb_NO|nl|pl_PL|pt|pt_BR|ru|sv|uk|zh_CN|zh_TW"};
Discourse.SiteSettings = jQuery.extend(true, {}, Discourse.SiteSettingsOriginal);
diff --git a/test/javascripts/helpers/create-pretender.js.es6 b/test/javascripts/helpers/create-pretender.js.es6
new file mode 100644
index 00000000000..af3d1a0da2e
--- /dev/null
+++ b/test/javascripts/helpers/create-pretender.js.es6
@@ -0,0 +1,52 @@
+/* global console */
+
+function parsePostData(query) {
+ var result = {};
+ query.split("&").forEach(function(part) {
+ var item = part.split("=");
+ result[item[0]] = decodeURIComponent(item[1]);
+ });
+ return result;
+}
+
+function json(code, obj) {
+ if (typeof code === "object") {
+ obj = code;
+ code = 200;
+ }
+ return [code, {"Content-Type": "application/json"}, JSON.stringify(obj)];
+}
+
+export default function() {
+ var server = new Pretender(function() {
+ this.post('/session', function(request) {
+ var data = parsePostData(request.requestBody);
+
+ if (data.password === 'correct') {
+ return json({username: 'eviltrout'});
+ }
+ return json(400, {error: 'invalid login'});
+ });
+
+ this.get('/users/hp.json', function() {
+ return json({"value":"32faff1b1ef1ac3","challenge":"61a3de0ccf086fb9604b76e884d75801"});
+ });
+
+ this.get('/users/check_username', function(request) {
+ if (request.queryParams.username === 'taken') {
+ return json({available: false, suggestion: 'nottaken'});
+ }
+ return json({available: true});
+ });
+
+ this.post('/users', function(request) {
+ return json({success: true});
+ });
+ });
+
+ server.unhandledRequest = function(verb, path) {
+ console.error('Unhandled request in test environment: ' + path + ' (' + verb + ')');
+ };
+
+ return server;
+}
diff --git a/test/javascripts/integration/header-anonymous-test.js.es6 b/test/javascripts/integration/header-anonymous-test.js.es6
index a8009c6e576..3a9d37912d1 100644
--- a/test/javascripts/integration/header-anonymous-test.js.es6
+++ b/test/javascripts/integration/header-anonymous-test.js.es6
@@ -1,8 +1,6 @@
integration("Header (Anonymous)");
test("header", function() {
- expect(14);
-
visit("/");
andThen(function() {
ok(exists("header"), "is rendered");
diff --git a/test/javascripts/integration/header-test-staff.js.es6 b/test/javascripts/integration/header-test-staff.js.es6
index c9caaef6225..d1adcf6c9ab 100644
--- a/test/javascripts/integration/header-test-staff.js.es6
+++ b/test/javascripts/integration/header-test-staff.js.es6
@@ -5,8 +5,6 @@ integration("Header (Staff)", {
});
test("header", function() {
- expect(6);
-
visit("/");
// Notifications
@@ -30,5 +28,4 @@ test("header", function() {
ok(exists("#user-dropdown:visible"), "is lazily rendered after user opens it");
ok(exists("#user-dropdown .user-dropdown-links"), "has showing / hiding user-dropdown links correctly bound");
});
-
});
diff --git a/test/javascripts/integration/sign-in-test.js.es6 b/test/javascripts/integration/sign-in-test.js.es6
index 4e86b2ffcef..d706cdfbf57 100644
--- a/test/javascripts/integration/sign-in-test.js.es6
+++ b/test/javascripts/integration/sign-in-test.js.es6
@@ -1,17 +1,61 @@
integration("Signing In");
-test("sign in with incorrect credentials", function() {
+test("sign in", function() {
visit("/");
click("header .login-button");
andThen(function() {
ok(exists('.login-modal'), "it shows the login modal");
});
- fillIn('#login-account-name', 'eviltrout');
- fillIn('#login-account-password', 'where da plankton at?');
- // The fixture is set to invalid login
+ // Test invalid password first
+ fillIn('#login-account-name', 'eviltrout');
+ fillIn('#login-account-password', 'incorrect');
click('.modal-footer .btn-primary');
andThen(function() {
- // ok(exists('#modal-alert:visible', 'it displays the login error'));
+ ok(exists('#modal-alert:visible', 'it displays the login error'));
+ not(exists('.modal-footer .btn-primary:disabled'), "enables the login button");
+ });
+
+ // Use the correct password
+ fillIn('#login-account-password', 'correct');
+ click('.modal-footer .btn-primary');
+ andThen(function() {
+ ok(exists('.modal-footer .btn-primary:disabled'), "disables the login button");
});
});
+
+test("create account", function() {
+ visit("/");
+ click("header .login-button");
+ click('#new-account-link');
+
+ andThen(function() {
+ ok(exists('.create-account'), "it shows the create account modal");
+ ok(exists('.modal-footer .btn-primary:disabled'), 'create account is disabled at first');
+ });
+
+ fillIn('#new-account-name', 'Dr. Good Tuna');
+ fillIn('#new-account-password', 'cool password bro');
+
+ // Check username
+ fillIn('#new-account-email', 'good.tuna@test.com');
+ fillIn('#new-account-username', 'taken');
+ andThen(function() {
+ ok(exists('#username-validation.bad'), 'the username validation is bad');
+ ok(exists('.modal-footer .btn-primary:disabled'), 'create account is still disabled');
+ });
+
+ fillIn('#new-account-username', 'goodtuna');
+ andThen(function() {
+ ok(exists('#username-validation.good'), 'the username validation is good');
+ not(exists('.modal-footer .btn-primary:disabled'), 'create account is enabled');
+ });
+
+ click('.modal-footer .btn-primary');
+ andThen(function() {
+ not(exists('.modal-body'), 'it hides the body when finished');
+ });
+
+});
+
+
diff --git a/test/javascripts/test_helper.js b/test/javascripts/test_helper.js
index 830d4417a61..542ac57178d 100644
--- a/test/javascripts/test_helper.js
+++ b/test/javascripts/test_helper.js
@@ -1,20 +1,23 @@
/*jshint maxlen:250 */
-/*global document, sinon, console, QUnit, Logster */
+/*global document, sinon, QUnit, Logster */
//= require env
-//= require ../../app/assets/javascripts/preload_store.js
+//= require ../../app/assets/javascripts/preload_store
// probe framework first
-//= require ../../app/assets/javascripts/discourse/lib/probes.js
+//= require ../../app/assets/javascripts/discourse/lib/probes
// Externals we need to load first
-//= require development/jquery-2.1.1.js
-//= require jquery.ui.widget.js
-//= require handlebars.js
-//= require development/ember.js
-//= require message-bus.js
-//= require ember-qunit.js
+//= require development/jquery-2.1.1
+//= require jquery.ui.widget
+//= require handlebars
+//= require development/ember
+//= require message-bus
+//= require ember-qunit
+//= require fake_xml_http_request
+//= require route-recognizer
+//= require pretender
//= require ../../app/assets/javascripts/locales/i18n
//= require ../../app/assets/javascripts/discourse/helpers/i18n_helpers
@@ -60,16 +63,6 @@ sinon.config = {
window.assetPath = function() { return null; };
-var oldAjax = $.ajax;
-$.ajax = function() {
- try {
- this.undef();
- } catch(e) {
- console.error("Discourse.Ajax called in test environment (" + arguments[0] + ")\n caller: " + e.stack.split("\n").slice(2).join("\n"));
- }
- return oldAjax.apply(this, arguments);
-};
-
// Stop the message bus so we don't get ajax calls
Discourse.MessageBus.stop();
@@ -92,8 +85,13 @@ if (window.Logster) {
window.Logster = { enabled: false };
}
-var origDebounce = Ember.run.debounce;
+var origDebounce = Ember.run.debounce,
+ createPretendServer = require('helpers/create-pretender', null, null, false).default,
+ server;
+
QUnit.testStart(function(ctx) {
+ server = createPretendServer();
+
// Allow our tests to change site settings and have them reset before the next test
Discourse.SiteSettings = jQuery.extend(true, {}, Discourse.SiteSettingsOriginal);
Discourse.BaseUri = "/";
@@ -118,6 +116,8 @@ QUnit.testDone(function() {
// Destroy any modals
$('.modal-backdrop').remove();
+
+ server.shutdown();
});
// Load ES6 tests
diff --git a/vendor/assets/javascripts/fake_xml_http_request.js b/vendor/assets/javascripts/fake_xml_http_request.js
new file mode 100644
index 00000000000..0284c65fca9
--- /dev/null
+++ b/vendor/assets/javascripts/fake_xml_http_request.js
@@ -0,0 +1,480 @@
+(function(undefined){
+/**
+ * Minimal Event interface implementation
+ *
+ * Original implementation by Sven Fuchs: https://gist.github.com/995028
+ * Modifications and tests by Christian Johansen.
+ *
+ * @author Sven Fuchs (svenfuchs@artweb-design.de)
+ * @author Christian Johansen (christian@cjohansen.no)
+ * @license BSD
+ *
+ * Copyright (c) 2011 Sven Fuchs, Christian Johansen
+ */
+
+var _Event = function Event(type, bubbles, cancelable, target) {
+ this.type = type;
+ this.bubbles = bubbles;
+ this.cancelable = cancelable;
+ this.target = target;
+};
+
+_Event.prototype = {
+ stopPropagation: function () {},
+ preventDefault: function () {
+ this.defaultPrevented = true;
+ }
+};
+
+/*
+ Used to set the statusText property of an xhr object
+*/
+var httpStatusCodes = {
+ 100: "Continue",
+ 101: "Switching Protocols",
+ 200: "OK",
+ 201: "Created",
+ 202: "Accepted",
+ 203: "Non-Authoritative Information",
+ 204: "No Content",
+ 205: "Reset Content",
+ 206: "Partial Content",
+ 300: "Multiple Choice",
+ 301: "Moved Permanently",
+ 302: "Found",
+ 303: "See Other",
+ 304: "Not Modified",
+ 305: "Use Proxy",
+ 307: "Temporary Redirect",
+ 400: "Bad Request",
+ 401: "Unauthorized",
+ 402: "Payment Required",
+ 403: "Forbidden",
+ 404: "Not Found",
+ 405: "Method Not Allowed",
+ 406: "Not Acceptable",
+ 407: "Proxy Authentication Required",
+ 408: "Request Timeout",
+ 409: "Conflict",
+ 410: "Gone",
+ 411: "Length Required",
+ 412: "Precondition Failed",
+ 413: "Request Entity Too Large",
+ 414: "Request-URI Too Long",
+ 415: "Unsupported Media Type",
+ 416: "Requested Range Not Satisfiable",
+ 417: "Expectation Failed",
+ 422: "Unprocessable Entity",
+ 500: "Internal Server Error",
+ 501: "Not Implemented",
+ 502: "Bad Gateway",
+ 503: "Service Unavailable",
+ 504: "Gateway Timeout",
+ 505: "HTTP Version Not Supported"
+};
+
+
+/*
+ Cross-browser XML parsing. Used to turn
+ XML responses into Document objects
+ Borrowed from JSpec
+*/
+function parseXML(text) {
+ var xmlDoc;
+
+ if (typeof DOMParser != "undefined") {
+ var parser = new DOMParser();
+ xmlDoc = parser.parseFromString(text, "text/xml");
+ } else {
+ xmlDoc = new ActiveXObject("Microsoft.XMLDOM");
+ xmlDoc.async = "false";
+ xmlDoc.loadXML(text);
+ }
+
+ return xmlDoc;
+}
+
+/*
+ Without mocking, the native XMLHttpRequest object will throw
+ an error when attempting to set these headers. We match this behavior.
+*/
+var unsafeHeaders = {
+ "Accept-Charset": true,
+ "Accept-Encoding": true,
+ "Connection": true,
+ "Content-Length": true,
+ "Cookie": true,
+ "Cookie2": true,
+ "Content-Transfer-Encoding": true,
+ "Date": true,
+ "Expect": true,
+ "Host": true,
+ "Keep-Alive": true,
+ "Referer": true,
+ "TE": true,
+ "Trailer": true,
+ "Transfer-Encoding": true,
+ "Upgrade": true,
+ "User-Agent": true,
+ "Via": true
+};
+
+/*
+ Adds an "event" onto the fake xhr object
+ that just calls the same-named method. This is
+ in case a library adds callbacks for these events.
+*/
+function _addEventListener(eventName, xhr){
+ xhr.addEventListener(eventName, function (event) {
+ var listener = xhr["on" + eventName];
+
+ if (listener && typeof listener == "function") {
+ listener(event);
+ }
+ });
+}
+
+/*
+ Constructor for a fake window.XMLHttpRequest
+*/
+function FakeXMLHttpRequest() {
+ this.readyState = FakeXMLHttpRequest.UNSENT;
+ this.requestHeaders = {};
+ this.requestBody = null;
+ this.status = 0;
+ this.statusText = "";
+
+ this._eventListeners = {};
+ var events = ["loadstart", "load", "abort", "loadend"];
+ for (var i = events.length - 1; i >= 0; i--) {
+ _addEventListener(events[i], this);
+ }
+}
+
+
+// These status codes are available on the native XMLHttpRequest
+// object, so we match that here in case a library is relying on them.
+FakeXMLHttpRequest.UNSENT = 0;
+FakeXMLHttpRequest.OPENED = 1;
+FakeXMLHttpRequest.HEADERS_RECEIVED = 2;
+FakeXMLHttpRequest.LOADING = 3;
+FakeXMLHttpRequest.DONE = 4;
+
+FakeXMLHttpRequest.prototype = {
+ UNSENT: 0,
+ OPENED: 1,
+ HEADERS_RECEIVED: 2,
+ LOADING: 3,
+ DONE: 4,
+ async: true,
+
+ /*
+ Duplicates the behavior of native XMLHttpRequest's open function
+ */
+ open: function open(method, url, async, username, password) {
+ this.method = method;
+ this.url = url;
+ this.async = typeof async == "boolean" ? async : true;
+ this.username = username;
+ this.password = password;
+ this.responseText = null;
+ this.responseXML = null;
+ this.requestHeaders = {};
+ this.sendFlag = false;
+ this._readyStateChange(FakeXMLHttpRequest.OPENED);
+ },
+
+ /*
+ Duplicates the behavior of native XMLHttpRequest's addEventListener function
+ */
+ addEventListener: function addEventListener(event, listener) {
+ this._eventListeners[event] = this._eventListeners[event] || [];
+ this._eventListeners[event].push(listener);
+ },
+
+ /*
+ Duplicates the behavior of native XMLHttpRequest's removeEventListener function
+ */
+ removeEventListener: function removeEventListener(event, listener) {
+ var listeners = this._eventListeners[event] || [];
+
+ for (var i = 0, l = listeners.length; i < l; ++i) {
+ if (listeners[i] == listener) {
+ return listeners.splice(i, 1);
+ }
+ }
+ },
+
+ /*
+ Duplicates the behavior of native XMLHttpRequest's dispatchEvent function
+ */
+ dispatchEvent: function dispatchEvent(event) {
+ var type = event.type;
+ var listeners = this._eventListeners[type] || [];
+
+ for (var i = 0; i < listeners.length; i++) {
+ if (typeof listeners[i] == "function") {
+ listeners[i].call(this, event);
+ } else {
+ listeners[i].handleEvent(event);
+ }
+ }
+
+ return !!event.defaultPrevented;
+ },
+
+ /*
+ Duplicates the behavior of native XMLHttpRequest's setRequestHeader function
+ */
+ setRequestHeader: function setRequestHeader(header, value) {
+ verifyState(this);
+
+ if (unsafeHeaders[header] || /^(Sec-|Proxy-)/.test(header)) {
+ throw new Error("Refused to set unsafe header \"" + header + "\"");
+ }
+
+ if (this.requestHeaders[header]) {
+ this.requestHeaders[header] += "," + value;
+ } else {
+ this.requestHeaders[header] = value;
+ }
+ },
+
+ /*
+ Duplicates the behavior of native XMLHttpRequest's send function
+ */
+ send: function send(data) {
+ verifyState(this);
+
+ if (!/^(get|head)$/i.test(this.method)) {
+ if (this.requestHeaders["Content-Type"]) {
+ var value = this.requestHeaders["Content-Type"].split(";");
+ this.requestHeaders["Content-Type"] = value[0] + ";charset=utf-8";
+ } else {
+ this.requestHeaders["Content-Type"] = "text/plain;charset=utf-8";
+ }
+
+ this.requestBody = data;
+ }
+
+ this.errorFlag = false;
+ this.sendFlag = this.async;
+ this._readyStateChange(FakeXMLHttpRequest.OPENED);
+
+ if (typeof this.onSend == "function") {
+ this.onSend(this);
+ }
+
+ this.dispatchEvent(new _Event("loadstart", false, false, this));
+ },
+
+ /*
+ Duplicates the behavior of native XMLHttpRequest's abort function
+ */
+ abort: function abort() {
+ this.aborted = true;
+ this.responseText = null;
+ this.errorFlag = true;
+ this.requestHeaders = {};
+
+ if (this.readyState > FakeXMLHttpRequest.UNSENT && this.sendFlag) {
+ this._readyStateChange(FakeXMLHttpRequest.DONE);
+ this.sendFlag = false;
+ }
+
+ this.readyState = FakeXMLHttpRequest.UNSENT;
+
+ this.dispatchEvent(new _Event("abort", false, false, this));
+ if (typeof this.onerror === "function") {
+ this.onerror();
+ }
+ },
+
+ /*
+ Duplicates the behavior of native XMLHttpRequest's getResponseHeader function
+ */
+ getResponseHeader: function getResponseHeader(header) {
+ if (this.readyState < FakeXMLHttpRequest.HEADERS_RECEIVED) {
+ return null;
+ }
+
+ if (/^Set-Cookie2?$/i.test(header)) {
+ return null;
+ }
+
+ header = header.toLowerCase();
+
+ for (var h in this.responseHeaders) {
+ if (h.toLowerCase() == header) {
+ return this.responseHeaders[h];
+ }
+ }
+
+ return null;
+ },
+
+ /*
+ Duplicates the behavior of native XMLHttpRequest's getAllResponseHeaders function
+ */
+ getAllResponseHeaders: function getAllResponseHeaders() {
+ if (this.readyState < FakeXMLHttpRequest.HEADERS_RECEIVED) {
+ return "";
+ }
+
+ var headers = "";
+
+ for (var header in this.responseHeaders) {
+ if (this.responseHeaders.hasOwnProperty(header) && !/^Set-Cookie2?$/i.test(header)) {
+ headers += header + ": " + this.responseHeaders[header] + "\r\n";
+ }
+ }
+
+ return headers;
+ },
+
+ /*
+ Places a FakeXMLHttpRequest object into the passed
+ state.
+ */
+ _readyStateChange: function _readyStateChange(state) {
+ this.readyState = state;
+
+ if (typeof this.onreadystatechange == "function") {
+ this.onreadystatechange();
+ }
+
+ this.dispatchEvent(new _Event("readystatechange"));
+
+ if (this.readyState == FakeXMLHttpRequest.DONE) {
+ this.dispatchEvent(new _Event("load", false, false, this));
+ this.dispatchEvent(new _Event("loadend", false, false, this));
+ }
+ },
+
+
+ /*
+ Sets the FakeXMLHttpRequest object's response headers and
+ places the object into readyState 2
+ */
+ _setResponseHeaders: function _setResponseHeaders(headers) {
+ this.responseHeaders = {};
+
+ for (var header in headers) {
+ if (headers.hasOwnProperty(header)) {
+ this.responseHeaders[header] = headers[header];
+ }
+ }
+
+ if (this.async) {
+ this._readyStateChange(FakeXMLHttpRequest.HEADERS_RECEIVED);
+ } else {
+ this.readyState = FakeXMLHttpRequest.HEADERS_RECEIVED;
+ }
+ },
+
+
+
+ /*
+ Sets the FakeXMLHttpRequest object's response body and
+ if body text is XML, sets responseXML to parsed document
+ object
+ */
+ _setResponseBody: function _setResponseBody(body) {
+ verifyRequestSent(this);
+ verifyHeadersReceived(this);
+ verifyResponseBodyType(body);
+
+ var chunkSize = this.chunkSize || 10;
+ var index = 0;
+ this.responseText = "";
+
+ do {
+ if (this.async) {
+ this._readyStateChange(FakeXMLHttpRequest.LOADING);
+ }
+
+ this.responseText += body.substring(index, index + chunkSize);
+ index += chunkSize;
+ } while (index < body.length);
+
+ var type = this.getResponseHeader("Content-Type");
+
+ if (this.responseText && (!type || /(text\/xml)|(application\/xml)|(\+xml)/.test(type))) {
+ try {
+ this.responseXML = parseXML(this.responseText);
+ } catch (e) {
+ // Unable to parse XML - no biggie
+ }
+ }
+
+ if (this.async) {
+ this._readyStateChange(FakeXMLHttpRequest.DONE);
+ } else {
+ this.readyState = FakeXMLHttpRequest.DONE;
+ }
+ },
+
+ /*
+ Forces a response on to the FakeXMLHttpRequest object.
+
+ This is the public API for faking responses. This function
+ takes a number status, headers object, and string body:
+
+ ```
+ xhr.respond(404, {Content-Type: 'text/plain'}, "Sorry. This object was not found.")
+
+ ```
+ */
+ respond: function respond(status, headers, body) {
+ this._setResponseHeaders(headers || {});
+ this.status = typeof status == "number" ? status : 200;
+ this.statusText = httpStatusCodes[this.status];
+ this._setResponseBody(body || "");
+ if (typeof this.onload === "function"){
+ this.onload();
+ }
+ }
+};
+
+function verifyState(xhr) {
+ if (xhr.readyState !== FakeXMLHttpRequest.OPENED) {
+ throw new Error("INVALID_STATE_ERR");
+ }
+
+ if (xhr.sendFlag) {
+ throw new Error("INVALID_STATE_ERR");
+ }
+}
+
+
+function verifyRequestSent(xhr) {
+ if (xhr.readyState == FakeXMLHttpRequest.DONE) {
+ throw new Error("Request done");
+ }
+}
+
+function verifyHeadersReceived(xhr) {
+ if (xhr.async && xhr.readyState != FakeXMLHttpRequest.HEADERS_RECEIVED) {
+ throw new Error("No headers received");
+ }
+}
+
+function verifyResponseBodyType(body) {
+ if (typeof body != "string") {
+ var error = new Error("Attempted to respond to fake XMLHttpRequest with " +
+ body + ", which is not a string.");
+ error.name = "InvalidBodyException";
+ throw error;
+ }
+}
+
+if (typeof module !== 'undefined' && module.exports) {
+ module.exports = FakeXMLHttpRequest;
+} else if (typeof define === 'function' && define.amd) {
+ define(function() { return FakeXMLHttpRequest; });
+} else if (typeof window !== 'undefined') {
+ window.FakeXMLHttpRequest = FakeXMLHttpRequest;
+} else if (this) {
+ this.FakeXMLHttpRequest = FakeXMLHttpRequest;
+}
+})();
diff --git a/vendor/assets/javascripts/pretender.js b/vendor/assets/javascripts/pretender.js
new file mode 100644
index 00000000000..b416deb6a4c
--- /dev/null
+++ b/vendor/assets/javascripts/pretender.js
@@ -0,0 +1,128 @@
+(function(window){
+
+var isNode = typeof process !== 'undefined' && process.toString() === '[object process]';
+var RouteRecognizer = isNode ? require('route-recognizer')['default'] : window.RouteRecognizer;
+var FakeXMLHttpRequest = isNode ? require('./bower_components/FakeXMLHttpRequest/fake_xml_http_request') : window.FakeXMLHttpRequest;
+
+function Pretender(maps){
+ maps = maps || function(){};
+ // Herein we keep track of RouteRecognizer instances
+ // keyed by HTTP method. Feel free to add more as needed.
+ this.registry = {
+ GET: new RouteRecognizer(),
+ PUT: new RouteRecognizer(),
+ POST: new RouteRecognizer(),
+ DELETE: new RouteRecognizer(),
+ PATCH: new RouteRecognizer(),
+ HEAD: new RouteRecognizer()
+ };
+
+ this.handlers = [];
+ this.handledRequests = [];
+ this.unhandledRequests = [];
+
+ // reference the native XMLHttpRequest object so
+ // it can be restored later
+ this._nativeXMLHttpRequest = window.XMLHttpRequest;
+
+ // capture xhr requests, channeling them into
+ // the route map.
+ window.XMLHttpRequest = interceptor(this);
+
+ // trigger the route map DSL.
+ maps.call(this);
+}
+
+function interceptor(pretender) {
+ function FakeRequest(){
+ // super()
+ FakeXMLHttpRequest.call(this);
+ }
+ // extend
+ var proto = new FakeXMLHttpRequest();
+ proto.send = function send(){
+ FakeXMLHttpRequest.prototype.send.apply(this, arguments);
+ pretender.handleRequest(this);
+ };
+
+ FakeRequest.prototype = proto;
+ return FakeRequest;
+}
+
+function verbify(verb){
+ return function(path, handler){
+ this.register(verb, path, handler);
+ };
+}
+
+Pretender.prototype = {
+ get: verbify('GET'),
+ post: verbify('POST'),
+ put: verbify('PUT'),
+ 'delete': verbify('DELETE'),
+ patch: verbify('PATCH'),
+ head: verbify('HEAD'),
+ register: function register(verb, path, handler){
+ handler.numberOfCalls = 0;
+ this.handlers.push(handler);
+
+ var registry = this.registry[verb];
+ registry.add([{path: path, handler: handler}]);
+ },
+ handleRequest: function handleRequest(request){
+ var verb = request.method.toUpperCase();
+ var path = request.url;
+ var handler = this._handlerFor(verb, path, request);
+
+ if (handler) {
+ handler.handler.numberOfCalls++;
+ this.handledRequests.push(request);
+
+ try {
+ var statusHeadersAndBody = handler.handler(request),
+ status = statusHeadersAndBody[0],
+ headers = statusHeadersAndBody[1],
+ body = this.prepareBody(statusHeadersAndBody[2]);
+ request.respond(status, headers, body);
+ this.handledRequest(verb, path, request);
+ } catch (error) {
+ this.erroredRequest(verb, path, request, error);
+ }
+ } else {
+ this.unhandledRequests.push(request);
+ this.unhandledRequest(verb, path, request);
+ }
+ },
+ prepareBody: function(body){ return body; },
+ handledRequest: function(verb, path, request){/* no-op */},
+ unhandledRequest: function(verb, path, request) {
+ throw new Error("Pretender intercepted "+verb+" "+path+" but no handler was defined for this type of request");
+ },
+ erroredRequest: function(verb, path, request, error){
+ error.message = "Pretender intercepted "+verb+" "+path+" but encountered an error: " + error.message;
+ throw error;
+ },
+ _handlerFor: function(verb, path, request){
+ var registry = this.registry[verb];
+ var matches = registry.recognize(path);
+
+ var match = matches ? matches[0] : null;
+ if (match) {
+ request.params = match.params;
+ request.queryParams = matches.queryParams;
+ }
+
+ return match;
+ },
+ shutdown: function shutdown(){
+ window.XMLHttpRequest = this._nativeXMLHttpRequest;
+ }
+};
+
+if (isNode) {
+ module.exports = Pretender;
+} else {
+ window.Pretender = Pretender;
+}
+
+})(window);
diff --git a/vendor/assets/javascripts/route-recognizer.js b/vendor/assets/javascripts/route-recognizer.js
new file mode 100644
index 00000000000..eb35d24aab7
--- /dev/null
+++ b/vendor/assets/javascripts/route-recognizer.js
@@ -0,0 +1,778 @@
+(function(global) {
+var define, requireModule, require, requirejs;
+
+(function() {
+
+ var _isArray;
+ if (!Array.isArray) {
+ _isArray = function (x) {
+ return Object.prototype.toString.call(x) === "[object Array]";
+ };
+ } else {
+ _isArray = Array.isArray;
+ }
+
+ var registry = {}, seen = {};
+ var FAILED = false;
+
+ var uuid = 0;
+
+ function tryFinally(tryable, finalizer) {
+ try {
+ return tryable();
+ } finally {
+ finalizer();
+ }
+ }
+
+
+ function Module(name, deps, callback, exports) {
+ var defaultDeps = ['require', 'exports', 'module'];
+
+ this.id = uuid++;
+ this.name = name;
+ this.deps = !deps.length && callback.length ? defaultDeps : deps;
+ this.exports = exports || { };
+ this.callback = callback;
+ this.state = undefined;
+ }
+
+ define = function(name, deps, callback) {
+ if (!_isArray(deps)) {
+ callback = deps;
+ deps = [];
+ }
+
+ registry[name] = new Module(name, deps, callback);
+ };
+
+ define.amd = {};
+
+ function reify(mod, name, seen) {
+ var deps = mod.deps;
+ var length = deps.length;
+ var reified = new Array(length);
+ var dep;
+ // TODO: new Module
+ // TODO: seen refactor
+ var module = { };
+
+ for (var i = 0, l = length; i < l; i++) {
+ dep = deps[i];
+ if (dep === 'exports') {
+ module.exports = reified[i] = seen;
+ } else if (dep === 'require') {
+ reified[i] = require;
+ } else if (dep === 'module') {
+ mod.exports = seen;
+ module = reified[i] = mod;
+ } else {
+ reified[i] = require(resolve(dep, name));
+ }
+ }
+
+ return {
+ deps: reified,
+ module: module
+ };
+ }
+
+ requirejs = require = requireModule = function(name) {
+ var mod = registry[name];
+ if (!mod) {
+ throw new Error('Could not find module ' + name);
+ }
+
+ if (mod.state !== FAILED &&
+ seen.hasOwnProperty(name)) {
+ return seen[name];
+ }
+
+ var reified;
+ var module;
+ var loaded = false;
+
+ seen[name] = { }; // placeholder for run-time cycles
+
+ tryFinally(function() {
+ reified = reify(mod, name, seen[name]);
+ module = mod.callback.apply(this, reified.deps);
+ loaded = true;
+ }, function() {
+ if (!loaded) {
+ mod.state = FAILED;
+ }
+ });
+
+ if (module === undefined && reified.module.exports) {
+ return (seen[name] = reified.module.exports);
+ } else {
+ return (seen[name] = module);
+ }
+ };
+
+ function resolve(child, name) {
+ if (child.charAt(0) !== '.') { return child; }
+
+ var parts = child.split('/');
+ var nameParts = name.split('/');
+ var parentBase = nameParts.slice(0, -1);
+
+ for (var i = 0, l = parts.length; i < l; i++) {
+ var part = parts[i];
+
+ if (part === '..') { parentBase.pop(); }
+ else if (part === '.') { continue; }
+ else { parentBase.push(part); }
+ }
+
+ return parentBase.join('/');
+ }
+
+ requirejs.entries = requirejs._eak_seen = registry;
+ requirejs.clear = function(){
+ requirejs.entries = requirejs._eak_seen = registry = {};
+ seen = state = {};
+ };
+})();
+
+define("route-recognizer",
+ ["route-recognizer/dsl","exports"],
+ function(__dependency1__, __exports__) {
+ "use strict";
+ var map = __dependency1__["default"];
+
+ var specials = [
+ '/', '.', '*', '+', '?', '|',
+ '(', ')', '[', ']', '{', '}', '\\'
+ ];
+
+ var escapeRegex = new RegExp('(\\' + specials.join('|\\') + ')', 'g');
+
+ function isArray(test) {
+ return Object.prototype.toString.call(test) === "[object Array]";
+ }
+
+ // A Segment represents a segment in the original route description.
+ // Each Segment type provides an `eachChar` and `regex` method.
+ //
+ // The `eachChar` method invokes the callback with one or more character
+ // specifications. A character specification consumes one or more input
+ // characters.
+ //
+ // The `regex` method returns a regex fragment for the segment. If the
+ // segment is a dynamic of star segment, the regex fragment also includes
+ // a capture.
+ //
+ // A character specification contains:
+ //
+ // * `validChars`: a String with a list of all valid characters, or
+ // * `invalidChars`: a String with a list of all invalid characters
+ // * `repeat`: true if the character specification can repeat
+
+ function StaticSegment(string) { this.string = string; }
+ StaticSegment.prototype = {
+ eachChar: function(callback) {
+ var string = this.string, ch;
+
+ for (var i=0, l=string.length; i " + n.nextStates.map(function(s) { return s.debug() }).join(" or ") + " )";
+ }).join(", ")
+ }
+ END IF **/
+
+ // This is a somewhat naive strategy, but should work in a lot of cases
+ // A better strategy would properly resolve /posts/:id/new and /posts/edit/:id.
+ //
+ // This strategy generally prefers more static and less dynamic matching.
+ // Specifically, it
+ //
+ // * prefers fewer stars to more, then
+ // * prefers using stars for less of the match to more, then
+ // * prefers fewer dynamic segments to more, then
+ // * prefers more static segments to more
+ function sortSolutions(states) {
+ return states.sort(function(a, b) {
+ if (a.types.stars !== b.types.stars) { return a.types.stars - b.types.stars; }
+
+ if (a.types.stars) {
+ if (a.types.statics !== b.types.statics) { return b.types.statics - a.types.statics; }
+ if (a.types.dynamics !== b.types.dynamics) { return b.types.dynamics - a.types.dynamics; }
+ }
+
+ if (a.types.dynamics !== b.types.dynamics) { return a.types.dynamics - b.types.dynamics; }
+ if (a.types.statics !== b.types.statics) { return b.types.statics - a.types.statics; }
+
+ return 0;
+ });
+ }
+
+ function recognizeChar(states, ch) {
+ var nextStates = [];
+
+ for (var i=0, l=states.length; i 2 && key.slice(keyLength -2) === '[]') {
+ isArray = true;
+ key = key.slice(0, keyLength - 2);
+ if(!queryParams[key]) {
+ queryParams[key] = [];
+ }
+ }
+ value = pair[1] ? decodeURIComponent(pair[1]) : '';
+ }
+ if (isArray) {
+ queryParams[key].push(value);
+ } else {
+ queryParams[key] = value;
+ }
+ }
+ return queryParams;
+ },
+
+ recognize: function(path) {
+ var states = [ this.rootState ],
+ pathLen, i, l, queryStart, queryParams = {},
+ isSlashDropped = false;
+
+ path = decodeURI(path);
+
+ queryStart = path.indexOf('?');
+ if (queryStart !== -1) {
+ var queryString = path.substr(queryStart + 1, path.length);
+ path = path.substr(0, queryStart);
+ queryParams = this.parseQueryString(queryString);
+ }
+
+ // DEBUG GROUP path
+
+ if (path.charAt(0) !== "/") { path = "/" + path; }
+
+ pathLen = path.length;
+ if (pathLen > 1 && path.charAt(pathLen - 1) === "/") {
+ path = path.substr(0, pathLen - 1);
+ isSlashDropped = true;
+ }
+
+ for (i=0, l=path.length; i