mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 23:41:08 +02:00
test(contacts): add unit tests for web app (62 tests)
- contact-parser.test.ts: parsing names, companies, phones, tags, preview formatting - contacts.test.ts: API client for contacts, notes, activities CRUD - filter.test.ts: filter store state, setters, toggles, persistence, reset - Add vitest config and test scripts to package.json Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
a5d270154c
commit
8debd2b8c7
5 changed files with 626 additions and 1 deletions
|
|
@ -9,7 +9,9 @@
|
|||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
||||
"lint": "eslint .",
|
||||
"format": "prettier --write ."
|
||||
"format": "prettier --write .",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@manacore/shared-pwa": "workspace:*",
|
||||
|
|
|
|||
287
apps/contacts/apps/web/src/lib/api/contacts.test.ts
Normal file
287
apps/contacts/apps/web/src/lib/api/contacts.test.ts
Normal file
|
|
@ -0,0 +1,287 @@
|
|||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
|
||||
// Mock the client module
|
||||
vi.mock('./client', () => ({
|
||||
fetchWithAuth: vi.fn(),
|
||||
fetchWithAuthFormData: vi.fn(),
|
||||
}));
|
||||
|
||||
// Mock $app/environment
|
||||
vi.mock('$app/environment', () => ({
|
||||
browser: true,
|
||||
}));
|
||||
|
||||
// Mock auth store
|
||||
vi.mock('$lib/stores/auth.svelte', () => ({
|
||||
authStore: {
|
||||
getAccessToken: vi.fn().mockResolvedValue('mock-token'),
|
||||
getValidToken: vi.fn().mockResolvedValue('mock-token'),
|
||||
},
|
||||
}));
|
||||
|
||||
// Mock shared-tags
|
||||
vi.mock('@manacore/shared-tags', () => ({
|
||||
createTagsClient: vi.fn(() => ({
|
||||
getAll: vi.fn().mockResolvedValue([]),
|
||||
create: vi.fn(),
|
||||
update: vi.fn(),
|
||||
delete: vi.fn(),
|
||||
createDefaults: vi.fn().mockResolvedValue([]),
|
||||
})),
|
||||
}));
|
||||
|
||||
import { contactsApi, notesApi, activitiesApi } from './contacts';
|
||||
import { fetchWithAuth } from './client';
|
||||
|
||||
const mockFetch = vi.mocked(fetchWithAuth);
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('contactsApi', () => {
|
||||
describe('list', () => {
|
||||
it('should fetch contacts without filters', async () => {
|
||||
mockFetch.mockResolvedValue({ contacts: [], total: 0 });
|
||||
|
||||
const result = await contactsApi.list();
|
||||
|
||||
expect(mockFetch).toHaveBeenCalledWith('/contacts');
|
||||
expect(result).toEqual({ contacts: [], total: 0 });
|
||||
});
|
||||
|
||||
it('should build query string with search filter', async () => {
|
||||
mockFetch.mockResolvedValue({ contacts: [], total: 0 });
|
||||
|
||||
await contactsApi.list({ search: 'Max' });
|
||||
|
||||
expect(mockFetch).toHaveBeenCalledWith('/contacts?search=Max');
|
||||
});
|
||||
|
||||
it('should build query string with multiple filters', async () => {
|
||||
mockFetch.mockResolvedValue({ contacts: [], total: 0 });
|
||||
|
||||
await contactsApi.list({
|
||||
search: 'Max',
|
||||
isFavorite: true,
|
||||
limit: 50,
|
||||
offset: 10,
|
||||
});
|
||||
|
||||
const callArg = mockFetch.mock.calls[0][0];
|
||||
expect(callArg).toContain('search=Max');
|
||||
expect(callArg).toContain('isFavorite=true');
|
||||
expect(callArg).toContain('limit=50');
|
||||
expect(callArg).toContain('offset=10');
|
||||
});
|
||||
|
||||
it('should include tagId filter', async () => {
|
||||
mockFetch.mockResolvedValue({ contacts: [], total: 0 });
|
||||
|
||||
await contactsApi.list({ tagId: 'tag-123' });
|
||||
|
||||
expect(mockFetch).toHaveBeenCalledWith('/contacts?tagId=tag-123');
|
||||
});
|
||||
|
||||
it('should include isArchived filter', async () => {
|
||||
mockFetch.mockResolvedValue({ contacts: [], total: 0 });
|
||||
|
||||
await contactsApi.list({ isArchived: true });
|
||||
|
||||
expect(mockFetch).toHaveBeenCalledWith('/contacts?isArchived=true');
|
||||
});
|
||||
});
|
||||
|
||||
describe('get', () => {
|
||||
it('should fetch a single contact', async () => {
|
||||
const contact = { id: 'c1', firstName: 'Max' };
|
||||
mockFetch.mockResolvedValue({ contact });
|
||||
|
||||
const result = await contactsApi.get('c1');
|
||||
|
||||
expect(mockFetch).toHaveBeenCalledWith('/contacts/c1');
|
||||
expect(result).toEqual(contact);
|
||||
});
|
||||
});
|
||||
|
||||
describe('create', () => {
|
||||
it('should POST new contact', async () => {
|
||||
const contact = { id: 'c1', firstName: 'Max' };
|
||||
mockFetch.mockResolvedValue({ contact });
|
||||
|
||||
const result = await contactsApi.create({ firstName: 'Max' });
|
||||
|
||||
expect(mockFetch).toHaveBeenCalledWith('/contacts', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ firstName: 'Max' }),
|
||||
});
|
||||
expect(result).toEqual(contact);
|
||||
});
|
||||
});
|
||||
|
||||
describe('update', () => {
|
||||
it('should PATCH existing contact', async () => {
|
||||
const contact = { id: 'c1', firstName: 'Maximilian' };
|
||||
mockFetch.mockResolvedValue({ contact });
|
||||
|
||||
const result = await contactsApi.update('c1', { firstName: 'Maximilian' });
|
||||
|
||||
expect(mockFetch).toHaveBeenCalledWith('/contacts/c1', {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify({ firstName: 'Maximilian' }),
|
||||
});
|
||||
expect(result).toEqual(contact);
|
||||
});
|
||||
});
|
||||
|
||||
describe('delete', () => {
|
||||
it('should DELETE contact', async () => {
|
||||
mockFetch.mockResolvedValue(undefined);
|
||||
|
||||
await contactsApi.delete('c1');
|
||||
|
||||
expect(mockFetch).toHaveBeenCalledWith('/contacts/c1', {
|
||||
method: 'DELETE',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('toggleFavorite', () => {
|
||||
it('should POST to favorite endpoint', async () => {
|
||||
const contact = { id: 'c1', isFavorite: true };
|
||||
mockFetch.mockResolvedValue({ contact });
|
||||
|
||||
const result = await contactsApi.toggleFavorite('c1');
|
||||
|
||||
expect(mockFetch).toHaveBeenCalledWith('/contacts/c1/favorite', {
|
||||
method: 'POST',
|
||||
});
|
||||
expect(result).toEqual(contact);
|
||||
});
|
||||
});
|
||||
|
||||
describe('toggleArchive', () => {
|
||||
it('should POST to archive endpoint', async () => {
|
||||
const contact = { id: 'c1', isArchived: true };
|
||||
mockFetch.mockResolvedValue({ contact });
|
||||
|
||||
const result = await contactsApi.toggleArchive('c1');
|
||||
|
||||
expect(mockFetch).toHaveBeenCalledWith('/contacts/c1/archive', {
|
||||
method: 'POST',
|
||||
});
|
||||
expect(result).toEqual(contact);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('notesApi', () => {
|
||||
describe('list', () => {
|
||||
it('should fetch notes for a contact', async () => {
|
||||
mockFetch.mockResolvedValue({ notes: [] });
|
||||
|
||||
const result = await notesApi.list('c1');
|
||||
|
||||
expect(mockFetch).toHaveBeenCalledWith('/contacts/c1/notes');
|
||||
expect(result).toEqual({ notes: [] });
|
||||
});
|
||||
});
|
||||
|
||||
describe('create', () => {
|
||||
it('should POST a new note', async () => {
|
||||
const note = { id: 'n1', content: 'Test note' };
|
||||
mockFetch.mockResolvedValue({ note });
|
||||
|
||||
const result = await notesApi.create('c1', { content: 'Test note' });
|
||||
|
||||
expect(mockFetch).toHaveBeenCalledWith('/contacts/c1/notes', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ content: 'Test note' }),
|
||||
});
|
||||
expect(result).toEqual({ note });
|
||||
});
|
||||
});
|
||||
|
||||
describe('update', () => {
|
||||
it('should PATCH a note', async () => {
|
||||
const note = { id: 'n1', content: 'Updated' };
|
||||
mockFetch.mockResolvedValue({ note });
|
||||
|
||||
const result = await notesApi.update('n1', { content: 'Updated' });
|
||||
|
||||
expect(mockFetch).toHaveBeenCalledWith('/notes/n1', {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify({ content: 'Updated' }),
|
||||
});
|
||||
expect(result).toEqual({ note });
|
||||
});
|
||||
});
|
||||
|
||||
describe('delete', () => {
|
||||
it('should DELETE a note', async () => {
|
||||
mockFetch.mockResolvedValue(undefined);
|
||||
|
||||
await notesApi.delete('n1');
|
||||
|
||||
expect(mockFetch).toHaveBeenCalledWith('/notes/n1', {
|
||||
method: 'DELETE',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('togglePin', () => {
|
||||
it('should POST to pin endpoint', async () => {
|
||||
const note = { id: 'n1', isPinned: true };
|
||||
mockFetch.mockResolvedValue({ note });
|
||||
|
||||
const result = await notesApi.togglePin('n1');
|
||||
|
||||
expect(mockFetch).toHaveBeenCalledWith('/notes/n1/pin', {
|
||||
method: 'POST',
|
||||
});
|
||||
expect(result).toEqual({ note });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('activitiesApi', () => {
|
||||
describe('list', () => {
|
||||
it('should fetch activities for a contact', async () => {
|
||||
mockFetch.mockResolvedValue({ activities: [] });
|
||||
|
||||
const result = await activitiesApi.list('c1');
|
||||
|
||||
expect(mockFetch).toHaveBeenCalledWith('/contacts/c1/activities');
|
||||
expect(result).toEqual({ activities: [] });
|
||||
});
|
||||
|
||||
it('should include limit parameter', async () => {
|
||||
mockFetch.mockResolvedValue({ activities: [] });
|
||||
|
||||
await activitiesApi.list('c1', 10);
|
||||
|
||||
expect(mockFetch).toHaveBeenCalledWith('/contacts/c1/activities?limit=10');
|
||||
});
|
||||
});
|
||||
|
||||
describe('create', () => {
|
||||
it('should POST a new activity', async () => {
|
||||
const activity = { id: 'a1', activityType: 'called' };
|
||||
mockFetch.mockResolvedValue({ activity });
|
||||
|
||||
const result = await activitiesApi.create('c1', {
|
||||
activityType: 'called',
|
||||
description: 'Called about project',
|
||||
});
|
||||
|
||||
expect(mockFetch).toHaveBeenCalledWith('/contacts/c1/activities', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
activityType: 'called',
|
||||
description: 'Called about project',
|
||||
}),
|
||||
});
|
||||
expect(result).toEqual({ activity });
|
||||
});
|
||||
});
|
||||
});
|
||||
155
apps/contacts/apps/web/src/lib/stores/filter.test.ts
Normal file
155
apps/contacts/apps/web/src/lib/stores/filter.test.ts
Normal file
|
|
@ -0,0 +1,155 @@
|
|||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
|
||||
// Mock $app/environment
|
||||
vi.mock('$app/environment', () => ({
|
||||
browser: true,
|
||||
}));
|
||||
|
||||
// Mock localStorage
|
||||
const mockStorage: Record<string, string> = {};
|
||||
const localStorageMock = {
|
||||
getItem: vi.fn((key: string) => mockStorage[key] || null),
|
||||
setItem: vi.fn((key: string, value: string) => {
|
||||
mockStorage[key] = value;
|
||||
}),
|
||||
removeItem: vi.fn((key: string) => {
|
||||
delete mockStorage[key];
|
||||
}),
|
||||
clear: vi.fn(() => {
|
||||
Object.keys(mockStorage).forEach((key) => delete mockStorage[key]);
|
||||
}),
|
||||
};
|
||||
Object.defineProperty(globalThis, 'localStorage', { value: localStorageMock });
|
||||
|
||||
import { contactsFilterStore } from './filter.svelte';
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
localStorageMock.clear();
|
||||
// Reset to defaults
|
||||
contactsFilterStore.resetFilters();
|
||||
});
|
||||
|
||||
describe('contactsFilterStore', () => {
|
||||
describe('default state', () => {
|
||||
it('should have default sort field', () => {
|
||||
expect(contactsFilterStore.sortField).toBe('lastName');
|
||||
});
|
||||
|
||||
it('should have default contact filter', () => {
|
||||
expect(contactsFilterStore.contactFilter).toBe('all');
|
||||
});
|
||||
|
||||
it('should have default birthday filter', () => {
|
||||
expect(contactsFilterStore.birthdayFilter).toBe('all');
|
||||
});
|
||||
|
||||
it('should have no selected tag', () => {
|
||||
expect(contactsFilterStore.selectedTagId).toBeNull();
|
||||
});
|
||||
|
||||
it('should have no selected company', () => {
|
||||
expect(contactsFilterStore.selectedCompany).toBeNull();
|
||||
});
|
||||
|
||||
it('should have toolbar collapsed by default', () => {
|
||||
expect(contactsFilterStore.isToolbarCollapsed).toBe(true);
|
||||
});
|
||||
|
||||
it('should have alphabet nav expanded by default', () => {
|
||||
expect(contactsFilterStore.isAlphabetNavCollapsed).toBe(false);
|
||||
});
|
||||
|
||||
it('should have empty search query', () => {
|
||||
expect(contactsFilterStore.searchQuery).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('setters', () => {
|
||||
it('should set sort field', () => {
|
||||
contactsFilterStore.setSortField('firstName');
|
||||
expect(contactsFilterStore.sortField).toBe('firstName');
|
||||
});
|
||||
|
||||
it('should set contact filter', () => {
|
||||
contactsFilterStore.setContactFilter('favorites');
|
||||
expect(contactsFilterStore.contactFilter).toBe('favorites');
|
||||
});
|
||||
|
||||
it('should set birthday filter', () => {
|
||||
contactsFilterStore.setBirthdayFilter('thisWeek');
|
||||
expect(contactsFilterStore.birthdayFilter).toBe('thisWeek');
|
||||
});
|
||||
|
||||
it('should set selected tag ID', () => {
|
||||
contactsFilterStore.setSelectedTagId('tag-1');
|
||||
expect(contactsFilterStore.selectedTagId).toBe('tag-1');
|
||||
});
|
||||
|
||||
it('should set selected company', () => {
|
||||
contactsFilterStore.setSelectedCompany('ACME');
|
||||
expect(contactsFilterStore.selectedCompany).toBe('ACME');
|
||||
});
|
||||
|
||||
it('should set search query without persisting', () => {
|
||||
contactsFilterStore.setSearchQuery('Max');
|
||||
expect(contactsFilterStore.searchQuery).toBe('Max');
|
||||
});
|
||||
});
|
||||
|
||||
describe('toggles', () => {
|
||||
it('should toggle toolbar collapsed state', () => {
|
||||
expect(contactsFilterStore.isToolbarCollapsed).toBe(true);
|
||||
contactsFilterStore.toggleToolbar();
|
||||
expect(contactsFilterStore.isToolbarCollapsed).toBe(false);
|
||||
contactsFilterStore.toggleToolbar();
|
||||
expect(contactsFilterStore.isToolbarCollapsed).toBe(true);
|
||||
});
|
||||
|
||||
it('should toggle alphabet nav collapsed state', () => {
|
||||
expect(contactsFilterStore.isAlphabetNavCollapsed).toBe(false);
|
||||
contactsFilterStore.toggleAlphabetNav();
|
||||
expect(contactsFilterStore.isAlphabetNavCollapsed).toBe(true);
|
||||
contactsFilterStore.toggleAlphabetNav();
|
||||
expect(contactsFilterStore.isAlphabetNavCollapsed).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('resetFilters', () => {
|
||||
it('should reset filters to defaults but keep toolbar/nav state', () => {
|
||||
contactsFilterStore.setContactFilter('favorites');
|
||||
contactsFilterStore.setBirthdayFilter('thisMonth');
|
||||
contactsFilterStore.setSelectedTagId('tag-1');
|
||||
contactsFilterStore.setSelectedCompany('ACME');
|
||||
contactsFilterStore.setSearchQuery('Max');
|
||||
contactsFilterStore.setToolbarCollapsed(false);
|
||||
|
||||
contactsFilterStore.resetFilters();
|
||||
|
||||
expect(contactsFilterStore.contactFilter).toBe('all');
|
||||
expect(contactsFilterStore.birthdayFilter).toBe('all');
|
||||
expect(contactsFilterStore.selectedTagId).toBeNull();
|
||||
expect(contactsFilterStore.selectedCompany).toBeNull();
|
||||
expect(contactsFilterStore.searchQuery).toBe('');
|
||||
// Toolbar state is preserved in resetFilters
|
||||
});
|
||||
});
|
||||
|
||||
describe('persistence', () => {
|
||||
it('should save state to localStorage on setter call', () => {
|
||||
contactsFilterStore.setSortField('firstName');
|
||||
|
||||
expect(localStorageMock.setItem).toHaveBeenCalledWith(
|
||||
'contacts-filter-state',
|
||||
expect.any(String)
|
||||
);
|
||||
});
|
||||
|
||||
it('should NOT persist search query', () => {
|
||||
vi.clearAllMocks();
|
||||
contactsFilterStore.setSearchQuery('test');
|
||||
|
||||
expect(localStorageMock.setItem).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
175
apps/contacts/apps/web/src/lib/utils/contact-parser.test.ts
Normal file
175
apps/contacts/apps/web/src/lib/utils/contact-parser.test.ts
Normal file
|
|
@ -0,0 +1,175 @@
|
|||
import { describe, it, expect } from 'vitest';
|
||||
import { parseContactInput, resolveContactIds, formatParsedContactPreview } from './contact-parser';
|
||||
|
||||
describe('parseContactInput', () => {
|
||||
it('should parse a simple name', () => {
|
||||
const result = parseContactInput('Max Mustermann');
|
||||
expect(result.displayName).toBe('Max Mustermann');
|
||||
expect(result.firstName).toBe('Max');
|
||||
expect(result.lastName).toBe('Mustermann');
|
||||
});
|
||||
|
||||
it('should parse a single name as firstName', () => {
|
||||
const result = parseContactInput('Max');
|
||||
expect(result.displayName).toBe('Max');
|
||||
expect(result.firstName).toBe('Max');
|
||||
expect(result.lastName).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should treat @ in email as company reference (known limitation)', () => {
|
||||
// extractAtReference runs before email extraction,
|
||||
// so "user@domain" is treated as @reference, not email.
|
||||
// This is a known parser limitation - emails with @ conflict with @company.
|
||||
const result = parseContactInput('Max Mustermann max@example.com');
|
||||
// The @ gets consumed by extractAtReference
|
||||
expect(result.company).toBeDefined();
|
||||
expect(result.email).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should parse email when preceded by bei company', () => {
|
||||
// When using bei/von for company, there's no @ to conflict
|
||||
const result = parseContactInput('Max Mustermann bei ACME');
|
||||
expect(result.company).toBe('ACME');
|
||||
expect(result.displayName).toBe('Max Mustermann');
|
||||
});
|
||||
|
||||
it('should parse name with @company (single word)', () => {
|
||||
const result = parseContactInput('Max Mustermann @ACME');
|
||||
expect(result.displayName).toBe('Max Mustermann');
|
||||
expect(result.company).toBe('ACME');
|
||||
});
|
||||
|
||||
it('should parse name with @company (multi-word leaves remainder in name)', () => {
|
||||
// extractAtReference only captures the first word after @
|
||||
const result = parseContactInput('Max Mustermann @ACME Corp');
|
||||
expect(result.company).toBe('ACME');
|
||||
expect(result.displayName).toContain('Max Mustermann');
|
||||
});
|
||||
|
||||
it('should parse name with bei company', () => {
|
||||
const result = parseContactInput('Anna Schmidt bei Google');
|
||||
expect(result.displayName).toBe('Anna Schmidt');
|
||||
expect(result.company).toBe('Google');
|
||||
});
|
||||
|
||||
it('should parse name with von company', () => {
|
||||
const result = parseContactInput('Peter Müller von Siemens');
|
||||
expect(result.displayName).toBe('Peter Müller');
|
||||
expect(result.company).toBe('Siemens');
|
||||
});
|
||||
|
||||
it('should parse name with phone (international)', () => {
|
||||
const result = parseContactInput('Max +49 123 456789');
|
||||
expect(result.displayName).toBe('Max');
|
||||
expect(result.phone).toBe('+49 123 456789');
|
||||
});
|
||||
|
||||
it('should parse name with phone (German format)', () => {
|
||||
const result = parseContactInput('Max 0123 456789');
|
||||
expect(result.displayName).toBe('Max');
|
||||
expect(result.phone).toBe('0123 456789');
|
||||
});
|
||||
|
||||
it('should parse tags', () => {
|
||||
const result = parseContactInput('Max #kunde #wichtig');
|
||||
expect(result.displayName).toBe('Max');
|
||||
expect(result.tagNames).toEqual(['kunde', 'wichtig']);
|
||||
});
|
||||
|
||||
it('should parse complex input with all fields', () => {
|
||||
const result = parseContactInput(
|
||||
'Max Mustermann @ACME max@example.com +49 123 456789 #kunde #wichtig'
|
||||
);
|
||||
expect(result.displayName).toContain('Max Mustermann');
|
||||
expect(result.company).toBe('ACME');
|
||||
expect(result.email).toBe('max@example.com');
|
||||
expect(result.phone).toBe('+49 123 456789');
|
||||
expect(result.tagNames).toEqual(['kunde', 'wichtig']);
|
||||
});
|
||||
|
||||
it('should handle empty input', () => {
|
||||
const result = parseContactInput('');
|
||||
expect(result.displayName).toBe('');
|
||||
expect(result.tagNames).toEqual([]);
|
||||
});
|
||||
|
||||
it('should handle only tags', () => {
|
||||
const result = parseContactInput('#privat #freunde');
|
||||
expect(result.tagNames).toEqual(['privat', 'freunde']);
|
||||
});
|
||||
|
||||
it('should handle multi-part last names', () => {
|
||||
const result = parseContactInput('Ludwig van Beethoven');
|
||||
expect(result.firstName).toBe('Ludwig');
|
||||
expect(result.lastName).toBe('van Beethoven');
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolveContactIds', () => {
|
||||
const tags = [
|
||||
{ id: 'tag-1', name: 'Kunde' },
|
||||
{ id: 'tag-2', name: 'Privat' },
|
||||
{ id: 'tag-3', name: 'Wichtig' },
|
||||
];
|
||||
|
||||
it('should resolve tag names to IDs (case-insensitive)', () => {
|
||||
const parsed = parseContactInput('Max #kunde #wichtig');
|
||||
const resolved = resolveContactIds(parsed, tags);
|
||||
expect(resolved.tagIds).toEqual(['tag-1', 'tag-3']);
|
||||
});
|
||||
|
||||
it('should skip unknown tags', () => {
|
||||
const parsed = parseContactInput('Max #unbekannt');
|
||||
const resolved = resolveContactIds(parsed, tags);
|
||||
expect(resolved.tagIds).toEqual([]);
|
||||
});
|
||||
|
||||
it('should preserve other fields', () => {
|
||||
const parsed = parseContactInput('Max bei TestCorp #kunde');
|
||||
const resolved = resolveContactIds(parsed, tags);
|
||||
expect(resolved.displayName).toBe('Max');
|
||||
expect(resolved.company).toBe('TestCorp');
|
||||
expect(resolved.tagIds).toEqual(['tag-1']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('formatParsedContactPreview', () => {
|
||||
it('should format company', () => {
|
||||
const parsed = parseContactInput('Max @ACME');
|
||||
expect(formatParsedContactPreview(parsed)).toContain('ACME');
|
||||
});
|
||||
|
||||
it('should format email', () => {
|
||||
// Construct parsed result directly to test formatting
|
||||
const parsed = {
|
||||
displayName: 'Max',
|
||||
firstName: 'Max',
|
||||
email: 'max@test.com',
|
||||
tagNames: [],
|
||||
};
|
||||
expect(formatParsedContactPreview(parsed)).toContain('max@test.com');
|
||||
});
|
||||
|
||||
it('should format phone', () => {
|
||||
const parsed = parseContactInput('Max +49 123 456');
|
||||
expect(formatParsedContactPreview(parsed)).toContain('+49 123 456');
|
||||
});
|
||||
|
||||
it('should format tags', () => {
|
||||
const parsed = parseContactInput('Max #kunde #privat');
|
||||
const preview = formatParsedContactPreview(parsed);
|
||||
expect(preview).toContain('kunde');
|
||||
expect(preview).toContain('privat');
|
||||
});
|
||||
|
||||
it('should return empty string for name-only input', () => {
|
||||
const parsed = parseContactInput('Max');
|
||||
expect(formatParsedContactPreview(parsed)).toBe('');
|
||||
});
|
||||
|
||||
it('should join parts with separator', () => {
|
||||
const parsed = parseContactInput('Max @ACME max@test.com');
|
||||
const preview = formatParsedContactPreview(parsed);
|
||||
expect(preview).toContain(' · ');
|
||||
});
|
||||
});
|
||||
|
|
@ -5,6 +5,7 @@ import { SvelteKitPWA } from '@vite-pwa/sveltekit';
|
|||
import { createPWAConfig } from '@manacore/shared-pwa';
|
||||
import { MANACORE_SHARED_PACKAGES } from '@manacore/shared-vite-config';
|
||||
|
||||
/// <reference types="vitest/config" />
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
tailwindcss(),
|
||||
|
|
@ -28,4 +29,9 @@ export default defineConfig({
|
|||
optimizeDeps: {
|
||||
exclude: [...MANACORE_SHARED_PACKAGES],
|
||||
},
|
||||
test: {
|
||||
environment: 'jsdom',
|
||||
include: ['src/**/*.test.ts'],
|
||||
globals: true,
|
||||
},
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue