Module: Dommy::Interaction::EventSynthesis

Defined in:
lib/dommy/interaction/event_synthesis.rb

Overview

Synthesizes and dispatches the DOM event sequences a real browser fires for user interactions, in the right order, so JS handlers (Stimulus actions, React synthetic events, …) run exactly as they would in a browser. Dispatch is Ruby-side; a JS-registered listener is invoked synchronously through the bridge (CallableInvoker → js_call).

Constant Summary collapse

BUBBLES =
{"bubbles" => true, "cancelable" => true, "composed" => true}.freeze
NAMED_KEYS =

Named keys for Driver#send_keys, mapped to their KeyboardEvent key/code pairs. Aliases (:up for :arrow_up, …) match Capybara's.

{
  enter: ["Enter", "Enter"],
  tab: ["Tab", "Tab"],
  escape: ["Escape", "Escape"],
  space: [" ", "Space"],
  backspace: ["Backspace", "Backspace"],
  delete: ["Delete", "Delete"],
  arrow_up: ["ArrowUp", "ArrowUp"],
  arrow_down: ["ArrowDown", "ArrowDown"],
  arrow_left: ["ArrowLeft", "ArrowLeft"],
  arrow_right: ["ArrowRight", "ArrowRight"],
  up: ["ArrowUp", "ArrowUp"],
  down: ["ArrowDown", "ArrowDown"],
  left: ["ArrowLeft", "ArrowLeft"],
  right: ["ArrowRight", "ArrowRight"],
  home: ["Home", "Home"],
  end: ["End", "End"],
  page_up: ["PageUp", "PageUp"],
  page_down: ["PageDown", "PageDown"],
}.freeze

Class Method Summary collapse

Class Method Details

.beforeinput(element, data, input_type) ⇒ Object

beforeinput precedes the value mutation and is cancelable (the input event that follows the mutation is not).



134
135
136
137
138
139
140
# File 'lib/dommy/interaction/event_synthesis.rb', line 134

def beforeinput(element, data, input_type)
  event = Dommy::InputEvent.new(
    "beforeinput", BUBBLES.merge("data" => data, "inputType" => input_type)
  )
  element.dispatch_event(event)
  event.default_prevented?
end

.blur(element) ⇒ Object



44
45
46
47
# File 'lib/dommy/interaction/event_synthesis.rb', line 44

def blur(element)
  element.blur if element.respond_to?(:blur)
  nil
end

.change(element) ⇒ Object



53
54
55
# File 'lib/dommy/interaction/event_synthesis.rb', line 53

def change(element)
  dispatch(element, Dommy::Event.new("change", "bubbles" => true))
end

.char_code(char) ⇒ Object

The KeyboardEvent code for a typed character ("a" -> "KeyA"). Best-effort: unknown characters get an empty code, like a real browser does for keys it can't map to a physical position.



145
146
147
148
149
150
151
152
# File 'lib/dommy/interaction/event_synthesis.rb', line 145

def char_code(char)
  case char
  when /\A[a-zA-Z]\z/ then "Key#{char.upcase}"
  when /\A[0-9]\z/ then "Digit#{char}"
  when " " then "Space"
  else ""
  end
end

.click(element) ⇒ Object

Full primary-button click: pointerdown → mousedown → focus → pointerup → mouseup → click. Returns true when the click default was prevented (the caller suppresses any follow-on navigation / submission).



18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# File 'lib/dommy/interaction/event_synthesis.rb', line 18

def click(element)
  dispatch(element, Dommy::PointerEvent.new("pointerdown", mouse_init))
  dispatch(element, Dommy::MouseEvent.new("mousedown", mouse_init))
  focus(element)
  dispatch(element, Dommy::PointerEvent.new("pointerup", mouse_init))
  dispatch(element, Dommy::MouseEvent.new("mouseup", mouse_init))
  event = Dommy::MouseEvent.new("click", mouse_init)
  element.dispatch_event(event)
  prevented = event.default_prevented?
  # Run the click's activation behavior (hyperlink navigation, …) the same
  # way Element#click does, so a synthetic/user click triggers the default
  # action too — unless it was prevented. Checkbox/radio toggling is handled
  # by the field interactor, not here (their activation_target is nil).
  element.__run_click_activation_behavior__(event) if !prevented && element.respond_to?(:__run_click_activation_behavior__)
  prevented
end

.composition_input(element, data) ⇒ Object

The input pair during composition: beforeinput then input, both with inputType insertCompositionText and isComposing true. Unlike ordinary typing, the composition beforeinput is NOT cancelable (spec).



125
126
127
128
129
130
# File 'lib/dommy/interaction/event_synthesis.rb', line 125

def composition_input(element, data)
  init = {"bubbles" => true, "composed" => true,
          "data" => data, "inputType" => "insertCompositionText", "isComposing" => true}
  dispatch(element, Dommy::InputEvent.new("beforeinput", init))
  dispatch(element, Dommy::InputEvent.new("input", init))
end

.compositionend(element, data) ⇒ Object



117
118
119
120
# File 'lib/dommy/interaction/event_synthesis.rb', line 117

def compositionend(element, data)
  dispatch(element, Dommy::CompositionEvent.new("compositionend",
    "bubbles" => true, "composed" => true, "data" => data))
end

.compositionstart(element, data = "") ⇒ Object

IME composition events. compositionstart is cancelable per spec; update / end are not.



108
109
110
# File 'lib/dommy/interaction/event_synthesis.rb', line 108

def compositionstart(element, data = "")
  dispatch(element, Dommy::CompositionEvent.new("compositionstart", BUBBLES.merge("data" => data)))
end

.compositionupdate(element, data) ⇒ Object



112
113
114
115
# File 'lib/dommy/interaction/event_synthesis.rb', line 112

def compositionupdate(element, data)
  dispatch(element, Dommy::CompositionEvent.new("compositionupdate",
    "bubbles" => true, "composed" => true, "data" => data))
end

.dispatch(element, event) ⇒ Object



57
58
59
# File 'lib/dommy/interaction/event_synthesis.rb', line 57

def dispatch(element, event)
  element.dispatch_event(event)
end

.focus(element) ⇒ Object

Run the element's focusing steps (Element#focus): moves document.activeElement and fires blur/focusout on the previously focused element plus focus/focusin here — a no-op when the element already holds focus, like a real browser.



39
40
41
42
# File 'lib/dommy/interaction/event_synthesis.rb', line 39

def focus(element)
  element.focus if element.respond_to?(:focus)
  nil
end

.input(element, data = nil, input_type = "insertText") ⇒ Object



49
50
51
# File 'lib/dommy/interaction/event_synthesis.rb', line 49

def input(element, data = nil, input_type = "insertText")
  dispatch(element, Dommy::InputEvent.new("input", BUBBLES.merge("data" => data, "inputType" => input_type)))
end

.key_init(key, code, extra = nil) ⇒ Object



154
155
156
157
# File 'lib/dommy/interaction/event_synthesis.rb', line 154

def key_init(key, code, extra = nil)
  init = BUBBLES.merge("key" => key, "code" => code)
  extra ? init.merge(extra) : init
end

.keydown(element, key, code, extra = nil) ⇒ Object

keydown is cancelable: a prevented keydown suppresses the key's default action (typing, implicit submission). Returns whether it was prevented, like #click.



87
88
89
90
91
# File 'lib/dommy/interaction/event_synthesis.rb', line 87

def keydown(element, key, code, extra = nil)
  event = Dommy::KeyboardEvent.new("keydown", key_init(key, code, extra))
  element.dispatch_event(event)
  event.default_prevented?
end

.keypress(element, key, code) ⇒ Object

keypress fires only for keys that produce a character (legacy but still widely handled). Also cancelable; a prevented keypress suppresses the character insertion.



96
97
98
99
100
# File 'lib/dommy/interaction/event_synthesis.rb', line 96

def keypress(element, key, code)
  event = Dommy::KeyboardEvent.new("keypress", key_init(key, code))
  element.dispatch_event(event)
  event.default_prevented?
end

.keyup(element, key, code, extra = nil) ⇒ Object



102
103
104
# File 'lib/dommy/interaction/event_synthesis.rb', line 102

def keyup(element, key, code, extra = nil)
  dispatch(element, Dommy::KeyboardEvent.new("keyup", key_init(key, code, extra)))
end

.mouse_initObject



159
160
161
# File 'lib/dommy/interaction/event_synthesis.rb', line 159

def mouse_init
  BUBBLES.merge("button" => 0, "clientX" => 0, "clientY" => 0)
end