diff --git a/.eslintignore b/.eslintignore index 11cbd52c0c..85e425de62 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 a0d4e623c0..21ac19b12a 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 08aa7c2c78..94d13a0e93 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/docs/api.md b/docs/api.md index 6da215cdef..accabd15ba 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 d6137f14fc..4e7b270a07 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/install-from-github.js b/install-from-github.js index f51d19f98b..fecd657f20 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) { @@ -85,8 +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))); } -})(); - +})(); \ No newline at end of file diff --git a/package.json b/package.json index 951d194d27..a8fd7056eb 100644 --- a/package.json +++ b/package.json @@ -31,11 +31,12 @@ "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 .", "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/packages/playwright-chromium/index.d.ts b/packages/playwright-chromium/index.d.ts new file mode 100644 index 0000000000..872acf1212 --- /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 0000000000..894df7d615 --- /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 0000000000..45cbed44d9 --- /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 2092f5a8e1..a5bc85a60f 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; diff --git a/packages/playwright/index.js b/packages/playwright/index.js index db84bb9d03..5591bbe62c 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/src/server/browserType.ts b/src/server/browserType.ts index eafb4a0b7f..7b76cf4dcc 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 8e38d429f7..cc3c93dbfb 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 b6b55cb8ae..d61cd4fad0 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 13f7a337f0..3b524b63f1 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/test/types.d.ts b/test/types.d.ts index 213c2ccaaa..65aedd5586 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; diff --git a/tsconfig.json b/tsconfig.json index 43224d7e28..37a8b36a86 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 f9a6c66dae..6a0d77f7a4 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 2030923828..ca66239ef1 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 f736ccb910..1bc338a036 100644 --- a/utils/doclint/check_public_api/MDBuilder.js +++ b/utils/doclint/check_public_api/MDBuilder.js @@ -107,6 +107,18 @@ class MDOutline { */ function parseClass(content) { const members = []; + 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')) + return NodeFilter.FILTER_ACCEPT; + return NodeFilter.FILTER_REJECT; + } + }); + const commentEnd = commentWalker.nextNode(); const headers = content.querySelectorAll('h4'); const name = content.firstChild.textContent; let extendsName = null; @@ -116,7 +128,7 @@ class MDOutline { commentStart = extendsElement.nextSibling; extendsName = extendsElement.querySelector('a').textContent; } - const comment = parseComment(extractSiblingsIntoFragment(commentStart, headers[0])); + 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)); @@ -143,7 +155,6 @@ class MDOutline { } /** - * @param {string} name * @param {DocumentFragment} content */ function parseMember(content) { @@ -178,7 +189,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 new file mode 100644 index 0000000000..6962c465fd --- /dev/null +++ b/utils/generate_types/index.js @@ -0,0 +1,392 @@ +/** + * 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'); +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 {Documentation} */ +let documentation; +(async function() { + 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')); + 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, ' ').trimLeft(); + }, (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)} +${overrides} + +${classes.map(classDesc => classToString(classDesc)).join('\n')} +${objectDefinitionsToString()} +`; + fs.writeFileSync(path.join(typesDir, '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 {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 {string} type + */ +function argNameForType(type) { + if (type === 'void') + return null; + 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, 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') + 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 {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 {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 {Documentation.Type} type + */ +function typeToString(type, ...namespace) { + if (!type) + return 'void'; + let typeString = stringifyType(parseType(type.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 + }; +} + +/** + * @return {string} + */ +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(', ')}) => ${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 {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, arg.name)}`).join(', ') + ')'; +} +/** + * @param {Documentation.Member} member + * @param {string} indent + */ +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 {Documentation} mdDoc + * @param {Documentation} jsDoc + * @return {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 {Documentation.Class} mdClass + * @param {Documentation.Class} jsClass + * @return {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 0000000000..23c76710fe --- /dev/null +++ b/utils/generate_types/overrides.d.ts @@ -0,0 +1,139 @@ +/** + * 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 + */ +type Serializable = {} +type ConnectionTransport = {} + +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 & { + waitFor: 'visible'|'attached'; +} + +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; + + $$(selector: K): Promise[]>; + $$(selector: string): Promise; + + $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: 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 { + evaluate(pageFunction: PageFunction, ...args: Boxed): Promise; + evaluateHandle(pageFunction: PageFunction, ...args: Boxed): Promise>; + + $(selector: K): 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: K, 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 { + 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; + + $$(selector: K): Promise[]>; + $$(selector: string): Promise; + + $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 BrowserType { + +} + +export interface ChromiumBrowser extends Browser { + contexts(): Array; + newContext(options?: BrowserNewContextOptions): Promise; +} + +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; + 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; +} + +type DeviceDescriptor = {viewport: BrowserNewContextOptionsViewport, userAgent: string}; + +export namespace errors { + +class TimeoutError extends Error {} + +} + +export const selectors: Selectors; +export const devices: {[name: string]: DeviceDescriptor} & DeviceDescriptor[]; diff --git a/utils/generate_types/parseOverrides.js b/utils/generate_types/parseOverrides.js new file mode 100644 index 0000000000..b4da73352e --- /dev/null +++ b/utils/generate_types/parseOverrides.js @@ -0,0 +1,92 @@ +/** + * 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'); +/** + * @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 0000000000..54555d4184 --- /dev/null +++ b/utils/generate_types/test/test.ts @@ -0,0 +1,557 @@ +/** + * 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; + +// 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')); + page.$eval('.foo', e => e.style); + } + + 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('**/*', (route, interceptedRequest) => { + if ( + interceptedRequest.url().endsWith('.png') || + interceptedRequest.url().endsWith('.jpg') + ) + route.abort(); + else route.continue(); + }); + + 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 }); + await watchDog; + + let currentURL: string; + page + .waitForSelector('img', { waitFor: '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.insertText('嗨'); + 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'); + + const 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(); +})(); + +// protocol +(async () => { + const browser = await playwright.chromium.launch(); + const context = await browser.newContext(); + const session = await context.newCDPSession(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(); + + + 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<(HTMLElement|SVGElement)[], typeof elements> = 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<(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; + }, 5); + + 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; + 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<(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; + }, 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 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}); + 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 waitFor = Math.random() > .5 ? 'attached' : 'visible'; + const handle = await frameLike.waitForSelector('something-strange', {waitFor}); + const elementAssertion: AssertType, typeof handle> = true; + const canBeNull: AssertType = false; + } + { + 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; + } + } + + + await browser.close(); +})(); + +// top level +(async () => { + playwright.chromium.connect; + playwright.errors.TimeoutError; + { + 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 = () => ({ + // 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); +})(); diff --git a/utils/generate_types/test/tsconfig.json b/utils/generate_types/test/tsconfig.json new file mode 100644 index 0000000000..f042a91379 --- /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