Fix #198: Fehlender View-Import in suppliers/list.php, veralteter E2E-Selektor

This commit is contained in:
fds2610-bot 2026-08-21 10:32:59 +02:00
parent 68ce22894f
commit c6164cf823
5 changed files with 106 additions and 2 deletions

View file

@ -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`).

View file

@ -1,3 +1,5 @@
<?php use Steuerkram\View; ?>
<div class="toolbar">
<div class="toolbar-left">
</div>

View file

@ -1 +1 @@
1-260802
1-260821

View file

@ -0,0 +1,96 @@
<?php
declare(strict_types=1);
/**
* Test 99: Klassen-Importe in den Views (Fix für /suppliers, HTTP 500)
*
* Views werden per include eingebunden und laufen dadurch im globalen
* Namespace – das `use Steuerkram\View;` des einbindenden Controllers gilt
* dort nicht. Fehlt der Import in der View selbst, wird `View::formatDate()`
* zu `\View::formatDate()` und die Seite stirbt mit "Class View not found".
*
* Der Fehler bleibt so lange unsichtbar, wie die betroffene Zeile nicht
* ausgeführt wird (in suppliers/list.php stand sie in einer foreach-Schleife,
* schlug also erst mit dem ersten Lieferanten zu). Dieser Test prüft alle
* Views statisch, damit ein fehlender Import nicht erst im Betrieb auffällt.
*/
class ViewKlassenImportTest extends TestCase
{
protected string $testName = 'Test 99: Klassen-Importe in den Views';
public function run(): void
{
$viewDir = realpath(__DIR__ . '/../src/Views');
$this->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('/(?<![\\\\$\w>])([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;
}
}

View file

@ -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()