diff --git a/client/src/components/Admin/AddonManager.test.tsx b/client/src/components/Admin/AddonManager.test.tsx index cfffacead..14afcc882 100644 --- a/client/src/components/Admin/AddonManager.test.tsx +++ b/client/src/components/Admin/AddonManager.test.tsx @@ -98,7 +98,7 @@ describe('AddonManager', () => { it('FE-ADMIN-ADDON-002: empty state when addons list is empty', async () => { render(); - 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 () => { diff --git a/client/src/components/Admin/AdminMcpTokensPanel.test.tsx b/client/src/components/Admin/AdminMcpTokensPanel.test.tsx index 8abcd44da..edd016b1c 100644 --- a/client/src/components/Admin/AdminMcpTokensPanel.test.tsx +++ b/client/src/components/Admin/AdminMcpTokensPanel.test.tsx @@ -49,7 +49,7 @@ describe('AdminMcpTokensPanel', () => { it('FE-ADMIN-MCP-002: empty state rendered when no tokens', async () => { render(); - 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(<>); - 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(); - 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(); }); }); diff --git a/client/src/components/Admin/CategoryManager.test.tsx b/client/src/components/Admin/CategoryManager.test.tsx index d3e5173cb..945ea698d 100644 --- a/client/src/components/Admin/CategoryManager.test.tsx +++ b/client/src/components/Admin/CategoryManager.test.tsx @@ -27,17 +27,17 @@ describe('CategoryManager', () => { it('FE-COMP-CAT-002: shows Categories title', async () => { render(); - await screen.findByText('Categories'); + expect(await screen.findByText('Categories')).toBeInTheDocument(); }); it('FE-COMP-CAT-003: shows empty state when no categories', async () => { render(); - 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(); - 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(); - 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 () => { diff --git a/client/src/components/Admin/DevNotificationsPanel.test.tsx b/client/src/components/Admin/DevNotificationsPanel.test.tsx index 35932e52e..2499bf595 100644 --- a/client/src/components/Admin/DevNotificationsPanel.test.tsx +++ b/client/src/components/Admin/DevNotificationsPanel.test.tsx @@ -87,7 +87,7 @@ describe('DevNotificationsPanel', () => { render(<>); 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(<>); 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(<>); 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 () => { diff --git a/client/src/components/Admin/GitHubPanel.test.tsx b/client/src/components/Admin/GitHubPanel.test.tsx index 57b6d1315..b369d200f 100644 --- a/client/src/components/Admin/GitHubPanel.test.tsx +++ b/client/src/components/Admin/GitHubPanel.test.tsx @@ -231,7 +231,7 @@ describe('GitHubPanel', () => { render(); 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 () => { diff --git a/client/src/components/Admin/PackingTemplateManager.test.tsx b/client/src/components/Admin/PackingTemplateManager.test.tsx index 8abb51cde..293d60801 100644 --- a/client/src/components/Admin/PackingTemplateManager.test.tsx +++ b/client/src/components/Admin/PackingTemplateManager.test.tsx @@ -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 () => { diff --git a/client/src/components/Admin/PermissionsPanel.test.tsx b/client/src/components/Admin/PermissionsPanel.test.tsx index fb7323ecf..d53ab7464 100644 --- a/client/src/components/Admin/PermissionsPanel.test.tsx +++ b/client/src/components/Admin/PermissionsPanel.test.tsx @@ -269,6 +269,6 @@ describe('PermissionsPanel', () => { ), ); renderPanel(); - await screen.findByText('Error'); + expect(await screen.findByText('Error')).toBeInTheDocument(); }); }); diff --git a/client/src/components/Budget/BudgetPanel.test.tsx b/client/src/components/Budget/BudgetPanel.test.tsx index fa8e496d9..7844e766d 100644 --- a/client/src/components/Budget/BudgetPanel.test.tsx +++ b/client/src/components/Budget/BudgetPanel.test.tsx @@ -32,7 +32,7 @@ describe('BudgetPanel', () => { http.get('/api/trips/1/budget', () => HttpResponse.json({ items: [] })) ); render(); - 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(); - 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(); - 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(); - 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(); - 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(); - 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(); - 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(); 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(); // 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(); - 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(); 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(); - 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(); - 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(); 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(); 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(); 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 () => { diff --git a/client/src/components/Collab/CollabChat.test.tsx b/client/src/components/Collab/CollabChat.test.tsx index 34f6339e6..2f818b142 100644 --- a/client/src/components/Collab/CollabChat.test.tsx +++ b/client/src/components/Collab/CollabChat.test.tsx @@ -53,7 +53,7 @@ describe('CollabChat', () => { it('FE-COMP-CHAT-002: shows empty state when no messages', async () => { render(); - 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(); - 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 () => { diff --git a/client/src/components/Collab/CollabNotes.test.tsx b/client/src/components/Collab/CollabNotes.test.tsx index dcd9f9bbd..b18854590 100644 --- a/client/src/components/Collab/CollabNotes.test.tsx +++ b/client/src/components/Collab/CollabNotes.test.tsx @@ -47,7 +47,7 @@ describe('CollabNotes', () => { it('FE-COMP-NOTES-002: shows empty state when no notes', async () => { render(); - 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(); - 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(); - 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(); + // 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(); 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(); 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(); - 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(); - 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(); - 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(); - 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 () => { diff --git a/client/src/components/Collab/CollabPolls.test.tsx b/client/src/components/Collab/CollabPolls.test.tsx index 5840333aa..2981db8df 100644 --- a/client/src/components/Collab/CollabPolls.test.tsx +++ b/client/src/components/Collab/CollabPolls.test.tsx @@ -55,7 +55,7 @@ beforeEach(() => { describe('CollabPolls', () => { it('FE-COMP-POLLS-001: renders empty state when no polls exist', async () => { render(); - 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(); - 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(); - 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).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(); - 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(); - 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(); - 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 () => { diff --git a/client/src/components/Files/FileManager.test.tsx b/client/src/components/Files/FileManager.test.tsx index 478caf79f..0ff151f32 100644 --- a/client/src/components/Files/FileManager.test.tsx +++ b/client/src/components/Files/FileManager.test.tsx @@ -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(); // 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 () => { diff --git a/client/src/components/Layout/InAppNotificationBell.test.tsx b/client/src/components/Layout/InAppNotificationBell.test.tsx index 45ccea8bc..b9fe381d7 100644 --- a/client/src/components/Layout/InAppNotificationBell.test.tsx +++ b/client/src/components/Layout/InAppNotificationBell.test.tsx @@ -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(); 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(); 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(); 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', () => { diff --git a/client/src/components/Packing/PackingListPanel.test.tsx b/client/src/components/Packing/PackingListPanel.test.tsx index a86cb9d28..63be3f0b3 100644 --- a/client/src/components/Packing/PackingListPanel.test.tsx +++ b/client/src/components/Packing/PackingListPanel.test.tsx @@ -171,7 +171,7 @@ describe('PackingListPanel', () => { const user = userEvent.setup(); render(); 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(); // "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 () => { diff --git a/client/src/components/Planner/DayDetailPanel.test.tsx b/client/src/components/Planner/DayDetailPanel.test.tsx index f8ee30de8..564ec67cc 100644 --- a/client/src/components/Planner/DayDetailPanel.test.tsx +++ b/client/src/components/Planner/DayDetailPanel.test.tsx @@ -158,7 +158,7 @@ describe('DayDetailPanel', () => { ), ); render(); - 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(); - 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(); - 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(); - 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(); - 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(); - 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(); // 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(); - 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(); 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(); // 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(); - 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 () => { diff --git a/client/src/components/Planner/PlaceFormModal.test.tsx b/client/src/components/Planner/PlaceFormModal.test.tsx index 063614aca..3d5d45265 100644 --- a/client/src/components/Planner/PlaceFormModal.test.tsx +++ b/client/src/components/Planner/PlaceFormModal.test.tsx @@ -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 () => { diff --git a/client/src/components/Planner/PlaceInspector.test.tsx b/client/src/components/Planner/PlaceInspector.test.tsx index 75fb0a8eb..6c464cad0 100644 --- a/client/src/components/Planner/PlaceInspector.test.tsx +++ b/client/src/components/Planner/PlaceInspector.test.tsx @@ -335,7 +335,7 @@ describe('PlaceInspector', () => { } as any); const p = buildPlace({ id: 201, google_place_id: 'ChIJ002' }); render(); - 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(); - 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')); diff --git a/client/src/components/Settings/AccountTab.test.tsx b/client/src/components/Settings/AccountTab.test.tsx index 54a0abe2c..11bd84931 100644 --- a/client/src/components/Settings/AccountTab.test.tsx +++ b/client/src/components/Settings/AccountTab.test.tsx @@ -74,7 +74,7 @@ describe('AccountTab', () => { render(<>); 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(<>); 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(<>); 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(); }); }); diff --git a/client/src/components/Settings/AirTrailConnectionSection.test.tsx b/client/src/components/Settings/AirTrailConnectionSection.test.tsx index 753f7cf77..301d3ccbf 100644 --- a/client/src/components/Settings/AirTrailConnectionSection.test.tsx +++ b/client/src/components/Settings/AirTrailConnectionSection.test.tsx @@ -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 () => { diff --git a/client/src/components/Settings/AppearanceSettingsTab.test.tsx b/client/src/components/Settings/AppearanceSettingsTab.test.tsx index 61ddaa6de..3317a818a 100644 --- a/client/src/components/Settings/AppearanceSettingsTab.test.tsx +++ b/client/src/components/Settings/AppearanceSettingsTab.test.tsx @@ -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 () => { diff --git a/client/src/components/Settings/DisplaySettingsTab.test.tsx b/client/src/components/Settings/DisplaySettingsTab.test.tsx index 66d565e87..54f22de99 100644 --- a/client/src/components/Settings/DisplaySettingsTab.test.tsx +++ b/client/src/components/Settings/DisplaySettingsTab.test.tsx @@ -165,7 +165,7 @@ describe('DisplaySettingsTab', () => { seedStore(useSettingsStore, { settings: buildSettings({ temperature_unit: 'celsius' }), updateSetting }); render(<>); 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(); }); }); diff --git a/client/src/components/Settings/IntegrationsTab.test.tsx b/client/src/components/Settings/IntegrationsTab.test.tsx index 47ec0e49d..b60726f98 100644 --- a/client/src/components/Settings/IntegrationsTab.test.tsx +++ b/client/src/components/Settings/IntegrationsTab.test.tsx @@ -60,7 +60,7 @@ describe('IntegrationsTab', () => { it('FE-COMP-INTEGRATIONS-003: MCP section is visible when mcp addon is enabled', async () => { enableMcp(); render(); - 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(); 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 () => { diff --git a/client/src/components/Settings/MapSettingsTab.test.tsx b/client/src/components/Settings/MapSettingsTab.test.tsx index 706a45b13..867465fbf 100644 --- a/client/src/components/Settings/MapSettingsTab.test.tsx +++ b/client/src/components/Settings/MapSettingsTab.test.tsx @@ -134,7 +134,7 @@ describe('MapSettingsTab', () => { }); render(<>); 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', () => { diff --git a/client/src/components/Settings/NotificationsTab.test.tsx b/client/src/components/Settings/NotificationsTab.test.tsx index b234b318a..78720dc35 100644 --- a/client/src/components/Settings/NotificationsTab.test.tsx +++ b/client/src/components/Settings/NotificationsTab.test.tsx @@ -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(); }); }); diff --git a/client/src/components/Settings/PasskeysSection.test.tsx b/client/src/components/Settings/PasskeysSection.test.tsx index 634153438..a78f7e093 100644 --- a/client/src/components/Settings/PasskeysSection.test.tsx +++ b/client/src/components/Settings/PasskeysSection.test.tsx @@ -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 () => { diff --git a/client/src/components/Settings/PhotoProvidersSection.test.tsx b/client/src/components/Settings/PhotoProvidersSection.test.tsx index 67b32030f..2f4df8114 100644 --- a/client/src/components/Settings/PhotoProvidersSection.test.tsx +++ b/client/src/components/Settings/PhotoProvidersSection.test.tsx @@ -81,7 +81,7 @@ describe('PhotoProvidersSection', () => { it('FE-COMP-PHOTOPROVIDERS-003: renders a section card for each active provider', async () => { seedMemoriesEnabled(); render(); - 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(); - 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(); 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 () => { diff --git a/client/src/components/Todo/TodoListPanel.test.tsx b/client/src/components/Todo/TodoListPanel.test.tsx index 592d56d6e..5940024e8 100644 --- a/client/src/components/Todo/TodoListPanel.test.tsx +++ b/client/src/components/Todo/TodoListPanel.test.tsx @@ -42,7 +42,7 @@ describe('TodoListPanel', () => { it('FE-COMP-TODO-002: raising addItemSignal opens the new task form', async () => { const { rerender } = render(); rerender(); - 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(); rerender(); - 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(); 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', () => { diff --git a/client/src/components/Trips/TripFormModal.test.tsx b/client/src/components/Trips/TripFormModal.test.tsx index 75982b6cd..b144809fb 100644 --- a/client/src/components/Trips/TripFormModal.test.tsx +++ b/client/src/components/Trips/TripFormModal.test.tsx @@ -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(); - 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 () => { diff --git a/client/src/components/Trips/TripMembersModal.test.tsx b/client/src/components/Trips/TripMembersModal.test.tsx index cff2f05a6..d0dfb2e97 100644 --- a/client/src/components/Trips/TripMembersModal.test.tsx +++ b/client/src/components/Trips/TripMembersModal.test.tsx @@ -99,18 +99,18 @@ describe('TripMembersModal', () => { it('FE-COMP-MEMBERS-003: shows owner username after load', async () => { render(); - await screen.findByText('owner'); + expect(await screen.findByText('owner')).toBeInTheDocument(); }); it('FE-COMP-MEMBERS-004: shows Owner label', async () => { render(); - await screen.findByText('Owner'); + expect(await screen.findByText('Owner')).toBeInTheDocument(); }); it('FE-COMP-MEMBERS-005: shows Access section heading', async () => { render(); // 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(); - await screen.findByText('alice'); + expect(await screen.findByText('alice')).toBeInTheDocument(); }); it('FE-COMP-MEMBERS-007: shows Invite User section', async () => { render(); - await screen.findByText('Invite User'); + expect(await screen.findByText('Invite User')).toBeInTheDocument(); }); it('FE-COMP-MEMBERS-008: shows Invite button', async () => { render(); - 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(); // 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(); - 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(); // 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(); - 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(); - 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(); - 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 () => { diff --git a/client/tests/unit/mobile/admin/MAdminPackingTemplateManager.test.tsx b/client/tests/unit/mobile/admin/MAdminPackingTemplateManager.test.tsx index eb808e101..a73c6deb7 100644 --- a/client/tests/unit/mobile/admin/MAdminPackingTemplateManager.test.tsx +++ b/client/tests/unit/mobile/admin/MAdminPackingTemplateManager.test.tsx @@ -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 () => { diff --git a/client/tests/unit/mobile/settings/MAirTrailConnectionSection.test.tsx b/client/tests/unit/mobile/settings/MAirTrailConnectionSection.test.tsx index cd38dfb23..ef1d605a4 100644 --- a/client/tests/unit/mobile/settings/MAirTrailConnectionSection.test.tsx +++ b/client/tests/unit/mobile/settings/MAirTrailConnectionSection.test.tsx @@ -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(); }); }); diff --git a/client/tests/unit/mobile/settings/MLlmConnectionSection.test.tsx b/client/tests/unit/mobile/settings/MLlmConnectionSection.test.tsx index 056891da2..7a868666d 100644 --- a/client/tests/unit/mobile/settings/MLlmConnectionSection.test.tsx +++ b/client/tests/unit/mobile/settings/MLlmConnectionSection.test.tsx @@ -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', () => { diff --git a/client/tests/unit/mobile/settings/MSettingsAccount.test.tsx b/client/tests/unit/mobile/settings/MSettingsAccount.test.tsx index 050bb281c..850e8400a 100644 --- a/client/tests/unit/mobile/settings/MSettingsAccount.test.tsx +++ b/client/tests/unit/mobile/settings/MSettingsAccount.test.tsx @@ -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 () => { diff --git a/client/tests/unit/mobile/settings/MSettingsAppearance.test.tsx b/client/tests/unit/mobile/settings/MSettingsAppearance.test.tsx index bb39734d0..add74896b 100644 --- a/client/tests/unit/mobile/settings/MSettingsAppearance.test.tsx +++ b/client/tests/unit/mobile/settings/MSettingsAppearance.test.tsx @@ -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 () => { diff --git a/client/tests/unit/mobile/settings/MSettingsGeneral.test.tsx b/client/tests/unit/mobile/settings/MSettingsGeneral.test.tsx index d7fed651b..d1a7a99ed 100644 --- a/client/tests/unit/mobile/settings/MSettingsGeneral.test.tsx +++ b/client/tests/unit/mobile/settings/MSettingsGeneral.test.tsx @@ -142,7 +142,7 @@ describe('MSettingsGeneral', () => { render(<>); 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(<>); 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', () => { diff --git a/client/tests/unit/mobile/settings/MSettingsPlugins.test.tsx b/client/tests/unit/mobile/settings/MSettingsPlugins.test.tsx index b58f62d2c..c1e871d79 100644 --- a/client/tests/unit/mobile/settings/MSettingsPlugins.test.tsx +++ b/client/tests/unit/mobile/settings/MSettingsPlugins.test.tsx @@ -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 () => { diff --git a/client/tests/unit/slices/filesSlice.test.ts b/client/tests/unit/slices/filesSlice.test.ts index 7f5adc8c5..dce431680 100644 --- a/client/tests/unit/slices/filesSlice.test.ts +++ b/client/tests/unit/slices/filesSlice.test.ts @@ -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'); }); }); diff --git a/server/tests/unit/nest/journey-domain.service.test.ts b/server/tests/unit/nest/journey-domain.service.test.ts index a9c7f3373..b6d445715 100644 --- a/server/tests/unit/nest/journey-domain.service.test.ts +++ b/server/tests/unit/nest/journey-domain.service.test.ts @@ -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); }); });