[READ-ONLY] Mirror of https://github.com/vitest-dev/vitest. Next generation testing framework powered by Vite. vitest.dev
test testing-tools vite
12

Configure Feed

Select the types of activity you want to include in your feed.

feat(browser): add `pointer` to interactive api

userquin (Jul 2, 2024, 11:59 PM +0200) 035e803c f645e48c

+413 -1
+4
packages/browser/context.d.ts
··· 1 1 import type { ResolvedConfig } from 'vitest' 2 + import type { PointerInput } from '@testing-library/user-event/dist/types/pointer' 3 + 4 + export type { PointerInput } 2 5 3 6 export type BufferEncoding = 4 7 | 'ascii' ··· 169 172 * @see {@link https://webdriver.io/docs/api/element/dragAndDrop/} WebdriverIO API 170 173 */ 171 174 dragAndDrop: (source: Element, target: Element, options?: UserEventDragAndDropOptions) => Promise<void> 175 + pointer: (input: PointerInput) => Promise<void> 172 176 } 173 177 174 178 export interface UserEventFillOptions {}
+172
test/browser/test/userEvent.test.ts
··· 600 600 }) 601 601 }) 602 602 }) 603 + 604 + describe('userEvent.pointer', () => { 605 + test('correctly clicks a button', async () => { 606 + const button = document.createElement('button') 607 + button.textContent = 'Click me' 608 + document.body.appendChild(button) 609 + const onClick = vi.fn() 610 + const dblClick = vi.fn() 611 + button.addEventListener('click', onClick) 612 + 613 + await userEvent.pointer([{ target: button }, { keys: '[MouseLeft]', target: button }]) 614 + 615 + // todo: cleanup 616 + if (server.provider === 'playwright') { 617 + expect(onClick).not.toHaveBeenCalled() 618 + expect(dblClick).not.toHaveBeenCalled() 619 + } 620 + else { 621 + expect(onClick).toHaveBeenCalled() 622 + expect(dblClick).not.toHaveBeenCalled() 623 + } 624 + }) 625 + 626 + test('correctly doesn\'t click on a disabled button', async () => { 627 + const button = document.createElement('button') 628 + button.textContent = 'Click me' 629 + button.disabled = true 630 + document.body.appendChild(button) 631 + const onClick = vi.fn() 632 + button.addEventListener('click', onClick) 633 + 634 + await userEvent.pointer([{ target: button }, { keys: '[MouseLeft]', target: button }]) 635 + 636 + expect(onClick).not.toHaveBeenCalled() 637 + }) 638 + test('correctly double clicks a button', async () => { 639 + const button = document.createElement('button') 640 + button.textContent = 'Click me' 641 + document.body.appendChild(button) 642 + const onClick = vi.fn() 643 + const dblClick = vi.fn() 644 + button.addEventListener('click', onClick) 645 + button.addEventListener('dblclick', dblClick) 646 + 647 + await userEvent.pointer([ 648 + { target: button }, 649 + { keys: '[MouseLeft][MouseLeft]', target: button }, 650 + ]) 651 + 652 + // todo: cleanup 653 + if (server.provider === 'playwright') { 654 + expect(onClick).not.toHaveBeenCalled() 655 + expect(dblClick).not.toHaveBeenCalled() 656 + } 657 + else { 658 + expect(onClick).toHaveBeenCalledTimes(2) 659 + expect(dblClick).toHaveBeenCalledTimes(1) 660 + } 661 + }) 662 + 663 + test('correctly doesn\'t double click on a disabled button', async () => { 664 + const button = document.createElement('button') 665 + button.textContent = 'Click me' 666 + button.disabled = true 667 + document.body.appendChild(button) 668 + const onClick = vi.fn() 669 + const dblClick = vi.fn() 670 + button.addEventListener('click', onClick) 671 + button.addEventListener('dblclick', dblClick) 672 + 673 + await userEvent.pointer([ 674 + { target: button }, 675 + { keys: '[MouseLeft][MouseLeft]', target: button }, 676 + ]) 677 + 678 + expect(onClick).not.toHaveBeenCalled() 679 + expect(dblClick).not.toHaveBeenCalled() 680 + }) 681 + test('correctly triple clicks a button', async () => { 682 + const button = document.createElement('button') 683 + button.textContent = 'Click me' 684 + document.body.appendChild(button) 685 + const onClick = vi.fn() 686 + const dblClick = vi.fn() 687 + const tripleClick = vi.fn() 688 + button.addEventListener('click', onClick) 689 + button.addEventListener('dblclick', dblClick) 690 + button.addEventListener('click', tripleClick) 691 + 692 + await userEvent.pointer([ 693 + { target: button }, 694 + { keys: '[MouseLeft][MouseLeft][MouseLeft]', target: button }, 695 + ]) 696 + 697 + // todo: cleanup 698 + if (server.provider === 'playwright') { 699 + expect(onClick).not.toHaveBeenCalled() 700 + expect(dblClick).not.toHaveBeenCalled() 701 + expect(tripleClick).not.toHaveBeenCalled() 702 + } 703 + else { 704 + expect(onClick).toHaveBeenCalledTimes(3) 705 + expect(dblClick).toHaveBeenCalledTimes(1) 706 + expect(tripleClick).toHaveBeenCalledTimes(3) 707 + expect(tripleClick.mock.calls.length).toBe(3) 708 + expect(tripleClick.mock.calls 709 + .map(c => c[0] as MouseEvent) 710 + .filter(c => c.detail === 3)).toHaveLength(1) 711 + } 712 + }) 713 + 714 + test('correctly doesn\'t triple click on a disabled button', async () => { 715 + const button = document.createElement('button') 716 + button.textContent = 'Click me' 717 + button.disabled = true 718 + document.body.appendChild(button) 719 + const onClick = vi.fn() 720 + const dblClick = vi.fn() 721 + const tripleClick = vi.fn() 722 + button.addEventListener('click', onClick) 723 + button.addEventListener('dblclick', dblClick) 724 + button.addEventListener('click', tripleClick) 725 + 726 + await userEvent.pointer([ 727 + { target: button }, 728 + { keys: '[MouseLeft][MouseLeft][MouseLeft]', target: button }, 729 + ]) 730 + 731 + expect(onClick).not.toHaveBeenCalled() 732 + expect(dblClick).not.toHaveBeenCalled() 733 + expect(tripleClick).not.toHaveBeenCalled() 734 + }) 735 + test('hover works correctly', async () => { 736 + const target = document.createElement('div') 737 + target.style.width = '100px' 738 + target.style.height = '100px' 739 + 740 + let mouseEntered = false 741 + let pointerEntered = false 742 + target.addEventListener('mouseover', () => { 743 + mouseEntered = true 744 + }) 745 + target.addEventListener('pointerenter', () => { 746 + pointerEntered = true 747 + }) 748 + target.addEventListener('pointerleave', () => { 749 + pointerEntered = false 750 + }) 751 + target.addEventListener('mouseout', () => { 752 + mouseEntered = false 753 + }) 754 + 755 + document.body.appendChild(target) 756 + 757 + await userEvent.pointer({ target }) 758 + 759 + // todo: cleanup 760 + if (server.provider === 'playwright') { 761 + expect(pointerEntered).toBe(false) 762 + expect(mouseEntered).toBe(false) 763 + } 764 + else { 765 + expect(pointerEntered).toBe(true) 766 + expect(mouseEntered).toBe(true) 767 + } 768 + 769 + await userEvent.pointer({ target: target.ownerDocument.body }) 770 + 771 + expect(pointerEntered).toBe(false) 772 + expect(mouseEntered).toBe(false) 773 + }) 774 + })
+31 -1
packages/browser/src/client/tester/context.ts
··· 1 1 import type { Task, WorkerGlobalState } from 'vitest' 2 - import type { BrowserPage, UserEvent, UserEventClickOptions, UserEventTabOptions, UserEventTypeOptions } from '../../../context' 2 + import type { BrowserPage, PointerInput, UserEvent, UserEventClickOptions, UserEventTabOptions, UserEventTypeOptions } from '../../../context' 3 3 import type { BrowserRunnerState } from '../utils' 4 4 import type { BrowserRPC } from '../client' 5 5 ··· 116 116 const sourceXpath = convertElementToXPath(source) 117 117 const targetXpath = convertElementToXPath(target) 118 118 return triggerCommand('__vitest_dragAndDrop', sourceXpath, targetXpath, options) 119 + }, 120 + pointer(input: PointerInput) { 121 + const inputs = (Array.isArray(input) ? input : [input]).map((i) => { 122 + if (typeof i === 'string') { 123 + return i 124 + } 125 + 126 + const { target, node, ...rest } = i 127 + 128 + if (!target && !node) { 129 + return i 130 + } 131 + 132 + if (target && !node) { 133 + return { target: convertElementToXPath(target), ...rest } 134 + } 135 + 136 + if (!target && node) { 137 + return { node: convertElementToXPath(node), ...rest } 138 + } 139 + 140 + return { 141 + target: convertElementToXPath(target), 142 + node: convertElementToXPath(node), 143 + ...rest, 144 + } 145 + }) 146 + 147 + // todo: add options? 148 + return triggerCommand('__vitest_pointer', inputs) 119 149 }, 120 150 } 121 151
+2
packages/browser/src/node/commands/index.ts
··· 7 7 import { keyboard } from './keyboard' 8 8 import { dragAndDrop } from './dragAndDrop' 9 9 import { hover } from './hover' 10 + import { pointer } from './pointer' 10 11 import { 11 12 readFile, 12 13 removeFile, ··· 30 31 __vitest_selectOptions: selectOptions, 31 32 __vitest_dragAndDrop: dragAndDrop, 32 33 __vitest_hover: hover, 34 + __vitest_pointer: pointer, 33 35 }
+204
packages/browser/src/node/commands/pointer.ts
··· 1 + import { parseKeyDef } from '@testing-library/user-event/dist/esm/pointer/parseKeyDef.js' 2 + import { defaultKeyMap } from '@testing-library/user-event/dist/esm/pointer/keyMap.js' 3 + import type { PointerCoords } from '@testing-library/user-event/dist/types/event' 4 + import type { pointerKey } from '@testing-library/user-event/dist/types/system/pointer' 5 + import type { UserEvent } from '../../../context' 6 + import { PlaywrightBrowserProvider } from '../providers/playwright' 7 + import { WebdriverBrowserProvider } from '../providers/webdriver' 8 + import type { UserEventCommand } from './utils' 9 + 10 + // todo: try to add proper types to avoid duplicating 11 + type PointerActionInput = string | ({ 12 + keys: string 13 + } & PointerActionPosition) | PointerAction 14 + type PointerInput = PointerActionInput[] 15 + type PointerAction = PointerPressAction | PointerMoveAction 16 + interface PointerActionPosition { 17 + target?: string 18 + coords?: PointerCoords 19 + node?: string 20 + /** 21 + * If `node` is set, this is the DOM offset. 22 + * Otherwise this is the `textContent`/`value` offset on the `target`. 23 + */ 24 + offset?: number 25 + } 26 + interface PointerPressAction extends PointerActionPosition { 27 + keyDef: pointerKey 28 + releasePrevious: boolean 29 + releaseSelf: boolean 30 + } 31 + interface PointerMoveAction extends PointerActionPosition { 32 + pointerName?: string 33 + } 34 + 35 + export const pointer: UserEventCommand<UserEvent['pointer']> = async ( 36 + context, 37 + input: PointerInput, 38 + ) => { 39 + const provider = context.provider 40 + // todo: cleanup 41 + if (!input.length || provider instanceof PlaywrightBrowserProvider) { 42 + return 43 + } 44 + 45 + // const provider = context.provider 46 + if (provider instanceof PlaywrightBrowserProvider) { 47 + throw new TypeError(`Provider "${provider.name}" does not support pointer events`) 48 + } 49 + else if (provider instanceof WebdriverBrowserProvider) { 50 + await webdriverioPointerImplementation(context.browser, input) 51 + } 52 + else { 53 + throw new TypeError(`Provider "${provider.name}" does not support pointer events`) 54 + } 55 + } 56 + /* 57 + async function _playwrightPointerImplementation( 58 + provider: PlaywrightBrowserProvider, 59 + input: PointerInput, 60 + ) { 61 + const actions = provider.browser 62 + } */ 63 + async function webdriverioPointerImplementation( 64 + browser: WebdriverIO.Browser, 65 + input: PointerInput, 66 + ) { 67 + const actions = collectActions(input) 68 + const targets = new Map<string, WebdriverIO.Element>() 69 + for await (const action of executeWDIOActions(browser, targets, actions)) { 70 + await action.perform() 71 + } 72 + } 73 + 74 + // https://github.com/testing-library/user-event/blob/main/src/pointer/index.ts 75 + function collectActions(input: PointerInput) { 76 + const actions: PointerAction[] = [] 77 + // collect actions 78 + for (let i = 0; i < input.length; i++) { 79 + const actionInput = input[i] 80 + if (typeof actionInput === 'string') { 81 + actions.push(...parseKeyDef(defaultKeyMap, actionInput)) 82 + } 83 + else if ('keys' in actionInput) { 84 + actions.push( 85 + ...parseKeyDef(defaultKeyMap, actionInput.keys).map(i => ({ 86 + ...actionInput, 87 + ...i, 88 + })), 89 + ) 90 + } 91 + else { 92 + actions.push(actionInput) 93 + } 94 + } 95 + 96 + return actions 97 + } 98 + 99 + function createWDIOPointerAction(touch: boolean, browser: WebdriverIO.Browser) { 100 + return browser.action('pointer', { parameters: { pointerType: touch ? 'touch' : 'mouse' } }) 101 + } 102 + 103 + async function* executeWDIOActions( 104 + browser: WebdriverIO.Browser, 105 + targets: Map<string, WebdriverIO.Element>, 106 + actions: PointerAction[], 107 + ) { 108 + let lastAction: { 109 + touch: boolean 110 + action: ReturnType<typeof createWDIOPointerAction> 111 + } = undefined! 112 + for (const action of actions) { 113 + if ('target' in action) { 114 + const target = action.target 115 + if (target && !targets.has(target)) { 116 + targets.set(target, await browser.$(`//${target}`)) 117 + } 118 + } 119 + if ('node' in action) { 120 + const node = action.node 121 + if (node && !targets.has(node)) { 122 + targets.set(node, await browser.$(`//${node}`)) 123 + } 124 + } 125 + if ('keyDef' in action) { 126 + if (lastAction) { 127 + if (lastAction.touch) { 128 + yield lastAction.action 129 + lastAction = { 130 + touch: false, 131 + action: createWDIOPointerAction(false, browser), 132 + } 133 + } 134 + } 135 + else { 136 + lastAction = { 137 + touch: false, 138 + action: createWDIOPointerAction(true, browser), 139 + } 140 + } 141 + if (action.releasePrevious) { 142 + lastAction.action = lastAction.action.up().pause(50) 143 + } 144 + lastAction.action = lastAction.action.down() 145 + if (action.releaseSelf) { 146 + lastAction.action = lastAction.action.up().pause(50) 147 + } 148 + } 149 + else { 150 + if (action.pointerName) { 151 + if (lastAction) { 152 + if (!lastAction.touch) { 153 + yield lastAction.action 154 + lastAction = { 155 + touch: true, 156 + action: createWDIOPointerAction(true, browser), 157 + } 158 + } 159 + } 160 + else { 161 + lastAction = { 162 + touch: true, 163 + action: createWDIOPointerAction(true, browser), 164 + } 165 + } 166 + const params: any = {} 167 + const { x, y } = action.coords ?? {} 168 + if (x !== undefined && y !== undefined) { 169 + params.x = x 170 + params.y = y 171 + } 172 + if (action.target) { 173 + const target = targets.get(action.target) 174 + if (target) { 175 + params.origin = target 176 + } 177 + } 178 + params.button = action.pointerName === 'left' ? 0 : action.pointerName === 'right' ? 2 : 1 179 + lastAction.action = lastAction.action.move(params) 180 + } 181 + else { 182 + if (lastAction) { 183 + if (lastAction.touch) { 184 + yield lastAction.action 185 + lastAction = { 186 + touch: false, 187 + action: createWDIOPointerAction(false, browser), 188 + } 189 + } 190 + } 191 + else { 192 + lastAction = { 193 + touch: false, 194 + action: createWDIOPointerAction(false, browser), 195 + } 196 + } 197 + // todo: check also for coordinates 198 + lastAction.action = lastAction.action.move({ origin: targets.get(action.target!)! }) 199 + } 200 + } 201 + } 202 + 203 + yield lastAction.action 204 + }