UNPKG

29 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');
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 {!Puppeteer.Page}
91 */
92 page() {
93 return this._page;
94 }
95
96 /**
97 * @return {!Frame}
98 */
99 mainFrame() {
100 return this._mainFrame;
101 }
102
103 /**
104 * @return {!Array<!Frame>}
105 */
106 frames() {
107 return Array.from(this._frames.values());
108 }
109
110 /**
111 * @param {!string} frameId
112 * @return {?Frame}
113 */
114 frame(frameId) {
115 return this._frames.get(frameId) || null;
116 }
117
118 /**
119 * @param {string} frameId
120 * @param {?string} parentFrameId
121 * @return {?Frame}
122 */
123 _onFrameAttached(frameId, parentFrameId) {
124 if (this._frames.has(frameId))
125 return;
126 assert(parentFrameId);
127 const parentFrame = this._frames.get(parentFrameId);
128 const frame = new Frame(this, this._client, parentFrame, frameId);
129 this._frames.set(frame._id, frame);
130 this.emit(FrameManager.Events.FrameAttached, frame);
131 }
132
133 /**
134 * @param {!Protocol.Page.Frame} framePayload
135 */
136 _onFrameNavigated(framePayload) {
137 const isMainFrame = !framePayload.parentId;
138 let frame = isMainFrame ? this._mainFrame : this._frames.get(framePayload.id);
139 assert(isMainFrame || frame, 'We either navigate top level or have old version of the navigated frame');
140
141 // Detach all child frames first.
142 if (frame) {
143 for (const child of frame.childFrames())
144 this._removeFramesRecursively(child);
145 }
146
147 // Update or create main frame.
148 if (isMainFrame) {
149 if (frame) {
150 // Update frame id to retain frame identity on cross-process navigation.
151 this._frames.delete(frame._id);
152 frame._id = framePayload.id;
153 } else {
154 // Initial main frame navigation.
155 frame = new Frame(this, this._client, null, framePayload.id);
156 }
157 this._frames.set(framePayload.id, frame);
158 this._mainFrame = frame;
159 }
160
161 // Update frame payload.
162 frame._navigated(framePayload);
163
164 this.emit(FrameManager.Events.FrameNavigated, frame);
165 }
166
167 /**
168 * @param {string} frameId
169 * @param {string} url
170 */
171 _onFrameNavigatedWithinDocument(frameId, url) {
172 const frame = this._frames.get(frameId);
173 if (!frame)
174 return;
175 frame._navigatedWithinDocument(url);
176 this.emit(FrameManager.Events.FrameNavigatedWithinDocument, frame);
177 this.emit(FrameManager.Events.FrameNavigated, frame);
178 }
179
180 /**
181 * @param {string} frameId
182 */
183 _onFrameDetached(frameId) {
184 const frame = this._frames.get(frameId);
185 if (frame)
186 this._removeFramesRecursively(frame);
187 }
188
189 _onExecutionContextCreated(contextPayload) {
190 const frameId = contextPayload.auxData ? contextPayload.auxData.frameId : null;
191 const frame = this._frames.get(frameId) || null;
192 /** @type {!ExecutionContext} */
193 const context = new ExecutionContext(this._client, contextPayload, frame);
194 this._contextIdToContext.set(contextPayload.id, context);
195 if (frame)
196 frame._addExecutionContext(context);
197 }
198
199 /**
200 * @param {number} executionContextId
201 */
202 _onExecutionContextDestroyed(executionContextId) {
203 const context = this._contextIdToContext.get(executionContextId);
204 if (!context)
205 return;
206 this._contextIdToContext.delete(executionContextId);
207 if (context.frame())
208 context.frame()._removeExecutionContext(context);
209 }
210
211 _onExecutionContextsCleared() {
212 for (const context of this._contextIdToContext.values()) {
213 if (context.frame())
214 context.frame()._removeExecutionContext(context);
215 }
216 this._contextIdToContext.clear();
217 }
218
219 /**
220 * @param {number} contextId
221 * @return {!ExecutionContext}
222 */
223 executionContextById(contextId) {
224 const context = this._contextIdToContext.get(contextId);
225 assert(context, 'INTERNAL ERROR: missing context with id = ' + contextId);
226 return context;
227 }
228
229 /**
230 * @param {!Frame} frame
231 */
232 _removeFramesRecursively(frame) {
233 for (const child of frame.childFrames())
234 this._removeFramesRecursively(child);
235 frame._detach();
236 this._frames.delete(frame._id);
237 this.emit(FrameManager.Events.FrameDetached, frame);
238 }
239}
240
241/** @enum {string} */
242FrameManager.Events = {
243 FrameAttached: 'frameattached',
244 FrameNavigated: 'framenavigated',
245 FrameDetached: 'framedetached',
246 LifecycleEvent: 'lifecycleevent',
247 FrameNavigatedWithinDocument: 'framenavigatedwithindocument',
248 ExecutionContextCreated: 'executioncontextcreated',
249 ExecutionContextDestroyed: 'executioncontextdestroyed',
250};
251
252/**
253 * @unrestricted
254 */
255class Frame {
256 /**
257 * @param {!FrameManager} frameManager
258 * @param {!Puppeteer.CDPSession} client
259 * @param {?Frame} parentFrame
260 * @param {string} frameId
261 */
262 constructor(frameManager, client, parentFrame, frameId) {
263 this._frameManager = frameManager;
264 this._client = client;
265 this._parentFrame = parentFrame;
266 this._url = '';
267 this._id = frameId;
268
269 /** @type {?Promise<!Puppeteer.ElementHandle>} */
270 this._documentPromise = null;
271 /** @type {?Promise<!ExecutionContext>} */
272 this._contextPromise = null;
273 this._contextResolveCallback = null;
274 this._setDefaultContext(null);
275
276 /** @type {!Set<!WaitTask>} */
277 this._waitTasks = new Set();
278 this._loaderId = '';
279 /** @type {!Set<string>} */
280 this._lifecycleEvents = new Set();
281
282 /** @type {!Set<!Frame>} */
283 this._childFrames = new Set();
284 if (this._parentFrame)
285 this._parentFrame._childFrames.add(this);
286 }
287
288 /**
289 * @param {!ExecutionContext} context
290 */
291 _addExecutionContext(context) {
292 if (context._isDefault)
293 this._setDefaultContext(context);
294 }
295
296 /**
297 * @param {!ExecutionContext} context
298 */
299 _removeExecutionContext(context) {
300 if (context._isDefault)
301 this._setDefaultContext(null);
302 }
303
304 /**
305 * @param {?ExecutionContext} context
306 */
307 _setDefaultContext(context) {
308 if (context) {
309 this._contextResolveCallback.call(null, context);
310 this._contextResolveCallback = null;
311 for (const waitTask of this._waitTasks)
312 waitTask.rerun();
313 } else {
314 this._documentPromise = null;
315 this._contextPromise = new Promise(fulfill => {
316 this._contextResolveCallback = fulfill;
317 });
318 }
319 }
320
321 /**
322 * @return {!Promise<!ExecutionContext>}
323 */
324 executionContext() {
325 return this._contextPromise;
326 }
327
328 /**
329 * @param {function()|string} pageFunction
330 * @param {!Array<*>} args
331 * @return {!Promise<!Puppeteer.JSHandle>}
332 */
333 async evaluateHandle(pageFunction, ...args) {
334 const context = await this._contextPromise;
335 return context.evaluateHandle(pageFunction, ...args);
336 }
337
338 /**
339 * @param {Function|string} pageFunction
340 * @param {!Array<*>} args
341 * @return {!Promise<*>}
342 */
343 async evaluate(pageFunction, ...args) {
344 const context = await this._contextPromise;
345 return context.evaluate(pageFunction, ...args);
346 }
347
348 /**
349 * @param {string} selector
350 * @return {!Promise<?Puppeteer.ElementHandle>}
351 */
352 async $(selector) {
353 const document = await this._document();
354 const value = await document.$(selector);
355 return value;
356 }
357
358 /**
359 * @return {!Promise<!Puppeteer.ElementHandle>}
360 */
361 async _document() {
362 if (this._documentPromise)
363 return this._documentPromise;
364 this._documentPromise = this._contextPromise.then(async context => {
365 const document = await context.evaluateHandle('document');
366 return document.asElement();
367 });
368 return this._documentPromise;
369 }
370
371 /**
372 * @param {string} expression
373 * @return {!Promise<!Array<!Puppeteer.ElementHandle>>}
374 */
375 async $x(expression) {
376 const document = await this._document();
377 const value = await document.$x(expression);
378 return value;
379 }
380
381 /**
382 * @param {string} selector
383 * @param {Function|string} pageFunction
384 * @param {!Array<*>} args
385 * @return {!Promise<(!Object|undefined)>}
386 */
387 async $eval(selector, pageFunction, ...args) {
388 const document = await this._document();
389 return document.$eval(selector, pageFunction, ...args);
390 }
391
392 /**
393 * @param {string} selector
394 * @param {Function|string} pageFunction
395 * @param {!Array<*>} args
396 * @return {!Promise<(!Object|undefined)>}
397 */
398 async $$eval(selector, pageFunction, ...args) {
399 const document = await this._document();
400 const value = await document.$$eval(selector, pageFunction, ...args);
401 return value;
402 }
403
404 /**
405 * @param {string} selector
406 * @return {!Promise<!Array<!Puppeteer.ElementHandle>>}
407 */
408 async $$(selector) {
409 const document = await this._document();
410 const value = await document.$$(selector);
411 return value;
412 }
413
414 /**
415 * @return {!Promise<String>}
416 */
417 async content() {
418 return await this.evaluate(() => {
419 let retVal = '';
420 if (document.doctype)
421 retVal = new XMLSerializer().serializeToString(document.doctype);
422 if (document.documentElement)
423 retVal += document.documentElement.outerHTML;
424 return retVal;
425 });
426 }
427
428 /**
429 * @param {string} html
430 */
431 async setContent(html) {
432 await this.evaluate(html => {
433 document.open();
434 document.write(html);
435 document.close();
436 }, html);
437 }
438
439 /**
440 * @return {string}
441 */
442 name() {
443 return this._name || '';
444 }
445
446 /**
447 * @return {string}
448 */
449 url() {
450 return this._url;
451 }
452
453 /**
454 * @return {?Frame}
455 */
456 parentFrame() {
457 return this._parentFrame;
458 }
459
460 /**
461 * @return {!Array.<!Frame>}
462 */
463 childFrames() {
464 return Array.from(this._childFrames);
465 }
466
467 /**
468 * @return {boolean}
469 */
470 isDetached() {
471 return this._detached;
472 }
473
474 /**
475 * @param {Object} options
476 * @return {!Promise<!Puppeteer.ElementHandle>}
477 */
478 async addScriptTag(options) {
479 if (typeof options.url === 'string') {
480 const url = options.url;
481 try {
482 const context = await this._contextPromise;
483 return (await context.evaluateHandle(addScriptUrl, url, options.type)).asElement();
484 } catch (error) {
485 throw new Error(`Loading script from ${url} failed`);
486 }
487 }
488
489 if (typeof options.path === 'string') {
490 let contents = await readFileAsync(options.path, 'utf8');
491 contents += '//# sourceURL=' + options.path.replace(/\n/g, '');
492 const context = await this._contextPromise;
493 return (await context.evaluateHandle(addScriptContent, contents, options.type)).asElement();
494 }
495
496 if (typeof options.content === 'string') {
497 const context = await this._contextPromise;
498 return (await context.evaluateHandle(addScriptContent, options.content, options.type)).asElement();
499 }
500
501 throw new Error('Provide an object with a `url`, `path` or `content` property');
502
503 /**
504 * @param {string} url
505 * @param {string} type
506 * @return {!Promise<!HTMLElement>}
507 */
508 async function addScriptUrl(url, type) {
509 const script = document.createElement('script');
510 script.src = url;
511 if (type)
512 script.type = type;
513 const promise = new Promise((res, rej) => {
514 script.onload = res;
515 script.onerror = rej;
516 });
517 document.head.appendChild(script);
518 await promise;
519 return script;
520 }
521
522 /**
523 * @param {string} content
524 * @param {string} type
525 * @return {!HTMLElement}
526 */
527 function addScriptContent(content, type = 'text/javascript') {
528 const script = document.createElement('script');
529 script.type = type;
530 script.text = content;
531 let error = null;
532 script.onerror = e => error = e;
533 document.head.appendChild(script);
534 if (error)
535 throw error;
536 return script;
537 }
538 }
539
540 /**
541 * @param {Object} options
542 * @return {!Promise<!Puppeteer.ElementHandle>}
543 */
544 async addStyleTag(options) {
545 if (typeof options.url === 'string') {
546 const url = options.url;
547 try {
548 const context = await this._contextPromise;
549 return (await context.evaluateHandle(addStyleUrl, url)).asElement();
550 } catch (error) {
551 throw new Error(`Loading style from ${url} failed`);
552 }
553 }
554
555 if (typeof options.path === 'string') {
556 let contents = await readFileAsync(options.path, 'utf8');
557 contents += '/*# sourceURL=' + options.path.replace(/\n/g, '') + '*/';
558 const context = await this._contextPromise;
559 return (await context.evaluateHandle(addStyleContent, contents)).asElement();
560 }
561
562 if (typeof options.content === 'string') {
563 const context = await this._contextPromise;
564 return (await context.evaluateHandle(addStyleContent, options.content)).asElement();
565 }
566
567 throw new Error('Provide an object with a `url`, `path` or `content` property');
568
569 /**
570 * @param {string} url
571 * @return {!Promise<!HTMLElement>}
572 */
573 async function addStyleUrl(url) {
574 const link = document.createElement('link');
575 link.rel = 'stylesheet';
576 link.href = url;
577 const promise = new Promise((res, rej) => {
578 link.onload = res;
579 link.onerror = rej;
580 });
581 document.head.appendChild(link);
582 await promise;
583 return link;
584 }
585
586 /**
587 * @param {string} content
588 * @return {!Promise<!HTMLElement>}
589 */
590 async function addStyleContent(content) {
591 const style = document.createElement('style');
592 style.type = 'text/css';
593 style.appendChild(document.createTextNode(content));
594 const promise = new Promise((res, rej) => {
595 style.onload = res;
596 style.onerror = rej;
597 });
598 document.head.appendChild(style);
599 await promise;
600 return style;
601 }
602 }
603
604 /**
605 * @param {string} selector
606 * @param {!Object=} options
607 */
608 async click(selector, options = {}) {
609 const handle = await this.$(selector);
610 assert(handle, 'No node found for selector: ' + selector);
611 await handle.click(options);
612 await handle.dispose();
613 }
614
615 /**
616 * @param {string} selector
617 */
618 async focus(selector) {
619 const handle = await this.$(selector);
620 assert(handle, 'No node found for selector: ' + selector);
621 await handle.focus();
622 await handle.dispose();
623 }
624
625 /**
626 * @param {string} selector
627 */
628 async hover(selector) {
629 const handle = await this.$(selector);
630 assert(handle, 'No node found for selector: ' + selector);
631 await handle.hover();
632 await handle.dispose();
633 }
634
635 /**
636 * @param {string} selector
637 * @param {!Array<string>} values
638 * @return {!Promise<!Array<string>>}
639 */
640 select(selector, ...values){
641 for (const value of values)
642 assert(helper.isString(value), 'Values must be strings. Found value "' + value + '" of type "' + (typeof value) + '"');
643 return this.$eval(selector, (element, values) => {
644 if (element.nodeName.toLowerCase() !== 'select')
645 throw new Error('Element is not a <select> element.');
646
647 const options = Array.from(element.options);
648 element.value = undefined;
649 for (const option of options) {
650 option.selected = values.includes(option.value);
651 if (option.selected && !element.multiple)
652 break;
653 }
654 element.dispatchEvent(new Event('input', { 'bubbles': true }));
655 element.dispatchEvent(new Event('change', { 'bubbles': true }));
656 return options.filter(option => option.selected).map(option => option.value);
657 }, values);
658 }
659
660 /**
661 * @param {string} selector
662 */
663 async tap(selector) {
664 const handle = await this.$(selector);
665 assert(handle, 'No node found for selector: ' + selector);
666 await handle.tap();
667 await handle.dispose();
668 }
669
670 /**
671 * @param {string} selector
672 * @param {string} text
673 * @param {{delay: (number|undefined)}=} options
674 */
675 async type(selector, text, options) {
676 const handle = await this.$(selector);
677 assert(handle, 'No node found for selector: ' + selector);
678 await handle.type(text, options);
679 await handle.dispose();
680 }
681
682 /**
683 * @param {(string|number|Function)} selectorOrFunctionOrTimeout
684 * @param {!Object=} options
685 * @param {!Array<*>} args
686 * @return {!Promise}
687 */
688 waitFor(selectorOrFunctionOrTimeout, options = {}, ...args) {
689 const xPathPattern = '//';
690
691 if (helper.isString(selectorOrFunctionOrTimeout)) {
692 const string = /** @type {string} */ (selectorOrFunctionOrTimeout);
693 if (string.startsWith(xPathPattern))
694 return this.waitForXPath(string, options);
695 return this.waitForSelector(string, options);
696 }
697 if (helper.isNumber(selectorOrFunctionOrTimeout))
698 return new Promise(fulfill => setTimeout(fulfill, selectorOrFunctionOrTimeout));
699 if (typeof selectorOrFunctionOrTimeout === 'function')
700 return this.waitForFunction(selectorOrFunctionOrTimeout, options, ...args);
701 return Promise.reject(new Error('Unsupported target type: ' + (typeof selectorOrFunctionOrTimeout)));
702 }
703
704 /**
705 * @param {string} selector
706 * @param {!Object=} options
707 * @return {!Promise}
708 */
709 waitForSelector(selector, options = {}) {
710 return this._waitForSelectorOrXPath(selector, false, options);
711 }
712
713 /**
714 * @param {string} xpath
715 * @param {!Object=} options
716 * @return {!Promise}
717 */
718 waitForXPath(xpath, options = {}) {
719 return this._waitForSelectorOrXPath(xpath, true, options);
720 }
721
722 /**
723 * @param {Function|string} pageFunction
724 * @param {!Object=} options
725 * @return {!Promise}
726 */
727 waitForFunction(pageFunction, options = {}, ...args) {
728 const timeout = helper.isNumber(options.timeout) ? options.timeout : 30000;
729 const polling = options.polling || 'raf';
730 return new WaitTask(this, pageFunction, 'function', polling, timeout, ...args).promise;
731 }
732
733 /**
734 * @return {!Promise<string>}
735 */
736 async title() {
737 return this.evaluate(() => document.title);
738 }
739
740 /**
741 * @param {string} selectorOrXPath
742 * @param {boolean} isXPath
743 * @param {!Object=} options
744 * @return {!Promise}
745 */
746 _waitForSelectorOrXPath(selectorOrXPath, isXPath, options = {}) {
747 const waitForVisible = !!options.visible;
748 const waitForHidden = !!options.hidden;
749 const polling = waitForVisible || waitForHidden ? 'raf' : 'mutation';
750 const timeout = helper.isNumber(options.timeout) ? options.timeout : 30000;
751 const title = `${isXPath ? 'XPath' : 'selector'} "${selectorOrXPath}"${waitForHidden ? ' to be hidden' : ''}`;
752 return new WaitTask(this, predicate, title, polling, timeout, selectorOrXPath, isXPath, waitForVisible, waitForHidden).promise;
753
754 /**
755 * @param {string} selectorOrXPath
756 * @param {boolean} isXPath
757 * @param {boolean} waitForVisible
758 * @param {boolean} waitForHidden
759 * @return {?Node|boolean}
760 */
761 function predicate(selectorOrXPath, isXPath, waitForVisible, waitForHidden) {
762 const node = isXPath
763 ? document.evaluate(selectorOrXPath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue
764 : document.querySelector(selectorOrXPath);
765 if (!node)
766 return waitForHidden;
767 if (!waitForVisible && !waitForHidden)
768 return node;
769 const element = /** @type {Element} */ (node.nodeType === Node.TEXT_NODE ? node.parentElement : node);
770
771 const style = window.getComputedStyle(element);
772 const isVisible = style && style.visibility !== 'hidden' && hasVisibleBoundingBox();
773 const success = (waitForVisible === isVisible || waitForHidden === !isVisible);
774 return success ? node : null;
775
776 /**
777 * @return {boolean}
778 */
779 function hasVisibleBoundingBox() {
780 const rect = element.getBoundingClientRect();
781 return !!(rect.top || rect.bottom || rect.width || rect.height);
782 }
783 }
784 }
785
786 /**
787 * @param {!Protocol.Page.Frame} framePayload
788 */
789 _navigated(framePayload) {
790 this._name = framePayload.name;
791 // TODO(lushnikov): remove this once requestInterception has loaderId exposed.
792 this._navigationURL = framePayload.url;
793 this._url = framePayload.url;
794 }
795
796 /**
797 * @param {string} url
798 */
799 _navigatedWithinDocument(url) {
800 this._url = url;
801 }
802
803 /**
804 * @param {string} loaderId
805 * @param {string} name
806 */
807 _onLifecycleEvent(loaderId, name) {
808 if (name === 'init') {
809 this._loaderId = loaderId;
810 this._lifecycleEvents.clear();
811 }
812 this._lifecycleEvents.add(name);
813 }
814
815 _onLoadingStopped() {
816 this._lifecycleEvents.add('DOMContentLoaded');
817 this._lifecycleEvents.add('load');
818 }
819
820 _detach() {
821 for (const waitTask of this._waitTasks)
822 waitTask.terminate(new Error('waitForFunction failed: frame got detached.'));
823 this._detached = true;
824 if (this._parentFrame)
825 this._parentFrame._childFrames.delete(this);
826 this._parentFrame = null;
827 }
828}
829helper.tracePublicAPI(Frame);
830
831class WaitTask {
832 /**
833 * @param {!Frame} frame
834 * @param {Function|string} predicateBody
835 * @param {string|number} polling
836 * @param {number} timeout
837 * @param {!Array<*>} args
838 */
839 constructor(frame, predicateBody, title, polling, timeout, ...args) {
840 if (helper.isString(polling))
841 assert(polling === 'raf' || polling === 'mutation', 'Unknown polling option: ' + polling);
842 else if (helper.isNumber(polling))
843 assert(polling > 0, 'Cannot poll with non-positive interval: ' + polling);
844 else
845 throw new Error('Unknown polling options: ' + polling);
846
847 this._frame = frame;
848 this._polling = polling;
849 this._timeout = timeout;
850 this._predicateBody = helper.isString(predicateBody) ? 'return ' + predicateBody : 'return (' + predicateBody + ')(...args)';
851 this._args = args;
852 this._runCount = 0;
853 frame._waitTasks.add(this);
854 this.promise = new Promise((resolve, reject) => {
855 this._resolve = resolve;
856 this._reject = reject;
857 });
858 // Since page navigation requires us to re-install the pageScript, we should track
859 // timeout on our end.
860 if (timeout) {
861 const timeoutError = new TimeoutError(`waiting for ${title} failed: timeout ${timeout}ms exceeded`);
862 this._timeoutTimer = setTimeout(() => this.terminate(timeoutError), timeout);
863 }
864 this.rerun();
865 }
866
867 /**
868 * @param {!Error} error
869 */
870 terminate(error) {
871 this._terminated = true;
872 this._reject(error);
873 this._cleanup();
874 }
875
876 async rerun() {
877 const runCount = ++this._runCount;
878 /** @type {?Puppeteer.JSHandle} */
879 let success = null;
880 let error = null;
881 try {
882 success = await (await this._frame.executionContext()).evaluateHandle(waitForPredicatePageFunction, this._predicateBody, this._polling, this._timeout, ...this._args);
883 } catch (e) {
884 error = e;
885 }
886
887 if (this._terminated || runCount !== this._runCount) {
888 if (success)
889 await success.dispose();
890 return;
891 }
892
893 // Ignore timeouts in pageScript - we track timeouts ourselves.
894 // If the frame's execution context has already changed, `frame.evaluate` will
895 // throw an error - ignore this predicate run altogether.
896 if (!error && await this._frame.evaluate(s => !s, success).catch(e => true)) {
897 await success.dispose();
898 return;
899 }
900
901 // When the page is navigated, the promise is rejected.
902 // We will try again in the new execution context.
903 if (error && error.message.includes('Execution context was destroyed'))
904 return;
905
906 // We could have tried to evaluate in a context which was already
907 // destroyed.
908 if (error && error.message.includes('Cannot find context with specified id'))
909 return;
910
911 if (error)
912 this._reject(error);
913 else
914 this._resolve(success);
915
916 this._cleanup();
917 }
918
919 _cleanup() {
920 clearTimeout(this._timeoutTimer);
921 this._frame._waitTasks.delete(this);
922 this._runningTask = null;
923 }
924}
925
926/**
927 * @param {string} predicateBody
928 * @param {string} polling
929 * @param {number} timeout
930 * @return {!Promise<*>}
931 */
932async function waitForPredicatePageFunction(predicateBody, polling, timeout, ...args) {
933 const predicate = new Function('...args', predicateBody);
934 let timedOut = false;
935 if (timeout)
936 setTimeout(() => timedOut = true, timeout);
937 if (polling === 'raf')
938 return await pollRaf();
939 if (polling === 'mutation')
940 return await pollMutation();
941 if (typeof polling === 'number')
942 return await pollInterval(polling);
943
944 /**
945 * @return {!Promise<*>}
946 */
947 function pollMutation() {
948 const success = predicate.apply(null, args);
949 if (success)
950 return Promise.resolve(success);
951
952 let fulfill;
953 const result = new Promise(x => fulfill = x);
954 const observer = new MutationObserver(mutations => {
955 if (timedOut) {
956 observer.disconnect();
957 fulfill();
958 }
959 const success = predicate.apply(null, args);
960 if (success) {
961 observer.disconnect();
962 fulfill(success);
963 }
964 });
965 observer.observe(document, {
966 childList: true,
967 subtree: true,
968 attributes: true
969 });
970 return result;
971 }
972
973 /**
974 * @return {!Promise<*>}
975 */
976 function pollRaf() {
977 let fulfill;
978 const result = new Promise(x => fulfill = x);
979 onRaf();
980 return result;
981
982 function onRaf() {
983 if (timedOut) {
984 fulfill();
985 return;
986 }
987 const success = predicate.apply(null, args);
988 if (success)
989 fulfill(success);
990 else
991 requestAnimationFrame(onRaf);
992 }
993 }
994
995 /**
996 * @param {number} pollInterval
997 * @return {!Promise<*>}
998 */
999 function pollInterval(pollInterval) {
1000 let fulfill;
1001 const result = new Promise(x => fulfill = x);
1002 onTimeout();
1003 return result;
1004
1005 function onTimeout() {
1006 if (timedOut) {
1007 fulfill();
1008 return;
1009 }
1010 const success = predicate.apply(null, args);
1011 if (success)
1012 fulfill(success);
1013 else
1014 setTimeout(onTimeout, pollInterval);
1015 }
1016 }
1017}
1018
1019module.exports = {FrameManager, Frame};