/** * 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 * as channels from '../protocol/channels'; import { Frame } from './frame'; import { JSHandle, serializeArgument, parseResult } from './jsHandle'; import { ChannelOwner } from './channelOwner'; import { SelectOption, FilePayload, Rect, SelectOptionOptions } from './types'; import fs from 'fs'; import * as mime from 'mime'; import path from 'path'; import * as util from 'util'; import { assert, isString, mkdirIfNeeded } from '../utils/utils'; import * as api from '../../types/types'; import * as structs from '../../types/structs'; const fsWriteFileAsync = util.promisify(fs.writeFile.bind(fs)); export class ElementHandle extends JSHandle implements api.ElementHandle { readonly _elementChannel: channels.ElementHandleChannel; static from(handle: channels.ElementHandleChannel): ElementHandle { return (handle as any)._object; } static fromNullable(handle: channels.ElementHandleChannel | undefined): ElementHandle | null { return handle ? ElementHandle.from(handle) : null; } constructor(parent: ChannelOwner, type: string, guid: string, initializer: channels.JSHandleInitializer) { super(parent, type, guid, initializer); this._elementChannel = this._channel as channels.ElementHandleChannel; } asElement(): T extends Node ? ElementHandle : null { return this as any; } async ownerFrame(): Promise { return this._wrapApiCall('elementHandle.ownerFrame', async (channel: channels.ElementHandleChannel) => { return Frame.fromNullable((await channel.ownerFrame()).frame); }); } async contentFrame(): Promise { return this._wrapApiCall('elementHandle.contentFrame', async (channel: channels.ElementHandleChannel) => { return Frame.fromNullable((await channel.contentFrame()).frame); }); } async getAttribute(name: string): Promise { return this._wrapApiCall('elementHandle.getAttribute', async (channel: channels.ElementHandleChannel) => { const value = (await channel.getAttribute({ name })).value; return value === undefined ? null : value; }); } async textContent(): Promise { return this._wrapApiCall('elementHandle.textContent', async (channel: channels.ElementHandleChannel) => { const value = (await channel.textContent()).value; return value === undefined ? null : value; }); } async innerText(): Promise { return this._wrapApiCall('elementHandle.innerText', async (channel: channels.ElementHandleChannel) => { return (await channel.innerText()).value; }); } async innerHTML(): Promise { return this._wrapApiCall('elementHandle.innerHTML', async (channel: channels.ElementHandleChannel) => { return (await channel.innerHTML()).value; }); } async isChecked(): Promise { return this._wrapApiCall('elementHandle.isChecked', async (channel: channels.ElementHandleChannel) => { return (await channel.isChecked()).value; }); } async isDisabled(): Promise { return this._wrapApiCall('elementHandle.isDisabled', async (channel: channels.ElementHandleChannel) => { return (await channel.isDisabled()).value; }); } async isEditable(): Promise { return this._wrapApiCall('elementHandle.isEditable', async (channel: channels.ElementHandleChannel) => { return (await channel.isEditable()).value; }); } async isEnabled(): Promise { return this._wrapApiCall('elementHandle.isEnabled', async (channel: channels.ElementHandleChannel) => { return (await channel.isEnabled()).value; }); } async isHidden(): Promise { return this._wrapApiCall('elementHandle.isHidden', async (channel: channels.ElementHandleChannel) => { return (await channel.isHidden()).value; }); } async isVisible(): Promise { return this._wrapApiCall('elementHandle.isVisible', async (channel: channels.ElementHandleChannel) => { return (await channel.isVisible()).value; }); } async dispatchEvent(type: string, eventInit: Object = {}) { return this._wrapApiCall('elementHandle.dispatchEvent', async (channel: channels.ElementHandleChannel) => { await channel.dispatchEvent({ type, eventInit: serializeArgument(eventInit) }); }); } async scrollIntoViewIfNeeded(options: channels.ElementHandleScrollIntoViewIfNeededOptions = {}) { return this._wrapApiCall('elementHandle.scrollIntoViewIfNeeded', async (channel: channels.ElementHandleChannel) => { await channel.scrollIntoViewIfNeeded(options); }); } async hover(options: channels.ElementHandleHoverOptions = {}): Promise { return this._wrapApiCall('elementHandle.hover', async (channel: channels.ElementHandleChannel) => { await channel.hover(options); }); } async click(options: channels.ElementHandleClickOptions = {}): Promise { return this._wrapApiCall('elementHandle.click', async (channel: channels.ElementHandleChannel) => { return await channel.click(options); }); } async dblclick(options: channels.ElementHandleDblclickOptions = {}): Promise { return this._wrapApiCall('elementHandle.dblclick', async (channel: channels.ElementHandleChannel) => { return await channel.dblclick(options); }); } async tap(options: channels.ElementHandleTapOptions = {}): Promise { return this._wrapApiCall('elementHandle.tap', async (channel: channels.ElementHandleChannel) => { return await channel.tap(options); }); } async selectOption(values: string | api.ElementHandle | SelectOption | string[] | api.ElementHandle[] | SelectOption[] | null, options: SelectOptionOptions = {}): Promise { return this._wrapApiCall('elementHandle.selectOption', async (channel: channels.ElementHandleChannel) => { const result = await channel.selectOption({ ...convertSelectOptionValues(values), ...options }); return result.values; }); } async fill(value: string, options: channels.ElementHandleFillOptions = {}): Promise { return this._wrapApiCall('elementHandle.fill', async (channel: channels.ElementHandleChannel) => { return await channel.fill({ value, ...options }); }); } async selectText(options: channels.ElementHandleSelectTextOptions = {}): Promise { return this._wrapApiCall('elementHandle.selectText', async (channel: channels.ElementHandleChannel) => { await channel.selectText(options); }); } async setInputFiles(files: string | FilePayload | string[] | FilePayload[], options: channels.ElementHandleSetInputFilesOptions = {}) { return this._wrapApiCall('elementHandle.setInputFiles', async (channel: channels.ElementHandleChannel) => { await channel.setInputFiles({ files: await convertInputFiles(files), ...options }); }); } async focus(): Promise { return this._wrapApiCall('elementHandle.focus', async (channel: channels.ElementHandleChannel) => { await channel.focus(); }); } async type(text: string, options: channels.ElementHandleTypeOptions = {}): Promise { return this._wrapApiCall('elementHandle.type', async (channel: channels.ElementHandleChannel) => { await channel.type({ text, ...options }); }); } async press(key: string, options: channels.ElementHandlePressOptions = {}): Promise { return this._wrapApiCall('elementHandle.press', async (channel: channels.ElementHandleChannel) => { await channel.press({ key, ...options }); }); } async check(options: channels.ElementHandleCheckOptions = {}) { return this._wrapApiCall('elementHandle.check', async (channel: channels.ElementHandleChannel) => { return await channel.check(options); }); } async uncheck(options: channels.ElementHandleUncheckOptions = {}) { return this._wrapApiCall('elementHandle.uncheck', async (channel: channels.ElementHandleChannel) => { return await channel.uncheck(options); }); } async boundingBox(): Promise { return this._wrapApiCall('elementHandle.boundingBox', async (channel: channels.ElementHandleChannel) => { const value = (await channel.boundingBox()).value; return value === undefined ? null : value; }); } async screenshot(options: channels.ElementHandleScreenshotOptions & { path?: string } = {}): Promise { return this._wrapApiCall('elementHandle.screenshot', async (channel: channels.ElementHandleChannel) => { const copy = { ...options }; if (!copy.type) copy.type = determineScreenshotType(options); const result = await channel.screenshot(copy); const buffer = Buffer.from(result.binary, 'base64'); if (options.path) { await mkdirIfNeeded(options.path); await fsWriteFileAsync(options.path, buffer); } return buffer; }); } async $(selector: string): Promise | null> { return this._wrapApiCall('elementHandle.$', async (channel: channels.ElementHandleChannel) => { return ElementHandle.fromNullable((await channel.querySelector({ selector })).element) as ElementHandle | null; }); } async $$(selector: string): Promise[]> { return this._wrapApiCall('elementHandle.$$', async (channel: channels.ElementHandleChannel) => { const result = await channel.querySelectorAll({ selector }); return result.elements.map(h => ElementHandle.from(h) as ElementHandle); }); } async $eval(selector: string, pageFunction: structs.PageFunctionOn, arg?: Arg): Promise { return this._wrapApiCall('elementHandle.$eval', async (channel: channels.ElementHandleChannel) => { const result = await channel.evalOnSelector({ selector, expression: String(pageFunction), isFunction: typeof pageFunction === 'function', arg: serializeArgument(arg) }); return parseResult(result.value); }); } async $$eval(selector: string, pageFunction: structs.PageFunctionOn, arg?: Arg): Promise { return this._wrapApiCall('elementHandle.$$eval', async (channel: channels.ElementHandleChannel) => { const result = await channel.evalOnSelectorAll({ selector, expression: String(pageFunction), isFunction: typeof pageFunction === 'function', arg: serializeArgument(arg) }); return parseResult(result.value); }); } async waitForElementState(state: 'visible' | 'hidden' | 'stable' | 'enabled' | 'disabled', options: channels.ElementHandleWaitForElementStateOptions = {}): Promise { return this._wrapApiCall('elementHandle.waitForElementState', async (channel: channels.ElementHandleChannel) => { return await channel.waitForElementState({ state, ...options }); }); } waitForSelector(selector: string, options: channels.ElementHandleWaitForSelectorOptions & { state: 'attached' | 'visible' }): Promise>; waitForSelector(selector: string, options?: channels.ElementHandleWaitForSelectorOptions): Promise | null>; async waitForSelector(selector: string, options: channels.ElementHandleWaitForSelectorOptions = {}): Promise | null> { return this._wrapApiCall('elementHandle.waitForSelector', async (channel: channels.ElementHandleChannel) => { const result = await channel.waitForSelector({ selector, ...options }); return ElementHandle.fromNullable(result.element) as ElementHandle | null; }); } } export function convertSelectOptionValues(values: string | api.ElementHandle | SelectOption | string[] | api.ElementHandle[] | SelectOption[] | null): { elements?: channels.ElementHandleChannel[], options?: SelectOption[] } { if (values === null) return {}; if (!Array.isArray(values)) values = [ values as any ]; if (!values.length) return {}; for (let i = 0; i < values.length; i++) assert(values[i] !== null, `options[${i}]: expected object, got null`); if (values[0] instanceof ElementHandle) return { elements: (values as ElementHandle[]).map((v: ElementHandle) => v._elementChannel) }; if (isString(values[0])) return { options: (values as string[]).map(value => ({ value })) }; return { options: values as SelectOption[] }; } type SetInputFilesFiles = channels.ElementHandleSetInputFilesParams['files']; export async function convertInputFiles(files: string | FilePayload | string[] | FilePayload[]): Promise { const items: (string | FilePayload)[] = Array.isArray(files) ? files : [ files ]; const filePayloads: SetInputFilesFiles = await Promise.all(items.map(async item => { if (typeof item === 'string') { return { name: path.basename(item), buffer: (await util.promisify(fs.readFile)(item)).toString('base64') }; } else { return { name: item.name, mimeType: item.mimeType, buffer: item.buffer.toString('base64'), }; } })); return filePayloads; } export function determineScreenshotType(options: { path?: string, type?: 'png' | 'jpeg' }): 'png' | 'jpeg' | undefined { if (options.path) { const mimeType = mime.getType(options.path); if (mimeType === 'image/png') return 'png'; else if (mimeType === 'image/jpeg') return 'jpeg'; throw new Error(`path: unsupported mime type "${mimeType}"`); } return options.type; }