Files
gui/pkgs/ui/components-test/Button.native.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

268 lines
7.3 KiB
TypeScript

import { getDefaultGuiConfig } from '@hanzogui/config-default'
import { Button } from '@hanzogui/button'
import { GuiProvider, View, createGui, styled } from '@hanzogui/core'
import {
getGestureHandler,
unstable_claimExternalPressOwnership,
unstable_hasExternalPressOwnership,
unstable_releaseExternalPressOwnership,
} from '@hanzogui/native'
import type { ReactNode } from 'react'
import TestRenderer, { act } from 'react-test-renderer'
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'
const conf = createGui(getDefaultGuiConfig())
const GESTURE_ENABLED_FREEZE_KEY = '__hanzogui_gesture_enabled_freeze__'
function createGestureStub() {
const gesture: any = {}
for (const method of [
'runOnJS',
'maxDuration',
'minDuration',
'manualActivation',
'hitSlop',
'onBegin',
'onStart',
'onEnd',
'onFinalize',
'onTouchesDown',
'onTouchesMove',
'onTouchesUp',
'onTouchesCancelled',
]) {
gesture[method] = () => gesture
}
return gesture
}
function resetGestureHandlerFreeze() {
delete (globalThis as any)[GESTURE_ENABLED_FREEZE_KEY]
}
function setGestureHandlerEnabled(
enabled: boolean,
GestureDetector?: any,
GestureOverrides?: Record<string, any>
) {
getGestureHandler().set({
enabled,
GestureDetector: enabled ? GestureDetector : null,
Gesture: enabled
? {
Tap: createGestureStub,
LongPress: createGestureStub,
Manual: createGestureStub,
Exclusive: (...gestures: any[]) => gestures[0],
...GestureOverrides,
}
: null,
ScrollView: null,
RootView: null,
})
}
async function renderButton(element: React.ReactElement) {
let rendered: TestRenderer.ReactTestRenderer | null = null
await act(async () => {
rendered = TestRenderer.create(
<GuiProvider config={conf} defaultTheme="light">
{element}
</GuiProvider>
)
})
return rendered!
}
function findWrappedText(rendered: TestRenderer.ReactTestRenderer) {
const nodes = rendered.root.findAll((node) => node.props.children === 'HELLO')
const textNode = nodes.find((node) => {
const type = node.type
return type === 'Text' || (typeof type === 'function' && type.name === 'Text')
})
if (!textNode) {
throw new Error('wrapped text node not found')
}
return textNode
}
function flattenStyle(style: any): Record<string, any> {
if (Array.isArray(style)) {
return Object.assign({}, ...style.map(flattenStyle))
}
return style || {}
}
beforeEach(() => {
resetGestureHandlerFreeze()
setGestureHandlerEnabled(false)
})
afterEach(() => {
vi.useRealTimers()
vi.restoreAllMocks()
resetGestureHandlerFreeze()
setGestureHandlerEnabled(false)
})
describe('Button native text props', () => {
test('passes maxFontSizeMultiplier from root props to wrapped text', async () => {
const rendered = await renderButton(<Button maxFontSizeMultiplier={1}>HELLO</Button>)
expect(findWrappedText(rendered).props.maxFontSizeMultiplier).toBe(1)
})
test('passes zero maxFontSizeMultiplier to wrapped text', async () => {
const rendered = await renderButton(<Button maxFontSizeMultiplier={0}>HELLO</Button>)
expect(findWrappedText(rendered).props.maxFontSizeMultiplier).toBe(0)
})
test('lets textProps override root text props on wrapped text', async () => {
const rendered = await renderButton(
<Button maxFontSizeMultiplier={2} textProps={{ maxFontSizeMultiplier: 1 }}>
HELLO
</Button>
)
expect(findWrappedText(rendered).props.maxFontSizeMultiplier).toBe(1)
})
test('passes styled text defaults to wrapped text', async () => {
const CappedButton = styled(Button, {
maxFontSizeMultiplier: 1,
})
const rendered = await renderButton(<CappedButton>HELLO</CappedButton>)
expect(findWrappedText(rendered).props.maxFontSizeMultiplier).toBe(1)
})
test('passes root text props to explicit Button.Text children', async () => {
const rendered = await renderButton(
<Button maxFontSizeMultiplier={1}>
<Button.Text>HELLO</Button.Text>
</Button>
)
expect(findWrappedText(rendered).props.maxFontSizeMultiplier).toBe(1)
})
test('passes Button.Apply text props to explicit Button.Text children', async () => {
const rendered = await renderButton(
<Button.Apply maxFontSizeMultiplier={1}>
<Button>
<Button.Text>HELLO</Button.Text>
</Button>
</Button.Apply>
)
expect(findWrappedText(rendered).props.maxFontSizeMultiplier).toBe(1)
})
test('does not pass cursor style to native text', async () => {
const rendered = await renderButton(<Button>HELLO</Button>)
expect(flattenStyle(findWrappedText(rendered).props.style).cursor).toBeUndefined()
})
test('uses responder events for real press handlers when RNGH is enabled', async () => {
vi.useFakeTimers()
const GestureDetector = ({ children }: { children: ReactNode }) => {
return children
}
let tapGestureCalls = 0
setGestureHandlerEnabled(true, GestureDetector, {
Tap: () => {
tapGestureCalls += 1
return createGestureStub()
},
})
const onPressIn = vi.fn()
const onPress = vi.fn()
const onPressOut = vi.fn()
const rendered = await renderButton(
<View
width={10}
height={10}
minPressDuration={0}
onPressIn={onPressIn}
onPress={onPress}
onPressOut={onPressOut}
/>
)
expect(tapGestureCalls).toBe(0)
const responderNode = rendered.root.find(
(node) =>
typeof node.props.onResponderGrant === 'function' &&
typeof node.props.onResponderRelease === 'function'
)
await act(async () => {
responderNode.props.onResponderGrant({})
responderNode.props.onResponderRelease({})
vi.runAllTimers()
})
expect(onPressIn).toHaveBeenCalledTimes(1)
expect(onPress).toHaveBeenCalledTimes(1)
expect(onPressOut).toHaveBeenCalledTimes(1)
})
test('responder press fallback respects external press ownership', async () => {
vi.useFakeTimers()
setGestureHandlerEnabled(true, ({ children }: { children: ReactNode }) => children)
const onPressIn = vi.fn()
const onPress = vi.fn()
const onPressOut = vi.fn()
const rendered = await renderButton(
<Button
minPressDuration={0}
onPressIn={onPressIn}
onPress={onPress}
onPressOut={onPressOut}
>
HELLO
</Button>
)
const responderNodes = rendered.root.findAll(
(node) =>
typeof node.props.onStartShouldSetResponder === 'function' &&
typeof node.props.onResponderGrant === 'function' &&
typeof node.props.onResponderRelease === 'function'
)
const owner = unstable_claimExternalPressOwnership('button-test')
expect(unstable_hasExternalPressOwnership()).toBe(true)
const responderNode = responderNodes.find(
(node) => node.props.onStartShouldSetResponder({}) === false
)
expect(responderNode).toBeTruthy()
await act(async () => {
responderNode!.props.onResponderGrant({})
unstable_releaseExternalPressOwnership(owner, 'button-test')
responderNode!.props.onResponderRelease({})
vi.runAllTimers()
})
expect(onPressIn).not.toHaveBeenCalled()
expect(onPress).not.toHaveBeenCalled()
expect(onPressOut).not.toHaveBeenCalled()
})
})