Skip to content

Commit

Permalink
feat: implement Input.setFiles (#1705)
Browse files Browse the repository at this point in the history
  • Loading branch information
jrandolf-2 authored Jan 15, 2024
1 parent 6eceae9 commit 50d1921
Show file tree
Hide file tree
Showing 9 changed files with 538 additions and 10 deletions.
3 changes: 3 additions & 0 deletions src/bidiMapper/BidiNoOpParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,9 @@ export class BidiNoOpParser implements BidiCommandParameterParser {
parseReleaseActionsParams(params: unknown): Input.ReleaseActionsParameters {
return params as Input.ReleaseActionsParameters;
}
parseSetFilesParams(params: unknown): Input.SetFilesParameters {
return params as Input.SetFilesParameters;
}
// keep-sorted end

// Network domain
Expand Down
1 change: 1 addition & 0 deletions src/bidiMapper/BidiParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ export interface BidiCommandParameterParser {
// keep-sorted start block=yes
parsePerformActionsParams(params: unknown): Input.PerformActionsParameters;
parseReleaseActionsParams(params: unknown): Input.ReleaseActionsParameters;
parseSetFilesParams(params: unknown): Input.SetFilesParameters;
// keep-sorted end

// Network domain
Expand Down
9 changes: 6 additions & 3 deletions src/bidiMapper/CommandProcessor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,10 @@ export class CommandProcessor extends EventEmitter<CommandProcessorEventsMap> {
cdpConnection,
browserCdpClient
);
this.#inputProcessor = new InputProcessor(browsingContextStorage);
this.#inputProcessor = new InputProcessor(
browsingContextStorage,
realmStorage
);
this.#networkProcessor = new NetworkProcessor(
browsingContextStorage,
networkStorage
Expand Down Expand Up @@ -220,8 +223,8 @@ export class CommandProcessor extends EventEmitter<CommandProcessorEventsMap> {
this.#parser.parseReleaseActionsParams(command.params)
);
case 'input.setFiles':
throw new UnsupportedOperationException(
`Command '${command.method}' not yet implemented.`
return await this.#inputProcessor.setFiles(
this.#parser.parseSetFilesParams(command.params)
);
// keep-sorted end

Expand Down
127 changes: 126 additions & 1 deletion src/bidiMapper/domains/input/InputProcessor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,22 +17,33 @@
import {
Input,
InvalidArgumentException,
NoSuchFrameException,
Script,
UnableToSetFileInputException,
type EmptyResult,
NoSuchElementException,
} from '../../../protocol/protocol.js';
import {assert} from '../../../utils/assert.js';
import type {BrowsingContextStorage} from '../context/BrowsingContextStorage.js';
import {ActionDispatcher} from '../input/ActionDispatcher.js';
import type {ActionOption} from '../input/ActionOption.js';
import {SourceType} from '../input/InputSource.js';
import type {InputState} from '../input/InputState.js';
import {InputStateManager} from '../input/InputStateManager.js';
import type {RealmStorage} from '../script/RealmStorage.js';

export class InputProcessor {
readonly #browsingContextStorage: BrowsingContextStorage;
readonly #realmStorage: RealmStorage;

readonly #inputStateManager = new InputStateManager();

constructor(browsingContextStorage: BrowsingContextStorage) {
constructor(
browsingContextStorage: BrowsingContextStorage,
realmStorage: RealmStorage
) {
this.#browsingContextStorage = browsingContextStorage;
this.#realmStorage = realmStorage;
}

async performActions(
Expand Down Expand Up @@ -66,6 +77,120 @@ export class InputProcessor {
return {};
}

async setFiles(params: Input.SetFilesParameters): Promise<EmptyResult> {
const realm = this.#realmStorage.findRealm({
browsingContextId: params.context,
});
if (realm === undefined) {
throw new NoSuchFrameException(
`Could not find browsingContext ${params.context}`
);
}

let isFileInput;
try {
const result = await realm.callFunction(
String(function getFiles(this: unknown) {
return (
this instanceof HTMLInputElement &&
this.type === 'file' &&
!this.disabled
);
}),
params.element,
[],
false,
Script.ResultOwnership.None,
{},
false
);
assert(result.type === 'success');
assert(result.result.type === 'boolean');
isFileInput = result.result.value;
} catch {
throw new NoSuchElementException(
`Could not find element ${params.element.sharedId}`
);
}

if (!isFileInput) {
throw new UnableToSetFileInputException(
`Element ${params.element.sharedId} is not a mutable file input.`
);
}

// Our goal here is to iterate over the input element files and get their
// file paths.
const paths: string[] = [];
for (let i = 0; i < params.files.length; ++i) {
const result: Script.EvaluateResult = await realm.callFunction(
String(function getFiles(this: HTMLInputElement, index: number) {
if (!this.files) {
// We use `null` because `item` also returns null.
return null;
}
return this.files.item(index);
}),
params.element,
[{type: 'number', value: 0}],
false,
Script.ResultOwnership.Root,
{},
false
);
assert(result.type === 'success');
if (result.result.type !== 'object') {
break;
}

const {handle}: {handle?: string} = result.result;
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.deserializeToCdpArg(params.element);
// This cannot throw since this was just used in `callFunction` above.
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: HTMLInputElement) {
this.dispatchEvent(
new Event('cancel', {
bubbles: true,
})
);
}),
params.element,
[],
false,
Script.ResultOwnership.None,
{},
false
);
}
return {};
}

#getActionsByTick(
params: Input.PerformActionsParameters,
inputState: InputState
Expand Down
12 changes: 6 additions & 6 deletions src/bidiMapper/domains/script/Realm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -408,10 +408,10 @@ export class Realm {
keyArg = {value: key};
} else {
// Key is a serialized value.
keyArg = await this.#deserializeToCdpArg(key);
keyArg = await this.deserializeToCdpArg(key);
}

const valueArg = await this.#deserializeToCdpArg(value);
const valueArg = await this.deserializeToCdpArg(value);

keyValueArray.push(keyArg);
keyValueArray.push(valueArg);
Expand All @@ -424,7 +424,7 @@ export class Realm {
listLocalValue: Script.ListLocalValue
): Promise<Protocol.Runtime.CallArgument[]> {
return await Promise.all(
listLocalValue.map((localValue) => this.#deserializeToCdpArg(localValue))
listLocalValue.map((localValue) => this.deserializeToCdpArg(localValue))
);
}

Expand Down Expand Up @@ -480,11 +480,11 @@ export class Realm {
}`;

const thisAndArgumentsList = [
await this.#deserializeToCdpArg(thisLocalValue),
await this.deserializeToCdpArg(thisLocalValue),
...(await Promise.all(
argumentsLocalValues.map(
async (argumentLocalValue: Script.LocalValue) =>
await this.#deserializeToCdpArg(argumentLocalValue)
await this.deserializeToCdpArg(argumentLocalValue)
)
)),
];
Expand Down Expand Up @@ -536,7 +536,7 @@ export class Realm {
};
}

async #deserializeToCdpArg(
async deserializeToCdpArg(
localValue: Script.LocalValue
): Promise<Protocol.Runtime.CallArgument> {
if ('sharedId' in localValue && localValue.sharedId) {
Expand Down
3 changes: 3 additions & 0 deletions src/bidiTab/BidiParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,9 @@ export class BidiParser implements BidiCommandParameterParser {
parseReleaseActionsParams(params: unknown): Input.ReleaseActionsParameters {
return Parser.Input.parseReleaseActionsParams(params);
}
parseSetFilesParams(params: unknown): Input.SetFilesParameters {
return Parser.Input.parseSetFilesParams(params);
}
// keep-sorted end

// Network domain
Expand Down
6 changes: 6 additions & 0 deletions src/protocol-parser/protocol-parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,12 @@ export namespace Input {
WebDriverBidi.Input.ReleaseActionsParametersSchema
);
}

export function parseSetFilesParams(
params: unknown
): Protocol.Input.SetFilesParameters {
return parseObject(params, WebDriverBidi.Input.SetFilesParametersSchema);
}
}

export namespace Storage {
Expand Down
Loading

0 comments on commit 50d1921

Please sign in to comment.