mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-11-13 15:51:00 +00:00
31859651d7
Every help screen checked against the v5 redesign. Tours is documented for the first time, as a screen of its own with twelve screens under it, and notifications get help of their own. All pictures are taken again on a realistic demo trip, with short loops where a still cannot show the motion. The English is plain and every language is written naturally rather than word for word. The switcher shows two levels at most, a screen's overview lists its sub-screens as cards above its guides, each wiki page is linked once, and the help sidebar is wider. Fixes found along the way: Save to Collection keeps a tour's route, a track dropped on the places column opens the Tours import, tour Share on desktop, the unsaved tour draft lives on the account, a route click no longer adds an end point, Trip places leaves the trip's tours out, Escape closes a stage's day menu, confirms before destructive deletes, DWD warnings sharing one id kept apart, a booking's date moves with it, and smaller fixes across notifications, bookings, the day plan, stays, Atlas, Collections, Vacay and the admin screens.
775 lines
32 KiB
TypeScript
775 lines
32 KiB
TypeScript
// 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';
|
|
import { resetAllStores } from '../../../tests/helpers/store';
|
|
import PackingTemplateManager from './PackingTemplateManager';
|
|
import { ToastContainer } from '../shared/Toast';
|
|
|
|
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 }
|
|
|
|
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, '[data-row="template"]');
|
|
const categoryButtons = (name: string) => rowButtons(name, '[data-row="category"]');
|
|
const itemButtons = (name: string) => rowButtons(name, '[data-row="item"]');
|
|
|
|
/** Answers the confirm dialog a template or category delete opens; its button is the last 'Delete' on the page. */
|
|
async function confirmDelete(user: ReturnType<typeof userEvent.setup>) {
|
|
const buttons = await screen.findAllByRole('button', { name: 'Delete' });
|
|
await user.click(buttons[buttons.length - 1]);
|
|
}
|
|
|
|
/** 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(
|
|
http.get('/api/admin/packing-templates', async () => {
|
|
await new Promise(r => setTimeout(r, 100));
|
|
return HttpResponse.json({ templates: [] });
|
|
})
|
|
);
|
|
render(<PackingTemplateManager />);
|
|
expect(document.querySelector('.animate-spin')).toBeInTheDocument();
|
|
});
|
|
|
|
it('FE-ADMIN-PKG-002: shows empty state when no templates', async () => {
|
|
render(<PackingTemplateManager />);
|
|
await screen.findByText('No templates created yet');
|
|
expect(screen.queryAllByRole('button', { name: /chevron/i })).toHaveLength(0);
|
|
});
|
|
|
|
it('FE-ADMIN-PKG-003: template list renders names and counts', async () => {
|
|
server.use(
|
|
http.get('/api/admin/packing-templates', () =>
|
|
HttpResponse.json({ templates: [tmpl1, tmpl2] })
|
|
)
|
|
);
|
|
render(<PackingTemplateManager />);
|
|
await screen.findByText('Beach Trip');
|
|
expect(screen.getByText('City Break')).toBeInTheDocument();
|
|
// tmpl1 has 2 categories and 5 items
|
|
expect(screen.getByText(/2 categories, 5 items/i)).toBeInTheDocument();
|
|
});
|
|
|
|
it('FE-ADMIN-PKG-004: clicking "+" shows create input', async () => {
|
|
const user = userEvent.setup();
|
|
render(<PackingTemplateManager />);
|
|
await screen.findByText('No templates created yet');
|
|
const createBtn = screen.getByRole('button', { name: /new template/i });
|
|
await user.click(createBtn);
|
|
expect(screen.getByPlaceholderText('Template name (e.g. Beach Holiday)')).toBeInTheDocument();
|
|
});
|
|
|
|
it('FE-ADMIN-PKG-005: creates template on Enter and shows success toast', async () => {
|
|
const user = userEvent.setup();
|
|
let postCalled = false;
|
|
server.use(
|
|
http.post('/api/admin/packing-templates', async () => {
|
|
postCalled = true;
|
|
return HttpResponse.json({ template: { id: 99, name: 'New Template' } });
|
|
})
|
|
);
|
|
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, 'New Template{Enter}');
|
|
await waitFor(() => expect(postCalled).toBe(true));
|
|
// "New Template" may appear both as the button label and the new list item
|
|
await waitFor(() => expect(screen.getAllByText('New Template').length).toBeGreaterThanOrEqual(1));
|
|
await screen.findByText('Template created');
|
|
});
|
|
|
|
it('FE-ADMIN-PKG-006: Escape dismisses create input without API call', async () => {
|
|
const user = userEvent.setup();
|
|
let postCalled = false;
|
|
server.use(
|
|
http.post('/api/admin/packing-templates', async () => {
|
|
postCalled = true;
|
|
return HttpResponse.json({ template: { id: 99, name: 'Should Not Appear' } });
|
|
})
|
|
);
|
|
render(<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, 'Test{Escape}');
|
|
await waitFor(() => {
|
|
expect(screen.queryByPlaceholderText('Template name (e.g. Beach Holiday)')).not.toBeInTheDocument();
|
|
});
|
|
expect(postCalled).toBe(false);
|
|
});
|
|
|
|
it('FE-ADMIN-PKG-007: expanding a template loads and displays its categories and items', 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, item2] })
|
|
)
|
|
);
|
|
render(<PackingTemplateManager />);
|
|
await screen.findByText('Beach Trip');
|
|
await user.click(screen.getByText('Beach Trip'));
|
|
await screen.findByText('Clothing');
|
|
expect(screen.getByText('T-shirt')).toBeInTheDocument();
|
|
expect(screen.getByText('Shorts')).toBeInTheDocument();
|
|
});
|
|
|
|
it('FE-ADMIN-PKG-008: collapsing an expanded template hides its 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.json({ categories: [cat1], items: [item1, item2] })
|
|
)
|
|
);
|
|
render(<PackingTemplateManager />);
|
|
await screen.findByText('Beach Trip');
|
|
await user.click(screen.getByText('Beach Trip'));
|
|
await screen.findByText('Clothing');
|
|
// Collapse by clicking again
|
|
await user.click(screen.getByText('Beach Trip'));
|
|
await waitFor(() => {
|
|
expect(screen.queryByText('Clothing')).not.toBeInTheDocument();
|
|
expect(screen.queryByText('T-shirt')).not.toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
it('FE-ADMIN-PKG-009: deleting a template removes it from the list and shows toast', async () => {
|
|
const user = userEvent.setup();
|
|
let deleteCalled = false;
|
|
server.use(
|
|
http.get('/api/admin/packing-templates', () =>
|
|
HttpResponse.json({ templates: [tmpl1, tmpl2] })
|
|
),
|
|
http.delete('/api/admin/packing-templates/1', () => {
|
|
deleteCalled = true;
|
|
return HttpResponse.json({ success: true });
|
|
})
|
|
);
|
|
render(<><ToastContainer /><PackingTemplateManager /></>);
|
|
await screen.findByText('Beach Trip');
|
|
expect(screen.getByText('City Break')).toBeInTheDocument();
|
|
|
|
// Template rows carry [chevron, name, edit, delete]
|
|
await user.click(templateButtons('Beach Trip')[3]);
|
|
expect(deleteCalled).toBe(false);
|
|
expect(screen.getByText('Delete the template "Beach Trip" with all its categories and items?')).toBeInTheDocument();
|
|
await confirmDelete(user);
|
|
await waitFor(() => expect(deleteCalled).toBe(true));
|
|
await waitFor(() => expect(screen.queryByText('Beach Trip')).not.toBeInTheDocument());
|
|
expect(screen.getByText('City Break')).toBeInTheDocument();
|
|
await screen.findByText('Template deleted');
|
|
});
|
|
|
|
it('FE-ADMIN-PKG-010: renaming a template inline updates the list', async () => {
|
|
const user = userEvent.setup();
|
|
let putCalled = false;
|
|
server.use(
|
|
http.get('/api/admin/packing-templates', () =>
|
|
HttpResponse.json({ templates: [tmpl1] })
|
|
),
|
|
http.put('/api/admin/packing-templates/1', async () => {
|
|
putCalled = true;
|
|
return HttpResponse.json({ success: true });
|
|
})
|
|
);
|
|
render(<PackingTemplateManager />);
|
|
await screen.findByText('Beach Trip');
|
|
|
|
await user.click(templateButtons('Beach Trip')[2]);
|
|
|
|
const input = screen.getByDisplayValue('Beach Trip');
|
|
await user.clear(input);
|
|
await user.type(input, 'Summer Packing{Enter}');
|
|
await waitFor(() => expect(putCalled).toBe(true));
|
|
await screen.findByText('Summer Packing');
|
|
});
|
|
|
|
it('FE-ADMIN-PKG-011: adding a category to an expanded 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: [], items: [] })
|
|
),
|
|
http.post('/api/admin/packing-templates/1/categories', async () =>
|
|
HttpResponse.json({ category: { id: 20, template_id: 1, name: 'Electronics', sort_order: 1 } })
|
|
)
|
|
);
|
|
render(<PackingTemplateManager />);
|
|
await screen.findByText('Beach Trip');
|
|
await user.click(screen.getByText('Beach Trip'));
|
|
// Wait for expanded state (Add category button should appear)
|
|
await screen.findByText('Add category');
|
|
await user.click(screen.getByText('Add category'));
|
|
const catInput = screen.getByPlaceholderText('Category name (e.g. Clothing)');
|
|
await user.type(catInput, 'Electronics{Enter}');
|
|
expect(await screen.findByText('Electronics')).toBeInTheDocument();
|
|
});
|
|
|
|
it('FE-ADMIN-PKG-012: adding an item to a 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: [] })
|
|
),
|
|
http.post('/api/admin/packing-templates/1/categories/10/items', async () =>
|
|
HttpResponse.json({ item: { id: 102, category_id: 10, name: 'Sandals', sort_order: 2 } })
|
|
)
|
|
);
|
|
render(<PackingTemplateManager />);
|
|
await screen.findByText('Beach Trip');
|
|
await user.click(screen.getByText('Beach Trip'));
|
|
await screen.findByText('Clothing');
|
|
|
|
// Click the "+" button on the Clothing category row
|
|
const clothingHeader = screen.getByText('Clothing').closest('div')!;
|
|
const addItemBtn = clothingHeader.querySelector('button') as HTMLElement;
|
|
await user.click(addItemBtn);
|
|
|
|
const itemInput = screen.getByPlaceholderText('Item name');
|
|
await user.type(itemInput, 'Sandals');
|
|
// Submit via Enter key (the input's onKeyDown handler triggers handleAddItem)
|
|
await user.type(itemInput, '{Enter}');
|
|
expect(await screen.findByText('Sandals')).toBeInTheDocument();
|
|
});
|
|
|
|
it('FE-ADMIN-PKG-013: renaming a category inline updates its name', 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: [] })
|
|
),
|
|
http.put('/api/admin/packing-templates/1/categories/10', async () =>
|
|
HttpResponse.json({ success: true })
|
|
)
|
|
);
|
|
render(<PackingTemplateManager />);
|
|
await screen.findByText('Beach Trip');
|
|
await user.click(screen.getByText('Beach Trip'));
|
|
await screen.findByText('Clothing');
|
|
|
|
// Category headers carry [add item, edit, delete]
|
|
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('Shoes')).toBeInTheDocument();
|
|
});
|
|
|
|
it('FE-ADMIN-PKG-014: deleting a category removes it and its items', 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, item2] })
|
|
),
|
|
http.delete('/api/admin/packing-templates/1/categories/10', () =>
|
|
HttpResponse.json({ success: true })
|
|
)
|
|
);
|
|
render(<PackingTemplateManager />);
|
|
await screen.findByText('Beach Trip');
|
|
await user.click(screen.getByText('Beach Trip'));
|
|
await screen.findByText('Clothing');
|
|
expect(screen.getByText('T-shirt')).toBeInTheDocument();
|
|
|
|
await user.click(categoryButtons('Clothing')[2]);
|
|
expect(screen.getByText('T-shirt')).toBeInTheDocument();
|
|
await confirmDelete(user);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.queryByText('Clothing')).not.toBeInTheDocument();
|
|
expect(screen.queryByText('T-shirt')).not.toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
it('FE-ADMIN-PKG-015: renaming an item inline updates its name', 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.put('/api/admin/packing-templates/1/items/100', async () =>
|
|
HttpResponse.json({ success: true })
|
|
)
|
|
);
|
|
render(<PackingTemplateManager />);
|
|
await screen.findByText('Beach Trip');
|
|
await user.click(screen.getByText('Beach Trip'));
|
|
await screen.findByText('T-shirt');
|
|
|
|
// Item rows carry [edit, delete]
|
|
await user.click(itemButtons('T-shirt')[0]);
|
|
|
|
const input = screen.getByDisplayValue('T-shirt');
|
|
await user.clear(input);
|
|
await user.type(input, 'Tank Top{Enter}');
|
|
expect(await screen.findByText('Tank Top')).toBeInTheDocument();
|
|
});
|
|
|
|
it('FE-ADMIN-PKG-016: deleting an item removes it from the list', 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, item2] })
|
|
),
|
|
http.delete('/api/admin/packing-templates/1/items/100', () =>
|
|
HttpResponse.json({ success: true })
|
|
)
|
|
);
|
|
render(<PackingTemplateManager />);
|
|
await screen.findByText('Beach Trip');
|
|
await user.click(screen.getByText('Beach Trip'));
|
|
await screen.findByText('T-shirt');
|
|
expect(screen.getByText('Shorts')).toBeInTheDocument();
|
|
|
|
await user.click(itemButtons('T-shirt')[1]);
|
|
|
|
await waitFor(() => expect(screen.queryByText('T-shirt')).not.toBeInTheDocument());
|
|
expect(screen.getByText('Shorts')).toBeInTheDocument();
|
|
});
|
|
|
|
it('FE-ADMIN-PKG-017: Escape cancels add category without saving', async () => {
|
|
const user = userEvent.setup();
|
|
let postCalled = false;
|
|
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', async () => {
|
|
postCalled = true;
|
|
return HttpResponse.json({ category: { id: 20, template_id: 1, name: 'Ignored', sort_order: 1 } });
|
|
})
|
|
);
|
|
render(<PackingTemplateManager />);
|
|
await screen.findByText('Beach Trip');
|
|
await user.click(screen.getByText('Beach Trip'));
|
|
await screen.findByText('Add category');
|
|
await user.click(screen.getByText('Add category'));
|
|
const catInput = screen.getByPlaceholderText('Category name (e.g. Clothing)');
|
|
await user.type(catInput, 'Test{Escape}');
|
|
await waitFor(() =>
|
|
expect(screen.queryByPlaceholderText('Category name (e.g. Clothing)')).not.toBeInTheDocument()
|
|
);
|
|
expect(postCalled).toBe(false);
|
|
});
|
|
|
|
it('FE-ADMIN-PKG-018: Escape cancels add item without saving', async () => {
|
|
const user = userEvent.setup();
|
|
let postCalled = false;
|
|
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', async () => {
|
|
postCalled = true;
|
|
return HttpResponse.json({ item: { id: 102, category_id: 10, name: 'Ignored', sort_order: 2 } });
|
|
})
|
|
);
|
|
render(<PackingTemplateManager />);
|
|
await screen.findByText('Beach Trip');
|
|
await user.click(screen.getByText('Beach Trip'));
|
|
await screen.findByText('Clothing');
|
|
|
|
const clothingHeader = screen.getByText('Clothing').closest('div')!;
|
|
const addItemBtn = clothingHeader.querySelector('button') as HTMLElement;
|
|
await user.click(addItemBtn);
|
|
|
|
const itemInput = screen.getByPlaceholderText('Item name');
|
|
await user.type(itemInput, 'Test{Escape}');
|
|
await waitFor(() =>
|
|
expect(screen.queryByPlaceholderText('Item name')).not.toBeInTheDocument()
|
|
);
|
|
expect(postCalled).toBe(false);
|
|
});
|
|
|
|
it('FE-ADMIN-PKG-019: Escape cancels template rename without saving', async () => {
|
|
const user = userEvent.setup();
|
|
let putCalled = false;
|
|
server.use(
|
|
http.get('/api/admin/packing-templates', () =>
|
|
HttpResponse.json({ templates: [tmpl1] })
|
|
),
|
|
http.put('/api/admin/packing-templates/1', async () => {
|
|
putCalled = true;
|
|
return HttpResponse.json({ success: true });
|
|
})
|
|
);
|
|
render(<PackingTemplateManager />);
|
|
await screen.findByText('Beach Trip');
|
|
|
|
await user.click(templateButtons('Beach Trip')[2]);
|
|
|
|
const input = screen.getByDisplayValue('Beach Trip');
|
|
await user.type(input, '{Escape}');
|
|
await waitFor(() => expect(screen.queryByDisplayValue('Beach Trip')).not.toBeInTheDocument());
|
|
expect(putCalled).toBe(false);
|
|
// Original name should be restored
|
|
expect(screen.getByText('Beach Trip')).toBeInTheDocument();
|
|
});
|
|
|
|
it('FE-ADMIN-PKG-020: X button on create template input dismisses it', async () => {
|
|
const user = userEvent.setup();
|
|
render(<PackingTemplateManager />);
|
|
await screen.findByText('No templates created yet');
|
|
await user.click(screen.getByRole('button', { name: /new template/i }));
|
|
expect(screen.getByPlaceholderText('Template name (e.g. Beach Holiday)')).toBeInTheDocument();
|
|
|
|
// Find the X (cancel) button in the create row — it's the last button in the create row
|
|
const createRow = screen.getByPlaceholderText('Template name (e.g. Beach Holiday)').closest('div')!;
|
|
const createRowButtons = Array.from(createRow.querySelectorAll('button'));
|
|
const cancelBtn = createRowButtons[createRowButtons.length - 1] as HTMLElement;
|
|
await user.click(cancelBtn);
|
|
|
|
await waitFor(() =>
|
|
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]);
|
|
await confirmDelete(user);
|
|
expect(await screen.findByText('Failed to delete template')).toBeInTheDocument();
|
|
expect(screen.getByText('Clothing')).toBeInTheDocument();
|
|
|
|
await user.click(templateButtons('Beach Trip')[3]);
|
|
await confirmDelete(user);
|
|
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]);
|
|
await confirmDelete(user);
|
|
|
|
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());
|
|
});
|
|
});
|