playwright-ruby-client 0.9.0 → 1.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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)