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

431 lines
16 KiB
TypeScript

import { expect, test } from '@playwright/test'
import { setupPage } from './test-utils'
/**
* Comprehensive tests for Sheet animation configurations
* Tests CSS driver, Motion driver, and various animation prop combinations
* Verifies that different animation configs produce measurably different animation speeds
*/
// Helper to measure actual animation duration by tracking position changes
async function measureSheetAnimationDuration(
page: any,
triggerTestId: string,
frameTestId: string,
closeTestId: string
): Promise<number> {
const closeButton = page.getByTestId(closeTestId)
// click trigger and measure how long until the frame's visual position stabilizes
const duration = await page.evaluate(
async ({ triggerId, frameId }: { triggerId: string; frameId: string }) => {
return new Promise<number>((resolve) => {
const trigger = document.querySelector(
`[data-testid="${triggerId}"]`
) as HTMLElement
if (!trigger) {
resolve(-1)
return
}
const startTime = performance.now()
let initialTop = -1
let lastTop = -1
let stableCount = 0
let hasMovedFromInitial = false
let checkCount = 0
const maxChecks = 200 // ~10 seconds max
trigger.click()
const checkAnimation = () => {
checkCount++
const frame = document.querySelector(
`[data-testid="${frameId}"]`
) as HTMLElement
if (!frame) {
if (checkCount < maxChecks) {
requestAnimationFrame(checkAnimation)
} else {
resolve(performance.now() - startTime)
}
return
}
// use getBoundingClientRect which reflects parent transforms
const currentTop = Math.round(frame.getBoundingClientRect().top)
// record the initial position on first check
if (initialTop === -1) {
initialTop = currentTop
}
// wait until the position has actually changed from the initial value
// before we start counting stability (animation might not start for a frame or two)
if (!hasMovedFromInitial && currentTop !== initialTop) {
hasMovedFromInitial = true
}
if (hasMovedFromInitial && currentTop === lastTop) {
stableCount++
if (stableCount >= 3) {
resolve(performance.now() - startTime)
return
}
} else {
stableCount = 0
lastTop = currentTop
}
if (checkCount < maxChecks) {
requestAnimationFrame(checkAnimation)
} else {
resolve(performance.now() - startTime)
}
}
requestAnimationFrame(checkAnimation)
})
},
{ triggerId: triggerTestId, frameId: frameTestId }
)
// close the sheet for next measurement
await closeButton.scrollIntoViewIfNeeded()
await closeButton.click({ timeout: 5000 })
await page.waitForTimeout(1500)
return Math.round(duration)
}
// ============================================================================
// CSS DRIVER TESTS
// Note: CSS driver has a known limitation where animation names don't translate
// to Sheet animation timing (the Sheet uses useAnimatedNumber internally which
// CSS driver doesn't animate - it relies on CSS transitions for component styles)
// ============================================================================
test.describe('Sheet Animation - CSS Driver', () => {
test.beforeEach(async ({ page }) => {
await setupPage(page, {
name: 'SheetAnimationCase',
type: 'useCase',
searchParams: { animationDriver: 'css' },
})
})
test('sheet transform has CSS transition applied', async ({ page }) => {
// This test verifies that the CSS driver properly applies a CSS transition
// to the Sheet's transform property, enabling smooth open/close animations
const trigger = page.getByTestId('animation-quick-trigger')
const closeButton = page.getByTestId('animation-quick-close')
await trigger.click()
await page.waitForTimeout(100) // Small wait for sheet to start rendering
// The transform is on the AnimatedView wrapper, which is the parent of Sheet.Frame
// We need to find the element with position:absolute that wraps the frame
const debugInfo = await page.evaluate(() => {
const frame = document.querySelector('[data-testid="animation-quick-frame"]')
if (!frame) return { found: false, error: 'frame not found' }
// The AnimatedView is a parent with position:absolute and transform
let animatedView: Element | null = frame
for (let i = 0; i < 5; i++) {
animatedView = animatedView?.parentElement || null
if (!animatedView) break
const style = getComputedStyle(animatedView)
if (style.position === 'absolute' && style.transform !== 'none') {
break
}
}
if (!animatedView) {
return { found: false, error: 'animatedView not found' }
}
const style = getComputedStyle(animatedView)
// Get all style attributes for debugging
const allStyles: Record<string, string> = {}
for (let i = 0; i < style.length; i++) {
const prop = style[i]
if (prop.includes('transition') || prop.includes('transform')) {
allStyles[prop] = style.getPropertyValue(prop)
}
}
return {
found: true,
tagName: animatedView.tagName,
transition: style.transition,
transform: style.transform,
inlineStyle: (animatedView as HTMLElement).style.cssText,
allTransitionStyles: allStyles,
}
})
// Check that there's a transition that applies to transform
// CSS transition "0.1s ease-in" (without property name) means "all 0.1s ease-in"
// which applies to all properties including transform
const hasTransition =
debugInfo.found && debugInfo.transition && debugInfo.transition !== 'all'
expect(hasTransition).toBe(true)
await closeButton.click()
await page.waitForTimeout(600)
})
test('sheet actually animates (transform changes over time)', async ({ page }) => {
// This test verifies that the sheet's transform actually changes over time
// (i.e., it animates) rather than instantly snapping to position
const trigger = page.getByTestId('animation-lazy-trigger')
const closeButton = page.getByTestId('animation-lazy-close')
await trigger.click()
// capture transform values over the animation duration inside the browser.
// keeping the sampler in-page avoids playwright round-trip latency shifting
// the samples past the short animation window under parallel load.
type TransformSample = { t: number; transform: string }
const samples: TransformSample[] = await page.evaluate(
() =>
new Promise<TransformSample[]>((resolve) => {
const startedAt = performance.now()
const samples: TransformSample[] = []
const readTransform = () => {
const frame = document.querySelector('[data-testid="animation-lazy-frame"]')
if (!frame) return 'frame-not-found'
let el: Element | null = frame
for (let j = 0; j < 5; j++) {
el = el?.parentElement || null
if (!el) break
const style = getComputedStyle(el)
if (style.position === 'absolute' && style.transform !== 'none') {
return style.transform
}
}
return 'parent-not-found'
}
const sample = () => {
const elapsed = performance.now() - startedAt
samples.push({ t: elapsed, transform: readTransform() })
if (elapsed >= 650) {
resolve(samples)
return
}
requestAnimationFrame(sample)
}
requestAnimationFrame(sample)
})
)
const transforms = samples.map((sample) => sample.transform)
// Filter out errors and get unique values
const uniqueTransforms = [
...new Set(transforms.filter((t) => !t.includes('not-found') && t !== 'none')),
]
// If animation is working, we should see multiple distinct transform values
// as the sheet animates from bottom to its snap point
expect(uniqueTransforms.length).toBeGreaterThan(1)
await closeButton.click()
await page.waitForTimeout(600)
})
test('all sheet variants open and close correctly', async ({ page }) => {
// CSS driver doesn't support transitionConfig prop reliably - only test transition variants
const testIds = ['animation-quick', 'animation-lazy', 'animation-slow']
for (const testId of testIds) {
const trigger = page.getByTestId(`${testId}-trigger`)
// Use .first() because Sheet passes testId to both Sheet.Frame and SheetCover
const frame = page.getByTestId(`${testId}-frame`).first()
const closeButton = page.getByTestId(`${testId}-close`)
await trigger.click()
await expect(frame).toBeVisible({ timeout: 5000 })
await closeButton.click()
// Wait longer for slow animations to complete
await page.waitForTimeout(1500)
await expect(frame).not.toBeInViewport({ ratio: 0.5 })
}
})
// transitionConfig not working reliably with CSS driver
test.skip('transitionConfig prop works without animation prop', async ({ page }) => {
// Use .first() because Sheet passes testId to both Sheet.Frame and SheetCover
const frame = page.getByTestId('transitionConfig-only-frame').first()
const trigger = page.getByTestId('transitionConfig-only-trigger')
const closeButton = page.getByTestId('transitionConfig-only-close')
await trigger.click()
await expect(frame).toBeVisible({ timeout: 3000 })
// Sheet should open successfully with only transitionConfig
await closeButton.click()
await page.waitForTimeout(1500)
await expect(frame).not.toBeInViewport({ ratio: 0.5 })
})
})
// ============================================================================
// MOTION DRIVER TESTS
// ============================================================================
test.describe('Sheet Animation - Motion Driver', () => {
test.beforeEach(async ({ page }) => {
await setupPage(page, {
name: 'SheetAnimationCase',
type: 'useCase',
searchParams: { animationDriver: 'motion' },
})
})
test('all sheet variants open and close correctly', async ({ page }) => {
// Skip transitionConfig variants - not working reliably
const testIds = ['animation-quick', 'animation-lazy', 'animation-slow']
for (const testId of testIds) {
const trigger = page.getByTestId(`${testId}-trigger`)
// Use .first() because Sheet passes testId to both Sheet.Frame and SheetCover
const frame = page.getByTestId(`${testId}-frame`).first()
const closeButton = page.getByTestId(`${testId}-close`)
await trigger.click()
await expect(frame).toBeVisible({ timeout: 5000 })
await closeButton.click()
// Wait longer for slow animations to complete
await page.waitForTimeout(2000)
await expect(frame).not.toBeInViewport({ ratio: 0.5 })
}
})
test('transitionConfig prop works without animation prop', async ({ page }) => {
// Use .first() because Sheet passes testId to both Sheet.Frame and SheetCover
const frame = page.getByTestId('transitionConfig-only-frame').first()
const trigger = page.getByTestId('transitionConfig-only-trigger')
const closeButton = page.getByTestId('transitionConfig-only-close')
await trigger.click()
await expect(frame).toBeVisible({ timeout: 3000 })
await closeButton.click()
await page.waitForTimeout(2000)
await expect(frame).not.toBeInViewport({ ratio: 0.5 })
})
test('transitionConfig overrides transition prop', async ({ page }) => {
// transition="quick" (fast) + transitionConfig={type:'timing',duration:1000} (slow)
// if override works, the 1000ms config should make it measurably slower than the 100ms baseline
const baselineDuration = await measureSheetAnimationDuration(
page,
'transitionConfig-only-trigger',
'transitionConfig-only-frame',
'transitionConfig-only-close'
)
const overrideDuration = await measureSheetAnimationDuration(
page,
'animation-plus-config-trigger',
'animation-plus-config-frame',
'animation-plus-config-close'
)
console.info(
`Motion Driver - baseline (100ms): ${baselineDuration}ms, override (1000ms): ${overrideDuration}ms`
)
// the 1000ms override should be significantly slower than the 100ms baseline
// use absolute +250ms margin: CI inflates both measurements (spring settling + overhead)
// which compresses the ratio, but absolute difference stays well above 250ms
expect(overrideDuration).toBeGreaterThan(baselineDuration + 250)
})
})
// ============================================================================
// REANIMATED DRIVER TESTS (default)
// ============================================================================
test.describe('Sheet Animation - Reanimated Driver (default)', () => {
test.beforeEach(async ({ page }) => {
await setupPage(page, {
name: 'SheetAnimationCase',
type: 'useCase',
// No animationDriver param = default reanimated
})
})
test('all sheet variants open and close correctly', async ({ page }) => {
const testIds = ['animation-quick', 'animation-lazy', 'animation-slow']
for (const testId of testIds) {
const trigger = page.getByTestId(`${testId}-trigger`)
// Use .first() because Sheet passes testId to both Sheet.Frame and SheetCover
const frame = page.getByTestId(`${testId}-frame`).first()
const closeButton = page.getByTestId(`${testId}-close`)
await trigger.click()
await expect(frame).toBeVisible({ timeout: 5000 })
await closeButton.click()
// Wait longer for slow animations to complete
await page.waitForTimeout(2000)
await expect(frame).not.toBeInViewport({ ratio: 0.5 })
}
})
test('transitionConfig prop works', async ({ page }) => {
// transitionConfig only works with JS animation drivers, not CSS
const driver = (test.info().project?.metadata as any)?.animationDriver
test.skip(driver === 'css', 'transitionConfig not supported by CSS animation driver')
// Use .first() because Sheet passes testId to both Sheet.Frame and SheetCover
const frame = page.getByTestId('transitionConfig-only-frame').first()
const trigger = page.getByTestId('transitionConfig-only-trigger')
const closeButton = page.getByTestId('transitionConfig-only-close')
await trigger.click()
await expect(frame).toBeVisible({ timeout: 3000 })
await closeButton.click()
await page.waitForTimeout(2000)
await expect(frame).not.toBeInViewport({ ratio: 0.5 })
})
test('transitionConfig overrides transition prop', async ({ page }) => {
// transitionConfig only works with JS animation drivers, not CSS
const driver = (test.info().project?.metadata as any)?.animationDriver
test.skip(driver === 'css', 'transitionConfig not supported by CSS animation driver')
// transition="quick" (fast) + transitionConfig={type:'timing',duration:1000} (slow)
// if override works, the 1000ms config should make it measurably slower than the 100ms baseline
const baselineDuration = await measureSheetAnimationDuration(
page,
'transitionConfig-only-trigger',
'transitionConfig-only-frame',
'transitionConfig-only-close'
)
const overrideDuration = await measureSheetAnimationDuration(
page,
'animation-plus-config-trigger',
'animation-plus-config-frame',
'animation-plus-config-close'
)
console.info(
`Reanimated Driver - baseline (100ms): ${baselineDuration}ms, override (1000ms): ${overrideDuration}ms`
)
// the 1000ms override should be significantly slower than the 100ms baseline
// use absolute +250ms margin: CI inflates both measurements (spring settling + overhead)
// which compresses the ratio, but absolute difference stays well above 250ms
expect(overrideDuration).toBeGreaterThan(baselineDuration + 250)
})
})