diff --git a/frontend/src/routes/persons/[id]/edit/NameHistoryEditCard.svelte b/frontend/src/routes/persons/[id]/edit/NameHistoryEditCard.svelte index a7e6c185..0273b5df 100644 --- a/frontend/src/routes/persons/[id]/edit/NameHistoryEditCard.svelte +++ b/frontend/src/routes/persons/[id]/edit/NameHistoryEditCard.svelte @@ -1,6 +1,7 @@ @@ -65,7 +75,7 @@ function confirmDelete(id: string) { {#if canWrite} -
{ - return async ({ update }) => { - showDeleteModal = false; - deleteTargetId = null; - await update(); - }; - }} - > - - -
- - - -{/if} diff --git a/frontend/src/routes/persons/[id]/edit/NameHistoryEditCard.svelte.spec.ts b/frontend/src/routes/persons/[id]/edit/NameHistoryEditCard.svelte.spec.ts new file mode 100644 index 00000000..271cf4aa --- /dev/null +++ b/frontend/src/routes/persons/[id]/edit/NameHistoryEditCard.svelte.spec.ts @@ -0,0 +1,118 @@ +import { describe, it, expect, vi, afterEach } from 'vitest'; +import { cleanup, render } from 'vitest-browser-svelte'; +import { page } from 'vitest/browser'; +import NameHistoryEditCard from './NameHistoryEditCard.svelte'; +import { createConfirmService, CONFIRM_KEY } from '$lib/services/confirm.svelte.js'; + +vi.mock('$app/forms', () => ({ enhance: () => () => {} })); + +afterEach(cleanup); + +const aliases = [ + { id: 'a1', lastName: 'Müller', firstName: 'Anna', type: 'BIRTH', sortOrder: 0 }, + { id: 'a2', lastName: 'Schmidt', firstName: null, type: 'MARRIED', sortOrder: 1 } +]; + +function renderCard(overrides: Record = {}) { + const service = createConfirmService(); + const result = render(NameHistoryEditCard, { + props: { aliases, canWrite: true, ...overrides }, + context: new Map([[CONFIRM_KEY, service]]) + }); + return { ...result, service }; +} + +// ─── Rendering ──────────────────────────────────────────────────────────────── + +describe('NameHistoryEditCard — rendering', () => { + it('renders alias last names', async () => { + renderCard(); + await expect.element(page.getByText('Müller')).toBeInTheDocument(); + await expect.element(page.getByText('Schmidt')).toBeInTheDocument(); + }); + + it('renders delete buttons when canWrite', async () => { + renderCard(); + const btns = document.querySelectorAll('button[type="button"]'); + expect(btns.length).toBeGreaterThanOrEqual(2); + }); + + it('does not render delete buttons when canWrite is false', async () => { + renderCard({ canWrite: false }); + const btns = document.querySelectorAll('button[type="button"]'); + expect(btns.length).toBe(0); + }); + + it('does not show the inline delete modal (replaced by ConfirmService)', async () => { + renderCard(); + // The old inline modal div with "fixed inset-0" should not exist + const modal = document.querySelector('.fixed.inset-0'); + expect(modal).toBeNull(); + }); +}); + +// ─── Delete confirmation ────────────────────────────────────────────────────── + +describe('NameHistoryEditCard — delete confirmation', () => { + it('opens confirm dialog when delete button is clicked', async () => { + const { service } = renderCard(); + const deleteBtn = document.querySelector('button[type="button"]')!; + deleteBtn.click(); + await vi.waitFor(() => expect(service.options).not.toBeNull()); + expect(service.options?.destructive).toBe(true); + service.settle(false); + }); + + it('submits removeAlias form when user confirms', async () => { + const { service } = renderCard(); + const requestSubmit = vi + .spyOn(HTMLFormElement.prototype, 'requestSubmit') + .mockImplementation(() => {}); + + const deleteBtn = document.querySelector('button[type="button"]')!; + deleteBtn.click(); + await vi.waitFor(() => expect(service.options).not.toBeNull()); + service.settle(true); + + // Wait for the async handleDelete callback to call requestSubmit + await vi.waitFor(() => expect(requestSubmit).toHaveBeenCalledOnce()); + requestSubmit.mockRestore(); + }); + + it('does not submit form when user cancels', async () => { + const { service } = renderCard(); + const requestSubmit = vi + .spyOn(HTMLFormElement.prototype, 'requestSubmit') + .mockImplementation(() => {}); + + const deleteBtn = document.querySelector('button[type="button"]')!; + deleteBtn.click(); + await vi.waitFor(() => expect(service.options).not.toBeNull()); + service.settle(false); + await vi.waitFor(() => expect(service.options).toBeNull()); + + // Allow pending microtasks to flush before asserting + await Promise.resolve(); + expect(requestSubmit).not.toHaveBeenCalled(); + requestSubmit.mockRestore(); + }); + + it('submits with the correct aliasId when second alias is deleted', async () => { + const { service } = renderCard(); + const requestSubmit = vi + .spyOn(HTMLFormElement.prototype, 'requestSubmit') + .mockImplementation(() => {}); + + // Click delete button for the second alias (Schmidt) + const deleteBtns = document.querySelectorAll('button[type="button"]'); + deleteBtns[1].click(); + await vi.waitFor(() => expect(service.options).not.toBeNull()); + service.settle(true); + + await vi.waitFor(() => expect(requestSubmit).toHaveBeenCalledOnce()); + const submittedForm = requestSubmit.mock.instances[0] as HTMLFormElement; + const aliasIdInput = submittedForm.querySelector('input[name="aliasId"]'); + expect(aliasIdInput?.value).toBe('a2'); + requestSubmit.mockRestore(); + }); +}); diff --git a/frontend/src/routes/persons/[id]/edit/NameHistoryEditCard.svelte.test.ts b/frontend/src/routes/persons/[id]/edit/NameHistoryEditCard.svelte.test.ts index ce5510cd..1073aed2 100644 --- a/frontend/src/routes/persons/[id]/edit/NameHistoryEditCard.svelte.test.ts +++ b/frontend/src/routes/persons/[id]/edit/NameHistoryEditCard.svelte.test.ts @@ -1,84 +1,100 @@ -import { describe, it, expect } from 'vitest'; -import { render } from 'vitest-browser-svelte'; +import { describe, it, expect, afterEach, vi } from 'vitest'; +import { cleanup, render } from 'vitest-browser-svelte'; import { page } from 'vitest/browser'; import NameHistoryEditCard from './NameHistoryEditCard.svelte'; +import { createConfirmService, CONFIRM_KEY } from '$lib/services/confirm.svelte.js'; + +vi.mock('$app/forms', () => ({ enhance: () => () => {} })); + +afterEach(cleanup); const aliases = [ { id: 'a1', lastName: 'de Gruyter', firstName: null, type: 'BIRTH', sortOrder: 0 }, { id: 'a2', lastName: 'Schmidt', firstName: 'Maria', type: 'WIDOWED', sortOrder: 1 } ]; +function renderCard(props: Record = {}) { + const service = createConfirmService(); + return { + ...render(NameHistoryEditCard, { + props: { aliases, canWrite: true, ...props }, + context: new Map([[CONFIRM_KEY, service]]) + }), + service + }; +} + describe('NameHistoryEditCard', () => { it('should render alias rows when aliases exist', async () => { - render(NameHistoryEditCard, { aliases, canWrite: true }); + renderCard(); await expect.element(page.getByText('de Gruyter')).toBeInTheDocument(); await expect.element(page.getByText('Schmidt')).toBeInTheDocument(); }); it('should show empty state when no aliases', async () => { - render(NameHistoryEditCard, { aliases: [], canWrite: true }); + renderCard({ aliases: [] }); const emptyText = document.querySelector('.italic'); expect(emptyText).not.toBeNull(); }); it('should show add form when canWrite is true', async () => { - render(NameHistoryEditCard, { aliases: [], canWrite: true }); + renderCard({ aliases: [] }); const form = document.querySelector('form[action="?/addAlias"]'); expect(form).not.toBeNull(); }); it('should hide add form when canWrite is false', async () => { - render(NameHistoryEditCard, { aliases: [], canWrite: false }); + renderCard({ aliases: [], canWrite: false }); const form = document.querySelector('form[action="?/addAlias"]'); expect(form).toBeNull(); }); it('should hide delete buttons when canWrite is false', async () => { - render(NameHistoryEditCard, { aliases, canWrite: false }); + renderCard({ canWrite: false }); const deleteButtons = document.querySelectorAll('button[aria-label*="Entfernen"]'); expect(deleteButtons.length).toBe(0); }); it('should show delete buttons when canWrite is true', async () => { - render(NameHistoryEditCard, { aliases, canWrite: true }); + renderCard(); const deleteButtons = document.querySelectorAll('button[aria-label*="Entfernen"]'); expect(deleteButtons.length).toBe(2); }); it('should include alias name in delete button aria-label', async () => { - render(NameHistoryEditCard, { aliases: [aliases[0]], canWrite: true }); + renderCard({ aliases: [aliases[0]] }); const btn = document.querySelector('button[aria-label*="de Gruyter"]'); expect(btn).not.toBeNull(); }); - it('should show delete modal when delete button is clicked', async () => { - render(NameHistoryEditCard, { aliases: [aliases[0]], canWrite: true }); + it('should open ConfirmService dialog (not inline modal) when delete button is clicked', async () => { + const { service } = renderCard({ aliases: [aliases[0]] }); - const deleteBtn = document.querySelector('button[aria-label*="de Gruyter"]')!; - deleteBtn.dispatchEvent(new MouseEvent('click', { bubbles: true })); + const deleteBtn = document.querySelector( + 'button[aria-label*="de Gruyter"]' + ) as HTMLButtonElement; + deleteBtn.click(); - await expect.element(page.getByText('Alias entfernen?')).toBeInTheDocument(); + await vi.waitFor(() => expect(service.options).not.toBeNull()); + expect(service.options?.destructive).toBe(true); + service.settle(false); }); it('should show alias error when provided', async () => { - render(NameHistoryEditCard, { - aliases: [], - canWrite: true, - aliasError: 'Something went wrong' - }); + renderCard({ aliases: [], aliasError: 'Something went wrong' }); await expect.element(page.getByText('Something went wrong')).toBeInTheDocument(); }); it('should have required attribute on lastName input', async () => { - render(NameHistoryEditCard, { aliases: [], canWrite: true }); + renderCard({ aliases: [] }); const input = document.querySelector('input[name="lastName"]') as HTMLInputElement; expect(input.required).toBe(true);