UNPKG

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