-
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
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
Changes from all commits
c6ea761
30c458f
1d477ab
a298b25
ab76c71
212dac1
9506828
cb717ab
7da03e8
d4620bd
24fd7dd
390457f
f6cfc27
1f12f7c
d14bebc
745a95a
c73e0d7
59d2fd2
512ffc6
1eda66c
6ff4a4f
f1559f1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -74,7 +74,7 @@ export const keyboardCleanup: UserEventCommand<(state: KeyboardState) => Promise | |
|
||
// fallback to insertText for non US key | ||
// https://github.com/microsoft/playwright/blob/50775698ae13642742f2a1e8983d1d686d7f192d/packages/playwright-core/src/server/input.ts#L95 | ||
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']) | ||
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']) | ||
|
||
export async function keyboardImplementation( | ||
pressed: Set<string>, | ||
|
@@ -144,8 +144,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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I changed this as it wasn't working when There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The difference here is The same is for There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is broken anyways regardless of what I do here. (see 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 | ||
|
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", | ||
] | ||
`) | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am confused why this is required here. All sent characters are from https://github.com/testing-library/user-event/blob/main/src/keyboard/keyMap.ts shouldn't we do this check in
keyboard
?We probably need a test for every key 👀
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
parseKeyDef
doesn't strictly check what's inside{xxx}
and, for example,{ControlOrMeta}
ends up withkeyDef: { key: 'ControlOrMeta', code: 'Unknown' }
. Then we only sendkeyDef.key
to the provider, so it's working.We should probably do something with this key translation layer, but what do you suggest for this specific
ControlOrMeta
etc... specifically? Is this about consistency between providers (like supportingControlOrMeta
both for playwright and webdriverio)?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it's about the compatibility with testing-library. If there are keys that are unique to providers, they can stay that way, but common keys should do the same thing and should be entered the same way like in the key map *so,
ControlOrMeta
isControl
in every provider or something) - we can extend the keyMap, by the wayThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay, that sounds like a good spec. I'll check later, but providers might only support only
{key}
concept (like{Control}
), but not physical[code]
(like[ControlLeft]
). Is alignment only for{key}
or you're thinking about to do[code]
too?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it's fine to fallback to
Control
whenControlLeft
is used. There is alocation
property on the even though, so it might be useful for some: https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/locationBut if there is no way to do that in provider, then we can't do anything about it yet