Skip to content

Commit e81bb99

Browse files
committed
test(measurements): add unit tests for chart rendering, aggregation, metric routing, and composite group cards (#1251)
Validates MeasurementBarChartWidgetFl rendering, aggregatePerDay logic, buildChartForMetricType routing, and CategoriesCard group composition for multi-value metrics like blood pressure.
1 parent c088af2 commit e81bb99

4 files changed

Lines changed: 349 additions & 3 deletions

File tree

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
/*
2+
* This file is part of wger Workout Manager <https://github.com/wger-project>.
3+
* Copyright (c) 2026 wger Team
4+
*
5+
* wger Workout Manager is free software: you can redistribute it and/or modify
6+
* it under the terms of the GNU Affero General Public License as published by
7+
* the Free Software Foundation, either version 3 of the License, or
8+
* (at your option) any later version.
9+
*
10+
* This program is distributed in the hope that it will be useful,
11+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
* GNU Affero General Public License for more details.
14+
*
15+
* You should have received a copy of the GNU Affero General Public License
16+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
17+
*/
18+
19+
import 'package:flutter/material.dart';
20+
import 'package:flutter_test/flutter_test.dart';
21+
import 'package:wger/features/measurements/models/measurement_category.dart';
22+
import 'package:wger/features/measurements/widgets/charts.dart';
23+
import 'package:wger/features/measurements/widgets/helpers.dart';
24+
import 'package:wger/l10n/generated/app_localizations.dart';
25+
26+
Widget _wrapChart(Widget chart) => MaterialApp(
27+
localizationsDelegates: AppLocalizations.localizationsDelegates,
28+
supportedLocales: AppLocalizations.supportedLocales,
29+
home: Scaffold(
30+
body: SizedBox(width: 400, height: 300, child: chart),
31+
),
32+
);
33+
34+
void main() {
35+
final entries = [
36+
MeasurementChartEntry(1000, DateTime(2026, 1, 1)),
37+
MeasurementChartEntry(2000, DateTime(2026, 1, 2)),
38+
];
39+
40+
group('buildChartForMetricType routing', () {
41+
testWidgets('steps -> MeasurementBarChartWidgetFl', (tester) async {
42+
final widget = buildChartForMetricType(MetricType.steps, entries, [], 'steps');
43+
await tester.pumpWidget(_wrapChart(widget));
44+
await tester.pumpAndSettle();
45+
46+
expect(find.byType(MeasurementBarChartWidgetFl), findsOneWidget);
47+
expect(find.byType(MeasurementChartWidgetFl), findsNothing);
48+
});
49+
50+
testWidgets('custom -> MeasurementChartWidgetFl', (tester) async {
51+
final widget = buildChartForMetricType(MetricType.custom, entries, [], 'cm');
52+
await tester.pumpWidget(_wrapChart(widget));
53+
await tester.pumpAndSettle();
54+
55+
expect(find.byType(MeasurementChartWidgetFl), findsOneWidget);
56+
expect(find.byType(MeasurementBarChartWidgetFl), findsNothing);
57+
});
58+
59+
testWidgets('energy -> MeasurementBarChartWidgetFl', (tester) async {
60+
final widget = buildChartForMetricType(MetricType.energy, entries, [], 'kcal');
61+
await tester.pumpWidget(_wrapChart(widget));
62+
await tester.pumpAndSettle();
63+
64+
expect(find.byType(MeasurementBarChartWidgetFl), findsOneWidget);
65+
});
66+
});
67+
}
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
/*
2+
* This file is part of wger Workout Manager <https://github.com/wger-project>.
3+
* Copyright (c) 2026 wger Team
4+
*
5+
* wger Workout Manager is free software: you can redistribute it and/or modify
6+
* it under the terms of the GNU Affero General Public License as published by
7+
* the Free Software Foundation, either version 3 of the License, or
8+
* (at your option) any later version.
9+
*
10+
* This program is distributed in the hope that it will be useful,
11+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
* GNU Affero General Public License for more details.
14+
*
15+
* You should have received a copy of the GNU Affero General Public License
16+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
17+
*/
18+
19+
import 'package:flutter/material.dart';
20+
import 'package:flutter_test/flutter_test.dart';
21+
import 'package:wger/features/measurements/models/measurement_category.dart';
22+
import 'package:wger/features/measurements/models/measurement_entry.dart';
23+
import 'package:wger/features/measurements/widgets/categories_card.dart';
24+
import 'package:wger/l10n/generated/app_localizations.dart';
25+
26+
Widget _wrap(Widget child) => MaterialApp(
27+
localizationsDelegates: AppLocalizations.localizationsDelegates,
28+
supportedLocales: AppLocalizations.supportedLocales,
29+
routes: {
30+
'/form': (ctx) => const Scaffold(body: Text('form screen')),
31+
},
32+
home: Scaffold(body: child),
33+
);
34+
35+
MeasurementCategory _bpGroup({bool withEntries = false}) {
36+
final sysEntries = withEntries
37+
? [
38+
MeasurementEntry(
39+
id: 'e1',
40+
categoryId: 'sys',
41+
date: DateTime(2026, 1, 1),
42+
value: 120,
43+
notes: '',
44+
),
45+
]
46+
: <MeasurementEntry>[];
47+
final diaEntries = withEntries
48+
? [
49+
MeasurementEntry(
50+
id: 'e2',
51+
categoryId: 'dia',
52+
date: DateTime(2026, 1, 1),
53+
value: 80,
54+
notes: '',
55+
),
56+
]
57+
: <MeasurementEntry>[];
58+
59+
final sys = MeasurementCategory(
60+
id: 'sys',
61+
name: 'Systolic',
62+
unit: 'mmHg',
63+
parentId: 'bp',
64+
order: 0,
65+
entries: sysEntries,
66+
);
67+
final dia = MeasurementCategory(
68+
id: 'dia',
69+
name: 'Diastolic',
70+
unit: 'mmHg',
71+
parentId: 'bp',
72+
order: 1,
73+
entries: diaEntries,
74+
);
75+
76+
return MeasurementCategory(
77+
id: 'bp',
78+
name: 'Blood pressure',
79+
unit: 'mmHg',
80+
metricType: MetricType.bloodPressure,
81+
children: [sys, dia],
82+
);
83+
}
84+
85+
void main() {
86+
group('CategoriesCard — group card', () {
87+
testWidgets('shows one ListTile per child with latest reading', (tester) async {
88+
await tester.pumpWidget(_wrap(CategoriesCard(_bpGroup(withEntries: true))));
89+
await tester.pumpAndSettle();
90+
91+
expect(find.byType(ListTile), findsNWidgets(2));
92+
expect(find.textContaining('120'), findsOneWidget);
93+
expect(find.textContaining('80'), findsOneWidget);
94+
});
95+
96+
testWidgets('shows dash when child has no entries', (tester) async {
97+
await tester.pumpWidget(_wrap(CategoriesCard(_bpGroup(withEntries: false))));
98+
await tester.pumpAndSettle();
99+
100+
// Text('—') should appear for both children
101+
expect(find.text('—'), findsNWidgets(2));
102+
});
103+
104+
testWidgets('add icon button is present on group card', (tester) async {
105+
await tester.pumpWidget(_wrap(CategoriesCard(_bpGroup())));
106+
await tester.pumpAndSettle();
107+
108+
expect(find.byIcon(Icons.add), findsOneWidget);
109+
});
110+
});
111+
}

test/features/measurements/widgets/charts_test.dart

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,18 +16,51 @@
1616
* along with this program. If not, see <http://www.gnu.org/licenses/>.
1717
*/
1818

19+
import 'package:fl_chart/fl_chart.dart';
20+
import 'package:flutter/material.dart';
1921
import 'package:flutter_test/flutter_test.dart';
2022
import 'package:wger/features/measurements/widgets/charts.dart';
23+
import 'package:wger/l10n/generated/app_localizations.dart';
24+
25+
Widget _wrap(Widget child) => MaterialApp(
26+
localizationsDelegates: AppLocalizations.localizationsDelegates,
27+
supportedLocales: AppLocalizations.supportedLocales,
28+
home: Scaffold(
29+
body: SizedBox(width: 400, height: 300, child: child),
30+
),
31+
);
2132

2233
void main() {
2334
MeasurementChartEntry entry(num value, DateTime date) => MeasurementChartEntry(value, date);
2435

25-
group('aggregatePerDay', () {
26-
test('returns an empty list for no entries', () {
36+
group('MeasurementBarChartWidgetFl', () {
37+
testWidgets('renders without error for empty entries', (tester) async {
38+
await tester.pumpWidget(
39+
_wrap(const MeasurementBarChartWidgetFl([], 'steps')),
40+
);
41+
await tester.pumpAndSettle();
42+
expect(find.byType(MeasurementBarChartWidgetFl), findsOneWidget);
43+
expect(tester.takeException(), isNull);
44+
});
45+
46+
// T-24
47+
testWidgets('BarChart is present for non-empty entries', (tester) async {
48+
final entries = [
49+
entry(1000, DateTime(2026, 1, 1)),
50+
entry(2000, DateTime(2026, 1, 2)),
51+
entry(1500, DateTime(2026, 1, 3)),
52+
];
53+
await tester.pumpWidget(_wrap(MeasurementBarChartWidgetFl(entries, 'steps')));
54+
await tester.pumpAndSettle();
55+
56+
expect(find.byType(BarChart), findsOneWidget);
57+
});
58+
59+
test('aggregatePerDay returns an empty list for no entries', () {
2760
expect(aggregatePerDay([]), isEmpty);
2861
});
2962

30-
test('sums entries sharing a calendar day into one point', () {
63+
test('aggregatePerDay sums entries sharing a calendar day into one point', () {
3164
final result = aggregatePerDay([
3265
entry(1000, DateTime(2026, 1, 1, 8)),
3366
entry(2500, DateTime(2026, 1, 1, 20)),
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
/*
2+
* This file is part of wger Workout Manager <https://github.com/wger-project>.
3+
* Copyright (c) 2026 - 2026 wger Team
4+
*
5+
* wger Workout Manager is free software: you can redistribute it and/or modify
6+
* it under the terms of the GNU Affero General Public License as published by
7+
* the Free Software Foundation, either version 3 of the License, or
8+
* (at your option) any later version.
9+
*
10+
* This program is distributed in the hope that it will be useful,
11+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
* GNU Affero General Public License for more details.
14+
*
15+
* You should have received a copy of the GNU Affero General Public License
16+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
17+
*/
18+
19+
import 'package:flutter/material.dart';
20+
import 'package:flutter_test/flutter_test.dart';
21+
import 'package:wger/features/measurements/models/measurement_category.dart';
22+
import 'package:wger/features/measurements/widgets/charts.dart';
23+
import 'package:wger/features/measurements/widgets/helpers.dart';
24+
import 'package:wger/l10n/generated/app_localizations.dart';
25+
26+
void main() {
27+
final rawEntries = [
28+
MeasurementChartEntry(10, DateTime(2026, 1, 1)),
29+
MeasurementChartEntry(20, DateTime(2026, 1, 10)),
30+
];
31+
final avgEntries = moving7dAverage(rawEntries);
32+
33+
group('getOverviewWidgets', () {
34+
testWidgets('empty raw shows no-data placeholder', (tester) async {
35+
late List<Widget> widgets;
36+
await tester.pumpWidget(
37+
MaterialApp(
38+
localizationsDelegates: AppLocalizations.localizationsDelegates,
39+
supportedLocales: AppLocalizations.supportedLocales,
40+
home: Builder(
41+
builder: (ctx) {
42+
widgets = getOverviewWidgets('Test', [], [], 'cm', ctx);
43+
return Scaffold(
44+
body: SingleChildScrollView(child: Column(children: widgets)),
45+
);
46+
},
47+
),
48+
),
49+
);
50+
await tester.pumpAndSettle();
51+
52+
expect(find.text('No data available'), findsOneWidget);
53+
});
54+
55+
testWidgets('non-empty avg includes MeasurementOverallChangeWidget', (tester) async {
56+
late List<Widget> widgets;
57+
await tester.pumpWidget(
58+
MaterialApp(
59+
localizationsDelegates: AppLocalizations.localizationsDelegates,
60+
supportedLocales: AppLocalizations.supportedLocales,
61+
home: Builder(
62+
builder: (ctx) {
63+
widgets = getOverviewWidgets('Test', rawEntries, avgEntries, 'cm', ctx);
64+
return Scaffold(
65+
body: SingleChildScrollView(child: Column(children: widgets)),
66+
);
67+
},
68+
),
69+
),
70+
);
71+
await tester.pumpAndSettle();
72+
73+
expect(find.byType(MeasurementOverallChangeWidget), findsOneWidget);
74+
});
75+
});
76+
77+
group('getOverviewWidgetsSeries — Indicator legend', () {
78+
testWidgets('three Indicator widgets for non-summed metric (custom)', (tester) async {
79+
await tester.pumpWidget(
80+
MaterialApp(
81+
localizationsDelegates: AppLocalizations.localizationsDelegates,
82+
supportedLocales: AppLocalizations.supportedLocales,
83+
home: Builder(
84+
builder: (ctx) {
85+
final widgets = getOverviewWidgetsSeries(
86+
'Weight',
87+
rawEntries,
88+
avgEntries,
89+
[],
90+
'kg',
91+
ctx,
92+
metricType: MetricType.custom,
93+
);
94+
return Scaffold(
95+
body: SingleChildScrollView(child: Column(children: widgets)),
96+
);
97+
},
98+
),
99+
),
100+
);
101+
await tester.pumpAndSettle();
102+
103+
expect(find.byType(Indicator), findsNWidgets(3));
104+
});
105+
106+
testWidgets('two Indicator widgets for summed metric (steps — no trend)', (tester) async {
107+
await tester.pumpWidget(
108+
MaterialApp(
109+
localizationsDelegates: AppLocalizations.localizationsDelegates,
110+
supportedLocales: AppLocalizations.supportedLocales,
111+
home: Builder(
112+
builder: (ctx) {
113+
final widgets = getOverviewWidgetsSeries(
114+
'Steps',
115+
rawEntries,
116+
avgEntries,
117+
[],
118+
'steps',
119+
ctx,
120+
metricType: MetricType.steps,
121+
);
122+
return Scaffold(
123+
body: SingleChildScrollView(child: Column(children: widgets)),
124+
);
125+
},
126+
),
127+
),
128+
);
129+
await tester.pumpAndSettle();
130+
131+
// isSummedPerDay -> trend indicator is hidden
132+
expect(find.byType(Indicator), findsNWidgets(2));
133+
});
134+
});
135+
}

0 commit comments

Comments
 (0)