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

Conversation

belwalshubham
Copy link
Contributor

@belwalshubham belwalshubham commented Apr 3, 2025

#12

  • Refactor user page code

Covered test cases for:

  • Rendering users and role dropdowns correctly.
  • Updating user roles and refreshing data.
  • Deleting a user with confirmation.
  • Handling pagination with the "Next" button.
  • Mocked useUserActions and useAuth hooks for isolated testing.

Summary by CodeRabbit

  • Tests

    • Added a comprehensive test suite to validate key user interactions, including data display, role updates, deletions, and pagination.
  • Refactor

    • Streamlined search and pagination handling for a more intuitive experience.
    • Enhanced loading feedback and user action responsiveness on the user management page.

@belwalshubham belwalshubham self-assigned this Apr 3, 2025
Copy link

coderabbitai bot commented Apr 7, 2025

Walkthrough

The pull request introduces a comprehensive test suite for the UsersPage component and refines the component’s code. The test file validates rendering, user role updates, deletions, and pagination using the React Testing Library with mocked hooks. In the component file, inline arrow functions have replaced separate handler functions, and additional data-testid attributes have been added to improve testability. The rendering logic has also been simplified by removing redundant conditional checks.

Changes

File(s) Change Summary
__tests__/UsersPage.test.js Added a new test suite for UsersPage that mocks hooks, verifies rendering of user data and roles, tests role updates, user deletions, and pagination behavior.
src/app/(app)/users/page.js Refactored UsersPage by removing dedicated handler functions in favor of inline arrow functions, added data-testid attributes to key elements, and streamlined rendering.

Sequence Diagram(s)

sequenceDiagram
  participant U as User
  participant SB as SearchBar
  participant UP as UsersPage
  participant UA as UserActions

  U->>SB: Types search query
  SB->>UP: Calls onSearchChange (setSearchQuery)
  UP->>UA: Fetches updated user list

  U->>UP: Clicks "Next" button
  UP->>UA: setCurrentPage(currentPage + 1)
  UA-->>UP: Returns paginated results

  U->>UP: Updates role / Requests deletion
  UP->>UA: Executes update or delete action
Loading

Poem

I'm a little bunny, hopping on the code trail,
With tests that confirm and refactors that never fail.
Inline arrows and IDs make my heart skip a beat,
Through each commit, oh, what a treat!
Hoppy and proud in this development tale! 🐰✨

✨ Finishing Touches
  • 📝 Generate Docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai plan to trigger planning for file edits and PR creation.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🔭 Outside diff range comments (1)
src/app/(app)/users/page.js (1)

10-45: 🛠️ Refactor suggestion

Consider implementing pagination state reset when search changes.

Currently, when a user performs a search, the pagination state isn't reset. This could lead to confusion if the filtered results have fewer pages than the original data, but the currentPage state remains unchanged.

Add a useEffect hook to reset pagination when search query changes:

  useEffect(() => {
    const lowerCaseQuery = searchQuery.toLowerCase();
    const results = users.filter(
      (user) =>
        user.name.toLowerCase().includes(lowerCaseQuery) ||
        user.email.toLowerCase().includes(lowerCaseQuery) ||
        user.roles.some((role) =>
          role.name.toLowerCase().includes(lowerCaseQuery)
        )
    );
    setFilteredUsers(results);
+   // Reset to first page when search query changes
+   if (searchQuery && currentPage !== 1) {
+     setCurrentPage(1);
+   }
-  }, [searchQuery, users]);
+  }, [searchQuery, users, currentPage]);
🧹 Nitpick comments (3)
__tests__/UsersPage.test.js (2)

72-84: Test for role updates is effective but could be more comprehensive.

While the test correctly verifies that the update function is called with the right parameters and that the user list is refreshed, consider adding assertions to verify UI feedback such as the toast message.

await waitFor(() => {
  expect(mockUpdateUserRoles).toHaveBeenCalledWith(2, ["Admin"]);
  expect(mockFetchUsers).toHaveBeenCalledTimes(2);
+  // Verify toast message is shown
+  expect(screen.getByText("Role updated successfully!")).toBeInTheDocument();
});

Note: You would need to mock the showToast function for this to work.


103-115: Pagination test verifies Next button functionality.

The test correctly verifies that clicking the Next button fetches the next page of users. Consider adding a similar test for the Previous button to ensure complete pagination coverage.

test("handles pagination with Previous button", async () => {
  mockFetchUsers.mockResolvedValueOnce({ data: mockUsers, last_page: 2 });
  
  render(<UsersPage />);
  
  // Set current page to 2
  await waitFor(() => expect(mockFetchUsers).toHaveBeenCalledTimes(1));
  const nextButton = await screen.findByTestId("pagination-next");
  fireEvent.click(nextButton);
  await waitFor(() => expect(mockFetchUsers).toHaveBeenCalledWith(2));
  
  // Now test Previous button
  const prevButton = await screen.findByTestId("pagination-previous");
  fireEvent.click(prevButton);
  
  await waitFor(() => expect(mockFetchUsers).toHaveBeenCalledWith(1));
});
src/app/(app)/users/page.js (1)

58-69: handleRoleChange could be optimized to avoid unnecessary fetches.

The current implementation fetches all users after a role update. For better performance, especially with large user lists, consider updating the user locally first before requesting a refresh.

  const handleRoleChange = async (userId, selectedRole) => {
    try {
      const rolePayload = selectedRole ? [selectedRole] : [];
      await updateUserRoles(userId, rolePayload);
      showToast("Role updated successfully!");
-     const updatedUsers = await fetchUsers(currentPage);
-     setUsers(updatedUsers?.data || []);
+     // Update the user locally first
+     setUsers(users.map(user => {
+       if (user.id === userId) {
+         return {
+           ...user,
+           roles: selectedRole ? [{ name: selectedRole }] : []
+         };
+       }
+       return user;
+     }));
+     // Then refresh data in the background
+     fetchUsers(currentPage).then(updatedUsers => {
+       setUsers(updatedUsers?.data || []);
+     });
    } catch (error) {
      showToast("An error occurred. Please try again.");
    }
  };
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 780c8fd and 24d5300.

📒 Files selected for processing (2)
  • __tests__/UsersPage.test.js (1 hunks)
  • src/app/(app)/users/page.js (3 hunks)
🔇 Additional comments (12)
__tests__/UsersPage.test.js (5)

1-10: Test setup follows good practices with proper imports and mocking.

The test file is well-structured with appropriate imports from testing libraries and proper mocking of the hooks. Using jest.mock() for the external hooks is a good approach for unit testing.


11-29: Good test data preparation.

The mock data for users and roles is well-structured and provides sufficient test coverage for different scenarios (admin vs regular user). This will allow the tests to verify various aspects of the UI rendering and interactions.


31-50: Well-structured test setup with proper isolation.

The beforeEach setup ensures each test runs with a fresh mock implementation, preventing test pollution. The mock implementation is comprehensive, covering all the necessary functions from the hooks.


52-70: Good initial rendering test with loading state verification.

This test effectively checks both the loading state and the fully rendered user list. It verifies that the loading indicator appears and disappears at the right time, and that user data and role dropdowns are properly rendered.


86-101: Delete user test covers the full workflow effectively.

This test comprehensively checks the user deletion flow, including the confirmation dialog and the subsequent API call. It correctly verifies that the user list is refreshed after deletion.

src/app/(app)/users/page.js (7)

86-86: Added data-testid for loading state to improve testability.

Adding the data-testid attribute to the loading message enhances testability by providing a reliable selector for tests.


94-94: Simplified event handler with inline arrow function.

Replacing a named handler function with an inline arrow function makes the code more concise in this case. Since the logic is simple (just setting state), this approach is reasonable.


117-118: Added data-testid to role select for improved testability.

The data-testid attribute with dynamic userId makes it easier to target specific role dropdowns in tests, which is a good practice for testability.


134-135: Added data-testid to delete button for improved testability.

Similar to the role select, the delete button now has a data-testid with the userId, making it easier to target in tests.


155-157: Added data-testid to pagination buttons and simplified with inline function.

The pagination buttons now have data-testid attributes and use inline arrow functions for the onClick handlers. This improves testability and makes the code more concise.


165-167: Added data-testid to Next button and simplified with inline function.

Similar to the Previous button, the Next button now has a data-testid and uses an inline arrow function, which improves testability and code conciseness.


177-177: Simplified onCancel handler with inline function.

Replacing a named handler function with an inline arrow function is appropriate here as the logic is simple (just resetting state).

Comment on lines +1 to +115
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));
});
});
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();
  });
});

Comment on lines +107 to +148
{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>
)}
))}
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!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants