fixed tests and Code edit and delete Name in TatortList
This commit is contained in:
@@ -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<HTMLInputElement | null>(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);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div data-testid="test-nameItemEditor">
|
||||
@@ -64,14 +66,18 @@
|
||||
bind:value={localName}
|
||||
onkeydown={handleKeydown}
|
||||
/>
|
||||
<button data-testid="commit-button" onclick={commitEdit}><Check /></button>
|
||||
<button
|
||||
data-testid="commit-button"
|
||||
disabled={!!error || localName === currentName}
|
||||
onclick={commitEdit}><Check /></button
|
||||
>
|
||||
<button data-testid="cancel-button" onclick={cancelEdit}><X /></button>
|
||||
{:else}
|
||||
<span>{localName}</span>
|
||||
<button data-testid="edit-button" onclick={startEdit}><Edit /></button>
|
||||
<button data-testid="delete-button" onclick={() => onDelete(currentName)}><Trash /></button>
|
||||
<button data-testid="delete-button" onclick={handleDeleteClick}><Trash /></button>
|
||||
{/if}
|
||||
{#if error()}
|
||||
<p class="text-red-500">{error()}</p>
|
||||
{#if error}
|
||||
<p class="text-red-500">{error}</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -26,11 +26,12 @@
|
||||
// add other properties as needed
|
||||
}
|
||||
|
||||
// 2) Lokaler, reaktiver State mit $state
|
||||
let crimesList = $state<ListItem[]>(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,
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if data.vorgang && data.crimesList}
|
||||
{#if data.vorgang && crimesList}
|
||||
<div class="-z-10 bg-white">
|
||||
<div class="flex flex-col items-center justify-center w-full">
|
||||
<h1 class="text-xl">Vorgang {vorgangName}</h1>
|
||||
@@ -146,7 +142,7 @@ Mit freundlichen Grüßen,
|
||||
{#if isEmptyList}
|
||||
<EmptyList></EmptyList>
|
||||
{:else}
|
||||
{#each data.crimesList as item}
|
||||
{#each crimesList as item (item.name)}
|
||||
<li data-testid="test-list-item">
|
||||
<div class=" flex gap-x-4">
|
||||
<a
|
||||
@@ -161,7 +157,7 @@ Mit freundlichen Grüßen,
|
||||
<div class="min-w-0 flex-auto">
|
||||
{#if admin}
|
||||
<NameItemEditor
|
||||
list={data.crimesList}
|
||||
list={crimesList}
|
||||
currentName={item.name}
|
||||
onSave={handleSave}
|
||||
onDelete={handleDelete}
|
||||
|
||||
@@ -1,94 +1,103 @@
|
||||
import { fireEvent, getByTestId, queryAllByTestId, render, screen, within } from '@testing-library/svelte';
|
||||
import { describe, expect, it, test, vi } from "vitest";
|
||||
import TatortListPage from "../src/routes/(token-based)/list/[vorgang]/+page.svelte";
|
||||
// @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';
|
||||
import TatortListPage from '../src/routes/(token-based)/list/[vorgang]/+page.svelte';
|
||||
import { baseData } from './fixtures';
|
||||
import { invalidateAll } from '$app/navigation';
|
||||
|
||||
import { tick } from 'svelte';
|
||||
|
||||
// Mock für invalidateAll
|
||||
vi.mock('$app/navigation', () => ({
|
||||
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)
|
||||
// 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<typeof vi.fn>;
|
||||
// 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);
|
||||
// });
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
|
||||
@@ -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" ],
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user