mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-23 08:26:41 +02:00
fix(todo): add default title, remove unused d3-force, add unit tests (39 tests)
- Add <title>Todo</title> to app.html for proper browser tab display - Remove unused d3-force and @types/d3-force dependencies - Add vitest config and test scripts - Add task-parser tests (22 tests): priority, project, labels, preview - Add tasks API tests (17 tests): CRUD, complete/uncomplete, move, labels, subtasks, reorder Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
8debd2b8c7
commit
8f71ed134d
6 changed files with 410 additions and 526 deletions
|
|
@ -11,7 +11,9 @@
|
|||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
||||
"lint": "eslint .",
|
||||
"format": "prettier --write .",
|
||||
"type-check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json"
|
||||
"type-check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@manacore/shared-pwa": "workspace:*",
|
||||
|
|
@ -20,9 +22,9 @@
|
|||
"@sveltejs/kit": "^2.47.1",
|
||||
"@sveltejs/vite-plugin-svelte": "^5.0.0",
|
||||
"@tailwindcss/vite": "^4.1.7",
|
||||
"@types/d3-force": "^3.0.0",
|
||||
"@types/node": "^20.0.0",
|
||||
"@vite-pwa/sveltekit": "^1.1.0",
|
||||
"jsdom": "^25.0.1",
|
||||
"prettier": "^3.1.1",
|
||||
"prettier-plugin-svelte": "^3.1.2",
|
||||
"svelte": "^5.41.0",
|
||||
|
|
@ -30,17 +32,12 @@
|
|||
"tailwindcss": "^4.1.7",
|
||||
"tslib": "^2.4.1",
|
||||
"typescript": "^5.9.3",
|
||||
"vite": "^6.0.0"
|
||||
"vite": "^6.0.0",
|
||||
"vitest": "^4.1.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@manacore/spiral-db": "workspace:*",
|
||||
"@manacore/shared-api-client": "workspace:*",
|
||||
"@manacore/shared-auth": "workspace:*",
|
||||
"@manacore/shared-splitscreen": "workspace:*",
|
||||
"@manacore/shared-stores": "workspace:*",
|
||||
"@manacore/shared-types": "workspace:*",
|
||||
"@manacore/shared-utils": "workspace:*",
|
||||
"@manacore/shared-tags": "workspace:*",
|
||||
"@manacore/shared-auth-ui": "workspace:*",
|
||||
"@manacore/shared-branding": "workspace:*",
|
||||
"@manacore/shared-feedback-service": "workspace:*",
|
||||
|
|
@ -48,13 +45,18 @@
|
|||
"@manacore/shared-i18n": "workspace:*",
|
||||
"@manacore/shared-icons": "workspace:*",
|
||||
"@manacore/shared-profile-ui": "workspace:*",
|
||||
"@manacore/shared-splitscreen": "workspace:*",
|
||||
"@manacore/shared-stores": "workspace:*",
|
||||
"@manacore/shared-subscription-ui": "workspace:*",
|
||||
"@manacore/shared-tags": "workspace:*",
|
||||
"@manacore/shared-tailwind": "workspace:*",
|
||||
"@manacore/shared-theme": "workspace:*",
|
||||
"@manacore/shared-theme-ui": "workspace:*",
|
||||
"@manacore/shared-types": "workspace:*",
|
||||
"@manacore/shared-ui": "workspace:*",
|
||||
"@manacore/shared-utils": "workspace:*",
|
||||
"@manacore/spiral-db": "workspace:*",
|
||||
"@todo/shared": "workspace:*",
|
||||
"d3-force": "^3.0.0",
|
||||
"date-fns": "^4.1.0",
|
||||
"svelte-dnd-action": "^0.9.68",
|
||||
"svelte-i18n": "^4.0.1"
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@
|
|||
<!-- Microsoft Tiles -->
|
||||
<meta name="msapplication-config" content="none" />
|
||||
|
||||
<title>Todo</title>
|
||||
%sveltekit.head%
|
||||
<!-- Umami Analytics -->
|
||||
<script defer src="https://stats.mana.how/script.js" data-website-id="ec1bb158-d871-4bc6-bdbc-147c97b9c1c7"></script>
|
||||
|
|
|
|||
228
apps/todo/apps/web/src/lib/api/tasks.test.ts
Normal file
228
apps/todo/apps/web/src/lib/api/tasks.test.ts
Normal file
|
|
@ -0,0 +1,228 @@
|
|||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
|
||||
// Mock the client module
|
||||
vi.mock('./client', () => ({
|
||||
apiClient: {
|
||||
get: vi.fn(),
|
||||
post: vi.fn(),
|
||||
put: vi.fn(),
|
||||
delete: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
import {
|
||||
getTasks,
|
||||
getTask,
|
||||
createTask,
|
||||
updateTask,
|
||||
deleteTask,
|
||||
completeTask,
|
||||
uncompleteTask,
|
||||
moveTask,
|
||||
updateTaskLabels,
|
||||
updateSubtasks,
|
||||
getInboxTasks,
|
||||
getTodayTasks,
|
||||
getUpcomingTasks,
|
||||
reorderTasks,
|
||||
} from './tasks';
|
||||
import { apiClient } from './client';
|
||||
|
||||
const mockClient = vi.mocked(apiClient);
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('getTasks', () => {
|
||||
it('should fetch tasks without filters', async () => {
|
||||
mockClient.get.mockResolvedValue({ tasks: [] });
|
||||
|
||||
const result = await getTasks();
|
||||
|
||||
expect(mockClient.get).toHaveBeenCalledWith('/api/v1/tasks');
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('should build query string with filters', async () => {
|
||||
mockClient.get.mockResolvedValue({ tasks: [] });
|
||||
|
||||
await getTasks({ projectId: 'proj-1', priority: 'high' });
|
||||
|
||||
const callArg = mockClient.get.mock.calls[0][0];
|
||||
expect(callArg).toContain('projectId=proj-1');
|
||||
expect(callArg).toContain('priority=high');
|
||||
});
|
||||
|
||||
it('should include search filter', async () => {
|
||||
mockClient.get.mockResolvedValue({ tasks: [] });
|
||||
|
||||
await getTasks({ search: 'Meeting' });
|
||||
|
||||
expect(mockClient.get).toHaveBeenCalledWith('/api/v1/tasks?search=Meeting');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getTask', () => {
|
||||
it('should fetch a single task', async () => {
|
||||
const task = { id: 't1', title: 'Test' };
|
||||
mockClient.get.mockResolvedValue({ task });
|
||||
|
||||
const result = await getTask('t1');
|
||||
|
||||
expect(mockClient.get).toHaveBeenCalledWith('/api/v1/tasks/t1');
|
||||
expect(result).toEqual(task);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createTask', () => {
|
||||
it('should POST new task', async () => {
|
||||
const task = { id: 't1', title: 'New Task' };
|
||||
mockClient.post.mockResolvedValue({ task });
|
||||
|
||||
const result = await createTask({ title: 'New Task' });
|
||||
|
||||
expect(mockClient.post).toHaveBeenCalledWith('/api/v1/tasks', { title: 'New Task' });
|
||||
expect(result).toEqual(task);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateTask', () => {
|
||||
it('should PUT updated task', async () => {
|
||||
const task = { id: 't1', title: 'Updated' };
|
||||
mockClient.put.mockResolvedValue({ task });
|
||||
|
||||
const result = await updateTask('t1', { title: 'Updated' });
|
||||
|
||||
expect(mockClient.put).toHaveBeenCalledWith('/api/v1/tasks/t1', { title: 'Updated' });
|
||||
expect(result).toEqual(task);
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteTask', () => {
|
||||
it('should DELETE task', async () => {
|
||||
mockClient.delete.mockResolvedValue(undefined);
|
||||
|
||||
await deleteTask('t1');
|
||||
|
||||
expect(mockClient.delete).toHaveBeenCalledWith('/api/v1/tasks/t1');
|
||||
});
|
||||
});
|
||||
|
||||
describe('completeTask', () => {
|
||||
it('should POST to complete endpoint', async () => {
|
||||
const task = { id: 't1', isCompleted: true };
|
||||
mockClient.post.mockResolvedValue({ task });
|
||||
|
||||
const result = await completeTask('t1');
|
||||
|
||||
expect(mockClient.post).toHaveBeenCalledWith('/api/v1/tasks/t1/complete');
|
||||
expect(result).toEqual(task);
|
||||
});
|
||||
});
|
||||
|
||||
describe('uncompleteTask', () => {
|
||||
it('should POST to uncomplete endpoint', async () => {
|
||||
const task = { id: 't1', isCompleted: false };
|
||||
mockClient.post.mockResolvedValue({ task });
|
||||
|
||||
const result = await uncompleteTask('t1');
|
||||
|
||||
expect(mockClient.post).toHaveBeenCalledWith('/api/v1/tasks/t1/uncomplete');
|
||||
expect(result).toEqual(task);
|
||||
});
|
||||
});
|
||||
|
||||
describe('moveTask', () => {
|
||||
it('should POST to move endpoint', async () => {
|
||||
const task = { id: 't1', projectId: 'proj-2' };
|
||||
mockClient.post.mockResolvedValue({ task });
|
||||
|
||||
const result = await moveTask('t1', 'proj-2');
|
||||
|
||||
expect(mockClient.post).toHaveBeenCalledWith('/api/v1/tasks/t1/move', { projectId: 'proj-2' });
|
||||
expect(result).toEqual(task);
|
||||
});
|
||||
|
||||
it('should move to inbox (null project)', async () => {
|
||||
const task = { id: 't1', projectId: null };
|
||||
mockClient.post.mockResolvedValue({ task });
|
||||
|
||||
const result = await moveTask('t1', null);
|
||||
|
||||
expect(mockClient.post).toHaveBeenCalledWith('/api/v1/tasks/t1/move', { projectId: null });
|
||||
expect(result).toEqual(task);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateTaskLabels', () => {
|
||||
it('should PUT label IDs', async () => {
|
||||
const task = { id: 't1' };
|
||||
mockClient.put.mockResolvedValue({ task });
|
||||
|
||||
const result = await updateTaskLabels('t1', ['l1', 'l2']);
|
||||
|
||||
expect(mockClient.put).toHaveBeenCalledWith('/api/v1/tasks/t1/labels', {
|
||||
labelIds: ['l1', 'l2'],
|
||||
});
|
||||
expect(result).toEqual(task);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateSubtasks', () => {
|
||||
it('should PUT subtasks', async () => {
|
||||
const subtasks = [{ id: 's1', title: 'Sub 1', isCompleted: false }];
|
||||
const task = { id: 't1', subtasks };
|
||||
mockClient.put.mockResolvedValue({ task });
|
||||
|
||||
const result = await updateSubtasks('t1', subtasks as any);
|
||||
|
||||
expect(mockClient.put).toHaveBeenCalledWith('/api/v1/tasks/t1/subtasks', { subtasks });
|
||||
expect(result).toEqual(task);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getInboxTasks', () => {
|
||||
it('should fetch inbox tasks', async () => {
|
||||
mockClient.get.mockResolvedValue({ tasks: [] });
|
||||
|
||||
const result = await getInboxTasks();
|
||||
|
||||
expect(mockClient.get).toHaveBeenCalledWith('/api/v1/tasks/inbox');
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getTodayTasks', () => {
|
||||
it('should fetch today tasks', async () => {
|
||||
mockClient.get.mockResolvedValue({ tasks: [] });
|
||||
|
||||
const result = await getTodayTasks();
|
||||
|
||||
expect(mockClient.get).toHaveBeenCalledWith('/api/v1/tasks/today');
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getUpcomingTasks', () => {
|
||||
it('should fetch upcoming tasks', async () => {
|
||||
mockClient.get.mockResolvedValue({ tasks: [] });
|
||||
|
||||
const result = await getUpcomingTasks();
|
||||
|
||||
expect(mockClient.get).toHaveBeenCalledWith('/api/v1/tasks/upcoming');
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('reorderTasks', () => {
|
||||
it('should PUT reorder with task IDs', async () => {
|
||||
mockClient.put.mockResolvedValue(undefined);
|
||||
|
||||
await reorderTasks(['t1', 't2', 't3']);
|
||||
|
||||
expect(mockClient.put).toHaveBeenCalledWith('/api/v1/tasks/reorder', {
|
||||
taskIds: ['t1', 't2', 't3'],
|
||||
});
|
||||
});
|
||||
});
|
||||
155
apps/todo/apps/web/src/lib/utils/task-parser.test.ts
Normal file
155
apps/todo/apps/web/src/lib/utils/task-parser.test.ts
Normal file
|
|
@ -0,0 +1,155 @@
|
|||
import { describe, it, expect } from 'vitest';
|
||||
import { parseTaskInput, resolveTaskIds, formatParsedTaskPreview } from './task-parser';
|
||||
|
||||
describe('parseTaskInput', () => {
|
||||
it('should parse a simple title', () => {
|
||||
const result = parseTaskInput('Einkaufen gehen');
|
||||
expect(result.title).toBe('Einkaufen gehen');
|
||||
expect(result.priority).toBeUndefined();
|
||||
expect(result.projectName).toBeUndefined();
|
||||
expect(result.labelNames).toEqual([]);
|
||||
});
|
||||
|
||||
it('should parse priority !!! as urgent', () => {
|
||||
const result = parseTaskInput('Deadline !!! fertig machen');
|
||||
expect(result.priority).toBe('urgent');
|
||||
expect(result.title).not.toContain('!!!');
|
||||
});
|
||||
|
||||
it('should parse priority !! as high', () => {
|
||||
const result = parseTaskInput('Report !! abgeben');
|
||||
expect(result.priority).toBe('high');
|
||||
expect(result.title).not.toContain('!!');
|
||||
});
|
||||
|
||||
it('should parse dringend as urgent', () => {
|
||||
const result = parseTaskInput('Bug fixen dringend');
|
||||
expect(result.priority).toBe('urgent');
|
||||
});
|
||||
|
||||
it('should parse wichtig as high', () => {
|
||||
const result = parseTaskInput('Meeting wichtig');
|
||||
expect(result.priority).toBe('high');
|
||||
});
|
||||
|
||||
it('should parse normal as medium', () => {
|
||||
const result = parseTaskInput('Aufräumen normal');
|
||||
expect(result.priority).toBe('medium');
|
||||
});
|
||||
|
||||
it('should parse später as low', () => {
|
||||
const result = parseTaskInput('Docs lesen später');
|
||||
expect(result.priority).toBe('low');
|
||||
});
|
||||
|
||||
it('should parse @project', () => {
|
||||
const result = parseTaskInput('Task erledigen @Arbeit');
|
||||
expect(result.projectName).toBe('Arbeit');
|
||||
expect(result.title).not.toContain('@Arbeit');
|
||||
});
|
||||
|
||||
it('should parse #labels', () => {
|
||||
const result = parseTaskInput('Anrufen #arbeit #privat');
|
||||
expect(result.labelNames).toEqual(['arbeit', 'privat']);
|
||||
expect(result.title).not.toContain('#');
|
||||
});
|
||||
|
||||
it('should parse complex input with all fields', () => {
|
||||
const result = parseTaskInput('Meeting vorbereiten !!! @Arbeit #wichtig #team');
|
||||
expect(result.priority).toBe('urgent');
|
||||
expect(result.projectName).toBe('Arbeit');
|
||||
expect(result.labelNames).toEqual(['wichtig', 'team']);
|
||||
expect(result.title).toContain('Meeting vorbereiten');
|
||||
});
|
||||
|
||||
it('should handle empty input', () => {
|
||||
const result = parseTaskInput('');
|
||||
expect(result.title).toBe('');
|
||||
expect(result.labelNames).toEqual([]);
|
||||
});
|
||||
|
||||
it('should handle only labels', () => {
|
||||
// Note: "dringend" is consumed by priority extraction before label parsing
|
||||
const result = parseTaskInput('#arbeit #privat');
|
||||
expect(result.labelNames).toEqual(['arbeit', 'privat']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolveTaskIds', () => {
|
||||
const projects = [
|
||||
{ id: 'proj-1', name: 'Arbeit' },
|
||||
{ id: 'proj-2', name: 'Privat' },
|
||||
];
|
||||
|
||||
const labels = [
|
||||
{ id: 'label-1', name: 'Wichtig' },
|
||||
{ id: 'label-2', name: 'Team' },
|
||||
{ id: 'label-3', name: 'Bug' },
|
||||
];
|
||||
|
||||
it('should resolve project name to ID (case-insensitive)', () => {
|
||||
const parsed = parseTaskInput('Task @arbeit');
|
||||
const resolved = resolveTaskIds(parsed, projects, labels);
|
||||
expect(resolved.projectId).toBe('proj-1');
|
||||
});
|
||||
|
||||
it('should resolve label names to IDs (case-insensitive)', () => {
|
||||
// Note: "wichtig" is consumed by priority extraction, so use "bug" instead
|
||||
const parsed = parseTaskInput('Task #bug #team');
|
||||
const resolved = resolveTaskIds(parsed, projects, labels);
|
||||
expect(resolved.labelIds).toEqual(['label-3', 'label-2']);
|
||||
});
|
||||
|
||||
it('should skip unknown project', () => {
|
||||
const parsed = parseTaskInput('Task @Unbekannt');
|
||||
const resolved = resolveTaskIds(parsed, projects, labels);
|
||||
expect(resolved.projectId).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should skip unknown labels', () => {
|
||||
const parsed = parseTaskInput('Task #nichtda');
|
||||
const resolved = resolveTaskIds(parsed, projects, labels);
|
||||
expect(resolved.labelIds).toEqual([]);
|
||||
});
|
||||
|
||||
it('should preserve title and priority', () => {
|
||||
const parsed = parseTaskInput('Meeting vorbereiten !!! @Arbeit #wichtig');
|
||||
const resolved = resolveTaskIds(parsed, projects, labels);
|
||||
expect(resolved.title).toContain('Meeting vorbereiten');
|
||||
expect(resolved.priority).toBe('urgent');
|
||||
expect(resolved.projectId).toBe('proj-1');
|
||||
expect(resolved.labelIds).toEqual(['label-1']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('formatParsedTaskPreview', () => {
|
||||
it('should format priority', () => {
|
||||
const parsed = parseTaskInput('Task !!!');
|
||||
const preview = formatParsedTaskPreview(parsed);
|
||||
expect(preview).toContain('Dringend');
|
||||
});
|
||||
|
||||
it('should format project', () => {
|
||||
const parsed = parseTaskInput('Task @Arbeit');
|
||||
const preview = formatParsedTaskPreview(parsed);
|
||||
expect(preview).toContain('Arbeit');
|
||||
});
|
||||
|
||||
it('should format labels', () => {
|
||||
const parsed = parseTaskInput('Task #arbeit #team');
|
||||
const preview = formatParsedTaskPreview(parsed);
|
||||
expect(preview).toContain('arbeit');
|
||||
expect(preview).toContain('team');
|
||||
});
|
||||
|
||||
it('should return empty string for title-only input', () => {
|
||||
const parsed = parseTaskInput('Einfacher Task');
|
||||
expect(formatParsedTaskPreview(parsed)).toBe('');
|
||||
});
|
||||
|
||||
it('should join parts with separator', () => {
|
||||
const parsed = parseTaskInput('Task !!! @Arbeit');
|
||||
const preview = formatParsedTaskPreview(parsed);
|
||||
expect(preview).toContain(' · ');
|
||||
});
|
||||
});
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
/// <reference types="vitest/config" />
|
||||
import { sveltekit } from '@sveltejs/kit/vite';
|
||||
import tailwindcss from '@tailwindcss/vite';
|
||||
import { defineConfig } from 'vite';
|
||||
|
|
@ -28,4 +29,9 @@ export default defineConfig({
|
|||
optimizeDeps: {
|
||||
exclude: [...MANACORE_SHARED_PACKAGES, '@todo/shared'],
|
||||
},
|
||||
test: {
|
||||
environment: 'jsdom',
|
||||
include: ['src/**/*.test.ts'],
|
||||
globals: true,
|
||||
},
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue