UNPKG

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