1/**
2 * Copyright 2018 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
17import utils from './utils.js';
18import expect from 'expect';
19import {
20  getTestState,
21  setupTestBrowserHooks,
22  setupTestPageAndContextHooks,
23  itFailsFirefox,
24  describeFailsFirefox,
25} from './mocha-utils'; // eslint-disable-line import/extensions
26
27const bigint = typeof BigInt !== 'undefined';
28
29describe('Evaluation specs', function () {
30  setupTestBrowserHooks();
31  setupTestPageAndContextHooks();
32
33  describe('Page.evaluate', function () {
34    it('should work', async () => {
35      const { page } = getTestState();
36
37      const result = await page.evaluate(() => 7 * 3);
38      expect(result).toBe(21);
39    });
40    (bigint ? it : xit)('should transfer BigInt', async () => {
41      const { page } = getTestState();
42
43      const result = await page.evaluate((a: BigInt) => a, BigInt(42));
44      expect(result).toBe(BigInt(42));
45    });
46    it('should transfer NaN', async () => {
47      const { page } = getTestState();
48
49      const result = await page.evaluate((a) => a, NaN);
50      expect(Object.is(result, NaN)).toBe(true);
51    });
52    it('should transfer -0', async () => {
53      const { page } = getTestState();
54
55      const result = await page.evaluate((a) => a, -0);
56      expect(Object.is(result, -0)).toBe(true);
57    });
58    it('should transfer Infinity', async () => {
59      const { page } = getTestState();
60
61      const result = await page.evaluate((a) => a, Infinity);
62      expect(Object.is(result, Infinity)).toBe(true);
63    });
64    it('should transfer -Infinity', async () => {
65      const { page } = getTestState();
66
67      const result = await page.evaluate((a) => a, -Infinity);
68      expect(Object.is(result, -Infinity)).toBe(true);
69    });
70    it('should transfer arrays', async () => {
71      const { page } = getTestState();
72
73      const result = await page.evaluate((a) => a, [1, 2, 3]);
74      expect(result).toEqual([1, 2, 3]);
75    });
76    it('should transfer arrays as arrays, not objects', async () => {
77      const { page } = getTestState();
78
79      const result = await page.evaluate((a) => Array.isArray(a), [1, 2, 3]);
80      expect(result).toBe(true);
81    });
82    it('should modify global environment', async () => {
83      const { page } = getTestState();
84
85      await page.evaluate(() => (globalThis.globalVar = 123));
86      expect(await page.evaluate('globalVar')).toBe(123);
87    });
88    it('should evaluate in the page context', async () => {
89      const { page, server } = getTestState();
90
91      await page.goto(server.PREFIX + '/global-var.html');
92      expect(await page.evaluate('globalVar')).toBe(123);
93    });
94    it(
95      'should return undefined for objects with symbols',
96      async () => {
97        const { page } = getTestState();
98
99        expect(await page.evaluate(() => [Symbol('foo4')])).toBe(undefined);
100      }
101    );
102    it('should work with function shorthands', async () => {
103      const { page } = getTestState();
104
105      const a = {
106        sum(a, b) {
107          return a + b;
108        },
109
110        async mult(a, b) {
111          return a * b;
112        },
113      };
114      expect(await page.evaluate(a.sum, 1, 2)).toBe(3);
115      expect(await page.evaluate(a.mult, 2, 4)).toBe(8);
116    });
117    it('should work with unicode chars', async () => {
118      const { page } = getTestState();
119
120      const result = await page.evaluate((a) => a['中文字符'], {
121        中文字符: 42,
122      });
123      expect(result).toBe(42);
124    });
125    it('should throw when evaluation triggers reload', async () => {
126      const { page } = getTestState();
127
128      let error = null;
129      await page
130        .evaluate(() => {
131          location.reload();
132          return new Promise(() => {});
133        })
134        .catch((error_) => (error = error_));
135      expect(error.message).toContain('Protocol error');
136    });
137    it('should await promise', async () => {
138      const { page } = getTestState();
139
140      const result = await page.evaluate(() => Promise.resolve(8 * 7));
141      expect(result).toBe(56);
142    });
143    it('should work right after framenavigated', async () => {
144      const { page, server } = getTestState();
145
146      let frameEvaluation = null;
147      page.on('framenavigated', async (frame) => {
148        frameEvaluation = frame.evaluate(() => 6 * 7);
149      });
150      await page.goto(server.EMPTY_PAGE);
151      expect(await frameEvaluation).toBe(42);
152    });
153    it('should work from-inside an exposed function', async () => {
154      const { page } = getTestState();
155
156      // Setup inpage callback, which calls Page.evaluate
157      await page.exposeFunction('callController', async function (a, b) {
158        return await page.evaluate<(a: number, b: number) => number>(
159          (a, b) => a * b,
160          a,
161          b
162        );
163      });
164      const result = await page.evaluate(async function () {
165        return await globalThis.callController(9, 3);
166      });
167      expect(result).toBe(27);
168    });
169    it('should reject promise with exception', async () => {
170      const { page } = getTestState();
171
172      let error = null;
173      await page
174        // @ts-expect-error we know the object doesn't exist
175        .evaluate(() => notExistingObject.property)
176        .catch((error_) => (error = error_));
177      expect(error).toBeTruthy();
178      expect(error.message).toContain('notExistingObject');
179    });
180    it('should support thrown strings as error messages', async () => {
181      const { page } = getTestState();
182
183      let error = null;
184      await page
185        .evaluate(() => {
186          throw 'qwerty';
187        })
188        .catch((error_) => (error = error_));
189      expect(error).toBeTruthy();
190      expect(error.message).toContain('qwerty');
191    });
192    it('should support thrown numbers as error messages', async () => {
193      const { page } = getTestState();
194
195      let error = null;
196      await page
197        .evaluate(() => {
198          throw 100500;
199        })
200        .catch((error_) => (error = error_));
201      expect(error).toBeTruthy();
202      expect(error.message).toContain('100500');
203    });
204    it('should return complex objects', async () => {
205      const { page } = getTestState();
206
207      const object = { foo: 'bar!' };
208      const result = await page.evaluate((a) => a, object);
209      expect(result).not.toBe(object);
210      expect(result).toEqual(object);
211    });
212    (bigint ? it : xit)('should return BigInt', async () => {
213      const { page } = getTestState();
214
215      const result = await page.evaluate(() => BigInt(42));
216      expect(result).toBe(BigInt(42));
217    });
218    it('should return NaN', async () => {
219      const { page } = getTestState();
220
221      const result = await page.evaluate(() => NaN);
222      expect(Object.is(result, NaN)).toBe(true);
223    });
224    it('should return -0', async () => {
225      const { page } = getTestState();
226
227      const result = await page.evaluate(() => -0);
228      expect(Object.is(result, -0)).toBe(true);
229    });
230    it('should return Infinity', async () => {
231      const { page } = getTestState();
232
233      const result = await page.evaluate(() => Infinity);
234      expect(Object.is(result, Infinity)).toBe(true);
235    });
236    it('should return -Infinity', async () => {
237      const { page } = getTestState();
238
239      const result = await page.evaluate(() => -Infinity);
240      expect(Object.is(result, -Infinity)).toBe(true);
241    });
242    it('should accept "undefined" as one of multiple parameters', async () => {
243      const { page } = getTestState();
244
245      const result = await page.evaluate(
246        (a, b) => Object.is(a, undefined) && Object.is(b, 'foo'),
247        undefined,
248        'foo'
249      );
250      expect(result).toBe(true);
251    });
252    it('should properly serialize null fields', async () => {
253      const { page } = getTestState();
254
255      expect(await page.evaluate(() => ({ a: undefined }))).toEqual({});
256    });
257    it(
258      'should return undefined for non-serializable objects',
259      async () => {
260        const { page } = getTestState();
261
262        expect(await page.evaluate(() => window)).toBe(undefined);
263      }
264    );
265    it('should fail for circular object', async () => {
266      const { page } = getTestState();
267
268      const result = await page.evaluate(() => {
269        const a: { [x: string]: any } = {};
270        const b = { a };
271        a.b = b;
272        return a;
273      });
274      expect(result).toBe(undefined);
275    });
276    it('should be able to throw a tricky error', async () => {
277      const { page } = getTestState();
278
279      const windowHandle = await page.evaluateHandle(() => window);
280      const errorText = await windowHandle
281        .jsonValue<string>()
282        .catch((error_) => error_.message);
283      const error = await page
284        .evaluate<(errorText: string) => Error>((errorText) => {
285          throw new Error(errorText);
286        }, errorText)
287        .catch((error_) => error_);
288      expect(error.message).toContain(errorText);
289    });
290    it('should accept a string', async () => {
291      const { page } = getTestState();
292
293      const result = await page.evaluate('1 + 2');
294      expect(result).toBe(3);
295    });
296    it('should accept a string with semi colons', async () => {
297      const { page } = getTestState();
298
299      const result = await page.evaluate('1 + 5;');
300      expect(result).toBe(6);
301    });
302    it('should accept a string with comments', async () => {
303      const { page } = getTestState();
304
305      const result = await page.evaluate('2 + 5;\n// do some math!');
306      expect(result).toBe(7);
307    });
308    it('should accept element handle as an argument', async () => {
309      const { page } = getTestState();
310
311      await page.setContent('<section>42</section>');
312      const element = await page.$('section');
313      const text = await page.evaluate<(e: HTMLElement) => string>(
314        (e) => e.textContent,
315        element
316      );
317      expect(text).toBe('42');
318    });
319    it('should throw if underlying element was disposed', async () => {
320      const { page } = getTestState();
321
322      await page.setContent('<section>39</section>');
323      const element = await page.$('section');
324      expect(element).toBeTruthy();
325      await element.dispose();
326      let error = null;
327      await page
328        .evaluate((e: HTMLElement) => e.textContent, element)
329        .catch((error_) => (error = error_));
330      expect(error.message).toContain('JSHandle is disposed');
331    });
332    it(
333      'should throw if elementHandles are from other frames',
334      async () => {
335        const { page, server } = getTestState();
336
337        await utils.attachFrame(page, 'frame1', server.EMPTY_PAGE);
338        const bodyHandle = await page.frames()[1].$('body');
339        let error = null;
340        await page
341          .evaluate((body: HTMLElement) => body.innerHTML, bodyHandle)
342          .catch((error_) => (error = error_));
343        expect(error).toBeTruthy();
344        expect(error.message).toContain(
345          'JSHandles can be evaluated only in the context they were created'
346        );
347      }
348    );
349    it('should simulate a user gesture', async () => {
350      const { page } = getTestState();
351
352      const result = await page.evaluate(() => {
353        document.body.appendChild(document.createTextNode('test'));
354        document.execCommand('selectAll');
355        return document.execCommand('copy');
356      });
357      expect(result).toBe(true);
358    });
359    it('should throw a nice error after a navigation', async () => {
360      const { page } = getTestState();
361
362      const executionContext = await page.mainFrame().executionContext();
363
364      await Promise.all([
365        page.waitForNavigation(),
366        executionContext.evaluate(() => window.location.reload()),
367      ]);
368      const error = await executionContext
369        .evaluate(() => null)
370        .catch((error_) => error_);
371      expect((error as Error).message).toContain('navigation');
372    });
373    it(
374      'should not throw an error when evaluation does a navigation',
375      async () => {
376        const { page, server } = getTestState();
377
378        await page.goto(server.PREFIX + '/one-style.html');
379        const result = await page.evaluate(() => {
380          (window as any).location = '/empty.html';
381          return [42];
382        });
383        expect(result).toEqual([42]);
384      }
385    );
386    it('should transfer 100Mb of data from page to node.js', async function () {
387      const { page } = getTestState();
388
389      const a = await page.evaluate<() => string>(() =>
390        Array(100 * 1024 * 1024 + 1).join('a')
391      );
392      expect(a.length).toBe(100 * 1024 * 1024);
393    });
394    it('should throw error with detailed information on exception inside promise ', async () => {
395      const { page } = getTestState();
396
397      let error = null;
398      await page
399        .evaluate(
400          () =>
401            new Promise(() => {
402              throw new Error('Error in promise');
403            })
404        )
405        .catch((error_) => (error = error_));
406      expect(error.message).toContain('Error in promise');
407    });
408  });
409
410  describe('Page.evaluateOnNewDocument', function () {
411    it('should evaluate before anything else on the page', async () => {
412      const { page, server } = getTestState();
413
414      await page.evaluateOnNewDocument(function () {
415        globalThis.injected = 123;
416      });
417      await page.goto(server.PREFIX + '/tamperable.html');
418      expect(await page.evaluate(() => globalThis.result)).toBe(123);
419    });
420    it('should work with CSP', async () => {
421      const { page, server } = getTestState();
422
423      server.setCSP('/empty.html', 'script-src ' + server.PREFIX);
424      await page.evaluateOnNewDocument(function () {
425        globalThis.injected = 123;
426      });
427      await page.goto(server.PREFIX + '/empty.html');
428      expect(await page.evaluate(() => globalThis.injected)).toBe(123);
429
430      // Make sure CSP works.
431      await page
432        .addScriptTag({ content: 'window.e = 10;' })
433        .catch((error) => void error);
434      expect(await page.evaluate(() => (window as any).e)).toBe(undefined);
435    });
436  });
437
438  describe('Frame.evaluate', function () {
439    it('should have different execution contexts', async () => {
440      const { page, server } = getTestState();
441
442      await page.goto(server.EMPTY_PAGE);
443      await utils.attachFrame(page, 'frame1', server.EMPTY_PAGE);
444      expect(page.frames().length).toBe(2);
445      await page.frames()[0].evaluate(() => (globalThis.FOO = 'foo'));
446      await page.frames()[1].evaluate(() => (globalThis.FOO = 'bar'));
447      expect(await page.frames()[0].evaluate(() => globalThis.FOO)).toBe('foo');
448      expect(await page.frames()[1].evaluate(() => globalThis.FOO)).toBe('bar');
449    });
450    it('should have correct execution contexts', async () => {
451      const { page, server } = getTestState();
452
453      await page.goto(server.PREFIX + '/frames/one-frame.html');
454      expect(page.frames().length).toBe(2);
455      expect(
456        await page.frames()[0].evaluate(() => document.body.textContent.trim())
457      ).toBe('');
458      expect(
459        await page.frames()[1].evaluate(() => document.body.textContent.trim())
460      ).toBe(`Hi, I'm frame`);
461    });
462    it('should execute after cross-site navigation', async () => {
463      const { page, server } = getTestState();
464
465      await page.goto(server.EMPTY_PAGE);
466      const mainFrame = page.mainFrame();
467      expect(await mainFrame.evaluate(() => window.location.href)).toContain(
468        'localhost'
469      );
470      await page.goto(server.CROSS_PROCESS_PREFIX + '/empty.html');
471      expect(await mainFrame.evaluate(() => window.location.href)).toContain(
472        '127'
473      );
474    });
475  });
476});
477