1 var __slice = [].slice;
2 var __indexOf = [].indexOf || function (item) {
3 for (var i = 0, l = this.length; i < l; i++) {
4 if (i in this && this[i] === item) return i;
9 Poltergeist.WebPage = (function () {
10 var command, delegate, commandFunctionBind, delegateFunctionBind, i, j, commandsLength, delegatesRefLength, commandsRef, delegatesRef,
13 //Native or not webpage callbacks
14 WebPage.CALLBACKS = ['onAlert', 'onConsoleMessage', 'onLoadFinished', 'onInitialized', 'onLoadStarted', 'onResourceRequested',
15 'onResourceReceived', 'onError', 'onNavigationRequested', 'onUrlChanged', 'onPageCreated', 'onClosing'];
17 // Delegates the execution to the phantomjs page native functions but directly available in the WebPage object
18 WebPage.DELEGATES = ['open', 'sendEvent', 'uploadFile', 'release', 'render', 'renderBase64', 'goBack', 'goForward', 'reload'];
20 //Commands to execute on behalf of the browser but on the current page
21 WebPage.COMMANDS = ['currentUrl', 'find', 'nodeCall', 'documentSize', 'beforeUpload', 'afterUpload', 'clearLocalStorage'];
23 WebPage.EXTENSIONS = [];
25 function WebPage(nativeWebPage) {
26 var callback, i, callBacksLength, callBacksRef;
28 //Lets create the native phantomjs webpage
29 if (nativeWebPage === null || typeof nativeWebPage == "undefined") {
30 this._native = require('webpage').create();
32 this._native = nativeWebPage;
38 this.state = 'default';
39 this.urlBlacklist = [];
42 this._networkTraffic = {};
43 this._tempHeaders = {};
44 this._blockedUrls = [];
46 callBacksRef = WebPage.CALLBACKS;
47 for (i = 0, callBacksLength = callBacksRef.length; i < callBacksLength; i++) {
48 callback = callBacksRef[i];
49 this.bindCallback(callback);
53 //Bind the commands we can run from the browser to the current page
54 commandsRef = WebPage.COMMANDS;
55 commandFunctionBind = function (command) {
56 return WebPage.prototype[command] = function () {
58 args = 1 <= arguments.length ? __slice.call(arguments, 0) : [];
59 return this.runCommand(command, args);
62 for (i = 0, commandsLength = commandsRef.length; i < commandsLength; i++) {
63 command = commandsRef[i];
64 commandFunctionBind(command);
67 //Delegates bind applications
68 delegatesRef = WebPage.DELEGATES;
69 delegateFunctionBind = function (delegate) {
70 return WebPage.prototype[delegate] = function () {
71 return this._native[delegate].apply(this._native, arguments);
74 for (j = 0, delegatesRefLength = delegatesRef.length; j < delegatesRefLength; j++) {
75 delegate = delegatesRef[j];
76 delegateFunctionBind(delegate);
80 * This callback is invoked after the web page is created but before a URL is loaded.
81 * The callback may be used to change global objects.
84 WebPage.prototype.onInitializedNative = function () {
88 this.removeTempHeaders();
89 return this.setScrollPosition({
96 * This callback is invoked when the WebPage object is being closed,
97 * either via page.close in the PhantomJS outer space or via window.close in the page's client-side.
100 WebPage.prototype.onClosingNative = function () {
102 return this.closed = true;
106 * This callback is invoked when there is a JavaScript console message on the web page.
107 * The callback may accept up to three arguments: the string for the message, the line number, and the source identifier.
113 WebPage.prototype.onConsoleMessageNative = function (message, line, sourceId) {
114 if (message === '__DOMContentLoaded') {
115 this.source = this._native.content;
118 console.log(message);
123 * This callback is invoked when the page starts the loading. There is no argument passed to the callback.
126 WebPage.prototype.onLoadStartedNative = function () {
127 this.state = 'loading';
128 return this.requestId = this.lastRequestId;
132 * This callback is invoked when the page finishes the loading.
133 * It may accept a single argument indicating the page's status: 'success' if no network errors occurred, otherwise 'fail'.
137 WebPage.prototype.onLoadFinishedNative = function (status) {
138 this.status = status;
139 this.state = 'default';
141 if (this.source === null || typeof this.source == "undefined") {
142 this.source = this._native.content;
144 this.source = this._native.content;
151 * This callback is invoked when there is a JavaScript execution error.
152 * It is a good way to catch problems when evaluating a script in the web page context.
153 * The arguments passed to the callback are the error message and the stack trace [as an Array].
158 WebPage.prototype.onErrorNative = function (message, stack) {
161 stackString = message;
162 stack.forEach(function (frame) {
164 stackString += " at " + frame.file + ":" + frame.line;
165 if (frame["function"] && frame["function"] !== '') {
166 return stackString += " in " + frame["function"];
170 return this.errors.push({
177 * This callback is invoked when the page requests a resource.
178 * The first argument to the callback is the requestData metadata object.
179 * The second argument is the networkRequest object itself.
181 * @param networkRequest
184 WebPage.prototype.onResourceRequestedNative = function (requestData, networkRequest) {
187 abort = this.urlBlacklist.some(function (blacklistedUrl) {
188 return requestData.url.indexOf(blacklistedUrl) !== -1;
192 if (this._blockedUrls.indexOf(requestData.url) === -1) {
193 this._blockedUrls.push(requestData.url);
195 //TODO: check this, as it raises onResourceError
196 return networkRequest.abort();
199 this.lastRequestId = requestData.id;
200 if (requestData.url === this.redirectURL) {
201 this.redirectURL = null;
202 this.requestId = requestData.id;
205 return this._networkTraffic[requestData.id] = {
206 request: requestData,
212 * This callback is invoked when a resource requested by the page is received.
213 * The only argument to the callback is the response metadata object.
217 WebPage.prototype.onResourceReceivedNative = function (response) {
218 var networkTrafficElement;
220 if ((networkTrafficElement = this._networkTraffic[response.id]) != null) {
221 networkTrafficElement.responseParts.push(response);
224 if (this.requestId === response.id) {
225 if (response.redirectURL) {
226 return this.redirectURL = response.redirectURL;
229 this.statusCode = response.status;
230 return this._responseHeaders = response.headers;
235 * Inject the poltergeist agent into the webpage
238 WebPage.prototype.injectAgent = function () {
239 var extension, isAgentInjected, i, extensionsRefLength, extensionsRef, injectionResults;
241 isAgentInjected = this["native"]().evaluate(function () {
242 return typeof window.__poltergeist;
245 if (isAgentInjected === "undefined") {
246 this["native"]().injectJs("" + phantom.libraryPath + "/agent.js");
247 extensionsRef = WebPage.EXTENSIONS;
248 injectionResults = [];
249 for (i = 0, extensionsRefLength = extensionsRef.length; i < extensionsRefLength; i++) {
250 extension = extensionsRef[i];
251 injectionResults.push(this["native"]().injectJs(extension));
253 return injectionResults;
258 * Injects a Javascript file extension into the
262 WebPage.prototype.injectExtension = function (file) {
263 //TODO: add error control, for example, check if file already in the extensions array, check if the file exists, etc.
264 WebPage.EXTENSIONS.push(file);
265 return this["native"]().injectJs(file);
269 * Returns the native phantomjs webpage object
272 WebPage.prototype["native"] = function () {
274 throw new Poltergeist.NoSuchWindowError;
281 * Returns the current page window name
284 WebPage.prototype.windowName = function () {
285 return this["native"]().windowName;
289 * Returns the keyCode of a given key as set in the phantomjs values
293 WebPage.prototype.keyCode = function (name) {
294 return this["native"]().event.key[name];
298 * Waits for the page to reach a certain state
303 WebPage.prototype.waitState = function (state, callback) {
305 if (this.state === state) {
306 return callback.call();
308 return setTimeout((function () {
309 return self.waitState(state, callback);
315 * Sets the browser header related to basic authentication protocol
320 WebPage.prototype.setHttpAuth = function (user, password) {
321 var allHeaders = this.getCustomHeaders();
323 if (user === false || password === false) {
324 if (allHeaders.hasOwnProperty("Authorization")) {
325 delete allHeaders["Authorization"];
327 this.setCustomHeaders(allHeaders);
331 var userName = user || "";
332 var userPassword = password || "";
334 allHeaders["Authorization"] = "Basic " + btoa(userName + ":" + userPassword);
335 this.setCustomHeaders(allHeaders);
340 * Returns all the network traffic associated to the rendering of this page
343 WebPage.prototype.networkTraffic = function () {
344 return this._networkTraffic;
348 * Clears all the recorded network traffic related to the current page
351 WebPage.prototype.clearNetworkTraffic = function () {
352 return this._networkTraffic = {};
356 * Returns the blocked urls that the page will not load
359 WebPage.prototype.blockedUrls = function () {
360 return this._blockedUrls;
364 * Clean all the urls that should not be loaded
367 WebPage.prototype.clearBlockedUrls = function () {
368 return this._blockedUrls = [];
372 * This property stores the content of the web page's currently active frame
373 * (which may or may not be the main frame), enclosed in an HTML/XML element.
376 WebPage.prototype.content = function () {
377 return this["native"]().frameContent;
381 * Returns the current active frame title
384 WebPage.prototype.title = function () {
385 return this["native"]().frameTitle;
389 * Returns if possible the frame url of the frame given by name
393 WebPage.prototype.frameUrl = function (frameName) {
396 query = function (frameName) {
398 if ((iframeReference = document.querySelector("iframe[name='" + frameName + "']")) != null) {
399 return iframeReference.src;
404 return this.evaluate(query, frameName);
408 * Remove the errors caught on the page
411 WebPage.prototype.clearErrors = function () {
412 return this.errors = [];
416 * Returns the response headers associated to this page
419 WebPage.prototype.responseHeaders = function () {
422 this._responseHeaders.forEach(function (item) {
423 return headers[item.name] = item.value;
429 * Get Cookies visible to the current URL (though, for setting, use of page.addCookie is preferred).
430 * This array will be pre-populated by any existing Cookie data visible to this URL that is stored in the CookieJar, if any.
433 WebPage.prototype.cookies = function () {
434 return this["native"]().cookies;
438 * Delete any Cookies visible to the current URL with a 'name' property matching cookieName.
439 * Returns true if successfully deleted, otherwise false.
443 WebPage.prototype.deleteCookie = function (name) {
444 return this["native"]().deleteCookie(name);
448 * This property gets the size of the viewport for the layout process.
451 WebPage.prototype.viewportSize = function () {
452 return this["native"]().viewportSize;
456 * This property sets the size of the viewport for the layout process.
460 WebPage.prototype.setViewportSize = function (size) {
461 return this["native"]().viewportSize = size;
465 * This property specifies the scaling factor for the page.render and page.renderBase64 functions.
469 WebPage.prototype.setZoomFactor = function (zoomFactor) {
470 return this["native"]().zoomFactor = zoomFactor;
474 * This property defines the size of the web page when rendered as a PDF.
475 * See: http://phantomjs.org/api/webpage/property/paper-size.html
479 WebPage.prototype.setPaperSize = function (size) {
480 return this["native"]().paperSize = size;
484 * This property gets the scroll position of the web page.
487 WebPage.prototype.scrollPosition = function () {
488 return this["native"]().scrollPosition;
492 * This property defines the scroll position of the web page.
496 WebPage.prototype.setScrollPosition = function (pos) {
497 return this["native"]().scrollPosition = pos;
502 * This property defines the rectangular area of the web page to be rasterized when page.render is invoked.
503 * If no clipping rectangle is set, page.render will process the entire web page.
506 WebPage.prototype.clipRect = function () {
507 return this["native"]().clipRect;
511 * This property defines the rectangular area of the web page to be rasterized when page.render is invoked.
512 * If no clipping rectangle is set, page.render will process the entire web page.
516 WebPage.prototype.setClipRect = function (rect) {
517 return this["native"]().clipRect = rect;
521 * Returns the size of an element given by a selector and its position relative to the viewport.
525 WebPage.prototype.elementBounds = function (selector) {
526 return this["native"]().evaluate(function (selector) {
527 return document.querySelector(selector).getBoundingClientRect();
532 * Defines the user agent sent to server when the web page requests resources.
536 WebPage.prototype.setUserAgent = function (userAgent) {
537 return this["native"]().settings.userAgent = userAgent;
541 * Returns the additional HTTP request headers that will be sent to the server for EVERY request.
544 WebPage.prototype.getCustomHeaders = function () {
545 return this["native"]().customHeaders;
549 * Gets the additional HTTP request headers that will be sent to the server for EVERY request.
553 WebPage.prototype.setCustomHeaders = function (headers) {
554 return this["native"]().customHeaders = headers;
558 * Adds a one time only request header, after being used it will be deleted
562 WebPage.prototype.addTempHeader = function (header) {
563 var name, value, tempHeaderResult;
564 tempHeaderResult = [];
565 for (name in header) {
566 if (header.hasOwnProperty(name)) {
567 value = header[name];
568 tempHeaderResult.push(this._tempHeaders[name] = value);
571 return tempHeaderResult;
575 * Remove the temporary headers we have set via addTempHeader
578 WebPage.prototype.removeTempHeaders = function () {
579 var allHeaders, name, value, tempHeadersRef;
580 allHeaders = this.getCustomHeaders();
581 tempHeadersRef = this._tempHeaders;
582 for (name in tempHeadersRef) {
583 if (tempHeadersRef.hasOwnProperty(name)) {
584 value = tempHeadersRef[name];
585 delete allHeaders[name];
589 return this.setCustomHeaders(allHeaders);
593 * If possible switch to the frame given by name
597 WebPage.prototype.pushFrame = function (name) {
598 if (this["native"]().switchToFrame(name)) {
599 this.frames.push(name);
606 * Switch to parent frame, use with caution:
607 * popFrame assumes you are in frame, pop frame not being in a frame
608 * leaves unexpected behaviour
611 WebPage.prototype.popFrame = function () {
612 //TODO: add some error control here, some way to check we are in a frame or not
614 return this["native"]().switchToParentFrame();
618 * Returns the webpage dimensions
619 * @return {{top: *, bottom: *, left: *, right: *, viewport: *, document: {height: number, width: number}}}
621 WebPage.prototype.dimensions = function () {
622 var scroll, viewport;
623 scroll = this.scrollPosition();
624 viewport = this.viewportSize();
627 bottom: scroll.top + viewport.height,
629 right: scroll.left + viewport.width,
631 document: this.documentSize()
636 * Returns webpage dimensions that are valid
637 * @return {{top: *, bottom: *, left: *, right: *, viewport: *, document: {height: number, width: number}}}
639 WebPage.prototype.validatedDimensions = function () {
640 var dimensions, documentDimensions;
642 dimensions = this.dimensions();
643 documentDimensions = dimensions.document;
645 if (dimensions.right > documentDimensions.width) {
646 dimensions.left = Math.max(0, dimensions.left - (dimensions.right - documentDimensions.width));
647 dimensions.right = documentDimensions.width;
650 if (dimensions.bottom > documentDimensions.height) {
651 dimensions.top = Math.max(0, dimensions.top - (dimensions.bottom - documentDimensions.height));
652 dimensions.bottom = documentDimensions.height;
655 this.setScrollPosition({
656 left: dimensions.left,
664 * Returns a Poltergeist.Node given by an id
666 * @return {Poltergeist.Node}
668 WebPage.prototype.get = function (id) {
669 return new Poltergeist.Node(this, id);
673 * Executes a phantomjs mouse event, for more info check: http://phantomjs.org/api/webpage/method/send-event.html
680 WebPage.prototype.mouseEvent = function (name, x, y, button) {
681 if (button == null) {
684 this.sendEvent('mousemove', x, y);
685 return this.sendEvent(name, x, y, button);
689 * Evaluates a javascript and returns the evaluation of such script
692 WebPage.prototype.evaluate = function () {
697 if (2 <= arguments.length) {
698 args = __slice.call(arguments, 1);
702 return JSON.parse(this.sanitize(this["native"]().evaluate("function() { return PoltergeistAgent.stringify(" + (this.stringifyCall(fn, args)) + ") }")));
706 * Does some string sanitation prior parsing
707 * @param potentialString
710 WebPage.prototype.sanitize = function (potentialString) {
711 if (typeof potentialString === "string") {
712 return potentialString.replace("\n", "\\n").replace("\r", "\\r");
715 return potentialString;
719 * Executes a script into the current page scope
723 WebPage.prototype.executeScript = function (script) {
724 return this["native"]().evaluateJavaScript(script);
728 * Executes a script via phantomjs evaluation
731 WebPage.prototype.execute = function () {
737 if (2 <= arguments.length) {
738 args = __slice.call(arguments, 1);
741 return this["native"]().evaluate("function() { " + (this.stringifyCall(fn, args)) + " }");
745 * Helper methods to do script evaluation and execution
750 WebPage.prototype.stringifyCall = function (fn, args) {
751 if (args.length === 0) {
752 return "(" + (fn.toString()) + ")()";
755 return "(" + (fn.toString()) + ").apply(this, JSON.parse(" + (JSON.stringify(JSON.stringify(args))) + "))";
759 * Binds callbacks to their respective Native implementations
763 WebPage.prototype.bindCallback = function (name) {
767 return this["native"]()[name] = function () {
769 if (self[name + 'Native'] != null) {
770 result = self[name + 'Native'].apply(self, arguments);
772 if (result !== false && (self[name] != null)) {
773 return self[name].apply(self, arguments);
779 * Runs a command delegating to the PoltergeistAgent
784 WebPage.prototype.runCommand = function (name, args) {
785 var method, result, selector;
787 result = this.evaluate(function (name, args) {
788 return window.__poltergeist.externalCall(name, args);
791 if (result !== null) {
792 if (result.error != null) {
793 switch (result.error.message) {
794 case 'PoltergeistAgent.ObsoleteNode':
795 throw new Poltergeist.ObsoleteNode;
797 case 'PoltergeistAgent.InvalidSelector':
800 throw new Poltergeist.InvalidSelector(method, selector);
803 throw new Poltergeist.BrowserError(result.error.message, result.error.stack);
812 * Tells if we can go back or not
815 WebPage.prototype.canGoBack = function () {
816 return this["native"]().canGoBack;
820 * Tells if we can go forward or not in the browser history
823 WebPage.prototype.canGoForward = function () {
824 return this["native"]().canGoForward;