mirror of
https://github.com/hanzoai/gui.git
synced 2026-10-25 02:31:41 +00:00
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]>
312 lines
11 KiB
TypeScript
312 lines
11 KiB
TypeScript
import { expect, test, type Page } from '@playwright/test'
|
|
import { setupPage } from './test-utils'
|
|
|
|
/**
|
|
* ENTER/EXIT TRANSITION TESTS
|
|
*
|
|
* Tests the transition prop's enter/exit-specific animations using timing animations.
|
|
* Supports syntax like: transition={{ enter: '500ms', exit: '100ms' }}
|
|
*
|
|
* These tests run across all animation drivers (css, native, reanimated, motion).
|
|
*/
|
|
|
|
const TOLERANCE = 0.2
|
|
|
|
async function getOpacity(page: Page, testId: string): Promise<number> {
|
|
return page.evaluate((id) => {
|
|
const el = document.querySelector(`[data-testid="${id}"]`)
|
|
if (!el) return -1
|
|
return Number.parseFloat(getComputedStyle(el).opacity)
|
|
}, testId)
|
|
}
|
|
|
|
async function elementExists(page: Page, testId: string): Promise<boolean> {
|
|
return page.evaluate((id) => !!document.querySelector(`[data-testid="${id}"]`), testId)
|
|
}
|
|
|
|
function isIntermediate(
|
|
value: number,
|
|
start: number,
|
|
end: number,
|
|
tolerance = TOLERANCE
|
|
): boolean {
|
|
const notAtStart = Math.abs(value - start) > tolerance
|
|
const notAtEnd = Math.abs(value - end) > tolerance
|
|
const min = Math.min(start, end)
|
|
const max = Math.max(start, end)
|
|
const inRange = value >= min - tolerance && value <= max + tolerance
|
|
return notAtStart && notAtEnd && inRange
|
|
}
|
|
|
|
// works in ci, flaky on local
|
|
test.fixme('Enter/Exit Transition Props', () => {
|
|
test.beforeEach(async ({ page }) => {
|
|
const driver = (test.info().project?.metadata as any)?.animationDriver
|
|
test.skip(driver === 'native', 'native driver has element detection issues on web')
|
|
|
|
await setupPage(page, {
|
|
name: 'AnimationComprehensiveCase',
|
|
type: 'useCase',
|
|
})
|
|
// wait for initial render and any enter animations to complete
|
|
// use longer timeout for CI environments which can be slower
|
|
await page.waitForTimeout(1500)
|
|
})
|
|
|
|
test('scenario 42: different enter/exit transitions - enter uses slow animation', async ({
|
|
page,
|
|
}) => {
|
|
expect(await elementExists(page, 'scenario-42-target'), 'Initially visible').toBe(
|
|
true
|
|
)
|
|
// wait for initial enter animation to complete (500ms animation + settling time)
|
|
await page.waitForFunction(
|
|
(testId) => {
|
|
const el = document.querySelector(`[data-testid="${testId}"]`)
|
|
return el && Number.parseFloat(getComputedStyle(el).opacity) >= 0.95
|
|
},
|
|
'scenario-42-target',
|
|
{ timeout: 3000 }
|
|
)
|
|
expect(await getOpacity(page, 'scenario-42-target'), 'Initial opacity').toBeCloseTo(
|
|
1,
|
|
1
|
|
)
|
|
|
|
// hide then show to test enter animation (enter=500ms)
|
|
await page.getByTestId('scenario-42-trigger').click()
|
|
await page.waitForTimeout(300) // exit=100ms + buffer
|
|
|
|
expect(await elementExists(page, 'scenario-42-target'), 'Hidden').toBe(false)
|
|
|
|
await page.getByTestId('scenario-42-trigger').click()
|
|
await page.waitForTimeout(50)
|
|
expect(await elementExists(page, 'scenario-42-target'), 'Should appear').toBe(true)
|
|
|
|
await page.waitForTimeout(150)
|
|
const midEnterOpacity = await getOpacity(page, 'scenario-42-target')
|
|
|
|
expect(
|
|
isIntermediate(midEnterOpacity, 0, 1),
|
|
`Mid-enter opacity (${midEnterOpacity.toFixed(2)}) should be intermediate at 200ms into 500ms`
|
|
).toBe(true)
|
|
|
|
await page.waitForTimeout(500)
|
|
expect(await getOpacity(page, 'scenario-42-target'), 'Final opacity').toBeCloseTo(
|
|
1,
|
|
1
|
|
)
|
|
})
|
|
|
|
test('scenario 42: different enter/exit transitions - exit uses fast animation', async ({
|
|
page,
|
|
}) => {
|
|
expect(await elementExists(page, 'scenario-42-target'), 'Initially visible').toBe(
|
|
true
|
|
)
|
|
|
|
// trigger exit (exit=100ms - fast)
|
|
await page.getByTestId('scenario-42-trigger').click()
|
|
await page.waitForTimeout(300) // 100ms + buffer
|
|
|
|
expect(await elementExists(page, 'scenario-42-target'), 'Gone after fast exit').toBe(
|
|
false
|
|
)
|
|
})
|
|
|
|
test('scenario 43: enter-only transition - enter uses specified, exit uses default', async ({
|
|
page,
|
|
}) => {
|
|
// enter=500ms, exit uses default=100ms
|
|
expect(await elementExists(page, 'scenario-43-target'), 'Initially visible').toBe(
|
|
true
|
|
)
|
|
|
|
// hide - uses default=100ms (fast)
|
|
await page.getByTestId('scenario-43-trigger').click()
|
|
await page.waitForTimeout(300)
|
|
expect(await elementExists(page, 'scenario-43-target'), 'Hidden').toBe(false)
|
|
|
|
// show - uses enter=500ms (slow)
|
|
await page.getByTestId('scenario-43-trigger').click()
|
|
await page.waitForTimeout(200)
|
|
const midOpacity = await getOpacity(page, 'scenario-43-target')
|
|
|
|
expect(
|
|
isIntermediate(midOpacity, 0, 1),
|
|
`Enter opacity (${midOpacity.toFixed(2)}) should be intermediate at 200ms into 500ms`
|
|
).toBe(true)
|
|
|
|
await page.waitForTimeout(500)
|
|
expect(await getOpacity(page, 'scenario-43-target'), 'Final').toBeCloseTo(1, 1)
|
|
})
|
|
|
|
test('scenario 44: exit-only transition - exit uses specified 500ms (slow)', async ({
|
|
page,
|
|
}) => {
|
|
// exit=500ms, enter uses default=100ms
|
|
expect(await elementExists(page, 'scenario-44-target'), 'Initially visible').toBe(
|
|
true
|
|
)
|
|
|
|
// trigger exit - uses 500ms (slow)
|
|
await page.getByTestId('scenario-44-trigger').click()
|
|
|
|
// at 200ms into a 500ms animation, should still be in progress
|
|
await page.waitForTimeout(200)
|
|
|
|
expect(
|
|
await elementExists(page, 'scenario-44-target'),
|
|
'Still exists during 500ms exit'
|
|
).toBe(true)
|
|
const midOpacity = await getOpacity(page, 'scenario-44-target')
|
|
|
|
// with 500ms exit, opacity should still be intermediate at 200ms
|
|
expect(
|
|
isIntermediate(midOpacity, 1, 0),
|
|
`Mid-exit opacity (${midOpacity.toFixed(2)}) should be intermediate at 200ms into 500ms`
|
|
).toBe(true)
|
|
|
|
// wait for completion (500ms + buffer)
|
|
await page.waitForTimeout(500)
|
|
expect(await elementExists(page, 'scenario-44-target'), 'Gone after 500ms exit').toBe(
|
|
false
|
|
)
|
|
})
|
|
|
|
test('scenario 45: enter/exit/default - property changes use default animation', async ({
|
|
page,
|
|
}) => {
|
|
// enter=300ms, exit=100ms, default=500ms for property changes
|
|
expect(await elementExists(page, 'scenario-45-target'), 'Initially visible').toBe(
|
|
true
|
|
)
|
|
const initialOpacity = await getOpacity(page, 'scenario-45-target')
|
|
expect(initialOpacity, 'Initial opacity').toBeCloseTo(1, 1)
|
|
|
|
// click prop button to change opacity (not enter/exit, so uses default=500ms)
|
|
await page.getByTestId('scenario-45-trigger-prop').click()
|
|
|
|
// at 150ms into 500ms animation, should be animating (not yet at final 0.5)
|
|
await page.waitForTimeout(150)
|
|
const midOpacity = await getOpacity(page, 'scenario-45-target')
|
|
|
|
// should still be above final value since animation takes 500ms
|
|
expect(
|
|
midOpacity,
|
|
`Mid opacity (${midOpacity.toFixed(2)}) should still be above 0.6`
|
|
).toBeGreaterThan(0.6)
|
|
|
|
// wait for completion (500ms animation + buffer)
|
|
await page.waitForTimeout(500)
|
|
expect(await getOpacity(page, 'scenario-45-target'), 'Final opacity').toBeCloseTo(
|
|
0.5,
|
|
1
|
|
)
|
|
})
|
|
|
|
test('scenario 46: enter/exit with per-property config - opacity uses its own animation', async ({
|
|
page,
|
|
}) => {
|
|
// enter=300ms for scale, exit=100ms for scale, but opacity always=500ms
|
|
// hide first
|
|
await page.getByTestId('scenario-46-trigger').click()
|
|
await page.waitForTimeout(700) // opacity=500ms + buffer
|
|
|
|
expect(await elementExists(page, 'scenario-46-target'), 'Hidden').toBe(false)
|
|
|
|
// show - opacity should use 500ms even though enter=300ms for other props
|
|
await page.getByTestId('scenario-46-trigger').click()
|
|
await page.waitForTimeout(200)
|
|
|
|
const midOpacity = await getOpacity(page, 'scenario-46-target')
|
|
|
|
// at 200ms into 500ms opacity animation, should be intermediate
|
|
expect(
|
|
isIntermediate(midOpacity, 0, 1),
|
|
`Mid-enter opacity (${midOpacity.toFixed(2)}) should be intermediate at 200ms into 500ms`
|
|
).toBe(true)
|
|
|
|
await page.waitForTimeout(500)
|
|
expect(await getOpacity(page, 'scenario-46-target'), 'Final').toBeCloseTo(1, 1)
|
|
})
|
|
|
|
test('scenario 47: enter/exit with delay - animations start after delay', async ({
|
|
page,
|
|
}) => {
|
|
// enter=300ms, exit=100ms, delay=200ms
|
|
// hide first to test enter animation with delay
|
|
await page.getByTestId('scenario-47-trigger').click()
|
|
await page.waitForTimeout(600) // 200ms delay + 100ms exit + buffer
|
|
|
|
expect(await elementExists(page, 'scenario-47-target'), 'Hidden').toBe(false)
|
|
|
|
// show - should have 200ms delay before enter animation starts
|
|
await page.getByTestId('scenario-47-trigger').click()
|
|
|
|
// element appears but at 100ms (during delay), should still be at enterStyle values
|
|
await page.waitForTimeout(100)
|
|
expect(
|
|
await elementExists(page, 'scenario-47-target'),
|
|
'Should appear immediately'
|
|
).toBe(true)
|
|
|
|
const duringDelayOpacity = await getOpacity(page, 'scenario-47-target')
|
|
// during 200ms delay, opacity should be near 0 (enterStyle)
|
|
expect(duringDelayOpacity, 'During delay, should be at enterStyle').toBeLessThan(0.3)
|
|
|
|
// after delay + some animation time (200ms delay + ~150ms into 300ms animation)
|
|
await page.waitForTimeout(250)
|
|
const afterDelayOpacity = await getOpacity(page, 'scenario-47-target')
|
|
// should be animating or completed by now
|
|
expect(afterDelayOpacity, 'After delay, should be animating').toBeGreaterThan(0.3)
|
|
|
|
// wait for full completion
|
|
await page.waitForTimeout(500)
|
|
expect(await getOpacity(page, 'scenario-47-target'), 'Final').toBeCloseTo(1, 1)
|
|
})
|
|
|
|
test('enter animation timing differs from exit timing', async ({ page }) => {
|
|
// scenario 42 uses enter=500ms, exit=100ms
|
|
const startTime = Date.now()
|
|
|
|
// trigger exit
|
|
await page.getByTestId('scenario-42-trigger').click()
|
|
|
|
// wait for element to disappear
|
|
await page.waitForFunction(
|
|
(testId) => !document.querySelector(`[data-testid="${testId}"]`),
|
|
'scenario-42-target',
|
|
{ timeout: 5000 }
|
|
)
|
|
|
|
const exitDuration = Date.now() - startTime
|
|
|
|
// exit should be fast (100ms + overhead)
|
|
expect(exitDuration, 'Exit should be fast').toBeLessThan(500)
|
|
|
|
// now trigger enter
|
|
const enterStart = Date.now()
|
|
await page.getByTestId('scenario-42-trigger').click()
|
|
|
|
// wait for element to fully appear
|
|
await page.waitForTimeout(100)
|
|
await page.waitForFunction(
|
|
(testId) => {
|
|
const el = document.querySelector(`[data-testid="${testId}"]`)
|
|
if (!el) return false
|
|
return Number.parseFloat(getComputedStyle(el).opacity) > 0.9
|
|
},
|
|
'scenario-42-target',
|
|
{ timeout: 5000 }
|
|
)
|
|
|
|
const enterDuration = Date.now() - enterStart
|
|
|
|
// enter should be slower than exit
|
|
expect(enterDuration, 'Enter should be slower than exit').toBeGreaterThan(
|
|
exitDuration
|
|
)
|
|
})
|
|
})
|