/** * `GET /api/steps` — what happens from a screen, as typed steps. * * Against a real index of a small Expo + React Native app, shaped to cross * every boundary the endpoint classifies: a screen whose handler (a * `useCallback`) calls a Swift method through an `RCT_EXTERN_MODULE` shim, * the Swift side sending an event the screen listens to, the listener calling * an API function that leaves the index (`client.post`), a store action in a * store file, and a navigation to a second screen behind a condition. The * pure layout is tested without an index in `ui-steps-model.test.ts`. */ import { describe, it, expect, beforeAll, afterAll } from 'vitest'; import * as fs from 'fs'; import * as os from 'os'; import * as path from 'path'; import { CodeGraph } from '../src'; import { initGrammars, loadAllGrammars } from '../src/extraction/grammars'; import { buildSteps, crossing, effectCategory, isStoreFile } from '../src/ui-server/api/steps'; let tmpDir: string; let cg: CodeGraph; function write(rel: string, content: string): void { const full = path.join(tmpDir, rel); fs.mkdirSync(path.dirname(full), { recursive: true }); fs.writeFileSync(full, content); } beforeAll(async () => { await initGrammars(); await loadAllGrammars(); tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-ui-steps-')); write('package.json', JSON.stringify({ name: 'app', dependencies: { expo: '52', 'expo-router': '4', 'react-native': '0.76' } })); write('src/app/_layout.tsx', 'export default function Layout() { return null }\n'); write('src/app/index.tsx', "import { router } from 'expo-router'\nexport default function Home() {\n return null\n}\n"); write( 'src/components/capture/capture-view.tsx', "import { NativeModules, NativeEventEmitter } from 'react-native'\n" + 'export const captureView = NativeModules.CaptureView\n' + 'export const nativeEmitter = new NativeEventEmitter(NativeModules.CaptureEvents)\n' ); write('src/api/client.ts', "import axios from 'axios'\nexport const client = axios.create({ baseURL: 'x' })\n"); write( 'src/api/frames.ts', "import { client } from './client'\n" + 'export async function uploadARCapture(uri: string) {\n' + " await client.post('/frames', { uri })\n" + " return client.get('/frames/status')\n" + '}\n' ); write( 'src/storage/capture.storage.ts', "import { create } from 'zustand'\n" + 'const useCaptureStorage = create((set) => ({\n' + ' zipUri: null,\n' + ' setZipUri: (zipUri: string) => set({ zipUri }),\n' + '}))\n' + 'export default useCaptureStorage\n' ); write( 'src/app/capture/review.tsx', "import { useCallback, useEffect } from 'react'\n" + "import { router } from 'expo-router'\n" + "import { captureView, nativeEmitter } from '../../components/capture/capture-view'\n" + "import { uploadARCapture } from '../../api/frames'\n" + "import useCaptureStorage from '../../storage/capture.storage'\n" + 'export default function ReviewScreen({ unlimited }: { unlimited: boolean }) {\n' + ' const setZipUri = useCaptureStorage((s) => s.setZipUri)\n' + ' const handleApprove = useCallback(() => {\n' + ' captureView.finalizeCaptureSession()\n' + ' }, [])\n' + ' const handleZipComplete = useCallback(async (data: { uri: string }) => {\n' + ' setZipUri(data.uri)\n' + ' await uploadARCapture(data.uri)\n' + " Alert.alert('Uploaded', data.uri, [{ text: 'OK' }])\n" + " if (unlimited) router.replace('/')\n" + ' }, [unlimited])\n' + ' useEffect(() => {\n' + " const sub = nativeEmitter.addListener('onZipComplete', handleZipComplete)\n" + ' return () => sub.remove()\n' + ' }, [handleZipComplete])\n' + ' const form = useForm({ onSubmit: () => handleSubmit() })\n' + ' function handleSubmit() {\n' + ' captureView.finalizeCaptureSession()\n' + ' }\n' + ' return