playwright-ruby-client 1.26.0 → 1.28.0

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/documentation/docs/api/api_request_context.md +86 -0
  3. data/documentation/docs/api/browser_context.md +3 -3
  4. data/documentation/docs/api/download.md +1 -1
  5. data/documentation/docs/api/element_handle.md +2 -1
  6. data/documentation/docs/api/file_chooser.md +1 -1
  7. data/documentation/docs/api/frame.md +151 -4
  8. data/documentation/docs/api/frame_locator.md +151 -4
  9. data/documentation/docs/api/js_handle.md +5 -3
  10. data/documentation/docs/api/keyboard.md +1 -1
  11. data/documentation/docs/api/locator.md +191 -6
  12. data/documentation/docs/api/page.md +166 -9
  13. data/documentation/docs/api/request.md +1 -1
  14. data/documentation/docs/api/tracing.md +1 -1
  15. data/documentation/docs/article/guides/rails_integration.md +1 -0
  16. data/documentation/docs/article/guides/rails_integration_with_null_driver.md +59 -0
  17. data/documentation/docs/include/api_coverage.md +32 -0
  18. data/lib/playwright/channel_owner.rb +41 -0
  19. data/lib/playwright/channel_owners/browser_context.rb +6 -0
  20. data/lib/playwright/channel_owners/element_handle.rb +8 -1
  21. data/lib/playwright/channel_owners/frame.rb +6 -0
  22. data/lib/playwright/channel_owners/page.rb +25 -28
  23. data/lib/playwright/channel_owners/selectors.rb +4 -0
  24. data/lib/playwright/connection.rb +4 -1
  25. data/lib/playwright/frame_locator_impl.rb +6 -2
  26. data/lib/playwright/locator_impl.rb +21 -31
  27. data/lib/playwright/locator_utils.rb +136 -0
  28. data/lib/playwright/utils.rb +6 -0
  29. data/lib/playwright/version.rb +2 -2
  30. data/lib/playwright_api/android.rb +12 -6
  31. data/lib/playwright_api/android_device.rb +6 -6
  32. data/lib/playwright_api/api_request_context.rb +86 -8
  33. data/lib/playwright_api/browser.rb +6 -6
  34. data/lib/playwright_api/browser_context.rb +9 -9
  35. data/lib/playwright_api/browser_type.rb +6 -6
  36. data/lib/playwright_api/cdp_session.rb +6 -6
  37. data/lib/playwright_api/console_message.rb +6 -6
  38. data/lib/playwright_api/dialog.rb +6 -6
  39. data/lib/playwright_api/download.rb +1 -1
  40. data/lib/playwright_api/element_handle.rb +9 -8
  41. data/lib/playwright_api/file_chooser.rb +1 -1
  42. data/lib/playwright_api/frame.rb +119 -11
  43. data/lib/playwright_api/frame_locator.rb +113 -5
  44. data/lib/playwright_api/js_handle.rb +7 -7
  45. data/lib/playwright_api/keyboard.rb +1 -1
  46. data/lib/playwright_api/locator.rb +145 -6
  47. data/lib/playwright_api/page.rb +133 -16
  48. data/lib/playwright_api/playwright.rb +6 -6
  49. data/lib/playwright_api/request.rb +9 -9
  50. data/lib/playwright_api/response.rb +8 -8
  51. data/lib/playwright_api/route.rb +6 -6
  52. data/lib/playwright_api/selectors.rb +14 -3
  53. data/lib/playwright_api/tracing.rb +7 -7
  54. data/lib/playwright_api/web_socket.rb +6 -6
  55. data/lib/playwright_api/worker.rb +8 -8
  56. metadata +5 -4
@@ -36,6 +36,7 @@ module Playwright
36
36
  @type = type
37
37
  @guid = guid
38
38
  @initializer = initializer
39
+ @event_to_subscription_mapping = {}
39
40
 
40
41
  after_initialize
41
42
  end
@@ -59,6 +60,46 @@ module Playwright
59
60
  @objects.clear
60
61
  end
61
62
 
63
+ private def set_event_to_subscription_mapping(event_to_subscription_mapping)
64
+ @event_to_subscription_mapping = event_to_subscription_mapping
65
+ end
66
+
67
+ private def update_subscription(event, enabled)
68
+ protocol_event = @event_to_subscription_mapping[event]
69
+ if protocol_event
70
+ payload = {
71
+ event: protocol_event,
72
+ enabled: enabled,
73
+ }
74
+ @channel.async_send_message_to_server('updateSubscription', payload)
75
+ end
76
+ end
77
+
78
+ # @override
79
+ def on(event, callback)
80
+ if listener_count(event) == 0
81
+ update_subscription(event, true)
82
+ end
83
+ super
84
+ end
85
+
86
+ # @override
87
+ def once(event, callback)
88
+ if listener_count(event) == 0
89
+ update_subscription(event, true)
90
+ end
91
+ super
92
+ end
93
+
94
+ # @override
95
+ def off(event, callback)
96
+ super
97
+ if listener_count(event) == 0
98
+ update_subscription(event, false)
99
+ end
100
+ end
101
+
102
+
62
103
  # Suppress long long inspect log and avoid RSpec from hanging up...
63
104
  def inspect
64
105
  to_s
@@ -58,6 +58,12 @@ module Playwright
58
58
  ChannelOwners::Page.from_nullable(params['page']),
59
59
  )
60
60
  })
61
+ set_event_to_subscription_mapping({
62
+ Events::BrowserContext::Request => "request",
63
+ Events::BrowserContext::Response => "response",
64
+ Events::BrowserContext::RequestFinished => "requestFinished",
65
+ Events::BrowserContext::RequestFailed => "requestFailed",
66
+ })
61
67
 
62
68
  @closed_promise = Concurrent::Promises.resolvable_future
63
69
  end
@@ -76,10 +76,17 @@ module Playwright
76
76
  nil
77
77
  end
78
78
 
79
- def hover(force: nil, modifiers: nil, position: nil, timeout: nil, trial: nil)
79
+ def hover(
80
+ force: nil,
81
+ modifiers: nil,
82
+ noWaitAfter: nil,
83
+ position: nil,
84
+ timeout: nil,
85
+ trial: nil)
80
86
  params = {
81
87
  force: force,
82
88
  modifiers: modifiers,
89
+ noWaitAfter: noWaitAfter,
83
90
  position: position,
84
91
  timeout: timeout,
85
92
  trial: trial,
@@ -1,6 +1,10 @@
1
+ require_relative '../locator_utils'
2
+
1
3
  module Playwright
2
4
  # @ref https://github.com/microsoft/playwright-python/blob/master/playwright/_impl/_frame.py
3
5
  define_channel_owner :Frame do
6
+ include LocatorUtils
7
+
4
8
  private def after_initialize
5
9
  if @initializer['parentFrame']
6
10
  @parent_frame = ChannelOwners::Frame.from(@initializer['parentFrame'])
@@ -457,6 +461,7 @@ module Playwright
457
461
  selector,
458
462
  force: nil,
459
463
  modifiers: nil,
464
+ noWaitAfter: nil,
460
465
  position: nil,
461
466
  strict: nil,
462
467
  timeout: nil,
@@ -465,6 +470,7 @@ module Playwright
465
470
  selector: selector,
466
471
  force: force,
467
472
  modifiers: modifiers,
473
+ noWaitAfter: noWaitAfter,
468
474
  position: position,
469
475
  strict: strict,
470
476
  timeout: timeout,
@@ -1,11 +1,14 @@
1
1
  require 'base64'
2
+ require_relative '../locator_utils'
2
3
 
3
4
  module Playwright
4
5
  # @ref https://github.com/microsoft/playwright-python/blob/master/playwright/_impl/_page.py
5
6
  define_channel_owner :Page do
6
7
  include Utils::Errors::SafeCloseError
8
+ include LocatorUtils
7
9
  attr_writer :owned_context
8
10
 
11
+
9
12
  private def after_initialize
10
13
  @browser_context = @parent
11
14
  @timeout_settings = TimeoutSettings.new(@browser_context.send(:_timeout_settings))
@@ -71,6 +74,14 @@ module Playwright
71
74
  worker = ChannelOwners::Worker.from(params['worker'])
72
75
  on_worker(worker)
73
76
  })
77
+
78
+ set_event_to_subscription_mapping({
79
+ Events::Page::Request => "request",
80
+ Events::Page::Response => "response",
81
+ Events::Page::RequestFinished => "requestFinished",
82
+ Events::Page::RequestFailed => "requestFailed",
83
+ Events::Page::FileChooser => "fileChooser",
84
+ })
74
85
  end
75
86
 
76
87
  attr_reader \
@@ -167,30 +178,6 @@ module Playwright
167
178
  video.send(:set_artifact, artifact)
168
179
  end
169
180
 
170
- # @override
171
- def on(event, callback)
172
- if event == Events::Page::FileChooser && listener_count(event) == 0
173
- @channel.async_send_message_to_server('setFileChooserInterceptedNoReply', intercepted: true)
174
- end
175
- super
176
- end
177
-
178
- # @override
179
- def once(event, callback)
180
- if event == Events::Page::FileChooser && listener_count(event) == 0
181
- @channel.async_send_message_to_server('setFileChooserInterceptedNoReply', intercepted: true)
182
- end
183
- super
184
- end
185
-
186
- # @override
187
- def off(event, callback)
188
- super
189
- if event == Events::Page::FileChooser && listener_count(event) == 0
190
- @channel.async_send_message_to_server('setFileChooserInterceptedNoReply', intercepted: false)
191
- end
192
- end
193
-
194
181
  def context
195
182
  @browser_context
196
183
  end
@@ -364,16 +351,24 @@ module Playwright
364
351
 
365
352
  def emulate_media(colorScheme: nil, forcedColors: nil, media: nil, reducedMotion: nil)
366
353
  params = {
367
- colorScheme: colorScheme,
368
- forcedColors: forcedColors,
369
- media: media,
370
- reducedMotion: reducedMotion,
354
+ colorScheme: no_override_if_null(colorScheme),
355
+ forcedColors: no_override_if_null(forcedColors),
356
+ media: no_override_if_null(media),
357
+ reducedMotion: no_override_if_null(reducedMotion),
371
358
  }.compact
372
359
  @channel.send_message_to_server('emulateMedia', params)
373
360
 
374
361
  nil
375
362
  end
376
363
 
364
+ private def no_override_if_null(target)
365
+ if target == 'null'
366
+ 'no-override'
367
+ else
368
+ target
369
+ end
370
+ end
371
+
377
372
  def set_viewport_size(viewportSize)
378
373
  @viewport_size = viewportSize
379
374
  @channel.send_message_to_server('setViewportSize', { viewportSize: viewportSize })
@@ -635,6 +630,7 @@ module Playwright
635
630
  selector,
636
631
  force: nil,
637
632
  modifiers: nil,
633
+ noWaitAfter: nil,
638
634
  position: nil,
639
635
  strict: nil,
640
636
  timeout: nil,
@@ -643,6 +639,7 @@ module Playwright
643
639
  selector,
644
640
  force: force,
645
641
  modifiers: modifiers,
642
+ noWaitAfter: noWaitAfter,
646
643
  position: position,
647
644
  strict: strict,
648
645
  timeout: timeout,
@@ -18,5 +18,9 @@ module Playwright
18
18
 
19
19
  nil
20
20
  end
21
+
22
+ def text_id_attribute=(attribute_name)
23
+ ::Playwright::LocatorUtils.instance_variable_set(:@test_id_attribute_name, attribute_name)
24
+ end
21
25
  end
22
26
  end
@@ -43,7 +43,10 @@ module Playwright
43
43
  end
44
44
 
45
45
  def initialize_playwright
46
- result = send_message_to_server('', 'initialize', { sdkLanguage: 'ruby' })
46
+ # Avoid Error: sdkLanguage: expected one of (javascript|python|java|csharp)
47
+ # ref: https://github.com/microsoft/playwright/pull/18308
48
+ # ref: https://github.com/YusukeIwaki/playwright-ruby-client/issues/228
49
+ result = send_message_to_server('', 'initialize', { sdkLanguage: 'python' })
47
50
  ChannelOwners::Playwright.from(result['playwright'])
48
51
  end
49
52
 
@@ -1,5 +1,9 @@
1
+ require_relative './locator_utils'
2
+
1
3
  module Playwright
2
4
  define_api_implementation :FrameLocatorImpl do
5
+ include LocatorUtils
6
+
3
7
  def initialize(frame:, timeout_settings:, frame_selector:)
4
8
  @frame = frame
5
9
  @timeout_settings = timeout_settings
@@ -10,7 +14,7 @@ module Playwright
10
14
  LocatorImpl.new(
11
15
  frame: @frame,
12
16
  timeout_settings: @timeout_settings,
13
- selector: "#{@frame_selector} >> control=enter-frame >> #{selector}",
17
+ selector: "#{@frame_selector} >> internal:control=enter-frame >> #{selector}",
14
18
  hasText: hasText,
15
19
  has: has,
16
20
  )
@@ -20,7 +24,7 @@ module Playwright
20
24
  FrameLocatorImpl.new(
21
25
  frame: @frame,
22
26
  timeout_settings: @timeout_settings,
23
- frame_selector: "#{@frame_selector} >> control=enter-frame >> #{selector}",
27
+ frame_selector: "#{@frame_selector} >> internal:control=enter-frame >> #{selector}",
24
28
  )
25
29
  end
26
30
 
@@ -1,49 +1,24 @@
1
1
  require 'json'
2
+ require_relative './locator_utils'
2
3
 
3
4
  module Playwright
4
- class EscapeWithQuotes
5
- def initialize(text, char = "'")
6
- stringified = text.to_json
7
- escaped_text = stringified[1...-1].gsub(/\\"/, '"')
8
-
9
- case char
10
- when '"'
11
- text = escaped_text.gsub(/["]/, '\\"')
12
- @text = "\"#{text}\""
13
- when "'"
14
- text = escaped_text.gsub(/[']/, '\\\'')
15
- @text = "'#{text}'"
16
- else
17
- raise ArgumentError.new('Invalid escape char')
18
- end
19
- end
20
-
21
- def to_s
22
- @text
23
- end
24
- end
25
-
26
5
  define_api_implementation :LocatorImpl do
6
+ include LocatorUtils
7
+
27
8
  def initialize(frame:, timeout_settings:, selector:, hasText: nil, has: nil)
28
9
  @frame = frame
29
10
  @timeout_settings = timeout_settings
30
11
  selector_scopes = [selector]
31
12
 
32
- case hasText
33
- when Regexp
34
- regex = JavaScript::Regex.new(hasText)
35
- source = EscapeWithQuotes.new(regex.source, '"')
36
- selector_scopes << "has=#{"text=/#{regex.source}/#{regex.flag}".to_json}"
37
- when String
38
- text = EscapeWithQuotes.new(hasText, '"')
39
- selector_scopes << ":scope:has-text(#{text})"
13
+ if hasText
14
+ selector_scopes << "internal:has-text=#{escape_for_text_selector(hasText, false)}"
40
15
  end
41
16
 
42
17
  if has
43
18
  unless same_frame?(has)
44
19
  raise DifferentFrameError.new
45
20
  end
46
- selector_scopes << "has=#{has.send(:selector_json)}"
21
+ selector_scopes << "internal:has=#{has.send(:selector_json)}"
47
22
  end
48
23
 
49
24
  @selector = selector_scopes.join(' >> ')
@@ -211,6 +186,10 @@ module Playwright
211
186
  @frame.fill(@selector, value, strict: true, force: force, noWaitAfter: noWaitAfter, timeout: timeout)
212
187
  end
213
188
 
189
+ def clear(force: nil, noWaitAfter: nil, timeout: nil)
190
+ @frame.fill(@selector, '', strict: true, force: force, noWaitAfter: noWaitAfter, timeout: timeout)
191
+ end
192
+
214
193
  def locator(selector, hasText: nil, has: nil)
215
194
  LocatorImpl.new(
216
195
  frame: @frame,
@@ -275,6 +254,15 @@ module Playwright
275
254
  @frame.focus(@selector, strict: true, timeout: timeout)
276
255
  end
277
256
 
257
+ def blur(timeout: nil)
258
+ params = {
259
+ selector: @selector,
260
+ strict: true,
261
+ timeout: timeout,
262
+ }.compact
263
+ @frame.channel.send_message_to_server('blur', params)
264
+ end
265
+
278
266
  def count
279
267
  @frame.eval_on_selector_all(@selector, 'ee => ee.length')
280
268
  end
@@ -286,6 +274,7 @@ module Playwright
286
274
  def hover(
287
275
  force: nil,
288
276
  modifiers: nil,
277
+ noWaitAfter: nil,
289
278
  position: nil,
290
279
  timeout: nil,
291
280
  trial: nil)
@@ -293,6 +282,7 @@ module Playwright
293
282
  strict: true,
294
283
  force: force,
295
284
  modifiers: modifiers,
285
+ noWaitAfter: noWaitAfter,
296
286
  position: position,
297
287
  timeout: timeout,
298
288
  trial: trial)
@@ -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(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(/["]/, '\\"')
129
+ if exact
130
+ "\"#{_text}\""
131
+ else
132
+ "\"#{_text}\"i"
133
+ end
134
+ end
135
+ end
136
+ end
@@ -55,6 +55,12 @@ module Playwright
55
55
  params[:storageState] = JSON.parse(File.read(params[:storageState]))
56
56
  end
57
57
 
58
+ %i[colorScheme reducedMotion forcedColors].each do |key|
59
+ if params[key] == 'null'
60
+ params[key] = 'no-override'
61
+ end
62
+ end
63
+
58
64
  params
59
65
  end
60
66
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Playwright
4
- VERSION = '1.26.0'
5
- COMPATIBLE_PLAYWRIGHT_VERSION = '1.26.0'
4
+ VERSION = '1.28.0'
5
+ COMPATIBLE_PLAYWRIGHT_VERSION = '1.28.1'
6
6
  end
@@ -25,6 +25,12 @@ module Playwright
25
25
  # ```
26
26
  class Android < PlaywrightApi
27
27
 
28
+ # This methods attaches Playwright to an existing Android device. Use [`method: Android.launchServer`] to launch a new
29
+ # Android server instance.
30
+ def connect(wsEndpoint, headers: nil, slowMo: nil, timeout: nil)
31
+ raise NotImplementedError.new('connect is not implemented yet.')
32
+ end
33
+
28
34
  # Returns the list of detected Android devices.
29
35
  def devices(host: nil, omitDriverInstall: nil, port: nil)
30
36
  wrap_impl(@impl.devices(host: unwrap_impl(host), omitDriverInstall: unwrap_impl(omitDriverInstall), port: unwrap_impl(port)))
@@ -36,12 +42,6 @@ module Playwright
36
42
  end
37
43
  alias_method :default_timeout=, :set_default_timeout
38
44
 
39
- # -- inherited from EventEmitter --
40
- # @nodoc
41
- def off(event, callback)
42
- event_emitter_proxy.off(event, callback)
43
- end
44
-
45
45
  # -- inherited from EventEmitter --
46
46
  # @nodoc
47
47
  def once(event, callback)
@@ -54,6 +54,12 @@ module Playwright
54
54
  event_emitter_proxy.on(event, callback)
55
55
  end
56
56
 
57
+ # -- inherited from EventEmitter --
58
+ # @nodoc
59
+ def off(event, callback)
60
+ event_emitter_proxy.off(event, callback)
61
+ end
62
+
57
63
  private def event_emitter_proxy
58
64
  @event_emitter_proxy ||= EventEmitterProxy.new(self, @impl)
59
65
  end
@@ -173,12 +173,6 @@ module Playwright
173
173
  wrap_impl(@impl.tap_on(unwrap_impl(selector), duration: unwrap_impl(duration), timeout: unwrap_impl(timeout)))
174
174
  end
175
175
 
176
- # -- inherited from EventEmitter --
177
- # @nodoc
178
- def off(event, callback)
179
- event_emitter_proxy.off(event, callback)
180
- end
181
-
182
176
  # -- inherited from EventEmitter --
183
177
  # @nodoc
184
178
  def once(event, callback)
@@ -191,6 +185,12 @@ module Playwright
191
185
  event_emitter_proxy.on(event, callback)
192
186
  end
193
187
 
188
+ # -- inherited from EventEmitter --
189
+ # @nodoc
190
+ def off(event, callback)
191
+ event_emitter_proxy.off(event, callback)
192
+ end
193
+
194
194
  private def event_emitter_proxy
195
195
  @event_emitter_proxy ||= EventEmitterProxy.new(self, @impl)
196
196
  end