UNPKG

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