refs #26 - code modularization
Showing
8 changed files
with
2175 additions
and
1967 deletions
... | @@ -25,1970 +25,17 @@ | ... | @@ -25,1970 +25,17 @@ |
25 | * DEALINGS IN THE SOFTWARE. | 25 | * DEALINGS IN THE SOFTWARE. |
26 | * | 26 | * |
27 | */ | 27 | */ |
28 | (function(phantom) { | 28 | var fs = require('fs'); |
29 | /** | 29 | |
30 | * Main Casper object. | 30 | function pathJoin() { |
31 | * | 31 | return Array.prototype.join.call(arguments, fs.separator); |
32 | * @param Object options Casper options | 32 | } |
33 | * @return Casper | 33 | |
34 | */ | 34 | var casperLibPath = pathJoin(fs.absolute('.'), 'lib'); |
35 | phantom.Casper = function(options) { | 35 | phantom.injectJs(pathJoin(casperLibPath, 'casper.js')); |
36 | var DEFAULT_DIE_MESSAGE = "Suite explicitely interrupted without any message given."; | 36 | phantom.injectJs(pathJoin(casperLibPath, 'clientutils.js')); |
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"; | 37 | phantom.injectJs(pathJoin(casperLibPath, 'colorizer.js')); |
38 | // init & checks | 38 | phantom.injectJs(pathJoin(casperLibPath, 'injector.js')); |
39 | if (!(this instanceof arguments.callee)) { | 39 | phantom.injectJs(pathJoin(casperLibPath, 'tester.js')); |
40 | return new Casper(options); | 40 | phantom.injectJs(pathJoin(casperLibPath, 'utils.js')); |
41 | } | 41 | phantom.injectJs(pathJoin(casperLibPath, 'xunit.js')); |
42 | // default options | ||
43 | this.defaults = { | ||
44 | clientScripts: [], | ||
45 | faultTolerant: true, | ||
46 | logLevel: "error", | ||
47 | httpStatusHandlers: {}, | ||
48 | onAlert: null, | ||
49 | onDie: null, | ||
50 | onError: null, | ||
51 | onLoadError: null, | ||
52 | onPageInitialized: null, | ||
53 | onResourceReceived: null, | ||
54 | onResourceRequested: null, | ||
55 | onStepComplete: null, | ||
56 | onStepTimeout: null, | ||
57 | onTimeout: null, | ||
58 | page: null, | ||
59 | pageSettings: { userAgent: DEFAULT_USER_AGENT }, | ||
60 | stepTimeout: null, | ||
61 | timeout: null, | ||
62 | verbose: false | ||
63 | }; | ||
64 | // privates | ||
65 | // local properties | ||
66 | this.checker = null; | ||
67 | this.colorizer = new phantom.Casper.Colorizer(); | ||
68 | this.currentUrl = 'about:blank'; | ||
69 | this.currentHTTPStatus = 200; | ||
70 | this.defaultWaitTimeout = 5000; | ||
71 | this.delayedExecution = false; | ||
72 | this.history = []; | ||
73 | this.loadInProgress = false; | ||
74 | this.logFormats = {}; | ||
75 | this.logLevels = ["debug", "info", "warning", "error"]; | ||
76 | this.logStyles = { | ||
77 | debug: 'INFO', | ||
78 | info: 'PARAMETER', | ||
79 | warning: 'COMMENT', | ||
80 | error: 'ERROR' | ||
81 | }; | ||
82 | this.options = mergeObjects(this.defaults, options); | ||
83 | this.page = null; | ||
84 | this.requestUrl = 'about:blank'; | ||
85 | this.result = { | ||
86 | log: [], | ||
87 | status: "success", | ||
88 | time: 0 | ||
89 | }; | ||
90 | this.started = false; | ||
91 | this.step = 0; | ||
92 | this.steps = []; | ||
93 | this.test = new phantom.Casper.Tester(this); | ||
94 | }; | ||
95 | |||
96 | /** | ||
97 | * Casper prototype | ||
98 | */ | ||
99 | phantom.Casper.prototype = { | ||
100 | /** | ||
101 | * Go a step back in browser's history | ||
102 | * | ||
103 | * @return Casper | ||
104 | */ | ||
105 | back: function() { | ||
106 | return this.then(function(self) { | ||
107 | self.evaluate(function() { | ||
108 | history.back(); | ||
109 | }); | ||
110 | }); | ||
111 | }, | ||
112 | |||
113 | /** | ||
114 | * Encodes a resource using the base64 algorithm synchroneously using | ||
115 | * client-side XMLHttpRequest. | ||
116 | * | ||
117 | * NOTE: we cannot use window.btoa() for some strange reasons here. | ||
118 | * | ||
119 | * @param String url The url to download | ||
120 | * @return string Base64 encoded result | ||
121 | */ | ||
122 | base64encode: function(url) { | ||
123 | return this.evaluate(function(url) { | ||
124 | return __utils__.getBase64(url); | ||
125 | }, { url: url }); | ||
126 | }, | ||
127 | |||
128 | /** | ||
129 | * Proxy method for WebPage#render. Adds a clipRect parameter for | ||
130 | * automatically set page clipRect setting values and sets it back once | ||
131 | * done. If the cliprect parameter is omitted, the full page viewport | ||
132 | * area will be rendered. | ||
133 | * | ||
134 | * @param String targetFile A target filename | ||
135 | * @param mixed clipRect An optional clipRect object (optional) | ||
136 | * @return Casper | ||
137 | */ | ||
138 | capture: function(targetFile, clipRect) { | ||
139 | var previousClipRect; | ||
140 | if (clipRect) { | ||
141 | if (!isType(clipRect, "object")) { | ||
142 | throw new Error("clipRect must be an Object instance."); | ||
143 | } | ||
144 | previousClipRect = this.page.clipRect; | ||
145 | this.page.clipRect = clipRect; | ||
146 | this.log('Capturing page to ' + targetFile + ' with clipRect' + JSON.stringify(clipRect), "debug"); | ||
147 | } else { | ||
148 | this.log('Capturing page to ' + targetFile, "debug"); | ||
149 | } | ||
150 | try { | ||
151 | this.page.render(targetFile); | ||
152 | } catch (e) { | ||
153 | this.log('Failed to capture screenshot as ' + targetFile + ': ' + e, "error"); | ||
154 | } | ||
155 | if (previousClipRect) { | ||
156 | this.page.clipRect = previousClipRect; | ||
157 | } | ||
158 | return this; | ||
159 | }, | ||
160 | |||
161 | /** | ||
162 | * Captures the page area containing the provided selector. | ||
163 | * | ||
164 | * @param String targetFile Target destination file path. | ||
165 | * @param String selector CSS3 selector | ||
166 | * @return Casper | ||
167 | */ | ||
168 | captureSelector: function(targetFile, selector) { | ||
169 | return this.capture(targetFile, this.evaluate(function(selector) { | ||
170 | try { | ||
171 | var clipRect = document.querySelector(selector).getBoundingClientRect(); | ||
172 | return { | ||
173 | top: clipRect.top, | ||
174 | left: clipRect.left, | ||
175 | width: clipRect.width, | ||
176 | height: clipRect.height | ||
177 | }; | ||
178 | } catch (e) { | ||
179 | __utils__.log("Unable to fetch bounds for element " + selector, "warning"); | ||
180 | } | ||
181 | }, { selector: selector })); | ||
182 | }, | ||
183 | |||
184 | /** | ||
185 | * Checks for any further navigation step to process. | ||
186 | * | ||
187 | * @param Casper self A self reference | ||
188 | * @param function onComplete An options callback to apply on completion | ||
189 | */ | ||
190 | checkStep: function(self, onComplete) { | ||
191 | var step = self.steps[self.step]; | ||
192 | if (!self.loadInProgress && isType(step, "function")) { | ||
193 | self.runStep(step); | ||
194 | } | ||
195 | if (!isType(step, "function") && !self.delayedExecution) { | ||
196 | self.result.time = new Date().getTime() - self.startTime; | ||
197 | self.log("Done " + self.steps.length + " steps in " + self.result.time + 'ms.', "info"); | ||
198 | clearInterval(self.checker); | ||
199 | if (isType(onComplete, "function")) { | ||
200 | try { | ||
201 | onComplete.call(self, self); | ||
202 | } catch (err) { | ||
203 | self.log("could not complete final step: " + err, "error"); | ||
204 | } | ||
205 | } else { | ||
206 | // default behavior is to exit phantom | ||
207 | self.exit(); | ||
208 | } | ||
209 | } | ||
210 | }, | ||
211 | |||
212 | /** | ||
213 | * Emulates a click on the element from the provided selector, if | ||
214 | * possible. In case of success, `true` is returned. | ||
215 | * | ||
216 | * @param String selector A DOM CSS3 compatible selector | ||
217 | * @param Boolean fallbackToHref Whether to try to relocate to the value of any href attribute (default: true) | ||
218 | * @return Boolean | ||
219 | */ | ||
220 | click: function(selector, fallbackToHref) { | ||
221 | fallbackToHref = isType(fallbackToHref, "undefined") ? true : !!fallbackToHref; | ||
222 | this.log("click on selector: " + selector, "debug"); | ||
223 | return this.evaluate(function(selector, fallbackToHref) { | ||
224 | return __utils__.click(selector, fallbackToHref); | ||
225 | }, { | ||
226 | selector: selector, | ||
227 | fallbackToHref: fallbackToHref | ||
228 | }); | ||
229 | }, | ||
230 | |||
231 | /** | ||
232 | * Creates a step definition. | ||
233 | * | ||
234 | * @param Function fn The step function to call | ||
235 | * @param Object options Step options | ||
236 | * @return Function The final step function | ||
237 | */ | ||
238 | createStep: function(fn, options) { | ||
239 | if (!isType(fn, "function")) { | ||
240 | throw "createStep(): a step definition must be a function"; | ||
241 | } | ||
242 | fn.options = isType(options, "object") ? options : {}; | ||
243 | return fn; | ||
244 | }, | ||
245 | |||
246 | /** | ||
247 | * Logs the HTML code of the current page. | ||
248 | * | ||
249 | * @return Casper | ||
250 | */ | ||
251 | debugHTML: function() { | ||
252 | this.echo(this.evaluate(function() { | ||
253 | return document.body.innerHTML; | ||
254 | })); | ||
255 | return this; | ||
256 | }, | ||
257 | |||
258 | /** | ||
259 | * Logs the textual contents of the current page. | ||
260 | * | ||
261 | * @return Casper | ||
262 | */ | ||
263 | debugPage: function() { | ||
264 | this.echo(this.evaluate(function() { | ||
265 | return document.body.innerText; | ||
266 | })); | ||
267 | return this; | ||
268 | }, | ||
269 | |||
270 | /** | ||
271 | * Exit phantom on failure, with a logged error message. | ||
272 | * | ||
273 | * @param String message An optional error message | ||
274 | * @param Number status An optional exit status code (must be > 0) | ||
275 | * @return Casper | ||
276 | */ | ||
277 | die: function(message, status) { | ||
278 | this.result.status = 'error'; | ||
279 | this.result.time = new Date().getTime() - this.startTime; | ||
280 | message = isType(message, "string") && message.length > 0 ? message : DEFAULT_DIE_MESSAGE; | ||
281 | this.log(message, "error"); | ||
282 | if (isType(this.options.onDie, "function")) { | ||
283 | this.options.onDie.call(this, this, message, status); | ||
284 | } | ||
285 | return this.exit(Number(status) > 0 ? Number(status) : 1); | ||
286 | }, | ||
287 | |||
288 | /** | ||
289 | * Iterates over the values of a provided array and execute a callback | ||
290 | * for each item. | ||
291 | * | ||
292 | * @param Array array | ||
293 | * @param Function fn Callback: function(self, item, index) | ||
294 | * @return Casper | ||
295 | */ | ||
296 | each: function(array, fn) { | ||
297 | if (!isType(array, "array")) { | ||
298 | self.log("each() only works with arrays", "error"); | ||
299 | return this; | ||
300 | } | ||
301 | (function(self) { | ||
302 | array.forEach(function(item, i) { | ||
303 | fn(self, item, i); | ||
304 | }); | ||
305 | })(this); | ||
306 | return this; | ||
307 | }, | ||
308 | |||
309 | /** | ||
310 | * Prints something to stdout. | ||
311 | * | ||
312 | * @param String text A string to echo to stdout | ||
313 | * @return Casper | ||
314 | */ | ||
315 | echo: function(text, style) { | ||
316 | console.log(style ? this.colorizer.colorize(text, style) : text); | ||
317 | return this; | ||
318 | }, | ||
319 | |||
320 | /** | ||
321 | * Evaluates an expression in the page context, a bit like what | ||
322 | * WebPage#evaluate does, but the passed function can also accept | ||
323 | * parameters if a context Object is also passed: | ||
324 | * | ||
325 | * casper.evaluate(function(username, password) { | ||
326 | * document.querySelector('#username').value = username; | ||
327 | * document.querySelector('#password').value = password; | ||
328 | * document.querySelector('#submit').click(); | ||
329 | * }, { | ||
330 | * username: 'Bazoonga', | ||
331 | * password: 'baz00nga' | ||
332 | * }) | ||
333 | * | ||
334 | * FIXME: waiting for a patch of PhantomJS to allow direct passing of | ||
335 | * arguments to the function. | ||
336 | * TODO: don't forget to keep this backward compatible. | ||
337 | * | ||
338 | * @param Function fn The function to be evaluated within current page DOM | ||
339 | * @param Object context Object containing the parameters to inject into the function | ||
340 | * @return mixed | ||
341 | * @see WebPage#evaluate | ||
342 | */ | ||
343 | evaluate: function(fn, context) { | ||
344 | context = isType(context, "object") ? context : {}; | ||
345 | var newFn = new phantom.Casper.FunctionArgsInjector(fn).process(context); | ||
346 | return this.page.evaluate(newFn); | ||
347 | }, | ||
348 | |||
349 | /** | ||
350 | * Evaluates an expression within the current page DOM and die() if it | ||
351 | * returns false. | ||
352 | * | ||
353 | * @param function fn The expression to evaluate | ||
354 | * @param String message The error message to log | ||
355 | * @return Casper | ||
356 | */ | ||
357 | evaluateOrDie: function(fn, message) { | ||
358 | if (!this.evaluate(fn)) { | ||
359 | return this.die(message); | ||
360 | } | ||
361 | return this; | ||
362 | }, | ||
363 | |||
364 | /** | ||
365 | * Checks if an element matching the provided CSS3 selector exists in | ||
366 | * current page DOM. | ||
367 | * | ||
368 | * @param String selector A CSS3 selector | ||
369 | * @return Boolean | ||
370 | */ | ||
371 | exists: function(selector) { | ||
372 | return this.evaluate(function(selector) { | ||
373 | return __utils__.exists(selector); | ||
374 | }, { selector: selector }); | ||
375 | }, | ||
376 | |||
377 | /** | ||
378 | * Checks if an element matching the provided CSS3 selector is visible | ||
379 | * current page DOM by checking that offsetWidth and offsetHeight are | ||
380 | * both non-zero. | ||
381 | * | ||
382 | * @param String selector A CSS3 selector | ||
383 | * @return Boolean | ||
384 | */ | ||
385 | visible: function(selector) { | ||
386 | return this.evaluate(function(selector) { | ||
387 | return __utils__.visible(selector); | ||
388 | }, { selector: selector }); | ||
389 | }, | ||
390 | |||
391 | /** | ||
392 | * Exits phantom. | ||
393 | * | ||
394 | * @param Number status Status | ||
395 | * @return Casper | ||
396 | */ | ||
397 | exit: function(status) { | ||
398 | phantom.exit(status); | ||
399 | return this; | ||
400 | }, | ||
401 | |||
402 | /** | ||
403 | * Fetches innerText within the element(s) matching a given CSS3 | ||
404 | * selector. | ||
405 | * | ||
406 | * @param String selector A CSS3 selector | ||
407 | * @return String | ||
408 | */ | ||
409 | fetchText: function(selector) { | ||
410 | return this.evaluate(function(selector) { | ||
411 | return __utils__.fetchText(selector); | ||
412 | }, { selector: selector }); | ||
413 | }, | ||
414 | |||
415 | /** | ||
416 | * Fills a form with provided field values. | ||
417 | * | ||
418 | * @param String selector A CSS3 selector to the target form to fill | ||
419 | * @param Object vals Field values | ||
420 | * @param Boolean submit Submit the form? | ||
421 | */ | ||
422 | fill: function(selector, vals, submit) { | ||
423 | submit = submit === true ? submit : false; | ||
424 | if (!isType(selector, "string") || !selector.length) { | ||
425 | throw "form selector must be a non-empty string"; | ||
426 | } | ||
427 | if (!isType(vals, "object")) { | ||
428 | throw "form values must be provided as an object"; | ||
429 | } | ||
430 | var fillResults = this.evaluate(function(selector, values) { | ||
431 | return __utils__.fill(selector, values); | ||
432 | }, { | ||
433 | selector: selector, | ||
434 | values: vals | ||
435 | }); | ||
436 | if (!fillResults) { | ||
437 | throw "unable to fill form"; | ||
438 | } else if (fillResults.errors.length > 0) { | ||
439 | (function(self){ | ||
440 | fillResults.errors.forEach(function(error) { | ||
441 | self.log("form error: " + error, "error"); | ||
442 | }); | ||
443 | })(this); | ||
444 | if (submit) { | ||
445 | this.log("errors encountered while filling form; submission aborted", "warning"); | ||
446 | submit = false; | ||
447 | } | ||
448 | } | ||
449 | // File uploads | ||
450 | if (fillResults.files && fillResults.files.length > 0) { | ||
451 | (function(self) { | ||
452 | fillResults.files.forEach(function(file) { | ||
453 | var fileFieldSelector = [selector, 'input[name="' + file.name + '"]'].join(' '); | ||
454 | self.page.uploadFile(fileFieldSelector, file.path); | ||
455 | }); | ||
456 | })(this); | ||
457 | } | ||
458 | // Form submission? | ||
459 | if (submit) { | ||
460 | this.evaluate(function(selector) { | ||
461 | var form = document.querySelector(selector); | ||
462 | var method = form.getAttribute('method').toUpperCase() || "GET"; | ||
463 | var action = form.getAttribute('action') || "unknown"; | ||
464 | __utils__.log('submitting form to ' + action + ', HTTP ' + method, 'info'); | ||
465 | form.submit(); | ||
466 | }, { selector: selector }); | ||
467 | } | ||
468 | }, | ||
469 | |||
470 | /** | ||
471 | * Go a step forward in browser's history | ||
472 | * | ||
473 | * @return Casper | ||
474 | */ | ||
475 | forward: function(then) { | ||
476 | return this.then(function(self) { | ||
477 | self.evaluate(function() { | ||
478 | history.forward(); | ||
479 | }); | ||
480 | }); | ||
481 | }, | ||
482 | |||
483 | /** | ||
484 | * Retrieves current document url. | ||
485 | * | ||
486 | * @return String | ||
487 | */ | ||
488 | getCurrentUrl: function() { | ||
489 | return decodeURIComponent(this.evaluate(function() { | ||
490 | return document.location.href; | ||
491 | })); | ||
492 | }, | ||
493 | |||
494 | /** | ||
495 | * Retrieves global variable. | ||
496 | * | ||
497 | * @param String name The name of the global variable to retrieve | ||
498 | * @return mixed | ||
499 | */ | ||
500 | getGlobal: function(name) { | ||
501 | var result = this.evaluate(function(name) { | ||
502 | var result = {}; | ||
503 | try { | ||
504 | result.value = JSON.stringify(window[name]); | ||
505 | } catch (e) { | ||
506 | result.error = 'Unable to JSON encode window.' + name + ': ' + e; | ||
507 | } | ||
508 | return result; | ||
509 | }, {'name': name}); | ||
510 | if (result.error) { | ||
511 | throw result.error; | ||
512 | } else { | ||
513 | return JSON.parse(result.value); | ||
514 | } | ||
515 | }, | ||
516 | |||
517 | /** | ||
518 | * Retrieves current page title, if any. | ||
519 | * | ||
520 | * @return String | ||
521 | */ | ||
522 | getTitle: function() { | ||
523 | return this.evaluate(function() { | ||
524 | return document.title; | ||
525 | }); | ||
526 | }, | ||
527 | |||
528 | /** | ||
529 | * Logs a message. | ||
530 | * | ||
531 | * @param String message The message to log | ||
532 | * @param String level The log message level (from Casper.logLevels property) | ||
533 | * @param String space Space from where the logged event occured (default: "phantom") | ||
534 | * @return Casper | ||
535 | */ | ||
536 | log: function(message, level, space) { | ||
537 | level = level && this.logLevels.indexOf(level) > -1 ? level : "debug"; | ||
538 | space = space ? space : "phantom"; | ||
539 | if (level === "error" && isType(this.options.onError, "function")) { | ||
540 | this.options.onError.call(this, this, message, space); | ||
541 | } | ||
542 | if (this.logLevels.indexOf(level) < this.logLevels.indexOf(this.options.logLevel)) { | ||
543 | return this; // skip logging | ||
544 | } | ||
545 | var entry = { | ||
546 | level: level, | ||
547 | space: space, | ||
548 | message: message, | ||
549 | date: new Date().toString() | ||
550 | }; | ||
551 | if (level in this.logFormats && isType(this.logFormats[level], "function")) { | ||
552 | message = this.logFormats[level](message, level, space); | ||
553 | } else { | ||
554 | var levelStr = this.colorizer.colorize('[' + level + ']', this.logStyles[level]); | ||
555 | message = levelStr + ' [' + space + '] ' + message; | ||
556 | } | ||
557 | if (this.options.verbose) { | ||
558 | this.echo(message); // direct output | ||
559 | } | ||
560 | this.result.log.push(entry); | ||
561 | return this; | ||
562 | }, | ||
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, options) { | ||
573 | options = isType(options, "object") ? options : {}; | ||
574 | this.requestUrl = location; | ||
575 | this.page.open(location); | ||
576 | return this; | ||
577 | }, | ||
578 | |||
579 | /** | ||
580 | * Repeats a step a given number of times. | ||
581 | * | ||
582 | * @param Number times Number of times to repeat step | ||
583 | * @aram function then The step closure | ||
584 | * @return Casper | ||
585 | * @see Casper#then | ||
586 | */ | ||
587 | repeat: function(times, then) { | ||
588 | for (var i = 0; i < times; i++) { | ||
589 | this.then(then); | ||
590 | } | ||
591 | return this; | ||
592 | }, | ||
593 | |||
594 | /** | ||
595 | * Runs the whole suite of steps. | ||
596 | * | ||
597 | * @param function onComplete an optional callback | ||
598 | * @param Number time an optional amount of milliseconds for interval checking | ||
599 | * @return Casper | ||
600 | */ | ||
601 | run: function(onComplete, time) { | ||
602 | if (!this.steps || this.steps.length < 1) { | ||
603 | this.log("No steps defined, aborting", "error"); | ||
604 | return this; | ||
605 | } | ||
606 | this.log("Running suite: " + this.steps.length + " step" + (this.steps.length > 1 ? "s" : ""), "info"); | ||
607 | this.checker = setInterval(this.checkStep, (time ? time: 250), this, onComplete); | ||
608 | return this; | ||
609 | }, | ||
610 | |||
611 | /** | ||
612 | * Runs a step. | ||
613 | * | ||
614 | * @param Function step | ||
615 | */ | ||
616 | runStep: function(step) { | ||
617 | var skipLog = isType(step.options, "object") && step.options.skipLog === true; | ||
618 | var stepInfo = "Step " + (this.step + 1) + "/" + this.steps.length; | ||
619 | var stepResult; | ||
620 | if (!skipLog) { | ||
621 | this.log(stepInfo + ' ' + this.getCurrentUrl() + ' (HTTP ' + this.currentHTTPStatus + ')', "info"); | ||
622 | } | ||
623 | if (isType(this.options.stepTimeout, "number") && this.options.stepTimeout > 0) { | ||
624 | var stepTimeoutCheckInterval = setInterval(function(self, start, stepNum) { | ||
625 | if (new Date().getTime() - start > self.options.stepTimeout) { | ||
626 | if (self.step == stepNum + 1) { | ||
627 | if (isType(self.options.onStepTimeout, "function")) { | ||
628 | self.options.onStepTimeout.call(self, self); | ||
629 | } else { | ||
630 | self.die("Maximum step execution timeout exceeded for step " + stepNum, "error"); | ||
631 | } | ||
632 | } | ||
633 | clearInterval(stepTimeoutCheckInterval); | ||
634 | } | ||
635 | }, this.options.stepTimeout, this, new Date().getTime(), this.step); | ||
636 | } | ||
637 | try { | ||
638 | stepResult = step.call(this, this); | ||
639 | } catch (e) { | ||
640 | if (this.options.faultTolerant) { | ||
641 | this.log("Step error: " + e, "error"); | ||
642 | } else { | ||
643 | throw e; | ||
644 | } | ||
645 | } | ||
646 | if (isType(this.options.onStepComplete, "function")) { | ||
647 | this.options.onStepComplete.call(this, this, stepResult); | ||
648 | } | ||
649 | if (!skipLog) { | ||
650 | this.log(stepInfo + ": done in " + (new Date().getTime() - this.startTime) + "ms.", "info"); | ||
651 | } | ||
652 | this.step++; | ||
653 | }, | ||
654 | |||
655 | /** | ||
656 | * Configures and starts Casper. | ||
657 | * | ||
658 | * @param String location An optional location to open on start | ||
659 | * @param function then Next step function to execute on page loaded (optional) | ||
660 | * @return Casper | ||
661 | */ | ||
662 | start: function(location, then) { | ||
663 | if (this.started) { | ||
664 | this.log("start failed: Casper has already started!", "error"); | ||
665 | } | ||
666 | this.log('Starting...', "info"); | ||
667 | this.startTime = new Date().getTime(); | ||
668 | this.steps = []; | ||
669 | this.step = 0; | ||
670 | // Option checks | ||
671 | if (this.logLevels.indexOf(this.options.logLevel) < 0) { | ||
672 | this.log("Unknown log level '" + this.options.logLevel + "', defaulting to 'warning'", "warning"); | ||
673 | this.options.logLevel = "warning"; | ||
674 | } | ||
675 | // WebPage | ||
676 | if (!isWebPage(this.page)) { | ||
677 | if (isWebPage(this.options.page)) { | ||
678 | this.page = this.options.page; | ||
679 | } else { | ||
680 | this.page = createPage(this); | ||
681 | } | ||
682 | } | ||
683 | this.page.settings = mergeObjects(this.page.settings, this.options.pageSettings); | ||
684 | if (isType(this.options.clipRect, "object")) { | ||
685 | this.page.clipRect = this.options.clipRect; | ||
686 | } | ||
687 | if (isType(this.options.viewportSize, "object")) { | ||
688 | this.page.viewportSize = this.options.viewportSize; | ||
689 | } | ||
690 | this.started = true; | ||
691 | if (isType(this.options.timeout, "number") && this.options.timeout > 0) { | ||
692 | this.log("Execution timeout set to " + this.options.timeout + 'ms', "info"); | ||
693 | setTimeout(function(self) { | ||
694 | if (isType(self.options.onTimeout, "function")) { | ||
695 | self.options.onTimeout.call(self, self); | ||
696 | } else { | ||
697 | self.die("Timeout of " + self.options.timeout + "ms exceeded, exiting."); | ||
698 | } | ||
699 | }, this.options.timeout, this); | ||
700 | } | ||
701 | if (isType(this.options.onPageInitialized, "function")) { | ||
702 | this.log("Post-configuring WebPage instance", "debug"); | ||
703 | this.options.onPageInitialized.call(this, this.page); | ||
704 | } | ||
705 | if (isType(location, "string") && location.length > 0) { | ||
706 | return this.thenOpen(location, isType(then, "function") ? then : this.createStep(function(self) { | ||
707 | self.log("start page is loaded", "debug"); | ||
708 | })); | ||
709 | } | ||
710 | return this; | ||
711 | }, | ||
712 | |||
713 | /** | ||
714 | * Schedules the next step in the navigation process. | ||
715 | * | ||
716 | * @param function step A function to be called as a step | ||
717 | * @return Casper | ||
718 | */ | ||
719 | then: function(step) { | ||
720 | if (!this.started) { | ||
721 | throw "Casper not started; please use Casper#start"; | ||
722 | } | ||
723 | if (!isType(step, "function")) { | ||
724 | throw "You can only define a step as a function"; | ||
725 | } | ||
726 | this.steps.push(step); | ||
727 | return this; | ||
728 | }, | ||
729 | |||
730 | /** | ||
731 | * Adds a new navigation step for clicking on a provided link selector | ||
732 | * and execute an optional next step. | ||
733 | * | ||
734 | * @param String selector A DOM CSS3 compatible selector | ||
735 | * @param Function then Next step function to execute on page loaded (optional) | ||
736 | * @param Boolean fallbackToHref Whether to try to relocate to the value of any href attribute (default: true) | ||
737 | * @return Casper | ||
738 | * @see Casper#click | ||
739 | * @see Casper#then | ||
740 | */ | ||
741 | thenClick: function(selector, then, fallbackToHref) { | ||
742 | this.then(function(self) { | ||
743 | self.click(selector, fallbackToHref); | ||
744 | }); | ||
745 | return isType(then, "function") ? this.then(then) : this; | ||
746 | }, | ||
747 | |||
748 | /** | ||
749 | * Adds a new navigation step to perform code evaluation within the | ||
750 | * current retrieved page DOM. | ||
751 | * | ||
752 | * @param function fn The function to be evaluated within current page DOM | ||
753 | * @param object context Optional function parameters context | ||
754 | * @return Casper | ||
755 | * @see Casper#evaluate | ||
756 | */ | ||
757 | thenEvaluate: function(fn, context) { | ||
758 | return this.then(function(self) { | ||
759 | self.evaluate(fn, context); | ||
760 | }); | ||
761 | }, | ||
762 | |||
763 | /** | ||
764 | * Adds a new navigation step for opening the provided location. | ||
765 | * | ||
766 | * @param String location The URL to load | ||
767 | * @param function then Next step function to execute on page loaded (optional) | ||
768 | * @return Casper | ||
769 | * @see Casper#open | ||
770 | */ | ||
771 | thenOpen: function(location, then) { | ||
772 | this.then(this.createStep(function(self) { | ||
773 | self.open(location); | ||
774 | }, { | ||
775 | skipLog: true | ||
776 | })); | ||
777 | return isType(then, "function") ? this.then(then) : this; | ||
778 | }, | ||
779 | |||
780 | /** | ||
781 | * Adds a new navigation step for opening and evaluate an expression | ||
782 | * against the DOM retrieved from the provided location. | ||
783 | * | ||
784 | * @param String location The url to open | ||
785 | * @param function fn The function to be evaluated within current page DOM | ||
786 | * @param object context Optional function parameters context | ||
787 | * @return Casper | ||
788 | * @see Casper#evaluate | ||
789 | * @see Casper#open | ||
790 | */ | ||
791 | thenOpenAndEvaluate: function(location, fn, context) { | ||
792 | return this.thenOpen(location).thenEvaluate(fn, context); | ||
793 | }, | ||
794 | |||
795 | /** | ||
796 | * Changes the current viewport size. | ||
797 | * | ||
798 | * @param Number width The viewport width, in pixels | ||
799 | * @param Number height The viewport height, in pixels | ||
800 | * @return Casper | ||
801 | */ | ||
802 | viewport: function(width, height) { | ||
803 | if (!isType(width, "number") || !isType(height, "number") || width <= 0 || height <= 0) { | ||
804 | throw new Error("Invalid viewport width/height set: " + width + 'x' + height); | ||
805 | } | ||
806 | this.page.viewportSize = { | ||
807 | width: width, | ||
808 | height: height | ||
809 | }; | ||
810 | return this; | ||
811 | }, | ||
812 | |||
813 | /** | ||
814 | * Adds a new step that will wait for a given amount of time (expressed | ||
815 | * in milliseconds) before processing an optional next one. | ||
816 | * | ||
817 | * @param Number timeout The max amount of time to wait, in milliseconds | ||
818 | * @param Function then Next step to process (optional) | ||
819 | * @return Casper | ||
820 | */ | ||
821 | wait: function(timeout, then) { | ||
822 | timeout = Number(timeout, 10); | ||
823 | if (!isType(timeout, "number") || timeout < 1) { | ||
824 | this.die("wait() only accepts a positive integer > 0 as a timeout value"); | ||
825 | } | ||
826 | if (then && !isType(then, "function")) { | ||
827 | this.die("wait() a step definition must be a function"); | ||
828 | } | ||
829 | return this.then(function(self) { | ||
830 | self.delayedExecution = true; | ||
831 | var start = new Date().getTime(); | ||
832 | var interval = setInterval(function(self, then) { | ||
833 | if (new Date().getTime() - start > timeout) { | ||
834 | self.delayedExecution = false; | ||
835 | self.log("wait() finished wating for " + timeout + "ms.", "info"); | ||
836 | if (then) { | ||
837 | self.then(then); | ||
838 | } | ||
839 | clearInterval(interval); | ||
840 | } | ||
841 | }, 100, self, then); | ||
842 | }); | ||
843 | }, | ||
844 | |||
845 | /** | ||
846 | * Waits until a function returns true to process a next step. | ||
847 | * | ||
848 | * @param Function testFx A function to be evaluated for returning condition satisfecit | ||
849 | * @param Function then The next step to perform (optional) | ||
850 | * @param Function onTimeout A callback function to call on timeout (optional) | ||
851 | * @param Number timeout The max amount of time to wait, in milliseconds (optional) | ||
852 | * @return Casper | ||
853 | */ | ||
854 | waitFor: function(testFx, then, onTimeout, timeout) { | ||
855 | timeout = timeout ? timeout : this.defaultWaitTimeout; | ||
856 | if (!isType(testFx, "function")) { | ||
857 | this.die("waitFor() needs a test function"); | ||
858 | } | ||
859 | if (then && !isType(then, "function")) { | ||
860 | this.die("waitFor() next step definition must be a function"); | ||
861 | } | ||
862 | this.delayedExecution = true; | ||
863 | var start = new Date().getTime(); | ||
864 | var condition = false; | ||
865 | var interval = setInterval(function(self, testFx, onTimeout) { | ||
866 | if ((new Date().getTime() - start < timeout) && !condition) { | ||
867 | condition = testFx(self); | ||
868 | } else { | ||
869 | self.delayedExecution = false; | ||
870 | if (!condition) { | ||
871 | self.log("Casper.waitFor() timeout", "warning"); | ||
872 | if (isType(onTimeout, "function")) { | ||
873 | onTimeout.call(self, self); | ||
874 | } else { | ||
875 | self.die("Expired timeout, exiting.", "error"); | ||
876 | } | ||
877 | clearInterval(interval); | ||
878 | } else { | ||
879 | self.log("waitFor() finished in " + (new Date().getTime() - start) + "ms.", "info"); | ||
880 | if (then) { | ||
881 | self.then(then); | ||
882 | } | ||
883 | clearInterval(interval); | ||
884 | } | ||
885 | } | ||
886 | }, 100, this, testFx, onTimeout); | ||
887 | return this; | ||
888 | }, | ||
889 | |||
890 | /** | ||
891 | * Waits until an element matching the provided CSS3 selector exists in | ||
892 | * remote DOM to process a next step. | ||
893 | * | ||
894 | * @param String selector A CSS3 selector | ||
895 | * @param Function then The next step to perform (optional) | ||
896 | * @param Function onTimeout A callback function to call on timeout (optional) | ||
897 | * @param Number timeout The max amount of time to wait, in milliseconds (optional) | ||
898 | * @return Casper | ||
899 | */ | ||
900 | waitForSelector: function(selector, then, onTimeout, timeout) { | ||
901 | timeout = timeout ? timeout : this.defaultWaitTimeout; | ||
902 | return this.waitFor(function(self) { | ||
903 | return self.exists(selector); | ||
904 | }, then, onTimeout, timeout); | ||
905 | }, | ||
906 | |||
907 | /** | ||
908 | * Waits until an element matching the provided CSS3 selector does not | ||
909 | * exist in the remote DOM to process a next step. | ||
910 | * | ||
911 | * @param String selector A CSS3 selector | ||
912 | * @param Function then The next step to perform (optional) | ||
913 | * @param Function onTimeout A callback function to call on timeout (optional) | ||
914 | * @param Number timeout The max amount of time to wait, in milliseconds (optional) | ||
915 | * @return Casper | ||
916 | */ | ||
917 | waitWhileSelector: function(selector, then, onTimeout, timeout) { | ||
918 | timeout = timeout ? timeout : this.defaultWaitTimeout; | ||
919 | return this.waitFor(function(self) { | ||
920 | return !self.exists(selector); | ||
921 | }, then, onTimeout, timeout); | ||
922 | }, | ||
923 | |||
924 | /** | ||
925 | * Waits until an element matching the provided CSS3 selector is | ||
926 | * visible in the remote DOM to process a next step. | ||
927 | * | ||
928 | * @param String selector A CSS3 selector | ||
929 | * @param Function then The next step to perform (optional) | ||
930 | * @param Function onTimeout A callback function to call on timeout (optional) | ||
931 | * @param Number timeout The max amount of time to wait, in milliseconds (optional) | ||
932 | * @return Casper | ||
933 | */ | ||
934 | waitUntilVisible: function(selector, then, onTimeout, timeout) { | ||
935 | timeout = timeout ? timeout : this.defaultWaitTimeout; | ||
936 | return this.waitFor(function(self) { | ||
937 | return self.visible(selector); | ||
938 | }, then, onTimeout, timeout); | ||
939 | }, | ||
940 | |||
941 | /** | ||
942 | * Waits until an element matching the provided CSS3 selector is no | ||
943 | * longer visible in remote DOM to process a next step. | ||
944 | * | ||
945 | * @param String selector A CSS3 selector | ||
946 | * @param Function then The next step to perform (optional) | ||
947 | * @param Function onTimeout A callback function to call on timeout (optional) | ||
948 | * @param Number timeout The max amount of time to wait, in milliseconds (optional) | ||
949 | * @return Casper | ||
950 | */ | ||
951 | waitWhileVisible: function(selector, then, onTimeout, timeout) { | ||
952 | timeout = timeout ? timeout : this.defaultWaitTimeout; | ||
953 | return this.waitFor(function(self) { | ||
954 | return !self.visible(selector); | ||
955 | }, then, onTimeout, timeout); | ||
956 | } | ||
957 | }; | ||
958 | |||
959 | /** | ||
960 | * Extends Casper's prototype with provided one. | ||
961 | * | ||
962 | * @param Object proto Prototype methods to add to Casper | ||
963 | */ | ||
964 | phantom.Casper.extend = function(proto) { | ||
965 | if (!isType(proto, "object")) { | ||
966 | throw "extends() only accept objects as prototypes"; | ||
967 | } | ||
968 | mergeObjects(phantom.Casper.prototype, proto); | ||
969 | }; | ||
970 | |||
971 | /** | ||
972 | * Casper client-side helpers. | ||
973 | */ | ||
974 | phantom.Casper.ClientUtils = function() { | ||
975 | /** | ||
976 | * Clicks on the DOM element behind the provided selector. | ||
977 | * | ||
978 | * @param String selector A CSS3 selector to the element to click | ||
979 | * @param Boolean fallbackToHref Whether to try to relocate to the value of any href attribute (default: true) | ||
980 | * @return Boolean | ||
981 | */ | ||
982 | this.click = function(selector, fallbackToHref) { | ||
983 | fallbackToHref = typeof fallbackToHref === "undefined" ? true : !!fallbackToHref; | ||
984 | var elem = this.findOne(selector); | ||
985 | if (!elem) { | ||
986 | return false; | ||
987 | } | ||
988 | var evt = document.createEvent("MouseEvents"); | ||
989 | evt.initMouseEvent("click", true, true, window, 1, 1, 1, 1, 1, false, false, false, false, 0, elem); | ||
990 | if (elem.dispatchEvent(evt)) { | ||
991 | return true; | ||
992 | } | ||
993 | if (fallbackToHref && elem.hasAttribute('href')) { | ||
994 | document.location = elem.getAttribute('href'); | ||
995 | return true; | ||
996 | } | ||
997 | return false; | ||
998 | }; | ||
999 | |||
1000 | /** | ||
1001 | * Base64 encodes a string, even binary ones. Succeeds where | ||
1002 | * window.btoa() fails. | ||
1003 | * | ||
1004 | * @param String str | ||
1005 | * @return string | ||
1006 | */ | ||
1007 | this.encode = function(str) { | ||
1008 | var CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; | ||
1009 | var out = "", i = 0, len = str.length, c1, c2, c3; | ||
1010 | while (i < len) { | ||
1011 | c1 = str.charCodeAt(i++) & 0xff; | ||
1012 | if (i == len) { | ||
1013 | out += CHARS.charAt(c1 >> 2); | ||
1014 | out += CHARS.charAt((c1 & 0x3) << 4); | ||
1015 | out += "=="; | ||
1016 | break; | ||
1017 | } | ||
1018 | c2 = str.charCodeAt(i++); | ||
1019 | if (i == len) { | ||
1020 | out += CHARS.charAt(c1 >> 2); | ||
1021 | out += CHARS.charAt(((c1 & 0x3)<< 4) | ((c2 & 0xF0) >> 4)); | ||
1022 | out += CHARS.charAt((c2 & 0xF) << 2); | ||
1023 | out += "="; | ||
1024 | break; | ||
1025 | } | ||
1026 | c3 = str.charCodeAt(i++); | ||
1027 | out += CHARS.charAt(c1 >> 2); | ||
1028 | out += CHARS.charAt(((c1 & 0x3) << 4) | ((c2 & 0xF0) >> 4)); | ||
1029 | out += CHARS.charAt(((c2 & 0xF) << 2) | ((c3 & 0xC0) >> 6)); | ||
1030 | out += CHARS.charAt(c3 & 0x3F); | ||
1031 | } | ||
1032 | return out; | ||
1033 | }; | ||
1034 | |||
1035 | /** | ||
1036 | * Checks if a given DOM element exists in remote page. | ||
1037 | * | ||
1038 | * @param String selector CSS3 selector | ||
1039 | * @return Boolean | ||
1040 | */ | ||
1041 | this.exists = function(selector) { | ||
1042 | try { | ||
1043 | return document.querySelectorAll(selector).length > 0; | ||
1044 | } catch (e) { | ||
1045 | return false; | ||
1046 | } | ||
1047 | }; | ||
1048 | |||
1049 | /** | ||
1050 | * Checks if a given DOM element is visible in remote page. | ||
1051 | * | ||
1052 | * @param String selector CSS3 selector | ||
1053 | * @return Boolean | ||
1054 | */ | ||
1055 | this.visible = function(selector) { | ||
1056 | try { | ||
1057 | var el = document.querySelector(selector); | ||
1058 | return el && el.style.visibility !== 'hidden' && el.offsetHeight > 0 && el.offsetWidth > 0; | ||
1059 | } catch (e) { | ||
1060 | return false; | ||
1061 | } | ||
1062 | }; | ||
1063 | |||
1064 | /** | ||
1065 | * Fetches innerText within the element(s) matching a given CSS3 | ||
1066 | * selector. | ||
1067 | * | ||
1068 | * @param String selector A CSS3 selector | ||
1069 | * @return String | ||
1070 | */ | ||
1071 | this.fetchText = function(selector) { | ||
1072 | var text = '', elements = this.findAll(selector); | ||
1073 | if (elements && elements.length) { | ||
1074 | Array.prototype.forEach.call(elements, function(element) { | ||
1075 | text += element.innerText; | ||
1076 | }); | ||
1077 | } | ||
1078 | return text; | ||
1079 | }; | ||
1080 | |||
1081 | /** | ||
1082 | * Fills a form with provided field values, and optionnaly submits it. | ||
1083 | * | ||
1084 | * @param HTMLElement|String form A form element, or a CSS3 selector to a form element | ||
1085 | * @param Object vals Field values | ||
1086 | * @return Object An object containing setting result for each field, including file uploads | ||
1087 | */ | ||
1088 | this.fill = function(form, vals) { | ||
1089 | var out = { | ||
1090 | errors: [], | ||
1091 | fields: [], | ||
1092 | files: [] | ||
1093 | }; | ||
1094 | if (!(form instanceof HTMLElement) || typeof form === "string") { | ||
1095 | __utils__.log("attempting to fetch form element from selector: '" + form + "'", "info"); | ||
1096 | try { | ||
1097 | form = document.querySelector(form); | ||
1098 | } catch (e) { | ||
1099 | if (e.name === "SYNTAX_ERR") { | ||
1100 | out.errors.push("invalid form selector provided: '" + form + "'"); | ||
1101 | return out; | ||
1102 | } | ||
1103 | } | ||
1104 | } | ||
1105 | if (!form) { | ||
1106 | out.errors.push("form not found"); | ||
1107 | return out; | ||
1108 | } | ||
1109 | for (var name in vals) { | ||
1110 | if (!vals.hasOwnProperty(name)) { | ||
1111 | continue; | ||
1112 | } | ||
1113 | var field = form.querySelectorAll('[name="' + name + '"]'); | ||
1114 | var value = vals[name]; | ||
1115 | if (!field) { | ||
1116 | out.errors.push('no field named "' + name + '" in form'); | ||
1117 | continue; | ||
1118 | } | ||
1119 | try { | ||
1120 | out.fields[name] = this.setField(field, value); | ||
1121 | } catch (err) { | ||
1122 | if (err.name === "FileUploadError") { | ||
1123 | out.files.push({ | ||
1124 | name: name, | ||
1125 | path: err.path | ||
1126 | }); | ||
1127 | } else { | ||
1128 | this.log(err, "error"); | ||
1129 | throw err; | ||
1130 | } | ||
1131 | } | ||
1132 | } | ||
1133 | return out; | ||
1134 | }; | ||
1135 | |||
1136 | /** | ||
1137 | * Finds all DOM elements matching by the provided selector. | ||
1138 | * | ||
1139 | * @param String selector CSS3 selector | ||
1140 | * @return NodeList|undefined | ||
1141 | */ | ||
1142 | this.findAll = function(selector) { | ||
1143 | try { | ||
1144 | return document.querySelectorAll(selector); | ||
1145 | } catch (e) { | ||
1146 | this.log('findAll(): invalid selector provided "' + selector + '":' + e, "error"); | ||
1147 | } | ||
1148 | }; | ||
1149 | |||
1150 | /** | ||
1151 | * Finds a DOM element by the provided selector. | ||
1152 | * | ||
1153 | * @param String selector CSS3 selector | ||
1154 | * @return HTMLElement|undefined | ||
1155 | */ | ||
1156 | this.findOne = function(selector) { | ||
1157 | try { | ||
1158 | return document.querySelector(selector); | ||
1159 | } catch (e) { | ||
1160 | this.log('findOne(): invalid selector provided "' + selector + '":' + e, "errors"); | ||
1161 | } | ||
1162 | }; | ||
1163 | |||
1164 | /** | ||
1165 | * Downloads a resource behind an url and returns its base64-encoded | ||
1166 | * contents. | ||
1167 | * | ||
1168 | * @param String url The resource url | ||
1169 | * @return String Base64 contents string | ||
1170 | */ | ||
1171 | this.getBase64 = function(url) { | ||
1172 | return this.encode(this.getBinary(url)); | ||
1173 | }; | ||
1174 | |||
1175 | /** | ||
1176 | * Retrieves string contents from a binary file behind an url. Silently | ||
1177 | * fails but log errors. | ||
1178 | * | ||
1179 | * @param String url | ||
1180 | * @return string | ||
1181 | */ | ||
1182 | this.getBinary = function(url) { | ||
1183 | try { | ||
1184 | var xhr = new XMLHttpRequest(); | ||
1185 | xhr.open("GET", url, false); | ||
1186 | xhr.overrideMimeType("text/plain; charset=x-user-defined"); | ||
1187 | xhr.send(null); | ||
1188 | return xhr.responseText; | ||
1189 | } catch (e) { | ||
1190 | if (e.name === "NETWORK_ERR" && e.code === 101) { | ||
1191 | this.log("unfortunately, casperjs cannot make cross domain ajax requests", "warning"); | ||
1192 | } | ||
1193 | this.log("error while fetching " + url + ": " + e, "error"); | ||
1194 | return ""; | ||
1195 | } | ||
1196 | }; | ||
1197 | |||
1198 | /** | ||
1199 | * Logs a message. | ||
1200 | * | ||
1201 | * @param String message | ||
1202 | * @param String level | ||
1203 | */ | ||
1204 | this.log = function(message, level) { | ||
1205 | console.log("[casper:" + (level || "debug") + "] " + message); | ||
1206 | }; | ||
1207 | |||
1208 | /** | ||
1209 | * Sets a field (or a set of fields) value. Fails silently, but log | ||
1210 | * error messages. | ||
1211 | * | ||
1212 | * @param HTMLElement|NodeList field One or more element defining a field | ||
1213 | * @param mixed value The field value to set | ||
1214 | */ | ||
1215 | this.setField = function(field, value) { | ||
1216 | var fields, out; | ||
1217 | value = value || ""; | ||
1218 | if (field instanceof NodeList) { | ||
1219 | fields = field; | ||
1220 | field = fields[0]; | ||
1221 | } | ||
1222 | if (!field instanceof HTMLElement) { | ||
1223 | this.log("invalid field type; only HTMLElement and NodeList are supported", "error"); | ||
1224 | } | ||
1225 | this.log('set "' + field.getAttribute('name') + '" field value to ' + value, "debug"); | ||
1226 | try { | ||
1227 | field.focus(); | ||
1228 | } catch (e) { | ||
1229 | __utils__.log("Unable to focus() input field " + field.getAttribute('name') + ": " + e, "warning"); | ||
1230 | } | ||
1231 | var nodeName = field.nodeName.toLowerCase(); | ||
1232 | switch (nodeName) { | ||
1233 | case "input": | ||
1234 | var type = field.getAttribute('type') || "text"; | ||
1235 | switch (type.toLowerCase()) { | ||
1236 | case "color": | ||
1237 | case "date": | ||
1238 | case "datetime": | ||
1239 | case "datetime-local": | ||
1240 | case "email": | ||
1241 | case "hidden": | ||
1242 | case "month": | ||
1243 | case "number": | ||
1244 | case "password": | ||
1245 | case "range": | ||
1246 | case "search": | ||
1247 | case "tel": | ||
1248 | case "text": | ||
1249 | case "time": | ||
1250 | case "url": | ||
1251 | case "week": | ||
1252 | field.value = value; | ||
1253 | break; | ||
1254 | case "checkbox": | ||
1255 | if (fields.length > 1) { | ||
1256 | var values = value; | ||
1257 | if (!Array.isArray(values)) { | ||
1258 | values = [values]; | ||
1259 | } | ||
1260 | Array.prototype.forEach.call(fields, function(f) { | ||
1261 | f.checked = values.indexOf(f.value) !== -1 ? true : false; | ||
1262 | }); | ||
1263 | } else { | ||
1264 | field.checked = value ? true : false; | ||
1265 | } | ||
1266 | break; | ||
1267 | case "file": | ||
1268 | throw { | ||
1269 | name: "FileUploadError", | ||
1270 | message: "file field must be filled using page.uploadFile", | ||
1271 | path: value | ||
1272 | }; | ||
1273 | case "radio": | ||
1274 | if (fields) { | ||
1275 | Array.prototype.forEach.call(fields, function(e) { | ||
1276 | e.checked = (e.value === value); | ||
1277 | }); | ||
1278 | } else { | ||
1279 | out = 'provided radio elements are empty'; | ||
1280 | } | ||
1281 | break; | ||
1282 | default: | ||
1283 | out = "unsupported input field type: " + type; | ||
1284 | break; | ||
1285 | } | ||
1286 | break; | ||
1287 | case "select": | ||
1288 | case "textarea": | ||
1289 | field.value = value; | ||
1290 | break; | ||
1291 | default: | ||
1292 | out = 'unsupported field type: ' + nodeName; | ||
1293 | break; | ||
1294 | } | ||
1295 | try { | ||
1296 | field.blur(); | ||
1297 | } catch (err) { | ||
1298 | __utils__.log("Unable to blur() input field " + field.getAttribute('name') + ": " + err, "warning"); | ||
1299 | } | ||
1300 | return out; | ||
1301 | }; | ||
1302 | }; | ||
1303 | |||
1304 | /** | ||
1305 | * This is a port of lime colorizer. | ||
1306 | * http://trac.symfony-project.org/browser/tools/lime/trunk/lib/lime.php) | ||
1307 | * | ||
1308 | * (c) Fabien Potencier, Symfony project, MIT license | ||
1309 | */ | ||
1310 | phantom.Casper.Colorizer = function() { | ||
1311 | var options = { bold: 1, underscore: 4, blink: 5, reverse: 7, conceal: 8 }; | ||
1312 | var foreground = { black: 30, red: 31, green: 32, yellow: 33, blue: 34, magenta: 35, cyan: 36, white: 37 }; | ||
1313 | var background = { black: 40, red: 41, green: 42, yellow: 43, blue: 44, magenta: 45, cyan: 46, white: 47 }; | ||
1314 | var styles = { | ||
1315 | 'ERROR': { bg: 'red', fg: 'white', bold: true }, | ||
1316 | 'INFO': { fg: 'green', bold: true }, | ||
1317 | 'TRACE': { fg: 'green', bold: true }, | ||
1318 | 'PARAMETER': { fg: 'cyan' }, | ||
1319 | 'COMMENT': { fg: 'yellow' }, | ||
1320 | 'WARNING': { fg: 'red', bold: true }, | ||
1321 | 'GREEN_BAR': { fg: 'white', bg: 'green', bold: true }, | ||
1322 | 'RED_BAR': { fg: 'white', bg: 'red', bold: true }, | ||
1323 | 'INFO_BAR': { fg: 'cyan', bold: true } | ||
1324 | }; | ||
1325 | |||
1326 | /** | ||
1327 | * Adds a style to provided text. | ||
1328 | * | ||
1329 | * @params String text | ||
1330 | * @params String styleName | ||
1331 | * @return String | ||
1332 | */ | ||
1333 | this.colorize = function(text, styleName) { | ||
1334 | if (styleName in styles) { | ||
1335 | return this.format(text, styles[styleName]); | ||
1336 | } | ||
1337 | return text; | ||
1338 | }; | ||
1339 | |||
1340 | /** | ||
1341 | * Formats a text using a style declaration object. | ||
1342 | * | ||
1343 | * @param String text | ||
1344 | * @param Object style | ||
1345 | * @return String | ||
1346 | */ | ||
1347 | this.format = function(text, style) { | ||
1348 | if (typeof style !== "object") { | ||
1349 | return text; | ||
1350 | } | ||
1351 | var codes = []; | ||
1352 | if (style.fg && foreground[style.fg]) { | ||
1353 | codes.push(foreground[style.fg]); | ||
1354 | } | ||
1355 | if (style.bg && background[style.bg]) { | ||
1356 | codes.push(background[style.bg]); | ||
1357 | } | ||
1358 | for (var option in options) { | ||
1359 | if (style[option] === true) { | ||
1360 | codes.push(options[option]); | ||
1361 | } | ||
1362 | } | ||
1363 | return "\033[" + codes.join(';') + 'm' + text + "\033[0m"; | ||
1364 | }; | ||
1365 | }; | ||
1366 | |||
1367 | /** | ||
1368 | * Casper tester: makes assertions, stores test results and display then. | ||
1369 | * | ||
1370 | */ | ||
1371 | phantom.Casper.Tester = function(casper, options) { | ||
1372 | this.options = isType(options, "object") ? options : {}; | ||
1373 | if (!casper instanceof phantom.Casper) { | ||
1374 | throw "phantom.Casper.Tester needs a phantom.Casper instance"; | ||
1375 | } | ||
1376 | |||
1377 | // locals | ||
1378 | var exporter = new phantom.Casper.XUnitExporter(); | ||
1379 | var PASS = this.options.PASS || "PASS"; | ||
1380 | var FAIL = this.options.FAIL || "FAIL"; | ||
1381 | |||
1382 | function compareArrays(a, b) { | ||
1383 | if (a.length !== b.length) { | ||
1384 | return false; | ||
1385 | } | ||
1386 | a.forEach(function(item, i) { | ||
1387 | if (isType(item, "array") && !compareArrays(item, b[i])) { | ||
1388 | return false; | ||
1389 | } | ||
1390 | if (item !== b[i]) { | ||
1391 | return false; | ||
1392 | } | ||
1393 | }); | ||
1394 | return true; | ||
1395 | } | ||
1396 | |||
1397 | // properties | ||
1398 | this.testResults = { | ||
1399 | passed: 0, | ||
1400 | failed: 0 | ||
1401 | }; | ||
1402 | |||
1403 | // methods | ||
1404 | /** | ||
1405 | * Asserts a condition resolves to true. | ||
1406 | * | ||
1407 | * @param Boolean condition | ||
1408 | * @param String message Test description | ||
1409 | */ | ||
1410 | this.assert = function(condition, message) { | ||
1411 | var status = PASS; | ||
1412 | if (condition === true) { | ||
1413 | style = 'INFO'; | ||
1414 | this.testResults.passed++; | ||
1415 | exporter.addSuccess("unknown", message); | ||
1416 | } else { | ||
1417 | status = FAIL; | ||
1418 | style = 'RED_BAR'; | ||
1419 | this.testResults.failed++; | ||
1420 | exporter.addFailure("unknown", message, 'test failed', "assert"); | ||
1421 | } | ||
1422 | casper.echo([this.colorize(status, style), this.formatMessage(message)].join(' ')); | ||
1423 | }; | ||
1424 | |||
1425 | /** | ||
1426 | * Asserts that two values are strictly equals. | ||
1427 | * | ||
1428 | * @param Mixed testValue The value to test | ||
1429 | * @param Mixed expected The expected value | ||
1430 | * @param String message Test description | ||
1431 | */ | ||
1432 | this.assertEquals = function(testValue, expected, message) { | ||
1433 | if (this.testEquals(testValue, expected)) { | ||
1434 | casper.echo(this.colorize(PASS, 'INFO') + ' ' + this.formatMessage(message)); | ||
1435 | this.testResults.passed++; | ||
1436 | exporter.addSuccess("unknown", message); | ||
1437 | } else { | ||
1438 | casper.echo(this.colorize(FAIL, 'RED_BAR') + ' ' + this.formatMessage(message, 'WARNING')); | ||
1439 | this.comment(' got: ' + testValue); | ||
1440 | this.comment(' expected: ' + expected); | ||
1441 | this.testResults.failed++; | ||
1442 | exporter.addFailure("unknown", message, "test failed; expected: " + expected + "; got: " + testValue, "assertEquals"); | ||
1443 | } | ||
1444 | }; | ||
1445 | |||
1446 | /** | ||
1447 | * Asserts that a code evaluation in remote DOM resolves to true. | ||
1448 | * | ||
1449 | * @param Function fn A function to be evaluated in remote DOM | ||
1450 | * @param String message Test description | ||
1451 | */ | ||
1452 | this.assertEval = function(fn, message) { | ||
1453 | return this.assert(casper.evaluate(fn), message); | ||
1454 | }; | ||
1455 | |||
1456 | /** | ||
1457 | * Asserts that the result of a code evaluation in remote DOM equals | ||
1458 | * an expected value. | ||
1459 | * | ||
1460 | * @param Function fn The function to be evaluated in remote DOM | ||
1461 | * @param Boolean expected The expected value | ||
1462 | * @param String message Test description | ||
1463 | */ | ||
1464 | this.assertEvalEquals = function(fn, expected, message) { | ||
1465 | return this.assertEquals(casper.evaluate(fn), expected, message); | ||
1466 | }; | ||
1467 | |||
1468 | /** | ||
1469 | * Asserts that an element matching the provided CSS3 selector exists in | ||
1470 | * remote DOM. | ||
1471 | * | ||
1472 | * @param String selector CSS3 selectore | ||
1473 | * @param String message Test description | ||
1474 | */ | ||
1475 | this.assertExists = function(selector, message) { | ||
1476 | return this.assert(casper.exists(selector), message); | ||
1477 | }; | ||
1478 | |||
1479 | /** | ||
1480 | * Asserts that a provided string matches a provided RegExp pattern. | ||
1481 | * | ||
1482 | * @param String subject The string to test | ||
1483 | * @param RegExp pattern A RegExp object instance | ||
1484 | * @param String message Test description | ||
1485 | */ | ||
1486 | this.assertMatch = function(subject, pattern, message) { | ||
1487 | if (pattern.test(subject)) { | ||
1488 | casper.echo(this.colorize(PASS, 'INFO') + ' ' + this.formatMessage(message)); | ||
1489 | this.testResults.passed++; | ||
1490 | exporter.addSuccess("unknown", message); | ||
1491 | } else { | ||
1492 | casper.echo(this.colorize(FAIL, 'RED_BAR') + ' ' + this.formatMessage(message, 'WARNING')); | ||
1493 | this.comment(' subject: ' + subject); | ||
1494 | this.comment(' pattern: ' + pattern.toString()); | ||
1495 | this.testResults.failed++; | ||
1496 | exporter.addFailure("unknown", message, "test failed; subject: " + subject + "; pattern: " + pattern.toString(), "assertMatch"); | ||
1497 | } | ||
1498 | }; | ||
1499 | |||
1500 | /** | ||
1501 | * Asserts a condition resolves to false. | ||
1502 | * | ||
1503 | * @param Boolean condition | ||
1504 | * @param String message Test description | ||
1505 | */ | ||
1506 | this.assertNot = function(condition, message) { | ||
1507 | return this.assert(!condition, message); | ||
1508 | }; | ||
1509 | |||
1510 | /** | ||
1511 | * Asserts that the provided function called with the given parameters | ||
1512 | * will raise an exception. | ||
1513 | * | ||
1514 | * @param Function fn The function to test | ||
1515 | * @param Array args The arguments to pass to the function | ||
1516 | * @param String message Test description | ||
1517 | */ | ||
1518 | this.assertRaises = function(fn, args, message) { | ||
1519 | try { | ||
1520 | fn.apply(null, args); | ||
1521 | this.fail(message); | ||
1522 | } catch (e) { | ||
1523 | this.pass(message); | ||
1524 | } | ||
1525 | }; | ||
1526 | |||
1527 | /** | ||
1528 | * Asserts that at least an element matching the provided CSS3 selector | ||
1529 | * exists in remote DOM. | ||
1530 | * | ||
1531 | * @param String selector A CSS3 selector string | ||
1532 | * @param String message Test description | ||
1533 | */ | ||
1534 | this.assertSelectorExists = function(selector, message) { | ||
1535 | return this.assert(this.exists(selector), message); | ||
1536 | }; | ||
1537 | |||
1538 | /** | ||
1539 | * Asserts that title of the remote page equals to the expected one. | ||
1540 | * | ||
1541 | * @param String expected The expected title string | ||
1542 | * @param String message Test description | ||
1543 | */ | ||
1544 | this.assertTitle = function(expected, message) { | ||
1545 | return this.assertEquals(casper.getTitle(), expected, message); | ||
1546 | }; | ||
1547 | |||
1548 | /** | ||
1549 | * Asserts that the provided input is of the given type. | ||
1550 | * | ||
1551 | * @param mixed input The value to test | ||
1552 | * @param String type The javascript type name | ||
1553 | * @param String message Test description | ||
1554 | */ | ||
1555 | this.assertType = function(input, type, message) { | ||
1556 | return this.assertEquals(betterTypeOf(input), type, message); | ||
1557 | }; | ||
1558 | |||
1559 | /** | ||
1560 | * Asserts that a the current page url matches the provided RegExp | ||
1561 | * pattern. | ||
1562 | * | ||
1563 | * @param RegExp pattern A RegExp object instance | ||
1564 | * @param String message Test description | ||
1565 | */ | ||
1566 | this.assertUrlMatch = function(pattern, message) { | ||
1567 | return this.assertMatch(casper.getCurrentUrl(), pattern, message); | ||
1568 | }; | ||
1569 | |||
1570 | /** | ||
1571 | * Render a colorized output. Basically a proxy method for | ||
1572 | * Casper.Colorizer#colorize() | ||
1573 | */ | ||
1574 | this.colorize = function(message, style) { | ||
1575 | return casper.colorizer.colorize(message, style); | ||
1576 | }; | ||
1577 | |||
1578 | /** | ||
1579 | * Writes a comment-style formatted message to stdout. | ||
1580 | * | ||
1581 | * @param String message | ||
1582 | */ | ||
1583 | this.comment = function(message) { | ||
1584 | casper.echo('# ' + message, 'COMMENT'); | ||
1585 | }; | ||
1586 | |||
1587 | /** | ||
1588 | * Tests equality between the two passed arguments. | ||
1589 | * | ||
1590 | * @param Mixed v1 | ||
1591 | * @param Mixed v2 | ||
1592 | * @param Boolean | ||
1593 | */ | ||
1594 | this.testEquals = function(v1, v2) { | ||
1595 | if (betterTypeOf(v1) !== betterTypeOf(v2)) { | ||
1596 | return false; | ||
1597 | } | ||
1598 | if (isType(v1, "function")) { | ||
1599 | return v1.toString() === v2.toString(); | ||
1600 | } | ||
1601 | if (v1 instanceof Object && v2 instanceof Object) { | ||
1602 | if (Object.keys(v1).length !== Object.keys(v2).length) { | ||
1603 | return false; | ||
1604 | } | ||
1605 | for (var k in v1) { | ||
1606 | if (!this.testEquals(v1[k], v2[k])) { | ||
1607 | return false; | ||
1608 | } | ||
1609 | } | ||
1610 | return true; | ||
1611 | } | ||
1612 | return v1 === v2; | ||
1613 | }; | ||
1614 | |||
1615 | /** | ||
1616 | * Writes an error-style formatted message to stdout. | ||
1617 | * | ||
1618 | * @param String message | ||
1619 | */ | ||
1620 | this.error = function(message) { | ||
1621 | casper.echo(message, 'ERROR'); | ||
1622 | }; | ||
1623 | |||
1624 | /** | ||
1625 | * Adds a failed test entry to the stack. | ||
1626 | * | ||
1627 | * @param String message | ||
1628 | */ | ||
1629 | this.fail = function(message) { | ||
1630 | this.assert(false, message); | ||
1631 | }; | ||
1632 | |||
1633 | /** | ||
1634 | * Formats a message to highlight some parts of it. | ||
1635 | * | ||
1636 | * @param String message | ||
1637 | * @param String style | ||
1638 | */ | ||
1639 | this.formatMessage = function(message, style) { | ||
1640 | var parts = /([a-z0-9_\.]+\(\))(.*)/i.exec(message); | ||
1641 | if (!parts) { | ||
1642 | return message; | ||
1643 | } | ||
1644 | return this.colorize(parts[1], 'PARAMETER') + this.colorize(parts[2], style); | ||
1645 | }; | ||
1646 | |||
1647 | /** | ||
1648 | * Writes an info-style formatted message to stdout. | ||
1649 | * | ||
1650 | * @param String message | ||
1651 | */ | ||
1652 | this.info = function(message) { | ||
1653 | casper.echo(message, 'PARAMETER'); | ||
1654 | }; | ||
1655 | |||
1656 | /** | ||
1657 | * Adds a successful test entry to the stack. | ||
1658 | * | ||
1659 | * @param String message | ||
1660 | */ | ||
1661 | this.pass = function(message) { | ||
1662 | this.assert(true, message); | ||
1663 | }; | ||
1664 | |||
1665 | /** | ||
1666 | * Render tests results, an optionnaly exit phantomjs. | ||
1667 | * | ||
1668 | * @param Boolean exit | ||
1669 | */ | ||
1670 | this.renderResults = function(exit, status, save) { | ||
1671 | save = isType(save, "string") ? save : this.options.save; | ||
1672 | var total = this.testResults.passed + this.testResults.failed, statusText, style, result; | ||
1673 | if (this.testResults.failed > 0) { | ||
1674 | statusText = FAIL; | ||
1675 | style = 'RED_BAR'; | ||
1676 | } else { | ||
1677 | statusText = PASS; | ||
1678 | style = 'GREEN_BAR'; | ||
1679 | } | ||
1680 | result = statusText + ' ' + total + ' tests executed, ' + this.testResults.passed + ' passed, ' + this.testResults.failed + ' failed.'; | ||
1681 | if (result.length < 80) { | ||
1682 | result += new Array(80 - result.length + 1).join(' '); | ||
1683 | } | ||
1684 | casper.echo(this.colorize(result, style)); | ||
1685 | if (save && isType(require, "function")) { | ||
1686 | try { | ||
1687 | require('fs').write(save, exporter.getXML(), 'w'); | ||
1688 | casper.echo('result log stored in ' + save, 'INFO'); | ||
1689 | } catch (e) { | ||
1690 | casper.echo('unable to write results to ' + save + '; ' + e, 'ERROR'); | ||
1691 | } | ||
1692 | } | ||
1693 | if (exit === true) { | ||
1694 | casper.exit(status || 0); | ||
1695 | } | ||
1696 | }; | ||
1697 | }; | ||
1698 | |||
1699 | /** | ||
1700 | * Function argument injector. | ||
1701 | * | ||
1702 | */ | ||
1703 | phantom.Casper.FunctionArgsInjector = function(fn) { | ||
1704 | if (!isType(fn, "function")) { | ||
1705 | throw "FunctionArgsInjector() can only process functions"; | ||
1706 | } | ||
1707 | this.fn = fn; | ||
1708 | |||
1709 | this.extract = function(fn) { | ||
1710 | var match = /^function\s?(\w+)?\s?\((.*)\)\s?\{([\s\S]*)\}/i.exec(fn.toString().trim()); | ||
1711 | if (match && match.length > 1) { | ||
1712 | var args = match[2].split(',').map(function(arg) { | ||
1713 | return arg.replace(new RegExp(/\/\*+.*\*\//ig), "").trim(); | ||
1714 | }).filter(function(arg) { | ||
1715 | return arg; | ||
1716 | }) || []; | ||
1717 | return { | ||
1718 | name: match[1] ? match[1].trim() : null, | ||
1719 | args: args, | ||
1720 | body: match[3] ? match[3].trim() : '' | ||
1721 | }; | ||
1722 | } | ||
1723 | }; | ||
1724 | |||
1725 | this.process = function(values) { | ||
1726 | var fnObj = this.extract(this.fn); | ||
1727 | if (!isType(fnObj, "object")) { | ||
1728 | throw "Unable to process function " + this.fn.toString(); | ||
1729 | } | ||
1730 | var inject = this.getArgsInjectionString(fnObj.args, values); | ||
1731 | return 'function ' + (fnObj.name || '') + '(){' + inject + fnObj.body + '}'; | ||
1732 | }; | ||
1733 | |||
1734 | this.getArgsInjectionString = function(args, values) { | ||
1735 | values = typeof values === "object" ? values : {}; | ||
1736 | var jsonValues = escape(encodeURIComponent(JSON.stringify(values))); | ||
1737 | var inject = [ | ||
1738 | 'var __casper_params__ = JSON.parse(decodeURIComponent(unescape(\'' + jsonValues + '\')));' | ||
1739 | ]; | ||
1740 | args.forEach(function(arg) { | ||
1741 | if (arg in values) { | ||
1742 | inject.push('var ' + arg + '=__casper_params__["' + arg + '"];'); | ||
1743 | } | ||
1744 | }); | ||
1745 | return inject.join('\n') + '\n'; | ||
1746 | }; | ||
1747 | }; | ||
1748 | |||
1749 | /** | ||
1750 | * JUnit XML (xUnit) exporter for test results. | ||
1751 | * | ||
1752 | */ | ||
1753 | phantom.Casper.XUnitExporter = function() { | ||
1754 | var node = function(name, attributes) { | ||
1755 | var node = document.createElement(name); | ||
1756 | for (var attrName in attributes) { | ||
1757 | var value = attributes[attrName]; | ||
1758 | if (attributes.hasOwnProperty(attrName) && isType(attrName, "string")) { | ||
1759 | node.setAttribute(attrName, value); | ||
1760 | } | ||
1761 | } | ||
1762 | return node; | ||
1763 | }; | ||
1764 | |||
1765 | var xml = node('testsuite'); | ||
1766 | xml.toString = function() { | ||
1767 | return this.outerHTML; // ouch | ||
1768 | }; | ||
1769 | |||
1770 | /** | ||
1771 | * Adds a successful test result | ||
1772 | * | ||
1773 | * @param String classname | ||
1774 | * @param String name | ||
1775 | */ | ||
1776 | this.addSuccess = function(classname, name) { | ||
1777 | xml.appendChild(node('testcase', { | ||
1778 | classname: classname, | ||
1779 | name: name | ||
1780 | })); | ||
1781 | }; | ||
1782 | |||
1783 | /** | ||
1784 | * Adds a failed test result | ||
1785 | * | ||
1786 | * @param String classname | ||
1787 | * @param String name | ||
1788 | * @param String message | ||
1789 | * @param String type | ||
1790 | */ | ||
1791 | this.addFailure = function(classname, name, message, type) { | ||
1792 | var fnode = node('testcase', { | ||
1793 | classname: classname, | ||
1794 | name: name | ||
1795 | }); | ||
1796 | var failure = node('failure', { | ||
1797 | type: type || "unknown" | ||
1798 | }); | ||
1799 | failure.appendChild(document.createTextNode(message || "no message left")); | ||
1800 | fnode.appendChild(failure); | ||
1801 | xml.appendChild(fnode); | ||
1802 | }; | ||
1803 | |||
1804 | /** | ||
1805 | * Retrieves generated XML object - actually an HTMLElement. | ||
1806 | * | ||
1807 | * @return HTMLElement | ||
1808 | */ | ||
1809 | this.getXML = function() { | ||
1810 | return xml; | ||
1811 | }; | ||
1812 | }; | ||
1813 | |||
1814 | /** | ||
1815 | * Provides a better typeof operator equivalent, able to retrieve the array | ||
1816 | * type. | ||
1817 | * | ||
1818 | * @param mixed input | ||
1819 | * @return String | ||
1820 | * @see http://javascriptweblog.wordpress.com/2011/08/08/fixing-the-javascript-typeof-operator/ | ||
1821 | */ | ||
1822 | function betterTypeOf(input) { | ||
1823 | try { | ||
1824 | return Object.prototype.toString.call(input).match(/^\[object\s(.*)\]$/)[1].toLowerCase(); | ||
1825 | } catch (e) { | ||
1826 | return typeof input; | ||
1827 | } | ||
1828 | } | ||
1829 | |||
1830 | /** | ||
1831 | * Creates a new WebPage instance for Casper use. | ||
1832 | * | ||
1833 | * @param Casper casper A Casper instance | ||
1834 | * @return WebPage | ||
1835 | */ | ||
1836 | function createPage(casper) { | ||
1837 | var page; | ||
1838 | if (phantom.version.major <= 1 && phantom.version.minor < 3 && isType(require, "function")) { | ||
1839 | page = new WebPage(); | ||
1840 | } else { | ||
1841 | page = require('webpage').create(); | ||
1842 | } | ||
1843 | page.onAlert = function(message) { | ||
1844 | casper.log('[alert] ' + message, "info", "remote"); | ||
1845 | if (isType(casper.options.onAlert, "function")) { | ||
1846 | casper.options.onAlert.call(casper, casper, message); | ||
1847 | } | ||
1848 | }; | ||
1849 | page.onConsoleMessage = function(msg) { | ||
1850 | var level = "info", test = /^\[casper:(\w+)\]\s?(.*)/.exec(msg); | ||
1851 | if (test && test.length === 3) { | ||
1852 | level = test[1]; | ||
1853 | msg = test[2]; | ||
1854 | } | ||
1855 | casper.log(msg, level, "remote"); | ||
1856 | }; | ||
1857 | page.onLoadStarted = function() { | ||
1858 | casper.loadInProgress = true; | ||
1859 | }; | ||
1860 | page.onLoadFinished = function(status) { | ||
1861 | if (status !== "success") { | ||
1862 | var message = 'Loading resource failed with status=' + status; | ||
1863 | if (casper.currentHTTPStatus) { | ||
1864 | message += ' (HTTP ' + casper.currentHTTPStatus + ')'; | ||
1865 | } | ||
1866 | message += ': ' + casper.requestUrl; | ||
1867 | casper.log(message, "warning"); | ||
1868 | if (isType(casper.options.onLoadError, "function")) { | ||
1869 | casper.options.onLoadError.call(casper, casper, casper.requestUrl, status); | ||
1870 | } | ||
1871 | } | ||
1872 | if (casper.options.clientScripts) { | ||
1873 | if (betterTypeOf(casper.options.clientScripts) !== "array") { | ||
1874 | casper.log("The clientScripts option must be an array", "error"); | ||
1875 | } else { | ||
1876 | for (var i = 0; i < casper.options.clientScripts.length; i++) { | ||
1877 | var script = casper.options.clientScripts[i]; | ||
1878 | if (casper.page.injectJs(script)) { | ||
1879 | casper.log('Automatically injected ' + script + ' client side', "debug"); | ||
1880 | } else { | ||
1881 | casper.log('Failed injecting ' + script + ' client side', "warning"); | ||
1882 | } | ||
1883 | } | ||
1884 | } | ||
1885 | } | ||
1886 | // Client-side utils injection | ||
1887 | var injected = page.evaluate(replaceFunctionPlaceholders(function() { | ||
1888 | eval("var ClientUtils = " + decodeURIComponent("%utils%")); | ||
1889 | __utils__ = new ClientUtils(); | ||
1890 | return __utils__ instanceof ClientUtils; | ||
1891 | }, { | ||
1892 | utils: encodeURIComponent(phantom.Casper.ClientUtils.toString()) | ||
1893 | })); | ||
1894 | if (!injected) { | ||
1895 | casper.log("Failed to inject Casper client-side utilities!", "warning"); | ||
1896 | } else { | ||
1897 | casper.log("Successfully injected Casper client-side utilities", "debug"); | ||
1898 | } | ||
1899 | // history | ||
1900 | casper.history.push(casper.getCurrentUrl()); | ||
1901 | casper.loadInProgress = false; | ||
1902 | }; | ||
1903 | page.onResourceReceived = function(resource) { | ||
1904 | if (isType(casper.options.onResourceReceived, "function")) { | ||
1905 | casper.options.onResourceReceived.call(casper, casper, resource); | ||
1906 | } | ||
1907 | if (resource.url === casper.requestUrl && resource.stage === "start") { | ||
1908 | casper.currentHTTPStatus = resource.status; | ||
1909 | if (isType(casper.options.httpStatusHandlers, "object") && resource.status in casper.options.httpStatusHandlers) { | ||
1910 | casper.options.httpStatusHandlers[resource.status](casper, resource); | ||
1911 | } | ||
1912 | casper.currentUrl = resource.url; | ||
1913 | } | ||
1914 | }; | ||
1915 | page.onResourceRequested = function(request) { | ||
1916 | if (isType(casper.options.onResourceRequested, "function")) { | ||
1917 | casper.options.onResourceRequested.call(casper, casper, request); | ||
1918 | } | ||
1919 | }; | ||
1920 | return page; | ||
1921 | } | ||
1922 | |||
1923 | /** | ||
1924 | * Shorthands for checking if a value is of the given type. Can check for | ||
1925 | * arrays. | ||
1926 | * | ||
1927 | * @param mixed what The value to check | ||
1928 | * @param String typeName The type name ("string", "number", "function", etc.) | ||
1929 | * @return Boolean | ||
1930 | */ | ||
1931 | function isType(what, typeName) { | ||
1932 | return betterTypeOf(what) === typeName; | ||
1933 | } | ||
1934 | |||
1935 | /** | ||
1936 | * Checks if the provided var is a WebPage instance | ||
1937 | * | ||
1938 | * @param mixed what | ||
1939 | * @return Boolean | ||
1940 | */ | ||
1941 | function isWebPage(what) { | ||
1942 | if (!what || !isType(what, "object")) { | ||
1943 | return false; | ||
1944 | } | ||
1945 | if (phantom.version.major <= 1 && phantom.version.minor < 3 && isType(require, "function")) { | ||
1946 | return what instanceof WebPage; | ||
1947 | } else { | ||
1948 | return what.toString().indexOf('WebPage(') === 0; | ||
1949 | } | ||
1950 | } | ||
1951 | |||
1952 | /** | ||
1953 | * Object recursive merging utility. | ||
1954 | * | ||
1955 | * @param Object obj1 the destination object | ||
1956 | * @param Object obj2 the source object | ||
1957 | * @return Object | ||
1958 | */ | ||
1959 | function mergeObjects(obj1, obj2) { | ||
1960 | for (var p in obj2) { | ||
1961 | try { | ||
1962 | if (obj2[p].constructor == Object) { | ||
1963 | obj1[p] = mergeObjects(obj1[p], obj2[p]); | ||
1964 | } else { | ||
1965 | obj1[p] = obj2[p]; | ||
1966 | } | ||
1967 | } catch(e) { | ||
1968 | obj1[p] = obj2[p]; | ||
1969 | } | ||
1970 | } | ||
1971 | return obj1; | ||
1972 | } | ||
1973 | |||
1974 | /** | ||
1975 | * Replaces a function string contents with placeholders provided by an | ||
1976 | * Object. | ||
1977 | * | ||
1978 | * @param Function fn The function | ||
1979 | * @param Object replacements Object containing placeholder replacements | ||
1980 | * @return String A function string representation | ||
1981 | */ | ||
1982 | function replaceFunctionPlaceholders(fn, replacements) { | ||
1983 | if (replacements && isType(replacements, "object")) { | ||
1984 | fn = fn.toString(); | ||
1985 | for (var placeholder in replacements) { | ||
1986 | var match = '%' + placeholder + '%'; | ||
1987 | do { | ||
1988 | fn = fn.replace(match, replacements[placeholder]); | ||
1989 | } while(fn.indexOf(match) !== -1); | ||
1990 | } | ||
1991 | } | ||
1992 | return fn; | ||
1993 | } | ||
1994 | })(phantom); | ... | ... |
lib/casper.js
0 → 100644
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 | httpStatusHandlers: {}, | ||
48 | onAlert: null, | ||
49 | onDie: null, | ||
50 | onError: null, | ||
51 | onLoadError: null, | ||
52 | onPageInitialized: null, | ||
53 | onResourceReceived: null, | ||
54 | onResourceRequested: null, | ||
55 | onStepComplete: null, | ||
56 | onStepTimeout: null, | ||
57 | onTimeout: null, | ||
58 | page: null, | ||
59 | pageSettings: { userAgent: DEFAULT_USER_AGENT }, | ||
60 | stepTimeout: null, | ||
61 | timeout: null, | ||
62 | verbose: false | ||
63 | }; | ||
64 | // privates | ||
65 | // local properties | ||
66 | this.checker = null; | ||
67 | this.colorizer = new phantom.Casper.Colorizer(); | ||
68 | this.currentUrl = 'about:blank'; | ||
69 | this.currentHTTPStatus = 200; | ||
70 | this.defaultWaitTimeout = 5000; | ||
71 | this.delayedExecution = false; | ||
72 | this.history = []; | ||
73 | this.loadInProgress = false; | ||
74 | this.logFormats = {}; | ||
75 | this.logLevels = ["debug", "info", "warning", "error"]; | ||
76 | this.logStyles = { | ||
77 | debug: 'INFO', | ||
78 | info: 'PARAMETER', | ||
79 | warning: 'COMMENT', | ||
80 | error: 'ERROR' | ||
81 | }; | ||
82 | this.options = mergeObjects(this.defaults, options); | ||
83 | this.page = null; | ||
84 | this.requestUrl = 'about:blank'; | ||
85 | this.result = { | ||
86 | log: [], | ||
87 | status: "success", | ||
88 | time: 0 | ||
89 | }; | ||
90 | this.started = false; | ||
91 | this.step = 0; | ||
92 | this.steps = []; | ||
93 | this.test = new phantom.Casper.Tester(this); | ||
94 | }; | ||
95 | |||
96 | /** | ||
97 | * Casper prototype | ||
98 | */ | ||
99 | phantom.Casper.prototype = { | ||
100 | /** | ||
101 | * Go a step back in browser's history | ||
102 | * | ||
103 | * @return Casper | ||
104 | */ | ||
105 | back: function() { | ||
106 | return this.then(function(self) { | ||
107 | self.evaluate(function() { | ||
108 | history.back(); | ||
109 | }); | ||
110 | }); | ||
111 | }, | ||
112 | |||
113 | /** | ||
114 | * Encodes a resource using the base64 algorithm synchroneously using | ||
115 | * client-side XMLHttpRequest. | ||
116 | * | ||
117 | * NOTE: we cannot use window.btoa() for some strange reasons here. | ||
118 | * | ||
119 | * @param String url The url to download | ||
120 | * @return string Base64 encoded result | ||
121 | */ | ||
122 | base64encode: function(url) { | ||
123 | return this.evaluate(function(url) { | ||
124 | return __utils__.getBase64(url); | ||
125 | }, { url: url }); | ||
126 | }, | ||
127 | |||
128 | /** | ||
129 | * Proxy method for WebPage#render. Adds a clipRect parameter for | ||
130 | * automatically set page clipRect setting values and sets it back once | ||
131 | * done. If the cliprect parameter is omitted, the full page viewport | ||
132 | * area will be rendered. | ||
133 | * | ||
134 | * @param String targetFile A target filename | ||
135 | * @param mixed clipRect An optional clipRect object (optional) | ||
136 | * @return Casper | ||
137 | */ | ||
138 | capture: function(targetFile, clipRect) { | ||
139 | var previousClipRect; | ||
140 | if (clipRect) { | ||
141 | if (!isType(clipRect, "object")) { | ||
142 | throw new Error("clipRect must be an Object instance."); | ||
143 | } | ||
144 | previousClipRect = this.page.clipRect; | ||
145 | this.page.clipRect = clipRect; | ||
146 | this.log('Capturing page to ' + targetFile + ' with clipRect' + JSON.stringify(clipRect), "debug"); | ||
147 | } else { | ||
148 | this.log('Capturing page to ' + targetFile, "debug"); | ||
149 | } | ||
150 | try { | ||
151 | this.page.render(targetFile); | ||
152 | } catch (e) { | ||
153 | this.log('Failed to capture screenshot as ' + targetFile + ': ' + e, "error"); | ||
154 | } | ||
155 | if (previousClipRect) { | ||
156 | this.page.clipRect = previousClipRect; | ||
157 | } | ||
158 | return this; | ||
159 | }, | ||
160 | |||
161 | /** | ||
162 | * Captures the page area containing the provided selector. | ||
163 | * | ||
164 | * @param String targetFile Target destination file path. | ||
165 | * @param String selector CSS3 selector | ||
166 | * @return Casper | ||
167 | */ | ||
168 | captureSelector: function(targetFile, selector) { | ||
169 | return this.capture(targetFile, this.evaluate(function(selector) { | ||
170 | try { | ||
171 | var clipRect = document.querySelector(selector).getBoundingClientRect(); | ||
172 | return { | ||
173 | top: clipRect.top, | ||
174 | left: clipRect.left, | ||
175 | width: clipRect.width, | ||
176 | height: clipRect.height | ||
177 | }; | ||
178 | } catch (e) { | ||
179 | __utils__.log("Unable to fetch bounds for element " + selector, "warning"); | ||
180 | } | ||
181 | }, { selector: selector })); | ||
182 | }, | ||
183 | |||
184 | /** | ||
185 | * Checks for any further navigation step to process. | ||
186 | * | ||
187 | * @param Casper self A self reference | ||
188 | * @param function onComplete An options callback to apply on completion | ||
189 | */ | ||
190 | checkStep: function(self, onComplete) { | ||
191 | var step = self.steps[self.step]; | ||
192 | if (!self.loadInProgress && isType(step, "function")) { | ||
193 | self.runStep(step); | ||
194 | } | ||
195 | if (!isType(step, "function") && !self.delayedExecution) { | ||
196 | self.result.time = new Date().getTime() - self.startTime; | ||
197 | self.log("Done " + self.steps.length + " steps in " + self.result.time + 'ms.', "info"); | ||
198 | clearInterval(self.checker); | ||
199 | if (isType(onComplete, "function")) { | ||
200 | try { | ||
201 | onComplete.call(self, self); | ||
202 | } catch (err) { | ||
203 | self.log("could not complete final step: " + err, "error"); | ||
204 | } | ||
205 | } else { | ||
206 | // default behavior is to exit phantom | ||
207 | self.exit(); | ||
208 | } | ||
209 | } | ||
210 | }, | ||
211 | |||
212 | /** | ||
213 | * Emulates a click on the element from the provided selector, if | ||
214 | * possible. In case of success, `true` is returned. | ||
215 | * | ||
216 | * @param String selector A DOM CSS3 compatible selector | ||
217 | * @param Boolean fallbackToHref Whether to try to relocate to the value of any href attribute (default: true) | ||
218 | * @return Boolean | ||
219 | */ | ||
220 | click: function(selector, fallbackToHref) { | ||
221 | fallbackToHref = isType(fallbackToHref, "undefined") ? true : !!fallbackToHref; | ||
222 | this.log("click on selector: " + selector, "debug"); | ||
223 | return this.evaluate(function(selector, fallbackToHref) { | ||
224 | return __utils__.click(selector, fallbackToHref); | ||
225 | }, { | ||
226 | selector: selector, | ||
227 | fallbackToHref: fallbackToHref | ||
228 | }); | ||
229 | }, | ||
230 | |||
231 | /** | ||
232 | * Creates a step definition. | ||
233 | * | ||
234 | * @param Function fn The step function to call | ||
235 | * @param Object options Step options | ||
236 | * @return Function The final step function | ||
237 | */ | ||
238 | createStep: function(fn, options) { | ||
239 | if (!isType(fn, "function")) { | ||
240 | throw "createStep(): a step definition must be a function"; | ||
241 | } | ||
242 | fn.options = isType(options, "object") ? options : {}; | ||
243 | return fn; | ||
244 | }, | ||
245 | |||
246 | /** | ||
247 | * Logs the HTML code of the current page. | ||
248 | * | ||
249 | * @return Casper | ||
250 | */ | ||
251 | debugHTML: function() { | ||
252 | this.echo(this.evaluate(function() { | ||
253 | return document.body.innerHTML; | ||
254 | })); | ||
255 | return this; | ||
256 | }, | ||
257 | |||
258 | /** | ||
259 | * Logs the textual contents of the current page. | ||
260 | * | ||
261 | * @return Casper | ||
262 | */ | ||
263 | debugPage: function() { | ||
264 | this.echo(this.evaluate(function() { | ||
265 | return document.body.innerText; | ||
266 | })); | ||
267 | return this; | ||
268 | }, | ||
269 | |||
270 | /** | ||
271 | * Exit phantom on failure, with a logged error message. | ||
272 | * | ||
273 | * @param String message An optional error message | ||
274 | * @param Number status An optional exit status code (must be > 0) | ||
275 | * @return Casper | ||
276 | */ | ||
277 | die: function(message, status) { | ||
278 | this.result.status = 'error'; | ||
279 | this.result.time = new Date().getTime() - this.startTime; | ||
280 | message = isType(message, "string") && message.length > 0 ? message : DEFAULT_DIE_MESSAGE; | ||
281 | this.log(message, "error"); | ||
282 | if (isType(this.options.onDie, "function")) { | ||
283 | this.options.onDie.call(this, this, message, status); | ||
284 | } | ||
285 | return this.exit(Number(status) > 0 ? Number(status) : 1); | ||
286 | }, | ||
287 | |||
288 | /** | ||
289 | * Iterates over the values of a provided array and execute a callback | ||
290 | * for each item. | ||
291 | * | ||
292 | * @param Array array | ||
293 | * @param Function fn Callback: function(self, item, index) | ||
294 | * @return Casper | ||
295 | */ | ||
296 | each: function(array, fn) { | ||
297 | if (!isType(array, "array")) { | ||
298 | self.log("each() only works with arrays", "error"); | ||
299 | return this; | ||
300 | } | ||
301 | (function(self) { | ||
302 | array.forEach(function(item, i) { | ||
303 | fn(self, item, i); | ||
304 | }); | ||
305 | })(this); | ||
306 | return this; | ||
307 | }, | ||
308 | |||
309 | /** | ||
310 | * Prints something to stdout. | ||
311 | * | ||
312 | * @param String text A string to echo to stdout | ||
313 | * @return Casper | ||
314 | */ | ||
315 | echo: function(text, style) { | ||
316 | console.log(style ? this.colorizer.colorize(text, style) : text); | ||
317 | return this; | ||
318 | }, | ||
319 | |||
320 | /** | ||
321 | * Evaluates an expression in the page context, a bit like what | ||
322 | * WebPage#evaluate does, but the passed function can also accept | ||
323 | * parameters if a context Object is also passed: | ||
324 | * | ||
325 | * casper.evaluate(function(username, password) { | ||
326 | * document.querySelector('#username').value = username; | ||
327 | * document.querySelector('#password').value = password; | ||
328 | * document.querySelector('#submit').click(); | ||
329 | * }, { | ||
330 | * username: 'Bazoonga', | ||
331 | * password: 'baz00nga' | ||
332 | * }) | ||
333 | * | ||
334 | * FIXME: waiting for a patch of PhantomJS to allow direct passing of | ||
335 | * arguments to the function. | ||
336 | * TODO: don't forget to keep this backward compatible. | ||
337 | * | ||
338 | * @param Function fn The function to be evaluated within current page DOM | ||
339 | * @param Object context Object containing the parameters to inject into the function | ||
340 | * @return mixed | ||
341 | * @see WebPage#evaluate | ||
342 | */ | ||
343 | evaluate: function(fn, context) { | ||
344 | context = isType(context, "object") ? context : {}; | ||
345 | var newFn = new phantom.Casper.FunctionArgsInjector(fn).process(context); | ||
346 | return this.page.evaluate(newFn); | ||
347 | }, | ||
348 | |||
349 | /** | ||
350 | * Evaluates an expression within the current page DOM and die() if it | ||
351 | * returns false. | ||
352 | * | ||
353 | * @param function fn The expression to evaluate | ||
354 | * @param String message The error message to log | ||
355 | * @return Casper | ||
356 | */ | ||
357 | evaluateOrDie: function(fn, message) { | ||
358 | if (!this.evaluate(fn)) { | ||
359 | return this.die(message); | ||
360 | } | ||
361 | return this; | ||
362 | }, | ||
363 | |||
364 | /** | ||
365 | * Checks if an element matching the provided CSS3 selector exists in | ||
366 | * current page DOM. | ||
367 | * | ||
368 | * @param String selector A CSS3 selector | ||
369 | * @return Boolean | ||
370 | */ | ||
371 | exists: function(selector) { | ||
372 | return this.evaluate(function(selector) { | ||
373 | return __utils__.exists(selector); | ||
374 | }, { selector: selector }); | ||
375 | }, | ||
376 | |||
377 | /** | ||
378 | * Checks if an element matching the provided CSS3 selector is visible | ||
379 | * current page DOM by checking that offsetWidth and offsetHeight are | ||
380 | * both non-zero. | ||
381 | * | ||
382 | * @param String selector A CSS3 selector | ||
383 | * @return Boolean | ||
384 | */ | ||
385 | visible: function(selector) { | ||
386 | return this.evaluate(function(selector) { | ||
387 | return __utils__.visible(selector); | ||
388 | }, { selector: selector }); | ||
389 | }, | ||
390 | |||
391 | /** | ||
392 | * Exits phantom. | ||
393 | * | ||
394 | * @param Number status Status | ||
395 | * @return Casper | ||
396 | */ | ||
397 | exit: function(status) { | ||
398 | phantom.exit(status); | ||
399 | return this; | ||
400 | }, | ||
401 | |||
402 | /** | ||
403 | * Fetches innerText within the element(s) matching a given CSS3 | ||
404 | * selector. | ||
405 | * | ||
406 | * @param String selector A CSS3 selector | ||
407 | * @return String | ||
408 | */ | ||
409 | fetchText: function(selector) { | ||
410 | return this.evaluate(function(selector) { | ||
411 | return __utils__.fetchText(selector); | ||
412 | }, { selector: selector }); | ||
413 | }, | ||
414 | |||
415 | /** | ||
416 | * Fills a form with provided field values. | ||
417 | * | ||
418 | * @param String selector A CSS3 selector to the target form to fill | ||
419 | * @param Object vals Field values | ||
420 | * @param Boolean submit Submit the form? | ||
421 | */ | ||
422 | fill: function(selector, vals, submit) { | ||
423 | submit = submit === true ? submit : false; | ||
424 | if (!isType(selector, "string") || !selector.length) { | ||
425 | throw "form selector must be a non-empty string"; | ||
426 | } | ||
427 | if (!isType(vals, "object")) { | ||
428 | throw "form values must be provided as an object"; | ||
429 | } | ||
430 | var fillResults = this.evaluate(function(selector, values) { | ||
431 | return __utils__.fill(selector, values); | ||
432 | }, { | ||
433 | selector: selector, | ||
434 | values: vals | ||
435 | }); | ||
436 | if (!fillResults) { | ||
437 | throw "unable to fill form"; | ||
438 | } else if (fillResults.errors.length > 0) { | ||
439 | (function(self){ | ||
440 | fillResults.errors.forEach(function(error) { | ||
441 | self.log("form error: " + error, "error"); | ||
442 | }); | ||
443 | })(this); | ||
444 | if (submit) { | ||
445 | this.log("errors encountered while filling form; submission aborted", "warning"); | ||
446 | submit = false; | ||
447 | } | ||
448 | } | ||
449 | // File uploads | ||
450 | if (fillResults.files && fillResults.files.length > 0) { | ||
451 | (function(self) { | ||
452 | fillResults.files.forEach(function(file) { | ||
453 | var fileFieldSelector = [selector, 'input[name="' + file.name + '"]'].join(' '); | ||
454 | self.page.uploadFile(fileFieldSelector, file.path); | ||
455 | }); | ||
456 | })(this); | ||
457 | } | ||
458 | // Form submission? | ||
459 | if (submit) { | ||
460 | this.evaluate(function(selector) { | ||
461 | var form = document.querySelector(selector); | ||
462 | var method = form.getAttribute('method').toUpperCase() || "GET"; | ||
463 | var action = form.getAttribute('action') || "unknown"; | ||
464 | __utils__.log('submitting form to ' + action + ', HTTP ' + method, 'info'); | ||
465 | form.submit(); | ||
466 | }, { selector: selector }); | ||
467 | } | ||
468 | }, | ||
469 | |||
470 | /** | ||
471 | * Go a step forward in browser's history | ||
472 | * | ||
473 | * @return Casper | ||
474 | */ | ||
475 | forward: function(then) { | ||
476 | return this.then(function(self) { | ||
477 | self.evaluate(function() { | ||
478 | history.forward(); | ||
479 | }); | ||
480 | }); | ||
481 | }, | ||
482 | |||
483 | /** | ||
484 | * Retrieves current document url. | ||
485 | * | ||
486 | * @return String | ||
487 | */ | ||
488 | getCurrentUrl: function() { | ||
489 | return decodeURIComponent(this.evaluate(function() { | ||
490 | return document.location.href; | ||
491 | })); | ||
492 | }, | ||
493 | |||
494 | /** | ||
495 | * Retrieves global variable. | ||
496 | * | ||
497 | * @param String name The name of the global variable to retrieve | ||
498 | * @return mixed | ||
499 | */ | ||
500 | getGlobal: function(name) { | ||
501 | var result = this.evaluate(function(name) { | ||
502 | var result = {}; | ||
503 | try { | ||
504 | result.value = JSON.stringify(window[name]); | ||
505 | } catch (e) { | ||
506 | result.error = 'Unable to JSON encode window.' + name + ': ' + e; | ||
507 | } | ||
508 | return result; | ||
509 | }, {'name': name}); | ||
510 | if (result.error) { | ||
511 | throw result.error; | ||
512 | } else { | ||
513 | return JSON.parse(result.value); | ||
514 | } | ||
515 | }, | ||
516 | |||
517 | /** | ||
518 | * Retrieves current page title, if any. | ||
519 | * | ||
520 | * @return String | ||
521 | */ | ||
522 | getTitle: function() { | ||
523 | return this.evaluate(function() { | ||
524 | return document.title; | ||
525 | }); | ||
526 | }, | ||
527 | |||
528 | /** | ||
529 | * Logs a message. | ||
530 | * | ||
531 | * @param String message The message to log | ||
532 | * @param String level The log message level (from Casper.logLevels property) | ||
533 | * @param String space Space from where the logged event occured (default: "phantom") | ||
534 | * @return Casper | ||
535 | */ | ||
536 | log: function(message, level, space) { | ||
537 | level = level && this.logLevels.indexOf(level) > -1 ? level : "debug"; | ||
538 | space = space ? space : "phantom"; | ||
539 | if (level === "error" && isType(this.options.onError, "function")) { | ||
540 | this.options.onError.call(this, this, message, space); | ||
541 | } | ||
542 | if (this.logLevels.indexOf(level) < this.logLevels.indexOf(this.options.logLevel)) { | ||
543 | return this; // skip logging | ||
544 | } | ||
545 | var entry = { | ||
546 | level: level, | ||
547 | space: space, | ||
548 | message: message, | ||
549 | date: new Date().toString() | ||
550 | }; | ||
551 | if (level in this.logFormats && isType(this.logFormats[level], "function")) { | ||
552 | message = this.logFormats[level](message, level, space); | ||
553 | } else { | ||
554 | var levelStr = this.colorizer.colorize('[' + level + ']', this.logStyles[level]); | ||
555 | message = levelStr + ' [' + space + '] ' + message; | ||
556 | } | ||
557 | if (this.options.verbose) { | ||
558 | this.echo(message); // direct output | ||
559 | } | ||
560 | this.result.log.push(entry); | ||
561 | return this; | ||
562 | }, | ||
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, options) { | ||
573 | options = isType(options, "object") ? options : {}; | ||
574 | this.requestUrl = location; | ||
575 | this.page.open(location); | ||
576 | return this; | ||
577 | }, | ||
578 | |||
579 | /** | ||
580 | * Repeats a step a given number of times. | ||
581 | * | ||
582 | * @param Number times Number of times to repeat step | ||
583 | * @aram function then The step closure | ||
584 | * @return Casper | ||
585 | * @see Casper#then | ||
586 | */ | ||
587 | repeat: function(times, then) { | ||
588 | for (var i = 0; i < times; i++) { | ||
589 | this.then(then); | ||
590 | } | ||
591 | return this; | ||
592 | }, | ||
593 | |||
594 | /** | ||
595 | * Runs the whole suite of steps. | ||
596 | * | ||
597 | * @param function onComplete an optional callback | ||
598 | * @param Number time an optional amount of milliseconds for interval checking | ||
599 | * @return Casper | ||
600 | */ | ||
601 | run: function(onComplete, time) { | ||
602 | if (!this.steps || this.steps.length < 1) { | ||
603 | this.log("No steps defined, aborting", "error"); | ||
604 | return this; | ||
605 | } | ||
606 | this.log("Running suite: " + this.steps.length + " step" + (this.steps.length > 1 ? "s" : ""), "info"); | ||
607 | this.checker = setInterval(this.checkStep, (time ? time: 250), this, onComplete); | ||
608 | return this; | ||
609 | }, | ||
610 | |||
611 | /** | ||
612 | * Runs a step. | ||
613 | * | ||
614 | * @param Function step | ||
615 | */ | ||
616 | runStep: function(step) { | ||
617 | var skipLog = isType(step.options, "object") && step.options.skipLog === true; | ||
618 | var stepInfo = "Step " + (this.step + 1) + "/" + this.steps.length; | ||
619 | var stepResult; | ||
620 | if (!skipLog) { | ||
621 | this.log(stepInfo + ' ' + this.getCurrentUrl() + ' (HTTP ' + this.currentHTTPStatus + ')', "info"); | ||
622 | } | ||
623 | if (isType(this.options.stepTimeout, "number") && this.options.stepTimeout > 0) { | ||
624 | var stepTimeoutCheckInterval = setInterval(function(self, start, stepNum) { | ||
625 | if (new Date().getTime() - start > self.options.stepTimeout) { | ||
626 | if (self.step == stepNum + 1) { | ||
627 | if (isType(self.options.onStepTimeout, "function")) { | ||
628 | self.options.onStepTimeout.call(self, self); | ||
629 | } else { | ||
630 | self.die("Maximum step execution timeout exceeded for step " + stepNum, "error"); | ||
631 | } | ||
632 | } | ||
633 | clearInterval(stepTimeoutCheckInterval); | ||
634 | } | ||
635 | }, this.options.stepTimeout, this, new Date().getTime(), this.step); | ||
636 | } | ||
637 | try { | ||
638 | stepResult = step.call(this, this); | ||
639 | } catch (e) { | ||
640 | if (this.options.faultTolerant) { | ||
641 | this.log("Step error: " + e, "error"); | ||
642 | } else { | ||
643 | throw e; | ||
644 | } | ||
645 | } | ||
646 | if (isType(this.options.onStepComplete, "function")) { | ||
647 | this.options.onStepComplete.call(this, this, stepResult); | ||
648 | } | ||
649 | if (!skipLog) { | ||
650 | this.log(stepInfo + ": done in " + (new Date().getTime() - this.startTime) + "ms.", "info"); | ||
651 | } | ||
652 | this.step++; | ||
653 | }, | ||
654 | |||
655 | /** | ||
656 | * Configures and starts Casper. | ||
657 | * | ||
658 | * @param String location An optional location to open on start | ||
659 | * @param function then Next step function to execute on page loaded (optional) | ||
660 | * @return Casper | ||
661 | */ | ||
662 | start: function(location, then) { | ||
663 | if (this.started) { | ||
664 | this.log("start failed: Casper has already started!", "error"); | ||
665 | } | ||
666 | this.log('Starting...', "info"); | ||
667 | this.startTime = new Date().getTime(); | ||
668 | this.steps = []; | ||
669 | this.step = 0; | ||
670 | // Option checks | ||
671 | if (this.logLevels.indexOf(this.options.logLevel) < 0) { | ||
672 | this.log("Unknown log level '" + this.options.logLevel + "', defaulting to 'warning'", "warning"); | ||
673 | this.options.logLevel = "warning"; | ||
674 | } | ||
675 | // WebPage | ||
676 | if (!isWebPage(this.page)) { | ||
677 | if (isWebPage(this.options.page)) { | ||
678 | this.page = this.options.page; | ||
679 | } else { | ||
680 | this.page = createPage(this); | ||
681 | } | ||
682 | } | ||
683 | this.page.settings = mergeObjects(this.page.settings, this.options.pageSettings); | ||
684 | if (isType(this.options.clipRect, "object")) { | ||
685 | this.page.clipRect = this.options.clipRect; | ||
686 | } | ||
687 | if (isType(this.options.viewportSize, "object")) { | ||
688 | this.page.viewportSize = this.options.viewportSize; | ||
689 | } | ||
690 | this.started = true; | ||
691 | if (isType(this.options.timeout, "number") && this.options.timeout > 0) { | ||
692 | this.log("Execution timeout set to " + this.options.timeout + 'ms', "info"); | ||
693 | setTimeout(function(self) { | ||
694 | if (isType(self.options.onTimeout, "function")) { | ||
695 | self.options.onTimeout.call(self, self); | ||
696 | } else { | ||
697 | self.die("Timeout of " + self.options.timeout + "ms exceeded, exiting."); | ||
698 | } | ||
699 | }, this.options.timeout, this); | ||
700 | } | ||
701 | if (isType(this.options.onPageInitialized, "function")) { | ||
702 | this.log("Post-configuring WebPage instance", "debug"); | ||
703 | this.options.onPageInitialized.call(this, this.page); | ||
704 | } | ||
705 | if (isType(location, "string") && location.length > 0) { | ||
706 | return this.thenOpen(location, isType(then, "function") ? then : this.createStep(function(self) { | ||
707 | self.log("start page is loaded", "debug"); | ||
708 | })); | ||
709 | } | ||
710 | return this; | ||
711 | }, | ||
712 | |||
713 | /** | ||
714 | * Schedules the next step in the navigation process. | ||
715 | * | ||
716 | * @param function step A function to be called as a step | ||
717 | * @return Casper | ||
718 | */ | ||
719 | then: function(step) { | ||
720 | if (!this.started) { | ||
721 | throw "Casper not started; please use Casper#start"; | ||
722 | } | ||
723 | if (!isType(step, "function")) { | ||
724 | throw "You can only define a step as a function"; | ||
725 | } | ||
726 | this.steps.push(step); | ||
727 | return this; | ||
728 | }, | ||
729 | |||
730 | /** | ||
731 | * Adds a new navigation step for clicking on a provided link selector | ||
732 | * and execute an optional next step. | ||
733 | * | ||
734 | * @param String selector A DOM CSS3 compatible selector | ||
735 | * @param Function then Next step function to execute on page loaded (optional) | ||
736 | * @param Boolean fallbackToHref Whether to try to relocate to the value of any href attribute (default: true) | ||
737 | * @return Casper | ||
738 | * @see Casper#click | ||
739 | * @see Casper#then | ||
740 | */ | ||
741 | thenClick: function(selector, then, fallbackToHref) { | ||
742 | this.then(function(self) { | ||
743 | self.click(selector, fallbackToHref); | ||
744 | }); | ||
745 | return isType(then, "function") ? this.then(then) : this; | ||
746 | }, | ||
747 | |||
748 | /** | ||
749 | * Adds a new navigation step to perform code evaluation within the | ||
750 | * current retrieved page DOM. | ||
751 | * | ||
752 | * @param function fn The function to be evaluated within current page DOM | ||
753 | * @param object context Optional function parameters context | ||
754 | * @return Casper | ||
755 | * @see Casper#evaluate | ||
756 | */ | ||
757 | thenEvaluate: function(fn, context) { | ||
758 | return this.then(function(self) { | ||
759 | self.evaluate(fn, context); | ||
760 | }); | ||
761 | }, | ||
762 | |||
763 | /** | ||
764 | * Adds a new navigation step for opening the provided location. | ||
765 | * | ||
766 | * @param String location The URL to load | ||
767 | * @param function then Next step function to execute on page loaded (optional) | ||
768 | * @return Casper | ||
769 | * @see Casper#open | ||
770 | */ | ||
771 | thenOpen: function(location, then) { | ||
772 | this.then(this.createStep(function(self) { | ||
773 | self.open(location); | ||
774 | }, { | ||
775 | skipLog: true | ||
776 | })); | ||
777 | return isType(then, "function") ? this.then(then) : this; | ||
778 | }, | ||
779 | |||
780 | /** | ||
781 | * Adds a new navigation step for opening and evaluate an expression | ||
782 | * against the DOM retrieved from the provided location. | ||
783 | * | ||
784 | * @param String location The url to open | ||
785 | * @param function fn The function to be evaluated within current page DOM | ||
786 | * @param object context Optional function parameters context | ||
787 | * @return Casper | ||
788 | * @see Casper#evaluate | ||
789 | * @see Casper#open | ||
790 | */ | ||
791 | thenOpenAndEvaluate: function(location, fn, context) { | ||
792 | return this.thenOpen(location).thenEvaluate(fn, context); | ||
793 | }, | ||
794 | |||
795 | /** | ||
796 | * Changes the current viewport size. | ||
797 | * | ||
798 | * @param Number width The viewport width, in pixels | ||
799 | * @param Number height The viewport height, in pixels | ||
800 | * @return Casper | ||
801 | */ | ||
802 | viewport: function(width, height) { | ||
803 | if (!isType(width, "number") || !isType(height, "number") || width <= 0 || height <= 0) { | ||
804 | throw new Error("Invalid viewport width/height set: " + width + 'x' + height); | ||
805 | } | ||
806 | this.page.viewportSize = { | ||
807 | width: width, | ||
808 | height: height | ||
809 | }; | ||
810 | return this; | ||
811 | }, | ||
812 | |||
813 | /** | ||
814 | * Adds a new step that will wait for a given amount of time (expressed | ||
815 | * in milliseconds) before processing an optional next one. | ||
816 | * | ||
817 | * @param Number timeout The max amount of time to wait, in milliseconds | ||
818 | * @param Function then Next step to process (optional) | ||
819 | * @return Casper | ||
820 | */ | ||
821 | wait: function(timeout, then) { | ||
822 | timeout = Number(timeout, 10); | ||
823 | if (!isType(timeout, "number") || timeout < 1) { | ||
824 | this.die("wait() only accepts a positive integer > 0 as a timeout value"); | ||
825 | } | ||
826 | if (then && !isType(then, "function")) { | ||
827 | this.die("wait() a step definition must be a function"); | ||
828 | } | ||
829 | return this.then(function(self) { | ||
830 | self.delayedExecution = true; | ||
831 | var start = new Date().getTime(); | ||
832 | var interval = setInterval(function(self, then) { | ||
833 | if (new Date().getTime() - start > timeout) { | ||
834 | self.delayedExecution = false; | ||
835 | self.log("wait() finished wating for " + timeout + "ms.", "info"); | ||
836 | if (then) { | ||
837 | self.then(then); | ||
838 | } | ||
839 | clearInterval(interval); | ||
840 | } | ||
841 | }, 100, self, then); | ||
842 | }); | ||
843 | }, | ||
844 | |||
845 | /** | ||
846 | * Waits until a function returns true to process a next step. | ||
847 | * | ||
848 | * @param Function testFx A function to be evaluated for returning condition satisfecit | ||
849 | * @param Function then The next step to perform (optional) | ||
850 | * @param Function onTimeout A callback function to call on timeout (optional) | ||
851 | * @param Number timeout The max amount of time to wait, in milliseconds (optional) | ||
852 | * @return Casper | ||
853 | */ | ||
854 | waitFor: function(testFx, then, onTimeout, timeout) { | ||
855 | timeout = timeout ? timeout : this.defaultWaitTimeout; | ||
856 | if (!isType(testFx, "function")) { | ||
857 | this.die("waitFor() needs a test function"); | ||
858 | } | ||
859 | if (then && !isType(then, "function")) { | ||
860 | this.die("waitFor() next step definition must be a function"); | ||
861 | } | ||
862 | this.delayedExecution = true; | ||
863 | var start = new Date().getTime(); | ||
864 | var condition = false; | ||
865 | var interval = setInterval(function(self, testFx, onTimeout) { | ||
866 | if ((new Date().getTime() - start < timeout) && !condition) { | ||
867 | condition = testFx(self); | ||
868 | } else { | ||
869 | self.delayedExecution = false; | ||
870 | if (!condition) { | ||
871 | self.log("Casper.waitFor() timeout", "warning"); | ||
872 | if (isType(onTimeout, "function")) { | ||
873 | onTimeout.call(self, self); | ||
874 | } else { | ||
875 | self.die("Expired timeout, exiting.", "error"); | ||
876 | } | ||
877 | clearInterval(interval); | ||
878 | } else { | ||
879 | self.log("waitFor() finished in " + (new Date().getTime() - start) + "ms.", "info"); | ||
880 | if (then) { | ||
881 | self.then(then); | ||
882 | } | ||
883 | clearInterval(interval); | ||
884 | } | ||
885 | } | ||
886 | }, 100, this, testFx, onTimeout); | ||
887 | return this; | ||
888 | }, | ||
889 | |||
890 | /** | ||
891 | * Waits until an element matching the provided CSS3 selector exists in | ||
892 | * remote DOM to process a next step. | ||
893 | * | ||
894 | * @param String selector A CSS3 selector | ||
895 | * @param Function then The next step to perform (optional) | ||
896 | * @param Function onTimeout A callback function to call on timeout (optional) | ||
897 | * @param Number timeout The max amount of time to wait, in milliseconds (optional) | ||
898 | * @return Casper | ||
899 | */ | ||
900 | waitForSelector: function(selector, then, onTimeout, timeout) { | ||
901 | timeout = timeout ? timeout : this.defaultWaitTimeout; | ||
902 | return this.waitFor(function(self) { | ||
903 | return self.exists(selector); | ||
904 | }, then, onTimeout, timeout); | ||
905 | }, | ||
906 | |||
907 | /** | ||
908 | * Waits until an element matching the provided CSS3 selector does not | ||
909 | * exist in the remote DOM to process a next step. | ||
910 | * | ||
911 | * @param String selector A CSS3 selector | ||
912 | * @param Function then The next step to perform (optional) | ||
913 | * @param Function onTimeout A callback function to call on timeout (optional) | ||
914 | * @param Number timeout The max amount of time to wait, in milliseconds (optional) | ||
915 | * @return Casper | ||
916 | */ | ||
917 | waitWhileSelector: function(selector, then, onTimeout, timeout) { | ||
918 | timeout = timeout ? timeout : this.defaultWaitTimeout; | ||
919 | return this.waitFor(function(self) { | ||
920 | return !self.exists(selector); | ||
921 | }, then, onTimeout, timeout); | ||
922 | }, | ||
923 | |||
924 | /** | ||
925 | * Waits until an element matching the provided CSS3 selector is | ||
926 | * visible in the remote DOM to process a next step. | ||
927 | * | ||
928 | * @param String selector A CSS3 selector | ||
929 | * @param Function then The next step to perform (optional) | ||
930 | * @param Function onTimeout A callback function to call on timeout (optional) | ||
931 | * @param Number timeout The max amount of time to wait, in milliseconds (optional) | ||
932 | * @return Casper | ||
933 | */ | ||
934 | waitUntilVisible: function(selector, then, onTimeout, timeout) { | ||
935 | timeout = timeout ? timeout : this.defaultWaitTimeout; | ||
936 | return this.waitFor(function(self) { | ||
937 | return self.visible(selector); | ||
938 | }, then, onTimeout, timeout); | ||
939 | }, | ||
940 | |||
941 | /** | ||
942 | * Waits until an element matching the provided CSS3 selector is no | ||
943 | * longer visible in remote DOM to process a next step. | ||
944 | * | ||
945 | * @param String selector A CSS3 selector | ||
946 | * @param Function then The next step to perform (optional) | ||
947 | * @param Function onTimeout A callback function to call on timeout (optional) | ||
948 | * @param Number timeout The max amount of time to wait, in milliseconds (optional) | ||
949 | * @return Casper | ||
950 | */ | ||
951 | waitWhileVisible: function(selector, then, onTimeout, timeout) { | ||
952 | timeout = timeout ? timeout : this.defaultWaitTimeout; | ||
953 | return this.waitFor(function(self) { | ||
954 | return !self.visible(selector); | ||
955 | }, then, onTimeout, timeout); | ||
956 | } | ||
957 | }; | ||
958 | |||
959 | /** | ||
960 | * Extends Casper's prototype with provided one. | ||
961 | * | ||
962 | * @param Object proto Prototype methods to add to Casper | ||
963 | */ | ||
964 | phantom.Casper.extend = function(proto) { | ||
965 | if (!isType(proto, "object")) { | ||
966 | throw "extends() only accept objects as prototypes"; | ||
967 | } | ||
968 | mergeObjects(phantom.Casper.prototype, proto); | ||
969 | }; | ||
970 | })(phantom); |
lib/clientutils.js
0 → 100644
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 | * Casper client-side helpers. | ||
31 | */ | ||
32 | phantom.Casper.ClientUtils = function() { | ||
33 | /** | ||
34 | * Clicks on the DOM element behind the provided selector. | ||
35 | * | ||
36 | * @param String selector A CSS3 selector to the element to click | ||
37 | * @param Boolean fallbackToHref Whether to try to relocate to the value of any href attribute (default: true) | ||
38 | * @return Boolean | ||
39 | */ | ||
40 | this.click = function(selector, fallbackToHref) { | ||
41 | fallbackToHref = typeof fallbackToHref === "undefined" ? true : !!fallbackToHref; | ||
42 | var elem = this.findOne(selector); | ||
43 | if (!elem) { | ||
44 | return false; | ||
45 | } | ||
46 | var evt = document.createEvent("MouseEvents"); | ||
47 | evt.initMouseEvent("click", true, true, window, 1, 1, 1, 1, 1, false, false, false, false, 0, elem); | ||
48 | if (elem.dispatchEvent(evt)) { | ||
49 | return true; | ||
50 | } | ||
51 | if (fallbackToHref && elem.hasAttribute('href')) { | ||
52 | document.location = elem.getAttribute('href'); | ||
53 | return true; | ||
54 | } | ||
55 | return false; | ||
56 | }; | ||
57 | |||
58 | /** | ||
59 | * Base64 encodes a string, even binary ones. Succeeds where | ||
60 | * window.btoa() fails. | ||
61 | * | ||
62 | * @param String str | ||
63 | * @return string | ||
64 | */ | ||
65 | this.encode = function(str) { | ||
66 | var CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; | ||
67 | var out = "", i = 0, len = str.length, c1, c2, c3; | ||
68 | while (i < len) { | ||
69 | c1 = str.charCodeAt(i++) & 0xff; | ||
70 | if (i == len) { | ||
71 | out += CHARS.charAt(c1 >> 2); | ||
72 | out += CHARS.charAt((c1 & 0x3) << 4); | ||
73 | out += "=="; | ||
74 | break; | ||
75 | } | ||
76 | c2 = str.charCodeAt(i++); | ||
77 | if (i == len) { | ||
78 | out += CHARS.charAt(c1 >> 2); | ||
79 | out += CHARS.charAt(((c1 & 0x3)<< 4) | ((c2 & 0xF0) >> 4)); | ||
80 | out += CHARS.charAt((c2 & 0xF) << 2); | ||
81 | out += "="; | ||
82 | break; | ||
83 | } | ||
84 | c3 = str.charCodeAt(i++); | ||
85 | out += CHARS.charAt(c1 >> 2); | ||
86 | out += CHARS.charAt(((c1 & 0x3) << 4) | ((c2 & 0xF0) >> 4)); | ||
87 | out += CHARS.charAt(((c2 & 0xF) << 2) | ((c3 & 0xC0) >> 6)); | ||
88 | out += CHARS.charAt(c3 & 0x3F); | ||
89 | } | ||
90 | return out; | ||
91 | }; | ||
92 | |||
93 | /** | ||
94 | * Checks if a given DOM element exists in remote page. | ||
95 | * | ||
96 | * @param String selector CSS3 selector | ||
97 | * @return Boolean | ||
98 | */ | ||
99 | this.exists = function(selector) { | ||
100 | try { | ||
101 | return document.querySelectorAll(selector).length > 0; | ||
102 | } catch (e) { | ||
103 | return false; | ||
104 | } | ||
105 | }; | ||
106 | |||
107 | /** | ||
108 | * Checks if a given DOM element is visible in remote page. | ||
109 | * | ||
110 | * @param String selector CSS3 selector | ||
111 | * @return Boolean | ||
112 | */ | ||
113 | this.visible = function(selector) { | ||
114 | try { | ||
115 | var el = document.querySelector(selector); | ||
116 | return el && el.style.visibility !== 'hidden' && el.offsetHeight > 0 && el.offsetWidth > 0; | ||
117 | } catch (e) { | ||
118 | return false; | ||
119 | } | ||
120 | }; | ||
121 | |||
122 | /** | ||
123 | * Fetches innerText within the element(s) matching a given CSS3 | ||
124 | * selector. | ||
125 | * | ||
126 | * @param String selector A CSS3 selector | ||
127 | * @return String | ||
128 | */ | ||
129 | this.fetchText = function(selector) { | ||
130 | var text = '', elements = this.findAll(selector); | ||
131 | if (elements && elements.length) { | ||
132 | Array.prototype.forEach.call(elements, function(element) { | ||
133 | text += element.innerText; | ||
134 | }); | ||
135 | } | ||
136 | return text; | ||
137 | }; | ||
138 | |||
139 | /** | ||
140 | * Fills a form with provided field values, and optionnaly submits it. | ||
141 | * | ||
142 | * @param HTMLElement|String form A form element, or a CSS3 selector to a form element | ||
143 | * @param Object vals Field values | ||
144 | * @return Object An object containing setting result for each field, including file uploads | ||
145 | */ | ||
146 | this.fill = function(form, vals) { | ||
147 | var out = { | ||
148 | errors: [], | ||
149 | fields: [], | ||
150 | files: [] | ||
151 | }; | ||
152 | if (!(form instanceof HTMLElement) || typeof form === "string") { | ||
153 | __utils__.log("attempting to fetch form element from selector: '" + form + "'", "info"); | ||
154 | try { | ||
155 | form = document.querySelector(form); | ||
156 | } catch (e) { | ||
157 | if (e.name === "SYNTAX_ERR") { | ||
158 | out.errors.push("invalid form selector provided: '" + form + "'"); | ||
159 | return out; | ||
160 | } | ||
161 | } | ||
162 | } | ||
163 | if (!form) { | ||
164 | out.errors.push("form not found"); | ||
165 | return out; | ||
166 | } | ||
167 | for (var name in vals) { | ||
168 | if (!vals.hasOwnProperty(name)) { | ||
169 | continue; | ||
170 | } | ||
171 | var field = form.querySelectorAll('[name="' + name + '"]'); | ||
172 | var value = vals[name]; | ||
173 | if (!field) { | ||
174 | out.errors.push('no field named "' + name + '" in form'); | ||
175 | continue; | ||
176 | } | ||
177 | try { | ||
178 | out.fields[name] = this.setField(field, value); | ||
179 | } catch (err) { | ||
180 | if (err.name === "FileUploadError") { | ||
181 | out.files.push({ | ||
182 | name: name, | ||
183 | path: err.path | ||
184 | }); | ||
185 | } else { | ||
186 | this.log(err, "error"); | ||
187 | throw err; | ||
188 | } | ||
189 | } | ||
190 | } | ||
191 | return out; | ||
192 | }; | ||
193 | |||
194 | /** | ||
195 | * Finds all DOM elements matching by the provided selector. | ||
196 | * | ||
197 | * @param String selector CSS3 selector | ||
198 | * @return NodeList|undefined | ||
199 | */ | ||
200 | this.findAll = function(selector) { | ||
201 | try { | ||
202 | return document.querySelectorAll(selector); | ||
203 | } catch (e) { | ||
204 | this.log('findAll(): invalid selector provided "' + selector + '":' + e, "error"); | ||
205 | } | ||
206 | }; | ||
207 | |||
208 | /** | ||
209 | * Finds a DOM element by the provided selector. | ||
210 | * | ||
211 | * @param String selector CSS3 selector | ||
212 | * @return HTMLElement|undefined | ||
213 | */ | ||
214 | this.findOne = function(selector) { | ||
215 | try { | ||
216 | return document.querySelector(selector); | ||
217 | } catch (e) { | ||
218 | this.log('findOne(): invalid selector provided "' + selector + '":' + e, "errors"); | ||
219 | } | ||
220 | }; | ||
221 | |||
222 | /** | ||
223 | * Downloads a resource behind an url and returns its base64-encoded | ||
224 | * contents. | ||
225 | * | ||
226 | * @param String url The resource url | ||
227 | * @return String Base64 contents string | ||
228 | */ | ||
229 | this.getBase64 = function(url) { | ||
230 | return this.encode(this.getBinary(url)); | ||
231 | }; | ||
232 | |||
233 | /** | ||
234 | * Retrieves string contents from a binary file behind an url. Silently | ||
235 | * fails but log errors. | ||
236 | * | ||
237 | * @param String url | ||
238 | * @return string | ||
239 | */ | ||
240 | this.getBinary = function(url) { | ||
241 | try { | ||
242 | var xhr = new XMLHttpRequest(); | ||
243 | xhr.open("GET", url, false); | ||
244 | xhr.overrideMimeType("text/plain; charset=x-user-defined"); | ||
245 | xhr.send(null); | ||
246 | return xhr.responseText; | ||
247 | } catch (e) { | ||
248 | if (e.name === "NETWORK_ERR" && e.code === 101) { | ||
249 | this.log("unfortunately, casperjs cannot make cross domain ajax requests", "warning"); | ||
250 | } | ||
251 | this.log("error while fetching " + url + ": " + e, "error"); | ||
252 | return ""; | ||
253 | } | ||
254 | }; | ||
255 | |||
256 | /** | ||
257 | * Logs a message. | ||
258 | * | ||
259 | * @param String message | ||
260 | * @param String level | ||
261 | */ | ||
262 | this.log = function(message, level) { | ||
263 | console.log("[casper:" + (level || "debug") + "] " + message); | ||
264 | }; | ||
265 | |||
266 | /** | ||
267 | * Sets a field (or a set of fields) value. Fails silently, but log | ||
268 | * error messages. | ||
269 | * | ||
270 | * @param HTMLElement|NodeList field One or more element defining a field | ||
271 | * @param mixed value The field value to set | ||
272 | */ | ||
273 | this.setField = function(field, value) { | ||
274 | var fields, out; | ||
275 | value = value || ""; | ||
276 | if (field instanceof NodeList) { | ||
277 | fields = field; | ||
278 | field = fields[0]; | ||
279 | } | ||
280 | if (!field instanceof HTMLElement) { | ||
281 | this.log("invalid field type; only HTMLElement and NodeList are supported", "error"); | ||
282 | } | ||
283 | this.log('set "' + field.getAttribute('name') + '" field value to ' + value, "debug"); | ||
284 | try { | ||
285 | field.focus(); | ||
286 | } catch (e) { | ||
287 | __utils__.log("Unable to focus() input field " + field.getAttribute('name') + ": " + e, "warning"); | ||
288 | } | ||
289 | var nodeName = field.nodeName.toLowerCase(); | ||
290 | switch (nodeName) { | ||
291 | case "input": | ||
292 | var type = field.getAttribute('type') || "text"; | ||
293 | switch (type.toLowerCase()) { | ||
294 | case "color": | ||
295 | case "date": | ||
296 | case "datetime": | ||
297 | case "datetime-local": | ||
298 | case "email": | ||
299 | case "hidden": | ||
300 | case "month": | ||
301 | case "number": | ||
302 | case "password": | ||
303 | case "range": | ||
304 | case "search": | ||
305 | case "tel": | ||
306 | case "text": | ||
307 | case "time": | ||
308 | case "url": | ||
309 | case "week": | ||
310 | field.value = value; | ||
311 | break; | ||
312 | case "checkbox": | ||
313 | if (fields.length > 1) { | ||
314 | var values = value; | ||
315 | if (!Array.isArray(values)) { | ||
316 | values = [values]; | ||
317 | } | ||
318 | Array.prototype.forEach.call(fields, function(f) { | ||
319 | f.checked = values.indexOf(f.value) !== -1 ? true : false; | ||
320 | }); | ||
321 | } else { | ||
322 | field.checked = value ? true : false; | ||
323 | } | ||
324 | break; | ||
325 | case "file": | ||
326 | throw { | ||
327 | name: "FileUploadError", | ||
328 | message: "file field must be filled using page.uploadFile", | ||
329 | path: value | ||
330 | }; | ||
331 | case "radio": | ||
332 | if (fields) { | ||
333 | Array.prototype.forEach.call(fields, function(e) { | ||
334 | e.checked = (e.value === value); | ||
335 | }); | ||
336 | } else { | ||
337 | out = 'provided radio elements are empty'; | ||
338 | } | ||
339 | break; | ||
340 | default: | ||
341 | out = "unsupported input field type: " + type; | ||
342 | break; | ||
343 | } | ||
344 | break; | ||
345 | case "select": | ||
346 | case "textarea": | ||
347 | field.value = value; | ||
348 | break; | ||
349 | default: | ||
350 | out = 'unsupported field type: ' + nodeName; | ||
351 | break; | ||
352 | } | ||
353 | try { | ||
354 | field.blur(); | ||
355 | } catch (err) { | ||
356 | __utils__.log("Unable to blur() input field " + field.getAttribute('name') + ": " + err, "warning"); | ||
357 | } | ||
358 | return out; | ||
359 | }; | ||
360 | }; | ||
361 | })(phantom); | ||
... | \ No newline at end of file | ... | \ No newline at end of file |
lib/colorizer.js
0 → 100644
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 | * This is a port of lime colorizer. | ||
31 | * http://trac.symfony-project.org/browser/tools/lime/trunk/lib/lime.php) | ||
32 | * | ||
33 | * (c) Fabien Potencier, Symfony project, MIT license | ||
34 | */ | ||
35 | phantom.Casper.Colorizer = function() { | ||
36 | var options = { bold: 1, underscore: 4, blink: 5, reverse: 7, conceal: 8 }; | ||
37 | var foreground = { black: 30, red: 31, green: 32, yellow: 33, blue: 34, magenta: 35, cyan: 36, white: 37 }; | ||
38 | var background = { black: 40, red: 41, green: 42, yellow: 43, blue: 44, magenta: 45, cyan: 46, white: 47 }; | ||
39 | var styles = { | ||
40 | 'ERROR': { bg: 'red', fg: 'white', bold: true }, | ||
41 | 'INFO': { fg: 'green', bold: true }, | ||
42 | 'TRACE': { fg: 'green', bold: true }, | ||
43 | 'PARAMETER': { fg: 'cyan' }, | ||
44 | 'COMMENT': { fg: 'yellow' }, | ||
45 | 'WARNING': { fg: 'red', bold: true }, | ||
46 | 'GREEN_BAR': { fg: 'white', bg: 'green', bold: true }, | ||
47 | 'RED_BAR': { fg: 'white', bg: 'red', bold: true }, | ||
48 | 'INFO_BAR': { fg: 'cyan', bold: true } | ||
49 | }; | ||
50 | |||
51 | /** | ||
52 | * Adds a style to provided text. | ||
53 | * | ||
54 | * @params String text | ||
55 | * @params String styleName | ||
56 | * @return String | ||
57 | */ | ||
58 | this.colorize = function(text, styleName) { | ||
59 | if (styleName in styles) { | ||
60 | return this.format(text, styles[styleName]); | ||
61 | } | ||
62 | return text; | ||
63 | }; | ||
64 | |||
65 | /** | ||
66 | * Formats a text using a style declaration object. | ||
67 | * | ||
68 | * @param String text | ||
69 | * @param Object style | ||
70 | * @return String | ||
71 | */ | ||
72 | this.format = function(text, style) { | ||
73 | if (typeof style !== "object") { | ||
74 | return text; | ||
75 | } | ||
76 | var codes = []; | ||
77 | if (style.fg && foreground[style.fg]) { | ||
78 | codes.push(foreground[style.fg]); | ||
79 | } | ||
80 | if (style.bg && background[style.bg]) { | ||
81 | codes.push(background[style.bg]); | ||
82 | } | ||
83 | for (var option in options) { | ||
84 | if (style[option] === true) { | ||
85 | codes.push(options[option]); | ||
86 | } | ||
87 | } | ||
88 | return "\033[" + codes.join(';') + 'm' + text + "\033[0m"; | ||
89 | }; | ||
90 | }; | ||
91 | })(phantom); | ||
... | \ No newline at end of file | ... | \ No newline at end of file |
lib/injector.js
0 → 100644
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 | * Function argument injector. | ||
31 | * | ||
32 | */ | ||
33 | phantom.Casper.FunctionArgsInjector = function(fn) { | ||
34 | if (!isType(fn, "function")) { | ||
35 | throw "FunctionArgsInjector() can only process functions"; | ||
36 | } | ||
37 | this.fn = fn; | ||
38 | |||
39 | this.extract = function(fn) { | ||
40 | var match = /^function\s?(\w+)?\s?\((.*)\)\s?\{([\s\S]*)\}/i.exec(fn.toString().trim()); | ||
41 | if (match && match.length > 1) { | ||
42 | var args = match[2].split(',').map(function(arg) { | ||
43 | return arg.replace(new RegExp(/\/\*+.*\*\//ig), "").trim(); | ||
44 | }).filter(function(arg) { | ||
45 | return arg; | ||
46 | }) || []; | ||
47 | return { | ||
48 | name: match[1] ? match[1].trim() : null, | ||
49 | args: args, | ||
50 | body: match[3] ? match[3].trim() : '' | ||
51 | }; | ||
52 | } | ||
53 | }; | ||
54 | |||
55 | this.process = function(values) { | ||
56 | var fnObj = this.extract(this.fn); | ||
57 | if (!isType(fnObj, "object")) { | ||
58 | throw "Unable to process function " + this.fn.toString(); | ||
59 | } | ||
60 | var inject = this.getArgsInjectionString(fnObj.args, values); | ||
61 | return 'function ' + (fnObj.name || '') + '(){' + inject + fnObj.body + '}'; | ||
62 | }; | ||
63 | |||
64 | this.getArgsInjectionString = function(args, values) { | ||
65 | values = typeof values === "object" ? values : {}; | ||
66 | var jsonValues = escape(encodeURIComponent(JSON.stringify(values))); | ||
67 | var inject = [ | ||
68 | 'var __casper_params__ = JSON.parse(decodeURIComponent(unescape(\'' + jsonValues + '\')));' | ||
69 | ]; | ||
70 | args.forEach(function(arg) { | ||
71 | if (arg in values) { | ||
72 | inject.push('var ' + arg + '=__casper_params__["' + arg + '"];'); | ||
73 | } | ||
74 | }); | ||
75 | return inject.join('\n') + '\n'; | ||
76 | }; | ||
77 | }; | ||
78 | })(phantom); | ||
... | \ No newline at end of file | ... | \ No newline at end of file |
lib/tester.js
0 → 100644
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 | * Casper tester: makes assertions, stores test results and display then. | ||
31 | * | ||
32 | */ | ||
33 | phantom.Casper.Tester = function(casper, options) { | ||
34 | this.options = isType(options, "object") ? options : {}; | ||
35 | if (!casper instanceof phantom.Casper) { | ||
36 | throw "phantom.Casper.Tester needs a phantom.Casper instance"; | ||
37 | } | ||
38 | |||
39 | // locals | ||
40 | var exporter = new phantom.Casper.XUnitExporter(); | ||
41 | var PASS = this.options.PASS || "PASS"; | ||
42 | var FAIL = this.options.FAIL || "FAIL"; | ||
43 | |||
44 | function compareArrays(a, b) { | ||
45 | if (a.length !== b.length) { | ||
46 | return false; | ||
47 | } | ||
48 | a.forEach(function(item, i) { | ||
49 | if (isType(item, "array") && !compareArrays(item, b[i])) { | ||
50 | return false; | ||
51 | } | ||
52 | if (item !== b[i]) { | ||
53 | return false; | ||
54 | } | ||
55 | }); | ||
56 | return true; | ||
57 | } | ||
58 | |||
59 | // properties | ||
60 | this.testResults = { | ||
61 | passed: 0, | ||
62 | failed: 0 | ||
63 | }; | ||
64 | |||
65 | // methods | ||
66 | /** | ||
67 | * Asserts a condition resolves to true. | ||
68 | * | ||
69 | * @param Boolean condition | ||
70 | * @param String message Test description | ||
71 | */ | ||
72 | this.assert = function(condition, message) { | ||
73 | var status = PASS; | ||
74 | if (condition === true) { | ||
75 | style = 'INFO'; | ||
76 | this.testResults.passed++; | ||
77 | exporter.addSuccess("unknown", message); | ||
78 | } else { | ||
79 | status = FAIL; | ||
80 | style = 'RED_BAR'; | ||
81 | this.testResults.failed++; | ||
82 | exporter.addFailure("unknown", message, 'test failed', "assert"); | ||
83 | } | ||
84 | casper.echo([this.colorize(status, style), this.formatMessage(message)].join(' ')); | ||
85 | }; | ||
86 | |||
87 | /** | ||
88 | * Asserts that two values are strictly equals. | ||
89 | * | ||
90 | * @param Mixed testValue The value to test | ||
91 | * @param Mixed expected The expected value | ||
92 | * @param String message Test description | ||
93 | */ | ||
94 | this.assertEquals = function(testValue, expected, message) { | ||
95 | if (this.testEquals(testValue, expected)) { | ||
96 | casper.echo(this.colorize(PASS, 'INFO') + ' ' + this.formatMessage(message)); | ||
97 | this.testResults.passed++; | ||
98 | exporter.addSuccess("unknown", message); | ||
99 | } else { | ||
100 | casper.echo(this.colorize(FAIL, 'RED_BAR') + ' ' + this.formatMessage(message, 'WARNING')); | ||
101 | this.comment(' got: ' + testValue); | ||
102 | this.comment(' expected: ' + expected); | ||
103 | this.testResults.failed++; | ||
104 | exporter.addFailure("unknown", message, "test failed; expected: " + expected + "; got: " + testValue, "assertEquals"); | ||
105 | } | ||
106 | }; | ||
107 | |||
108 | /** | ||
109 | * Asserts that a code evaluation in remote DOM resolves to true. | ||
110 | * | ||
111 | * @param Function fn A function to be evaluated in remote DOM | ||
112 | * @param String message Test description | ||
113 | */ | ||
114 | this.assertEval = function(fn, message) { | ||
115 | return this.assert(casper.evaluate(fn), message); | ||
116 | }; | ||
117 | |||
118 | /** | ||
119 | * Asserts that the result of a code evaluation in remote DOM equals | ||
120 | * an expected value. | ||
121 | * | ||
122 | * @param Function fn The function to be evaluated in remote DOM | ||
123 | * @param Boolean expected The expected value | ||
124 | * @param String message Test description | ||
125 | */ | ||
126 | this.assertEvalEquals = function(fn, expected, message) { | ||
127 | return this.assertEquals(casper.evaluate(fn), expected, message); | ||
128 | }; | ||
129 | |||
130 | /** | ||
131 | * Asserts that an element matching the provided CSS3 selector exists in | ||
132 | * remote DOM. | ||
133 | * | ||
134 | * @param String selector CSS3 selectore | ||
135 | * @param String message Test description | ||
136 | */ | ||
137 | this.assertExists = function(selector, message) { | ||
138 | return this.assert(casper.exists(selector), message); | ||
139 | }; | ||
140 | |||
141 | /** | ||
142 | * Asserts that a provided string matches a provided RegExp pattern. | ||
143 | * | ||
144 | * @param String subject The string to test | ||
145 | * @param RegExp pattern A RegExp object instance | ||
146 | * @param String message Test description | ||
147 | */ | ||
148 | this.assertMatch = function(subject, pattern, message) { | ||
149 | if (pattern.test(subject)) { | ||
150 | casper.echo(this.colorize(PASS, 'INFO') + ' ' + this.formatMessage(message)); | ||
151 | this.testResults.passed++; | ||
152 | exporter.addSuccess("unknown", message); | ||
153 | } else { | ||
154 | casper.echo(this.colorize(FAIL, 'RED_BAR') + ' ' + this.formatMessage(message, 'WARNING')); | ||
155 | this.comment(' subject: ' + subject); | ||
156 | this.comment(' pattern: ' + pattern.toString()); | ||
157 | this.testResults.failed++; | ||
158 | exporter.addFailure("unknown", message, "test failed; subject: " + subject + "; pattern: " + pattern.toString(), "assertMatch"); | ||
159 | } | ||
160 | }; | ||
161 | |||
162 | /** | ||
163 | * Asserts a condition resolves to false. | ||
164 | * | ||
165 | * @param Boolean condition | ||
166 | * @param String message Test description | ||
167 | */ | ||
168 | this.assertNot = function(condition, message) { | ||
169 | return this.assert(!condition, message); | ||
170 | }; | ||
171 | |||
172 | /** | ||
173 | * Asserts that the provided function called with the given parameters | ||
174 | * will raise an exception. | ||
175 | * | ||
176 | * @param Function fn The function to test | ||
177 | * @param Array args The arguments to pass to the function | ||
178 | * @param String message Test description | ||
179 | */ | ||
180 | this.assertRaises = function(fn, args, message) { | ||
181 | try { | ||
182 | fn.apply(null, args); | ||
183 | this.fail(message); | ||
184 | } catch (e) { | ||
185 | this.pass(message); | ||
186 | } | ||
187 | }; | ||
188 | |||
189 | /** | ||
190 | * Asserts that at least an element matching the provided CSS3 selector | ||
191 | * exists in remote DOM. | ||
192 | * | ||
193 | * @param String selector A CSS3 selector string | ||
194 | * @param String message Test description | ||
195 | */ | ||
196 | this.assertSelectorExists = function(selector, message) { | ||
197 | return this.assert(this.exists(selector), message); | ||
198 | }; | ||
199 | |||
200 | /** | ||
201 | * Asserts that title of the remote page equals to the expected one. | ||
202 | * | ||
203 | * @param String expected The expected title string | ||
204 | * @param String message Test description | ||
205 | */ | ||
206 | this.assertTitle = function(expected, message) { | ||
207 | return this.assertEquals(casper.getTitle(), expected, message); | ||
208 | }; | ||
209 | |||
210 | /** | ||
211 | * Asserts that the provided input is of the given type. | ||
212 | * | ||
213 | * @param mixed input The value to test | ||
214 | * @param String type The javascript type name | ||
215 | * @param String message Test description | ||
216 | */ | ||
217 | this.assertType = function(input, type, message) { | ||
218 | return this.assertEquals(betterTypeOf(input), type, message); | ||
219 | }; | ||
220 | |||
221 | /** | ||
222 | * Asserts that a the current page url matches the provided RegExp | ||
223 | * pattern. | ||
224 | * | ||
225 | * @param RegExp pattern A RegExp object instance | ||
226 | * @param String message Test description | ||
227 | */ | ||
228 | this.assertUrlMatch = function(pattern, message) { | ||
229 | return this.assertMatch(casper.getCurrentUrl(), pattern, message); | ||
230 | }; | ||
231 | |||
232 | /** | ||
233 | * Render a colorized output. Basically a proxy method for | ||
234 | * Casper.Colorizer#colorize() | ||
235 | */ | ||
236 | this.colorize = function(message, style) { | ||
237 | return casper.colorizer.colorize(message, style); | ||
238 | }; | ||
239 | |||
240 | /** | ||
241 | * Writes a comment-style formatted message to stdout. | ||
242 | * | ||
243 | * @param String message | ||
244 | */ | ||
245 | this.comment = function(message) { | ||
246 | casper.echo('# ' + message, 'COMMENT'); | ||
247 | }; | ||
248 | |||
249 | /** | ||
250 | * Tests equality between the two passed arguments. | ||
251 | * | ||
252 | * @param Mixed v1 | ||
253 | * @param Mixed v2 | ||
254 | * @param Boolean | ||
255 | */ | ||
256 | this.testEquals = function(v1, v2) { | ||
257 | if (betterTypeOf(v1) !== betterTypeOf(v2)) { | ||
258 | return false; | ||
259 | } | ||
260 | if (isType(v1, "function")) { | ||
261 | return v1.toString() === v2.toString(); | ||
262 | } | ||
263 | if (v1 instanceof Object && v2 instanceof Object) { | ||
264 | if (Object.keys(v1).length !== Object.keys(v2).length) { | ||
265 | return false; | ||
266 | } | ||
267 | for (var k in v1) { | ||
268 | if (!this.testEquals(v1[k], v2[k])) { | ||
269 | return false; | ||
270 | } | ||
271 | } | ||
272 | return true; | ||
273 | } | ||
274 | return v1 === v2; | ||
275 | }; | ||
276 | |||
277 | /** | ||
278 | * Writes an error-style formatted message to stdout. | ||
279 | * | ||
280 | * @param String message | ||
281 | */ | ||
282 | this.error = function(message) { | ||
283 | casper.echo(message, 'ERROR'); | ||
284 | }; | ||
285 | |||
286 | /** | ||
287 | * Adds a failed test entry to the stack. | ||
288 | * | ||
289 | * @param String message | ||
290 | */ | ||
291 | this.fail = function(message) { | ||
292 | this.assert(false, message); | ||
293 | }; | ||
294 | |||
295 | /** | ||
296 | * Formats a message to highlight some parts of it. | ||
297 | * | ||
298 | * @param String message | ||
299 | * @param String style | ||
300 | */ | ||
301 | this.formatMessage = function(message, style) { | ||
302 | var parts = /([a-z0-9_\.]+\(\))(.*)/i.exec(message); | ||
303 | if (!parts) { | ||
304 | return message; | ||
305 | } | ||
306 | return this.colorize(parts[1], 'PARAMETER') + this.colorize(parts[2], style); | ||
307 | }; | ||
308 | |||
309 | /** | ||
310 | * Writes an info-style formatted message to stdout. | ||
311 | * | ||
312 | * @param String message | ||
313 | */ | ||
314 | this.info = function(message) { | ||
315 | casper.echo(message, 'PARAMETER'); | ||
316 | }; | ||
317 | |||
318 | /** | ||
319 | * Adds a successful test entry to the stack. | ||
320 | * | ||
321 | * @param String message | ||
322 | */ | ||
323 | this.pass = function(message) { | ||
324 | this.assert(true, message); | ||
325 | }; | ||
326 | |||
327 | /** | ||
328 | * Render tests results, an optionnaly exit phantomjs. | ||
329 | * | ||
330 | * @param Boolean exit | ||
331 | */ | ||
332 | this.renderResults = function(exit, status, save) { | ||
333 | save = isType(save, "string") ? save : this.options.save; | ||
334 | var total = this.testResults.passed + this.testResults.failed, statusText, style, result; | ||
335 | if (this.testResults.failed > 0) { | ||
336 | statusText = FAIL; | ||
337 | style = 'RED_BAR'; | ||
338 | } else { | ||
339 | statusText = PASS; | ||
340 | style = 'GREEN_BAR'; | ||
341 | } | ||
342 | result = statusText + ' ' + total + ' tests executed, ' + this.testResults.passed + ' passed, ' + this.testResults.failed + ' failed.'; | ||
343 | if (result.length < 80) { | ||
344 | result += new Array(80 - result.length + 1).join(' '); | ||
345 | } | ||
346 | casper.echo(this.colorize(result, style)); | ||
347 | if (save && isType(require, "function")) { | ||
348 | try { | ||
349 | require('fs').write(save, exporter.getXML(), 'w'); | ||
350 | casper.echo('result log stored in ' + save, 'INFO'); | ||
351 | } catch (e) { | ||
352 | casper.echo('unable to write results to ' + save + '; ' + e, 'ERROR'); | ||
353 | } | ||
354 | } | ||
355 | if (exit === true) { | ||
356 | casper.exit(status || 0); | ||
357 | } | ||
358 | }; | ||
359 | }; | ||
360 | })(phantom); | ||
... | \ No newline at end of file | ... | \ No newline at end of file |
lib/utils.js
0 → 100644
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 | |||
29 | /** | ||
30 | * Provides a better typeof operator equivalent, able to retrieve the array | ||
31 | * type. | ||
32 | * | ||
33 | * @param mixed input | ||
34 | * @return String | ||
35 | * @see http://javascriptweblog.wordpress.com/2011/08/08/fixing-the-javascript-typeof-operator/ | ||
36 | */ | ||
37 | function betterTypeOf(input) { | ||
38 | try { | ||
39 | return Object.prototype.toString.call(input).match(/^\[object\s(.*)\]$/)[1].toLowerCase(); | ||
40 | } catch (e) { | ||
41 | return typeof input; | ||
42 | } | ||
43 | } | ||
44 | |||
45 | /** | ||
46 | * Creates a new WebPage instance for Casper use. | ||
47 | * | ||
48 | * @param Casper casper A Casper instance | ||
49 | * @return WebPage | ||
50 | */ | ||
51 | function createPage(casper) { | ||
52 | var page; | ||
53 | if (phantom.version.major <= 1 && phantom.version.minor < 3 && isType(require, "function")) { | ||
54 | page = new WebPage(); | ||
55 | } else { | ||
56 | page = require('webpage').create(); | ||
57 | } | ||
58 | page.onAlert = function(message) { | ||
59 | casper.log('[alert] ' + message, "info", "remote"); | ||
60 | if (isType(casper.options.onAlert, "function")) { | ||
61 | casper.options.onAlert.call(casper, casper, message); | ||
62 | } | ||
63 | }; | ||
64 | page.onConsoleMessage = function(msg) { | ||
65 | var level = "info", test = /^\[casper:(\w+)\]\s?(.*)/.exec(msg); | ||
66 | if (test && test.length === 3) { | ||
67 | level = test[1]; | ||
68 | msg = test[2]; | ||
69 | } | ||
70 | casper.log(msg, level, "remote"); | ||
71 | }; | ||
72 | page.onLoadStarted = function() { | ||
73 | casper.loadInProgress = true; | ||
74 | }; | ||
75 | page.onLoadFinished = function(status) { | ||
76 | if (status !== "success") { | ||
77 | var message = 'Loading resource failed with status=' + status; | ||
78 | if (casper.currentHTTPStatus) { | ||
79 | message += ' (HTTP ' + casper.currentHTTPStatus + ')'; | ||
80 | } | ||
81 | message += ': ' + casper.requestUrl; | ||
82 | casper.log(message, "warning"); | ||
83 | if (isType(casper.options.onLoadError, "function")) { | ||
84 | casper.options.onLoadError.call(casper, casper, casper.requestUrl, status); | ||
85 | } | ||
86 | } | ||
87 | if (casper.options.clientScripts) { | ||
88 | if (betterTypeOf(casper.options.clientScripts) !== "array") { | ||
89 | casper.log("The clientScripts option must be an array", "error"); | ||
90 | } else { | ||
91 | for (var i = 0; i < casper.options.clientScripts.length; i++) { | ||
92 | var script = casper.options.clientScripts[i]; | ||
93 | if (casper.page.injectJs(script)) { | ||
94 | casper.log('Automatically injected ' + script + ' client side', "debug"); | ||
95 | } else { | ||
96 | casper.log('Failed injecting ' + script + ' client side', "warning"); | ||
97 | } | ||
98 | } | ||
99 | } | ||
100 | } | ||
101 | // Client-side utils injection | ||
102 | var injected = page.evaluate(replaceFunctionPlaceholders(function() { | ||
103 | eval("var ClientUtils = " + decodeURIComponent("%utils%")); | ||
104 | __utils__ = new ClientUtils(); | ||
105 | return __utils__ instanceof ClientUtils; | ||
106 | }, { | ||
107 | utils: encodeURIComponent(phantom.Casper.ClientUtils.toString()) | ||
108 | })); | ||
109 | if (!injected) { | ||
110 | casper.log("Failed to inject Casper client-side utilities!", "warning"); | ||
111 | } else { | ||
112 | casper.log("Successfully injected Casper client-side utilities", "debug"); | ||
113 | } | ||
114 | // history | ||
115 | casper.history.push(casper.getCurrentUrl()); | ||
116 | casper.loadInProgress = false; | ||
117 | }; | ||
118 | page.onResourceReceived = function(resource) { | ||
119 | if (isType(casper.options.onResourceReceived, "function")) { | ||
120 | casper.options.onResourceReceived.call(casper, casper, resource); | ||
121 | } | ||
122 | if (resource.url === casper.requestUrl && resource.stage === "start") { | ||
123 | casper.currentHTTPStatus = resource.status; | ||
124 | if (isType(casper.options.httpStatusHandlers, "object") && resource.status in casper.options.httpStatusHandlers) { | ||
125 | casper.options.httpStatusHandlers[resource.status](casper, resource); | ||
126 | } | ||
127 | casper.currentUrl = resource.url; | ||
128 | } | ||
129 | }; | ||
130 | page.onResourceRequested = function(request) { | ||
131 | if (isType(casper.options.onResourceRequested, "function")) { | ||
132 | casper.options.onResourceRequested.call(casper, casper, request); | ||
133 | } | ||
134 | }; | ||
135 | return page; | ||
136 | } | ||
137 | |||
138 | /** | ||
139 | * Shorthands for checking if a value is of the given type. Can check for | ||
140 | * arrays. | ||
141 | * | ||
142 | * @param mixed what The value to check | ||
143 | * @param String typeName The type name ("string", "number", "function", etc.) | ||
144 | * @return Boolean | ||
145 | */ | ||
146 | function isType(what, typeName) { | ||
147 | return betterTypeOf(what) === typeName; | ||
148 | } | ||
149 | |||
150 | /** | ||
151 | * Checks if the provided var is a WebPage instance | ||
152 | * | ||
153 | * @param mixed what | ||
154 | * @return Boolean | ||
155 | */ | ||
156 | function isWebPage(what) { | ||
157 | if (!what || !isType(what, "object")) { | ||
158 | return false; | ||
159 | } | ||
160 | if (phantom.version.major <= 1 && phantom.version.minor < 3 && isType(require, "function")) { | ||
161 | return what instanceof WebPage; | ||
162 | } else { | ||
163 | return what.toString().indexOf('WebPage(') === 0; | ||
164 | } | ||
165 | } | ||
166 | |||
167 | /** | ||
168 | * Object recursive merging utility. | ||
169 | * | ||
170 | * @param Object obj1 the destination object | ||
171 | * @param Object obj2 the source object | ||
172 | * @return Object | ||
173 | */ | ||
174 | function mergeObjects(obj1, obj2) { | ||
175 | for (var p in obj2) { | ||
176 | try { | ||
177 | if (obj2[p].constructor == Object) { | ||
178 | obj1[p] = mergeObjects(obj1[p], obj2[p]); | ||
179 | } else { | ||
180 | obj1[p] = obj2[p]; | ||
181 | } | ||
182 | } catch(e) { | ||
183 | obj1[p] = obj2[p]; | ||
184 | } | ||
185 | } | ||
186 | return obj1; | ||
187 | } | ||
188 | |||
189 | /** | ||
190 | * Replaces a function string contents with placeholders provided by an | ||
191 | * Object. | ||
192 | * | ||
193 | * @param Function fn The function | ||
194 | * @param Object replacements Object containing placeholder replacements | ||
195 | * @return String A function string representation | ||
196 | */ | ||
197 | function replaceFunctionPlaceholders(fn, replacements) { | ||
198 | if (replacements && isType(replacements, "object")) { | ||
199 | fn = fn.toString(); | ||
200 | for (var placeholder in replacements) { | ||
201 | var match = '%' + placeholder + '%'; | ||
202 | do { | ||
203 | fn = fn.replace(match, replacements[placeholder]); | ||
204 | } while(fn.indexOf(match) !== -1); | ||
205 | } | ||
206 | } | ||
207 | return fn; | ||
208 | } |
lib/xunit.js
0 → 100644
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 | * JUnit XML (xUnit) exporter for test results. | ||
31 | * | ||
32 | */ | ||
33 | phantom.Casper.XUnitExporter = function() { | ||
34 | var node = function(name, attributes) { | ||
35 | var node = document.createElement(name); | ||
36 | for (var attrName in attributes) { | ||
37 | var value = attributes[attrName]; | ||
38 | if (attributes.hasOwnProperty(attrName) && isType(attrName, "string")) { | ||
39 | node.setAttribute(attrName, value); | ||
40 | } | ||
41 | } | ||
42 | return node; | ||
43 | }; | ||
44 | |||
45 | var xml = node('testsuite'); | ||
46 | xml.toString = function() { | ||
47 | return this.outerHTML; // ouch | ||
48 | }; | ||
49 | |||
50 | /** | ||
51 | * Adds a successful test result | ||
52 | * | ||
53 | * @param String classname | ||
54 | * @param String name | ||
55 | */ | ||
56 | this.addSuccess = function(classname, name) { | ||
57 | xml.appendChild(node('testcase', { | ||
58 | classname: classname, | ||
59 | name: name | ||
60 | })); | ||
61 | }; | ||
62 | |||
63 | /** | ||
64 | * Adds a failed test result | ||
65 | * | ||
66 | * @param String classname | ||
67 | * @param String name | ||
68 | * @param String message | ||
69 | * @param String type | ||
70 | */ | ||
71 | this.addFailure = function(classname, name, message, type) { | ||
72 | var fnode = node('testcase', { | ||
73 | classname: classname, | ||
74 | name: name | ||
75 | }); | ||
76 | var failure = node('failure', { | ||
77 | type: type || "unknown" | ||
78 | }); | ||
79 | failure.appendChild(document.createTextNode(message || "no message left")); | ||
80 | fnode.appendChild(failure); | ||
81 | xml.appendChild(fnode); | ||
82 | }; | ||
83 | |||
84 | /** | ||
85 | * Retrieves generated XML object - actually an HTMLElement. | ||
86 | * | ||
87 | * @return HTMLElement | ||
88 | */ | ||
89 | this.getXML = function() { | ||
90 | return xml; | ||
91 | }; | ||
92 | }; | ||
93 | })(phantom); | ||
... | \ No newline at end of file | ... | \ No newline at end of file |
-
Please register or sign in to post a comment