save changes, bin aber noch nicht fertig

This commit is contained in:
2025-09-10 17:50:10 +02:00
parent 8803187ce1
commit 47ca05f2d4
4 changed files with 125 additions and 134 deletions

View File

@@ -10,27 +10,20 @@
token?: string; token?: string;
// add other properties as needed // add other properties as needed
} }
let { let { list, currentName, onSave = () => {}, onDelete = () => {} } = $props();
list,
editedName = $bindable(),
currentName,
onSave = () => {},
onDelete = () => {}
} = $props();
let localName = $state(currentName); let localName = $state(currentName);
let wasCancelled = $state(false);
let error: string = $derived(validateName(localName));
let isEditing = $state(false); let isEditing = $state(false);
let inputRef: HTMLInputElement;
function validateName(name: string) { let error = $derived(() => validateName(localName));
let inputRef = $state<HTMLInputElement | null>(null);
function validateName(name: string | undefined | null) {
if (!name) return 'Name darf nicht leer sein.';
const trimmed = name.trim(); const trimmed = name.trim();
if (!trimmed) { if (!trimmed) return 'Name darf nicht leer sein.';
return 'Name darf nicht leer sein.';
}
const duplicate = list.some( const duplicate = list.some(
(item: ListItem) => item.name === trimmed && item.name !== currentName (item: ListItem) => item.name === trimmed && item.name !== currentName
@@ -41,67 +34,44 @@
return ''; return '';
} }
function commitIfValid() { function startEdit() {
if (!error && !wasCancelled && localName != currentName) { isEditing = true;
const trimmedName: string = localName.trim(); tick().then(() => inputRef?.focus());
inputRef?.blur();
isEditing = false;
onSave(trimmedName, currentName);
} else {
cancelEdit();
}
} }
function resetEdit() { function cancelEdit() {
inputRef?.blur(); localName = currentName;
isEditing = false;
}
function commitEdit() {
if (!error() && localName != currentName) onSave(localName, currentName);
isEditing = false; isEditing = false;
} }
function handleKeydown(event: KeyboardEvent) { function handleKeydown(event: KeyboardEvent) {
if (event.key === 'Enter') { if (event.key === 'Enter') commitEdit();
event.preventDefault(); if (event.key === 'Escape') cancelEdit();
commitIfValid();
} else if (event.key === 'Escape') {
event.preventDefault();
localName = currentName;
resetEdit();
}
}
async function startEdit() {
isEditing = true;
await tick();
inputRef?.focus();
}
function cancelEdit() {
resetEdit();
wasCancelled = true;
localName = currentName;
} }
</script> </script>
<div data-testid="test-nameItemEditor"> <div data-testid="test-nameItemEditor">
<input
data-testid="test-input"
bind:this={inputRef}
bind:value={localName}
onfocus={() => {
isEditing = true;
}}
onblur={commitIfValid}
onkeydown={handleKeydown}
/>
{#if isEditing} {#if isEditing}
<button data-testid="commit-button" disabled={wasCancelled} onclick={commitIfValid} <input
><Check /></button data-testid="test-input"
> bind:this={inputRef}
bind:value={localName}
onkeydown={handleKeydown}
/>
<button data-testid="commit-button" onclick={commitEdit}><Check /></button>
<button data-testid="cancel-button" onclick={cancelEdit}><X /></button> <button data-testid="cancel-button" onclick={cancelEdit}><X /></button>
{:else} {:else}
<span>{localName}</span>
<button data-testid="edit-button" onclick={startEdit}><Edit /></button> <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={() => onDelete(currentName)}><Trash /></button>
{/if} {/if}
{#if error} {#if error()}
<p class="text-red-500">{error}</p> <p class="text-red-500">{error()}</p>
{/if} {/if}
</div> </div>

View File

@@ -25,7 +25,6 @@
prefix?: string; prefix?: string;
// add other properties as needed // add other properties as needed
} }
console.log(data.url);
let vorgangName: string = data.vorgang.vorgangName; let vorgangName: string = data.vorgang.vorgangName;
let crimesList: ListItem[] = $state(data.crimesList); let crimesList: ListItem[] = $state(data.crimesList);
@@ -44,6 +43,8 @@
async function handleSave(newName: string, oldName: string) { async function handleSave(newName: string, oldName: string) {
open = true; open = true;
inProgress = true; inProgress = true;
console.log('debug handleSave', newName, oldName);
try { try {
const res = await fetch(`/api/list/${vorgangToken}/${oldName}`, { const res = await fetch(`/api/list/${vorgangToken}/${oldName}`, {
method: 'PUT', method: 'PUT',
@@ -51,18 +52,18 @@
'Content-Type': 'application/json' 'Content-Type': 'application/json'
}, },
body: JSON.stringify({ vorgangToken, oldName, newName }) body: JSON.stringify({ vorgangToken, oldName, newName })
}) });
.then(() => {
inProgress = false; if (res.ok) {
invalidateAll(); inProgress = false;
crimesList = data.crimesList; invalidateAll();
open = false; data.crimesList = newName;
}) open = false;
.catch((err) => { } else {
inProgress = false; inProgress = false;
isError = true; isError = true;
console.log('ERROR', err); throw new Error('Fehler beim Speichern');
}); }
} catch (err) { } catch (err) {
isError = true; isError = true;
inProgress = false; inProgress = false;
@@ -94,7 +95,7 @@
.catch((err) => { .catch((err) => {
isError = true; isError = true;
inProgress = false; inProgress = false;
console.log('ERROR', err); console.error('ERROR', err);
}); });
} catch (err) { } catch (err) {
isError = true; isError = true;
@@ -145,7 +146,7 @@ Mit freundlichen Grüßen,
{#if isEmptyList} {#if isEmptyList}
<EmptyList></EmptyList> <EmptyList></EmptyList>
{:else} {:else}
{#each data.crimesList as item, crimeListItemIndex} {#each data.crimesList as item}
<li data-testid="test-list-item"> <li data-testid="test-list-item">
<div class=" flex gap-x-4"> <div class=" flex gap-x-4">
<a <a
@@ -161,7 +162,6 @@ Mit freundlichen Grüßen,
{#if admin} {#if admin}
<NameItemEditor <NameItemEditor
list={data.crimesList} list={data.crimesList}
editedName={data.crimeNames[crimeListItemIndex]}
currentName={item.name} currentName={item.name}
onSave={handleSave} onSave={handleSave}
onDelete={handleDelete} onDelete={handleDelete}

View File

@@ -1,7 +1,8 @@
import { fireEvent, getAllByTestId, queryAllByTestId, render, screen, within } from '@testing-library/svelte'; import { fireEvent, getByTestId, queryAllByTestId, render, screen, within } from '@testing-library/svelte';
import { describe, expect, it, vi } from "vitest"; import { describe, expect, it, test, vi } from "vitest";
import TatortListPage from "../src/routes/(token-based)/list/[vorgang]/+page.svelte"; import TatortListPage from "../src/routes/(token-based)/list/[vorgang]/+page.svelte";
import { baseData } from './fixtures'; import { baseData } from './fixtures';
import { invalidateAll } from '$app/navigation';
// Mock für invalidateAll // Mock für invalidateAll
@@ -9,69 +10,85 @@ vi.mock('$app/navigation', () => ({
invalidateAll: vi.fn() invalidateAll: vi.fn()
})); }));
// Minimaler fetch-Mock
global.fetch = vi.fn().mockResolvedValue({ ok: true });
describe('Seite: Vorgangsansicht', () => { describe('Seite: Vorgangsansicht', () => {
describe('Szenario: Admin + Liste gefüllt', () => { test.todo('Share Link disabled wenn Liste leer');
it('ändert den Namen nach Speichern', async () => { describe('Szenario: Admin + Liste gefüllt', () => {
const testData = structuredClone(baseData); test.todo('Share Link Link generierung richtig');
const oldName = testData.crimesList[0].name;
render(TatortListPage, { props: { data: testData } }); 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 firstItem = screen.getAllByTestId('test-list-item')[0]; const { getAllByTestId } = render(TatortListPage, { props: { data: testData } });
await fireEvent.click(within(firstItem).getByTestId('edit-button'));
const input = within(firstItem).getByRole('textbox'); const firstItem = getAllByTestId('test-list-item')[0];
await fireEvent.input(input, { target: { value: 'Fall-B' } }); const editButton = within(firstItem).getByTestId('edit-button');
await fireEvent.click(editButton);
await fireEvent.click(within(firstItem).getByTestId('commit-button')); const input = within(firstItem).getByTestId('test-input');
expect(input).toHaveValue(oldName)
await fireEvent.input(input, { target: { value: newName } });
// Erwartung: fetch wurde aufgerufen const commitButton = within(firstItem).getByTestId('commit-button');
expect(global.fetch).toHaveBeenCalledWith( await fireEvent.click(commitButton);
expect.stringContaining(`/api/list/${testData.vorgang.vorgangToken}/${oldName}`),
expect.any(Object)
);
// Erwartung: neuer Name ist sofort im DOM sichtbar // const fetchMock = global.fetch as ReturnType<typeof vi.fn>;
expect(within(firstItem).getByRole('textbox')).toHaveValue('Fall-B'); // console.log('Fetch calls:', fetchMock.mock.calls);
}); // // 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})
// })
// );
it('entfernt das Listenelement nach Löschen', async () => { // expect(invalidateAll).toHaveBeenCalled();
const testData = structuredClone(baseData); // Erwartung: neuer Name ist sofort im DOM sichtbar
testData.url = new URL('https://example.com/vorgang-1'); // Fix für Invalid URL // expect(within(firstItem).getByRole('textbox')).toHaveValue(newName);
const toDelete = testData.crimesList[0]; // const editedLink = within(firstItem).getByRole('link');
// const editedExpectedHref = `/view/${vorgangToken}/${newName}?pin=${testData.vorgang.vorgangPIN}`;
global.fetch = vi.fn().mockResolvedValue({ ok: true }); // expect(editedLink).toBeInTheDocument();
// expect(editedLink).toHaveAttribute('href', editedExpectedHref);
// expect(editedLink).toHaveAttribute('title', newName);
});
render(TatortListPage, { props: { data: testData } }); // it('entfernt das Listenelement nach Löschen', async () => {
const deletedFirstItem = screen.getAllByTestId('test-list-item')[0]; // const testData = structuredClone(baseData);
// testData.url = new URL('https://example.com/vorgang-1'); // Fix für Invalid URL
// const toDelete = testData.crimesList[0];
// global.fetch = vi.fn().mockResolvedValue({ ok: true });
const deletedLink = within(deletedFirstItem).getByRole('link'); // 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 deletedExpectedHref = `/view/${testData.vorgang.vorgangToken}/${toDelete.name}?pin=${testData.vorgang.vorgangPIN}`; // expect(deletedLink).toBeInTheDocument();
// expect(deletedLink).toHaveAttribute('href', deletedExpectedHref);
// expect(deletedLink).toHaveAttribute('title', toDelete.name);
// await fireEvent.click(within(deletedFirstItem).getByTestId('delete-button'));
expect(deletedLink).toBeInTheDocument(); // // Erwartung: fetch wurde aufgerufen
// expect(global.fetch).toHaveBeenCalledWith(
// expect.stringContaining(`/api/vorgang-1/${toDelete.name}`),
// expect.any(Object)
// );
expect(deletedLink).toHaveAttribute('href', deletedExpectedHref); // // Erwartung: Element ist nicht mehr im DOM
expect(deletedLink).toHaveAttribute('title', toDelete.name); // expect(within(deletedFirstItem).getByRole('textbox')).toHaveValue(toDelete.name);
// });
});
await fireEvent.click(within(deletedFirstItem).getByTestId('delete-button'));
// Erwartung: fetch wurde aufgerufen
expect(global.fetch).toHaveBeenCalledWith(
expect.stringContaining(`/api/vorgang-1/${toDelete.name}`),
expect.any(Object)
);
// Erwartung: Element ist nicht mehr im DOM
expect(within(deletedFirstItem).getByRole('textbox')).toHaveValue(toDelete.name);
});
});
}); });

View File

@@ -9,19 +9,21 @@ describe('Seite: Vorgangsansicht', () => {
describe('Szenario: Liste leer (unabhängig von Rolle)', () => { describe('Szenario: Liste leer (unabhängig von Rolle)', () => {
it('zeigt Hinweistext bei leerer Liste', () => { it('zeigt Hinweistext bei leerer Liste', () => {
const testData = { ...baseData, crimesList: [] }; const testData = { ...baseData, crimesList: [] };
const { getByTestId } = render(TatortListPage, {props:{data: testData}}); const { getByTestId } = render(TatortListPage, {props:{data: testData}});
expect(getByTestId('empty-list')).toBeInTheDocument(); expect(getByTestId('empty-list')).toBeInTheDocument();
}); });
it('zeigt keinen Listeneintrag', () => {
const items = screen.queryAllByTestId('test-list-item');
expect(items).toHaveLength(0); it('zeigt keinen Listeneintrag', () => {
const items = screen.queryAllByTestId('test-list-item');
expect(items).toHaveLength(0);
}); });
}); });
describe('Szenario: Liste gefüllt (unabhängig von Rolle)', () => { describe('Szenario: Liste gefüllt (unabhängig von Rolle)', () => {
it('rendert mindestens ein Listenelement bei vorhandenen crimesList-Daten und prüft ob Link vorhanden', () => { it('rendert mindestens ein Listenelement bei vorhandenen crimesList-Daten', () => {
const testData = { ...baseData }; const testData = { ...baseData };
const { queryAllByTestId } = render(TatortListPage, {props:{data: testData}}); const { queryAllByTestId } = render(TatortListPage, {props:{data: testData}});
const items = queryAllByTestId('test-list-item'); const items = queryAllByTestId('test-list-item');
@@ -65,7 +67,9 @@ describe('Seite: Vorgangsansicht', () => {
expect(items.length).toBeGreaterThan(0); expect(items.length).toBeGreaterThan(0);
}); });
});
test.todo('Modal testen, wenn open')
});
describe('Szenario: Viewer + Liste gefüllt', () => { describe('Szenario: Viewer + Liste gefüllt', () => {
const testData = { ...baseData, user: { ...baseData.user, admin: false }}; const testData = { ...baseData, user: { ...baseData.user, admin: false }};
@@ -80,6 +84,6 @@ describe('Seite: Vorgangsansicht', () => {
}); });
test.todo('zeigt keinen Share-Link oder PIN') test.todo('zeigt keinen Share-Link oder PIN')
test.todo('Modal testen, wenn open')
}); });
}); });