1c0a7cb850
Phase 4 — New Widget Types: - Clock/Weather, System Stats, RSS/Feed, Calendar, Markdown, Metric/Counter, Link Group, Camera/Stream widgets - Backend services with caching for each data source - Full creation form with dynamic config fields per type Phase 5 — Visual & Styling Enhancements: - Glassmorphism card style (solid/glass/outline) - Board-level themes with per-board hue/saturation - Animated SVG status rings replacing static dots - Card size options (compact/medium/large) - Custom CSS injection (admin + per-board, sanitized) - Wallpaper backgrounds with blur/overlay/parallax Phase 6 — Functional Features: - Favorites bar with drag-and-drop reordering - Recent apps tracking with privacy toggle - Uptime dashboard page (/status, guest-accessible) - Notifications system (Discord/Slack/Telegram/HTTP webhooks) - App tags with filtering in board view - Multi-URL app cards with expandable sub-links - Personal API tokens with scoped permissions - Audit log with retention and admin viewer Phase 7 — Quality of Life: - Onboarding wizard (5-step first-launch setup) - App URL health preview with favicon/title detection - Board templates (4 built-in + custom import/export) - Keyboard shortcut overlay (j/k nav, 1-9 boards, ? help) 212 files changed, 15641 insertions, 980 deletions. Build, lint, type check, and 222 tests all pass.
173 lines
4.5 KiB
TypeScript
173 lines
4.5 KiB
TypeScript
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
|
|
vi.mock('../../prisma.js', () => ({
|
|
prisma: {
|
|
board: {
|
|
findMany: vi.fn(),
|
|
findUnique: vi.fn(),
|
|
findFirst: vi.fn(),
|
|
create: vi.fn(),
|
|
update: vi.fn(),
|
|
updateMany: vi.fn(),
|
|
delete: vi.fn()
|
|
},
|
|
section: {
|
|
findUnique: vi.fn(),
|
|
findFirst: vi.fn(),
|
|
create: vi.fn(),
|
|
update: vi.fn(),
|
|
delete: vi.fn()
|
|
},
|
|
widget: {
|
|
findUnique: vi.fn(),
|
|
findFirst: vi.fn(),
|
|
create: vi.fn(),
|
|
update: vi.fn(),
|
|
delete: vi.fn()
|
|
}
|
|
}
|
|
}));
|
|
|
|
import { prisma } from '../../prisma.js';
|
|
import * as boardService from '../boardService.js';
|
|
|
|
const mockBoard = prisma.board as unknown as Record<string, ReturnType<typeof vi.fn>>;
|
|
const mockSection = prisma.section as unknown as Record<string, ReturnType<typeof vi.fn>>;
|
|
const mockWidget = prisma.widget as unknown as Record<string, ReturnType<typeof vi.fn>>;
|
|
|
|
describe('boardService', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
describe('findAllBoards', () => {
|
|
it('returns all boards', async () => {
|
|
const boards = [{ id: '1', name: 'Main', _count: { sections: 2 } }];
|
|
mockBoard.findMany.mockResolvedValue(boards);
|
|
|
|
const result = await boardService.findAllBoards();
|
|
expect(result).toEqual(boards);
|
|
});
|
|
});
|
|
|
|
describe('findBoardById', () => {
|
|
it('returns board with sections and widgets', async () => {
|
|
const board = { id: '1', name: 'Main', sections: [] };
|
|
mockBoard.findUnique.mockResolvedValue(board);
|
|
|
|
const result = await boardService.findBoardById('1');
|
|
expect(result.name).toBe('Main');
|
|
});
|
|
|
|
it('throws when not found', async () => {
|
|
mockBoard.findUnique.mockResolvedValue(null);
|
|
await expect(boardService.findBoardById('missing')).rejects.toThrow('Board not found');
|
|
});
|
|
});
|
|
|
|
describe('createBoard', () => {
|
|
it('creates a board', async () => {
|
|
mockBoard.create.mockResolvedValue({ id: '1', name: 'New Board' });
|
|
|
|
const result = await boardService.createBoard({ name: 'New Board' });
|
|
expect(result.name).toBe('New Board');
|
|
});
|
|
|
|
it('unsets other defaults when creating a default board', async () => {
|
|
mockBoard.updateMany.mockResolvedValue({ count: 1 });
|
|
mockBoard.create.mockResolvedValue({ id: '1', name: 'Default', isDefault: true });
|
|
|
|
await boardService.createBoard({ name: 'Default', isDefault: true });
|
|
|
|
expect(mockBoard.updateMany).toHaveBeenCalledWith({
|
|
where: { isDefault: true },
|
|
data: { isDefault: false }
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('updateBoard', () => {
|
|
it('updates board fields', async () => {
|
|
mockBoard.findUnique.mockResolvedValue({ id: '1' });
|
|
mockBoard.update.mockResolvedValue({ id: '1', name: 'Updated' });
|
|
|
|
const result = await boardService.updateBoard('1', { name: 'Updated' });
|
|
expect(result.name).toBe('Updated');
|
|
});
|
|
});
|
|
|
|
describe('removeBoard', () => {
|
|
it('deletes a board', async () => {
|
|
mockBoard.findUnique.mockResolvedValue({ id: '1' });
|
|
mockBoard.delete.mockResolvedValue({});
|
|
|
|
await boardService.removeBoard('1');
|
|
expect(mockBoard.delete).toHaveBeenCalledWith({ where: { id: '1' } });
|
|
});
|
|
});
|
|
|
|
describe('createSection', () => {
|
|
it('creates a section with auto-calculated order', async () => {
|
|
mockSection.findFirst.mockResolvedValue({ order: 2 });
|
|
mockSection.create.mockResolvedValue({
|
|
id: 's1',
|
|
title: 'Media',
|
|
order: 3
|
|
});
|
|
|
|
const result = await boardService.createSection({
|
|
boardId: 'b1',
|
|
title: 'Media'
|
|
});
|
|
|
|
expect(result.order).toBe(3);
|
|
});
|
|
|
|
it('starts order at 0 for first section', async () => {
|
|
mockSection.findFirst.mockResolvedValue(null);
|
|
mockSection.create.mockResolvedValue({
|
|
id: 's1',
|
|
title: 'First',
|
|
order: 0
|
|
});
|
|
|
|
await boardService.createSection({ boardId: 'b1', title: 'First' });
|
|
|
|
expect(mockSection.create).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
data: expect.objectContaining({ order: 0 })
|
|
})
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('createWidget', () => {
|
|
it('creates a widget', async () => {
|
|
mockWidget.findFirst.mockResolvedValue(null);
|
|
mockWidget.create.mockResolvedValue({
|
|
id: 'w1',
|
|
type: 'app',
|
|
order: 0
|
|
});
|
|
|
|
const result = await boardService.createWidget({
|
|
sectionId: 's1',
|
|
type: 'app',
|
|
config: JSON.stringify({ appId: 'test-app-1' })
|
|
});
|
|
|
|
expect(result.type).toBe('app');
|
|
});
|
|
});
|
|
|
|
describe('removeWidget', () => {
|
|
it('deletes a widget', async () => {
|
|
mockWidget.findUnique.mockResolvedValue({ id: 'w1' });
|
|
mockWidget.delete.mockResolvedValue({});
|
|
|
|
await boardService.removeWidget('w1');
|
|
expect(mockWidget.delete).toHaveBeenCalledWith({ where: { id: 'w1' } });
|
|
});
|
|
});
|
|
});
|