diff --git a/src/lib/components/NameItemEditor.svelte b/src/lib/components/NameItemEditor.svelte index ddfab02..58c4297 100644 --- a/src/lib/components/NameItemEditor.svelte +++ b/src/lib/components/NameItemEditor.svelte @@ -8,35 +8,33 @@ interface ListItem { name: string; token?: string; - // add other properties as needed } - let { list, currentName, onSave = () => {}, onDelete = () => {} } = $props(); - let localName = $state(currentName); - let isEditing = $state(false); + export let list: ListItem[] = []; + export let currentName: string = ''; + export let onSave: (n: string, o: string) => unknown = () => {}; + export let onDelete: (n: string) => unknown = () => {}; - let error = $derived(() => validateName(localName)); + // lokaler State + let localName = currentName; + let isEditing = false; + let inputRef: HTMLInputElement | null = null; - let inputRef = $state(null); - - function validateName(name: string | undefined | null) { - if (!name) return 'Name darf nicht leer sein.'; - const trimmed = name.trim(); + $: error = validateName(localName); + function validateName(name: string): string { + const trimmed = name?.trim() ?? ''; if (!trimmed) return 'Name darf nicht leer sein.'; - - const duplicate = list.some( - (item: ListItem) => item.name === trimmed && item.name !== currentName - ); - - if (duplicate) return 'Name existiert bereits.'; - + if (list.some((item) => item.name === trimmed && item.name !== currentName)) { + return 'Name existiert bereits.'; + } return ''; } - function startEdit() { + async function startEdit() { isEditing = true; - tick().then(() => inputRef?.focus()); + await tick(); + inputRef?.focus(); } function cancelEdit() { @@ -45,7 +43,7 @@ } function commitEdit() { - if (!error() && localName != currentName) onSave(localName, currentName); + if (!error && localName != currentName) onSave(localName, currentName); isEditing = false; } @@ -54,6 +52,10 @@ if (event.key === 'Enter') commitEdit(); if (event.key === 'Escape') cancelEdit(); } + + function handleDeleteClick() { + onDelete(currentName); + }
@@ -64,14 +66,18 @@ bind:value={localName} onkeydown={handleKeydown} /> - + {:else} {localName} - + {/if} - {#if error()} -

{error()}

+ {#if error} +

{error}

{/if}
diff --git a/src/routes/(token-based)/list/[vorgang]/+page.svelte b/src/routes/(token-based)/list/[vorgang]/+page.svelte index c84fefb..e22bc2c 100644 --- a/src/routes/(token-based)/list/[vorgang]/+page.svelte +++ b/src/routes/(token-based)/list/[vorgang]/+page.svelte @@ -26,11 +26,12 @@ // add other properties as needed } + // 2) Lokaler, reaktiver State mit $state + let crimesList = $state(data.crimesList); let vorgangName: string = data.vorgang.vorgangName; - let crimesList: ListItem[] = $state(data.crimesList); const vorgangPIN: string = data.vorgang.vorgangPIN; let vorgangToken: string = data.vorgang.vorgangToken; - let isEmptyList = $derived(crimesList && crimesList.length === 0); + let isEmptyList = $derived(crimesList.length === 0); //Variablen für Modal let open = $state(false); @@ -38,11 +39,12 @@ let isError = $state(false); //Variable um nur admin UI anzuzeigen - let admin = data?.user?.admin; + let admin = $state(data?.user?.admin); async function handleSave(newName: string, oldName: string) { open = true; inProgress = true; + isError = false; console.log('debug handleSave', newName, oldName); try { @@ -54,20 +56,15 @@ body: JSON.stringify({ vorgangToken, oldName, newName }) }); - if (res.ok) { - inProgress = false; - invalidateAll(); - data.crimesList = newName; - open = false; - } else { - inProgress = false; - isError = true; + if (!res.ok) { throw new Error('Fehler beim Speichern'); } + await invalidateAll(); + open = false; } catch (err) { + console.error('⚠️ Netzwerkfehler beim Speichern', err); isError = true; - inProgress = false; - console.error('⚠️ Netzwerkfehler:', err); + } finally { inProgress = false; } } @@ -75,32 +72,31 @@ async function handleDelete(tatort: string) { open = true; inProgress = true; - let url = new URL(data.url); - url.pathname += `/${tatort}`; + isError = false; + let path = new URL(data.url).pathname; + path += `/${tatort}`; try { - const res = await fetch(`/api${url.pathname}`, { + const res = await fetch(`/api${path}`, { method: 'DELETE', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ vorgangToken, tatort }) - }) - .then(() => { - inProgress = false; - console.log('🗑️ Erfolgreich gelöscht:', url.pathname); - invalidateAll(); - crimesList = data.crimesList; - }) - .catch((err) => { - isError = true; - inProgress = false; - console.error('ERROR', err); - }); + }); + + if (!res.ok) { + throw new Error('Fehler beim Löschen'); + } + crimesList = crimesList.filter((i) => i.name !== tatort); + await invalidateAll(); + console.log('🗑️ Erfolgreich gelöscht:', path); + open = false; } catch (err) { + console.error('⚠️ Netzwerkfehler beim Speichern', err); isError = true; + } finally { inProgress = false; - console.error('⚠️ Netzwerkfehler beim Löschen:', err); } } @@ -129,7 +125,7 @@ Mit freundlichen Grüßen, } -{#if data.vorgang && data.crimesList} +{#if data.vorgang && crimesList}

Vorgang {vorgangName}

@@ -146,7 +142,7 @@ Mit freundlichen Grüßen, {#if isEmptyList} {:else} - {#each data.crimesList as item} + {#each crimesList as item (item.name)}
  • {#if admin} ({ - invalidateAll: vi.fn() -})); +vi.spyOn(nav, 'invalidateAll').mockResolvedValue(); +global.fetch = vi.fn().mockResolvedValue({ ok: true }); describe('Seite: Vorgangsansicht', () => { test.todo('Share Link disabled wenn Liste leer'); - describe('Szenario: Admin + Liste gefüllt', () => { + describe('Szenario: Admin + Liste gefüllt - Funktionalität', () => { test.todo('Share Link Link generierung richtig'); - it('ändert den Namen nach Speichern', async () => { - const testData = structuredClone(baseData); - const oldName = testData.crimesList[0].name; - const newName = 'Fall-B'; - const list = testData.crimesList - const vorgangToken = testData.vorgang.vorgangToken - // Minimaler fetch-Mock -global.fetch = vi.fn().mockResolvedValue({ ok: true }) as typeof fetch; - const { getAllByTestId } = render(TatortListPage, { props: { data: testData } }); + it('führt PUT-Request aus und aktualisiert UI nach onSave', async () => { + const data = structuredClone(baseData); + const oldName = data.crimesList[0].name; + const newName = 'Fall-C'; - const firstItem = getAllByTestId('test-list-item')[0]; - const editButton = within(firstItem).getByTestId('edit-button'); - await fireEvent.click(editButton); + render(TatortListPage, { props: { data } }); + const listItem = screen.getAllByTestId('test-list-item')[0]; + // teste ob alter Name angezeigt: + expect(listItem).toHaveTextContent(oldName); - const input = within(firstItem).getByTestId('test-input'); - expect(input).toHaveValue(oldName) - await fireEvent.input(input, { target: { value: newName } }); + // Editmodus + await fireEvent.click(within(listItem).getByTestId('edit-button')); + const input = within(listItem).getByTestId('test-input'); + await fireEvent.input(input, { target: { value: newName } }); - const commitButton = within(firstItem).getByTestId('commit-button'); - await fireEvent.click(commitButton); + // Commit + await fireEvent.click(within(listItem).getByTestId('commit-button')); + await Promise.resolve(); // wartet reaktive Updates ab - // const fetchMock = global.fetch as ReturnType; - // console.log('Fetch calls:', fetchMock.mock.calls); + // FETCH-CHECK + expect(global.fetch).toHaveBeenCalledWith( + `/api/list/${data.vorgang.vorgangToken}/${oldName}`, + expect.objectContaining({ + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + vorgangToken: data.vorgang.vorgangToken, + oldName, + newName + }) + }) + ); - // // Erwartung: fetch wurde aufgerufen - // expect(global.fetch).toHaveBeenCalledWith( - // expect.stringContaining(`/api/list/${vorgangToken}/${oldName}`), - // expect.objectContaining({ - // method: 'PUT', - // headers: { 'Content-Type': 'application/json' }, - // body: JSON.stringify({vorgangToken, oldName, newName}) - // }) - // ); + // INVALIDATE-CHECK + expect(nav.invalidateAll).toHaveBeenCalled(); - // expect(invalidateAll).toHaveBeenCalled(); - // Erwartung: neuer Name ist sofort im DOM sichtbar - // expect(within(firstItem).getByRole('textbox')).toHaveValue(newName); - // const editedLink = within(firstItem).getByRole('link'); - // const editedExpectedHref = `/view/${vorgangToken}/${newName}?pin=${testData.vorgang.vorgangPIN}`; - - // expect(editedLink).toBeInTheDocument(); - // expect(editedLink).toHaveAttribute('href', editedExpectedHref); - // expect(editedLink).toHaveAttribute('title', newName); + // UI-UPDATE + expect(within(listItem).getByText(newName)).toBeInTheDocument(); }); - // it('entfernt das Listenelement nach Löschen', async () => { - // const testData = structuredClone(baseData); - // testData.url = new URL('https://example.com/vorgang-1'); // Fix für Invalid URL - // const toDelete = testData.crimesList[0]; + it('führt DELETE-Request aus und entfernt Element aus UI', async () => { + const testData = structuredClone(baseData); + const oldName = testData.crimesList[0].name; - // global.fetch = vi.fn().mockResolvedValue({ ok: true }); + // Rendern und initiale Liste prüfen + render(TatortListPage, { props: { data: testData } }); + const initialItems = screen.getAllByTestId('test-list-item'); + expect(initialItems).toHaveLength(testData.crimesList.length); - // render(TatortListPage, { props: { data: testData } }); - // const deletedFirstItem = screen.getAllByTestId('test-list-item')[0]; - // const deletedLink = within(deletedFirstItem).getByRole('link'); - // const deletedExpectedHref = `/view/${testData.vorgang.vorgangToken}/${toDelete.name}?pin=${testData.vorgang.vorgangPIN}`; + 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(); - // expect(deletedLink).toBeInTheDocument(); - // expect(deletedLink).toHaveAttribute('href', deletedExpectedHref); - // expect(deletedLink).toHaveAttribute('title', toDelete.name); - // await fireEvent.click(within(deletedFirstItem).getByTestId('delete-button')); + // FETCH-CHECK: URL & Payload + // entspricht: new URL(data.url).pathname + '/' + oldName + const expectedPath = new URL(testData.url).pathname; + expect(global.fetch).toHaveBeenCalledWith( + `/api${expectedPath}/${oldName}`, + expect.objectContaining({ + method: 'DELETE', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + vorgangToken: testData.vorgang.vorgangToken, + tatort: oldName + }) + }) + ); + // INVALIDATE-CHECK + expect(nav.invalidateAll).toHaveBeenCalled(); - // // Erwartung: fetch wurde aufgerufen - // expect(global.fetch).toHaveBeenCalledWith( - // expect.stringContaining(`/api/vorgang-1/${toDelete.name}`), - // expect.any(Object) - // ); + // UI-UPDATE: Element entfernt + const updatedItems = screen.queryAllByTestId('test-list-item'); + expect(updatedItems).toHaveLength(testData.crimesList.length - 1); + expect(screen.queryByText(oldName)).toBeNull(); - // // Erwartung: Element ist nicht mehr im DOM - // expect(within(deletedFirstItem).getByRole('textbox')).toHaveValue(toDelete.name); - // }); + }); }); }); - - diff --git a/tests/fixtures.ts b/tests/fixtures.ts index cb40c9f..4126fb5 100644 --- a/tests/fixtures.ts +++ b/tests/fixtures.ts @@ -6,7 +6,7 @@ id: "admin", } const testCrimesList = [ { - name: 'modell-A', + name: 'Fall-A', lastModified: '2025-08-28T09:44:12.453Z', etag: '558f35716f6af953f9bb5d75f6d77e6a', size: 8947140, @@ -14,7 +14,7 @@ const testCrimesList = [ show_button: true }, { - name: 'Fall-A', + name: 'Fall-B', lastModified: '2025-08-28T10:37:20.142Z', etag: '43e3989c32c4682bee407baaf83b6fa0', size: 35788560, @@ -42,6 +42,6 @@ export const baseData = { vorgang: testVorgangsList[0], vorgangList: testVorgangsList, crimesList: testCrimesList, - url: new URL(`https://example.com/${testVorgangsList[0].vorgangToken}`), + url: `https://example.com/${testVorgangsList[0].vorgangToken}`, crimeNames: [ "modell-A", "Fall-A" ], }