UNPKG

37.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} = require('./ExecutionContext');
21const {TimeoutError} = require('./Errors');
22const {NetworkManager} = require('./NetworkManager');
23const {Connection} = require('./Connection');
24
25const readFileAsync = helper.promisify(fs.readFile);
26
27class FrameManager extends EventEmitter {
28 /**
29 * @param {!Puppeteer.CDPSession} client
30 * @param {!Protocol.Page.FrameTree} frameTree
31 * @param {!Puppeteer.Page} page
32 * @param {!Puppeteer.NetworkManager} networkManager
33 */
34 constructor(client, frameTree, page, networkManager) {
35 super();
36 this._client = client;
37 this._page = page;
38 this._networkManager = networkManager;
39 this._defaultNavigationTimeout = 30000;
40 /** @type {!Map<string, !Frame>} */
41 this._frames = new Map();
42 /** @type {!Map<number, !ExecutionContext>} */
43 this._contextIdToContext = new Map();
44
45 this._client.on('Page.frameAttached', event => this._onFrameAttached(event.frameId, event.parentFrameId));
46 this._client.on('Page.frameNavigated', event => this._onFrameNavigated(event.frame));
47 this._client.on('Page.navigatedWithinDocument', event => this._onFrameNavigatedWithinDocument(event.frameId, event.url));
48 this._client.on('Page.frameDetached', event => this._onFrameDetached(event.frameId));
49 this._client.on('Page.frameStoppedLoading', event => this._onFrameStoppedLoading(event.frameId));
50 this._client.on('Runtime.executionContextCreated', event => this._onExecutionContextCreated(event.context));
51 this._client.on('Runtime.executionContextDestroyed', event => this._onExecutionContextDestroyed(event.executionContextId));
52 this._client.on('Runtime.executionContextsCleared', event => this._onExecutionContextsCleared());
53 this._client.on('Page.lifecycleEvent', event => this._onLifecycleEvent(event));
54
55 this._handleFrameTree(frameTree);
56 }
57
58 /**
59 * @param {number} timeout
60 */
61 setDefaultNavigationTimeout(timeout) {
62 this._defaultNavigationTimeout = timeout;
63 }
64
65 /**
66 * @param {!Puppeteer.Frame} frame
67 * @param {string} url
68 * @param {!Object=} options
69 * @return {!Promise<?Puppeteer.Response>}
70 */
71 async navigateFrame(frame, url, options = {}) {
72 const referrer = typeof options.referer === 'string' ? options.referer : this._networkManager.extraHTTPHeaders()['referer'];
73
74 const timeout = typeof options.timeout === 'number' ? options.timeout : this._defaultNavigationTimeout;
75 const watcher = new NavigatorWatcher(this._client, this, this._networkManager, frame, timeout, options);
76 let ensureNewDocumentNavigation = false;
77 let error = await Promise.race([
78 navigate(this._client, url, referrer, frame._id),
79 watcher.timeoutOrTerminationPromise(),
80 ]);
81 if (!error) {
82 error = await Promise.race([
83 watcher.timeoutOrTerminationPromise(),
84 ensureNewDocumentNavigation ? watcher.newDocumentNavigationPromise() : watcher.sameDocumentNavigationPromise(),
85 ]);
86 }
87 watcher.dispose();
88 if (error)
89 throw error;
90 return watcher.navigationResponse();
91
92 /**
93 * @param {!Puppeteer.CDPSession} client
94 * @param {string} url
95 * @param {string} referrer
96 * @param {string} frameId
97 * @return {!Promise<?Error>}
98 */
99 async function navigate(client, url, referrer, frameId) {
100 try {
101 const response = await client.send('Page.navigate', {url, referrer, frameId});
102 ensureNewDocumentNavigation = !!response.loaderId;
103 return response.errorText ? new Error(`${response.errorText} at ${url}`) : null;
104 } catch (error) {
105 return error;
106 }
107 }
108 }
109
110 /**
111 * @param {!Puppeteer.Frame} frame
112 * @param {!Object=} options
113 * @return {!Promise<?Puppeteer.Response>}
114 */
115 async waitForFrameNavigation(frame, options) {
116 const timeout = typeof options.timeout === 'number' ? options.timeout : this._defaultNavigationTimeout;
117 const watcher = new NavigatorWatcher(this._client, this, this._networkManager, frame, timeout, options);
118 const error = await Promise.race([
119 watcher.timeoutOrTerminationPromise(),
120 watcher.sameDocumentNavigationPromise(),
121 watcher.newDocumentNavigationPromise()
122 ]);
123 watcher.dispose();
124 if (error)
125 throw error;
126 return watcher.navigationResponse();
127 }
128
129 /**
130 * @param {!Protocol.Page.lifecycleEventPayload} event
131 */
132 _onLifecycleEvent(event) {
133 const frame = this._frames.get(event.frameId);
134 if (!frame)
135 return;
136 frame._onLifecycleEvent(event.loaderId, event.name);
137 this.emit(FrameManager.Events.LifecycleEvent, frame);
138 }
139
140 /**
141 * @param {string} frameId
142 */
143 _onFrameStoppedLoading(frameId) {
144 const frame = this._frames.get(frameId);
145 if (!frame)
146 return;
147 frame._onLoadingStopped();
148 this.emit(FrameManager.Events.LifecycleEvent, frame);
149 }
150
151 /**
152 * @param {!Protocol.Page.FrameTree} frameTree
153 */
154 _handleFrameTree(frameTree) {
155 if (frameTree.frame.parentId)
156 this._onFrameAttached(frameTree.frame.id, frameTree.frame.parentId);
157 this._onFrameNavigated(frameTree.frame);
158 if (!frameTree.childFrames)
159 return;
160
161 for (const child of frameTree.childFrames)
162 this._handleFrameTree(child);
163 }
164
165 /**
166 * @return {!Puppeteer.Page}
167 */
168 page() {
169 return this._page;
170 }
171
172 /**
173 * @return {!Frame}
174 */
175 mainFrame() {
176 return this._mainFrame;
177 }
178
179 /**
180 * @return {!Array<!Frame>}
181 */
182 frames() {
183 return Array.from(this._frames.values());
184 }
185
186 /**
187 * @param {!string} frameId
188 * @return {?Frame}
189 */
190 frame(frameId) {
191 return this._frames.get(frameId) || null;
192 }
193
194 /**
195 * @param {string} frameId
196 * @param {?string} parentFrameId
197 */
198 _onFrameAttached(frameId, parentFrameId) {
199 if (this._frames.has(frameId))
200 return;
201 assert(parentFrameId);
202 const parentFrame = this._frames.get(parentFrameId);
203 const frame = new Frame(this, this._client, parentFrame, frameId);
204 this._frames.set(frame._id, frame);
205 this.emit(FrameManager.Events.FrameAttached, frame);
206 }
207
208 /**
209 * @param {!Protocol.Page.Frame} framePayload
210 */
211 _onFrameNavigated(framePayload) {
212 const isMainFrame = !framePayload.parentId;
213 let frame = isMainFrame ? this._mainFrame : this._frames.get(framePayload.id);
214 assert(isMainFrame || frame, 'We either navigate top level or have old version of the navigated frame');
215
216 // Detach all child frames first.
217 if (frame) {
218 for (const child of frame.childFrames())
219 this._removeFramesRecursively(child);
220 }
221
222 // Update or create main frame.
223 if (isMainFrame) {
224 if (frame) {
225 // Update frame id to retain frame identity on cross-process navigation.
226 this._frames.delete(frame._id);
227 frame._id = framePayload.id;
228 } else {
229 // Initial main frame navigation.
230 frame = new Frame(this, this._client, null, framePayload.id);
231 }
232 this._frames.set(framePayload.id, frame);
233 this._mainFrame = frame;
234 }
235
236 // Update frame payload.
237 frame._navigated(framePayload);
238
239 this.emit(FrameManager.Events.FrameNavigated, frame);
240 }
241
242 /**
243 * @param {string} frameId
244 * @param {string} url
245 */
246 _onFrameNavigatedWithinDocument(frameId, url) {
247 const frame = this._frames.get(frameId);
248 if (!frame)
249 return;
250 frame._navigatedWithinDocument(url);
251 this.emit(FrameManager.Events.FrameNavigatedWithinDocument, frame);
252 this.emit(FrameManager.Events.FrameNavigated, frame);
253 }
254
255 /**
256 * @param {string} frameId
257 */
258 _onFrameDetached(frameId) {
259 const frame = this._frames.get(frameId);
260 if (frame)
261 this._removeFramesRecursively(frame);
262 }
263
264 _onExecutionContextCreated(contextPayload) {
265 const frameId = contextPayload.auxData ? contextPayload.auxData.frameId : null;
266 const frame = this._frames.get(frameId) || null;
267 /** @type {!ExecutionContext} */
268 const context = new ExecutionContext(this._client, contextPayload, frame);
269 this._contextIdToContext.set(contextPayload.id, context);
270 if (frame)
271 frame._addExecutionContext(context);
272 }
273
274 /**
275 * @param {number} executionContextId
276 */
277 _onExecutionContextDestroyed(executionContextId) {
278 const context = this._contextIdToContext.get(executionContextId);
279 if (!context)
280 return;
281 this._contextIdToContext.delete(executionContextId);
282 if (context.frame())
283 context.frame()._removeExecutionContext(context);
284 }
285
286 _onExecutionContextsCleared() {
287 for (const context of this._contextIdToContext.values()) {
288 if (context.frame())
289 context.frame()._removeExecutionContext(context);
290 }
291 this._contextIdToContext.clear();
292 }
293
294 /**
295 * @param {number} contextId
296 * @return {!ExecutionContext}
297 */
298 executionContextById(contextId) {
299 const context = this._contextIdToContext.get(contextId);
300 assert(context, 'INTERNAL ERROR: missing context with id = ' + contextId);
301 return context;
302 }
303
304 /**
305 * @param {!Frame} frame
306 */
307 _removeFramesRecursively(frame) {
308 for (const child of frame.childFrames())
309 this._removeFramesRecursively(child);
310 frame._detach();
311 this._frames.delete(frame._id);
312 this.emit(FrameManager.Events.FrameDetached, frame);
313 }
314}
315
316/** @enum {string} */
317FrameManager.Events = {
318 FrameAttached: 'frameattached',
319 FrameNavigated: 'framenavigated',
320 FrameDetached: 'framedetached',
321 LifecycleEvent: 'lifecycleevent',
322 FrameNavigatedWithinDocument: 'framenavigatedwithindocument',
323 ExecutionContextCreated: 'executioncontextcreated',
324 ExecutionContextDestroyed: 'executioncontextdestroyed',
325};
326
327/**
328 * @unrestricted
329 */
330class Frame {
331 /**
332 * @param {!FrameManager} frameManager
333 * @param {!Puppeteer.CDPSession} client
334 * @param {?Frame} parentFrame
335 * @param {string} frameId
336 */
337 constructor(frameManager, client, parentFrame, frameId) {
338 this._frameManager = frameManager;
339 this._client = client;
340 this._parentFrame = parentFrame;
341 this._url = '';
342 this._id = frameId;
343 this._detached = false;
344
345 /** @type {?Promise<!Puppeteer.ElementHandle>} */
346 this._documentPromise = null;
347 /** @type {!Promise<!ExecutionContext>} */
348 this._contextPromise;
349 this._contextResolveCallback = null;
350 this._setDefaultContext(null);
351
352
353 /** @type {!Set<!WaitTask>} */
354 this._waitTasks = new Set();
355 this._loaderId = '';
356 /** @type {!Set<string>} */
357 this._lifecycleEvents = new Set();
358
359 /** @type {!Set<!Frame>} */
360 this._childFrames = new Set();
361 if (this._parentFrame)
362 this._parentFrame._childFrames.add(this);
363 }
364
365 /**
366 * @param {!ExecutionContext} context
367 */
368 _addExecutionContext(context) {
369 if (context._isDefault)
370 this._setDefaultContext(context);
371 }
372
373 /**
374 * @param {!ExecutionContext} context
375 */
376 _removeExecutionContext(context) {
377 if (context._isDefault)
378 this._setDefaultContext(null);
379 }
380
381 /**
382 * @param {?ExecutionContext} context
383 */
384 _setDefaultContext(context) {
385 if (context) {
386 this._contextResolveCallback.call(null, context);
387 this._contextResolveCallback = null;
388 for (const waitTask of this._waitTasks)
389 waitTask.rerun();
390 } else {
391 this._documentPromise = null;
392 this._contextPromise = new Promise(fulfill => {
393 this._contextResolveCallback = fulfill;
394 });
395 }
396 }
397
398 /**
399 * @param {string} url
400 * @param {!Object=} options
401 * @return {!Promise<?Puppeteer.Response>}
402 */
403 async goto(url, options = {}) {
404 return await this._frameManager.navigateFrame(this, url, options);
405 }
406
407 /**
408 * @param {!Object=} options
409 * @return {!Promise<?Puppeteer.Response>}
410 */
411 async waitForNavigation(options = {}) {
412 return await this._frameManager.waitForFrameNavigation(this, options);
413 }
414
415 /**
416 * @return {!Promise<!ExecutionContext>}
417 */
418 executionContext() {
419 return this._contextPromise;
420 }
421
422 /**
423 * @param {function()|string} pageFunction
424 * @param {!Array<*>} args
425 * @return {!Promise<!Puppeteer.JSHandle>}
426 */
427 async evaluateHandle(pageFunction, ...args) {
428 const context = await this._contextPromise;
429 return context.evaluateHandle(pageFunction, ...args);
430 }
431
432 /**
433 * @param {Function|string} pageFunction
434 * @param {!Array<*>} args
435 * @return {!Promise<*>}
436 */
437 async evaluate(pageFunction, ...args) {
438 const context = await this._contextPromise;
439 return context.evaluate(pageFunction, ...args);
440 }
441
442 /**
443 * @param {string} selector
444 * @return {!Promise<?Puppeteer.ElementHandle>}
445 */
446 async $(selector) {
447 const document = await this._document();
448 const value = await document.$(selector);
449 return value;
450 }
451
452 /**
453 * @return {!Promise<!Puppeteer.ElementHandle>}
454 */
455 async _document() {
456 if (this._documentPromise)
457 return this._documentPromise;
458 this._documentPromise = this._contextPromise.then(async context => {
459 const document = await context.evaluateHandle('document');
460 return document.asElement();
461 });
462 return this._documentPromise;
463 }
464
465 /**
466 * @param {string} expression
467 * @return {!Promise<!Array<!Puppeteer.ElementHandle>>}
468 */
469 async $x(expression) {
470 const document = await this._document();
471 const value = await document.$x(expression);
472 return value;
473 }
474
475 /**
476 * @param {string} selector
477 * @param {Function|string} pageFunction
478 * @param {!Array<*>} args
479 * @return {!Promise<(!Object|undefined)>}
480 */
481 async $eval(selector, pageFunction, ...args) {
482 const document = await this._document();
483 return document.$eval(selector, pageFunction, ...args);
484 }
485
486 /**
487 * @param {string} selector
488 * @param {Function|string} pageFunction
489 * @param {!Array<*>} args
490 * @return {!Promise<(!Object|undefined)>}
491 */
492 async $$eval(selector, pageFunction, ...args) {
493 const document = await this._document();
494 const value = await document.$$eval(selector, pageFunction, ...args);
495 return value;
496 }
497
498 /**
499 * @param {string} selector
500 * @return {!Promise<!Array<!Puppeteer.ElementHandle>>}
501 */
502 async $$(selector) {
503 const document = await this._document();
504 const value = await document.$$(selector);
505 return value;
506 }
507
508 /**
509 * @return {!Promise<String>}
510 */
511 async content() {
512 return await this.evaluate(() => {
513 let retVal = '';
514 if (document.doctype)
515 retVal = new XMLSerializer().serializeToString(document.doctype);
516 if (document.documentElement)
517 retVal += document.documentElement.outerHTML;
518 return retVal;
519 });
520 }
521
522 /**
523 * @param {string} html
524 */
525 async setContent(html) {
526 await this.evaluate(html => {
527 document.open();
528 document.write(html);
529 document.close();
530 }, html);
531 }
532
533 /**
534 * @return {string}
535 */
536 name() {
537 return this._name || '';
538 }
539
540 /**
541 * @return {string}
542 */
543 url() {
544 return this._url;
545 }
546
547 /**
548 * @return {?Frame}
549 */
550 parentFrame() {
551 return this._parentFrame;
552 }
553
554 /**
555 * @return {!Array.<!Frame>}
556 */
557 childFrames() {
558 return Array.from(this._childFrames);
559 }
560
561 /**
562 * @return {boolean}
563 */
564 isDetached() {
565 return this._detached;
566 }
567
568 /**
569 * @param {Object} options
570 * @return {!Promise<!Puppeteer.ElementHandle>}
571 */
572 async addScriptTag(options) {
573 if (typeof options.url === 'string') {
574 const url = options.url;
575 try {
576 const context = await this._contextPromise;
577 return (await context.evaluateHandle(addScriptUrl, url, options.type)).asElement();
578 } catch (error) {
579 throw new Error(`Loading script from ${url} failed`);
580 }
581 }
582
583 if (typeof options.path === 'string') {
584 let contents = await readFileAsync(options.path, 'utf8');
585 contents += '//# sourceURL=' + options.path.replace(/\n/g, '');
586 const context = await this._contextPromise;
587 return (await context.evaluateHandle(addScriptContent, contents, options.type)).asElement();
588 }
589
590 if (typeof options.content === 'string') {
591 const context = await this._contextPromise;
592 return (await context.evaluateHandle(addScriptContent, options.content, options.type)).asElement();
593 }
594
595 throw new Error('Provide an object with a `url`, `path` or `content` property');
596
597 /**
598 * @param {string} url
599 * @param {string} type
600 * @return {!Promise<!HTMLElement>}
601 */
602 async function addScriptUrl(url, type) {
603 const script = document.createElement('script');
604 script.src = url;
605 if (type)
606 script.type = type;
607 const promise = new Promise((res, rej) => {
608 script.onload = res;
609 script.onerror = rej;
610 });
611 document.head.appendChild(script);
612 await promise;
613 return script;
614 }
615
616 /**
617 * @param {string} content
618 * @param {string} type
619 * @return {!HTMLElement}
620 */
621 function addScriptContent(content, type = 'text/javascript') {
622 const script = document.createElement('script');
623 script.type = type;
624 script.text = content;
625 let error = null;
626 script.onerror = e => error = e;
627 document.head.appendChild(script);
628 if (error)
629 throw error;
630 return script;
631 }
632 }
633
634 /**
635 * @param {Object} options
636 * @return {!Promise<!Puppeteer.ElementHandle>}
637 */
638 async addStyleTag(options) {
639 if (typeof options.url === 'string') {
640 const url = options.url;
641 try {
642 const context = await this._contextPromise;
643 return (await context.evaluateHandle(addStyleUrl, url)).asElement();
644 } catch (error) {
645 throw new Error(`Loading style from ${url} failed`);
646 }
647 }
648
649 if (typeof options.path === 'string') {
650 let contents = await readFileAsync(options.path, 'utf8');
651 contents += '/*# sourceURL=' + options.path.replace(/\n/g, '') + '*/';
652 const context = await this._contextPromise;
653 return (await context.evaluateHandle(addStyleContent, contents)).asElement();
654 }
655
656 if (typeof options.content === 'string') {
657 const context = await this._contextPromise;
658 return (await context.evaluateHandle(addStyleContent, options.content)).asElement();
659 }
660
661 throw new Error('Provide an object with a `url`, `path` or `content` property');
662
663 /**
664 * @param {string} url
665 * @return {!Promise<!HTMLElement>}
666 */
667 async function addStyleUrl(url) {
668 const link = document.createElement('link');
669 link.rel = 'stylesheet';
670 link.href = url;
671 const promise = new Promise((res, rej) => {
672 link.onload = res;
673 link.onerror = rej;
674 });
675 document.head.appendChild(link);
676 await promise;
677 return link;
678 }
679
680 /**
681 * @param {string} content
682 * @return {!Promise<!HTMLElement>}
683 */
684 async function addStyleContent(content) {
685 const style = document.createElement('style');
686 style.type = 'text/css';
687 style.appendChild(document.createTextNode(content));
688 const promise = new Promise((res, rej) => {
689 style.onload = res;
690 style.onerror = rej;
691 });
692 document.head.appendChild(style);
693 await promise;
694 return style;
695 }
696 }
697
698 /**
699 * @param {string} selector
700 * @param {!Object=} options
701 */
702 async click(selector, options = {}) {
703 const handle = await this.$(selector);
704 assert(handle, 'No node found for selector: ' + selector);
705 await handle.click(options);
706 await handle.dispose();
707 }
708
709 /**
710 * @param {string} selector
711 */
712 async focus(selector) {
713 const handle = await this.$(selector);
714 assert(handle, 'No node found for selector: ' + selector);
715 await handle.focus();
716 await handle.dispose();
717 }
718
719 /**
720 * @param {string} selector
721 */
722 async hover(selector) {
723 const handle = await this.$(selector);
724 assert(handle, 'No node found for selector: ' + selector);
725 await handle.hover();
726 await handle.dispose();
727 }
728
729 /**
730 * @param {string} selector
731 * @param {!Array<string>} values
732 * @return {!Promise<!Array<string>>}
733 */
734 select(selector, ...values){
735 for (const value of values)
736 assert(helper.isString(value), 'Values must be strings. Found value "' + value + '" of type "' + (typeof value) + '"');
737 return this.$eval(selector, (element, values) => {
738 if (element.nodeName.toLowerCase() !== 'select')
739 throw new Error('Element is not a <select> element.');
740
741 const options = Array.from(element.options);
742 element.value = undefined;
743 for (const option of options) {
744 option.selected = values.includes(option.value);
745 if (option.selected && !element.multiple)
746 break;
747 }
748 element.dispatchEvent(new Event('input', { 'bubbles': true }));
749 element.dispatchEvent(new Event('change', { 'bubbles': true }));
750 return options.filter(option => option.selected).map(option => option.value);
751 }, values);
752 }
753
754 /**
755 * @param {string} selector
756 */
757 async tap(selector) {
758 const handle = await this.$(selector);
759 assert(handle, 'No node found for selector: ' + selector);
760 await handle.tap();
761 await handle.dispose();
762 }
763
764 /**
765 * @param {string} selector
766 * @param {string} text
767 * @param {{delay: (number|undefined)}=} options
768 */
769 async type(selector, text, options) {
770 const handle = await this.$(selector);
771 assert(handle, 'No node found for selector: ' + selector);
772 await handle.type(text, options);
773 await handle.dispose();
774 }
775
776 /**
777 * @param {(string|number|Function)} selectorOrFunctionOrTimeout
778 * @param {!Object=} options
779 * @param {!Array<*>} args
780 * @return {!Promise}
781 */
782 waitFor(selectorOrFunctionOrTimeout, options = {}, ...args) {
783 const xPathPattern = '//';
784
785 if (helper.isString(selectorOrFunctionOrTimeout)) {
786 const string = /** @type {string} */ (selectorOrFunctionOrTimeout);
787 if (string.startsWith(xPathPattern))
788 return this.waitForXPath(string, options);
789 return this.waitForSelector(string, options);
790 }
791 if (helper.isNumber(selectorOrFunctionOrTimeout))
792 return new Promise(fulfill => setTimeout(fulfill, /** @type {number} */ (selectorOrFunctionOrTimeout)));
793 if (typeof selectorOrFunctionOrTimeout === 'function')
794 return this.waitForFunction(selectorOrFunctionOrTimeout, options, ...args);
795 return Promise.reject(new Error('Unsupported target type: ' + (typeof selectorOrFunctionOrTimeout)));
796 }
797
798 /**
799 * @param {string} selector
800 * @param {!Object=} options
801 * @return {!Promise}
802 */
803 waitForSelector(selector, options = {}) {
804 return this._waitForSelectorOrXPath(selector, false, options);
805 }
806
807 /**
808 * @param {string} xpath
809 * @param {!Object=} options
810 * @return {!Promise}
811 */
812 waitForXPath(xpath, options = {}) {
813 return this._waitForSelectorOrXPath(xpath, true, options);
814 }
815
816 /**
817 * @param {Function|string} pageFunction
818 * @param {!Object=} options
819 * @return {!Promise}
820 */
821 waitForFunction(pageFunction, options = {}, ...args) {
822 const timeout = helper.isNumber(options.timeout) ? options.timeout : 30000;
823 const polling = options.polling || 'raf';
824 return new WaitTask(this, pageFunction, 'function', polling, timeout, ...args).promise;
825 }
826
827 /**
828 * @return {!Promise<string>}
829 */
830 async title() {
831 return this.evaluate(() => document.title);
832 }
833
834 /**
835 * @param {string} selectorOrXPath
836 * @param {boolean} isXPath
837 * @param {!Object=} options
838 * @return {!Promise}
839 */
840 _waitForSelectorOrXPath(selectorOrXPath, isXPath, options = {}) {
841 const waitForVisible = !!options.visible;
842 const waitForHidden = !!options.hidden;
843 const polling = waitForVisible || waitForHidden ? 'raf' : 'mutation';
844 const timeout = helper.isNumber(options.timeout) ? options.timeout : 30000;
845 const title = `${isXPath ? 'XPath' : 'selector'} "${selectorOrXPath}"${waitForHidden ? ' to be hidden' : ''}`;
846 return new WaitTask(this, predicate, title, polling, timeout, selectorOrXPath, isXPath, waitForVisible, waitForHidden).promise;
847
848 /**
849 * @param {string} selectorOrXPath
850 * @param {boolean} isXPath
851 * @param {boolean} waitForVisible
852 * @param {boolean} waitForHidden
853 * @return {?Node|boolean}
854 */
855 function predicate(selectorOrXPath, isXPath, waitForVisible, waitForHidden) {
856 const node = isXPath
857 ? document.evaluate(selectorOrXPath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue
858 : document.querySelector(selectorOrXPath);
859 if (!node)
860 return waitForHidden;
861 if (!waitForVisible && !waitForHidden)
862 return node;
863 const element = /** @type {Element} */ (node.nodeType === Node.TEXT_NODE ? node.parentElement : node);
864
865 const style = window.getComputedStyle(element);
866 const isVisible = style && style.visibility !== 'hidden' && hasVisibleBoundingBox();
867 const success = (waitForVisible === isVisible || waitForHidden === !isVisible);
868 return success ? node : null;
869
870 /**
871 * @return {boolean}
872 */
873 function hasVisibleBoundingBox() {
874 const rect = element.getBoundingClientRect();
875 return !!(rect.top || rect.bottom || rect.width || rect.height);
876 }
877 }
878 }
879
880 /**
881 * @param {!Protocol.Page.Frame} framePayload
882 */
883 _navigated(framePayload) {
884 this._name = framePayload.name;
885 // TODO(lushnikov): remove this once requestInterception has loaderId exposed.
886 this._navigationURL = framePayload.url;
887 this._url = framePayload.url;
888 }
889
890 /**
891 * @param {string} url
892 */
893 _navigatedWithinDocument(url) {
894 this._url = url;
895 }
896
897 /**
898 * @param {string} loaderId
899 * @param {string} name
900 */
901 _onLifecycleEvent(loaderId, name) {
902 if (name === 'init') {
903 this._loaderId = loaderId;
904 this._lifecycleEvents.clear();
905 }
906 this._lifecycleEvents.add(name);
907 }
908
909 _onLoadingStopped() {
910 this._lifecycleEvents.add('DOMContentLoaded');
911 this._lifecycleEvents.add('load');
912 }
913
914 _detach() {
915 for (const waitTask of this._waitTasks)
916 waitTask.terminate(new Error('waitForFunction failed: frame got detached.'));
917 this._detached = true;
918 if (this._parentFrame)
919 this._parentFrame._childFrames.delete(this);
920 this._parentFrame = null;
921 }
922}
923helper.tracePublicAPI(Frame);
924
925class WaitTask {
926 /**
927 * @param {!Frame} frame
928 * @param {Function|string} predicateBody
929 * @param {string|number} polling
930 * @param {number} timeout
931 * @param {!Array<*>} args
932 */
933 constructor(frame, predicateBody, title, polling, timeout, ...args) {
934 if (helper.isString(polling))
935 assert(polling === 'raf' || polling === 'mutation', 'Unknown polling option: ' + polling);
936 else if (helper.isNumber(polling))
937 assert(polling > 0, 'Cannot poll with non-positive interval: ' + polling);
938 else
939 throw new Error('Unknown polling options: ' + polling);
940
941 this._frame = frame;
942 this._polling = polling;
943 this._timeout = timeout;
944 this._predicateBody = helper.isString(predicateBody) ? 'return ' + predicateBody : 'return (' + predicateBody + ')(...args)';
945 this._args = args;
946 this._runCount = 0;
947 frame._waitTasks.add(this);
948 this.promise = new Promise((resolve, reject) => {
949 this._resolve = resolve;
950 this._reject = reject;
951 });
952 // Since page navigation requires us to re-install the pageScript, we should track
953 // timeout on our end.
954 if (timeout) {
955 const timeoutError = new TimeoutError(`waiting for ${title} failed: timeout ${timeout}ms exceeded`);
956 this._timeoutTimer = setTimeout(() => this.terminate(timeoutError), timeout);
957 }
958 this.rerun();
959 }
960
961 /**
962 * @param {!Error} error
963 */
964 terminate(error) {
965 this._terminated = true;
966 this._reject(error);
967 this._cleanup();
968 }
969
970 async rerun() {
971 const runCount = ++this._runCount;
972 /** @type {?Puppeteer.JSHandle} */
973 let success = null;
974 let error = null;
975 try {
976 success = await (await this._frame.executionContext()).evaluateHandle(waitForPredicatePageFunction, this._predicateBody, this._polling, this._timeout, ...this._args);
977 } catch (e) {
978 error = e;
979 }
980
981 if (this._terminated || runCount !== this._runCount) {
982 if (success)
983 await success.dispose();
984 return;
985 }
986
987 // Ignore timeouts in pageScript - we track timeouts ourselves.
988 // If the frame's execution context has already changed, `frame.evaluate` will
989 // throw an error - ignore this predicate run altogether.
990 if (!error && await this._frame.evaluate(s => !s, success).catch(e => true)) {
991 await success.dispose();
992 return;
993 }
994
995 // When the page is navigated, the promise is rejected.
996 // We will try again in the new execution context.
997 if (error && error.message.includes('Execution context was destroyed'))
998 return;
999
1000 // We could have tried to evaluate in a context which was already
1001 // destroyed.
1002 if (error && error.message.includes('Cannot find context with specified id'))
1003 return;
1004
1005 if (error)
1006 this._reject(error);
1007 else
1008 this._resolve(success);
1009
1010 this._cleanup();
1011 }
1012
1013 _cleanup() {
1014 clearTimeout(this._timeoutTimer);
1015 this._frame._waitTasks.delete(this);
1016 this._runningTask = null;
1017 }
1018}
1019
1020/**
1021 * @param {string} predicateBody
1022 * @param {string} polling
1023 * @param {number} timeout
1024 * @return {!Promise<*>}
1025 */
1026async function waitForPredicatePageFunction(predicateBody, polling, timeout, ...args) {
1027 const predicate = new Function('...args', predicateBody);
1028 let timedOut = false;
1029 if (timeout)
1030 setTimeout(() => timedOut = true, timeout);
1031 if (polling === 'raf')
1032 return await pollRaf();
1033 if (polling === 'mutation')
1034 return await pollMutation();
1035 if (typeof polling === 'number')
1036 return await pollInterval(polling);
1037
1038 /**
1039 * @return {!Promise<*>}
1040 */
1041 function pollMutation() {
1042 const success = predicate.apply(null, args);
1043 if (success)
1044 return Promise.resolve(success);
1045
1046 let fulfill;
1047 const result = new Promise(x => fulfill = x);
1048 const observer = new MutationObserver(mutations => {
1049 if (timedOut) {
1050 observer.disconnect();
1051 fulfill();
1052 }
1053 const success = predicate.apply(null, args);
1054 if (success) {
1055 observer.disconnect();
1056 fulfill(success);
1057 }
1058 });
1059 observer.observe(document, {
1060 childList: true,
1061 subtree: true,
1062 attributes: true
1063 });
1064 return result;
1065 }
1066
1067 /**
1068 * @return {!Promise<*>}
1069 */
1070 function pollRaf() {
1071 let fulfill;
1072 const result = new Promise(x => fulfill = x);
1073 onRaf();
1074 return result;
1075
1076 function onRaf() {
1077 if (timedOut) {
1078 fulfill();
1079 return;
1080 }
1081 const success = predicate.apply(null, args);
1082 if (success)
1083 fulfill(success);
1084 else
1085 requestAnimationFrame(onRaf);
1086 }
1087 }
1088
1089 /**
1090 * @param {number} pollInterval
1091 * @return {!Promise<*>}
1092 */
1093 function pollInterval(pollInterval) {
1094 let fulfill;
1095 const result = new Promise(x => fulfill = x);
1096 onTimeout();
1097 return result;
1098
1099 function onTimeout() {
1100 if (timedOut) {
1101 fulfill();
1102 return;
1103 }
1104 const success = predicate.apply(null, args);
1105 if (success)
1106 fulfill(success);
1107 else
1108 setTimeout(onTimeout, pollInterval);
1109 }
1110 }
1111}
1112
1113class NavigatorWatcher {
1114 /**
1115 * @param {!Puppeteer.CDPSession} client
1116 * @param {!FrameManager} frameManager
1117 * @param {!NetworkManager} networkManager
1118 * @param {!Puppeteer.Frame} frame
1119 * @param {number} timeout
1120 * @param {!Object=} options
1121 */
1122 constructor(client, frameManager, networkManager, frame, timeout, options = {}) {
1123 assert(options.networkIdleTimeout === undefined, 'ERROR: networkIdleTimeout option is no longer supported.');
1124 assert(options.networkIdleInflight === undefined, 'ERROR: networkIdleInflight option is no longer supported.');
1125 assert(options.waitUntil !== 'networkidle', 'ERROR: "networkidle" option is no longer supported. Use "networkidle2" instead');
1126 let waitUntil = ['load'];
1127 if (Array.isArray(options.waitUntil))
1128 waitUntil = options.waitUntil.slice();
1129 else if (typeof options.waitUntil === 'string')
1130 waitUntil = [options.waitUntil];
1131 this._expectedLifecycle = waitUntil.map(value => {
1132 const protocolEvent = puppeteerToProtocolLifecycle[value];
1133 assert(protocolEvent, 'Unknown value for options.waitUntil: ' + value);
1134 return protocolEvent;
1135 });
1136
1137 this._frameManager = frameManager;
1138 this._networkManager = networkManager;
1139 this._frame = frame;
1140 this._initialLoaderId = frame._loaderId;
1141 this._timeout = timeout;
1142 /** @type {?Puppeteer.Request} */
1143 this._navigationRequest = null;
1144 this._hasSameDocumentNavigation = false;
1145 this._eventListeners = [
1146 helper.addEventListener(Connection.fromSession(client), Connection.Events.Disconnected, () => this._terminate(new Error('Navigation failed because browser has disconnected!'))),
1147 helper.addEventListener(this._frameManager, FrameManager.Events.LifecycleEvent, this._checkLifecycleComplete.bind(this)),
1148 helper.addEventListener(this._frameManager, FrameManager.Events.FrameNavigatedWithinDocument, this._navigatedWithinDocument.bind(this)),
1149 helper.addEventListener(this._frameManager, FrameManager.Events.FrameDetached, this._onFrameDetached.bind(this)),
1150 helper.addEventListener(this._networkManager, NetworkManager.Events.Request, this._onRequest.bind(this)),
1151 ];
1152
1153 this._sameDocumentNavigationPromise = new Promise(fulfill => {
1154 this._sameDocumentNavigationCompleteCallback = fulfill;
1155 });
1156
1157 this._newDocumentNavigationPromise = new Promise(fulfill => {
1158 this._newDocumentNavigationCompleteCallback = fulfill;
1159 });
1160
1161 this._timeoutPromise = this._createTimeoutPromise();
1162 this._terminationPromise = new Promise(fulfill => {
1163 this._terminationCallback = fulfill;
1164 });
1165 }
1166
1167 /**
1168 * @param {!Puppeteer.Request} request
1169 */
1170 _onRequest(request) {
1171 if (request.frame() !== this._frame || !request.isNavigationRequest())
1172 return;
1173 this._navigationRequest = request;
1174 }
1175
1176 /**
1177 * @param {!Puppeteer.Frame} frame
1178 */
1179 _onFrameDetached(frame) {
1180 if (this._frame === frame) {
1181 this._terminationCallback.call(null, new Error('Navigating frame was detached'));
1182 return;
1183 }
1184 this._checkLifecycleComplete();
1185 }
1186
1187 /**
1188 * @return {?Puppeteer.Response}
1189 */
1190 navigationResponse() {
1191 return this._navigationRequest ? this._navigationRequest.response() : null;
1192 }
1193
1194 /**
1195 * @param {!Error} error
1196 */
1197 _terminate(error) {
1198 this._terminationCallback.call(null, error);
1199 }
1200
1201 /**
1202 * @return {!Promise<?Error>}
1203 */
1204 sameDocumentNavigationPromise() {
1205 return this._sameDocumentNavigationPromise;
1206 }
1207
1208 /**
1209 * @return {!Promise<?Error>}
1210 */
1211 newDocumentNavigationPromise() {
1212 return this._newDocumentNavigationPromise;
1213 }
1214
1215 /**
1216 * @return {!Promise<?Error>}
1217 */
1218 timeoutOrTerminationPromise() {
1219 return Promise.race([this._timeoutPromise, this._terminationPromise]);
1220 }
1221
1222 /**
1223 * @return {!Promise<?Error>}
1224 */
1225 _createTimeoutPromise() {
1226 if (!this._timeout)
1227 return new Promise(() => {});
1228 const errorMessage = 'Navigation Timeout Exceeded: ' + this._timeout + 'ms exceeded';
1229 return new Promise(fulfill => this._maximumTimer = setTimeout(fulfill, this._timeout))
1230 .then(() => new TimeoutError(errorMessage));
1231 }
1232
1233 /**
1234 * @param {!Puppeteer.Frame} frame
1235 */
1236 _navigatedWithinDocument(frame) {
1237 if (frame !== this._frame)
1238 return;
1239 this._hasSameDocumentNavigation = true;
1240 this._checkLifecycleComplete();
1241 }
1242
1243 _checkLifecycleComplete() {
1244 // We expect navigation to commit.
1245 if (this._frame._loaderId === this._initialLoaderId && !this._hasSameDocumentNavigation)
1246 return;
1247 if (!checkLifecycle(this._frame, this._expectedLifecycle))
1248 return;
1249 if (this._hasSameDocumentNavigation)
1250 this._sameDocumentNavigationCompleteCallback();
1251 if (this._frame._loaderId !== this._initialLoaderId)
1252 this._newDocumentNavigationCompleteCallback();
1253
1254 /**
1255 * @param {!Puppeteer.Frame} frame
1256 * @param {!Array<string>} expectedLifecycle
1257 * @return {boolean}
1258 */
1259 function checkLifecycle(frame, expectedLifecycle) {
1260 for (const event of expectedLifecycle) {
1261 if (!frame._lifecycleEvents.has(event))
1262 return false;
1263 }
1264 for (const child of frame.childFrames()) {
1265 if (!checkLifecycle(child, expectedLifecycle))
1266 return false;
1267 }
1268 return true;
1269 }
1270 }
1271
1272 dispose() {
1273 helper.removeEventListeners(this._eventListeners);
1274 clearTimeout(this._maximumTimer);
1275 }
1276}
1277
1278const puppeteerToProtocolLifecycle = {
1279 'load': 'load',
1280 'domcontentloaded': 'DOMContentLoaded',
1281 'networkidle0': 'networkIdle',
1282 'networkidle2': 'networkAlmostIdle',
1283};
1284
1285module.exports = {FrameManager, Frame};