import 'package:encrypter/encrypter/xor.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:frontend/settings/settings.dart'; import 'package:frontend/utils/pref_utils.dart'; import 'package:frontend/utils/push_service.dart'; import 'package:http_mock_adapter/http_mock_adapter.dart'; import '../helpers/backend_test_harness.dart'; import '../helpers/pump_app.dart'; // A fake so the toggle can be exercised without Firebase. class _FakePushService implements PushService { _FakePushService({this.supported = true, this.enableResult = true}); final bool supported; final bool enableResult; int enableCalls = 0; int disableCalls = 0; @override bool get isSupported => supported; @override Future enable() async { enableCalls++; return enableResult; } @override Future disable() async { disableCalls++; } } const _toggleText = 'Push notifications on this device'; void main() { late DioAdapter adapter; setUp(() async { adapter = await setUpBackendForTest( prefs: { 'base_url': 'http://my.server/api', 'api_key': XOR().xorEncode('topsecret'), 'theme': 'catppuccin_mocha', }, ); // The mock SharedPreferences persist across tests in this isolate, so reset // the per-device push intent to a known-off baseline before each test. await PrefUtil.setValue('push_enabled', false); }); // Stubs the backend config endpoint the settings screen reads on load. void stubNotificationMethod(String method) { adapter.onGet( '/config', (server) => server.reply(200, { 'notifications': {'method': method}, }), ); } // Pumps frames so the async config load (resolved via Dio's zero-duration // timer) settles, for assertions that expect the toggle to be absent. Future settleConfig(WidgetTester tester) async { for (var i = 0; i < 10; i++) { await tester.pump(const Duration(milliseconds: 10)); } } testWidgets('shows the push toggle when the backend method is push', ( tester, ) async { stubNotificationMethod('push'); await pumpScreen(tester, Settings(pushService: _FakePushService())); await pumpUntilFound(tester, find.text(_toggleText)); expect(find.text(_toggleText), findsOneWidget); }); testWidgets('hides the push toggle when push is unsupported', (tester) async { stubNotificationMethod('push'); await pumpScreen( tester, Settings(pushService: _FakePushService(supported: false)), ); await settleConfig(tester); expect(find.text(_toggleText), findsNothing); }); testWidgets('hides the push toggle when the backend method is not push', ( tester, ) async { stubNotificationMethod('pushover'); await pumpScreen(tester, Settings(pushService: _FakePushService())); await settleConfig(tester); expect(find.text(_toggleText), findsNothing); }); testWidgets('enabling push registers and shows a success message', ( tester, ) async { stubNotificationMethod('push'); final service = _FakePushService(enableResult: true); await pumpScreen(tester, Settings(pushService: service)); await pumpUntilFound(tester, find.byType(SwitchListTile)); await tester.tap(find.byType(SwitchListTile)); await pumpUntilFound( tester, find.text('Push notifications enabled on this device'), ); expect(service.enableCalls, 1); expect(PrefUtil.getValue('push_enabled', false), isTrue); expect( tester.widget(find.byType(SwitchListTile)).value, isTrue, ); }); testWidgets('a declined permission leaves the toggle off with an error', ( tester, ) async { stubNotificationMethod('push'); final service = _FakePushService(enableResult: false); await pumpScreen(tester, Settings(pushService: service)); await pumpUntilFound(tester, find.byType(SwitchListTile)); await tester.tap(find.byType(SwitchListTile)); await pumpUntilFound(tester, find.textContaining('Could not enable push')); expect(service.enableCalls, 1); expect( tester.widget(find.byType(SwitchListTile)).value, isFalse, ); }); testWidgets('disabling push unregisters and shows a success message', ( tester, ) async { stubNotificationMethod('push'); await PrefUtil.setValue('push_enabled', true); final service = _FakePushService(); await pumpScreen(tester, Settings(pushService: service)); await pumpUntilFound(tester, find.byType(SwitchListTile)); // Starts on because the stored intent is enabled. expect( tester.widget(find.byType(SwitchListTile)).value, isTrue, ); await tester.tap(find.byType(SwitchListTile)); await pumpUntilFound( tester, find.text('Push notifications disabled on this device'), ); expect(service.disableCalls, 1); expect(PrefUtil.getValue('push_enabled', false), isFalse); }); testWidgets('hides the test button when push is off on this device', ( tester, ) async { stubNotificationMethod('push'); await PrefUtil.setValue('push_enabled', false); await pumpScreen(tester, Settings(pushService: _FakePushService())); await pumpUntilFound(tester, find.byType(SwitchListTile)); expect(find.text('Send test notification'), findsNothing); }); testWidgets('reports the device count when a test notification is sent', ( tester, ) async { stubNotificationMethod('push'); adapter.onPost( '/notifications/test', (server) => server.reply(200, {'delivered': 2}), ); await PrefUtil.setValue('push_enabled', true); await pumpScreen(tester, Settings(pushService: _FakePushService())); await pumpUntilFound(tester, find.text('Send test notification')); await tester.tap(find.text('Send test notification')); await pumpUntilFound( tester, find.text('Test notification sent to 2 devices.'), ); }); testWidgets('warns when a test notification reaches no devices', ( tester, ) async { stubNotificationMethod('push'); adapter.onPost( '/notifications/test', (server) => server.reply(200, {'delivered': 0}), ); await PrefUtil.setValue('push_enabled', true); await pumpScreen(tester, Settings(pushService: _FakePushService())); await pumpUntilFound(tester, find.text('Send test notification')); await tester.tap(find.text('Send test notification')); await pumpUntilFound( tester, find.text('No devices are registered to receive push notifications.'), ); }); }