Something went wrong. Try again.
[READ-ONLY] Mirror of https://github.com/vitest-dev/vitest. Next generation testing framework powered by Vite. vitest.dev
test testing-tools vite
Something went wrong. Try again.
29 kB · 816 lines
TypeScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817/* oxlint-disable typescript/method-signature-style */
import type { CustomComparatorsRegistry } from '@vitest/browser'import type { MockedModule } from '@vitest/mocker'import type { Browser, BrowserContext, BrowserContextOptions, ConnectOptions, Frame, FrameLocator, LaunchOptions, Page, CDPSession as PlaywrightCDPSession,} from 'playwright'import type { SourceMap } from 'rollup'import type { ResolvedConfig } from 'vite'import type { Locator, ScreenshotComparatorRegistry, ScreenshotMatcherOptions,} from 'vitest/browser'import type { BrowserCommand, BrowserModuleMocker, BrowserProvider, BrowserProviderOption, CDPSession, TestProject,} from 'vitest/node'import { defineBrowserProvider } from '@vitest/browser'import { createManualModuleSource } from '@vitest/mocker/node'import { resolve } from 'pathe'import c from 'tinyrainbow'import { createDebugger, isCSSRequest } from 'vitest/node'import commands from './commands'import { distRoot } from './constants'
const debug = createDebugger('vitest:browser:playwright')
const playwrightBrowsers = ['firefox', 'webkit', 'chromium'] as consttype PlaywrightBrowser = (typeof playwrightBrowsers)[number]
// Enable intercepting of requests made by service workers - experimental API is only available in Chromium based browsers// Requests from service workers are only available on context.route() https://playwright.dev/docs/service-workers-experimentalprocess.env.PW_EXPERIMENTAL_SERVICE_WORKER_NETWORK_EVENTS ??= '1'
export interface PlaywrightProviderOptions { /** * The options passed down to [`playwright.connect`](https://playwright.dev/docs/api/class-browsertype#browser-type-launch) method. * @see {@link https://playwright.dev/docs/api/class-browsertype#browser-type-launch} */ launchOptions?: Omit<LaunchOptions, 'tracesDir'> /** * The options passed down to [`playwright.connect`](https://playwright.dev/docs/api/class-browsertype#browser-type-connect) method. * * This is used only if you connect remotely to the playwright instance via a WebSocket connection. * @see {@link https://playwright.dev/docs/api/class-browsertype#browser-type-connect} */ connectOptions?: ConnectOptions & { wsEndpoint: string } /** * The options passed down to [`browser.newContext`](https://playwright.dev/docs/api/class-browser#browser-new-context) method. * @see {@link https://playwright.dev/docs/api/class-browser#browser-new-context} */ contextOptions?: Omit<BrowserContextOptions, 'ignoreHTTPSErrors' | 'serviceWorkers'> /** * The maximum time in milliseconds to wait for `userEvent` action to complete. * @default 0 (no timeout) */ actionTimeout?: number
/** * Use a persistent context instead of a regular browser context. * This allows browser state (cookies, localStorage, DevTools settings, etc.) to persist between test runs. * When set to `true`, the user data is stored in `./node_modules/.cache/vitest-playwright-user-data`. * When set to a string, the value is used as the path to the user data directory. * * Note: This option is ignored when running tests in parallel (e.g. headless with fileParallelism enabled) * because persistent context cannot be shared across parallel sessions. * @default false * @see {@link https://playwright.dev/docs/api/class-browsertype#browser-type-launch-persistent-context} */ persistentContext?: boolean | string}
export function playwright( options: PlaywrightProviderOptions = {},): BrowserProviderOption<PlaywrightProviderOptions> { return defineBrowserProvider({ name: 'playwright', supportedBrowser: playwrightBrowsers, options, prewarm(ctx) { prewarmBrowser(ctx, options) }, providerFactory(project) { return new PlaywrightBrowserProvider(project, options) }, })}
interface WarmBrowser { promise: Promise<Browser> launchOptionsJson: string pending: Set<WarmBrowser>}
// the subset of `TestProject` the launch-option resolution needs; `prewarm`// runs before the project exists and receives the same shapeinterface LaunchContext { config: TestProject['config'] vitest: TestProject['vitest']}
// The resolved config object is passed unchanged to the eventual TestProject,// so it identifies the browser this project can adopt.const warmBrowsers = new WeakMap<LaunchContext['config'], WarmBrowser>()const pendingWarmBrowsers = new WeakMap<LaunchContext['vitest'], Set<WarmBrowser>>()
// starts importing playwright and launching the browser while the node side// is still creating the vite server, so the launch latency overlaps it. The// launch options are resolved by the same code as the real launch — if they// still differ by the time the provider opens the browser, the warm instance// is discarded, so this is always safefunction prewarmBrowser(project: LaunchContext, options: PlaywrightProviderOptions): void { const browserName = project.config.browser.name if ( options.connectOptions || options.persistentContext || // don't speculate on debugging flows project.vitest.config.inspector.enabled ) { return } if (!browserName || !(playwrightBrowsers as readonly string[]).includes(browserName)) { return } if (warmBrowsers.has(project.config)) { return } let pending = pendingWarmBrowsers.get(project.vitest) if (!pending) { const pendingBrowsers = new Set<WarmBrowser>() pending = pendingBrowsers pendingWarmBrowsers.set(project.vitest, pendingBrowsers) // Browsers whose projects never initialize a provider (they have no test // files to run) are cleaned up when Vitest closes. project.vitest.onClose(() => closeWarmBrowsers(pendingBrowsers)) } const launchOptions = resolveLaunchOptions( project.config.browser, project.vitest.config.inspector, options, browserName, ) const entry: WarmBrowser = { launchOptionsJson: JSON.stringify(launchOptions), pending, promise: (async () => { debug?.('[%s] prewarming the browser', browserName) const playwright = await import('playwright') return playwright[browserName as PlaywrightBrowser].launch(launchOptions) })(), } // if the warm launch fails, drop it so the real launch retries // and surfaces the error through the normal path entry.promise.catch(() => { if (warmBrowsers.get(project.config) === entry) { warmBrowsers.delete(project.config) entry.pending.delete(entry) } }) pending.add(entry) warmBrowsers.set(project.config, entry)}
function takeWarmBrowser(config: LaunchContext['config']): WarmBrowser | undefined { const warm = warmBrowsers.get(config) if (warm) { warmBrowsers.delete(config) warm.pending.delete(warm) } return warm}
async function closeWarmBrowsers(pending: Set<WarmBrowser>): Promise<void> { const closing = Array.from(pending, (warm) => warm.promise.then((browser) => browser.close()).catch(() => {}), ) pending.clear() await Promise.all(closing)}
function resolveLaunchOptions( browser: TestProject['config']['browser'], inspector: TestProject['vitest']['config']['inspector'], providerOptions: PlaywrightProviderOptions, browserName: string,): LaunchOptions { const launchOptions: LaunchOptions = { ...providerOptions.launchOptions, headless: browser.headless, }
if (typeof browser.trace === 'object' && browser.trace.tracesDir) { launchOptions.tracesDir = browser.trace.tracesDir }
if (inspector.enabled) { // NodeJS equivalent defaults: https://nodejs.org/en/learn/getting-started/debugging#enable-inspector const port = inspector.port || 9229
launchOptions.args ||= [] launchOptions.args.push(`--remote-debugging-port=${port}`) }
// start Vitest UI maximized only on supported browsers if (browser.ui && browserName === 'chromium') { if (!launchOptions.args) { launchOptions.args = [] } if ( !launchOptions.args.includes('--start-maximized') && !launchOptions.args.includes('--start-fullscreen') ) { launchOptions.args.push('--start-maximized') } }
return launchOptions}
export class PlaywrightBrowserProvider implements BrowserProvider { public name = 'playwright' as const public supportsParallelism = true
public browser: Browser | null = null public persistentContext: BrowserContext | null = null
public contexts: Map<string, BrowserContext> = new Map() public pages: Map<string, Page> = new Map() public mocker: BrowserModuleMocker public browserName: PlaywrightBrowser
private browserPromise: Promise<Browser> | null = null private closing = false
public tracingContexts: Set<string> = new Set() public pendingTraces: Map<string, string> = new Map()
public initScripts: string[] = [resolve(distRoot, 'locators.js')]
constructor( private project: TestProject, private options: PlaywrightProviderOptions, ) { this.browserName = project.config.browser.name as PlaywrightBrowser this.mocker = this.createMocker()
for (const [name, command] of Object.entries(commands)) { project.browser!.registerCommand(name as any, command as BrowserCommand) }
// make sure the traces are finished if the test hangs process.on('SIGTERM', this.onSIGTERM) }
private onSIGTERM = () => { if (!this.browser) { return } const promises = [] for (const [trace, contextId] of this.pendingTraces.entries()) { promises.push( (() => { const context = this.contexts.get(contextId) return context?.tracing.stopChunk({ path: trace }) })(), ) } return Promise.allSettled(promises) }
private async openBrowser(openBrowserOptions: { parallel: boolean }) { await this._throwIfClosing()
if (this.browserPromise) { debug?.('[%s] the browser is resolving, reusing the promise', this.browserName) return this.browserPromise }
if (this.browser) { debug?.('[%s] the browser is resolved, reusing it', this.browserName) return this.browser }
this.browserPromise = (async () => { const playwright = await import('playwright')
const launchOptions = resolveLaunchOptions( this.project.config.browser, this.project.vitest.config.inspector, this.options, this.browserName, )
const inspector = this.project.vitest.config.inspector if (inspector.enabled) { const port = inspector.port || 9229 const host = inspector.host || '127.0.0.1'
if (host !== 'localhost' && host !== '127.0.0.1' && host !== '::1') { this.project.vitest.logger.warn( `Custom inspector host "${host}" will be ignored. Chromium only allows remote debugging on localhost.`, ) } this.project.vitest.logger.log(`Debugger listening on ws://127.0.0.1:${port}`) }
debug?.( '[%s] initializing the browser with launch options: %O', this.browserName, launchOptions, )
if (this.options.connectOptions) { let { wsEndpoint, headers = {}, ...connectOptions } = this.options.connectOptions if ('x-playwright-launch-options' in headers) { this.project.vitest.logger.warn( c.yellow( 'Detected "x-playwright-launch-options" in connectOptions.headers. Provider config launchOptions is ignored.', ), ) } else { headers = { ...headers, 'x-playwright-launch-options': JSON.stringify(launchOptions) } } this.browser = await playwright[this.browserName].connect(wsEndpoint, { ...connectOptions, headers, }) this.browserPromise = null return this.browser }
let persistentContextOption = this.options.persistentContext if (persistentContextOption && openBrowserOptions.parallel) { persistentContextOption = false this.project.vitest.logger.warn( c.yellow( `The persistentContext option is ignored because tests are running in parallel.`, ), ) } if (persistentContextOption) { const userDataDir = typeof this.options.persistentContext === 'string' ? this.options.persistentContext : './node_modules/.cache/vitest-playwright-user-data' // TODO: how to avoid default "about" page? this.persistentContext = await playwright[this.browserName].launchPersistentContext( userDataDir, { ...launchOptions, ...this.getContextOptions(), }, ) this.browser = this.persistentContext.browser()! } else { const warm = takeWarmBrowser(this.project.config) if (warm && warm.launchOptionsJson === JSON.stringify(launchOptions)) { const browser = await warm.promise.catch(() => null) if (browser?.isConnected()) { debug?.('[%s] adopting the prewarmed browser', this.browserName) this.browser = browser this.browserPromise = null return this.browser } } else if (warm) { debug?.('[%s] discarding the prewarmed browser, launch options changed', this.browserName) void warm.promise.then((browser) => browser.close()).catch(() => {}) } this.browser = await playwright[this.browserName].launch(launchOptions) } this.browserPromise = null return this.browser })()
return this.browserPromise }
private createMocker(): BrowserModuleMocker { const idPredicates = new Map<string, (url: URL) => boolean>() const sessionIds = new Map<string, Set<string>>()
function createPredicate(url: string) { const moduleUrl = new URL(url, 'http://localhost') const predicate = (url: URL) => { if (url.searchParams.has('_vitest_original')) { return false }
// different modules, ignore request if (url.pathname !== moduleUrl.pathname) { return false }
url.searchParams.delete('t') url.searchParams.delete('v') url.searchParams.delete('import')
// different search params, ignore request if (url.searchParams.size !== moduleUrl.searchParams.size) { return false }
// check that all search params are the same for (const [param, value] of url.searchParams.entries()) { if (moduleUrl.searchParams.get(param) !== value) { return false } }
return true } return { url: moduleUrl.href, predicate } }
function predicateKey(sessionId: string, url: string) { return `${sessionId}:${url}` }
return { register: async (sessionId: string, module: MockedModule): Promise<void> => { const page = this.getPage(sessionId) const { url: moduleUrl, predicate } = createPredicate(module.url) const key = predicateKey(sessionId, moduleUrl) const existingPredicate = idPredicates.get(key) if (existingPredicate) { await page.context().unroute(existingPredicate) } const ids = sessionIds.get(sessionId) ?? new Set<string>() ids.add(moduleUrl) sessionIds.set(sessionId, ids) idPredicates.set(key, predicate) await page.context().route(predicate, async (route) => { if (module.type === 'manual') { const exports = Object.keys(await module.resolve()) const body = createManualModuleSource(module.url, exports) return route.fulfill({ body, headers: getHeaders(this.project.browser!.vite.config), }) }
// webkit doesn't support redirect responses // https://github.com/microsoft/playwright/issues/18318 const isWebkit = this.browserName === 'webkit' if (isWebkit) { let url: string if (module.type === 'redirect') { const redirect = new URL(module.redirect) url = redirect.href.slice(redirect.origin.length) } else { const request = new URL(route.request().url()) request.searchParams.set('mock', module.type) url = request.href.slice(request.origin.length) }
const result = await this.project.browser!.vite.transformRequest(url).catch(() => null) if (!result) { return route.continue() } let content = result.code if (result.map && 'version' in result.map && result.map.mappings) { const type = isDirectCSSRequest(url) ? 'css' : 'js' content = getCodeWithSourcemap(type, content.toString(), result.map) } return route.fulfill({ body: content, headers: getHeaders(this.project.browser!.vite.config), }) }
if (module.type === 'redirect') { return route.fulfill({ status: 302, headers: { Location: module.redirect, }, }) } else if (module.type === 'automock' || module.type === 'autospy') { const url = new URL(route.request().url()) url.searchParams.set('mock', module.type) return route.fulfill({ status: 302, headers: { Location: url.href, }, }) } else { // all types are exhausted const _module: never = module } }) }, delete: async (sessionId: string, id: string): Promise<void> => { const page = this.getPage(sessionId) const key = predicateKey(sessionId, id) const predicate = idPredicates.get(key) if (predicate) { await page .context() .unroute(predicate) .finally(() => idPredicates.delete(key)) } }, clear: async (sessionId: string): Promise<void> => { const page = this.getPage(sessionId) const ids = sessionIds.get(sessionId) ?? new Set<string>() const promises = Array.from(ids, (id) => { const key = predicateKey(sessionId, id) const predicate = idPredicates.get(key) if (predicate) { return page .context() .unroute(predicate) .finally(() => idPredicates.delete(key)) } return null }) await Promise.all(promises).finally(() => sessionIds.delete(sessionId)) }, } }
private async createContext(sessionId: string, openBrowserOptions: { parallel: boolean }) { await this._throwIfClosing()
if (this.contexts.has(sessionId)) { debug?.('[%s][%s] the context already exists, reusing it', sessionId, this.browserName) return this.contexts.get(sessionId)! }
const browser = await this.openBrowser(openBrowserOptions) await this._throwIfClosing(browser) const actionTimeout = this.options.actionTimeout const options = this.getContextOptions() // TODO: investigate the consequences for Vitest 5 // else { // if UI is disabled, keep the iframe scale to 1 // options.viewport ??= this.project.config.browser.viewport // } const context = this.persistentContext ?? (await browser.newContext(options)) await this._throwIfClosing(context) if (actionTimeout != null) { context.setDefaultTimeout(actionTimeout) } debug?.('[%s][%s] the context is ready', sessionId, this.browserName) this.contexts.set(sessionId, context) return context }
private getContextOptions(): BrowserContextOptions { const contextOptions = this.options.contextOptions ?? {} const options = { ...contextOptions, ignoreHTTPSErrors: true, } satisfies BrowserContextOptions // A `null` viewport lets the page adopt the real window size, which is only // meaningful for a headed UI. In headless mode there is no real window, so it // would inherit the host's device scale factor and produce screenshots that // differ from non-UI runs on the same machine. if (this.project.config.browser.ui && !this.project.config.browser.headless) { options.viewport = null } return options }
public getPage(sessionId: string): Page { const page = this.pages.get(sessionId) if (!page) { throw new Error(`Page "${sessionId}" not found in ${this.browserName} browser.`) } return page }
public getCommandsContext(sessionId: string): { page: Page context: BrowserContext frame: () => Promise<Frame> readonly iframe: FrameLocator } { const page = this.getPage(sessionId) return { page, context: this.contexts.get(sessionId)!, frame(): Promise<Frame> { return new Promise<Frame>((resolve, reject) => { const frame = page.frame('vitest-iframe') if (frame) { return resolve(frame) }
const timeout = setTimeout(() => { const err = new Error( `Cannot find "vitest-iframe" on the page. This is a bug in Vitest, please report it.`, ) reject(err) }, 1000).unref() page.on('frameattached', (frame) => { clearTimeout(timeout) resolve(frame) }) }) }, get iframe(): FrameLocator { return page.frameLocator('[data-vitest="true"]')! }, } }
private async openBrowserPage(sessionId: string, options: { parallel: boolean }) { await this._throwIfClosing()
if (this.pages.has(sessionId)) { debug?.('[%s][%s] the page already exists, closing the old one', sessionId, this.browserName) const page = this.pages.get(sessionId)! await page.close() this.pages.delete(sessionId) }
const context = await this.createContext(sessionId, options) const page = await context.newPage() debug?.('[%s][%s] the page is ready', sessionId, this.browserName) await this._throwIfClosing(page) this.pages.set(sessionId, page)
// fail the run immediately with an attributed error; otherwise the crash // is only visible as a websocket disconnect, if the browser closes it at all page.on('crash', () => { debug?.('[%s][%s] the page crashed', sessionId, this.browserName) const session = this.project.vitest._browserSessions.getSession(sessionId) session?.fail( new Error( `The ${this.browserName} page crashed while running tests. This can happen if the browser ran out of memory.`, ), ) })
if (process.env.VITEST_PW_DEBUG) { page.on('requestfailed', (request) => { console.error( '[PW Error]', request.resourceType(), 'request failed for', request.url(), 'url:', request.failure()?.errorText, ) }) }
return page }
async openPage(sessionId: string, url: string, options: { parallel: boolean }): Promise<void> { debug?.('[%s][%s] creating the browser page for %s', sessionId, this.browserName, url) const browserPage = await this.openBrowserPage(sessionId, options) debug?.('[%s][%s] browser page is created, opening %s', sessionId, this.browserName, url) await browserPage.goto(url, { timeout: 0 }) await this._throwIfClosing(browserPage) }
private async _throwIfClosing(disposable?: { close: () => Promise<void> }) { if (this.closing) { debug?.( '[%s] provider was closed, cannot perform the action on %s', this.browserName, String(disposable), ) await disposable?.close() this.pages.clear() this.contexts.clear() this.browser = null this.browserPromise = null throw new Error(`[vitest] The provider was closed.`) } }
async getCDPSession(sessionid: string): Promise<CDPSession> { const page = this.getPage(sessionid) const cdp = await page.context().newCDPSession(page) return { send: cdp.send.bind(cdp), on: cdp.on.bind(cdp), off: cdp.off.bind(cdp), once: cdp.once.bind(cdp), // For now this isn't typed as CDPSession but exposed only for `maybeCollectChromiumGarbage` detach: cdp.detach.bind(cdp), } as any // overloaded CDPSession type is too tricky in monorepo }
async close(): Promise<void> { process.off('SIGTERM', this.onSIGTERM)
debug?.('[%s] closing provider', this.browserName) this.closing = true // a prewarmed browser that was never adopted must not outlive the provider const warm = takeWarmBrowser(this.project.config) if (warm) { void warm.promise.then((browser) => browser.close()).catch(() => {}) } if (this.browserPromise) { await this.browserPromise this.browserPromise = null } const browser = this.browser this.browser = null await Promise.all(Array.from(this.pages.values(), (p) => p.close())) this.pages.clear() if (this.persistentContext) { await this.persistentContext.close() } else { await Promise.all(Array.from(this.contexts.values(), (c) => c.close())) } this.contexts.clear() await browser?.close() debug?.('[%s] provider is closed', this.browserName) }}
function getHeaders(config: ResolvedConfig) { const headers: Record<string, string> = { 'Content-Type': 'application/javascript', }
for (const name in config.server.headers) { headers[name] = String(config.server.headers[name]!) } return headers}
function getCodeWithSourcemap(type: 'js' | 'css', code: string, map: SourceMap): string { if (type === 'js') { code += `\n//# sourceMappingURL=${genSourceMapUrl(map)}` } else if (type === 'css') { code += `\n/*# sourceMappingURL=${genSourceMapUrl(map)} */` }
return code}
function genSourceMapUrl(map: SourceMap | string): string { if (typeof map !== 'string') { map = JSON.stringify(map) } return `data:application/json;base64,${Buffer.from(map).toString('base64')}`}
const directRequestRE = /[?&]direct\b/
function isDirectCSSRequest(request: string): boolean { return isCSSRequest(request) && directRequestRE.test(request)}
declare module 'vitest/node' { export interface BrowserCommandContext { page: Page frame(): Promise<Frame> iframe: FrameLocator context: BrowserContext }
export interface _BrowserNames { playwright: PlaywrightBrowser }
export interface ToMatchScreenshotOptions extends Omit<ScreenshotMatcherOptions, 'comparatorName' | 'comparatorOptions'>, CustomComparatorsRegistry {}
export interface ToMatchScreenshotComparators extends ScreenshotComparatorRegistry {}}
type PWHoverOptions = NonNullable<Parameters<Page['hover']>[1]>type PWClickOptions = NonNullable<Parameters<Page['click']>[1]>type PWDoubleClickOptions = NonNullable<Parameters<Page['dblclick']>[1]>type PWFillOptions = NonNullable<Parameters<Page['fill']>[2]>type PWScreenshotOptions = NonNullable<Parameters<Page['screenshot']>[0]>type PWSelectOptions = NonNullable<Parameters<Page['selectOption']>[2]>type PWDragAndDropOptions = NonNullable<Parameters<Page['dragAndDrop']>[2]>type PWSetInputFiles = NonNullable<Parameters<Page['setInputFiles']>[2]>// Must be re-aliased here or rollup-plugin-dts removes the import alias and you end up with a circular referencetype PWCDPSession = Pick<PlaywrightCDPSession, 'send' | 'on' | 'off' | 'once'>
export { type PWCDPSession as CDPSession }
declare module 'vitest/browser' { export interface UserEventHoverOptions extends PWHoverOptions {} export interface UserEventClickOptions extends PWClickOptions {} export interface UserEventDoubleClickOptions extends PWDoubleClickOptions {} export interface UserEventTripleClickOptions extends PWClickOptions {} export interface UserEventFillOptions extends PWFillOptions {} export interface UserEventSelectOptions extends PWSelectOptions {} export interface UserEventDragAndDropOptions extends PWDragAndDropOptions {} export interface UserEventUploadOptions extends PWSetInputFiles {}
export interface ScreenshotOptions extends Omit<PWScreenshotOptions, 'mask'> { mask?: ReadonlyArray<Element | Locator> | undefined }
export interface CDPSession extends PWCDPSession {}}