123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707 |
- /**
- * 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.
- */
- const fs = require('fs');
- const {helper, assert} = require('./helper');
- const {LifecycleWatcher} = require('./LifecycleWatcher');
- const {TimeoutError} = require('./Errors');
- const readFileAsync = helper.promisify(fs.readFile);
- /**
- * @unrestricted
- */
- class DOMWorld {
- /**
- * @param {!Puppeteer.FrameManager} frameManager
- * @param {!Puppeteer.Frame} frame
- * @param {!Puppeteer.TimeoutSettings} timeoutSettings
- */
- constructor(frameManager, frame, timeoutSettings) {
- this._frameManager = frameManager;
- this._frame = frame;
- this._timeoutSettings = timeoutSettings;
- /** @type {?Promise<!Puppeteer.ElementHandle>} */
- this._documentPromise = null;
- /** @type {!Promise<!Puppeteer.ExecutionContext>} */
- this._contextPromise;
- this._contextResolveCallback = null;
- this._setContext(null);
- /** @type {!Set<!WaitTask>} */
- this._waitTasks = new Set();
- this._detached = false;
- }
- /**
- * @return {!Puppeteer.Frame}
- */
- frame() {
- return this._frame;
- }
- /**
- * @param {?Puppeteer.ExecutionContext} context
- */
- _setContext(context) {
- 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;
- });
- }
- }
- /**
- * @return {boolean}
- */
- _hasContext() {
- return !this._contextResolveCallback;
- }
- _detach() {
- this._detached = true;
- for (const waitTask of this._waitTasks)
- waitTask.terminate(new Error('waitForFunction failed: frame got detached.'));
- }
- /**
- * @return {!Promise<!Puppeteer.ExecutionContext>}
- */
- executionContext() {
- 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
- * @return {!Promise<!Puppeteer.JSHandle>}
- */
- async evaluateHandle(pageFunction, ...args) {
- const context = await this.executionContext();
- return context.evaluateHandle(pageFunction, ...args);
- }
- /**
- * @param {Function|string} pageFunction
- * @param {!Array<*>} args
- * @return {!Promise<*>}
- */
- async evaluate(pageFunction, ...args) {
- const context = await this.executionContext();
- return context.evaluate(pageFunction, ...args);
- }
- /**
- * @param {string} selector
- * @return {!Promise<?Puppeteer.ElementHandle>}
- */
- async $(selector) {
- const document = await this._document();
- const value = await document.$(selector);
- return value;
- }
- /**
- * @return {!Promise<!Puppeteer.ElementHandle>}
- */
- async _document() {
- 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;
- }
- /**
- * @param {string} expression
- * @return {!Promise<!Array<!Puppeteer.ElementHandle>>}
- */
- async $x(expression) {
- const document = await this._document();
- const value = await document.$x(expression);
- return value;
- }
- /**
- * @param {string} selector
- * @param {Function|string} pageFunction
- * @param {!Array<*>} args
- * @return {!Promise<(!Object|undefined)>}
- */
- async $eval(selector, pageFunction, ...args) {
- const document = await this._document();
- return document.$eval(selector, pageFunction, ...args);
- }
- /**
- * @param {string} selector
- * @param {Function|string} pageFunction
- * @param {!Array<*>} args
- * @return {!Promise<(!Object|undefined)>}
- */
- async $$eval(selector, pageFunction, ...args) {
- const document = await this._document();
- const value = await document.$$eval(selector, pageFunction, ...args);
- return value;
- }
- /**
- * @param {string} selector
- * @return {!Promise<!Array<!Puppeteer.ElementHandle>>}
- */
- async $$(selector) {
- const document = await this._document();
- const value = await document.$$(selector);
- return value;
- }
- /**
- * @return {!Promise<String>}
- */
- async content() {
- return await this.evaluate(() => {
- let retVal = '';
- if (document.doctype)
- retVal = new XMLSerializer().serializeToString(document.doctype);
- if (document.documentElement)
- retVal += document.documentElement.outerHTML;
- return retVal;
- });
- }
- /**
- * @param {string} html
- * @param {!{timeout?: number, waitUntil?: string|!Array<string>}=} options
- */
- async setContent(html, options = {}) {
- const {
- waitUntil = ['load'],
- timeout = this._timeoutSettings.navigationTimeout(),
- } = options;
- // We rely upon the fact that document.open() will reset frame lifecycle with "init"
- // lifecycle event. @see https://crrev.com/608658
- await this.evaluate(html => {
- document.open();
- document.write(html);
- document.close();
- }, html);
- 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
- * @return {!Promise<!Puppeteer.ElementHandle>}
- */
- async addScriptTag(options) {
- 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');
- /**
- * @param {string} url
- * @param {string} type
- * @return {!Promise<!HTMLElement>}
- */
- async function addScriptUrl(url, type) {
- 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;
- }
- /**
- * @param {string} content
- * @param {string} type
- * @return {!HTMLElement}
- */
- function addScriptContent(content, type = 'text/javascript') {
- 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;
- }
- }
- /**
- * @param {!{url?: string, path?: string, content?: string}} options
- * @return {!Promise<!Puppeteer.ElementHandle>}
- */
- async addStyleTag(options) {
- 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');
- /**
- * @param {string} url
- * @return {!Promise<!HTMLElement>}
- */
- async function addStyleUrl(url) {
- 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;
- }
- /**
- * @param {string} content
- * @return {!Promise<!HTMLElement>}
- */
- async function addStyleContent(content) {
- 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;
- }
- }
- /**
- * @param {string} selector
- * @param {!{delay?: number, button?: "left"|"right"|"middle", clickCount?: number}=} options
- */
- async click(selector, options) {
- const handle = await this.$(selector);
- assert(handle, 'No node found for selector: ' + selector);
- await handle.click(options);
- await handle.dispose();
- }
- /**
- * @param {string} selector
- */
- async focus(selector) {
- const handle = await this.$(selector);
- assert(handle, 'No node found for selector: ' + selector);
- await handle.focus();
- await handle.dispose();
- }
- /**
- * @param {string} selector
- */
- async hover(selector) {
- const handle = await this.$(selector);
- assert(handle, 'No node found for selector: ' + selector);
- await handle.hover();
- await handle.dispose();
- }
- /**
- * @param {string} selector
- * @param {!Array<string>} values
- * @return {!Promise<!Array<string>>}
- */
- async select(selector, ...values) {
- const handle = await this.$(selector);
- assert(handle, 'No node found for selector: ' + selector);
- const result = await handle.select(...values);
- await handle.dispose();
- return result;
- }
- /**
- * @param {string} selector
- */
- async tap(selector) {
- const handle = await this.$(selector);
- assert(handle, 'No node found for selector: ' + selector);
- await handle.tap();
- await handle.dispose();
- }
- /**
- * @param {string} selector
- * @param {string} text
- * @param {{delay: (number|undefined)}=} options
- */
- async 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();
- }
- /**
- * @param {string} selector
- * @param {!{visible?: boolean, hidden?: boolean, timeout?: number}=} options
- * @return {!Promise<?Puppeteer.ElementHandle>}
- */
- waitForSelector(selector, options) {
- return this._waitForSelectorOrXPath(selector, false, options);
- }
- /**
- * @param {string} xpath
- * @param {!{visible?: boolean, hidden?: boolean, timeout?: number}=} options
- * @return {!Promise<?Puppeteer.ElementHandle>}
- */
- waitForXPath(xpath, options) {
- return this._waitForSelectorOrXPath(xpath, true, options);
- }
- /**
- * @param {Function|string} pageFunction
- * @param {!{polling?: string|number, timeout?: number}=} options
- * @return {!Promise<!Puppeteer.JSHandle>}
- */
- waitForFunction(pageFunction, options = {}, ...args) {
- const {
- polling = 'raf',
- timeout = this._timeoutSettings.timeout(),
- } = options;
- return new WaitTask(this, pageFunction, 'function', polling, timeout, ...args).promise;
- }
- /**
- * @return {!Promise<string>}
- */
- async title() {
- return this.evaluate(() => document.title);
- }
- /**
- * @param {string} selectorOrXPath
- * @param {boolean} isXPath
- * @param {!{visible?: boolean, hidden?: boolean, timeout?: number}=} options
- * @return {!Promise<?Puppeteer.ElementHandle>}
- */
- async _waitForSelectorOrXPath(selectorOrXPath, isXPath, options = {}) {
- const {
- visible: waitForVisible = false,
- hidden: waitForHidden = false,
- timeout = this._timeoutSettings.timeout(),
- } = options;
- const polling = waitForVisible || waitForHidden ? 'raf' : 'mutation';
- const title = `${isXPath ? 'XPath' : 'selector'} "${selectorOrXPath}"${waitForHidden ? ' to be hidden' : ''}`;
- 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();
- /**
- * @param {string} selectorOrXPath
- * @param {boolean} isXPath
- * @param {boolean} waitForVisible
- * @param {boolean} waitForHidden
- * @return {?Node|boolean}
- */
- function predicate(selectorOrXPath, isXPath, waitForVisible, waitForHidden) {
- 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;
- const element = /** @type {Element} */ (node.nodeType === Node.TEXT_NODE ? node.parentElement : node);
- const style = window.getComputedStyle(element);
- const isVisible = style && style.visibility !== 'hidden' && hasVisibleBoundingBox();
- const success = (waitForVisible === isVisible || waitForHidden === !isVisible);
- return success ? node : null;
- /**
- * @return {boolean}
- */
- function hasVisibleBoundingBox() {
- const rect = element.getBoundingClientRect();
- return !!(rect.top || rect.bottom || rect.width || rect.height);
- }
- }
- }
- }
- class WaitTask {
- /**
- * @param {!DOMWorld} domWorld
- * @param {Function|string} predicateBody
- * @param {string|number} polling
- * @param {number} timeout
- * @param {!Array<*>} args
- */
- constructor(domWorld, predicateBody, title, polling, timeout, ...args) {
- 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);
- this.promise = new Promise((resolve, reject) => {
- 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();
- }
- /**
- * @param {!Error} error
- */
- terminate(error) {
- this._terminated = true;
- this._reject(error);
- this._cleanup();
- }
- async rerun() {
- const runCount = ++this._runCount;
- /** @type {?Puppeteer.JSHandle} */
- let success = null;
- let error = null;
- try {
- success = await (await this._domWorld.executionContext()).evaluateHandle(waitForPredicatePageFunction, this._predicateBody, this._polling, this._timeout, ...this._args);
- } catch (e) {
- error = e;
- }
- 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.
- if (!error && await this._domWorld.evaluate(s => !s, success).catch(e => true)) {
- 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();
- }
- _cleanup() {
- clearTimeout(this._timeoutTimer);
- this._domWorld._waitTasks.delete(this);
- this._runningTask = null;
- }
- }
- /**
- * @param {string} predicateBody
- * @param {string} polling
- * @param {number} timeout
- * @return {!Promise<*>}
- */
- async function waitForPredicatePageFunction(predicateBody, polling, timeout, ...args) {
- 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<*>}
- */
- function pollMutation() {
- const success = predicate.apply(null, args);
- if (success)
- return Promise.resolve(success);
- let fulfill;
- const result = new Promise(x => fulfill = x);
- const observer = new MutationObserver(mutations => {
- if (timedOut) {
- observer.disconnect();
- fulfill();
- }
- const success = predicate.apply(null, args);
- if (success) {
- observer.disconnect();
- fulfill(success);
- }
- });
- observer.observe(document, {
- childList: true,
- subtree: true,
- attributes: true
- });
- return result;
- }
- /**
- * @return {!Promise<*>}
- */
- function pollRaf() {
- let fulfill;
- const result = new Promise(x => fulfill = x);
- onRaf();
- return result;
- function onRaf() {
- if (timedOut) {
- fulfill();
- return;
- }
- const success = predicate.apply(null, args);
- if (success)
- fulfill(success);
- else
- requestAnimationFrame(onRaf);
- }
- }
- /**
- * @param {number} pollInterval
- * @return {!Promise<*>}
- */
- function pollInterval(pollInterval) {
- let fulfill;
- const result = new Promise(x => fulfill = x);
- onTimeout();
- return result;
- function onTimeout() {
- if (timedOut) {
- fulfill();
- return;
- }
- const success = predicate.apply(null, args);
- if (success)
- fulfill(success);
- else
- setTimeout(onTimeout, pollInterval);
- }
- }
- }
- module.exports = {DOMWorld};
|