|
| 1 | +import { screen, waitFor } from '@testing-library/react'; |
| 2 | +import { act } from 'react'; |
| 3 | +import { Root, createRoot } from 'react-dom/client'; |
| 4 | +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; |
| 5 | +import { App } from '../src/app/App'; |
| 6 | +import { MOCK_USER_ID, setupTestDOM } from './test-setup'; |
| 7 | + |
| 8 | +// Get access to the PowerSync database instance exposed on window |
| 9 | +declare global { |
| 10 | + interface Window { |
| 11 | + _powersync: import('@powersync/web').PowerSyncDatabase; |
| 12 | + } |
| 13 | +} |
| 14 | + |
| 15 | +describe('TodoLists E2E', () => { |
| 16 | + let root: Root | null = null; |
| 17 | + let container: HTMLElement; |
| 18 | + |
| 19 | + beforeEach(async () => { |
| 20 | + // Set up DOM |
| 21 | + container = setupTestDOM(); |
| 22 | + await renderAppAndWaitForTodoListsScreen(); |
| 23 | + }); |
| 24 | + |
| 25 | + afterEach(async () => { |
| 26 | + // Cleanup |
| 27 | + if (root) { |
| 28 | + root.unmount(); |
| 29 | + root = null; |
| 30 | + } |
| 31 | + |
| 32 | + // Clean up the PowerSync database if it exists |
| 33 | + if (window._powersync) { |
| 34 | + try { |
| 35 | + await window._powersync.disconnectAndClear(); |
| 36 | + } catch (e) { |
| 37 | + // Ignore errors during cleanup |
| 38 | + } |
| 39 | + } |
| 40 | + }); |
| 41 | + |
| 42 | + /** |
| 43 | + * Helper to render app and wait for the Todo Lists screen to appear |
| 44 | + */ |
| 45 | + async function renderAppAndWaitForTodoListsScreen() { |
| 46 | + await act(async () => { |
| 47 | + root = createRoot(container); |
| 48 | + root.render(<App />); |
| 49 | + }); |
| 50 | + |
| 51 | + // Wait for PowerSync to be initialized |
| 52 | + await waitFor( |
| 53 | + () => { |
| 54 | + expect(window._powersync).toBeDefined(); |
| 55 | + }, |
| 56 | + { timeout: 10000 } |
| 57 | + ); |
| 58 | + |
| 59 | + // Wait for the Todo Lists screen to appear (app auto-navigates when authenticated) |
| 60 | + await waitFor( |
| 61 | + () => { |
| 62 | + expect(screen.getByText('Todo Lists')).toBeTruthy(); |
| 63 | + }, |
| 64 | + { timeout: 10000 } |
| 65 | + ); |
| 66 | + } |
| 67 | + |
| 68 | + /** |
| 69 | + * Helper to insert a list and wait for it to appear |
| 70 | + */ |
| 71 | + async function insertList(name: string) { |
| 72 | + const listId = crypto.randomUUID(); |
| 73 | + |
| 74 | + await act(async () => { |
| 75 | + await window._powersync.execute(`INSERT INTO lists (id, name, owner_id, created_at) VALUES (?, ?, ?, ?)`, [ |
| 76 | + listId, |
| 77 | + name, |
| 78 | + MOCK_USER_ID, |
| 79 | + new Date().toISOString() |
| 80 | + ]); |
| 81 | + }); |
| 82 | + |
| 83 | + return listId; |
| 84 | + } |
| 85 | + |
| 86 | + /** |
| 87 | + * Helper to insert a todo |
| 88 | + */ |
| 89 | + async function insertTodo(listId: string, description: string, completed: boolean = false) { |
| 90 | + const todoId = crypto.randomUUID(); |
| 91 | + |
| 92 | + await act(async () => { |
| 93 | + await window._powersync.execute( |
| 94 | + `INSERT INTO todos (id, list_id, description, created_by, created_at, completed, completed_at, completed_by) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, |
| 95 | + [ |
| 96 | + todoId, |
| 97 | + listId, |
| 98 | + description, |
| 99 | + MOCK_USER_ID, |
| 100 | + new Date().toISOString(), |
| 101 | + completed ? 1 : 0, |
| 102 | + completed ? new Date().toISOString() : null, |
| 103 | + completed ? MOCK_USER_ID : null |
| 104 | + ] |
| 105 | + ); |
| 106 | + }); |
| 107 | + |
| 108 | + return todoId; |
| 109 | + } |
| 110 | + |
| 111 | + it('should load the app and show the Todo Lists screen', async () => { |
| 112 | + // Verify we're on the Todo Lists page |
| 113 | + expect(screen.getByText('Todo Lists')).toBeTruthy(); |
| 114 | + }); |
| 115 | + |
| 116 | + it('should display a list widget after inserting a list via PowerSync SQL', async () => { |
| 117 | + const listName = 'My Shopping List'; |
| 118 | + await insertList(listName); |
| 119 | + |
| 120 | + // Wait for the list widget to render with our list name |
| 121 | + await waitFor( |
| 122 | + () => { |
| 123 | + expect(screen.getByText(listName)).toBeTruthy(); |
| 124 | + }, |
| 125 | + { timeout: 10000 } |
| 126 | + ); |
| 127 | + |
| 128 | + // Verify action buttons are present |
| 129 | + expect(screen.getByRole('button', { name: /delete/i })).toBeTruthy(); |
| 130 | + expect(screen.getByRole('button', { name: /proceed/i })).toBeTruthy(); |
| 131 | + }); |
| 132 | + |
| 133 | + it('should display multiple list widgets after inserting multiple lists', async () => { |
| 134 | + // Insert multiple lists |
| 135 | + await insertList('Groceries'); |
| 136 | + await insertList('Work Tasks'); |
| 137 | + await insertList('Personal Goals'); |
| 138 | + |
| 139 | + // Wait for all list widgets to render |
| 140 | + await waitFor( |
| 141 | + () => { |
| 142 | + expect(screen.getByText('Groceries')).toBeTruthy(); |
| 143 | + expect(screen.getByText('Work Tasks')).toBeTruthy(); |
| 144 | + expect(screen.getByText('Personal Goals')).toBeTruthy(); |
| 145 | + }, |
| 146 | + { timeout: 10000 } |
| 147 | + ); |
| 148 | + |
| 149 | + // Verify we have 3 delete buttons (one per list) |
| 150 | + const deleteButtons = screen.getAllByRole('button', { name: /delete/i }); |
| 151 | + expect(deleteButtons).toHaveLength(3); |
| 152 | + }); |
| 153 | + |
| 154 | + it('should display list with correct todo counts (pending and completed)', async () => { |
| 155 | + const listName = 'My Task List'; |
| 156 | + const listId = await insertList(listName); |
| 157 | + |
| 158 | + // Insert todos - 2 incomplete, 1 completed |
| 159 | + await insertTodo(listId, 'Buy groceries', false); |
| 160 | + await insertTodo(listId, 'Call mom', false); |
| 161 | + await insertTodo(listId, 'Finish report', true); |
| 162 | + |
| 163 | + // Wait for the list widget to render with correct stats |
| 164 | + await waitFor( |
| 165 | + () => { |
| 166 | + expect(screen.getByText(listName)).toBeTruthy(); |
| 167 | + // Should show "2 pending, 1 completed" in the description |
| 168 | + expect(screen.getByText(/2 pending/i)).toBeTruthy(); |
| 169 | + expect(screen.getByText(/1 completed/i)).toBeTruthy(); |
| 170 | + }, |
| 171 | + { timeout: 10000 } |
| 172 | + ); |
| 173 | + }); |
| 174 | + |
| 175 | + it('should render list widgets with delete and navigate action buttons', async () => { |
| 176 | + const listName = 'Test List'; |
| 177 | + await insertList(listName); |
| 178 | + |
| 179 | + // Wait for the list widget with action buttons |
| 180 | + await waitFor( |
| 181 | + () => { |
| 182 | + expect(screen.getByText(listName)).toBeTruthy(); |
| 183 | + expect(screen.getByRole('button', { name: /delete/i })).toBeTruthy(); |
| 184 | + expect(screen.getByRole('button', { name: /proceed/i })).toBeTruthy(); |
| 185 | + }, |
| 186 | + { timeout: 10000 } |
| 187 | + ); |
| 188 | + }); |
| 189 | + |
| 190 | + it('should display the floating action button to add new lists', async () => { |
| 191 | + // The FAB should be present - find by class |
| 192 | + const fab = document.querySelector('.MuiFab-root'); |
| 193 | + expect(fab).not.toBeNull(); |
| 194 | + }); |
| 195 | +}); |
0 commit comments