playwright-ruby-client 1.26.0 → 1.27.0

Sign up to get free protection for your applications and to get access to all the features.
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