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"`. 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 ## screenshot-option-fonts
* langs: js
* experimental
- `fonts` <[ScreenshotFonts]<"ready"|"nowait">> - `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"`. 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-quality-%%
- %%-screenshot-option-path-%% - %%-screenshot-option-path-%%
- %%-screenshot-option-scale-%% - %%-screenshot-option-scale-%%
- %%-screenshot-option-fonts-%%
- %%-screenshot-option-caret-%% - %%-screenshot-option-caret-%%
- %%-screenshot-option-type-%% - %%-screenshot-option-type-%%
- %%-screenshot-option-mask-%% - %%-screenshot-option-mask-%%

View file

@ -36,7 +36,9 @@
"./lib/utils/timeoutRunner": "./lib/utils/timeoutRunner.js", "./lib/utils/timeoutRunner": "./lib/utils/timeoutRunner.js",
"./lib/remote/playwrightServer": "./lib/remote/playwrightServer.js", "./lib/remote/playwrightServer": "./lib/remote/playwrightServer.js",
"./lib/remote/playwrightClient": "./lib/remote/playwrightClient.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", "types": "types/types.d.ts",
"bin": { "bin": {

View file

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

View file

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

View file

@ -23,7 +23,7 @@ import type { Frame } from './frames';
import type { ParsedSelector } from './isomorphic/selectorParser'; import type { ParsedSelector } from './isomorphic/selectorParser';
import type * as types from './types'; import type * as types from './types';
import type { Progress } from './progress'; import type { Progress } from './progress';
import { assert } from '../utils'; import { assert, experimentalFeaturesEnabled } from '../utils';
import { MultiMap } from '../utils/multimap'; import { MultiMap } from '../utils/multimap';
declare global { declare global {
@ -322,6 +322,9 @@ function trimClipToSize(clip: types.Rect, size: types.Size): types.Rect {
} }
function validateScreenshotOptions(options: ScreenshotOptions): 'png' | 'jpeg' { 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; let format: 'png' | 'jpeg' | null = null;
// options.type takes precedence over inferring the type from options.path // 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). // 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. * 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 * Can be converted to JSON

View file

@ -14,12 +14,12 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
import { Protocol } from './protocol'; import { Protocol } from 'playwright-core/types/protocol';
import { ChildProcess } from 'child_process'; import { ChildProcess } from 'child_process';
import { EventEmitter } from 'events'; import { EventEmitter } from 'events';
import { Readable } from 'stream'; import { Readable } from 'stream';
import { ReadStream } from 'fs'; 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 & { type PageWaitForSelectorOptionsNotHidden = PageWaitForSelectorOptions & {
state?: 'visible'|'attached'; state?: 'visible'|'attached';

View file

@ -14,6 +14,5 @@
* limitations under the License. * limitations under the License.
*/ */
export * from 'playwright-core';
export * from './types/test'; export * from './types/test';
export { default } 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 [page, locator] = pageOrLocator.constructor.name === 'Page' ? [(pageOrLocator as PageEx), undefined] : [(pageOrLocator as Locator).page() as PageEx, pageOrLocator as LocatorEx];
const screenshotOptions = { const screenshotOptions = {
animations: config?.animations ?? 'disabled', animations: config?.animations ?? 'disabled',
_fonts: config?.fonts ?? 'ready', fonts: process.env.PLAYWRIGHT_EXPERIMENTAL_FEATURES ? (config?.fonts ?? 'ready') : undefined,
scale: config?.scale ?? 'css', scale: config?.scale ?? 'css',
caret: config?.caret ?? 'hide', caret: config?.caret ?? 'hide',
...helper.allOptions, ...helper.allOptions,

View file

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

View file

@ -15,8 +15,8 @@
* limitations under the License. * limitations under the License.
*/ */
import type { FullConfig, FullProject, TestStatus, TestError } from './test'; import type { FullConfig, FullProject, TestStatus, TestError } from '@playwright/test';
export type { FullConfig, TestStatus, TestError } from './test'; export type { FullConfig, TestStatus, TestError } from '@playwright/test';
/** /**
* `Suite` is a group of tests. All tests in Playwright Test form the following hierarchy: * `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. * limitations under the License.
*/ */
// eslint-disable-next-line spaced-comment
/// <reference path="./experimental.d.ts" />
import type { Fixtures } from '@playwright/test'; import type { Fixtures } from '@playwright/test';
import type { ChildProcess } from 'child_process'; import type { ChildProcess } from 'child_process';
import { execSync, spawn } 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() { async setup() {
this._impl = await start({ 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; return this._impl.playwright;
} }

View file

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

View file

@ -785,13 +785,13 @@ it.describe('page screenshot animations', () => {
const noIconsScreenshot = await page.screenshot(); const noIconsScreenshot = await page.screenshot();
// Make sure screenshot times out while webfont is stalled. // Make sure screenshot times out while webfont is stalled.
const error = await page.screenshot({ const error = await page.screenshot({
_fonts: 'ready', fonts: 'ready',
timeout: 200, timeout: 200,
} as any).catch(e => e); }).catch(e => e);
expect(error.message).toContain('waiting for fonts to load...'); expect(error.message).toContain('waiting for fonts to load...');
expect(error.message).toContain('Timeout 200ms exceeded'); expect(error.message).toContain('Timeout 200ms exceeded');
const [iconsScreenshot] = await Promise.all([ const [iconsScreenshot] = await Promise.all([
page.screenshot({ _fonts: 'ready' } as any), page.screenshot({ fonts: 'ready' }),
server.serveFile(serverRequest, serverResponse), server.serveFile(serverRequest, serverResponse),
]); ]);
expect(iconsScreenshot).toMatchSnapshot('screenshot-web-font.png', { 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) => { test('should disable animations by default', async ({ runInlineTest }, testInfo) => {
const cssTransitionURL = pathToFileURL(path.join(__dirname, '../assets/css-transition.html')); const cssTransitionURL = pathToFileURL(path.join(__dirname, '../assets/css-transition.html'));
const result = await runInlineTest({ const result = await runInlineTest({
...playwrightConfig({}),
'a.spec.js': ` 'a.spec.js': `
pwt.test('is a test', async ({ page }) => { pwt.test('is a test', async ({ page }) => {
await page.goto('${cssTransitionURL}'); await page.goto('${cssTransitionURL}');
@ -156,11 +157,7 @@ test('should report _toHaveScreenshot step with expectation name in title', asyn
} }
module.exports = Reporter; module.exports = Reporter;
`, `,
'playwright.config.ts': ` ...playwrightConfig({ reporter: './reporter' }),
module.exports = {
reporter: './reporter',
};
`,
'a.spec.js': ` 'a.spec.js': `
pwt.test('is a test', async ({ page }) => { pwt.test('is a test', async ({ page }) => {
// Named expectation. // Named expectation.
@ -371,8 +368,7 @@ test('should compile with different option combinations', async ({ runTSC }) =>
maxDiffPixelRatio: 0.2, maxDiffPixelRatio: 0.2,
animations: "disabled", animations: "disabled",
omitBackground: true, omitBackground: true,
// TODO: uncomment when enabling "fonts". fonts: "nowait",
// fonts: "nowait",
caret: "initial", caret: "initial",
scale: "device", scale: "device",
timeout: 1000, 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 }) => { test('should fail when given non-png snapshot name', async ({ runInlineTest }) => {
const result = await runInlineTest({ const result = await runInlineTest({
...playwrightConfig({}),
'a.spec.js': ` 'a.spec.js': `
pwt.test('is a test', async ({ page }) => { pwt.test('is a test', async ({ page }) => {
await expect(page)._toHaveScreenshot('snapshot.jpeg'); 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 }) => { test('should fail when given buffer', async ({ runInlineTest }) => {
const result = await runInlineTest({ const result = await runInlineTest({
...playwrightConfig({}),
'a.spec.js': ` 'a.spec.js': `
pwt.test('is a test', async ({ page }) => { pwt.test('is a test', async ({ page }) => {
await expect(Buffer.from([1]))._toHaveScreenshot(); 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 }) => { test('should throw for invalid maxDiffPixels values', async ({ runInlineTest }) => {
expect((await runInlineTest({ expect((await runInlineTest({
...playwrightConfig({}),
'a.spec.js': ` 'a.spec.js': `
pwt.test('is a test', async ({ page }) => { pwt.test('is a test', async ({ page }) => {
await expect(page)._toHaveScreenshot({ 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 }) => { test('should throw for invalid maxDiffPixelRatio values', async ({ runInlineTest }) => {
expect((await runInlineTest({ expect((await runInlineTest({
...playwrightConfig({}),
'a.spec.js': ` 'a.spec.js': `
pwt.test('is a test', async ({ page }) => { pwt.test('is a test', async ({ page }) => {
await expect(page)._toHaveScreenshot({ await expect(page)._toHaveScreenshot({

View file

@ -74,7 +74,7 @@ class ApiParser {
continue; 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); this.classes.set(clazz.name, clazz);
} }
@ -102,11 +102,11 @@ class ApiParser {
const comments = extractComments(spec); const comments = extractComments(spec);
let member; let member;
if (match[1] === 'event') 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') 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])) { 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')) if (match[1].includes('async'))
member.async = true; member.async = true;
if (match[1].includes('optional')) if (match[1].includes('optional'))
@ -167,7 +167,7 @@ class ApiParser {
let options = method.argsArray.find(o => o.name === 'options'); let options = method.argsArray.find(o => o.name === 'options');
if (!options) { if (!options) {
const type = new Documentation.Type('Object', []); 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); method.argsArray.push(options);
} }
const p = this.parseProperty(spec); const p = this.parseProperty(spec);
@ -188,7 +188,7 @@ class ApiParser {
const name = text.substring(0, typeStart).replace(/\`/g, '').trim(); const name = text.substring(0, typeStart).replace(/\`/g, '').trim();
const comments = extractComments(spec); const comments = extractComments(spec);
const { type, optional } = this.parseType(param); 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 { name, text } = parseVariable(child.text);
const comments = /** @type {MarkdownNode[]} */ ([{ type: 'text', text }]); const comments = /** @type {MarkdownNode[]} */ ([{ type: 'text', text }]);
const childType = this.parseType(child); 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); const type = Documentation.Type.parse(arg.type, properties);
return { type, optional: arg.optional }; return { type, optional: arg.optional };
@ -300,13 +300,11 @@ function applyTemplates(body, params) {
* @returns {MarkdownNode[]} * @returns {MarkdownNode[]}
*/ */
function extractComments(item) { function extractComments(item) {
return (item.children || []).filter(c => { return childrenWithoutProperties(item).filter(c => {
if (c.type.startsWith('h')) if (c.type.startsWith('h'))
return false; return false;
if (c.type === 'li' && c.liType === 'default') if (c.type === 'li' && c.liType === 'default')
return false; return false;
if (c.type === 'li' && c.text.startsWith('langs:'))
return false;
return true; return true;
}); });
} }
@ -346,12 +344,27 @@ function extractLangs(spec) {
return {}; 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 * @param {MarkdownNode} spec
* @returns {MarkdownNode[]} * @returns {MarkdownNode[]}
*/ */
function childrenWithoutProperties(spec) { 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} * @return {!Documentation}
*/ */
mergeWith(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(); 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() { index() {
for (const cls of this.classesArray) { for (const cls of this.classesArray) {
this.classes.set(cls.name, cls); this.classes.set(cls.name, cls);
@ -149,23 +161,28 @@ class Documentation {
clazz.visit(item => item.comment = generateSourceCodeComment(item.spec)); clazz.visit(item => item.comment = generateSourceCodeComment(item.spec));
} }
clone() {
return new Documentation(this.classesArray.map(cls => cls.clone()));
}
} }
Documentation.Class = class { Documentation.Class = class {
/** /**
* @param {Langs} langs * @param {Langs} langs
* @param {boolean} experimental
* @param {string} name * @param {string} name
* @param {!Array<!Documentation.Member>} membersArray * @param {!Array<!Documentation.Member>} membersArray
* @param {?string=} extendsName * @param {?string=} extendsName
* @param {MarkdownNode[]=} spec * @param {MarkdownNode[]=} spec
*/ */
constructor(langs, name, membersArray, extendsName = null, spec = undefined) { constructor(langs, experimental, name, membersArray, extendsName = null, spec = undefined) {
this.langs = langs; this.langs = langs;
this.experimental = experimental;
this.name = name; this.name = name;
this.membersArray = membersArray; this.membersArray = membersArray;
this.spec = spec; this.spec = spec;
this.extends = extendsName; this.extends = extendsName;
this.comment = ''; this.comment = '';
this.index(); this.index();
const match = name.match(/(API|JS|CDP|[A-Z])(.*)/); const match = name.match(/(API|JS|CDP|[A-Z])(.*)/);
this.varName = match[1].toLowerCase() + match[2]; 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 * @param {string} lang
*/ */
@ -218,6 +241,17 @@ Documentation.Class = class {
this.membersArray = membersArray; 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) { validateOrder(errors, cls) {
const members = this.membersArray; const members = this.membersArray;
// Events should go first. // Events should go first.
@ -280,15 +314,17 @@ Documentation.Member = class {
/** /**
* @param {string} kind * @param {string} kind
* @param {Langs} langs * @param {Langs} langs
* @param {boolean} experimental
* @param {string} name * @param {string} name
* @param {?Documentation.Type} type * @param {?Documentation.Type} type
* @param {!Array<!Documentation.Member>} argsArray * @param {!Array<!Documentation.Member>} argsArray
* @param {MarkdownNode[]=} spec * @param {MarkdownNode[]=} spec
* @param {boolean=} required * @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.kind = kind;
this.langs = langs; this.langs = langs;
this.experimental = experimental;
this.name = name; this.name = name;
this.type = type; this.type = type;
this.spec = spec; this.spec = spec;
@ -355,13 +391,27 @@ Documentation.Member = class {
overriddenArg.filterForLanguage(lang); overriddenArg.filterForLanguage(lang);
if (overriddenArg.name === 'options' && !overriddenArg.type.properties.length) if (overriddenArg.name === 'options' && !overriddenArg.type.properties.length)
continue; continue;
overriddenArg.type.filterForLanguage(lang);
argsArray.push(overriddenArg); argsArray.push(overriddenArg);
} }
this.argsArray = argsArray; 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() { 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.async = this.async;
result.paramOrOption = this.paramOrOption; result.paramOrOption = this.paramOrOption;
return result; return result;
@ -369,37 +419,40 @@ Documentation.Member = class {
/** /**
* @param {Langs} langs * @param {Langs} langs
* @param {boolean} experimental
* @param {string} name * @param {string} name
* @param {!Array<!Documentation.Member>} argsArray * @param {!Array<!Documentation.Member>} argsArray
* @param {?Documentation.Type} returnType * @param {?Documentation.Type} returnType
* @param {MarkdownNode[]=} spec * @param {MarkdownNode[]=} spec
* @return {!Documentation.Member} * @return {!Documentation.Member}
*/ */
static createMethod(langs, name, argsArray, returnType, spec) { static createMethod(langs, experimental, name, argsArray, returnType, spec) {
return new Documentation.Member('method', langs, name, returnType, argsArray, spec); return new Documentation.Member('method', langs, experimental, name, returnType, argsArray, spec);
} }
/** /**
* @param {!Langs} langs * @param {!Langs} langs
* @param {boolean} experimental
* @param {!string} name * @param {!string} name
* @param {!Documentation.Type} type * @param {!Documentation.Type} type
* @param {!MarkdownNode[]=} spec * @param {!MarkdownNode[]=} spec
* @param {boolean=} required * @param {boolean=} required
* @return {!Documentation.Member} * @return {!Documentation.Member}
*/ */
static createProperty(langs, name, type, spec, required) { static createProperty(langs, experimental, name, type, spec, required) {
return new Documentation.Member('property', langs, name, type, [], spec, required); return new Documentation.Member('property', langs, experimental, name, type, [], spec, required);
} }
/** /**
* @param {Langs} langs * @param {Langs} langs
* @param {boolean} experimental
* @param {string} name * @param {string} name
* @param {?Documentation.Type=} type * @param {?Documentation.Type=} type
* @param {MarkdownNode[]=} spec * @param {MarkdownNode[]=} spec
* @return {!Documentation.Member} * @return {!Documentation.Member}
*/ */
static createEvent(langs, name, type = null, spec) { static createEvent(langs, experimental, name, type = null, spec) {
return new Documentation.Member('event', langs, name, type, [], spec); return new Documentation.Member('event', langs, experimental, name, type, [], spec);
} }
/** /**
@ -488,16 +541,17 @@ Documentation.Type = class {
*/ */
constructor(name, properties) { constructor(name, properties) {
this.name = name.replace(/^\[/, '').replace(/\]$/, ''); this.name = name.replace(/^\[/, '').replace(/\]$/, '');
/** @type {Documentation.Member[] | undefined} */
this.properties = this.name === 'Object' ? properties : undefined; this.properties = this.name === 'Object' ? properties : undefined;
/** @type {Documentation.Type[]} | undefined */ /** @type {Documentation.Type[] | undefined} */
this.union; this.union;
/** @type {Documentation.Type[]} | undefined */ /** @type {Documentation.Type[] | undefined} */
this.args; this.args;
/** @type {Documentation.Type} | undefined */ /** @type {Documentation.Type | undefined} */
this.returnType; this.returnType;
/** @type {Documentation.Type[]} | undefined */ /** @type {Documentation.Type[] | undefined} */
this.templates; this.templates;
/** @type {string | undefined } */ /** @type {string | undefined} */
this.expression; 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[]} * @returns {Documentation.Member[]}
*/ */
@ -550,6 +618,19 @@ Documentation.Type = class {
this.properties = properties; 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 * @param {Documentation.Type[]} result
*/ */

View file

@ -26,8 +26,6 @@ const { parseOverrides } = require('./parseOverrides');
const exported = require('./exported.json'); const exported = require('./exported.json');
const { parseApi } = require('../doclint/api_parser'); const { parseApi } = require('../doclint/api_parser');
/** @typedef {import('../doclint/documentation').Member} Member */
Error.stackTraceLimit = 50; Error.stackTraceLimit = 50;
class TypesGenerator { class TypesGenerator {
@ -38,10 +36,11 @@ class TypesGenerator {
* overridesToDocsClassMapping?: Map<string, string>, * overridesToDocsClassMapping?: Map<string, string>,
* ignoreMissing?: Set<string>, * ignoreMissing?: Set<string>,
* doNotExportClassNames?: Set<string>, * doNotExportClassNames?: Set<string>,
* includeExperimental?: boolean,
* }} options * }} options
*/ */
constructor(options) { constructor(options) {
/** @type {Array<{name: string, properties: Member[]}>} */ /** @type {Array<{name: string, properties: Documentation.Member[]}>} */
this.objectDefinitions = []; this.objectDefinitions = [];
/** @type {Set<string>} */ /** @type {Set<string>} */
this.handledMethods = new Set(); this.handledMethods = new Set();
@ -50,6 +49,10 @@ class TypesGenerator {
this.overridesToDocsClassMapping = options.overridesToDocsClassMapping || new Map(); this.overridesToDocsClassMapping = options.overridesToDocsClassMapping || new Map();
this.ignoreMissing = options.ignoreMissing || new Set(); this.ignoreMissing = options.ignoreMissing || new Set();
this.doNotExportClassNames = options.doNotExportClassNames || 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>} * @returns {Promise<string>}
*/ */
async generateTypes(overridesFile) { async generateTypes(overridesFile) {
this.documentation.filterForLanguage('js');
this.documentation.copyDocsFromSuperclasses([]);
const createMarkdownLink = (member, text) => { const createMarkdownLink = (member, text) => {
const className = toKebabCase(member.clazz.name); const className = toKebabCase(member.clazz.name);
const memberName = toKebabCase(member.name); const memberName = toKebabCase(member.name);
@ -238,7 +238,7 @@ class TypesGenerator {
type, type,
params, params,
eventName, eventName,
comment: value.comment comment: value.comment,
}); });
} }
return descriptions; return descriptions;
@ -366,10 +366,21 @@ class TypesGenerator {
return this.stringifySimpleType(type, indent, ...namespace); return this.stringifySimpleType(type, indent, ...namespace);
} }
/**
* @param {Documentation.Member[]} properties
* @param {string} name
* @param {string=} indent
* @returns {string}
*/
stringifyObjectType(properties, name, indent = '') { stringifyObjectType(properties, name, indent = '') {
const parts = []; const parts = [];
parts.push(`{`); 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 + '}'); parts.push(indent + '}');
return parts.join('\n'); return parts.join('\n');
} }
@ -476,15 +487,124 @@ class TypesGenerator {
} }
(async function () { (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} filePath
* @param {string} content * @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') 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'); const existing = fs.readFileSync(filePath, 'utf8');
if (existing === content) if (existing === content)
return; return;
@ -493,82 +613,18 @@ class TypesGenerator {
fs.writeFileSync(filePath, content, 'utf8'); fs.writeFileSync(filePath, content, 'utf8');
} }
let hadChanges = false;
const coreTypesDir = path.join(PROJECT_DIR, 'packages', 'playwright-core', 'types'); const coreTypesDir = path.join(PROJECT_DIR, 'packages', 'playwright-core', 'types');
if (!fs.existsSync(coreTypesDir)) if (!fs.existsSync(coreTypesDir))
fs.mkdirSync(coreTypesDir) fs.mkdirSync(coreTypesDir)
const testTypesDir = path.join(PROJECT_DIR, 'packages', 'playwright-test', 'types'); const testTypesDir = path.join(PROJECT_DIR, 'packages', 'playwright-test', 'types');
if (!fs.existsSync(testTypesDir)) if (!fs.existsSync(testTypesDir))
fs.mkdirSync(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')); 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);
const assertionClasses = new Set(['LocatorAssertions', 'PageAssertions', 'APIResponseAssertions', 'ScreenshotAssertions']); writeFile(path.join(testTypesDir, 'test.d.ts'), await generateTestTypes(false), true);
const apiDocumentation = parseApi(path.join(PROJECT_DIR, 'docs', 'src', 'api')); writeFile(path.join(testTypesDir, 'testReporter.d.ts'), await generateReporterTypes(false), true);
apiDocumentation.index(); writeFile(path.join(PROJECT_DIR, 'tests', 'config', 'experimental.d.ts'), await generateExperimentalTypes(), true);
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);
process.exit(hadChanges && process.argv.includes('--check-clean') ? 1 : 0); process.exit(hadChanges && process.argv.includes('--check-clean') ? 1 : 0);
})().catch(e => { })().catch(e => {
console.error(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'; import type { APIRequestContext, Browser, BrowserContext, BrowserContextOptions, Page, LaunchOptions, ViewportSize, Geolocation, HTTPCredentials, Locator, APIResponse } from 'playwright-core';
export * from 'playwright-core';
export type ReporterDescription = export type ReporterDescription =
['dot'] | ['dot'] |
@ -371,7 +372,7 @@ type MakeMatchers<R, T> = BaseMatchers<R, T> & {
ExtraMatchers<T, Locator, LocatorAssertions> & ExtraMatchers<T, Locator, LocatorAssertions> &
ExtraMatchers<T, APIResponse, APIResponseAssertions>; ExtraMatchers<T, APIResponse, APIResponseAssertions>;
export declare type Expect = { export type Expect = {
<T = unknown>(actual: T, messageOrOptions?: string | { message?: string }): MakeMatchers<void, T>; <T = unknown>(actual: T, messageOrOptions?: string | { message?: string }): MakeMatchers<void, T>;
soft: <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> & { poll: <T = unknown>(actual: () => T | Promise<T>, messageOrOptions?: string | { message?: string, timeout?: number }) => BaseMatchers<Promise<void>, T> & {
@ -450,12 +451,14 @@ type SupportedExpectProperties =
'toThrow' | 'toThrow' |
'toThrowError' 'toThrowError'
// --- BEGINGLOBAL ---
declare global { declare global {
export namespace PlaywrightTest { export namespace PlaywrightTest {
export interface Matchers<R, T = unknown> { export interface Matchers<R, T = unknown> {
} }
} }
} }
// --- ENDGLOBAL ---
/** /**
* These tests are executed in Playwright environment that launches the browser * These tests are executed in Playwright environment that launches the browser

View file

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

View file

@ -13,12 +13,12 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
import { Protocol } from './protocol'; import { Protocol } from 'playwright-core/types/protocol';
import { ChildProcess } from 'child_process'; import { ChildProcess } from 'child_process';
import { EventEmitter } from 'events'; import { EventEmitter } from 'events';
import { Readable } from 'stream'; import { Readable } from 'stream';
import { ReadStream } from 'fs'; 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 & { type PageWaitForSelectorOptionsNotHidden = PageWaitForSelectorOptions & {
state?: 'visible'|'attached'; state?: 'visible'|'attached';