mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-24 07:53:09 +00:00
@@ -1,5 +1,5 @@
|
||||
// FE-ADMIN-PKG-001 to FE-ADMIN-PKG-020
|
||||
import { render, screen, waitFor } from '../../../tests/helpers/render';
|
||||
// FE-ADMIN-PKG-001 to FE-ADMIN-PKG-032
|
||||
import { render, screen, waitFor, within } from '../../../tests/helpers/render';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { http, HttpResponse } from 'msw';
|
||||
import { server } from '../../../tests/helpers/msw/server';
|
||||
@@ -18,6 +18,23 @@ beforeEach(() => {
|
||||
resetAllStores();
|
||||
});
|
||||
|
||||
/** Template rows carry [chevron, name, edit, delete]; category headers [add item, edit, delete]. */
|
||||
function rowButtons(name: string, selector: string): HTMLElement[] {
|
||||
const row = screen.getByText(name).closest(selector) as HTMLElement;
|
||||
return within(row).getAllByRole('button');
|
||||
}
|
||||
|
||||
const templateButtons = (name: string) => rowButtons(name, '.px-5.py-3');
|
||||
const categoryButtons = (name: string) => rowButtons(name, '.bg-slate-50');
|
||||
const itemButtons = (name: string) => rowButtons(name, '.group');
|
||||
|
||||
/** Expands the single fixture template and waits for its content. */
|
||||
async function expandBeachTrip(user: ReturnType<typeof userEvent.setup>, firstChild: string) {
|
||||
await screen.findByText('Beach Trip');
|
||||
await user.click(screen.getByText('Beach Trip'));
|
||||
await screen.findByText(firstChild);
|
||||
}
|
||||
|
||||
describe('PackingTemplateManager', () => {
|
||||
it('FE-ADMIN-PKG-001: shows loading spinner on mount', async () => {
|
||||
server.use(
|
||||
@@ -233,7 +250,7 @@ describe('PackingTemplateManager', () => {
|
||||
await user.click(screen.getByText('Add category'));
|
||||
const catInput = screen.getByPlaceholderText('Category name (e.g. Clothing)');
|
||||
await user.type(catInput, 'Electronics{Enter}');
|
||||
await screen.findByText('Electronics');
|
||||
expect(await screen.findByText('Electronics')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-ADMIN-PKG-012: adding an item to a category', async () => {
|
||||
@@ -263,7 +280,7 @@ describe('PackingTemplateManager', () => {
|
||||
await user.type(itemInput, 'Sandals');
|
||||
// Submit via Enter key (the input's onKeyDown handler triggers handleAddItem)
|
||||
await user.type(itemInput, '{Enter}');
|
||||
await screen.findByText('Sandals');
|
||||
expect(await screen.findByText('Sandals')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-ADMIN-PKG-013: renaming a category inline updates its name', async () => {
|
||||
@@ -295,7 +312,7 @@ describe('PackingTemplateManager', () => {
|
||||
const catInput = screen.getByDisplayValue('Clothing');
|
||||
await user.clear(catInput);
|
||||
await user.type(catInput, 'Shoes{Enter}');
|
||||
await screen.findByText('Shoes');
|
||||
expect(await screen.findByText('Shoes')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-ADMIN-PKG-014: deleting a category removes it and its items', async () => {
|
||||
@@ -362,7 +379,7 @@ describe('PackingTemplateManager', () => {
|
||||
const input = screen.getByDisplayValue('T-shirt');
|
||||
await user.clear(input);
|
||||
await user.type(input, 'Tank Top{Enter}');
|
||||
await screen.findByText('Tank Top');
|
||||
expect(await screen.findByText('Tank Top')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-ADMIN-PKG-016: deleting an item removes it from the list', async () => {
|
||||
@@ -508,4 +525,296 @@ describe('PackingTemplateManager', () => {
|
||||
expect(screen.queryByPlaceholderText('Template name (e.g. Beach Holiday)')).not.toBeInTheDocument()
|
||||
);
|
||||
});
|
||||
|
||||
it('FE-ADMIN-PKG-021: a failing template list toasts and shows the empty state', async () => {
|
||||
server.use(http.get('/api/admin/packing-templates', () => HttpResponse.error()));
|
||||
render(<><ToastContainer /><PackingTemplateManager /></>);
|
||||
|
||||
expect(await screen.findByText('Failed to load templates')).toBeInTheDocument();
|
||||
expect(screen.getByText('No templates created yet')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-ADMIN-PKG-022: a failing expand toasts and leaves the template without content', async () => {
|
||||
const user = userEvent.setup();
|
||||
server.use(
|
||||
http.get('/api/admin/packing-templates', () => HttpResponse.json({ templates: [tmpl1] })),
|
||||
http.get('/api/admin/packing-templates/1', () => HttpResponse.error()),
|
||||
);
|
||||
render(<><ToastContainer /><PackingTemplateManager /></>);
|
||||
await screen.findByText('Beach Trip');
|
||||
|
||||
await user.click(screen.getByText('Beach Trip'));
|
||||
|
||||
expect(await screen.findByText('Failed to load templates')).toBeInTheDocument();
|
||||
expect(screen.getByText('Add category')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-ADMIN-PKG-023: an empty name is not submitted and a failing create toasts', async () => {
|
||||
const user = userEvent.setup();
|
||||
let posts = 0;
|
||||
server.use(
|
||||
http.post('/api/admin/packing-templates', () => {
|
||||
posts += 1;
|
||||
return HttpResponse.json({ error: 'nope' }, { status: 500 });
|
||||
}),
|
||||
);
|
||||
render(<><ToastContainer /><PackingTemplateManager /></>);
|
||||
await screen.findByText('No templates created yet');
|
||||
|
||||
await user.click(screen.getByRole('button', { name: /new template/i }));
|
||||
const input = screen.getByPlaceholderText('Template name (e.g. Beach Holiday)');
|
||||
await user.type(input, ' {Enter}');
|
||||
expect(posts).toBe(0);
|
||||
|
||||
await user.clear(input);
|
||||
await user.type(input, 'Ski trip{Enter}');
|
||||
expect(await screen.findByText('Failed to create template')).toBeInTheDocument();
|
||||
expect(posts).toBe(1);
|
||||
});
|
||||
|
||||
it('FE-ADMIN-PKG-024: deleting the expanded template collapses it, a failing delete toasts', async () => {
|
||||
const user = userEvent.setup();
|
||||
let calls = 0;
|
||||
server.use(
|
||||
http.get('/api/admin/packing-templates', () => HttpResponse.json({ templates: [tmpl1] })),
|
||||
http.get('/api/admin/packing-templates/1', () => HttpResponse.json({ categories: [cat1], items: [item1] })),
|
||||
http.delete('/api/admin/packing-templates/1', () => {
|
||||
calls += 1;
|
||||
return calls === 1
|
||||
? HttpResponse.json({ error: 'in use' }, { status: 500 })
|
||||
: HttpResponse.json({ success: true });
|
||||
}),
|
||||
);
|
||||
render(<><ToastContainer /><PackingTemplateManager /></>);
|
||||
await expandBeachTrip(user, 'Clothing');
|
||||
|
||||
await user.click(templateButtons('Beach Trip')[3]);
|
||||
expect(await screen.findByText('Failed to delete template')).toBeInTheDocument();
|
||||
expect(screen.getByText('Clothing')).toBeInTheDocument();
|
||||
|
||||
await user.click(templateButtons('Beach Trip')[3]);
|
||||
await screen.findByText('Template deleted');
|
||||
await waitFor(() => expect(screen.queryByText('Clothing')).not.toBeInTheDocument());
|
||||
expect(screen.getByText('No templates created yet')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-ADMIN-PKG-025: a blank rename closes the editor, a failing rename toasts, blur commits', async () => {
|
||||
const user = userEvent.setup();
|
||||
let puts = 0;
|
||||
server.use(
|
||||
http.get('/api/admin/packing-templates', () => HttpResponse.json({ templates: [tmpl1] })),
|
||||
http.put('/api/admin/packing-templates/1', () => {
|
||||
puts += 1;
|
||||
return HttpResponse.json({ error: 'nope' }, { status: 500 });
|
||||
}),
|
||||
);
|
||||
render(<><ToastContainer /><PackingTemplateManager /></>);
|
||||
await screen.findByText('Beach Trip');
|
||||
|
||||
await user.click(templateButtons('Beach Trip')[2]);
|
||||
await user.clear(screen.getByDisplayValue('Beach Trip'));
|
||||
await user.type(screen.getByRole('textbox'), '{Enter}');
|
||||
await waitFor(() => expect(screen.getByText('Beach Trip')).toBeInTheDocument());
|
||||
expect(puts).toBe(0);
|
||||
|
||||
// Blurring the field commits the pending name — here the request fails
|
||||
await user.click(templateButtons('Beach Trip')[2]);
|
||||
const input = screen.getByDisplayValue('Beach Trip');
|
||||
await user.clear(input);
|
||||
await user.type(input, 'Winter');
|
||||
await user.tab();
|
||||
expect(await screen.findByText('Failed to save')).toBeInTheDocument();
|
||||
expect(puts).toBe(1);
|
||||
});
|
||||
|
||||
it('FE-ADMIN-PKG-026: a blank category is not posted, a failing add toasts and X cancels', async () => {
|
||||
const user = userEvent.setup();
|
||||
let posts = 0;
|
||||
server.use(
|
||||
http.get('/api/admin/packing-templates', () => HttpResponse.json({ templates: [tmpl1] })),
|
||||
http.get('/api/admin/packing-templates/1', () => HttpResponse.json({ categories: [], items: [] })),
|
||||
http.post('/api/admin/packing-templates/1/categories', () => {
|
||||
posts += 1;
|
||||
return HttpResponse.json({ error: 'nope' }, { status: 500 });
|
||||
}),
|
||||
);
|
||||
render(<><ToastContainer /><PackingTemplateManager /></>);
|
||||
await expandBeachTrip(user, 'Add category');
|
||||
|
||||
await user.click(screen.getByText('Add category'));
|
||||
const catInput = screen.getByPlaceholderText('Category name (e.g. Clothing)');
|
||||
await user.type(catInput, ' {Enter}');
|
||||
expect(posts).toBe(0);
|
||||
|
||||
await user.clear(catInput);
|
||||
await user.type(catInput, 'Electronics{Enter}');
|
||||
expect(await screen.findByText('Failed to save')).toBeInTheDocument();
|
||||
|
||||
const cancel = within(catInput.parentElement as HTMLElement).getAllByRole('button')[1];
|
||||
await user.click(cancel);
|
||||
await waitFor(() =>
|
||||
expect(screen.queryByPlaceholderText('Category name (e.g. Clothing)')).not.toBeInTheDocument(),
|
||||
);
|
||||
});
|
||||
|
||||
it('FE-ADMIN-PKG-027: a blank category rename closes the editor and a failing rename toasts', async () => {
|
||||
const user = userEvent.setup();
|
||||
let puts = 0;
|
||||
server.use(
|
||||
http.get('/api/admin/packing-templates', () => HttpResponse.json({ templates: [tmpl1] })),
|
||||
http.get('/api/admin/packing-templates/1', () => HttpResponse.json({ categories: [cat1], items: [] })),
|
||||
http.put('/api/admin/packing-templates/1/categories/10', () => {
|
||||
puts += 1;
|
||||
return HttpResponse.json({ error: 'nope' }, { status: 500 });
|
||||
}),
|
||||
);
|
||||
render(<><ToastContainer /><PackingTemplateManager /></>);
|
||||
await expandBeachTrip(user, 'Clothing');
|
||||
|
||||
await user.click(categoryButtons('Clothing')[1]);
|
||||
await user.clear(screen.getByDisplayValue('Clothing'));
|
||||
await user.tab();
|
||||
await waitFor(() => expect(screen.getByText('Clothing')).toBeInTheDocument());
|
||||
expect(puts).toBe(0);
|
||||
|
||||
await user.click(categoryButtons('Clothing')[1]);
|
||||
const catInput = screen.getByDisplayValue('Clothing');
|
||||
await user.clear(catInput);
|
||||
await user.type(catInput, 'Shoes{Enter}');
|
||||
expect(await screen.findByText('Failed to save')).toBeInTheDocument();
|
||||
expect(puts).toBe(1);
|
||||
});
|
||||
|
||||
it('FE-ADMIN-PKG-028: a failing category delete toasts and keeps the category', async () => {
|
||||
const user = userEvent.setup();
|
||||
server.use(
|
||||
http.get('/api/admin/packing-templates', () => HttpResponse.json({ templates: [tmpl1] })),
|
||||
http.get('/api/admin/packing-templates/1', () => HttpResponse.json({ categories: [cat1], items: [item1] })),
|
||||
http.delete('/api/admin/packing-templates/1/categories/10', () => HttpResponse.error()),
|
||||
);
|
||||
render(<><ToastContainer /><PackingTemplateManager /></>);
|
||||
await expandBeachTrip(user, 'Clothing');
|
||||
|
||||
await user.click(categoryButtons('Clothing')[2]);
|
||||
|
||||
expect(await screen.findByText('Failed to delete category')).toBeInTheDocument();
|
||||
expect(screen.getByText('Clothing')).toBeInTheDocument();
|
||||
expect(screen.getByText('T-shirt')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-ADMIN-PKG-029: the add-item button posts the item, a failing add toasts and X closes the row', async () => {
|
||||
const user = userEvent.setup();
|
||||
let posts = 0;
|
||||
server.use(
|
||||
http.get('/api/admin/packing-templates', () => HttpResponse.json({ templates: [tmpl1] })),
|
||||
http.get('/api/admin/packing-templates/1', () => HttpResponse.json({ categories: [cat1], items: [] })),
|
||||
http.post('/api/admin/packing-templates/1/categories/10/items', () => {
|
||||
posts += 1;
|
||||
return posts === 1
|
||||
? HttpResponse.json({ item: { id: 102, category_id: 10, name: 'Sandals', sort_order: 0 } })
|
||||
: HttpResponse.json({ error: 'nope' }, { status: 500 });
|
||||
}),
|
||||
);
|
||||
render(<><ToastContainer /><PackingTemplateManager /></>);
|
||||
await expandBeachTrip(user, 'Clothing');
|
||||
|
||||
await user.click(categoryButtons('Clothing')[0]);
|
||||
const itemInput = screen.getByPlaceholderText('Item name');
|
||||
const addRow = itemInput.parentElement as HTMLElement;
|
||||
expect(within(addRow).getAllByRole('button')[0]).toBeDisabled();
|
||||
|
||||
await user.type(itemInput, 'Sandals');
|
||||
await user.click(within(addRow).getAllByRole('button')[0]);
|
||||
await screen.findByText('Sandals');
|
||||
|
||||
await user.type(screen.getByPlaceholderText('Item name'), 'Towel');
|
||||
await user.click(within(addRow).getAllByRole('button')[0]);
|
||||
expect(await screen.findByText('Failed to save')).toBeInTheDocument();
|
||||
|
||||
await user.click(within(addRow).getAllByRole('button')[1]);
|
||||
await waitFor(() => expect(screen.queryByPlaceholderText('Item name')).not.toBeInTheDocument());
|
||||
});
|
||||
|
||||
it('FE-ADMIN-PKG-030: the item editor commits on the check button, cancels on X and ignores a blank name', async () => {
|
||||
const user = userEvent.setup();
|
||||
let puts = 0;
|
||||
server.use(
|
||||
http.get('/api/admin/packing-templates', () => HttpResponse.json({ templates: [tmpl1] })),
|
||||
http.get('/api/admin/packing-templates/1', () => HttpResponse.json({ categories: [cat1], items: [item1] })),
|
||||
http.put('/api/admin/packing-templates/1/items/100', () => {
|
||||
puts += 1;
|
||||
return puts === 1
|
||||
? HttpResponse.json({ success: true })
|
||||
: HttpResponse.json({ error: 'nope' }, { status: 500 });
|
||||
}),
|
||||
);
|
||||
render(<><ToastContainer /><PackingTemplateManager /></>);
|
||||
await expandBeachTrip(user, 'T-shirt');
|
||||
|
||||
// A blank name just closes the editor
|
||||
await user.click(itemButtons('T-shirt')[0]);
|
||||
const blank = screen.getByDisplayValue('T-shirt');
|
||||
await user.clear(blank);
|
||||
await user.click(within(blank.parentElement as HTMLElement).getAllByRole('button')[0]);
|
||||
await waitFor(() => expect(screen.getByText('T-shirt')).toBeInTheDocument());
|
||||
expect(puts).toBe(0);
|
||||
|
||||
// X discards the pending name
|
||||
await user.click(itemButtons('T-shirt')[0]);
|
||||
const editing = screen.getByDisplayValue('T-shirt');
|
||||
await user.clear(editing);
|
||||
await user.type(editing, 'Discarded');
|
||||
await user.click(within(editing.parentElement as HTMLElement).getAllByRole('button')[1]);
|
||||
await waitFor(() => expect(screen.getByText('T-shirt')).toBeInTheDocument());
|
||||
expect(puts).toBe(0);
|
||||
|
||||
// The check button commits
|
||||
await user.click(itemButtons('T-shirt')[0]);
|
||||
const editing2 = screen.getByDisplayValue('T-shirt');
|
||||
await user.clear(editing2);
|
||||
await user.type(editing2, 'Tank Top');
|
||||
await user.click(within(editing2.parentElement as HTMLElement).getAllByRole('button')[0]);
|
||||
await screen.findByText('Tank Top');
|
||||
expect(puts).toBe(1);
|
||||
|
||||
// A failing rename keeps the editor open and toasts
|
||||
await user.click(itemButtons('Tank Top')[0]);
|
||||
const editing3 = screen.getByDisplayValue('Tank Top');
|
||||
await user.clear(editing3);
|
||||
await user.type(editing3, 'Vest{Enter}');
|
||||
expect(await screen.findByText('Failed to save')).toBeInTheDocument();
|
||||
expect(screen.getByDisplayValue('Vest')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-ADMIN-PKG-031: a failing item delete toasts and keeps the item', async () => {
|
||||
const user = userEvent.setup();
|
||||
server.use(
|
||||
http.get('/api/admin/packing-templates', () => HttpResponse.json({ templates: [tmpl1] })),
|
||||
http.get('/api/admin/packing-templates/1', () => HttpResponse.json({ categories: [cat1], items: [item1] })),
|
||||
http.delete('/api/admin/packing-templates/1/items/100', () => HttpResponse.error()),
|
||||
);
|
||||
render(<><ToastContainer /><PackingTemplateManager /></>);
|
||||
await expandBeachTrip(user, 'T-shirt');
|
||||
|
||||
await user.click(itemButtons('T-shirt')[1]);
|
||||
|
||||
expect(await screen.findByText('Failed to delete item')).toBeInTheDocument();
|
||||
expect(screen.getByText('T-shirt')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-ADMIN-PKG-032: the chevron button expands and collapses the template', async () => {
|
||||
const user = userEvent.setup();
|
||||
server.use(
|
||||
http.get('/api/admin/packing-templates', () => HttpResponse.json({ templates: [tmpl1] })),
|
||||
http.get('/api/admin/packing-templates/1', () => HttpResponse.json({ categories: [cat1], items: [item1] })),
|
||||
);
|
||||
render(<PackingTemplateManager />);
|
||||
await screen.findByText('Beach Trip');
|
||||
|
||||
await user.click(templateButtons('Beach Trip')[0]);
|
||||
await screen.findByText('Clothing');
|
||||
|
||||
await user.click(templateButtons('Beach Trip')[0]);
|
||||
await waitFor(() => expect(screen.queryByText('Clothing')).not.toBeInTheDocument());
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user