feat(inventory): filter by category and lot form + a11y polish
Add filter chips above the inventory list, one per category in use and one per lot form actually held; results apply search ∧ category ∧ form. An active filter that empties the list shows a distinct 'no matches' state and a clear-filters button. VarietyListItem now carries the set of lot forms it holds (one aggregate query), and the stream re-emits on lot changes. Accessibility: category headers use the text theme so they honour the system font scale; the list avatar is excluded from semantics (the tile title already announces the name); the edit action uses the green action colour (≥3:1 on the canvas, up from 2.62:1). Add category/form/clear filter tests and a tap-target-size guideline test.
This commit is contained in:
parent
42c16c0e3f
commit
9558115c1e
9 changed files with 337 additions and 24 deletions
|
|
@ -1,8 +1,10 @@
|
|||
import 'dart:typed_data';
|
||||
|
||||
import 'package:commons_core/commons_core.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:tane/db/database.dart';
|
||||
import 'package:tane/db/enums.dart';
|
||||
import 'package:tane/i18n/strings.g.dart';
|
||||
import 'package:tane/ui/inventory_list_screen.dart';
|
||||
|
||||
|
|
@ -48,7 +50,8 @@ void main() {
|
|||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('Grandma tomato'), findsOneWidget);
|
||||
expect(find.text('Solanaceae'), findsOneWidget); // category header
|
||||
// The category now appears both as a group header and a filter chip.
|
||||
expect(find.text('Solanaceae'), findsWidgets);
|
||||
await disposeTree(tester);
|
||||
},
|
||||
);
|
||||
|
|
@ -71,6 +74,120 @@ void main() {
|
|||
await disposeTree(tester);
|
||||
});
|
||||
|
||||
testWidgets('a category chip filters the list to that category', (
|
||||
tester,
|
||||
) async {
|
||||
final repo = newTestRepository(db);
|
||||
await repo.addQuickVariety(label: 'Tomato', category: 'Solanaceae');
|
||||
await repo.addQuickVariety(label: 'Bean', category: 'Fabaceae');
|
||||
|
||||
await tester.pumpWidget(
|
||||
wrapScreen(repository: repo, child: const InventoryListScreen()),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.tap(
|
||||
find.byKey(const Key('inventory.filter.category.Solanaceae')),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('Tomato'), findsOneWidget);
|
||||
expect(find.text('Bean'), findsNothing);
|
||||
await disposeTree(tester);
|
||||
});
|
||||
|
||||
testWidgets('a form chip filters the list to that lot form', (tester) async {
|
||||
final repo = newTestRepository(db);
|
||||
const oneUnit = Quantity(kind: QuantityKind.packet, count: 1);
|
||||
await repo.addQuickVariety(
|
||||
label: 'SeedOnly',
|
||||
quantity: oneUnit,
|
||||
lotType: LotType.seed,
|
||||
);
|
||||
await repo.addQuickVariety(
|
||||
label: 'PlantOnly',
|
||||
quantity: oneUnit,
|
||||
lotType: LotType.plant,
|
||||
);
|
||||
|
||||
await tester.pumpWidget(
|
||||
wrapScreen(repository: repo, child: const InventoryListScreen()),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.tap(find.byKey(const Key('inventory.filter.type.seed')));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('SeedOnly'), findsOneWidget);
|
||||
expect(find.text('PlantOnly'), findsNothing);
|
||||
await disposeTree(tester);
|
||||
});
|
||||
|
||||
testWidgets('clearing filters restores the full list', (tester) async {
|
||||
final repo = newTestRepository(db);
|
||||
await repo.addQuickVariety(label: 'Tomato', category: 'Solanaceae');
|
||||
await repo.addQuickVariety(label: 'Bean', category: 'Fabaceae');
|
||||
|
||||
await tester.pumpWidget(
|
||||
wrapScreen(repository: repo, child: const InventoryListScreen()),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.tap(
|
||||
find.byKey(const Key('inventory.filter.category.Fabaceae')),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Tomato'), findsNothing);
|
||||
|
||||
await tester.tap(find.byKey(const Key('inventory.filter.clear')));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Tomato'), findsOneWidget);
|
||||
expect(find.text('Bean'), findsOneWidget);
|
||||
await disposeTree(tester);
|
||||
});
|
||||
|
||||
testWidgets('a filter that hides everything shows the no-matches state', (
|
||||
tester,
|
||||
) async {
|
||||
final repo = newTestRepository(db);
|
||||
const oneUnit = Quantity(kind: QuantityKind.packet, count: 1);
|
||||
await repo.addQuickVariety(
|
||||
label: 'SeedOnly',
|
||||
quantity: oneUnit,
|
||||
lotType: LotType.seed,
|
||||
);
|
||||
|
||||
await tester.pumpWidget(
|
||||
wrapScreen(repository: repo, child: const InventoryListScreen()),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Search excludes the only item, so the list empties under an active query.
|
||||
await tester.enterText(
|
||||
find.byKey(const Key('inventory.search')),
|
||||
'zzz-nope',
|
||||
);
|
||||
await tester.tap(find.byKey(const Key('inventory.filter.type.seed')));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('No seeds match your filters.'), findsOneWidget);
|
||||
await disposeTree(tester);
|
||||
});
|
||||
|
||||
testWidgets('meets the tap-target size guideline', (tester) async {
|
||||
final repo = newTestRepository(db);
|
||||
await repo.addQuickVariety(label: 'Tomato', category: 'Solanaceae');
|
||||
|
||||
await tester.pumpWidget(
|
||||
wrapScreen(repository: repo, child: const InventoryListScreen()),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await expectLater(tester, meetsGuideline(androidTapTargetGuideline));
|
||||
await expectLater(tester, meetsGuideline(iOSTapTargetGuideline));
|
||||
await disposeTree(tester);
|
||||
});
|
||||
|
||||
testWidgets('renders in Spanish when the locale is es', (tester) async {
|
||||
final repo = newTestRepository(db);
|
||||
await tester.pumpWidget(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue