ActionDispatcher.js 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598
  1. "use strict";
  2. /**
  3. * Copyright 2023 Google LLC.
  4. * Copyright (c) Microsoft Corporation.
  5. *
  6. * Licensed under the Apache License, Version 2.0 (the "License");
  7. * you may not use this file except in compliance with the License.
  8. * You may obtain a copy of the License at
  9. *
  10. * http://www.apache.org/licenses/LICENSE-2.0
  11. *
  12. * Unless required by applicable law or agreed to in writing, software
  13. * distributed under the License is distributed on an "AS IS" BASIS,
  14. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  15. * See the License for the specific language governing permissions and
  16. * limitations under the License.
  17. */
  18. Object.defineProperty(exports, "__esModule", { value: true });
  19. exports.ActionDispatcher = void 0;
  20. const protocol_js_1 = require("../../../protocol/protocol.js");
  21. const assert_js_1 = require("../../../utils/assert.js");
  22. const USKeyboardLayout_js_1 = require("./USKeyboardLayout.js");
  23. const keyUtils_js_1 = require("./keyUtils.js");
  24. /** https://w3c.github.io/webdriver/#dfn-center-point */
  25. const CALCULATE_IN_VIEW_CENTER_PT_DECL = ((i) => {
  26. const t = i.getClientRects()[0], e = Math.max(0, Math.min(t.x, t.x + t.width)), n = Math.min(window.innerWidth, Math.max(t.x, t.x + t.width)), h = Math.max(0, Math.min(t.y, t.y + t.height)), m = Math.min(window.innerHeight, Math.max(t.y, t.y + t.height));
  27. return [e + ((n - e) >> 1), h + ((m - h) >> 1)];
  28. }).toString();
  29. const IS_MAC_DECL = (() => {
  30. return navigator.platform.toLowerCase().includes('mac');
  31. }).toString();
  32. async function getElementCenter(context, element) {
  33. const { result } = await (await context.getOrCreateSandbox(undefined)).callFunction(CALCULATE_IN_VIEW_CENTER_PT_DECL, { type: 'undefined' }, [element], false, 'none', {});
  34. if (result.type === 'exception') {
  35. throw new protocol_js_1.Message.NoSuchElementException(`Origin element ${element.sharedId} was not found`);
  36. }
  37. (0, assert_js_1.assert)(result.result.type === 'array');
  38. (0, assert_js_1.assert)(result.result.value?.[0]?.type === 'number');
  39. (0, assert_js_1.assert)(result.result.value?.[1]?.type === 'number');
  40. const { result: { value: [{ value: x }, { value: y }], }, } = result;
  41. return { x: x, y: y };
  42. }
  43. class ActionDispatcher {
  44. static isMacOS = async (context) => {
  45. const { result } = await (await context.getOrCreateSandbox(undefined)).callFunction(IS_MAC_DECL, { type: 'undefined' }, [], false, 'none', {});
  46. (0, assert_js_1.assert)(result.type !== 'exception');
  47. (0, assert_js_1.assert)(result.result.type === 'boolean');
  48. return result.result.value;
  49. };
  50. #tickStart = 0;
  51. #tickDuration = 0;
  52. #inputState;
  53. #context;
  54. #isMacOS;
  55. constructor(inputState, context, isMacOS) {
  56. this.#inputState = inputState;
  57. this.#context = context;
  58. this.#isMacOS = isMacOS;
  59. }
  60. async dispatchActions(optionsByTick) {
  61. await this.#inputState.queue.run(async () => {
  62. for (const options of optionsByTick) {
  63. await this.dispatchTickActions(options);
  64. }
  65. });
  66. }
  67. async dispatchTickActions(options) {
  68. this.#tickStart = performance.now();
  69. this.#tickDuration = 0;
  70. for (const { action } of options) {
  71. if ('duration' in action && action.duration !== undefined) {
  72. this.#tickDuration = Math.max(this.#tickDuration, action.duration);
  73. }
  74. }
  75. const promises = [
  76. new Promise((resolve) => setTimeout(resolve, this.#tickDuration)),
  77. ];
  78. for (const option of options) {
  79. promises.push(this.#dispatchAction(option));
  80. }
  81. await Promise.all(promises);
  82. }
  83. async #dispatchAction({ id, action }) {
  84. const source = this.#inputState.get(id);
  85. const keyState = this.#inputState.getGlobalKeyState();
  86. switch (action.type) {
  87. case protocol_js_1.Input.ActionType.KeyDown: {
  88. // SAFETY: The source is validated before.
  89. await this.#dispatchKeyDownAction(source, action);
  90. this.#inputState.cancelList.push({
  91. id,
  92. action: {
  93. ...action,
  94. type: protocol_js_1.Input.ActionType.KeyUp,
  95. },
  96. });
  97. break;
  98. }
  99. case protocol_js_1.Input.ActionType.KeyUp: {
  100. // SAFETY: The source is validated before.
  101. await this.#dispatchKeyUpAction(source, action);
  102. break;
  103. }
  104. case protocol_js_1.Input.ActionType.Pause: {
  105. // TODO: Implement waiting on the input source.
  106. break;
  107. }
  108. case protocol_js_1.Input.ActionType.PointerDown: {
  109. // SAFETY: The source is validated before.
  110. await this.#dispatchPointerDownAction(source, keyState, action);
  111. this.#inputState.cancelList.push({
  112. id,
  113. action: {
  114. ...action,
  115. type: protocol_js_1.Input.ActionType.PointerUp,
  116. },
  117. });
  118. break;
  119. }
  120. case protocol_js_1.Input.ActionType.PointerMove: {
  121. // SAFETY: The source is validated before.
  122. await this.#dispatchPointerMoveAction(source, keyState, action);
  123. break;
  124. }
  125. case protocol_js_1.Input.ActionType.PointerUp: {
  126. // SAFETY: The source is validated before.
  127. await this.#dispatchPointerUpAction(source, keyState, action);
  128. break;
  129. }
  130. case protocol_js_1.Input.ActionType.Scroll: {
  131. // SAFETY: The source is validated before.
  132. await this.#dispatchScrollAction(source, keyState, action);
  133. break;
  134. }
  135. }
  136. }
  137. #dispatchPointerDownAction(source, keyState, action) {
  138. const { button } = action;
  139. if (source.pressed.has(button)) {
  140. return;
  141. }
  142. source.pressed.add(button);
  143. const { x, y, subtype: pointerType } = source;
  144. const { width, height, pressure, twist, tangentialPressure } = action;
  145. const { tiltX, tiltY } = 'tiltX' in action ? action : {};
  146. // TODO: Implement azimuth/altitude angle.
  147. // --- Platform-specific code begins here ---
  148. const { modifiers } = keyState;
  149. switch (pointerType) {
  150. case protocol_js_1.Input.PointerType.Mouse:
  151. case protocol_js_1.Input.PointerType.Pen:
  152. source.setClickCount({ x, y, timeStamp: performance.now() });
  153. // TODO: Implement width and height when available.
  154. return this.#context.cdpTarget.cdpClient.sendCommand('Input.dispatchMouseEvent', {
  155. type: 'mousePressed',
  156. x,
  157. y,
  158. modifiers,
  159. button: (() => {
  160. switch (button) {
  161. case 0:
  162. return 'left';
  163. case 1:
  164. return 'middle';
  165. case 2:
  166. return 'right';
  167. case 3:
  168. return 'back';
  169. case 4:
  170. return 'forward';
  171. default:
  172. return 'none';
  173. }
  174. })(),
  175. buttons: source.buttons,
  176. clickCount: source.clickCount,
  177. pointerType,
  178. tangentialPressure,
  179. tiltX,
  180. tiltY,
  181. twist,
  182. force: pressure,
  183. });
  184. case protocol_js_1.Input.PointerType.Touch:
  185. return this.#context.cdpTarget.cdpClient.sendCommand('Input.dispatchTouchEvent', {
  186. type: 'touchStart',
  187. touchPoints: [
  188. {
  189. x,
  190. y,
  191. radiusX: width,
  192. radiusY: height,
  193. tangentialPressure,
  194. tiltX,
  195. tiltY,
  196. twist,
  197. force: pressure,
  198. id: source.pointerId,
  199. },
  200. ],
  201. modifiers,
  202. });
  203. }
  204. // --- Platform-specific code ends here ---
  205. }
  206. #dispatchPointerUpAction(source, keyState, action) {
  207. const { button } = action;
  208. if (!source.pressed.has(button)) {
  209. return;
  210. }
  211. source.pressed.delete(button);
  212. const { x, y, subtype: pointerType } = source;
  213. // --- Platform-specific code begins here ---
  214. const { modifiers } = keyState;
  215. switch (pointerType) {
  216. case protocol_js_1.Input.PointerType.Mouse:
  217. case protocol_js_1.Input.PointerType.Pen:
  218. // TODO: Implement width and height when available.
  219. return this.#context.cdpTarget.cdpClient.sendCommand('Input.dispatchMouseEvent', {
  220. type: 'mouseReleased',
  221. x,
  222. y,
  223. modifiers,
  224. button: (() => {
  225. switch (button) {
  226. case 0:
  227. return 'left';
  228. case 1:
  229. return 'middle';
  230. case 2:
  231. return 'right';
  232. case 3:
  233. return 'back';
  234. case 4:
  235. return 'forward';
  236. default:
  237. return 'none';
  238. }
  239. })(),
  240. buttons: source.buttons,
  241. clickCount: source.clickCount,
  242. pointerType,
  243. });
  244. case protocol_js_1.Input.PointerType.Touch:
  245. return this.#context.cdpTarget.cdpClient.sendCommand('Input.dispatchTouchEvent', {
  246. type: 'touchEnd',
  247. touchPoints: [
  248. {
  249. x,
  250. y,
  251. id: source.pointerId,
  252. },
  253. ],
  254. modifiers,
  255. });
  256. }
  257. // --- Platform-specific code ends here ---
  258. }
  259. async #dispatchPointerMoveAction(source, keyState, action) {
  260. const { x: startX, y: startY, subtype: pointerType } = source;
  261. const { width, height, pressure, twist, tangentialPressure, x: offsetX, y: offsetY, origin = 'viewport', duration = this.#tickDuration, } = action;
  262. const { tiltX, tiltY } = 'tiltX' in action ? action : {};
  263. // TODO: Implement azimuth/altitude angle.
  264. const { targetX, targetY } = await this.#getCoordinateFromOrigin(origin, offsetX, offsetY, startX, startY);
  265. if (targetX < 0 || targetY < 0) {
  266. throw new protocol_js_1.Message.MoveTargetOutOfBoundsException(`Cannot move beyond viewport (x: ${targetX}, y: ${targetY})`);
  267. }
  268. let last;
  269. do {
  270. const ratio = duration > 0 ? (performance.now() - this.#tickStart) / duration : 1;
  271. last = ratio >= 1;
  272. let x;
  273. let y;
  274. if (last) {
  275. x = targetX;
  276. y = targetY;
  277. }
  278. else {
  279. x = Math.round(ratio * (targetX - startX) + startX);
  280. y = Math.round(ratio * (targetY - startY) + startY);
  281. }
  282. if (source.x !== x || source.y !== y) {
  283. // --- Platform-specific code begins here ---
  284. const { modifiers } = keyState;
  285. switch (pointerType) {
  286. case protocol_js_1.Input.PointerType.Mouse:
  287. case protocol_js_1.Input.PointerType.Pen:
  288. // TODO: Implement width and height when available.
  289. await this.#context.cdpTarget.cdpClient.sendCommand('Input.dispatchMouseEvent', {
  290. type: 'mouseMoved',
  291. x,
  292. y,
  293. modifiers,
  294. clickCount: 0,
  295. buttons: source.buttons,
  296. pointerType,
  297. tangentialPressure,
  298. tiltX,
  299. tiltY,
  300. twist,
  301. force: pressure,
  302. });
  303. break;
  304. case protocol_js_1.Input.PointerType.Touch:
  305. await this.#context.cdpTarget.cdpClient.sendCommand('Input.dispatchTouchEvent', {
  306. type: 'touchMove',
  307. touchPoints: [
  308. {
  309. x,
  310. y,
  311. radiusX: width,
  312. radiusY: height,
  313. tangentialPressure,
  314. tiltX,
  315. tiltY,
  316. twist,
  317. force: pressure,
  318. id: source.pointerId,
  319. },
  320. ],
  321. modifiers,
  322. });
  323. break;
  324. }
  325. // --- Platform-specific code ends here ---
  326. source.x = x;
  327. source.y = y;
  328. }
  329. } while (!last);
  330. }
  331. async #getCoordinateFromOrigin(origin, offsetX, offsetY, startX, startY) {
  332. let targetX;
  333. let targetY;
  334. switch (origin) {
  335. case 'viewport':
  336. targetX = offsetX;
  337. targetY = offsetY;
  338. break;
  339. case 'pointer':
  340. targetX = startX + offsetX;
  341. targetY = startY + offsetY;
  342. break;
  343. default: {
  344. const { x: posX, y: posY } = await getElementCenter(this.#context, origin.element);
  345. // SAFETY: These can never be special numbers.
  346. targetX = posX + offsetX;
  347. targetY = posY + offsetY;
  348. break;
  349. }
  350. }
  351. return { targetX, targetY };
  352. }
  353. async #dispatchScrollAction(_source, keyState, action) {
  354. const { deltaX: targetDeltaX, deltaY: targetDeltaY, x: offsetX, y: offsetY, origin = 'viewport', duration = this.#tickDuration, } = action;
  355. if (origin === 'pointer') {
  356. throw new protocol_js_1.Message.InvalidArgumentException('"pointer" origin is invalid for scrolling.');
  357. }
  358. const { targetX, targetY } = await this.#getCoordinateFromOrigin(origin, offsetX, offsetY, 0, 0);
  359. if (targetX < 0 || targetY < 0) {
  360. throw new protocol_js_1.Message.MoveTargetOutOfBoundsException(`Cannot move beyond viewport (x: ${targetX}, y: ${targetY})`);
  361. }
  362. let currentDeltaX = 0;
  363. let currentDeltaY = 0;
  364. let last;
  365. do {
  366. const ratio = duration > 0 ? (performance.now() - this.#tickStart) / duration : 1;
  367. last = ratio >= 1;
  368. let deltaX;
  369. let deltaY;
  370. if (last) {
  371. deltaX = targetDeltaX - currentDeltaX;
  372. deltaY = targetDeltaY - currentDeltaY;
  373. }
  374. else {
  375. deltaX = Math.round(ratio * targetDeltaX - currentDeltaX);
  376. deltaY = Math.round(ratio * targetDeltaY - currentDeltaY);
  377. }
  378. if (deltaX !== 0 || deltaY !== 0) {
  379. // --- Platform-specific code begins here ---
  380. const { modifiers } = keyState;
  381. await this.#context.cdpTarget.cdpClient.sendCommand('Input.dispatchMouseEvent', {
  382. type: 'mouseWheel',
  383. deltaX,
  384. deltaY,
  385. x: targetX,
  386. y: targetY,
  387. modifiers,
  388. });
  389. // --- Platform-specific code ends here ---
  390. currentDeltaX += deltaX;
  391. currentDeltaY += deltaY;
  392. }
  393. } while (!last);
  394. }
  395. #dispatchKeyDownAction(source, action) {
  396. const rawKey = action.value;
  397. const key = (0, keyUtils_js_1.getNormalizedKey)(rawKey);
  398. const repeat = source.pressed.has(key);
  399. const code = (0, keyUtils_js_1.getKeyCode)(rawKey);
  400. const location = (0, keyUtils_js_1.getKeyLocation)(rawKey);
  401. switch (key) {
  402. case 'Alt':
  403. source.alt = true;
  404. break;
  405. case 'Shift':
  406. source.shift = true;
  407. break;
  408. case 'Control':
  409. source.ctrl = true;
  410. break;
  411. case 'Meta':
  412. source.meta = true;
  413. break;
  414. }
  415. source.pressed.add(key);
  416. const { modifiers } = source;
  417. // --- Platform-specific code begins here ---
  418. // The spread is a little hack so JS gives us an array of unicode characters
  419. // to measure.
  420. const unmodifiedText = getKeyEventUnmodifiedText(key, source);
  421. const text = getKeyEventText(code ?? '', source) ?? unmodifiedText;
  422. let command;
  423. // The following commands need to be declared because Chromium doesn't
  424. // handle them. See
  425. // https://source.chromium.org/chromium/chromium/src/+/refs/heads/main:third_party/blink/renderer/core/editing/editing_behavior.cc;l=169;drc=b8143cf1dfd24842890fcd831c4f5d909bef4fc4;bpv=0;bpt=1.
  426. if (this.#isMacOS && source.meta) {
  427. switch (code) {
  428. case 'KeyA':
  429. command = 'SelectAll';
  430. break;
  431. case 'KeyC':
  432. command = 'Copy';
  433. break;
  434. case 'KeyV':
  435. command = source.shift ? 'PasteAndMatchStyle' : 'Paste';
  436. break;
  437. case 'KeyX':
  438. command = 'Cut';
  439. break;
  440. case 'KeyZ':
  441. command = source.shift ? 'Redo' : 'Undo';
  442. break;
  443. default:
  444. }
  445. }
  446. return this.#context.cdpTarget.cdpClient.sendCommand('Input.dispatchKeyEvent', {
  447. type: text ? 'keyDown' : 'rawKeyDown',
  448. windowsVirtualKeyCode: USKeyboardLayout_js_1.KeyToKeyCode[key],
  449. key,
  450. code,
  451. text,
  452. unmodifiedText,
  453. autoRepeat: repeat,
  454. isSystemKey: source.alt || undefined,
  455. location: location < 3 ? location : undefined,
  456. isKeypad: location === 3,
  457. modifiers,
  458. commands: command ? [command] : undefined,
  459. });
  460. // --- Platform-specific code ends here ---
  461. }
  462. #dispatchKeyUpAction(source, action) {
  463. const rawKey = action.value;
  464. const key = (0, keyUtils_js_1.getNormalizedKey)(rawKey);
  465. if (!source.pressed.has(key)) {
  466. return;
  467. }
  468. const code = (0, keyUtils_js_1.getKeyCode)(rawKey);
  469. const location = (0, keyUtils_js_1.getKeyLocation)(rawKey);
  470. switch (key) {
  471. case 'Alt':
  472. source.alt = false;
  473. break;
  474. case 'Shift':
  475. source.shift = false;
  476. break;
  477. case 'Control':
  478. source.ctrl = false;
  479. break;
  480. case 'Meta':
  481. source.meta = false;
  482. break;
  483. }
  484. source.pressed.delete(key);
  485. const { modifiers } = source;
  486. // --- Platform-specific code begins here ---
  487. // The spread is a little hack so JS gives us an array of unicode characters
  488. // to measure.
  489. const unmodifiedText = getKeyEventUnmodifiedText(key, source);
  490. const text = getKeyEventText(code ?? '', source) ?? unmodifiedText;
  491. return this.#context.cdpTarget.cdpClient.sendCommand('Input.dispatchKeyEvent', {
  492. type: 'keyUp',
  493. windowsVirtualKeyCode: USKeyboardLayout_js_1.KeyToKeyCode[key],
  494. key,
  495. code,
  496. text,
  497. unmodifiedText,
  498. location: location < 3 ? location : undefined,
  499. isSystemKey: source.alt || undefined,
  500. isKeypad: location === 3,
  501. modifiers,
  502. });
  503. // --- Platform-specific code ends here ---
  504. }
  505. }
  506. exports.ActionDispatcher = ActionDispatcher;
  507. const getKeyEventUnmodifiedText = (key, source) => {
  508. if (key === 'Enter') {
  509. return '\r';
  510. }
  511. return [...key].length === 1
  512. ? source.shift
  513. ? key.toLocaleUpperCase('en-US')
  514. : key
  515. : undefined;
  516. };
  517. const getKeyEventText = (code, source) => {
  518. if (source.ctrl) {
  519. switch (code) {
  520. case 'Digit2':
  521. if (source.shift) {
  522. return '\x00';
  523. }
  524. break;
  525. case 'KeyA':
  526. return '\x01';
  527. case 'KeyB':
  528. return '\x02';
  529. case 'KeyC':
  530. return '\x03';
  531. case 'KeyD':
  532. return '\x04';
  533. case 'KeyE':
  534. return '\x05';
  535. case 'KeyF':
  536. return '\x06';
  537. case 'KeyG':
  538. return '\x07';
  539. case 'KeyH':
  540. return '\x08';
  541. case 'KeyI':
  542. return '\x09';
  543. case 'KeyJ':
  544. return '\x0A';
  545. case 'KeyK':
  546. return '\x0B';
  547. case 'KeyL':
  548. return '\x0C';
  549. case 'KeyM':
  550. return '\x0D';
  551. case 'KeyN':
  552. return '\x0E';
  553. case 'KeyO':
  554. return '\x0F';
  555. case 'KeyP':
  556. return '\x10';
  557. case 'KeyQ':
  558. return '\x11';
  559. case 'KeyR':
  560. return '\x12';
  561. case 'KeyS':
  562. return '\x13';
  563. case 'KeyT':
  564. return '\x14';
  565. case 'KeyU':
  566. return '\x15';
  567. case 'KeyV':
  568. return '\x16';
  569. case 'KeyW':
  570. return '\x17';
  571. case 'KeyX':
  572. return '\x18';
  573. case 'KeyY':
  574. return '\x19';
  575. case 'KeyZ':
  576. return '\x1A';
  577. case 'BracketLeft':
  578. return '\x1B';
  579. case 'Backslash':
  580. return '\x1C';
  581. case 'BracketRight':
  582. return '\x1D';
  583. case 'Digit6':
  584. if (source.shift) {
  585. return '\x1E';
  586. }
  587. break;
  588. case 'Minus':
  589. return '\x1F';
  590. }
  591. return '';
  592. }
  593. if (source.alt) {
  594. return '';
  595. }
  596. return;
  597. };
  598. //# sourceMappingURL=ActionDispatcher.js.map