playwright-ruby-client 0.9.0 → 1.14.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/documentation/docs/api/accessibility.md +51 -1
  3. data/documentation/docs/api/browser_context.md +28 -0
  4. data/documentation/docs/api/element_handle.md +4 -5
  5. data/documentation/docs/api/experimental/android.md +15 -2
  6. data/documentation/docs/api/frame.md +66 -97
  7. data/documentation/docs/api/locator.md +28 -41
  8. data/documentation/docs/api/mouse.md +3 -4
  9. data/documentation/docs/api/page.md +41 -1
  10. data/documentation/docs/api/request.md +15 -19
  11. data/documentation/docs/api/touchscreen.md +8 -0
  12. data/documentation/docs/api/tracing.md +13 -12
  13. data/documentation/docs/api/worker.md +46 -8
  14. data/documentation/docs/article/guides/inspector.md +1 -1
  15. data/documentation/docs/article/guides/playwright_on_alpine_linux.md +1 -1
  16. data/documentation/docs/article/guides/semi_automation.md +1 -1
  17. data/documentation/docs/article/guides/use_storage_state.md +78 -0
  18. data/documentation/docs/include/api_coverage.md +13 -13
  19. data/lib/playwright/accessibility_impl.rb +50 -0
  20. data/lib/playwright/channel_owners/browser_context.rb +45 -0
  21. data/lib/playwright/channel_owners/frame.rb +9 -0
  22. data/lib/playwright/channel_owners/page.rb +31 -2
  23. data/lib/playwright/channel_owners/request.rb +8 -8
  24. data/lib/playwright/channel_owners/worker.rb +23 -0
  25. data/lib/playwright/locator_impl.rb +3 -3
  26. data/lib/playwright/touchscreen_impl.rb +7 -0
  27. data/lib/playwright/tracing_impl.rb +9 -8
  28. data/lib/playwright/version.rb +1 -1
  29. data/lib/playwright_api/accessibility.rb +1 -1
  30. data/lib/playwright_api/android.rb +15 -2
  31. data/lib/playwright_api/browser_context.rb +8 -8
  32. data/lib/playwright_api/element_handle.rb +1 -1
  33. data/lib/playwright_api/frame.rb +5 -3
  34. data/lib/playwright_api/locator.rb +3 -3
  35. data/lib/playwright_api/page.rb +8 -6
  36. data/lib/playwright_api/touchscreen.rb +1 -1
  37. data/lib/playwright_api/worker.rb +13 -3
  38. metadata +4 -2
@@ -0,0 +1,78 @@
1
+ ---
2
+ sidebar_position: 6
3
+ ---
4
+
5
+ # Reuse Cookie and LocalStorage
6
+
7
+ In most cases, authentication state is stored in cookie or local storage. When we just want to keep authenticated, it is a good solution to dump/load 'storage state' (= Cookie + LocalStorage).
8
+ https://playwright.dev/docs/next/auth#reuse-authentication-state
9
+
10
+ * Dump storage state using [BrowserContext#storage_state](/docs/api/browser_context#storage_state) with `path: /path/to/state.json`
11
+ * Load storage state by specifying the parameter `storageState: /path/to/state.json` into [Browser#new_context](/docs/api/browser#new_context) or [Browser#new_page](/docs/api/browser#new_page)
12
+
13
+ ## Example
14
+
15
+ Generally in browser automation, it is very difficult to bypass 2FA or reCAPTCHA in login screen. In such cases, we would consider
16
+
17
+ * Authenticate manually by hand
18
+ * Resume automation with the authentication result
19
+
20
+
21
+ ```ruby {16,21}
22
+ require 'playwright'
23
+ require 'pry'
24
+
25
+ force_login = !File.exist?('github_state.json')
26
+
27
+ Playwright.create(playwright_cli_executable_path: 'npx playwright') do |playwright|
28
+ if force_login
29
+ # Use headful mode for manual operation.
30
+ playwright.chromium.launch(headless: false, channel: 'chrome') do |browser|
31
+ page = browser.new_page
32
+ page.goto('https://github.com/login')
33
+
34
+ # Login manually.
35
+ binding.pry
36
+
37
+ page.context.storage_state(path: 'github_state.json')
38
+ end
39
+ end
40
+
41
+ playwright.chromium.launch do |browser|
42
+ page = browser.new_page(storageState: 'github_state.json')
43
+ page.goto('https://github.com/notifications')
44
+ page.screenshot(path: 'github_notification.png')
45
+ end
46
+ end
47
+ ```
48
+
49
+ When we execute this script at the first time (without github_state.json), login screen is shown:
50
+
51
+ ![login screen is shown](https://user-images.githubusercontent.com/11763113/129394130-7a248f6a-56f0-40b0-a4dd-f0f65d71b3a9.png)
52
+
53
+ and input credentials manually:
54
+
55
+ ![input credentials manually](https://user-images.githubusercontent.com/11763113/129394155-fccc280e-5e6b-46c7-8a4d-a99d7db02c7f.png)
56
+
57
+ and hit `exit` in Pry console.
58
+
59
+ ```
60
+
61
+ 9:
62
+ 10: # Login manually. Hit `exit` in Pry console after authenticated.
63
+ 11: require 'pry'
64
+ 12: binding.pry
65
+ 13:
66
+ => 14: page.context.storage_state(path: 'github_state.json')
67
+ 15: end if force_login
68
+ 16:
69
+ 17: playwright.chromium.launch do |browser|
70
+ 18: page = browser.new_page(storageState: 'github_state.json')
71
+ 19: page.goto('https://github.com/notifications')
72
+
73
+ [1] pry(main)> exit
74
+ ```
75
+
76
+ then we can enjoy automation with keeping authenticated. Login screen is never shown until github_state.json is deleted :)
77
+
78
+ ![github_notification.png](https://user-images.githubusercontent.com/11763113/129394879-838797eb-135f-41ab-b965-8d6fabde6109.png)
@@ -65,7 +65,7 @@
65
65
 
66
66
  ## Touchscreen
67
67
 
68
- * ~~tap_point~~
68
+ * tap_point
69
69
 
70
70
  ## JSHandle
71
71
 
@@ -116,9 +116,9 @@
116
116
  * wait_for_element_state
117
117
  * wait_for_selector
118
118
 
119
- ## ~~Accessibility~~
119
+ ## Accessibility
120
120
 
121
- * ~~snapshot~~
121
+ * snapshot
122
122
 
123
123
  ## FileChooser
124
124
 
@@ -178,14 +178,14 @@
178
178
  * wait_for_load_state
179
179
  * expect_navigation
180
180
  * wait_for_selector
181
- * ~~wait_for_timeout~~
181
+ * wait_for_timeout
182
182
  * wait_for_url
183
183
 
184
184
  ## Worker
185
185
 
186
- * ~~evaluate~~
187
- * ~~evaluate_handle~~
188
- * ~~url~~
186
+ * evaluate
187
+ * evaluate_handle
188
+ * url
189
189
 
190
190
  ## Selectors
191
191
 
@@ -296,11 +296,11 @@
296
296
  * expect_request_finished
297
297
  * expect_response
298
298
  * wait_for_selector
299
- * ~~wait_for_timeout~~
299
+ * wait_for_timeout
300
300
  * wait_for_url
301
301
  * expect_websocket
302
- * ~~expect_worker~~
303
- * ~~workers~~
302
+ * expect_worker
303
+ * workers
304
304
  * ~~wait_for_event~~
305
305
  * accessibility
306
306
  * keyboard
@@ -311,7 +311,7 @@
311
311
 
312
312
  * add_cookies
313
313
  * add_init_script
314
- * ~~background_pages~~
314
+ * background_pages
315
315
  * browser
316
316
  * clear_cookies
317
317
  * clear_permissions
@@ -324,13 +324,13 @@
324
324
  * new_page
325
325
  * pages
326
326
  * route
327
- * ~~service_workers~~
327
+ * service_workers
328
328
  * set_default_navigation_timeout
329
329
  * set_default_timeout
330
330
  * set_extra_http_headers
331
331
  * set_geolocation
332
332
  * set_offline
333
- * ~~storage_state~~
333
+ * storage_state
334
334
  * unroute
335
335
  * expect_event
336
336
  * expect_page
@@ -0,0 +1,50 @@
1
+ module Playwright
2
+ define_api_implementation :AccessibilityImpl do
3
+ def initialize(channel)
4
+ @channel = channel
5
+ end
6
+
7
+ def snapshot(interestingOnly: nil, root: nil)
8
+ params = {
9
+ interestingOnly: interestingOnly,
10
+ root: root&.channel,
11
+ }.compact
12
+ result = @channel.send_message_to_server('accessibilitySnapshot', params)
13
+ format_ax_node_from_protocol(result) if result
14
+ result
15
+ end
16
+
17
+ # original JS implementation create a new Hash from ax_node,
18
+ # but this implementation directly modify ax_node and don't return hash.
19
+ private def format_ax_node_from_protocol(ax_node)
20
+ value = ax_node.delete('valueNumber') || ax_node.delete('valueString')
21
+ ax_node['value'] = value unless value.nil?
22
+
23
+ checked =
24
+ case ax_node['checked']
25
+ when 'checked'
26
+ true
27
+ when 'unchecked'
28
+ false
29
+ else
30
+ ax_node['checked']
31
+ end
32
+ ax_node['checked'] = checked unless checked.nil?
33
+
34
+ pressed =
35
+ case ax_node['pressed']
36
+ when 'pressed'
37
+ true
38
+ when 'released'
39
+ false
40
+ else
41
+ ax_node['pressed']
42
+ end
43
+ ax_node['pressed'] = pressed unless pressed.nil?
44
+
45
+ ax_node['children']&.each do |child|
46
+ format_ax_node_from_protocol(child)
47
+ end
48
+ end
49
+ end
50
+ end
@@ -11,6 +11,8 @@ module Playwright
11
11
  @routes = []
12
12
  @bindings = {}
13
13
  @timeout_settings = TimeoutSettings.new
14
+ @service_workers = Set.new
15
+ @background_pages = Set.new
14
16
 
15
17
  @tracing = TracingImpl.new(@channel, self)
16
18
  @channel.on('bindingCall', ->(params) { on_binding(ChannelOwners::BindingCall.from(params['binding'])) })
@@ -19,6 +21,12 @@ module Playwright
19
21
  @channel.on('route', ->(params) {
20
22
  on_route(ChannelOwners::Route.from(params['route']), ChannelOwners::Request.from(params['request']))
21
23
  })
24
+ @channel.on('backgroundPage', ->(params) {
25
+ on_background_page(ChannelOwners::Page.from(params['page']))
26
+ })
27
+ @channel.on('serviceWorker', ->(params) {
28
+ on_service_worker(ChannelOwners::Worker.from(params['worker']))
29
+ })
22
30
  @channel.on('request', ->(params) {
23
31
  on_request(
24
32
  ChannelOwners::Request.from(params['request']),
@@ -56,6 +64,11 @@ module Playwright
56
64
  page.send(:emit_popup_event_from_browser_context)
57
65
  end
58
66
 
67
+ private def on_background_page(page)
68
+ @background_pages << page
69
+ emit(Events::BrowserContext::BackgroundPage, page)
70
+ end
71
+
59
72
  private def on_route(route, request)
60
73
  # It is not desired to use PlaywrightApi.wrap directly.
61
74
  # However it is a little difficult to define wrapper for `handler` parameter in generate_api.
@@ -98,6 +111,20 @@ module Playwright
98
111
  page&.emit(Events::Page::Response, response)
99
112
  end
100
113
 
114
+ private def on_service_worker(worker)
115
+ worker.context = self
116
+ @service_workers << worker
117
+ emit(Events::BrowserContext::ServiceWorker, worker)
118
+ end
119
+
120
+ def background_pages
121
+ @background_pages.to_a
122
+ end
123
+
124
+ def service_workers
125
+ @service_workers.to_a
126
+ end
127
+
101
128
  def new_cdp_session(page)
102
129
  resp = @channel.send_message_to_server('newCDPSession', page: page.channel)
103
130
  ChannelOwners::CDPSession.from(resp)
@@ -279,6 +306,16 @@ module Playwright
279
306
  @channel.send_message_to_server('pause')
280
307
  end
281
308
 
309
+ def storage_state(path: nil)
310
+ @channel.send_message_to_server_result('storageState', {}).tap do |result|
311
+ if path
312
+ File.open(path, 'w') do |f|
313
+ f.write(JSON.dump(result))
314
+ end
315
+ end
316
+ end
317
+ end
318
+
282
319
  def expect_page(predicate: nil, timeout: nil)
283
320
  params = {
284
321
  predicate: predicate,
@@ -292,6 +329,14 @@ module Playwright
292
329
  @pages.delete(page)
293
330
  end
294
331
 
332
+ private def remove_background_page(page)
333
+ @background_pages.delete(page)
334
+ end
335
+
336
+ private def remove_service_worker(worker)
337
+ @service_workers.delete(worker)
338
+ end
339
+
295
340
  # called from Page with send(:_timeout_settings), so keep private.
296
341
  private def _timeout_settings
297
342
  @timeout_settings
@@ -309,15 +309,20 @@ module Playwright
309
309
  target,
310
310
  force: nil,
311
311
  noWaitAfter: nil,
312
+ sourcePosition: nil,
312
313
  strict: nil,
314
+ targetPosition: nil,
313
315
  timeout: nil,
314
316
  trial: nil)
317
+
315
318
  params = {
316
319
  source: source,
317
320
  target: target,
318
321
  force: force,
319
322
  noWaitAfter: noWaitAfter,
323
+ sourcePosition: sourcePosition,
320
324
  strict: strict,
325
+ targetPosition: targetPosition,
321
326
  timeout: timeout,
322
327
  trial: trial,
323
328
  }.compact
@@ -579,6 +584,10 @@ module Playwright
579
584
  nil
580
585
  end
581
586
 
587
+ def wait_for_timeout(timeout)
588
+ sleep(timeout / 1000.0)
589
+ end
590
+
582
591
  def wait_for_function(pageFunction, arg: nil, polling: nil, timeout: nil)
583
592
  if polling.is_a?(String) && polling != 'raf'
584
593
  raise ArgumentError.new("Unknown polling option: #{polling}")
@@ -9,7 +9,7 @@ module Playwright
9
9
  private def after_initialize
10
10
  @browser_context = @parent
11
11
  @timeout_settings = TimeoutSettings.new(@browser_context.send(:_timeout_settings))
12
- @accessibility = Accessibility.new(@channel)
12
+ @accessibility = AccessibilityImpl.new(@channel)
13
13
  @keyboard = KeyboardImpl.new(@channel)
14
14
  @mouse = MouseImpl.new(@channel)
15
15
  @touchscreen = TouchscreenImpl.new(@channel)
@@ -21,6 +21,7 @@ module Playwright
21
21
  }
22
22
  end
23
23
  @closed = false
24
+ @workers = Set.new
24
25
  @bindings = {}
25
26
  @routes = []
26
27
 
@@ -66,7 +67,7 @@ module Playwright
66
67
  })
67
68
  @channel.on('worker', ->(params) {
68
69
  worker = ChannelOwners::Worker.from(params['worker'])
69
- # on_worker(worker)
70
+ on_worker(worker)
70
71
  })
71
72
  end
72
73
 
@@ -110,9 +111,16 @@ module Playwright
110
111
  @browser_context.send(:on_binding, binding_call)
111
112
  end
112
113
 
114
+ private def on_worker(worker)
115
+ worker.page = self
116
+ @workers << worker
117
+ emit(Events::Page::Worker, worker)
118
+ end
119
+
113
120
  private def on_close
114
121
  @closed = true
115
122
  @browser_context.send(:remove_page, self)
123
+ @browser_context.send(:remove_background_page, self)
116
124
  emit(Events::Page::Close)
117
125
  end
118
126
 
@@ -467,7 +475,9 @@ module Playwright
467
475
  target,
468
476
  force: nil,
469
477
  noWaitAfter: nil,
478
+ sourcePosition: nil,
470
479
  strict: nil,
480
+ targetPosition: nil,
471
481
  timeout: nil,
472
482
  trial: nil)
473
483
 
@@ -476,7 +486,9 @@ module Playwright
476
486
  target,
477
487
  force: force,
478
488
  noWaitAfter: noWaitAfter,
489
+ sourcePosition: sourcePosition,
479
490
  strict: strict,
491
+ targetPosition: targetPosition,
480
492
  timeout: timeout,
481
493
  trial: trial)
482
494
  end
@@ -690,10 +702,18 @@ module Playwright
690
702
  trial: trial)
691
703
  end
692
704
 
705
+ def wait_for_timeout(timeout)
706
+ @main_frame.wait_for_timeout(timeout)
707
+ end
708
+
693
709
  def wait_for_function(pageFunction, arg: nil, polling: nil, timeout: nil)
694
710
  @main_frame.wait_for_function(pageFunction, arg: arg, polling: polling, timeout: timeout)
695
711
  end
696
712
 
713
+ def workers
714
+ @workers.to_a
715
+ end
716
+
697
717
  def pause
698
718
  @browser_context.send(:pause)
699
719
  end
@@ -865,6 +885,10 @@ module Playwright
865
885
  expect_event(Events::Page::WebSocket, predicate: predicate, timeout: timeout, &block)
866
886
  end
867
887
 
888
+ def expect_worker(predicate: nil, timeout: nil, &block)
889
+ expect_event(Events::Page::Worker, predicate: predicate, timeout: timeout, &block)
890
+ end
891
+
868
892
  # called from Frame with send(:timeout_settings)
869
893
  private def timeout_settings
870
894
  @timeout_settings
@@ -875,6 +899,11 @@ module Playwright
875
899
  @bindings.key?(name)
876
900
  end
877
901
 
902
+ # called from Worker#on_close
903
+ private def remove_worker(worker)
904
+ @workers.delete(worker)
905
+ end
906
+
878
907
  # Expose guid for library developers.
879
908
  # Not intended to be used by users.
880
909
  def guid
@@ -100,14 +100,14 @@ module Playwright
100
100
  request_start:,
101
101
  response_start:)
102
102
 
103
- @timing["startTime"] = start_time
104
- @timing["domainLookupStart"] = domain_lookup_start
105
- @timing["domainLookupEnd"] = domain_lookup_end
106
- @timing["connectStart"] = connect_start
107
- @timing["secureConnectionStart"] = secure_connection_start
108
- @timing["connectEnd"] = connect_end
109
- @timing["requestStart"] = request_start
110
- @timing["responseStart"] = response_start
103
+ @timing[:startTime] = start_time
104
+ @timing[:domainLookupStart] = domain_lookup_start
105
+ @timing[:domainLookupEnd] = domain_lookup_end
106
+ @timing[:connectStart] = connect_start
107
+ @timing[:secureConnectionStart] = secure_connection_start
108
+ @timing[:connectEnd] = connect_end
109
+ @timing[:requestStart] = request_start
110
+ @timing[:responseStart] = response_start
111
111
  end
112
112
 
113
113
  private def update_headers(headers)