test: add unit tests for poultry repository and searchable dropdown functionalities
- Introduced tests for `PoultryScienceRepositoryImp` to validate delegated remote calls. - Added comprehensive tests for `SearchableDropdownLogic` covering selection, overlay, and search logic. - Enhanced `SearchableDropdown` widget tests for multi-select, label building, and overlay management.
This commit is contained in:
@@ -58,7 +58,8 @@ class _MonthlyDataCalendarState extends State<MonthlyDataCalendar> {
|
||||
@override
|
||||
void didUpdateWidget(MonthlyDataCalendar oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.selectedDate != widget.selectedDate || oldWidget.dayData != widget.dayData) {
|
||||
if (oldWidget.selectedDate != widget.selectedDate ||
|
||||
oldWidget.dayData != widget.dayData) {
|
||||
_generateCalendar();
|
||||
_updateDisplayValue();
|
||||
}
|
||||
@@ -79,25 +80,31 @@ class _MonthlyDataCalendarState extends State<MonthlyDataCalendar> {
|
||||
final twoDaysAgoStr = twoDaysAgo.formatCompactDate();
|
||||
final oneDayAgoStr = oneDayAgo.formatCompactDate();
|
||||
|
||||
return dateStr == todayStr || dateStr == twoDaysAgoStr || dateStr == oneDayAgoStr;
|
||||
return dateStr == todayStr ||
|
||||
dateStr == twoDaysAgoStr ||
|
||||
dateStr == oneDayAgoStr;
|
||||
}
|
||||
|
||||
void _generateCalendar() {
|
||||
final days = _computeCalendarDays();
|
||||
setState(() {
|
||||
_calendarDays = days;
|
||||
});
|
||||
}
|
||||
|
||||
List<DayInfo?> _computeCalendarDays() {
|
||||
final days = <DayInfo?>[];
|
||||
final year = _currentMonth.year;
|
||||
final month = _currentMonth.month;
|
||||
final daysInMonth = _currentMonth.monthLength;
|
||||
|
||||
// Get first day of month to determine starting position
|
||||
final firstDayOfMonth = Jalali(year, month, 1);
|
||||
final dayOfWeek = firstDayOfMonth.weekDay; // 1 = Saturday in shamsi_date
|
||||
final dayOfWeek = firstDayOfMonth.weekDay;
|
||||
|
||||
// Add empty cells for days before the first day of month
|
||||
for (int i = 1; i < dayOfWeek; i++) {
|
||||
days.add(null);
|
||||
}
|
||||
|
||||
// Add all days of the month
|
||||
for (int day = 1; day <= daysInMonth; day++) {
|
||||
final date = Jalali(year, month, day);
|
||||
final today = Jalali.now();
|
||||
@@ -111,21 +118,23 @@ class _MonthlyDataCalendarState extends State<MonthlyDataCalendar> {
|
||||
date: date,
|
||||
day: day,
|
||||
formattedDate: formattedDate,
|
||||
isToday: date.year == today.year && date.month == today.month && date.day == today.day,
|
||||
isToday:
|
||||
date.year == today.year &&
|
||||
date.month == today.month &&
|
||||
date.day == today.day,
|
||||
isEnabled: isEnabled,
|
||||
hasZeroValue: hasZeroValue,
|
||||
remainingStock: data?.value ?? 0,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_calendarDays = days;
|
||||
});
|
||||
return days;
|
||||
}
|
||||
|
||||
void _handleDayClick(DayInfo dayInfo) {
|
||||
if (dayInfo.isEnabled && !dayInfo.hasZeroValue && widget.onDateSelect != null) {
|
||||
if (dayInfo.isEnabled &&
|
||||
!dayInfo.hasZeroValue &&
|
||||
widget.onDateSelect != null) {
|
||||
widget.onDateSelect!(dayInfo);
|
||||
Navigator.pop(context);
|
||||
}
|
||||
@@ -176,7 +185,8 @@ class _MonthlyDataCalendarState extends State<MonthlyDataCalendar> {
|
||||
|
||||
if (dayInfo != null) {
|
||||
final persianDay = _toPersianNumber(dayInfo.day);
|
||||
_textController.text = '$persianDay ${_monthNames[dayInfo.date.month - 1]}';
|
||||
_textController.text =
|
||||
'$persianDay ${_monthNames[dayInfo.date.month - 1]}';
|
||||
} else {
|
||||
_textController.text = widget.selectedDate ?? '';
|
||||
}
|
||||
@@ -198,23 +208,31 @@ class _MonthlyDataCalendarState extends State<MonthlyDataCalendar> {
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return Dialog(
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
),
|
||||
child: Container(
|
||||
constraints: const BoxConstraints(maxWidth: 650, maxHeight: 650),
|
||||
child: Card(
|
||||
elevation: 3,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_buildHeader(),
|
||||
const SizedBox(height: 16),
|
||||
_buildDayNamesHeader(),
|
||||
const SizedBox(height: 8),
|
||||
_buildCalendarGrid(),
|
||||
],
|
||||
child: StatefulBuilder(
|
||||
builder: (context, setStateDialog) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_buildHeaderWithDialogSet(setStateDialog),
|
||||
const SizedBox(height: 16),
|
||||
_buildDayNamesHeader(),
|
||||
const SizedBox(height: 8),
|
||||
_buildCalendarGrid(),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -224,6 +242,64 @@ class _MonthlyDataCalendarState extends State<MonthlyDataCalendar> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHeaderWithDialogSet(
|
||||
void Function(void Function()) setStateDialog,
|
||||
) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.only(bottom: 16),
|
||||
decoration: const BoxDecoration(
|
||||
border: Border(bottom: BorderSide(color: Color(0xFFF0F0F0), width: 2)),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
setStateDialog(() {
|
||||
if (_currentMonth.month == 1) {
|
||||
_currentMonth = Jalali(_currentMonth.year - 1, 12, 1);
|
||||
} else {
|
||||
_currentMonth = Jalali(
|
||||
_currentMonth.year,
|
||||
_currentMonth.month - 1,
|
||||
1,
|
||||
);
|
||||
}
|
||||
_calendarDays = _computeCalendarDays();
|
||||
});
|
||||
},
|
||||
icon: const Icon(Icons.chevron_left),
|
||||
),
|
||||
Text(
|
||||
'${_monthNames[_currentMonth.month - 1]} ${_toPersianNumber(_currentMonth.year)}',
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 20,
|
||||
color: Color(0xFF333333),
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
setStateDialog(() {
|
||||
if (_currentMonth.month == 12) {
|
||||
_currentMonth = Jalali(_currentMonth.year + 1, 1, 1);
|
||||
} else {
|
||||
_currentMonth = Jalali(
|
||||
_currentMonth.year,
|
||||
_currentMonth.month + 1,
|
||||
1,
|
||||
);
|
||||
}
|
||||
_calendarDays = _computeCalendarDays();
|
||||
});
|
||||
},
|
||||
icon: const Icon(Icons.chevron_right),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHeader() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.only(bottom: 16),
|
||||
@@ -233,7 +309,10 @@ class _MonthlyDataCalendarState extends State<MonthlyDataCalendar> {
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
IconButton(onPressed: _handlePrevMonth, icon: const Icon(Icons.chevron_right)),
|
||||
IconButton(
|
||||
onPressed: _handlePrevMonth,
|
||||
icon: const Icon(Icons.chevron_left),
|
||||
),
|
||||
Text(
|
||||
'${_monthNames[_currentMonth.month - 1]} ${_toPersianNumber(_currentMonth.year)}',
|
||||
style: const TextStyle(
|
||||
@@ -242,7 +321,10 @@ class _MonthlyDataCalendarState extends State<MonthlyDataCalendar> {
|
||||
color: Color(0xFF333333),
|
||||
),
|
||||
),
|
||||
IconButton(onPressed: _handleNextMonth, icon: const Icon(Icons.chevron_left)),
|
||||
IconButton(
|
||||
onPressed: _handleNextMonth,
|
||||
icon: const Icon(Icons.chevron_right),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -333,7 +415,9 @@ class _MonthlyDataCalendarState extends State<MonthlyDataCalendar> {
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 14,
|
||||
color: dayInfo.isToday ? const Color(0xFFFF9800) : const Color(0xFF333333),
|
||||
color: dayInfo.isToday
|
||||
? const Color(0xFFFF9800)
|
||||
: const Color(0xFF333333),
|
||||
),
|
||||
),
|
||||
if (data != null && data.value != null) ...[
|
||||
|
||||
@@ -96,7 +96,7 @@ class SearchableDropdownLogic<T> extends GetxController {
|
||||
if (selectedItem != null) {
|
||||
this.selectedItem.value = selectedItem;
|
||||
} else {
|
||||
this.selectedItem.value = initialValue != null ? [?initialValue] : [];
|
||||
this.selectedItem.value = initialValue != null ? [initialValue as T] : [];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,188 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:rasadyar_core/presentation/widget/overlay_dropdown_widget/multi_select_dropdown/multi_select_dropdown_logic.dart';
|
||||
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('SearchableDropdownLogic', () {
|
||||
test('initializes selectedItem from provided list', () {
|
||||
final logic = SearchableDropdownLogic<String>(
|
||||
items: const ['A', 'B'],
|
||||
selectedItem: const ['A'],
|
||||
itemBuilder: (i) => Text(i),
|
||||
);
|
||||
expect(logic.selectedItem, ['A']);
|
||||
});
|
||||
|
||||
test('initializes selectedItem from initialValue when single select', () {
|
||||
final logic = SearchableDropdownLogic<String>(
|
||||
items: const ['A', 'B'],
|
||||
singleSelect: true,
|
||||
initialValue: 'B',
|
||||
itemBuilder: (i) => Text(i),
|
||||
);
|
||||
expect(logic.selectedItem, ['B']);
|
||||
});
|
||||
|
||||
testWidgets('showOverlay sets isOpen and inserts overlay', (tester) async {
|
||||
final logic = SearchableDropdownLogic<String>(
|
||||
items: const ['A', 'B'],
|
||||
itemBuilder: (i) => Text(i),
|
||||
);
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Scaffold(
|
||||
body: Builder(
|
||||
builder: (context) {
|
||||
return CompositedTransformTarget(
|
||||
link: logic.layerLink,
|
||||
child: GestureDetector(
|
||||
onTap: () => logic.showOverlay(context),
|
||||
child: const SizedBox(width: 200, height: 40),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
expect(logic.isOpen.value, false);
|
||||
await tester.tap(find.byType(GestureDetector));
|
||||
await tester.pump();
|
||||
expect(logic.isOpen.value, true);
|
||||
expect(find.text('نتیجهای یافت نشد.'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('removeOverlay resets isOpen', (tester) async {
|
||||
final logic = SearchableDropdownLogic<String>(
|
||||
items: const ['A', 'B'],
|
||||
itemBuilder: (i) => Text(i),
|
||||
);
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Scaffold(
|
||||
body: Builder(
|
||||
builder: (context) {
|
||||
return CompositedTransformTarget(
|
||||
link: logic.layerLink,
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
if (!logic.isOpen.value) {
|
||||
logic.showOverlay(context);
|
||||
} else {
|
||||
logic.removeOverlay();
|
||||
}
|
||||
},
|
||||
child: const SizedBox(width: 200, height: 40),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.tap(find.byType(GestureDetector));
|
||||
await tester.pump();
|
||||
expect(logic.isOpen.value, true);
|
||||
await tester.tap(find.byType(GestureDetector));
|
||||
await tester.pump();
|
||||
expect(logic.isOpen.value, false);
|
||||
});
|
||||
|
||||
testWidgets('tap item adds to selected and calls onChanged', (tester) async {
|
||||
String? changed;
|
||||
final logic = SearchableDropdownLogic<String>(
|
||||
items: const ['A', 'B'],
|
||||
onChanged: (s) => changed = s,
|
||||
itemBuilder: (i) => Text(i),
|
||||
);
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Scaffold(
|
||||
body: Builder(
|
||||
builder: (context) {
|
||||
return CompositedTransformTarget(
|
||||
link: logic.layerLink,
|
||||
child: GestureDetector(
|
||||
onTap: () => logic.showOverlay(context),
|
||||
child: const SizedBox(width: 200, height: 40),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.tap(find.byType(GestureDetector));
|
||||
await tester.pump();
|
||||
await tester.tap(find.text('A'));
|
||||
await tester.pump();
|
||||
expect(logic.selectedItem.contains('A'), true);
|
||||
expect(changed, 'A');
|
||||
});
|
||||
|
||||
testWidgets('singleSelect updates searchController text using labelBuilder', (tester) async {
|
||||
final logic = SearchableDropdownLogic<String>(
|
||||
items: const ['A', 'B'],
|
||||
singleSelect: true,
|
||||
labelBuilder: (s) => 'L:$s',
|
||||
itemBuilder: (i) => Text(i),
|
||||
);
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Scaffold(
|
||||
body: Builder(
|
||||
builder: (context) {
|
||||
return CompositedTransformTarget(
|
||||
link: logic.layerLink,
|
||||
child: GestureDetector(
|
||||
onTap: () => logic.showOverlay(context),
|
||||
child: const SizedBox(width: 200, height: 40),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.tap(find.byType(GestureDetector));
|
||||
await tester.pump();
|
||||
await tester.tap(find.text('B'));
|
||||
await tester.pump();
|
||||
expect(logic.searchController.text, 'L:B');
|
||||
});
|
||||
|
||||
testWidgets('performSearch uses onSearch and updates filteredItems', (tester) async {
|
||||
final logic = SearchableDropdownLogic<String>(
|
||||
items: const ['A', 'B', 'C'],
|
||||
onSearch: (q) async => ['B'],
|
||||
itemBuilder: (i) => Text(i),
|
||||
);
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Scaffold(
|
||||
body: Builder(
|
||||
builder: (context) {
|
||||
return CompositedTransformTarget(
|
||||
link: logic.layerLink,
|
||||
child: GestureDetector(
|
||||
onTap: () => logic.showOverlay(context),
|
||||
child: const SizedBox(width: 200, height: 40),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.tap(find.byType(GestureDetector));
|
||||
await tester.pump();
|
||||
logic.performSearch('x');
|
||||
await tester.pumpAndSettle();
|
||||
expect(logic.filteredItems, ['B']);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:rasadyar_core/presentation/widget/overlay_dropdown_widget/multi_select_dropdown/multi_select_dropdown.dart';
|
||||
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('SearchableDropdown widget', () {
|
||||
testWidgets('asserts builders based on singleSelect', (tester) async {
|
||||
expect(
|
||||
() => SearchableDropdown<String>(
|
||||
items: const ['A', 'B'],
|
||||
itemBuilder: Text.new,
|
||||
singleSelect: true,
|
||||
),
|
||||
throwsA(isA<AssertionError>()),
|
||||
);
|
||||
expect(
|
||||
() => SearchableDropdown<String>(
|
||||
items: const ['A', 'B'],
|
||||
itemBuilder: Text.new,
|
||||
singleSelect: false,
|
||||
),
|
||||
throwsA(isA<AssertionError>()),
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('single select: selecting item sets text and calls onChanged', (tester) async {
|
||||
String? changed;
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Scaffold(
|
||||
body: SearchableDropdown<String>(
|
||||
items: const ['A', 'B'],
|
||||
singleSelect: true,
|
||||
singleLabelBuilder: (s) => 'L:$s',
|
||||
itemBuilder: Text.new,
|
||||
onChanged: (v) => changed = v,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.tap(find.byType(TextField));
|
||||
await tester.pump();
|
||||
await tester.tap(find.text('A'));
|
||||
await tester.pump();
|
||||
final tf = tester.widget<TextField>(find.byType(TextField));
|
||||
expect(tf.controller!.text, 'L:A');
|
||||
expect(changed, 'A');
|
||||
});
|
||||
|
||||
testWidgets('multi select: shows chips and allows removing selection', (tester) async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Scaffold(
|
||||
body: SearchableDropdown<String>(
|
||||
items: const ['A', 'B', 'C'],
|
||||
singleSelect: false,
|
||||
multiLabelBuilder: (s) => Container(
|
||||
key: Key('chip_${s ?? ''}'),
|
||||
padding: const EdgeInsets.all(4),
|
||||
child: Text(s ?? ''),
|
||||
),
|
||||
itemBuilder: Text.new,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.tap(find.byType(TextField));
|
||||
await tester.pump();
|
||||
await tester.tap(find.text('A'));
|
||||
await tester.pump();
|
||||
await tester.tap(find.byType(TextField));
|
||||
await tester.pump();
|
||||
await tester.tap(find.text('B'));
|
||||
await tester.pump();
|
||||
expect(find.byKey(const Key('chip_A')), findsOneWidget);
|
||||
expect(find.byKey(const Key('chip_B')), findsOneWidget);
|
||||
await tester.tap(find.byKey(const Key('chip_A')));
|
||||
await tester.pump();
|
||||
expect(find.byKey(const Key('chip_A')), findsNothing);
|
||||
expect(find.byKey(const Key('chip_B')), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('readOnly toggles based on onSearch presence', (tester) async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Scaffold(
|
||||
body: Column(
|
||||
children: [
|
||||
SearchableDropdown<String>(
|
||||
items: const ['A'],
|
||||
singleSelect: true,
|
||||
singleLabelBuilder: (s) => s ?? '',
|
||||
itemBuilder: Text.new,
|
||||
),
|
||||
SearchableDropdown<String>(
|
||||
items: const ['A'],
|
||||
singleSelect: true,
|
||||
singleLabelBuilder: (s) => s ?? '',
|
||||
itemBuilder: Text.new,
|
||||
onSearch: (q) async => ['A'],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
final textFields = tester.widgetList<TextField>(find.byType(TextField)).toList();
|
||||
expect(textFields[0].readOnly, true);
|
||||
expect(textFields[1].readOnly, false);
|
||||
});
|
||||
|
||||
testWidgets('tapping TextField toggles overlay list visibility', (tester) async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Scaffold(
|
||||
body: SearchableDropdown<String>(
|
||||
items: const ['A', 'B'],
|
||||
singleSelect: true,
|
||||
singleLabelBuilder: (s) => s ?? '',
|
||||
itemBuilder: Text.new,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
expect(find.text('A'), findsNothing);
|
||||
await tester.tap(find.byType(TextField));
|
||||
await tester.pump();
|
||||
expect(find.text('A'), findsOneWidget);
|
||||
await tester.tap(find.text('A'));
|
||||
await tester.pump();
|
||||
expect(find.text('A'), findsNothing);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user