Skip to content

Commit

Permalink
fix(browser): cleanup keyboard state (#6731)
Browse files Browse the repository at this point in the history
  • Loading branch information
hi-ogawa authored Oct 21, 2024
1 parent 63f8b07 commit 19278f4
Show file tree
Hide file tree
Showing 11 changed files with 164 additions and 8 deletions.
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",
}
`)
})

0 comments on commit 19278f4

Please sign in to comment.