diff --git a/frontend/src/routes/AppRoutes.jsx b/frontend/src/routes/AppRoutes.jsx
index 841bac4..94a98c2 100644
--- a/frontend/src/routes/AppRoutes.jsx
+++ b/frontend/src/routes/AppRoutes.jsx
@@ -1,20 +1,74 @@
-import React from 'react';
+import React, { Suspense, lazy } from 'react';
import { Routes, Route, Navigate } from 'react-router-dom';
-import Login from '../features/auth/pages/Login';
-import Register from '../features/auth/pages/Register';
-import Dashboard from '../features/dashboard/pages/Dashboard';
-import LandingPage from '../features/landing/pages/LandingPage';
-import CloudflareTestPage from './CloudflareTestPage';
-import GroupAvatarTestPage from '../../tests/GroupAvatarTestPage';
-import VerifyEmail from '../features/auth/pages/VerifyEmail';
+import { Loader2 } from 'lucide-react';
import { useAuthContext } from '../context/useAuthContext';
+// Lazy-loaded production routes
+const LandingPage = lazy(() => import('../features/landing/pages/LandingPage'));
+const Login = lazy(() => import('../features/auth/pages/Login'));
+const Register = lazy(() => import('../features/auth/pages/Register'));
+const VerifyEmail = lazy(() => import('../features/auth/pages/VerifyEmail'));
+const Dashboard = lazy(() => import('../features/dashboard/pages/Dashboard'));
+
+// Internal test routes: only loaded in local development environments
+let GroupAvatarTestPage = null;
+let CloudflareTestPage = null;
+
+if (import.meta.env.DEV) {
+ GroupAvatarTestPage = lazy(() => import('../../tests/GroupAvatarTestPage'));
+ CloudflareTestPage = lazy(() => import('./CloudflareTestPage'));
+}
+
+// Accessible, responsive route loading fallback
+export const RouteLoadingFallback = () => (
+
+);
+
// Simple check to safeguard secure UI containers
-const ProtectedRoute = ({ children }) => {
+export const ProtectedRoute = ({ children }) => {
const { user, loading } = useAuthContext();
if (loading) {
- return null;
+ return ;
}
const token = user?.token || (typeof localStorage !== 'undefined' ? localStorage.getItem('token') : null);
@@ -27,25 +81,32 @@ const ProtectedRoute = ({ children }) => {
export default function AppRoutes() {
return (
-
- } />
- } />
- } />
-
-
- } />
+ }>
+
+ } />
+ } />
+ } />
+ } />
- } />
+ {/* Internal test routes excluded from production bundles */}
+ {import.meta.env.DEV && GroupAvatarTestPage && (
+ } />
+ )}
+ {import.meta.env.DEV && CloudflareTestPage && (
+ } />
+ )}
+
+
+
+ }
+ />
-
-
-
- }
- />
-
+ } />
+
+
);
}
\ No newline at end of file
diff --git a/frontend/tests/routeLoading.test.js b/frontend/tests/routeLoading.test.js
new file mode 100644
index 0000000..f09f54a
--- /dev/null
+++ b/frontend/tests/routeLoading.test.js
@@ -0,0 +1,110 @@
+import test from 'node:test';
+import assert from 'node:assert/strict';
+import fs from 'node:fs';
+import path from 'node:path';
+import { fileURLToPath } from 'node:url';
+
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = path.dirname(__filename);
+const frontendDir = path.resolve(__dirname, '..');
+
+test('FE-007: Route loading optimization and test route isolation', async (t) => {
+ const appRoutesPath = path.join(frontendDir, 'src/routes/AppRoutes.jsx');
+
+ await t.test('verifies AppRoutes.jsx uses React.lazy for production pages', () => {
+ assert.ok(fs.existsSync(appRoutesPath), 'AppRoutes.jsx must exist');
+ const content = fs.readFileSync(appRoutesPath, 'utf8');
+
+ // Verify lazy loading of main routes
+ assert.ok(
+ content.includes("lazy(() => import('../features/landing/pages/LandingPage'))"),
+ 'LandingPage must be lazy loaded'
+ );
+ assert.ok(
+ content.includes("lazy(() => import('../features/auth/pages/Login'))"),
+ 'Login must be lazy loaded'
+ );
+ assert.ok(
+ content.includes("lazy(() => import('../features/auth/pages/Register'))"),
+ 'Register must be lazy loaded'
+ );
+ assert.ok(
+ content.includes("lazy(() => import('../features/auth/pages/VerifyEmail'))"),
+ 'VerifyEmail must be lazy loaded'
+ );
+ assert.ok(
+ content.includes("lazy(() => import('../features/dashboard/pages/Dashboard'))"),
+ 'Dashboard must be lazy loaded'
+ );
+ });
+
+ await t.test('verifies internal test routes are strictly gated by import.meta.env.DEV', () => {
+ const content = fs.readFileSync(appRoutesPath, 'utf8');
+
+ assert.ok(
+ content.includes('if (import.meta.env.DEV)'),
+ 'Test page dynamic imports must be enclosed in import.meta.env.DEV check'
+ );
+ assert.ok(
+ content.includes('import.meta.env.DEV && GroupAvatarTestPage'),
+ '/test route must be gated by import.meta.env.DEV'
+ );
+ assert.ok(
+ content.includes('import.meta.env.DEV && CloudflareTestPage'),
+ '/test/cloudflare route must be gated by import.meta.env.DEV'
+ );
+ });
+
+ await t.test('verifies Suspense and RouteLoadingFallback are configured', () => {
+ const content = fs.readFileSync(appRoutesPath, 'utf8');
+
+ assert.ok(
+ content.includes('}>'),
+ 'Routes must be wrapped with Suspense fallback'
+ );
+ assert.ok(
+ content.includes('role="status"'),
+ 'RouteLoadingFallback must include accessible role="status"'
+ );
+ });
+
+ await t.test('verifies production build dist directory excludes test page chunks and splits code', () => {
+ const distAssetsDir = path.join(frontendDir, 'dist/assets');
+ if (!fs.existsSync(distAssetsDir)) {
+ // If dist has not been built yet, skip asset inspection
+ return;
+ }
+
+ const distFiles = fs.readdirSync(distAssetsDir);
+
+ // Ensure no GroupAvatarTestPage or CloudflareTestPage chunks exist in dist
+ const hasGroupAvatarChunk = distFiles.some((f) => f.includes('GroupAvatarTestPage'));
+ const hasCloudflareChunk = distFiles.some((f) => f.includes('CloudflareTestPage'));
+
+ assert.strictEqual(
+ hasGroupAvatarChunk,
+ false,
+ 'GroupAvatarTestPage must not be emitted in production dist assets'
+ );
+ assert.strictEqual(
+ hasCloudflareChunk,
+ false,
+ 'CloudflareTestPage must not be emitted in production dist assets'
+ );
+
+ // Verify index chunk is significantly smaller than the original 708kB single-bundle
+ const indexChunkFile = distFiles.find((f) => f.startsWith('index-') && f.endsWith('.js'));
+ assert.ok(indexChunkFile, 'Production index-*.js chunk must exist');
+
+ const indexStats = fs.statSync(path.join(distAssetsDir, indexChunkFile));
+ const indexSizeKb = indexStats.size / 1024;
+ assert.ok(
+ indexSizeKb < 400,
+ `Expected entry chunk to be < 400 kB due to code splitting, actual: ${indexSizeKb.toFixed(2)} kB`
+ );
+
+ // Verify separate Dashboard chunk exists
+ const dashboardChunkFile = distFiles.find((f) => f.startsWith('Dashboard-') && f.endsWith('.js'));
+ assert.ok(dashboardChunkFile, 'Dashboard must be split into its own chunk');
+ });
+});