Skip to content

Commit 19278f4

Browse files
authoredOct 21, 2024··
fix(browser): cleanup keyboard state (#6731)
1 parent 63f8b07 commit 19278f4

File tree

11 files changed

+164
-8
lines changed

11 files changed

+164
-8
lines changed
 

‎docs/guide/browser/context.md

+1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ The `userEvent` API is explained in detail at [Interactivity API](/guide/browser
2020
*/
2121
export const userEvent: {
2222
setup: () => UserEvent
23+
cleanup: () => Promise<void>
2324
click: (element: Element, options?: UserEventClickOptions) => Promise<void>
2425
dblClick: (element: Element, options?: UserEventDoubleClickOptions) => Promise<void>
2526
tripleClick: (element: Element, options?: UserEventTripleClickOptions) => Promise<void>

‎packages/browser/context.d.ts

+6
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,12 @@ export interface UserEvent {
5959
* @see {@link https://vitest.dev/guide/browser/interactivity-api.html#userevent-setup}
6060
*/
6161
setup: () => UserEvent
62+
/**
63+
* Cleans up the user event instance, releasing any resources or state it holds,
64+
* such as keyboard press state. For the default `userEvent` instance, this method
65+
* is automatically called after each test case.
66+
*/
67+
cleanup: () => Promise<void>
6268
/**
6369
* Click on an element. Uses provider's API under the hood and supports all its options.
6470
* @see {@link https://playwright.dev/docs/api/class-locator#locator-click} Playwright API

‎packages/browser/src/client/tester/context.ts

+12-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { RunnerTask } from 'vitest'
22
import type { BrowserRPC } from '@vitest/browser/client'
3-
import type { UserEvent as TestingLibraryUserEvent } from '@testing-library/user-event'
3+
import type { Options as TestingLibraryOptions, UserEvent as TestingLibraryUserEvent } from '@testing-library/user-event'
44
import type {
55
BrowserPage,
66
Locator,
@@ -29,14 +29,23 @@ function triggerCommand<T>(command: string, ...args: any[]) {
2929
return rpc().triggerCommand<T>(contextId, command, filepath(), args)
3030
}
3131

32-
export function createUserEvent(__tl_user_event__?: TestingLibraryUserEvent): UserEvent {
32+
export function createUserEvent(__tl_user_event_base__?: TestingLibraryUserEvent, options?: TestingLibraryOptions): UserEvent {
33+
let __tl_user_event__ = __tl_user_event_base__?.setup(options ?? {})
3334
const keyboard = {
3435
unreleased: [] as string[],
3536
}
3637

3738
return {
3839
setup(options?: any) {
39-
return createUserEvent(__tl_user_event__?.setup(options))
40+
return createUserEvent(__tl_user_event_base__, options)
41+
},
42+
async cleanup() {
43+
if (typeof __tl_user_event_base__ !== 'undefined') {
44+
__tl_user_event__ = __tl_user_event_base__?.setup(options ?? {})
45+
return
46+
}
47+
await triggerCommand('__vitest_cleanup', keyboard)
48+
keyboard.unreleased = []
4049
},
4150
click(element: Element | Locator, options: UserEventClickOptions = {}) {
4251
return convertToLocator(element).click(processClickOptions(options))

‎packages/browser/src/client/tester/runner.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import type { VitestExecutor } from 'vitest/execute'
44
import { NodeBenchmarkRunner, VitestTestRunner } from 'vitest/runners'
55
import { loadDiffConfig, loadSnapshotSerializers, takeCoverageInsideWorker } from 'vitest/browser'
66
import { TraceMap, originalPositionFor } from 'vitest/utils'
7-
import { page } from '@vitest/browser/context'
7+
import { page, userEvent } from '@vitest/browser/context'
88
import { globalChannel } from '@vitest/browser/client'
99
import { executor } from '../utils'
1010
import { VitestBrowserSnapshotEnvironment } from './snapshot'
@@ -41,6 +41,7 @@ export function createBrowserRunner(
4141
}
4242

4343
onAfterRunTask = async (task: Task) => {
44+
await userEvent.cleanup()
4445
await super.onAfterRunTask?.(task)
4546

4647
if (this.config.bail && task.result?.state === 'fail') {

‎packages/browser/src/node/commands/index.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { clear } from './clear'
44
import { fill } from './fill'
55
import { selectOptions } from './select'
66
import { tab } from './tab'
7-
import { keyboard } from './keyboard'
7+
import { keyboard, keyboardCleanup } from './keyboard'
88
import { dragAndDrop } from './dragAndDrop'
99
import { hover } from './hover'
1010
import { upload } from './upload'
@@ -34,4 +34,5 @@ export default {
3434
__vitest_selectOptions: selectOptions,
3535
__vitest_dragAndDrop: dragAndDrop,
3636
__vitest_hover: hover,
37+
__vitest_cleanup: keyboardCleanup,
3738
}

‎packages/browser/src/node/commands/keyboard.ts

+23
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,29 @@ export const keyboard: UserEventCommand<(text: string, state: KeyboardState) =>
4949
}
5050
}
5151

52+
export const keyboardCleanup: UserEventCommand<(state: KeyboardState) => Promise<void>> = async (
53+
context,
54+
state,
55+
) => {
56+
const { provider, contextId } = context
57+
if (provider instanceof PlaywrightBrowserProvider) {
58+
const page = provider.getPage(contextId)
59+
for (const key of state.unreleased) {
60+
await page.keyboard.up(key)
61+
}
62+
}
63+
else if (provider instanceof WebdriverBrowserProvider) {
64+
const keyboard = provider.browser!.action('key')
65+
for (const key of state.unreleased) {
66+
keyboard.up(key)
67+
}
68+
await keyboard.perform()
69+
}
70+
else {
71+
throw new TypeError(`Provider "${context.provider.name}" does not support keyboard api`)
72+
}
73+
}
74+
5275
export async function keyboardImplementation(
5376
pressed: Set<string>,
5477
provider: BrowserProvider,

‎packages/browser/src/node/plugins/pluginContext.ts

+4-3
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,8 @@ async function getUserEventImport(provider: BrowserProvider, resolve: (id: strin
9797
if (!resolved) {
9898
throw new Error(`Failed to resolve user-event package from ${__dirname}`)
9999
}
100-
return `import { userEvent as __vitest_user_event__ } from '${slash(
101-
`/@fs/${resolved.id}`,
102-
)}'\nconst _userEventSetup = __vitest_user_event__.setup()\n`
100+
return `\
101+
import { userEvent as __vitest_user_event__ } from '${slash(`/@fs/${resolved.id}`)}'
102+
const _userEventSetup = __vitest_user_event__
103+
`
103104
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { expect, onTestFinished, test } from 'vitest'
2+
import { userEvent } from '@vitest/browser/context'
3+
4+
test('cleanup1', async () => {
5+
let logs: any[] = [];
6+
function handler(e: KeyboardEvent) {
7+
logs.push([e.key, e.altKey]);
8+
};
9+
document.addEventListener('keydown', handler)
10+
onTestFinished(() => {
11+
document.removeEventListener('keydown', handler);
12+
})
13+
14+
await userEvent.keyboard('{Tab}')
15+
await userEvent.keyboard("{Alt>}")
16+
expect(logs).toMatchInlineSnapshot(`
17+
[
18+
[
19+
"Tab",
20+
false,
21+
],
22+
[
23+
"Alt",
24+
true,
25+
],
26+
]
27+
`)
28+
})
29+
30+
// test per-test cleanup
31+
test('cleanup1.2', async () => {
32+
let logs: any[] = [];
33+
function handler(e: KeyboardEvent) {
34+
logs.push([e.key, e.altKey]);
35+
};
36+
document.addEventListener('keydown', handler)
37+
onTestFinished(() => {
38+
document.removeEventListener('keydown', handler);
39+
})
40+
41+
await userEvent.keyboard('{Tab}')
42+
await userEvent.keyboard("{Alt>}")
43+
expect(logs).toMatchInlineSnapshot(`
44+
[
45+
[
46+
"Tab",
47+
false,
48+
],
49+
[
50+
"Alt",
51+
true,
52+
],
53+
]
54+
`)
55+
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { expect, onTestFinished, test } from 'vitest'
2+
import { userEvent } from '@vitest/browser/context'
3+
4+
// test per-test-file cleanup just in case
5+
6+
test('cleanup2', async () => {
7+
let logs: any[] = [];
8+
function handler(e: KeyboardEvent) {
9+
logs.push([e.key, e.altKey]);
10+
};
11+
document.addEventListener('keydown', handler)
12+
onTestFinished(() => {
13+
document.removeEventListener('keydown', handler);
14+
})
15+
16+
await userEvent.keyboard('{Tab}')
17+
await userEvent.keyboard("{Alt>}")
18+
expect(logs).toMatchInlineSnapshot(`
19+
[
20+
[
21+
"Tab",
22+
false,
23+
],
24+
[
25+
"Alt",
26+
true,
27+
],
28+
]
29+
`)
30+
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { fileURLToPath } from 'node:url'
2+
import { defineConfig } from 'vitest/config'
3+
4+
const provider = process.env.PROVIDER || 'playwright'
5+
const name =
6+
process.env.BROWSER || (provider === 'playwright' ? 'chromium' : 'chrome')
7+
8+
export default defineConfig({
9+
cacheDir: fileURLToPath(new URL("./node_modules/.vite", import.meta.url)),
10+
test: {
11+
browser: {
12+
enabled: true,
13+
provider,
14+
name,
15+
},
16+
},
17+
})

‎test/browser/specs/runner.test.ts

+12
Original file line numberDiff line numberDiff line change
@@ -137,3 +137,15 @@ error with a stack
137137
expect(stderr).toContain('Access denied to "/inaccesible/path".')
138138
})
139139
})
140+
141+
test('user-event', async () => {
142+
const { ctx } = await runBrowserTests({
143+
root: './fixtures/user-event',
144+
})
145+
expect(Object.fromEntries(ctx.state.getFiles().map(f => [f.name, f.result.state]))).toMatchInlineSnapshot(`
146+
{
147+
"cleanup1.test.ts": "pass",
148+
"cleanup2.test.ts": "pass",
149+
}
150+
`)
151+
})

0 commit comments

Comments
 (0)
Please sign in to comment.