1
0
Fork 0
mirror of https://github.com/teampoltergeist/poltergeist.git synced 2022-11-09 12:05:00 -05:00
teampoltergeist--poltergeist/lib/capybara/poltergeist/client/browser.coffee
2018-02-02 11:22:27 -05:00

592 lines
19 KiB
CoffeeScript

class Poltergeist.Browser
constructor: (width, height) ->
@width = width || 1024
@height = height || 768
@pages = []
@js_errors = true
@_debug = false
@_counter = 0
@processed_modal_messages = []
@confirm_processes = []
@prompt_responses = []
this.resetPage()
resetPage: ->
[@_counter, @pages] = [0, []]
if @page?
unless @page.closed
@page.clearLocalStorage() if @page.currentUrl() != 'about:blank'
@page.close()
phantom.clearCookies()
@page = @currentPage = new Poltergeist.WebPage
@page.setViewportSize(width: @width, height: @height)
@page.handle = "#{@_counter++}"
@pages.push(@page)
@processed_modal_messages = []
@confirm_processes = []
@prompt_responses = []
@setupPageHandlers(@page)
return
setupPageHandlers: (page) ->
page.native().onAlert = (msg) =>
@setModalMessage msg
return
page.native().onConfirm = (msg) =>
process = @confirm_processes.pop()
process = true if process == undefined
@setModalMessage msg
return process
page.native().onPrompt = (msg, defaultVal) =>
response = @prompt_responses.pop()
response = defaultVal if (response == undefined || response == false)
@setModalMessage msg
return response
page.onPageCreated = (newPage) =>
_page = new Poltergeist.WebPage(newPage)
_page.handle = "#{@_counter++}"
_page.urlBlacklist = page.urlBlacklist
_page.urlWhitelist = page.urlWhitelist
_page.setViewportSize(page.viewportSize())
_page.setUserAgent(page.getUserAgent())
_page.setCustomHeaders(page.getCustomHeaders())
@setupPageHandlers(_page)
@pages.push(_page)
return
getPageByHandle: (handle) ->
@pages.filter((p) -> !p.closed && p.handle == handle)[0]
runCommand: (command) ->
@current_command = command
@currentPage.state = 'default'
this[command.name].apply(this, command.args)
debug: (message) ->
console.log "poltergeist [#{new Date().getTime()}] #{message}" if @_debug
setModalMessage: (msg) ->
@processed_modal_messages.push(msg)
return
add_extension: (extension) ->
if @currentPage.injectExtension extension
@current_command.sendResponse 'success'
else
@current_command.sendError(new Poltergeist.BrowserError("Unable to load extension: #{extension}"))
node: (page_id, id) ->
if @currentPage.id == page_id
@currentPage.get(id)
else
throw new Poltergeist.ObsoleteNode
visit: (url, max_wait=0) ->
@currentPage.state = 'loading'
#reset modal processing state when changing page
@processed_modal_messages = []
@confirm_processes = []
@prompt_responses = []
# Prevent firing `page.onInitialized` event twice. Calling currentUrl
# method before page is actually opened fires this event for the first time.
# The second time will be in the right place after `page.open`
prevUrl = if @currentPage.source is null then 'about:blank' else @currentPage.currentUrl()
@currentPage.open(url)
if /#/.test(url) && prevUrl.split('#')[0] == url.split('#')[0]
# Hash change occurred, so there will be no onLoadFinished
@currentPage.state = 'default'
@current_command.sendResponse(status: 'success')
else
command = @current_command
loading_page = @currentPage
@currentPage.waitState 'default', ->
if @statusCode == null && @status == 'fail'
command.sendError(new Poltergeist.StatusFailError(url))
else
command.sendResponse(status: @status)
, max_wait, ->
resources = @openResourceRequests()
msg = if resources.length
"Timed out with the following resources still waiting #{resources.join(',')}"
else
"Timed out with no open resource requests"
command.sendError(new Poltergeist.StatusFailError(url,msg))
return
current_url: ->
@current_command.sendResponse @currentPage.currentUrl()
status_code: ->
@current_command.sendResponse @currentPage.statusCode
body: ->
@current_command.sendResponse @currentPage.content()
source: ->
@current_command.sendResponse @currentPage.source
title: ->
@current_command.sendResponse @currentPage.title()
find: (method, selector) ->
@current_command.sendResponse(page_id: @currentPage.id, ids: @currentPage.find(method, selector))
find_within: (page_id, id, method, selector) ->
@current_command.sendResponse this.node(page_id, id).find(method, selector)
all_text: (page_id, id) ->
@current_command.sendResponse this.node(page_id, id).allText()
visible_text: (page_id, id) ->
@current_command.sendResponse this.node(page_id, id).visibleText()
delete_text: (page_id, id) ->
@current_command.sendResponse this.node(page_id, id).deleteText()
property: (page_id, id, name) ->
@current_command.sendResponse this.node(page_id, id).getProperty(name)
attribute: (page_id, id, name) ->
@current_command.sendResponse this.node(page_id, id).getAttribute(name)
attributes: (page_id, id, name) ->
@current_command.sendResponse this.node(page_id, id).getAttributes()
parents: (page_id, id) ->
@current_command.sendResponse this.node(page_id, id).parentIds()
value: (page_id, id) ->
@current_command.sendResponse this.node(page_id, id).value()
set: (page_id, id, value) ->
this.node(page_id, id).set(value)
@current_command.sendResponse(true)
# PhantomJS only allows us to reference the element by CSS selector, not XPath,
# so we have to add an attribute to the element to identify it, then remove it
# afterwards.
select_file: (page_id, id, value) ->
node = this.node(page_id, id)
@currentPage.beforeUpload(node.id)
@currentPage.uploadFile('[_poltergeist_selected]', value)
@currentPage.afterUpload(node.id)
if phantom.version.major == 2 && phantom.version.minor == 0
# In phantomjs 2.0.x - uploadFile only fully works if executed within a user action
# It does however setup the filenames to be uploaded, so if we then click on the
# file input element the filenames will get set
@click(page_id, id)
else
@current_command.sendResponse(true)
select: (page_id, id, value) ->
@current_command.sendResponse this.node(page_id, id).select(value)
tag_name: (page_id, id) ->
@current_command.sendResponse this.node(page_id, id).tagName()
visible: (page_id, id) ->
@current_command.sendResponse this.node(page_id, id).isVisible()
disabled: (page_id, id) ->
@current_command.sendResponse this.node(page_id, id).isDisabled()
path: (page_id, id) ->
@current_command.sendResponse this.node(page_id, id).path()
evaluate: (script, args...) ->
for arg in args when @_isElementArgument(arg)
throw new Poltergeist.ObsoleteNode if arg["ELEMENT"]["page_id"] != @currentPage.id
@current_command.sendResponse @currentPage.evaluate("function() { return #{script} }", args...)
evaluate_async: (script, max_wait, args...) ->
for arg in args when @_isElementArgument(arg)
throw new Poltergeist.ObsoleteNode if arg["ELEMENT"]["page_id"] != @currentPage.id
command = @current_command
cb = (result)=>
command.sendResponse(result)
@currentPage.evaluate_async("function() { #{script} }", cb, args...)
setTimeout(=>
command.sendError(new Poltergeist.ScriptTimeoutError)
, max_wait*1000)
execute: (script, args...) ->
for arg in args when @_isElementArgument(arg)
throw new Poltergeist.ObsoleteNode if arg["ELEMENT"]["page_id"] != @currentPage.id
@currentPage.execute("function() { #{script} }", args...)
@current_command.sendResponse(true)
frameUrl: (frame_name) ->
@currentPage.frameUrl(frame_name)
pushFrame: (command, name, timeout) ->
if Array.isArray(name)
frame = this.node(name...)
name = frame.getAttribute('name') || frame.getAttribute('id')
unless name
frame.setAttribute('name', "_random_name_#{new Date().getTime()}")
name = frame.getAttribute('name')
frame_url = @frameUrl(name)
if frame_url in @currentPage.blockedUrls()
command.sendResponse(true)
else if @currentPage.pushFrame(name)
if frame_url && (frame_url != 'about:blank') && (@currentPage.currentUrl() == 'about:blank')
@currentPage.state = 'awaiting_frame_load'
@currentPage.waitState 'default', ->
command.sendResponse(true)
else
command.sendResponse(true)
else
if new Date().getTime() < timeout
setTimeout((=> @pushFrame(command, name, timeout)), 50)
else
command.sendError(new Poltergeist.FrameNotFound(name))
push_frame: (name, timeout = (new Date().getTime()) + 2000) ->
@pushFrame(@current_command, name, timeout)
pop_frame: (pop_all = false)->
@current_command.sendResponse(@currentPage.popFrame(pop_all))
window_handles: ->
handles = @pages.filter((p) -> !p.closed).map((p) -> p.handle)
@current_command.sendResponse(handles)
window_handle: (name = null) ->
handle = if name
page = @pages.filter((p) -> !p.closed && p.windowName() == name)[0]
if page then page.handle else null
else
@currentPage.handle
@current_command.sendResponse(handle)
switch_to_window: (handle) ->
command = @current_command
new_page = @getPageByHandle(handle)
if new_page
if new_page != @currentPage
new_page.waitState 'default', =>
@currentPage = new_page
command.sendResponse(true)
else
command.sendResponse(true)
else
throw new Poltergeist.NoSuchWindowError
open_new_window: ->
this.execute 'window.open()'
@current_command.sendResponse(true)
close_window: (handle) ->
page = @getPageByHandle(handle)
if page
page.close()
@current_command.sendResponse(true)
else
@current_command.sendResponse(false)
mouse_event: (page_id, id, name, keys=[], offset={}) ->
# Get the node before changing state, in case there is an exception
node = this.node(page_id, id)
# If the event triggers onNavigationRequested, we will transition to the 'loading'
# state and wait for onLoadFinished before sending a response.
@currentPage.state = 'mouse_event'
last_mouse_event = node.mouseEvent(name, keys, offset)
event_page = @currentPage
command = @current_command
setTimeout ->
# If the state is still the same then navigation event won't happen
if event_page.state == 'mouse_event'
event_page.state = 'default'
command.sendResponse(position: last_mouse_event)
else
event_page.waitState 'default', ->
command.sendResponse(position: last_mouse_event)
, 5
click: (page_id, id, keys, offset) ->
this.mouse_event page_id, id, 'click', keys, offset
right_click: (page_id, id, keys, offset) ->
this.mouse_event page_id, id, 'rightclick', keys, offset
double_click: (page_id, id, keys, offset) ->
this.mouse_event page_id, id, 'doubleclick', keys, offset
hover: (page_id, id) ->
this.mouse_event page_id, id, 'mousemove'
click_coordinates: (x, y) ->
@currentPage.sendEvent('click', x, y)
@current_command.sendResponse(click: { x: x, y: y })
drag: (page_id, id, other_id) ->
this.node(page_id, id).dragTo this.node(page_id, other_id)
@current_command.sendResponse(true)
drag_by: (page_id, id, x, y) ->
this.node(page_id, id).dragBy(x, y)
@current_command.sendResponse(true)
trigger: (page_id, id, event) ->
this.node(page_id, id).trigger(event)
@current_command.sendResponse(event)
equals: (page_id, id, other_id) ->
@current_command.sendResponse this.node(page_id, id).isEqual(this.node(page_id, other_id))
reset: ->
this.resetPage()
@current_command.sendResponse(true)
scroll_to: (left, top) ->
@currentPage.setScrollPosition(left: left, top: top)
@current_command.sendResponse(true)
send_keys: (page_id, id, keys) ->
target = this.node(page_id, id)
# Programmatically generated focus doesn't work for `sendKeys`.
# That's why we need something more realistic like user behavior.
if !target.containsSelection()
target.mouseEvent('click')
@_send_keys_with_modifiers(keys)
@current_command.sendResponse(true)
_send_keys_with_modifiers: (keys, current_modifier_code = 0) ->
for sequence in keys
if sequence.key?
if !(key=@currentPage.keyCode(sequence.key))
@current_command.sendError(new Poltergeist.KeyError("Unknown key: #{sequence.key}"))
return
else if sequence.keys?
key=sequence.keys
else
key=sequence
if sequence.modifier?
modifier_keys = @currentPage.keyModifierKeys(sequence.modifier)
modifier_code = @currentPage.keyModifierCode(sequence.modifier) | current_modifier_code
@currentPage.sendEvent('keydown', modifier_key) for modifier_key in modifier_keys
@_send_keys_with_modifiers([].concat(key), modifier_code)
@currentPage.sendEvent('keyup', modifier_key) for modifier_key in modifier_keys
else
@currentPage.sendEvent('keypress', key, null, null, current_modifier_code)
return true
render_base64: (format, { full = false, selector = null } = {})->
window_scroll_position = @currentPage.native().evaluate("function(){ return [window.pageXOffset, window.pageYOffset] }")
dimensions = this.set_clip_rect(full, selector)
encoded_image = @currentPage.renderBase64(format)
@currentPage.setScrollPosition(left: dimensions.left, top: dimensions.top)
@currentPage.native().evaluate("window.scrollTo", window_scroll_position...)
@current_command.sendResponse(encoded_image)
render: (path, { full = false, selector = null, format = null, quality = null } = {} ) ->
window_scroll_position = @currentPage.native().evaluate("function(){ return [window.pageXOffset, window.pageYOffset] }")
dimensions = this.set_clip_rect(full, selector)
options = {}
options["format"] = format if format?
options["quality"] = quality if quality?
@currentPage.setScrollPosition(left: 0, top: 0)
@currentPage.render(path, options)
@currentPage.setScrollPosition(left: dimensions.left, top: dimensions.top)
@currentPage.native().evaluate("window.scrollTo", window_scroll_position...)
@current_command.sendResponse(true)
set_clip_rect: (full, selector) ->
dimensions = @currentPage.validatedDimensions()
[document, viewport] = [dimensions.document, dimensions.viewport]
rect = if full
left: 0, top: 0, width: document.width, height: document.height
else
if selector?
@currentPage.elementBounds(selector)
else
left: 0, top: 0, width: viewport.width, height: viewport.height
@currentPage.setClipRect(rect)
dimensions
set_paper_size: (size) ->
@currentPage.setPaperSize(size)
@current_command.sendResponse(true)
set_zoom_factor: (zoom_factor) ->
@currentPage.setZoomFactor(zoom_factor)
@current_command.sendResponse(true)
resize: (width, height) ->
@currentPage.setViewportSize(width: width, height: height)
@current_command.sendResponse(true)
network_traffic: (type) ->
@current_command.sendResponse(@currentPage.networkTraffic(type))
clear_network_traffic: ->
@currentPage.clearNetworkTraffic()
@current_command.sendResponse(true)
set_proxy: (ip, port, type, user, password) ->
phantom.setProxy(ip, port, type, user, password)
@current_command.sendResponse(true)
get_headers: ->
@current_command.sendResponse(@currentPage.getCustomHeaders())
set_headers: (headers) ->
# Workaround for https://code.google.com/p/phantomjs/issues/detail?id=745
@currentPage.setUserAgent(headers['User-Agent']) if headers['User-Agent']
@currentPage.setCustomHeaders(headers)
@current_command.sendResponse(true)
add_headers: (headers) ->
allHeaders = @currentPage.getCustomHeaders()
for name, value of headers
allHeaders[name] = value
this.set_headers(allHeaders)
add_header: (header, { permanent = true }) ->
unless permanent == true
@currentPage.addTempHeader(header)
@currentPage.addTempHeaderToRemoveOnRedirect(header) if permanent == "no_redirect"
this.add_headers(header)
response_headers: ->
@current_command.sendResponse(@currentPage.responseHeaders())
cookies: ->
@current_command.sendResponse(@currentPage.cookies())
# We're using phantom.addCookie so that cookies can be set
# before the first page load has taken place.
set_cookie: (cookie) ->
phantom.addCookie(cookie)
@current_command.sendResponse(true)
remove_cookie: (name) ->
@currentPage.deleteCookie(name)
@current_command.sendResponse(true)
clear_cookies: () ->
phantom.clearCookies()
@current_command.sendResponse(true)
cookies_enabled: (flag) ->
phantom.cookiesEnabled = flag
@current_command.sendResponse(true)
set_http_auth: (user, password) ->
@currentPage.setHttpAuth(user, password)
@current_command.sendResponse(true)
set_js_errors: (value) ->
@js_errors = value
@current_command.sendResponse(true)
set_debug: (value) ->
@_debug = value
@current_command.sendResponse(true)
exit: ->
phantom.exit()
noop: ->
# NOOOOOOP!
# This command is purely for testing error handling
browser_error: ->
throw new Error('zomg')
go_back: ->
if @currentPage.canGoBack
@currentPage.state = 'wait_for_loading'
@currentPage.goBack()
@_waitForHistoryChange()
else
@current_command.sendResponse(false)
go_forward: ->
if @currentPage.canGoForward
@currentPage.state = 'wait_for_loading'
@currentPage.goForward()
@_waitForHistoryChange()
else
@current_command.sendResponse(false)
refresh: ->
@currentPage.state = 'wait_for_loading'
@currentPage.reload()
@_waitForHistoryChange()
set_url_whitelist: (wildcards...)->
@currentPage.urlWhitelist = (@_wildcardToRegexp(wc) for wc in wildcards)
@current_command.sendResponse(true)
set_url_blacklist: (wildcards...)->
@currentPage.urlBlacklist = (@_wildcardToRegexp(wc) for wc in wildcards)
@current_command.sendResponse(true)
set_confirm_process: (process) ->
@confirm_processes.push process
@current_command.sendResponse(true)
set_prompt_response: (response) ->
@prompt_responses.push response
@current_command.sendResponse(true)
modal_message: ->
@current_command.sendResponse(@processed_modal_messages.shift())
clear_memory_cache: ->
@currentPage.clearMemoryCache()
@current_command.sendResponse(true)
_waitForHistoryChange: ->
command = @current_command
@currentPage.waitState ['loading','default'], (cur_state) ->
if cur_state == 'loading'
# loading has started, wait for completion
@waitState 'default', ->
command.sendResponse(true)
else
# page has loaded
command.sendResponse(true)
, 0.5, ->
# if haven't moved to loading/default in time assume history API state change
@state = 'default'
command.sendResponse(true)
_wildcardToRegexp: (wildcard)->
wildcard = wildcard.replace(/[\-\[\]\/\{\}\(\)\+\.\\\^\$\|]/g, "\\$&")
wildcard = wildcard.replace(/\*/g, ".*")
wildcard = wildcard.replace(/\?/g, ".")
new RegExp(wildcard, "i")
_isElementArgument: (arg)->
typeof(arg) == "object" and typeof(arg['ELEMENT']) == "object"