1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 |
|
9 |
|
10 |
|
11 |
|
12 |
|
13 |
|
14 |
|
15 |
|
16 |
|
17 | const {helper, assert, debugError} = require('./helper');
|
18 | const path = require('path');
|
19 |
|
20 | const EVALUATION_SCRIPT_URL = '__puppeteer_evaluation_script__';
|
21 | const SOURCE_URL_REGEX = /^[\040\t]*\/\/[@#] sourceURL=\s*(\S*?)\s*$/m;
|
22 |
|
23 | function createJSHandle(context, remoteObject) {
|
24 | const frame = context.frame();
|
25 | if (remoteObject.subtype === 'node' && frame) {
|
26 | const frameManager = frame._frameManager;
|
27 | return new ElementHandle(context, context._client, remoteObject, frameManager.page(), frameManager);
|
28 | }
|
29 | return new JSHandle(context, context._client, remoteObject);
|
30 | }
|
31 |
|
32 | class ExecutionContext {
|
33 | |
34 |
|
35 |
|
36 |
|
37 |
|
38 | constructor(client, contextPayload, frame) {
|
39 | this._client = client;
|
40 | this._frame = frame;
|
41 | this._contextId = contextPayload.id;
|
42 | this._isDefault = contextPayload.auxData ? !!contextPayload.auxData['isDefault'] : false;
|
43 | }
|
44 |
|
45 | |
46 |
|
47 |
|
48 | frame() {
|
49 | return this._frame;
|
50 | }
|
51 |
|
52 | |
53 |
|
54 |
|
55 |
|
56 |
|
57 | async evaluate(pageFunction, ...args) {
|
58 | const handle = await this.evaluateHandle(pageFunction, ...args);
|
59 | const result = await handle.jsonValue().catch(error => {
|
60 | if (error.message.includes('Object reference chain is too long'))
|
61 | return;
|
62 | if (error.message.includes('Object couldn\'t be returned by value'))
|
63 | return;
|
64 | throw error;
|
65 | });
|
66 | await handle.dispose();
|
67 | return result;
|
68 | }
|
69 |
|
70 | |
71 |
|
72 |
|
73 |
|
74 |
|
75 | async evaluateHandle(pageFunction, ...args) {
|
76 | const suffix = `//# sourceURL=${EVALUATION_SCRIPT_URL}`;
|
77 |
|
78 | if (helper.isString(pageFunction)) {
|
79 | const contextId = this._contextId;
|
80 | const expression = (pageFunction);
|
81 | const expressionWithSourceUrl = SOURCE_URL_REGEX.test(expression) ? expression : expression + '\n' + suffix;
|
82 | const {exceptionDetails, result: remoteObject} = await this._client.send('Runtime.evaluate', {
|
83 | expression: expressionWithSourceUrl,
|
84 | contextId,
|
85 | returnByValue: false,
|
86 | awaitPromise: true,
|
87 | userGesture: true
|
88 | }).catch(rewriteError);
|
89 | if (exceptionDetails)
|
90 | throw new Error('Evaluation failed: ' + helper.getExceptionMessage(exceptionDetails));
|
91 | return createJSHandle(this, remoteObject);
|
92 | }
|
93 |
|
94 | if (typeof pageFunction !== 'function')
|
95 | throw new Error('The following is not a function: ' + pageFunction);
|
96 |
|
97 | const { exceptionDetails, result: remoteObject } = await this._client.send('Runtime.callFunctionOn', {
|
98 | functionDeclaration: pageFunction.toString() + '\n' + suffix + '\n',
|
99 | executionContextId: this._contextId,
|
100 | arguments: args.map(convertArgument.bind(this)),
|
101 | returnByValue: false,
|
102 | awaitPromise: true,
|
103 | userGesture: true
|
104 | }).catch(rewriteError);
|
105 | if (exceptionDetails)
|
106 | throw new Error('Evaluation failed: ' + helper.getExceptionMessage(exceptionDetails));
|
107 | return createJSHandle(this, remoteObject);
|
108 |
|
109 | |
110 |
|
111 |
|
112 |
|
113 |
|
114 | function convertArgument(arg) {
|
115 | if (Object.is(arg, -0))
|
116 | return { unserializableValue: '-0' };
|
117 | if (Object.is(arg, Infinity))
|
118 | return { unserializableValue: 'Infinity' };
|
119 | if (Object.is(arg, -Infinity))
|
120 | return { unserializableValue: '-Infinity' };
|
121 | if (Object.is(arg, NaN))
|
122 | return { unserializableValue: 'NaN' };
|
123 | const objectHandle = arg && (arg instanceof JSHandle) ? arg : null;
|
124 | if (objectHandle) {
|
125 | if (objectHandle._context !== this)
|
126 | throw new Error('JSHandles can be evaluated only in the context they were created!');
|
127 | if (objectHandle._disposed)
|
128 | throw new Error('JSHandle is disposed!');
|
129 | if (objectHandle._remoteObject.unserializableValue)
|
130 | return { unserializableValue: objectHandle._remoteObject.unserializableValue };
|
131 | if (!objectHandle._remoteObject.objectId)
|
132 | return { value: objectHandle._remoteObject.value };
|
133 | return { objectId: objectHandle._remoteObject.objectId };
|
134 | }
|
135 | return { value: arg };
|
136 | }
|
137 |
|
138 | |
139 |
|
140 |
|
141 |
|
142 | function rewriteError(error) {
|
143 | if (error.message.endsWith('Cannot find context with specified id'))
|
144 | throw new Error('Execution context was destroyed, most likely because of a navigation.');
|
145 | throw error;
|
146 | }
|
147 | }
|
148 |
|
149 | |
150 |
|
151 |
|
152 |
|
153 | async queryObjects(prototypeHandle) {
|
154 | assert(!prototypeHandle._disposed, 'Prototype JSHandle is disposed!');
|
155 | assert(prototypeHandle._remoteObject.objectId, 'Prototype JSHandle must not be referencing primitive value');
|
156 | const response = await this._client.send('Runtime.queryObjects', {
|
157 | prototypeObjectId: prototypeHandle._remoteObject.objectId
|
158 | });
|
159 | return createJSHandle(this, response.objects);
|
160 | }
|
161 | }
|
162 |
|
163 | class JSHandle {
|
164 | |
165 |
|
166 |
|
167 |
|
168 |
|
169 | constructor(context, client, remoteObject) {
|
170 | this._context = context;
|
171 | this._client = client;
|
172 | this._remoteObject = remoteObject;
|
173 | this._disposed = false;
|
174 | }
|
175 |
|
176 | |
177 |
|
178 |
|
179 | executionContext() {
|
180 | return this._context;
|
181 | }
|
182 |
|
183 | |
184 |
|
185 |
|
186 |
|
187 | async getProperty(propertyName) {
|
188 | const objectHandle = await this._context.evaluateHandle((object, propertyName) => {
|
189 | const result = {__proto__: null};
|
190 | result[propertyName] = object[propertyName];
|
191 | return result;
|
192 | }, this, propertyName);
|
193 | const properties = await objectHandle.getProperties();
|
194 | const result = properties.get(propertyName) || null;
|
195 | await objectHandle.dispose();
|
196 | return result;
|
197 | }
|
198 |
|
199 | |
200 |
|
201 |
|
202 | async getProperties() {
|
203 | const response = await this._client.send('Runtime.getProperties', {
|
204 | objectId: this._remoteObject.objectId,
|
205 | ownProperties: true
|
206 | });
|
207 | const result = new Map();
|
208 | for (const property of response.result) {
|
209 | if (!property.enumerable)
|
210 | continue;
|
211 | result.set(property.name, createJSHandle(this._context, property.value));
|
212 | }
|
213 | return result;
|
214 | }
|
215 |
|
216 | |
217 |
|
218 |
|
219 | async jsonValue() {
|
220 | if (this._remoteObject.objectId) {
|
221 | const response = await this._client.send('Runtime.callFunctionOn', {
|
222 | functionDeclaration: 'function() { return this; }',
|
223 | objectId: this._remoteObject.objectId,
|
224 | returnByValue: true,
|
225 | awaitPromise: true,
|
226 | });
|
227 | return helper.valueFromRemoteObject(response.result);
|
228 | }
|
229 | return helper.valueFromRemoteObject(this._remoteObject);
|
230 | }
|
231 |
|
232 | |
233 |
|
234 |
|
235 | asElement() {
|
236 | return null;
|
237 | }
|
238 |
|
239 | async dispose() {
|
240 | if (this._disposed)
|
241 | return;
|
242 | this._disposed = true;
|
243 | await helper.releaseObject(this._client, this._remoteObject);
|
244 | }
|
245 |
|
246 | |
247 |
|
248 |
|
249 |
|
250 | toString() {
|
251 | if (this._remoteObject.objectId) {
|
252 | const type = this._remoteObject.subtype || this._remoteObject.type;
|
253 | return 'JSHandle@' + type;
|
254 | }
|
255 | return 'JSHandle:' + helper.valueFromRemoteObject(this._remoteObject);
|
256 | }
|
257 | }
|
258 |
|
259 |
|
260 | class ElementHandle extends JSHandle {
|
261 | |
262 |
|
263 |
|
264 |
|
265 |
|
266 |
|
267 |
|
268 | constructor(context, client, remoteObject, page, frameManager) {
|
269 | super(context, client, remoteObject);
|
270 | this._client = client;
|
271 | this._remoteObject = remoteObject;
|
272 | this._page = page;
|
273 | this._frameManager = frameManager;
|
274 | this._disposed = false;
|
275 | }
|
276 |
|
277 | |
278 |
|
279 |
|
280 |
|
281 | asElement() {
|
282 | return this;
|
283 | }
|
284 |
|
285 | |
286 |
|
287 |
|
288 | async contentFrame() {
|
289 | const nodeInfo = await this._client.send('DOM.describeNode', {
|
290 | objectId: this._remoteObject.objectId
|
291 | });
|
292 | if (typeof nodeInfo.node.frameId !== 'string')
|
293 | return null;
|
294 | return this._frameManager.frame(nodeInfo.node.frameId);
|
295 | }
|
296 |
|
297 | async _scrollIntoViewIfNeeded() {
|
298 | const error = await this.executionContext().evaluate(async(element, pageJavascriptEnabled) => {
|
299 | if (!element.isConnected)
|
300 | return 'Node is detached from document';
|
301 | if (element.nodeType !== Node.ELEMENT_NODE)
|
302 | return 'Node is not of type HTMLElement';
|
303 |
|
304 | if (!pageJavascriptEnabled) {
|
305 | element.scrollIntoView({block: 'center', inline: 'center', behavior: 'instant'});
|
306 | return false;
|
307 | }
|
308 | const visibleRatio = await new Promise(resolve => {
|
309 | const observer = new IntersectionObserver(entries => {
|
310 | resolve(entries[0].intersectionRatio);
|
311 | observer.disconnect();
|
312 | });
|
313 | observer.observe(element);
|
314 | });
|
315 | if (visibleRatio !== 1.0)
|
316 | element.scrollIntoView({block: 'center', inline: 'center', behavior: 'instant'});
|
317 | return false;
|
318 | }, this, this._page._javascriptEnabled);
|
319 | if (error)
|
320 | throw new Error(error);
|
321 | }
|
322 |
|
323 | |
324 |
|
325 |
|
326 | async _clickablePoint() {
|
327 | const result = await this._client.send('DOM.getContentQuads', {
|
328 | objectId: this._remoteObject.objectId
|
329 | }).catch(debugError);
|
330 | if (!result || !result.quads.length)
|
331 | throw new Error('Node is either not visible or not an HTMLElement');
|
332 |
|
333 | const quads = result.quads.map(quad => this._fromProtocolQuad(quad)).filter(quad => computeQuadArea(quad) > 1);
|
334 | if (!quads.length)
|
335 | throw new Error('Node is either not visible or not an HTMLElement');
|
336 |
|
337 | const quad = quads[0];
|
338 | let x = 0;
|
339 | let y = 0;
|
340 | for (const point of quad) {
|
341 | x += point.x;
|
342 | y += point.y;
|
343 | }
|
344 | return {
|
345 | x: x / 4,
|
346 | y: y / 4
|
347 | };
|
348 | }
|
349 |
|
350 | |
351 |
|
352 |
|
353 | _getBoxModel() {
|
354 | return this._client.send('DOM.getBoxModel', {
|
355 | objectId: this._remoteObject.objectId
|
356 | }).catch(error => debugError(error));
|
357 | }
|
358 |
|
359 | |
360 |
|
361 |
|
362 |
|
363 | _fromProtocolQuad(quad) {
|
364 | return [
|
365 | {x: quad[0], y: quad[1]},
|
366 | {x: quad[2], y: quad[3]},
|
367 | {x: quad[4], y: quad[5]},
|
368 | {x: quad[6], y: quad[7]}
|
369 | ];
|
370 | }
|
371 |
|
372 | async hover() {
|
373 | await this._scrollIntoViewIfNeeded();
|
374 | const {x, y} = await this._clickablePoint();
|
375 | await this._page.mouse.move(x, y);
|
376 | }
|
377 |
|
378 | |
379 |
|
380 |
|
381 | async click(options = {}) {
|
382 | await this._scrollIntoViewIfNeeded();
|
383 | const {x, y} = await this._clickablePoint();
|
384 | await this._page.mouse.click(x, y, options);
|
385 | }
|
386 |
|
387 | |
388 |
|
389 |
|
390 |
|
391 | async uploadFile(...filePaths) {
|
392 | const files = filePaths.map(filePath => path.resolve(filePath));
|
393 | const objectId = this._remoteObject.objectId;
|
394 | return this._client.send('DOM.setFileInputFiles', { objectId, files });
|
395 | }
|
396 |
|
397 | async tap() {
|
398 | await this._scrollIntoViewIfNeeded();
|
399 | const {x, y} = await this._clickablePoint();
|
400 | await this._page.touchscreen.tap(x, y);
|
401 | }
|
402 |
|
403 | async focus() {
|
404 | await this.executionContext().evaluate(element => element.focus(), this);
|
405 | }
|
406 |
|
407 | |
408 |
|
409 |
|
410 |
|
411 | async type(text, options) {
|
412 | await this.focus();
|
413 | await this._page.keyboard.type(text, options);
|
414 | }
|
415 |
|
416 | |
417 |
|
418 |
|
419 |
|
420 | async press(key, options) {
|
421 | await this.focus();
|
422 | await this._page.keyboard.press(key, options);
|
423 | }
|
424 |
|
425 | |
426 |
|
427 |
|
428 | async boundingBox() {
|
429 | const result = await this._getBoxModel();
|
430 |
|
431 | if (!result)
|
432 | return null;
|
433 |
|
434 | const quad = result.model.border;
|
435 | const x = Math.min(quad[0], quad[2], quad[4], quad[6]);
|
436 | const y = Math.min(quad[1], quad[3], quad[5], quad[7]);
|
437 | const width = Math.max(quad[0], quad[2], quad[4], quad[6]) - x;
|
438 | const height = Math.max(quad[1], quad[3], quad[5], quad[7]) - y;
|
439 |
|
440 | return {x, y, width, height};
|
441 | }
|
442 |
|
443 | |
444 |
|
445 |
|
446 | async boxModel() {
|
447 | const result = await this._getBoxModel();
|
448 |
|
449 | if (!result)
|
450 | return null;
|
451 |
|
452 | const {content, padding, border, margin, width, height} = result.model;
|
453 | return {
|
454 | content: this._fromProtocolQuad(content),
|
455 | padding: this._fromProtocolQuad(padding),
|
456 | border: this._fromProtocolQuad(border),
|
457 | margin: this._fromProtocolQuad(margin),
|
458 | width,
|
459 | height
|
460 | };
|
461 | }
|
462 |
|
463 | |
464 |
|
465 |
|
466 |
|
467 |
|
468 | async screenshot(options = {}) {
|
469 | let needsViewportReset = false;
|
470 |
|
471 | let boundingBox = await this.boundingBox();
|
472 | assert(boundingBox, 'Node is either not visible or not an HTMLElement');
|
473 |
|
474 | const viewport = this._page.viewport();
|
475 |
|
476 | if (viewport && (boundingBox.width > viewport.width || boundingBox.height > viewport.height)) {
|
477 | const newViewport = {
|
478 | width: Math.max(viewport.width, Math.ceil(boundingBox.width)),
|
479 | height: Math.max(viewport.height, Math.ceil(boundingBox.height)),
|
480 | };
|
481 | await this._page.setViewport(Object.assign({}, viewport, newViewport));
|
482 |
|
483 | needsViewportReset = true;
|
484 | }
|
485 |
|
486 | await this._scrollIntoViewIfNeeded();
|
487 |
|
488 | boundingBox = await this.boundingBox();
|
489 | assert(boundingBox, 'Node is either not visible or not an HTMLElement');
|
490 |
|
491 | const { layoutViewport: { pageX, pageY } } = await this._client.send('Page.getLayoutMetrics');
|
492 |
|
493 | const clip = Object.assign({}, boundingBox);
|
494 | clip.x += pageX;
|
495 | clip.y += pageY;
|
496 |
|
497 | const imageData = await this._page.screenshot(Object.assign({}, {
|
498 | clip
|
499 | }, options));
|
500 |
|
501 | if (needsViewportReset)
|
502 | await this._page.setViewport(viewport);
|
503 |
|
504 | return imageData;
|
505 | }
|
506 |
|
507 | |
508 |
|
509 |
|
510 |
|
511 | async $(selector) {
|
512 | const handle = await this.executionContext().evaluateHandle(
|
513 | (element, selector) => element.querySelector(selector),
|
514 | this, selector
|
515 | );
|
516 | const element = handle.asElement();
|
517 | if (element)
|
518 | return element;
|
519 | await handle.dispose();
|
520 | return null;
|
521 | }
|
522 |
|
523 | |
524 |
|
525 |
|
526 |
|
527 | async $$(selector) {
|
528 | const arrayHandle = await this.executionContext().evaluateHandle(
|
529 | (element, selector) => element.querySelectorAll(selector),
|
530 | this, selector
|
531 | );
|
532 | const properties = await arrayHandle.getProperties();
|
533 | await arrayHandle.dispose();
|
534 | const result = [];
|
535 | for (const property of properties.values()) {
|
536 | const elementHandle = property.asElement();
|
537 | if (elementHandle)
|
538 | result.push(elementHandle);
|
539 | }
|
540 | return result;
|
541 | }
|
542 |
|
543 | |
544 |
|
545 |
|
546 |
|
547 |
|
548 |
|
549 | async $eval(selector, pageFunction, ...args) {
|
550 | const elementHandle = await this.$(selector);
|
551 | if (!elementHandle)
|
552 | throw new Error(`Error: failed to find element matching selector "${selector}"`);
|
553 | const result = await this.executionContext().evaluate(pageFunction, elementHandle, ...args);
|
554 | await elementHandle.dispose();
|
555 | return result;
|
556 | }
|
557 |
|
558 | |
559 |
|
560 |
|
561 |
|
562 |
|
563 |
|
564 | async $$eval(selector, pageFunction, ...args) {
|
565 | const arrayHandle = await this.executionContext().evaluateHandle(
|
566 | (element, selector) => Array.from(element.querySelectorAll(selector)),
|
567 | this, selector
|
568 | );
|
569 |
|
570 | const result = await this.executionContext().evaluate(pageFunction, arrayHandle, ...args);
|
571 | await arrayHandle.dispose();
|
572 | return result;
|
573 | }
|
574 |
|
575 | |
576 |
|
577 |
|
578 |
|
579 | async $x(expression) {
|
580 | const arrayHandle = await this.executionContext().evaluateHandle(
|
581 | (element, expression) => {
|
582 | const document = element.ownerDocument || element;
|
583 | const iterator = document.evaluate(expression, element, null, XPathResult.ORDERED_NODE_ITERATOR_TYPE);
|
584 | const array = [];
|
585 | let item;
|
586 | while ((item = iterator.iterateNext()))
|
587 | array.push(item);
|
588 | return array;
|
589 | },
|
590 | this, expression
|
591 | );
|
592 | const properties = await arrayHandle.getProperties();
|
593 | await arrayHandle.dispose();
|
594 | const result = [];
|
595 | for (const property of properties.values()) {
|
596 | const elementHandle = property.asElement();
|
597 | if (elementHandle)
|
598 | result.push(elementHandle);
|
599 | }
|
600 | return result;
|
601 | }
|
602 |
|
603 | |
604 |
|
605 |
|
606 | isIntersectingViewport() {
|
607 | return this.executionContext().evaluate(async element => {
|
608 | const visibleRatio = await new Promise(resolve => {
|
609 | const observer = new IntersectionObserver(entries => {
|
610 | resolve(entries[0].intersectionRatio);
|
611 | observer.disconnect();
|
612 | });
|
613 | observer.observe(element);
|
614 | });
|
615 | return visibleRatio > 0;
|
616 | }, this);
|
617 | }
|
618 | }
|
619 |
|
620 | function computeQuadArea(quad) {
|
621 |
|
622 |
|
623 | let area = 0;
|
624 | for (let i = 0; i < quad.length; ++i) {
|
625 | const p1 = quad[i];
|
626 | const p2 = quad[(i + 1) % quad.length];
|
627 | area += (p1.x * p2.y - p2.x * p1.y) / 2;
|
628 | }
|
629 | return area;
|
630 | }
|
631 |
|
632 | helper.tracePublicAPI(ElementHandle);
|
633 | helper.tracePublicAPI(JSHandle);
|
634 | helper.tracePublicAPI(ExecutionContext);
|
635 |
|
636 | module.exports = {ExecutionContext, JSHandle, ElementHandle, createJSHandle, EVALUATION_SCRIPT_URL};
|