Phase 6.5: Tag-Forms on RHF + Zod

- TagPicker create-tag form: RHF+Zod (name required, color optional with default)
- Error display under name field
- Color picker uses setTagValue from RHF
- Preserved all existing functionality: search, assign, unassign
- Added 2 validation tests (empty name, valid submit)
This commit is contained in:
Agent Zero
2026-07-24 00:33:57 +02:00
parent eb2f37b2bc
commit 3e8038b75e
2 changed files with 96 additions and 17 deletions
@@ -0,0 +1,66 @@
import React from 'react';
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
vi.mock('@/api/tags', () => ({
fetchTags: vi.fn().mockResolvedValue([]),
assignTag: vi.fn().mockResolvedValue({}),
unassignTag: vi.fn().mockResolvedValue({}),
createTag: vi.fn().mockResolvedValue({ id: 't1', name: 'NewTag', color: '#3B82F6' }),
}));
vi.mock('@/components/ui/Toast', () => ({
useToast: () => ({ success: vi.fn(), error: vi.fn(), info: vi.fn(), warning: vi.fn() }),
}));
import { TagPicker } from '@/components/tags/TagPicker';
import { createTag } from '@/api/tags';
beforeEach(() => {
vi.clearAllMocks();
});
describe('TagPicker validation (RHF + Zod)', () => {
it('shows validation error when tag name is empty', async () => {
render(<TagPicker entityType="contact" entityId="e1" />);
// Wait for loading to finish and find create button
await waitFor(() => {
expect(screen.getByTestId('tag-picker')).toBeInTheDocument();
}, { timeout: 3000 });
// Click create button to show form (German: 'Neues Tag')
const createBtn = screen.getByText(/neues tag|create/i);
fireEvent.click(createBtn);
// Submit empty form
await waitFor(() => {
expect(screen.getByTestId('create-tag-form')).toBeInTheDocument();
});
const form = screen.getByTestId('create-tag-form').querySelector('form');
expect(form).toBeTruthy();
fireEvent.submit(form!);
await waitFor(() => {
const errors = screen.getAllByText(/erforderlich|required/i);
expect(errors.length).toBeGreaterThan(0);
});
});
it('calls createTag when form is valid', async () => {
render(<TagPicker entityType="contact" entityId="e1" />);
await waitFor(() => {
expect(screen.getByTestId('tag-picker')).toBeInTheDocument();
}, { timeout: 3000 });
const createBtn = screen.getByText(/neues tag|create/i);
fireEvent.click(createBtn);
await waitFor(() => {
expect(screen.getByTestId('create-tag-form')).toBeInTheDocument();
});
// Fill in tag name
const form = screen.getByTestId('create-tag-form').querySelector('form');
const input = form!.querySelector('input');
expect(input).toBeTruthy();
fireEvent.change(input!, { target: { value: 'Important' } });
fireEvent.submit(form!);
await waitFor(() => {
expect(createTag).toHaveBeenCalledWith(expect.objectContaining({ name: 'Important' }));
}, { timeout: 3000 });
});
});