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
Skips a test or a group of tests.
Unconditionally skip a test, this is similar syntax to [`method: Test.(call)`]:
Declares a skipped test, similarly to [`method: Test.(call)`]. Skipped test is never run.
```js js-flavor=js
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
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
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
const { test, expect } = require('@playwright/test');
### param: Test.skip#4.condition
- `callback` <[function]\([Fixtures]\):[boolean]>
test.beforeEach(async ({ page }) => {
test.skip(process.env.APP_VERSION === 'v1', 'There are no settings in v1');
await page.goto('/settings');
});
```
A function that returns whether to skip, based on test fixtures. Test or tests are skipped when the return value is `true`.
```js js-flavor=ts
import { test, expect } from '@playwright/test';
### param: Test.skip#4.description
- `description` <[void]|[string]>
test.beforeEach(async ({ page }) => {
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.
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">>
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`].
Expected status is usually compared with the actual [`property: TestInfo.status`]:
@ -246,7 +246,7 @@ Timeout in milliseconds.
## 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
- `condition` <[void]|[boolean]>

View File

@ -18,7 +18,7 @@ Learn more about [test annotations](./test-annotations.md).
- type: <[TestStatus]<"passed"|"failed"|"timedOut"|"skipped">>
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'`.
* 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[];
/**
* Expected test status.
* - Tests marked as
* [test.skip(titleOrCondition, testFunctionOrDescription)](https://playwright.dev/docs/api/class-test#test-skip) or
* - Tests marked as [test.skip(title, testFunction)](https://playwright.dev/docs/api/class-test#test-skip-1) or
* [test.fixme([condition, description])](https://playwright.dev/docs/api/class-test#test-fixme) are expected to be
* `'skipped'`.
* - 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);
if (!clazz)
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)
throw new Error(`Invalid method ${className}.${methodName} when parsing: ${match[0]}`);
if (!name)

View File

@ -312,6 +312,12 @@ Documentation.Member = class {
};
this.async = false;
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
* @type {Boolean}

View File

@ -87,9 +87,14 @@ class TypesGenerator {
return '';
handledClasses.add(className);
return this.writeComment(docClass.comment) + '\n';
}, (className, methodName) => {
}, (className, methodName, overloadIndex) => {
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)
return '';
this.handledMethods.add(`${className}.${methodName}`);
@ -381,7 +386,7 @@ class TypesGenerator {
argsFromMember(member, indent, ...namespace) {
if (member.kind === 'property')
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(): void;
skip(condition: boolean): void;
skip(condition: boolean, description: string): void;
skip(callback: (args: TestArgs & WorkerArgs) => boolean): void;
skip(callback: (args: TestArgs & WorkerArgs) => boolean, description: string): void;
skip(condition: boolean, description?: string): void;
skip(callback: (args: TestArgs & WorkerArgs) => boolean, description?: string): void;
fixme(): void;
fixme(condition: boolean): void;
fixme(condition: boolean, description: string): void;

View File

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