Files
TREK/client/tests/unit/mobile/admin/MAdminPackingTemplateManager.test.tsx
T
Maurice e3c524dd02 test: give every test case an assertion of its own
SonarQube's S2699 flagged 196 test cases as assertion-less. Almost all of
them end on a bare `await screen.findByText(...)`, which does throw when the
text never shows up — the test is real, the analyser just cannot see it.
Spelling the check out keeps the behaviour identical and makes it readable.

Writing them out turned three tests red, each for a good reason:

- FE-COMP-NOTES-008 read the heading out of the loading panel, which renders
  its own copy and swaps the whole node out once the fetch settles. It now
  waits for the loaded state.
- FE-COMP-PHOTOPROVIDERS-014 matched /connected/i, which also matches the
  "Not connected" the badge starts as — the test passed whether or not the
  button did anything. Exact text now.
- FE-FILES-002 compared an empty list against the empty list it already was.
  It seeds a file first, so the 500 has something to leave alone.

JOURNEY-SVC-066 asserted nothing at all behind a "should not throw" comment;
it now says that out loud and checks no journey entry is left orphaned.
2026-08-25 11:02:39 +02:00

678 lines
27 KiB
TypeScript

// FE-MOB-APKG-001 to FE-MOB-APKG-028
import { describe, it, expect, beforeEach } from 'vitest';
import userEvent from '@testing-library/user-event';
import { http, HttpResponse } from 'msw';
import { render, screen, fireEvent, waitFor, within } from '../../../helpers/render';
import { server } from '../../../helpers/msw/server';
import { resetAllStores } from '../../../helpers/store';
import { ToastContainer } from '../../../../src/components/shared/Toast';
import MAdminPackingTemplateManager from '../../../../src/mobile/screens/admin/MAdminPackingTemplateManager';
const tmpl1 = { id: 1, name: 'Beach Trip', item_count: 5, category_count: 2, created_by_name: 'admin' };
const tmpl2 = { id: 2, name: 'City Break', item_count: 3, category_count: 1, created_by_name: 'admin' };
const cat1 = { id: 10, template_id: 1, name: 'Clothing', sort_order: 0 };
const item1 = { id: 100, category_id: 10, name: 'T-shirt', sort_order: 0 };
const item2 = { id: 101, category_id: 10, name: 'Shorts', sort_order: 1 };
function list(templates: unknown[]) {
return http.get('/api/admin/packing-templates', () => HttpResponse.json({ templates }));
}
function detail(id: number, categories: unknown[], items: unknown[]) {
return http.get(`/api/admin/packing-templates/${id}`, () => HttpResponse.json({ categories, items }));
}
function renderManager() {
return render(<><ToastContainer /><MAdminPackingTemplateManager /></>);
}
// Template rows nest the name two levels below the row that holds the icon buttons.
function templateRow(name: string): HTMLElement {
return screen.getByText(name).parentElement!.parentElement as HTMLElement;
}
function categoryHeader(name: string): HTMLElement {
return screen.getByText(name).closest('div') as HTMLElement;
}
function itemRow(name: string): HTMLElement {
return screen.getByText(name).closest('div') as HTMLElement;
}
beforeEach(() => {
resetAllStores();
});
describe('MAdminPackingTemplateManager', () => {
it('FE-MOB-APKG-001: shows the spinner while templates load, then the empty state', async () => {
server.use(
http.get('/api/admin/packing-templates', async () => {
await new Promise(r => setTimeout(r, 60));
return HttpResponse.json({ templates: [] });
}),
);
renderManager();
expect(document.querySelector('.animate-spin')).toBeInTheDocument();
expect(screen.getByText('Packing Templates')).toBeInTheDocument();
expect(screen.getByText('Create reusable packing lists for your trips')).toBeInTheDocument();
await screen.findByText('No templates created yet');
expect(document.querySelector('.animate-spin')).not.toBeInTheDocument();
});
it('FE-MOB-APKG-002: renders each template with its category and item counts', async () => {
server.use(list([tmpl1, tmpl2]));
renderManager();
await screen.findByText('Beach Trip');
expect(screen.getByText('City Break')).toBeInTheDocument();
expect(screen.getByText('2 categories · 5 items')).toBeInTheDocument();
expect(screen.getByText('1 categories · 3 items')).toBeInTheDocument();
});
it('FE-MOB-APKG-003: a failing list request surfaces the load error toast', async () => {
server.use(http.get('/api/admin/packing-templates', () => HttpResponse.error()));
renderManager();
await screen.findByText('Failed to load templates');
expect(screen.getByText('No templates created yet')).toBeInTheDocument();
});
it('FE-MOB-APKG-004: creating a template prepends it, expands it and toasts', async () => {
const user = userEvent.setup();
let posted: unknown = null;
server.use(
list([tmpl1]),
http.post('/api/admin/packing-templates', async ({ request }) => {
posted = await request.json();
return HttpResponse.json({ template: { id: 99, name: 'Winter Trip' } });
}),
detail(99, [], []),
);
renderManager();
await screen.findByText('Beach Trip');
await user.click(screen.getByRole('button', { name: 'New Template' }));
await user.type(screen.getByPlaceholderText('Template name (e.g. Beach Holiday)'), ' Winter Trip {Enter}');
await waitFor(() => expect(posted).toEqual({ name: 'Winter Trip' }));
await screen.findByText('Template created');
expect(screen.getByText('0 categories · 0 items')).toBeInTheDocument();
// The new template is expanded right away, so its category editor is visible.
expect(screen.getByRole('button', { name: 'Add category' })).toBeInTheDocument();
expect(screen.queryByPlaceholderText('Template name (e.g. Beach Holiday)')).not.toBeInTheDocument();
});
it('FE-MOB-APKG-005: saving a blank template name is a no-op', async () => {
const user = userEvent.setup();
let postCalls = 0;
server.use(
http.post('/api/admin/packing-templates', () => {
postCalls += 1;
return HttpResponse.json({ template: { id: 5, name: '' } });
}),
);
renderManager();
await screen.findByText('No templates created yet');
await user.click(screen.getByRole('button', { name: 'New Template' }));
await user.type(screen.getByPlaceholderText('Template name (e.g. Beach Holiday)'), ' ');
await user.click(screen.getByRole('button', { name: 'Save' }));
expect(postCalls).toBe(0);
expect(screen.getByPlaceholderText('Template name (e.g. Beach Holiday)')).toBeInTheDocument();
});
it('FE-MOB-APKG-006: a failing create shows the create error toast', async () => {
const user = userEvent.setup();
server.use(
http.post('/api/admin/packing-templates', () => HttpResponse.json({ error: 'nope' }, { status: 500 })),
);
renderManager();
await screen.findByText('No templates created yet');
await user.click(screen.getByRole('button', { name: 'New Template' }));
await user.type(screen.getByPlaceholderText('Template name (e.g. Beach Holiday)'), 'Boom{Enter}');
expect(await screen.findByText('Failed to create template')).toBeInTheDocument();
});
it('FE-MOB-APKG-007: Escape and the cancel button both close the create field', async () => {
const user = userEvent.setup();
renderManager();
await screen.findByText('No templates created yet');
await user.click(screen.getByRole('button', { name: 'New Template' }));
await user.type(screen.getByPlaceholderText('Template name (e.g. Beach Holiday)'), 'Draft{Escape}');
await waitFor(() =>
expect(screen.queryByPlaceholderText('Template name (e.g. Beach Holiday)')).not.toBeInTheDocument(),
);
await user.click(screen.getByRole('button', { name: 'New Template' }));
await user.click(screen.getByRole('button', { name: 'Cancel' }));
await waitFor(() =>
expect(screen.queryByPlaceholderText('Template name (e.g. Beach Holiday)')).not.toBeInTheDocument(),
);
});
it('FE-MOB-APKG-008: expanding loads categories and items, collapsing hides them again', async () => {
const user = userEvent.setup();
server.use(list([tmpl1]), detail(1, [cat1], [item1, item2]));
renderManager();
await screen.findByText('Beach Trip');
await user.click(screen.getByRole('button', { name: 'Expand' }));
await screen.findByText('Clothing');
expect(screen.getByText('T-shirt')).toBeInTheDocument();
expect(screen.getByText('Shorts')).toBeInTheDocument();
// The category header counts its items.
expect(within(categoryHeader('Clothing')).getByText('2')).toBeInTheDocument();
await user.click(screen.getByRole('button', { name: 'Collapse' }));
await waitFor(() => expect(screen.queryByText('Clothing')).not.toBeInTheDocument());
expect(screen.queryByText('T-shirt')).not.toBeInTheDocument();
});
it('FE-MOB-APKG-009: a failing detail request toasts and leaves the template empty', async () => {
const user = userEvent.setup();
server.use(
list([tmpl1]),
http.get('/api/admin/packing-templates/1', () => HttpResponse.error()),
);
renderManager();
await screen.findByText('Beach Trip');
await user.click(screen.getByText('Beach Trip'));
await screen.findByText('Failed to load templates');
expect(screen.getByRole('button', { name: 'Add category' })).toBeInTheDocument();
});
it('FE-MOB-APKG-010: deleting a template removes it and collapses the expanded panel', async () => {
const user = userEvent.setup();
let deleted = false;
server.use(
list([tmpl1, tmpl2]),
detail(1, [cat1], [item1]),
http.delete('/api/admin/packing-templates/1', () => {
deleted = true;
return HttpResponse.json({ success: true });
}),
);
renderManager();
await screen.findByText('Beach Trip');
await user.click(screen.getByText('Beach Trip'));
await screen.findByText('Clothing');
await user.click(within(templateRow('Beach Trip')).getByRole('button', { name: 'Delete' }));
await waitFor(() => expect(deleted).toBe(true));
await waitFor(() => expect(screen.queryByText('Beach Trip')).not.toBeInTheDocument());
expect(screen.queryByText('Clothing')).not.toBeInTheDocument();
expect(screen.getByText('City Break')).toBeInTheDocument();
await screen.findByText('Template deleted');
});
it('FE-MOB-APKG-011: a failing template delete keeps the row and toasts', async () => {
const user = userEvent.setup();
server.use(
list([tmpl1]),
http.delete('/api/admin/packing-templates/1', () => HttpResponse.error()),
);
renderManager();
await screen.findByText('Beach Trip');
await user.click(within(templateRow('Beach Trip')).getByRole('button', { name: 'Delete' }));
await screen.findByText('Failed to delete template');
expect(screen.getByText('Beach Trip')).toBeInTheDocument();
});
it('FE-MOB-APKG-012: renaming a template persists the trimmed name', async () => {
const user = userEvent.setup();
let body: unknown = null;
server.use(
list([tmpl1]),
http.put('/api/admin/packing-templates/1', async ({ request }) => {
body = await request.json();
return HttpResponse.json({ success: true });
}),
);
renderManager();
await screen.findByText('Beach Trip');
await user.click(within(templateRow('Beach Trip')).getByRole('button', { name: 'Edit' }));
const input = screen.getByDisplayValue('Beach Trip');
await user.clear(input);
await user.type(input, 'Summer Packing{Enter}');
await waitFor(() => expect(body).toEqual({ name: 'Summer Packing' }));
await screen.findByText('Summer Packing');
});
it('FE-MOB-APKG-013: blurring an emptied template name cancels the rename', async () => {
const user = userEvent.setup();
let putCalls = 0;
server.use(
list([tmpl1]),
http.put('/api/admin/packing-templates/1', () => {
putCalls += 1;
return HttpResponse.json({ success: true });
}),
);
renderManager();
await screen.findByText('Beach Trip');
await user.click(within(templateRow('Beach Trip')).getByRole('button', { name: 'Edit' }));
const input = screen.getByDisplayValue('Beach Trip');
await user.clear(input);
fireEvent.blur(input);
await waitFor(() => expect(screen.getByText('Beach Trip')).toBeInTheDocument());
expect(putCalls).toBe(0);
});
it('FE-MOB-APKG-014: Escape cancels the rename and a failing rename toasts', async () => {
const user = userEvent.setup();
let putCalls = 0;
server.use(
list([tmpl1]),
http.put('/api/admin/packing-templates/1', () => {
putCalls += 1;
return HttpResponse.error();
}),
);
renderManager();
await screen.findByText('Beach Trip');
await user.click(within(templateRow('Beach Trip')).getByRole('button', { name: 'Edit' }));
await user.type(screen.getByDisplayValue('Beach Trip'), '{Escape}');
await waitFor(() => expect(screen.queryByDisplayValue('Beach Trip')).not.toBeInTheDocument());
expect(putCalls).toBe(0);
await user.click(within(templateRow('Beach Trip')).getByRole('button', { name: 'Edit' }));
await user.type(screen.getByDisplayValue('Beach Trip'), ' 2{Enter}');
await screen.findByText('Failed to save');
expect(putCalls).toBe(1);
});
it('FE-MOB-APKG-015: adding a category appends it to the expanded template', async () => {
const user = userEvent.setup();
let body: unknown = null;
server.use(
list([tmpl1]),
detail(1, [], []),
http.post('/api/admin/packing-templates/1/categories', async ({ request }) => {
body = await request.json();
return HttpResponse.json({ category: { id: 20, template_id: 1, name: 'Electronics', sort_order: 1 } });
}),
);
renderManager();
await screen.findByText('Beach Trip');
await user.click(screen.getByText('Beach Trip'));
await user.click(await screen.findByRole('button', { name: 'Add category' }));
await user.type(screen.getByPlaceholderText('Category name (e.g. Clothing)'), 'Electronics{Enter}');
await waitFor(() => expect(body).toEqual({ name: 'Electronics' }));
await screen.findByText('Electronics');
expect(screen.queryByPlaceholderText('Category name (e.g. Clothing)')).not.toBeInTheDocument();
});
it('FE-MOB-APKG-016: a blank category name is ignored and Escape closes the field', async () => {
const user = userEvent.setup();
let postCalls = 0;
server.use(
list([tmpl1]),
detail(1, [], []),
http.post('/api/admin/packing-templates/1/categories', () => {
postCalls += 1;
return HttpResponse.json({ category: { id: 20, template_id: 1, name: 'x', sort_order: 1 } });
}),
);
renderManager();
await screen.findByText('Beach Trip');
await user.click(screen.getByText('Beach Trip'));
await user.click(await screen.findByRole('button', { name: 'Add category' }));
await user.click(screen.getByRole('button', { name: 'Save' }));
expect(postCalls).toBe(0);
await user.type(screen.getByPlaceholderText('Category name (e.g. Clothing)'), 'Draft{Escape}');
await waitFor(() =>
expect(screen.queryByPlaceholderText('Category name (e.g. Clothing)')).not.toBeInTheDocument(),
);
// Reopening starts from an empty field and the cancel button closes it too.
await user.click(screen.getByRole('button', { name: 'Add category' }));
expect(screen.getByPlaceholderText('Category name (e.g. Clothing)')).toHaveValue('');
await user.click(screen.getByRole('button', { name: 'Cancel' }));
await waitFor(() =>
expect(screen.queryByPlaceholderText('Category name (e.g. Clothing)')).not.toBeInTheDocument(),
);
expect(postCalls).toBe(0);
});
it('FE-MOB-APKG-017: a failing category create toasts the save error', async () => {
const user = userEvent.setup();
server.use(
list([tmpl1]),
detail(1, [], []),
http.post('/api/admin/packing-templates/1/categories', () => HttpResponse.error()),
);
renderManager();
await screen.findByText('Beach Trip');
await user.click(screen.getByText('Beach Trip'));
await user.click(await screen.findByRole('button', { name: 'Add category' }));
await user.type(screen.getByPlaceholderText('Category name (e.g. Clothing)'), 'Electronics{Enter}');
expect(await screen.findByText('Failed to save')).toBeInTheDocument();
});
it('FE-MOB-APKG-018: renaming a category updates its header', async () => {
const user = userEvent.setup();
let body: unknown = null;
server.use(
list([tmpl1]),
detail(1, [cat1], []),
http.put('/api/admin/packing-templates/1/categories/10', async ({ request }) => {
body = await request.json();
return HttpResponse.json({ success: true });
}),
);
renderManager();
await screen.findByText('Beach Trip');
await user.click(screen.getByText('Beach Trip'));
await screen.findByText('Clothing');
await user.click(within(categoryHeader('Clothing')).getByRole('button', { name: 'Edit' }));
const input = screen.getByDisplayValue('Clothing');
await user.clear(input);
await user.type(input, 'Shoes{Enter}');
await waitFor(() => expect(body).toEqual({ name: 'Shoes' }));
await screen.findByText('Shoes');
});
it('FE-MOB-APKG-019: Escape and an emptied field both cancel the category rename', async () => {
const user = userEvent.setup();
let putCalls = 0;
server.use(
list([tmpl1]),
detail(1, [cat1], []),
http.put('/api/admin/packing-templates/1/categories/10', () => {
putCalls += 1;
return HttpResponse.json({ success: true });
}),
);
renderManager();
await screen.findByText('Beach Trip');
await user.click(screen.getByText('Beach Trip'));
await screen.findByText('Clothing');
await user.click(within(categoryHeader('Clothing')).getByRole('button', { name: 'Edit' }));
await user.type(screen.getByDisplayValue('Clothing'), '{Escape}');
await waitFor(() => expect(screen.queryByDisplayValue('Clothing')).not.toBeInTheDocument());
await user.click(within(categoryHeader('Clothing')).getByRole('button', { name: 'Edit' }));
const input = screen.getByDisplayValue('Clothing');
await user.clear(input);
fireEvent.blur(input);
await waitFor(() => expect(screen.getByText('Clothing')).toBeInTheDocument());
expect(putCalls).toBe(0);
});
it('FE-MOB-APKG-029: a failing category rename toasts and keeps the old name', async () => {
const user = userEvent.setup();
server.use(
list([tmpl1]),
detail(1, [cat1], []),
http.put('/api/admin/packing-templates/1/categories/10', () => HttpResponse.error()),
);
renderManager();
await screen.findByText('Beach Trip');
await user.click(screen.getByText('Beach Trip'));
await screen.findByText('Clothing');
await user.click(within(categoryHeader('Clothing')).getByRole('button', { name: 'Edit' }));
await user.type(screen.getByDisplayValue('Clothing'), ' XL{Enter}');
await screen.findByText('Failed to save');
expect(screen.getByDisplayValue('Clothing XL')).toBeInTheDocument();
});
it('FE-MOB-APKG-020: deleting a category drops its items too', async () => {
const user = userEvent.setup();
let deleted = false;
server.use(
list([tmpl1]),
detail(1, [cat1], [item1, item2]),
http.delete('/api/admin/packing-templates/1/categories/10', () => {
deleted = true;
return HttpResponse.json({ success: true });
}),
);
renderManager();
await screen.findByText('Beach Trip');
await user.click(screen.getByText('Beach Trip'));
await screen.findByText('Clothing');
await user.click(within(categoryHeader('Clothing')).getByRole('button', { name: 'Delete' }));
await waitFor(() => expect(deleted).toBe(true));
await waitFor(() => expect(screen.queryByText('Clothing')).not.toBeInTheDocument());
expect(screen.queryByText('T-shirt')).not.toBeInTheDocument();
expect(screen.queryByText('Shorts')).not.toBeInTheDocument();
});
it('FE-MOB-APKG-021: a failing category delete toasts and keeps the category', async () => {
const user = userEvent.setup();
server.use(
list([tmpl1]),
detail(1, [cat1], [item1]),
http.delete('/api/admin/packing-templates/1/categories/10', () => HttpResponse.error()),
);
renderManager();
await screen.findByText('Beach Trip');
await user.click(screen.getByText('Beach Trip'));
await screen.findByText('Clothing');
await user.click(within(categoryHeader('Clothing')).getByRole('button', { name: 'Delete' }));
await screen.findByText('Failed to delete');
expect(screen.getByText('Clothing')).toBeInTheDocument();
});
it('FE-MOB-APKG-022: adding an item posts to the category and clears the field', async () => {
const user = userEvent.setup();
let body: unknown = null;
server.use(
list([tmpl1]),
detail(1, [cat1], []),
http.post('/api/admin/packing-templates/1/categories/10/items', async ({ request }) => {
body = await request.json();
return HttpResponse.json({ item: { id: 102, category_id: 10, name: 'Sandals', sort_order: 2 } });
}),
);
renderManager();
await screen.findByText('Beach Trip');
await user.click(screen.getByText('Beach Trip'));
await screen.findByText('Clothing');
await user.click(within(categoryHeader('Clothing')).getByRole('button', { name: 'Item name' }));
await user.type(screen.getByPlaceholderText('Item name'), 'Sandals{Enter}');
await waitFor(() => expect(body).toEqual({ name: 'Sandals' }));
await screen.findByText('Sandals');
expect(screen.getByPlaceholderText('Item name')).toHaveValue('');
});
it('FE-MOB-APKG-023: the add-item confirm button is disabled while the field is empty', async () => {
const user = userEvent.setup();
server.use(
list([tmpl1]),
detail(1, [cat1], []),
http.post('/api/admin/packing-templates/1/categories/10/items', () =>
HttpResponse.json({ item: { id: 103, category_id: 10, name: 'Hat', sort_order: 3 } }),
),
);
renderManager();
await screen.findByText('Beach Trip');
await user.click(screen.getByText('Beach Trip'));
await screen.findByText('Clothing');
await user.click(within(categoryHeader('Clothing')).getByRole('button', { name: 'Item name' }));
const addRow = screen.getByPlaceholderText('Item name').closest('div') as HTMLElement;
const confirm = within(addRow).getByRole('button', { name: 'Item name' });
expect(confirm).toBeDisabled();
await user.type(screen.getByPlaceholderText('Item name'), 'Hat');
expect(confirm).toBeEnabled();
await user.click(confirm);
await screen.findByText('Hat');
});
it('FE-MOB-APKG-024: Escape and cancel close the add-item field without posting', async () => {
const user = userEvent.setup();
let postCalls = 0;
server.use(
list([tmpl1]),
detail(1, [cat1], []),
http.post('/api/admin/packing-templates/1/categories/10/items', () => {
postCalls += 1;
return HttpResponse.json({ item: { id: 104, category_id: 10, name: 'x', sort_order: 4 } });
}),
);
renderManager();
await screen.findByText('Beach Trip');
await user.click(screen.getByText('Beach Trip'));
await screen.findByText('Clothing');
const plus = within(categoryHeader('Clothing')).getByRole('button', { name: 'Item name' });
await user.click(plus);
await user.type(screen.getByPlaceholderText('Item name'), 'Draft{Escape}');
await waitFor(() => expect(screen.queryByPlaceholderText('Item name')).not.toBeInTheDocument());
await user.click(plus);
await user.type(screen.getByPlaceholderText('Item name'), 'Draft');
const addRow = screen.getByPlaceholderText('Item name').closest('div') as HTMLElement;
await user.click(within(addRow).getByRole('button', { name: 'Cancel' }));
await waitFor(() => expect(screen.queryByPlaceholderText('Item name')).not.toBeInTheDocument());
// The plus button toggles the field shut when pressed a second time.
await user.click(plus);
expect(screen.getByPlaceholderText('Item name')).toBeInTheDocument();
await user.click(plus);
await waitFor(() => expect(screen.queryByPlaceholderText('Item name')).not.toBeInTheDocument());
expect(postCalls).toBe(0);
});
it('FE-MOB-APKG-025: a failing item create toasts the save error', async () => {
const user = userEvent.setup();
server.use(
list([tmpl1]),
detail(1, [cat1], []),
http.post('/api/admin/packing-templates/1/categories/10/items', () => HttpResponse.error()),
);
renderManager();
await screen.findByText('Beach Trip');
await user.click(screen.getByText('Beach Trip'));
await screen.findByText('Clothing');
await user.click(within(categoryHeader('Clothing')).getByRole('button', { name: 'Item name' }));
await user.type(screen.getByPlaceholderText('Item name'), 'Sandals{Enter}');
expect(await screen.findByText('Failed to save')).toBeInTheDocument();
});
it('FE-MOB-APKG-026: renaming an item persists via the save button', async () => {
const user = userEvent.setup();
let body: unknown = null;
server.use(
list([tmpl1]),
detail(1, [cat1], [item1]),
http.put('/api/admin/packing-templates/1/items/100', async ({ request }) => {
body = await request.json();
return HttpResponse.json({ success: true });
}),
);
renderManager();
await screen.findByText('Beach Trip');
await user.click(screen.getByText('Beach Trip'));
await screen.findByText('T-shirt');
await user.click(within(itemRow('T-shirt')).getByRole('button', { name: 'Edit' }));
const input = screen.getByDisplayValue('T-shirt');
await user.clear(input);
await user.type(input, 'Tank Top');
await user.click(screen.getByRole('button', { name: 'Save' }));
await waitFor(() => expect(body).toEqual({ name: 'Tank Top' }));
await screen.findByText('Tank Top');
});
it('FE-MOB-APKG-027: item rename cancels on Escape, on cancel and on an empty name', async () => {
const user = userEvent.setup();
let putCalls = 0;
server.use(
list([tmpl1]),
detail(1, [cat1], [item1]),
http.put('/api/admin/packing-templates/1/items/100', () => {
putCalls += 1;
return HttpResponse.error();
}),
);
renderManager();
await screen.findByText('Beach Trip');
await user.click(screen.getByText('Beach Trip'));
await screen.findByText('T-shirt');
await user.click(within(itemRow('T-shirt')).getByRole('button', { name: 'Edit' }));
await user.type(screen.getByDisplayValue('T-shirt'), '{Escape}');
await waitFor(() => expect(screen.queryByDisplayValue('T-shirt')).not.toBeInTheDocument());
await user.click(within(itemRow('T-shirt')).getByRole('button', { name: 'Edit' }));
await user.click(screen.getByRole('button', { name: 'Cancel' }));
await waitFor(() => expect(screen.queryByDisplayValue('T-shirt')).not.toBeInTheDocument());
await user.click(within(itemRow('T-shirt')).getByRole('button', { name: 'Edit' }));
const input = screen.getByDisplayValue('T-shirt');
await user.clear(input);
await user.type(input, '{Enter}');
await waitFor(() => expect(screen.getByText('T-shirt')).toBeInTheDocument());
expect(putCalls).toBe(0);
// A rejected rename keeps the old name and toasts.
await user.click(within(itemRow('T-shirt')).getByRole('button', { name: 'Edit' }));
await user.type(screen.getByDisplayValue('T-shirt'), ' XL{Enter}');
await screen.findByText('Failed to save');
expect(putCalls).toBe(1);
});
it('FE-MOB-APKG-028: deleting an item removes only that item, failures toast', async () => {
const user = userEvent.setup();
server.use(
list([tmpl1]),
detail(1, [cat1], [item1, item2]),
http.delete('/api/admin/packing-templates/1/items/100', () => HttpResponse.json({ success: true })),
http.delete('/api/admin/packing-templates/1/items/101', () => HttpResponse.error()),
);
renderManager();
await screen.findByText('Beach Trip');
await user.click(screen.getByText('Beach Trip'));
await screen.findByText('T-shirt');
await user.click(within(itemRow('T-shirt')).getByRole('button', { name: 'Delete' }));
await waitFor(() => expect(screen.queryByText('T-shirt')).not.toBeInTheDocument());
expect(screen.getByText('Shorts')).toBeInTheDocument();
await user.click(within(itemRow('Shorts')).getByRole('button', { name: 'Delete' }));
await screen.findByText('Failed to delete');
expect(screen.getByText('Shorts')).toBeInTheDocument();
});
});