import React from 'react' import { act, create, type ReactTestRenderer } from 'react-test-renderer' import type { HistoryEntry, UserTemplate } from '@d3ro/api-client' const mockLoadTemplateLibrary = jest.fn() const mockSelectTemplate = jest.fn() const mockSearchMemos = jest.fn() const mockAddMemoTag = jest.fn() jest.mock('@react-navigation/native', () => ({ useIsFocused: () => true })) jest.mock('react-native-safe-area-context', () => ({ useSafeAreaInsets: () => ({ top: 0, bottom: 0, left: 0, right: 0 }) })) jest.mock('@d3ro/i18n', () => ({ useI18n: () => ({ t: (key: string, params?: Record) => params === undefined ? key : `${key}:${JSON.stringify(params)}`, formatDate: () => '2026-08-21', }), })) jest.mock('../src/lib/auth-context', () => ({ useAuth: () => ({ user: { id: '11111111-1111-4111-8111-111111111111' }, session: null, loading: false, }), })) jest.mock('../src/lib/preferences-context', () => ({ useMobilePreferences: () => ({ palette: { bg: { app: '#000', sidebar: '#111', card: '#181818', inset: '#202020' }, text: { primary: '#fff', muted: '#999' }, border: { default: '#444', subtle: '#333' }, accent: { main: '#7cf', dim: '#234' }, tag: { red: '#f66' }, }, }), })) jest.mock('../src/theme/themed-components', () => { const ReactValue = require('react') as typeof React const { Pressable, Text, View } = require('react-native') as typeof import('react-native') return { ThemeText: ({ children, ...props }: React.PropsWithChildren>) => ( {children} ), ThemeCard: ({ children, ...props }: React.PropsWithChildren>) => ( {children} ), ThemeButton: ({ label, onPress, disabled, ...props }: { label: string onPress?: () => void disabled?: boolean [key: string]: unknown }) => ( {label} ), __ReactValue: ReactValue, } }) jest.mock('../src/features/templates', () => { class MockTemplateServiceError extends Error { code: string constructor(mockCode: string, mockMessage: string) { super(mockMessage) this.code = mockCode } } return { TemplateServiceError: MockTemplateServiceError, loadTemplateLibrary: (...args: unknown[]) => mockLoadTemplateLibrary(...args), selectTemplate: (...args: unknown[]) => mockSelectTemplate(...args), subscribeToTemplates: () => ({ unsubscribe: async () => undefined }), createTemplate: jest.fn(), updateTemplate: jest.fn(), deleteTemplate: jest.fn(), normalizeTemplateDraft: jest.fn(() => ({})), } }) jest.mock('../src/features/memos', () => { class MockMemoServiceError extends Error { code: string constructor(mockCode: string, mockMessage: string) { super(mockMessage) this.code = mockCode } } const normalizeMemoTag = (value: string) => { const tag = value.trim().replace(/\s+/g, ' ') if (tag.length === 0) throw new MockMemoServiceError('validation', 'invalid') return { tag, normalizedTag: tag.toLowerCase() } } return { MemoServiceError: MockMemoServiceError, normalizeMemoTag, searchMemos: (...args: unknown[]) => mockSearchMemos(...args), addMemoTag: (...args: unknown[]) => mockAddMemoTag(...args), removeMemoTag: jest.fn(async () => undefined), renameMemoTag: jest.fn(async () => 1), buildMemoShareText: jest.fn(() => 'share'), subscribeToMemoTags: () => ({ unsubscribe: async () => undefined }), } }) import TemplatesScreen from '../src/screens/TemplatesScreen' import MemosScreen from '../src/screens/MemosScreen' import { TemplateServiceError } from '../src/features/templates' import { MemoServiceError } from '../src/features/memos' const USER_ID = '11111111-1111-4111-8111-111111111111' function template(): UserTemplate { return { id: 'aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa', user_id: USER_ID, template_kind: 'dictation', builtin_key: 'builtin-email', name: 'Email', description: 'Email template', fields: [{ id: 'body', name: 'body', label: 'Body', promptText: 'Body?', required: true, maxDurationSec: 120 }], output_format: '{{body}}', template_type: null, system_prompt: null, is_builtin: true, revision: 1, created_at: '2026-08-21T00:00:00.000Z', updated_at: '2026-08-21T00:00:00.000Z', } } function history(): HistoryEntry { return { id: 'bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb', user_id: USER_ID, title: 'Launch note', original_text: 'Ship after checks', polished_text: null, focused_app: null, focused_app_name: null, focused_app_window_title: null, mode: 'dictation', status: 'completed', error_code: null, audio_storage_key: null, duration: 1, detected_language: 'en', mic_device: null, word_count: 3, stt_model: null, llm_model: null, stt_latency_ms: null, llm_latency_ms: null, app_version: '1.0.0', summary_text: null, is_favorite: false, revision: 1, created_at: '2026-08-21T00:00:00.000Z', updated_at: '2026-08-21T00:00:00.000Z', } } async function flush(): Promise { await act(async () => { await Promise.resolve() await Promise.resolve() }) } describe('template and memo screens', () => { beforeEach(() => { mockLoadTemplateLibrary.mockReset() mockSelectTemplate.mockReset() mockSearchMemos.mockReset() mockAddMemoTag.mockReset() }) it('renders template loading data and rolls an optimistic selection back on conflict', async () => { mockLoadTemplateLibrary.mockResolvedValue({ templates: [template()], selections: {} }) mockSelectTemplate.mockRejectedValue(new TemplateServiceError('conflict', 'conflict')) let renderer: ReactTestRenderer await act(async () => { renderer = create() }) await flush() expect(renderer!.root.findByProps({ testID: `template-${template().id}` })).toBeTruthy() await act(async () => { renderer!.root.findByProps({ testID: `template-select-${template().id}` }).props.onPress() await Promise.resolve() }) await flush() expect(mockSelectTemplate).toHaveBeenCalledTimes(1) const text = renderer!.root.findAllByType(require('react-native').Text) .map((node) => node.props.children).flat().join(' ') expect(text).toContain('mobile.templates.error.conflict') expect(text).not.toContain('mobile.templates.selected') await act(async () => renderer!.unmount()) }) it('renders memo data and rolls an optimistic tag create back when the server rejects it', async () => { mockSearchMemos.mockResolvedValue({ items: [{ history: history(), tags: [] }], tagSummaries: [], }) mockAddMemoTag.mockRejectedValue(new MemoServiceError('network', 'offline')) let renderer: ReactTestRenderer await act(async () => { renderer = create() }) await flush() const memoId = history().id await act(async () => { renderer!.root.findByProps({ testID: `memo-add-tag-${memoId}` }).props.onPress() }) const editor = renderer!.root.findByProps({ testID: `memo-tag-editor-${memoId}` }) const input = editor.findByType(require('react-native').TextInput) await act(async () => input.props.onChangeText('Launch')) await act(async () => { renderer!.root.findByProps({ testID: `memo-tag-save-${memoId}` }).props.onPress() await Promise.resolve() }) await flush() expect(mockAddMemoTag).toHaveBeenCalledWith(USER_ID, memoId, 'Launch') const text = renderer!.root.findAllByType(require('react-native').Text) .map((node) => node.props.children).flat().join(' ') expect(text).toContain('mobile.memos.error.network') expect(text).not.toContain('#Launch') await act(async () => renderer!.unmount()) }) })