playwright/src/webkit/JSHandle.ts
2019-11-27 16:21:29 -08:00

150 lines
5.5 KiB
TypeScript

/**
* 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 * as fs from 'fs';
import { debugError, helper } from '../helper';
import * as input from '../input';
import * as dom from '../dom';
import * as frames from '../frames';
import * as types from '../types';
import { TargetSession } from './Connection';
import { ExecutionContextDelegate, markJSHandle, toRemoteObject } from './ExecutionContext';
import { FrameManager } from './FrameManager';
import { Protocol } from './protocol';
import * as js from '../javascript';
const writeFileAsync = helper.promisify(fs.writeFile);
export function createJSHandle(context: js.ExecutionContext, remoteObject: Protocol.Runtime.RemoteObject) {
const frame = context.frame();
if (remoteObject.subtype === 'node' && frame) {
const frameManager = frame._delegate as FrameManager;
const delegate = new DOMWorldDelegate((context._delegate as ExecutionContextDelegate)._session, frameManager);
const handle = new dom.ElementHandle(context, frameManager.page().keyboard, frameManager.page().mouse, delegate);
markJSHandle(handle, remoteObject);
return handle;
}
const handle = new js.JSHandle(context);
markJSHandle(handle, remoteObject);
return handle;
}
class DOMWorldDelegate implements dom.DOMWorldDelegate {
private _client: TargetSession;
private _frameManager: FrameManager;
constructor(client: TargetSession, frameManager: FrameManager) {
this._client = client;
this._frameManager = frameManager;
}
async contentFrame(handle: dom.ElementHandle): Promise<frames.Frame | null> {
throw new Error('contentFrame() is not implemented');
}
isJavascriptEnabled(): boolean {
return this._frameManager.page()._javascriptEnabled;
}
async boundingBox(handle: dom.ElementHandle): Promise<types.Rect | null> {
throw new Error('boundingBox() is not implemented');
}
async screenshot(handle: dom.ElementHandle, options: any = {}): Promise<string | Buffer> {
const objectId = toRemoteObject(handle).objectId;
this._client.send('DOM.getDocument');
const {nodeId} = await this._client.send('DOM.requestNode', {objectId});
const result = await this._client.send('Page.snapshotNode', {nodeId});
const prefix = 'data:image/png;base64,';
const buffer = Buffer.from(result.dataURL.substr(prefix.length), 'base64');
if (options.path)
await writeFileAsync(options.path, buffer);
return buffer;
}
async ensurePointerActionPoint(handle: dom.ElementHandle, relativePoint?: types.Point): Promise<types.Point> {
await handle._scrollIntoViewIfNeeded();
if (!relativePoint)
return this._clickablePoint(handle);
const box = await this.boundingBox(handle);
return { x: box.x + relativePoint.x, y: box.y + relativePoint.y };
}
private async _clickablePoint(handle: dom.ElementHandle): Promise<types.Point> {
const fromProtocolQuad = (quad: number[]): types.Point[] => {
return [
{x: quad[0], y: quad[1]},
{x: quad[2], y: quad[3]},
{x: quad[4], y: quad[5]},
{x: quad[6], y: quad[7]}
];
};
const intersectQuadWithViewport = (quad: types.Point[], width: number, height: number): types.Point[] => {
return quad.map(point => ({
x: Math.min(Math.max(point.x, 0), width),
y: Math.min(Math.max(point.y, 0), height),
}));
};
const computeQuadArea = (quad: types.Point[]) => {
// Compute sum of all directed areas of adjacent triangles
// https://en.wikipedia.org/wiki/Polygon#Simple_polygons
let area = 0;
for (let i = 0; i < quad.length; ++i) {
const p1 = quad[i];
const p2 = quad[(i + 1) % quad.length];
area += (p1.x * p2.y - p2.x * p1.y) / 2;
}
return Math.abs(area);
};
const [result, viewport] = await Promise.all([
this._client.send('DOM.getContentQuads', {
objectId: toRemoteObject(handle).objectId
}).catch(debugError),
handle.evaluate(() => ({ clientWidth: innerWidth, clientHeight: innerHeight })),
]);
if (!result || !result.quads.length)
throw new Error('Node is either not visible or not an HTMLElement');
// Filter out quads that have too small area to click into.
const {clientWidth, clientHeight} = viewport;
const quads = result.quads.map(fromProtocolQuad)
.map(quad => intersectQuadWithViewport(quad, clientWidth, clientHeight))
.filter(quad => computeQuadArea(quad) > 1);
if (!quads.length)
throw new Error('Node is either not visible or not an HTMLElement');
// Return the middle point of the first quad.
const quad = quads[0];
let x = 0;
let y = 0;
for (const point of quad) {
x += point.x;
y += point.y;
}
return {
x: x / 4,
y: y / 4
};
}
async setInputFiles(handle: dom.ElementHandle, files: input.FilePayload[]): Promise<void> {
const objectId = toRemoteObject(handle).objectId;
await this._client.send('DOM.setInputFiles', { objectId, files });
}
}