From 631604f11699ab868cf684be2509f15f2c388bdb Mon Sep 17 00:00:00 2001 From: Joel Einbinder Date: Fri, 28 Feb 2020 16:36:07 -0800 Subject: [PATCH 01/14] types: better types --- .eslintignore | 7 +- .gitignore | 1 + .npmignore | 2 +- index.d.ts | 14 +- package.json | 5 +- src/server/browserType.ts | 4 +- src/server/chromium.ts | 2 +- src/server/firefox.ts | 2 +- src/server/webkit.ts | 2 +- tsconfig.json | 2 +- .../doclint/check_public_api/Documentation.js | 18 +- utils/doclint/check_public_api/JSBuilder.js | 12 +- utils/doclint/check_public_api/MDBuilder.js | 12 +- utils/generate_types/index.js | 336 ++++++++++++++ utils/generate_types/overrides.d.ts | 88 ++++ utils/generate_types/parseOverrides.js | 76 ++++ utils/generate_types/test/test.ts | 428 ++++++++++++++++++ utils/generate_types/test/tsconfig.json | 11 + 18 files changed, 993 insertions(+), 29 deletions(-) create mode 100644 utils/generate_types/index.js create mode 100644 utils/generate_types/overrides.d.ts create mode 100644 utils/generate_types/parseOverrides.js create mode 100644 utils/generate_types/test/test.ts create mode 100644 utils/generate_types/test/tsconfig.json diff --git a/.eslintignore b/.eslintignore index 11cbd52c0c9b1..85e425de620fd 100644 --- a/.eslintignore +++ b/.eslintignore @@ -3,12 +3,13 @@ third_party/* utils/browser/playwright-web.js utils/doclint/check_public_api/test/ utils/testrunner/examples/ -node6/* -node6-test/* -node6-testrunner/* lib/ *.js src/generated/* src/chromium/protocol.ts src/firefox/protocol.ts src/webkit/protocol.ts +/types/* +/index.d.ts +utils/generate_types/overrides.d.ts +utils/generate_types/test/test.ts diff --git a/.gitignore b/.gitignore index a0d4e623c07fd..21ac19b12a565 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,4 @@ lib/ playwright-*.tgz /web.js /web.js.map +/types/* diff --git a/.npmignore b/.npmignore index 08aa7c2c78cc9..94d13a0e93e5f 100644 --- a/.npmignore +++ b/.npmignore @@ -7,7 +7,7 @@ # Injected files are included via lib/generated, see src/injected/README.md lib/injected/ #types -!lib/**/*.d.ts +!types/* !index.d.ts # used for npm install scripts diff --git a/index.d.ts b/index.d.ts index d6137f14fcf0e..4e7b270a0749a 100644 --- a/index.d.ts +++ b/index.d.ts @@ -14,11 +14,9 @@ * limitations under the License. */ -export * from './lib/api'; -export const devices: typeof import('./lib/deviceDescriptors').DeviceDescriptors; -export const errors: { TimeoutError: typeof import('./lib/errors').TimeoutError }; -export const chromium: import('./lib/server/chromium').Chromium; -export const firefox: import('./lib/server/firefox').Firefox; -export const webkit: import('./lib/server/webkit').WebKit; -export const selectors: import('./lib/api').Selectors; -export type PlaywrightWeb = typeof import('./lib/web'); +import * as types from './types/types'; + +export * from './types/types'; +export const webkit: types.BrowserType; +export const chromium: types.BrowserType; +export const firefox: types.BrowserType; diff --git a/package.json b/package.json index 951d194d27d7c..9abcb7f88acbf 100644 --- a/package.json +++ b/package.json @@ -33,9 +33,10 @@ "debug-test": "node --inspect-brk test/test.js", "clean": "rimraf lib", "prepare": "node install-from-github.js", - "build": "node utils/runWebpack.js --mode='development' && tsc -p .", + "build": "node utils/runWebpack.js --mode='development' && tsc -p . && npm run generate-types", "watch": "node utils/runWebpack.js --mode='development' --watch --silent | tsc -w -p .", - "version": "node utils/sync_package_versions.js && npm run doc" + "version": "node utils/sync_package_versions.js && npm run doc", + "generate-types": "node utils/generate_types/" }, "author": { "name": "Microsoft Corporation" diff --git a/src/server/browserType.ts b/src/server/browserType.ts index eafb4a0b7f454..7b76cf4dcc546 100644 --- a/src/server/browserType.ts +++ b/src/server/browserType.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { Browser, ConnectOptions } from '../browser'; +import { ConnectOptions } from '../browser'; import { BrowserContext } from '../browserContext'; import { BrowserServer } from './browserServer'; @@ -39,7 +39,7 @@ export type LaunchOptions = BrowserArgOptions & { env?: {[key: string]: string} | undefined }; -export interface BrowserType { +export interface BrowserType { executablePath(): string; name(): string; launch(options?: LaunchOptions & { slowMo?: number }): Promise; diff --git a/src/server/chromium.ts b/src/server/chromium.ts index 8e38d429f7358..cc3c93dbfbaa7 100644 --- a/src/server/chromium.ts +++ b/src/server/chromium.ts @@ -32,7 +32,7 @@ import { Events } from '../events'; import { ConnectionTransport } from '../transport'; import { BrowserContext } from '../browserContext'; -export class Chromium implements BrowserType { +export class Chromium implements BrowserType { private _executablePath: (string|undefined); executablePath(): string { diff --git a/src/server/firefox.ts b/src/server/firefox.ts index b6b55cb8aeb1e..d61cd4fad0284 100644 --- a/src/server/firefox.ts +++ b/src/server/firefox.ts @@ -32,7 +32,7 @@ import { launchProcess, waitForLine } from './processLauncher'; const mkdtempAsync = platform.promisify(fs.mkdtemp); -export class Firefox implements BrowserType { +export class Firefox implements BrowserType { private _executablePath: (string|undefined); executablePath(): string { diff --git a/src/server/webkit.ts b/src/server/webkit.ts index 13f7a337f0b70..3b524b63f128d 100644 --- a/src/server/webkit.ts +++ b/src/server/webkit.ts @@ -32,7 +32,7 @@ import { BrowserServer } from './browserServer'; import { Events } from '../events'; import { BrowserContext } from '../browserContext'; -export class WebKit implements BrowserType { +export class WebKit implements BrowserType { private _executablePath: (string|undefined); executablePath(): string { diff --git a/tsconfig.json b/tsconfig.json index 43224d7e28f0d..37a8b36a86b20 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -7,7 +7,7 @@ "rootDir": "./src", "outDir": "./lib", "strict": true, - "declaration": true + "declaration": false }, "compileOnSave": true, "include": ["src/**/*.ts"], diff --git a/utils/doclint/check_public_api/Documentation.js b/utils/doclint/check_public_api/Documentation.js index f9a6c66dae188..6a0d77f7a4aab 100644 --- a/utils/doclint/check_public_api/Documentation.js +++ b/utils/doclint/check_public_api/Documentation.js @@ -33,12 +33,14 @@ Documentation.Class = class { * @param {!Array} membersArray * @param {?string=} extendsName * @param {string=} comment + * @param {string[]=} templates */ - constructor(name, membersArray, extendsName = null, comment = '') { + constructor(name, membersArray, extendsName = null, comment = '', templates = []) { this.name = name; this.membersArray = membersArray; this.comment = comment; this.extends = extendsName; + this.templates = templates; this.index(); } @@ -124,8 +126,12 @@ Documentation.Member = class { * @param {string} name * @param {?Documentation.Type} type * @param {!Array} argsArray + * @param {string=} comment + * @param {string=} returnComment + * @param {boolean=} required + * @param {string[]=} templates */ - constructor(kind, name, type, argsArray, comment = '', returnComment = '', required = true) { + constructor(kind, name, type, argsArray, comment = '', returnComment = '', required = true, templates = []) { if (name === 'code') debugger; this.kind = kind; this.name = name; @@ -134,6 +140,7 @@ Documentation.Member = class { this.returnComment = returnComment; this.argsArray = argsArray; this.required = required; + this.templates = templates; /** @type {!Map} */ this.args = new Map(); for (const arg of argsArray) @@ -144,10 +151,13 @@ Documentation.Member = class { * @param {string} name * @param {!Array} argsArray * @param {?Documentation.Type} returnType + * @param {string=} returnComment + * @param {string=} comment + * @param {string[]=} templates * @return {!Documentation.Member} */ - static createMethod(name, argsArray, returnType, returnComment, comment) { - return new Documentation.Member('method', name, returnType, argsArray, comment, returnComment); + static createMethod(name, argsArray, returnType, returnComment, comment, templates) { + return new Documentation.Member('method', name, returnType, argsArray, comment, returnComment, undefined, templates); } /** diff --git a/utils/doclint/check_public_api/JSBuilder.js b/utils/doclint/check_public_api/JSBuilder.js index 2030923828da6..ca66239ef1abe 100644 --- a/utils/doclint/check_public_api/JSBuilder.js +++ b/utils/doclint/check_public_api/JSBuilder.js @@ -81,7 +81,7 @@ function checkSources(sources, externalDependencies) { visit(classesByName.get(parent)); }; visit(cls); - return new Documentation.Class(expandPrefix(cls.name), Array.from(membersMap.values())); + return new Documentation.Class(expandPrefix(cls.name), Array.from(membersMap.values()), undefined, cls.comment, cls.templates); }); } @@ -264,6 +264,7 @@ function checkSources(sources, externalDependencies) { function serializeClass(className, symbol, node) { /** @type {!Array} */ const members = classEvents.get(className) || []; + const templates = []; for (const [name, member] of symbol.members || []) { if (className === 'Error') continue; @@ -281,13 +282,15 @@ function checkSources(sources, externalDependencies) { } const memberType = checker.getTypeOfSymbolAtLocation(member, member.valueDeclaration); const signature = signatureForType(memberType); - if (signature) + if (member.flags & ts.SymbolFlags.TypeParameter) + templates.push(name); + else if (signature) members.push(serializeSignature(name, signature)); else members.push(serializeProperty(name, memberType)); } - return new Documentation.Class(className, members); + return new Documentation.Class(className, members, undefined, undefined, templates); } /** @@ -312,8 +315,9 @@ function checkSources(sources, externalDependencies) { function serializeSignature(name, signature) { const minArgumentCount = signature.minArgumentCount || 0; const parameters = signature.parameters.map((s, index) => serializeSymbol(s, [], index < minArgumentCount)); + const templates = signature.typeParameters ? signature.typeParameters.map(t => t.symbol.name) : []; const returnType = serializeType(signature.getReturnType()); - return Documentation.Member.createMethod(name, parameters, returnType.name !== 'void' ? returnType : null); + return Documentation.Member.createMethod(name, parameters, returnType.name !== 'void' ? returnType : null, undefined, undefined, templates); } /** diff --git a/utils/doclint/check_public_api/MDBuilder.js b/utils/doclint/check_public_api/MDBuilder.js index f736ccb910b21..3c018c4bd8c3c 100644 --- a/utils/doclint/check_public_api/MDBuilder.js +++ b/utils/doclint/check_public_api/MDBuilder.js @@ -107,6 +107,16 @@ class MDOutline { */ function parseClass(content) { const members = []; + const commentWalker = document.createTreeWalker(content, NodeFilter.SHOW_COMMENT, { + acceptNode(node) { + if (!(node instanceof Comment)) + return NodeFilter.FILTER_REJECT; + if (node.data.trim() === 'GEN:toc') + return NodeFilter.FILTER_ACCEPT; + return NodeFilter.FILTER_REJECT; + } + }); + const tocStart = commentWalker.nextNode(); const headers = content.querySelectorAll('h4'); const name = content.firstChild.textContent; let extendsName = null; @@ -116,7 +126,7 @@ class MDOutline { commentStart = extendsElement.nextSibling; extendsName = extendsElement.querySelector('a').textContent; } - const comment = parseComment(extractSiblingsIntoFragment(commentStart, headers[0])); + const comment = parseComment(extractSiblingsIntoFragment(commentStart, tocStart)); for (let i = 0; i < headers.length; i++) { const fragment = extractSiblingsIntoFragment(headers[i], headers[i + 1]); members.push(parseMember(fragment)); diff --git a/utils/generate_types/index.js b/utils/generate_types/index.js new file mode 100644 index 0000000000000..74272386c1c26 --- /dev/null +++ b/utils/generate_types/index.js @@ -0,0 +1,336 @@ +//@ts-check +const path = require('path'); +const Source = require('../doclint/Source'); +const {chromium} = require('../../index'); +const Documentation = require('../doclint/check_public_api/Documentation'); +const PROJECT_DIR = path.join(__dirname, '..', '..'); +const fs = require('fs'); +const {parseOverrides} = require('./parseOverrides'); +const objectDefinitions = []; +const handledMethods = new Set(); +/** @type {import('../doclint/check_public_api/Documentation')} */ +let documentation; +(async function() { + fs.writeFileSync(path.join(PROJECT_DIR, 'types', 'protocol.d.ts'), fs.readFileSync(path.join(PROJECT_DIR, 'src', 'chromium', 'protocol.ts')), 'utf8'); + const browser = await chromium.launch(); + const page = await browser.newPage(); + const api = await Source.readFile(path.join(PROJECT_DIR, 'docs', 'api.md')); + const {documentation: mdDocumentation} = await require('../doclint/check_public_api/MDBuilder')(page, [api]); + await browser.close(); + const sources = await Source.readdir(path.join(PROJECT_DIR, 'src')); + const {documentation: jsDocumentation} = await require('../doclint/check_public_api/JSBuilder').checkSources(sources, Object.keys(require('../../src/web.webpack.config').externals)); + documentation = mergeDocumentation(mdDocumentation, jsDocumentation); + const handledClasses = new Set(); + + function docClassForName(name) { + const docClass = documentation.classes.get(name); + if (!docClass) + throw new Error(`Unknown override class "${name}"`); + return docClass; + } + const overrides = await parseOverrides(className => { + handledClasses.add(className); + return writeComment(docClassForName(className).comment) + '\n'; + }, (className, methodName) => { + const docClass = docClassForName(className); + const method = docClass.methods.get(methodName); + handledMethods.add(`${className}.${methodName}`); + if (!method) { + if (new Set(['on', 'addListener', 'off', 'removeListener', 'once']).has(methodName)) + return ''; + throw new Error(`Unknown override method "${className}.${methodName}"`); + } + return memberJSDOC(method, ' '); + }, (className) => { + + return classBody(docClassForName(className)); + }); + const classes = documentation.classesArray.filter(cls => !handledClasses.has(cls.name)); + 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 {} +${overrides} + +${classes.map(classDesc => classToString(classDesc)).join('\n')} +${objectDefinitionsToString()} +`; + fs.writeFileSync(path.join(PROJECT_DIR, 'types', 'types.d.ts'), output, 'utf8'); +})().catch(e => { + console.error(e); + process.exit(1); +}) + +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('../doclint/check_public_api/Documentation').Class} classDesc + */ +function classToString(classDesc) { + const parts = []; + if (classDesc.comment) { + parts.push(writeComment(classDesc.comment)) + } + if (classDesc.templates.length) + console.error(`expected an override for "${classDesc.name}" becasue it is templated`); + parts.push(`export interface ${classDesc.name} ${classDesc.extends ? `extends ${classDesc.extends} ` : ''}{`); + parts.push(classBody(classDesc)); + parts.push('}\n'); + return parts.join('\n'); +} + +/** + * @param {import('../doclint/check_public_api/Documentation').Class} classDesc + */ +function classBody(classDesc) { + const parts = []; + for (const method of ['on', 'once', 'addListener']) { + for (const [eventName, value] of classDesc.events) { + if (value.comment) + parts.push(writeComment(value.comment, ' ')); + 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 => { + if (member.kind === 'event') + return ''; + const jsdoc = memberJSDOC(member, ' '); + const args = argsFromMember(member, classDesc.name); + const type = typeToString(member.type, classDesc.name, member.name); + // do this late, because we still want object definitions for overridden types + if (!hasOwnMethod(classDesc, member.name)) + return ''; + if (member.templates.length) + console.error(`expected an override for "${classDesc.name}.${member.name}" becasue it is templated`); + return `${jsdoc}${member.name}${args}: ${type};` + }).filter(x => x).join('\n\n')); + return parts.join('\n'); +} + +/** + * @param {import('../doclint/check_public_api/Documentation').Class} classDesc + * @param {string} methodName + */ +function hasOwnMethod(classDesc, methodName) { + if (handledMethods.has(`${classDesc.name}.${methodName}`)) + return false; + while (classDesc = parentClass(classDesc)) { + if (classDesc.members.has(methodName)) + return false; + } + return true; +} + +/** + * @param {import('../doclint/check_public_api/Documentation').Class} classDesc + */ +function parentClass(classDesc) { + if (!classDesc.extends) + return null; + return documentation.classes.get(classDesc.extends); +} + +function writeComment(comment, indent = '') { + const parts = []; + parts.push(indent + '/**'); + parts.push(...comment.split('\n').map(line => indent + ' * ' + line.replace(/\*\//g, '*\\/'))); + parts.push(indent + ' */'); + return parts.join('\n'); +} +function writeComment2(comment, indent = '') { + const parts = []; + parts.push('/**'); + parts.push(...comment.split('\n').map(line => indent + ' * ' + line.replace(/\*\//g, '*\\/'))); + parts.push(indent + ' */\n' + indent); + return parts.join('\n'); +} + +/** + * @param {import('../doclint/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('../doclint/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('../doclint/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 indent; + return writeComment(lines.join('\n'), indent) + '\n' + indent; +} + +/** + * @param {import('../doclint/check_public_api/Documentation')} mdDoc + * @param {import('../doclint/check_public_api/Documentation')} jsDoc + * @return {import('../doclint/check_public_api/Documentation')} + */ +function mergeDocumentation(mdDoc, jsDoc) { + const classes = []; + for (const mdClass of mdDoc.classesArray) { + const jsClass = jsDoc.classes.get(mdClass.name); + if (!jsClass) + classes.push(mdClass); + else + classes.push(mergeClasses(mdClass, jsClass)); + } + + return mdDoc; +} + +/** + * @param {import('../doclint/check_public_api/Documentation').Class} mdClass + * @param {import('../doclint/check_public_api/Documentation').Class} jsClass + * @return {import('../doclint/check_public_api/Documentation').Class} + */ +function mergeClasses(mdClass, jsClass) { + mdClass.templates = jsClass.templates; + for (const member of mdClass.membersArray) + member.templates = jsClass.members.get(member.name).templates; + return mdClass; +} \ No newline at end of file diff --git a/utils/generate_types/overrides.d.ts b/utils/generate_types/overrides.d.ts new file mode 100644 index 0000000000000..66ae153abbe50 --- /dev/null +++ b/utils/generate_types/overrides.d.ts @@ -0,0 +1,88 @@ +import {Protocol} from './protocol'; + +type Boxed = { [Index in keyof Args]: Args[Index] | JSHandle }; +type PageFunction = string | ((...args: Args) => R | Promise); +type PageFunctionOn = string | ((on: On, ...args: Args) => R | Promise); + +type Handle = T extends Node ? ElementHandle : JSHandle; +type ElementHandleForTag = ElementHandle; + +type WaitForSelectorOptionsNotHidden = PageWaitForSelectorOptions & { + visibility: 'visible'|'any'; +} + +export interface Page { + context(): C; + + evaluate(pageFunction: PageFunction, ...args: Boxed): Promise; + evaluateHandle(pageFunction: PageFunction, ...args: Boxed): Promise>; + + $(selector: K): Promise | null>; + $(selector: string): Promise | null>; + + $eval(selector: K, pageFunction: PageFunctionOn, ...args: Boxed): Promise; + $eval(selector: string, pageFunction: PageFunctionOn, ...args: Boxed): Promise; + + $$eval(selector: K, pageFunction: PageFunctionOn, ...args: Boxed): Promise; + $$eval(selector: string, pageFunction: PageFunctionOn, ...args: Boxed): Promise; + + waitForSelector(selector: string, options?: WaitForSelectorOptionsNotHidden): Promise; + waitForSelector(selector: string, options: PageWaitForSelectorOptions): Promise; +} + +export interface Frame { + evaluate(pageFunction: PageFunction, ...args: Boxed): Promise; + evaluateHandle(pageFunction: PageFunction, ...args: Boxed): Promise>; + + $(selector: K): Promise | null>; + $(selector: string): Promise | null>; + + $eval(selector: K, pageFunction: PageFunctionOn, ...args: Boxed): Promise; + $eval(selector: string, pageFunction: PageFunctionOn, ...args: Boxed): Promise; + + $$eval(selector: K, pageFunction: PageFunctionOn, ...args: Boxed): Promise; + $$eval(selector: string, pageFunction: PageFunctionOn, ...args: Boxed): Promise; +} + +export interface Worker { + evaluate(pageFunction: PageFunction, ...args: Boxed): Promise; + evaluateHandle(pageFunction: PageFunction, ...args: Boxed): Promise>; +} + +export interface JSHandle { + jsonValue(): Promise; + evaluate(pageFunction: PageFunctionOn, ...args: Boxed): Promise; + evaluateHandle(pageFunction: PageFunctionOn, ...args: Boxed): Promise>; + asElement(): T extends Node ? ElementHandle : null; +} + +export interface ElementHandle extends JSHandle { + $(selector: K): Promise | null>; + $(selector: string): Promise | null>; + + $eval(selector: K, pageFunction: PageFunctionOn, ...args: Boxed): Promise; + $eval(selector: string, pageFunction: PageFunctionOn, ...args: Boxed): Promise; + + $$eval(selector: K, pageFunction: PageFunctionOn, ...args: Boxed): Promise; + $$eval(selector: string, pageFunction: PageFunctionOn, ...args: Boxed): Promise; +} + +export interface ChromiumBrowser extends Browser { + newPage(options?: BrowserNewPageOptions): Promise>; +} + +export interface BrowserType { + +} + +export interface ChromiumSession { + on: (event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this; + addListener: (event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this; + off: (event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this; + removeListener: (event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this; + once: (event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this; + send( + method: T, + params?: Protocol.CommandParameters[T] + ): Promise; +} \ No newline at end of file diff --git a/utils/generate_types/parseOverrides.js b/utils/generate_types/parseOverrides.js new file mode 100644 index 0000000000000..4fe7cd9572c4a --- /dev/null +++ b/utils/generate_types/parseOverrides.js @@ -0,0 +1,76 @@ +const path = require('path'); +const ts = require('typescript'); +/** + * @param {(className: string) => string} commentForClass + * @param {(className: string, methodName: string) => string} commentForMethod + * @param {(className: string) => string} extraForClass + */ +async function parseOverrides(commentForClass, commentForMethod, extraForClass) { + const filePath = path.join(__dirname, 'overrides.d.ts'); + const program = ts.createProgram({ + rootNames: [filePath], + options: { + target: ts.ScriptTarget.ESNext + } + }); + const checker = program.getTypeChecker(); + const replacers = []; + const file = program.getSourceFile(filePath); + + visit(file); + + let src = file.text; + for (const replacer of replacers.sort((a, b) => b.pos - a.pos)) { + src = src.substring(0, replacer.pos) + replacer.text + src.substring(replacer.pos); + } + return src; + + /** + * @param {!ts.Node} node + */ + function visit(node) { + if (ts.isClassDeclaration(node) || ts.isClassExpression(node) || ts.isInterfaceDeclaration(node)) { + const symbol = node.name ? checker.getSymbolAtLocation(node.name) : node.symbol; + let className = symbol.getName(); + if (className === '__class') { + let parent = node; + while (parent.parent) + parent = parent.parent; + className = path.basename(parent.fileName, '.js'); + + } + if (className) + serializeClass(className, symbol, node); + } + ts.forEachChild(node, visit); + } + + + /** + * @param {string} className + * @param {!ts.Symbol} symbol + * @param {ts.Node} node + */ + function serializeClass(className, symbol, node) { + replacers.push({ + pos: node.getStart(file, false), + text: commentForClass(className), + }); + for (const [name, member] of symbol.members || []) { + if (member.flags & ts.SymbolFlags.TypeParameter) + continue; + const pos = member.valueDeclaration.getStart(file, false) + replacers.push({ + pos, + text: commentForMethod(className, name), + }); + } + replacers.push({ + pos: node.getEnd(file) - 1, + text: extraForClass(className), + }); + } + +} + +module.exports = {parseOverrides}; \ No newline at end of file diff --git a/utils/generate_types/test/test.ts b/utils/generate_types/test/test.ts new file mode 100644 index 0000000000000..0e8b161c303ce --- /dev/null +++ b/utils/generate_types/test/test.ts @@ -0,0 +1,428 @@ +import * as playwright from "../../../index"; +type AssertType = S extends T ? AssertNotAny : false; +type AssertNotAny = {notRealProperty: number} extends S ? false : true; + +// Examples taken from README +(async () => { + const browser = await playwright.chromium.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.chromium.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.chromium.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.chromium.launch().then(async browser => { + const page = await browser.newPage(); + page.on("console", message => { + console.log(message.text()); + }); + page.evaluate(() => console.log(5, "hello", { foo: "bar" })); + + { + const result = await page.evaluate(() => { + return Promise.resolve(8 * 7); + }); + const assertion : AssertType = true; + console.log(await page.evaluate("1 + 2")); + } + + const bodyHandle = await page.$("body"); + if (!bodyHandle) + return; + { + const html = await page.evaluate( + (body: HTMLElement) => body.innerHTML, + bodyHandle + ); + const assertion : AssertType = true; + } +}); + +import * as crypto from "crypto"; +import * as fs from "fs"; + +playwright.chromium.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 = "PUPPETEER"; + 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({media: "screen"}); + await page.pdf({ path: "page.pdf" }); + + await page.route('**/*', 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.setViewportSize({ width: 50, height: 50 }); + await watchDog; + + let currentURL: string; + page + .waitForSelector("img", { visibility: 'visible' }) + .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.sendCharacters("嗨"); + await browser.startTracing(page, { path: 'trace.json'}); + await page.goto("https://www.google.com"); + await browser.stopTracing(); + + 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.chromium.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.chromium.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 + }); + console.log(button.toString()); + input.type("Hello World", { delay: 10 }); + + const buttonText = await (await button.getProperty('textContent')).jsonValue(); + await page.context().clearCookies(); + + 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(); + + // 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.firefox.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(); +})(); + +// typed handles +(async () => { + const browser = await playwright.webkit.launch(); + const page = await browser.newPage(); + const windowHandle = await page.evaluateHandle(() => window); + { + const value = await page.evaluate(() => 1); + const assertion: AssertType = true; + } + { + const value = await page.mainFrame().evaluate(() => 'hello'); + const assertion: AssertType = true; + } + { + const value = await page.workers()[0].evaluate(() => [1,2,3]); + const assertion: AssertType = true; + } + { + const value = await windowHandle.evaluate((x: Window, b) => b, 'world'); + const assertion: AssertType = true; + } + + + { + const handle = await page.evaluateHandle(() => 'hello'); + const value = await handle.jsonValue(); + const assertion: AssertType = true; + } + { + const handle = await page.mainFrame().evaluateHandle(() => ['a', 'b', 'c']); + const value = await handle.jsonValue(); + const assertion: AssertType = true; + } + { + const handle = await page.workers()[0].evaluateHandle(() => 123); + const value = await handle.jsonValue(); + const assertion: AssertType = true; + } + { + const handle = await windowHandle.evaluateHandle((x: Window, b) => b, 123); + const value = await handle.jsonValue(); + const assertion: AssertType = true; + } + + { + const handle = await page.evaluateHandle(() => document.createElement('body')); + const assertion: AssertType, typeof handle> = true; + await handle.evaluate(body => { + const assertion: AssertType = true; + }); + } + + await browser.close(); +})(); + +// targets/protocol +(async () => { + const browser = await playwright.chromium.launch(); + const page = await browser.newPage(); + const context = page.context(); + const target = context.pageTarget(page); + + { + const url = target.url(); + const assertion : AssertType = true; + } + + { + const session = await target.createCDPSession(); + + session.on('Runtime.executionContextCreated', payload => { + const id = payload.context.id; + const assertion : AssertType = true; + }) + + const obj = await session.send('Runtime.evaluate', { + expression: '1 + 1' + }); + const type = obj.result.type; + const assertion : AssertType = true; + await session.detach() + } + + + // TODO popups need to be chromium pages :O + + + await browser.close(); +})(); + +(async () => { + const browser = await playwright.firefox.launch(); + const page = await browser.newPage(); + const context = page.context(); + const oneTwoThree = ('pageTarget' in context) ? context['pageTarget'] : 123; + const assertion : AssertType<123, typeof oneTwoThree> = true; + await browser.close(); +})(); + +// $eval + +(async () => { + const browser = await playwright.webkit.launch(); + const page = await browser.newPage(); + await page.$eval('span', (element, x) => { + const spanAssertion : AssertType = true; + const numberAssertion : AssertType = true; + }, 5); + await page.$eval('my-custom-element', (element, x) => { + const elementAssertion : AssertType = true; + const numberAssertion : AssertType = true; + }, 5); + await page.$$eval('my-custom-element', (elements, x) => { + const elementAssertion : AssertType = true; + const numberAssertion : AssertType = true; + }, 5); + await page.$$eval('input', (elements, x) => { + const elementAssertion : AssertType = true; + const numberAssertion : AssertType = true; + }, 5); + + const frame = page.mainFrame(); + await frame.$eval('span', (element, x, y) => { + const spanAssertion : AssertType = true; + const numberAssertion : AssertType = true; + const stringAssertion : AssertType = true; + }, 5, 'asdf'); + await frame.$eval('my-custom-element', (element) => { + const elementAssertion : AssertType = true; + }); + await frame.$$eval('my-custom-element', (elements, x, y) => { + const elementAssertion : AssertType = true; + const numberAssertion : AssertType = true; + const stringAssertion : AssertType = true; + }, 5, await page.evaluateHandle(() => 'asdf')); + await frame.$$eval('input', (elements, x) => { + const elementAssertion : AssertType = true; + const numberAssertion : AssertType = true; + }, 5); + + const something = Math.random() > .5 ? 'visible' : 'any'; + const handle = await page.waitForSelector('a', {visibility: something}); + await handle.$eval('span', (element, x, y) => { + const spanAssertion : AssertType = true; + const numberAssertion : AssertType = true; + const stringAssertion : AssertType = true; + }, 5, 'asdf'); + await handle.$eval('my-custom-element', (element) => { + const elementAssertion : AssertType = true; + }); + await handle.$$eval('my-custom-element', (elements, x, y) => { + const elementAssertion : AssertType = true; + const numberAssertion : AssertType = true; + const stringAssertion : AssertType = true; + }, 5, await page.evaluateHandle(() => 'asdf')); + await handle.$$eval('input', (elements, x) => { + const elementAssertion : AssertType = true; + const numberAssertion : AssertType = true; + }, 5); + await browser.close(); +})(); diff --git a/utils/generate_types/test/tsconfig.json b/utils/generate_types/test/tsconfig.json new file mode 100644 index 0000000000000..f042a91379e00 --- /dev/null +++ b/utils/generate_types/test/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "strict": true, + "target": "es2015", + "noEmit": true, + "moduleResolution": "node" + }, + "include": [ + "test.ts" + ] +} \ No newline at end of file From 462e231dce4eed7944bcec44c93284b5c92ddeb1 Mon Sep 17 00:00:00 2001 From: Joel Einbinder Date: Tue, 3 Mar 2020 17:41:06 -0800 Subject: [PATCH 02/14] a lot of fixes --- utils/doclint/check_public_api/MDBuilder.js | 4 +- utils/generate_types/index.js | 32 +++-- utils/generate_types/overrides.d.ts | 54 ++++++--- utils/generate_types/test/test.ts | 125 +++++++++++++++----- 4 files changed, 160 insertions(+), 55 deletions(-) diff --git a/utils/doclint/check_public_api/MDBuilder.js b/utils/doclint/check_public_api/MDBuilder.js index 3c018c4bd8c3c..5b85fac541b7b 100644 --- a/utils/doclint/check_public_api/MDBuilder.js +++ b/utils/doclint/check_public_api/MDBuilder.js @@ -111,7 +111,7 @@ class MDOutline { acceptNode(node) { if (!(node instanceof Comment)) return NodeFilter.FILTER_REJECT; - if (node.data.trim() === 'GEN:toc') + if (node.data.trim().startsWith('GEN:toc')) return NodeFilter.FILTER_ACCEPT; return NodeFilter.FILTER_REJECT; } @@ -188,7 +188,7 @@ class MDOutline { errors.push(`${name} has mistyped 'return' type declaration: expected exactly '${expectedText}', found '${actualText}'.`); } } - const comment = parseComment(extractSiblingsIntoFragment(ul ? ul.nextSibling : content)); + const comment = parseComment(extractSiblingsIntoFragment(ul ? ul.nextSibling : content.querySelector('h4').nextSibling)); return { name, args, diff --git a/utils/generate_types/index.js b/utils/generate_types/index.js index 74272386c1c26..f924974c5e3b0 100644 --- a/utils/generate_types/index.js +++ b/utils/generate_types/index.js @@ -11,7 +11,10 @@ const handledMethods = new Set(); /** @type {import('../doclint/check_public_api/Documentation')} */ let documentation; (async function() { - fs.writeFileSync(path.join(PROJECT_DIR, 'types', 'protocol.d.ts'), fs.readFileSync(path.join(PROJECT_DIR, 'src', 'chromium', 'protocol.ts')), 'utf8'); + const typesDir = path.join(PROJECT_DIR, 'types'); + if (!fs.existsSync(typesDir)) + fs.mkdirSync(typesDir) + fs.writeFileSync(path.join(typesDir, 'protocol.d.ts'), fs.readFileSync(path.join(PROJECT_DIR, 'src', 'chromium', 'protocol.ts')), 'utf8'); const browser = await chromium.launch(); const page = await browser.newPage(); const api = await Source.readFile(path.join(PROJECT_DIR, 'docs', 'api.md')); @@ -40,9 +43,8 @@ let documentation; return ''; throw new Error(`Unknown override method "${className}.${methodName}"`); } - return memberJSDOC(method, ' '); + return memberJSDOC(method, ' ').trimLeft(); }, (className) => { - return classBody(docClassForName(className)); }); const classes = documentation.classesArray.filter(cls => !handledClasses.has(cls.name)); @@ -59,7 +61,7 @@ ${overrides} ${classes.map(classDesc => classToString(classDesc)).join('\n')} ${objectDefinitionsToString()} `; - fs.writeFileSync(path.join(PROJECT_DIR, 'types', 'types.d.ts'), output, 'utf8'); + fs.writeFileSync(path.join(typesDir, 'types.d.ts'), output, 'utf8'); })().catch(e => { console.error(e); process.exit(1); @@ -97,6 +99,15 @@ function classToString(classDesc) { return parts.join('\n'); } +/** + * @param {string} type + */ +function argNameForType(type) { + if (type === 'void') + return null; + return type[0].toLowerCase() + type.slice(1); +} + /** * @param {import('../doclint/check_public_api/Documentation').Class} classDesc */ @@ -106,7 +117,10 @@ function classBody(classDesc) { for (const [eventName, value] of classDesc.events) { if (value.comment) parts.push(writeComment(value.comment, ' ')); - parts.push(` ${method}(event: '${eventName}', listener: (arg0 : ${typeToString(value && value.type, classDesc.name, eventName, 'payload')}) => void): this;\n`); + const type = typeToString(value && value.type, classDesc.name, eventName, 'payload'); + const argName = argNameForType(type); + const params = argName ? `${argName} : ${type}` : ''; + parts.push(` ${method}(event: '${eventName}', listener: (${params}) => void): this;\n`); } } const members = classDesc.membersArray.filter(member => member.kind !== 'event'); @@ -171,8 +185,6 @@ 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); @@ -242,6 +254,9 @@ function parseType(type) { }; } +/** + * @return {string} + */ function stringifyType(parsedType) { if (!parsedType) return 'void'; @@ -255,7 +270,7 @@ function stringifyType(parsedType) { arg.next = null; stringArgs.push(stringifyType(arg)); } - out = `((${stringArgs.map((type, index) => `arg${index} : ${type}`).join(', ')}, ...args: any[]) => ${stringifyType(parsedType.retType)})`; + out = `((${stringArgs.map((type, index) => `arg${index} : ${type}`).join(', ')}) => ${stringifyType(parsedType.retType)})`; } else if (parsedType.name === 'function') { out = 'Function'; } @@ -292,6 +307,7 @@ function argsFromMember(member, ...namespace) { } /** * @param {import('../doclint/check_public_api/Documentation').Member} member + * @param {string} indent */ function memberJSDOC(member, indent) { const lines = []; diff --git a/utils/generate_types/overrides.d.ts b/utils/generate_types/overrides.d.ts index 66ae153abbe50..f340de303361b 100644 --- a/utils/generate_types/overrides.d.ts +++ b/utils/generate_types/overrides.d.ts @@ -11,23 +11,29 @@ type WaitForSelectorOptionsNotHidden = PageWaitForSelectorOptions & { visibility: 'visible'|'any'; } -export interface Page { - context(): C; +type HTMLOrSVGElement = SVGElement | HTMLElement; +type HTMLOrSVGElementHandle = ElementHandle; +export interface Page { evaluate(pageFunction: PageFunction, ...args: Boxed): Promise; evaluateHandle(pageFunction: PageFunction, ...args: Boxed): Promise>; $(selector: K): Promise | null>; - $(selector: string): Promise | null>; + $(selector: string): Promise; + + $$(selector: K): Promise[]>; + $$(selector: string): Promise; $eval(selector: K, pageFunction: PageFunctionOn, ...args: Boxed): Promise; - $eval(selector: string, pageFunction: PageFunctionOn, ...args: Boxed): Promise; + $eval(selector: string, pageFunction: PageFunctionOn, ...args: Boxed): Promise; $$eval(selector: K, pageFunction: PageFunctionOn, ...args: Boxed): Promise; - $$eval(selector: string, pageFunction: PageFunctionOn, ...args: Boxed): Promise; + $$eval(selector: string, pageFunction: PageFunctionOn, ...args: Boxed): Promise; - waitForSelector(selector: string, options?: WaitForSelectorOptionsNotHidden): Promise; - waitForSelector(selector: string, options: PageWaitForSelectorOptions): Promise; + waitForSelector(selector: K, options?: WaitForSelectorOptionsNotHidden): Promise>; + waitForSelector(selector: string, options?: WaitForSelectorOptionsNotHidden): Promise; + waitForSelector(selector: K, options: PageWaitForSelectorOptions): Promise | null>; + waitForSelector(selector: string, options: PageWaitForSelectorOptions): Promise; } export interface Frame { @@ -35,13 +41,21 @@ export interface Frame { evaluateHandle(pageFunction: PageFunction, ...args: Boxed): Promise>; $(selector: K): Promise | null>; - $(selector: string): Promise | null>; + $(selector: string): Promise; + + $$(selector: K): Promise[]>; + $$(selector: string): Promise; $eval(selector: K, pageFunction: PageFunctionOn, ...args: Boxed): Promise; - $eval(selector: string, pageFunction: PageFunctionOn, ...args: Boxed): Promise; + $eval(selector: string, pageFunction: PageFunctionOn, ...args: Boxed): Promise; $$eval(selector: K, pageFunction: PageFunctionOn, ...args: Boxed): Promise; - $$eval(selector: string, pageFunction: PageFunctionOn, ...args: Boxed): Promise; + $$eval(selector: string, pageFunction: PageFunctionOn, ...args: Boxed): Promise; + + waitForSelector(selector: K, options?: WaitForSelectorOptionsNotHidden): Promise>; + waitForSelector(selector: string, options?: WaitForSelectorOptionsNotHidden): Promise; + waitForSelector(selector: K, options: PageWaitForSelectorOptions): Promise | null>; + waitForSelector(selector: string, options: PageWaitForSelectorOptions): Promise; } export interface Worker { @@ -58,24 +72,28 @@ export interface JSHandle { export interface ElementHandle extends JSHandle { $(selector: K): Promise | null>; - $(selector: string): Promise | null>; + $(selector: string): Promise; + + $$(selector: K): Promise[]>; + $$(selector: string): Promise; $eval(selector: K, pageFunction: PageFunctionOn, ...args: Boxed): Promise; - $eval(selector: string, pageFunction: PageFunctionOn, ...args: Boxed): Promise; + $eval(selector: string, pageFunction: PageFunctionOn, ...args: Boxed): Promise; $$eval(selector: K, pageFunction: PageFunctionOn, ...args: Boxed): Promise; - $$eval(selector: string, pageFunction: PageFunctionOn, ...args: Boxed): Promise; + $$eval(selector: string, pageFunction: PageFunctionOn, ...args: Boxed): Promise; } -export interface ChromiumBrowser extends Browser { - newPage(options?: BrowserNewPageOptions): Promise>; +export interface BrowserType { + } -export interface BrowserType { - +export interface ChromiumBrowser extends Browser { + contexts(): Array; + newContext(options?: BrowserNewContextOptions): Promise; } -export interface ChromiumSession { +export interface ChromiumSession { on: (event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this; addListener: (event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this; off: (event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this; diff --git a/utils/generate_types/test/test.ts b/utils/generate_types/test/test.ts index 0e8b161c303ce..549a43d3f9243 100644 --- a/utils/generate_types/test/test.ts +++ b/utils/generate_types/test/test.ts @@ -54,6 +54,7 @@ playwright.chromium.launch().then(async browser => { }); const assertion : AssertType = true; console.log(await page.evaluate("1 + 2")); + page.$eval('.foo', e => e.style); } const bodyHandle = await page.$("body"); @@ -115,8 +116,16 @@ playwright.chromium.launch().then(async browser => { else interceptedRequest.continue(); }); - page.keyboard.type("Hello"); // Types instantly - page.keyboard.type("World", { delay: 100 }); // Types slower, like a user + await page.route(str => { + const assertion : AssertType = true; + return true; + }, interceptedRequest => { + interceptedRequest.continue(); + return 'something random for no reason'; + }); + + await page.keyboard.type("Hello"); // Types instantly + await page.keyboard.type("World", { delay: 100 }); // Types slower, like a user const watchDog = page.waitForFunction("window.innerWidth < 100"); page.setViewportSize({ width: 50, height: 50 }); @@ -320,36 +329,24 @@ playwright.chromium.launch().then(async browser => { await browser.close(); })(); -// targets/protocol +// protocol (async () => { const browser = await playwright.chromium.launch(); - const page = await browser.newPage(); - const context = page.context(); - const target = context.pageTarget(page); - - { - const url = target.url(); - const assertion : AssertType = true; - } - - { - const session = await target.createCDPSession(); + const context = await browser.newContext(); + const session = await context.createSession(await context.newPage()); - session.on('Runtime.executionContextCreated', payload => { - const id = payload.context.id; - const assertion : AssertType = true; - }) - const obj = await session.send('Runtime.evaluate', { - expression: '1 + 1' - }); - const type = obj.result.type; - const assertion : AssertType = true; - await session.detach() - } - + session.on('Runtime.executionContextCreated', payload => { + const id = payload.context.id; + const assertion : AssertType = true; + }) - // TODO popups need to be chromium pages :O + const obj = await session.send('Runtime.evaluate', { + expression: '1 + 1' + }); + const type = obj.result.type; + const assertion : AssertType = true; + await session.detach() await browser.close(); @@ -426,3 +423,77 @@ playwright.chromium.launch().then(async browser => { }, 5); await browser.close(); })(); + +// query selectors + +(async () => { + const browser = await playwright.chromium.launch(); + const page = await browser.newPage(); + const frame = page.mainFrame(); + const element = await page.waitForSelector('some-fake-element'); + const elementLikes = [page, frame, element]; + for (const elementLike of elementLikes) { + { + const handle = await elementLike.$('body'); + const bodyAssertion : AssertType, typeof handle> = true; + } + { + const handle = await elementLike.$('something-strange'); + const top = await handle!.evaluate(element => element.style.top); + const assertion : AssertType = true; + } + + { + const handles = await elementLike.$$('body'); + const bodyAssertion : AssertType[], typeof handles> = true; + } + + { + const handles = await elementLike.$$('something-strange'); + const top = await handles[0].evaluate(element => element.style.top); + const assertion : AssertType = true; + } + } + + const frameLikes = [page, frame]; + for (const frameLike of frameLikes) { + { + const handle = await frameLike.waitForSelector('body'); + const bodyAssertion : AssertType, typeof handle> = true; + const canBeNull : AssertType = false; + } + { + const visibility = Math.random() > .5 ? 'any': 'visible'; + const handle = await frameLike.waitForSelector('body', {visibility}); + const bodyAssertion : AssertType, typeof handle> = true; + const canBeNull : AssertType = false; + } + { + const visibility = Math.random() > .5 ? 'hidden': 'visible'; + const handle = await frameLike.waitForSelector('body', {visibility}); + const bodyAssertion : AssertType, typeof handle> = true; + const canBeNull : AssertType = true; + } + + { + const handle = await frameLike.waitForSelector('something-strange'); + const elementAssertion : AssertType, typeof handle> = true; + const canBeNull : AssertType = false; + } + { + const visibility = Math.random() > .5 ? 'any': 'visible'; + const handle = await frameLike.waitForSelector('something-strange', {visibility}); + const elementAssertion : AssertType, typeof handle> = true; + const canBeNull : AssertType = false; + } + { + const visibility = Math.random() > .5 ? 'hidden': 'visible'; + const handle = await frameLike.waitForSelector('something-strange', {visibility}); + const elementAssertion : AssertType, typeof handle> = true; + const canBeNull : AssertType = true; + } + } + + + await browser.close(); +})(); From 20f5428f4a8e41b171ac8eae324ae13e0a2d10c9 Mon Sep 17 00:00:00 2001 From: Joel Einbinder Date: Tue, 10 Mar 2020 22:54:42 +0100 Subject: [PATCH 03/14] easy fixes --- utils/generate_types/index.js | 51 +++-- utils/generate_types/overrides.d.ts | 22 +++ utils/generate_types/parseOverrides.js | 16 ++ utils/generate_types/test/test.ts | 262 +++++++++++++------------ 4 files changed, 207 insertions(+), 144 deletions(-) diff --git a/utils/generate_types/index.js b/utils/generate_types/index.js index f924974c5e3b0..79e64acf985de 100644 --- a/utils/generate_types/index.js +++ b/utils/generate_types/index.js @@ -1,3 +1,19 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * 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. + */ + //@ts-check const path = require('path'); const Source = require('../doclint/Source'); @@ -8,7 +24,7 @@ const fs = require('fs'); const {parseOverrides} = require('./parseOverrides'); const objectDefinitions = []; const handledMethods = new Set(); -/** @type {import('../doclint/check_public_api/Documentation')} */ +/** @type {Documentation} */ let documentation; (async function() { const typesDir = path.join(PROJECT_DIR, 'types'); @@ -49,13 +65,6 @@ let documentation; }); const classes = documentation.classesArray.filter(cls => !handledClasses.has(cls.name)); 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 {} ${overrides} ${classes.map(classDesc => classToString(classDesc)).join('\n')} @@ -84,7 +93,7 @@ function nameForProperty(member) { } /** - * @param {import('../doclint/check_public_api/Documentation').Class} classDesc + * @param {Documentation.Class} classDesc */ function classToString(classDesc) { const parts = []; @@ -109,7 +118,7 @@ function argNameForType(type) { } /** - * @param {import('../doclint/check_public_api/Documentation').Class} classDesc + * @param {Documentation.Class} classDesc */ function classBody(classDesc) { const parts = []; @@ -141,7 +150,7 @@ function classBody(classDesc) { } /** - * @param {import('../doclint/check_public_api/Documentation').Class} classDesc + * @param {Documentation.Class} classDesc * @param {string} methodName */ function hasOwnMethod(classDesc, methodName) { @@ -155,7 +164,7 @@ function hasOwnMethod(classDesc, methodName) { } /** - * @param {import('../doclint/check_public_api/Documentation').Class} classDesc + * @param {Documentation.Class} classDesc */ function parentClass(classDesc) { if (!classDesc.extends) @@ -179,7 +188,7 @@ function writeComment2(comment, indent = '') { } /** - * @param {import('../doclint/check_public_api/Documentation').Type} type + * @param {Documentation.Type} type */ function typeToString(type, ...namespace) { if (!type) @@ -298,7 +307,7 @@ function matchingBracket(str, open, close) { } /** - * @param {import('../doclint/check_public_api/Documentation').Member} member + * @param {Documentation.Member} member */ function argsFromMember(member, ...namespace) { if (member.kind === 'property') @@ -306,7 +315,7 @@ function argsFromMember(member, ...namespace) { return '(' + member.argsArray.map(arg => `${nameForProperty(arg)}: ${typeToString(arg.type, ...namespace, member.name, 'options')}`).join(', ') + ')'; } /** - * @param {import('../doclint/check_public_api/Documentation').Member} member + * @param {Documentation.Member} member * @param {string} indent */ function memberJSDOC(member, indent) { @@ -322,9 +331,9 @@ function memberJSDOC(member, indent) { } /** - * @param {import('../doclint/check_public_api/Documentation')} mdDoc - * @param {import('../doclint/check_public_api/Documentation')} jsDoc - * @return {import('../doclint/check_public_api/Documentation')} + * @param {Documentation} mdDoc + * @param {Documentation} jsDoc + * @return {Documentation} */ function mergeDocumentation(mdDoc, jsDoc) { const classes = []; @@ -340,9 +349,9 @@ function mergeDocumentation(mdDoc, jsDoc) { } /** - * @param {import('../doclint/check_public_api/Documentation').Class} mdClass - * @param {import('../doclint/check_public_api/Documentation').Class} jsClass - * @return {import('../doclint/check_public_api/Documentation').Class} + * @param {Documentation.Class} mdClass + * @param {Documentation.Class} jsClass + * @return {Documentation.Class} */ function mergeClasses(mdClass, jsClass) { mdClass.templates = jsClass.templates; diff --git a/utils/generate_types/overrides.d.ts b/utils/generate_types/overrides.d.ts index f340de303361b..a9f65826637dd 100644 --- a/utils/generate_types/overrides.d.ts +++ b/utils/generate_types/overrides.d.ts @@ -1,4 +1,26 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * 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 {Protocol} from './protocol'; +import { ChildProcess } from 'child_process'; +import { EventEmitter } from 'events'; +/** + * Can be converted to JSON + */ +interface Serializable {} +interface ConnectionTransport {} type Boxed = { [Index in keyof Args]: Args[Index] | JSHandle }; type PageFunction = string | ((...args: Args) => R | Promise); diff --git a/utils/generate_types/parseOverrides.js b/utils/generate_types/parseOverrides.js index 4fe7cd9572c4a..b4da73352e995 100644 --- a/utils/generate_types/parseOverrides.js +++ b/utils/generate_types/parseOverrides.js @@ -1,3 +1,19 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * 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. + */ + const path = require('path'); const ts = require('typescript'); /** diff --git a/utils/generate_types/test/test.ts b/utils/generate_types/test/test.ts index 549a43d3f9243..ab3dc0df1bbe4 100644 --- a/utils/generate_types/test/test.ts +++ b/utils/generate_types/test/test.ts @@ -1,4 +1,20 @@ -import * as playwright from "../../../index"; +/** + * Copyright (c) Microsoft Corporation. + * + * 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 * as playwright from '../../../index'; type AssertType = S extends T ? AssertNotAny : false; type AssertNotAny = {notRealProperty: number} extends S ? false : true; @@ -6,8 +22,8 @@ type AssertNotAny = {notRealProperty: number} extends S ? false : true; (async () => { const browser = await playwright.chromium.launch(); const page = await browser.newPage(); - await page.goto("https://example.com"); - await page.screenshot({ path: "example.png" }); + await page.goto('https://example.com'); + await page.screenshot({ path: 'example.png' }); browser.close(); })(); @@ -15,8 +31,8 @@ type AssertNotAny = {notRealProperty: number} extends S ? false : true; (async () => { const browser = await playwright.chromium.launch(); const page = await browser.newPage(); - await page.goto("https://news.ycombinator.com", { waitUntil: "networkidle0" }); - await page.pdf({ path: "hn.pdf", format: "A4" }); + await page.goto('https://news.ycombinator.com', { waitUntil: 'networkidle0' }); + await page.pdf({ path: 'hn.pdf', format: 'A4' }); browser.close(); })(); @@ -24,7 +40,7 @@ type AssertNotAny = {notRealProperty: number} extends S ? false : true; (async () => { const browser = await playwright.chromium.launch(); const page = await browser.newPage(); - await page.goto("https://example.com"); + await page.goto('https://example.com'); // Get the "viewport" of the page, as reported by the page. const dimensions = await page.evaluate(() => { @@ -35,7 +51,7 @@ type AssertNotAny = {notRealProperty: number} extends S ? false : true; }; }); - console.log("Dimensions:", dimensions); + console.log('Dimensions:', dimensions); browser.close(); })(); @@ -43,56 +59,56 @@ type AssertNotAny = {notRealProperty: number} extends S ? false : true; // The following examples are taken from the docs itself playwright.chromium.launch().then(async browser => { const page = await browser.newPage(); - page.on("console", message => { + page.on('console', message => { console.log(message.text()); }); - page.evaluate(() => console.log(5, "hello", { foo: "bar" })); + page.evaluate(() => console.log(5, 'hello', { foo: 'bar' })); { const result = await page.evaluate(() => { return Promise.resolve(8 * 7); }); - const assertion : AssertType = true; - console.log(await page.evaluate("1 + 2")); + const assertion: AssertType = true; + console.log(await page.evaluate('1 + 2')); page.$eval('.foo', e => e.style); } - const bodyHandle = await page.$("body"); + const bodyHandle = await page.$('body'); if (!bodyHandle) return; { const html = await page.evaluate( - (body: HTMLElement) => body.innerHTML, - bodyHandle + (body: HTMLElement) => body.innerHTML, + bodyHandle ); - const assertion : AssertType = true; + const assertion: AssertType = true; } }); -import * as crypto from "crypto"; -import * as fs from "fs"; +import * as crypto from 'crypto'; +import * as fs from 'fs'; playwright.chromium.launch().then(async browser => { const page = await browser.newPage(); - page.on("console", console.log); - await page.exposeFunction("md5", (text: string) => + page.on('console', console.log); + await page.exposeFunction('md5', (text: string) => crypto - .createHash("md5") - .update(text) - .digest("hex") + .createHash('md5') + .update(text) + .digest('hex') ); await page.evaluate(async () => { // use window.md5 to compute hashes - const myString = "PUPPETEER"; + const myString = 'PUPPETEER'; 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) => { + page.on('console', console.log); + await page.exposeFunction('readfile', async (filePath: string) => { return new Promise((resolve, reject) => { - fs.readFile(filePath, "utf8", (err, text) => { + fs.readFile(filePath, 'utf8', (err, text) => { if (err) reject(err); else resolve(text); }); @@ -100,71 +116,71 @@ playwright.chromium.launch().then(async browser => { }); await page.evaluate(async () => { // use window.readfile to read contents of a file - const content = await (window as any).readfile("/etc/hosts"); + const content = await (window as any).readfile('/etc/hosts'); console.log(content); }); - await page.emulateMedia({media: "screen"}); - await page.pdf({ path: "page.pdf" }); + await page.emulateMedia({media: 'screen'}); + await page.pdf({ path: 'page.pdf' }); await page.route('**/*', interceptedRequest => { if ( - interceptedRequest.url().endsWith(".png") || - interceptedRequest.url().endsWith(".jpg") + interceptedRequest.url().endsWith('.png') || + interceptedRequest.url().endsWith('.jpg') ) interceptedRequest.abort(); else interceptedRequest.continue(); }); await page.route(str => { - const assertion : AssertType = true; + const assertion: AssertType = true; return true; }, interceptedRequest => { interceptedRequest.continue(); return 'something random for no reason'; }); - await page.keyboard.type("Hello"); // Types instantly - await page.keyboard.type("World", { delay: 100 }); // Types slower, like a user + await page.keyboard.type('Hello'); // Types instantly + await page.keyboard.type('World', { delay: 100 }); // Types slower, like a user - const watchDog = page.waitForFunction("window.innerWidth < 100"); + const watchDog = page.waitForFunction('window.innerWidth < 100'); page.setViewportSize({ width: 50, height: 50 }); await watchDog; let currentURL: string; page - .waitForSelector("img", { visibility: 'visible' }) - .then(() => console.log("First URL with image: " + currentURL)); + .waitForSelector('img', { visibility: 'visible' }) + .then(() => console.log('First URL with image: ' + currentURL)); for (currentURL of [ - "https://example.com", - "https://google.com", - "https://bbc.com" - ]) { + '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"); + 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.sendCharacters("嗨"); + for (let i = 0; i < ' World'.length; i++) + page.keyboard.press('ArrowLeft'); + + page.keyboard.up('Shift'); + page.keyboard.press('Backspace'); + page.keyboard.sendCharacters('嗨'); await browser.startTracing(page, { path: 'trace.json'}); - await page.goto("https://www.google.com"); + await page.goto('https://www.google.com'); await browser.stopTracing(); - page.on("dialog", async dialog => { + page.on('dialog', async dialog => { console.log(dialog.message()); await dialog.dismiss(); browser.close(); }); - const inputElement = (await page.$("input[type=submit]"))!; + const inputElement = (await page.$('input[type=submit]'))!; await inputElement.click(); }); @@ -180,8 +196,8 @@ playwright.chromium.launch().then(async browser => { handleSIGTERM: true, }); const page = await browser.newPage(); - await page.goto("https://example.com"); - await page.screenshot({ path: "example.png" }); + await page.goto('https://example.com'); + await page.screenshot({ path: 'example.png' }); browser.close(); })(); @@ -195,9 +211,9 @@ playwright.chromium.launch().then(async browser => { } }); const page = await browser.newPage(); - const button = (await page.$("#myButton"))!; - const div = (await page.$("#myDiv"))!; - const input = (await page.$("#myInput"))!; + const button = (await page.$('#myButton'))!; + const div = (await page.$('#myDiv'))!; + const input = (await page.$('#myInput'))!; if (!button) throw new Error('Unable to select myButton'); @@ -206,26 +222,26 @@ playwright.chromium.launch().then(async browser => { throw new Error('Unable to select myInput'); await page.addStyleTag({ - url: "https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" + url: 'https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css' }); console.log(page.url()); - page.type("#myInput", "Hello World!"); + page.type('#myInput', 'Hello World!'); - page.on("console", (event: playwright.ConsoleMessage, ...args: any[]) => { + 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.press('Enter'); await button.screenshot({ - type: "jpeg", + type: 'jpeg', omitBackground: true }); console.log(button.toString()); - input.type("Hello World", { delay: 10 }); + input.type('Hello World', { delay: 10 }); const buttonText = await (await button.getProperty('textContent')).jsonValue(); await page.context().clearCookies(); @@ -237,7 +253,7 @@ playwright.chromium.launch().then(async browser => { // evaluate example const bodyHandle = (await page.$('body'))!; - const html = await page.evaluate((body : HTMLBodyElement) => body.innerHTML, bodyHandle); + const html = await page.evaluate((body: HTMLBodyElement) => body.innerHTML, bodyHandle); await bodyHandle.dispose(); // getProperties example @@ -260,12 +276,12 @@ playwright.chromium.launch().then(async browser => { (async () => { const browser = await playwright.firefox.launch(); const page = await browser.newPage(); - await page.goto("https://example.com"); + 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) => { + const elementText = await page.$$eval('.someClassName', elements => { console.log(elements.length); console.log(elements.map(x => x)[0].textContent); return elements[3].innerHTML; @@ -338,15 +354,15 @@ playwright.chromium.launch().then(async browser => { session.on('Runtime.executionContextCreated', payload => { const id = payload.context.id; - const assertion : AssertType = true; - }) + const assertion: AssertType = true; + }); const obj = await session.send('Runtime.evaluate', { expression: '1 + 1' }); const type = obj.result.type; - const assertion : AssertType = true; - await session.detach() + const assertion: AssertType = true; + await session.detach(); await browser.close(); @@ -357,7 +373,7 @@ playwright.chromium.launch().then(async browser => { const page = await browser.newPage(); const context = page.context(); const oneTwoThree = ('pageTarget' in context) ? context['pageTarget'] : 123; - const assertion : AssertType<123, typeof oneTwoThree> = true; + const assertion: AssertType<123, typeof oneTwoThree> = true; await browser.close(); })(); @@ -367,59 +383,59 @@ playwright.chromium.launch().then(async browser => { const browser = await playwright.webkit.launch(); const page = await browser.newPage(); await page.$eval('span', (element, x) => { - const spanAssertion : AssertType = true; - const numberAssertion : AssertType = true; + const spanAssertion: AssertType = true; + const numberAssertion: AssertType = true; }, 5); await page.$eval('my-custom-element', (element, x) => { - const elementAssertion : AssertType = true; - const numberAssertion : AssertType = true; + const elementAssertion: AssertType = true; + const numberAssertion: AssertType = true; }, 5); await page.$$eval('my-custom-element', (elements, x) => { - const elementAssertion : AssertType = true; - const numberAssertion : AssertType = true; + const elementAssertion: AssertType<(HTMLElement|SVGElement)[], typeof elements> = true; + const numberAssertion: AssertType = true; }, 5); await page.$$eval('input', (elements, x) => { - const elementAssertion : AssertType = true; - const numberAssertion : AssertType = true; + const elementAssertion: AssertType = true; + const numberAssertion: AssertType = true; }, 5); const frame = page.mainFrame(); await frame.$eval('span', (element, x, y) => { - const spanAssertion : AssertType = true; - const numberAssertion : AssertType = true; - const stringAssertion : AssertType = true; + const spanAssertion: AssertType = true; + const numberAssertion: AssertType = true; + const stringAssertion: AssertType = true; }, 5, 'asdf'); - await frame.$eval('my-custom-element', (element) => { - const elementAssertion : AssertType = true; + await frame.$eval('my-custom-element', element => { + const elementAssertion: AssertType = true; }); await frame.$$eval('my-custom-element', (elements, x, y) => { - const elementAssertion : AssertType = true; - const numberAssertion : AssertType = true; - const stringAssertion : AssertType = true; + const elementAssertion: AssertType<(HTMLElement|SVGElement)[], typeof elements> = true; + const numberAssertion: AssertType = true; + const stringAssertion: AssertType = true; }, 5, await page.evaluateHandle(() => 'asdf')); await frame.$$eval('input', (elements, x) => { - const elementAssertion : AssertType = true; - const numberAssertion : AssertType = true; + const elementAssertion: AssertType = true; + const numberAssertion: AssertType = true; }, 5); const something = Math.random() > .5 ? 'visible' : 'any'; const handle = await page.waitForSelector('a', {visibility: something}); await handle.$eval('span', (element, x, y) => { - const spanAssertion : AssertType = true; - const numberAssertion : AssertType = true; - const stringAssertion : AssertType = true; + const spanAssertion: AssertType = true; + const numberAssertion: AssertType = true; + const stringAssertion: AssertType = true; }, 5, 'asdf'); - await handle.$eval('my-custom-element', (element) => { - const elementAssertion : AssertType = true; + await handle.$eval('my-custom-element', element => { + const elementAssertion: AssertType = true; }); await handle.$$eval('my-custom-element', (elements, x, y) => { - const elementAssertion : AssertType = true; - const numberAssertion : AssertType = true; - const stringAssertion : AssertType = true; + const elementAssertion: AssertType<(HTMLElement|SVGElement)[], typeof elements> = true; + const numberAssertion: AssertType = true; + const stringAssertion: AssertType = true; }, 5, await page.evaluateHandle(() => 'asdf')); await handle.$$eval('input', (elements, x) => { - const elementAssertion : AssertType = true; - const numberAssertion : AssertType = true; + const elementAssertion: AssertType = true; + const numberAssertion: AssertType = true; }, 5); await browser.close(); })(); @@ -435,23 +451,23 @@ playwright.chromium.launch().then(async browser => { for (const elementLike of elementLikes) { { const handle = await elementLike.$('body'); - const bodyAssertion : AssertType, typeof handle> = true; + const bodyAssertion: AssertType, typeof handle> = true; } { const handle = await elementLike.$('something-strange'); const top = await handle!.evaluate(element => element.style.top); - const assertion : AssertType = true; + const assertion: AssertType = true; } { const handles = await elementLike.$$('body'); - const bodyAssertion : AssertType[], typeof handles> = true; + const bodyAssertion: AssertType[], typeof handles> = true; } { const handles = await elementLike.$$('something-strange'); const top = await handles[0].evaluate(element => element.style.top); - const assertion : AssertType = true; + const assertion: AssertType = true; } } @@ -459,38 +475,38 @@ playwright.chromium.launch().then(async browser => { for (const frameLike of frameLikes) { { const handle = await frameLike.waitForSelector('body'); - const bodyAssertion : AssertType, typeof handle> = true; - const canBeNull : AssertType = false; + const bodyAssertion: AssertType, typeof handle> = true; + const canBeNull: AssertType = false; } { - const visibility = Math.random() > .5 ? 'any': 'visible'; + const visibility = Math.random() > .5 ? 'any' : 'visible'; const handle = await frameLike.waitForSelector('body', {visibility}); - const bodyAssertion : AssertType, typeof handle> = true; - const canBeNull : AssertType = false; + const bodyAssertion: AssertType, typeof handle> = true; + const canBeNull: AssertType = false; } { - const visibility = Math.random() > .5 ? 'hidden': 'visible'; - const handle = await frameLike.waitForSelector('body', {visibility}); - const bodyAssertion : AssertType, typeof handle> = true; - const canBeNull : AssertType = true; + const waitFor = Math.random() > .5 ? 'hidden' : 'visible'; + const handle = await frameLike.waitForSelector('body', {waitFor}); + const bodyAssertion: AssertType, typeof handle> = true; + const canBeNull: AssertType = true; } { const handle = await frameLike.waitForSelector('something-strange'); - const elementAssertion : AssertType, typeof handle> = true; - const canBeNull : AssertType = false; + const elementAssertion: AssertType, typeof handle> = true; + const canBeNull: AssertType = false; } { - const visibility = Math.random() > .5 ? 'any': 'visible'; + const visibility = Math.random() > .5 ? 'any' : 'visible'; const handle = await frameLike.waitForSelector('something-strange', {visibility}); - const elementAssertion : AssertType, typeof handle> = true; - const canBeNull : AssertType = false; + const elementAssertion: AssertType, typeof handle> = true; + const canBeNull: AssertType = false; } { - const visibility = Math.random() > .5 ? 'hidden': 'visible'; - const handle = await frameLike.waitForSelector('something-strange', {visibility}); - const elementAssertion : AssertType, typeof handle> = true; - const canBeNull : AssertType = true; + const waitFor = Math.random() > .5 ? 'hidden' : 'visible'; + const handle = await frameLike.waitForSelector('something-strange', {waitFor}); + const elementAssertion: AssertType, typeof handle> = true; + const canBeNull: AssertType = true; } } From 43f3553059cdf2cb84fff0339a958eec4bb10ebb Mon Sep 17 00:00:00 2001 From: Joel Einbinder Date: Tue, 10 Mar 2020 23:37:08 +0100 Subject: [PATCH 04/14] selectors and errors --- index.d.ts | 2 ++ utils/generate_types/test/test.ts | 28 ++++++++++++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/index.d.ts b/index.d.ts index 4e7b270a0749a..daf0fd366f153 100644 --- a/index.d.ts +++ b/index.d.ts @@ -20,3 +20,5 @@ export * from './types/types'; export const webkit: types.BrowserType; export const chromium: types.BrowserType; export const firefox: types.BrowserType; +export const errors: types.BrowserTypeErrors; +export const selectors: types.Selectors; diff --git a/utils/generate_types/test/test.ts b/utils/generate_types/test/test.ts index ab3dc0df1bbe4..874ca94032d9e 100644 --- a/utils/generate_types/test/test.ts +++ b/utils/generate_types/test/test.ts @@ -513,3 +513,31 @@ playwright.chromium.launch().then(async browser => { await browser.close(); })(); + +// top level +(async () => { + playwright.chromium.connect; + playwright.errors.TimeoutError; + + // Must be a function that evaluates to a selector engine instance. + const createTagNameEngine = () => ({ + // Creates a selector that matches given target when queried at the root. + // Can return undefined if unable to create one. + create(root: Element, target: Element) { + return root.querySelector(target.tagName) === target ? target.tagName : undefined; + }, + + // Returns the first element matching given selector in the root's subtree. + query(root: Element, selector: string) { + return root.querySelector(selector); + }, + + // Returns all elements matching given selector in the root's subtree. + queryAll(root: Element, selector: string) { + return Array.from(root.querySelectorAll(selector)); + } + }); + + // Register the engine. Selectors will be prefixed with "tag=". + await playwright.selectors.register('tag', createTagNameEngine); +})(); From 2d3c7d5cc1a78abe248c1ccc94899f62b3976013 Mon Sep 17 00:00:00 2001 From: Joel Einbinder Date: Wed, 11 Mar 2020 00:29:37 +0100 Subject: [PATCH 05/14] device descriptors --- docs/api.md | 2 +- index.d.ts | 1 + utils/generate_types/overrides.d.ts | 8 +++++--- utils/generate_types/test/test.ts | 1 + 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/docs/api.md b/docs/api.md index eced6a3818fdc..794b16943142f 100644 --- a/docs/api.md +++ b/docs/api.md @@ -3246,7 +3246,7 @@ ResourceType will be one of the following: `document`, `stylesheet`, `image`, `m #### response.body() -- returns: > Promise which resolves to a buffer with response body. +- returns: <[Promise]<[Buffer]>> Promise which resolves to a buffer with response body. #### response.finished() - returns: <[Promise]> Waits for this response to finish, returns failure error if request failed. diff --git a/index.d.ts b/index.d.ts index daf0fd366f153..e1813773fb179 100644 --- a/index.d.ts +++ b/index.d.ts @@ -22,3 +22,4 @@ export const chromium: types.BrowserType; export const firefox: types.BrowserType; export const errors: types.BrowserTypeErrors; export const selectors: types.Selectors; +export const devices: types.Devices; diff --git a/utils/generate_types/overrides.d.ts b/utils/generate_types/overrides.d.ts index a9f65826637dd..5aed0719f8a34 100644 --- a/utils/generate_types/overrides.d.ts +++ b/utils/generate_types/overrides.d.ts @@ -19,8 +19,8 @@ import { EventEmitter } from 'events'; /** * Can be converted to JSON */ -interface Serializable {} -interface ConnectionTransport {} +type Serializable = {} +type ConnectionTransport = {} type Boxed = { [Index in keyof Args]: Args[Index] | JSHandle }; type PageFunction = string | ((...args: Args) => R | Promise); @@ -125,4 +125,6 @@ export interface ChromiumSession { method: T, params?: Protocol.CommandParameters[T] ): Promise; -} \ No newline at end of file +} + +export type Devices = {[name: string]: {viewport: BrowserNewContextOptionsViewport, userAgent: string}}; diff --git a/utils/generate_types/test/test.ts b/utils/generate_types/test/test.ts index 874ca94032d9e..a6649f00011c9 100644 --- a/utils/generate_types/test/test.ts +++ b/utils/generate_types/test/test.ts @@ -518,6 +518,7 @@ playwright.chromium.launch().then(async browser => { (async () => { playwright.chromium.connect; playwright.errors.TimeoutError; + const iPhone = playwright.devices['iPhone']; // Must be a function that evaluates to a selector engine instance. const createTagNameEngine = () => ({ From 6c657b8c694b8edab8dc750837ad48184b09a14e Mon Sep 17 00:00:00 2001 From: Joel Einbinder Date: Wed, 11 Mar 2020 00:51:06 +0100 Subject: [PATCH 06/14] fix class comments when there is no toc --- utils/doclint/check_public_api/MDBuilder.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/utils/doclint/check_public_api/MDBuilder.js b/utils/doclint/check_public_api/MDBuilder.js index 5b85fac541b7b..1bc338a036cca 100644 --- a/utils/doclint/check_public_api/MDBuilder.js +++ b/utils/doclint/check_public_api/MDBuilder.js @@ -107,8 +107,10 @@ class MDOutline { */ function parseClass(content) { const members = []; - const commentWalker = document.createTreeWalker(content, NodeFilter.SHOW_COMMENT, { + const commentWalker = document.createTreeWalker(content, NodeFilter.SHOW_COMMENT | NodeFilter.SHOW_ELEMENT, { acceptNode(node) { + if (node instanceof HTMLElement && node.tagName === 'H4') + return NodeFilter.FILTER_ACCEPT; if (!(node instanceof Comment)) return NodeFilter.FILTER_REJECT; if (node.data.trim().startsWith('GEN:toc')) @@ -116,7 +118,7 @@ class MDOutline { return NodeFilter.FILTER_REJECT; } }); - const tocStart = commentWalker.nextNode(); + const commentEnd = commentWalker.nextNode(); const headers = content.querySelectorAll('h4'); const name = content.firstChild.textContent; let extendsName = null; @@ -126,7 +128,7 @@ class MDOutline { commentStart = extendsElement.nextSibling; extendsName = extendsElement.querySelector('a').textContent; } - const comment = parseComment(extractSiblingsIntoFragment(commentStart, tocStart)); + const comment = parseComment(extractSiblingsIntoFragment(commentStart, commentEnd)); for (let i = 0; i < headers.length; i++) { const fragment = extractSiblingsIntoFragment(headers[i], headers[i + 1]); members.push(parseMember(fragment)); @@ -153,7 +155,6 @@ class MDOutline { } /** - * @param {string} name * @param {DocumentFragment} content */ function parseMember(content) { From 039e98dab8b866dde928c1c3e2603c790fae2e04 Mon Sep 17 00:00:00 2001 From: Joel Einbinder Date: Wed, 11 Mar 2020 23:08:17 +0100 Subject: [PATCH 07/14] fixes --- utils/generate_types/overrides.d.ts | 5 +++-- utils/generate_types/test/test.ts | 29 +++++++++++++++++++++-------- 2 files changed, 24 insertions(+), 10 deletions(-) diff --git a/utils/generate_types/overrides.d.ts b/utils/generate_types/overrides.d.ts index 5aed0719f8a34..29098edc10a7f 100644 --- a/utils/generate_types/overrides.d.ts +++ b/utils/generate_types/overrides.d.ts @@ -30,7 +30,7 @@ type Handle = T extends Node ? ElementHandle : JSHandle; type ElementHandleForTag = ElementHandle; type WaitForSelectorOptionsNotHidden = PageWaitForSelectorOptions & { - visibility: 'visible'|'any'; + waitFor: 'visible'|'attached'; } type HTMLOrSVGElement = SVGElement | HTMLElement; @@ -127,4 +127,5 @@ export interface ChromiumSession { ): Promise; } -export type Devices = {[name: string]: {viewport: BrowserNewContextOptionsViewport, userAgent: string}}; +type DeviceDescriptor = {viewport: BrowserNewContextOptionsViewport, userAgent: string}; +export type Devices = {[name: string]: DeviceDescriptor} & DeviceDescriptor[]; diff --git a/utils/generate_types/test/test.ts b/utils/generate_types/test/test.ts index a6649f00011c9..1b5fe89d9d0f0 100644 --- a/utils/generate_types/test/test.ts +++ b/utils/generate_types/test/test.ts @@ -149,7 +149,7 @@ playwright.chromium.launch().then(async browser => { let currentURL: string; page - .waitForSelector('img', { visibility: 'visible' }) + .waitForSelector('img', { waitFor: 'visible' }) .then(() => console.log('First URL with image: ' + currentURL)); for (currentURL of [ 'https://example.com', @@ -418,8 +418,8 @@ playwright.chromium.launch().then(async browser => { const numberAssertion: AssertType = true; }, 5); - const something = Math.random() > .5 ? 'visible' : 'any'; - const handle = await page.waitForSelector('a', {visibility: something}); + const something = Math.random() > .5 ? 'visible' : 'attached'; + const handle = await page.waitForSelector('a', {waitFor: something}); await handle.$eval('span', (element, x, y) => { const spanAssertion: AssertType = true; const numberAssertion: AssertType = true; @@ -479,11 +479,16 @@ playwright.chromium.launch().then(async browser => { const canBeNull: AssertType = false; } { - const visibility = Math.random() > .5 ? 'any' : 'visible'; - const handle = await frameLike.waitForSelector('body', {visibility}); + const waitFor = Math.random() > .5 ? 'attached' : 'visible'; + const handle = await frameLike.waitForSelector('body', {waitFor}); const bodyAssertion: AssertType, typeof handle> = true; const canBeNull: AssertType = false; } + { + const handle = await frameLike.waitForSelector('body', {waitFor: 'hidden'}); + const bodyAssertion: AssertType, typeof handle> = true; + const canBeNull: AssertType = true; + } { const waitFor = Math.random() > .5 ? 'hidden' : 'visible'; const handle = await frameLike.waitForSelector('body', {waitFor}); @@ -497,8 +502,8 @@ playwright.chromium.launch().then(async browser => { const canBeNull: AssertType = false; } { - const visibility = Math.random() > .5 ? 'any' : 'visible'; - const handle = await frameLike.waitForSelector('something-strange', {visibility}); + const waitFor = Math.random() > .5 ? 'attached' : 'visible'; + const handle = await frameLike.waitForSelector('something-strange', {waitFor}); const elementAssertion: AssertType, typeof handle> = true; const canBeNull: AssertType = false; } @@ -518,7 +523,15 @@ playwright.chromium.launch().then(async browser => { (async () => { playwright.chromium.connect; playwright.errors.TimeoutError; - const iPhone = playwright.devices['iPhone']; + { + const iPhone = playwright.devices['iPhone']; + const assertion: AssertType = true; + const numberAssertion: AssertType = true; + } + { + const agents = playwright.devices.map(x => x.userAgent); + const assertion: AssertType = true; + } // Must be a function that evaluates to a selector engine instance. const createTagNameEngine = () => ({ From a3763b4f5188f05f03936fc9b199f3fee2179a87 Mon Sep 17 00:00:00 2001 From: Joel Einbinder Date: Thu, 19 Mar 2020 22:05:38 +0100 Subject: [PATCH 08/14] rebase, timeouterrors --- index.d.ts | 3 --- packages/playwright/index.js | 1 - utils/generate_types/index.js | 2 +- utils/generate_types/overrides.d.ts | 22 +++++++++++++--------- utils/generate_types/test/test.ts | 10 +++++----- 5 files changed, 19 insertions(+), 19 deletions(-) diff --git a/index.d.ts b/index.d.ts index e1813773fb179..4e7b270a0749a 100644 --- a/index.d.ts +++ b/index.d.ts @@ -20,6 +20,3 @@ export * from './types/types'; export const webkit: types.BrowserType; export const chromium: types.BrowserType; export const firefox: types.BrowserType; -export const errors: types.BrowserTypeErrors; -export const selectors: types.Selectors; -export const devices: types.Devices; diff --git a/packages/playwright/index.js b/packages/playwright/index.js index db84bb9d034cf..5591bbe62ce4c 100644 --- a/packages/playwright/index.js +++ b/packages/playwright/index.js @@ -30,4 +30,3 @@ try { throw new Error('ERROR: Playwright did not download browsers'); } - diff --git a/utils/generate_types/index.js b/utils/generate_types/index.js index 79e64acf985de..098ba4bf6199f 100644 --- a/utils/generate_types/index.js +++ b/utils/generate_types/index.js @@ -102,7 +102,7 @@ function classToString(classDesc) { } if (classDesc.templates.length) console.error(`expected an override for "${classDesc.name}" becasue it is templated`); - parts.push(`export interface ${classDesc.name} ${classDesc.extends ? `extends ${classDesc.extends} ` : ''}{`); + parts.push(`export class ${classDesc.name} ${classDesc.extends ? `extends ${classDesc.extends} ` : ''}{`); parts.push(classBody(classDesc)); parts.push('}\n'); return parts.join('\n'); diff --git a/utils/generate_types/overrides.d.ts b/utils/generate_types/overrides.d.ts index 29098edc10a7f..1d45ca6316450 100644 --- a/utils/generate_types/overrides.d.ts +++ b/utils/generate_types/overrides.d.ts @@ -36,7 +36,7 @@ type WaitForSelectorOptionsNotHidden = PageWaitForSelectorOptions & { type HTMLOrSVGElement = SVGElement | HTMLElement; type HTMLOrSVGElementHandle = ElementHandle; -export interface Page { +export class Page { evaluate(pageFunction: PageFunction, ...args: Boxed): Promise; evaluateHandle(pageFunction: PageFunction, ...args: Boxed): Promise>; @@ -58,7 +58,7 @@ export interface Page { waitForSelector(selector: string, options: PageWaitForSelectorOptions): Promise; } -export interface Frame { +export class Frame { evaluate(pageFunction: PageFunction, ...args: Boxed): Promise; evaluateHandle(pageFunction: PageFunction, ...args: Boxed): Promise>; @@ -80,19 +80,19 @@ export interface Frame { waitForSelector(selector: string, options: PageWaitForSelectorOptions): Promise; } -export interface Worker { +export class Worker { evaluate(pageFunction: PageFunction, ...args: Boxed): Promise; evaluateHandle(pageFunction: PageFunction, ...args: Boxed): Promise>; } -export interface JSHandle { +export class JSHandle { jsonValue(): Promise; evaluate(pageFunction: PageFunctionOn, ...args: Boxed): Promise; evaluateHandle(pageFunction: PageFunctionOn, ...args: Boxed): Promise>; asElement(): T extends Node ? ElementHandle : null; } -export interface ElementHandle extends JSHandle { +export class ElementHandle extends JSHandle { $(selector: K): Promise | null>; $(selector: string): Promise; @@ -106,16 +106,16 @@ export interface ElementHandle extends JSHandle { $$eval(selector: string, pageFunction: PageFunctionOn, ...args: Boxed): Promise; } -export interface BrowserType { +export class BrowserType { } -export interface ChromiumBrowser extends Browser { +export class ChromiumBrowser extends Browser { contexts(): Array; newContext(options?: BrowserNewContextOptions): Promise; } -export interface ChromiumSession { +export class CDPSession { on: (event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this; addListener: (event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this; off: (event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this; @@ -128,4 +128,8 @@ export interface ChromiumSession { } type DeviceDescriptor = {viewport: BrowserNewContextOptionsViewport, userAgent: string}; -export type Devices = {[name: string]: DeviceDescriptor} & DeviceDescriptor[]; +export const errors: { + TimeoutError: typeof TimeoutError +}; +export const selectors: Selectors; +export const devices: {[name: string]: DeviceDescriptor} & DeviceDescriptor[]; diff --git a/utils/generate_types/test/test.ts b/utils/generate_types/test/test.ts index 1b5fe89d9d0f0..54555d4184c4d 100644 --- a/utils/generate_types/test/test.ts +++ b/utils/generate_types/test/test.ts @@ -123,13 +123,13 @@ playwright.chromium.launch().then(async browser => { await page.emulateMedia({media: 'screen'}); await page.pdf({ path: 'page.pdf' }); - await page.route('**/*', interceptedRequest => { + await page.route('**/*', (route, interceptedRequest) => { if ( interceptedRequest.url().endsWith('.png') || interceptedRequest.url().endsWith('.jpg') ) - interceptedRequest.abort(); - else interceptedRequest.continue(); + route.abort(); + else route.continue(); }); await page.route(str => { @@ -169,7 +169,7 @@ playwright.chromium.launch().then(async browser => { page.keyboard.up('Shift'); page.keyboard.press('Backspace'); - page.keyboard.sendCharacters('嗨'); + page.keyboard.insertText('嗨'); await browser.startTracing(page, { path: 'trace.json'}); await page.goto('https://www.google.com'); await browser.stopTracing(); @@ -349,7 +349,7 @@ playwright.chromium.launch().then(async browser => { (async () => { const browser = await playwright.chromium.launch(); const context = await browser.newContext(); - const session = await context.createSession(await context.newPage()); + const session = await context.newCDPSession(await context.newPage()); session.on('Runtime.executionContextCreated', payload => { From a70f9075ff3c8ca6373bad34a423e117ac87fd74 Mon Sep 17 00:00:00 2001 From: Joel Einbinder Date: Thu, 19 Mar 2020 22:48:52 +0100 Subject: [PATCH 09/14] dont emit multiple payload interfaces, use arg name instead of options --- utils/generate_types/index.js | 47 +++++++++++++++++++++++++++++------ 1 file changed, 39 insertions(+), 8 deletions(-) diff --git a/utils/generate_types/index.js b/utils/generate_types/index.js index 098ba4bf6199f..f652ee4833887 100644 --- a/utils/generate_types/index.js +++ b/utils/generate_types/index.js @@ -117,21 +117,52 @@ function argNameForType(type) { return type[0].toLowerCase() + type.slice(1); } +/** + * @param {Documentation.Class} classDesc + */ +function hasUniqueEvents(classDesc) { + if (!classDesc.events.size) + return false; + const parent = parentClass(classDesc); + if (!parent) + return true; + return Array.from(classDesc.events.keys()).some(eventName => !parent.events.has(eventName)); +} + +/** + * @param {Documentation.Class} classDesc + */ +function createEventDescriptions(classDesc) { + if (!hasUniqueEvents(classDesc)) + return []; + const descriptions = []; + for (const [eventName, value] of classDesc.events) { + const type = typeToString(value && value.type, classDesc.name, eventName, 'payload'); + const argName = argNameForType(type); + const params = argName ? `${argName} : ${type}` : ''; + descriptions.push({ + params, + eventName, + comment: value.comment + }); + } + return descriptions; +} + /** * @param {Documentation.Class} classDesc */ function classBody(classDesc) { const parts = []; + const eventDescriptions = createEventDescriptions(classDesc); for (const method of ['on', 'once', 'addListener']) { - for (const [eventName, value] of classDesc.events) { - if (value.comment) - parts.push(writeComment(value.comment, ' ')); - const type = typeToString(value && value.type, classDesc.name, eventName, 'payload'); - const argName = argNameForType(type); - const params = argName ? `${argName} : ${type}` : ''; - parts.push(` ${method}(event: '${eventName}', listener: (${params}) => void): this;\n`); + for (const {eventName, params, comment} of eventDescriptions) { + if (comment) + parts.push(writeComment(comment, ' ')); + parts.push(` ${method}(event: '${eventName}', listener: (${params}) => void): this;\n`); } } + const members = classDesc.membersArray.filter(member => member.kind !== 'event'); parts.push(members.map(member => { if (member.kind === 'event') @@ -312,7 +343,7 @@ function matchingBracket(str, open, close) { function argsFromMember(member, ...namespace) { if (member.kind === 'property') return ''; - return '(' + member.argsArray.map(arg => `${nameForProperty(arg)}: ${typeToString(arg.type, ...namespace, member.name, 'options')}`).join(', ') + ')'; + return '(' + member.argsArray.map(arg => `${nameForProperty(arg)}: ${typeToString(arg.type, ...namespace, member.name, arg.name)}`).join(', ') + ')'; } /** * @param {Documentation.Member} member From fd02ed40aaf832523d1b104342f87f3c1ed8b7c8 Mon Sep 17 00:00:00 2001 From: Joel Einbinder Date: Thu, 19 Mar 2020 23:01:35 +0100 Subject: [PATCH 10/14] proper types for packages --- packages/playwright-chromium/index.d.ts | 20 ++++++++++++++++++++ packages/playwright-firefox/index.d.ts | 20 ++++++++++++++++++++ packages/playwright-webkit/index.d.ts | 20 ++++++++++++++++++++ packages/playwright/index.d.ts | 23 ++++++++++++++++++++++- 4 files changed, 82 insertions(+), 1 deletion(-) create mode 100644 packages/playwright-chromium/index.d.ts create mode 100644 packages/playwright-firefox/index.d.ts create mode 100644 packages/playwright-webkit/index.d.ts diff --git a/packages/playwright-chromium/index.d.ts b/packages/playwright-chromium/index.d.ts new file mode 100644 index 0000000000000..872acf1212ed6 --- /dev/null +++ b/packages/playwright-chromium/index.d.ts @@ -0,0 +1,20 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * 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 * as types from 'playwright-core/types/types'; + +export * from 'playwright-core/types/types'; +export const chromium: types.BrowserType; diff --git a/packages/playwright-firefox/index.d.ts b/packages/playwright-firefox/index.d.ts new file mode 100644 index 0000000000000..894df7d615a65 --- /dev/null +++ b/packages/playwright-firefox/index.d.ts @@ -0,0 +1,20 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * 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 * as types from 'playwright-core/types/types'; + +export * from 'playwright-core/types/types'; +export const firefox: types.BrowserType; diff --git a/packages/playwright-webkit/index.d.ts b/packages/playwright-webkit/index.d.ts new file mode 100644 index 0000000000000..45cbed44d9ec9 --- /dev/null +++ b/packages/playwright-webkit/index.d.ts @@ -0,0 +1,20 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * 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 * as types from 'playwright-core/types/types'; + +export * from 'playwright-core/types/types'; +export const webkit: types.BrowserType; diff --git a/packages/playwright/index.d.ts b/packages/playwright/index.d.ts index 2092f5a8e13b3..a5bc85a60f126 100644 --- a/packages/playwright/index.d.ts +++ b/packages/playwright/index.d.ts @@ -1 +1,22 @@ -export * from "playwright-core"; +/** + * Copyright (c) Microsoft Corporation. + * + * 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 * as types from 'playwright-core/types/types'; + +export * from 'playwright-core/types/types'; +export const webkit: types.BrowserType; +export const chromium: types.BrowserType; +export const firefox: types.BrowserType; From 98b718a5f116b29a8f0c12989eee5a4bf6571e8d Mon Sep 17 00:00:00 2001 From: Joel Einbinder Date: Fri, 20 Mar 2020 00:16:19 +0100 Subject: [PATCH 11/14] go back to interfaces, error namespace --- utils/generate_types/index.js | 2 +- utils/generate_types/overrides.d.ts | 26 +++++++++++++++----------- 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/utils/generate_types/index.js b/utils/generate_types/index.js index f652ee4833887..6962c465fdb64 100644 --- a/utils/generate_types/index.js +++ b/utils/generate_types/index.js @@ -102,7 +102,7 @@ function classToString(classDesc) { } if (classDesc.templates.length) console.error(`expected an override for "${classDesc.name}" becasue it is templated`); - parts.push(`export class ${classDesc.name} ${classDesc.extends ? `extends ${classDesc.extends} ` : ''}{`); + parts.push(`export interface ${classDesc.name} ${classDesc.extends ? `extends ${classDesc.extends} ` : ''}{`); parts.push(classBody(classDesc)); parts.push('}\n'); return parts.join('\n'); diff --git a/utils/generate_types/overrides.d.ts b/utils/generate_types/overrides.d.ts index 1d45ca6316450..82009dfa9e121 100644 --- a/utils/generate_types/overrides.d.ts +++ b/utils/generate_types/overrides.d.ts @@ -36,7 +36,7 @@ type WaitForSelectorOptionsNotHidden = PageWaitForSelectorOptions & { type HTMLOrSVGElement = SVGElement | HTMLElement; type HTMLOrSVGElementHandle = ElementHandle; -export class Page { +export interface Page { evaluate(pageFunction: PageFunction, ...args: Boxed): Promise; evaluateHandle(pageFunction: PageFunction, ...args: Boxed): Promise>; @@ -58,7 +58,7 @@ export class Page { waitForSelector(selector: string, options: PageWaitForSelectorOptions): Promise; } -export class Frame { +export interface Frame { evaluate(pageFunction: PageFunction, ...args: Boxed): Promise; evaluateHandle(pageFunction: PageFunction, ...args: Boxed): Promise>; @@ -80,19 +80,19 @@ export class Frame { waitForSelector(selector: string, options: PageWaitForSelectorOptions): Promise; } -export class Worker { +export interface Worker { evaluate(pageFunction: PageFunction, ...args: Boxed): Promise; evaluateHandle(pageFunction: PageFunction, ...args: Boxed): Promise>; } -export class JSHandle { +export interface JSHandle { jsonValue(): Promise; evaluate(pageFunction: PageFunctionOn, ...args: Boxed): Promise; evaluateHandle(pageFunction: PageFunctionOn, ...args: Boxed): Promise>; asElement(): T extends Node ? ElementHandle : null; } -export class ElementHandle extends JSHandle { +export interface ElementHandle extends JSHandle { $(selector: K): Promise | null>; $(selector: string): Promise; @@ -106,16 +106,16 @@ export class ElementHandle extends JSHandle { $$eval(selector: string, pageFunction: PageFunctionOn, ...args: Boxed): Promise; } -export class BrowserType { +export interface BrowserType { } -export class ChromiumBrowser extends Browser { +export interface ChromiumBrowser extends Browser { contexts(): Array; newContext(options?: BrowserNewContextOptions): Promise; } -export class CDPSession { +export interface CDPSession { on: (event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this; addListener: (event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this; off: (event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this; @@ -128,8 +128,12 @@ export class CDPSession { } type DeviceDescriptor = {viewport: BrowserNewContextOptionsViewport, userAgent: string}; -export const errors: { - TimeoutError: typeof TimeoutError -}; + +export namespace errors { + +class TimeoutError extends Error {} + +} + export const selectors: Selectors; export const devices: {[name: string]: DeviceDescriptor} & DeviceDescriptor[]; From 5b16a76f0effeadb355f5a502482d5516a5c45e5 Mon Sep 17 00:00:00 2001 From: Joel Einbinder Date: Fri, 20 Mar 2020 00:57:50 +0100 Subject: [PATCH 12/14] generate types on install, clean types on clean --- install-from-github.js | 9 ++++++++- package.json | 4 ++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/install-from-github.js b/install-from-github.js index f51d19f98b66c..6220cb5195a59 100644 --- a/install-from-github.js +++ b/install-from-github.js @@ -17,9 +17,11 @@ // This file is only run when someone installs via the github repo +const {execSync} = require('child_process'); + try { console.log('Building playwright...'); - require('child_process').execSync('npm run build', { + execSync('npm run build', { stdio: 'ignore' }); } catch (e) { @@ -90,3 +92,8 @@ const DOWNLOAD_PATHS = { } })(); +try { + console.log('Generating types...'); + execSync('npm run generate-types'); +} catch (e) { +} diff --git a/package.json b/package.json index 9abcb7f88acbf..a8fd7056eb62b 100644 --- a/package.json +++ b/package.json @@ -31,9 +31,9 @@ "test-infra": "node utils/doclint/check_public_api/test/test.js && node utils/doclint/preprocessor/test.js && node utils/testrunner/test/test.js", "lint": "npm run eslint && npm run tsc && npm run doc && npm run test-infra", "debug-test": "node --inspect-brk test/test.js", - "clean": "rimraf lib", + "clean": "rimraf lib && rimraf types", "prepare": "node install-from-github.js", - "build": "node utils/runWebpack.js --mode='development' && tsc -p . && npm run generate-types", + "build": "node utils/runWebpack.js --mode='development' && tsc -p .", "watch": "node utils/runWebpack.js --mode='development' --watch --silent | tsc -w -p .", "version": "node utils/sync_package_versions.js && npm run doc", "generate-types": "node utils/generate_types/" From 81b1dc3253eb0a6571f369592ab5d8c8125963d9 Mon Sep 17 00:00:00 2001 From: Joel Einbinder Date: Fri, 20 Mar 2020 01:00:43 +0100 Subject: [PATCH 13/14] spacing --- utils/generate_types/overrides.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utils/generate_types/overrides.d.ts b/utils/generate_types/overrides.d.ts index 82009dfa9e121..23c76710fee0e 100644 --- a/utils/generate_types/overrides.d.ts +++ b/utils/generate_types/overrides.d.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import {Protocol} from './protocol'; +import { Protocol } from './protocol'; import { ChildProcess } from 'child_process'; import { EventEmitter } from 'events'; /** From 72dbef90df8ecb1ff672d585e07dc0f9da0b51fa Mon Sep 17 00:00:00 2001 From: Joel Einbinder Date: Fri, 20 Mar 2020 01:22:15 +0100 Subject: [PATCH 14/14] fix install script and test types --- install-from-github.js | 14 +++++++------- test/types.d.ts | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/install-from-github.js b/install-from-github.js index 6220cb5195a59..fecd657f20ea8 100644 --- a/install-from-github.js +++ b/install-from-github.js @@ -87,13 +87,13 @@ const DOWNLOAD_PATHS = { directories.add(path.join(__dirname, '.local-webkit')); await Promise.all([...directories].map(directory => rmAsync(directory))); + try { + console.log('Generating types...'); + execSync('npm run generate-types'); + } catch (e) { + } + async function readdirAsync(dirpath) { return fs.promises.readdir(dirpath).then(dirs => dirs.map(dir => path.join(dirpath, dir))); } -})(); - -try { - console.log('Generating types...'); - execSync('npm run generate-types'); -} catch (e) { -} +})(); \ No newline at end of file diff --git a/test/types.d.ts b/test/types.d.ts index 213c2ccaaab3d..65aedd5586a60 100644 --- a/test/types.d.ts +++ b/test/types.d.ts @@ -48,7 +48,7 @@ interface TestSetup { MAC: boolean; LINUX: boolean; WIN: boolean; - playwright: import('../src/server/browserType').BrowserType; + playwright: import('../src/server/browserType').BrowserType; selectors: import('../src/selectors').Selectors; expect(value: T): Expect; defaultBrowserOptions: import('../src/server/browserType').LaunchOptions;