diff --git a/Changes.md b/Changes.md index a71f27f..8c00ded 100644 --- a/Changes.md +++ b/Changes.md @@ -2,6 +2,12 @@ --- +## 2026-08-21 – Lieferantenliste (/suppliers) lief in HTTP 500: fehlender View-Import (#198) + +- **`src/Views/suppliers/list.php`:** Die View rief `View::formatDate()` ohne `use Steuerkram\View;` auf. Views werden per `include` eingebunden und laufen dadurch im globalen Namespace – das `use` des einbindenden Controllers gilt dort nicht, der Aufruf ging also auf `\View::formatDate()` und die Seite starb mit `Class "View" not found`. Sichtbar wurde das erst mit dem ersten Lieferanten, weil die Zeile in der `foreach`-Schleife steht. Import ergänzt; alle übrigen Views hatten ihn bereits. +- **`tests/99_ViewKlassenImportTest.php`** (neu): prüft alle Views in `src/Views/` statisch – jeder unqualifizierte `Klasse::`-Aufruf muss entweder per `use` importiert sein oder eine global existierende Klasse (`DateTime`, `PDO`, …) meinen. Fängt fehlende Importe ab, bevor sie im Betrieb auffallen. +- **`tests/e2e/specs/rechnung-export.spec.js`:** klickte noch `button[onclick^="showAddLineDialog"]`; der Inline-Handler-Umbau aus #160 hat daraus `data-action="show-add-line-dialog"` gemacht – fünf E2E-Tests liefen deshalb in den Timeout. Selektor nachgezogen. + ## 2026-08-21 – run-tests.sh: Commit-Gate meldete fälschlich Erfolg bei fehlgeschlagenen Tests (#195) - **`scripts/run-tests.sh`:** Die Auswertung von `php tests/run.php --json` hing an `python3`, das weder im Container noch auf dem Host installiert ist – der Fallback `|| echo "0"` verwandelte jeden Parse-Fehler in „0 fehlgeschlagene Tests", das Commit-Gate griff also nie. Erfolg/Misserfolg hängt jetzt direkt am Exit-Code von `php tests/run.php --json`; Detailauswertung (`failed`, `failures`) läuft über `php -r` statt Python. Lässt sich die JSON-Ausgabe nicht auswerten, bricht das Skript mit einem Fehler ab statt fälschlich Erfolg zu melden. Der zuvor ungenutzte `EXIT_CODE` (verworfen durch ein voranstehendes `|| true`) wird jetzt tatsächlich ausgewertet. Die JSON-Payloads für die automatische Issue-Erstellung werden ebenfalls per `php -r` statt Python gebaut (Body-Text jetzt mit echten Zeilenumbrüchen statt literalem `\n`). diff --git a/src/Views/suppliers/list.php b/src/Views/suppliers/list.php index a301e1b..e72977d 100644 --- a/src/Views/suppliers/list.php +++ b/src/Views/suppliers/list.php @@ -1,3 +1,5 @@ + +
diff --git a/src/version.txt b/src/version.txt index eefae6e..e3d6081 100644 --- a/src/version.txt +++ b/src/version.txt @@ -1 +1 @@ -1-260802 +1-260821 diff --git a/tests/99_ViewKlassenImportTest.php b/tests/99_ViewKlassenImportTest.php new file mode 100644 index 0000000..9b9af4a --- /dev/null +++ b/tests/99_ViewKlassenImportTest.php @@ -0,0 +1,96 @@ +assert($viewDir !== false, 'src/Views/ existiert'); + + $dateien = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($viewDir)); + $geprueft = 0; + $fehler = []; + + foreach ($dateien as $datei) { + if (!$datei->isFile() || $datei->getExtension() !== 'php') { + continue; + } + + $inhalt = file_get_contents($datei->getPathname()); + $relativ = str_replace($viewDir . '/', '', $datei->getPathname()); + $geprueft++; + + foreach ($this->unaufgeloesteKlassen($inhalt) as $klasse) { + $fehler[] = "$relativ verwendet $klasse:: ohne Import"; + } + } + + $this->assertGreaterThan(20, $geprueft, 'Es wurden Views eingelesen'); + $this->assert( + $fehler === [], + 'Jede View importiert die Klassen, die sie statisch aufruft' + . ($fehler === [] ? '' : ': ' . implode('; ', $fehler)) + ); + } + + /** + * Liefert die in $inhalt statisch aufgerufenen Klassennamen, die weder + * global existieren noch per use-Statement importiert sind. + * + * @return string[] + */ + private function unaufgeloesteKlassen(string $inhalt): array + { + preg_match_all( + '/\buse\s+([A-Za-z0-9_\\\\]+)(?:\s+as\s+([A-Za-z0-9_]+))?\s*;/i', + $inhalt, + $useTreffer, + PREG_SET_ORDER + ); + + $importiert = []; + foreach ($useTreffer as $treffer) { + $teile = explode('\\', $treffer[1]); + $name = $treffer[2] ?? end($teile); + $importiert[$name] = true; + } + + // Aufrufe wie View::formatDate(), aber nicht \Steuerkram\View::, + // $obj::, self::/static::/parent:: + preg_match_all('/(?])([A-Z][A-Za-z0-9_]*)\s*::/', $inhalt, $aufrufe); + + $offen = []; + foreach (array_unique($aufrufe[1]) as $klasse) { + if (isset($importiert[$klasse])) { + continue; + } + // Global existierende Klassen (DateTime, PDO, …) sind unkritisch. + if (class_exists($klasse, false) || interface_exists($klasse, false) || enum_exists($klasse, false)) { + continue; + } + if (class_exists('\\' . $klasse) || interface_exists('\\' . $klasse)) { + continue; + } + $offen[] = $klasse; + } + + return $offen; + } +} diff --git a/tests/e2e/specs/rechnung-export.spec.js b/tests/e2e/specs/rechnung-export.spec.js index 2066c29..e35e9dd 100644 --- a/tests/e2e/specs/rechnung-export.spec.js +++ b/tests/e2e/specs/rechnung-export.spec.js @@ -28,7 +28,7 @@ async function entwurfAnlegen(page, { tag, einzelpreis = '100.00', ustSatz = '19 await page.fill('#betreff', tag) // Position über den "+"-Button und das Popup hinzufügen. - await page.click('button[onclick^="showAddLineDialog"]') + await page.click('button[data-action="show-add-line-dialog"]') await page.click('#add-line-popup >> text=Neue Position') const zeile = page.locator('#invoice-lines-body tr').last()