UNPKG

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