UNPKG

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