fix(test): override FlutterError.onError after app.main() to fix E2E hang

app.main() synchronously sets FlutterError.onError to its crash-screen
handler, overwriting the filter the test had registered first. The test
binding's _runTest finally-block checks FlutterError.onError != _recordError
and fires assertion '_pendingExceptionDetails != null', which prevents the
integration test framework from calling exit() — causing the process to hang
for the full 360-second timeout.

Fix: capture the binding's error recorder (bindingError) before app.main(),
call app.main() first, then install the DEFUNCT/DISPOSED filter pointing at
bindingError, and restore to bindingError in teardown. This keeps the crash
handler from interfering with the test binding's error tracking.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Thomas SharedInbox
2026-05-14 22:19:11 +02:00
co-authored by Claude Sonnet 4.6
parent cc108b4788
commit a4cbe35b0f
+18 -11
View File
@@ -130,17 +130,12 @@ void main() {
addTearDown(tester.view.resetPhysicalSize); addTearDown(tester.view.resetPhysicalSize);
addTearDown(tester.view.resetDevicePixelRatio); addTearDown(tester.view.resetDevicePixelRatio);
// On Android, the keyboard-dismiss / window-resize cycle can trigger // Capture the test binding's error recorder BEFORE app.main() so we can
// one final layout pass on already-disposed render objects (DEFUNCT). // restore it in teardown. app.main() sets its own FlutterError.onError
// These spurious overflow errors have no effect on real functionality; // (crash-screen handler); we must override it AFTER the call so our
// filter them so they don't fail the test. // filter takes precedence, yet teardown still restores to the binding's
final prevError = FlutterError.onError; // recorder (not to the crash handler).
FlutterError.onError = (details) { final bindingError = FlutterError.onError;
final msg = details.toString();
if (msg.contains('DEFUNCT') || msg.contains('DISPOSED')) return;
prevError?.call(details);
};
addTearDown(() => FlutterError.onError = prevError);
_log('app start'); _log('app start');
app.main( app.main(
@@ -155,6 +150,18 @@ void main() {
accountConnectionStatusProvider.overrideWith((ref, _) async {}), accountConnectionStatusProvider.overrideWith((ref, _) async {}),
], ],
); );
// Override the crash handler that app.main() just installed with a
// filter that forwards non-spurious errors to the binding's recorder.
// On Android/Linux, keyboard-dismiss or teardown can produce
// DEFUNCT/DISPOSED layout errors; discard those silently.
FlutterError.onError = (details) {
final msg = details.toString();
if (msg.contains('DEFUNCT') || msg.contains('DISPOSED')) return;
bindingError?.call(details);
};
addTearDown(() => FlutterError.onError = bindingError);
await pumpUntil(tester, find.text('Welcome to SharedInbox')); await pumpUntil(tester, find.text('Welcome to SharedInbox'));
_log('app settled'); _log('app settled');