Skip to content

Feat/user page test cases #48

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
115 changes: 115 additions & 0 deletions __tests__/UsersPage.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
import UsersPage from "../src/app/(app)/users/page";
import { useUserActions } from "@/hooks/user";
import { useAuth } from "@/hooks/auth";
import React from "react";

// Mock hooks
jest.mock("@/hooks/user");
jest.mock("@/hooks/auth");

const mockUsers = [
{
id: 1,
name: "John Doe",
email: "[email protected]",
roles: [{ name: "Admin" }],
},
{
id: 2,
name: "Jane Smith",
email: "[email protected]",
roles: [{ name: "User" }],
},
];

const mockRoles = [
{ id: 1, name: "Admin" },
{ id: 2, name: "User" },
];

describe("UsersPage Component", () => {
let mockFetchUsers, mockFetchRoles, mockUpdateUserRoles, mockDeleteUser;

beforeEach(() => {
mockFetchUsers = jest
.fn()
.mockResolvedValue({ data: mockUsers, last_page: 1 });
mockFetchRoles = jest.fn().mockResolvedValue(mockRoles);
mockUpdateUserRoles = jest.fn().mockResolvedValue({});
mockDeleteUser = jest.fn().mockResolvedValue({});

useUserActions.mockReturnValue({
fetchUsers: mockFetchUsers,
fetchRoles: mockFetchRoles,
updateUserRoles: mockUpdateUserRoles,
deleteUser: mockDeleteUser,
});

useAuth.mockReturnValue({ user: { id: 3, name: "Admin User" } });
});

test("renders user list and roles dropdown correctly", async () => {
render(<UsersPage />);

// Check loading state
expect(screen.getByTestId("loading-users")).toBeInTheDocument();

await waitFor(() => {
expect(mockFetchUsers).toHaveBeenCalledTimes(1);
expect(screen.queryByTestId("loading-users")).not.toBeInTheDocument();
});

// Verify user data
expect(screen.getByText("John Doe")).toBeInTheDocument();
expect(screen.getByText("Jane Smith")).toBeInTheDocument();

// Verify role selects
expect(screen.getByTestId("role-select-1")).toBeInTheDocument();
expect(screen.getByTestId("role-select-2")).toBeInTheDocument();
});

test("updates user role and refreshes data", async () => {
render(<UsersPage />);

await waitFor(() => expect(mockFetchUsers).toHaveBeenCalledTimes(1));

const roleSelect = await screen.findByTestId("role-select-2");
fireEvent.change(roleSelect, { target: { value: "Admin" } });

await waitFor(() => {
expect(mockUpdateUserRoles).toHaveBeenCalledWith(2, ["Admin"]);
expect(mockFetchUsers).toHaveBeenCalledTimes(2);
});
});

test("deletes a user after confirmation", async () => {
render(<UsersPage />);

await waitFor(() => expect(mockFetchUsers).toHaveBeenCalledTimes(1));

const deleteButton = await screen.findByTestId("delete-user-2");
fireEvent.click(deleteButton);

const confirmButton = await screen.findByText("Confirm");
fireEvent.click(confirmButton);

await waitFor(() => {
expect(mockDeleteUser).toHaveBeenCalledWith(2);
expect(mockFetchUsers).toHaveBeenCalledTimes(2);
});
});

test("handles pagination with Next button", async () => {
mockFetchUsers.mockResolvedValueOnce({ data: mockUsers, last_page: 2 });

render(<UsersPage />);

await waitFor(() => expect(mockFetchUsers).toHaveBeenCalledTimes(1));

const nextButton = await screen.findByTestId("pagination-next");
fireEvent.click(nextButton);

await waitFor(() => expect(mockFetchUsers).toHaveBeenCalledWith(2));
});
});
Comment on lines +1 to +115
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Consider adding tests for search functionality and error handling.

The current test suite covers rendering, role updates, deletion, and pagination but doesn't test the search functionality or error handling. Adding tests for these scenarios would provide more comprehensive coverage.

For search functionality:

test("filters users based on search query", async () => {
  render(<UsersPage />);
  
  await waitFor(() => expect(mockFetchUsers).toHaveBeenCalledTimes(1));
  
  const searchBar = screen.getByPlaceholderText("Search...");
  fireEvent.change(searchBar, { target: { value: "john" } });
  
  expect(screen.getByText("John Doe")).toBeInTheDocument();
  expect(screen.queryByText("Jane Smith")).not.toBeInTheDocument();
});

For error handling:

test("shows error message when API call fails", async () => {
  // Mock failed API call
  mockFetchUsers.mockRejectedValueOnce(new Error("API Error"));
  
  render(<UsersPage />);
  
  await waitFor(() => {
    expect(screen.getByText("An error occurred. Please try again.")).toBeInTheDocument();
  });
});

111 changes: 46 additions & 65 deletions src/app/(app)/users/page.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,25 +83,15 @@ const UsersPage = () => {
setUserToDelete(null);
};

const handleSearchChange = (value) => {
setSearchQuery(value);
};

const handlePageChange = (newPage) => {
if (newPage >= 1 && newPage <= totalPages) {
setCurrentPage(newPage);
}
};

if (loading) return <p>Loading users...</p>;
if (loading) return <p data-testid="loading-users">Loading users...</p>;

return (
<div className="py-12">
<div className="max-w-7xl mx-auto sm:px-6 lg:px-8">
<h1 className="text-2xl font-bold mb-6">Manage Users</h1>
<SearchBar
searchQuery={searchQuery}
onSearchChange={handleSearchChange}
onSearchChange={(value) => setSearchQuery(value)}
/>
<div className="overflow-x-auto mt-4">
<table className="min-w-full table-auto border rounded-lg overflow-hidden">
Expand All @@ -114,73 +104,67 @@ const UsersPage = () => {
</tr>
</thead>
<tbody>
{filteredUsers.length > 0 ? (
filteredUsers.map((user) => (
<tr key={user.id} className="odd:bg-gray-50 even:bg-white">
<td className="p-4 text-gray-700 truncate max-w-xs">
{user.name}
</td>
<td className="p-4 text-gray-700 truncate max-w-xs">
{user.email}
</td>
<td className="p-4 text-center">
<select
value={user?.roles?.[0]?.name || ""}
onChange={(e) =>
handleRoleChange(user.id, e.target.value)
{filteredUsers.map((user) => (
<tr key={user.id} className="odd:bg-gray-50 even:bg-white">
<td className="p-4 text-gray-700 truncate max-w-xs">
{user.name}
</td>
<td className="p-4 text-gray-700 truncate max-w-xs">
{user.email}
</td>
<td className="p-4 text-center">
<select
data-testid={`role-select-${user.id}`}
value={user?.roles?.[0]?.name || ""}
onChange={(e) =>
handleRoleChange(user.id, e.target.value)
}
className="w-32 p-2 border rounded"
>
<option value="">Select Role</option>
{roles.map((role) => (
<option key={role.id} value={role.name}>
{role.name}
</option>
))}
</select>
</td>
<td className="p-4 text-center">
<Button
data-testid={`delete-user-${user.id}`}
onClick={() => {
if (user.id !== currentUser.id) {
setUserToDelete(user.id);
setIsPopupOpen(true);
}
className="w-32 p-2 border rounded"
>
<option value="">Select Role</option>
{roles.map((role) => (
<option key={role.id} value={role.name}>
{role.name}
</option>
))}
</select>
</td>
<td className="p-4 text-center">
<Button
onClick={() => {
if (user.id !== currentUser.id) {
setUserToDelete(user.id);
setIsPopupOpen(true);
}
}}
disabled={user.id === currentUser.id}
className="bg-red-500 hover:bg-red-600"
>
Delete
</Button>
</td>
</tr>
))
) : (
<tr>
<td colSpan="4" className="p-4 text-center">
No users found.
}}
disabled={user.id === currentUser.id}
className="bg-red-500 hover:bg-red-600"
>
Delete
</Button>
</td>
</tr>
)}
))}
Comment on lines +107 to +148
Copy link

@coderabbitai coderabbitai bot Apr 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

User list rendering has been simplified but lacks empty state handling.

The code now directly maps over filteredUsers without checking if the array is empty. While this simplifies the code, it doesn't handle the case when no users match the search criteria.

Add a conditional to handle empty state:

-{filteredUsers.map((user) => (
+{filteredUsers.length > 0 ? (
+  filteredUsers.map((user) => (
     <tr key={user.id} className="odd:bg-gray-50 even:bg-white">
       {/* Row content */}
     </tr>
-  ))}
+  ))
+) : (
+  <tr>
+    <td colSpan="4" className="p-4 text-center text-gray-500">
+      No users found
+    </td>
+  </tr>
+)}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{filteredUsers.map((user) => (
<tr key={user.id} className="odd:bg-gray-50 even:bg-white">
<td className="p-4 text-gray-700 truncate max-w-xs">
{user.name}
</td>
<td className="p-4 text-gray-700 truncate max-w-xs">
{user.email}
</td>
<td className="p-4 text-center">
<select
data-testid={`role-select-${user.id}`}
value={user?.roles?.[0]?.name || ""}
onChange={(e) =>
handleRoleChange(user.id, e.target.value)
}
className="w-32 p-2 border rounded"
>
<option value="">Select Role</option>
{roles.map((role) => (
<option key={role.id} value={role.name}>
{role.name}
</option>
))}
</select>
</td>
<td className="p-4 text-center">
<Button
data-testid={`delete-user-${user.id}`}
onClick={() => {
if (user.id !== currentUser.id) {
setUserToDelete(user.id);
setIsPopupOpen(true);
}
className="w-32 p-2 border rounded"
>
<option value="">Select Role</option>
{roles.map((role) => (
<option key={role.id} value={role.name}>
{role.name}
</option>
))}
</select>
</td>
<td className="p-4 text-center">
<Button
onClick={() => {
if (user.id !== currentUser.id) {
setUserToDelete(user.id);
setIsPopupOpen(true);
}
}}
disabled={user.id === currentUser.id}
className="bg-red-500 hover:bg-red-600"
>
Delete
</Button>
</td>
</tr>
))
) : (
<tr>
<td colSpan="4" className="p-4 text-center">
No users found.
}}
disabled={user.id === currentUser.id}
className="bg-red-500 hover:bg-red-600"
>
Delete
</Button>
</td>
</tr>
)}
))}
{filteredUsers.length > 0 ? (
filteredUsers.map((user) => (
<tr key={user.id} className="odd:bg-gray-50 even:bg-white">
<td className="p-4 text-gray-700 truncate max-w-xs">
{user.name}
</td>
<td className="p-4 text-gray-700 truncate max-w-xs">
{user.email}
</td>
<td className="p-4 text-center">
<select
data-testid={`role-select-${user.id}`}
value={user?.roles?.[0]?.name || ""}
onChange={(e) =>
handleRoleChange(user.id, e.target.value)
}
className="w-32 p-2 border rounded"
>
<option value="">Select Role</option>
{roles.map((role) => (
<option key={role.id} value={role.name}>
{role.name}
</option>
))}
</select>
</td>
<td className="p-4 text-center">
<Button
data-testid={`delete-user-${user.id}`}
onClick={() => {
if (user.id !== currentUser.id) {
setUserToDelete(user.id);
setIsPopupOpen(true);
}
}}
disabled={user.id === currentUser.id}
className="bg-red-500 hover:bg-red-600"
>
Delete
</Button>
</td>
</tr>
))
) : (
<tr>
<td colSpan="4" className="p-4 text-center text-gray-500">
No users found
</td>
</tr>
)}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@belwalshubham I think we need empty state handling.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

</tbody>
</table>
</div>

<div className="flex justify-between mt-6">
<Button
onClick={() => handlePageChange(currentPage - 1)}
data-testid="pagination-previous"
onClick={() => setCurrentPage(currentPage - 1)}
disabled={currentPage === 1}
className="px-4 py-2 bg-gray-300 rounded"
>
Previous
</Button>
<span>
Page {currentPage} of {totalPages}
</span>
<Button
onClick={() => handlePageChange(currentPage + 1)}
data-testid="pagination-next"
onClick={() => setCurrentPage(currentPage + 1)}
disabled={currentPage === totalPages}
className="px-4 py-2 bg-gray-300 rounded"
>
Next
</Button>
Expand All @@ -190,10 +174,7 @@ const UsersPage = () => {
isOpen={isPopupOpen}
message="Are you sure you want to delete this user?"
onConfirm={handleConfirmDelete}
onCancel={() => {
setIsPopupOpen(false);
setUserToDelete(null);
}}
onCancel={() => setIsPopupOpen(false)}
/>
</div>
</div>
Expand Down