diff --git a/src/lib/components/NameItemEditor.svelte b/src/lib/components/NameItemEditor.svelte index 4a1bddf..71164c0 100644 --- a/src/lib/components/NameItemEditor.svelte +++ b/src/lib/components/NameItemEditor.svelte @@ -1,90 +1,83 @@ -
- - - +
+ {#if isEditing} + + + + {:else} + {localName} + + + {/if} {#if error} -

{error}

+

{error}

{/if}
diff --git a/src/lib/config.ts b/src/lib/config.ts index 0775514..2e2bf34 100644 --- a/src/lib/config.ts +++ b/src/lib/config.ts @@ -1,3 +1,3 @@ import { readFileSync } from 'fs'; -export default JSON.parse(readFileSync('./config.json').toString()); +export default JSON.parse(readFileSync('./config_prod.json').toString()); diff --git a/src/routes/(token-based)/list/[vorgang]/+page.svelte b/src/routes/(token-based)/list/[vorgang]/+page.svelte index 2893b11..ccbf121 100644 --- a/src/routes/(token-based)/list/[vorgang]/+page.svelte +++ b/src/routes/(token-based)/list/[vorgang]/+page.svelte @@ -26,8 +26,8 @@ // add other properties as needed } + 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.length === 0); @@ -37,12 +37,12 @@ let inProgress = $state(false); let isError = $state(false); - //Variable um nur admin UI anzuzeigen let admin = data?.user?.admin; async function handleSave(newName: string, oldName: string) { open = true; inProgress = true; + isError = false; try { const res = await fetch(`/api/list/${vorgangToken}/${oldName}`, { method: 'PUT', @@ -50,29 +50,18 @@ 'Content-Type': 'application/json' }, body: JSON.stringify({ vorgangToken, oldName, newName }) - }) - .then(() => { - inProgress = false; - }) - .catch((err) => { - inProgress = false; - isError = true; - console.log('ERROR', err); - }); + }); if (!res.ok) { - const msg = await res.text(); - console.error('❌ Fehler beim Umbenennen:', msg); - isError = true; - inProgress = false; - } else { - await invalidateAll(); - crimesList = data.crimesList; - open = false; - inProgress = false; + throw new Error('Fehler beim Speichern'); } + await invalidateAll(); + crimesList = data.crimesList; + open = false; } catch (err) { - console.error('⚠️ Netzwerkfehler:', err); + console.error('⚠️ Netzwerkfehler beim Speichern', err); + isError = true; + } finally { inProgress = false; } } @@ -80,39 +69,31 @@ async function handleDelete(tatort: string) { open = true; inProgress = true; - let url = new URL(data.url); - url.pathname += `/${tatort}`; - console.log('Delete tatort: ', `/api${url.pathname}`, url.pathname); + 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; - }) - .catch((err) => { - isError = true; - inProgress = false; - console.log('ERROR', err); - }); - if (!res.ok) { - const msg = await res.text(); - console.error('❌ Fehler beim Löschen:', msg); - } else { - console.log('🗑️ Erfolgreich gelöscht:', url.pathname); - await invalidateAll(); + }); - crimesList = data.crimesList; + 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); } } @@ -141,7 +122,7 @@ Mit freundlichen Grüßen, } -{#if data.vorgang && data.crimesList} +{#if data.vorgang && crimesList}

Vorgang {vorgangName}

@@ -158,10 +139,11 @@ Mit freundlichen Grüßen, {#if isEmptyList} {:else} - {#each data.crimesList as item, crimeListItemIndex} + {#each crimesList as item (item.name)}
  • {#if admin} {:else} - {item.name} + {item.name} +

    {/if} {#if item.size}

    diff --git a/tests/ComponentEmptyList.view.test.ts b/tests/ComponentEmptyList.view.test.ts new file mode 100644 index 0000000..08ac671 --- /dev/null +++ b/tests/ComponentEmptyList.view.test.ts @@ -0,0 +1,9 @@ +import { render, screen } from '@testing-library/svelte'; +import EmptyList from '$lib/components/EmptyList.svelte' +import { describe, expect, it } from 'vitest'; + +describe('Komponente: EmptyList', () => { + it('zeigt Hinweistext "Keine Einträge"', () => { + render(EmptyList); + expect(screen.getByText(/keine Einträge/i)).toBeInTheDocument(); }); +}); diff --git a/tests/ComponentNameItemEditor.test.ts b/tests/ComponentNameItemEditor.test.ts new file mode 100644 index 0000000..b32cfbb --- /dev/null +++ b/tests/ComponentNameItemEditor.test.ts @@ -0,0 +1,142 @@ +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/TatortList.test.ts b/tests/TatortList.test.ts new file mode 100644 index 0000000..459499e --- /dev/null +++ b/tests/TatortList.test.ts @@ -0,0 +1,84 @@ +import { render, fireEvent, screen, within } from '@testing-library/svelte'; +import { describe, it, expect, vi, test } from 'vitest'; +import * as nav from '$app/navigation'; +import TatortListPage from '../src/routes/(token-based)/list/[vorgang]/+page.svelte'; +import { baseData } from './fixtures'; +import { tick } from 'svelte'; + +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 - 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; + const newName = 'Fall-C'; + + render(TatortListPage, { props: { data } }); + const listItem = screen.getAllByTestId('test-list-item')[0]; + expect(listItem).toHaveTextContent(oldName); + + await fireEvent.click(within(listItem).getByTestId('edit-button')); + const input = within(listItem).getByTestId('test-input'); + await fireEvent.input(input, { target: { value: newName } }); + + await fireEvent.click(within(listItem).getByTestId('commit-button')); + await tick(); + + 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 + }) + }) + ); + + expect(nav.invalidateAll).toHaveBeenCalled(); + expect(within(listItem).getByText(newName)).toBeInTheDocument(); + }); + + it('führt DELETE-Request aus und entfernt Element aus UI', async () => { + const testData = structuredClone(baseData); + const oldName = testData.crimesList[0].name; + + 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]; + expect(listItem).toHaveTextContent(oldName); + const del = within(listItem).getByTestId('delete-button'); + expect(del).toBeInTheDocument() + await fireEvent.click(within(listItem).getByTestId('delete-button')); + await tick(); + + let expectedPath = new URL(testData.url).pathname; + expectedPath += `/${oldName}` + expect(global.fetch).toHaveBeenCalledWith( + `/api${expectedPath}`, + expect.objectContaining({ + method: 'DELETE', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + vorgangToken: testData.vorgang.vorgangToken, + tatort: oldName + }) + }) + ); + expect(nav.invalidateAll).toHaveBeenCalled(); + const updatedItems = screen.queryAllByTestId('test-list-item'); + expect(updatedItems).toHaveLength(testData.crimesList.length - 1); + expect(screen.queryByText(oldName)).toBeNull(); + }); + }); +}); diff --git a/tests/TatortList.view.test.ts b/tests/TatortList.view.test.ts index 5c0e9dd..05464c9 100644 --- a/tests/TatortList.view.test.ts +++ b/tests/TatortList.view.test.ts @@ -1,67 +1,87 @@ -import { render } from '@testing-library/svelte'; -import { describe, expect, it } from 'vitest'; +import { render, screen, within } from '@testing-library/svelte'; +import { describe, expect, it, test } from 'vitest'; import TatortListPage from '../src/routes/(token-based)/list/[vorgang]/+page.svelte'; +import { baseData } from './fixtures'; -const testUser = { - admin: true, - exp: 1757067123, - iat: 1757063523, - id: 'admin' -}; -const testCrimesList = [ - { - name: 'model-A', - lastModified: '2025-08-28T09:44:12.453Z', - etag: '558f35716f6af953f9bb5d75f6d77e6a', - size: 8947140, - prefix: '7596e4d5-c51f-482d-a4aa-ff76434305fc', - show_button: true - }, - { - name: 'model-z', - lastModified: '2025-08-28T10:37:20.142Z', - etag: '43e3989c32c4682bee407baaf83b6fa0', - size: 35788560, - prefix: '7596e4d5-c51f-482d-a4aa-ff76434305fc', - show_button: true - } -]; +describe('Seite: Vorgangsansicht', () => { + test.todo('zeigt PIN und Share-Link, wenn Admin'); + test.todo('zeigt PIN und Share-Link disabeld, wenn Liste leer'); -const testVorgangsList = [ - { - vorgangName: 'vorgang-1', - vorgangPIN: 'pin-123', - vorgangToken: 'c322f26f-8c5e-4cb9-94b3-b5433bf5109e' - }, - { - vorgangName: 'vorgang-2', - vorgangPIN: 'pin-2', - vorgangToken: 'cb0051bc-5f38-47b8-943c-9352d4d9c984' - } -]; + describe('Szenario: Liste leer (unabhängig von Rolle)', () => { + it('zeigt Hinweistext bei leerer Liste', () => { + const testData = { ...baseData, crimesList: [] }; + const { getByTestId } = render(TatortListPage, { props: { data: testData } }); -const baseData = { - user: testUser, - vorgang: testVorgangsList[0], - vorgangList: testVorgangsList, - crimesList: testCrimesList, - url: 'https://www.google.com', - crimeNames: [testCrimesList[0].name] -}; + expect(getByTestId('empty-list')).toBeInTheDocument(); + }); -describe('Tatort Liste Page EmptyList-Komponente View', () => { - it('zeigt EmptyList-Komponente an, wenn Liste leer ist', () => { - const testData = { ...baseData, crimesList: [] }; - const { getByTestId } = render(TatortListPage, { props: { data: testData } }); + it('zeigt keinen Listeneintrag', () => { + const items = screen.queryAllByTestId('test-list-item'); - expect(getByTestId('empty-list')).toBeInTheDocument(); + expect(items).toHaveLength(0); + }); }); - it('zeigt Liste(min. 1 li-Element) an, wenn Liste vorhanden ist', () => { - const testData = { ...baseData }; - const { getAllByTestId } = render(TatortListPage, { props: { data: testData } }); - const items = getAllByTestId('test-list-item'); + describe('Szenario: Liste gefüllt (unabhängig von Rolle)', () => { + it('rendert mindestens ein Listenelement bei vorhandenen crimesList-Daten', () => { + const testData = { ...baseData }; + const { queryAllByTestId } = render(TatortListPage, { props: { data: testData } }); + const items = queryAllByTestId('test-list-item'); - expect(items).toHaveLength(2); + expect(items.length).toBeGreaterThan(0); + }); + + it('zeigt für jeden Eintrag einen Link', () => { + const testData = { ...baseData }; + render(TatortListPage, { props: { data: testData } }); + const links = screen.queryAllByTestId('crime-link'); + + expect(links).toHaveLength(testData.crimesList.length); + }); + + it('prüft href und title jedes Links', () => { + const testData = { ...baseData }; + const { queryAllByTestId } = render(TatortListPage, { props: { data: testData } }); + const items = queryAllByTestId('test-list-item'); + + items.forEach((item, i) => { + const link = within(item).getByRole('link'); + const expectedHref = `/view/${testData.vorgang.vorgangToken}/${testData.crimesList[i].name}?pin=${testData.vorgang.vorgangPIN}`; + + expect(link).toBeInTheDocument(); + expect(link).toHaveAttribute('href', expectedHref); + expect(link).toHaveAttribute('title', testData.crimesList[i].name); + }); + }); + + test.todo('testet zuletzt angezeigt, wenn item.lastModified'); + test.todo('zeigt Dateigröße, wenn item.size vorhanden ist'); + }); + + describe('Szenario: Admin + Liste gefüllt', () => { + const testData = { ...baseData, user: { ...baseData.user, admin: true } }; + it('zeigt Listeneinträge mit Komponente NameItemEditor', () => { + const { getAllByTestId } = render(TatortListPage, { props: { data: testData } }); + const items = getAllByTestId('test-nameItemEditor'); + + expect(items.length).toBeGreaterThan(0); + }); + + test.todo('Modal testen, wenn open'); + }); + + describe('Szenario: Viewer + Liste gefüllt', () => { + const testData = { ...baseData, user: { ...baseData.user, admin: false } }; + it('zeigt Listeneinträge mit p', () => { + render(TatortListPage, { props: { data: testData } }); + const paragraphs = screen.queryAllByTestId('test-nameItem-p'); + + expect(paragraphs).toHaveLength(testData.crimesList.length); + paragraphs.forEach((p, i) => { + expect(p).toHaveTextContent(testData.crimesList[i].name); + }); + }); + + test.todo('zeigt keinen Share-Link oder PIN'); }); }); diff --git a/tests/VorgangList.view.test.ts b/tests/VorgangList.view.test.ts index f1187dc..41a8be1 100644 --- a/tests/VorgangList.view.test.ts +++ b/tests/VorgangList.view.test.ts @@ -1,52 +1,7 @@ import { render } from '@testing-library/svelte'; import { describe, expect, it } from 'vitest'; import VorgangListPage from '../src/routes/(angemeldet)/list/+page.svelte'; -const testUser = { - admin: true, - exp: 1757067123, - iat: 1757063523, - id: 'admin' -}; -const testCrimesList = [ - { - name: 'model-A', - lastModified: '2025-08-28T09:44:12.453Z', - etag: '558f35716f6af953f9bb5d75f6d77e6a', - size: 8947140, - prefix: '7596e4d5-c51f-482d-a4aa-ff76434305fc', - show_button: true - }, - { - name: 'model-z', - lastModified: '2025-08-28T10:37:20.142Z', - etag: '43e3989c32c4682bee407baaf83b6fa0', - size: 35788560, - prefix: '7596e4d5-c51f-482d-a4aa-ff76434305fc', - show_button: true - } -]; - -const testVorgangsList = [ - { - vorgangName: 'vorgang-1', - vorgangPIN: 'pin-123', - vorgangToken: 'c322f26f-8c5e-4cb9-94b3-b5433bf5109e' - }, - { - vorgangName: 'vorgang-2', - vorgangPIN: 'pin-2', - vorgangToken: 'cb0051bc-5f38-47b8-943c-9352d4d9c984' - } -]; - -const baseData = { - user: testUser, - vorgang: testVorgangsList[0], - vorgangList: testVorgangsList, - crimesList: testCrimesList, - url: URL, - crimeNames: ['modell-A'] -}; +import { baseData } from './fixtures'; describe('Vorgänge Liste Page EmptyList-Komponente View', () => { it('zeigt EmptyList-Komponente an, wenn Liste leer ist', () => { @@ -56,11 +11,11 @@ describe('Vorgänge Liste Page EmptyList-Komponente View', () => { expect(getByTestId('empty-list')).toBeInTheDocument(); }); - it('zeigt Liste(min. 1 li-Element) an, wenn Liste vorhanden ist', () => { + it('zeigt Liste(mockData 2 Elemente) an, wenn Liste vorhanden ist', () => { const testData = { ...baseData }; const { getAllByTestId } = render(VorgangListPage, { props: { data: testData } }); const items = getAllByTestId('test-list-item'); - expect(items).toHaveLength(2); + expect(items.length).toBeGreaterThan(0); }); }); diff --git a/tests/fixtures.ts b/tests/fixtures.ts new file mode 100644 index 0000000..270a845 --- /dev/null +++ b/tests/fixtures.ts @@ -0,0 +1,46 @@ +const testUser = { + admin: true, + exp: 1757067123, + iat: 1757063523, + id: 'admin' +}; +const testCrimesList = [ + { + name: 'Fall-A', + lastModified: '2025-08-28T09:44:12.453Z', + etag: '558f35716f6af953f9bb5d75f6d77e6a', + size: 8947140, + prefix: '7596e4d5-c51f-482d-a4aa-ff76434305fc', + show_button: true + }, + { + name: 'Fall-B', + lastModified: '2025-08-28T10:37:20.142Z', + etag: '43e3989c32c4682bee407baaf83b6fa0', + size: 35788560, + prefix: '7596e4d5-c51f-482d-a4aa-ff76434305fc', + show_button: true + } +]; + +const testVorgangsList = [ + { + vorgangName: 'vorgang-1', + vorgangPIN: 'pin-123', + vorgangToken: 'c322f26f-8c5e-4cb9-94b3-b5433bf5109e' + }, + { + vorgangName: 'vorgang-2', + vorgangPIN: 'pin-2', + vorgangToken: 'cb0051bc-5f38-47b8-943c-9352d4d9c984' + } +]; + +export const baseData = { + user: testUser, + vorgang: testVorgangsList[0], + vorgangList: testVorgangsList, + crimesList: testCrimesList, + url: `https://example.com/${testVorgangsList[0].vorgangToken}`, + crimeNames: ['modell-A', 'Fall-A'] +};