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

Merged
merged 22 commits into from
Dec 23, 2024
Merged
Show file tree
Hide file tree
Changes from 13 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
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 -147 to -148
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.

Copy link
Member

@sheremet-va sheremet-va Dec 20, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The difference here is AltrRight for example - see https://github.com/testing-library/user-event/blob/main/src/keyboard/keyMap.ts. Without this, it will always trigger Alt, not AltRight

The same is for ControlLeft/ControlRight

Copy link
Contributor Author

@hi-ogawa hi-ogawa Dec 23, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is broken anyways regardless of what I do here. (see test/browser/fixtures/user-event/keyboard.test.ts. on main [Shift] works differently, but [Shift] shouldn't actually work since there's no such code).

I don't think solving this is a scope of PR adding copy/cut/paste sugars, so we can follow this up separately #7118.

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",
}
`)
})