Event.sys.mjs (8575B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 const lazy = {}; 6 7 ChromeUtils.defineESModuleGetters(lazy, { 8 keyData: "chrome://remote/content/shared/webdriver/KeyData.sys.mjs", 9 }); 10 11 /** Provides functionality for creating and sending DOM events. */ 12 export const event = {}; 13 14 const _eventUtils = new WeakMap(); 15 16 function _getEventUtils(win) { 17 if (!_eventUtils.has(win)) { 18 const eventUtilsObject = { 19 window: win, 20 parent: win, 21 _EU_Ci: Ci, 22 _EU_Cc: Cc, 23 }; 24 Services.scriptloader.loadSubScript( 25 "chrome://remote/content/external/EventUtils.js", 26 eventUtilsObject 27 ); 28 _eventUtils.set(win, eventUtilsObject); 29 } 30 return _eventUtils.get(win); 31 } 32 33 event.MouseEvents = { 34 click: 0, 35 dblclick: 1, 36 mousedown: 2, 37 mouseup: 3, 38 mouseover: 4, 39 mouseout: 5, 40 }; 41 42 event.Modifiers = { 43 shiftKey: 0, 44 ctrlKey: 1, 45 altKey: 2, 46 metaKey: 3, 47 }; 48 49 event.MouseButton = { 50 isPrimary(button) { 51 return button === 0; 52 }, 53 isAuxiliary(button) { 54 return button === 1; 55 }, 56 isSecondary(button) { 57 return button === 2; 58 }, 59 }; 60 61 /** 62 * Synthesize a mouse event in `win` at a point. 63 * 64 * If the type is specified in `event`, a mouse event of that type is 65 * fired. Otherwise, a mousedown followed by a mouseup is performed. 66 * 67 * @param {number} left - Value for the X offset in CSS pixels. 68 * @param {number} top - Value for the Y offset in CSS pixels. 69 * @param {module:EventUtils~MouseEventData} event - Details of the mouse event 70 * to dispatch. 71 * @param {DOMWindow} win - DOM window used to dispatch the event. 72 * 73 * @returns {Promise<boolean>} Promise that resolves to a boolean, 74 * indicating whether the event had preventDefault() called on it. 75 */ 76 event.synthesizeMouseAtPoint = function (left, top, event, win) { 77 if (!event.asyncEnabled) { 78 return Promise.resolve( 79 _getEventUtils(win).synthesizeMouseAtPoint(left, top, event, win) 80 ); 81 } 82 83 // A callback must be used when handling events with the `asyncEnabled` 84 // flag set to `true`, as these events are synthesized in the parent process. 85 // We need to wait for them to be fully dispatched to the content process 86 // before continuing. 87 const { promise, resolve } = Promise.withResolvers(); 88 const preventDefaultFlag = _getEventUtils(win).synthesizeMouseAtPoint( 89 left, 90 top, 91 event, 92 win, 93 () => resolve() 94 ); 95 96 return promise.then(() => preventDefaultFlag); 97 }; 98 99 /** 100 * Synthesize a touch event at a point. 101 * 102 * If the type is specified in opts, a touch event of that type is 103 * fired. Otherwise, a touchstart followed by a touchend is performed. 104 * 105 * @param {number} left 106 * Offset from viewport left, in CSS pixels 107 * @param {number} top 108 * Offset from viewport top, in CSS pixels 109 * @param {object} opts 110 * Object which may contain the properties "id", "rx", "ry", "angle", 111 * "force", "shiftKey", "ctrlKey", "altKey", "metaKey", "accessKey", 112 * "type". 113 * @param {Window} win 114 * Window object. 115 * 116 * @returns {boolean} defaultPrevented 117 */ 118 event.synthesizeTouchAtPoint = function (left, top, opts, win) { 119 return _getEventUtils(win).synthesizeTouchAtPoint(left, top, opts, win); 120 }; 121 122 /** 123 * Synthesize a wheel event in `win` at a point, without flushing layout. 124 * 125 * @param {number} left - Floating-point value for the X offset in CSS pixels. 126 * @param {number} top - Floating-point value for the Y offset in CSS pixels. 127 * @param {module:EventUtils~WheelEventData} event - Details of the wheel event 128 * to dispatch. 129 * @param {DOMWindow} win - DOM window used to dispatch the event. 130 * 131 * @returns {Promise} - Promise that resolves once the event has been dispatched. 132 */ 133 event.synthesizeWheelAtPoint = function (left, top, event, win) { 134 const dpr = win.devicePixelRatio; 135 136 // All delta properties expect the value in device pixels while the 137 // WebDriver specification uses CSS pixels. 138 // TODO: check if the conversion needs to be done at the platform level 139 if (typeof event.deltaX !== "undefined") { 140 event.deltaX *= dpr; 141 } 142 if (typeof event.deltaY !== "undefined") { 143 event.deltaY *= dpr; 144 } 145 if (typeof event.deltaZ !== "undefined") { 146 event.deltaZ *= dpr; 147 } 148 149 return new Promise(resolve => 150 _getEventUtils(win).synthesizeWheelAtPoint(left, top, event, win, resolve) 151 ); 152 }; 153 154 event.synthesizeMultiTouch = function (opts, win) { 155 const modifiers = _getEventUtils(win)._parseModifiers(opts); 156 win.windowUtils.sendTouchEvent( 157 opts.type, 158 opts.id, 159 opts.x, 160 opts.y, 161 opts.rx, 162 opts.ry, 163 opts.angle, 164 opts.force, 165 opts.tiltx, 166 opts.tilty, 167 opts.twist, 168 modifiers 169 ); 170 }; 171 172 /** 173 * Synthesize a keydown event for a single key. 174 * 175 * @param {object} key 176 * Key data as returned by keyData.getData 177 * @param {Window} win 178 * Window object. 179 */ 180 event.sendKeyDown = function (key, win) { 181 event.sendSingleKey(key, win, "keydown"); 182 }; 183 184 /** 185 * Synthesize a keyup event for a single key. 186 * 187 * @param {object} key 188 * Key data as returned by keyData.getData 189 * @param {Window} win 190 * Window object. 191 */ 192 event.sendKeyUp = function (key, win) { 193 event.sendSingleKey(key, win, "keyup"); 194 }; 195 196 /** 197 * Synthesize a key event for a single key. 198 * 199 * @param {object} key 200 * Key data as returned by keyData.getData 201 * @param {Window} win 202 * Window object. 203 * @param {string=} type 204 * Event to emit. By default the full keydown/keypressed/keyup event 205 * sequence is emitted. 206 */ 207 event.sendSingleKey = function (key, win, type = null) { 208 let keyValue = key.key; 209 if (!key.printable) { 210 keyValue = `KEY_${keyValue}`; 211 } 212 const event = { 213 code: key.code, 214 location: key.location, 215 altKey: key.altKey ?? false, 216 shiftKey: key.shiftKey ?? false, 217 ctrlKey: key.ctrlKey ?? false, 218 metaKey: key.metaKey ?? false, 219 repeat: key.repeat ?? false, 220 }; 221 if (type) { 222 event.type = type; 223 } 224 _getEventUtils(win).synthesizeKey(keyValue, event, win); 225 }; 226 227 /** 228 * Send a string as a series of keypresses. 229 * 230 * @param {string} keyString 231 * Sequence of characters to send as key presses 232 * @param {Window} win 233 * Window object 234 */ 235 event.sendKeys = function (keyString, win) { 236 const modifiers = {}; 237 for (let modifier in event.Modifiers) { 238 modifiers[modifier] = false; 239 } 240 241 for (let keyValue of keyString) { 242 // keyValue will contain enough to represent the UTF-16 encoding of a single abstract character 243 // i.e. either a single scalar value, or a surrogate pair 244 if (modifiers.shiftKey) { 245 keyValue = lazy.keyData.getShiftedKey(keyValue); 246 } 247 const data = lazy.keyData.getData(keyValue); 248 const key = { ...data, ...modifiers }; 249 if (data.modifier) { 250 // Negating the state of the modifier here is not spec compliant but 251 // makes us compatible to Chrome's behavior for now. That's fine unless 252 // we know the correct behavior. 253 // 254 // @see: https://github.com/w3c/webdriver/issues/1734 255 modifiers[data.modifier] = !modifiers[data.modifier]; 256 } 257 event.sendSingleKey(key, win); 258 } 259 }; 260 261 event.sendEvent = function (eventType, el, modifiers = {}, opts = {}) { 262 opts.canBubble = opts.canBubble || true; 263 264 let doc = el.ownerDocument || el.document; 265 let ev = doc.createEvent("Event"); 266 267 ev.shiftKey = modifiers.shift; 268 ev.metaKey = modifiers.meta; 269 ev.altKey = modifiers.alt; 270 ev.ctrlKey = modifiers.ctrl; 271 272 ev.initEvent(eventType, opts.canBubble, true); 273 el.dispatchEvent(ev); 274 }; 275 276 event.mouseover = function (el, modifiers = {}, opts = {}) { 277 return event.sendEvent("mouseover", el, modifiers, opts); 278 }; 279 280 event.mousemove = function (el, modifiers = {}, opts = {}) { 281 return event.sendEvent("mousemove", el, modifiers, opts); 282 }; 283 284 event.mousedown = function (el, modifiers = {}, opts = {}) { 285 return event.sendEvent("mousedown", el, modifiers, opts); 286 }; 287 288 event.mouseup = function (el, modifiers = {}, opts = {}) { 289 return event.sendEvent("mouseup", el, modifiers, opts); 290 }; 291 292 event.cancel = function (el, modifiers = {}, opts = {}) { 293 return event.sendEvent("cancel", el, modifiers, opts); 294 }; 295 296 event.click = function (el, modifiers = {}, opts = {}) { 297 return event.sendEvent("click", el, modifiers, opts); 298 }; 299 300 event.change = function (el, modifiers = {}, opts = {}) { 301 return event.sendEvent("change", el, modifiers, opts); 302 }; 303 304 event.input = function (el, modifiers = {}, opts = {}) { 305 return event.sendEvent("input", el, modifiers, opts); 306 };