|
1 | 1 | import { flush } from '@angular/core/testing'; |
2 | 2 | import { takeWhile } from 'rxjs/operators'; |
3 | 3 | import { BehaviorSubject } from 'rxjs'; |
4 | | -import { l as triggerBlur, m as isTextInput, o as clearElement, t as triggerFocus, c as dispatchMouseEvent, n as typeInElement, a as dispatchFakeEvent, k as createFakeEvent, d as dispatchEvent, e as dispatchPointerEvent } from '../type-in-element-de7fd3bb.mjs'; |
5 | | -import { _ as _getTextWithExcludedElements, T as TestKey, b as HarnessEnvironment, h as handleAutoChangeDetectionStatus, s as stopHandlingAutoChangeDetectionStatus } from '../text-filtering-e9a6b8d9.mjs'; |
6 | | -import { B as BACKSPACE, T as TAB, h as ENTER, S as SHIFT, C as CONTROL, A as ALT, e as ESCAPE, b as PAGE_UP, P as PAGE_DOWN, E as END, H as HOME, L as LEFT_ARROW, U as UP_ARROW, R as RIGHT_ARROW, D as DOWN_ARROW, I as INSERT, n as DELETE, au as F1, av as F2, aw as F3, ax as F4, ay as F5, az as F6, aA as F7, aB as F8, aC as F9, aD as F10, aE as F11, aF as F12, a as META, aT as COMMA } from '../keycodes-0e4398c6.mjs'; |
7 | | -import '../test-element-errors-83375db9.mjs'; |
| 4 | +import { g as getNoKeysSpecifiedError, _ as _getTextWithExcludedElements, T as TestKey, b as HarnessEnvironment, h as handleAutoChangeDetectionStatus, s as stopHandlingAutoChangeDetectionStatus } from '../text-filtering-55cbd0d9.mjs'; |
| 5 | +import { aV as PERIOD, B as BACKSPACE, T as TAB, h as ENTER, S as SHIFT, C as CONTROL, A as ALT, e as ESCAPE, b as PAGE_UP, P as PAGE_DOWN, E as END, H as HOME, L as LEFT_ARROW, U as UP_ARROW, R as RIGHT_ARROW, D as DOWN_ARROW, I as INSERT, n as DELETE, au as F1, av as F2, aw as F3, ax as F4, ay as F5, az as F6, aA as F7, aB as F8, aC as F9, aD as F10, aE as F11, aF as F12, a as META, aT as COMMA } from '../keycodes-0e4398c6.mjs'; |
8 | 6 |
|
9 | 7 | /** Unique symbol that is used to patch a property to a proxy zone. */ |
10 | 8 | const stateObservableSymbol = Symbol('ProxyZone_PATCHED#stateObservable'); |
@@ -81,6 +79,293 @@ class TaskStateZoneInterceptor { |
81 | 79 | } |
82 | 80 | } |
83 | 81 |
|
| 82 | +/** Used to generate unique IDs for events. */ |
| 83 | +/** |
| 84 | + * Creates a browser MouseEvent with the specified options. |
| 85 | + * @docs-private |
| 86 | + */ |
| 87 | +function createMouseEvent(type, clientX = 0, clientY = 0, offsetX = 0, offsetY = 0, button = 0, modifiers = {}) { |
| 88 | + // Note: We cannot determine the position of the mouse event based on the screen |
| 89 | + // because the dimensions and position of the browser window are not available |
| 90 | + // To provide reasonable `screenX` and `screenY` coordinates, we simply use the |
| 91 | + // client coordinates as if the browser is opened in fullscreen. |
| 92 | + const screenX = clientX; |
| 93 | + const screenY = clientY; |
| 94 | + const event = new MouseEvent(type, { |
| 95 | + bubbles: true, |
| 96 | + cancelable: true, |
| 97 | + composed: true, // Required for shadow DOM events. |
| 98 | + view: window, |
| 99 | + detail: 1, |
| 100 | + relatedTarget: null, |
| 101 | + screenX, |
| 102 | + screenY, |
| 103 | + clientX, |
| 104 | + clientY, |
| 105 | + ctrlKey: modifiers.control, |
| 106 | + altKey: modifiers.alt, |
| 107 | + shiftKey: modifiers.shift, |
| 108 | + metaKey: modifiers.meta, |
| 109 | + button: button, |
| 110 | + buttons: 1, |
| 111 | + }); |
| 112 | + // The `MouseEvent` constructor doesn't allow us to pass these properties into the constructor. |
| 113 | + // Override them to `1`, because they're used for fake screen reader event detection. |
| 114 | + if (offsetX != null) { |
| 115 | + defineReadonlyEventProperty(event, 'offsetX', offsetX); |
| 116 | + } |
| 117 | + if (offsetY != null) { |
| 118 | + defineReadonlyEventProperty(event, 'offsetY', offsetY); |
| 119 | + } |
| 120 | + return event; |
| 121 | +} |
| 122 | +/** |
| 123 | + * Creates a browser `PointerEvent` with the specified options. Pointer events |
| 124 | + * by default will appear as if they are the primary pointer of their type. |
| 125 | + * https://www.w3.org/TR/pointerevents2/#dom-pointerevent-isprimary. |
| 126 | + * |
| 127 | + * For example, if pointer events for a multi-touch interaction are created, the non-primary |
| 128 | + * pointer touches would need to be represented by non-primary pointer events. |
| 129 | + * |
| 130 | + * @docs-private |
| 131 | + */ |
| 132 | +function createPointerEvent(type, clientX = 0, clientY = 0, offsetX, offsetY, options = { isPrimary: true }) { |
| 133 | + const event = new PointerEvent(type, { |
| 134 | + bubbles: true, |
| 135 | + cancelable: true, |
| 136 | + composed: true, // Required for shadow DOM events. |
| 137 | + view: window, |
| 138 | + clientX, |
| 139 | + clientY, |
| 140 | + ...options, |
| 141 | + }); |
| 142 | + if (offsetX != null) { |
| 143 | + defineReadonlyEventProperty(event, 'offsetX', offsetX); |
| 144 | + } |
| 145 | + if (offsetY != null) { |
| 146 | + defineReadonlyEventProperty(event, 'offsetY', offsetY); |
| 147 | + } |
| 148 | + return event; |
| 149 | +} |
| 150 | +/** |
| 151 | + * Creates a keyboard event with the specified key and modifiers. |
| 152 | + * @docs-private |
| 153 | + */ |
| 154 | +function createKeyboardEvent(type, keyCode = 0, key = '', modifiers = {}, code = '') { |
| 155 | + return new KeyboardEvent(type, { |
| 156 | + bubbles: true, |
| 157 | + cancelable: true, |
| 158 | + composed: true, // Required for shadow DOM events. |
| 159 | + view: window, |
| 160 | + keyCode, |
| 161 | + key, |
| 162 | + shiftKey: modifiers.shift, |
| 163 | + metaKey: modifiers.meta, |
| 164 | + altKey: modifiers.alt, |
| 165 | + ctrlKey: modifiers.control, |
| 166 | + code, |
| 167 | + }); |
| 168 | +} |
| 169 | +/** |
| 170 | + * Creates a fake event object with any desired event type. |
| 171 | + * @docs-private |
| 172 | + */ |
| 173 | +function createFakeEvent(type, bubbles = false, cancelable = true, composed = true) { |
| 174 | + return new Event(type, { bubbles, cancelable, composed }); |
| 175 | +} |
| 176 | +/** |
| 177 | + * Defines a readonly property on the given event object. Readonly properties on an event object |
| 178 | + * are always set as configurable as that matches default readonly properties for DOM event objects. |
| 179 | + */ |
| 180 | +function defineReadonlyEventProperty(event, propertyName, value) { |
| 181 | + Object.defineProperty(event, propertyName, { get: () => value, configurable: true }); |
| 182 | +} |
| 183 | + |
| 184 | +/** |
| 185 | + * Utility to dispatch any event on a Node. |
| 186 | + * @docs-private |
| 187 | + */ |
| 188 | +function dispatchEvent(node, event) { |
| 189 | + node.dispatchEvent(event); |
| 190 | + return event; |
| 191 | +} |
| 192 | +/** |
| 193 | + * Shorthand to dispatch a fake event on a specified node. |
| 194 | + * @docs-private |
| 195 | + */ |
| 196 | +function dispatchFakeEvent(node, type, bubbles) { |
| 197 | + return dispatchEvent(node, createFakeEvent(type, bubbles)); |
| 198 | +} |
| 199 | +/** |
| 200 | + * Shorthand to dispatch a keyboard event with a specified key code and |
| 201 | + * optional modifiers. |
| 202 | + * @docs-private |
| 203 | + */ |
| 204 | +function dispatchKeyboardEvent(node, type, keyCode, key, modifiers, code) { |
| 205 | + return dispatchEvent(node, createKeyboardEvent(type, keyCode, key, modifiers, code)); |
| 206 | +} |
| 207 | +/** |
| 208 | + * Shorthand to dispatch a mouse event on the specified coordinates. |
| 209 | + * @docs-private |
| 210 | + */ |
| 211 | +function dispatchMouseEvent(node, type, clientX = 0, clientY = 0, offsetX, offsetY, button, modifiers) { |
| 212 | + return dispatchEvent(node, createMouseEvent(type, clientX, clientY, offsetX, offsetY, button, modifiers)); |
| 213 | +} |
| 214 | +/** |
| 215 | + * Shorthand to dispatch a pointer event on the specified coordinates. |
| 216 | + * @docs-private |
| 217 | + */ |
| 218 | +function dispatchPointerEvent(node, type, clientX = 0, clientY = 0, offsetX, offsetY, options) { |
| 219 | + return dispatchEvent(node, createPointerEvent(type, clientX, clientY, offsetX, offsetY, options)); |
| 220 | +} |
| 221 | + |
| 222 | +function triggerFocusChange(element, event) { |
| 223 | + let eventFired = false; |
| 224 | + const handler = () => (eventFired = true); |
| 225 | + element.addEventListener(event, handler); |
| 226 | + element[event](); |
| 227 | + element.removeEventListener(event, handler); |
| 228 | + if (!eventFired) { |
| 229 | + dispatchFakeEvent(element, event); |
| 230 | + } |
| 231 | +} |
| 232 | +/** @docs-private */ |
| 233 | +function triggerFocus(element) { |
| 234 | + triggerFocusChange(element, 'focus'); |
| 235 | +} |
| 236 | +/** @docs-private */ |
| 237 | +function triggerBlur(element) { |
| 238 | + triggerFocusChange(element, 'blur'); |
| 239 | +} |
| 240 | + |
| 241 | +/** Input types for which the value can be entered incrementally. */ |
| 242 | +const incrementalInputTypes = new Set([ |
| 243 | + 'text', |
| 244 | + 'email', |
| 245 | + 'hidden', |
| 246 | + 'password', |
| 247 | + 'search', |
| 248 | + 'tel', |
| 249 | + 'url', |
| 250 | +]); |
| 251 | +/** |
| 252 | + * Manual mapping of some common characters to their `code` in a keyboard event. Non-exhaustive, see |
| 253 | + * the tables on MDN for more info: https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/keyCode |
| 254 | + */ |
| 255 | +const charsToCodes = { |
| 256 | + ' ': 'Space', |
| 257 | + '.': 'Period', |
| 258 | + ',': 'Comma', |
| 259 | + '`': 'Backquote', |
| 260 | + '-': 'Minus', |
| 261 | + '=': 'Equal', |
| 262 | + '[': 'BracketLeft', |
| 263 | + ']': 'BracketRight', |
| 264 | + '\\': 'Backslash', |
| 265 | + '/': 'Slash', |
| 266 | + "'": 'Quote', |
| 267 | + '"': 'Quote', |
| 268 | + ';': 'Semicolon', |
| 269 | +}; |
| 270 | +/** |
| 271 | + * Determines the `KeyboardEvent.key` from a character. See #27034 and |
| 272 | + * https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/code |
| 273 | + */ |
| 274 | +function getKeyboardEventCode(char) { |
| 275 | + if (char.length !== 1) { |
| 276 | + return ''; |
| 277 | + } |
| 278 | + const charCode = char.charCodeAt(0); |
| 279 | + // Key is a letter between a and z, uppercase or lowercase. |
| 280 | + if ((charCode >= 97 && charCode <= 122) || (charCode >= 65 && charCode <= 90)) { |
| 281 | + return `Key${char.toUpperCase()}`; |
| 282 | + } |
| 283 | + // Digits from 0 to 9. |
| 284 | + if (48 <= charCode && charCode <= 57) { |
| 285 | + return `Digit${char}`; |
| 286 | + } |
| 287 | + return charsToCodes[char] ?? ''; |
| 288 | +} |
| 289 | +/** |
| 290 | + * Checks whether the given Element is a text input element. |
| 291 | + * @docs-private |
| 292 | + */ |
| 293 | +function isTextInput(element) { |
| 294 | + const nodeName = element.nodeName.toLowerCase(); |
| 295 | + return nodeName === 'input' || nodeName === 'textarea'; |
| 296 | +} |
| 297 | +function typeInElement(element, ...modifiersAndKeys) { |
| 298 | + const first = modifiersAndKeys[0]; |
| 299 | + let modifiers; |
| 300 | + let rest; |
| 301 | + if (first !== undefined && |
| 302 | + typeof first !== 'string' && |
| 303 | + first.keyCode === undefined && |
| 304 | + first.key === undefined) { |
| 305 | + modifiers = first; |
| 306 | + rest = modifiersAndKeys.slice(1); |
| 307 | + } |
| 308 | + else { |
| 309 | + modifiers = {}; |
| 310 | + rest = modifiersAndKeys; |
| 311 | + } |
| 312 | + const isInput = isTextInput(element); |
| 313 | + const inputType = element.getAttribute('type') || 'text'; |
| 314 | + const keys = rest |
| 315 | + .map(k => typeof k === 'string' |
| 316 | + ? k.split('').map(c => ({ |
| 317 | + keyCode: c.toUpperCase().charCodeAt(0), |
| 318 | + key: c, |
| 319 | + code: getKeyboardEventCode(c), |
| 320 | + })) |
| 321 | + : [k]) |
| 322 | + .reduce((arr, k) => arr.concat(k), []); |
| 323 | + // Throw an error if no keys have been specified. Calling this function with no |
| 324 | + // keys should not result in a focus event being dispatched unexpectedly. |
| 325 | + if (keys.length === 0) { |
| 326 | + throw getNoKeysSpecifiedError(); |
| 327 | + } |
| 328 | + // We simulate the user typing in a value by incrementally assigning the value below. The problem |
| 329 | + // is that for some input types, the browser won't allow for an invalid value to be set via the |
| 330 | + // `value` property which will always be the case when going character-by-character. If we detect |
| 331 | + // such an input, we have to set the value all at once or listeners to the `input` event (e.g. |
| 332 | + // the `ReactiveFormsModule` uses such an approach) won't receive the correct value. |
| 333 | + const enterValueIncrementally = inputType === 'number' |
| 334 | + ? // The value can be set character by character in number inputs if it doesn't have any decimals. |
| 335 | + keys.every(key => key.key !== '.' && key.key !== '-' && key.keyCode !== PERIOD) |
| 336 | + : incrementalInputTypes.has(inputType); |
| 337 | + triggerFocus(element); |
| 338 | + // When we aren't entering the value incrementally, assign it all at once ahead |
| 339 | + // of time so that any listeners to the key events below will have access to it. |
| 340 | + if (!enterValueIncrementally) { |
| 341 | + element.value = keys.reduce((value, key) => value + (key.key || ''), ''); |
| 342 | + } |
| 343 | + for (const key of keys) { |
| 344 | + dispatchKeyboardEvent(element, 'keydown', key.keyCode, key.key, modifiers, key.code); |
| 345 | + dispatchKeyboardEvent(element, 'keypress', key.keyCode, key.key, modifiers, key.code); |
| 346 | + if (isInput && key.key && key.key.length === 1) { |
| 347 | + if (enterValueIncrementally) { |
| 348 | + element.value += key.key; |
| 349 | + dispatchFakeEvent(element, 'input'); |
| 350 | + } |
| 351 | + } |
| 352 | + dispatchKeyboardEvent(element, 'keyup', key.keyCode, key.key, modifiers, key.code); |
| 353 | + } |
| 354 | + // Since we weren't dispatching `input` events while sending the keys, we have to do it now. |
| 355 | + if (!enterValueIncrementally) { |
| 356 | + dispatchFakeEvent(element, 'input'); |
| 357 | + } |
| 358 | +} |
| 359 | +/** |
| 360 | + * Clears the text in an input or textarea element. |
| 361 | + * @docs-private |
| 362 | + */ |
| 363 | +function clearElement(element) { |
| 364 | + triggerFocus(element); |
| 365 | + element.value = ''; |
| 366 | + dispatchFakeEvent(element, 'input'); |
| 367 | +} |
| 368 | + |
84 | 369 | /** Maps `TestKey` constants to the `keyCode` and `key` values used by native browser events. */ |
85 | 370 | const keyMap = { |
86 | 371 | [TestKey.BACKSPACE]: { keyCode: BACKSPACE, key: 'Backspace', code: 'Backspace' }, |
|
0 commit comments