@tduncan / Cortana / commits / 62f879e

fix #44: resolve drawer navigation state errors and remove issue template from crash reports

abdulmominsakib committed Aug 12, 2026 at 19:21 UTC 62f879e8690824b41096a7a7e328890a8001027d
6 files changed +257 -27
lib/app.dart
+16 -18
@@ -273,10 +273,6 @@ class AppShell extends ConsumerWidget {
273
274 const AppShell({super.key, required this.child});
275
276 - static void openDrawer(BuildContext context) {
277 - Scaffold.of(context).openDrawer();
278 - }
279 -
276 @override
277 Widget build(BuildContext context, WidgetRef ref) {
278 final theme = Theme.of(context);
@@ -340,20 +336,22 @@ class AppShell extends ConsumerWidget {
336 }
337
338 return Scaffold(
343 - body: PopScope(
344 - canPop: false,
345 - onPopInvokedWithResult: (didPop, _) {
346 - if (didPop) return;
347 - if (isHome && !hasActiveChat) {
348 - // On the empty home screen, open the side drawer instead
349 - // of exiting the app — matches the menu icon on the
350 - // top-left.
351 - Scaffold.of(context).openDrawer();
352 - return;
353 - }
354 - handleBack();
355 - },
356 - child: child,
339 + body: Builder(
340 + builder: (scaffoldContext) => PopScope(
341 + canPop: false,
342 + onPopInvokedWithResult: (didPop, _) {
343 + if (didPop) return;
344 + if (isHome && !hasActiveChat) {
345 + // On the empty home screen, open the side drawer instead
346 + // of exiting the app — matches the menu icon on the
347 + // top-left. This context must be below the Scaffold.
348 + Scaffold.of(scaffoldContext).openDrawer();
349 + return;
350 + }
351 + handleBack();
352 + },
353 + child: child,
354 + ),
355 ),
356 drawer: const ConversationDrawer(),
357 );
lib/core/services/crash_report_service.dart
-1
@@ -288,7 +288,6 @@ class CrashReportService {
288 'title': title,
289 'body': report.markdownBody,
290 'labels': 'crash,bug',
291 - 'template': issueTemplate,
291 };
292 return Uri.https('github.com', '/$repoOwner/$repoName/issues/new', params);
293 }
lib/features/sidebar/components/active_server_indicator.dart
+4 -5
@@ -96,6 +96,8 @@ class ActiveServerIndicator extends ConsumerWidget {
96 child: InkWell(
97 onTap: () {
98 final servers = serversAsync.value ?? [];
99 + final scaffold = Scaffold.maybeOf(context);
100 + final router = GoRouter.of(context);
101
102 showModalBottomSheet(
103 context: context,
@@ -311,11 +313,8 @@ class ActiveServerIndicator extends ConsumerWidget {
313 leading: const HugeIcon(icon: HugeIcons.strokeRoundedSettings01, size: 18),
314 onPressed: () {
315 Navigator.pop(context);
314 - context.go(AppRoutes.servers);
315 - if (Scaffold.maybeOf(context)?.isDrawerOpen ??
316 - false) {
317 - Navigator.pop(context);
318 - }
316 + scaffold?.closeDrawer();
317 + router.go(AppRoutes.servers);
318 },
319 child: Text(l10n.manage_servers),
320 ),
test/app_shell_back_navigation_test.dart new
+123
@@ -0,0 +1,123 @@
1 +import 'package:flutter/material.dart';
2 +import 'package:flutter_riverpod/flutter_riverpod.dart';
3 +import 'package:flutter_test/flutter_test.dart';
4 +import 'package:go_router/go_router.dart';
5 +import 'package:localmind/app.dart';
6 +import 'package:localmind/core/routes/app_routes.dart';
7 +import 'package:localmind/core/theme/app_theme.dart';
8 +import 'package:localmind/features/chat/providers/chat_providers.dart';
9 +import 'package:localmind/features/servers/data/models/server.dart';
10 +import 'package:localmind/features/servers/providers/server_providers.dart';
11 +import 'package:localmind/features/tts/providers/tts_providers.dart';
12 +import 'package:localmind/l10n/app_localizations.dart';
13 +import 'package:shadcn_ui/shadcn_ui.dart';
14 +
15 +void main() {
16 + late GoRouter router;
17 +
18 + setUp(() {
19 + router = GoRouter(
20 + initialLocation: AppRoutes.home,
21 + routes: [
22 + ShellRoute(
23 + builder: (context, state, child) => AppShell(child: child),
24 + routes: [
25 + GoRoute(
26 + path: AppRoutes.home,
27 + builder: (context, state) => const Text('Home'),
28 + ),
29 + GoRoute(
30 + path: AppRoutes.ttsModels,
31 + builder: (context, state) => const Text('TTS models'),
32 + ),
33 + ],
34 + ),
35 + ],
36 + );
37 + addTearDown(router.dispose);
38 + });
39 +
40 + Future<void> pumpShell(
41 + WidgetTester tester, {
42 + String location = AppRoutes.home,
43 + }) async {
44 + tester.view.physicalSize = const Size(400, 800);
45 + tester.view.devicePixelRatio = 1;
46 + addTearDown(tester.view.resetPhysicalSize);
47 + addTearDown(tester.view.resetDevicePixelRatio);
48 +
49 + router.go(location);
50 + await tester.pumpWidget(
51 + ProviderScope(
52 + overrides: [
53 + chatProvider.overrideWith(_StubChatNotifier.new),
54 + activeServerProvider.overrideWith(_StubActiveServerNotifier.new),
55 + serversProvider.overrideWith(_StubServersNotifier.new),
56 + ttsProvider.overrideWith(_StubTtsNotifier.new),
57 + ],
58 + child: ShadTheme(
59 + data: AppTheme.lightShadTheme,
60 + child: MaterialApp.router(
61 + locale: const Locale('en'),
62 + localizationsDelegates: AppLocalizations.localizationsDelegates,
63 + supportedLocales: AppLocalizations.supportedLocales,
64 + routerConfig: router,
65 + ),
66 + ),
67 + ),
68 + );
69 + await tester.pump();
70 + }
71 +
72 + testWidgets('back on empty mobile home opens the drawer without crashing', (
73 + tester,
74 + ) async {
75 + await pumpShell(tester);
76 +
77 + await tester.binding.handlePopRoute();
78 + await tester.pump();
79 +
80 + final scaffold = tester.state<ScaffoldState>(find.byType(Scaffold));
81 + expect(scaffold.isDrawerOpen, isTrue);
82 + expect(tester.takeException(), isNull);
83 + });
84 +
85 + testWidgets('back from TTS returns home, then opens the drawer safely', (
86 + tester,
87 + ) async {
88 + await pumpShell(tester, location: AppRoutes.ttsModels);
89 + expect(find.text('TTS models'), findsOneWidget);
90 +
91 + await tester.binding.handlePopRoute();
92 + await tester.pumpAndSettle();
93 + expect(find.text('Home'), findsOneWidget);
94 + expect(tester.takeException(), isNull);
95 +
96 + await tester.binding.handlePopRoute();
97 + await tester.pump();
98 +
99 + final scaffold = tester.state<ScaffoldState>(find.byType(Scaffold));
100 + expect(scaffold.isDrawerOpen, isTrue);
101 + expect(tester.takeException(), isNull);
102 + });
103 +}
104 +
105 +class _StubChatNotifier extends ChatNotifier {
106 + @override
107 + ChatState build() => const ChatState();
108 +}
109 +
110 +class _StubActiveServerNotifier extends ActiveServerNotifier {
111 + @override
112 + Server? build() => null;
113 +}
114 +
115 +class _StubServersNotifier extends ServersNotifier {
116 + @override
117 + Future<List<Server>> build() async => const [];
118 +}
119 +
120 +class _StubTtsNotifier extends TtsNotifier {
121 + @override
122 + TtsState build() => const TtsState();
123 +}
test/core/services/crash_report_service_test.dart
+3 -3
@@ -132,7 +132,7 @@ void main() {
132 });
133
134 group('CrashReportService', () {
135 - test('buildGitHubIssueUrl points to the configured repo and template', () {
135 + test('buildGitHubIssueUrl prefills diagnostics without a template', () {
136 final report = _makeReport(
137 error: Exception('Boom'),
138 stack: StackTrace.current,
@@ -140,10 +140,10 @@ void main() {
140 final uri = CrashReportService.instance.buildGitHubIssueUrl(report);
141 expect(uri.host, 'github.com');
142 expect(uri.path, '/abdulmominsakib/localmind/issues/new');
143 - expect(uri.queryParameters['template'], 'crash_report.md');
143 + expect(uri.queryParameters, isNot(contains('template')));
144 expect(uri.queryParameters['labels'], 'crash,bug');
145 expect(uri.queryParameters['title'], 'Crash: Exception: Boom');
146 - expect(uri.queryParameters['body'], isNotNull);
146 + expect(uri.queryParameters['body'], report.markdownBody);
147 });
148
149 test('buildFeedbackIssueUrl points to the feedback form', () {
test/features/sidebar/active_server_indicator_test.dart new
+111
@@ -0,0 +1,111 @@
1 +import 'package:flutter/material.dart';
2 +import 'package:flutter_riverpod/flutter_riverpod.dart';
3 +import 'package:flutter_test/flutter_test.dart';
4 +import 'package:go_router/go_router.dart';
5 +import 'package:localmind/core/models/enums.dart';
6 +import 'package:localmind/core/routes/app_routes.dart';
7 +import 'package:localmind/core/theme/app_theme.dart';
8 +import 'package:localmind/features/servers/data/models/server.dart';
9 +import 'package:localmind/features/servers/providers/server_providers.dart';
10 +import 'package:localmind/features/sidebar/components/active_server_indicator.dart';
11 +import 'package:localmind/l10n/app_localizations.dart';
12 +import 'package:shadcn_ui/shadcn_ui.dart';
13 +
14 +void main() {
15 + testWidgets('Manage Servers closes the sheet and drawer before navigating', (
16 + tester,
17 + ) async {
18 + tester.view.physicalSize = const Size(400, 800);
19 + tester.view.devicePixelRatio = 1;
20 + addTearDown(tester.view.resetPhysicalSize);
21 + addTearDown(tester.view.resetDevicePixelRatio);
22 +
23 + final server = Server(
24 + id: 'on-device',
25 + name: 'Test Server',
26 + type: ServerType.onDevice,
27 + host: '',
28 + port: 0,
29 + createdAt: DateTime.utc(2026, 8, 12),
30 + lastConnectedAt: DateTime.utc(2026, 8, 12),
31 + status: ConnectionStatus.connected,
32 + );
33 + final scaffoldKey = GlobalKey<ScaffoldState>();
34 + final router = GoRouter(
35 + routes: [
36 + ShellRoute(
37 + builder: (context, state, child) => Scaffold(
38 + key: scaffoldKey,
39 + drawer: const Drawer(child: ActiveServerIndicator()),
40 + body: child,
41 + ),
42 + routes: [
43 + GoRoute(
44 + path: AppRoutes.home,
45 + builder: (context, state) => const Text('Home page'),
46 + ),
47 + GoRoute(
48 + path: AppRoutes.servers,
49 + builder: (context, state) => const Text('Servers page'),
50 + ),
51 + ],
52 + ),
53 + ],
54 + );
55 + addTearDown(router.dispose);
56 +
57 + await tester.pumpWidget(
58 + ProviderScope(
59 + overrides: [
60 + activeServerProvider.overrideWith(
61 + () => _StubActiveServerNotifier(server),
62 + ),
63 + serversProvider.overrideWith(() => _StubServersNotifier(server)),
64 + ],
65 + child: ShadTheme(
66 + data: AppTheme.lightShadTheme,
67 + child: MaterialApp.router(
68 + locale: const Locale('en'),
69 + localizationsDelegates: AppLocalizations.localizationsDelegates,
70 + supportedLocales: AppLocalizations.supportedLocales,
71 + routerConfig: router,
72 + ),
73 + ),
74 + ),
75 + );
76 +
77 + scaffoldKey.currentState!.openDrawer();
78 + await tester.pumpAndSettle();
79 + expect(scaffoldKey.currentState!.isDrawerOpen, isTrue);
80 +
81 + await tester.tap(find.text('Test Server'));
82 + await tester.pumpAndSettle();
83 + expect(find.text('Switch Server'), findsOneWidget);
84 +
85 + await tester.tap(find.text('Manage Servers'));
86 + await tester.pumpAndSettle();
87 +
88 + expect(find.text('Servers page'), findsOneWidget);
89 + expect(find.text('Switch Server'), findsNothing);
90 + expect(scaffoldKey.currentState!.isDrawerOpen, isFalse);
91 + expect(tester.takeException(), isNull);
92 + });
93 +}
94 +
95 +class _StubActiveServerNotifier extends ActiveServerNotifier {
96 + _StubActiveServerNotifier(this.server);
97 +
98 + final Server server;
99 +
100 + @override
101 + Server? build() => server;
102 +}
103 +
104 +class _StubServersNotifier extends ServersNotifier {
105 + _StubServersNotifier(this.server);
106 +
107 + final Server server;
108 +
109 + @override
110 + Future<List<Server>> build() async => [server];
111 +}