UNPKG

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