fix(web): check a radio or checkbox through its own label when the label covers it
SA
9h ago+261+261+012345678901234567890123456789-2-2-0123456789
What and why
- Element Plus draws
el-radio-button,el-radio, andel-checkboxwith 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, socheck,uncheck, andtapon the input failedNOT_ACTIONABLE(<span class="el-radio-button__inner"> intercepts pointer events,element is not visible), and an agent step on such a radio endedAUTOMATION_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 failsNOT_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 (sotapnever follows a link), or a boxless one with no rendered label (display: noneorvisibility: hidden,checkVisibility) is clicked as before.- One in-page read (
pickStandIn, alocator.evaluatethat 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 keepslocator.click, re-resolving, under the same deadline. A locator that never appears still failsNOT_ACTIONABLE(tap did not become actionable in time: ... waiting for locator(...)) within one timeout. setCheckedkeeps 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 newa control its own label coverscases (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 stayNOT_ACTIONABLEand unchecked; a boxless checkbox whose first label is hidden bydisplayor byvisibilityand 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 stayNOT_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 failureintercepts pointer eventsorelement is not visible. The review-round tests were red before their fixes (element is not visibleon thevisibility: hiddenlabel,checked state did not changewith the link pressed instead of the radio). On this branch:Tests 59 passed (59); the full web suiteTests 626 passed | 1 skipped (627),pnpm checkgreen.- 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/webbuilt from main and from this branch:
| main | this branch |
|---|---|
Tests 3 failed | 0 passed (3), 93.94s | Tests 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/**/*.mdxpage for the behavior, andskills/e2e/if the skill describes it. - Contract change: emitted
.d.tsreviewed,tests/types/sdk-types.tsupdated; wire change edits the schema and both fixtures. - Engine contract change (
e2e/engine): changesets fore2e,@e2e-dev/web, and@e2e-dev/mobile.
🤖 Generated with Claude Code