playwright-ruby-client 1.62.0 → 1.64.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. checksums.yaml +4 -4
  2. data/documentation/docs/api/api_request_context.md +50 -0
  3. data/documentation/docs/api/browser.md +2 -0
  4. data/documentation/docs/api/browser_context.md +3 -3
  5. data/documentation/docs/api/browser_type.md +1 -0
  6. data/documentation/docs/api/clock.md +1 -0
  7. data/documentation/docs/api/credentials.md +6 -3
  8. data/documentation/docs/api/frame.md +18 -2
  9. data/documentation/docs/api/frame_locator.md +20 -0
  10. data/documentation/docs/api/locator.md +66 -0
  11. data/documentation/docs/api/page.md +44 -2
  12. data/documentation/docs/api/request.md +1 -1
  13. data/documentation/docs/api/response.md +2 -1
  14. data/documentation/docs/api/tracing.md +5 -3
  15. data/documentation/docs/api/web_mcp.md +34 -0
  16. data/documentation/docs/article/guides/playwright_on_alpine_linux.md +1 -1
  17. data/documentation/docs/include/api_coverage.md +13 -0
  18. data/lib/playwright/api_request_impl.rb +3 -1
  19. data/lib/playwright/channel.rb +2 -1
  20. data/lib/playwright/channel_owners/api_request_context.rb +16 -0
  21. data/lib/playwright/channel_owners/browser.rb +1 -0
  22. data/lib/playwright/channel_owners/browser_context.rb +21 -33
  23. data/lib/playwright/channel_owners/frame.rb +6 -5
  24. data/lib/playwright/channel_owners/local_utils.rb +5 -0
  25. data/lib/playwright/channel_owners/page.rb +12 -3
  26. data/lib/playwright/channel_owners/tracing.rb +45 -13
  27. data/lib/playwright/connection.rb +6 -2
  28. data/lib/playwright/credentials_impl.rb +2 -1
  29. data/lib/playwright/errors.rb +4 -1
  30. data/lib/playwright/events.rb +2 -0
  31. data/lib/playwright/frame_locator_impl.rb +20 -5
  32. data/lib/playwright/locator_impl.rb +26 -8
  33. data/lib/playwright/screencast.rb +4 -2
  34. data/lib/playwright/transport.rb +21 -5
  35. data/lib/playwright/utils.rb +32 -0
  36. data/lib/playwright/version.rb +2 -2
  37. data/lib/playwright/web_mcp_impl.rb +21 -0
  38. data/lib/playwright_api/api_request_context.rb +47 -7
  39. data/lib/playwright_api/browser.rb +10 -8
  40. data/lib/playwright_api/browser_context.rb +17 -17
  41. data/lib/playwright_api/browser_type.rb +8 -7
  42. data/lib/playwright_api/cdp_session.rb +6 -6
  43. data/lib/playwright_api/clock.rb +1 -0
  44. data/lib/playwright_api/credentials.rb +7 -4
  45. data/lib/playwright_api/dialog.rb +6 -6
  46. data/lib/playwright_api/element_handle.rb +6 -6
  47. data/lib/playwright_api/frame.rb +33 -16
  48. data/lib/playwright_api/frame_locator.rb +20 -0
  49. data/lib/playwright_api/js_handle.rb +6 -6
  50. data/lib/playwright_api/locator.rb +63 -3
  51. data/lib/playwright_api/page.rb +57 -17
  52. data/lib/playwright_api/playwright.rb +6 -6
  53. data/lib/playwright_api/request.rb +7 -7
  54. data/lib/playwright_api/response.rb +8 -7
  55. data/lib/playwright_api/route.rb +6 -6
  56. data/lib/playwright_api/tracing.rb +12 -10
  57. data/lib/playwright_api/web_mcp.rb +26 -0
  58. data/lib/playwright_api/web_socket.rb +6 -6
  59. data/lib/playwright_api/worker.rb +6 -6
  60. data/sig/playwright.rbs +26 -12
  61. metadata +5 -2
@@ -1,6 +1,8 @@
1
1
  module Playwright
2
2
  # @ref https://github.com/microsoft/playwright-python/blob/master/playwright/_impl/_browser_context.py
3
3
  define_channel_owner :BrowserContext do
4
+ include Utils::ClearCookiesParameters
5
+
4
6
  include Utils::PrepareBrowserContextOptions
5
7
 
6
8
  attr_accessor :browser
@@ -42,6 +44,11 @@ module Playwright
42
44
  @channel.on('dialog', ->(params) {
43
45
  on_dialog(ChannelOwners::Dialog.from(params['dialog']))
44
46
  })
47
+ @channel.on('dialogClosed', ->(params) {
48
+ dialog = ChannelOwners::Dialog.from(params['dialog'])
49
+ emit(Events::BrowserContext::DialogClosed, dialog)
50
+ dialog.page&.emit(Events::Page::DialogClosed, dialog)
51
+ })
45
52
  @channel.on('request', ->(params) {
46
53
  on_request(
47
54
  ChannelOwners::Request.from(params['request']),
@@ -69,6 +76,7 @@ module Playwright
69
76
  set_event_to_subscription_mapping({
70
77
  Events::BrowserContext::Console => 'console',
71
78
  Events::BrowserContext::Dialog => 'dialog',
79
+ Events::BrowserContext::DialogClosed => 'dialogClosed',
72
80
  Events::BrowserContext::Request => "request",
73
81
  Events::BrowserContext::Response => "response",
74
82
  Events::BrowserContext::RequestFinished => "requestFinished",
@@ -93,6 +101,7 @@ module Playwright
93
101
  private def update_options(context_options:, browser_options:)
94
102
  @options = context_options
95
103
  @tracing.send(:update_traces_dir, browser_options[:tracesDir])
104
+ @request.tracing.send(:update_traces_dir, browser_options[:tracesDir])
96
105
  end
97
106
 
98
107
  private def on_page(page)
@@ -267,36 +276,7 @@ module Playwright
267
276
  end
268
277
 
269
278
  def clear_cookies(domain: nil, name: nil, path: nil)
270
- params = {}
271
-
272
- case name
273
- when String
274
- params[:name] = name
275
- when Regexp
276
- regex = JavaScript::Regex.new(name)
277
- params[:nameRegexSource] = regex.source
278
- params[:nameRegexFlags] = regex.flag
279
- end
280
-
281
- case domain
282
- when String
283
- params[:domain] = domain
284
- when Regexp
285
- regex = JavaScript::Regex.new(domain)
286
- params[:domainRegexSource] = regex.source
287
- params[:domainRegexFlags] = regex.flag
288
- end
289
-
290
- case path
291
- when String
292
- params[:path] = path
293
- when Regexp
294
- regex = JavaScript::Regex.new(path)
295
- params[:pathRegexSource] = regex.source
296
- params[:pathRegexFlags] = regex.flag
297
- end
298
-
299
- @channel.send_message_to_server('clearCookies', params)
279
+ @channel.send_message_to_server('clearCookies', clear_cookies_params(domain: domain, name: name, path: path))
300
280
  end
301
281
 
302
282
  def grant_permissions(permissions, origin: nil)
@@ -418,6 +398,7 @@ module Playwright
418
398
  @browser.browser_type.send(:playwright_selectors_browser_contexts).delete(self)
419
399
  end
420
400
  @tracing.send(:reset_stack_counter)
401
+ @request.tracing.send(:reset_stack_counter)
421
402
  emit(Events::BrowserContext::Close)
422
403
  @closed_promise.fulfill(true)
423
404
  end
@@ -427,9 +408,15 @@ module Playwright
427
408
  @close_was_called = true
428
409
  @close_reason = reason
429
410
  @request.dispose(reason: reason)
430
- @tracing.send(:export_all_hars)
411
+ har_error = nil
412
+ begin
413
+ @tracing.send(:export_all_hars)
414
+ rescue => error
415
+ har_error = error
416
+ end
431
417
  @channel.send_message_to_server('close', { reason: reason }.compact)
432
418
  @closed_promise.value!
419
+ raise har_error if har_error
433
420
  nil
434
421
  end
435
422
 
@@ -445,7 +432,7 @@ module Playwright
445
432
  #
446
433
  # So, launch inspector as Python app.
447
434
  # NOTE: This should be used only for Page#pause at this moment.
448
- @channel.send_message_to_server('enableRecorder', language: :python)
435
+ @channel.send_message_to_server('showRecorder', language: :python)
449
436
  @debug_console_enabled = true
450
437
  end
451
438
 
@@ -462,9 +449,10 @@ module Playwright
462
449
  @channel.send_message_to_server('pause')
463
450
  end
464
451
 
465
- def storage_state(path: nil, indexedDB: nil, credentials: nil)
452
+ def storage_state(path: nil, indexedDB: nil, opfs: nil, credentials: nil)
466
453
  params = {
467
454
  indexedDB: indexedDB,
455
+ opfs: opfs,
468
456
  credentials: credentials,
469
457
  }.compact
470
458
  @channel.send_message_to_server_result('storageState', params).tap do |result|
@@ -17,6 +17,7 @@ module Playwright
17
17
  @child_frames = Set.new
18
18
  @load_states = Set.new(@initializer['loadStates'])
19
19
  @event_emitter = Object.new.extend(EventEmitter)
20
+ @webmcp = WebMCPImpl.new(self)
20
21
 
21
22
  @channel.on('loadstate', ->(params) {
22
23
  on_load_state(add: params['add'], remove: params['remove'])
@@ -24,7 +25,7 @@ module Playwright
24
25
  @channel.on('navigated', method(:on_frame_navigated))
25
26
  end
26
27
 
27
- attr_reader :page, :parent_frame
28
+ attr_reader :page, :parent_frame, :webmcp
28
29
  attr_writer :detached
29
30
 
30
31
  private def _timeout(timeout)
@@ -272,8 +273,8 @@ module Playwright
272
273
  JavaScript::Expression.new(pageFunction, arg).eval_on_selector_all(@channel, selector)
273
274
  end
274
275
 
275
- def content
276
- @channel.send_message_to_server('content')
276
+ def content(includeShadow: nil)
277
+ @channel.send_message_to_server('content', { includeShadow: includeShadow }.compact)
277
278
  end
278
279
 
279
280
  def set_content(html, timeout: nil, waitUntil: nil)
@@ -506,8 +507,8 @@ module Playwright
506
507
  hasText: hasText)
507
508
  end
508
509
 
509
- def frame_locator(selector)
510
- FrameLocatorImpl.new(frame: self, frame_selector: selector)
510
+ def frame_locator(selector = nil)
511
+ FrameLocatorImpl.new(frame: self, frame_selector: selector || 'internal:control=any-frame')
511
512
  end
512
513
 
513
514
  def focus(selector, strict: nil, timeout: nil)
@@ -62,6 +62,11 @@ module Playwright
62
62
  nil
63
63
  end
64
64
 
65
+ def as_locator_description(selector)
66
+ # Ruby uses the Python SDK language for upstream code generation and the inspector.
67
+ @channel.send_message_to_server('asLocatorDescription', selector: selector, sdkLanguage: 'python')
68
+ end
69
+
65
70
  def connect(params)
66
71
  result = @channel.send_message_to_server_result('connect', params)
67
72
  ChannelOwners::JsonPipe.from(result['pipe'])
@@ -72,6 +72,7 @@ module Playwright
72
72
  set_event_to_subscription_mapping({
73
73
  Events::Page::Console => "console",
74
74
  Events::Page::Dialog => "dialog",
75
+ Events::Page::DialogClosed => "dialogClosed",
75
76
  Events::Page::Request => "request",
76
77
  Events::Page::Response => "response",
77
78
  Events::Page::RequestFinished => "requestFinished",
@@ -337,8 +338,16 @@ module Playwright
337
338
  @main_frame.url
338
339
  end
339
340
 
340
- def content
341
- @main_frame.content
341
+ def content(includeShadow: nil)
342
+ @main_frame.content(includeShadow: includeShadow)
343
+ end
344
+
345
+ def webmcp
346
+ @main_frame.webmcp
347
+ end
348
+
349
+ def get_by_ref(ref)
350
+ locator("aria-ref=#{ref}")
342
351
  end
343
352
 
344
353
  def set_content(html, timeout: nil, waitUntil: nil)
@@ -723,7 +732,7 @@ module Playwright
723
732
  hasText: hasText)
724
733
  end
725
734
 
726
- def frame_locator(selector)
735
+ def frame_locator(selector = nil)
727
736
  @main_frame.frame_locator(selector)
728
737
  end
729
738
 
@@ -5,23 +5,26 @@ module Playwright
5
5
  @har_id = nil
6
6
  end
7
7
 
8
- def start(name: nil, title: nil, screenshots: nil, snapshots: nil, sources: nil, live: nil)
8
+ def start(name: nil, title: nil, screenshots: nil, snapshots: nil, ariaSnapshots: nil, screenSnapshots: nil, sources: nil, live: nil)
9
9
  params = {
10
10
  name: name,
11
- screenshots: screenshots,
12
- snapshots: snapshots,
13
- sources: sources,
11
+ screencast: screenshots,
12
+ snapshotDom: snapshots,
13
+ snapshotAria: ariaSnapshots,
14
+ snapshotScreen: screenSnapshots,
14
15
  live: live,
15
16
  }.compact
16
- @include_sources = params[:sources] || false
17
+ @include_sources = sources || false
17
18
  @channel.send_message_to_server('tracingStart', params)
18
19
  trace_name = @channel.send_message_to_server('tracingStartChunk', { title: title, name: name }.compact)
19
20
  start_collecting_stacks(trace_name)
21
+ DisposableStub.new { stop }
20
22
  end
21
23
 
22
24
  def start_chunk(title: nil, name: nil)
23
25
  trace_name = @channel.send_message_to_server('tracingStartChunk', { title: title, name: name }.compact)
24
26
  start_collecting_stacks(trace_name)
27
+ DisposableStub.new { stop_chunk }
25
28
  end
26
29
 
27
30
  private def start_collecting_stacks(trace_name)
@@ -38,19 +41,43 @@ module Playwright
38
41
  end
39
42
 
40
43
  def stop(path: nil)
41
- do_stop_chunk(file_path: path)
44
+ save_error = nil
45
+ begin
46
+ do_stop_chunk(file_path: path)
47
+ rescue => error
48
+ save_error = error
49
+ end
42
50
  @channel.send_message_to_server('tracingStop')
51
+ raise save_error if save_error
43
52
  end
44
53
 
45
54
  private def do_stop_chunk(file_path:)
46
55
  reset_stack_counter
56
+ stacks_id = @stacks_id
57
+ @stacks_id = nil
58
+ begin
59
+ save_chunk(file_path: file_path, stacks_id: stacks_id)
60
+ rescue
61
+ # Release the session even if saving fails so subsequent traces start fresh.
62
+ if stacks_id
63
+ begin
64
+ @connection.local_utils&.trace_discarded(stacks_id)
65
+ rescue
66
+ # Preserve the error from saving the trace.
67
+ end
68
+ end
69
+ raise
70
+ end
71
+ end
72
+
73
+ private def save_chunk(file_path:, stacks_id:)
47
74
  local_utils = @connection.local_utils
48
75
 
49
76
  unless file_path
50
77
  # Not interested in any artifacts
51
78
  @channel.send_message_to_server('tracingStopChunk', mode: 'discard')
52
- if @stacks_id
53
- local_utils.trace_discarded(@stacks_id) if local_utils
79
+ if stacks_id
80
+ local_utils.trace_discarded(stacks_id) if local_utils
54
81
  end
55
82
 
56
83
  return
@@ -66,7 +93,7 @@ module Playwright
66
93
  local_utils.zip(
67
94
  zipFile: file_path,
68
95
  entries: result['entries'],
69
- stacksId: @stacks_id,
96
+ stacksId: stacks_id,
70
97
  mode: 'write',
71
98
  includeSources: @include_sources,
72
99
  )
@@ -78,8 +105,8 @@ module Playwright
78
105
  result = @channel.send_message_to_server_result('tracingStopChunk', mode: 'archive')
79
106
  # The artifact may be missing if the browser closed while stopping tracing.
80
107
  unless result['artifact']
81
- if @stacks_id
82
- local_utils.trace_discarded(@stacks_id) if local_utils
108
+ if stacks_id
109
+ local_utils.trace_discarded(stacks_id) if local_utils
83
110
  end
84
111
 
85
112
  return
@@ -87,7 +114,12 @@ module Playwright
87
114
 
88
115
  # Save trace to the final local file.
89
116
  artifact = ChannelOwners::Artifact.from(result['artifact'])
90
- artifact.save_as(file_path)
117
+ begin
118
+ artifact.save_as(file_path)
119
+ rescue
120
+ artifact.delete rescue nil
121
+ raise
122
+ end
91
123
  artifact.delete
92
124
 
93
125
  return unless local_utils
@@ -95,7 +127,7 @@ module Playwright
95
127
  local_utils.zip(
96
128
  zipFile: file_path,
97
129
  entries: [],
98
- stacksId: @stacks_id,
130
+ stacksId: stacks_id,
99
131
  mode: 'append',
100
132
  includeSources: @include_sources,
101
133
  )
@@ -24,6 +24,7 @@ module Playwright
24
24
  @waiting_for_object = {} # Hash[ guid => Promise<ChannelOwner> ]
25
25
  @callbacks = {} # Hash [ guid => Promise<ChannelOwner> ]
26
26
  @callbacks_mutex = Mutex.new
27
+ @call_id_prefix = Array.new(4) { ('a'..'z').to_a.sample }.join
27
28
  @root_object = RootChannelOwner.new(self)
28
29
  @remote = false
29
30
  @tracing_count = 0
@@ -77,7 +78,10 @@ module Playwright
77
78
  if method == '__waitInfo__' && @closed_error
78
79
  return Concurrent::Promises.fulfilled_future(nil)
79
80
  end
80
- return if @closed_error
81
+ # After the driver is gone, fail fast with the close error instead of
82
+ # returning nil (which would blow up with NoMethodError on .value!).
83
+ # ref: https://github.com/YusukeIwaki/playwright-ruby-client/issues/392
84
+ return Concurrent::Promises.rejected_future(@closed_error) if @closed_error
81
85
 
82
86
  callback = Concurrent::Promises.resolvable_future
83
87
  fire_and_forget = method == '__waitInfo__'
@@ -150,7 +154,7 @@ module Playwright
150
154
  @last_id ||= 0
151
155
  @last_id += 1
152
156
  end
153
- block.call(id)
157
+ block.call("#{@call_id_prefix}@#{id}")
154
158
  end
155
159
 
156
160
  # @param guid [String]
@@ -11,13 +11,14 @@ module Playwright
11
11
  nil
12
12
  end
13
13
 
14
- def create(rpId, id: nil, userHandle: nil, privateKey: nil, publicKey: nil)
14
+ def create(rpId, id: nil, userHandle: nil, privateKey: nil, publicKey: nil, signCount: nil)
15
15
  params = {
16
16
  rpId: rpId,
17
17
  id: id,
18
18
  userHandle: userHandle,
19
19
  privateKey: privateKey,
20
20
  publicKey: publicKey,
21
+ signCount: signCount,
21
22
  }.compact
22
23
  @browser_context.channel.send_message_to_server('credentialsCreate', params)
23
24
  end
@@ -2,7 +2,7 @@ module Playwright
2
2
  class Error < StandardError
3
3
  # ref: https://github.com/microsoft/playwright-python/blob/0b4a980fed366c4c1dee9bfcdd72662d629fdc8d/playwright/_impl/_helper.py#L155
4
4
  def self.parse(error_payload)
5
- if error_payload['name'] == 'TimeoutError'
5
+ error = if error_payload['name'] == 'TimeoutError'
6
6
  TimeoutError.new(
7
7
  message: error_payload['message'],
8
8
  stack: error_payload['stack'],
@@ -19,6 +19,8 @@ module Playwright
19
19
  stack: error_payload['stack'],
20
20
  )
21
21
  end
22
+ error.code = error_payload['code']
23
+ error
22
24
  end
23
25
 
24
26
  # @param name [String]
@@ -34,6 +36,7 @@ module Playwright
34
36
  attr_reader :name, :message, :stack, :raw_log
35
37
  # Error details for `expect` failures (received value, timedOut, customErrorMessage).
36
38
  attr_accessor :details
39
+ attr_accessor :code
37
40
 
38
41
  def log=(log)
39
42
  return unless log
@@ -29,6 +29,7 @@ end
29
29
  Close: 'close',
30
30
  Console: 'console',
31
31
  Dialog: 'dialog',
32
+ DialogClosed: 'dialogclosed',
32
33
  Download: 'download',
33
34
  FrameAttached: 'frameattached',
34
35
  FrameDetached: 'framedetached',
@@ -53,6 +54,7 @@ end
53
54
  Crash: 'crash',
54
55
  Console: 'console',
55
56
  Dialog: 'dialog',
57
+ DialogClosed: 'dialogclosed',
56
58
  Download: 'download',
57
59
  FileChooser: 'filechooser',
58
60
  DOMContentLoaded: 'domcontentloaded',
@@ -13,6 +13,21 @@ module Playwright
13
13
  @frame.send(:_timeout, timeout)
14
14
  end
15
15
 
16
+ private def child_selector(selector)
17
+ if @frame_selector == 'internal:control=any-frame'
18
+ "#{@frame_selector} >> #{selector}"
19
+ else
20
+ "#{@frame_selector} >> internal:control=enter-frame >> #{selector}"
21
+ end
22
+ end
23
+
24
+ private def nth_selector(index)
25
+ if @frame_selector == 'internal:control=any-frame'
26
+ raise 'Selecting the nth frame is not allowed on frameLocator().'
27
+ end
28
+ "#{@frame_selector} >> nth=#{index}"
29
+ end
30
+
16
31
  def locator(
17
32
  selector,
18
33
  has: nil,
@@ -21,7 +36,7 @@ module Playwright
21
36
  hasText: nil)
22
37
  LocatorImpl.new(
23
38
  frame: @frame,
24
- selector: "#{@frame_selector} >> internal:control=enter-frame >> #{selector}",
39
+ selector: child_selector(selector),
25
40
  has: has,
26
41
  hasNot: hasNot,
27
42
  hasNotText: hasNotText,
@@ -38,28 +53,28 @@ module Playwright
38
53
  def frame_locator(selector)
39
54
  FrameLocatorImpl.new(
40
55
  frame: @frame,
41
- frame_selector: "#{@frame_selector} >> internal:control=enter-frame >> #{selector}",
56
+ frame_selector: child_selector(selector),
42
57
  )
43
58
  end
44
59
 
45
60
  def first
46
61
  FrameLocatorImpl.new(
47
62
  frame: @frame,
48
- frame_selector: "#{@frame_selector} >> nth=0",
63
+ frame_selector: nth_selector(0),
49
64
  )
50
65
  end
51
66
 
52
67
  def last
53
68
  FrameLocatorImpl.new(
54
69
  frame: @frame,
55
- frame_selector: "#{@frame_selector} >> nth=-1",
70
+ frame_selector: nth_selector(-1),
56
71
  )
57
72
  end
58
73
 
59
74
  def nth(index)
60
75
  FrameLocatorImpl.new(
61
76
  frame: @frame,
62
- frame_selector: "#{@frame_selector} >> nth=#{index}",
77
+ frame_selector: nth_selector(index),
63
78
  )
64
79
  end
65
80
  end
@@ -5,8 +5,9 @@ module Playwright
5
5
  define_api_implementation :LocatorImpl do
6
6
  include LocatorUtils
7
7
 
8
- def initialize(frame:, selector:, has: nil, hasNot: nil, hasNotText: nil, hasText: nil, visible: nil)
8
+ def initialize(frame:, selector:, has: nil, hasNot: nil, hasNotText: nil, hasText: nil, visible: nil, locator_code: nil)
9
9
  @frame = frame
10
+ @locator_code = locator_code
10
11
  selector_scopes = [selector]
11
12
 
12
13
  if hasText
@@ -39,11 +40,18 @@ module Playwright
39
40
  end
40
41
 
41
42
  def to_s
42
- if @description
43
- "Locator@#{@selector} (#{@description})"
44
- else
45
- "Locator@#{@selector}"
46
- end
43
+ custom_description = description
44
+ return custom_description if custom_description
45
+ return @locator_code if @locator_code
46
+
47
+ local_utils = @frame.instance_variable_get(:@connection).local_utils
48
+ return "Locator@#{@selector}" unless local_utils
49
+
50
+ local_utils.as_locator_description(@selector)
51
+ end
52
+
53
+ def within(locator)
54
+ locator.locator(self)
47
55
  end
48
56
 
49
57
  private def to_protocol
@@ -232,6 +240,11 @@ module Playwright
232
240
  hasNot: nil,
233
241
  hasNotText: nil,
234
242
  hasText: nil)
243
+ if selector.is_a?(LocatorImpl)
244
+ raise ArgumentError, 'Locators must belong to the same frame.' unless same_frame?(selector)
245
+
246
+ selector = "internal:chain=#{selector.send(:selector_json)}"
247
+ end
235
248
  LocatorImpl.new(
236
249
  frame: @frame,
237
250
  selector: "#{@selector} >> #{selector}",
@@ -296,6 +309,10 @@ module Playwright
296
309
  )
297
310
  end
298
311
 
312
+ def visible
313
+ filter(visible: true)
314
+ end
315
+
299
316
  def first
300
317
  LocatorImpl.new(
301
318
  frame: @frame,
@@ -449,10 +466,11 @@ module Playwright
449
466
  end
450
467
 
451
468
  def normalize
452
- resolved_selector = @frame.channel.send_message_to_server('resolveSelector', { selector: @selector })
469
+ result = @frame.channel.send_message_to_server_result('resolveSelector', { selector: @selector, sdkLanguage: 'python' })
453
470
  LocatorImpl.new(
454
471
  frame: @frame,
455
- selector: resolved_selector,
472
+ selector: result['resolvedSelector'],
473
+ locator_code: result['locatorCode'],
456
474
  )
457
475
  end
458
476
 
@@ -11,7 +11,7 @@ module Playwright
11
11
  @page.send(:channel).on('screencastFrame', method(:handle_screencast_frame))
12
12
  end
13
13
 
14
- def start(path: nil, size: nil, quality: nil, &on_frame)
14
+ def start(path: nil, size: nil, quality: nil, fps: nil, &on_frame)
15
15
  raise 'Screencast is already started' if @started
16
16
 
17
17
  @started = true
@@ -21,6 +21,7 @@ module Playwright
21
21
  params[:record] = true if path
22
22
  params[:size] = size if size
23
23
  params[:quality] = quality if quality
24
+ params[:fps] = fps unless fps.nil?
24
25
  params[:sendFrames] = true if on_frame
25
26
 
26
27
  result = @page.send(:channel).send_message_to_server_result('screencastStart', params)
@@ -63,12 +64,13 @@ module Playwright
63
64
  @page.send(:channel).send_message_to_server('screencastChapter', params)
64
65
  end
65
66
 
66
- def show_actions(duration: nil, fontSize: nil, position: nil, cursor: nil)
67
+ def show_actions(duration: nil, fontSize: nil, position: nil, cursor: nil, style: nil)
67
68
  params = {}
68
69
  params[:duration] = duration if duration
69
70
  params[:fontSize] = fontSize if fontSize
70
71
  params[:position] = position if position
71
72
  params[:cursor] = cursor if cursor
73
+ params[:style] = style if style
72
74
  @page.send(:channel).send_message_to_server('screencastShowActions', params)
73
75
  DisposableStub.new { hide_actions }
74
76
  end
@@ -32,10 +32,15 @@ module Playwright
32
32
  def send_message(message)
33
33
  debug_send_message(message) if @debug
34
34
  msg = JSON.dump(message)
35
- @mutex.synchronize {
36
- @stdin.write([msg.bytes.length].pack('V')) # unsigned 32bit, little endian, real byte size instead of chars
37
- @stdin.write(msg) # write UTF-8 in binary mode as byte stream
38
- }
35
+ # Build the whole frame (4-byte little-endian length header + JSON payload) in a
36
+ # single buffer, and write it at once. Two separate writes can be torn by an
37
+ # asynchronous exception (e.g. Timeout::Error from Timeout.timeout, delivered via
38
+ # Thread#raise), which crashes the driver with a JSON parse error.
39
+ # ref: https://github.com/YusukeIwaki/playwright-ruby-client/issues/392
40
+ frame = [msg.bytesize].pack('V') + msg.b # unsigned 32bit, little endian, real byte size instead of chars
41
+ Thread.handle_interrupt(Object => :never) do
42
+ @mutex.synchronize { @stdin.write(frame) } # write UTF-8 in binary mode as byte stream
43
+ end
39
44
  rescue Errno::EPIPE, IOError
40
45
  raise AlreadyDisconnectedError.new('send_message failed')
41
46
  end
@@ -85,6 +90,10 @@ module Playwright
85
90
  debug_recv_message(obj) if @debug
86
91
  @on_message&.call(obj)
87
92
  end
93
+ # exited the loop on clean EOF. The driver is gone, so reject pending
94
+ # callbacks instead of hanging forever on promises that will never resolve.
95
+ # ref: https://github.com/YusukeIwaki/playwright-ruby-client/issues/392
96
+ @on_driver_closed&.call
88
97
  rescue IOError
89
98
  # disconnected by remote.
90
99
  @on_driver_closed&.call
@@ -92,6 +101,9 @@ module Playwright
92
101
 
93
102
  def handle_stderr
94
103
  while err = @stderr.read
104
+ # IO#read without length on a pipe returns "" (not nil) at EOF.
105
+ # Bail out instead of spinning forever on empty reads.
106
+ break if err.empty?
95
107
  # sometimed driver crashes with the error below.
96
108
  # --------
97
109
  # undefined:1
@@ -103,7 +115,11 @@ module Playwright
103
115
  # at Transport.transport.onmessage (/home/runner/work/playwright-ruby-client/playwright-ruby-client/node_modules/playwright/lib/cli/driver.js:42:73)
104
116
  # at Immediate.<anonymous> (/home/runner/work/playwright-ruby-client/playwright-ruby-client/node_modules/playwright/lib/protocol/transport.js:74:26)
105
117
  # at processImmediate (internal/timers.js:461:21)
106
- if err.include?('undefined:1')
118
+ #
119
+ # Node >= 16 prints '<anonymous_script>:1' instead of 'undefined:1'.
120
+ # ref: https://github.com/YusukeIwaki/playwright-ruby-client/issues/392
121
+ if err.include?('undefined:1') || err.include?('<anonymous_script>:1') ||
122
+ err.include?('is not valid JSON')
107
123
  @on_driver_crashed&.call
108
124
  break
109
125
  end
@@ -18,9 +18,38 @@ module Playwright
18
18
  end
19
19
 
20
20
  module Utils
21
+ module ClearCookiesParameters
22
+ private def clear_cookies_params(domain:, name:, path:)
23
+ params = {}
24
+ { domain: domain, name: name, path: path }.each do |key, value|
25
+ case value
26
+ when String
27
+ params[key] = value
28
+ when Regexp
29
+ regex = JavaScript::Regex.new(value)
30
+ params["#{key}RegexSource".to_sym] = regex.source
31
+ params["#{key}RegexFlags".to_sym] = regex.flag
32
+ end
33
+ end
34
+ params
35
+ end
36
+ end
37
+
38
+ module PrepareHttpCredentials
39
+ private def prepare_http_credentials(credentials)
40
+ return nil unless credentials
41
+
42
+ list = credentials.is_a?(Array) ? credentials : [credentials]
43
+ list.empty? ? nil : list
44
+ end
45
+ end
46
+
21
47
  module PrepareBrowserContextOptions
48
+ include PrepareHttpCredentials
49
+
22
50
  # @see https://github.com/microsoft/playwright/blob/5a2cfdbd47ed3c3deff77bb73e5fac34241f649d/src/client/browserContext.ts#L265
23
51
  private def prepare_browser_context_options(params)
52
+ params[:httpCredentials] = prepare_http_credentials(params[:httpCredentials])
24
53
  if params[:noViewport] == 0
25
54
  params.delete(:noViewport)
26
55
  params[:noDefaultViewport] = true
@@ -35,6 +64,9 @@ module Playwright
35
64
  if params[:record_video_size]
36
65
  params[:recordVideo][:size] = params.delete(:record_video_size)
37
66
  end
67
+ unless params[:record_video_fps].nil?
68
+ params[:recordVideo][:fps] = params.delete(:record_video_fps)
69
+ end
38
70
  if params[:record_video_show_actions]
39
71
  params[:recordVideo][:showActions] = params.delete(:record_video_show_actions)
40
72
  end