PageRenderTime 57ms CodeModel.GetById 17ms RepoModel.GetById 1ms app.codeStats 0ms

/casper.js

https://github.com/canadaduane/casperjs
JavaScript | 2705 lines | 1833 code | 155 blank | 717 comment | 230 complexity | 70e217597e077315cfd9dc39a9049c56 MD5 | raw file

Large files files are truncated, but you can click here to view the full file

  1. /*!
  2. * Casper is a navigation utility for PhantomJS.
  3. *
  4. * Documentation: http://n1k0.github.com/casperjs/
  5. * Repository: http://github.com/n1k0/casperjs
  6. *
  7. * Copyright (c) 2011 Nicolas Perriault
  8. *
  9. * Permission is hereby granted, free of charge, to any person obtaining a
  10. * copy of this software and associated documentation files (the "Software"),
  11. * to deal in the Software without restriction, including without limitation
  12. * the rights to use, copy, modify, merge, publish, distribute, sublicense,
  13. * and/or sell copies of the Software, and to permit persons to whom the
  14. * Software is furnished to do so, subject to the following conditions:
  15. *
  16. * The above copyright notice and this permission notice shall be included
  17. * in all copies or substantial portions of the Software.
  18. *
  19. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
  20. * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  21. * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
  22. * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  23. * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
  24. * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
  25. * DEALINGS IN THE SOFTWARE.
  26. *
  27. */
  28. (function(phantom) {
  29. /**
  30. * Main Casper object.
  31. *
  32. * @param Object options Casper options
  33. * @return Casper
  34. */
  35. phantom.Casper = function(options) {
  36. var DEFAULT_DIE_MESSAGE = "Suite explicitely interrupted without any message given.";
  37. var DEFAULT_USER_AGENT = "Mozilla/5.0 (Windows NT 6.0) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/13.0.782.112 Safari/535.1";
  38. // init & checks
  39. if (!(this instanceof arguments.callee)) {
  40. return new Casper(options);
  41. }
  42. // default options
  43. this.defaults = {
  44. clientScripts: [],
  45. faultTolerant: true,
  46. logLevel: "error",
  47. onDie: null,
  48. onError: null,
  49. onLoadError: null,
  50. onPageInitialized: null,
  51. page: null,
  52. pageSettings: { userAgent: DEFAULT_USER_AGENT },
  53. timeout: null,
  54. verbose: false
  55. };
  56. // privates
  57. // local properties
  58. this.checker = null;
  59. this.colorizer = new phantom.Casper.Colorizer();
  60. this.currentUrl = 'about:blank';
  61. this.currentHTTPStatus = 200;
  62. this.defaultWaitTimeout = 5000;
  63. this.delayedExecution = false;
  64. this.history = [];
  65. this.loadInProgress = false;
  66. this.logLevels = ["debug", "info", "warning", "error"];
  67. this.logStyles = {
  68. debug: 'INFO',
  69. info: 'PARAMETER',
  70. warning: 'COMMENT',
  71. error: 'ERROR'
  72. };
  73. this.options = mergeObjects(this.defaults, options);
  74. this.page = null;
  75. this.requestUrl = 'about:blank';
  76. this.result = {
  77. log: [],
  78. status: "success",
  79. time: 0
  80. };
  81. this.started = false;
  82. this.step = 0;
  83. this.steps = [];
  84. this.test = new phantom.Casper.Tester(this);
  85. };
  86. /**
  87. * Casper prototype
  88. */
  89. phantom.Casper.prototype = {
  90. /**
  91. * Go a step back in browser's history
  92. *
  93. * @return Casper
  94. */
  95. back: function() {
  96. return this.then(function(self) {
  97. self.evaluate(function() {
  98. history.back();
  99. });
  100. });
  101. },
  102. /**
  103. * Encodes a resource using the base64 algorithm synchroneously using
  104. * client-side XMLHttpRequest.
  105. *
  106. * NOTE: we cannot use window.btoa() for some strange reasons here.
  107. *
  108. * @param String url The url to download
  109. * @return string Base64 encoded result
  110. */
  111. base64encode: function(url) {
  112. return this.evaluate(function() {
  113. return __utils__.getBase64(__casper_params__.url);
  114. }, {
  115. url: url
  116. });
  117. },
  118. /**
  119. * Use Gibberish AES library to encode a string using a password.
  120. *
  121. * @param String text Some text to encode
  122. * @param String password The password to use to encode the text
  123. * @return String AES encoded result
  124. */
  125. aesEncode: function(text, password) {
  126. try {
  127. var encode = GibberishAES.enc;
  128. } catch (e) {
  129. throw 'GibberishAES library not found. Did you forget to phantom.injectJs("gibberish-aes.js")?';
  130. }
  131. return encode(text, password);
  132. },
  133. /**
  134. * Use Gibberish AES library to decode a string using a password.
  135. *
  136. * @param String text Some text to decode
  137. * @param String password The password to use to decode the text
  138. * @return String Decoded result
  139. */
  140. aesDecode: function(text, password) {
  141. try {
  142. var decode = GibberishAES.dec;
  143. } catch (e) {
  144. throw 'GibberishAES library not found. Did you forget to phantom.injectJs("gibberish-aes.js")?';
  145. }
  146. return decode(text, password);
  147. },
  148. /**
  149. * Proxy method for WebPage#render. Adds a clipRect parameter for
  150. * automatically set page clipRect setting values and sets it back once
  151. * done. If the cliprect parameter is omitted, the full page viewport
  152. * area will be rendered.
  153. *
  154. * @param String targetFile A target filename
  155. * @param mixed clipRect An optional clipRect object (optional)
  156. * @return Casper
  157. */
  158. capture: function(targetFile, clipRect) {
  159. var previousClipRect;
  160. if (clipRect) {
  161. if (!isType(clipRect, "object")) {
  162. throw new Error("clipRect must be an Object instance.");
  163. }
  164. previousClipRect = this.page.clipRect;
  165. this.page.clipRect = clipRect;
  166. this.log('Capturing page to ' + targetFile + ' with clipRect' + JSON.stringify(clipRect), "debug");
  167. } else {
  168. this.log('Capturing page to ' + targetFile, "debug");
  169. }
  170. try {
  171. this.page.render(targetFile);
  172. } catch (e) {
  173. this.log('Failed to capture screenshot as ' + targetFile + ': ' + e, "error");
  174. }
  175. if (previousClipRect) {
  176. this.page.clipRect = previousClipRect;
  177. }
  178. return this;
  179. },
  180. /**
  181. * Captures the page area containing the provided selector.
  182. *
  183. * @param String targetFile Target destination file path.
  184. * @param String selector CSS3 selector
  185. * @return Casper
  186. */
  187. captureSelector: function(targetFile, selector) {
  188. return this.capture(targetFile, this.evaluate(function() {
  189. try {
  190. var clipRect = document.querySelector(__casper_params__.selector).getBoundingClientRect();
  191. return {
  192. top: clipRect.top,
  193. left: clipRect.left,
  194. width: clipRect.width,
  195. height: clipRect.height
  196. };
  197. } catch (e) {
  198. __utils__.log("Unable to fetch bounds for element " + __casper_params__.selector, "warning");
  199. }
  200. }, {
  201. selector: selector
  202. }));
  203. },
  204. /**
  205. * Checks for any further navigation step to process.
  206. *
  207. * @param Casper self A self reference
  208. * @param function onComplete An options callback to apply on completion
  209. */
  210. checkStep: function(self, onComplete) {
  211. var step = self.steps[self.step];
  212. if (!self.loadInProgress && isType(step, "function")) {
  213. var curStepNum = self.step + 1;
  214. var stepInfo = "Step " + curStepNum + "/" + self.steps.length + ": ";
  215. self.log(stepInfo + self.page.evaluate(function() {
  216. return document.location.href;
  217. }) + ' (HTTP ' + self.currentHTTPStatus + ')', "info");
  218. try {
  219. step(self);
  220. } catch (e) {
  221. if (self.options.faultTolerant) {
  222. self.log("Step error: " + e, "error");
  223. } else {
  224. throw e;
  225. }
  226. }
  227. var time = new Date().getTime() - self.startTime;
  228. self.log(stepInfo + "done in " + time + "ms.", "info");
  229. self.step++;
  230. }
  231. if (!isType(step, "function") && !self.delayedExecution) {
  232. self.result.time = new Date().getTime() - self.startTime;
  233. self.log("Done " + self.steps.length + " steps in " + self.result.time + 'ms.', "info");
  234. clearInterval(self.checker);
  235. if (isType(onComplete, "function")) {
  236. try {
  237. onComplete(self);
  238. } catch (err) {
  239. self.log("could not complete final step: " + err, "error");
  240. }
  241. } else {
  242. // default behavior is to exit phantom
  243. self.exit();
  244. }
  245. }
  246. },
  247. /**
  248. * Emulates a click on the element from the provided selector, if
  249. * possible. In case of success, `true` is returned.
  250. *
  251. * @param String selector A DOM CSS3 compatible selector
  252. * @param Boolean fallbackToHref Whether to try to relocate to the value of any href attribute (default: true)
  253. * @return Boolean
  254. */
  255. click: function(selector, fallbackToHref) {
  256. fallbackToHref = isType(fallbackToHref, "undefined") ? true : !!fallbackToHref;
  257. this.log("click on selector: " + selector, "debug");
  258. return this.evaluate(function() {
  259. return __utils__.click(__casper_params__.selector, __casper_params__.fallbackToHref);
  260. }, {
  261. selector: selector,
  262. fallbackToHref: fallbackToHref
  263. });
  264. },
  265. /**
  266. * Logs the HTML code of the current page.
  267. *
  268. * @return Casper
  269. */
  270. debugHTML: function() {
  271. this.echo(this.evaluate(function() {
  272. return document.body.innerHTML;
  273. }));
  274. return this;
  275. },
  276. /**
  277. * Logs the textual contents of the current page.
  278. *
  279. * @return Casper
  280. */
  281. debugPage: function() {
  282. this.echo(this.evaluate(function() {
  283. return document.body.innerText;
  284. }));
  285. return this;
  286. },
  287. /**
  288. * Exit phantom on failure, with a logged error message.
  289. *
  290. * @param String message An optional error message
  291. * @param Number status An optional exit status code (must be > 0)
  292. * @return Casper
  293. */
  294. die: function(message, status) {
  295. this.result.status = 'error';
  296. this.result.time = new Date().getTime() - this.startTime;
  297. message = isType(message, "string") && message.length > 0 ? message : DEFAULT_DIE_MESSAGE;
  298. this.log(message, "error");
  299. if (isType(this.options.onDie, "function")) {
  300. this.options.onDie(this, message, status);
  301. }
  302. return this.exit(Number(status) > 0 ? Number(status) : 1);
  303. },
  304. /**
  305. * Iterates over the values of a provided array and execute a callback
  306. * for each item.
  307. *
  308. * @param Array array
  309. * @param Function fn Callback: function(self, item, index)
  310. * @return Casper
  311. */
  312. each: function(array, fn) {
  313. if (array.constructor !== Array) {
  314. self.log("each() only works with arrays", "error");
  315. return this;
  316. }
  317. (function(self) {
  318. array.forEach(function(item, i) {
  319. fn(self, item, i);
  320. });
  321. })(this);
  322. return this;
  323. },
  324. /**
  325. * Prints something to stdout.
  326. *
  327. * @param String text A string to echo to stdout
  328. * @return Casper
  329. */
  330. echo: function(text, style) {
  331. console.log(style ? this.colorizer.colorize(text, style) : text);
  332. return this;
  333. },
  334. /**
  335. * Evaluates an expression in the page context, a bit like what
  336. * WebPage#evaluate does, but can also replace values by their
  337. * placeholer names:
  338. *
  339. * casper.evaluate(function() {
  340. * document.querySelector('#username').value = '%username%';
  341. * document.querySelector('#password').value = '%password%';
  342. * document.querySelector('#submit').click();
  343. * }, {
  344. * username: 'Bazoonga',
  345. * password: 'baz00nga'
  346. * })
  347. *
  348. * As an alternative, CasperJS injects a `__casper_params__` Object
  349. * instance containing all the parameters you passed:
  350. *
  351. * casper.evaluate(function() {
  352. * document.querySelector('#username').value = __casper_params__.username;
  353. * document.querySelector('#password').value = __casper_params__.password;
  354. * document.querySelector('#submit').click();
  355. * }, {
  356. * username: 'Bazoonga',
  357. * password: 'baz00nga'
  358. * })
  359. *
  360. * FIXME: waiting for a patch of PhantomJS to allow direct passing of
  361. * arguments to the function.
  362. * TODO: don't forget to keep this backward compatible.
  363. *
  364. * @param function fn The function to be evaluated within current page DOM
  365. * @param object replacements Parameters to pass to the remote environment
  366. * @return mixed
  367. * @see WebPage#evaluate
  368. */
  369. evaluate: function(fn, replacements) {
  370. replacements = isType(replacements, "object") ? replacements : {};
  371. this.page.evaluate(replaceFunctionPlaceholders(function() {
  372. window.__casper_params__ = {};
  373. try {
  374. var jsonString = unescape(decodeURIComponent('%replacements%'));
  375. window.__casper_params__ = JSON.parse(jsonString);
  376. } catch (e) {
  377. __utils__.log("Unable to replace parameters: " + e, "error");
  378. }
  379. }, {
  380. replacements: encodeURIComponent(escape(JSON.stringify(replacements).replace("'", "\'")))
  381. }));
  382. return this.page.evaluate(replaceFunctionPlaceholders(fn, replacements));
  383. },
  384. /**
  385. * Evaluates an expression within the current page DOM and die() if it
  386. * returns false.
  387. *
  388. * @param function fn The expression to evaluate
  389. * @param String message The error message to log
  390. * @return Casper
  391. */
  392. evaluateOrDie: function(fn, message) {
  393. if (!this.evaluate(fn)) {
  394. return this.die(message);
  395. }
  396. return this;
  397. },
  398. /**
  399. * Checks if an element matching the provided CSS3 selector exists in
  400. * current page DOM.
  401. *
  402. * @param String selector A CSS3 selector
  403. * @return Boolean
  404. */
  405. exists: function(selector) {
  406. return this.evaluate(function() {
  407. return __utils__.exists(__casper_params__.selector);
  408. }, {
  409. selector: selector
  410. });
  411. },
  412. /**
  413. * Exits phantom.
  414. *
  415. * @param Number status Status
  416. * @return Casper
  417. */
  418. exit: function(status) {
  419. phantom.exit(status);
  420. return this;
  421. },
  422. /**
  423. * Fetches innerText within the element(s) matching a given CSS3
  424. * selector.
  425. *
  426. * @param String selector A CSS3 selector
  427. * @return String
  428. */
  429. fetchText: function(selector) {
  430. return this.evaluate(function() {
  431. return __utils__.fetchText(__casper_params__.selector);
  432. }, {
  433. selector: selector
  434. });
  435. },
  436. /**
  437. * Fills a form with provided field values.
  438. *
  439. * @param String selector A CSS3 selector to the target form to fill
  440. * @param Object vals Field values
  441. * @param Boolean submit Submit the form?
  442. */
  443. fill: function(selector, vals, submit) {
  444. submit = submit === true ? submit : false;
  445. if (!isType(selector, "string") || !selector.length) {
  446. throw "form selector must be a non-empty string";
  447. }
  448. if (!isType(vals, "object")) {
  449. throw "form values must be provided as an object";
  450. }
  451. var fillResults = this.evaluate(function() {
  452. return __utils__.fill(__casper_params__.selector, __casper_params__.values);
  453. }, {
  454. selector: selector,
  455. values: vals
  456. });
  457. if (!fillResults) {
  458. throw "unable to fill form";
  459. } else if (fillResults.errors.length > 0) {
  460. (function(self){
  461. fillResults.errors.forEach(function(error) {
  462. self.log("form error: " + error, "error");
  463. });
  464. })(this);
  465. if (submit) {
  466. this.log("errors encountered while filling form; submission aborted", "warning");
  467. submit = false;
  468. }
  469. }
  470. // File uploads
  471. if (fillResults.files && fillResults.files.length > 0) {
  472. (function(self) {
  473. fillResults.files.forEach(function(file) {
  474. var fileFieldSelector = [selector, 'input[name="' + file.name + '"]'].join(' ');
  475. self.page.uploadFile(fileFieldSelector, file.path);
  476. });
  477. })(this);
  478. }
  479. // Form submission?
  480. if (submit) {
  481. this.evaluate(function() {
  482. var form = document.querySelector(__casper_params__.selector);
  483. var method = form.getAttribute('method').toUpperCase() || "GET";
  484. var action = form.getAttribute('action') || "unknown";
  485. __utils__.log('submitting form to ' + action + ', HTTP ' + method, 'info');
  486. form.submit();
  487. }, {
  488. selector: selector
  489. });
  490. }
  491. },
  492. /**
  493. * Go a step forward in browser's history
  494. *
  495. * @return Casper
  496. */
  497. forward: function(then) {
  498. return this.then(function(self) {
  499. self.evaluate(function() {
  500. history.forward();
  501. });
  502. });
  503. },
  504. /**
  505. * Retrieves current document url.
  506. *
  507. * @return String
  508. */
  509. getCurrentUrl: function() {
  510. return decodeURIComponent(this.evaluate(function() {
  511. return document.location.href;
  512. }));
  513. },
  514. /**
  515. * Retrieves global variable.
  516. *
  517. * @param String name The name of the global variable to retrieve
  518. * @return mixed
  519. */
  520. getGlobal: function(name) {
  521. return this.evaluate(function() {
  522. return window[window.__casper_params__.name];
  523. }, {'name': name});
  524. },
  525. /**
  526. * Retrieves current page title, if any.
  527. *
  528. * @return String
  529. */
  530. getTitle: function() {
  531. return this.evaluate(function() {
  532. return document.title;
  533. });
  534. },
  535. /**
  536. * Logs a message.
  537. *
  538. * @param String message The message to log
  539. * @param String level The log message level (from Casper.logLevels property)
  540. * @param String space Space from where the logged event occured (default: "phantom")
  541. * @return Casper
  542. */
  543. log: function(message, level, space) {
  544. level = level && this.logLevels.indexOf(level) > -1 ? level : "debug";
  545. space = space ? space : "phantom";
  546. if (level === "error" && isType(this.options.onError, "function")) {
  547. this.options.onError(this, message, space);
  548. }
  549. if (this.logLevels.indexOf(level) < this.logLevels.indexOf(this.options.logLevel)) {
  550. return this; // skip logging
  551. }
  552. if (this.options.verbose) {
  553. var levelStr = this.colorizer.colorize('[' + level + ']', this.logStyles[level]);
  554. this.echo(levelStr + ' [' + space + '] ' + message); // direct output
  555. }
  556. this.result.log.push({
  557. level: level,
  558. space: space,
  559. message: message,
  560. date: new Date().toString()
  561. });
  562. return this;
  563. },
  564. /**
  565. * Opens a page. Takes only one argument, the url to open (using the
  566. * callback argument would defeat the whole purpose of Casper
  567. * actually).
  568. *
  569. * @param String location The url to open
  570. * @return Casper
  571. */
  572. open: function(location) {
  573. this.requestUrl = location;
  574. this.page.open(location);
  575. return this;
  576. },
  577. /**
  578. * Repeats a step a given number of times.
  579. *
  580. * @param Number times Number of times to repeat step
  581. * @aram function then The step closure
  582. * @return Casper
  583. * @see Casper#then
  584. */
  585. repeat: function(times, then) {
  586. for (var i = 0; i < times; i++) {
  587. this.then(then);
  588. }
  589. return this;
  590. },
  591. /**
  592. * Runs the whole suite of steps.
  593. *
  594. * @param function onComplete an optional callback
  595. * @param Number time an optional amount of milliseconds for interval checking
  596. * @return Casper
  597. */
  598. run: function(onComplete, time) {
  599. if (!this.steps || this.steps.length < 1) {
  600. this.log("No steps defined, aborting", "error");
  601. return this;
  602. }
  603. this.log("Running suite: " + this.steps.length + " step" + (this.steps.length > 1 ? "s" : ""), "info");
  604. this.checker = setInterval(this.checkStep, (time ? time: 250), this, onComplete);
  605. return this;
  606. },
  607. /**
  608. * Configures and starts Casper.
  609. *
  610. * @param String location An optional location to open on start
  611. * @param function then Next step function to execute on page loaded (optional)
  612. * @return Casper
  613. */
  614. start: function(location, then) {
  615. if (this.started) {
  616. this.log("start failed: Casper has already started!", "error");
  617. }
  618. this.log('Starting...', "info");
  619. this.startTime = new Date().getTime();
  620. this.steps = [];
  621. this.step = 0;
  622. // Option checks
  623. if (this.logLevels.indexOf(this.options.logLevel) < 0) {
  624. this.log("Unknown log level '" + this.options.logLevel + "', defaulting to 'warning'", "warning");
  625. this.options.logLevel = "warning";
  626. }
  627. // WebPage
  628. if (!isWebPage(this.page)) {
  629. if (isWebPage(this.options.page)) {
  630. this.page = this.options.page;
  631. } else {
  632. this.page = createPage(this);
  633. }
  634. }
  635. this.page.settings = mergeObjects(this.page.settings, this.options.pageSettings);
  636. if (isType(this.options.clipRect, "object")) {
  637. this.page.clipRect = this.options.clipRect;
  638. }
  639. if (isType(this.options.viewportSize, "object")) {
  640. this.page.viewportSize = this.options.viewportSize;
  641. }
  642. this.started = true;
  643. if (isType(this.options.timeout, "number") && this.options.timeout > 0) {
  644. self.log("execution timeout set to " + this.options.timeout + 'ms', "info");
  645. setTimeout(function(self) {
  646. self.log("timeout of " + self.options.timeout + "ms exceeded", "info").exit();
  647. }, this.options.timeout, this);
  648. }
  649. if (isType(this.options.onPageInitialized, "function")) {
  650. this.log("Post-configuring WebPage instance", "debug");
  651. this.options.onPageInitialized(this.page);
  652. }
  653. if (isType(location, "string") && location.length > 0) {
  654. if (isType(then, "function")) {
  655. return this.open(location).then(then);
  656. } else {
  657. return this.open(location);
  658. }
  659. }
  660. return this;
  661. },
  662. /**
  663. * Schedules the next step in the navigation process.
  664. *
  665. * @param function step A function to be called as a step
  666. * @return Casper
  667. */
  668. then: function(step) {
  669. if (!this.started) {
  670. throw "Casper not started; please use Casper#start";
  671. }
  672. if (!isType(step, "function")) {
  673. throw "You can only define a step as a function";
  674. }
  675. this.steps.push(step);
  676. return this;
  677. },
  678. /**
  679. * Adds a new navigation step for clicking on a provided link selector
  680. * and execute an optional next step.
  681. *
  682. * @param String selector A DOM CSS3 compatible selector
  683. * @param Function then Next step function to execute on page loaded (optional)
  684. * @param Boolean fallbackToHref Whether to try to relocate to the value of any href attribute (default: true)
  685. * @return Casper
  686. * @see Casper#click
  687. * @see Casper#then
  688. */
  689. thenClick: function(selector, then, fallbackToHref) {
  690. this.then(function(self) {
  691. self.click(selector, fallbackToHref);
  692. });
  693. return isType(then, "function") ? this.then(then) : this;
  694. },
  695. /**
  696. * Adds a new navigation step to perform code evaluation within the
  697. * current retrieved page DOM.
  698. *
  699. * @param function fn The function to be evaluated within current page DOM
  700. * @param object replacements Optional replacements to performs, eg. for '%foo%' => {foo: 'bar'}
  701. * @return Casper
  702. * @see Casper#evaluate
  703. */
  704. thenEvaluate: function(fn, replacements) {
  705. return this.then(function(self) {
  706. self.evaluate(fn, replacements);
  707. });
  708. },
  709. /**
  710. * Adds a new navigation step for opening the provided location.
  711. *
  712. * @param String location The URL to load
  713. * @param function then Next step function to execute on page loaded (optional)
  714. * @return Casper
  715. * @see Casper#open
  716. */
  717. thenOpen: function(location, then) {
  718. this.then(function(self) {
  719. self.open(location);
  720. });
  721. return isType(then, "function") ? this.then(then) : this;
  722. },
  723. /**
  724. * Adds a new navigation step for opening and evaluate an expression
  725. * against the DOM retrieved from the provided location.
  726. *
  727. * @param String location The url to open
  728. * @param function fn The function to be evaluated within current page DOM
  729. * @param object replacements Optional replacements to performs, eg. for '%foo%' => {foo: 'bar'}
  730. * @return Casper
  731. * @see Casper#evaluate
  732. * @see Casper#open
  733. */
  734. thenOpenAndEvaluate: function(location, fn, replacements) {
  735. return this.thenOpen(location).thenEvaluate(fn, replacements);
  736. },
  737. /**
  738. * Changes the current viewport size.
  739. *
  740. * @param Number width The viewport width, in pixels
  741. * @param Number height The viewport height, in pixels
  742. * @return Casper
  743. */
  744. viewport: function(width, height) {
  745. if (!isType(width, "number") || !isType(height, "number") || width <= 0 || height <= 0) {
  746. throw new Error("Invalid viewport width/height set: " + width + 'x' + height);
  747. }
  748. this.page.viewportSize = {
  749. width: width,
  750. height: height
  751. };
  752. return this;
  753. },
  754. /**
  755. * Adds a new step that will wait for a given amount of time (expressed
  756. * in milliseconds) before processing an optional next one.
  757. *
  758. * @param Number timeout The max amount of time to wait, in milliseconds
  759. * @param Function then Next step to process (optional)
  760. * @return Casper
  761. */
  762. wait: function(timeout, then) {
  763. timeout = Number(timeout, 10);
  764. if (!isType(timeout, "number") || timeout < 1) {
  765. this.die("wait() only accepts a positive integer > 0 as a timeout value");
  766. }
  767. if (then && !isType(then, "function")) {
  768. this.die("wait() a step definition must be a function");
  769. }
  770. return this.then(function(self) {
  771. self.delayedExecution = true;
  772. var start = new Date().getTime();
  773. var interval = setInterval(function(self, then) {
  774. if (new Date().getTime() - start > timeout) {
  775. self.delayedExecution = false;
  776. self.log("wait() finished wating for " + timeout + "ms.", "info");
  777. if (then) {
  778. self.then(then);
  779. }
  780. clearInterval(interval);
  781. }
  782. }, 100, self, then);
  783. });
  784. },
  785. /**
  786. * Waits until a function returns true to process a next step.
  787. *
  788. * @param Function testFx A function to be evaluated for returning condition satisfecit
  789. * @param Function then The next step to perform (optional)
  790. * @param Function onTimeout A callback function to call on timeout (optional)
  791. * @param Number timeout The max amount of time to wait, in milliseconds (optional)
  792. * @return Casper
  793. */
  794. waitFor: function(testFx, then, onTimeout, timeout) {
  795. timeout = timeout ? timeout : this.defaultWaitTimeout;
  796. if (!isType(testFx, "function")) {
  797. this.die("waitFor() needs a test function");
  798. }
  799. if (then && !isType(then, "function")) {
  800. this.die("waitFor() next step definition must be a function");
  801. }
  802. this.delayedExecution = true;
  803. var start = new Date().getTime();
  804. var condition = false;
  805. var interval = setInterval(function(self, testFx, onTimeout) {
  806. if ((new Date().getTime() - start < timeout) && !condition) {
  807. condition = testFx(self);
  808. } else {
  809. self.delayedExecution = false;
  810. if (!condition) {
  811. self.log("Casper.waitFor() timeout", "warning");
  812. if (isType(onTimeout, "function")) {
  813. onTimeout(self);
  814. } else {
  815. self.die("Expired timeout, exiting.", "error");
  816. }
  817. clearInterval(interval);
  818. } else {
  819. self.log("waitFor() finished in " + (new Date().getTime() - start) + "ms.", "info");
  820. if (then) {
  821. self.then(then);
  822. }
  823. clearInterval(interval);
  824. }
  825. }
  826. }, 100, this, testFx, onTimeout);
  827. return this;
  828. },
  829. /**
  830. * Waits until an element matching the provided CSS3 selector exists in
  831. * remote DOM to process a next step.
  832. *
  833. * @param String selector A CSS3 selector
  834. * @param Function then The next step to perform (optional)
  835. * @param Function onTimeout A callback function to call on timeout (optional)
  836. * @param Number timeout The max amount of time to wait, in milliseconds (optional)
  837. * @return Casper
  838. */
  839. waitForSelector: function(selector, then, onTimeout, timeout) {
  840. timeout = timeout ? timeout : this.defaultWaitTimeout;
  841. return this.waitFor(function(self) {
  842. return self.exists(selector);
  843. }, then, onTimeout, timeout);
  844. }
  845. };
  846. /**
  847. * Extends Casper's prototype with provided one.
  848. *
  849. * @param Object proto Prototype methods to add to Casper
  850. */
  851. phantom.Casper.extend = function(proto) {
  852. if (!isType(proto, "object")) {
  853. throw "extends() only accept objects as prototypes";
  854. }
  855. mergeObjects(phantom.Casper.prototype, proto);
  856. };
  857. /**
  858. * Casper client-side helpers.
  859. */
  860. phantom.Casper.ClientUtils = function() {
  861. /**
  862. * Clicks on the DOM element behind the provided selector.
  863. *
  864. * @param String selector A CSS3 selector to the element to click
  865. * @param Boolean fallbackToHref Whether to try to relocate to the value of any href attribute (default: true)
  866. * @return Boolean
  867. */
  868. this.click = function(selector, fallbackToHref) {
  869. fallbackToHref = typeof fallbackToHref === "undefined" ? true : !!fallbackToHref;
  870. var elem = this.findOne(selector);
  871. if (!elem) {
  872. return false;
  873. }
  874. var evt = document.createEvent("MouseEvents");
  875. evt.initMouseEvent("click", true, true, window, 1, 1, 1, 1, 1, false, false, false, false, 0, elem);
  876. if (elem.dispatchEvent(evt)) {
  877. return true;
  878. }
  879. if (fallbackToHref && elem.hasAttribute('href')) {
  880. document.location = elem.getAttribute('href');
  881. return true;
  882. }
  883. return false;
  884. };
  885. /**
  886. * Base64 encodes a string, even binary ones. Succeeds where
  887. * window.btoa() fails.
  888. *
  889. * @param String str
  890. * @return string
  891. */
  892. this.encode = function(str) {
  893. var CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
  894. var out = "", i = 0, len = str.length, c1, c2, c3;
  895. while (i < len) {
  896. c1 = str.charCodeAt(i++) & 0xff;
  897. if (i == len) {
  898. out += CHARS.charAt(c1 >> 2);
  899. out += CHARS.charAt((c1 & 0x3) << 4);
  900. out += "==";
  901. break;
  902. }
  903. c2 = str.charCodeAt(i++);
  904. if (i == len) {
  905. out += CHARS.charAt(c1 >> 2);
  906. out += CHARS.charAt(((c1 & 0x3)<< 4) | ((c2 & 0xF0) >> 4));
  907. out += CHARS.charAt((c2 & 0xF) << 2);
  908. out += "=";
  909. break;
  910. }
  911. c3 = str.charCodeAt(i++);
  912. out += CHARS.charAt(c1 >> 2);
  913. out += CHARS.charAt(((c1 & 0x3) << 4) | ((c2 & 0xF0) >> 4));
  914. out += CHARS.charAt(((c2 & 0xF) << 2) | ((c3 & 0xC0) >> 6));
  915. out += CHARS.charAt(c3 & 0x3F);
  916. }
  917. return out;
  918. };
  919. /**
  920. * Checks if a given DOM element exists in remote page.
  921. *
  922. * @param String selector CSS3 selector
  923. * @return Boolean
  924. */
  925. this.exists = function(selector) {
  926. try {
  927. return document.querySelectorAll(selector).length > 0;
  928. } catch (e) {
  929. return false;
  930. }
  931. };
  932. /**
  933. * Fetches innerText within the element(s) matching a given CSS3
  934. * selector.
  935. *
  936. * @param String selector A CSS3 selector
  937. * @return String
  938. */
  939. this.fetchText = function(selector) {
  940. var text = '', elements = this.findAll(selector);
  941. if (elements && elements.length) {
  942. Array.prototype.forEach.call(elements, function(element) {
  943. text += element.innerText;
  944. });
  945. }
  946. return text;
  947. };
  948. /**
  949. * Fills a form with provided field values, and optionnaly submits it.
  950. *
  951. * @param HTMLElement|String form A form element, or a CSS3 selector to a form element
  952. * @param Object vals Field values
  953. * @return Object An object containing setting result for each field, including file uploads
  954. */
  955. this.fill = function(form, vals) {
  956. var out = {
  957. errors: [],
  958. fields: [],
  959. files: []
  960. };
  961. if (!(form instanceof HTMLElement) || typeof form === "string") {
  962. __utils__.log("attempting to fetch form element from selector: '" + form + "'", "info");
  963. try {
  964. form = document.querySelector(form);
  965. } catch (e) {
  966. if (e.name === "SYNTAX_ERR") {
  967. out.errors.push("invalid form selector provided: '" + form + "'");
  968. return out;
  969. }
  970. }
  971. }
  972. if (!form) {
  973. out.errors.push("form not found");
  974. return out;
  975. }
  976. for (var name in vals) {
  977. if (!vals.hasOwnProperty(name)) {
  978. continue;
  979. }
  980. var field = form.querySelectorAll('[name="' + name + '"]');
  981. var value = vals[name];
  982. if (!field) {
  983. out.errors.push('no field named "' + name + '" in form');
  984. continue;
  985. }
  986. try {
  987. out.fields[name] = this.setField(field, value);
  988. } catch (err) {
  989. if (err.name === "FileUploadError") {
  990. out.files.push({
  991. name: name,
  992. path: err.path
  993. });
  994. } else {
  995. throw err;
  996. }
  997. }
  998. }
  999. return out;
  1000. };
  1001. /**
  1002. * Finds all DOM elements matching by the provided selector.
  1003. *
  1004. * @param String selector CSS3 selector
  1005. * @return NodeList|undefined
  1006. */
  1007. this.findAll = function(selector) {
  1008. try {
  1009. return document.querySelectorAll(selector);
  1010. } catch (e) {
  1011. this.log('findAll(): invalid selector provided "' + selector + '":' + e, "error");
  1012. }
  1013. };
  1014. /**
  1015. * Finds a DOM element by the provided selector.
  1016. *
  1017. * @param String selector CSS3 selector
  1018. * @return HTMLElement|undefined
  1019. */
  1020. this.findOne = function(selector) {
  1021. try {
  1022. return document.querySelector(selector);
  1023. } catch (e) {
  1024. this.log('findOne(): invalid selector provided "' + selector + '":' + e, "errors");
  1025. }
  1026. };
  1027. /**
  1028. * Downloads a resource behind an url and returns its base64-encoded
  1029. * contents.
  1030. *
  1031. * @param String url The resource url
  1032. * @return String Base64 contents string
  1033. */
  1034. this.getBase64 = function(url) {
  1035. return this.encode(this.getBinary(url));
  1036. };
  1037. /**
  1038. * Retrieves string contents from a binary file behind an url. Silently
  1039. * fails but log errors.
  1040. *
  1041. * @param String url
  1042. * @return string
  1043. */
  1044. this.getBinary = function(url) {
  1045. try {
  1046. var xhr = new XMLHttpRequest();
  1047. xhr.open("GET", url, false);
  1048. xhr.overrideMimeType("text/plain; charset=x-user-defined");
  1049. xhr.send(null);
  1050. return xhr.responseText;
  1051. } catch (e) {
  1052. if (e.name === "NETWORK_ERR" && e.code === 101) {
  1053. this.log("unfortunately, casperjs cannot make cross domain ajax requests", "warning");
  1054. }
  1055. this.log("error while fetching " + url + ": " + e, "error");
  1056. return "";
  1057. }
  1058. };
  1059. /**
  1060. * Logs a message.
  1061. *
  1062. * @param String message
  1063. * @param String level
  1064. */
  1065. this.log = function(message, level) {
  1066. console.log("[casper:" + (level || "debug") + "] " + message);
  1067. };
  1068. /**
  1069. * Sets a field (or a set of fields) value. Fails silently, but log
  1070. * error messages.
  1071. *
  1072. * @param HTMLElement|NodeList field One or more element defining a field
  1073. * @param mixed value The field value to set
  1074. */
  1075. this.setField = function(field, value) {
  1076. var fields, out;
  1077. value = value || "";
  1078. if (field instanceof NodeList) {
  1079. fields = field;
  1080. field = fields[0];
  1081. }
  1082. if (!field instanceof HTMLElement) {
  1083. this.log("invalid field type; only HTMLElement and NodeList are supported", "error");
  1084. }
  1085. this.log('set "' + field.getAttribute('name') + '" field value to ' + value, "debug");
  1086. try {
  1087. field.focus();
  1088. } catch (e) {
  1089. __utils__.log("Unable to focus() input field " + field.getAttribute('name') + ": " + e, "warning");
  1090. }
  1091. var nodeName = field.nodeName.toLowerCase();
  1092. switch (nodeName) {
  1093. case "input":
  1094. var type = field.getAttribute('type') || "text";
  1095. switch (type.toLowerCase()) {
  1096. case "color":
  1097. case "date":
  1098. case "datetime":
  1099. case "datetime-local":
  1100. case "email":
  1101. case "hidden":
  1102. case "month":
  1103. case "number":
  1104. case "password":
  1105. case "range":
  1106. case "search":
  1107. case "tel":
  1108. case "text":
  1109. case "time":
  1110. case "url":
  1111. case "week":
  1112. field.value = value;
  1113. break;
  1114. case "checkbox":
  1115. field.setAttribute('checked', value ? "checked" : "");
  1116. break;
  1117. case "file":
  1118. throw {
  1119. name: "FileUploadError",
  1120. message: "file field must be filled using page.uploadFile",
  1121. path: value
  1122. };
  1123. case "radio":
  1124. if (fields) {
  1125. Array.prototype.forEach.call(fields, function(e) {
  1126. e.checked = (e.value === value);
  1127. });
  1128. } else {
  1129. out = 'provided radio elements are empty';
  1130. }
  1131. break;
  1132. default:
  1133. out = "unsupported input field type: " + type;
  1134. break;
  1135. }
  1136. break;
  1137. case "select":
  1138. case "textarea":
  1139. field.value = value;
  1140. break;
  1141. default:
  1142. out = 'unsupported field type: ' + nodeName;
  1143. break;
  1144. }
  1145. try {
  1146. field.blur();
  1147. } catch (err) {
  1148. __utils__.log("Unable to blur() input field " + field.getAttribute('name') + ": " + err, "warning");
  1149. }
  1150. return out;
  1151. };
  1152. };
  1153. /**
  1154. * This is a port of lime colorizer.
  1155. * http://trac.symfony-project.org/browser/tools/lime/trunk/lib/lime.php)
  1156. *
  1157. * (c) Fabien Potencier, Symfony project, MIT license
  1158. */
  1159. phantom.Casper.Colorizer = function() {
  1160. var options = { bold: 1, underscore: 4, blink: 5, reverse: 7, conceal: 8 };
  1161. var foreground = { black: 30, red: 31, green: 32, yellow: 33, blue: 34, magenta: 35, cyan: 36, white: 37 };
  1162. var background = { black: 40, red: 41, green: 42, yellow: 43, blue: 44, magenta: 45, cyan: 46, white: 47 };
  1163. var styles = {
  1164. 'ERROR': { bg: 'red', fg: 'white', bold: true },
  1165. 'INFO': { fg: 'green', bold: true },
  1166. 'TRACE': { fg: 'green', bold: true },
  1167. 'PARAMETER': { fg: 'cyan' },
  1168. 'COMMENT': { fg: 'yellow' },
  1169. 'WARNING': { fg: 'red', bold: true },
  1170. 'GREEN_BAR': { fg: 'white', bg: 'green', bold: true },
  1171. 'RED_BAR': { fg: 'white', bg: 'red', bold: true },
  1172. 'INFO_BAR': { fg: 'cyan', bold: true }
  1173. };
  1174. /**
  1175. * Adds a style to provided text.
  1176. *
  1177. * @params String text
  1178. * @params String styleName
  1179. * @return String
  1180. */
  1181. this.colorize = function(text, styleName) {
  1182. if (styleName in styles) {
  1183. return this.format(text, styles[styleName]);
  1184. }
  1185. return text;
  1186. };
  1187. /**
  1188. * Formats a text using a style declaration object.
  1189. *
  1190. * @param String text
  1191. * @param Object style
  1192. * @return String
  1193. */
  1194. this.format = function(text, style) {
  1195. if (typeof style !== "object") {
  1196. return text;
  1197. }
  1198. var codes = [];
  1199. if (style.fg && foreground[style.fg]) {
  1200. codes.push(foreground[style.fg]);
  1201. }
  1202. if (style.bg && background[style.bg]) {
  1203. codes.push(background[style.bg]);
  1204. }
  1205. for (var option in options) {
  1206. if (style[option] === true) {
  1207. codes.push(options[option]);
  1208. }
  1209. }
  1210. return "\033[" + codes.join(';') + 'm' + text + "\033[0m";
  1211. };
  1212. };
  1213. /**
  1214. * Casper tester: makes assertions, stores test results and display them.
  1215. *
  1216. */
  1217. phantom.Casper.Tester = function(casper, options) {
  1218. this.options = isType(options, "object") ? options : {};
  1219. if (!casper instanceof phantom.Casper) {
  1220. throw "phantom.Casper.Tester needs a phantom.Casper instance";
  1221. }
  1222. // locals
  1223. var exporter = new phantom.Casper.XUnitExporter();
  1224. var PASS = this.options.PASS || "PASS";
  1225. var FAIL = this.options.FAIL || "FAIL";
  1226. // properties
  1227. this.testResults = {
  1228. passed: 0,
  1229. failed: 0
  1230. };
  1231. // methods
  1232. /**
  1233. * Asserts a condition resolves to true.
  1234. *
  1235. * @param Boolean condition
  1236. * @param String message Test description
  1237. */
  1238. this.assert = function(condition, message) {
  1239. var status = PASS;
  1240. if (condition === true) {
  1241. style = 'INFO';
  1242. this.testResults.passed++;
  1243. exporter.addSuccess("unknown", message);
  1244. } else {
  1245. status = FAIL;
  1246. style = 'RED_BAR';
  1247. this.testResults.failed++;
  1248. exporter.addFailure("unknown", message, 'test failed', "assert");
  1249. }
  1250. casper.echo([this.colorize(status, style), this.formatMessage(message)].join(' '));
  1251. };
  1252. /**
  1253. * Asserts that two values are strictly equals.
  1254. *
  1255. * @param Boolean testValue The value to test
  1256. * @param Boolean expected The expected value
  1257. * @param String message T…

Large files files are truncated, but you can click here to view the full file