feat: support experimental doc entries (#13446)

feat: support experimental doc entries

- Params/options/members are marked as experimental in the docs.
- `experimental.d.ts` is generated that contains all types and
  includes experimental features.
- `experimental.d.ts` is references in our tests so that we
  can test experimental features.
- `fonts` option is restored as experimental.
This commit is contained in:
Dmitry Gozman 2022-04-13 16:13:30 -07:00 committed by GitHub
parent 166675b9c1
commit 20dcc45afa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 20588 additions and 136 deletions

View File

@ -960,6 +960,8 @@ An object which specifies clipping of the resulting image. Should have the follo
When set to `"css"`, screenshot will have a single pixel per each css pixel on the page. For high-dpi devices, this will keep screenshots small. Using `"device"` option will produce a single pixel per each device pixel, so screenhots of high-dpi devices will be twice as large or even larger. Defaults to `"device"`.
## screenshot-option-fonts
* langs: js
* experimental
- `fonts` <[ScreenshotFonts]<"ready"|"nowait">>
When set to `"ready"`, screenshot will wait for [`document.fonts.ready`](https://developer.mozilla.org/en-US/docs/Web/API/FontFaceSet/ready) promise to resolve in all frames. Defaults to `"nowait"`.
@ -975,6 +977,7 @@ When set to `"hide"`, screenshot will hide text caret. When set to `"initial"`,
- %%-screenshot-option-quality-%%
- %%-screenshot-option-path-%%
- %%-screenshot-option-scale-%%
- %%-screenshot-option-fonts-%%
- %%-screenshot-option-caret-%%
- %%-screenshot-option-type-%%
- %%-screenshot-option-mask-%%

View File

@ -36,7 +36,9 @@
"./lib/utils/timeoutRunner": "./lib/utils/timeoutRunner.js",
"./lib/remote/playwrightServer": "./lib/remote/playwrightServer.js",
"./lib/remote/playwrightClient": "./lib/remote/playwrightClient.js",
"./lib/server": "./lib/server/index.js"
"./lib/server": "./lib/server/index.js",
"./types/protocol": "./types/protocol.d.ts",
"./types/structs": "./types/structs.d.ts"
},
"types": "types/types.d.ts",
"bin": {

View File

@ -201,7 +201,6 @@ export class ElementHandle<T extends Node = Node> extends JSHandle<T> implements
selector: locator._selector,
}));
}
copy.fonts = (options as any)._fonts;
const result = await this._elementChannel.screenshot(copy);
const buffer = Buffer.from(result.binary, 'base64');
if (options.path) {

View File

@ -492,7 +492,6 @@ export class Page extends ChannelOwner<channels.PageChannel> implements api.Page
selector: locator._selector,
}));
}
copy.fonts = (options as any)._fonts;
const result = await this._channel.screenshot(copy);
const buffer = Buffer.from(result.binary, 'base64');
if (options.path) {

View File

@ -23,7 +23,7 @@ import type { Frame } from './frames';
import type { ParsedSelector } from './isomorphic/selectorParser';
import type * as types from './types';
import type { Progress } from './progress';
import { assert } from '../utils';
import { assert, experimentalFeaturesEnabled } from '../utils';
import { MultiMap } from '../utils/multimap';
declare global {
@ -322,6 +322,9 @@ function trimClipToSize(clip: types.Rect, size: types.Size): types.Rect {
}
function validateScreenshotOptions(options: ScreenshotOptions): 'png' | 'jpeg' {
if (options.fonts && !experimentalFeaturesEnabled())
throw new Error(`To use the experimental option "fonts", set PLAYWRIGHT_EXPERIMENTAL_FEATURES=1 enviroment variable.`);
let format: 'png' | 'jpeg' | null = null;
// options.type takes precedence over inferring the type from options.path
// because it may be a 0-length file with no extension created beforehand (i.e. as a temp file).

View File

@ -14,7 +14,7 @@
* limitations under the License.
*/
import { JSHandle, ElementHandle, Frame, Page, BrowserContext, Locator } from './types';
import { JSHandle, ElementHandle, Frame, Page, BrowserContext } from 'playwright-core';
/**
* Can be converted to JSON

View File

@ -14,12 +14,12 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Protocol } from './protocol';
import { Protocol } from 'playwright-core/types/protocol';
import { ChildProcess } from 'child_process';
import { EventEmitter } from 'events';
import { Readable } from 'stream';
import { ReadStream } from 'fs';
import { Serializable, EvaluationArgument, PageFunction, PageFunctionOn, SmartHandle, ElementHandleForTag, BindingSource } from './structs';
import { Serializable, EvaluationArgument, PageFunction, PageFunctionOn, SmartHandle, ElementHandleForTag, BindingSource } from 'playwright-core/types/structs';
type PageWaitForSelectorOptionsNotHidden = PageWaitForSelectorOptions & {
state?: 'visible'|'attached';

View File

@ -14,6 +14,5 @@
* limitations under the License.
*/
export * from 'playwright-core';
export * from './types/test';
export { default } from './types/test';

View File

@ -308,7 +308,7 @@ export async function toHaveScreenshot(
const [page, locator] = pageOrLocator.constructor.name === 'Page' ? [(pageOrLocator as PageEx), undefined] : [(pageOrLocator as Locator).page() as PageEx, pageOrLocator as LocatorEx];
const screenshotOptions = {
animations: config?.animations ?? 'disabled',
_fonts: config?.fonts ?? 'ready',
fonts: process.env.PLAYWRIGHT_EXPERIMENTAL_FEATURES ? (config?.fonts ?? 'ready') : undefined,
scale: config?.scale ?? 'css',
caret: config?.caret ?? 'hide',
...helper.allOptions,

View File

@ -16,6 +16,7 @@
*/
import type { APIRequestContext, Browser, BrowserContext, BrowserContextOptions, Page, LaunchOptions, ViewportSize, Geolocation, HTTPCredentials, Locator, APIResponse } from 'playwright-core';
export * from 'playwright-core';
export type ReporterDescription =
['dot'] |
@ -2917,7 +2918,7 @@ type MakeMatchers<R, T> = BaseMatchers<R, T> & {
ExtraMatchers<T, Locator, LocatorAssertions> &
ExtraMatchers<T, APIResponse, APIResponseAssertions>;
export declare type Expect = {
export type Expect = {
<T = unknown>(actual: T, messageOrOptions?: string | { message?: string }): MakeMatchers<void, T>;
soft: <T = unknown>(actual: T, messageOrOptions?: string | { message?: string }) => MakeMatchers<void, T>;
poll: <T = unknown>(actual: () => T | Promise<T>, messageOrOptions?: string | { message?: string, timeout?: number }) => BaseMatchers<Promise<void>, T> & {
@ -2996,12 +2997,14 @@ type SupportedExpectProperties =
'toThrow' |
'toThrowError'
// --- BEGINGLOBAL ---
declare global {
export namespace PlaywrightTest {
export interface Matchers<R, T = unknown> {
}
}
}
// --- ENDGLOBAL ---
/**
* These tests are executed in Playwright environment that launches the browser

View File

@ -15,8 +15,8 @@
* limitations under the License.
*/
import type { FullConfig, FullProject, TestStatus, TestError } from './test';
export type { FullConfig, TestStatus, TestError } from './test';
import type { FullConfig, FullProject, TestStatus, TestError } from '@playwright/test';
export type { FullConfig, TestStatus, TestError } from '@playwright/test';
/**
* `Suite` is a group of tests. All tests in Playwright Test form the following hierarchy:

View File

@ -14,6 +14,9 @@
* limitations under the License.
*/
// eslint-disable-next-line spaced-comment
/// <reference path="./experimental.d.ts" />
import type { Fixtures } from '@playwright/test';
import type { ChildProcess } from 'child_process';
import { execSync, spawn } from 'child_process';

20281
tests/config/experimental.d.ts vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@ -30,7 +30,8 @@ export class DriverTestMode implements TestMode {
async setup() {
this._impl = await start({
NODE_OPTIONS: undefined // Hide driver process while debugging.
NODE_OPTIONS: undefined, // Hide driver process while debugging.
PLAYWRIGHT_EXPERIMENTAL_FEATURES: '1',
});
return this._impl.playwright;
}

View File

@ -72,6 +72,9 @@ if (mode === 'service') {
command: 'npx playwright experimental-grid-server',
port: 3333,
reuseExistingServer: true,
env: {
PLAYWRIGHT_EXPERIMENTAL_FEATURES: '1',
},
};
}
@ -80,6 +83,9 @@ if (mode === 'service2') {
command: 'npx playwright run-server --port=3333',
port: 3333,
reuseExistingServer: true,
env: {
PLAYWRIGHT_EXPERIMENTAL_FEATURES: '1',
},
};
config.use.connectOptions = {
wsEndpoint: 'ws://localhost:3333/',

View File

@ -785,13 +785,13 @@ it.describe('page screenshot animations', () => {
const noIconsScreenshot = await page.screenshot();
// Make sure screenshot times out while webfont is stalled.
const error = await page.screenshot({
_fonts: 'ready',
fonts: 'ready',
timeout: 200,
} as any).catch(e => e);
}).catch(e => e);
expect(error.message).toContain('waiting for fonts to load...');
expect(error.message).toContain('Timeout 200ms exceeded');
const [iconsScreenshot] = await Promise.all([
page.screenshot({ _fonts: 'ready' } as any),
page.screenshot({ fonts: 'ready' }),
server.serveFile(serverRequest, serverResponse),
]);
expect(iconsScreenshot).toMatchSnapshot('screenshot-web-font.png', {

View File

@ -63,6 +63,7 @@ test('should fail to screenshot a page with infinite animation', async ({ runInl
test('should disable animations by default', async ({ runInlineTest }, testInfo) => {
const cssTransitionURL = pathToFileURL(path.join(__dirname, '../assets/css-transition.html'));
const result = await runInlineTest({
...playwrightConfig({}),
'a.spec.js': `
pwt.test('is a test', async ({ page }) => {
await page.goto('${cssTransitionURL}');
@ -156,11 +157,7 @@ test('should report _toHaveScreenshot step with expectation name in title', asyn
}
module.exports = Reporter;
`,
'playwright.config.ts': `
module.exports = {
reporter: './reporter',
};
`,
...playwrightConfig({ reporter: './reporter' }),
'a.spec.js': `
pwt.test('is a test', async ({ page }) => {
// Named expectation.
@ -371,8 +368,7 @@ test('should compile with different option combinations', async ({ runTSC }) =>
maxDiffPixelRatio: 0.2,
animations: "disabled",
omitBackground: true,
// TODO: uncomment when enabling "fonts".
// fonts: "nowait",
fonts: "nowait",
caret: "initial",
scale: "device",
timeout: 1000,
@ -401,6 +397,7 @@ test('should fail when screenshot is different size', async ({ runInlineTest })
test('should fail when given non-png snapshot name', async ({ runInlineTest }) => {
const result = await runInlineTest({
...playwrightConfig({}),
'a.spec.js': `
pwt.test('is a test', async ({ page }) => {
await expect(page)._toHaveScreenshot('snapshot.jpeg');
@ -413,6 +410,7 @@ test('should fail when given non-png snapshot name', async ({ runInlineTest }) =
test('should fail when given buffer', async ({ runInlineTest }) => {
const result = await runInlineTest({
...playwrightConfig({}),
'a.spec.js': `
pwt.test('is a test', async ({ page }) => {
await expect(Buffer.from([1]))._toHaveScreenshot();
@ -798,6 +796,7 @@ test('should respect maxDiffPixelRatio option', async ({ runInlineTest }) => {
test('should throw for invalid maxDiffPixels values', async ({ runInlineTest }) => {
expect((await runInlineTest({
...playwrightConfig({}),
'a.spec.js': `
pwt.test('is a test', async ({ page }) => {
await expect(page)._toHaveScreenshot({
@ -810,6 +809,7 @@ test('should throw for invalid maxDiffPixels values', async ({ runInlineTest })
test('should throw for invalid maxDiffPixelRatio values', async ({ runInlineTest }) => {
expect((await runInlineTest({
...playwrightConfig({}),
'a.spec.js': `
pwt.test('is a test', async ({ page }) => {
await expect(page)._toHaveScreenshot({

View File

@ -74,7 +74,7 @@ class ApiParser {
continue;
}
}
const clazz = new Documentation.Class(extractLangs(node), name, [], extendsName, extractComments(node));
const clazz = new Documentation.Class(extractLangs(node), extractExperimental(node), name, [], extendsName, extractComments(node));
this.classes.set(clazz.name, clazz);
}
@ -102,11 +102,11 @@ class ApiParser {
const comments = extractComments(spec);
let member;
if (match[1] === 'event')
member = Documentation.Member.createEvent(extractLangs(spec), name, returnType, comments);
member = Documentation.Member.createEvent(extractLangs(spec), extractExperimental(spec), name, returnType, comments);
if (match[1] === 'property')
member = Documentation.Member.createProperty(extractLangs(spec), name, returnType, comments, !optional);
member = Documentation.Member.createProperty(extractLangs(spec), extractExperimental(spec), name, returnType, comments, !optional);
if (['method', 'async method', 'optional method', 'optional async method'].includes(match[1])) {
member = Documentation.Member.createMethod(extractLangs(spec), name, [], returnType, comments);
member = Documentation.Member.createMethod(extractLangs(spec), extractExperimental(spec), name, [], returnType, comments);
if (match[1].includes('async'))
member.async = true;
if (match[1].includes('optional'))
@ -167,7 +167,7 @@ class ApiParser {
let options = method.argsArray.find(o => o.name === 'options');
if (!options) {
const type = new Documentation.Type('Object', []);
options = Documentation.Member.createProperty({}, 'options', type, undefined, false);
options = Documentation.Member.createProperty({}, false /* experimental */, 'options', type, undefined, false);
method.argsArray.push(options);
}
const p = this.parseProperty(spec);
@ -188,7 +188,7 @@ class ApiParser {
const name = text.substring(0, typeStart).replace(/\`/g, '').trim();
const comments = extractComments(spec);
const { type, optional } = this.parseType(param);
return Documentation.Member.createProperty(extractLangs(spec), name, type, comments, !optional);
return Documentation.Member.createProperty(extractLangs(spec), extractExperimental(spec), name, type, comments, !optional);
}
/**
@ -202,7 +202,7 @@ class ApiParser {
const { name, text } = parseVariable(child.text);
const comments = /** @type {MarkdownNode[]} */ ([{ type: 'text', text }]);
const childType = this.parseType(child);
properties.push(Documentation.Member.createProperty({}, name, childType.type, comments, !childType.optional));
properties.push(Documentation.Member.createProperty({}, false /* experimental */, name, childType.type, comments, !childType.optional));
}
const type = Documentation.Type.parse(arg.type, properties);
return { type, optional: arg.optional };
@ -300,13 +300,11 @@ function applyTemplates(body, params) {
* @returns {MarkdownNode[]}
*/
function extractComments(item) {
return (item.children || []).filter(c => {
return childrenWithoutProperties(item).filter(c => {
if (c.type.startsWith('h'))
return false;
if (c.type === 'li' && c.liType === 'default')
return false;
if (c.type === 'li' && c.text.startsWith('langs:'))
return false;
return true;
});
}
@ -346,12 +344,27 @@ function extractLangs(spec) {
return {};
}
/**
* @param {MarkdownNode} spec
* @returns {boolean}
*/
function extractExperimental(spec) {
for (const child of spec.children) {
if (child.type === 'li' && child.liType === 'bullet' && child.text === 'experimental')
return true;
}
return false;
}
/**
* @param {MarkdownNode} spec
* @returns {MarkdownNode[]}
*/
function childrenWithoutProperties(spec) {
return spec.children.filter(c => c.liType !== 'bullet' || !c.text.startsWith('langs'));
return (spec.children || []).filter(c => {
const isProperty = c.liType === 'bullet' && (c.text.startsWith('langs:') || c.text === 'experimental');
return !isProperty;
});
}
/**

View File

@ -66,7 +66,7 @@ class Documentation {
* @return {!Documentation}
*/
mergeWith(documentation) {
return new Documentation([...this.classesArray, ...documentation.classesArray]);
return new Documentation([...this.classesArray, ...documentation.classesArray].map(cls => cls.clone()));
}
/**
@ -108,6 +108,18 @@ class Documentation {
this.index();
}
filterOutExperimental() {
const classesArray = [];
for (const clazz of this.classesArray) {
if (clazz.experimental)
continue;
clazz.filterOutExperimental();
classesArray.push(clazz);
}
this.classesArray = classesArray;
this.index();
}
index() {
for (const cls of this.classesArray) {
this.classes.set(cls.name, cls);
@ -149,23 +161,28 @@ class Documentation {
clazz.visit(item => item.comment = generateSourceCodeComment(item.spec));
}
clone() {
return new Documentation(this.classesArray.map(cls => cls.clone()));
}
}
Documentation.Class = class {
/**
* @param {Langs} langs
* @param {boolean} experimental
* @param {string} name
* @param {!Array<!Documentation.Member>} membersArray
* @param {?string=} extendsName
* @param {MarkdownNode[]=} spec
*/
constructor(langs, name, membersArray, extendsName = null, spec = undefined) {
constructor(langs, experimental, name, membersArray, extendsName = null, spec = undefined) {
this.langs = langs;
this.experimental = experimental;
this.name = name;
this.membersArray = membersArray;
this.spec = spec;
this.extends = extendsName;
this.comment = '';
this.comment = '';
this.index();
const match = name.match(/(API|JS|CDP|[A-Z])(.*)/);
this.varName = match[1].toLowerCase() + match[2];
@ -204,6 +221,12 @@ Documentation.Class = class {
}
}
clone() {
const cls = new Documentation.Class(this.langs, this.experimental, this.name, this.membersArray.map(m => m.clone()), this.extends, this.spec);
cls.comment = this.comment;
return cls;
}
/**
* @param {string} lang
*/
@ -218,6 +241,17 @@ Documentation.Class = class {
this.membersArray = membersArray;
}
filterOutExperimental() {
const membersArray = [];
for (const member of this.membersArray) {
if (member.experimental)
continue;
member.filterOutExperimental();
membersArray.push(member);
}
this.membersArray = membersArray;
}
validateOrder(errors, cls) {
const members = this.membersArray;
// Events should go first.
@ -280,15 +314,17 @@ Documentation.Member = class {
/**
* @param {string} kind
* @param {Langs} langs
* @param {boolean} experimental
* @param {string} name
* @param {?Documentation.Type} type
* @param {!Array<!Documentation.Member>} argsArray
* @param {MarkdownNode[]=} spec
* @param {boolean=} required
*/
constructor(kind, langs, name, type, argsArray, spec = undefined, required = true) {
constructor(kind, langs, experimental, name, type, argsArray, spec = undefined, required = true) {
this.kind = kind;
this.langs = langs;
this.experimental = experimental;
this.name = name;
this.type = type;
this.spec = spec;
@ -355,13 +391,27 @@ Documentation.Member = class {
overriddenArg.filterForLanguage(lang);
if (overriddenArg.name === 'options' && !overriddenArg.type.properties.length)
continue;
overriddenArg.type.filterForLanguage(lang);
argsArray.push(overriddenArg);
}
this.argsArray = argsArray;
}
filterOutExperimental() {
this.type.filterOutExperimental();
const argsArray = [];
for (const arg of this.argsArray) {
if (arg.experimental)
continue;
arg.type.filterOutExperimental();
argsArray.push(arg);
}
this.argsArray = argsArray;
}
clone() {
const result = new Documentation.Member(this.kind, this.langs, this.name, this.type, this.argsArray, this.spec, this.required);
const result = new Documentation.Member(this.kind, this.langs, this.experimental, this.name, this.type.clone(), this.argsArray.map(arg => arg.clone()), this.spec, this.required);
result.alias = this.alias;
result.async = this.async;
result.paramOrOption = this.paramOrOption;
return result;
@ -369,37 +419,40 @@ Documentation.Member = class {
/**
* @param {Langs} langs
* @param {boolean} experimental
* @param {string} name
* @param {!Array<!Documentation.Member>} argsArray
* @param {?Documentation.Type} returnType
* @param {MarkdownNode[]=} spec
* @return {!Documentation.Member}
*/
static createMethod(langs, name, argsArray, returnType, spec) {
return new Documentation.Member('method', langs, name, returnType, argsArray, spec);
static createMethod(langs, experimental, name, argsArray, returnType, spec) {
return new Documentation.Member('method', langs, experimental, name, returnType, argsArray, spec);
}
/**
* @param {!Langs} langs
* @param {boolean} experimental
* @param {!string} name
* @param {!Documentation.Type} type
* @param {!MarkdownNode[]=} spec
* @param {boolean=} required
* @return {!Documentation.Member}
*/
static createProperty(langs, name, type, spec, required) {
return new Documentation.Member('property', langs, name, type, [], spec, required);
static createProperty(langs, experimental, name, type, spec, required) {
return new Documentation.Member('property', langs, experimental, name, type, [], spec, required);
}
/**
* @param {Langs} langs
* @param {boolean} experimental
* @param {string} name
* @param {?Documentation.Type=} type
* @param {MarkdownNode[]=} spec
* @return {!Documentation.Member}
*/
static createEvent(langs, name, type = null, spec) {
return new Documentation.Member('event', langs, name, type, [], spec);
static createEvent(langs, experimental, name, type = null, spec) {
return new Documentation.Member('event', langs, experimental, name, type, [], spec);
}
/**
@ -488,16 +541,17 @@ Documentation.Type = class {
*/
constructor(name, properties) {
this.name = name.replace(/^\[/, '').replace(/\]$/, '');
/** @type {Documentation.Member[] | undefined} */
this.properties = this.name === 'Object' ? properties : undefined;
/** @type {Documentation.Type[]} | undefined */
/** @type {Documentation.Type[] | undefined} */
this.union;
/** @type {Documentation.Type[]} | undefined */
/** @type {Documentation.Type[] | undefined} */
this.args;
/** @type {Documentation.Type} | undefined */
/** @type {Documentation.Type | undefined} */
this.returnType;
/** @type {Documentation.Type[]} | undefined */
/** @type {Documentation.Type[] | undefined} */
this.templates;
/** @type {string | undefined } */
/** @type {string | undefined} */
this.expression;
}
@ -510,6 +564,20 @@ Documentation.Type = class {
}
}
clone() {
const type = new Documentation.Type(this.name, this.properties ? this.properties.map(prop => prop.clone()) : undefined);
if (this.union)
type.union = this.union.map(type => type.clone());
if (this.args)
type.args = this.args.map(type => type.clone());
if (this.returnType)
type.returnType = this.returnType.clone();
if (this.templates)
type.templates = this.templates.map(type => type.clone());
type.expression = this.expression;
return type;
}
/**
* @returns {Documentation.Member[]}
*/
@ -550,6 +618,19 @@ Documentation.Type = class {
this.properties = properties;
}
filterOutExperimental() {
if (!this.properties)
return;
const properties = [];
for (const prop of this.properties) {
if (prop.experimental)
continue;
prop.filterOutExperimental();
properties.push(prop);
}
this.properties = properties;
}
/**
* @param {Documentation.Type[]} result
*/

View File

@ -26,8 +26,6 @@ const { parseOverrides } = require('./parseOverrides');
const exported = require('./exported.json');
const { parseApi } = require('../doclint/api_parser');
/** @typedef {import('../doclint/documentation').Member} Member */
Error.stackTraceLimit = 50;
class TypesGenerator {
@ -38,10 +36,11 @@ class TypesGenerator {
* overridesToDocsClassMapping?: Map<string, string>,
* ignoreMissing?: Set<string>,
* doNotExportClassNames?: Set<string>,
* includeExperimental?: boolean,
* }} options
*/
constructor(options) {
/** @type {Array<{name: string, properties: Member[]}>} */
/** @type {Array<{name: string, properties: Documentation.Member[]}>} */
this.objectDefinitions = [];
/** @type {Set<string>} */
this.handledMethods = new Set();
@ -50,6 +49,10 @@ class TypesGenerator {
this.overridesToDocsClassMapping = options.overridesToDocsClassMapping || new Map();
this.ignoreMissing = options.ignoreMissing || new Set();
this.doNotExportClassNames = options.doNotExportClassNames || new Set();
this.documentation.filterForLanguage('js');
if (!options.includeExperimental)
this.documentation.filterOutExperimental();
this.documentation.copyDocsFromSuperclasses([]);
}
/**
@ -57,9 +60,6 @@ class TypesGenerator {
* @returns {Promise<string>}
*/
async generateTypes(overridesFile) {
this.documentation.filterForLanguage('js');
this.documentation.copyDocsFromSuperclasses([]);
const createMarkdownLink = (member, text) => {
const className = toKebabCase(member.clazz.name);
const memberName = toKebabCase(member.name);
@ -238,7 +238,7 @@ class TypesGenerator {
type,
params,
eventName,
comment: value.comment
comment: value.comment,
});
}
return descriptions;
@ -366,10 +366,21 @@ class TypesGenerator {
return this.stringifySimpleType(type, indent, ...namespace);
}
/**
* @param {Documentation.Member[]} properties
* @param {string} name
* @param {string=} indent
* @returns {string}
*/
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(properties.map(member => {
const comment = this.memberJSDOC(member, indent + ' ');
const args = this.argsFromMember(member, indent + ' ', name);
const type = this.stringifyComplexType(member.type, indent + ' ', name, member.name);
return `${comment}${this.nameForProperty(member)}${args}: ${type};`;
}).join('\n\n'));
parts.push(indent + '}');
return parts.join('\n');
}
@ -476,15 +487,124 @@ class TypesGenerator {
}
(async function () {
let hadChanges = false;
const coreDocumentation = parseApi(path.join(PROJECT_DIR, 'docs', 'src', 'api'));
const testDocumentation = parseApi(path.join(PROJECT_DIR, 'docs', 'src', 'test-api'), path.join(PROJECT_DIR, 'docs', 'src', 'api', 'params.md'));
const reporterDocumentation = parseApi(path.join(PROJECT_DIR, 'docs', 'src', 'test-reporter-api'));
const assertionClasses = new Set(['LocatorAssertions', 'PageAssertions', 'APIResponseAssertions', 'ScreenshotAssertions']);
/**
* @param {boolean} includeExperimental
* @returns {Promise<string>}
*/
async function generateCoreTypes(includeExperimental) {
const documentation = coreDocumentation.clone();
const generator = new TypesGenerator({
documentation,
classNamesToGenerate: new Set(coreDocumentation.classesArray.map(cls => cls.name).filter(name => !assertionClasses.has(name) && name !== 'PlaywrightAssertions')),
includeExperimental,
});
let types = await generator.generateTypes(path.join(__dirname, 'overrides.d.ts'));
const namedDevices = Object.keys(devices).map(name => ` ${JSON.stringify(name)}: DeviceDescriptor;`).join('\n');
types += [
`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))
types = types.replace(new RegExp('\\b' + key + '\\b', 'g'), value);
return types;
}
/**
* @param {boolean} includeExperimental
* @returns {Promise<string>}
*/
async function generateTestTypes(includeExperimental) {
const documentation = coreDocumentation.mergeWith(testDocumentation);
const generator = new TypesGenerator({
documentation,
classNamesToGenerate: new Set(['TestError', 'TestInfo', 'WorkerInfo', ...assertionClasses]),
overridesToDocsClassMapping: new Map([
['TestType', 'Test'],
['Config', 'TestConfig'],
['FullConfig', 'TestConfig'],
['Project', 'TestProject'],
['PlaywrightWorkerOptions', 'TestOptions'],
['PlaywrightTestOptions', 'TestOptions'],
['PlaywrightWorkerArgs', 'Fixtures'],
['PlaywrightTestArgs', 'Fixtures'],
]),
ignoreMissing: new Set([
'FullConfig.version',
'FullConfig.rootDir',
'SuiteFunction',
'TestFunction',
'PlaywrightWorkerOptions.defaultBrowserType',
'PlaywrightWorkerArgs.playwright',
'Matchers',
]),
doNotExportClassNames: new Set(assertionClasses),
includeExperimental,
});
return await generator.generateTypes(path.join(__dirname, 'overrides-test.d.ts'));
}
/**
* @param {boolean} includeExperimental
* @returns {Promise<string>}
*/
async function generateReporterTypes(includeExperimental) {
const documentation = coreDocumentation.mergeWith(testDocumentation).mergeWith(reporterDocumentation);
const generator = new TypesGenerator({
documentation,
classNamesToGenerate: new Set(reporterDocumentation.classesArray.map(cls => cls.name)),
ignoreMissing: new Set(['FullResult']),
includeExperimental,
});
return await generator.generateTypes(path.join(__dirname, 'overrides-testReporter.d.ts'));
}
async function generateExperimentalTypes() {
const core = await generateCoreTypes(true);
const test = await generateTestTypes(true);
const reporter = await generateReporterTypes(true);
const lines = [
`// This file is generated by ${__filename.substring(path.join(__dirname, '..', '..').length).split(path.sep).join(path.posix.sep)}`,
`declare module 'playwright-core' {`,
...core.split('\n'),
`}`,
`declare module '@playwright/test' {`,
...test.split('\n'),
`}`,
`declare module '@playwright/test/reporter' {`,
...reporter.split('\n'),
`}`,
];
const cutFrom = lines.findIndex(line => line.includes('BEGINGLOBAL'));
const cutTo = lines.findIndex(line => line.includes('ENDGLOBAL'));
lines.splice(cutFrom, cutTo - cutFrom + 1);
return lines.join('\n');
}
/**
* @param {string} filePath
* @param {string} content
* @param {boolean} removeTrailingWhiteSpace
*/
function writeFile(filePath, content) {
function writeFile(filePath, content, removeTrailingWhiteSpace) {
content = content.replace(/\r\n/g, '\n');
if (removeTrailingWhiteSpace)
content = content.replace(/( +)\n/g, '\n'); // remove trailing whitespace
if (os.platform() === 'win32')
content = content.replace(/\r\n/g, '\n').replace(/\n/g, '\r\n');
content = content.replace(/\n/g, '\r\n');
const existing = fs.readFileSync(filePath, 'utf8');
if (existing === content)
return;
@ -493,82 +613,18 @@ class TypesGenerator {
fs.writeFileSync(filePath, content, 'utf8');
}
let hadChanges = false;
const coreTypesDir = path.join(PROJECT_DIR, 'packages', 'playwright-core', 'types');
if (!fs.existsSync(coreTypesDir))
fs.mkdirSync(coreTypesDir)
const testTypesDir = path.join(PROJECT_DIR, 'packages', 'playwright-test', 'types');
if (!fs.existsSync(testTypesDir))
fs.mkdirSync(testTypesDir)
writeFile(path.join(coreTypesDir, 'protocol.d.ts'), fs.readFileSync(path.join(PROJECT_DIR, 'packages', 'playwright-core', 'src', 'server', 'chromium', 'protocol.d.ts'), 'utf8'));
const assertionClasses = new Set(['LocatorAssertions', 'PageAssertions', 'APIResponseAssertions', 'ScreenshotAssertions']);
const apiDocumentation = parseApi(path.join(PROJECT_DIR, 'docs', 'src', 'api'));
apiDocumentation.index();
const apiTypesGenerator = new TypesGenerator({
documentation: apiDocumentation,
classNamesToGenerate: new Set(apiDocumentation.classesArray.map(cls => cls.name).filter(name => !assertionClasses.has(name) && name !== 'PlaywrightAssertions')),
});
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))
apiTypes = apiTypes.replace(new RegExp('\\b' + key + '\\b', 'g'), value);
apiTypes = apiTypes.replace(/( +)\n/g, '\n'); // remove trailing whitespace
writeFile(path.join(coreTypesDir, '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({
documentation: testDocumentation,
classNamesToGenerate: new Set(['TestError', 'TestInfo', 'WorkerInfo', ...assertionClasses]),
overridesToDocsClassMapping: new Map([
['TestType', 'Test'],
['Config', 'TestConfig'],
['FullConfig', 'TestConfig'],
['Project', 'TestProject'],
['PlaywrightWorkerOptions', 'TestOptions'],
['PlaywrightTestOptions', 'TestOptions'],
['PlaywrightWorkerArgs', 'Fixtures'],
['PlaywrightTestArgs', 'Fixtures'],
]),
ignoreMissing: new Set([
'FullConfig.version',
'FullConfig.rootDir',
'SuiteFunction',
'TestFunction',
'PlaywrightWorkerOptions.defaultBrowserType',
'PlaywrightWorkerArgs.playwright',
'Matchers',
]),
doNotExportClassNames: new Set(assertionClasses),
});
let testTypes = await testTypesGenerator.generateTypes(path.join(__dirname, 'overrides-test.d.ts'));
testTypes = testTypes.replace(/( +)\n/g, '\n'); // remove trailing whitespace
writeFile(path.join(testTypesDir, '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({
documentation: testReporterDocumentation,
classNamesToGenerate: new Set(testReporterOnlyDocumentation.classesArray.map(cls => cls.name)),
ignoreMissing: new Set(['FullResult']),
});
let testReporterTypes = await testReporterTypesGenerator.generateTypes(path.join(__dirname, 'overrides-testReporter.d.ts'));
testReporterTypes = testReporterTypes.replace(/( +)\n/g, '\n'); // remove trailing whitespace
writeFile(path.join(testTypesDir, 'testReporter.d.ts'), testReporterTypes);
writeFile(path.join(coreTypesDir, 'protocol.d.ts'), fs.readFileSync(path.join(PROJECT_DIR, 'packages', 'playwright-core', 'src', 'server', 'chromium', 'protocol.d.ts'), 'utf8'), false);
writeFile(path.join(coreTypesDir, 'types.d.ts'), await generateCoreTypes(false), true);
writeFile(path.join(testTypesDir, 'test.d.ts'), await generateTestTypes(false), true);
writeFile(path.join(testTypesDir, 'testReporter.d.ts'), await generateReporterTypes(false), true);
writeFile(path.join(PROJECT_DIR, 'tests', 'config', 'experimental.d.ts'), await generateExperimentalTypes(), true);
process.exit(hadChanges && process.argv.includes('--check-clean') ? 1 : 0);
})().catch(e => {
console.error(e);

View File

@ -15,6 +15,7 @@
*/
import type { APIRequestContext, Browser, BrowserContext, BrowserContextOptions, Page, LaunchOptions, ViewportSize, Geolocation, HTTPCredentials, Locator, APIResponse } from 'playwright-core';
export * from 'playwright-core';
export type ReporterDescription =
['dot'] |
@ -371,7 +372,7 @@ type MakeMatchers<R, T> = BaseMatchers<R, T> & {
ExtraMatchers<T, Locator, LocatorAssertions> &
ExtraMatchers<T, APIResponse, APIResponseAssertions>;
export declare type Expect = {
export type Expect = {
<T = unknown>(actual: T, messageOrOptions?: string | { message?: string }): MakeMatchers<void, T>;
soft: <T = unknown>(actual: T, messageOrOptions?: string | { message?: string }) => MakeMatchers<void, T>;
poll: <T = unknown>(actual: () => T | Promise<T>, messageOrOptions?: string | { message?: string, timeout?: number }) => BaseMatchers<Promise<void>, T> & {
@ -450,12 +451,14 @@ type SupportedExpectProperties =
'toThrow' |
'toThrowError'
// --- BEGINGLOBAL ---
declare global {
export namespace PlaywrightTest {
export interface Matchers<R, T = unknown> {
}
}
}
// --- ENDGLOBAL ---
/**
* These tests are executed in Playwright environment that launches the browser

View File

@ -14,8 +14,8 @@
* limitations under the License.
*/
import type { FullConfig, FullProject, TestStatus, TestError } from './test';
export type { FullConfig, TestStatus, TestError } from './test';
import type { FullConfig, FullProject, TestStatus, TestError } from '@playwright/test';
export type { FullConfig, TestStatus, TestError } from '@playwright/test';
export interface Suite {
project(): FullProject | undefined;

View File

@ -13,12 +13,12 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Protocol } from './protocol';
import { Protocol } from 'playwright-core/types/protocol';
import { ChildProcess } from 'child_process';
import { EventEmitter } from 'events';
import { Readable } from 'stream';
import { ReadStream } from 'fs';
import { Serializable, EvaluationArgument, PageFunction, PageFunctionOn, SmartHandle, ElementHandleForTag, BindingSource } from './structs';
import { Serializable, EvaluationArgument, PageFunction, PageFunctionOn, SmartHandle, ElementHandleForTag, BindingSource } from 'playwright-core/types/structs';
type PageWaitForSelectorOptionsNotHidden = PageWaitForSelectorOptions & {
state?: 'visible'|'attached';