From 80d097547af7fa3650f8d5a68768d13c5b54dcdb Mon Sep 17 00:00:00 2001 From: Akpolo Ogagaoghene Prince Date: Mon, 30 Mar 2026 11:38:47 +0100 Subject: [PATCH] feat(#352): add skeleton loaders for employee table Replace blank table body with animated pulse skeleton rows while data is loading. Add `isLoading` prop to `EmployeeList` and a separate `EmployeeSkeletonRow` component that mirrors the column layout of the real rows using Tailwind's `animate-pulse`. Adds two new tests: - skeleton rows are rendered (not employee data) when isLoading=true - empty-state message appears when not loading and list is empty --- frontend/src/components/EmployeeList.tsx | 48 ++++++++++++++++++- .../__tests__/EmployeeList.test.tsx | 20 ++++++++ 2 files changed, 67 insertions(+), 1 deletion(-) diff --git a/frontend/src/components/EmployeeList.tsx b/frontend/src/components/EmployeeList.tsx index 9167749..2b92767 100644 --- a/frontend/src/components/EmployeeList.tsx +++ b/frontend/src/components/EmployeeList.tsx @@ -20,6 +20,7 @@ interface Employee { interface EmployeeListProps { employees: Employee[]; + isLoading?: boolean; onEmployeeClick?: (employee: Employee) => void; onAddEmployee: (employee: Employee) => void; onEditEmployee?: (employee: Employee) => void; @@ -27,8 +28,49 @@ interface EmployeeListProps { onUpdateEmployeeImage?: (id: string, imageUrl: string) => void; } +const SKELETON_ROW_COUNT = 5; + +const EmployeeSkeletonRow: React.FC = () => ( + + {/* Name column */} + +
+
+
+
+
+
+
+ + {/* Role */} + +
+ + {/* Wallet */} + +
+ + {/* Salary */} + +
+ + {/* Status */} + +
+ + {/* Actions */} + +
+
+
+
+ + +); + export const EmployeeList: React.FC = ({ employees, + isLoading = false, onAddEmployee, onEditEmployee, onRemoveEmployee, @@ -212,7 +254,11 @@ export const EmployeeList: React.FC = ({ - {sortedEmployees.length === 0 ? ( + {isLoading ? ( + Array.from({ length: SKELETON_ROW_COUNT }, (_, i) => ( + + )) + ) : sortedEmployees.length === 0 ? ( {debouncedSearch ? `No employees match "${debouncedSearch}"` : 'No employees found'} diff --git a/frontend/src/components/__tests__/EmployeeList.test.tsx b/frontend/src/components/__tests__/EmployeeList.test.tsx index 022464a..ad7a294 100644 --- a/frontend/src/components/__tests__/EmployeeList.test.tsx +++ b/frontend/src/components/__tests__/EmployeeList.test.tsx @@ -40,4 +40,24 @@ describe('EmployeeList', () => { expect(email).toHaveAttribute('title', employee.email); expect(email.className).toContain('truncate'); }); + + test('renders skeleton rows and hides employee data while loading', () => { + render(); + + // Employee data must not be visible during loading + expect(screen.queryByLabelText(`Employee name: ${employee.name}`)).toBeNull(); + expect(screen.queryByLabelText(`Employee email: ${employee.email}`)).toBeNull(); + + // Skeleton rows are rendered with pulse animation + const rows = document.querySelectorAll('tbody tr'); + expect(rows.length).toBe(5); + rows.forEach((row) => { + expect(row.className).toContain('animate-pulse'); + }); + }); + + test('renders empty state message when not loading and no employees exist', () => { + render(); + expect(screen.getByText('No employees found')).toBeTruthy(); + }); });