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

feat(browser): add pointer to interactive api #6026

Draft
wants to merge 5 commits into
base: main
Choose a base branch
from
Draft
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
4 changes: 4 additions & 0 deletions packages/browser/context.d.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import type { ResolvedConfig } from 'vitest'
import type { PointerInput } from '@testing-library/user-event/dist/types/pointer'

export type { PointerInput }

export type BufferEncoding =
| 'ascii'
Expand Down Expand Up @@ -169,6 +172,7 @@ export interface UserEvent {
* @see {@link https://webdriver.io/docs/api/element/dragAndDrop/} WebdriverIO API
*/
dragAndDrop: (source: Element, target: Element, options?: UserEventDragAndDropOptions) => Promise<void>
pointer: (input: PointerInput) => Promise<void>
}

export interface UserEventFillOptions {}
Expand Down
32 changes: 31 additions & 1 deletion packages/browser/src/client/tester/context.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { Task, WorkerGlobalState } from 'vitest'
import type { BrowserPage, UserEvent, UserEventClickOptions, UserEventTabOptions, UserEventTypeOptions } from '../../../context'
import type { BrowserPage, PointerInput, UserEvent, UserEventClickOptions, UserEventTabOptions, UserEventTypeOptions } from '../../../context'
import type { BrowserRunnerState } from '../utils'
import type { BrowserRPC } from '../client'

Expand Down Expand Up @@ -130,6 +130,36 @@ export const userEvent: UserEvent = {
const css = convertElementToCssSelector(element.ownerDocument.body)
return triggerCommand('__vitest_hover', css)
},
pointer(input: PointerInput) {
const inputs = (Array.isArray(input) ? input : [input]).map((i) => {
if (typeof i === 'string') {
return i
}

const { target, node, ...rest } = i

if (!target && !node) {
return i
}

if (target && !node) {
return { target: convertElementToCssSelector(target), ...rest }
}

if (!target && node) {
return { node: convertElementToCssSelector(node), ...rest }
}

return {
target: convertElementToCssSelector(target),
node: convertElementToCssSelector(node),
...rest,
}
})

// todo: add options?
return triggerCommand('__vitest_pointer', inputs)
},

// non userEvent events, but still useful
fill(element: Element, text: string, options) {
Expand Down
2 changes: 2 additions & 0 deletions packages/browser/src/node/commands/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { tab } from './tab'
import { keyboard } from './keyboard'
import { dragAndDrop } from './dragAndDrop'
import { hover } from './hover'
import { pointer } from './pointer'
import {
readFile,
removeFile,
Expand All @@ -30,4 +31,5 @@ export default {
__vitest_selectOptions: selectOptions,
__vitest_dragAndDrop: dragAndDrop,
__vitest_hover: hover,
__vitest_pointer: pointer,
}
56 changes: 56 additions & 0 deletions packages/browser/src/node/commands/pointer-helper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { parseKeyDef } from '@testing-library/user-event/dist/esm/pointer/parseKeyDef.js'
import { defaultKeyMap } from '@testing-library/user-event/dist/esm/pointer/keyMap.js'

// @ts-expect-error no types
import type { PointerCoords } from '@testing-library/user-event/dist/types/event'
// @ts-expect-error no types
import type { pointerKey } from '@testing-library/user-event/dist/types/system/pointer'

// todo: try to add proper types to avoid duplicating
export type PointerActionInput = string | ({
keys: string
} & PointerActionPosition) | PointerAction
export type PointerInput = PointerActionInput[]
export type PointerAction = PointerPressAction | PointerMoveAction
export interface PointerActionPosition {
target?: string
coords?: PointerCoords
node?: string
/**
* If `node` is set, this is the DOM offset.
* Otherwise this is the `textContent`/`value` offset on the `target`.
*/
offset?: number
}
export interface PointerPressAction extends PointerActionPosition {
keyDef: pointerKey
releasePrevious: boolean
releaseSelf: boolean
}
export interface PointerMoveAction extends PointerActionPosition {
pointerName?: string
}

// https://github.com/testing-library/user-event/blob/main/src/pointer/index.ts
export function* collectActions(input: PointerInput) {
// collect actions
for (let i = 0; i < input.length; i++) {
const actionInput = input[i]
if (typeof actionInput === 'string') {
for (const i of parseKeyDef(defaultKeyMap, actionInput)) {
yield i
}
}
else if ('keys' in actionInput) {
for (const i of parseKeyDef(defaultKeyMap, actionInput.keys)) {
yield {
...actionInput,
...i,
}
}
}
else {
yield actionInput
}
}
}
35 changes: 35 additions & 0 deletions packages/browser/src/node/commands/pointer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import type { UserEvent } from '../../../context'
import { PlaywrightBrowserProvider } from '../providers/playwright'
import { WebdriverBrowserProvider } from '../providers/webdriver'
import type { UserEventCommand } from './utils'
import type { PointerInput } from './pointer-helper'

export const pointer: UserEventCommand<UserEvent['pointer']> = async (
context,
input: PointerInput,
) => {
const provider = context.provider
// todo: cleanup
if (!input.length || provider instanceof PlaywrightBrowserProvider) {
return
}

// const provider = context.provider
if (provider instanceof PlaywrightBrowserProvider) {
throw new TypeError(`Provider "${provider.name}" does not support pointer events`)
}
else if (provider instanceof WebdriverBrowserProvider) {
const browser = context.browser
await import('./webdriver-pointer').then(({ webdriverPointerImplementation }) => webdriverPointerImplementation(browser, input))
}
else {
throw new TypeError(`Provider "${provider.name}" does not support pointer events`)
}
}
/*
async function _playwrightPointerImplementation(
provider: PlaywrightBrowserProvider,
input: PointerInput,
) {
const actions = provider.browser
} */
120 changes: 120 additions & 0 deletions packages/browser/src/node/commands/webdriver-pointer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import type { PointerInput } from './pointer-helper'
import { collectActions } from './pointer-helper'

export async function webdriverPointerImplementation(
browser: WebdriverIO.Browser,
input: PointerInput,
) {
const targets = new Map<string, WebdriverIO.Element>()
for await (const action of collectWDIOActions(browser, targets, input)) {
await action.perform()
}
}

// hack to infer types
function createWDIOPointerAction(touch: boolean, browser: WebdriverIO.Browser) {
return browser.action('pointer', { parameters: { pointerType: touch ? 'touch' : 'mouse' } })
}

async function* collectWDIOActions(
browser: WebdriverIO.Browser,
targets: Map<string, WebdriverIO.Element>,
input: PointerInput,
) {
let lastAction: {
touch: boolean
action: ReturnType<typeof createWDIOPointerAction>
} = undefined!
for (const action of collectActions(input)) {
if ('target' in action) {
const target = action.target
if (target && !targets.has(target)) {
targets.set(target, await browser.$(target))
}
}
if ('node' in action) {
const node = action.node
if (node && !targets.has(node)) {
targets.set(node, await browser.$(node))
}
}
if ('keyDef' in action) {
if (lastAction) {
if (lastAction.touch) {
yield lastAction.action
lastAction = {
touch: false,
action: createWDIOPointerAction(false, browser),
}
}
}
else {
lastAction = {
touch: false,
action: createWDIOPointerAction(true, browser),
}
}
if (action.releasePrevious) {
lastAction.action = lastAction.action.up().pause(50)
}
lastAction.action = lastAction.action.down()
if (action.releaseSelf) {
lastAction.action = lastAction.action.up().pause(50)
}
}
else {
if (action.pointerName) {
if (lastAction) {
if (!lastAction.touch) {
yield lastAction.action
lastAction = {
touch: true,
action: createWDIOPointerAction(true, browser),
}
}
}
else {
lastAction = {
touch: true,
action: createWDIOPointerAction(true, browser),
}
}
const params: any = {}
const { x, y } = action.coords ?? {}
if (x !== undefined && y !== undefined) {
params.x = x
params.y = y
}
if (action.target) {
const target = targets.get(action.target)
if (target) {
params.origin = target
}
}
params.button = action.pointerName === 'left' ? 0 : action.pointerName === 'right' ? 2 : 1
lastAction.action = lastAction.action.move(params)
}
else {
if (lastAction) {
if (lastAction.touch) {
yield lastAction.action
lastAction = {
touch: false,
action: createWDIOPointerAction(false, browser),
}
}
}
else {
lastAction = {
touch: false,
action: createWDIOPointerAction(false, browser),
}
}
// todo: check also for coordinates?
lastAction.action = lastAction.action.move({ origin: targets.get(action.target!)! })
}
}
}

yield lastAction.action
}
Loading
Loading