import { readFileSync } from 'node:fs'; import { join } from 'node:path'; import { describe, expect, test } from 'bun:test'; const ROOT = process.cwd(); const EXPO_EXAMPLE_DIRS = [ 'examples/expo-showcase', 'examples/food_drink/restaurant', 'examples/shopping_commerce/marketplace', 'examples/shopping_commerce/storefront', 'examples/social_community/community-feed', 'examples/social_community/photo-social', 'examples/social_community/private-messaging', 'examples/social_community/visual-discovery', ] as const; const ROUTER_EXAMPLE_DIRS = EXPO_EXAMPLE_DIRS.filter( (directory) => directory !== 'examples/expo-showcase', ); const ZORA_PROVIDER_HOSTS = [ ['examples/basic-app', 'App.tsx'], ['examples/expo-showcase', 'App.tsx'], ...ROUTER_EXAMPLE_DIRS.map((directory) => [directory, 'app', '_layout.tsx'] as const), ] as const; const RNVI_PLUGINS = [ '@react-native-vector-icons/fontawesome', '@react-native-vector-icons/fontawesome5', '@react-native-vector-icons/fontawesome6', '@react-native-vector-icons/ionicons', ] as const; const CARET_SEMVER_RANGE = /^\^\d+\.\d+\.\d+$/u; const EXACT_SEMVER_VERSION = /^\d+\.\d+\.\d+$/u; const MINOR_WILDCARD_SEMVER_RANGE = /^\d+\.\d+\.x$/u; const TILDE_SEMVER_RANGE = /^~\d+\.\d+\.\d+$/u; function readJson(path: string): Record { const value: unknown = JSON.parse(readFileSync(path, 'utf8')); if (typeof value !== 'object' || value === null || Array.isArray(value)) { throw new Error(`Expected an object in ${path}.`); } return value as Record; } function readRecord(record: Record, key: string): Record { const value = Reflect.get(record, key); if (typeof value !== 'object' || value === null || Array.isArray(value)) { throw new Error(`Expected ${key} to be an object.`); } return value as Record; } function readValue(record: Record, key: string): unknown { return Reflect.get(record, key); } function readExamplePackage(directory: string): Record { return readJson(join(ROOT, directory, 'package.json')); } describe('portable ZORA package boundary', () => { test('uses the expected dependency range styles without Expo runtime peers', () => { const packageJson = readJson(join(ROOT, 'package.json')); const dependencies = readRecord(packageJson, 'dependencies'); const peers = readRecord(packageJson, 'peerDependencies'); const development = readRecord(packageJson, 'devDependencies'); expect(readValue(dependencies, '@ankhorage/surface')).toMatch(CARET_SEMVER_RANGE); expect(readValue(dependencies, '@ankhorage/contracts')).toMatch(CARET_SEMVER_RANGE); expect(peers.react).toMatch(EXACT_SEMVER_VERSION); expect(readValue(peers, 'react-native')).toMatch(MINOR_WILDCARD_SEMVER_RANGE); expect(readValue(peers, 'react-native-svg')).toBe('15.15.4'); expect(readValue(development, 'react-native-svg')).toBe('15.15.4'); expect(readValue(peers, 'react-native-web')).toMatch(TILDE_SEMVER_RANGE); expect(readValue(peers, 'react-native-gesture-handler')).toMatch(TILDE_SEMVER_RANGE); expect(readValue(peers, 'react-native-reanimated')).toMatch(EXACT_SEMVER_VERSION); expect(readValue(peers, 'react-native-worklets')).toMatch(EXACT_SEMVER_VERSION); expect(development.typescript).toMatch(TILDE_SEMVER_RANGE); for (const expoPackage of ['@expo/vector-icons', 'expo-font', 'expo-linear-gradient']) { expect(readValue(peers, expoPackage)).toBeUndefined(); expect(readValue(development, expoPackage)).toBeUndefined(); } }); test('contains no Expo runtime import or direct React Native layout API', () => { const runtimeSource = readFileSync( join(ROOT, 'src', 'features', 'gradient', 'adapters', 'inbound', 'Gradient.tsx'), 'utf8', ); const shellSource = readFileSync( join(ROOT, 'src', 'features', 'layout', 'adapters', 'inbound', 'AppShell.tsx'), 'utf8', ); expect(runtimeSource).not.toContain("from 'expo"); expect(shellSource).not.toContain("from 'react-native'"); expect(shellSource).not.toContain('absoluteFillObject'); expect(shellSource).toContain('pointerEvents="box-none"'); }); }); describe('Expo 57 example boundary', () => { test('uses Renovate-safe dependency range styles across every Expo example', () => { const expoVersions = new Set(); for (const directory of EXPO_EXAMPLE_DIRS) { const packageJson = readExamplePackage(directory); const dependencies = readRecord(packageJson, 'dependencies'); const development = readRecord(packageJson, 'devDependencies'); const expoVersion = readValue(dependencies, 'expo'); expect(readValue(dependencies, '@ankhorage/zora')).toMatch(CARET_SEMVER_RANGE); expect(expoVersion).toMatch(EXACT_SEMVER_VERSION); expect(dependencies.react).toMatch(EXACT_SEMVER_VERSION); expect(readValue(dependencies, 'react-native')).toMatch(EXACT_SEMVER_VERSION); expect(readValue(dependencies, 'react-native-gesture-handler')).toMatch(TILDE_SEMVER_RANGE); expect(readValue(dependencies, 'react-native-reanimated')).toMatch(EXACT_SEMVER_VERSION); expect(readValue(dependencies, 'react-native-worklets')).toMatch(EXACT_SEMVER_VERSION); expect(readValue(dependencies, 'react-native-web')).toMatch(TILDE_SEMVER_RANGE); expect(development.typescript).toMatch(TILDE_SEMVER_RANGE); expect(readValue(dependencies, '@expo/vector-icons')).toBeUndefined(); if (typeof expoVersion === 'string') { expoVersions.add(expoVersion); } } expect(expoVersions.size).toBe(1); const [expoVersion] = expoVersions; const scaffoldSource = readFileSync( join(ROOT, 'scripts', 'scaffold-zora-example-app.ts'), 'utf8', ); expect(scaffoldSource).toContain(`expo: '${expoVersion}',`); expect(scaffoldSource).toContain("navigator: '^3.2.3'"); expect(scaffoldSource).toContain("from 'expo-router/unstable-native-tabs'"); expect(scaffoldSource).toContain(''); expect(scaffoldSource).not.toContain('ZoraTabBar'); }); test('uses Navigator native tabs in every router example', () => { for (const directory of ROUTER_EXAMPLE_DIRS) { const packageJson = readExamplePackage(directory); const dependencies = readRecord(packageJson, 'dependencies'); const layoutSource = readFileSync( join(ROOT, directory, 'app', '(tabs)', '_layout.tsx'), 'utf8', ); expect(readValue(dependencies, '@ankhorage/navigator')).toMatch(CARET_SEMVER_RANGE); expect(layoutSource).toContain("from '@ankhorage/navigator/tabs/native-icons'"); expect(layoutSource).toContain("from 'expo-router/unstable-native-tabs'"); expect(layoutSource).toContain("Platform.OS === 'web'"); expect(layoutSource).toContain('Promise.resolve(null)'); expect(layoutSource).toContain(''); expect(layoutSource).not.toContain('ZoraTabBar'); expect(layoutSource).not.toContain('ZoraNavigationRouteMap'); } }); test('keeps GestureHandlerRootView outside ZoraProvider in every host app', () => { for (const sourcePath of ZORA_PROVIDER_HOSTS) { const [directory] = sourcePath; const source = readFileSync(join(ROOT, ...sourcePath), 'utf8'); const packageJson = readExamplePackage(directory); const dependencies = readRecord(packageJson, 'dependencies'); const gestureRootPosition = source.indexOf(' { for (const directory of EXPO_EXAMPLE_DIRS) { const appJson = readJson(join(ROOT, directory, 'app.json')); const expo = readRecord(appJson, 'expo'); const { plugins } = expo; expect(Array.isArray(plugins)).toBe(true); for (const plugin of RNVI_PLUGINS) { expect(plugins).toContain(plugin); } } }); test('loads every icon font face exercised by Web acceptance', () => { const webFontSource = readFileSync( join(ROOT, 'examples', 'expo-showcase', 'useZoraIconFonts.web.ts'), 'utf8', ); expect(webFontSource).toContain('Ionicons.ttf'); expect(webFontSource).toContain('FontAwesome.ttf'); expect(webFontSource).toContain('FontAwesome5_Brands.ttf'); expect(webFontSource).toContain('FontAwesome5_Solid.ttf'); expect(webFontSource).toContain('FontAwesome6_Brands.ttf'); }); });