Skip to content

Commit 90cfd60

Browse files
authored
Merge pull request #1203 from anjakDev/feature/630-show-brand-for-ingredients
Feature(#630): Show brand for ingredients
2 parents 802fc4d + 70a5ae3 commit 90cfd60

7 files changed

Lines changed: 125 additions & 3 deletions

File tree

lib/models/nutrition/ingredient.dart

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,10 @@ class Ingredient {
6767
@JsonKey(required: true)
6868
final String name;
6969

70+
/// Brand of the product
71+
@JsonKey(name: 'brand')
72+
final String? brand;
73+
7074
@JsonKey(required: true, name: 'created')
7175
final DateTime created;
7276

@@ -123,6 +127,7 @@ class Ingredient {
123127
required this.id,
124128
required this.code,
125129
required this.name,
130+
this.brand,
126131
required this.created,
127132
required this.energy,
128133
required this.carbohydrates,

lib/models/nutrition/ingredient.g.dart

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

lib/widgets/nutrition/ingredient_dialogs.dart

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,22 @@ class IngredientDetails extends StatelessWidget {
108108
}
109109

110110
return AlertDialog(
111-
title: (snapshot.hasData) ? Text(ingredient!.name) : null,
111+
title: snapshot.hasData
112+
? Column(
113+
crossAxisAlignment: CrossAxisAlignment.start,
114+
mainAxisSize: MainAxisSize.min,
115+
children: [
116+
Text(ingredient!.name),
117+
if (ingredient.brand != null && ingredient.brand!.isNotEmpty)
118+
Text(
119+
ingredient.brand!,
120+
style: Theme.of(context).textTheme.titleSmall?.copyWith(
121+
color: Theme.of(context).colorScheme.onSurfaceVariant,
122+
),
123+
),
124+
],
125+
)
126+
: null,
112127
content: SingleChildScrollView(
113128
child: Padding(
114129
padding: const EdgeInsets.all(8.0),

lib/widgets/nutrition/widgets.dart

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -212,8 +212,21 @@ class _IngredientTypeaheadState extends ConsumerState<IngredientTypeahead> {
212212
: const CircleIconAvatar(
213213
Icon(Icons.image, color: Colors.grey),
214214
),
215-
title: Text(
216-
ingredient.name,
215+
title: Text.rich(
216+
TextSpan(
217+
children: [
218+
TextSpan(text: ingredient.name),
219+
if (ingredient.brand != null && ingredient.brand!.isNotEmpty)
220+
TextSpan(
221+
text: ' ${ingredient.brand}',
222+
style: TextStyle(
223+
color: Theme.of(
224+
context,
225+
).colorScheme.onSurfaceVariant.withValues(alpha: 0.7),
226+
),
227+
),
228+
],
229+
),
217230
maxLines: 2,
218231
overflow: TextOverflow.ellipsis,
219232
),

test/nutrition/ingredient_typeahead_test.dart

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,27 @@ void main() {
174174
expect(find.text('Vegan'), findsNothing);
175175
});
176176

177+
testWidgets('Shows brand inline in search result tile', (WidgetTester tester) async {
178+
// ingredient3 (Broccoli cake) has brand 'Weightwatchers'
179+
when(
180+
mockNutrition.searchIngredient(
181+
any,
182+
languageCode: anyNamed('languageCode'),
183+
searchLanguage: anyNamed('searchLanguage'),
184+
isVegan: anyNamed('isVegan'),
185+
isVegetarian: anyNamed('isVegetarian'),
186+
nutriscoreMax: anyNamed('nutriscoreMax'),
187+
),
188+
).thenAnswer((_) => Future.value([ingredient3]));
189+
190+
await tester.pumpWidget(createWidgetUnderTest());
191+
await tester.enterText(find.byType(TextFormField), 'Broccoli');
192+
await tester.pump(const Duration(milliseconds: 600));
193+
await tester.pumpAndSettle();
194+
195+
expect(find.textContaining('Weightwatchers'), findsOneWidget);
196+
});
197+
177198
testWidgets('Shows no dietary chips when ingredient has no info', (WidgetTester tester) async {
178199
// ingredient2 (Burger soup) has no dietary info
179200
when(

test/widgets/nutrition/ingredient_dialogs_test.dart

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,34 @@ import 'package:wger/l10n/generated/app_localizations.dart';
2222
import 'package:wger/models/nutrition/ingredient.dart';
2323
import 'package:wger/widgets/nutrition/ingredient_dialogs.dart';
2424

25+
import '../../../test_data/nutritional_plans.dart';
26+
27+
Future<void> pumpIngredientDetailsDialog(
28+
WidgetTester tester, {
29+
required AsyncSnapshot<Ingredient> snapshot,
30+
}) async {
31+
await tester.pumpWidget(
32+
MaterialApp(
33+
locale: const Locale('en'),
34+
localizationsDelegates: AppLocalizations.localizationsDelegates,
35+
supportedLocales: AppLocalizations.supportedLocales,
36+
home: Scaffold(
37+
body: Builder(
38+
builder: (context) => ElevatedButton(
39+
onPressed: () {
40+
showDialog(
41+
context: context,
42+
builder: (_) => IngredientDetails(snapshot),
43+
);
44+
},
45+
child: const Text('Show Dialog'),
46+
),
47+
),
48+
),
49+
),
50+
);
51+
}
52+
2553
Future<void> pumpIngredientScanDialog(
2654
WidgetTester tester, {
2755
required AsyncSnapshot<Ingredient?> snapshot,
@@ -54,6 +82,39 @@ Future<void> pumpIngredientScanDialog(
5482
}
5583

5684
void main() {
85+
group('IngredientDetails tests', () {
86+
testWidgets('shows brand below name in title when ingredient has a brand', (
87+
WidgetTester tester,
88+
) async {
89+
// ingredient3 (Broccoli cake, brand: 'Weightwatchers')
90+
final snapshot = AsyncSnapshot<Ingredient>.withData(ConnectionState.done, ingredient3);
91+
92+
await pumpIngredientDetailsDialog(tester, snapshot: snapshot);
93+
await tester.tap(find.text('Show Dialog'));
94+
await tester.pumpAndSettle();
95+
96+
expect(find.byType(AlertDialog), findsOneWidget);
97+
expect(find.text('Broccoli cake'), findsOneWidget);
98+
expect(find.text('Weightwatchers'), findsOneWidget);
99+
});
100+
101+
testWidgets('does not show brand in title when ingredient has no brand', (
102+
WidgetTester tester,
103+
) async {
104+
// ingredient1 (Water, brand: null)
105+
final snapshot = AsyncSnapshot<Ingredient>.withData(ConnectionState.done, ingredient1);
106+
107+
await pumpIngredientDetailsDialog(tester, snapshot: snapshot);
108+
await tester.tap(find.text('Show Dialog'));
109+
await tester.pumpAndSettle();
110+
111+
expect(find.byType(AlertDialog), findsOneWidget);
112+
expect(find.text('Water'), findsOneWidget);
113+
// brand: null must not render as the literal string "null"
114+
expect(find.text('null'), findsNothing);
115+
});
116+
});
117+
57118
group('IngredientScanResultDialog tests', () {
58119
const testBarcode = '1234567890123';
59120

test_data/nutritional_plans.dart

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ final ingredient1 = Ingredient(
3232
id: 1,
3333
code: '123456787',
3434
name: 'Water',
35+
brand: null,
3536
created: DateTime(2021, 5, 1),
3637
energy: 500,
3738
carbohydrates: 10,
@@ -52,6 +53,7 @@ final ingredient2 = Ingredient(
5253
id: 2,
5354
code: '123456788',
5455
name: 'Burger soup',
56+
brand: null,
5557
created: DateTime(2021, 5, 10),
5658
energy: 25,
5759
carbohydrates: 10,
@@ -69,6 +71,7 @@ final ingredient3 = Ingredient(
6971
id: 3,
7072
code: '123456789',
7173
name: 'Broccoli cake',
74+
brand: 'Weightwatchers',
7275
created: DateTime(2021, 5, 2),
7376
energy: 1200,
7477
carbohydrates: 110,
@@ -86,6 +89,7 @@ final muesli = Ingredient(
8689
id: 1,
8790
code: '123456787',
8891
name: 'Müsli',
92+
brand: 'Spar Gourmet',
8993
created: DateTime(2021, 5, 1),
9094
energy: 500,
9195
carbohydrates: 10,
@@ -106,6 +110,7 @@ final milk = Ingredient(
106110
id: 1,
107111
code: '123456787',
108112
name: 'Milk',
113+
brand: null,
109114
created: DateTime(2021, 5, 1),
110115
energy: 500,
111116
carbohydrates: 10,

0 commit comments

Comments
 (0)