From 57313e3f73531abb51b380c1a5a9587592685dfc Mon Sep 17 00:00:00 2001 From: Joel Einbinder Date: Fri, 6 Dec 2019 11:28:23 -0800 Subject: [PATCH] feat: basic d.ts file (#161) `protocol.d.ts` had to move to `protocol.ts` otherwise typescript refuses to include it in the out directory. Removed the old d.ts generator. It will need to be rewritten anyway. These new types include private stuff that they probably shouldn't, and are missing documentation. I'll follow up with a better d.ts generator later. #6 --- .eslintignore | 6 +- .gitignore | 7 +- .npmignore | 3 + index.d.ts | 9 + package.json | 3 +- tsconfig.json | 5 +- utils/doclint/generate_types/index.js | 221 -------------- utils/doclint/generate_types/test/test.ts | 283 ------------------ .../doclint/generate_types/test/tsconfig.json | 12 - utils/protocol-types-generator/index.js | 12 +- 10 files changed, 27 insertions(+), 534 deletions(-) create mode 100644 index.d.ts delete mode 100644 utils/doclint/generate_types/index.js delete mode 100644 utils/doclint/generate_types/test/test.ts delete mode 100644 utils/doclint/generate_types/test/tsconfig.json diff --git a/.eslintignore b/.eslintignore index 687648ad61102..11cbd52c0c9b1 100644 --- a/.eslintignore +++ b/.eslintignore @@ -9,6 +9,6 @@ node6-testrunner/* lib/ *.js src/generated/* -src/chromium/protocol.d.ts -src/firefox/protocol.d.ts -src/webkit/protocol.d.ts +src/chromium/protocol.ts +src/firefox/protocol.ts +src/webkit/protocol.ts diff --git a/.gitignore b/.gitignore index 61a0cef2d329c..8150e8620bc20 100644 --- a/.gitignore +++ b/.gitignore @@ -15,10 +15,9 @@ package-lock.json yarn.lock /node6 /src/generated/* -/src/chromium/protocol.d.ts -/src/firefox/protocol.d.ts -/src/webkit/protocol.d.ts +/src/chromium/protocol.ts +/src/firefox/protocol.ts +/src/webkit/protocol.ts /utils/browser/playwright-web.js -/index.d.ts lib/ playwright-*.tgz diff --git a/.npmignore b/.npmignore index ae04ec74724a8..d800013bafde3 100644 --- a/.npmignore +++ b/.npmignore @@ -6,6 +6,9 @@ !lib/**/*.js # Injected files are included via lib/generated, see src/injected/README.md lib/injected/ +#types +!lib/**/*.d.ts +!index.d.ts # root for "playwright" package !index.js diff --git a/index.d.ts b/index.d.ts new file mode 100644 index 0000000000000..8b3a05e3940f8 --- /dev/null +++ b/index.d.ts @@ -0,0 +1,9 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +import * as chromium from './chromium'; +import * as firefox from './firefox'; +import * as webkit from './webkit'; +declare function pickBrowser(browser: 'chromium'): typeof chromium; +declare function pickBrowser(browser: 'firefox'): typeof firefox; +declare function pickBrowser(browser: 'webkit'): typeof webkit; +export = pickBrowser; \ No newline at end of file diff --git a/package.json b/package.json index 375a96b1f0b41..4828c8d15975d 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "wunit": "cross-env BROWSER=webkit node test/test.js", "debug-unit": "node --inspect-brk test/test.js", "test-doclint": "node utils/doclint/check_public_api/test/test.js && node utils/doclint/preprocessor/test.js", - "test": "npm run lint --silent && npm run coverage && npm run test-doclint && npm run test-types && node utils/testrunner/test/test.js", + "test": "npm run lint --silent && npm run coverage && npm run test-doclint && node utils/testrunner/test/test.js", "prepare": "node install.js", "lint": "([ \"$CI\" = true ] && eslint --quiet -f codeframe --ext js,ts ./src || eslint --ext js,ts ./src) && npm run tsc && npm run doc", "doc": "node utils/doclint/cli.js", @@ -28,7 +28,6 @@ "watch": "node utils/runWebpack.js --mode='development' --watch --silent | tsc -w -p .", "apply-next-version": "node utils/apply_next_version.js", "bundle": "npx browserify -r ./index.js:playwright -o utils/browser/playwright-web.js", - "test-types": "node utils/doclint/generate_types && npx -p typescript@2.1 tsc -p utils/doclint/generate_types/test/", "unit-bundle": "node utils/browser/test.js" }, "author": { diff --git a/tsconfig.json b/tsconfig.json index 37ac591efbe33..6bf59db7ef034 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,14 +1,13 @@ { "compilerOptions": { - "allowJs": true, - "checkJs": true, "target": "ESNext", "module": "commonjs", "lib": ["esnext", "dom"], "sourceMap": true, "rootDir": "./src", "outDir": "./lib", - "strictBindCallApply": true + "strictBindCallApply": true, + "declaration": true }, "compileOnSave": true, "include": ["src/**/*.ts"], diff --git a/utils/doclint/generate_types/index.js b/utils/doclint/generate_types/index.js deleted file mode 100644 index 30bc59d876e14..0000000000000 --- a/utils/doclint/generate_types/index.js +++ /dev/null @@ -1,221 +0,0 @@ -const path = require('path'); -const Source = require('../Source'); -const playwright = require('../../..'); -const PROJECT_DIR = path.join(__dirname, '..', '..', '..'); -const fs = require('fs'); -const objectDefinitions = []; -(async function() { - const browser = await playwright.launch(); - const page = (await browser.pages())[0]; - const api = await Source.readFile(path.join(PROJECT_DIR, 'docs', 'api.md')); - const {documentation} = await require('../check_public_api/MDBuilder')(page, [api]); - await browser.close(); - const classes = documentation.classesArray.slice(1); - const root = documentation.classesArray[0]; - const output = `// This file is generated by ${__filename.substring(path.join(__dirname, '..', '..').length)} -import { ChildProcess } from 'child_process'; -import { EventEmitter } from 'events'; -/** - * Can be converted to JSON - */ -interface Serializable {} -interface ConnectionTransport {} - -${root.methodsArray.map(method => ` -${memberJSDOC(method, '')}export function ${method.name}${argsFromMember(method)} : ${typeToString(method.type, method.name)}; -`).join('')} -${root.propertiesArray.map(property => ` -${memberJSDOC(property, '')}export const ${property.name}${argsFromMember(property)} : ${typeToString(property.type, property.name)}; -`).join('')} -${classes.map(classDesc => classToString(classDesc)).join('\n')} -${objectDefinitionsToString()} -`; - fs.writeFileSync(path.join(PROJECT_DIR, 'index.d.ts'), output, 'utf8'); -})(); - -function objectDefinitionsToString() { - let definition; - const parts = []; - while ((definition = objectDefinitions.pop())) { - const {name, properties} = definition; - parts.push(`interface ${name} {`); - parts.push(properties.map(member => ` ${memberJSDOC(member, ' ')}${nameForProperty(member)}${argsFromMember(member, name)}: ${typeToString(member.type, name, member.name)};`).join('\n\n')); - parts.push('}\n'); - } - return parts.join('\n'); -} - -function nameForProperty(member) { - return (member.required || member.name.startsWith('...')) ? member.name : member.name + '?'; -} - -/** - * @param {import('./check_public_api/Documentation').Class} classDesc - */ -function classToString(classDesc) { - const parts = []; - if (classDesc.comment) { - parts.push(`/** - * ${classDesc.comment.split('\n').join('\n * ')} - */`); - } - parts.push(`export interface ${classDesc.name} ${classDesc.extends ? `extends ${classDesc.extends} ` : ''}{`); - for (const method of ['on', 'once', 'addListener']) { - for (const [eventName, value] of classDesc.events) { - if (value.comment) { - parts.push(' /**'); - parts.push(...value.comment.split('\n').map(line => ' * ' + line)); - parts.push(' */'); - } - parts.push(` ${method}(event: '${eventName}', listener: (arg0 : ${typeToString(value && value.type, classDesc.name, eventName, 'payload')}) => void): this;\n`); - } - } - const members = classDesc.membersArray.filter(member => member.kind !== 'event'); - parts.push(members.map(member => ` ${memberJSDOC(member, ' ')}${member.name}${argsFromMember(member, classDesc.name)}: ${typeToString(member.type, classDesc.name, member.name)};`).join('\n\n')); - parts.push('}\n'); - return parts.join('\n'); -} - -/** - * @param {import('./check_public_api/Documentation').Type} type - */ -function typeToString(type, ...namespace) { - if (!type) - return 'void'; - let typeString = stringifyType(parseType(type.name)); - for (let i = 0; i < type.properties.length; i++) - typeString = typeString.replace('arg' + i, type.properties[i].name); - if (type.properties.length && typeString.indexOf('Object') !== -1) { - const name = namespace.map(n => n[0].toUpperCase() + n.substring(1)).join(''); - typeString = typeString.replace('Object', name); - objectDefinitions.push({name, properties: type.properties}); - } - return typeString; -} - -/** - * @param {string} type - */ -function parseType(type) { - type = type.trim(); - if (type.startsWith('?')) { - const parsed = parseType(type.substring(1)); - parsed.nullable = true; - return parsed; - } - if (type.startsWith('...')) - return parseType('Array<' + type.substring(3) + '>'); - let name = type; - let next = null; - let template = null; - let args = null; - let retType = null; - let firstTypeLength = type.length; - for (let i = 0; i < type.length; i++) { - if (type[i] === '<') { - name = type.substring(0, i); - const matching = matchingBracket(type.substring(i), '<', '>'); - template = parseType(type.substring(i + 1, i + matching - 1)); - firstTypeLength = i + matching; - break; - } - if (type[i] === '(') { - name = type.substring(0, i); - const matching = matchingBracket(type.substring(i), '(', ')'); - args = parseType(type.substring(i + 1, i + matching - 1)); - i = i + matching; - if (type[i] === ':') { - retType = parseType(type.substring(i + 1)); - next = retType.next; - retType.next = null; - break; - } - } - if (type[i] === '|' || type[i] === ',') { - name = type.substring(0, i); - firstTypeLength = i; - break; - } - } - let pipe = null; - if (type[firstTypeLength] === '|') - pipe = parseType(type.substring(firstTypeLength + 1)); - else if (type[firstTypeLength] === ',') - next = parseType(type.substring(firstTypeLength + 1)); - if (name === 'Promise' && !template) - template = parseType('void'); - return { - name, - args, - retType, - template, - pipe, - next - }; -} - -function stringifyType(parsedType) { - if (!parsedType) - return 'void'; - let out = parsedType.name; - if (parsedType.args) { - let args = parsedType.args; - const stringArgs = []; - while (args) { - const arg = args; - args = args.next; - arg.next = null; - stringArgs.push(stringifyType(arg)); - } - out = `(${stringArgs.map((type, index) => `arg${index} : ${type}`).join(', ')}, ...args: any[]) => ${stringifyType(parsedType.retType)}`; - } else if (parsedType.name === 'function') { - out = 'Function'; - } - if (parsedType.nullable) - out = 'null|' + out; - if (parsedType.template) - out += '<' + stringifyType(parsedType.template) + '>'; - if (parsedType.pipe) - out += '|' + stringifyType(parsedType.pipe); - if (parsedType.next) - out += ', ' + stringifyType(parsedType.next); - return out.trim(); -} - -function matchingBracket(str, open, close) { - let count = 1; - let i = 1; - for (; i < str.length && count; i++) { - if (str[i] === open) - count++; - else if (str[i] === close) - count--; - } - return i; -} - -/** - * @param {import('./check_public_api/Documentation').Member} member - */ -function argsFromMember(member, ...namespace) { - if (member.kind === 'property') - return ''; - return '(' + member.argsArray.map(arg => `${nameForProperty(arg)}: ${typeToString(arg.type, ...namespace, member.name, 'options')}`).join(', ') + ')'; -} -/** - * @param {import('./check_public_api/Documentation').Member} member - */ -function memberJSDOC(member, indent) { - const lines = []; - if (member.comment) - lines.push(...member.comment.split('\n')); - lines.push(...member.argsArray.map(arg => `@param ${arg.name.replace(/\./g, '')} ${arg.comment.replace('\n', ' ')}`)); - if (member.returnComment) - lines.push(`@returns ${member.returnComment}`); - if (!lines.length) - return ''; - return `/** -${indent} * ${lines.join('\n' + indent + ' * ')} -${indent} */ -${indent}`; -} diff --git a/utils/doclint/generate_types/test/test.ts b/utils/doclint/generate_types/test/test.ts deleted file mode 100644 index 6ad3f30e03fdf..0000000000000 --- a/utils/doclint/generate_types/test/test.ts +++ /dev/null @@ -1,283 +0,0 @@ -import * as playwright from "../../../../index"; - -// Examples taken from README -(async () => { - const browser = await playwright.launch(); - const page = await browser.newPage(); - await page.goto("https://example.com"); - await page.screenshot({ path: "example.png" }); - - browser.close(); -})(); - -(async () => { - const browser = await playwright.launch(); - const page = await browser.newPage(); - await page.goto("https://news.ycombinator.com", { waitUntil: "networkidle0" }); - await page.pdf({ path: "hn.pdf", format: "A4" }); - - browser.close(); -})(); - -(async () => { - const browser = await playwright.launch(); - const page = await browser.newPage(); - await page.goto("https://example.com"); - - // Get the "viewport" of the page, as reported by the page. - const dimensions = await page.evaluate(() => { - return { - width: document.documentElement.clientWidth, - height: document.documentElement.clientHeight, - deviceScaleFactor: window.devicePixelRatio - }; - }); - - console.log("Dimensions:", dimensions); - - browser.close(); -})(); - -// The following examples are taken from the docs itself -playwright.launch().then(async browser => { - const page = await browser.newPage(); - page.on("console", (...args: any[]) => { - for (let i = 0; i < args.length; ++i) console.log(`${i}: ${args[i]}`); - }); - page.evaluate(() => console.log(5, "hello", { foo: "bar" })); - - const result = await page.evaluate(() => { - return Promise.resolve(8 * 7); - }); - console.log(await page.evaluate("1 + 2")); - - const bodyHandle = await page.$("body"); - - // Typings for this are really difficult since they depend on internal state - // of the page class. - const html = await page.evaluate( - (body: HTMLElement) => body.innerHTML, - bodyHandle - ); -}); - -import * as crypto from "crypto"; -import * as fs from "fs"; - -playwright.launch().then(async browser => { - const page = await browser.newPage(); - page.on("console", console.log); - await page.exposeFunction("md5", (text: string) => - crypto - .createHash("md5") - .update(text) - .digest("hex") - ); - await page.evaluate(async () => { - // use window.md5 to compute hashes - const myString = "PLAYWRIGHT"; - const myHash = await (window as any).md5(myString); - console.log(`md5 of ${myString} is ${myHash}`); - }); - browser.close(); - - page.on("console", console.log); - await page.exposeFunction("readfile", async (filePath: string) => { - return new Promise((resolve, reject) => { - fs.readFile(filePath, "utf8", (err, text) => { - if (err) reject(err); - else resolve(text); - }); - }); - }); - await page.evaluate(async () => { - // use window.readfile to read contents of a file - const content = await (window as any).readfile("/etc/hosts"); - console.log(content); - }); - - await page.emulateMedia("screen"); - await page.pdf({ path: "page.pdf" }); - - await page.interception.enable(); - page.on("request", interceptedRequest => { - if ( - interceptedRequest.url().endsWith(".png") || - interceptedRequest.url().endsWith(".jpg") - ) - interceptedRequest.abort(); - else interceptedRequest.continue(); - }); - - page.keyboard.type("Hello"); // Types instantly - page.keyboard.type("World", { delay: 100 }); // Types slower, like a user - - const watchDog = page.waitForFunction("window.innerWidth < 100"); - page.setViewport({ width: 50, height: 50 }); - await watchDog; - - let currentURL: string; - page - .waitForSelector("img", { visible: true }) - .then(() => console.log("First URL with image: " + currentURL)); - for (currentURL of [ - "https://example.com", - "https://google.com", - "https://bbc.com" - ]) { - await page.goto(currentURL); - } - - page.keyboard.type("Hello World!"); - page.keyboard.press("ArrowLeft"); - - page.keyboard.down("Shift"); - // tslint:disable-next-line prefer-for-of - for (let i = 0; i < " World".length; i++) { - page.keyboard.press("ArrowLeft"); - } - page.keyboard.up("Shift"); - page.keyboard.press("Backspace"); - page.keyboard.sendCharacter("嗨"); - - await page.tracing.start({ path: "trace.json" }); - await page.goto("https://www.google.com"); - await page.tracing.stop(); - - page.on("dialog", async dialog => { - console.log(dialog.message()); - await dialog.dismiss(); - browser.close(); - }); - - const inputElement = (await page.$("input[type=submit]"))!; - await inputElement.click(); -}); - -// Example with launch options -(async () => { - const browser = await playwright.launch({ - args: [ - '--no-sandbox', - '--disable-setuid-sandbox', - ], - handleSIGINT: true, - handleSIGHUP: true, - handleSIGTERM: true, - }); - const page = await browser.newPage(); - await page.goto("https://example.com"); - await page.screenshot({ path: "example.png" }); - - browser.close(); -})(); - -// Test v0.12 features -(async () => { - const browser = await playwright.launch({ - devtools: true, - env: { - JEST_TEST: true - } - }); - const page = await browser.newPage(); - const button = (await page.$("#myButton"))!; - const div = (await page.$("#myDiv"))!; - const input = (await page.$("#myInput"))!; - - if (!button) - throw new Error('Unable to select myButton'); - - if (!input) - throw new Error('Unable to select myInput'); - - await page.addStyleTag({ - url: "https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" - }); - - console.log(page.url()); - - page.type("#myInput", "Hello World!"); - - page.on("console", (event: playwright.ConsoleMessage, ...args: any[]) => { - console.log(event.text, event.type); - for (let i = 0; i < args.length; ++i) console.log(`${i}: ${args[i]}`); - }); - - await button.focus(); - await button.press("Enter"); - await button.screenshot({ - type: "jpeg", - omitBackground: true, - clip: { - x: 0, - y: 0, - width: 200, - height: 100 - } - }); - console.log(button.toString()); - input.type("Hello World", { delay: 10 }); - - const buttonText = await (await button.getProperty('textContent')).jsonValue(); - - await page.deleteCookie(...await page.cookies()); - - const metrics = await page.metrics(); - console.log(metrics.Documents, metrics.Frames, metrics.JSEventListeners); - - const navResponse = await page.waitForNavigation({ - timeout: 1000 - }); - console.log(navResponse.ok, navResponse.status, navResponse.url, navResponse.headers); - - // evaluate example - const bodyHandle = (await page.$('body'))!; - const html = await page.evaluate((body : HTMLBodyElement) => body.innerHTML, bodyHandle); - await bodyHandle.dispose(); - - // getProperties example - const handle = await page.evaluateHandle(() => ({ window, document })); - const properties = await handle.getProperties(); - const windowHandle = properties.get('window'); - const documentHandle = properties.get('document'); - await handle.dispose(); - - // queryObjects example - // Create a Map object - await page.evaluate(() => (window as any).map = new Map()); - // Get a handle to the Map object prototype - const mapPrototype = await page.evaluateHandle(() => Map.prototype); - // Query all map instances into an array - const mapInstances = await page.queryObjects(mapPrototype); - // Count amount of map objects in heap - const count = await page.evaluate((maps: Map[]) => maps.length, mapInstances); - await mapInstances.dispose(); - await mapPrototype.dispose(); - - // evaluateHandle example - const aHandle = await page.evaluateHandle(() => document.body); - const resultHandle = await page.evaluateHandle((body: Element) => body.innerHTML, aHandle); - console.log(await resultHandle.jsonValue()); - await resultHandle.dispose(); - - browser.close(); -})(); - -// test $eval and $$eval -(async () => { - const browser = await playwright.launch(); - const page = await browser.newPage(); - await page.goto("https://example.com"); - await page.$eval('#someElement', (element, text: string) => { - return element.innerHTML = text; - }, 'hey'); - - let elementText = await page.$$eval('.someClassName', (elements) => { - console.log(elements.length); - console.log(elements.map(x => x)[0].textContent); - return elements[3].innerHTML; - }); - - browser.close(); -})(); diff --git a/utils/doclint/generate_types/test/tsconfig.json b/utils/doclint/generate_types/test/tsconfig.json deleted file mode 100644 index ea3b5252e8851..0000000000000 --- a/utils/doclint/generate_types/test/tsconfig.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "compilerOptions": { - "noImplicitAny": true, - "target": "es2015", - "noEmit": true, - "types": ["node"] - }, - "include": [ - "test.ts", - "../../../../index.d.ts" - ] -} \ No newline at end of file diff --git a/utils/protocol-types-generator/index.js b/utils/protocol-types-generator/index.js index 458c1d82e4d9a..ab506d7b99e78 100644 --- a/utils/protocol-types-generator/index.js +++ b/utils/protocol-types-generator/index.js @@ -6,7 +6,7 @@ const vm = require('vm'); const os = require('os'); async function generateChromeProtocol(revision) { - const outputPath = path.join(__dirname, '..', '..', 'src', 'chromium', 'protocol.d.ts'); + const outputPath = path.join(__dirname, '..', '..', 'src', 'chromium', 'protocol.ts'); if (revision.local && fs.existsSync(outputPath)) return; const playwright = await require('../../chromium'); @@ -18,17 +18,17 @@ async function generateChromeProtocol(revision) { const version = await browser.version(); await browser.close(); fs.writeFileSync(outputPath, jsonToTS(json)); - console.log(`Wrote protocol.d.ts for ${version} to ${path.relative(process.cwd(), outputPath)}`); + console.log(`Wrote protocol.ts for ${version} to ${path.relative(process.cwd(), outputPath)}`); } async function generateWebKitProtocol(revision) { - const outputPath = path.join(__dirname, '..', '..', 'src', 'webkit', 'protocol.d.ts'); + const outputPath = path.join(__dirname, '..', '..', 'src', 'webkit', 'protocol.ts'); if (revision.local && fs.existsSync(outputPath)) return; const json = JSON.parse(fs.readFileSync(path.join(revision.folderPath, 'protocol.json'), 'utf8')); fs.writeFileSync(outputPath, jsonToTS({domains: json})); - console.log(`Wrote protocol.d.ts for WebKit to ${path.relative(process.cwd(), outputPath)}`); + console.log(`Wrote protocol.ts for WebKit to ${path.relative(process.cwd(), outputPath)}`); } function jsonToTS(json) { @@ -118,7 +118,7 @@ function typeOfProperty(property, domain) { } async function generateFirefoxProtocol(revision) { - const outputPath = path.join(__dirname, '..', '..', 'src', 'firefox', 'protocol.d.ts'); + const outputPath = path.join(__dirname, '..', '..', 'src', 'firefox', 'protocol.ts'); if (revision.local && fs.existsSync(outputPath)) return; const omnija = os.platform() === 'darwin' ? @@ -164,7 +164,7 @@ async function generateFirefoxProtocol(revision) { } const json = vm.runInContext(`(${inject})();${protocolJSCode}; this.protocol.types = types; this.protocol;`, ctx); fs.writeFileSync(outputPath, firefoxJSONToTS(json)); - console.log(`Wrote protocol.d.ts for Firefox to ${path.relative(process.cwd(), outputPath)}`); + console.log(`Wrote protocol.ts for Firefox to ${path.relative(process.cwd(), outputPath)}`); } function firefoxJSONToTS(json) {