From 3c8582c622a4957ce2103f7db754e62b7003e5ef Mon Sep 17 00:00:00 2001 From: KavinduLakshan393 Date: Fri, 21 Aug 2026 11:06:52 +0530 Subject: [PATCH 01/19] test(cv): add live frontend acceptance suite --- docs/testing/BMD_007_LIVE_ACCEPTANCE.md | 36 +++++ e2e-live/cv-generation.live.spec.ts | 192 ++++++++++++++++++++++++ package.json | 1 + playwright.cv-live.config.ts | 31 ++++ 4 files changed, 260 insertions(+) create mode 100644 docs/testing/BMD_007_LIVE_ACCEPTANCE.md create mode 100644 e2e-live/cv-generation.live.spec.ts create mode 100644 playwright.cv-live.config.ts diff --git a/docs/testing/BMD_007_LIVE_ACCEPTANCE.md b/docs/testing/BMD_007_LIVE_ACCEPTANCE.md new file mode 100644 index 0000000..3957156 --- /dev/null +++ b/docs/testing/BMD_007_LIVE_ACCEPTANCE.md @@ -0,0 +1,36 @@ +# BMD-007 Live Frontend Acceptance + +This suite exercises the real Spring Boot, PostgreSQL, XeLaTeX, and frontend integration. It does not intercept or mock API requests. + +## Prerequisites + +- backend running at `http://127.0.0.1:8080` with XeLaTeX available; +- a dedicated, registered Student acceptance account; +- an Admin acceptance account; +- the Student account has a complete profile suitable for CV generation. + +Set credentials in the current PowerShell process. Do not commit them: + +```powershell +$env:CV_LIVE_STUDENT_EMAIL="student@example.test" +$env:CV_LIVE_STUDENT_PASSWORD="replace-locally" +$env:CV_LIVE_ADMIN_EMAIL="admin@example.test" +$env:CV_LIVE_ADMIN_PASSWORD="replace-locally" +``` + +If the backend uses another origin, set `CV_LIVE_BACKEND_URL`. The Vite proxy currently expects the application backend on port `8080`. + +Run: + +```powershell +npm ci +npm run e2e:cv-live +``` + +The three serial tests verify: + +1. Student preview, save, and real PDF download; +2. Profile mutation, `OUTDATED`, replacement, restoration, and `CURRENT`; +3. Admin latest-saved-CV visibility and real PDF download. + +The suite restores the Student's original profile summary and saves a final replacement so the active CV finishes in the `CURRENT` state. diff --git a/e2e-live/cv-generation.live.spec.ts b/e2e-live/cv-generation.live.spec.ts new file mode 100644 index 0000000..311c4f2 --- /dev/null +++ b/e2e-live/cv-generation.live.spec.ts @@ -0,0 +1,192 @@ +import { + expect, + request as playwrightRequest, + test, + type APIRequestContext, + type Page, +} from '@playwright/test' + +const backendUrl = process.env.CV_LIVE_BACKEND_URL ?? 'http://127.0.0.1:8080' +const studentEmail = process.env.CV_LIVE_STUDENT_EMAIL +const studentPassword = process.env.CV_LIVE_STUDENT_PASSWORD +const adminEmail = process.env.CV_LIVE_ADMIN_EMAIL +const adminPassword = process.env.CV_LIVE_ADMIN_PASSWORD + +type LoginResponse = { accessToken: string } +type StudentProfile = { studentId: string; summary: string | null; version: number } +type SavedCv = { revision: number } + +let api: APIRequestContext +let studentToken: string +let adminToken: string +let studentId: string +let originalSummary: string | null +let profileVersion: number + +function requireCredentials() { + const missing = [ + ['CV_LIVE_STUDENT_EMAIL', studentEmail], + ['CV_LIVE_STUDENT_PASSWORD', studentPassword], + ['CV_LIVE_ADMIN_EMAIL', adminEmail], + ['CV_LIVE_ADMIN_PASSWORD', adminPassword], + ] + .filter(([, value]) => !value) + .map(([name]) => name) + + if (missing.length) { + throw new Error(`Missing live CV acceptance variables: ${missing.join(', ')}`) + } +} + +async function loginApi(role: 'student' | 'admin', email: string, password: string) { + const response = await api.post(`/api/v1/auth/${role}/login`, { data: { email, password } }) + expect( + response.ok(), + `${role} API login failed: ${response.status()} ${await response.text()}`, + ).toBeTruthy() + return ((await response.json()) as LoginResponse).accessToken +} + +async function loginUi(page: Page, role: 'student' | 'admin', email: string, password: string) { + await page.goto(`/${role}/login`, { waitUntil: 'domcontentloaded' }) + await page.getByLabel(role === 'student' ? 'University Email' : 'Admin Email Address').fill(email) + await page.getByLabel(role === 'student' ? 'Password' : 'Security Password').fill(password) + await page.getByRole('button', { name: 'Log In' }).click() + await expect(page).toHaveURL(new RegExp(`/${role}/dashboard$`)) +} + +async function openCvBuilder(page: Page) { + await page.goto('/student/cv-builder', { waitUntil: 'domcontentloaded' }) + await expect(page.getByRole('heading', { level: 1, name: 'LaTeX CV Builder' })).toBeVisible() + await expect(page.getByRole('button', { name: /Generate Preview|Update Preview/ })).toBeEnabled() +} + +async function generateAndSave(page: Page) { + const previewResponse = page.waitForResponse( + (response) => + response.url().endsWith('/api/v1/me/cv/preview') && response.request().method() === 'POST', + ) + await page.getByRole('button', { name: /Generate Preview|Update Preview/ }).click() + expect((await previewResponse).status()).toBe(200) + await expect(page.getByTitle('Generated CV visual preview')).toBeVisible() + + const saveResponse = page.waitForResponse( + (response) => response.url().endsWith('/api/v1/me/cv') && response.request().method() === 'PUT', + ) + await page.getByRole('button', { name: 'Save Current CV Version' }).click() + const saved = await saveResponse + expect([200, 201]).toContain(saved.status()) + await expect(page.getByText(/CV saved|CV updated/)).toBeVisible() + return (await saved.json()) as SavedCv +} + +async function patchSummary(summary: string | null, version: number) { + const response = await api.patch('/api/v1/me/profile', { + data: { summary }, + headers: { + Authorization: `Bearer ${studentToken}`, + 'If-Match': `"${version}"`, + }, + }) + expect( + response.ok(), + `Profile update failed: ${response.status()} ${await response.text()}`, + ).toBeTruthy() + return (await response.json()) as StudentProfile +} + +test.describe.serial('BMD-007 live CV acceptance', () => { + test.beforeAll(async () => { + requireCredentials() + api = await playwrightRequest.newContext({ baseURL: backendUrl }) + studentToken = await loginApi('student', studentEmail!, studentPassword!) + adminToken = await loginApi('admin', adminEmail!, adminPassword!) + + const profileResponse = await api.get('/api/v1/me/profile', { + headers: { Authorization: `Bearer ${studentToken}` }, + }) + expect(profileResponse.ok()).toBeTruthy() + const profile = (await profileResponse.json()) as StudentProfile + studentId = profile.studentId + originalSummary = profile.summary + profileVersion = profile.version + }) + + test.afterAll(async () => { + await api?.dispose() + }) + + test('Student previews, saves, and downloads a real PDF', async ({ page }) => { + await loginUi(page, 'student', studentEmail!, studentPassword!) + await openCvBuilder(page) + await generateAndSave(page) + + const downloadResponse = page.waitForResponse((response) => + response.url().endsWith('/api/v1/me/cv/download'), + ) + await page.getByRole('button', { name: 'Download Current CV PDF' }).click() + const response = await downloadResponse + expect(response.status()).toBe(200) + expect(response.headers()['content-type']).toContain('application/pdf') + expect((await response.body()).subarray(0, 5).toString()).toBe('%PDF-') + }) + + test('Profile mutation marks the CV OUTDATED and replacement restores CURRENT', async ({ + page, + }) => { + const probe = `BMD-007 live freshness probe ${Date.now()}` + const mutated = await patchSummary(probe, profileVersion) + profileVersion = mutated.version + + const outdatedResponse = await api.get('/api/v1/me/cv/source-freshness', { + headers: { Authorization: `Bearer ${studentToken}` }, + }) + expect(outdatedResponse.ok()).toBeTruthy() + expect(await outdatedResponse.json()).toMatchObject({ status: 'OUTDATED' }) + + await loginUi(page, 'student', studentEmail!, studentPassword!) + await openCvBuilder(page) + await expect(page.getByRole('heading', { name: 'Your saved CV needs an update' })).toBeVisible() + const replacement = await generateAndSave(page) + expect(replacement.revision).toBeGreaterThan(1) + + const restored = await patchSummary(originalSummary, profileVersion) + profileVersion = restored.version + await page.reload({ waitUntil: 'domcontentloaded' }) + await expect( + page.getByRole('button', { name: /Generate Preview|Update Preview/ }), + ).toBeEnabled() + await generateAndSave(page) + + const currentResponse = await api.get('/api/v1/me/cv/source-freshness', { + headers: { Authorization: `Bearer ${studentToken}` }, + }) + expect(currentResponse.ok()).toBeTruthy() + expect(await currentResponse.json()).toMatchObject({ status: 'CURRENT' }) + }) + + test('Admin sees and downloads the Student latest saved CV', async ({ page }) => { + await loginUi(page, 'admin', adminEmail!, adminPassword!) + await page.goto(`/admin/students/${studentId}`, { waitUntil: 'domcontentloaded' }) + await expect(page.getByText('Latest saved CV', { exact: true })).toBeVisible() + await expect(page.getByText('Current', { exact: true })).toBeVisible() + + const metadata = await api.get(`/api/v1/admin/students/${studentId}/latest-cv`, { + headers: { Authorization: `Bearer ${adminToken}` }, + }) + expect(metadata.ok()).toBeTruthy() + expect(await metadata.json()).toMatchObject({ + availability: 'AVAILABLE', + freshnessStatus: 'CURRENT', + }) + + const downloadResponse = page.waitForResponse((response) => + response.url().endsWith(`/api/v1/admin/students/${studentId}/latest-cv/download`), + ) + await page.getByRole('button', { name: 'Download latest CV' }).click() + const response = await downloadResponse + expect(response.status()).toBe(200) + expect(response.headers()['content-type']).toContain('application/pdf') + expect((await response.body()).subarray(0, 5).toString()).toBe('%PDF-') + }) +}) diff --git a/package.json b/package.json index de4c9cd..955ea79 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "test": "vitest run --config vitest.config.mjs --reporter=dot --pool=forks --maxWorkers=1 --fileParallelism=false", "test:coverage": "vitest run --coverage --config vitest.config.mjs --pool=threads --maxWorkers=4", "e2e": "playwright test", + "e2e:cv-live": "playwright test --config=playwright.cv-live.config.ts", "e2e:motion": "playwright test e2e/motion-accessibility.spec.ts", "e2e:visual": "playwright test e2e/skeleton-visuals.spec.ts --workers=1", "e2e:cross-browser": "playwright test --project=chromium --project=firefox --project=webkit --project=edge", diff --git a/playwright.cv-live.config.ts b/playwright.cv-live.config.ts new file mode 100644 index 0000000..084c719 --- /dev/null +++ b/playwright.cv-live.config.ts @@ -0,0 +1,31 @@ +import { defineConfig, devices } from '@playwright/test' + +const frontendHost = '127.0.0.1' +const frontendPort = 5175 + +export default defineConfig({ + testDir: './e2e-live', + testMatch: /cv-generation\.live\.spec\.ts/, + fullyParallel: false, + workers: 1, + timeout: 180_000, + reporter: [['list'], ['html', { open: 'never', outputFolder: 'playwright-report/cv-live' }]], + expect: { timeout: 30_000 }, + use: { + baseURL: `http://${frontendHost}:${frontendPort}`, + trace: 'retain-on-failure', + video: 'retain-on-failure', + }, + webServer: { + command: `npm run dev -- --mode e2e --host ${frontendHost} --port ${frontendPort} --strictPort`, + url: `http://${frontendHost}:${frontendPort}`, + reuseExistingServer: false, + timeout: 120_000, + }, + projects: [ + { + name: 'chromium-cv-live', + use: { ...devices['Desktop Chrome'] }, + }, + ], +}) From a447b3426579bc6e88b84c9769b0d13dec7085f1 Mon Sep 17 00:00:00 2001 From: KavinduLakshan393 Date: Sat, 22 Aug 2026 06:41:06 +0530 Subject: [PATCH 02/19] chore: synchronize package-lock.json peer dependencies metadata --- package-lock.json | 29 +++++++++-------------------- 1 file changed, 9 insertions(+), 20 deletions(-) diff --git a/package-lock.json b/package-lock.json index 41196e9..374b288 100644 --- a/package-lock.json +++ b/package-lock.json @@ -113,7 +113,6 @@ "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", @@ -473,7 +472,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" }, @@ -497,7 +495,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" } @@ -1949,7 +1946,8 @@ "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@types/babel__core": { "version": "7.20.5", @@ -2034,7 +2032,6 @@ "integrity": "sha512-QWlFW2wf3nTjC13/DqRnBpR4ZO36VJH/JVBkA/vcnmbTBNQIlnObqyqZE1tUR7+Ni23Lda8R1BxMfbXRpCUx5g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -2045,7 +2042,6 @@ "integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -2056,7 +2052,6 @@ "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", "dev": true, "license": "MIT", - "peer": true, "peerDependencies": { "@types/react": "^19.2.0" } @@ -2130,7 +2125,6 @@ "integrity": "sha512-sPhE4iHuJDSvoAiec+Ro8JyXw8f0ql13HFR82P99nCm9GwTEKG0KYLvDe6REk8BCXuit6vJAv/Yxg5ABaNS2rA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.62.1", "@typescript-eslint/types": "8.62.1", @@ -2505,7 +2499,6 @@ "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2685,7 +2678,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.10.38", "caniuse-lite": "^1.0.30001799", @@ -3068,7 +3060,8 @@ "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/dompurify": { "version": "3.4.12", @@ -3255,7 +3248,6 @@ "integrity": "sha512-DgZS62aPLXKlnxILS/AYCoRvHaZeXceIzlXPkkGGzJWSow1aEk0lbTlxUSlyjC8jcaKxAdOnTDz+o1JFSBsyjw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -4187,7 +4179,6 @@ "integrity": "sha512-8i7LzZj7BF8uplX+ZyOlIz86V6TAsSs+np6m1kpW9u0JWi4z/1t+FzcK1aek+ybTnAC4KhBL4uXCNT0wcUIeCw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "cssstyle": "^4.1.0", "data-urls": "^5.0.0", @@ -4340,6 +4331,7 @@ "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", "dev": true, "license": "MIT", + "peer": true, "bin": { "lz-string": "bin/bin.js" } @@ -4773,7 +4765,6 @@ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -4874,6 +4865,7 @@ "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", @@ -4889,6 +4881,7 @@ "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=10" }, @@ -4911,7 +4904,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.7.tgz", "integrity": "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -4921,7 +4913,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.7.tgz", "integrity": "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==", "license": "MIT", - "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -4934,7 +4925,8 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/react-refresh": { "version": "0.17.0", @@ -5556,7 +5548,6 @@ "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -5653,7 +5644,6 @@ "integrity": "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", @@ -5767,7 +5757,6 @@ "integrity": "sha512-KrxIJ62Fd89gfysR4WotlgZABiz2dqFPgqGzX7s+CwsqLFomRH7777ZcrOD6+WVAh7khPQP41A+BKbpcJFrdEg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/chai": "^5.2.2", "@vitest/expect": "3.2.7", From a69768e523ff97253ceab4a850c70d21553796b9 Mon Sep 17 00:00:00 2001 From: KavinduLakshan393 Date: Sat, 22 Aug 2026 06:42:24 +0530 Subject: [PATCH 03/19] docs: remove obsolete Gateway_Intro_Rebuild_Implementation doc --- Gateway_Intro_Rebuild_Implementation.md | 1450 ----------------------- 1 file changed, 1450 deletions(-) delete mode 100644 Gateway_Intro_Rebuild_Implementation.md diff --git a/Gateway_Intro_Rebuild_Implementation.md b/Gateway_Intro_Rebuild_Implementation.md deleted file mode 100644 index df707c2..0000000 --- a/Gateway_Intro_Rebuild_Implementation.md +++ /dev/null @@ -1,1450 +0,0 @@ -# Gateway Intro and Landing Page Rebuild - -> **Implementation status:** Completed. This document retains the original rebuild proposal and -> timing rationale. The delivered logo treatment was refined after the proposal: both the intro and -> final gateway now use `LogoDrawReveal`, which draws one continuous rounded white SVG path with a -> `220`-unit stroke. It has no scale expansion, highlight sweep, secondary guide stroke, or break -> between the C and V. The source files listed below are authoritative when an embedded proposal -> snippet differs from the final implementation. - -## Scope - -This implementation is based on the supplied `Frontend.zip` snapshot. - -The current gateway is a fixed, two-column page: - -- The left hero uses the existing dark gateway palette and a photographic background. -- The left-side copy is static in `src/features/home/pages/HomePage.tsx`, original lines 11–16. -- The right side contains the existing Student and Admin access cards. -- The route is lazy-loaded and currently uses `GatewaySkeleton`, so the fallback also needs to match the first intro frame to prevent a skeleton flash before the animation starts. -- Existing design tokens already define the institutional navy/slate palette, Google Sans typography, radii, shadows, and motion curves. This solution consumes those variables rather than introducing a second design system. - -## Resulting page flow - -| Phase | Timing | Behaviour | -| ------------------ | ---------: | ----------------------------------------------------------------------------------------------------------- | -| Logo reveal | 2,000 ms | A single continuous white SVG path draws the complete C/V mark without scaling or a secondary guide stroke. | -| Caption 1 | 1,250 ms | “Build your academic profile.” | -| Caption 2 | 1,250 ms | Smooth vertical scroll to “Showcase skills and experience.” | -| Caption 3 | 1,250 ms | Smooth vertical scroll to “Connect with the right opportunities.” | -| Landing transition | 420 ms | The intro overlay fades and scales away, revealing the final gateway. | -| Final gateway | Continuous | The same three captions loop through a natural typewriter/delete sequence on the left side. | - -The intro’s slide viewport and final gateway page both hide scrollbars. The slide movement is transform-based, so it does not alter the document scroll position. - -## Design and integration decisions - -- All new selectors use the `gateway-v2-` namespace, preventing collisions with the existing gateway rules in `src/index.css`. -- Existing global tokens and shared `.button` variants are reused. -- The existing `/logo (2).png` asset remains the preload fallback; the animated runtime mark uses an inline SVG path, so no binary asset changes are required. -- The CSS is loaded from `src/main.tsx`, so the Suspense fallback and lazy-loaded page share the same first-paint styling. -- The gateway content is marked inert and hidden from assistive technology until the intro completes. -- The Skip button traps keyboard focus during the overlay and Escape also skips. -- After completion, focus moves to the gateway heading. -- `prefers-reduced-motion` bypasses the long sequence and avoids the typewriter character animation. -- No new npm dependencies are introduced. - -## File map - -### Existing files changed - -1. `src/main.tsx` -2. `src/shared/skeletons/GatewaySkeleton.tsx` -3. `src/features/home/pages/HomePage.tsx` - -### New files - -1. `src/features/home/data/gatewayCaptions.ts` -2. `src/features/home/hooks/usePrefersReducedMotion.ts` -3. `src/features/home/components/TypewriterText.tsx` -4. `src/features/home/components/GatewayIntro.tsx` -5. `src/features/home/components/LogoDrawReveal.tsx` -6. `src/features/home/styles/gateway.css` -7. `src/features/home/tests/GatewayIntro.test.tsx` -8. `src/features/home/tests/HomePage.test.tsx` - ---- - -# Existing file modifications - -Line numbers below refer to the unmodified files in the supplied `Frontend.zip`. - -## `src/main.tsx` - -**Change location:** Insert the CSS import immediately after original line 5 (`import './index.css'`). No other original lines change. - -**Final file contents:** - -```tsx -import { StrictMode } from 'react' -import { createRoot } from 'react-dom/client' -import App from './App' -import { env } from './app/config/env' -import './index.css' -import './features/home/styles/gateway.css' -import './styles/skeleton-system.css' -import './styles/sprint78-wireframe-alignment.css' -import './styles/internship-management-complete.css' -import './styles/shortlisted-page.css' - -const rootElement = document.getElementById('root') - -if (!rootElement) { - throw new Error('Application root element was not found.') -} - -async function enableDevelopmentMocks() { - if (!env.enableApiMocks || env.isProduction) return - const { worker } = await import('./mocks/browser') - await worker.start({ onUnhandledRequest: 'bypass' }) -} - -void enableDevelopmentMocks().then(() => { - createRoot(rootElement).render( - - - , - ) -}) -``` - -## `src/shared/skeletons/GatewaySkeleton.tsx` - -**Change location:** Replace original lines 1–38 in full. - -**Final file contents:** - -```tsx -import { SkeletonStatusRegion } from './SkeletonPrimitives' - -export function GatewaySkeleton() { - return ( - -