Skip to content

Commit

Permalink
fix(doc): check and update optional types in the api (#1206)
Browse files Browse the repository at this point in the history
This adds a new check to doclint for whether a member is correctly marked as optional. 
part of #6
  • Loading branch information
JoelEinbinder committed Mar 4, 2020
1 parent f4e9b50 commit 8aa88d5
Show file tree
Hide file tree
Showing 6 changed files with 84 additions and 34 deletions.
50 changes: 25 additions & 25 deletions docs/api.md
Expand Up @@ -71,7 +71,7 @@ This object can be used to launch or connect to Chromium, returning instances of
#### playwright.devices
- returns: <[Object]>

Returns a list of devices to be used with [`browser.newContext(options)`](#browsernewcontextoptions) or [`browser.newPage(options)`](#browsernewpageoptions). Actual list of devices can be found in [src/deviceDescriptors.ts](https://github.com/Microsoft/playwright/blob/master/src/deviceDescriptors.ts).
Returns a list of devices to be used with [`browser.newContext([options])`](#browsernewcontextoptions) or [`browser.newPage([options])`](#browsernewpageoptions). Actual list of devices can be found in [src/deviceDescriptors.ts](https://github.com/Microsoft/playwright/blob/master/src/deviceDescriptors.ts).

```js
const { webkit, devices } = require('playwright');
Expand Down Expand Up @@ -145,14 +145,14 @@ const { firefox } = require('playwright'); // Or 'chromium' or 'webkit'.
})();
```

See [ChromiumBrowser], [FirefoxBrowser] and [WebKitBrowser] for browser-specific features. Note that [browserType.connect(options)](#browsertypeconnectoptions) and [browserType.launch(options)](#browsertypelaunchoptions) always return a specific browser instance, based on the browser being connected to or launched.
See [ChromiumBrowser], [FirefoxBrowser] and [WebKitBrowser] for browser-specific features. Note that [browserType.connect(options)](#browsertypeconnectoptions) and [browserType.launch([options])](#browsertypelaunchoptions) always return a specific browser instance, based on the browser being connected to or launched.

<!-- GEN:toc -->
- [event: 'disconnected'](#event-disconnected)
- [browser.close()](#browserclose)
- [browser.contexts()](#browsercontexts)
- [browser.isConnected()](#browserisconnected)
- [browser.newContext(options)](#browsernewcontextoptions)
- [browser.newContext([options])](#browsernewcontextoptions)
- [browser.newPage([options])](#browsernewpageoptions)
<!-- GEN:stop -->

Expand Down Expand Up @@ -182,7 +182,7 @@ a single instance of [BrowserContext].

Indicates that the browser is connected.

#### browser.newContext(options)
#### browser.newContext([options])
- `options` <[Object]>
- `ignoreHTTPSErrors` <?[boolean]> Whether to ignore HTTPS errors during navigation. Defaults to `false`.
- `bypassCSP` <?[boolean]> Toggles bypassing page's Content-Security-Policy.
Expand Down Expand Up @@ -604,8 +604,8 @@ page.removeListener('request', logRequest);
- [page.evaluate(pageFunction[, ...args])](#pageevaluatepagefunction-args)
- [page.evaluateHandle(pageFunction[, ...args])](#pageevaluatehandlepagefunction-args)
- [page.exposeFunction(name, playwrightFunction)](#pageexposefunctionname-playwrightfunction)
- [page.fill(selector, value, options)](#pagefillselector-value-options)
- [page.focus(selector, options)](#pagefocusselector-options)
- [page.fill(selector, value[, options])](#pagefillselector-value-options)
- [page.focus(selector[, options])](#pagefocusselector-options)
- [page.frames()](#pageframes)
- [page.goBack([options])](#pagegobackoptions)
- [page.goForward([options])](#pagegoforwardoptions)
Expand All @@ -620,7 +620,7 @@ page.removeListener('request', logRequest);
- [page.reload([options])](#pagereloadoptions)
- [page.route(url, handler)](#pagerouteurl-handler)
- [page.screenshot([options])](#pagescreenshotoptions)
- [page.select(selector, value, options)](#pageselectselector-value-options)
- [page.select(selector, value[, options])](#pageselectselector-value-options)
- [page.setCacheEnabled([enabled])](#pagesetcacheenabledenabled)
- [page.setContent(html[, options])](#pagesetcontenthtml-options)
- [page.setDefaultNavigationTimeout(timeout)](#pagesetdefaultnavigationtimeouttimeout)
Expand Down Expand Up @@ -1135,7 +1135,7 @@ const fs = require('fs');
})();
```

#### page.fill(selector, value, options)
#### page.fill(selector, value[, options])
- `selector` <[string]> A selector to query page for.
- `value` <[string]> Value to fill for the `<input>`, `<textarea>` or `[contenteditable]` element.
- `options` <[Object]>
Expand All @@ -1150,7 +1150,7 @@ If there's no text `<input>`, `<textarea>` or `[contenteditable]` element matchi
Shortcut for [page.mainFrame().fill()](#framefillselector-value)

#### page.focus(selector, options)
#### page.focus(selector[, options])
- `selector` <[string]> A selector of an element to focus. If there are multiple elements satisfying the selector, the first will be focused.
- `options` <[Object]>
- `waitFor` <[boolean]> Whether to wait for the element to be present in the dom. Defaults to `true`.
Expand Down Expand Up @@ -1216,7 +1216,7 @@ Navigate to the next page in history.
> **NOTE** Headless mode doesn't support navigation to a PDF document. See the [upstream issue](https://bugs.chromium.org/p/chromium/issues/detail?id=761295).
Shortcut for [page.mainFrame().goto(url, options)](#framegotourl-options)
Shortcut for [page.mainFrame().goto(url[, options])](#framegotourl-options)

#### page.hover(selector[, options])
- `selector` <[string]> A selector to search for element to hover. If there are multiple elements satisfying the selector, the first will be hovered.
Expand Down Expand Up @@ -1381,7 +1381,7 @@ await browser.close();

> **NOTE** Screenshots take at least 1/6 second on Chromium OS X and Chromium Windows. See https://crbug.com/741689 for discussion.
#### page.select(selector, value, options)
#### page.select(selector, value[, options])
- `selector` <[string]> A selector to query frame for.
- `value` <[string]|[ElementHandle]|[Object]|[Array]<[string]>|[Array]<[ElementHandle]>|[Array]<[Object]>> Options to select. If the `<select>` has the `multiple` attribute, all matching options are selected, otherwise only the first option matching one of the passed options is selected. String values are equivalent to `{value:'string'}`. Option is considered matching if all specified properties match.
- `value` <[string]> Matches by `option.value`.
Expand Down Expand Up @@ -1412,7 +1412,7 @@ page.select('select#colors', { value: 'blue' }, { index: 2 }, 'red');
Shortcut for [page.mainFrame().select()](#frameselectselector-values)

#### page.setCacheEnabled([enabled])
- `enabled` <[boolean]> sets the `enabled` state of the cache.
- `enabled` <[boolean]> sets the `enabled` state of the cache. Defaults to `true`.
- returns: <[Promise]>

Toggles ignoring cache for each request based on the enabled state. By default, caching is enabled.
Expand Down Expand Up @@ -1468,7 +1468,7 @@ The extra HTTP headers will be sent with every request the page initiates.
- `height` <[number]> page height in pixels. **required**
- returns: <[Promise]>

In the case of multiple pages in a single browser, each page can have its own viewport size. However, [browser.newContext(options)](#browsernewcontextoptions) allows to set viewport size (and more) for all pages in the context at once.
In the case of multiple pages in a single browser, each page can have its own viewport size. However, [browser.newContext([options])](#browsernewcontextoptions) allows to set viewport size (and more) for all pages in the context at once.

`page.setViewportSize` will resize the page. A lot of websites don't expect phones to change size, so you should set the viewport size before navigating to the page.

Expand Down Expand Up @@ -1800,15 +1800,15 @@ An example of getting text from an iframe element:
- [frame.dblclick(selector[, options])](#framedblclickselector-options)
- [frame.evaluate(pageFunction[, ...args])](#frameevaluatepagefunction-args)
- [frame.evaluateHandle(pageFunction[, ...args])](#frameevaluatehandlepagefunction-args)
- [frame.fill(selector, value, options)](#framefillselector-value-options)
- [frame.focus(selector, options)](#framefocusselector-options)
- [frame.fill(selector, value[, options])](#framefillselector-value-options)
- [frame.focus(selector[, options])](#framefocusselector-options)
- [frame.frameElement()](#frameframeelement)
- [frame.goto(url[, options])](#framegotourl-options)
- [frame.hover(selector[, options])](#framehoverselector-options)
- [frame.isDetached()](#frameisdetached)
- [frame.name()](#framename)
- [frame.parentFrame()](#frameparentframe)
- [frame.select(selector, value, options)](#frameselectselector-value-options)
- [frame.select(selector, value[, options])](#frameselectselector-value-options)
- [frame.setContent(html[, options])](#framesetcontenthtml-options)
- [frame.title()](#frametitle)
- [frame.tripleclick(selector[, options])](#frametripleclickselector-options)
Expand Down Expand Up @@ -2025,7 +2025,7 @@ console.log(await resultHandle.jsonValue());
await resultHandle.dispose();
```

#### frame.fill(selector, value, options)
#### frame.fill(selector, value[, options])
- `selector` <[string]> A selector to query page for.
- `value` <[string]> Value to fill for the `<input>`, `<textarea>` or `[contenteditable]` element.
- `options` <[Object]>
Expand All @@ -2036,7 +2036,7 @@ await resultHandle.dispose();
This method focuses the element and triggers an `input` event after filling.
If there's no text `<input>`, `<textarea>` or `[contenteditable]` element matching `selector`, the method throws an error.

#### frame.focus(selector, options)
#### frame.focus(selector[, options])
- `selector` <[string]> A selector of an element to focus. If there are multiple elements satisfying the selector, the first will be focused.
- `options` <[Object]>
- `waitFor` <[boolean]> Whether to wait for the element to be present in the dom. Defaults to `true`.
Expand Down Expand Up @@ -2116,7 +2116,7 @@ If the name is empty, returns the id attribute instead.
#### frame.parentFrame()
- returns: <?[Frame]> Parent frame, if any. Detached frames and main frames return `null`.

#### frame.select(selector, value, options)
#### frame.select(selector, value[, options])
- `selector` <[string]> A selector to query frame for.
- `value` <[string]|[ElementHandle]|[Object]|[Array]<[string]>|[Array]<[ElementHandle]>|[Array]<[Object]>> Options to select. If the `<select>` has the `multiple` attribute, all matching options are selected, otherwise only the first option matching one of the passed options is selected. String values are equivalent to `{value:'string'}`. Option is considered matching if all specified properties match.
- `value` <[string]> Matches by `option.value`.
Expand Down Expand Up @@ -3488,7 +3488,7 @@ This methods attaches Playwright to an existing browser instance.
#### browserType.devices
- returns: <[Object]>

Returns a list of devices to be used with [`browser.newContext(options)`](#browsernewcontextoptions) and [`browser.newPage(options)`](#browsernewpageoptions). Actual list of devices can be found in [src/deviceDescriptors.ts](https://github.com/Microsoft/playwright/blob/master/src/deviceDescriptors.ts).
Returns a list of devices to be used with [`browser.newContext([options])`](#browsernewcontextoptions) and [`browser.newPage([options])`](#browsernewpageoptions). Actual list of devices can be found in [src/deviceDescriptors.ts](https://github.com/Microsoft/playwright/blob/master/src/deviceDescriptors.ts).

```js
const { webkit } = require('playwright');
Expand Down Expand Up @@ -3640,23 +3640,23 @@ await browser.stopTracing();

<!-- GEN:toc -->
- [chromiumBrowser.createBrowserSession()](#chromiumbrowsercreatebrowsersession)
- [chromiumBrowser.startTracing(page, [options])](#chromiumbrowserstarttracingpage-options)
- [chromiumBrowser.startTracing([page, options])](#chromiumbrowserstarttracingpage-options)
- [chromiumBrowser.stopTracing()](#chromiumbrowserstoptracing)
<!-- GEN:stop -->
<!-- GEN:toc-extends-Browser -->
- [event: 'disconnected'](#event-disconnected)
- [browser.close()](#browserclose)
- [browser.contexts()](#browsercontexts)
- [browser.isConnected()](#browserisconnected)
- [browser.newContext(options)](#browsernewcontextoptions)
- [browser.newContext([options])](#browsernewcontextoptions)
- [browser.newPage([options])](#browsernewpageoptions)
<!-- GEN:stop -->

#### chromiumBrowser.createBrowserSession()
- returns: <[Promise]<[ChromiumSession]>> Promise that resolves to the newly created browser
session.

#### chromiumBrowser.startTracing(page, [options])
#### chromiumBrowser.startTracing([page, options])
- `page` <[Page]> Optional, if specified, tracing includes screenshots of the given page.
- `options` <[Object]>
- `path` <[string]> A path to write the trace file to.
Expand Down Expand Up @@ -3839,7 +3839,7 @@ Firefox browser instance does not expose Firefox-specific features.
- [browser.close()](#browserclose)
- [browser.contexts()](#browsercontexts)
- [browser.isConnected()](#browserisconnected)
- [browser.newContext(options)](#browsernewcontextoptions)
- [browser.newContext([options])](#browsernewcontextoptions)
- [browser.newPage([options])](#browsernewpageoptions)
<!-- GEN:stop -->

Expand All @@ -3854,7 +3854,7 @@ WebKit browser instance does not expose WebKit-specific features.
- [browser.close()](#browserclose)
- [browser.contexts()](#browsercontexts)
- [browser.isConnected()](#browserisconnected)
- [browser.newContext(options)](#browsernewcontextoptions)
- [browser.newContext([options])](#browsernewcontextoptions)
- [browser.newPage([options])](#browsernewpageoptions)
<!-- GEN:stop -->

Expand Down
2 changes: 1 addition & 1 deletion src/chromium/crBrowser.ts
Expand Up @@ -150,7 +150,7 @@ export class CRBrowser extends platform.EventEmitter implements Browser {
return await this._connection.createBrowserSession();
}

async startTracing(page: Page | undefined, options: { path?: string; screenshots?: boolean; categories?: string[]; } = {}) {
async startTracing(page?: Page, options: { path?: string; screenshots?: boolean; categories?: string[]; } = {}) {
assert(!this._tracingRecording, 'Cannot start recording trace while already recording trace.');
this._tracingClient = page ? (page._delegate as CRPage)._client : this._client;

Expand Down
55 changes: 49 additions & 6 deletions utils/doclint/check_public_api/JSBuilder.js
Expand Up @@ -40,6 +40,7 @@ function checkSources(sources, externalDependencies) {
options: {
allowJs: true,
target: ts.ScriptTarget.ESNext,
strict: true
},
rootNames: sources.map(source => source.filePath())
});
Expand Down Expand Up @@ -152,15 +153,39 @@ function checkSources(sources, externalDependencies) {
return parents;
}

function serializeSymbol(symbol, circular = []) {
/**
* @param {ts.Symbol} symbol
* @param {string[]=} circular
* @param {boolean=} parentRequired
*/
function serializeSymbol(symbol, circular = [], parentRequired = true) {
const type = checker.getTypeOfSymbolAtLocation(symbol, symbol.valueDeclaration);
const name = symbol.getName();
if (symbol.valueDeclaration && symbol.valueDeclaration.dotDotDotToken) {
const innerType = serializeType(type.typeArguments ? type.typeArguments[0] : type, circular);
innerType.name = '...' + innerType.name;
return Documentation.Member.createProperty('...' + name, innerType);
const required = false;
return Documentation.Member.createProperty('...' + name, innerType, undefined, required);
}
return Documentation.Member.createProperty(name, serializeType(type, circular));

const required = parentRequired && !typeHasUndefined(type);
return Documentation.Member.createProperty(name, serializeType(type, circular), undefined, required);
}

/**
* @param {!ts.Type} type
*/
function typeHasUndefined(type) {
if (!type.isUnion())
return type.flags & ts.TypeFlags.Undefined;
return type.types.some(typeHasUndefined);
}

/**
* @param {!ts.Type} type
*/
function isNotNullish(type) {
return !((type.flags & ts.TypeFlags.Undefined) || (type.flags & ts.TypeFlags.Null));
}

/**
Expand Down Expand Up @@ -208,7 +233,9 @@ function checkSources(sources, externalDependencies) {
return new Documentation.Type('Object', properties);
}
if (type.isUnion() && (typeName.includes('|') || type.types.every(type => type.isStringLiteral() || type.intrinsicName === 'number'))) {
const types = type.types.map(type => serializeType(type, circular));
const types = type.types
.filter(isNotNullish)
.map(type => serializeType(type, circular));
const name = types.map(type => type.name).join('|');
const properties = [].concat(...types.map(type => type.properties));
return new Documentation.Type(name.replace(/false\|true/g, 'boolean'), properties);
Expand Down Expand Up @@ -253,7 +280,7 @@ function checkSources(sources, externalDependencies) {
continue;
}
const memberType = checker.getTypeOfSymbolAtLocation(member, member.valueDeclaration);
const signature = memberType.getCallSignatures()[0];
const signature = signatureForType(memberType);
if (signature)
members.push(serializeSignature(name, signature));
else
Expand All @@ -263,12 +290,28 @@ function checkSources(sources, externalDependencies) {
return new Documentation.Class(className, members);
}

/**
* @param {ts.Type} type
*/
function signatureForType(type) {
const signatures = type.getCallSignatures();
if (signatures.length)
return signatures[0];
if (type.isUnion()) {
const innerTypes = type.types.filter(isNotNullish);
if (innerTypes.length === 1)
return signatureForType(innerTypes[0]);
}
return null;
}

/**
* @param {string} name
* @param {!ts.Signature} signature
*/
function serializeSignature(name, signature) {
const parameters = signature.parameters.map(s => serializeSymbol(s));
const minArgumentCount = signature.minArgumentCount || 0;
const parameters = signature.parameters.map((s, index) => serializeSymbol(s, [], index < minArgumentCount));
const returnType = serializeType(signature.getReturnType());
return Documentation.Member.createMethod(name, parameters, returnType.name !== 'void' ? returnType : null);
}
Expand Down
2 changes: 1 addition & 1 deletion utils/doclint/check_public_api/MDBuilder.js
Expand Up @@ -154,7 +154,7 @@ class MDOutline {
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);
property.required = !optionalparams.has(property.name) && !property.name.startsWith('...');
args.push(property);
} else if (element.matches('li') && element.firstChild.nodeType === Element.TEXT_NODE && element.firstChild.textContent.toLowerCase().startsWith('return')) {
returnType = parseProperty(element);
Expand Down
7 changes: 7 additions & 0 deletions utils/doclint/check_public_api/index.js
Expand Up @@ -143,6 +143,13 @@ function compareDocumentations(actual, expected) {
for (const arg of argsDiff.extra)
text.push(`- Non-existing argument found: ${arg}`);
errors.push(text.join('\n'));
} else {
for (const name of actualMethod.args.keys()) {
const actual = actualMethod.args.get(name);
const expected = expectedMethod.args.get(name);
if (actual.required !== expected.required)
errors.push(`${className}.${methodName}(): ${name} should be ${expected.required ? 'required' : 'optional'}`);
}
}

for (const arg of argsDiff.equal)
Expand Down
Expand Up @@ -4,7 +4,7 @@ class A {
constructor(delegate) {
}

get getter() {
get getter() : any {
return null;
}

Expand Down

0 comments on commit 8aa88d5

Please sign in to comment.