UNPKG

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