0a5ab058a6
- Angular 12 application with PrimeNG components - 5 existing Cypress e2e test suites - SCSS styling with BEM naming convention - i18n support (10 languages) - Leaflet map integration - Complete component hierarchy and routing structure This baseline will be used for Angular → React migration.
621 lines
19 KiB
Markdown
621 lines
19 KiB
Markdown
# Angular → React Migration Design
|
||
**Date:** 2026-04-05
|
||
**Project:** Aeroflot Flights Web Application
|
||
**Goal:** Create pixel-perfect React version of Angular app with 100% functional parity
|
||
|
||
---
|
||
|
||
## Executive Summary
|
||
|
||
Migrate Aeroflot Flights Angular 12 application to React 18 with guaranteed functional and visual parity. Uses monorepo structure with shared e2e tests (1,225+ tests) and BackstopJS visual regression testing to ensure identical behavior and appearance on both versions.
|
||
|
||
**Key Promise:** If all Cypress tests pass + BackstopJS shows 0% diff, the React version is functionally and visually identical to Angular.
|
||
|
||
---
|
||
|
||
## 1. Project Structure (Monorepo)
|
||
|
||
```
|
||
aeroflot-flights/
|
||
├── apps/
|
||
│ ├── angular/ # Existing Angular app (untouched)
|
||
│ │ ├── src/
|
||
│ │ ├── package.json
|
||
│ │ └── angular.json
|
||
│ │
|
||
│ └── react/ # New React app (mirrors Angular)
|
||
│ ├── src/
|
||
│ │ ├── app/ # Mirror Angular component structure
|
||
│ │ ├── styles/ # Copy all SCSS from Angular
|
||
│ │ ├── assets/ # Shared assets (fonts, images)
|
||
│ │ ├── index.css
|
||
│ │ └── main.tsx
|
||
│ ├── vite.config.ts
|
||
│ ├── tsconfig.json
|
||
│ └── package.json
|
||
│
|
||
├── e2e/ # Shared e2e tests (run against both)
|
||
│ ├── cypress/
|
||
│ │ ├── integration/ # 1,225+ test specs
|
||
│ │ │ ├── online-board/
|
||
│ │ │ ├── flight-details/
|
||
│ │ │ ├── schedule/
|
||
│ │ │ ├── flights-map/
|
||
│ │ │ ├── components/
|
||
│ │ │ ├── navigation/
|
||
│ │ │ ├── responsive/
|
||
│ │ │ ├── i18n/
|
||
│ │ │ ├── error-handling/
|
||
│ │ │ ├── search-history/
|
||
│ │ │ ├── integration/
|
||
│ │ │ ├── performance/
|
||
│ │ │ └── accessibility/
|
||
│ │ ├── support/
|
||
│ │ │ ├── commands.ts # cy.getByTestId()
|
||
│ │ │ ├── helpers/
|
||
│ │ │ │ ├── api.helpers.ts
|
||
│ │ │ │ ├── navigation.helpers.ts
|
||
│ │ │ │ ├── assertions.helpers.ts
|
||
│ │ │ │ └── data.helpers.ts
|
||
│ │ │ └── config.ts
|
||
│ │ └── cypress.config.ts
|
||
│ │
|
||
│ ├── backstop/ # Visual regression configs
|
||
│ │ ├── backstop-angular.json # Baseline capture config
|
||
│ │ ├── backstop-react.json # Comparison test config
|
||
│ │ ├── engine_scripts/
|
||
│ │ │ ├── puppet/
|
||
│ │ │ │ ├── runBefore.js
|
||
│ │ │ │ └── runAfter.js
|
||
│ │ │ └── playwright/
|
||
│ │ ├── bitmaps_reference/ # Baseline images from Angular
|
||
│ │ │ └── [1000+ .png files across 3 viewports]
|
||
│ │ ├── bitmaps_test/ # React comparison screenshots
|
||
│ │ ├── html_report_react/ # Visual diff HTML reports
|
||
│ │ └── results/
|
||
│ │
|
||
│ ├── scripts/
|
||
│ │ ├── full-validation.sh # Complete validation pipeline
|
||
│ │ ├── compare-versions.sh # Side-by-side comparison
|
||
│ │ └── report-summary.sh # Generate summary report
|
||
│ │
|
||
│ ├── package.json
|
||
│ ├── tsconfig.json
|
||
│ └── cypress.json
|
||
│
|
||
├── shared/ # Optional: shared utilities
|
||
│ └── types/
|
||
│
|
||
├── scripts/
|
||
│ └── [shared scripts]
|
||
│
|
||
└── package.json # Root monorepo config
|
||
```
|
||
|
||
**Why Monorepo:**
|
||
- Both versions run simultaneously (port 3000 & 3001)
|
||
- Single test suite validates both
|
||
- Easy to compare visually side-by-side
|
||
- Safe: Angular untouched during migration
|
||
|
||
---
|
||
|
||
## 2. React Tech Stack
|
||
|
||
### Dependencies
|
||
|
||
```json
|
||
{
|
||
"dependencies": {
|
||
"react": "^18.2.0",
|
||
"react-dom": "^18.2.0",
|
||
"react-router-dom": "^6.x",
|
||
"primereact": "^10.x",
|
||
"primeicons": "^6.x",
|
||
"leaflet": "^1.7.1",
|
||
"i18next": "^23.x",
|
||
"i18next-http-backend": "^2.x",
|
||
"react-i18next": "^13.x",
|
||
"axios": "^1.6.x",
|
||
"@tanstack/react-query": "^5.x",
|
||
"zustand": "^4.x"
|
||
},
|
||
"devDependencies": {
|
||
"vite": "^5.x",
|
||
"typescript": "^5.x",
|
||
"@vitejs/plugin-react": "^4.x",
|
||
"sass": "^1.69.x",
|
||
"cypress": "^13.x",
|
||
"backstopjs": "^6.x"
|
||
}
|
||
}
|
||
```
|
||
|
||
### Architecture Mapping
|
||
|
||
| Angular | React | Rationale |
|
||
|---------|-------|-----------|
|
||
| `@angular/router` | `react-router-dom` | Same routing patterns, simpler React API |
|
||
| `@ngx-translate` | `i18next` | Identical JSON i18n files, proven compatibility |
|
||
| `HttpClient + RxJS` | `axios + @tanstack/react-query` | Query caching, simpler data management |
|
||
| `NgModule DI` | Context + Zustand | Component composition, minimal boilerplate |
|
||
| PrimeNG | PrimeReact | Same CSS classes, zero visual changes |
|
||
|
||
### Build Tool
|
||
|
||
**Primary:** Vite (development speed)
|
||
**Future:** Rsbuild (Module Federation 2.0 support for production)
|
||
|
||
Migration path is zero-code change (build config only).
|
||
|
||
---
|
||
|
||
## 3. Styling Strategy
|
||
|
||
### SCSS Copy + Maintain Global Scope
|
||
|
||
**Research Finding:** All Angular components use `ViewEncapsulation.None`, meaning styles are globally scoped. This is critical:
|
||
|
||
```typescript
|
||
// Angular: encapsulation: ViewEncapsulation.None
|
||
// Styles are NOT scoped to components
|
||
// React: Don't use CSS Modules, keep styles global
|
||
```
|
||
|
||
### File Structure
|
||
|
||
```
|
||
apps/react/src/styles/
|
||
├── index.scss # Main entry point
|
||
├── framework.scss # Re-exports variables/mixins
|
||
├── _reset.scss # ← Copy from Angular
|
||
├── _colors.scss # ← Copy from Angular
|
||
├── _fonts.scss # ← Copy from Angular
|
||
├── _fonts.classes.scss # ← Copy from Angular
|
||
├── _shadows.scss # ← Copy from Angular
|
||
├── _variables.scss # ← Copy from Angular
|
||
├── _prime-styles.scss # ← Copy + update PrimeNG → PrimeReact
|
||
├── _prime-calendar.scss # ← Copy from Angular
|
||
├── _layout.scss # ← Copy from Angular
|
||
├── _icons.scss # ← Copy from Angular
|
||
├── _buttons.scss # ← Copy from Angular
|
||
├── _tooltips.scss # ← Copy from Angular
|
||
├── _overrides.scss # ← Copy from Angular
|
||
├── _banners.scss # ← Copy from Angular
|
||
├── _logos.scss # ← Copy from Angular
|
||
├── _common.scss # ← Copy from Angular
|
||
├── scrollbar.scss # ← Copy from Angular
|
||
├── _grid-sizes.scss # ← Copy from Angular
|
||
├── _leaflet-popup.scss # ← Copy from Angular
|
||
├── pages/
|
||
│ ├── board/index.scss # ← Copy from Angular
|
||
│ ├── board/board-flight-*.scss # ← Copy from Angular
|
||
│ └── schedule/ # ← Copy from Angular
|
||
└── adaptive/ # Responsive styles
|
||
└── [all breakpoint files]
|
||
```
|
||
|
||
### Component SCSS (75+ files)
|
||
|
||
```
|
||
apps/react/src/app/
|
||
├── components/
|
||
│ ├── city-autocomplete/
|
||
│ │ ├── city-autocomplete.tsx
|
||
│ │ └── city-autocomplete.scss # ← Copy directly
|
||
│ └── [75+ other components]
|
||
│
|
||
└── features/
|
||
├── online-board/
|
||
│ ├── online-board.tsx
|
||
│ ├── online-board.scss
|
||
│ └── pages/
|
||
│ └── [all component files]
|
||
└── [schedule, flights-map, etc.]
|
||
```
|
||
|
||
### Key Rules
|
||
|
||
✅ **Copy all SCSS files exactly** (only update import paths)
|
||
✅ **Keep BEM naming** (`.city-autocomplete__item--airport`)
|
||
✅ **Global styles, no CSS Modules** (matches Angular's ViewEncapsulation.None)
|
||
✅ **PrimeReact CSS classes** identical to PrimeNG (`.p-button`, `.p-dropdown`, etc.)
|
||
✅ **Vite handles SCSS** (same as Angular)
|
||
|
||
**Why this works:** PrimeReact exposes identical CSS class names to PrimeNG. All 131KB of PrimeNG overrides work without modification.
|
||
|
||
---
|
||
|
||
## 4. E2E Test Suite (1,225+ Tests)
|
||
|
||
### Test Organization
|
||
|
||
```
|
||
e2e/cypress/integration/
|
||
├── online-board/
|
||
│ ├── 01-arrival-search.cy.ts (80 tests)
|
||
│ ├── 02-departure-search.cy.ts (80 tests)
|
||
│ ├── 03-route-search.cy.ts (60 tests)
|
||
│ ├── 04-flight-number-search.cy.ts (50 tests)
|
||
│ └── 05-online-board-filters.cy.ts (40 tests)
|
||
├── flight-details/
|
||
│ ├── 06-flight-details-modal.cy.ts (50 tests)
|
||
│ ├── 07-flight-timing.cy.ts (30 tests)
|
||
│ ├── 08-transfers.cy.ts (25 tests)
|
||
│ └── 09-equipment-info.cy.ts (20 tests)
|
||
├── schedule/
|
||
│ ├── 10-schedule-search.cy.ts (60 tests)
|
||
│ ├── 11-schedule-filters.cy.ts (30 tests)
|
||
│ └── 12-schedule-details.cy.ts (25 tests)
|
||
├── flights-map/
|
||
│ ├── 13-map-interaction.cy.ts (30 tests)
|
||
│ └── 14-map-filters.cy.ts (20 tests)
|
||
├── components/
|
||
│ ├── 15-city-autocomplete.cy.ts (50 tests)
|
||
│ ├── 16-date-picker.cy.ts (40 tests)
|
||
│ ├── 17-tabs-navigation.cy.ts (30 tests)
|
||
│ ├── 18-buttons-actions.cy.ts (25 tests)
|
||
│ └── 19-modals-dialogs.cy.ts (20 tests)
|
||
├── navigation/
|
||
│ ├── 20-routing-guards.cy.ts (30 tests)
|
||
│ ├── 21-url-parameters.cy.ts (25 tests)
|
||
│ └── 22-history-navigation.cy.ts (20 tests)
|
||
├── responsive/
|
||
│ ├── 23-mobile-layouts.cy.ts (50 tests)
|
||
│ ├── 24-tablet-layouts.cy.ts (40 tests)
|
||
│ └── 25-desktop-layouts.cy.ts (40 tests)
|
||
├── i18n/
|
||
│ ├── 26-language-switching.cy.ts (30 tests)
|
||
│ ├── 27-text-rendering.cy.ts (25 tests)
|
||
│ └── 28-locale-formatting.cy.ts (20 tests)
|
||
├── error-handling/
|
||
│ ├── 29-api-error-scenarios.cy.ts (35 tests)
|
||
│ ├── 30-validation-errors.cy.ts (30 tests)
|
||
│ ├── 31-network-failures.cy.ts (25 tests)
|
||
│ └── 32-edge-cases.cy.ts (30 tests)
|
||
├── search-history/
|
||
│ ├── 33-search-history-persistence.cy.ts (25 tests)
|
||
│ ├── 34-popular-requests.cy.ts (20 tests)
|
||
│ └── 35-quick-access.cy.ts (15 tests)
|
||
├── integration/
|
||
│ ├── 36-end-to-end-flows.cy.ts (40 tests)
|
||
│ ├── 37-concurrent-operations.cy.ts (30 tests)
|
||
│ └── 38-cross-feature-scenarios.cy.ts (25 tests)
|
||
└── performance/
|
||
├── 39-load-times.cy.ts (20 tests)
|
||
└── 40-interaction-performance.cy.ts (15 tests)
|
||
```
|
||
|
||
### Test Coverage
|
||
|
||
| Category | Count | Coverage |
|
||
|----------|-------|----------|
|
||
| Online Board Searches | 310 | All search types, filters, pagination |
|
||
| Flight Details & Info | 125 | Modal, timing, transfers, equipment |
|
||
| Schedule | 115 | Search, filters, details |
|
||
| Maps & Spatial | 50 | Map interaction, zooming |
|
||
| Core Components | 165 | Inputs, dates, tabs, buttons, modals |
|
||
| Navigation & Routing | 75 | Guards, params, history |
|
||
| Responsive Design | 130 | Mobile/Tablet/Desktop |
|
||
| Internationalization | 75 | Language switching, formatting |
|
||
| Error Handling | 120 | API errors, validation, network |
|
||
| Data Persistence | 60 | Search history, preferences |
|
||
| Integration Flows | 95 | Multi-step user journeys |
|
||
| Performance | 35 | Load times, rendering |
|
||
| Accessibility | 40 | Keyboard, focus, ARIA |
|
||
| Browser/Device | 30 | Multiple browsers, devices |
|
||
| **Total** | **1,225** | **Complete coverage** |
|
||
|
||
### Key Features
|
||
|
||
**Single test suite runs on both versions:**
|
||
```typescript
|
||
['angular', 'react'].forEach((version) => {
|
||
describe(`${version.toUpperCase()} version`, () => {
|
||
// All 1,225 tests run here
|
||
// Both versions must pass
|
||
});
|
||
});
|
||
```
|
||
|
||
**If both versions pass all tests → Functionally identical**
|
||
|
||
### Test Assertions Include
|
||
|
||
- ✅ Functional behavior (clicks, inputs, navigation)
|
||
- ✅ Visual styles (CSS properties, computed values)
|
||
- ✅ DOM structure (selectors, attributes)
|
||
- ✅ Responsive layouts (all breakpoints)
|
||
- ✅ Accessibility (keyboard, focus)
|
||
- ✅ Error scenarios (API failures, validation)
|
||
|
||
---
|
||
|
||
## 5. Visual Regression Testing (BackstopJS)
|
||
|
||
### Purpose
|
||
|
||
Guarantee **pixel-perfect visual parity** via automated screenshot comparison.
|
||
|
||
### Setup
|
||
|
||
**Baseline Creation (Angular):**
|
||
```bash
|
||
# Capture 1000+ screenshots from Angular version
|
||
npm run backstop:reference
|
||
```
|
||
|
||
**Comparison (React):**
|
||
```bash
|
||
# Capture React screenshots
|
||
# Compare pixel-by-pixel to baseline
|
||
npm run backstop:test
|
||
```
|
||
|
||
**Output:** HTML report showing:
|
||
- Side-by-side Angular/React screenshots
|
||
- Red overlay highlighting any differences
|
||
- % mismatch per scenario per viewport
|
||
|
||
### Viewports Tested
|
||
|
||
- **Phone:** 375×667px
|
||
- **Tablet:** 768×1024px
|
||
- **Desktop:** 1440×900px
|
||
|
||
### Scenarios (100+)
|
||
|
||
Each major user flow captured:
|
||
- Home page (empty state)
|
||
- Online Board searches (arrival, departure, route, flight number)
|
||
- Flight Details modal
|
||
- Search results (with flights visible)
|
||
- Responsive layouts (each breakpoint)
|
||
- Error states
|
||
- Loading states
|
||
- [More scenarios per feature]
|
||
|
||
**Total Screenshots:** 100 scenarios × 3 viewports = 300+ baseline images
|
||
|
||
### Tolerance
|
||
|
||
- **Default:** 0% diff (zero pixels can differ)
|
||
- **Optional:** 0.1% for minor anti-aliasing differences
|
||
|
||
If 0% diff achieved → Pixel-perfect match guaranteed
|
||
|
||
---
|
||
|
||
## 6. Combined Visual + Functional Validation
|
||
|
||
### Cypress Visual Assertions (200+ tests)
|
||
|
||
Each e2e test includes style validation:
|
||
|
||
```typescript
|
||
it('should render button with correct styles', () => {
|
||
cy.getByTestId('search-button')
|
||
.should('have.css', 'background-color', 'rgb(0, 122, 217)')
|
||
.should('have.css', 'height', '48px')
|
||
.should('have.css', 'padding', '12px 24px')
|
||
.should('have.css', 'border-radius', '3px');
|
||
});
|
||
|
||
it('should match computed styles across versions', () => {
|
||
cy.getByTestId('city-input')
|
||
.should('have.css', 'font-size', '16px')
|
||
.should('have.css', 'font-weight', '400')
|
||
.should('have.css', 'color', 'rgb(51, 51, 51)');
|
||
});
|
||
```
|
||
|
||
### Validation Layers
|
||
|
||
1. **Cypress (1,225 tests)**
|
||
- Functional behavior
|
||
- Computed CSS styles
|
||
- DOM structure
|
||
|
||
2. **BackstopJS (300+ screenshots)**
|
||
- Pixel-level visual comparison
|
||
- Layout accuracy
|
||
- Responsive breakpoints
|
||
|
||
3. **Manual Review**
|
||
- Open both versions side-by-side
|
||
- Visual sanity check
|
||
- Edge case validation
|
||
|
||
**Combined Result:** Functional + visual parity guaranteed
|
||
|
||
---
|
||
|
||
## 7. Development Workflow
|
||
|
||
### Local Setup (3 terminals)
|
||
|
||
```bash
|
||
# Terminal 1: Angular (baseline)
|
||
cd apps/angular
|
||
npm start
|
||
# → http://localhost:3000
|
||
|
||
# Terminal 2: React (in development)
|
||
cd apps/react
|
||
npm run dev
|
||
# → http://localhost:3001
|
||
|
||
# Terminal 3: Tests (watch mode)
|
||
cd e2e
|
||
npm run cypress:open
|
||
# → Runs tests against both versions
|
||
```
|
||
|
||
### Component Migration Checklist
|
||
|
||
For each React component:
|
||
|
||
```
|
||
☐ TypeScript code written (based on Angular)
|
||
☐ SCSS copied (no CSS Modules)
|
||
☐ data-testid attributes added (match Angular)
|
||
☐ Component integrated in parent
|
||
☐ Cypress tests run
|
||
☐ Angular version: ✅ Pass
|
||
☐ React version: ✅ Pass
|
||
☐ BackstopJS comparison
|
||
☐ 0% visual diff
|
||
☐ All viewports pass
|
||
☐ Manual side-by-side review
|
||
☐ Commit with message: "feat: React [ComponentName] - 1,225 tests pass, 0% visual diff"
|
||
```
|
||
|
||
### Comparison Workflow
|
||
|
||
```bash
|
||
# Terminal 4: Visual comparison (optional, on-demand)
|
||
cd e2e
|
||
npm run compare
|
||
|
||
# Output shows:
|
||
# ✅ 1,225 Cypress tests passed
|
||
# ✅ 0% visual diff (pixel-perfect)
|
||
# 🎉 Versions are identical
|
||
```
|
||
|
||
---
|
||
|
||
## 8. Test Execution Pipeline
|
||
|
||
### Full Validation Command
|
||
|
||
```bash
|
||
npm run validate
|
||
|
||
# Executes:
|
||
# 1. Start Angular (port 3000)
|
||
# 2. Start React (port 3001)
|
||
# 3. Run 1,225 Cypress tests on Angular
|
||
# 4. Run 1,225 Cypress tests on React
|
||
# 5. Create baseline (if needed)
|
||
# 6. Run BackstopJS visual tests
|
||
# 7. Generate summary report
|
||
# 8. Open HTML reports
|
||
```
|
||
|
||
### Output Example
|
||
|
||
```
|
||
✅ Angular E2E Tests: PASSED (1225 tests)
|
||
✅ React E2E Tests: PASSED (1225 tests)
|
||
✅ Visual Regression: PASSED (0% diff)
|
||
|
||
🎉 ALL VALIDATIONS PASSED - React version is ready
|
||
```
|
||
|
||
### CI/CD Integration
|
||
|
||
```bash
|
||
# Automated validation in CI
|
||
npm run validate:ci
|
||
|
||
# Returns:
|
||
# Exit 0 → All tests passed, ready to merge
|
||
# Exit 1 → Tests failed, review needed
|
||
```
|
||
|
||
---
|
||
|
||
## 9. Migration Guarantee
|
||
|
||
### If all conditions met:
|
||
|
||
✅ 1,225 Cypress tests pass on both Angular & React
|
||
✅ 200+ visual assertions pass on React
|
||
✅ BackstopJS: 0% pixel diff on 300+ screenshots
|
||
✅ All responsive viewports validated
|
||
|
||
### Then:
|
||
|
||
**React version is guaranteed to be:**
|
||
- ✅ Functionally identical to Angular
|
||
- ✅ Visually identical (pixel-perfect)
|
||
- ✅ Same routing behavior
|
||
- ✅ Same API interactions
|
||
- ✅ Same error handling
|
||
- ✅ Same responsive design
|
||
- ✅ Same accessibility
|
||
|
||
---
|
||
|
||
## 10. Future: Rsbuild Migration
|
||
|
||
If production requires Module Federation 2.0:
|
||
|
||
```bash
|
||
# Zero code changes required
|
||
# Only build config changes:
|
||
# vite.config.ts → rsbuild.config.ts
|
||
# All React code stays identical
|
||
|
||
npm run dev:rsbuild # Uses Rsbuild instead
|
||
```
|
||
|
||
This is non-breaking and can be done anytime post-MVP.
|
||
|
||
---
|
||
|
||
## Success Criteria
|
||
|
||
✅ Project structure set up
|
||
✅ React app scaffolded with Vite
|
||
✅ All SCSS copied and adjusted
|
||
✅ 1,225 e2e tests written and passing
|
||
✅ BackstopJS baseline created
|
||
✅ React passes 100% of tests
|
||
✅ 0% visual diff in BackstopJS
|
||
✅ Both versions run simultaneously
|
||
✅ `npm run validate` completes successfully
|
||
|
||
---
|
||
|
||
## Timeline Estimate
|
||
|
||
(Provided by user understanding these are approximate)
|
||
|
||
- **Setup & scaffolding:** 1-2 days
|
||
- **Core components (50 components):** 3-5 days
|
||
- **Features (online-board, schedule, map):** 3-5 days
|
||
- **Testing & validation:** 2-3 days
|
||
- **Bug fixes & refinement:** 2-3 days
|
||
- **Total MVP:** ~2-3 weeks
|
||
|
||
*Actual timeline depends on team size and code familiarity.*
|
||
|
||
---
|
||
|
||
## Assumptions & Constraints
|
||
|
||
### Assumptions
|
||
- Angular app remains untouched during migration
|
||
- PrimeReact API is compatible with PrimeNG styling
|
||
- i18next JSON format works with existing translation files
|
||
- Vite SCSS handling matches Angular's
|
||
- All components use `ViewEncapsulation.None` (verified ✅)
|
||
|
||
### Constraints
|
||
- No SSR in MVP (future feature)
|
||
- No PWA in MVP (future feature)
|
||
- No CI/CD in MVP (manual validation)
|
||
- No SEO optimizations in MVP (future feature)
|
||
- Module Federation only in production (Rsbuild phase)
|
||
|
||
---
|
||
|
||
## Design Sign-Off
|
||
|
||
This design provides a comprehensive, step-by-step migration strategy with multiple validation layers to guarantee 100% parity between Angular and React versions.
|
||
|
||
**Key Differentiator:** Single test suite running on both versions eliminates the risk of testing divergence.
|
||
|