From d9f88f314f2bf29c83f1e95b5aa93e867fb4a518 Mon Sep 17 00:00:00 2001 From: Greg Price Date: Thu, 19 Dec 2024 11:54:45 -0800 Subject: [PATCH] store: Add details to error on no PerAccountStoreWidget This draws from MediaQuery.of and debugCheckHasMediaQuery. Prompted by this question: https://chat.zulip.org/#narrow/channel/516-mobile-dev-help/topic/No.20PerAccountStoreWidget.20ancestor/near/2008484 --- lib/widgets/store.dart | 21 +++++++++++++++++++-- test/widgets/store_test.dart | 18 ++++++++++++++++++ 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/lib/widgets/store.dart b/lib/widgets/store.dart index 1d37bcfd17..13c27b9165 100644 --- a/lib/widgets/store.dart +++ b/lib/widgets/store.dart @@ -164,7 +164,7 @@ class PerAccountStoreWidget extends StatefulWidget { /// * [InheritedNotifier], which provides the "dependency" mechanism. static PerAccountStore of(BuildContext context) { final widget = context.dependOnInheritedWidgetOfExactType<_PerAccountStoreInheritedWidget>(); - assert(widget != null, 'No PerAccountStoreWidget ancestor'); + assert(_debugCheckFound(context, widget)); return widget!.store; } @@ -183,12 +183,29 @@ class PerAccountStoreWidget extends StatefulWidget { /// Like [of], the cost of this method is O(1) with a small constant factor. static int accountIdOf(BuildContext context) { final element = context.getElementForInheritedWidgetOfExactType<_PerAccountStoreInheritedWidget>(); - assert(element != null, 'No PerAccountStoreWidget ancestor'); + assert(_debugCheckFound(context, element)); final widget = element!.findAncestorWidgetOfExactType(); assert(widget != null); return widget!.accountId; } + static bool _debugCheckFound(BuildContext context, Object? ancestor) { + // Compare [debugCheckHasMediaQuery], and its caller [MediaQuery.of]. + assert(() { + if (ancestor != null) return true; + throw FlutterError.fromParts([ + ErrorSummary('No PerAccountStoreWidget ancestor found.'), + ErrorDescription('${context.widget.runtimeType} widgets require a PerAccountStoreWidget ancestor.'), + context.describeWidget('The specific widget that could not find a PerAccountStoreWidget ancestor was'), + context.describeOwnershipChain('The ownership chain for the affected widget is'), + ErrorHint('For a new page in the app, consider MaterialAccountWidgetRoute ' + 'or AccountPageRouteBuilder.'), + ErrorHint('In tests, consider TestZulipApp with its accountId field.'), + ]); + }()); + return true; + } + /// Whether there is a relevant account specified for this widget. static bool debugExistsOf(BuildContext context) { return context.getElementForInheritedWidgetOfExactType<_PerAccountStoreInheritedWidget>() != null; diff --git a/test/widgets/store_test.dart b/test/widgets/store_test.dart index b93cc91ff5..e2d7821ae3 100644 --- a/test/widgets/store_test.dart +++ b/test/widgets/store_test.dart @@ -95,6 +95,24 @@ void main() { tester.widget(find.text('found store, account: ${eg.selfAccount.id}')); }); + testWidgets('PerAccountStoreWidget.of detailed error', (tester) async { + addTearDown(testBinding.reset); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: GlobalStoreWidget( + // no PerAccountStoreWidget + child: Builder( + builder: (context) { + final store = PerAccountStoreWidget.of(context); + return Text('found store, account: ${store.accountId}'); + })))); + await tester.pump(); + check(tester.takeException()) + .has((x) => x.toString(), 'toString') // TODO(checks): what's a good convention for this? + .contains('consider MaterialAccountWidgetRoute'); + }); + testWidgets('PerAccountStoreWidget immediate data after first loaded', (tester) async { await testBinding.globalStore.add(eg.selfAccount, eg.initialSnapshot()); addTearDown(testBinding.reset);