327 lines
12 KiB
TypeScript
327 lines
12 KiB
TypeScript
import { describe, it, expect, vi } from 'vitest';
|
|
import {
|
|
fetchBoards,
|
|
fetchBoardWithColumns,
|
|
createBoard,
|
|
updateBoard,
|
|
deleteBoard,
|
|
createColumn,
|
|
updateColumn,
|
|
deleteColumn,
|
|
createCard,
|
|
updateCard,
|
|
deleteCard,
|
|
moveCard,
|
|
subscribeToBoard,
|
|
} from './kanban';
|
|
|
|
// ── Supabase mock builder ────────────────────────────────────────────────────
|
|
|
|
function mockChain(resolvedValue: { data: any; error: any }) {
|
|
const chain: any = {};
|
|
const methods = ['from', 'select', 'insert', 'update', 'delete', 'eq', 'in', 'order', 'single'];
|
|
for (const m of methods) {
|
|
chain[m] = vi.fn(() => chain);
|
|
}
|
|
chain.single = vi.fn(() => Promise.resolve(resolvedValue));
|
|
chain.then = (resolve: any) => resolve(resolvedValue);
|
|
return chain;
|
|
}
|
|
|
|
function mockSupabase(resolvedValue: { data: any; error: any }) {
|
|
const chain = mockChain(resolvedValue);
|
|
return { from: vi.fn(() => chain), _chain: chain } as any;
|
|
}
|
|
|
|
// ── Boards ───────────────────────────────────────────────────────────────────
|
|
|
|
describe('fetchBoards', () => {
|
|
it('returns boards for an org', async () => {
|
|
const boards = [{ id: 'b1', name: 'Board 1', org_id: 'o1' }];
|
|
const sb = mockSupabase({ data: boards, error: null });
|
|
expect(await fetchBoards(sb, 'o1')).toEqual(boards);
|
|
});
|
|
|
|
it('returns empty array when null', async () => {
|
|
const sb = mockSupabase({ data: null, error: null });
|
|
expect(await fetchBoards(sb, 'o1')).toEqual([]);
|
|
});
|
|
|
|
it('throws on error', async () => {
|
|
const sb = mockSupabase({ data: null, error: { message: 'fail' } });
|
|
await expect(fetchBoards(sb, 'o1')).rejects.toEqual({ message: 'fail' });
|
|
});
|
|
});
|
|
|
|
describe('fetchBoardWithColumns', () => {
|
|
it('returns board with columns and cards', async () => {
|
|
const board = { id: 'b1', name: 'Board', org_id: 'o1' };
|
|
const columns = [{ id: 'c1', board_id: 'b1', name: 'To Do', position: 0 }];
|
|
const cards = [{ id: 'k1', column_id: 'c1', title: 'Task', position: 0, assignee_id: null }];
|
|
|
|
const chain: any = {};
|
|
const methods = ['from', 'select', 'eq', 'in', 'order', 'single'];
|
|
for (const m of methods) {
|
|
chain[m] = vi.fn(() => chain);
|
|
}
|
|
|
|
// Track calls: board.single, columns.order (thenable), cards.order (thenable), then tags/checklists/profiles
|
|
let singleIdx = 0;
|
|
chain.single = vi.fn(() => {
|
|
singleIdx++;
|
|
if (singleIdx === 1) return Promise.resolve({ data: board, error: null });
|
|
return Promise.resolve({ data: null, error: null });
|
|
});
|
|
|
|
let thenIdx = 0;
|
|
chain.then = (resolve: any) => {
|
|
thenIdx++;
|
|
if (thenIdx === 1) return resolve({ data: columns, error: null }); // columns
|
|
if (thenIdx === 2) return resolve({ data: cards, error: null }); // cards
|
|
return resolve({ data: [], error: null }); // tags, checklists, profiles
|
|
};
|
|
|
|
const sb = { from: vi.fn(() => chain) } as any;
|
|
const result = await fetchBoardWithColumns(sb, 'b1');
|
|
|
|
expect(result).not.toBeNull();
|
|
expect(result!.id).toBe('b1');
|
|
expect(result!.columns).toHaveLength(1);
|
|
expect(result!.columns[0].cards).toHaveLength(1);
|
|
});
|
|
|
|
it('returns board with empty columns when no columns exist', async () => {
|
|
const board = { id: 'b1', name: 'Board', org_id: 'o1' };
|
|
const chain: any = {};
|
|
const methods = ['from', 'select', 'eq', 'in', 'order', 'single'];
|
|
for (const m of methods) {
|
|
chain[m] = vi.fn(() => chain);
|
|
}
|
|
|
|
let singleIdx = 0;
|
|
chain.single = vi.fn(() => {
|
|
singleIdx++;
|
|
if (singleIdx === 1) return Promise.resolve({ data: board, error: null });
|
|
return Promise.resolve({ data: null, error: null });
|
|
});
|
|
|
|
chain.then = (resolve: any) => resolve({ data: [], error: null }); // empty columns
|
|
|
|
const sb = { from: vi.fn(() => chain) } as any;
|
|
const result = await fetchBoardWithColumns(sb, 'b1');
|
|
|
|
expect(result).not.toBeNull();
|
|
expect(result!.columns).toEqual([]);
|
|
});
|
|
|
|
it('returns null when board not found', async () => {
|
|
const chain: any = {};
|
|
const methods = ['from', 'select', 'eq', 'in', 'order', 'single'];
|
|
for (const m of methods) {
|
|
chain[m] = vi.fn(() => chain);
|
|
}
|
|
|
|
chain.single = vi.fn(() => Promise.resolve({ data: null, error: null }));
|
|
chain.then = (resolve: any) => resolve({ data: [], error: null });
|
|
|
|
const sb = { from: vi.fn(() => chain) } as any;
|
|
const result = await fetchBoardWithColumns(sb, 'b1');
|
|
expect(result).toBeNull();
|
|
});
|
|
|
|
it('throws when board fetch fails', async () => {
|
|
const chain: any = {};
|
|
const methods = ['from', 'select', 'eq', 'order', 'single', 'in'];
|
|
for (const m of methods) {
|
|
chain[m] = vi.fn(() => chain);
|
|
}
|
|
chain.single = vi.fn(() => {
|
|
return Promise.resolve({ data: null, error: { message: 'board fail' } });
|
|
});
|
|
chain.then = (resolve: any) => resolve({ data: [], error: null });
|
|
const sb = { from: vi.fn(() => chain) } as any;
|
|
|
|
await expect(fetchBoardWithColumns(sb, 'b1')).rejects.toEqual({ message: 'board fail' });
|
|
});
|
|
|
|
it('throws when columns fetch fails', async () => {
|
|
const board = { id: 'b1', name: 'Board' };
|
|
const chain: any = {};
|
|
const methods = ['from', 'select', 'eq', 'in', 'order', 'single'];
|
|
for (const m of methods) {
|
|
chain[m] = vi.fn(() => chain);
|
|
}
|
|
|
|
chain.single = vi.fn(() => Promise.resolve({ data: board, error: null }));
|
|
chain.then = (resolve: any) => resolve({ data: null, error: { message: 'col fail' } });
|
|
|
|
const sb = { from: vi.fn(() => chain) } as any;
|
|
await expect(fetchBoardWithColumns(sb, 'b1')).rejects.toEqual({ message: 'col fail' });
|
|
});
|
|
});
|
|
|
|
describe('subscribeToBoard', () => {
|
|
it('sets up realtime subscription and returns channel', () => {
|
|
const channel: any = {};
|
|
channel.on = vi.fn(() => channel);
|
|
channel.subscribe = vi.fn(() => channel);
|
|
const sb = { channel: vi.fn(() => channel) } as any;
|
|
|
|
const result = subscribeToBoard(sb, 'b1', ['c1'], vi.fn(), vi.fn());
|
|
expect(sb.channel).toHaveBeenCalledWith('kanban:b1');
|
|
expect(channel.on).toHaveBeenCalledTimes(2);
|
|
expect(channel.subscribe).toHaveBeenCalledOnce();
|
|
expect(result).toBe(channel);
|
|
});
|
|
});
|
|
|
|
describe('createBoard', () => {
|
|
it('creates board and default columns', async () => {
|
|
const board = { id: 'b1', name: 'New Board', org_id: 'o1' };
|
|
const sb = mockSupabase({ data: board, error: null });
|
|
const result = await createBoard(sb, 'o1', 'New Board');
|
|
expect(result).toEqual(board);
|
|
// Should have called from('kanban_columns') for default columns
|
|
expect(sb.from).toHaveBeenCalledWith('kanban_columns');
|
|
});
|
|
|
|
it('throws on error', async () => {
|
|
const sb = mockSupabase({ data: null, error: { message: 'fail' } });
|
|
await expect(createBoard(sb, 'o1', 'X')).rejects.toEqual({ message: 'fail' });
|
|
});
|
|
});
|
|
|
|
describe('updateBoard', () => {
|
|
it('updates without error', async () => {
|
|
const sb = mockSupabase({ data: null, error: null });
|
|
await expect(updateBoard(sb, 'b1', 'Renamed')).resolves.toBeUndefined();
|
|
});
|
|
|
|
it('throws on error', async () => {
|
|
const sb = mockSupabase({ data: null, error: { message: 'fail' } });
|
|
await expect(updateBoard(sb, 'b1', 'X')).rejects.toEqual({ message: 'fail' });
|
|
});
|
|
});
|
|
|
|
describe('deleteBoard', () => {
|
|
it('deletes without error', async () => {
|
|
const sb = mockSupabase({ data: null, error: null });
|
|
await expect(deleteBoard(sb, 'b1')).resolves.toBeUndefined();
|
|
});
|
|
|
|
it('throws on error', async () => {
|
|
const sb = mockSupabase({ data: null, error: { message: 'fail' } });
|
|
await expect(deleteBoard(sb, 'b1')).rejects.toEqual({ message: 'fail' });
|
|
});
|
|
});
|
|
|
|
// ── Columns ──────────────────────────────────────────────────────────────────
|
|
|
|
describe('createColumn', () => {
|
|
it('creates and returns column', async () => {
|
|
const col = { id: 'c1', name: 'To Do', board_id: 'b1', position: 0 };
|
|
const sb = mockSupabase({ data: col, error: null });
|
|
expect(await createColumn(sb, 'b1', 'To Do', 0)).toEqual(col);
|
|
});
|
|
|
|
it('throws on error', async () => {
|
|
const sb = mockSupabase({ data: null, error: { message: 'fail' } });
|
|
await expect(createColumn(sb, 'b1', 'X', 0)).rejects.toEqual({ message: 'fail' });
|
|
});
|
|
});
|
|
|
|
describe('updateColumn', () => {
|
|
it('updates without error', async () => {
|
|
const sb = mockSupabase({ data: null, error: null });
|
|
await expect(updateColumn(sb, 'c1', { name: 'Done' })).resolves.toBeUndefined();
|
|
});
|
|
|
|
it('throws on error', async () => {
|
|
const sb = mockSupabase({ data: null, error: { message: 'fail' } });
|
|
await expect(updateColumn(sb, 'c1', { name: 'X' })).rejects.toEqual({ message: 'fail' });
|
|
});
|
|
});
|
|
|
|
describe('deleteColumn', () => {
|
|
it('deletes without error', async () => {
|
|
const sb = mockSupabase({ data: null, error: null });
|
|
await expect(deleteColumn(sb, 'c1')).resolves.toBeUndefined();
|
|
});
|
|
|
|
it('throws on error', async () => {
|
|
const sb = mockSupabase({ data: null, error: { message: 'fail' } });
|
|
await expect(deleteColumn(sb, 'c1')).rejects.toEqual({ message: 'fail' });
|
|
});
|
|
});
|
|
|
|
// ── Cards ────────────────────────────────────────────────────────────────────
|
|
|
|
describe('createCard', () => {
|
|
it('creates and returns card', async () => {
|
|
const card = { id: 'k1', title: 'Task', column_id: 'c1', position: 0 };
|
|
const sb = mockSupabase({ data: card, error: null });
|
|
expect(await createCard(sb, 'c1', 'Task', 0, 'user1')).toEqual(card);
|
|
});
|
|
|
|
it('throws on error', async () => {
|
|
const sb = mockSupabase({ data: null, error: { message: 'fail' } });
|
|
await expect(createCard(sb, 'c1', 'X', 0, 'user1')).rejects.toEqual({ message: 'fail' });
|
|
});
|
|
});
|
|
|
|
describe('updateCard', () => {
|
|
it('updates without error', async () => {
|
|
const sb = mockSupabase({ data: null, error: null });
|
|
await expect(updateCard(sb, 'k1', { title: 'Updated' })).resolves.toBeUndefined();
|
|
});
|
|
|
|
it('throws on error', async () => {
|
|
const sb = mockSupabase({ data: null, error: { message: 'fail' } });
|
|
await expect(updateCard(sb, 'k1', { title: 'X' })).rejects.toEqual({ message: 'fail' });
|
|
});
|
|
});
|
|
|
|
describe('deleteCard', () => {
|
|
it('deletes without error', async () => {
|
|
const sb = mockSupabase({ data: null, error: null });
|
|
await expect(deleteCard(sb, 'k1')).resolves.toBeUndefined();
|
|
});
|
|
|
|
it('throws on error', async () => {
|
|
const sb = mockSupabase({ data: null, error: { message: 'fail' } });
|
|
await expect(deleteCard(sb, 'k1')).rejects.toEqual({ message: 'fail' });
|
|
});
|
|
});
|
|
|
|
// ── moveCard ─────────────────────────────────────────────────────────────────
|
|
|
|
describe('moveCard', () => {
|
|
it('reorders cards in target column', async () => {
|
|
const targetCards = [
|
|
{ id: 'k1', position: 0 },
|
|
{ id: 'k2', position: 1 },
|
|
];
|
|
const chain: any = {};
|
|
const methods = ['from', 'select', 'update', 'eq', 'in', 'order', 'single'];
|
|
for (const m of methods) {
|
|
chain[m] = vi.fn(() => chain);
|
|
}
|
|
chain.then = (resolve: any) => resolve({ data: targetCards, error: null });
|
|
const sb = { from: vi.fn(() => chain) } as any;
|
|
|
|
await expect(moveCard(sb, 'k3', 'col1', 1)).resolves.toBeUndefined();
|
|
});
|
|
|
|
it('throws when fetch fails', async () => {
|
|
const chain: any = {};
|
|
const methods = ['from', 'select', 'eq', 'in', 'order'];
|
|
for (const m of methods) {
|
|
chain[m] = vi.fn(() => chain);
|
|
}
|
|
chain.then = (resolve: any) => resolve({ data: null, error: { message: 'fetch fail' } });
|
|
const sb = { from: vi.fn(() => chain) } as any;
|
|
|
|
await expect(moveCard(sb, 'k1', 'col1', 0)).rejects.toEqual({ message: 'fetch fail' });
|
|
});
|
|
});
|