also looking at this
feat: finish native customization and platform seams
#401
21 files
+707
-17
| @@ -1,7 +1,7 @@ | ||
| 1 | import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'node:fs' | |
| 1 | import { existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, rmSync, writeFileSync } from 'node:fs' | |
| 2 | 2 | import { join } from 'node:path' |
| 3 | 3 | import { afterAll, describe, expect, it } from 'bun:test' |
| 4 | import { ANDROID_MIN_SDK, build, init, installRuntime, renderAndroidDeepLinks, renderAndroidPermissions, resolveRuntimeDir, syncAndroidWebAssets, syncNativeFonts } from './index' | |
| 4 | import { ANDROID_MIN_SDK, build, init, installRuntime, renderAndroidDeepLinks, renderAndroidPermissions, resolveRuntimeDir, runPostGenerateHook, syncAndroidWebAssets, syncNativeFonts } from './index' | |
| 5 | 5 | import { createTempDir, removeAllTempDirs } from '../../typescript/src/temp-dir' |
| 6 | 6 | |
| 7 | 7 | // Every scratch directory this file made goes when the file is done (#357). |
| @@ -15,6 +15,56 @@ function generatedFiles(path: string): string[] { | ||
| 15 | 15 | } |
| 16 | 16 | |
| 17 | 17 | describe('Craft Android builder', () => { |
| 18 | it('runs a declared postGenerate command from the project root with metadata', async () => { | |
| 19 | const output = createTempDir('craft-android-post-generate-') | |
| 20 | await init({ name: 'Hook App', output, runtimeDir: null, config: { | |
| 21 | postGenerate: { command: 'sh', args: ['-c', 'printf "%s|%s|%s" "$PWD" "$CRAFT_PLATFORM" "$CRAFT_GENERATE_PHASE" > hook.txt'] }, | |
| 22 | } }) | |
| 23 | const config = JSON.parse(readFileSync(join(output, 'craft.config.json'), 'utf8')) | |
| 24 | expect(config.postGenerate).toEqual({ command: 'sh', args: ['-c', 'printf "%s|%s|%s" "$PWD" "$CRAFT_PLATFORM" "$CRAFT_GENERATE_PHASE" > hook.txt'] }) | |
| 25 | runPostGenerateHook(config.postGenerate, output) | |
| 26 | expect(readFileSync(join(output, 'hook.txt'), 'utf8')).toBe(`${realpathSync(output)}|android|build`) | |
| 27 | }) | |
| 28 | ||
| 29 | it('runs the postGenerate hook during the Android build before compilation', async () => { | |
| 30 | const output = createTempDir('craft-android-post-generate-build-') | |
| 31 | await init({ name: 'Hook Build', output, runtimeDir: null, config: { | |
| 32 | postGenerate: { command: 'sh', args: ['-c', 'printf "%s" "$CRAFT_GENERATE_PHASE" > hook-phase.txt'] }, | |
| 33 | } }) | |
| 34 | await build({ output, compile: false }) | |
| 35 | expect(readFileSync(join(output, 'hook-phase.txt'), 'utf8')).toBe('build') | |
| 36 | }) | |
| 37 | ||
| 38 | it('reports invalid Android postGenerate commands and non-zero exits', () => { | |
| 39 | const output = createTempDir('craft-android-post-generate-errors-') | |
| 40 | expect(() => runPostGenerateHook({ command: '' }, output)).toThrow('postGenerate.command must be a non-empty executable') | |
| 41 | expect(() => runPostGenerateHook({ command: join(output, 'missing-hook') }, output)).toThrow('postGenerate hook could not start') | |
| 42 | expect(() => runPostGenerateHook({ command: 'sh', args: ['-c', 'printf "patch failed" >&2; exit 7'] }, output)) | |
| 43 | .toThrow('postGenerate hook failed') | |
| 44 | }) | |
| 45 | ||
| 46 | it('generates static tab-bar selection appearance and validates its shape', async () => { | |
| 47 | const output = createTempDir('craft-android-tab-bar-appearance-') | |
| 48 | await init({ name: 'Tab Bar', output, runtimeDir: null, config: { | |
| 49 | renderer: 'native', | |
| 50 | tabBar: { selection: { cornerRadius: 8, color: '#123456' } }, | |
| 51 | } }) | |
| 52 | const config = JSON.parse(readFileSync(join(output, 'craft.config.json'), 'utf8')) | |
| 53 | const shell = readFileSync(join(output, 'app/src/main/java/com/craft/tabbar/CraftNativeShell.kt'), 'utf8') | |
| 54 | const activity = readFileSync(join(output, 'app/src/main/java/com/craft/tabbar/MainActivity.kt'), 'utf8') | |
| 55 | expect(config.tabBar).toEqual({ selection: { cornerRadius: 8, color: '#123456' } }) | |
| 56 | expect(shell).toContain('data class CraftNativeTabBarAppearance') | |
| 57 | expect(shell).toContain('private fun tabCellBackground(index: Int, dark: Boolean)') | |
| 58 | expect(activity).toContain('routes.tabBarAppearance') | |
| 59 | await expect(init({ name: 'Invalid Tab Bar', output: join(output, 'invalid'), runtimeDir: null, config: { | |
| 60 | renderer: 'native', tabBar: { selection: { cornerRadius: -1 } }, | |
| 61 | } })).rejects.toThrow('cornerRadius must be a finite non-negative number') | |
| 62 | await expect(init({ name: 'Invalid Tab Color', output: join(output, 'invalid-color'), runtimeDir: null, config: { | |
| 63 | renderer: 'native', tabBar: { selection: { color: 'red' } }, | |
| 64 | } })).rejects.toThrow('color must be a #RRGGBB color') | |
| 65 | }) | |
| 66 | ||
| 67 | ||
| 18 | 68 | it('bundles custom fonts with their licenses and removes retired fonts', async () => { |
| 19 | 69 | const root = createTempDir('craft-native-fonts-') |
| 20 | 70 | const font = join(root, 'Signature.ttf') |