Files
gui/apps/kitchen-sink/tests/DialogPointerEvents.animated.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

109 lines
4.1 KiB
TypeScript

import { expect, test } from '@playwright/test'
import { setupPage } from './test-utils'
/**
* Test for dialog pointer events unlock timing
*
* Verifies that when a dialog closes, the dialog content gets pointer-events: none
* immediately, allowing clicks to pass through to elements behind during the
* exit animation.
*
* This is an animated test because the bug only manifests with animations enabled.
*/
test.describe('Dialog Pointer Events Unlock', () => {
test.beforeEach(async ({ page }) => {
await setupPage(page, { name: 'DialogPointerEventsCase', type: 'useCase' })
})
test('dialog content allows click-through during exit animation', async ({ page }) => {
// get initial click count
const clickCountEl = page.getByTestId('click-count')
await expect(clickCountEl).toHaveText('0')
// open the dialog
const triggerButton = page.getByTestId('dialog-trigger')
await triggerButton.click()
// wait for dialog to be visible
const dialogClose = page.locator('[data-testid="dialog-close"]')
await expect(dialogClose).toBeVisible({ timeout: 5000 })
// wait for enter animation to settle
await page.waitForTimeout(800)
// set up monitoring to check dialog content pointer events after close
const results = await page.evaluate(async () => {
return new Promise<{
contentPointerEvents: string[]
buttonAccessible: boolean[]
dialogStillAnimating: boolean
}>((resolve) => {
const contentPointerEvents: string[] = []
const buttonAccessible: boolean[] = []
const closeButton = document.querySelector(
'[data-testid="dialog-close"]'
) as HTMLElement
const dialogContent = document.querySelector('[role="dialog"]') as HTMLElement
const backgroundBtn = document.querySelector(
'[data-testid="background-button"]'
) as HTMLElement
// start sampling immediately
const interval = setInterval(() => {
if (dialogContent) {
contentPointerEvents.push(getComputedStyle(dialogContent).pointerEvents)
}
// check if the background button is accessible (not blocked by dialog)
const btnRect = backgroundBtn?.getBoundingClientRect()
if (btnRect) {
const elementAtPoint = document.elementFromPoint(
btnRect.left + btnRect.width / 2,
btnRect.top + btnRect.height / 2
)
buttonAccessible.push(
elementAtPoint === backgroundBtn || backgroundBtn.contains(elementAtPoint)
)
}
}, 20)
// click close button
closeButton.click()
// after 300ms, check state and stop sampling
setTimeout(() => {
clearInterval(interval)
const stillAnimating = dialogContent
? getComputedStyle(dialogContent).opacity !== '0'
: false
resolve({
contentPointerEvents,
buttonAccessible,
dialogStillAnimating: stillAnimating,
})
}, 300)
})
})
console.log('Content pointer events during exit:', results.contentPointerEvents)
console.log('Button accessible during exit:', results.buttonAccessible)
console.log('Dialog still animating at end:', results.dialogStillAnimating)
// the key assertion: the background button should become accessible quickly
// (within first few samples) - this is the actual user-facing behavior we care about
// note: the dialog portal container gets pointer-events: none, which allows clicks
// to pass through even if the dialog content itself has pointer-events: auto
const buttonAccessibleEarly = results.buttonAccessible.some(
(accessible, i) => i < 8 && accessible
)
expect(buttonAccessibleEarly).toBeTruthy()
// wait for dialog to fully close (reanimated driver can be much slower in CI)
await expect(dialogClose).not.toBeVisible({ timeout: 10000 })
// verify background button is clickable
const backgroundButton = page.getByTestId('background-button')
await backgroundButton.click()
await expect(clickCountEl).toHaveText('1')
})
})