From c9cfbd91f38098a8a26b47f4dedc86c1636e86dc Mon Sep 17 00:00:00 2001
From: jpie02 <128189069+jpie02@users.noreply.github.com>
Date: Wed, 3 Dec 2025 14:51:42 +0100
Subject: [PATCH 01/11] Add Vitest unit and integration tests for the
LibrarianAddBook.tsx page.
---
web_app/package.json | 7 +-
web_app/src/Librarian/LibrarianAddBook.tsx | 62 ++++-
.../Librarian/LibrarianAddBook.test.tsx | 256 ++++++++++++++++++
web_app/src/Tests/setup.ts | 3 +
web_app/tsconfig.node.json | 5 +-
web_app/vite.config.ts | 9 +-
6 files changed, 329 insertions(+), 13 deletions(-)
create mode 100644 web_app/src/Tests/VitestTests/Librarian/LibrarianAddBook.test.tsx
create mode 100644 web_app/src/Tests/setup.ts
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/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/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'
+ }
})
From abfcfc25a6d4d78ee11711db60f9889f9a60b8bb Mon Sep 17 00:00:00 2001
From: jpie02 <128189069+jpie02@users.noreply.github.com>
Date: Wed, 3 Dec 2025 15:52:46 +0100
Subject: [PATCH 02/11] Add Vitest unit and integration tests for the
LibrarianHomePage.tsx page.
---
.../Librarian/LibrarianHomePage.test.tsx | 221 ++++++++++++++++++
1 file changed, 221 insertions(+)
create mode 100644 web_app/src/Tests/VitestTests/Librarian/LibrarianHomePage.test.tsx
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('/');
+ });
+ });
+});
From 5dbd73935149117853dc4f675f0b50f2eab66d13 Mon Sep 17 00:00:00 2001
From: jpie02 <128189069+jpie02@users.noreply.github.com>
Date: Wed, 3 Dec 2025 16:41:23 +0100
Subject: [PATCH 03/11] Add Vitest unit and integration tests for the
LibrarianOrders.tsx page.
---
.../Librarian/LibrarianOrders.test.tsx | 177 ++++++++++++++++++
1 file changed, 177 insertions(+)
create mode 100644 web_app/src/Tests/VitestTests/Librarian/LibrarianOrders.test.tsx
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()
+ );
+ });
+});
From 52ec17e64b11d4884bdb73f3543744109ee6c355 Mon Sep 17 00:00:00 2001
From: jpie02 <128189069+jpie02@users.noreply.github.com>
Date: Wed, 3 Dec 2025 18:24:33 +0100
Subject: [PATCH 04/11] Add Vitest unit and integration tests for the
LibrarianReaders.tsx page.
---
.../Librarian/LibrarianReaders.test.tsx | 228 ++++++++++++++++++
1 file changed, 228 insertions(+)
create mode 100644 web_app/src/Tests/VitestTests/Librarian/LibrarianReaders.test.tsx
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();
+ });
+});
From f8a4153bd690ec9b32c9ed8ef178d15d248a0288 Mon Sep 17 00:00:00 2001
From: jpie02 <128189069+jpie02@users.noreply.github.com>
Date: Wed, 3 Dec 2025 18:49:43 +0100
Subject: [PATCH 05/11] Add Vitest unit and integration tests for the
LibrarianReturns.tsx page.
---
.../Librarian/LibrarianReturns.test.tsx | 204 ++++++++++++++++++
1 file changed, 204 insertions(+)
create mode 100644 web_app/src/Tests/VitestTests/Librarian/LibrarianReturns.test.tsx
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('/');
+ });
+});
From 1b4a67c9cfb81d25d4877a05ece6516e95cb3214 Mon Sep 17 00:00:00 2001
From: jpie02 <128189069+jpie02@users.noreply.github.com>
Date: Mon, 8 Dec 2025 12:11:03 +0100
Subject: [PATCH 06/11] Add Vitest unit and integration tests for the
LibraryAdminAddLibrarian..tsx page.
---
.../LibraryAdminAddLibrarian.test.tsx | 122 ++++++++++++++++++
1 file changed, 122 insertions(+)
create mode 100644 web_app/src/Tests/VitestTests/LibraryAdmin/LibraryAdminAddLibrarian.test.tsx
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('/');
+ });
+});
From 4cbeef35beff71fac28e795f3bd088a73b896123 Mon Sep 17 00:00:00 2001
From: jpie02 <128189069+jpie02@users.noreply.github.com>
Date: Tue, 9 Dec 2025 16:24:31 +0100
Subject: [PATCH 07/11] Add Vitest unit and integration tests for the
LibraryAdminAddLibrary.tsx page.
---
.../LibraryAdmin/LibraryAdminAddLibrary.tsx | 10 ++
.../LibraryAdminAddLibrary.test.tsx | 129 ++++++++++++++++++
2 files changed, 139 insertions(+)
create mode 100644 web_app/src/Tests/VitestTests/LibraryAdmin/LibraryAdminAddLibrary.test.tsx
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/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();
+ });
+});
From 6af4915b10a890de858e67311aea87a2409e0fc7 Mon Sep 17 00:00:00 2001
From: jpie02 <128189069+jpie02@users.noreply.github.com>
Date: Tue, 9 Dec 2025 18:32:39 +0100
Subject: [PATCH 08/11] Add Vitest unit and integration tests for the
LibraryAdminHomePage.tsx page.
---
.../LibraryAdminHomePage.test.tsx | 178 ++++++++++++++++++
1 file changed, 178 insertions(+)
create mode 100644 web_app/src/Tests/VitestTests/LibraryAdmin/LibraryAdminHomePage.test.tsx
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('/');
+ });
+});
From f2aac1accec229ea698e65fee24559a43f07c11e Mon Sep 17 00:00:00 2001
From: jpie02 <128189069+jpie02@users.noreply.github.com>
Date: Wed, 10 Dec 2025 15:29:47 +0100
Subject: [PATCH 09/11] Add Vitest unit and integration tests for the
SystemAdminHomePage.tsx page.
---
.../SystemAdmin/SystemAdminHomePage.test.tsx | 192 ++++++++++++++++++
1 file changed, 192 insertions(+)
create mode 100644 web_app/src/Tests/VitestTests/SystemAdmin/SystemAdminHomePage.test.tsx
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..d0040aca
--- /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('UNIT: 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('UNIT: 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('INTEGRATION (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('INTEGRATION (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: City Library')).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('INTEGRATION: 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()
+ );
+ });
+});
From 46fbccf32c4e3408db303db8e489b23743d75a79 Mon Sep 17 00:00:00 2001
From: jpie02 <128189069+jpie02@users.noreply.github.com>
Date: Wed, 10 Dec 2025 18:11:45 +0100
Subject: [PATCH 10/11] Add Vitest unit and integration tests for the
SubmissionDetailsDriver.tsx page.
---
.../SubmissionDetailsDriver.test.tsx | 200 ++++++++++++++++++
.../SystemAdmin/SystemAdminHomePage.test.tsx | 12 +-
2 files changed, 206 insertions(+), 6 deletions(-)
create mode 100644 web_app/src/Tests/VitestTests/SystemAdmin/SubmissionDetailsDriver.test.tsx
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..c476e71f
--- /dev/null
+++ b/web_app/src/Tests/VitestTests/SystemAdmin/SubmissionDetailsDriver.test.tsx
@@ -0,0 +1,200 @@
+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 Rider Logo')).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);
+ });
+});
diff --git a/web_app/src/Tests/VitestTests/SystemAdmin/SystemAdminHomePage.test.tsx b/web_app/src/Tests/VitestTests/SystemAdmin/SystemAdminHomePage.test.tsx
index d0040aca..8e1bf686 100644
--- a/web_app/src/Tests/VitestTests/SystemAdmin/SystemAdminHomePage.test.tsx
+++ b/web_app/src/Tests/VitestTests/SystemAdmin/SystemAdminHomePage.test.tsx
@@ -54,7 +54,7 @@ describe('SystemAdminHomePage', () => {
localStorage.clear();
});
- it('UNIT: Renders the dashboard static elements and initializes WebSockets', () => {
+ it('Renders the dashboard static elements and initializes WebSockets', () => {
renderComponent();
expect(screen.getByAltText('Book Rider Logo')).toBeInTheDocument();
@@ -65,7 +65,7 @@ describe('SystemAdminHomePage', () => {
expect(useWebSocketNotification).toHaveBeenCalledWith('administrator/driver-applications', expect.any(Function));
});
- it('UNIT: Handles Logout', () => {
+ it('handles Logout', () => {
renderComponent();
localStorage.setItem('role', 'admin');
@@ -76,7 +76,7 @@ describe('SystemAdminHomePage', () => {
expect(mockedNavigate).toHaveBeenCalledWith('/');
});
- it('INTEGRATION (Driver): Fetches data, displays list, and processes "Open" action', async () => {
+ it('Driver: fetches data, displays list, and processes "Open" action', async () => {
(global.fetch as any).mockResolvedValueOnce({
ok: true,
json: async () => mockDriverData,
@@ -112,7 +112,7 @@ describe('SystemAdminHomePage', () => {
});
});
- it('INTEGRATION (Library): Fetches data, displays list, and processes "Open" action', async () => {
+ 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';
@@ -145,7 +145,7 @@ describe('SystemAdminHomePage', () => {
fireEvent.click(screen.getByText('Podania o zatwierdzenie bibliotek'));
await waitFor(() => {
- expect(screen.getByText('Nazwa biblioteki: City Library')).toBeInTheDocument();
+ expect(screen.getByText('Nazwa biblioteki: Library 1')).toBeInTheDocument();
});
const openBtn = screen.getByText('Otwórz');
@@ -160,7 +160,7 @@ describe('SystemAdminHomePage', () => {
});
});
- it('INTEGRATION: Appends new data when "Load More" is clicked', async () => {
+ it('appends new data when "Load More" is clicked', async () => {
(global.fetch as any).mockResolvedValueOnce({
ok: true,
json: async () => mockDriverData,
From 82c21cfe961b3a7648a4c849ca5df983d2178d29 Mon Sep 17 00:00:00 2001
From: jpie02 <128189069+jpie02@users.noreply.github.com>
Date: Wed, 10 Dec 2025 18:26:46 +0100
Subject: [PATCH 11/11] Add Vitest unit and integration tests for the
SubmissionDetailsLibrary.tsx page.
---
.../SubmissionDetailsDriver.test.tsx | 41 +++-
.../SubmissionDetailsLibrary.test.tsx | 228 ++++++++++++++++++
2 files changed, 268 insertions(+), 1 deletion(-)
create mode 100644 web_app/src/Tests/VitestTests/SystemAdmin/SubmissionDetailsLibrary.test.tsx
diff --git a/web_app/src/Tests/VitestTests/SystemAdmin/SubmissionDetailsDriver.test.tsx b/web_app/src/Tests/VitestTests/SystemAdmin/SubmissionDetailsDriver.test.tsx
index c476e71f..a6dc47e0 100644
--- a/web_app/src/Tests/VitestTests/SystemAdmin/SubmissionDetailsDriver.test.tsx
+++ b/web_app/src/Tests/VitestTests/SystemAdmin/SubmissionDetailsDriver.test.tsx
@@ -61,7 +61,7 @@ describe('SubmissionDetailsDriver', () => {
renderComponent();
- expect(screen.getByAltText('Book Rider Logo')).toBeInTheDocument();
+ expect(screen.getByAltText(/Book.*Logo/i)).toBeInTheDocument();
expect(screen.getByText('admin@test.com')).toBeInTheDocument();
await waitFor(() => {
@@ -197,4 +197,43 @@ describe('SubmissionDetailsDriver', () => {
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);
+ });
+});