d3ro-voice/apps/mobile-rn/__tests__/ui-native-theme.test.tsx
2026-08-29 18:33:45 +09:00

133 lines
4.5 KiB
TypeScript

import React from 'react'
import { readFileSync } from 'node:fs'
import { resolve } from 'node:path'
import ReactTestRenderer, { act } from 'react-test-renderer'
import { AccessibilityInfo, Animated, StyleSheet, View } from 'react-native'
import {
MetalCard,
NativeThemeProvider,
PhosphorText,
PhysicalButton,
WaveBars,
d3roNativePalette,
} from '@d3ro/ui-native'
import { getMobileThemePalette } from '../src/theme/mobile-theme'
const THEME_PARITY_SCREENS = [
'AccountScreen.tsx',
'LoginScreen.tsx',
'SignUpScreen.tsx',
'ForgotPasswordScreen.tsx',
'UpdatePasswordScreen.tsx',
'HistoryScreen.tsx',
'HistoryDetailScreen.tsx',
'RecordScreen.tsx',
'ProPaywallScreen.tsx',
] as const
function hostByTestId(
root: ReactTestRenderer.ReactTestInstance,
testID: string,
): ReactTestRenderer.ReactTestInstance {
const matches = root.findAllByProps({ testID })
const host = matches[matches.length - 1]
if (host === undefined) throw new Error(`Missing host node: ${testID}`)
return host
}
function ThemeFixture({ mode }: { mode: 'light' | 'dark' }): React.ReactElement {
const palette = getMobileThemePalette(mode)
return (
<NativeThemeProvider palette={palette}>
<MetalCard testID="theme-card">
<PhosphorText testID="theme-text" color="primary">
Theme probe
</PhosphorText>
<PhysicalButton testID="theme-button" label="Continue" />
</MetalCard>
</NativeThemeProvider>
)
}
describe('ui-native runtime theme boundary', () => {
test.each(THEME_PARITY_SCREENS)(
'%s resolves colors from mobile preferences at render time',
(screenName) => {
const source = readFileSync(
resolve(__dirname, '..', 'src', 'screens', screenName),
'utf8',
)
expect(source).toContain('useMobilePreferences')
expect(source).not.toContain('d3roNativePalette')
expect(source).not.toMatch(/const\s+styles\s*=\s*StyleSheet\.create/)
},
)
test('updates shared component colors immediately when the provider palette changes', () => {
let renderer: ReactTestRenderer.ReactTestRenderer
act(() => {
renderer = ReactTestRenderer.create(<ThemeFixture mode="dark" />)
})
const dark = getMobileThemePalette('dark')
const darkText = hostByTestId(renderer!.root, 'theme-text')
const darkCard = hostByTestId(renderer!.root, 'theme-card')
const darkButton = hostByTestId(renderer!.root, 'theme-button')
expect(StyleSheet.flatten(darkText.props.style).color).toBe(dark.text.primary)
expect(StyleSheet.flatten(darkCard.props.style).backgroundColor).toBe(dark.bg.card)
expect(StyleSheet.flatten(darkButton.props.style).backgroundColor).toBe(dark.accent.main)
act(() => {
renderer!.update(<ThemeFixture mode="light" />)
})
const light = getMobileThemePalette('light')
const lightText = hostByTestId(renderer!.root, 'theme-text')
const lightCard = hostByTestId(renderer!.root, 'theme-card')
const lightButton = hostByTestId(renderer!.root, 'theme-button')
expect(StyleSheet.flatten(lightText.props.style).color).toBe(light.text.primary)
expect(StyleSheet.flatten(lightCard.props.style).backgroundColor).toBe(light.bg.card)
expect(StyleSheet.flatten(lightButton.props.style).backgroundColor).toBe(light.accent.main)
act(() => renderer!.unmount())
})
test('keeps the legacy fallback usable outside a provider', () => {
let renderer: ReactTestRenderer.ReactTestRenderer
act(() => {
renderer = ReactTestRenderer.create(
<View>
<PhosphorText testID="fallback-text">Fallback</PhosphorText>
</View>,
)
})
const fallback = hostByTestId(renderer!.root, 'fallback-text')
expect(fallback.props.children).toBe('Fallback')
expect(StyleSheet.flatten(fallback.props.style).color).toBe(
d3roNativePalette.text.primary,
)
act(() => renderer!.unmount())
})
test('keeps waveform motion stopped when the OS requests reduced motion', async () => {
const reduceMotion = jest
.spyOn(AccessibilityInfo, 'isReduceMotionEnabled')
.mockResolvedValue(true)
const loop = jest.spyOn(Animated, 'loop')
let renderer: ReactTestRenderer.ReactTestRenderer
await act(async () => {
renderer = ReactTestRenderer.create(
<NativeThemeProvider palette={getMobileThemePalette('dark')}>
<WaveBars active barCount={2} />
</NativeThemeProvider>,
)
await Promise.resolve()
})
expect(loop).not.toHaveBeenCalled()
act(() => renderer!.unmount())
loop.mockRestore()
reduceMotion.mockRestore()
})
})