test(frontend): task dialog, sharing avatars, trash, API key scopes, passwords (FLAW-19)
Covers opening and closing the task dialog (the former hook-order bug), date/time fields and the view-only mode for viewer lists, the avatar stack of shared items, the two-tap delete in the trash, scope selection and badges of API keys, and the shared password rule. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
4c50d15915
commit
0fdf46635b
5 files changed
+174
No files matched your search
@@ -0,0 +1,22 @@
|
|||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { render } from '@testing-library/react';
|
||||||
|
import { AvatarStack } from '../AvatarStack';
|
||||||
|
|
||||||
|
const people = ['Anna', 'Ben', 'Carla', 'Dana', 'Emil'].map((name, i) => ({ id: `u${i}`, name }));
|
||||||
|
|
||||||
|
describe('AvatarStack (who a list or note is shared with)', () => {
|
||||||
|
it('shows at most three people and the rest as +n', () => {
|
||||||
|
const { container } = render(<AvatarStack people={people} />);
|
||||||
|
expect(container.textContent).toContain('+2');
|
||||||
|
expect(container.firstElementChild?.children.length).toBe(4); // 3 avatars + overflow circle
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders nothing for an unshared item', () => {
|
||||||
|
expect(render(<AvatarStack people={[]} />).container.firstChild).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('needs no overflow circle when everyone fits', () => {
|
||||||
|
const { container } = render(<AvatarStack people={people.slice(0, 2)} />);
|
||||||
|
expect(container.textContent).not.toContain('+');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||||
|
import { render, screen, fireEvent, act } from '@testing-library/react';
|
||||||
|
|
||||||
|
const createAPIKey = vi.fn(async () => ({ id: 'k1', userId: 'u', name: 'HA', keyPrefix: 'mindsift_sec_ab', scopes: 'webhook:triage,webhook:list', createdAt: '' }));
|
||||||
|
vi.mock('../../../lib/api', () => ({
|
||||||
|
api: {
|
||||||
|
getAPIKeys: vi.fn(async () => [
|
||||||
|
{ id: 'k0', userId: 'u', name: 'Rauchmelder', keyPrefix: 'mindsift_sec_xy', scopes: 'webhook:triage,webhook:task', createdAt: '' },
|
||||||
|
]),
|
||||||
|
getAllLists: vi.fn(async () => []),
|
||||||
|
getTaskLists: vi.fn(async () => []),
|
||||||
|
createAPIKey: (...args: unknown[]) => createAPIKey(...(args as [])),
|
||||||
|
deleteAPIKey: vi.fn(),
|
||||||
|
testWebhookIngest: vi.fn(),
|
||||||
|
},
|
||||||
|
triggerHaptic: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
import { ApiKeysPanel } from '../ApiKeysPanel';
|
||||||
|
|
||||||
|
describe('ApiKeysPanel', () => {
|
||||||
|
beforeEach(() => createAPIKey.mockClear());
|
||||||
|
|
||||||
|
it('shows the scopes of existing keys as badges', async () => {
|
||||||
|
render(<ApiKeysPanel />);
|
||||||
|
expect(await screen.findByText('Rauchmelder')).toBeTruthy();
|
||||||
|
expect(screen.getAllByText('📋 Aufgaben').length).toBeGreaterThan(1); // chip + badge
|
||||||
|
});
|
||||||
|
|
||||||
|
it('creates a key with the chosen scopes and never without one', async () => {
|
||||||
|
render(<ApiKeysPanel />);
|
||||||
|
const triage = screen.getByRole('checkbox', { name: /Triage/ });
|
||||||
|
fireEvent.click(triage); // last remaining scope cannot be switched off
|
||||||
|
expect(triage.getAttribute('aria-checked')).toBe('true');
|
||||||
|
fireEvent.click(screen.getByRole('checkbox', { name: /Listeneinträge/ }));
|
||||||
|
await act(async () => {
|
||||||
|
fireEvent.click(screen.getByRole('button', { name: /API-Key erzeugen/ }));
|
||||||
|
});
|
||||||
|
expect(createAPIKey).toHaveBeenCalledWith('Home Assistant', ['webhook:triage', 'webhook:list']);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
import { describe, it, expect, vi } from 'vitest';
|
||||||
|
import { render, screen, fireEvent } from '@testing-library/react';
|
||||||
|
import { TaskDetailModal } from '../TaskDetailModal';
|
||||||
|
import { Thought } from '../../../lib/types';
|
||||||
|
|
||||||
|
const task: Thought = {
|
||||||
|
id: 'th_1',
|
||||||
|
userId: 'usr_1',
|
||||||
|
content: 'Fenster putzen',
|
||||||
|
tags: [],
|
||||||
|
priority: 'normal',
|
||||||
|
source: 'manual',
|
||||||
|
status: 'converted',
|
||||||
|
isCompleted: false,
|
||||||
|
kanbanStatus: 'todo',
|
||||||
|
dueDate: '2026-10-30T14:30',
|
||||||
|
createdAt: '2026-10-29T08:00:00Z',
|
||||||
|
};
|
||||||
|
const columns = [
|
||||||
|
{ id: 'todo', name: 'Zu erledigen' },
|
||||||
|
{ id: 'done', name: 'Erledigt', isDone: true },
|
||||||
|
];
|
||||||
|
const noop = vi.fn(async () => {});
|
||||||
|
|
||||||
|
const props = (over: Partial<React.ComponentProps<typeof TaskDetailModal>> = {}) => ({
|
||||||
|
task,
|
||||||
|
isOpen: true,
|
||||||
|
onClose: vi.fn(),
|
||||||
|
activeColumns: columns,
|
||||||
|
taskLists: [],
|
||||||
|
onUpdateTitle: noop,
|
||||||
|
onUpdateStatus: noop,
|
||||||
|
onUpdatePriority: noop,
|
||||||
|
onUpdateDueDate: vi.fn(async () => {}),
|
||||||
|
onUpdateTaskList: noop,
|
||||||
|
onUpdateNotes: noop,
|
||||||
|
onToggleSubtask: noop,
|
||||||
|
onAddSubtask: noop,
|
||||||
|
onDeleteSubtask: noop,
|
||||||
|
onConvertToDoc: vi.fn(),
|
||||||
|
onDeleteTask: vi.fn(),
|
||||||
|
...over,
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('TaskDetailModal', () => {
|
||||||
|
it('opens and closes without breaking the hook order', () => {
|
||||||
|
const { rerender, container } = render(<TaskDetailModal {...props({ isOpen: false })} />);
|
||||||
|
expect(container.firstChild).toBeNull();
|
||||||
|
rerender(<TaskDetailModal {...props()} />);
|
||||||
|
expect(screen.getByText('Aufgabe bearbeiten')).toBeTruthy();
|
||||||
|
rerender(<TaskDetailModal {...props({ isOpen: false })} />);
|
||||||
|
rerender(<TaskDetailModal {...props()} />);
|
||||||
|
expect(screen.getByText('Aufgabe bearbeiten')).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows date and time separately and removes only the time', () => {
|
||||||
|
const onUpdateDueDate = vi.fn(async () => {});
|
||||||
|
render(<TaskDetailModal {...props({ onUpdateDueDate })} />);
|
||||||
|
expect((screen.getByLabelText('Fälligkeitsdatum') as HTMLInputElement).value).toBe('2026-10-30');
|
||||||
|
expect((screen.getByLabelText('Uhrzeit (optional)') as HTMLInputElement).value).toBe('14:30');
|
||||||
|
fireEvent.click(screen.getByTitle(/Uhrzeit entfernen/));
|
||||||
|
expect(onUpdateDueDate).toHaveBeenCalledWith(task, '2026-10-30');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('is view-only for tasks in lists shared with viewer rights', () => {
|
||||||
|
render(<TaskDetailModal {...props({ readOnly: true })} />);
|
||||||
|
expect(screen.getByText('Aufgabe ansehen')).toBeTruthy();
|
||||||
|
expect((screen.getByLabelText('Fälligkeitsdatum') as HTMLInputElement).closest('fieldset')?.disabled).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import { describe, it, expect, vi } from 'vitest';
|
||||||
|
import { render, screen, fireEvent, act } from '@testing-library/react';
|
||||||
|
import { TrashView } from '../TrashView';
|
||||||
|
|
||||||
|
const items = [
|
||||||
|
{ id: 'doc_1', title: 'Urlaub', icon: '🏔️', folderPath: 'Privat', deletedAt: '2026-10-29T08:00:00Z', isOwner: true },
|
||||||
|
];
|
||||||
|
|
||||||
|
describe('TrashView', () => {
|
||||||
|
it('deletes for good only on the second tap', async () => {
|
||||||
|
const onPurge = vi.fn(async () => {});
|
||||||
|
render(<TrashView items={items} onOpen={vi.fn()} onRestore={vi.fn(async () => {})} onPurge={onPurge} onEmpty={vi.fn(async () => {})} onBackMobile={vi.fn()} />);
|
||||||
|
fireEvent.click(screen.getByText('Endgültig löschen'));
|
||||||
|
expect(onPurge).not.toHaveBeenCalled();
|
||||||
|
await act(async () => {
|
||||||
|
fireEvent.click(screen.getByText('Wirklich löschen?'));
|
||||||
|
});
|
||||||
|
expect(onPurge).toHaveBeenCalledWith('doc_1');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('restores with one tap and shows the original folder', async () => {
|
||||||
|
const onRestore = vi.fn(async () => {});
|
||||||
|
render(<TrashView items={items} onOpen={vi.fn()} onRestore={onRestore} onPurge={vi.fn(async () => {})} onEmpty={vi.fn(async () => {})} onBackMobile={vi.fn()} />);
|
||||||
|
expect(screen.getByText('Privat')).toBeTruthy();
|
||||||
|
await act(async () => {
|
||||||
|
fireEvent.click(screen.getByText('Wiederherstellen'));
|
||||||
|
});
|
||||||
|
expect(onRestore).toHaveBeenCalledWith('doc_1');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { passwordProblem } from '../password';
|
||||||
|
|
||||||
|
describe('passwordProblem (same rule as the server)', () => {
|
||||||
|
it('needs 8 characters, counted as characters', () => {
|
||||||
|
expect(passwordProblem('kurz')).toMatch(/mindestens 8 Zeichen/);
|
||||||
|
expect(passwordProblem(' ')).toMatch(/mindestens 8 Zeichen/);
|
||||||
|
expect(passwordProblem('geheim12')).toBeNull();
|
||||||
|
expect(passwordProblem('äöüßäöüß')).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in new issue
Block a user