UNPKG

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