Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/guide/browser/context.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ The `userEvent` API is explained in detail at [Interactivity API](/guide/browser
*/
export const userEvent: {
setup: () => UserEvent
cleanup: () => Promise<void>
click: (element: Element, options?: UserEventClickOptions) => Promise<void>
dblClick: (element: Element, options?: UserEventDoubleClickOptions) => Promise<void>
tripleClick: (element: Element, options?: UserEventTripleClickOptions) => Promise<void>
Expand Down
6 changes: 6 additions & 0 deletions packages/browser/context.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,12 @@ export interface UserEvent {
* @see {@link https://vitest.dev/guide/browser/interactivity-api.html#userevent-setup}
*/
setup: () => UserEvent
/**
* Cleans up the user event instance, releasing any resources or state it holds,
* such as keyboard press state. For the default `userEvent` instance, this method
* is automatically called after each test case.
*/
cleanup: () => Promise<void>
/**
* Click on an element. Uses provider's API under the hood and supports all its options.
* @see {@link https://playwright.dev/docs/api/class-locator#locator-click} Playwright API
Expand Down
15 changes: 12 additions & 3 deletions packages/browser/src/client/tester/context.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { RunnerTask } from 'vitest'
import type { BrowserRPC } from '@vitest/browser/client'
import type { UserEvent as TestingLibraryUserEvent } from '@testing-library/user-event'
import type { Options as TestingLibraryOptions, UserEvent as TestingLibraryUserEvent } from '@testing-library/user-event'
import type {
BrowserPage,
Locator,
Expand Down Expand Up @@ -29,14 +29,23 @@ function triggerCommand<T>(command: string, ...args: any[]) {
return rpc().triggerCommand<T>(contextId, command, filepath(), args)
}

export function createUserEvent(__tl_user_event__?: TestingLibraryUserEvent): UserEvent {
export function createUserEvent(__tl_user_event_base__?: TestingLibraryUserEvent, options?: TestingLibraryOptions): UserEvent {
let __tl_user_event__ = __tl_user_event_base__?.setup(options ?? {})
const keyboard = {
unreleased: [] as string[],
}

return {
setup(options?: any) {
return createUserEvent(__tl_user_event__?.setup(options))
return createUserEvent(__tl_user_event_base__, options)
},
async cleanup() {
if (typeof __tl_user_event_base__ !== 'undefined') {
__tl_user_event__ = __tl_user_event_base__?.setup(options ?? {})
return
}
await triggerCommand('__vitest_cleanup', keyboard)
keyboard.unreleased = []
},
click(element: Element | Locator, options: UserEventClickOptions = {}) {
return convertToLocator(element).click(processClickOptions(options))
Expand Down
3 changes: 2 additions & 1 deletion packages/browser/src/client/tester/runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type { VitestExecutor } from 'vitest/execute'
import { NodeBenchmarkRunner, VitestTestRunner } from 'vitest/runners'
import { loadDiffConfig, loadSnapshotSerializers, takeCoverageInsideWorker } from 'vitest/browser'
import { TraceMap, originalPositionFor } from 'vitest/utils'
import { page } from '@vitest/browser/context'
import { page, userEvent } from '@vitest/browser/context'
import { globalChannel } from '@vitest/browser/client'
import { executor } from '../utils'
import { VitestBrowserSnapshotEnvironment } from './snapshot'
Expand Down Expand Up @@ -41,6 +41,7 @@ export function createBrowserRunner(
}

onAfterRunTask = async (task: Task) => {
await userEvent.cleanup()
await super.onAfterRunTask?.(task)

if (this.config.bail && task.result?.state === 'fail') {
Expand Down
3 changes: 2 additions & 1 deletion packages/browser/src/node/commands/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { clear } from './clear'
import { fill } from './fill'
import { selectOptions } from './select'
import { tab } from './tab'
import { keyboard } from './keyboard'
import { keyboard, keyboardCleanup } from './keyboard'
import { dragAndDrop } from './dragAndDrop'
import { hover } from './hover'
import { upload } from './upload'
Expand Down Expand Up @@ -34,4 +34,5 @@ export default {
__vitest_selectOptions: selectOptions,
__vitest_dragAndDrop: dragAndDrop,
__vitest_hover: hover,
__vitest_cleanup: keyboardCleanup,
}
23 changes: 23 additions & 0 deletions packages/browser/src/node/commands/keyboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,29 @@ export const keyboard: UserEventCommand<(text: string, state: KeyboardState) =>
}
}

export const keyboardCleanup: UserEventCommand<(state: KeyboardState) => Promise<void>> = async (
context,
state,
) => {
const { provider, contextId } = context
if (provider instanceof PlaywrightBrowserProvider) {
const page = provider.getPage(contextId)
for (const key of state.unreleased) {
await page.keyboard.up(key)
}
}
else if (provider instanceof WebdriverBrowserProvider) {
const keyboard = provider.browser!.action('key')
for (const key of state.unreleased) {
keyboard.up(key)
}
await keyboard.perform()
}
else {
throw new TypeError(`Provider "${context.provider.name}" does not support keyboard api`)
}
}

export async function keyboardImplementation(
pressed: Set<string>,
provider: BrowserProvider,
Expand Down
7 changes: 4 additions & 3 deletions packages/browser/src/node/plugins/pluginContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,8 @@ async function getUserEventImport(provider: BrowserProvider, resolve: (id: strin
if (!resolved) {
throw new Error(`Failed to resolve user-event package from ${__dirname}`)
}
return `import { userEvent as __vitest_user_event__ } from '${slash(
`/@fs/${resolved.id}`,
)}'\nconst _userEventSetup = __vitest_user_event__.setup()\n`
return `\
import { userEvent as __vitest_user_event__ } from '${slash(`/@fs/${resolved.id}`)}'
const _userEventSetup = __vitest_user_event__
`
}
55 changes: 55 additions & 0 deletions test/browser/fixtures/user-event/cleanup1.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { expect, onTestFinished, test } from 'vitest'
import { userEvent } from '@vitest/browser/context'

test('cleanup1', async () => {
let logs: any[] = [];
function handler(e: KeyboardEvent) {
logs.push([e.key, e.altKey]);
};
document.addEventListener('keydown', handler)
onTestFinished(() => {
document.removeEventListener('keydown', handler);
})

await userEvent.keyboard('{Tab}')
await userEvent.keyboard("{Alt>}")
expect(logs).toMatchInlineSnapshot(`
[
[
"Tab",
false,
],
[
"Alt",
true,
],
]
`)
})

// test per-test cleanup
test('cleanup1.2', async () => {
let logs: any[] = [];
function handler(e: KeyboardEvent) {
logs.push([e.key, e.altKey]);
};
document.addEventListener('keydown', handler)
onTestFinished(() => {
document.removeEventListener('keydown', handler);
})

await userEvent.keyboard('{Tab}')
await userEvent.keyboard("{Alt>}")
expect(logs).toMatchInlineSnapshot(`
[
[
"Tab",
false,
],
[
"Alt",
true,
],
]
`)
})
30 changes: 30 additions & 0 deletions test/browser/fixtures/user-event/cleanup2.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { expect, onTestFinished, test } from 'vitest'
import { userEvent } from '@vitest/browser/context'

// test per-test-file cleanup just in case

test('cleanup2', async () => {
let logs: any[] = [];
function handler(e: KeyboardEvent) {
logs.push([e.key, e.altKey]);
};
document.addEventListener('keydown', handler)
onTestFinished(() => {
document.removeEventListener('keydown', handler);
})

await userEvent.keyboard('{Tab}')
await userEvent.keyboard("{Alt>}")
expect(logs).toMatchInlineSnapshot(`
[
[
"Tab",
false,
],
[
"Alt",
true,
],
]
`)
})
17 changes: 17 additions & 0 deletions test/browser/fixtures/user-event/vitest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { fileURLToPath } from 'node:url'
import { defineConfig } from 'vitest/config'

const provider = process.env.PROVIDER || 'playwright'
const name =
process.env.BROWSER || (provider === 'playwright' ? 'chromium' : 'chrome')

export default defineConfig({
cacheDir: fileURLToPath(new URL("./node_modules/.vite", import.meta.url)),
test: {
browser: {
enabled: true,
provider,
name,
},
},
})
12 changes: 12 additions & 0 deletions test/browser/specs/runner.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,3 +137,15 @@ error with a stack
expect(stderr).toContain('Access denied to "/inaccesible/path".')
})
})

test('user-event', async () => {
const { ctx } = await runBrowserTests({
root: './fixtures/user-event',
})
expect(Object.fromEntries(ctx.state.getFiles().map(f => [f.name, f.result.state]))).toMatchInlineSnapshot(`
{
"cleanup1.test.ts": "pass",
"cleanup2.test.ts": "pass",
}
`)
})