chore: generate JSDoc for testing types from md reference (#7799)

This commit is contained in:
Dmitry Gozman 2021-07-27 19:10:55 -07:00 committed by GitHub
parent cc43f9339f
commit 34b96a5759
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 3235 additions and 1157 deletions

2807
types/test.d.ts vendored

File diff suppressed because it is too large Load Diff

View File

@ -1,3 +1,4 @@
// This file is generated by /utils/generate_types/index.js
/**
* Copyright (c) Microsoft Corporation.
*
@ -18,187 +19,195 @@ import type { FullConfig, TestStatus, TestError } from './test';
export type { FullConfig, TestStatus, TestError } from './test';
/**
* Location where TestCase or Suite was defined.
* Represents a location in the source code where [TestCase] or [Suite] is defined.
*/
export interface Location {
/**
* Path to the file.
* Path to the source file.
*/
file: string;
/**
* Line number in the file.
* Line number in the source file.
*/
line: number;
/**
* Column number in the file.
* Column number in the source file.
*/
column: number;
}
/**
* A group of tests. All tests are reported in the following hierarchy:
* - Root suite
* - Project suite #1 (for each project)
* - File suite #1 (for each file in the project)
* - Suites for any describe() calls
* - TestCase #1 defined in the file or describe() group
* - TestCase #2
* ... < more test cases >
* `Suite` is a group of tests. All tests in Playwright Test form the following hierarchy:
* - Root suite has a child suite for each [TestProject].
* - Project suite #1. Has a child suite for each test file in the project.
* - File suite #1
* - [TestCase] #1
* - [TestCase] #2
* - Suite corresponding to a
* [test.describe(title, callback)](https://playwright.dev/docs/api/class-test#test-describe) group
* - [TestCase] #1 in a group
* - [TestCase] #2 in a group
* - < more test cases ... >
* - File suite #2
* ... < more file suites >
* - Second project suite
* ... < more project suites >
* - < more file suites ... >
* - Project suite #2
* - < more project suites ... >
*
* Reporter is given a root suite in the
* [reporter.onBegin(config, suite)](https://playwright.dev/docs/api/class-reporter#reporter-on-begin) method.
*/
export interface Suite {
/**
* Suite title:
* - Empty for root suite.
* - Project name for project suite.
* - File path for file suite.
* - Title passed to describe() for describe suites.
* Suite title.
* - Empty for root suite.
* - Project name for project suite.
* - File path for file suite.
* - Title passed to [test.describe(title, callback)](https://playwright.dev/docs/api/class-test#test-describe) for a
* group suite.
*/
title: string;
/**
* Location where the suite is defined.
* Location in the source where the suite is defined. Missing for root and project suites.
*/
location?: Location;
/**
* Child suites.
* Child suites. See [Suite] for the hierarchy of suites.
*/
suites: Suite[];
/**
* Test cases in the suite. Note that only test cases defined directly in this suite
* are in the list. Any test cases defined in nested describe() groups are listed
* in the child `suites`.
* Test cases in the suite. Note that only test cases defined directly in this suite are in the list. Any test cases
* defined in nested [test.describe(title, callback)](https://playwright.dev/docs/api/class-test#test-describe) groups are
* listed in the child [suite.suites](https://playwright.dev/docs/api/class-suite#suite-suites).
*/
tests: TestCase[];
/**
* A list of titles from the root down to this suite.
* Returns a list of titles from the root down to this suite.
*/
titlePath(): string[];
/**
* Returns the list of all test cases in this suite and its descendants.
* Returns the list of all test cases in this suite and its descendants, as opposite to
* [suite.tests](https://playwright.dev/docs/api/class-suite#suite-tests).
*/
allTests(): TestCase[];
}
/**
* `TestCase` corresponds to a test() call in a test file. When a single test() is
* running in multiple projects or repeated multiple times, it will have multiple
* `TestCase` objects in corresponding projects' suites.
* `TestCase` corresponds to every [test.(call)(title, testFunction)](https://playwright.dev/docs/api/class-test#test-call)
* call in a test file. When a single
* [test.(call)(title, testFunction)](https://playwright.dev/docs/api/class-test#test-call) is running in multiple projects
* or repeated multiple times, it will have multiple `TestCase` objects in corresponding projects' suites.
*/
export interface TestCase {
/**
* Test title as passed to the test() call.
* Test title as passed to the [test.(call)(title, testFunction)](https://playwright.dev/docs/api/class-test#test-call)
* call.
*/
title: string;
/**
* Location where the test is defined.
* Location in the source where the test is defined.
*/
location: Location;
/**
* A list of titles from the root down to this test.
* Returns a list of titles from the root down to this test.
*/
titlePath(): string[];
/**
* Expected status.
* - Tests marked as test.skip() or test.fixme() are expected to be 'skipped'.
* - Tests marked as test.fail() are expected to be 'failed'.
* - Other tests are expected to be 'passed'.
* Expected test status.
* - Tests marked as [test.skip([condition, description])](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
* `'skipped'`.
* - Tests marked as [test.fail([condition, description])](https://playwright.dev/docs/api/class-test#test-fail) are
* expected to be `'failed'`.
* - Other tests are expected to be `'passed'`.
*
* See also [testResult.status](https://playwright.dev/docs/api/class-testresult#test-result-status) for the actual status.
*/
expectedStatus: TestStatus;
/**
* The timeout given to the test. Affected by timeout in the configuration file,
* and calls to test.setTimeout() or test.slow().
* The timeout given to the test. Affected by
* [testConfig.timeout](https://playwright.dev/docs/api/class-testconfig#test-config-timeout),
* [testProject.timeout](https://playwright.dev/docs/api/class-testproject#test-project-timeout),
* [test.setTimeout(timeout)](https://playwright.dev/docs/api/class-test#test-set-timeout),
* [test.slow([condition, description])](https://playwright.dev/docs/api/class-test#test-slow) and
* [testInfo.setTimeout(timeout)](https://playwright.dev/docs/api/class-testinfo#test-info-set-timeout).
*/
timeout: number;
/**
* Annotations collected for this test. For example, calling
* `test.skip(true, 'just because')` will produce an annotation
* `{ type: 'skip', description: 'just because' }`.
* The list of annotations applicable to the current test. Includes annotations from the test, annotations from all
* [test.describe(title, callback)](https://playwright.dev/docs/api/class-test#test-describe) groups the test belongs to
* and file-level annotations for the test file.
*
* Annotations are available during test execution through
* [testInfo.annotations](https://playwright.dev/docs/api/class-testinfo#test-info-annotations).
*
* Learn more about [test annotations](https://playwright.dev/docs/test-annotations).
*/
annotations: { type: string, description?: string }[];
/**
* The maxmium number of retries given to this test in the configuration.
* The maximum number of retries given to this test in the configuration.
*
* Learn more about [test retries](https://playwright.dev/docs/test-retries).
*/
retries: number;
/**
* Results for each run of this test.
*/
results: TestResult[];
/**
* Testing outcome for this test. Note that outcome does not directly match to the status:
* - Test that is expected to fail and actually fails is 'expected'.
* - Test that passes on a second retry is 'flaky'.
* Testing outcome for this test. Note that outcome is not the same as
* [testResult.status](https://playwright.dev/docs/api/class-testresult#test-result-status):
* - Test that is expected to fail and actually fails is `'expected'`.
* - Test that passes on a second retry is `'flaky'`.
*/
outcome(): 'skipped' | 'expected' | 'unexpected' | 'flaky';
/**
* Whether the test is considered running fine.
* Non-ok tests fail the test run with non-zero exit code.
* Whether the test is considered running fine. Non-ok tests fail the test run with non-zero exit code.
*/
ok(): boolean;
}
/**
* A result of a single test run.
* A result of a single [TestCase] run.
*/
export interface TestResult {
/**
* When test is retries multiple times, each retry attempt is given a sequential number.
*
* Learn more about [test retries](https://playwright.dev/docs/test-retries).
*/
retry: number;
/**
* Index of the worker where the test was run.
*
* Learn more about [parallelism and sharding](https://playwright.dev/docs/test-parallel) with Playwright Test.
*/
workerIndex: number;
/**
* Test run start time.
* Start time of this particular test run.
*/
startTime: Date;
/**
* Running time in milliseconds.
*/
duration: number;
/**
* The status of this test result.
* The status of this test result. See also
* [testCase.expectedStatus](https://playwright.dev/docs/api/class-testcase#test-case-expected-status).
*/
status?: TestStatus;
/**
* An error from this test result, if any.
* An error thrown during the test execution, if any.
*/
error?: TestError;
/**
* Any attachments created during the test run.
* The list of files or buffers attached during the test execution through
* [testInfo.attachments](https://playwright.dev/docs/api/class-testinfo#test-info-attachments).
*/
attachments: { name: string, path?: string, body?: Buffer, contentType: string }[];
/**
* Anything written to the standard output during the test run.
*/
stdout: (string | Buffer)[];
/**
* Anything written to the standard error during the test run.
*/
@ -220,47 +229,138 @@ export interface FullResult {
}
/**
* Test runner notifies reporter about various events during the test run.
* Test runner notifies the reporter about various events during test execution. All methods of the reporter are optional.
*
* You can create a custom reporter my implementing a class with some of the reporter methods. Make sure to export this
* class as default.
*
* ```js js-flavor=js
* // my-awesome-reporter.js
* // @ts-check
*
* /** @implements {import('@playwright/test/reporter').Reporter} *\/
* class MyReporter {
* onBegin(config, suite) {
* console.log(`Starting the run with ${suite.allTests().length} tests`);
* }
*
* onTestBegin(test) {
* console.log(`Starting test ${test.title}`);
* }
*
* onTestEnd(test, result) {
* console.log(`Finished test ${test.title}: ${result.status}`);
* }
*
* onEnd(result) {
* console.log(`Finished the run: ${result.status}`);
* }
* }
*
* module.exports = MyReporter;
* ```
*
* ```js js-flavor=ts
* // playwright.config.ts
* import { Reporter } from '@playwright/test/reporter';
*
* class MyReporter implements Reporter {
* onBegin(config, suite) {
* console.log(`Starting the run with ${suite.allTests().length} tests`);
* }
*
* onTestBegin(test) {
* console.log(`Starting test ${test.title}`);
* }
*
* onTestEnd(test, result) {
* console.log(`Finished test ${test.title}: ${result.status}`);
* }
*
* onEnd(result) {
* console.log(`Finished the run: ${result.status}`);
* }
* }
* export default MyReporter;
* ```
*
* Now use this reporter with [testConfig.reporter](https://playwright.dev/docs/api/class-testconfig#test-config-reporter).
*
* ```js js-flavor=js
* // playwright.config.js
* // @ts-check
*
* /** @type {import('@playwright/test').PlaywrightTestConfig} *\/
* const config = {
* reporter: './my-awesome-reporter.js',
* };
*
* module.exports = config;
* ```
*
* ```js js-flavor=ts
* // playwright.config.ts
* import { PlaywrightTestConfig } from '@playwright/test';
*
* const config: PlaywrightTestConfig = {
* reporter: './my-awesome-reporter.ts',
* };
* export default config;
* ```
*
* Learn more about [reporters](https://playwright.dev/docs/test-reporters).
*/
export interface Reporter {
/**
* Called once before running tests.
* All tests have been already discovered and put into a hierarchy, see `Suite` description.
* Called once before running tests. All tests have been already discovered and put into a hierarchy of [Suite]s.
* @param config Resolved configuration.
* @param suite The root suite that contains all projects, files and test cases.
*/
onBegin?(config: FullConfig, suite: Suite): void;
/**
* Called after a test has been started in the worker process.
* @param test Test that has been started.
*/
onTestBegin?(test: TestCase): void;
/**
* Called when something has been written to the standard output in the worker process.
* When `test` is given, output happened while the test was running.
* @param chunk Output chunk.
* @param test Test that was running. Note that output may happen when to test is running, in which case this will be [void].
*/
onStdOut?(chunk: string | Buffer, test?: TestCase): void;
/**
* Called when something has been written to the standard error in the worker process.
* When `test` is given, output happened while the test was running.
* @param chunk Output chunk.
* @param test Test that was running. Note that output may happen when to test is running, in which case this will be [void].
*/
onStdErr?(chunk: string | Buffer, test?: TestCase): void;
/**
* Called after a test has been finished in the worker process.
* @param test Test that has been finished.
* @param result Result of the test run.
*/
onTestEnd?(test: TestCase, result: TestResult): void;
/**
* Called on some global error, for example unhandled expection in the worker process.
* Called on some global error, for example unhandled exception in the worker process.
* @param error The error.
*/
onError?(error: TestError): void;
/**
* Called after all tests has been run, or when testing has been interrupted.
* Called after all tests has been run, or testing has been interrupted. Note that this method may return a [Promise] and
* Playwright Test will await it.
* @param result Result of the full test run. - `'passed'` - Everything went as expected.
* - `'failed'` - Any test has failed.
* - `'timedout'` - The
* [testConfig.globalTimeout](https://playwright.dev/docs/api/class-testconfig#test-config-global-timeout) has been
* reached.
* - `'interrupted'` - Interrupted by the user.
*/
onEnd?(result: FullResult): void | Promise<void>;
}
// This is required to not export everything by default. See https://github.com/Microsoft/TypeScript/issues/19545#issuecomment-340490459
export {};

View File

@ -26,419 +26,482 @@ const {parseOverrides} = require('./parseOverrides');
const exported = require('./exported.json');
const { parseApi } = require('../doclint/api_parser');
const objectDefinitions = [];
const handledMethods = new Set();
/** @type {Documentation} */
let documentation;
let hadChanges = false;
/** @typedef {import('../doclint/documentation').Member} Member */
Error.stackTraceLimit = 50;
class TypesGenerator {
/**
* @param {Documentation} documentation
*/
constructor(documentation) {
/** @type {Array<{name: string, properties: Member[]}>} */
this.objectDefinitions = [];
/** @type {Set<string>} */
this.handledMethods = new Set();
this.documentation = documentation;
}
/**
* @param {string} overridesFile
* @param {Map<string, string>=} docsOnlyClassMapping
* @returns {Promise<string>}
*/
async generateTypes(overridesFile, docsOnlyClassMapping) {
this.documentation.filterForLanguage('js');
this.documentation.copyDocsFromSuperclasses([]);
const createMarkdownLink = (member, text) => {
const className = toKebabCase(member.clazz.name);
const memberName = toKebabCase(member.name);
let hash = null
if (member.kind === 'property' || member.kind === 'method')
hash = `${className}-${memberName}`.toLowerCase();
else if (member.kind === 'event')
hash = `${className}-event-${memberName}`.toLowerCase();
return `[${text}](https://playwright.dev/docs/api/class-${member.clazz.name.toLowerCase()}#${hash})`;
};
this.documentation.setLinkRenderer(item => {
const { clazz, member, param, option } = item;
if (param)
return `\`${param}\``;
if (option)
return `\`${option}\``;
if (clazz)
return `[${clazz.name}]`;
if (member.kind === 'method')
return createMarkdownLink(member, `${member.clazz.varName}.${member.alias}(${this.renderJSSignature(member.argsArray)})`);
if (member.kind === 'event')
return createMarkdownLink(member, `${member.clazz.varName}.on('${member.alias.toLowerCase()}')`);
if (member.kind === 'property')
return createMarkdownLink(member, `${member.clazz.varName}.${member.alias}`);
throw new Error('Unknown member kind ' + member.kind);
});
this.documentation.generateSourceCodeComments();
const handledClasses = new Set();
const overrides = await parseOverrides(overridesFile, className => {
const docClass = this.docClassForName(className, docsOnlyClassMapping);
if (!docClass)
return '';
handledClasses.add(className);
return this.writeComment(docClass.comment) + '\n';
}, (className, methodName) => {
const docClass = this.docClassForName(className, docsOnlyClassMapping);
const method = docClass ? docClass.membersArray.find(m => m.alias === methodName) : undefined;
if (docsOnlyClassMapping && !method)
return '';
this.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 this.memberJSDOC(method, ' ').trimLeft();
}, (className) => {
const docClass = this.docClassForName(className, docsOnlyClassMapping);
return (!docsOnlyClassMapping && docClass) ? this.classBody(docClass) : '';
});
const classes = this.documentation.classesArray.filter(cls => !handledClasses.has(cls.name));
return [
`// This file is generated by ${__filename.substring(path.join(__dirname, '..', '..').length).split(path.sep).join(path.posix.sep)}`,
overrides,
'',
docsOnlyClassMapping ? '' : classes.map(classDesc => this.classToString(classDesc)).join('\n'),
this.objectDefinitionsToString(overrides),
'',
].join('\n');
}
/**
* @param {string} name
* @param {Map<string, string> | undefined} docsOnlyClassMapping
*/
docClassForName(name, docsOnlyClassMapping) {
name = (docsOnlyClassMapping ? docsOnlyClassMapping.get(name) : undefined) || name;
const docClass = this.documentation.classes.get(name);
if (!docClass && !docsOnlyClassMapping)
throw new Error(`Unknown override class ${name}`);
return docClass;
}
/**
* @param {string} overriddes
*/
objectDefinitionsToString(overriddes) {
let definition;
const parts = [];
const internalWords = new Set(overriddes.split(/[^\w$]/g));
while ((definition = this.objectDefinitions.pop())) {
const {name, properties} = definition;
const shouldExport = !!exported[name];
const usedInternally = internalWords.has(name);
if (!usedInternally && !shouldExport)
continue;
parts.push(`${shouldExport ? 'export ' : ''}interface ${name} ${this.stringifyObjectType(properties, name, '')}\n`)
}
return parts.join('\n');
}
nameForProperty(member) {
return (member.required || member.alias.startsWith('...')) ? member.alias : member.alias + '?';
}
/**
* @param {Documentation.Class} classDesc
*/
classToString(classDesc) {
const parts = [];
if (classDesc.comment) {
parts.push(this.writeComment(classDesc.comment))
}
parts.push(`export interface ${classDesc.name} ${classDesc.extends ? `extends ${classDesc.extends} ` : ''}{`);
parts.push(this.classBody(classDesc));
parts.push('}\n');
return parts.join('\n');
}
/**
* @param {string} type
*/
argNameForType(type) {
if (type === 'void')
return null;
if (type.includes('{'))
return 'data';
return (type[0].toLowerCase() + type.slice(1)).replace(/\|/g, 'Or');
}
/**
* @param {Documentation.Class} classDesc
*/
hasUniqueEvents(classDesc) {
if (!classDesc.events.size)
return false;
const parent = this.parentClass(classDesc);
if (!parent)
return true;
return Array.from(classDesc.events.keys()).some(eventName => !parent.events.has(eventName));
}
/**
* @param {Documentation.Class} classDesc
*/
createEventDescriptions(classDesc) {
if (!this.hasUniqueEvents(classDesc))
return [];
const descriptions = [];
for (let [eventName, value] of classDesc.events) {
eventName = eventName.toLowerCase();
const type = this.stringifyComplexType(value && value.type, '', classDesc.name, eventName, 'payload');
const argName = this.argNameForType(type);
const params = argName ? `${argName}: ${type}` : '';
descriptions.push({
type,
params,
eventName,
comment: value.comment
});
}
return descriptions;
}
/**
* @param {Documentation.Class} classDesc
*/
classBody(classDesc) {
const parts = [];
const eventDescriptions = this.createEventDescriptions(classDesc);
const commentForMethod = {
off: 'Removes an event listener added by `on` or `addListener`.',
removeListener: 'Removes an event listener added by `on` or `addListener`.',
once: 'Adds an event listener that will be automatically removed after it is triggered once. See `addListener` for more information about this event.'
}
for (const method of ['on', 'once', 'addListener', 'removeListener', 'off']) {
for (const {eventName, params, comment} of eventDescriptions) {
if ((method === 'on' || method === 'addListener') && comment)
parts.push(this.writeComment(comment, ' '));
else
parts.push(this.writeComment(commentForMethod[method], ' '));
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 '';
if (member.alias === 'waitForEvent') {
const parts = [];
for (const {eventName, params, comment, type} of eventDescriptions) {
if (comment)
parts.push(this.writeComment(comment, ' '));
parts.push(` ${member.alias}(event: '${eventName}', optionsOrPredicate?: { predicate?: (${params}) => boolean | Promise<boolean>, timeout?: number } | ((${params}) => boolean | Promise<boolean>)): Promise<${type}>;\n`);
}
return parts.join('\n');
}
const jsdoc = this.memberJSDOC(member, ' ');
const args = this.argsFromMember(member, ' ', classDesc.name);
let type = this.stringifyComplexType(member.type, ' ', classDesc.name, member.alias);
if (member.async)
type = `Promise<${type}>`;
// do this late, because we still want object definitions for overridden types
if (!this.hasOwnMethod(classDesc, member.alias))
return '';
return `${jsdoc}${member.alias}${args}: ${type};`
}).filter(x => x).join('\n\n'));
return parts.join('\n');
}
/**
* @param {Documentation.Class} classDesc
* @param {string} methodName
*/
hasOwnMethod(classDesc, methodName) {
if (this.handledMethods.has(`${classDesc.name}.${methodName}`))
return false;
while (classDesc = this.parentClass(classDesc)) {
if (classDesc.members.has(methodName))
return false;
}
return true;
}
/**
* @param {Documentation.Class} classDesc
*/
parentClass(classDesc) {
if (!classDesc.extends)
return null;
return this.documentation.classes.get(classDesc.extends);
}
writeComment(comment, indent = '') {
const parts = [];
const out = [];
const pushLine = (line) => {
if (line || out[out.length - 1])
out.push(line)
};
let skipExample = false;
for (let line of comment.split('\n')) {
const match = line.match(/```(\w+)/);
if (match) {
const lang = match[1];
skipExample = !["html", "yml", "bash", "js"].includes(lang);
} else if (skipExample && line.trim().startsWith('```')) {
skipExample = false;
continue;
}
if (!skipExample)
pushLine(line);
}
comment = out.join('\n');
comment = comment.replace(/\[([^\]]+)\]\(\.\/([^\)]+)\)/g, (match, p1, p2) => {
return `[${p1}](https://playwright.dev/docs/${p2.replace('.md', '')})`;
});
parts.push(indent + '/**');
parts.push(...comment.split('\n').map(line => indent + ' * ' + line.replace(/\*\//g, '*\\/')));
parts.push(indent + ' */');
return parts.join('\n');
}
/**
* @param {Documentation.Type} type
*/
stringifyComplexType(type, indent, ...namespace) {
if (!type)
return 'void';
return this.stringifySimpleType(type, indent, ...namespace);
}
stringifyObjectType(properties, name, indent = '') {
const parts = [];
parts.push(`{`);
parts.push(properties.map(member => `${this.memberJSDOC(member, indent + ' ')}${this.nameForProperty(member)}${this.argsFromMember(member, indent + ' ', name)}: ${this.stringifyComplexType(member.type, indent + ' ', name, member.name)};`).join('\n\n'));
parts.push(indent + '}');
return parts.join('\n');
}
/**
* @param {Documentation.Type=} type
* @returns{string}
*/
stringifySimpleType(type, indent = '', ...namespace) {
if (!type)
return 'void';
if (type.name === 'Object' && type.templates) {
const keyType = this.stringifySimpleType(type.templates[0], indent, ...namespace);
const valueType = this.stringifySimpleType(type.templates[1], indent, ...namespace);
return `{ [key: ${keyType}]: ${valueType}; }`;
}
let out = type.name;
if (out === 'int' || out === 'float')
out = 'number';
if (type.name === 'Object' && type.properties && type.properties.length) {
const name = namespace.map(n => n[0].toUpperCase() + n.substring(1)).join('');
const shouldExport = exported[name];
const properties = namespace[namespace.length -1] === 'options' ? type.sortedProperties() : type.properties;
this.objectDefinitions.push({name, properties});
if (shouldExport) {
out = name;
} else {
out = this.stringifyObjectType(properties, name, indent);
}
}
if (type.args) {
const stringArgs = type.args.map(a => ({
type: this.stringifySimpleType(a, indent, ...namespace),
name: a.name.toLowerCase()
}));
out = `((${stringArgs.map(({name, type}) => `${name}: ${type}`).join(', ')}) => ${this.stringifySimpleType(type.returnType, indent, ...namespace)})`;
} else if (type.name === 'function') {
out = 'Function';
}
if (out === 'path')
return 'string';
if (out === 'Any')
return 'any';
if (type.templates)
out += '<' + type.templates.map(t => this.stringifySimpleType(t, indent, ...namespace)).join(', ') + '>';
if (type.union)
out = type.union.map(t => this.stringifySimpleType(t, indent, ...namespace)).join('|');
return out.trim();
}
/**
* @param {Documentation.Member} member
*/
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(', ') + ')';
}
/**
* @param {Documentation.Member} member
* @param {string} indent
*/
memberJSDOC(member, indent) {
const lines = [];
if (member.comment)
lines.push(...member.comment.split('\n'));
if (member.deprecated)
lines.push('@deprecated');
lines.push(...member.argsArray.map(arg => `@param ${arg.alias.replace(/\./g, '')} ${arg.comment.replace('\n', ' ')}`));
if (!lines.length)
return indent;
return this.writeComment(lines.join('\n'), indent) + '\n' + indent;
}
/**
* @param {Documentation.Member[]} args
*/
renderJSSignature(args) {
const tokens = [];
let hasOptional = false;
for (const arg of args) {
const name = arg.alias;
const optional = !arg.required;
if (tokens.length) {
if (optional && !hasOptional)
tokens.push(`[, ${name}`);
else
tokens.push(`, ${name}`);
} else {
if (optional && !hasOptional)
tokens.push(`[${name}`);
else
tokens.push(`${name}`);
}
hasOptional = hasOptional || optional;
}
if (hasOptional)
tokens.push(']');
return tokens.join('');
}
}
(async function() {
let hadChanges = false;
/**
* @param {string} filePath
* @param {string} content
*/
function writeFile(filePath, content) {
if (os.platform() === 'win32')
content = content.replace(/\r\n/g, '\n').replace(/\n/g, '\r\n');
const existing = fs.readFileSync(filePath, 'utf8');
if (existing === content)
return;
hadChanges = true;
console.error(`Writing //${path.relative(PROJECT_DIR, filePath)}`);
fs.writeFileSync(filePath, content, 'utf8');
}
const typesDir = path.join(PROJECT_DIR, 'types');
if (!fs.existsSync(typesDir))
fs.mkdirSync(typesDir)
writeFile(path.join(typesDir, 'protocol.d.ts'), fs.readFileSync(path.join(PROJECT_DIR, 'src', 'server', 'chromium', 'protocol.d.ts'), 'utf8'));
documentation = parseApi(path.join(PROJECT_DIR, 'docs', 'src', 'api'));
documentation.filterForLanguage('js');
documentation.copyDocsFromSuperclasses([]);
const createMarkdownLink = (member, text) => {
const className = toKebabCase(member.clazz.name);
const memberName = toKebabCase(member.name);
let hash = null
if (member.kind === 'property' || member.kind === 'method')
hash = `${className}-${memberName}`.toLowerCase();
else if (member.kind === 'event')
hash = `${className}-event-${memberName}`.toLowerCase();
return `[${text}](https://playwright.dev/docs/api/class-${member.clazz.name.toLowerCase()}#${hash})`;
};
documentation.setLinkRenderer(item => {
const { clazz, member, param, option } = item;
if (param)
return `\`${param}\``;
if (option)
return `\`${option}\``;
if (clazz)
return `[${clazz.name}]`;
if (member.kind === 'method')
return createMarkdownLink(member, `${member.clazz.varName}.${member.alias}(${renderJSSignature(member.argsArray)})`);
if (member.kind === 'event')
return createMarkdownLink(member, `${member.clazz.varName}.on('${member.alias.toLowerCase()}')`);
if (member.kind === 'property')
return createMarkdownLink(member, `${member.clazz.varName}.${member.alias}`);
throw new Error('Unknown member kind ' + member.kind);
});
documentation.generateSourceCodeComments();
const apiDocumentation = parseApi(path.join(PROJECT_DIR, 'docs', 'src', 'api'));
// Root module types are overridden.
const playwrightClass = documentation.classes.get('Playwright');
documentation.classes.delete('Playwright');
documentation.classesArray.splice(documentation.classesArray.indexOf(playwrightClass), 1);
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.methodsArray.find(m => m.alias === 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));
let output = `// This file is generated by ${__filename.substring(path.join(__dirname, '..', '..').length).split(path.sep).join(path.posix.sep)}
${overrides}
${classes.map(classDesc => classToString(classDesc)).join('\n')}
${objectDefinitionsToString(overrides)}
${generateDevicesTypes()}
export interface ChromiumBrowserContext extends BrowserContext { }
export interface ChromiumBrowser extends Browser { }
export interface FirefoxBrowser extends Browser { }
export interface WebKitBrowser extends Browser { }
export interface ChromiumCoverage extends Coverage { }
`;
apiDocumentation.classesArray = apiDocumentation.classesArray.filter(cls => cls.name !== 'Playwright');
apiDocumentation.index();
const apiTypesGenerator = new TypesGenerator(apiDocumentation);
let apiTypes = await apiTypesGenerator.generateTypes(path.join(__dirname, 'overrides.d.ts'));
const namedDevices = Object.keys(devices).map(name => ` ${JSON.stringify(name)}: DeviceDescriptor;`).join('\n');
apiTypes += [
`type Devices = {`,
namedDevices,
` [key: string]: DeviceDescriptor;`,
`}`,
``,
`export interface ChromiumBrowserContext extends BrowserContext { }`,
`export interface ChromiumBrowser extends Browser { }`,
`export interface FirefoxBrowser extends Browser { }`,
`export interface WebKitBrowser extends Browser { }`,
`export interface ChromiumCoverage extends Coverage { }`,
``,
].join('\n');
for (const [key, value] of Object.entries(exported))
output = output.replace(new RegExp('\\b' + key + '\\b', 'g'), value);
// remove trailing whitespace
output = output.replace(/( +)\n/g, '\n');
writeFile(path.join(typesDir, 'types.d.ts'), output);
apiTypes = apiTypes.replace(new RegExp('\\b' + key + '\\b', 'g'), value);
apiTypes = apiTypes.replace(/( +)\n/g, '\n'); // remove trailing whitespace
writeFile(path.join(typesDir, 'types.d.ts'), apiTypes);
const testOnlyDocumentation = parseApi(path.join(PROJECT_DIR, 'docs', 'src', 'test-api'), path.join(PROJECT_DIR, 'docs', 'src', 'api', 'params.md'));
const testDocumentation = apiDocumentation.mergeWith(testOnlyDocumentation);
const testTypesGenerator = new TypesGenerator(testDocumentation);
const testClassMapping = new Map([
['TestType', 'Test'],
['Config', 'TestConfig'],
['FullConfig', 'TestConfig'],
['Project', 'TestProject'],
['PlaywrightWorkerOptions', 'Fixtures'],
['PlaywrightTestOptions', 'Fixtures'],
['PlaywrightWorkerArgs', 'Fixtures'],
['PlaywrightTestArgs', 'Fixtures'],
]);
let testTypes = await testTypesGenerator.generateTypes(path.join(__dirname, 'overrides-test.d.ts'), testClassMapping);
testTypes = testTypes.replace(/( +)\n/g, '\n'); // remove trailing whitespace
writeFile(path.join(typesDir, 'test.d.ts'), testTypes);
const testReporterOnlyDocumentation = parseApi(path.join(PROJECT_DIR, 'docs', 'src', 'test-reporter-api'));
const testReporterDocumentation = testDocumentation.mergeWith(testReporterOnlyDocumentation);
const testReporterTypesGenerator = new TypesGenerator(testReporterDocumentation);
let testReporterTypes = await testReporterTypesGenerator.generateTypes(path.join(__dirname, 'overrides-testReporter.d.ts'), new Map());
testReporterTypes = testReporterTypes.replace(/( +)\n/g, '\n'); // remove trailing whitespace
writeFile(path.join(typesDir, 'testReporter.d.ts'), testReporterTypes);
process.exit(hadChanges && process.argv.includes('--check-clean') ? 1 : 0);
})().catch(e => {
console.error(e);
process.exit(1);
});
function writeFile(filePath, content) {
if (os.platform() === 'win32')
content = content.replace(/\r\n/g, '\n').replace(/\n/g, '\r\n');
const existing = fs.readFileSync(filePath, 'utf8');
if (existing === content)
return;
hadChanges = true;
console.error(`Writing //${path.relative(PROJECT_DIR, filePath)}`);
fs.writeFileSync(filePath, content, 'utf8');
}
/**
* @param {string} overriddes
*/
function objectDefinitionsToString(overriddes) {
let definition;
const parts = [];
const internalWords = new Set(overriddes.split(/[^\w$]/g));
while ((definition = objectDefinitions.pop())) {
const {name, properties} = definition;
const shouldExport = !!exported[name];
const usedInternally = internalWords.has(name);
if (!usedInternally && !shouldExport)
continue;
parts.push(`${shouldExport ? 'export ' : ''}interface ${name} ${stringifyObjectType(properties, name, '')}\n`)
}
return parts.join('\n');
}
function nameForProperty(member) {
return (member.required || member.alias.startsWith('...')) ? member.alias : member.alias + '?';
}
/**
* @param {Documentation.Class} classDesc
*/
function classToString(classDesc) {
const parts = [];
if (classDesc.comment) {
parts.push(writeComment(classDesc.comment))
}
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;
if (type.includes('{'))
return 'data';
return (type[0].toLowerCase() + type.slice(1)).replace(/\|/g, 'Or');
}
/**
* @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 (let [eventName, value] of classDesc.events) {
eventName = eventName.toLowerCase();
const type = stringifyComplexType(value && value.type, '', classDesc.name, eventName, 'payload');
const argName = argNameForType(type);
const params = argName ? `${argName}: ${type}` : '';
descriptions.push({
type,
params,
eventName,
comment: value.comment
});
}
return descriptions;
}
/**
* @param {Documentation.Class} classDesc
*/
function classBody(classDesc) {
const parts = [];
const eventDescriptions = createEventDescriptions(classDesc);
const commentForMethod = {
off: 'Removes an event listener added by `on` or `addListener`.',
removeListener: 'Removes an event listener added by `on` or `addListener`.',
once: 'Adds an event listener that will be automatically removed after it is triggered once. See `addListener` for more information about this event.'
}
for (const method of ['on', 'once', 'addListener', 'removeListener', 'off']) {
for (const {eventName, params, comment} of eventDescriptions) {
if ((method === 'on' || method === 'addListener') && comment)
parts.push(writeComment(comment, ' '));
else
parts.push(writeComment(commentForMethod[method], ' '));
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 '';
if (member.alias === 'waitForEvent') {
const parts = [];
for (const {eventName, params, comment, type} of eventDescriptions) {
if (comment)
parts.push(writeComment(comment, ' '));
parts.push(` ${member.alias}(event: '${eventName}', optionsOrPredicate?: { predicate?: (${params}) => boolean | Promise<boolean>, timeout?: number } | ((${params}) => boolean | Promise<boolean>)): Promise<${type}>;\n`);
}
return parts.join('\n');
}
const jsdoc = memberJSDOC(member, ' ');
const args = argsFromMember(member, ' ', classDesc.name);
let type = stringifyComplexType(member.type, ' ', classDesc.name, member.alias);
if (member.async)
type = `Promise<${type}>`;
// do this late, because we still want object definitions for overridden types
if (!hasOwnMethod(classDesc, member.alias))
return '';
return `${jsdoc}${member.alias}${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 = [];
const out = [];
const pushLine = (line) => {
if (line || out[out.length - 1])
out.push(line)
};
let skipExample = false;
for (let line of comment.split('\n')) {
const match = line.match(/```(\w+)/);
if (match) {
const lang = match[1];
skipExample = !["html", "yml", "bash", "js"].includes(lang);
} else if (skipExample && line.trim().startsWith('```')) {
skipExample = false;
continue;
}
if (!skipExample)
pushLine(line);
}
comment = out.join('\n');
comment = comment.replace(/\[([^\]]+)\]\(\.\/([^\)]+)\)/g, (match, p1, p2) => {
return `[${p1}](https://playwright.dev/docs/${p2.replace('.md', '')})`;
});
parts.push(indent + '/**');
parts.push(...comment.split('\n').map(line => indent + ' * ' + line.replace(/\*\//g, '*\\/')));
parts.push(indent + ' */');
return parts.join('\n');
}
/**
* @param {Documentation.Type} type
*/
function stringifyComplexType(type, indent, ...namespace) {
if (!type)
return 'void';
return stringifySimpleType(type, indent, ...namespace);
}
function stringifyObjectType(properties, name, indent = '') {
const parts = [];
parts.push(`{`);
parts.push(properties.map(member => `${memberJSDOC(member, indent + ' ')}${nameForProperty(member)}${argsFromMember(member, indent + ' ', name)}: ${stringifyComplexType(member.type, indent + ' ', name, member.name)};`).join('\n\n'));
parts.push(indent + '}');
return parts.join('\n');
}
/**
* @param {Documentation.Type=} type
* @returns{string}
*/
function stringifySimpleType(type, indent = '', ...namespace) {
if (!type)
return 'void';
if (type.name === 'Object' && type.templates) {
const keyType = stringifySimpleType(type.templates[0], indent, ...namespace);
const valueType = stringifySimpleType(type.templates[1], indent, ...namespace);
return `{ [key: ${keyType}]: ${valueType}; }`;
}
let out = type.name;
if (out === 'int' || out === 'float')
out = 'number';
if (type.name === 'Object' && type.properties && type.properties.length) {
const name = namespace.map(n => n[0].toUpperCase() + n.substring(1)).join('');
const shouldExport = exported[name];
const properties = namespace[namespace.length -1] === 'options' ? type.sortedProperties() : type.properties;
objectDefinitions.push({name, properties: properties});
if (shouldExport) {
out = name;
} else {
out = stringifyObjectType(properties, name, indent);
}
}
if (type.args) {
const stringArgs = type.args.map(a => ({
type: stringifySimpleType(a, indent, ...namespace),
name: a.name.toLowerCase()
}));
out = `((${stringArgs.map(({name, type}) => `${name}: ${type}`).join(', ')}) => ${stringifySimpleType(type.returnType, indent, ...namespace)})`;
} else if (type.name === 'function') {
out = 'Function';
}
if (out === 'path')
return 'string';
if (out === 'Any')
return 'any';
if (type.templates)
out += '<' + type.templates.map(t => stringifySimpleType(t, indent, ...namespace)).join(', ') + '>';
if (type.union)
out = type.union.map(t => stringifySimpleType(t, indent, ...namespace)).join('|');
return out.trim();
}
/**
* @param {Documentation.Member} member
*/
function argsFromMember(member, indent, ...namespace) {
if (member.kind === 'property')
return '';
return '(' + member.argsArray.map(arg => `${nameForProperty(arg)}: ${stringifyComplexType(arg.type, indent, ...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'));
if (member.deprecated)
lines.push('@deprecated');
lines.push(...member.argsArray.map(arg => `@param ${arg.alias.replace(/\./g, '')} ${arg.comment.replace('\n', ' ')}`));
if (!lines.length)
return indent;
return writeComment(lines.join('\n'), indent) + '\n' + indent;
}
function generateDevicesTypes() {
const namedDevices =
Object.keys(devices)
.map(name => ` ${JSON.stringify(name)}: DeviceDescriptor;`)
.join('\n');
return `type Devices = {
${namedDevices}
[key: string]: DeviceDescriptor;
}`;
}
/**
* @param {Documentation.Member[]} args
*/
function renderJSSignature(args) {
const tokens = [];
let hasOptional = false;
for (const arg of args) {
const name = arg.alias;
const optional = !arg.required;
if (tokens.length) {
if (optional && !hasOptional)
tokens.push(`[, ${name}`);
else
tokens.push(`, ${name}`);
} else {
if (optional && !hasOptional)
tokens.push(`[${name}`);
else
tokens.push(`${name}`);
}
hasOptional = hasOptional || optional;
}
if (hasOptional)
tokens.push(']');
return tokens.join('');
}

338
utils/generate_types/overrides-test.d.ts vendored Normal file
View File

@ -0,0 +1,338 @@
/**
* 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 type { Browser, BrowserContext, BrowserContextOptions, Page, LaunchOptions, ViewportSize, Geolocation, HTTPCredentials } from './types';
import type { Expect } from './testExpect';
export type { Expect } from './testExpect';
export type ReporterDescription =
['dot'] |
['line'] |
['list'] |
['junit'] | ['junit', { outputFile?: string, stripANSIControlSequences?: boolean }] |
['json'] | ['json', { outputFile?: string }] |
['null'] |
[string] | [string, any];
export type Shard = { total: number, current: number } | null;
export type ReportSlowTests = { max: number, threshold: number } | null;
export type PreserveOutput = 'always' | 'never' | 'failures-only';
export type UpdateSnapshots = 'all' | 'none' | 'missing';
type FixtureDefine<TestArgs extends KeyValue = {}, WorkerArgs extends KeyValue = {}> = { test: TestType<TestArgs, WorkerArgs>, fixtures: Fixtures<{}, {}, TestArgs, WorkerArgs> };
type ExpectSettings = {
toMatchSnapshot?: {
// Pixel match threshold.
threshold?: number
}
};
interface TestProject {
expect?: ExpectSettings;
metadata?: any;
name?: string;
outputDir?: string;
repeatEach?: number;
retries?: number;
testDir?: string;
testIgnore?: string | RegExp | (string | RegExp)[];
testMatch?: string | RegExp | (string | RegExp)[];
timeout?: number;
}
export interface Project<TestArgs = {}, WorkerArgs = {}> extends TestProject {
define?: FixtureDefine | FixtureDefine[];
use?: Fixtures<{}, {}, TestArgs, WorkerArgs>;
}
export type FullProject<TestArgs = {}, WorkerArgs = {}> = Required<Project<TestArgs, WorkerArgs>>;
export type LaunchConfig = {
/**
* Shell command to start. For example `npm run start`.
*/
command: string,
/**
* The port that your http server is expected to appear on. If specified it does wait until it accepts connections.
*/
waitForPort?: number,
/**
* How long to wait for the process to start up and be available in milliseconds. Defaults to 60000.
*/
waitForPortTimeout?: number,
/**
* If true it will verify that the given port via `waitForPort` is available and throw otherwise.
* This should commonly set to !!process.env.CI to allow the local dev server when running tests locally.
*/
strict?: boolean
/**
* Environment variables, process.env by default
*/
env?: Record<string, string>,
/**
* Current working directory of the spawned process. Default is process.cwd().
*/
cwd?: string,
};
type LiteralUnion<T extends U, U = string> = T | (U & { zz_IGNORE_ME?: never });
interface TestConfig {
forbidOnly?: boolean;
globalSetup?: string;
globalTeardown?: string;
globalTimeout?: number;
grep?: RegExp | RegExp[];
grepInvert?: RegExp | RegExp[];
maxFailures?: number;
preserveOutput?: PreserveOutput;
projects?: Project[];
quiet?: boolean;
reporter?: LiteralUnion<'list'|'dot'|'line'|'json'|'junit'|'null', string> | ReporterDescription[];
reportSlowTests?: ReportSlowTests;
shard?: Shard;
updateSnapshots?: UpdateSnapshots;
_launch?: LaunchConfig | LaunchConfig[];
workers?: number;
expect?: ExpectSettings;
metadata?: any;
name?: string;
outputDir?: string;
repeatEach?: number;
retries?: number;
testDir?: string;
testIgnore?: string | RegExp | (string | RegExp)[];
testMatch?: string | RegExp | (string | RegExp)[];
timeout?: number;
}
export interface Config<TestArgs = {}, WorkerArgs = {}> extends TestConfig {
projects?: Project<TestArgs, WorkerArgs>[];
define?: FixtureDefine | FixtureDefine[];
use?: Fixtures<{}, {}, TestArgs, WorkerArgs>;
}
export interface FullConfig {
forbidOnly: boolean;
globalSetup: string | null;
globalTeardown: string | null;
globalTimeout: number;
grep: RegExp | RegExp[];
grepInvert: RegExp | RegExp[] | null;
maxFailures: number;
preserveOutput: PreserveOutput;
projects: FullProject[];
reporter: ReporterDescription[];
reportSlowTests: ReportSlowTests;
rootDir: string;
quiet: boolean;
shard: Shard;
updateSnapshots: UpdateSnapshots;
workers: number;
_launch: LaunchConfig[];
}
export type TestStatus = 'passed' | 'failed' | 'timedOut' | 'skipped';
export interface TestError {
message?: string;
stack?: string;
value?: string;
}
export interface WorkerInfo {
config: FullConfig;
project: FullProject;
workerIndex: number;
}
export interface TestInfo {
config: FullConfig;
project: FullProject;
workerIndex: number;
title: string;
file: string;
line: number;
column: number;
fn: Function;
skip(): void;
skip(condition: boolean): void;
skip(condition: boolean, description: string): void;
fixme(): void;
fixme(condition: boolean): void;
fixme(condition: boolean, description: string): void;
fail(): void;
fail(condition: boolean): void;
fail(condition: boolean, description: string): void;
slow(): void;
slow(condition: boolean): void;
slow(condition: boolean, description: string): void;
setTimeout(timeout: number): void;
expectedStatus: TestStatus;
timeout: number;
annotations: { type: string, description?: string }[];
attachments: { name: string, path?: string, body?: Buffer, contentType: string }[];
repeatEachIndex: number;
retry: number;
duration: number;
status?: TestStatus;
error?: TestError;
stdout: (string | Buffer)[];
stderr: (string | Buffer)[];
snapshotSuffix: string;
outputDir: string;
snapshotPath: (snapshotName: string) => string;
outputPath: (...pathSegments: string[]) => string;
}
interface SuiteFunction {
(title: string, callback: () => void): void;
}
interface TestFunction<TestArgs> {
(title: string, testFunction: (args: TestArgs, testInfo: TestInfo) => Promise<void> | void): void;
}
export interface TestType<TestArgs extends KeyValue, WorkerArgs extends KeyValue> extends TestFunction<TestArgs & WorkerArgs> {
only: TestFunction<TestArgs & WorkerArgs>;
describe: SuiteFunction & {
only: SuiteFunction;
};
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;
fixme(): void;
fixme(condition: boolean): void;
fixme(condition: boolean, description: string): void;
fixme(callback: (args: TestArgs & WorkerArgs) => boolean): void;
fixme(callback: (args: TestArgs & WorkerArgs) => boolean, description: string): void;
fail(): void;
fail(condition: boolean): void;
fail(condition: boolean, description: string): void;
fail(callback: (args: TestArgs & WorkerArgs) => boolean): void;
fail(callback: (args: TestArgs & WorkerArgs) => boolean, description: string): void;
slow(): void;
slow(condition: boolean): void;
slow(condition: boolean, description: string): void;
slow(callback: (args: TestArgs & WorkerArgs) => boolean): void;
slow(callback: (args: TestArgs & WorkerArgs) => boolean, description: string): void;
setTimeout(timeout: number): void;
beforeEach(inner: (args: TestArgs & WorkerArgs, testInfo: TestInfo) => Promise<any> | any): void;
afterEach(inner: (args: TestArgs & WorkerArgs, testInfo: TestInfo) => Promise<any> | any): void;
beforeAll(inner: (args: WorkerArgs, workerInfo: WorkerInfo) => Promise<any> | any): void;
afterAll(inner: (args: WorkerArgs, workerInfo: WorkerInfo) => Promise<any> | any): void;
use(fixtures: Fixtures<{}, {}, TestArgs, WorkerArgs>): void;
expect: Expect;
declare<T extends KeyValue = {}, W extends KeyValue = {}>(): TestType<TestArgs & T, WorkerArgs & W>;
extend<T, W extends KeyValue = {}>(fixtures: Fixtures<T, W, TestArgs, WorkerArgs>): TestType<TestArgs & T, WorkerArgs & W>;
}
type KeyValue = { [key: string]: any };
export type TestFixture<R, Args extends KeyValue> = (args: Args, use: (r: R) => Promise<void>, testInfo: TestInfo) => any;
export type WorkerFixture<R, Args extends KeyValue> = (args: Args, use: (r: R) => Promise<void>, workerInfo: WorkerInfo) => any;
type TestFixtureValue<R, Args> = R | TestFixture<R, Args>;
type WorkerFixtureValue<R, Args> = R | WorkerFixture<R, Args>;
export type Fixtures<T extends KeyValue = {}, W extends KeyValue = {}, PT extends KeyValue = {}, PW extends KeyValue = {}> = {
[K in keyof PW]?: WorkerFixtureValue<PW[K], W & PW>;
} & {
[K in keyof PT]?: TestFixtureValue<PT[K], T & W & PT & PW>;
} & {
[K in keyof W]?: [WorkerFixtureValue<W[K], W & PW>, { scope: 'worker', auto?: boolean }];
} & {
[K in keyof T]?: TestFixtureValue<T[K], T & W & PT & PW> | [TestFixtureValue<T[K], T & W & PT & PW>, { scope?: 'test', auto?: boolean }];
};
type BrowserName = 'chromium' | 'firefox' | 'webkit';
type BrowserChannel = Exclude<LaunchOptions['channel'], undefined>;
type ColorScheme = Exclude<BrowserContextOptions['colorScheme'], undefined>;
type ExtraHTTPHeaders = Exclude<BrowserContextOptions['extraHTTPHeaders'], undefined>;
type Proxy = Exclude<BrowserContextOptions['proxy'], undefined>;
type StorageState = Exclude<BrowserContextOptions['storageState'], undefined>;
export interface PlaywrightWorkerOptions {
browserName: BrowserName;
defaultBrowserType: BrowserName;
headless: boolean | undefined;
channel: BrowserChannel | undefined;
launchOptions: LaunchOptions;
}
export type VideoMode = 'off' | 'on' | 'retain-on-failure' | 'on-first-retry' | /** deprecated */ 'retry-with-video';
export interface PlaywrightTestOptions {
screenshot: 'off' | 'on' | 'only-on-failure';
trace: 'off' | 'on' | 'retain-on-failure' | 'on-first-retry' | /** deprecated */ 'retry-with-trace';
video: VideoMode | { mode: VideoMode, size: ViewportSize };
acceptDownloads: boolean | undefined;
bypassCSP: boolean | undefined;
colorScheme: ColorScheme | undefined;
deviceScaleFactor: number | undefined;
extraHTTPHeaders: ExtraHTTPHeaders | undefined;
geolocation: Geolocation | undefined;
hasTouch: boolean | undefined;
httpCredentials: HTTPCredentials | undefined;
ignoreHTTPSErrors: boolean | undefined;
isMobile: boolean | undefined;
javaScriptEnabled: boolean | undefined;
locale: string | undefined;
offline: boolean | undefined;
permissions: string[] | undefined;
proxy: Proxy | undefined;
storageState: StorageState | undefined;
timezoneId: string | undefined;
userAgent: string | undefined;
viewport: ViewportSize | null | undefined;
baseURL: string | undefined;
contextOptions: BrowserContextOptions;
}
export interface PlaywrightWorkerArgs {
playwright: typeof import('..');
browser: Browser;
}
export interface PlaywrightTestArgs {
context: BrowserContext;
page: Page;
}
export type PlaywrightTestProject<TestArgs = {}, WorkerArgs = {}> = Project<PlaywrightTestOptions & TestArgs, PlaywrightWorkerOptions & WorkerArgs>;
export type PlaywrightTestConfig<TestArgs = {}, WorkerArgs = {}> = Config<PlaywrightTestOptions & TestArgs, PlaywrightWorkerOptions & WorkerArgs>;
/**
* These tests are executed in Playwright environment that launches the browser
* and provides a fresh page to each test.
*/
export const test: TestType<PlaywrightTestArgs & PlaywrightTestOptions, PlaywrightWorkerArgs & PlaywrightWorkerOptions>;
export default test;
export const _baseTest: TestType<{}, {}>;
export const expect: Expect;
// This is required to not export everything by default. See https://github.com/Microsoft/TypeScript/issues/19545#issuecomment-340490459
export {};

View File

@ -0,0 +1,85 @@
/**
* 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 type { FullConfig, TestStatus, TestError } from './test';
export type { FullConfig, TestStatus, TestError } from './test';
export interface Location {
file: string;
line: number;
column: number;
}
export interface Suite {
title: string;
location?: Location;
suites: Suite[];
tests: TestCase[];
titlePath(): string[];
allTests(): TestCase[];
}
export interface TestCase {
title: string;
location: Location;
titlePath(): string[];
expectedStatus: TestStatus;
timeout: number;
annotations: { type: string, description?: string }[];
retries: number;
results: TestResult[];
outcome(): 'skipped' | 'expected' | 'unexpected' | 'flaky';
ok(): boolean;
}
export interface TestResult {
retry: number;
workerIndex: number;
startTime: Date;
duration: number;
status?: TestStatus;
error?: TestError;
attachments: { name: string, path?: string, body?: Buffer, contentType: string }[];
stdout: (string | Buffer)[];
stderr: (string | Buffer)[];
}
/**
* Result of the full test run.
*/
export interface FullResult {
/**
* Status:
* - 'passed' - everything went as expected.
* - 'failed' - any test has failed.
* - 'timedout' - the global time has been reached.
* - 'interrupted' - interrupted by the user.
*/
status: 'passed' | 'failed' | 'timedout' | 'interrupted';
}
export interface Reporter {
onBegin?(config: FullConfig, suite: Suite): void;
onTestBegin?(test: TestCase): void;
onStdOut?(chunk: string | Buffer, test?: TestCase): void;
onStdErr?(chunk: string | Buffer, test?: TestCase): void;
onTestEnd?(test: TestCase, result: TestResult): void;
onError?(error: TestError): void;
onEnd?(result: FullResult): void | Promise<void>;
}
// This is required to not export everything by default. See https://github.com/Microsoft/TypeScript/issues/19545#issuecomment-340490459
export {};

View File

@ -16,13 +16,14 @@
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
* @param {string} filePath
* @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');
async function parseOverrides(filePath, commentForClass, commentForMethod, extraForClass) {
const program = ts.createProgram({
rootNames: [filePath],
options: {
@ -75,7 +76,9 @@ async function parseOverrides(commentForClass, commentForMethod, extraForClass)
for (const [name, member] of symbol.members || []) {
if (member.flags & ts.SymbolFlags.TypeParameter)
continue;
const pos = member.valueDeclaration.getStart(file, false)
if (!member.valueDeclaration)
continue;
const pos = member.valueDeclaration.getStart(file, false);
replacers.push({
pos,
text: commentForMethod(className, name),