Commit 90afad2b 90afad2b87dc605e350a3e8537d94c4787f78ab3 by Nicolas Perriault

Implemented a better alternative for retrieving contextualized parameters in Casper#evaluate()

Now as an alternative, CasperJS injects a `__casper_params__` Object instance containing all the parameters you passed:

    casper.evaluate(function() {
        document.querySelector('#username').value = __casper_params__.username;
        document.querySelector('#password').value = __casper_params__.password;
        document.querySelector('#submit').click();
    }, {
        username: 'Bazoonga',
        password: 'baz00nga'
    })
1 parent ae7d5b78
...@@ -97,7 +97,7 @@ ...@@ -97,7 +97,7 @@
97 */ 97 */
98 base64encode: function(url) { 98 base64encode: function(url) {
99 return this.evaluate(function() { 99 return this.evaluate(function() {
100 return __utils__.getBase64('%url%'); 100 return __utils__.getBase64(__casper_params__.url);
101 }, { 101 }, {
102 url: url 102 url: url
103 }); 103 });
...@@ -134,18 +134,18 @@ ...@@ -134,18 +134,18 @@
134 captureSelector: function(targetFile, selector) { 134 captureSelector: function(targetFile, selector) {
135 return this.capture(targetFile, this.evaluate(function() { 135 return this.capture(targetFile, this.evaluate(function() {
136 try { 136 try {
137 var clipRect = document.querySelector('%selector%').getBoundingClientRect(); 137 var clipRect = document.querySelector(__casper_params__.selector).getBoundingClientRect();
138 return { 138 return {
139 top: clipRect.top, 139 top: clipRect.top,
140 left: clipRect.left, 140 left: clipRect.left,
141 width: clipRect.width, 141 width: clipRect.width,
142 height: clipRect.height 142 height: clipRect.height
143 }; 143 };
144 } catch (e) { 144 } catch (e) {
145 __utils__.log('unable to fetch bounds for element ' + selector, 'warning'); 145 __utils__.log("Unable to fetch bounds for element " + __casper_params__.selector, "warning");
146 } 146 }
147 }, { 147 }, {
148 selector: selector.replace("'", "\'") 148 selector: selector
149 })); 149 }));
150 }, 150 },
151 151
...@@ -197,15 +197,18 @@ ...@@ -197,15 +197,18 @@
197 * Emulates a click on the element from the provided selector, if 197 * Emulates a click on the element from the provided selector, if
198 * possible. In case of success, `true` is returned. 198 * possible. In case of success, `true` is returned.
199 * 199 *
200 * @param String selector A DOM CSS3 compatible selector 200 * @param String selector A DOM CSS3 compatible selector
201 * @param Boolean fallbackToHref Whether to try to relocate to the value of any href attribute (default: true)
201 * @return Boolean 202 * @return Boolean
202 */ 203 */
203 click: function(selector) { 204 click: function(selector, fallbackToHref) {
205 fallbackToHref = typeof(fallbackToHref) == "undefined" ? true : false;
204 this.log("click on selector: " + selector, "debug"); 206 this.log("click on selector: " + selector, "debug");
205 return this.evaluate(function() { 207 return this.evaluate(function() {
206 return __utils__.click('%selector%'); 208 return __utils__.click(__casper_params__.selector, __casper_params__.fallbackToHref);
207 }, { 209 }, {
208 selector: selector.replace("'", "\'") 210 selector: selector,
211 fallbackToHref: fallbackToHref
209 }); 212 });
210 }, 213 },
211 214
...@@ -297,16 +300,40 @@ ...@@ -297,16 +300,40 @@
297 * password: 'baz00nga' 300 * password: 'baz00nga'
298 * }) 301 * })
299 * 302 *
303 * As an alternative, CasperJS injects a `__casper_params__` Object
304 * instance containing all the parameters you passed:
305 *
306 * casper.evaluate(function() {
307 * document.querySelector('#username').value = __casper_params__.username;
308 * document.querySelector('#password').value = __casper_params__.password;
309 * document.querySelector('#submit').click();
310 * }, {
311 * username: 'Bazoonga',
312 * password: 'baz00nga'
313 * })
314 *
300 * FIXME: waiting for a patch of PhantomJS to allow direct passing of 315 * FIXME: waiting for a patch of PhantomJS to allow direct passing of
301 * arguments to the function. 316 * arguments to the function.
302 * TODO: don't forget to keep this backward compatible. 317 * TODO: don't forget to keep this backward compatible.
303 * 318 *
304 * @param function fn The function to be evaluated within current page DOM 319 * @param function fn The function to be evaluated within current page DOM
305 * @param object replacements Optional replacements to performs, eg. for '%foo%' => {foo: 'bar'} 320 * @param object replacements Parameters to pass to the remote environment
306 * @return mixed 321 * @return mixed
307 * @see WebPage#evaluate 322 * @see WebPage#evaluate
308 */ 323 */
309 evaluate: function(fn, replacements) { 324 evaluate: function(fn, replacements) {
325 replacements = typeof(replacements) === "object" ? replacements : {};
326 this.page.evaluate(replaceFunctionPlaceholders(function() {
327 window.__casper_params__ = {};
328 try {
329 var jsonString = unescape(decodeURIComponent('%replacements%'));
330 window.__casper_params__ = JSON.parse(jsonString);
331 } catch (e) {
332 __utils__.log("Unable to replace parameters: " + e, "error");
333 }
334 }, {
335 replacements: encodeURIComponent(escape(JSON.stringify(replacements).replace("'", "\'")))
336 }));
310 return this.page.evaluate(replaceFunctionPlaceholders(fn, replacements)); 337 return this.page.evaluate(replaceFunctionPlaceholders(fn, replacements));
311 }, 338 },
312 339
...@@ -314,8 +341,8 @@ ...@@ -314,8 +341,8 @@
314 * Evaluates an expression within the current page DOM and die() if it 341 * Evaluates an expression within the current page DOM and die() if it
315 * returns false. 342 * returns false.
316 * 343 *
317 * @param function fn Expression to evaluate 344 * @param function fn The expression to evaluate
318 * @param String message Error message to log 345 * @param String message The error message to log
319 * @return Casper 346 * @return Casper
320 */ 347 */
321 evaluateOrDie: function(fn, message) { 348 evaluateOrDie: function(fn, message) {
...@@ -334,7 +361,7 @@ ...@@ -334,7 +361,7 @@
334 */ 361 */
335 exists: function(selector) { 362 exists: function(selector) {
336 return this.evaluate(function() { 363 return this.evaluate(function() {
337 return __utils__.exists('%selector%'); 364 return __utils__.exists(__casper_params__.selector);
338 }, { 365 }, {
339 selector: selector 366 selector: selector
340 }); 367 });
...@@ -360,9 +387,9 @@ ...@@ -360,9 +387,9 @@
360 */ 387 */
361 fetchText: function(selector) { 388 fetchText: function(selector) {
362 return this.evaluate(function() { 389 return this.evaluate(function() {
363 return __utils__.fetchText('%selector%'); 390 return __utils__.fetchText(__casper_params__.selector);
364 }, { 391 }, {
365 selector: selector.replace("'", "\'") 392 selector: selector
366 }); 393 });
367 }, 394 },
368 395
...@@ -382,10 +409,10 @@ ...@@ -382,10 +409,10 @@
382 throw "form values must be provided as an object"; 409 throw "form values must be provided as an object";
383 } 410 }
384 var fillResults = this.evaluate(function() { 411 var fillResults = this.evaluate(function() {
385 return __utils__.fill('%selector%', JSON.parse('%values%')); 412 return __utils__.fill(__casper_params__.selector, __casper_params__.values);
386 }, { 413 }, {
387 selector: selector.replace("'", "\'"), 414 selector: selector,
388 values: JSON.stringify(vals).replace("'", "\'") 415 values: vals
389 }); 416 });
390 if (!fillResults) { 417 if (!fillResults) {
391 throw "unable to fill form"; 418 throw "unable to fill form";
...@@ -412,13 +439,13 @@ ...@@ -412,13 +439,13 @@
412 // Form submission? 439 // Form submission?
413 if (submit) { 440 if (submit) {
414 this.evaluate(function() { 441 this.evaluate(function() {
415 var form = document.querySelector('%selector%'); 442 var form = document.querySelector(__casper_params__.selector);
416 var method = form.getAttribute('method').toUpperCase() || "GET"; 443 var method = form.getAttribute('method').toUpperCase() || "GET";
417 var action = form.getAttribute('action') || "unknown"; 444 var action = form.getAttribute('action') || "unknown";
418 __utils__.log('submitting form to ' + action + ', HTTP ' + method, 'info'); 445 __utils__.log('submitting form to ' + action + ', HTTP ' + method, 'info');
419 form.submit(); 446 form.submit();
420 }, { 447 }, {
421 selector: selector.replace("'", "\'") 448 selector: selector
422 }); 449 });
423 } 450 }
424 }, 451 },
...@@ -634,8 +661,8 @@ ...@@ -634,8 +661,8 @@
634 }, 661 },
635 662
636 /** 663 /**
637 * Waits a given amount of time (expressed in milliseconds) before 664 * Adds a new step that will wait for a given amount of time (expressed
638 * processing next step, which can passed as an optional argument. 665 * in milliseconds) before processing an optional next one.
639 * 666 *
640 * @param Number timeout The max amount of time to wait, in milliseconds 667 * @param Number timeout The max amount of time to wait, in milliseconds
641 * @param Function then Next step to process (optional) 668 * @param Function then Next step to process (optional)
...@@ -648,19 +675,20 @@ ...@@ -648,19 +675,20 @@
648 if (then && typeof(then) !== "function") { 675 if (then && typeof(then) !== "function") {
649 this.die("wait() a step definition must be a function"); 676 this.die("wait() a step definition must be a function");
650 } 677 }
651 this.delayedExecution = true; 678 return this.then(function(self) {
652 var start = new Date().getTime(); 679 self.delayedExecution = true;
653 var interval = setInterval(function(self, then) { 680 var start = new Date().getTime();
654 if (new Date().getTime() - start > timeout) { 681 var interval = setInterval(function(self, then) {
655 self.delayedExecution = false; 682 if (new Date().getTime() - start > timeout) {
656 self.log("wait() finished wating for " + timeout + "ms.", "info"); 683 self.delayedExecution = false;
657 if (then) { 684 self.log("wait() finished wating for " + timeout + "ms.", "info");
658 self.then(then); 685 if (then) {
686 self.then(then);
687 }
688 clearInterval(interval);
659 } 689 }
660 clearInterval(interval); 690 }, 100, self, then);
661 } 691 });
662 }, 100, this, then);
663 return this;
664 }, 692 },
665 693
666 /** 694 /**
...@@ -745,10 +773,12 @@ ...@@ -745,10 +773,12 @@
745 /** 773 /**
746 * Clicks on the DOM element behind the provided selector. 774 * Clicks on the DOM element behind the provided selector.
747 * 775 *
748 * @param String selector A CSS3 selector to the element to click 776 * @param String selector A CSS3 selector to the element to click
777 * @param Boolean fallbackToHref Whether to try to relocate to the value of any href attribute (default: true)
749 * @return Boolean 778 * @return Boolean
750 */ 779 */
751 this.click = function(selector) { 780 this.click = function(selector, fallbackToHref) {
781 fallbackToHref = typeof(fallbackToHref) == "undefined" ? true : false;
752 var elem = this.findOne(selector); 782 var elem = this.findOne(selector);
753 if (!elem) { 783 if (!elem) {
754 return false; 784 return false;
...@@ -758,7 +788,7 @@ ...@@ -758,7 +788,7 @@
758 if (elem.dispatchEvent(evt)) { 788 if (elem.dispatchEvent(evt)) {
759 return true; 789 return true;
760 } 790 }
761 if (elem.hasAttribute('href')) { 791 if (fallbackToHref && elem.hasAttribute('href')) {
762 document.location = elem.getAttribute('href'); 792 document.location = elem.getAttribute('href');
763 return true; 793 return true;
764 } 794 }
...@@ -1226,6 +1256,17 @@ ...@@ -1226,6 +1256,17 @@
1226 }; 1256 };
1227 1257
1228 /** 1258 /**
1259 * Asserts that the provided input is of the given type.
1260 *
1261 * @param mixed input The value to test
1262 * @param String type The javascript type name
1263 * @param String message Test description
1264 */
1265 this.assertType = function(input, type, message) {
1266 return this.assertEquals(betterTypeOf(input), type, message);
1267 };
1268
1269 /**
1229 * Asserts that a the current page url matches the provided RegExp 1270 * Asserts that a the current page url matches the provided RegExp
1230 * pattern. 1271 * pattern.
1231 * 1272 *
...@@ -1403,6 +1444,21 @@ ...@@ -1403,6 +1444,21 @@
1403 }; 1444 };
1404 1445
1405 /** 1446 /**
1447 * Provides a better typeof operator equivalent
1448 *
1449 * @param mixed input
1450 * @return String
1451 * @see http://javascriptweblog.wordpress.com/2011/08/08/fixing-the-javascript-typeof-operator/
1452 */
1453 function betterTypeOf(input) {
1454 try {
1455 return Object.prototype.toString.call(input).match(/^\[object\s(.*)\]$/)[1].toLowerCase();
1456 } catch (e) {
1457 return typeof input;
1458 }
1459 }
1460
1461 /**
1406 * Creates a new WebPage instance for Casper use. 1462 * Creates a new WebPage instance for Casper use.
1407 * 1463 *
1408 * @param Casper casper A Casper instance 1464 * @param Casper casper A Casper instance
...@@ -1522,10 +1578,10 @@ ...@@ -1522,10 +1578,10 @@
1522 function replaceFunctionPlaceholders(fn, replacements) { 1578 function replaceFunctionPlaceholders(fn, replacements) {
1523 if (replacements && typeof replacements === "object") { 1579 if (replacements && typeof replacements === "object") {
1524 fn = fn.toString(); 1580 fn = fn.toString();
1525 for (var p in replacements) { 1581 for (var placeholder in replacements) {
1526 var match = '%' + p + '%'; 1582 var match = '%' + placeholder + '%';
1527 do { 1583 do {
1528 fn = fn.replace(match, replacements[p]); 1584 fn = fn.replace(match, replacements[placeholder]);
1529 } while(fn.indexOf(match) !== -1); 1585 } while(fn.indexOf(match) !== -1);
1530 } 1586 }
1531 } 1587 }
......
...@@ -57,6 +57,28 @@ casper.then(function(self) { ...@@ -57,6 +57,28 @@ casper.then(function(self) {
57 57
58 casper.test.assert(casper.steps.length === 2, 'then() adds a new navigation step'); 58 casper.test.assert(casper.steps.length === 2, 'then() adds a new navigation step');
59 59
60 // Casper#evaluate()
61
62 casper.then(function(self) {
63 var params = {
64 "boolean true": true,
65 "boolean false": false,
66 "int number": 42,
67 "float number": 1337.42,
68 "string": "plop! \"Ÿ£$\" 'no'",
69 "array": [1, 2, 3],
70 "object": {a: 1, b: 2}
71 };
72 var casperParams = self.evaluate(function() {
73 return __casper_params__;
74 }, params);
75 self.test.assertType(casperParams, "object", 'Casper.evaluate() exposes parameters in a dedicated object');
76 self.test.assertEquals(Object.keys(casperParams).length, 7, 'Casper.evaluate() exposes parameters object has the correct length');
77 for (var param in casperParams) {
78 self.test.assertEquals(JSON.stringify(casperParams[param]), JSON.stringify(params[param]), 'Casper.evaluate() can pass a ' + param);
79 }
80 });
81
60 // Casper#fill() 82 // Casper#fill()
61 casper.then(function(self) { 83 casper.then(function(self) {
62 self.test.assertTitle('CasperJS test form', 'click() casper can click on a text link and react when it is loaded 2/2'); 84 self.test.assertTitle('CasperJS test form', 'click() casper can click on a text link and react when it is loaded 2/2');
...@@ -116,7 +138,7 @@ casper.test.assertMatch(xunit.getXML(), /<testcase classname="foo" name="bar"/, ...@@ -116,7 +138,7 @@ casper.test.assertMatch(xunit.getXML(), /<testcase classname="foo" name="bar"/,
116 xunit.addFailure('bar', 'baz', 'wrong', 'chucknorriz'); 138 xunit.addFailure('bar', 'baz', 'wrong', 'chucknorriz');
117 casper.test.assertMatch(xunit.getXML(), /<testcase classname="bar" name="baz"><failure type="chucknorriz">wrong/, 'addFailure() adds a failed testcase'); 139 casper.test.assertMatch(xunit.getXML(), /<testcase classname="bar" name="baz"><failure type="chucknorriz">wrong/, 'addFailure() adds a failed testcase');
118 140
119 // Casper.ClientUtils.log 141 // Casper.ClientUtils.log()
120 casper.then(function(self) { 142 casper.then(function(self) {
121 casper.test.comment('client utils log'); 143 casper.test.comment('client utils log');
122 var oldLevel = casper.options.logLevel; 144 var oldLevel = casper.options.logLevel;
...@@ -136,30 +158,30 @@ casper.then(function(self) { ...@@ -136,30 +158,30 @@ casper.then(function(self) {
136 casper.options.verbose = true; 158 casper.options.verbose = true;
137 }); 159 });
138 160
139 // Casper.wait 161 // Casper.wait()
140 var start = new Date().getTime(); 162 var start = new Date().getTime();
141 casper.wait(1000); 163 casper.wait(1000, function(self) {
142 casper.then(function(self) { 164 self.test.comment('wait');
143 casper.test.comment('wait');
144 self.test.assert(new Date().getTime() - start > 1000, 'Casper.wait() can wait for a given amount of time'); 165 self.test.assert(new Date().getTime() - start > 1000, 'Casper.wait() can wait for a given amount of time');
145 });
146 166
147 // Casper.waitFor 167 // Casper.waitFor()
148 casper.thenOpen('tests/site/waitFor.html', function(self) { 168 casper.thenOpen('tests/site/waitFor.html', function(self) {
149 casper.test.comment('waitFor'); 169 casper.test.comment('waitFor');
150 self.waitFor(function(self) { 170 self.waitFor(function(self) {
151 return self.evaluate(function() { 171 return self.evaluate(function() {
152 return document.querySelectorAll('li').length === 4; 172 return document.querySelectorAll('li').length === 4;
173 });
174 }, function(self) {
175 self.test.pass('Casper.waitFor() can wait for something to happen');
176 }, function(self) {
177 self.test.fail('Casper.waitFor() can wait for something to happen');
153 }); 178 });
154 }, function(self) {
155 self.test.pass('Casper.waitFor() can wait for something to happen');
156 }, function(self) {
157 self.test.fail('Casper.waitFor() can wait for something to happen');
158 }); 179 });
159 }); 180 });
160 181
161 // run suite 182 // run suite
162 casper.run(function(self) { 183 casper.run(function(self) {
184 self.test.comment('logging, again');
163 self.test.assertEquals(self.result.log.length, 3, 'log() logged messages'); 185 self.test.assertEquals(self.result.log.length, 3, 'log() logged messages');
164 self.test.renderResults(true, 0, save); 186 self.test.renderResults(true, 0, save);
165 }); 187 });
......