diff --git a/.eslintignore b/.eslintignore index 687648ad61..11cbd52c0c 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 61a0cef2d3..8150e8620b 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 ae04ec7472..d800013baf 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 0000000000..8b3a05e394 --- /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 375a96b1f0..4828c8d159 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 37ac591efb..6bf59db7ef 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 30bc59d876..0000000000 --- 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 6ad3f30e03..0000000000 --- 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 ea3b5252e8..0000000000 --- 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 458c1d82e4..ab506d7b99 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) {