diff --git a/src/App.js b/src/App.js index 045add0..5b4693a 100644 --- a/src/App.js +++ b/src/App.js @@ -1,16 +1,30 @@ +import { useEffect } from 'react' +import { useDispatch } from 'react-redux' +import { onAuthStateChanged } from 'firebase/auth' import { BrowserRouter, Routes, Route } from 'react-router-dom' import FilterSection from './components/filterSection/FilterSection' import ManualAddBookSection from './components/manualAddBookSection/ManualAddBookSection' import SearchBookSection from './components/searchBookSection/SearchBookSection' +import RandomBookSection from './components/randomBookSection/RandomBookSection' import BookListSection from './components/BookListSection/BookListSection' import Header from './components/header/Header' import Login from './components/user/login/Login' import Signup from './components/user/signup/Signup' import Error from './components/error/Error' +import { syncLoadBook } from './redux/slices/booksSlice' +import { auth } from './services/firebaseConfig' import './App.css' -import RandomBookSection from './components/randomBookSection/RandomBookSection' function App() { + const dispatch = useDispatch() + + useEffect(() => { + const unsubscribe = onAuthStateChanged(auth, (user) => { + dispatch(syncLoadBook()) + }) + return () => unsubscribe() + }, [dispatch]) + return ( { - //App main component elements: - - beforeEach(() => { + beforeEach(async () => { resetStore() - setup(App, store) + await act(async () => { + setup(App, store) + }) //main components elements: header = screen.getByTestId('header_container') @@ -44,6 +44,10 @@ describe('App Component Tests', () => { bookListSection_component = screen.getByTestId('bookList_section') }) + afterEach(() => { + jest.clearAllMocks() + }) + test('Should render the Header to be in the DOM and should contain text "My Books Storage"', async () => { expect(header).toBeInTheDocument() expect(header).toHaveTextContent('My Books Storage') @@ -63,9 +67,11 @@ describe('App Component Tests', () => { }) describe('App functional Tests', () => { - beforeEach(() => { + beforeEach(async () => { resetStore() - setup(App, store) + await act(async () => { + setup(App, store) + }) // ManualAddBookSection form elements: titleInput = screen.getByTestId('manualAddBook_input_title') @@ -78,6 +84,10 @@ describe('App functional Tests', () => { clearAllFiltersBtn = screen.getByTestId('filter_clear_btn') }) + afterEach(() => { + jest.clearAllMocks() + }) + test('Should Submit a new book to the BookListSection', () => { submitNewBook(bookTitleName, bookAuthorName) diff --git a/src/api/services/searchBookService.js b/src/api/services/searchBookService.js index 7db62b6..425a1a9 100644 --- a/src/api/services/searchBookService.js +++ b/src/api/services/searchBookService.js @@ -26,9 +26,9 @@ export async function searchBookService(query) { title: item.volumeInfo.title, image: item.volumeInfo.imageLinks?.thumbnail, authors: item.volumeInfo.authors?.join(', ') || 'Unknown Author', - publishedDate: item.volumeInfo.publishedDate, + publishedDate: item.volumeInfo.publishedDate || 'N/A', language: item.volumeInfo.language, - description: item.volumeInfo.description, + description: item.volumeInfo.description || 'No description available', bookId: item.id, })) } else { diff --git a/src/components/BookListSection/BookListSection.jsx b/src/components/BookListSection/BookListSection.jsx index 5b1a75f..1589b4e 100644 --- a/src/components/BookListSection/BookListSection.jsx +++ b/src/components/BookListSection/BookListSection.jsx @@ -1,11 +1,13 @@ +import { useEffect } from 'react' import { useDispatch, useSelector } from 'react-redux' import { setError } from '../../redux/slices/errorSlice' import Book from './book/Book' import './BookListSection.css' import { selectBook, - deleteBook, - toggleFavorite, + syncLoadBook, + syncDeleteBook, + syncToggleFavorite, } from '../../redux/slices/booksSlice' import { selectTitleFilter, @@ -18,11 +20,17 @@ const BookListSection = ({ 'data-testid': testId }) => { const titleFilter = useSelector(selectTitleFilter) const authorsFilter = useSelector(selectAuthorsFilter) + useEffect(() => { + if (process.env.NODE_ENV !== 'test') { + dispatch(syncLoadBook()) + } + }, [dispatch]) + const handleDeleteBook = (id) => { const book = books.find((book) => book.id === id) if (book) { if (!book.isFavorite) { - dispatch(deleteBook(id)) + dispatch(syncDeleteBook(id)) } else { dispatch(setError("Can't Delete Favorite Book!")) } @@ -30,7 +38,7 @@ const BookListSection = ({ 'data-testid': testId }) => { } const toggleFavoriteBook = (id) => { - dispatch(toggleFavorite(id)) + dispatch(syncToggleFavorite(id)) } const filteredBooksArr = books.filter((book) => { diff --git a/src/components/BookListSection/BookListSection.test.js b/src/components/BookListSection/BookListSection.test.js index 0f7139c..eb54323 100644 --- a/src/components/BookListSection/BookListSection.test.js +++ b/src/components/BookListSection/BookListSection.test.js @@ -1,25 +1,39 @@ -import { within, fireEvent } from '@testing-library/react' +import { within, fireEvent, screen, act } from '@testing-library/react' import { v4 as uuidv4 } from 'uuid' import { setup } from '../../setupTests' +import store from '../../redux/store' import BookListSection from './BookListSection' import { createStore } from '../../redux/store' +import App from '../../App' +let bookListComponent, noBooksMessage const bookTitleName = 'Test Book Title' const bookAuthorName = 'Test Book Author' const bookId = uuidv4() describe('BookList Component Tests', () => { - test('Should display "No books in my list..." when books list is empty', () => { - // Create the mocked store with no books in the preloaded state - const mockedStore = createStore({ - books: [], + beforeEach(async () => { + await act(async () => { + setup(App, store) }) - // Render the BookList component with the mock store - const { container } = setup(BookListSection, mockedStore) + // Locate BookListSection elements + bookListComponent = screen.getByTestId('bookList_section') + noBooksMessage = screen.queryByTestId('bookList_emptyMsg') + }) - const noBooksSign = within(container).getByTestId('bookList_emptyMsg') - expect(noBooksSign).toBeInTheDocument() + afterEach(() => { + document.body.innerHTML = '' + }) + + test('Should render the BookListSection component', () => { + expect(bookListComponent).toBeInTheDocument() + expect(screen.getByText('My Book List')).toBeInTheDocument() + }) + + test('Should display "No books in my list..." when books list is empty', () => { + expect(noBooksMessage).toBeInTheDocument() + expect(noBooksMessage.textContent).toBe('No books in my list...') }) test('Should display a book in the list when there is one book', () => { @@ -58,7 +72,7 @@ describe('BookList Component Tests', () => { expect(noBooksSign).toBeInTheDocument() }) - test('Should not able to delete Favorite book', () => { + test('Should toggle favorite and delete functionality', () => { const mockedStore = createStore({ books: [ { @@ -72,41 +86,32 @@ describe('BookList Component Tests', () => { const { container } = setup(BookListSection, mockedStore) - const bookItems = within(container).getByTestId( - `bookList_item id=${bookId}` - ) - let isFavoriteFalse = within(bookItems).getByTestId( + const bookItem = within(container).getByTestId(`bookList_item id=${bookId}`) + const favoriteToggle = within(bookItem).getByTestId( 'book_favorite_toggle_false' ) - expect(isFavoriteFalse).toBeInTheDocument() - fireEvent.click(isFavoriteFalse) - let isFavoriteTrue = within(bookItems).getByTestId( - 'book_favorite_toggle_true' - ) - expect(isFavoriteTrue).toBeInTheDocument() + // Toggle favorite on + fireEvent.click(favoriteToggle) + expect( + within(bookItem).getByTestId('book_favorite_toggle_true') + ).toBeInTheDocument() - //try to delete toggled book: - let deleteBookBtn = within(bookItems).getByTestId('delete_book_btn') + // Try deleting the book + const deleteBookBtn = within(bookItem).getByTestId('delete_book_btn') fireEvent.click(deleteBookBtn) - expect(bookItems).toBeInTheDocument() + expect(bookItem).toBeInTheDocument() - //untoggle favorite: - isFavoriteTrue = within(bookItems).getByTestId('book_favorite_toggle_true') - fireEvent.click(isFavoriteTrue) - isFavoriteFalse = within(bookItems).getByTestId( - 'book_favorite_toggle_false' - ) - expect(isFavoriteFalse).toBeInTheDocument() + // Untoggle favorite + fireEvent.click(within(bookItem).getByTestId('book_favorite_toggle_true')) + expect( + within(bookItem).getByTestId('book_favorite_toggle_false') + ).toBeInTheDocument() - //delete book: - deleteBookBtn = within(bookItems).getByTestId('delete_book_btn') + // Delete book fireEvent.click(deleteBookBtn) - - //assert if the book deleted from the DOM: - const deletedBookItem = within(container).queryByTestId( - `bookList_item id=${bookId}` - ) - expect(deletedBookItem).toBeNull() + expect( + within(container).queryByTestId(`bookList_item id=${bookId}`) + ).toBeNull() }) }) diff --git a/src/components/filterSection/FilterSection.test.js b/src/components/filterSection/FilterSection.test.js index a726f4d..9ff5c3e 100644 --- a/src/components/filterSection/FilterSection.test.js +++ b/src/components/filterSection/FilterSection.test.js @@ -1,4 +1,4 @@ -import { fireEvent, screen } from '@testing-library/react' +import { fireEvent, screen, act } from '@testing-library/react' import { setup } from '../../setupTests' import store from '../../redux/store' import App from '../../App' @@ -9,8 +9,10 @@ const authorsFilterStr = 'authors' let filterByTitleInput, filterbyAuthorsInput, clearAllFiltersBtn describe('FilterSection Component Tests', () => { - beforeEach(() => { - setup(App, store) + beforeEach(async () => { + await act(async () => { + setup(App, store) + }) // FilterSection elements: filterByTitleInput = screen.getByTestId('filter_title_input') @@ -18,6 +20,10 @@ describe('FilterSection Component Tests', () => { clearAllFiltersBtn = screen.getByTestId('filter_clear_btn') }) + afterEach(() => { + document.body.innerHTML = '' + }) + test('Should render TitleFilter input', () => { expect(filterByTitleInput).toBeInTheDocument() expect(filterByTitleInput).toBeEnabled() diff --git a/src/components/manualAddBookSection/ManualAddBookSection.jsx b/src/components/manualAddBookSection/ManualAddBookSection.jsx index f8aa248..5efbc3a 100644 --- a/src/components/manualAddBookSection/ManualAddBookSection.jsx +++ b/src/components/manualAddBookSection/ManualAddBookSection.jsx @@ -1,6 +1,6 @@ import { useState } from 'react' import { useDispatch, useSelector } from 'react-redux' -import { addBook, selectBook } from '../../redux/slices/booksSlice' +import { selectBook, syncAddBook } from '../../redux/slices/booksSlice' import createBook from '../../utils/createBook' import './ManualAddBookSection.css' import { setError } from '../../redux/slices/errorSlice' @@ -25,7 +25,7 @@ const ManualAddBookSection = ({ 'data-testid': testId }) => { e.preventDefault() if (title.trim() && authors.trim()) { if (!filterExistingBooks()) { - dispatch(addBook(createBook({ title, authors }))) + dispatch(syncAddBook(createBook({ title, authors }))) } else { dispatch(setError('The book is already in the List!')) } diff --git a/src/components/manualAddBookSection/ManualAddBookSection.test.js b/src/components/manualAddBookSection/ManualAddBookSection.test.js index 92594ca..6d2dd50 100644 --- a/src/components/manualAddBookSection/ManualAddBookSection.test.js +++ b/src/components/manualAddBookSection/ManualAddBookSection.test.js @@ -1,4 +1,4 @@ -import { fireEvent, screen } from '@testing-library/react' +import { fireEvent, screen, act } from '@testing-library/react' import { setup } from '../../setupTests' import store from '../../redux/store' import App from '../../App' @@ -9,8 +9,10 @@ const bookAuthorName = 'Test Book Author' let titleInput, authorsInput, submitBookBtn, bookListComponent describe('ManualAddBookSection Component Tests', () => { - beforeEach(() => { - setup(App, store) + beforeEach(async () => { + await act(async () => { + setup(App, store) + }) // ManualAddBookSection elements: titleInput = screen.getByTestId('manualAddBook_input_title') @@ -19,6 +21,10 @@ describe('ManualAddBookSection Component Tests', () => { bookListComponent = screen.getByTestId('bookList_section') }) + afterEach(() => { + document.body.innerHTML = '' + }) + test('Should render Title input', () => { expect(titleInput).toBeEnabled() expect(titleInput).toBeInTheDocument() diff --git a/src/components/searchBookSection/searchBookModal/SearchBookModal.css b/src/components/searchBookSection/searchBookModal/SearchBookModal.css index 823cc57..f7726ff 100644 --- a/src/components/searchBookSection/searchBookModal/SearchBookModal.css +++ b/src/components/searchBookSection/searchBookModal/SearchBookModal.css @@ -23,14 +23,15 @@ div[data-testid='modal_book_content'] { div[data-testid='book_left_content'] { display: flex; - align-items: center; + align-items: flex-start; justify-content: center; width: 30%; } img[data-testid='modal_book_img'] { - min-width: 50%; - height: 100%; + min-width: 70%; + max-height: 100%; + padding-top: 3rem; } div[data-testid='book_right_content'] { diff --git a/src/components/searchBookSection/searchBookModal/SearchBookModal.jsx b/src/components/searchBookSection/searchBookModal/SearchBookModal.jsx index 74a8680..691542a 100644 --- a/src/components/searchBookSection/searchBookModal/SearchBookModal.jsx +++ b/src/components/searchBookSection/searchBookModal/SearchBookModal.jsx @@ -1,6 +1,6 @@ import { useEffect } from 'react' import { useDispatch, useSelector } from 'react-redux' -import { addBook, selectBook } from '../../../redux/slices/booksSlice' +import { selectBook, syncAddBook } from '../../../redux/slices/booksSlice' import createBook from '../../../utils/createBook' import Button from '../../common/button/Button' import Modal from '../../common/modal/Modal' @@ -28,7 +28,7 @@ const SearchBookModal = ({ isOpen, onClose, searchResults }) => { }, [onClose, filteredSearchResults]) const handleAddBook = (bookFound) => { - dispatch(addBook(createBook(bookFound))) + dispatch(syncAddBook(createBook(bookFound))) } if (!searchResults || searchResults.length === 0) @@ -61,14 +61,13 @@ const SearchBookModal = ({ isOpen, onClose, searchResults }) => { ? 'Authors:' : 'Author:'} {' '} - {book.authors || 'Unknown Authors'} + {book.authors}

- Published Year: {book.publishedDate || 'N/A'} + Published Year: {book.publishedDate}

- Description:{' '} - {book.description || 'No description available'} + Description: {book.description}