final code review (Opus) 발견 2 important issues: 1. SyncConflict.noteId 가 실제로 export filename slug (date-id8-slug) 였음 — UUID 가 아니라 git checkout path 의 stem. 명명 혼동 → 'path' 로 rename (실제 의미와 일치). 2. ConflictModal preview 가 항상 빈 문자열이라 사용자가 비교 없이 local/remote 선택해야 했음. runSync 의 conflict 분기에서 `git show :2:<path>` (ours) + `:3:<path>` (theirs) 호출 추가하여 localText/remoteText 채움. 영향: - SyncService.SyncConflict + shared/types.ts.SyncConflict: noteId → path - SyncService.resolveConflict(path, choice) — 'notes/...md' 그대로 받음 - pathToNoteId 헬퍼 제거 (불필요) - ConflictModal: c.noteId → c.path, busy 상태 + 표시 모두 path 키 - IPC handler / preload bridge / InboxApi 시그니처 모두 path 로 통일 - SyncService.bidirectional/resolveConflict/sync-ipc/ConflictModal 4 test 갱신 regression 회귀 패턴 검사: rename 후 NoteRepository / SyncService / IPC / UI 의 모든 conflict-related path 일관 (typecheck 0).
62 lines
2.5 KiB
TypeScript
62 lines
2.5 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';
|
|
import React from 'react';
|
|
|
|
const { mockListConflicts, mockResolveConflict } = vi.hoisted(() => ({
|
|
mockListConflicts: vi.fn(),
|
|
mockResolveConflict: vi.fn()
|
|
}));
|
|
|
|
vi.mock('../../src/renderer/inbox/api.js', () => ({
|
|
inboxApi: { listConflicts: mockListConflicts, resolveConflict: mockResolveConflict }
|
|
}));
|
|
|
|
import { ConflictModal } from '../../src/renderer/inbox/components/ConflictModal';
|
|
|
|
describe('ConflictModal', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
cleanup();
|
|
mockListConflicts.mockResolvedValue([
|
|
{ path: 'notes/n1.md', localText: 'local A', remoteText: 'remote A' },
|
|
{ path: 'notes/n2.md', localText: 'local B', remoteText: 'remote B' }
|
|
]);
|
|
mockResolveConflict.mockResolvedValue({ ok: true });
|
|
});
|
|
|
|
it('open 시 listConflicts 호출 + 양 conflict preview 표시', async () => {
|
|
render(<ConflictModal onClose={() => {}} onResolved={() => {}} />);
|
|
await waitFor(() => screen.getByText(/local A/));
|
|
expect(screen.getByText(/local A/)).toBeInTheDocument();
|
|
expect(screen.getByText(/remote A/)).toBeInTheDocument();
|
|
expect(screen.getByText(/local B/)).toBeInTheDocument();
|
|
// path 가 표시됨 (Cut E final review fix — noteId → path)
|
|
expect(screen.getByText('notes/n1.md')).toBeInTheDocument();
|
|
});
|
|
|
|
it('내 것 사용 클릭 → resolveConflict(path, "local") 호출', async () => {
|
|
render(<ConflictModal onClose={() => {}} onResolved={() => {}} />);
|
|
await waitFor(() => screen.getByText(/local A/));
|
|
const buttons = screen.getAllByRole('button', { name: /내 것 사용/ });
|
|
fireEvent.click(buttons[0]!);
|
|
await waitFor(() => {
|
|
expect(mockResolveConflict).toHaveBeenCalledWith('notes/n1.md', 'local');
|
|
});
|
|
});
|
|
|
|
it('마지막 conflict 해결 → onResolved + onClose 호출', async () => {
|
|
mockListConflicts.mockResolvedValueOnce([{ path: 'notes/n1.md', localText: 'a', remoteText: 'b' }]);
|
|
const onResolved = vi.fn();
|
|
const onClose = vi.fn();
|
|
render(<ConflictModal onClose={onClose} onResolved={onResolved} />);
|
|
await waitFor(() => screen.getByRole('button', { name: /원격 사용/ }));
|
|
fireEvent.click(screen.getByRole('button', { name: /원격 사용/ }));
|
|
await waitFor(() => {
|
|
expect(onResolved).toHaveBeenCalled();
|
|
expect(onClose).toHaveBeenCalled();
|
|
});
|
|
});
|
|
});
|