- m009 마이그레이션: notebooks.sort_order INTEGER 컬럼 추가, 기존 rows created_at 순으로 backfill - NotebookRepository.list ORDER BY sort_order ASC, name ASC 로 변경 - NotebookRepository.create 신규 노트북 sort_order = max+1 자동 할당 - NotebookRepository.reorder(id, direction) — swap transaction 으로 atomic 순서 변경 - IPC notebook:reorder 핸들러 등록, preload/shared types pass-through - 테스트 45개 추가 (m009, reorder 케이스 4, list ORDER BY, IPC 핸들러 2) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
120 lines
4.6 KiB
TypeScript
120 lines
4.6 KiB
TypeScript
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
|
|
vi.mock('electron', () => ({
|
|
default: { ipcMain: { handle: vi.fn(), on: vi.fn() } }
|
|
}));
|
|
|
|
import electron from 'electron';
|
|
import { registerNotebookApi } from '../../src/main/ipc/notebookApi.js';
|
|
|
|
function getHandler(channel: string): (...args: unknown[]) => unknown {
|
|
const handle = (electron.ipcMain as unknown as { handle: ReturnType<typeof vi.fn> }).handle;
|
|
const call = handle.mock.calls.find((c) => c[0] === channel);
|
|
if (!call) throw new Error(`channel ${channel} not registered`);
|
|
return call[1] as (...args: unknown[]) => unknown;
|
|
}
|
|
|
|
function makeRepo() {
|
|
return {
|
|
list: vi.fn(() => [] as never[]),
|
|
create: vi.fn(),
|
|
rename: vi.fn(),
|
|
setColor: vi.fn(),
|
|
delete: vi.fn(() => ({ ok: true })),
|
|
moveNote: vi.fn(),
|
|
findById: vi.fn(() => null),
|
|
reorder: vi.fn(() => ({ ok: true }))
|
|
};
|
|
}
|
|
|
|
describe('notebookApi IPC', () => {
|
|
beforeEach(() => {
|
|
(electron.ipcMain as unknown as { handle: ReturnType<typeof vi.fn> }).handle.mockClear();
|
|
});
|
|
|
|
it('notebook:list — repo.list 결과 반환', async () => {
|
|
const repo = makeRepo();
|
|
repo.list.mockReturnValue([{ id: '1', name: '기본', color: null, createdAt: 't', updatedAt: 't', noteCount: 0 }] as never);
|
|
registerNotebookApi({ repo: repo as never });
|
|
const h = getHandler('notebook:list');
|
|
const r = await h({}) as unknown[];
|
|
expect(r).toHaveLength(1);
|
|
});
|
|
|
|
it('notebook:create — UNIQUE 위반 시 ok:false reason="duplicate_name"', async () => {
|
|
const repo = makeRepo();
|
|
repo.create.mockImplementation(() => { throw new Error('UNIQUE constraint failed: notebooks.name'); });
|
|
registerNotebookApi({ repo: repo as never });
|
|
const h = getHandler('notebook:create');
|
|
const r = await h({}, { name: '회사' }) as { ok: boolean; reason?: string };
|
|
expect(r.ok).toBe(false);
|
|
expect(r.reason).toBe('duplicate_name');
|
|
});
|
|
|
|
it('notebook:create — 빈 이름 시 ok:false reason="empty_name"', async () => {
|
|
const repo = makeRepo();
|
|
registerNotebookApi({ repo: repo as never });
|
|
const h = getHandler('notebook:create');
|
|
const r = await h({}, { name: ' ' }) as { ok: boolean; reason?: string };
|
|
expect(r.ok).toBe(false);
|
|
expect(r.reason).toBe('empty_name');
|
|
expect(repo.create).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('notebook:rename — UNIQUE 위반 ok:false reason="duplicate_name"', async () => {
|
|
const repo = makeRepo();
|
|
repo.rename.mockImplementation(() => { throw new Error('UNIQUE constraint failed: notebooks.name'); });
|
|
registerNotebookApi({ repo: repo as never });
|
|
const h = getHandler('notebook:rename');
|
|
const r = await h({}, 'id1', '회사') as { ok: boolean; reason?: string };
|
|
expect(r.ok).toBe(false);
|
|
expect(r.reason).toBe('duplicate_name');
|
|
});
|
|
|
|
it('notebook:delete — has_notes 시 ok:false reason 전달', async () => {
|
|
const repo = makeRepo();
|
|
repo.delete.mockReturnValue({ ok: false, reason: 'has_notes' } as never);
|
|
registerNotebookApi({ repo: repo as never });
|
|
const h = getHandler('notebook:delete');
|
|
const r = await h({}, 'id1');
|
|
expect(r).toEqual({ ok: false, reason: 'has_notes' });
|
|
});
|
|
|
|
it('notebook:move-note — repo.moveNote 호출 + ok:true', async () => {
|
|
const repo = makeRepo();
|
|
registerNotebookApi({ repo: repo as never });
|
|
const h = getHandler('notebook:move-note');
|
|
const r = await h({}, 'n1', 'nb-2');
|
|
expect(repo.moveNote).toHaveBeenCalledWith('n1', 'nb-2');
|
|
expect(r).toEqual({ ok: true });
|
|
});
|
|
|
|
it('notebook:set-color — repo.setColor 호출 + ok:true', async () => {
|
|
const repo = makeRepo();
|
|
registerNotebookApi({ repo: repo as never });
|
|
const h = getHandler('notebook:set-color');
|
|
const r = await h({}, 'id1', '#fff');
|
|
expect(repo.setColor).toHaveBeenCalledWith('id1', '#fff');
|
|
expect(r).toEqual({ ok: true });
|
|
});
|
|
|
|
it('notebook:reorder — repo.reorder 호출 + ok:true 전달', async () => {
|
|
const repo = makeRepo();
|
|
repo.reorder.mockReturnValue({ ok: true } as never);
|
|
registerNotebookApi({ repo: repo as never });
|
|
const h = getHandler('notebook:reorder');
|
|
const r = await h({}, 'nb-1', 'up');
|
|
expect(repo.reorder).toHaveBeenCalledWith('nb-1', 'up');
|
|
expect(r).toEqual({ ok: true });
|
|
});
|
|
|
|
it('notebook:reorder — 첫 번째 항목 up 시 ok:false 전달', async () => {
|
|
const repo = makeRepo();
|
|
repo.reorder.mockReturnValue({ ok: false } as never);
|
|
registerNotebookApi({ repo: repo as never });
|
|
const h = getHandler('notebook:reorder');
|
|
const r = await h({}, 'nb-first', 'up');
|
|
expect(r).toEqual({ ok: false });
|
|
});
|
|
});
|