forked from eatvio/eatvio_app
feat(push_notifications): issue-660, fix: issue-908
This commit is contained in:
@@ -2,6 +2,7 @@ import 'dart:io';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:eatvio_app/core/error_reporting/error_report_policy.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:sentry_flutter/sentry_flutter.dart';
|
||||
|
||||
@@ -72,6 +73,24 @@ void main() {
|
||||
test('returns true for arbitrary exceptions', () {
|
||||
expect(ErrorReportPolicy.shouldReport(StateError('x')), true);
|
||||
});
|
||||
|
||||
test('returns false for HMS client api invalid', () {
|
||||
expect(
|
||||
ErrorReportPolicy.shouldReport(
|
||||
PlatformException(code: 'HMS_TOKEN_ERROR', message: '907135003: client api invalid'),
|
||||
),
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
test('returns true for other HMS token errors', () {
|
||||
expect(
|
||||
ErrorReportPolicy.shouldReport(
|
||||
PlatformException(code: 'HMS_TOKEN_ERROR', message: 'Missing Huawei appId'),
|
||||
),
|
||||
true,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group('ErrorReportPolicy.allowSentryEvent', () {
|
||||
@@ -121,6 +140,13 @@ void main() {
|
||||
expect(ErrorReportPolicy.allowSentryEvent(event, hint), true);
|
||||
});
|
||||
|
||||
test('drops HMS client api invalid before send', () {
|
||||
final event = SentryEvent(
|
||||
throwable: PlatformException(code: 'HMS_TOKEN_ERROR', message: '907135003: client api invalid'),
|
||||
);
|
||||
expect(ErrorReportPolicy.allowSentryEvent(event, hint), false);
|
||||
});
|
||||
|
||||
test('drops cancelled Dio from SDK', () {
|
||||
final e = DioException(
|
||||
requestOptions: RequestOptions(path: '/x'),
|
||||
|
||||
112
test/core/push/hms_client_error_test.dart
Normal file
112
test/core/push/hms_client_error_test.dart
Normal file
@@ -0,0 +1,112 @@
|
||||
import 'package:eatvio_app/core/push/hms_client_error.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
void main() {
|
||||
group('isHuaweiFamilyBrand', () {
|
||||
test('matches Huawei and Honor brand or manufacturer', () {
|
||||
expect(
|
||||
isHuaweiFamilyBrand(brand: 'HONOR', manufacturer: 'HONOR'),
|
||||
isTrue,
|
||||
);
|
||||
expect(
|
||||
isHuaweiFamilyBrand(brand: 'Huawei', manufacturer: 'HUAWEI'),
|
||||
isTrue,
|
||||
);
|
||||
expect(
|
||||
isHuaweiFamilyBrand(brand: 'samsung', manufacturer: 'HONOR'),
|
||||
isTrue,
|
||||
);
|
||||
});
|
||||
|
||||
test('does not match other Android brands', () {
|
||||
expect(
|
||||
isHuaweiFamilyBrand(brand: 'samsung', manufacturer: 'Samsung'),
|
||||
isFalse,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group('isHmsClientUnavailable', () {
|
||||
test('matches 907135003 client api invalid', () {
|
||||
expect(
|
||||
isHmsClientUnavailable(
|
||||
PlatformException(
|
||||
code: 'HMS_TOKEN_ERROR',
|
||||
message: '907135003: client api invalid',
|
||||
),
|
||||
),
|
||||
isTrue,
|
||||
);
|
||||
});
|
||||
|
||||
test('does not match a missing app id', () {
|
||||
expect(
|
||||
isHmsClientUnavailable(
|
||||
PlatformException(
|
||||
code: 'HMS_TOKEN_ERROR',
|
||||
message: 'Missing Huawei appId',
|
||||
),
|
||||
),
|
||||
isFalse,
|
||||
);
|
||||
expect(isHmsClientUnavailable(StateError('907135003')), isFalse);
|
||||
});
|
||||
});
|
||||
|
||||
group('pushPlatform', () {
|
||||
test('uses huawei only when an HMS token exists', () {
|
||||
expect(
|
||||
pushPlatform(
|
||||
isIos: false,
|
||||
huaweiFamily: true,
|
||||
hmsAvailable: true,
|
||||
hasHmsToken: true,
|
||||
),
|
||||
'huawei',
|
||||
);
|
||||
});
|
||||
|
||||
test('falls back to android on Honor without a valid HMS client', () {
|
||||
expect(
|
||||
pushPlatform(
|
||||
isIos: false,
|
||||
huaweiFamily: true,
|
||||
hmsAvailable: false,
|
||||
hasHmsToken: false,
|
||||
),
|
||||
'android',
|
||||
);
|
||||
expect(
|
||||
pushPlatform(
|
||||
isIos: false,
|
||||
huaweiFamily: true,
|
||||
hmsAvailable: true,
|
||||
hasHmsToken: false,
|
||||
),
|
||||
'android',
|
||||
);
|
||||
});
|
||||
|
||||
test('keeps ios and plain android', () {
|
||||
expect(
|
||||
pushPlatform(
|
||||
isIos: true,
|
||||
huaweiFamily: false,
|
||||
hmsAvailable: false,
|
||||
hasHmsToken: false,
|
||||
),
|
||||
'ios',
|
||||
);
|
||||
expect(
|
||||
pushPlatform(
|
||||
isIos: false,
|
||||
huaweiFamily: false,
|
||||
hmsAvailable: false,
|
||||
hasHmsToken: false,
|
||||
),
|
||||
'android',
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
26
test/core/utils/url_utils_test.dart
Normal file
26
test/core/utils/url_utils_test.dart
Normal file
@@ -0,0 +1,26 @@
|
||||
import 'package:eatvio_app/core/utils/url_utils.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
void main() {
|
||||
const baseUrl = 'https://dev2.eatvio.com';
|
||||
|
||||
test('keeps an absolute default avatar url unchanged', () {
|
||||
const avatar =
|
||||
'https://ui-avatars.dev2.eatvio.com/api/?name=%D0%9A+%D0%90&color=7F9CF5&background=EBF4FF';
|
||||
|
||||
expect(joinBaseUrl(baseUrl, avatar), avatar);
|
||||
});
|
||||
|
||||
test('keeps an absolute http url unchanged', () {
|
||||
const photo = 'http://localhost/storage/avatar.jpg';
|
||||
|
||||
expect(joinBaseUrl(baseUrl, photo), photo);
|
||||
});
|
||||
|
||||
test('prefixes a relative storage path with the base url', () {
|
||||
expect(
|
||||
joinBaseUrl(baseUrl, '/storage/profile-photos/photo.jpg'),
|
||||
'https://dev2.eatvio.com/storage/profile-photos/photo.jpg',
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -17,6 +17,7 @@ import 'package:eatvio_app/features/feed/presentation/bloc/feed_cubit/feed_state
|
||||
import 'package:eatvio_app/generated/l10n.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_keyboard_visibility/flutter_keyboard_visibility.dart';
|
||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
@@ -60,6 +61,7 @@ void main() {
|
||||
});
|
||||
|
||||
tearDown(() {
|
||||
KeyboardVisibilityTesting.setVisibilityForTesting(false);
|
||||
blogPostCreateCubit.close();
|
||||
});
|
||||
|
||||
@@ -128,12 +130,16 @@ void main() {
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('does not add extra space above the title field', (tester) async {
|
||||
testWidgets('does not add extra space above the title field', (
|
||||
tester,
|
||||
) async {
|
||||
await tester.pumpWidget(createTestWidget());
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final titleTop = tester.getTopLeft(find.text('Заголовок')).dy;
|
||||
final scrollTop = tester.getTopLeft(find.byType(SingleChildScrollView)).dy;
|
||||
final scrollTop = tester
|
||||
.getTopLeft(find.byType(SingleChildScrollView))
|
||||
.dy;
|
||||
expect(titleTop - scrollTop, 16);
|
||||
});
|
||||
|
||||
@@ -281,6 +287,44 @@ void main() {
|
||||
expect(find.text('Создать'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
'keeps create button above the editor toolbar when the keyboard is visible',
|
||||
(tester) async {
|
||||
await tester.pumpWidget(createTestWidget());
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
KeyboardVisibilityTesting.setVisibilityForTesting(true);
|
||||
await tester.pump();
|
||||
await tester.pump();
|
||||
|
||||
final createButton = find.byKey(
|
||||
AppKeys.blogPostCreateScreenCreateButton,
|
||||
);
|
||||
final editorToolbar = find.byKey(
|
||||
const ValueKey('blogPostCreateEditorToolbar'),
|
||||
);
|
||||
|
||||
expect(createButton, findsOneWidget);
|
||||
expect(editorToolbar, findsOneWidget);
|
||||
|
||||
final buttonRect = tester.getRect(createButton);
|
||||
final toolbarRect = tester.getRect(editorToolbar);
|
||||
|
||||
expect(buttonRect.overlaps(toolbarRect), isFalse);
|
||||
expect(buttonRect.top, greaterThanOrEqualTo(toolbarRect.bottom));
|
||||
|
||||
await tester.tap(createButton);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(
|
||||
find.text(
|
||||
'Добавьте заголовок, описание, изображение или ссылку на видео',
|
||||
),
|
||||
findsOneWidget,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets('does not submit when form is empty', (tester) async {
|
||||
when(
|
||||
() => mockBlogPostRepository.createBlogPost(
|
||||
|
||||
@@ -23,7 +23,7 @@ void main() {
|
||||
);
|
||||
const state = NotificationSettingsStateLoaded(notificationSettings: settings);
|
||||
|
||||
expect(state.props, [settings]);
|
||||
expect(state.props, [settings, null, null]);
|
||||
expect(state.notificationSettings, settings);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:eatvio_app/features/menu/presentation/bloc/notification_settings_cubit/notification_settings_cubit.dart';
|
||||
import 'package:eatvio_app/features/menu/presentation/bloc/notification_settings_cubit/notification_settings_state.dart';
|
||||
import 'package:eatvio_app/features/menu/repositories/notification_settings_repository.dart';
|
||||
@@ -103,6 +104,51 @@ void main() {
|
||||
final errorState = notificationSettingsCubit!.state as NotificationSettingsStateError;
|
||||
expect(errorState.errorMessage, contains('Ошибка обновления'));
|
||||
});
|
||||
|
||||
test('updateNotificationSettings - validation error keeps the screen and exposes an alert', () async {
|
||||
notificationSettingsCubit = NotificationSettingsCubit(
|
||||
notificationSettingsRepository: mockNotificationSettingsRepository,
|
||||
);
|
||||
when(mockNotificationSettingsRepository.getNotificationSettings()).thenAnswer((_) async => MenuFixtures.notificationSettingsModel());
|
||||
await notificationSettingsCubit!.getNotificationSettings();
|
||||
|
||||
const message = 'Между приемами пищи должна быть разница минимум час';
|
||||
when(mockNotificationSettingsRepository.updateNotificationSettings(any)).thenThrow(
|
||||
DioException(
|
||||
requestOptions: RequestOptions(path: '/api/profile/update_notification_settings'),
|
||||
type: DioExceptionType.badResponse,
|
||||
response: Response(
|
||||
requestOptions: RequestOptions(path: '/api/profile/update_notification_settings'),
|
||||
statusCode: 422,
|
||||
data: {
|
||||
'message': message,
|
||||
'errors': {
|
||||
'diary_reminder_notifications.second_reminder': [message],
|
||||
},
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
await notificationSettingsCubit!.updateNotificationSettings({
|
||||
'diary_reminder_notifications': {
|
||||
'second_reminder': {'enabled': true, 'local_time_hours': 9, 'local_time_mins': 30},
|
||||
},
|
||||
});
|
||||
|
||||
expect(notificationSettingsCubit!.state, isA<NotificationSettingsStateLoaded>());
|
||||
final loadedState = notificationSettingsCubit!.state as NotificationSettingsStateLoaded;
|
||||
expect(loadedState.alertMessage, message);
|
||||
expect(loadedState.notificationSettings.newUpdatesNotification, true);
|
||||
|
||||
when(mockNotificationSettingsRepository.updateNotificationSettings(any)).thenThrow(Exception('сеть'));
|
||||
await notificationSettingsCubit!.updateNotificationSettings({'new_updates_notification': false});
|
||||
|
||||
final retried = notificationSettingsCubit!.state as NotificationSettingsStateLoaded;
|
||||
expect(retried.alertMessage, 'Не удалось сохранить настройки уведомлений');
|
||||
expect(retried.alertId, isNot(loadedState.alertId));
|
||||
expect(retried.notificationSettings, loadedState.notificationSettings);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:eatvio_app/features/menu/presentation/notification_settings_user_message.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
void main() {
|
||||
group('notificationSettingsUpdateAlertMessage', () {
|
||||
test('uses the Laravel validation message for a meal reminder gap', () {
|
||||
const message = 'Между приемами пищи должна быть разница минимум час';
|
||||
final error = DioException(
|
||||
requestOptions: RequestOptions(
|
||||
path: '/api/profile/update_notification_settings',
|
||||
),
|
||||
type: DioExceptionType.badResponse,
|
||||
response: Response(
|
||||
requestOptions: RequestOptions(
|
||||
path: '/api/profile/update_notification_settings',
|
||||
),
|
||||
statusCode: 422,
|
||||
data: {
|
||||
'message': message,
|
||||
'errors': {
|
||||
'diary_reminder_notifications.second_reminder': [message],
|
||||
},
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
expect(notificationSettingsUpdateAlertMessage(error), message);
|
||||
});
|
||||
|
||||
test('uses a short message for other failures', () {
|
||||
expect(
|
||||
notificationSettingsUpdateAlertMessage(Exception('stack')),
|
||||
notificationSettingsSaveFailedMessage,
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:eatvio_app/core/app_config.dart';
|
||||
import 'package:eatvio_app/core/app_initializer.dart';
|
||||
import 'package:eatvio_app/core/locator_service.dart';
|
||||
@@ -7,6 +8,7 @@ import 'package:eatvio_app/features/menu/presentation/bloc/notification_settings
|
||||
import 'package:eatvio_app/features/menu/presentation/pages/notifications_settings_screen.dart';
|
||||
import 'package:eatvio_app/features/menu/repositories/notification_settings_repository.dart';
|
||||
import 'package:eatvio_app/generated/l10n.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
@@ -111,5 +113,55 @@ void main() {
|
||||
expect(find.textContaining('Ошибка'), findsOneWidget);
|
||||
expect(find.text('Повторить'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('shows an alert instead of replacing the screen when reminder times are too close', (tester) async {
|
||||
tester.view.physicalSize = const Size(800, 1600);
|
||||
tester.view.devicePixelRatio = 1.0;
|
||||
addTearDown(() => tester.view.resetPhysicalSize());
|
||||
|
||||
const message = 'Между приемами пищи должна быть разница минимум час';
|
||||
final repository = MockNotificationSettingsRepository();
|
||||
final cubit = NotificationSettingsCubit(notificationSettingsRepository: repository);
|
||||
|
||||
when(repository.getNotificationSettings()).thenAnswer((_) async => mockNotificationSettings);
|
||||
when(repository.updateNotificationSettings(any)).thenThrow(
|
||||
DioException(
|
||||
requestOptions: RequestOptions(path: '/api/profile/update_notification_settings'),
|
||||
type: DioExceptionType.badResponse,
|
||||
response: Response(
|
||||
requestOptions: RequestOptions(path: '/api/profile/update_notification_settings'),
|
||||
statusCode: 422,
|
||||
data: {
|
||||
'message': message,
|
||||
'errors': {
|
||||
'diary_reminder_notifications.second_reminder': [message],
|
||||
},
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
sl.registerLazySingleton<NotificationSettingsRepository>(() => repository);
|
||||
sl.registerLazySingleton<NotificationSettingsCubit>(() => cubit);
|
||||
|
||||
await tester.pumpWidget(wrap(const NotificationsSettingsScreen()));
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
|
||||
expect(find.text('Новые обновления'), findsOneWidget);
|
||||
|
||||
await tester.tap(find.byType(CupertinoSwitch).first);
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 300));
|
||||
|
||||
expect(find.text(message), findsOneWidget);
|
||||
expect(find.text('Новые обновления'), findsOneWidget);
|
||||
expect(find.text('Повторить'), findsNothing);
|
||||
|
||||
await tester.tap(find.text('OK'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('Новые обновления'), findsOneWidget);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user