iqjax 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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));