Class: Dommy::Interaction::FieldInteractor

Inherits:
Object
  • Object
show all
Defined in:
lib/dommy/interaction/field_interactor.rb

Overview

Drives form fields: fills text inputs, toggles radios / checkboxes, selects options, attaches files. Mutates the live Dommy elements AND fires the browser's input/change (and focus) events so JS handlers (React onChange, Stimulus, …) react. The value is written directly on the element — the React "native setter" path — so a framework's value tracker sees the change rather than swallowing it.

Constant Summary collapse

MIME_TYPES =

Minimal extension → MIME map for attach_file (core stays Rack-free).

{
  ".txt" => "text/plain", ".html" => "text/html", ".htm" => "text/html",
  ".json" => "application/json", ".csv" => "text/csv", ".xml" => "application/xml",
  ".png" => "image/png", ".jpg" => "image/jpeg", ".jpeg" => "image/jpeg",
  ".gif" => "image/gif", ".pdf" => "application/pdf"
}.freeze
TEXT_ENTRY_INPUT_TYPES =
%w[text search email url tel password number].freeze

Instance Method Summary collapse

Constructor Details

#initialize(finder, document) ⇒ FieldInteractor

Returns a new instance of FieldInteractor.



20
21
22
23
# File 'lib/dommy/interaction/field_interactor.rb', line 20

def initialize(finder, document)
  @finder = finder
  @document = document
end

Instance Method Details

#attach_file(locator, path) ⇒ Object

Raises:



51
52
53
54
55
56
57
58
59
60
61
# File 'lib/dommy/interaction/field_interactor.rb', line 51

def attach_file(locator, path)
  input = @finder.find_field(locator)
  raise FileNotFoundError, "no such file: #{path}" unless ::File.exist?(path)

  file = Dommy::File.new(
    [::File.binread(path)], ::File.basename(path), "type" => mime_type_for(path)
  )
  input.__driver_set_files__([file])
  EventSynthesis.change(input)
  input
end

#backspace(element) ⇒ Object

Delete the last character of a text-entry field's value. Same return contract as #insert_text.



112
113
114
115
116
117
118
119
120
121
122
# File 'lib/dommy/interaction/field_interactor.rb', line 112

def backspace(element)
  return false unless text_entry_field?(element)

  value = element.value.to_s
  return false if value.empty?
  return false if EventSynthesis.beforeinput(element, nil, "deleteContentBackward")

  element.value = value[0...-1]
  EventSynthesis.input(element, nil, "deleteContentBackward")
  true
end

#cancel_composition_text(element, base) ⇒ Object

Cancel a composition: restore the field's pre-composition value and fire a deleteCompositionText input. Same return contract.



138
139
140
141
142
143
144
# File 'lib/dommy/interaction/field_interactor.rb', line 138

def cancel_composition_text(element, base)
  return false unless text_entry_field?(element)

  element.value = base
  EventSynthesis.input(element, nil, "deleteCompositionText")
  true
end

#check(locator) ⇒ Object



43
44
45
# File 'lib/dommy/interaction/field_interactor.rb', line 43

def check(locator)
  toggle(@finder.find_field(locator), true)
end

#choose(locator) ⇒ Object



34
35
36
37
38
39
40
41
# File 'lib/dommy/interaction/field_interactor.rb', line 34

def choose(locator)
  radio = @finder.find_field(locator)
  clear_radio_group(radio)
  radio.checked = true
  EventSynthesis.input(radio)
  EventSynthesis.change(radio)
  radio
end

#fill_in(locator, with:) ⇒ Object



25
26
27
28
29
30
31
32
# File 'lib/dommy/interaction/field_interactor.rb', line 25

def fill_in(locator, with:)
  field = @finder.find_field(locator)
  EventSynthesis.focus(field)
  field.value = with.to_s
  EventSynthesis.input(field, with.to_s)
  EventSynthesis.change(field)
  field
end

#insert_text(element, text) ⇒ Object

Append text to a text-entry field's value (no caret/selection model: insertion is at the end), firing cancelable beforeinput then input. Returns whether the field accepted the insertion (false for a non-text-entry field or a prevented beforeinput) — used by Driver's send_keys to decide whether a key's default action ran.



101
102
103
104
105
106
107
108
# File 'lib/dommy/interaction/field_interactor.rb', line 101

def insert_text(element, text)
  return false unless text_entry_field?(element)
  return false if EventSynthesis.beforeinput(element, text, "insertText")

  element.value = element.value.to_s + text
  EventSynthesis.input(element, text)
  true
end

#select(value, from:) ⇒ Object



63
64
65
66
67
68
69
70
71
72
73
# File 'lib/dommy/interaction/field_interactor.rb', line 63

def select(value, from:)
  select_el = @finder.find_field(from)
  option = @finder.find_option(select_el, value)
  raise ElementNotFoundError, "no option #{value.inspect} in #{from.inspect}" unless option

  select_el.options.each { |o| o.remove_attribute("selected") } unless select_el.multiple
  option.set_attribute("selected", "")
  EventSynthesis.input(select_el)
  EventSynthesis.change(select_el)
  select_el
end

#set_composition_text(element, base, update) ⇒ Object

Show composition state update in a text-entry field (value becomes base + update) with the insertCompositionText beforeinput/input pair. No beforeinput veto: composition inputs are not cancelable (spec). Same return contract as #insert_text.



128
129
130
131
132
133
134
# File 'lib/dommy/interaction/field_interactor.rb', line 128

def set_composition_text(element, base, update)
  return false unless text_entry_field?(element)

  element.value = base + update
  EventSynthesis.composition_input(element, update)
  true
end

#text_entry_field?(element) ⇒ Boolean

Returns:

  • (Boolean)


88
89
90
91
92
93
94
# File 'lib/dommy/interaction/field_interactor.rb', line 88

def text_entry_field?(element)
  return true if element.local_name == "textarea"
  return false unless element.local_name == "input"

  type = (element.get_attribute("type") || "text").downcase
  TEXT_ENTRY_INPUT_TYPES.include?(type)
end

#uncheck(locator) ⇒ Object



47
48
49
# File 'lib/dommy/interaction/field_interactor.rb', line 47

def uncheck(locator)
  toggle(@finder.find_field(locator), false)
end

#unselect(value, from:) ⇒ Object



75
76
77
78
79
80
81
82
83
84
# File 'lib/dommy/interaction/field_interactor.rb', line 75

def unselect(value, from:)
  select_el = @finder.find_field(from)
  option = @finder.find_option(select_el, value)
  return select_el unless option

  option.remove_attribute("selected")
  EventSynthesis.input(select_el)
  EventSynthesis.change(select_el)
  select_el
end