playwright-ruby-client 0.7.1 → 1.14.beta1

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 (56) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +26 -0
  3. data/documentation/docs/api/accessibility.md +52 -1
  4. data/documentation/docs/api/browser.md +8 -2
  5. data/documentation/docs/api/browser_context.md +28 -0
  6. data/documentation/docs/api/browser_type.md +1 -0
  7. data/documentation/docs/api/download.md +97 -0
  8. data/documentation/docs/api/element_handle.md +38 -4
  9. data/documentation/docs/api/experimental/android_device.md +1 -0
  10. data/documentation/docs/api/frame.md +89 -17
  11. data/documentation/docs/api/keyboard.md +11 -20
  12. data/documentation/docs/api/locator.md +650 -0
  13. data/documentation/docs/api/page.md +135 -19
  14. data/documentation/docs/api/response.md +16 -0
  15. data/documentation/docs/api/touchscreen.md +8 -0
  16. data/documentation/docs/api/worker.md +37 -0
  17. data/documentation/docs/article/guides/inspector.md +31 -0
  18. data/documentation/docs/article/guides/playwright_on_alpine_linux.md +91 -0
  19. data/documentation/docs/article/guides/semi_automation.md +5 -1
  20. data/documentation/docs/include/api_coverage.md +77 -14
  21. data/lib/playwright.rb +36 -4
  22. data/lib/playwright/accessibility_impl.rb +50 -0
  23. data/lib/playwright/channel_owners/artifact.rb +4 -0
  24. data/lib/playwright/channel_owners/browser_context.rb +77 -3
  25. data/lib/playwright/channel_owners/element_handle.rb +11 -4
  26. data/lib/playwright/channel_owners/frame.rb +107 -34
  27. data/lib/playwright/channel_owners/page.rb +163 -55
  28. data/lib/playwright/channel_owners/response.rb +8 -0
  29. data/lib/playwright/channel_owners/worker.rb +23 -0
  30. data/lib/playwright/connection.rb +2 -4
  31. data/lib/playwright/{download.rb → download_impl.rb} +5 -1
  32. data/lib/playwright/javascript/expression.rb +5 -4
  33. data/lib/playwright/locator_impl.rb +314 -0
  34. data/lib/playwright/route_handler_entry.rb +3 -2
  35. data/lib/playwright/timeout_settings.rb +4 -4
  36. data/lib/playwright/touchscreen_impl.rb +7 -0
  37. data/lib/playwright/transport.rb +0 -1
  38. data/lib/playwright/url_matcher.rb +12 -2
  39. data/lib/playwright/version.rb +2 -2
  40. data/lib/playwright/web_socket_client.rb +164 -0
  41. data/lib/playwright/web_socket_transport.rb +104 -0
  42. data/lib/playwright_api/accessibility.rb +1 -1
  43. data/lib/playwright_api/android_device.rb +6 -5
  44. data/lib/playwright_api/browser.rb +10 -4
  45. data/lib/playwright_api/browser_context.rb +12 -7
  46. data/lib/playwright_api/browser_type.rb +2 -1
  47. data/lib/playwright_api/cdp_session.rb +6 -6
  48. data/lib/playwright_api/download.rb +70 -0
  49. data/lib/playwright_api/element_handle.rb +38 -18
  50. data/lib/playwright_api/frame.rb +95 -44
  51. data/lib/playwright_api/locator.rb +509 -0
  52. data/lib/playwright_api/page.rb +102 -49
  53. data/lib/playwright_api/response.rb +10 -0
  54. data/lib/playwright_api/touchscreen.rb +1 -1
  55. data/lib/playwright_api/worker.rb +13 -3
  56. metadata +17 -7
@@ -0,0 +1,91 @@
1
+ ---
2
+ sidebar_position: 7
3
+ ---
4
+
5
+ # Playwright on Alpine Linux
6
+
7
+ **NOTE: This feature is EXPERIMENTAL.**
8
+
9
+ Playwright actually requires a permission for shell command execution, and many run-time dependencies for each browser.
10
+
11
+ ![all-in-one](https://user-images.githubusercontent.com/11763113/124934388-9c9c9100-e03f-11eb-8f13-324afac3be2a.png)
12
+
13
+ This all-in-one architecture is reasonable for browser automation in our own computers.
14
+
15
+ However we may have trouble with bringing Playwright into:
16
+
17
+ * Docker
18
+ * Alpine Linux
19
+ * Serverless computing
20
+ * AWS Lambda
21
+ * Google Cloud Functions
22
+ * PaaS
23
+ * Heroku
24
+ * Google App Engine
25
+
26
+ This article introduces a way to separate environments into client (for executing Playwright script) and server (for working with browsers). The main use-case assumes Docker (using Alpine Linux), however the way can be applied also into other use-cases.
27
+
28
+ ## Overview
29
+
30
+ Playwrignt Ruby client is running on Alpine Linux. It just sends/receives JSON messages of Playwright-protocol via WebSocket.
31
+
32
+ Playwright server is running on a container of [official Docker image](https://hub.docker.com/_/microsoft-playwright). It just operates browsers in response to the JSON messages from WebSocket.
33
+
34
+ ![overview](https://user-images.githubusercontent.com/11763113/124934448-ad4d0700-e03f-11eb-942e-b9f3282bb703.png)
35
+
36
+ ## Playwright client
37
+
38
+ Many example uses `Playwright#create`, which internally uses Pipe (stdin/stdout) transport for Playwright-protocol messaging. Instead, **just use `Playwright#connect_to_playwright_server(endpoint)`** for WebSocket transport.
39
+
40
+ ```ruby {3}
41
+ require 'playwright'
42
+
43
+ Playwright.connect_to_playwright_server('wss://example.com:8888/ws') do |playwright|
44
+ playwright.chromium.launch do |browser|
45
+ page = browser.new_page
46
+ page.goto('https://github.com/microsoft/playwright')
47
+ page.screenshot(path: 'github-microsoft-playwright.png')
48
+ end
49
+ end
50
+ ```
51
+
52
+ `wss://example.com:8888/ws` is an example of endpoint URL of the Playwright server. In local development environment, it is typically `"ws://127.0.0.1:#{port}/ws"`.
53
+
54
+ ## Playwright server
55
+
56
+ With the [official Docker image](https://hub.docker.com/_/microsoft-playwright) or in the local development environment with Node.js, just execute `npx playwright install && npx playwright run-server $PORT`. (`$PORT` is a port number of the server)
57
+
58
+ If custom Docker image is preferred, build it as follows:
59
+
60
+ ```Dockerfile
61
+ FROM mcr.microsoft.com/playwright:focal
62
+
63
+ WORKDIR /root
64
+ RUN npm install playwright@1.12.3 && ./node_modules/.bin/playwright install
65
+
66
+ ENV PORT 8888
67
+ CMD ["./node_modules/.bin/playwright", "run-server", "$PORT"]
68
+ ```
69
+
70
+ ## Debugging for connection
71
+
72
+ The client and server are really quiet. This chapter shows how to check if the communication on the WebSocket works well or not.
73
+
74
+ ### Show JSON message on client
75
+
76
+ Just set an environment variable `DEBUG=1`.
77
+
78
+ ```
79
+ DEBUG=1 bundle exec ruby some-automation-with-playwright.rb
80
+ ```
81
+
82
+
83
+ ### Enable verbose logging on server
84
+
85
+ Just set an environment variable `DEBUG=pw:*` or `DEBUG=pw:server`
86
+
87
+ ```
88
+ DEBUG=pw:* npx playwright run-server 8888
89
+ ```
90
+
91
+ See [the official documentation](https://playwright.dev/docs/debug/#verbose-api-logs) for details.
@@ -1,3 +1,7 @@
1
+ ---
2
+ sidebar_position: 5
3
+ ---
4
+
1
5
  # Semi-automation
2
6
 
3
7
  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).
@@ -10,7 +14,7 @@ Keep in mind repeatedly that persistent browser context is NOT RECOMMENDED for m
10
14
 
11
15
  ## Pause automation for manual operation
12
16
 
13
- `Page#pause` is not implemented yet, however we can use `binding.pry` (with `pry-byebug` installed) instead.
17
+ We can simply use `binding.pry` (with `pry-byebug` installed).
14
18
 
15
19
  ```ruby {4}
16
20
  playwright.chromium.launch_persistent_context('./data/', headless: false) do |context|
@@ -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
@@ -63,7 +65,7 @@
63
65
 
64
66
  ## Touchscreen
65
67
 
66
- * ~~tap_point~~
68
+ * tap_point
67
69
 
68
70
  ## JSHandle
69
71
 
@@ -91,6 +93,7 @@
91
93
  * hover
92
94
  * inner_html
93
95
  * inner_text
96
+ * input_value
94
97
  * checked?
95
98
  * disabled?
96
99
  * editable?
@@ -113,9 +116,9 @@
113
116
  * wait_for_element_state
114
117
  * wait_for_selector
115
118
 
116
- ## ~~Accessibility~~
119
+ ## Accessibility
117
120
 
118
- * ~~snapshot~~
121
+ * snapshot
119
122
 
120
123
  ## FileChooser
121
124
 
@@ -134,6 +137,7 @@
134
137
  * content
135
138
  * dblclick
136
139
  * dispatch_event
140
+ * drag_and_drop
137
141
  * eval_on_selector
138
142
  * eval_on_selector_all
139
143
  * evaluate
@@ -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?
@@ -153,6 +158,7 @@
153
158
  * enabled?
154
159
  * hidden?
155
160
  * visible?
161
+ * locator
156
162
  * name
157
163
  * page
158
164
  * parent_frame
@@ -172,14 +178,14 @@
172
178
  * wait_for_load_state
173
179
  * expect_navigation
174
180
  * wait_for_selector
175
- * ~~wait_for_timeout~~
181
+ * wait_for_timeout
176
182
  * wait_for_url
177
183
 
178
184
  ## Worker
179
185
 
180
- * ~~evaluate~~
181
- * ~~evaluate_handle~~
182
- * ~~url~~
186
+ * evaluate
187
+ * evaluate_handle
188
+ * url
183
189
 
184
190
  ## Selectors
185
191
 
@@ -200,6 +206,17 @@
200
206
  * message
201
207
  * type
202
208
 
209
+ ## Download
210
+
211
+ * cancel
212
+ * delete
213
+ * failure
214
+ * page
215
+ * path
216
+ * save_as
217
+ * suggested_filename
218
+ * url
219
+
203
220
  ## Page
204
221
 
205
222
  * add_init_script
@@ -213,6 +230,7 @@
213
230
  * context
214
231
  * dblclick
215
232
  * dispatch_event
233
+ * drag_and_drop
216
234
  * emulate_media
217
235
  * eval_on_selector
218
236
  * eval_on_selector_all
@@ -231,6 +249,7 @@
231
249
  * hover
232
250
  * inner_html
233
251
  * inner_text
252
+ * input_value
234
253
  * checked?
235
254
  * closed?
236
255
  * disabled?
@@ -238,9 +257,10 @@
238
257
  * enabled?
239
258
  * hidden?
240
259
  * visible?
260
+ * locator
241
261
  * main_frame
242
262
  * opener
243
- * ~~pause~~
263
+ * pause
244
264
  * pdf
245
265
  * press
246
266
  * query_selector
@@ -276,11 +296,11 @@
276
296
  * expect_request_finished
277
297
  * expect_response
278
298
  * wait_for_selector
279
- * ~~wait_for_timeout~~
299
+ * wait_for_timeout
280
300
  * wait_for_url
281
301
  * expect_websocket
282
- * ~~expect_worker~~
283
- * ~~workers~~
302
+ * expect_worker
303
+ * workers
284
304
  * ~~wait_for_event~~
285
305
  * accessibility
286
306
  * keyboard
@@ -291,7 +311,7 @@
291
311
 
292
312
  * add_cookies
293
313
  * add_init_script
294
- * ~~background_pages~~
314
+ * background_pages
295
315
  * browser
296
316
  * clear_cookies
297
317
  * clear_permissions
@@ -304,13 +324,13 @@
304
324
  * new_page
305
325
  * pages
306
326
  * route
307
- * ~~service_workers~~
327
+ * service_workers
308
328
  * set_default_navigation_timeout
309
329
  * set_default_timeout
310
330
  * set_extra_http_headers
311
331
  * set_geolocation
312
332
  * set_offline
313
- * ~~storage_state~~
333
+ * storage_state
314
334
  * unroute
315
335
  * expect_event
316
336
  * expect_page
@@ -357,6 +377,49 @@
357
377
  * start
358
378
  * stop
359
379
 
380
+ ## Locator
381
+
382
+ * all_inner_texts
383
+ * all_text_contents
384
+ * bounding_box
385
+ * check
386
+ * click
387
+ * count
388
+ * dblclick
389
+ * dispatch_event
390
+ * element_handle
391
+ * element_handles
392
+ * evaluate
393
+ * evaluate_all
394
+ * evaluate_handle
395
+ * fill
396
+ * first
397
+ * focus
398
+ * get_attribute
399
+ * hover
400
+ * inner_html
401
+ * inner_text
402
+ * input_value
403
+ * checked?
404
+ * disabled?
405
+ * editable?
406
+ * enabled?
407
+ * hidden?
408
+ * visible?
409
+ * last
410
+ * locator
411
+ * nth
412
+ * press
413
+ * screenshot
414
+ * scroll_into_view_if_needed
415
+ * select_option
416
+ * select_text
417
+ * set_input_files
418
+ * tap_point
419
+ * text_content
420
+ * type
421
+ * uncheck
422
+
360
423
  ## Android
361
424
 
362
425
  * devices
data/lib/playwright.rb CHANGED
@@ -17,7 +17,6 @@ require 'playwright/utils'
17
17
  require 'playwright/api_implementation'
18
18
  require 'playwright/channel'
19
19
  require 'playwright/channel_owner'
20
- require 'playwright/download'
21
20
  require 'playwright/http_headers'
22
21
  require 'playwright/input_files'
23
22
  require 'playwright/connection'
@@ -59,7 +58,8 @@ module Playwright
59
58
  # and we *must* call execution.stop on the end.
60
59
  # The instance of playwright is available by calling execution.playwright
61
60
  module_function def create(playwright_cli_executable_path:, &block)
62
- connection = Connection.new(playwright_cli_executable_path: playwright_cli_executable_path)
61
+ transport = Transport.new(playwright_cli_executable_path: playwright_cli_executable_path)
62
+ connection = Connection.new(transport)
63
63
  connection.async_run
64
64
 
65
65
  execution =
@@ -82,7 +82,39 @@ module Playwright
82
82
  end
83
83
  end
84
84
 
85
- module_function def instance
86
- @playwright_instance
85
+ # Connects to Playwright server, launched by `npx playwright run-server` via WebSocket transport.
86
+ #
87
+ # Playwright.connect_to_playwright_server(...) do |playwright|
88
+ # browser = playwright.chromium.launch
89
+ # ...
90
+ # end
91
+ #
92
+ # @experimental
93
+ module_function def connect_to_playwright_server(ws_endpoint, &block)
94
+ require 'playwright/web_socket_client'
95
+ require 'playwright/web_socket_transport'
96
+
97
+ transport = WebSocketTransport.new(ws_endpoint: ws_endpoint)
98
+ connection = Connection.new(transport)
99
+ connection.async_run
100
+
101
+ execution =
102
+ begin
103
+ playwright = connection.wait_for_object_with_known_name('Playwright')
104
+ Execution.new(connection, PlaywrightApi.wrap(playwright))
105
+ rescue
106
+ connection.stop
107
+ raise
108
+ end
109
+
110
+ if block
111
+ begin
112
+ block.call(execution.playwright)
113
+ ensure
114
+ execution.stop
115
+ end
116
+ else
117
+ execution
118
+ end
87
119
  end
88
120
  end
@@ -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
@@ -26,5 +26,9 @@ module Playwright
26
26
  def delete
27
27
  @channel.send_message_to_server('delete')
28
28
  end
29
+
30
+ def cancel
31
+ @channel.send_message_to_server('cancel')
32
+ end
29
33
  end
30
34
  end
@@ -8,9 +8,11 @@ module Playwright
8
8
 
9
9
  private def after_initialize
10
10
  @pages = Set.new
11
- @routes = Set.new
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)
@@ -210,8 +237,8 @@ module Playwright
210
237
  end
211
238
 
212
239
  def route(url, handler)
213
- entry = RouteHandlerEntry.new(url, handler)
214
- @routes << entry
240
+ entry = RouteHandlerEntry.new(url, base_url, handler)
241
+ @routes.unshift(entry)
215
242
  if @routes.count >= 1
216
243
  @channel.send_message_to_server('setNetworkInterceptionEnabled', enabled: true)
217
244
  end
@@ -250,10 +277,45 @@ module Playwright
250
277
  raise unless safe_close_error?(err)
251
278
  end
252
279
 
280
+ # REMARK: enable_debug_console is playwright-ruby-client specific method.
281
+ def enable_debug_console!
282
+ # Ruby is not supported in Playwright officially,
283
+ # and causes error:
284
+ #
285
+ # Error:
286
+ # ===============================
287
+ # Unsupported language: 'ruby'
288
+ # ===============================
289
+ #
290
+ # So, launch inspector as Python app.
291
+ # NOTE: This should be used only for Page#pause at this moment.
292
+ @channel.send_message_to_server('recorderSupplementEnable', language: :python)
293
+ @debug_console_enabled = true
294
+ end
295
+
296
+ class DebugConsoleNotEnabledError < StandardError
297
+ def initialize
298
+ super('Debug console should be enabled in advance, by calling `browser_context.enable_debug_console!`')
299
+ end
300
+ end
301
+
253
302
  def pause
303
+ unless @debug_console_enabled
304
+ raise DebugConsoleNotEnabledError.new
305
+ end
254
306
  @channel.send_message_to_server('pause')
255
307
  end
256
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
+
257
319
  def expect_page(predicate: nil, timeout: nil)
258
320
  params = {
259
321
  predicate: predicate,
@@ -267,6 +329,14 @@ module Playwright
267
329
  @pages.delete(page)
268
330
  end
269
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
+
270
340
  # called from Page with send(:_timeout_settings), so keep private.
271
341
  private def _timeout_settings
272
342
  @timeout_settings
@@ -275,5 +345,9 @@ module Playwright
275
345
  private def has_record_video_option?
276
346
  @options.key?(:recordVideo)
277
347
  end
348
+
349
+ private def base_url
350
+ @options[:baseURL]
351
+ end
278
352
  end
279
353
  end