UNPKG

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