diff --git a/src/lib/components/NameItemEditor.svelte b/src/lib/components/NameItemEditor.svelte index 58c4297..e39143e 100644 --- a/src/lib/components/NameItemEditor.svelte +++ b/src/lib/components/NameItemEditor.svelte @@ -11,11 +11,10 @@ } export let list: ListItem[] = []; - export let currentName: string = ''; + export let currentName: string; export let onSave: (n: string, o: string) => unknown = () => {}; export let onDelete: (n: string) => unknown = () => {}; - // lokaler State let localName = currentName; let isEditing = false; let inputRef: HTMLInputElement | null = null; diff --git a/src/routes/(token-based)/list/[vorgang]/+page.svelte b/src/routes/(token-based)/list/[vorgang]/+page.svelte index e22bc2c..6a429c3 100644 --- a/src/routes/(token-based)/list/[vorgang]/+page.svelte +++ b/src/routes/(token-based)/list/[vorgang]/+page.svelte @@ -26,7 +26,6 @@ // add other properties as needed } - // 2) Lokaler, reaktiver State mit $state let crimesList = $state(data.crimesList); let vorgangName: string = data.vorgang.vorgangName; const vorgangPIN: string = data.vorgang.vorgangPIN; @@ -38,7 +37,6 @@ let inProgress = $state(false); let isError = $state(false); - //Variable um nur admin UI anzuzeigen let admin = $state(data?.user?.admin); async function handleSave(newName: string, oldName: string) { diff --git a/tests/ComponentNameItemEditor.test.ts b/tests/ComponentNameItemEditor.test.ts new file mode 100644 index 0000000..ff1d504 --- /dev/null +++ b/tests/ComponentNameItemEditor.test.ts @@ -0,0 +1,144 @@ +import { fireEvent, render, screen } from '@testing-library/svelte'; +import { describe, expect, it, test, vi } from "vitest"; +import NameItemEditor from '$lib/components/NameItemEditor.svelte'; +import { baseData } from './fixtures'; + + + +const testCrimesListIndex = 0; +const testItem = baseData.crimesList[testCrimesListIndex]; + const testCurrentName = testItem.name; + const testLocalName = 'Fall-C' + + +describe('NameItemEditor - Funktionalität', () => { + const onSave = vi.fn(); + const onDelete = vi.fn(); + const baseProps = { + list: baseData.crimesList, + currentName: testCurrentName, + onSave, + onDelete} + + test.todo('FocusIn nach Klick auf edit') + + it('zeigt initial Edit/Delete Buttons und aktuellen Namen', () => { + render(NameItemEditor, {props: baseProps}); + + expect(screen.getByTestId('edit-button')).toBeInTheDocument(); + expect(screen.getByTestId('delete-button')).toBeInTheDocument(); + expect(screen.queryByTestId('commit-button')).toBeNull(); + expect(screen.queryByTestId('cancel-button')).toBeNull(); + expect(screen.getByText(testCurrentName)).toBeInTheDocument(); + }); + + it('wechselt zu Commit/Cancel nach Klick auf Edit', async () => { + render(NameItemEditor, {props: baseProps}); + await fireEvent.click(screen.getByTestId('edit-button')); + const input = screen.getByTestId('test-input'); + + expect(screen.getByTestId('commit-button')).toBeInTheDocument(); + expect(screen.getByTestId('cancel-button')).toBeInTheDocument(); + expect(screen.queryByTestId('edit-button')).toBeNull(); + expect(screen.queryByTestId('delete-button')).toBeNull(); + expect(screen.getAllByRole('textbox')).toHaveLength(1); + expect(input).toHaveValue(testCurrentName); + }); + + it('zeigt Fehlermeldung bei leerem Namen', async () => { + render(NameItemEditor, { props: baseProps }); + await fireEvent.click(screen.getByTestId('edit-button')); + + const input = screen.getByTestId('test-input'); + await fireEvent.input(input, { target: { value: '' } }); + + expect(screen.getByText('Name darf nicht leer sein.')).toBeInTheDocument(); + expect(onSave).not.toHaveBeenCalled(); + }); + + it('entfernt Fehlermeldung live beim nächsten gültigen Tastendruck', async () => { + render(NameItemEditor, { + props: { + list: baseData.crimesList, + currentName: baseData.crimesList[0].name, + onSave: vi.fn(), + onDelete: vi.fn() + } + }); + + await fireEvent.click(screen.getByTestId('edit-button')); + const input = screen.getByTestId('test-input'); + + await fireEvent.input(input, { target: { value: '' } }); + expect(screen.getByText('Name darf nicht leer sein.')).toBeInTheDocument(); + + await fireEvent.input(input, { target: { value: 'Fall-C' } }); + + expect(screen.queryByText('Name darf nicht leer sein.')).toBeNull(); +}); + + it('zeigt Fehlermeldung bei Duplikat', async () => { + const duplicateName = baseData.crimesList[1].name; + render(NameItemEditor, { props: baseProps }); + await fireEvent.click(screen.getByTestId('edit-button')); + + const input = screen.getByTestId('test-input'); + await fireEvent.input(input, { target: { value: duplicateName } }); + + expect(screen.getByText('Name existiert bereits.')).toBeInTheDocument(); + expect(onSave).not.toHaveBeenCalled(); + }); + + it('ruft onSave korrekt auf bei gültigem Namen', async () => { + render(NameItemEditor, { props: baseProps }); + await fireEvent.click(screen.getByTestId('edit-button')); + + const input = screen.getByTestId('test-input'); + await fireEvent.input(input, { target: { value: testLocalName } }); + await fireEvent.click(screen.getByTestId('commit-button')); + + expect(onSave).toHaveBeenCalledWith(testLocalName, testCurrentName); + }); + + it('ruft onDelete korrekt auf', async () => { + render(NameItemEditor, { props: baseProps }); + await fireEvent.click(screen.getByTestId('delete-button')); + + expect(onDelete).toHaveBeenCalledWith(testCurrentName); + }); + + it('setzt Zustand zurück bei Cancel', async () => { + render(NameItemEditor, { props: baseProps }); + await fireEvent.click(screen.getByTestId('edit-button')); + + const input = screen.getByTestId('test-input'); + await fireEvent.input(input, { target: { value: 'Zwischentext' } }); + await fireEvent.click(screen.getByTestId('cancel-button')); + + expect(screen.getByText(testCurrentName)).toBeInTheDocument(); + expect(screen.getByTestId('edit-button')).toBeInTheDocument(); + }); + + it('triggert Save bei Enter-Taste', async () => { + render(NameItemEditor, { props: baseProps }); + await fireEvent.click(screen.getByTestId('edit-button')); + + const input = screen.getByTestId('test-input'); + await fireEvent.input(input, { target: { value: 'ViaEnter' } }); + await fireEvent.keyDown(input, { key: 'Enter' }); + + expect(onSave).toHaveBeenCalledWith('ViaEnter', testCurrentName); + }); + + it('bricht ab bei Escape-Taste', async () => { + render(NameItemEditor, { props: baseProps }); + await fireEvent.click(screen.getByTestId('edit-button')); + + const input = screen.getByTestId('test-input'); + await fireEvent.input(input, { target: { value: 'Zwischentext' } }); + await fireEvent.keyDown(input, { key: 'Escape' }); + + expect(screen.getByText(testCurrentName)).toBeInTheDocument(); + expect(onSave).not.toHaveBeenCalled(); + }); +}); diff --git a/tests/ComponentNameItemEditor.view.test.ts b/tests/ComponentNameItemEditor.view.test.ts deleted file mode 100644 index d25b7a9..0000000 --- a/tests/ComponentNameItemEditor.view.test.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { fireEvent, render } from '@testing-library/svelte'; -import { describe, expect, it, vi } from "vitest"; -import NameItemEditor from '$lib/components/NameItemEditor.svelte'; -import { baseData } from './fixtures'; - - - -const testCrimesListIndex = 0; -const testItem = baseData.crimesList[testCrimesListIndex]; - const editedName = baseData.crimeNames[testCrimesListIndex]; - const currentName = testItem.name; - const onSave = vi.fn(); - const onDelete = vi.fn(); - -describe('Button-Anzeige mit Icons', () => { - const baseProps = {list: baseData.crimesList, editedName, currentName, onDelete, onSave} - it('zeigt Edit/Delete Buttons initial', () => { - const { getByTestId, queryByTestId } = render(NameItemEditor, { - props: baseProps - - }); - - expect(getByTestId('edit-button')).toBeInTheDocument(); - expect(getByTestId('delete-button')).toBeInTheDocument(); - expect(queryByTestId('commit-button')).toBeNull(); - expect(queryByTestId('cancel-button')).toBeNull(); - }); - - it('zeigt Commit/Cancel Buttons nach Klick auf Edit', async () => { - const { getByTestId, queryByTestId } = render(NameItemEditor, { - props: baseProps - }); - - await fireEvent.click(getByTestId('edit-button')); - - expect(getByTestId('commit-button')).toBeInTheDocument(); - expect(getByTestId('cancel-button')).toBeInTheDocument(); - expect(queryByTestId('edit-button')).toBeNull(); - expect(queryByTestId('delete-button')).toBeNull(); - }); - - -}); diff --git a/tests/TatortList.test.ts b/tests/TatortList.test.ts index dfcf3c8..459499e 100644 --- a/tests/TatortList.test.ts +++ b/tests/TatortList.test.ts @@ -1,4 +1,3 @@ -// @vitest-environment jsdom import { render, fireEvent, screen, within } from '@testing-library/svelte'; import { describe, it, expect, vi, test } from 'vitest'; import * as nav from '$app/navigation'; @@ -6,7 +5,6 @@ import TatortListPage from '../src/routes/(token-based)/list/[vorgang]/+page.sve import { baseData } from './fixtures'; import { tick } from 'svelte'; -// Mock für invalidateAll vi.spyOn(nav, 'invalidateAll').mockResolvedValue(); global.fetch = vi.fn().mockResolvedValue({ ok: true }); @@ -16,7 +14,6 @@ describe('Seite: Vorgangsansicht', () => { describe('Szenario: Admin + Liste gefüllt - Funktionalität', () => { test.todo('Share Link Link generierung richtig'); - it('führt PUT-Request aus und aktualisiert UI nach onSave', async () => { const data = structuredClone(baseData); const oldName = data.crimesList[0].name; @@ -24,19 +21,15 @@ describe('Seite: Vorgangsansicht', () => { render(TatortListPage, { props: { data } }); const listItem = screen.getAllByTestId('test-list-item')[0]; - // teste ob alter Name angezeigt: expect(listItem).toHaveTextContent(oldName); - // Editmodus await fireEvent.click(within(listItem).getByTestId('edit-button')); const input = within(listItem).getByTestId('test-input'); await fireEvent.input(input, { target: { value: newName } }); - // Commit await fireEvent.click(within(listItem).getByTestId('commit-button')); - await Promise.resolve(); // wartet reaktive Updates ab + await tick(); - // FETCH-CHECK expect(global.fetch).toHaveBeenCalledWith( `/api/list/${data.vorgang.vorgangToken}/${oldName}`, expect.objectContaining({ @@ -50,10 +43,7 @@ describe('Seite: Vorgangsansicht', () => { }) ); - // INVALIDATE-CHECK expect(nav.invalidateAll).toHaveBeenCalled(); - - // UI-UPDATE expect(within(listItem).getByText(newName)).toBeInTheDocument(); }); @@ -61,26 +51,21 @@ describe('Seite: Vorgangsansicht', () => { const testData = structuredClone(baseData); const oldName = testData.crimesList[0].name; - // Rendern und initiale Liste prüfen render(TatortListPage, { props: { data: testData } }); const initialItems = screen.getAllByTestId('test-list-item'); expect(initialItems).toHaveLength(testData.crimesList.length); const listItem = screen.getAllByTestId('test-list-item')[0]; - // teste ob alter Name angezeigt: expect(listItem).toHaveTextContent(oldName); - // Delete-Button klicken const del = within(listItem).getByTestId('delete-button'); expect(del).toBeInTheDocument() await fireEvent.click(within(listItem).getByTestId('delete-button')); - // auf reaktive Updates warten await tick(); - // FETCH-CHECK: URL & Payload - // entspricht: new URL(data.url).pathname + '/' + oldName - const expectedPath = new URL(testData.url).pathname; + let expectedPath = new URL(testData.url).pathname; + expectedPath += `/${oldName}` expect(global.fetch).toHaveBeenCalledWith( - `/api${expectedPath}/${oldName}`, + `/api${expectedPath}`, expect.objectContaining({ method: 'DELETE', headers: { 'Content-Type': 'application/json' }, @@ -90,14 +75,10 @@ describe('Seite: Vorgangsansicht', () => { }) }) ); - // INVALIDATE-CHECK expect(nav.invalidateAll).toHaveBeenCalled(); - - // UI-UPDATE: Element entfernt - const updatedItems = screen.queryAllByTestId('test-list-item'); - expect(updatedItems).toHaveLength(testData.crimesList.length - 1); - expect(screen.queryByText(oldName)).toBeNull(); - + const updatedItems = screen.queryAllByTestId('test-list-item'); + expect(updatedItems).toHaveLength(testData.crimesList.length - 1); + expect(screen.queryByText(oldName)).toBeNull(); }); }); });