playwright-ruby-client 1.26.0 → 1.27.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 (35) 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 +1 -1
  6. data/documentation/docs/api/file_chooser.md +1 -1
  7. data/documentation/docs/api/frame.md +103 -4
  8. data/documentation/docs/api/frame_locator.md +104 -4
  9. data/documentation/docs/api/locator.md +121 -6
  10. data/documentation/docs/api/page.md +118 -9
  11. data/documentation/docs/api/tracing.md +1 -1
  12. data/documentation/docs/article/guides/rails_integration_with_null_driver.md +59 -0
  13. data/documentation/docs/include/api_coverage.md +29 -0
  14. data/lib/playwright/channel_owners/frame.rb +4 -0
  15. data/lib/playwright/channel_owners/page.rb +2 -0
  16. data/lib/playwright/channel_owners/selectors.rb +4 -0
  17. data/lib/playwright/frame_locator_impl.rb +6 -2
  18. data/lib/playwright/locator_impl.rb +7 -31
  19. data/lib/playwright/locator_utils.rb +142 -0
  20. data/lib/playwright/version.rb +2 -2
  21. data/lib/playwright_api/api_request_context.rb +80 -2
  22. data/lib/playwright_api/browser_context.rb +3 -3
  23. data/lib/playwright_api/download.rb +1 -1
  24. data/lib/playwright_api/element_handle.rb +1 -1
  25. data/lib/playwright_api/file_chooser.rb +1 -1
  26. data/lib/playwright_api/frame.rb +78 -4
  27. data/lib/playwright_api/frame_locator.rb +78 -3
  28. data/lib/playwright_api/locator.rb +94 -5
  29. data/lib/playwright_api/page.rb +92 -9
  30. data/lib/playwright_api/request.rb +4 -4
  31. data/lib/playwright_api/response.rb +4 -4
  32. data/lib/playwright_api/selectors.rb +11 -0
  33. data/lib/playwright_api/tracing.rb +1 -1
  34. data/lib/playwright_api/worker.rb +4 -4
  35. metadata +4 -3
@@ -276,6 +276,20 @@ def drag_and_drop(
276
276
  trial: nil)
277
277
  ```
278
278
 
279
+ This method drags the source element to the target element. It will first move to the source element, perform a
280
+ `mousedown`, then move to the target element and perform a `mouseup`.
281
+
282
+ ```ruby
283
+ page.drag_and_drop("#source", "#target")
284
+ # or specify exact positions relative to the top-left corners of the elements:
285
+ page.drag_and_drop(
286
+ "#source",
287
+ "#target",
288
+ sourcePosition: { x: 34, y: 7 },
289
+ targetPosition: { x: 10, y: 20 },
290
+ )
291
+ ```
292
+
279
293
 
280
294
 
281
295
  ## emulate_media
@@ -592,7 +606,7 @@ that iframe. Following snippet locates element with text "Submit" in the iframe
592
606
  id="my-frame">`:
593
607
 
594
608
  ```ruby
595
- locator = page.frame_locator("#my-iframe").locator("text=Submit")
609
+ locator = page.frame_locator("#my-iframe").get_by_text("Submit")
596
610
  locator.click
597
611
  ```
598
612
 
@@ -614,6 +628,103 @@ def get_attribute(selector, name, strict: nil, timeout: nil)
614
628
 
615
629
  Returns element attribute value.
616
630
 
631
+ ## get_by_alt_text
632
+
633
+ ```
634
+ def get_by_alt_text(text, exact: nil)
635
+ ```
636
+
637
+ Allows locating elements by their alt text. For example, this method will find the image by alt text "Castle":
638
+
639
+ ```html
640
+ <img alt='Castle'>
641
+ ```
642
+
643
+
644
+ ## get_by_label
645
+
646
+ ```
647
+ def get_by_label(text, exact: nil)
648
+ ```
649
+
650
+ Allows locating input elements by the text of the associated label. For example, this method will find the input by
651
+ label text Password in the following DOM:
652
+
653
+ ```html
654
+ <label for="password-input">Password:</label>
655
+ <input id="password-input">
656
+ ```
657
+
658
+
659
+ ## get_by_placeholder
660
+
661
+ ```
662
+ def get_by_placeholder(text, exact: nil)
663
+ ```
664
+
665
+ Allows locating input elements by the placeholder text. For example, this method will find the input by placeholder
666
+ "Country":
667
+
668
+ ```html
669
+ <input placeholder="Country">
670
+ ```
671
+
672
+
673
+ ## get_by_role
674
+
675
+ ```
676
+ def get_by_role(
677
+ role,
678
+ checked: nil,
679
+ disabled: nil,
680
+ expanded: nil,
681
+ includeHidden: nil,
682
+ level: nil,
683
+ name: nil,
684
+ pressed: nil,
685
+ selected: nil)
686
+ ```
687
+
688
+ Allows locating elements by their [ARIA role](https://www.w3.org/TR/wai-aria-1.2/#roles),
689
+ [ARIA attributes](https://www.w3.org/TR/wai-aria-1.2/#aria-attributes) and
690
+ [accessible name](https://w3c.github.io/accname/#dfn-accessible-name). Note that role selector **does not replace**
691
+ accessibility audits and conformance tests, but rather gives early feedback about the ARIA guidelines.
692
+
693
+ Note that many html elements have an implicitly
694
+ [defined role](https://w3c.github.io/html-aam/#html-element-role-mappings) that is recognized by the role selector. You
695
+ can find all the [supported roles here](https://www.w3.org/TR/wai-aria-1.2/#role_definitions). ARIA guidelines **do not
696
+ recommend** duplicating implicit roles and attributes by setting `role` and/or `aria-*` attributes to default values.
697
+
698
+ ## get_by_test_id
699
+
700
+ ```
701
+ def get_by_test_id(testId)
702
+ ```
703
+
704
+ Locate element by the test id. By default, the `data-testid` attribute is used as a test id. Use
705
+ [Selectors#set_test_id_attribute](./selectors#set_test_id_attribute) to configure a different test id attribute if necessary.
706
+
707
+ ## get_by_text
708
+
709
+ ```
710
+ def get_by_text(text, exact: nil)
711
+ ```
712
+
713
+ Allows locating elements that contain given text.
714
+
715
+ ## get_by_title
716
+
717
+ ```
718
+ def get_by_title(text, exact: nil)
719
+ ```
720
+
721
+ Allows locating elements by their title. For example, this method will find the button by its title "Submit":
722
+
723
+ ```html
724
+ <button title='Place the order'>Order Now</button>
725
+ ```
726
+
727
+
617
728
  ## go_back
618
729
 
619
730
  ```
@@ -780,14 +891,12 @@ considered not visible.
780
891
  def locator(selector, has: nil, hasText: nil)
781
892
  ```
782
893
 
783
- The method returns an element locator that can be used to perform actions on the page. Locator is resolved to the
784
- element immediately before performing an action, so a series of actions on the same locator can in fact be performed on
785
- different DOM elements. That would happen if the DOM structure between those actions has changed.
894
+ The method returns an element locator that can be used to perform actions on this page / frame. Locator is resolved to
895
+ the element immediately before performing an action, so a series of actions on the same locator can in fact be performed
896
+ on different DOM elements. That would happen if the DOM structure between those actions has changed.
786
897
 
787
898
  [Learn more about locators](https://playwright.dev/python/docs/locators).
788
899
 
789
- Shortcut for main frame's [Frame#locator](./frame#locator).
790
-
791
900
  ## main_frame
792
901
 
793
902
  ```
@@ -1364,7 +1473,7 @@ value. Will throw an error if the page is closed before the event is fired. Retu
1364
1473
 
1365
1474
  ```ruby
1366
1475
  frame = page.expect_event("framenavigated") do
1367
- page.click("button")
1476
+ page.get_by_role("button")
1368
1477
  end
1369
1478
  ```
1370
1479
 
@@ -1415,13 +1524,13 @@ This resolves when the page reaches a required load state, `load` by default. Th
1415
1524
  when this method is called. If current document has already reached the required state, resolves immediately.
1416
1525
 
1417
1526
  ```ruby
1418
- page.click("button") # click triggers navigation.
1527
+ page.get_by_role("button").click # click triggers navigation.
1419
1528
  page.wait_for_load_state # the promise resolves after "load" event.
1420
1529
  ```
1421
1530
 
1422
1531
  ```ruby
1423
1532
  popup = page.expect_popup do
1424
- page.click("button") # click triggers a popup.
1533
+ page.get_by_role("button").click # click triggers a popup.
1425
1534
  end
1426
1535
 
1427
1536
  # Following resolves after "domcontentloaded" event.
@@ -58,7 +58,7 @@ page = context.new_page
58
58
  page.goto("https://playwright.dev")
59
59
 
60
60
  context.tracing.start_chunk
61
- page.locator("text=Get Started").click
61
+ page.get_by_text("Get Started").click
62
62
  # Everything between start_chunk and stop_chunk will be recorded in the trace.
63
63
  context.tracing.stop_chunk(path: "trace1.zip")
64
64
 
@@ -84,3 +84,62 @@ describe 'example', driver: :null do
84
84
  end
85
85
  end
86
86
  ```
87
+
88
+ ## Minitest Usage
89
+
90
+ We can do something similar with the default Rails setup using Minitest. Here's the same example written with Minitest:
91
+
92
+ ```rb
93
+ # test/application_system_test_case.rb
94
+
95
+ require 'playwright'
96
+
97
+ class CapybaraNullDriver < Capybara::Driver::Base
98
+ def needs_server?
99
+ true
100
+ end
101
+ end
102
+
103
+ Capybara.register_driver(:null) { CapybaraNullDriver.new }
104
+
105
+ class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
106
+ driven_by :null
107
+
108
+ def self.playwright
109
+ @playwright ||= Playwright.create(playwright_cli_executable_path: Rails.root.join("node_modules/.bin/playwright"))
110
+ end
111
+
112
+ def before_setup
113
+ super
114
+ base_url = Capybara.current_session.server.base_url
115
+ @playwright_browser = self.class.playwright.playwright.chromium.launch(headless: false)
116
+ @playwright_page = @playwright_browser.new_page(baseURL: base_url)
117
+ end
118
+
119
+ def after_teardown
120
+ super
121
+ @browser.close
122
+ end
123
+ end
124
+ ```
125
+
126
+ And here is the same test:
127
+
128
+ ```rb
129
+ require "application_system_test_case"
130
+
131
+ class ExampleTest < ApplicationSystemTestCase
132
+ def setup
133
+ @user = User.create!
134
+ @page = @playwright_page
135
+ end
136
+
137
+ test 'can browse' do
138
+ @page.goto("/tests/#{user.id}")
139
+ @page.wait_for_selector('input').type('hoge')
140
+ @page.keyboard.press('Enter')
141
+
142
+ assert @page.text_content('#content').include?('hoge')
143
+ end
144
+ end
145
+ ```
@@ -159,6 +159,13 @@
159
159
  * frame_element
160
160
  * frame_locator
161
161
  * get_attribute
162
+ * get_by_alt_text
163
+ * get_by_label
164
+ * get_by_placeholder
165
+ * get_by_role
166
+ * get_by_test_id
167
+ * get_by_text
168
+ * get_by_title
162
169
  * goto
163
170
  * hover
164
171
  * inner_html
@@ -204,6 +211,7 @@
204
211
  ## Selectors
205
212
 
206
213
  * register
214
+ * ~~set_test_id_attribute~~
207
215
 
208
216
  ## ConsoleMessage
209
217
 
@@ -258,6 +266,13 @@
258
266
  * frame_locator
259
267
  * frames
260
268
  * get_attribute
269
+ * get_by_alt_text
270
+ * get_by_label
271
+ * get_by_placeholder
272
+ * get_by_role
273
+ * get_by_test_id
274
+ * get_by_text
275
+ * get_by_title
261
276
  * go_back
262
277
  * go_forward
263
278
  * goto
@@ -423,6 +438,13 @@
423
438
  * focus
424
439
  * frame_locator
425
440
  * get_attribute
441
+ * get_by_alt_text
442
+ * get_by_label
443
+ * get_by_placeholder
444
+ * get_by_role
445
+ * get_by_test_id
446
+ * get_by_text
447
+ * get_by_title
426
448
  * highlight
427
449
  * hover
428
450
  * inner_html
@@ -455,6 +477,13 @@
455
477
 
456
478
  * first
457
479
  * frame_locator
480
+ * get_by_alt_text
481
+ * get_by_label
482
+ * get_by_placeholder
483
+ * get_by_role
484
+ * get_by_test_id
485
+ * get_by_text
486
+ * get_by_title
458
487
  * last
459
488
  * locator
460
489
  * nth
@@ -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'])
@@ -1,9 +1,11 @@
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
 
9
11
  private def after_initialize
@@ -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
@@ -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,25 @@
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
+ text_selector = "text=#{escape_for_text_selector(hasText, false)}"
15
+ selector_scopes << "internal:has=#{text_selector.to_json}"
40
16
  end
41
17
 
42
18
  if has
43
19
  unless same_frame?(has)
44
20
  raise DifferentFrameError.new
45
21
  end
46
- selector_scopes << "has=#{has.send(:selector_json)}"
22
+ selector_scopes << "internal:has=#{has.send(:selector_json)}"
47
23
  end
48
24
 
49
25
  @selector = selector_scopes.join(' >> ')
@@ -0,0 +1,142 @@
1
+ module Playwright
2
+ module LocatorUtils
3
+ def get_by_test_id(test_id)
4
+ locator(get_by_test_id_selector(test_id))
5
+ end
6
+
7
+ def get_by_alt_text(text, exact: false)
8
+ locator(get_by_alt_text_selector(text, exact: exact))
9
+ end
10
+
11
+ def get_by_label(text, exact: false)
12
+ locator(get_by_label_selector(text, exact: exact))
13
+ end
14
+
15
+ def get_by_placeholder(text, exact: false)
16
+ locator(get_by_placeholder_selector(text, exact: exact))
17
+ end
18
+
19
+ def get_by_text(text, exact: false)
20
+ locator(get_by_text_selector(text, exact: exact))
21
+ end
22
+
23
+ def get_by_title(text, exact: false)
24
+ locator(get_by_title_selector(text, exact: exact))
25
+ end
26
+
27
+ def get_by_role(role, **options)
28
+ locator(get_by_role_selector(role, **(options.compact)))
29
+ end
30
+
31
+ # set from Playwright::Selectors#test_id_attribute=
32
+ @test_id_attribute_name = 'data-testid'
33
+
34
+ private def get_by_attribute_text_selector(attr_name, text, exact: false)
35
+ "internal:attr=[#{attr_name}=#{escape_for_attribute_selector_or_regex(text, exact)}]"
36
+ end
37
+
38
+ private def get_by_test_id_selector(test_id)
39
+ get_by_attribute_text_selector(
40
+ ::Playwright::LocatorUtils.instance_variable_get(:@test_id_attribute_name),
41
+ test_id,
42
+ exact: 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
+ "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, false)]},
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
+ "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 = text.gsub(/["]/, '\\"')
107
+ return "\"#{_text}\""
108
+ end
109
+
110
+ if text.include?('"') || text.include?('>>') || text.start_with?('/')
111
+ _text = escape_for_regex(text).gsub(/\s+/, '\\s+')
112
+ return "/#{_text}/i"
113
+ end
114
+
115
+ text
116
+ end
117
+
118
+ # @param text [Regexp|String]
119
+ private def escape_for_attribute_selector_or_regex(text, exact)
120
+ if text.is_a?(Regexp)
121
+ regex = JavaScript::Regex.new(text)
122
+ "/#{regex.source}/#{regex.flag}"
123
+ else
124
+ escape_for_attribute_selector(text, exact)
125
+ end
126
+ end
127
+
128
+ # @param text [String]
129
+ private def escape_for_attribute_selector(text, exact)
130
+ # TODO: this should actually be
131
+ # cssEscape(value).replace(/\\ /g, ' ')
132
+ # However, our attribute selectors do not conform to CSS parsing spec,
133
+ # so we escape them differently.
134
+ _text = text.gsub(/["]/, '\\"')
135
+ if exact
136
+ "\"#{_text}\""
137
+ else
138
+ "\"#{_text}\"i"
139
+ end
140
+ end
141
+ end
142
+ 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.27.0'
5
+ COMPATIBLE_PLAYWRIGHT_VERSION = '1.27.0'
6
6
  end