testing-library / testing-library/user-event

`user.type()` doesn't handle clearing of textareas in shadow DOM

Open
#1,314 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

bug needs assessment
Dominant language
TypeScript
Stars
2.3k
Forks
258
PR merge metrics
No merged PRs in 30d

Description

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");
});

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start from the user.type() handling and the cache behavior demonstrated in the reproduction tests, comparing the light-DOM and shadow-DOM cases. Trace how textarea values are cached after direct clearing or form.reset(), then make the shadow-DOM case produce only "world" on the second call and verify the existing reproduction expectations.

Written by the indexing model from the issue text.

Assessment

Tech stack
typescript
Domain
testing
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
68/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.