playwright-ruby-client 1.39.0 → 1.40.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/documentation/docs/api/api_request_context.md +1 -2
  3. data/documentation/docs/api/browser.md +10 -14
  4. data/documentation/docs/api/browser_context.md +32 -51
  5. data/documentation/docs/api/browser_type.md +8 -12
  6. data/documentation/docs/api/dialog.md +15 -18
  7. data/documentation/docs/api/download.md +10 -12
  8. data/documentation/docs/api/element_handle.md +9 -7
  9. data/documentation/docs/api/frame.md +28 -55
  10. data/documentation/docs/api/locator.md +24 -23
  11. data/documentation/docs/api/locator_assertions.md +652 -0
  12. data/documentation/docs/api/page.md +53 -103
  13. data/documentation/docs/api/playwright.md +20 -23
  14. data/documentation/docs/api/selectors.md +29 -34
  15. data/documentation/docs/article/guides/rails_integration.md +80 -51
  16. data/documentation/docs/article/guides/rspec_integration.md +59 -0
  17. data/documentation/docs/include/api_coverage.md +43 -0
  18. data/documentation/docusaurus.config.js +1 -1
  19. data/documentation/package.json +7 -7
  20. data/documentation/yarn.lock +4641 -5023
  21. data/lib/playwright/api_response_impl.rb +2 -2
  22. data/lib/playwright/channel.rb +1 -1
  23. data/lib/playwright/channel_owners/api_request_context.rb +12 -3
  24. data/lib/playwright/channel_owners/browser.rb +11 -7
  25. data/lib/playwright/channel_owners/browser_context.rb +35 -15
  26. data/lib/playwright/channel_owners/frame.rb +38 -14
  27. data/lib/playwright/channel_owners/page.rb +29 -16
  28. data/lib/playwright/channel_owners/web_socket.rb +8 -13
  29. data/lib/playwright/connection.rb +18 -2
  30. data/lib/playwright/errors.rb +22 -2
  31. data/lib/playwright/input_files.rb +4 -4
  32. data/lib/playwright/javascript/value_serializer.rb +1 -1
  33. data/lib/playwright/locator_assertions_impl.rb +417 -0
  34. data/lib/playwright/locator_impl.rb +24 -5
  35. data/lib/playwright/test.rb +68 -0
  36. data/lib/playwright/utils.rb +3 -10
  37. data/lib/playwright/version.rb +2 -2
  38. data/lib/playwright/waiter.rb +146 -0
  39. data/lib/playwright.rb +1 -1
  40. data/lib/playwright_api/api_request_context.rb +1 -2
  41. data/lib/playwright_api/browser.rb +2 -2
  42. data/lib/playwright_api/browser_context.rb +3 -3
  43. data/lib/playwright_api/browser_type.rb +2 -1
  44. data/lib/playwright_api/download.rb +1 -2
  45. data/lib/playwright_api/element_handle.rb +4 -1
  46. data/lib/playwright_api/frame.rb +4 -1
  47. data/lib/playwright_api/locator.rb +9 -1
  48. data/lib/playwright_api/locator_assertions.rb +561 -0
  49. data/lib/playwright_api/page.rb +16 -13
  50. data/lib/playwright_api/request.rb +4 -4
  51. data/lib/playwright_api/worker.rb +4 -4
  52. data/sig/playwright.rbs +48 -5
  53. metadata +9 -4
  54. data/lib/playwright/wait_helper.rb +0 -73
@@ -0,0 +1,652 @@
1
+ ---
2
+ sidebar_position: 10
3
+ ---
4
+
5
+ # LocatorAssertions
6
+
7
+
8
+ The LocatorAssertions class provides assertion methods for RSpec like `expect(locator).to have_text("Something")`. Note that we have to explicitly include `playwright/test` and `Playwright::Test::Matchers` for using RSpec matchers.
9
+
10
+ ```ruby
11
+ require 'playwright/test'
12
+
13
+ describe 'your system testing' do
14
+ include Playwright::Test::Matchers
15
+ ```
16
+
17
+ Since the matcher comes with auto-waiting feature, we don't have to describe trivial codes waiting for elements any more :)
18
+
19
+ ```ruby
20
+ page.content = <<~HTML
21
+ <div id="my_status" class="status">Pending</div>
22
+ <button onClick="setTimeout(() => { document.getElementById('my_status').innerText='Something Submitted!!' }, 2000)">Click me</button>
23
+ HTML
24
+
25
+ page.get_by_role("button").click
26
+ expect(page.locator(".status")).to have_text("Submitted") # auto-waiting
27
+ ```
28
+
29
+ ## not_to_be_attached
30
+
31
+ ```ruby
32
+ expect(locator).not_to be_attached(attached: nil, timeout: nil)
33
+ ```
34
+
35
+
36
+ The opposite of [LocatorAssertions#to_be_attached](./locator_assertions#to_be_attached).
37
+
38
+ ## not_to_be_checked
39
+
40
+ ```ruby
41
+ expect(locator).not_to be_checked(timeout: nil)
42
+ ```
43
+
44
+
45
+ The opposite of [LocatorAssertions#to_be_checked](./locator_assertions#to_be_checked).
46
+
47
+ ## not_to_be_disabled
48
+
49
+ ```ruby
50
+ expect(locator).not_to be_disabled(timeout: nil)
51
+ ```
52
+
53
+
54
+ The opposite of [LocatorAssertions#to_be_disabled](./locator_assertions#to_be_disabled).
55
+
56
+ ## not_to_be_editable
57
+
58
+ ```ruby
59
+ expect(locator).not_to be_editable(editable: nil, timeout: nil)
60
+ ```
61
+
62
+
63
+ The opposite of [LocatorAssertions#to_be_editable](./locator_assertions#to_be_editable).
64
+
65
+ ## not_to_be_empty
66
+
67
+ ```ruby
68
+ expect(locator).not_to be_empty(timeout: nil)
69
+ ```
70
+
71
+
72
+ The opposite of [LocatorAssertions#to_be_empty](./locator_assertions#to_be_empty).
73
+
74
+ ## not_to_be_enabled
75
+
76
+ ```ruby
77
+ expect(locator).not_to be_enabled(enabled: nil, timeout: nil)
78
+ ```
79
+
80
+
81
+ The opposite of [LocatorAssertions#to_be_enabled](./locator_assertions#to_be_enabled).
82
+
83
+ ## not_to_be_focused
84
+
85
+ ```ruby
86
+ expect(locator).not_to be_focused(timeout: nil)
87
+ ```
88
+
89
+
90
+ The opposite of [LocatorAssertions#to_be_focused](./locator_assertions#to_be_focused).
91
+
92
+ ## not_to_be_hidden
93
+
94
+ ```ruby
95
+ expect(locator).not_to be_hidden(timeout: nil)
96
+ ```
97
+
98
+
99
+ The opposite of [LocatorAssertions#to_be_hidden](./locator_assertions#to_be_hidden).
100
+
101
+ ## not_to_be_in_viewport
102
+
103
+ ```ruby
104
+ expect(locator).not_to be_in_viewport(ratio: nil, timeout: nil)
105
+ ```
106
+
107
+
108
+ The opposite of [LocatorAssertions#to_be_in_viewport](./locator_assertions#to_be_in_viewport).
109
+
110
+ ## not_to_be_visible
111
+
112
+ ```ruby
113
+ expect(locator).not_to be_visible(timeout: nil, visible: nil)
114
+ ```
115
+
116
+
117
+ The opposite of [LocatorAssertions#to_be_visible](./locator_assertions#to_be_visible).
118
+
119
+ ## not_to_contain_text
120
+
121
+ ```ruby
122
+ expect(locator).not_to contain_text(expected, ignoreCase: nil, timeout: nil, useInnerText: nil)
123
+ ```
124
+
125
+
126
+ The opposite of [LocatorAssertions#to_contain_text](./locator_assertions#to_contain_text).
127
+
128
+ ## not_to_have_attribute
129
+
130
+ ```ruby
131
+ expect(locator).not_to have_attribute(name, value, timeout: nil)
132
+ ```
133
+
134
+
135
+ The opposite of [LocatorAssertions#to_have_attribute](./locator_assertions#to_have_attribute).
136
+
137
+ ## not_to_have_class
138
+
139
+ ```ruby
140
+ expect(locator).not_to have_class(expected, timeout: nil)
141
+ ```
142
+
143
+
144
+ The opposite of [LocatorAssertions#to_have_class](./locator_assertions#to_have_class).
145
+
146
+ ## not_to_have_count
147
+
148
+ ```ruby
149
+ expect(locator).not_to have_count(count, timeout: nil)
150
+ ```
151
+
152
+
153
+ The opposite of [LocatorAssertions#to_have_count](./locator_assertions#to_have_count).
154
+
155
+ ## not_to_have_css
156
+
157
+ ```ruby
158
+ expect(locator).not_to have_css(name, value, timeout: nil)
159
+ ```
160
+
161
+
162
+ The opposite of [LocatorAssertions#to_have_css](./locator_assertions#to_have_css).
163
+
164
+ ## not_to_have_id
165
+
166
+ ```ruby
167
+ expect(locator).not_to have_id(id, timeout: nil)
168
+ ```
169
+
170
+
171
+ The opposite of [LocatorAssertions#to_have_id](./locator_assertions#to_have_id).
172
+
173
+ ## not_to_have_js_property
174
+
175
+ ```ruby
176
+ expect(locator).not_to have_js_property(name, value, timeout: nil)
177
+ ```
178
+
179
+
180
+ The opposite of [LocatorAssertions#to_have_js_property](./locator_assertions#to_have_js_property).
181
+
182
+ ## not_to_have_text
183
+
184
+ ```ruby
185
+ expect(locator).not_to have_text(expected, ignoreCase: nil, timeout: nil, useInnerText: nil)
186
+ ```
187
+
188
+
189
+ The opposite of [LocatorAssertions#to_have_text](./locator_assertions#to_have_text).
190
+
191
+ ## not_to_have_value
192
+
193
+ ```ruby
194
+ expect(locator).not_to have_value(value, timeout: nil)
195
+ ```
196
+
197
+
198
+ The opposite of [LocatorAssertions#to_have_value](./locator_assertions#to_have_value).
199
+
200
+ ## not_to_have_values
201
+
202
+ ```ruby
203
+ expect(locator).not_to have_values(values, timeout: nil)
204
+ ```
205
+
206
+
207
+ The opposite of [LocatorAssertions#to_have_values](./locator_assertions#to_have_values).
208
+
209
+ ## to_be_attached
210
+
211
+ ```ruby
212
+ expect(locator).to be_attached(attached: nil, timeout: nil)
213
+ ```
214
+
215
+
216
+ Ensures that [Locator](./locator) points to an [attached](https://playwright.dev/python/docs/actionability#attached) DOM node.
217
+
218
+ **Usage**
219
+
220
+ ```ruby
221
+ page.content = <<~HTML
222
+ <div id="hidden_status" style="display: none">Pending</div>
223
+ <button onClick="document.getElementById('hidden_status').innerText='Hidden text'">Click me</button>
224
+ HTML
225
+
226
+ page.get_by_role("button").click
227
+ expect(page.get_by_text("Hidden text")).to be_attached
228
+ ```
229
+
230
+ ## to_be_checked
231
+
232
+ ```ruby
233
+ expect(locator).to be_checked(checked: nil, timeout: nil)
234
+ ```
235
+
236
+
237
+ Ensures the [Locator](./locator) points to a checked input.
238
+
239
+ **Usage**
240
+
241
+ ```ruby
242
+ locator = page.get_by_label("Subscribe to newsletter")
243
+ expect(locator).to be_checked
244
+ ```
245
+
246
+ ## to_be_disabled
247
+
248
+ ```ruby
249
+ expect(locator).to be_disabled(timeout: nil)
250
+ ```
251
+
252
+
253
+ Ensures the [Locator](./locator) points to a disabled element. Element is disabled if it has "disabled" attribute
254
+ or is disabled via ['aria-disabled'](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-disabled).
255
+ Note that only native control elements such as HTML `button`, `input`, `select`, `textarea`, `option`, `optgroup`
256
+ can be disabled by setting "disabled" attribute. "disabled" attribute on other elements is ignored
257
+ by the browser.
258
+
259
+ **Usage**
260
+
261
+ ```ruby
262
+ locator = page.locator("button.submit")
263
+ locator.click
264
+ expect(locator).to be_disabled
265
+ ```
266
+
267
+ ## to_be_editable
268
+
269
+ ```ruby
270
+ expect(locator).to be_editable(editable: nil, timeout: nil)
271
+ ```
272
+
273
+
274
+ Ensures the [Locator](./locator) points to an editable element.
275
+
276
+ **Usage**
277
+
278
+ ```ruby
279
+ locator = page.get_by_role("textbox")
280
+ expect(locator).to be_editable
281
+ ```
282
+
283
+ ## to_be_empty
284
+
285
+ ```ruby
286
+ expect(locator).to be_empty(timeout: nil)
287
+ ```
288
+
289
+
290
+ Ensures the [Locator](./locator) points to an empty editable element or to a DOM node that has no text.
291
+
292
+ **Usage**
293
+
294
+ ```ruby
295
+ locator = page.locator("div.warning")
296
+ expect(locator).to be_empty
297
+ ```
298
+
299
+ ## to_be_enabled
300
+
301
+ ```ruby
302
+ expect(locator).to be_enabled(enabled: nil, timeout: nil)
303
+ ```
304
+
305
+
306
+ Ensures the [Locator](./locator) points to an enabled element.
307
+
308
+ **Usage**
309
+
310
+ ```ruby
311
+ locator = page.locator("button.submit")
312
+ expect(locator).to be_enabled
313
+ ```
314
+
315
+ ## to_be_focused
316
+
317
+ ```ruby
318
+ expect(locator).to be_focused(timeout: nil)
319
+ ```
320
+
321
+
322
+ Ensures the [Locator](./locator) points to a focused DOM node.
323
+
324
+ **Usage**
325
+
326
+ ```ruby
327
+ locator = page.get_by_role("textbox")
328
+ expect(locator).to be_focused
329
+ ```
330
+
331
+ ## to_be_hidden
332
+
333
+ ```ruby
334
+ expect(locator).to be_hidden(timeout: nil)
335
+ ```
336
+
337
+
338
+ Ensures that [Locator](./locator) either does not resolve to any DOM node, or resolves to a [non-visible](https://playwright.dev/python/docs/actionability#visible) one.
339
+
340
+ **Usage**
341
+
342
+ ```ruby
343
+ locator = page.locator(".my-element")
344
+ expect(locator).to be_hidden
345
+ ```
346
+
347
+ ## to_be_in_viewport
348
+
349
+ ```ruby
350
+ expect(locator).to be_in_viewport(ratio: nil, timeout: nil)
351
+ ```
352
+
353
+
354
+ Ensures the [Locator](./locator) points to an element that intersects viewport, according to the [intersection observer API](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API).
355
+
356
+ **Usage**
357
+
358
+ ```ruby
359
+ locator = page.get_by_role("button")
360
+ # Make sure at least some part of element intersects viewport.
361
+ expect(locator).to be_in_viewport
362
+ # Make sure element is fully outside of viewport.
363
+ expect(locator).not_to be_in_viewport
364
+ # Make sure that at least half of the element intersects viewport.
365
+ expect(locator).to be_in_viewport(ratio: 0.5)
366
+ ```
367
+
368
+ ## to_be_visible
369
+
370
+ ```ruby
371
+ expect(locator).to be_visible(timeout: nil, visible: nil)
372
+ ```
373
+
374
+
375
+ Ensures that [Locator](./locator) points to an [attached](https://playwright.dev/python/docs/actionability#attached) and [visible](https://playwright.dev/python/docs/actionability#visible) DOM node.
376
+
377
+ To check that at least one element from the list is visible, use [Locator#first](./locator#first).
378
+
379
+ **Usage**
380
+
381
+ ```ruby
382
+ # A specific element is visible.
383
+ expect(page.get_by_text("Welcome")).to be_visible
384
+
385
+ # At least one item in the list is visible.
386
+ expect(page.get_by_test_id("todo-item").first).to be_visible
387
+
388
+ # At least one of the two elements is visible, possibly both.
389
+ expect(
390
+ page.get_by_role('button', name: 'Sign in').or(page.get_by_role('button', name: 'Sign up')).first
391
+ ).to be_visible
392
+ ```
393
+
394
+ ## to_contain_text
395
+
396
+ ```ruby
397
+ expect(locator).to contain_text(expected, ignoreCase: nil, timeout: nil, useInnerText: nil)
398
+ ```
399
+
400
+
401
+ Ensures the [Locator](./locator) points to an element that contains the given text. You can use regular expressions for the value as well.
402
+
403
+ **Details**
404
+
405
+ When `expected` parameter is a string, Playwright will normalize whitespaces and line breaks both in the actual text and
406
+ in the expected string before matching. When regular expression is used, the actual text is matched as is.
407
+
408
+ **Usage**
409
+
410
+ ```ruby
411
+ locator = page.locator('.title')
412
+ expect(locator).to contain_text("substring")
413
+ expect(locator).to contain_text(/\d messages/)
414
+ ```
415
+
416
+ If you pass an array as an expected value, the expectations are:
417
+ 1. Locator resolves to a list of elements.
418
+ 1. Elements from a **subset** of this list contain text from the expected array, respectively.
419
+ 1. The matching subset of elements has the same order as the expected array.
420
+ 1. Each text value from the expected array is matched by some element from the list.
421
+
422
+ For example, consider the following list:
423
+
424
+ ```html
425
+ <ul>
426
+ <li>Item Text 1</li>
427
+ <li>Item Text 2</li>
428
+ <li>Item Text 3</li>
429
+ </ul>
430
+ ```
431
+
432
+ Let's see how we can use the assertion:
433
+
434
+ ```ruby
435
+ # ✓ Contains the right items in the right order
436
+ expect(page.locator("ul > li")).to contain_text(["Text 1", "Text 3", "Text 4"])
437
+
438
+ # ✖ Wrong order
439
+ expect(page.locator("ul > li")).to contain_text(["Text 3", "Text 2"])
440
+
441
+ # ✖ No item contains this text
442
+ expect(page.locator("ul > li")).to contain_text(["Some 33"])
443
+
444
+ # ✖ Locator points to the outer list element, not to the list items
445
+ expect(page.locator("ul")).to contain_text(["Text 3"])
446
+ ```
447
+
448
+ ## to_have_attribute
449
+
450
+ ```ruby
451
+ expect(locator).to have_attribute(name, value, ignoreCase: nil, timeout: nil)
452
+ ```
453
+
454
+
455
+ Ensures the [Locator](./locator) points to an element with given attribute.
456
+
457
+ **Usage**
458
+
459
+ ```ruby
460
+ locator = page.locator("input")
461
+ expect(locator).to have_attribute("type", "text")
462
+ ```
463
+
464
+ ## to_have_class
465
+
466
+ ```ruby
467
+ expect(locator).to have_class(expected, timeout: nil)
468
+ ```
469
+
470
+
471
+ Ensures the [Locator](./locator) points to an element with given CSS classes. This needs to be a full match
472
+ or using a relaxed regular expression.
473
+
474
+ **Usage**
475
+
476
+ ```html
477
+ <div class='selected row' id='component'></div>
478
+ ```
479
+
480
+ ```ruby
481
+ locator = page.locator("#component")
482
+ expect(locator).to have_class(/selected/)
483
+ expect(locator).to have_class("selected row")
484
+ ```
485
+
486
+ Note that if array is passed as an expected value, entire lists of elements can be asserted:
487
+
488
+ ```ruby
489
+ locator = page.locator("list > .component")
490
+ expect(locator).to have_class(["component", "component selected", "component"])
491
+ ```
492
+
493
+ ## to_have_count
494
+
495
+ ```ruby
496
+ expect(locator).to have_count(count, timeout: nil)
497
+ ```
498
+
499
+
500
+ Ensures the [Locator](./locator) resolves to an exact number of DOM nodes.
501
+
502
+ **Usage**
503
+
504
+ ```ruby
505
+ locator = page.locator("list > .component")
506
+ expect(locator).to have_count(3)
507
+ ```
508
+
509
+ ## to_have_css
510
+
511
+ ```ruby
512
+ expect(locator).to have_css(name, value, timeout: nil)
513
+ ```
514
+
515
+
516
+ Ensures the [Locator](./locator) resolves to an element with the given computed CSS style.
517
+
518
+ **Usage**
519
+
520
+ ```ruby
521
+ locator = page.get_by_role("button")
522
+ expect(locator).to have_css("display", "flex")
523
+ ```
524
+
525
+ ## to_have_id
526
+
527
+ ```ruby
528
+ expect(locator).to have_id(id, timeout: nil)
529
+ ```
530
+
531
+
532
+ Ensures the [Locator](./locator) points to an element with the given DOM Node ID.
533
+
534
+ **Usage**
535
+
536
+ ```ruby
537
+ locator = page.get_by_role("textbox")
538
+ expect(locator).to have_id("lastname")
539
+ ```
540
+
541
+ ## to_have_js_property
542
+
543
+ ```ruby
544
+ expect(locator).to have_js_property(name, value, timeout: nil)
545
+ ```
546
+
547
+
548
+ Ensures the [Locator](./locator) points to an element with given JavaScript property. Note that this property can be
549
+ of a primitive type as well as a plain serializable JavaScript object.
550
+
551
+ **Usage**
552
+
553
+ ```ruby
554
+ locator = page.locator(".component")
555
+ expect(locator).to have_js_property("loaded", true)
556
+ ```
557
+
558
+ ## to_have_text
559
+
560
+ ```ruby
561
+ expect(locator).to have_text(expected, ignoreCase: nil, timeout: nil, useInnerText: nil)
562
+ ```
563
+
564
+
565
+ Ensures the [Locator](./locator) points to an element with the given text. You can use regular expressions for the value as well.
566
+
567
+ **Details**
568
+
569
+ When `expected` parameter is a string, Playwright will normalize whitespaces and line breaks both in the actual text and
570
+ in the expected string before matching. When regular expression is used, the actual text is matched as is.
571
+
572
+ **Usage**
573
+
574
+ ```ruby
575
+ locator = page.locator(".title")
576
+ expect(locator).to have_text(/Welcome, Test User/)
577
+ expect(locator).to have_text(/Welcome, .*/)
578
+ ```
579
+
580
+ If you pass an array as an expected value, the expectations are:
581
+ 1. Locator resolves to a list of elements.
582
+ 1. The number of elements equals the number of expected values in the array.
583
+ 1. Elements from the list have text matching expected array values, one by one, in order.
584
+
585
+ For example, consider the following list:
586
+
587
+ ```html
588
+ <ul>
589
+ <li>Text 1</li>
590
+ <li>Text 2</li>
591
+ <li>Text 3</li>
592
+ </ul>
593
+ ```
594
+
595
+ Let's see how we can use the assertion:
596
+
597
+ ```ruby
598
+ # ✓ Has the right items in the right order
599
+ expect(page.locator("ul > li")).to have_text(["Text 1", "Text 2", "Text 3"])
600
+
601
+ # ✖ Wrong order
602
+ expect(page.locator("ul > li")).to have_text(["Text 3", "Text 2", "Text 1"])
603
+
604
+ # ✖ Last item does not match
605
+ expect(page.locator("ul > li")).to have_text(["Text 1", "Text 2", "Text"])
606
+
607
+ # ✖ Locator points to the outer list element, not to the list items
608
+ expect(page.locator("ul")).to have_text(["Text 1", "Text 2", "Text 3"])
609
+ ```
610
+
611
+ ## to_have_value
612
+
613
+ ```ruby
614
+ expect(locator).to have_value(value, timeout: nil)
615
+ ```
616
+
617
+
618
+ Ensures the [Locator](./locator) points to an element with the given input value. You can use regular expressions for the value as well.
619
+
620
+ **Usage**
621
+
622
+ ```ruby
623
+ locator = page.locator("input[type=number]")
624
+ expect(locator).to have_value(/^[0-9]$/)
625
+ ```
626
+
627
+ ## to_have_values
628
+
629
+ ```ruby
630
+ expect(locator).to have_values(values, timeout: nil)
631
+ ```
632
+
633
+
634
+ Ensures the [Locator](./locator) points to multi-select/combobox (i.e. a `select` with the `multiple` attribute) and the specified values are selected.
635
+
636
+ **Usage**
637
+
638
+ For example, given the following element:
639
+
640
+ ```html
641
+ <select id="favorite-colors" multiple>
642
+ <option value="R">Red</option>
643
+ <option value="G">Green</option>
644
+ <option value="B">Blue</option>
645
+ </select>
646
+ ```
647
+
648
+ ```ruby
649
+ locator = page.locator("id=favorite-colors")
650
+ locator.select_option(["R", "G"])
651
+ expect(locator).to have_values([/R/, /G/])
652
+ ```