303 lines
9.6 KiB
TypeScript
303 lines
9.6 KiB
TypeScript
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<string, unknown>) => 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<Record<string, unknown>>) => (
|
|
<Text {...props}>{children}</Text>
|
|
),
|
|
ThemeCard: ({ children, ...props }: React.PropsWithChildren<Record<string, unknown>>) => (
|
|
<View {...props}>{children}</View>
|
|
),
|
|
ThemeButton: ({
|
|
label,
|
|
onPress,
|
|
disabled,
|
|
...props
|
|
}: {
|
|
label: string
|
|
onPress?: () => void
|
|
disabled?: boolean
|
|
[key: string]: unknown
|
|
}) => (
|
|
<Pressable {...props} disabled={disabled} onPress={onPress}>
|
|
<Text>{label}</Text>
|
|
</Pressable>
|
|
),
|
|
}
|
|
})
|
|
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<void> {
|
|
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(<MeetingsScreen navigation={navigation as never} />)
|
|
})
|
|
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(<MeetingsScreen navigation={navigation as never} />)
|
|
})
|
|
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(<MeetingsScreen navigation={navigation as never} />)
|
|
})
|
|
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())
|
|
})
|
|
})
|