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 ( Theme probe ) } 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() }) 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() }) 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( Fallback , ) }) 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( , ) await Promise.resolve() }) expect(loop).not.toHaveBeenCalled() act(() => renderer!.unmount()) loop.mockRestore() reduceMotion.mockRestore() }) })