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,544 @@
1
+ require 'json'
2
+ require_relative './locator_utils'
3
+
4
+ module Playwright
5
+ define_api_implementation :LocatorImpl do
6
+ include LocatorUtils
7
+
8
+ def initialize(frame:, selector:, has: nil, hasNot: nil, hasNotText: nil, hasText: nil, visible: nil)
9
+ @frame = frame
10
+ selector_scopes = [selector]
11
+
12
+ if hasText
13
+ selector_scopes << "internal:has-text=#{escape_for_text_selector(hasText, false)}"
14
+ end
15
+
16
+ if has
17
+ unless same_frame?(has)
18
+ raise DifferentFrameError.new('has')
19
+ end
20
+ selector_scopes << "internal:has=#{has.send(:selector_json)}"
21
+ end
22
+
23
+ if hasNotText
24
+ selector_scopes << "internal:has-not-text=#{escape_for_text_selector(hasNotText, false)}"
25
+ end
26
+
27
+ if hasNot
28
+ unless same_frame?(hasNot)
29
+ raise DifferentFrameError.new('hasNot')
30
+ end
31
+ selector_scopes << "internal:has-not=#{hasNot.send(:selector_json)}"
32
+ end
33
+
34
+ if [true, false].include?(visible)
35
+ selector_scopes << "visible=#{visible}"
36
+ end
37
+
38
+ @selector = selector_scopes.join(' >> ')
39
+ end
40
+
41
+ def to_s
42
+ if @description
43
+ "Locator@#{@selector} (#{@description})"
44
+ else
45
+ "Locator@#{@selector}"
46
+ end
47
+ end
48
+
49
+ private def to_protocol
50
+ {
51
+ frame: @frame.channel,
52
+ selector: @selector,
53
+ }
54
+ end
55
+
56
+ class DifferentFrameError < StandardError
57
+ def initialize(method_name)
58
+ super("Inner \"#{method_name}\" locator must belong to the same frame.")
59
+ end
60
+ end
61
+
62
+ private def same_frame?(other)
63
+ @frame == other.instance_variable_get(:@frame)
64
+ end
65
+
66
+ private def selector_json
67
+ @selector.to_json
68
+ end
69
+
70
+ private def _timeout(timeout)
71
+ @frame.send(:_timeout, timeout)
72
+ end
73
+
74
+ private def with_element(timeout: nil, &block)
75
+ timeout_or_default = _timeout(timeout)
76
+ start_time = Time.now
77
+
78
+ handle = @frame.wait_for_selector(@selector, strict: true, state: 'attached', timeout: timeout_or_default)
79
+ unless handle
80
+ raise "Could not resolve #{@selector} to DOM Element"
81
+ end
82
+
83
+ call_options = {
84
+ timeout: (timeout_or_default - (Time.now - start_time) * 1000).to_i,
85
+ }
86
+
87
+ begin
88
+ block.call(handle, call_options)
89
+ ensure
90
+ handle.dispose
91
+ end
92
+ end
93
+
94
+ def page
95
+ @frame.page
96
+ end
97
+
98
+ def bounding_box(timeout: nil)
99
+ with_element(timeout: timeout) do |handle|
100
+ handle.bounding_box
101
+ end
102
+ end
103
+
104
+ def check(
105
+ force: nil,
106
+ noWaitAfter: nil,
107
+ position: nil,
108
+ timeout: nil,
109
+ trial: nil)
110
+
111
+ @frame.check(@selector,
112
+ strict: true,
113
+ force: force,
114
+ noWaitAfter: noWaitAfter,
115
+ position: position,
116
+ timeout: timeout,
117
+ trial: trial)
118
+ end
119
+
120
+ def click(
121
+ button: nil,
122
+ clickCount: nil,
123
+ delay: nil,
124
+ force: nil,
125
+ modifiers: nil,
126
+ noWaitAfter: nil,
127
+ position: nil,
128
+ timeout: nil,
129
+ trial: nil,
130
+ steps: nil)
131
+
132
+ @frame.click(@selector,
133
+ strict: true,
134
+ button: button,
135
+ clickCount: clickCount,
136
+ delay: delay,
137
+ force: force,
138
+ modifiers: modifiers,
139
+ noWaitAfter: noWaitAfter,
140
+ position: position,
141
+ timeout: timeout,
142
+ trial: trial,
143
+ steps: steps)
144
+ end
145
+
146
+ def dblclick(
147
+ button: nil,
148
+ delay: nil,
149
+ force: nil,
150
+ modifiers: nil,
151
+ noWaitAfter: nil,
152
+ position: nil,
153
+ timeout: nil,
154
+ trial: nil,
155
+ steps: nil)
156
+
157
+ @frame.dblclick(@selector,
158
+ strict: true,
159
+ button: button,
160
+ delay: delay,
161
+ force: force,
162
+ modifiers: modifiers,
163
+ noWaitAfter: noWaitAfter,
164
+ position: position,
165
+ timeout: timeout,
166
+ trial: trial,
167
+ steps: steps)
168
+ end
169
+
170
+ def dispatch_event(type, eventInit: nil, timeout: nil)
171
+ @frame.dispatch_event(@selector, type, strict: true, eventInit: eventInit, timeout: timeout)
172
+ end
173
+
174
+ def drag_to(target,
175
+ force: nil,
176
+ noWaitAfter: nil,
177
+ sourcePosition: nil,
178
+ targetPosition: nil,
179
+ timeout: nil,
180
+ trial: nil,
181
+ steps: nil)
182
+
183
+ @frame.drag_and_drop(
184
+ @selector,
185
+ target.instance_variable_get(:@selector),
186
+ force: force,
187
+ noWaitAfter: noWaitAfter,
188
+ sourcePosition: sourcePosition,
189
+ targetPosition: targetPosition,
190
+ timeout: timeout,
191
+ trial: trial,
192
+ strict: true,
193
+ steps: steps,
194
+ )
195
+ end
196
+
197
+ def evaluate(expression, arg: nil, timeout: nil)
198
+ with_element(timeout: timeout) do |handle|
199
+ handle.evaluate(expression, arg: arg)
200
+ end
201
+ end
202
+
203
+ def evaluate_all(expression, arg: nil)
204
+ @frame.eval_on_selector_all(@selector, expression, arg: arg)
205
+ end
206
+
207
+ def evaluate_handle(expression, arg: nil, timeout: nil)
208
+ with_element(timeout: timeout) do |handle|
209
+ handle.evaluate_handle(expression, arg: arg)
210
+ end
211
+ end
212
+
213
+ def fill(value, force: nil, noWaitAfter: nil, timeout: nil)
214
+ @frame.fill(@selector, value, strict: true, force: force, noWaitAfter: noWaitAfter, timeout: timeout)
215
+ end
216
+
217
+ def clear(force: nil, noWaitAfter: nil, timeout: nil)
218
+ @frame.fill(@selector, '', strict: true, force: force, noWaitAfter: noWaitAfter, timeout: timeout)
219
+ end
220
+
221
+ def locator(
222
+ selector,
223
+ has: nil,
224
+ hasNot: nil,
225
+ hasNotText: nil,
226
+ hasText: nil)
227
+ LocatorImpl.new(
228
+ frame: @frame,
229
+ selector: "#{@selector} >> #{selector}",
230
+ has: has,
231
+ hasNot: hasNot,
232
+ hasNotText: hasNotText,
233
+ hasText: hasText)
234
+ end
235
+
236
+ def frame_locator(selector)
237
+ FrameLocatorImpl.new(
238
+ frame: @frame,
239
+ frame_selector: "#{@selector} >> #{selector}",
240
+ )
241
+ end
242
+
243
+ def element_handle(timeout: nil)
244
+ @frame.wait_for_selector(@selector, strict: true, state: 'attached', timeout: timeout)
245
+ end
246
+
247
+ def element_handles
248
+ @frame.query_selector_all(@selector)
249
+ end
250
+
251
+ def content_frame
252
+ FrameLocatorImpl.new(
253
+ frame: @frame,
254
+ frame_selector: @selector,
255
+ )
256
+ end
257
+
258
+ def describe(description)
259
+ LocatorImpl.new(
260
+ frame: @frame,
261
+ selector: "#{@selector} >> internal:describe=#{description.to_json}",
262
+ )
263
+ end
264
+
265
+ def description
266
+ if @selector =~ / >> internal:describe=("(?:[^"\\]|\\.)*")$/
267
+ begin
268
+ description = JSON.parse($1)
269
+ if description.is_a?(String)
270
+ return description
271
+ end
272
+ rescue JSON::ParserError
273
+ end
274
+ end
275
+
276
+ nil
277
+ end
278
+
279
+ def filter(has: nil, hasNot: nil, hasNotText: nil, hasText: nil, visible: nil)
280
+ LocatorImpl.new(
281
+ frame: @frame,
282
+ selector: @selector,
283
+ has: has,
284
+ hasNot: hasNot,
285
+ hasNotText: hasNotText,
286
+ hasText: hasText,
287
+ visible: visible,
288
+ )
289
+ end
290
+
291
+ def first
292
+ LocatorImpl.new(
293
+ frame: @frame,
294
+ selector: "#{@selector} >> nth=0",
295
+ )
296
+ end
297
+
298
+ def last
299
+ LocatorImpl.new(
300
+ frame: @frame,
301
+ selector: "#{@selector} >> nth=-1",
302
+ )
303
+ end
304
+
305
+ def nth(index)
306
+ LocatorImpl.new(
307
+ frame: @frame,
308
+ selector: "#{@selector} >> nth=#{index}",
309
+ )
310
+ end
311
+
312
+ def and(locator)
313
+ unless same_frame?(locator)
314
+ raise DifferentFrameError.new('and')
315
+ end
316
+ LocatorImpl.new(
317
+ frame: @frame,
318
+ selector: "#{@selector} >> internal:and=#{locator.send(:selector_json)}",
319
+ )
320
+ end
321
+
322
+ def or(locator)
323
+ unless same_frame?(locator)
324
+ raise DifferentFrameError.new('or')
325
+ end
326
+ LocatorImpl.new(
327
+ frame: @frame,
328
+ selector: "#{@selector} >> internal:or=#{locator.send(:selector_json)}",
329
+ )
330
+ end
331
+
332
+ def focus(timeout: nil)
333
+ @frame.focus(@selector, strict: true, timeout: timeout)
334
+ end
335
+
336
+ def blur(timeout: nil)
337
+ params = {
338
+ selector: @selector,
339
+ strict: true,
340
+ timeout: _timeout(timeout),
341
+ }.compact
342
+ @frame.channel.send_message_to_server('blur', params)
343
+ end
344
+
345
+ def all
346
+ count.times.map { |i| nth(i) }
347
+ end
348
+
349
+ def count
350
+ @frame.eval_on_selector_all(@selector, 'ee => ee.length')
351
+ end
352
+
353
+ def get_attribute(name, timeout: nil)
354
+ @frame.get_attribute(@selector, name, strict: true, timeout: timeout)
355
+ end
356
+
357
+ def resolve_selector
358
+ @frame.channel.send_message_to_server('resolveSelector', { selector: @selector })
359
+ end
360
+
361
+ def hover(
362
+ force: nil,
363
+ modifiers: nil,
364
+ noWaitAfter: nil,
365
+ position: nil,
366
+ timeout: nil,
367
+ trial: nil)
368
+ @frame.hover(@selector,
369
+ strict: true,
370
+ force: force,
371
+ modifiers: modifiers,
372
+ noWaitAfter: noWaitAfter,
373
+ position: position,
374
+ timeout: timeout,
375
+ trial: trial)
376
+ end
377
+
378
+ def inner_html(timeout: nil)
379
+ @frame.inner_html(@selector, strict: true, timeout: timeout)
380
+ end
381
+
382
+ def inner_text(timeout: nil)
383
+ @frame.inner_text(@selector, strict: true, timeout: timeout)
384
+ end
385
+
386
+ def input_value(timeout: nil)
387
+ @frame.input_value(@selector, strict: true, timeout: timeout)
388
+ end
389
+
390
+ %i[checked? disabled? editable? enabled? hidden? visible?].each do |method_name|
391
+ define_method(method_name) do |timeout: nil|
392
+ @frame.public_send(method_name, @selector, strict: true, timeout: timeout)
393
+ end
394
+ end
395
+
396
+ def press(key, delay: nil, noWaitAfter: nil, timeout: nil)
397
+ @frame.press(@selector, key, strict: true, noWaitAfter: noWaitAfter, timeout: timeout)
398
+ end
399
+
400
+ def screenshot(
401
+ animations: nil,
402
+ caret: nil,
403
+ mask: nil,
404
+ maskColor: nil,
405
+ omitBackground: nil,
406
+ path: nil,
407
+ quality: nil,
408
+ scale: nil,
409
+ style: nil,
410
+ timeout: nil,
411
+ type: nil)
412
+
413
+ with_element(timeout: timeout) do |handle, options|
414
+ handle.screenshot(
415
+ animations: animations,
416
+ caret: caret,
417
+ mask: mask,
418
+ maskColor: maskColor,
419
+ omitBackground: omitBackground,
420
+ path: path,
421
+ quality: quality,
422
+ scale: scale,
423
+ style: style,
424
+ timeout: options[:timeout],
425
+ type: type)
426
+ end
427
+ end
428
+
429
+ def aria_snapshot(timeout: nil)
430
+ @frame.channel.send_message_to_server('ariaSnapshot', {
431
+ selector: @selector,
432
+ timeout: _timeout(timeout),
433
+ }.compact)
434
+ end
435
+
436
+ def scroll_into_view_if_needed(timeout: nil)
437
+ with_element(timeout: timeout) do |handle, options|
438
+ handle.scroll_into_view_if_needed(timeout: options[:timeout])
439
+ end
440
+ end
441
+
442
+ def select_option(
443
+ element: nil,
444
+ index: nil,
445
+ value: nil,
446
+ label: nil,
447
+ force: nil,
448
+ noWaitAfter: nil,
449
+ timeout: nil)
450
+
451
+ @frame.select_option(@selector,
452
+ strict: true,
453
+ element: element,
454
+ index: index,
455
+ value: value,
456
+ label: label,
457
+ force: force,
458
+ noWaitAfter: noWaitAfter,
459
+ timeout: timeout)
460
+ end
461
+
462
+ def select_text(force: nil, timeout: nil)
463
+ with_element(timeout: timeout) do |handle, options|
464
+ handle.select_text(force: force, timeout: options[:timeout])
465
+ end
466
+ end
467
+
468
+ def set_input_files(files, noWaitAfter: nil, timeout: nil)
469
+ @frame.set_input_files(@selector, files, strict: true, noWaitAfter: noWaitAfter, timeout: timeout)
470
+ end
471
+
472
+ def tap_point(
473
+ force: nil,
474
+ modifiers: nil,
475
+ noWaitAfter: nil,
476
+ position: nil,
477
+ timeout: nil,
478
+ trial: nil)
479
+ @frame.tap_point(@selector,
480
+ strict: true,
481
+ force: force,
482
+ modifiers: modifiers,
483
+ noWaitAfter: noWaitAfter,
484
+ position: position,
485
+ timeout: timeout,
486
+ trial: trial)
487
+ end
488
+
489
+ def text_content(timeout: nil)
490
+ @frame.text_content(@selector, strict: true, timeout: timeout)
491
+ end
492
+
493
+ def type(text, delay: nil, noWaitAfter: nil, timeout: nil)
494
+ @frame.type(@selector, text, strict: true, delay: delay, noWaitAfter: noWaitAfter, timeout: timeout)
495
+ end
496
+
497
+ def press_sequentially(text, delay: nil, noWaitAfter: nil, timeout: nil)
498
+ type(text, delay: delay, noWaitAfter: noWaitAfter, timeout: timeout)
499
+ end
500
+
501
+ def uncheck(
502
+ force: nil,
503
+ noWaitAfter: nil,
504
+ position: nil,
505
+ timeout: nil,
506
+ trial: nil)
507
+ @frame.uncheck(@selector,
508
+ strict: true,
509
+ force: force,
510
+ noWaitAfter: noWaitAfter,
511
+ position: position,
512
+ timeout: timeout,
513
+ trial: trial)
514
+ end
515
+
516
+ def wait_for(state: nil, timeout: nil)
517
+ @frame.wait_for_selector(@selector, strict: true, state: state, timeout: timeout)
518
+ end
519
+
520
+ def set_checked(checked, **options)
521
+ if checked
522
+ check(**options)
523
+ else
524
+ uncheck(**options)
525
+ end
526
+ end
527
+
528
+ def all_inner_texts
529
+ @frame.eval_on_selector_all(@selector, 'ee => ee.map(e => e.innerText)')
530
+ end
531
+
532
+ def all_text_contents
533
+ @frame.eval_on_selector_all(@selector, "ee => ee.map(e => e.textContent || '')")
534
+ end
535
+
536
+ def highlight
537
+ @frame.highlight(@selector)
538
+ end
539
+
540
+ def expect(expression, options, title)
541
+ @frame.expect(@selector, expression, options, title)
542
+ end
543
+ end
544
+ end
@@ -0,0 +1,136 @@
1
+ require 'json'
2
+
3
+ module Playwright
4
+ module LocatorUtils
5
+ def get_by_test_id(test_id)
6
+ test_id_attribute_name = ::Playwright::LocatorUtils.instance_variable_get(:@test_id_attribute_name)
7
+ locator(get_by_test_id_selector(test_id_attribute_name, test_id))
8
+ end
9
+
10
+ def get_by_alt_text(text, exact: false)
11
+ locator(get_by_alt_text_selector(text, exact: exact))
12
+ end
13
+
14
+ def get_by_label(text, exact: false)
15
+ locator(get_by_label_selector(text, exact: exact))
16
+ end
17
+
18
+ def get_by_placeholder(text, exact: false)
19
+ locator(get_by_placeholder_selector(text, exact: exact))
20
+ end
21
+
22
+ def get_by_text(text, exact: false)
23
+ locator(get_by_text_selector(text, exact: exact))
24
+ end
25
+
26
+ def get_by_title(text, exact: false)
27
+ locator(get_by_title_selector(text, exact: exact))
28
+ end
29
+
30
+ def get_by_role(role, **options)
31
+ locator(get_by_role_selector(role, **(options.compact)))
32
+ end
33
+
34
+ # set from Playwright::Selectors#test_id_attribute=
35
+ @test_id_attribute_name = 'data-testid'
36
+
37
+ private def get_by_attribute_text_selector(attr_name, text, exact: false)
38
+ "internal:attr=[#{attr_name}=#{escape_for_attribute_selector_or_regex(text, exact)}]"
39
+ end
40
+
41
+ private def get_by_test_id_selector(test_id_attribute_name, test_id)
42
+ "internal:testid=[#{test_id_attribute_name}=#{escape_for_attribute_selector_or_regex(test_id, true)}]"
43
+ end
44
+
45
+ private def get_by_label_selector(text, exact:)
46
+ "internal:label=#{escape_for_text_selector(text, exact)}"
47
+ end
48
+
49
+ private def get_by_alt_text_selector(text, exact:)
50
+ get_by_attribute_text_selector('alt', text, exact: exact)
51
+ end
52
+
53
+ private def get_by_title_selector(text, exact:)
54
+ get_by_attribute_text_selector('title', text, exact: exact)
55
+ end
56
+
57
+ private def get_by_placeholder_selector(text, exact:)
58
+ get_by_attribute_text_selector('placeholder', text, exact: exact)
59
+ end
60
+
61
+ private def get_by_text_selector(text, exact:)
62
+ "internal:text=#{escape_for_text_selector(text, exact)}"
63
+ end
64
+
65
+ private def get_by_role_selector(role, **options)
66
+ props = []
67
+
68
+ ex = {
69
+ includeHidden: -> (value) { ['include-hidden', value.to_s] },
70
+ name: -> (value) { ['name', escape_for_attribute_selector_or_regex(value, options[:exact])]},
71
+ }
72
+
73
+ %i[
74
+ checked
75
+ disabled
76
+ selected
77
+ expanded
78
+ includeHidden
79
+ level
80
+ name
81
+ pressed
82
+ ].each do |attr_name|
83
+ if options.key?(attr_name)
84
+ attr_value = options[attr_name]
85
+ props << (ex[attr_name]&.call(attr_value) || [attr_name, attr_value.to_s])
86
+ end
87
+ end
88
+
89
+ opts = props.map { |k, v| "[#{k}=#{v}]"}.join('')
90
+ "internal:role=#{role}#{opts}"
91
+ end
92
+
93
+ # @param text [String]
94
+ private def escape_for_regex(text)
95
+ text.gsub(/[.*+?^>${}()|\[\]\\]/) { "\\#{$&}" }
96
+ end
97
+
98
+ # @param text [Regexp|String]
99
+ private def escape_for_text_selector(text, exact)
100
+ if text.is_a?(Regexp)
101
+ regex = JavaScript::Regex.new(text)
102
+ return "/#{regex.source}/#{regex.flag}"
103
+ end
104
+
105
+ if exact
106
+ "#{text.to_json}s"
107
+ else
108
+ "#{text.to_json}i"
109
+ end
110
+ end
111
+
112
+ # @param text [Regexp|String]
113
+ private def escape_for_attribute_selector_or_regex(text, exact)
114
+ if text.is_a?(Regexp)
115
+ regex = JavaScript::Regex.new(text)
116
+ "/#{regex.source}/#{regex.flag}"
117
+ else
118
+ escape_for_attribute_selector(text, exact)
119
+ end
120
+ end
121
+
122
+ # @param text [String]
123
+ private def escape_for_attribute_selector(text, exact)
124
+ # TODO: this should actually be
125
+ # cssEscape(value).replace(/\\ /g, ' ')
126
+ # However, our attribute selectors do not conform to CSS parsing spec,
127
+ # so we escape them differently.
128
+ _text = text.gsub(/\\/) { "\\\\" }.gsub(/["]/, '\\"')
129
+ if exact
130
+ "\"#{_text}\""
131
+ else
132
+ "\"#{_text}\"i"
133
+ end
134
+ end
135
+ end
136
+ end