playwright-ruby-client 0.7.1 → 1.14.beta1

Sign up to get free protection for your applications and to get access to all the features.
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