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

162 lines
5.6 KiB
TypeScript

import { expect, test } from '@playwright/test'
import { setupPage } from './test-utils'
// the @hanzogui/animations-react-native driver (animationDriver=native) previously
// snapped Accordion.HeightAnimator open/closed: height was not in any animatable
// key map so it landed in the static style and jumped 0 -> full in one frame.
// layout dimension keys now animate on the JS driver. these assertions are
// behavioral: opening must pass through intermediate heights, closing must tween
// down to 0.
test('native driver: accordion height opens through intermediate frames and closes to 0', async ({
page,
}) => {
await setupPage(page, {
name: 'AccordionDefaultOpenCase',
type: 'useCase',
searchParams: { animationDriver: 'native' },
})
await page.waitForSelector('#def-trigger2', { timeout: 30_000 })
const result = await page.evaluate(async () => {
const trigger = document.getElementById('def-trigger2')
const wrapper = document.getElementById('def-height2')
if (!trigger || !wrapper) throw new Error('accordion test nodes missing')
const sampleFor = async (durationMs: number) => {
const heights: number[] = []
const startedAt = performance.now()
do {
await new Promise(requestAnimationFrame)
heights.push(wrapper.getBoundingClientRect().height)
} while (performance.now() - startedAt < durationMs)
return heights
}
trigger.click()
const opening = await sampleFor(600)
const openHeight = opening.at(-1) ?? 0
trigger.click()
const closing = await sampleFor(600)
return { opening, openHeight, closing }
})
// fully opens to a real height
expect(result.openHeight).toBeGreaterThan(10)
// opening is smooth: passes through heights strictly between 0 and the final,
// rather than a single jump straight to openHeight
expect(
result.opening.some((height) => height > 1 && height < result.openHeight - 1)
).toBe(true)
// no single frame covers the whole distance (would indicate a snap)
const maxOpenJump = result.opening
.slice(1)
.reduce((max, height, i) => Math.max(max, Math.abs(height - result.opening[i])), 0)
expect(maxOpenJump).toBeLessThan(result.openHeight - 1)
// closing tweens back down through intermediate heights and lands at 0
expect(
result.closing.some((height) => height > 1 && height < result.openHeight - 1)
).toBe(true)
expect(result.closing.at(-1)).toBeLessThanOrEqual(1)
})
// once an open item settles it releases its measured pixel height back to auto,
// so later content growth stays fluid. the native driver previously kept the
// settled height's Animated.Value forever, pinning the wrapper at the old pixel
// value: released-to-auto never took effect and growing the content stayed
// clipped. the driver now drops animated keys that left the incoming style.
test('native driver: settled accordion releases to auto and follows content growth', async ({
page,
}) => {
await setupPage(page, {
name: 'AccordionDefaultOpenCase',
type: 'useCase',
searchParams: { animationDriver: 'native' },
})
await page.waitForSelector('#def-trigger2', { timeout: 30_000 })
const wrapperHeight = () =>
page.$eval('#def-height2', (el) => Math.round(el.getBoundingClientRect().height))
const wrapperInlineHeight = () =>
page.$eval('#def-height2', (el) => (el as HTMLElement).style.height)
await page.click('#def-trigger2')
// open animation (300ms) + the 100ms quiet window before release to auto
await page.waitForTimeout(900)
const settledHeight = await wrapperHeight()
// released back to auto: no pinned inline pixel height remains
expect((await wrapperInlineHeight()).endsWith('px')).toBe(false)
expect(settledHeight).toBeGreaterThan(10)
// grow the content: an auto wrapper must follow the taller measurement
await page.click('#grow-content')
await page.waitForTimeout(700)
const grownHeight = await wrapperHeight()
expect(grownHeight).toBeGreaterThan(settledHeight + 5)
// shrink back: the wrapper follows down again
await page.click('#grow-content')
await page.waitForTimeout(700)
const shrunkHeight = await wrapperHeight()
expect(shrunkHeight).toBeLessThan(grownHeight - 5)
})
test('native driver: height and opacity animate together on one node', async ({
page,
}) => {
const pageErrors: Error[] = []
page.on('pageerror', (error) => pageErrors.push(error))
await setupPage(page, {
name: 'NativeMixedDriverCase',
type: 'useCase',
searchParams: { animationDriver: 'native' },
})
const node = page.locator('#native-mixed-driver-node')
await expect(node).toHaveCSS('height', '40px')
await expect(node).toHaveCSS('opacity', '1')
await page.click('#native-mixed-driver-toggle')
await expect(node).toHaveCSS('height', '160px')
await expect(node).toHaveCSS('opacity', '0')
await page.click('#native-mixed-driver-toggle')
await expect(node).toHaveCSS('height', '40px')
await expect(node).toHaveCSS('opacity', '1')
expect(pageErrors).toEqual([])
})
test('native driver: pseudo-only key survives a render and clears on release', async ({
page,
}) => {
await setupPage(page, {
name: 'NativeMixedDriverCase',
type: 'useCase',
searchParams: { animationDriver: 'native' },
})
const node = page.locator('#native-pseudo-only-node')
await expect(node).toHaveCSS('opacity', '1')
await node.hover()
await expect(node).toHaveCSS('opacity', '0.2')
await page.evaluate(() => {
document.getElementById('native-pseudo-only-rerender')?.click()
})
await expect(page.locator('#native-pseudo-only-render-count')).toHaveText('1')
await expect(node).toHaveCSS('opacity', '0.2')
await page.mouse.move(0, 0)
await expect(node).toHaveCSS('opacity', '1')
})