feat: map shapes, image persistence, grab tool, layer rename/delete, i18n, page metadata
This commit is contained in:
326
src/lib/api/kanban.test.ts
Normal file
326
src/lib/api/kanban.test.ts
Normal file
@@ -0,0 +1,326 @@
|
||||
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' });
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user