chore(types): generate our own d.ts file from api.md (#3744)

Generate `//index.d.ts` file with precise typescript definitions for all of the
Puppeteer API.
This commit is contained in:
Joel Einbinder 2019-01-28 18:12:45 -05:00 committed by Andrey Lushnikov
parent 63d9ac4df8
commit f2c968fdb8
13 changed files with 706 additions and 75 deletions

View File

@ -11,7 +11,8 @@ install:
- if "%nodejs_version%" == "8.11.3" (
npm run lint &&
npm run coverage &&
npm run test-doclint
npm run test-doclint &&
npm run test-types
) else (
npm run unit-node6
)

View File

@ -20,6 +20,7 @@ task:
lint_script: npm run lint
coverage_script: npm run coverage
test_doclint_script: npm run test-doclint
test_types_script: npm run test-types
task:
osx_instance:
@ -34,3 +35,4 @@ task:
lint_script: npm run lint
coverage_script: npm run coverage
test_doclint_script: npm run test-doclint
test_types_script: npm run test-types

1
.gitignore vendored
View File

@ -12,3 +12,4 @@ yarn.lock
/node6
/lib/protocol.d.ts
/utils/browser/puppeteer-web.js
/index.d.ts

View File

@ -19,6 +19,7 @@ script:
- 'if [ "$NODE8" = "true" ]; then npm run lint; fi'
- 'if [ "$NODE8" = "true" ]; then npm run coverage; fi'
- 'if [ "$NODE8" = "true" ]; then npm run test-doclint; fi'
- 'if [ "$NODE8" = "true" ]; then npm run test-types; fi'
- 'if [ "$NODE8" = "true" ]; then npm run bundle; fi'
- 'if [ "$NODE8" = "true" ]; then npm run unit-bundle; fi'
- 'if [ "$NODE6" = "true" ]; then npm run unit-node6; fi'

View File

@ -565,7 +565,7 @@ The method initiates a GET request to download the revision from the host.
- returns: <[Promise]<[Array]<[string]>>> A list of all revisions available locally on disk.
#### browserFetcher.platform()
- returns: <[string]> Returns one of `mac`, `linux`, `win32` or `win64`.
- returns: <[string]> One of `mac`, `linux`, `win32` or `win64`.
#### browserFetcher.remove(revision)
- `revision` <[string]> a revision to remove. The method will throw if the revision has not been downloaded.
@ -582,7 +582,7 @@ The method initiates a GET request to download the revision from the host.
### class: Browser
* extends: [`EventEmitter`](https://nodejs.org/api/events.html#events_class_eventemitter)
* extends: [EventEmitter](https://nodejs.org/api/events.html#events_class_eventemitter)
A Browser is created when Puppeteer connects to a Chromium instance, either through [`puppeteer.launch`](#puppeteerlaunchoptions) or [`puppeteer.connect`](#puppeteerconnectoptions).
@ -734,7 +734,7 @@ You can find the `webSocketDebuggerUrl` from `http://${host}:${port}/json/versio
### class: BrowserContext
* extends: [`EventEmitter`](https://nodejs.org/api/events.html#events_class_eventemitter)
* extends: [EventEmitter](https://nodejs.org/api/events.html#events_class_eventemitter)
BrowserContexts provide a way to operate multiple independent browser sessions. When a browser is launched, it has
a single BrowserContext used by default. The method `browser.newPage()` creates a page in the default browser context.
@ -864,7 +864,7 @@ const newWindowTarget = await browserContext.waitForTarget(target => target.url(
### class: Page
* extends: [`EventEmitter`](https://nodejs.org/api/events.html#events_class_eventemitter)
* extends: [EventEmitter](https://nodejs.org/api/events.html#events_class_eventemitter)
Page provides methods to interact with a single tab or [extension background page](https://developer.chrome.com/extensions/background_pages) in Chromium. One [Browser] instance might have multiple [Page] instances.
@ -1035,7 +1035,7 @@ Shortcut for [page.mainFrame().$$(selector)](#frameselector-1).
#### page.$$eval(selector, pageFunction[, ...args])
- `selector` <[string]> A [selector] to query page for
- `pageFunction` <[function]> Function to be evaluated in browser context
- `pageFunction` <[function]\([Array]<[Element]>\)> Function to be evaluated in browser context
- `...args` <...[Serializable]|[JSHandle]> Arguments to pass to `pageFunction`
- returns: <[Promise]<[Serializable]>> Promise which resolves to the return value of `pageFunction`
@ -1050,7 +1050,7 @@ const divsCounts = await page.$$eval('div', divs => divs.length);
#### page.$eval(selector, pageFunction[, ...args])
- `selector` <[string]> A [selector] to query page for
- `pageFunction` <[function]> Function to be evaluated in browser context
- `pageFunction` <[function]\([Element]\)> Function to be evaluated in browser context
- `...args` <...[Serializable]|[JSHandle]> Arguments to pass to `pageFunction`
- returns: <[Promise]<[Serializable]>> Promise which resolves to the return value of `pageFunction`
@ -1464,7 +1464,7 @@ Indicates that the page has been closed.
- returns: <[Keyboard]>
#### page.mainFrame()
- returns: <[Frame]> returns page's main frame.
- returns: <[Frame]> The page's main frame.
Page is guaranteed to have a main frame which persists during navigations.
@ -1609,7 +1609,7 @@ Shortcut for [page.mainFrame().executionContext().queryObjects(prototypeHandle)]
#### page.select(selector, ...values)
- `selector` <[string]> A [selector] to query page for
- `...values` <...[string]> Values of options to select. If the `<select>` has the `multiple` attribute, all values are considered, otherwise only the first one is taken into account.
- returns: <[Promise]<[Array]<[string]>>> Returns an array of option values that have been successfully selected.
- returns: <[Promise]<[Array]<[string]>>> An array of option values that have been successfully selected.
Triggers a `change` and `input` event once all the provided options have been selected.
If there's no `<select>` element matching `selector`, the method throws an error.
@ -1767,7 +1767,7 @@ Shortcut for [page.mainFrame().tap(selector)](#frametapselector).
- returns: <[Target]> a target this page was created from.
#### page.title()
- returns: <[Promise]<[string]>> Returns page's title.
- returns: <[Promise]<[string]>> The page's title.
Shortcut for [page.mainFrame().title()](#frametitle).
@ -2042,7 +2042,7 @@ Most of the accessibility tree gets filtered out when converting from Blink AX T
#### accessibility.snapshot([options])
- `options` <[Object]>
- `interestingOnly` <[boolean]> Prune uninteresting nodes from the tree. Defaults to `true`.
- returns: <[Promise]<[Object]>> Returns an [AXNode] object with the following properties:
- returns: <[Promise]<[Object]>> An [AXNode] object with the following properties:
- `role` <[string]> The [role](https://www.w3.org/TR/wai-aria/#usage_intro).
- `name` <[string]> A human readable name for the node.
- `value` <[string]|[number]> The current value of the node.
@ -2382,7 +2382,7 @@ The method runs `document.querySelectorAll` within the frame. If no elements mat
#### frame.$$eval(selector, pageFunction[, ...args])
- `selector` <[string]> A [selector] to query frame for
- `pageFunction` <[function]> Function to be evaluated in browser context
- `pageFunction` <[function]\([Array]<[Element]>\)> Function to be evaluated in browser context
- `...args` <...[Serializable]|[JSHandle]> Arguments to pass to `pageFunction`
- returns: <[Promise]<[Serializable]>> Promise which resolves to the return value of `pageFunction`
@ -2397,7 +2397,7 @@ const divsCounts = await frame.$$eval('div', divs => divs.length);
#### frame.$eval(selector, pageFunction[, ...args])
- `selector` <[string]> A [selector] to query frame for
- `pageFunction` <[function]> Function to be evaluated in browser context
- `pageFunction` <[function]\([Element]\)> Function to be evaluated in browser context
- `...args` <...[Serializable]|[JSHandle]> Arguments to pass to `pageFunction`
- returns: <[Promise]<[Serializable]>> Promise which resolves to the return value of `pageFunction`
@ -2580,12 +2580,12 @@ If the name is empty, returns the id attribute instead.
> **NOTE** This value is calculated once when the frame is created, and will not update if the attribute is changed later.
#### frame.parentFrame()
- returns: <?[Frame]> Returns parent frame, if any. Detached frames and main frames return `null`.
- returns: <?[Frame]> Parent frame, if any. Detached frames and main frames return `null`.
#### frame.select(selector, ...values)
- `selector` <[string]> A [selector] to query frame for
- `...values` <...[string]> Values of options to select. If the `<select>` has the `multiple` attribute, all values are considered, otherwise only the first one is taken into account.
- returns: <[Promise]<[Array]<[string]>>> Returns an array of option values that have been successfully selected.
- returns: <[Promise]<[Array]<[string]>>> An array of option values that have been successfully selected.
Triggers a `change` and `input` event once all the provided options have been selected.
If there's no `<select>` element matching `selector`, the method throws an error.
@ -2614,7 +2614,7 @@ This method fetches an element with `selector`, scrolls it into view if needed,
If there's no element matching `selector`, the method throws an error.
#### frame.title()
- returns: <[Promise]<[string]>> Returns page's title.
- returns: <[Promise]<[string]>> The page's title.
#### frame.type(selector, text[, options])
- `selector` <[string]> A [selector] of an element to type into. If there are multiple elements satisfying the selector, the first will be used.
@ -2925,8 +2925,7 @@ function, it **will not be called**.
> **NOTE** The method will return an empty JSON object if the referenced object is not stringifiable. It will throw an error if the object has circular references.
### class: ElementHandle
> **NOTE** Class [ElementHandle] extends [JSHandle].
* extends: [JSHandle]
ElementHandle represents an in-page DOM element. ElementHandles can be created with the [page.$](#pageselector) method.
@ -2960,7 +2959,7 @@ The method runs `element.querySelectorAll` within the page. If no elements match
#### elementHandle.$$eval(selector, pageFunction[, ...args])
- `selector` <[string]> A [selector] to query page for
- `pageFunction` <[function]> Function to be evaluated in browser context
- `pageFunction` <[function]\([Array]<[Element]>\)> Function to be evaluated in browser context
- `...args` <...[Serializable]|[JSHandle]> Arguments to pass to `pageFunction`
- returns: <[Promise]<[Serializable]>> Promise which resolves to the return value of `pageFunction`
@ -2982,7 +2981,7 @@ expect(await feedHandle.$$eval('.tweet', nodes => nodes.map(n => n.innerText))).
#### elementHandle.$eval(selector, pageFunction[, ...args])
- `selector` <[string]> A [selector] to query page for
- `pageFunction` <[function]> Function to be evaluated in browser context
- `pageFunction` <[function]\([Element]\)> Function to be evaluated in browser context
- `...args` <...[Serializable]|[JSHandle]> Arguments to pass to `pageFunction`
- returns: <[Promise]<[Serializable]>> Promise which resolves to the return value of `pageFunction`
@ -3432,7 +3431,7 @@ Identifies what kind of target this is. Can be `"page"`, [`"background_page"`](h
### class: CDPSession
* extends: [`EventEmitter`](https://nodejs.org/api/events.html#events_class_eventemitter)
* extends: [EventEmitter](https://nodejs.org/api/events.html#events_class_eventemitter)
The `CDPSession` instances are used to talk raw Chrome Devtools Protocol:
- protocol methods can be called with `session.send` method.

View File

@ -46,7 +46,6 @@ class Page extends EventEmitter {
await client.send('Page.enable');
const {frameTree} = await client.send('Page.getFrameTree');
const page = new Page(client, target, frameTree, ignoreHTTPSErrors, screenshotTaskQueue);
await Promise.all([
client.send('Target.setAutoAttach', {autoAttach: true, waitForDebuggerOnStart: false, flatten: true}),
client.send('Page.setLifecycleEventsEnabled', { enabled: true }),

View File

@ -14,18 +14,19 @@
"unit": "node test/test.js",
"debug-unit": "node --inspect-brk test/test.js",
"test-doclint": "node utils/doclint/check_public_api/test/test.js && node utils/doclint/preprocessor/test.js",
"test": "npm run lint --silent && npm run coverage && npm run test-doclint && npm run test-node6-transformer",
"test": "npm run lint --silent && npm run coverage && npm run test-doclint && npm run test-node6-transformer && npm run test-types",
"install": "node install.js",
"lint": "([ \"$CI\" = true ] && eslint --quiet -f codeframe . || eslint .) && npm run tsc && npm run doc",
"doc": "node utils/doclint/cli.js",
"coverage": "cross-env COVERAGE=true npm run unit",
"test-node6-transformer": "node utils/node6-transform/test/test.js",
"build": "node utils/node6-transform/index.js",
"build": "node utils/node6-transform/index.js && node utils/doclint/generate_types",
"unit-node6": "node node6/test/test.js",
"tsc": "tsc -p .",
"prepublishOnly": "npm run build",
"apply-next-version": "node utils/apply_next_version.js",
"bundle": "npx browserify -r ./index.js:puppeteer -o utils/browser/puppeteer-web.js",
"test-types": "node utils/doclint/generate_types && npx -p typescript@2.1 tsc -p utils/doclint/generate_types/test/",
"unit-bundle": "node utils/browser/test.js"
},
"author": "The Chromium Authors",

View File

@ -31,8 +31,10 @@ Documentation.Class = class {
/**
* @param {string} name
* @param {!Array<!Documentation.Member>} membersArray
* @param {?string=} extendsName
* @param {string=} comment
*/
constructor(name, membersArray) {
constructor(name, membersArray, extendsName = null, comment = '') {
this.name = name;
this.membersArray = membersArray;
/** @type {!Map<string, !Documentation.Member>} */
@ -43,6 +45,8 @@ Documentation.Class = class {
this.methods = new Map();
/** @type {!Map<string, !Documentation.Member>} */
this.events = new Map();
this.comment = comment;
this.extends = extendsName;
for (const member of membersArray) {
this.members.set(member.name, member);
if (member.kind === 'method')
@ -59,14 +63,17 @@ Documentation.Member = class {
/**
* @param {string} kind
* @param {string} name
* @param {!Documentation.Type} type
* @param {?Documentation.Type} type
* @param {!Array<!Documentation.Member>} argsArray
*/
constructor(kind, name, type, argsArray) {
constructor(kind, name, type, argsArray, comment = '', returnComment = '', required = true) {
this.kind = kind;
this.name = name;
this.type = type;
this.comment = comment;
this.returnComment = returnComment;
this.argsArray = argsArray;
this.required = required;
/** @type {!Map<string, !Documentation.Member>} */
this.args = new Map();
for (const arg of argsArray)
@ -79,25 +86,29 @@ Documentation.Member = class {
* @param {?Documentation.Type} returnType
* @return {!Documentation.Member}
*/
static createMethod(name, argsArray, returnType) {
return new Documentation.Member('method', name, returnType, argsArray,);
static createMethod(name, argsArray, returnType, returnComment, comment) {
return new Documentation.Member('method', name, returnType, argsArray, comment, returnComment);
}
/**
* @param {string} name
* @param {!Documentation.Type}
* @param {!Documentation.Type} type
* @param {string=} comment
* @param {boolean=} required
* @return {!Documentation.Member}
*/
static createProperty(name, type) {
return new Documentation.Member('property', name, type, []);
static createProperty(name, type, comment, required) {
return new Documentation.Member('property', name, type, [], comment, undefined, required);
}
/**
* @param {string} name
* @param {?Documentation.Type=} type
* @param {string=} comment
* @return {!Documentation.Member}
*/
static createEvent(name) {
return new Documentation.Member('event', name, null, []);
static createEvent(name, type = null, comment) {
return new Documentation.Member('event', name, type, [], comment);
}
};

View File

@ -30,57 +30,44 @@ class MDOutline {
const writer = new commonmark.HtmlRenderer();
const html = writer.render(parsed);
page.on('console', msg => {
console.log(msg.text());
});
// Extract headings.
await page.setContent(html);
const {classes, errors} = await page.evaluate(() => {
const classes = [];
let currentClass = {};
let member = {};
const errors = [];
for (const element of document.body.querySelectorAll('h3, h4, h4 + ul > li')) {
if (element.matches('h3')) {
currentClass = {
name: element.textContent,
members: [],
};
classes.push(currentClass);
} else if (element.matches('h4')) {
member = {
name: element.textContent,
args: [],
returnType: null
};
currentClass.members.push(member);
} else if (element.matches('li') && element.firstChild.matches && element.firstChild.matches('code')) {
member.args.push(parseProperty(element));
} else if (element.matches('li') && element.firstChild.nodeType === Element.TEXT_NODE && element.firstChild.textContent.toLowerCase().startsWith('return')) {
member.returnType = parseProperty(element);
const expectedText = 'returns: ';
let actualText = element.firstChild.textContent;
let angleIndex = actualText.indexOf('<');
let spaceIndex = actualText.indexOf(' ');
angleIndex = angleIndex === -1 ? actualText.length : angleIndex;
spaceIndex = spaceIndex === -1 ? actualText.length : spaceIndex + 1;
actualText = actualText.substring(0, Math.min(angleIndex, spaceIndex));
if (actualText !== expectedText)
errors.push(`${member.name} has mistyped 'return' type declaration: expected exactly '${expectedText}', found '${actualText}'.`);
}
const headers = document.body.querySelectorAll('h3');
for (let i = 0; i < headers.length; i++) {
const fragment = extractSiblingsIntoFragment(headers[i], headers[i + 1]);
classes.push(parseClass(fragment));
}
return {classes, errors};
/**
* @param {HTMLLIElement} element
*/
function parseProperty(element) {
const str = element.textContent;
const name = str.substring(0, str.indexOf('<')).trim();
const clone = element.cloneNode(true);
const ul = clone.querySelector(':scope > ul');
const str = parseComment(extractSiblingsIntoFragment(clone.firstChild, ul));
const name = str.substring(0, str.indexOf('<')).replace(/\`/g, '').trim();
const type = findType(str);
const properties = [];
const comment = str.substring(str.indexOf('<') + type.length + 2).trim();
// Strings have enum values instead of properties
if (!type.includes('string')) {
for (const childElement of element.querySelectorAll(':scope > ul > li'))
properties.push(parseProperty(childElement));
for (const childElement of element.querySelectorAll(':scope > ul > li')) {
const property = parseProperty(childElement);
property.required = property.comment.includes('***required***');
properties.push(property);
}
}
return {
name,
type,
comment,
properties
};
}
@ -100,6 +87,107 @@ class MDOutline {
}
return 'unknown';
}
/**
* @param {DocumentFragment} content
*/
function parseClass(content) {
const members = [];
const headers = content.querySelectorAll('h4');
const name = content.firstChild.textContent;
let extendsName = null;
let commentStart = content.firstChild.nextSibling;
const extendsElement = content.querySelector('ul');
if (extendsElement && extendsElement.textContent.trim().startsWith('extends:')) {
commentStart = extendsElement.nextSibling;
extendsName = extendsElement.querySelector('a').textContent;
}
const comment = parseComment(extractSiblingsIntoFragment(commentStart, headers[0]));
for (let i = 0; i < headers.length; i++) {
const fragment = extractSiblingsIntoFragment(headers[i], headers[i + 1]);
members.push(parseMember(fragment));
}
return {
name,
comment,
extendsName,
members
};
}
/**
* @param {Node} content
*/
function parseComment(content) {
for (const code of content.querySelectorAll('pre > code'))
code.replaceWith('```' + code.className.substring('language-'.length) + '\n' + code.textContent + '```');
for (const code of content.querySelectorAll('code'))
code.replaceWith('`' + code.textContent + '`');
for (const strong of content.querySelectorAll('strong'))
strong.replaceWith('**' + parseComment(strong) + '**');
return content.textContent.trim();
}
/**
* @param {string} name
* @param {DocumentFragment} content
*/
function parseMember(content) {
const name = content.firstChild.textContent;
const args = [];
let returnType = null;
const paramRegex = /^\w+\.[\w$]+\((.*)\)$/;
const matches = paramRegex.exec(name) || ['', ''];
const parameters = matches[1];
const optionalStartIndex = parameters.indexOf('[');
const optinalParamsStr = optionalStartIndex !== -1 ? parameters.substring(optionalStartIndex).replace(/[\[\]]/g, '') : '';
const optionalparams = new Set(optinalParamsStr.split(',').filter(x => x).map(x => x.trim()));
const ul = content.querySelector('ul');
for (const element of content.querySelectorAll('h4 + ul > li')) {
if (element.matches('li') && element.textContent.trim().startsWith('<')) {
returnType = parseProperty(element);
} else if (element.matches('li') && element.firstChild.matches && element.firstChild.matches('code')) {
const property = parseProperty(element);
property.required = !optionalparams.has(property.name);
args.push(property);
} else if (element.matches('li') && element.firstChild.nodeType === Element.TEXT_NODE && element.firstChild.textContent.toLowerCase().startsWith('return')) {
returnType = parseProperty(element);
const expectedText = 'returns: ';
let actualText = element.firstChild.textContent;
let angleIndex = actualText.indexOf('<');
let spaceIndex = actualText.indexOf(' ');
angleIndex = angleIndex === -1 ? actualText.length : angleIndex;
spaceIndex = spaceIndex === -1 ? actualText.length : spaceIndex + 1;
actualText = actualText.substring(0, Math.min(angleIndex, spaceIndex));
if (actualText !== expectedText)
errors.push(`${name} has mistyped 'return' type declaration: expected exactly '${expectedText}', found '${actualText}'.`);
}
}
const comment = parseComment(extractSiblingsIntoFragment(ul ? ul.nextSibling : content));
return {
name,
args,
returnType,
comment
};
}
/**
* @param {!Node} fromInclusive
* @param {!Node} toExclusive
* @return {!DocumentFragment}
*/
function extractSiblingsIntoFragment(fromInclusive, toExclusive) {
const fragment = document.createDocumentFragment();
let node = fromInclusive;
while (node && node !== toExclusive) {
const next = node.nextSibling;
fragment.appendChild(node);
node = next;
}
return fragment;
}
});
return new MDOutline(classes, errors);
}
@ -114,11 +202,15 @@ class MDOutline {
const eventRegex = /^event: '(\w+)'$/;
let currentClassName = null;
let currentClassMembers = [];
let currentClassComment = '';
let currentClassExtends = null;
for (const cls of classes) {
const match = cls.name.match(classHeading);
if (!match)
continue;
currentClassName = match[1];
currentClassComment = cls.comment;
currentClassExtends = cls.extendsName;
for (const member of cls.members) {
if (constructorRegex.test(member.name)) {
const match = member.name.match(constructorRegex);
@ -147,15 +239,20 @@ class MDOutline {
this.errors.push(`Heading arguments for "${member.name}" do not match described ones, i.e. "${parameters}" != "${member.args.map(a => a.name).join(', ')}"`);
const args = member.args.map(createPropertyFromJSON);
let returnType = null;
if (member.returnType)
returnType = createPropertyFromJSON(member.returnType).type;
const method = Documentation.Member.createMethod(methodName, args, returnType);
let returnComment = '';
if (member.returnType) {
const returnProperty = createPropertyFromJSON(member.returnType);
returnType = returnProperty.type;
returnComment = returnProperty.comment;
}
const method = Documentation.Member.createMethod(methodName, args, returnType, returnComment, member.comment);
currentClassMembers.push(method);
}
function createPropertyFromJSON(payload) {
const type = new Documentation.Type(payload.type, payload.properties.map(createPropertyFromJSON));
return Documentation.Member.createProperty(payload.name, type);
const required = payload.required;
return Documentation.Member.createProperty(payload.name, type, payload.comment, required);
}
function handleProperty(member, className, propertyName) {
@ -163,7 +260,9 @@ class MDOutline {
this.errors.push(`Failed to process header as property: ${member.name}`);
return;
}
currentClassMembers.push(Documentation.Member.createProperty(propertyName));
const type = member.returnType ? member.returnType.type : null;
const properties = member.returnType ? member.returnType.properties : [];
currentClassMembers.push(createPropertyFromJSON({type, name: propertyName, properties, comment: member.comment}));
}
function handleEvent(member, eventName) {
@ -171,13 +270,13 @@ class MDOutline {
this.errors.push(`Failed to process header as event: ${member.name}`);
return;
}
currentClassMembers.push(Documentation.Member.createEvent(eventName));
currentClassMembers.push(Documentation.Member.createEvent(eventName, member.returnType && createPropertyFromJSON(member.returnType).type, member.comment));
}
function flushClassIfNeeded() {
if (currentClassName === null)
return;
this.classes.push(new Documentation.Class(currentClassName, currentClassMembers));
this.classes.push(new Documentation.Class(currentClassName, currentClassMembers, currentClassExtends, currentClassComment));
currentClassName = null;
currentClassMembers = [];
}

View File

@ -5,6 +5,9 @@
"members": [
{
"name": "frame",
"type": {
"name": "[Frame]"
},
"kind": "event"
},
{
@ -25,6 +28,9 @@
},
{
"name": "url",
"type": {
"name": "string"
},
"kind": "property"
}
]

View File

@ -0,0 +1,217 @@
const path = require('path');
const Source = require('../Source');
const puppeteer = require('../../..');
const PROJECT_DIR = path.join(__dirname, '..', '..', '..');
const fs = require('fs');
const objectDefinitions = [];
(async function() {
const browser = await puppeteer.launch();
const page = (await browser.pages())[0];
const api = await Source.readFile(path.join(PROJECT_DIR, 'docs', 'api.md'));
const {documentation} = await require('../check_public_api/MDBuilder')(page, [api]);
await browser.close();
const classes = documentation.classesArray.slice(1);
const root = documentation.classesArray[0];
const output = `// This file is generated by ${__filename.substring(path.join(__dirname, '..', '..').length)}
import { ChildProcess } from 'child_process';
import { EventEmitter } from 'events';
/**
* Can be converted to JSON
*/
interface Serializable {}
interface ConnectionTransport {}
${root.membersArray.map(member => `
${memberJSDOC(member, '')}export function ${member.name}${argsFromMember(member)} : ${typeToString(member.type, member.name)};
`).join('')}
${classes.map(classDesc => classToString(classDesc)).join('\n')}
${objectDefinitionsToString()}
`;
fs.writeFileSync(path.join(PROJECT_DIR, 'index.d.ts'), output, 'utf8');
})();
function objectDefinitionsToString() {
let definition;
const parts = [];
while ((definition = objectDefinitions.pop())) {
const {name, properties} = definition;
parts.push(`interface ${name} {`);
parts.push(properties.map(member => ` ${memberJSDOC(member, ' ')}${nameForProperty(member)}${argsFromMember(member, name)}: ${typeToString(member.type, name, member.name)};`).join('\n\n'));
parts.push('}\n');
}
return parts.join('\n');
}
function nameForProperty(member) {
return (member.required || member.name.startsWith('...')) ? member.name : member.name + '?';
}
/**
* @param {import('./check_public_api/Documentation').Class} classDesc
*/
function classToString(classDesc) {
const parts = [];
if (classDesc.comment) {
parts.push(`/**
* ${classDesc.comment.split('\n').join('\n * ')}
*/`);
}
parts.push(`export interface ${classDesc.name} ${classDesc.extends ? `extends ${classDesc.extends} ` : ''}{`);
for (const method of ['on', 'once', 'addListener']) {
for (const [eventName, value] of classDesc.events) {
if (value.comment) {
parts.push(' /**');
parts.push(...value.comment.split('\n').map(line => ' * ' + line));
parts.push(' */');
}
parts.push(` ${method}(event: '${eventName}', listener: (arg0 : ${typeToString(value && value.type, classDesc.name, eventName, 'payload')}) => void): this;\n`);
}
}
parts.push(classDesc.membersArray.map(member => ` ${memberJSDOC(member, ' ')}${member.name}${argsFromMember(member, classDesc.name)}: ${typeToString(member.type, classDesc.name, member.name)};`).join('\n\n'));
parts.push('}\n');
return parts.join('\n');
}
/**
* @param {import('./check_public_api/Documentation').Type} type
*/
function typeToString(type, ...namespace) {
if (!type)
return 'void';
let typeString = stringifyType(parseType(type.name));
for (let i = 0; i < type.properties.length; i++)
typeString = typeString.replace('arg' + i, type.properties[i].name);
if (type.properties.length && typeString.indexOf('Object') !== -1) {
const name = namespace.map(n => n[0].toUpperCase() + n.substring(1)).join('');
typeString = typeString.replace('Object', name);
objectDefinitions.push({name, properties: type.properties});
}
return typeString;
}
/**
* @param {string} type
*/
function parseType(type) {
type = type.trim();
if (type.startsWith('?')) {
const parsed = parseType(type.substring(1));
parsed.nullable = true;
return parsed;
}
if (type.startsWith('...'))
return parseType('Array<' + type.substring(3) + '>');
let name = type;
let next = null;
let template = null;
let args = null;
let retType = null;
let firstTypeLength = type.length;
for (let i = 0; i < type.length; i++) {
if (type[i] === '<') {
name = type.substring(0, i);
const matching = matchingBracket(type.substring(i), '<', '>');
template = parseType(type.substring(i + 1, i + matching - 1));
firstTypeLength = i + matching;
break;
}
if (type[i] === '(') {
name = type.substring(0, i);
const matching = matchingBracket(type.substring(i), '(', ')');
args = parseType(type.substring(i + 1, i + matching - 1));
i = i + matching;
if (type[i] === ':') {
retType = parseType(type.substring(i + 1));
next = retType.next;
retType.next = null;
break;
}
}
if (type[i] === '|' || type[i] === ',') {
name = type.substring(0, i);
firstTypeLength = i;
break;
}
}
let pipe = null;
if (type[firstTypeLength] === '|')
pipe = parseType(type.substring(firstTypeLength + 1));
else if (type[firstTypeLength] === ',')
next = parseType(type.substring(firstTypeLength + 1));
if (name === 'Promise' && !template)
template = parseType('void');
return {
name,
args,
retType,
template,
pipe,
next
};
}
function stringifyType(parsedType) {
if (!parsedType)
return 'void';
let out = parsedType.name;
if (parsedType.args) {
let args = parsedType.args;
const stringArgs = [];
while (args) {
const arg = args;
args = args.next;
arg.next = null;
stringArgs.push(stringifyType(arg));
}
out = `(${stringArgs.map((type, index) => `arg${index} : ${type}`).join(', ')}, ...args: any[]) => ${stringifyType(parsedType.retType)}`;
} else if (parsedType.name === 'function') {
out = 'Function';
}
if (parsedType.nullable)
out = 'null|' + out;
if (parsedType.template)
out += '<' + stringifyType(parsedType.template) + '>';
if (parsedType.pipe)
out += '|' + stringifyType(parsedType.pipe);
if (parsedType.next)
out += ', ' + stringifyType(parsedType.next);
return out.trim();
}
function matchingBracket(str, open, close) {
let count = 1;
let i = 1;
for (; i < str.length && count; i++) {
if (str[i] === open)
count++;
else if (str[i] === close)
count--;
}
return i;
}
/**
* @param {import('./check_public_api/Documentation').Member} member
*/
function argsFromMember(member, ...namespace) {
if (member.kind === 'property')
return '';
return '(' + member.argsArray.map(arg => `${nameForProperty(arg)}: ${typeToString(arg.type, ...namespace, member.name, 'options')}`).join(', ') + ')';
}
/**
* @param {import('./check_public_api/Documentation').Member} member
*/
function memberJSDOC(member, indent) {
const lines = [];
if (member.comment)
lines.push(...member.comment.split('\n'));
lines.push(...member.argsArray.map(arg => `@param ${arg.name.replace(/\./g, '')} ${arg.comment.replace('\n', ' ')}`));
if (member.returnComment)
lines.push(`@returns ${member.returnComment}`);
if (!lines.length)
return '';
return `/**
${indent} * ${lines.join('\n' + indent + ' * ')}
${indent} */
${indent}`;
}

View File

@ -0,0 +1,283 @@
import * as puppeteer from "../../../../index";
// Examples taken from README
(async () => {
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto("https://example.com");
await page.screenshot({ path: "example.png" });
browser.close();
})();
(async () => {
const browser = await puppeteer.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 puppeteer.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
puppeteer.launch().then(async browser => {
const page = await browser.newPage();
page.on("console", (...args: any[]) => {
for (let i = 0; i < args.length; ++i) console.log(`${i}: ${args[i]}`);
});
page.evaluate(() => console.log(5, "hello", { foo: "bar" }));
const result = await page.evaluate(() => {
return Promise.resolve(8 * 7);
});
console.log(await page.evaluate("1 + 2"));
const bodyHandle = await page.$("body");
// Typings for this are really difficult since they depend on internal state
// of the page class.
const html = await page.evaluate(
(body: HTMLElement) => body.innerHTML,
bodyHandle
);
});
import * as crypto from "crypto";
import * as fs from "fs";
puppeteer.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("screen");
await page.pdf({ path: "page.pdf" });
await page.setRequestInterception(true);
page.on("request", interceptedRequest => {
if (
interceptedRequest.url().endsWith(".png") ||
interceptedRequest.url().endsWith(".jpg")
)
interceptedRequest.abort();
else interceptedRequest.continue();
});
page.keyboard.type("Hello"); // Types instantly
page.keyboard.type("World", { delay: 100 }); // Types slower, like a user
const watchDog = page.waitForFunction("window.innerWidth < 100");
page.setViewport({ width: 50, height: 50 });
await watchDog;
let currentURL: string;
page
.waitForSelector("img", { visible: true })
.then(() => console.log("First URL with image: " + currentURL));
for (currentURL of [
"https://example.com",
"https://google.com",
"https://bbc.com"
]) {
await page.goto(currentURL);
}
page.keyboard.type("Hello World!");
page.keyboard.press("ArrowLeft");
page.keyboard.down("Shift");
// tslint:disable-next-line prefer-for-of
for (let i = 0; i < " World".length; i++) {
page.keyboard.press("ArrowLeft");
}
page.keyboard.up("Shift");
page.keyboard.press("Backspace");
page.keyboard.sendCharacter("嗨");
await page.tracing.start({ path: "trace.json" });
await page.goto("https://www.google.com");
await page.tracing.stop();
page.on("dialog", async dialog => {
console.log(dialog.message());
await dialog.dismiss();
browser.close();
});
const inputElement = (await page.$("input[type=submit]"))!;
await inputElement.click();
});
// Example with launch options
(async () => {
const browser = await puppeteer.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 puppeteer.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: puppeteer.ConsoleMessage, ...args: any[]) => {
console.log(event.text, event.type);
for (let i = 0; i < args.length; ++i) console.log(`${i}: ${args[i]}`);
});
await button.focus();
await button.press("Enter");
await button.screenshot({
type: "jpeg",
omitBackground: true,
clip: {
x: 0,
y: 0,
width: 200,
height: 100
}
});
console.log(button.toString());
input.type("Hello World", { delay: 10 });
const buttonText = await (await button.getProperty('textContent')).jsonValue();
await page.deleteCookie(...await page.cookies());
const metrics = await page.metrics();
console.log(metrics.Documents, metrics.Frames, metrics.JSEventListeners);
const navResponse = await page.waitForNavigation({
timeout: 1000
});
console.log(navResponse.ok, navResponse.status, navResponse.url, navResponse.headers);
// evaluate example
const bodyHandle = (await page.$('body'))!;
const html = await page.evaluate((body : HTMLBodyElement) => body.innerHTML, bodyHandle);
await bodyHandle.dispose();
// getProperties example
const handle = await page.evaluateHandle(() => ({ window, document }));
const properties = await handle.getProperties();
const windowHandle = properties.get('window');
const documentHandle = properties.get('document');
await handle.dispose();
// queryObjects example
// Create a Map object
await page.evaluate(() => (window as any).map = new Map());
// Get a handle to the Map object prototype
const mapPrototype = await page.evaluateHandle(() => Map.prototype);
// Query all map instances into an array
const mapInstances = await page.queryObjects(mapPrototype);
// Count amount of map objects in heap
const count = await page.evaluate((maps: Map<any, any>[]) => maps.length, mapInstances);
await mapInstances.dispose();
await mapPrototype.dispose();
// evaluateHandle example
const aHandle = await page.evaluateHandle(() => document.body);
const resultHandle = await page.evaluateHandle((body: Element) => body.innerHTML, aHandle);
console.log(await resultHandle.jsonValue());
await resultHandle.dispose();
browser.close();
})();
// test $eval and $$eval
(async () => {
const browser = await puppeteer.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();
})();

View File

@ -0,0 +1,11 @@
{
"compilerOptions": {
"noImplicitAny": true,
"target": "es2015",
"noEmit": true
},
"include": [
"test.ts",
"../../../../index.d.ts"
]
}