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
+}