[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): support clipboard api `userEvent.copy, cut, paste` (#6769)

authored by

Hiroshi Ogawa and committed by
GitHub
(Dec 23, 2024, 9:32 AM +0100) 843a621e de5ce3d9

+228 -6
+21
packages/browser/context.d.ts
··· 173 173 */ 174 174 upload: (element: Element | Locator, files: File | File[] | string | string[]) => Promise<void> 175 175 /** 176 + * Copies the selected content. 177 + * @see {@link https://playwright.dev/docs/api/class-keyboard} Playwright API 178 + * @see {@link https://webdriver.io/docs/api/browser/keys//} WebdriverIO API 179 + * @see {@link https://testing-library.com/docs/user-event/clipboard#copy} testing-library API 180 + */ 181 + copy: () => Promise<void> 182 + /** 183 + * Cuts the selected content. 184 + * @see {@link https://playwright.dev/docs/api/class-keyboard} Playwright API 185 + * @see {@link https://webdriver.io/docs/api/browser/keys//} WebdriverIO API 186 + * @see {@link https://testing-library.com/docs/user-event/clipboard#cut} testing-library API 187 + */ 188 + cut: () => Promise<void> 189 + /** 190 + * Pastes the copied or cut content. 191 + * @see {@link https://playwright.dev/docs/api/class-keyboard} Playwright API 192 + * @see {@link https://webdriver.io/docs/api/browser/keys//} WebdriverIO API 193 + * @see {@link https://testing-library.com/docs/user-event/clipboard#paste} testing-library API 194 + */ 195 + paste: () => Promise<void> 196 + /** 176 197 * Fills an input element with text. This will remove any existing text in the input before typing the new text. 177 198 * Uses provider's API under the hood. 178 199 * This API is faster than using `userEvent.type` or `userEvent.keyboard`, but it **doesn't support** [user-event `keyboard` syntax](https://testing-library.com/docs/user-event/keyboard) (e.g., `{Shift}`).
+78
docs/guide/browser/interactivity-api.md
··· 518 518 519 519 - [Playwright `frame.dragAndDrop` API](https://playwright.dev/docs/api/class-frame#frame-drag-and-drop) 520 520 - [WebdriverIO `element.dragAndDrop` API](https://webdriver.io/docs/api/element/dragAndDrop/) 521 + 522 + ## userEvent.copy 523 + 524 + ```ts 525 + function copy(): Promise<void> 526 + ``` 527 + 528 + Copy the selected text to the clipboard. 529 + 530 + ```js 531 + import { page, userEvent } from '@vitest/browser/context' 532 + 533 + test('copy and paste', async () => { 534 + // write to 'source' 535 + await userEvent.click(page.getByPlaceholder('source')) 536 + await userEvent.keyboard('hello') 537 + 538 + // select and copy 'source' 539 + await userEvent.dblClick(page.getByPlaceholder('source')) 540 + await userEvent.copy() 541 + 542 + // paste to 'target' 543 + await userEvent.click(page.getByPlaceholder('target')) 544 + await userEvent.paste() 545 + 546 + await expect.element(page.getByPlaceholder('source')).toHaveTextContent('hello') 547 + await expect.element(page.getByPlaceholder('target')).toHaveTextContent('hello') 548 + }) 549 + ``` 550 + 551 + References: 552 + 553 + - [testing-library `copy` API](https://testing-library.com/docs/user-event/convenience/#copy) 554 + 555 + ## userEvent.cut 556 + 557 + ```ts 558 + function cut(): Promise<void> 559 + ``` 560 + 561 + Cut the selected text to the clipboard. 562 + 563 + ```js 564 + import { page, userEvent } from '@vitest/browser/context' 565 + 566 + test('copy and paste', async () => { 567 + // write to 'source' 568 + await userEvent.click(page.getByPlaceholder('source')) 569 + await userEvent.keyboard('hello') 570 + 571 + // select and cut 'source' 572 + await userEvent.dblClick(page.getByPlaceholder('source')) 573 + await userEvent.cut() 574 + 575 + // paste to 'target' 576 + await userEvent.click(page.getByPlaceholder('target')) 577 + await userEvent.paste() 578 + 579 + await expect.element(page.getByPlaceholder('source')).toHaveTextContent('') 580 + await expect.element(page.getByPlaceholder('target')).toHaveTextContent('hello') 581 + }) 582 + ``` 583 + 584 + References: 585 + 586 + - [testing-library `cut` API](https://testing-library.com/docs/user-event/clipboard#cut) 587 + 588 + ## userEvent.paste 589 + 590 + ```ts 591 + function paste(): Promise<void> 592 + ``` 593 + 594 + Paste the text from the clipboard. See [`userEvent.copy`](#userevent-copy) and [`userEvent.cut`](#userevent-cut) for usage examples. 595 + 596 + References: 597 + 598 + - [testing-library `paste` API](https://testing-library.com/docs/user-event/clipboard#paste)
+3 -1
test/browser/specs/runner.test.ts
··· 141 141 }) 142 142 143 143 test('user-event', async () => { 144 - const { stdout } = await runBrowserTests({ 144 + const { stdout, stderr } = await runBrowserTests({ 145 145 root: './fixtures/user-event', 146 146 }) 147 + onTestFailed(() => console.error(stderr)) 147 148 instances.forEach(({ browser }) => { 148 149 expect(stdout).toReportPassedTest('cleanup-retry.test.ts', browser) 149 150 expect(stdout).toReportPassedTest('cleanup1.test.ts', browser) 150 151 expect(stdout).toReportPassedTest('cleanup2.test.ts', browser) 151 152 expect(stdout).toReportPassedTest('keyboard.test.ts', browser) 153 + expect(stdout).toReportPassedTest('clipboard.test.ts', browser) 152 154 }) 153 155 }) 154 156
+45
test/browser/fixtures/user-event/clipboard.test.ts
··· 1 + import { expect, test } from 'vitest'; 2 + import { page, userEvent } from '@vitest/browser/context'; 3 + 4 + test('clipboard', async () => { 5 + // make it smaller since webdriverio fails when scaled 6 + page.viewport(300, 300) 7 + 8 + document.body.innerHTML = ` 9 + <input placeholder="first" /> 10 + <input placeholder="second" /> 11 + <input placeholder="third" /> 12 + `; 13 + 14 + // write first "hello" and copy to clipboard 15 + await userEvent.click(page.getByPlaceholder('first')); 16 + await userEvent.keyboard('hello'); 17 + await userEvent.dblClick(page.getByPlaceholder('first')); 18 + await userEvent.copy(); 19 + 20 + // paste into second 21 + await userEvent.click(page.getByPlaceholder('second')); 22 + await userEvent.paste(); 23 + 24 + // append first "world" and cut 25 + await userEvent.click(page.getByPlaceholder('first')); 26 + await userEvent.keyboard('world'); 27 + await userEvent.dblClick(page.getByPlaceholder('first')); 28 + await userEvent.cut(); 29 + 30 + // paste it to third 31 + await userEvent.click(page.getByPlaceholder('third')); 32 + await userEvent.paste(); 33 + 34 + expect([ 35 + (page.getByPlaceholder('first').element() as any).value, 36 + (page.getByPlaceholder('second').element() as any).value, 37 + (page.getByPlaceholder('third').element() as any).value, 38 + ]).toMatchInlineSnapshot(` 39 + [ 40 + "", 41 + "hello", 42 + "helloworld", 43 + ] 44 + `) 45 + });
+49 -1
test/browser/fixtures/user-event/keyboard.test.ts
··· 1 1 import { expect, test } from 'vitest' 2 - import { userEvent, page } from '@vitest/browser/context' 2 + import { userEvent, page, server } from '@vitest/browser/context' 3 3 4 4 test('non US keys', async () => { 5 5 document.body.innerHTML = ` ··· 50 50 await userEvent.click(el) 51 51 await userEvent.keyboard('{/Shift}') 52 52 await expect.poll(() => el.textContent).toContain("[ok]") 53 + }) 54 + 55 + // TODO: https://github.com/vitest-dev/vitest/issues/7118 56 + // https://testing-library.com/docs/user-event/keyboard 57 + // https://github.com/testing-library/user-event/blob/main/src/keyboard/keyMap.ts 58 + // https://playwright.dev/docs/api/class-keyboard 59 + // https://webdriver.io/docs/api/browser/keys/ 60 + test('special keys', async () => { 61 + async function testKeyboard(text: string) { 62 + let data: any; 63 + function handler(e: KeyboardEvent) { 64 + data = `${e.key}|${e.code}|${e.location}`; 65 + } 66 + document.addEventListener('keydown', handler) 67 + try { 68 + await userEvent.keyboard(text) 69 + } catch(e) { 70 + return 'ERROR'; 71 + } finally { 72 + document.removeEventListener('keydown', handler) 73 + } 74 + return data 75 + } 76 + 77 + if (server.provider === 'playwright') { 78 + expect(await testKeyboard('{Shift}')).toMatchInlineSnapshot(`"Shift|ShiftLeft|1"`); 79 + expect(await testKeyboard('{ShiftLeft}')).toMatchInlineSnapshot(`"Shift|ShiftLeft|1"`); 80 + expect(await testKeyboard('{ShiftRight}')).toMatchInlineSnapshot(`"Shift|ShiftRight|2"`); 81 + expect(await testKeyboard('[Shift]')).toMatchInlineSnapshot(`undefined`); 82 + expect(await testKeyboard('[ShiftLeft]')).toMatchInlineSnapshot(`"Shift|ShiftLeft|1"`); 83 + expect(await testKeyboard('[ShiftRight]')).toMatchInlineSnapshot(`"Shift|ShiftLeft|1"`); 84 + } 85 + if (server.provider === 'webdriverio') { 86 + expect(await testKeyboard('{Shift}')).toMatchInlineSnapshot(`"Shift|ShiftLeft|1"`); 87 + expect(await testKeyboard('{ShiftLeft}')).toMatchInlineSnapshot(`"ERROR"`); 88 + expect(await testKeyboard('{ShiftRight}')).toMatchInlineSnapshot(`"ERROR"`); 89 + expect(await testKeyboard('[Shift]')).toMatchInlineSnapshot(`"ERROR"`); 90 + expect(await testKeyboard('[ShiftLeft]')).toMatchInlineSnapshot(`"Shift|ShiftLeft|1"`); 91 + expect(await testKeyboard('[ShiftRight]')).toMatchInlineSnapshot(`"Shift|ShiftLeft|1"`); 92 + } 93 + if (server.provider === 'preview') { 94 + expect(await testKeyboard('{Shift}')).toMatchInlineSnapshot(`"Shift|ShiftLeft|0"`); 95 + expect(await testKeyboard('{ShiftLeft}')).toMatchInlineSnapshot(`"ShiftLeft|Unknown|0"`); 96 + expect(await testKeyboard('{ShiftRight}')).toMatchInlineSnapshot(`"ShiftRight|Unknown|0"`); 97 + expect(await testKeyboard('[Shift]')).toMatchInlineSnapshot(`"Unknown|Shift|0"`); 98 + expect(await testKeyboard('[ShiftLeft]')).toMatchInlineSnapshot(`"Shift|ShiftLeft|0"`); 99 + expect(await testKeyboard('[ShiftRight]')).toMatchInlineSnapshot(`"Shift|ShiftRight|0"`); 100 + } 53 101 })
+30 -1
packages/browser/src/client/tester/context.ts
··· 38 38 unreleased: [] as string[], 39 39 } 40 40 41 - return { 41 + // https://playwright.dev/docs/api/class-keyboard 42 + // https://webdriver.io/docs/api/browser/keys/ 43 + const modifier = provider === `playwright` 44 + ? 'ControlOrMeta' 45 + : provider === 'webdriverio' 46 + ? 'Ctrl' 47 + : 'Control' 48 + 49 + const userEvent: UserEvent = { 42 50 setup() { 43 51 return createUserEvent() 44 52 }, ··· 111 119 keyboard.unreleased = unreleased 112 120 }) 113 121 }, 122 + async copy() { 123 + await userEvent.keyboard(`{${modifier}>}{c}{/${modifier}}`) 124 + }, 125 + async cut() { 126 + await userEvent.keyboard(`{${modifier}>}{x}{/${modifier}}`) 127 + }, 128 + async paste() { 129 + await userEvent.keyboard(`{${modifier}>}{v}{/${modifier}}`) 130 + }, 114 131 } 132 + return userEvent 115 133 } 116 134 117 135 function createPreviewUserEvent(userEventBase: TestingLibraryUserEvent, options: TestingLibraryOptions): UserEvent { 118 136 let userEvent = userEventBase.setup(options) 137 + let clipboardData: DataTransfer | undefined 119 138 120 139 function toElement(element: Element | Locator) { 121 140 return element instanceof Element ? element : element.element() ··· 195 214 }, 196 215 async keyboard(text: string) { 197 216 await userEvent.keyboard(text) 217 + }, 218 + 219 + async copy() { 220 + clipboardData = await userEvent.copy() 221 + }, 222 + async cut() { 223 + clipboardData = await userEvent.cut() 224 + }, 225 + async paste() { 226 + await userEvent.paste(clipboardData) 198 227 }, 199 228 } 200 229
+2 -3
packages/browser/src/node/commands/keyboard.ts
··· 74 74 75 75 // fallback to insertText for non US key 76 76 // https://github.com/microsoft/playwright/blob/50775698ae13642742f2a1e8983d1d686d7f192d/packages/playwright-core/src/server/input.ts#L95 77 - const VALID_KEYS = new Set(['Escape', 'F1', 'F2', 'F3', 'F4', 'F5', 'F6', 'F7', 'F8', 'F9', 'F10', 'F11', 'F12', 'Backquote', '`', '~', 'Digit1', '1', '!', 'Digit2', '2', '@', 'Digit3', '3', '#', 'Digit4', '4', '$', 'Digit5', '5', '%', 'Digit6', '6', '^', 'Digit7', '7', '&', 'Digit8', '8', '*', 'Digit9', '9', '(', 'Digit0', '0', ')', 'Minus', '-', '_', 'Equal', '=', '+', 'Backslash', '\\', '|', 'Backspace', 'Tab', 'KeyQ', 'q', 'Q', 'KeyW', 'w', 'W', 'KeyE', 'e', 'E', 'KeyR', 'r', 'R', 'KeyT', 't', 'T', 'KeyY', 'y', 'Y', 'KeyU', 'u', 'U', 'KeyI', 'i', 'I', 'KeyO', 'o', 'O', 'KeyP', 'p', 'P', 'BracketLeft', '[', '{', 'BracketRight', ']', '}', 'CapsLock', 'KeyA', 'a', 'A', 'KeyS', 's', 'S', 'KeyD', 'd', 'D', 'KeyF', 'f', 'F', 'KeyG', 'g', 'G', 'KeyH', 'h', 'H', 'KeyJ', 'j', 'J', 'KeyK', 'k', 'K', 'KeyL', 'l', 'L', 'Semicolon', ';', ':', 'Quote', '\'', '"', 'Enter', '\n', '\r', 'ShiftLeft', 'Shift', 'KeyZ', 'z', 'Z', 'KeyX', 'x', 'X', 'KeyC', 'c', 'C', 'KeyV', 'v', 'V', 'KeyB', 'b', 'B', 'KeyN', 'n', 'N', 'KeyM', 'm', 'M', 'Comma', ',', '<', 'Period', '.', '>', 'Slash', '/', '?', 'ShiftRight', 'ControlLeft', 'Control', 'MetaLeft', 'Meta', 'AltLeft', 'Alt', 'Space', ' ', 'AltRight', 'AltGraph', 'MetaRight', 'ContextMenu', 'ControlRight', 'PrintScreen', 'ScrollLock', 'Pause', 'PageUp', 'PageDown', 'Insert', 'Delete', 'Home', 'End', 'ArrowLeft', 'ArrowUp', 'ArrowRight', 'ArrowDown', 'NumLock', 'NumpadDivide', 'NumpadMultiply', 'NumpadSubtract', 'Numpad7', 'Numpad8', 'Numpad9', 'Numpad4', 'Numpad5', 'Numpad6', 'NumpadAdd', 'Numpad1', 'Numpad2', 'Numpad3', 'Numpad0', 'NumpadDecimal', 'NumpadEnter']) 77 + const VALID_KEYS = new Set(['Escape', 'F1', 'F2', 'F3', 'F4', 'F5', 'F6', 'F7', 'F8', 'F9', 'F10', 'F11', 'F12', 'Backquote', '`', '~', 'Digit1', '1', '!', 'Digit2', '2', '@', 'Digit3', '3', '#', 'Digit4', '4', '$', 'Digit5', '5', '%', 'Digit6', '6', '^', 'Digit7', '7', '&', 'Digit8', '8', '*', 'Digit9', '9', '(', 'Digit0', '0', ')', 'Minus', '-', '_', 'Equal', '=', '+', 'Backslash', '\\', '|', 'Backspace', 'Tab', 'KeyQ', 'q', 'Q', 'KeyW', 'w', 'W', 'KeyE', 'e', 'E', 'KeyR', 'r', 'R', 'KeyT', 't', 'T', 'KeyY', 'y', 'Y', 'KeyU', 'u', 'U', 'KeyI', 'i', 'I', 'KeyO', 'o', 'O', 'KeyP', 'p', 'P', 'BracketLeft', '[', '{', 'BracketRight', ']', '}', 'CapsLock', 'KeyA', 'a', 'A', 'KeyS', 's', 'S', 'KeyD', 'd', 'D', 'KeyF', 'f', 'F', 'KeyG', 'g', 'G', 'KeyH', 'h', 'H', 'KeyJ', 'j', 'J', 'KeyK', 'k', 'K', 'KeyL', 'l', 'L', 'Semicolon', ';', ':', 'Quote', '\'', '"', 'Enter', '\n', '\r', 'ShiftLeft', 'Shift', 'KeyZ', 'z', 'Z', 'KeyX', 'x', 'X', 'KeyC', 'c', 'C', 'KeyV', 'v', 'V', 'KeyB', 'b', 'B', 'KeyN', 'n', 'N', 'KeyM', 'm', 'M', 'Comma', ',', '<', 'Period', '.', '>', 'Slash', '/', '?', 'ShiftRight', 'ControlLeft', 'Control', 'MetaLeft', 'Meta', 'AltLeft', 'Alt', 'Space', ' ', 'AltRight', 'AltGraph', 'MetaRight', 'ContextMenu', 'ControlRight', 'PrintScreen', 'ScrollLock', 'Pause', 'PageUp', 'PageDown', 'Insert', 'Delete', 'Home', 'End', 'ArrowLeft', 'ArrowUp', 'ArrowRight', 'ArrowDown', 'NumLock', 'NumpadDivide', 'NumpadMultiply', 'NumpadSubtract', 'Numpad7', 'Numpad8', 'Numpad9', 'Numpad4', 'Numpad5', 'Numpad6', 'NumpadAdd', 'Numpad1', 'Numpad2', 'Numpad3', 'Numpad0', 'NumpadDecimal', 'NumpadEnter', 'ControlOrMeta']) 78 78 79 79 export async function keyboardImplementation( 80 80 pressed: Set<string>, ··· 144 144 145 145 for (const { releasePrevious, releaseSelf, repeat, keyDef } of actions) { 146 146 let key = keyDef.key! 147 - const code = 'location' in keyDef ? keyDef.key! : keyDef.code! 148 - const special = Key[code as 'Shift'] 147 + const special = Key[key as 'Shift'] 149 148 150 149 if (special) { 151 150 key = special