chore: remove non-input related stuff from input (#369)
This commit is contained in:
parent
310d4b193b
commit
f1d6fe6bd8
|
|
@ -16,7 +16,6 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Page } from './page';
|
import { Page } from './page';
|
||||||
import * as input from './input';
|
|
||||||
import * as network from './network';
|
import * as network from './network';
|
||||||
import * as types from './types';
|
import * as types from './types';
|
||||||
|
|
||||||
|
|
@ -40,8 +39,8 @@ export type BrowserContextOptions = {
|
||||||
ignoreHTTPSErrors?: boolean,
|
ignoreHTTPSErrors?: boolean,
|
||||||
javaScriptEnabled?: boolean,
|
javaScriptEnabled?: boolean,
|
||||||
bypassCSP?: boolean,
|
bypassCSP?: boolean,
|
||||||
mediaType?: input.MediaType,
|
mediaType?: types.MediaType,
|
||||||
colorScheme?: input.ColorScheme,
|
colorScheme?: types.ColorScheme,
|
||||||
userAgent?: string,
|
userAgent?: string,
|
||||||
timezoneId?: string,
|
timezoneId?: string,
|
||||||
geolocation?: types.Geolocation
|
geolocation?: types.Geolocation
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,6 @@ import { CRWorkers, CRWorker } from './features/crWorkers';
|
||||||
import { CRBrowser } from './crBrowser';
|
import { CRBrowser } from './crBrowser';
|
||||||
import { BrowserContext } from '../browserContext';
|
import { BrowserContext } from '../browserContext';
|
||||||
import * as types from '../types';
|
import * as types from '../types';
|
||||||
import * as input from '../input';
|
|
||||||
import { ConsoleMessage } from '../console';
|
import { ConsoleMessage } from '../console';
|
||||||
import * as accessibility from '../accessibility';
|
import * as accessibility from '../accessibility';
|
||||||
|
|
||||||
|
|
@ -309,8 +308,8 @@ export class CRPage implements PageDelegate {
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
async setEmulateMedia(mediaType: input.MediaType | null, mediaColorScheme: input.ColorScheme | null): Promise<void> {
|
async setEmulateMedia(mediaType: types.MediaType | null, colorScheme: types.ColorScheme | null): Promise<void> {
|
||||||
const features = mediaColorScheme ? [{ name: 'prefers-color-scheme', value: mediaColorScheme }] : [];
|
const features = colorScheme ? [{ name: 'prefers-color-scheme', value: colorScheme }] : [];
|
||||||
await this._client.send('Emulation.setEmulatedMedia', { media: mediaType || '', features });
|
await this._client.send('Emulation.setEmulatedMedia', { media: mediaType || '', features });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -459,8 +458,8 @@ export class CRPage implements PageDelegate {
|
||||||
return { width: layoutMetrics.layoutViewport.clientWidth, height: layoutMetrics.layoutViewport.clientHeight };
|
return { width: layoutMetrics.layoutViewport.clientWidth, height: layoutMetrics.layoutViewport.clientHeight };
|
||||||
}
|
}
|
||||||
|
|
||||||
async setInputFiles(handle: dom.ElementHandle, files: input.FilePayload[]): Promise<void> {
|
async setInputFiles(handle: dom.ElementHandle, files: types.FilePayload[]): Promise<void> {
|
||||||
await handle.evaluate(input.setFileInputFunction, files);
|
await handle.evaluate(dom.setFileInputFunction, files);
|
||||||
}
|
}
|
||||||
|
|
||||||
async adoptElementHandle<T extends Node>(handle: dom.ElementHandle<T>, to: dom.FrameExecutionContext): Promise<dom.ElementHandle<T>> {
|
async adoptElementHandle<T extends Node>(handle: dom.ElementHandle<T>, to: dom.FrameExecutionContext): Promise<dom.ElementHandle<T>> {
|
||||||
|
|
|
||||||
111
src/dom.ts
111
src/dom.ts
|
|
@ -10,6 +10,10 @@ import * as zsSelectorEngineSource from './generated/zsSelectorEngineSource';
|
||||||
import { assert, helper, debugError } from './helper';
|
import { assert, helper, debugError } from './helper';
|
||||||
import Injected from './injected/injected';
|
import Injected from './injected/injected';
|
||||||
import { Page } from './page';
|
import { Page } from './page';
|
||||||
|
import * as path from 'path';
|
||||||
|
import * as fs from 'fs';
|
||||||
|
|
||||||
|
const readFileAsync = helper.promisify(fs.readFile);
|
||||||
|
|
||||||
export class FrameExecutionContext extends js.ExecutionContext {
|
export class FrameExecutionContext extends js.ExecutionContext {
|
||||||
readonly frame: frames.Frame;
|
readonly frame: frames.Frame;
|
||||||
|
|
@ -284,7 +288,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
|
||||||
return this._performPointerAction(point => this._page.mouse.tripleclick(point.x, point.y, options), options);
|
return this._performPointerAction(point => this._page.mouse.tripleclick(point.x, point.y, options), options);
|
||||||
}
|
}
|
||||||
|
|
||||||
async select(...values: (string | ElementHandle | input.SelectOption)[]): Promise<string[]> {
|
async select(...values: (string | ElementHandle | types.SelectOption)[]): Promise<string[]> {
|
||||||
const options = values.map(value => typeof value === 'object' ? value : { value });
|
const options = values.map(value => typeof value === 'object' ? value : { value });
|
||||||
for (const option of options) {
|
for (const option of options) {
|
||||||
if (option instanceof ElementHandle)
|
if (option instanceof ElementHandle)
|
||||||
|
|
@ -296,18 +300,92 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
|
||||||
if (option.index !== undefined)
|
if (option.index !== undefined)
|
||||||
assert(helper.isNumber(option.index), 'Indices must be numbers. Found index "' + option.index + '" of type "' + (typeof option.index) + '"');
|
assert(helper.isNumber(option.index), 'Indices must be numbers. Found index "' + option.index + '" of type "' + (typeof option.index) + '"');
|
||||||
}
|
}
|
||||||
return this._evaluateInUtility(input.selectFunction, ...options);
|
return this._evaluateInUtility((node: Node, ...optionsToSelect: (Node | types.SelectOption)[]) => {
|
||||||
|
if (node.nodeName.toLowerCase() !== 'select')
|
||||||
|
throw new Error('Element is not a <select> element.');
|
||||||
|
const element = node as HTMLSelectElement;
|
||||||
|
|
||||||
|
const options = Array.from(element.options);
|
||||||
|
element.value = undefined;
|
||||||
|
for (let index = 0; index < options.length; index++) {
|
||||||
|
const option = options[index];
|
||||||
|
option.selected = optionsToSelect.some(optionToSelect => {
|
||||||
|
if (optionToSelect instanceof Node)
|
||||||
|
return option === optionToSelect;
|
||||||
|
let matches = true;
|
||||||
|
if (optionToSelect.value !== undefined)
|
||||||
|
matches = matches && optionToSelect.value === option.value;
|
||||||
|
if (optionToSelect.label !== undefined)
|
||||||
|
matches = matches && optionToSelect.label === option.label;
|
||||||
|
if (optionToSelect.index !== undefined)
|
||||||
|
matches = matches && optionToSelect.index === index;
|
||||||
|
return matches;
|
||||||
|
});
|
||||||
|
if (option.selected && !element.multiple)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
element.dispatchEvent(new Event('input', { 'bubbles': true }));
|
||||||
|
element.dispatchEvent(new Event('change', { 'bubbles': true }));
|
||||||
|
return options.filter(option => option.selected).map(option => option.value);
|
||||||
|
}, ...options);
|
||||||
}
|
}
|
||||||
|
|
||||||
async fill(value: string): Promise<void> {
|
async fill(value: string): Promise<void> {
|
||||||
assert(helper.isString(value), 'Value must be string. Found value "' + value + '" of type "' + (typeof value) + '"');
|
assert(helper.isString(value), 'Value must be string. Found value "' + value + '" of type "' + (typeof value) + '"');
|
||||||
const error = await this._evaluateInUtility(input.fillFunction);
|
const error = await this._evaluateInUtility((node: Node) => {
|
||||||
|
if (node.nodeType !== Node.ELEMENT_NODE)
|
||||||
|
return 'Node is not of type HTMLElement';
|
||||||
|
const element = node as HTMLElement;
|
||||||
|
if (!element.isConnected)
|
||||||
|
return 'Element is not attached to the DOM';
|
||||||
|
if (!element.ownerDocument || !element.ownerDocument.defaultView)
|
||||||
|
return 'Element does not belong to a window';
|
||||||
|
|
||||||
|
const style = element.ownerDocument.defaultView.getComputedStyle(element);
|
||||||
|
if (!style || style.visibility === 'hidden')
|
||||||
|
return 'Element is hidden';
|
||||||
|
if (!element.offsetParent && element.tagName !== 'BODY')
|
||||||
|
return 'Element is not visible';
|
||||||
|
if (element.nodeName.toLowerCase() === 'input') {
|
||||||
|
const input = element as HTMLInputElement;
|
||||||
|
const type = input.getAttribute('type') || '';
|
||||||
|
const kTextInputTypes = new Set(['', 'email', 'password', 'search', 'tel', 'text', 'url']);
|
||||||
|
if (!kTextInputTypes.has(type.toLowerCase()))
|
||||||
|
return 'Cannot fill input of type "' + type + '".';
|
||||||
|
if (input.disabled)
|
||||||
|
return 'Cannot fill a disabled input.';
|
||||||
|
if (input.readOnly)
|
||||||
|
return 'Cannot fill a readonly input.';
|
||||||
|
input.selectionStart = 0;
|
||||||
|
input.selectionEnd = input.value.length;
|
||||||
|
input.focus();
|
||||||
|
} else if (element.nodeName.toLowerCase() === 'textarea') {
|
||||||
|
const textarea = element as HTMLTextAreaElement;
|
||||||
|
if (textarea.disabled)
|
||||||
|
return 'Cannot fill a disabled textarea.';
|
||||||
|
if (textarea.readOnly)
|
||||||
|
return 'Cannot fill a readonly textarea.';
|
||||||
|
textarea.selectionStart = 0;
|
||||||
|
textarea.selectionEnd = textarea.value.length;
|
||||||
|
textarea.focus();
|
||||||
|
} else if (element.isContentEditable) {
|
||||||
|
const range = element.ownerDocument.createRange();
|
||||||
|
range.selectNodeContents(element);
|
||||||
|
const selection = element.ownerDocument.defaultView.getSelection();
|
||||||
|
selection.removeAllRanges();
|
||||||
|
selection.addRange(range);
|
||||||
|
element.focus();
|
||||||
|
} else {
|
||||||
|
return 'Element is not an <input>, <textarea> or [contenteditable] element.';
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
if (error)
|
if (error)
|
||||||
throw new Error(error);
|
throw new Error(error);
|
||||||
await this._page.keyboard.sendCharacters(value);
|
await this._page.keyboard.sendCharacters(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
async setInputFiles(...files: (string | input.FilePayload)[]) {
|
async setInputFiles(...files: (string | types.FilePayload)[]) {
|
||||||
const multiple = await this._evaluateInUtility((node: Node) => {
|
const multiple = await this._evaluateInUtility((node: Node) => {
|
||||||
if (node.nodeType !== Node.ELEMENT_NODE || (node as Element).tagName !== 'INPUT')
|
if (node.nodeType !== Node.ELEMENT_NODE || (node as Element).tagName !== 'INPUT')
|
||||||
throw new Error('Node is not an HTMLInputElement');
|
throw new Error('Node is not an HTMLInputElement');
|
||||||
|
|
@ -315,7 +393,18 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
|
||||||
return input.multiple;
|
return input.multiple;
|
||||||
});
|
});
|
||||||
assert(multiple || files.length <= 1, 'Non-multiple file input can only accept single file!');
|
assert(multiple || files.length <= 1, 'Non-multiple file input can only accept single file!');
|
||||||
await this._page._delegate.setInputFiles(this, await input.loadFiles(files));
|
const filePayloads = await Promise.all(files.map(async item => {
|
||||||
|
if (typeof item === 'string') {
|
||||||
|
const file: types.FilePayload = {
|
||||||
|
name: path.basename(item),
|
||||||
|
type: 'application/octet-stream',
|
||||||
|
data: (await readFileAsync(item)).toString('base64')
|
||||||
|
};
|
||||||
|
return file;
|
||||||
|
}
|
||||||
|
return item;
|
||||||
|
}));
|
||||||
|
await this._page._delegate.setInputFiles(this, filePayloads);
|
||||||
}
|
}
|
||||||
|
|
||||||
async focus() {
|
async focus() {
|
||||||
|
|
@ -454,3 +543,15 @@ export function waitForSelectorTask(selector: string, visibility: types.Visibili
|
||||||
}, await context._injected(), selector, visibility, timeout);
|
}, await context._injected(), selector, visibility, timeout);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const setFileInputFunction = async (element: HTMLInputElement, payloads: types.FilePayload[]) => {
|
||||||
|
const files = await Promise.all(payloads.map(async (file: types.FilePayload) => {
|
||||||
|
const result = await fetch(`data:${file.type};base64,${file.data}`);
|
||||||
|
return new File([await result.blob()], file.name);
|
||||||
|
}));
|
||||||
|
const dt = new DataTransfer();
|
||||||
|
for (const file of files)
|
||||||
|
dt.items.add(file);
|
||||||
|
element.files = dt.files;
|
||||||
|
element.dispatchEvent(new Event('input', { 'bubbles': true }));
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,6 @@ import { FFNetworkManager } from './ffNetworkManager';
|
||||||
import { Events } from '../events';
|
import { Events } from '../events';
|
||||||
import * as dialog from '../dialog';
|
import * as dialog from '../dialog';
|
||||||
import { Protocol } from './protocol';
|
import { Protocol } from './protocol';
|
||||||
import * as input from '../input';
|
|
||||||
import { RawMouseImpl, RawKeyboardImpl } from './ffInput';
|
import { RawMouseImpl, RawKeyboardImpl } from './ffInput';
|
||||||
import { BrowserContext } from '../browserContext';
|
import { BrowserContext } from '../browserContext';
|
||||||
import { getAccessibilityTree } from './ffAccessibility';
|
import { getAccessibilityTree } from './ffAccessibility';
|
||||||
|
|
@ -213,10 +212,10 @@ export class FFPage implements PageDelegate {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async setEmulateMedia(mediaType: input.MediaType | null, mediaColorScheme: input.ColorScheme | null): Promise<void> {
|
async setEmulateMedia(mediaType: types.MediaType | null, colorScheme: types.ColorScheme | null): Promise<void> {
|
||||||
await this._session.send('Page.setEmulatedMedia', {
|
await this._session.send('Page.setEmulatedMedia', {
|
||||||
type: mediaType === null ? undefined : mediaType,
|
type: mediaType === null ? undefined : mediaType,
|
||||||
colorScheme: mediaColorScheme === null ? undefined : mediaColorScheme
|
colorScheme: colorScheme === null ? undefined : colorScheme
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -339,8 +338,8 @@ export class FFPage implements PageDelegate {
|
||||||
return this._page.evaluate(() => ({ width: innerWidth, height: innerHeight }));
|
return this._page.evaluate(() => ({ width: innerWidth, height: innerHeight }));
|
||||||
}
|
}
|
||||||
|
|
||||||
async setInputFiles(handle: dom.ElementHandle, files: input.FilePayload[]): Promise<void> {
|
async setInputFiles(handle: dom.ElementHandle, files: types.FilePayload[]): Promise<void> {
|
||||||
await handle.evaluate(input.setFileInputFunction, files);
|
await handle.evaluate(dom.setFileInputFunction, files);
|
||||||
}
|
}
|
||||||
|
|
||||||
async adoptElementHandle<T extends Node>(handle: dom.ElementHandle<T>, to: dom.FrameExecutionContext): Promise<dom.ElementHandle<T>> {
|
async adoptElementHandle<T extends Node>(handle: dom.ElementHandle<T>, to: dom.FrameExecutionContext): Promise<dom.ElementHandle<T>> {
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ import * as js from './javascript';
|
||||||
import * as dom from './dom';
|
import * as dom from './dom';
|
||||||
import * as network from './network';
|
import * as network from './network';
|
||||||
import { helper, assert, RegisteredListener } from './helper';
|
import { helper, assert, RegisteredListener } from './helper';
|
||||||
import { ClickOptions, MultiClickOptions, PointerActionOptions, SelectOption } from './input';
|
import { ClickOptions, MultiClickOptions, PointerActionOptions } from './input';
|
||||||
import { TimeoutError } from './errors';
|
import { TimeoutError } from './errors';
|
||||||
import { Events } from './events';
|
import { Events } from './events';
|
||||||
import { Page } from './page';
|
import { Page } from './page';
|
||||||
|
|
@ -646,7 +646,7 @@ export class Frame {
|
||||||
await handle.dispose();
|
await handle.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
async select(selector: string, value: string | dom.ElementHandle | SelectOption | string[] | dom.ElementHandle[] | SelectOption[] | undefined, options?: WaitForOptions): Promise<string[]> {
|
async select(selector: string, value: string | dom.ElementHandle | types.SelectOption | string[] | dom.ElementHandle[] | types.SelectOption[] | undefined, options?: WaitForOptions): Promise<string[]> {
|
||||||
const handle = await this._optionallyWaitForSelectorInUtilityContext(selector, options);
|
const handle = await this._optionallyWaitForSelectorInUtilityContext(selector, options);
|
||||||
const values = value === undefined ? [] : Array.isArray(value) ? value : [value];
|
const values = value === undefined ? [] : Array.isArray(value) ? value : [value];
|
||||||
const result = await handle.select(...values);
|
const result = await handle.select(...values);
|
||||||
|
|
|
||||||
128
src/input.ts
128
src/input.ts
|
|
@ -1,14 +1,10 @@
|
||||||
// Copyright (c) Microsoft Corporation.
|
// Copyright (c) Microsoft Corporation.
|
||||||
// Licensed under the MIT license.
|
// Licensed under the MIT license.
|
||||||
|
|
||||||
import * as fs from 'fs';
|
import { assert } from './helper';
|
||||||
import * as path from 'path';
|
|
||||||
import { assert, helper } from './helper';
|
|
||||||
import * as types from './types';
|
import * as types from './types';
|
||||||
import * as keyboardLayout from './usKeyboardLayout';
|
import * as keyboardLayout from './usKeyboardLayout';
|
||||||
|
|
||||||
const readFileAsync = helper.promisify(fs.readFile);
|
|
||||||
|
|
||||||
export type Modifier = 'Alt' | 'Control' | 'Meta' | 'Shift';
|
export type Modifier = 'Alt' | 'Control' | 'Meta' | 'Shift';
|
||||||
export type Button = 'left' | 'right' | 'middle';
|
export type Button = 'left' | 'right' | 'middle';
|
||||||
|
|
||||||
|
|
@ -28,12 +24,6 @@ export type MultiClickOptions = PointerActionOptions & {
|
||||||
button?: Button;
|
button?: Button;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SelectOption = {
|
|
||||||
value?: string;
|
|
||||||
label?: string;
|
|
||||||
index?: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const keypadLocation = keyboardLayout.keypadLocation;
|
export const keypadLocation = keyboardLayout.keypadLocation;
|
||||||
|
|
||||||
type KeyDescription = {
|
type KeyDescription = {
|
||||||
|
|
@ -292,119 +282,3 @@ export class Mouse {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const selectFunction = (node: Node, ...optionsToSelect: (Node | SelectOption)[]) => {
|
|
||||||
if (node.nodeName.toLowerCase() !== 'select')
|
|
||||||
throw new Error('Element is not a <select> element.');
|
|
||||||
const element = node as HTMLSelectElement;
|
|
||||||
|
|
||||||
const options = Array.from(element.options);
|
|
||||||
element.value = undefined;
|
|
||||||
for (let index = 0; index < options.length; index++) {
|
|
||||||
const option = options[index];
|
|
||||||
option.selected = optionsToSelect.some(optionToSelect => {
|
|
||||||
if (optionToSelect instanceof Node)
|
|
||||||
return option === optionToSelect;
|
|
||||||
let matches = true;
|
|
||||||
if (optionToSelect.value !== undefined)
|
|
||||||
matches = matches && optionToSelect.value === option.value;
|
|
||||||
if (optionToSelect.label !== undefined)
|
|
||||||
matches = matches && optionToSelect.label === option.label;
|
|
||||||
if (optionToSelect.index !== undefined)
|
|
||||||
matches = matches && optionToSelect.index === index;
|
|
||||||
return matches;
|
|
||||||
});
|
|
||||||
if (option.selected && !element.multiple)
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
element.dispatchEvent(new Event('input', { 'bubbles': true }));
|
|
||||||
element.dispatchEvent(new Event('change', { 'bubbles': true }));
|
|
||||||
return options.filter(option => option.selected).map(option => option.value);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const fillFunction = (node: Node) => {
|
|
||||||
if (node.nodeType !== Node.ELEMENT_NODE)
|
|
||||||
return 'Node is not of type HTMLElement';
|
|
||||||
const element = node as HTMLElement;
|
|
||||||
if (!element.isConnected)
|
|
||||||
return 'Element is not attached to the DOM';
|
|
||||||
if (!element.ownerDocument || !element.ownerDocument.defaultView)
|
|
||||||
return 'Element does not belong to a window';
|
|
||||||
|
|
||||||
const style = element.ownerDocument.defaultView.getComputedStyle(element);
|
|
||||||
if (!style || style.visibility === 'hidden')
|
|
||||||
return 'Element is hidden';
|
|
||||||
if (!element.offsetParent && element.tagName !== 'BODY')
|
|
||||||
return 'Element is not visible';
|
|
||||||
if (element.nodeName.toLowerCase() === 'input') {
|
|
||||||
const input = element as HTMLInputElement;
|
|
||||||
const type = input.getAttribute('type') || '';
|
|
||||||
const kTextInputTypes = new Set(['', 'email', 'password', 'search', 'tel', 'text', 'url']);
|
|
||||||
if (!kTextInputTypes.has(type.toLowerCase()))
|
|
||||||
return 'Cannot fill input of type "' + type + '".';
|
|
||||||
if (input.disabled)
|
|
||||||
return 'Cannot fill a disabled input.';
|
|
||||||
if (input.readOnly)
|
|
||||||
return 'Cannot fill a readonly input.';
|
|
||||||
input.selectionStart = 0;
|
|
||||||
input.selectionEnd = input.value.length;
|
|
||||||
input.focus();
|
|
||||||
} else if (element.nodeName.toLowerCase() === 'textarea') {
|
|
||||||
const textarea = element as HTMLTextAreaElement;
|
|
||||||
if (textarea.disabled)
|
|
||||||
return 'Cannot fill a disabled textarea.';
|
|
||||||
if (textarea.readOnly)
|
|
||||||
return 'Cannot fill a readonly textarea.';
|
|
||||||
textarea.selectionStart = 0;
|
|
||||||
textarea.selectionEnd = textarea.value.length;
|
|
||||||
textarea.focus();
|
|
||||||
} else if (element.isContentEditable) {
|
|
||||||
const range = element.ownerDocument.createRange();
|
|
||||||
range.selectNodeContents(element);
|
|
||||||
const selection = element.ownerDocument.defaultView.getSelection();
|
|
||||||
selection.removeAllRanges();
|
|
||||||
selection.addRange(range);
|
|
||||||
element.focus();
|
|
||||||
} else {
|
|
||||||
return 'Element is not an <input>, <textarea> or [contenteditable] element.';
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const loadFiles = async (items: (string|FilePayload)[]): Promise<FilePayload[]> => {
|
|
||||||
return Promise.all(items.map(async item => {
|
|
||||||
if (typeof item === 'string') {
|
|
||||||
const file: FilePayload = {
|
|
||||||
name: path.basename(item),
|
|
||||||
type: 'application/octet-stream',
|
|
||||||
data: (await readFileAsync(item)).toString('base64')
|
|
||||||
};
|
|
||||||
return file;
|
|
||||||
} else {
|
|
||||||
return item as FilePayload;
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
export const setFileInputFunction = async (element: HTMLInputElement, payloads: FilePayload[]) => {
|
|
||||||
const files = await Promise.all(payloads.map(async (file: FilePayload) => {
|
|
||||||
const result = await fetch(`data:${file.type};base64,${file.data}`);
|
|
||||||
return new File([await result.blob()], file.name);
|
|
||||||
}));
|
|
||||||
const dt = new DataTransfer();
|
|
||||||
for (const file of files)
|
|
||||||
dt.items.add(file);
|
|
||||||
element.files = dt.files;
|
|
||||||
element.dispatchEvent(new Event('input', { 'bubbles': true }));
|
|
||||||
};
|
|
||||||
|
|
||||||
export type FilePayload = {
|
|
||||||
name: string,
|
|
||||||
type: string,
|
|
||||||
data: string
|
|
||||||
};
|
|
||||||
|
|
||||||
export type MediaType = 'screen' | 'print';
|
|
||||||
export const mediaTypes: Set<MediaType> = new Set(['screen', 'print']);
|
|
||||||
export type ColorScheme = 'dark' | 'light' | 'no-preference';
|
|
||||||
export const mediaColorSchemes: Set<ColorScheme> = new Set(['dark', 'light', 'no-preference']);
|
|
||||||
|
|
|
||||||
16
src/page.ts
16
src/page.ts
|
|
@ -47,7 +47,7 @@ export interface PageDelegate {
|
||||||
|
|
||||||
setExtraHTTPHeaders(extraHTTPHeaders: network.Headers): Promise<void>;
|
setExtraHTTPHeaders(extraHTTPHeaders: network.Headers): Promise<void>;
|
||||||
setViewport(viewport: types.Viewport): Promise<void>;
|
setViewport(viewport: types.Viewport): Promise<void>;
|
||||||
setEmulateMedia(mediaType: input.MediaType | null, colorScheme: input.ColorScheme | null): Promise<void>;
|
setEmulateMedia(mediaType: types.MediaType | null, colorScheme: types.ColorScheme | null): Promise<void>;
|
||||||
setCacheEnabled(enabled: boolean): Promise<void>;
|
setCacheEnabled(enabled: boolean): Promise<void>;
|
||||||
setRequestInterception(enabled: boolean): Promise<void>;
|
setRequestInterception(enabled: boolean): Promise<void>;
|
||||||
setOfflineMode(enabled: boolean): Promise<void>;
|
setOfflineMode(enabled: boolean): Promise<void>;
|
||||||
|
|
@ -65,7 +65,7 @@ export interface PageDelegate {
|
||||||
getOwnerFrame(handle: dom.ElementHandle): Promise<frames.Frame | null>;
|
getOwnerFrame(handle: dom.ElementHandle): Promise<frames.Frame | null>;
|
||||||
getContentQuads(handle: dom.ElementHandle): Promise<types.Quad[] | null>;
|
getContentQuads(handle: dom.ElementHandle): Promise<types.Quad[] | null>;
|
||||||
layoutViewport(): Promise<{ width: number, height: number }>;
|
layoutViewport(): Promise<{ width: number, height: number }>;
|
||||||
setInputFiles(handle: dom.ElementHandle, files: input.FilePayload[]): Promise<void>;
|
setInputFiles(handle: dom.ElementHandle, files: types.FilePayload[]): Promise<void>;
|
||||||
getBoundingBox(handle: dom.ElementHandle): Promise<types.Rect | null>;
|
getBoundingBox(handle: dom.ElementHandle): Promise<types.Rect | null>;
|
||||||
|
|
||||||
getAccessibilityTree(): Promise<accessibility.AXNode>;
|
getAccessibilityTree(): Promise<accessibility.AXNode>;
|
||||||
|
|
@ -73,8 +73,8 @@ export interface PageDelegate {
|
||||||
|
|
||||||
type PageState = {
|
type PageState = {
|
||||||
viewport: types.Viewport | null;
|
viewport: types.Viewport | null;
|
||||||
mediaType: input.MediaType | null;
|
mediaType: types.MediaType | null;
|
||||||
colorScheme: input.ColorScheme | null;
|
colorScheme: types.ColorScheme | null;
|
||||||
extraHTTPHeaders: network.Headers | null;
|
extraHTTPHeaders: network.Headers | null;
|
||||||
cacheEnabled: boolean | null;
|
cacheEnabled: boolean | null;
|
||||||
interceptNetwork: boolean | null;
|
interceptNetwork: boolean | null;
|
||||||
|
|
@ -370,9 +370,9 @@ export class Page extends EventEmitter {
|
||||||
return waitPromise;
|
return waitPromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
async emulateMedia(options: { type?: input.MediaType, colorScheme?: input.ColorScheme }) {
|
async emulateMedia(options: { type?: types.MediaType, colorScheme?: types.ColorScheme }) {
|
||||||
assert(!options.type || input.mediaTypes.has(options.type), 'Unsupported media type: ' + options.type);
|
assert(!options.type || types.mediaTypes.has(options.type), 'Unsupported media type: ' + options.type);
|
||||||
assert(!options.colorScheme || input.mediaColorSchemes.has(options.colorScheme), 'Unsupported color scheme: ' + options.colorScheme);
|
assert(!options.colorScheme || types.colorSchemes.has(options.colorScheme), 'Unsupported color scheme: ' + options.colorScheme);
|
||||||
if (options.type !== undefined)
|
if (options.type !== undefined)
|
||||||
this._state.mediaType = options.type;
|
this._state.mediaType = options.type;
|
||||||
if (options.colorScheme !== undefined)
|
if (options.colorScheme !== undefined)
|
||||||
|
|
@ -476,7 +476,7 @@ export class Page extends EventEmitter {
|
||||||
return this.mainFrame().hover(selector, options);
|
return this.mainFrame().hover(selector, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
async select(selector: string, value: string | dom.ElementHandle | input.SelectOption | string[] | dom.ElementHandle[] | input.SelectOption[] | undefined, options?: frames.WaitForOptions): Promise<string[]> {
|
async select(selector: string, value: string | dom.ElementHandle | types.SelectOption | string[] | dom.ElementHandle[] | types.SelectOption[] | undefined, options?: frames.WaitForOptions): Promise<string[]> {
|
||||||
return this.mainFrame().select(selector, value, options);
|
return this.mainFrame().select(selector, value, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
22
src/types.ts
22
src/types.ts
|
|
@ -55,10 +55,28 @@ export type URLMatch = string | RegExp | ((url: kurl.URL) => boolean);
|
||||||
export type Credentials = {
|
export type Credentials = {
|
||||||
username: string;
|
username: string;
|
||||||
password: string;
|
password: string;
|
||||||
}
|
};
|
||||||
|
|
||||||
export type Geolocation = {
|
export type Geolocation = {
|
||||||
longitude?: number;
|
longitude?: number;
|
||||||
latitude?: number;
|
latitude?: number;
|
||||||
accuracy?: number | undefined;
|
accuracy?: number | undefined;
|
||||||
}
|
};
|
||||||
|
|
||||||
|
export type SelectOption = {
|
||||||
|
value?: string;
|
||||||
|
label?: string;
|
||||||
|
index?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type FilePayload = {
|
||||||
|
name: string,
|
||||||
|
type: string,
|
||||||
|
data: string,
|
||||||
|
};
|
||||||
|
|
||||||
|
export type MediaType = 'screen' | 'print';
|
||||||
|
export const mediaTypes: Set<MediaType> = new Set(['screen', 'print']);
|
||||||
|
|
||||||
|
export type ColorScheme = 'dark' | 'light' | 'no-preference';
|
||||||
|
export const colorSchemes: Set<ColorScheme> = new Set(['dark', 'light', 'no-preference']);
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,6 @@ import * as dialog from '../dialog';
|
||||||
import { WKBrowser } from './wkBrowser';
|
import { WKBrowser } from './wkBrowser';
|
||||||
import { BrowserContext } from '../browserContext';
|
import { BrowserContext } from '../browserContext';
|
||||||
import { RawMouseImpl, RawKeyboardImpl } from './wkInput';
|
import { RawMouseImpl, RawKeyboardImpl } from './wkInput';
|
||||||
import * as input from '../input';
|
|
||||||
import * as types from '../types';
|
import * as types from '../types';
|
||||||
import * as jpeg from 'jpeg-js';
|
import * as jpeg from 'jpeg-js';
|
||||||
import { PNG } from 'pngjs';
|
import { PNG } from 'pngjs';
|
||||||
|
|
@ -288,12 +287,12 @@ export class WKPage implements PageDelegate {
|
||||||
await session.send('Network.setExtraHTTPHeaders', { headers });
|
await session.send('Network.setExtraHTTPHeaders', { headers });
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _setEmulateMedia(session: WKTargetSession, mediaType: input.MediaType | null, mediaColorScheme: input.ColorScheme | null): Promise<void> {
|
private async _setEmulateMedia(session: WKTargetSession, mediaType: types.MediaType | null, colorScheme: types.ColorScheme | null): Promise<void> {
|
||||||
const promises = [];
|
const promises = [];
|
||||||
promises.push(session.send('Page.setEmulatedMedia', { media: mediaType || '' }));
|
promises.push(session.send('Page.setEmulatedMedia', { media: mediaType || '' }));
|
||||||
if (mediaColorScheme !== null) {
|
if (colorScheme !== null) {
|
||||||
let appearance: any = '';
|
let appearance: any = '';
|
||||||
switch (mediaColorScheme) {
|
switch (colorScheme) {
|
||||||
case 'light': appearance = 'Light'; break;
|
case 'light': appearance = 'Light'; break;
|
||||||
case 'dark': appearance = 'Dark'; break;
|
case 'dark': appearance = 'Dark'; break;
|
||||||
}
|
}
|
||||||
|
|
@ -306,8 +305,8 @@ export class WKPage implements PageDelegate {
|
||||||
await this._setExtraHTTPHeaders(this._session, headers);
|
await this._setExtraHTTPHeaders(this._session, headers);
|
||||||
}
|
}
|
||||||
|
|
||||||
async setEmulateMedia(mediaType: input.MediaType | null, mediaColorScheme: input.ColorScheme | null): Promise<void> {
|
async setEmulateMedia(mediaType: types.MediaType | null, colorScheme: types.ColorScheme | null): Promise<void> {
|
||||||
await this._setEmulateMedia(this._session, mediaType, mediaColorScheme);
|
await this._setEmulateMedia(this._session, mediaType, colorScheme);
|
||||||
}
|
}
|
||||||
|
|
||||||
async setViewport(viewport: types.Viewport): Promise<void> {
|
async setViewport(viewport: types.Viewport): Promise<void> {
|
||||||
|
|
@ -468,7 +467,7 @@ export class WKPage implements PageDelegate {
|
||||||
return this._page.evaluate(() => ({ width: innerWidth, height: innerHeight }));
|
return this._page.evaluate(() => ({ width: innerWidth, height: innerHeight }));
|
||||||
}
|
}
|
||||||
|
|
||||||
async setInputFiles(handle: dom.ElementHandle, files: input.FilePayload[]): Promise<void> {
|
async setInputFiles(handle: dom.ElementHandle, files: types.FilePayload[]): Promise<void> {
|
||||||
const objectId = toRemoteObject(handle).objectId;
|
const objectId = toRemoteObject(handle).objectId;
|
||||||
await this._session.send('DOM.setInputFiles', { objectId, files });
|
await this._session.send('DOM.setInputFiles', { objectId, files });
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue