casperjs 1.0.0.RC1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (125) hide show
  1. data/CHANGELOG.md +179 -0
  2. data/LICENSE.md +19 -0
  3. data/README.md +30 -0
  4. data/bin/bootstrap.js +292 -0
  5. data/bin/usage.txt +10 -0
  6. data/casperjs.gemspec +21 -0
  7. data/modules/casper.js +1679 -0
  8. data/modules/cli.js +138 -0
  9. data/modules/clientutils.js +595 -0
  10. data/modules/colorizer.js +129 -0
  11. data/modules/events.js +247 -0
  12. data/modules/injector.js +93 -0
  13. data/modules/mouse.js +110 -0
  14. data/modules/querystring.js +187 -0
  15. data/modules/tester.js +807 -0
  16. data/modules/utils.js +429 -0
  17. data/modules/vendors/coffee-script.js +8 -0
  18. data/modules/xunit.js +123 -0
  19. data/package.json +35 -0
  20. data/rubybin/casperjs +57 -0
  21. data/samples/bbcshots.coffee +64 -0
  22. data/samples/bbcshots.js +80 -0
  23. data/samples/cliplay.coffee +19 -0
  24. data/samples/cliplay.js +21 -0
  25. data/samples/customevents.coffee +11 -0
  26. data/samples/customevents.js +13 -0
  27. data/samples/customlogging.coffee +33 -0
  28. data/samples/customlogging.js +42 -0
  29. data/samples/download.coffee +10 -0
  30. data/samples/download.js +11 -0
  31. data/samples/dynamic.coffee +60 -0
  32. data/samples/dynamic.js +65 -0
  33. data/samples/each.coffee +14 -0
  34. data/samples/each.js +17 -0
  35. data/samples/events.coffee +34 -0
  36. data/samples/events.js +41 -0
  37. data/samples/extends.coffee +29 -0
  38. data/samples/extends.js +37 -0
  39. data/samples/googlelinks.coffee +27 -0
  40. data/samples/googlelinks.js +33 -0
  41. data/samples/googlematch.coffee +47 -0
  42. data/samples/googlematch.js +65 -0
  43. data/samples/googlepagination.coffee +40 -0
  44. data/samples/googlepagination.js +51 -0
  45. data/samples/googletesting.coffee +17 -0
  46. data/samples/googletesting.js +23 -0
  47. data/samples/logcolor.coffee +10 -0
  48. data/samples/logcolor.js +11 -0
  49. data/samples/metaextract.coffee +23 -0
  50. data/samples/metaextract.js +29 -0
  51. data/samples/multirun.coffee +37 -0
  52. data/samples/multirun.js +56 -0
  53. data/samples/screenshot.coffee +28 -0
  54. data/samples/screenshot.js +33 -0
  55. data/samples/statushandlers.coffee +15 -0
  56. data/samples/statushandlers.js +19 -0
  57. data/samples/steptimeout.coffee +37 -0
  58. data/samples/steptimeout.js +45 -0
  59. data/samples/timeout.coffee +39 -0
  60. data/samples/timeout.js +47 -0
  61. data/tests/run.js +76 -0
  62. data/tests/site/alert.html +10 -0
  63. data/tests/site/click.html +40 -0
  64. data/tests/site/confirm.html +12 -0
  65. data/tests/site/elementattribute.html +6 -0
  66. data/tests/site/error.html +10 -0
  67. data/tests/site/form.html +26 -0
  68. data/tests/site/global.html +9 -0
  69. data/tests/site/images/phantom.png +0 -0
  70. data/tests/site/index.html +17 -0
  71. data/tests/site/mouse-events.html +47 -0
  72. data/tests/site/multiple-forms.html +16 -0
  73. data/tests/site/page1.html +8 -0
  74. data/tests/site/page2.html +8 -0
  75. data/tests/site/page3.html +8 -0
  76. data/tests/site/prompt.html +12 -0
  77. data/tests/site/resources.html +16 -0
  78. data/tests/site/result.html +11 -0
  79. data/tests/site/test.html +10 -0
  80. data/tests/site/visible.html +17 -0
  81. data/tests/site/waitFor.html +22 -0
  82. data/tests/suites/casper/agent.js +24 -0
  83. data/tests/suites/casper/capture.js +31 -0
  84. data/tests/suites/casper/click.js +61 -0
  85. data/tests/suites/casper/confirm.js +21 -0
  86. data/tests/suites/casper/elementattribute.js +8 -0
  87. data/tests/suites/casper/encode.js +20 -0
  88. data/tests/suites/casper/evaluate.js +27 -0
  89. data/tests/suites/casper/events.js +38 -0
  90. data/tests/suites/casper/exists.js +9 -0
  91. data/tests/suites/casper/fetchtext.js +9 -0
  92. data/tests/suites/casper/flow.coffee +38 -0
  93. data/tests/suites/casper/formfill.js +69 -0
  94. data/tests/suites/casper/global.js +9 -0
  95. data/tests/suites/casper/history.js +21 -0
  96. data/tests/suites/casper/hooks.js +41 -0
  97. data/tests/suites/casper/logging.js +38 -0
  98. data/tests/suites/casper/mouseevents.js +27 -0
  99. data/tests/suites/casper/onerror.js +19 -0
  100. data/tests/suites/casper/open.js +73 -0
  101. data/tests/suites/casper/prompt.js +17 -0
  102. data/tests/suites/casper/resources.coffee +24 -0
  103. data/tests/suites/casper/start.js +15 -0
  104. data/tests/suites/casper/steps.js +32 -0
  105. data/tests/suites/casper/viewport.js +11 -0
  106. data/tests/suites/casper/visible.js +17 -0
  107. data/tests/suites/casper/wait.js +27 -0
  108. data/tests/suites/casper/xpath.js +32 -0
  109. data/tests/suites/cli.js +125 -0
  110. data/tests/suites/clientutils.js +84 -0
  111. data/tests/suites/coffee.coffee +19 -0
  112. data/tests/suites/fs.js +36 -0
  113. data/tests/suites/http_status.js +28 -0
  114. data/tests/suites/injector.js +64 -0
  115. data/tests/suites/tester.js +121 -0
  116. data/tests/suites/utils.js +209 -0
  117. data/tests/suites/xunit.js +16 -0
  118. data/tests/testdir/01_a/abc.js +0 -0
  119. data/tests/testdir/01_a/def.js +0 -0
  120. data/tests/testdir/02_b/abc.js +0 -0
  121. data/tests/testdir/03_a.js +0 -0
  122. data/tests/testdir/03_b.js +0 -0
  123. data/tests/testdir/04/01_init.js +0 -0
  124. data/tests/testdir/04/02_do.js +0 -0
  125. metadata +192 -0
@@ -0,0 +1,110 @@
1
+ /*!
2
+ * Casper is a navigation utility for PhantomJS.
3
+ *
4
+ * Documentation: http://casperjs.org/
5
+ * Repository: http://github.com/n1k0/casperjs
6
+ *
7
+ * Copyright (c) 2011-2012 Nicolas Perriault
8
+ *
9
+ * Part of source code is Copyright Joyent, Inc. and other Node contributors.
10
+ *
11
+ * Permission is hereby granted, free of charge, to any person obtaining a
12
+ * copy of this software and associated documentation files (the "Software"),
13
+ * to deal in the Software without restriction, including without limitation
14
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
15
+ * and/or sell copies of the Software, and to permit persons to whom the
16
+ * Software is furnished to do so, subject to the following conditions:
17
+ *
18
+ * The above copyright notice and this permission notice shall be included
19
+ * in all copies or substantial portions of the Software.
20
+ *
21
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
22
+ * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
23
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
24
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
25
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
26
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
27
+ * DEALINGS IN THE SOFTWARE.
28
+ *
29
+ */
30
+
31
+ /*global CasperError exports require*/
32
+
33
+ var utils = require('utils');
34
+
35
+ exports.create = function create(casper) {
36
+ "use strict";
37
+ return new Mouse(casper);
38
+ };
39
+
40
+ var Mouse = function Mouse(casper) {
41
+ "use strict";
42
+ if (!utils.isCasperObject(casper)) {
43
+ throw new CasperError('Mouse() needs a Casper instance');
44
+ }
45
+
46
+ var slice = Array.prototype.slice;
47
+
48
+ var nativeEvents = ['mouseup', 'mousedown', 'click', 'mousemove'];
49
+ var emulatedEvents = ['mouseover', 'mouseout'];
50
+ var supportedEvents = nativeEvents.concat(emulatedEvents);
51
+
52
+ function computeCenter(selector) {
53
+ var bounds = casper.getElementBounds(selector);
54
+ if (utils.isClipRect(bounds)) {
55
+ var x = Math.round(bounds.left + bounds.width / 2);
56
+ var y = Math.round(bounds.top + bounds.height / 2);
57
+ return [x, y];
58
+ }
59
+ }
60
+
61
+ function processEvent(type, args) {
62
+ if (!utils.isString(type) || supportedEvents.indexOf(type) === -1) {
63
+ throw new CasperError('Mouse.processEvent(): Unsupported mouse event type: ' + type);
64
+ }
65
+ if (emulatedEvents.indexOf(type) > -1) {
66
+ casper.log("Mouse.processEvent(): no native fallback for type " + type, "warning");
67
+ }
68
+ args = slice.call(args); // cast Arguments -> Array
69
+ casper.emit('mouse.' + type.replace('mouse', ''), args);
70
+ switch (args.length) {
71
+ case 0:
72
+ throw new CasperError('Mouse.processEvent(): Too few arguments');
73
+ case 1:
74
+ // selector
75
+ var selector = args[0];
76
+ casper.page.sendEvent.apply(casper.page, [type].concat(computeCenter(selector)));
77
+ break;
78
+ case 2:
79
+ // coordinates
80
+ if (!utils.isNumber(args[0]) || !utils.isNumber(args[1])) {
81
+ throw new CasperError('Mouse.processEvent(): No valid coordinates passed: ' + args);
82
+ }
83
+ casper.page.sendEvent(type, args[0], args[1]);
84
+ break;
85
+ default:
86
+ throw new CasperError('Mouse.processEvent(): Too many arguments');
87
+ }
88
+ }
89
+
90
+ this.processEvent = function() {
91
+ processEvent(arguments[0], slice.call(arguments, 1));
92
+ };
93
+
94
+ this.click = function click() {
95
+ processEvent('click', arguments);
96
+ };
97
+
98
+ this.down = function down() {
99
+ processEvent('mousedown', arguments);
100
+ };
101
+
102
+ this.move = function move() {
103
+ processEvent('mousemove', arguments);
104
+ };
105
+
106
+ this.up = function up() {
107
+ processEvent('mouseup', arguments);
108
+ };
109
+ };
110
+ exports.Mouse = Mouse;
@@ -0,0 +1,187 @@
1
+ // Copyright Joyent, Inc. and other Node contributors.
2
+ //
3
+ // Permission is hereby granted, free of charge, to any person obtaining a
4
+ // copy of this software and associated documentation files (the
5
+ // "Software"), to deal in the Software without restriction, including
6
+ // without limitation the rights to use, copy, modify, merge, publish,
7
+ // distribute, sublicense, and/or sell copies of the Software, and to permit
8
+ // persons to whom the Software is furnished to do so, subject to the
9
+ // following conditions:
10
+ //
11
+ // The above copyright notice and this permission notice shall be included
12
+ // in all copies or substantial portions of the Software.
13
+ //
14
+ // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
15
+ // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
17
+ // NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
18
+ // DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
19
+ // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
20
+ // USE OR OTHER DEALINGS IN THE SOFTWARE.
21
+
22
+ // Query String Utilities
23
+
24
+ var QueryString = exports;
25
+ //var urlDecode = process.binding('http_parser').urlDecode; // phantomjs incompatible
26
+
27
+
28
+ // If obj.hasOwnProperty has been overridden, then calling
29
+ // obj.hasOwnProperty(prop) will break.
30
+ // See: https://github.com/joyent/node/issues/1707
31
+ function hasOwnProperty(obj, prop) {
32
+ return Object.prototype.hasOwnProperty.call(obj, prop);
33
+ }
34
+
35
+
36
+ function charCode(c) {
37
+ return c.charCodeAt(0);
38
+ }
39
+
40
+
41
+ // a safe fast alternative to decodeURIComponent
42
+ QueryString.unescapeBuffer = function(s, decodeSpaces) {
43
+ var out = new Buffer(s.length);
44
+ var state = 'CHAR'; // states: CHAR, HEX0, HEX1
45
+ var n, m, hexchar;
46
+
47
+ for (var inIndex = 0, outIndex = 0; inIndex <= s.length; inIndex++) {
48
+ var c = s.charCodeAt(inIndex);
49
+ switch (state) {
50
+ case 'CHAR':
51
+ switch (c) {
52
+ case charCode('%'):
53
+ n = 0;
54
+ m = 0;
55
+ state = 'HEX0';
56
+ break;
57
+ case charCode('+'):
58
+ if (decodeSpaces) c = charCode(' ');
59
+ // pass thru
60
+ default:
61
+ out[outIndex++] = c;
62
+ break;
63
+ }
64
+ break;
65
+
66
+ case 'HEX0':
67
+ state = 'HEX1';
68
+ hexchar = c;
69
+ if (charCode('0') <= c && c <= charCode('9')) {
70
+ n = c - charCode('0');
71
+ } else if (charCode('a') <= c && c <= charCode('f')) {
72
+ n = c - charCode('a') + 10;
73
+ } else if (charCode('A') <= c && c <= charCode('F')) {
74
+ n = c - charCode('A') + 10;
75
+ } else {
76
+ out[outIndex++] = charCode('%');
77
+ out[outIndex++] = c;
78
+ state = 'CHAR';
79
+ break;
80
+ }
81
+ break;
82
+
83
+ case 'HEX1':
84
+ state = 'CHAR';
85
+ if (charCode('0') <= c && c <= charCode('9')) {
86
+ m = c - charCode('0');
87
+ } else if (charCode('a') <= c && c <= charCode('f')) {
88
+ m = c - charCode('a') + 10;
89
+ } else if (charCode('A') <= c && c <= charCode('F')) {
90
+ m = c - charCode('A') + 10;
91
+ } else {
92
+ out[outIndex++] = charCode('%');
93
+ out[outIndex++] = hexchar;
94
+ out[outIndex++] = c;
95
+ break;
96
+ }
97
+ out[outIndex++] = 16 * n + m;
98
+ break;
99
+ }
100
+ }
101
+
102
+ // TODO support returning arbitrary buffers.
103
+
104
+ return out.slice(0, outIndex - 1);
105
+ };
106
+
107
+
108
+ QueryString.unescape = function(s, decodeSpaces) {
109
+ return QueryString.unescapeBuffer(s, decodeSpaces).toString();
110
+ };
111
+
112
+
113
+ QueryString.escape = function(str) {
114
+ return encodeURIComponent(str);
115
+ };
116
+
117
+ var stringifyPrimitive = function(v) {
118
+ switch (typeof v) {
119
+ case 'string':
120
+ return v;
121
+
122
+ case 'boolean':
123
+ return v ? 'true' : 'false';
124
+
125
+ case 'number':
126
+ return isFinite(v) ? v : '';
127
+
128
+ default:
129
+ return '';
130
+ }
131
+ };
132
+
133
+
134
+ QueryString.stringify = QueryString.encode = function(obj, sep, eq, name) {
135
+ sep = sep || '&';
136
+ eq = eq || '=';
137
+ obj = (obj === null) ? undefined : obj;
138
+
139
+ switch (typeof obj) {
140
+ case 'object':
141
+ return Object.keys(obj).map(function(k) {
142
+ if (Array.isArray(obj[k])) {
143
+ return obj[k].map(function(v) {
144
+ return QueryString.escape(stringifyPrimitive(k)) +
145
+ eq +
146
+ QueryString.escape(stringifyPrimitive(v));
147
+ }).join(sep);
148
+ } else {
149
+ return QueryString.escape(stringifyPrimitive(k)) +
150
+ eq +
151
+ QueryString.escape(stringifyPrimitive(obj[k]));
152
+ }
153
+ }).join(sep);
154
+
155
+ default:
156
+ if (!name) return '';
157
+ return QueryString.escape(stringifyPrimitive(name)) + eq +
158
+ QueryString.escape(stringifyPrimitive(obj));
159
+ }
160
+ };
161
+
162
+ // Parse a key=val string.
163
+ QueryString.parse = QueryString.decode = function(qs, sep, eq) {
164
+ sep = sep || '&';
165
+ eq = eq || '=';
166
+ var obj = {};
167
+
168
+ if (typeof qs !== 'string' || qs.length === 0) {
169
+ return obj;
170
+ }
171
+
172
+ qs.split(sep).forEach(function(kvp) {
173
+ var x = kvp.split(eq);
174
+ var k = QueryString.unescape(x[0], true);
175
+ var v = QueryString.unescape(x.slice(1).join(eq), true);
176
+
177
+ if (!hasOwnProperty(obj, k)) {
178
+ obj[k] = v;
179
+ } else if (!Array.isArray(obj[k])) {
180
+ obj[k] = [obj[k], v];
181
+ } else {
182
+ obj[k].push(v);
183
+ }
184
+ });
185
+
186
+ return obj;
187
+ };
@@ -0,0 +1,807 @@
1
+ /*!
2
+ * Casper is a navigation utility for PhantomJS.
3
+ *
4
+ * Documentation: http://casperjs.org/
5
+ * Repository: http://github.com/n1k0/casperjs
6
+ *
7
+ * Copyright (c) 2011-2012 Nicolas Perriault
8
+ *
9
+ * Part of source code is Copyright Joyent, Inc. and other Node contributors.
10
+ *
11
+ * Permission is hereby granted, free of charge, to any person obtaining a
12
+ * copy of this software and associated documentation files (the "Software"),
13
+ * to deal in the Software without restriction, including without limitation
14
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
15
+ * and/or sell copies of the Software, and to permit persons to whom the
16
+ * Software is furnished to do so, subject to the following conditions:
17
+ *
18
+ * The above copyright notice and this permission notice shall be included
19
+ * in all copies or substantial portions of the Software.
20
+ *
21
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
22
+ * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
23
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
24
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
25
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
26
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
27
+ * DEALINGS IN THE SOFTWARE.
28
+ *
29
+ */
30
+
31
+ /*global CasperError exports phantom require*/
32
+
33
+ var fs = require('fs');
34
+ var events = require('events');
35
+ var utils = require('utils');
36
+ var f = utils.format;
37
+
38
+ exports.create = function create(casper, options) {
39
+ "use strict";
40
+ return new Tester(casper, options);
41
+ };
42
+
43
+ /**
44
+ * Casper tester: makes assertions, stores test results and display then.
45
+ *
46
+ * @param Casper casper A valid Casper instance
47
+ * @param Object|null options Options object
48
+ */
49
+ var Tester = function Tester(casper, options) {
50
+ "use strict";
51
+
52
+ if (!utils.isCasperObject(casper)) {
53
+ throw new CasperError("Tester needs a Casper instance");
54
+ }
55
+
56
+ this.currentTestFile = null;
57
+ this.exporter = require('xunit').create();
58
+ this.includes = [];
59
+ this.running = false;
60
+ this.suites = [];
61
+ this.options = utils.mergeObjects({
62
+ failText: "FAIL", // text to use for a successful test
63
+ passText: "PASS", // text to use for a failed test
64
+ pad: 80 // maximum number of chars for a result line
65
+ }, options);
66
+
67
+ // properties
68
+ this.testResults = {
69
+ passed: 0,
70
+ failed: 0,
71
+ passes: [],
72
+ failures: []
73
+ };
74
+
75
+ // events
76
+ casper.on('error', function(msg, backtrace) {
77
+ var line = 0;
78
+ try {
79
+ line = backtrace[0].line;
80
+ } catch (e) {}
81
+ this.test.uncaughtError(msg, this.test.currentTestFile, line);
82
+ this.test.done();
83
+ });
84
+
85
+ casper.on('step.error', function onStepError(e) {
86
+ this.test.uncaughtError(e, this.test.currentTestFile);
87
+ this.test.done();
88
+ });
89
+
90
+ this.on('success', function onSuccess(success) {
91
+ this.testResults.passes.push(success);
92
+ this.exporter.addSuccess(fs.absolute(success.file), success.message || success.standard);
93
+ });
94
+
95
+ this.on('fail', function onFail(failure) {
96
+ // export
97
+ this.exporter.addFailure(
98
+ fs.absolute(failure.file),
99
+ failure.message || failure.standard,
100
+ failure.standard || "test failed",
101
+ failure.type || "unknown"
102
+ );
103
+ this.testResults.failures.push(failure);
104
+ // special printing
105
+ if (failure.type) {
106
+ this.comment(' type: ' + failure.type);
107
+ }
108
+ if (failure.values && Object.keys(failure.values).length > 0) {
109
+ for (var name in failure.values) {
110
+ this.comment(' ' + name + ': ' + utils.serialize(failure.values[name]));
111
+ }
112
+ }
113
+ });
114
+
115
+ // methods
116
+ /**
117
+ * Asserts that a condition strictly resolves to true. Also returns an
118
+ * "assertion object" containing useful informations about the test case
119
+ * results.
120
+ *
121
+ * This method is also used as the base one used for all other `assert*`
122
+ * family methods; supplementary informations are then passed using the
123
+ * `context` argument.
124
+ *
125
+ * @param Boolean subject The condition to test
126
+ * @param String message Test description
127
+ * @param Object|null context Assertion context object (Optional)
128
+ * @return Object An assertion result object
129
+ */
130
+ this.assert = this.assertTrue = function assert(subject, message, context) {
131
+ return this.processAssertionResult(utils.mergeObjects({
132
+ success: subject === true,
133
+ type: "assert",
134
+ standard: "Subject is strictly true",
135
+ message: message,
136
+ file: this.currentTestFile,
137
+ values: {
138
+ subject: utils.getPropertyPath(context, 'values.subject') || subject
139
+ }
140
+ }, context || {}));
141
+ };
142
+
143
+ /**
144
+ * Asserts that two values are strictly equals.
145
+ *
146
+ * @param Mixed subject The value to test
147
+ * @param Mixed expected The expected value
148
+ * @param String message Test description (Optional)
149
+ * @return Object An assertion result object
150
+ */
151
+ this.assertEquals = this.assertEqual = function assertEquals(subject, expected, message) {
152
+ return this.assert(this.testEquals(subject, expected), message, {
153
+ type: "assertEquals",
154
+ standard: "Subject equals the expected value",
155
+ values: {
156
+ subject: subject,
157
+ expected: expected
158
+ }
159
+ });
160
+ };
161
+
162
+ /**
163
+ * Asserts that two values are strictly not equals.
164
+ *
165
+ * @param Mixed subject The value to test
166
+ * @param Mixed expected The unwanted value
167
+ * @param String|null message Test description (Optional)
168
+ * @return Object An assertion result object
169
+ */
170
+ this.assertNotEquals = function assertNotEquals(subject, shouldnt, message) {
171
+ return this.assert(!this.testEquals(subject, shouldnt), message, {
172
+ type: "assertNotEquals",
173
+ standard: "Subject doesn't equal what it shouldn't be",
174
+ values: {
175
+ subject: subject,
176
+ shouldnt: shouldnt
177
+ }
178
+ });
179
+ };
180
+
181
+ /**
182
+ * Asserts that a code evaluation in remote DOM resolves to true.
183
+ *
184
+ * @param Function fn A function to be evaluated in remote DOM
185
+ * @param String message Test description
186
+ * @param Object params Object containing the parameters to inject into the function (optional)
187
+ * @return Object An assertion result object
188
+ */
189
+ this.assertEval = this.assertEvaluate = function assertEval(fn, message, params) {
190
+ return this.assert(casper.evaluate(fn, params), message, {
191
+ type: "assertEval",
192
+ standard: "Evaluated function returns true",
193
+ values: {
194
+ fn: fn,
195
+ params: params
196
+ }
197
+ });
198
+ };
199
+
200
+ /**
201
+ * Asserts that the result of a code evaluation in remote DOM equals
202
+ * an expected value.
203
+ *
204
+ * @param Function fn The function to be evaluated in remote DOM
205
+ * @param Boolean expected The expected value
206
+ * @param String|null message Test description
207
+ * @param Object|null params Object containing the parameters to inject into the function (optional)
208
+ * @return Object An assertion result object
209
+ */
210
+ this.assertEvalEquals = this.assertEvalEqual = function assertEvalEquals(fn, expected, message, params) {
211
+ var subject = casper.evaluate(fn, params);
212
+ return this.assert(this.testEquals(subject, expected), message, {
213
+ type: "assertEvalEquals",
214
+ standard: "Evaluated function returns the expected value",
215
+ values: {
216
+ fn: fn,
217
+ params: params,
218
+ subject: subject,
219
+ expected: expected
220
+ }
221
+ });
222
+ };
223
+
224
+ /**
225
+ * Asserts that an element matching the provided selector expression exists in
226
+ * remote DOM.
227
+ *
228
+ * @param String selector Selector expression
229
+ * @param String message Test description
230
+ * @return Object An assertion result object
231
+ */
232
+ this.assertExists = this.assertExist = this.assertSelectorExists = this.assertSelectorExist = function assertExists(selector, message) {
233
+ return this.assert(casper.exists(selector), message, {
234
+ type: "assertExists",
235
+ standard: f("Found an element matching %s", this.colorize(selector, 'COMMENT')),
236
+ values: {
237
+ selector: selector
238
+ }
239
+ });
240
+ };
241
+
242
+ /**
243
+ * Asserts that an element matching the provided selector expression does not
244
+ * exists in remote DOM.
245
+ *
246
+ * @param String selector Selector expression
247
+ * @param String message Test description
248
+ * @return Object An assertion result object
249
+ */
250
+ this.assertDoesntExist = this.assertNotExists = function assertDoesntExist(selector, message) {
251
+ return this.assert(!casper.exists(selector), message, {
252
+ type: "assertDoesntExist",
253
+ standard: f("No element matching selector %s is found", this.colorize(selector, 'COMMENT')),
254
+ values: {
255
+ selector: selector
256
+ }
257
+ });
258
+ };
259
+
260
+ /**
261
+ * Asserts that current HTTP status is the one passed as argument.
262
+ *
263
+ * @param Number status HTTP status code
264
+ * @param String message Test description
265
+ * @return Object An assertion result object
266
+ */
267
+ this.assertHttpStatus = function assertHttpStatus(status, message) {
268
+ var currentHTTPStatus = casper.currentHTTPStatus;
269
+ return this.assert(this.testEquals(casper.currentHTTPStatus, status), message, {
270
+ type: "assertHttpStatus",
271
+ standard: f("HTTP status code is %s", this.colorize(status, 'COMMENT')),
272
+ values: {
273
+ current: currentHTTPStatus,
274
+ expected: status
275
+ }
276
+ });
277
+ };
278
+
279
+ /**
280
+ * Asserts that a provided string matches a provided RegExp pattern.
281
+ *
282
+ * @param String subject The string to test
283
+ * @param RegExp pattern A RegExp object instance
284
+ * @param String message Test description
285
+ * @return Object An assertion result object
286
+ */
287
+ this.assertMatch = this.assertMatches = function assertMatch(subject, pattern, message) {
288
+ return this.assert(pattern.test(subject), message, {
289
+ type: "assertMatch",
290
+ standard: "Subject matches the provided pattern",
291
+ values: {
292
+ subject: subject,
293
+ pattern: pattern.toString()
294
+ }
295
+ });
296
+ };
297
+
298
+ /**
299
+ * Asserts a condition resolves to false.
300
+ *
301
+ * @param Boolean condition The condition to test
302
+ * @param String message Test description
303
+ * @return Object An assertion result object
304
+ */
305
+ this.assertNot = function assertNot(condition, message) {
306
+ return this.assert(!condition, message, {
307
+ type: "assertNot",
308
+ standard: "Subject is falsy",
309
+ values: {
310
+ condition: condition
311
+ }
312
+ });
313
+ };
314
+
315
+ /**
316
+ * Asserts that the provided function called with the given parameters
317
+ * will raise an exception.
318
+ *
319
+ * @param Function fn The function to test
320
+ * @param Array args The arguments to pass to the function
321
+ * @param String message Test description
322
+ * @return Object An assertion result object
323
+ */
324
+ this.assertRaises = this.assertRaise = this.assertThrows = function assertRaises(fn, args, message) {
325
+ var context = {
326
+ type: "assertRaises",
327
+ standard: "Function raises an error"
328
+ };
329
+ try {
330
+ fn.apply(null, args);
331
+ this.assert(false, message, context);
332
+ } catch (error) {
333
+ this.assert(true, message, utils.mergeObjects(context, {
334
+ values: {
335
+ error: error
336
+ }
337
+ }));
338
+ }
339
+ };
340
+
341
+ /**
342
+ * Asserts that the current page has a resource that matches the provided test
343
+ *
344
+ * @param Function/String test A test function that is called with every response
345
+ * @param String message Test description
346
+ * @return Object An assertion result object
347
+ */
348
+ this.assertResourceExists = this.assertResourceExist = function assertResourceExists(test, message) {
349
+ return this.assert(casper.resourceExists(test), message, {
350
+ type: "assertResourceExists",
351
+ standard: "Expected resource has been found",
352
+ values: {
353
+ test: test
354
+ }
355
+ });
356
+ };
357
+
358
+ /**
359
+ * Asserts that given text exits in the document body.
360
+ *
361
+ * @param String text Text to be found
362
+ * @param String message Test description
363
+ * @return Object An assertion result object
364
+ */
365
+ this.assertTextExists = this.assertTextExist = function assertTextExists(text, message) {
366
+ var textFound = (casper.evaluate(function _evaluate() {
367
+ return document.body.textContent || document.body.innerText;
368
+ }).indexOf(text) !== -1);
369
+ return this.assert(textFound, message, {
370
+ type: "assertTextExists",
371
+ standard: "Found expected text within the document body",
372
+ values: {
373
+ text: text
374
+ }
375
+ });
376
+ };
377
+
378
+ /**
379
+ * Asserts that title of the remote page equals to the expected one.
380
+ *
381
+ * @param String expected The expected title string
382
+ * @param String message Test description
383
+ * @return Object An assertion result object
384
+ */
385
+ this.assertTitle = function assertTitle(expected, message) {
386
+ var currentTitle = casper.getTitle();
387
+ return this.assert(this.testEquals(currentTitle, expected), message, {
388
+ type: "assertTitle",
389
+ standard: f('Page title is "%s"', this.colorize(expected, 'COMMENT')),
390
+ values: {
391
+ subject: currentTitle,
392
+ expected: expected
393
+ }
394
+ });
395
+ };
396
+
397
+ /**
398
+ * Asserts that title of the remote page matched the provided pattern.
399
+ *
400
+ * @param RegExp pattern The pattern to test the title against
401
+ * @param String message Test description
402
+ * @return Object An assertion result object
403
+ */
404
+ this.assertTitleMatch = this.assertTitleMatches = function assertTitleMatch(pattern, message) {
405
+ var currentTitle = casper.getTitle();
406
+ return this.assert(pattern.test(currentTitle), message, {
407
+ type: "assertTitle",
408
+ details: "Page title does not match the provided pattern",
409
+ values: {
410
+ subject: currentTitle,
411
+ pattern: pattern.toString()
412
+ }
413
+ });
414
+ };
415
+
416
+ /**
417
+ * Asserts that the provided subject is of the given type.
418
+ *
419
+ * @param mixed subject The value to test
420
+ * @param String type The javascript type name
421
+ * @param String message Test description
422
+ * @return Object An assertion result object
423
+ */
424
+ this.assertType = function assertType(subject, type, message) {
425
+ var actual = utils.betterTypeOf(subject);
426
+ return this.assert(this.testEquals(actual, type), message, {
427
+ type: "assertType",
428
+ standard: f('Subject type is "%s"', this.colorize(type, 'COMMENT')),
429
+ values: {
430
+ subject: subject,
431
+ type: type,
432
+ actual: actual
433
+ }
434
+ });
435
+ };
436
+
437
+ /**
438
+ * Asserts that a the current page url matches the provided RegExp
439
+ * pattern.
440
+ *
441
+ * @param RegExp pattern A RegExp object instance
442
+ * @param String message Test description
443
+ * @return Object An assertion result object
444
+ */
445
+ this.assertUrlMatch = this.assertUrlMatches = function assertUrlMatch(pattern, message) {
446
+ var currentUrl = casper.getCurrentUrl();
447
+ return this.assert(pattern.test(currentUrl), message, {
448
+ type: "assertUrlMatch",
449
+ standard: "Current url matches the provided pattern",
450
+ values: {
451
+ currentUrl: currentUrl,
452
+ pattern: pattern.toString()
453
+ }
454
+ });
455
+ };
456
+
457
+ /**
458
+ * Prints out a colored bar onto the console.
459
+ *
460
+ */
461
+ this.bar = function bar(text, style) {
462
+ casper.echo(text, style, this.options.pad);
463
+ };
464
+
465
+ /**
466
+ * Render a colorized output. Basically a proxy method for
467
+ * Casper.Colorizer#colorize()
468
+ */
469
+ this.colorize = function colorize(message, style) {
470
+ return casper.getColorizer().colorize(message, style);
471
+ };
472
+
473
+ /**
474
+ * Writes a comment-style formatted message to stdout.
475
+ *
476
+ * @param String message
477
+ */
478
+ this.comment = function comment(message) {
479
+ casper.echo('# ' + message, 'COMMENT');
480
+ };
481
+
482
+ /**
483
+ * Declares the current test suite done.
484
+ *
485
+ */
486
+ this.done = function done() {
487
+ this.running = false;
488
+ };
489
+
490
+ /**
491
+ * Writes an error-style formatted message to stdout.
492
+ *
493
+ * @param String message
494
+ */
495
+ this.error = function error(message) {
496
+ casper.echo(message, 'ERROR');
497
+ };
498
+
499
+ /**
500
+ * Executes a file, wraping and evaluating its code in an isolated
501
+ * environment where only the current `casper` instance is passed.
502
+ *
503
+ * @param String file Absolute path to some js/coffee file
504
+ */
505
+ this.exec = function exec(file) {
506
+ file = this.filter('exec.file', file) || file;
507
+ if (!fs.isFile(file) || !utils.isJsFile(file)) {
508
+ var e = new CasperError(f("Cannot exec %s: can only exec() files with .js or .coffee extensions", file));
509
+ e.fileName = file;
510
+ throw e;
511
+ }
512
+ this.currentTestFile = file;
513
+ phantom.injectJs(file);
514
+ };
515
+
516
+ /**
517
+ * Adds a failed test entry to the stack.
518
+ *
519
+ * @param String message
520
+ */
521
+ this.fail = function fail(message) {
522
+ return this.assert(false, message, {
523
+ type: "fail",
524
+ standard: "explicit call to fail()"
525
+ });
526
+ };
527
+
528
+ /**
529
+ * Recursively finds all test files contained in a given directory.
530
+ *
531
+ * @param String dir Path to some directory to scan
532
+ */
533
+ this.findTestFiles = function findTestFiles(dir) {
534
+ var self = this;
535
+ if (!fs.isDirectory(dir)) {
536
+ return [];
537
+ }
538
+ var entries = fs.list(dir).filter(function _filter(entry) {
539
+ return entry !== '.' && entry !== '..';
540
+ }).map(function _map(entry) {
541
+ return fs.absolute(fs.pathJoin(dir, entry));
542
+ });
543
+ entries.forEach(function _forEach(entry) {
544
+ if (fs.isDirectory(entry)) {
545
+ entries = entries.concat(self.findTestFiles(entry));
546
+ }
547
+ });
548
+ return entries.filter(function _filter(entry) {
549
+ return utils.isJsFile(fs.absolute(fs.pathJoin(dir, entry)));
550
+ }).sort();
551
+ };
552
+
553
+ /**
554
+ * Formats a message to highlight some parts of it.
555
+ *
556
+ * @param String message
557
+ * @param String style
558
+ */
559
+ this.formatMessage = function formatMessage(message, style) {
560
+ var parts = /^([a-z0-9_\.]+\(\))(.*)/i.exec(message);
561
+ if (!parts) {
562
+ return message;
563
+ }
564
+ return this.colorize(parts[1], 'PARAMETER') + this.colorize(parts[2], style);
565
+ };
566
+
567
+ /**
568
+ * Retrieves current failure data and all failed cases.
569
+ *
570
+ * @return Object casedata An object containg information about cases
571
+ * @return Number casedata.length The number of failed cases
572
+ * @return Array casedata.cases An array of all the failed case objects
573
+ */
574
+ this.getFailures = function getFailures() {
575
+ return {
576
+ length: this.testResults.failed,
577
+ cases: this.testResults.failures
578
+ };
579
+ };
580
+
581
+ /**
582
+ * Retrieves current passed data and all passed cases.
583
+ *
584
+ * @return Object casedata An object containg information about cases
585
+ * @return Number casedata.length The number of passed cases
586
+ * @return Array casedata.cases An array of all the passed case objects
587
+ */
588
+ this.getPasses = function getPasses() {
589
+ return {
590
+ length: this.testResults.passed,
591
+ cases: this.testResults.passes
592
+ };
593
+ };
594
+
595
+ /**
596
+ * Writes an info-style formatted message to stdout.
597
+ *
598
+ * @param String message
599
+ */
600
+ this.info = function info(message) {
601
+ casper.echo(message, 'PARAMETER');
602
+ };
603
+
604
+ /**
605
+ * Adds a successful test entry to the stack.
606
+ *
607
+ * @param String message
608
+ */
609
+ this.pass = function pass(message) {
610
+ return this.assert(true, message, {
611
+ type: "pass",
612
+ standard: "explicit call to pass()"
613
+ });
614
+ };
615
+
616
+ /**
617
+ * Processes an assertion result by emitting the appropriate event and
618
+ * printing result onto the console.
619
+ *
620
+ * @param Object result An assertion result object
621
+ * @return Object The passed assertion result Object
622
+ */
623
+ this.processAssertionResult = function processAssertionResult(result) {
624
+ var eventName, style, status;
625
+ if (result.success === true) {
626
+ eventName = 'success';
627
+ style = 'INFO';
628
+ status = this.options.passText;
629
+ this.testResults.passed++;
630
+ } else {
631
+ eventName = 'fail';
632
+ style = 'RED_BAR';
633
+ status = this.options.failText;
634
+ this.testResults.failed++;
635
+ }
636
+ var message = result.message || result.standard;
637
+ casper.echo([this.colorize(status, style), this.formatMessage(message)].join(' '));
638
+ this.emit(eventName, result);
639
+ return result;
640
+ };
641
+
642
+ /**
643
+ * Renders a detailed report for each failed test.
644
+ *
645
+ * @param Array failures
646
+ */
647
+ this.renderFailureDetails = function renderFailureDetails(failures) {
648
+ if (failures.length === 0) {
649
+ return;
650
+ }
651
+ casper.echo(f("\nDetails for the %d failed test%s:\n", failures.length, failures.length > 1 ? "s" : ""), "PARAMETER");
652
+ failures.forEach(function _forEach(failure) {
653
+ var type, message, line;
654
+ type = failure.type || "unknown";
655
+ line = ~~failure.line;
656
+ message = failure.message;
657
+ casper.echo(f('In %s:%s', failure.file, line));
658
+ casper.echo(f(' %s: %s', type, message || failure.standard || "(no message was entered)"), "COMMENT");
659
+ });
660
+ };
661
+
662
+ /**
663
+ * Render tests results, an optionnaly exit phantomjs.
664
+ *
665
+ * @param Boolean exit
666
+ */
667
+ this.renderResults = function renderResults(exit, status, save) {
668
+ save = utils.isString(save) ? save : this.options.save;
669
+ var total = this.testResults.passed + this.testResults.failed, statusText, style, result;
670
+ var exitStatus = ~~(status || (this.testResults.failed > 0 ? 1 : 0));
671
+ if (total === 0) {
672
+ statusText = this.options.failText;
673
+ style = 'RED_BAR';
674
+ result = f("%s Looks like you didn't run any test.", statusText);
675
+ } else {
676
+ if (this.testResults.failed > 0) {
677
+ statusText = this.options.failText;
678
+ style = 'RED_BAR';
679
+ } else {
680
+ statusText = this.options.passText;
681
+ style = 'GREEN_BAR';
682
+ }
683
+ result = f('%s %s tests executed, %d passed, %d failed.',
684
+ statusText, total, this.testResults.passed, this.testResults.failed);
685
+ }
686
+ casper.echo(result, style, this.options.pad);
687
+ if (this.testResults.failed > 0) {
688
+ this.renderFailureDetails(this.testResults.failures);
689
+ }
690
+ if (save && utils.isFunction(require)) {
691
+ try {
692
+ fs.write(save, this.exporter.getXML(), 'w');
693
+ casper.echo(f('Result log stored in %s', save), 'INFO', 80);
694
+ } catch (e) {
695
+ casper.echo(f('Unable to write results to %s: %s', save, e), 'ERROR', 80);
696
+ }
697
+ }
698
+ if (exit === true) {
699
+ casper.exit(exitStatus);
700
+ }
701
+ };
702
+
703
+ /**
704
+ * Runs al suites contained in the paths passed as arguments.
705
+ *
706
+ */
707
+ this.runSuites = function runSuites() {
708
+ var testFiles = [], self = this;
709
+ if (arguments.length === 0) {
710
+ throw new CasperError("runSuites() needs at least one path argument");
711
+ }
712
+ Array.prototype.forEach.call(arguments, function _forEach(path) {
713
+ if (!fs.exists(path)) {
714
+ self.bar(f("Path %s doesn't exist", path), "RED_BAR");
715
+ }
716
+ if (fs.isDirectory(path)) {
717
+ testFiles = testFiles.concat(self.findTestFiles(path));
718
+ } else if (fs.isFile(path)) {
719
+ testFiles.push(path);
720
+ }
721
+ });
722
+ if (testFiles.length === 0) {
723
+ this.bar(f("No test file found in %s, aborting.", Array.prototype.slice.call(arguments)), "RED_BAR");
724
+ casper.exit(1);
725
+ }
726
+ var current = 0;
727
+ var interval = setInterval(function _check(self) {
728
+ if (self.running) {
729
+ return;
730
+ }
731
+ if (current === testFiles.length) {
732
+ self.emit('tests.complete');
733
+ clearInterval(interval);
734
+ } else {
735
+ self.runTest(testFiles[current]);
736
+ current++;
737
+ }
738
+ }, 100, this);
739
+ };
740
+
741
+ /**
742
+ * Runs a test file
743
+ *
744
+ */
745
+ this.runTest = function runTest(testFile) {
746
+ this.bar(f('Test file: %s', testFile), 'INFO_BAR');
747
+ this.running = true; // this.running is set back to false with done()
748
+ this.includes.forEach(function(include) {
749
+ phantom.injectJs(include);
750
+ });
751
+ this.exec(testFile);
752
+ };
753
+
754
+ /**
755
+ * Tests equality between the two passed arguments.
756
+ *
757
+ * @param Mixed v1
758
+ * @param Mixed v2
759
+ * @param Boolean
760
+ */
761
+ this.testEquals = this.testEqual = function testEquals(v1, v2) {
762
+ if (utils.betterTypeOf(v1) !== utils.betterTypeOf(v2)) {
763
+ return false;
764
+ }
765
+ if (utils.isFunction(v1)) {
766
+ return v1.toString() === v2.toString();
767
+ }
768
+ if (v1 instanceof Object && v2 instanceof Object) {
769
+ if (Object.keys(v1).length !== Object.keys(v2).length) {
770
+ return false;
771
+ }
772
+ for (var k in v1) {
773
+ if (!this.testEquals(v1[k], v2[k])) {
774
+ return false;
775
+ }
776
+ }
777
+ return true;
778
+ }
779
+ return v1 === v2;
780
+ };
781
+
782
+ /**
783
+ * Processes an error caught while running tests contained in a given test
784
+ * file.
785
+ *
786
+ * @param Error|String error The error
787
+ * @param String file Test file where the error occured
788
+ * @param Number line Line number (optional)
789
+ */
790
+ this.uncaughtError = function uncaughtError(error, file, line) {
791
+ return this.processAssertionResult({
792
+ success: false,
793
+ type: "uncaughtError",
794
+ file: file,
795
+ line: ~~line || "unknown",
796
+ message: utils.isObject(error) ? error.message : error,
797
+ values: {
798
+ error: error
799
+ }
800
+ });
801
+ };
802
+ };
803
+
804
+ // Tester class is an EventEmitter
805
+ utils.inherits(Tester, events.EventEmitter);
806
+
807
+ exports.Tester = Tester;