ea2b0d1f62
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.
222 lines
7.3 KiB
TypeScript
222 lines
7.3 KiB
TypeScript
import { spawnSync } from 'child_process';
|
|
import { version } from '../package.json';
|
|
import path from 'path';
|
|
import fs from 'fs';
|
|
const PROJECT_FOLDERS_ROOT = 'test-ts-types';
|
|
const EXPECTED_ERRORS = new Map<string, string[]>([
|
|
[
|
|
'ts-esm-import-esm-output',
|
|
[
|
|
"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(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'.",
|
|
],
|
|
],
|
|
[
|
|
'ts-esm-import-cjs-output',
|
|
[
|
|
"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(12,39): error TS2554: Expected 0 arguments, but got 1.',
|
|
],
|
|
],
|
|
[
|
|
'ts-cjs-import-cjs-output',
|
|
[
|
|
"bad.ts(5,35): error TS2551: Property 'launh' does not exist on type",
|
|
"bad.ts(7,29): error TS2551: Property 'devics' does not exist on type",
|
|
'bad.ts(11,39): error TS2554: Expected 0 arguments, but got 1.',
|
|
],
|
|
],
|
|
[
|
|
'js-esm-import-cjs-output',
|
|
[
|
|
"bad.js(5,35): error TS2551: Property 'launh' 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(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'.",
|
|
],
|
|
],
|
|
[
|
|
'js-cjs-import-esm-output',
|
|
[
|
|
"bad.js(5,35): error TS2551: Property 'launh' 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(15,9): error TS2322: Type 'ElementHandle<HTMLElement> | null' is not assignable to type 'ElementHandle<HTMLElement>'",
|
|
],
|
|
],
|
|
[
|
|
'js-esm-import-esm-output',
|
|
[
|
|
"bad.js(5,35): error TS2551: Property 'launh' 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(15,9): error TS2322: Type 'ElementHandle<HTMLElement> | null' is not assignable to type 'ElementHandle<HTMLElement>'",
|
|
],
|
|
],
|
|
[
|
|
'js-cjs-import-cjs-output',
|
|
[
|
|
"bad.js(5,35): error TS2551: Property 'launh' 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(15,9): error TS2322: Type 'ElementHandle<HTMLElement> | null' is not assignable to type 'ElementHandle<HTMLElement>'",
|
|
],
|
|
],
|
|
]);
|
|
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() {
|
|
console.log('Packing Puppeteer');
|
|
const result = spawnSync('npm', ['pack'], {
|
|
encoding: 'utf-8',
|
|
});
|
|
if (result.status !== 0) {
|
|
console.log('Failed to pack Puppeteer', result.stderr);
|
|
process.exit(1);
|
|
}
|
|
|
|
// Move from puppeteer-X.Y.Z.tgz to puppeteer.tgz so we don't have to update
|
|
// it when versions change.
|
|
const moveResult = spawnSync('mv', [
|
|
`puppeteer-${version}.tgz`,
|
|
'puppeteer.tgz',
|
|
]);
|
|
if (moveResult.status !== 0) {
|
|
console.log('Failed to rename Puppeteer tar', moveResult.stderr);
|
|
process.exit(1);
|
|
}
|
|
|
|
return `puppeteer.tgz`;
|
|
}
|
|
|
|
const tar = packPuppeteer();
|
|
const tarPath = path.join(process.cwd(), tar);
|
|
|
|
function compileAndCatchErrors(projectLocation) {
|
|
const { status, stdout, stderr } = spawnSync('npm', ['run', 'compile'], {
|
|
cwd: projectLocation,
|
|
encoding: 'utf-8',
|
|
});
|
|
const tsErrorMesssage = stdout.split('\n');
|
|
|
|
if (status === 0) {
|
|
console.error(
|
|
`Running tsc on ${projectLocation} succeeded without triggering the expected errors.`
|
|
);
|
|
console.log(stdout, stderr);
|
|
process.exit(1);
|
|
}
|
|
|
|
return {
|
|
tsErrorMesssage,
|
|
};
|
|
}
|
|
|
|
function testProject(folder: string) {
|
|
console.log('\nTesting:', folder);
|
|
const projectLocation = path.join(
|
|
process.cwd(),
|
|
PROJECT_FOLDERS_ROOT,
|
|
folder
|
|
);
|
|
|
|
const tarLocation = path.relative(projectLocation, tarPath);
|
|
console.log('===> Clearing left over node_modules to ensure clean slate');
|
|
try {
|
|
fs.rmdirSync(path.join(projectLocation, 'node_modules'), {
|
|
recursive: true,
|
|
});
|
|
} catch (_error) {
|
|
// We don't care if this errors because if it did it's most likely because
|
|
// there was no node_modules folder, which is fine.
|
|
}
|
|
console.log('===> Installing Puppeteer from tar file', tarLocation);
|
|
const { status, stderr, stdout } = spawnSync(
|
|
'npm',
|
|
['install', tarLocation],
|
|
{
|
|
env: {
|
|
...process.env,
|
|
PUPPETEER_SKIP_DOWNLOAD: '1',
|
|
},
|
|
cwd: projectLocation,
|
|
encoding: 'utf-8',
|
|
}
|
|
);
|
|
|
|
if (status > 0) {
|
|
console.error(
|
|
'Installing the tar file unexpectedly failed',
|
|
stdout,
|
|
stderr
|
|
);
|
|
process.exit(status);
|
|
}
|
|
console.log('===> Running compile to ensure expected errors only.');
|
|
const result = compileAndCatchErrors(projectLocation);
|
|
const expectedErrors = EXPECTED_ERRORS.get(folder) || [];
|
|
if (
|
|
result.tsErrorMesssage.find(
|
|
(line) => line.includes('good.ts') || line.includes('good.js')
|
|
)
|
|
) {
|
|
console.error(
|
|
`Error for ${projectLocation} contained unexpected failures in good.ts/good.js:\n${result.tsErrorMesssage.join(
|
|
'\n'
|
|
)}`
|
|
);
|
|
process.exit(1);
|
|
}
|
|
const errorsInTsMessage = result.tsErrorMesssage.filter(
|
|
(line) => line.includes('bad.ts') || line.includes('bad.js')
|
|
);
|
|
const expectedErrorsThatHaveOccurred = new Set<string>();
|
|
const unexpectedErrors = errorsInTsMessage.filter((message) => {
|
|
const isExpected = expectedErrors.some((expectedError) => {
|
|
const isExpected = message.startsWith(expectedError);
|
|
if (isExpected) {
|
|
expectedErrorsThatHaveOccurred.add(expectedError);
|
|
}
|
|
return isExpected;
|
|
});
|
|
return !isExpected;
|
|
});
|
|
|
|
if (unexpectedErrors.length) {
|
|
console.error(
|
|
`${projectLocation} had unexpected TS errors: ${unexpectedErrors.join(
|
|
'\n'
|
|
)}`
|
|
);
|
|
process.exit(1);
|
|
}
|
|
expectedErrors.forEach((expected) => {
|
|
if (!expectedErrorsThatHaveOccurred.has(expected)) {
|
|
console.error(
|
|
`${projectLocation} expected error that was not thrown: ${expected}`
|
|
);
|
|
process.exit(1);
|
|
}
|
|
});
|
|
console.log('===> ✅ Type-checked correctly.');
|
|
}
|
|
|
|
PROJECT_FOLDERS.forEach((folder) => {
|
|
testProject(folder);
|
|
});
|