/** * @license * Copyright 2018 Google Inc. * SPDX-License-Identifier: Apache-2.0 */ import expect from 'expect'; import {getTestState, setupTestBrowserHooks} from './mocha-utils.js'; import {attachFrame} from './utils.js'; describe('Evaluation specs', function () { setupTestBrowserHooks(); describe('Page.evaluate', function () { it('should work', async () => { const {page} = await getTestState(); const result = await page.evaluate(() => { return 7 * 3; }); expect(result).toBe(21); }); it('should transfer BigInt', async () => { const {page} = await getTestState(); const result = await page.evaluate((a: bigint) => { return a; }, BigInt(42)); expect(result).toBe(BigInt(42)); }); it('should transfer NaN', async () => { const {page} = await getTestState(); const result = await page.evaluate(a => { return a; }, NaN); expect(Object.is(result, NaN)).toBe(true); }); it('should transfer -0', async () => { const {page} = await getTestState(); const result = await page.evaluate(a => { return a; }, -0); expect(Object.is(result, -0)).toBe(true); }); it('should transfer Infinity', async () => { const {page} = await getTestState(); const result = await page.evaluate(a => { return a; }, Infinity); expect(Object.is(result, Infinity)).toBe(true); }); it('should transfer -Infinity', async () => { const {page} = await getTestState(); const result = await page.evaluate(a => { return a; }, -Infinity); expect(Object.is(result, -Infinity)).toBe(true); }); it('should transfer arrays', async () => { const {page} = await getTestState(); const result = await page.evaluate( a => { return a; }, [1, 2, 3] ); expect(result).toEqual([1, 2, 3]); }); it('should transfer arrays as arrays, not objects', async () => { const {page} = await getTestState(); const result = await page.evaluate( a => { return Array.isArray(a); }, [1, 2, 3] ); expect(result).toBe(true); }); it('should modify global environment', async () => { const {page} = await getTestState(); await page.evaluate(() => { return ((globalThis as any).globalVar = 123); }); expect(await page.evaluate('globalVar')).toBe(123); }); it('should evaluate in the page context', async () => { const {page, server} = await getTestState(); await page.goto(server.PREFIX + '/global-var.html'); expect(await page.evaluate('globalVar')).toBe(123); }); it('should replace symbols with undefined', async () => { const {page} = await getTestState(); expect( await page.evaluate(() => { return [Symbol('foo4'), 'foo']; }) ).toEqual([undefined, 'foo']); }); it('should work with function shorthands', async () => { const {page} = await getTestState(); const a = { sum(a: number, b: number) { return a + b; }, async mult(a: number, b: number) { return a * b; }, }; expect(await page.evaluate(a.sum, 1, 2)).toBe(3); expect(await page.evaluate(a.mult, 2, 4)).toBe(8); }); it('should work with unicode chars', async () => { const {page} = await getTestState(); const result = await page.evaluate( a => { return a['中文字符']; }, { 中文字符: 42, } ); expect(result).toBe(42); }); it('should throw when evaluation triggers reload', async () => { const {page} = await getTestState(); let error!: Error; await page .evaluate(() => { location.reload(); return new Promise(() => {}); }) .catch(error_ => { return (error = error_); }); expect(error.message).toContain('Protocol error'); }); it('should await promise', async () => { const {page} = await getTestState(); const result = await page.evaluate(() => { return Promise.resolve(8 * 7); }); expect(result).toBe(56); }); it('should work right after framenavigated', async () => { const {page, server} = await getTestState(); let frameEvaluation = null; page.on('framenavigated', async frame => { frameEvaluation = frame.evaluate(() => { return 6 * 7; }); }); await page.goto(server.EMPTY_PAGE); expect(await frameEvaluation).toBe(42); }); it('should work from-inside an exposed function', async () => { const {page} = await getTestState(); // Setup inpage callback, which calls Page.evaluate await page.exposeFunction( 'callController', async function (a: number, b: number) { return await page.evaluate( (a: number, b: number): number => { return a * b; }, a, b ); } ); const result = await page.evaluate(async function () { return (globalThis as any).callController(9, 3); }); expect(result).toBe(27); }); it('should reject promise with exception', async () => { const {page} = await getTestState(); let error!: Error; await page .evaluate(() => { // @ts-expect-error we know the object doesn't exist return notExistingObject.property; }) .catch(error_ => { return (error = error_); }); expect(error).toBeTruthy(); expect(error.message).toContain('notExistingObject'); }); it('should support thrown strings as error messages', async () => { const {page} = await getTestState(); let error!: Error; await page .evaluate(() => { throw 'qwerty'; }) .catch(error_ => { return (error = error_); }); expect(error).toEqual('qwerty'); }); it('should support thrown numbers as error messages', async () => { const {page} = await getTestState(); let error!: Error; await page .evaluate(() => { throw 100500; }) .catch(error_ => { return (error = error_); }); expect(error).toEqual(100500); }); it('should return complex objects', async () => { const {page} = await getTestState(); const object = {foo: 'bar!'}; const result = await page.evaluate(a => { return a; }, object); expect(result).not.toBe(object); expect(result).toEqual(object); }); it('should return BigInt', async () => { const {page} = await getTestState(); const result = await page.evaluate(() => { return BigInt(42); }); expect(result).toBe(BigInt(42)); }); it('should return NaN', async () => { const {page} = await getTestState(); const result = await page.evaluate(() => { return NaN; }); expect(Object.is(result, NaN)).toBe(true); }); it('should return -0', async () => { const {page} = await getTestState(); const result = await page.evaluate(() => { return -0; }); expect(Object.is(result, -0)).toBe(true); }); it('should return Infinity', async () => { const {page} = await getTestState(); const result = await page.evaluate(() => { return Infinity; }); expect(Object.is(result, Infinity)).toBe(true); }); it('should return -Infinity', async () => { const {page} = await getTestState(); const result = await page.evaluate(() => { return -Infinity; }); expect(Object.is(result, -Infinity)).toBe(true); }); it('should accept "null" as one of multiple parameters', async () => { const {page} = await getTestState(); const result = await page.evaluate( (a, b) => { return Object.is(a, null) && Object.is(b, 'foo'); }, null, 'foo' ); expect(result).toBe(true); }); it('should properly serialize null fields', async () => { const {page} = await getTestState(); expect( await page.evaluate(() => { return {a: undefined}; }) ).toEqual({}); }); it('should return undefined for non-serializable objects', async () => { const {page} = await getTestState(); expect( await page.evaluate(() => { return window; }) ).toBe(undefined); }); it('should return promise as empty object', async () => { const {page} = await getTestState(); const result = await page.evaluate(() => { return { promise: new Promise(resolve => { setTimeout(resolve, 1000); }), }; }); expect(result).toEqual({ promise: {}, }); }); it('should work for circular object', async () => { const {page} = await getTestState(); const result = await page.evaluate(() => { const a: Record = { c: 5, d: { foo: 'bar', }, }; const b = {a}; a['b'] = b; return a; }); expect(result).toMatchObject({ c: 5, d: { foo: 'bar', }, b: { a: undefined, }, }); }); it('should accept a string', async () => { const {page} = await getTestState(); const result = await page.evaluate('1 + 2'); expect(result).toBe(3); }); it('should accept a string with semi colons', async () => { const {page} = await getTestState(); const result = await page.evaluate('1 + 5;'); expect(result).toBe(6); }); it('should accept a string with comments', async () => { const {page} = await getTestState(); const result = await page.evaluate('2 + 5;\n// do some math!'); expect(result).toBe(7); }); it('should accept element handle as an argument', async () => { const {page} = await getTestState(); await page.setContent('
42
'); using element = (await page.$('section'))!; const text = await page.evaluate(e => { return e.textContent; }, element); expect(text).toBe('42'); }); it('should throw if underlying element was disposed', async () => { const {page} = await getTestState(); await page.setContent('
39
'); using element = (await page.$('section'))!; expect(element).toBeTruthy(); // We want to dispose early. await element.dispose(); let error!: Error; await page .evaluate(e => { return e.textContent; }, element) .catch(error_ => { return (error = error_); }); expect(error.message).toContain('JSHandle is disposed'); }); it('should throw if elementHandles are from other frames', async () => { const {page, server} = await getTestState(); await attachFrame(page, 'frame1', server.EMPTY_PAGE); using bodyHandle = await page.frames()[1]!.$('body'); let error!: Error; await page .evaluate(body => { return body?.innerHTML; }, bodyHandle) .catch(error_ => { return (error = error_); }); expect(error).toBeTruthy(); expect(error.message).atLeastOneToContain([ 'JSHandles can be evaluated only in the context they were created', "Trying to evaluate JSHandle from different frames. Usually this means you're using a handle from a page on a different page.", ]); }); it('should simulate a user gesture', async () => { const {page} = await getTestState(); const result = await page.evaluate(() => { document.body.appendChild(document.createTextNode('test')); document.execCommand('selectAll'); return document.execCommand('copy'); }); expect(result).toBe(true); }); it('should not throw an error when evaluation does a navigation', async () => { const {page, server} = await getTestState(); await page.goto(server.PREFIX + '/one-style.html'); const onRequest = server.waitForRequest('/empty.html'); const result = await page.evaluate(() => { (window as any).location = '/empty.html'; return [42]; }); expect(result).toEqual([42]); await onRequest; }); it('should transfer 100Mb of data from page to node.js', async function () { this.timeout(25_000); const {page} = await getTestState(); const a = await page.evaluate(() => { return Array(100 * 1024 * 1024 + 1).join('a'); }); expect(a.length).toBe(100 * 1024 * 1024); }); it('should throw error with detailed information on exception inside promise', async () => { const {page} = await getTestState(); let error!: Error; await page .evaluate(() => { return new Promise(() => { throw new Error('Error in promise'); }); }) .catch(error_ => { return (error = error_); }); expect(error.message).toContain('Error in promise'); }); it('should return properly serialize objects with unknown type fields', async () => { const {page} = await getTestState(); await page.setContent( "" ); const result = await page.evaluate(async () => { const image = document.querySelector('img')!; const imageBitmap = await createImageBitmap(image); return { a: 'foo', b: imageBitmap, }; }); expect(result).toEqual({ a: 'foo', b: undefined, }); }); }); describe('Page.evaluateOnNewDocument', function () { it('should evaluate before anything else on the page', async () => { const {page, server} = await getTestState(); await page.evaluateOnNewDocument(function () { (globalThis as any).injected = 123; }); await page.goto(server.PREFIX + '/tamperable.html'); expect( await page.evaluate(() => { return (globalThis as any).result; }) ).toBe(123); }); it('should work with CSP', async () => { const {page, server} = await getTestState(); server.setCSP('/empty.html', 'script-src ' + server.PREFIX); await page.evaluateOnNewDocument(function () { (globalThis as any).injected = 123; }); await page.goto(server.PREFIX + '/empty.html'); expect( await page.evaluate(() => { return (globalThis as any).injected; }) ).toBe(123); // Make sure CSP works. await page.addScriptTag({content: 'window.e = 10;'}).catch(error => { return void error; }); expect( await page.evaluate(() => { return (window as any).e; }) ).toBe(undefined); }); }); describe('Page.removeScriptToEvaluateOnNewDocument', function () { it('should remove new document script', async () => { const {page, server} = await getTestState(); const {identifier} = await page.evaluateOnNewDocument(function () { (globalThis as any).injected = 123; }); await page.goto(server.PREFIX + '/tamperable.html'); expect( await page.evaluate(() => { return (globalThis as any).result; }) ).toBe(123); await page.removeScriptToEvaluateOnNewDocument(identifier); await page.reload(); expect( await page.evaluate(() => { return (globalThis as any).result || null; }) ).toBe(null); }); }); describe('Frame.evaluate', function () { it('should have different execution contexts', async () => { const {page, server} = await getTestState(); await page.goto(server.EMPTY_PAGE); await attachFrame(page, 'frame1', server.EMPTY_PAGE); expect(page.frames()).toHaveLength(2); await page.frames()[0]!.evaluate(() => { return ((globalThis as any).FOO = 'foo'); }); await page.frames()[1]!.evaluate(() => { return ((globalThis as any).FOO = 'bar'); }); expect( await page.frames()[0]!.evaluate(() => { return (globalThis as any).FOO; }) ).toBe('foo'); expect( await page.frames()[1]!.evaluate(() => { return (globalThis as any).FOO; }) ).toBe('bar'); }); it('should have correct execution contexts', async () => { const {page, server} = await getTestState(); await page.goto(server.PREFIX + '/frames/one-frame.html'); expect(page.frames()).toHaveLength(2); expect( await page.frames()[0]!.evaluate(() => { return document.body.textContent!.trim(); }) ).toBe(''); expect( await page.frames()[1]!.evaluate(() => { return document.body.textContent!.trim(); }) ).toBe(`Hi, I'm frame`); }); it('should execute after cross-site navigation', async () => { const {page, server} = await getTestState(); await page.goto(server.EMPTY_PAGE); const mainFrame = page.mainFrame(); expect( await mainFrame.evaluate(() => { return window.location.href; }) ).toContain('localhost'); await page.goto(server.CROSS_PROCESS_PREFIX + '/empty.html'); expect( await mainFrame.evaluate(() => { return window.location.href; }) ).toContain('127'); }); }); });