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, target_id, browser_context_id, js_errors: false, url_blacklist: [], url_whitelist: [], extensions: []) ⇒ Page

Returns a new instance of Page.



35
36
37
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
72
73
74
# File 'lib/capybara/apparition/page.rb', line 35

def initialize(browser, session, target_id, browser_context_id,
               js_errors: false, url_blacklist: [], url_whitelist: [], extensions: [])
  @target_id = target_id
  @browser_context_id = browser_context_id
  @browser = browser
  @session = session
  @keyboard = Keyboard.new(self)
  @mouse = Mouse.new(self, @keyboard)
  @modals = []
  @modal_messages = []
  @frames = Capybara::Apparition::FrameManager.new(@target_id)
  @response_headers = {}
  @status_code = 0
  @url_blacklist = url_blacklist || []
  @url_whitelist = 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

  extensions.each do |name|
    add_extension(name)
  end

  setup_network_interception if browser.proxy_auth
end

Instance Attribute Details

#browser_context_idObject (readonly)

Returns the value of attribute browser_context_id.



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

def browser_context_id
  @browser_context_id
end

#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.



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

def network_traffic
  @network_traffic
end

#perm_headersObject

Returns the value of attribute perm_headers.



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

def perm_headers
  @perm_headers
end

#status_codeObject (readonly)

Returns the value of attribute status_code.



241
242
243
# File 'lib/capybara/apparition/page.rb', line 241

def status_code
  @status_code
end

#target_idObject (readonly)

Returns the value of attribute target_id.



16
17
18
# File 'lib/capybara/apparition/page.rb', line 16

def target_id
  @target_id
end

#temp_headersObject

Returns the value of attribute temp_headers.



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

def temp_headers
  @temp_headers
end

#temp_no_redirect_headersObject

Returns the value of attribute temp_no_redirect_headers.



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

def temp_no_redirect_headers
  @temp_no_redirect_headers
end

#url_blacklist=(blacklist) ⇒ Object



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

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

#url_whitelist=(whitelist) ⇒ Object



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

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, browser_context_id, ignore_https_errors: false, **options) ⇒ Object



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

def self.create(browser, session, id, browser_context_id,
                ignore_https_errors: false, **options)
  session.async_command 'Page.enable'

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

  page = Page.new(browser, session, id, browser_context_id, **options)

  session.async_commands 'Network.enable', 'Runtime.enable', 'Security.enable', 'DOM.enable'
  session.async_command 'Security.setIgnoreCertificateErrors', ignore: !!ignore_https_errors
  if Capybara.save_path
    session.async_command 'Page.setDownloadBehavior', behavior: 'allow', downloadPath: Capybara.save_path
  end
  page
end

Instance Method Details

#add_extension(filename) ⇒ Object



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

def add_extension(filename)
  command('Page.addScriptToEvaluateOnNewDocument', source: File.read(filename))
rescue Errno::ENOENT
  raise ::Capybara::Apparition::BrowserError.new('name' => "Unable to load extension: #{filename}", 'args' => nil)
end

#add_modal(modal_response) ⇒ Object



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

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

#async_command(name, **params) ⇒ Object



368
369
370
# File 'lib/capybara/apparition/page.rb', line 368

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

#clear_network_trafficObject



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

def clear_network_traffic
  @network_traffic = []
end

#click_at(x, y) ⇒ Object



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

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

#command(name, **params) ⇒ Object



364
365
366
# File 'lib/capybara/apparition/page.rb', line 364

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

#contentObject



270
271
272
273
274
275
276
277
278
279
280
# File 'lib/capybara/apparition/page.rb', line 270

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



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

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

#current_frame_offsetObject



139
140
141
# File 'lib/capybara/apparition/page.rb', line 139

def current_frame_offset
  frame_offset(current_frame)
end

#current_stateObject



135
136
137
# File 'lib/capybara/apparition/page.rb', line 135

def current_state
  main_frame.state
end

#current_urlObject



296
297
298
299
# File 'lib/capybara/apparition/page.rb', line 296

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

#element_from_point(x:, y:) ⇒ Object



301
302
303
304
305
306
307
308
309
310
# File 'lib/capybara/apparition/page.rb', line 301

def element_from_point(x:, y:)
  r_o = _raw_evaluate("document.elementFromPoint(#{x}, #{y})", context_id: main_frame.context_id)
  while r_o && (/^iframe/.match? r_o['description'])
    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



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

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

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



216
217
218
# File 'lib/capybara/apparition/page.rb', line 216

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

#execute(script, *args) ⇒ Object



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

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

#extra_headersObject



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

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

#find(method, selector) ⇒ Object



195
196
197
198
199
200
201
202
203
204
205
# File 'lib/capybara/apparition/page.rb', line 195

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(format(query, selector: js_escaped_selector))
  (result || []).map { |r_o| [self, r_o['objectId'], tag_name: r_o['description'].split(/[\.#]/, 2)[0]] }
rescue ::Capybara::Apparition::BrowserError => e
  raise unless /is not a valid (XPath expression|selector)/.match? e.name

  raise Capybara::Apparition::InvalidSelector, 'args' => [method, selector]
end

#frame_titleObject



359
360
361
362
# File 'lib/capybara/apparition/page.rb', line 359

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

#frame_urlObject



312
313
314
315
# File 'lib/capybara/apparition/page.rb', line 312

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

#fullscreenObject



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

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

#go_backObject



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

def go_back
  wait_for_loaded
  go_history(-1)
end

#go_forwardObject



232
233
234
235
# File 'lib/capybara/apparition/page.rb', line 232

def go_forward
  wait_for_loaded
  go_history(+1)
end

#inherit(page) ⇒ Object



385
386
387
388
389
390
391
392
# File 'lib/capybara/apparition/page.rb', line 385

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



394
395
396
397
398
# File 'lib/capybara/apparition/page.rb', line 394

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

#maximizeObject



346
347
348
349
350
351
352
# File 'lib/capybara/apparition/page.rb', line 346

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



191
192
193
# File 'lib/capybara/apparition/page.rb', line 191

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

#proxy_credentials=(creds) ⇒ Object



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

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

#push_frame(frame_el) ⇒ Object



172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
# File 'lib/capybara/apparition/page.rb', line 172

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[frame_id]).nil? || frame.loading?
    # Wait for the frame creation messages to be processed
    if timer.expired?
      puts 'Timed out waiting for frame to be ready'
      raise TimeoutError.new('push_frame')
    end
    sleep 0.1
  end

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

#refreshObject



220
221
222
223
224
225
# File 'lib/capybara/apparition/page.rb', line 220

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

#render(options) ⇒ Object



143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
# File 'lib/capybara/apparition/page.rb', line 143

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 = { scale: scale }
    if @browser.paper_size
      params[:paperWidth] = @browser.paper_size[:width].to_f
      params[:paperHeight] = @browser.paper_size[:height].to_f
    end
    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



80
81
82
83
84
85
86
87
88
# File 'lib/capybara/apparition/page.rb', line 80

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

#response_headersObject



237
238
239
# File 'lib/capybara/apparition/page.rb', line 237

def response_headers
  @response_headers[current_frame.id] || {}
end

#scroll_to(left, top) ⇒ Object



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

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



317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
# File 'lib/capybara/apparition/page.rb', line 317

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



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

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

#update_headers(async: false) ⇒ Object



376
377
378
379
380
381
382
383
# File 'lib/capybara/apparition/page.rb', line 376

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

#usable?Boolean

Returns:

  • (Boolean)


76
77
78
# File 'lib/capybara/apparition/page.rb', line 76

def usable?
  !!current_frame&.context_id
end

#visit(url) ⇒ Object



282
283
284
285
286
287
288
289
290
291
292
293
294
# File 'lib/capybara/apparition/page.rb', line 282

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:



243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
# File 'lib/capybara/apparition/page.rb', line 243

def wait_for_loaded(allow_obsolete: false)
  # We can't reliably detect if the page is loaded, so just ensure the context
  # is usable
  timer = Capybara::Helpers.timer(expire_in: 30)
  page_function = '(function(){ return 1 == 1; })()'
  begin
    response = command('Runtime.evaluate',
                       expression: page_function,
                       contextId: current_frame.context_id,
                       returnByValue: false,
                       awaitPromise: true)
    process_response(response)
    current_frame.loaded!
  rescue # rubocop:disable Style/RescueStandardError
    return if allow_obsolete && current_frame.obsolete?

    unless timer.expired?
      sleep 0.05
      retry
    end
    puts 'Timedout waiting for page to be loaded' if ENV['DEBUG']
    raise TimeoutError.new('wait_for_loaded')
  end

  raise JavascriptError.new(js_error) if @js_error
end