playwright-ruby-client 1.60.0 → 1.61.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 (47) hide show
  1. checksums.yaml +4 -4
  2. data/documentation/docs/api/api_response.md +18 -0
  3. data/documentation/docs/api/browser_context.md +6 -0
  4. data/documentation/docs/api/browser_type.md +1 -0
  5. data/documentation/docs/api/credentials.md +132 -0
  6. data/documentation/docs/api/frame.md +4 -4
  7. data/documentation/docs/api/page.md +12 -2
  8. data/documentation/docs/api/touchscreen.md +1 -1
  9. data/documentation/docs/api/web_storage.md +65 -0
  10. data/documentation/docs/include/api_coverage.md +20 -0
  11. data/lib/playwright/api_response_impl.rb +8 -0
  12. data/lib/playwright/channel_owners/browser_context.rb +2 -1
  13. data/lib/playwright/channel_owners/browser_type.rb +2 -1
  14. data/lib/playwright/channel_owners/frame.rb +36 -17
  15. data/lib/playwright/channel_owners/page.rb +12 -2
  16. data/lib/playwright/connection.rb +7 -1
  17. data/lib/playwright/credentials_impl.rb +35 -0
  18. data/lib/playwright/errors.rb +4 -1
  19. data/lib/playwright/locator_utils.rb +9 -1
  20. data/lib/playwright/screencast.rb +3 -1
  21. data/lib/playwright/version.rb +2 -2
  22. data/lib/playwright/waiter.rb +9 -41
  23. data/lib/playwright/web_storage_impl.rb +34 -0
  24. data/lib/playwright_api/api_request_context.rb +6 -6
  25. data/lib/playwright_api/api_response.rb +12 -0
  26. data/lib/playwright_api/browser.rb +6 -6
  27. data/lib/playwright_api/browser_context.rb +23 -16
  28. data/lib/playwright_api/browser_type.rb +8 -7
  29. data/lib/playwright_api/cdp_session.rb +6 -6
  30. data/lib/playwright_api/credentials.rb +120 -0
  31. data/lib/playwright_api/dialog.rb +6 -6
  32. data/lib/playwright_api/element_handle.rb +6 -6
  33. data/lib/playwright_api/frame.rb +14 -14
  34. data/lib/playwright_api/js_handle.rb +6 -6
  35. data/lib/playwright_api/locator.rb +5 -5
  36. data/lib/playwright_api/page.rb +32 -20
  37. data/lib/playwright_api/playwright.rb +6 -6
  38. data/lib/playwright_api/request.rb +8 -8
  39. data/lib/playwright_api/response.rb +6 -6
  40. data/lib/playwright_api/route.rb +6 -6
  41. data/lib/playwright_api/touchscreen.rb +1 -1
  42. data/lib/playwright_api/tracing.rb +6 -6
  43. data/lib/playwright_api/web_socket.rb +6 -6
  44. data/lib/playwright_api/web_storage.rb +48 -0
  45. data/lib/playwright_api/worker.rb +8 -8
  46. data/sig/playwright.rbs +21 -1
  47. metadata +8 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ac9256b441d66c5a19585735b23567b6dc1eedca695812ee3388f48918054970
4
- data.tar.gz: 9a13c1dcfc540d7bc1120743af1806a338185a2bd6eb41331e2273425af790c6
3
+ metadata.gz: c45763ab237c198b8f2aa3afa3114ae0bf821481e57711516f5014547079f183
4
+ data.tar.gz: ed8d42f43cb28c8ae2a45755332582b6e51ac993a751de94fb1a25fff43f5532
5
5
  SHA512:
6
- metadata.gz: f8fae6bb376c4586a90464a2db4d06b3362a822d191456dcf5ad75f74ae67ed99462403aa4cdca4d817eb58d59ec30380a9b72c5d6cee2f5475a0fda4e2c05a8
7
- data.tar.gz: b59cc6913b92826850611eab5967f72c2545549f2ac83c8268d59d6ef61152523a1fcb62759278487661369449a817222ecd514a0c066ea913b3e4607a4b0463
6
+ metadata.gz: 90ac4618f354ca98c4d5a5c7284b23d752a098bf7128a246076964381f36d93680b78c89599b15b175afdb7274e6cea1882cfab64f9d9eab6dcb1317b0f0f894
7
+ data.tar.gz: 91cdfb64498f4e76f0f3a6f726fd49fa3d36dd7c6b692e843455bf8a9160a94216f13e324ffe90a2472c0b9ad573bd1d1ab241590629357d01562991ea299c63
@@ -77,6 +77,24 @@ def ok
77
77
 
78
78
  Contains a boolean stating whether the response was successful (status in the range 200-299) or not.
79
79
 
80
+ ## security_details
81
+
82
+ ```
83
+ def security_details
84
+ ```
85
+
86
+
87
+ Returns SSL and other security information. Resolves to `null` for non-HTTPS responses. For redirected requests, returns the information for the last request in the redirect chain.
88
+
89
+ ## server_addr
90
+
91
+ ```
92
+ def server_addr
93
+ ```
94
+
95
+
96
+ Returns the IP address and port of the server. Resolves to `null` if the server address is not available. For redirected requests, returns the information for the last request in the redirect chain.
97
+
80
98
  ## status
81
99
 
82
100
  ```
@@ -521,6 +521,12 @@ Will throw an error if the context closes before new [Page](./page) is created.
521
521
 
522
522
  Playwright has ability to mock clock and passage of time.
523
523
 
524
+ ## credentials
525
+
526
+
527
+ Virtual WebAuthn authenticator for this context. Lets tests seed credentials and intercept
528
+ `navigator.credentials.create()` / `navigator.credentials.get()` ceremonies.
529
+
524
530
  ## request
525
531
 
526
532
 
@@ -41,6 +41,7 @@ This method attaches Playwright to an existing browser instance created via `Bro
41
41
  ```
42
42
  def connect_over_cdp(
43
43
  endpointURL,
44
+ artifactsDir: nil,
44
45
  headers: nil,
45
46
  isLocal: nil,
46
47
  noDefaults: nil,
@@ -0,0 +1,132 @@
1
+ ---
2
+ sidebar_position: 10
3
+ ---
4
+
5
+ # Credentials
6
+
7
+
8
+ [Credentials](./credentials) is a virtual WebAuthn authenticator scoped to a [BrowserContext](./browser_context). It lets tests
9
+ register passkeys and answer `navigator.credentials.create()` / `navigator.credentials.get()`
10
+ ceremonies in the page, without a real authenticator or hardware security key.
11
+
12
+ There are two common ways to use it:
13
+
14
+ **Usage: seed a known credential**
15
+
16
+ ```ruby
17
+ context = browser.new_context
18
+
19
+ # A passkey your backend already provisioned for a test user.
20
+ context.credentials.create(
21
+ "example.com",
22
+ id: known_credential_id, # base64url
23
+ userHandle: known_user_handle, # base64url
24
+ privateKey: known_private_key, # base64url PKCS#8 (DER)
25
+ publicKey: known_public_key, # base64url SPKI (DER)
26
+ )
27
+ context.credentials.install
28
+
29
+ page = context.new_page
30
+ page.goto("https://example.com/login")
31
+ # The page's navigator.credentials.get() is answered with the seeded passkey.
32
+ ```
33
+
34
+ **Usage: capture a passkey, then reuse it**
35
+
36
+ ```ruby
37
+ # setup test: let the app register a passkey, then save it.
38
+ context = browser.new_context
39
+ context.credentials.install
40
+
41
+ page = context.new_page
42
+ page.goto("https://example.com/register")
43
+ page.get_by_role("button", name: "Create a passkey").click
44
+
45
+ # Read back the passkey the page registered - it includes the private key.
46
+ credential = context.credentials.get(rpId: "example.com").first
47
+ File.write("playwright/.auth/passkey.json", JSON.generate(credential))
48
+ ```
49
+
50
+ ```ruby
51
+ # later test: seed the captured passkey so the app starts already enrolled.
52
+ credential = JSON.parse(File.read("playwright/.auth/passkey.json"))
53
+ context = browser.new_context
54
+ context.credentials.create(
55
+ credential["rpId"],
56
+ id: credential["id"],
57
+ userHandle: credential["userHandle"],
58
+ privateKey: credential["privateKey"],
59
+ publicKey: credential["publicKey"],
60
+ )
61
+ context.credentials.install
62
+
63
+ page = context.new_page
64
+ page.goto("https://example.com/login")
65
+ # navigator.credentials.get() resolves the captured passkey - already signed in.
66
+ ```
67
+
68
+ **Defaults**
69
+
70
+ ## install
71
+
72
+ ```
73
+ def install
74
+ ```
75
+
76
+
77
+ Installs the virtual WebAuthn authenticator into the context, overriding
78
+ `navigator.credentials.create()` and `navigator.credentials.get()` in all current
79
+ and future pages. Call this before the page first touches `navigator.credentials`.
80
+
81
+ Required: until [Credentials#install](./credentials#install) is called, no interception is in place and the page sees
82
+ the platform's native (or absent) WebAuthn behaviour. Seeding credentials with
83
+ [Credentials#create](./credentials#create) without installing populates the authenticator, but the
84
+ page will never see those credentials.
85
+
86
+ ## create
87
+
88
+ ```
89
+ def create(
90
+ rpId,
91
+ id: nil,
92
+ privateKey: nil,
93
+ publicKey: nil,
94
+ userHandle: nil)
95
+ ```
96
+
97
+
98
+ Seeds a virtual WebAuthn credential and returns it.
99
+
100
+ With only `rpId`, generates a fresh **ECDSA P-256** keypair, credential id and user handle. The
101
+ seeded credential is discoverable (resident), so the page can resolve it from both
102
+ username-then-passkey and usernameless passkey flows. The returned object carries the private and public keys, so it can be persisted to disk and re-seeded in a later test.
103
+
104
+ To **import a known credential**, supply all four of `id`, `userHandle`, `privateKey` and
105
+ `publicKey` together.
106
+
107
+ Call [Credentials#install](./credentials#install) before navigating to a page that uses WebAuthn.
108
+
109
+ ## delete
110
+
111
+ ```
112
+ def delete(id)
113
+ ```
114
+
115
+
116
+ Removes a credential from the authenticator by its id. Works for any credential currently held —
117
+ both those seeded with [Credentials#create](./credentials#create) and those the page registered itself by
118
+ calling `navigator.credentials.create()`.
119
+
120
+ ## get
121
+
122
+ ```
123
+ def get(id: nil, rpId: nil)
124
+ ```
125
+
126
+
127
+ Returns every credential currently held by the authenticator, optionally filtered by `rpId` or
128
+ `id`. This includes both credentials seeded with [Credentials#create](./credentials#create) and credentials
129
+ the page registered itself by calling `navigator.credentials.create()`.
130
+
131
+ Each returned credential includes its private and public keys, so a passkey the app just
132
+ registered can be saved and re-seeded into a later test with [Credentials#create](./credentials#create) — see the second example in the class overview.
@@ -291,7 +291,7 @@ puts frame.evaluate("1 + #{x}") # => "11"
291
291
  [ElementHandle](./element_handle) instances can be passed as an argument to the [Frame#evaluate](./frame#evaluate):
292
292
 
293
293
  ```ruby
294
- body_handle = frame.query_selector("body")
294
+ body_handle = frame.evaluate_handle("document.body")
295
295
  html = frame.evaluate("([body, suffix]) => body.innerHTML + suffix", arg: [body_handle, "hello"])
296
296
  body_handle.dispose
297
297
  ```
@@ -321,14 +321,14 @@ a_window_handle # handle for the window object.
321
321
  A string can also be passed in instead of a function.
322
322
 
323
323
  ```ruby
324
- a_handle = page.evaluate_handle("document") # handle for the "document"
324
+ a_handle = frame.evaluate_handle("document") # handle for the "document"
325
325
  ```
326
326
 
327
327
  [JSHandle](./js_handle) instances can be passed as an argument to the [Frame#evaluate_handle](./frame#evaluate_handle):
328
328
 
329
329
  ```ruby
330
- body_handle = page.evaluate_handle("document.body")
331
- result_handle = page.evaluate_handle("body => body.innerHTML", arg: body_handle)
330
+ a_handle = frame.evaluate_handle("document.body")
331
+ result_handle = frame.evaluate_handle("body => body.innerHTML", arg: a_handle)
332
332
  puts result_handle.json_value
333
333
  result_handle.dispose
334
334
  ```
@@ -413,7 +413,7 @@ puts page.evaluate("1 + #{x}") # => "11"
413
413
  [ElementHandle](./element_handle) instances can be passed as an argument to the [Page#evaluate](./page#evaluate):
414
414
 
415
415
  ```ruby
416
- body_handle = page.query_selector("body")
416
+ body_handle = page.evaluate_handle("document.body")
417
417
  html = page.evaluate("([body, suffix]) => body.innerHTML + suffix", arg: [body_handle, "hello"])
418
418
  body_handle.dispose
419
419
  ```
@@ -1561,7 +1561,7 @@ This method taps an element matching `selector` by performing the following step
1561
1561
  When all steps combined have not finished during the specified `timeout`, this method throws a
1562
1562
  `TimeoutError`. Passing zero timeout disables this.
1563
1563
 
1564
- **NOTE**: [Page#tap_point](./page#tap_point) the method will throw if `hasTouch` option of the browser context is false.
1564
+ **NOTE**: [Page#tap_point](./page#tap_point) will throw if the `hasTouch` option of the browser context is false.
1565
1565
 
1566
1566
  ## text_content
1567
1567
 
@@ -2001,6 +2001,16 @@ Playwright has ability to mock clock and passage of time.
2001
2001
 
2002
2002
  ## keyboard
2003
2003
 
2004
+ ## local_storage
2005
+
2006
+
2007
+ Provides access to the page's `localStorage` for the current origin. See [WebStorage](./web_storage).
2008
+
2009
+ ## session_storage
2010
+
2011
+
2012
+ Provides access to the page's `sessionStorage` for the current origin. See [WebStorage](./web_storage).
2013
+
2004
2014
  ## mouse
2005
2015
 
2006
2016
  ## request
@@ -19,4 +19,4 @@ def tap_point(x, y)
19
19
 
20
20
  Dispatches a `touchstart` and `touchend` event with a single touch at the position (`x`,`y`).
21
21
 
22
- **NOTE**: [Page#tap_point](./page#tap_point) the method will throw if `hasTouch` option of the browser context is false.
22
+ **NOTE**: [Touchscreen#tap_point](./touchscreen#tap_point) will throw if the `hasTouch` option of the browser context is false.
@@ -0,0 +1,65 @@
1
+ ---
2
+ sidebar_position: 10
3
+ ---
4
+
5
+ # WebStorage
6
+
7
+
8
+ WebStorage exposes the page's `localStorage` or `sessionStorage` for the current origin via an async,
9
+ [browser-consistent](https://developer.mozilla.org/en-US/docs/Web/API/Storage) API.
10
+
11
+ Instances are accessed through [Page#local_storage](./page#local_storage) and [Page#session_storage](./page#session_storage).
12
+
13
+ ```ruby
14
+ page.goto("https://example.com")
15
+ page.local_storage.set_item("token", "abc")
16
+ token = page.local_storage.get_item("token")
17
+ all = page.local_storage.items
18
+ page.local_storage.remove_item("token")
19
+ page.local_storage.clear
20
+ ```
21
+
22
+ ## items
23
+
24
+ ```
25
+ def items
26
+ ```
27
+
28
+
29
+ Returns all items in the storage as name/value pairs.
30
+
31
+ ## get_item
32
+
33
+ ```
34
+ def get_item(name)
35
+ ```
36
+
37
+
38
+ Returns the value for the given `name` if present.
39
+
40
+ ## set_item
41
+
42
+ ```
43
+ def set_item(name, value)
44
+ ```
45
+
46
+
47
+ Sets the value for the given `name`. Overwrites any existing value for that name.
48
+
49
+ ## remove_item
50
+
51
+ ```
52
+ def remove_item(name)
53
+ ```
54
+
55
+
56
+ Removes the item with the given `name`. No-op if the item is absent.
57
+
58
+ ## clear
59
+
60
+ ```
61
+ def clear
62
+ ```
63
+
64
+
65
+ Removes all items from the storage.
@@ -365,6 +365,8 @@
365
365
  * ~~wait_for_event~~
366
366
  * clock
367
367
  * keyboard
368
+ * local_storage
369
+ * session_storage
368
370
  * mouse
369
371
  * request
370
372
  * screencast
@@ -405,6 +407,7 @@
405
407
  * expect_page
406
408
  * ~~wait_for_event~~
407
409
  * clock
410
+ * credentials
408
411
  * ~~debugger~~
409
412
  * request
410
413
  * tracing
@@ -553,6 +556,8 @@
553
556
  * headers_array
554
557
  * json
555
558
  * ok
559
+ * security_details
560
+ * server_addr
556
561
  * status
557
562
  * status_text
558
563
  * text
@@ -638,3 +643,18 @@
638
643
  * not_to_match_aria_snapshot
639
644
  * to_have_title
640
645
  * to_have_url
646
+
647
+ ## WebStorage
648
+
649
+ * items
650
+ * get_item
651
+ * set_item
652
+ * remove_item
653
+ * clear
654
+
655
+ ## Credentials
656
+
657
+ * install
658
+ * create
659
+ * delete
660
+ * get
@@ -39,6 +39,14 @@ module Playwright
39
39
  @headers.headers_array
40
40
  end
41
41
 
42
+ def security_details
43
+ @initializer['securityDetails']
44
+ end
45
+
46
+ def server_addr
47
+ @initializer['serverAddr']
48
+ end
49
+
42
50
  class AlreadyDisposedError < StandardError
43
51
  def initialize
44
52
  super('Response has been disposed')
@@ -5,7 +5,7 @@ module Playwright
5
5
 
6
6
  attr_accessor :browser
7
7
  attr_writer :owner_page, :options
8
- attr_reader :clock, :tracing, :request
8
+ attr_reader :clock, :tracing, :request, :credentials
9
9
 
10
10
  private def after_initialize
11
11
  @options = @initializer['options']
@@ -20,6 +20,7 @@ module Playwright
20
20
  @request = ChannelOwners::APIRequestContext.from(@initializer['requestContext'])
21
21
  @request.send(:_update_timeout_settings, @timeout_settings)
22
22
  @clock = ClockImpl.new(self)
23
+ @credentials = CredentialsImpl.new(self)
23
24
 
24
25
  @channel.on('bindingCall', ->(params) { on_binding(ChannelOwners::BindingCall.from(params['binding'])) })
25
26
  @channel.once('close', ->(_) { on_close })
@@ -92,11 +92,12 @@ module Playwright
92
92
  raise
93
93
  end
94
94
 
95
- def connect_over_cdp(endpointURL, headers: nil, isLocal: nil, noDefaults: nil, slowMo: nil, timeout: nil, &block)
95
+ def connect_over_cdp(endpointURL, artifactsDir: nil, headers: nil, isLocal: nil, noDefaults: nil, slowMo: nil, timeout: nil, &block)
96
96
  raise 'Connecting over CDP is only supported in Chromium.' unless name == 'chromium'
97
97
 
98
98
  params = {
99
99
  endpointURL: endpointURL,
100
+ artifactsDir: artifactsDir,
100
101
  headers: headers,
101
102
  isLocal: isLocal,
102
103
  noDefaults: noDefaults,
@@ -725,25 +725,44 @@ module Playwright
725
725
  .serialize
726
726
  end
727
727
 
728
- result = @channel.send_message_to_server_result(
729
- title, # title
730
- "expect", # method
731
- { # params
732
- selector: selector,
733
- expression: expression,
734
- **options,
735
- }.compact
736
- )
737
-
738
- if result.key?('received')
739
- if result['received'].is_a?(Hash) && result['received'].key?('value')
740
- result['received']['value'] = JavaScript::ValueParser.new(result['received']['value']).parse
741
- elsif !result['received'].is_a?(Hash)
742
- result['received'] = JavaScript::ValueParser.new(result['received']).parse
728
+ is_not = options[:isNot] || false
729
+
730
+ # Since Playwright 1.61, the `expect` command resolves on a successful
731
+ # match and rejects with FrameExpectErrorDetails on a mismatch/timeout.
732
+ begin
733
+ @channel.send_message_to_server_result(
734
+ title, # title
735
+ "expect", # method
736
+ { # params
737
+ selector: selector,
738
+ expression: expression,
739
+ **options,
740
+ }.compact
741
+ )
742
+ { 'matches' => !is_not }
743
+ rescue ::Playwright::Error => err
744
+ details = err.details || {}
745
+ received = details['received']
746
+ if received.is_a?(Hash)
747
+ received = received.dup
748
+ if received.key?('value')
749
+ received['value'] = JavaScript::ValueParser.new(received['value']).parse
750
+ end
743
751
  end
744
- end
745
752
 
746
- result
753
+ error_message =
754
+ if details['customErrorMessage']
755
+ "Error: #{details['customErrorMessage']}"
756
+ end
757
+
758
+ {
759
+ 'matches' => is_not,
760
+ 'received' => received,
761
+ 'log' => err.raw_log,
762
+ 'timedOut' => details['timedOut'],
763
+ 'errorMessage' => error_message,
764
+ }.compact
765
+ end
747
766
  end
748
767
 
749
768
  private def drop_payload_params(payload)
@@ -86,6 +86,14 @@ module Playwright
86
86
  :viewport_size,
87
87
  :main_frame
88
88
 
89
+ def local_storage
90
+ @local_storage ||= WebStorageImpl.new(self, 'local')
91
+ end
92
+
93
+ def session_storage
94
+ @session_storage ||= WebStorageImpl.new(self, 'session')
95
+ end
96
+
89
97
  private def on_frame_attached(frame)
90
98
  frame.send(:update_page_from_page, self)
91
99
  @frames << frame
@@ -513,13 +521,15 @@ module Playwright
513
521
  end
514
522
  if @owned_context
515
523
  @owned_context.close
524
+ elsif runBeforeUnload
525
+ @channel.send_message_to_server('runBeforeUnload')
516
526
  else
517
- options = { runBeforeUnload: runBeforeUnload }.compact
527
+ options = { reason: reason }.compact
518
528
  @channel.send_message_to_server('close', options)
519
529
  end
520
530
  nil
521
531
  rescue => err
522
- raise if !target_closed_error?(err) || !runBeforeUnload
532
+ raise if !target_closed_error?(err) || runBeforeUnload
523
533
  end
524
534
 
525
535
  def closed?
@@ -74,14 +74,18 @@ module Playwright
74
74
  end
75
75
 
76
76
  def async_send_message_to_server(guid, method, params, metadata: nil)
77
+ if method == '__waitInfo__' && @closed_error
78
+ return Concurrent::Promises.fulfilled_future(nil)
79
+ end
77
80
  return if @closed_error
78
81
 
79
82
  callback = Concurrent::Promises.resolvable_future
83
+ fire_and_forget = method == '__waitInfo__'
80
84
 
81
85
  with_generated_id do |id|
82
86
  # register callback promise object first.
83
87
  # @see https://github.com/YusukeIwaki/puppeteer-ruby/pull/34
84
- @callbacks_mutex.synchronize { @callbacks[id] = callback }
88
+ @callbacks_mutex.synchronize { @callbacks[id] = callback } unless fire_and_forget
85
89
 
86
90
  _metadata = {}
87
91
  frames = []
@@ -112,6 +116,7 @@ module Playwright
112
116
  callback.reject(err)
113
117
  raise unless err.is_a?(Transport::AlreadyDisconnectedError)
114
118
  end
119
+ callback.fulfill(nil) if fire_and_forget
115
120
 
116
121
  if @tracing_count > 0 && !frames.empty? && guid != 'localUtils' && !remote?
117
122
  @local_utils.add_stack_to_tracing_no_reply(id, frames)
@@ -168,6 +173,7 @@ module Playwright
168
173
  if error && !msg['result']
169
174
  parsed_error = ::Playwright::Error.parse(error['error'])
170
175
  parsed_error.log = msg['log']
176
+ parsed_error.details = msg['errorDetails']
171
177
  callback.reject(parsed_error)
172
178
  else
173
179
  result = replace_guids_with_channels(msg['result'])
@@ -0,0 +1,35 @@
1
+ module Playwright
2
+ # ref: https://github.com/microsoft/playwright/blob/main/packages/playwright-core/src/client/credentials.ts
3
+ define_api_implementation :CredentialsImpl do
4
+ # @param browser_context [ChannelOwners::BrowserContext]
5
+ def initialize(browser_context)
6
+ @browser_context = browser_context
7
+ end
8
+
9
+ def install
10
+ @browser_context.channel.send_message_to_server('credentialsInstall')
11
+ nil
12
+ end
13
+
14
+ def create(rpId, id: nil, userHandle: nil, privateKey: nil, publicKey: nil)
15
+ params = {
16
+ rpId: rpId,
17
+ id: id,
18
+ userHandle: userHandle,
19
+ privateKey: privateKey,
20
+ publicKey: publicKey,
21
+ }.compact
22
+ @browser_context.channel.send_message_to_server('credentialsCreate', params)
23
+ end
24
+
25
+ def get(id: nil, rpId: nil)
26
+ params = { id: id, rpId: rpId }.compact
27
+ @browser_context.channel.send_message_to_server('credentialsGet', params)
28
+ end
29
+
30
+ def delete(id)
31
+ @browser_context.channel.send_message_to_server('credentialsDelete', id: id)
32
+ nil
33
+ end
34
+ end
35
+ end
@@ -31,10 +31,13 @@ module Playwright
31
31
  @stack = stack
32
32
  end
33
33
 
34
- attr_reader :name, :message, :stack
34
+ attr_reader :name, :message, :stack, :raw_log
35
+ # Error details for `expect` failures (received value, timedOut, customErrorMessage).
36
+ attr_accessor :details
35
37
 
36
38
  def log=(log)
37
39
  return unless log
40
+ @raw_log = log
38
41
  format_call_log = log.join("\n - ")
39
42
  @message = "#{@message}\nCall log:\n#{format_call_log}\n"
40
43
  end
@@ -39,7 +39,15 @@ module Playwright
39
39
  end
40
40
 
41
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)}]"
42
+ "internal:testid=[#{encode_test_id_attribute_name(test_id_attribute_name)}=#{escape_for_attribute_selector_or_regex(test_id, true)}]"
43
+ end
44
+
45
+ private def encode_test_id_attribute_name(test_id_attribute_name)
46
+ if test_id_attribute_name.include?(',')
47
+ JSON.generate(test_id_attribute_name)
48
+ else
49
+ test_id_attribute_name
50
+ end
43
51
  end
44
52
 
45
53
  private def get_by_label_selector(text, exact:)
@@ -63,11 +63,12 @@ module Playwright
63
63
  @page.send(:channel).send_message_to_server('screencastChapter', params)
64
64
  end
65
65
 
66
- def show_actions(duration: nil, fontSize: nil, position: nil)
66
+ def show_actions(duration: nil, fontSize: nil, position: nil, cursor: nil)
67
67
  params = {}
68
68
  params[:duration] = duration if duration
69
69
  params[:fontSize] = fontSize if fontSize
70
70
  params[:position] = position if position
71
+ params[:cursor] = cursor if cursor
71
72
  @page.send(:channel).send_message_to_server('screencastShowActions', params)
72
73
  DisposableStub.new { hide_actions }
73
74
  end
@@ -87,6 +88,7 @@ module Playwright
87
88
  private def handle_screencast_frame(event)
88
89
  @on_frame&.call({
89
90
  data: Base64.strict_decode64(event['data']),
91
+ timestamp: event['timestamp'],
90
92
  viewportWidth: event['viewportWidth'],
91
93
  viewportHeight: event['viewportHeight'],
92
94
  })
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Playwright
4
- VERSION = '1.60.0'
5
- COMPATIBLE_PLAYWRIGHT_VERSION = '1.60.0'
4
+ VERSION = '1.61.0'
5
+ COMPATIBLE_PLAYWRIGHT_VERSION = '1.61.1'
6
6
  end