Files
web-app-launcher/src/lib/server/services/__tests__/boardService.test.ts
T
alexei.dolgolyov 1c0a7cb850 feat: Phases 4-7 — Full Feature Expansion (26 features)
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.
2026-03-25 14:18:10 +03:00

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' } });
});
});
});