199 lines
6.5 KiB
Dart
199 lines
6.5 KiB
Dart
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:receipity/models/line_item.dart';
|
|
import 'package:receipity/models/product.dart';
|
|
import 'package:receipity/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);
|
|
});
|
|
}
|