Files
gui/apps/kitchen-sink/tests/MenuHoverKeyboardBugs.test.tsx
zeekayandHanzo Dev 69c6e860d7 transplant onto tamagui v2.6.0, brand as a re-runnable transform
This repo could never sync. There was NO merge-base with tamagui — 372 commits
here against 15,791 there, unrelated histories — because it began as a
copy-paste rebrand with a flattened layout. Every "sync" since was a hand
re-application that drifted, which is why the tree sat on 7.3.1 while upstream
shipped fixes we then rediscovered by hand.

This commit's PARENT IS TAMAGUI. A future sync is `git merge tama/main` then
`node scripts/rebrand.mjs` — a real merge, because the history is now shared.

174 packages: 166 upstream, our 8 restored (chrome, shell, telemetry, recipes,
get, font-geist-{sans,mono}, @hanzo/booker), bento-get gained. 170/170 build.

Our patches to files UPSTREAM ALSO OWNS are carried, which the package-level
delta missed and which was where the value actually was:
  - useMedia's server snapshot (`initState ?? getMedia()`). Upstream still
    returns undefined there, so every Tamagui Dialog kills a prerender — the
    exact crash hanzo.app spent a day on.
  - data-slot on web host nodes, absent upstream entirely.
Both applied ONTO upstream's newer files (it rewrote useMedia +150/-63 and
createComponent +160/-55), marked `PATCH (ours, carried across the transplant)`.

Seven traps are encoded in scripts/rebrand.mjs, each found by a gate:
skip itself and lockfiles; special-case the double-branded names in BOTH scoped
and bare form; map path strings, not just paths; drop a level for the flattened
group; check disk rather than a stale index.

Rust build artifacts are NOT committed. An earlier draft of this commit swept
apps/desktop/src-tauri/target/debug in via `git add -A` — 3,859 files including
a 272MB .a, over GitHub's 100MB cap, so the push could never have landed. They
are gitignored now.

Co-Authored-By: Hanzo Dev <[email protected]>
2026-07-30 18:25:06 -07:00

253 lines
9.5 KiB
TypeScript

import { expect, test } from '@playwright/test'
import { setupPage } from './test-utils'
test.describe('Menu hover/keyboard bug fixes', () => {
test.beforeEach(async ({ page }) => {
await setupPage(page, { name: 'MenuAccessibilityCase', type: 'useCase' })
})
/**
* Bug 1: hoverStyle should not cause double highlighting
* When you hover an item and then use keyboard to navigate away WITHOUT moving the mouse,
* the hovered item's hoverStyle can persist alongside the keyboard-focused item's highlight,
* resulting in two items appearing highlighted at once.
*
* The key scenario: mouse is still over item1, but keyboard navigates to item2.
* Both items should NOT have highlighted background - only item2 should.
*/
test('no double highlight: when keyboard navigates while mouse stays over original item', async ({
page,
}) => {
const trigger = page.getByTestId('menu-trigger')
await trigger.focus()
await page.keyboard.press('Enter')
await page.waitForTimeout(300)
const menuContent = page.getByTestId('menu-content')
await expect(menuContent).toBeVisible()
const item1 = page.getByTestId('menu-item-1')
const item2 = page.getByTestId('menu-item-2')
// get the default (non-highlighted) background color of item2
const defaultBg = await item2.evaluate((el) => getComputedStyle(el).backgroundColor)
// hover item 1 with mouse - it should get highlighted
await item1.hover()
await page.waitForTimeout(150)
// item1 should be focused and highlighted
await expect(item1).toBeFocused()
const item1BgAfterHover = await item1.evaluate(
(el) => getComputedStyle(el).backgroundColor
)
// item1 should have a different (highlighted) background
expect(item1BgAfterHover).not.toBe(defaultBg)
// now use keyboard to move to item 2
// CRITICAL: do NOT move the mouse - it stays over item1
// this is the exact scenario where hoverStyle causes double highlight
await page.keyboard.press('ArrowDown')
await page.waitForTimeout(150)
// item2 should now be focused and highlighted
await expect(item2).toBeFocused()
// get backgrounds - CRITICAL TEST: only item2 should have highlight bg
const item1BgAfterArrow = await item1.evaluate(
(el) => getComputedStyle(el).backgroundColor
)
const item2BgAfterArrow = await item2.evaluate(
(el) => getComputedStyle(el).backgroundColor
)
// item2 should have highlighted bg (via data-highlighted/focusStyle)
expect(item2BgAfterArrow).not.toBe(defaultBg)
// CRITICAL: item1 should NOT have highlighted background even though mouse is still over it
// If this fails, it means hoverStyle is causing double highlight
expect(item1BgAfterArrow).toBe(defaultBg)
})
/**
* Bug 2: After hovering a submenu trigger (before the auto-open timer fires),
* pressing ArrowRight should open the submenu and focus its first item.
*
* When you hover over a submenu trigger with the mouse and IMMEDIATELY press ArrowRight
* (before the 100ms auto-open delay), the submenu should open and focus should move into it.
*/
test('hovering submenu trigger then immediately pressing ArrowRight opens and focuses submenu', async ({
page,
}) => {
const trigger = page.getByTestId('menu-trigger')
await trigger.focus()
await page.keyboard.press('Enter')
await page.waitForTimeout(300)
const menuContent = page.getByTestId('menu-content')
await expect(menuContent).toBeVisible()
const submenuTrigger = page.getByTestId('submenu-trigger')
// hover the submenu trigger with mouse
await submenuTrigger.hover()
// don't wait for the auto-open timer - press arrow key immediately
await page.waitForTimeout(50) // just enough for the focus to happen
// the submenu trigger should be focused from the hover
await expect(submenuTrigger).toBeFocused()
// now press ArrowRight to open the submenu - the submenu might not be open yet
// because the 100ms auto-open timer hasn't fired
const submenuContent = page.getByTestId('submenu-content')
await page.keyboard.press('ArrowRight')
await page.waitForTimeout(300)
// the submenu should be open
await expect(submenuContent).toBeVisible({ timeout: 2000 })
// CRITICAL: the first submenu item should now be focused
// this is the key difference from mouse-hover behavior
const submenuItem1 = page.getByTestId('submenu-item-1')
await expect(submenuItem1).toBeFocused()
})
/**
* Bug 2b: When submenu is already open via hover, pressing ArrowRight should focus
* the first submenu item (moving focus into the submenu).
*
* Scenario: User hovers submenu trigger, submenu auto-opens, user presses ArrowRight
* expecting to move focus into the already-open submenu.
*/
test('when submenu is already open via hover, ArrowRight focuses first submenu item', async ({
page,
}) => {
const trigger = page.getByTestId('menu-trigger')
await trigger.focus()
await page.keyboard.press('Enter')
await page.waitForTimeout(300)
const menuContent = page.getByTestId('menu-content')
await expect(menuContent).toBeVisible()
const submenuTrigger = page.getByTestId('submenu-trigger')
// hover the submenu trigger and wait for it to auto-open
await submenuTrigger.hover()
await page.waitForTimeout(200) // wait for 100ms auto-open delay
const submenuContent = page.getByTestId('submenu-content')
await expect(submenuContent).toBeVisible({ timeout: 2000 })
// at this point, submenu is open but trigger is still focused (from hover)
await expect(submenuTrigger).toBeFocused()
// pressing ArrowRight should move focus INTO the submenu
await page.keyboard.press('ArrowRight')
await page.waitForTimeout(150)
// the first submenu item should now be focused
const submenuItem1 = page.getByTestId('submenu-item-1')
await expect(submenuItem1).toBeFocused()
})
/**
* Bug 2c: When a submenu is open via mouse hover (not keyboard), pressing keyboard keys
* should still work to navigate within it.
*
* Scenario: User opens menu, hovers over submenu trigger (which opens submenu),
* then hovers over a submenu item, then tries to use keyboard to navigate.
*/
test('submenu opened via hover then keyboard navigation works', async ({ page }) => {
const trigger = page.getByTestId('menu-trigger')
await trigger.focus()
await page.keyboard.press('Enter')
await page.waitForTimeout(300)
const menuContent = page.getByTestId('menu-content')
await expect(menuContent).toBeVisible()
// hover the submenu trigger - this should open the submenu after a delay
const submenuTrigger = page.getByTestId('submenu-trigger')
await submenuTrigger.hover()
await page.waitForTimeout(300) // wait for the 100ms delay to open
const submenuContent = page.getByTestId('submenu-content')
await expect(submenuContent).toBeVisible({ timeout: 2000 })
// now hover a submenu item
const submenuItem1 = page.getByTestId('submenu-item-1')
await submenuItem1.hover()
await page.waitForTimeout(150)
await expect(submenuItem1).toBeFocused()
// now try keyboard navigation - ArrowDown should go to submenu-item-2
await page.keyboard.press('ArrowDown')
await page.waitForTimeout(150)
const submenuItem2 = page.getByTestId('submenu-item-2')
await expect(submenuItem2).toBeFocused()
// ArrowLeft should close submenu and return to parent
await page.keyboard.press('ArrowLeft')
await page.waitForTimeout(300)
await expect(submenuContent).not.toBeVisible()
await expect(submenuTrigger).toBeFocused()
})
/**
* Bug 2c: After hovering a submenu item (inside an open submenu), pressing ArrowRight
* should NOT do anything weird (or should do the right thing depending on context).
*
* This tests that after the user hovers items in the submenu with their mouse,
* then switches to keyboard navigation, it works correctly.
*/
test('hovering submenu item then using keyboard navigation works', async ({ page }) => {
const trigger = page.getByTestId('menu-trigger')
await trigger.focus()
await page.keyboard.press('Enter')
await page.waitForTimeout(300)
const menuContent = page.getByTestId('menu-content')
await expect(menuContent).toBeVisible()
// open the submenu via keyboard first
const submenuTrigger = page.getByTestId('submenu-trigger')
await submenuTrigger.focus()
await page.keyboard.press('ArrowRight')
await page.waitForTimeout(300)
const submenuContent = page.getByTestId('submenu-content')
await expect(submenuContent).toBeVisible()
// now hover the first submenu item with mouse
const submenuItem1 = page.getByTestId('submenu-item-1')
await submenuItem1.hover()
await page.waitForTimeout(150)
await expect(submenuItem1).toBeFocused()
// hover the second submenu item
const submenuItem2 = page.getByTestId('submenu-item-2')
await submenuItem2.hover()
await page.waitForTimeout(150)
await expect(submenuItem2).toBeFocused()
// now use keyboard to navigate back up
await page.keyboard.press('ArrowUp')
await page.waitForTimeout(150)
// the first item should now be focused (keyboard navigation should work)
await expect(submenuItem1).toBeFocused()
// pressing ArrowLeft should close the submenu and return focus to the trigger
await page.keyboard.press('ArrowLeft')
await page.waitForTimeout(300)
await expect(submenuContent).not.toBeVisible()
await expect(submenuTrigger).toBeFocused()
})
})