Skip to content

Commit

Permalink
feat: enhance layout, add tests, and improve folder structure in Libr…
Browse files Browse the repository at this point in the history
…aryPage
  • Loading branch information
amalv committed Dec 27, 2023
1 parent fc8acf9 commit 3c32ba7
Show file tree
Hide file tree
Showing 12 changed files with 189 additions and 182 deletions.
62 changes: 27 additions & 35 deletions src/components/LibraryPage/LibraryPage.test.tsx
Original file line number Diff line number Diff line change
@@ -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 <div>Books</div>;
},
vi.mock("./hooks", () => ({
useLibraryPage: vi.fn(),
}));

describe("LibraryPage", () => {
it("renders correctly", () => {
render(<LibraryPage />);

expect(screen.getByText("Books")).toBeInTheDocument();
vi.mock("./components/Books", () => ({
Books: () => <div>Books</div>,
}));

expect(mockBooks).toHaveBeenCalledWith(
expect.objectContaining({
search: "", // Initial debouncedSearch value
limit: 50,
})
);
});
vi.mock("./components/Search", () => ({
Search: () => <div>Search</div>,
}));

it("updates search state correctly", async () => {
render(<LibraryPage />);
vi.mock("./components/UserAuthentication", () => ({
UserAuthentication: () => <div>UserAuthentication</div>,
}));

// 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(<LibraryPage />);
expect(screen.getByText("Books")).toBeInTheDocument();
expect(screen.getByText("Search")).toBeInTheDocument();
expect(screen.getByText("UserAuthentication")).toBeInTheDocument();
});
});
90 changes: 11 additions & 79 deletions src/components/LibraryPage/LibraryPage.tsx
Original file line number Diff line number Diff line change
@@ -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<SetStateAction<string>>;
}

const SearchInput = ({ search, setSearch }: SearchInputProps) => {
const handleSearchChange = (event: ChangeEvent<HTMLInputElement>) => {
setSearch(event.target.value);
};

return (
<Box
py={2}
display="flex"
justifyContent="center"
width="100%"
alignSelf="center"
>
<StyledTextField
label="Search by title or author"
variant="outlined"
onChange={handleSearchChange}
fullWidth
value={search}
/>
</Box>
);
};
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: <CircularProgress />,
authenticated:
user && user.name ? (
<div>
<Avatar onClick={handleMenuOpen}>{user?.name[0]}</Avatar>
<Menu
anchorEl={anchorEl}
open={Boolean(anchorEl)}
onClose={handleMenuClose}
>
<MenuItem onClick={handleLogout}>Logout</MenuItem>
</Menu>
</div>
) : null,
unauthenticated: <LoginButton />,
};
const { search, setSearch, debouncedSearch, error, setError } =
useLibraryPage();

return (
<Root>
<Snackbar
aria-live="polite"
open={!!error}
autoHideDuration={6000}
onClose={() => setError(null)}
Expand All @@ -88,15 +20,15 @@ export const LibraryPage = () => {
</Alert>
</Snackbar>
<Grid container>
<Grid item xs={1} sm={1} md={2} />
<Grid item xs={10} sm={10} md={8}>
<Grid item xs={0.5} sm={1} md={2} lg={2} />
<Grid item xs={11} sm={10} md={8} lg={8}>
<Box display="flex" justifyContent="flex-end" mt={2}>
{userStates[userState]}
<UserAuthentication />
</Box>
<SearchInput search={search} setSearch={setSearch} />
<Search search={search} setSearch={setSearch} />
<Books search={debouncedSearch} limit={50} />
</Grid>
<Grid item xs={1} sm={1} md={2} />
<Grid item xs={0.5} sm={1} md={2} lg={2} />
</Grid>
</Root>
);
Expand Down
9 changes: 1 addition & 8 deletions src/components/LibraryPage/components/Books/Books.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,14 +70,7 @@ export const Books = ({ search, limit }: BooksProps) => {
</Alert>
</Snackbar>
{loading ? (
<Grid
item
xs={12}
container
justifyContent="center"
alignItems="center"
minHeight="200px"
>
<Grid item xs={12} container minHeight="200px">
<CircularProgress />
</Grid>
) : books.length > 0 ? (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ interface BookCardProps {
}

export const BookCard: React.FC<BookCardProps> = ({ book }) => (
<Grid item xs={6} sm={4} md={4} lg={4} xl={2} key={book.title}>
<Grid item xs={6} sm={4} md={4} lg={3} xl={2} key={book.title}>
<CardWrapper>
<CardActionAreaWrapper>
<Cover
Expand Down
32 changes: 32 additions & 0 deletions src/components/LibraryPage/components/Search/Search.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { ChangeEvent, Dispatch, SetStateAction } from "react";
import { Box } from "@mui/material";
import { StyledTextField } from "./SearchInput.styles";

interface SearchProps {
search: string;
setSearch: Dispatch<SetStateAction<string>>;
}

export const Search = ({ search, setSearch }: SearchProps) => {
const handleSearchChange = (event: ChangeEvent<HTMLInputElement>) => {
setSearch(event.target.value);
};

return (
<Box
py={2}
display="flex"
justifyContent="center"
width="100%"
alignSelf="center"
>
<StyledTextField
label="Search by title or author"
variant="outlined"
onChange={handleSearchChange}
fullWidth
value={search}
/>
</Box>
);
};
22 changes: 22 additions & 0 deletions src/components/LibraryPage/components/Search/SearchInput.styles.ts
Original file line number Diff line number Diff line change
@@ -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
};
}
}
`;
});
1 change: 1 addition & 0 deletions src/components/LibraryPage/components/Search/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./Search";
Original file line number Diff line number Diff line change
@@ -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(<UserAuthentication />);
expect(screen.getByRole("progressbar")).toBeInTheDocument();
});

it("should render authenticated state", async () => {
(useAuth0 as jest.Mock).mockReturnValue({
isLoading: false,
user: { name: "Test User" },
});

render(<UserAuthentication />);
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(<UserAuthentication />);
expect(screen.getByRole("button", { name: /log in/i })).toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
@@ -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 | HTMLElement>(null);

const handleMenuOpen = useCallback(
(event: React.MouseEvent<HTMLDivElement>) => {
setAnchorEl(event.currentTarget);
},
[]
);

const handleMenuClose = useCallback(() => {
setAnchorEl(null);
}, []);

const handleLogout = useCallback(
(event: React.MouseEvent<HTMLLIElement>) => {
event.preventDefault();
logout();
},
[logout]
);

if (isLoading) {
return <CircularProgress />;
}

if (user?.name) {
return (
<div>
<Avatar onClick={handleMenuOpen}>{user.name[0]}</Avatar>
<Menu
anchorEl={anchorEl}
open={Boolean(anchorEl)}
onClose={handleMenuClose}
>
<MenuItem onClick={handleLogout}>Logout</MenuItem>
</Menu>
</div>
);
}

return <LoginButton />;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./UserAuthentication";
2 changes: 2 additions & 0 deletions src/components/LibraryPage/components/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
export * from "./Books";
export * from "./Search";
export * from "./UserAuthentication";
Loading

0 comments on commit 3c32ba7

Please sign in to comment.