Skip to content

Commit

Permalink
feat(browser): add commands to communicate betweens server and the br…
Browse files Browse the repository at this point in the history
…owser (#5097)
  • Loading branch information
sheremet-va committed May 14, 2024
1 parent 222ce44 commit aa431f4
Show file tree
Hide file tree
Showing 23 changed files with 683 additions and 29 deletions.
7 changes: 7 additions & 0 deletions docs/config/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <Version>2.0.0</Version> {#browser-commands}

- **Type:** `Record<string, BrowserCommand>`
- **Default:** `{ readFile, writeFile, ... }`

Custom [commands](/guide/browser#commands) that can be import during browser tests from `@vitest/browser/commands`.

### clearMocks

- **Type:** `boolean`
Expand Down
159 changes: 159 additions & 0 deletions docs/guide/browser.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,165 @@ npx vitest --browser.name=chrome --browser.headless

In this case, Vitest will run in headless mode using the Chrome browser.

## Context <Version>2.0.0</Version> {#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 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
}
```

## Commands <Version>2.0.0</Version> {#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 { server } from '@vitest/browser/context'

const { readFile, writeFile, removeFile } = server.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<void>
```

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/context`:

```ts
import { commands } from '@vitest/browser/context'
import { expect, test } from 'vitest'
test('custom command works correctly', async () => {
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/context' {
interface BrowserCommands {
myCustomCommand: (arg1: string, arg2: string) => Promise<{
someValue: true
}>
}
}
```

::: warning
Custom functions will override built-in ones if they have the same name.
:::

## Limitations

### Thread Blocking Dialogs
Expand Down
83 changes: 83 additions & 0 deletions packages/browser/context.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
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<string>
writeFile: (path: string, content: string, options?: BufferEncoding | FsOptions & { mode?: number | string }) => Promise<void>
removeFile: (path: string) => Promise<void>
sendKeys: (payload: SendKeysPayload) => Promise<void>
}

type Platform =
| 'aix'
| 'android'
| 'darwin'
| 'freebsd'
| 'haiku'
| 'linux'
| 'openbsd'
| 'sunos'
| 'win32'
| 'cygwin'
| '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
/**
* Name of the browser provider.
*/
provider: 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
}
2 changes: 2 additions & 0 deletions packages/browser/context.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// empty file to not break bundling
// Vitest resolves "@vitest/browser/context" as a virtual module instead
4 changes: 4 additions & 0 deletions packages/browser/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@
"types": "./providers.d.ts",
"default": "./dist/providers.js"
},
"./context": {
"types": "./context.d.ts",
"default": "./context.js"
},
"./providers/webdriverio": {
"types": "./providers/webdriverio.d.ts"
},
Expand Down
34 changes: 34 additions & 0 deletions packages/browser/src/node/commands/fs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
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 { 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<Parameters<BrowserCommands['readFile']>> = 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<Parameters<BrowserCommands['writeFile']>> = 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(filepath, data, options)
}

export const removeFile: BrowserCommand<Parameters<BrowserCommands['removeFile']>> = async ({ project, testPath = process.cwd() }, path) => {
const filepath = resolve(dirname(testPath), path)
assertFileAccess(filepath, project)
await fsp.rm(filepath)
}
13 changes: 13 additions & 0 deletions packages/browser/src/node/commands/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import {
readFile,
removeFile,
writeFile,
} from './fs'
import { sendKeys } from './keyboard'

export default {
readFile,
removeFile,
writeFile,
sendKeys,
}

0 comments on commit aa431f4

Please sign in to comment.