From 7844f6d85688ce2f0accac657e09fffd6153ddea Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Thu, 1 Feb 2024 11:49:47 +0100 Subject: [PATCH 01/15] feat(browser): add commands to communicate betweens server and the browser --- packages/browser/commands.d.ts | 18 ++++ packages/browser/src/node/commands/fs.ts | 34 +++++++ packages/browser/src/node/commands/index.ts | 13 +++ .../browser/src/node/commands/keyboard.ts | 92 +++++++++++++++++++ packages/browser/src/node/index.ts | 27 ++++++ .../browser/src/node/providers/playwright.ts | 33 ++++--- .../browser/src/node/providers/webdriver.ts | 23 ++--- packages/browser/src/node/types.ts | 5 + packages/vitest/src/api/setup.ts | 8 ++ packages/vitest/src/api/types.ts | 1 + packages/vitest/src/node/workspace.ts | 4 + packages/vitest/src/types/browser.ts | 7 ++ 12 files changed, 240 insertions(+), 25 deletions(-) create mode 100644 packages/browser/commands.d.ts create mode 100644 packages/browser/src/node/commands/fs.ts create mode 100644 packages/browser/src/node/commands/index.ts create mode 100644 packages/browser/src/node/commands/keyboard.ts create mode 100644 packages/browser/src/node/types.ts diff --git a/packages/browser/commands.d.ts b/packages/browser/commands.d.ts new file mode 100644 index 000000000000..30d8c97ea2a0 --- /dev/null +++ b/packages/browser/commands.d.ts @@ -0,0 +1,18 @@ +declare module '$commands' { + interface FsOptions { + encoding?: BufferEncoding + flag?: string | number + } + + interface TypePayload { type: string } + interface PressPayload { press: string } + interface DownPayload { down: string } + interface UpPayload { up: string } + + type SendKeysPayload = TypePayload | PressPayload | DownPayload | UpPayload + + export const readFile: (path: string, options?: BufferEncoding | FsOptions) => Promise + export const writeFile: (path: string, data: string, options?: BufferEncoding | FsOptions & { mode?: number | string }) => Promise + export const removeFile: (path: string) => Promise + export const sendKeys: (keys: SendKeysPayload) => Promise +} diff --git a/packages/browser/src/node/commands/fs.ts b/packages/browser/src/node/commands/fs.ts new file mode 100644 index 000000000000..ff3e00f5a684 --- /dev/null +++ b/packages/browser/src/node/commands/fs.ts @@ -0,0 +1,34 @@ +import fs, { promises as fsp } from 'node:fs' +import { dirname, resolve } from 'node:path' +import { isFileServingAllowed } from 'vite' +import type { WorkspaceProject } from 'vitest/node' +import type { BrowserCommand } from '../types' + +export interface FsOptions { + encoding?: BufferEncoding + flag?: string | number +} + +function assertFileAccess(path: string, project: WorkspaceProject) { + const resolvedPath = resolve(path) + if (!isFileServingAllowed(resolvedPath, project.server) && !isFileServingAllowed(resolvedPath, project.ctx.server)) + throw new Error(`Access denied to "${resolvedPath}". See Vite config documentation for "server.fs": https://vitejs.dev/config/server-options.html#server-fs-strict.`) +} + +export const readFile: BrowserCommand<[string, BufferEncoding | FsOptions]> = async ([path, options], { project }) => { + assertFileAccess(path, project) + return fsp.readFile(path, options).catch(() => null) +} + +export const writeFile: BrowserCommand<[string, string, BufferEncoding | FsOptions & { mode?: number | string }]> = async ([path, data, options], { project }) => { + assertFileAccess(path, project) + const dir = dirname(path) + if (!fs.existsSync(dir)) + await fsp.mkdir(dir, { recursive: true }) + await fsp.writeFile(path, data, options).catch(() => null) +} + +export const removeFile: BrowserCommand<[string]> = async ([path], { project }) => { + assertFileAccess(path, project) + await fsp.rm(path).catch(() => null) +} diff --git a/packages/browser/src/node/commands/index.ts b/packages/browser/src/node/commands/index.ts new file mode 100644 index 000000000000..bd42c06fce08 --- /dev/null +++ b/packages/browser/src/node/commands/index.ts @@ -0,0 +1,13 @@ +import { + readFile, + removeFile, + writeFile, +} from './fs' +import { sendKeys } from './keyboard' + +export default { + readFile, + removeFile, + writeFile, + sendKeys, +} diff --git a/packages/browser/src/node/commands/keyboard.ts b/packages/browser/src/node/commands/keyboard.ts new file mode 100644 index 000000000000..c64f0142ec7c --- /dev/null +++ b/packages/browser/src/node/commands/keyboard.ts @@ -0,0 +1,92 @@ +// based on https://github.com/modernweb-dev/web/blob/f7fcf29cb79e82ad5622665d76da3f6b23d0ef43/packages/test-runner-commands/src/sendKeysPlugin.ts + +import type { Page } from 'playwright' +import type { BrowserCommand } from '../types' + +// TODO: remove repetition from commands.d.ts +interface TypePayload { type: string } +interface PressPayload { press: string } +interface DownPayload { down: string } +interface UpPayload { up: string } + +export type SendKeysPayload = TypePayload | PressPayload | DownPayload | UpPayload + +function isObject(payload: unknown): payload is Record { + return payload != null && typeof payload === 'object' +} + +function isSendKeysPayload(payload: unknown): boolean { + const validOptions = ['type', 'press', 'down', 'up'] + + if (!isObject(payload)) + throw new Error('You must provide a `SendKeysPayload` object') + + const numberOfValidOptions = Object.keys(payload).filter(key => + validOptions.includes(key), + ).length + const unknownOptions = Object.keys(payload).filter(key => !validOptions.includes(key)) + + if (numberOfValidOptions > 1) { + throw new Error( + `You must provide ONLY one of the following properties to pass to the browser runner: ${validOptions.join( + ', ', + )}.`, + ) + } + if (numberOfValidOptions === 0) { + throw new Error( + `You must provide one of the following properties to pass to the browser runner: ${validOptions.join( + ', ', + )}.`, + ) + } + if (unknownOptions.length > 0) + throw new Error(`Unknown options \`${unknownOptions.join(', ')}\` present.`) + + return true +} + +function isTypePayload(payload: SendKeysPayload): payload is TypePayload { + return 'type' in payload +} + +function isPressPayload(payload: SendKeysPayload): payload is PressPayload { + return 'press' in payload +} + +function isDownPayload(payload: SendKeysPayload): payload is DownPayload { + return 'down' in payload +} + +function isUpPayload(payload: SendKeysPayload): payload is UpPayload { + return 'up' in payload +} + +export const sendKeys: BrowserCommand<[SendKeysPayload]> = async ([payload], { provider }) => { + if (!isSendKeysPayload(payload) || !payload) + throw new Error('You must provide a `SendKeysPayload` object') + + if (provider.name === 'playwright') { + const page = ((provider as any).page as Page) + if (isTypePayload(payload)) + await page.keyboard.type(payload.type) + else if (isPressPayload(payload)) + await page.keyboard.press(payload.press) + else if (isDownPayload(payload)) + await page.keyboard.down(payload.down) + else if (isUpPayload(payload)) + await page.keyboard.up(payload.up) + } + else if (provider.name === 'webdriverio') { + const browser = (provider as any).browser as WebdriverIO.Browser + if (isTypePayload(payload)) + await browser.keys(payload.type.split('')) + else if (isPressPayload(payload)) + await browser.keys([payload.press]) + else + throw new Error('Only "press" and "type" are supported by webdriverio.') + } + else { + throw new Error(`"sendKeys" is not supported for ${provider.name} browser provider.`) + } +} diff --git a/packages/browser/src/node/index.ts b/packages/browser/src/node/index.ts index c4465f438326..fb1439f929e4 100644 --- a/packages/browser/src/node/index.ts +++ b/packages/browser/src/node/index.ts @@ -8,11 +8,16 @@ import type { BrowserScript, WorkspaceProject } from 'vitest/node' import { coverageConfigDefaults } from 'vitest/config' import { slash } from '@vitest/utils' import { injectVitestModule } from './esmInjector' +import builtinCommands from './commands' export default (project: WorkspaceProject, base = '/'): Plugin[] => { const pkgRoot = resolve(fileURLToPath(import.meta.url), '../..') const distRoot = resolve(pkgRoot, 'dist') + project.config.browser.commands ??= {} + for (const [name, command] of Object.entries(builtinCommands)) + project.config.browser.commands[name] ??= command + return [ { enforce: 'pre', @@ -187,6 +192,28 @@ export default (project: WorkspaceProject, base = '/'): Plugin[] => { return useId }, }, + { + name: 'vitest:browser:virtual-module:commands', + resolveId(id) { + if (id === '$commands') + return '\0$commands' + }, + load(id) { + if (id === '\0$commands') { + const commands = Object.keys(project.config.browser.commands ?? {}) + const code = ` +const rpc = () => __vitest_worker__.rpc +${commands.map((command) => { + // TODO: refactor into a separate function + if (!/^[a-zA-Z_$][a-zA-Z_$0-9]*$/.test(command)) + throw new Error(`Invalid command name "${command}". Only alphanumeric characters, $ and _ are allowed.`) + return `export const ${command} = (...args) => rpc().triggerCommand('${command}', args)` +})} + ` + return code + } + }, + }, { name: 'vitest:browser:esm-injector', enforce: 'post', diff --git a/packages/browser/src/node/providers/playwright.ts b/packages/browser/src/node/providers/playwright.ts index e6cf5c048fcf..e9d746594f90 100644 --- a/packages/browser/src/node/providers/playwright.ts +++ b/packages/browser/src/node/providers/playwright.ts @@ -11,9 +11,10 @@ export interface PlaywrightProviderOptions extends BrowserProviderInitialization export class PlaywrightBrowserProvider implements BrowserProvider { public name = 'playwright' - private cachedBrowser: Browser | null = null - private cachedPage: Page | null = null - private browser!: PlaywrightBrowser + public browser: Browser | null = null + public page: Page | null = null + + private browserName!: PlaywrightBrowser private ctx!: WorkspaceProject private options?: { @@ -27,26 +28,30 @@ export class PlaywrightBrowserProvider implements BrowserProvider { initialize(project: WorkspaceProject, { browser, options }: PlaywrightProviderOptions) { this.ctx = project - this.browser = browser + this.browserName = browser this.options = options as any } private async openBrowserPage() { - if (this.cachedPage) - return this.cachedPage + if (this.page) + return this.page const options = this.ctx.config.browser const playwright = await import('playwright') - const browser = await playwright[this.browser].launch({ + const browser = await playwright[this.browserName].launch({ ...this.options?.launch, headless: options.headless, }) - this.cachedBrowser = browser - this.cachedPage = await browser.newPage(this.options?.page) + this.browser = browser + this.page = await browser.newPage(this.options?.page) + + this.page.on('close', () => { + browser.close() + }) - return this.cachedPage + return this.page } async openPage(url: string) { @@ -55,10 +60,10 @@ export class PlaywrightBrowserProvider implements BrowserProvider { } async close() { - const page = this.cachedPage - this.cachedPage = null - const browser = this.cachedBrowser - this.cachedBrowser = null + const page = this.page + this.page = null + const browser = this.browser + this.browser = null await page?.close() await browser?.close() } diff --git a/packages/browser/src/node/providers/webdriver.ts b/packages/browser/src/node/providers/webdriver.ts index 5a63411ae9b2..3a768430e203 100644 --- a/packages/browser/src/node/providers/webdriver.ts +++ b/packages/browser/src/node/providers/webdriver.ts @@ -11,8 +11,9 @@ interface WebdriverProviderOptions extends BrowserProviderInitializationOptions export class WebdriverBrowserProvider implements BrowserProvider { public name = 'webdriverio' - private cachedBrowser: WebdriverIO.Browser | null = null - private browser!: WebdriverBrowser + public browser: WebdriverIO.Browser | null = null + + private browserName!: WebdriverBrowser private ctx!: WorkspaceProject private options?: RemoteOptions @@ -23,17 +24,17 @@ export class WebdriverBrowserProvider implements BrowserProvider { async initialize(ctx: WorkspaceProject, { browser, options }: WebdriverProviderOptions) { this.ctx = ctx - this.browser = browser + this.browserName = browser this.options = options as RemoteOptions } async openBrowser() { - if (this.cachedBrowser) - return this.cachedBrowser + if (this.browser) + return this.browser const options = this.ctx.config.browser - if (this.browser === 'safari') { + if (this.browserName === 'safari') { if (options.headless) throw new Error('You\'ve enabled headless mode for Safari but it doesn\'t currently support it.') } @@ -41,19 +42,19 @@ export class WebdriverBrowserProvider implements BrowserProvider { const { remote } = await import('webdriverio') // TODO: close everything, if browser is closed from the outside - this.cachedBrowser = await remote({ + this.browser = await remote({ ...this.options, logLevel: 'error', capabilities: this.buildCapabilities(), }) - return this.cachedBrowser + return this.browser } private buildCapabilities() { const capabilities: RemoteOptions['capabilities'] = { ...this.options?.capabilities, - browserName: this.browser, + browserName: this.browserName, } const headlessMap = { @@ -63,7 +64,7 @@ export class WebdriverBrowserProvider implements BrowserProvider { } as const const options = this.ctx.config.browser - const browser = this.browser + const browser = this.browserName if (browser !== 'safari' && options.headless) { const [key, args] = headlessMap[browser] const currentValues = (this.options?.capabilities as any)?.[key] || {} @@ -81,7 +82,7 @@ export class WebdriverBrowserProvider implements BrowserProvider { async close() { await Promise.all([ - this.cachedBrowser?.sessionId ? this.cachedBrowser?.deleteSession?.() : null, + this.browser?.sessionId ? this.browser?.deleteSession?.() : null, ]) // TODO: right now process can only exit with timeout, if we use browser // needs investigating diff --git a/packages/browser/src/node/types.ts b/packages/browser/src/node/types.ts new file mode 100644 index 000000000000..26accfb06087 --- /dev/null +++ b/packages/browser/src/node/types.ts @@ -0,0 +1,5 @@ +import type { BrowserProvider, WorkspaceProject } from 'vitest/node' + +export interface BrowserCommand { + (payload: T, options: { provider: BrowserProvider; project: WorkspaceProject }): void +} diff --git a/packages/vitest/src/api/setup.ts b/packages/vitest/src/api/setup.ts index 37c3bb92dbb2..3eebce76981e 100644 --- a/packages/vitest/src/api/setup.ts +++ b/packages/vitest/src/api/setup.ts @@ -151,6 +151,14 @@ export function setup(vitestOrWorkspace: Vitest | WorkspaceProject, _server?: Vi }, // TODO: have a separate websocket conection for private browser API + triggerCommand(command: string, payload: unknown[]) { + if (!('ctx' in vitestOrWorkspace) || !vitestOrWorkspace.browserProvider) + throw new Error('Commands are only available for browser tests.') + const commands = vitestOrWorkspace.config.browser?.commands + if (!commands || !commands[command]) + throw new Error(`Unknown command "${command}".`) + return commands[command](payload, { project: vitestOrWorkspace, provider: vitestOrWorkspace.browserProvider }) + }, getBrowserFiles() { if (!('ctx' in vitestOrWorkspace)) throw new Error('`getBrowserTestFiles` is only available in the browser API') diff --git a/packages/vitest/src/api/types.ts b/packages/vitest/src/api/types.ts index 45afb11fdf6e..3b3b50ed07b9 100644 --- a/packages/vitest/src/api/types.ts +++ b/packages/vitest/src/api/types.ts @@ -37,6 +37,7 @@ export interface WebSocketHandlers { finishBrowserTests: () => void getBrowserFiles: () => string[] debug: (...args: string[]) => void + triggerCommand: (command: string, payload: unknown[]) => Promise } export interface WebSocketEvents extends Pick { diff --git a/packages/vitest/src/node/workspace.ts b/packages/vitest/src/node/workspace.ts index 4d92596e3bc8..697c8b5474b6 100644 --- a/packages/vitest/src/node/workspace.ts +++ b/packages/vitest/src/node/workspace.ts @@ -398,6 +398,10 @@ export class WorkspaceProject { ...this.server?.config.env, ...this.config.env, }, + browser: { + ...this.ctx.config.browser, + commands: {}, + }, }, this.ctx.configOverride || {} as any) as ResolvedConfig } diff --git a/packages/vitest/src/types/browser.ts b/packages/vitest/src/types/browser.ts index 5beb46cd6879..28b795df24a4 100644 --- a/packages/vitest/src/types/browser.ts +++ b/packages/vitest/src/types/browser.ts @@ -103,6 +103,13 @@ export interface BrowserConfigOptions { * Scripts injected into the main window. */ indexScripts?: BrowserScript[] + + // TODO + commands?: Record +} + +interface BrowserCommand { + (payload: any, options: { provider: BrowserProvider; project: WorkspaceProject }): Awaitable } export interface BrowserScript { From 722b1b67eebd7bb046962c0e3c2c02caa70845bf Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Fri, 10 May 2024 12:42:19 +0200 Subject: [PATCH 02/15] refactor: move things around --- packages/browser/commands.d.ts | 19 +------- packages/browser/package.json | 3 ++ packages/browser/rollup.config.js | 8 +++- packages/browser/src/node/commands/fs.ts | 16 ++++++- packages/browser/src/node/commands/index.ts | 13 +++++ .../browser/src/node/commands/keyboard.ts | 11 ++--- packages/browser/src/node/index.ts | 33 ++----------- packages/browser/src/node/plugins/commands.ts | 47 +++++++++++++++++++ .../browser/src/node/providers/playwright.ts | 4 -- packages/vitest/src/node/index.ts | 3 ++ 10 files changed, 97 insertions(+), 60 deletions(-) create mode 100644 packages/browser/src/node/plugins/commands.ts diff --git a/packages/browser/commands.d.ts b/packages/browser/commands.d.ts index 30d8c97ea2a0..b0a25a4254ca 100644 --- a/packages/browser/commands.d.ts +++ b/packages/browser/commands.d.ts @@ -1,18 +1 @@ -declare module '$commands' { - interface FsOptions { - encoding?: BufferEncoding - flag?: string | number - } - - interface TypePayload { type: string } - interface PressPayload { press: string } - interface DownPayload { down: string } - interface UpPayload { up: string } - - type SendKeysPayload = TypePayload | PressPayload | DownPayload | UpPayload - - export const readFile: (path: string, options?: BufferEncoding | FsOptions) => Promise - export const writeFile: (path: string, data: string, options?: BufferEncoding | FsOptions & { mode?: number | string }) => Promise - export const removeFile: (path: string) => Promise - export const sendKeys: (keys: SendKeysPayload) => Promise -} +export * from './dist/commands.d.ts' diff --git a/packages/browser/package.json b/packages/browser/package.json index 24da788ca964..609c3d6128f1 100644 --- a/packages/browser/package.json +++ b/packages/browser/package.json @@ -24,6 +24,9 @@ "types": "./providers.d.ts", "default": "./dist/providers.js" }, + "./commands": { + "types": "./commands.d.ts" + }, "./providers/webdriverio": { "types": "./providers/webdriverio.d.ts" }, diff --git a/packages/browser/rollup.config.js b/packages/browser/rollup.config.js index 910c332a6996..f5eceb83a2e3 100644 --- a/packages/browser/rollup.config.js +++ b/packages/browser/rollup.config.js @@ -14,6 +14,7 @@ const external = [ /^@?vitest(\/|$)/, 'worker_threads', 'node:worker_threads', + /node:/, ] const plugins = [ @@ -43,9 +44,12 @@ export default () => [ plugins, }, { - input: input.index, + input: { + index: input.index, + commands: './src/node/commands/index.ts', + }, output: { - file: 'dist/index.d.ts', + dir: 'dist', format: 'esm', }, external, diff --git a/packages/browser/src/node/commands/fs.ts b/packages/browser/src/node/commands/fs.ts index ff3e00f5a684..7b38b593ac26 100644 --- a/packages/browser/src/node/commands/fs.ts +++ b/packages/browser/src/node/commands/fs.ts @@ -1,9 +1,23 @@ import fs, { promises as fsp } from 'node:fs' import { dirname, resolve } from 'node:path' -import { isFileServingAllowed } from 'vite' +import { isFileServingAllowed } from 'vitest/node' import type { WorkspaceProject } from 'vitest/node' import type { BrowserCommand } from '../types' +export type BufferEncoding = + | 'ascii' + | 'utf8' + | 'utf-8' + | 'utf16le' + | 'utf-16le' + | 'ucs2' + | 'ucs-2' + | 'base64' + | 'base64url' + | 'latin1' + | 'binary' + | 'hex' + export interface FsOptions { encoding?: BufferEncoding flag?: string | number diff --git a/packages/browser/src/node/commands/index.ts b/packages/browser/src/node/commands/index.ts index bd42c06fce08..45c7a6d2daba 100644 --- a/packages/browser/src/node/commands/index.ts +++ b/packages/browser/src/node/commands/index.ts @@ -11,3 +11,16 @@ export default { writeFile, sendKeys, } + +export type { + BufferEncoding, + FsOptions, +} from './fs' + +export type { + TypePayload, + PressPayload, + DownPayload, + UpPayload, + SendKeysPayload, +} from './keyboard' diff --git a/packages/browser/src/node/commands/keyboard.ts b/packages/browser/src/node/commands/keyboard.ts index c64f0142ec7c..887b126d42a3 100644 --- a/packages/browser/src/node/commands/keyboard.ts +++ b/packages/browser/src/node/commands/keyboard.ts @@ -3,11 +3,10 @@ import type { Page } from 'playwright' import type { BrowserCommand } from '../types' -// TODO: remove repetition from commands.d.ts -interface TypePayload { type: string } -interface PressPayload { press: string } -interface DownPayload { down: string } -interface UpPayload { up: string } +export interface TypePayload { type: string } +export interface PressPayload { press: string } +export interface DownPayload { down: string } +export interface UpPayload { up: string } export type SendKeysPayload = TypePayload | PressPayload | DownPayload | UpPayload @@ -67,7 +66,7 @@ export const sendKeys: BrowserCommand<[SendKeysPayload]> = async ([payload], { p throw new Error('You must provide a `SendKeysPayload` object') if (provider.name === 'playwright') { - const page = ((provider as any).page as Page) + const page = (provider as any).page as Page if (isTypePayload(payload)) await page.keyboard.type(payload.type) else if (isPressPayload(payload)) diff --git a/packages/browser/src/node/index.ts b/packages/browser/src/node/index.ts index fb1439f929e4..d2ea13ae323f 100644 --- a/packages/browser/src/node/index.ts +++ b/packages/browser/src/node/index.ts @@ -2,22 +2,18 @@ import { fileURLToPath } from 'node:url' import { readFile } from 'node:fs/promises' import { basename, join, resolve } from 'pathe' import sirv from 'sirv' -import type { Plugin, ViteDevServer } from 'vite' +import type { ViteDevServer } from 'vite' import type { ResolvedConfig } from 'vitest' -import type { BrowserScript, WorkspaceProject } from 'vitest/node' +import type { BrowserScript, Plugin, WorkspaceProject } from 'vitest/node' import { coverageConfigDefaults } from 'vitest/config' import { slash } from '@vitest/utils' import { injectVitestModule } from './esmInjector' -import builtinCommands from './commands' +import BrowserCommands from './plugins/commands' export default (project: WorkspaceProject, base = '/'): Plugin[] => { const pkgRoot = resolve(fileURLToPath(import.meta.url), '../..') const distRoot = resolve(pkgRoot, 'dist') - project.config.browser.commands ??= {} - for (const [name, command] of Object.entries(builtinCommands)) - project.config.browser.commands[name] ??= command - return [ { enforce: 'pre', @@ -192,28 +188,7 @@ export default (project: WorkspaceProject, base = '/'): Plugin[] => { return useId }, }, - { - name: 'vitest:browser:virtual-module:commands', - resolveId(id) { - if (id === '$commands') - return '\0$commands' - }, - load(id) { - if (id === '\0$commands') { - const commands = Object.keys(project.config.browser.commands ?? {}) - const code = ` -const rpc = () => __vitest_worker__.rpc -${commands.map((command) => { - // TODO: refactor into a separate function - if (!/^[a-zA-Z_$][a-zA-Z_$0-9]*$/.test(command)) - throw new Error(`Invalid command name "${command}". Only alphanumeric characters, $ and _ are allowed.`) - return `export const ${command} = (...args) => rpc().triggerCommand('${command}', args)` -})} - ` - return code - } - }, - }, + BrowserCommands(project), { name: 'vitest:browser:esm-injector', enforce: 'post', diff --git a/packages/browser/src/node/plugins/commands.ts b/packages/browser/src/node/plugins/commands.ts new file mode 100644 index 000000000000..71182c199def --- /dev/null +++ b/packages/browser/src/node/plugins/commands.ts @@ -0,0 +1,47 @@ +import type { Plugin } from 'vite' +import type { WorkspaceProject } from 'vitest/node' +import builtinCommands from '../commands/index' +import type { BrowserCommand } from '../types' + +const VIRTUAL_ID_COMMANDS = '\0@vitest/browser/commands' +const ID_COMMANDS = '@vitest/browser/commands' + +export default function BrowserCommands(project: WorkspaceProject): Plugin { + project.config.browser.commands ??= {} + for (const [name, command] of Object.entries(builtinCommands)) + project.config.browser.commands[name] ??= command + + // validate names because they can't be used as identifiers + for (const command in project.config.browser.commands) { + if (!/^[a-zA-Z_$][a-zA-Z_$0-9]*$/.test(command)) + throw new Error(`Invalid command name "${command}". Only alphanumeric characters, $ and _ are allowed.`) + } + + return { + name: 'vitest:browser:virtual-module:commands', + resolveId(id) { + if (id === ID_COMMANDS) + return VIRTUAL_ID_COMMANDS + }, + load(id) { + if (id === VIRTUAL_ID_COMMANDS) + return generateCommandsFile(project.config.browser.commands ?? {}) + }, + } +} + +function generateCommandsFile(commandsMap: Record>) { + const commands = Object.keys(commandsMap) + + if (!commands.length) + return '' + + const prepare = ` +const rpc = () => __vitest_worker__.rpc +` + const commandsCode = commands.map((command) => { + return `export const ${command} = (...args) => rpc().triggerCommand('${command}', args)` + }) + + return `${prepare}\n${commandsCode.join('\n')}` +} diff --git a/packages/browser/src/node/providers/playwright.ts b/packages/browser/src/node/providers/playwright.ts index e9d746594f90..d7c19d724cb9 100644 --- a/packages/browser/src/node/providers/playwright.ts +++ b/packages/browser/src/node/providers/playwright.ts @@ -47,10 +47,6 @@ export class PlaywrightBrowserProvider implements BrowserProvider { this.browser = browser this.page = await browser.newPage(this.options?.page) - this.page.on('close', () => { - browser.close() - }) - return this.page } diff --git a/packages/vitest/src/node/index.ts b/packages/vitest/src/node/index.ts index 92bacfeaee5a..634c9c97661c 100644 --- a/packages/vitest/src/node/index.ts +++ b/packages/vitest/src/node/index.ts @@ -22,3 +22,6 @@ export type { export type { JsonOptions } from './reporters/json' export type { JUnitOptions } from './reporters/junit' export type { HTMLOptions } from './reporters/html' + +export { isFileServingAllowed } from 'vite' +export type { Plugin } from 'vite' From 0da0510b1eff0c277e67dbe0a71788088f5fdae9 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Fri, 10 May 2024 12:58:45 +0200 Subject: [PATCH 03/15] chore: don't generate public commands file --- packages/browser/commands.d.ts | 34 +++++++++++++++++- packages/browser/rollup.config.js | 8 ++--- packages/browser/src/node/commands/fs.ts | 35 +++++-------------- packages/browser/src/node/commands/index.ts | 13 ------- .../browser/src/node/commands/keyboard.ts | 13 +++---- packages/browser/src/node/index.ts | 2 ++ packages/browser/src/node/plugins/commands.ts | 3 +- packages/browser/src/node/types.ts | 5 --- packages/vitest/src/node/index.ts | 1 + packages/vitest/src/types/browser.ts | 6 ++-- 10 files changed, 54 insertions(+), 66 deletions(-) delete mode 100644 packages/browser/src/node/types.ts diff --git a/packages/browser/commands.d.ts b/packages/browser/commands.d.ts index b0a25a4254ca..b101b551cc62 100644 --- a/packages/browser/commands.d.ts +++ b/packages/browser/commands.d.ts @@ -1 +1,33 @@ -export * from './dist/commands.d.ts' +// we cannot reexport our implementation because it relies on Node.js APIs + +export type BufferEncoding = + | 'ascii' + | 'utf8' + | 'utf-8' + | 'utf16le' + | 'utf-16le' + | 'ucs2' + | 'ucs-2' + | 'base64' + | 'base64url' + | 'latin1' + | 'binary' + | 'hex' + +export interface FsOptions { + encoding?: BufferEncoding + flag?: string | number +} + +export declare function readFile(path: string, options?: BufferEncoding | FsOptions): Promise +export declare function writeFile(path: string, content: string, options?: BufferEncoding | FsOptions & { mode?: number | string }): Promise +export declare function removeFile(path: string): Promise + +export interface TypePayload { type: string } +export interface PressPayload { press: string } +export interface DownPayload { down: string } +export interface UpPayload { up: string } + +export type SendKeysPayload = TypePayload | PressPayload | DownPayload | UpPayload + +export declare function sendKeys(payload: SendKeysPayload): Promise diff --git a/packages/browser/rollup.config.js b/packages/browser/rollup.config.js index f5eceb83a2e3..910c332a6996 100644 --- a/packages/browser/rollup.config.js +++ b/packages/browser/rollup.config.js @@ -14,7 +14,6 @@ const external = [ /^@?vitest(\/|$)/, 'worker_threads', 'node:worker_threads', - /node:/, ] const plugins = [ @@ -44,12 +43,9 @@ export default () => [ plugins, }, { - input: { - index: input.index, - commands: './src/node/commands/index.ts', - }, + input: input.index, output: { - dir: 'dist', + file: 'dist/index.d.ts', format: 'esm', }, external, diff --git a/packages/browser/src/node/commands/fs.ts b/packages/browser/src/node/commands/fs.ts index 7b38b593ac26..13ac0fdfb128 100644 --- a/packages/browser/src/node/commands/fs.ts +++ b/packages/browser/src/node/commands/fs.ts @@ -1,27 +1,8 @@ import fs, { promises as fsp } from 'node:fs' import { dirname, resolve } from 'node:path' import { isFileServingAllowed } from 'vitest/node' -import type { WorkspaceProject } from 'vitest/node' -import type { BrowserCommand } from '../types' - -export type BufferEncoding = - | 'ascii' - | 'utf8' - | 'utf-8' - | 'utf16le' - | 'utf-16le' - | 'ucs2' - | 'ucs-2' - | 'base64' - | 'base64url' - | 'latin1' - | 'binary' - | 'hex' - -export interface FsOptions { - encoding?: BufferEncoding - flag?: string | number -} +import type { BrowserCommand, WorkspaceProject } from 'vitest/node' +import type Types from '../../../commands' function assertFileAccess(path: string, project: WorkspaceProject) { const resolvedPath = resolve(path) @@ -29,20 +10,20 @@ function assertFileAccess(path: string, project: WorkspaceProject) { throw new Error(`Access denied to "${resolvedPath}". See Vite config documentation for "server.fs": https://vitejs.dev/config/server-options.html#server-fs-strict.`) } -export const readFile: BrowserCommand<[string, BufferEncoding | FsOptions]> = async ([path, options], { project }) => { +export const readFile: BrowserCommand> = async ([path, options], { project }) => { assertFileAccess(path, project) - return fsp.readFile(path, options).catch(() => null) + return fsp.readFile(path, options) } -export const writeFile: BrowserCommand<[string, string, BufferEncoding | FsOptions & { mode?: number | string }]> = async ([path, data, options], { project }) => { +export const writeFile: BrowserCommand> = async ([path, data, options], { project }) => { assertFileAccess(path, project) const dir = dirname(path) if (!fs.existsSync(dir)) await fsp.mkdir(dir, { recursive: true }) - await fsp.writeFile(path, data, options).catch(() => null) + await fsp.writeFile(path, data, options) } -export const removeFile: BrowserCommand<[string]> = async ([path], { project }) => { +export const removeFile: BrowserCommand> = async ([path], { project }) => { assertFileAccess(path, project) - await fsp.rm(path).catch(() => null) + await fsp.rm(path) } diff --git a/packages/browser/src/node/commands/index.ts b/packages/browser/src/node/commands/index.ts index 45c7a6d2daba..bd42c06fce08 100644 --- a/packages/browser/src/node/commands/index.ts +++ b/packages/browser/src/node/commands/index.ts @@ -11,16 +11,3 @@ export default { writeFile, sendKeys, } - -export type { - BufferEncoding, - FsOptions, -} from './fs' - -export type { - TypePayload, - PressPayload, - DownPayload, - UpPayload, - SendKeysPayload, -} from './keyboard' diff --git a/packages/browser/src/node/commands/keyboard.ts b/packages/browser/src/node/commands/keyboard.ts index 887b126d42a3..de5d67af90af 100644 --- a/packages/browser/src/node/commands/keyboard.ts +++ b/packages/browser/src/node/commands/keyboard.ts @@ -1,14 +1,9 @@ // based on https://github.com/modernweb-dev/web/blob/f7fcf29cb79e82ad5622665d76da3f6b23d0ef43/packages/test-runner-commands/src/sendKeysPlugin.ts import type { Page } from 'playwright' -import type { BrowserCommand } from '../types' - -export interface TypePayload { type: string } -export interface PressPayload { press: string } -export interface DownPayload { down: string } -export interface UpPayload { up: string } - -export type SendKeysPayload = TypePayload | PressPayload | DownPayload | UpPayload +import type { BrowserCommand } from 'vitest/node' +import type Types from '../../../commands' +import type { DownPayload, PressPayload, SendKeysPayload, TypePayload, UpPayload } from '../../../commands' function isObject(payload: unknown): payload is Record { return payload != null && typeof payload === 'object' @@ -61,7 +56,7 @@ function isUpPayload(payload: SendKeysPayload): payload is UpPayload { return 'up' in payload } -export const sendKeys: BrowserCommand<[SendKeysPayload]> = async ([payload], { provider }) => { +export const sendKeys: BrowserCommand> = async ([payload], { provider }) => { if (!isSendKeysPayload(payload) || !payload) throw new Error('You must provide a `SendKeysPayload` object') diff --git a/packages/browser/src/node/index.ts b/packages/browser/src/node/index.ts index d2ea13ae323f..1c2a5f70346e 100644 --- a/packages/browser/src/node/index.ts +++ b/packages/browser/src/node/index.ts @@ -10,6 +10,8 @@ import { slash } from '@vitest/utils' import { injectVitestModule } from './esmInjector' import BrowserCommands from './plugins/commands' +export type { BrowserCommand } from 'vitest/node' + export default (project: WorkspaceProject, base = '/'): Plugin[] => { const pkgRoot = resolve(fileURLToPath(import.meta.url), '../..') const distRoot = resolve(pkgRoot, 'dist') diff --git a/packages/browser/src/node/plugins/commands.ts b/packages/browser/src/node/plugins/commands.ts index 71182c199def..b5ae7ba87977 100644 --- a/packages/browser/src/node/plugins/commands.ts +++ b/packages/browser/src/node/plugins/commands.ts @@ -1,7 +1,6 @@ import type { Plugin } from 'vite' -import type { WorkspaceProject } from 'vitest/node' +import type { BrowserCommand, WorkspaceProject } from 'vitest/node' import builtinCommands from '../commands/index' -import type { BrowserCommand } from '../types' const VIRTUAL_ID_COMMANDS = '\0@vitest/browser/commands' const ID_COMMANDS = '@vitest/browser/commands' diff --git a/packages/browser/src/node/types.ts b/packages/browser/src/node/types.ts deleted file mode 100644 index 26accfb06087..000000000000 --- a/packages/browser/src/node/types.ts +++ /dev/null @@ -1,5 +0,0 @@ -import type { BrowserProvider, WorkspaceProject } from 'vitest/node' - -export interface BrowserCommand { - (payload: T, options: { provider: BrowserProvider; project: WorkspaceProject }): void -} diff --git a/packages/vitest/src/node/index.ts b/packages/vitest/src/node/index.ts index 634c9c97661c..5cfe0bc9a01b 100644 --- a/packages/vitest/src/node/index.ts +++ b/packages/vitest/src/node/index.ts @@ -18,6 +18,7 @@ export type { BrowserProvider, BrowserProviderOptions, BrowserScript, + BrowserCommand, } from '../types/browser' export type { JsonOptions } from './reporters/json' export type { JUnitOptions } from './reporters/junit' diff --git a/packages/vitest/src/types/browser.ts b/packages/vitest/src/types/browser.ts index 28b795df24a4..e652d2645e2c 100644 --- a/packages/vitest/src/types/browser.ts +++ b/packages/vitest/src/types/browser.ts @@ -105,11 +105,11 @@ export interface BrowserConfigOptions { indexScripts?: BrowserScript[] // TODO - commands?: Record + commands?: Record> } -interface BrowserCommand { - (payload: any, options: { provider: BrowserProvider; project: WorkspaceProject }): Awaitable +export interface BrowserCommand { + (payload: T, options: { provider: BrowserProvider; project: WorkspaceProject }): Awaitable } export interface BrowserScript { From 681dae9baf1b9a31c9bd98e50d0447294a1e5991 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Fri, 10 May 2024 13:25:48 +0200 Subject: [PATCH 04/15] chore: update BrowserCommand type --- packages/browser/src/node/commands/fs.ts | 6 +++--- packages/browser/src/node/commands/keyboard.ts | 2 +- packages/vitest/src/api/setup.ts | 2 +- packages/vitest/src/node/cli/cli-config.ts | 1 + packages/vitest/src/types/browser.ts | 4 ++-- 5 files changed, 8 insertions(+), 7 deletions(-) diff --git a/packages/browser/src/node/commands/fs.ts b/packages/browser/src/node/commands/fs.ts index 13ac0fdfb128..2e6f4193037c 100644 --- a/packages/browser/src/node/commands/fs.ts +++ b/packages/browser/src/node/commands/fs.ts @@ -10,12 +10,12 @@ function assertFileAccess(path: string, project: WorkspaceProject) { throw new Error(`Access denied to "${resolvedPath}". See Vite config documentation for "server.fs": https://vitejs.dev/config/server-options.html#server-fs-strict.`) } -export const readFile: BrowserCommand> = async ([path, options], { project }) => { +export const readFile: BrowserCommand> = async ({ project }, path, options) => { assertFileAccess(path, project) return fsp.readFile(path, options) } -export const writeFile: BrowserCommand> = async ([path, data, options], { project }) => { +export const writeFile: BrowserCommand> = async ({ project }, path, data, options) => { assertFileAccess(path, project) const dir = dirname(path) if (!fs.existsSync(dir)) @@ -23,7 +23,7 @@ export const writeFile: BrowserCommand> = asy await fsp.writeFile(path, data, options) } -export const removeFile: BrowserCommand> = async ([path], { project }) => { +export const removeFile: BrowserCommand> = async ({ project }, path) => { assertFileAccess(path, project) await fsp.rm(path) } diff --git a/packages/browser/src/node/commands/keyboard.ts b/packages/browser/src/node/commands/keyboard.ts index de5d67af90af..bc5141aa6401 100644 --- a/packages/browser/src/node/commands/keyboard.ts +++ b/packages/browser/src/node/commands/keyboard.ts @@ -56,7 +56,7 @@ function isUpPayload(payload: SendKeysPayload): payload is UpPayload { return 'up' in payload } -export const sendKeys: BrowserCommand> = async ([payload], { provider }) => { +export const sendKeys: BrowserCommand> = async ({ provider }, payload) => { if (!isSendKeysPayload(payload) || !payload) throw new Error('You must provide a `SendKeysPayload` object') diff --git a/packages/vitest/src/api/setup.ts b/packages/vitest/src/api/setup.ts index 3eebce76981e..6e9331db0fa2 100644 --- a/packages/vitest/src/api/setup.ts +++ b/packages/vitest/src/api/setup.ts @@ -157,7 +157,7 @@ export function setup(vitestOrWorkspace: Vitest | WorkspaceProject, _server?: Vi const commands = vitestOrWorkspace.config.browser?.commands if (!commands || !commands[command]) throw new Error(`Unknown command "${command}".`) - return commands[command](payload, { project: vitestOrWorkspace, provider: vitestOrWorkspace.browserProvider }) + return commands[command]({ project: vitestOrWorkspace, provider: vitestOrWorkspace.browserProvider }, ...payload) }, getBrowserFiles() { if (!('ctx' in vitestOrWorkspace)) diff --git a/packages/vitest/src/node/cli/cli-config.ts b/packages/vitest/src/node/cli/cli-config.ts index c9c935373e00..04048ae37f76 100644 --- a/packages/vitest/src/node/cli/cli-config.ts +++ b/packages/vitest/src/node/cli/cli-config.ts @@ -352,6 +352,7 @@ export const cliOptionsConfig: VitestCLIOptions = { }, indexScripts: null, testerScripts: null, + commands: null, }, }, pool: { diff --git a/packages/vitest/src/types/browser.ts b/packages/vitest/src/types/browser.ts index e652d2645e2c..a40d2c41427d 100644 --- a/packages/vitest/src/types/browser.ts +++ b/packages/vitest/src/types/browser.ts @@ -108,8 +108,8 @@ export interface BrowserConfigOptions { commands?: Record> } -export interface BrowserCommand { - (payload: T, options: { provider: BrowserProvider; project: WorkspaceProject }): Awaitable +export interface BrowserCommand { + (options: { provider: BrowserProvider; project: WorkspaceProject }, ...payload: Payload): Awaitable } export interface BrowserScript { From e92bf6996268015e00e33a5c1971a4cd9d1d540b Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Fri, 10 May 2024 18:23:23 +0200 Subject: [PATCH 05/15] test: add tests for built-in commands --- packages/browser/commands.js | 2 + packages/browser/package.json | 3 +- packages/browser/src/node/commands/fs.ts | 31 +++-- packages/browser/src/node/plugins/commands.ts | 3 +- packages/ui/client/auto-imports.d.ts | 1 + packages/ui/client/components.d.ts | 2 +- packages/vitest/src/api/setup.ts | 8 +- packages/vitest/src/api/types.ts | 2 +- packages/vitest/src/types/browser.ts | 8 +- test/browser/test/commands.test.ts | 120 ++++++++++++++++++ 10 files changed, 160 insertions(+), 20 deletions(-) create mode 100644 packages/browser/commands.js create mode 100644 test/browser/test/commands.test.ts diff --git a/packages/browser/commands.js b/packages/browser/commands.js new file mode 100644 index 000000000000..af8cff3be496 --- /dev/null +++ b/packages/browser/commands.js @@ -0,0 +1,2 @@ +// empty file to not break bundling +// Vitest resolves "@vitest/browser/commands" as a virtual module instead diff --git a/packages/browser/package.json b/packages/browser/package.json index 609c3d6128f1..05722ad8d905 100644 --- a/packages/browser/package.json +++ b/packages/browser/package.json @@ -25,7 +25,8 @@ "default": "./dist/providers.js" }, "./commands": { - "types": "./commands.d.ts" + "types": "./commands.d.ts", + "default": "./commands.js" }, "./providers/webdriverio": { "types": "./providers/webdriverio.d.ts" diff --git a/packages/browser/src/node/commands/fs.ts b/packages/browser/src/node/commands/fs.ts index 2e6f4193037c..08172c0e03d6 100644 --- a/packages/browser/src/node/commands/fs.ts +++ b/packages/browser/src/node/commands/fs.ts @@ -5,25 +5,30 @@ import type { BrowserCommand, WorkspaceProject } from 'vitest/node' import type Types from '../../../commands' function assertFileAccess(path: string, project: WorkspaceProject) { - const resolvedPath = resolve(path) - if (!isFileServingAllowed(resolvedPath, project.server) && !isFileServingAllowed(resolvedPath, project.ctx.server)) - throw new Error(`Access denied to "${resolvedPath}". See Vite config documentation for "server.fs": https://vitejs.dev/config/server-options.html#server-fs-strict.`) + if (!isFileServingAllowed(path, project.server) && !isFileServingAllowed(path, project.ctx.server)) + throw new Error(`Access denied to "${path}". See Vite config documentation for "server.fs": https://vitejs.dev/config/server-options.html#server-fs-strict.`) } -export const readFile: BrowserCommand> = async ({ project }, path, options) => { - assertFileAccess(path, project) - return fsp.readFile(path, options) +export const readFile: BrowserCommand> = async ({ project, testPath = process.cwd() }, path, options = {}) => { + const filepath = resolve(dirname(testPath), path) + assertFileAccess(filepath, project) + // never return a Buffer + if (typeof options === 'object' && !options.encoding) + options.encoding = 'utf-8' + return fsp.readFile(filepath, options) } -export const writeFile: BrowserCommand> = async ({ project }, path, data, options) => { - assertFileAccess(path, project) - const dir = dirname(path) +export const writeFile: BrowserCommand> = async ({ project, testPath = process.cwd() }, path, data, options) => { + const filepath = resolve(dirname(testPath), path) + assertFileAccess(filepath, project) + const dir = dirname(filepath) if (!fs.existsSync(dir)) await fsp.mkdir(dir, { recursive: true }) - await fsp.writeFile(path, data, options) + await fsp.writeFile(filepath, data, options) } -export const removeFile: BrowserCommand> = async ({ project }, path) => { - assertFileAccess(path, project) - await fsp.rm(path) +export const removeFile: BrowserCommand> = async ({ project, testPath = process.cwd() }, path) => { + const filepath = resolve(dirname(testPath), path) + assertFileAccess(filepath, project) + await fsp.rm(filepath) } diff --git a/packages/browser/src/node/plugins/commands.ts b/packages/browser/src/node/plugins/commands.ts index b5ae7ba87977..612be920bd1f 100644 --- a/packages/browser/src/node/plugins/commands.ts +++ b/packages/browser/src/node/plugins/commands.ts @@ -18,6 +18,7 @@ export default function BrowserCommands(project: WorkspaceProject): Plugin { return { name: 'vitest:browser:virtual-module:commands', + enforce: 'pre', resolveId(id) { if (id === ID_COMMANDS) return VIRTUAL_ID_COMMANDS @@ -39,7 +40,7 @@ function generateCommandsFile(commandsMap: Record>) const rpc = () => __vitest_worker__.rpc ` const commandsCode = commands.map((command) => { - return `export const ${command} = (...args) => rpc().triggerCommand('${command}', args)` + return `export const ${command} = (...args) => rpc().triggerCommand('${command}', __vitest_worker__.filepath || __vitest_worker__.current?.file?.filepath || undefined, args)` }) return `${prepare}\n${commandsCode.join('\n')}` diff --git a/packages/ui/client/auto-imports.d.ts b/packages/ui/client/auto-imports.d.ts index 25801881e95f..1ae5d56efcd3 100644 --- a/packages/ui/client/auto-imports.d.ts +++ b/packages/ui/client/auto-imports.d.ts @@ -329,4 +329,5 @@ declare global { declare global { // @ts-ignore export type { Component, ComponentPublicInstance, ComputedRef, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, VNode, WritableComputedRef } from 'vue' + import('vue') } diff --git a/packages/ui/client/components.d.ts b/packages/ui/client/components.d.ts index afe16661a6de..2cbf34ccdfa9 100644 --- a/packages/ui/client/components.d.ts +++ b/packages/ui/client/components.d.ts @@ -1,10 +1,10 @@ /* eslint-disable */ -/* prettier-ignore */ // @ts-nocheck // Generated by unplugin-vue-components // Read more: https://github.com/vuejs/core/pull/3399 export {} +/* prettier-ignore */ declare module 'vue' { export interface GlobalComponents { CodeMirror: typeof import('./components/CodeMirror.vue')['default'] diff --git a/packages/vitest/src/api/setup.ts b/packages/vitest/src/api/setup.ts index 6e9331db0fa2..9a1685f9e8a9 100644 --- a/packages/vitest/src/api/setup.ts +++ b/packages/vitest/src/api/setup.ts @@ -151,13 +151,17 @@ export function setup(vitestOrWorkspace: Vitest | WorkspaceProject, _server?: Vi }, // TODO: have a separate websocket conection for private browser API - triggerCommand(command: string, payload: unknown[]) { + triggerCommand(command: string, testPath: string | undefined, payload: unknown[]) { if (!('ctx' in vitestOrWorkspace) || !vitestOrWorkspace.browserProvider) throw new Error('Commands are only available for browser tests.') const commands = vitestOrWorkspace.config.browser?.commands if (!commands || !commands[command]) throw new Error(`Unknown command "${command}".`) - return commands[command]({ project: vitestOrWorkspace, provider: vitestOrWorkspace.browserProvider }, ...payload) + return commands[command]({ + testPath, + project: vitestOrWorkspace, + provider: vitestOrWorkspace.browserProvider, + }, ...payload) }, getBrowserFiles() { if (!('ctx' in vitestOrWorkspace)) diff --git a/packages/vitest/src/api/types.ts b/packages/vitest/src/api/types.ts index 3b3b50ed07b9..3684cc9b4b0a 100644 --- a/packages/vitest/src/api/types.ts +++ b/packages/vitest/src/api/types.ts @@ -37,7 +37,7 @@ export interface WebSocketHandlers { finishBrowserTests: () => void getBrowserFiles: () => string[] debug: (...args: string[]) => void - triggerCommand: (command: string, payload: unknown[]) => Promise + triggerCommand: (command: string, testPath: string | undefined, payload: unknown[]) => Promise } export interface WebSocketEvents extends Pick { diff --git a/packages/vitest/src/types/browser.ts b/packages/vitest/src/types/browser.ts index a40d2c41427d..2fa58c565468 100644 --- a/packages/vitest/src/types/browser.ts +++ b/packages/vitest/src/types/browser.ts @@ -108,8 +108,14 @@ export interface BrowserConfigOptions { commands?: Record> } +export interface BrowserCommandContext { + testPath: string | undefined + provider: BrowserProvider + project: WorkspaceProject +} + export interface BrowserCommand { - (options: { provider: BrowserProvider; project: WorkspaceProject }, ...payload: Payload): Awaitable + (context: BrowserCommandContext, ...payload: Payload): Awaitable } export interface BrowserScript { diff --git a/test/browser/test/commands.test.ts b/test/browser/test/commands.test.ts new file mode 100644 index 000000000000..6c4ac99f4a30 --- /dev/null +++ b/test/browser/test/commands.test.ts @@ -0,0 +1,120 @@ +import { readFile, removeFile, sendKeys, writeFile } from '@vitest/browser/commands' +import { expect, it } from 'vitest' + +it('can manipulate files', async () => { + const file = './test.txt' + + try { + await readFile(file) + expect.unreachable() + } + catch (err) { + expect(err.message).toMatch(`ENOENT: no such file or directory, open`) + expect(err.message).toMatch(`test/browser/test/test.txt`) + } + + await writeFile(file, 'hello world') + const content = await readFile(file) + + expect(content).toBe('hello world') + + await removeFile(file) + + try { + await readFile(file) + expect.unreachable() + } + catch (err) { + expect(err.message).toMatch(`ENOENT: no such file or directory, open`) + expect(err.message).toMatch(`test/browser/test/test.txt`) + } +}) + +// Test Cases from https://modern-web.dev/docs/test-runner/commands/#writing-and-reading-files +it('natively types into an input', async () => { + const keys = 'abc123' + const input = document.createElement('input') + document.body.append(input) + input.focus() + + await sendKeys({ + type: keys, + }) + + expect(input.value).to.equal(keys) + input.remove() +}) + +it('natively presses `Tab`', async () => { + const input1 = document.createElement('input') + const input2 = document.createElement('input') + document.body.append(input1, input2) + input1.focus() + expect(document.activeElement).to.equal(input1) + + await sendKeys({ + press: 'Tab', + }) + + expect(document.activeElement).to.equal(input2) + input1.remove() + input2.remove() +}) + +it('natively presses `Shift+Tab`', async () => { + const input1 = document.createElement('input') + const input2 = document.createElement('input') + document.body.append(input1, input2) + input2.focus() + expect(document.activeElement).to.equal(input2) + + await sendKeys({ + down: 'Shift', + }) + await sendKeys({ + press: 'Tab', + }) + await sendKeys({ + up: 'Shift', + }) + + expect(document.activeElement).to.equal(input1) + input1.remove() + input2.remove() +}) + +it('natively holds and then releases a key', async () => { + const input = document.createElement('input') + document.body.append(input) + input.focus() + + await sendKeys({ + down: 'Shift', + }) + // Note that pressed modifier keys are only respected when using `press` or + // `down`, and only when using the `Key...` variants. + await sendKeys({ + press: 'KeyA', + }) + await sendKeys({ + press: 'KeyB', + }) + await sendKeys({ + press: 'KeyC', + }) + await sendKeys({ + up: 'Shift', + }) + await sendKeys({ + press: 'KeyA', + }) + await sendKeys({ + press: 'KeyB', + }) + await sendKeys({ + press: 'KeyC', + }) + + expect(input.value).to.equal('ABCabc') + input.remove() +}) From 20444836dea7dad2d6324e00018428004fd9260c Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Fri, 10 May 2024 18:28:33 +0200 Subject: [PATCH 06/15] test: skip unsupported tests --- test/browser/test/commands.test.ts | 6 ++++-- test/browser/tsconfig.json | 1 + test/browser/vitest.config.mts | 3 +++ 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/test/browser/test/commands.test.ts b/test/browser/test/commands.test.ts index 6c4ac99f4a30..8aa1d165deb7 100644 --- a/test/browser/test/commands.test.ts +++ b/test/browser/test/commands.test.ts @@ -1,6 +1,8 @@ import { readFile, removeFile, sendKeys, writeFile } from '@vitest/browser/commands' import { expect, it } from 'vitest' +const provider = import.meta.env.PROVIDER || 'playwright' + it('can manipulate files', async () => { const file = './test.txt' @@ -61,7 +63,7 @@ it('natively presses `Tab`', async () => { input2.remove() }) -it('natively presses `Shift+Tab`', async () => { +it.skipIf(provider === 'webdriverio')('natively presses `Shift+Tab`', async () => { const input1 = document.createElement('input') const input2 = document.createElement('input') document.body.append(input1, input2) @@ -83,7 +85,7 @@ it('natively presses `Shift+Tab`', async () => { input2.remove() }) -it('natively holds and then releases a key', async () => { +it.skipIf(provider === 'webdriverio')('natively holds and then releases a key', async () => { const input = document.createElement('input') document.body.append(input) input.focus() diff --git a/test/browser/tsconfig.json b/test/browser/tsconfig.json index 183104f0320b..dd26cb7f7263 100644 --- a/test/browser/tsconfig.json +++ b/test/browser/tsconfig.json @@ -5,6 +5,7 @@ "paths": { "#src/*": ["./src/*"] }, + "types": ["vite/client"], "esModuleInterop": true } } diff --git a/test/browser/vitest.config.mts b/test/browser/vitest.config.mts index 75f3435ad6c2..27a57768b699 100644 --- a/test/browser/vitest.config.mts +++ b/test/browser/vitest.config.mts @@ -18,6 +18,9 @@ export default defineConfig({ optimizeDeps: { include: ['@vitest/cjs-lib'], }, + define: { + 'import.meta.env.PROVIDER': JSON.stringify(provider), + }, test: { include: ['test/**.test.{ts,js}'], // having a snapshot environment doesn't affect browser tests From 1e88a05947e54a76391842b5b8e42e58b35c9c6c Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Fri, 10 May 2024 19:06:18 +0200 Subject: [PATCH 07/15] refactor: move plugin export, add docs and test for cusotm commands --- docs/.vitepress/components.d.ts | 2 +- docs/config/index.md | 7 ++ docs/guide/browser.md | 115 ++++++++++++++++++ packages/browser/src/node/index.ts | 4 +- packages/browser/src/node/plugins/commands.ts | 2 +- packages/vitest/src/config.ts | 1 + packages/vitest/src/node/index.ts | 1 - test/browser/test/commands.test.ts | 19 ++- test/browser/vitest.config.mts | 8 ++ 9 files changed, 153 insertions(+), 6 deletions(-) diff --git a/docs/.vitepress/components.d.ts b/docs/.vitepress/components.d.ts index 8557a7924e77..5805e44bf38f 100644 --- a/docs/.vitepress/components.d.ts +++ b/docs/.vitepress/components.d.ts @@ -1,10 +1,10 @@ /* eslint-disable */ -/* prettier-ignore */ // @ts-nocheck // Generated by unplugin-vue-components // Read more: https://github.com/vuejs/core/pull/3399 export {} +/* prettier-ignore */ declare module 'vue' { export interface GlobalComponents { Contributors: typeof import('./components/Contributors.vue')['default'] diff --git a/docs/config/index.md b/docs/config/index.md index 63d7a2e61150..7c8ae5ef7932 100644 --- a/docs/config/index.md +++ b/docs/config/index.md @@ -1679,6 +1679,13 @@ Custom scripts that should be injected into the tester HTML before the tests env The script `src` and `content` will be processed by Vite plugins. +#### browser.commands 2.0.0 {#browser-commands} + +- **Type:** `Record` +- **Default:** `{ readFile, writeFile, ... }` + +Custom [commands](/guide/browser#commands) that can be import during browser tests from `@vitest/browser/commands`. + ### clearMocks - **Type:** `boolean` diff --git a/docs/guide/browser.md b/docs/guide/browser.md index e19dba6cc2de..73bd9fcf8439 100644 --- a/docs/guide/browser.md +++ b/docs/guide/browser.md @@ -119,6 +119,121 @@ npx vitest --browser.name=chrome --browser.headless In this case, Vitest will run in headless mode using the Chrome browser. +## Commands 2.0.0 {#commands} + +Command is a function that invokes another function on the server and passes down the result back to the browser. Vitest exposes several built-in commands you can use in your browser tests. + +## Built-in Commands + +### Files Handling + +You can use `readFile`, `writeFile` and `removeFile` API to handle files inside your browser tests. All paths are resolved relative to the test file even if they are called in a helper function located in another file. + +By default, Vitest uses `utf-8` encoding but you can override it with options. + +::: tip +This API follows [`server.fs`](https://vitejs.dev/config/server-options.html#server-fs-allow) limitations for security reasons. +::: + +```ts +import { readFile, removeFile, writeFile } from '@vitest/browser/commands' + +it('handles files', async () => { + const file = './test.txt' + + await writeFile(file, 'hello world') + const content = await readFile(file) + + expect(content).toBe('hello world') + + await removeFile(file) +}) +``` + +### Keyboard Interactions + +Vitest also implements Web Test Runner's [`sendKeys` API](https://modern-web.dev/docs/test-runner/commands/#send-keys). It accepts an object with a single property: + +- `type` - types a sequence of characters, this API _is not_ affected by modifier keys, so having `Shift` won't make letters uppercase +- `press` - presses a single key, this API _is_ affected by modifier keys, so having `Shift` will make subsequent characters uppercase +- `up` - holds down a key (supported only with `playwright` provider) +- `down` - releases a key (supported only with `playwright` provider) + +```ts +interface TypePayload { type: string } +interface PressPayload { press: string } +interface DownPayload { down: string } +interface UpPayload { up: string } + +type SendKeysPayload = TypePayload | PressPayload | DownPayload | UpPayload + +declare function sendKeys(payload: SendKeysPayload): Promise +``` + +This is just a simple wrapper around providers APIs. Please refer to their respective documentations for details: + +- [Playwright Keyboard API](https://playwright.dev/docs/api/class-keyboard) +- [Webdriver Keyboard API](https://webdriver.io/docs/api/browser/keys/) + +## Custom Commands + +You can also add your own commands via [`browser.commands`](/config/#browser-commands) config option. If you develop a library, you can provide them via a `config` hook inside a plugin: + +```ts +import type { Plugin } from 'vitest/config' +import type { BrowserCommand } from 'vitest/node' + +const myCustomCommand: BrowserCommand<[arg1: string, arg2: string]> = ({ + testPath, + provider +}, arg1, arg2) => { + if (provider.name === 'playwright') { + console.log(testPath, arg1, arg2) + return { someValue: true } + } + + throw new Error(`provider ${provider.name} is not supported`) +} + +export default function BrowserCommands(): Plugin { + return { + name: 'vitest:custom-commands', + config() { + return { + test: { + browser: { + commands: { + myCustomCommand, + } + } + } + } + } + } +} +``` + +Then you can call it inside your test by importing it from `@vitest/browser/commands`: + +```ts +import { myCustomCommand } from '@vitest/browser/commands' +import { expect, test } from 'vitest' + +test('keys are sent', async () => { + const result = await myCustomCommand('test1', 'test2') + expect(result).toEqual({ someValue: true }) +}) + +// if you are using TypeScript, you can augment the module +declare module '@vitest/browser/commands' { + export function myCustomCommand(arg1: string, arg2: string): Promise<{ someValue: true }> +} +``` + +::: warn +Custom functions will override built-in ones if they have the same name. +::: + ## Limitations ### Thread Blocking Dialogs diff --git a/packages/browser/src/node/index.ts b/packages/browser/src/node/index.ts index 1c2a5f70346e..482f24a41108 100644 --- a/packages/browser/src/node/index.ts +++ b/packages/browser/src/node/index.ts @@ -4,8 +4,8 @@ import { basename, join, resolve } from 'pathe' import sirv from 'sirv' import type { ViteDevServer } from 'vite' import type { ResolvedConfig } from 'vitest' -import type { BrowserScript, Plugin, WorkspaceProject } from 'vitest/node' -import { coverageConfigDefaults } from 'vitest/config' +import type { BrowserScript, WorkspaceProject } from 'vitest/node' +import { type Plugin, coverageConfigDefaults } from 'vitest/config' import { slash } from '@vitest/utils' import { injectVitestModule } from './esmInjector' import BrowserCommands from './plugins/commands' diff --git a/packages/browser/src/node/plugins/commands.ts b/packages/browser/src/node/plugins/commands.ts index 612be920bd1f..1367dd40196d 100644 --- a/packages/browser/src/node/plugins/commands.ts +++ b/packages/browser/src/node/plugins/commands.ts @@ -1,4 +1,4 @@ -import type { Plugin } from 'vite' +import type { Plugin } from 'vitest/config' import type { BrowserCommand, WorkspaceProject } from 'vitest/node' import builtinCommands from '../commands/index' diff --git a/packages/vitest/src/config.ts b/packages/vitest/src/config.ts index e207476f31c2..76d07b1a6f3b 100644 --- a/packages/vitest/src/config.ts +++ b/packages/vitest/src/config.ts @@ -9,6 +9,7 @@ export interface UserWorkspaceConfig extends ViteUserConfig { export { configDefaults, defaultInclude, defaultExclude, coverageConfigDefaults } from './defaults' export { mergeConfig } from 'vite' export { extraInlineDeps } from './constants' +export type { Plugin } from 'vite' export type { ConfigEnv, ViteUserConfig as UserConfig } export type UserConfigFnObject = (env: ConfigEnv) => ViteUserConfig diff --git a/packages/vitest/src/node/index.ts b/packages/vitest/src/node/index.ts index 5cfe0bc9a01b..7781136ab7c8 100644 --- a/packages/vitest/src/node/index.ts +++ b/packages/vitest/src/node/index.ts @@ -25,4 +25,3 @@ export type { JUnitOptions } from './reporters/junit' export type { HTMLOptions } from './reporters/html' export { isFileServingAllowed } from 'vite' -export type { Plugin } from 'vite' diff --git a/test/browser/test/commands.test.ts b/test/browser/test/commands.test.ts index 8aa1d165deb7..bf2221e6e56a 100644 --- a/test/browser/test/commands.test.ts +++ b/test/browser/test/commands.test.ts @@ -1,4 +1,4 @@ -import { readFile, removeFile, sendKeys, writeFile } from '@vitest/browser/commands' +import { myCustomCommand, readFile, removeFile, sendKeys, writeFile } from '@vitest/browser/commands' import { expect, it } from 'vitest' const provider = import.meta.env.PROVIDER || 'playwright' @@ -120,3 +120,20 @@ it.skipIf(provider === 'webdriverio')('natively holds and then releases a key', expect(input.value).to.equal('ABCabc') input.remove() }) + +it('can run custom commands', async () => { + const result = await myCustomCommand('arg1', 'arg2') + expect(result).toEqual({ + testPath: expect.stringMatching('test/browser/test/commands.test.ts'), + arg1: 'arg1', + arg2: 'arg2', + }) +}) + +declare module '@vitest/browser/commands' { + export function myCustomCommand(arg1: string, arg2: string): Promise<{ + testPath: string + arg1: string + arg2: string + }> +} diff --git a/test/browser/vitest.config.mts b/test/browser/vitest.config.mts index 27a57768b699..e28393e80bfd 100644 --- a/test/browser/vitest.config.mts +++ b/test/browser/vitest.config.mts @@ -1,6 +1,7 @@ import { dirname, resolve } from 'node:path' import { fileURLToPath } from 'node:url' import { defineConfig } from 'vitest/config' +import type { BrowserCommand } from 'vitest/node' const dir = dirname(fileURLToPath(import.meta.url)) @@ -9,6 +10,10 @@ function noop() {} const provider = process.env.PROVIDER || 'playwright' const browser = process.env.BROWSER || (provider === 'playwright' ? 'chromium' : 'chrome') +const myCustomCommand: BrowserCommand<[arg1: string, arg2: string]> = ({ testPath }, arg1, arg2) => { + return { testPath, arg1, arg2 } +} + export default defineConfig({ server: { headers: { @@ -63,6 +68,9 @@ export default defineConfig({ content: 'if(__injected[0] !== 3) throw new Error("injected not working")', }, ], + commands: { + myCustomCommand, + }, }, alias: { '#src': resolve(dir, './src'), From 205d799dac2df8438e7ab8323642249f39310ac9 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Fri, 10 May 2024 19:08:31 +0200 Subject: [PATCH 08/15] chore: fix docs warning --- docs/guide/browser.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guide/browser.md b/docs/guide/browser.md index 73bd9fcf8439..9b7f3594a4bd 100644 --- a/docs/guide/browser.md +++ b/docs/guide/browser.md @@ -230,7 +230,7 @@ declare module '@vitest/browser/commands' { } ``` -::: warn +::: warning Custom functions will override built-in ones if they have the same name. ::: From 1833ba637432f68fad434740b792de6f221f5a2c Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Fri, 10 May 2024 19:24:08 +0200 Subject: [PATCH 09/15] chore: fix browser tests --- test/browser/specs/runner.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/browser/specs/runner.test.ts b/test/browser/specs/runner.test.ts index 680cd1f0d7e3..99085ca1a63b 100644 --- a/test/browser/specs/runner.test.ts +++ b/test/browser/specs/runner.test.ts @@ -30,8 +30,8 @@ describe.each([ console.error(stderr) }) - expect(browserResultJson.testResults).toHaveLength(15) - expect(passedTests).toHaveLength(13) + expect(browserResultJson.testResults).toHaveLength(16) + expect(passedTests).toHaveLength(14) expect(failedTests).toHaveLength(2) expect(stderr).not.toContain('has been externalized for browser compatibility') From 3f050246dfdf63ae67de66c7b16291f87ac7fbeb Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Fri, 10 May 2024 19:25:11 +0200 Subject: [PATCH 10/15] chore: cleanup --- docs/guide/browser.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guide/browser.md b/docs/guide/browser.md index 9b7f3594a4bd..bcbac6ea4fc5 100644 --- a/docs/guide/browser.md +++ b/docs/guide/browser.md @@ -219,7 +219,7 @@ Then you can call it inside your test by importing it from `@vitest/browser/comm import { myCustomCommand } from '@vitest/browser/commands' import { expect, test } from 'vitest' -test('keys are sent', async () => { +test('custom command works correctly', async () => { const result = await myCustomCommand('test1', 'test2') expect(result).toEqual({ someValue: true }) }) From cac4690a2b6384bd92ac82f220c709c78a2b1aa7 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Fri, 10 May 2024 20:00:32 +0200 Subject: [PATCH 11/15] feat: add context module --- docs/guide/browser.md | 9 +++++++ packages/browser/context.d.ts | 17 ++++++++++++ packages/browser/context.js | 2 ++ packages/browser/package.json | 4 +++ packages/browser/src/node/index.ts | 2 ++ packages/browser/src/node/plugins/context.ts | 27 ++++++++++++++++++++ test/browser/test/commands.test.ts | 19 +++++++++++--- 7 files changed, 77 insertions(+), 3 deletions(-) create mode 100644 packages/browser/context.d.ts create mode 100644 packages/browser/context.js create mode 100644 packages/browser/src/node/plugins/context.ts diff --git a/docs/guide/browser.md b/docs/guide/browser.md index bcbac6ea4fc5..4727b8da9750 100644 --- a/docs/guide/browser.md +++ b/docs/guide/browser.md @@ -119,6 +119,15 @@ npx vitest --browser.name=chrome --browser.headless In this case, Vitest will run in headless mode using the Chrome browser. +## Context 2.0.0 {#context} + +Vitest exposes a context module via `@vitest/browser/context` entry point. As of 2.0, it exposes a small set of utilities that might be useful to you in tests. + +```ts +export const getServerPlatform: () => Platform +export const getConfig: () => ResolvedConfig +``` + ## Commands 2.0.0 {#commands} Command is a function that invokes another function on the server and passes down the result back to the browser. Vitest exposes several built-in commands you can use in your browser tests. diff --git a/packages/browser/context.d.ts b/packages/browser/context.d.ts new file mode 100644 index 000000000000..fdedbfa25d97 --- /dev/null +++ b/packages/browser/context.d.ts @@ -0,0 +1,17 @@ +import type { ResolvedConfig } from 'vitest' + +type Platform = + | 'aix' + | 'android' + | 'darwin' + | 'freebsd' + | 'haiku' + | 'linux' + | 'openbsd' + | 'sunos' + | 'win32' + | 'cygwin' + | 'netbsd' + +export const getServerPlatform: () => Platform +export const getConfig: () => ResolvedConfig diff --git a/packages/browser/context.js b/packages/browser/context.js new file mode 100644 index 000000000000..97779fdf5917 --- /dev/null +++ b/packages/browser/context.js @@ -0,0 +1,2 @@ +// empty file to not break bundling +// Vitest resolves "@vitest/browser/context" as a virtual module instead diff --git a/packages/browser/package.json b/packages/browser/package.json index 05722ad8d905..249cec376b58 100644 --- a/packages/browser/package.json +++ b/packages/browser/package.json @@ -28,6 +28,10 @@ "types": "./commands.d.ts", "default": "./commands.js" }, + "./context": { + "types": "./context.d.ts", + "default": "./context.js" + }, "./providers/webdriverio": { "types": "./providers/webdriverio.d.ts" }, diff --git a/packages/browser/src/node/index.ts b/packages/browser/src/node/index.ts index 482f24a41108..8f932abcd3cd 100644 --- a/packages/browser/src/node/index.ts +++ b/packages/browser/src/node/index.ts @@ -9,6 +9,7 @@ import { type Plugin, coverageConfigDefaults } from 'vitest/config' import { slash } from '@vitest/utils' import { injectVitestModule } from './esmInjector' import BrowserCommands from './plugins/commands' +import BrowserContext from './plugins/context' export type { BrowserCommand } from 'vitest/node' @@ -190,6 +191,7 @@ export default (project: WorkspaceProject, base = '/'): Plugin[] => { return useId }, }, + BrowserContext(project), BrowserCommands(project), { name: 'vitest:browser:esm-injector', diff --git a/packages/browser/src/node/plugins/context.ts b/packages/browser/src/node/plugins/context.ts new file mode 100644 index 000000000000..c0a58d2a5adb --- /dev/null +++ b/packages/browser/src/node/plugins/context.ts @@ -0,0 +1,27 @@ +import type { Plugin } from 'vitest/config' +import type { WorkspaceProject } from 'vitest/node' + +const VIRTUAL_ID_CONTEXT = '\0@vitest/browser/context' +const ID_CONTEXT = '@vitest/browser/context' + +export default function BrowserContext(project: WorkspaceProject): Plugin { + return { + name: 'vitest:browser:virtual-module:context', + enforce: 'pre', + resolveId(id) { + if (id === ID_CONTEXT) + return VIRTUAL_ID_CONTEXT + }, + load(id) { + if (id === VIRTUAL_ID_CONTEXT) + return generateContextFile(project) + }, + } +} + +function generateContextFile(_project: WorkspaceProject) { + return ` +export const getServerPlatform = () => ${JSON.stringify(process.platform)} +export const getConfig = () => __vitest_browser_runner__.config + ` +} diff --git a/test/browser/test/commands.test.ts b/test/browser/test/commands.test.ts index bf2221e6e56a..968c54354d65 100644 --- a/test/browser/test/commands.test.ts +++ b/test/browser/test/commands.test.ts @@ -1,4 +1,11 @@ -import { myCustomCommand, readFile, removeFile, sendKeys, writeFile } from '@vitest/browser/commands' +import { + myCustomCommand, + readFile, + removeFile, + sendKeys, + writeFile, +} from '@vitest/browser/commands' +import { getServerPlatform } from '@vitest/browser/context' import { expect, it } from 'vitest' const provider = import.meta.env.PROVIDER || 'playwright' @@ -12,7 +19,10 @@ it('can manipulate files', async () => { } catch (err) { expect(err.message).toMatch(`ENOENT: no such file or directory, open`) - expect(err.message).toMatch(`test/browser/test/test.txt`) + if (getServerPlatform() === 'win32') + expect(err.message).toMatch('test\\browser\\test\\test.txt') + else + expect(err.message).toMatch('test/browser/test/test.txt') } await writeFile(file, 'hello world') @@ -28,7 +38,10 @@ it('can manipulate files', async () => { } catch (err) { expect(err.message).toMatch(`ENOENT: no such file or directory, open`) - expect(err.message).toMatch(`test/browser/test/test.txt`) + if (getServerPlatform() === 'win32') + expect(err.message).toMatch('test\\browser\\test\\test.txt') + else + expect(err.message).toMatch('test/browser/test/test.txt') } }) From cadb4e3a03d8569d461e277ba7b70cac492f8cc1 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Fri, 10 May 2024 20:02:37 +0200 Subject: [PATCH 12/15] chore: rename --- docs/guide/browser.md | 8 ++++++-- packages/browser/context.d.ts | 8 ++++++-- packages/browser/src/node/plugins/context.ts | 13 ++++++++++--- test/browser/test/commands.test.ts | 6 +++--- 4 files changed, 25 insertions(+), 10 deletions(-) diff --git a/docs/guide/browser.md b/docs/guide/browser.md index 4727b8da9750..0598c5309978 100644 --- a/docs/guide/browser.md +++ b/docs/guide/browser.md @@ -124,8 +124,12 @@ In this case, Vitest will run in headless mode using the Chrome browser. Vitest exposes a context module via `@vitest/browser/context` entry point. As of 2.0, it exposes a small set of utilities that might be useful to you in tests. ```ts -export const getServerPlatform: () => Platform -export const getConfig: () => ResolvedConfig +export const server: { + platform: Platform +} +export const page: { + config: ResolvedConfig +} ``` ## Commands 2.0.0 {#commands} diff --git a/packages/browser/context.d.ts b/packages/browser/context.d.ts index fdedbfa25d97..777ab9f9c738 100644 --- a/packages/browser/context.d.ts +++ b/packages/browser/context.d.ts @@ -13,5 +13,9 @@ type Platform = | 'cygwin' | 'netbsd' -export const getServerPlatform: () => Platform -export const getConfig: () => ResolvedConfig +export const server: { + platform: Platform +} +export const page: { + config: ResolvedConfig +} diff --git a/packages/browser/src/node/plugins/context.ts b/packages/browser/src/node/plugins/context.ts index c0a58d2a5adb..154713063163 100644 --- a/packages/browser/src/node/plugins/context.ts +++ b/packages/browser/src/node/plugins/context.ts @@ -21,7 +21,14 @@ export default function BrowserContext(project: WorkspaceProject): Plugin { function generateContextFile(_project: WorkspaceProject) { return ` -export const getServerPlatform = () => ${JSON.stringify(process.platform)} -export const getConfig = () => __vitest_browser_runner__.config - ` +export const server = { + platform: ${JSON.stringify(process.platform)} +} + +export const page = { + get config() { + return __vitest_browser_runner__.config + } +} +` } diff --git a/test/browser/test/commands.test.ts b/test/browser/test/commands.test.ts index 968c54354d65..a72688658beb 100644 --- a/test/browser/test/commands.test.ts +++ b/test/browser/test/commands.test.ts @@ -5,7 +5,7 @@ import { sendKeys, writeFile, } from '@vitest/browser/commands' -import { getServerPlatform } from '@vitest/browser/context' +import { server } from '@vitest/browser/context' import { expect, it } from 'vitest' const provider = import.meta.env.PROVIDER || 'playwright' @@ -19,7 +19,7 @@ it('can manipulate files', async () => { } catch (err) { expect(err.message).toMatch(`ENOENT: no such file or directory, open`) - if (getServerPlatform() === 'win32') + if (server.platform === 'win32') expect(err.message).toMatch('test\\browser\\test\\test.txt') else expect(err.message).toMatch('test/browser/test/test.txt') @@ -38,7 +38,7 @@ it('can manipulate files', async () => { } catch (err) { expect(err.message).toMatch(`ENOENT: no such file or directory, open`) - if (getServerPlatform() === 'win32') + if (server.platform === 'win32') expect(err.message).toMatch('test\\browser\\test\\test.txt') else expect(err.message).toMatch('test/browser/test/test.txt') From 4136df94466e33d1431b0d52d368362ce7389ccf Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Fri, 10 May 2024 21:38:57 +0200 Subject: [PATCH 13/15] refactor: remove /commands module, export commands from the context --- docs/guide/browser.md | 43 ++++++++++++-- packages/browser/commands.d.ts | 33 ----------- packages/browser/commands.js | 2 - packages/browser/context.d.ts | 58 +++++++++++++++++++ packages/browser/package.json | 4 -- packages/browser/src/node/commands/fs.ts | 8 +-- .../browser/src/node/commands/keyboard.ts | 12 +++- packages/browser/src/node/index.ts | 2 - packages/browser/src/node/plugins/commands.ts | 47 --------------- packages/browser/src/node/plugins/context.ts | 30 +++++++++- test/browser/test/commands.test.ts | 29 +++++----- 11 files changed, 148 insertions(+), 120 deletions(-) delete mode 100644 packages/browser/commands.d.ts delete mode 100644 packages/browser/commands.js delete mode 100644 packages/browser/src/node/plugins/commands.ts diff --git a/docs/guide/browser.md b/docs/guide/browser.md index 0598c5309978..c49eb8c06c35 100644 --- a/docs/guide/browser.md +++ b/docs/guide/browser.md @@ -125,9 +125,34 @@ Vitest exposes a context module via `@vitest/browser/context` entry point. As of ```ts export const server: { + /** + * Platform the Vitest server is running on. + * The same as calling `process.platform` on the server. + */ platform: Platform + /** + * Runtime version of the Vitest server. + * The same as calling `process.version` on the server. + */ + version: string + /** + * Available commands for the browser. + * @see {@link https://vitest.dev/guide/browser#commands} + */ + commands: BrowserCommands } + +/** + * Available commands for the browser. + * A shortcut to `server.commands`. + * @see {@link https://vitest.dev/guide/browser#commands} + */ +export const commands: BrowserCommands + export const page: { + /** + * Serialized test config. + */ config: ResolvedConfig } ``` @@ -149,7 +174,9 @@ This API follows [`server.fs`](https://vitejs.dev/config/server-options.html#ser ::: ```ts -import { readFile, removeFile, writeFile } from '@vitest/browser/commands' +import { server } from '@vitest/browser/context' + +const { readFile, writeFile, removeFile } = server.commands it('handles files', async () => { const file = './test.txt' @@ -226,20 +253,24 @@ export default function BrowserCommands(): Plugin { } ``` -Then you can call it inside your test by importing it from `@vitest/browser/commands`: +Then you can call it inside your test by importing it from `@vitest/browser/context`: ```ts -import { myCustomCommand } from '@vitest/browser/commands' +import { commands } from '@vitest/browser/context' import { expect, test } from 'vitest' test('custom command works correctly', async () => { - const result = await myCustomCommand('test1', 'test2') + const result = await commands.myCustomCommand('test1', 'test2') expect(result).toEqual({ someValue: true }) }) // if you are using TypeScript, you can augment the module -declare module '@vitest/browser/commands' { - export function myCustomCommand(arg1: string, arg2: string): Promise<{ someValue: true }> +declare module '@vitest/browser/context' { + interface BrowserCommands { + myCustomCommand: (arg1: string, arg2: string) => Promise<{ + someValue: true + }> + } } ``` diff --git a/packages/browser/commands.d.ts b/packages/browser/commands.d.ts deleted file mode 100644 index b101b551cc62..000000000000 --- a/packages/browser/commands.d.ts +++ /dev/null @@ -1,33 +0,0 @@ -// we cannot reexport our implementation because it relies on Node.js APIs - -export type BufferEncoding = - | 'ascii' - | 'utf8' - | 'utf-8' - | 'utf16le' - | 'utf-16le' - | 'ucs2' - | 'ucs-2' - | 'base64' - | 'base64url' - | 'latin1' - | 'binary' - | 'hex' - -export interface FsOptions { - encoding?: BufferEncoding - flag?: string | number -} - -export declare function readFile(path: string, options?: BufferEncoding | FsOptions): Promise -export declare function writeFile(path: string, content: string, options?: BufferEncoding | FsOptions & { mode?: number | string }): Promise -export declare function removeFile(path: string): Promise - -export interface TypePayload { type: string } -export interface PressPayload { press: string } -export interface DownPayload { down: string } -export interface UpPayload { up: string } - -export type SendKeysPayload = TypePayload | PressPayload | DownPayload | UpPayload - -export declare function sendKeys(payload: SendKeysPayload): Promise diff --git a/packages/browser/commands.js b/packages/browser/commands.js deleted file mode 100644 index af8cff3be496..000000000000 --- a/packages/browser/commands.js +++ /dev/null @@ -1,2 +0,0 @@ -// empty file to not break bundling -// Vitest resolves "@vitest/browser/commands" as a virtual module instead diff --git a/packages/browser/context.d.ts b/packages/browser/context.d.ts index 777ab9f9c738..562c00cdf10a 100644 --- a/packages/browser/context.d.ts +++ b/packages/browser/context.d.ts @@ -1,5 +1,38 @@ import type { ResolvedConfig } from 'vitest' +export type BufferEncoding = + | 'ascii' + | 'utf8' + | 'utf-8' + | 'utf16le' + | 'utf-16le' + | 'ucs2' + | 'ucs-2' + | 'base64' + | 'base64url' + | 'latin1' + | 'binary' + | 'hex' + +export interface FsOptions { + encoding?: BufferEncoding + flag?: string | number +} + +export interface TypePayload { type: string } +export interface PressPayload { press: string } +export interface DownPayload { down: string } +export interface UpPayload { up: string } + +export type SendKeysPayload = TypePayload | PressPayload | DownPayload | UpPayload + +export interface BrowserCommands { + readFile: (path: string, options?: BufferEncoding | FsOptions) => Promise + writeFile: (path: string, content: string, options?: BufferEncoding | FsOptions & { mode?: number | string }) => Promise + removeFile: (path: string) => Promise + sendKeys: (payload: SendKeysPayload) => Promise +} + type Platform = | 'aix' | 'android' @@ -14,8 +47,33 @@ type Platform = | 'netbsd' export const server: { + /** + * Platform the Vitest server is running on. + * The same as calling `process.platform` on the server. + */ platform: Platform + /** + * Runtime version of the Vitest server. + * The same as calling `process.version` on the server. + */ + version: string + /** + * Available commands for the browser. + * @see {@link https://vitest.dev/guide/browser#commands} + */ + commands: BrowserCommands } + +/** + * Available commands for the browser. + * A shortcut to `server.commands`. + * @see {@link https://vitest.dev/guide/browser#commands} + */ +export const commands: BrowserCommands + export const page: { + /** + * Serialized test config. + */ config: ResolvedConfig } diff --git a/packages/browser/package.json b/packages/browser/package.json index 249cec376b58..180603142af1 100644 --- a/packages/browser/package.json +++ b/packages/browser/package.json @@ -24,10 +24,6 @@ "types": "./providers.d.ts", "default": "./dist/providers.js" }, - "./commands": { - "types": "./commands.d.ts", - "default": "./commands.js" - }, "./context": { "types": "./context.d.ts", "default": "./context.js" diff --git a/packages/browser/src/node/commands/fs.ts b/packages/browser/src/node/commands/fs.ts index 08172c0e03d6..fc10e7a91822 100644 --- a/packages/browser/src/node/commands/fs.ts +++ b/packages/browser/src/node/commands/fs.ts @@ -2,14 +2,14 @@ import fs, { promises as fsp } from 'node:fs' import { dirname, resolve } from 'node:path' import { isFileServingAllowed } from 'vitest/node' import type { BrowserCommand, WorkspaceProject } from 'vitest/node' -import type Types from '../../../commands' +import type { BrowserCommands } from '../../../context' function assertFileAccess(path: string, project: WorkspaceProject) { if (!isFileServingAllowed(path, project.server) && !isFileServingAllowed(path, project.ctx.server)) throw new Error(`Access denied to "${path}". See Vite config documentation for "server.fs": https://vitejs.dev/config/server-options.html#server-fs-strict.`) } -export const readFile: BrowserCommand> = async ({ project, testPath = process.cwd() }, path, options = {}) => { +export const readFile: BrowserCommand> = async ({ project, testPath = process.cwd() }, path, options = {}) => { const filepath = resolve(dirname(testPath), path) assertFileAccess(filepath, project) // never return a Buffer @@ -18,7 +18,7 @@ export const readFile: BrowserCommand> = async return fsp.readFile(filepath, options) } -export const writeFile: BrowserCommand> = async ({ project, testPath = process.cwd() }, path, data, options) => { +export const writeFile: BrowserCommand> = async ({ project, testPath = process.cwd() }, path, data, options) => { const filepath = resolve(dirname(testPath), path) assertFileAccess(filepath, project) const dir = dirname(filepath) @@ -27,7 +27,7 @@ export const writeFile: BrowserCommand> = asy await fsp.writeFile(filepath, data, options) } -export const removeFile: BrowserCommand> = async ({ project, testPath = process.cwd() }, path) => { +export const removeFile: BrowserCommand> = async ({ project, testPath = process.cwd() }, path) => { const filepath = resolve(dirname(testPath), path) assertFileAccess(filepath, project) await fsp.rm(filepath) diff --git a/packages/browser/src/node/commands/keyboard.ts b/packages/browser/src/node/commands/keyboard.ts index bc5141aa6401..5f9dba3f2a52 100644 --- a/packages/browser/src/node/commands/keyboard.ts +++ b/packages/browser/src/node/commands/keyboard.ts @@ -2,8 +2,14 @@ import type { Page } from 'playwright' import type { BrowserCommand } from 'vitest/node' -import type Types from '../../../commands' -import type { DownPayload, PressPayload, SendKeysPayload, TypePayload, UpPayload } from '../../../commands' +import type { + BrowserCommands, + DownPayload, + PressPayload, + SendKeysPayload, + TypePayload, + UpPayload, +} from '../../../context' function isObject(payload: unknown): payload is Record { return payload != null && typeof payload === 'object' @@ -56,7 +62,7 @@ function isUpPayload(payload: SendKeysPayload): payload is UpPayload { return 'up' in payload } -export const sendKeys: BrowserCommand> = async ({ provider }, payload) => { +export const sendKeys: BrowserCommand> = async ({ provider }, payload) => { if (!isSendKeysPayload(payload) || !payload) throw new Error('You must provide a `SendKeysPayload` object') diff --git a/packages/browser/src/node/index.ts b/packages/browser/src/node/index.ts index 8f932abcd3cd..cb53328707e2 100644 --- a/packages/browser/src/node/index.ts +++ b/packages/browser/src/node/index.ts @@ -8,7 +8,6 @@ import type { BrowserScript, WorkspaceProject } from 'vitest/node' import { type Plugin, coverageConfigDefaults } from 'vitest/config' import { slash } from '@vitest/utils' import { injectVitestModule } from './esmInjector' -import BrowserCommands from './plugins/commands' import BrowserContext from './plugins/context' export type { BrowserCommand } from 'vitest/node' @@ -192,7 +191,6 @@ export default (project: WorkspaceProject, base = '/'): Plugin[] => { }, }, BrowserContext(project), - BrowserCommands(project), { name: 'vitest:browser:esm-injector', enforce: 'post', diff --git a/packages/browser/src/node/plugins/commands.ts b/packages/browser/src/node/plugins/commands.ts deleted file mode 100644 index 1367dd40196d..000000000000 --- a/packages/browser/src/node/plugins/commands.ts +++ /dev/null @@ -1,47 +0,0 @@ -import type { Plugin } from 'vitest/config' -import type { BrowserCommand, WorkspaceProject } from 'vitest/node' -import builtinCommands from '../commands/index' - -const VIRTUAL_ID_COMMANDS = '\0@vitest/browser/commands' -const ID_COMMANDS = '@vitest/browser/commands' - -export default function BrowserCommands(project: WorkspaceProject): Plugin { - project.config.browser.commands ??= {} - for (const [name, command] of Object.entries(builtinCommands)) - project.config.browser.commands[name] ??= command - - // validate names because they can't be used as identifiers - for (const command in project.config.browser.commands) { - if (!/^[a-zA-Z_$][a-zA-Z_$0-9]*$/.test(command)) - throw new Error(`Invalid command name "${command}". Only alphanumeric characters, $ and _ are allowed.`) - } - - return { - name: 'vitest:browser:virtual-module:commands', - enforce: 'pre', - resolveId(id) { - if (id === ID_COMMANDS) - return VIRTUAL_ID_COMMANDS - }, - load(id) { - if (id === VIRTUAL_ID_COMMANDS) - return generateCommandsFile(project.config.browser.commands ?? {}) - }, - } -} - -function generateCommandsFile(commandsMap: Record>) { - const commands = Object.keys(commandsMap) - - if (!commands.length) - return '' - - const prepare = ` -const rpc = () => __vitest_worker__.rpc -` - const commandsCode = commands.map((command) => { - return `export const ${command} = (...args) => rpc().triggerCommand('${command}', __vitest_worker__.filepath || __vitest_worker__.current?.file?.filepath || undefined, args)` - }) - - return `${prepare}\n${commandsCode.join('\n')}` -} diff --git a/packages/browser/src/node/plugins/context.ts b/packages/browser/src/node/plugins/context.ts index 154713063163..c5cfc3bbba91 100644 --- a/packages/browser/src/node/plugins/context.ts +++ b/packages/browser/src/node/plugins/context.ts @@ -1,10 +1,21 @@ import type { Plugin } from 'vitest/config' import type { WorkspaceProject } from 'vitest/node' +import builtinCommands from '../commands/index' const VIRTUAL_ID_CONTEXT = '\0@vitest/browser/context' const ID_CONTEXT = '@vitest/browser/context' export default function BrowserContext(project: WorkspaceProject): Plugin { + project.config.browser.commands ??= {} + for (const [name, command] of Object.entries(builtinCommands)) + project.config.browser.commands[name] ??= command + + // validate names because they can't be used as identifiers + for (const command in project.config.browser.commands) { + if (!/^[a-zA-Z_$][a-zA-Z_$0-9]*$/.test(command)) + throw new Error(`Invalid command name "${command}". Only alphanumeric characters, $ and _ are allowed.`) + } + return { name: 'vitest:browser:virtual-module:context', enforce: 'pre', @@ -19,12 +30,25 @@ export default function BrowserContext(project: WorkspaceProject): Plugin { } } -function generateContextFile(_project: WorkspaceProject) { +function generateContextFile(project: WorkspaceProject) { + const commands = Object.keys(project.config.browser.commands ?? {}) + const filepathCode = '__vitest_worker__.filepath || __vitest_worker__.current?.file?.filepath || undefined' + + const commandsCode = commands.map((command) => { + return ` ["${command}"]: (...args) => rpc().triggerCommand("${command}", ${filepathCode}, args),` + }).join('\n') + return ` +const rpc = () => __vitest_worker__.rpc + export const server = { - platform: ${JSON.stringify(process.platform)} + platform: ${JSON.stringify(process.platform)}, + version: ${JSON.stringify(process.version)}, + commands: { + ${commandsCode} + } } - +export const commands = server.commands export const page = { get config() { return __vitest_browser_runner__.config diff --git a/test/browser/test/commands.test.ts b/test/browser/test/commands.test.ts index a72688658beb..87ad2a84d586 100644 --- a/test/browser/test/commands.test.ts +++ b/test/browser/test/commands.test.ts @@ -1,15 +1,10 @@ -import { - myCustomCommand, - readFile, - removeFile, - sendKeys, - writeFile, -} from '@vitest/browser/commands' -import { server } from '@vitest/browser/context' +import { commands, server } from '@vitest/browser/context' import { expect, it } from 'vitest' const provider = import.meta.env.PROVIDER || 'playwright' +const { readFile, writeFile, removeFile, sendKeys, myCustomCommand } = server.commands + it('can manipulate files', async () => { const file = './test.txt' @@ -52,7 +47,7 @@ it('natively types into an input', async () => { document.body.append(input) input.focus() - await sendKeys({ + await commands.sendKeys({ type: keys, }) @@ -67,7 +62,7 @@ it('natively presses `Tab`', async () => { input1.focus() expect(document.activeElement).to.equal(input1) - await sendKeys({ + await commands.sendKeys({ press: 'Tab', }) @@ -143,10 +138,12 @@ it('can run custom commands', async () => { }) }) -declare module '@vitest/browser/commands' { - export function myCustomCommand(arg1: string, arg2: string): Promise<{ - testPath: string - arg1: string - arg2: string - }> +declare module '@vitest/browser/context' { + interface BrowserCommands { + myCustomCommand: (arg1: string, arg2: string) => Promise<{ + testPath: string + arg1: string + arg2: string + }> + } } From adfaf3cb18cacd68b169b7fcc252c980943324d9 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Tue, 14 May 2024 19:02:05 +0200 Subject: [PATCH 14/15] chore: add jsdoc to commands API --- packages/vitest/src/types/browser.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/vitest/src/types/browser.ts b/packages/vitest/src/types/browser.ts index 2fa58c565468..f7987cf7bc24 100644 --- a/packages/vitest/src/types/browser.ts +++ b/packages/vitest/src/types/browser.ts @@ -104,7 +104,11 @@ export interface BrowserConfigOptions { */ indexScripts?: BrowserScript[] - // TODO + /** + * Commands that will be executed on the server + * via the browser `import("@vitest/browser/context").commands` API. + * @see {@link https://vitest.dev/guide/browser#commands} + */ commands?: Record> } From 483c259f623ea07ef3cb8bf81aeb5eded760693c Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Tue, 14 May 2024 19:04:33 +0200 Subject: [PATCH 15/15] chore: expose "provider" property --- packages/browser/context.d.ts | 4 ++++ packages/browser/src/node/plugins/context.ts | 3 ++- test/browser/test/commands.test.ts | 6 ++---- test/browser/vitest.config.mts | 3 --- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/browser/context.d.ts b/packages/browser/context.d.ts index 562c00cdf10a..b9ba9fbc9ce9 100644 --- a/packages/browser/context.d.ts +++ b/packages/browser/context.d.ts @@ -57,6 +57,10 @@ export const server: { * The same as calling `process.version` on the server. */ version: string + /** + * Name of the browser provider. + */ + provider: string /** * Available commands for the browser. * @see {@link https://vitest.dev/guide/browser#commands} diff --git a/packages/browser/src/node/plugins/context.ts b/packages/browser/src/node/plugins/context.ts index c5cfc3bbba91..2eee16e33fa3 100644 --- a/packages/browser/src/node/plugins/context.ts +++ b/packages/browser/src/node/plugins/context.ts @@ -12,7 +12,7 @@ export default function BrowserContext(project: WorkspaceProject): Plugin { // validate names because they can't be used as identifiers for (const command in project.config.browser.commands) { - if (!/^[a-zA-Z_$][a-zA-Z_$0-9]*$/.test(command)) + if (!/^[a-z_$][\w$]*$/i.test(command)) throw new Error(`Invalid command name "${command}". Only alphanumeric characters, $ and _ are allowed.`) } @@ -44,6 +44,7 @@ const rpc = () => __vitest_worker__.rpc export const server = { platform: ${JSON.stringify(process.platform)}, version: ${JSON.stringify(process.version)}, + provider: ${JSON.stringify(project.browserProvider!.name)}, commands: { ${commandsCode} } diff --git a/test/browser/test/commands.test.ts b/test/browser/test/commands.test.ts index 87ad2a84d586..a14c7c1da164 100644 --- a/test/browser/test/commands.test.ts +++ b/test/browser/test/commands.test.ts @@ -1,8 +1,6 @@ import { commands, server } from '@vitest/browser/context' import { expect, it } from 'vitest' -const provider = import.meta.env.PROVIDER || 'playwright' - const { readFile, writeFile, removeFile, sendKeys, myCustomCommand } = server.commands it('can manipulate files', async () => { @@ -71,7 +69,7 @@ it('natively presses `Tab`', async () => { input2.remove() }) -it.skipIf(provider === 'webdriverio')('natively presses `Shift+Tab`', async () => { +it.skipIf(server.provider === 'webdriverio')('natively presses `Shift+Tab`', async () => { const input1 = document.createElement('input') const input2 = document.createElement('input') document.body.append(input1, input2) @@ -93,7 +91,7 @@ it.skipIf(provider === 'webdriverio')('natively presses `Shift+Tab`', async () = input2.remove() }) -it.skipIf(provider === 'webdriverio')('natively holds and then releases a key', async () => { +it.skipIf(server.provider === 'webdriverio')('natively holds and then releases a key', async () => { const input = document.createElement('input') document.body.append(input) input.focus() diff --git a/test/browser/vitest.config.mts b/test/browser/vitest.config.mts index e28393e80bfd..2308d6c4cee0 100644 --- a/test/browser/vitest.config.mts +++ b/test/browser/vitest.config.mts @@ -23,9 +23,6 @@ export default defineConfig({ optimizeDeps: { include: ['@vitest/cjs-lib'], }, - define: { - 'import.meta.env.PROVIDER': JSON.stringify(provider), - }, test: { include: ['test/**.test.{ts,js}'], // having a snapshot environment doesn't affect browser tests