diff --git a/docs/api.md b/docs/api.md index c0349ae105..cd9ffa783b 100644 --- a/docs/api.md +++ b/docs/api.md @@ -587,7 +587,7 @@ const playwright = require('playwright'); (async () => { const browser = await playwright.launch(); // Store the endpoint to be able to reconnect to Chromium - const browserWSEndpoint = browser.wsEndpoint(); + const browserWSEndpoint = browser.chromium.wsEndpoint(); // Disconnect playwright from Chromium browser.disconnect(); diff --git a/src/chromium/DOMWorld.ts b/src/chromium/DOMWorld.ts index da5e038239..f065aa790c 100644 --- a/src/chromium/DOMWorld.ts +++ b/src/chromium/DOMWorld.ts @@ -21,9 +21,10 @@ import { ExecutionContext } from './ExecutionContext'; import { Frame } from './Frame'; import { FrameManager } from './FrameManager'; import { assert, helper } from '../helper'; -import { ElementHandle, JSHandle, ClickOptions, PointerActionOptions, MultiClickOptions, SelectOption } from './JSHandle'; +import { ElementHandle, JSHandle } from './JSHandle'; import { LifecycleWatcher } from './LifecycleWatcher'; import { TimeoutSettings } from '../TimeoutSettings'; +import { ClickOptions, MultiClickOptions, PointerActionOptions, SelectOption } from '../input'; const readFileAsync = helper.promisify(fs.readFile); export class DOMWorld { @@ -280,63 +281,6 @@ export class DOMWorld { } } - async click(selector: string, options?: ClickOptions) { - const handle = await this.$(selector); - assert(handle, 'No node found for selector: ' + selector); - await handle.click(options); - await handle.dispose(); - } - - async dblclick(selector: string, options?: MultiClickOptions) { - const handle = await this.$(selector); - assert(handle, 'No node found for selector: ' + selector); - await handle.dblclick(options); - await handle.dispose(); - } - - async tripleclick(selector: string, options?: MultiClickOptions) { - const handle = await this.$(selector); - assert(handle, 'No node found for selector: ' + selector); - await handle.tripleclick(options); - await handle.dispose(); - } - - async fill(selector: string, value: string) { - const handle = await this.$(selector); - assert(handle, 'No node found for selector: ' + selector); - await handle.fill(value); - await handle.dispose(); - } - - async focus(selector: string) { - const handle = await this.$(selector); - assert(handle, 'No node found for selector: ' + selector); - await handle.focus(); - await handle.dispose(); - } - - async hover(selector: string, options?: PointerActionOptions) { - const handle = await this.$(selector); - assert(handle, 'No node found for selector: ' + selector); - await handle.hover(options); - await handle.dispose(); - } - - async select(selector: string, ...values: (string | ElementHandle | SelectOption)[]): Promise { - const handle = await this.$(selector); - assert(handle, 'No node found for selector: ' + selector); - const result = await handle.select(...values); - await handle.dispose(); - return result; - } - - async type(selector: string, text: string, options: { delay: (number | undefined); } | undefined) { - const handle = await this.$(selector); - assert(handle, 'No node found for selector: ' + selector); - await handle.type(text, options); - await handle.dispose(); - } - waitForSelector(selector: string, options: { visible?: boolean; hidden?: boolean; timeout?: number; } | undefined): Promise { return this._waitForSelectorOrXPath(selector, false, options); } diff --git a/src/chromium/Frame.ts b/src/chromium/Frame.ts index a10639cc2a..719d0d7edd 100644 --- a/src/chromium/Frame.ts +++ b/src/chromium/Frame.ts @@ -15,12 +15,13 @@ * limitations under the License. */ -import { helper } from '../helper'; +import { helper, assert } from '../helper'; +import { ClickOptions, MultiClickOptions, PointerActionOptions, SelectOption } from '../input'; import { CDPSession } from './Connection'; import { DOMWorld } from './DOMWorld'; import { ExecutionContext } from './ExecutionContext'; import { FrameManager } from './FrameManager'; -import { ClickOptions, ElementHandle, JSHandle, MultiClickOptions, PointerActionOptions, SelectOption } from './JSHandle'; +import { ElementHandle, JSHandle } from './JSHandle'; import { Response } from './NetworkManager'; import { Protocol } from './protocol'; @@ -141,37 +142,62 @@ export class Frame { } async click(selector: string, options?: ClickOptions) { - return this._secondaryWorld.click(selector, options); + const handle = await this._secondaryWorld.$(selector); + assert(handle, 'No node found for selector: ' + selector); + await handle.click(options); + await handle.dispose(); } async dblclick(selector: string, options?: MultiClickOptions) { - return this._secondaryWorld.dblclick(selector, options); + const handle = await this._secondaryWorld.$(selector); + assert(handle, 'No node found for selector: ' + selector); + await handle.dblclick(options); + await handle.dispose(); } async tripleclick(selector: string, options?: MultiClickOptions) { - return this._secondaryWorld.tripleclick(selector, options); + const handle = await this._secondaryWorld.$(selector); + assert(handle, 'No node found for selector: ' + selector); + await handle.tripleclick(options); + await handle.dispose(); } async fill(selector: string, value: string) { - return this._secondaryWorld.fill(selector, value); + const handle = await this._secondaryWorld.$(selector); + assert(handle, 'No node found for selector: ' + selector); + await handle.fill(value); + await handle.dispose(); } async focus(selector: string) { - return this._secondaryWorld.focus(selector); + const handle = await this._secondaryWorld.$(selector); + assert(handle, 'No node found for selector: ' + selector); + await handle.focus(); + await handle.dispose(); } async hover(selector: string, options?: PointerActionOptions) { - return this._secondaryWorld.hover(selector, options); + const handle = await this._secondaryWorld.$(selector); + assert(handle, 'No node found for selector: ' + selector); + await handle.hover(options); + await handle.dispose(); } - async select(selector: string, ...values: (string | ElementHandle | SelectOption)[]): Promise{ + async select(selector: string, ...values: (string | ElementHandle | SelectOption)[]): Promise { + const handle = await this._secondaryWorld.$(selector); + assert(handle, 'No node found for selector: ' + selector); const secondaryExecutionContext = await this._secondaryWorld.executionContext(); - const adoptedValues = values.map(async value => value instanceof ElementHandle ? secondaryExecutionContext._adoptElementHandle(value) : value); - return this._secondaryWorld.select(selector, ...(await Promise.all(adoptedValues))); + const adoptedValues = await Promise.all(values.map(async value => value instanceof ElementHandle ? secondaryExecutionContext._adoptElementHandle(value) : value)); + const result = await handle.select(...adoptedValues); + await handle.dispose(); + return result; } async type(selector: string, text: string, options: { delay: (number | undefined); } | undefined) { - return this._mainWorld.type(selector, text, options); + const handle = await this._secondaryWorld.$(selector); + assert(handle, 'No node found for selector: ' + selector); + await handle.type(text, options); + await handle.dispose(); } waitFor(selectorOrFunctionOrTimeout: (string | number | Function), options: any = {}, ...args: any[]): Promise { diff --git a/src/chromium/Input.ts b/src/chromium/Input.ts index 393bb90575..eb471725d5 100644 --- a/src/chromium/Input.ts +++ b/src/chromium/Input.ts @@ -15,9 +15,10 @@ * limitations under the License. */ -import { CDPSession } from './Connection'; import { assert } from '../helper'; +import { Modifier, Button } from '../input'; import { keyDefinitions } from '../USKeyboardLayout'; +import { CDPSession } from './Connection'; type KeyDescription = { keyCode: number, @@ -27,11 +28,8 @@ type KeyDescription = { location: number, }; -export type Modifier = 'Alt' | 'Control' | 'Meta' | 'Shift'; const kModifiers: Modifier[] = ['Alt', 'Control', 'Meta', 'Shift']; -export type Button = 'left' | 'right' | 'middle'; - export class Keyboard { private _client: CDPSession; _modifiers = 0; diff --git a/src/chromium/JSHandle.ts b/src/chromium/JSHandle.ts index bd2045d3b8..7f44ce0ab8 100644 --- a/src/chromium/JSHandle.ts +++ b/src/chromium/JSHandle.ts @@ -16,43 +16,21 @@ */ import * as path from 'path'; +import { assert, debugError, helper } from '../helper'; +import { ClickOptions, Modifier, MultiClickOptions, PointerActionOptions, SelectOption, selectFunction } from '../input'; import { CDPSession } from './Connection'; import { ExecutionContext } from './ExecutionContext'; import { Frame } from './Frame'; import { FrameManager } from './FrameManager'; -import { assert, debugError, helper } from '../helper'; -import { valueFromRemoteObject, releaseObject } from './protocolHelper'; import { Page } from './Page'; -import { Modifier, Button } from './Input'; import { Protocol } from './protocol'; +import { releaseObject, valueFromRemoteObject } from './protocolHelper'; type Point = { x: number; y: number; }; -export type PointerActionOptions = { - modifiers?: Modifier[]; - relativePoint?: Point; -}; - -export type ClickOptions = PointerActionOptions & { - delay?: number; - button?: Button; - clickCount?: number; -}; - -export type MultiClickOptions = PointerActionOptions & { - delay?: number; - button?: Button; -}; - -export type SelectOption = { - value?: string; - label?: string; - index?: number; -}; - export function createJSHandle(context: ExecutionContext, remoteObject: Protocol.Runtime.RemoteObject) { const frame = context.frame(); if (remoteObject.subtype === 'node' && frame) { @@ -335,33 +313,7 @@ export class ElementHandle extends JSHandle { if (option.index !== undefined) assert(helper.isNumber(option.index), 'Indices must be numbers. Found index "' + option.index + '" of type "' + (typeof option.index) + '"'); } - return this.evaluate((element: HTMLSelectElement, ...optionsToSelect: (Node | SelectOption)[]) => { - if (element.nodeName.toLowerCase() !== 'select') - throw new Error('Element is not a element.'); - - const options = Array.from(element.options); - element.value = undefined; - for (const option of options) { - option.selected = values.includes(option.value); - 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); - }, values) as Promise; - } - - async type(selector: string, text: string, options: { delay: (number | undefined); } | undefined) { - const handle = await this.$(selector); - assert(handle, 'No node found for selector: ' + selector); - await handle.type(text, options); - await handle.dispose(); - } - waitForSelector(selector: string, options: { timeout?: number; visible?: boolean; hidden?: boolean; } | undefined): Promise { return this._waitForSelectorOrXPath(selector, false, options); } diff --git a/src/firefox/FrameManager.ts b/src/firefox/FrameManager.ts index ef00787123..4a8dd79027 100644 --- a/src/firefox/FrameManager.ts +++ b/src/firefox/FrameManager.ts @@ -8,6 +8,9 @@ import {ExecutionContext} from './ExecutionContext'; import {NavigationWatchdog, NextNavigationWatchdog} from './NavigationWatchdog'; import {DOMWorld} from './DOMWorld'; import { JSHandle, ElementHandle } from './JSHandle'; +import { TimeoutSettings } from '../TimeoutSettings'; +import { NetworkManager } from './NetworkManager'; +import { MultiClickOptions, ClickOptions, SelectOption } from '../input'; export const FrameManagerEvents = { FrameNavigated: Symbol('FrameManagerEvents.FrameNavigated'), @@ -21,7 +24,7 @@ export class FrameManager extends EventEmitter { _page: Page; _networkManager: any; _timeoutSettings: any; - _mainFrame: any; + _mainFrame: Frame; _frames: Map; _contextIdToContext: Map; _eventListeners: RegisteredListener[]; @@ -71,7 +74,7 @@ export class FrameManager extends EventEmitter { return this._frames.get(frameId); } - mainFrame() { + mainFrame(): Frame { return this._mainFrame; } @@ -139,18 +142,19 @@ export class FrameManager extends EventEmitter { export class Frame { _parentFrame: Frame|null = null; private _session: JugglerSession; - _page: any; + _page: Page; _frameManager: FrameManager; - private _networkManager: any; - private _timeoutSettings: any; + private _networkManager: NetworkManager; + private _timeoutSettings: TimeoutSettings; _frameId: string; _url: string = ''; private _name: string = ''; _children: Set; private _detached: boolean; _firedEvents: Set; - _mainWorld: any; - _lastCommittedNavigationId: any; + _mainWorld: DOMWorld; + _lastCommittedNavigationId: string; + constructor(session: JugglerSession, frameManager : FrameManager, networkManager, page: Page, frameId: string, timeoutSettings) { this._session = session; this._page = page; @@ -248,20 +252,54 @@ export class Frame { return watchDog.navigationResponse(); } - async click(selector: string, options: { delay?: number; button?: string; clickCount?: number; } | undefined = {}) { - return this._mainWorld.click(selector, options); + async click(selector: string, options?: ClickOptions) { + const handle = await this.$(selector); + assert(handle, 'No node found for selector: ' + selector); + await handle.click(options); + await handle.dispose(); + } + + async dblclick(selector: string, options?: MultiClickOptions) { + const handle = await this.$(selector); + assert(handle, 'No node found for selector: ' + selector); + await handle.dblclick(options); + await handle.dispose(); + } + + async tripleclick(selector: string, options?: MultiClickOptions) { + const handle = await this.$(selector); + assert(handle, 'No node found for selector: ' + selector); + await handle.tripleclick(options); + await handle.dispose(); + } + + async select(selector: string, ...values: (string | ElementHandle | SelectOption)[]): Promise { + const handle = await this.$(selector); + assert(handle, 'No node found for selector: ' + selector); + const result = await handle.select(...values); + await handle.dispose(); + return result; } async type(selector: string, text: string, options: { delay: (number | undefined); } | undefined) { - return this._mainWorld.type(selector, text, options); + const handle = await this.$(selector); + assert(handle, 'No node found for selector: ' + selector); + await handle.type(text, options); + await handle.dispose(); } async focus(selector: string) { - return this._mainWorld.focus(selector); + const handle = await this.$(selector); + assert(handle, 'No node found for selector: ' + selector); + await handle.focus(); + await handle.dispose(); } async hover(selector: string) { - return this._mainWorld.hover(selector); + const handle = await this.$(selector); + assert(handle, 'No node found for selector: ' + selector); + await handle.hover(); + await handle.dispose(); } _detach() { @@ -278,10 +316,6 @@ export class Frame { this._firedEvents.clear(); } - select(selector: string, ...values: Array): Promise> { - return this._mainWorld.select(selector, ...values); - } - waitFor(selectorOrFunctionOrTimeout: (string | number | Function), options: { polling?: string | number; timeout?: number; visible?: boolean; hidden?: boolean; } | undefined, ...args: Array): Promise { const xPathPattern = '//'; diff --git a/src/firefox/Input.ts b/src/firefox/Input.ts index 29571b0c18..238a710624 100644 --- a/src/firefox/Input.ts +++ b/src/firefox/Input.ts @@ -17,6 +17,7 @@ import { keyDefinitions } from '../USKeyboardLayout'; import { JugglerSession } from './Connection'; +import { Button, ClickOptions, MultiClickOptions } from '../input'; interface KeyDescription { keyCode: number; @@ -186,7 +187,7 @@ export class Mouse { } } - async click(x: number, y: number, options: { delay?: number; button?: string; clickCount?: number; } | undefined = {}) { + async click(x: number, y: number, options: ClickOptions = {}) { const {delay = null} = options; if (delay !== null) { await Promise.all([ @@ -204,6 +205,55 @@ export class Mouse { } } + async dblclick(x: number, y: number, options: MultiClickOptions = {}) { + const { delay = null } = options; + if (delay !== null) { + await this.move(x, y); + await this.down({ ...options, clickCount: 1 }); + await new Promise(f => setTimeout(f, delay)); + await this.up({ ...options, clickCount: 1 }); + await new Promise(f => setTimeout(f, delay)); + await this.down({ ...options, clickCount: 2 }); + await new Promise(f => setTimeout(f, delay)); + await this.up({ ...options, clickCount: 2 }); + } else { + await Promise.all([ + this.move(x, y), + this.down({ ...options, clickCount: 1 }), + this.up({ ...options, clickCount: 1 }), + this.down({ ...options, clickCount: 2 }), + this.up({ ...options, clickCount: 2 }), + ]); + } + } + + async tripleclick(x: number, y: number, options: MultiClickOptions = {}) { + const { delay = null } = options; + if (delay !== null) { + await this.move(x, y); + await this.down({ ...options, clickCount: 1 }); + await new Promise(f => setTimeout(f, delay)); + await this.up({ ...options, clickCount: 1 }); + await new Promise(f => setTimeout(f, delay)); + await this.down({ ...options, clickCount: 2 }); + await new Promise(f => setTimeout(f, delay)); + await this.up({ ...options, clickCount: 2 }); + await new Promise(f => setTimeout(f, delay)); + await this.down({ ...options, clickCount: 3 }); + await new Promise(f => setTimeout(f, delay)); + await this.up({ ...options, clickCount: 3 }); + } else { + await Promise.all([ + this.move(x, y), + this.down({ ...options, clickCount: 1 }), + this.up({ ...options, clickCount: 1 }), + this.down({ ...options, clickCount: 2 }), + this.up({ ...options, clickCount: 2 }), + this.down({ ...options, clickCount: 3 }), + this.up({ ...options, clickCount: 3 }), + ]); + } + } async down(options: { button?: string; clickCount?: number; } | undefined = {}) { const { button = 'left', diff --git a/src/firefox/JSHandle.ts b/src/firefox/JSHandle.ts index 63cfa1ca03..931cfa6978 100644 --- a/src/firefox/JSHandle.ts +++ b/src/firefox/JSHandle.ts @@ -1,17 +1,37 @@ -import {assert, debugError} from '../helper'; +/** + * Copyright 2019 Google Inc. All rights reserved. + * Modifications 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. + */ + +import {assert, debugError, helper} from '../helper'; import * as path from 'path'; import {ExecutionContext} from './ExecutionContext'; import {Frame} from './FrameManager'; +import { JugglerSession } from './Connection'; +import { MultiClickOptions, ClickOptions, selectFunction, SelectOption } from '../input'; export class JSHandle { _context: ExecutionContext; - _session: any; - _executionContextId: any; - _objectId: any; - _type: any; - _subtype: any; + protected _session: JugglerSession; + private _executionContextId: string; + protected _objectId: string; + private _type: string; + private _subtype: string; _disposed: boolean; _protocolValue: { unserializableValue: any; value: any; objectId: any; }; + constructor(context: ExecutionContext, payload: any) { this._context = context; this._session = this._context._session; @@ -31,6 +51,14 @@ export class JSHandle { return this._context; } + async evaluate(pageFunction: Function | string, ...args: any[]): Promise<(any)> { + return await this.executionContext().evaluate(pageFunction, this, ...args); + } + + async evaluateHandle(pageFunction: Function | string, ...args: any[]): Promise { + return await this.executionContext().evaluateHandle(pageFunction, this, ...args); + } + toString(): string { if (this._objectId) return 'JSHandle@' + (this._subtype || this._type); @@ -268,11 +296,23 @@ export class ElementHandle extends JSHandle { throw new Error(error); } - async click(options: { delay?: number; button?: string; clickCount?: number; } | undefined) { + async click(options?: ClickOptions) { await this._scrollIntoViewIfNeeded(); const {x, y} = await this._clickablePoint(); await this._frame._page.mouse.click(x, y, options); } + + async dblclick(options?: MultiClickOptions): Promise { + await this._scrollIntoViewIfNeeded(); + const {x, y} = await this._clickablePoint(); + await this._frame._page.mouse.dblclick(x, y, options); + } + + async tripleclick(options?: MultiClickOptions): Promise { + await this._scrollIntoViewIfNeeded(); + const {x, y} = await this._clickablePoint(); + await this._frame._page.mouse.tripleclick(x, y, options); + } async uploadFile(...filePaths: Array) { const files = filePaths.map(filePath => path.resolve(filePath)); @@ -303,6 +343,20 @@ export class ElementHandle extends JSHandle { await this._frame._page.keyboard.press(key, options); } + async select(...values: (string | ElementHandle | SelectOption)[]): Promise { + const options = values.map(value => typeof value === 'object' ? value : { value }); + for (const option of options) { + if (option instanceof ElementHandle) + continue; + if (option.value !== undefined) + assert(helper.isString(option.value), 'Values must be strings. Found value "' + option.value + '" of type "' + (typeof option.value) + '"'); + if (option.label !== undefined) + assert(helper.isString(option.label), 'Labels must be strings. Found label "' + option.label + '" of type "' + (typeof option.label) + '"'); + if (option.index !== undefined) + assert(helper.isNumber(option.index), 'Indices must be numbers. Found index "' + option.index + '" of type "' + (typeof option.index) + '"'); + } + return this.evaluate(selectFunction, ...options); + } async _clickablePoint(): Promise<{ x: number; y: number; }> { const result = await this._session.send('Page.getContentQuads', { diff --git a/src/firefox/Page.ts b/src/firefox/Page.ts index 0d67c6b4a4..1106d62818 100644 --- a/src/firefox/Page.ts +++ b/src/firefox/Page.ts @@ -10,11 +10,12 @@ import { Dialog } from './Dialog'; import { Events } from './events'; import { Accessibility } from './features/accessibility'; import { Interception } from './features/interception'; -import { FrameManager, FrameManagerEvents, normalizeWaitUntil } from './FrameManager'; +import { FrameManager, FrameManagerEvents, normalizeWaitUntil, Frame } from './FrameManager'; import { Keyboard, Mouse } from './Input'; import { createHandle, ElementHandle, JSHandle } from './JSHandle'; import { NavigationWatchdog } from './NavigationWatchdog'; import { NetworkManager, NetworkManagerEvents, Request, Response } from './NetworkManager'; +import { ClickOptions, MultiClickOptions } from '../input'; const writeFileAsync = helper.promisify(fs.writeFile); @@ -327,7 +328,7 @@ export class Page extends EventEmitter { this.emit(Events.Page.Dialog, new Dialog(this._session, params)); } - mainFrame() { + mainFrame(): Frame { return this._frameManager.mainFrame(); } @@ -460,19 +461,27 @@ export class Page extends EventEmitter { } async evaluate(pageFunction, ...args) { - return await this._frameManager.mainFrame().evaluate(pageFunction, ...args); + return await this.mainFrame().evaluate(pageFunction, ...args); } async addScriptTag(options: { content?: string; path?: string; type?: string; url?: string; }): Promise { - return await this._frameManager.mainFrame().addScriptTag(options); + return await this.mainFrame().addScriptTag(options); } async addStyleTag(options: { content?: string; path?: string; url?: string; }): Promise { - return await this._frameManager.mainFrame().addStyleTag(options); + return await this.mainFrame().addStyleTag(options); } - async click(selector: string, options: { delay?: number; button?: string; clickCount?: number; } | undefined = {}) { - return await this._frameManager.mainFrame().click(selector, options); + async click(selector: string, options?: ClickOptions) { + return await this.mainFrame().click(selector, options); + } + + async dblclick(selector: string, options?: MultiClickOptions) { + return this.mainFrame().dblclick(selector, options); + } + + async tripleclick(selector: string, options?: MultiClickOptions) { + return this.mainFrame().tripleclick(selector, options); } async type(selector: string, text: string, options: { delay: (number | undefined); } | undefined) { diff --git a/src/firefox/features/interception.ts b/src/firefox/features/interception.ts index ffcdab1277..70b177bf68 100644 --- a/src/firefox/features/interception.ts +++ b/src/firefox/features/interception.ts @@ -10,12 +10,12 @@ export class Interception { this._networkManager = networkManager; } - enable() { - this._networkManager.setRequestInterception(true); + async enable() { + await this._networkManager.setRequestInterception(true); } - disable() { - this._networkManager.setRequestInterception(false); + async disable() { + await this._networkManager.setRequestInterception(false); } async continue(request: Request, overrides: { url?: string; method?: string; postData?: string; headers?: {[key: string]: string}; } = {}) { diff --git a/src/input.ts b/src/input.ts new file mode 100644 index 0000000000..55689a78cc --- /dev/null +++ b/src/input.ts @@ -0,0 +1,63 @@ +import { assert } from "console"; +import { helper } from "./helper"; + +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +export type Modifier = 'Alt' | 'Control' | 'Meta' | 'Shift'; +export type Button = 'left' | 'right' | 'middle'; + +type Point = { + x: number; + y: number; +}; + +export type PointerActionOptions = { + modifiers?: Modifier[]; + relativePoint?: Point; +}; + +export type ClickOptions = PointerActionOptions & { + delay?: number; + button?: Button; + clickCount?: number; +}; + +export type MultiClickOptions = PointerActionOptions & { + delay?: number; + button?: Button; +}; + +export type SelectOption = { + value?: string; + label?: string; + index?: number; +}; + +export const selectFunction = (element: HTMLSelectElement, ...optionsToSelect: (Node | SelectOption)[]) => { + if (element.nodeName.toLowerCase() !== 'select') + throw new Error('Element is not a element.'); - - const options = Array.from(element.options); - element.value = undefined; - for (const option of options) { - option.selected = values.includes(option.value); - 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); - }, values); + async select(selector: string, ...values: (string | ElementHandle | SelectOption)[]): Promise { + const handle = await this.$(selector); + assert(handle, 'No node found for selector: ' + selector); + const result = await handle.select(...values); + await handle.dispose(); + return result; } async type(selector: string, text: string, options: { delay: (number | undefined); } | undefined) { diff --git a/src/webkit/Input.ts b/src/webkit/Input.ts index bac8907427..ff437a492e 100644 --- a/src/webkit/Input.ts +++ b/src/webkit/Input.ts @@ -18,6 +18,7 @@ import { TargetSession } from './Connection'; import { assert } from '../helper'; import { keyDefinitions } from '../USKeyboardLayout'; +import { MultiClickOptions, ClickOptions } from '../input'; type KeyDescription = { keyCode: number, @@ -215,7 +216,7 @@ export class Mouse { } } - async click(x: number, y: number, options: { delay?: number; button?: Button; clickCount?: number; } = {}) { + async click(x: number, y: number, options: ClickOptions = {}) { const {delay = null} = options; if (delay !== null) { await Promise.all([ @@ -233,7 +234,7 @@ export class Mouse { } } - async dblclick(x: number, y: number, options: { delay?: number; button?: Button; } = {}) { + async dblclick(x: number, y: number, options: MultiClickOptions = {}) { const { delay = null } = options; if (delay !== null) { await this.move(x, y); @@ -255,7 +256,7 @@ export class Mouse { } } - async tripleclick(x: number, y: number, options: { delay?: number; button?: Button; } = {}) { + async tripleclick(x: number, y: number, options: MultiClickOptions = {}) { const { delay = null } = options; if (delay !== null) { await this.move(x, y); diff --git a/src/webkit/JSHandle.ts b/src/webkit/JSHandle.ts index 6dc15e4e0a..2ec3ef24b5 100644 --- a/src/webkit/JSHandle.ts +++ b/src/webkit/JSHandle.ts @@ -16,21 +16,15 @@ */ import * as fs from 'fs'; import { assert, debugError, helper } from '../helper'; +import { ClickOptions, MultiClickOptions, selectFunction, SelectOption } from '../input'; import { TargetSession } from './Connection'; import { ExecutionContext } from './ExecutionContext'; import { FrameManager } from './FrameManager'; -import { Button } from './Input'; import { Page } from './Page'; import { Protocol } from './protocol'; import { releaseObject, valueFromRemoteObject } from './protocolHelper'; const writeFileAsync = helper.promisify(fs.writeFile); -export type ClickOptions = { - delay?: number; - button?: Button; - clickCount?: number; -}; - export function createJSHandle(context: ExecutionContext, remoteObject: Protocol.Runtime.RemoteObject) { const frame = context.frame(); if (remoteObject.subtype === 'node' && frame) { @@ -223,24 +217,31 @@ export class ElementHandle extends JSHandle { await this._page.mouse.click(x, y, options); } - async select(...values: string[]): Promise { - for (const value of values) - assert(helper.isString(value), 'Values must be strings. Found value "' + value + '" of type "' + (typeof value) + '"'); - return this.evaluate((element: HTMLSelectElement, values: string[]) => { - if (element.nodeName.toLowerCase() !== 'select') - throw new Error('Element is not a