Merge pull request 'Fix #195 + #198: run-tests.sh-Commit-Gate und fehlender View-Import in suppliers/list.php' (#197) from fds2610-bot/Steuerkram:main into main

Reviewed-on: https://codeberg.org/fds2610/Steuerkram/pulls/197
This commit is contained in:
fds2610 2026-08-21 11:10:16 +02:00
commit 941f69b5e3
9 changed files with 237 additions and 44 deletions

View file

@ -57,8 +57,8 @@ Jede Testklasse erweitert `TestCase` und implementiert `run()`.
```bash ```bash
docker compose exec -u www-data web php tests/run.php # alle Tests ausführen docker compose exec -u www-data web php tests/run.php # alle Tests ausführen
./scripts/run-tests.sh # Tests + automatische Issue-Erstellung bei Fehlern docker compose exec -u www-data web ./scripts/run-tests.sh # Tests + automatische Issue-Erstellung bei Fehlern
./scripts/run-tests.sh 42 # Tests; bei Fehlern Kommentar in Issue #42 docker compose exec -u www-data web ./scripts/run-tests.sh 42 # Tests; bei Fehlern Kommentar in Issue #42
``` ```
**Immer `-u www-data`:** ohne die Option laufen die Tests als root und legen **Immer `-u www-data`:** ohne die Option laufen die Tests als root und legen

View file

@ -2,6 +2,18 @@
--- ---
## 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`).
- **`tests/run.php` / `tests/TestCase.php`:** `--json`-Modus gab bisher zusätzlich zur JSON-Ausgabe die komplette Fortschrittsanzeige (✓/✗ je Assertion) auf STDOUT aus – dadurch war die JSON-Ausgabe nie für sich allein parsbar, unabhängig vom Python-Problem. `TestCase::setQuiet()` unterdrückt diese Ausgabe im `--json`-Modus; PHP-Warnungen/-Notices einzelner Tests gehen in diesem Modus zusätzlich per `ini_set('display_errors', 'stderr')` nach STDERR statt STDOUT.
- **`CLAUDE.md`:** Aufrufzeilen für `./scripts/run-tests.sh` auf `docker compose exec -u www-data web ./scripts/run-tests.sh` korrigiert – das Skript läuft nur im Container, auf dem Host ist kein PHP installiert.
## 2026-08-21 – SMTP-SSRF-Schutz: IP-Pinning gegen DNS-Rebinding, Admin-Pflicht für SMTP-Einstellungen (#193) ## 2026-08-21 – SMTP-SSRF-Schutz: IP-Pinning gegen DNS-Rebinding, Admin-Pflicht für SMTP-Einstellungen (#193)
- **`src/Services/SsrfGuard.php`:** neue `resolvePinnedHost(string $host): string[]` – wie `assertPublicHost()`, gibt aber zusätzlich die geprüfte(n) IP-Adresse(n) zurück, analog zu `resolvePinned()` für https-URLs. `assertPublicHost()` ruft sie jetzt intern auf. - **`src/Services/SsrfGuard.php`:** neue `resolvePinnedHost(string $host): string[]` – wie `assertPublicHost()`, gibt aber zusätzlich die geprüfte(n) IP-Adresse(n) zurück, analog zu `resolvePinned()` für https-URLs. `assertPublicHost()` ruft sie jetzt intern auf.

View file

@ -3,8 +3,8 @@
# ============================================================ # ============================================================
# Steuerkram Test-Runner mit automatischer Issue-Erstellung # Steuerkram Test-Runner mit automatischer Issue-Erstellung
# #
# Verwendung: # Verwendung (im Container):
# ./scripts/run-tests.sh [ISSUE_NUMMER] # docker compose exec -u www-data web ./scripts/run-tests.sh [ISSUE_NUMMER]
# #
# Parameter: # Parameter:
# ISSUE_NUMMER (optional) Nummer des aktuell bearbeiteten Issues, # ISSUE_NUMMER (optional) Nummer des aktuell bearbeiteten Issues,
@ -13,7 +13,7 @@
# Umgebungsvariablen: # Umgebungsvariablen:
# CODEBERG_TOKEN Codeberg-API-Token (für Issue-Erstellung) # CODEBERG_TOKEN Codeberg-API-Token (für Issue-Erstellung)
# #
# Exit-Code: 0 = alle Tests OK, 1 = Fehler gefunden # Exit-Code: 0 = alle Tests OK, 1 = Fehler gefunden (oder Auswertung fehlgeschlagen)
# ============================================================ # ============================================================
set -euo pipefail set -euo pipefail
@ -39,22 +39,42 @@ fi
# ── Tests ausführen ────────────────────────────────────────── # ── Tests ausführen ──────────────────────────────────────────
echo "" echo ""
echo "Führe Tests aus..." echo "Führe Tests aus..."
JSON_OUTPUT=$($PHP tests/run.php --json 2>&1) || true # STDERR wird bewusst NICHT mitgefangen: tests/run.php schreibt im --json-Modus
EXIT_CODE=$? # etwaige PHP-Warnungen/-Notices dorthin, damit STDOUT ausschließlich das JSON
# enthält (siehe tests/run.php). STDERR landet direkt im Terminal.
JSON_OUTPUT=$($PHP tests/run.php --json) && EXIT_CODE=0 || EXIT_CODE=$?
# Auch die lesbare Ausgabe zeigen # Auch die lesbare Ausgabe zeigen
$PHP tests/run.php || true $PHP tests/run.php || true
# ── Ergebnis auswerten ─────────────────────────────────────── # ── Ergebnis auswerten ───────────────────────────────────────
FAILED=$(echo "$JSON_OUTPUT" | python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('failed',0))" 2>/dev/null || echo "0") if [ "$EXIT_CODE" -eq 0 ]; then
FAILURES_JSON=$(echo "$JSON_OUTPUT" | python3 -c "import json,sys; d=json.load(sys.stdin); print(json.dumps(d.get('failures',[])))" 2>/dev/null || echo "[]")
if [ "$FAILED" = "0" ] || [ "$FAILED" = "" ]; then
echo "" echo ""
echo "✓ Alle Tests bestanden. Commit kann fortgesetzt werden." echo "✓ Alle Tests bestanden. Commit kann fortgesetzt werden."
exit 0 exit 0
fi fi
# tests/run.php meldet Fehlschläge (oder einen sonstigen Fehler) über den
# Exit-Code. Die Details holen wir uns per PHP aus dem JSON – lässt sich die
# Ausgabe nicht auswerten, bricht das Skript hier ab (set -e), statt
# fälschlich Erfolg zu melden.
FAILED=$(echo "$JSON_OUTPUT" | php -r '
$d = json_decode(stream_get_contents(STDIN), true);
if (!is_array($d) || !array_key_exists("failed", $d)) {
fwrite(STDERR, "Ungültige JSON-Ausgabe von tests/run.php --json\n");
exit(1);
}
echo $d["failed"];
')
FAILURES_JSON=$(echo "$JSON_OUTPUT" | php -r '
$d = json_decode(stream_get_contents(STDIN), true);
if (!is_array($d) || !array_key_exists("failures", $d)) {
fwrite(STDERR, "Ungültige JSON-Ausgabe von tests/run.php --json\n");
exit(1);
}
echo json_encode($d["failures"]);
')
echo "" echo ""
echo "✗ ${FAILED} Test(s) fehlgeschlagen." echo "✗ ${FAILED} Test(s) fehlgeschlagen."
@ -63,7 +83,10 @@ if [ -z "${CODEBERG_TOKEN:-}" ]; then
echo "" echo ""
echo "Hinweis: CODEBERG_TOKEN nicht gesetzt – keine automatischen Issues erstellt." echo "Hinweis: CODEBERG_TOKEN nicht gesetzt – keine automatischen Issues erstellt."
echo "Fehlgeschlagene Tests:" echo "Fehlgeschlagene Tests:"
echo "$FAILURES_JSON" | python3 -c "import json,sys; [print(' •', f) for f in json.load(sys.stdin)]" 2>/dev/null || true echo "$FAILURES_JSON" | php -r '
$d = json_decode(stream_get_contents(STDIN), true);
foreach ((is_array($d) ? $d : []) as $f) { echo " • {$f}\n"; }
'
exit 1 exit 1
fi fi
@ -79,51 +102,83 @@ while IFS= read -r failure; do
# Titel kürzen (max. 100 Zeichen) # Titel kürzen (max. 100 Zeichen)
TITLE="[BUG] Test fehlgeschlagen: ${failure:0:80}" TITLE="[BUG] Test fehlgeschlagen: ${failure:0:80}"
# Body zusammenbauen # Body zusammenbauen (mit echten Zeilenumbrüchen, kein "\n"-Text)
BODY="## Automatisch erkannter Testfehler\n\n" BODY_LINES=(
BODY+="**Fehlermeldung:** \`${failure}\`\n\n" "## Automatisch erkannter Testfehler"
BODY+="**Erkannt am:** $(date '+%Y-%m-%d %H:%M:%S')\n\n" ""
BODY+="**Commit:** $(git rev-parse --short HEAD 2>/dev/null || echo 'unbekannt')\n\n" "**Fehlermeldung:** \`${failure}\`"
""
"**Erkannt am:** $(date '+%Y-%m-%d %H:%M:%S')"
""
"**Commit:** $(git rev-parse --short HEAD 2>/dev/null || echo 'unbekannt')"
""
)
if [ -n "$CURRENT_ISSUE" ]; then if [ -n "$CURRENT_ISSUE" ]; then
BODY+="Dieser Fehler wurde beim Bearbeiten von Issue #${CURRENT_ISSUE} entdeckt.\n\n" BODY_LINES+=("Dieser Fehler wurde beim Bearbeiten von Issue #${CURRENT_ISSUE} entdeckt." "")
fi fi
BODY_LINES+=(
"### Schritte zur Reproduktion"
"1. \`docker compose exec -u www-data web php tests/run.php\` ausführen"
"2. Fehler tritt im oben genannten Test auf"
""
"_Dieser Issue wurde automatisch durch den Test-Runner erstellt._"
"_Das Label 'automate' wurde bewusst NICHT gesetzt (Human-in-the-loop)._"
)
BODY=$(printf '%s\n' "${BODY_LINES[@]}")
BODY+="### Schritte zur Reproduktion\n" PAYLOAD=$(TITLE="$TITLE" BODY="$BODY" php -r '
BODY+="1. \`docker compose exec -u www-data web php tests/run.php\` ausführen\n" echo json_encode([
BODY+="2. Fehler tritt im oben genannten Test auf\n\n" "title" => getenv("TITLE"),
BODY+="_Dieser Issue wurde automatisch durch den Test-Runner erstellt._\n" "body" => getenv("BODY"),
BODY+="_Das Label 'automate' wurde bewusst NICHT gesetzt (Human-in-the-loop)._" "labels" => [],
]);
')
RESPONSE=$(curl -s -X POST \ RESPONSE=$(curl -s -X POST \
-H "Authorization: token $CODEBERG_TOKEN" \ -H "Authorization: token $CODEBERG_TOKEN" \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
-d "$(python3 -c "import json; print(json.dumps({'title': '${TITLE}', 'body': '${BODY}', 'labels': []}))")" \ -d "$PAYLOAD" \
"${API_BASE}/repos/${REPO}/issues") "${API_BASE}/repos/${REPO}/issues")
ISSUE_NR=$(echo "$RESPONSE" | python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('number','?'))" 2>/dev/null || echo "?") ISSUE_NR=$(echo "$RESPONSE" | php -r '
$d = json_decode(stream_get_contents(STDIN), true);
echo (is_array($d) && isset($d["number"])) ? $d["number"] : "?";
')
CREATED_ISSUES+=("#${ISSUE_NR}") CREATED_ISSUES+=("#${ISSUE_NR}")
echo " → Issue #${ISSUE_NR} erstellt: ${failure:0:60}..." echo " → Issue #${ISSUE_NR} erstellt: ${failure:0:60}..."
done < <(echo "$FAILURES_JSON" | python3 -c "import json,sys; [print(f) for f in json.load(sys.stdin)]" 2>/dev/null) done < <(echo "$FAILURES_JSON" | php -r '
$d = json_decode(stream_get_contents(STDIN), true);
foreach ((is_array($d) ? $d : []) as $f) { echo $f, "\n"; }
')
# ── Kommentar im aktuellen Issue hinterlassen ───────────────── # ── Kommentar im aktuellen Issue hinterlassen ─────────────────
if [ -n "$CURRENT_ISSUE" ] && [ ${#CREATED_ISSUES[@]} -gt 0 ]; then if [ -n "$CURRENT_ISSUE" ] && [ ${#CREATED_ISSUES[@]} -gt 0 ]; then
ISSUE_LINKS=$(printf '%s, ' "${CREATED_ISSUES[@]}") ISSUE_LINKS=$(printf '%s, ' "${CREATED_ISSUES[@]}")
ISSUE_LINKS="${ISSUE_LINKS%, }" ISSUE_LINKS="${ISSUE_LINKS%, }"
COMMENT="### Automatische Testläufe\n\n" COMMENT_LINES=(
COMMENT+="Beim Bearbeiten dieses Issues wurden **${FAILED} Test(s)** fehlgeschlagen.\n\n" "### Automatische Testläufe"
COMMENT+="Folgende Bug-Issues wurden automatisch erstellt (ohne 'automate'-Label – bitte manuell prüfen):\n\n" ""
"Beim Bearbeiten dieses Issues wurden **${FAILED} Test(s)** fehlgeschlagen."
""
"Folgende Bug-Issues wurden automatisch erstellt (ohne 'automate'-Label – bitte manuell prüfen):"
""
)
for issue in "${CREATED_ISSUES[@]}"; do for issue in "${CREATED_ISSUES[@]}"; do
COMMENT+="- ${issue}\n" COMMENT_LINES+=("- ${issue}")
done done
COMMENT+="\n_Commit: $(git rev-parse --short HEAD 2>/dev/null || echo 'unbekannt')_" COMMENT_LINES+=("" "_Commit: $(git rev-parse --short HEAD 2>/dev/null || echo 'unbekannt')_")
COMMENT=$(printf '%s\n' "${COMMENT_LINES[@]}")
PAYLOAD=$(COMMENT="$COMMENT" php -r '
echo json_encode(["body" => getenv("COMMENT")]);
')
curl -s -X POST \ curl -s -X POST \
-H "Authorization: token $CODEBERG_TOKEN" \ -H "Authorization: token $CODEBERG_TOKEN" \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
-d "$(python3 -c "import json; print(json.dumps({'body': '${COMMENT}'}))")" \ -d "$PAYLOAD" \
"${API_BASE}/repos/${REPO}/issues/${CURRENT_ISSUE}/comments" > /dev/null "${API_BASE}/repos/${REPO}/issues/${CURRENT_ISSUE}/comments" > /dev/null
echo "" echo ""

View file

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

@ -14,6 +14,14 @@ abstract class TestCase
private static int $totalFailed = 0; private static int $totalFailed = 0;
private static array $allFailures = []; private static array $allFailures = [];
/** Unterdrückt die Fortschrittsausgabe (z. B. im --json-Modus von tests/run.php). */
private static bool $quiet = false;
public static function setQuiet(bool $quiet): void
{
self::$quiet = $quiet;
}
protected int $passed = 0; protected int $passed = 0;
protected int $failed = 0; protected int $failed = 0;
protected array $failures = []; protected array $failures = [];
@ -95,6 +103,8 @@ abstract class TestCase
continue; continue;
} }
if (!self::isInsideCleanupRoot($path)) { if (!self::isInsideCleanupRoot($path)) {
// Nicht durch $quiet unterdrückt: Test 79 prüft diese Meldung gezielt
// über ob_start()/ob_get_clean() ihrer eigenen TestCase-Instanz.
echo " \e[33m!\e[0m Nicht aufgeräumt (außerhalb der Testverzeichnisse): {$path}\n"; echo " \e[33m!\e[0m Nicht aufgeräumt (außerhalb der Testverzeichnisse): {$path}\n";
continue; continue;
} }
@ -145,13 +155,17 @@ abstract class TestCase
{ {
if ($condition) { if ($condition) {
$this->passed++; $this->passed++;
if (!self::$quiet) {
echo " \e[32m✓\e[0m {$message}\n"; echo " \e[32m✓\e[0m {$message}\n";
}
} else { } else {
$this->failed++; $this->failed++;
$this->failures[] = "[{$this->testName}] {$message}"; $this->failures[] = "[{$this->testName}] {$message}";
if (!self::$quiet) {
echo " \e[31m✗\e[0m {$message}\n"; echo " \e[31m✗\e[0m {$message}\n";
} }
} }
}
protected function assertEquals(mixed $expected, mixed $actual, string $message): void protected function assertEquals(mixed $expected, mixed $actual, string $message): void
{ {
@ -185,7 +199,9 @@ abstract class TestCase
public function execute(): array public function execute(): array
{ {
if (!self::$quiet) {
echo "\n\e[1m{$this->testName}\e[0m\n"; echo "\n\e[1m{$this->testName}\e[0m\n";
}
$this->setUp(); $this->setUp();
try { try {
$this->run(); $this->run();
@ -193,15 +209,19 @@ abstract class TestCase
$this->failed++; $this->failed++;
$msg = "[{$this->testName}] Ausnahme: " . $e->getMessage() . ' in ' . $e->getFile() . ':' . $e->getLine(); $msg = "[{$this->testName}] Ausnahme: " . $e->getMessage() . ' in ' . $e->getFile() . ':' . $e->getLine();
$this->failures[] = $msg; $this->failures[] = $msg;
if (!self::$quiet) {
echo " \e[31m✗\e[0m Unerwartete Ausnahme: " . $e->getMessage() . "\n"; echo " \e[31m✗\e[0m Unerwartete Ausnahme: " . $e->getMessage() . "\n";
}
} finally { } finally {
$this->tearDown(); $this->tearDown();
} }
if (!self::$quiet) {
$status = $this->failed === 0 $status = $this->failed === 0
? "\e[32mBestanden ({$this->passed}/{$this->passed})\e[0m" ? "\e[32mBestanden ({$this->passed}/{$this->passed})\e[0m"
: "\e[31m{$this->failed} Fehler, {$this->passed} OK\e[0m"; : "\e[31m{$this->failed} Fehler, {$this->passed} OK\e[0m";
echo " → {$status}\n"; echo " → {$status}\n";
}
self::$totalPassed += $this->passed; self::$totalPassed += $this->passed;
self::$totalFailed += $this->failed; self::$totalFailed += $this->failed;

View file

@ -28,7 +28,7 @@ async function entwurfAnlegen(page, { tag, einzelpreis = '100.00', ustSatz = '19
await page.fill('#betreff', tag) await page.fill('#betreff', tag)
// Position über den "+"-Button und das Popup hinzufügen. // 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') await page.click('#add-line-popup >> text=Neue Position')
const zeile = page.locator('#invoice-lines-body tr').last() const zeile = page.locator('#invoice-lines-body tr').last()

View file

@ -28,6 +28,14 @@ require __DIR__ . '/TestCase.php';
\Steuerkram\Config::load(); \Steuerkram\Config::load();
$jsonMode = in_array('--json', $argv ?? [], true); $jsonMode = in_array('--json', $argv ?? [], true);
TestCase::setQuiet($jsonMode);
// PHP-Warnungen/-Notices einzelner Tests (z. B. absichtlich provozierte
// Fehlerfälle) dürfen die JSON-Ausgabe auf STDOUT nicht verunreinigen –
// scripts/run-tests.sh wertet sie sonst nicht mehr auswertbar aus.
if ($jsonMode) {
ini_set('display_errors', 'stderr');
}
/** /**
* Erfasst den Bestand unterhalb der Datenverzeichnisse. * Erfasst den Bestand unterhalb der Datenverzeichnisse.