iqjax 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,90 @@
1
+ var fs = require("fs");
2
+
3
+ function ping() {
4
+ fs.write("/dev/stdout", ".", "w");
5
+ }
6
+
7
+ function snapshot(page, filepath) {
8
+ page.viewportSize = { width: 1024, height: 768 };
9
+ page.render(filepath);
10
+ }
11
+
12
+ /**
13
+ * Wait until the test condition is true or a timeout occurs. Useful for waiting
14
+ * on a server response or for a ui change (fadeIn, etc.) to occur.
15
+ *
16
+ * @param testFx javascript condition that evaluates to a boolean,
17
+ * it can be passed in as a string (e.g.: "1 == 1" or "$('#bar').is(':visible')" or
18
+ * as a callback function.
19
+ * @param onReady what to do when testFx condition is fulfilled,
20
+ * it can be passed in as a string (e.g.: "1 == 1" or "$('#bar').is(':visible')" or
21
+ * as a callback function.
22
+ * @param timeOutMillis the max amount of time to wait. If not specified, 3 sec is used.
23
+ */
24
+ function waitFor(testFx, onReady, timeOutMillis) {
25
+ var maxtimeOutMillis = timeOutMillis ? timeOutMillis : 3001, //< Default Max Timout is 3s
26
+ start = new Date().getTime(),
27
+ condition = false,
28
+ interval = setInterval(function() {
29
+ ping();
30
+ if ( (new Date().getTime() - start < maxtimeOutMillis) && !condition ) {
31
+ // If not time-out yet and condition not yet fulfilled
32
+ condition = (typeof(testFx) === "string" ? eval(testFx) : testFx()); //< defensive code
33
+ } else {
34
+ if(!condition) {
35
+ // If condition still not fulfilled (timeout but condition is 'false')
36
+ console.log("'waitFor()' timeout");
37
+ phantom.exit(1);
38
+ } else {
39
+ // Condition fulfilled (timeout and/or condition is 'true')
40
+ console.log("'waitFor()' finished in " + (new Date().getTime() - start) + "ms.");
41
+ typeof(onReady) === "string" ? eval(onReady) : onReady(); //< Do what it's supposed to do once the condition is fulfilled
42
+ clearInterval(interval); //< Stop this interval
43
+ }
44
+ }
45
+ }, 25); //< repeat check every 25 ms
46
+ ping();
47
+ };
48
+
49
+
50
+ if (phantom.args.length === 0 || phantom.args.length > 2) {
51
+ console.log('Usage: run-qunit.js URL');
52
+ phantom.exit(1);
53
+ }
54
+
55
+ var page = new WebPage();
56
+
57
+ // Route "console.log()" calls from within the Page context to the main Phantom context (i.e. current "this")
58
+ page.onConsoleMessage = function(msg) {
59
+ console.log(msg);
60
+ };
61
+
62
+ page.open(phantom.args[0], function(status){
63
+ if (status !== "success") {
64
+ console.log("Unable to access network");
65
+ phantom.exit(1);
66
+ } else {
67
+ waitFor(function(){
68
+ return page.evaluate(function(){
69
+ var el = document.getElementById('qunit-testresult');
70
+ if (el && el.innerText.match('completed')) {
71
+ return true;
72
+ }
73
+ return false;
74
+ });
75
+ }, function(){
76
+ var failedNum = page.evaluate(function(){
77
+ var el = document.getElementById('qunit-testresult');
78
+ console.log(el.innerText);
79
+ try {
80
+ return el.getElementsByClassName('failed')[0].innerHTML;
81
+ } catch (e) { }
82
+ return 10000;
83
+ });
84
+ if(parseInt(failedNum, 10) > 0) {
85
+ snapshot(page, "snapshot.png");
86
+ }
87
+ phantom.exit((parseInt(failedNum, 10) > 0) ? 1 : 0);
88
+ });
89
+ }
90
+ });
@@ -0,0 +1,36 @@
1
+ # encoding: UTF-8
2
+
3
+ # Copyright 2012 innoQ Deutschland GmbH
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+
17
+ require 'test/unit'
18
+
19
+ class ClientTest < Test::Unit::TestCase
20
+
21
+ def test_PhantomJS_availability
22
+ system("phantomjs --version")
23
+ assert $?.success?
24
+ end
25
+
26
+ def test_client_side_test_suites
27
+ assert _run("index.html")
28
+ end
29
+
30
+ def _run(suite)
31
+ path = File.expand_path(File.dirname(__FILE__))
32
+ system("cd #{path} && phantomjs lib/run-qunit.js #{suite}")
33
+ return $?.success?
34
+ end
35
+
36
+ end
@@ -0,0 +1,175 @@
1
+ /*jslint vars: true, unparam: true, browser: true, white: true */
2
+ /*global jQuery, QUnit, module, test, ok, strictEqual, raises */
3
+
4
+ /* Copyright 2012 innoQ Deutschland GmbH
5
+
6
+ Licensed under the Apache License, Version 2.0 (the "License");
7
+ you may not use this file except in compliance with the License.
8
+ You may obtain a copy of the License at
9
+
10
+ http://www.apache.org/licenses/LICENSE-2.0
11
+
12
+ Unless required by applicable law or agreed to in writing, software
13
+ distributed under the License is distributed on an "AS IS" BASIS,
14
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ See the License for the specific language governing permissions and
16
+ limitations under the License.
17
+ */
18
+
19
+ (function($) {
20
+
21
+ "use strict";
22
+
23
+ module("iQjax", {
24
+
25
+ setup: function() {
26
+ var fixtures = $("#qunit-fixture");
27
+ this.fixturesHtml = fixtures.html();
28
+ fixtures.empty(); // prevents duplicate IDs
29
+ this.ctx = $("<div><p /></div>").appendTo(document.body);
30
+
31
+ $.mockjaxSettings['log'] = function(msg) { };
32
+
33
+ // Mock /items/:id/edit
34
+ $.mockjax(function(settings) {
35
+ var match = settings.url.match(/^\/items\/([0-9]+)\/edit$/);
36
+ if (match) {
37
+ return {
38
+ responseTime: 10,
39
+ responseText: '<form action="/items/' + match[1] +
40
+ '" method="POST"><input type="Submit" value="Save"></form>'
41
+ };
42
+ }
43
+ return;
44
+ });
45
+ // Mock /items/new
46
+ $.mockjax({
47
+ url: "/items/new",
48
+ responseText: '<form action="/items" method="POST"><input type="Submit" value="Save"></form>'
49
+ });
50
+ // Mock /items/:id and /items (the #create action)
51
+ $.mockjax(function(settings) {
52
+ var match = settings.url.match(/^\/items(\/([0-9]+))?(\?.*)?$/);
53
+ if (match) {
54
+ var itemName = match[2] || "new";
55
+ return {
56
+ responseTime: 10,
57
+ headers: {
58
+ "X-IQJAX": "item" + itemName
59
+ },
60
+ responseText: '<li id="item' + itemName +
61
+ '">Item from Server</li>'
62
+ };
63
+ }
64
+ return;
65
+ });
66
+ },
67
+
68
+ teardown: function() {
69
+ $("#qunit-fixture").html(this.fixturesHtml);
70
+ this.ctx.remove();
71
+ QUnit.reset();
72
+ }
73
+
74
+ });
75
+
76
+ test("uses container for result of AJAX call", function() {
77
+ this.ctx.html(this.fixturesHtml);
78
+ var root = $("#basic", this.ctx),
79
+ target = $("#my-container1");
80
+
81
+ root.iqjax({ target: "#my-container1" }).bind({
82
+ "iqjax:content": function() {
83
+ QUnit.start();
84
+ strictEqual($("form input[type=submit]", target).length, 1,
85
+ "target container should contain a form on `iqjax:content`");
86
+ $("form", target).submit();
87
+ QUnit.stop();
88
+ },
89
+ "iqjax:update": function() {
90
+ strictEqual(target.children().length, 0,
91
+ "target container should be empty on `iqjax:update`");
92
+ strictEqual($("#item1", root).text(), "Item from Server",
93
+ "collection item should be replaced by the item from the server on `iqjax:update`");
94
+ QUnit.start();
95
+ }
96
+ });
97
+
98
+ root.find("#item1 a").click();
99
+ QUnit.stop();
100
+ });
101
+
102
+ test("appends new items to the list", function() {
103
+ this.ctx.html(this.fixturesHtml);
104
+ var root = $("#basic", this.ctx),
105
+ target = $("#my-container1");
106
+
107
+ root.iqjax({ target: "#my-container1" }).bind({
108
+ "iqjax:content": function() {
109
+ QUnit.start();
110
+ strictEqual($("form input[type=submit]", target).length, 1,
111
+ "target container should contain the form");
112
+ $("form", target).submit();
113
+ QUnit.stop();
114
+ },
115
+ "iqjax:update": function() {
116
+ strictEqual($("#itemnew", root).text(), "Item from Server");
117
+ QUnit.start();
118
+ }
119
+ });
120
+
121
+ root.find("#new").click();
122
+ QUnit.stop();
123
+
124
+ });
125
+
126
+ test("replaces items outside the collection scope", function() {
127
+ this.ctx.html(this.fixturesHtml);
128
+ var root = $("#basic", this.ctx),
129
+ target = $("#my-container1");
130
+
131
+ var oldContent = $("#item-list", root).text();
132
+ root.iqjax({ target: "#my-container1" }).bind({
133
+ "iqjax:content": function() {
134
+ QUnit.start();
135
+ $("form", target).submit();
136
+ QUnit.stop();
137
+ },
138
+ "iqjax:update": function() {
139
+ strictEqual($("#other-item-list #item3", root).text(), "Item from Server");
140
+ strictEqual($("#item-list", root).text(), oldContent);
141
+ QUnit.start();
142
+ }
143
+ });
144
+
145
+ root.find("#dummy-item-pointing-to-item3 a").click();
146
+ QUnit.stop();
147
+
148
+ });
149
+
150
+ test("replaces element with result of iQjax call", function() {
151
+ return; // FIXME: TEST DISABLED
152
+ this.ctx.html(this.fixturesHtml);
153
+ var root = $("#basic", this.ctx);
154
+
155
+ root.iqjax({
156
+ "iqjax:content": function() {
157
+ QUnit.start();
158
+ // #item1 should be replaced by the form
159
+ strictEqual($("form input[type=submit]", root).length, 1);
160
+ $("form", root).submit();
161
+ QUnit.stop();
162
+ },
163
+ "iqjax:update": function() {
164
+ QUnit.start();
165
+ // form should be replaced by the "new" li element
166
+ strictEqual($("#item1", root).text(), "Item from Server");
167
+ }
168
+
169
+ });
170
+
171
+ root.find("#item1 a").click();
172
+ QUnit.stop();
173
+ });
174
+
175
+ }(jQuery));
@@ -0,0 +1,200 @@
1
+ /*jslint vars: true, unparam: true, browser: true, white: true */
2
+ /*global jQuery */
3
+
4
+ /* Copyright 2012 innoQ Deutschland GmbH
5
+
6
+ Licensed under the Apache License, Version 2.0 (the "License");
7
+ you may not use this file except in compliance with the License.
8
+ You may obtain a copy of the License at
9
+
10
+ http://www.apache.org/licenses/LICENSE-2.0
11
+
12
+ Unless required by applicable law or agreed to in writing, software
13
+ distributed under the License is distributed on an "AS IS" BASIS,
14
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ See the License for the specific language governing permissions and
16
+ limitations under the License.
17
+ */
18
+
19
+ // dynamic loading of content
20
+ // inspired by https://github.com/defunkt/jquery-iqjax - though minus the
21
+ // History API and based on Rails's data-remote functionality and error handling
22
+ (function($) {
23
+
24
+ "use strict";
25
+
26
+ var iqjax_uri, requestMethod;
27
+
28
+ var IQjax = function(context, target) {
29
+ // NB: context and target must not be descendants of each other
30
+ this.context = context;
31
+ this.target = target;
32
+ this.indicator = $(".indicator", target);
33
+ if(!this.indicator.length) {
34
+ this.indicator = $('<div class="indicator hidden"></div>');
35
+ }
36
+
37
+ // NB: redefining `this` in event handlers (via `proxy`) is dangerously
38
+ // misleading, but avoids non-standard function signatures for event
39
+ // handlers - plus for instance methods, it's actually more intuitive
40
+
41
+ // cancel buttons
42
+ var uri = document.location.toString().split("#")[0]; // XXX: use as selector makes for brittle heuristic?
43
+ this.target.on("click", 'a[href="' + uri + '"]', $.proxy(this.onCancel, this));
44
+
45
+ var selector = "a[data-remote], form[data-remote]";
46
+ var handlers = {
47
+ "ajax:beforeSend": $.proxy(this.beforeSend, this),
48
+ "ajax:success": $.proxy(this.onSuccess, this),
49
+ "ajax:error": $.proxy(this.onError, this)
50
+ };
51
+ this.context.add(this.target).on(handlers, selector);
52
+
53
+ // dirty state: protect against accidental dismissal
54
+ var self = this;
55
+ this.target.on("change", "input, textarea, select", function(ev) {
56
+ self.dirty = true;
57
+ });
58
+ };
59
+ $.extend(IQjax.prototype, {
60
+ onCancel: function(ev) {
61
+ if(!this.checkDirty(ev)) {
62
+ this.reset();
63
+ }
64
+ ev.preventDefault();
65
+ },
66
+ beforeSend: function(ev, xhr, settings) {
67
+ if(this.checkDirty(ev)) {
68
+ return false;
69
+ }
70
+
71
+ var contextAction = $.contains(this.context[0], ev.currentTarget); // TODO: rename -- XXX: hacky?
72
+ if(contextAction) {
73
+ this.reset();
74
+ }
75
+ $(ev.currentTarget).addClass("active");
76
+ this.target.prepend(this.indicator);
77
+ this.indicator.show();
78
+ settings.url = iqjax_uri(settings.url);
79
+ this.target.children().not(this.indicator).css("opacity", 0.5);
80
+ },
81
+ onSuccess: function(ev, data, status, xhr) {
82
+ if(!ev.currentTarget.parentNode) {
83
+ // FIXME: it's not clear under what circumstances this occurs;
84
+ // apparently, for reasons yet unknown, this event is erroneously
85
+ // triggered twice (and not all such duplicate events are
86
+ // intercepted by this hack, e.g. DELETE operations)
87
+ return;
88
+ }
89
+
90
+ var targetAction = $.contains(this.target[0], ev.currentTarget), // TODO: rename -- XXX: hacky?
91
+ el = $(ev.currentTarget),
92
+ reqMethod = "GET",
93
+ origin;
94
+
95
+ if(el.is("form")) {
96
+ if(targetAction) {
97
+ this.onUpdate.call(this, data, status, xhr); // TODO: should trigger event
98
+ return;
99
+ } else {
100
+ reqMethod = requestMethod(el);
101
+ origin = el.closest(".iqjax-entity"); // TODO: document
102
+ }
103
+ }
104
+
105
+ if(origin && reqMethod === "DELETE") {
106
+ this.indicator.hide();
107
+ origin.slideUp($.proxy(origin.remove, origin));
108
+ } else {
109
+ this.display(data);
110
+ }
111
+ },
112
+ onError: function(ev, xhr, error, exc) {
113
+ var cType = xhr.getResponseHeader("Content-Type"),
114
+ isHTML = cType ? cType.match(/\btext\/html\b/) : false;
115
+ this.display(xhr.responseText || error, !isHTML);
116
+ },
117
+ onUpdate: function(data, status, xhr) {
118
+ var src = xhr.getResponseHeader("X-IQJAX"), // TODO: document
119
+ item = $("<div />").html(data).find("#" + src),
120
+ origin = $(".active", this.context),
121
+ container = origin.closest("[data-iqjax-collection]"), // TODO: document
122
+ form = $("form.active", this.target),
123
+ reqMethod = requestMethod(form);
124
+
125
+ this.target.empty();
126
+
127
+ container = container.jquery ? container : $(container);
128
+ // account for nested update targets -- XXX: hacky!?
129
+ if(reqMethod === "PUT" && origin.closest(".iqjax-entity")[0] === container[0]) {
130
+ container = container.parent().closest("[data-iqjax-collection]");
131
+ }
132
+ container = $(container.data("iqjax-collection"));
133
+
134
+ if(reqMethod === "GET") { // multi-step forms
135
+ this.display(data);
136
+ } else {
137
+ this.reset();
138
+ }
139
+
140
+ var el = $("#" + src, self.context);
141
+ if(el.length) {
142
+ el.replaceWith(item);
143
+ } else { // new
144
+ container.append(item);
145
+ }
146
+
147
+ item.addClass("success").removeClass("success", "glacial");
148
+ this.context.trigger("iqjax:update", { item: item, doc: data });
149
+ },
150
+ reset: function() {
151
+ this.target.empty();
152
+ this.dirty = false;
153
+ $(".active", this.context).removeClass("active");
154
+ },
155
+ display: function(txt, plain) {
156
+ this.indicator.hide();
157
+ this.target[plain ? "text" : "html"](txt).prepend(this.indicator);
158
+ // intercept interactions to prevent full page reload
159
+ $("form", this.target).attr("data-remote", true);
160
+ this.context.trigger("iqjax:content", { iqjax: this });
161
+ },
162
+ // dirty state: protect against accidental dismissal
163
+ dirtyMsg: "Es gibt ungespeicherte Änderungen - fortfahren (Änderungen werden verworfen)?", // TODO: rephrase
164
+ checkDirty: function(ev) {
165
+ var isSubmit = $(ev.currentTarget).is("form");
166
+ if(this.dirty && !isSubmit) {
167
+ if(confirm(this.dirtyMsg)) {
168
+ this.dirty = false;
169
+ } else {
170
+ return true;
171
+ }
172
+ }
173
+ }
174
+ });
175
+
176
+ // hack to prevent cache confusion (browser caches should distinguish between
177
+ // iQjax and non-iQjax requests) - it'd be more elegant to modify the jqXHR's
178
+ // `data` property here, but it appears that's being overridden by jQuery later
179
+ iqjax_uri = function(uri) {
180
+ return uri + (uri.indexOf("?") === -1 ? "?" : "&") + "_iqjax=1";
181
+ };
182
+
183
+ requestMethod = function(form) {
184
+ var m = $("input[name=_method]", form).val() || form.attr("method") || "GET";
185
+ return m.toUpperCase();
186
+ };
187
+
188
+ // uses dynamic in-page loading for child elements matching `a[data-remote]`
189
+ // options.target is the DOM element within which contents are to be displayed
190
+ // (defaults to selector contained in context element's `data-iqjax` attribute)
191
+ $.fn.iqjax = function(options) {
192
+ options = options || {};
193
+ return this.each(function(i, node) {
194
+ var context = $(this),
195
+ target = $(options.target || context.data("iqjax")); // TODO: document
196
+ new IQjax(context, target);
197
+ });
198
+ };
199
+
200
+ }(jQuery));