puppeteer/test/src/evaluation.spec.ts

623 lines
18 KiB
TypeScript
Raw Normal View History

/**
* Copyright 2018 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import expect from 'expect';
import {
2020-05-07 10:54:55 +00:00
getTestState,
setupTestBrowserHooks,
setupTestPageAndContextHooks,
} from './mocha-utils.js';
import {attachFrame} from './utils.js';
2020-05-07 10:54:55 +00:00
describe('Evaluation specs', function () {
setupTestBrowserHooks();
setupTestPageAndContextHooks();
2020-05-07 10:54:55 +00:00
describe('Page.evaluate', function () {
it('should work', async () => {
const {page} = getTestState();
const result = await page.evaluate(() => {
return 7 * 3;
});
expect(result).toBe(21);
});
it('should transfer BigInt', async () => {
const {page} = getTestState();
const result = await page.evaluate((a: bigint) => {
return a;
}, BigInt(42));
expect(result).toBe(BigInt(42));
});
it('should transfer NaN', async () => {
const {page} = getTestState();
const result = await page.evaluate(a => {
return a;
}, NaN);
expect(Object.is(result, NaN)).toBe(true);
});
it('should transfer -0', async () => {
const {page} = getTestState();
const result = await page.evaluate(a => {
return a;
}, -0);
expect(Object.is(result, -0)).toBe(true);
});
it('should transfer Infinity', async () => {
const {page} = getTestState();
const result = await page.evaluate(a => {
return a;
}, Infinity);
expect(Object.is(result, Infinity)).toBe(true);
});
it('should transfer -Infinity', async () => {
const {page} = getTestState();
const result = await page.evaluate(a => {
return a;
}, -Infinity);
expect(Object.is(result, -Infinity)).toBe(true);
});
2020-05-07 10:54:55 +00:00
it('should transfer arrays', async () => {
const {page} = getTestState();
const result = await page.evaluate(
a => {
return a;
},
[1, 2, 3]
);
2020-05-07 10:54:55 +00:00
expect(result).toEqual([1, 2, 3]);
});
2020-05-07 10:54:55 +00:00
it('should transfer arrays as arrays, not objects', async () => {
const {page} = getTestState();
const result = await page.evaluate(
a => {
return Array.isArray(a);
},
[1, 2, 3]
);
expect(result).toBe(true);
});
2020-05-07 10:54:55 +00:00
it('should modify global environment', async () => {
const {page} = getTestState();
await page.evaluate(() => {
return ((globalThis as any).globalVar = 123);
});
expect(await page.evaluate('globalVar')).toBe(123);
});
2020-05-07 10:54:55 +00:00
it('should evaluate in the page context', async () => {
const {page, server} = getTestState();
await page.goto(server.PREFIX + '/global-var.html');
expect(await page.evaluate('globalVar')).toBe(123);
});
it('should return undefined for objects with symbols', async () => {
const {page} = getTestState();
expect(
await page.evaluate(() => {
return [Symbol('foo4')];
})
).toBe(undefined);
});
2020-05-07 10:54:55 +00:00
it('should work with function shorthands', async () => {
const {page} = getTestState();
const a = {
sum(a: number, b: number) {
2020-05-07 10:54:55 +00:00
return a + b;
},
async mult(a: number, b: number) {
2020-05-07 10:54:55 +00:00
return a * b;
},
};
expect(await page.evaluate(a.sum, 1, 2)).toBe(3);
expect(await page.evaluate(a.mult, 2, 4)).toBe(8);
});
2020-05-07 10:54:55 +00:00
it('should work with unicode chars', async () => {
const {page} = getTestState();
const result = await page.evaluate(
a => {
return a['中文字符'];
},
{
中文字符: 42,
}
);
expect(result).toBe(42);
});
it('should throw when evaluation triggers reload', async () => {
const {page} = getTestState();
let error!: Error;
2020-05-07 10:54:55 +00:00
await page
.evaluate(() => {
location.reload();
return new Promise(() => {});
})
.catch(error_ => {
return (error = error_);
});
expect(error.message).toContain('Protocol error');
});
2020-05-07 10:54:55 +00:00
it('should await promise', async () => {
const {page} = getTestState();
const result = await page.evaluate(() => {
return Promise.resolve(8 * 7);
});
expect(result).toBe(56);
});
2020-05-07 10:54:55 +00:00
it('should work right after framenavigated', async () => {
const {page, server} = 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} = 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
);
}
);
2020-05-07 10:54:55 +00:00
const result = await page.evaluate(async function () {
return await (globalThis as any).callController(9, 3);
});
expect(result).toBe(27);
});
2020-05-07 10:54:55 +00:00
it('should reject promise with exception', async () => {
const {page} = getTestState();
let error!: Error;
2020-05-07 10:54:55 +00:00
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');
});
2020-05-07 10:54:55 +00:00
it('should support thrown strings as error messages', async () => {
const {page} = getTestState();
let error!: Error;
2020-05-07 10:54:55 +00:00
await page
.evaluate(() => {
throw 'qwerty';
})
.catch(error_ => {
return (error = error_);
});
expect(error).toEqual('qwerty');
});
2020-05-07 10:54:55 +00:00
it('should support thrown numbers as error messages', async () => {
const {page} = getTestState();
let error!: Error;
2020-05-07 10:54:55 +00:00
await page
.evaluate(() => {
throw 100500;
})
.catch(error_ => {
return (error = error_);
});
expect(error).toEqual(100500);
});
2020-05-07 10:54:55 +00:00
it('should return complex objects', async () => {
const {page} = 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} = getTestState();
const result = await page.evaluate(() => {
return BigInt(42);
});
expect(result).toBe(BigInt(42));
});
it('should return NaN', async () => {
const {page} = getTestState();
const result = await page.evaluate(() => {
return NaN;
});
expect(Object.is(result, NaN)).toBe(true);
});
it('should return -0', async () => {
const {page} = getTestState();
const result = await page.evaluate(() => {
return -0;
});
expect(Object.is(result, -0)).toBe(true);
});
it('should return Infinity', async () => {
const {page} = getTestState();
const result = await page.evaluate(() => {
return Infinity;
});
expect(Object.is(result, Infinity)).toBe(true);
});
it('should return -Infinity', async () => {
const {page} = 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} = getTestState();
2020-05-07 10:54:55 +00:00
const result = await page.evaluate(
(a, b) => {
return Object.is(a, null) && Object.is(b, 'foo');
},
null,
2020-05-07 10:54:55 +00:00
'foo'
);
expect(result).toBe(true);
});
2020-05-07 10:54:55 +00:00
it('should properly serialize null fields', async () => {
const {page} = getTestState();
expect(
await page.evaluate(() => {
return {a: undefined};
})
).toEqual({});
});
it('should return undefined for non-serializable objects', async () => {
const {page} = getTestState();
expect(
await page.evaluate(() => {
return window;
})
).toBe(undefined);
});
2023-02-02 14:14:28 +00:00
it('should return promise as empty object', async () => {
const {page} = getTestState();
const result = await page.evaluate(() => {
return {
promise: new Promise(resolve => {
setTimeout(resolve, 1000);
}),
};
});
expect(result).toEqual({
promise: {},
});
});
it('should fail for circular object', async () => {
const {page} = getTestState();
const result = await page.evaluate(() => {
const a: {[x: string]: any} = {};
const b = {a};
a['b'] = b;
return a;
});
expect(result).toBe(undefined);
});
it('should be able to throw a tricky error', async () => {
const {page} = getTestState();
const windowHandle = await page.evaluateHandle(() => {
return window;
});
const errorText = await windowHandle.jsonValue().catch(error_ => {
return error_.message;
});
2020-05-07 10:54:55 +00:00
const error = await page
feat!: type inference for evaluation types (#8547) This PR greatly improves the types within Puppeteer: - **Almost everything** is auto-deduced. - Parameters don't need to be specified in the function. They are deduced from the spread. - Return types don't need to be specified. They are deduced from the function. (More on this below) - Selections based on tag names correctly deduce element type, similar to TypeScript's mechanism for `getElementByTagName`. - [**BREAKING CHANGE**] We've removed the ability to declare return types in type arguments for the following reasons: 1. Setting them will indubitably break auto-deduction. 2. You can just use `as ...` in TypeScript to coerce the correct type (given it makes sense). - [**BREAKING CHANGE**] `waitFor` is officially gone. To migrate to these changes, there are only four things you may need to change: - If you set a return type using the `ReturnType` type parameter, remove it and use `as ...` and `HandleFor` (if necessary). ⛔ `evaluate<ReturnType>(a: number, b: number) => {...}, a, b)` ✅ `(await evaluate(a, b) => {...}, a, b)) as ReturnType` ⛔ `evaluateHandle<ReturnType>(a: number, b: number) => {...}, a, b)` ✅ `(await evaluateHandle(a, b) => {...}, a, b)) as HandleFor<ReturnType>` - If you set any type parameters in the *parameters* of an evaluation function, remove them. ⛔ `evaluate(a: number, b: number) => {...}, a, b)` ✅ `evaluate(a, b) => {...}, a, b)` - If you set any type parameters in the method's declaration, remove them. ⛔ `evaluate<(a: number, b: number) => void>((a, b) => {...}, a, b)` ✅ `evaluate(a, b) => {...}, a, b)`
2022-06-23 09:29:46 +00:00
.evaluate(errorText => {
2020-05-07 10:54:55 +00:00
throw new Error(errorText);
}, errorText)
.catch(error_ => {
return error_;
});
expect(error.message).toContain(errorText);
});
2020-05-07 10:54:55 +00:00
it('should accept a string', async () => {
const {page} = getTestState();
const result = await page.evaluate('1 + 2');
expect(result).toBe(3);
});
2020-05-07 10:54:55 +00:00
it('should accept a string with semi colons', async () => {
const {page} = getTestState();
const result = await page.evaluate('1 + 5;');
expect(result).toBe(6);
});
2020-05-07 10:54:55 +00:00
it('should accept a string with comments', async () => {
const {page} = 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} = getTestState();
await page.setContent('<section>42</section>');
const 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} = getTestState();
2020-05-07 10:54:55 +00:00
await page.setContent('<section>39</section>');
const element = (await page.$('section'))!;
expect(element).toBeTruthy();
await element.dispose();
let error!: Error;
await page
.evaluate((e: HTMLElement) => {
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} = getTestState();
await attachFrame(page, 'frame1', server.EMPTY_PAGE);
const 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).toContain(
'JSHandles can be evaluated only in the context they were created'
);
});
it('should simulate a user gesture', async () => {
const {page} = 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 throw a nice error after a navigation', async () => {
const {page} = getTestState();
const executionContext = await page.mainFrame().executionContext();
await Promise.all([
page.waitForNavigation(),
executionContext.evaluate(() => {
return window.location.reload();
}),
]);
2020-05-07 10:54:55 +00:00
const error = await executionContext
.evaluate(() => {
return null;
})
.catch(error_ => {
return error_;
});
expect((error as Error).message).toContain('navigation');
});
it('should not throw an error when evaluation does a navigation', async () => {
const {page, server} = getTestState();
2020-05-07 10:54:55 +00:00
await page.goto(server.PREFIX + '/one-style.html');
const result = await page.evaluate(() => {
(window as any).location = '/empty.html';
return [42];
});
expect(result).toEqual([42]);
});
it('should transfer 100Mb of data from page to node.js', async function () {
this.timeout(25_000);
const {page} = getTestState();
2020-05-07 10:54:55 +00:00
feat!: type inference for evaluation types (#8547) This PR greatly improves the types within Puppeteer: - **Almost everything** is auto-deduced. - Parameters don't need to be specified in the function. They are deduced from the spread. - Return types don't need to be specified. They are deduced from the function. (More on this below) - Selections based on tag names correctly deduce element type, similar to TypeScript's mechanism for `getElementByTagName`. - [**BREAKING CHANGE**] We've removed the ability to declare return types in type arguments for the following reasons: 1. Setting them will indubitably break auto-deduction. 2. You can just use `as ...` in TypeScript to coerce the correct type (given it makes sense). - [**BREAKING CHANGE**] `waitFor` is officially gone. To migrate to these changes, there are only four things you may need to change: - If you set a return type using the `ReturnType` type parameter, remove it and use `as ...` and `HandleFor` (if necessary). ⛔ `evaluate<ReturnType>(a: number, b: number) => {...}, a, b)` ✅ `(await evaluate(a, b) => {...}, a, b)) as ReturnType` ⛔ `evaluateHandle<ReturnType>(a: number, b: number) => {...}, a, b)` ✅ `(await evaluateHandle(a, b) => {...}, a, b)) as HandleFor<ReturnType>` - If you set any type parameters in the *parameters* of an evaluation function, remove them. ⛔ `evaluate(a: number, b: number) => {...}, a, b)` ✅ `evaluate(a, b) => {...}, a, b)` - If you set any type parameters in the method's declaration, remove them. ⛔ `evaluate<(a: number, b: number) => void>((a, b) => {...}, a, b)` ✅ `evaluate(a, b) => {...}, a, b)`
2022-06-23 09:29:46 +00:00
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} = getTestState();
let error!: Error;
2020-05-07 10:54:55 +00:00
await page
.evaluate(() => {
return new Promise(() => {
throw new Error('Error in promise');
});
})
.catch(error_ => {
return (error = error_);
});
expect(error.message).toContain('Error in promise');
});
});
describe('Page.evaluateOnNewDocument', function () {
2020-05-07 10:54:55 +00:00
it('should evaluate before anything else on the page', async () => {
const {page, server} = getTestState();
2020-05-07 10:54:55 +00:00
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);
});
2020-05-07 10:54:55 +00:00
it('should work with CSP', async () => {
const {page, server} = getTestState();
server.setCSP('/empty.html', 'script-src ' + server.PREFIX);
2020-05-07 10:54:55 +00:00
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} = 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);
});
});
2020-05-07 10:54:55 +00:00
describe('Frame.evaluate', function () {
it('should have different execution contexts', async () => {
const {page, server} = 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} = getTestState();
await page.goto(server.PREFIX + '/frames/one-frame.html');
expect(page.frames()).toHaveLength(2);
2020-05-07 10:54:55 +00:00
expect(
await page.frames()[0]!.evaluate(() => {
return document.body.textContent!.trim();
})
2020-05-07 10:54:55 +00:00
).toBe('');
expect(
await page.frames()[1]!.evaluate(() => {
return document.body.textContent!.trim();
})
2020-05-07 10:54:55 +00:00
).toBe(`Hi, I'm frame`);
});
2020-05-07 10:54:55 +00:00
it('should execute after cross-site navigation', async () => {
const {page, server} = 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');
});
});
});