Files
tessera-ctl/.planning/quick/260921-ldf-wackeltest-fehler-melden-haekchen-bildsc/260921-ldf-SUMMARY.md
T
schalli a6181e2751 docs(quick-260921-ldf): Wackeltest als Produktfehler nachgewiesen und behoben
Zusammenfassung, Debug-Akte und Knowledge-Base-Eintrag zum Quick-Vorgang
260921-ldf.

Die zentrale Frage war, ob hinter dem Wackeltest aus CI-Lauf 395 ein
echter Nutzerfehler steckt. Sie ist gemessen beantwortet, nicht
geschaetzt: ein MutationObserver ueber jeden DOM-Commit zeigt den
Zustand "Vorschaubild sichtbar, Haekchen aus" bei JEDEM Oeffnen als
echten, festgeschriebenen DOM-Zustand, der ohne act() zwei volle
Makrotask-Runden haelt. Zwischen zwei Makrotasks darf der Browser
zeichnen - der Nutzer kann das also sehen. Was nicht erreichbar ist:
jemand schickt ab und das Bild fehlt, denn die Korrektur kommt binnen
Millisekunden.

Stabilitaet: 20 von 20 vollen Laeufen gruen, je 531 Tests. Die
Zusammenfassung ordnet das ehrlich ein - bei einer Ausgangsrate von
1:17 waeren 20 gruene Laeufe auch ohne Fix zu rund 30 Prozent zu
erwarten. Der tragende Beleg ist der deterministische: den falschen
Zwischenzustand gibt es nicht mehr, und Test 14/15 sind gegen den Stand
davor in fuenf von fuenf Laeufen rot.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TPPB4ApQxzSU1rwV2Ffj9J
2026-09-21 15:50:37 +02:00

13 KiB

phase, plan, subsystem, tags, requires, provides, affects, actuals, tech-stack, key-files, key-decisions, patterns-established, requirements-completed, duration, completed, status
phase plan subsystem tags requires provides affects actuals tech-stack key-files key-decisions patterns-established requirements-completed duration completed status
quick-260921-ldf 01 ui
react
vitest
flaky-test
bug-report
state-management
react-effects
phase provides
quick-260914-m97 Fehler-melden-Knopf mit Bildaufnahme VOR dem Dialog; Test 1 als Zusicherung dieser Reihenfolge
phase provides
quick-260918-gza Vier Herkunftsfelder in der Nutzlast (Test 12/13 derselben Datei)
Wackeltest aus CI-Lauf 395 ursaechlich beseitigt — als Produktfehler, nicht als Testfehler
Fehler-melden-Dialog zeigt Vorschaubild und Haekchen ab dem ERSTEN Commit stimmig, statt einen Commit spaeter
Erneutes Oeffnen nach einem Versand zeigt keinen alten Danke-Bildschirm und keinen alten Text mehr
Test 14/15: MutationObserver-Pruefung ueber JEDEN Commit statt einer Stichprobe am Ende — deterministisch rot vor dem Fix
Zwei Konstruktionsfehler im Fehler-melden-Test behoben (expect in der Attrappe, nicht zurueckgesetzte document.body-Groesse)
web-bug-report
web-test-hygiene
tokens tasks commits
11800 1 3
added patterns
Abgeleiteten Zustand beim RENDERN ableiten, nicht per useEffect nachziehen: passive Effekte laufen nach dem Commit, also schreibt React zwangslaeufig erst den falschen und dann den richtigen Zustand in den DOM. Muster hier: `const attach = screenshot !== null && (attachChoice ?? true)` — eine eigene Zustandsvariable haelt nur noch die bewusste Wahl des Nutzers, nicht den abgeleiteten Wert.
Dialoge nur einhaengen, solange sie offen sind (`{open && <Dialog ... />}`), statt sie dauerhaft eingehaengt zu lassen und `null` zurueckgeben zu lassen. Sonst laufen die useState-Startwerte genau einmal — zu einem Zeitpunkt, an dem die spaeteren Daten noch nicht da sind — und jedes weitere Oeffnen braucht einen zuruecksetzenden Effekt, der genau diese Luecke aufreisst.
Wackeltests mit einem MutationObserver ueber JEDEN DOM-Commit untersuchen statt mit einer Stichprobe am Ende. Der Unterschied zwischen 'der Nutzer sieht das nie' und 'das steht bei jedem Oeffnen im DOM' ist genau so zu messen und nicht anders zu erraten.
Wackelursache einkreisen, indem man die Beobachtungszeit kuenstlich verschiebt (ein setTimeout in der Attrappenkette) statt auf einen Zufallstreffer unter Last zu warten: aus 1:17 wird 4 von 4.
Kein expect() innerhalb einer Attrappe, die in einem await der Komponente laeuft — ein Fehler daraus wird von einem catch in der Produktionskette verschluckt und der Test faellt an ganz anderer Stelle mit irrefuehrender Meldung durch. Beobachtung festhalten, im Testkoerper pruefen.
Object.defineProperty auf document.body (scrollWidth/scrollHeight) ueberlebt cleanup() und damit die ganze Testdatei — in afterEach per Reflect.deleteProperty zuruecknehmen.
created modified
apps/web/src/components/bug-report/bug-report-dialog.tsx
apps/web/src/components/bug-report/bug-report-button.tsx
apps/web/src/components/bug-report/bug-report-button.test.tsx
Urteil Produktfehler, nicht Testfehler — und zwar gemessen, nicht geschaetzt: ein MutationObserver ueber jeden Commit zeigt den Zustand 'Vorschaubild sichtbar, Haekchen aus' bei JEDEM Oeffnen als echten, festgeschriebenen DOM-Zustand, nicht nur unter Last. Ohne act() haelt er zwei volle Makrotask-Runden.
Repariert wurde der Ursache-Code, nicht der Test. Test 1 prueft unveraendert dieselbe Zusicherung; kein retry, kein hoeheres Zeitlimit — die Ursache war nie blosse Zeit, sondern ein falscher Zustand, den es jetzt nicht mehr gibt.
Beide Teilursachen beseitigt, nicht nur eine. Die abgeleitete Ableitung allein wuerde die Abwahl des Nutzers ueber das Schliessen hinaus festhalten; das bedingte Einhaengen allein liesse den Fehler wiederkehren, falls Bild und Oeffnen je in getrennten Commits landeten. Erst zusammen ist die Zusicherung strukturell erzwungen.
Der zuruecksetzende useEffect entfaellt ersatzlos statt umgebaut zu werden — ein frischer Mount setzt Status, Text, Fehlerstand und Haekchen schon durch die useState-Startwerte zurueck.
Die geschluckte Ausnahme in captureScreenshot (catch liefert null) bleibt bewusst stehen: das Bild ist eine Beigabe, der Bericht geht auch ohne. Angepasst wurde stattdessen der Test, der sein expect in diese Kette gelegt hatte.
Bei einem Wackeltest zuerst fragen, ob der beobachtete Zwischenzustand ueberhaupt in den DOM geschrieben wird. Wird er es, ist es ein Produktfehler und der Test hat recht behalten — auch wenn die sichtbare Wirkung nur ein kurzes Flackern ist.
Ursache
Urteil
Reparatur
Nebenbefund-1
Nebenbefund-2
Stabilitaet
~1h (eine Sitzung) 2026-09-21 complete

Quick-Vorgang 260921-ldf: Wackeltest Fehler-melden-Haekchen Summary

Der Wackeltest hatte recht: der Fehler-melden-Dialog schrieb bei JEDEM Oeffnen zuerst den Zustand "Vorschaubild sichtbar, Haekchen aus" in den DOM und korrigierte ihn erst einen Commit spaeter — ein Produktfehler, kein Testfehler. Repariert ist der Ursache-Code; der Test prueft unveraendert dasselbe und wackelt nicht mehr.

Die Ursache in einem Satz

Der Dialog war dauerhaft eingehaengt, sodass useState(screenshot !== null) nur ein einziges Mal lief — beim allerersten Mount des Knopfs, als noch gar kein Bild da war — und der richtige Wert erst von einem useEffect nachgezogen wurde, der per Bauart NACH dem Commit laeuft.

Das Urteil: Produktfehler

Das war die eigentliche Frage, und sie ist gemessen worden statt geschaetzt. Ein MutationObserver ueber document.body protokolliert jeden einzelnen DOM-Commit waehrend des Oeffnens. Gegen den Stand vor dem Fix, mit einer Attrappe, die rein in Mikrotasks aufloest, also ohne jede kuenstliche Verzoegerung:

COMMIT dialog=false img=nein box=-
COMMIT dialog=false img=nein box=-
COMMIT dialog=true  img=ja   box=AUS   <- falsch, aber festgeschrieben
COMMIT dialog=true  img=ja   box=AN

Der falsche Zustand ist also kein Testartefakt und keine Frage der Last. Er entsteht bei jedem Oeffnen. Ein zweiter Versuch, diesmal mit einem rohen Klick ohne act() und einer Stichprobe pro Ereignisschleifen-Runde, zeigt, wie lange er haelt:

runde 1  bild=ja haekchen=AUS
runde 2  bild=ja haekchen=AUS
runde 3  bild=ja haekchen=AN

Zwei volle Makrotask-Runden. Zwischen zwei Makrotasks darf der Browser zeichnen — der Nutzer kann diesen Zustand also sehen. Damit ist die Kernfrage beantwortet: ja, ein echter Nutzer geraet in diesen Zustand.

Was dabei ehrlich dazugehoert: die Korrektur kommt binnen Millisekunden, lange bevor jemand "Senden" druecken kann. Der befuerchtete Fall — jemand sieht sein Bild, schickt ab, und das Bild fehlt — ist damit nicht erreichbar. Was bleibt, ist ein kurzes Flackern beim Oeffnen. Echt, sichtbar, aber ohne Datenverlust. Der Grund, es trotzdem im Produktcode zu reparieren statt im Test: der Test hat einen wirklich vorhandenen falschen Zustand gefunden, und wer ihn im Test wegberuhigt, laesst den Zustand stehen.

Nicht gemessen und deshalb hier auch nicht behauptet: ob der Browser den Zwischenschritt tatsaechlich in jedem Fall zeichnet. Belegt ist, dass er zwei Zeichengelegenheiten lang besteht.

Wie die Ursache gefunden wurde

Drei Schritte, jeder mit einem eigenen Messergebnis:

  1. Beobachtungszeit kuenstlich verschoben. Statt auf einen Zufallstreffer unter Last zu warten, bekam die toPng-Attrappe ein setTimeout in die Kette. Ergebnis: 4 von 4 Fehlschlaegen mit exakt der CI-Meldung, schon bei 0 ms. Damit war klar, dass die Last nur der Ausloeser ist und jede Makrotask-Grenze genuegt.
  2. Eine naheliegende Erklaerung widerlegt. Der Verdacht lag auf await import('html-to-image') in captureScreenshot. Gemessen: der Import loest auf, bevor ein zuvor gesetzter setTimeout(0) feuert — er ueberschreitet keine Makrotask-Grenze und ist nicht die Ursache.
  3. Jeden Commit protokolliert. Erst das zeigte, dass der falsche Zustand nicht gelegentlich unter Last entsteht, sondern immer.

Dabei fiel eine zweite Auspraegung derselben Ursache auf: beim erneuten Oeffnen nach einem Versand stand zwei Runden lang der alte Danke-Bildschirm im DOM, bevor das frische Formular erschien. Auch status, description und failedStatus wurden erst per Effekt zurueckgesetzt.

Die Reparatur

Zwei Teilursachen, beide beseitigt — einzeln reicht keine:

  • bug-report-button.tsx: Der Dialog wird nur noch eingehaengt, solange er offen ist. Jedes Oeffnen ist damit ein frischer Mount, und die useState-Startwerte gelten schon im ersten Commit. Der zuruecksetzende useEffect entfaellt ersatzlos — er hatte genau die Luecke aufgerissen, die er schliessen sollte.
  • bug-report-dialog.tsx: Das Haekchen wird beim Rendern abgeleitet statt nachgezogen: const attach = screenshot !== null && (attachChoice ?? true). attachChoice haelt nur noch die bewusste Abwahl des Nutzers.

Nach dem Fix zeigt dasselbe Commit-Protokoll den ersten Commit mit Dialog bereits als img=ja box=AN, und der rohe Klick ist schon in Runde 1 richtig. Es gibt keinen falschen Zwischenzustand mehr, den man beobachten koennte — deshalb kann der Test auch nicht mehr wackeln.

Der Nachweis: Test 14 und 15

Der alte Test 1 fand den Fehler nur durch Zufall — er las den DOM einmal, nachdem findByRole den Dialog gemeldet hatte, und traf mal den falschen ersten, meist den richtigen zweiten Commit. Ein Fehlschlag auf rund siebzehn volle Laeufe.

Die beiden neuen Tests pruefen stattdessen jeden Commit und dulden keinen einzigen mit sichtbarem Bild und ausgeschaltetem Haekchen. Test 14 deckt das erste Oeffnen ab, Test 15 das erneute Oeffnen nach einem Versand mit abgewaehltem Haekchen und getipptem Text.

Gegen den Stand vor dem Fix (8d604b8, Quellcode zurueckgesetzt, Tests behalten) sind beide in 5 von 5 Laeufen rot; mit dem Fix gruen. Kein retry, kein hoeheres Zeitlimit — beides waere hier auch falsch gewesen, weil die Ursache nie blosse Zeit war.

Test 1 bleibt in der Sache unveraendert und prueft weiterhin dieselbe Zusicherung. Die Pruefmenge ist gewachsen, nicht geschrumpft.

Die zwei Nebenbefunde

Beide waren nicht die Ursache, beide haetten die Suche in die Irre fuehren koennen. Beide sind erledigt (Commit c0ab5b5, vor dem Fix, unabhaengig davon gruen).

1. expect innerhalb der Attrappe. Test 1 pruefte mitten in der toPng-Attrappe, dass noch kein Dialog im DOM steht. Wirft dieses expect, landet der Fehler mitten im await von captureScreenshot, und dessen catch liefert still null zurueck. Der Test waere dann nicht an der geprueften Stelle durchgefallen, sondern viel spaeter mit der Meldung, es gebe kein Vorschaubild — eine Meldung, die in die voellig falsche Richtung zeigt. Die Attrappe haelt die Beobachtung jetzt nur fest, geprueft wird im Testkoerper. Bewiesen wird dasselbe.

2. document.body-Groesse ohne Ruecknahme. scrollWidth/scrollHeight wurden per Object.defineProperty auf 3200x1000 gesetzt und nie zurueckgenommen. Die eigene Eigenschaft verdeckt den Getter von Element.prototype, und document.body ueberlebt cleanup() — alle zwoelf folgenden Tests der Datei sahen weiterhin 3200x1000. stubBodyGroesse merkt sich das jetzt, afterEach nimmt es per Reflect.deleteProperty zurueck.

Commits

  1. Zwei Konstruktionsfehler im Fehler-melden-Test behoben — c0ab5b5 (test)
  2. Fehler-melden-Dialog stimmt ab dem ersten Commit, nicht erst einen spaeter — de7fdb7 (fix)
  3. Test 14/15 halten jeden Commit fest statt nur den Endzustand — 9f02fcc (test)

Pruefstand

  • pnpm lint: 5/5 erfolgreich, keine Fehlerstufe, 399 Warnungen — unveraendert, nicht gewachsen
  • pnpm type-check: 4/4 erfolgreich
  • apps/web: 73 Dateien / 531 Tests (529 + Test 14/15), keine Datei dazugekommen oder weggefallen
  • apps/api: 72 Dateien / 1143 Tests, unveraendert — nicht beruehrt
  • Stabilitaet: 20 von 20 vollen Laeufen pnpm --filter @tessera/web exec vitest run gruen, 0 Fehlschlaege, je 531 Tests (je ~31 s, gegen den committeten Endstand, nacheinander)

Dazu die ehrliche Einordnung: 20 saubere Laeufe sind fuer sich genommen kein starker Beleg. Bei der gemessenen Ausgangsrate von 1:17 waeren 20 gruene Laeufe auch ohne jede Reparatur noch mit rund 30 Prozent Wahrscheinlichkeit zu erwarten. Der eigentliche Beleg ist ein anderer und er ist deterministisch: der falsche Zwischenzustand existiert nicht mehr. Das Commit-Protokoll zeigt den ersten Commit mit Dialog bereits als img=ja box=AN, und Test 14/15 sind gegen den Stand davor in 5 von 5 Laeufen rot. Es gibt schlicht nichts mehr, was der Test zufaellig falsch antreffen koennte. Die 20 Laeufe bestaetigen das nur, sie tragen es nicht.

Was nicht angefasst wurde

  • Keine Versionsspruenge, keine neuen Abhaengigkeiten, kein repo-weites Umformatieren
  • STATE.md und ROADMAP.md unberuehrt (Sache des Orchestrators)
  • Nicht gepusht
  • Die geschluckte Ausnahme in captureScreenshot bleibt bewusst stehen — das Bild ist eine Beigabe, der Bericht geht auch ohne