Reproduction example
https://codesandbox.io/p/devbox/still-leaf-dfv3dd?file=%2Fmwe.ts&workspaceId=ws_WjQcfkhNBektSLNjvEXy89
Prerequisites
Do user.type() on e.g. a textarea in shadow DOM / web component, clear it with e.g. .value = "" or form.reset(), and user.type() some more.
Expected behavior
The testing library should handle the clearing correctly. It works as expected in light DOM.
Actual behavior
When you user.type() the second time, the content from the first user.type() get put into the textarea.
User-event version
14.6.1
Environment
@testing-library/user-event: 14.6.1, Node v24.15.0, Linux
Additional context
This MWE should make it explicit. It seems to be a problem with how the caching is handled? Note that the unexpected behavior is the most clear on the second to last line.
// @vitest-environment jsdom
/* package.json:
{
"name": "user-event-shadow-dom-repro",
"private": true,
"type": "module",
"scripts": {
"test": "vitest run"
},
"devDependencies": {
"@testing-library/user-event": "^14.6.1",
"jsdom": "^30.0.1",
"vitest": "^4.1.10"
}
}
*/
import userEvent from "@testing-library/user-event";
import { expect, test } from "vitest";
function getCachedUIValue(ta: HTMLTextAreaElement) {
for (const sym of Object.getOwnPropertySymbols(ta)) {
if (sym.description === "Displayed value in UI") {
return (ta as unknown as Record<symbol, unknown>)[sym] ?? "";
}
}
}
function lightTextarea(): HTMLTextAreaElement {
const ta = document.createElement("textarea");
document.body.append(ta);
return ta;
}
test("light DOM works as expected", async () => {
const user = userEvent.setup();
const ta = lightTextarea();
// Type something into the <textarea>
await user.type(ta, "hello");
expect(ta.value).toBe("hello");
expect(getCachedUIValue(ta)).toBe("hello");
// Clear the textarea content. FWIW, form.reset() has the same effect.
ta.value = "";
expect(ta.value).toBe("");
expect(getCachedUIValue(ta)).toBe("");
// Type some more text. In principle, the <textarea> should now only contain "world"
await user.type(ta, "world");
expect(ta.value).toBe("world");
expect(getCachedUIValue(ta)).toBe("world");
});
// MWE
function shadowTextarea(): { host: HTMLElement; ta: HTMLTextAreaElement } {
const host = document.createElement("div");
const shadow = host.attachShadow({ mode: "open" });
const ta = document.createElement("textarea");
shadow.append(ta);
document.body.append(host);
return { host, ta };
}
test.fails("but shadow DOM doesn't", async () => {
const user = userEvent.setup();
const { ta } = shadowTextarea();
// Type something into the <textarea>
await user.type(ta, "hello");
expect(ta.value).toBe("hello");
expect(getCachedUIValue(ta)).toBe("hello");
// Clear the textarea content. FWIW, form.reset() has the same effect.
ta.value = "";
expect(ta.value).toBe("");
expect(getCachedUIValue(ta)).toBe("");
// Type some more text. In principle, the <textarea> should now only contain "world"
await user.type(ta, "world");
expect(ta.value).toBe("world");
expect(getCachedUIValue(ta)).toBe("world");
});
test("instead, shadow DOM has an outdated cache", async () => {
const user = userEvent.setup();
const { ta } = shadowTextarea();
// Type something into the <textarea>
await user.type(ta, "hello");
expect(ta.value).toBe("hello");
expect(getCachedUIValue(ta)).toBe("hello");
// Clear the textarea content. FWIW, form.reset() has the same effect.
ta.value = "";
expect(ta.value).toBe("");
expect(getCachedUIValue(ta)).toBe("hello");
// Type some more text. In principle, the <textarea> should now only contain "world"
await user.type(ta, "world");
// !!! This is the main MWE / unexpected behavior
expect(ta.value).toBe("helloworld");
// !!!
expect(getCachedUIValue(ta)).toBe("helloworld");
});
Reproduction example
https://codesandbox.io/p/devbox/still-leaf-dfv3dd?file=%2Fmwe.ts&workspaceId=ws_WjQcfkhNBektSLNjvEXy89
Prerequisites
Do
user.type()on e.g. a textarea in shadow DOM / web component, clear it with e.g..value = ""orform.reset(), anduser.type()some more.Expected behavior
The testing library should handle the clearing correctly. It works as expected in light DOM.
Actual behavior
When you
user.type()the second time, the content from the firstuser.type()get put into the textarea.User-event version
14.6.1
Environment
@testing-library/user-event:14.6.1, Nodev24.15.0, LinuxAdditional context
This MWE should make it explicit. It seems to be a problem with how the caching is handled? Note that the unexpected behavior is the most clear on the second to last line.