capybara-playwright-driver 0.5.8 → 0.5.9

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fe14f82e64d08efc962dda883c9c4a6b54b794c75bb5458c522fbf88e839267a
4
- data.tar.gz: 59156f6e1a5db3dd787b1342cafd5c9f8aca74df33042013304cdd029875e64c
3
+ metadata.gz: a319465a40c53ac5b47d86ccd4d15136c20dcf7f6dde6fe17b3906d44cb2fd94
4
+ data.tar.gz: 1e38fb6020a87a07e6eaa8e997084b3abd98a0a8d5cbec4e92819a960c525f62
5
5
  SHA512:
6
- metadata.gz: 3bbf39026f4bc9dfdce2c319510c709e00b6eb5a890e8057566f57a2b804e2890ff3ee97d67a8ba5da3fe7ede58c526f4b239cd425fe6ac47f049075a83a0a7d
7
- data.tar.gz: 813ccb3ce551cb1a3484d261096d8847d045cd694b6dc8275ba27f4012fab798ed23ad2bf36f2996818b653bf6ed4d0213d8f7c9f131cccf1c242450ea86327c
6
+ metadata.gz: 5d1d8aeccb05f0c667c3b1a617cb1d2f19eb7fa096788a46b81985730f1a75b6ee7196ad952d2152c173b7401980090f738db399503b3ddceac097ced637678c
7
+ data.tar.gz: 141fd5024a9d322faf9031951e0f74a61426af350ed06d73017b2db90ec5b866e1af9b17edd5017e5c35f79f198cfa5916b220afcf621db9a2538c5fb50f443b
data/AGENTS.md ADDED
@@ -0,0 +1,107 @@
1
+ # AGENTS.md
2
+
3
+ ## Ruby Style
4
+ - This repository is Ruby-first. Prefer Ruby naming and control flow over Python- or Java-style ceremony.
5
+ - Use names that describe the role in the domain. Avoid vague plumbing names like `context`, `handler`, `data`, or `control` unless they are the clearest possible name.
6
+ - Boolean-returning methods must end with `?`.
7
+ - Prefer `find`, `any?`, early return, and guard clauses over `value = nil` plus later reassignment.
8
+ - Do not add `attr_reader` for every instance variable in a small internal object. If the state is only used internally, access the instance variable directly.
9
+ - If an object is already scoped to one responsibility, avoid redundant prefixes in every private method name.
10
+ - Do not prefix Ruby private methods with `_`. Use `private` for visibility and a normal method name for intent.
11
+
12
+ ## Naming Examples
13
+
14
+ Bad:
15
+
16
+ ```ruby
17
+ def _playwright_try_get_by_label(locator)
18
+ # ...
19
+ end
20
+ ```
21
+
22
+ Good:
23
+
24
+ ```ruby
25
+ def find_by_label(locator)
26
+ # ...
27
+ end
28
+ ```
29
+
30
+ Bad:
31
+
32
+ ```ruby
33
+ def click_associated_label(control)
34
+ return true if control.evaluate("el => !!el.checked")
35
+ end
36
+ ```
37
+
38
+ Good:
39
+
40
+ ```ruby
41
+ def click_associated_label?(playwright_locator)
42
+ return true if playwright_locator.evaluate("el => !!el.checked")
43
+ end
44
+ ```
45
+
46
+ Bad:
47
+
48
+ ```ruby
49
+ def initialize(node_context:, selector:, locator:, checked:)
50
+ @node_context = node_context
51
+ @selector = selector
52
+ @locator = locator
53
+ @checked = checked
54
+ end
55
+ ```
56
+
57
+ Good:
58
+
59
+ ```ruby
60
+ def initialize(node:, node_type:, locator:, checked:)
61
+ @node = node
62
+ @node_type = node_type
63
+ @locator = locator
64
+ @checked = checked
65
+ end
66
+ ```
67
+
68
+ Bad:
69
+
70
+ ```ruby
71
+ control = nil
72
+
73
+ candidates.each do |candidate|
74
+ next unless matches?(candidate)
75
+
76
+ control = candidate
77
+ break
78
+ end
79
+
80
+ control
81
+ ```
82
+
83
+ Good:
84
+
85
+ ```ruby
86
+ candidates.find do |element_handle|
87
+ matches?(element_handle)
88
+ end
89
+ ```
90
+
91
+ Bad:
92
+
93
+ ```ruby
94
+ attr_reader :node, :node_type, :locator, :checked
95
+ ```
96
+
97
+ Good:
98
+
99
+ ```ruby
100
+ def click_associated_label?(playwright_locator)
101
+ return true if playwright_locator.evaluate("el => !!el.checked") == @checked
102
+ end
103
+ ```
104
+
105
+ ## General Guidance
106
+ - Prefer concise Ruby that reads top-to-bottom without carrying unnecessary temporary state.
107
+ - Prefer small private helper objects only when they reduce complexity. Once extracted, give them Ruby-like method names and keep their public surface minimal.
@@ -48,7 +48,7 @@ module Capybara
48
48
  private def create_page(browser_context)
49
49
  browser_context.new_page.tap do |page|
50
50
  page.on('close', -> {
51
- if @playwright_page
51
+ if @playwright_page&.guid == page.guid
52
52
  @playwright_page = nil
53
53
  end
54
54
  })
@@ -31,30 +31,127 @@ module Capybara
31
31
  Node::Element.prepend(WithElementHandlePatch)
32
32
 
33
33
  module NodeActionsAllowLabelClickPatch
34
+ class SelectableElementHandler
35
+ def initialize(node:, node_type:, locator:, checked:)
36
+ @node = node
37
+ @node_type = node_type
38
+ @locator = locator
39
+ @checked = checked
40
+ end
41
+
42
+ def set_checked_state_via_label?
43
+ playwright_checkable = find_playwright_element_handle_by_non_label_locator
44
+ playwright_checkable ||= playwright_locator_by_label unless @locator.nil?
45
+ return false unless playwright_checkable
46
+
47
+ click_associated_label?(playwright_checkable)
48
+ rescue Capybara::ElementNotFound, Capybara::ExpectationNotMet, ::Playwright::Error
49
+ false
50
+ end
51
+
52
+ private
53
+
54
+ def playwright_locator_by_label
55
+ driver.with_playwright_page do |playwright_page|
56
+ return playwright_page.get_by_label(@locator.to_s)
57
+ end
58
+ end
59
+
60
+ def click_associated_label?(playwright_element_handle_or_locator)
61
+ return true if playwright_element_handle_or_locator.evaluate('el => !!el.checked') == @checked
62
+
63
+ label_element_handle = playwright_element_handle_or_locator.evaluate_handle('(el) => (el.labels && el.labels[0]) || el.closest("label") || null')
64
+ return false unless label_element_handle.is_a?(::Playwright::ElementHandle)
65
+
66
+ label_element_handle.click
67
+
68
+ playwright_element_handle_or_locator.evaluate('el => !!el.checked') == @checked
69
+ end
70
+
71
+ def find_playwright_element_handle_by_non_label_locator
72
+ return nil if @locator.nil?
73
+
74
+ locator_string = @locator.to_s
75
+ test_id_attr = session_options.test_id&.to_s
76
+
77
+ driver.with_playwright_page do |playwright_page|
78
+ return non_label_playwright_element_handle_candidates(playwright_page).find do |element_handle|
79
+ attribute_values = element_handle.evaluate(<<~JAVASCRIPT, arg: test_id_attr)
80
+ (el, testIdAttr) => ({
81
+ id: el.id || '',
82
+ name: el.getAttribute('name') || '',
83
+ testId: testIdAttr ? (el.getAttribute(testIdAttr) || '') : '',
84
+ })
85
+ JAVASCRIPT
86
+ [attribute_values['id'], attribute_values['name'], attribute_values['testId']].include?(locator_string)
87
+ end
88
+ end
89
+ rescue ::Playwright::Error
90
+ nil
91
+ end
92
+
93
+ def non_label_playwright_element_handle_candidates(playwright_page)
94
+ input_type =
95
+ case @node_type
96
+ when :checkbox
97
+ 'checkbox'
98
+ when :radio_button
99
+ 'radio'
100
+ else
101
+ return []
102
+ end
103
+
104
+ current_scope = scope_element
105
+ return current_scope.query_selector_all(%(input[type="#{input_type}"])) if current_scope
106
+
107
+ playwright_page.capybara_current_frame.query_selector_all(%(input[type="#{input_type}"]))
108
+ end
109
+
110
+ def scope_element
111
+ return nil unless @node.is_a?(Capybara::Node::Element)
112
+ return nil unless @node.send(:base).is_a?(Capybara::Playwright::Node)
113
+
114
+ @node.send(:base).send(:element)
115
+ end
116
+
117
+ def driver
118
+ @node.send(:driver)
119
+ end
120
+
121
+ def session_options
122
+ @node.send(:session_options)
123
+ end
124
+ end
125
+
34
126
  def choose(locator = nil, **options)
35
- _playwright_check_via_get_by_label(locator, checked: true, **options) { super }
127
+ check_via_label_click(:radio_button, locator, checked: true, **options) { super }
36
128
  end
37
129
 
38
130
  def check(locator = nil, **options)
39
- _playwright_check_via_get_by_label(locator, checked: true, **options) { super }
131
+ check_via_label_click(:checkbox, locator, checked: true, **options) { super }
40
132
  end
41
133
 
42
134
  def uncheck(locator = nil, **options)
43
- _playwright_check_via_get_by_label(locator, checked: false, **options) { super }
135
+ check_via_label_click(:checkbox, locator, checked: false, **options) { super }
44
136
  end
45
137
 
46
- private def _playwright_check_via_get_by_label(locator, checked:, allow_label_click: session_options.automatic_label_click, **options)
47
- unless _playwright_use_get_by_label?(locator, allow_label_click, options)
138
+ private def check_via_label_click(node_type, locator, checked:, allow_label_click: session_options.automatic_label_click, **options)
139
+ unless should_use_label_click?(allow_label_click, options)
48
140
  return yield
49
141
  end
50
142
 
51
- return self if _playwright_try_get_by_label(locator, checked: checked)
143
+ handler = SelectableElementHandler.new(
144
+ node: self,
145
+ node_type: node_type,
146
+ locator: locator,
147
+ checked: checked,
148
+ )
149
+ return self if handler.set_checked_state_via_label?
52
150
 
53
151
  yield
54
152
  end
55
153
 
56
- private def _playwright_use_get_by_label?(locator, allow_label_click, options)
57
- return false if locator.nil?
154
+ private def should_use_label_click?(allow_label_click, options)
58
155
  return false unless allow_label_click
59
156
  return false unless driver.is_a?(Capybara::Playwright::Driver)
60
157
  return false if Hash.try_convert(allow_label_click)
@@ -62,33 +159,6 @@ module Capybara
62
159
 
63
160
  true
64
161
  end
65
-
66
- private def _playwright_try_get_by_label(locator, checked:)
67
- handled = false
68
- driver.with_playwright_page do |playwright_page|
69
- begin
70
- control_locator = playwright_page.get_by_label(locator.to_s)
71
- control = control_locator
72
-
73
- if control.evaluate('el => !!el.checked') != checked
74
- label = control.evaluate_handle('(el) => (el.labels && el.labels[0]) || el.closest("label") || null')
75
- next unless label.is_a?(::Playwright::ElementHandle)
76
-
77
- label.click
78
- end
79
-
80
- next unless control.evaluate('el => !!el.checked') == checked
81
-
82
- handled = true
83
- rescue ::Playwright::Error
84
- handled = false
85
- end
86
- end
87
-
88
- handled
89
- rescue StandardError
90
- false
91
- end
92
162
  end
93
163
  if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new('2.7')
94
164
  # Prepend to Node::Base instead of Node::Actions because Ruby < 3.1
@@ -128,6 +198,20 @@ module Capybara
128
198
  end
129
199
  ::Playwright::ElementHandle.prepend(CapybaraObscuredPatch)
130
200
 
201
+ # ref: https://github.com/teamcapybara/capybara/pull/2424
202
+ module ElementDropPathCompatPatch
203
+ def drop(*args)
204
+ options = args.map { |arg| arg.respond_to?(:to_path) ? arg.to_path : arg }
205
+ synchronize { base.drop(*options) }
206
+ self
207
+ end
208
+ end
209
+ if Gem::Version.new(Capybara::VERSION) < Gem::Version.new('3.34.0')
210
+ # Older Capybara returns early for Pathname arguments in Element#drop,
211
+ # so the driver implementation never runs.
212
+ Node::Element.prepend(ElementDropPathCompatPatch)
213
+ end
214
+
131
215
  module Playwright
132
216
  # Selector and checking methods are derived from twapole/apparition
133
217
  # Action methods (click, select_option, ...) uses playwright.
@@ -315,13 +399,31 @@ module Capybara
315
399
 
316
400
  class TextInput < Settable
317
401
  def set(value, **options)
402
+ case options[:clear]
403
+ when :backspace
404
+ @element.press('End', timeout: @timeout)
405
+ existing_text = @element.evaluate('el => el.value')
406
+ existing_text.length.times { @element.press('Backspace', timeout: @timeout) }
407
+ when :none
408
+ @element.press('End', timeout: @timeout)
409
+ when Array
410
+ @internal_logger.warn "options { clear: #{options[:clear]} } is ignored"
411
+ end
412
+
318
413
  text = value.to_s
319
- if text.end_with?("\n")
320
- @element.fill(text[0...-1], timeout: @timeout)
321
- @element.press('Enter', timeout: @timeout)
414
+ if press_enter = text.end_with?("\n")
415
+ text = text[0...-1]
416
+ end
417
+
418
+ if options[:clear] == :none
419
+ @element.type(text, timeout: @timeout)
322
420
  else
323
421
  @element.fill(text, timeout: @timeout)
324
422
  end
423
+
424
+ if press_enter
425
+ @element.press('Enter', timeout: @timeout)
426
+ end
325
427
  rescue ::Playwright::TimeoutError
326
428
  raise if @element.editable?
327
429
 
@@ -782,8 +884,47 @@ module Capybara
782
884
  end
783
885
  end
784
886
 
887
+ ATTACH_FILE = <<~JAVASCRIPT
888
+ () => {
889
+ const input = document.createElement('INPUT');
890
+ input.type = 'file';
891
+ input.multiple = true;
892
+ input.style.display = 'none';
893
+ document.body.appendChild(input);
894
+ return input;
895
+ }
896
+ JAVASCRIPT
897
+
898
+ DROP_FILE = <<~JAVASCRIPT
899
+ (el, input) => {
900
+ const dt = new DataTransfer();
901
+ for (const file of input.files) { dt.items.add(file); }
902
+ input.remove();
903
+ el.dispatchEvent(new DragEvent('drop', {
904
+ cancelable: true, bubbles: true, dataTransfer: dt
905
+ }));
906
+ }
907
+ JAVASCRIPT
908
+
909
+ DROP_STRING = <<~JAVASCRIPT
910
+ (el, items) => {
911
+ const dt = new DataTransfer();
912
+ for (const item of items) { dt.items.add(item.data, item.type); }
913
+ el.dispatchEvent(new DragEvent('drop', {
914
+ cancelable: true, bubbles: true, dataTransfer: dt
915
+ }));
916
+ }
917
+ JAVASCRIPT
918
+
785
919
  def drop(*args)
786
- raise NotImplementedError
920
+ if args.first.is_a?(String) || args.first.is_a?(Pathname)
921
+ input = @page.evaluate_handle(ATTACH_FILE)
922
+ input.as_element.set_input_files(args.map(&:to_s))
923
+ @element.evaluate(DROP_FILE, arg: input)
924
+ else
925
+ items = args.flat_map { |arg| arg.map { |(type, data)| { type: type, data: data } } }
926
+ @element.evaluate(DROP_STRING, arg: items)
927
+ end
787
928
  end
788
929
 
789
930
  def scroll_by(x, y)
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Capybara
4
4
  module Playwright
5
- VERSION = '0.5.8'
5
+ VERSION = '0.5.9'
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: capybara-playwright-driver
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.8
4
+ version: 0.5.9
5
5
  platform: ruby
6
6
  authors:
7
7
  - YusukeIwaki
@@ -58,6 +58,7 @@ extensions: []
58
58
  extra_rdoc_files: []
59
59
  files:
60
60
  - ".rspec"
61
+ - AGENTS.md
61
62
  - CODE_OF_CONDUCT.md
62
63
  - Gemfile
63
64
  - LICENSE.txt