UNPKG

7.42 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 debugError = require('debug')(`puppeteer:error`);
18/** @type {?Map<string, boolean>} */
19let apiCoverage = null;
20class Helper {
21 /**
22 * @param {Function|string} fun
23 * @param {!Array<*>} args
24 * @return {string}
25 */
26 static evaluationString(fun, ...args) {
27 if (Helper.isString(fun)) {
28 console.assert(args.length === 0, 'Cannot evaluate a string with arguments');
29 return /** @type {string} */ (fun);
30 }
31 return `(${fun})(${args.map(serializeArgument).join(',')})`;
32
33 /**
34 * @param {*} arg
35 * @return {string}
36 */
37 function serializeArgument(arg) {
38 if (Object.is(arg, undefined))
39 return 'undefined';
40 return JSON.stringify(arg);
41 }
42 }
43
44 /**
45 * @param {!Object} exceptionDetails
46 * @return {string}
47 */
48 static getExceptionMessage(exceptionDetails) {
49 if (exceptionDetails.exception)
50 return exceptionDetails.exception.description;
51 let message = exceptionDetails.text;
52 if (exceptionDetails.stackTrace) {
53 for (const callframe of exceptionDetails.stackTrace.callFrames) {
54 const location = callframe.url + ':' + callframe.lineNumber + ':' + callframe.columnNumber;
55 const functionName = callframe.functionName || '<anonymous>';
56 message += `\n at ${functionName} (${location})`;
57 }
58 }
59 return message;
60 }
61
62 /**
63 * @param {!Object} remoteObject
64 * @return {*}
65 */
66 static valueFromRemoteObject(remoteObject) {
67 console.assert(!remoteObject.objectId, 'Cannot extract value when objectId is given');
68 if (remoteObject.unserializableValue) {
69 switch (remoteObject.unserializableValue) {
70 case '-0':
71 return -0;
72 case 'NaN':
73 return NaN;
74 case 'Infinity':
75 return Infinity;
76 case '-Infinity':
77 return -Infinity;
78 default:
79 throw new Error('Unsupported unserializable value: ' + remoteObject.unserializableValue);
80 }
81 }
82 return remoteObject.value;
83 }
84
85 /**
86 * @param {!Puppeteer.CDPSession} client
87 * @param {!Object} remoteObject
88 */
89 static async releaseObject(client, remoteObject) {
90 if (!remoteObject.objectId)
91 return;
92 await client.send('Runtime.releaseObject', {objectId: remoteObject.objectId}).catch(error => {
93 // Exceptions might happen in case of a page been navigated or closed.
94 // Swallow these since they are harmless and we don't leak anything in this case.
95 debugError(error);
96 });
97 }
98
99 /**
100 * @param {!Object} classType
101 */
102 static tracePublicAPI(classType) {
103 let className = classType.prototype.constructor.name;
104 className = className.substring(0, 1).toLowerCase() + className.substring(1);
105 const debug = require('debug')(`puppeteer:${className}`);
106 if (!debug.enabled && !apiCoverage)
107 return;
108 for (const methodName of Reflect.ownKeys(classType.prototype)) {
109 const method = Reflect.get(classType.prototype, methodName);
110 if (methodName === 'constructor' || typeof methodName !== 'string' || methodName.startsWith('_') || typeof method !== 'function')
111 continue;
112 if (apiCoverage)
113 apiCoverage.set(`${className}.${methodName}`, false);
114 Reflect.set(classType.prototype, methodName, function(...args) {
115 const argsText = args.map(stringifyArgument).join(', ');
116 const callsite = `${className}.${methodName}(${argsText})`;
117 if (debug.enabled)
118 debug(callsite);
119 if (apiCoverage)
120 apiCoverage.set(`${className}.${methodName}`, true);
121 return method.call(this, ...args);
122 });
123 }
124
125 if (classType.Events) {
126 if (apiCoverage) {
127 for (const event of Object.values(classType.Events))
128 apiCoverage.set(`${className}.emit(${JSON.stringify(event)})`, false);
129 }
130 const method = Reflect.get(classType.prototype, 'emit');
131 Reflect.set(classType.prototype, 'emit', function(event, ...args) {
132 const argsText = [JSON.stringify(event)].concat(args.map(stringifyArgument)).join(', ');
133 if (debug.enabled && this.listenerCount(event))
134 debug(`${className}.emit(${argsText})`);
135 if (apiCoverage && this.listenerCount(event))
136 apiCoverage.set(`${className}.emit(${JSON.stringify(event)})`, true);
137 return method.call(this, event, ...args);
138 });
139 }
140
141 /**
142 * @param {!Object} arg
143 * @return {string}
144 */
145 function stringifyArgument(arg) {
146 if (Helper.isString(arg) || Helper.isNumber(arg) || !arg)
147 return JSON.stringify(arg);
148 if (typeof arg === 'function') {
149 let text = arg.toString().split('\n').map(line => line.trim()).join('');
150 if (text.length > 20)
151 text = text.substring(0, 20) + '…';
152 return `"${text}"`;
153 }
154 const state = {};
155 const keys = Object.keys(arg);
156 for (const key of keys) {
157 const value = arg[key];
158 if (Helper.isString(value) || Helper.isNumber(value))
159 state[key] = JSON.stringify(value);
160 }
161 const name = arg.constructor.name === 'Object' ? '' : arg.constructor.name;
162 return name + JSON.stringify(state);
163 }
164 }
165
166 /**
167 * @param {!NodeJS.EventEmitter} emitter
168 * @param {string} eventName
169 * @param {function(?)} handler
170 * @return {{emitter: !NodeJS.EventEmitter, eventName: string, handler: function(?)}}
171 */
172 static addEventListener(emitter, eventName, handler) {
173 emitter.on(eventName, handler);
174 return { emitter, eventName, handler };
175 }
176
177 /**
178 * @param {!Array<{emitter: !NodeJS.EventEmitter, eventName: string, handler: function(?)}>} listeners
179 */
180 static removeEventListeners(listeners) {
181 for (const listener of listeners)
182 listener.emitter.removeListener(listener.eventName, listener.handler);
183 listeners.splice(0, listeners.length);
184 }
185
186 /**
187 * @return {?Map<string, boolean>}
188 */
189 static publicAPICoverage() {
190 return apiCoverage;
191 }
192
193 static recordPublicAPICoverage() {
194 apiCoverage = new Map();
195 }
196
197 /**
198 * @param {!Object} obj
199 * @return {boolean}
200 */
201 static isString(obj) {
202 return typeof obj === 'string' || obj instanceof String;
203 }
204
205 /**
206 * @param {!Object} obj
207 * @return {boolean}
208 */
209 static isNumber(obj) {
210 return typeof obj === 'number' || obj instanceof Number;
211 }
212
213 static promisify(nodeFunction) {
214 function promisified(...args) {
215 return new Promise((resolve, reject) => {
216 function callback(err, ...result) {
217 if (err)
218 return reject(err);
219 if (result.length === 1)
220 return resolve(result[0]);
221 return resolve(result);
222 }
223 nodeFunction.call(null, ...args, callback);
224 });
225 }
226 return promisified;
227 }
228}
229
230module.exports = {
231 helper: Helper,
232 debugError
233};