UNPKG

12.4 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 */
16
17const {helper, assert} = require('./helper');
18
19const EVALUATION_SCRIPT_URL = '__puppeteer_evaluation_script__';
20const SOURCE_URL_REGEX = /^[\040\t]*\/\/[@#] sourceURL=\s*(\S*?)\s*$/m;
21
22class ExecutionContext {
23 /**
24 * @param {!Puppeteer.CDPSession} client
25 * @param {!Protocol.Runtime.ExecutionContextDescription} contextPayload
26 * @param {function(!Protocol.Runtime.RemoteObject):!JSHandle} objectHandleFactory
27 * @param {?Puppeteer.Frame} frame
28 */
29 constructor(client, contextPayload, objectHandleFactory, frame) {
30 this._client = client;
31 this._frame = frame;
32 this._contextId = contextPayload.id;
33 this._objectHandleFactory = objectHandleFactory;
34 }
35
36 /**
37 * @return {?Puppeteer.Frame}
38 */
39 frame() {
40 return this._frame;
41 }
42
43 /**
44 * @param {Function|string} pageFunction
45 * @param {...*} args
46 * @return {!Promise<(!Object|undefined)>}
47 */
48 /* async */ evaluate(pageFunction, ...args) {return (fn => {
49 const gen = fn.call(this);
50 return new Promise((resolve, reject) => {
51 function step(key, arg) {
52 let info, value;
53 try {
54 info = gen[key](arg);
55 value = info.value;
56 } catch (error) {
57 reject(error);
58 return;
59 }
60 if (info.done) {
61 resolve(value);
62 } else {
63 return Promise.resolve(value).then(
64 value => {
65 step('next', value);
66 },
67 err => {
68 step('throw', err);
69 });
70 }
71 }
72 return step('next');
73 });
74})(function*(){
75 const handle = (yield this.evaluateHandle(pageFunction, ...args));
76 const result = (yield handle.jsonValue().catch(error => {
77 if (error.message.includes('Object reference chain is too long'))
78 return;
79 if (error.message.includes('Object couldn\'t be returned by value'))
80 return;
81 throw error;
82 }));
83 (yield handle.dispose());
84 return result;
85 });}
86
87 /**
88 * @param {Function|string} pageFunction
89 * @param {...*} args
90 * @return {!Promise<!JSHandle>}
91 */
92 /* async */ evaluateHandle(pageFunction, ...args) {return (fn => {
93 const gen = fn.call(this);
94 return new Promise((resolve, reject) => {
95 function step(key, arg) {
96 let info, value;
97 try {
98 info = gen[key](arg);
99 value = info.value;
100 } catch (error) {
101 reject(error);
102 return;
103 }
104 if (info.done) {
105 resolve(value);
106 } else {
107 return Promise.resolve(value).then(
108 value => {
109 step('next', value);
110 },
111 err => {
112 step('throw', err);
113 });
114 }
115 }
116 return step('next');
117 });
118})(function*(){
119 const suffix = `//# sourceURL=${EVALUATION_SCRIPT_URL}`;
120
121 if (helper.isString(pageFunction)) {
122 const contextId = this._contextId;
123 const expression = /** @type {string} */ (pageFunction);
124 const expressionWithSourceUrl = SOURCE_URL_REGEX.test(expression) ? expression : expression + '\n' + suffix;
125 const {exceptionDetails, result: remoteObject} = (yield this._client.send('Runtime.evaluate', {
126 expression: expressionWithSourceUrl,
127 contextId,
128 returnByValue: false,
129 awaitPromise: true,
130 userGesture: true
131 }).catch(rewriteError));
132 if (exceptionDetails)
133 throw new Error('Evaluation failed: ' + helper.getExceptionMessage(exceptionDetails));
134 return this._objectHandleFactory(remoteObject);
135 }
136
137 if (typeof pageFunction !== 'function')
138 throw new Error('The following is not a function: ' + pageFunction);
139
140 const { exceptionDetails, result: remoteObject } = (yield this._client.send('Runtime.callFunctionOn', {
141 functionDeclaration: pageFunction.toString() + '\n' + suffix + '\n',
142 executionContextId: this._contextId,
143 arguments: args.map(convertArgument.bind(this)),
144 returnByValue: false,
145 awaitPromise: true,
146 userGesture: true
147 }).catch(rewriteError));
148 if (exceptionDetails)
149 throw new Error('Evaluation failed: ' + helper.getExceptionMessage(exceptionDetails));
150 return this._objectHandleFactory(remoteObject);
151
152 /**
153 * @param {*} arg
154 * @return {*}
155 * @this {ExecutionContext}
156 */
157 function convertArgument(arg) {
158 if (Object.is(arg, -0))
159 return { unserializableValue: '-0' };
160 if (Object.is(arg, Infinity))
161 return { unserializableValue: 'Infinity' };
162 if (Object.is(arg, -Infinity))
163 return { unserializableValue: '-Infinity' };
164 if (Object.is(arg, NaN))
165 return { unserializableValue: 'NaN' };
166 const objectHandle = arg && (arg instanceof JSHandle) ? arg : null;
167 if (objectHandle) {
168 if (objectHandle._context !== this)
169 throw new Error('JSHandles can be evaluated only in the context they were created!');
170 if (objectHandle._disposed)
171 throw new Error('JSHandle is disposed!');
172 if (objectHandle._remoteObject.unserializableValue)
173 return { unserializableValue: objectHandle._remoteObject.unserializableValue };
174 if (!objectHandle._remoteObject.objectId)
175 return { value: objectHandle._remoteObject.value };
176 return { objectId: objectHandle._remoteObject.objectId };
177 }
178 return { value: arg };
179 }
180
181 /**
182 * @param {!Error} error
183 * @return {!Protocol.Runtime.evaluateReturnValue}
184 */
185 function rewriteError(error) {
186 if (error.message.endsWith('Cannot find context with specified id'))
187 throw new Error('Execution context was destroyed, most likely because of a navigation.');
188 throw error;
189 }
190 });}
191
192 /**
193 * @param {!JSHandle} prototypeHandle
194 * @return {!Promise<!JSHandle>}
195 */
196 /* async */ queryObjects(prototypeHandle) {return (fn => {
197 const gen = fn.call(this);
198 return new Promise((resolve, reject) => {
199 function step(key, arg) {
200 let info, value;
201 try {
202 info = gen[key](arg);
203 value = info.value;
204 } catch (error) {
205 reject(error);
206 return;
207 }
208 if (info.done) {
209 resolve(value);
210 } else {
211 return Promise.resolve(value).then(
212 value => {
213 step('next', value);
214 },
215 err => {
216 step('throw', err);
217 });
218 }
219 }
220 return step('next');
221 });
222})(function*(){
223 assert(!prototypeHandle._disposed, 'Prototype JSHandle is disposed!');
224 assert(prototypeHandle._remoteObject.objectId, 'Prototype JSHandle must not be referencing primitive value');
225 const response = (yield this._client.send('Runtime.queryObjects', {
226 prototypeObjectId: prototypeHandle._remoteObject.objectId
227 }));
228 return this._objectHandleFactory(response.objects);
229 });}
230}
231
232class JSHandle {
233 /**
234 * @param {!ExecutionContext} context
235 * @param {!Puppeteer.CDPSession} client
236 * @param {!Protocol.Runtime.RemoteObject} remoteObject
237 */
238 constructor(context, client, remoteObject) {
239 this._context = context;
240 this._client = client;
241 this._remoteObject = remoteObject;
242 this._disposed = false;
243 }
244
245 /**
246 * @return {!ExecutionContext}
247 */
248 executionContext() {
249 return this._context;
250 }
251
252 /**
253 * @param {string} propertyName
254 * @return {!Promise<?JSHandle>}
255 */
256 /* async */ getProperty(propertyName) {return (fn => {
257 const gen = fn.call(this);
258 return new Promise((resolve, reject) => {
259 function step(key, arg) {
260 let info, value;
261 try {
262 info = gen[key](arg);
263 value = info.value;
264 } catch (error) {
265 reject(error);
266 return;
267 }
268 if (info.done) {
269 resolve(value);
270 } else {
271 return Promise.resolve(value).then(
272 value => {
273 step('next', value);
274 },
275 err => {
276 step('throw', err);
277 });
278 }
279 }
280 return step('next');
281 });
282})(function*(){
283 const objectHandle = (yield this._context.evaluateHandle((object, propertyName) => {
284 const result = {__proto__: null};
285 result[propertyName] = object[propertyName];
286 return result;
287 }, this, propertyName));
288 const properties = (yield objectHandle.getProperties());
289 const result = properties.get(propertyName) || null;
290 (yield objectHandle.dispose());
291 return result;
292 });}
293
294 /**
295 * @return {!Promise<Map<string, !JSHandle>>}
296 */
297 /* async */ getProperties() {return (fn => {
298 const gen = fn.call(this);
299 return new Promise((resolve, reject) => {
300 function step(key, arg) {
301 let info, value;
302 try {
303 info = gen[key](arg);
304 value = info.value;
305 } catch (error) {
306 reject(error);
307 return;
308 }
309 if (info.done) {
310 resolve(value);
311 } else {
312 return Promise.resolve(value).then(
313 value => {
314 step('next', value);
315 },
316 err => {
317 step('throw', err);
318 });
319 }
320 }
321 return step('next');
322 });
323})(function*(){
324 const response = (yield this._client.send('Runtime.getProperties', {
325 objectId: this._remoteObject.objectId,
326 ownProperties: true
327 }));
328 const result = new Map();
329 for (const property of response.result) {
330 if (!property.enumerable)
331 continue;
332 result.set(property.name, this._context._objectHandleFactory(property.value));
333 }
334 return result;
335 });}
336
337 /**
338 * @return {!Promise<?Object>}
339 */
340 /* async */ jsonValue() {return (fn => {
341 const gen = fn.call(this);
342 return new Promise((resolve, reject) => {
343 function step(key, arg) {
344 let info, value;
345 try {
346 info = gen[key](arg);
347 value = info.value;
348 } catch (error) {
349 reject(error);
350 return;
351 }
352 if (info.done) {
353 resolve(value);
354 } else {
355 return Promise.resolve(value).then(
356 value => {
357 step('next', value);
358 },
359 err => {
360 step('throw', err);
361 });
362 }
363 }
364 return step('next');
365 });
366})(function*(){
367 if (this._remoteObject.objectId) {
368 const response = (yield this._client.send('Runtime.callFunctionOn', {
369 functionDeclaration: 'function() { return this; }',
370 objectId: this._remoteObject.objectId,
371 returnByValue: true,
372 awaitPromise: true,
373 }));
374 return helper.valueFromRemoteObject(response.result);
375 }
376 return helper.valueFromRemoteObject(this._remoteObject);
377 });}
378
379 /**
380 * @return {?Puppeteer.ElementHandle}
381 */
382 asElement() {
383 return null;
384 }
385
386 /* async */ dispose() {return (fn => {
387 const gen = fn.call(this);
388 return new Promise((resolve, reject) => {
389 function step(key, arg) {
390 let info, value;
391 try {
392 info = gen[key](arg);
393 value = info.value;
394 } catch (error) {
395 reject(error);
396 return;
397 }
398 if (info.done) {
399 resolve(value);
400 } else {
401 return Promise.resolve(value).then(
402 value => {
403 step('next', value);
404 },
405 err => {
406 step('throw', err);
407 });
408 }
409 }
410 return step('next');
411 });
412})(function*(){
413 if (this._disposed)
414 return;
415 this._disposed = true;
416 (yield helper.releaseObject(this._client, this._remoteObject));
417 });}
418
419 /**
420 * @override
421 * @return {string}
422 */
423 toString() {
424 if (this._remoteObject.objectId) {
425 const type = this._remoteObject.subtype || this._remoteObject.type;
426 return 'JSHandle@' + type;
427 }
428 return 'JSHandle:' + helper.valueFromRemoteObject(this._remoteObject);
429 }
430}
431
432helper.tracePublicAPI(JSHandle);
433module.exports = {ExecutionContext, JSHandle, EVALUATION_SCRIPT_URL};