mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-18 21:16:47 +00:00
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.
This commit is contained in:
@@ -98,7 +98,7 @@ describe('AddonManager', () => {
|
||||
|
||||
it('FE-ADMIN-ADDON-002: empty state when addons list is empty', async () => {
|
||||
render(<AddonManager />);
|
||||
await screen.findByText('No addons available');
|
||||
expect(await screen.findByText('No addons available')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-ADMIN-ADDON-003: trip addons section renders with correct section header', async () => {
|
||||
|
||||
@@ -49,7 +49,7 @@ describe('AdminMcpTokensPanel', () => {
|
||||
|
||||
it('FE-ADMIN-MCP-002: empty state rendered when no tokens', async () => {
|
||||
render(<AdminMcpTokensPanel />);
|
||||
await screen.findByText('No MCP tokens have been created yet');
|
||||
expect(await screen.findByText('No MCP tokens have been created yet')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-ADMIN-MCP-003: token list renders correctly', async () => {
|
||||
@@ -195,7 +195,7 @@ describe('AdminMcpTokensPanel', () => {
|
||||
)
|
||||
);
|
||||
render(<><ToastContainer /><AdminMcpTokensPanel /></>);
|
||||
await screen.findByText('Failed to load tokens');
|
||||
expect(await screen.findByText('Failed to load tokens')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-ADMIN-MCP-011: OAuth sessions loading spinner shown on mount', async () => {
|
||||
@@ -216,7 +216,7 @@ describe('AdminMcpTokensPanel', () => {
|
||||
)
|
||||
);
|
||||
render(<AdminMcpTokensPanel />);
|
||||
await screen.findByText('No active OAuth sessions');
|
||||
expect(await screen.findByText('No active OAuth sessions')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-ADMIN-MCP-013: OAuth sessions list renders with scopes', async () => {
|
||||
@@ -318,6 +318,6 @@ describe('AdminMcpTokensPanel', () => {
|
||||
const deleteBtns = screen.getAllByRole('button', { name: 'Delete' });
|
||||
const confirmBtn = deleteBtns.find(b => !b.title);
|
||||
await user.click(confirmBtn ?? deleteBtns[deleteBtns.length - 1]);
|
||||
await screen.findByText('Failed to revoke session');
|
||||
expect(await screen.findByText('Failed to revoke session')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -27,17 +27,17 @@ describe('CategoryManager', () => {
|
||||
|
||||
it('FE-COMP-CAT-002: shows Categories title', async () => {
|
||||
render(<CategoryManager />);
|
||||
await screen.findByText('Categories');
|
||||
expect(await screen.findByText('Categories')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-COMP-CAT-003: shows empty state when no categories', async () => {
|
||||
render(<CategoryManager />);
|
||||
await screen.findByText('No categories yet');
|
||||
expect(await screen.findByText('No categories yet')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-COMP-CAT-004: shows New Category button', async () => {
|
||||
render(<CategoryManager />);
|
||||
await screen.findByText('New Category');
|
||||
expect(await screen.findByText('New Category')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-COMP-CAT-005: clicking New Category shows form', async () => {
|
||||
@@ -129,7 +129,7 @@ describe('CategoryManager', () => {
|
||||
|
||||
it('FE-COMP-CAT-010: shows subtitle text', async () => {
|
||||
render(<CategoryManager />);
|
||||
await screen.findByText('Manage categories for places');
|
||||
expect(await screen.findByText('Manage categories for places')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-COMP-CAT-011: category count is shown', async () => {
|
||||
|
||||
@@ -87,7 +87,7 @@ describe('DevNotificationsPanel', () => {
|
||||
render(<><ToastContainer /><DevNotificationsPanel /></>);
|
||||
await screen.findByText('Type Testing');
|
||||
await user.click(screen.getByText('Simple → Me').closest('button')!);
|
||||
await screen.findByText('Sent: simple-me');
|
||||
expect(await screen.findByText('Sent: simple-me')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-ADMIN-DEVNOTIF-007: all buttons disabled while a send is in-flight', async () => {
|
||||
@@ -120,7 +120,7 @@ describe('DevNotificationsPanel', () => {
|
||||
render(<><ToastContainer /><DevNotificationsPanel /></>);
|
||||
await screen.findByText('Type Testing');
|
||||
await user.click(screen.getByText('Simple → Me').closest('button')!);
|
||||
await screen.findByText('No channel configured');
|
||||
expect(await screen.findByText('No channel configured')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-ADMIN-DEVNOTIF-008b: a failure without an error field falls back to the generic text', async () => {
|
||||
@@ -133,7 +133,7 @@ describe('DevNotificationsPanel', () => {
|
||||
render(<><ToastContainer /><DevNotificationsPanel /></>);
|
||||
await screen.findByText('Type Testing');
|
||||
await user.click(screen.getByText('Simple → Me').closest('button')!);
|
||||
await screen.findByText('Failed');
|
||||
expect(await screen.findByText('Failed')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-ADMIN-DEVNOTIF-009: changing trip selector updates payload targetId', async () => {
|
||||
|
||||
@@ -231,7 +231,7 @@ describe('GitHubPanel', () => {
|
||||
render(<GitHubPanel />);
|
||||
await screen.findByText('v1.7.0');
|
||||
await user.click(screen.getByText('Show details'));
|
||||
await screen.findByText('This is a plain paragraph without any markdown syntax.');
|
||||
expect(await screen.findByText('This is a plain paragraph without any markdown syntax.')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-ADMIN-GH-014: markdown link with safe href renders as anchor', async () => {
|
||||
|
||||
@@ -250,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 () => {
|
||||
@@ -280,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 () => {
|
||||
@@ -312,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 () => {
|
||||
@@ -379,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 () => {
|
||||
|
||||
@@ -269,6 +269,6 @@ describe('PermissionsPanel', () => {
|
||||
),
|
||||
);
|
||||
renderPanel();
|
||||
await screen.findByText('Error');
|
||||
expect(await screen.findByText('Error')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -32,7 +32,7 @@ describe('BudgetPanel', () => {
|
||||
http.get('/api/trips/1/budget', () => HttpResponse.json({ items: [] }))
|
||||
);
|
||||
render(<BudgetPanel tripId={1} />);
|
||||
await screen.findByText('No budget created yet');
|
||||
expect(await screen.findByText('No budget created yet')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-COMP-BUDGET-002: shows empty state text body', async () => {
|
||||
@@ -40,7 +40,7 @@ describe('BudgetPanel', () => {
|
||||
http.get('/api/trips/1/budget', () => HttpResponse.json({ items: [] }))
|
||||
);
|
||||
render(<BudgetPanel tripId={1} />);
|
||||
await screen.findByText(/Create categories and entries/i);
|
||||
expect(await screen.findByText(/Create categories and entries/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-COMP-BUDGET-003: shows category input in empty state when user can edit', async () => {
|
||||
@@ -48,7 +48,7 @@ describe('BudgetPanel', () => {
|
||||
http.get('/api/trips/1/budget', () => HttpResponse.json({ items: [] }))
|
||||
);
|
||||
render(<BudgetPanel tripId={1} />);
|
||||
await screen.findByPlaceholderText('Enter category name...');
|
||||
expect(await screen.findByPlaceholderText('Enter category name...')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-COMP-BUDGET-004: renders budget items from store after load', async () => {
|
||||
@@ -57,7 +57,7 @@ describe('BudgetPanel', () => {
|
||||
http.get('/api/trips/1/budget', () => HttpResponse.json({ items: [item] }))
|
||||
);
|
||||
render(<BudgetPanel tripId={1} />);
|
||||
await screen.findByText('Hotel Paris');
|
||||
expect(await screen.findByText('Hotel Paris')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-COMP-BUDGET-005: renders category section header', async () => {
|
||||
@@ -87,7 +87,7 @@ describe('BudgetPanel', () => {
|
||||
http.get('/api/trips/1/budget', () => HttpResponse.json({ items: [item] }))
|
||||
);
|
||||
render(<BudgetPanel tripId={1} />);
|
||||
await screen.findByText('Budget');
|
||||
expect(await screen.findByText('Budget')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-COMP-BUDGET-008: shows CSV export button', async () => {
|
||||
@@ -96,7 +96,7 @@ describe('BudgetPanel', () => {
|
||||
http.get('/api/trips/1/budget', () => HttpResponse.json({ items: [item] }))
|
||||
);
|
||||
render(<BudgetPanel tripId={1} />);
|
||||
await screen.findByText('CSV');
|
||||
expect(await screen.findByText('CSV')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-COMP-BUDGET-009: add item row visible in table', async () => {
|
||||
@@ -105,7 +105,7 @@ describe('BudgetPanel', () => {
|
||||
http.get('/api/trips/1/budget', () => HttpResponse.json({ items: [item] }))
|
||||
);
|
||||
render(<BudgetPanel tripId={1} />);
|
||||
await screen.findByPlaceholderText('New Entry');
|
||||
expect(await screen.findByPlaceholderText('New Entry')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-COMP-BUDGET-010: adding new item via form calls POST and shows item', async () => {
|
||||
@@ -124,7 +124,7 @@ describe('BudgetPanel', () => {
|
||||
await user.type(nameInput, 'Restaurant Dinner');
|
||||
const addBtn = screen.getByTitle('Add Reservation');
|
||||
await user.click(addBtn);
|
||||
await screen.findByText('Restaurant Dinner');
|
||||
expect(await screen.findByText('Restaurant Dinner')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-COMP-BUDGET-011: delete button present for items when user can edit', async () => {
|
||||
@@ -161,7 +161,7 @@ describe('BudgetPanel', () => {
|
||||
);
|
||||
render(<BudgetPanel tripId={1} />);
|
||||
await screen.findByText('Hotel A');
|
||||
await screen.findByText('Hotel B');
|
||||
expect(await screen.findByText('Hotel B')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-COMP-BUDGET-014: items from different categories render separate sections', async () => {
|
||||
@@ -183,7 +183,7 @@ describe('BudgetPanel', () => {
|
||||
);
|
||||
render(<BudgetPanel tripId={1} />);
|
||||
// Component renders even in empty state
|
||||
await screen.findByText('No budget created yet');
|
||||
expect(await screen.findByText('No budget created yet')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-COMP-BUDGET-016: trip currency EUR is shown in header for item rows', async () => {
|
||||
@@ -193,7 +193,7 @@ describe('BudgetPanel', () => {
|
||||
http.get('/api/trips/1/budget', () => HttpResponse.json({ items: [item] }))
|
||||
);
|
||||
render(<BudgetPanel tripId={1} />);
|
||||
await screen.findByText('Misc');
|
||||
expect(await screen.findByText('Misc')).toBeInTheDocument();
|
||||
// Row exists - EUR formatting would appear in values
|
||||
});
|
||||
|
||||
@@ -233,7 +233,7 @@ describe('BudgetPanel', () => {
|
||||
render(<BudgetPanel tripId={1} />);
|
||||
const nameInput = await screen.findByPlaceholderText('New Entry');
|
||||
await user.type(nameInput, 'Pizza{Enter}');
|
||||
await screen.findByText('Pizza');
|
||||
expect(await screen.findByText('Pizza')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-COMP-BUDGET-020: component renders without crashing with empty tripMembers', async () => {
|
||||
@@ -241,7 +241,7 @@ describe('BudgetPanel', () => {
|
||||
http.get('/api/trips/1/budget', () => HttpResponse.json({ items: [] }))
|
||||
);
|
||||
render(<BudgetPanel tripId={1} tripMembers={[]} />);
|
||||
await screen.findByText('No budget created yet');
|
||||
expect(await screen.findByText('No budget created yet')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-COMP-BUDGET-021: inline edit name cell — clicking a name cell makes it editable', async () => {
|
||||
@@ -342,7 +342,7 @@ describe('BudgetPanel', () => {
|
||||
http.get('/api/trips/1/budget', () => HttpResponse.json({ items: [] }))
|
||||
);
|
||||
render(<BudgetPanel tripId={1} />);
|
||||
await screen.findByPlaceholderText('Enter category name...');
|
||||
expect(await screen.findByPlaceholderText('Enter category name...')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-COMP-BUDGET-028: creating a new category via input calls POST and adds a section', async () => {
|
||||
@@ -356,7 +356,7 @@ describe('BudgetPanel', () => {
|
||||
render(<BudgetPanel tripId={1} />);
|
||||
const input = await screen.findByPlaceholderText('Enter category name...');
|
||||
await user.type(input, 'Souvenirs{Enter}');
|
||||
await screen.findByText('Souvenirs');
|
||||
expect(await screen.findByText('Souvenirs')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-COMP-BUDGET-029: settlement section renders flows with usernames', async () => {
|
||||
@@ -387,7 +387,7 @@ describe('BudgetPanel', () => {
|
||||
await user.click(settlementBtn);
|
||||
// alice and bob should appear in balances section
|
||||
await screen.findByText('alice');
|
||||
await screen.findByText('bob');
|
||||
expect(await screen.findByText('bob')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-COMP-BUDGET-030: per-person summary renders usernames', async () => {
|
||||
@@ -408,7 +408,7 @@ describe('BudgetPanel', () => {
|
||||
];
|
||||
render(<BudgetPanel tripId={1} tripMembers={tripMembers} />);
|
||||
await screen.findByText('Shared Dinner');
|
||||
await screen.findByText('testuser');
|
||||
expect(await screen.findByText('testuser')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-COMP-BUDGET-032: grand total row shows sum across all categories', async () => {
|
||||
@@ -451,7 +451,7 @@ describe('BudgetPanel', () => {
|
||||
render(<BudgetPanel tripId={1} />);
|
||||
await screen.findByText('Train');
|
||||
// expense_date is rendered as plain text in read-only mode
|
||||
await screen.findByText('2025-06-15');
|
||||
expect(await screen.findByText('2025-06-15')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-COMP-BUDGET-035: settlement section with avatar renders user avatar image', async () => {
|
||||
|
||||
@@ -53,7 +53,7 @@ describe('CollabChat', () => {
|
||||
|
||||
it('FE-COMP-CHAT-002: shows empty state when no messages', async () => {
|
||||
render(<CollabChat {...defaultProps} />);
|
||||
await screen.findByText('Start the conversation');
|
||||
expect(await screen.findByText('Start the conversation')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-COMP-CHAT-003: shows message input placeholder', async () => {
|
||||
@@ -85,7 +85,7 @@ describe('CollabChat', () => {
|
||||
)
|
||||
);
|
||||
render(<CollabChat {...defaultProps} />);
|
||||
await screen.findByText('Hello world!');
|
||||
expect(await screen.findByText('Hello world!')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-COMP-CHAT-006: typing in input updates text field', async () => {
|
||||
@@ -599,7 +599,7 @@ describe('CollabChat', () => {
|
||||
await screen.findByText('New 100');
|
||||
const loadMoreBtn = screen.getByRole('button', { name: /load/i });
|
||||
await user.click(loadMoreBtn);
|
||||
await screen.findByText('Older message');
|
||||
expect(await screen.findByText('Older message')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-COMP-CHAT-033: clicking delete on own message marks it as deleted', async () => {
|
||||
|
||||
@@ -47,7 +47,7 @@ describe('CollabNotes', () => {
|
||||
|
||||
it('FE-COMP-NOTES-002: shows empty state when no notes', async () => {
|
||||
render(<CollabNotes {...defaultProps} />);
|
||||
await screen.findByText('No notes yet');
|
||||
expect(await screen.findByText('No notes yet')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-COMP-NOTES-003: shows New Note button', async () => {
|
||||
@@ -70,7 +70,7 @@ describe('CollabNotes', () => {
|
||||
)
|
||||
);
|
||||
render(<CollabNotes {...defaultProps} />);
|
||||
await screen.findByText('Packing Tips');
|
||||
expect(await screen.findByText('Packing Tips')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-COMP-NOTES-005: clicking New Note opens modal', async () => {
|
||||
@@ -79,7 +79,7 @@ describe('CollabNotes', () => {
|
||||
await screen.findByText('No notes yet');
|
||||
await user.click(screen.getByText('New Note'));
|
||||
// Modal opens with a title input — placeholder is "Note title" (no ellipsis)
|
||||
await screen.findByPlaceholderText('Note title');
|
||||
expect(await screen.findByPlaceholderText('Note title')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-COMP-NOTES-006: note title is shown in the grid', async () => {
|
||||
@@ -96,7 +96,7 @@ describe('CollabNotes', () => {
|
||||
)
|
||||
);
|
||||
render(<CollabNotes {...defaultProps} />);
|
||||
await screen.findByText('My Checklist');
|
||||
expect(await screen.findByText('My Checklist')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-COMP-NOTES-007: multiple notes all render', async () => {
|
||||
@@ -117,8 +117,11 @@ describe('CollabNotes', () => {
|
||||
|
||||
it('FE-COMP-NOTES-008: Notes title heading is shown', async () => {
|
||||
render(<CollabNotes {...defaultProps} />);
|
||||
// The loading panel renders its own copy of the heading and swaps the whole
|
||||
// node out once the fetch settles, so wait for the loaded state first.
|
||||
await screen.findByText('No notes yet');
|
||||
// collab.notes.title = "Notes"
|
||||
await screen.findByText('Notes');
|
||||
expect(screen.getByText('Notes')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-COMP-NOTES-009: create note calls POST API', async () => {
|
||||
@@ -236,7 +239,7 @@ describe('CollabNotes', () => {
|
||||
render(<CollabNotes {...defaultProps} />);
|
||||
await screen.findByText('Editable Note');
|
||||
await user.click(screen.getByTitle('Edit'));
|
||||
await screen.findByDisplayValue('Editable Note');
|
||||
expect(await screen.findByDisplayValue('Editable Note')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-COMP-NOTES-016: category filter hides notes from other categories', async () => {
|
||||
@@ -280,7 +283,7 @@ describe('CollabNotes', () => {
|
||||
},
|
||||
});
|
||||
});
|
||||
await screen.findByText('Live Note');
|
||||
expect(await screen.findByText('Live Note')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-COMP-NOTES-018: WebSocket collab:note:deleted event removes note', async () => {
|
||||
@@ -403,7 +406,7 @@ describe('CollabNotes', () => {
|
||||
await screen.findByText('No notes yet');
|
||||
await user.click(screen.getByTitle('Manage Categories'));
|
||||
// The modal header renders "Category Settings" or similar
|
||||
await screen.findByText('Manage Categories', { selector: 'h3' });
|
||||
expect(await screen.findByText('Manage Categories', { selector: 'h3' })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-COMP-NOTES-025: CategorySettingsModal shows no categories message when empty', async () => {
|
||||
@@ -411,7 +414,7 @@ describe('CollabNotes', () => {
|
||||
render(<CollabNotes {...defaultProps} />);
|
||||
await screen.findByText('No notes yet');
|
||||
await user.click(screen.getByTitle('Manage Categories'));
|
||||
await screen.findByText('No categories yet');
|
||||
expect(await screen.findByText('No categories yet')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-COMP-NOTES-026: CategorySettingsModal add new category', async () => {
|
||||
@@ -426,7 +429,7 @@ describe('CollabNotes', () => {
|
||||
const addBtn = newCatInput.nextElementSibling as HTMLElement;
|
||||
await user.click(addBtn);
|
||||
// "Transport" category appears in the modal
|
||||
await screen.findByText('Transport');
|
||||
expect(await screen.findByText('Transport')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-COMP-NOTES-027: CategorySettingsModal close button dismisses it', async () => {
|
||||
@@ -674,7 +677,7 @@ describe('CollabNotes', () => {
|
||||
// Click the PDF badge to open FilePreviewPortal
|
||||
await user.click(screen.getByText('PDF'));
|
||||
// FilePreviewPortal renders the file name in the header
|
||||
await screen.findByText('document.pdf');
|
||||
expect(await screen.findByText('document.pdf')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-COMP-NOTES-037: note with website shows website thumbnail component', async () => {
|
||||
@@ -1348,7 +1351,7 @@ describe('CollabNotes details', () => {
|
||||
it('FE-W5CNT-001: a corrupt category cache in localStorage is ignored', async () => {
|
||||
localStorage.setItem('collab-cats-1', '{not json');
|
||||
render(<CollabNotes {...defaultProps} />);
|
||||
await screen.findByText('No notes yet');
|
||||
expect(await screen.findByText('No notes yet')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-W5CNT-002: a category without a stored colour falls back to the first palette entry', async () => {
|
||||
@@ -1369,13 +1372,13 @@ describe('CollabNotes details', () => {
|
||||
it('FE-W5CNT-004: notes served as a bare array are rendered', async () => {
|
||||
serveNotes([buildNote({ title: 'Array note' })]);
|
||||
render(<CollabNotes {...defaultProps} />);
|
||||
await screen.findByText('Array note');
|
||||
expect(await screen.findByText('Array note')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-W5CNT-005: an empty payload yields an empty list', async () => {
|
||||
serveNotes(null);
|
||||
render(<CollabNotes {...defaultProps} />);
|
||||
await screen.findByText('No notes yet');
|
||||
expect(await screen.findByText('No notes yet')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-W5CNT-006: a failing load falls back to the empty state', async () => {
|
||||
@@ -1383,7 +1386,7 @@ describe('CollabNotes details', () => {
|
||||
http.get('/api/trips/1/collab/notes', () => new HttpResponse(null, { status: 500 })),
|
||||
);
|
||||
render(<CollabNotes {...defaultProps} />);
|
||||
await screen.findByText('No notes yet');
|
||||
expect(await screen.findByText('No notes yet')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-W5CNT-007: a WebSocket create for a note already in the list does not duplicate it', async () => {
|
||||
@@ -1434,7 +1437,7 @@ describe('CollabNotes details', () => {
|
||||
await user.click(screen.getByText('New Note'));
|
||||
await user.type(await screen.findByPlaceholderText('Note title'), 'Fresh note');
|
||||
await user.click(screen.getByRole('button', { name: 'Create' }));
|
||||
await screen.findByText('Fresh note');
|
||||
expect(await screen.findByText('Fresh note')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-W5CNT-011: an empty create response leaves the list untouched', async () => {
|
||||
|
||||
@@ -55,7 +55,7 @@ beforeEach(() => {
|
||||
describe('CollabPolls', () => {
|
||||
it('FE-COMP-POLLS-001: renders empty state when no polls exist', async () => {
|
||||
render(<CollabPolls {...defaultProps} />);
|
||||
await screen.findByText(/no polls yet|collab\.polls\.empty/i);
|
||||
expect(await screen.findByText(/no polls yet|collab\.polls\.empty/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-COMP-POLLS-002: shows loading spinner initially', async () => {
|
||||
@@ -79,7 +79,7 @@ describe('CollabPolls', () => {
|
||||
),
|
||||
);
|
||||
render(<CollabPolls {...defaultProps} />);
|
||||
await screen.findByText('Best destination?');
|
||||
expect(await screen.findByText('Best destination?')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-COMP-POLLS-004: renders poll options', async () => {
|
||||
@@ -108,7 +108,7 @@ describe('CollabPolls', () => {
|
||||
await screen.findByText(/no polls yet|collab\.polls\.empty/i);
|
||||
await user.click(screen.getByRole('button', { name: /new/i }));
|
||||
// Modal has a question placeholder input
|
||||
await screen.findByPlaceholderText(/what should we do/i);
|
||||
expect(await screen.findByPlaceholderText(/what should we do/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-COMP-POLLS-007: create modal requires question and at least 2 options to enable submit', async () => {
|
||||
@@ -153,7 +153,7 @@ describe('CollabPolls', () => {
|
||||
await user.type(optionInputs[1], 'Japanese');
|
||||
|
||||
await user.click(screen.getByRole('button', { name: /create|collab\.polls\.create/i }));
|
||||
await screen.findByText('Where to eat?');
|
||||
expect(await screen.findByText('Where to eat?')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-COMP-POLLS-009: voting on an option calls POST vote API', async () => {
|
||||
@@ -188,7 +188,7 @@ describe('CollabPolls', () => {
|
||||
),
|
||||
);
|
||||
render(<CollabPolls {...defaultProps} />);
|
||||
await screen.findByText(/closed/i);
|
||||
expect(await screen.findByText(/closed/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-COMP-POLLS-011: closed poll options are disabled (cannot vote)', async () => {
|
||||
@@ -236,7 +236,7 @@ describe('CollabPolls', () => {
|
||||
const listener = (addListener as ReturnType<typeof vi.fn>).mock.calls[0][0];
|
||||
listener({ tripId: 1, type: 'collab:poll:created', poll: buildPoll({ id: 77, question: 'Live poll?' }) });
|
||||
|
||||
await screen.findByText('Live poll?');
|
||||
expect(await screen.findByText('Live poll?')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-COMP-POLLS-014: WebSocket collab:poll:deleted event removes poll', async () => {
|
||||
@@ -337,7 +337,7 @@ describe('CollabPolls details', () => {
|
||||
it('FE-W5CPL-005: a poll served as a bare array is rendered', async () => {
|
||||
servePolls([buildPoll({ question: 'Array shaped?' })]);
|
||||
render(<CollabPolls {...defaultProps} />);
|
||||
await screen.findByText('Array shaped?');
|
||||
expect(await screen.findByText('Array shaped?')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-W5CPL-006: options without a voters array count as zero votes', async () => {
|
||||
@@ -484,13 +484,13 @@ describe('CollabPolls details', () => {
|
||||
http.get('/api/trips/1/collab/polls', () => new HttpResponse(null, { status: 500 })),
|
||||
);
|
||||
render(<CollabPolls {...defaultProps} />);
|
||||
await screen.findByText(/no polls yet|collab\.polls\.empty/i);
|
||||
expect(await screen.findByText(/no polls yet|collab\.polls\.empty/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-W5CPL-028: a payload without a polls key yields an empty list', async () => {
|
||||
servePolls({});
|
||||
render(<CollabPolls {...defaultProps} />);
|
||||
await screen.findByText(/no polls yet|collab\.polls\.empty/i);
|
||||
expect(await screen.findByText(/no polls yet|collab\.polls\.empty/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-W5CPL-015: a failing close shows an error and leaves the poll open', async () => {
|
||||
|
||||
@@ -176,7 +176,7 @@ describe('FileManager', () => {
|
||||
await user.click(trashBtn);
|
||||
|
||||
// Trashed file should appear
|
||||
await screen.findByText('old.pdf');
|
||||
expect(await screen.findByText('old.pdf')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-COMP-FILEMANAGER-007: restore button calls filesApi.restore', async () => {
|
||||
@@ -407,7 +407,7 @@ describe('FileManager', () => {
|
||||
const assignBtn = screen.getByTitle(/assign/i);
|
||||
await user.click(assignBtn);
|
||||
|
||||
await screen.findByText('Eiffel Tower');
|
||||
expect(await screen.findByText('Eiffel Tower')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-COMP-FILEMANAGER-021: file description is shown when present', () => {
|
||||
@@ -445,7 +445,7 @@ describe('FileManager', () => {
|
||||
const assignBtn = screen.getByTitle(/assign/i);
|
||||
await user.click(assignBtn);
|
||||
|
||||
await screen.findByText('Hotel Paris');
|
||||
expect(await screen.findByText('Hotel Paris')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-COMP-FILEMANAGER-024: clicking a place in assign modal calls filesApi.update', async () => {
|
||||
@@ -492,7 +492,7 @@ describe('FileManager', () => {
|
||||
|
||||
await user.click(screen.getByTitle(/assign/i));
|
||||
await screen.findByText('Notre Dame');
|
||||
await screen.findByText('Airbnb');
|
||||
expect(await screen.findByText('Airbnb')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-COMP-FILEMANAGER-027: paste event uploads file when user can upload', async () => {
|
||||
@@ -543,7 +543,7 @@ describe('FileManager', () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
await user.click(screen.getByTitle(/assign/i));
|
||||
await screen.findByText('Arc de Triomphe');
|
||||
expect(await screen.findByText('Arc de Triomphe')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-COMP-FILEMANAGER-030: file with linked place shows source badge', async () => {
|
||||
@@ -554,7 +554,7 @@ describe('FileManager', () => {
|
||||
render(<FileManager {...defaultProps} files={[file]} places={[place]} />);
|
||||
|
||||
// Source badge text includes place name
|
||||
await screen.findByText(/Colosseum/);
|
||||
expect(await screen.findByText(/Colosseum/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-COMP-FILEMANAGER-031: unlink place from assign modal calls filesApi.update', async () => {
|
||||
|
||||
@@ -62,7 +62,7 @@ describe('InAppNotificationBell', () => {
|
||||
const bell = screen.getAllByRole('button')[0];
|
||||
await user.click(bell);
|
||||
// Panel shows "Notifications" title
|
||||
await screen.findByText('Notifications');
|
||||
expect(await screen.findByText('Notifications')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-COMP-BELL-004: notification panel shows empty state when no notifications', async () => {
|
||||
@@ -76,7 +76,7 @@ describe('InAppNotificationBell', () => {
|
||||
render(<InAppNotificationBell />);
|
||||
const bell = screen.getAllByRole('button')[0];
|
||||
await user.click(bell);
|
||||
await screen.findByText('No notifications');
|
||||
expect(await screen.findByText('No notifications')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-COMP-BELL-005: shows unread badge count when there are unread notifications', async () => {
|
||||
@@ -98,7 +98,7 @@ describe('InAppNotificationBell', () => {
|
||||
render(<InAppNotificationBell />);
|
||||
const bell = screen.getAllByRole('button')[0];
|
||||
await user.click(bell);
|
||||
await screen.findByTitle('Mark all read');
|
||||
expect(await screen.findByTitle('Mark all read')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-COMP-BELL-008: panel shows empty description when no notifications', async () => {
|
||||
@@ -111,7 +111,7 @@ describe('InAppNotificationBell', () => {
|
||||
const user = userEvent.setup();
|
||||
render(<InAppNotificationBell />);
|
||||
await user.click(screen.getAllByRole('button')[0]);
|
||||
await screen.findByText("You're all caught up!");
|
||||
expect(await screen.findByText("You're all caught up!")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-COMP-BELL-009: bell is accessible as a button', () => {
|
||||
|
||||
@@ -171,7 +171,7 @@ describe('PackingListPanel', () => {
|
||||
const user = userEvent.setup();
|
||||
render(<PackingListPanel tripId={1} items={[]} />);
|
||||
await user.click(screen.getByText('Add list'));
|
||||
await screen.findByPlaceholderText('List name (e.g. Clothing)');
|
||||
expect(await screen.findByPlaceholderText('List name (e.g. Clothing)')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-COMP-PACKING-016: delete item button exists and triggers API call', async () => {
|
||||
@@ -392,7 +392,7 @@ describe('PackingListPanel', () => {
|
||||
render(<PackingListPanel tripId={1} items={[]} />);
|
||||
|
||||
// "Apply template" button appears when templates are available
|
||||
await screen.findByText('Apply template');
|
||||
expect(await screen.findByText('Apply template')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-COMP-PACKING-031: "Uncheck All" bulk action calls PUT to uncheck checked items', async () => {
|
||||
|
||||
@@ -158,7 +158,7 @@ describe('DayDetailPanel', () => {
|
||||
),
|
||||
);
|
||||
render(<DayDetailPanel {...defaultProps} lat={48.8566} lng={2.3522} />);
|
||||
await screen.findByText(/22°C/);
|
||||
expect(await screen.findByText(/22°C/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-PLANNER-DAYDETAIL-011: weather in Fahrenheit when setting is fahrenheit', async () => {
|
||||
@@ -171,7 +171,7 @@ describe('DayDetailPanel', () => {
|
||||
),
|
||||
);
|
||||
render(<DayDetailPanel {...defaultProps} lat={48.8566} lng={2.3522} />);
|
||||
await screen.findByText(/32°F/);
|
||||
expect(await screen.findByText(/32°F/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-PLANNER-DAYDETAIL-012: no weather shows "No weather data" message', async () => {
|
||||
@@ -179,7 +179,7 @@ describe('DayDetailPanel', () => {
|
||||
http.get('/api/weather/detailed', () => HttpResponse.json({ error: true })),
|
||||
);
|
||||
render(<DayDetailPanel {...defaultProps} lat={48.8566} lng={2.3522} />);
|
||||
await screen.findByText(/No weather/i);
|
||||
expect(await screen.findByText(/No weather/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// ── Reservations ─────────────────────────────────────────────────────────────
|
||||
@@ -197,7 +197,7 @@ describe('DayDetailPanel', () => {
|
||||
assignments={{ '1': [{ id: 50, place, place_id: place.id, day_id: 1, order_index: 0, notes: null }] }}
|
||||
reservations={[reservation]}
|
||||
/>);
|
||||
await screen.findByText('Museum Tour Ticket');
|
||||
expect(await screen.findByText('Museum Tour Ticket')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-PLANNER-DAYDETAIL-014: reservations from OTHER days are not shown', async () => {
|
||||
@@ -266,7 +266,7 @@ describe('DayDetailPanel', () => {
|
||||
),
|
||||
);
|
||||
render(<DayDetailPanel {...defaultProps} />);
|
||||
await screen.findByText('Grand Hotel');
|
||||
expect(await screen.findByText('Grand Hotel')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-PLANNER-DAYDETAIL-018: check-in time shown for check-in day', async () => {
|
||||
@@ -305,7 +305,7 @@ describe('DayDetailPanel', () => {
|
||||
day={checkOutDay}
|
||||
days={[day, checkOutDay]}
|
||||
/>);
|
||||
await screen.findByText('11:00');
|
||||
expect(await screen.findByText('11:00')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-PLANNER-DAYDETAIL-020: confirmation code shown', async () => {
|
||||
@@ -320,7 +320,7 @@ describe('DayDetailPanel', () => {
|
||||
),
|
||||
);
|
||||
render(<DayDetailPanel {...defaultProps} />);
|
||||
await screen.findByText('HOTEL99');
|
||||
expect(await screen.findByText('HOTEL99')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-PLANNER-DAYDETAIL-021: accommodation edit/remove buttons shown when canEditDays=true', async () => {
|
||||
@@ -396,7 +396,7 @@ describe('DayDetailPanel', () => {
|
||||
it('FE-PLANNER-DAYDETAIL-023: "Add accommodation" button visible when canEditDays=true and no accommodation', async () => {
|
||||
seedStore(useAuthStore, { user: buildAdmin(), isAuthenticated: true });
|
||||
render(<DayDetailPanel {...defaultProps} />);
|
||||
await screen.findByText(/Add accommodation/i);
|
||||
expect(await screen.findByText(/Add accommodation/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-PLANNER-DAYDETAIL-024: clicking add accommodation opens hotel picker', async () => {
|
||||
@@ -466,7 +466,7 @@ describe('DayDetailPanel', () => {
|
||||
await screen.findByText('5.2 mm');
|
||||
await screen.findByText('30 km/h');
|
||||
await screen.findByText('06:30');
|
||||
await screen.findByText('20:15');
|
||||
expect(await screen.findByText('20:15')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-PLANNER-DAYDETAIL-027: weather chips show Fahrenheit wind speed', async () => {
|
||||
@@ -487,7 +487,7 @@ describe('DayDetailPanel', () => {
|
||||
);
|
||||
render(<DayDetailPanel {...defaultProps} lat={48.8566} lng={2.3522} />);
|
||||
// 50 km/h * 0.621371 ≈ 31 mph
|
||||
await screen.findByText('31 mph');
|
||||
expect(await screen.findByText('31 mph')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// ── Hotel picker interactions ─────────────────────────────────────────────────
|
||||
@@ -516,7 +516,7 @@ describe('DayDetailPanel', () => {
|
||||
await userEvent.click(addButton);
|
||||
await screen.findByText('Hotel du Nord');
|
||||
await screen.findByText('Hotel du Sud');
|
||||
await screen.findByText('102 Quai de Jemmapes');
|
||||
expect(await screen.findByText('102 Quai de Jemmapes')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-PLANNER-DAYDETAIL-030: selecting a place in hotel picker enables save button', async () => {
|
||||
@@ -691,7 +691,7 @@ describe('DayDetailPanel', () => {
|
||||
),
|
||||
);
|
||||
render(<DayDetailPanel {...defaultProps} lat={48.8566} lng={2.3522} />);
|
||||
await screen.findByText(/Ø/);
|
||||
expect(await screen.findByText(/Ø/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-PLANNER-DAYDETAIL-038: hotel picker with category filter renders category buttons', async () => {
|
||||
@@ -722,7 +722,7 @@ describe('DayDetailPanel', () => {
|
||||
render(<DayDetailPanel {...defaultProps} />);
|
||||
await screen.findByText('Existing Hotel');
|
||||
// "Add accommodation" dashed button should also appear for adding more
|
||||
await screen.findByText(/Add accommodation/i);
|
||||
expect(await screen.findByText(/Add accommodation/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-PLANNER-DAYDETAIL-041: save new accommodation calls API and updates list', async () => {
|
||||
@@ -838,7 +838,7 @@ describe('DayDetailPanel', () => {
|
||||
);
|
||||
render(<DayDetailPanel {...defaultProps} lat={48.8566} lng={2.3522} />);
|
||||
// Should show "No weather" after error (catch sets weather to null)
|
||||
await screen.findByText(/No weather/i);
|
||||
expect(await screen.findByText(/No weather/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-PLANNER-DAYDETAIL-046: save edited accommodation calls update API', async () => {
|
||||
@@ -1251,7 +1251,7 @@ describe('DayDetailPanel', () => {
|
||||
|
||||
// Intermediate day (id=1, position 9): old filter: 1>=17 → false. New: 9 in [0,15] → visible.
|
||||
render(<DayDetailPanel {...defaultProps} day={days[9]} days={days} />);
|
||||
await screen.findByText('Full Trip Hotel');
|
||||
expect(await screen.findByText('Full Trip Hotel')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-PLANNER-DAYDETAIL-040: 12h time format renders reservation time with AM/PM', async () => {
|
||||
|
||||
@@ -183,7 +183,7 @@ describe('PlaceFormModal', () => {
|
||||
const searchBtn = within(searchRow).getByRole('button');
|
||||
await user.click(searchBtn);
|
||||
|
||||
await screen.findByText('Eiffel Tower');
|
||||
expect(await screen.findByText('Eiffel Tower')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-PLANNER-PLACEFORM-019: pressing Enter in search input triggers search', async () => {
|
||||
@@ -201,7 +201,7 @@ describe('PlaceFormModal', () => {
|
||||
await user.type(searchInput, 'Eiffel Tower');
|
||||
await user.keyboard('{Enter}');
|
||||
|
||||
await screen.findByText('Eiffel Tower');
|
||||
expect(await screen.findByText('Eiffel Tower')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-PLANNER-PLACEFORM-020: clicking a maps result fills the form', async () => {
|
||||
|
||||
@@ -335,7 +335,7 @@ describe('PlaceInspector', () => {
|
||||
} as any);
|
||||
const p = buildPlace({ id: 201, google_place_id: 'ChIJ002' });
|
||||
render(<PlaceInspector {...defaultProps} place={p} />);
|
||||
await screen.findByText(/4\.5/);
|
||||
expect(await screen.findByText(/4\.5/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-PLANNER-INSPECTOR-025: opening hours shown when available', async () => {
|
||||
@@ -361,7 +361,7 @@ describe('PlaceInspector', () => {
|
||||
} as any);
|
||||
const p = buildPlace({ id: 203, google_place_id: 'ChIJ004' });
|
||||
render(<PlaceInspector {...defaultProps} place={p} />);
|
||||
await screen.findByText(/open/i);
|
||||
expect(await screen.findByText(/open/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-PLANNER-INSPECTOR-027: mapsApi.details NOT called when place has no google_place_id or osm_id', async () => {
|
||||
@@ -394,7 +394,7 @@ describe('PlaceInspector', () => {
|
||||
// Click the expand button (file count label button)
|
||||
if (filesBtn) {
|
||||
await user.click(filesBtn);
|
||||
await screen.findByText('photo.jpg');
|
||||
expect(await screen.findByText('photo.jpg')).toBeInTheDocument();
|
||||
} else {
|
||||
// Try clicking the last non-footer button
|
||||
const toggleButtons = allButtons.filter(btn => !btn.closest('footer'));
|
||||
|
||||
@@ -74,7 +74,7 @@ describe('AccountTab', () => {
|
||||
render(<><ToastContainer /><AccountTab /></>);
|
||||
await user.click(screen.getByText('Update password'));
|
||||
// Validation fires: first checks currentPassword — "Current password is required"
|
||||
await screen.findByText(/Current password is required/i);
|
||||
expect(await screen.findByText(/Current password is required/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-COMP-ACCOUNT-011: password mismatch shows error', async () => {
|
||||
@@ -86,7 +86,7 @@ describe('AccountTab', () => {
|
||||
await user.type(passwordInputs[1], 'NewPassword1!');
|
||||
await user.type(passwordInputs[2], 'DifferentPass1!');
|
||||
await user.click(screen.getByText('Update password'));
|
||||
await screen.findByText('Passwords do not match');
|
||||
expect(await screen.findByText('Passwords do not match')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-COMP-ACCOUNT-012: valid password change calls API', async () => {
|
||||
@@ -140,7 +140,7 @@ describe('AccountTab – Profile', () => {
|
||||
seedStore(useAuthStore, { updateProfile: vi.fn().mockResolvedValue(undefined) });
|
||||
render(<><ToastContainer /><AccountTab /></>);
|
||||
await user.click(screen.getByRole('button', { name: /save profile/i }));
|
||||
await screen.findByText('Profile saved');
|
||||
expect(await screen.findByText('Profile saved')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-COMP-ACCOUNT-016: failed save shows error toast with error message', async () => {
|
||||
@@ -148,7 +148,7 @@ describe('AccountTab – Profile', () => {
|
||||
seedStore(useAuthStore, { updateProfile: vi.fn().mockRejectedValue(new Error('Server error')) });
|
||||
render(<><ToastContainer /><AccountTab /></>);
|
||||
await user.click(screen.getByRole('button', { name: /save profile/i }));
|
||||
await screen.findByText('Server error');
|
||||
expect(await screen.findByText('Server error')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-COMP-ACCOUNT-017: Save button shows spinner while saving', async () => {
|
||||
@@ -171,7 +171,7 @@ describe('AccountTab – Password change', () => {
|
||||
await user.type(passwordInputs[1], 'short');
|
||||
await user.type(passwordInputs[2], 'short');
|
||||
await user.click(screen.getByText('Update password'));
|
||||
await screen.findByText(/at least 8 characters/i);
|
||||
expect(await screen.findByText(/at least 8 characters/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-COMP-ACCOUNT-019: password change clears fields on success', async () => {
|
||||
@@ -205,7 +205,7 @@ describe('AccountTab – Password change', () => {
|
||||
await user.type(passwordInputs[1], 'NewPassword1!');
|
||||
await user.type(passwordInputs[2], 'NewPassword1!');
|
||||
await user.click(screen.getByText('Update password'));
|
||||
await screen.findByText('Wrong password');
|
||||
expect(await screen.findByText('Wrong password')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-COMP-ACCOUNT-021: password section hidden in OIDC-only mode', async () => {
|
||||
@@ -376,7 +376,7 @@ describe('AccountTab – MFA', () => {
|
||||
const codeInput = screen.getByPlaceholderText('6-digit code');
|
||||
await user.type(codeInput, '123456');
|
||||
await user.click(screen.getByRole('button', { name: 'Disable 2FA' }));
|
||||
await screen.findByText('Two-factor authentication disabled');
|
||||
expect(await screen.findByText('Two-factor authentication disabled')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-COMP-ACCOUNT-035: policy warning shown when MFA is required by policy', () => {
|
||||
@@ -577,7 +577,7 @@ describe('AccountTab – Backup codes', () => {
|
||||
|
||||
await user.click(await screen.findByRole('button', { name: 'Copy codes' }));
|
||||
|
||||
await screen.findByText('Error');
|
||||
expect(await screen.findByText('Error')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-COMP-ACCOUNT-052: Download TXT builds a blob, names the file and revokes the URL', async () => {
|
||||
@@ -682,7 +682,7 @@ describe('AccountTab – Avatar upload', () => {
|
||||
const input = document.querySelector('input[type="file"]') as HTMLInputElement;
|
||||
await user.upload(input, new File(['avatar'], 'me.png', { type: 'image/png' }));
|
||||
|
||||
await screen.findByText('Upload failed');
|
||||
expect(await screen.findByText('Upload failed')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-COMP-ACCOUNT-058: a change event without a file uploads nothing', () => {
|
||||
@@ -723,7 +723,7 @@ describe('AccountTab – Avatar upload', () => {
|
||||
const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement;
|
||||
await user.click(fileInput.parentElement!.querySelectorAll('button')[1]);
|
||||
|
||||
await screen.findByText('Upload failed');
|
||||
expect(await screen.findByText('Upload failed')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -177,7 +177,7 @@ describe('AirTrailConnectionSection', () => {
|
||||
await screen.findByDisplayValue('https://air.example.com');
|
||||
await user.click(screen.getByRole('button', { name: /^Save$/ }));
|
||||
|
||||
await screen.findByText('Not connected');
|
||||
expect(await screen.findByText('Not connected')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-COMP-AIRTRAIL-010: a rejected save surfaces the server error message', async () => {
|
||||
@@ -193,7 +193,7 @@ describe('AirTrailConnectionSection', () => {
|
||||
await screen.findByDisplayValue('https://air.example.com');
|
||||
await user.click(screen.getByRole('button', { name: /^Save$/ }));
|
||||
|
||||
await screen.findByText('Instance unreachable');
|
||||
expect(await screen.findByText('Instance unreachable')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-COMP-AIRTRAIL-011: a save failure without a server message falls back to the generic error', async () => {
|
||||
@@ -207,7 +207,7 @@ describe('AirTrailConnectionSection', () => {
|
||||
await screen.findByDisplayValue('https://air.example.com');
|
||||
await user.click(screen.getByRole('button', { name: /^Save$/ }));
|
||||
|
||||
await screen.findByText('Could not save the connection');
|
||||
expect(await screen.findByText('Could not save the connection')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-COMP-AIRTRAIL-012: Save is locked while the request is in flight', async () => {
|
||||
@@ -265,7 +265,7 @@ describe('AirTrailConnectionSection', () => {
|
||||
await screen.findByDisplayValue('https://air.example.com');
|
||||
await user.click(screen.getByRole('button', { name: /Test connection/ }));
|
||||
|
||||
await screen.findByText('Connected — 0 flight(s) found');
|
||||
expect(await screen.findByText('Connected — 0 flight(s) found')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-COMP-AIRTRAIL-015: a refused test shows the returned error and keeps the badge off', async () => {
|
||||
|
||||
@@ -120,7 +120,7 @@ describe('AppearanceSettingsTab – custom accent and sliders', () => {
|
||||
|
||||
await user.click(screen.getByText('Teal'));
|
||||
|
||||
await screen.findByText('Appearance rejected', undefined, PERSIST);
|
||||
expect(await screen.findByText('Appearance rejected', undefined, PERSIST)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-COMP-APPEARANCE-011: a failing colour-mode change surfaces the error toast', async () => {
|
||||
@@ -130,7 +130,7 @@ describe('AppearanceSettingsTab – custom accent and sliders', () => {
|
||||
|
||||
await user.click(screen.getByText('Dark'));
|
||||
|
||||
await screen.findByText('Mode rejected');
|
||||
expect(await screen.findByText('Mode rejected')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-COMP-APPEARANCE-012: picking Custom reveals the accent picker', async () => {
|
||||
|
||||
@@ -165,7 +165,7 @@ describe('DisplaySettingsTab', () => {
|
||||
seedStore(useSettingsStore, { settings: buildSettings({ temperature_unit: 'celsius' }), updateSetting });
|
||||
render(<><ToastContainer /><DisplaySettingsTab /></>);
|
||||
await user.click(screen.getByText('°F Fahrenheit'));
|
||||
await screen.findByText('Server error');
|
||||
expect(await screen.findByText('Server error')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-COMP-DISPLAY-027: temperature unit local state updates optimistically before API resolves', async () => {
|
||||
@@ -214,7 +214,7 @@ describe('DisplaySettingsTab – Display currency', () => {
|
||||
await user.click(screen.getByRole('button', { name: /USD/ }));
|
||||
await user.click(await screen.findByText('Trip currency'));
|
||||
|
||||
await screen.findByText('Currency locked');
|
||||
expect(await screen.findByText('Currency locked')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -261,7 +261,7 @@ describe('DisplaySettingsTab – Compact language picker', () => {
|
||||
await user.click(within(wrap).getByRole('button'));
|
||||
await user.click(within(wrap).getByText('Deutsch').closest('button')!);
|
||||
|
||||
await screen.findByText('Language locked');
|
||||
expect(await screen.findByText('Language locked')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-COMP-DISPLAY-048: a rejected pick from the desktop grid surfaces the error', async () => {
|
||||
@@ -271,7 +271,7 @@ describe('DisplaySettingsTab – Compact language picker', () => {
|
||||
|
||||
await user.click(screen.getByText('Deutsch'));
|
||||
|
||||
await screen.findByText('Language locked');
|
||||
expect(await screen.findByText('Language locked')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-COMP-DISPLAY-038: a mousedown outside the picker closes it', async () => {
|
||||
@@ -320,7 +320,7 @@ describe('DisplaySettingsTab – Map and privacy toggles', () => {
|
||||
|
||||
await user.click(within(optionBlock(/booking route labels/i)).getByText(/^On$/));
|
||||
|
||||
await screen.findByText('Labels locked');
|
||||
expect(await screen.findByText('Labels locked')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-COMP-DISPLAY-042: a rejected always-show-routes change surfaces the error', async () => {
|
||||
@@ -330,7 +330,7 @@ describe('DisplaySettingsTab – Map and privacy toggles', () => {
|
||||
|
||||
await user.click(within(optionBlock(/always show booking routes/i)).getByText(/^On$/));
|
||||
|
||||
await screen.findByText('Routes locked');
|
||||
expect(await screen.findByText('Routes locked')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-COMP-DISPLAY-043: the POI pill defaults to On and can be turned off', async () => {
|
||||
@@ -353,7 +353,7 @@ describe('DisplaySettingsTab – Map and privacy toggles', () => {
|
||||
|
||||
await user.click(within(optionBlock(/explore places on the map/i)).getByText(/^Off$/));
|
||||
|
||||
await screen.findByText('POI locked');
|
||||
expect(await screen.findByText('POI locked')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-COMP-DISPLAY-045: turning blur booking codes on persists true', async () => {
|
||||
@@ -441,6 +441,6 @@ describe('DisplaySettingsTab – startup destination', () => {
|
||||
|
||||
await user.click(within(optionBlock(/^Start page$/)).getByText('Active trip'));
|
||||
|
||||
await screen.findByText('Start locked');
|
||||
expect(await screen.findByText('Start locked')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -60,7 +60,7 @@ describe('IntegrationsTab', () => {
|
||||
it('FE-COMP-INTEGRATIONS-003: MCP section is visible when mcp addon is enabled', async () => {
|
||||
enableMcp();
|
||||
render(<IntegrationsTab />);
|
||||
await screen.findByText('MCP Configuration');
|
||||
expect(await screen.findByText('MCP Configuration')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-COMP-INTEGRATIONS-004: MCP endpoint URL is displayed', async () => {
|
||||
@@ -92,7 +92,7 @@ describe('IntegrationsTab', () => {
|
||||
render(<IntegrationsTab />);
|
||||
await screen.findByText('MCP Configuration');
|
||||
await user.click(screen.getByRole('button', { name: /API Tokens/i }));
|
||||
await screen.findByText('No tokens yet. Create one to connect MCP clients.');
|
||||
expect(await screen.findByText('No tokens yet. Create one to connect MCP clients.')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-COMP-INTEGRATIONS-007: token list renders when tokens exist', async () => {
|
||||
@@ -112,7 +112,7 @@ describe('IntegrationsTab', () => {
|
||||
await screen.findByText('MCP Configuration');
|
||||
await user.click(screen.getByRole('button', { name: /API Tokens/i }));
|
||||
await screen.findByText('My Token');
|
||||
await screen.findByText('Other Token');
|
||||
expect(await screen.findByText('Other Token')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-COMP-INTEGRATIONS-008: clicking "Create New Token" button opens the modal', async () => {
|
||||
@@ -123,7 +123,7 @@ describe('IntegrationsTab', () => {
|
||||
await user.click(screen.getByRole('button', { name: /API Tokens/i }));
|
||||
const createBtn = screen.getByRole('button', { name: /Create New Token/i });
|
||||
await user.click(createBtn);
|
||||
await screen.findByText('Create API Token');
|
||||
expect(await screen.findByText('Create API Token')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-COMP-INTEGRATIONS-009: Create button in modal is disabled when name is empty', async () => {
|
||||
@@ -591,7 +591,7 @@ describe('IntegrationsTab', () => {
|
||||
// Confirm — button text is 'Rotate'
|
||||
const rotateBtns = screen.getAllByRole('button', { name: /^Rotate$/i });
|
||||
await user.click(rotateBtns[rotateBtns.length - 1]);
|
||||
await screen.findByText(/new-rotated-secret/);
|
||||
expect(await screen.findByText(/new-rotated-secret/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-COMP-INTEGRATIONS-030: revoke OAuth session removes it from list', async () => {
|
||||
@@ -649,7 +649,7 @@ describe('IntegrationsTab', () => {
|
||||
await user.type(screen.getByPlaceholderText(/Claude Web, My MCP App/i), 'Fail Client');
|
||||
await user.type(screen.getByPlaceholderText(/https:\/\/your-app/i), 'http://localhost');
|
||||
await user.click(screen.getByRole('button', { name: /Register Client/i }));
|
||||
await screen.findByText(/Failed to register/i);
|
||||
expect(await screen.findByText(/Failed to register/i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -893,7 +893,7 @@ describe('IntegrationsTab – failure toasts', () => {
|
||||
await user.type(screen.getByPlaceholderText(/Claude Desktop/i), 'Doomed');
|
||||
await user.click(screen.getByRole('button', { name: /^Create Token$/i }));
|
||||
|
||||
await screen.findByText('Failed to create token');
|
||||
expect(await screen.findByText('Failed to create token')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-COMP-INTEGRATIONS-045: a failing token deletion toasts and keeps the token', async () => {
|
||||
@@ -934,7 +934,7 @@ describe('IntegrationsTab – failure toasts', () => {
|
||||
await user.click(screen.getByTitle('Rotate Secret'));
|
||||
const rotateBtns = await screen.findAllByRole('button', { name: /^Rotate$/i });
|
||||
await user.click(rotateBtns[rotateBtns.length - 1]);
|
||||
await screen.findByText('Failed to rotate client secret');
|
||||
expect(await screen.findByText('Failed to rotate client secret')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-COMP-INTEGRATIONS-047: a failing session revoke toasts and keeps the session', async () => {
|
||||
|
||||
@@ -134,7 +134,7 @@ describe('MapSettingsTab', () => {
|
||||
});
|
||||
render(<><ToastContainer /><MapSettingsTab /></>);
|
||||
await user.click(screen.getByText('Save Map'));
|
||||
await screen.findByText('Save failed');
|
||||
expect(await screen.findByText('Save failed')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-COMP-MAP-016: preset dropdown is rendered', () => {
|
||||
|
||||
@@ -1007,7 +1007,7 @@ describe('NotificationsTab — ntfy credentials', () => {
|
||||
await screen.findByDisplayValue('alerts');
|
||||
await user.click(screen.getByRole('button', { name: 'Save' }));
|
||||
|
||||
await screen.findByText('Error');
|
||||
expect(await screen.findByText('Error')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-COMP-NOTIFICATIONS-019: Clear wipes the stored token and hides the button', async () => {
|
||||
@@ -1073,7 +1073,7 @@ describe('NotificationsTab — ntfy credentials', () => {
|
||||
await screen.findByDisplayValue('alerts');
|
||||
await user.click(screen.getByRole('button', { name: 'Test' }));
|
||||
|
||||
await screen.findByText('Topic not found');
|
||||
expect(await screen.findByText('Topic not found')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-COMP-NOTIFICATIONS-023: a network error during the test falls back to the generic message', async () => {
|
||||
@@ -1087,7 +1087,7 @@ describe('NotificationsTab — ntfy credentials', () => {
|
||||
await screen.findByDisplayValue('alerts');
|
||||
await user.click(screen.getByRole('button', { name: 'Test' }));
|
||||
|
||||
await screen.findByText('Test ntfy notification failed');
|
||||
expect(await screen.findByText('Test ntfy notification failed')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-COMP-NOTIFICATIONS-024: without a topic the test button stays disabled', async () => {
|
||||
@@ -1141,7 +1141,7 @@ describe('NotificationsTab — webhook and channel-test failures', () => {
|
||||
await screen.findByRole('textbox');
|
||||
await user.click(screen.getByRole('button', { name: 'Save' }));
|
||||
|
||||
await screen.findByText('Error');
|
||||
expect(await screen.findByText('Error')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-COMP-NOTIFICATIONS-027: a network error during the webhook test falls back to the generic message', async () => {
|
||||
@@ -1154,7 +1154,7 @@ describe('NotificationsTab — webhook and channel-test failures', () => {
|
||||
await screen.findByRole('textbox');
|
||||
await user.click(screen.getByRole('button', { name: 'Test' }));
|
||||
|
||||
await screen.findByText('Test webhook failed');
|
||||
expect(await screen.findByText('Test webhook failed')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-COMP-NOTIFICATIONS-028: a refused plugin channel test reports the returned error', async () => {
|
||||
@@ -1169,7 +1169,7 @@ describe('NotificationsTab — webhook and channel-test failures', () => {
|
||||
|
||||
await user.click(await screen.findByRole('button', { name: /send test/i }));
|
||||
|
||||
await screen.findByText('Gotify rejected the token');
|
||||
expect(await screen.findByText('Gotify rejected the token')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-COMP-NOTIFICATIONS-029: a plugin channel test that errors out falls back to the generic message', async () => {
|
||||
@@ -1182,6 +1182,6 @@ describe('NotificationsTab — webhook and channel-test failures', () => {
|
||||
|
||||
await user.click(await screen.findByRole('button', { name: /send test/i }));
|
||||
|
||||
await screen.findByText('Test failed.');
|
||||
expect(await screen.findByText('Test failed.')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -344,7 +344,7 @@ describe('PasskeysSection', () => {
|
||||
await user.type(screen.getByDisplayValue('MacBook'), '-x');
|
||||
await user.click(screen.getByRole('button', { name: 'Save' }));
|
||||
|
||||
await screen.findByText('Name taken');
|
||||
expect(await screen.findByText('Name taken')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-COMP-PASSKEYS-020: deleting asks for the password, calls the API and can be cancelled', async () => {
|
||||
|
||||
@@ -81,7 +81,7 @@ describe('PhotoProvidersSection', () => {
|
||||
it('FE-COMP-PHOTOPROVIDERS-003: renders a section card for each active provider', async () => {
|
||||
seedMemoriesEnabled();
|
||||
render(<PhotoProvidersSection />);
|
||||
await screen.findByText('Immich');
|
||||
expect(await screen.findByText('Immich')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-COMP-PHOTOPROVIDERS-004: renders field inputs for each provider field', async () => {
|
||||
@@ -95,7 +95,7 @@ describe('PhotoProvidersSection', () => {
|
||||
it('FE-COMP-PHOTOPROVIDERS-005: non-secret field is prefilled with value from settings API', async () => {
|
||||
seedMemoriesEnabled();
|
||||
render(<PhotoProvidersSection />);
|
||||
await screen.findByDisplayValue('https://photos.example.com');
|
||||
expect(await screen.findByDisplayValue('https://photos.example.com')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-COMP-PHOTOPROVIDERS-006: secret field is NOT prefilled (blank value)', async () => {
|
||||
@@ -240,7 +240,9 @@ describe('PhotoProvidersSection', () => {
|
||||
await screen.findByText('Immich');
|
||||
const testBtn = screen.getByRole('button', { name: /test connection/i });
|
||||
await user.click(testBtn);
|
||||
await screen.findByText(/connected/i);
|
||||
// Exact text on purpose: the badge always renders, and a loose /connected/i
|
||||
// also matches the "Not connected" it starts out as.
|
||||
expect(await screen.findByText('Connected')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-COMP-PHOTOPROVIDERS-015: failed test shows error toast', async () => {
|
||||
@@ -258,7 +260,7 @@ describe('PhotoProvidersSection', () => {
|
||||
await screen.findByText('Immich');
|
||||
const testBtn = screen.getByRole('button', { name: /test connection/i });
|
||||
await user.click(testBtn);
|
||||
await screen.findByText(/Auth failed/i);
|
||||
expect(await screen.findByText(/Auth failed/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-COMP-PHOTOPROVIDERS-016: Test button is disabled while test is in progress', async () => {
|
||||
@@ -327,7 +329,7 @@ describe('PhotoProvidersSection', () => {
|
||||
seedMemoriesEnabled([fakeProvider, secondProvider]);
|
||||
render(<PhotoProvidersSection />);
|
||||
await screen.findByText('Immich');
|
||||
await screen.findByText('Piwigo');
|
||||
expect(await screen.findByText('Piwigo')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -433,7 +435,7 @@ describe('PhotoProvidersSection – checkbox fields and failures', () => {
|
||||
await screen.findByText('Immich');
|
||||
await user.click(screen.getByRole('button', { name: /test connection/i }));
|
||||
|
||||
await screen.findByText('Could not connect to Immich');
|
||||
expect(await screen.findByText('Could not connect to Immich')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-COMP-PHOTOPROVIDERS-024: a provider without a test POST route probes over GET instead', async () => {
|
||||
|
||||
@@ -42,7 +42,7 @@ describe('TodoListPanel', () => {
|
||||
it('FE-COMP-TODO-002: raising addItemSignal opens the new task form', async () => {
|
||||
const { rerender } = render(<TodoListPanel tripId={1} items={[]} addItemSignal={0} />);
|
||||
rerender(<TodoListPanel tripId={1} items={[]} addItemSignal={1} />);
|
||||
await screen.findByText('Create task');
|
||||
expect(await screen.findByText('Create task')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-COMP-TODO-003: sidebar filter buttons are rendered', () => {
|
||||
@@ -125,7 +125,7 @@ describe('TodoListPanel', () => {
|
||||
it('FE-COMP-TODO-011: raising addItemSignal opens detail form with Create task button', async () => {
|
||||
const { rerender } = render(<TodoListPanel tripId={1} items={[]} addItemSignal={0} />);
|
||||
rerender(<TodoListPanel tripId={1} items={[]} addItemSignal={1} />);
|
||||
await screen.findByText('Create task');
|
||||
expect(await screen.findByText('Create task')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-COMP-TODO-012: toggling item calls toggleTodoItem action', async () => {
|
||||
@@ -158,7 +158,7 @@ describe('TodoListPanel', () => {
|
||||
render(<TodoListPanel tripId={1} items={items} />);
|
||||
await user.click(screen.getByText('Click Me'));
|
||||
// Detail pane should open showing the task title
|
||||
await screen.findByText('Task');
|
||||
expect(await screen.findByText('Task')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-COMP-TODO-014: category filter appears in sidebar for items with categories', () => {
|
||||
|
||||
@@ -112,7 +112,7 @@ describe('TripFormModal', () => {
|
||||
await user.click(submitBtn.closest('button') || submitBtn);
|
||||
}
|
||||
// Error: "Title is required"
|
||||
await screen.findByText('Title is required');
|
||||
expect(await screen.findByText('Title is required')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-COMP-TRIPFORM-010: typing title and submitting calls onSave', async () => {
|
||||
@@ -229,7 +229,7 @@ describe('TripFormModal', () => {
|
||||
)
|
||||
);
|
||||
render(<TripFormModal {...defaultProps} trip={null} />);
|
||||
await screen.findByText('Travel buddies');
|
||||
expect(await screen.findByText('Travel buddies')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-COMP-TRIPFORM-024: selecting a member adds a chip', async () => {
|
||||
@@ -305,7 +305,7 @@ describe('TripFormModal', () => {
|
||||
const submitBtns = screen.getAllByText('Create New Trip');
|
||||
const submitBtn = submitBtns.find(el => el.closest('button'))!;
|
||||
await user.click(submitBtn.closest('button')!);
|
||||
await screen.findByText('Server error');
|
||||
expect(await screen.findByText('Server error')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-COMP-TRIPFORM-028: loading spinner shown while submitting', async () => {
|
||||
@@ -720,7 +720,7 @@ describe('TripFormModal', () => {
|
||||
await user.type(screen.getByPlaceholderText('Search destination photos'), 'nowhere');
|
||||
await user.click(screen.getByRole('button', { name: /Search Unsplash/i }));
|
||||
|
||||
await screen.findByText('No images found');
|
||||
expect(await screen.findByText('No images found')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-COMP-TRIPFORM-054: a failing search shows the server error', async () => {
|
||||
@@ -735,7 +735,7 @@ describe('TripFormModal', () => {
|
||||
await user.type(screen.getByPlaceholderText('Search destination photos'), 'alps');
|
||||
await user.click(screen.getByRole('button', { name: /Search Unsplash/i }));
|
||||
|
||||
await screen.findByText('Unsplash key missing');
|
||||
expect(await screen.findByText('Unsplash key missing')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-COMP-TRIPFORM-055: a photo without a photographer falls back in the label and drops the credit', async () => {
|
||||
@@ -1104,7 +1104,7 @@ describe('TripFormModal', () => {
|
||||
await user.type(screen.getByPlaceholderText(/Summer in Japan/i), 'Broken');
|
||||
await submitNewTrip(user);
|
||||
|
||||
await screen.findByText('Failed to save');
|
||||
expect(await screen.findByText('Failed to save')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-COMP-TRIPFORM-077: a save error from the date-shift step is shown on that step', async () => {
|
||||
|
||||
@@ -99,18 +99,18 @@ describe('TripMembersModal', () => {
|
||||
|
||||
it('FE-COMP-MEMBERS-003: shows owner username after load', async () => {
|
||||
render(<TripMembersModal {...defaultProps} />);
|
||||
await screen.findByText('owner');
|
||||
expect(await screen.findByText('owner')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-COMP-MEMBERS-004: shows Owner label', async () => {
|
||||
render(<TripMembersModal {...defaultProps} />);
|
||||
await screen.findByText('Owner');
|
||||
expect(await screen.findByText('Owner')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-COMP-MEMBERS-005: shows Access section heading', async () => {
|
||||
render(<TripMembersModal {...defaultProps} />);
|
||||
// Text is "Access (1 person)" so use regex
|
||||
await screen.findByText(/Access/i);
|
||||
expect(await screen.findByText(/Access/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-COMP-MEMBERS-006: shows member when members are loaded', async () => {
|
||||
@@ -124,17 +124,17 @@ describe('TripMembersModal', () => {
|
||||
)
|
||||
);
|
||||
render(<TripMembersModal {...defaultProps} />);
|
||||
await screen.findByText('alice');
|
||||
expect(await screen.findByText('alice')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-COMP-MEMBERS-007: shows Invite User section', async () => {
|
||||
render(<TripMembersModal {...defaultProps} />);
|
||||
await screen.findByText('Invite User');
|
||||
expect(await screen.findByText('Invite User')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-COMP-MEMBERS-008: shows Invite button', async () => {
|
||||
render(<TripMembersModal {...defaultProps} />);
|
||||
await screen.findByRole('button', { name: /Invite/i });
|
||||
expect(await screen.findByRole('button', { name: /Invite/i })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-COMP-MEMBERS-009: Cancel/close button is present', () => {
|
||||
@@ -148,7 +148,7 @@ describe('TripMembersModal', () => {
|
||||
it('FE-COMP-MEMBERS-010: shows member count of 1 with owner', async () => {
|
||||
render(<TripMembersModal {...defaultProps} />);
|
||||
// 1 person (just owner)
|
||||
await screen.findByText(/1 person/i);
|
||||
expect(await screen.findByText(/1 person/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-COMP-MEMBERS-011: members count increases when member is added', async () => {
|
||||
@@ -162,13 +162,13 @@ describe('TripMembersModal', () => {
|
||||
)
|
||||
);
|
||||
render(<TripMembersModal {...defaultProps} />);
|
||||
await screen.findByText(/2 persons/i);
|
||||
expect(await screen.findByText(/2 persons/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-COMP-MEMBERS-012: shows "you" label next to current user', async () => {
|
||||
render(<TripMembersModal {...defaultProps} />);
|
||||
// Rendered as "(you)" — use regex to find it
|
||||
await screen.findByText(/\(you\)/i);
|
||||
expect(await screen.findByText(/\(you\)/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-COMP-MEMBERS-013: shows remove access button for members (not owner)', async () => {
|
||||
@@ -237,7 +237,7 @@ describe('TripMembersModal', () => {
|
||||
seedStore(useTripStore, { trip: buildTrip({ id: 1, user_id: ownerUser.id }) });
|
||||
|
||||
render(<TripMembersModal {...defaultProps} />);
|
||||
await screen.findByText('Public Link');
|
||||
expect(await screen.findByText('Public Link')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-COMP-MEMBERS-018: create share link shows URL after clicking create', async () => {
|
||||
@@ -462,7 +462,7 @@ describe('TripMembersModal', () => {
|
||||
);
|
||||
|
||||
render(<TripMembersModal {...defaultProps} />);
|
||||
await screen.findByText('All users already have access.');
|
||||
expect(await screen.findByText('All users already have access.')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-COMP-MEMBERS-026: owner sees the guests section and can add a guest (#1362)', async () => {
|
||||
@@ -519,7 +519,7 @@ describe('TripMembersModal', () => {
|
||||
mockRoster([], { id: ownerUser.id, username: '', avatar_url: null });
|
||||
render(<TripMembersModal {...defaultProps} />);
|
||||
|
||||
await screen.findByText('?');
|
||||
expect(await screen.findByText('?')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-COMP-MEMBERS-030: a failing roster load shows an error toast and an empty list', async () => {
|
||||
|
||||
@@ -136,7 +136,7 @@ describe('MAdminPackingTemplateManager', () => {
|
||||
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');
|
||||
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 () => {
|
||||
@@ -369,7 +369,7 @@ describe('MAdminPackingTemplateManager', () => {
|
||||
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');
|
||||
expect(await screen.findByText('Failed to save')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-MOB-APKG-018: renaming a category updates its header', async () => {
|
||||
@@ -586,7 +586,7 @@ describe('MAdminPackingTemplateManager', () => {
|
||||
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');
|
||||
expect(await screen.findByText('Failed to save')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-MOB-APKG-026: renaming an item persists via the save button', async () => {
|
||||
|
||||
@@ -162,7 +162,7 @@ describe('MAirTrailConnectionSection', () => {
|
||||
renderSection();
|
||||
|
||||
await user.click(await screen.findByRole('button', { name: /Save/ }));
|
||||
await screen.findByText('Not connected');
|
||||
expect(await screen.findByText('Not connected')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-MOB-SETAIR-010: a rejected save surfaces the server error message', async () => {
|
||||
@@ -176,7 +176,7 @@ describe('MAirTrailConnectionSection', () => {
|
||||
renderSection();
|
||||
|
||||
await user.click(await screen.findByRole('button', { name: /Save/ }));
|
||||
await screen.findByText('Instance unreachable');
|
||||
expect(await screen.findByText('Instance unreachable')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-MOB-SETAIR-011: a successful test reports the flight count and flips the badge', async () => {
|
||||
@@ -214,6 +214,6 @@ describe('MAirTrailConnectionSection', () => {
|
||||
renderSection();
|
||||
|
||||
await user.click(await screen.findByRole('button', { name: /Test connection/ }));
|
||||
await screen.findByText('Connection failed');
|
||||
expect(await screen.findByText('Connection failed')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -170,7 +170,7 @@ describe('MLlmConnectionSection', () => {
|
||||
renderSection();
|
||||
|
||||
await user.click(screen.getByRole('button', { name: /Save/ }));
|
||||
await screen.findByText('Could not save AI settings');
|
||||
expect(await screen.findByText('Could not save AI settings')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-MOB-SETLLM-013: a stored local provider falls back to OpenAI without saving anything', () => {
|
||||
|
||||
@@ -153,7 +153,7 @@ describe('MSettingsAccount – profile', () => {
|
||||
|
||||
fireEvent.change(fileInput(), { target: { files: [new File(['x'], 'a.png', { type: 'image/png' })] } });
|
||||
|
||||
await screen.findByText('Upload failed');
|
||||
expect(await screen.findByText('Upload failed')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-MOB-SETACC-009: a change event without a file is ignored', () => {
|
||||
@@ -191,7 +191,7 @@ describe('MSettingsAccount – profile', () => {
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'Remove Profile Picture' }));
|
||||
|
||||
await screen.findByText('Removal failed');
|
||||
expect(await screen.findByText('Removal failed')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-MOB-SETACC-012: Save sends the edited fields through updateProfile', async () => {
|
||||
@@ -230,7 +230,7 @@ describe('MSettingsAccount – profile', () => {
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'Save' }));
|
||||
|
||||
await screen.findByText('Error');
|
||||
expect(await screen.findByText('Error')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-MOB-SETACC-013c: without a signed-in user both profile fields start empty', () => {
|
||||
@@ -251,7 +251,7 @@ describe('MSettingsAccount – profile', () => {
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'Save' }));
|
||||
|
||||
await screen.findByText('Email already taken');
|
||||
expect(await screen.findByText('Email already taken')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-MOB-SETACC-014: Save is disabled while the request is in flight', async () => {
|
||||
@@ -291,7 +291,7 @@ describe('MSettingsAccount – password', () => {
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'Update password' }));
|
||||
|
||||
await screen.findByText('Current password is required');
|
||||
expect(await screen.findByText('Current password is required')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-MOB-SETACC-018: an empty new password is rejected', async () => {
|
||||
@@ -301,7 +301,7 @@ describe('MSettingsAccount – password', () => {
|
||||
await user.type(screen.getByPlaceholderText('Current password'), 'old-secret');
|
||||
await user.click(screen.getByRole('button', { name: 'Update password' }));
|
||||
|
||||
await screen.findByText('Please enter current and new password');
|
||||
expect(await screen.findByText('Please enter current and new password')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-MOB-SETACC-019: a new password below eight characters is rejected', async () => {
|
||||
@@ -313,7 +313,7 @@ describe('MSettingsAccount – password', () => {
|
||||
await user.type(screen.getByPlaceholderText('Confirm new password'), 'short');
|
||||
await user.click(screen.getByRole('button', { name: 'Update password' }));
|
||||
|
||||
await screen.findByText('Password must be at least 8 characters');
|
||||
expect(await screen.findByText('Password must be at least 8 characters')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-MOB-SETACC-020: a mismatching confirmation is rejected', async () => {
|
||||
@@ -325,7 +325,7 @@ describe('MSettingsAccount – password', () => {
|
||||
await user.type(screen.getByPlaceholderText('Confirm new password'), 'NewPassword2!');
|
||||
await user.click(screen.getByRole('button', { name: 'Update password' }));
|
||||
|
||||
await screen.findByText('Passwords do not match');
|
||||
expect(await screen.findByText('Passwords do not match')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-MOB-SETACC-021: a valid change hits the API and clears the three fields', async () => {
|
||||
@@ -361,7 +361,7 @@ describe('MSettingsAccount – password', () => {
|
||||
await user.type(screen.getByPlaceholderText('Confirm new password'), 'NewPassword1!');
|
||||
await user.click(screen.getByRole('button', { name: 'Update password' }));
|
||||
|
||||
await screen.findByText('Wrong password');
|
||||
expect(await screen.findByText('Wrong password')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -453,7 +453,7 @@ describe('MSettingsAccount – MFA', () => {
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'Set up authenticator' }));
|
||||
|
||||
await screen.findByText('MFA is off');
|
||||
expect(await screen.findByText('MFA is off')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-MOB-SETACC-032: enabling stores the backup codes and renders them', async () => {
|
||||
@@ -493,7 +493,7 @@ describe('MSettingsAccount – MFA', () => {
|
||||
await user.type(screen.getByPlaceholderText('6-digit code'), '123456');
|
||||
await user.click(screen.getByRole('button', { name: 'Enable 2FA' }));
|
||||
|
||||
await screen.findByText('Invalid code');
|
||||
expect(await screen.findByText('Invalid code')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-MOB-SETACC-035: the disable form needs both a password and a six-digit code', async () => {
|
||||
@@ -547,7 +547,7 @@ describe('MSettingsAccount – MFA', () => {
|
||||
await user.type(screen.getByPlaceholderText('6-digit code'), '123456');
|
||||
await user.click(screen.getByRole('button', { name: 'Disable 2FA' }));
|
||||
|
||||
await screen.findByText('Code expired');
|
||||
expect(await screen.findByText('Code expired')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -614,7 +614,7 @@ describe('MSettingsAccount – backup codes', () => {
|
||||
|
||||
await user.click(await screen.findByRole('button', { name: 'Copy codes' }));
|
||||
|
||||
await screen.findByText('Error');
|
||||
expect(await screen.findByText('Error')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-MOB-SETACC-044: Download TXT builds a blob and revokes the object URL', async () => {
|
||||
@@ -1020,7 +1020,7 @@ describe('MSettingsAccount – passkeys', () => {
|
||||
await user.type(within(row).getByDisplayValue('iPhone'), '2');
|
||||
await user.click(within(row).getByRole('button', { name: 'Save' }));
|
||||
|
||||
await screen.findByText('Name taken');
|
||||
expect(await screen.findByText('Name taken')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-MOB-SETACC-068: deleting asks for the password and removes the credential', async () => {
|
||||
@@ -1068,7 +1068,7 @@ describe('MSettingsAccount – passkeys', () => {
|
||||
await user.type(within(box).getByPlaceholderText('Current password'), 'nope');
|
||||
await user.click(within(box).getByRole('button', { name: 'Delete' }));
|
||||
|
||||
await screen.findByText('Wrong password');
|
||||
expect(await screen.findByText('Wrong password')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-MOB-SETACC-070: cancelling the delete step-up closes the box', async () => {
|
||||
@@ -1151,7 +1151,7 @@ describe('MSettingsAccount – passkeys', () => {
|
||||
await user.type(within(form).getByPlaceholderText('Current password'), 'my-password');
|
||||
await user.click(within(form).getByRole('button', { name: 'Add a passkey' }));
|
||||
|
||||
await screen.findByText('Passkey setup cancelled');
|
||||
expect(await screen.findByText('Passkey setup cancelled')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-MOB-SETACC-073: a rejected password step-up shows the server message', async () => {
|
||||
|
||||
@@ -110,7 +110,7 @@ describe('MSettingsAppearance', () => {
|
||||
renderAppearance();
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'Dark' }));
|
||||
await screen.findByText('Server down');
|
||||
expect(await screen.findByText('Server down')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-MOB-SETAPP-006: choosing a scheme applies it to the DOM and persists it debounced', async () => {
|
||||
@@ -146,7 +146,7 @@ describe('MSettingsAppearance', () => {
|
||||
renderAppearance();
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'Amber' }));
|
||||
await screen.findByText('Blob too large');
|
||||
expect(await screen.findByText('Blob too large')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-MOB-SETAPP-009: the custom scheme reveals the accent presets and the contrast badge', async () => {
|
||||
|
||||
@@ -142,7 +142,7 @@ describe('MSettingsGeneral', () => {
|
||||
render(<><ToastContainer /><MSettingsGeneral /></>);
|
||||
|
||||
await user.click(screen.getByRole('button', { name: '°F Fahrenheit' }));
|
||||
await screen.findByText('Error saving setting');
|
||||
expect(await screen.findByText('Error saving setting')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-MOB-SET-011: a non-Error rejection falls back to the generic message', async () => {
|
||||
@@ -154,7 +154,7 @@ describe('MSettingsGeneral', () => {
|
||||
render(<><ToastContainer /><MSettingsGeneral /></>);
|
||||
|
||||
await user.click(screen.getByRole('button', { name: '°F Fahrenheit' }));
|
||||
await screen.findByText('Error');
|
||||
expect(await screen.findByText('Error')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-MOB-SET-012: the startup card defaults to the dashboard and hides the tab row', () => {
|
||||
|
||||
@@ -123,7 +123,7 @@ describe('MSettingsPlugins', () => {
|
||||
server.use(http.get('/api/plugin-activity', () => HttpResponse.json({}, { status: 500 })));
|
||||
renderScreen();
|
||||
|
||||
await screen.findByText('No plugin activity yet.');
|
||||
expect(await screen.findByText('No plugin activity yet.')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-MOB-SETPLG-005: declared user fields render as the matching mobile controls', async () => {
|
||||
@@ -296,7 +296,7 @@ describe('MSettingsPlugins', () => {
|
||||
await screen.findByText('Success');
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'Probe' }));
|
||||
await screen.findByText('Error');
|
||||
expect(await screen.findByText('Error')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-MOB-SETPLG-014: unlabelled fields fall back to their key, hints render below', async () => {
|
||||
@@ -335,7 +335,7 @@ describe('MSettingsPlugins', () => {
|
||||
|
||||
await screen.findByText('Actions');
|
||||
await user.click(screen.getByRole('button', { name: 'Test connection' }));
|
||||
await screen.findByText('Error');
|
||||
expect(await screen.findByText('Error')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-MOB-SETPLG-016: a danger action asks for confirmation before it runs', async () => {
|
||||
|
||||
@@ -41,14 +41,21 @@ describe('filesSlice', () => {
|
||||
});
|
||||
|
||||
it('FE-FILES-002: loadFiles silently catches errors', async () => {
|
||||
const existing = buildTripFile({ trip_id: 1, filename: 'existing.pdf' });
|
||||
seedStore(useTripStore, { files: [existing] });
|
||||
|
||||
server.use(
|
||||
http.get('/api/trips/1/files', () =>
|
||||
HttpResponse.json({ message: 'Error' }, { status: 500 })
|
||||
),
|
||||
);
|
||||
|
||||
// Should not throw
|
||||
await useTripStore.getState().loadFiles(1);
|
||||
// Swallows the failure instead of rejecting, and leaves the list alone
|
||||
await expect(useTripStore.getState().loadFiles(1)).resolves.toBeUndefined();
|
||||
|
||||
const files = useTripStore.getState().files;
|
||||
expect(files).toHaveLength(1);
|
||||
expect(files[0].filename).toBe('existing.pdf');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1161,8 +1161,12 @@ describe('onPlaceDeleted', () => {
|
||||
const trip = createTrip(testDb, user.id, { title: 'Unlinked' });
|
||||
const place = createPlace(testDb, trip.id, { name: 'Nowhere' });
|
||||
|
||||
// Should not throw
|
||||
svc.onPlaceDeleted(place.id);
|
||||
expect(() => svc.onPlaceDeleted(place.id)).not.toThrow();
|
||||
|
||||
const orphaned = testDb.prepare(
|
||||
"SELECT COUNT(*) AS n FROM journey_entries WHERE source_place_id = ?"
|
||||
).get(place.id) as { n: number };
|
||||
expect(orphaned.n).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user