Skip to content

fix(admin): don't steal slash menu selection on stationary pointer#1013

Merged
ascorbic merged 1 commit into
mainfrom
fix/slash-menu-mouseenter-race
May 13, 2026
Merged

fix(admin): don't steal slash menu selection on stationary pointer#1013
ascorbic merged 1 commit into
mainfrom
fix/slash-menu-mouseenter-race

Conversation

@ascorbic
Copy link
Copy Markdown
Collaborator

What does this PR do?

Fixes the recurring "Browser Tests" flake in tests/editor/slash-menu.test.tsx. Also reverts the temporary diagnostic instrumentation from #1010 (workflow artifact upload + diagWaitFor helpers), which we no longer need.

Root cause

The slash command menu renders right at the text cursor's clientRect. Each rendered item has onMouseEnter={() => setSelectedIndex(index)} to support the "hover to select" UX.

The problem: mouseenter fires when an element appears under a stationary pointer, not only when the pointer moves over it. In CI, Vitest's docs explicitly call this out:

With playwright and webdriverio providers, interactions are performed by the underlying browser driver. That means some interaction state, like pointer position and the resulting hover state, can persist between tests in the same file.
Vitest resets unreleased keyboard state automatically before starting each test case, but pointer position and the resulting hover state are not reset automatically since resetting pointer position can be expensive.

So whenever an earlier test left the pointer at a Y coordinate that happens to overlap a slash menu item in the next test, that item's mouseenter fires the instant the menu renders and overrides the keyboard-driven default selection of 0.

This was strictly intermittent locally (we never reproduced on macOS even at 40x CDP CPU throttle) but near-deterministic on Linux CI when the slash-menu suite ran slowly enough for the menu render to happen after the pointer settled.

Evidence

The diagnostic dumps from #1010 caught it red-handed on the very next CI run after merging. From the failing run logs (job):

Test Expected selected Actual selected
highlights the first item by default item 0 item 4
moves selection down with ArrowDown item 1 item 5
ArrowUp from second item: first ArrowDown item 1 item 5
wraps selection around (ArrowUp from first) item 10 item 3
Enter converts to heading h1 created menu closed but executed item 4 instead

Pattern: the wrong item is consistently expected + 4 or close to it -- a constant offset that lines up with the runner's lingering Y coord.

Fix

Add a hasMouseMovedRef to the menu container. It starts false when the menu opens, flips to true only on pointermove, and is reset on close. The per-item onMouseEnter handler now no-ops while the flag is false:

```tsx
onPointerMove={() => { hasMouseMovedRef.current = true; }}
// ...
onMouseEnter={() => {
if (hasMouseMovedRef.current) {
setSelectedIndex(index);
}
}}
```

This preserves the legitimate "hover to select" UX after real pointer movement, but ignores mouseenter events that fire merely because an item appeared under a stationary pointer.

Existing test update

The highlights item on mouse hover test calls userEvent.hover(items[2]). Under Playwright, hover teleports the cursor to the target and fires pointerenter but not pointermove. The test now dispatches a synthetic pointermove on the menu container first so the gate opens, then calls userEvent.hover as before. This matches the new contract -- selection only follows the pointer once the pointer is actually being moved.

Revert of #1010 diagnostics

This PR also reverts:

  • The Upload failure screenshots step in .github/workflows/ci.yml
  • The dumpMenuState + diagWaitFor helpers in packages/admin/tests/editor/slash-menu.test.tsx (and switches the 5 wrapped sites back to plain vi.waitFor)

The diag instrumentation did its job: caught the bug on the first CI run after merging.

Verification

  • pnpm --filter @emdash-cms/admin typecheck clean
  • pnpm lint shows no new diagnostics on changed files
  • 5/5 clean runs of tests/editor/slash-menu.test.tsx (23 tests each)
  • pnpm format ran

Closes #

Type of change

  • Bug fix
  • Feature (requires maintainer-approved Discussion)
  • Refactor (no behavior change)
  • Translation
  • Documentation
  • Performance improvement
  • Tests
  • Chore (dependencies, CI, tooling)

Checklist

AI-generated code disclosure

  • This PR includes AI-generated code -- model/tool: Claude Opus 4.7 (opencode)

When the slash menu opens, it renders right at the text cursor. If the
user's mouse pointer happens to sit at the same coords -- which is the
default in CI test environments where pointer position persists across
tests in the same file -- the menu's items received pointerenter the
moment they appeared and called setSelectedIndex, overriding the
keyboard-driven default selection of 0.

In CI this manifested as 5 deterministic-on-slow-runner failures in
tests/editor/slash-menu.test.tsx (highlights-the-first-item-by-default,
ArrowDown/ArrowUp navigation, wrap-around, Enter-converts-to-heading).
Diagnostic dumps from #1010 showed the menu rendering with item 3/4/5
selected instead of the expected item -- a constant offset that lined
up with the runner's lingering pointer Y coord.

Gate the per-item onMouseEnter handler on a hasMouseMovedRef that only
flips to true after a pointermove event on the menu container. Reset
the ref each time the menu closes. This preserves mouse-hover-selects
once the user is actually moving the pointer over the menu, but
ignores pointer-was-already-here, which is what we want.

The existing 'highlights item on mouse hover' test relies on
userEvent.hover, which under Playwright teleports the cursor to the
target and fires pointerenter but not pointermove -- so the test now
explicitly dispatches a pointermove on the menu container first,
matching the new contract.

Reverts the temporary diagnostic instrumentation from #1010 (workflow
artifact upload + diagWaitFor helpers in slash-menu.test.tsx) now that
we have the fix.

Closes #1010 follow-up.
Copilot AI review requested due to automatic review settings May 13, 2026 10:39
@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages Bot commented May 13, 2026

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
emdash-perf-coordinator 248c0d2 May 13 2026, 10:39 AM

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented May 13, 2026

🦋 Changeset detected

Latest commit: 248c0d2

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 13 packages
Name Type
@emdash-cms/admin Patch
emdash Patch
@emdash-cms/cloudflare Patch
@emdash-cms/fixture-perf-site Patch
@emdash-cms/perf-demo-site Patch
@emdash-cms/cache-demo-site Patch
@emdash-cms/auth Patch
@emdash-cms/blocks Patch
@emdash-cms/gutenberg-to-portable-text Patch
@emdash-cms/x402 Patch
create-emdash Patch
@emdash-cms/auth-atproto Patch
@emdash-cms/plugin-embeds Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@cloudflare-workers-and-pages
Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
emdash-i18n 248c0d2 May 13 2026, 10:39 AM

@cloudflare-workers-and-pages
Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
docs 248c0d2 May 13 2026, 10:40 AM

@cloudflare-workers-and-pages
Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
emdash-demo-cache 248c0d2 May 13 2026, 10:40 AM

@cloudflare-workers-and-pages
Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
emdash-playground 248c0d2 May 13 2026, 10:40 AM

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented May 13, 2026

Open in StackBlitz

@emdash-cms/admin

npm i https://pkg.pr.new/@emdash-cms/admin@1013

@emdash-cms/auth

npm i https://pkg.pr.new/@emdash-cms/auth@1013

@emdash-cms/blocks

npm i https://pkg.pr.new/@emdash-cms/blocks@1013

@emdash-cms/cloudflare

npm i https://pkg.pr.new/@emdash-cms/cloudflare@1013

emdash

npm i https://pkg.pr.new/emdash@1013

create-emdash

npm i https://pkg.pr.new/create-emdash@1013

@emdash-cms/gutenberg-to-portable-text

npm i https://pkg.pr.new/@emdash-cms/gutenberg-to-portable-text@1013

@emdash-cms/x402

npm i https://pkg.pr.new/@emdash-cms/x402@1013

@emdash-cms/plugin-ai-moderation

npm i https://pkg.pr.new/@emdash-cms/plugin-ai-moderation@1013

@emdash-cms/plugin-atproto

npm i https://pkg.pr.new/@emdash-cms/plugin-atproto@1013

@emdash-cms/plugin-audit-log

npm i https://pkg.pr.new/@emdash-cms/plugin-audit-log@1013

@emdash-cms/plugin-color

npm i https://pkg.pr.new/@emdash-cms/plugin-color@1013

@emdash-cms/plugin-embeds

npm i https://pkg.pr.new/@emdash-cms/plugin-embeds@1013

@emdash-cms/plugin-forms

npm i https://pkg.pr.new/@emdash-cms/plugin-forms@1013

@emdash-cms/plugin-webhook-notifier

npm i https://pkg.pr.new/@emdash-cms/plugin-webhook-notifier@1013

commit: 248c0d2

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR fixes a CI-only flake in the admin Portable Text editor’s slash-command menu where a stationary pointer could trigger mouseenter on render and unexpectedly override the keyboard-selected menu item. It also removes temporary diagnostic helpers and CI artifact upload steps that were added to identify the root cause.

Changes:

  • Gate slash-menu hover selection so mouseenter only updates selection after real pointer movement since the menu opened.
  • Update the slash-menu hover test to reflect the new “pointer must have moved” contract.
  • Revert temporary CI/test diagnostics introduced for investigating the flake, and add a changeset for the patch release.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.

File Description
packages/admin/src/components/PortableTextEditor.tsx Adds a “mouse moved” gate to prevent stationary-pointer mouseenter from stealing selection in the slash menu.
packages/admin/tests/editor/slash-menu.test.tsx Removes diagnostic helpers and adapts the hover test to open the new gate before hovering.
.github/workflows/ci.yml Removes the failure-only artifact upload step for vitest browser screenshots (diagnostics revert).
.changeset/fix-slash-menu-mouseenter-race.md Adds a patch changeset describing the behavior fix.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +1126 to +1127
onPointerMove={() => {
hasMouseMovedRef.current = true;
@ascorbic ascorbic merged commit 0cd8c6d into main May 13, 2026
40 checks passed
@ascorbic ascorbic deleted the fix/slash-menu-mouseenter-race branch May 13, 2026 10:50
@emdashbot emdashbot Bot mentioned this pull request May 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants