Files
receipity/test/product_names_test.dart
T
2026-08-31 17:05:22 +02:00

199 lines
6.4 KiB
Dart

import 'package:flutter_test/flutter_test.dart';
import 'package:groci/models/line_item.dart';
import 'package:groci/models/product.dart';
import 'package:groci/utils/product_names.dart';
import 'test_helpers.dart';
Product _product(int id, String name, {String? supermarket}) => Product(
id: id,
name: name,
lastPrice: 1,
timesSeen: 1,
updatedAt: DateTime(2026, 1, 1),
supermarket: supermarket,
);
void main() {
ensureTestSqlite();
group('nameConflictGroups', () {
test('groups products that differ only by case at the same store', () {
final groups = nameConflictGroups([
_product(1, 'Broccoli', supermarket: 'Jumbo'),
_product(2, 'BROCCOLI', supermarket: 'Jumbo'),
_product(3, 'Milk', supermarket: 'Jumbo'),
]);
expect(groups, hasLength(1));
expect(groups.single.map((product) => product.id), [1, 2]);
});
test('ignores same name at different stores', () {
final groups = nameConflictGroups([
_product(1, 'Broccoli', supermarket: 'Jumbo'),
_product(2, 'BROCCOLI', supermarket: 'Lidl'),
]);
expect(groups, isEmpty);
});
});
group('buildNameConflictGroups', () {
test('finds conflicts from trip lines linked to different products', () {
final groups = buildNameConflictGroups(
products: [
_product(1, 'Broccoli', supermarket: 'Jumbo'),
_product(2, 'BROCCOLI', supermarket: 'Jumbo'),
],
shopLines: const [
(productId: 1, name: 'Broccoli', supermarket: 'Jumbo'),
(productId: 2, name: 'BROCCOLI', supermarket: 'Jumbo'),
],
);
expect(groups, hasLength(1));
expect(groups.single.products.map((product) => product.id), [1, 2]);
expect(groups.single.supermarket, 'Jumbo');
});
});
group('findSpellingVariants', () {
test('finds multiple receipt spellings for one product at one store', () {
final variants = findSpellingVariants(
products: [_product(1, 'Broccoli', supermarket: 'Jumbo')],
shopLines: const [
(productId: 1, name: 'Broccoli', supermarket: 'Jumbo'),
(productId: 1, name: 'BROCCOLI', supermarket: 'Jumbo'),
],
);
expect(variants, hasLength(1));
expect(variants.single.product.id, 1);
expect(variants.single.supermarket, 'Jumbo');
expect(variants.single.variants, ['Broccoli', 'BROCCOLI']);
});
test('ignores different spellings at different stores', () {
final variants = findSpellingVariants(
products: [_product(1, 'Broccoli', supermarket: 'Jumbo')],
shopLines: const [
(productId: 1, name: 'Broccoli', supermarket: 'Jumbo'),
(productId: 1, name: 'BROCCOLI', supermarket: 'Lidl'),
],
);
expect(variants, isEmpty);
});
});
test('same name at different supermarkets creates separate products', () async {
final repository = memoryRepository();
await repository.saveShop(
supermarket: 'Jumbo',
shoppedAt: DateTime(2026, 8, 1),
items: [LineItem(id: '1', name: 'Milk', price: 1.5)],
);
await repository.saveShop(
supermarket: 'Lidl',
shoppedAt: DateTime(2026, 8, 2),
items: [LineItem(id: '2', name: 'Milk', price: 1.2)],
);
final products = await repository.getAll();
expect(products, hasLength(2));
expect(products.map((p) => p.supermarket).toSet(), {'Jumbo', 'Lidl'});
expect(await repository.getProductNameIssues(), isEmpty);
});
test('getNameConflictGroups finds duplicates in the database', () async {
final repository = memoryRepository();
await seedCaseConflictCatalog(repository);
final groups = await repository.getNameConflictGroups();
expect(groups, hasLength(1));
expect(groups.single.map((product) => product.name).toSet(), {
'Broccoli',
'BROCCOLI',
});
});
test('getProductNameIssues finds receipt spelling variants', () async {
final repository = memoryRepository();
await repository.saveShop(
supermarket: 'Jumbo',
shoppedAt: DateTime(2026, 8, 1),
items: [LineItem(id: '1', name: 'Broccoli', price: 1.5)],
);
await repository.saveShop(
supermarket: 'Jumbo',
shoppedAt: DateTime(2026, 8, 2),
items: [LineItem(id: '2', name: 'BROCCOLI', price: 1.8)],
);
final issues = await repository.getProductNameIssues();
expect(issues.mergeGroups, isEmpty);
expect(issues.spellingVariants, hasLength(1));
expect(issues.spellingVariants.single.supermarket, 'Jumbo');
expect(issues.spellingVariants.single.variants, ['Broccoli', 'BROCCOLI']);
});
test('dismissNameIssue hides a spelling variant', () async {
final repository = memoryRepository();
await repository.saveShop(
supermarket: 'Jumbo',
shoppedAt: DateTime(2026, 8, 1),
items: [LineItem(id: '1', name: 'Broccoli', price: 1.5)],
);
await repository.saveShop(
supermarket: 'Jumbo',
shoppedAt: DateTime(2026, 8, 2),
items: [LineItem(id: '2', name: 'BROCCOLI', price: 1.8)],
);
final issue =
(await repository.getProductNameIssues()).spellingVariants.single;
await repository.dismissNameIssue(issue.key);
expect(await repository.getProductNameIssues(), isEmpty);
});
test('mergeProducts combines purchases onto the kept product', () async {
final repository = memoryRepository();
await seedCaseConflictCatalog(repository);
await repository.mergeProducts(keepId: 1, sourceIds: [2]);
final after = await repository.getAll();
expect(after, hasLength(1));
expect(after.single.name, 'Broccoli');
expect(after.single.timesSeen, 2);
expect(await repository.getPurchases(1), hasLength(2));
});
test('unifyProductSpelling updates catalog and receipt lines', () async {
final repository = memoryRepository();
await repository.saveShop(
supermarket: 'Jumbo',
shoppedAt: DateTime(2026, 8, 1),
items: [LineItem(id: '1', name: 'Broccoli', price: 1.5)],
);
await repository.saveShop(
supermarket: 'Jumbo',
shoppedAt: DateTime(2026, 8, 2),
items: [LineItem(id: '2', name: 'BROCCOLI', price: 1.8)],
);
final product = (await repository.getAll()).single;
await repository.unifyProductSpelling(
productId: product.id,
name: 'BROCCOLI',
supermarket: 'Jumbo',
);
final updated = (await repository.getProduct(product.id))!;
expect(updated.name, 'BROCCOLI');
expect((await repository.getProductNameIssues()).isEmpty, isTrue);
});
}