From d541efeb37271501fce3176b33e68cb95be4ae2d Mon Sep 17 00:00:00 2001 From: shukebeta Date: Tue, 14 Jul 2026 17:15:44 +1200 Subject: [PATCH] feat: remind user to test an untested Telegram sync setting After a Telegram sync setting is saved it starts untested and may not actually sync (wrong channel id, bad token, bot not admin). The Add flow previously saved and dismissed silently, so a broken setting could go unnoticed. - add_telegram_setting.dart: after a successful add, show a non-blocking reminder ("Test now" / "Later"). Test now runs the existing test flow against the just-entered values (the Test endpoint keys off the posted fields, not a saved-row id) and surfaces a success SnackBar; either choice keeps the setting saved. Saving is never blocked. - telegram_sync_settings.dart: add an explicit "Untested" badge beside the status pill so untested settings stay flagged at a glance. - Add widget tests covering the reminder, Later (no test), and Test now. Closes #51 Co-Authored-By: Claude Opus 4.8 --- .../settings/add_telegram_setting.dart | 35 +++++-- .../settings/telegram_sync_settings.dart | 17 ++++ test/widgets/add_telegram_setting_test.dart | 98 +++++++++++++++++++ 3 files changed, 141 insertions(+), 9 deletions(-) create mode 100644 test/widgets/add_telegram_setting_test.dart diff --git a/lib/screens/settings/add_telegram_setting.dart b/lib/screens/settings/add_telegram_setting.dart index ab36ede3..b0cb47bd 100644 --- a/lib/screens/settings/add_telegram_setting.dart +++ b/lib/screens/settings/add_telegram_setting.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:happy_notes/entities/telegram_settings.dart'; import '../../dependency_injection.dart'; +import '../../services/dialog_services.dart'; import 'telegram_sync_settings_controller.dart'; import '../../utils/util.dart'; // Import Util @@ -82,16 +83,32 @@ class AddTelegramSettingState extends State { setState(() => _isLoading = true); try { - await _settingsController.addTelegramSetting( - TelegramSettings( - syncType: _syncType, - syncValue: _syncType == 4 ? _tagController.text : '', - channelId: _channelIdController.text.trim(), - channelName: _channelNameController.text.trim(), - tokenRemark: _remarkController.text.trim(), - encryptedToken: _tokenController.text.trim(), - ), + final setting = TelegramSettings( + syncType: _syncType, + syncValue: _syncType == 4 ? _tagController.text : '', + channelId: _channelIdController.text.trim(), + channelName: _channelNameController.text.trim(), + tokenRemark: _remarkController.text.trim(), + encryptedToken: _tokenController.text.trim(), ); + await _settingsController.addTelegramSetting(setting); + if (mounted) { + // The setting is saved but untested and may not sync. Remind the user + // to test it, without blocking — either choice keeps the setting saved. + final testNow = await DialogService.showConfirmDialog( + context, + title: 'Test this setting?', + text: "This setting hasn't been tested yet and may not sync. Test it now?", + noText: 'Later', + yesText: 'Test now', + ); + if (testNow == true && mounted) { + final messenger = ScaffoldMessenger.of(context); + if (await _settingsController.testTelegramSetting(context, setting)) { + Util.showInfo(messenger, 'Test message sent successfully.'); + } + } + } if (mounted) { Navigator.of(context).pop(); } diff --git a/lib/screens/settings/telegram_sync_settings.dart b/lib/screens/settings/telegram_sync_settings.dart index 68835bf6..ca777929 100644 --- a/lib/screens/settings/telegram_sync_settings.dart +++ b/lib/screens/settings/telegram_sync_settings.dart @@ -125,6 +125,23 @@ class TelegramSyncSettingsState extends State { ), ), ), + if (!setting.isTested) ...[ + const SizedBox(width: 8), + Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + color: Colors.orange.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(16), + ), + child: const Text( + 'Untested', + style: TextStyle( + color: Colors.orange, + fontWeight: FontWeight.bold, + ), + ), + ), + ], const Spacer(), Wrap( spacing: 8, diff --git a/test/widgets/add_telegram_setting_test.dart b/test/widgets/add_telegram_setting_test.dart new file mode 100644 index 00000000..9b1421fc --- /dev/null +++ b/test/widgets/add_telegram_setting_test.dart @@ -0,0 +1,98 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:get_it/get_it.dart'; +import 'package:happy_notes/apis/telegram_settings_api.dart'; +import 'package:happy_notes/entities/telegram_settings.dart'; +import 'package:happy_notes/screens/settings/add_telegram_setting.dart'; +import 'package:happy_notes/screens/settings/telegram_sync_settings_controller.dart'; +import 'package:happy_notes/services/telegram_settings_service.dart'; + +import '../test_helpers/seq_logger_setup.dart'; + +/// Stub controller: records calls and never touches the network. +class StubTelegramSyncSettingsController extends TelegramSyncSettingsController { + StubTelegramSyncSettingsController() + : super( + telegramSettingService: + TelegramSettingsService(telegramSettingsApi: TelegramSettingsApi()), + ); + + bool addCalled = false; + bool testCalled = false; + bool testResult = true; + + @override + Future addTelegramSetting(TelegramSettings setting) async { + addCalled = true; + return true; + } + + @override + Future testTelegramSetting(BuildContext context, TelegramSettings setting) async { + testCalled = true; + return testResult; + } +} + +void main() { + late StubTelegramSyncSettingsController stub; + + setUp(() async { + setupSeqLoggerForTesting(); + await GetIt.instance.reset(); + stub = StubTelegramSyncSettingsController(); + GetIt.instance.registerSingleton(stub); + }); + + tearDown(() async { + await GetIt.instance.reset(); + }); + + // Note: while the reminder dialog is open, _saveSetting is still awaiting it, + // so the save-button spinner (CircularProgressIndicator) is still animating. + // pumpAndSettle would time out on that perpetual animation — pump fixed + // durations instead. + Future settle(WidgetTester tester) async { + await tester.pump(); + await tester.pump(const Duration(milliseconds: 400)); + } + + Future fillAndSave(WidgetTester tester) async { + await tester.pumpWidget(const MaterialApp(home: AddTelegramSetting())); + await tester.enterText(find.widgetWithText(TextFormField, 'Channel ID'), '-100123'); + await tester.enterText(find.widgetWithText(TextFormField, 'Channel Name'), 'My Channel'); + await tester.enterText(find.widgetWithText(TextFormField, 'Telegram Bot Token'), 'token'); + await tester.enterText(find.widgetWithText(TextFormField, 'Token Remark'), 'remark'); + await tester.tap(find.text('Save Settings')); + await settle(tester); + } + + testWidgets('save shows a non-blocking test reminder', (tester) async { + await fillAndSave(tester); + + expect(stub.addCalled, isTrue); + expect(find.text('Test now'), findsOneWidget); + expect(find.text('Later'), findsOneWidget); + }); + + testWidgets('choosing Later dismisses without testing', (tester) async { + await fillAndSave(tester); + + await tester.tap(find.text('Later')); + await settle(tester); + + expect(stub.testCalled, isFalse); + // Dialog dismissed; the setting stays saved (save was never blocked). + expect(find.text('Test now'), findsNothing); + }); + + testWidgets('choosing Test now runs the test flow', (tester) async { + await fillAndSave(tester); + + await tester.tap(find.text('Test now')); + await settle(tester); + + expect(stub.testCalled, isTrue); + expect(find.text('Test now'), findsNothing); + }); +}