mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-20 05:56:53 +00:00
678 lines
27 KiB
TypeScript
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}');
|
|
|
|
await screen.findByText('Failed to create template');
|
|
});
|
|
|
|
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}');
|
|
|
|
await screen.findByText('Failed to save');
|
|
});
|
|
|
|
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 template');
|
|
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}');
|
|
|
|
await screen.findByText('Failed to save');
|
|
});
|
|
|
|
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 template');
|
|
expect(screen.getByText('Shorts')).toBeInTheDocument();
|
|
});
|
|
});
|