- InboxApi.listNotes / listByStatus / search signature 에 notebookId? 옵션 추가 - countsByStatus 반환 타입에서 archived 제거 (active/completed/trashed 만) - inbox:list-by-status 핸들러: archived 수신 시 빈 배열 graceful fallback - inbox:counts-by-status 핸들러: notebookId opts 추가, archived 키 제거 - store.ts: countsByStatus 결과 spread 시 archived:0 fallback (Task 15/16 까지 UI 보존) - App.test.tsx: countsByStatus mock 에서 archived 제거 + 탭 count 기대값 보관(0) 으로 조정 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
190 lines
8.2 KiB
TypeScript
190 lines
8.2 KiB
TypeScript
// @vitest-environment jsdom
|
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
import '@testing-library/jest-dom/vitest';
|
|
import { render, screen, fireEvent, cleanup, waitFor } from '@testing-library/react';
|
|
|
|
vi.mock('../../src/renderer/inbox/api.js', () => ({
|
|
inboxApi: {
|
|
listNotes: vi.fn(async () => []),
|
|
listByStatus: vi.fn(async () => []),
|
|
countsByStatus: vi.fn(async () => ({ active: 0, completed: 0, trashed: 0 })),
|
|
getContinuity: vi.fn(async () => ({
|
|
weekStart: '', weekCount: 0, weekTarget: 7,
|
|
consecutiveCompleteWeeks: 0, showRecoveryToast: false, lastNoteAt: null
|
|
})),
|
|
getPendingCount: vi.fn(async () => 0),
|
|
getOllamaStatus: vi.fn(async () => ({ ok: true })),
|
|
getTodayCount: vi.fn(async () => 0),
|
|
getTrashCount: vi.fn(async () => 0),
|
|
listExpired: vi.fn(async () => []),
|
|
getFailedCount: vi.fn(async () => 0),
|
|
listRecallCandidate: vi.fn(async () => null),
|
|
onNoteUpdated: vi.fn(() => () => undefined),
|
|
onOllamaStatus: vi.fn(() => () => undefined),
|
|
onNavigate: vi.fn(() => () => undefined),
|
|
// 4 섹션 mounted 시 호출되는 stub
|
|
loadOllamaSettings: vi.fn(async () => ({ endpoint: '', model: '' })),
|
|
saveOllamaSettings: vi.fn(async () => ({ ok: true })),
|
|
ollamaRecheck: vi.fn(async () => ({ ok: true })),
|
|
getAutostart: vi.fn(async () => ({
|
|
openAtLogin: false,
|
|
diagnostic: {
|
|
withArgs: { openAtLogin: false, executableWillLaunchAtLogin: false },
|
|
noArgs: { openAtLogin: false, executableWillLaunchAtLogin: false },
|
|
execPath: '/p'
|
|
}
|
|
})),
|
|
setAutostart: vi.fn(async () => ({
|
|
openAtLogin: false,
|
|
diagnostic: {
|
|
withArgs: { openAtLogin: false, executableWillLaunchAtLogin: false },
|
|
noArgs: { openAtLogin: false, executableWillLaunchAtLogin: false },
|
|
execPath: '/p'
|
|
}
|
|
})),
|
|
runBackup: vi.fn(async () => ({ ok: true })),
|
|
runExport: vi.fn(async () => ({ ok: true })),
|
|
runImport: vi.fn(async () => ({ ok: true })),
|
|
runSync: vi.fn(async () => ({ ok: true })),
|
|
runExportTelemetry: vi.fn(async () => ({ ok: true })),
|
|
getAppInfo: vi.fn(async () => ({ version: '0.2.7', electron: '?', node: '?', os: '?', profileDir: '?' })),
|
|
openProfileDir: vi.fn(async () => undefined),
|
|
copyAppInfo: vi.fn(async () => undefined),
|
|
// v0.2.9 Cut B Task 12 — onboarding wizard 분기. default 는 onboarding_completed=true 라 wizard 미표시.
|
|
getSettings: vi.fn(async () => ({ onboarding_completed: true })),
|
|
setAiEnabled: vi.fn(async () => ({ ok: true as const })),
|
|
setOnboardingCompleted: vi.fn(async () => ({ ok: true as const })),
|
|
// v0.2.9 Cut B Task 16 — AiProviderSection 가 SettingsPage 렌더 시 호출.
|
|
getDisabledCount: vi.fn(async () => 0),
|
|
enqueueDisabled: vi.fn(async () => ({ count: 0 })),
|
|
// v0.3.0 Cut E — SyncSection 이 SettingsPage 에 마운트되어 호출.
|
|
getSyncStatus: vi.fn(async () => ({ lastAt: null, lastResult: null, nextAt: null })),
|
|
setSyncAutoEnabled: vi.fn(async () => ({ ok: true as const })),
|
|
setSyncIntervalMin: vi.fn(async () => ({ ok: true as const })),
|
|
configureSync: vi.fn(async () => ({ ok: true as const })),
|
|
testSyncConnection: vi.fn(async () => ({ ok: true as const })),
|
|
// v0.3.1 Cut F — VisionSection 이 AiProviderSection 에 마운트되어 호출.
|
|
getVisionModels: vi.fn(async () => ({ models: [], at: null, selected: null })),
|
|
setVisionModel: vi.fn(async () => ({ ok: true as const })),
|
|
refreshVisionCache: vi.fn(async () => ({ ok: true as const, models: [] }))
|
|
}
|
|
}));
|
|
|
|
import { App } from '../../src/renderer/inbox/App';
|
|
import { useInbox } from '../../src/renderer/inbox/store';
|
|
import { inboxApi } from '../../src/renderer/inbox/api.js';
|
|
|
|
describe('App — settings view', () => {
|
|
beforeEach(() => {
|
|
cleanup();
|
|
useInbox.setState({
|
|
view: 'inbox',
|
|
counts: { active: 0, completed: 0, archived: 0, trashed: 0 },
|
|
showSettings: false, showTrash: false,
|
|
notes: [], trashNotes: [], trashCount: 0
|
|
});
|
|
});
|
|
|
|
it('renders SettingsPage when showSettings=true', async () => {
|
|
useInbox.setState({ showSettings: true });
|
|
render(<App />);
|
|
expect(await screen.findByText('설정')).toBeInTheDocument();
|
|
expect(screen.getByText('AI 제공자')).toBeInTheDocument();
|
|
});
|
|
|
|
it('header gear icon click sets showSettings=true', async () => {
|
|
render(<App />);
|
|
fireEvent.click(await screen.findByLabelText('설정 열기'));
|
|
expect(useInbox.getState().showSettings).toBe(true);
|
|
});
|
|
|
|
it('inbox:navigate "settings" event sets showSettings=true', async () => {
|
|
const navHandlers: Array<(view: 'inbox' | 'trash' | 'settings') => void> = [];
|
|
vi.mocked(inboxApi.onNavigate).mockImplementation((cb) => {
|
|
navHandlers.push(cb);
|
|
return () => {
|
|
const i = navHandlers.indexOf(cb);
|
|
if (i >= 0) navHandlers.splice(i, 1);
|
|
};
|
|
});
|
|
render(<App />);
|
|
await waitFor(() => expect(navHandlers.length).toBeGreaterThan(0));
|
|
navHandlers.forEach((h) => h('settings'));
|
|
await waitFor(() => expect(useInbox.getState().showSettings).toBe(true));
|
|
});
|
|
});
|
|
|
|
describe('App header — 4 tabs', () => {
|
|
beforeEach(() => {
|
|
cleanup();
|
|
useInbox.setState({
|
|
view: 'inbox',
|
|
counts: { active: 5, completed: 3, archived: 2, trashed: 1 },
|
|
notes: [], trashNotes: [], trashCount: 0,
|
|
showTrash: false, showSettings: false
|
|
});
|
|
// loadInitial 이 비동기로 counts 를 덮어씀 — onboarding wizard async gate (Task 12) 도입
|
|
// 후 render 가 await 후 발생하므로 mock 의 countsByStatus 가 테스트 기대값을 반환하도록 갱신.
|
|
// v0.4 — countsByStatus 응답에서 archived 제거 (store 가 archived:0 fallback 추가).
|
|
vi.mocked(inboxApi.countsByStatus).mockResolvedValue({ active: 5, completed: 3, trashed: 1 });
|
|
});
|
|
|
|
it('renders 4 tabs with counts', async () => {
|
|
render(<App />);
|
|
expect(await screen.findByRole('tab', { name: /Inbox\(5\)/ })).toBeInTheDocument();
|
|
expect(screen.getByRole('tab', { name: /완료\(3\)/ })).toBeInTheDocument();
|
|
// v0.4 — archived count 는 IPC 응답에서 제거됨 → store 가 0 fallback. 보관 탭은 Task 15 에서 제거 예정.
|
|
expect(screen.getByRole('tab', { name: /보관\(0\)/ })).toBeInTheDocument();
|
|
expect(screen.getByRole('tab', { name: /휴지통\(1\)/ })).toBeInTheDocument();
|
|
});
|
|
|
|
it('clicking 완료 tab sets view=completed', async () => {
|
|
render(<App />);
|
|
fireEvent.click(await screen.findByRole('tab', { name: /완료/ }));
|
|
expect(useInbox.getState().view).toBe('completed');
|
|
});
|
|
|
|
it('aria-selected reflects current view', async () => {
|
|
useInbox.setState({ view: 'archived' });
|
|
render(<App />);
|
|
const archivedBtn = await screen.findByRole('tab', { name: /보관/ });
|
|
expect(archivedBtn.getAttribute('aria-selected')).toBe('true');
|
|
const inboxBtn = screen.getByRole('tab', { name: /Inbox/ });
|
|
expect(inboxBtn.getAttribute('aria-selected')).toBe('false');
|
|
});
|
|
|
|
it('inbox tab has aria-selected="true" when active', async () => {
|
|
render(<App />);
|
|
const inboxTab = await screen.findByRole('tab', { name: /Inbox/ });
|
|
expect(inboxTab).toHaveAttribute('aria-selected', 'true');
|
|
});
|
|
});
|
|
|
|
describe('App — onboarding wizard', () => {
|
|
beforeEach(() => {
|
|
cleanup();
|
|
useInbox.setState({
|
|
view: 'inbox',
|
|
counts: { active: 0, completed: 0, archived: 0, trashed: 0 },
|
|
showSettings: false, showTrash: false,
|
|
notes: [], trashNotes: [], trashCount: 0
|
|
});
|
|
// 각 테스트가 getSettings 의 default mock 을 직접 override.
|
|
vi.mocked(inboxApi.getSettings).mockResolvedValue({ onboarding_completed: true });
|
|
});
|
|
|
|
it('renders OnboardingWizard when onboarding_completed=false', async () => {
|
|
vi.mocked(inboxApi.getSettings).mockResolvedValue({ onboarding_completed: false });
|
|
render(<App />);
|
|
await screen.findByText(/Inkling 사용 시작/);
|
|
expect(screen.getByRole('dialog', { name: /시작 안내/ })).toBeInTheDocument();
|
|
});
|
|
|
|
it('does not render OnboardingWizard when onboarding_completed=true', async () => {
|
|
vi.mocked(inboxApi.getSettings).mockResolvedValue({ onboarding_completed: true });
|
|
render(<App />);
|
|
await screen.findByRole('tab', { name: /Inbox/ });
|
|
expect(screen.queryByText(/Inkling 사용 시작/)).toBeNull();
|
|
});
|
|
});
|