docs: introduce overloads, generate JSDoc for overloads (#8485)

- Each overload, e.g. for `page.evaluate`, shows a nice autocomplete doc,
  not only the first one.
- We can have multiple overloads directly on the docs page, e.g.
  `test.skip(title, fn)` and `test.skip(condition, description)`.
  These overloads are internally named `Test.skip#1` and all aliased
  to `test.skip`.
This commit is contained in:
Dmitry Gozman 2021-08-27 21:57:40 -07:00 committed by GitHub
parent 2f18c62a19
commit bb5e44fbc4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 2441 additions and 81 deletions

View File

@ -599,12 +599,9 @@ Timeout in milliseconds.
## method: Test.skip#1
## method: Test.skip Declares a skipped test, similarly to [`method: Test.(call)`]. Skipped test is never run.
Skips a test or a group of tests.
Unconditionally skip a test, this is similar syntax to [`method: Test.(call)`]:
```js js-flavor=js ```js js-flavor=js
const { test, expect } = require('@playwright/test'); const { test, expect } = require('@playwright/test');
@ -622,7 +619,72 @@ test.skip('broken test', async ({ page }) => {
}); });
``` ```
Conditionally skip a test with an optional description. In this case, call `test.skip()` inside the test function: ### param: Test.skip#1.title
- `title` <[string]>
Test title.
### param: Test.skip#1.testFunction
- `testFunction` <[function]\([Fixtures], [TestInfo]\)>
Test function that takes one or two arguments: an object with fixtures and optional [TestInfo].
## method: Test.skip#2
Unconditionally skip a test. Test is immediately aborted when you call [`method: Test.skip#2`].
```js js-flavor=js
const { test, expect } = require('@playwright/test');
test('skipped test', async ({ page }) => {
test.skip();
// ...
});
```
```js js-flavor=ts
import { test, expect } from '@playwright/test';
test('skipped test', async ({ page }) => {
test.skip();
// ...
});
```
Unconditionally skip all tests in a file or [`method: Test.describe`] group:
```js js-flavor=js
const { test, expect } = require('@playwright/test');
test.skip();
test('skipped test 1', async ({ page }) => {
// ...
});
test('skipped test 2', async ({ page }) => {
// ...
});
```
```js js-flavor=ts
import { test, expect } from '@playwright/test';
test.skip();
test('skipped test 1', async ({ page }) => {
// ...
});
test('skipped test 2', async ({ page }) => {
// ...
});
```
## method: Test.skip#3
Conditionally skip a test with an optional description.
```js js-flavor=js ```js js-flavor=js
const { test, expect } = require('@playwright/test'); const { test, expect } = require('@playwright/test');
@ -642,7 +704,42 @@ test('skip in WebKit', async ({ page, browserName }) => {
}); });
``` ```
Conditionally skip all tests in a file or [`method: Test.describe`] group: Skip from [`method: Test.beforeEach`] hook:
```js js-flavor=js
const { test, expect } = require('@playwright/test');
test.beforeEach(async ({ page }) => {
test.skip(process.env.APP_VERSION === 'v1', 'There are no settings in v1');
await page.goto('/settings');
});
```
```js js-flavor=ts
import { test, expect } from '@playwright/test';
test.beforeEach(async ({ page }) => {
test.skip(process.env.APP_VERSION === 'v1', 'There are no settings in v1');
await page.goto('/settings');
});
```
### param: Test.skip#3.condition
- `condition` <[boolean]>
A skip condition. Test or tests are skipped when the condition is `true`.
### param: Test.skip#3.description
- `description` <[void]|[string]>
An optional description that will be reflected in a test report.
## method: Test.skip#4
Conditionally skips all tests in a file or [`method: Test.describe`] group.
```js js-flavor=js ```js js-flavor=js
const { test, expect } = require('@playwright/test'); const { test, expect } = require('@playwright/test');
@ -670,35 +767,16 @@ test('skip in WebKit 2', async ({ page }) => {
}); });
``` ```
Skip from a hook:
```js js-flavor=js ### param: Test.skip#4.condition
const { test, expect } = require('@playwright/test'); - `callback` <[function]\([Fixtures]\):[boolean]>
test.beforeEach(async ({ page }) => { A function that returns whether to skip, based on test fixtures. Test or tests are skipped when the return value is `true`.
test.skip(process.env.APP_VERSION === 'v1', 'There are no settings in v1');
await page.goto('/settings');
});
```
```js js-flavor=ts ### param: Test.skip#4.description
import { test, expect } from '@playwright/test'; - `description` <[void]|[string]>
test.beforeEach(async ({ page }) => { An optional description that will be reflected in a test report.
test.skip(process.env.APP_VERSION === 'v1', 'There are no settings in v1');
await page.goto('/settings');
});
```
### param: Test.skip.condition
- `titleOrCondition` <[string]|[void]|[boolean]|[function]\([Fixtures]\):[boolean]>
When used with `test.skip('test', () => {})` notation, first argument is a test title. Otherwise it is an optional skip condition - either a boolean value, or a function that takes a fixtures object and returns a boolean. Test or tests are skipped when the condition is `true`.
### param: Test.skip.description
- `testFunctionOrDescription` <[function]\([Fixtures], [TestInfo]\)|[void]|[string]>
When used with `test.skip('test', () => {})` notation, second argument is a test function. Otherwise it is an optional description that will be reflected in a test report.

View File

@ -97,7 +97,7 @@ An error thrown during test execution, if any.
- type: <[TestStatus]<"passed"|"failed"|"timedOut"|"skipped">> - type: <[TestStatus]<"passed"|"failed"|"timedOut"|"skipped">>
Expected status for the currently running test. This is usually `'passed'`, except for a few cases: Expected status for the currently running test. This is usually `'passed'`, except for a few cases:
* `'skipped'` for skipped tests, e.g. with [`method: Test.skip`]; * `'skipped'` for skipped tests, e.g. with [`method: Test.skip#2`];
* `'failed'` for tests marked as failed with [`method: Test.fail`]. * `'failed'` for tests marked as failed with [`method: Test.fail`].
Expected status is usually compared with the actual [`property: TestInfo.status`]: Expected status is usually compared with the actual [`property: TestInfo.status`]:
@ -246,7 +246,7 @@ Timeout in milliseconds.
## method: TestInfo.skip ## method: TestInfo.skip
Skips the currently running test. This is similar to [`method: Test.skip`]. Skips the currently running test. This is similar to [`method: Test.skip#2`].
### param: TestInfo.skip.condition ### param: TestInfo.skip.condition
- `condition` <[void]|[boolean]> - `condition` <[void]|[boolean]>

View File

@ -18,7 +18,7 @@ Learn more about [test annotations](./test-annotations.md).
- type: <[TestStatus]<"passed"|"failed"|"timedOut"|"skipped">> - type: <[TestStatus]<"passed"|"failed"|"timedOut"|"skipped">>
Expected test status. Expected test status.
* Tests marked as [`method: Test.skip`] or [`method: Test.fixme`] are expected to be `'skipped'`. * Tests marked as [`method: Test.skip#1`] or [`method: Test.fixme`] are expected to be `'skipped'`.
* Tests marked as [`method: Test.fail`] are expected to be `'failed'`. * Tests marked as [`method: Test.fail`] are expected to be `'failed'`.
* Other tests are expected to be `'passed'`. * Other tests are expected to be `'passed'`.

1144
types/test.d.ts vendored

File diff suppressed because it is too large Load Diff

View File

@ -113,8 +113,7 @@ export interface TestCase {
titlePath(): string[]; titlePath(): string[];
/** /**
* Expected test status. * Expected test status.
* - Tests marked as * - Tests marked as [test.skip(title, testFunction)](https://playwright.dev/docs/api/class-test#test-skip-1) or
* [test.skip(titleOrCondition, testFunctionOrDescription)](https://playwright.dev/docs/api/class-test#test-skip) or
* [test.fixme([condition, description])](https://playwright.dev/docs/api/class-test#test-fixme) are expected to be * [test.fixme([condition, description])](https://playwright.dev/docs/api/class-test#test-fixme) are expected to be
* `'skipped'`. * `'skipped'`.
* - Tests marked as [test.fail([condition, description])](https://playwright.dev/docs/api/class-test#test-fail) are * - Tests marked as [test.fail([condition, description])](https://playwright.dev/docs/api/class-test#test-fail) are

1185
types/types.d.ts vendored

File diff suppressed because it is too large Load Diff

View File

@ -136,7 +136,7 @@ class ApiParser {
const clazz = this.classes.get(className); const clazz = this.classes.get(className);
if (!clazz) if (!clazz)
throw new Error('Invalid class ' + className); throw new Error('Invalid class ' + className);
const method = clazz.membersArray.find(m => m.kind === 'method' && m.alias === methodName); const method = clazz.membersArray.find(m => m.kind === 'method' && m.name === methodName);
if (!method) if (!method)
throw new Error(`Invalid method ${className}.${methodName} when parsing: ${match[0]}`); throw new Error(`Invalid method ${className}.${methodName} when parsing: ${match[0]}`);
if (!name) if (!name)

View File

@ -312,6 +312,12 @@ Documentation.Member = class {
}; };
this.async = false; this.async = false;
this.alias = name; this.alias = name;
this.overloadIndex = 0;
if (name.includes('#')) {
const match = name.match(/(.*)#(.*)/);
this.alias = match[1];
this.overloadIndex = (+match[2]) - 1;
}
/** /**
* Param is true and option false * Param is true and option false
* @type {Boolean} * @type {Boolean}

View File

@ -87,9 +87,14 @@ class TypesGenerator {
return ''; return '';
handledClasses.add(className); handledClasses.add(className);
return this.writeComment(docClass.comment) + '\n'; return this.writeComment(docClass.comment) + '\n';
}, (className, methodName) => { }, (className, methodName, overloadIndex) => {
const docClass = this.docClassForName(className, docsOnlyClassMapping); const docClass = this.docClassForName(className, docsOnlyClassMapping);
const method = docClass ? docClass.membersArray.find(m => m.alias === methodName) : undefined; let method;
if (docClass) {
const methods = docClass.membersArray.filter(m => m.alias === methodName && m.kind !== 'event').sort((a, b) => a.overloadIndex - b.overloadIndex);
// Use the last overload when not enough overloads are defined in docs.
method = methods.find(m => m.overloadIndex === overloadIndex) || methods[methods.length - 1];
}
if (docsOnlyClassMapping && !method) if (docsOnlyClassMapping && !method)
return ''; return '';
this.handledMethods.add(`${className}.${methodName}`); this.handledMethods.add(`${className}.${methodName}`);
@ -381,7 +386,7 @@ class TypesGenerator {
argsFromMember(member, indent, ...namespace) { argsFromMember(member, indent, ...namespace) {
if (member.kind === 'property') if (member.kind === 'property')
return ''; return '';
return '(' + member.argsArray.map(arg => `${this.nameForProperty(arg)}: ${this.stringifyComplexType(arg.type, indent, ...namespace, member.name, arg.name)}`).join(', ') + ')'; return '(' + member.argsArray.map(arg => `${this.nameForProperty(arg)}: ${this.stringifyComplexType(arg.type, indent, ...namespace, member.alias, arg.alias)}`).join(', ') + ')';
} }
/** /**

View File

@ -229,10 +229,8 @@ export interface TestType<TestArgs extends KeyValue, WorkerArgs extends KeyValue
}; };
skip(title: string, testFunction: (args: TestArgs, testInfo: TestInfo) => Promise<void> | void): void; skip(title: string, testFunction: (args: TestArgs, testInfo: TestInfo) => Promise<void> | void): void;
skip(): void; skip(): void;
skip(condition: boolean): void; skip(condition: boolean, description?: string): void;
skip(condition: boolean, description: string): void; skip(callback: (args: TestArgs & WorkerArgs) => boolean, description?: string): void;
skip(callback: (args: TestArgs & WorkerArgs) => boolean): void;
skip(callback: (args: TestArgs & WorkerArgs) => boolean, description: string): void;
fixme(): void; fixme(): void;
fixme(condition: boolean): void; fixme(condition: boolean): void;
fixme(condition: boolean, description: string): void; fixme(condition: boolean, description: string): void;

View File

@ -20,7 +20,7 @@ const ts = require('typescript');
/** /**
* @param {string} filePath * @param {string} filePath
* @param {(className: string) => string} commentForClass * @param {(className: string) => string} commentForClass
* @param {(className: string, methodName: string) => string} commentForMethod * @param {(className: string, methodName: string, overloadIndex: number) => string} commentForMethod
* @param {(className: string) => string} extraForClass * @param {(className: string) => string} extraForClass
*/ */
async function parseOverrides(filePath, commentForClass, commentForMethod, extraForClass) { async function parseOverrides(filePath, commentForClass, commentForMethod, extraForClass) {
@ -76,14 +76,17 @@ async function parseOverrides(filePath, commentForClass, commentForMethod, extra
for (const [name, member] of symbol.members || []) { for (const [name, member] of symbol.members || []) {
if (member.flags & ts.SymbolFlags.TypeParameter) if (member.flags & ts.SymbolFlags.TypeParameter)
continue; continue;
if (!member.valueDeclaration) if (!member.declarations)
continue; continue;
const pos = member.valueDeclaration.getStart(file, false); for (let index = 0; index < member.declarations.length; index++) {
const declaration = member.declarations[index];
const pos = declaration.getStart(file, false);
replacers.push({ replacers.push({
pos, pos,
text: commentForMethod(className, name), text: commentForMethod(className, name, index),
}); });
} }
}
replacers.push({ replacers.push({
pos: node.getEnd(file) - 1, pos: node.getEnd(file) - 1,
text: extraForClass(className), text: extraForClass(className),