diff --git a/web_app/package.json b/web_app/package.json index a1a96fbc..c81d3bb5 100644 --- a/web_app/package.json +++ b/web_app/package.json @@ -21,20 +21,25 @@ }, "devDependencies": { "@eslint/js": "^9.13.0", + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.0", "@types/node": "^22.13.5", "@types/react": "^18.3.12", "@types/react-dom": "^18.3.1", "@types/swagger-ui-react": "^4.18.3", "@vitejs/plugin-react": "^4.3.3", + "@vitest/ui": "^4.0.14", "autoprefixer": "^10.4.20", "eslint": "^9.13.0", "eslint-plugin-react-hooks": "^5.0.0", "eslint-plugin-react-refresh": "^0.4.14", "globals": "^15.11.0", + "jsdom": "^27.2.0", "postcss": "^8.5.1", "tailwindcss": "^3.4.17", "typescript": "~5.6.2", "typescript-eslint": "^8.11.0", - "vite": "^5.4.10" + "vite": "^7.2.4", + "vitest": "^4.0.14" } } diff --git a/web_app/src/Librarian/LibrarianAddBook.tsx b/web_app/src/Librarian/LibrarianAddBook.tsx index 4898b6c3..038eba0f 100644 --- a/web_app/src/Librarian/LibrarianAddBook.tsx +++ b/web_app/src/Librarian/LibrarianAddBook.tsx @@ -99,9 +99,7 @@ const LibrarianAddBook: React.FC = () => { if (!authorOptions.includes(author)) { try { const token = localStorage.getItem('access_token'); - if (!token) { - return; - } + if (!token) return; const response = await fetch(`${API_BASE_URL}/api/authors`, { method: 'POST', @@ -113,16 +111,26 @@ const LibrarianAddBook: React.FC = () => { }); if (!response.ok) { + if (response.status === 409) { + throw new Error("Ten autor już istnieje."); + } + const errorText = await response.text(); - throw new Error(`Błąd ${response.status}: ${errorText}`); + const finalMessage = errorText || response.statusText; + throw new Error(`Błąd ${response.status}: ${finalMessage}`); } await fetchAuthors(''); await fetchPublishers(''); + toast.success(`Dodano nowego autora: ${author}`); } catch (err) { console.error("Error adding author:", err); - setError('Wystąpił błąd podczas dodawania autora.'); + const msg = (err as Error).message || "Błąd dodawania autora"; + setError(msg); + toast.error(msg); + + setAuthors(prev => prev.filter(a => a !== author)); } } } @@ -166,14 +174,26 @@ const LibrarianAddBook: React.FC = () => { body: JSON.stringify({ name: publisherName }), }); - if (!response.ok) throw new Error(await response.text()); + if (!response.ok) { + if (response.status === 409) { + throw new Error("To wydawnictwo już istnieje."); + } + + const errorText = await response.text(); + const finalMessage = errorText || response.statusText; + throw new Error(`Błąd ${response.status}: ${finalMessage}`); + } await fetchPublishers(''); + toast.success(`Dodano nowe wydawnictwo: ${publisherName}`); } catch (err) { console.error("Error adding publisher:", err); + const msg = (err as Error).message || "Błąd dodawania wydawnictwa"; + + toast.error(msg); + setPublisher(''); } } - //setPublisherInput(''); setShowPublisherDropdown(false); }; @@ -187,7 +207,7 @@ const LibrarianAddBook: React.FC = () => { }; const handleAddBook = async () => { - if (!title.trim() || !categoryName.trim() || authors.length === 0 || !publisher.trim() || !isbn.trim() || !language.trim() || releaseYear === null) { + if (!title.trim() || !categoryName.trim() || authors.length === 0 || !publisher.trim() || !isbn.trim() || !language.trim() || releaseYear === '') { setError('Wszystkie pola są wymagane.'); return; } @@ -228,12 +248,34 @@ const LibrarianAddBook: React.FC = () => { if (!response.ok) { const errorText = await response.text(); - throw new Error(`Błąd ${response.status}: ${errorText}`); + + if (response.status === 409) { + throw new Error("Książka o podanym numerze ISBN już istnieje."); + } + + if (response.status === 400) { + throw new Error(errorText || "Nieprawidłowe dane formularza."); + } + + if (response.status === 401) { + throw new Error("Sesja wygasła. Proszę zalogować się ponownie."); + } + + if (response.status === 413) { + throw new Error("Wybrane zdjęcie jest zbyt duże."); + } + + // Code 500 and other unknown + const finalMessage = errorText || response.statusText || 'Unknown Server Error'; + throw new Error(`Błąd ${response.status}: ${finalMessage}`); } + toast.success("Książka została dodana!"); navigate('/librarian-dashboard'); } catch (error) { - setError((error as Error).message || 'Wystąpił błąd podczas dodawania książki.'); + const msg = (error as Error).message || 'Wystąpił błąd.'; + setError(msg); + toast.error(msg); } finally { setLoading(false); } diff --git a/web_app/src/LibraryAdmin/LibraryAdminAddLibrary.tsx b/web_app/src/LibraryAdmin/LibraryAdminAddLibrary.tsx index 0bf996e0..0c07bb86 100644 --- a/web_app/src/LibraryAdmin/LibraryAdminAddLibrary.tsx +++ b/web_app/src/LibraryAdmin/LibraryAdminAddLibrary.tsx @@ -24,6 +24,8 @@ const LibraryAdminAddLibrary: React.FC = () => { emailAddress: '', }); + const [errorMessage, setErrorMessage] = useState(''); + const handleLogout = () => { localStorage.removeItem('access_token'); localStorage.removeItem('role'); @@ -38,6 +40,7 @@ const LibraryAdminAddLibrary: React.FC = () => { const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); + setErrorMessage(''); const requestBody = { street: formData.addressLine, @@ -64,9 +67,11 @@ const LibraryAdminAddLibrary: React.FC = () => { navigate('/processing-info'); } else { console.error('Error submitting request', response.statusText); + setErrorMessage('Wystąpił błąd podczas wysyłania formularza.'); } } catch (error) { console.error('Error:', error); + setErrorMessage('Nie udało się połączyć z serwerem. Sprawdź swoje połączenie internetowe.'); } }; @@ -129,6 +134,11 @@ const LibraryAdminAddLibrary: React.FC = () => { Złóż podanie + {errorMessage && ( +

+ {errorMessage} +

+ )} diff --git a/web_app/src/Tests/VitestTests/Librarian/LibrarianAddBook.test.tsx b/web_app/src/Tests/VitestTests/Librarian/LibrarianAddBook.test.tsx new file mode 100644 index 00000000..cc757dbe --- /dev/null +++ b/web_app/src/Tests/VitestTests/Librarian/LibrarianAddBook.test.tsx @@ -0,0 +1,256 @@ +import { render, screen, fireEvent, waitFor } from '@testing-library/react'; +import { describe, it, expect, vi, beforeEach, type Mock } from 'vitest'; +import { BrowserRouter } from 'react-router-dom'; +import LibrarianAddBook from '../../../Librarian/LibrarianAddBook'; +import { toast } from 'react-toastify'; +import { useWebSocketNotification } from '../../../Utils/useWebSocketNotification'; + +const mocks = vi.hoisted(() => { + return { + navigate: vi.fn(), + wsCallback: vi.fn(), + }; +}); + +vi.mock('react-router-dom', async () => { + const actual = await vi.importActual('react-router-dom'); + return { + ...actual, + useNavigate: () => mocks.navigate, + }; +}); + +vi.mock('react-toastify', () => ({ + toast: { + info: vi.fn(), + error: vi.fn(), + success: vi.fn(), + warning: vi.fn(), + }, +})); + +vi.mock('../../../Utils/useWebSocketNotification', () => ({ + useWebSocketNotification: vi.fn(), +})); + +const fillFormAndSubmit = async () => { + fireEvent.change(screen.getByPlaceholderText('Tytuł'), { target: { value: 'Test Book' } }); + fireEvent.change(screen.getByPlaceholderText('ISBN'), { target: { value: '123-456' } }); + fireEvent.change(screen.getByPlaceholderText('Rok wydania'), { target: { value: '2023' } }); + + const pubInput = screen.getByPlaceholderText('Dodaj wydawnictwo'); + fireEvent.change(pubInput, { target: { value: 'Publisher 1' } }); + await waitFor(() => fireEvent.click(screen.getByText('Publisher 1'))); + + const authorInput = screen.getByPlaceholderText('Dodaj autora'); + fireEvent.change(authorInput, { target: { value: 'Author 1' } }); + await waitFor(() => fireEvent.click(screen.getByText('Author 1'))); + + await waitFor(() => { + fireEvent.change(screen.getByDisplayValue('Wybierz kategorię'), { target: { value: 'Fiction' } }); + }); + await waitFor(() => { + fireEvent.change(screen.getByDisplayValue('Wybierz język'), { target: { value: 'English' } }); + }); + + fireEvent.click(screen.getByText('Dodaj')); +}; + +const defaultFetch = (url: string | Request, options?: RequestInit) => { + const urlString = url.toString(); + + if (urlString.includes('/categories')) return Promise.resolve({ ok: true, json: async () => [{ id: 1, name: 'Fiction' }] }); + if (urlString.includes('/languages')) return Promise.resolve({ ok: true, json: async () => [{ id: 1, name: 'English' }] }); + if (urlString.includes('/authors')) return Promise.resolve({ ok: true, json: async () => [{ name: 'Author 1' }] }); + if (urlString.includes('/publishers')) return Promise.resolve({ ok: true, json: async () => [{ id: 1, name: 'Publisher 1' }] }); + + if (options?.method === 'POST' && urlString.includes('/books')) { + return Promise.resolve({ + ok: true, + json: async () => ({ success: true }), + }); + } + if (options?.method === 'POST') { + return Promise.resolve({ ok: true, json: async () => ({}) }); + } + + return Promise.resolve({ ok: true, json: async () => [] }); +}; + +beforeEach(() => { + vi.clearAllMocks(); + localStorage.setItem('access_token', 'mock-token'); + global.fetch = vi.fn(defaultFetch) as any; +}); + +describe('LibrarianAddBook', () => { + + it('renders all form fields', () => { + render(); + expect(screen.getByPlaceholderText('Tytuł')).toBeInTheDocument(); + }); + + it('shows validation error if required fields are empty', async () => { + render(); + fireEvent.click(screen.getByText('Dodaj')); + expect(await screen.findByText('Wszystkie pola są wymagane.')).toBeInTheDocument(); + }); + + it('submits the form successfully', async () => { + render(); + await fillFormAndSubmit(); + await waitFor(() => { + expect(mocks.navigate).toHaveBeenCalledWith('/librarian-dashboard'); + expect(toast.success).toHaveBeenCalledWith("Książka została dodana!"); + }); + }); + + it('triggers WebSocket toast notification', () => { + let triggerNotification: ((data: any) => void) | undefined; + (useWebSocketNotification as Mock).mockImplementation((_path, cb) => { + triggerNotification = cb; + }); + + render(); + + if (triggerNotification) triggerNotification({ }); + else throw new Error('Hook not called'); + + expect(toast.info).toHaveBeenCalledWith( + 'Otrzymano nowe zamówienie!', + expect.objectContaining({ position: 'bottom-right' }) + ); + }); + + it('displays error for Duplicate ISBN (409 Conflict)', async () => { + (global.fetch as Mock).mockImplementation(async (url, options) => { + if (options?.method === 'POST' && url.toString().includes('/books')) { + return { + ok: false, + status: 409, + statusText: 'Conflict', + text: async () => 'Ignored Text', + }; + } + return defaultFetch(url, options); + }); + + render(); + await fillFormAndSubmit(); + + expect(await screen.findByText("Książka o podanym numerze ISBN już istnieje.")).toBeInTheDocument(); + expect(toast.error).toHaveBeenCalledWith("Książka o podanym numerze ISBN już istnieje."); + }); + + it('displays error for Payload Too Large (413)', async () => { + (global.fetch as Mock).mockImplementation(async (url, options) => { + if (options?.method === 'POST' && url.toString().includes('/books')) { + return { ok: false, status: 413, statusText: 'Payload Too Large', text: async () => '' }; + } + return defaultFetch(url, options); + }); + + render(); + await fillFormAndSubmit(); + + expect(await screen.findByText("Wybrane zdjęcie jest zbyt duże.")).toBeInTheDocument(); + }); + + it('displays error for Unauthorized / Session Expired (401)', async () => { + (global.fetch as Mock).mockImplementation(async (url, options) => { + if (options?.method === 'POST' && url.toString().includes('/books')) { + return { ok: false, status: 401, statusText: 'Unauthorized', text: async () => '' }; + } + return defaultFetch(url, options); + }); + + render(); + await fillFormAndSubmit(); + + expect(await screen.findByText("Sesja wygasła. Proszę zalogować się ponownie.")).toBeInTheDocument(); + }); + + it('displays error for Bad Request (400)', async () => { + (global.fetch as Mock).mockImplementation(async (url, options) => { + if (options?.method === 'POST' && url.toString().includes('/books')) { + return { ok: false, status: 400, statusText: 'Bad Request', text: async () => 'Nieprawidłowy format ISBN' }; + } + return defaultFetch(url, options); + }); + + render(); + await fillFormAndSubmit(); + + expect(await screen.findByText("Nieprawidłowy format ISBN")).toBeInTheDocument(); + }); + + it('displays generic error for Server Crash (500)', async () => { + (global.fetch as Mock).mockImplementation(async (url, options) => { + if (options?.method === 'POST' && url.toString().includes('/books')) { + return { ok: false, status: 500, statusText: 'Internal Server Error', text: async () => '' }; + } + return defaultFetch(url, options); + }); + + render(); + await fillFormAndSubmit(); + + expect(await screen.findByText(/Błąd 500: Internal Server Error/i)).toBeInTheDocument(); + }); + + it('displays error when adding a duplicate Author (409)', async () => { + (global.fetch as Mock).mockImplementation(async (url, options) => { + const urlStr = url.toString(); + if (options?.method === 'POST' && urlStr.includes('/authors')) { + return { + ok: false, + status: 409, + statusText: 'Conflict', + text: async () => 'Conflict', + }; + } + return defaultFetch(url, options); + }); + + render(); + + const authorInput = screen.getByPlaceholderText('Dodaj autora'); + fireEvent.change(authorInput, { target: { value: 'New Author' } }); + + await waitFor(() => { + const addOption = screen.getByText('Dodaj "New Author"'); + fireEvent.click(addOption); + }); + + expect(toast.error).toHaveBeenCalledWith("Ten autor już istnieje."); + expect(await screen.findByText("Ten autor już istnieje.")).toBeInTheDocument(); + }); + + it('displays error when adding a duplicate Publisher (409)', async () => { + (global.fetch as Mock).mockImplementation(async (url, options) => { + const urlStr = url.toString(); + if (options?.method === 'POST' && urlStr.includes('/publishers')) { + return { + ok: false, + status: 409, + statusText: 'Conflict', + text: async () => 'Conflict', + }; + } + return defaultFetch(url, options); + }); + + render(); + + const pubInput = screen.getByPlaceholderText('Dodaj wydawnictwo'); + fireEvent.change(pubInput, { target: { value: 'New Publisher' } }); + + await waitFor(() => { + const addOption = screen.getByText('Dodaj "New Publisher"'); + fireEvent.click(addOption); + }); + + expect(toast.error).toHaveBeenCalledWith("To wydawnictwo już istnieje."); + }); + +}); diff --git a/web_app/src/Tests/VitestTests/Librarian/LibrarianHomePage.test.tsx b/web_app/src/Tests/VitestTests/Librarian/LibrarianHomePage.test.tsx new file mode 100644 index 00000000..6f91b542 --- /dev/null +++ b/web_app/src/Tests/VitestTests/Librarian/LibrarianHomePage.test.tsx @@ -0,0 +1,221 @@ +import { render, screen, fireEvent, waitFor } from '@testing-library/react'; +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import LibrarianHomePage from '../../../Librarian/LibrarianHomePage'; +import { BrowserRouter } from 'react-router-dom'; + +vi.mock('../Utils/useWebSocketNotification.tsx', () => ({ + useWebSocketNotification: vi.fn(), +})); + +vi.mock('react-toastify', () => ({ + toast: { info: vi.fn(), success: vi.fn(), error: vi.fn() }, +})); + +const mockedNavigate = vi.fn(); +vi.mock('react-router-dom', async () => { + const actual = await vi.importActual('react-router-dom'); + return { + ...actual, + useNavigate: () => mockedNavigate, + }; +}); + +vi.stubGlobal('import.meta', { env: { VITE_API_BASE_URL: 'http://localhost:8080' } }); + +const renderComponent = () => { + return render( + + + + ); +}; + +const setupFetchMock = () => { + global.fetch = vi.fn().mockImplementation((url: string, options: any) => { + if (url.includes('/libraries/assigned')) { + return Promise.resolve({ ok: true, json: () => Promise.resolve({ id: 99, name: 'Main Library' }) }); + } + if (url.includes('/api/categories')) { + return Promise.resolve({ ok: true, json: () => Promise.resolve([{ name: 'Fantasy' }]) }); + } + if (url.includes('/api/languages')) { + return Promise.resolve({ ok: true, json: () => Promise.resolve([{ name: 'Polish' }]) }); + } + + if (url.includes('/books/search?')) { + return Promise.resolve({ + ok: true, + json: () => Promise.resolve({ + content: [ + { + id: 101, + title: 'The Witcher', + authorNames: ['Sapkowski'], + releaseYear: 1993, + categoryName: 'Fantasy', + publisherName: 'SuperNowa', + isbn: '12345', + languageName: 'Polish', + image: 'img.jpg' + } + ], + last: true + }) + }); + } + + if (url.includes('/books/my-library') && options?.method === 'DELETE') { + return Promise.resolve({ + ok: true, + status: 200, + json: () => Promise.resolve({}) + }); + } + + if (url.includes('/add-existing') && options?.method === 'POST') { + return Promise.resolve({ ok: true, json: () => Promise.resolve({}) }); + } + + return Promise.resolve({ ok: true, json: () => Promise.resolve([]) }); + }) as any; +}; + +describe('LibrarianHomePage Integration Tests', () => { + + beforeEach(() => { + vi.clearAllMocks(); + setupFetchMock(); + Storage.prototype.getItem = vi.fn(() => 'fake_token'); + Storage.prototype.removeItem = vi.fn(); + }); + + describe('Initialization & Rendering', () => { + it('renders the dashboard and fetches initial library data', async () => { + renderComponent(); + + expect(screen.getByText('Wyszukaj książkę')).toBeInTheDocument(); + expect(screen.getByPlaceholderText('Tytuł')).toBeInTheDocument(); + + await waitFor(() => { + expect(global.fetch).toHaveBeenCalledWith( + expect.stringContaining('/libraries/assigned'), + expect.any(Object) + ); + expect(global.fetch).toHaveBeenCalledWith( + expect.stringContaining('/api/categories'), + expect.any(Object) + ); + }); + }); + }); + + describe('Search Functionality', () => { + it('performs a book search and renders results', async () => { + renderComponent(); + + const titleInput = screen.getByPlaceholderText('Tytuł'); + fireEvent.change(titleInput, { target: { value: 'Witcher' } }); + + const searchBtn = screen.getByRole('button', { name: /Szukaj/i }); + fireEvent.click(searchBtn); + + await waitFor(() => { + expect(screen.getByText('The Witcher (1993)')).toBeInTheDocument(); + expect(screen.getByText('Autor: Sapkowski')).toBeInTheDocument(); + }); + + expect(global.fetch).toHaveBeenCalledWith( + expect.stringContaining('title=Witcher'), + expect.objectContaining({ method: 'GET' }) + ); + }); + + it('handles empty search results gracefully', async () => { + (global.fetch as any).mockImplementationOnce((url: string) => { + if (url.includes('/search')) { + return Promise.resolve({ ok: true, json: () => Promise.resolve({ content: [], last: true }) }); + } + return Promise.resolve({ ok: true, json: () => Promise.resolve([]) }); + }); + + renderComponent(); + + const searchBtn = screen.getByRole('button', { name: /Szukaj/i }); + fireEvent.click(searchBtn); + + await waitFor(() => { + expect(screen.getByText('Nie znaleziono książki.')).toBeInTheDocument(); + }); + }); + }); + + describe('Library Management (Add/Delete)', () => { + it('allows adding a selected book to the library', async () => { + renderComponent(); + + fireEvent.click(screen.getByRole('button', { name: /Szukaj/i })); + const bookItem = await screen.findByText('The Witcher (1993)'); + fireEvent.click(bookItem); + + expect(screen.getByText(/Zaznaczone książki: 1/i)).toBeInTheDocument(); + const addBtn = screen.getByRole('button', { name: /Dodaj wybrane książki do biblioteki/i }); + fireEvent.click(addBtn); + + await waitFor(() => { + expect(global.fetch).toHaveBeenCalledWith( + expect.stringContaining('/api/books/add-existing/101?libraryId=99'), + expect.objectContaining({ method: 'POST' }) + ); + }); + + expect(await screen.findByText('Pomyślnie dodano książki do biblioteki.')).toBeInTheDocument(); + }); + + it('allows deleting a book from the library when "search in my library" is checked', async () => { + renderComponent(); + + const myLibCheckbox = screen.getByRole('checkbox'); + fireEvent.click(myLibCheckbox); + + fireEvent.click(screen.getByRole('button', { name: /Szukaj/i })); + + const bookItem = await screen.findByText('The Witcher (1993)'); + fireEvent.click(bookItem); + + const deleteBtn = screen.getByRole('button', { name: /Usuń wybrane książki/i }); + expect(deleteBtn).toBeInTheDocument(); + expect(screen.queryByText(/Dodaj wybrane/i)).not.toBeInTheDocument(); + + fireEvent.click(deleteBtn); + + await waitFor(() => { + expect(global.fetch).toHaveBeenCalledWith( + expect.stringContaining('/api/books/my-library/101'), + expect.objectContaining({ method: 'DELETE' }) + ); + }); + + expect(await screen.findByText('Wybrane książki zostały usunięte z biblioteki.')).toBeInTheDocument(); + }); + }); + + describe('Navigation & Auth', () => { + it('redirects to Add Book page', () => { + renderComponent(); + const link = screen.getByText('Dodaj nową książkę'); + fireEvent.click(link); + expect(mockedNavigate).toHaveBeenCalledWith('/add-book'); + }); + + it('handles logout correctly', () => { + renderComponent(); + + const logoutBtn = screen.getByRole('button', { name: /Wyloguj się/i }); + fireEvent.click(logoutBtn); + + expect(localStorage.removeItem).toHaveBeenCalledWith('access_token'); + expect(localStorage.removeItem).toHaveBeenCalledWith('role'); + expect(mockedNavigate).toHaveBeenCalledWith('/'); + }); + }); +}); diff --git a/web_app/src/Tests/VitestTests/Librarian/LibrarianOrders.test.tsx b/web_app/src/Tests/VitestTests/Librarian/LibrarianOrders.test.tsx new file mode 100644 index 00000000..e9f3617c --- /dev/null +++ b/web_app/src/Tests/VitestTests/Librarian/LibrarianOrders.test.tsx @@ -0,0 +1,177 @@ +import { render, screen, fireEvent, waitFor } from '@testing-library/react'; +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import LibrarianOrders from '../../../Librarian/LibrarianOrders'; +import { BrowserRouter } from 'react-router-dom'; + +vi.mock('../Utils/useWebSocketNotification.tsx', () => ({ + useWebSocketNotification: vi.fn(), +})); + +vi.mock('react-toastify', () => ({ + toast: { info: vi.fn(), success: vi.fn(), error: vi.fn() }, +})); + +const mockedNavigate = vi.fn(); +vi.mock('react-router-dom', async () => { + const actual = await vi.importActual('react-router-dom'); + return { + ...actual, + useNavigate: () => mockedNavigate, + }; +}); + +vi.stubGlobal('import.meta', { env: { VITE_API_BASE_URL: 'http://localhost:8080' } }); + +const createMockOrder = (id: number, status: string) => ({ + orderId: id, + userId: 'user-123', + status: status, + createdAt: new Date().toISOString(), + orderItems: [ + { + quantity: 1, + book: { + id: 101, + title: `Book Title ${id}`, + authorNames: ['Author A'], + image: 'img.jpg', + isbn: '123', + categoryName: 'Fiction', + releaseYear: 2020, + publisherName: 'Pub', + languageName: 'PL' + } + } + ], + driverId: '' +}); + +const setupFetchMock = () => { + global.fetch = vi.fn().mockImplementation((url: string, options: any) => { + const method = options?.method || 'GET'; + + if (method === 'GET') { + if (url.includes('/pending')) return Promise.resolve({ ok: true, json: () => Promise.resolve({ content: [createMockOrder(1, 'PENDING')] }) }); + if (url.includes('/in-realization')) return Promise.resolve({ ok: true, json: () => Promise.resolve({ content: [createMockOrder(2, 'IN_REALIZATION')] }) }); + if (url.includes('/completed')) return Promise.resolve({ ok: true, json: () => Promise.resolve({ content: [createMockOrder(3, 'COMPLETED')] }) }); + } + + if ((url.includes('/accept') || url.includes('/decline')) && method === 'PATCH') { + return Promise.resolve({ ok: true, status: 200, json: () => Promise.resolve({}) }); + } + if (url.includes('/handover') && method === 'PUT') { + return Promise.resolve({ ok: true, status: 200, json: () => Promise.resolve({}) }); + } + + return Promise.resolve({ ok: false, status: 404 }); + }) as any; +}; + +describe('LibrarianOrders Integration Tests', () => { + + beforeEach(() => { + vi.clearAllMocks(); + setupFetchMock(); + Storage.prototype.getItem = vi.fn(() => 'fake_token'); + }); + + const renderComponent = () => { + render( + + + + ); + }; + + it('renders and segregates orders into correct columns based on status', async () => { + renderComponent(); + await waitFor(() => { + expect(screen.getByText('Book Title 1')).toBeInTheDocument(); + expect(screen.getByText('Book Title 2')).toBeInTheDocument(); + expect(screen.getByText('Book Title 3')).toBeInTheDocument(); + }); + }); + + it('successfully accepts a pending order', async () => { + renderComponent(); + await waitFor(() => expect(screen.getByText('Book Title 1')).toBeInTheDocument()); + + const acceptBtns = screen.getAllByRole('button', { name: /Zatwierdź/i }); + fireEvent.click(acceptBtns[0]); + + await waitFor(() => { + expect(global.fetch).toHaveBeenCalledWith( + expect.stringContaining('/api/orders/1/accept'), + expect.objectContaining({ method: 'PATCH' }) + ); + }); + }); + + it('handles the rejection flow with a selected reason', async () => { + renderComponent(); + await waitFor(() => expect(screen.getByText('Book Title 1')).toBeInTheDocument()); + + const rejectBtns = screen.getAllByRole('button', { name: /Odrzuć/i }); + fireEvent.click(rejectBtns[0]); + + const reasonRadio = await screen.findByLabelText(/brak w zbiorach biblioteki/i); + fireEvent.click(reasonRadio); + + const confirmBtn = screen.getByRole('button', { name: /Potwierdź odrzucenie/i }); + fireEvent.click(confirmBtn); + + await waitFor(() => { + expect(global.fetch).toHaveBeenCalledWith( + expect.stringContaining('/api/orders/1/decline'), + expect.objectContaining({ + method: 'PATCH', + body: JSON.stringify({ reason: 'Brak w zbiorach biblioteki' }) + }) + ); + }); + + await waitFor(() => { + expect(screen.queryByText('Wybierz przyczynę odmowy:')).not.toBeInTheDocument(); + }); + }); + + it('handles the handover flow by assigning a driver', async () => { + renderComponent(); + await waitFor(() => expect(screen.getByText('Book Title 2')).toBeInTheDocument()); + + const finishOrderBtn = screen.getByRole('button', { name: /Zakończ zamówienie/i }); + fireEvent.click(finishOrderBtn); + + const driverInput = await screen.findByPlaceholderText(/Wprowadź ID kierowcy/i); + fireEvent.change(driverInput, { target: { value: 'Driver-X' } }); + + const handoverBtn = screen.getByRole('button', { name: /Przekaż książkę/i }); + fireEvent.click(handoverBtn); + + await waitFor(() => { + expect(global.fetch).toHaveBeenCalledWith( + expect.stringContaining('/api/orders/2/handover?driverId=Driver-X'), + expect.objectContaining({ method: 'PUT' }) + ); + }); + + expect(await screen.findByText(/zostało przekazane pomyślnie/i)).toBeInTheDocument(); + }); + + it('validates input before allowing handover', async () => { + renderComponent(); + await waitFor(() => expect(screen.getByText('Book Title 2')).toBeInTheDocument()); + + fireEvent.click(screen.getByRole('button', { name: /Zakończ zamówienie/i })); + + const handoverBtn = screen.getByRole('button', { name: /Przekaż książkę/i }); + fireEvent.click(handoverBtn); + + expect(await screen.findByText('Proszę wprowadzić ID kierowcy.')).toBeInTheDocument(); + + expect(global.fetch).not.toHaveBeenCalledWith( + expect.stringContaining('/handover'), + expect.anything() + ); + }); +}); diff --git a/web_app/src/Tests/VitestTests/Librarian/LibrarianReaders.test.tsx b/web_app/src/Tests/VitestTests/Librarian/LibrarianReaders.test.tsx new file mode 100644 index 00000000..60749753 --- /dev/null +++ b/web_app/src/Tests/VitestTests/Librarian/LibrarianReaders.test.tsx @@ -0,0 +1,228 @@ +import { render, screen, fireEvent, waitFor } from '@testing-library/react'; +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import LibrarianReaders from '../../../Librarian/LibrarianReaders'; +import { BrowserRouter } from 'react-router-dom'; + +vi.mock('../Utils/useWebSocketNotification.tsx', () => ({ + useWebSocketNotification: vi.fn(), +})); + +vi.mock('react-toastify', () => ({ + toast: { info: vi.fn(), success: vi.fn(), error: vi.fn() }, +})); + +const mockedNavigate = vi.fn(); +vi.mock('react-router-dom', async () => { + const actual = await vi.importActual('react-router-dom'); + return { + ...actual, + useNavigate: () => mockedNavigate, + }; +}); + +vi.stubGlobal('import.meta', { env: { VITE_API_BASE_URL: 'http://localhost:8080' } }); + +const mockReaderDetails = { + userId: '1001', + cardId: 'CARD-555', + firstName: 'John', + lastName: 'Doe', + expirationDate: '2030-12-31' +}; + +const setupFetchMock = () => { + global.fetch = vi.fn().mockImplementation((url: string, options: any) => { + const method = options?.method || 'GET'; + + if (url.includes('/api/library-cards') && method === 'POST') { + return Promise.resolve({ + ok: true, + status: 201, + json: () => Promise.resolve({}) + }); + } + + if (url.includes('/api/library-cards/') && method === 'GET') { + return Promise.resolve({ + ok: true, + status: 200, + json: () => Promise.resolve([mockReaderDetails]) + }); + } + + return Promise.resolve({ ok: false, status: 404 }); + }) as any; +}; + +describe('LibrarianReaders Integration Tests', () => { + + const alertMock = vi.spyOn(window, 'alert').mockImplementation(() => {}); + + beforeEach(() => { + vi.clearAllMocks(); + setupFetchMock(); + Storage.prototype.getItem = vi.fn((key) => { + if (key === 'access_token') return 'fake_token'; + return null; + }); + Storage.prototype.removeItem = vi.fn(); + }); + + const renderComponent = () => { + render( + + + + ); + }; + + it('renders the creation form and search form correctly', () => { + renderComponent(); + + expect(screen.getByText('Dodaj nowego użytkownika BookRider:')).toBeInTheDocument(); + expect(screen.getByLabelText(/^Identyfikator użytkownika:/i)).toBeInTheDocument(); + expect(screen.getByLabelText(/Identyfikator karty/i)).toBeInTheDocument(); + expect(screen.getByText('Wyszukaj użytkownika:')).toBeInTheDocument(); + }); + + it('successfully creates a library card and resets the form', async () => { + renderComponent(); + + fireEvent.change(screen.getByLabelText(/^Identyfikator użytkownika:/i), { target: { value: 'user-123' } }); + fireEvent.change(screen.getByLabelText(/Identyfikator karty/i), { target: { value: 'card-999' } }); + fireEvent.change(screen.getByLabelText(/Imię/i), { target: { value: 'Alice' } }); + fireEvent.change(screen.getByLabelText(/Nazwisko/i), { target: { value: 'Smith' } }); + fireEvent.change(screen.getByLabelText(/Data ważności/i), { target: { value: '2030-01-01' } }); + + const addBtn = screen.getByRole('button', { name: /^Dodaj$/i }); + fireEvent.click(addBtn); + + await waitFor(() => { + expect(global.fetch).toHaveBeenCalledWith( + expect.stringContaining('/api/library-cards'), + expect.objectContaining({ + method: 'POST', + body: JSON.stringify({ + userId: 'user-123', + cardId: 'card-999', + firstName: 'Alice', + lastName: 'Smith', + expirationDate: '2030-01-01' + }) + }) + ); + }); + + expect(alertMock).toHaveBeenCalledWith('Library card created successfully'); + + expect(screen.getByLabelText(/^Identyfikator użytkownika:/i)).toHaveValue(''); + expect(screen.getByLabelText(/Imię/i)).toHaveValue(''); + }); + + it('searches for a library card and displays the results', async () => { + renderComponent(); + + const searchInput = screen.getByLabelText(/Wprowadź identyfikator użytkownika/i); + fireEvent.change(searchInput, { target: { value: '1001' } }); + + const searchBtn = screen.getByRole('button', { name: /Wyszukaj/i }); + fireEvent.click(searchBtn); + + await waitFor(() => { + expect(global.fetch).toHaveBeenCalledWith( + expect.stringContaining('/api/library-cards/1001'), + expect.objectContaining({ method: 'GET' }) + ); + }); + + expect(await screen.findByText('Szczegóły wyszukiwanego konta:')).toBeInTheDocument(); + + expect(screen.getByText('John')).toBeInTheDocument(); + expect(screen.getByText('Doe')).toBeInTheDocument(); + expect(screen.getByText('CARD-555')).toBeInTheDocument(); + }); + + it('handles missing token scenario gracefully', async () => { + vi.spyOn(Storage.prototype, 'getItem').mockReturnValue(null); + const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); + + renderComponent(); + + fireEvent.click(screen.getByRole('button', { name: /^Dodaj$/i })); + + expect(global.fetch).not.toHaveBeenCalled(); + expect(consoleSpy).toHaveBeenCalledWith('No token found'); + }); + + it('handles logout correctly', () => { + renderComponent(); + + const logoutBtn = screen.getByRole('button', { name: /Wyloguj się/i }); + fireEvent.click(logoutBtn); + + expect(localStorage.removeItem).toHaveBeenCalledWith('access_token'); + expect(mockedNavigate).toHaveBeenCalledWith('/'); + }); + + it('handles failure when linking card to user (e.g. User ID not found or Card conflict)', async () => { + const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); + const alertMock = vi.spyOn(window, 'alert').mockImplementation(() => {}); + + vi.spyOn(global, 'fetch').mockResolvedValueOnce({ + ok: false, + status: 400, + json: async () => ({ message: "User not found or Card ID exists" }) + } as Response); + + renderComponent(); + + fireEvent.change(screen.getByLabelText(/^Identyfikator użytkownika:/i), { target: { value: 'invalid-user-id' } }); + fireEvent.change(screen.getByLabelText(/Identyfikator karty/i), { target: { value: 'duplicate-card' } }); + fireEvent.change(screen.getByLabelText(/Imię/i), { target: { value: 'John' } }); + + fireEvent.click(screen.getByRole('button', { name: /^Dodaj$/i })); + + await waitFor(() => { + expect(global.fetch).toHaveBeenCalled(); + }); + + expect(alertMock).not.toHaveBeenCalled(); + + expect(consoleSpy).toHaveBeenCalledWith( + 'Error creating library card: ', + expect.any(Error) + ); + + expect(screen.getByLabelText(/^Identyfikator użytkownika:/i)).toHaveValue('invalid-user-id'); + }); + + it('handles search failure (User/Card not found)', async () => { + const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); + + vi.spyOn(global, 'fetch').mockResolvedValueOnce({ + ok: false, + status: 404, + json: async () => ({}) + } as Response); + + renderComponent(); + + const searchInput = screen.getByLabelText(/Wprowadź identyfikator użytkownika/i); + fireEvent.change(searchInput, { target: { value: 'non-existent-id' } }); + + const searchBtn = screen.getByRole('button', { name: /Wyszukaj/i }); + fireEvent.click(searchBtn); + + await waitFor(() => { + expect(global.fetch).toHaveBeenCalled(); + }); + + expect(consoleSpy).toHaveBeenCalledWith( + 'Error searching for library card: ', + expect.any(Error) + ); + + expect(screen.queryByText('Szczegóły wyszukiwanego konta:')).not.toBeInTheDocument(); + expect(screen.queryByText('ID karty:')).not.toBeInTheDocument(); + }); +}); diff --git a/web_app/src/Tests/VitestTests/Librarian/LibrarianReturns.test.tsx b/web_app/src/Tests/VitestTests/Librarian/LibrarianReturns.test.tsx new file mode 100644 index 00000000..74d65e49 --- /dev/null +++ b/web_app/src/Tests/VitestTests/Librarian/LibrarianReturns.test.tsx @@ -0,0 +1,204 @@ +import { render, screen, fireEvent, waitFor } from '@testing-library/react'; +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import LibrarianReturns from '../../../Librarian/LibrarianReturns'; +import { BrowserRouter } from 'react-router-dom'; + +vi.mock('../Utils/useWebSocketNotification.tsx', () => ({ + useWebSocketNotification: vi.fn(), +})); + +vi.mock('react-toastify', () => ({ + toast: { info: vi.fn(), success: vi.fn(), error: vi.fn() }, +})); + +const mockedNavigate = vi.fn(); +vi.mock('react-router-dom', async () => { + const actual = await vi.importActual('react-router-dom'); + return { + ...actual, + useNavigate: () => mockedNavigate, + }; +}); + +vi.stubGlobal('import.meta', { env: { VITE_API_BASE_URL: 'http://localhost:8080' } }); + +const mockReturnDetails = { + id: 101, + orderId: 500, + status: 'IN_PROGRESS', + rentalReturnItems: [ + { + id: 1, + rentalId: 99, + returnedQuantity: 1, + book: { + id: 10, + title: 'Clean Code', + authorNames: ['Robert Martin'], + isbn: '978-0132350884', + categoryName: 'Tech', + image: 'img.jpg' + } + } + ] +}; + +const setupFetchMock = () => { + global.fetch = vi.fn().mockImplementation((url: string, options: any) => { + const method = options?.method || 'GET'; + + if (method === 'GET') { + if (url.includes('/api/rental-returns/101')) { + return Promise.resolve({ + ok: true, + json: () => Promise.resolve(mockReturnDetails) + }); + } + if (url.includes('/api/rental-returns/latest-by-driver/DRIVER-1')) { + return Promise.resolve({ + ok: true, + json: () => Promise.resolve(mockReturnDetails) + }); + } + return Promise.resolve({ ok: false, status: 404 }); + } + + if (method === 'PATCH') { + if (url.includes('/complete-in-person')) { + return Promise.resolve({ ok: true, json: () => Promise.resolve({}) }); + } + if (url.includes('/complete-delivery')) { + return Promise.resolve({ ok: true, json: () => Promise.resolve({}) }); + } + } + + return Promise.resolve({ ok: false, status: 500 }); + }) as any; +}; + +describe('LibrarianReturns Integration Tests', () => { + beforeEach(() => { + vi.clearAllMocks(); + setupFetchMock(); + Storage.prototype.getItem = vi.fn(() => 'fake_token'); + Storage.prototype.removeItem = vi.fn(); + }); + + const renderComponent = () => { + render( + + + + ); + }; + + it('renders return type toggles and switches inputs correctly', () => { + renderComponent(); + + expect(screen.getByText('Zwrot osobiście')).toBeInTheDocument(); + expect(screen.getByLabelText(/ID zwrotu/i)).toBeInTheDocument(); + expect(screen.queryByLabelText(/ID kierowcy/i)).not.toBeInTheDocument(); + + fireEvent.click(screen.getByText('Zwrot przez kierowcę')); + + expect(screen.getByLabelText(/ID kierowcy/i)).toBeInTheDocument(); + expect(screen.queryByLabelText(/ID zwrotu/i)).not.toBeInTheDocument(); + }); + + it('successfully processes an "In-Person" return flow', async () => { + renderComponent(); + + fireEvent.change(screen.getByLabelText(/ID zwrotu/i), { target: { value: '101' } }); + + const fetchBtn = screen.getByRole('button', { name: /^Zwróć$/i }); + fireEvent.click(fetchBtn); + + await waitFor(() => { + expect(global.fetch).toHaveBeenCalledWith( + expect.stringContaining('/api/rental-returns/101'), + expect.objectContaining({ method: 'GET' }) + ); + expect(screen.getByText('Clean Code')).toBeInTheDocument(); + expect(screen.getByText(/Robert Martin/)).toBeInTheDocument(); + }); + + const confirmBtn = screen.getByText('Zatwierdź zwrot'); + fireEvent.click(confirmBtn); + + await waitFor(() => { + expect(global.fetch).toHaveBeenCalledWith( + expect.stringContaining('/api/rental-returns/101/complete-in-person'), + expect.objectContaining({ method: 'PATCH' }) + ); + expect(screen.getByText('Zwrot zakończony pomyślnie.')).toBeInTheDocument(); + }); + }); + + it('successfully processes a "Driver" return flow', async () => { + renderComponent(); + + fireEvent.click(screen.getByText('Zwrot przez kierowcę')); + + fireEvent.change(screen.getByLabelText(/ID kierowcy/i), { target: { value: 'DRIVER-1' } }); + + const fetchBtn = screen.getByRole('button', { name: /^Zwróć$/i }); + fireEvent.click(fetchBtn); + + await waitFor(() => { + expect(global.fetch).toHaveBeenCalledWith( + expect.stringContaining('/api/rental-returns/latest-by-driver/DRIVER-1'), + expect.objectContaining({ method: 'GET' }) + ); + expect(screen.getByText('Clean Code')).toBeInTheDocument(); + }); + + const confirmBtn = screen.getByText('Zatwierdź zwrot'); + fireEvent.click(confirmBtn); + + await waitFor(() => { + expect(global.fetch).toHaveBeenCalledWith( + expect.stringContaining('/api/rental-returns/101/complete-delivery'), + expect.objectContaining({ method: 'PATCH' }) + ); + expect(screen.getByText('Zwrot przez kierowcę zakończony pomyślnie.')).toBeInTheDocument(); + }); + }); + + it('validates empty inputs before fetching', async () => { + renderComponent(); + + const fetchBtn = screen.getByRole('button', { name: /^Zwróć$/i }); + fireEvent.click(fetchBtn); + + expect(screen.getByText('Nieprawidłowe ID zwrotu')).toBeInTheDocument(); + expect(global.fetch).not.toHaveBeenCalled(); + + fireEvent.click(screen.getByText('Zwrot przez kierowcę')); + fireEvent.click(screen.getByRole('button', { name: /^Zwróć$/i })); + + expect(screen.getByText('Nieprawidłowe ID kierowcy')).toBeInTheDocument(); + expect(global.fetch).not.toHaveBeenCalled(); + }); + + it('handles API errors gracefully (e.g. Return Not Found)', async () => { + renderComponent(); + + fireEvent.change(screen.getByLabelText(/ID zwrotu/i), { target: { value: '999' } }); + fireEvent.click(screen.getByRole('button', { name: /^Zwróć$/i })); + + await waitFor(() => { + expect(screen.getByText('Wystąpił błąd podczas pobierania danych.')).toBeInTheDocument(); + }); + + expect(screen.queryByText('Szczegóły zwrotu:')).not.toBeInTheDocument(); + }); + + it('handles logout correctly', () => { + renderComponent(); + + fireEvent.click(screen.getByRole('button', { name: /Wyloguj się/i })); + + expect(localStorage.removeItem).toHaveBeenCalledWith('access_token'); + expect(mockedNavigate).toHaveBeenCalledWith('/'); + }); +}); diff --git a/web_app/src/Tests/VitestTests/LibraryAdmin/LibraryAdminAddLibrarian.test.tsx b/web_app/src/Tests/VitestTests/LibraryAdmin/LibraryAdminAddLibrarian.test.tsx new file mode 100644 index 00000000..89be28e1 --- /dev/null +++ b/web_app/src/Tests/VitestTests/LibraryAdmin/LibraryAdminAddLibrarian.test.tsx @@ -0,0 +1,122 @@ +import { render, screen, fireEvent, waitFor } from '@testing-library/react'; +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import LibraryAdminHomePage from '../../../LibraryAdmin/LibraryAdminAddLibrarian'; +import { BrowserRouter } from 'react-router-dom'; + +const mockedNavigate = vi.fn(); +vi.mock('react-router-dom', async () => { + const actual = await vi.importActual('react-router-dom'); + return { + ...actual, + useNavigate: () => mockedNavigate, + }; +}); + +vi.stubGlobal('import.meta', { env: { VITE_API_BASE_URL: 'http://localhost:8080' } }); + +describe('LibraryAdminHomePage Integration Tests', () => { + + beforeEach(() => { + vi.clearAllMocks(); + + Storage.prototype.getItem = vi.fn((key) => { + if (key === 'access_token') return 'fake_admin_token'; + return null; + }); + Storage.prototype.removeItem = vi.fn(); + + global.fetch = vi.fn(); + }); + + const renderComponent = () => { + render( + + + + ); + }; + + it('renders the form inputs and allows user typing', () => { + renderComponent(); + + expect(screen.getByPlaceholderText('Nazwa użytkownika')).toBeInTheDocument(); + expect(screen.getByPlaceholderText('Imię')).toBeInTheDocument(); + expect(screen.getByPlaceholderText('Nazwisko')).toBeInTheDocument(); + expect(screen.getByRole('button', { name: /Dodaj bibliotekarza/i })).toBeInTheDocument(); + + const usernameInput = screen.getByPlaceholderText('Nazwa użytkownika'); + fireEvent.change(usernameInput, { target: { value: 'new_librarian' } }); + expect(usernameInput).toHaveValue('new_librarian'); + }); + + it('successfully adds a librarian and resets the form', async () => { + vi.spyOn(global, 'fetch').mockResolvedValueOnce({ + ok: true, + status: 201, + json: async () => ({}) + } as Response); + + renderComponent(); + + fireEvent.change(screen.getByPlaceholderText('Nazwa użytkownika'), { target: { value: 'lib_john' } }); + fireEvent.change(screen.getByPlaceholderText('Imię'), { target: { value: 'John' } }); + fireEvent.change(screen.getByPlaceholderText('Nazwisko'), { target: { value: 'Doe' } }); + + const addBtn = screen.getByRole('button', { name: /Dodaj bibliotekarza/i }); + fireEvent.click(addBtn); + + await waitFor(() => { + expect(global.fetch).toHaveBeenCalledWith( + expect.stringContaining('/api/library-admins/librarians'), + expect.objectContaining({ + method: 'POST', + headers: expect.objectContaining({ + 'Authorization': 'Bearer fake_admin_token', + 'Content-Type': 'application/json' + }), + body: JSON.stringify({ + username: 'lib_john', + firstName: 'John', + lastName: 'Doe' + }) + }) + ); + }); + + expect(await screen.findByText('Dodano bibliotekarza.')).toBeInTheDocument(); + + expect(screen.getByPlaceholderText('Nazwa użytkownika')).toHaveValue(''); + expect(screen.getByPlaceholderText('Imię')).toHaveValue(''); + expect(screen.getByPlaceholderText('Nazwisko')).toHaveValue(''); + }); + + it('handles server errors gracefully (e.g., username taken)', async () => { + vi.spyOn(global, 'fetch').mockResolvedValueOnce({ + ok: false, + status: 400, + json: async () => ({ message: "Username exists" }) + } as Response); + + renderComponent(); + + fireEvent.change(screen.getByPlaceholderText('Nazwa użytkownika'), { target: { value: 'duplicate_user' } }); + + fireEvent.click(screen.getByRole('button', { name: /Dodaj bibliotekarza/i })); + + expect(await screen.findByText('Nie udało się dodać bibliotekarza.')).toBeInTheDocument(); + + expect(screen.getByPlaceholderText('Nazwa użytkownika')).toHaveValue('duplicate_user'); + }); + + it('handles logout correctly', () => { + renderComponent(); + + const logoutBtn = screen.getByRole('button', { name: /Wyloguj się/i }); + fireEvent.click(logoutBtn); + + expect(localStorage.removeItem).toHaveBeenCalledWith('access_token'); + expect(localStorage.removeItem).toHaveBeenCalledWith('role'); + + expect(mockedNavigate).toHaveBeenCalledWith('/'); + }); +}); diff --git a/web_app/src/Tests/VitestTests/LibraryAdmin/LibraryAdminAddLibrary.test.tsx b/web_app/src/Tests/VitestTests/LibraryAdmin/LibraryAdminAddLibrary.test.tsx new file mode 100644 index 00000000..c747e729 --- /dev/null +++ b/web_app/src/Tests/VitestTests/LibraryAdmin/LibraryAdminAddLibrary.test.tsx @@ -0,0 +1,129 @@ +import { render, screen, fireEvent, waitFor } from '@testing-library/react'; +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import LibraryAdminAddLibrary from '../../../LibraryAdmin/LibraryAdminAddLibrary'; +import { BrowserRouter } from 'react-router-dom'; + +const mockedNavigate = vi.fn(); +vi.mock('react-router-dom', async () => { + const actual = await vi.importActual('react-router-dom'); + return { + ...actual, + useNavigate: () => mockedNavigate, + }; +}); + +vi.stubGlobal('import.meta', { env: { VITE_API_BASE_URL: 'http://localhost:8080' } }); + +const fillForm = (overrides = {}) => { + const defaults = { + libraryName: 'Test Library', + addressLine: 'Test Street 1', + city: 'Test City', + postalCode: '00-000', + phoneNumber: '123456789', + emailAddress: 'test@example.com' + }; + const data = { ...defaults, ...overrides }; + + fireEvent.change(screen.getByLabelText(/Nazwa biblioteki:/i), { target: { value: data.libraryName } }); + fireEvent.change(screen.getByLabelText(/Ulica i nr budynku:/i), { target: { value: data.addressLine } }); + fireEvent.change(screen.getByLabelText(/Miasto:/i), { target: { value: data.city } }); + fireEvent.change(screen.getByLabelText(/Kod pocztowy:/i), { target: { value: data.postalCode } }); + fireEvent.change(screen.getByLabelText(/Numer telefonu:/i), { target: { value: data.phoneNumber } }); + fireEvent.change(screen.getByLabelText(/Adres e-mail:/i), { target: { value: data.emailAddress } }); +}; + +describe('LibraryAdminAddLibrary Integration Tests', () => { + + beforeEach(() => { + vi.clearAllMocks(); + + Storage.prototype.getItem = vi.fn((key) => { + if (key === 'access_token') return 'fake_admin_token'; + return null; + }); + Storage.prototype.removeItem = vi.fn(); + + global.fetch = vi.fn(); + + vi.spyOn(console, 'error').mockImplementation(() => {}); + }); + + const renderComponent = () => { + render( + + + + ); + }; + + it('renders all form fields and accepts user input', () => { + renderComponent(); + + expect(screen.getByLabelText(/Nazwa biblioteki:/i)).toBeInTheDocument(); + expect(screen.getByLabelText(/Ulica i nr budynku:/i)).toBeInTheDocument(); + + expect(screen.getByRole('button', { name: /Złóż podanie/i })).toBeInTheDocument(); + + const nameInput = screen.getByLabelText(/Nazwa biblioteki:/i); + fireEvent.change(nameInput, { target: { value: 'Central Library' } }); + expect(nameInput).toHaveValue('Central Library'); + }); + + it('submits the form with correctly mapped payload and navigates on success', async () => { + vi.spyOn(global, 'fetch').mockResolvedValueOnce({ + ok: true, + status: 201, + json: async () => ({}) + } as Response); + + renderComponent(); + + fillForm({ + libraryName: 'Grand Library', + addressLine: '123 Main St', + city: 'Warsaw', + postalCode: '00-001', + phoneNumber: '123456789', + emailAddress: 'contact@library.com' + }); + + fireEvent.click(screen.getByRole('button', { name: /Złóż podanie/i })); + + await waitFor(() => { + expect(global.fetch).toHaveBeenCalledWith( + expect.stringContaining('/api/library-requests'), + expect.objectContaining({ + method: 'POST', + body: JSON.stringify({ + street: '123 Main St', + city: 'Warsaw', + postalCode: '00-001', + libraryName: 'Grand Library', + phoneNumber: '123456789', + libraryEmail: 'contact@library.com', + }), + }) + ); + }); + + expect(mockedNavigate).toHaveBeenCalledWith('/processing-info'); + }); + + it('displays error message when server rejects the request', async () => { + vi.spyOn(global, 'fetch').mockResolvedValueOnce({ + ok: false, + status: 500, + statusText: 'Internal Server Error' + } as Response); + + renderComponent(); + fillForm(); + + fireEvent.click(screen.getByRole('button', { name: /Złóż podanie/i })); + + expect(await screen.findByText('Wystąpił błąd podczas wysyłania formularza.')).toBeInTheDocument(); + + expect(mockedNavigate).not.toHaveBeenCalled(); + }); +}); diff --git a/web_app/src/Tests/VitestTests/LibraryAdmin/LibraryAdminHomePage.test.tsx b/web_app/src/Tests/VitestTests/LibraryAdmin/LibraryAdminHomePage.test.tsx new file mode 100644 index 00000000..32648c02 --- /dev/null +++ b/web_app/src/Tests/VitestTests/LibraryAdmin/LibraryAdminHomePage.test.tsx @@ -0,0 +1,178 @@ +import { render, screen, fireEvent, waitFor, cleanup } from '@testing-library/react'; +import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest'; +import { BrowserRouter } from 'react-router-dom'; +import LibraryAdminHomePage from '../../../LibraryAdmin/LibraryAdminHomePage'; + +const mockedNavigate = vi.fn(); + +vi.mock('react-router-dom', async () => { + const actual = await vi.importActual('react-router-dom'); + return { + ...actual, + useNavigate: () => mockedNavigate, + }; +}); + +vi.stubGlobal('import', { meta: { env: { VITE_API_BASE_URL: 'http://localhost:8080' } } }); + +describe('LibraryAdminHomePage', () => { + const renderComponent = () => { + return render( + + + + ); + }; + + const mockLibrarians = [ + { id: '1', username: 'john_doe', firstName: 'John', lastName: 'Doe' }, + { id: '2', username: 'jane_smith', firstName: 'Jane', lastName: 'Smith' }, + ]; + + beforeEach(() => { + vi.clearAllMocks(); + global.fetch = vi.fn(); + localStorage.setItem('access_token', 'fake-token'); + }); + + afterEach(() => { + cleanup(); + localStorage.clear(); + }); + + it('renders the header and fetches/displays all librarians on mount', async () => { + (global.fetch as any).mockResolvedValue({ + ok: true, + json: async () => mockLibrarians, + }); + + renderComponent(); + + expect(screen.getByText('Wyszukaj bibliotekarza')).toBeInTheDocument(); + + await waitFor(() => { + expect(screen.getByText('John Doe')).toBeInTheDocument(); + expect(screen.getByText('Jane Smith')).toBeInTheDocument(); + }); + }); + + it('displays error message if fetching librarians fails', async () => { + (global.fetch as any).mockResolvedValue({ ok: false }); + + renderComponent(); + + await waitFor(() => { + expect(screen.getByText('Nie udało się pobrać listy bibliotekarzy.')).toBeInTheDocument(); + }); + }); + + it('searches for a specific librarian when "Szukaj" is clicked', async () => { + (global.fetch as any).mockImplementation((url: string) => { + if (url.includes('username=')) { + return Promise.resolve({ + ok: true, + json: async () => [mockLibrarians[0]], + }); + } + return Promise.resolve({ + ok: true, + json: async () => [], + }); + }); + + renderComponent(); + + const input = screen.getByPlaceholderText('Nazwa użytkownika'); + const searchBtn = screen.getByText('Szukaj'); + + fireEvent.change(input, { target: { value: 'john_doe' } }); + fireEvent.click(searchBtn); + + await waitFor(() => { + expect(screen.getByText('John Doe')).toBeInTheDocument(); + expect(screen.queryByText('Jane Smith')).not.toBeInTheDocument(); + }); + }); + + it('displays "not found" message if search yields no results', async () => { + (global.fetch as any).mockImplementation((url: string) => { + if (url.includes('username=')) { + return Promise.resolve({ + ok: false, + status: 404, + json: async () => ({}), + }); + } + return Promise.resolve({ + ok: true, + json: async () => [{ id: '99', username: 'init_user', firstName: 'Init', lastName: 'User' }], + }); + }); + + renderComponent(); + + await waitFor(() => { + expect(screen.getByText('Init User')).toBeInTheDocument(); + }); + + const input = screen.getByPlaceholderText('Nazwa użytkownika'); + const searchBtn = screen.getByText('Szukaj'); + + fireEvent.change(input, { target: { value: 'unknown_user' } }); + fireEvent.click(searchBtn); + + await waitFor(() => { + expect(screen.getByText('Nie znaleziono bibliotekarza.')).toBeInTheDocument(); + }); + }); + + it('resets librarian password successfully', async () => { + (global.fetch as any).mockResolvedValueOnce({ ok: true, json: async () => mockLibrarians }); + renderComponent(); + + await waitFor(() => screen.getByText('John Doe')); + + const resetBtns = screen.getAllByText('Zresetuj hasło'); + + (global.fetch as any).mockResolvedValueOnce({ ok: true }); + + fireEvent.click(resetBtns[0]); + + await waitFor(() => { + expect(screen.getByText('Hasło bibliotekarza zresetowano pomyślnie.')).toBeInTheDocument(); + }); + }); + + it('deletes a librarian and removes them from the UI', async () => { + (global.fetch as any).mockResolvedValueOnce({ ok: true, json: async () => mockLibrarians }); + renderComponent(); + + await waitFor(() => screen.getByText('John Doe')); + + const deleteBtns = screen.getAllByText('Usuń'); + + (global.fetch as any).mockResolvedValueOnce({ ok: true }); + + fireEvent.click(deleteBtns[0]); + + await waitFor(() => { + expect(screen.getByText('Usunięto bibliotekarza.')).toBeInTheDocument(); + expect(screen.queryByText('John Doe')).not.toBeInTheDocument(); + }); + }); + + it('handles logout by clearing storage and navigating', () => { + (global.fetch as any).mockResolvedValue({ ok: true, json: async () => [] }); + + localStorage.setItem('username', 'admin'); + localStorage.setItem('role', 'admin'); + + renderComponent(); + + const logoutBtn = screen.getByText('Wyloguj się'); + fireEvent.click(logoutBtn); + + expect(localStorage.getItem('access_token')).toBeNull(); + expect(mockedNavigate).toHaveBeenCalledWith('/'); + }); +}); diff --git a/web_app/src/Tests/VitestTests/SystemAdmin/SubmissionDetailsDriver.test.tsx b/web_app/src/Tests/VitestTests/SystemAdmin/SubmissionDetailsDriver.test.tsx new file mode 100644 index 00000000..a6dc47e0 --- /dev/null +++ b/web_app/src/Tests/VitestTests/SystemAdmin/SubmissionDetailsDriver.test.tsx @@ -0,0 +1,239 @@ +import { render, screen, fireEvent, waitFor, cleanup } from '@testing-library/react'; +import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest'; +import { BrowserRouter } from 'react-router-dom'; +import SubmissionDetailsDriver from '../../../SystemAdmin/SubmissionDetailsDriver'; + +const mockedNavigate = vi.fn(); +vi.mock('react-router-dom', async () => { + const actual = await vi.importActual('react-router-dom'); + return { + ...actual, + useNavigate: () => mockedNavigate, + useParams: () => ({ submissionId: '123' }), + }; +}); + +vi.stubGlobal('import', { meta: { env: { VITE_API_BASE_URL: 'http://localhost:8080' } } }); + +describe('SubmissionDetailsDriver', () => { + const mockDriverApp = { + id: 123, + userEmail: 'driver@test.com', + reviewerID: null, + status: 'PENDING', + submittedAt: '2023-11-01T10:00:00Z', + reviewedAt: null, + rejectionReason: null, + driverDocuments: [ + { + documentType: 'License', + documentPhotoUrl: 'http://img.url/license.jpg', + expiryDate: '2025-01-01T00:00:00Z', + }, + ], + }; + + const renderComponent = () => { + return render( + + + + ); + }; + + beforeEach(() => { + vi.clearAllMocks(); + global.fetch = vi.fn(); + localStorage.setItem('access_token', 'fake-token'); + localStorage.setItem('email', 'admin@test.com'); + }); + + afterEach(() => { + cleanup(); + localStorage.clear(); + }); + + it('renders loading state then displays application details', async () => { + (global.fetch as any).mockResolvedValueOnce({ + ok: true, + json: async () => mockDriverApp, + }); + + renderComponent(); + + expect(screen.getByAltText(/Book.*Logo/i)).toBeInTheDocument(); + expect(screen.getByText('admin@test.com')).toBeInTheDocument(); + + await waitFor(() => { + expect(screen.getByText('Szczegóły podania nr: 123')).toBeInTheDocument(); + expect(screen.getByText('Email użytkownika:')).toBeInTheDocument(); + expect(screen.getByText('driver@test.com')).toBeInTheDocument(); + expect(screen.getByText('License')).toBeInTheDocument(); + }); + + expect(global.fetch).toHaveBeenCalledWith( + expect.stringContaining('/api/driver-applications/123'), + expect.objectContaining({ method: 'GET' }) + ); + }); + + it('handles API errors gracefully', async () => { + (global.fetch as any).mockResolvedValueOnce({ + ok: false, + statusText: 'Not Found', + }); + + renderComponent(); + + await waitFor(() => { + expect(screen.getByText('Błąd podczas pobierania danych.')).toBeInTheDocument(); + }); + }); + + it('approves the application when "Zatwierdź" is clicked', async () => { + (global.fetch as any).mockResolvedValueOnce({ + ok: true, + json: async () => mockDriverApp, + }); + + renderComponent(); + + const approveBtn = await screen.findByText('Zatwierdź'); + + (global.fetch as any).mockResolvedValueOnce({ + ok: true, + json: async () => ({}), + }); + + fireEvent.click(approveBtn); + + await waitFor(() => { + expect(global.fetch).toHaveBeenCalledWith( + expect.stringContaining('/api/driver-applications/123/status?status=APPROVED'), + expect.objectContaining({ method: 'PUT' }) + ); + expect(mockedNavigate).toHaveBeenCalledWith('/system-admin-dashboard'); + }); + }); + + it('rejects the application with a standard reason', async () => { + (global.fetch as any).mockResolvedValueOnce({ + ok: true, + json: async () => mockDriverApp, + }); + + renderComponent(); + + const declineBtn = await screen.findByText('Odrzuć'); + fireEvent.click(declineBtn); + + const reasonRadio = screen.getByLabelText('Nieprawidłowy dokument'); + fireEvent.click(reasonRadio); + + (global.fetch as any).mockResolvedValueOnce({ + ok: true, + json: async () => ({}), + }); + + const submitBtn = screen.getByText('Wyślij'); + fireEvent.click(submitBtn); + + await waitFor(() => { + const expectedUrl = '/api/driver-applications/123/status?status=REJECTED&rejectionReason=' + encodeURIComponent('Nieprawidłowy dokument'); + expect(global.fetch).toHaveBeenCalledWith( + expect.stringContaining(expectedUrl), + expect.objectContaining({ method: 'PUT' }) + ); + expect(mockedNavigate).toHaveBeenCalledWith('/system-admin-dashboard'); + }); + }); + + it('rejects the application with a CUSTOM reason', async () => { + (global.fetch as any).mockResolvedValueOnce({ + ok: true, + json: async () => mockDriverApp, + }); + + renderComponent(); + + fireEvent.click(await screen.findByText('Odrzuć')); + + fireEvent.click(screen.getByLabelText('Inne')); + + const input = screen.getByPlaceholderText('Podaj przyczynę odmowy akceptacji aplikacji'); + fireEvent.change(input, { target: { value: 'Custom Reason Test' } }); + + (global.fetch as any).mockResolvedValueOnce({ + ok: true, + json: async () => ({}), + }); + + fireEvent.click(screen.getByText('Wyślij')); + + await waitFor(() => { + const expectedUrl = encodeURIComponent('Custom Reason Test'); + expect(global.fetch).toHaveBeenCalledWith( + expect.stringContaining(expectedUrl), + expect.anything() + ); + }); + }); + + it('displays validation error if no reason is selected', async () => { + (global.fetch as any).mockResolvedValueOnce({ + ok: true, + json: async () => mockDriverApp, + }); + + renderComponent(); + + fireEvent.click(await screen.findByText('Odrzuć')); + + fireEvent.click(screen.getByText('Wyślij')); + + await waitFor(() => { + expect(screen.getByText('Proszę podać powód odrzucenia.')).toBeInTheDocument(); + }); + + expect(global.fetch).toHaveBeenCalledTimes(1); + }); + + it('displays validation error if "Inne" is selected but input is empty', async () => { + (global.fetch as any).mockResolvedValueOnce({ + ok: true, + json: async () => mockDriverApp, + }); + + renderComponent(); + + fireEvent.click(await screen.findByText('Odrzuć')); + + fireEvent.click(screen.getByLabelText('Inne')); + + fireEvent.click(screen.getByText('Wyślij')); + + await waitFor(() => { + expect(screen.getByText('Proszę podać powód odrzucenia.')).toBeInTheDocument(); + }); + + expect(global.fetch).toHaveBeenCalledTimes(1); + }); + + it('handles logout by clearing storage and navigating', async () => { + (global.fetch as any).mockResolvedValueOnce({ + ok: true, + json: async () => mockDriverApp, + }); + + renderComponent(); + + const logoutBtn = await screen.findByText('Wyloguj się'); + + fireEvent.click(logoutBtn); + + expect(localStorage.getItem('access_token')).toBeNull(); + expect(localStorage.getItem('role')).toBeNull(); + + expect(mockedNavigate).toHaveBeenCalledWith('/'); + }); +}); diff --git a/web_app/src/Tests/VitestTests/SystemAdmin/SubmissionDetailsLibrary.test.tsx b/web_app/src/Tests/VitestTests/SystemAdmin/SubmissionDetailsLibrary.test.tsx new file mode 100644 index 00000000..4f0a19a6 --- /dev/null +++ b/web_app/src/Tests/VitestTests/SystemAdmin/SubmissionDetailsLibrary.test.tsx @@ -0,0 +1,228 @@ +import { render, screen, fireEvent, waitFor, cleanup } from '@testing-library/react'; +import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest'; +import { BrowserRouter } from 'react-router-dom'; +import SubmissionDetailsLibrary from '../../../SystemAdmin/SubmissionDetailsLibrary'; + +const mockedNavigate = vi.fn(); +vi.mock('react-router-dom', async () => { + const actual = await vi.importActual('react-router-dom'); + return { + ...actual, + useNavigate: () => mockedNavigate, + useParams: () => ({ submissionId: '555' }), + }; +}); + +vi.stubGlobal('import', { meta: { env: { VITE_API_BASE_URL: 'http://localhost:8080' } } }); + +describe('SubmissionDetailsLibrary', () => { + const mockLibraryRequest = { + id: 555, + creatorEmail: 'librarian@test.com', + reviewerId: null, + address: { + id: 1, + street: 'Main St 123', + city: 'Warsaw', + postalCode: '00-001', + latitude: 52.2297, + longitude: 21.0122, + }, + libraryName: 'Central Library', + phoneNumber: '123-456-789', + libraryEmail: 'contact@library.com', + status: 'PENDING', + submittedAt: '2023-11-15T10:00:00Z', + reviewedAt: null, + rejectionReason: null, + }; + + const renderComponent = () => { + return render( + + + + ); + }; + + beforeEach(() => { + vi.clearAllMocks(); + global.fetch = vi.fn(); + localStorage.setItem('access_token', 'fake-admin-token'); + localStorage.setItem('email', 'admin@test.com'); + }); + + afterEach(() => { + cleanup(); + localStorage.clear(); + }); + + it('renders loading state then displays library details', async () => { + (global.fetch as any).mockResolvedValueOnce({ + ok: true, + json: async () => mockLibraryRequest, + }); + + renderComponent(); + + expect(screen.getByAltText('Book Rider Logo')).toBeInTheDocument(); + expect(screen.getByText('admin@test.com')).toBeInTheDocument(); + + await waitFor(() => { + expect(screen.getByText('Szczegóły podania nr: 555')).toBeInTheDocument(); + expect(screen.getByText('Central Library')).toBeInTheDocument(); + expect(screen.getByText('Main St 123, Warsaw, 00-001')).toBeInTheDocument(); + expect(screen.getByText('librarian@test.com')).toBeInTheDocument(); + expect(screen.getByText('123-456-789')).toBeInTheDocument(); + }); + + expect(global.fetch).toHaveBeenCalledWith( + expect.stringContaining('/api/library-requests/555'), + expect.objectContaining({ method: 'GET' }) + ); + }); + + it('handles API errors gracefully', async () => { + (global.fetch as any).mockResolvedValueOnce({ + ok: false, + statusText: 'Internal Server Error', + }); + + renderComponent(); + + await waitFor(() => { + expect(screen.getByText('Error: Failed to fetch library request')).toBeInTheDocument(); + }); + }); + + it('approves the library request when "Zatwierdź" is clicked', async () => { + (global.fetch as any).mockResolvedValueOnce({ + ok: true, + json: async () => mockLibraryRequest, + }); + + renderComponent(); + + const approveBtn = await screen.findByText('Zatwierdź'); + + (global.fetch as any).mockResolvedValueOnce({ + ok: true, + json: async () => ({}), + }); + + fireEvent.click(approveBtn); + + await waitFor(() => { + expect(global.fetch).toHaveBeenCalledWith( + expect.stringContaining('/api/library-requests/555/status?status=APPROVED'), + expect.objectContaining({ method: 'PUT' }) + ); + expect(mockedNavigate).toHaveBeenCalledWith('/system-admin-dashboard'); + }); + }); + + it('rejects the request with a standard reason', async () => { + (global.fetch as any).mockResolvedValueOnce({ + ok: true, + json: async () => mockLibraryRequest, + }); + + renderComponent(); + + const declineBtn = await screen.findByText('Odrzuć'); + fireEvent.click(declineBtn); + + const reasonRadio = screen.getByLabelText(/Biblioteka została już dodana/i); + fireEvent.click(reasonRadio); + + (global.fetch as any).mockResolvedValueOnce({ + ok: true, + json: async () => ({}), + }); + + const submitBtn = screen.getByText('Wyślij'); + fireEvent.click(submitBtn); + + await waitFor(() => { + const reason = 'Biblioteka została już dodana do systemu'; + const expectedUrl = `/api/library-requests/555/status?status=REJECTED&rejectionReason=${encodeURIComponent(reason)}`; + + expect(global.fetch).toHaveBeenCalledWith( + expect.stringContaining(expectedUrl), + expect.objectContaining({ method: 'PUT' }) + ); + expect(mockedNavigate).toHaveBeenCalledWith('/system-admin-dashboard'); + }); + }); + + it('rejects the request with a CUSTOM reason', async () => { + (global.fetch as any).mockResolvedValueOnce({ + ok: true, + json: async () => mockLibraryRequest, + }); + + renderComponent(); + + fireEvent.click(await screen.findByText('Odrzuć')); + + fireEvent.click(screen.getByLabelText('Inne')); + + const input = screen.getByPlaceholderText('Podaj przyczynę odmowy akceptacji podania'); + fireEvent.change(input, { target: { value: 'Fake Library Address' } }); + + (global.fetch as any).mockResolvedValueOnce({ + ok: true, + json: async () => ({}), + }); + + fireEvent.click(screen.getByText('Wyślij')); + + await waitFor(() => { + const expectedUrl = encodeURIComponent('Fake Library Address'); + expect(global.fetch).toHaveBeenCalledWith( + expect.stringContaining(expectedUrl), + expect.anything() + ); + }); + }); + + it('displays validation error if "Inne" is selected but input is empty', async () => { + (global.fetch as any).mockResolvedValueOnce({ + ok: true, + json: async () => mockLibraryRequest, + }); + + renderComponent(); + + fireEvent.click(await screen.findByText('Odrzuć')); + + fireEvent.click(screen.getByLabelText('Inne')); + + fireEvent.click(screen.getByText('Wyślij')); + + await waitFor(() => { + expect(screen.getByText('Podaj przyczynę odmowy.')).toBeInTheDocument(); + }); + + expect(global.fetch).toHaveBeenCalledTimes(1); + }); + + it('displays validation error if NO reason is selected', async () => { + (global.fetch as any).mockResolvedValueOnce({ + ok: true, + json: async () => mockLibraryRequest, + }); + + renderComponent(); + + fireEvent.click(await screen.findByText('Odrzuć')); + + fireEvent.click(screen.getByText('Wyślij')); + + await waitFor(() => { + expect(screen.getByText('Proszę podać powód odrzucenia.')).toBeInTheDocument(); + }); + + expect(global.fetch).toHaveBeenCalledTimes(1); + }); +}); diff --git a/web_app/src/Tests/VitestTests/SystemAdmin/SystemAdminHomePage.test.tsx b/web_app/src/Tests/VitestTests/SystemAdmin/SystemAdminHomePage.test.tsx new file mode 100644 index 00000000..8e1bf686 --- /dev/null +++ b/web_app/src/Tests/VitestTests/SystemAdmin/SystemAdminHomePage.test.tsx @@ -0,0 +1,192 @@ +import { render, screen, fireEvent, waitFor, cleanup } from '@testing-library/react'; +import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest'; +import { BrowserRouter } from 'react-router-dom'; +import SystemAdminHomePage from '../../../SystemAdmin/SystemAdminHomePage'; +import { useWebSocketNotification } from '../../../Utils/useWebSocketNotification'; + +const mockedNavigate = vi.fn(); +vi.mock('react-router-dom', async () => { + const actual = await vi.importActual('react-router-dom'); + return { + ...actual, + useNavigate: () => mockedNavigate, + }; +}); + +vi.mock('../../../Utils/useWebSocketNotification', () => ({ + useWebSocketNotification: vi.fn(), +})); + +vi.mock('react-toastify', () => ({ + toast: { + info: vi.fn(), + error: vi.fn(), + }, +})); + +vi.stubGlobal('import', { meta: { env: { VITE_API_BASE_URL: 'http://localhost:8080' } } }); + +describe('SystemAdminHomePage', () => { + const renderComponent = () => { + return render( + + + + ); + }; + + const mockDriverData = [ + { id: 101, driverEmail: 'driver@test.com', status: 'PENDING', submittedAt: '2023-10-01T10:00:00Z' } + ]; + + const mockLibraryData = [ + { id: 202, creatorEmail: 'lib@test.com', libraryName: 'Library 1', status: 'PENDING', submittedAt: '2023-10-02T12:00:00Z' } + ]; + + beforeEach(() => { + vi.clearAllMocks(); + global.fetch = vi.fn(); + localStorage.setItem('access_token', 'fake-admin-token'); + }); + + afterEach(() => { + cleanup(); + localStorage.clear(); + }); + + it('Renders the dashboard static elements and initializes WebSockets', () => { + renderComponent(); + + expect(screen.getByAltText('Book Rider Logo')).toBeInTheDocument(); + expect(screen.getByText('Podania o zatwierdzenie bibliotek')).toBeInTheDocument(); + expect(screen.getByText('Podania o zatwierdzenie kierowców')).toBeInTheDocument(); + + expect(useWebSocketNotification).toHaveBeenCalledWith('administrator/library-requests', expect.any(Function)); + expect(useWebSocketNotification).toHaveBeenCalledWith('administrator/driver-applications', expect.any(Function)); + }); + + it('handles Logout', () => { + renderComponent(); + localStorage.setItem('role', 'admin'); + + const logoutBtn = screen.getByText('Wyloguj się'); + fireEvent.click(logoutBtn); + + expect(localStorage.getItem('access_token')).toBeNull(); + expect(mockedNavigate).toHaveBeenCalledWith('/'); + }); + + it('Driver: fetches data, displays list, and processes "Open" action', async () => { + (global.fetch as any).mockResolvedValueOnce({ + ok: true, + json: async () => mockDriverData, + }); + + renderComponent(); + + fireEvent.click(screen.getByText('Podania o zatwierdzenie kierowców')); + + await waitFor(() => { + expect(global.fetch).toHaveBeenCalledWith( + expect.stringContaining('/api/driver-applications'), + expect.objectContaining({ method: 'GET' }) + ); + expect(screen.getByText('ID podania: 101')).toBeInTheDocument(); + expect(screen.getByText('Utworzone przez: driver@test.com')).toBeInTheDocument(); + }); + + const openBtn = screen.getByText('Otwórz'); + (global.fetch as any).mockResolvedValueOnce({ + ok: true, + json: async () => ({}), + }); + + fireEvent.click(openBtn); + + await waitFor(() => { + expect(global.fetch).toHaveBeenCalledWith( + expect.stringContaining('/api/driver-applications/101/status?status=UNDER_REVIEW'), + expect.objectContaining({ method: 'PUT' }) + ); + expect(mockedNavigate).toHaveBeenCalledWith('/submissionDetailsDriver/101'); + }); + }); + + it('Library: fetches data, displays list, and processes "Open" action', async () => { + (global.fetch as any).mockImplementation((url: string, options: any) => { + const method = options?.method || 'GET'; + + if (url.includes('driver-applications') && method === 'GET') { + return Promise.resolve({ ok: true, json: async () => [] }); + } + + if (url.includes('library-requests') && method === 'GET') { + return Promise.resolve({ ok: true, json: async () => mockLibraryData }); + } + + if (url.includes('status') && method === 'PUT') { + return Promise.resolve({ ok: true, json: async () => ({}) }); + } + + return Promise.resolve({ ok: false }); + }); + + renderComponent(); + + fireEvent.click(screen.getByText('Podania o zatwierdzenie kierowców')); + + await waitFor(() => { + expect(global.fetch).toHaveBeenCalledWith( + expect.stringContaining('/api/driver-applications'), + expect.objectContaining({ method: 'GET' }) + ); + }); + + fireEvent.click(screen.getByText('Podania o zatwierdzenie bibliotek')); + + await waitFor(() => { + expect(screen.getByText('Nazwa biblioteki: Library 1')).toBeInTheDocument(); + }); + + const openBtn = screen.getByText('Otwórz'); + fireEvent.click(openBtn); + + await waitFor(() => { + expect(global.fetch).toHaveBeenCalledWith( + expect.stringContaining('/api/library-requests/202/status?status=UNDER_REVIEW'), + expect.objectContaining({ method: 'PUT' }) + ); + expect(mockedNavigate).toHaveBeenCalledWith('/submissionDetailsLibrary/202'); + }); + }); + + it('appends new data when "Load More" is clicked', async () => { + (global.fetch as any).mockResolvedValueOnce({ + ok: true, + json: async () => mockDriverData, + }); + + renderComponent(); + fireEvent.click(screen.getByText('Podania o zatwierdzenie kierowców')); + + await screen.findByText('ID podania: 101'); + + (global.fetch as any).mockResolvedValueOnce({ + ok: true, + json: async () => [{ id: 999, driverEmail: 'page2@test.com', status: 'PENDING', submittedAt: '2023-10-03T10:00:00Z' }], + }); + + const loadMoreBtn = screen.getByText('Załaduj więcej'); + fireEvent.click(loadMoreBtn); + + await waitFor(() => { + expect(screen.getByText('ID podania: 101')).toBeInTheDocument(); + expect(screen.getByText('ID podania: 999')).toBeInTheDocument(); + }); + + expect(global.fetch).toHaveBeenCalledWith( + expect.stringContaining('page=1'), + expect.anything() + ); + }); +}); diff --git a/web_app/src/Tests/setup.ts b/web_app/src/Tests/setup.ts new file mode 100644 index 00000000..491e973d --- /dev/null +++ b/web_app/src/Tests/setup.ts @@ -0,0 +1,3 @@ +import '@testing-library/jest-dom'; + + diff --git a/web_app/tsconfig.node.json b/web_app/tsconfig.node.json index abcd7f0d..1cfe7e7e 100644 --- a/web_app/tsconfig.node.json +++ b/web_app/tsconfig.node.json @@ -18,7 +18,10 @@ "noUnusedLocals": true, "noUnusedParameters": true, "noFallthroughCasesInSwitch": true, - "noUncheckedSideEffectImports": true + "noUncheckedSideEffectImports": true, + + /* Testing with Vitest */ + "types": ["vitest/globals"] }, "include": ["vite.config.ts"] } diff --git a/web_app/vite.config.ts b/web_app/vite.config.ts index 8b0f57b9..6d050ca4 100644 --- a/web_app/vite.config.ts +++ b/web_app/vite.config.ts @@ -1,7 +1,14 @@ -import { defineConfig } from 'vite' +/// + +import { defineConfig } from 'vitest/config' import react from '@vitejs/plugin-react' // https://vite.dev/config/ export default defineConfig({ plugins: [react()], + test: { + globals: true, + environment: 'jsdom', + setupFiles: './src/Tests/setup.ts' + } })