UNPKG

9.79 kBJavaScriptView Raw
1/**
2 * Copyright 2017 Google Inc. All rights reserved.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16const path = require('path');
17const {JSHandle} = require('./ExecutionContext');
18const {helper, debugError} = require('./helper');
19
20class ElementHandle extends JSHandle {
21 /**
22 * @param {!Puppeteer.ExecutionContext} context
23 * @param {!Puppeteer.CDPSession} client
24 * @param {!Protocol.Runtime.RemoteObject} remoteObject
25 * @param {!Puppeteer.Page} page
26 * @param {!Puppeteer.FrameManager} frameManager
27 */
28 constructor(context, client, remoteObject, page, frameManager) {
29 super(context, client, remoteObject);
30 this._client = client;
31 this._remoteObject = remoteObject;
32 this._page = page;
33 this._frameManager = frameManager;
34 this._disposed = false;
35 }
36
37 /**
38 * @override
39 * @return {?ElementHandle}
40 */
41 asElement() {
42 return this;
43 }
44
45 /**
46 * @return {!Promise<?Puppeteer.Frame>}
47 */
48 async contentFrame() {
49 const nodeInfo = await this._client.send('DOM.describeNode', {
50 objectId: this._remoteObject.objectId
51 });
52 if (typeof nodeInfo.node.frameId !== 'string')
53 return null;
54 return this._frameManager.frame(nodeInfo.node.frameId);
55 }
56
57 async _scrollIntoViewIfNeeded() {
58 const error = await this.executionContext().evaluate(element => {
59 if (!element.isConnected)
60 return 'Node is detached from document';
61 if (element.nodeType !== Node.ELEMENT_NODE)
62 return 'Node is not of type HTMLElement';
63 element.scrollIntoViewIfNeeded();
64 return false;
65 }, this);
66 if (error)
67 throw new Error(error);
68 }
69
70 /**
71 * @return {!Promise<{x: number, y: number}>}
72 */
73 async _visibleCenter() {
74 await this._scrollIntoViewIfNeeded();
75 const box = await this._assertBoundingBox();
76 return {
77 x: box.x + box.width / 2,
78 y: box.y + box.height / 2
79 };
80 }
81
82 /**
83 * @return {!Promise<void|Protocol.DOM.getBoxModelReturnValue>}
84 */
85 _getBoxModel() {
86 return this._client.send('DOM.getBoxModel', {
87 objectId: this._remoteObject.objectId
88 }).catch(error => debugError(error));
89 }
90
91 /**
92 * @param {!Array<number>} quad
93 * @return {!Array<object>}
94 */
95 _fromProtocolQuad(quad) {
96 return [
97 {x: quad[0], y: quad[1]},
98 {x: quad[2], y: quad[3]},
99 {x: quad[4], y: quad[5]},
100 {x: quad[6], y: quad[7]}
101 ];
102 }
103
104 async hover() {
105 const {x, y} = await this._visibleCenter();
106 await this._page.mouse.move(x, y);
107 }
108
109 /**
110 * @param {!Object=} options
111 */
112 async click(options = {}) {
113 const {x, y} = await this._visibleCenter();
114 await this._page.mouse.click(x, y, options);
115 }
116
117 /**
118 * @param {!Array<string>} filePaths
119 * @return {!Promise}
120 */
121 async uploadFile(...filePaths) {
122 const files = filePaths.map(filePath => path.resolve(filePath));
123 const objectId = this._remoteObject.objectId;
124 return this._client.send('DOM.setFileInputFiles', { objectId, files });
125 }
126
127 async tap() {
128 const {x, y} = await this._visibleCenter();
129 await this._page.touchscreen.tap(x, y);
130 }
131
132 async focus() {
133 await this.executionContext().evaluate(element => element.focus(), this);
134 }
135
136 /**
137 * @param {string} text
138 * @param {{delay: (number|undefined)}=} options
139 */
140 async type(text, options) {
141 await this.focus();
142 await this._page.keyboard.type(text, options);
143 }
144
145 /**
146 * @param {string} key
147 * @param {!Object=} options
148 */
149 async press(key, options) {
150 await this.focus();
151 await this._page.keyboard.press(key, options);
152 }
153
154 /**
155 * @return {!Promise<?{x: number, y: number, width: number, height: number}>}
156 */
157 async boundingBox() {
158 const result = await this._getBoxModel();
159
160 if (!result)
161 return null;
162
163 const quad = result.model.border;
164 const x = Math.min(quad[0], quad[2], quad[4], quad[6]);
165 const y = Math.min(quad[1], quad[3], quad[5], quad[7]);
166 const width = Math.max(quad[0], quad[2], quad[4], quad[6]) - x;
167 const height = Math.max(quad[1], quad[3], quad[5], quad[7]) - y;
168
169 return {x, y, width, height};
170 }
171
172 /**
173 * @return {!Promise<?object>}
174 */
175 async boxModel() {
176 const result = await this._getBoxModel();
177
178 if (!result)
179 return null;
180
181 const {content, padding, border, margin, width, height} = result.model;
182 return {
183 content: this._fromProtocolQuad(content),
184 padding: this._fromProtocolQuad(padding),
185 border: this._fromProtocolQuad(border),
186 margin: this._fromProtocolQuad(margin),
187 width,
188 height
189 };
190 }
191
192 /**
193 * @return {!Promise<?{x: number, y: number, width: number, height: number}>}
194 */
195 async _assertBoundingBox() {
196 const boundingBox = await this.boundingBox();
197 if (boundingBox)
198 return boundingBox;
199
200 throw new Error('Node is either not visible or not an HTMLElement');
201 }
202
203 /**
204 *
205 * @param {!Object=} options
206 * @returns {!Promise<Object>}
207 */
208 async screenshot(options = {}) {
209 let needsViewportReset = false;
210
211 let boundingBox = await this._assertBoundingBox();
212
213 const viewport = this._page.viewport();
214
215 if (boundingBox.width > viewport.width || boundingBox.height > viewport.height) {
216 const newViewport = {
217 width: Math.max(viewport.width, Math.ceil(boundingBox.width)),
218 height: Math.max(viewport.height, Math.ceil(boundingBox.height)),
219 };
220 await this._page.setViewport(Object.assign({}, viewport, newViewport));
221
222 needsViewportReset = true;
223 }
224
225 await this.executionContext().evaluate(function(element) {
226 element.scrollIntoView({block: 'center', inline: 'center', behavior: 'instant'});
227 }, this);
228
229 boundingBox = await this._assertBoundingBox();
230
231 const { layoutViewport: { pageX, pageY } } = await this._client.send('Page.getLayoutMetrics');
232
233 const clip = Object.assign({}, boundingBox);
234 clip.x += pageX;
235 clip.y += pageY;
236
237 const imageData = await this._page.screenshot(Object.assign({}, {
238 clip
239 }, options));
240
241 if (needsViewportReset)
242 await this._page.setViewport(viewport);
243
244 return imageData;
245 }
246
247 /**
248 * @param {string} selector
249 * @return {!Promise<?ElementHandle>}
250 */
251 async $(selector) {
252 const handle = await this.executionContext().evaluateHandle(
253 (element, selector) => element.querySelector(selector),
254 this, selector
255 );
256 const element = handle.asElement();
257 if (element)
258 return element;
259 await handle.dispose();
260 return null;
261 }
262
263 /**
264 * @param {string} selector
265 * @return {!Promise<!Array<!ElementHandle>>}
266 */
267 async $$(selector) {
268 const arrayHandle = await this.executionContext().evaluateHandle(
269 (element, selector) => element.querySelectorAll(selector),
270 this, selector
271 );
272 const properties = await arrayHandle.getProperties();
273 await arrayHandle.dispose();
274 const result = [];
275 for (const property of properties.values()) {
276 const elementHandle = property.asElement();
277 if (elementHandle)
278 result.push(elementHandle);
279 }
280 return result;
281 }
282
283 /**
284 * @param {string} selector
285 * @param {Function|String} pageFunction
286 * @param {!Array<*>} args
287 * @return {!Promise<(!Object|undefined)>}
288 */
289 async $eval(selector, pageFunction, ...args) {
290 const elementHandle = await this.$(selector);
291 if (!elementHandle)
292 throw new Error(`Error: failed to find element matching selector "${selector}"`);
293 const result = await this.executionContext().evaluate(pageFunction, elementHandle, ...args);
294 await elementHandle.dispose();
295 return result;
296 }
297
298 /**
299 * @param {string} selector
300 * @param {Function|String} pageFunction
301 * @param {!Array<*>} args
302 * @return {!Promise<(!Object|undefined)>}
303 */
304 async $$eval(selector, pageFunction, ...args) {
305 const arrayHandle = await this.executionContext().evaluateHandle(
306 (element, selector) => Array.from(element.querySelectorAll(selector)),
307 this, selector
308 );
309 if (!(await arrayHandle.jsonValue()).length)
310 throw new Error(`Error: failed to find elements matching selector "${selector}"`);
311
312 const result = await this.executionContext().evaluate(pageFunction, arrayHandle, ...args);
313 await arrayHandle.dispose();
314 return result;
315 }
316
317 /**
318 * @param {string} expression
319 * @return {!Promise<!Array<!ElementHandle>>}
320 */
321 async $x(expression) {
322 const arrayHandle = await this.executionContext().evaluateHandle(
323 (element, expression) => {
324 const document = element.ownerDocument || element;
325 const iterator = document.evaluate(expression, element, null, XPathResult.ORDERED_NODE_ITERATOR_TYPE);
326 const array = [];
327 let item;
328 while ((item = iterator.iterateNext()))
329 array.push(item);
330 return array;
331 },
332 this, expression
333 );
334 const properties = await arrayHandle.getProperties();
335 await arrayHandle.dispose();
336 const result = [];
337 for (const property of properties.values()) {
338 const elementHandle = property.asElement();
339 if (elementHandle)
340 result.push(elementHandle);
341 }
342 return result;
343 }
344}
345
346module.exports = ElementHandle;
347helper.tracePublicAPI(ElementHandle);