playwright-ruby-client 0.0.8 → 1.58.1.alpha1

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 (209) hide show
  1. checksums.yaml +4 -4
  2. data/AGENTS.md +4 -0
  3. data/CLAUDE/api_generation.md +28 -0
  4. data/CLAUDE/ci_expectations.md +23 -0
  5. data/CLAUDE/gem_release_flow.md +39 -0
  6. data/CLAUDE/past_upgrade_pr_patterns.md +42 -0
  7. data/CLAUDE/playwright_upgrade_workflow.md +35 -0
  8. data/CLAUDE/rspec_debugging.md +30 -0
  9. data/CLAUDE/unimplemented_examples.md +18 -0
  10. data/CLAUDE.md +32 -0
  11. data/CONTRIBUTING.md +5 -0
  12. data/README.md +60 -16
  13. data/documentation/README.md +33 -0
  14. data/documentation/babel.config.js +3 -0
  15. data/documentation/docs/api/api_request.md +7 -0
  16. data/documentation/docs/api/api_request_context.md +298 -0
  17. data/documentation/docs/api/api_response.md +114 -0
  18. data/documentation/docs/api/browser.md +237 -0
  19. data/documentation/docs/api/browser_context.md +503 -0
  20. data/documentation/docs/api/browser_type.md +184 -0
  21. data/documentation/docs/api/cdp_session.md +44 -0
  22. data/documentation/docs/api/clock.md +154 -0
  23. data/documentation/docs/api/console_message.md +85 -0
  24. data/documentation/docs/api/dialog.md +84 -0
  25. data/documentation/docs/api/download.md +111 -0
  26. data/documentation/docs/api/element_handle.md +694 -0
  27. data/documentation/docs/api/experimental/_category_.yml +3 -0
  28. data/documentation/docs/api/experimental/android.md +42 -0
  29. data/documentation/docs/api/experimental/android_device.md +109 -0
  30. data/documentation/docs/api/experimental/android_input.md +43 -0
  31. data/documentation/docs/api/experimental/android_socket.md +7 -0
  32. data/documentation/docs/api/experimental/android_web_view.md +7 -0
  33. data/documentation/docs/api/file_chooser.md +53 -0
  34. data/documentation/docs/api/frame.md +1218 -0
  35. data/documentation/docs/api/frame_locator.md +348 -0
  36. data/documentation/docs/api/js_handle.md +121 -0
  37. data/documentation/docs/api/keyboard.md +170 -0
  38. data/documentation/docs/api/locator.md +1495 -0
  39. data/documentation/docs/api/locator_assertions.md +827 -0
  40. data/documentation/docs/api/mouse.md +86 -0
  41. data/documentation/docs/api/page.md +1946 -0
  42. data/documentation/docs/api/page_assertions.md +65 -0
  43. data/documentation/docs/api/playwright.md +66 -0
  44. data/documentation/docs/api/request.md +255 -0
  45. data/documentation/docs/api/response.md +176 -0
  46. data/documentation/docs/api/route.md +205 -0
  47. data/documentation/docs/api/selectors.md +63 -0
  48. data/documentation/docs/api/touchscreen.md +22 -0
  49. data/documentation/docs/api/tracing.md +129 -0
  50. data/documentation/docs/api/web_socket.md +51 -0
  51. data/documentation/docs/api/worker.md +83 -0
  52. data/documentation/docs/article/api_coverage.mdx +11 -0
  53. data/documentation/docs/article/getting_started.md +161 -0
  54. data/documentation/docs/article/guides/_category_.yml +3 -0
  55. data/documentation/docs/article/guides/download_playwright_driver.md +55 -0
  56. data/documentation/docs/article/guides/inspector.md +31 -0
  57. data/documentation/docs/article/guides/launch_browser.md +121 -0
  58. data/documentation/docs/article/guides/playwright_on_alpine_linux.md +112 -0
  59. data/documentation/docs/article/guides/rails_integration.md +278 -0
  60. data/documentation/docs/article/guides/rails_integration_with_null_driver.md +145 -0
  61. data/documentation/docs/article/guides/recording_video.md +79 -0
  62. data/documentation/docs/article/guides/rspec_integration.md +59 -0
  63. data/documentation/docs/article/guides/semi_automation.md +71 -0
  64. data/documentation/docs/article/guides/use_storage_state.md +78 -0
  65. data/documentation/docs/include/api_coverage.md +671 -0
  66. data/documentation/docusaurus.config.js +114 -0
  67. data/documentation/package.json +39 -0
  68. data/documentation/sidebars.js +15 -0
  69. data/documentation/src/components/HomepageFeatures.js +61 -0
  70. data/documentation/src/components/HomepageFeatures.module.css +13 -0
  71. data/documentation/src/css/custom.css +44 -0
  72. data/documentation/src/pages/index.js +49 -0
  73. data/documentation/src/pages/index.module.css +41 -0
  74. data/documentation/src/pages/markdown-page.md +7 -0
  75. data/documentation/static/.nojekyll +0 -0
  76. data/documentation/static/img/playwright-logo.svg +9 -0
  77. data/documentation/static/img/playwright-ruby-client.png +0 -0
  78. data/documentation/static/img/undraw_dropdown_menu.svg +1 -0
  79. data/documentation/static/img/undraw_web_development.svg +1 -0
  80. data/documentation/static/img/undraw_windows.svg +1 -0
  81. data/documentation/yarn.lock +9005 -0
  82. data/lib/playwright/{input_types/android_input.rb → android_input_impl.rb} +5 -1
  83. data/lib/playwright/api_implementation.rb +18 -0
  84. data/lib/playwright/api_response_impl.rb +77 -0
  85. data/lib/playwright/channel.rb +62 -1
  86. data/lib/playwright/channel_owner.rb +70 -7
  87. data/lib/playwright/channel_owners/android.rb +16 -3
  88. data/lib/playwright/channel_owners/android_device.rb +22 -66
  89. data/lib/playwright/channel_owners/api_request_context.rb +247 -0
  90. data/lib/playwright/channel_owners/artifact.rb +40 -0
  91. data/lib/playwright/channel_owners/binding_call.rb +70 -0
  92. data/lib/playwright/channel_owners/browser.rb +114 -22
  93. data/lib/playwright/channel_owners/browser_context.rb +589 -15
  94. data/lib/playwright/channel_owners/browser_type.rb +90 -1
  95. data/lib/playwright/channel_owners/cdp_session.rb +19 -0
  96. data/lib/playwright/channel_owners/dialog.rb +32 -0
  97. data/lib/playwright/channel_owners/element_handle.rb +107 -43
  98. data/lib/playwright/channel_owners/fetch_request.rb +8 -0
  99. data/lib/playwright/channel_owners/frame.rb +334 -104
  100. data/lib/playwright/channel_owners/js_handle.rb +9 -13
  101. data/lib/playwright/channel_owners/local_utils.rb +82 -0
  102. data/lib/playwright/channel_owners/page.rb +778 -95
  103. data/lib/playwright/channel_owners/playwright.rb +25 -30
  104. data/lib/playwright/channel_owners/request.rb +120 -18
  105. data/lib/playwright/channel_owners/response.rb +113 -0
  106. data/lib/playwright/channel_owners/route.rb +181 -0
  107. data/lib/playwright/channel_owners/stream.rb +30 -0
  108. data/lib/playwright/channel_owners/tracing.rb +117 -0
  109. data/lib/playwright/channel_owners/web_socket.rb +96 -0
  110. data/lib/playwright/channel_owners/worker.rb +46 -0
  111. data/lib/playwright/channel_owners/writable_stream.rb +14 -0
  112. data/lib/playwright/clock_impl.rb +67 -0
  113. data/lib/playwright/connection.rb +111 -63
  114. data/lib/playwright/console_message_impl.rb +29 -0
  115. data/lib/playwright/download_impl.rb +32 -0
  116. data/lib/playwright/errors.rb +42 -5
  117. data/lib/playwright/event_emitter.rb +17 -3
  118. data/lib/playwright/event_emitter_proxy.rb +49 -0
  119. data/lib/playwright/events.rb +10 -5
  120. data/lib/playwright/file_chooser_impl.rb +24 -0
  121. data/lib/playwright/frame_locator_impl.rb +66 -0
  122. data/lib/playwright/har_router.rb +89 -0
  123. data/lib/playwright/http_headers.rb +14 -0
  124. data/lib/playwright/input_files.rb +102 -15
  125. data/lib/playwright/javascript/expression.rb +7 -11
  126. data/lib/playwright/javascript/regex.rb +23 -0
  127. data/lib/playwright/javascript/source_url.rb +16 -0
  128. data/lib/playwright/javascript/value_parser.rb +108 -19
  129. data/lib/playwright/javascript/value_serializer.rb +47 -8
  130. data/lib/playwright/javascript/visitor_info.rb +26 -0
  131. data/lib/playwright/javascript.rb +2 -10
  132. data/lib/playwright/{input_types/keyboard.rb → keyboard_impl.rb} +6 -2
  133. data/lib/playwright/locator_assertions_impl.rb +571 -0
  134. data/lib/playwright/locator_impl.rb +544 -0
  135. data/lib/playwright/locator_utils.rb +136 -0
  136. data/lib/playwright/mouse_impl.rb +57 -0
  137. data/lib/playwright/page_assertions_impl.rb +154 -0
  138. data/lib/playwright/playwright_api.rb +102 -30
  139. data/lib/playwright/raw_headers.rb +61 -0
  140. data/lib/playwright/route_handler.rb +78 -0
  141. data/lib/playwright/select_option_values.rb +34 -13
  142. data/lib/playwright/selectors_impl.rb +45 -0
  143. data/lib/playwright/test.rb +102 -0
  144. data/lib/playwright/timeout_settings.rb +9 -4
  145. data/lib/playwright/touchscreen_impl.rb +14 -0
  146. data/lib/playwright/transport.rb +61 -10
  147. data/lib/playwright/url_matcher.rb +24 -2
  148. data/lib/playwright/utils.rb +48 -13
  149. data/lib/playwright/version.rb +2 -1
  150. data/lib/playwright/video.rb +54 -0
  151. data/lib/playwright/waiter.rb +166 -0
  152. data/lib/playwright/web_socket_client.rb +167 -0
  153. data/lib/playwright/web_socket_transport.rb +116 -0
  154. data/lib/playwright.rb +188 -11
  155. data/lib/playwright_api/android.rb +46 -11
  156. data/lib/playwright_api/android_device.rb +182 -31
  157. data/lib/playwright_api/android_input.rb +22 -13
  158. data/lib/playwright_api/android_socket.rb +18 -0
  159. data/lib/playwright_api/android_web_view.rb +24 -0
  160. data/lib/playwright_api/api_request.rb +26 -0
  161. data/lib/playwright_api/api_request_context.rb +311 -0
  162. data/lib/playwright_api/api_response.rb +92 -0
  163. data/lib/playwright_api/browser.rb +116 -103
  164. data/lib/playwright_api/browser_context.rb +290 -389
  165. data/lib/playwright_api/browser_type.rb +96 -118
  166. data/lib/playwright_api/cdp_session.rb +36 -39
  167. data/lib/playwright_api/clock.rb +121 -0
  168. data/lib/playwright_api/console_message.rb +35 -19
  169. data/lib/playwright_api/dialog.rb +53 -50
  170. data/lib/playwright_api/download.rb +49 -43
  171. data/lib/playwright_api/element_handle.rb +354 -402
  172. data/lib/playwright_api/file_chooser.rb +15 -18
  173. data/lib/playwright_api/frame.rb +703 -603
  174. data/lib/playwright_api/frame_locator.rb +285 -0
  175. data/lib/playwright_api/js_handle.rb +50 -76
  176. data/lib/playwright_api/keyboard.rb +67 -146
  177. data/lib/playwright_api/locator.rb +1304 -0
  178. data/lib/playwright_api/locator_assertions.rb +704 -0
  179. data/lib/playwright_api/mouse.rb +23 -29
  180. data/lib/playwright_api/page.rb +1196 -1176
  181. data/lib/playwright_api/page_assertions.rb +60 -0
  182. data/lib/playwright_api/playwright.rb +54 -122
  183. data/lib/playwright_api/request.rb +112 -74
  184. data/lib/playwright_api/response.rb +92 -20
  185. data/lib/playwright_api/route.rb +152 -62
  186. data/lib/playwright_api/selectors.rb +47 -61
  187. data/lib/playwright_api/touchscreen.rb +8 -2
  188. data/lib/playwright_api/tracing.rb +128 -0
  189. data/lib/playwright_api/web_socket.rb +43 -5
  190. data/lib/playwright_api/worker.rb +74 -34
  191. data/playwright.gemspec +14 -9
  192. data/sig/playwright.rbs +658 -0
  193. metadata +216 -50
  194. data/docs/api_coverage.md +0 -354
  195. data/lib/playwright/channel_owners/chromium_browser.rb +0 -8
  196. data/lib/playwright/channel_owners/chromium_browser_context.rb +0 -8
  197. data/lib/playwright/channel_owners/console_message.rb +0 -21
  198. data/lib/playwright/channel_owners/firefox_browser.rb +0 -8
  199. data/lib/playwright/channel_owners/selectors.rb +0 -4
  200. data/lib/playwright/channel_owners/webkit_browser.rb +0 -8
  201. data/lib/playwright/input_type.rb +0 -19
  202. data/lib/playwright/input_types/mouse.rb +0 -4
  203. data/lib/playwright/input_types/touchscreen.rb +0 -4
  204. data/lib/playwright/javascript/function.rb +0 -67
  205. data/lib/playwright/wait_helper.rb +0 -73
  206. data/lib/playwright_api/accessibility.rb +0 -93
  207. data/lib/playwright_api/binding_call.rb +0 -23
  208. data/lib/playwright_api/chromium_browser_context.rb +0 -57
  209. data/lib/playwright_api/video.rb +0 -24
@@ -0,0 +1,117 @@
1
+ module Playwright
2
+ define_channel_owner :Tracing do
3
+ def start(name: nil, title: nil, screenshots: nil, snapshots: nil, sources: nil)
4
+ params = {
5
+ name: name,
6
+ screenshots: screenshots,
7
+ snapshots: snapshots,
8
+ sources: sources,
9
+ }.compact
10
+ @include_sources = params[:sources] || false
11
+ @channel.send_message_to_server('tracingStart', params)
12
+ trace_name = @channel.send_message_to_server('tracingStartChunk', { title: title, name: name }.compact)
13
+ start_collecting_stacks(trace_name)
14
+ end
15
+
16
+ def start_chunk(title: nil, name: nil)
17
+ trace_name = @channel.send_message_to_server('tracingStartChunk', { title: title, name: name }.compact)
18
+ start_collecting_stacks(trace_name)
19
+ end
20
+
21
+ private def start_collecting_stacks(trace_name)
22
+ unless @is_tracing
23
+ @is_tracing = true
24
+ @connection.set_in_tracing(true)
25
+ end
26
+ local_utils = @connection.local_utils
27
+ @stacks_id = local_utils&.tracing_started(@traces_dir, trace_name)
28
+ end
29
+
30
+ def stop_chunk(path: nil)
31
+ do_stop_chunk(file_path: path)
32
+ end
33
+
34
+ def stop(path: nil)
35
+ do_stop_chunk(file_path: path)
36
+ @channel.send_message_to_server('tracingStop')
37
+ end
38
+
39
+ private def do_stop_chunk(file_path:)
40
+ if @is_tracing
41
+ @is_tracing = false
42
+ @connection.set_in_tracing(false)
43
+ end
44
+ local_utils = @connection.local_utils
45
+
46
+ unless file_path
47
+ # Not interested in any artifacts
48
+ @channel.send_message_to_server('tracingStopChunk', mode: 'discard')
49
+ if @stacks_id
50
+ local_utils.trace_discarded(@stacks_id) if local_utils
51
+ end
52
+
53
+ return
54
+ end
55
+
56
+ is_local = !@connection.remote?
57
+ if is_local
58
+ unless local_utils
59
+ raise 'Cannot save trace because localUtils is unavailable.'
60
+ end
61
+
62
+ result = @channel.send_message_to_server_result('tracingStopChunk', mode: 'entries')
63
+ local_utils.zip(
64
+ zipFile: file_path,
65
+ entries: result['entries'],
66
+ stacksId: @stacks_id,
67
+ mode: 'write',
68
+ includeSources: @include_sources,
69
+ )
70
+
71
+ return
72
+ end
73
+
74
+
75
+ result = @channel.send_message_to_server_result('tracingStopChunk', mode: 'archive')
76
+ # The artifact may be missing if the browser closed while stopping tracing.
77
+ unless result['artifact']
78
+ if @stacks_id
79
+ local_utils.trace_discarded(@stacks_id) if local_utils
80
+ end
81
+
82
+ return
83
+ end
84
+
85
+ # Save trace to the final local file.
86
+ artifact = ChannelOwners::Artifact.from(result['artifact'])
87
+ artifact.save_as(file_path)
88
+ artifact.delete
89
+
90
+ return unless local_utils
91
+
92
+ local_utils.zip(
93
+ zipFile: file_path,
94
+ entries: [],
95
+ stacksId: @stacks_id,
96
+ mode: 'append',
97
+ includeSources: @include_sources,
98
+ )
99
+ end
100
+
101
+ private def update_traces_dir(traces_dir)
102
+ @traces_dir = traces_dir
103
+ end
104
+
105
+ def group(name, location: nil)
106
+ params = {
107
+ name: name,
108
+ location: location,
109
+ }.compact
110
+ @channel.send_message_to_server('tracingGroup', params)
111
+ end
112
+
113
+ def group_end
114
+ @channel.send_message_to_server('tracingGroupEnd')
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,96 @@
1
+ require 'base64'
2
+
3
+ module Playwright
4
+ define_channel_owner :WebSocket do
5
+ private def after_initialize
6
+ @closed = false
7
+
8
+ @channel.on('frameSent', -> (params) {
9
+ on_frame_sent(params['opcode'], params['data'])
10
+ })
11
+ @channel.on('frameReceived', -> (params) {
12
+ on_frame_received(params['opcode'], params['data'])
13
+ })
14
+ @channel.on('socketError', -> (params) {
15
+ emit(Events::WebSocket::Error, params['error'])
16
+ })
17
+ @channel.on('close', -> (_) { on_close })
18
+ end
19
+
20
+ def url
21
+ @initializer['url']
22
+ end
23
+
24
+ class SocketClosedError < StandardError
25
+ def initialize
26
+ super('Socket closed')
27
+ end
28
+ end
29
+
30
+ class SocketError < StandardError
31
+ def initialize
32
+ super('Socket error')
33
+ end
34
+ end
35
+
36
+ def expect_event(event, predicate: nil, timeout: nil, &block)
37
+ waiter = Waiter.new(self, wait_name: "WebSocket.expect_event(#{event})")
38
+ timeout_value = timeout || @parent.send(:_timeout_settings).timeout
39
+ waiter.reject_on_timeout(timeout_value, "Timeout #{timeout_value}ms exceeded while waiting for event \"#{event}\"")
40
+
41
+ unless event == Events::WebSocket::Close
42
+ waiter.reject_on_event(self, Events::WebSocket::Close, SocketClosedError.new)
43
+ end
44
+
45
+ unless event == Events::WebSocket::Error
46
+ waiter.reject_on_event(self, Events::WebSocket::Error, SocketError.new)
47
+ end
48
+
49
+ waiter.reject_on_event(@parent, 'close', -> { @parent.send(:close_error_with_reason) })
50
+ waiter.wait_for_event(self, event, predicate: predicate)
51
+ if @closed
52
+ if event == Events::WebSocket::Close
53
+ waiter.force_fulfill(nil)
54
+ else
55
+ waiter.force_reject(SocketClosedError.new)
56
+ end
57
+ end
58
+ block&.call
59
+ if @closed
60
+ if event == Events::WebSocket::Close
61
+ waiter.force_fulfill(nil)
62
+ else
63
+ waiter.force_reject(SocketClosedError.new)
64
+ end
65
+ end
66
+
67
+ waiter.result.value!
68
+ end
69
+ alias_method :wait_for_event, :expect_event
70
+
71
+ private def on_frame_sent(opcode, data)
72
+ if opcode == 2
73
+ emit(Events::WebSocket::FrameSent, Base64.strict_decode64(data))
74
+ else
75
+ emit(Events::WebSocket::FrameSent, data)
76
+ end
77
+ end
78
+
79
+ private def on_frame_received(opcode, data)
80
+ if opcode == 2
81
+ emit(Events::WebSocket::FrameReceived, Base64.strict_decode64(data))
82
+ else
83
+ emit(Events::WebSocket::FrameReceived, data)
84
+ end
85
+ end
86
+
87
+ def closed?
88
+ @closed
89
+ end
90
+
91
+ private def on_close
92
+ @closed = true
93
+ emit(Events::WebSocket::Close)
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,46 @@
1
+ module Playwright
2
+ define_channel_owner :Worker do
3
+ attr_writer :context, :page
4
+
5
+ private def after_initialize
6
+ @channel.once('close', ->(_) { on_close })
7
+
8
+ set_event_to_subscription_mapping({
9
+ Events::Worker::Console => "console",
10
+ })
11
+ end
12
+
13
+ private def on_close
14
+ @page&.send(:remove_worker, self)
15
+ @context&.send(:remove_service_worker, self)
16
+ emit(Events::Worker::Close, self)
17
+ end
18
+
19
+ def url
20
+ @initializer['url']
21
+ end
22
+
23
+ def evaluate(expression, arg: nil)
24
+ JavaScript::Expression.new(expression, arg).evaluate(@channel)
25
+ end
26
+
27
+ def evaluate_handle(expression, arg: nil)
28
+ JavaScript::Expression.new(expression, arg).evaluate_handle(@channel)
29
+ end
30
+
31
+ def expect_event(event, predicate: nil, timeout: nil, &block)
32
+ waiter = Waiter.new(self, wait_name: "Worker.expect_event(#{event})")
33
+ timeout_value = timeout || @page&.send(:_timeout_settings)&.timeout || @context&.send(:_timeout_settings)&.timeout
34
+ waiter.reject_on_timeout(timeout_value, "Timeout #{timeout_value}ms exceeded while waiting for event \"#{event}\"")
35
+
36
+ unless event == Events::Worker::Close
37
+ waiter.reject_on_event(self, Events::Worker::Close, TargetClosedError.new)
38
+ end
39
+
40
+ waiter.wait_for_event(self, event, predicate: predicate)
41
+ block&.call
42
+
43
+ waiter.result.value!
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,14 @@
1
+ require 'base64'
2
+
3
+ module Playwright
4
+ define_channel_owner :WritableStream do
5
+ # @param readable [File|IO]
6
+ def write(readable, bufsize = 1048576)
7
+ while buf = readable.read(bufsize)
8
+ binary = Base64.strict_encode64(buf)
9
+ @channel.send_message_to_server('write', binary: binary)
10
+ end
11
+ @channel.send_message_to_server('close')
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,67 @@
1
+ module Playwright
2
+ define_api_implementation :ClockImpl do
3
+ def initialize(browser_context)
4
+ @browser_context = browser_context
5
+ end
6
+
7
+ def install(time: nil)
8
+ if time
9
+ @browser_context.send(:clock_install, parse_time(time))
10
+ else
11
+ @browser_context.send(:clock_install, {})
12
+ end
13
+ end
14
+
15
+ def fast_forward(ticks)
16
+ @browser_context.send(:clock_fast_forward, parse_ticks(ticks))
17
+ end
18
+
19
+ def pause_at(time)
20
+ @browser_context.send(:clock_pause_at, parse_time(time))
21
+ end
22
+
23
+ def resume
24
+ @browser_context.send(:clock_resume)
25
+ end
26
+
27
+ def run_for(ticks)
28
+ @browser_context.send(:clock_run_for, parse_ticks(ticks))
29
+ end
30
+
31
+ def set_fixed_time(time)
32
+ @browser_context.send(:clock_set_fixed_time, parse_time(time))
33
+ end
34
+
35
+ def set_system_time(time)
36
+ @browser_context.send(:clock_set_system_time, parse_time(time))
37
+ end
38
+
39
+ private def parse_time(time)
40
+ case time
41
+ when Integer
42
+ { timeNumber: time }
43
+ when String
44
+ { timeString: time }
45
+ when DateTime
46
+ { timeNumber: time.to_time.to_i * 1000 }
47
+ else
48
+ if time.respond_to?(:utc)
49
+ { timeNumber: time.utc.to_i * 1000 }
50
+ else
51
+ raise ArgumentError.new('time must be either integer, string or a Time object')
52
+ end
53
+ end
54
+ end
55
+
56
+ private def parse_ticks(ticks)
57
+ case ticks
58
+ when Integer
59
+ { ticksNumber: ticks }
60
+ when String
61
+ { ticksString: ticks }
62
+ else
63
+ raise ArgumentError.new('ticks must be either integer or string')
64
+ end
65
+ end
66
+ end
67
+ end
@@ -5,70 +5,124 @@ module Playwright
5
5
  # https://github.com/microsoft/playwright-python/blob/master/playwright/_impl/_connection.py
6
6
  # https://github.com/microsoft/playwright-java/blob/master/playwright/src/main/java/com/microsoft/playwright/impl/Connection.java
7
7
  class Connection
8
- def initialize(playwright_cli_executable_path:)
9
- @transport = Transport.new(
10
- playwright_cli_executable_path: playwright_cli_executable_path
11
- )
8
+ def initialize(transport)
9
+ @transport = transport
12
10
  @transport.on_message_received do |message|
13
11
  dispatch(message)
14
12
  end
13
+ @transport.on_driver_crashed do
14
+ callbacks = @callbacks_mutex.synchronize { @callbacks.values }
15
+ callbacks.each { |callback| callback.reject(::Playwright::DriverCrashedError.new) }
16
+ raise ::Playwright::DriverCrashedError.new
17
+ end
18
+ @transport.on_driver_closed do
19
+ cleanup
20
+ end
15
21
 
16
22
  @objects = {} # Hash[ guid => ChannelOwner ]
23
+ @objects_mutex = Mutex.new
17
24
  @waiting_for_object = {} # Hash[ guid => Promise<ChannelOwner> ]
18
25
  @callbacks = {} # Hash [ guid => Promise<ChannelOwner> ]
26
+ @callbacks_mutex = Mutex.new
19
27
  @root_object = RootChannelOwner.new(self)
28
+ @remote = false
29
+ @tracing_count = 0
30
+ @closed_error = nil
31
+ end
32
+
33
+ attr_reader :local_utils
34
+
35
+ def mark_as_remote
36
+ @remote = true
20
37
  end
21
38
 
22
- def run
23
- @transport.run
39
+ def remote?
40
+ @remote
41
+ end
42
+
43
+ def async_run
44
+ @transport.async_run
24
45
  end
25
46
 
26
47
  def stop
27
48
  @transport.stop
49
+ cleanup
28
50
  end
29
51
 
30
- def async_wait_for_object_with_known_name(guid)
31
- if @objects[guid]
32
- return @objects[guid]
52
+ def cleanup(cause: nil)
53
+ @closed_error = TargetClosedError.new(message: cause)
54
+ callbacks = @callbacks_mutex.synchronize do
55
+ @callbacks.values.tap { @callbacks.clear }
33
56
  end
57
+ callbacks.each { |callback| callback.reject(@closed_error) }
58
+ end
34
59
 
35
- callback = Concurrent::Promises.resolvable_future
36
- @waiting_for_object[guid] = callback
37
- callback
60
+ def initialize_playwright
61
+ # Avoid Error: sdkLanguage: expected one of (javascript|python|java|csharp)
62
+ # ref: https://github.com/microsoft/playwright/pull/18308
63
+ # ref: https://github.com/YusukeIwaki/playwright-ruby-client/issues/228
64
+ result = send_message_to_server('', 'initialize', { sdkLanguage: 'python' })
65
+ ChannelOwners::Playwright.from(result['playwright'])
38
66
  end
39
67
 
40
- def wait_for_object_with_known_name(guid)
41
- async_wait_for_object_with_known_name.value!
68
+ def set_in_tracing(value)
69
+ if value
70
+ @tracing_count += 1
71
+ else
72
+ @tracing_count -= 1
73
+ end
42
74
  end
43
75
 
44
- def async_send_message_to_server(guid, method, params)
76
+ def async_send_message_to_server(guid, method, params, metadata: nil)
77
+ return if @closed_error
78
+
45
79
  callback = Concurrent::Promises.resolvable_future
46
80
 
47
81
  with_generated_id do |id|
48
82
  # register callback promise object first.
49
83
  # @see https://github.com/YusukeIwaki/puppeteer-ruby/pull/34
50
- @callbacks[id] = callback
84
+ @callbacks_mutex.synchronize { @callbacks[id] = callback }
85
+
86
+ _metadata = {}
87
+ frames = []
88
+ if metadata
89
+ frames = metadata[:stack]
90
+ _metadata[:wallTime] = metadata[:wallTime]
91
+ _metadata[:apiName] = metadata[:apiName]
92
+ _metadata[:location] = metadata[:stack].first
93
+ _metadata[:internal] = !metadata[:apiName]
94
+ if metadata[:title]
95
+ _metadata[:title] = metadata[:title]
96
+ end
97
+ end
98
+ _metadata.compact!
51
99
 
52
100
  message = {
53
101
  id: id,
54
102
  guid: guid,
55
103
  method: method,
56
104
  params: replace_channels_with_guids(params),
105
+ metadata: _metadata,
57
106
  }
107
+
58
108
  begin
59
109
  @transport.send_message(message)
60
110
  rescue => err
61
- @callbacks.delete(id)
111
+ @callbacks_mutex.synchronize { @callbacks.delete(id) }
62
112
  callback.reject(err)
63
113
  raise unless err.is_a?(Transport::AlreadyDisconnectedError)
64
114
  end
115
+
116
+ if @tracing_count > 0 && !frames.empty? && guid != 'localUtils' && !remote?
117
+ @local_utils.add_stack_to_tracing_no_reply(id, frames)
118
+ end
65
119
  end
66
120
 
67
121
  callback
68
122
  end
69
123
 
70
- def send_message_to_server(guid, method, params)
71
- async_send_message_to_server(guid, method, params).value!
124
+ def send_message_to_server(guid, method, params, metadata: nil)
125
+ async_send_message_to_server(guid, method, params, metadata: metadata).value!
72
126
  end
73
127
 
74
128
  private
@@ -79,35 +133,42 @@ module Playwright
79
133
  # end
80
134
  # ````
81
135
  def with_generated_id(&block)
82
- @last_id ||= 0
83
- block.call(@last_id += 1)
136
+ id = @callbacks_mutex.synchronize do
137
+ @last_id ||= 0
138
+ @last_id += 1
139
+ end
140
+ block.call(id)
84
141
  end
85
142
 
86
143
  # @param guid [String]
87
144
  # @param parent [Playwright::ChannelOwner]
88
145
  # @note This method should be used internally. Accessed via .send method from Playwright::ChannelOwner, so keep private!
89
146
  def update_object_from_channel_owner(guid, parent)
90
- @objects[guid] = parent
147
+ @objects_mutex.synchronize { @objects[guid] = parent }
91
148
  end
92
149
 
93
150
  # @param guid [String]
94
151
  # @note This method should be used internally. Accessed via .send method from Playwright::ChannelOwner, so keep private!
95
152
  def delete_object_from_channel_owner(guid)
96
- @objects.delete(guid)
153
+ @objects_mutex.synchronize { @objects.delete(guid) }
97
154
  end
98
155
 
99
156
  def dispatch(msg)
157
+ return if @closed_error
158
+
100
159
  id = msg['id']
101
160
  if id
102
- callback = @callbacks.delete(id)
161
+ callback = @callbacks_mutex.synchronize { @callbacks.delete(id) }
103
162
 
104
163
  unless callback
105
164
  raise "Cannot find command to respond: #{id}"
106
165
  end
107
166
 
108
167
  error = msg['error']
109
- if error
110
- callback.reject(::Playwright::Error.parse(error['error']))
168
+ if error && !msg['result']
169
+ parsed_error = ::Playwright::Error.parse(error['error'])
170
+ parsed_error.log = msg['log']
171
+ callback.reject(parsed_error)
111
172
  else
112
173
  result = replace_guids_with_channels(msg['result'])
113
174
  callback.fulfill(result)
@@ -121,28 +182,37 @@ module Playwright
121
182
  params = msg['params']
122
183
 
123
184
  if method == "__create__"
124
- create_remote_object(
185
+ remote_object = create_remote_object(
125
186
  parent_guid: guid,
126
187
  type: params["type"],
127
188
  guid: params["guid"],
128
189
  initializer: params["initializer"],
129
190
  )
191
+ if remote_object.is_a?(ChannelOwners::LocalUtils)
192
+ @local_utils = remote_object
193
+ end
130
194
  return
131
195
  end
132
196
 
133
- if method == "__dispose__"
134
- object = @objects[guid]
135
- unless object
136
- raise "Cannot find object to dispose: #{guid}"
197
+ object = @objects_mutex.synchronize { @objects[guid] }
198
+ unless object
199
+ raise "Cannot find object to \"#{method}\": #{guid}"
200
+ end
201
+
202
+ if method == "__adopt__"
203
+ child = @objects_mutex.synchronize { @objects[params["guid"]] }
204
+ unless child
205
+ raise "Unknown new child: #{params['guid']}"
137
206
  end
138
- object.send(:dispose!)
207
+ object.send(:adopt!, child)
139
208
  return
140
209
  end
141
210
 
142
- object = @objects[guid]
143
- unless object
144
- raise "Cannot find object to emit \"#{method}\": #{guid}"
211
+ if method == "__dispose__"
212
+ object.send(:dispose!, reason: params["reason"])
213
+ return
145
214
  end
215
+
146
216
  object.channel.emit(method, replace_guids_with_channels(params))
147
217
  end
148
218
 
@@ -177,8 +247,9 @@ module Playwright
177
247
 
178
248
  if payload.is_a?(Hash)
179
249
  guid = payload['guid']
180
- if guid && @objects[guid]
181
- return @objects[guid].channel
250
+ if guid
251
+ object = @objects_mutex.synchronize { @objects[guid] }
252
+ return object.channel if object
182
253
  end
183
254
 
184
255
  return payload.map { |k, v| [k, replace_guids_with_channels(v)] }.to_h
@@ -189,38 +260,15 @@ module Playwright
189
260
 
190
261
  # @return [Playwright::ChannelOwner|nil]
191
262
  def create_remote_object(parent_guid:, type:, guid:, initializer:)
192
- parent = @objects[parent_guid]
263
+ parent = @objects_mutex.synchronize { @objects[parent_guid] }
193
264
  unless parent
194
265
  raise "Cannot find parent object #{parent_guid} to create #{guid}"
195
266
  end
196
267
  initializer = replace_guids_with_channels(initializer)
197
268
 
198
- class_name = case type
199
- when 'Browser'
200
- case initializer['name']
201
- when 'chromium'
202
- 'ChromiumBrowser'
203
- when 'webkit'
204
- 'WebKitBrowser'
205
- when 'firefox'
206
- 'FirefoxBrowser'
207
- else
208
- 'Browser'
209
- end
210
- when 'BrowserContext'
211
- browser_name = initializer['browserName']
212
- if browser_name == 'chromium'
213
- 'ChromiumBrowserContext'
214
- else
215
- 'BrowserContext'
216
- end
217
- else
218
- type
219
- end
220
-
221
269
  result =
222
270
  begin
223
- ChannelOwners.const_get(class_name).new(
271
+ ChannelOwners.const_get(type).new(
224
272
  parent,
225
273
  type,
226
274
  guid,
@@ -230,7 +278,7 @@ module Playwright
230
278
  raise "Missing type #{type}"
231
279
  end
232
280
 
233
- callback = @waiting_for_object.delete(guid)
281
+ callback = @objects_mutex.synchronize { @waiting_for_object.delete(guid) }
234
282
  callback&.fulfill(result)
235
283
 
236
284
  result
@@ -0,0 +1,29 @@
1
+ module Playwright
2
+ define_api_implementation :ConsoleMessageImpl do
3
+ def initialize(event, page, worker)
4
+ @event = event
5
+ @page = page
6
+ @worker = worker
7
+ end
8
+
9
+ attr_reader :page, :worker
10
+
11
+ def type
12
+ @event['type']
13
+ end
14
+
15
+ def text
16
+ @event['text']
17
+ end
18
+
19
+ def args
20
+ @event['args']&.map do |arg|
21
+ ChannelOwner.from(arg)
22
+ end
23
+ end
24
+
25
+ def location
26
+ @event['location']
27
+ end
28
+ end
29
+ end