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