UNPKG

43.7 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 */
16
17const fs = require('fs');
18const EventEmitter = require('events');
19const {helper, assert} = require('./helper');
20const {ExecutionContext, JSHandle} = require('./ExecutionContext');
21const {ElementHandle} = require('./ElementHandle');
22const {TimeoutError} = require('./Errors');
23
24const readFileAsync = helper.promisify(fs.readFile);
25
26class FrameManager extends EventEmitter {
27 /**
28 * @param {!Puppeteer.CDPSession} client
29 * @param {!Protocol.Page.FrameTree} frameTree
30 * @param {!Puppeteer.Page} page
31 */
32 constructor(client, frameTree, page) {
33 super();
34 this._client = client;
35 this._page = page;
36 /** @type {!Map<string, !Frame>} */
37 this._frames = new Map();
38 /** @type {!Map<number, !ExecutionContext>} */
39 this._contextIdToContext = new Map();
40
41 this._client.on('Page.frameAttached', event => this._onFrameAttached(event.frameId, event.parentFrameId));
42 this._client.on('Page.frameNavigated', event => this._onFrameNavigated(event.frame));
43 this._client.on('Page.navigatedWithinDocument', event => this._onFrameNavigatedWithinDocument(event.frameId, event.url));
44 this._client.on('Page.frameDetached', event => this._onFrameDetached(event.frameId));
45 this._client.on('Page.frameStoppedLoading', event => this._onFrameStoppedLoading(event.frameId));
46 this._client.on('Runtime.executionContextCreated', event => this._onExecutionContextCreated(event.context));
47 this._client.on('Runtime.executionContextDestroyed', event => this._onExecutionContextDestroyed(event.executionContextId));
48 this._client.on('Runtime.executionContextsCleared', event => this._onExecutionContextsCleared());
49 this._client.on('Page.lifecycleEvent', event => this._onLifecycleEvent(event));
50
51 this._handleFrameTree(frameTree);
52 }
53
54 /**
55 * @param {!Protocol.Page.lifecycleEventPayload} event
56 */
57 _onLifecycleEvent(event) {
58 const frame = this._frames.get(event.frameId);
59 if (!frame)
60 return;
61 frame._onLifecycleEvent(event.loaderId, event.name);
62 this.emit(FrameManager.Events.LifecycleEvent, frame);
63 }
64
65 /**
66 * @param {string} frameId
67 */
68 _onFrameStoppedLoading(frameId) {
69 const frame = this._frames.get(frameId);
70 if (!frame)
71 return;
72 frame._onLoadingStopped();
73 this.emit(FrameManager.Events.LifecycleEvent, frame);
74 }
75
76 /**
77 * @param {!Protocol.Page.FrameTree} frameTree
78 */
79 _handleFrameTree(frameTree) {
80 if (frameTree.frame.parentId)
81 this._onFrameAttached(frameTree.frame.id, frameTree.frame.parentId);
82 this._onFrameNavigated(frameTree.frame);
83 if (!frameTree.childFrames)
84 return;
85
86 for (const child of frameTree.childFrames)
87 this._handleFrameTree(child);
88 }
89
90 /**
91 * @return {!Frame}
92 */
93 mainFrame() {
94 return this._mainFrame;
95 }
96
97 /**
98 * @return {!Array<!Frame>}
99 */
100 frames() {
101 return Array.from(this._frames.values());
102 }
103
104 /**
105 * @param {!string} frameId
106 * @return {?Frame}
107 */
108 frame(frameId) {
109 return this._frames.get(frameId) || null;
110 }
111
112 /**
113 * @param {string} frameId
114 * @param {?string} parentFrameId
115 * @return {?Frame}
116 */
117 _onFrameAttached(frameId, parentFrameId) {
118 if (this._frames.has(frameId))
119 return;
120 assert(parentFrameId);
121 const parentFrame = this._frames.get(parentFrameId);
122 const frame = new Frame(this._client, parentFrame, frameId);
123 this._frames.set(frame._id, frame);
124 this.emit(FrameManager.Events.FrameAttached, frame);
125 }
126
127 /**
128 * @param {!Protocol.Page.Frame} framePayload
129 */
130 _onFrameNavigated(framePayload) {
131 const isMainFrame = !framePayload.parentId;
132 let frame = isMainFrame ? this._mainFrame : this._frames.get(framePayload.id);
133 assert(isMainFrame || frame, 'We either navigate top level or have old version of the navigated frame');
134
135 // Detach all child frames first.
136 if (frame) {
137 for (const child of frame.childFrames())
138 this._removeFramesRecursively(child);
139 }
140
141 // Update or create main frame.
142 if (isMainFrame) {
143 if (frame) {
144 // Update frame id to retain frame identity on cross-process navigation.
145 this._frames.delete(frame._id);
146 frame._id = framePayload.id;
147 } else {
148 // Initial main frame navigation.
149 frame = new Frame(this._client, null, framePayload.id);
150 }
151 this._frames.set(framePayload.id, frame);
152 this._mainFrame = frame;
153 }
154
155 // Update frame payload.
156 frame._navigated(framePayload);
157
158 this.emit(FrameManager.Events.FrameNavigated, frame);
159 }
160
161 /**
162 * @param {string} frameId
163 * @param {string} url
164 */
165 _onFrameNavigatedWithinDocument(frameId, url) {
166 const frame = this._frames.get(frameId);
167 if (!frame)
168 return;
169 frame._navigatedWithinDocument(url);
170 this.emit(FrameManager.Events.FrameNavigatedWithinDocument, frame);
171 this.emit(FrameManager.Events.FrameNavigated, frame);
172 }
173
174 /**
175 * @param {string} frameId
176 */
177 _onFrameDetached(frameId) {
178 const frame = this._frames.get(frameId);
179 if (frame)
180 this._removeFramesRecursively(frame);
181 }
182
183 _onExecutionContextCreated(contextPayload) {
184 const frameId = contextPayload.auxData ? contextPayload.auxData.frameId : null;
185 const frame = this._frames.get(frameId) || null;
186 /** @type {!ExecutionContext} */
187 const context = new ExecutionContext(this._client, contextPayload, obj => this.createJSHandle(context, obj), frame);
188 this._contextIdToContext.set(contextPayload.id, context);
189 if (frame)
190 frame._addExecutionContext(context);
191 }
192
193 /**
194 * @param {number} executionContextId
195 */
196 _onExecutionContextDestroyed(executionContextId) {
197 const context = this._contextIdToContext.get(executionContextId);
198 if (!context)
199 return;
200 this._contextIdToContext.delete(executionContextId);
201 if (context.frame())
202 context.frame()._removeExecutionContext(context);
203 }
204
205 _onExecutionContextsCleared() {
206 for (const context of this._contextIdToContext.values()) {
207 if (context.frame())
208 context.frame()._removeExecutionContext(context);
209 }
210 this._contextIdToContext.clear();
211 }
212
213 /**
214 * @param {number} contextId
215 * @return {!ExecutionContext}
216 */
217 executionContextById(contextId) {
218 const context = this._contextIdToContext.get(contextId);
219 assert(context, 'INTERNAL ERROR: missing context with id = ' + contextId);
220 return context;
221 }
222
223 /**
224 * @param {!ExecutionContext} context
225 * @param {!Protocol.Runtime.RemoteObject} remoteObject
226 * @return {!JSHandle}
227 */
228 createJSHandle(context, remoteObject) {
229 if (remoteObject.subtype === 'node')
230 return new ElementHandle(context, this._client, remoteObject, this._page, this);
231 return new JSHandle(context, this._client, remoteObject);
232 }
233
234 /**
235 * @param {!Frame} frame
236 */
237 _removeFramesRecursively(frame) {
238 for (const child of frame.childFrames())
239 this._removeFramesRecursively(child);
240 frame._detach();
241 this._frames.delete(frame._id);
242 this.emit(FrameManager.Events.FrameDetached, frame);
243 }
244}
245
246/** @enum {string} */
247FrameManager.Events = {
248 FrameAttached: 'frameattached',
249 FrameNavigated: 'framenavigated',
250 FrameDetached: 'framedetached',
251 LifecycleEvent: 'lifecycleevent',
252 FrameNavigatedWithinDocument: 'framenavigatedwithindocument',
253 ExecutionContextCreated: 'executioncontextcreated',
254 ExecutionContextDestroyed: 'executioncontextdestroyed',
255};
256
257/**
258 * @unrestricted
259 */
260class Frame {
261 /**
262 * @param {!Puppeteer.CDPSession} client
263 * @param {?Frame} parentFrame
264 * @param {string} frameId
265 */
266 constructor(client, parentFrame, frameId) {
267 this._client = client;
268 this._parentFrame = parentFrame;
269 this._url = '';
270 this._id = frameId;
271
272 /** @type {?Promise<!ElementHandle>} */
273 this._documentPromise = null;
274 /** @type {?Promise<!ExecutionContext>} */
275 this._contextPromise = null;
276 this._contextResolveCallback = null;
277 this._setDefaultContext(null);
278
279 /** @type {!Set<!WaitTask>} */
280 this._waitTasks = new Set();
281 this._loaderId = '';
282 /** @type {!Set<string>} */
283 this._lifecycleEvents = new Set();
284
285 /** @type {!Set<!Frame>} */
286 this._childFrames = new Set();
287 if (this._parentFrame)
288 this._parentFrame._childFrames.add(this);
289 }
290
291 /**
292 * @param {!ExecutionContext} context
293 */
294 _addExecutionContext(context) {
295 if (context._isDefault)
296 this._setDefaultContext(context);
297 }
298
299 /**
300 * @param {!ExecutionContext} context
301 */
302 _removeExecutionContext(context) {
303 if (context._isDefault)
304 this._setDefaultContext(null);
305 }
306
307 /**
308 * @param {?ExecutionContext} context
309 */
310 _setDefaultContext(context) {
311 if (context) {
312 this._contextResolveCallback.call(null, context);
313 this._contextResolveCallback = null;
314 for (const waitTask of this._waitTasks)
315 waitTask.rerun();
316 } else {
317 this._documentPromise = null;
318 this._contextPromise = new Promise(fulfill => {
319 this._contextResolveCallback = fulfill;
320 });
321 }
322 }
323
324 /**
325 * @return {!Promise<!ExecutionContext>}
326 */
327 executionContext() {
328 return this._contextPromise;
329 }
330
331 /**
332 * @param {function()|string} pageFunction
333 * @param {!Array<*>} args
334 * @return {!Promise<!Puppeteer.JSHandle>}
335 */
336 /* async */ evaluateHandle(pageFunction, ...args) {return (fn => {
337 const gen = fn.call(this);
338 return new Promise((resolve, reject) => {
339 function step(key, arg) {
340 let info, value;
341 try {
342 info = gen[key](arg);
343 value = info.value;
344 } catch (error) {
345 reject(error);
346 return;
347 }
348 if (info.done) {
349 resolve(value);
350 } else {
351 return Promise.resolve(value).then(
352 value => {
353 step('next', value);
354 },
355 err => {
356 step('throw', err);
357 });
358 }
359 }
360 return step('next');
361 });
362})(function*(){
363 const context = (yield this._contextPromise);
364 return context.evaluateHandle(pageFunction, ...args);
365 });}
366
367 /**
368 * @param {Function|string} pageFunction
369 * @param {!Array<*>} args
370 * @return {!Promise<*>}
371 */
372 /* async */ evaluate(pageFunction, ...args) {return (fn => {
373 const gen = fn.call(this);
374 return new Promise((resolve, reject) => {
375 function step(key, arg) {
376 let info, value;
377 try {
378 info = gen[key](arg);
379 value = info.value;
380 } catch (error) {
381 reject(error);
382 return;
383 }
384 if (info.done) {
385 resolve(value);
386 } else {
387 return Promise.resolve(value).then(
388 value => {
389 step('next', value);
390 },
391 err => {
392 step('throw', err);
393 });
394 }
395 }
396 return step('next');
397 });
398})(function*(){
399 const context = (yield this._contextPromise);
400 return context.evaluate(pageFunction, ...args);
401 });}
402
403 /**
404 * @param {string} selector
405 * @return {!Promise<?ElementHandle>}
406 */
407 /* async */ $(selector) {return (fn => {
408 const gen = fn.call(this);
409 return new Promise((resolve, reject) => {
410 function step(key, arg) {
411 let info, value;
412 try {
413 info = gen[key](arg);
414 value = info.value;
415 } catch (error) {
416 reject(error);
417 return;
418 }
419 if (info.done) {
420 resolve(value);
421 } else {
422 return Promise.resolve(value).then(
423 value => {
424 step('next', value);
425 },
426 err => {
427 step('throw', err);
428 });
429 }
430 }
431 return step('next');
432 });
433})(function*(){
434 const document = (yield this._document());
435 const value = (yield document.$(selector));
436 return value;
437 });}
438
439 /**
440 * @return {!Promise<!ElementHandle>}
441 */
442 /* async */ _document() {return (fn => {
443 const gen = fn.call(this);
444 return new Promise((resolve, reject) => {
445 function step(key, arg) {
446 let info, value;
447 try {
448 info = gen[key](arg);
449 value = info.value;
450 } catch (error) {
451 reject(error);
452 return;
453 }
454 if (info.done) {
455 resolve(value);
456 } else {
457 return Promise.resolve(value).then(
458 value => {
459 step('next', value);
460 },
461 err => {
462 step('throw', err);
463 });
464 }
465 }
466 return step('next');
467 });
468})(function*(){
469 if (this._documentPromise)
470 return this._documentPromise;
471 this._documentPromise = this._contextPromise.then(/* async */ context => {return (fn => {
472 const gen = fn.call(this);
473 return new Promise((resolve, reject) => {
474 function step(key, arg) {
475 let info, value;
476 try {
477 info = gen[key](arg);
478 value = info.value;
479 } catch (error) {
480 reject(error);
481 return;
482 }
483 if (info.done) {
484 resolve(value);
485 } else {
486 return Promise.resolve(value).then(
487 value => {
488 step('next', value);
489 },
490 err => {
491 step('throw', err);
492 });
493 }
494 }
495 return step('next');
496 });
497})(function*(){
498 const document = (yield context.evaluateHandle('document'));
499 return document.asElement();
500 });});
501 return this._documentPromise;
502 });}
503
504 /**
505 * @param {string} expression
506 * @return {!Promise<!Array<!ElementHandle>>}
507 */
508 /* async */ $x(expression) {return (fn => {
509 const gen = fn.call(this);
510 return new Promise((resolve, reject) => {
511 function step(key, arg) {
512 let info, value;
513 try {
514 info = gen[key](arg);
515 value = info.value;
516 } catch (error) {
517 reject(error);
518 return;
519 }
520 if (info.done) {
521 resolve(value);
522 } else {
523 return Promise.resolve(value).then(
524 value => {
525 step('next', value);
526 },
527 err => {
528 step('throw', err);
529 });
530 }
531 }
532 return step('next');
533 });
534})(function*(){
535 const document = (yield this._document());
536 const value = (yield document.$x(expression));
537 return value;
538 });}
539
540 /**
541 * @param {string} selector
542 * @param {Function|string} pageFunction
543 * @param {!Array<*>} args
544 * @return {!Promise<(!Object|undefined)>}
545 */
546 /* async */ $eval(selector, pageFunction, ...args) {return (fn => {
547 const gen = fn.call(this);
548 return new Promise((resolve, reject) => {
549 function step(key, arg) {
550 let info, value;
551 try {
552 info = gen[key](arg);
553 value = info.value;
554 } catch (error) {
555 reject(error);
556 return;
557 }
558 if (info.done) {
559 resolve(value);
560 } else {
561 return Promise.resolve(value).then(
562 value => {
563 step('next', value);
564 },
565 err => {
566 step('throw', err);
567 });
568 }
569 }
570 return step('next');
571 });
572})(function*(){
573 const document = (yield this._document());
574 return document.$eval(selector, pageFunction, ...args);
575 });}
576
577 /**
578 * @param {string} selector
579 * @param {Function|string} pageFunction
580 * @param {!Array<*>} args
581 * @return {!Promise<(!Object|undefined)>}
582 */
583 /* async */ $$eval(selector, pageFunction, ...args) {return (fn => {
584 const gen = fn.call(this);
585 return new Promise((resolve, reject) => {
586 function step(key, arg) {
587 let info, value;
588 try {
589 info = gen[key](arg);
590 value = info.value;
591 } catch (error) {
592 reject(error);
593 return;
594 }
595 if (info.done) {
596 resolve(value);
597 } else {
598 return Promise.resolve(value).then(
599 value => {
600 step('next', value);
601 },
602 err => {
603 step('throw', err);
604 });
605 }
606 }
607 return step('next');
608 });
609})(function*(){
610 const document = (yield this._document());
611 const value = (yield document.$$eval(selector, pageFunction, ...args));
612 return value;
613 });}
614
615 /**
616 * @param {string} selector
617 * @return {!Promise<!Array<!ElementHandle>>}
618 */
619 /* async */ $$(selector) {return (fn => {
620 const gen = fn.call(this);
621 return new Promise((resolve, reject) => {
622 function step(key, arg) {
623 let info, value;
624 try {
625 info = gen[key](arg);
626 value = info.value;
627 } catch (error) {
628 reject(error);
629 return;
630 }
631 if (info.done) {
632 resolve(value);
633 } else {
634 return Promise.resolve(value).then(
635 value => {
636 step('next', value);
637 },
638 err => {
639 step('throw', err);
640 });
641 }
642 }
643 return step('next');
644 });
645})(function*(){
646 const document = (yield this._document());
647 const value = (yield document.$$(selector));
648 return value;
649 });}
650
651 /**
652 * @return {!Promise<String>}
653 */
654 /* async */ content() {return (fn => {
655 const gen = fn.call(this);
656 return new Promise((resolve, reject) => {
657 function step(key, arg) {
658 let info, value;
659 try {
660 info = gen[key](arg);
661 value = info.value;
662 } catch (error) {
663 reject(error);
664 return;
665 }
666 if (info.done) {
667 resolve(value);
668 } else {
669 return Promise.resolve(value).then(
670 value => {
671 step('next', value);
672 },
673 err => {
674 step('throw', err);
675 });
676 }
677 }
678 return step('next');
679 });
680})(function*(){
681 return (yield this.evaluate(() => {
682 let retVal = '';
683 if (document.doctype)
684 retVal = new XMLSerializer().serializeToString(document.doctype);
685 if (document.documentElement)
686 retVal += document.documentElement.outerHTML;
687 return retVal;
688 }));
689 });}
690
691 /**
692 * @param {string} html
693 */
694 /* async */ setContent(html) {return (fn => {
695 const gen = fn.call(this);
696 return new Promise((resolve, reject) => {
697 function step(key, arg) {
698 let info, value;
699 try {
700 info = gen[key](arg);
701 value = info.value;
702 } catch (error) {
703 reject(error);
704 return;
705 }
706 if (info.done) {
707 resolve(value);
708 } else {
709 return Promise.resolve(value).then(
710 value => {
711 step('next', value);
712 },
713 err => {
714 step('throw', err);
715 });
716 }
717 }
718 return step('next');
719 });
720})(function*(){
721 (yield this.evaluate(html => {
722 document.open();
723 document.write(html);
724 document.close();
725 }, html));
726 });}
727
728 /**
729 * @return {string}
730 */
731 name() {
732 return this._name || '';
733 }
734
735 /**
736 * @return {string}
737 */
738 url() {
739 return this._url;
740 }
741
742 /**
743 * @return {?Frame}
744 */
745 parentFrame() {
746 return this._parentFrame;
747 }
748
749 /**
750 * @return {!Array.<!Frame>}
751 */
752 childFrames() {
753 return Array.from(this._childFrames);
754 }
755
756 /**
757 * @return {boolean}
758 */
759 isDetached() {
760 return this._detached;
761 }
762
763 /**
764 * @param {Object} options
765 * @return {!Promise<!ElementHandle>}
766 */
767 /* async */ addScriptTag(options) {return (fn => {
768 const gen = fn.call(this);
769 return new Promise((resolve, reject) => {
770 function step(key, arg) {
771 let info, value;
772 try {
773 info = gen[key](arg);
774 value = info.value;
775 } catch (error) {
776 reject(error);
777 return;
778 }
779 if (info.done) {
780 resolve(value);
781 } else {
782 return Promise.resolve(value).then(
783 value => {
784 step('next', value);
785 },
786 err => {
787 step('throw', err);
788 });
789 }
790 }
791 return step('next');
792 });
793})(function*(){
794 if (typeof options.url === 'string') {
795 const url = options.url;
796 try {
797 const context = (yield this._contextPromise);
798 return ((yield context.evaluateHandle(addScriptUrl, url, options.type))).asElement();
799 } catch (error) {
800 throw new Error(`Loading script from ${url} failed`);
801 }
802 }
803
804 if (typeof options.path === 'string') {
805 let contents = (yield readFileAsync(options.path, 'utf8'));
806 contents += '//# sourceURL=' + options.path.replace(/\n/g, '');
807 const context = (yield this._contextPromise);
808 return ((yield context.evaluateHandle(addScriptContent, contents, options.type))).asElement();
809 }
810
811 if (typeof options.content === 'string') {
812 const context = (yield this._contextPromise);
813 return ((yield context.evaluateHandle(addScriptContent, options.content, options.type))).asElement();
814 }
815
816 throw new Error('Provide an object with a `url`, `path` or `content` property');
817
818 /**
819 * @param {string} url
820 * @param {string} type
821 * @return {!Promise<!HTMLElement>}
822 */
823 /* async */ function addScriptUrl(url, type) {return (fn => {
824 const gen = fn.call(this);
825 return new Promise((resolve, reject) => {
826 function step(key, arg) {
827 let info, value;
828 try {
829 info = gen[key](arg);
830 value = info.value;
831 } catch (error) {
832 reject(error);
833 return;
834 }
835 if (info.done) {
836 resolve(value);
837 } else {
838 return Promise.resolve(value).then(
839 value => {
840 step('next', value);
841 },
842 err => {
843 step('throw', err);
844 });
845 }
846 }
847 return step('next');
848 });
849})(function*(){
850 const script = document.createElement('script');
851 script.src = url;
852 if (type)
853 script.type = type;
854 const promise = new Promise((res, rej) => {
855 script.onload = res;
856 script.onerror = rej;
857 });
858 document.head.appendChild(script);
859 (yield promise);
860 return script;
861 });}
862
863 /**
864 * @param {string} content
865 * @param {string} type
866 * @return {!HTMLElement}
867 */
868 function addScriptContent(content, type = 'text/javascript') {
869 const script = document.createElement('script');
870 script.type = type;
871 script.text = content;
872 let error = null;
873 script.onerror = e => error = e;
874 document.head.appendChild(script);
875 if (error)
876 throw error;
877 return script;
878 }
879 });}
880
881 /**
882 * @param {Object} options
883 * @return {!Promise<!ElementHandle>}
884 */
885 /* async */ addStyleTag(options) {return (fn => {
886 const gen = fn.call(this);
887 return new Promise((resolve, reject) => {
888 function step(key, arg) {
889 let info, value;
890 try {
891 info = gen[key](arg);
892 value = info.value;
893 } catch (error) {
894 reject(error);
895 return;
896 }
897 if (info.done) {
898 resolve(value);
899 } else {
900 return Promise.resolve(value).then(
901 value => {
902 step('next', value);
903 },
904 err => {
905 step('throw', err);
906 });
907 }
908 }
909 return step('next');
910 });
911})(function*(){
912 if (typeof options.url === 'string') {
913 const url = options.url;
914 try {
915 const context = (yield this._contextPromise);
916 return ((yield context.evaluateHandle(addStyleUrl, url))).asElement();
917 } catch (error) {
918 throw new Error(`Loading style from ${url} failed`);
919 }
920 }
921
922 if (typeof options.path === 'string') {
923 let contents = (yield readFileAsync(options.path, 'utf8'));
924 contents += '/*# sourceURL=' + options.path.replace(/\n/g, '') + '*/';
925 const context = (yield this._contextPromise);
926 return ((yield context.evaluateHandle(addStyleContent, contents))).asElement();
927 }
928
929 if (typeof options.content === 'string') {
930 const context = (yield this._contextPromise);
931 return ((yield context.evaluateHandle(addStyleContent, options.content))).asElement();
932 }
933
934 throw new Error('Provide an object with a `url`, `path` or `content` property');
935
936 /**
937 * @param {string} url
938 * @return {!Promise<!HTMLElement>}
939 */
940 /* async */ function addStyleUrl(url) {return (fn => {
941 const gen = fn.call(this);
942 return new Promise((resolve, reject) => {
943 function step(key, arg) {
944 let info, value;
945 try {
946 info = gen[key](arg);
947 value = info.value;
948 } catch (error) {
949 reject(error);
950 return;
951 }
952 if (info.done) {
953 resolve(value);
954 } else {
955 return Promise.resolve(value).then(
956 value => {
957 step('next', value);
958 },
959 err => {
960 step('throw', err);
961 });
962 }
963 }
964 return step('next');
965 });
966})(function*(){
967 const link = document.createElement('link');
968 link.rel = 'stylesheet';
969 link.href = url;
970 const promise = new Promise((res, rej) => {
971 link.onload = res;
972 link.onerror = rej;
973 });
974 document.head.appendChild(link);
975 (yield promise);
976 return link;
977 });}
978
979 /**
980 * @param {string} content
981 * @return {!Promise<!HTMLElement>}
982 */
983 /* async */ function addStyleContent(content) {return (fn => {
984 const gen = fn.call(this);
985 return new Promise((resolve, reject) => {
986 function step(key, arg) {
987 let info, value;
988 try {
989 info = gen[key](arg);
990 value = info.value;
991 } catch (error) {
992 reject(error);
993 return;
994 }
995 if (info.done) {
996 resolve(value);
997 } else {
998 return Promise.resolve(value).then(
999 value => {
1000 step('next', value);
1001 },
1002 err => {
1003 step('throw', err);
1004 });
1005 }
1006 }
1007 return step('next');
1008 });
1009})(function*(){
1010 const style = document.createElement('style');
1011 style.type = 'text/css';
1012 style.appendChild(document.createTextNode(content));
1013 const promise = new Promise((res, rej) => {
1014 style.onload = res;
1015 style.onerror = rej;
1016 });
1017 document.head.appendChild(style);
1018 (yield promise);
1019 return style;
1020 });}
1021 });}
1022
1023 /**
1024 * @param {string} selector
1025 * @param {!Object=} options
1026 */
1027 /* async */ click(selector, options = {}) {return (fn => {
1028 const gen = fn.call(this);
1029 return new Promise((resolve, reject) => {
1030 function step(key, arg) {
1031 let info, value;
1032 try {
1033 info = gen[key](arg);
1034 value = info.value;
1035 } catch (error) {
1036 reject(error);
1037 return;
1038 }
1039 if (info.done) {
1040 resolve(value);
1041 } else {
1042 return Promise.resolve(value).then(
1043 value => {
1044 step('next', value);
1045 },
1046 err => {
1047 step('throw', err);
1048 });
1049 }
1050 }
1051 return step('next');
1052 });
1053})(function*(){
1054 const handle = (yield this.$(selector));
1055 assert(handle, 'No node found for selector: ' + selector);
1056 (yield handle.click(options));
1057 (yield handle.dispose());
1058 });}
1059
1060 /**
1061 * @param {string} selector
1062 */
1063 /* async */ focus(selector) {return (fn => {
1064 const gen = fn.call(this);
1065 return new Promise((resolve, reject) => {
1066 function step(key, arg) {
1067 let info, value;
1068 try {
1069 info = gen[key](arg);
1070 value = info.value;
1071 } catch (error) {
1072 reject(error);
1073 return;
1074 }
1075 if (info.done) {
1076 resolve(value);
1077 } else {
1078 return Promise.resolve(value).then(
1079 value => {
1080 step('next', value);
1081 },
1082 err => {
1083 step('throw', err);
1084 });
1085 }
1086 }
1087 return step('next');
1088 });
1089})(function*(){
1090 const handle = (yield this.$(selector));
1091 assert(handle, 'No node found for selector: ' + selector);
1092 (yield handle.focus());
1093 (yield handle.dispose());
1094 });}
1095
1096 /**
1097 * @param {string} selector
1098 */
1099 /* async */ hover(selector) {return (fn => {
1100 const gen = fn.call(this);
1101 return new Promise((resolve, reject) => {
1102 function step(key, arg) {
1103 let info, value;
1104 try {
1105 info = gen[key](arg);
1106 value = info.value;
1107 } catch (error) {
1108 reject(error);
1109 return;
1110 }
1111 if (info.done) {
1112 resolve(value);
1113 } else {
1114 return Promise.resolve(value).then(
1115 value => {
1116 step('next', value);
1117 },
1118 err => {
1119 step('throw', err);
1120 });
1121 }
1122 }
1123 return step('next');
1124 });
1125})(function*(){
1126 const handle = (yield this.$(selector));
1127 assert(handle, 'No node found for selector: ' + selector);
1128 (yield handle.hover());
1129 (yield handle.dispose());
1130 });}
1131
1132 /**
1133 * @param {string} selector
1134 * @param {!Array<string>} values
1135 * @return {!Promise<!Array<string>>}
1136 */
1137 select(selector, ...values){
1138 for (const value of values)
1139 assert(helper.isString(value), 'Values must be strings. Found value "' + value + '" of type "' + (typeof value) + '"');
1140 return this.$eval(selector, (element, values) => {
1141 if (element.nodeName.toLowerCase() !== 'select')
1142 throw new Error('Element is not a <select> element.');
1143
1144 const options = Array.from(element.options);
1145 element.value = undefined;
1146 for (const option of options) {
1147 option.selected = values.includes(option.value);
1148 if (option.selected && !element.multiple)
1149 break;
1150 }
1151 element.dispatchEvent(new Event('input', { 'bubbles': true }));
1152 element.dispatchEvent(new Event('change', { 'bubbles': true }));
1153 return options.filter(option => option.selected).map(option => option.value);
1154 }, values);
1155 }
1156
1157 /**
1158 * @param {string} selector
1159 */
1160 /* async */ tap(selector) {return (fn => {
1161 const gen = fn.call(this);
1162 return new Promise((resolve, reject) => {
1163 function step(key, arg) {
1164 let info, value;
1165 try {
1166 info = gen[key](arg);
1167 value = info.value;
1168 } catch (error) {
1169 reject(error);
1170 return;
1171 }
1172 if (info.done) {
1173 resolve(value);
1174 } else {
1175 return Promise.resolve(value).then(
1176 value => {
1177 step('next', value);
1178 },
1179 err => {
1180 step('throw', err);
1181 });
1182 }
1183 }
1184 return step('next');
1185 });
1186})(function*(){
1187 const handle = (yield this.$(selector));
1188 assert(handle, 'No node found for selector: ' + selector);
1189 (yield handle.tap());
1190 (yield handle.dispose());
1191 });}
1192
1193 /**
1194 * @param {string} selector
1195 * @param {string} text
1196 * @param {{delay: (number|undefined)}=} options
1197 */
1198 /* async */ type(selector, text, options) {return (fn => {
1199 const gen = fn.call(this);
1200 return new Promise((resolve, reject) => {
1201 function step(key, arg) {
1202 let info, value;
1203 try {
1204 info = gen[key](arg);
1205 value = info.value;
1206 } catch (error) {
1207 reject(error);
1208 return;
1209 }
1210 if (info.done) {
1211 resolve(value);
1212 } else {
1213 return Promise.resolve(value).then(
1214 value => {
1215 step('next', value);
1216 },
1217 err => {
1218 step('throw', err);
1219 });
1220 }
1221 }
1222 return step('next');
1223 });
1224})(function*(){
1225 const handle = (yield this.$(selector));
1226 assert(handle, 'No node found for selector: ' + selector);
1227 (yield handle.type(text, options));
1228 (yield handle.dispose());
1229 });}
1230
1231 /**
1232 * @param {(string|number|Function)} selectorOrFunctionOrTimeout
1233 * @param {!Object=} options
1234 * @param {!Array<*>} args
1235 * @return {!Promise}
1236 */
1237 waitFor(selectorOrFunctionOrTimeout, options = {}, ...args) {
1238 const xPathPattern = '//';
1239
1240 if (helper.isString(selectorOrFunctionOrTimeout)) {
1241 const string = /** @type {string} */ (selectorOrFunctionOrTimeout);
1242 if (string.startsWith(xPathPattern))
1243 return this.waitForXPath(string, options);
1244 return this.waitForSelector(string, options);
1245 }
1246 if (helper.isNumber(selectorOrFunctionOrTimeout))
1247 return new Promise(fulfill => setTimeout(fulfill, selectorOrFunctionOrTimeout));
1248 if (typeof selectorOrFunctionOrTimeout === 'function')
1249 return this.waitForFunction(selectorOrFunctionOrTimeout, options, ...args);
1250 return Promise.reject(new Error('Unsupported target type: ' + (typeof selectorOrFunctionOrTimeout)));
1251 }
1252
1253 /**
1254 * @param {string} selector
1255 * @param {!Object=} options
1256 * @return {!Promise}
1257 */
1258 waitForSelector(selector, options = {}) {
1259 return this._waitForSelectorOrXPath(selector, false, options);
1260 }
1261
1262 /**
1263 * @param {string} xpath
1264 * @param {!Object=} options
1265 * @return {!Promise}
1266 */
1267 waitForXPath(xpath, options = {}) {
1268 return this._waitForSelectorOrXPath(xpath, true, options);
1269 }
1270
1271 /**
1272 * @param {Function|string} pageFunction
1273 * @param {!Object=} options
1274 * @return {!Promise}
1275 */
1276 waitForFunction(pageFunction, options = {}, ...args) {
1277 const timeout = helper.isNumber(options.timeout) ? options.timeout : 30000;
1278 const polling = options.polling || 'raf';
1279 return new WaitTask(this, pageFunction, 'function', polling, timeout, ...args).promise;
1280 }
1281
1282 /**
1283 * @return {!Promise<string>}
1284 */
1285 /* async */ title() {return (fn => {
1286 const gen = fn.call(this);
1287 return new Promise((resolve, reject) => {
1288 function step(key, arg) {
1289 let info, value;
1290 try {
1291 info = gen[key](arg);
1292 value = info.value;
1293 } catch (error) {
1294 reject(error);
1295 return;
1296 }
1297 if (info.done) {
1298 resolve(value);
1299 } else {
1300 return Promise.resolve(value).then(
1301 value => {
1302 step('next', value);
1303 },
1304 err => {
1305 step('throw', err);
1306 });
1307 }
1308 }
1309 return step('next');
1310 });
1311})(function*(){
1312 return this.evaluate(() => document.title);
1313 });}
1314
1315 /**
1316 * @param {string} selectorOrXPath
1317 * @param {boolean} isXPath
1318 * @param {!Object=} options
1319 * @return {!Promise}
1320 */
1321 _waitForSelectorOrXPath(selectorOrXPath, isXPath, options = {}) {
1322 const waitForVisible = !!options.visible;
1323 const waitForHidden = !!options.hidden;
1324 const polling = waitForVisible || waitForHidden ? 'raf' : 'mutation';
1325 const timeout = helper.isNumber(options.timeout) ? options.timeout : 30000;
1326 const title = `${isXPath ? 'XPath' : 'selector'} "${selectorOrXPath}"${waitForHidden ? ' to be hidden' : ''}`;
1327 return new WaitTask(this, predicate, title, polling, timeout, selectorOrXPath, isXPath, waitForVisible, waitForHidden).promise;
1328
1329 /**
1330 * @param {string} selectorOrXPath
1331 * @param {boolean} isXPath
1332 * @param {boolean} waitForVisible
1333 * @param {boolean} waitForHidden
1334 * @return {?Node|boolean}
1335 */
1336 function predicate(selectorOrXPath, isXPath, waitForVisible, waitForHidden) {
1337 const node = isXPath
1338 ? document.evaluate(selectorOrXPath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue
1339 : document.querySelector(selectorOrXPath);
1340 if (!node)
1341 return waitForHidden;
1342 if (!waitForVisible && !waitForHidden)
1343 return node;
1344 const element = /** @type {Element} */ (node.nodeType === Node.TEXT_NODE ? node.parentElement : node);
1345
1346 const style = window.getComputedStyle(element);
1347 const isVisible = style && style.visibility !== 'hidden' && hasVisibleBoundingBox();
1348 const success = (waitForVisible === isVisible || waitForHidden === !isVisible);
1349 return success ? node : null;
1350
1351 /**
1352 * @return {boolean}
1353 */
1354 function hasVisibleBoundingBox() {
1355 const rect = element.getBoundingClientRect();
1356 return !!(rect.top || rect.bottom || rect.width || rect.height);
1357 }
1358 }
1359 }
1360
1361 /**
1362 * @param {!Protocol.Page.Frame} framePayload
1363 */
1364 _navigated(framePayload) {
1365 this._name = framePayload.name;
1366 // TODO(lushnikov): remove this once requestInterception has loaderId exposed.
1367 this._navigationURL = framePayload.url;
1368 this._url = framePayload.url;
1369 }
1370
1371 /**
1372 * @param {string} url
1373 */
1374 _navigatedWithinDocument(url) {
1375 this._url = url;
1376 }
1377
1378 /**
1379 * @param {string} loaderId
1380 * @param {string} name
1381 */
1382 _onLifecycleEvent(loaderId, name) {
1383 if (name === 'init') {
1384 this._loaderId = loaderId;
1385 this._lifecycleEvents.clear();
1386 }
1387 this._lifecycleEvents.add(name);
1388 }
1389
1390 _onLoadingStopped() {
1391 this._lifecycleEvents.add('DOMContentLoaded');
1392 this._lifecycleEvents.add('load');
1393 }
1394
1395 _detach() {
1396 for (const waitTask of this._waitTasks)
1397 waitTask.terminate(new Error('waitForFunction failed: frame got detached.'));
1398 this._detached = true;
1399 if (this._parentFrame)
1400 this._parentFrame._childFrames.delete(this);
1401 this._parentFrame = null;
1402 }
1403}
1404helper.tracePublicAPI(Frame);
1405
1406class WaitTask {
1407 /**
1408 * @param {!Frame} frame
1409 * @param {Function|string} predicateBody
1410 * @param {string|number} polling
1411 * @param {number} timeout
1412 * @param {!Array<*>} args
1413 */
1414 constructor(frame, predicateBody, title, polling, timeout, ...args) {
1415 if (helper.isString(polling))
1416 assert(polling === 'raf' || polling === 'mutation', 'Unknown polling option: ' + polling);
1417 else if (helper.isNumber(polling))
1418 assert(polling > 0, 'Cannot poll with non-positive interval: ' + polling);
1419 else
1420 throw new Error('Unknown polling options: ' + polling);
1421
1422 this._frame = frame;
1423 this._polling = polling;
1424 this._timeout = timeout;
1425 this._predicateBody = helper.isString(predicateBody) ? 'return ' + predicateBody : 'return (' + predicateBody + ')(...args)';
1426 this._args = args;
1427 this._runCount = 0;
1428 frame._waitTasks.add(this);
1429 this.promise = new Promise((resolve, reject) => {
1430 this._resolve = resolve;
1431 this._reject = reject;
1432 });
1433 // Since page navigation requires us to re-install the pageScript, we should track
1434 // timeout on our end.
1435 if (timeout) {
1436 const timeoutError = new TimeoutError(`waiting for ${title} failed: timeout ${timeout}ms exceeded`);
1437 this._timeoutTimer = setTimeout(() => this.terminate(timeoutError), timeout);
1438 }
1439 this.rerun();
1440 }
1441
1442 /**
1443 * @param {!Error} error
1444 */
1445 terminate(error) {
1446 this._terminated = true;
1447 this._reject(error);
1448 this._cleanup();
1449 }
1450
1451 /* async */ rerun() {return (fn => {
1452 const gen = fn.call(this);
1453 return new Promise((resolve, reject) => {
1454 function step(key, arg) {
1455 let info, value;
1456 try {
1457 info = gen[key](arg);
1458 value = info.value;
1459 } catch (error) {
1460 reject(error);
1461 return;
1462 }
1463 if (info.done) {
1464 resolve(value);
1465 } else {
1466 return Promise.resolve(value).then(
1467 value => {
1468 step('next', value);
1469 },
1470 err => {
1471 step('throw', err);
1472 });
1473 }
1474 }
1475 return step('next');
1476 });
1477})(function*(){
1478 const runCount = ++this._runCount;
1479 /** @type {?JSHandle} */
1480 let success = null;
1481 let error = null;
1482 try {
1483 success = (yield ((yield this._frame.executionContext())).evaluateHandle(waitForPredicatePageFunction, this._predicateBody, this._polling, this._timeout, ...this._args));
1484 } catch (e) {
1485 error = e;
1486 }
1487
1488 if (this._terminated || runCount !== this._runCount) {
1489 if (success)
1490 (yield success.dispose());
1491 return;
1492 }
1493
1494 // Ignore timeouts in pageScript - we track timeouts ourselves.
1495 // If the frame's execution context has already changed, `frame.evaluate` will
1496 // throw an error - ignore this predicate run altogether.
1497 if (!error && (yield this._frame.evaluate(s => !s, success).catch(e => true))) {
1498 (yield success.dispose());
1499 return;
1500 }
1501
1502 // When the page is navigated, the promise is rejected.
1503 // We will try again in the new execution context.
1504 if (error && error.message.includes('Execution context was destroyed'))
1505 return;
1506
1507 // We could have tried to evaluate in a context which was already
1508 // destroyed.
1509 if (error && error.message.includes('Cannot find context with specified id'))
1510 return;
1511
1512 if (error)
1513 this._reject(error);
1514 else
1515 this._resolve(success);
1516
1517 this._cleanup();
1518 });}
1519
1520 _cleanup() {
1521 clearTimeout(this._timeoutTimer);
1522 this._frame._waitTasks.delete(this);
1523 this._runningTask = null;
1524 }
1525}
1526
1527/**
1528 * @param {string} predicateBody
1529 * @param {string} polling
1530 * @param {number} timeout
1531 * @return {!Promise<*>}
1532 */
1533/* async */ function waitForPredicatePageFunction(predicateBody, polling, timeout, ...args) {return (fn => {
1534 const gen = fn.call(this);
1535 return new Promise((resolve, reject) => {
1536 function step(key, arg) {
1537 let info, value;
1538 try {
1539 info = gen[key](arg);
1540 value = info.value;
1541 } catch (error) {
1542 reject(error);
1543 return;
1544 }
1545 if (info.done) {
1546 resolve(value);
1547 } else {
1548 return Promise.resolve(value).then(
1549 value => {
1550 step('next', value);
1551 },
1552 err => {
1553 step('throw', err);
1554 });
1555 }
1556 }
1557 return step('next');
1558 });
1559})(function*(){
1560 const predicate = new Function('...args', predicateBody);
1561 let timedOut = false;
1562 if (timeout)
1563 setTimeout(() => timedOut = true, timeout);
1564 if (polling === 'raf')
1565 return (yield pollRaf());
1566 if (polling === 'mutation')
1567 return (yield pollMutation());
1568 if (typeof polling === 'number')
1569 return (yield pollInterval(polling));
1570
1571 /**
1572 * @return {!Promise<*>}
1573 */
1574 function pollMutation() {
1575 const success = predicate.apply(null, args);
1576 if (success)
1577 return Promise.resolve(success);
1578
1579 let fulfill;
1580 const result = new Promise(x => fulfill = x);
1581 const observer = new MutationObserver(mutations => {
1582 if (timedOut) {
1583 observer.disconnect();
1584 fulfill();
1585 }
1586 const success = predicate.apply(null, args);
1587 if (success) {
1588 observer.disconnect();
1589 fulfill(success);
1590 }
1591 });
1592 observer.observe(document, {
1593 childList: true,
1594 subtree: true,
1595 attributes: true
1596 });
1597 return result;
1598 }
1599
1600 /**
1601 * @return {!Promise<*>}
1602 */
1603 function pollRaf() {
1604 let fulfill;
1605 const result = new Promise(x => fulfill = x);
1606 onRaf();
1607 return result;
1608
1609 function onRaf() {
1610 if (timedOut) {
1611 fulfill();
1612 return;
1613 }
1614 const success = predicate.apply(null, args);
1615 if (success)
1616 fulfill(success);
1617 else
1618 requestAnimationFrame(onRaf);
1619 }
1620 }
1621
1622 /**
1623 * @param {number} pollInterval
1624 * @return {!Promise<*>}
1625 */
1626 function pollInterval(pollInterval) {
1627 let fulfill;
1628 const result = new Promise(x => fulfill = x);
1629 onTimeout();
1630 return result;
1631
1632 function onTimeout() {
1633 if (timedOut) {
1634 fulfill();
1635 return;
1636 }
1637 const success = predicate.apply(null, args);
1638 if (success)
1639 fulfill(success);
1640 else
1641 setTimeout(onTimeout, pollInterval);
1642 }
1643 }
1644});}
1645
1646module.exports = {FrameManager, Frame};