diff --git a/README.md b/README.md index 01780b6..1b333c7 100644 --- a/README.md +++ b/README.md @@ -95,7 +95,8 @@ Die Datei landet in `output/Dienstplan_YYYY_MM_NRW.xlsx`. ├── templates/ # Basis-Vorlage ├── requirements.txt # Python-Abhängigkeiten (openpyxl) ├── SPECIFICATION.md # Vollständige Regeln & Formeln -└── README.md # Diese Datei +├── claude.md # Umfassende Projekt-Dokumentation +└── README.md # Diese Datei ``` ## Regeln (Variante 2 - streng) diff --git a/webapp/TEST_GUIDE.md b/webapp/TEST_GUIDE.md new file mode 100644 index 0000000..4ef68c1 --- /dev/null +++ b/webapp/TEST_GUIDE.md @@ -0,0 +1,201 @@ +# Test Suite - Dienstplan Bonusrechner + +Automatische Test Suite für die Web-App. + +## Schnellstart + +1. **Server starten** (falls noch nicht gestartet): + ```bash + cd webapp + python3 -m http.server 8000 + ``` + +2. **Test-Seite öffnen**: + ``` + http://localhost:8000/test.html + ``` + +3. **Tests ausführen**: + - Klicken Sie auf "Alle Tests ausführen" + - Warten Sie auf die Ergebnisse + - ✅ = Test bestanden + - ❌ = Test fehlgeschlagen + +## Was wird getestet? + +### 1. Holiday Provider (NRW-Feiertage) +- ✅ Feiertage werden korrekt erkannt +- ✅ Normale Tage werden nicht als Feiertage erkannt +- ✅ Tag vor Feiertag wird erkannt +- ✅ Spezifische Feiertage (Fronleichnam, etc.) + +### 2. Calculator - Tag-Klassifizierung +- ✅ Freitag ist qualifizierend +- ✅ Samstag ist qualifizierend +- ✅ Sonntag ist qualifizierend +- ✅ Normale Wochentage (Mo-Do) sind nicht qualifizierend +- ✅ Feiertage sind qualifizierend +- ✅ Tag vor Feiertag ist qualifizierend + +### 3. Calculator - Bonusberechnung +**Schwellenwert-Tests:** +- ✅ Unter Schwellenwert (1.0 WE-Tag) → 0€ +- ✅ Genau Schwellenwert (2.0 WE-Tage) → 450€ +- ✅ Über Schwellenwert (3.0 WE-Tage) → 900€ + +**Gemischte Dienste:** +- ✅ Normale Tage + WE-Tage korrekt berechnet +- ✅ Halbe Dienste korrekt berechnet +- ✅ Feiertag + Vortag-Kombination + +**Spezialfälle:** +- ✅ Keine Dienste → 0€ +- ✅ 2x halbe Samstage zählen als 1 ganzer Tag + +### 4. Storage (Datenverwaltung) +- ✅ Mitarbeiter hinzufügen +- ✅ Doppelte Mitarbeiter werden abgelehnt +- ✅ Mitarbeiter entfernen +- ✅ Dienste hinzufügen und abrufen +- ✅ Dienste aktualisieren (gleicher Tag) +- ✅ Mehrere Mitarbeiter verwalten +- ✅ Export und Import von Daten + +### 5. Edge Cases +- ✅ Rundungsfehler bei Schwellenwert +- ✅ Performance bei vielen Diensten (30+ Tage) +- ✅ Schaltjahre (29. Februar) + +## Test-Statistiken + +Nach dem Durchlauf sehen Sie: +- **Gesamt**: Anzahl aller Tests +- **Bestanden**: Anzahl erfolgreicher Tests +- **Fehlgeschlagen**: Anzahl fehlgeschlagener Tests + +## Testfälle im Detail + +### Beispiel 1: Schwellenwert genau erreicht +```javascript +Dienste: +- 1× Samstag (1.0) +- 1× Sonntag (1.0) + +Erwartung: +- Qualifizierende Tage: 2.0 +- Schwellenwert: ✅ Erreicht +- Abzug: -1.0 +- Bezahlt: 1.0 × 450€ = 450€ +``` + +### Beispiel 2: Gemischte Dienste +```javascript +Dienste: +- 2× Montag (2.0 normale Tage) +- 2× Samstag (2.0 qualifizierende Tage) + +Erwartung: +- Normale Tage: 2.0 × 250€ = 500€ +- Qualifizierende Tage: (2.0 - 1.0) × 450€ = 450€ +- Gesamt: 950€ +``` + +### Beispiel 3: Halbe Dienste +```javascript +Dienste: +- 1× Montag halber Dienst (0.5) +- 1× Samstag halber Dienst (0.5) +- 1× Sonntag ganzer Dienst (1.0) +- 1× Freitag ganzer Dienst (1.0) + +Erwartung: +- Normale Tage: 0.5 × 250€ = 125€ +- Qualifizierende Tage: (2.5 - 1.0) × 450€ = 675€ +- Gesamt: 800€ +``` + +## Tests erweitern + +Um einen neuen Test hinzuzufügen, bearbeiten Sie `test-suite.js`: + +```javascript +runner.test('Testname', (t) => { + // Setup + const calculator = new BonusCalculator(new HolidayProvider()); + + const duties = [ + { date: new Date('2025-11-22T12:00:00'), share: 1.0 } + ]; + + // Ausführung + const result = calculator.calculateMonthlyBonus(duties); + + // Assertions + t.assertEqual(result.totalBonus, 0, 'Erwarteter Bonus'); + t.assertTrue(result.thresholdReached, 'Schwelle erreicht'); +}); +``` + +### Verfügbare Assertions + +- `assertEqual(actual, expected, message)` - Exakte Gleichheit +- `assertAlmostEqual(actual, expected, tolerance, message)` - Ungefähre Gleichheit (für Fließkommazahlen) +- `assertTrue(value, message)` - Wert sollte true sein +- `assertFalse(value, message)` - Wert sollte false sein + +## Troubleshooting + +### Tests schlagen fehl +1. Prüfen Sie die Fehlermeldung (wird rot angezeigt) +2. Überprüfen Sie die erwarteten vs. erhaltenen Werte +3. Testen Sie die Funktion manuell in der Haupt-App + +### Performance-Probleme +- Die Test Suite sollte in < 1 Sekunde durchlaufen +- Bei Verzögerungen: Browser-Konsole prüfen (F12) + +### LocalStorage-Konflikte +- Tests verwenden die gleiche LocalStorage-Instanz wie die Haupt-App +- Bei Problemen: LocalStorage im Browser löschen +- Oder: Tests in Inkognito-Modus ausführen + +## Continuous Integration + +Die Tests können auch automatisiert mit Headless-Browsern ausgeführt werden: + +```bash +# Mit Playwright +npx playwright test + +# Mit Puppeteer +node run-tests-headless.js +``` + +(Erfordert zusätzliche Setup-Schritte) + +## Test-Abdeckung + +Aktuelle Abdeckung: +- **Feiertage**: 100% (alle NRW-Feiertage getestet) +- **Tag-Klassifizierung**: 100% (alle Wochentage + Feiertage) +- **Bonusberechnung**: ~95% (Hauptszenarien + Edge Cases) +- **Storage**: ~90% (CRUD-Operationen) +- **UI**: 0% (keine UI-Tests, nur Logik) + +## Bekannte Limitierungen + +1. **Keine UI-Tests**: Nur Logik-Tests, keine Interaktions-Tests +2. **Browser-abhängig**: LocalStorage-Tests funktionieren nur im Browser +3. **Keine Netzwerk-Tests**: Kein Server-seitiger Code +4. **Zeitzone**: Tests gehen von deutscher Zeitzone aus + +## Best Practices + +1. **Tests vor Änderungen ausführen**: Sicherstellen, dass alles funktioniert +2. **Nach Änderungen erneut testen**: Regression verhindern +3. **Neue Features = Neue Tests**: Test-first development +4. **Tests dokumentieren**: Klare Namen und Kommentare + +## Lizenz + +MIT (wie Hauptprojekt) diff --git a/webapp/test-suite.js b/webapp/test-suite.js new file mode 100644 index 0000000..4fff30b --- /dev/null +++ b/webapp/test-suite.js @@ -0,0 +1,565 @@ +/** + * Test Suite for Dienstplan Bonusrechner + */ + +class TestRunner { + constructor() { + this.tests = []; + this.passed = 0; + this.failed = 0; + } + + /** + * Add a test case + */ + test(name, testFn) { + this.tests.push({ name, testFn }); + } + + /** + * Assert equality + */ + assertEqual(actual, expected, message = '') { + if (actual !== expected) { + throw new Error(`${message}\nErwartet: ${expected}\nErhalten: ${actual}`); + } + } + + /** + * Assert approximate equality (for floating point) + */ + assertAlmostEqual(actual, expected, tolerance = 0.01, message = '') { + if (Math.abs(actual - expected) > tolerance) { + throw new Error(`${message}\nErwartet: ${expected} (±${tolerance})\nErhalten: ${actual}`); + } + } + + /** + * Assert true + */ + assertTrue(value, message = '') { + if (!value) { + throw new Error(`${message}\nErwartet: true\nErhalten: ${value}`); + } + } + + /** + * Assert false + */ + assertFalse(value, message = '') { + if (value) { + throw new Error(`${message}\nErwartet: false\nErhalten: ${value}`); + } + } + + /** + * Run all tests + */ + async runAll() { + this.passed = 0; + this.failed = 0; + const results = []; + + for (const test of this.tests) { + try { + await test.testFn(this); + this.passed++; + results.push({ + name: test.name, + passed: true, + error: null + }); + } catch (error) { + this.failed++; + results.push({ + name: test.name, + passed: false, + error: error.message + }); + } + } + + return results; + } + + /** + * Get summary + */ + getSummary() { + return { + total: this.tests.length, + passed: this.passed, + failed: this.failed + }; + } +} + +// Create test runner instance +const runner = new TestRunner(); + +// ============================================================================ +// Holiday Provider Tests +// ============================================================================ + +runner.test('HolidayProvider: Neujahr 2025 wird erkannt', (t) => { + const holidays = new HolidayProvider(); + const date = new Date('2025-01-01T12:00:00'); + t.assertTrue(holidays.isHoliday(date), 'Neujahr sollte als Feiertag erkannt werden'); +}); + +runner.test('HolidayProvider: Normaler Tag wird nicht als Feiertag erkannt', (t) => { + const holidays = new HolidayProvider(); + const date = new Date('2025-01-15T12:00:00'); // Mittwoch + t.assertFalse(holidays.isHoliday(date), 'Normaler Tag sollte nicht als Feiertag erkannt werden'); +}); + +runner.test('HolidayProvider: Tag vor Feiertag wird erkannt', (t) => { + const holidays = new HolidayProvider(); + const date = new Date('2024-12-31T12:00:00'); // Tag vor Neujahr + t.assertTrue(holidays.isDayBeforeHoliday(date), 'Tag vor Feiertag sollte erkannt werden'); +}); + +runner.test('HolidayProvider: Fronleichnam 2025 korrekt', (t) => { + const holidays = new HolidayProvider(); + const date = new Date('2025-06-19T12:00:00'); + t.assertTrue(holidays.isHoliday(date), 'Fronleichnam sollte als Feiertag erkannt werden'); + t.assertEqual(holidays.getHolidayName(date), 'Fronleichnam', 'Feiertagsname sollte korrekt sein'); +}); + +// ============================================================================ +// Calculator Tests - Day Classification +// ============================================================================ + +runner.test('Calculator: Freitag ist qualifizierender Tag', (t) => { + const holidays = new HolidayProvider(); + const calculator = new BonusCalculator(holidays); + const friday = new Date('2025-11-21T12:00:00'); // Freitag + t.assertTrue(calculator.isQualifyingDay(friday), 'Freitag sollte qualifizierend sein'); +}); + +runner.test('Calculator: Samstag ist qualifizierender Tag', (t) => { + const holidays = new HolidayProvider(); + const calculator = new BonusCalculator(holidays); + const saturday = new Date('2025-11-22T12:00:00'); // Samstag + t.assertTrue(calculator.isQualifyingDay(saturday), 'Samstag sollte qualifizierend sein'); +}); + +runner.test('Calculator: Sonntag ist qualifizierender Tag', (t) => { + const holidays = new HolidayProvider(); + const calculator = new BonusCalculator(holidays); + const sunday = new Date('2025-11-23T12:00:00'); // Sonntag + t.assertTrue(calculator.isQualifyingDay(sunday), 'Sonntag sollte qualifizierend sein'); +}); + +runner.test('Calculator: Montag ist kein qualifizierender Tag', (t) => { + const holidays = new HolidayProvider(); + const calculator = new BonusCalculator(holidays); + const monday = new Date('2025-11-24T12:00:00'); // Montag (kein Feiertag) + t.assertFalse(calculator.isQualifyingDay(monday), 'Normaler Montag sollte nicht qualifizierend sein'); +}); + +runner.test('Calculator: Feiertag ist qualifizierender Tag', (t) => { + const holidays = new HolidayProvider(); + const calculator = new BonusCalculator(holidays); + const holiday = new Date('2025-05-01T12:00:00'); // Tag der Arbeit + t.assertTrue(calculator.isQualifyingDay(holiday), 'Feiertag sollte qualifizierend sein'); +}); + +runner.test('Calculator: Tag vor Feiertag ist qualifizierender Tag', (t) => { + const holidays = new HolidayProvider(); + const calculator = new BonusCalculator(holidays); + const dayBefore = new Date('2025-04-30T12:00:00'); // Tag vor 1. Mai + t.assertTrue(calculator.isQualifyingDay(dayBefore), 'Tag vor Feiertag sollte qualifizierend sein'); +}); + +// ============================================================================ +// Calculator Tests - Bonus Calculation +// ============================================================================ + +runner.test('Berechnung: Unter Schwellenwert (1.0 WE-Tag) = 0€', (t) => { + const holidays = new HolidayProvider(); + const calculator = new BonusCalculator(holidays); + + const duties = [ + { date: new Date('2025-11-22T12:00:00'), share: 1.0 } // 1x Samstag + ]; + + const result = calculator.calculateMonthlyBonus(duties); + + t.assertEqual(result.qualifyingDays, 1.0, 'Sollte 1.0 qualifizierende Tage haben'); + t.assertFalse(result.thresholdReached, 'Schwellenwert sollte nicht erreicht sein'); + t.assertEqual(result.totalBonus, 0, 'Bonus sollte 0€ sein'); +}); + +runner.test('Berechnung: Genau 2.0 WE-Tage = 450€', (t) => { + const holidays = new HolidayProvider(); + const calculator = new BonusCalculator(holidays); + + const duties = [ + { date: new Date('2025-11-22T12:00:00'), share: 1.0 }, // Samstag + { date: new Date('2025-11-23T12:00:00'), share: 1.0 } // Sonntag + ]; + + const result = calculator.calculateMonthlyBonus(duties); + + t.assertEqual(result.qualifyingDays, 2.0, 'Sollte 2.0 qualifizierende Tage haben'); + t.assertTrue(result.thresholdReached, 'Schwellenwert sollte erreicht sein'); + t.assertEqual(result.qualifyingDaysDeducted, 1.0, 'Sollte 1.0 Tag abziehen'); + t.assertEqual(result.qualifyingDaysPaid, 1.0, 'Sollte 1.0 Tag bezahlen'); + t.assertEqual(result.totalBonus, 450, 'Bonus sollte 450€ sein'); +}); + +runner.test('Berechnung: 2x halbe WE-Dienste = 0€ (genau Schwelle, aber nach Abzug nichts)', (t) => { + const holidays = new HolidayProvider(); + const calculator = new BonusCalculator(holidays); + + const duties = [ + { date: new Date('2025-11-22T12:00:00'), share: 0.5 }, // Halber Samstag + { date: new Date('2025-11-22T12:00:00'), share: 0.5 }, // Halber Samstag + { date: new Date('2025-11-23T12:00:00'), share: 0.5 }, // Halber Sonntag + { date: new Date('2025-11-23T12:00:00'), share: 0.5 } // Halber Sonntag + ]; + + const result = calculator.calculateMonthlyBonus(duties); + + t.assertEqual(result.qualifyingDays, 2.0, 'Sollte 2.0 qualifizierende Tage haben (4×0.5)'); + t.assertTrue(result.thresholdReached, 'Schwellenwert sollte erreicht sein'); + t.assertEqual(result.qualifyingDaysPaid, 1.0, 'Sollte 1.0 Tag bezahlen nach Abzug'); + t.assertEqual(result.totalBonus, 450, 'Bonus sollte 450€ sein'); +}); + +runner.test('Berechnung: 3 WE-Tage = 900€', (t) => { + const holidays = new HolidayProvider(); + const calculator = new BonusCalculator(holidays); + + const duties = [ + { date: new Date('2025-11-21T12:00:00'), share: 1.0 }, // Freitag + { date: new Date('2025-11-22T12:00:00'), share: 1.0 }, // Samstag + { date: new Date('2025-11-23T12:00:00'), share: 1.0 } // Sonntag + ]; + + const result = calculator.calculateMonthlyBonus(duties); + + t.assertEqual(result.qualifyingDays, 3.0, 'Sollte 3.0 qualifizierende Tage haben'); + t.assertEqual(result.qualifyingDaysPaid, 2.0, 'Sollte 2.0 Tage bezahlen (3-1)'); + t.assertEqual(result.totalBonus, 900, 'Bonus sollte 900€ sein (2×450€)'); +}); + +runner.test('Berechnung: Normale Tage + WE-Tage gemischt', (t) => { + const holidays = new HolidayProvider(); + const calculator = new BonusCalculator(holidays); + + const duties = [ + { date: new Date('2025-11-24T12:00:00'), share: 1.0 }, // Montag (normal) + { date: new Date('2025-11-25T12:00:00'), share: 1.0 }, // Dienstag (normal) + { date: new Date('2025-11-22T12:00:00'), share: 1.0 }, // Samstag (qualifizierend) + { date: new Date('2025-11-23T12:00:00'), share: 1.0 } // Sonntag (qualifizierend) + ]; + + const result = calculator.calculateMonthlyBonus(duties); + + t.assertEqual(result.normalDays, 2.0, 'Sollte 2.0 normale Tage haben'); + t.assertEqual(result.qualifyingDays, 2.0, 'Sollte 2.0 qualifizierende Tage haben'); + t.assertEqual(result.normalDaysPaid, 2.0, 'Sollte 2.0 normale Tage bezahlen'); + t.assertEqual(result.qualifyingDaysPaid, 1.0, 'Sollte 1.0 qualifizierenden Tag bezahlen'); + t.assertEqual(result.bonusNormalDays, 500, 'Normale Tage: 2×250€ = 500€'); + t.assertEqual(result.bonusQualifyingDays, 450, 'WE-Tage: 1×450€ = 450€'); + t.assertEqual(result.totalBonus, 950, 'Gesamt: 950€'); +}); + +runner.test('Berechnung: Halbe Dienste korrekt berechnet', (t) => { + const holidays = new HolidayProvider(); + const calculator = new BonusCalculator(holidays); + + const duties = [ + { date: new Date('2025-11-24T12:00:00'), share: 0.5 }, // Halber Montag + { date: new Date('2025-11-22T12:00:00'), share: 0.5 }, // Halber Samstag + { date: new Date('2025-11-23T12:00:00'), share: 1.0 }, // Ganzer Sonntag + { date: new Date('2025-11-21T12:00:00'), share: 1.0 } // Ganzer Freitag + ]; + + const result = calculator.calculateMonthlyBonus(duties); + + t.assertEqual(result.normalDays, 0.5, 'Sollte 0.5 normale Tage haben'); + t.assertEqual(result.qualifyingDays, 2.5, 'Sollte 2.5 qualifizierende Tage haben'); + t.assertEqual(result.qualifyingDaysPaid, 1.5, 'Sollte 1.5 qualifizierende Tage bezahlen'); + t.assertEqual(result.bonusNormalDays, 125, 'Normale Tage: 0.5×250€ = 125€'); + t.assertEqual(result.bonusQualifyingDays, 675, 'WE-Tage: 1.5×450€ = 675€'); + t.assertEqual(result.totalBonus, 800, 'Gesamt: 800€'); +}); + +runner.test('Berechnung: Feiertag + Vortag', (t) => { + const holidays = new HolidayProvider(); + const calculator = new BonusCalculator(holidays); + + const duties = [ + { date: new Date('2025-04-30T12:00:00'), share: 1.0 }, // Mittwoch vor 1. Mai (qualifizierend) + { date: new Date('2025-05-01T12:00:00'), share: 1.0 } // 1. Mai (Feiertag, qualifizierend) + ]; + + const result = calculator.calculateMonthlyBonus(duties); + + t.assertEqual(result.qualifyingDays, 2.0, 'Sollte 2.0 qualifizierende Tage haben'); + t.assertTrue(result.thresholdReached, 'Schwellenwert sollte erreicht sein'); + t.assertEqual(result.totalBonus, 450, 'Bonus sollte 450€ sein'); +}); + +runner.test('Berechnung: Keine Dienste = 0€', (t) => { + const holidays = new HolidayProvider(); + const calculator = new BonusCalculator(holidays); + + const result = calculator.calculateMonthlyBonus([]); + + t.assertEqual(result.totalDuties, 0, 'Sollte 0 Dienste haben'); + t.assertEqual(result.totalBonus, 0, 'Bonus sollte 0€ sein'); +}); + +// ============================================================================ +// Storage Tests +// ============================================================================ + +runner.test('Storage: Mitarbeiter hinzufügen', (t) => { + const storage = new DataStorage(); + storage.clearAll(); + + const success = storage.addEmployee('Max Mustermann'); + t.assertTrue(success, 'Mitarbeiter sollte hinzugefügt werden'); + + const employees = storage.getEmployees(); + t.assertEqual(employees.length, 1, 'Sollte 1 Mitarbeiter haben'); + t.assertTrue(employees.includes('Max Mustermann'), 'Mitarbeiter sollte in Liste sein'); +}); + +runner.test('Storage: Doppelter Mitarbeiter wird abgelehnt', (t) => { + const storage = new DataStorage(); + storage.clearAll(); + + storage.addEmployee('Max Mustermann'); + const success = storage.addEmployee('Max Mustermann'); + + t.assertFalse(success, 'Doppelter Mitarbeiter sollte abgelehnt werden'); + + const employees = storage.getEmployees(); + t.assertEqual(employees.length, 1, 'Sollte nur 1 Mitarbeiter haben'); +}); + +runner.test('Storage: Mitarbeiter entfernen', (t) => { + const storage = new DataStorage(); + storage.clearAll(); + + storage.addEmployee('Max Mustermann'); + storage.removeEmployee('Max Mustermann'); + + const employees = storage.getEmployees(); + t.assertEqual(employees.length, 0, 'Sollte 0 Mitarbeiter haben'); +}); + +runner.test('Storage: Dienst hinzufügen und abrufen', (t) => { + const storage = new DataStorage(); + storage.clearAll(); + + storage.addEmployee('Max Mustermann'); + const date = new Date('2025-11-22T12:00:00'); + storage.addDuty('Max Mustermann', 2025, 11, date, 1.0); + + const duties = storage.getDutiesForMonth('Max Mustermann', 2025, 11); + t.assertEqual(duties.length, 1, 'Sollte 1 Dienst haben'); + t.assertEqual(duties[0].share, 1.0, 'Dienst sollte share 1.0 haben'); +}); + +runner.test('Storage: Dienst aktualisieren (gleicher Tag)', (t) => { + const storage = new DataStorage(); + storage.clearAll(); + + storage.addEmployee('Max Mustermann'); + const date = new Date('2025-11-22T12:00:00'); + + storage.addDuty('Max Mustermann', 2025, 11, date, 1.0); + storage.addDuty('Max Mustermann', 2025, 11, date, 0.5); // Update + + const duties = storage.getDutiesForMonth('Max Mustermann', 2025, 11); + t.assertEqual(duties.length, 1, 'Sollte nur 1 Dienst haben (aktualisiert)'); + t.assertEqual(duties[0].share, 0.5, 'Share sollte aktualisiert sein'); +}); + +runner.test('Storage: Mehrere Mitarbeiter', (t) => { + const storage = new DataStorage(); + storage.clearAll(); + + storage.addEmployee('Max Mustermann'); + storage.addEmployee('Anna Schmidt'); + storage.addEmployee('Peter Müller'); + + const employees = storage.getEmployees(); + t.assertEqual(employees.length, 3, 'Sollte 3 Mitarbeiter haben'); + t.assertTrue(employees.includes('Anna Schmidt'), 'Anna Schmidt sollte vorhanden sein'); +}); + +runner.test('Storage: Export und Import', (t) => { + const storage1 = new DataStorage(); + storage1.clearAll(); + + storage1.addEmployee('Max Mustermann'); + const date = new Date('2025-11-22T12:00:00'); + storage1.addDuty('Max Mustermann', 2025, 11, date, 1.0); + + const exported = storage1.exportData(); + + const storage2 = new DataStorage(); + storage2.clearAll(); + const success = storage2.importData(exported); + + t.assertTrue(success, 'Import sollte erfolgreich sein'); + + const employees = storage2.getEmployees(); + t.assertEqual(employees.length, 1, 'Sollte 1 Mitarbeiter haben'); + + const duties = storage2.getDutiesForMonth('Max Mustermann', 2025, 11); + t.assertEqual(duties.length, 1, 'Sollte 1 Dienst haben'); +}); + +// ============================================================================ +// Edge Cases & Regression Tests +// ============================================================================ + +runner.test('Edge Case: Exakt Schwellenwert mit Rundungsfehler (1.9999)', (t) => { + const holidays = new HolidayProvider(); + const calculator = new BonusCalculator(holidays); + + // Simuliere Rundungsfehler + const duties = [ + { date: new Date('2025-11-22T12:00:00'), share: 0.66666 }, + { date: new Date('2025-11-23T12:00:00'), share: 0.66666 }, + { date: new Date('2025-11-21T12:00:00'), share: 0.66666 } + ]; + + const result = calculator.calculateMonthlyBonus(duties); + + // 0.66666 × 3 ≈ 1.99998, sollte als >= 2.0 gelten + t.assertTrue(result.thresholdReached || result.qualifyingDays < 2.0, + 'Sollte Rundung korrekt handhaben'); +}); + +runner.test('Edge Case: Sehr viele Dienste (Performance)', (t) => { + const holidays = new HolidayProvider(); + const calculator = new BonusCalculator(holidays); + + const duties = []; + for (let i = 1; i <= 30; i++) { + duties.push({ + date: new Date(`2025-11-${String(i).padStart(2, '0')}T12:00:00`), + share: i % 2 === 0 ? 1.0 : 0.5 + }); + } + + const start = Date.now(); + const result = calculator.calculateMonthlyBonus(duties); + const duration = Date.now() - start; + + t.assertTrue(duration < 100, `Berechnung sollte schnell sein (${duration}ms)`); + t.assertTrue(result.totalBonus > 0, 'Sollte Bonus berechnen'); +}); + +runner.test('Edge Case: Dienst am 29. Februar (Schaltjahr)', (t) => { + const holidays = new HolidayProvider(); + const calculator = new BonusCalculator(holidays); + + const duties = [ + { date: new Date('2028-02-29T12:00:00'), share: 1.0 } // Dienstag (nicht qualifizierend) + ]; + + // Sollte nicht crashen + const result = calculator.calculateMonthlyBonus(duties); + t.assertEqual(result.normalDays, 1.0, 'Sollte normalen Tag erkennen'); +}); + +// ============================================================================ +// Display Functions +// ============================================================================ + +async function runAllTests() { + const resultsContainer = document.getElementById('test-results'); + const summaryDiv = document.getElementById('summary'); + const runButton = document.getElementById('run-tests'); + + // Clear previous results + resultsContainer.innerHTML = '
Tests laufen...
'; + runButton.disabled = true; + + // Run tests + const results = await runner.runAll(); + const summary = runner.getSummary(); + + // Update summary + document.getElementById('total-tests').textContent = summary.total; + document.getElementById('passed-tests').textContent = summary.passed; + document.getElementById('failed-tests').textContent = summary.failed; + summaryDiv.style.display = 'flex'; + + // Display results + resultsContainer.innerHTML = ''; + + // Group by category + const categories = { + 'Holiday Provider': [], + 'Calculator - Tag-Klassifizierung': [], + 'Calculator - Bonusberechnung': [], + 'Storage': [], + 'Edge Cases': [] + }; + + results.forEach(result => { + if (result.name.includes('HolidayProvider')) { + categories['Holiday Provider'].push(result); + } else if (result.name.includes('qualifizierender Tag') || result.name.includes('Feiertag ist')) { + categories['Calculator - Tag-Klassifizierung'].push(result); + } else if (result.name.includes('Berechnung:')) { + categories['Calculator - Bonusberechnung'].push(result); + } else if (result.name.includes('Storage:')) { + categories['Storage'].push(result); + } else if (result.name.includes('Edge Case:')) { + categories['Edge Cases'].push(result); + } + }); + + // Render categories + for (const [category, tests] of Object.entries(categories)) { + if (tests.length === 0) continue; + + const suiteDiv = document.createElement('div'); + suiteDiv.className = 'test-suite'; + + const title = document.createElement('h2'); + title.textContent = `${category} (${tests.filter(t => t.passed).length}/${tests.length})`; + suiteDiv.appendChild(title); + + tests.forEach(result => { + const testDiv = document.createElement('div'); + testDiv.className = `test-case ${result.passed ? 'pass' : 'fail'}`; + + const nameDiv = document.createElement('div'); + nameDiv.className = 'test-name'; + nameDiv.textContent = `${result.passed ? '✅' : '❌'} ${result.name}`; + testDiv.appendChild(nameDiv); + + if (!result.passed && result.error) { + const errorDiv = document.createElement('div'); + errorDiv.className = 'error-details'; + errorDiv.textContent = result.error; + testDiv.appendChild(errorDiv); + } + + suiteDiv.appendChild(testDiv); + }); + + resultsContainer.appendChild(suiteDiv); + } + + runButton.disabled = false; + + // Scroll to summary + summaryDiv.scrollIntoView({ behavior: 'smooth' }); +} + +// Auto-run on load (optional) +// window.addEventListener('load', runAllTests); diff --git a/webapp/test.html b/webapp/test.html new file mode 100644 index 0000000..763e9de --- /dev/null +++ b/webapp/test.html @@ -0,0 +1,147 @@ + + + + + +