UNPKG

23.6 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, assert, 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() {return (fn => {
49 const gen = fn.call(this);
50 return new Promise((resolve, reject) => {
51 function step(key, arg) {
52 let info, value;
53 try {
54 info = gen[key](arg);
55 value = info.value;
56 } catch (error) {
57 reject(error);
58 return;
59 }
60 if (info.done) {
61 resolve(value);
62 } else {
63 return Promise.resolve(value).then(
64 value => {
65 step('next', value);
66 },
67 err => {
68 step('throw', err);
69 });
70 }
71 }
72 return step('next');
73 });
74})(function*(){
75 const nodeInfo = (yield this._client.send('DOM.describeNode', {
76 objectId: this._remoteObject.objectId
77 }));
78 if (typeof nodeInfo.node.frameId !== 'string')
79 return null;
80 return this._frameManager.frame(nodeInfo.node.frameId);
81 });}
82
83 /* async */ _scrollIntoViewIfNeeded() {return (fn => {
84 const gen = fn.call(this);
85 return new Promise((resolve, reject) => {
86 function step(key, arg) {
87 let info, value;
88 try {
89 info = gen[key](arg);
90 value = info.value;
91 } catch (error) {
92 reject(error);
93 return;
94 }
95 if (info.done) {
96 resolve(value);
97 } else {
98 return Promise.resolve(value).then(
99 value => {
100 step('next', value);
101 },
102 err => {
103 step('throw', err);
104 });
105 }
106 }
107 return step('next');
108 });
109})(function*(){
110 const error = (yield this.executionContext().evaluate(/* async */(element, pageJavascriptEnabled) => {return (fn => {
111 const gen = fn.call(this);
112 return new Promise((resolve, reject) => {
113 function step(key, arg) {
114 let info, value;
115 try {
116 info = gen[key](arg);
117 value = info.value;
118 } catch (error) {
119 reject(error);
120 return;
121 }
122 if (info.done) {
123 resolve(value);
124 } else {
125 return Promise.resolve(value).then(
126 value => {
127 step('next', value);
128 },
129 err => {
130 step('throw', err);
131 });
132 }
133 }
134 return step('next');
135 });
136})(function*(){
137 if (!element.isConnected)
138 return 'Node is detached from document';
139 if (element.nodeType !== Node.ELEMENT_NODE)
140 return 'Node is not of type HTMLElement';
141 // force-scroll if page's javascript is disabled.
142 if (!pageJavascriptEnabled) {
143 element.scrollIntoView({block: 'center', inline: 'center', behavior: 'instant'});
144 return false;
145 }
146 const visibleRatio = (yield new Promise(resolve => {
147 const observer = new IntersectionObserver(entries => {
148 resolve(entries[0].intersectionRatio);
149 observer.disconnect();
150 });
151 observer.observe(element);
152 }));
153 if (visibleRatio !== 1.0)
154 element.scrollIntoView({block: 'center', inline: 'center', behavior: 'instant'});
155 return false;
156 });}, this, this._page._javascriptEnabled));
157 if (error)
158 throw new Error(error);
159 });}
160
161 /**
162 * @return {!Promise<!{x: number, y: number}>}
163 */
164 /* async */ _clickablePoint() {return (fn => {
165 const gen = fn.call(this);
166 return new Promise((resolve, reject) => {
167 function step(key, arg) {
168 let info, value;
169 try {
170 info = gen[key](arg);
171 value = info.value;
172 } catch (error) {
173 reject(error);
174 return;
175 }
176 if (info.done) {
177 resolve(value);
178 } else {
179 return Promise.resolve(value).then(
180 value => {
181 step('next', value);
182 },
183 err => {
184 step('throw', err);
185 });
186 }
187 }
188 return step('next');
189 });
190})(function*(){
191 const result = (yield this._client.send('DOM.getContentQuads', {
192 objectId: this._remoteObject.objectId
193 }).catch(debugError));
194 if (!result || !result.quads.length)
195 throw new Error('Node is either not visible or not an HTMLElement');
196 // Filter out quads that have too small area to click into.
197 const quads = result.quads.map(quad => this._fromProtocolQuad(quad)).filter(quad => computeQuadArea(quad) > 1);
198 if (!quads.length)
199 throw new Error('Node is either not visible or not an HTMLElement');
200 // Return the middle point of the first quad.
201 const quad = quads[0];
202 let x = 0;
203 let y = 0;
204 for (const point of quad) {
205 x += point.x;
206 y += point.y;
207 }
208 return {
209 x: x / 4,
210 y: y / 4
211 };
212 });}
213
214 /**
215 * @return {!Promise<void|Protocol.DOM.getBoxModelReturnValue>}
216 */
217 _getBoxModel() {
218 return this._client.send('DOM.getBoxModel', {
219 objectId: this._remoteObject.objectId
220 }).catch(error => debugError(error));
221 }
222
223 /**
224 * @param {!Array<number>} quad
225 * @return {!Array<object>}
226 */
227 _fromProtocolQuad(quad) {
228 return [
229 {x: quad[0], y: quad[1]},
230 {x: quad[2], y: quad[3]},
231 {x: quad[4], y: quad[5]},
232 {x: quad[6], y: quad[7]}
233 ];
234 }
235
236 /* async */ hover() {return (fn => {
237 const gen = fn.call(this);
238 return new Promise((resolve, reject) => {
239 function step(key, arg) {
240 let info, value;
241 try {
242 info = gen[key](arg);
243 value = info.value;
244 } catch (error) {
245 reject(error);
246 return;
247 }
248 if (info.done) {
249 resolve(value);
250 } else {
251 return Promise.resolve(value).then(
252 value => {
253 step('next', value);
254 },
255 err => {
256 step('throw', err);
257 });
258 }
259 }
260 return step('next');
261 });
262})(function*(){
263 (yield this._scrollIntoViewIfNeeded());
264 const {x, y} = (yield this._clickablePoint());
265 (yield this._page.mouse.move(x, y));
266 });}
267
268 /**
269 * @param {!Object=} options
270 */
271 /* async */ click(options = {}) {return (fn => {
272 const gen = fn.call(this);
273 return new Promise((resolve, reject) => {
274 function step(key, arg) {
275 let info, value;
276 try {
277 info = gen[key](arg);
278 value = info.value;
279 } catch (error) {
280 reject(error);
281 return;
282 }
283 if (info.done) {
284 resolve(value);
285 } else {
286 return Promise.resolve(value).then(
287 value => {
288 step('next', value);
289 },
290 err => {
291 step('throw', err);
292 });
293 }
294 }
295 return step('next');
296 });
297})(function*(){
298 (yield this._scrollIntoViewIfNeeded());
299 const {x, y} = (yield this._clickablePoint());
300 (yield this._page.mouse.click(x, y, options));
301 });}
302
303 /**
304 * @param {!Array<string>} filePaths
305 * @return {!Promise}
306 */
307 /* async */ uploadFile(...filePaths) {return (fn => {
308 const gen = fn.call(this);
309 return new Promise((resolve, reject) => {
310 function step(key, arg) {
311 let info, value;
312 try {
313 info = gen[key](arg);
314 value = info.value;
315 } catch (error) {
316 reject(error);
317 return;
318 }
319 if (info.done) {
320 resolve(value);
321 } else {
322 return Promise.resolve(value).then(
323 value => {
324 step('next', value);
325 },
326 err => {
327 step('throw', err);
328 });
329 }
330 }
331 return step('next');
332 });
333})(function*(){
334 const files = filePaths.map(filePath => path.resolve(filePath));
335 const objectId = this._remoteObject.objectId;
336 return this._client.send('DOM.setFileInputFiles', { objectId, files });
337 });}
338
339 /* async */ tap() {return (fn => {
340 const gen = fn.call(this);
341 return new Promise((resolve, reject) => {
342 function step(key, arg) {
343 let info, value;
344 try {
345 info = gen[key](arg);
346 value = info.value;
347 } catch (error) {
348 reject(error);
349 return;
350 }
351 if (info.done) {
352 resolve(value);
353 } else {
354 return Promise.resolve(value).then(
355 value => {
356 step('next', value);
357 },
358 err => {
359 step('throw', err);
360 });
361 }
362 }
363 return step('next');
364 });
365})(function*(){
366 (yield this._scrollIntoViewIfNeeded());
367 const {x, y} = (yield this._clickablePoint());
368 (yield this._page.touchscreen.tap(x, y));
369 });}
370
371 /* async */ focus() {return (fn => {
372 const gen = fn.call(this);
373 return new Promise((resolve, reject) => {
374 function step(key, arg) {
375 let info, value;
376 try {
377 info = gen[key](arg);
378 value = info.value;
379 } catch (error) {
380 reject(error);
381 return;
382 }
383 if (info.done) {
384 resolve(value);
385 } else {
386 return Promise.resolve(value).then(
387 value => {
388 step('next', value);
389 },
390 err => {
391 step('throw', err);
392 });
393 }
394 }
395 return step('next');
396 });
397})(function*(){
398 (yield this.executionContext().evaluate(element => element.focus(), this));
399 });}
400
401 /**
402 * @param {string} text
403 * @param {{delay: (number|undefined)}=} options
404 */
405 /* async */ type(text, options) {return (fn => {
406 const gen = fn.call(this);
407 return new Promise((resolve, reject) => {
408 function step(key, arg) {
409 let info, value;
410 try {
411 info = gen[key](arg);
412 value = info.value;
413 } catch (error) {
414 reject(error);
415 return;
416 }
417 if (info.done) {
418 resolve(value);
419 } else {
420 return Promise.resolve(value).then(
421 value => {
422 step('next', value);
423 },
424 err => {
425 step('throw', err);
426 });
427 }
428 }
429 return step('next');
430 });
431})(function*(){
432 (yield this.focus());
433 (yield this._page.keyboard.type(text, options));
434 });}
435
436 /**
437 * @param {string} key
438 * @param {!Object=} options
439 */
440 /* async */ press(key, options) {return (fn => {
441 const gen = fn.call(this);
442 return new Promise((resolve, reject) => {
443 function step(key, arg) {
444 let info, value;
445 try {
446 info = gen[key](arg);
447 value = info.value;
448 } catch (error) {
449 reject(error);
450 return;
451 }
452 if (info.done) {
453 resolve(value);
454 } else {
455 return Promise.resolve(value).then(
456 value => {
457 step('next', value);
458 },
459 err => {
460 step('throw', err);
461 });
462 }
463 }
464 return step('next');
465 });
466})(function*(){
467 (yield this.focus());
468 (yield this._page.keyboard.press(key, options));
469 });}
470
471 /**
472 * @return {!Promise<?{x: number, y: number, width: number, height: number}>}
473 */
474 /* async */ boundingBox() {return (fn => {
475 const gen = fn.call(this);
476 return new Promise((resolve, reject) => {
477 function step(key, arg) {
478 let info, value;
479 try {
480 info = gen[key](arg);
481 value = info.value;
482 } catch (error) {
483 reject(error);
484 return;
485 }
486 if (info.done) {
487 resolve(value);
488 } else {
489 return Promise.resolve(value).then(
490 value => {
491 step('next', value);
492 },
493 err => {
494 step('throw', err);
495 });
496 }
497 }
498 return step('next');
499 });
500})(function*(){
501 const result = (yield this._getBoxModel());
502
503 if (!result)
504 return null;
505
506 const quad = result.model.border;
507 const x = Math.min(quad[0], quad[2], quad[4], quad[6]);
508 const y = Math.min(quad[1], quad[3], quad[5], quad[7]);
509 const width = Math.max(quad[0], quad[2], quad[4], quad[6]) - x;
510 const height = Math.max(quad[1], quad[3], quad[5], quad[7]) - y;
511
512 return {x, y, width, height};
513 });}
514
515 /**
516 * @return {!Promise<?object>}
517 */
518 /* async */ boxModel() {return (fn => {
519 const gen = fn.call(this);
520 return new Promise((resolve, reject) => {
521 function step(key, arg) {
522 let info, value;
523 try {
524 info = gen[key](arg);
525 value = info.value;
526 } catch (error) {
527 reject(error);
528 return;
529 }
530 if (info.done) {
531 resolve(value);
532 } else {
533 return Promise.resolve(value).then(
534 value => {
535 step('next', value);
536 },
537 err => {
538 step('throw', err);
539 });
540 }
541 }
542 return step('next');
543 });
544})(function*(){
545 const result = (yield this._getBoxModel());
546
547 if (!result)
548 return null;
549
550 const {content, padding, border, margin, width, height} = result.model;
551 return {
552 content: this._fromProtocolQuad(content),
553 padding: this._fromProtocolQuad(padding),
554 border: this._fromProtocolQuad(border),
555 margin: this._fromProtocolQuad(margin),
556 width,
557 height
558 };
559 });}
560
561 /**
562 *
563 * @param {!Object=} options
564 * @returns {!Promise<Object>}
565 */
566 /* async */ screenshot(options = {}) {return (fn => {
567 const gen = fn.call(this);
568 return new Promise((resolve, reject) => {
569 function step(key, arg) {
570 let info, value;
571 try {
572 info = gen[key](arg);
573 value = info.value;
574 } catch (error) {
575 reject(error);
576 return;
577 }
578 if (info.done) {
579 resolve(value);
580 } else {
581 return Promise.resolve(value).then(
582 value => {
583 step('next', value);
584 },
585 err => {
586 step('throw', err);
587 });
588 }
589 }
590 return step('next');
591 });
592})(function*(){
593 let needsViewportReset = false;
594
595 let boundingBox = (yield this.boundingBox());
596 assert(boundingBox, 'Node is either not visible or not an HTMLElement');
597
598 const viewport = this._page.viewport();
599
600 if (boundingBox.width > viewport.width || boundingBox.height > viewport.height) {
601 const newViewport = {
602 width: Math.max(viewport.width, Math.ceil(boundingBox.width)),
603 height: Math.max(viewport.height, Math.ceil(boundingBox.height)),
604 };
605 (yield this._page.setViewport(Object.assign({}, viewport, newViewport)));
606
607 needsViewportReset = true;
608 }
609
610 (yield this._scrollIntoViewIfNeeded());
611
612 boundingBox = (yield this.boundingBox());
613 assert(boundingBox, 'Node is either not visible or not an HTMLElement');
614
615 const { layoutViewport: { pageX, pageY } } = (yield this._client.send('Page.getLayoutMetrics'));
616
617 const clip = Object.assign({}, boundingBox);
618 clip.x += pageX;
619 clip.y += pageY;
620
621 const imageData = (yield this._page.screenshot(Object.assign({}, {
622 clip
623 }, options)));
624
625 if (needsViewportReset)
626 (yield this._page.setViewport(viewport));
627
628 return imageData;
629 });}
630
631 /**
632 * @param {string} selector
633 * @return {!Promise<?ElementHandle>}
634 */
635 /* async */ $(selector) {return (fn => {
636 const gen = fn.call(this);
637 return new Promise((resolve, reject) => {
638 function step(key, arg) {
639 let info, value;
640 try {
641 info = gen[key](arg);
642 value = info.value;
643 } catch (error) {
644 reject(error);
645 return;
646 }
647 if (info.done) {
648 resolve(value);
649 } else {
650 return Promise.resolve(value).then(
651 value => {
652 step('next', value);
653 },
654 err => {
655 step('throw', err);
656 });
657 }
658 }
659 return step('next');
660 });
661})(function*(){
662 const handle = (yield this.executionContext().evaluateHandle(
663 (element, selector) => element.querySelector(selector),
664 this, selector
665 ));
666 const element = handle.asElement();
667 if (element)
668 return element;
669 (yield handle.dispose());
670 return null;
671 });}
672
673 /**
674 * @param {string} selector
675 * @return {!Promise<!Array<!ElementHandle>>}
676 */
677 /* async */ $$(selector) {return (fn => {
678 const gen = fn.call(this);
679 return new Promise((resolve, reject) => {
680 function step(key, arg) {
681 let info, value;
682 try {
683 info = gen[key](arg);
684 value = info.value;
685 } catch (error) {
686 reject(error);
687 return;
688 }
689 if (info.done) {
690 resolve(value);
691 } else {
692 return Promise.resolve(value).then(
693 value => {
694 step('next', value);
695 },
696 err => {
697 step('throw', err);
698 });
699 }
700 }
701 return step('next');
702 });
703})(function*(){
704 const arrayHandle = (yield this.executionContext().evaluateHandle(
705 (element, selector) => element.querySelectorAll(selector),
706 this, selector
707 ));
708 const properties = (yield arrayHandle.getProperties());
709 (yield arrayHandle.dispose());
710 const result = [];
711 for (const property of properties.values()) {
712 const elementHandle = property.asElement();
713 if (elementHandle)
714 result.push(elementHandle);
715 }
716 return result;
717 });}
718
719 /**
720 * @param {string} selector
721 * @param {Function|String} pageFunction
722 * @param {!Array<*>} args
723 * @return {!Promise<(!Object|undefined)>}
724 */
725 /* async */ $eval(selector, pageFunction, ...args) {return (fn => {
726 const gen = fn.call(this);
727 return new Promise((resolve, reject) => {
728 function step(key, arg) {
729 let info, value;
730 try {
731 info = gen[key](arg);
732 value = info.value;
733 } catch (error) {
734 reject(error);
735 return;
736 }
737 if (info.done) {
738 resolve(value);
739 } else {
740 return Promise.resolve(value).then(
741 value => {
742 step('next', value);
743 },
744 err => {
745 step('throw', err);
746 });
747 }
748 }
749 return step('next');
750 });
751})(function*(){
752 const elementHandle = (yield this.$(selector));
753 if (!elementHandle)
754 throw new Error(`Error: failed to find element matching selector "${selector}"`);
755 const result = (yield this.executionContext().evaluate(pageFunction, elementHandle, ...args));
756 (yield elementHandle.dispose());
757 return result;
758 });}
759
760 /**
761 * @param {string} selector
762 * @param {Function|String} pageFunction
763 * @param {!Array<*>} args
764 * @return {!Promise<(!Object|undefined)>}
765 */
766 /* async */ $$eval(selector, pageFunction, ...args) {return (fn => {
767 const gen = fn.call(this);
768 return new Promise((resolve, reject) => {
769 function step(key, arg) {
770 let info, value;
771 try {
772 info = gen[key](arg);
773 value = info.value;
774 } catch (error) {
775 reject(error);
776 return;
777 }
778 if (info.done) {
779 resolve(value);
780 } else {
781 return Promise.resolve(value).then(
782 value => {
783 step('next', value);
784 },
785 err => {
786 step('throw', err);
787 });
788 }
789 }
790 return step('next');
791 });
792})(function*(){
793 const arrayHandle = (yield this.executionContext().evaluateHandle(
794 (element, selector) => Array.from(element.querySelectorAll(selector)),
795 this, selector
796 ));
797
798 const result = (yield this.executionContext().evaluate(pageFunction, arrayHandle, ...args));
799 (yield arrayHandle.dispose());
800 return result;
801 });}
802
803 /**
804 * @param {string} expression
805 * @return {!Promise<!Array<!ElementHandle>>}
806 */
807 /* async */ $x(expression) {return (fn => {
808 const gen = fn.call(this);
809 return new Promise((resolve, reject) => {
810 function step(key, arg) {
811 let info, value;
812 try {
813 info = gen[key](arg);
814 value = info.value;
815 } catch (error) {
816 reject(error);
817 return;
818 }
819 if (info.done) {
820 resolve(value);
821 } else {
822 return Promise.resolve(value).then(
823 value => {
824 step('next', value);
825 },
826 err => {
827 step('throw', err);
828 });
829 }
830 }
831 return step('next');
832 });
833})(function*(){
834 const arrayHandle = (yield this.executionContext().evaluateHandle(
835 (element, expression) => {
836 const document = element.ownerDocument || element;
837 const iterator = document.evaluate(expression, element, null, XPathResult.ORDERED_NODE_ITERATOR_TYPE);
838 const array = [];
839 let item;
840 while ((item = iterator.iterateNext()))
841 array.push(item);
842 return array;
843 },
844 this, expression
845 ));
846 const properties = (yield arrayHandle.getProperties());
847 (yield arrayHandle.dispose());
848 const result = [];
849 for (const property of properties.values()) {
850 const elementHandle = property.asElement();
851 if (elementHandle)
852 result.push(elementHandle);
853 }
854 return result;
855 });}
856
857 /**
858 * @returns {!Promise<boolean>}
859 */
860 isIntersectingViewport() {
861 return this.executionContext().evaluate(/* async */ element => {return (fn => {
862 const gen = fn.call(this);
863 return new Promise((resolve, reject) => {
864 function step(key, arg) {
865 let info, value;
866 try {
867 info = gen[key](arg);
868 value = info.value;
869 } catch (error) {
870 reject(error);
871 return;
872 }
873 if (info.done) {
874 resolve(value);
875 } else {
876 return Promise.resolve(value).then(
877 value => {
878 step('next', value);
879 },
880 err => {
881 step('throw', err);
882 });
883 }
884 }
885 return step('next');
886 });
887})(function*(){
888 const visibleRatio = (yield new Promise(resolve => {
889 const observer = new IntersectionObserver(entries => {
890 resolve(entries[0].intersectionRatio);
891 observer.disconnect();
892 });
893 observer.observe(element);
894 }));
895 return visibleRatio > 0;
896 });}, this);
897 }
898}
899
900function computeQuadArea(quad) {
901 // Compute sum of all directed areas of adjacent triangles
902 // https://en.wikipedia.org/wiki/Polygon#Simple_polygons
903 let area = 0;
904 for (let i = 0; i < quad.length; ++i) {
905 const p1 = quad[i];
906 const p2 = quad[(i + 1) % quad.length];
907 area += (p1.x * p2.y - p2.x * p1.y) / 2;
908 }
909 return area;
910}
911
912module.exports = {ElementHandle};
913helper.tracePublicAPI(ElementHandle);