diff --git a/lib/ElementHandle.js b/lib/ElementHandle.js deleted file mode 100644 index 14275529..00000000 --- a/lib/ElementHandle.js +++ /dev/null @@ -1,393 +0,0 @@ -/** - * Copyright 2017 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 path = require('path'); -const {JSHandle} = require('./ExecutionContext'); -const {helper, assert, debugError} = require('./helper'); - -class ElementHandle extends JSHandle { - /** - * @param {!Puppeteer.ExecutionContext} context - * @param {!Puppeteer.CDPSession} client - * @param {!Protocol.Runtime.RemoteObject} remoteObject - * @param {!Puppeteer.Page} page - * @param {!Puppeteer.FrameManager} frameManager - */ - constructor(context, client, remoteObject, page, frameManager) { - super(context, client, remoteObject); - this._client = client; - this._remoteObject = remoteObject; - this._page = page; - this._frameManager = frameManager; - this._disposed = false; - } - - /** - * @override - * @return {?ElementHandle} - */ - asElement() { - return this; - } - - /** - * @return {!Promise} - */ - async contentFrame() { - const nodeInfo = await this._client.send('DOM.describeNode', { - objectId: this._remoteObject.objectId - }); - if (typeof nodeInfo.node.frameId !== 'string') - return null; - return this._frameManager.frame(nodeInfo.node.frameId); - } - - async _scrollIntoViewIfNeeded() { - const error = await this.executionContext().evaluate(async(element, pageJavascriptEnabled) => { - if (!element.isConnected) - return 'Node is detached from document'; - if (element.nodeType !== Node.ELEMENT_NODE) - return 'Node is not of type HTMLElement'; - // force-scroll if page's javascript is disabled. - if (!pageJavascriptEnabled) { - element.scrollIntoView({block: 'center', inline: 'center', behavior: 'instant'}); - return false; - } - const visibleRatio = await new Promise(resolve => { - const observer = new IntersectionObserver(entries => { - resolve(entries[0].intersectionRatio); - observer.disconnect(); - }); - observer.observe(element); - }); - if (visibleRatio !== 1.0) - element.scrollIntoView({block: 'center', inline: 'center', behavior: 'instant'}); - return false; - }, this, this._page._javascriptEnabled); - if (error) - throw new Error(error); - } - - /** - * @return {!Promise} - */ - async _clickablePoint() { - const result = await this._client.send('DOM.getContentQuads', { - objectId: this._remoteObject.objectId - }).catch(debugError); - 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 quads = result.quads.map(quad => this._fromProtocolQuad(quad)).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 - }; - } - - /** - * @return {!Promise} - */ - _getBoxModel() { - return this._client.send('DOM.getBoxModel', { - objectId: this._remoteObject.objectId - }).catch(error => debugError(error)); - } - - /** - * @param {!Array} quad - * @return {!Array} - */ - _fromProtocolQuad(quad) { - 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]} - ]; - } - - async hover() { - await this._scrollIntoViewIfNeeded(); - const {x, y} = await this._clickablePoint(); - await this._page.mouse.move(x, y); - } - - /** - * @param {!Object=} options - */ - async click(options = {}) { - await this._scrollIntoViewIfNeeded(); - const {x, y} = await this._clickablePoint(); - await this._page.mouse.click(x, y, options); - } - - /** - * @param {!Array} filePaths - * @return {!Promise} - */ - async uploadFile(...filePaths) { - const files = filePaths.map(filePath => path.resolve(filePath)); - const objectId = this._remoteObject.objectId; - return this._client.send('DOM.setFileInputFiles', { objectId, files }); - } - - async tap() { - await this._scrollIntoViewIfNeeded(); - const {x, y} = await this._clickablePoint(); - await this._page.touchscreen.tap(x, y); - } - - async focus() { - await this.executionContext().evaluate(element => element.focus(), this); - } - - /** - * @param {string} text - * @param {{delay: (number|undefined)}=} options - */ - async type(text, options) { - await this.focus(); - await this._page.keyboard.type(text, options); - } - - /** - * @param {string} key - * @param {!Object=} options - */ - async press(key, options) { - await this.focus(); - await this._page.keyboard.press(key, options); - } - - /** - * @return {!Promise} - */ - async boundingBox() { - const result = await this._getBoxModel(); - - if (!result) - return null; - - const quad = result.model.border; - const x = Math.min(quad[0], quad[2], quad[4], quad[6]); - const y = Math.min(quad[1], quad[3], quad[5], quad[7]); - const width = Math.max(quad[0], quad[2], quad[4], quad[6]) - x; - const height = Math.max(quad[1], quad[3], quad[5], quad[7]) - y; - - return {x, y, width, height}; - } - - /** - * @return {!Promise} - */ - async boxModel() { - const result = await this._getBoxModel(); - - if (!result) - return null; - - const {content, padding, border, margin, width, height} = result.model; - return { - content: this._fromProtocolQuad(content), - padding: this._fromProtocolQuad(padding), - border: this._fromProtocolQuad(border), - margin: this._fromProtocolQuad(margin), - width, - height - }; - } - - /** - * - * @param {!Object=} options - * @returns {!Promise} - */ - async screenshot(options = {}) { - let needsViewportReset = false; - - let boundingBox = await this.boundingBox(); - assert(boundingBox, 'Node is either not visible or not an HTMLElement'); - - const viewport = this._page.viewport(); - - if (boundingBox.width > viewport.width || boundingBox.height > viewport.height) { - const newViewport = { - width: Math.max(viewport.width, Math.ceil(boundingBox.width)), - height: Math.max(viewport.height, Math.ceil(boundingBox.height)), - }; - await this._page.setViewport(Object.assign({}, viewport, newViewport)); - - needsViewportReset = true; - } - - await this._scrollIntoViewIfNeeded(); - - boundingBox = await this.boundingBox(); - assert(boundingBox, 'Node is either not visible or not an HTMLElement'); - - const { layoutViewport: { pageX, pageY } } = await this._client.send('Page.getLayoutMetrics'); - - const clip = Object.assign({}, boundingBox); - clip.x += pageX; - clip.y += pageY; - - const imageData = await this._page.screenshot(Object.assign({}, { - clip - }, options)); - - if (needsViewportReset) - await this._page.setViewport(viewport); - - return imageData; - } - - /** - * @param {string} selector - * @return {!Promise} - */ - async $(selector) { - const handle = await this.executionContext().evaluateHandle( - (element, selector) => element.querySelector(selector), - this, selector - ); - const element = handle.asElement(); - if (element) - return element; - await handle.dispose(); - return null; - } - - /** - * @param {string} selector - * @return {!Promise>} - */ - async $$(selector) { - const arrayHandle = await this.executionContext().evaluateHandle( - (element, selector) => element.querySelectorAll(selector), - this, selector - ); - const properties = await arrayHandle.getProperties(); - await arrayHandle.dispose(); - const result = []; - for (const property of properties.values()) { - const elementHandle = property.asElement(); - if (elementHandle) - result.push(elementHandle); - } - return result; - } - - /** - * @param {string} selector - * @param {Function|String} pageFunction - * @param {!Array<*>} args - * @return {!Promise<(!Object|undefined)>} - */ - async $eval(selector, pageFunction, ...args) { - const elementHandle = await this.$(selector); - if (!elementHandle) - throw new Error(`Error: failed to find element matching selector "${selector}"`); - const result = await this.executionContext().evaluate(pageFunction, elementHandle, ...args); - await elementHandle.dispose(); - return result; - } - - /** - * @param {string} selector - * @param {Function|String} pageFunction - * @param {!Array<*>} args - * @return {!Promise<(!Object|undefined)>} - */ - async $$eval(selector, pageFunction, ...args) { - const arrayHandle = await this.executionContext().evaluateHandle( - (element, selector) => Array.from(element.querySelectorAll(selector)), - this, selector - ); - - const result = await this.executionContext().evaluate(pageFunction, arrayHandle, ...args); - await arrayHandle.dispose(); - return result; - } - - /** - * @param {string} expression - * @return {!Promise>} - */ - async $x(expression) { - const arrayHandle = await this.executionContext().evaluateHandle( - (element, expression) => { - const document = element.ownerDocument || element; - const iterator = document.evaluate(expression, element, null, XPathResult.ORDERED_NODE_ITERATOR_TYPE); - const array = []; - let item; - while ((item = iterator.iterateNext())) - array.push(item); - return array; - }, - this, expression - ); - const properties = await arrayHandle.getProperties(); - await arrayHandle.dispose(); - const result = []; - for (const property of properties.values()) { - const elementHandle = property.asElement(); - if (elementHandle) - result.push(elementHandle); - } - return result; - } - - /** - * @returns {!Promise} - */ - isIntersectingViewport() { - return this.executionContext().evaluate(async element => { - const visibleRatio = await new Promise(resolve => { - const observer = new IntersectionObserver(entries => { - resolve(entries[0].intersectionRatio); - observer.disconnect(); - }); - observer.observe(element); - }); - return visibleRatio > 0; - }, this); - } -} - -function computeQuadArea(quad) { - // 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 area; -} - -module.exports = {ElementHandle}; -helper.tracePublicAPI(ElementHandle); diff --git a/lib/ExecutionContext.js b/lib/ExecutionContext.js index ff66de87..ca6b0d2d 100644 --- a/lib/ExecutionContext.js +++ b/lib/ExecutionContext.js @@ -14,24 +14,32 @@ * limitations under the License. */ -const {helper, assert} = require('./helper'); +const {helper, assert, debugError} = require('./helper'); +const path = require('path'); const EVALUATION_SCRIPT_URL = '__puppeteer_evaluation_script__'; const SOURCE_URL_REGEX = /^[\040\t]*\/\/[@#] sourceURL=\s*(\S*?)\s*$/m; +function createJSHandle(context, remoteObject) { + const frame = context.frame(); + if (remoteObject.subtype === 'node' && frame) { + const frameManager = frame._frameManager; + return new ElementHandle(context, context._client, remoteObject, frameManager.page(), frameManager); + } + return new JSHandle(context, context._client, remoteObject); +} + class ExecutionContext { /** * @param {!Puppeteer.CDPSession} client * @param {!Protocol.Runtime.ExecutionContextDescription} contextPayload - * @param {function(!Protocol.Runtime.RemoteObject):!JSHandle} objectHandleFactory * @param {?Puppeteer.Frame} frame */ - constructor(client, contextPayload, objectHandleFactory, frame) { + constructor(client, contextPayload, frame) { this._client = client; this._frame = frame; this._contextId = contextPayload.id; this._isDefault = contextPayload.auxData ? !!contextPayload.auxData['isDefault'] : false; - this._objectHandleFactory = objectHandleFactory; } /** @@ -80,7 +88,7 @@ class ExecutionContext { }).catch(rewriteError); if (exceptionDetails) throw new Error('Evaluation failed: ' + helper.getExceptionMessage(exceptionDetails)); - return this._objectHandleFactory(remoteObject); + return createJSHandle(this, remoteObject); } if (typeof pageFunction !== 'function') @@ -96,7 +104,7 @@ class ExecutionContext { }).catch(rewriteError); if (exceptionDetails) throw new Error('Evaluation failed: ' + helper.getExceptionMessage(exceptionDetails)); - return this._objectHandleFactory(remoteObject); + return createJSHandle(this, remoteObject); /** * @param {*} arg @@ -148,7 +156,7 @@ class ExecutionContext { const response = await this._client.send('Runtime.queryObjects', { prototypeObjectId: prototypeHandle._remoteObject.objectId }); - return this._objectHandleFactory(response.objects); + return createJSHandle(this, response.objects); } } @@ -200,7 +208,7 @@ class JSHandle { for (const property of response.result) { if (!property.enumerable) continue; - result.set(property.name, this._context._objectHandleFactory(property.value)); + result.set(property.name, createJSHandle(this._context, property.value)); } return result; } @@ -248,5 +256,381 @@ class JSHandle { } } + +class ElementHandle extends JSHandle { + /** + * @param {!Puppeteer.ExecutionContext} context + * @param {!Puppeteer.CDPSession} client + * @param {!Protocol.Runtime.RemoteObject} remoteObject + * @param {!Puppeteer.Page} page + * @param {!Puppeteer.FrameManager} frameManager + */ + constructor(context, client, remoteObject, page, frameManager) { + super(context, client, remoteObject); + this._client = client; + this._remoteObject = remoteObject; + this._page = page; + this._frameManager = frameManager; + this._disposed = false; + } + + /** + * @override + * @return {?ElementHandle} + */ + asElement() { + return this; + } + + /** + * @return {!Promise} + */ + async contentFrame() { + const nodeInfo = await this._client.send('DOM.describeNode', { + objectId: this._remoteObject.objectId + }); + if (typeof nodeInfo.node.frameId !== 'string') + return null; + return this._frameManager.frame(nodeInfo.node.frameId); + } + + async _scrollIntoViewIfNeeded() { + const error = await this.executionContext().evaluate(async(element, pageJavascriptEnabled) => { + if (!element.isConnected) + return 'Node is detached from document'; + if (element.nodeType !== Node.ELEMENT_NODE) + return 'Node is not of type HTMLElement'; + // force-scroll if page's javascript is disabled. + if (!pageJavascriptEnabled) { + element.scrollIntoView({block: 'center', inline: 'center', behavior: 'instant'}); + return false; + } + const visibleRatio = await new Promise(resolve => { + const observer = new IntersectionObserver(entries => { + resolve(entries[0].intersectionRatio); + observer.disconnect(); + }); + observer.observe(element); + }); + if (visibleRatio !== 1.0) + element.scrollIntoView({block: 'center', inline: 'center', behavior: 'instant'}); + return false; + }, this, this._page._javascriptEnabled); + if (error) + throw new Error(error); + } + + /** + * @return {!Promise} + */ + async _clickablePoint() { + const result = await this._client.send('DOM.getContentQuads', { + objectId: this._remoteObject.objectId + }).catch(debugError); + 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 quads = result.quads.map(quad => this._fromProtocolQuad(quad)).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 + }; + } + + /** + * @return {!Promise} + */ + _getBoxModel() { + return this._client.send('DOM.getBoxModel', { + objectId: this._remoteObject.objectId + }).catch(error => debugError(error)); + } + + /** + * @param {!Array} quad + * @return {!Array} + */ + _fromProtocolQuad(quad) { + 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]} + ]; + } + + async hover() { + await this._scrollIntoViewIfNeeded(); + const {x, y} = await this._clickablePoint(); + await this._page.mouse.move(x, y); + } + + /** + * @param {!Object=} options + */ + async click(options = {}) { + await this._scrollIntoViewIfNeeded(); + const {x, y} = await this._clickablePoint(); + await this._page.mouse.click(x, y, options); + } + + /** + * @param {!Array} filePaths + * @return {!Promise} + */ + async uploadFile(...filePaths) { + const files = filePaths.map(filePath => path.resolve(filePath)); + const objectId = this._remoteObject.objectId; + return this._client.send('DOM.setFileInputFiles', { objectId, files }); + } + + async tap() { + await this._scrollIntoViewIfNeeded(); + const {x, y} = await this._clickablePoint(); + await this._page.touchscreen.tap(x, y); + } + + async focus() { + await this.executionContext().evaluate(element => element.focus(), this); + } + + /** + * @param {string} text + * @param {{delay: (number|undefined)}=} options + */ + async type(text, options) { + await this.focus(); + await this._page.keyboard.type(text, options); + } + + /** + * @param {string} key + * @param {!Object=} options + */ + async press(key, options) { + await this.focus(); + await this._page.keyboard.press(key, options); + } + + /** + * @return {!Promise} + */ + async boundingBox() { + const result = await this._getBoxModel(); + + if (!result) + return null; + + const quad = result.model.border; + const x = Math.min(quad[0], quad[2], quad[4], quad[6]); + const y = Math.min(quad[1], quad[3], quad[5], quad[7]); + const width = Math.max(quad[0], quad[2], quad[4], quad[6]) - x; + const height = Math.max(quad[1], quad[3], quad[5], quad[7]) - y; + + return {x, y, width, height}; + } + + /** + * @return {!Promise} + */ + async boxModel() { + const result = await this._getBoxModel(); + + if (!result) + return null; + + const {content, padding, border, margin, width, height} = result.model; + return { + content: this._fromProtocolQuad(content), + padding: this._fromProtocolQuad(padding), + border: this._fromProtocolQuad(border), + margin: this._fromProtocolQuad(margin), + width, + height + }; + } + + /** + * + * @param {!Object=} options + * @returns {!Promise} + */ + async screenshot(options = {}) { + let needsViewportReset = false; + + let boundingBox = await this.boundingBox(); + assert(boundingBox, 'Node is either not visible or not an HTMLElement'); + + const viewport = this._page.viewport(); + + if (boundingBox.width > viewport.width || boundingBox.height > viewport.height) { + const newViewport = { + width: Math.max(viewport.width, Math.ceil(boundingBox.width)), + height: Math.max(viewport.height, Math.ceil(boundingBox.height)), + }; + await this._page.setViewport(Object.assign({}, viewport, newViewport)); + + needsViewportReset = true; + } + + await this._scrollIntoViewIfNeeded(); + + boundingBox = await this.boundingBox(); + assert(boundingBox, 'Node is either not visible or not an HTMLElement'); + + const { layoutViewport: { pageX, pageY } } = await this._client.send('Page.getLayoutMetrics'); + + const clip = Object.assign({}, boundingBox); + clip.x += pageX; + clip.y += pageY; + + const imageData = await this._page.screenshot(Object.assign({}, { + clip + }, options)); + + if (needsViewportReset) + await this._page.setViewport(viewport); + + return imageData; + } + + /** + * @param {string} selector + * @return {!Promise} + */ + async $(selector) { + const handle = await this.executionContext().evaluateHandle( + (element, selector) => element.querySelector(selector), + this, selector + ); + const element = handle.asElement(); + if (element) + return element; + await handle.dispose(); + return null; + } + + /** + * @param {string} selector + * @return {!Promise>} + */ + async $$(selector) { + const arrayHandle = await this.executionContext().evaluateHandle( + (element, selector) => element.querySelectorAll(selector), + this, selector + ); + const properties = await arrayHandle.getProperties(); + await arrayHandle.dispose(); + const result = []; + for (const property of properties.values()) { + const elementHandle = property.asElement(); + if (elementHandle) + result.push(elementHandle); + } + return result; + } + + /** + * @param {string} selector + * @param {Function|String} pageFunction + * @param {!Array<*>} args + * @return {!Promise<(!Object|undefined)>} + */ + async $eval(selector, pageFunction, ...args) { + const elementHandle = await this.$(selector); + if (!elementHandle) + throw new Error(`Error: failed to find element matching selector "${selector}"`); + const result = await this.executionContext().evaluate(pageFunction, elementHandle, ...args); + await elementHandle.dispose(); + return result; + } + + /** + * @param {string} selector + * @param {Function|String} pageFunction + * @param {!Array<*>} args + * @return {!Promise<(!Object|undefined)>} + */ + async $$eval(selector, pageFunction, ...args) { + const arrayHandle = await this.executionContext().evaluateHandle( + (element, selector) => Array.from(element.querySelectorAll(selector)), + this, selector + ); + + const result = await this.executionContext().evaluate(pageFunction, arrayHandle, ...args); + await arrayHandle.dispose(); + return result; + } + + /** + * @param {string} expression + * @return {!Promise>} + */ + async $x(expression) { + const arrayHandle = await this.executionContext().evaluateHandle( + (element, expression) => { + const document = element.ownerDocument || element; + const iterator = document.evaluate(expression, element, null, XPathResult.ORDERED_NODE_ITERATOR_TYPE); + const array = []; + let item; + while ((item = iterator.iterateNext())) + array.push(item); + return array; + }, + this, expression + ); + const properties = await arrayHandle.getProperties(); + await arrayHandle.dispose(); + const result = []; + for (const property of properties.values()) { + const elementHandle = property.asElement(); + if (elementHandle) + result.push(elementHandle); + } + return result; + } + + /** + * @returns {!Promise} + */ + isIntersectingViewport() { + return this.executionContext().evaluate(async element => { + const visibleRatio = await new Promise(resolve => { + const observer = new IntersectionObserver(entries => { + resolve(entries[0].intersectionRatio); + observer.disconnect(); + }); + observer.observe(element); + }); + return visibleRatio > 0; + }, this); + } +} + +function computeQuadArea(quad) { + // 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 area; +} + +helper.tracePublicAPI(ElementHandle); helper.tracePublicAPI(JSHandle); -module.exports = {ExecutionContext, JSHandle, EVALUATION_SCRIPT_URL}; +helper.tracePublicAPI(ExecutionContext); + +module.exports = {ExecutionContext, JSHandle, ElementHandle, createJSHandle, EVALUATION_SCRIPT_URL}; diff --git a/lib/FrameManager.js b/lib/FrameManager.js index 349ae47a..eb1e5a89 100644 --- a/lib/FrameManager.js +++ b/lib/FrameManager.js @@ -17,8 +17,7 @@ const fs = require('fs'); const EventEmitter = require('events'); const {helper, assert} = require('./helper'); -const {ExecutionContext, JSHandle} = require('./ExecutionContext'); -const {ElementHandle} = require('./ElementHandle'); +const {ExecutionContext} = require('./ExecutionContext'); const {TimeoutError} = require('./Errors'); const readFileAsync = helper.promisify(fs.readFile); @@ -87,6 +86,13 @@ class FrameManager extends EventEmitter { this._handleFrameTree(child); } + /** + * @return {!Puppeteer.Page} + */ + page() { + return this._page; + } + /** * @return {!Frame} */ @@ -119,7 +125,7 @@ class FrameManager extends EventEmitter { return; assert(parentFrameId); const parentFrame = this._frames.get(parentFrameId); - const frame = new Frame(this._client, parentFrame, frameId); + const frame = new Frame(this, this._client, parentFrame, frameId); this._frames.set(frame._id, frame); this.emit(FrameManager.Events.FrameAttached, frame); } @@ -146,7 +152,7 @@ class FrameManager extends EventEmitter { frame._id = framePayload.id; } else { // Initial main frame navigation. - frame = new Frame(this._client, null, framePayload.id); + frame = new Frame(this, this._client, null, framePayload.id); } this._frames.set(framePayload.id, frame); this._mainFrame = frame; @@ -184,7 +190,7 @@ class FrameManager extends EventEmitter { const frameId = contextPayload.auxData ? contextPayload.auxData.frameId : null; const frame = this._frames.get(frameId) || null; /** @type {!ExecutionContext} */ - const context = new ExecutionContext(this._client, contextPayload, obj => this.createJSHandle(context, obj), frame); + const context = new ExecutionContext(this._client, contextPayload, frame); this._contextIdToContext.set(contextPayload.id, context); if (frame) frame._addExecutionContext(context); @@ -220,17 +226,6 @@ class FrameManager extends EventEmitter { return context; } - /** - * @param {!ExecutionContext} context - * @param {!Protocol.Runtime.RemoteObject} remoteObject - * @return {!JSHandle} - */ - createJSHandle(context, remoteObject) { - if (remoteObject.subtype === 'node') - return new ElementHandle(context, this._client, remoteObject, this._page, this); - return new JSHandle(context, this._client, remoteObject); - } - /** * @param {!Frame} frame */ @@ -259,17 +254,19 @@ FrameManager.Events = { */ class Frame { /** + * @param {!FrameManager} frameManager * @param {!Puppeteer.CDPSession} client * @param {?Frame} parentFrame * @param {string} frameId */ - constructor(client, parentFrame, frameId) { + constructor(frameManager, client, parentFrame, frameId) { + this._frameManager = frameManager; this._client = client; this._parentFrame = parentFrame; this._url = ''; this._id = frameId; - /** @type {?Promise} */ + /** @type {?Promise} */ this._documentPromise = null; /** @type {?Promise} */ this._contextPromise = null; @@ -350,7 +347,7 @@ class Frame { /** * @param {string} selector - * @return {!Promise} + * @return {!Promise} */ async $(selector) { const document = await this._document(); @@ -359,7 +356,7 @@ class Frame { } /** - * @return {!Promise} + * @return {!Promise} */ async _document() { if (this._documentPromise) @@ -373,7 +370,7 @@ class Frame { /** * @param {string} expression - * @return {!Promise>} + * @return {!Promise>} */ async $x(expression) { const document = await this._document(); @@ -406,7 +403,7 @@ class Frame { /** * @param {string} selector - * @return {!Promise>} + * @return {!Promise>} */ async $$(selector) { const document = await this._document(); @@ -476,7 +473,7 @@ class Frame { /** * @param {Object} options - * @return {!Promise} + * @return {!Promise} */ async addScriptTag(options) { if (typeof options.url === 'string') { @@ -542,7 +539,7 @@ class Frame { /** * @param {Object} options - * @return {!Promise} + * @return {!Promise} */ async addStyleTag(options) { if (typeof options.url === 'string') { @@ -878,7 +875,7 @@ class WaitTask { async rerun() { const runCount = ++this._runCount; - /** @type {?JSHandle} */ + /** @type {?Puppeteer.JSHandle} */ let success = null; let error = null; try { diff --git a/lib/Page.js b/lib/Page.js index 0e2c2fb3..e1183274 100644 --- a/lib/Page.js +++ b/lib/Page.js @@ -27,6 +27,7 @@ const Tracing = require('./Tracing'); const {helper, debugError, assert} = require('./helper'); const {Coverage} = require('./Coverage'); const {Worker} = require('./Worker'); +const {createJSHandle} = require('./ExecutionContext'); const writeFileAsync = helper.promisify(fs.writeFile); @@ -480,7 +481,7 @@ class Page extends EventEmitter { */ async _onConsoleAPI(event) { const context = this._frameManager.executionContextById(event.executionContextId); - const values = event.args.map(arg => this._frameManager.createJSHandle(context, arg)); + const values = event.args.map(arg => createJSHandle(context, arg)); this._addConsoleMessage(event.type, values); } diff --git a/lib/Worker.js b/lib/Worker.js index 5404115e..df41f186 100644 --- a/lib/Worker.js +++ b/lib/Worker.js @@ -33,7 +33,7 @@ class Worker extends EventEmitter { let jsHandleFactory; this._client.once('Runtime.executionContextCreated', async event => { jsHandleFactory = remoteObject => new JSHandle(executionContext, client, remoteObject); - const executionContext = new ExecutionContext(client, event.context, jsHandleFactory, null); + const executionContext = new ExecutionContext(client, event.context, null); this._executionContextCallback(executionContext); }); // This might fail if the target is closed before we recieve all execution contexts. diff --git a/lib/externs.d.ts b/lib/externs.d.ts index 7f7b41ff..a651a0c1 100644 --- a/lib/externs.d.ts +++ b/lib/externs.d.ts @@ -5,8 +5,7 @@ import {Page as RealPage} from './Page.js'; import {TaskQueue as RealTaskQueue} from './TaskQueue.js'; import {Mouse as RealMouse, Keyboard as RealKeyboard, Touchscreen as RealTouchscreen} from './Input.js'; import {Frame as RealFrame, FrameManager as RealFrameManager} from './FrameManager.js'; -import {JSHandle as RealJSHandle, ExecutionContext as RealExecutionContext} from './ExecutionContext.js'; -import {ElementHandle as RealElementHandle} from './ElementHandle.js'; +import {JSHandle as RealJSHandle, ElementHandle as RealElementHandle, ExecutionContext as RealExecutionContext} from './ExecutionContext.js'; import { NetworkManager as RealNetworkManager, Request as RealRequest, Response as RealResponse } from './NetworkManager.js'; import * as child_process from 'child_process'; declare global {