Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(browser): fix user event state on preview provider #7041

Merged
merged 11 commits into from
Dec 9, 2024
122 changes: 101 additions & 21 deletions packages/browser/src/client/tester/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,21 +30,20 @@ function triggerCommand<T>(command: string, ...args: any[]) {
}

export function createUserEvent(__tl_user_event_base__?: TestingLibraryUserEvent, options?: TestingLibraryOptions): UserEvent {
let __tl_user_event__ = __tl_user_event_base__?.setup(options ?? {})
if (__tl_user_event_base__) {
return createPreviewUserEvent(__tl_user_event_base__, options ?? {})
}

const keyboard = {
unreleased: [] as string[],
}

return {
setup(options?: any) {
return createUserEvent(__tl_user_event_base__, options)
setup() {
return createUserEvent()
},
async cleanup() {
return ensureAwaited(async () => {
if (typeof __tl_user_event_base__ !== 'undefined') {
__tl_user_event__ = __tl_user_event_base__?.setup(options ?? {})
return
}
await triggerCommand('__vitest_cleanup', keyboard)
keyboard.unreleased = []
})
Expand Down Expand Up @@ -87,14 +86,6 @@ export function createUserEvent(__tl_user_event_base__?: TestingLibraryUserEvent
// testing-library user-event
async type(element: Element | Locator, text: string, options: UserEventTypeOptions = {}) {
return ensureAwaited(async () => {
if (typeof __tl_user_event__ !== 'undefined') {
return __tl_user_event__.type(
element instanceof Element ? element : element.element(),
text,
options,
)
}

const selector = convertToSelector(element)
const { unreleased } = await triggerCommand<{ unreleased: string[] }>(
'__vitest_type',
Expand All @@ -107,17 +98,11 @@ export function createUserEvent(__tl_user_event_base__?: TestingLibraryUserEvent
},
tab(options: UserEventTabOptions = {}) {
return ensureAwaited(() => {
if (typeof __tl_user_event__ !== 'undefined') {
return __tl_user_event__.tab(options)
}
return triggerCommand('__vitest_tab', options)
})
},
async keyboard(text: string) {
return ensureAwaited(async () => {
if (typeof __tl_user_event__ !== 'undefined') {
return __tl_user_event__.keyboard(text)
}
const { unreleased } = await triggerCommand<{ unreleased: string[] }>(
'__vitest_keyboard',
text,
Expand All @@ -129,6 +114,101 @@ export function createUserEvent(__tl_user_event_base__?: TestingLibraryUserEvent
}
}

function createPreviewUserEvent(userEventBase: TestingLibraryUserEvent, options: TestingLibraryOptions): UserEvent {
let userEvent = userEventBase.setup(options)

function toElement(element: Element | Locator) {
return element instanceof Element ? element : element.element()
}

const vitestUserEvent: UserEvent = {
setup(options?: any) {
return createPreviewUserEvent(userEventBase, options)
},
async cleanup() {
userEvent = userEventBase.setup(options ?? {})
},
async click(element) {
await userEvent.click(toElement(element))
},
async dblClick(element) {
await userEvent.dblClick(toElement(element))
},
async tripleClick(element) {
await userEvent.tripleClick(toElement(element))
},
async selectOptions(element, value) {
const options = (Array.isArray(value) ? value : [value]).map((option) => {
if (typeof option !== 'string') {
return toElement(option)
}
return option
})
await userEvent.selectOptions(
element,
options as string[] | HTMLElement[],
)
},
async clear(element) {
await userEvent.clear(toElement(element))
},
async hover(element: Element | Locator) {
await userEvent.hover(toElement(element))
},
async unhover(element: Element | Locator) {
await userEvent.unhover(toElement(element))
},
async upload(element: Element | Locator, files: string | string[] | File | File[]) {
const uploadPromise = (Array.isArray(files) ? files : [files]).map(async (file) => {
if (typeof file !== 'string') {
return file
}

const { content: base64, basename, mime } = await triggerCommand<{
content: string
basename: string
mime: string
}>('__vitest_fileInfo', file, 'base64')

const fileInstance = fetch(`data:${mime};base64,${base64}`)
.then(r => r.blob())
.then(blob => new File([blob], basename, { type: mime }))
return fileInstance
})
const uploadFiles = await Promise.all(uploadPromise)
return userEvent.upload(toElement(element) as HTMLElement, uploadFiles)
},

async fill(element: Element | Locator, text: string) {
await userEvent.clear(toElement(element))
return userEvent.type(toElement(element), text)
},
async dragAndDrop() {
throw new Error(`The "preview" provider doesn't support 'userEvent.dragAndDrop'`)
},

async type(element: Element | Locator, text: string, options: UserEventTypeOptions = {}) {
await userEvent.type(toElement(element), text, options)
},
async tab(options: UserEventTabOptions = {}) {
await userEvent.tab(options)
},
async keyboard(text: string) {
await userEvent.keyboard(text)
},
}

for (const [name, fn] of Object.entries(vitestUserEvent)) {
if (name !== 'setup') {
(vitestUserEvent as any)[name] = function (this: any, ...args: any[]) {
return ensureAwaited(() => fn.apply(this, args))
}
}
}

return vitestUserEvent
}

export function cdp() {
return getBrowserState().cdp!
}
Expand Down
55 changes: 11 additions & 44 deletions packages/browser/src/client/tester/locators/preview.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { userEvent } from '@testing-library/user-event'
import { page, server } from '@vitest/browser/context'
import { page, server, userEvent } from '@vitest/browser/context'
import {
getByAltTextSelector,
getByLabelSelector,
Expand All @@ -9,7 +8,7 @@ import {
getByTextSelector,
getByTitleSelector,
} from 'ivya'
import { convertElementToCssSelector, ensureAwaited } from '../../utils'
import { convertElementToCssSelector } from '../../utils'
import { getElementError } from '../public-utils'
import { Locator, selectorEngine } from './index'

Expand Down Expand Up @@ -58,71 +57,39 @@ class PreviewLocator extends Locator {
}

click(): Promise<void> {
return ensureAwaited(() => userEvent.click(this.element()))
return userEvent.click(this.element())
}

dblClick(): Promise<void> {
return ensureAwaited(() => userEvent.dblClick(this.element()))
return userEvent.dblClick(this.element())
}

tripleClick(): Promise<void> {
return ensureAwaited(() => userEvent.tripleClick(this.element()))
return userEvent.tripleClick(this.element())
}

hover(): Promise<void> {
return ensureAwaited(() => userEvent.hover(this.element()))
return userEvent.hover(this.element())
}

unhover(): Promise<void> {
return ensureAwaited(() => userEvent.unhover(this.element()))
return userEvent.unhover(this.element())
}

async fill(text: string): Promise<void> {
await this.clear()
return ensureAwaited(() => userEvent.type(this.element(), text))
return userEvent.fill(this.element(), text)
}

async upload(file: string | string[] | File | File[]): Promise<void> {
const uploadPromise = (Array.isArray(file) ? file : [file]).map(async (file) => {
if (typeof file !== 'string') {
return file
}

const { content: base64, basename, mime } = await this.triggerCommand<{
content: string
basename: string
mime: string
}>('__vitest_fileInfo', file, 'base64')

const fileInstance = fetch(`data:${mime};base64,${base64}`)
.then(r => r.blob())
.then(blob => new File([blob], basename, { type: mime }))
return fileInstance
})
const uploadFiles = await Promise.all(uploadPromise)
return ensureAwaited(() => userEvent.upload(this.element() as HTMLElement, uploadFiles))
return userEvent.upload(this.element(), file)
}

selectOptions(options_: string | string[] | HTMLElement | HTMLElement[] | Locator | Locator[]): Promise<void> {
const options = (Array.isArray(options_) ? options_ : [options_]).map((option) => {
if (typeof option !== 'string' && 'element' in option) {
return option.element() as HTMLElement
}
return option
})
return ensureAwaited(() => userEvent.selectOptions(this.element(), options as string[] | HTMLElement[]))
}

async dropTo(): Promise<void> {
throw new Error('The "preview" provider doesn\'t support `dropTo` method.')
return userEvent.selectOptions(this.element(), options_)
}

clear(): Promise<void> {
return ensureAwaited(() => userEvent.clear(this.element()))
}

async screenshot(): Promise<never> {
throw new Error('The "preview" provider doesn\'t support `screenshot` method.')
return userEvent.clear(this.element())
Comment on lines -124 to -125
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I removed these since Locator base class implementation already handles errors for unsupported __vitest_xxx commands.

}

protected locator(selector: string) {
Expand Down
19 changes: 18 additions & 1 deletion test/browser/fixtures/user-event/keyboard.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { expect, test } from 'vitest'
import { userEvent, page, server } from '@vitest/browser/context'
import { userEvent, page } from '@vitest/browser/context'

test('non US keys', async () => {
document.body.innerHTML = `
Expand Down Expand Up @@ -34,3 +34,20 @@ test('non US keys', async () => {
console.error(e)
}
})

test('click with modifier', async () => {
document.body.innerHTML = `
<div id="test">test shift and click</div>
`
const el = document.getElementById("test")
el.addEventListener("pointerup", (e) => {
if (e.shiftKey && e.type === 'pointerup') {
el.textContent += " [ok]"
}
});

await userEvent.keyboard('{Shift>}')
await userEvent.click(el)
await userEvent.keyboard('{/Shift}')
await expect.poll(() => el.textContent).toContain("[ok]")
})
Loading