2019-01-16 01:21:23 +00:00
|
|
|
/**
|
|
|
|
* Copyright 2019 Google Inc. All rights reserved.
|
|
|
|
*
|
|
|
|
* 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.
|
|
|
|
*/
|
|
|
|
|
2020-04-28 14:35:43 +00:00
|
|
|
import * as fs from 'fs';
|
|
|
|
import {helper, assert} from './helper';
|
|
|
|
import {LifecycleWatcher, PuppeteerLifeCycleEvent} from './LifecycleWatcher';
|
|
|
|
import {TimeoutError} from './Errors';
|
|
|
|
import {JSHandle, ElementHandle} from './JSHandle';
|
|
|
|
import {ExecutionContext} from './ExecutionContext';
|
|
|
|
import {TimeoutSettings} from './TimeoutSettings';
|
|
|
|
import {MouseButtonInput} from './Input';
|
2020-04-29 11:28:16 +00:00
|
|
|
import {FrameManager, Frame} from './FrameManager';
|
2020-04-20 10:32:08 +00:00
|
|
|
|
2019-01-16 01:21:23 +00:00
|
|
|
const readFileAsync = helper.promisify(fs.readFile);
|
|
|
|
|
2020-04-29 11:28:16 +00:00
|
|
|
export interface WaitForSelectorOptions {
|
2020-04-28 14:35:43 +00:00
|
|
|
visible?: boolean;
|
|
|
|
hidden?: boolean;
|
|
|
|
timeout?: number;
|
|
|
|
}
|
|
|
|
|
|
|
|
export class DOMWorld {
|
2020-04-29 11:28:16 +00:00
|
|
|
_frameManager: FrameManager;
|
|
|
|
_frame: Frame;
|
2020-04-28 14:35:43 +00:00
|
|
|
_timeoutSettings: TimeoutSettings;
|
|
|
|
_documentPromise?: Promise<ElementHandle> = null;
|
|
|
|
_contextPromise?: Promise<ExecutionContext> = null;
|
|
|
|
|
|
|
|
_contextResolveCallback?: (x?: ExecutionContext) => void = null;
|
|
|
|
|
|
|
|
_detached = false;
|
|
|
|
_waitTasks = new Set<WaitTask>();
|
|
|
|
|
2020-04-29 11:28:16 +00:00
|
|
|
constructor(frameManager: FrameManager, frame: Frame, timeoutSettings: TimeoutSettings) {
|
2019-01-16 01:21:23 +00:00
|
|
|
this._frameManager = frameManager;
|
|
|
|
this._frame = frame;
|
2019-01-29 01:16:12 +00:00
|
|
|
this._timeoutSettings = timeoutSettings;
|
2019-01-16 01:21:23 +00:00
|
|
|
this._setContext(null);
|
|
|
|
}
|
|
|
|
|
2020-04-29 11:28:16 +00:00
|
|
|
frame(): Frame {
|
2019-01-22 22:55:33 +00:00
|
|
|
return this._frame;
|
|
|
|
}
|
|
|
|
|
2019-01-16 01:21:23 +00:00
|
|
|
/**
|
chore: migrate src/ExecutionContext (#5705)
* chore: migrate src/ExecutionContext to TypeScript
I spent a while trying to decide on the best course of action for
typing the `evaluate` function.
Ideally I wanted to use generics so that as a user you could type
something like:
```
handle.evaluate<HTMLElement, number, boolean>((node, x) => true, 5)
```
And have TypeScript know the arguments of `node` and `x` based on those
generics. But I hit two problems with that:
* you have to have n overloads of `evaluate` to cope for as many number
of arguments as you can be bothered too (e.g. we'd need an overload for
1 arg, 2 args, 3 args, etc)
* I decided it's actually confusing because you don't know as a user
what those generics actually map to.
So in the end I went with one generic which is the return type of the
function:
```
handle.evaluate<boolean>((node, x) => true, 5)
```
And `node` and `x` get typed as `any` which means you can tell TS
yourself:
```
handle.evaluate<boolean>((node: HTMLElement, x: number) => true, 5)
```
I'd like to find a way to force that the arguments after the function do
match the arguments you've given (in the above example, TS would moan if
I swapped that `5` for `"foo"`), but I tried a few things and to be
honest the complexity of the types wasn't worth it, I don't think.
I'm very open to tweaking these but I'd rather ship this and tweak going
forwards rather than spend hours now tweaking. Once we ship these
typedefs and get feedback from the community I'm sure we can improve
them.
2020-04-22 09:33:44 +00:00
|
|
|
* @param {?ExecutionContext} context
|
2019-01-16 01:21:23 +00:00
|
|
|
*/
|
2020-04-28 14:35:43 +00:00
|
|
|
_setContext(context?: ExecutionContext): void {
|
2019-01-16 01:21:23 +00:00
|
|
|
if (context) {
|
|
|
|
this._contextResolveCallback.call(null, context);
|
|
|
|
this._contextResolveCallback = null;
|
|
|
|
for (const waitTask of this._waitTasks)
|
|
|
|
waitTask.rerun();
|
|
|
|
} else {
|
|
|
|
this._documentPromise = null;
|
|
|
|
this._contextPromise = new Promise(fulfill => {
|
|
|
|
this._contextResolveCallback = fulfill;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-04-28 14:35:43 +00:00
|
|
|
_hasContext(): boolean {
|
2019-04-11 20:26:18 +00:00
|
|
|
return !this._contextResolveCallback;
|
|
|
|
}
|
|
|
|
|
2020-04-28 14:35:43 +00:00
|
|
|
_detach(): void {
|
2019-01-16 01:21:23 +00:00
|
|
|
this._detached = true;
|
|
|
|
for (const waitTask of this._waitTasks)
|
|
|
|
waitTask.terminate(new Error('waitForFunction failed: frame got detached.'));
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
chore: migrate src/ExecutionContext (#5705)
* chore: migrate src/ExecutionContext to TypeScript
I spent a while trying to decide on the best course of action for
typing the `evaluate` function.
Ideally I wanted to use generics so that as a user you could type
something like:
```
handle.evaluate<HTMLElement, number, boolean>((node, x) => true, 5)
```
And have TypeScript know the arguments of `node` and `x` based on those
generics. But I hit two problems with that:
* you have to have n overloads of `evaluate` to cope for as many number
of arguments as you can be bothered too (e.g. we'd need an overload for
1 arg, 2 args, 3 args, etc)
* I decided it's actually confusing because you don't know as a user
what those generics actually map to.
So in the end I went with one generic which is the return type of the
function:
```
handle.evaluate<boolean>((node, x) => true, 5)
```
And `node` and `x` get typed as `any` which means you can tell TS
yourself:
```
handle.evaluate<boolean>((node: HTMLElement, x: number) => true, 5)
```
I'd like to find a way to force that the arguments after the function do
match the arguments you've given (in the above example, TS would moan if
I swapped that `5` for `"foo"`), but I tried a few things and to be
honest the complexity of the types wasn't worth it, I don't think.
I'm very open to tweaking these but I'd rather ship this and tweak going
forwards rather than spend hours now tweaking. Once we ship these
typedefs and get feedback from the community I'm sure we can improve
them.
2020-04-22 09:33:44 +00:00
|
|
|
* @return {!Promise<!ExecutionContext>}
|
2019-01-16 01:21:23 +00:00
|
|
|
*/
|
2020-04-28 14:35:43 +00:00
|
|
|
executionContext(): Promise<ExecutionContext> {
|
2019-01-16 01:21:23 +00:00
|
|
|
if (this._detached)
|
|
|
|
throw new Error(`Execution Context is not available in detached frame "${this._frame.url()}" (are you trying to evaluate?)`);
|
|
|
|
return this._contextPromise;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param {Function|string} pageFunction
|
|
|
|
* @param {!Array<*>} args
|
2020-04-21 11:11:06 +00:00
|
|
|
* @return {!Promise<!JSHandle>}
|
2019-01-16 01:21:23 +00:00
|
|
|
*/
|
2020-04-28 14:35:43 +00:00
|
|
|
async evaluateHandle(pageFunction: Function | string, ...args: unknown[]): Promise<JSHandle> {
|
2019-01-16 01:21:23 +00:00
|
|
|
const context = await this.executionContext();
|
|
|
|
return context.evaluateHandle(pageFunction, ...args);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param {Function|string} pageFunction
|
|
|
|
* @param {!Array<*>} args
|
|
|
|
* @return {!Promise<*>}
|
|
|
|
*/
|
2020-04-28 14:35:43 +00:00
|
|
|
async evaluate<ReturnType extends any>(pageFunction: Function | string, ...args: unknown[]): Promise<ReturnType> {
|
2019-01-16 01:21:23 +00:00
|
|
|
const context = await this.executionContext();
|
2020-04-28 14:35:43 +00:00
|
|
|
return context.evaluate<ReturnType>(pageFunction, ...args);
|
2019-01-16 01:21:23 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param {string} selector
|
2020-04-21 11:11:06 +00:00
|
|
|
* @return {!Promise<?ElementHandle>}
|
2019-01-16 01:21:23 +00:00
|
|
|
*/
|
2020-04-28 14:35:43 +00:00
|
|
|
async $(selector: string): Promise<ElementHandle | null> {
|
2019-01-16 01:21:23 +00:00
|
|
|
const document = await this._document();
|
|
|
|
const value = await document.$(selector);
|
|
|
|
return value;
|
|
|
|
}
|
|
|
|
|
2020-04-28 14:35:43 +00:00
|
|
|
async _document(): Promise<ElementHandle> {
|
2019-01-16 01:21:23 +00:00
|
|
|
if (this._documentPromise)
|
|
|
|
return this._documentPromise;
|
|
|
|
this._documentPromise = this.executionContext().then(async context => {
|
|
|
|
const document = await context.evaluateHandle('document');
|
|
|
|
return document.asElement();
|
|
|
|
});
|
|
|
|
return this._documentPromise;
|
|
|
|
}
|
|
|
|
|
2020-04-28 14:35:43 +00:00
|
|
|
async $x(expression: string): Promise<ElementHandle[]> {
|
2019-01-16 01:21:23 +00:00
|
|
|
const document = await this._document();
|
|
|
|
const value = await document.$x(expression);
|
|
|
|
return value;
|
|
|
|
}
|
|
|
|
|
2020-04-28 14:35:43 +00:00
|
|
|
async $eval<ReturnType extends any>(selector: string, pageFunction: Function | string, ...args: unknown[]): Promise<ReturnType> {
|
2019-01-16 01:21:23 +00:00
|
|
|
const document = await this._document();
|
2020-04-28 14:35:43 +00:00
|
|
|
return document.$eval<ReturnType>(selector, pageFunction, ...args);
|
2019-01-16 01:21:23 +00:00
|
|
|
}
|
|
|
|
|
2020-04-28 14:35:43 +00:00
|
|
|
async $$eval<ReturnType extends any>(selector: string, pageFunction: Function | string, ...args: unknown[]): Promise<ReturnType> {
|
2019-01-16 01:21:23 +00:00
|
|
|
const document = await this._document();
|
2020-04-28 14:35:43 +00:00
|
|
|
const value = await document.$$eval<ReturnType>(selector, pageFunction, ...args);
|
2019-01-16 01:21:23 +00:00
|
|
|
return value;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param {string} selector
|
2020-04-21 11:11:06 +00:00
|
|
|
* @return {!Promise<!Array<!ElementHandle>>}
|
2019-01-16 01:21:23 +00:00
|
|
|
*/
|
2020-04-28 14:35:43 +00:00
|
|
|
async $$(selector: string): Promise<ElementHandle[]> {
|
2019-01-16 01:21:23 +00:00
|
|
|
const document = await this._document();
|
|
|
|
const value = await document.$$(selector);
|
|
|
|
return value;
|
|
|
|
}
|
|
|
|
|
2020-04-28 14:35:43 +00:00
|
|
|
async content(): Promise<string> {
|
2019-01-16 01:21:23 +00:00
|
|
|
return await this.evaluate(() => {
|
|
|
|
let retVal = '';
|
|
|
|
if (document.doctype)
|
|
|
|
retVal = new XMLSerializer().serializeToString(document.doctype);
|
|
|
|
if (document.documentElement)
|
|
|
|
retVal += document.documentElement.outerHTML;
|
|
|
|
return retVal;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2020-04-28 14:35:43 +00:00
|
|
|
async setContent(html: string, options: {timeout?: number; waitUntil?: PuppeteerLifeCycleEvent | PuppeteerLifeCycleEvent[]} = {}): Promise<void> {
|
2019-01-16 01:21:23 +00:00
|
|
|
const {
|
|
|
|
waitUntil = ['load'],
|
2019-01-29 01:16:12 +00:00
|
|
|
timeout = this._timeoutSettings.navigationTimeout(),
|
2019-01-16 01:21:23 +00:00
|
|
|
} = options;
|
|
|
|
// We rely upon the fact that document.open() will reset frame lifecycle with "init"
|
|
|
|
// lifecycle event. @see https://crrev.com/608658
|
2019-05-22 18:21:45 +00:00
|
|
|
await this.evaluate(html => {
|
2019-01-16 01:21:23 +00:00
|
|
|
document.open();
|
2019-05-22 18:21:45 +00:00
|
|
|
document.write(html);
|
2019-01-16 01:21:23 +00:00
|
|
|
document.close();
|
2019-05-22 18:21:45 +00:00
|
|
|
}, html);
|
2019-01-16 01:21:23 +00:00
|
|
|
const watcher = new LifecycleWatcher(this._frameManager, this._frame, waitUntil, timeout);
|
|
|
|
const error = await Promise.race([
|
|
|
|
watcher.timeoutOrTerminationPromise(),
|
|
|
|
watcher.lifecyclePromise(),
|
|
|
|
]);
|
|
|
|
watcher.dispose();
|
|
|
|
if (error)
|
|
|
|
throw error;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param {!{url?: string, path?: string, content?: string, type?: string}} options
|
2020-04-21 11:11:06 +00:00
|
|
|
* @return {!Promise<!ElementHandle>}
|
2019-01-16 01:21:23 +00:00
|
|
|
*/
|
2020-04-28 14:35:43 +00:00
|
|
|
async addScriptTag(options: {url?: string; path?: string; content?: string; type?: string}): Promise<ElementHandle> {
|
2019-01-16 01:21:23 +00:00
|
|
|
const {
|
|
|
|
url = null,
|
|
|
|
path = null,
|
|
|
|
content = null,
|
|
|
|
type = ''
|
|
|
|
} = options;
|
|
|
|
if (url !== null) {
|
|
|
|
try {
|
|
|
|
const context = await this.executionContext();
|
|
|
|
return (await context.evaluateHandle(addScriptUrl, url, type)).asElement();
|
|
|
|
} catch (error) {
|
|
|
|
throw new Error(`Loading script from ${url} failed`);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (path !== null) {
|
|
|
|
let contents = await readFileAsync(path, 'utf8');
|
|
|
|
contents += '//# sourceURL=' + path.replace(/\n/g, '');
|
|
|
|
const context = await this.executionContext();
|
|
|
|
return (await context.evaluateHandle(addScriptContent, contents, type)).asElement();
|
|
|
|
}
|
|
|
|
|
|
|
|
if (content !== null) {
|
|
|
|
const context = await this.executionContext();
|
|
|
|
return (await context.evaluateHandle(addScriptContent, content, type)).asElement();
|
|
|
|
}
|
|
|
|
|
|
|
|
throw new Error('Provide an object with a `url`, `path` or `content` property');
|
|
|
|
|
2020-04-28 14:35:43 +00:00
|
|
|
async function addScriptUrl(url: string, type: string): Promise<HTMLElement> {
|
2019-01-16 01:21:23 +00:00
|
|
|
const script = document.createElement('script');
|
|
|
|
script.src = url;
|
|
|
|
if (type)
|
|
|
|
script.type = type;
|
|
|
|
const promise = new Promise((res, rej) => {
|
|
|
|
script.onload = res;
|
|
|
|
script.onerror = rej;
|
|
|
|
});
|
|
|
|
document.head.appendChild(script);
|
|
|
|
await promise;
|
|
|
|
return script;
|
|
|
|
}
|
|
|
|
|
2020-04-28 14:35:43 +00:00
|
|
|
function addScriptContent(content: string, type = 'text/javascript'): HTMLElement {
|
2019-01-16 01:21:23 +00:00
|
|
|
const script = document.createElement('script');
|
|
|
|
script.type = type;
|
|
|
|
script.text = content;
|
|
|
|
let error = null;
|
|
|
|
script.onerror = e => error = e;
|
|
|
|
document.head.appendChild(script);
|
|
|
|
if (error)
|
|
|
|
throw error;
|
|
|
|
return script;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-04-28 14:35:43 +00:00
|
|
|
async addStyleTag(options: {url?: string; path?: string; content?: string}): Promise<ElementHandle> {
|
2019-01-16 01:21:23 +00:00
|
|
|
const {
|
|
|
|
url = null,
|
|
|
|
path = null,
|
|
|
|
content = null
|
|
|
|
} = options;
|
|
|
|
if (url !== null) {
|
|
|
|
try {
|
|
|
|
const context = await this.executionContext();
|
|
|
|
return (await context.evaluateHandle(addStyleUrl, url)).asElement();
|
|
|
|
} catch (error) {
|
|
|
|
throw new Error(`Loading style from ${url} failed`);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (path !== null) {
|
|
|
|
let contents = await readFileAsync(path, 'utf8');
|
|
|
|
contents += '/*# sourceURL=' + path.replace(/\n/g, '') + '*/';
|
|
|
|
const context = await this.executionContext();
|
|
|
|
return (await context.evaluateHandle(addStyleContent, contents)).asElement();
|
|
|
|
}
|
|
|
|
|
|
|
|
if (content !== null) {
|
|
|
|
const context = await this.executionContext();
|
|
|
|
return (await context.evaluateHandle(addStyleContent, content)).asElement();
|
|
|
|
}
|
|
|
|
|
|
|
|
throw new Error('Provide an object with a `url`, `path` or `content` property');
|
|
|
|
|
2020-04-28 14:35:43 +00:00
|
|
|
async function addStyleUrl(url: string): Promise<HTMLElement> {
|
2019-01-16 01:21:23 +00:00
|
|
|
const link = document.createElement('link');
|
|
|
|
link.rel = 'stylesheet';
|
|
|
|
link.href = url;
|
|
|
|
const promise = new Promise((res, rej) => {
|
|
|
|
link.onload = res;
|
|
|
|
link.onerror = rej;
|
|
|
|
});
|
|
|
|
document.head.appendChild(link);
|
|
|
|
await promise;
|
|
|
|
return link;
|
|
|
|
}
|
|
|
|
|
2020-04-28 14:35:43 +00:00
|
|
|
async function addStyleContent(content: string): Promise<HTMLElement> {
|
2019-01-16 01:21:23 +00:00
|
|
|
const style = document.createElement('style');
|
|
|
|
style.type = 'text/css';
|
|
|
|
style.appendChild(document.createTextNode(content));
|
|
|
|
const promise = new Promise((res, rej) => {
|
|
|
|
style.onload = res;
|
|
|
|
style.onerror = rej;
|
|
|
|
});
|
|
|
|
document.head.appendChild(style);
|
|
|
|
await promise;
|
|
|
|
return style;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-04-28 14:35:43 +00:00
|
|
|
async click(selector: string, options: {delay?: number; button?: MouseButtonInput; clickCount?: number}): Promise<void> {
|
2019-01-16 01:21:23 +00:00
|
|
|
const handle = await this.$(selector);
|
|
|
|
assert(handle, 'No node found for selector: ' + selector);
|
|
|
|
await handle.click(options);
|
|
|
|
await handle.dispose();
|
|
|
|
}
|
|
|
|
|
2020-04-28 14:35:43 +00:00
|
|
|
async focus(selector: string): Promise<void> {
|
2019-01-16 01:21:23 +00:00
|
|
|
const handle = await this.$(selector);
|
|
|
|
assert(handle, 'No node found for selector: ' + selector);
|
|
|
|
await handle.focus();
|
|
|
|
await handle.dispose();
|
|
|
|
}
|
|
|
|
|
2020-04-28 14:35:43 +00:00
|
|
|
async hover(selector: string): Promise<void> {
|
2019-01-16 01:21:23 +00:00
|
|
|
const handle = await this.$(selector);
|
|
|
|
assert(handle, 'No node found for selector: ' + selector);
|
|
|
|
await handle.hover();
|
|
|
|
await handle.dispose();
|
|
|
|
}
|
|
|
|
|
2020-04-28 14:35:43 +00:00
|
|
|
async select(selector: string, ...values: string[]): Promise<string[]> {
|
2019-09-04 22:19:34 +00:00
|
|
|
const handle = await this.$(selector);
|
|
|
|
assert(handle, 'No node found for selector: ' + selector);
|
|
|
|
const result = await handle.select(...values);
|
|
|
|
await handle.dispose();
|
|
|
|
return result;
|
2019-01-16 01:21:23 +00:00
|
|
|
}
|
|
|
|
|
2020-04-28 14:35:43 +00:00
|
|
|
async tap(selector: string): Promise<void> {
|
2019-01-16 01:21:23 +00:00
|
|
|
const handle = await this.$(selector);
|
|
|
|
assert(handle, 'No node found for selector: ' + selector);
|
|
|
|
await handle.tap();
|
|
|
|
await handle.dispose();
|
|
|
|
}
|
|
|
|
|
2020-04-28 14:35:43 +00:00
|
|
|
async type(selector: string, text: string, options?: {delay: number}): Promise<void> {
|
2019-01-16 01:21:23 +00:00
|
|
|
const handle = await this.$(selector);
|
|
|
|
assert(handle, 'No node found for selector: ' + selector);
|
|
|
|
await handle.type(text, options);
|
|
|
|
await handle.dispose();
|
|
|
|
}
|
|
|
|
|
2020-04-28 14:35:43 +00:00
|
|
|
waitForSelector(selector: string, options: WaitForSelectorOptions): Promise<ElementHandle | null> {
|
2019-01-16 01:21:23 +00:00
|
|
|
return this._waitForSelectorOrXPath(selector, false, options);
|
|
|
|
}
|
|
|
|
|
2020-04-28 14:35:43 +00:00
|
|
|
waitForXPath(xpath: string, options: WaitForSelectorOptions): Promise<ElementHandle | null> {
|
2019-01-16 01:21:23 +00:00
|
|
|
return this._waitForSelectorOrXPath(xpath, true, options);
|
|
|
|
}
|
|
|
|
|
2020-04-28 14:35:43 +00:00
|
|
|
waitForFunction(pageFunction: Function | string, options: {polling?: string | number; timeout?: number} = {}, ...args: unknown[]): Promise<JSHandle> {
|
2019-01-16 01:21:23 +00:00
|
|
|
const {
|
|
|
|
polling = 'raf',
|
2019-01-29 01:16:12 +00:00
|
|
|
timeout = this._timeoutSettings.timeout(),
|
2019-01-16 01:21:23 +00:00
|
|
|
} = options;
|
|
|
|
return new WaitTask(this, pageFunction, 'function', polling, timeout, ...args).promise;
|
|
|
|
}
|
|
|
|
|
2020-04-28 14:35:43 +00:00
|
|
|
async title(): Promise<string> {
|
2019-01-16 01:21:23 +00:00
|
|
|
return this.evaluate(() => document.title);
|
|
|
|
}
|
|
|
|
|
2020-04-28 14:35:43 +00:00
|
|
|
private async _waitForSelectorOrXPath(selectorOrXPath: string, isXPath: boolean, options: WaitForSelectorOptions = {}): Promise<ElementHandle | null> {
|
2019-01-16 01:21:23 +00:00
|
|
|
const {
|
|
|
|
visible: waitForVisible = false,
|
|
|
|
hidden: waitForHidden = false,
|
2019-01-29 01:16:12 +00:00
|
|
|
timeout = this._timeoutSettings.timeout(),
|
2019-01-16 01:21:23 +00:00
|
|
|
} = options;
|
|
|
|
const polling = waitForVisible || waitForHidden ? 'raf' : 'mutation';
|
|
|
|
const title = `${isXPath ? 'XPath' : 'selector'} "${selectorOrXPath}"${waitForHidden ? ' to be hidden' : ''}`;
|
2019-01-28 19:24:53 +00:00
|
|
|
const waitTask = new WaitTask(this, predicate, title, polling, timeout, selectorOrXPath, isXPath, waitForVisible, waitForHidden);
|
|
|
|
const handle = await waitTask.promise;
|
|
|
|
if (!handle.asElement()) {
|
|
|
|
await handle.dispose();
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
return handle.asElement();
|
2019-01-16 01:21:23 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* @param {string} selectorOrXPath
|
|
|
|
* @param {boolean} isXPath
|
|
|
|
* @param {boolean} waitForVisible
|
|
|
|
* @param {boolean} waitForHidden
|
|
|
|
* @return {?Node|boolean}
|
|
|
|
*/
|
2020-04-28 14:35:43 +00:00
|
|
|
function predicate(selectorOrXPath: string, isXPath: boolean, waitForVisible: boolean, waitForHidden: boolean): Node | null | boolean {
|
2019-01-16 01:21:23 +00:00
|
|
|
const node = isXPath
|
|
|
|
? document.evaluate(selectorOrXPath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue
|
|
|
|
: document.querySelector(selectorOrXPath);
|
|
|
|
if (!node)
|
|
|
|
return waitForHidden;
|
|
|
|
if (!waitForVisible && !waitForHidden)
|
|
|
|
return node;
|
2020-04-28 14:35:43 +00:00
|
|
|
const element = (node.nodeType === Node.TEXT_NODE ? node.parentElement : node as Element);
|
2019-01-16 01:21:23 +00:00
|
|
|
|
|
|
|
const style = window.getComputedStyle(element);
|
|
|
|
const isVisible = style && style.visibility !== 'hidden' && hasVisibleBoundingBox();
|
|
|
|
const success = (waitForVisible === isVisible || waitForHidden === !isVisible);
|
|
|
|
return success ? node : null;
|
|
|
|
|
2020-04-28 14:35:43 +00:00
|
|
|
function hasVisibleBoundingBox(): boolean {
|
2019-01-16 01:21:23 +00:00
|
|
|
const rect = element.getBoundingClientRect();
|
|
|
|
return !!(rect.top || rect.bottom || rect.width || rect.height);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
class WaitTask {
|
2020-04-28 14:35:43 +00:00
|
|
|
_domWorld: DOMWorld;
|
|
|
|
_polling: string | number;
|
|
|
|
_timeout: number;
|
|
|
|
_predicateBody: string;
|
|
|
|
_args: unknown[];
|
|
|
|
_runCount = 0;
|
|
|
|
promise: Promise<JSHandle>;
|
|
|
|
_resolve: (x: JSHandle) => void;
|
|
|
|
_reject: (x: Error) => void;
|
|
|
|
_timeoutTimer?: NodeJS.Timeout;
|
|
|
|
_terminated = false;
|
|
|
|
|
|
|
|
constructor(domWorld: DOMWorld, predicateBody: Function | string, title: string, polling: string | number, timeout: number, ...args: unknown[]) {
|
2019-01-16 01:21:23 +00:00
|
|
|
if (helper.isString(polling))
|
|
|
|
assert(polling === 'raf' || polling === 'mutation', 'Unknown polling option: ' + polling);
|
|
|
|
else if (helper.isNumber(polling))
|
|
|
|
assert(polling > 0, 'Cannot poll with non-positive interval: ' + polling);
|
|
|
|
else
|
|
|
|
throw new Error('Unknown polling options: ' + polling);
|
|
|
|
|
|
|
|
this._domWorld = domWorld;
|
|
|
|
this._polling = polling;
|
|
|
|
this._timeout = timeout;
|
|
|
|
this._predicateBody = helper.isString(predicateBody) ? 'return (' + predicateBody + ')' : 'return (' + predicateBody + ')(...args)';
|
|
|
|
this._args = args;
|
|
|
|
this._runCount = 0;
|
|
|
|
domWorld._waitTasks.add(this);
|
2020-04-28 14:35:43 +00:00
|
|
|
this.promise = new Promise<JSHandle>((resolve, reject) => {
|
2019-01-16 01:21:23 +00:00
|
|
|
this._resolve = resolve;
|
|
|
|
this._reject = reject;
|
|
|
|
});
|
|
|
|
// Since page navigation requires us to re-install the pageScript, we should track
|
|
|
|
// timeout on our end.
|
|
|
|
if (timeout) {
|
|
|
|
const timeoutError = new TimeoutError(`waiting for ${title} failed: timeout ${timeout}ms exceeded`);
|
|
|
|
this._timeoutTimer = setTimeout(() => this.terminate(timeoutError), timeout);
|
|
|
|
}
|
|
|
|
this.rerun();
|
|
|
|
}
|
|
|
|
|
2020-04-28 14:35:43 +00:00
|
|
|
terminate(error: Error): void {
|
2019-01-16 01:21:23 +00:00
|
|
|
this._terminated = true;
|
|
|
|
this._reject(error);
|
|
|
|
this._cleanup();
|
|
|
|
}
|
|
|
|
|
2020-04-28 14:35:43 +00:00
|
|
|
async rerun(): Promise<void> {
|
2019-01-16 01:21:23 +00:00
|
|
|
const runCount = ++this._runCount;
|
2020-04-21 11:11:06 +00:00
|
|
|
/** @type {?JSHandle} */
|
2019-01-16 01:21:23 +00:00
|
|
|
let success = null;
|
|
|
|
let error = null;
|
|
|
|
try {
|
|
|
|
success = await (await this._domWorld.executionContext()).evaluateHandle(waitForPredicatePageFunction, this._predicateBody, this._polling, this._timeout, ...this._args);
|
2020-04-28 13:16:28 +00:00
|
|
|
} catch (error_) {
|
|
|
|
error = error_;
|
2019-01-16 01:21:23 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if (this._terminated || runCount !== this._runCount) {
|
|
|
|
if (success)
|
|
|
|
await success.dispose();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Ignore timeouts in pageScript - we track timeouts ourselves.
|
|
|
|
// If the frame's execution context has already changed, `frame.evaluate` will
|
|
|
|
// throw an error - ignore this predicate run altogether.
|
2020-04-28 14:35:43 +00:00
|
|
|
if (!error && await this._domWorld.evaluate(s => !s, success).catch(() => true)) {
|
2019-01-16 01:21:23 +00:00
|
|
|
await success.dispose();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// When the page is navigated, the promise is rejected.
|
|
|
|
// We will try again in the new execution context.
|
|
|
|
if (error && error.message.includes('Execution context was destroyed'))
|
|
|
|
return;
|
|
|
|
|
|
|
|
// We could have tried to evaluate in a context which was already
|
|
|
|
// destroyed.
|
|
|
|
if (error && error.message.includes('Cannot find context with specified id'))
|
|
|
|
return;
|
|
|
|
|
|
|
|
if (error)
|
|
|
|
this._reject(error);
|
|
|
|
else
|
|
|
|
this._resolve(success);
|
|
|
|
|
|
|
|
this._cleanup();
|
|
|
|
}
|
|
|
|
|
2020-04-28 14:35:43 +00:00
|
|
|
_cleanup(): void {
|
2019-01-16 01:21:23 +00:00
|
|
|
clearTimeout(this._timeoutTimer);
|
|
|
|
this._domWorld._waitTasks.delete(this);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-04-28 14:35:43 +00:00
|
|
|
async function waitForPredicatePageFunction(predicateBody: string, polling: string, timeout: number, ...args: unknown[]): Promise<unknown> {
|
2019-01-16 01:21:23 +00:00
|
|
|
const predicate = new Function('...args', predicateBody);
|
|
|
|
let timedOut = false;
|
|
|
|
if (timeout)
|
|
|
|
setTimeout(() => timedOut = true, timeout);
|
|
|
|
if (polling === 'raf')
|
|
|
|
return await pollRaf();
|
|
|
|
if (polling === 'mutation')
|
|
|
|
return await pollMutation();
|
|
|
|
if (typeof polling === 'number')
|
|
|
|
return await pollInterval(polling);
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @return {!Promise<*>}
|
|
|
|
*/
|
2020-04-28 14:35:43 +00:00
|
|
|
function pollMutation(): Promise<unknown> {
|
|
|
|
const success = predicate(...args);
|
2019-01-16 01:21:23 +00:00
|
|
|
if (success)
|
|
|
|
return Promise.resolve(success);
|
|
|
|
|
|
|
|
let fulfill;
|
|
|
|
const result = new Promise(x => fulfill = x);
|
2020-04-28 14:35:43 +00:00
|
|
|
const observer = new MutationObserver(() => {
|
2019-01-16 01:21:23 +00:00
|
|
|
if (timedOut) {
|
|
|
|
observer.disconnect();
|
|
|
|
fulfill();
|
|
|
|
}
|
2020-04-28 14:35:43 +00:00
|
|
|
const success = predicate(...args);
|
2019-01-16 01:21:23 +00:00
|
|
|
if (success) {
|
|
|
|
observer.disconnect();
|
|
|
|
fulfill(success);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
observer.observe(document, {
|
|
|
|
childList: true,
|
|
|
|
subtree: true,
|
|
|
|
attributes: true
|
|
|
|
});
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
2020-04-28 14:35:43 +00:00
|
|
|
function pollRaf(): Promise<unknown> {
|
2019-01-16 01:21:23 +00:00
|
|
|
let fulfill;
|
|
|
|
const result = new Promise(x => fulfill = x);
|
|
|
|
onRaf();
|
|
|
|
return result;
|
|
|
|
|
2020-04-28 14:35:43 +00:00
|
|
|
function onRaf(): void {
|
2019-01-16 01:21:23 +00:00
|
|
|
if (timedOut) {
|
|
|
|
fulfill();
|
|
|
|
return;
|
|
|
|
}
|
2020-04-28 14:35:43 +00:00
|
|
|
const success = predicate(...args);
|
2019-01-16 01:21:23 +00:00
|
|
|
if (success)
|
|
|
|
fulfill(success);
|
|
|
|
else
|
|
|
|
requestAnimationFrame(onRaf);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-04-28 14:35:43 +00:00
|
|
|
function pollInterval(pollInterval: number): Promise<unknown> {
|
2019-01-16 01:21:23 +00:00
|
|
|
let fulfill;
|
|
|
|
const result = new Promise(x => fulfill = x);
|
|
|
|
onTimeout();
|
|
|
|
return result;
|
|
|
|
|
2020-04-28 14:35:43 +00:00
|
|
|
function onTimeout(): void {
|
2019-01-16 01:21:23 +00:00
|
|
|
if (timedOut) {
|
|
|
|
fulfill();
|
|
|
|
return;
|
|
|
|
}
|
2020-04-28 14:35:43 +00:00
|
|
|
const success = predicate(...args);
|
2019-01-16 01:21:23 +00:00
|
|
|
if (success)
|
|
|
|
fulfill(success);
|
|
|
|
else
|
|
|
|
setTimeout(onTimeout, pollInterval);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|