"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.InputProcessor = void 0; /* * Copyright 2023 Google LLC. * Copyright (c) Microsoft Corporation. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ const protocol_js_1 = require("../../../protocol/protocol.js"); const assert_js_1 = require("../../../utils/assert.js"); const ActionDispatcher_js_1 = require("../input/ActionDispatcher.js"); const InputStateManager_js_1 = require("../input/InputStateManager.js"); class InputProcessor { #browsingContextStorage; #inputStateManager = new InputStateManager_js_1.InputStateManager(); constructor(browsingContextStorage) { this.#browsingContextStorage = browsingContextStorage; } async performActions(params) { const context = this.#browsingContextStorage.getContext(params.context); const inputState = this.#inputStateManager.get(context.top); const actionsByTick = this.#getActionsByTick(params, inputState); const dispatcher = new ActionDispatcher_js_1.ActionDispatcher(inputState, context, await ActionDispatcher_js_1.ActionDispatcher.isMacOS(context).catch(() => false)); await dispatcher.dispatchActions(actionsByTick); return {}; } async releaseActions(params) { const context = this.#browsingContextStorage.getContext(params.context); const topContext = context.top; const inputState = this.#inputStateManager.get(topContext); const dispatcher = new ActionDispatcher_js_1.ActionDispatcher(inputState, context, await ActionDispatcher_js_1.ActionDispatcher.isMacOS(context).catch(() => false)); await dispatcher.dispatchTickActions(inputState.cancelList.reverse()); this.#inputStateManager.delete(topContext); return {}; } async setFiles(params) { const context = this.#browsingContextStorage.getContext(params.context); const realm = await context.getOrCreateSandbox(undefined); let result; try { result = await realm.callFunction(String(function getFiles(fileListLength) { if (!(this instanceof HTMLInputElement)) { if (this instanceof Element) { return 1 /* ErrorCode.Element */; } return 0 /* ErrorCode.Node */; } if (this.type !== 'file') { return 2 /* ErrorCode.Type */; } if (this.disabled) { return 3 /* ErrorCode.Disabled */; } if (fileListLength > 1 && !this.multiple) { return 4 /* ErrorCode.Multiple */; } return; }), false, params.element, [{ type: 'number', value: params.files.length }]); } catch { throw new protocol_js_1.NoSuchNodeException(`Could not find element ${params.element.sharedId}`); } (0, assert_js_1.assert)(result.type === 'success'); if (result.result.type === 'number') { switch (result.result.value) { case 0 /* ErrorCode.Node */: { throw new protocol_js_1.NoSuchElementException(`Could not find element ${params.element.sharedId}`); } case 1 /* ErrorCode.Element */: { throw new protocol_js_1.UnableToSetFileInputException(`Element ${params.element.sharedId} is not a input`); } case 2 /* ErrorCode.Type */: { throw new protocol_js_1.UnableToSetFileInputException(`Input element ${params.element.sharedId} is not a file type`); } case 3 /* ErrorCode.Disabled */: { throw new protocol_js_1.UnableToSetFileInputException(`Input element ${params.element.sharedId} is disabled`); } case 4 /* ErrorCode.Multiple */: { throw new protocol_js_1.UnableToSetFileInputException(`Cannot set multiple files on a non-multiple input element`); } } } /** * The zero-length array is a special case, it seems that * DOM.setFileInputFiles does not actually update the files in that case, so * the solution is to eval the element value to a new FileList directly. */ if (params.files.length === 0) { // XXX: These events should converted to trusted events. Perhaps do this // in `DOM.setFileInputFiles`? await realm.callFunction(String(function dispatchEvent() { if (this.files?.length === 0) { this.dispatchEvent(new Event('cancel', { bubbles: true, })); return; } this.files = new DataTransfer().files; // Dispatch events for this case because it should behave akin to a user action. this.dispatchEvent(new Event('input', { bubbles: true, composed: true })); this.dispatchEvent(new Event('change', { bubbles: true })); }), false, params.element); return {}; } // Our goal here is to iterate over the input element files and get their // file paths. const paths = []; for (let i = 0; i < params.files.length; ++i) { const result = await realm.callFunction(String(function getFiles(index) { return this.files?.item(index); }), false, params.element, [{ type: 'number', value: 0 }], "root" /* Script.ResultOwnership.Root */); (0, assert_js_1.assert)(result.type === 'success'); if (result.result.type !== 'object') { break; } const { handle } = result.result; (0, assert_js_1.assert)(handle !== undefined); const { path } = await realm.cdpClient.sendCommand('DOM.getFileInfo', { objectId: handle, }); paths.push(path); // Cleanup the handle. void realm.disown(handle).catch(undefined); } paths.sort(); // We create a new array so we preserve the order of the original files. const sortedFiles = [...params.files].sort(); if (paths.length !== params.files.length || sortedFiles.some((path, index) => { return paths[index] !== path; })) { const { objectId } = await realm.deserializeForCdp(params.element); // This cannot throw since this was just used in `callFunction` above. (0, assert_js_1.assert)(objectId !== undefined); await realm.cdpClient.sendCommand('DOM.setFileInputFiles', { files: params.files, objectId, }); } else { // XXX: We should dispatch a trusted event. await realm.callFunction(String(function dispatchEvent() { this.dispatchEvent(new Event('cancel', { bubbles: true, })); }), false, params.element); } return {}; } #getActionsByTick(params, inputState) { const actionsByTick = []; for (const action of params.actions) { switch (action.type) { case "pointer" /* SourceType.Pointer */: { action.parameters ??= { pointerType: "mouse" /* Input.PointerType.Mouse */ }; action.parameters.pointerType ??= "mouse" /* Input.PointerType.Mouse */; const source = inputState.getOrCreate(action.id, "pointer" /* SourceType.Pointer */, action.parameters.pointerType); if (source.subtype !== action.parameters.pointerType) { throw new protocol_js_1.InvalidArgumentException(`Expected input source ${action.id} to be ${source.subtype}; got ${action.parameters.pointerType}.`); } break; } default: inputState.getOrCreate(action.id, action.type); } const actions = action.actions.map((item) => ({ id: action.id, action: item, })); for (let i = 0; i < actions.length; i++) { if (actionsByTick.length === i) { actionsByTick.push([]); } actionsByTick[i].push(actions[i]); } } return actionsByTick; } } exports.InputProcessor = InputProcessor; //# sourceMappingURL=InputProcessor.js.map