chore: remove non-input related stuff from input (#369)

This commit is contained in:
Dmitry Gozman 2020-01-03 12:59:06 -08:00 committed by Pavel Feldman
parent 310d4b193b
commit f1d6fe6bd8
9 changed files with 153 additions and 164 deletions

View file

@ -16,7 +16,6 @@
*/
import { Page } from './page';
import * as input from './input';
import * as network from './network';
import * as types from './types';
@ -40,8 +39,8 @@ export type BrowserContextOptions = {
ignoreHTTPSErrors?: boolean,
javaScriptEnabled?: boolean,
bypassCSP?: boolean,
mediaType?: input.MediaType,
colorScheme?: input.ColorScheme,
mediaType?: types.MediaType,
colorScheme?: types.ColorScheme,
userAgent?: string,
timezoneId?: string,
geolocation?: types.Geolocation

View file

@ -36,7 +36,6 @@ import { CRWorkers, CRWorker } from './features/crWorkers';
import { CRBrowser } from './crBrowser';
import { BrowserContext } from '../browserContext';
import * as types from '../types';
import * as input from '../input';
import { ConsoleMessage } from '../console';
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> {
const features = mediaColorScheme ? [{ name: 'prefers-color-scheme', value: mediaColorScheme }] : [];
async setEmulateMedia(mediaType: types.MediaType | null, colorScheme: types.ColorScheme | null): Promise<void> {
const features = colorScheme ? [{ name: 'prefers-color-scheme', value: colorScheme }] : [];
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 };
}
async setInputFiles(handle: dom.ElementHandle, files: input.FilePayload[]): Promise<void> {
await handle.evaluate(input.setFileInputFunction, files);
async setInputFiles(handle: dom.ElementHandle, files: types.FilePayload[]): Promise<void> {
await handle.evaluate(dom.setFileInputFunction, files);
}
async adoptElementHandle<T extends Node>(handle: dom.ElementHandle<T>, to: dom.FrameExecutionContext): Promise<dom.ElementHandle<T>> {

View file

@ -10,6 +10,10 @@ import * as zsSelectorEngineSource from './generated/zsSelectorEngineSource';
import { assert, helper, debugError } from './helper';
import Injected from './injected/injected';
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 {
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);
}
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 });
for (const option of options) {
if (option instanceof ElementHandle)
@ -296,18 +300,92 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
if (option.index !== undefined)
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> {
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)
throw new Error(error);
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) => {
if (node.nodeType !== Node.ELEMENT_NODE || (node as Element).tagName !== 'INPUT')
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;
});
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() {
@ -454,3 +543,15 @@ export function waitForSelectorTask(selector: string, visibility: types.Visibili
}, 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 }));
};

View file

@ -25,7 +25,6 @@ import { FFNetworkManager } from './ffNetworkManager';
import { Events } from '../events';
import * as dialog from '../dialog';
import { Protocol } from './protocol';
import * as input from '../input';
import { RawMouseImpl, RawKeyboardImpl } from './ffInput';
import { BrowserContext } from '../browserContext';
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', {
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 }));
}
async setInputFiles(handle: dom.ElementHandle, files: input.FilePayload[]): Promise<void> {
await handle.evaluate(input.setFileInputFunction, files);
async setInputFiles(handle: dom.ElementHandle, files: types.FilePayload[]): Promise<void> {
await handle.evaluate(dom.setFileInputFunction, files);
}
async adoptElementHandle<T extends Node>(handle: dom.ElementHandle<T>, to: dom.FrameExecutionContext): Promise<dom.ElementHandle<T>> {

View file

@ -21,7 +21,7 @@ import * as js from './javascript';
import * as dom from './dom';
import * as network from './network';
import { helper, assert, RegisteredListener } from './helper';
import { ClickOptions, MultiClickOptions, PointerActionOptions, SelectOption } from './input';
import { ClickOptions, MultiClickOptions, PointerActionOptions } from './input';
import { TimeoutError } from './errors';
import { Events } from './events';
import { Page } from './page';
@ -646,7 +646,7 @@ export class Frame {
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 values = value === undefined ? [] : Array.isArray(value) ? value : [value];
const result = await handle.select(...values);

View file

@ -1,14 +1,10 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
import * as fs from 'fs';
import * as path from 'path';
import { assert, helper } from './helper';
import { assert } from './helper';
import * as types from './types';
import * as keyboardLayout from './usKeyboardLayout';
const readFileAsync = helper.promisify(fs.readFile);
export type Modifier = 'Alt' | 'Control' | 'Meta' | 'Shift';
export type Button = 'left' | 'right' | 'middle';
@ -28,12 +24,6 @@ export type MultiClickOptions = PointerActionOptions & {
button?: Button;
};
export type SelectOption = {
value?: string;
label?: string;
index?: number;
};
export const keypadLocation = keyboardLayout.keypadLocation;
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']);

View file

@ -47,7 +47,7 @@ export interface PageDelegate {
setExtraHTTPHeaders(extraHTTPHeaders: network.Headers): 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>;
setRequestInterception(enabled: boolean): Promise<void>;
setOfflineMode(enabled: boolean): Promise<void>;
@ -65,7 +65,7 @@ export interface PageDelegate {
getOwnerFrame(handle: dom.ElementHandle): Promise<frames.Frame | null>;
getContentQuads(handle: dom.ElementHandle): Promise<types.Quad[] | null>;
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>;
getAccessibilityTree(): Promise<accessibility.AXNode>;
@ -73,8 +73,8 @@ export interface PageDelegate {
type PageState = {
viewport: types.Viewport | null;
mediaType: input.MediaType | null;
colorScheme: input.ColorScheme | null;
mediaType: types.MediaType | null;
colorScheme: types.ColorScheme | null;
extraHTTPHeaders: network.Headers | null;
cacheEnabled: boolean | null;
interceptNetwork: boolean | null;
@ -370,9 +370,9 @@ export class Page extends EventEmitter {
return waitPromise;
}
async emulateMedia(options: { type?: input.MediaType, colorScheme?: input.ColorScheme }) {
assert(!options.type || input.mediaTypes.has(options.type), 'Unsupported media type: ' + options.type);
assert(!options.colorScheme || input.mediaColorSchemes.has(options.colorScheme), 'Unsupported color scheme: ' + options.colorScheme);
async emulateMedia(options: { type?: types.MediaType, colorScheme?: types.ColorScheme }) {
assert(!options.type || types.mediaTypes.has(options.type), 'Unsupported media type: ' + options.type);
assert(!options.colorScheme || types.colorSchemes.has(options.colorScheme), 'Unsupported color scheme: ' + options.colorScheme);
if (options.type !== undefined)
this._state.mediaType = options.type;
if (options.colorScheme !== undefined)
@ -476,7 +476,7 @@ export class Page extends EventEmitter {
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);
}

View file

@ -55,10 +55,28 @@ export type URLMatch = string | RegExp | ((url: kurl.URL) => boolean);
export type Credentials = {
username: string;
password: string;
}
};
export type Geolocation = {
longitude?: number;
latitude?: number;
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']);

View file

@ -29,7 +29,6 @@ import * as dialog from '../dialog';
import { WKBrowser } from './wkBrowser';
import { BrowserContext } from '../browserContext';
import { RawMouseImpl, RawKeyboardImpl } from './wkInput';
import * as input from '../input';
import * as types from '../types';
import * as jpeg from 'jpeg-js';
import { PNG } from 'pngjs';
@ -288,12 +287,12 @@ export class WKPage implements PageDelegate {
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 = [];
promises.push(session.send('Page.setEmulatedMedia', { media: mediaType || '' }));
if (mediaColorScheme !== null) {
if (colorScheme !== null) {
let appearance: any = '';
switch (mediaColorScheme) {
switch (colorScheme) {
case 'light': appearance = 'Light'; break;
case 'dark': appearance = 'Dark'; break;
}
@ -306,8 +305,8 @@ export class WKPage implements PageDelegate {
await this._setExtraHTTPHeaders(this._session, headers);
}
async setEmulateMedia(mediaType: input.MediaType | null, mediaColorScheme: input.ColorScheme | null): Promise<void> {
await this._setEmulateMedia(this._session, mediaType, mediaColorScheme);
async setEmulateMedia(mediaType: types.MediaType | null, colorScheme: types.ColorScheme | null): Promise<void> {
await this._setEmulateMedia(this._session, mediaType, colorScheme);
}
async setViewport(viewport: types.Viewport): Promise<void> {
@ -468,7 +467,7 @@ export class WKPage implements PageDelegate {
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;
await this._session.send('DOM.setInputFiles', { objectId, files });
}