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
237
apps/mobile-rn/__tests__/templates-memos-screens.test.tsx
Normal file
237
apps/mobile-rn/__tests__/templates-memos-screens.test.tsx
Normal file
|
|
@ -0,0 +1,237 @@
|
|||
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<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' },
|
||||
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<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>
|
||||
),
|
||||
__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<void> {
|
||||
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(<TemplatesScreen />)
|
||||
})
|
||||
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(<MemosScreen />)
|
||||
})
|
||||
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())
|
||||
})
|
||||
})
|
||||
Loading…
Add table
Add a link
Reference in a new issue