UNPKG

37.4 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 mime = require('mime');
20const {NetworkManager} = require('./NetworkManager');
21const {NavigatorWatcher} = require('./NavigatorWatcher');
22const {Dialog} = require('./Dialog');
23const {EmulationManager} = require('./EmulationManager');
24const {FrameManager} = require('./FrameManager');
25const {Keyboard, Mouse, Touchscreen} = require('./Input');
26const Tracing = require('./Tracing');
27const {helper, debugError, assert} = require('./helper');
28const {Coverage} = require('./Coverage');
29const {Worker} = require('./Worker');
30const {createJSHandle} = require('./ExecutionContext');
31
32const writeFileAsync = helper.promisify(fs.writeFile);
33
34class Page extends EventEmitter {
35 /**
36 * @param {!Puppeteer.CDPSession} client
37 * @param {!Puppeteer.Target} target
38 * @param {boolean} ignoreHTTPSErrors
39 * @param {?Puppeteer.Viewport} defaultViewport
40 * @param {!Puppeteer.TaskQueue} screenshotTaskQueue
41 * @return {!Promise<!Page>}
42 */
43 static async create(client, target, ignoreHTTPSErrors, defaultViewport, screenshotTaskQueue) {
44
45 await client.send('Page.enable');
46 const {frameTree} = await client.send('Page.getFrameTree');
47 const page = new Page(client, target, frameTree, ignoreHTTPSErrors, screenshotTaskQueue);
48
49 await Promise.all([
50 client.send('Target.setAutoAttach', {autoAttach: true, waitForDebuggerOnStart: false}),
51 client.send('Page.setLifecycleEventsEnabled', { enabled: true }),
52 client.send('Network.enable', {}),
53 client.send('Runtime.enable', {}),
54 client.send('Security.enable', {}),
55 client.send('Performance.enable', {}),
56 client.send('Log.enable', {}),
57 ]);
58 if (ignoreHTTPSErrors)
59 await client.send('Security.setOverrideCertificateErrors', {override: true});
60 // Initialize default page size.
61 if (defaultViewport)
62 await page.setViewport(defaultViewport);
63
64 return page;
65 }
66
67 /**
68 * @param {!Puppeteer.CDPSession} client
69 * @param {!Puppeteer.Target} target
70 * @param {!Protocol.Page.FrameTree} frameTree
71 * @param {boolean} ignoreHTTPSErrors
72 * @param {!Puppeteer.TaskQueue} screenshotTaskQueue
73 */
74 constructor(client, target, frameTree, ignoreHTTPSErrors, screenshotTaskQueue) {
75 super();
76 this._closed = false;
77 this._client = client;
78 this._target = target;
79 this._keyboard = new Keyboard(client);
80 this._mouse = new Mouse(client, this._keyboard);
81 this._touchscreen = new Touchscreen(client, this._keyboard);
82 /** @type {!FrameManager} */
83 this._frameManager = new FrameManager(client, frameTree, this);
84 this._networkManager = new NetworkManager(client, this._frameManager);
85 this._emulationManager = new EmulationManager(client);
86 this._tracing = new Tracing(client);
87 /** @type {!Map<string, Function>} */
88 this._pageBindings = new Map();
89 this._ignoreHTTPSErrors = ignoreHTTPSErrors;
90 this._coverage = new Coverage(client);
91 this._defaultNavigationTimeout = 30000;
92 this._javascriptEnabled = true;
93 /** @type {?Puppeteer.Viewport} */
94 this._viewport = null;
95
96 this._screenshotTaskQueue = screenshotTaskQueue;
97
98 /** @type {!Map<string, Worker>} */
99 this._workers = new Map();
100 client.on('Target.attachedToTarget', event => {
101 if (event.targetInfo.type !== 'worker') {
102 // If we don't detach from service workers, they will never die.
103 client.send('Target.detachFromTarget', {
104 sessionId: event.sessionId
105 }).catch(debugError);
106 return;
107 }
108 const session = client._createSession(event.targetInfo.type, event.sessionId);
109 const worker = new Worker(session, event.targetInfo.url, this._addConsoleMessage.bind(this), this._handleException.bind(this));
110 this._workers.set(event.sessionId, worker);
111 this.emit(Page.Events.WorkerCreated, worker);
112
113 });
114 client.on('Target.detachedFromTarget', event => {
115 const worker = this._workers.get(event.sessionId);
116 if (!worker)
117 return;
118 this.emit(Page.Events.WorkerDestroyed, worker);
119 this._workers.delete(event.sessionId);
120 });
121
122 this._frameManager.on(FrameManager.Events.FrameAttached, event => this.emit(Page.Events.FrameAttached, event));
123 this._frameManager.on(FrameManager.Events.FrameDetached, event => this.emit(Page.Events.FrameDetached, event));
124 this._frameManager.on(FrameManager.Events.FrameNavigated, event => this.emit(Page.Events.FrameNavigated, event));
125
126 this._networkManager.on(NetworkManager.Events.Request, event => this.emit(Page.Events.Request, event));
127 this._networkManager.on(NetworkManager.Events.Response, event => this.emit(Page.Events.Response, event));
128 this._networkManager.on(NetworkManager.Events.RequestFailed, event => this.emit(Page.Events.RequestFailed, event));
129 this._networkManager.on(NetworkManager.Events.RequestFinished, event => this.emit(Page.Events.RequestFinished, event));
130
131 client.on('Page.domContentEventFired', event => this.emit(Page.Events.DOMContentLoaded));
132 client.on('Page.loadEventFired', event => this.emit(Page.Events.Load));
133 client.on('Runtime.consoleAPICalled', event => this._onConsoleAPI(event));
134 client.on('Runtime.bindingCalled', event => this._onBindingCalled(event));
135 client.on('Page.javascriptDialogOpening', event => this._onDialog(event));
136 client.on('Runtime.exceptionThrown', exception => this._handleException(exception.exceptionDetails));
137 client.on('Security.certificateError', event => this._onCertificateError(event));
138 client.on('Inspector.targetCrashed', event => this._onTargetCrashed());
139 client.on('Performance.metrics', event => this._emitMetrics(event));
140 client.on('Log.entryAdded', event => this._onLogEntryAdded(event));
141 this._target._isClosedPromise.then(() => {
142 this.emit(Page.Events.Close);
143 this._closed = true;
144 });
145 }
146
147 /**
148 * @param {!{longitude: number, latitude: number, accuracy: (number|undefined)}} options
149 */
150 async setGeolocation(options) {
151 const { longitude, latitude, accuracy = 0} = options;
152 if (longitude < -180 || longitude > 180)
153 throw new Error(`Invalid longitude "${longitude}": precondition -180 <= LONGITUDE <= 180 failed.`);
154 if (latitude < -90 || latitude > 90)
155 throw new Error(`Invalid latitude "${latitude}": precondition -90 <= LATITUDE <= 90 failed.`);
156 if (accuracy < 0)
157 throw new Error(`Invalid accuracy "${accuracy}": precondition 0 <= ACCURACY failed.`);
158 await this._client.send('Emulation.setGeolocationOverride', {longitude, latitude, accuracy});
159 }
160
161 /**
162 * @return {!Puppeteer.Target}
163 */
164 target() {
165 return this._target;
166 }
167
168 /**
169 * @return {!Puppeteer.Browser}
170 */
171 browser() {
172 return this._target.browser();
173 }
174
175 _onTargetCrashed() {
176 this.emit('error', new Error('Page crashed!'));
177 }
178
179 /**
180 * @param {!Protocol.Log.entryAddedPayload} event
181 */
182 _onLogEntryAdded(event) {
183 const {level, text, args, source} = event.entry;
184 if (args)
185 args.map(arg => helper.releaseObject(this._client, arg));
186 if (source !== 'worker')
187 this.emit(Page.Events.Console, new ConsoleMessage(level, text));
188 }
189
190 /**
191 * @return {!Puppeteer.Frame}
192 */
193 mainFrame() {
194 return this._frameManager.mainFrame();
195 }
196
197 /**
198 * @return {!Keyboard}
199 */
200 get keyboard() {
201 return this._keyboard;
202 }
203
204 /**
205 * @return {!Touchscreen}
206 */
207 get touchscreen() {
208 return this._touchscreen;
209 }
210
211 /**
212 * @return {!Coverage}
213 */
214 get coverage() {
215 return this._coverage;
216 }
217
218 /**
219 * @return {!Tracing}
220 */
221 get tracing() {
222 return this._tracing;
223 }
224
225 /**
226 * @return {!Array<Puppeteer.Frame>}
227 */
228 frames() {
229 return this._frameManager.frames();
230 }
231
232 /**
233 * @return {!Array<!Worker>}
234 */
235 workers() {
236 return Array.from(this._workers.values());
237 }
238
239 /**
240 * @param {boolean} value
241 */
242 async setRequestInterception(value) {
243 return this._networkManager.setRequestInterception(value);
244 }
245
246 /**
247 * @param {boolean} enabled
248 */
249 setOfflineMode(enabled) {
250 return this._networkManager.setOfflineMode(enabled);
251 }
252
253 /**
254 * @param {number} timeout
255 */
256 setDefaultNavigationTimeout(timeout) {
257 this._defaultNavigationTimeout = timeout;
258 }
259
260 /**
261 * @param {!Protocol.Security.certificateErrorPayload} event
262 */
263 _onCertificateError(event) {
264 if (!this._ignoreHTTPSErrors)
265 return;
266 this._client.send('Security.handleCertificateError', {
267 eventId: event.eventId,
268 action: 'continue'
269 }).catch(debugError);
270 }
271
272 /**
273 * @param {string} selector
274 * @return {!Promise<?Puppeteer.ElementHandle>}
275 */
276 async $(selector) {
277 return this.mainFrame().$(selector);
278 }
279
280 /**
281 * @param {function()|string} pageFunction
282 * @param {!Array<*>} args
283 * @return {!Promise<!Puppeteer.JSHandle>}
284 */
285 async evaluateHandle(pageFunction, ...args) {
286 const context = await this.mainFrame().executionContext();
287 return context.evaluateHandle(pageFunction, ...args);
288 }
289
290 /**
291 * @param {!Puppeteer.JSHandle} prototypeHandle
292 * @return {!Promise<!Puppeteer.JSHandle>}
293 */
294 async queryObjects(prototypeHandle) {
295 const context = await this.mainFrame().executionContext();
296 return context.queryObjects(prototypeHandle);
297 }
298
299 /**
300 * @param {string} selector
301 * @param {function()|string} pageFunction
302 * @param {!Array<*>} args
303 * @return {!Promise<(!Object|undefined)>}
304 */
305 async $eval(selector, pageFunction, ...args) {
306 return this.mainFrame().$eval(selector, pageFunction, ...args);
307 }
308
309 /**
310 * @param {string} selector
311 * @param {Function|string} pageFunction
312 * @param {!Array<*>} args
313 * @return {!Promise<(!Object|undefined)>}
314 */
315 async $$eval(selector, pageFunction, ...args) {
316 return this.mainFrame().$$eval(selector, pageFunction, ...args);
317 }
318
319 /**
320 * @param {string} selector
321 * @return {!Promise<!Array<!Puppeteer.ElementHandle>>}
322 */
323 async $$(selector) {
324 return this.mainFrame().$$(selector);
325 }
326
327 /**
328 * @param {string} expression
329 * @return {!Promise<!Array<!Puppeteer.ElementHandle>>}
330 */
331 async $x(expression) {
332 return this.mainFrame().$x(expression);
333 }
334
335 /**
336 * @param {!Array<string>} urls
337 * @return {!Promise<!Array<Network.Cookie>>}
338 */
339 async cookies(...urls) {
340 return (await this._client.send('Network.getCookies', {
341 urls: urls.length ? urls : [this.url()]
342 })).cookies;
343 }
344
345 /**
346 * @param {Array<Network.CookieParam>} cookies
347 */
348 async deleteCookie(...cookies) {
349 const pageURL = this.url();
350 for (const cookie of cookies) {
351 const item = Object.assign({}, cookie);
352 if (!cookie.url && pageURL.startsWith('http'))
353 item.url = pageURL;
354 await this._client.send('Network.deleteCookies', item);
355 }
356 }
357
358 /**
359 * @param {Array<Network.CookieParam>} cookies
360 */
361 async setCookie(...cookies) {
362 const pageURL = this.url();
363 const startsWithHTTP = pageURL.startsWith('http');
364 const items = cookies.map(cookie => {
365 const item = Object.assign({}, cookie);
366 if (!item.url && startsWithHTTP)
367 item.url = pageURL;
368 assert(
369 item.url !== 'about:blank',
370 `Blank page can not have cookie "${item.name}"`
371 );
372 assert(
373 !String.prototype.startsWith.call(item.url || '', 'data:'),
374 `Data URL page can not have cookie "${item.name}"`
375 );
376 return item;
377 });
378 await this.deleteCookie(...items);
379 if (items.length)
380 await this._client.send('Network.setCookies', { cookies: items });
381 }
382
383 /**
384 * @param {Object} options
385 * @return {!Promise<!Puppeteer.ElementHandle>}
386 */
387 async addScriptTag(options) {
388 return this.mainFrame().addScriptTag(options);
389 }
390
391 /**
392 * @param {Object} options
393 * @return {!Promise<!Puppeteer.ElementHandle>}
394 */
395 async addStyleTag(options) {
396 return this.mainFrame().addStyleTag(options);
397 }
398
399 /**
400 * @param {string} name
401 * @param {function(?)} puppeteerFunction
402 */
403 async exposeFunction(name, puppeteerFunction) {
404 if (this._pageBindings.has(name))
405 throw new Error(`Failed to add page binding with name ${name}: window['${name}'] already exists!`);
406 this._pageBindings.set(name, puppeteerFunction);
407
408 const expression = helper.evaluationString(addPageBinding, name);
409 await this._client.send('Runtime.addBinding', {name: name});
410 await this._client.send('Page.addScriptToEvaluateOnNewDocument', {source: expression});
411 await Promise.all(this.frames().map(frame => frame.evaluate(expression).catch(debugError)));
412
413 function addPageBinding(bindingName) {
414 const binding = window[bindingName];
415 window[bindingName] = async(...args) => {
416 const me = window[bindingName];
417 let callbacks = me['callbacks'];
418 if (!callbacks) {
419 callbacks = new Map();
420 me['callbacks'] = callbacks;
421 }
422 const seq = (me['lastSeq'] || 0) + 1;
423 me['lastSeq'] = seq;
424 const promise = new Promise(fulfill => callbacks.set(seq, fulfill));
425 binding(JSON.stringify({name: bindingName, seq, args}));
426 return promise;
427 };
428 }
429 }
430
431 /**
432 * @param {?{username: string, password: string}} credentials
433 */
434 async authenticate(credentials) {
435 return this._networkManager.authenticate(credentials);
436 }
437
438 /**
439 * @param {!Object<string, string>} headers
440 */
441 async setExtraHTTPHeaders(headers) {
442 return this._networkManager.setExtraHTTPHeaders(headers);
443 }
444
445 /**
446 * @param {string} userAgent
447 */
448 async setUserAgent(userAgent) {
449 return this._networkManager.setUserAgent(userAgent);
450 }
451
452 /**
453 * @return {!Promise<!Object>}
454 */
455 async metrics() {
456 const response = await this._client.send('Performance.getMetrics');
457 return this._buildMetricsObject(response.metrics);
458 }
459
460 /**
461 * @param {*} event
462 */
463 _emitMetrics(event) {
464 this.emit(Page.Events.Metrics, {
465 title: event.title,
466 metrics: this._buildMetricsObject(event.metrics)
467 });
468 }
469
470 /**
471 * @param {?Array<!Protocol.Performance.Metric>} metrics
472 * @return {!Object}
473 */
474 _buildMetricsObject(metrics) {
475 const result = {};
476 for (const metric of metrics || []) {
477 if (supportedMetrics.has(metric.name))
478 result[metric.name] = metric.value;
479 }
480 return result;
481 }
482
483 /**
484 * @param {!Protocol.Runtime.ExceptionDetails} exceptionDetails
485 */
486 _handleException(exceptionDetails) {
487 const message = helper.getExceptionMessage(exceptionDetails);
488 const err = new Error(message);
489 err.stack = ''; // Don't report clientside error with a node stack attached
490 this.emit(Page.Events.PageError, err);
491 }
492
493 /**
494 * @param {!Protocol.Runtime.consoleAPICalledPayload} event
495 */
496 async _onConsoleAPI(event) {
497 const context = this._frameManager.executionContextById(event.executionContextId);
498 const values = event.args.map(arg => createJSHandle(context, arg));
499 this._addConsoleMessage(event.type, values);
500 }
501
502 /**
503 * @param {!Protocol.Runtime.bindingCalledPayload} event
504 */
505 async _onBindingCalled(event) {
506 const {name, seq, args} = JSON.parse(event.payload);
507 const result = await this._pageBindings.get(name)(...args);
508 const expression = helper.evaluationString(deliverResult, name, seq, result);
509 this._client.send('Runtime.evaluate', { expression, contextId: event.executionContextId }).catch(debugError);
510
511 function deliverResult(name, seq, result) {
512 window[name]['callbacks'].get(seq)(result);
513 window[name]['callbacks'].delete(seq);
514 }
515 }
516
517 /**
518 * @param {string} type
519 * @param {!Array<!Puppeteer.JSHandle>} args
520 */
521 _addConsoleMessage(type, args) {
522 if (!this.listenerCount(Page.Events.Console)) {
523 args.forEach(arg => arg.dispose());
524 return;
525 }
526 const textTokens = [];
527 for (const arg of args) {
528 const remoteObject = arg._remoteObject;
529 if (remoteObject.objectId)
530 textTokens.push(arg.toString());
531 else
532 textTokens.push(helper.valueFromRemoteObject(remoteObject));
533 }
534 const message = new ConsoleMessage(type, textTokens.join(' '), args);
535 this.emit(Page.Events.Console, message);
536 }
537
538 _onDialog(event) {
539 let dialogType = null;
540 if (event.type === 'alert')
541 dialogType = Dialog.Type.Alert;
542 else if (event.type === 'confirm')
543 dialogType = Dialog.Type.Confirm;
544 else if (event.type === 'prompt')
545 dialogType = Dialog.Type.Prompt;
546 else if (event.type === 'beforeunload')
547 dialogType = Dialog.Type.BeforeUnload;
548 assert(dialogType, 'Unknown javascript dialog type: ' + event.type);
549 const dialog = new Dialog(this._client, dialogType, event.message, event.defaultPrompt);
550 this.emit(Page.Events.Dialog, dialog);
551 }
552
553 /**
554 * @return {!string}
555 */
556 url() {
557 return this.mainFrame().url();
558 }
559
560 /**
561 * @return {!Promise<String>}
562 */
563 async content() {
564 return await this._frameManager.mainFrame().content();
565 }
566
567 /**
568 * @param {string} html
569 */
570 async setContent(html) {
571 await this._frameManager.mainFrame().setContent(html);
572 }
573
574 /**
575 * @param {string} url
576 * @param {!Object=} options
577 * @return {!Promise<?Puppeteer.Response>}
578 */
579 async goto(url, options = {}) {
580 const referrer = typeof options.referer === 'string' ? options.referer : this._networkManager.extraHTTPHeaders()['referer'];
581
582 /** @type {Map<string, !Puppeteer.Request>} */
583 const requests = new Map();
584 const eventListeners = [
585 helper.addEventListener(this._networkManager, NetworkManager.Events.Request, request => {
586 if (!requests.get(request.url()))
587 requests.set(request.url(), request);
588 })
589 ];
590
591 const mainFrame = this._frameManager.mainFrame();
592 const timeout = typeof options.timeout === 'number' ? options.timeout : this._defaultNavigationTimeout;
593 const watcher = new NavigatorWatcher(this._frameManager, mainFrame, timeout, options);
594 const navigationPromise = watcher.navigationPromise();
595 let error = await Promise.race([
596 navigate(this._client, url, referrer),
597 navigationPromise,
598 ]);
599 if (!error)
600 error = await navigationPromise;
601 watcher.cancel();
602 helper.removeEventListeners(eventListeners);
603 if (error)
604 throw error;
605 const request = requests.get(mainFrame._navigationURL);
606 return request ? request.response() : null;
607
608 /**
609 * @param {!Puppeteer.CDPSession} client
610 * @param {string} url
611 * @param {string} referrer
612 * @return {!Promise<?Error>}
613 */
614 async function navigate(client, url, referrer) {
615 try {
616 const response = await client.send('Page.navigate', {url, referrer});
617 return response.errorText ? new Error(`${response.errorText} at ${url}`) : null;
618 } catch (error) {
619 return error;
620 }
621 }
622 }
623
624 /**
625 * @param {!Object=} options
626 * @return {!Promise<?Puppeteer.Response>}
627 */
628 async reload(options) {
629 const [response] = await Promise.all([
630 this.waitForNavigation(options),
631 this._client.send('Page.reload')
632 ]);
633 return response;
634 }
635
636 /**
637 * @param {!Object=} options
638 * @return {!Promise<?Puppeteer.Response>}
639 */
640 async waitForNavigation(options = {}) {
641 const mainFrame = this._frameManager.mainFrame();
642 const timeout = typeof options.timeout === 'number' ? options.timeout : this._defaultNavigationTimeout;
643 const watcher = new NavigatorWatcher(this._frameManager, mainFrame, timeout, options);
644
645 const responses = new Map();
646 const listener = helper.addEventListener(this._networkManager, NetworkManager.Events.Response, response => responses.set(response.url(), response));
647 const error = await watcher.navigationPromise();
648 helper.removeEventListeners([listener]);
649 if (error)
650 throw error;
651 return responses.get(this.mainFrame().url()) || null;
652 }
653
654 /**
655 * @param {(string|Function)} urlOrPredicate
656 * @param {!Object=} options
657 * @return {!Promise<!Puppeteer.Request>}
658 */
659 async waitForRequest(urlOrPredicate, options = {}) {
660 const timeout = typeof options.timeout === 'number' ? options.timeout : 30000;
661 return helper.waitForEvent(this._networkManager, NetworkManager.Events.Request, request => {
662 if (helper.isString(urlOrPredicate))
663 return (urlOrPredicate === request.url());
664 if (typeof urlOrPredicate === 'function')
665 return !!(urlOrPredicate(request));
666 return false;
667 }, timeout);
668 }
669
670 /**
671 * @param {(string|Function)} urlOrPredicate
672 * @param {!Object=} options
673 * @return {!Promise<!Puppeteer.Response>}
674 */
675 async waitForResponse(urlOrPredicate, options = {}) {
676 const timeout = typeof options.timeout === 'number' ? options.timeout : 30000;
677 return helper.waitForEvent(this._networkManager, NetworkManager.Events.Response, response => {
678 if (helper.isString(urlOrPredicate))
679 return (urlOrPredicate === response.url());
680 if (typeof urlOrPredicate === 'function')
681 return !!(urlOrPredicate(response));
682 return false;
683 }, timeout);
684 }
685
686 /**
687 * @param {!Object=} options
688 * @return {!Promise<?Puppeteer.Response>}
689 */
690 async goBack(options) {
691 return this._go(-1, options);
692 }
693
694 /**
695 * @param {!Object=} options
696 * @return {!Promise<?Puppeteer.Response>}
697 */
698 async goForward(options) {
699 return this._go(+1, options);
700 }
701
702 /**
703 * @param {!Object=} options
704 * @return {!Promise<?Puppeteer.Response>}
705 */
706 async _go(delta, options) {
707 const history = await this._client.send('Page.getNavigationHistory');
708 const entry = history.entries[history.currentIndex + delta];
709 if (!entry)
710 return null;
711 const [response] = await Promise.all([
712 this.waitForNavigation(options),
713 this._client.send('Page.navigateToHistoryEntry', {entryId: entry.id}),
714 ]);
715 return response;
716 }
717
718 async bringToFront() {
719 await this._client.send('Page.bringToFront');
720 }
721
722 /**
723 * @param {!Object} options
724 */
725 async emulate(options) {
726 return Promise.all([
727 this.setViewport(options.viewport),
728 this.setUserAgent(options.userAgent)
729 ]);
730 }
731
732 /**
733 * @param {boolean} enabled
734 */
735 async setJavaScriptEnabled(enabled) {
736 if (this._javascriptEnabled === enabled)
737 return;
738 this._javascriptEnabled = enabled;
739 await this._client.send('Emulation.setScriptExecutionDisabled', { value: !enabled });
740 }
741
742 /**
743 * @param {boolean} enabled
744 */
745 async setBypassCSP(enabled) {
746 await this._client.send('Page.setBypassCSP', { enabled });
747 }
748
749 /**
750 * @param {?string} mediaType
751 */
752 async emulateMedia(mediaType) {
753 assert(mediaType === 'screen' || mediaType === 'print' || mediaType === null, 'Unsupported media type: ' + mediaType);
754 await this._client.send('Emulation.setEmulatedMedia', {media: mediaType || ''});
755 }
756
757 /**
758 * @param {!Puppeteer.Viewport} viewport
759 */
760 async setViewport(viewport) {
761 const needsReload = await this._emulationManager.emulateViewport(viewport);
762 this._viewport = viewport;
763 if (needsReload)
764 await this.reload();
765 }
766
767 /**
768 * @return {?Puppeteer.Viewport}
769 */
770 viewport() {
771 return this._viewport;
772 }
773
774 /**
775 * @param {function()|string} pageFunction
776 * @param {!Array<*>} args
777 * @return {!Promise<*>}
778 */
779 async evaluate(pageFunction, ...args) {
780 return this._frameManager.mainFrame().evaluate(pageFunction, ...args);
781 }
782
783 /**
784 * @param {function()|string} pageFunction
785 * @param {!Array<*>} args
786 */
787 async evaluateOnNewDocument(pageFunction, ...args) {
788 const source = helper.evaluationString(pageFunction, ...args);
789 await this._client.send('Page.addScriptToEvaluateOnNewDocument', { source });
790 }
791
792 /**
793 * @param {Boolean} enabled
794 * @returns {!Promise}
795 */
796 async setCacheEnabled(enabled = true) {
797 await this._client.send('Network.setCacheDisabled', {cacheDisabled: !enabled});
798 }
799
800 /**
801 * @param {!Object=} options
802 * @return {!Promise<!Buffer|!String>}
803 */
804 async screenshot(options = {}) {
805 let screenshotType = null;
806 // options.type takes precedence over inferring the type from options.path
807 // because it may be a 0-length file with no extension created beforehand (i.e. as a temp file).
808 if (options.type) {
809 assert(options.type === 'png' || options.type === 'jpeg', 'Unknown options.type value: ' + options.type);
810 screenshotType = options.type;
811 } else if (options.path) {
812 const mimeType = mime.getType(options.path);
813 if (mimeType === 'image/png')
814 screenshotType = 'png';
815 else if (mimeType === 'image/jpeg')
816 screenshotType = 'jpeg';
817 assert(screenshotType, 'Unsupported screenshot mime type: ' + mimeType);
818 }
819
820 if (!screenshotType)
821 screenshotType = 'png';
822
823 if (options.quality) {
824 assert(screenshotType === 'jpeg', 'options.quality is unsupported for the ' + screenshotType + ' screenshots');
825 assert(typeof options.quality === 'number', 'Expected options.quality to be a number but found ' + (typeof options.quality));
826 assert(Number.isInteger(options.quality), 'Expected options.quality to be an integer');
827 assert(options.quality >= 0 && options.quality <= 100, 'Expected options.quality to be between 0 and 100 (inclusive), got ' + options.quality);
828 }
829 assert(!options.clip || !options.fullPage, 'options.clip and options.fullPage are exclusive');
830 if (options.clip) {
831 assert(typeof options.clip.x === 'number', 'Expected options.clip.x to be a number but found ' + (typeof options.clip.x));
832 assert(typeof options.clip.y === 'number', 'Expected options.clip.y to be a number but found ' + (typeof options.clip.y));
833 assert(typeof options.clip.width === 'number', 'Expected options.clip.width to be a number but found ' + (typeof options.clip.width));
834 assert(typeof options.clip.height === 'number', 'Expected options.clip.height to be a number but found ' + (typeof options.clip.height));
835 }
836 return this._screenshotTaskQueue.postTask(this._screenshotTask.bind(this, screenshotType, options));
837 }
838
839 /**
840 * @param {"png"|"jpeg"} format
841 * @param {!Object=} options
842 * @return {!Promise<!Buffer|!String>}
843 */
844 async _screenshotTask(format, options) {
845 await this._client.send('Target.activateTarget', {targetId: this._target._targetId});
846 let clip = options.clip ? Object.assign({}, options['clip']) : undefined;
847 if (clip)
848 clip.scale = 1;
849
850 if (options.fullPage) {
851 assert(this._viewport, 'fullPage screenshots do not work without first setting viewport.');
852 const metrics = await this._client.send('Page.getLayoutMetrics');
853 const width = Math.ceil(metrics.contentSize.width);
854 const height = Math.ceil(metrics.contentSize.height);
855
856 // Overwrite clip for full page at all times.
857 clip = { x: 0, y: 0, width, height, scale: 1 };
858 const mobile = this._viewport.isMobile || false;
859 const deviceScaleFactor = this._viewport.deviceScaleFactor || 1;
860 const landscape = this._viewport.isLandscape || false;
861 /** @type {!Protocol.Emulation.ScreenOrientation} */
862 const screenOrientation = landscape ? { angle: 90, type: 'landscapePrimary' } : { angle: 0, type: 'portraitPrimary' };
863 await this._client.send('Emulation.setDeviceMetricsOverride', { mobile, width, height, deviceScaleFactor, screenOrientation });
864 }
865
866 if (options.omitBackground)
867 await this._client.send('Emulation.setDefaultBackgroundColorOverride', { color: { r: 0, g: 0, b: 0, a: 0 } });
868 const result = await this._client.send('Page.captureScreenshot', { format, quality: options.quality, clip });
869 if (options.omitBackground)
870 await this._client.send('Emulation.setDefaultBackgroundColorOverride');
871
872 if (options.fullPage)
873 await this.setViewport(this._viewport);
874
875 const buffer = options.encoding === 'base64' ? result.data : Buffer.from(result.data, 'base64');
876 if (options.path)
877 await writeFileAsync(options.path, buffer);
878 return buffer;
879 }
880
881 /**
882 * @param {!Object=} options
883 * @return {!Promise<!Buffer>}
884 */
885 async pdf(options = {}) {
886 const scale = options.scale || 1;
887 const displayHeaderFooter = !!options.displayHeaderFooter;
888 const headerTemplate = options.headerTemplate || '';
889 const footerTemplate = options.footerTemplate || '';
890 const printBackground = !!options.printBackground;
891 const landscape = !!options.landscape;
892 const pageRanges = options.pageRanges || '';
893
894 let paperWidth = 8.5;
895 let paperHeight = 11;
896 if (options.format) {
897 const format = Page.PaperFormats[options.format.toLowerCase()];
898 assert(format, 'Unknown paper format: ' + options.format);
899 paperWidth = format.width;
900 paperHeight = format.height;
901 } else {
902 paperWidth = convertPrintParameterToInches(options.width) || paperWidth;
903 paperHeight = convertPrintParameterToInches(options.height) || paperHeight;
904 }
905
906 const marginOptions = options.margin || {};
907 const marginTop = convertPrintParameterToInches(marginOptions.top) || 0;
908 const marginLeft = convertPrintParameterToInches(marginOptions.left) || 0;
909 const marginBottom = convertPrintParameterToInches(marginOptions.bottom) || 0;
910 const marginRight = convertPrintParameterToInches(marginOptions.right) || 0;
911 const preferCSSPageSize = options.preferCSSPageSize || false;
912
913 const result = await this._client.send('Page.printToPDF', {
914 landscape: landscape,
915 displayHeaderFooter: displayHeaderFooter,
916 headerTemplate: headerTemplate,
917 footerTemplate: footerTemplate,
918 printBackground: printBackground,
919 scale: scale,
920 paperWidth: paperWidth,
921 paperHeight: paperHeight,
922 marginTop: marginTop,
923 marginBottom: marginBottom,
924 marginLeft: marginLeft,
925 marginRight: marginRight,
926 pageRanges: pageRanges,
927 preferCSSPageSize: preferCSSPageSize
928 });
929 const buffer = Buffer.from(result.data, 'base64');
930 if (options.path)
931 await writeFileAsync(options.path, buffer);
932 return buffer;
933 }
934
935 /**
936 * @return {!Promise<string>}
937 */
938 async title() {
939 return this.mainFrame().title();
940 }
941
942 /**
943 * @param {!{runBeforeUnload: (boolean|undefined)}=} options
944 */
945 async close(options = {runBeforeUnload: undefined}) {
946 assert(!!this._client._connection, 'Protocol error: Connection closed. Most likely the page has been closed.');
947 const runBeforeUnload = !!options.runBeforeUnload;
948 if (runBeforeUnload) {
949 await this._client.send('Page.close');
950 } else {
951 await this._client._connection.send('Target.closeTarget', { targetId: this._target._targetId });
952 await this._target._isClosedPromise;
953 }
954 }
955
956 /**
957 * @return {boolean}
958 */
959 isClosed() {
960 return this._closed;
961 }
962
963 /**
964 * @return {!Mouse}
965 */
966 get mouse() {
967 return this._mouse;
968 }
969
970 /**
971 * @param {string} selector
972 * @param {!Object=} options
973 */
974 click(selector, options = {}) {
975 return this.mainFrame().click(selector, options);
976 }
977
978 /**
979 * @param {string} selector
980 */
981 focus(selector) {
982 return this.mainFrame().focus(selector);
983 }
984
985 /**
986 * @param {string} selector
987 */
988 hover(selector) {
989 return this.mainFrame().hover(selector);
990 }
991
992 /**
993 * @param {string} selector
994 * @param {!Array<string>} values
995 * @return {!Promise<!Array<string>>}
996 */
997 select(selector, ...values) {
998 return this.mainFrame().select(selector, ...values);
999 }
1000
1001 /**
1002 * @param {string} selector
1003 */
1004 tap(selector) {
1005 return this.mainFrame().tap(selector);
1006 }
1007
1008 /**
1009 * @param {string} selector
1010 * @param {string} text
1011 * @param {{delay: (number|undefined)}=} options
1012 */
1013 type(selector, text, options) {
1014 return this.mainFrame().type(selector, text, options);
1015 }
1016
1017 /**
1018 * @param {(string|number|Function)} selectorOrFunctionOrTimeout
1019 * @param {!Object=} options
1020 * @param {!Array<*>} args
1021 * @return {!Promise}
1022 */
1023 waitFor(selectorOrFunctionOrTimeout, options = {}, ...args) {
1024 return this.mainFrame().waitFor(selectorOrFunctionOrTimeout, options, ...args);
1025 }
1026
1027 /**
1028 * @param {string} selector
1029 * @param {!Object=} options
1030 * @return {!Promise}
1031 */
1032 waitForSelector(selector, options = {}) {
1033 return this.mainFrame().waitForSelector(selector, options);
1034 }
1035
1036 /**
1037 * @param {string} xpath
1038 * @param {!Object=} options
1039 * @return {!Promise}
1040 */
1041 waitForXPath(xpath, options = {}) {
1042 return this.mainFrame().waitForXPath(xpath, options);
1043 }
1044
1045 /**
1046 * @param {function()} pageFunction
1047 * @param {!Object=} options
1048 * @param {!Array<*>} args
1049 * @return {!Promise}
1050 */
1051 waitForFunction(pageFunction, options = {}, ...args) {
1052 return this.mainFrame().waitForFunction(pageFunction, options, ...args);
1053 }
1054}
1055
1056/** @type {!Set<string>} */
1057const supportedMetrics = new Set([
1058 'Timestamp',
1059 'Documents',
1060 'Frames',
1061 'JSEventListeners',
1062 'Nodes',
1063 'LayoutCount',
1064 'RecalcStyleCount',
1065 'LayoutDuration',
1066 'RecalcStyleDuration',
1067 'ScriptDuration',
1068 'TaskDuration',
1069 'JSHeapUsedSize',
1070 'JSHeapTotalSize',
1071]);
1072
1073/** @enum {string} */
1074Page.PaperFormats = {
1075 letter: {width: 8.5, height: 11},
1076 legal: {width: 8.5, height: 14},
1077 tabloid: {width: 11, height: 17},
1078 ledger: {width: 17, height: 11},
1079 a0: {width: 33.1, height: 46.8 },
1080 a1: {width: 23.4, height: 33.1 },
1081 a2: {width: 16.5, height: 23.4 },
1082 a3: {width: 11.7, height: 16.5 },
1083 a4: {width: 8.27, height: 11.7 },
1084 a5: {width: 5.83, height: 8.27 },
1085 a6: {width: 4.13, height: 5.83 },
1086};
1087
1088const unitToPixels = {
1089 'px': 1,
1090 'in': 96,
1091 'cm': 37.8,
1092 'mm': 3.78
1093};
1094
1095/**
1096 * @param {(string|number|undefined)} parameter
1097 * @return {(number|undefined)}
1098 */
1099function convertPrintParameterToInches(parameter) {
1100 if (typeof parameter === 'undefined')
1101 return undefined;
1102 let pixels;
1103 if (helper.isNumber(parameter)) {
1104 // Treat numbers as pixel values to be aligned with phantom's paperSize.
1105 pixels = /** @type {number} */ (parameter);
1106 } else if (helper.isString(parameter)) {
1107 const text = /** @type {string} */ (parameter);
1108 let unit = text.substring(text.length - 2).toLowerCase();
1109 let valueText = '';
1110 if (unitToPixels.hasOwnProperty(unit)) {
1111 valueText = text.substring(0, text.length - 2);
1112 } else {
1113 // In case of unknown unit try to parse the whole parameter as number of pixels.
1114 // This is consistent with phantom's paperSize behavior.
1115 unit = 'px';
1116 valueText = text;
1117 }
1118 const value = Number(valueText);
1119 assert(!isNaN(value), 'Failed to parse parameter value: ' + text);
1120 pixels = value * unitToPixels[unit];
1121 } else {
1122 throw new Error('page.pdf() Cannot handle parameter type: ' + (typeof parameter));
1123 }
1124 return pixels / 96;
1125}
1126
1127Page.Events = {
1128 Close: 'close',
1129 Console: 'console',
1130 Dialog: 'dialog',
1131 DOMContentLoaded: 'domcontentloaded',
1132 Error: 'error',
1133 // Can't use just 'error' due to node.js special treatment of error events.
1134 // @see https://nodejs.org/api/events.html#events_error_events
1135 PageError: 'pageerror',
1136 Request: 'request',
1137 Response: 'response',
1138 RequestFailed: 'requestfailed',
1139 RequestFinished: 'requestfinished',
1140 FrameAttached: 'frameattached',
1141 FrameDetached: 'framedetached',
1142 FrameNavigated: 'framenavigated',
1143 Load: 'load',
1144 Metrics: 'metrics',
1145 WorkerCreated: 'workercreated',
1146 WorkerDestroyed: 'workerdestroyed',
1147};
1148
1149
1150/**
1151 * @typedef {Object} Network.Cookie
1152 * @property {string} name
1153 * @property {string} value
1154 * @property {string} domain
1155 * @property {string} path
1156 * @property {number} expires
1157 * @property {number} size
1158 * @property {boolean} httpOnly
1159 * @property {boolean} secure
1160 * @property {boolean} session
1161 * @property {("Strict"|"Lax")=} sameSite
1162 */
1163
1164
1165/**
1166 * @typedef {Object} Network.CookieParam
1167 * @property {string} name
1168 * @property {string} value
1169 * @property {string=} url
1170 * @property {string=} domain
1171 * @property {string=} path
1172 * @property {number=} expires
1173 * @property {boolean=} httpOnly
1174 * @property {boolean=} secure
1175 * @property {("Strict"|"Lax")=} sameSite
1176 */
1177
1178class ConsoleMessage {
1179 /**
1180 * @param {string} type
1181 * @param {string} text
1182 * @param {!Array<!Puppeteer.JSHandle>} args
1183 */
1184 constructor(type, text, args = []) {
1185 this._type = type;
1186 this._text = text;
1187 this._args = args;
1188 }
1189
1190 /**
1191 * @return {string}
1192 */
1193 type() {
1194 return this._type;
1195 }
1196
1197 /**
1198 * @return {string}
1199 */
1200 text() {
1201 return this._text;
1202 }
1203
1204 /**
1205 * @return {!Array<!Puppeteer.JSHandle>}
1206 */
1207 args() {
1208 return this._args;
1209 }
1210}
1211
1212
1213module.exports = {Page};
1214helper.tracePublicAPI(Page);