import React from 'react' import { act, create, type ReactTestRenderer } from 'react-test-renderer' import type { Meeting, UserTemplate } from '@d3ro/api-client' const mockListMeetingsPage = jest.fn() const mockLoadTemplateLibrary = jest.fn() const mockCreateMeetingWorkspaceWithMetadata = jest.fn() const mockNavigate = jest.fn() jest.mock('@react-navigation/native', () => { const ReactValue = require('react') as typeof React return { useFocusEffect: (callback: () => void | (() => void)) => { ReactValue.useEffect(callback, []) }, } }) 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', strong: '#666' }, accent: { main: '#7cf', dim: '#234' }, tag: { red: '#f66', green: '#6f6', orange: '#fa6' }, }, }), })) jest.mock('../src/theme/themed-components', () => { 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} ), } }) jest.mock('../src/features/meetings/meetings-service', () => { class MockMeetingServiceError extends Error { code: string constructor(mockCode: string, mockMessage: string) { super(mockMessage) this.code = mockCode } } return { MeetingServiceError: MockMeetingServiceError, createMeetingCreationIdempotencyKey: () => 'cccccccc-cccc-4ccc-8ccc-cccccccccccc', createMeetingWorkspaceWithMetadata: (...args: unknown[]) => ( mockCreateMeetingWorkspaceWithMetadata(...args) ), listMeetingsPage: (...args: unknown[]) => mockListMeetingsPage(...args), updateMeetingTitleRevisionSafe: jest.fn(), deleteMeetingRevisionSafe: jest.fn(), } }) jest.mock('../src/features/templates', () => ({ loadTemplateLibrary: (...args: unknown[]) => mockLoadTemplateLibrary(...args), })) import { TextInput } from 'react-native' import MeetingsScreen from '../src/screens/MeetingsScreen' const USER_ID = '11111111-1111-4111-8111-111111111111' const TEMPLATE_A_ID = 'aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa' const TEMPLATE_B_ID = 'bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb' const CREATION_KEY = 'cccccccc-cccc-4ccc-8ccc-cccccccccccc' const CREATED_MEETING_ID = 'dddddddd-dddd-4ddd-8ddd-dddddddddddd' function template(id: string, name: string): UserTemplate { return { id, user_id: USER_ID, template_kind: 'meeting_document', builtin_key: null, name, description: `${name} description`, fields: [], output_format: null, template_type: 'minutes', system_prompt: 'Write minutes.', is_builtin: false, revision: 1, created_at: '2026-08-21T00:00:00.000Z', updated_at: '2026-08-21T00:00:00.000Z', } } function createdMeeting(): Meeting { return { id: CREATED_MEETING_ID, user_id: USER_ID, team_id: null, title: 'Launch review', status: 'recording', started_at: '2026-08-21T00:00:00.000Z', ended_at: null, duration_ms: null, raw_transcript: null, edited_transcript: null, minutes_markdown: null, minutes_json: null, stt_model: null, llm_model: null, stt_latency_ms: null, llm_latency_ms: null, error_message: null, audio_storage_key: null, language: 'en', attendees: ['Yun Chan', 'Product Team'], template_id: TEMPLATE_B_ID, creation_idempotency_key: CREATION_KEY, creation_request_hash: 'a'.repeat(64), 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('meeting metadata creation screen', () => { beforeEach(() => { mockListMeetingsPage.mockReset() mockLoadTemplateLibrary.mockReset() mockCreateMeetingWorkspaceWithMetadata.mockReset() mockNavigate.mockReset() mockListMeetingsPage.mockResolvedValue({ meetings: [], total: 0, page: 0, pageSize: 30, hasMore: false, }) mockLoadTemplateLibrary.mockResolvedValue({ templates: [template(TEMPLATE_A_ID, 'Minutes'), template(TEMPLATE_B_ID, 'Report')], selections: { meeting_document: { template_id: TEMPLATE_A_ID } }, }) mockCreateMeetingWorkspaceWithMetadata.mockResolvedValue(createdMeeting()) }) test('submits title, attendees, language, and an explicit template then opens server readback', async () => { const navigation = { getParent: () => null, navigate: mockNavigate, } let renderer: ReactTestRenderer await act(async () => { renderer = create() }) await flush() await act(async () => { renderer!.root.findByProps({ testID: 'meetings-create' }).props.onPress() }) await flush() await act(async () => { renderer!.root.findByProps({ testID: 'meeting-title-input' }).props.onChangeText(' Launch review ') renderer!.root.findByProps({ testID: 'meeting-attendees-input' }).props.onChangeText( ' Yun Chan, Product Team ', ) renderer!.root.findByProps({ testID: 'meeting-language-en' }).props.onPress() renderer!.root.findByProps({ testID: `meeting-template-${TEMPLATE_B_ID}` }).props.onPress() }) expect(renderer!.root.findAllByType(TextInput)).toHaveLength(3) await act(async () => { renderer!.root.findByProps({ testID: 'meeting-editor-save' }).props.onPress() await Promise.resolve() }) await flush() expect(mockCreateMeetingWorkspaceWithMetadata).toHaveBeenCalledWith(USER_ID, { title: ' Launch review ', attendees: ['Yun Chan', 'Product Team'], language: 'en', templateId: TEMPLATE_B_ID, idempotencyKey: CREATION_KEY, }) expect(mockNavigate).toHaveBeenCalledWith('MeetingDetail', { meetingId: CREATED_MEETING_ID, }) expect(renderer!.root.findAllByProps({ testID: 'meeting-editor-modal' })).toHaveLength(0) await act(() => renderer!.unmount()) }) test('creates without a template when the explicit none option is selected', async () => { const navigation = { getParent: () => null, navigate: mockNavigate, } let renderer: ReactTestRenderer await act(async () => { renderer = create() }) await flush() await act(async () => { renderer!.root.findByProps({ testID: 'meetings-create' }).props.onPress() }) await flush() await act(async () => { renderer!.root.findByProps({ testID: 'meeting-title-input' }).props.onChangeText('Adhoc sync') renderer!.root.findByProps({ testID: 'meeting-template-none' }).props.onPress() }) await act(async () => { renderer!.root.findByProps({ testID: 'meeting-editor-save' }).props.onPress() await Promise.resolve() }) await flush() expect(mockCreateMeetingWorkspaceWithMetadata).toHaveBeenCalledWith(USER_ID, { title: 'Adhoc sync', attendees: [], language: 'ko', templateId: null, idempotencyKey: CREATION_KEY, }) await act(() => renderer!.unmount()) }) test('still creates when the template library cannot be loaded', async () => { mockLoadTemplateLibrary.mockRejectedValue(new Error('offline')) const navigation = { getParent: () => null, navigate: mockNavigate, } let renderer: ReactTestRenderer await act(async () => { renderer = create() }) await flush() await act(async () => { renderer!.root.findByProps({ testID: 'meetings-create' }).props.onPress() }) await flush() expect( renderer!.root.findAllByProps({ testID: 'meeting-templates-empty' }).length, ).toBeGreaterThanOrEqual(1) expect( renderer!.root.findAllByProps({ testID: 'meeting-editor-save' }).length, ).toBeGreaterThanOrEqual(1) await act(async () => { renderer!.root.findByProps({ testID: 'meeting-title-input' }).props.onChangeText('Offline meeting') }) await act(async () => { renderer!.root.findByProps({ testID: 'meeting-editor-save' }).props.onPress() await Promise.resolve() }) await flush() expect(mockCreateMeetingWorkspaceWithMetadata).toHaveBeenCalledWith(USER_ID, { title: 'Offline meeting', attendees: [], language: 'ko', templateId: null, idempotencyKey: CREATION_KEY, }) await act(() => renderer!.unmount()) }) })