// @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( {}} 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( {}} 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(); await waitFor(() => screen.getByRole('button', { name: /원격 사용/ })); fireEvent.click(screen.getByRole('button', { name: /원격 사용/ })); await waitFor(() => { expect(onResolved).toHaveBeenCalled(); expect(onClose).toHaveBeenCalled(); }); }); it('각 conflict row 에 local/remote inline 설명 표시', async () => { render( {}} onResolved={() => {}} onOpenHelp={() => {}} />); await waitFor(() => screen.getByText(/local A/)); expect(screen.getAllByText(/이 기기의 변경을 보존/).length).toBeGreaterThanOrEqual(2); expect(screen.getAllByText(/원격의 변경을 가져오고/).length).toBeGreaterThanOrEqual(2); }); it('"자세히 보기" 클릭 → onOpenHelp("main-conflict") 호출', async () => { const onOpenHelp = vi.fn(); render( {}} onResolved={() => {}} onOpenHelp={onOpenHelp} />); await waitFor(() => screen.getByText(/local A/)); const links = screen.getAllByRole('button', { name: /자세히 보기/ }); fireEvent.click(links[0]!); expect(onOpenHelp).toHaveBeenCalledWith('main-conflict'); }); it('onOpenHelp 미제공 → "자세히 보기" 링크 미렌더', async () => { render( {}} onResolved={() => {}} />); await waitFor(() => screen.getByText(/local A/)); expect(screen.queryByRole('button', { name: /자세히 보기/ })).toBeNull(); }); });