# Angular → React Migration Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Migrate Aeroflot Flights Angular 12 application to React 18 with pixel-perfect visual and functional parity, validated by 1,225+ e2e tests and BackstopJS visual regression. **Architecture:** Monorepo structure with both Angular and React versions running simultaneously. Single e2e test suite validates both. SCSS copied directly (no CSS Modules). PrimeReact replaces PrimeNG (identical CSS classes). Vite for dev speed, future Rsbuild support. **Tech Stack:** React 18, Vite, PrimeReact, react-router-dom, i18next, axios, @tanstack/react-query, Zustand, Cypress, BackstopJS --- ## Phase 1: Infrastructure Setup ### Task 1: Create monorepo structure and move Angular app to apps/angular **Files:** - Create: `apps/angular/` (move existing `ClientApp/` here) - Create: `apps/react/` (new React app) - Create: `e2e/` (shared e2e tests) - Create: `package.json` (root monorepo config) - [ ] **Step 1: Create root package.json for monorepo** ```json { "name": "aeroflot-flights", "private": true, "workspaces": [ "apps/angular", "apps/react", "e2e" ], "scripts": { "dev:angular": "cd apps/angular && npm start", "dev:react": "cd apps/react && npm run dev", "dev:both": "concurrently \"npm run dev:angular\" \"npm run dev:react\"", "validate": "cd e2e && bash ../scripts/full-validation.sh" }, "devDependencies": { "concurrently": "^8.2.0" } } ``` - [ ] **Step 2: Move Angular ClientApp to apps/angular** ```bash mkdir -p apps mv ClientApp apps/angular cd apps/angular # Verify angular.json exists test -f angular.json && echo "✅ Angular app moved" ``` - [ ] **Step 3: Create apps/react directory structure** ```bash mkdir -p apps/react/src/{app,styles,assets,components,features} mkdir -p apps/react/public ``` - [ ] **Step 4: Create e2e directory structure** ```bash mkdir -p e2e/{cypress/integration/{online-board,flight-details,schedule,flights-map,components,navigation,responsive,i18n,error-handling,search-history,integration,performance,accessibility},cypress/support/helpers,backstop/{engine_scripts/{puppet,playwright},bitmaps_reference,bitmaps_test,html_report_react,results},scripts} ``` - [ ] **Step 5: Commit** ```bash git add -A git commit -m "infrastructure: create monorepo structure with apps/angular, apps/react, e2e directories" ``` --- ### Task 2: Scaffold React app with Vite **Files:** - Create: `apps/react/package.json` - Create: `apps/react/vite.config.ts` - Create: `apps/react/tsconfig.json` - Create: `apps/react/src/main.tsx` - Create: `apps/react/src/index.html` - Create: `apps/react/src/app/App.tsx` - [ ] **Step 1: Create React package.json** ```json { "name": "@aeroflot-flights/react", "version": "1.0.0", "type": "module", "scripts": { "dev": "vite", "build": "tsc && vite build", "preview": "vite preview" }, "dependencies": { "react": "^18.2.0", "react-dom": "^18.2.0", "react-router-dom": "^6.15.0", "primereact": "^10.0.0", "primeicons": "^6.0.0", "leaflet": "^1.7.1", "i18next": "^23.7.0", "i18next-http-backend": "^2.4.0", "react-i18next": "^13.5.0", "axios": "^1.6.0", "@tanstack/react-query": "^5.28.0", "zustand": "^4.4.0" }, "devDependencies": { "vite": "^5.0.0", "@vitejs/plugin-react": "^4.2.0", "typescript": "^5.3.0", "@types/react": "^18.2.0", "@types/react-dom": "^18.2.0", "sass": "^1.69.0" } } ``` - [ ] **Step 2: Create vite.config.ts** ```typescript import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' import path from 'path' export default defineConfig({ plugins: [react()], server: { port: 3001, open: false, }, resolve: { alias: { '@app': path.resolve(__dirname, './src/app'), '@styles': path.resolve(__dirname, './src/styles'), '@assets': path.resolve(__dirname, './src/assets'), }, }, build: { outDir: 'dist', sourcemap: false, }, }) ``` - [ ] **Step 3: Create tsconfig.json** ```json { "compilerOptions": { "target": "ES2020", "useDefineForClassFields": true, "lib": ["ES2020", "DOM", "DOM.Iterable"], "module": "ESNext", "skipLibCheck": true, "esModuleInterop": true, "allowSyntheticDefaultImports": true, "strict": true, "noUnusedLocals": true, "noUnusedParameters": true, "noFallthroughCasesInSwitch": true, "resolveJsonModule": true, "moduleResolution": "bundler", "baseUrl": ".", "paths": { "@app/*": ["./src/app/*"], "@styles/*": ["./src/styles/*"], "@assets/*": ["./src/assets/*"] } }, "include": ["src"], "references": [{ "path": "./tsconfig.node.json" }] } ``` - [ ] **Step 4: Create tsconfig.node.json** ```json { "compilerOptions": { "composite": true, "skipLibCheck": true, "module": "ESNext", "moduleResolution": "bundler", "allowSyntheticDefaultImports": true }, "include": ["vite.config.ts"] } ``` - [ ] **Step 5: Create src/index.html** ```html Aeroflot Flights
``` - [ ] **Step 6: Create src/main.tsx** ```typescript import React from 'react' import ReactDOM from 'react-dom/client' import App from './app/App' import './styles/index.scss' ReactDOM.createRoot(document.getElementById('root')!).render( , ) ``` - [ ] **Step 7: Create src/app/App.tsx** ```typescript import React from 'react' export default function App() { return (

Aeroflot Flights - React Migration

Coming soon...

) } ``` - [ ] **Step 8: Test React app builds** ```bash cd apps/react npm install npm run build # Expected: dist/ folder created with index.html, assets/ echo "✅ React app builds successfully" ``` - [ ] **Step 9: Commit** ```bash git add apps/react/ git commit -m "infrastructure: scaffold React app with Vite, TypeScript, and basic App component" ``` --- ### Task 3: Set up e2e folder with Cypress config **Files:** - Create: `e2e/package.json` - Create: `e2e/cypress.config.ts` - Create: `e2e/tsconfig.json` - Create: `e2e/cypress/support/commands.ts` - Create: `e2e/cypress/support/index.ts` - [ ] **Step 1: Create e2e/package.json** ```json { "name": "@aeroflot-flights/e2e", "version": "1.0.0", "private": true, "scripts": { "cypress:open": "cypress open", "cypress:run": "cypress run", "backstop:reference": "backstop reference --config=backstop/backstop-angular.json", "backstop:test": "backstop test --config=backstop/backstop-react.json", "validate": "bash ../scripts/full-validation.sh" }, "devDependencies": { "cypress": "^13.6.0", "backstopjs": "^6.3.0", "@cypress/schematic": "^2.5.0", "typescript": "^5.3.0" } } ``` - [ ] **Step 2: Create cypress.config.ts** ```typescript import { defineConfig } from 'cypress' export default defineConfig({ e2e: { baseUrl: 'http://localhost:3000', supportFile: 'cypress/support/index.ts', specPattern: 'cypress/integration/**/*.cy.ts', viewportWidth: 1440, viewportHeight: 900, video: false, screenshotOnRunFailure: true, requestTimeout: 10000, responseTimeout: 10000, defaultCommandTimeout: 10000, }, component: { devServer: { framework: 'react', bundler: 'vite', }, }, }) ``` - [ ] **Step 3: Create cypress/support/commands.ts** ```typescript /// Cypress.Commands.add('getByTestId', (id: string, timeout = 8000) => { return cy.get(`[data-testid="${id}"]`, { timeout }) }) Cypress.Commands.add('forbidGeolocation', () => { cy.window().then(win => { cy.stub(win.navigator.geolocation, 'getCurrentPosition').rejects( new Error('Geolocation forbidden') ) }) }) declare global { namespace Cypress { interface Chainable { getByTestId(id: string, timeout?: number): Chainable> forbidGeolocation(): Chainable } } } export {} ``` - [ ] **Step 4: Create cypress/support/index.ts** ```typescript import './commands' beforeEach(() => { // Clear localStorage before each test cy.window().then(win => { win.localStorage.clear() }) }) afterEach(() => { // Clean up after each test cy.window().then(win => { win.localStorage.clear() }) }) ``` - [ ] **Step 5: Create e2e/tsconfig.json** ```json { "compilerOptions": { "target": "ES2020", "lib": ["ES2020", "DOM"], "module": "ESNext", "moduleResolution": "node", "strict": true, "resolveJsonModule": true, "types": ["cypress", "node"] }, "include": ["cypress/**/*.ts", "cypress/**/*.tsx"] } ``` - [ ] **Step 6: Verify Cypress installs** ```bash cd e2e npm install npx cypress --version # Expected: Version output like "13.6.0" echo "✅ Cypress installed" ``` - [ ] **Step 7: Commit** ```bash git add e2e/ git commit -m "infrastructure: set up e2e folder with Cypress config and support commands" ``` --- ## Phase 2: Styling Foundation ### Task 4: Copy global SCSS files from Angular to React **Files:** - Create: `apps/react/src/styles/` (all 30 global SCSS files from Angular) - [ ] **Step 1: Copy framework and global style files** ```bash cd apps/react/src/styles # Copy from Angular cp ../../../apps/angular/src/styles/framework.scss . cp ../../../apps/angular/src/styles/_reset.scss . cp ../../../apps/angular/src/styles/_colors.scss . cp ../../../apps/angular/src/styles/_fonts.scss . cp ../../../apps/angular/src/styles/_fonts.classes.scss . cp ../../../apps/angular/src/styles/_shadows.scss . cp ../../../apps/angular/src/styles/_variables.scss . cp ../../../apps/angular/src/styles/_prime-styles.scss . cp ../../../apps/angular/src/styles/_prime-calendar.scss . cp ../../../apps/angular/src/styles/_layout.scss . cp ../../../apps/angular/src/styles/_icons.scss . cp ../../../apps/angular/src/styles/_buttons.scss . cp ../../../apps/angular/src/styles/_tooltips.scss . cp ../../../apps/angular/src/styles/_overrides.scss . cp ../../../apps/angular/src/styles/_banners.scss . cp ../../../apps/angular/src/styles/_logos.scss . cp ../../../apps/angular/src/styles/_common.scss . cp ../../../apps/angular/src/styles/_scrollbar.scss . cp ../../../apps/angular/src/styles/_grid-sizes.scss . cp ../../../apps/angular/src/styles/_leaflet-popup.scss . cp ../../../apps/angular/src/styles/styles/_mixins.scss ./ cp ../../../apps/angular/src/styles/_screen.scss . cp ../../../apps/angular/src/styles/_positioning.scss . cp ../../../apps/angular/src/styles/_layouts.scss . echo "✅ Global SCSS files copied" ``` - [ ] **Step 2: Copy pages and adaptive styles** ```bash # Create page directories mkdir -p pages/board pages/schedule adaptive # Copy page styles cp ../../../apps/angular/src/styles/pages/board/*.scss pages/board/ cp ../../../apps/angular/src/styles/pages/schedule/*.scss pages/schedule/ cp ../../../apps/angular/src/styles/pages/adaptive/*.scss adaptive/ echo "✅ Page and adaptive styles copied" ``` - [ ] **Step 3: Create index.scss that imports all styles** ```scss @import 'framework'; @import 'reset'; @import 'screen'; @import 'layouts'; @import 'colors'; @import 'fonts'; @import 'fonts.classes'; @import 'shadows'; @import 'variables'; @import 'prime-styles'; @import 'layout'; @import 'icons'; @import 'buttons'; @import 'tooltips'; @import 'overrides'; @import 'prime-calendar'; @import 'pages/board/index'; @import 'pages/schedule/index'; @import 'banners'; @import 'logos'; @import 'common'; @import 'scrollbar'; @import 'grid-sizes'; @import 'leaflet-popup'; @import 'positioning'; @import 'mixins'; ``` - [ ] **Step 4: Copy assets (fonts, images)** ```bash # Copy fonts mkdir -p ../assets/fonts cp ../../../apps/angular/src/assets/fonts/* ../assets/fonts/ # Copy images mkdir -p ../assets/images cp -r ../../../apps/angular/src/assets/*.{png,jpg,jpeg,svg,ico} ../assets/ 2>/dev/null || true echo "✅ Assets copied" ``` - [ ] **Step 5: Verify SCSS compiles without errors** ```bash cd apps/react npm install # Try building npm run build 2>&1 | grep -i "error" || echo "✅ SCSS compiles successfully" ``` - [ ] **Step 6: Commit** ```bash git add apps/react/src/styles apps/react/src/assets git commit -m "styles: copy all global and page SCSS files and assets from Angular" ``` --- ### Task 5: Copy component SCSS files from Angular to React **Files:** - Create: `apps/react/src/app/` (75+ component SCSS files from Angular) - [ ] **Step 1: Create component directory structure** ```bash # Create all component directories matching Angular structure mkdir -p apps/react/src/app/components/city-autocomplete mkdir -p apps/react/src/app/components/page-layout mkdir -p apps/react/src/app/features/online-board/pages mkdir -p apps/react/src/app/features/online-board/components mkdir -p apps/react/src/app/features/schedule/pages mkdir -p apps/react/src/app/features/flights-map mkdir -p apps/react/src/app/modules/components mkdir -p apps/react/src/app/modules/pages echo "✅ Component directories created" ``` - [ ] **Step 2: Copy component SCSS files** ```bash # This is a bulk copy operation - copy all .scss files from Angular components find apps/angular/src/app -name "*.component.scss" | while read file; do # Get relative path rel_path="${file#apps/angular/src/app/}" # Create target directory target_dir="apps/react/src/app/$(dirname "$rel_path")" mkdir -p "$target_dir" # Copy file, renaming .component.scss to .scss target_file="${file#apps/angular/}" target_file="${target_file%.component.scss}.scss" cp "$file" "apps/react/${target_file%.component.scss}.scss" done echo "✅ Component SCSS files copied" ``` - [ ] **Step 3: Verify all SCSS files copied** ```bash # Count SCSS files angular_count=$(find apps/angular/src/app -name "*.component.scss" | wc -l) react_count=$(find apps/react/src/app -name "*.scss" | wc -l) echo "Angular SCSS components: $angular_count" echo "React SCSS files: $react_count" if [ "$react_count" -gt "50" ]; then echo "✅ All component SCSS files copied" else echo "⚠️ Warning: React has fewer SCSS files than expected" fi ``` - [ ] **Step 4: Create component index files that import SCSS** Each component will have minimal TypeScript that imports its styles: ```bash cat > apps/react/src/app/components/city-autocomplete/city-autocomplete.scss << 'EOF' @use "src/styles/framework" as *; .city-autocomplete { display: flex; flex-direction: column; &__labels-container { justify-content: space-between; margin-bottom: $space-m; width: 100%; display: flex; align-items: center; } &__label { @include font-overflow(); @include font-small($gray); } &__input { display: flex; flex-direction: row; position: relative; align-items: center; width: 100%; @include control-border-shadow(); } // Copy all remaining styles from Angular } EOF ``` - [ ] **Step 5: Commit** ```bash git add apps/react/src/app git commit -m "styles: copy all 75+ component SCSS files from Angular maintaining folder structure" ``` --- ## Phase 3: Core Components ### Task 6: Create Button component (first core UI component) **Files:** - Create: `apps/react/src/app/components/button/button.tsx` - Create: `apps/react/src/app/components/button/button.scss` - Create: `apps/react/src/app/components/button/index.ts` - [ ] **Step 1: Create button component** ```typescript // apps/react/src/app/components/button/button.tsx import React from 'react' import './button.scss' export interface ButtonProps extends React.ButtonHTMLAttributes { variant?: 'primary' | 'secondary' | 'tertiary' size?: 'small' | 'medium' | 'large' disabled?: boolean loading?: boolean children: React.ReactNode } export const Button: React.FC = ({ variant = 'primary', size = 'medium', disabled = false, loading = false, className = '', ...props }) => { return ( ) } ``` - [ ] **Step 2: Copy button SCSS from Angular** ```bash # Copy from Angular's button styles cp apps/angular/src/styles/_buttons.scss apps/react/src/app/components/button/button.scss # Verify it exists test -f apps/react/src/app/components/button/button.scss && echo "✅ Button SCSS copied" ``` - [ ] **Step 3: Create index.ts for exports** ```typescript // apps/react/src/app/components/button/index.ts export { Button } from './button' export type { ButtonProps } from './button' ``` - [ ] **Step 4: Create test file** ```typescript // e2e/cypress/integration/components/button.cy.ts describe('Button Component', () => { beforeEach(() => { cy.visit('http://localhost:3001') }) it('should render button with text', () => { cy.getByTestId('button').should('exist') }) it('should have correct styling', () => { cy.getByTestId('button') .should('have.css', 'padding') .should('have.css', 'border-radius') }) }) ``` - [ ] **Step 5: Verify button compiles** ```bash cd apps/react npm run build 2>&1 | grep -i "error" || echo "✅ Button component compiles" ``` - [ ] **Step 6: Commit** ```bash git add apps/react/src/app/components/button e2e/cypress/integration/components/button.cy.ts git commit -m "feat: implement Button component with styling from Angular" ``` --- ### Task 7: Create Input component **Files:** - Create: `apps/react/src/app/components/input/input.tsx` - Create: `apps/react/src/app/components/input/input.scss` - Create: `apps/react/src/app/components/input/index.ts` - [ ] **Step 1: Create input component** ```typescript // apps/react/src/app/components/input/input.tsx import React from 'react' import './input.scss' export interface InputProps extends React.InputHTMLAttributes { label?: string error?: string helperText?: string } export const Input: React.FC = ({ label, error, helperText, className = '', ...props }) => { return (
{label && } {error && {error}} {helperText && {helperText}}
) } ``` - [ ] **Step 2: Create input SCSS** ```scss // apps/react/src/app/components/input/input.scss @use "src/styles/framework" as *; .input-wrapper { display: flex; flex-direction: column; margin-bottom: $space-m; } .input-label { @include font-small($gray); margin-bottom: $space-s2; font-weight: 600; } .input { padding: $space-s2 $space-m; border: 1px solid $border-input; border-radius: $border-radius; font-size: 16px; transition: border-color 0.3s ease; &:focus { outline: none; border-color: $primary-color; } &--error { border-color: $red; } } .input-error { @include font-small($red); margin-top: $space-s; } .input-helper { @include font-small($gray); margin-top: $space-s; } ``` - [ ] **Step 3: Create index.ts** ```typescript // apps/react/src/app/components/input/index.ts export { Input } from './input' export type { InputProps } from './input' ``` - [ ] **Step 4: Create test** ```typescript // e2e/cypress/integration/components/input.cy.ts describe('Input Component', () => { beforeEach(() => { cy.visit('http://localhost:3001') }) it('should render input with label', () => { cy.getByTestId('input').should('exist') }) it('should show error state', () => { cy.getByTestId('input') .should('exist') }) }) ``` - [ ] **Step 5: Commit** ```bash git add apps/react/src/app/components/input e2e/cypress/integration/components/input.cy.ts git commit -m "feat: implement Input component with error and helper text support" ``` --- ### Task 8: Create core component collection (Modal, Tabs, DatePicker) **Files:** Create 3 more core components (Modal, Tabs, DatePicker) - [ ] **Step 1: Create Modal component** ```typescript // apps/react/src/app/components/modal/modal.tsx import React from 'react' import './modal.scss' export interface ModalProps { isOpen: boolean onClose: () => void title?: string children: React.ReactNode size?: 'small' | 'medium' | 'large' } export const Modal: React.FC = ({ isOpen, onClose, title, children, size = 'medium', }) => { if (!isOpen) return null return ( <>
{title && (

{title}

)}
{children}
) } ``` - [ ] **Step 2: Create Tabs component** ```typescript // apps/react/src/app/components/tabs/tabs.tsx import React, { useState } from 'react' import './tabs.scss' export interface Tab { id: string label: string content: React.ReactNode } export interface TabsProps { tabs: Tab[] defaultTab?: string onTabChange?: (tabId: string) => void } export const Tabs: React.FC = ({ tabs, defaultTab = tabs[0]?.id, onTabChange, }) => { const [activeTab, setActiveTab] = useState(defaultTab) const handleTabClick = (tabId: string) => { setActiveTab(tabId) onTabChange?.(tabId) } return (
{tabs.map(tab => ( ))}
{tabs.map(tab => ( ))}
) } ``` - [ ] **Step 3: Create DatePicker component** ```typescript // apps/react/src/app/components/date-picker/date-picker.tsx import React from 'react' import './date-picker.scss' export interface DatePickerProps { value?: string onChange?: (date: string) => void placeholder?: string label?: string error?: string } export const DatePicker: React.FC = ({ value, onChange, placeholder = 'DD.MM.YYYY', label, error, }) => { return (
{label && } onChange?.(e.target.value)} placeholder={placeholder} data-testid="date-picker-input" /> {error && {error}}
) } ``` - [ ] **Step 4: Create SCSS for all three components** ```bash # Copy SCSS from Angular for modals, tabs, date pickers cp apps/angular/src/styles/pages/board/components/page-tabs.scss apps/react/src/app/components/tabs/tabs.scss cp apps/angular/src/styles/styles/_prime-calendar.scss apps/react/src/app/components/date-picker/date-picker.scss # Create modal SCSS cat > apps/react/src/app/components/modal/modal.scss << 'EOF' @use "src/styles/framework" as *; .modal-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.4); z-index: 999; } .modal { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background-color: white; border-radius: $border-radius; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); z-index: 1000; max-height: 90vh; overflow-y: auto; &--small { width: 400px; } &--medium { width: 600px; } &--large { width: 800px; } } .modal-header { display: flex; justify-content: space-between; align-items: center; padding: $space-l; border-bottom: 1px solid #e0e0e0; } .modal-title { margin: 0; font-size: 20px; font-weight: 600; } .modal-close { background: none; border: none; font-size: 24px; cursor: pointer; color: #666; &:hover { color: #000; } } .modal-content { padding: $space-l; } EOF ``` - [ ] **Step 5: Create index.ts for each component** ```bash cat > apps/react/src/app/components/modal/index.ts << 'EOF' export { Modal } from './modal' export type { ModalProps } from './modal' EOF cat > apps/react/src/app/components/tabs/index.ts << 'EOF' export { Tabs } from './tabs' export type { TabsProps, Tab } from './tabs' EOF cat > apps/react/src/app/components/date-picker/index.ts << 'EOF' export { DatePicker } from './date-picker' export type { DatePickerProps } from './date-picker' EOF ``` - [ ] **Step 6: Create tests for all three** ```typescript // e2e/cypress/integration/components/modal.cy.ts describe('Modal Component', () => { it('should render modal when open', () => { cy.visit('http://localhost:3001') cy.getByTestId('modal').should('exist') }) }) // e2e/cypress/integration/components/tabs.cy.ts describe('Tabs Component', () => { it('should switch tabs when clicked', () => { cy.visit('http://localhost:3001') cy.getByTestId('tabs').should('exist') }) }) // e2e/cypress/integration/components/date-picker.cy.ts describe('DatePicker Component', () => { it('should accept date input', () => { cy.visit('http://localhost:3001') cy.getByTestId('date-picker-input').type('01.01.2024') }) }) ``` - [ ] **Step 7: Commit** ```bash git add apps/react/src/app/components/{modal,tabs,date-picker} git add e2e/cypress/integration/components/{modal,tabs,date-picker}.cy.ts git commit -m "feat: implement core UI components (Modal, Tabs, DatePicker) with styling and tests" ``` --- ## Phase 4: Routing Setup ### Task 9: Create React Router configuration **Files:** - Create: `apps/react/src/app/App.tsx` (with routing) - Create: `apps/react/src/app/router.tsx` - Create: `apps/react/src/app/pages/` (stub pages) - [ ] **Step 1: Create router configuration** ```typescript // apps/react/src/app/router.tsx import { createBrowserRouter } from 'react-router-dom' import App from './App' import Home from './pages/Home' import NotFound from './pages/NotFound' export const router = createBrowserRouter( [ { path: '/', element: , children: [ { index: true, element: , }, { path: ':lang/onlineboard/arrival/:code/:datetime', element:
Arrival Board Page
, }, { path: ':lang/onlineboard/departure/:code/:datetime', element:
Departure Board Page
, }, { path: ':lang/schedule/:code/:datetime', element:
Schedule Page
, }, { path: '*', element: , }, ], }, ], { basename: '/', } ) ``` - [ ] **Step 2: Update App.tsx to use router** ```typescript // apps/react/src/app/App.tsx import React from 'react' import { Outlet } from 'react-router-dom' import './App.scss' export default function App() { return (

Aeroflot Flights

© 2024 Aeroflot

) } ``` - [ ] **Step 3: Create stub pages** ```typescript // apps/react/src/app/pages/Home.tsx import React from 'react' export default function Home() { return (

Home

Welcome to Aeroflot Flights

) } // apps/react/src/app/pages/NotFound.tsx import React from 'react' export default function NotFound() { return (

404 - Page Not Found

) } ``` - [ ] **Step 4: Update main.tsx to use router** ```typescript // apps/react/src/main.tsx import React from 'react' import ReactDOM from 'react-dom/client' import { RouterProvider } from 'react-router-dom' import { router } from './app/router' import './styles/index.scss' ReactDOM.createRoot(document.getElementById('root')!).render( , ) ``` - [ ] **Step 5: Create App.scss** ```scss // apps/react/src/app/App.scss @use "src/styles/framework" as *; .app { display: flex; flex-direction: column; min-height: 100vh; } .app-header { background-color: white; padding: $space-xl; border-bottom: 1px solid #e0e0e0; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); h1 { margin: 0; font-size: 24px; font-weight: 600; } } .app-main { flex: 1; padding: $space-xl; max-width: $site-width; width: 100%; margin: 0 auto; } .app-footer { background-color: #f5f5f5; padding: $space-xl; text-align: center; border-top: 1px solid #e0e0e0; p { margin: 0; color: #666; font-size: 14px; } } ``` - [ ] **Step 6: Verify routing works** ```bash cd apps/react npm run dev & sleep 3 curl -s http://localhost:3001 | grep -q "Aeroflot Flights" && echo "✅ React routing works" pkill -f "vite" ``` - [ ] **Step 7: Commit** ```bash git add apps/react/src/app/{router.tsx,App.tsx,App.scss,pages/} git add apps/react/src/main.tsx git commit -m "feat: set up React Router with basic routing and layout structure" ``` --- ## Phase 5: E2E Test Infrastructure (Expanded) ### Task 10: Create test helper files and base test templates **Files:** - Create: `e2e/cypress/support/helpers/api.helpers.ts` - Create: `e2e/cypress/support/helpers/navigation.helpers.ts` - Create: `e2e/cypress/support/helpers/assertions.helpers.ts` - Create: `e2e/cypress/support/helpers/data.helpers.ts` - Create: `e2e/cypress/integration/online-board/01-arrival-search.cy.ts` - [ ] **Step 1: Create API helpers** ```typescript // e2e/cypress/support/helpers/api.helpers.ts export const setupApiInterceptors = () => { cy.intercept('GET', '**/api/flights/**', { fixture: 'flights.json' }).as('getFlights') cy.intercept('GET', '**/api/cities/**', { fixture: 'cities.json' }).as('getCities') cy.intercept('GET', '**/api/schedule/**', { fixture: 'schedule.json' }).as('getSchedule') } export const mockApiError = (endpoint: string, statusCode: number = 500) => { cy.intercept('GET', endpoint, { statusCode, body: { error: 'API Error' }, }).as('apiError') } export const mockApiSuccess = (endpoint: string, fixture: string) => { cy.intercept('GET', endpoint, { fixture }).as('apiSuccess') } ``` - [ ] **Step 2: Create navigation helpers** ```typescript // e2e/cypress/support/helpers/navigation.helpers.ts export const navigateToHome = () => { cy.visit('/') } export const navigateToArrivalBoard = (cityCode: string, date: string) => { cy.visit(`/ru-ru/onlineboard/arrival/${cityCode}/${date}-0000-2400`) } export const navigateToDepartureBoard = (cityCode: string, date: string) => { cy.visit(`/ru-ru/onlineboard/departure/${cityCode}/${date}-0000-2400`) } export const navigateToSchedule = (cityCode: string, date: string) => { cy.visit(`/ru-ru/schedule/${cityCode}/${date}`) } ``` - [ ] **Step 3: Create assertions helpers** ```typescript // e2e/cypress/support/helpers/assertions.helpers.ts export const assertButtonStylesCorrect = (selector: string) => { cy.get(selector) .should('have.css', 'padding') .should('have.css', 'border-radius') .should('have.css', 'background-color') } export const assertInputStylesCorrect = (selector: string) => { cy.get(selector) .should('have.css', 'padding') .should('have.css', 'border-radius') .should('have.css', 'border') } export const assertModalStylesCorrect = (selector: string) => { cy.get(selector) .should('have.css', 'position', 'fixed') .should('have.css', 'background-color') .should('have.css', 'z-index') } export const assertResponsiveLayout = () => { // Mobile cy.viewport(375, 667) cy.getByTestId('page-loaded').should('be.visible') // Tablet cy.viewport(768, 1024) cy.getByTestId('page-loaded').should('be.visible') // Desktop cy.viewport(1440, 900) cy.getByTestId('page-loaded').should('be.visible') } ``` - [ ] **Step 4: Create data helpers** ```typescript // e2e/cypress/support/helpers/data.helpers.ts export const TEST_DATA = { cities: { moscow: { code: 'SVO', name: 'Москва' }, spb: { code: 'LED', name: 'Санкт-Петербург' }, anapa: { code: 'AAQ', name: 'Анапа' }, }, dates: { today: new Date().toLocaleDateString('ru-RU'), tomorrow: new Date(Date.now() + 86400000).toLocaleDateString('ru-RU'), nextWeek: new Date(Date.now() + 604800000).toLocaleDateString('ru-RU'), }, } export const generateFlightNumber = () => { return `SU${Math.floor(Math.random() * 1000)}` } export const getCurrentDate = (format = 'DD.MM.YYYY') => { const now = new Date() const day = String(now.getDate()).padStart(2, '0') const month = String(now.getMonth() + 1).padStart(2, '0') const year = now.getFullYear() return `${day}.${month}.${year}` } ``` - [ ] **Step 5: Create first test suite template** ```typescript // e2e/cypress/integration/online-board/01-arrival-search.cy.ts import * as moment from 'moment' describe('Online Board: Arrival Search', () => { const BASE_URLS = { angular: 'http://localhost:3000', react: 'http://localhost:3001', } const arrivalCity = { name: 'Анапа', code: 'AAQ', } const today = moment().format('DD.MM.YYYY') ;['angular', 'react'].forEach(version => { describe(`${version.toUpperCase()} version`, () => { beforeEach(() => { cy.visit(`${BASE_URLS[version]}/`) cy.intercept('GET', '**api/flights/**').as('getFlights') }) it('should render home page', () => { cy.getByTestId('page-loaded').should('exist') }) it('should have arrival search button', () => { cy.getByTestId('arrival-search-button').should('be.visible') }) it('should search flights with city input', () => { cy.getByTestId('city-autocomplete-input') .should('exist') .type(`${arrivalCity.name}`) cy.getByTestId('calendar-input').type(today) cy.getByTestId('arrival-search-button').click() cy.wait('@getFlights') cy.getByTestId('flight-result').should('have.length.at.least', 0) }) it('should display search results', () => { cy.getByTestId('city-autocomplete-input').type(`${arrivalCity.name}`) cy.getByTestId('calendar-input').type(today) cy.getByTestId('arrival-search-button').click() cy.wait('@getFlights') // Results should be visible or empty state should show cy.getByTestId('board-search-result') .or(cy.getByTestId('empty-state')) .should('be.visible') }) }) }) }) ``` - [ ] **Step 6: Update cypress support/index.ts to load helpers** ```typescript // e2e/cypress/support/index.ts import './commands' import './helpers/api.helpers' import './helpers/navigation.helpers' import './helpers/assertions.helpers' import './helpers/data.helpers' beforeEach(() => { cy.window().then(win => { win.localStorage.clear() }) }) afterEach(() => { cy.window().then(win => { win.localStorage.clear() }) }) ``` - [ ] **Step 7: Verify tests can run (will fail, which is expected)** ```bash cd e2e npm run cypress:run -- --config baseUrl=http://localhost:3001 --spec "cypress/integration/online-board/01-arrival-search.cy.ts" || echo "⚠️ Tests running (failures expected at this stage)" ``` - [ ] **Step 8: Commit** ```bash git add e2e/cypress/support/helpers/ e2e/cypress/integration/online-board/ git commit -m "test: create e2e test helpers and first Arrival Search test suite" ``` --- ## Phase 6: Feature Implementation (Online Board Arrival) ### Task 11: Implement CityAutocomplete component **Files:** - Create: `apps/react/src/app/components/city-autocomplete/city-autocomplete.tsx` - Create: `apps/react/src/app/components/city-autocomplete/city-autocomplete.scss` - Create: `apps/react/src/app/components/city-autocomplete/index.ts` *(Detailed component implementation following the same pattern as Button - copy logic from Angular, update React patterns)* - [ ] **Step 1-5:** [Same pattern as Task 6 - Button component] - Create component with React hooks - Copy SCSS from Angular - Create index.ts - Add data-testid attributes matching Angular version - Test and commit --- ### Task 12: Implement OnlineBoard Arrival feature *(High-level feature integrating multiple components)* - [ ] **Steps 1-8:** Build Arrivalsearch page using components from Task 11 and earlier core components --- ## Phase 7: BackstopJS Setup ### Task 13: Create BackstopJS configuration for Angular (baseline) **Files:** - Create: `e2e/backstop/backstop-angular.json` - Create: `e2e/backstop/engine_scripts/puppet/runBefore.js` - Create: `e2e/backstop/engine_scripts/puppet/runAfter.js` *(Detailed configuration as specified in design document)* --- ### Task 14: Create BackstopJS configuration for React (comparison) **Files:** - Create: `e2e/backstop/backstop-react.json` *(Mirror of angular config, pointing to React localhost:3001)* --- ## Phase 8: Validation Scripts ### Task 15: Create full-validation.sh script **Files:** - Create: `scripts/full-validation.sh` *(Complete automation script as specified in design document)* --- ## Phase 9: Complete Test Suite ### Tasks 16-55: Write remaining 1,000+ tests Organized by feature: - **Tasks 16-20:** Online Board tests (80+ tests each module) - **Tasks 21-25:** Flight Details tests (50+ tests) - **Tasks 26-30:** Schedule tests (50+ tests) - **Tasks 31-35:** Components tests (50+ tests) - **Tasks 36-40:** Navigation tests (30+ tests) - **Tasks 41-45:** Responsive tests (50+ tests) - **Tasks 46-50:** i18n tests (30+ tests) - **Tasks 51-55:** Error handling & integration tests (100+ tests) Each task follows same pattern: 1. Write failing tests 2. Verify they fail 3. Implement feature/component 4. Verify tests pass 5. Commit --- ## Phase 10: Visual Validation & Final Checks ### Task 56: Generate BackstopJS baseline from Angular - [ ] **Step 1:** Start Angular version ```bash cd apps/angular && npm start & sleep 10 ``` - [ ] **Step 2:** Run BackstopJS reference capture ```bash cd e2e npm run backstop:reference ``` - [ ] **Step 3:** Verify baseline images created ```bash test -d backstop/bitmaps_reference && [ $(ls -1 backstop/bitmaps_reference | wc -l) -gt 100 ] && echo "✅ Baseline created" ``` - [ ] **Step 4:** Commit baseline ```bash git add e2e/backstop/bitmaps_reference/ git commit -m "test: generate BackstopJS baseline from Angular version" ``` --- ### Task 57: Run BackstopJS comparison on React - [ ] **Step 1:** Start React version ```bash cd apps/react && npm run dev & sleep 10 ``` - [ ] **Step 2:** Run BackstopJS test comparison ```bash cd e2e npm run backstop:test ``` - [ ] **Step 3:** Review visual diff report ```bash open e2e/backstop/html_report_react/index.html ``` - [ ] **Step 4:** Document any visual differences** If differences exist, note them and create issues for fixes. - [ ] **Step 5:** Commit results ```bash git add e2e/backstop/bitmaps_test_react/ git commit -m "test: run BackstopJS comparison - React vs Angular baseline" ``` --- ### Task 58: Fix any visual differences For each visual diff found: - [ ] **Step 1:** Identify the CSS property causing diff - [ ] **Step 2:** Update React component SCSS - [ ] **Step 3:** Regenerate BackstopJS screenshot - [ ] **Step 4:** Verify 0% diff - [ ] **Step 5:** Commit fix --- ### Task 59: Run full validation suite - [ ] **Step 1:** Start both versions ```bash npm run dev:both & sleep 10 ``` - [ ] **Step 2:** Run full validation ```bash npm run validate ``` - [ ] **Step 3:** Verify all tests pass Expected output: ``` ✅ Angular E2E Tests: PASSED (1225 tests) ✅ React E2E Tests: PASSED (1225 tests) ✅ Visual Regression: PASSED (0% diff) 🎉 ALL VALIDATIONS PASSED ``` - [ ] **Step 4:** Final commit** ```bash git commit -m "test: complete full validation - 1,225 tests pass, 0% visual diff" ``` --- ## Execution Checklist After all tasks completed: - [ ] All 1,225+ e2e tests pass on both Angular and React - [ ] BackstopJS shows 0% visual diff - [ ] `npm run validate` completes successfully - [ ] Both versions run on localhost:3000 and 3001 - [ ] All commits are clean and descriptive - [ ] Repository ready for production testing --- ## Notes - Each task should produce working, testable code - Commit after every logical step - Use exact file paths provided - Follow TDD when applicable (write test, watch fail, implement, watch pass) - Update this checklist as you progress - Stop if you encounter blockers and document them