save changes, bin aber noch nicht fertig
This commit is contained in:
@@ -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">
|
||||||
|
{#if isEditing}
|
||||||
<input
|
<input
|
||||||
data-testid="test-input"
|
data-testid="test-input"
|
||||||
bind:this={inputRef}
|
bind:this={inputRef}
|
||||||
bind:value={localName}
|
bind:value={localName}
|
||||||
onfocus={() => {
|
|
||||||
isEditing = true;
|
|
||||||
}}
|
|
||||||
onblur={commitIfValid}
|
|
||||||
onkeydown={handleKeydown}
|
onkeydown={handleKeydown}
|
||||||
/>
|
/>
|
||||||
{#if isEditing}
|
<button data-testid="commit-button" onclick={commitEdit}><Check /></button>
|
||||||
<button data-testid="commit-button" disabled={wasCancelled} onclick={commitIfValid}
|
|
||||||
><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>
|
||||||
|
|||||||
@@ -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(() => {
|
|
||||||
|
if (res.ok) {
|
||||||
inProgress = false;
|
inProgress = false;
|
||||||
invalidateAll();
|
invalidateAll();
|
||||||
crimesList = data.crimesList;
|
data.crimesList = newName;
|
||||||
open = false;
|
open = false;
|
||||||
})
|
} else {
|
||||||
.catch((err) => {
|
|
||||||
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}
|
||||||
|
|||||||
@@ -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,68 +10,84 @@ 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', () => {
|
||||||
|
test.todo('Share Link disabled wenn Liste leer');
|
||||||
describe('Szenario: Admin + Liste gefüllt', () => {
|
describe('Szenario: Admin + Liste gefüllt', () => {
|
||||||
it('ändert den Namen nach Speichern', async () => {
|
test.todo('Share Link Link generierung richtig');
|
||||||
|
|
||||||
|
it('ändert den Namen nach Speichern', async () => {
|
||||||
const testData = structuredClone(baseData);
|
const testData = structuredClone(baseData);
|
||||||
const oldName = testData.crimesList[0].name;
|
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;
|
||||||
|
|
||||||
render(TatortListPage, { props: { data: testData } });
|
const { getAllByTestId } = render(TatortListPage, { props: { data: testData } });
|
||||||
|
|
||||||
const firstItem = screen.getAllByTestId('test-list-item')[0];
|
const firstItem = getAllByTestId('test-list-item')[0];
|
||||||
await fireEvent.click(within(firstItem).getByTestId('edit-button'));
|
const editButton = within(firstItem).getByTestId('edit-button');
|
||||||
|
await fireEvent.click(editButton);
|
||||||
|
|
||||||
const input = within(firstItem).getByRole('textbox');
|
const input = within(firstItem).getByTestId('test-input');
|
||||||
await fireEvent.input(input, { target: { value: 'Fall-B' } });
|
expect(input).toHaveValue(oldName)
|
||||||
|
await fireEvent.input(input, { target: { value: newName } });
|
||||||
|
|
||||||
await fireEvent.click(within(firstItem).getByTestId('commit-button'));
|
const commitButton = within(firstItem).getByTestId('commit-button');
|
||||||
|
await fireEvent.click(commitButton);
|
||||||
|
|
||||||
// Erwartung: fetch wurde aufgerufen
|
// const fetchMock = global.fetch as ReturnType<typeof vi.fn>;
|
||||||
expect(global.fetch).toHaveBeenCalledWith(
|
// console.log('Fetch calls:', fetchMock.mock.calls);
|
||||||
expect.stringContaining(`/api/list/${testData.vorgang.vorgangToken}/${oldName}`),
|
|
||||||
expect.any(Object)
|
|
||||||
);
|
|
||||||
|
|
||||||
// Erwartung: neuer Name ist sofort im DOM sichtbar
|
// // Erwartung: fetch wurde aufgerufen
|
||||||
expect(within(firstItem).getByRole('textbox')).toHaveValue('Fall-B');
|
// 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})
|
||||||
|
// })
|
||||||
|
// );
|
||||||
|
|
||||||
});
|
// 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}`;
|
||||||
|
|
||||||
it('entfernt das Listenelement nach Löschen', async () => {
|
// expect(editedLink).toBeInTheDocument();
|
||||||
const testData = structuredClone(baseData);
|
// expect(editedLink).toHaveAttribute('href', editedExpectedHref);
|
||||||
testData.url = new URL('https://example.com/vorgang-1'); // Fix für Invalid URL
|
// expect(editedLink).toHaveAttribute('title', newName);
|
||||||
const toDelete = testData.crimesList[0];
|
|
||||||
|
|
||||||
global.fetch = vi.fn().mockResolvedValue({ ok: true });
|
|
||||||
|
|
||||||
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}`;
|
|
||||||
|
|
||||||
expect(deletedLink).toBeInTheDocument();
|
|
||||||
|
|
||||||
expect(deletedLink).toHaveAttribute('href', deletedExpectedHref);
|
|
||||||
expect(deletedLink).toHaveAttribute('title', 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);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 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];
|
||||||
|
|
||||||
|
// global.fetch = vi.fn().mockResolvedValue({ ok: true });
|
||||||
|
|
||||||
|
// 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}`;
|
||||||
|
|
||||||
|
// expect(deletedLink).toBeInTheDocument();
|
||||||
|
// expect(deletedLink).toHaveAttribute('href', deletedExpectedHref);
|
||||||
|
// expect(deletedLink).toHaveAttribute('title', 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);
|
||||||
|
// });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -14,14 +14,16 @@ describe('Seite: Vorgangsansicht', () => {
|
|||||||
|
|
||||||
expect(getByTestId('empty-list')).toBeInTheDocument();
|
expect(getByTestId('empty-list')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('zeigt keinen Listeneintrag', () => {
|
it('zeigt keinen Listeneintrag', () => {
|
||||||
const items = screen.queryAllByTestId('test-list-item');
|
const items = screen.queryAllByTestId('test-list-item');
|
||||||
|
|
||||||
expect(items).toHaveLength(0);
|
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,6 +67,8 @@ 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', () => {
|
||||||
@@ -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')
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user