playwright-ruby-client 0.6.4 → 0.8.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 (52) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +26 -0
  3. data/documentation/docs/api/browser.md +10 -0
  4. data/documentation/docs/api/browser_context.md +10 -0
  5. data/documentation/docs/api/browser_type.md +53 -0
  6. data/documentation/docs/api/cdp_session.md +41 -1
  7. data/documentation/docs/api/element_handle.md +11 -2
  8. data/documentation/docs/api/frame.md +15 -1
  9. data/documentation/docs/api/page.md +33 -1
  10. data/documentation/docs/api/response.md +16 -0
  11. data/documentation/docs/api/route.md +20 -21
  12. data/documentation/docs/api/web_socket.md +38 -1
  13. data/documentation/docs/article/guides/launch_browser.md +2 -0
  14. data/documentation/docs/article/guides/rails_integration.md +2 -2
  15. data/documentation/docs/article/guides/semi_automation.md +67 -0
  16. data/documentation/docs/include/api_coverage.md +19 -13
  17. data/lib/playwright.rb +36 -3
  18. data/lib/playwright/channel_owners/browser.rb +5 -0
  19. data/lib/playwright/channel_owners/browser_context.rb +5 -0
  20. data/lib/playwright/channel_owners/browser_type.rb +18 -0
  21. data/lib/playwright/channel_owners/cdp_session.rb +19 -0
  22. data/lib/playwright/channel_owners/element_handle.rb +11 -4
  23. data/lib/playwright/channel_owners/frame.rb +14 -2
  24. data/lib/playwright/channel_owners/page.rb +21 -2
  25. data/lib/playwright/channel_owners/response.rb +9 -1
  26. data/lib/playwright/channel_owners/web_socket.rb +87 -0
  27. data/lib/playwright/connection.rb +2 -4
  28. data/lib/playwright/transport.rb +0 -1
  29. data/lib/playwright/version.rb +2 -2
  30. data/lib/playwright/web_socket_client.rb +164 -0
  31. data/lib/playwright/web_socket_transport.rb +104 -0
  32. data/lib/playwright_api/android.rb +6 -6
  33. data/lib/playwright_api/android_device.rb +8 -8
  34. data/lib/playwright_api/browser.rb +7 -7
  35. data/lib/playwright_api/browser_context.rb +7 -7
  36. data/lib/playwright_api/browser_type.rb +9 -8
  37. data/lib/playwright_api/cdp_session.rb +30 -8
  38. data/lib/playwright_api/console_message.rb +6 -6
  39. data/lib/playwright_api/dialog.rb +6 -6
  40. data/lib/playwright_api/element_handle.rb +17 -11
  41. data/lib/playwright_api/frame.rb +20 -9
  42. data/lib/playwright_api/js_handle.rb +6 -6
  43. data/lib/playwright_api/page.rb +28 -17
  44. data/lib/playwright_api/playwright.rb +6 -6
  45. data/lib/playwright_api/request.rb +6 -6
  46. data/lib/playwright_api/response.rb +15 -10
  47. data/lib/playwright_api/route.rb +11 -6
  48. data/lib/playwright_api/selectors.rb +6 -6
  49. data/lib/playwright_api/web_socket.rb +28 -6
  50. data/lib/playwright_api/worker.rb +6 -6
  51. data/playwright.gemspec +2 -1
  52. metadata +37 -18
@@ -117,3 +117,5 @@ Playwright.create(playwright_cli_executable_path: 'npx playwright') do |playwrig
117
117
  end
118
118
  end
119
119
  ```
120
+
121
+ Note that each browser context is isolated and not persisted by default. When persistent browser context is needed, we can use [BrowserType#launch_persistent_context](/docs/api/browser_type#launch_persistent_context).
@@ -41,7 +41,7 @@ Capybara.default_max_wait_time = 15
41
41
 
42
42
  ### (Optional) Update default driver
43
43
 
44
- By default, Capybara driver is set to `:rack_test`, which works only with non-JS contents. If your Rails application has many JavaScript contents, it is recommended to change the default driver to `:playwrite`.
44
+ By default, Capybara driver is set to `:rack_test`, which works only with non-JS contents. If your Rails application has many JavaScript contents, it is recommended to change the default driver to `:playwright`.
45
45
 
46
46
  ```rb
47
47
  Capybara.default_driver = :playwright
@@ -202,4 +202,4 @@ end
202
202
 
203
203
  * Playwright doesn't allow clicking invisible DOM elements or moving elements. `click` sometimes doesn't work as Selenium does. See the detail in https://playwright.dev/docs/actionability/
204
204
  * `current_window.maximize` and `current_window.fullscreen` work only on headful (non-headless) mode, as selenium driver does.
205
- * `Capybara::Node::Element#drag_to` does not accept `html5` parameter, since [Playwright doesn't implement the feature yet](https://github.com/microsoft/playwright/pull/6207).
205
+ * `Capybara::Node::Element#drag_to` does not accept `html5` parameter. HTML5 drag and drop is not fully supported in Playwright.
@@ -0,0 +1,67 @@
1
+ # Semi-automation
2
+
3
+ Playwright Browser context is isolated and not persisted by default. But we can also use persistent browser context using [BrowserType#launch_persistent_context](/docs/api/browser_type#launch_persistent_context).
4
+ This allow us to intermediate into automation, for example
5
+
6
+ * Authenticate with OAuth2 manually before automation
7
+ * Testing a page after some chrome extensions are installed manually
8
+
9
+ Keep in mind repeatedly that persistent browser context is NOT RECOMMENDED for most cases because it would bring many side effects.
10
+
11
+ ## Pause automation for manual operation
12
+
13
+ `Page#pause` is not implemented yet, however we can use `binding.pry` (with `pry-byebug` installed) instead.
14
+
15
+ ```ruby {4}
16
+ playwright.chromium.launch_persistent_context('./data/', headless: false) do |context|
17
+ page = context.new_page
18
+ page.goto('https://example.com/')
19
+ binding.pry
20
+ end
21
+ ```
22
+
23
+ When script is executed, it is paused as below.
24
+
25
+ ```
26
+ 3:
27
+ 4: playwright.chromium.launch_persistent_context('./data/', headless: false) do |context|
28
+ 5: page = context.new_page
29
+ 6: page.goto('https://example.com/')
30
+ => 7: binding.pry
31
+ 8: end
32
+
33
+ [1] pry(main)>
34
+ ```
35
+
36
+ We can inspect using `page`, `context` and also we can operate something manually during the pause.
37
+
38
+ See https://github.com/deivid-rodriguez/pry-byebug for more detailed debugging options.
39
+
40
+ ## Working with Chrome extensions
41
+
42
+ **Playwright disables the Chrome extension feature by default.**
43
+ We have to enable it for installing Chrome extension, by passing these 3 parameters on launch.
44
+
45
+ * `acceptDownloads: true`
46
+ * `headless: false`
47
+ * `ignoreDefaultArgs: ['--disable-extensions']`
48
+
49
+ ```ruby
50
+ require 'playwright'
51
+ require 'pry'
52
+
53
+ Playwright.create(playwright_cli_executable_path: './node_modules/.bin/playwright') do |playwright|
54
+ launch_params = {
55
+ acceptDownloads: true,
56
+ channel: 'chrome',
57
+ headless: false,
58
+ ignoreDefaultArgs: ['--disable-extensions'],
59
+ }
60
+
61
+ playwright.chromium.launch_persistent_context('./data/', **launch_params) do |context|
62
+ page = context.new_page
63
+ page.goto('https://example.com/')
64
+ binding.pry
65
+ end
66
+ end
67
+ ```
@@ -26,6 +26,8 @@
26
26
  * json
27
27
  * ok
28
28
  * request
29
+ * security_details
30
+ * server_addr
29
31
  * status
30
32
  * status_text
31
33
  * text
@@ -36,14 +38,15 @@
36
38
  * abort
37
39
  * continue
38
40
  * fulfill
41
+ * ~~intercept~~
39
42
  * request
40
43
 
41
- ## ~~WebSocket~~
44
+ ## WebSocket
42
45
 
43
- * ~~closed?~~
44
- * ~~url~~
45
- * ~~expect_event~~
46
- * ~~wait_for_event~~
46
+ * closed?
47
+ * url
48
+ * expect_event
49
+ * wait_for_event
47
50
 
48
51
  ## Keyboard
49
52
 
@@ -91,6 +94,7 @@
91
94
  * hover
92
95
  * inner_html
93
96
  * inner_text
97
+ * input_value
94
98
  * checked?
95
99
  * disabled?
96
100
  * editable?
@@ -146,6 +150,7 @@
146
150
  * hover
147
151
  * inner_html
148
152
  * inner_text
153
+ * input_value
149
154
  * checked?
150
155
  * detached?
151
156
  * disabled?
@@ -231,6 +236,7 @@
231
236
  * hover
232
237
  * inner_html
233
238
  * inner_text
239
+ * input_value
234
240
  * checked?
235
241
  * closed?
236
242
  * disabled?
@@ -273,12 +279,12 @@
273
279
  * expect_navigation
274
280
  * expect_popup
275
281
  * expect_request
276
- * ~~expect_request_finished~~
282
+ * expect_request_finished
277
283
  * expect_response
278
284
  * wait_for_selector
279
285
  * ~~wait_for_timeout~~
280
286
  * wait_for_url
281
- * ~~expect_websocket~~
287
+ * expect_websocket
282
288
  * ~~expect_worker~~
283
289
  * ~~workers~~
284
290
  * ~~wait_for_event~~
@@ -300,7 +306,7 @@
300
306
  * expose_binding
301
307
  * expose_function
302
308
  * grant_permissions
303
- * ~~new_cdp_session~~
309
+ * new_cdp_session
304
310
  * new_page
305
311
  * pages
306
312
  * route
@@ -317,17 +323,17 @@
317
323
  * ~~wait_for_event~~
318
324
  * tracing
319
325
 
320
- ## ~~CDPSession~~
326
+ ## CDPSession
321
327
 
322
- * ~~detach~~
323
- * ~~send_message~~
328
+ * detach
329
+ * send_message
324
330
 
325
331
  ## Browser
326
332
 
327
333
  * close
328
334
  * contexts
329
335
  * connected?
330
- * ~~new_browser_cdp_session~~
336
+ * new_browser_cdp_session
331
337
  * new_context
332
338
  * new_page
333
339
  * start_tracing
@@ -340,7 +346,7 @@
340
346
  * connect_over_cdp
341
347
  * executable_path
342
348
  * launch
343
- * ~~launch_persistent_context~~
349
+ * launch_persistent_context
344
350
  * name
345
351
 
346
352
  ## Playwright
data/lib/playwright.rb CHANGED
@@ -59,7 +59,8 @@ module Playwright
59
59
  # and we *must* call execution.stop on the end.
60
60
  # The instance of playwright is available by calling execution.playwright
61
61
  module_function def create(playwright_cli_executable_path:, &block)
62
- connection = Connection.new(playwright_cli_executable_path: playwright_cli_executable_path)
62
+ transport = Transport.new(playwright_cli_executable_path: playwright_cli_executable_path)
63
+ connection = Connection.new(transport)
63
64
  connection.async_run
64
65
 
65
66
  execution =
@@ -82,7 +83,39 @@ module Playwright
82
83
  end
83
84
  end
84
85
 
85
- module_function def instance
86
- @playwright_instance
86
+ # Connects to Playwright server, launched by `npx playwright run-server` via WebSocket transport.
87
+ #
88
+ # Playwright.connect_to_playwright_server(...) do |playwright|
89
+ # browser = playwright.chromium.launch
90
+ # ...
91
+ # end
92
+ #
93
+ # @experimental
94
+ module_function def connect_to_playwright_server(ws_endpoint, &block)
95
+ require 'playwright/web_socket_client'
96
+ require 'playwright/web_socket_transport'
97
+
98
+ transport = WebSocketTransport.new(ws_endpoint: ws_endpoint)
99
+ connection = Connection.new(transport)
100
+ connection.async_run
101
+
102
+ execution =
103
+ begin
104
+ playwright = connection.wait_for_object_with_known_name('Playwright')
105
+ Execution.new(connection, PlaywrightApi.wrap(playwright))
106
+ rescue
107
+ connection.stop
108
+ raise
109
+ end
110
+
111
+ if block
112
+ begin
113
+ block.call(execution.playwright)
114
+ ensure
115
+ execution.stop
116
+ end
117
+ else
118
+ execution
119
+ end
87
120
  end
88
121
  end
@@ -67,6 +67,11 @@ module Playwright
67
67
  @initializer['version']
68
68
  end
69
69
 
70
+ def new_browser_cdp_session
71
+ resp = @channel.send_message_to_server('newBrowserCDPSession')
72
+ ChannelOwners::CDPSession.from(resp)
73
+ end
74
+
70
75
  def start_tracing(page: nil, categories: nil, path: nil, screenshots: nil)
71
76
  params = {
72
77
  page: page&.channel,
@@ -98,6 +98,11 @@ module Playwright
98
98
  page&.emit(Events::Page::Response, response)
99
99
  end
100
100
 
101
+ def new_cdp_session(page)
102
+ resp = @channel.send_message_to_server('newCDPSession', page: page.channel)
103
+ ChannelOwners::CDPSession.from(resp)
104
+ end
105
+
101
106
  def set_default_navigation_timeout(timeout)
102
107
  @timeout_settings.default_navigation_timeout = timeout
103
108
  @channel.send_message_to_server('setDefaultNavigationTimeoutNoReply', timeout: timeout)
@@ -1,5 +1,7 @@
1
1
  module Playwright
2
2
  define_channel_owner :BrowserType do
3
+ include Utils::PrepareBrowserContextOptions
4
+
3
5
  def name
4
6
  @initializer['name']
5
7
  end
@@ -20,6 +22,22 @@ module Playwright
20
22
  end
21
23
  end
22
24
 
25
+ def launch_persistent_context(userDataDir, **options, &block)
26
+ params = options.dup
27
+ prepare_browser_context_options(params)
28
+ params['userDataDir'] = userDataDir
29
+
30
+ resp = @channel.send_message_to_server('launchPersistentContext', params.compact)
31
+ context = ChannelOwners::Browser.from(resp)
32
+ return context unless block
33
+
34
+ begin
35
+ block.call(context)
36
+ ensure
37
+ context.close
38
+ end
39
+ end
40
+
23
41
  def connect_over_cdp(endpointURL, headers: nil, slowMo: nil, timeout: nil, &block)
24
42
  raise 'Connecting over CDP is only supported in Chromium.' unless name == 'chromium'
25
43
 
@@ -0,0 +1,19 @@
1
+ module Playwright
2
+ define_channel_owner :CDPSession do
3
+ private def after_initialize
4
+ @channel.on('event', method(:on_event))
5
+ end
6
+
7
+ private def on_event(params)
8
+ emit(params['method'], params['params'])
9
+ end
10
+
11
+ def send_message(method, params: {})
12
+ @channel.send_message_to_server('send', method: method, params: params)
13
+ end
14
+
15
+ def detach
16
+ @channel.send_message_to_server('detach')
17
+ end
18
+ end
19
+ end
@@ -146,6 +146,7 @@ module Playwright
146
146
  index: nil,
147
147
  value: nil,
148
148
  label: nil,
149
+ force: nil,
149
150
  noWaitAfter: nil,
150
151
  timeout: nil)
151
152
  base_params = SelectOptionValues.new(
@@ -154,7 +155,7 @@ module Playwright
154
155
  value: value,
155
156
  label: label,
156
157
  ).as_params
157
- params = base_params.merge({ noWaitAfter: noWaitAfter, timeout: timeout }.compact)
158
+ params = base_params.merge({ force: force, noWaitAfter: noWaitAfter, timeout: timeout }.compact)
158
159
  @channel.send_message_to_server('selectOption', params)
159
160
  end
160
161
 
@@ -179,9 +180,10 @@ module Playwright
179
180
  nil
180
181
  end
181
182
 
182
- def fill(value, noWaitAfter: nil, timeout: nil)
183
+ def fill(value, force: nil, noWaitAfter: nil, timeout: nil)
183
184
  params = {
184
185
  value: value,
186
+ force: force,
185
187
  noWaitAfter: noWaitAfter,
186
188
  timeout: timeout,
187
189
  }.compact
@@ -190,13 +192,18 @@ module Playwright
190
192
  nil
191
193
  end
192
194
 
193
- def select_text(timeout: nil)
194
- params = { timeout: timeout }.compact
195
+ def select_text(force: nil, timeout: nil)
196
+ params = { force: force, timeout: timeout }.compact
195
197
  @channel.send_message_to_server('selectText', params)
196
198
 
197
199
  nil
198
200
  end
199
201
 
202
+ def input_value(timeout: nil)
203
+ params = { timeout: timeout }.compact
204
+ @channel.send_message_to_server('inputValue', params)
205
+ end
206
+
200
207
  def set_input_files(files, noWaitAfter: nil, timeout: nil)
201
208
  file_payloads = InputFiles.new(files).as_params
202
209
  params = { files: file_payloads, noWaitAfter: noWaitAfter, timeout: timeout }.compact
@@ -342,10 +342,16 @@ module Playwright
342
342
  nil
343
343
  end
344
344
 
345
- def fill(selector, value, noWaitAfter: nil, timeout: nil)
345
+ def fill(
346
+ selector,
347
+ value,
348
+ force: nil,
349
+ noWaitAfter: nil,
350
+ timeout: nil)
346
351
  params = {
347
352
  selector: selector,
348
353
  value: value,
354
+ force: force,
349
355
  noWaitAfter: noWaitAfter,
350
356
  timeout: timeout,
351
357
  }.compact
@@ -410,6 +416,7 @@ module Playwright
410
416
  index: nil,
411
417
  value: nil,
412
418
  label: nil,
419
+ force: nil,
413
420
  noWaitAfter: nil,
414
421
  timeout: nil)
415
422
  base_params = SelectOptionValues.new(
@@ -418,10 +425,15 @@ module Playwright
418
425
  value: value,
419
426
  label: label,
420
427
  ).as_params
421
- params = base_params.merge({ selector: selector, noWaitAfter: noWaitAfter, timeout: timeout }.compact)
428
+ params = base_params.merge({ selector: selector, force: force, noWaitAfter: noWaitAfter, timeout: timeout }.compact)
422
429
  @channel.send_message_to_server('selectOption', params)
423
430
  end
424
431
 
432
+ def input_value(selector, timeout: nil)
433
+ params = { selector: selector, timeout: timeout }.compact
434
+ @channel.send_message_to_server('inputValue', params)
435
+ end
436
+
425
437
  def set_input_files(selector, files, noWaitAfter: nil, timeout: nil)
426
438
  file_payloads = InputFiles.new(files).as_params
427
439
  params = { files: file_payloads, selector: selector, noWaitAfter: noWaitAfter, timeout: timeout }.compact
@@ -505,8 +505,13 @@ module Playwright
505
505
  )
506
506
  end
507
507
 
508
- def fill(selector, value, noWaitAfter: nil, timeout: nil)
509
- @main_frame.fill(selector, value, noWaitAfter: noWaitAfter, timeout: timeout)
508
+ def fill(
509
+ selector,
510
+ value,
511
+ force: nil,
512
+ noWaitAfter: nil,
513
+ timeout: nil)
514
+ @main_frame.fill(selector, value, force: force, noWaitAfter: noWaitAfter, timeout: timeout)
510
515
  end
511
516
 
512
517
  def focus(selector, timeout: nil)
@@ -552,6 +557,7 @@ module Playwright
552
557
  index: nil,
553
558
  value: nil,
554
559
  label: nil,
560
+ force: nil,
555
561
  noWaitAfter: nil,
556
562
  timeout: nil)
557
563
  @main_frame.select_option(
@@ -560,11 +566,16 @@ module Playwright
560
566
  index: index,
561
567
  value: value,
562
568
  label: label,
569
+ force: force,
563
570
  noWaitAfter: noWaitAfter,
564
571
  timeout: timeout,
565
572
  )
566
573
  end
567
574
 
575
+ def input_value(selector, timeout: nil)
576
+ @main_frame.input_value(selector, timeout: timeout)
577
+ end
578
+
568
579
  def set_input_files(selector, files, noWaitAfter: nil, timeout: nil)
569
580
  @main_frame.set_input_files(selector, files, noWaitAfter: noWaitAfter, timeout: timeout)
570
581
  end
@@ -759,6 +770,10 @@ module Playwright
759
770
  expect_event(Events::Page::Request, predicate: predicate, timeout: timeout, &block)
760
771
  end
761
772
 
773
+ def expect_request_finished(predicate: nil, timeout: nil, &block)
774
+ expect_event(Events::Page::RequestFinished, predicate: predicate, timeout: timeout, &block)
775
+ end
776
+
762
777
  def expect_response(urlOrPredicate, timeout: nil, &block)
763
778
  predicate =
764
779
  case urlOrPredicate
@@ -774,6 +789,10 @@ module Playwright
774
789
  expect_event(Events::Page::Response, predicate: predicate, timeout: timeout, &block)
775
790
  end
776
791
 
792
+ def expect_websocket(predicate: nil, timeout: nil, &block)
793
+ expect_event(Events::Page::WebSocket, predicate: predicate, timeout: timeout, &block)
794
+ end
795
+
777
796
  # called from Frame with send(:timeout_settings)
778
797
  private def timeout_settings
779
798
  @timeout_settings