mirror of
https://github.com/smmarty/friflex_flutter_starter.git
synced 2025-12-21 17:10:45 +00:00
feat(app): Добавить снекбар
This commit is contained in:
42
.gitignore
vendored
42
.gitignore
vendored
@@ -43,44 +43,4 @@ app.*.map.json
|
||||
/android/app/debug
|
||||
/android/app/profile
|
||||
/android/app/release
|
||||
ios/.gitignore
|
||||
ios/Podfile
|
||||
ios/Podfile.lock
|
||||
ios/Flutter/AppFrameworkInfo.plist
|
||||
ios/Flutter/Debug.xcconfig
|
||||
ios/Flutter/Release.xcconfig
|
||||
ios/Runner/AppDelegate.swift
|
||||
ios/Runner/Info.plist
|
||||
ios/Runner/Runner-Bridging-Header.h
|
||||
ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
|
||||
ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png
|
||||
ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png
|
||||
ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png
|
||||
ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png
|
||||
ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png
|
||||
ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png
|
||||
ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png
|
||||
ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png
|
||||
ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png
|
||||
ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png
|
||||
ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png
|
||||
ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png
|
||||
ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png
|
||||
ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png
|
||||
ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png
|
||||
ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json
|
||||
ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png
|
||||
ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png
|
||||
ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png
|
||||
ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md
|
||||
ios/Runner/Base.lproj/LaunchScreen.storyboard
|
||||
ios/Runner/Base.lproj/Main.storyboard
|
||||
ios/Runner.xcodeproj/project.pbxproj
|
||||
ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata
|
||||
ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
|
||||
ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
|
||||
ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
|
||||
ios/Runner.xcworkspace/contents.xcworkspacedata
|
||||
ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
|
||||
ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
|
||||
ios/RunnerTests/RunnerTests.swift
|
||||
|
||||
|
||||
12
.metadata
12
.metadata
@@ -4,7 +4,7 @@
|
||||
# This file should be version controlled and should not be manually edited.
|
||||
|
||||
version:
|
||||
revision: "ea121f8859e4b13e47a8f845e4586164519588bc"
|
||||
revision: "be698c48a6750c8cb8e61c740ca9991bb947aba2"
|
||||
channel: "stable"
|
||||
|
||||
project_type: app
|
||||
@@ -13,11 +13,11 @@ project_type: app
|
||||
migration:
|
||||
platforms:
|
||||
- platform: root
|
||||
create_revision: ea121f8859e4b13e47a8f845e4586164519588bc
|
||||
base_revision: ea121f8859e4b13e47a8f845e4586164519588bc
|
||||
- platform: web
|
||||
create_revision: ea121f8859e4b13e47a8f845e4586164519588bc
|
||||
base_revision: ea121f8859e4b13e47a8f845e4586164519588bc
|
||||
create_revision: be698c48a6750c8cb8e61c740ca9991bb947aba2
|
||||
base_revision: be698c48a6750c8cb8e61c740ca9991bb947aba2
|
||||
- platform: android
|
||||
create_revision: be698c48a6750c8cb8e61c740ca9991bb947aba2
|
||||
base_revision: be698c48a6750c8cb8e61c740ca9991bb947aba2
|
||||
|
||||
# User provided section
|
||||
|
||||
|
||||
5
.vscode/settings.json
vendored
5
.vscode/settings.json
vendored
@@ -1,5 +1,10 @@
|
||||
{
|
||||
"cSpell.words": [
|
||||
"Снекбар",
|
||||
"снекбара",
|
||||
"снекбарами",
|
||||
"снекбаров",
|
||||
"cupertino",
|
||||
"unawaited"
|
||||
]
|
||||
}
|
||||
@@ -13,24 +13,22 @@ final class _Dev {
|
||||
static const String baseUrl = 'https://dev';
|
||||
|
||||
static const List<int> _enviedkeysecretKey = <int>[
|
||||
4165345137,
|
||||
3493800243,
|
||||
502170426,
|
||||
1144186709,
|
||||
921404830,
|
||||
4081271781,
|
||||
];
|
||||
|
||||
static const List<int> _envieddatasecretKey = <int>[
|
||||
4165345045,
|
||||
3493800278,
|
||||
502170444,
|
||||
1144186673,
|
||||
921404923,
|
||||
4081271699,
|
||||
];
|
||||
|
||||
static final String secretKey = String.fromCharCodes(
|
||||
List<int>.generate(
|
||||
static final String secretKey = String.fromCharCodes(List<int>.generate(
|
||||
_envieddatasecretKey.length,
|
||||
(int i) => i,
|
||||
growable: false,
|
||||
).map((int i) => _envieddatasecretKey[i] ^ _enviedkeysecretKey[i]),
|
||||
);
|
||||
).map((int i) => _envieddatasecretKey[i] ^ _enviedkeysecretKey[i]));
|
||||
}
|
||||
|
||||
// coverage:ignore-file
|
||||
@@ -38,64 +36,60 @@ final class _Dev {
|
||||
// generated_from: env/prod.env
|
||||
final class _Prod {
|
||||
static const List<int> _enviedkeybaseUrl = <int>[
|
||||
1959698309,
|
||||
1370422491,
|
||||
1593239974,
|
||||
2980796982,
|
||||
3484307801,
|
||||
2340951854,
|
||||
4002048327,
|
||||
2957329110,
|
||||
3569108013,
|
||||
2324336979,
|
||||
691664904,
|
||||
2999310215,
|
||||
3830062036,
|
||||
1024953349,
|
||||
2723997296,
|
||||
2959773775,
|
||||
4255295633,
|
||||
114701674,
|
||||
1920572043,
|
||||
3423730200,
|
||||
3647804248,
|
||||
2815792265,
|
||||
3038381766,
|
||||
655048609,
|
||||
];
|
||||
|
||||
static const List<int> _envieddatabaseUrl = <int>[
|
||||
1959698413,
|
||||
1370422447,
|
||||
1593240018,
|
||||
2980796998,
|
||||
3484307754,
|
||||
2340951828,
|
||||
4002048360,
|
||||
2957329145,
|
||||
3569108061,
|
||||
2324336929,
|
||||
691664999,
|
||||
2999310307,
|
||||
3830062012,
|
||||
1024953457,
|
||||
2723997188,
|
||||
2959773759,
|
||||
4255295714,
|
||||
114701648,
|
||||
1920572068,
|
||||
3423730231,
|
||||
3647804200,
|
||||
2815792379,
|
||||
3038381737,
|
||||
655048645,
|
||||
];
|
||||
|
||||
static final String baseUrl = String.fromCharCodes(
|
||||
List<int>.generate(
|
||||
static final String baseUrl = String.fromCharCodes(List<int>.generate(
|
||||
_envieddatabaseUrl.length,
|
||||
(int i) => i,
|
||||
growable: false,
|
||||
).map((int i) => _envieddatabaseUrl[i] ^ _enviedkeybaseUrl[i]),
|
||||
);
|
||||
).map((int i) => _envieddatabaseUrl[i] ^ _enviedkeybaseUrl[i]));
|
||||
|
||||
static const List<int> _enviedkeysecretKey = <int>[
|
||||
4268709792,
|
||||
3715718791,
|
||||
3691995036,
|
||||
2677812110,
|
||||
359753139,
|
||||
3208048313,
|
||||
1722903860,
|
||||
3044498179,
|
||||
];
|
||||
|
||||
static const List<int> _envieddatasecretKey = <int>[
|
||||
4268709840,
|
||||
3715718901,
|
||||
3691995123,
|
||||
2677812202,
|
||||
359753155,
|
||||
3208048331,
|
||||
1722903899,
|
||||
3044498279,
|
||||
];
|
||||
|
||||
static final String secretKey = String.fromCharCodes(
|
||||
List<int>.generate(
|
||||
static final String secretKey = String.fromCharCodes(List<int>.generate(
|
||||
_envieddatasecretKey.length,
|
||||
(int i) => i,
|
||||
growable: false,
|
||||
).map((int i) => _envieddatasecretKey[i] ^ _enviedkeysecretKey[i]),
|
||||
);
|
||||
).map((int i) => _envieddatasecretKey[i] ^ _enviedkeysecretKey[i]));
|
||||
}
|
||||
|
||||
// coverage:ignore-file
|
||||
@@ -103,66 +97,62 @@ final class _Prod {
|
||||
// generated_from: env/stage.env
|
||||
final class _Stage {
|
||||
static const List<int> _enviedkeybaseUrl = <int>[
|
||||
3824074796,
|
||||
1785932277,
|
||||
700105518,
|
||||
2614901365,
|
||||
2850858902,
|
||||
3082107206,
|
||||
3784178565,
|
||||
815141967,
|
||||
4277092750,
|
||||
3942345021,
|
||||
1481955512,
|
||||
3678805330,
|
||||
206487437,
|
||||
2791397647,
|
||||
1739207173,
|
||||
306419752,
|
||||
1371918084,
|
||||
4062400488,
|
||||
3004897854,
|
||||
2820011348,
|
||||
1751321626,
|
||||
3103957517,
|
||||
2168627914,
|
||||
1003673382,
|
||||
1168070657,
|
||||
568662299,
|
||||
];
|
||||
|
||||
static const List<int> _envieddatabaseUrl = <int>[
|
||||
3824074820,
|
||||
1785932161,
|
||||
700105562,
|
||||
2614901253,
|
||||
2850858981,
|
||||
3082107260,
|
||||
3784178602,
|
||||
815141984,
|
||||
4277092861,
|
||||
3942345033,
|
||||
1481955545,
|
||||
3678805301,
|
||||
206487528,
|
||||
2791397735,
|
||||
1739207281,
|
||||
306419804,
|
||||
1371918196,
|
||||
4062400411,
|
||||
3004897796,
|
||||
2820011387,
|
||||
1751321653,
|
||||
3103957630,
|
||||
2168627902,
|
||||
1003673415,
|
||||
1168070758,
|
||||
568662398,
|
||||
];
|
||||
|
||||
static final String baseUrl = String.fromCharCodes(
|
||||
List<int>.generate(
|
||||
static final String baseUrl = String.fromCharCodes(List<int>.generate(
|
||||
_envieddatabaseUrl.length,
|
||||
(int i) => i,
|
||||
growable: false,
|
||||
).map((int i) => _envieddatabaseUrl[i] ^ _enviedkeybaseUrl[i]),
|
||||
);
|
||||
).map((int i) => _envieddatabaseUrl[i] ^ _enviedkeybaseUrl[i]));
|
||||
|
||||
static const List<int> _enviedkeysecretKey = <int>[
|
||||
1473916388,
|
||||
2313056220,
|
||||
2069119783,
|
||||
32407210,
|
||||
317937387,
|
||||
2132342089,
|
||||
2579069434,
|
||||
3904165526,
|
||||
3391356107,
|
||||
1192880530,
|
||||
];
|
||||
|
||||
static const List<int> _envieddatasecretKey = <int>[
|
||||
1473916311,
|
||||
2313056168,
|
||||
2069119814,
|
||||
32407245,
|
||||
317937294,
|
||||
2132342074,
|
||||
2579069326,
|
||||
3904165623,
|
||||
3391356076,
|
||||
1192880631,
|
||||
];
|
||||
|
||||
static final String secretKey = String.fromCharCodes(
|
||||
List<int>.generate(
|
||||
static final String secretKey = String.fromCharCodes(List<int>.generate(
|
||||
_envieddatasecretKey.length,
|
||||
(int i) => i,
|
||||
growable: false,
|
||||
).map((int i) => _envieddatasecretKey[i] ^ _enviedkeysecretKey[i]),
|
||||
);
|
||||
).map((int i) => _envieddatasecretKey[i] ^ _enviedkeysecretKey[i]));
|
||||
}
|
||||
|
||||
@@ -6,4 +6,10 @@ extension AppColorsScheme on ColorScheme {
|
||||
|
||||
// Тестовый цвет
|
||||
Color get testColor => _isDark ? Colors.green : Colors.red;
|
||||
|
||||
/// Цвет заднего фона снекбара с ошибкой
|
||||
Color get errorSnackbarBackground => const Color(0xFFD24720);
|
||||
|
||||
/// Цвет заднего фона снекбара с успехом
|
||||
Color get successSnackbarBackground => const Color(0xFF6FB62C);
|
||||
}
|
||||
|
||||
278
lib/app/ui_kit/app_snackbar.dart
Normal file
278
lib/app/ui_kit/app_snackbar.dart
Normal file
@@ -0,0 +1,278 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:friflex_starter/app/app_context_ext.dart';
|
||||
import 'package:friflex_starter/app/theme/app_colors_scheme.dart';
|
||||
import 'package:friflex_starter/app/ui_kit/app_box.dart';
|
||||
|
||||
/// {@template app_snackbar}
|
||||
/// Перечисление для типов снекбаров
|
||||
/// Используется для определения цвета и назначения снекбара
|
||||
/// {@endtemplate}
|
||||
enum TypeSnackBar {
|
||||
/// Снекбар с успехом
|
||||
success,
|
||||
|
||||
/// Снекбар с ошибкой
|
||||
error,
|
||||
}
|
||||
|
||||
/// {@template app_snackbar}
|
||||
/// Менеджер для управления снекбарами
|
||||
/// Используется для показа снекбаров в верхней части экрана
|
||||
/// Имеет статические методы для показа снекбаров с разными типами
|
||||
/// [showError] - для показа снекбара с ошибкой,
|
||||
/// [showSuccess] - для показа снекбара с успехом
|
||||
/// {@endtemplate}
|
||||
class AppSnackBar extends StatefulWidget {
|
||||
/// {@macro app_snackbar}
|
||||
const AppSnackBar._({
|
||||
required this.message,
|
||||
required this.type,
|
||||
required this.displayDuration,
|
||||
this.onDismiss,
|
||||
});
|
||||
|
||||
/// Сообщение, которое будет отображаться в снекбаре
|
||||
final String message;
|
||||
|
||||
/// Тип снекбара, определяющий его цвет и назначение
|
||||
final TypeSnackBar type;
|
||||
|
||||
/// Продолжительность отображения снекбара
|
||||
final Duration displayDuration;
|
||||
|
||||
/// Функция, вызываемая при закрытии снекбара
|
||||
final VoidCallback? onDismiss;
|
||||
|
||||
@override
|
||||
State<AppSnackBar> createState() => _AppSnackBarState();
|
||||
|
||||
/// Показать снекбар с ошибкой
|
||||
/// [context] - контекст, в котором будет показан снекбар
|
||||
/// [message] - сообщение, которое будет отображаться в снекбаре
|
||||
/// [displayDuration] - продолжительность отображения снекбара
|
||||
/// По умолчанию 3 секунды
|
||||
static void showError(
|
||||
BuildContext context, {
|
||||
required String message,
|
||||
Duration displayDuration = const Duration(seconds: 3),
|
||||
}) {
|
||||
_show(
|
||||
context: context,
|
||||
message: message,
|
||||
type: TypeSnackBar.error,
|
||||
displayDuration: displayDuration,
|
||||
);
|
||||
}
|
||||
|
||||
/// Показать снекбар с успехом
|
||||
/// [context] - контекст, в котором будет показан снекбар
|
||||
/// [message] - сообщение, которое будет отображаться в снекбаре
|
||||
/// [displayDuration] - продолжительность отображения снекбара
|
||||
/// По умолчанию 3 секунды
|
||||
static void showSuccess({
|
||||
required BuildContext context,
|
||||
required String message,
|
||||
Duration displayDuration = const Duration(seconds: 3),
|
||||
}) {
|
||||
_show(
|
||||
context: context,
|
||||
message: message,
|
||||
type: TypeSnackBar.success,
|
||||
displayDuration: displayDuration,
|
||||
);
|
||||
}
|
||||
|
||||
/// Приватный метод для показа снекбара
|
||||
/// Используется статическими методами [showError] и [showSuccess]
|
||||
static void _show({
|
||||
required BuildContext context,
|
||||
required String message,
|
||||
required TypeSnackBar type,
|
||||
required Duration displayDuration,
|
||||
}) {
|
||||
// Удаляем предыдущий снекбар
|
||||
_removeCurrentSnackBar();
|
||||
|
||||
if (!context.mounted) return;
|
||||
|
||||
final overlay = Overlay.of(context);
|
||||
final overlayEntry = OverlayEntry(
|
||||
builder: (context) => AppSnackBar._(
|
||||
message: message,
|
||||
type: type,
|
||||
displayDuration: displayDuration,
|
||||
onDismiss: _removeCurrentSnackBar,
|
||||
),
|
||||
);
|
||||
|
||||
_currentSnackBar = overlayEntry;
|
||||
overlay.insert(overlayEntry);
|
||||
}
|
||||
|
||||
static OverlayEntry? _currentSnackBar;
|
||||
|
||||
static void _removeCurrentSnackBar() {
|
||||
_currentSnackBar?.remove();
|
||||
_currentSnackBar = null;
|
||||
}
|
||||
}
|
||||
|
||||
class _AppSnackBarState extends State<AppSnackBar>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late AnimationController _animationController;
|
||||
late Animation<double> _slideAnimation;
|
||||
Timer? _dismissTimer;
|
||||
bool _isInitialized = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_animationController = AnimationController(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
vsync: this,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
if (!_isInitialized) {
|
||||
_initAnimation();
|
||||
_startDismissTimer();
|
||||
_isInitialized = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// Инициализация анимации для снекбара
|
||||
/// Используется для определения начальной и конечной позиции снекбара
|
||||
/// Начальная позиция - за пределами экрана, конечная - 15 пикселей ниже верхнего отступа
|
||||
void _initAnimation() {
|
||||
final topPadding = MediaQuery.of(context).padding.top;
|
||||
// Начальная позиция снекбара - за пределами экрана
|
||||
final startPosition = -200.0;
|
||||
// Конечная позиция снекбара - 15 пикселей ниже верхнего отступа
|
||||
final endPosition = topPadding + 15;
|
||||
// Создание анимации с использованием Tween
|
||||
_slideAnimation = Tween<double>(begin: startPosition, end: endPosition)
|
||||
.animate(
|
||||
CurvedAnimation(parent: _animationController, curve: Curves.easeOut),
|
||||
);
|
||||
|
||||
_animationController.forward();
|
||||
}
|
||||
|
||||
/// Запуск таймера для автоматического закрытия снекбара
|
||||
/// Таймер срабатывает по истечении [widget.displayDuration]
|
||||
/// и вызывает метод [_dismiss] для закрытия снекбара
|
||||
void _startDismissTimer() {
|
||||
_dismissTimer = Timer(widget.displayDuration, _dismiss);
|
||||
}
|
||||
|
||||
/// Закрытие снекбара
|
||||
/// Отменяет таймер, если он существует, и запускает обратную анимацию
|
||||
/// После завершения анимации вызывает функцию [widget.onDismiss], если она задана
|
||||
/// Если виджет не смонтирован, ничего не делает
|
||||
void _dismiss() {
|
||||
if (!mounted) return;
|
||||
|
||||
_dismissTimer?.cancel();
|
||||
_animationController.reverse().then((_) {
|
||||
if (mounted) {
|
||||
widget.onDismiss?.call();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_dismissTimer?.cancel();
|
||||
_animationController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AnimatedBuilder(
|
||||
animation: _slideAnimation,
|
||||
builder: (context, child) {
|
||||
return Positioned(
|
||||
top: _slideAnimation.value,
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: Center(
|
||||
child: GestureDetector(
|
||||
onTap: _dismiss,
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child: Container(
|
||||
constraints: const BoxConstraints(maxWidth: 350),
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16),
|
||||
decoration: BoxDecoration(
|
||||
color: _getBackgroundColor(widget.type),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 16,
|
||||
horizontal: 16,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_Icon(type: widget.type),
|
||||
const WBox(10),
|
||||
Flexible(
|
||||
child: Text(
|
||||
widget.message,
|
||||
maxLines: 3,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Получение цвета фона снекбара в зависимости от его типа
|
||||
/// Возвращает [Color] в зависимости от типа снекбара
|
||||
/// [TypeSnackBar.success] - цвет успеха
|
||||
/// [TypeSnackBar.error] - цвет ошибки
|
||||
Color _getBackgroundColor(TypeSnackBar type) {
|
||||
return switch (type) {
|
||||
TypeSnackBar.success => context.colors.successSnackbarBackground,
|
||||
TypeSnackBar.error => context.colors.errorSnackbarBackground,
|
||||
};
|
||||
}
|
||||
}
|
||||
/// {@template _Icon}
|
||||
/// Виджет для отображения иконки в снекбаре
|
||||
/// Используется для отображения иконки в зависимости от типа снекбара
|
||||
/// {@endtemplate}
|
||||
class _Icon extends StatelessWidget {
|
||||
/// {@macro _Icon}
|
||||
/// Создает экземпляр иконки для снекбара
|
||||
/// Принимает [type] - тип снекбара, определяющий иконку
|
||||
/// Используется для отображения иконки успеха или ошибки
|
||||
const _Icon({required this.type});
|
||||
|
||||
/// Тип снекбара, определяющий иконку
|
||||
final TypeSnackBar type;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return switch (type) {
|
||||
TypeSnackBar.success => Icon(
|
||||
Icons.check_circle,
|
||||
color: Colors.white,
|
||||
size: 32,
|
||||
),
|
||||
TypeSnackBar.error => Icon(Icons.error, color: Colors.white, size: 32),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'package:friflex_starter/features/debug/screens/components_screen.dart';
|
||||
import 'package:friflex_starter/features/debug/screens/debug_screen.dart';
|
||||
import 'package:friflex_starter/features/debug/screens/icons_screen.dart';
|
||||
import 'package:friflex_starter/features/debug/screens/lang_screen.dart';
|
||||
@@ -18,6 +19,7 @@ abstract final class DebugRoutes {
|
||||
static const String iconsScreenName = 'icons_screen';
|
||||
static const String themeScreenName = 'theme_screen';
|
||||
static const String langScreenName = 'lang_screen';
|
||||
static const String componentsScreenName = 'components_screen';
|
||||
|
||||
/// Пути к экранам
|
||||
static const String debugScreenPath = '/debug';
|
||||
@@ -26,6 +28,7 @@ abstract final class DebugRoutes {
|
||||
static const String iconsScreenPath = 'debug/icons';
|
||||
static const String themeScreenPath = 'debug/theme';
|
||||
static const String langScreenPath = 'debug/lang';
|
||||
static const String componentsScreenPath = 'debug/components';
|
||||
|
||||
/// Метод для создания роутов для отладки
|
||||
///
|
||||
@@ -62,6 +65,11 @@ abstract final class DebugRoutes {
|
||||
name: langScreenName,
|
||||
builder: (context, state) => const LangScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: componentsScreenPath,
|
||||
name: componentsScreenName,
|
||||
builder: (context, state) => const ComponentsScreen(),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
52
lib/features/debug/screens/components_screen.dart
Normal file
52
lib/features/debug/screens/components_screen.dart
Normal file
@@ -0,0 +1,52 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:friflex_starter/app/ui_kit/app_box.dart';
|
||||
import 'package:friflex_starter/app/ui_kit/app_snackbar.dart';
|
||||
|
||||
/// {@template ComponentsScreen}
|
||||
/// Экран для демонстрации компонентов приложения.
|
||||
/// {@endtemplate}
|
||||
class ComponentsScreen extends StatefulWidget {
|
||||
/// {@macro ComponentsScreen}
|
||||
const ComponentsScreen({super.key});
|
||||
|
||||
@override
|
||||
State<ComponentsScreen> createState() => _ComponentsScreenState();
|
||||
}
|
||||
|
||||
class _ComponentsScreenState extends State<ComponentsScreen> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Компоненты')),
|
||||
body: Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const HBox(16),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
AppSnackBar.showError(
|
||||
context,
|
||||
message:
|
||||
'Произошла ошибка, это просто длинное сообщение, для проверки, которое занимает 3 строчки',
|
||||
);
|
||||
},
|
||||
child: const Text('Показать снекбар с ошибкой'),
|
||||
),
|
||||
const HBox(16),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
AppSnackBar.showSuccess(
|
||||
context: context,
|
||||
message:
|
||||
'Все супер, это просто длинное сообщение, для проверки, которое занимает 3 строчки',
|
||||
);
|
||||
},
|
||||
child: const Text('Показать снекбар с успехом'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,12 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:friflex_starter/app/app_box.dart';
|
||||
import 'package:friflex_starter/app/ui_kit/app_box.dart';
|
||||
import 'package:friflex_starter/app/app_context_ext.dart';
|
||||
import 'package:friflex_starter/features/debug/debug_routes.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
/// {@template debug_screen}
|
||||
/// Экран для отладки приложения
|
||||
/// {@endtemplate}
|
||||
class DebugScreen extends StatelessWidget {
|
||||
const DebugScreen({super.key});
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:friflex_starter/app/app_box.dart';
|
||||
import 'package:friflex_starter/app/ui_kit/app_box.dart';
|
||||
import 'package:friflex_starter/gen/assets.gen.dart';
|
||||
|
||||
/// {@template IconsScreen}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:friflex_starter/app/app_box.dart';
|
||||
import 'package:friflex_starter/app/ui_kit/app_box.dart';
|
||||
import 'package:friflex_starter/features/main/presentation/main_routes.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ class $AssetsFontsGen {
|
||||
montserratExtraBold,
|
||||
montserratMedium,
|
||||
montserratRegular,
|
||||
montserratSemiBold,
|
||||
montserratSemiBold
|
||||
];
|
||||
}
|
||||
|
||||
@@ -63,7 +63,7 @@ class $AssetsLottieGen {
|
||||
}
|
||||
|
||||
class Assets {
|
||||
const Assets._();
|
||||
Assets._();
|
||||
|
||||
static const $AssetsFontsGen fonts = $AssetsFontsGen();
|
||||
static const $AssetsIconsGen icons = $AssetsIconsGen();
|
||||
@@ -71,11 +71,17 @@ class Assets {
|
||||
}
|
||||
|
||||
class SvgGenImage {
|
||||
const SvgGenImage(this._assetName, {this.size, this.flavors = const {}})
|
||||
: _isVecFormat = false;
|
||||
const SvgGenImage(
|
||||
this._assetName, {
|
||||
this.size,
|
||||
this.flavors = const {},
|
||||
}) : _isVecFormat = false;
|
||||
|
||||
const SvgGenImage.vec(this._assetName, {this.size, this.flavors = const {}})
|
||||
: _isVecFormat = true;
|
||||
const SvgGenImage.vec(
|
||||
this._assetName, {
|
||||
this.size,
|
||||
this.flavors = const {},
|
||||
}) : _isVecFormat = true;
|
||||
|
||||
final String _assetName;
|
||||
final Size? size;
|
||||
@@ -129,8 +135,7 @@ class SvgGenImage {
|
||||
placeholderBuilder: placeholderBuilder,
|
||||
semanticsLabel: semanticsLabel,
|
||||
excludeFromSemantics: excludeFromSemantics,
|
||||
colorFilter:
|
||||
colorFilter ??
|
||||
colorFilter: colorFilter ??
|
||||
(color == null ? null : ColorFilter.mode(color, colorBlendMode)),
|
||||
clipBehavior: clipBehavior,
|
||||
cacheColorFilter: cacheColorFilter,
|
||||
@@ -143,7 +148,10 @@ class SvgGenImage {
|
||||
}
|
||||
|
||||
class LottieGenImage {
|
||||
const LottieGenImage(this._assetName, {this.flavors = const {}});
|
||||
const LottieGenImage(
|
||||
this._assetName, {
|
||||
this.flavors = const {},
|
||||
});
|
||||
|
||||
final String _assetName;
|
||||
final Set<String> flavors;
|
||||
@@ -160,8 +168,11 @@ class LottieGenImage {
|
||||
_lottie.LottieImageProviderFactory? imageProviderFactory,
|
||||
Key? key,
|
||||
AssetBundle? bundle,
|
||||
Widget Function(BuildContext, Widget, _lottie.LottieComposition?)?
|
||||
frameBuilder,
|
||||
Widget Function(
|
||||
BuildContext,
|
||||
Widget,
|
||||
_lottie.LottieComposition?,
|
||||
)? frameBuilder,
|
||||
ImageErrorWidgetBuilder? errorBuilder,
|
||||
double? width,
|
||||
double? height,
|
||||
@@ -171,9 +182,6 @@ class LottieGenImage {
|
||||
bool? addRepaintBoundary,
|
||||
FilterQuality? filterQuality,
|
||||
void Function(String)? onWarning,
|
||||
_lottie.LottieDecoder? decoder,
|
||||
_lottie.RenderCache? renderCache,
|
||||
bool? backgroundLoading,
|
||||
}) {
|
||||
return _lottie.Lottie.asset(
|
||||
_assetName,
|
||||
@@ -198,9 +206,6 @@ class LottieGenImage {
|
||||
addRepaintBoundary: addRepaintBoundary,
|
||||
filterQuality: filterQuality,
|
||||
onWarning: onWarning,
|
||||
decoder: decoder,
|
||||
renderCache: renderCache,
|
||||
backgroundLoading: backgroundLoading,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
/// {@template HBox}
|
||||
/// HBox виджет для вертикального отступа (Надстройка над SizedBox)
|
||||
/// [height] - высота отступа
|
||||
/// [key] - ключ виджета
|
||||
/// {@endtemplate}
|
||||
class HBox extends SizedBox {
|
||||
/// {@macro HBox}
|
||||
const HBox(double height, {super.key}) : super(height: height);
|
||||
}
|
||||
|
||||
/// {@template WBox}
|
||||
/// WBox виджет для вертикального отступа (Надстройка над SizedBox)
|
||||
/// [width] - ширина отступа
|
||||
/// [key] - ключ виджета
|
||||
/// {@endtemplate}
|
||||
class WBox extends SizedBox {
|
||||
/// {@macro WBox}
|
||||
const WBox(double width, {super.key}) : super(width: width);
|
||||
}
|
||||
@@ -53,7 +53,7 @@ dev_dependencies:
|
||||
|
||||
flutter:
|
||||
uses-material-design: true
|
||||
generate: false
|
||||
generate: true
|
||||
assets:
|
||||
- assets/icons/
|
||||
- assets/fonts/
|
||||
|
||||
524
test/components/app_snackbar_test.dart
Normal file
524
test/components/app_snackbar_test.dart
Normal file
@@ -0,0 +1,524 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:friflex_starter/app/ui_kit/app_snackbar.dart';
|
||||
|
||||
/// Тесты для виджета AppSnackBar
|
||||
void main() {
|
||||
group('AppSnackBar Widget Tests', () {
|
||||
late Widget testApp;
|
||||
|
||||
/// Создание мок-темы с необходимыми цветами для снекбара
|
||||
ColorScheme createMockColorScheme() {
|
||||
return const ColorScheme.light().copyWith(
|
||||
// Добавляем кастомные цвета через extension методы
|
||||
);
|
||||
}
|
||||
|
||||
/// Создание мок-темы с правильными стилями текста
|
||||
TextTheme createMockTextTheme() {
|
||||
return const TextTheme();
|
||||
}
|
||||
|
||||
setUp(() {
|
||||
testApp = MaterialApp(
|
||||
theme: ThemeData(
|
||||
colorScheme: createMockColorScheme(),
|
||||
textTheme: createMockTextTheme(),
|
||||
),
|
||||
home: const Scaffold(body: Center(child: Text('Тестовый экран'))),
|
||||
);
|
||||
});
|
||||
|
||||
testTester(String description, WidgetTesterCallback callback) {
|
||||
testWidgets(description, (tester) async {
|
||||
await tester.pumpWidget(testApp);
|
||||
await callback(tester);
|
||||
});
|
||||
}
|
||||
|
||||
group('AppSnackBar.showError', () {
|
||||
testTester('показывает снекбар с ошибкой', (tester) async {
|
||||
const errorMessage = 'Произошла ошибка';
|
||||
|
||||
// Показываем снекбар с ошибкой
|
||||
AppSnackBar.showError(
|
||||
tester.element(find.byType(Scaffold)),
|
||||
message: errorMessage,
|
||||
);
|
||||
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
|
||||
// Проверяем, что снекбар отображается
|
||||
expect(find.byType(AppSnackBar), findsOneWidget);
|
||||
expect(find.text(errorMessage), findsOneWidget);
|
||||
|
||||
// Проверяем наличие иконки ошибки
|
||||
expect(find.byType(Icon), findsOneWidget);
|
||||
});
|
||||
|
||||
testTester('показывает снекбар с кастомной продолжительностью', (
|
||||
tester,
|
||||
) async {
|
||||
const errorMessage = 'Кастомная ошибка';
|
||||
const customDuration = Duration(seconds: 5);
|
||||
|
||||
AppSnackBar.showError(
|
||||
tester.element(find.byType(Scaffold)),
|
||||
message: errorMessage,
|
||||
displayDuration: customDuration,
|
||||
);
|
||||
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
|
||||
expect(find.byType(AppSnackBar), findsOneWidget);
|
||||
expect(find.text(errorMessage), findsOneWidget);
|
||||
});
|
||||
|
||||
testTester('заменяет предыдущий снекбар при повторном вызове', (
|
||||
tester,
|
||||
) async {
|
||||
const firstMessage = 'Первая ошибка';
|
||||
const secondMessage = 'Вторая ошибка';
|
||||
|
||||
// Показываем первый снекбар
|
||||
AppSnackBar.showError(
|
||||
tester.element(find.byType(Scaffold)),
|
||||
message: firstMessage,
|
||||
);
|
||||
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
|
||||
expect(find.text(firstMessage), findsOneWidget);
|
||||
|
||||
// Показываем второй снекбар
|
||||
AppSnackBar.showError(
|
||||
tester.element(find.byType(Scaffold)),
|
||||
message: secondMessage,
|
||||
);
|
||||
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
|
||||
// Проверяем, что показывается только второй снекбар
|
||||
expect(find.text(firstMessage), findsNothing);
|
||||
expect(find.text(secondMessage), findsOneWidget);
|
||||
expect(find.byType(AppSnackBar), findsOneWidget);
|
||||
});
|
||||
});
|
||||
|
||||
group('AppSnackBar.showSuccess', () {
|
||||
testTester('показывает снекбар с успехом', (tester) async {
|
||||
const successMessage = 'Операция выполнена успешно';
|
||||
|
||||
AppSnackBar.showSuccess(
|
||||
context: tester.element(find.byType(Scaffold)),
|
||||
message: successMessage,
|
||||
);
|
||||
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
|
||||
expect(find.byType(AppSnackBar), findsOneWidget);
|
||||
expect(find.text(successMessage), findsOneWidget);
|
||||
expect(find.byType(Icon), findsOneWidget);
|
||||
});
|
||||
|
||||
testTester('показывает снекбар с кастомной продолжительностью', (
|
||||
tester,
|
||||
) async {
|
||||
const successMessage = 'Кастомный успех';
|
||||
const customDuration = Duration(seconds: 2);
|
||||
|
||||
AppSnackBar.showSuccess(
|
||||
context: tester.element(find.byType(Scaffold)),
|
||||
message: successMessage,
|
||||
displayDuration: customDuration,
|
||||
);
|
||||
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
|
||||
expect(find.byType(AppSnackBar), findsOneWidget);
|
||||
expect(find.text(successMessage), findsOneWidget);
|
||||
});
|
||||
});
|
||||
|
||||
group('AppSnackBar виджет поведение', () {
|
||||
testTester('показывает анимацию появления', (tester) async {
|
||||
const message = 'Тестовое сообщение';
|
||||
|
||||
AppSnackBar.showError(
|
||||
tester.element(find.byType(Scaffold)),
|
||||
message: message,
|
||||
);
|
||||
|
||||
// Проверяем, что анимация началась
|
||||
await tester.pump();
|
||||
expect(find.byType(AnimatedBuilder), findsAtLeastNWidgets(1));
|
||||
|
||||
// Ждем завершения анимации
|
||||
await tester.pump(const Duration(milliseconds: 300));
|
||||
|
||||
expect(find.byType(AppSnackBar), findsOneWidget);
|
||||
expect(find.text(message), findsOneWidget);
|
||||
});
|
||||
|
||||
testTester('закрывается при тапе', (tester) async {
|
||||
const message = 'Тап для закрытия';
|
||||
|
||||
AppSnackBar.showError(
|
||||
tester.element(find.byType(Scaffold)),
|
||||
message: message,
|
||||
);
|
||||
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 350));
|
||||
|
||||
expect(find.byType(AppSnackBar), findsOneWidget);
|
||||
|
||||
// Тапаем по снекбару
|
||||
await tester.tap(find.byType(GestureDetector));
|
||||
await tester.pump();
|
||||
|
||||
// Ждем завершения анимации закрытия и удаления из Overlay
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Проверяем, что снекбар исчез
|
||||
expect(find.byType(AppSnackBar), findsNothing);
|
||||
});
|
||||
|
||||
testTester('автоматически закрывается через указанное время', (
|
||||
tester,
|
||||
) async {
|
||||
const message = 'Автозакрытие';
|
||||
const duration = Duration(milliseconds: 500);
|
||||
|
||||
AppSnackBar.showError(
|
||||
tester.element(find.byType(Scaffold)),
|
||||
message: message,
|
||||
displayDuration: duration,
|
||||
);
|
||||
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 350));
|
||||
|
||||
expect(find.byType(AppSnackBar), findsOneWidget);
|
||||
|
||||
// Ждем автоматического закрытия
|
||||
await tester.pump(duration);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byType(AppSnackBar), findsNothing);
|
||||
});
|
||||
|
||||
testTester('отображает длинное сообщение с ellipsis', (tester) async {
|
||||
const longMessage =
|
||||
'Это очень длинное сообщение, которое должно отображаться в нескольких строках и обрезаться с помощью ellipsis если оно слишком длинное для отображения в виджете снекбара';
|
||||
|
||||
AppSnackBar.showError(
|
||||
tester.element(find.byType(Scaffold)),
|
||||
message: longMessage,
|
||||
);
|
||||
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
|
||||
expect(find.byType(AppSnackBar), findsOneWidget);
|
||||
|
||||
// Проверяем, что текст имеет ограничение по строкам
|
||||
final textWidget = tester.widget<Text>(find.text(longMessage));
|
||||
expect(textWidget.maxLines, equals(3));
|
||||
expect(textWidget.overflow, equals(TextOverflow.ellipsis));
|
||||
});
|
||||
|
||||
testTester('имеет правильную структуру виджета', (tester) async {
|
||||
const message = 'Структура';
|
||||
|
||||
AppSnackBar.showError(
|
||||
tester.element(find.byType(Scaffold)),
|
||||
message: message,
|
||||
);
|
||||
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
|
||||
// Проверяем основную структуру
|
||||
expect(find.byType(Positioned), findsOneWidget);
|
||||
expect(find.byType(Material), findsAtLeastNWidgets(1));
|
||||
expect(find.byType(GestureDetector), findsOneWidget);
|
||||
expect(find.byType(Container), findsAtLeastNWidgets(1));
|
||||
expect(find.byType(Row), findsOneWidget);
|
||||
expect(find.byType(Icon), findsOneWidget);
|
||||
expect(find.byType(Text), findsAtLeastNWidgets(1));
|
||||
});
|
||||
|
||||
testTester('имеет правильные отступы и размеры', (tester) async {
|
||||
const message = 'Размеры';
|
||||
|
||||
AppSnackBar.showError(
|
||||
tester.element(find.byType(Scaffold)),
|
||||
message: message,
|
||||
);
|
||||
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
|
||||
// Проверяем ограничения максимальной ширины
|
||||
final constraintsWidget = tester.widget<Container>(
|
||||
find.descendant(
|
||||
of: find.byType(GestureDetector),
|
||||
matching: find.byType(Container),
|
||||
),
|
||||
);
|
||||
|
||||
expect(constraintsWidget.constraints?.maxWidth, equals(350));
|
||||
|
||||
// Проверяем отступы
|
||||
expect(
|
||||
constraintsWidget.margin,
|
||||
equals(const EdgeInsets.symmetric(horizontal: 16)),
|
||||
);
|
||||
expect(
|
||||
constraintsWidget.padding,
|
||||
equals(const EdgeInsets.symmetric(vertical: 16, horizontal: 16)),
|
||||
);
|
||||
});
|
||||
|
||||
testTester('имеет правильное скругление углов', (tester) async {
|
||||
const message = 'Скругление';
|
||||
|
||||
AppSnackBar.showError(
|
||||
tester.element(find.byType(Scaffold)),
|
||||
message: message,
|
||||
);
|
||||
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
|
||||
final container = tester.widget<Container>(
|
||||
find.descendant(
|
||||
of: find.byType(GestureDetector),
|
||||
matching: find.byType(Container),
|
||||
),
|
||||
);
|
||||
|
||||
final decoration = container.decoration as BoxDecoration;
|
||||
expect(decoration.borderRadius, equals(BorderRadius.circular(16)));
|
||||
});
|
||||
});
|
||||
|
||||
group('Управление состоянием', () {
|
||||
testTester('правильно обрабатывает отсутствие mounted контекста', (
|
||||
tester,
|
||||
) async {
|
||||
const message = 'Unmounted контекст';
|
||||
|
||||
// Создаем виджет, который будет удален
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Builder(
|
||||
builder: (context) {
|
||||
return Scaffold(
|
||||
body: ElevatedButton(
|
||||
onPressed: () {
|
||||
AppSnackBar.showError(context, message: message);
|
||||
},
|
||||
child: const Text('Show SnackBar'),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// Нажимаем кнопку для показа снекбара
|
||||
await tester.tap(find.byType(ElevatedButton));
|
||||
await tester.pump();
|
||||
|
||||
// Заменяем весь виджет (контекст становится unmounted)
|
||||
await tester.pumpWidget(
|
||||
const MaterialApp(home: Scaffold(body: Text('New Screen'))),
|
||||
);
|
||||
|
||||
// Проверяем, что ошибки не возникает
|
||||
expect(tester.takeException(), isNull);
|
||||
});
|
||||
|
||||
testTester('правильно очищает ресурсы при dispose', (tester) async {
|
||||
const message = 'Очистка ресурсов';
|
||||
|
||||
AppSnackBar.showError(
|
||||
tester.element(find.byType(Scaffold)),
|
||||
message: message,
|
||||
);
|
||||
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
|
||||
expect(find.byType(AppSnackBar), findsOneWidget);
|
||||
|
||||
// Заменяем весь виджет для вызова dispose
|
||||
await tester.pumpWidget(
|
||||
const MaterialApp(home: Scaffold(body: Text('New Screen'))),
|
||||
);
|
||||
|
||||
// Проверяем, что ошибки не возникает при dispose
|
||||
expect(tester.takeException(), isNull);
|
||||
});
|
||||
});
|
||||
|
||||
group('Управление снекбарами', () {
|
||||
testTester('правильно заменяет предыдущий снекбар новым', (tester) async {
|
||||
const firstMessage = 'Первый снекбар';
|
||||
const secondMessage = 'Второй снекбар';
|
||||
|
||||
// Показываем первый снекбар
|
||||
AppSnackBar.showError(
|
||||
tester.element(find.byType(Scaffold)),
|
||||
message: firstMessage,
|
||||
);
|
||||
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
|
||||
expect(find.text(firstMessage), findsOneWidget);
|
||||
|
||||
// Показываем второй снекбар (должен заменить первый)
|
||||
AppSnackBar.showError(
|
||||
tester.element(find.byType(Scaffold)),
|
||||
message: secondMessage,
|
||||
);
|
||||
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
|
||||
// Проверяем, что остался только второй снекбар
|
||||
expect(find.text(firstMessage), findsNothing);
|
||||
expect(find.text(secondMessage), findsOneWidget);
|
||||
});
|
||||
|
||||
testTester('правильно заменяет снекбар error на success', (tester) async {
|
||||
const errorMessage = 'Ошибка';
|
||||
const successMessage = 'Успех';
|
||||
|
||||
// Показываем снекбар с ошибкой
|
||||
AppSnackBar.showError(
|
||||
tester.element(find.byType(Scaffold)),
|
||||
message: errorMessage,
|
||||
);
|
||||
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
|
||||
expect(find.text(errorMessage), findsOneWidget);
|
||||
|
||||
// Показываем снекбар с успехом
|
||||
AppSnackBar.showSuccess(
|
||||
context: tester.element(find.byType(Scaffold)),
|
||||
message: successMessage,
|
||||
);
|
||||
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
|
||||
// Проверяем, что показывается только снекбар с успехом
|
||||
expect(find.text(errorMessage), findsNothing);
|
||||
expect(find.text(successMessage), findsOneWidget);
|
||||
});
|
||||
});
|
||||
|
||||
group('Анимация', () {
|
||||
testTester('анимация появления работает корректно', (tester) async {
|
||||
const message = 'Анимация появления';
|
||||
|
||||
AppSnackBar.showError(
|
||||
tester.element(find.byType(Scaffold)),
|
||||
message: message,
|
||||
);
|
||||
|
||||
// В начале анимации снекбар должен находиться за пределами экрана
|
||||
await tester.pump();
|
||||
|
||||
final positionedWidget = tester.widget<Positioned>(
|
||||
find.byType(Positioned),
|
||||
);
|
||||
expect(positionedWidget.top, lessThan(0));
|
||||
|
||||
// После завершения анимации снекбар должен быть виден
|
||||
await tester.pump(const Duration(milliseconds: 300));
|
||||
|
||||
final positionedWidgetAfter = tester.widget<Positioned>(
|
||||
find.byType(Positioned),
|
||||
);
|
||||
expect(positionedWidgetAfter.top, greaterThan(0));
|
||||
});
|
||||
|
||||
testTester('анимация скрытия работает корректно при тапе', (
|
||||
tester,
|
||||
) async {
|
||||
const message = 'Анимация скрытия';
|
||||
|
||||
AppSnackBar.showError(
|
||||
tester.element(find.byType(Scaffold)),
|
||||
message: message,
|
||||
);
|
||||
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 350));
|
||||
|
||||
expect(find.byType(AppSnackBar), findsOneWidget);
|
||||
|
||||
// Тапаем для закрытия
|
||||
await tester.tap(find.byType(GestureDetector));
|
||||
await tester.pump();
|
||||
|
||||
// Анимация закрытия должна начаться
|
||||
expect(find.byType(AppSnackBar), findsOneWidget);
|
||||
|
||||
// После завершения анимации снекбар должен исчезнуть
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.byType(AppSnackBar), findsNothing);
|
||||
});
|
||||
});
|
||||
|
||||
group('Интеграция с MediaQuery', () {
|
||||
testTester('правильно позиционируется с учетом SafeArea', (tester) async {
|
||||
const message = 'SafeArea test';
|
||||
|
||||
// Создаем приложение с кастомными отступами
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: MediaQuery(
|
||||
data: const MediaQueryData(
|
||||
padding: EdgeInsets.only(top: 50), // Симулируем статус бар
|
||||
),
|
||||
child: const Scaffold(body: Center(child: Text('Test'))),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
AppSnackBar.showError(
|
||||
tester.element(find.byType(Scaffold)),
|
||||
message: message,
|
||||
);
|
||||
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 350));
|
||||
|
||||
// Проверяем, что снекбар отображается (позиция может варьироваться в зависимости от того,
|
||||
// когда именно MediaQuery применяется)
|
||||
final positionedWidget = tester.widget<Positioned>(
|
||||
find.byType(Positioned),
|
||||
);
|
||||
expect(
|
||||
positionedWidget.top,
|
||||
greaterThan(0),
|
||||
); // Снекбар должен быть видимым
|
||||
expect(find.byType(AppSnackBar), findsOneWidget);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
|
||||
Reference in New Issue
Block a user