diff --git a/src/components/LibraryPage/LibraryPage.test.tsx b/src/components/LibraryPage/LibraryPage.test.tsx index f86b18d..e519fb7 100644 --- a/src/components/LibraryPage/LibraryPage.test.tsx +++ b/src/components/LibraryPage/LibraryPage.test.tsx @@ -1,46 +1,38 @@ -import { render, screen, fireEvent, act } from "@testing-library/react"; -import { describe, expect, it, vi } from "vitest"; +import { render, screen } from "@testing-library/react"; import { LibraryPage } from "./LibraryPage"; -import { BooksProps } from "./components"; +import { useLibraryPage } from "./hooks"; +import { vi } from "vitest"; -const mockBooks = vi.fn(); -vi.mock("./components/Books/Books", () => ({ - Books: (props: BooksProps) => { - mockBooks(props); - return
Books
; - }, +vi.mock("./hooks", () => ({ + useLibraryPage: vi.fn(), })); -describe("LibraryPage", () => { - it("renders correctly", () => { - render(); - - expect(screen.getByText("Books")).toBeInTheDocument(); +vi.mock("./components/Books", () => ({ + Books: () =>
Books
, +})); - expect(mockBooks).toHaveBeenCalledWith( - expect.objectContaining({ - search: "", // Initial debouncedSearch value - limit: 50, - }) - ); - }); +vi.mock("./components/Search", () => ({ + Search: () =>
Search
, +})); - it("updates search state correctly", async () => { - render(); +vi.mock("./components/UserAuthentication", () => ({ + UserAuthentication: () =>
UserAuthentication
, +})); - // Simulate user input to the SearchInput component - fireEvent.change(screen.getByLabelText("Search by title or author"), { - target: { value: "New search value" }, +describe("LibraryPage", () => { + it("renders without crashing", () => { + (useLibraryPage as jest.Mock).mockReturnValue({ + search: "", + setSearch: vi.fn(), + debouncedSearch: "", + error: null, + handleLogout: vi.fn(), + setError: vi.fn(), }); - // Wait for the debounce delay - await act(() => new Promise((resolve) => setTimeout(resolve, 500))); - - // Check that the Books component was called with the updated debouncedSearch value - expect(mockBooks).toHaveBeenCalledWith( - expect.objectContaining({ - search: "New search value", - }) - ); + render(); + expect(screen.getByText("Books")).toBeInTheDocument(); + expect(screen.getByText("Search")).toBeInTheDocument(); + expect(screen.getByText("UserAuthentication")).toBeInTheDocument(); }); }); diff --git a/src/components/LibraryPage/LibraryPage.tsx b/src/components/LibraryPage/LibraryPage.tsx index fff8493..a7bdb56 100644 --- a/src/components/LibraryPage/LibraryPage.tsx +++ b/src/components/LibraryPage/LibraryPage.tsx @@ -1,84 +1,16 @@ -import { ChangeEvent, Dispatch, SetStateAction } from "react"; import { useLibraryPage } from "./hooks"; -import { - Alert, - Avatar, - Box, - CircularProgress, - Grid, - Menu, - MenuItem, - Snackbar, -} from "@mui/material"; -import { Root, StyledTextField } from "./LibraryPage.styles"; -import { Books } from "./components"; -import { LoginButton } from "./components/LoginButton/LoginButton"; - -interface SearchInputProps { - search: string; - setSearch: Dispatch>; -} - -const SearchInput = ({ search, setSearch }: SearchInputProps) => { - const handleSearchChange = (event: ChangeEvent) => { - setSearch(event.target.value); - }; - - return ( - - - - ); -}; +import { Alert, Box, Grid, Snackbar } from "@mui/material"; +import { Root } from "./LibraryPage.styles"; +import { Books, Search, UserAuthentication } from "./components"; export const LibraryPage = () => { - const { - search, - setSearch, - debouncedSearch, - error, - user, - anchorEl, - handleMenuOpen, - handleMenuClose, - handleLogout, - setError, - userState, - } = useLibraryPage(); - - const userStates = { - loading: , - authenticated: - user && user.name ? ( -
- {user?.name[0]} - - Logout - -
- ) : null, - unauthenticated: , - }; + const { search, setSearch, debouncedSearch, error, setError } = + useLibraryPage(); return ( setError(null)} @@ -88,15 +20,15 @@ export const LibraryPage = () => { - - + + - {userStates[userState]} + - + - + ); diff --git a/src/components/LibraryPage/components/Books/Books.tsx b/src/components/LibraryPage/components/Books/Books.tsx index 5e1cbdf..dbc88ef 100644 --- a/src/components/LibraryPage/components/Books/Books.tsx +++ b/src/components/LibraryPage/components/Books/Books.tsx @@ -70,14 +70,7 @@ export const Books = ({ search, limit }: BooksProps) => { {loading ? ( - + ) : books.length > 0 ? ( diff --git a/src/components/LibraryPage/components/Books/components/BookCard/BookCard.tsx b/src/components/LibraryPage/components/Books/components/BookCard/BookCard.tsx index 53d3a72..fc7ae9f 100644 --- a/src/components/LibraryPage/components/Books/components/BookCard/BookCard.tsx +++ b/src/components/LibraryPage/components/Books/components/BookCard/BookCard.tsx @@ -20,7 +20,7 @@ interface BookCardProps { } export const BookCard: React.FC = ({ book }) => ( - + >; +} + +export const Search = ({ search, setSearch }: SearchProps) => { + const handleSearchChange = (event: ChangeEvent) => { + setSearch(event.target.value); + }; + + return ( + + + + ); +}; diff --git a/src/components/LibraryPage/components/Search/SearchInput.styles.ts b/src/components/LibraryPage/components/Search/SearchInput.styles.ts new file mode 100644 index 0000000..adcaa29 --- /dev/null +++ b/src/components/LibraryPage/components/Search/SearchInput.styles.ts @@ -0,0 +1,22 @@ +import { TextField } from "@mui/material"; +import { styled } from "@mui/material/styles"; + +export const StyledTextField = styled(TextField)(({ theme }) => { + return ` + .MuiInputBase-input { + background-color: ${ + theme.palette?.mode === "dark" + ? "rgba(255, 255, 255, 0.15)" + : undefined + }; + color: ${theme.palette?.mode === "dark" ? "#fff" : undefined}; + &::placeholder { + color: ${ + theme.palette?.mode === "dark" + ? "rgba(255, 255, 255, 0.5)" + : undefined + }; + } + } + `; +}); diff --git a/src/components/LibraryPage/components/Search/index.ts b/src/components/LibraryPage/components/Search/index.ts new file mode 100644 index 0000000..f3cfe1b --- /dev/null +++ b/src/components/LibraryPage/components/Search/index.ts @@ -0,0 +1 @@ +export * from "./Search"; diff --git a/src/components/LibraryPage/components/UserAuthentication/UserAuthentication.test.tsx b/src/components/LibraryPage/components/UserAuthentication/UserAuthentication.test.tsx new file mode 100644 index 0000000..1d19ec8 --- /dev/null +++ b/src/components/LibraryPage/components/UserAuthentication/UserAuthentication.test.tsx @@ -0,0 +1,40 @@ +import { render, fireEvent, waitFor, screen } from "@testing-library/react"; +import { UserAuthentication } from "./UserAuthentication"; +import { useAuth0 } from "@auth0/auth0-react"; +import { vi } from "vitest"; + +vi.mock("@auth0/auth0-react", () => ({ + useAuth0: vi.fn(), +})); + +describe("UserAuthentication", () => { + it("should render loading state", () => { + (useAuth0 as jest.Mock).mockReturnValue({ + isLoading: true, + }); + + render(); + expect(screen.getByRole("progressbar")).toBeInTheDocument(); + }); + + it("should render authenticated state", async () => { + (useAuth0 as jest.Mock).mockReturnValue({ + isLoading: false, + user: { name: "Test User" }, + }); + + render(); + fireEvent.click(screen.getByText("T")); + await waitFor(() => expect(screen.getByRole("menu")).toBeInTheDocument()); + }); + + it("should render unauthenticated state", () => { + (useAuth0 as jest.Mock).mockReturnValue({ + isLoading: false, + user: null, + }); + + render(); + expect(screen.getByRole("button", { name: /log in/i })).toBeInTheDocument(); + }); +}); diff --git a/src/components/LibraryPage/components/UserAuthentication/UserAuthentication.tsx b/src/components/LibraryPage/components/UserAuthentication/UserAuthentication.tsx new file mode 100644 index 0000000..2e415ec --- /dev/null +++ b/src/components/LibraryPage/components/UserAuthentication/UserAuthentication.tsx @@ -0,0 +1,49 @@ +import { Avatar, CircularProgress, Menu, MenuItem } from "@mui/material"; +import { useAuth0 } from "@auth0/auth0-react"; +import { useState, useCallback } from "react"; +import { LoginButton } from "../LoginButton"; + +export const UserAuthentication = () => { + const { user, isLoading, logout } = useAuth0(); + const [anchorEl, setAnchorEl] = useState(null); + + const handleMenuOpen = useCallback( + (event: React.MouseEvent) => { + setAnchorEl(event.currentTarget); + }, + [] + ); + + const handleMenuClose = useCallback(() => { + setAnchorEl(null); + }, []); + + const handleLogout = useCallback( + (event: React.MouseEvent) => { + event.preventDefault(); + logout(); + }, + [logout] + ); + + if (isLoading) { + return ; + } + + if (user?.name) { + return ( +
+ {user.name[0]} + + Logout + +
+ ); + } + + return ; +}; diff --git a/src/components/LibraryPage/components/UserAuthentication/index.ts b/src/components/LibraryPage/components/UserAuthentication/index.ts new file mode 100644 index 0000000..39b7edd --- /dev/null +++ b/src/components/LibraryPage/components/UserAuthentication/index.ts @@ -0,0 +1 @@ +export * from "./UserAuthentication"; diff --git a/src/components/LibraryPage/components/index.ts b/src/components/LibraryPage/components/index.ts index 355dfc2..0ddee35 100644 --- a/src/components/LibraryPage/components/index.ts +++ b/src/components/LibraryPage/components/index.ts @@ -1 +1,3 @@ export * from "./Books"; +export * from "./Search"; +export * from "./UserAuthentication"; diff --git a/src/components/LibraryPage/hooks/useLibraryPage.ts b/src/components/LibraryPage/hooks/useLibraryPage.ts index 570a376..804fa8d 100644 --- a/src/components/LibraryPage/hooks/useLibraryPage.ts +++ b/src/components/LibraryPage/hooks/useLibraryPage.ts @@ -1,22 +1,12 @@ -import { useState, useCallback, useMemo } from "react"; +import { useState } from "react"; import { useDebounce } from "./useDebounce"; -import { User, useAuth0 } from "@auth0/auth0-react"; - -type UserState = "loading" | "authenticated" | "unauthenticated"; interface LibraryPageHook { search: string; setSearch: React.Dispatch>; debouncedSearch: string; error: Error | null; - user: User | undefined; - anchorEl: null | HTMLElement; - isLoading: boolean; - handleMenuOpen: (event: React.MouseEvent) => void; - handleMenuClose: () => void; - handleLogout: (event: React.MouseEvent) => void; setError: React.Dispatch>; - userState: UserState; } export const useLibraryPage = ( @@ -25,53 +15,6 @@ export const useLibraryPage = ( const [error, setError] = useState(null); const [search, setSearch] = useState(""); const debouncedSearch = useDebounce(search, debounceDelay); - const { user, logout, isLoading } = useAuth0(); - const [anchorEl, setAnchorEl] = useState(null); - - const handleMenuOpen = useCallback( - (event: React.MouseEvent) => { - setAnchorEl(event.currentTarget); - }, - [] - ); - - const handleMenuClose = useCallback(() => { - setAnchorEl(null); - }, []); - - const handleLogout = useCallback( - async (event: React.MouseEvent) => { - event.preventDefault(); - try { - await logout(); - } catch (error) { - setError(new Error("Logout failed")); - console.error("Logout failed"); - } - }, - [logout] - ); - - const getUserState = (): UserState => { - if (isLoading) return "loading"; - if (user && user.name) return "authenticated"; - return "unauthenticated"; - }; - - const userState = useMemo(getUserState, [isLoading, user]); - return { - search, - setSearch, - debouncedSearch, - isLoading, - user, - anchorEl, - error, - handleMenuOpen, - handleMenuClose, - handleLogout, - setError, - userState, - }; + return { search, setSearch, debouncedSearch, error, setError }; };