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

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
78 changes: 78 additions & 0 deletions docs/guide/browser/interactivity-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -530,3 +530,81 @@ References:

- [Playwright `frame.dragAndDrop` API](https://playwright.dev/docs/api/class-frame#frame-drag-and-drop)
- [WebdriverIO `element.dragAndDrop` API](https://webdriver.io/docs/api/element/dragAndDrop/)

## userEvent.copy

```ts
function copy(): Promise<void>
```

Copy the selected text to the clipboard.

```js
import { page, userEvent } from '@vitest/browser/context'

test('copy and paste', async () => {
// write to 'source'
await userEvent.click(page.getByPlaceholder('source'))
await userEvent.keyboard('hello')

// select and copy 'source'
await userEvent.dblClick(page.getByPlaceholder('source'))
await userEvent.copy()

// paste to 'target'
await userEvent.click(page.getByPlaceholder('target'))
await userEvent.paste()

await expect.element(page.getByPlaceholder('source')).toHaveTextContent('hello')
await expect.element(page.getByPlaceholder('target')).toHaveTextContent('hello')
})
```

References:

- [testing-library `copy` API](https://testing-library.com/docs/user-event/convenience/#copy)

## userEvent.cut

```ts
function cut(): Promise<void>
```

Cut the selected text to the clipboard.

```js
import { page, userEvent } from '@vitest/browser/context'

test('copy and paste', async () => {
// write to 'source'
await userEvent.click(page.getByPlaceholder('source'))
await userEvent.keyboard('hello')

// select and cut 'source'
await userEvent.dblClick(page.getByPlaceholder('source'))
await userEvent.cut()

// paste to 'target'
await userEvent.click(page.getByPlaceholder('target'))
await userEvent.paste()

await expect.element(page.getByPlaceholder('source')).toHaveTextContent('')
await expect.element(page.getByPlaceholder('target')).toHaveTextContent('hello')
})
```

References:

- [testing-library `cut` API](https://testing-library.com/docs/user-event/clipboard#cut)

## userEvent.paste

```ts
function paste(): Promise<void>
```

Paste the text from the clipboard. See [`userEvent.copy`](#userevent-copy) and [`userEvent.cut`](#userevent-cut) for usage examples.

References:

- [testing-library `paste` API](https://testing-library.com/docs/user-event/clipboard#paste)
21 changes: 21 additions & 0 deletions packages/browser/context.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,27 @@ export interface UserEvent {
* @see {@link https://testing-library.com/docs/user-event/utility#upload} testing-library API
*/
upload: (element: Element | Locator, files: File | File[] | string | string[]) => Promise<void>
/**
* Copies the selected content.
* @see {@link https://playwright.dev/docs/api/class-keyboard} Playwright API
* @see {@link https://webdriver.io/docs/api/browser/keys//} WebdriverIO API
* @see {@link https://testing-library.com/docs/user-event/clipboard#copy} testing-library API
*/
copy: () => Promise<void>
/**
* Cuts the selected content.
* @see {@link https://playwright.dev/docs/api/class-keyboard} Playwright API
* @see {@link https://webdriver.io/docs/api/browser/keys//} WebdriverIO API
* @see {@link https://testing-library.com/docs/user-event/clipboard#cut} testing-library API
*/
cut: () => Promise<void>
/**
* Pastes the copied or cut content.
* @see {@link https://playwright.dev/docs/api/class-keyboard} Playwright API
* @see {@link https://webdriver.io/docs/api/browser/keys//} WebdriverIO API
* @see {@link https://testing-library.com/docs/user-event/clipboard#paste} testing-library API
*/
paste: () => Promise<void>
/**
* Fills an input element with text. This will remove any existing text in the input before typing the new text.
* Uses provider's API under the hood.
Expand Down
34 changes: 33 additions & 1 deletion packages/browser/src/client/tester/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,21 @@ 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 ?? {})
let clipboardData: any
const keyboard = {
unreleased: [] as string[],
}

return {
// https://playwright.dev/docs/api/class-keyboard
// https://webdriver.io/docs/api/browser/keys/
const modifier
= provider === `playwright`
? 'ControlOrMeta'
: provider === 'webdriverio'
? 'Ctrl'
: 'Control'

const userEvent: UserEvent = {
setup(options?: any) {
return createUserEvent(__tl_user_event_base__, options)
},
Expand Down Expand Up @@ -118,7 +128,29 @@ export function createUserEvent(__tl_user_event_base__?: TestingLibraryUserEvent
)
keyboard.unreleased = unreleased
},
async copy() {
if (typeof __tl_user_event__ !== 'undefined') {
clipboardData = await __tl_user_event__.copy()
return
}
await userEvent.keyboard(`{${modifier}>}{c}{/${modifier}}`)
},
async cut() {
if (typeof __tl_user_event__ !== 'undefined') {
clipboardData = await __tl_user_event__.cut()
return
}
await userEvent.keyboard(`{${modifier}>}{x}{/${modifier}}`)
},
async paste() {
if (typeof __tl_user_event__ !== 'undefined') {
await __tl_user_event__.paste(clipboardData)
return
}
await userEvent.keyboard(`{${modifier}>}{v}{/${modifier}}`)
},
}
return userEvent
}

export function cdp() {
Expand Down
3 changes: 1 addition & 2 deletions packages/browser/src/node/commands/keyboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,8 +129,7 @@ export async function keyboardImplementation(

for (const { releasePrevious, releaseSelf, repeat, keyDef } of actions) {
let key = keyDef.key!
const code = 'location' in keyDef ? keyDef.key! : keyDef.code!
const special = Key[code as 'Shift']
Comment on lines -132 to -133
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 changed this as it wasn't working when key === 'Ctrl', which is webdriverio's special key to switch modifier keys based on platform. I'm not sure the original code's intent, but it didn't break existing tests.

const special = Key[key as 'Shift']

if (special) {
key = special
Expand Down
45 changes: 45 additions & 0 deletions test/browser/fixtures/user-event/clipboard.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { expect, test } from 'vitest';
import { page, userEvent } from '@vitest/browser/context';

test('clipboard', async () => {
// make it smaller since webdriverio fails when scaled
page.viewport(300, 300)

document.body.innerHTML = `
<input placeholder="first" />
<input placeholder="second" />
<input placeholder="third" />
`;

// write first "hello" and copy to clipboard
await userEvent.click(page.getByPlaceholder('first'));
await userEvent.keyboard('hello');
await userEvent.dblClick(page.getByPlaceholder('first'));
await userEvent.copy();

// paste into second
await userEvent.click(page.getByPlaceholder('second'));
await userEvent.paste();

// append first "world" and cut
await userEvent.click(page.getByPlaceholder('first'));
await userEvent.keyboard('world');
await userEvent.dblClick(page.getByPlaceholder('first'));
await userEvent.cut();

// paste it to third
await userEvent.click(page.getByPlaceholder('third'));
await userEvent.paste();

expect([
(page.getByPlaceholder('first').element() as any).value,
(page.getByPlaceholder('second').element() as any).value,
(page.getByPlaceholder('third').element() as any).value,
]).toMatchInlineSnapshot(`
[
"",
"hello",
"helloworld",
]
`)
});
5 changes: 4 additions & 1 deletion test/browser/specs/runner.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,14 +139,17 @@ error with a stack
})

test('user-event', async () => {
const { ctx } = await runBrowserTests({
const { ctx, stderr } = await runBrowserTests({
root: './fixtures/user-event',
})
onTestFailed(() => console.error(stderr))

expect(Object.fromEntries(ctx.state.getFiles().map(f => [f.name, f.result.state]))).toMatchInlineSnapshot(`
{
"cleanup-retry.test.ts": "pass",
"cleanup1.test.ts": "pass",
"cleanup2.test.ts": "pass",
"clipboard.test.ts": "pass",
}
`)
})