feat: alphabet list, change Layout etc
This commit is contained in:
@@ -2,7 +2,7 @@ import 'package:drift/native.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:kooltap/database/app_database.dart';
|
||||
|
||||
/// The parts of the v12 schema that the v13 migration touches.
|
||||
/// The parts of the v12 schema that the v13 and v14 migrations touch.
|
||||
const _v12Schema = [
|
||||
'CREATE TABLE bar_tabs (id TEXT NOT NULL, customer_name TEXT NOT NULL, '
|
||||
"status TEXT NOT NULL DEFAULT 'open', opened_at INTEGER NOT NULL, "
|
||||
@@ -15,6 +15,13 @@ const _v12Schema = [
|
||||
'CREATE TABLE tab_item_purchases (id TEXT NOT NULL, '
|
||||
'tab_item_id TEXT NOT NULL, purchased_at INTEGER NOT NULL, '
|
||||
'removed_at INTEGER NULL, PRIMARY KEY (id))',
|
||||
'CREATE TABLE products (id TEXT NOT NULL, name TEXT NOT NULL, '
|
||||
'category TEXT NOT NULL, stock_quantity INTEGER NOT NULL, '
|
||||
'low_stock_threshold INTEGER NOT NULL, price_in_cents INTEGER NOT NULL, '
|
||||
'image_path TEXT NULL, active INTEGER NOT NULL DEFAULT 1, '
|
||||
'PRIMARY KEY (id))',
|
||||
"INSERT INTO products VALUES ('prod-1', 'Beer', 'Drinks', 20, 5, 400, "
|
||||
'NULL, 1)',
|
||||
"INSERT INTO bar_tabs VALUES ('tab-1', 'Alice', 'open', 1700000000, NULL, 4)",
|
||||
"INSERT INTO tab_items VALUES ('item-1', 'tab-1', 'prod-1', 'Beer', 3, 400, "
|
||||
'1700000100)',
|
||||
@@ -68,6 +75,13 @@ void main() {
|
||||
expect(purchases.where((row) => row.removedAt != null), hasLength(1));
|
||||
});
|
||||
|
||||
test('gives existing products a counted stock', () async {
|
||||
final product = await database.select(database.products).getSingle();
|
||||
|
||||
expect(product.infiniteStock, false);
|
||||
expect(product.stockQuantity, 20);
|
||||
});
|
||||
|
||||
test('deletes purchases whose item no longer exists', () async {
|
||||
final orphan = await (database.select(
|
||||
database.tabItemPurchases,
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:kooltap/l10n/app_localizations.dart';
|
||||
import 'package:kooltap/models/product.dart';
|
||||
import 'package:kooltap/views/bar/product_grid.dart';
|
||||
import 'package:kooltap/views/widgets/product_tile.dart';
|
||||
|
||||
final _products = [
|
||||
for (var i = 0; i < 60; i++)
|
||||
Product(
|
||||
id: 'prod-$i',
|
||||
name: 'Product $i',
|
||||
category: 'Drinks',
|
||||
stockQuantity: 10,
|
||||
lowStockThreshold: 2,
|
||||
priceInCents: 300,
|
||||
),
|
||||
];
|
||||
|
||||
void main() {
|
||||
Future<void> pumpGrid(WidgetTester tester, int rows, Size size) async {
|
||||
tester.view.physicalSize = size;
|
||||
tester.view.devicePixelRatio = 1;
|
||||
addTearDown(tester.view.reset);
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
||||
supportedLocales: AppLocalizations.supportedLocales,
|
||||
home: Scaffold(
|
||||
body: ProductGrid(
|
||||
products: _products,
|
||||
rows: rows,
|
||||
hasSelectedTab: true,
|
||||
onProductTap: (_) {},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
for (final size in [const Size(853, 720), const Size(1280, 1000)]) {
|
||||
for (var rows = 2; rows <= 6; rows++) {
|
||||
testWidgets('shows exactly $rows rows at $size', (tester) async {
|
||||
await pumpGrid(tester, rows, size);
|
||||
|
||||
final grid = tester.getRect(find.byType(GridView));
|
||||
final tiles = tester
|
||||
.widgetList(find.byType(ProductTile))
|
||||
.map((tile) => tester.getRect(find.byWidget(tile)))
|
||||
.toList();
|
||||
|
||||
final fullyVisibleRows = {
|
||||
for (final tile in tiles)
|
||||
if (tile.top >= grid.top && tile.bottom <= grid.bottom + 0.5)
|
||||
tile.top.round(),
|
||||
};
|
||||
expect(fullyVisibleRows, hasLength(rows));
|
||||
|
||||
final lastRowBottom = tiles
|
||||
.where((tile) => tile.bottom <= grid.bottom + 0.5)
|
||||
.map((tile) => tile.bottom)
|
||||
.reduce((a, b) => a > b ? a : b);
|
||||
expect(
|
||||
grid.bottom - lastRowBottom,
|
||||
closeTo(16, 1),
|
||||
reason: 'the rows fill the height down to the bottom padding',
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -319,5 +319,64 @@ void main() {
|
||||
expect(product!.stockQuantity, 0);
|
||||
});
|
||||
});
|
||||
|
||||
group('infinite stock', () {
|
||||
setUp(() async {
|
||||
await database
|
||||
.into(database.products)
|
||||
.insert(
|
||||
ProductsCompanion.insert(
|
||||
id: 'water',
|
||||
name: 'Water',
|
||||
category: 'Drinks',
|
||||
stockQuantity: 0,
|
||||
lowStockThreshold: 0,
|
||||
infiniteStock: const Value(true),
|
||||
priceInCents: 100,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
test('sells without stock and leaves the count alone', () async {
|
||||
await service.decreaseStock('water', 5);
|
||||
await service.increaseStock('water', 2);
|
||||
|
||||
final product = await service.getProductById('water');
|
||||
expect(product!.stockQuantity, 0);
|
||||
expect(product.infiniteStock, true);
|
||||
expect(product.isOutOfStock, false);
|
||||
expect(product.isLowStock, false);
|
||||
});
|
||||
|
||||
test('still reports a missing product', () async {
|
||||
await expectLater(
|
||||
service.increaseStock('nonexistent', 1),
|
||||
throwsA(isA<ProductNotFoundException>()),
|
||||
);
|
||||
});
|
||||
|
||||
test('is saved by create and update', () async {
|
||||
await service.createProduct(
|
||||
name: 'Tap water',
|
||||
category: 'Drinks',
|
||||
stockQuantity: 0,
|
||||
lowStockThreshold: 0,
|
||||
infiniteStock: true,
|
||||
priceInCents: 0,
|
||||
imagePath: null,
|
||||
);
|
||||
final created = (await service.getProducts()).firstWhere(
|
||||
(product) => product.name == 'Tap water',
|
||||
);
|
||||
expect(created.infiniteStock, true);
|
||||
|
||||
await service.updateProduct(
|
||||
created.copyWith(infiniteStock: false, stockQuantity: 4),
|
||||
);
|
||||
final updated = await service.getProductById(created.id);
|
||||
expect(updated!.infiniteStock, false);
|
||||
expect(updated.stockQuantity, 4);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:kooltap/l10n/app_localizations.dart';
|
||||
import 'package:kooltap/models/bar_tab.dart';
|
||||
import 'package:kooltap/utils/name_index.dart';
|
||||
import 'package:kooltap/views/bar/tab_panel.dart';
|
||||
|
||||
const _names = [
|
||||
'Zoë', 'anna', 'Émile', 'Bram', 'Mieke', 'Marc', 'Ward', 'Tom', 'Sofie', //
|
||||
'Lotte', 'Koen', 'Jan', 'Inge', 'Hanne', 'Greet', 'Frank', 'Dirk', 'Chris',
|
||||
'Nele', 'Olaf', 'Pieter', 'Rik', 'Vera', 'Yves', '2de tafel', 'Eva',
|
||||
];
|
||||
|
||||
void main() {
|
||||
group('name index', () {
|
||||
test('folds case and accents into the index letter', () {
|
||||
expect(indexLetterOf('émile'), 'E');
|
||||
expect(indexLetterOf('Zoë'), 'Z');
|
||||
expect(indexLetterOf(' anna'), 'A');
|
||||
expect(indexLetterOf('2de tafel'), '#');
|
||||
expect(indexLetterOf(''), '#');
|
||||
});
|
||||
|
||||
test('sorts A–Z regardless of case and accents, # last', () {
|
||||
final sorted = ['Zoë', 'émile', 'Eva', 'anna', '2de tafel', 'Bram']
|
||||
..sort(compareByIndex);
|
||||
|
||||
expect(sorted, ['anna', 'Bram', 'émile', 'Eva', 'Zoë', '2de tafel']);
|
||||
});
|
||||
});
|
||||
|
||||
group('TabPanel letter index', () {
|
||||
final tabs = [
|
||||
for (final (index, name) in _names.indexed)
|
||||
BarTab(
|
||||
id: 'tab-$index',
|
||||
customerName: name,
|
||||
openedAt: DateTime(2026, 10, 11, 12, index),
|
||||
items: const [],
|
||||
),
|
||||
];
|
||||
|
||||
Future<void> pumpPanel(
|
||||
WidgetTester tester, {
|
||||
bool showSearch = false,
|
||||
}) async {
|
||||
tester.view.physicalSize = const Size(500, 800);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
addTearDown(tester.view.reset);
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
||||
supportedLocales: AppLocalizations.supportedLocales,
|
||||
home: Scaffold(
|
||||
body: TabPanel(
|
||||
tabs: tabs,
|
||||
selectedTab: null,
|
||||
selectedTabId: null,
|
||||
stockByProductId: const {},
|
||||
showSearch: showSearch,
|
||||
onTabSelected: (_) {},
|
||||
onItemQuantityChanged: (_, _) async {},
|
||||
onCloseTabPressed: () {},
|
||||
onTabEdited: (_) async {},
|
||||
onTabDeleted: (_) async {},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String firstVisibleName(WidgetTester tester) {
|
||||
final list = tester.getRect(find.byType(OpenTabsList));
|
||||
final visible = _names.where((name) {
|
||||
final finder = find.text(name);
|
||||
if (finder.evaluate().isEmpty) return false;
|
||||
final rect = tester.getRect(finder);
|
||||
return rect.top >= list.top && rect.bottom <= list.bottom;
|
||||
}).toList();
|
||||
|
||||
return visible.reduce(
|
||||
(a, b) =>
|
||||
tester.getRect(find.text(a)).top < tester.getRect(find.text(b)).top
|
||||
? a
|
||||
: b,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> tapLetter(WidgetTester tester, String letter) async {
|
||||
await tester.tap(
|
||||
find.descendant(
|
||||
of: find.byWidgetPredicate(
|
||||
(widget) => widget.runtimeType.toString() == '_LetterIndexBar',
|
||||
),
|
||||
matching: find.text(letter),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
|
||||
testWidgets('lists the tabs alphabetically', (tester) async {
|
||||
await pumpPanel(tester);
|
||||
|
||||
expect(firstVisibleName(tester), 'anna');
|
||||
});
|
||||
|
||||
testWidgets('tapping a letter scrolls to its first name', (tester) async {
|
||||
await pumpPanel(tester);
|
||||
|
||||
await tapLetter(tester, 'M');
|
||||
expect(firstVisibleName(tester), 'Marc');
|
||||
|
||||
await tapLetter(tester, 'E');
|
||||
expect(firstVisibleName(tester), 'Émile');
|
||||
});
|
||||
|
||||
testWidgets('shows the search bar only when asked', (tester) async {
|
||||
await pumpPanel(tester);
|
||||
expect(find.byType(TextField), findsNothing);
|
||||
|
||||
await pumpPanel(tester, showSearch: true);
|
||||
expect(find.byType(TextField), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('closing the search clears the filter', (tester) async {
|
||||
await pumpPanel(tester, showSearch: true);
|
||||
await tester.enterText(find.byType(TextField), 'Marc');
|
||||
await tester.pump();
|
||||
expect(find.text('anna'), findsNothing);
|
||||
|
||||
await pumpPanel(tester);
|
||||
expect(find.text('anna'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('a letter without names goes to the next one', (tester) async {
|
||||
await pumpPanel(tester);
|
||||
|
||||
await tapLetter(tester, 'Q');
|
||||
expect(firstVisibleName(tester), 'Rik');
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user