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.
- checksums.yaml +4 -4
- data/documentation/docs/api/accessibility.md +51 -1
- data/documentation/docs/api/browser_context.md +28 -0
- data/documentation/docs/api/element_handle.md +4 -5
- data/documentation/docs/api/experimental/android.md +15 -2
- data/documentation/docs/api/frame.md +66 -97
- data/documentation/docs/api/locator.md +28 -41
- data/documentation/docs/api/mouse.md +3 -4
- data/documentation/docs/api/page.md +41 -1
- data/documentation/docs/api/request.md +15 -19
- data/documentation/docs/api/touchscreen.md +8 -0
- data/documentation/docs/api/tracing.md +13 -12
- data/documentation/docs/api/worker.md +46 -8
- data/documentation/docs/article/guides/inspector.md +1 -1
- data/documentation/docs/article/guides/playwright_on_alpine_linux.md +1 -1
- data/documentation/docs/article/guides/semi_automation.md +1 -1
- data/documentation/docs/article/guides/use_storage_state.md +78 -0
- data/documentation/docs/include/api_coverage.md +13 -13
- data/lib/playwright/accessibility_impl.rb +50 -0
- data/lib/playwright/channel_owners/browser_context.rb +45 -0
- data/lib/playwright/channel_owners/frame.rb +9 -0
- data/lib/playwright/channel_owners/page.rb +31 -2
- data/lib/playwright/channel_owners/request.rb +8 -8
- data/lib/playwright/channel_owners/worker.rb +23 -0
- data/lib/playwright/locator_impl.rb +3 -3
- data/lib/playwright/touchscreen_impl.rb +7 -0
- data/lib/playwright/tracing_impl.rb +9 -8
- data/lib/playwright/version.rb +1 -1
- data/lib/playwright_api/accessibility.rb +1 -1
- data/lib/playwright_api/android.rb +15 -2
- data/lib/playwright_api/browser_context.rb +8 -8
- data/lib/playwright_api/element_handle.rb +1 -1
- data/lib/playwright_api/frame.rb +5 -3
- data/lib/playwright_api/locator.rb +3 -3
- data/lib/playwright_api/page.rb +8 -6
- data/lib/playwright_api/touchscreen.rb +1 -1
- data/lib/playwright_api/worker.rb +13 -3
- 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
|
-
*
|
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
|
-
##
|
119
|
+
## Accessibility
|
120
120
|
|
121
|
-
*
|
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
|
-
*
|
181
|
+
* wait_for_timeout
|
182
182
|
* wait_for_url
|
183
183
|
|
184
184
|
## Worker
|
185
185
|
|
186
|
-
*
|
187
|
-
*
|
188
|
-
*
|
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
|
-
*
|
299
|
+
* wait_for_timeout
|
300
300
|
* wait_for_url
|
301
301
|
* expect_websocket
|
302
|
-
*
|
303
|
-
*
|
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
|
-
*
|
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
|
-
*
|
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
|
-
*
|
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 =
|
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
|
-
|
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[
|
104
|
-
@timing[
|
105
|
-
@timing[
|
106
|
-
@timing[
|
107
|
-
@timing[
|
108
|
-
@timing[
|
109
|
-
@timing[
|
110
|
-
@timing[
|
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)
|