feat(release): prepare 1.1.0 candidate
This commit is contained in:
parent
5a34f66981
commit
5205dcdfa9
736 changed files with 115667 additions and 12203 deletions
303
apps/mobile-rn/__tests__/meeting-creation-screen.test.tsx
Normal file
303
apps/mobile-rn/__tests__/meeting-creation-screen.test.tsx
Normal file
|
|
@ -0,0 +1,303 @@
|
|||
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())
|
||||
})
|
||||
})
|
||||
Loading…
Add table
Add a link
Reference in a new issue