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]>
268 lines
7.3 KiB
TypeScript
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()
|
|
})
|
|
})
|