import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest' import { render, screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' import EmployeeStatusCard from '../EmployeeStatusCard' import { onboardingService } from '../../services/onboarding.service' import type { EmployeeStatusResponse } from '../../types/onboarding' // Mock the onboarding service vi.mock('../../services/onboarding.service', () => ({ onboardingService: { getEmployeeStatus: vi.fn(), offboardEmployee: vi.fn(), }, })) // Mock Next.js navigation vi.mock('next/navigation', () => ({ useRouter: () => ({ push: vi.fn(), refresh: vi.fn(), }), })) describe('EmployeeStatusCard', () => { // Mock window.confirm const originalConfirm = window.confirm beforeEach(() => { // Mock window.confirm to return true by default window.confirm = vi.fn(() => true) }) afterEach(() => { // Restore original confirm window.confirm = originalConfirm }) const mockEmployeeStatus: EmployeeStatusResponse = { employee: { tenant_id: 1, seq_no: 1, tenant_emp_code: 'PWD0001', name: '王明', keycloak_user_id: '550e8400-e29b-41d4-a716-446655440000', keycloak_username: 'wang.ming', hire_date: '2026-02-21', resign_date: null, employment_status: 'active', storage_quota_gb: 20, email_quota_mb: 5120, }, departments: [ { department_id: 9, department_name: '玄鐵風能', position: '資深工程師', membership_type: 'permanent', joined_at: '2026-02-21T10:00:00', }, { department_id: 10, department_name: '國際碳權', position: '顧問', membership_type: 'concurrent', joined_at: '2026-02-21T10:00:00', }, ], roles: [ { role_id: 1, role_name: 'HR管理員', role_code: 'HR_ADMIN', assigned_at: '2026-02-21T10:00:00', }, { role_id: 2, role_name: '系統管理員', role_code: 'SYS_ADMIN', assigned_at: '2026-02-21T10:00:00', }, ], services: [ { service_id: 1, service_name: '單一簽入', service_code: 'SSO', quota_gb: null, quota_mb: null, enabled_at: '2026-02-21T10:00:00', }, { service_id: 2, service_name: '雲端硬碟', service_code: 'DRIVE', quota_gb: 20, quota_mb: null, enabled_at: '2026-02-21T10:00:00', }, { service_id: 3, service_name: '電子郵件', service_code: 'EMAIL', quota_gb: null, quota_mb: 5120, enabled_at: '2026-02-21T10:00:00', }, ], } beforeEach(() => { vi.clearAllMocks() }) describe('Loading State', () => { it('should show loading spinner while fetching data', () => { vi.mocked(onboardingService.getEmployeeStatus).mockImplementation( () => new Promise(() => {}) // Never resolves ) render() expect(screen.getByText(/Loading/i)).toBeInTheDocument() }) }) describe('Employee Basic Information', () => { it('should display employee basic information', async () => { vi.mocked(onboardingService.getEmployeeStatus).mockResolvedValueOnce(mockEmployeeStatus) render() await waitFor(() => { expect(screen.getByText('王明')).toBeInTheDocument() expect(screen.getByText('PWD0001')).toBeInTheDocument() expect(screen.getByText('wang.ming')).toBeInTheDocument() expect(screen.getByText(/2026-02-21/)).toBeInTheDocument() }) }) it('should display employment status badge', async () => { vi.mocked(onboardingService.getEmployeeStatus).mockResolvedValueOnce(mockEmployeeStatus) render() await waitFor(() => { expect(screen.getByText(/Active/i)).toBeInTheDocument() }) }) it('should display storage and email quotas', async () => { vi.mocked(onboardingService.getEmployeeStatus).mockResolvedValueOnce(mockEmployeeStatus) render() await waitFor(() => { const storageQuotas = screen.getAllByText(/20 GB/i) expect(storageQuotas.length).toBeGreaterThan(0) const emailQuotas = screen.getAllByText(/5120 MB/i) expect(emailQuotas.length).toBeGreaterThan(0) }) }) it('should display Keycloak information', async () => { vi.mocked(onboardingService.getEmployeeStatus).mockResolvedValueOnce(mockEmployeeStatus) render() await waitFor(() => { expect(screen.getByText(/550e8400-e29b-41d4-a716-446655440000/i)).toBeInTheDocument() }) }) }) describe('Department List', () => { it('should display all departments', async () => { vi.mocked(onboardingService.getEmployeeStatus).mockResolvedValueOnce(mockEmployeeStatus) render() await waitFor(() => { expect(screen.getByText('玄鐵風能')).toBeInTheDocument() expect(screen.getByText('國際碳權')).toBeInTheDocument() }) }) it('should display department positions', async () => { vi.mocked(onboardingService.getEmployeeStatus).mockResolvedValueOnce(mockEmployeeStatus) render() await waitFor(() => { expect(screen.getByText('資深工程師')).toBeInTheDocument() expect(screen.getByText('顧問')).toBeInTheDocument() }) }) it('should display membership types', async () => { vi.mocked(onboardingService.getEmployeeStatus).mockResolvedValueOnce(mockEmployeeStatus) render() await waitFor(() => { expect(screen.getByText(/Permanent/i)).toBeInTheDocument() expect(screen.getByText(/Concurrent/i)).toBeInTheDocument() }) }) it('should show message when no departments assigned', async () => { const statusWithNoDepts: EmployeeStatusResponse = { ...mockEmployeeStatus, departments: [], } vi.mocked(onboardingService.getEmployeeStatus).mockResolvedValueOnce(statusWithNoDepts) render() await waitFor(() => { expect(screen.getByText(/No departments assigned/i)).toBeInTheDocument() }) }) }) describe('Role List', () => { it('should display all roles', async () => { vi.mocked(onboardingService.getEmployeeStatus).mockResolvedValueOnce(mockEmployeeStatus) render() await waitFor(() => { expect(screen.getByText('HR管理員')).toBeInTheDocument() expect(screen.getByText('系統管理員')).toBeInTheDocument() }) }) it('should display role codes', async () => { vi.mocked(onboardingService.getEmployeeStatus).mockResolvedValueOnce(mockEmployeeStatus) render() await waitFor(() => { expect(screen.getByText('HR_ADMIN')).toBeInTheDocument() expect(screen.getByText('SYS_ADMIN')).toBeInTheDocument() }) }) it('should show message when no roles assigned', async () => { const statusWithNoRoles: EmployeeStatusResponse = { ...mockEmployeeStatus, roles: [], } vi.mocked(onboardingService.getEmployeeStatus).mockResolvedValueOnce(statusWithNoRoles) render() await waitFor(() => { expect(screen.getByText(/No roles assigned/i)).toBeInTheDocument() }) }) }) describe('Service List', () => { it('should display all enabled services', async () => { vi.mocked(onboardingService.getEmployeeStatus).mockResolvedValueOnce(mockEmployeeStatus) render() await waitFor(() => { expect(screen.getByText('單一簽入')).toBeInTheDocument() expect(screen.getByText('雲端硬碟')).toBeInTheDocument() expect(screen.getByText('電子郵件')).toBeInTheDocument() }) }) it('should display service codes', async () => { vi.mocked(onboardingService.getEmployeeStatus).mockResolvedValueOnce(mockEmployeeStatus) render() await waitFor(() => { expect(screen.getByText('SSO')).toBeInTheDocument() expect(screen.getByText('DRIVE')).toBeInTheDocument() expect(screen.getByText('EMAIL')).toBeInTheDocument() }) }) it('should display service quotas when available', async () => { vi.mocked(onboardingService.getEmployeeStatus).mockResolvedValueOnce(mockEmployeeStatus) render() await waitFor(() => { // Drive quota const driveQuota = screen.getAllByText(/20 GB/i) expect(driveQuota.length).toBeGreaterThan(0) // Email quota const emailQuota = screen.getAllByText(/5120 MB/i) expect(emailQuota.length).toBeGreaterThan(0) }) }) it('should show message when no services enabled', async () => { const statusWithNoServices: EmployeeStatusResponse = { ...mockEmployeeStatus, services: [], } vi.mocked(onboardingService.getEmployeeStatus).mockResolvedValueOnce(statusWithNoServices) render() await waitFor(() => { expect(screen.getByText(/No services enabled/i)).toBeInTheDocument() }) }) }) describe('Error Handling', () => { it('should display error message when API fails', async () => { const errorMessage = 'Employee not found' vi.mocked(onboardingService.getEmployeeStatus).mockRejectedValueOnce({ response: { data: { detail: errorMessage }, status: 404, }, }) render() await waitFor(() => { expect(screen.getByText(errorMessage)).toBeInTheDocument() }) }) it('should display generic error message for unknown errors', async () => { vi.mocked(onboardingService.getEmployeeStatus).mockRejectedValueOnce(new Error('Network error')) render() await waitFor(() => { expect(screen.getByText(/Failed to load employee status/i)).toBeInTheDocument() }) }) }) describe('Offboard Action', () => { it('should display offboard button for active employees', async () => { vi.mocked(onboardingService.getEmployeeStatus).mockResolvedValueOnce(mockEmployeeStatus) render() await waitFor(() => { expect(screen.getByRole('button', { name: /Offboard/i })).toBeInTheDocument() }) }) it('should not display offboard button when showActions is false', async () => { vi.mocked(onboardingService.getEmployeeStatus).mockResolvedValueOnce(mockEmployeeStatus) render() await waitFor(() => { expect(screen.queryByRole('button', { name: /Offboard/i })).not.toBeInTheDocument() }) }) it('should call offboardEmployee when offboard button is clicked', async () => { const user = userEvent.setup() vi.mocked(onboardingService.getEmployeeStatus).mockResolvedValueOnce(mockEmployeeStatus) vi.mocked(onboardingService.offboardEmployee).mockResolvedValueOnce({ message: 'Employee offboarded successfully', employee: { tenant_emp_code: 'PWD0001', resign_date: '2026-02-21' }, summary: { departments_removed: 2, roles_revoked: 2, services_disabled: 3 }, }) render() await waitFor(() => { expect(screen.getByRole('button', { name: /Offboard/i })).toBeInTheDocument() }) const offboardButton = screen.getByRole('button', { name: /Offboard/i }) await user.click(offboardButton) await waitFor(() => { expect(onboardingService.offboardEmployee).toHaveBeenCalledWith(1, 1) }) }) it('should show success message after offboarding', async () => { const user = userEvent.setup() const offboardedStatus = { ...mockEmployeeStatus, employee: { ...mockEmployeeStatus.employee, employment_status: 'resigned' as const, resign_date: '2026-02-21' }, } vi.mocked(onboardingService.getEmployeeStatus) .mockResolvedValueOnce(mockEmployeeStatus) // Initial load .mockResolvedValueOnce(offboardedStatus) // After offboard refresh vi.mocked(onboardingService.offboardEmployee).mockResolvedValueOnce({ message: 'Employee offboarded successfully', employee: { tenant_emp_code: 'PWD0001', resign_date: '2026-02-21' }, summary: { departments_removed: 2, roles_revoked: 2, services_disabled: 3 }, }) render() await waitFor(() => { expect(screen.getByRole('button', { name: /Offboard/i })).toBeInTheDocument() }) const offboardButton = screen.getByRole('button', { name: /Offboard/i }) await user.click(offboardButton) await waitFor(() => { expect(screen.getByText(/Employee offboarded successfully/i)).toBeInTheDocument() }) }) it('should handle offboard errors gracefully', async () => { const user = userEvent.setup() const errorMessage = 'Failed to offboard employee' vi.mocked(onboardingService.getEmployeeStatus).mockResolvedValueOnce(mockEmployeeStatus) vi.mocked(onboardingService.offboardEmployee).mockRejectedValueOnce({ response: { data: { detail: errorMessage }, status: 500, }, }) render() await waitFor(() => { expect(screen.getByRole('button', { name: /Offboard/i })).toBeInTheDocument() }) const offboardButton = screen.getByRole('button', { name: /Offboard/i }) await user.click(offboardButton) await waitFor(() => { expect(screen.getByText(errorMessage)).toBeInTheDocument() }) }) }) describe('Refresh Functionality', () => { it('should refresh employee status when refresh is called', async () => { vi.mocked(onboardingService.getEmployeeStatus).mockResolvedValue(mockEmployeeStatus) const { rerender } = render() await waitFor(() => { expect(screen.getByText('王明')).toBeInTheDocument() }) expect(onboardingService.getEmployeeStatus).toHaveBeenCalledTimes(1) // Trigger refresh by rerendering with key change rerender() await waitFor(() => { expect(onboardingService.getEmployeeStatus).toHaveBeenCalledTimes(2) }) }) }) })