class Poltergeist.WebPage @CALLBACKS = ['onConsoleMessage','onError', 'onLoadFinished', 'onInitialized', 'onLoadStarted', 'onResourceRequested', 'onResourceReceived', 'onResourceError', 'onNavigationRequested', 'onUrlChanged', 'onPageCreated', 'onClosing'] @DELEGATES = ['open', 'sendEvent', 'uploadFile', 'release', 'render', 'renderBase64', 'goBack', 'goForward'] @COMMANDS = ['currentUrl', 'find', 'nodeCall', 'documentSize', 'beforeUpload', 'afterUpload', 'clearLocalStorage'] @EXTENSIONS = [] constructor: (@_native) -> @_native or= require('webpage').create() @id = 0 @source = null @closed = false @state = 'default' @urlWhitelist = [] @urlBlacklist = [] @frames = [] @errors = [] @_networkTraffic = {} @_tempHeaders = {} @_blockedUrls = [] @_requestedResources = {} for callback in WebPage.CALLBACKS this.bindCallback(callback) for command in @COMMANDS do (command) => this.prototype[command] = (args...) -> this.runCommand(command, args) for delegate in @DELEGATES do (delegate) => this.prototype[delegate] = -> @_native[delegate].apply(@_native, arguments) onInitializedNative: -> @id += 1 @source = null @injectAgent() this.removeTempHeaders() this.setScrollPosition(left: 0, top: 0) onClosingNative: -> @handle = null @closed = true onConsoleMessageNative: (message) -> if message == '__DOMContentLoaded' @source = @_native.content false else console.log(message) onLoadStartedNative: -> @state = 'loading' @requestId = @lastRequestId @_requestedResources = {} onLoadFinishedNative: (@status) -> @state = 'default' @source or= @_native.content onErrorNative: (message, stack) -> stackString = message stack.forEach (frame) -> stackString += "\n" stackString += " at #{frame.file}:#{frame.line}" stackString += " in #{frame.function}" if frame.function && frame.function != '' @errors.push(message: message, stack: stackString) return true onResourceRequestedNative: (request, net) -> useWhitelist = @urlWhitelist.length > 0 whitelisted = @urlWhitelist.some (whitelisted_regex) -> whitelisted_regex.test request.url blacklisted = @urlBlacklist.some (blacklisted_regex) -> blacklisted_regex.test request.url abort = false if useWhitelist && !whitelisted abort = true if blacklisted abort = true if abort @_blockedUrls.push request.url unless request.url in @_blockedUrls net.abort() else @lastRequestId = request.id if @normalizeURL(request.url) == @redirectURL @redirectURL = null @requestId = request.id @_networkTraffic[request.id] = { request: request, responseParts: [] error: null } @_requestedResources[request.id] = request.url return true onResourceReceivedNative: (response) -> @_networkTraffic[response.id]?.responseParts.push(response) if response.stage == 'end' delete @_requestedResources[response.id] if @requestId == response.id if response.redirectURL @redirectURL = @normalizeURL(response.redirectURL) else @statusCode = response.status @_responseHeaders = response.headers return true onResourceErrorNative: (errorResponse) -> @_networkTraffic[errorResponse.id]?.error = errorResponse delete @_requestedResources[errorResponse.id] return true injectAgent: -> if this.native().evaluate(-> typeof __poltergeist) == "undefined" this.native().injectJs "#{phantom.libraryPath}/agent.js" for extension in WebPage.EXTENSIONS this.native().injectJs extension return true return false injectExtension: (file) -> WebPage.EXTENSIONS.push file this.native().injectJs file native: -> if @closed throw new Poltergeist.NoSuchWindowError else @_native windowName: -> this.native().windowName keyCode: (name) -> this.native().event.key[name] keyModifierCode: (names) -> modifiers = this.native().event.modifier names = names.split(',').map ((name) -> modifiers[name]) names[0] | names[1] # return codes for 1 or 2 modifiers keyModifierKeys: (names) -> names.split(',').map (name) => this.keyCode(name.charAt(0).toUpperCase() + name.substring(1)) _waitState_until: (state, callback, timeout, timeout_callback) -> if (@state == state) callback.call(this) else if new Date().getTime() > timeout timeout_callback.call(this) else setTimeout (=> @_waitState_until(state, callback, timeout, timeout_callback)), 100 waitState: (state, callback, max_wait=0, timeout_callback) -> # callback and timeout_callback will be called with this == the current page if @state == state callback.call(this) else if max_wait != 0 timeout = new Date().getTime() + (max_wait*1000) setTimeout (=> @_waitState_until(state, callback, timeout, timeout_callback)), 100 else setTimeout (=> @waitState(state, callback)), 100 setHttpAuth: (user, password) -> this.native().settings.userName = user this.native().settings.password = password return true networkTraffic: -> @_networkTraffic clearNetworkTraffic: -> @_networkTraffic = {} return true blockedUrls: -> @_blockedUrls clearBlockedUrls: -> @_blockedUrls = [] return true openResourceRequests: -> url for own id, url of @_requestedResources content: -> this.native().frameContent title: -> this.native().frameTitle frameUrl: (frameNameOrId) -> query = (frameNameOrId) -> document.querySelector("iframe[name='#{frameNameOrId}'], iframe[id='#{frameNameOrId}']")?.src this.evaluate(query, frameNameOrId) clearErrors: -> @errors = [] return true responseHeaders: -> headers = {} @_responseHeaders.forEach (item) -> headers[item.name] = item.value headers cookies: -> this.native().cookies deleteCookie: (name) -> this.native().deleteCookie(name) viewportSize: -> this.native().viewportSize setViewportSize: (size) -> this.native().viewportSize = size setZoomFactor: (zoom_factor) -> this.native().zoomFactor = zoom_factor setPaperSize: (size) -> this.native().paperSize = size scrollPosition: -> this.native().scrollPosition setScrollPosition: (pos) -> this.native().scrollPosition = pos clipRect: -> this.native().clipRect setClipRect: (rect) -> this.native().clipRect = rect elementBounds: (selector) -> this.native().evaluate( (selector) -> document.querySelector(selector).getBoundingClientRect() , selector ) setUserAgent: (userAgent) -> this.native().settings.userAgent = userAgent getCustomHeaders: -> this.native().customHeaders setCustomHeaders: (headers) -> this.native().customHeaders = headers addTempHeader: (header) -> for name, value of header @_tempHeaders[name] = value @_tempHeaders removeTempHeaders: -> allHeaders = this.getCustomHeaders() for name, value of @_tempHeaders delete allHeaders[name] this.setCustomHeaders(allHeaders) pushFrame: (name) -> if this.native().switchToFrame(name) @frames.push(name) return true else frame_no = this.native().evaluate( (frame_name) -> frames = document.querySelectorAll("iframe, frame") (idx for f, idx in frames when f?['name'] == frame_name or f?['id'] == frame_name)[0] , name) if frame_no? and this.native().switchToFrame(frame_no) @frames.push(name) return true else return false popFrame: -> @frames.pop() this.native().switchToParentFrame() dimensions: -> scroll = this.scrollPosition() viewport = this.viewportSize() top: scroll.top, bottom: scroll.top + viewport.height, left: scroll.left, right: scroll.left + viewport.width, viewport: viewport document: this.documentSize() # A work around for http://code.google.com/p/phantomjs/issues/detail?id=277 validatedDimensions: -> dimensions = this.dimensions() document = dimensions.document if dimensions.right > document.width dimensions.left = Math.max(0, dimensions.left - (dimensions.right - document.width)) dimensions.right = document.width if dimensions.bottom > document.height dimensions.top = Math.max(0, dimensions.top - (dimensions.bottom - document.height)) dimensions.bottom = document.height this.setScrollPosition(left: dimensions.left, top: dimensions.top) dimensions get: (id) -> new Poltergeist.Node(this, id) # Before each mouse event we make sure that the mouse is moved to where the # event will take place. This deals with e.g. :hover changes. mouseEvent: (name, x, y, button = 'left') -> this.sendEvent('mousemove', x, y) this.sendEvent(name, x, y, button) evaluate: (fn, args...) -> this.injectAgent() JSON.parse this.sanitize(this.native().evaluate("function() { return PoltergeistAgent.stringify(#{this.stringifyCall(fn, args)}) }")) sanitize: (potential_string) -> if typeof(potential_string) == "string" # JSON doesn't like \r or \n in strings unless escaped potential_string.replace("\n","\\n").replace("\r","\\r") else potential_string execute: (fn, args...) -> this.native().evaluate("function() { #{this.stringifyCall(fn, args)} }") stringifyCall: (fn, args) -> if args.length == 0 "(#{fn.toString()})()" else # The JSON.stringify happens twice because the second time we are essentially # escaping the string. "(#{fn.toString()}).apply(this, PoltergeistAgent.JSON.parse(#{JSON.stringify(JSON.stringify(args))}))" # For some reason phantomjs seems to have trouble with doing 'fat arrow' binding here, # hence the 'that' closure. bindCallback: (name) -> that = this this.native()[name] = -> if that[name + 'Native']? # For internal callbacks result = that[name + 'Native'].apply(that, arguments) if result != false && that[name]? # For externally set callbacks that[name].apply(that, arguments) return true # Any error raised here or inside the evaluate will get reported to # phantom.onError. If result is null, that means there was an error # inside the agent. runCommand: (name, args) -> result = this.evaluate( (name, args) -> __poltergeist.externalCall(name, args), name, args ) if result != null if result.error? switch result.error.message when 'PoltergeistAgent.ObsoleteNode' throw new Poltergeist.ObsoleteNode when 'PoltergeistAgent.InvalidSelector' [method, selector] = args throw new Poltergeist.InvalidSelector(method, selector) else throw new Poltergeist.BrowserError(result.error.message, result.error.stack) else result.value canGoBack: -> this.native().canGoBack canGoForward: -> this.native().canGoForward normalizeURL: (url) -> parser = document.createElement('a') parser.href = url return parser.href clearMemoryCache: -> clearMemoryCache = this.native().clearMemoryCache if typeof clearMemoryCache == "function" clearMemoryCache() else throw new Poltergeist.UnsupportedFeature("clearMemoryCache is supported since PhantomJS 2.0.0")