Merge branch 'development' into f090_magic_strings_refactoring

This commit is contained in:
2025-09-24 12:45:17 +02:00
15 changed files with 510 additions and 146 deletions

View File

@@ -25,7 +25,7 @@ describe('API-Endpoints: list', () => {
expect(response.status).toBe(401);
const json = await response.json();
const errorObj = { error: 'Unauthorized' }
const errorObj = { error: 'Unauthorized' };
expect(json).toEqual(errorObj);
});
@@ -61,4 +61,4 @@ describe('API-Endpoints: list', () => {
const json = await response.json();
expect(json).toEqual(testVorgaenge);
});
});
});

View File

@@ -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(); });
});

View File

@@ -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();
});
});

84
tests/TatortList.test.ts Normal file
View File

@@ -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();
});
});
});

View File

@@ -0,0 +1,87 @@
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';
describe('Seite: Vorgangsansicht', () => {
test.todo('zeigt PIN und Share-Link, wenn Admin');
test.todo('zeigt PIN und Share-Link disabeld, wenn Liste leer');
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 } });
expect(getByTestId('empty-list')).toBeInTheDocument();
});
it('zeigt keinen Listeneintrag', () => {
const items = screen.queryAllByTestId('test-list-item');
expect(items).toHaveLength(0);
});
});
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.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');
});
});

View File

@@ -0,0 +1,21 @@
import { render } from '@testing-library/svelte';
import { describe, expect, it } from 'vitest';
import VorgangListPage from '../src/routes/(angemeldet)/list/+page.svelte';
import { baseData } from './fixtures';
describe('Vorgänge Liste Page EmptyList-Komponente View', () => {
it('zeigt EmptyList-Komponente an, wenn Liste leer ist', () => {
const testData = { ...baseData, vorgangList: [] };
const { getByTestId } = render(VorgangListPage, { props: { data: testData } });
expect(getByTestId('empty-list')).toBeInTheDocument();
});
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.length).toBeGreaterThan(0);
});
});

46
tests/fixtures.ts Normal file
View File

@@ -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']
};