feat(release): prepare 1.1.0 candidate

This commit is contained in:
Yun Chan 2026-08-29 18:33:45 +09:00
parent 5a34f66981
commit 5205dcdfa9
736 changed files with 115667 additions and 12203 deletions

View 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())
})
})