Some checks failed
System Monitoring / Health Checks (push) Has been cancelled
System Monitoring / Performance Monitoring (push) Has been cancelled
System Monitoring / Database Monitoring (push) Has been cancelled
System Monitoring / Cache Monitoring (push) Has been cancelled
System Monitoring / Log Monitoring (push) Has been cancelled
System Monitoring / Resource Monitoring (push) Has been cancelled
System Monitoring / Uptime Monitoring (push) Has been cancelled
System Monitoring / Backup Monitoring (push) Has been cancelled
System Monitoring / Security Monitoring (push) Has been cancelled
System Monitoring / Monitoring Dashboard (push) Has been cancelled
System Monitoring / Alerting (push) Has been cancelled
Security Scanning / Dependency Scanning (push) Has been cancelled
Security Scanning / Code Security Scanning (push) Has been cancelled
Security Scanning / Secrets Scanning (push) Has been cancelled
Security Scanning / Container Security Scanning (push) Has been cancelled
Security Scanning / Compliance Checking (push) Has been cancelled
Security Scanning / Security Dashboard (push) Has been cancelled
Security Scanning / Security Remediation (push) Has been cancelled
687 lines
22 KiB
TypeScript
687 lines
22 KiB
TypeScript
/**
|
|
* 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 }) => (
|
|
<div data-testid="module-switcher">
|
|
<div data-testid="current-module">{currentModule}</div>
|
|
<div data-testid="available-modules">
|
|
{modules.map((module) => (
|
|
<button
|
|
key={module.id}
|
|
data-testid={`module-${module.code}`}
|
|
onClick={() => onModuleChange(module.code)}
|
|
disabled={!module.enabled}
|
|
>
|
|
{module.name}
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
);
|
|
|
|
const RetailModule = ({ tenantId, userData }) => (
|
|
<div data-testid="retail-module">
|
|
<h2>Retail Management</h2>
|
|
<div data-testid="tenant-data">{tenantId}</div>
|
|
<div data-testid="user-data">{userData?.name}</div>
|
|
<button data-testid="add-product-btn">Add Product</button>
|
|
<button data-testid="view-sales-btn">View Sales</button>
|
|
<div data-testid="retail-stats">
|
|
<div>Total Products: 150</div>
|
|
<div>Today's Sales: RM 2,450</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
|
|
const HealthcareModule = ({ tenantId, userData }) => (
|
|
<div data-testid="healthcare-module">
|
|
<h2>Healthcare Management</h2>
|
|
<div data-testid="tenant-data">{tenantId}</div>
|
|
<div data-testid="user-data">{userData?.name}</div>
|
|
<button data-testid="add-patient-btn">Add Patient</button>
|
|
<button data-testid="schedule-appointment-btn">Schedule Appointment</button>
|
|
<div data-testid="healthcare-stats">
|
|
<div>Total Patients: 250</div>
|
|
<div>Today's Appointments: 12</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
|
|
const EducationModule = ({ tenantId, userData }) => (
|
|
<div data-testid="education-module">
|
|
<h2>Education Management</h2>
|
|
<div data-testid="tenant-data">{tenantId}</div>
|
|
<div data-testid="user-data">{userData?.name}</div>
|
|
<button data-testid="add-student-btn">Add Student</button>
|
|
<button data-testid="manage-classes-btn">Manage Classes</button>
|
|
<div data-testid="education-stats">
|
|
<div>Total Students: 500</div>
|
|
<div>Active Classes: 25</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
|
|
const LogisticsModule = ({ tenantId, userData }) => (
|
|
<div data-testid="logistics-module">
|
|
<h2>Logistics Management</h2>
|
|
<div data-testid="tenant-data">{tenantId}</div>
|
|
<div data-testid="user-data">{userData?.name}</div>
|
|
<button data-testid="add-shipment-btn">Add Shipment</button>
|
|
<button data-testid="track-vehicles-btn">Track Vehicles</button>
|
|
<div data-testid="logistics-stats">
|
|
<div>Active Shipments: 45</div>
|
|
<div>Vehicle Fleet: 12</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
|
|
const BeautyModule = ({ tenantId, userData }) => (
|
|
<div data-testid="beauty-module">
|
|
<h2>Beauty Management</h2>
|
|
<div data-testid="tenant-data">{tenantId}</div>
|
|
<div data-testid="user-data">{userData?.name}</div>
|
|
<button data-testid="add-client-btn">Add Client</button>
|
|
<button data-testid="manage-services-btn">Manage Services</button>
|
|
<div data-testid="beauty-stats">
|
|
<div>Total Clients: 180</div>
|
|
<div>Active Services: 35</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
|
|
const SharedDataPanel = ({ sharedData, onUpdateSharedData }) => (
|
|
<div data-testid="shared-data-panel">
|
|
<h3>Shared Data</h3>
|
|
<div data-testid="shared-business-name">{sharedData?.businessName}</div>
|
|
<div data-testid="shared-contact-email">{sharedData?.contactEmail}</div>
|
|
<button data-testid="update-shared-data-btn" onClick={onUpdateSharedData}>
|
|
Update Shared Data
|
|
</button>
|
|
</div>
|
|
);
|
|
|
|
const PermissionAlert = ({ hasPermission, requiredPermission }) => (
|
|
<div data-testid="permission-alert">
|
|
{hasPermission ? (
|
|
<div data-testid="permission-granted">Access granted for {requiredPermission}</div>
|
|
) : (
|
|
<div data-testid="permission-denied">Access denied for {requiredPermission}</div>
|
|
)}
|
|
</div>
|
|
);
|
|
|
|
const LoadingSpinner = () => (
|
|
<div data-testid="loading-spinner">Loading module...</div>
|
|
);
|
|
|
|
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 (
|
|
<div data-testid="error-boundary">
|
|
<h2>Something went wrong</h2>
|
|
<div data-testid="error-message">{error?.message}</div>
|
|
<button onClick={() => setHasError(false)}>Try Again</button>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
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 <LoadingSpinner />;
|
|
}
|
|
|
|
if (error) {
|
|
return (
|
|
<div data-testid="error-container">
|
|
<div data-testid="error-message">{error.message}</div>
|
|
<button onClick={() => setError(null)}>Retry</button>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div data-testid="module-app">
|
|
<ModuleSwitcher
|
|
currentModule={currentModule}
|
|
onModuleChange={handleModuleChange}
|
|
modules={modules}
|
|
/>
|
|
|
|
<SharedDataPanel
|
|
sharedData={sharedData}
|
|
onUpdateSharedData={handleUpdateSharedData}
|
|
/>
|
|
|
|
<Routes>
|
|
<Route path="/retail" element={
|
|
<ErrorBoundary onError={setError}>
|
|
<RetailModule tenantId="tenant-001" userData={{ name: 'Test User' }} />
|
|
</ErrorBoundary>
|
|
} />
|
|
<Route path="/healthcare" element={
|
|
<ErrorBoundary onError={setError}>
|
|
<HealthcareModule tenantId="tenant-001" userData={{ name: 'Test User' }} />
|
|
</ErrorBoundary>
|
|
} />
|
|
<Route path="/education" element={
|
|
<ErrorBoundary onError={setError}>
|
|
<EducationModule tenantId="tenant-001" userData={{ name: 'Test User' }} />
|
|
</ErrorBoundary>
|
|
} />
|
|
<Route path="/logistics" element={
|
|
<ErrorBoundary onError={setError}>
|
|
<LogisticsModule tenantId="tenant-001" userData={{ name: 'Test User' }} />
|
|
</ErrorBoundary>
|
|
} />
|
|
<Route path="/beauty" element={
|
|
<ErrorBoundary onError={setError}>
|
|
<BeautyModule tenantId="tenant-001" userData={{ name: 'Test User' }} />
|
|
</ErrorBoundary>
|
|
} />
|
|
</Routes>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
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(
|
|
<BrowserRouter initialEntries={['/retail']}>
|
|
<ModuleApp />
|
|
</BrowserRouter>
|
|
);
|
|
|
|
// 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(
|
|
<BrowserRouter initialEntries={['/retail']}>
|
|
<ModuleApp />
|
|
</BrowserRouter>
|
|
);
|
|
|
|
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(
|
|
<BrowserRouter initialEntries={['/retail']}>
|
|
<ModuleApp />
|
|
</BrowserRouter>
|
|
);
|
|
|
|
// 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(
|
|
<BrowserRouter initialEntries={['/${module}']}>
|
|
<ModuleApp />
|
|
</BrowserRouter>
|
|
);
|
|
|
|
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(
|
|
<BrowserRouter initialEntries={['/retail']}>
|
|
<ModuleApp />
|
|
</BrowserRouter>
|
|
);
|
|
|
|
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(
|
|
<BrowserRouter initialEntries={['/retail']}>
|
|
<ModuleApp />
|
|
</BrowserRouter>
|
|
);
|
|
|
|
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(
|
|
<BrowserRouter initialEntries={['/retail']}>
|
|
<ModuleApp />
|
|
</BrowserRouter>
|
|
);
|
|
|
|
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(
|
|
<BrowserRouter initialEntries={['/retail']}>
|
|
<ModuleApp />
|
|
</BrowserRouter>
|
|
);
|
|
|
|
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(
|
|
<BrowserRouter initialEntries={['/retail']}>
|
|
<ModuleApp />
|
|
</BrowserRouter>
|
|
);
|
|
|
|
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(
|
|
<BrowserRouter initialEntries={['/retail']}>
|
|
<ModuleApp />
|
|
</BrowserRouter>
|
|
);
|
|
|
|
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(
|
|
<BrowserRouter initialEntries={['/retail']}>
|
|
<ModuleApp />
|
|
</BrowserRouter>
|
|
);
|
|
|
|
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(
|
|
<BrowserRouter initialEntries={['/retail']}>
|
|
<ModuleApp />
|
|
</BrowserRouter>
|
|
);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('retail-module')).toBeInTheDocument();
|
|
});
|
|
|
|
// Module should be loaded on demand
|
|
expect(mockModuleLoad).toHaveBeenCalled();
|
|
});
|
|
|
|
test('caches loaded modules', async () => {
|
|
render(
|
|
<BrowserRouter initialEntries={['/retail']}>
|
|
<ModuleApp />
|
|
</BrowserRouter>
|
|
);
|
|
|
|
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);
|
|
});
|
|
});
|
|
}); |