mirror of
https://github.com/puppeteer/puppeteer
synced 2024-06-14 14:02:48 +00:00
chore: improve type inference of evaluate (#7267)
This commit updates the JSHandle class to take a generic representing the underlying object that it's wrapping. We can then define `ElementHandle` as a class that extends `JSHandle<Element>` and begin to get better type inference. Prior to this commit the following code would have `d` set to `any`: ``` const div: page.$<HTMLDivElement>('div') const text = await div.evaluate(d => d.innerText) ``` You could work around this in two ways: ``` const text = await div.evaluate<(d: HTMLDivElement) => string>(d => d.innerText) const text = await div.evaluate((d: HTMLDivElement) => d.innerText) ``` But both of these have two issues: 1. Requires the user to type extra information. 2. There's no type checking: in the code above I could type `d` as `number` and TS would be happy. With the change here to `evaluate` the user can now type the original code: ``` const div: page.$<HTMLDivElement>('div') const text = await div.evaluate(d => d.innerText) ``` And TypeScript will know that `d` is an `HTMLDivElement`. This change brings us inline with the approach that @types/puppeteer takes. If we land this and it works, we can do the same with `evaluateHandle` to hopefully make a similar improvement there. BREAKING: because this changes the types, which were previously `any`, this is technically a breaking change as users using TS could start getting errors after this change is released.
This commit is contained in:
parent
457651d0a3
commit
ea2b0d1f62
@ -10,6 +10,10 @@ const EXPECTED_ERRORS = new Map<string, string[]>([
|
|||||||
"bad.ts(6,35): error TS2551: Property 'launh' does not exist on type",
|
"bad.ts(6,35): error TS2551: Property 'launh' does not exist on type",
|
||||||
"bad.ts(8,29): error TS2551: Property 'devics' does not exist on type",
|
"bad.ts(8,29): error TS2551: Property 'devics' does not exist on type",
|
||||||
'bad.ts(12,39): error TS2554: Expected 0 arguments, but got 1.',
|
'bad.ts(12,39): error TS2554: Expected 0 arguments, but got 1.',
|
||||||
|
"bad.ts(20,5): error TS2345: Argument of type '(divElem: number) => any' is not assignable to parameter of type 'EvaluateFn<HTMLAnchorElement>",
|
||||||
|
"bad.ts(20,34): error TS2339: Property 'innerText' does not exist on type 'number'.",
|
||||||
|
"bad.ts(24,45): error TS2344: Type '(x: number) => string' does not satisfy the constraint 'EvaluateFn<HTMLAnchorElement>'.",
|
||||||
|
"bad.ts(27,34): error TS2339: Property 'innerText' does not exist on type 'number'.",
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
@ -35,6 +39,8 @@ const EXPECTED_ERRORS = new Map<string, string[]>([
|
|||||||
"bad.js(7,29): error TS2551: Property 'devics' does not exist on type",
|
"bad.js(7,29): error TS2551: Property 'devics' does not exist on type",
|
||||||
'bad.js(11,39): error TS2554: Expected 0 arguments, but got 1.',
|
'bad.js(11,39): error TS2554: Expected 0 arguments, but got 1.',
|
||||||
"bad.js(15,9): error TS2322: Type 'ElementHandle<HTMLElement> | null' is not assignable to type 'ElementHandle<HTMLElement>'",
|
"bad.js(15,9): error TS2322: Type 'ElementHandle<HTMLElement> | null' is not assignable to type 'ElementHandle<HTMLElement>'",
|
||||||
|
"bad.js(22,5): error TS2345: Argument of type '(divElem: number) => any' is not assignable to parameter of type 'EvaluateFn<HTMLElement>'.",
|
||||||
|
"bad.js(22,26): error TS2339: Property 'innerText' does not exist on type 'number'.",
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
@ -67,6 +73,13 @@ const EXPECTED_ERRORS = new Map<string, string[]>([
|
|||||||
]);
|
]);
|
||||||
const PROJECT_FOLDERS = [...EXPECTED_ERRORS.keys()];
|
const PROJECT_FOLDERS = [...EXPECTED_ERRORS.keys()];
|
||||||
|
|
||||||
|
if (!process.env.CI) {
|
||||||
|
console.log(`IMPORTANT: this script assumes you have compiled Puppeteer
|
||||||
|
and its types file before running. Make sure you have run:
|
||||||
|
=> npm run tsc && npm run generate-d-ts
|
||||||
|
before executing this script locally.`);
|
||||||
|
}
|
||||||
|
|
||||||
function packPuppeteer() {
|
function packPuppeteer() {
|
||||||
console.log('Packing Puppeteer');
|
console.log('Packing Puppeteer');
|
||||||
const result = spawnSync('npm', ['pack'], {
|
const result = spawnSync('npm', ['pack'], {
|
||||||
|
@ -105,7 +105,7 @@ export function createJSHandle(
|
|||||||
*
|
*
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
export class JSHandle {
|
export class JSHandle<HandleObjectType = unknown> {
|
||||||
/**
|
/**
|
||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
@ -154,7 +154,7 @@ export class JSHandle {
|
|||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
|
|
||||||
async evaluate<T extends EvaluateFn>(
|
async evaluate<T extends EvaluateFn<HandleObjectType>>(
|
||||||
pageFunction: T | string,
|
pageFunction: T | string,
|
||||||
...args: SerializableOrJSHandle[]
|
...args: SerializableOrJSHandle[]
|
||||||
): Promise<UnwrapPromiseLike<EvaluateFnReturnType<T>>> {
|
): Promise<UnwrapPromiseLike<EvaluateFnReturnType<T>>> {
|
||||||
@ -193,7 +193,7 @@ export class JSHandle {
|
|||||||
*/
|
*/
|
||||||
async getProperty(propertyName: string): Promise<JSHandle | undefined> {
|
async getProperty(propertyName: string): Promise<JSHandle | undefined> {
|
||||||
const objectHandle = await this.evaluateHandle(
|
const objectHandle = await this.evaluateHandle(
|
||||||
(object: HTMLElement, propertyName: string) => {
|
(object: Element, propertyName: string) => {
|
||||||
const result = { __proto__: null };
|
const result = { __proto__: null };
|
||||||
result[propertyName] = object[propertyName];
|
result[propertyName] = object[propertyName];
|
||||||
return result;
|
return result;
|
||||||
@ -328,7 +328,7 @@ export class JSHandle {
|
|||||||
*/
|
*/
|
||||||
export class ElementHandle<
|
export class ElementHandle<
|
||||||
ElementType extends Element = Element
|
ElementType extends Element = Element
|
||||||
> extends JSHandle {
|
> extends JSHandle<ElementType> {
|
||||||
private _page: Page;
|
private _page: Page;
|
||||||
private _frameManager: FrameManager;
|
private _frameManager: FrameManager;
|
||||||
|
|
||||||
@ -521,24 +521,25 @@ export class ElementHandle<
|
|||||||
'"'
|
'"'
|
||||||
);
|
);
|
||||||
|
|
||||||
return this.evaluate<
|
return this.evaluate<(element: Element, values: string[]) => string[]>(
|
||||||
(element: HTMLSelectElement, values: string[]) => string[]
|
(element, values) => {
|
||||||
>((element, values) => {
|
if (!(element instanceof HTMLSelectElement))
|
||||||
if (element.nodeName.toLowerCase() !== 'select')
|
throw new Error('Element is not a <select> element.');
|
||||||
throw new Error('Element is not a <select> element.');
|
|
||||||
|
|
||||||
const options = Array.from(element.options);
|
const options = Array.from(element.options);
|
||||||
element.value = undefined;
|
element.value = undefined;
|
||||||
for (const option of options) {
|
for (const option of options) {
|
||||||
option.selected = values.includes(option.value);
|
option.selected = values.includes(option.value);
|
||||||
if (option.selected && !element.multiple) break;
|
if (option.selected && !element.multiple) break;
|
||||||
}
|
}
|
||||||
element.dispatchEvent(new Event('input', { bubbles: true }));
|
element.dispatchEvent(new Event('input', { bubbles: true }));
|
||||||
element.dispatchEvent(new Event('change', { bubbles: true }));
|
element.dispatchEvent(new Event('change', { bubbles: true }));
|
||||||
return options
|
return options
|
||||||
.filter((option) => option.selected)
|
.filter((option) => option.selected)
|
||||||
.map((option) => option.value);
|
.map((option) => option.value);
|
||||||
}, values);
|
},
|
||||||
|
values
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -549,9 +550,14 @@ export class ElementHandle<
|
|||||||
* relative to the {@link https://nodejs.org/api/process.html#process_process_cwd | current working directory}
|
* relative to the {@link https://nodejs.org/api/process.html#process_process_cwd | current working directory}
|
||||||
*/
|
*/
|
||||||
async uploadFile(...filePaths: string[]): Promise<void> {
|
async uploadFile(...filePaths: string[]): Promise<void> {
|
||||||
const isMultiple = await this.evaluate<
|
const isMultiple = await this.evaluate<(element: Element) => boolean>(
|
||||||
(element: HTMLInputElement) => boolean
|
(element) => {
|
||||||
>((element) => element.multiple);
|
if (!(element instanceof HTMLInputElement)) {
|
||||||
|
throw new Error('uploadFile can only be called on an input element.');
|
||||||
|
}
|
||||||
|
return element.multiple;
|
||||||
|
}
|
||||||
|
);
|
||||||
assert(
|
assert(
|
||||||
filePaths.length <= 1 || isMultiple,
|
filePaths.length <= 1 || isMultiple,
|
||||||
'Multiple file uploads only work with <input type=file multiple>'
|
'Multiple file uploads only work with <input type=file multiple>'
|
||||||
@ -588,7 +594,7 @@ export class ElementHandle<
|
|||||||
// not actually update the files in that case, so the solution is to eval the element
|
// not actually update the files in that case, so the solution is to eval the element
|
||||||
// value to a new FileList directly.
|
// value to a new FileList directly.
|
||||||
if (files.length === 0) {
|
if (files.length === 0) {
|
||||||
await this.evaluate<(element: HTMLInputElement) => void>((element) => {
|
await (this as ElementHandle<HTMLInputElement>).evaluate((element) => {
|
||||||
element.files = new DataTransfer().files;
|
element.files = new DataTransfer().files;
|
||||||
|
|
||||||
// Dispatch events for this case because it should behave akin to a user action.
|
// Dispatch events for this case because it should behave akin to a user action.
|
||||||
@ -619,7 +625,7 @@ export class ElementHandle<
|
|||||||
* Calls {@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus | focus} on the element.
|
* Calls {@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus | focus} on the element.
|
||||||
*/
|
*/
|
||||||
async focus(): Promise<void> {
|
async focus(): Promise<void> {
|
||||||
await this.evaluate<(element: HTMLElement) => void>((element) =>
|
await (this as ElementHandle<HTMLElement>).evaluate((element) =>
|
||||||
element.focus()
|
element.focus()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
"compile": "../../node_modules/.bin/tsc"
|
"compile": "../../node_modules/.bin/tsc"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"typescript": "^4.1.3"
|
"typescript": "4.2.4"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"puppeteer": "file:../../puppeteer.tgz"
|
"puppeteer": "file:../../puppeteer.tgz"
|
||||||
|
@ -14,5 +14,12 @@ async function run() {
|
|||||||
*/
|
*/
|
||||||
const div = await page.$('div');
|
const div = await page.$('div');
|
||||||
console.log('got a div!', div);
|
console.log('got a div!', div);
|
||||||
|
const contentsOfDiv = await div.evaluate(
|
||||||
|
/**
|
||||||
|
* @param {number} divElem
|
||||||
|
* @returns number
|
||||||
|
*/
|
||||||
|
(divElem) => divElem.innerText
|
||||||
|
);
|
||||||
}
|
}
|
||||||
run();
|
run();
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
"compile": "../../node_modules/.bin/tsc"
|
"compile": "../../node_modules/.bin/tsc"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"typescript": "^4.1.3"
|
"typescript": "4.2.4"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"puppeteer": "file:../../puppeteer.tgz"
|
"puppeteer": "file:../../puppeteer.tgz"
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
"compile": "../../node_modules/.bin/tsc"
|
"compile": "../../node_modules/.bin/tsc"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"typescript": "^4.1.3"
|
"typescript": "4.2.4"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"puppeteer": "file:../../puppeteer.tgz"
|
"puppeteer": "file:../../puppeteer.tgz"
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
"compile": "../../node_modules/.bin/tsc"
|
"compile": "../../node_modules/.bin/tsc"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"typescript": "^4.1.3"
|
"typescript": "4.2.4"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"puppeteer": "file:../../puppeteer.tgz"
|
"puppeteer": "file:../../puppeteer.tgz"
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
"compile": "../../node_modules/.bin/tsc"
|
"compile": "../../node_modules/.bin/tsc"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"typescript": "^4.1.3"
|
"typescript": "4.2.4"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"puppeteer": "file:../../puppeteer.tgz"
|
"puppeteer": "file:../../puppeteer.tgz"
|
||||||
|
@ -10,9 +10,21 @@ async function run() {
|
|||||||
const browser2 = await puppeteer.launch();
|
const browser2 = await puppeteer.launch();
|
||||||
// 'foo' is invalid argument
|
// 'foo' is invalid argument
|
||||||
const page = await browser2.newPage('foo');
|
const page = await browser2.newPage('foo');
|
||||||
const div = (await page.$('div')) as puppeteer.ElementHandle<
|
const div = (await page.$(
|
||||||
HTMLAnchorElement
|
'div'
|
||||||
>;
|
)) as puppeteer.ElementHandle<HTMLAnchorElement>;
|
||||||
console.log('got a div!', div);
|
console.log('got a div!', div);
|
||||||
|
const contentsOfDiv = await div.evaluate(
|
||||||
|
// Bad: the type system will know here that divElem is an HTMLAnchorElement
|
||||||
|
// and won't let me tell it it's a number
|
||||||
|
(divElem: number) => divElem.innerText
|
||||||
|
);
|
||||||
|
// Bad: the type system will know here that divElem is an HTMLAnchorElement
|
||||||
|
// and won't let me tell it it's a number via the generic
|
||||||
|
const contentsOfDiv2 = await div.evaluate<(x: number) => string>(
|
||||||
|
// Bad: now I've forced it to be a number (which is an error also)
|
||||||
|
// I can't call `innerText` on it.
|
||||||
|
(divElem: number) => divElem.innerText
|
||||||
|
);
|
||||||
}
|
}
|
||||||
run();
|
run();
|
||||||
|
@ -9,5 +9,7 @@ async function run() {
|
|||||||
const page = await browser.newPage();
|
const page = await browser.newPage();
|
||||||
const div = (await page.$('div')) as ElementHandle<HTMLAnchorElement>;
|
const div = (await page.$('div')) as ElementHandle<HTMLAnchorElement>;
|
||||||
console.log('got a div!', div);
|
console.log('got a div!', div);
|
||||||
|
|
||||||
|
const contentsOfDiv = await div.evaluate((divElem) => divElem.innerText);
|
||||||
}
|
}
|
||||||
run();
|
run();
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
"compile": "../../node_modules/.bin/tsc"
|
"compile": "../../node_modules/.bin/tsc"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"typescript": "^4.1.3"
|
"typescript": "4.2.4"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"puppeteer": "file:../../puppeteer.tgz"
|
"puppeteer": "file:../../puppeteer.tgz"
|
||||||
|
@ -28,10 +28,12 @@ const EXCLUDE_PROPERTIES = new Set([
|
|||||||
'Page.create',
|
'Page.create',
|
||||||
'JSHandle.toString',
|
'JSHandle.toString',
|
||||||
'TimeoutError.name',
|
'TimeoutError.name',
|
||||||
/* This isn't an actual property, but a TypeScript generic.
|
/* These are not actual properties, but a TypeScript generic.
|
||||||
* DocLint incorrectly parses it as a property.
|
* DocLint incorrectly parses it as a property.
|
||||||
*/
|
*/
|
||||||
'ElementHandle.ElementType',
|
'ElementHandle.ElementType',
|
||||||
|
'ElementHandle.HandleObjectType',
|
||||||
|
'JSHandle.HandleObjectType',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -885,6 +887,20 @@ function compareDocumentations(actual, expected) {
|
|||||||
expectedName: 'unknown',
|
expectedName: 'unknown',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
[
|
||||||
|
'Method Page.queryObjects() prototypeHandle',
|
||||||
|
{
|
||||||
|
actualName: 'JSHandle',
|
||||||
|
expectedName: 'JSHandle<unknown>',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'Method ExecutionContext.queryObjects() prototypeHandle',
|
||||||
|
{
|
||||||
|
actualName: 'JSHandle',
|
||||||
|
expectedName: 'JSHandle<unknown>',
|
||||||
|
},
|
||||||
|
],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const expectedForSource = expectedNamingMismatches.get(source);
|
const expectedForSource = expectedNamingMismatches.get(source);
|
||||||
|
Loading…
Reference in New Issue
Block a user