UNPKG

9.62 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 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 || exceptionDetails.exception.value;
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 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) {return (fn => {
104 const gen = fn.call(this);
105 return new Promise((resolve, reject) => {
106 function step(key, arg) {
107 let info, value;
108 try {
109 info = gen[key](arg);
110 value = info.value;
111 } catch (error) {
112 reject(error);
113 return;
114 }
115 if (info.done) {
116 resolve(value);
117 } else {
118 return Promise.resolve(value).then(
119 value => {
120 step('next', value);
121 },
122 err => {
123 step('throw', err);
124 });
125 }
126 }
127 return step('next');
128 });
129})(function*(){
130 if (!remoteObject.objectId)
131 return;
132 (yield client.send('Runtime.releaseObject', {objectId: remoteObject.objectId}).catch(error => {
133 // Exceptions might happen in case of a page been navigated or closed.
134 // Swallow these since they are harmless and we don't leak anything in this case.
135 debugError(error);
136 }));
137 });}
138
139 /**
140 * @param {!Object} classType
141 * @param {string=} publicName
142 */
143 static tracePublicAPI(classType, publicName) {
144 let className = publicName || classType.prototype.constructor.name;
145 className = className.substring(0, 1).toLowerCase() + className.substring(1);
146 const debug = require('debug')(`puppeteer:${className}`);
147 if (!debug.enabled && !apiCoverage)
148 return;
149 for (const methodName of Reflect.ownKeys(classType.prototype)) {
150 const method = Reflect.get(classType.prototype, methodName);
151 if (methodName === 'constructor' || typeof methodName !== 'string' || methodName.startsWith('_') || typeof method !== 'function')
152 continue;
153 if (apiCoverage)
154 apiCoverage.set(`${className}.${methodName}`, false);
155 Reflect.set(classType.prototype, methodName, function(...args) {
156 const argsText = args.map(stringifyArgument).join(', ');
157 const callsite = `${className}.${methodName}(${argsText})`;
158 if (debug.enabled)
159 debug(callsite);
160 if (apiCoverage)
161 apiCoverage.set(`${className}.${methodName}`, true);
162 return method.call(this, ...args);
163 });
164 }
165
166 if (classType.Events) {
167 if (apiCoverage) {
168 for (const event of Object.values(classType.Events))
169 apiCoverage.set(`${className}.emit(${JSON.stringify(event)})`, false);
170 }
171 const method = Reflect.get(classType.prototype, 'emit');
172 Reflect.set(classType.prototype, 'emit', function(event, ...args) {
173 const argsText = [JSON.stringify(event)].concat(args.map(stringifyArgument)).join(', ');
174 if (debug.enabled && this.listenerCount(event))
175 debug(`${className}.emit(${argsText})`);
176 if (apiCoverage && this.listenerCount(event))
177 apiCoverage.set(`${className}.emit(${JSON.stringify(event)})`, true);
178 return method.call(this, event, ...args);
179 });
180 }
181
182 /**
183 * @param {!Object} arg
184 * @return {string}
185 */
186 function stringifyArgument(arg) {
187 if (Helper.isString(arg) || Helper.isNumber(arg) || !arg)
188 return JSON.stringify(arg);
189 if (typeof arg === 'function') {
190 let text = arg.toString().split('\n').map(line => line.trim()).join('');
191 if (text.length > 20)
192 text = text.substring(0, 20) + '…';
193 return `"${text}"`;
194 }
195 const state = {};
196 const keys = Object.keys(arg);
197 for (const key of keys) {
198 const value = arg[key];
199 if (Helper.isString(value) || Helper.isNumber(value))
200 state[key] = JSON.stringify(value);
201 }
202 const name = arg.constructor.name === 'Object' ? '' : arg.constructor.name;
203 return name + JSON.stringify(state);
204 }
205 }
206
207 /**
208 * @param {!NodeJS.EventEmitter} emitter
209 * @param {string} eventName
210 * @param {function(?)} handler
211 * @return {{emitter: !NodeJS.EventEmitter, eventName: string, handler: function(?)}}
212 */
213 static addEventListener(emitter, eventName, handler) {
214 emitter.on(eventName, handler);
215 return { emitter, eventName, handler };
216 }
217
218 /**
219 * @param {!Array<{emitter: !NodeJS.EventEmitter, eventName: string, handler: function(?)}>} listeners
220 */
221 static removeEventListeners(listeners) {
222 for (const listener of listeners)
223 listener.emitter.removeListener(listener.eventName, listener.handler);
224 listeners.splice(0, listeners.length);
225 }
226
227 /**
228 * @return {?Map<string, boolean>}
229 */
230 static publicAPICoverage() {
231 return apiCoverage;
232 }
233
234 static recordPublicAPICoverage() {
235 apiCoverage = new Map();
236 }
237
238 /**
239 * @param {!Object} obj
240 * @return {boolean}
241 */
242 static isString(obj) {
243 return typeof obj === 'string' || obj instanceof String;
244 }
245
246 /**
247 * @param {!Object} obj
248 * @return {boolean}
249 */
250 static isNumber(obj) {
251 return typeof obj === 'number' || obj instanceof Number;
252 }
253
254 static promisify(nodeFunction) {
255 function promisified(...args) {
256 return new Promise((resolve, reject) => {
257 function callback(err, ...result) {
258 if (err)
259 return reject(err);
260 if (result.length === 1)
261 return resolve(result[0]);
262 return resolve(result);
263 }
264 nodeFunction.call(null, ...args, callback);
265 });
266 }
267 return promisified;
268 }
269
270 /**
271 * @param {!NodeJS.EventEmitter} emitter
272 * @param {string} eventName
273 * @param {function} predicate
274 * @return {!Promise}
275 */
276 static waitForEvent(emitter, eventName, predicate, timeout) {
277 let eventTimeout, resolveCallback, rejectCallback;
278 const promise = new Promise((resolve, reject) => {
279 resolveCallback = resolve;
280 rejectCallback = reject;
281 });
282 const listener = Helper.addEventListener(emitter, eventName, event => {
283 if (!predicate(event))
284 return;
285 cleanup();
286 resolveCallback(event);
287 });
288 if (timeout) {
289 eventTimeout = setTimeout(() => {
290 cleanup();
291 rejectCallback(new Error('Timeout exceeded while waiting for event'));
292 }, timeout);
293 }
294 function cleanup() {
295 Helper.removeEventListeners([listener]);
296 clearTimeout(eventTimeout);
297 }
298 return promise;
299 }
300}
301
302/**
303 * @param {*} value
304 * @param {string=} message
305 */
306function assert(value, message) {
307 if (!value)
308 throw new Error(message);
309}
310
311module.exports = {
312 helper: Helper,
313 assert,
314 debugError
315};