Class: Capybara::Apparition::Page

Inherits:
Object
  • Object
show all
Defined in:
lib/capybara/apparition/page.rb

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(browser, session, id, _ignore_https_errors, _screenshot_task_queue, js_errors) ⇒ Page

Returns a new instance of Page.



38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
# File 'lib/capybara/apparition/page.rb', line 38

def initialize(browser, session, id, _ignore_https_errors, _screenshot_task_queue, js_errors)
  @target_id = id
  @browser = browser
  @session = session
  @keyboard = Keyboard.new(self)
  @mouse = Mouse.new(self, @keyboard)
  @modals = []
  @modal_messages = []
  @frames = Capybara::Apparition::FrameManager.new(id)
  @response_headers = {}
  @status_code = 0
  @url_blacklist = []
  @url_whitelist = []
  @credentials = nil
  @auth_attempts = []
  @proxy_credentials = nil
  @proxy_auth_attempts = []
  @perm_headers = {}
  @temp_headers = {}
  @temp_no_redirect_headers = {}
  @viewport_size = nil
  @network_traffic = []
  @open_resource_requests = {}
  @raise_js_errors = js_errors
  @js_error = nil
  @modal_mutex = Mutex.new
  @modal_closed = ConditionVariable.new

  register_event_handlers

  register_js_error_handler # if js_errors

  setup_network_interception if browser.proxy_auth
end

Instance Attribute Details

#keyboardObject (readonly)

Returns the value of attribute keyboard.



11
12
13
# File 'lib/capybara/apparition/page.rb', line 11

def keyboard
  @keyboard
end

Returns the value of attribute modal_messages.



10
11
12
# File 'lib/capybara/apparition/page.rb', line 10

def modal_messages
  @modal_messages
end

#mouseObject (readonly)

Returns the value of attribute mouse.



11
12
13
# File 'lib/capybara/apparition/page.rb', line 11

def mouse
  @mouse
end

#network_trafficObject (readonly)

Returns the value of attribute network_traffic.



14
15
16
# File 'lib/capybara/apparition/page.rb', line 14

def network_traffic
  @network_traffic
end

#perm_headersObject

Returns the value of attribute perm_headers.



13
14
15
# File 'lib/capybara/apparition/page.rb', line 13

def perm_headers
  @perm_headers
end

#response_headersObject (readonly)

Returns the value of attribute response_headers.



228
229
230
# File 'lib/capybara/apparition/page.rb', line 228

def response_headers
  @response_headers
end

#status_codeObject (readonly)

Returns the value of attribute status_code.



230
231
232
# File 'lib/capybara/apparition/page.rb', line 230

def status_code
  @status_code
end

#temp_headersObject

Returns the value of attribute temp_headers.



13
14
15
# File 'lib/capybara/apparition/page.rb', line 13

def temp_headers
  @temp_headers
end

#temp_no_redirect_headersObject

Returns the value of attribute temp_no_redirect_headers.



13
14
15
# File 'lib/capybara/apparition/page.rb', line 13

def temp_no_redirect_headers
  @temp_no_redirect_headers
end

#url_blacklist=(blacklist) ⇒ Object



102
103
104
105
# File 'lib/capybara/apparition/page.rb', line 102

def url_blacklist=(blacklist)
  @url_blacklist = blacklist
  setup_network_blocking
end

#url_whitelist=(whitelist) ⇒ Object



107
108
109
110
# File 'lib/capybara/apparition/page.rb', line 107

def url_whitelist=(whitelist)
  @url_whitelist = whitelist
  setup_network_blocking
end

#viewport_sizeObject (readonly)

Returns the value of attribute viewport_size.



12
13
14
# File 'lib/capybara/apparition/page.rb', line 12

def viewport_size
  @viewport_size
end

Class Method Details

.create(browser, session, id, ignore_https_errors: false, screenshot_task_queue: nil, js_errors: false) ⇒ Object



16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# File 'lib/capybara/apparition/page.rb', line 16

def self.create(browser, session, id, ignore_https_errors: false, screenshot_task_queue: nil, js_errors: false)
  session.command 'Page.enable'

  # Provides a lot of info - but huge overhead
  # session.command 'Page.setLifecycleEventsEnabled', enabled: true

  page = Page.new(browser, session, id, ignore_https_errors, screenshot_task_queue, js_errors)

  session.async_commands 'Network.enable', 'Runtime.enable', 'Security.enable', 'DOM.enable'
  # session.command 'Network.enable'
  # session.command 'Runtime.enable'
  # session.command 'Security.enable'
  # session.command 'Security.setOverrideCertificateErrors', override: true if ignore_https_errors
  session.command 'Security.setIgnoreCertificateErrors', ignore: !!ignore_https_errors
  # session.command 'DOM.enable'
  # session.command 'Log.enable'
  if Capybara.save_path
    session.command 'Page.setDownloadBehavior', behavior: 'allow', downloadPath: Capybara.save_path
  end
  page
end

Instance Method Details

#add_modal(modal_response) ⇒ Object



87
88
89
90
# File 'lib/capybara/apparition/page.rb', line 87

def add_modal(modal_response)
  @last_modal_message = nil
  @modals.push(modal_response)
end

#async_command(name, **params) ⇒ Object



344
345
346
# File 'lib/capybara/apparition/page.rb', line 344

def async_command(name, **params)
  @browser.command_for_session(@session.session_id, name, params).discard_result
end

#clear_network_trafficObject



112
113
114
# File 'lib/capybara/apparition/page.rb', line 112

def clear_network_traffic
  @network_traffic = []
end

#click_at(x, y) ⇒ Object



121
122
123
124
# File 'lib/capybara/apparition/page.rb', line 121

def click_at(x, y)
  wait_for_loaded
  @mouse.click_at(x: x, y: y)
end

#command(name, **params) ⇒ Object



340
341
342
# File 'lib/capybara/apparition/page.rb', line 340

def command(name, **params)
  @browser.command_for_session(@session.session_id, name, params).result
end

#contentObject



245
246
247
248
249
250
251
252
253
254
255
# File 'lib/capybara/apparition/page.rb', line 245

def content
  wait_for_loaded
  _raw_evaluate("(function(){
    let val = '';
    if (document.doctype)
      val = new XMLSerializer().serializeToString(document.doctype);
    if (document.documentElement)
      val += document.documentElement.outerHTML;
    return val;
  })()")
end

#credentials=(creds) ⇒ Object



97
98
99
100
# File 'lib/capybara/apparition/page.rb', line 97

def credentials=(creds)
  @credentials = creds
  setup_network_interception
end

#current_frame_offsetObject



130
131
132
# File 'lib/capybara/apparition/page.rb', line 130

def current_frame_offset
  frame_offset(current_frame)
end

#current_stateObject



126
127
128
# File 'lib/capybara/apparition/page.rb', line 126

def current_state
  main_frame.state
end

#current_urlObject



272
273
274
275
# File 'lib/capybara/apparition/page.rb', line 272

def current_url
  wait_for_loaded
  _raw_evaluate('window.location.href', context_id: main_frame.context_id)
end

#element_from_point(x:, y:) ⇒ Object



277
278
279
280
281
282
283
284
285
286
# File 'lib/capybara/apparition/page.rb', line 277

def element_from_point(x:, y:)
  r_o = _raw_evaluate("document.elementFromPoint(#{x}, #{y})", context_id: main_frame.context_id)
  while r_o && (r_o['description'] =~ /^iframe/)
    frame_node = command('DOM.describeNode', objectId: r_o['objectId'])
    frame = @frames.get(frame_node.dig('node', 'frameId'))
    fo = frame_offset(frame)
    r_o = _raw_evaluate("document.elementFromPoint(#{x - fo[:x]}, #{y - fo[:y]})", context_id: frame.context_id)
  end
  r_o
end

#evaluate(script, *args) ⇒ Object



203
204
205
# File 'lib/capybara/apparition/page.rb', line 203

def evaluate(script, *args)
  eval_wrapped_script(EVALUATE_WITH_ID_JS, script, args)
end

#evaluate_async(script, _wait_time, *args) ⇒ Object



207
208
209
# File 'lib/capybara/apparition/page.rb', line 207

def evaluate_async(script, _wait_time, *args)
  eval_wrapped_script(EVALUATE_ASYNC_JS, script, args)
end

#execute(script, *args) ⇒ Object



198
199
200
201
# File 'lib/capybara/apparition/page.rb', line 198

def execute(script, *args)
  eval_wrapped_script(EXECUTE_JS, script, args)
  nil
end

#extra_headersObject



348
349
350
# File 'lib/capybara/apparition/page.rb', line 348

def extra_headers
  temp_headers.merge(perm_headers).merge(temp_no_redirect_headers)
end

#find(method, selector) ⇒ Object



186
187
188
189
190
191
192
193
194
195
196
# File 'lib/capybara/apparition/page.rb', line 186

def find(method, selector)
  wait_for_loaded
  js_escaped_selector = selector.gsub('\\', '\\\\\\').gsub('"', '\"')
  query = method == :css ? CSS_FIND_JS : XPATH_FIND_JS
  result = _raw_evaluate(query % js_escaped_selector)
  (result || []).map { |r_o| [self, r_o['objectId']] }
rescue ::Capybara::Apparition::BrowserError => e
  raise unless e.name =~ /is not a valid (XPath expression|selector)/

  raise Capybara::Apparition::InvalidSelector, [method, selector]
end

#frame_titleObject



335
336
337
338
# File 'lib/capybara/apparition/page.rb', line 335

def frame_title
  wait_for_loaded
  _raw_evaluate('document.title')
end

#frame_urlObject



288
289
290
291
# File 'lib/capybara/apparition/page.rb', line 288

def frame_url
  wait_for_loaded
  _raw_evaluate('window.location.href')
end

#fullscreenObject



317
318
319
320
# File 'lib/capybara/apparition/page.rb', line 317

def fullscreen
  result = @browser.command('Browser.getWindowForTarget', targetId: @target_id)
  @browser.command('Browser.setWindowBounds', windowId: result['windowId'], bounds: { windowState: 'fullscreen' })
end

#go_backObject



218
219
220
221
# File 'lib/capybara/apparition/page.rb', line 218

def go_back
  wait_for_loaded
  go_history(-1)
end

#go_forwardObject



223
224
225
226
# File 'lib/capybara/apparition/page.rb', line 223

def go_forward
  wait_for_loaded
  go_history(+1)
end

#inherit(page) ⇒ Object



361
362
363
364
365
366
367
368
# File 'lib/capybara/apparition/page.rb', line 361

def inherit(page)
  if page
    self.url_whitelist = page.url_whitelist.dup
    self.url_blacklist = page.url_blacklist.dup
    set_viewport(page.viewport_size) if page.viewport_size
  end
  self
end

#js_errorObject



370
371
372
373
374
# File 'lib/capybara/apparition/page.rb', line 370

def js_error
  res = @js_error
  @js_error = nil
  res
end

#maximizeObject



322
323
324
325
326
327
328
# File 'lib/capybara/apparition/page.rb', line 322

def maximize
  screen_width, screen_height = *evaluate('[window.screen.width, window.screen.height]')
  set_viewport(width: screen_width, height: screen_height)

  result = @browser.command('Browser.getWindowForTarget', targetId: @target_id)
  @browser.command('Browser.setWindowBounds', windowId: result['windowId'], bounds: { windowState: 'maximized' })
end

#pop_frame(top: false) ⇒ Object



182
183
184
# File 'lib/capybara/apparition/page.rb', line 182

def pop_frame(top: false)
  @frames.pop_frame(top: top)
end

#proxy_credentials=(creds) ⇒ Object



92
93
94
95
# File 'lib/capybara/apparition/page.rb', line 92

def proxy_credentials=(creds)
  @proxy_credentials = creds
  setup_network_interception
end

#push_frame(frame_el) ⇒ Object



162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
# File 'lib/capybara/apparition/page.rb', line 162

def push_frame(frame_el)
  node = command('DOM.describeNode', objectId: frame_el.base.id)
  frame_id = node['node']['frameId']

  timer = Capybara::Helpers.timer(expire_in: 10)
  while (frame = @frames.get(frame_id)).nil? || frame.loading?
    # Wait for the frame creation messages to be processed
    if timer.expired?
      puts 'Timed out waiting from frame to be ready'
      raise TimeoutError.new('push_frame')
    end
    sleep 0.1
  end
  return unless frame

  frame.element_id = frame_el.base.id
  @frames.push_frame(frame.id)
  frame
end

#refreshObject



211
212
213
214
215
216
# File 'lib/capybara/apparition/page.rb', line 211

def refresh
  wait_for_loaded
  main_frame.reloading!
  command('Page.reload', ignoreCache: true)
  wait_for_loaded
end

#render(options) ⇒ Object



134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
# File 'lib/capybara/apparition/page.rb', line 134

def render(options)
  wait_for_loaded
  pixel_ratio = evaluate('window.devicePixelRatio')
  scale = (@browser.zoom_factor || 1).to_f / pixel_ratio
  if options[:format].to_s == 'pdf'
    params = {}
    params[:paperWidth] = @browser.paper_size[:width].to_f if @browser.paper_size
    params[:paperHeight] = @browser.paper_size[:height].to_f if @browser.paper_size
    params[:scale] = scale
    command('Page.printToPDF', params)
  else
    clip_options = if options[:selector]
      pos = evaluate("document.querySelector('#{options.delete(:selector)}').getBoundingClientRect().toJSON();")
      %w[x y width height].each_with_object({}) { |key, hash| hash[key] = pos[key] }
    elsif options[:full]
      evaluate <<~JS
        { width: document.documentElement.clientWidth, height: document.documentElement.clientHeight}
      JS
    else
      evaluate <<~JS
        { width: window.innerWidth, height: window.innerHeight }
      JS
    end
    options[:clip] = { x: 0, y: 0, scale: scale }.merge(clip_options)
    command('Page.captureScreenshot', options)
  end['data']
end

#resetObject



77
78
79
80
81
82
83
84
85
# File 'lib/capybara/apparition/page.rb', line 77

def reset
  @modals.clear
  @modal_messages.clear
  @response_headers = {}
  @status_code = 0
  @auth_attempts = []
  @proxy_auth_attempts = []
  @perm_headers = {}
end

#scroll_to(left, top) ⇒ Object



116
117
118
119
# File 'lib/capybara/apparition/page.rb', line 116

def scroll_to(left, top)
  wait_for_loaded
  execute('window.scrollTo(arguments[0], arguments[1])', left, top)
end

#set_viewport(width:, height:, screen: nil) ⇒ Object



293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
# File 'lib/capybara/apparition/page.rb', line 293

def set_viewport(width:, height:, screen: nil)
  wait_for_loaded
  @viewport_size = { width: width, height: height }
  result = @browser.command('Browser.getWindowForTarget', targetId: @target_id)
  begin
    @browser.command('Browser.setWindowBounds',
                     windowId: result['windowId'],
                     bounds: { width: width, height: height })
  rescue WrongWorld # TODO: Fix Error naming here
    @browser.command('Browser.setWindowBounds', windowId: result['windowId'], bounds: { windowState: 'normal' })
    retry
  end

  metrics = {
    mobile: false,
    width: width,
    height: height,
    deviceScaleFactor: 1
  }
  metrics[:screenWidth], metrics[:screenHeight] = *screen if screen

  command('Emulation.setDeviceMetricsOverride', metrics)
end

#titleObject



330
331
332
333
# File 'lib/capybara/apparition/page.rb', line 330

def title
  wait_for_loaded
  _raw_evaluate('document.title', context_id: main_frame.context_id)
end

#update_headers(async: false) ⇒ Object



352
353
354
355
356
357
358
359
# File 'lib/capybara/apparition/page.rb', line 352

def update_headers(async: false)
  method = async ? :async_command : :command
  if (ua = extra_headers.find { |k, _v| k =~ /^User-Agent$/i })
    send(method, 'Network.setUserAgentOverride', userAgent: ua[1])
  end
  send(method, 'Network.setExtraHTTPHeaders', headers: extra_headers)
  setup_network_interception
end

#usable?Boolean

Returns:

  • (Boolean)


73
74
75
# File 'lib/capybara/apparition/page.rb', line 73

def usable?
  !!current_frame&.context_id
end

#visit(url) ⇒ Object



257
258
259
260
261
262
263
264
265
266
267
268
269
270
# File 'lib/capybara/apparition/page.rb', line 257

def visit(url)
  wait_for_loaded
  @status_code = 0
  navigate_opts = { url: url, transitionType: 'reload' }
  navigate_opts[:referrer] = extra_headers['Referer'] if extra_headers['Referer']
  response = command('Page.navigate', navigate_opts)

  raise StatusFailError, 'args' => [url, response['errorText']] if response['errorText']

  main_frame.loading(response['loaderId'])
  wait_for_loaded
rescue TimeoutError
  raise StatusFailError.new('args' => [url])
end

#wait_for_loaded(allow_obsolete: false) ⇒ Object

Raises:



232
233
234
235
236
237
238
239
240
241
242
243
# File 'lib/capybara/apparition/page.rb', line 232

def wait_for_loaded(allow_obsolete: false)
  timer = Capybara::Helpers.timer(expire_in: 10)
  cf = current_frame
  until cf.usable? || (allow_obsolete && cf.obsolete?) || @js_error
    if timer.expired?
      puts 'Timedout waiting for page to be loaded'
      raise TimeoutError.new('wait_for_loaded')
    end
    sleep 0.05
  end
  raise JavascriptError.new(js_error) if @js_error
end