fix(web): check a radio or checkbox through its own label when the label covers it

SA
9h ago
+261-2

What and why

  • Element Plus draws el-radio-button, el-radio, and el-checkbox with the native input under the label's styled spans (opacity: 0; z-index: -1), or with no box at all (width: 0; height: 0). Playwright's hit-target check accepts only the target or its descendants, so check, uncheck, and tap on the input failed NOT_ACTIONABLE (<span class="el-radio-button__inner"> intercepts pointer events, element is not visible), and an agent step on such a radio ended AUTOMATION_UNSUPPORTED. Plain Playwright .check() fails the same way.
  • clickControl (packages/web/src/actions.ts) now clicks a native checkbox or radio through its own label when the element at the input's center is inside one of its labels, or the input has no box (then its first label with a box). The click lands at the input's point relative to the label, so a link elsewhere in the label is not what gets pressed, and Playwright's own actionability checks run on the label: the label's content passes, anything else on top still fails NOT_ACTIONABLE. A disabled input (a disabled <fieldset>'s included, :disabled), an input that is on top itself, one covered by something outside its labels, one whose label puts a link, button, or other interactive element at the click point (so tap never follows a link), or a boxless one with no rendered label (display: none or visibility: hidden, checkVisibility) is clicked as before.
  • One in-page read (pickStandIn, a locator.evaluate that waits for the locator under the action's timeout) decides whether a label stands in; the label handle is resolved only then. A tap on anything else pays that one read and keeps locator.click, re-resolving, under the same deadline. A locator that never appears still fails NOT_ACTIONABLE (tap did not become actionable in time: ... waiting for locator(...)) within one timeout.
  • setChecked keeps polling the input's checked state after the label click.
  • Holds in a shadow root (getRootNode().elementFromPoint) and inside a frame (the frame's own viewport coordinates).
await screen.getByRole('radio', '安装').check();   // was ACTION_FAILED: intercepts pointer events
await screen.getByRole('checkbox', 'Agree').check(); // was ACTION_FAILED: element is not visible

Issue

Fixes #1276

Verified

Ran it locally: yes

  • packages/web/tests/integration/check-actions.test.ts, the new a control its own label covers cases (check and tap on each of the three Element Plus shapes, through a locator and an element handle; a modal-covered and a disabled control that must stay NOT_ACTIONABLE and unchecked; a boxless checkbox whose first label is hidden by display or by visibility and whose second is not, and one whose every label is hidden; a radio under a link in its own label and a boxless checkbox whose label has a link at its centre, both of which must stay NOT_ACTIONABLE, unchecked, and on the same URL; a radio-button inside <fieldset disabled>; a shadow root; a frame; a tap on a locator that never appears). With the source stashed to main: Tests 15 failed | 31 passed (46), every failure intercepts pointer events or element is not visible. The review-round tests were red before their fixes (element is not visible on the visibility: hidden label, checked state did not change with the link pressed instead of the radio). On this branch: Tests 59 passed (59); the full web suite Tests 626 passed | 1 skipped (627), pnpm check green.
  • Built CLI on a scratch project whose page carries the real theme-chalk CSS for the three controls (el-radio-button.css, el-radio.css, el-checkbox.css) and three deterministic tests, node node_modules/e2e/dist/cli/bin.js run, @e2e-dev/web built from main and from this branch:
mainthis branch
Tests 3 failed | 0 passed (3), 93.94sTests 3 passed (3), 4.74s
ACTION_FAILED: check did not become actionable in time: elementHandle.click: Timeout 29490ms exceeded. <span class="el-radio-button__inner">安装</span> intercepts pointer events: getByRole("radio", name: "安装")✓ checks an Element Plus radio button 1.09s
ACTION_FAILED: check did not become actionable in time: elementHandle.click: Timeout 29549ms exceeded. element is not visible: getByRole("checkbox", name: "Agree")✓ checks an Element Plus checkbox 734ms
ACTION_FAILED: tap did not become actionable in time: locator.click: Timeout 29602ms exceeded. <span class="el-radio__inner"></span> intercepts pointer events: getByRole("radio", name: "Large")✓ clicks an Element Plus radio 552ms

Checklist

  • Changeset added (pnpm changeset) for any change to a published package, or the change is testbed, docs, or CI only.
  • Breaking change: title carries !, the changeset body names what breaks and what replaces it, and the deprecation warning landed a window earlier (stability policy in CONTRIBUTING.md).
  • Docs updated in the same PR: the docs/**/*.mdx page for the behavior, and skills/e2e/ if the skill describes it.
  • Contract change: emitted .d.ts reviewed, tests/types/sdk-types.ts updated; wire change edits the schema and both fixtures.
  • Engine contract change (e2e/engine): changesets for e2e, @e2e-dev/web, and @e2e-dev/mobile.

🤖 Generated with Claude Code