Skip to content

test(DropdownMenuItem): the audit opens its menu and captures a row's highlight under load - #9495

Merged
cixzhang merged 1 commit into
meta:mainfrom
vjeux:uikit/c08-menuitem-audit-paint
Nov 3, 2026
Merged

cixzhang merged 1 commit into
meta:mainfrom
vjeux:uikit/c08-menuitem-audit-paint

Conversation

@vjeux

@vjeux vjeux commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

Maintainer impact

Under load, the DropdownMenuItem item-states audit failed in two ways. Its focus-light receipt recorded the focused row with its focus outline drawn and a transparent background, so the check that the focused row's background differs from a resting row's failed; focus-dark, hover-light and hover-dark read the row through the same capture. And at eight workers on two cores, about one case in fifty failed at its first step, because the menu the story opens was closed. The file passes when run alone.

Intended invariant

Each case records the state it names, in the menu the story opened: an interacted row painting the menu's highlight, and nothing mid-transition. The checks themselves are unchanged.

Change

The capture. Cause: DropdownMenuItem renders through Item's interactive row, which transitions background-color (--duration-fast-min, 95 ms in the audit's theme). A focus draws the outline at once and starts the background's transition. The spec stopped motion only after the interaction, and a transition already running keeps its duration whatever a later style sets; then it waited 40 ms. When frames are starved, the capture reads the background before the transition has started. A per-frame trace shows it: the first frame after the focus has the outline drawn and the background still transparent, and the overlay arrives over the next 95 ms.

  • Motion stops before the interaction: holdMotionStill moves ahead of the focus or hover, so no background transition starts.
  • The 40 ms sleep is replaced by waits on what the capture records: fonts loaded, every finite animation and transition on the page finished (the menu's entrance included), and an interacted row's computed background equal to --color-overlay-hover resolved in that row's theme and color scheme (a token that doesn't resolve fails). Two frames then render before the receipt is read.
  • The highlight wait doesn't fail by itself: a row that never paints still writes its receipt and screenshot, and interactionPaintPass fails it.
  • The sibling cases share this path: focus-dark, and both hover cases (hovering a row focuses it, so it paints the same highlight). The rest cases also wait for any running animation before their receipt is read.

The opening. Cause: the story's play opened the menu with userEvent.click, whose events carry no coordinates, so the press model hit-tests its release at the page's corner. DropdownMenu lets the release of the press that opened it act once the press has lasted 300 ms, and a mouse release outside the trigger and the menu closes the menu. On a loaded machine the open's own synchronous render inside the pointerdown takes that long: in a traced failure the press lasted 318 ms and its release closed the menu.

  • The play presses and releases at the trigger's center (userEvent.pointer with coords), where a release never closes the menu. It is still a mouse press, so the hover cases keep the pointer's look (no focus ring on the hovered row).
  • Known follow-up, not part of this change: the press model measures the settle delay with Date.now() in its handlers, so a slow open render counts as press time; that is the useMenuPress timing class fix(core): a long press held through a busy main thread still reports, on the touch events' clock #9455 names.

Evidence

  • Failure before the change, on 4 workers pinned to two cores, ×4:
    • The highlight painted 400 ms late (a transition delay on the rows): main's spec fails focus-light in 4 of 4 runs. The focus style landing 400 ms after the focus: a focus case fails in 4 of 4. The failing receipt matches the reported one: the focused row's background rgba(0, 0, 0, 0), its outline drawn.
    • 400 ms of synchronous work in the press's pointerdown, as a slow open render: main's story and spec fail at the menu's first check in 4 of 4 runs (the release closed the menu).
  • Success after the change: the same three seams pass, 52 of 52 each; a row that never paints still writes its receipt and screenshot and fails on interactionPaintPass. Without seams: 13 of 13 alone, 156 of 156 on the pin (three ×4 rounds), and 104 of 104 at eight workers on two cores. The receipts match the old play's in all 12 cases.
  • Product/runtime behavior verified unchanged: test and story only.

Scope

  • No intended public API, product behavior, visual direction, or policy change.
  • Product changes discovered during the work were removed or split.
  • Public text and artifacts contain no internal Meta context.

Testing

  • pnpm exec playwright test --config playwright.config.ts packages/core/src/DropdownMenu/__tests__/DropdownMenuItemAudit.a11y.chromium.spec.ts --project chromium, alone and as taskset -c 2,3 … --workers=4 --repeat-each 4, against a Storybook build stamped with the checkout's revision as CI stamps it; main's side of each seam ran against a Storybook built from main's story. The seams were injected into local copies of the spec and are not committed.
  • Local gates at 900141e (one commit on main fc44bc3), on this head merged into main 449486f as the final CI run tests it: pnpm review:pre (only the public-boundary item applies), pnpm test:node (16,062 tests) and pnpm test:ui (18,860 tests) pass, and Prettier and strict ESLint pass on both files. With a Storybook built from that tree and stamped as CI stamps it, every DropdownMenu accessibility contract and the menu's cross-browser focus-return spec pass in Chromium, 165 of 165. The browser results above ran at a01158f, whose code differs from this head only in comments: the spec passed 13 of 13 alone, 156 of 156 on 4 workers pinned to two cores (three ×4 rounds), and 104 of 104 at 8 workers on the same two cores.

@vercel

vercel Bot commented Nov 2, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
astryx Ready Ready Preview Nov 3, 2026 4:55am UTC

Request Review

@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Meta Open Source bot. label Nov 2, 2026
@astracat-bot

astracat-bot Bot commented Nov 2, 2026 •

Copy link
Copy Markdown

AI review status for this pull request.

Review status Updated
🟢 Reviewed (for maintainers only) Nov 3, 2026, 5:08 AM UTC

@github-actions

github-actions Bot commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

PR Analysis Report

🔎 Preview · 900141e

Storybook · Sandbox · built from this PR's head

Modified components

Component Size LoC Visual · A11y · RTL
ContextMenuItem 50.2 kB (0) · 9.9 kB CSS (0) — — · — · —
DropdownMenu 70.5 kB (0) · 15.4 kB CSS (0) — ✓ · ✓ · ✓
DropdownMenuCheckboxItem 18.2 kB (0) · 5.9 kB CSS (0) — ✓ · ✓ · ✓
6 more components
Component Size LoC Visual · A11y · RTL
DropdownMenuDivider 5.2 kB (0) · 548 B CSS (0) — ✓ · ✓ · —
DropdownMenuGroup 6.2 kB (0) · 3.4 kB CSS (0) — ✓ · ✓ · —
DropdownMenuItem 17.4 kB (0) · 5.5 kB CSS (0) — ✓ · ✓ · —
DropdownMenuRadioGroup 6.1 kB (0) · 3.3 kB CSS (0) — ✓ · ✓ · —
DropdownMenuRadioItem 18.2 kB (0) · 5.9 kB CSS (0) — ✓ · ✓ · —
DropdownMenuSubMenu 55.7 kB (0) · 9.9 kB CSS (0) — ✓ · ✓ · ✓

Package size: no published package changed; no new dependencies.

API changes: none in this pull request

Committed API report behind this build: core 5 rows; run pnpm api:report.

Touched components checked; add ci:run-visual, ci:run-a11y or ci:run-rtl to run the full suites.


Generated by PR Enrichment workflow | View full report

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changes requested, for the description only: the settle-path fix itself looks sound. The Maintainer impact paragraph says every case's screenshot could also catch the menu's entrance fade. But each screenshot already follows holdMotionStill and passes animations: 'disabled', which finishes finite animations and transitions before capture. Please drop that clause, or say that the rest cases now also wait for any running animation before their receipt is read.

[Automated review]

Comment on lines +188 to +195
await expect
.poll(
async () => highlighted.evaluate(paintsHighlight, HIGHLIGHT_TOKEN),
{
message: `the interacted row paints ${HIGHLIGHT_TOKEN}`,
},
)
.toBe(true);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-blocking: this poll throws before the receipt and screenshot are written (lines 397–402). A focus or hover case that never paints --color-overlay-hover now fails with the poll's message and expected true, received false, and leaves no receipt. That receipt is what diagnosed this flake. Letting the wait time out without throwing would keep the wait and let interactionPaintPass decide, with the receipt still written.

@vjeux

vjeux commented Nov 2, 2026

Copy link
Copy Markdown
Contributor Author

Description corrected: Maintainer impact no longer says the screenshots could catch the menu's entrance fade (each one passes animations: 'disabled'), and Change now says the rest cases also wait for any running animation before their receipt is read.

On the poll: agreed. A wait that ends without throwing, so a row that never paints still writes its receipt and screenshot and fails on interactionPaintPass, is ready for the next push.

@vjeux vjeux changed the title test(DropdownMenuItem): the audit captures a row's highlight once it has painted test(DropdownMenuItem): the audit opens its menu and captures a row's highlight under load Nov 2, 2026
@vjeux
vjeux force-pushed the uikit/c08-menuitem-audit-paint branch from e06b099 to a01158f Compare November 2, 2026 08:05
@vjeux

vjeux commented Nov 2, 2026

Copy link
Copy Markdown
Contributor Author

Done in a01158f: the highlight wait no longer throws, so a row that never paints still writes its receipt and screenshot and fails on interactionPaintPass. The description now matches this head, including a second load cause the PR also fixes: the story's opening click carried no coordinates, so after a slow open render its release, hit-tested at the page's corner, closed the menu.

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ready to approve: both load fixes match their stated causes, and the audit's checks are unchanged. One optional touch-up: the comment on HIGHLIGHT_TOKEN cites component:DropdownMenuItem, but that record names no token; --color-overlay-hover comes from the row's :focus style in DropdownMenuItem.tsx.

[Automated review]


const OUTPUT = path.resolve('dropdown-menu-item-audit-evidence');
const STORY_ID = 'core-dropdownmenuitem--item-states';
/** The overlay a focused or hovered row paints (`component:DropdownMenuItem`). */

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Optional: component:DropdownMenuItem names no token here (FR8 says only "hover background"). --color-overlay-hover comes from the row's :focus style in DropdownMenuItem.tsx, so citing that source would keep this comment accurate.

@vjeux

vjeux commented Nov 2, 2026

Copy link
Copy Markdown
Contributor Author

Agreed: the comment should cite the row's :focus style in DropdownMenuItem.tsx, since the record names no token. The one-line fix is staged for the next push, if one is called for.

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ready to approve: both load fixes match their stated causes, the audit's checks are unchanged, and the HIGHLIGHT_TOKEN comment now points at the row's :focus style in DropdownMenuItem.tsx. Nothing further is needed.

[Automated review]

… highlight under load

Two load causes in the item-states audit.

The capture. The focus and hover cases read the interacted row's
background right after the interaction, but the row transitions
background-color (Item's interactive rows, --duration-fast-min): a focus
draws the outline at once and starts the background's transition, and
the spec stopped motion only after the interaction, when a running
transition keeps its duration. With starved frames the capture read the
background before the transition started. Motion now stops before the
interaction, and instead of sleeping 40 ms the capture waits for fonts,
every finite animation and transition to finish, and an interacted row's
background to equal --color-overlay-hover resolved in its theme and color
scheme, then two rendered frames. That wait doesn't fail by itself: a row
that never paints still writes its receipt and screenshot, and the
receipt's check fails.

The opening. The story's play opened the menu with a click that carries
no coordinates, so its release landed at the page's corner. DropdownMenu
lets the release of the press that opened it act once the press has
lasted 300 ms, and on a loaded machine the open's own render takes that
long; a mouse release outside the trigger and the menu then closes the
menu it opened. The play now presses and releases at the trigger's
center, where a release never closes it.

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ready to approve: both load fixes match their stated causes, the audit's checks are unchanged, and a row that never paints still fails interactionPaintPass with its receipt and screenshot written. Nothing further is needed.

[Automated review]

@vjeux
vjeux marked this pull request as ready for review November 3, 2026 05:12
@cixzhang cixzhang added the ci:run A maintainer lets this outside contributor's pull request run CI (AST-030 FR17) label Nov 3, 2026
@cixzhang
cixzhang marked this pull request as draft November 3, 2026 06:06
@cixzhang
cixzhang marked this pull request as ready for review November 3, 2026 06:06
@cixzhang
cixzhang merged commit d40b519 into meta:main Nov 3, 2026
151 of 156 checks passed
@vjeux
vjeux deleted the uikit/c08-menuitem-audit-paint branch November 3, 2026 07:01

This branch was successfully deployed

1 active deployment
Preview — 900141e8 Deployed Nov 3, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci:run A maintainer lets this outside contributor's pull request run CI (AST-030 FR17) CLA Signed This label is managed by the Meta Open Source bot.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants