UNPKG

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