mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 21:01:08 +02:00
test(manacore): add 48 unit tests for dashboard, API client, and credits
- Dashboard widget registry: 14 tests (types, metadata, size classes) - Default dashboard config: 12 tests (layout, validation, i18n keys) - Base API client: 15 tests (retry logic, auth headers, error handling) - Credits service: 7 tests (balance, transactions, packages, usage) - Updated audit score from 80 to 82 (testing: 12 → 48) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
32fba2b7b7
commit
135b65bcd6
5 changed files with 579 additions and 11 deletions
|
|
@ -1,16 +1,16 @@
|
|||
---
|
||||
title: 'ManaCore: Production Readiness Audit'
|
||||
description: 'Multi-App Ecosystem Dashboard mit 25 Web-Routes, 11 Dashboard-Widgets, Onboarding, 5 Sprachen, Mobile App, Landing Page'
|
||||
description: 'Multi-App Ecosystem Dashboard mit 25 Web-Routes, 11 Dashboard-Widgets, 48 Tests, Onboarding, 5 Sprachen, Mobile App, Landing Page'
|
||||
date: 2026-03-19
|
||||
app: 'manacore'
|
||||
author: 'Till Schneider'
|
||||
tags: ['audit', 'manacore', 'production-readiness', 'platform']
|
||||
score: 80
|
||||
score: 82
|
||||
scores:
|
||||
backend: 55
|
||||
frontend: 88
|
||||
database: 70
|
||||
testing: 12
|
||||
testing: 48
|
||||
deployment: 90
|
||||
documentation: 88
|
||||
security: 80
|
||||
|
|
@ -22,8 +22,8 @@ stats:
|
|||
webRoutes: 25
|
||||
components: 35
|
||||
dbTables: 0
|
||||
testFiles: 0
|
||||
testCount: 0
|
||||
testFiles: 4
|
||||
testCount: 48
|
||||
languages: 5
|
||||
---
|
||||
|
||||
|
|
@ -82,20 +82,24 @@ ManaCore ist das **Herzstück des Monorepos** - das Multi-App Ecosystem Dashboar
|
|||
- Kein lokaler Cache/Offline-Speicher
|
||||
- Widget-Daten nicht persistiert
|
||||
|
||||
## Testing (12/100)
|
||||
## Testing (48/100)
|
||||
|
||||
**Stärken:**
|
||||
|
||||
- Vitest konfiguriert mit Coverage (package.json)
|
||||
- Playwright für E2E Tests eingerichtet
|
||||
- @vitest/coverage-v8 und @vitest/ui installiert
|
||||
- 4 Test-Dateien mit 48 Unit Tests:
|
||||
- Dashboard Widget Registry Tests (14 Tests)
|
||||
- Default Dashboard Config Tests (12 Tests)
|
||||
- Base API Client mit Retry-Logik (15 Tests)
|
||||
- Credits Service API Tests (7 Tests)
|
||||
|
||||
**Lücken:**
|
||||
|
||||
- **0 Unit Tests**
|
||||
- **0 E2E Tests**
|
||||
- **0 Integration Tests**
|
||||
- Testing-Infrastruktur vorhanden, aber nicht genutzt
|
||||
- Keine E2E Tests (Playwright Setup vorhanden)
|
||||
- Keine Store-Tests (Auth, Dashboard State)
|
||||
- Keine Komponenten-Tests
|
||||
|
||||
## Deployment (90/100)
|
||||
|
||||
|
|
@ -167,6 +171,6 @@ ManaCore ist das **Herzstück des Monorepos** - das Multi-App Ecosystem Dashboar
|
|||
|
||||
## Top-3 Empfehlungen
|
||||
|
||||
1. **Tests schreiben** - Auth Store Unit Tests, Dashboard Widget Tests, E2E für Login/Onboarding Flow → Testing von 12 auf 50+
|
||||
1. **E2E Tests** - Playwright Tests für Login/Onboarding/Dashboard Flow → Testing von 48 auf 70+
|
||||
2. **Error Tracking** - GlitchTip Integration (wie andere Backends) für Production Monitoring
|
||||
3. **PWA verifizieren** - Offline-Modus testen, Service Worker prüfen, Manifest validieren
|
||||
|
|
|
|||
222
apps/manacore/apps/web/src/lib/api/base-client.test.ts
Normal file
222
apps/manacore/apps/web/src/lib/api/base-client.test.ts
Normal file
|
|
@ -0,0 +1,222 @@
|
|||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
|
||||
// Mock $app/environment before importing module
|
||||
vi.mock('$app/environment', () => ({
|
||||
browser: true,
|
||||
}));
|
||||
|
||||
// Mock auth store
|
||||
vi.mock('$lib/stores/auth.svelte', () => ({
|
||||
authStore: {
|
||||
getAccessToken: vi.fn().mockResolvedValue('mock-token-123'),
|
||||
},
|
||||
}));
|
||||
|
||||
import { fetchWithRetry, createApiClient, type RetryConfig, type ApiResult } from './base-client';
|
||||
|
||||
describe('fetchWithRetry', () => {
|
||||
beforeEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('should return data on successful request', async () => {
|
||||
const mockData = { id: 1, name: 'test' };
|
||||
global.fetch = vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
json: () => Promise.resolve(mockData),
|
||||
});
|
||||
|
||||
const result = await fetchWithRetry<typeof mockData>('https://api.example.com/data');
|
||||
|
||||
expect(result.data).toEqual(mockData);
|
||||
expect(result.error).toBeNull();
|
||||
});
|
||||
|
||||
it('should include Authorization header with token', async () => {
|
||||
global.fetch = vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({}),
|
||||
});
|
||||
|
||||
await fetchWithRetry('https://api.example.com/data');
|
||||
|
||||
expect(global.fetch).toHaveBeenCalledWith(
|
||||
'https://api.example.com/data',
|
||||
expect.objectContaining({
|
||||
headers: expect.objectContaining({
|
||||
Authorization: 'Bearer mock-token-123',
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should return error on 401 without retrying', async () => {
|
||||
global.fetch = vi.fn().mockResolvedValue({
|
||||
ok: false,
|
||||
status: 401,
|
||||
});
|
||||
|
||||
const result = await fetchWithRetry('https://api.example.com/data');
|
||||
|
||||
expect(result.data).toBeNull();
|
||||
expect(result.error).toContain('Authentication failed');
|
||||
expect(global.fetch).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should return error on 403 without retrying', async () => {
|
||||
global.fetch = vi.fn().mockResolvedValue({
|
||||
ok: false,
|
||||
status: 403,
|
||||
});
|
||||
|
||||
const result = await fetchWithRetry('https://api.example.com/data');
|
||||
|
||||
expect(result.data).toBeNull();
|
||||
expect(result.error).toContain('Authentication failed');
|
||||
expect(global.fetch).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should return error on 4xx client errors without retrying', async () => {
|
||||
global.fetch = vi.fn().mockResolvedValue({
|
||||
ok: false,
|
||||
status: 404,
|
||||
json: () => Promise.resolve({ message: 'Not found' }),
|
||||
});
|
||||
|
||||
const result = await fetchWithRetry('https://api.example.com/data');
|
||||
|
||||
expect(result.data).toBeNull();
|
||||
expect(result.error).toBe('Not found');
|
||||
expect(global.fetch).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should return error on network failure without retrying', async () => {
|
||||
global.fetch = vi.fn().mockRejectedValue(new Error('Failed to fetch'));
|
||||
|
||||
const result = await fetchWithRetry('https://api.example.com/data');
|
||||
|
||||
expect(result.data).toBeNull();
|
||||
expect(result.error).toBe('Service nicht erreichbar');
|
||||
expect(global.fetch).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should return error on connection refused', async () => {
|
||||
global.fetch = vi.fn().mockRejectedValue(new Error('ERR_CONNECTION_REFUSED'));
|
||||
|
||||
const result = await fetchWithRetry('https://api.example.com/data');
|
||||
|
||||
expect(result.data).toBeNull();
|
||||
expect(result.error).toBe('Service nicht erreichbar');
|
||||
});
|
||||
|
||||
it('should retry on 5xx server errors', async () => {
|
||||
global.fetch = vi
|
||||
.fn()
|
||||
.mockResolvedValueOnce({ ok: false, status: 500 })
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ success: true }),
|
||||
});
|
||||
|
||||
const result = await fetchWithRetry('https://api.example.com/data', {}, { retryDelay: 1 });
|
||||
|
||||
expect(result.data).toEqual({ success: true });
|
||||
expect(global.fetch).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('should respect maxRetries config', async () => {
|
||||
global.fetch = vi.fn().mockResolvedValue({ ok: false, status: 500 });
|
||||
|
||||
const result = await fetchWithRetry(
|
||||
'https://api.example.com/data',
|
||||
{},
|
||||
{ maxRetries: 1, retryDelay: 1 }
|
||||
);
|
||||
|
||||
expect(result.data).toBeNull();
|
||||
expect(result.error).toContain('HTTP 500');
|
||||
// 1 initial + 1 retry = 2
|
||||
expect(global.fetch).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createApiClient', () => {
|
||||
beforeEach(() => {
|
||||
global.fetch = vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ result: 'ok' }),
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('should create a client with get, post, put, delete methods', () => {
|
||||
const client = createApiClient('https://api.example.com');
|
||||
|
||||
expect(typeof client.get).toBe('function');
|
||||
expect(typeof client.post).toBe('function');
|
||||
expect(typeof client.put).toBe('function');
|
||||
expect(typeof client.delete).toBe('function');
|
||||
});
|
||||
|
||||
it('should prepend base URL to endpoints', async () => {
|
||||
const client = createApiClient('https://api.example.com');
|
||||
await client.get('/users');
|
||||
|
||||
expect(global.fetch).toHaveBeenCalledWith('https://api.example.com/users', expect.any(Object));
|
||||
});
|
||||
|
||||
it('should send GET requests correctly', async () => {
|
||||
const client = createApiClient('https://api.example.com');
|
||||
const result = await client.get('/data');
|
||||
|
||||
expect(result.data).toEqual({ result: 'ok' });
|
||||
expect(global.fetch).toHaveBeenCalledWith(
|
||||
'https://api.example.com/data',
|
||||
expect.objectContaining({ method: 'GET' })
|
||||
);
|
||||
});
|
||||
|
||||
it('should send POST requests with body', async () => {
|
||||
const client = createApiClient('https://api.example.com');
|
||||
await client.post('/data', { name: 'test' });
|
||||
|
||||
expect(global.fetch).toHaveBeenCalledWith(
|
||||
'https://api.example.com/data',
|
||||
expect.objectContaining({
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ name: 'test' }),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should send PUT requests with body', async () => {
|
||||
const client = createApiClient('https://api.example.com');
|
||||
await client.put('/data/1', { name: 'updated' });
|
||||
|
||||
expect(global.fetch).toHaveBeenCalledWith(
|
||||
'https://api.example.com/data/1',
|
||||
expect.objectContaining({
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ name: 'updated' }),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should send DELETE requests', async () => {
|
||||
const client = createApiClient('https://api.example.com');
|
||||
await client.delete('/data/1');
|
||||
|
||||
expect(global.fetch).toHaveBeenCalledWith(
|
||||
'https://api.example.com/data/1',
|
||||
expect.objectContaining({ method: 'DELETE' })
|
||||
);
|
||||
});
|
||||
});
|
||||
137
apps/manacore/apps/web/src/lib/api/credits.test.ts
Normal file
137
apps/manacore/apps/web/src/lib/api/credits.test.ts
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
|
||||
// Mock $app/environment
|
||||
vi.mock('$app/environment', () => ({
|
||||
browser: true,
|
||||
}));
|
||||
|
||||
// Mock auth store
|
||||
vi.mock('$lib/stores/auth.svelte', () => ({
|
||||
authStore: {
|
||||
getAccessToken: vi.fn().mockResolvedValue('test-token'),
|
||||
},
|
||||
}));
|
||||
|
||||
// Mock config
|
||||
vi.mock('./config', () => ({
|
||||
getManaAuthUrl: vi.fn().mockReturnValue('http://localhost:3001'),
|
||||
}));
|
||||
|
||||
import { creditsService } from './credits';
|
||||
|
||||
describe('creditsService', () => {
|
||||
beforeEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe('getBalance', () => {
|
||||
it('should fetch credit balance from correct endpoint', async () => {
|
||||
const mockBalance = { balance: 100, totalEarned: 500, totalSpent: 400 };
|
||||
global.fetch = vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
json: () => Promise.resolve(mockBalance),
|
||||
});
|
||||
|
||||
const result = await creditsService.getBalance();
|
||||
|
||||
expect(result).toEqual(mockBalance);
|
||||
expect(global.fetch).toHaveBeenCalledWith(
|
||||
'http://localhost:3001/api/v1/credits/balance',
|
||||
expect.objectContaining({
|
||||
headers: expect.objectContaining({
|
||||
Authorization: 'Bearer test-token',
|
||||
}),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw on failed request', async () => {
|
||||
global.fetch = vi.fn().mockResolvedValue({
|
||||
ok: false,
|
||||
status: 500,
|
||||
json: () => Promise.resolve({ message: 'Server error' }),
|
||||
});
|
||||
|
||||
await expect(creditsService.getBalance()).rejects.toThrow('Server error');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getTransactions', () => {
|
||||
it('should fetch transactions with default pagination', async () => {
|
||||
const mockTransactions = [{ id: '1', type: 'purchase', amount: 100 }];
|
||||
global.fetch = vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
json: () => Promise.resolve(mockTransactions),
|
||||
});
|
||||
|
||||
const result = await creditsService.getTransactions();
|
||||
|
||||
expect(result).toEqual(mockTransactions);
|
||||
expect(global.fetch).toHaveBeenCalledWith(
|
||||
'http://localhost:3001/api/v1/credits/transactions?limit=50&offset=0',
|
||||
expect.any(Object)
|
||||
);
|
||||
});
|
||||
|
||||
it('should pass custom limit and offset', async () => {
|
||||
global.fetch = vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
json: () => Promise.resolve([]),
|
||||
});
|
||||
|
||||
await creditsService.getTransactions(10, 20);
|
||||
|
||||
expect(global.fetch).toHaveBeenCalledWith(
|
||||
'http://localhost:3001/api/v1/credits/transactions?limit=10&offset=20',
|
||||
expect.any(Object)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getPackages', () => {
|
||||
it('should fetch packages without auth (public endpoint)', async () => {
|
||||
const mockPackages = [{ id: 'pkg-1', name: 'Starter', credits: 100 }];
|
||||
global.fetch = vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
json: () => Promise.resolve(mockPackages),
|
||||
});
|
||||
|
||||
const result = await creditsService.getPackages();
|
||||
|
||||
expect(result).toEqual(mockPackages);
|
||||
// Should NOT have Authorization header (public endpoint)
|
||||
expect(global.fetch).toHaveBeenCalledWith('http://localhost:3001/api/v1/credits/packages');
|
||||
});
|
||||
|
||||
it('should throw on failed request', async () => {
|
||||
global.fetch = vi.fn().mockResolvedValue({ ok: false, status: 500 });
|
||||
|
||||
await expect(creditsService.getPackages()).rejects.toThrow('Failed to fetch packages');
|
||||
});
|
||||
});
|
||||
|
||||
describe('useCredits', () => {
|
||||
it('should send credit usage request', async () => {
|
||||
const mockResponse = { success: true, newBalance: { balance: 90 } };
|
||||
global.fetch = vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
json: () => Promise.resolve(mockResponse),
|
||||
});
|
||||
|
||||
const result = await creditsService.useCredits(10, 'chat', 'AI generation');
|
||||
|
||||
expect(result).toEqual(mockResponse);
|
||||
expect(global.fetch).toHaveBeenCalledWith(
|
||||
'http://localhost:3001/api/v1/credits/use',
|
||||
expect.objectContaining({
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ amount: 10, appId: 'chat', description: 'AI generation' }),
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
import { describe, it, expect } from 'vitest';
|
||||
import { DEFAULT_DASHBOARD_CONFIG, DASHBOARD_STORAGE_KEY } from './default-dashboard';
|
||||
import { getWidgetMeta, type WidgetSize } from '$lib/types/dashboard';
|
||||
|
||||
describe('DEFAULT_DASHBOARD_CONFIG', () => {
|
||||
it('should have a 12-column grid', () => {
|
||||
expect(DEFAULT_DASHBOARD_CONFIG.gridColumns).toBe(12);
|
||||
});
|
||||
|
||||
it('should have 3 default widgets', () => {
|
||||
expect(DEFAULT_DASHBOARD_CONFIG.widgets).toHaveLength(3);
|
||||
});
|
||||
|
||||
it('should have all widgets visible by default', () => {
|
||||
for (const widget of DEFAULT_DASHBOARD_CONFIG.widgets) {
|
||||
expect(widget.visible).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it('should include clock, tasks-today, and calendar widgets', () => {
|
||||
const types = DEFAULT_DASHBOARD_CONFIG.widgets.map((w) => w.type);
|
||||
expect(types).toContain('clock-timers');
|
||||
expect(types).toContain('tasks-today');
|
||||
expect(types).toContain('calendar-events');
|
||||
});
|
||||
|
||||
it('should have unique widget IDs', () => {
|
||||
const ids = DEFAULT_DASHBOARD_CONFIG.widgets.map((w) => w.id);
|
||||
const uniqueIds = new Set(ids);
|
||||
expect(uniqueIds.size).toBe(ids.length);
|
||||
});
|
||||
|
||||
it('should have valid widget types that exist in registry', () => {
|
||||
for (const widget of DEFAULT_DASHBOARD_CONFIG.widgets) {
|
||||
const meta = getWidgetMeta(widget.type);
|
||||
expect(meta).toBeDefined();
|
||||
}
|
||||
});
|
||||
|
||||
it('should have valid sizes', () => {
|
||||
const validSizes: WidgetSize[] = ['small', 'medium', 'large', 'full'];
|
||||
for (const widget of DEFAULT_DASHBOARD_CONFIG.widgets) {
|
||||
expect(validSizes).toContain(widget.size);
|
||||
}
|
||||
});
|
||||
|
||||
it('should have valid grid positions', () => {
|
||||
for (const widget of DEFAULT_DASHBOARD_CONFIG.widgets) {
|
||||
expect(widget.position.x).toBeGreaterThanOrEqual(0);
|
||||
expect(widget.position.x).toBeLessThan(12);
|
||||
expect(widget.position.y).toBeGreaterThanOrEqual(0);
|
||||
}
|
||||
});
|
||||
|
||||
it('should have i18n title keys', () => {
|
||||
for (const widget of DEFAULT_DASHBOARD_CONFIG.widgets) {
|
||||
expect(widget.title).toMatch(/^dashboard\.widgets\..+\.title$/);
|
||||
}
|
||||
});
|
||||
|
||||
it('should have a lastModified timestamp', () => {
|
||||
expect(DEFAULT_DASHBOARD_CONFIG.lastModified).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('DASHBOARD_STORAGE_KEY', () => {
|
||||
it('should be a non-empty string', () => {
|
||||
expect(DASHBOARD_STORAGE_KEY).toBeTruthy();
|
||||
expect(typeof DASHBOARD_STORAGE_KEY).toBe('string');
|
||||
});
|
||||
|
||||
it('should contain manacore identifier', () => {
|
||||
expect(DASHBOARD_STORAGE_KEY).toContain('manacore');
|
||||
});
|
||||
});
|
||||
130
apps/manacore/apps/web/src/lib/types/dashboard.test.ts
Normal file
130
apps/manacore/apps/web/src/lib/types/dashboard.test.ts
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
import { describe, it, expect } from 'vitest';
|
||||
import {
|
||||
getWidgetMeta,
|
||||
WIDGET_REGISTRY,
|
||||
WIDGET_SIZE_CLASSES,
|
||||
type WidgetType,
|
||||
type WidgetSize,
|
||||
} from './dashboard';
|
||||
|
||||
describe('WIDGET_REGISTRY', () => {
|
||||
it('should contain 13 widget definitions', () => {
|
||||
expect(WIDGET_REGISTRY).toHaveLength(13);
|
||||
});
|
||||
|
||||
it('should have unique types for all widgets', () => {
|
||||
const types = WIDGET_REGISTRY.map((w) => w.type);
|
||||
const uniqueTypes = new Set(types);
|
||||
expect(uniqueTypes.size).toBe(types.length);
|
||||
});
|
||||
|
||||
it('should have required fields for every widget', () => {
|
||||
for (const widget of WIDGET_REGISTRY) {
|
||||
expect(widget.type).toBeTruthy();
|
||||
expect(widget.nameKey).toBeTruthy();
|
||||
expect(widget.descriptionKey).toBeTruthy();
|
||||
expect(widget.icon).toBeTruthy();
|
||||
expect(widget.defaultSize).toBeTruthy();
|
||||
expect(typeof widget.allowMultiple).toBe('boolean');
|
||||
}
|
||||
});
|
||||
|
||||
it('should have valid default sizes', () => {
|
||||
const validSizes: WidgetSize[] = ['small', 'medium', 'large', 'full'];
|
||||
for (const widget of WIDGET_REGISTRY) {
|
||||
expect(validSizes).toContain(widget.defaultSize);
|
||||
}
|
||||
});
|
||||
|
||||
it('should have valid required backends', () => {
|
||||
const validBackends = [
|
||||
'todo',
|
||||
'calendar',
|
||||
'chat',
|
||||
'contacts',
|
||||
'zitare',
|
||||
'picture',
|
||||
'manadeck',
|
||||
'clock',
|
||||
'storage',
|
||||
'mana-core-auth',
|
||||
undefined,
|
||||
];
|
||||
for (const widget of WIDGET_REGISTRY) {
|
||||
expect(validBackends).toContain(widget.requiredBackend);
|
||||
}
|
||||
});
|
||||
|
||||
it('should include all expected widget types', () => {
|
||||
const types = WIDGET_REGISTRY.map((w) => w.type);
|
||||
expect(types).toContain('credits');
|
||||
expect(types).toContain('quick-actions');
|
||||
expect(types).toContain('transactions');
|
||||
expect(types).toContain('tasks-today');
|
||||
expect(types).toContain('tasks-upcoming');
|
||||
expect(types).toContain('calendar-events');
|
||||
expect(types).toContain('chat-recent');
|
||||
expect(types).toContain('contacts-favorites');
|
||||
expect(types).toContain('zitare-quote');
|
||||
expect(types).toContain('picture-recent');
|
||||
expect(types).toContain('manadeck-progress');
|
||||
expect(types).toContain('clock-timers');
|
||||
expect(types).toContain('storage-usage');
|
||||
});
|
||||
|
||||
it('should have i18n-style name keys', () => {
|
||||
for (const widget of WIDGET_REGISTRY) {
|
||||
expect(widget.nameKey).toMatch(/^dashboard\.widgets\..+\.title$/);
|
||||
expect(widget.descriptionKey).toMatch(/^dashboard\.widgets\..+\.description$/);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('getWidgetMeta', () => {
|
||||
it('should return metadata for a valid widget type', () => {
|
||||
const meta = getWidgetMeta('credits');
|
||||
expect(meta).toBeDefined();
|
||||
expect(meta!.type).toBe('credits');
|
||||
expect(meta!.requiredBackend).toBe('mana-core-auth');
|
||||
});
|
||||
|
||||
it('should return undefined for an invalid widget type', () => {
|
||||
const meta = getWidgetMeta('nonexistent' as WidgetType);
|
||||
expect(meta).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return correct metadata for clock-timers', () => {
|
||||
const meta = getWidgetMeta('clock-timers');
|
||||
expect(meta).toBeDefined();
|
||||
expect(meta!.defaultSize).toBe('small');
|
||||
expect(meta!.requiredBackend).toBe('clock');
|
||||
expect(meta!.allowMultiple).toBe(false);
|
||||
});
|
||||
|
||||
it('should return correct metadata for each widget type', () => {
|
||||
for (const widget of WIDGET_REGISTRY) {
|
||||
const meta = getWidgetMeta(widget.type);
|
||||
expect(meta).toBeDefined();
|
||||
expect(meta).toEqual(widget);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('WIDGET_SIZE_CLASSES', () => {
|
||||
it('should have all four size classes', () => {
|
||||
expect(WIDGET_SIZE_CLASSES).toHaveProperty('small');
|
||||
expect(WIDGET_SIZE_CLASSES).toHaveProperty('medium');
|
||||
expect(WIDGET_SIZE_CLASSES).toHaveProperty('large');
|
||||
expect(WIDGET_SIZE_CLASSES).toHaveProperty('full');
|
||||
});
|
||||
|
||||
it('should contain col-span-12 in all sizes for mobile', () => {
|
||||
for (const [, className] of Object.entries(WIDGET_SIZE_CLASSES)) {
|
||||
expect(className).toContain('col-span-12');
|
||||
}
|
||||
});
|
||||
|
||||
it('full size should only use col-span-12', () => {
|
||||
expect(WIDGET_SIZE_CLASSES.full).toBe('col-span-12');
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue