Commit 32f0d2cb 32f0d2cba040895b557ccb67ffb47a0d051c3f0d by Nicolas Perriault

Casper.open() can now perform POST, PUT, HEAD and DELETE HTTP requests

1 parent 375bf0b4
...@@ -32,6 +32,7 @@ var colorizer = require('colorizer'); ...@@ -32,6 +32,7 @@ var colorizer = require('colorizer');
32 var events = require('events'); 32 var events = require('events');
33 var fs = require('fs'); 33 var fs = require('fs');
34 var mouse = require('mouse'); 34 var mouse = require('mouse');
35 var qs = require('querystring');
35 var tester = require('tester'); 36 var tester = require('tester');
36 var utils = require('utils'); 37 var utils = require('utils');
37 var f = utils.format; 38 var f = utils.format;
...@@ -645,27 +646,58 @@ Casper.prototype.mouseClick = function(selector) { ...@@ -645,27 +646,58 @@ Casper.prototype.mouseClick = function(selector) {
645 }; 646 };
646 647
647 /** 648 /**
648 * Opens a page. Takes only one argument, the url to open (using the 649 * Performs an HTTP request.
649 * callback argument would defeat the whole purpose of Casper
650 * actually).
651 * 650 *
652 * @param String location The url to open 651 * @param String location The url to open
652 * @param Object settings The request settings
653 * @return Casper 653 * @return Casper
654 */ 654 */
655 Casper.prototype.open = function(location, options) { 655 Casper.prototype.open = function(location, settings) {
656 options = utils.isObject(options) ? options : {}; 656 var self = this;
657 this.requestUrl = location; 657 // settings validation
658 // http auth 658 if (!settings) {
659 var httpAuthMatch = location.match(/^https?:\/\/(.+):(.+)@/i); 659 settings = {
660 if (httpAuthMatch) { 660 method: "get"
661 var httpAuth = {
662 username: httpAuthMatch[1],
663 password: httpAuthMatch[2]
664 }; 661 };
665 this.setHttpAuth(httpAuth.username, httpAuth.password);
666 } 662 }
667 this.emit('open', location); 663 if (!utils.isObject(settings)) {
668 this.page.open(this.filter('open.location', location) || location); 664 throw new Error("open(): request settings must be an Object");
665 }
666 // http method
667 // taken from https://github.com/ariya/phantomjs/blob/master/src/webpage.cpp#L302
668 var methods = ["get", "head", "put", "post", "delete"];
669 if (settings.method && (!utils.isString(settings.method) || methods.indexOf(settings.method) === -1)) {
670 throw new Error("open(): settings.method must be part of " + methods.join(', '));
671 }
672 // http data
673 if (settings.data) {
674 if (utils.isObject(settings.data)) { // query object
675 settings.data = qs.encode(settings.data);
676 } else if (!utils.isString(settings.data)) {
677 throw new Error("open(): invalid request settings data value: " + settings.data);
678 }
679 }
680 // current request url
681 this.requestUrl = this.filter('open.location', location) || location;
682 // http auth
683 if (settings.username && settings.password) {
684 this.setHttpAuth(settings.username, settings.password);
685 } else {
686 var httpAuthMatch = location.match(/^https?:\/\/(.+):(.+)@/i);
687 if (httpAuthMatch) {
688 var httpAuth = {
689 username: httpAuthMatch[1],
690 password: httpAuthMatch[2]
691 };
692 this.setHttpAuth(httpAuth.username, httpAuth.password);
693 }
694 }
695 this.emit('open', this.requestUrl, settings);
696 //this.page.open(this.requestUrl, settings.method, settings.data);
697 this.page.openUrl(this.requestUrl, {
698 operation: settings.method,
699 data: settings.data
700 }, this.page.settings);
669 return this; 701 return this;
670 }; 702 };
671 703
...@@ -741,7 +773,7 @@ Casper.prototype.runStep = function(step) { ...@@ -741,7 +773,7 @@ Casper.prototype.runStep = function(step) {
741 var stepInfo = f("Step %d/%d", this.step, this.steps.length); 773 var stepInfo = f("Step %d/%d", this.step, this.steps.length);
742 var stepResult; 774 var stepResult;
743 if (!skipLog) { 775 if (!skipLog) {
744 this.log(stepInfo + f('%s (HTTP %d)', this.getCurrentUrl(), this.currentHTTPStatus), "info"); 776 this.log(stepInfo + f(' %s (HTTP %d)', this.getCurrentUrl(), this.currentHTTPStatus), "info");
745 } 777 }
746 if (utils.isNumber(this.options.stepTimeout) && this.options.stepTimeout > 0) { 778 if (utils.isNumber(this.options.stepTimeout) && this.options.stepTimeout > 0) {
747 var stepTimeoutCheckInterval = setInterval(function(self, start, stepNum) { 779 var stepTimeoutCheckInterval = setInterval(function(self, start, stepNum) {
...@@ -1218,16 +1250,15 @@ function createPage(casper) { ...@@ -1218,16 +1250,15 @@ function createPage(casper) {
1218 } 1250 }
1219 if (casper.options.clientScripts) { 1251 if (casper.options.clientScripts) {
1220 if (!utils.isArray(casper.options.clientScripts)) { 1252 if (!utils.isArray(casper.options.clientScripts)) {
1221 casper.log("The clientScripts option must be an array", "error"); 1253 throw new Error("The clientScripts option must be an array");
1222 } else { 1254 } else {
1223 for (var i = 0; i < casper.options.clientScripts.length; i++) { 1255 casper.options.clientScripts.forEach(function(script) {
1224 var script = casper.options.clientScripts[i];
1225 if (casper.page.injectJs(script)) { 1256 if (casper.page.injectJs(script)) {
1226 casper.log(f('Automatically injected %s client side', script), "debug"); 1257 casper.log(f('Automatically injected %s client side', script), "debug");
1227 } else { 1258 } else {
1228 casper.log(f('Failed injecting %s client side', script), "warning"); 1259 casper.log(f('Failed injecting %s client side', script), "warning");
1229 } 1260 }
1230 } 1261 });
1231 } 1262 }
1232 } 1263 }
1233 // Client-side utils injection 1264 // Client-side utils injection
......
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 };
...@@ -14,4 +14,4 @@ ...@@ -14,4 +14,4 @@
14 <li>three</li> 14 <li>three</li>
15 </ul> 15 </ul>
16 </body> 16 </body>
17 </html>
...\ No newline at end of file ...\ No newline at end of file
17 </html>
......
...@@ -5,7 +5,7 @@ ...@@ -5,7 +5,7 @@
5 t.assertEquals(self.fetchText('ul li'), 'onetwothree', 'Casper.fetchText() can retrieve text contents'); 5 t.assertEquals(self.fetchText('ul li'), 'onetwothree', 'Casper.fetchText() can retrieve text contents');
6 }); 6 });
7 7
8 casper.run(function(self) { 8 casper.run(function() {
9 t.done(); 9 t.done();
10 }); 10 });
11 })(casper.test); 11 })(casper.test);
......
...@@ -31,7 +31,7 @@ ...@@ -31,7 +31,7 @@
31 self.options.onAlert = function(self, message) { 31 self.options.onAlert = function(self, message) {
32 t.assertEquals(message, 'plop', 'Casper.options.onAlert() can intercept an alert message'); 32 t.assertEquals(message, 'plop', 'Casper.options.onAlert() can intercept an alert message');
33 }; 33 };
34 }).thenOpen('tests/site/alert.html').click('button', function(self) { 34 }).thenOpen('tests/site/alert.html').thenClick('button', function(self) {
35 self.options.onAlert = null; 35 self.options.onAlert = null;
36 }); 36 });
37 37
......
1 (function(t) {
2 var current = 0, tests = [
3 function(settings) {
4 t.assertEquals(settings, {
5 method: "get"
6 }, "Casper.open() used the expected GET settings");
7 },
8 function(settings) {
9 t.assertEquals(settings, {
10 method: "post",
11 data: "plop=42&chuck=norris"
12 }, "Casper.open() used the expected POST settings");
13 },
14 function(settings) {
15 t.assertEquals(settings, {
16 method: "put",
17 data: "plop=42&chuck=norris"
18 }, "Casper.open() used the expected PUT settings");
19 },
20 function(settings) {
21 t.assertEquals(settings, {
22 method: "get",
23 username: 'bob',
24 password: 'sinclar'
25 }, "Casper.open() used the expected HTTP auth settings");
26 }
27 ];
28
29 casper.start();
30
31 casper.on('open', function(url, settings) {
32 tests[current++](settings);
33 });
34
35 // GET
36 casper.open('tests/site/index.html').then(function() {
37 t.pass("Casper.open() can open and load a location using GET");
38 });
39
40 // POST
41 casper.open('tests/site/index.html', {
42 method: 'post',
43 data: {
44 plop: 42,
45 chuck: 'norris'
46 }
47 }).then(function() {
48 t.pass("Casper.open() can open and load a location using POST");
49 });
50
51 // PUT
52 casper.open('tests/site/index.html', {
53 method: 'put',
54 data: {
55 plop: 42,
56 chuck: 'norris'
57 }
58 }).then(function() {
59 t.pass("Casper.open() can open and load a location using PUT");
60 });
61
62 // HTTP Auth
63 casper.open('tests/site/index.html', {
64 method: 'get',
65 username: 'bob',
66 password: 'sinclar'
67 }).then(function() {
68 t.pass("Casper.open() can open and load a location using HTTP auth");
69 });
70
71 casper.run(function() {
72 this.removeAllListeners('open');
73 t.done();
74 });
75 })(casper.test);
...@@ -28,7 +28,7 @@ ...@@ -28,7 +28,7 @@
28 self.test.assertEquals(i, item - 1, 'Casper.each() passes a contextualized index'); 28 self.test.assertEquals(i, item - 1, 'Casper.each() passes a contextualized index');
29 }); 29 });
30 30
31 casper.run(function(self) { 31 casper.run(function() {
32 t.done(); 32 t.done();
33 }); 33 });
34 })(casper.test); 34 })(casper.test);
......