Commit d8d08333 d8d083331dc4ac399fc803b3ce4bfc211c939d89 by Nicolas Perriault

refactored Casper.evaluate() in order to accept arguments!

So basically now you can do:

``` js
casper.evaluate(function(username, password) {
    document.querySelector('#username').value = username;
    document.querySelector('#password').value = password;
    document.querySelector('#submit').click();
}, {
    username: 'Bazoonga',
    password: 'baz00nga'
})
```
1 parent 4e490488
...@@ -120,11 +120,9 @@ ...@@ -120,11 +120,9 @@
120 * @return string Base64 encoded result 120 * @return string Base64 encoded result
121 */ 121 */
122 base64encode: function(url) { 122 base64encode: function(url) {
123 return this.evaluate(function() { 123 return this.evaluate(function(url) {
124 return __utils__.getBase64(__casper_params__.url); 124 return __utils__.getBase64(url);
125 }, { 125 }, { url: url });
126 url: url
127 });
128 }, 126 },
129 127
130 /** 128 /**
...@@ -168,9 +166,9 @@ ...@@ -168,9 +166,9 @@
168 * @return Casper 166 * @return Casper
169 */ 167 */
170 captureSelector: function(targetFile, selector) { 168 captureSelector: function(targetFile, selector) {
171 return this.capture(targetFile, this.evaluate(function() { 169 return this.capture(targetFile, this.evaluate(function(selector) {
172 try { 170 try {
173 var clipRect = document.querySelector(__casper_params__.selector).getBoundingClientRect(); 171 var clipRect = document.querySelector(selector).getBoundingClientRect();
174 return { 172 return {
175 top: clipRect.top, 173 top: clipRect.top,
176 left: clipRect.left, 174 left: clipRect.left,
...@@ -178,11 +176,9 @@ ...@@ -178,11 +176,9 @@
178 height: clipRect.height 176 height: clipRect.height
179 }; 177 };
180 } catch (e) { 178 } catch (e) {
181 __utils__.log("Unable to fetch bounds for element " + __casper_params__.selector, "warning"); 179 __utils__.log("Unable to fetch bounds for element " + selector, "warning");
182 } 180 }
183 }, { 181 }, { selector: selector }));
184 selector: selector
185 }));
186 }, 182 },
187 183
188 /** 184 /**
...@@ -224,8 +220,8 @@ ...@@ -224,8 +220,8 @@
224 click: function(selector, fallbackToHref) { 220 click: function(selector, fallbackToHref) {
225 fallbackToHref = isType(fallbackToHref, "undefined") ? true : !!fallbackToHref; 221 fallbackToHref = isType(fallbackToHref, "undefined") ? true : !!fallbackToHref;
226 this.log("click on selector: " + selector, "debug"); 222 this.log("click on selector: " + selector, "debug");
227 return this.evaluate(function() { 223 return this.evaluate(function(selector, fallbackToHref) {
228 return __utils__.click(__casper_params__.selector, __casper_params__.fallbackToHref); 224 return __utils__.click(selector, fallbackToHref);
229 }, { 225 }, {
230 selector: selector, 226 selector: selector,
231 fallbackToHref: fallbackToHref 227 fallbackToHref: fallbackToHref
...@@ -323,24 +319,12 @@ ...@@ -323,24 +319,12 @@
323 319
324 /** 320 /**
325 * Evaluates an expression in the page context, a bit like what 321 * Evaluates an expression in the page context, a bit like what
326 * WebPage#evaluate does, but can also replace values by their 322 * WebPage#evaluate does, but the passed function can also accept
327 * placeholer names: 323 * parameters if a context Object is also passed:
328 * 324 *
329 * casper.evaluate(function() { 325 * casper.evaluate(function(username, password) {
330 * document.querySelector('#username').value = '%username%'; 326 * document.querySelector('#username').value = username;
331 * document.querySelector('#password').value = '%password%'; 327 * document.querySelector('#password').value = password;
332 * document.querySelector('#submit').click();
333 * }, {
334 * username: 'Bazoonga',
335 * password: 'baz00nga'
336 * })
337 *
338 * As an alternative, CasperJS injects a `__casper_params__` Object
339 * instance containing all the parameters you passed:
340 *
341 * casper.evaluate(function() {
342 * document.querySelector('#username').value = __casper_params__.username;
343 * document.querySelector('#password').value = __casper_params__.password;
344 * document.querySelector('#submit').click(); 328 * document.querySelector('#submit').click();
345 * }, { 329 * }, {
346 * username: 'Bazoonga', 330 * username: 'Bazoonga',
...@@ -351,25 +335,15 @@ ...@@ -351,25 +335,15 @@
351 * arguments to the function. 335 * arguments to the function.
352 * TODO: don't forget to keep this backward compatible. 336 * TODO: don't forget to keep this backward compatible.
353 * 337 *
354 * @param function fn The function to be evaluated within current page DOM 338 * @param Function fn The function to be evaluated within current page DOM
355 * @param object replacements Parameters to pass to the remote environment 339 * @param Object context Object containing the parameters to inject into the function
356 * @return mixed 340 * @return mixed
357 * @see WebPage#evaluate 341 * @see WebPage#evaluate
358 */ 342 */
359 evaluate: function(fn, replacements) { 343 evaluate: function(fn, context) {
360 replacements = isType(replacements, "object") ? replacements : {}; 344 context = isType(context, "object") ? context : {};
361 this.page.evaluate(replaceFunctionPlaceholders(function() { 345 var newFn = new phantom.Casper.FunctionArgsInjector(fn, context).process();
362 window.__casper_params__ = {}; 346 return this.page.evaluate(newFn);
363 try {
364 var jsonString = unescape(decodeURIComponent('%replacements%'));
365 window.__casper_params__ = JSON.parse(jsonString);
366 } catch (e) {
367 __utils__.log("Unable to replace parameters: " + e, "error");
368 }
369 }, {
370 replacements: encodeURIComponent(escape(JSON.stringify(replacements).replace("'", "\'")))
371 }));
372 return this.page.evaluate(replaceFunctionPlaceholders(fn, replacements));
373 }, 347 },
374 348
375 /** 349 /**
...@@ -395,11 +369,9 @@ ...@@ -395,11 +369,9 @@
395 * @return Boolean 369 * @return Boolean
396 */ 370 */
397 exists: function(selector) { 371 exists: function(selector) {
398 return this.evaluate(function() { 372 return this.evaluate(function(selector) {
399 return __utils__.exists(__casper_params__.selector); 373 return __utils__.exists(selector);
400 }, { 374 }, { selector: selector });
401 selector: selector
402 });
403 }, 375 },
404 376
405 /** 377 /**
...@@ -411,11 +383,9 @@ ...@@ -411,11 +383,9 @@
411 * @return Boolean 383 * @return Boolean
412 */ 384 */
413 visible: function(selector) { 385 visible: function(selector) {
414 return this.evaluate(function() { 386 return this.evaluate(function(selector) {
415 return __utils__.visible(__casper_params__.selector); 387 return __utils__.visible(selector);
416 }, { 388 }, { selector: selector });
417 selector: selector
418 });
419 }, 389 },
420 390
421 /** 391 /**
...@@ -437,11 +407,9 @@ ...@@ -437,11 +407,9 @@
437 * @return String 407 * @return String
438 */ 408 */
439 fetchText: function(selector) { 409 fetchText: function(selector) {
440 return this.evaluate(function() { 410 return this.evaluate(function(selector) {
441 return __utils__.fetchText(__casper_params__.selector); 411 return __utils__.fetchText(selector);
442 }, { 412 }, { selector: selector });
443 selector: selector
444 });
445 }, 413 },
446 414
447 /** 415 /**
...@@ -459,8 +427,8 @@ ...@@ -459,8 +427,8 @@
459 if (!isType(vals, "object")) { 427 if (!isType(vals, "object")) {
460 throw "form values must be provided as an object"; 428 throw "form values must be provided as an object";
461 } 429 }
462 var fillResults = this.evaluate(function() { 430 var fillResults = this.evaluate(function(selector, values) {
463 return __utils__.fill(__casper_params__.selector, __casper_params__.values); 431 return __utils__.fill(selector, values);
464 }, { 432 }, {
465 selector: selector, 433 selector: selector,
466 values: vals 434 values: vals
...@@ -489,15 +457,13 @@ ...@@ -489,15 +457,13 @@
489 } 457 }
490 // Form submission? 458 // Form submission?
491 if (submit) { 459 if (submit) {
492 this.evaluate(function() { 460 this.evaluate(function(selector) {
493 var form = document.querySelector(__casper_params__.selector); 461 var form = document.querySelector(selector);
494 var method = form.getAttribute('method').toUpperCase() || "GET"; 462 var method = form.getAttribute('method').toUpperCase() || "GET";
495 var action = form.getAttribute('action') || "unknown"; 463 var action = form.getAttribute('action') || "unknown";
496 __utils__.log('submitting form to ' + action + ', HTTP ' + method, 'info'); 464 __utils__.log('submitting form to ' + action + ', HTTP ' + method, 'info');
497 form.submit(); 465 form.submit();
498 }, { 466 }, { selector: selector });
499 selector: selector
500 });
501 } 467 }
502 }, 468 },
503 469
...@@ -532,8 +498,7 @@ ...@@ -532,8 +498,7 @@
532 * @return mixed 498 * @return mixed
533 */ 499 */
534 getGlobal: function(name) { 500 getGlobal: function(name) {
535 var result = this.evaluate(function() { 501 var result = this.evaluate(function(name) {
536 var name = window.__casper_params__.name;
537 var result = {}; 502 var result = {};
538 try { 503 try {
539 result.value = JSON.stringify(window[name]); 504 result.value = JSON.stringify(window[name]);
...@@ -786,14 +751,14 @@ ...@@ -786,14 +751,14 @@
786 * Adds a new navigation step to perform code evaluation within the 751 * Adds a new navigation step to perform code evaluation within the
787 * current retrieved page DOM. 752 * current retrieved page DOM.
788 * 753 *
789 * @param function fn The function to be evaluated within current page DOM 754 * @param function fn The function to be evaluated within current page DOM
790 * @param object replacements Optional replacements to performs, eg. for '%foo%' => {foo: 'bar'} 755 * @param object context Optional function parameters context
791 * @return Casper 756 * @return Casper
792 * @see Casper#evaluate 757 * @see Casper#evaluate
793 */ 758 */
794 thenEvaluate: function(fn, replacements) { 759 thenEvaluate: function(fn, context) {
795 return this.then(function(self) { 760 return this.then(function(self) {
796 self.evaluate(fn, replacements); 761 self.evaluate(fn, context);
797 }); 762 });
798 }, 763 },
799 764
...@@ -818,15 +783,15 @@ ...@@ -818,15 +783,15 @@
818 * Adds a new navigation step for opening and evaluate an expression 783 * Adds a new navigation step for opening and evaluate an expression
819 * against the DOM retrieved from the provided location. 784 * against the DOM retrieved from the provided location.
820 * 785 *
821 * @param String location The url to open 786 * @param String location The url to open
822 * @param function fn The function to be evaluated within current page DOM 787 * @param function fn The function to be evaluated within current page DOM
823 * @param object replacements Optional replacements to performs, eg. for '%foo%' => {foo: 'bar'} 788 * @param object context Optional function parameters context
824 * @return Casper 789 * @return Casper
825 * @see Casper#evaluate 790 * @see Casper#evaluate
826 * @see Casper#open 791 * @see Casper#open
827 */ 792 */
828 thenOpenAndEvaluate: function(location, fn, replacements) { 793 thenOpenAndEvaluate: function(location, fn, context) {
829 return this.thenOpen(location).thenEvaluate(fn, replacements); 794 return this.thenOpen(location).thenEvaluate(fn, context);
830 }, 795 },
831 796
832 /** 797 /**
...@@ -1681,6 +1646,51 @@ ...@@ -1681,6 +1646,51 @@
1681 }; 1646 };
1682 1647
1683 /** 1648 /**
1649 * Function argument injector.
1650 *
1651 */
1652 phantom.Casper.FunctionArgsInjector = function(fn, values) {
1653 this.fn = fn;
1654 this.values = typeof values === "object" ? values : {};
1655
1656 this.extract = function(fn) {
1657 var match = /^function\s?(\w+)?\s?\((.*)\)\s?\{([\s\S]*)\}/i.exec(fn.toString().trim());
1658 if (match && match.length > 1) {
1659 var args = match[2].split(',').map(function(arg) {
1660 return arg.replace(new RegExp(/\/\*+.*\*\//ig), "").trim();
1661 }).filter(function(arg) {
1662 return arg;
1663 }) || [];
1664 return {
1665 name: match[1] ? match[1].trim() : null,
1666 args: args,
1667 body: match[3] ? match[3].trim() : ''
1668 };
1669 }
1670 };
1671
1672 this.process = function() {
1673 var fnObj = this.extract(this.fn);
1674 var inject = this.getArgsInjectionString(fnObj.args, this.values);
1675 return 'function ' + (fnObj.name || '') + '(){' + inject + fnObj.body + '}';
1676 };
1677
1678 this.getArgsInjectionString = function(args, values) {
1679 values = typeof values === "object" ? values : {};
1680 var jsonValues = escape(encodeURIComponent(JSON.stringify(values)));
1681 var inject = [
1682 'var __casper_params__ = JSON.parse(decodeURIComponent(unescape(\'' + jsonValues + '\')));'
1683 ];
1684 args.forEach(function(arg) {
1685 if (arg in values) {
1686 inject.push('var ' + arg + '=__casper_params__["' + arg + '"];');
1687 }
1688 });
1689 return inject.join('\n') + '\n';
1690 };
1691 };
1692
1693 /**
1684 * JUnit XML (xUnit) exporter for test results. 1694 * JUnit XML (xUnit) exporter for test results.
1685 * 1695 *
1686 */ 1696 */
...@@ -1817,7 +1827,7 @@ ...@@ -1817,7 +1827,7 @@
1817 } 1827 }
1818 } 1828 }
1819 } 1829 }
1820 // Client utils injection 1830 // Client-side utils injection
1821 var injected = page.evaluate(replaceFunctionPlaceholders(function() { 1831 var injected = page.evaluate(replaceFunctionPlaceholders(function() {
1822 eval("var ClientUtils = " + decodeURIComponent("%utils%")); 1832 eval("var ClientUtils = " + decodeURIComponent("%utils%"));
1823 __utils__ = new ClientUtils(); 1833 __utils__ = new ClientUtils();
......
...@@ -78,7 +78,7 @@ fs.remove(testFile); ...@@ -78,7 +78,7 @@ fs.remove(testFile);
78 78
79 // Casper#evaluate() 79 // Casper#evaluate()
80 casper.then(function(self) { 80 casper.then(function(self) {
81 self.test.comment('evaluating'); 81 self.test.comment('Casper.evaluate()');
82 var params = { 82 var params = {
83 "boolean true": true, 83 "boolean true": true,
84 "boolean false": false, 84 "boolean false": false,
...@@ -92,7 +92,7 @@ casper.then(function(self) { ...@@ -92,7 +92,7 @@ casper.then(function(self) {
92 return __casper_params__; 92 return __casper_params__;
93 }, params); 93 }, params);
94 self.test.assertType(casperParams, "object", 'Casper.evaluate() exposes parameters in a dedicated object'); 94 self.test.assertType(casperParams, "object", 'Casper.evaluate() exposes parameters in a dedicated object');
95 self.test.assertEquals(Object.keys(casperParams).length, 7, 'Casper.evaluate() exposes parameters object has the correct length'); 95 self.test.assertEquals(Object.keys(casperParams).length, 7, 'Casper.evaluate() object containing parameters has the correct length');
96 for (var param in casperParams) { 96 for (var param in casperParams) {
97 self.test.assertEquals(JSON.stringify(casperParams[param]), JSON.stringify(params[param]), 'Casper.evaluate() can pass a ' + param); 97 self.test.assertEquals(JSON.stringify(casperParams[param]), JSON.stringify(params[param]), 'Casper.evaluate() can pass a ' + param);
98 self.test.assertEquals(typeof casperParams[param], typeof params[param], 'Casper.evaluate() preserves the ' + param + ' type'); 98 self.test.assertEquals(typeof casperParams[param], typeof params[param], 'Casper.evaluate() preserves the ' + param + ' type');
......