"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 |