Skip to content

Commit

Permalink
feat: browser userEvent proof of concept
Browse files Browse the repository at this point in the history
  • Loading branch information
sheremet-va committed May 25, 2024
1 parent a50330e commit 400986c
Show file tree
Hide file tree
Showing 7 changed files with 97 additions and 1 deletion.
6 changes: 6 additions & 0 deletions packages/browser/context.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ export interface BrowserCommands {
sendKeys: (payload: SendKeysPayload) => Promise<void>
}

export interface UserEvent {
click: (selector: Element, options?: any) => Promise<void>
}

type Platform =
| 'aix'
| 'android'
Expand Down Expand Up @@ -68,6 +72,8 @@ export const server: {
commands: BrowserCommands
}

export const userEvent: UserEvent

/**
* Available commands for the browser.
* A shortcut to `server.commands`.
Expand Down
1 change: 1 addition & 0 deletions packages/browser/src/client/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ function createIframe(container: HTMLDivElement, file: string) {
const iframe = document.createElement('iframe')
iframe.setAttribute('loading', 'eager')
iframe.setAttribute('src', `${url.pathname}__vitest_test__/__test__/${encodeURIComponent(file)}`)
iframe.setAttribute('id', 'vitest-tester-frame')
iframes.set(file, iframe)
container.appendChild(iframe)
return iframe
Expand Down
22 changes: 22 additions & 0 deletions packages/browser/src/node/commands/click.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import type { Page } from 'playwright'
import type { UserEvent } from '../../../context'
import type { UserEventCommand } from './utils'

// TODO: options
export const click: UserEventCommand<UserEvent['click']> = async (
{ provider },
element,
options,
) => {
if (provider.name === 'playwright') {
const page = (provider as any).page as Page
await page.frameLocator('#vitest-tester-frame').locator(element).click(options)
}
if (provider.name === 'webdriverio') {
// TODO: test
const page = (provider as any).browser as WebdriverIO.Browser
const frame = await page.findElement('css selector', '#vitest-tester-frame')
await page.switchToFrame(frame)
;(await page.$(element)).click(options)
}
}
2 changes: 2 additions & 0 deletions packages/browser/src/node/commands/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { click } from './click'
import {
readFile,
removeFile,
Expand All @@ -10,4 +11,5 @@ export default {
removeFile,
writeFile,
sendKeys,
__vitest_click: click,
}
10 changes: 10 additions & 0 deletions packages/browser/src/node/commands/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import type { BrowserCommand } from 'vitest/node'

export type UserEventCommand<T extends (...args: any) => any> = BrowserCommand<
ConvertUserEventParameters<Parameters<T>>
>

type ConvertElementToLocator<T> = T extends Element ? string : T
type ConvertUserEventParameters<T extends unknown[]> = {
[K in keyof T]: ConvertElementToLocator<T[K]>
}
40 changes: 39 additions & 1 deletion packages/browser/src/node/plugins/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,11 @@ function generateContextFile(project: WorkspaceProject) {
const filepathCode = '__vitest_worker__.filepath || __vitest_worker__.current?.file?.filepath || undefined'

const commandsCode = commands.map((command) => {
return ` ["${command}"]: (...args) => rpc().triggerCommand("${command}", ${filepathCode}, args),`
return ` ["${command}"]: (...args) => rpc().triggerCommand("${command}", filepath(), args),`
}).join('\n')

return `
const filepath = () => ${filepathCode}
const rpc = () => __vitest_worker__.rpc
export const server = {
Expand All @@ -53,7 +54,44 @@ export const commands = server.commands
export const page = {
get config() {
return __vitest_browser_runner__.config
},
}
export const userEvent = ${getUserEventScript(project)}
function convertElementToXPath(element) {
if (!element || !(element instanceof Element)) {
// TODO: better error message
throw new Error('Expected element to be an instance of Element')
}
return getPathTo(element)
}
function getPathTo(element) {
if (element.id !== '')
return \`id("\${element.id}")\`
if (element === document.body)
return element.tagName
let ix = 0
const siblings = element.parentNode.childNodes
for (let i = 0; i < siblings.length; i++) {
const sibling = siblings[i]
if (sibling === element)
return \`\${getPathTo(element.parentNode)}/\${element.tagName}[\${ix + 1}]\`
if (sibling.nodeType === 1 && sibling.tagName === element.tagName)
ix++
}
}
`
}

function getUserEventScript(project: WorkspaceProject) {
if (project.browserProvider?.name === 'none')
return `__vitest_user_event__`
return `{
async click(element, options) {
return rpc().triggerCommand('__vitest_click', filepath(), [\`xpath=\${convertElementToXPath(element)}\`, options]);
},
}`
}
17 changes: 17 additions & 0 deletions test/browser/test/click.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { userEvent } from '@vitest/browser/context'
import { test, vi } from 'vitest'

test('clicks on an element', async () => {
const button = document.createElement('button')
button.textContent = 'Hello World'
const fn = vi.fn()
button.onclick = () => {
fn()
}
document.body.appendChild(button)

await userEvent.click(button, {
timeout: 2000,
})
// expect(fn).toHaveBeenCalled()
})

0 comments on commit 400986c

Please sign in to comment.