/** * Frontend Integration Tests - Module Integration * * Tests for module-specific integration: * - Module switching and data isolation * - Cross-module data sharing * - Module-specific permissions * - Module loading performance * - Error handling across modules * * Author: Claude */ import React from 'react'; import { render, screen, fireEvent, waitFor, act } from '@testing-library/react'; import '@testing-library/jest-dom'; import { BrowserRouter, Routes, Route, useNavigate } from 'react-router-dom'; import userEvent from '@testing-library/user-event'; // Mock services const mockModuleService = { getModules: jest.fn(), activateModule: jest.fn(), deactivateModule: jest.fn(), getModuleData: jest.fn(), validateModuleAccess: jest.fn(), }; const mockPermissionService = { checkPermission: jest.fn(), getUserPermissions: jest.fn(), }; const mockDataService = { getTenantData: jest.fn(), getSharedData: jest.fn(), updateSharedData: jest.fn(), }; // Mock components for different modules const ModuleSwitcher = ({ currentModule, onModuleChange, modules }) => (
{currentModule}
{modules.map((module) => ( ))}
); const RetailModule = ({ tenantId, userData }) => (

Retail Management

{tenantId}
{userData?.name}
Total Products: 150
Today's Sales: RM 2,450
); const HealthcareModule = ({ tenantId, userData }) => (

Healthcare Management

{tenantId}
{userData?.name}
Total Patients: 250
Today's Appointments: 12
); const EducationModule = ({ tenantId, userData }) => (

Education Management

{tenantId}
{userData?.name}
Total Students: 500
Active Classes: 25
); const LogisticsModule = ({ tenantId, userData }) => (

Logistics Management

{tenantId}
{userData?.name}
Active Shipments: 45
Vehicle Fleet: 12
); const BeautyModule = ({ tenantId, userData }) => (

Beauty Management

{tenantId}
{userData?.name}
Total Clients: 180
Active Services: 35
); const SharedDataPanel = ({ sharedData, onUpdateSharedData }) => (

Shared Data

{sharedData?.businessName}
{sharedData?.contactEmail}
); const PermissionAlert = ({ hasPermission, requiredPermission }) => (
{hasPermission ? (
Access granted for {requiredPermission}
) : (
Access denied for {requiredPermission}
)}
); const LoadingSpinner = () => (
Loading module...
); const ErrorBoundary = ({ children, onError }) => { const [hasError, setHasError] = React.useState(false); const [error, setError] = React.useState(null); React.useEffect(() => { const handleError = (event) => { setHasError(true); setError(event.error); onError?.(event.error); }; window.addEventListener('error', handleError); return () => window.removeEventListener('error', handleError); }, [onError]); if (hasError) { return (

Something went wrong

{error?.message}
); } return <>{children}; }; // Test app with module routing const ModuleApp = () => { const [currentModule, setCurrentModule] = React.useState('retail'); const [modules, setModules] = React.useState([]); const [sharedData, setSharedData] = React.useState(null); const [loading, setLoading] = React.useState(true); const [error, setError] = React.useState(null); const navigate = useNavigate(); React.useEffect(() => { loadModules(); loadSharedData(); }, []); const loadModules = async () => { try { setLoading(true); const response = await mockModuleService.getModules(); setModules(response.data); } catch (err) { setError(err); } finally { setLoading(false); } }; const loadSharedData = async () => { try { const response = await mockDataService.getSharedData(); setSharedData(response.data); } catch (err) { console.error('Failed to load shared data:', err); } }; const handleModuleChange = async (moduleCode) => { try { await mockModuleService.validateModuleAccess(moduleCode); setCurrentModule(moduleCode); navigate(`/${moduleCode}`); } catch (err) { setError(err); } }; const handleUpdateSharedData = async () => { try { const updatedData = { ...sharedData, businessName: 'Updated Business Name' }; await mockDataService.updateSharedData(updatedData); setSharedData(updatedData); } catch (err) { setError(err); } }; if (loading) { return ; } if (error) { return (
{error.message}
); } return (
} /> } /> } /> } /> } />
); }; describe('Module Integration Tests', () => { const user = userEvent.setup(); beforeEach(() => { jest.clearAllMocks(); // Mock default responses mockModuleService.getModules.mockResolvedValue({ data: [ { id: 1, code: 'retail', name: 'Retail Management', enabled: true }, { id: 2, code: 'healthcare', name: 'Healthcare Management', enabled: true }, { id: 3, code: 'education', name: 'Education Management', enabled: true }, { id: 4, code: 'logistics', name: 'Logistics Management', enabled: true }, { id: 5, code: 'beauty', name: 'Beauty Management', enabled: true }, ] }); mockDataService.getSharedData.mockResolvedValue({ data: { businessName: 'Test Business Sdn Bhd', contactEmail: 'contact@test.com', address: '123 Test Street', phone: '+60123456789' } }); mockModuleService.validateModuleAccess.mockResolvedValue(true); mockPermissionService.checkPermission.mockResolvedValue(true); }); describe('Module Switching', () => { test('successfully switches between modules', async () => { render( ); // Wait for initial load await waitFor(() => { expect(screen.getByTestId('module-app')).toBeInTheDocument(); }); // Should start with retail module expect(screen.getByTestId('current-module')).toHaveTextContent('retail'); expect(screen.getByTestId('retail-module')).toBeInTheDocument(); // Switch to healthcare module const healthcareButton = screen.getByTestId('module-healthcare'); await user.click(healthcareButton); await waitFor(() => { expect(screen.getByTestId('current-module')).toHaveTextContent('healthcare'); expect(screen.getByTestId('healthcare-module')).toBeInTheDocument(); expect(screen.queryByTestId('retail-module')).not.toBeInTheDocument(); }); // Verify module access validation was called expect(mockModuleService.validateModuleAccess).toHaveBeenCalledWith('healthcare'); }); test('maintains shared data across modules', async () => { render( ); await waitFor(() => { expect(screen.getByTestId('shared-data-panel')).toBeInTheDocument(); }); // Check shared data in retail module expect(screen.getByTestId('shared-business-name')).toHaveTextContent('Test Business Sdn Bhd'); expect(screen.getByTestId('shared-contact-email')).toHaveTextContent('contact@test.com'); // Switch to healthcare module const healthcareButton = screen.getByTestId('module-healthcare'); await user.click(healthcareButton); await waitFor(() => { expect(screen.getByTestId('healthcare-module')).toBeInTheDocument(); }); // Shared data should still be available expect(screen.getByTestId('shared-business-name')).toHaveTextContent('Test Business Sdn Bhd'); expect(screen.getByTestId('shared-contact-email')).toHaveTextContent('contact@test.com'); }); test('handles module loading states', async () => { // Mock slow loading mockModuleService.getModules.mockImplementationOnce(() => new Promise(resolve => setTimeout(resolve, 1000)) ); render( ); // Should show loading spinner initially expect(screen.getByTestId('loading-spinner')).toBeInTheDocument(); // Wait for loading to complete await waitFor(() => { expect(screen.queryByTestId('loading-spinner')).not.toBeInTheDocument(); expect(screen.getByTestId('module-app')).toBeInTheDocument(); }); }); }); describe('Module-Specific Features', () => { test.each([ { module: 'retail', button: 'add-product-btn', stats: 'retail-stats' }, { module: 'healthcare', button: 'add-patient-btn', stats: 'healthcare-stats' }, { module: 'education', button: 'add-student-btn', stats: 'education-stats' }, { module: 'logistics', button: 'add-shipment-btn', stats: 'logistics-stats' }, { module: 'beauty', button: 'add-client-btn', stats: 'beauty-stats' }, ])('loads $module module with correct features', async ({ module, button, stats }) => { render( ); await waitFor(() => { expect(screen.getByTestId(`${module}-module`)).toBeInTheDocument(); }); // Check module-specific buttons expect(screen.getByTestId(button)).toBeInTheDocument(); // Check module-specific stats expect(screen.getByTestId(stats)).toBeInTheDocument(); // Check tenant data isolation expect(screen.getByTestId('tenant-data')).toHaveTextContent('tenant-001'); // Check user data sharing expect(screen.getByTestId('user-data')).toHaveTextContent('Test User'); }); }); describe('Permission Management', () => { test('validates module access permissions', async () => { // Mock permission denied for logistics module mockModuleService.validateModuleAccess.mockImplementationOnce((moduleCode) => { if (moduleCode === 'logistics') { return Promise.reject(new Error('Access denied')); } return Promise.resolve(true); }); render( ); await waitFor(() => { expect(screen.getByTestId('retail-module')).toBeInTheDocument(); }); // Try to access logistics module const logisticsButton = screen.getByTestId('module-logistics'); await user.click(logisticsButton); await waitFor(() => { expect(screen.getByTestId('error-container')).toBeInTheDocument(); expect(screen.getByTestId('error-message')).toHaveTextContent('Access denied'); }); // Should not switch to logistics module expect(screen.queryByTestId('logistics-module')).not.toBeInTheDocument(); expect(screen.getByTestId('retail-module')).toBeInTheDocument(); }); test('checks feature-level permissions', async () => { mockPermissionService.checkPermission.mockImplementation((permission) => { const deniedPermissions = ['delete_products', 'manage_patients']; return Promise.resolve(!deniedPermissions.includes(permission)); }); render( ); await waitFor(() => { expect(screen.getByTestId('retail-module')).toBeInTheDocument(); }); // Test retail permissions expect(await mockPermissionService.checkPermission('view_products')).resolves.toBe(true); expect(await mockPermissionService.checkPermission('delete_products')).resolves.toBe(false); }); }); describe('Data Isolation and Sharing', () => { test('maintains tenant data isolation', async () => { // Mock different tenant data for different modules mockDataService.getTenantData.mockImplementation((module) => { const tenantData = { retail: { id: 'tenant-retail', name: 'Retail Business' }, healthcare: { id: 'tenant-healthcare', name: 'Healthcare Business' }, }; return Promise.resolve({ data: tenantData[module] }); }); render( ); await waitFor(() => { expect(screen.getByTestId('retail-module')).toBeInTheDocument(); }); // Check retail tenant data expect(screen.getByTestId('tenant-data')).toHaveTextContent('tenant-001'); // Switch to healthcare module const healthcareButton = screen.getByTestId('module-healthcare'); await user.click(healthcareButton); await waitFor(() => { expect(screen.getByTestId('healthcare-module')).toBeInTheDocument(); }); // Should still use the same tenant (data isolation at tenant level) expect(screen.getByTestId('tenant-data')).toHaveTextContent('tenant-001'); }); test('updates shared data across modules', async () => { render( ); await waitFor(() => { expect(screen.getByTestId('shared-data-panel')).toBeInTheDocument(); }); // Update shared data const updateButton = screen.getByTestId('update-shared-data-btn'); await user.click(updateButton); await waitFor(() => { expect(mockDataService.updateSharedData).toHaveBeenCalled(); expect(screen.getByTestId('shared-business-name')).toHaveTextContent('Updated Business Name'); }); // Switch to another module const healthcareButton = screen.getByTestId('module-healthcare'); await user.click(healthcareButton); await waitFor(() => { expect(screen.getByTestId('healthcare-module')).toBeInTheDocument(); }); // Updated shared data should be available expect(screen.getByTestId('shared-business-name')).toHaveTextContent('Updated Business Name'); }); }); describe('Error Handling', () => { test('handles module loading errors gracefully', async () => { mockModuleService.getModules.mockRejectedValueOnce(new Error('Failed to load modules')); render( ); await waitFor(() => { expect(screen.getByTestId('error-container')).toBeInTheDocument(); expect(screen.getByTestId('error-message')).toHaveTextContent('Failed to load modules'); }); }); test('handles individual module errors', async () => { render( ); await waitFor(() => { expect(screen.getByTestId('retail-module')).toBeInTheDocument(); }); // Simulate module error const error = new Error('Module crashed'); fireEvent.error(window, error); await waitFor(() => { expect(screen.getByTestId('error-boundary')).toBeInTheDocument(); expect(screen.getByTestId('error-message')).toHaveTextContent('Module crashed'); }); }); test('recovers from errors with retry', async () => { mockModuleService.getModules.mockRejectedValueOnce(new Error('Network error')); render( ); await waitFor(() => { expect(screen.getByTestId('error-container')).toBeInTheDocument(); }); // Mock successful retry mockModuleService.getModules.mockResolvedValueOnce({ data: [ { id: 1, code: 'retail', name: 'Retail Management', enabled: true }, ] }); // Click retry button const retryButton = screen.getByText('Retry'); await user.click(retryButton); await waitFor(() => { expect(screen.getByTestId('module-app')).toBeInTheDocument(); expect(screen.getByTestId('retail-module')).toBeInTheDocument(); }); }); }); describe('Performance Optimization', () => { test('lazy loads modules on demand', async () => { const mockModuleLoad = jest.fn(); // Mock dynamic import jest.mock('react-lazy', () => ({ lazy: (importFn) => { mockModuleLoad(); return importFn(); } })); render( ); await waitFor(() => { expect(screen.getByTestId('retail-module')).toBeInTheDocument(); }); // Module should be loaded on demand expect(mockModuleLoad).toHaveBeenCalled(); }); test('caches loaded modules', async () => { render( ); await waitFor(() => { expect(screen.getByTestId('retail-module')).toBeInTheDocument(); }); // Switch away and back to retail module const healthcareButton = screen.getByTestId('module-healthcare'); await user.click(healthcareButton); await waitFor(() => { expect(screen.getByTestId('healthcare-module')).toBeInTheDocument(); }); const retailButton = screen.getByTestId('module-retail'); await user.click(retailButton); await waitFor(() => { expect(screen.getByTestId('retail-module')).toBeInTheDocument(); }); // Should not reload modules (check call count) expect(mockModuleService.getModules).toHaveBeenCalledTimes(1); }); }); });