210 lines
5.6 KiB
Dart
210 lines
5.6 KiB
Dart
import 'dart:async';
|
|
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:kooltap/models/bar_tab.dart';
|
|
import 'package:kooltap/models/payment_method.dart';
|
|
import 'package:kooltap/models/tab_item.dart';
|
|
import 'package:kooltap/services/bar_tab_service.dart';
|
|
import 'package:kooltap/viewmodels/bar_screen_view_model.dart';
|
|
import 'package:mocktail/mocktail.dart';
|
|
|
|
class MockBarTabService extends Mock implements BarTabService {}
|
|
|
|
BarTab _tab(String id, {int drinks = 0}) {
|
|
return BarTab(
|
|
id: id,
|
|
customerName: id,
|
|
openedAt: DateTime(2026),
|
|
items: [
|
|
if (drinks > 0)
|
|
TabItem(
|
|
id: '$id-item',
|
|
tabId: id,
|
|
productId: 'prod-1',
|
|
productName: 'Beer',
|
|
quantity: drinks,
|
|
unitPriceInCents: 400,
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
void main() {
|
|
late MockBarTabService barTabService;
|
|
late StreamController<List<BarTab>> openTabs;
|
|
late BarScreenViewModel viewModel;
|
|
|
|
setUp(() {
|
|
barTabService = MockBarTabService();
|
|
openTabs = StreamController<List<BarTab>>.broadcast();
|
|
when(
|
|
() => barTabService.watchOpenTabs(),
|
|
).thenAnswer((_) => openTabs.stream);
|
|
|
|
viewModel = BarScreenViewModel(barTabService: barTabService);
|
|
});
|
|
|
|
tearDown(() async {
|
|
viewModel.dispose();
|
|
await openTabs.close();
|
|
});
|
|
|
|
test('starts with no open tabs and nothing selected', () async {
|
|
viewModel.ensureLoaded();
|
|
expect(viewModel.isLoading, true);
|
|
|
|
openTabs.add([_tab('a'), _tab('b')]);
|
|
await pumpEventQueue();
|
|
|
|
expect(viewModel.isLoading, false);
|
|
expect(viewModel.tabs, isEmpty);
|
|
expect(viewModel.closedTabs.map((tab) => tab.id), ['a', 'b']);
|
|
expect(viewModel.selectedTabId, isNull);
|
|
});
|
|
|
|
test('tabs with drinks on them are open', () async {
|
|
viewModel.ensureLoaded();
|
|
openTabs.add([_tab('a', drinks: 2), _tab('b')]);
|
|
await pumpEventQueue();
|
|
|
|
expect(viewModel.tabs.map((tab) => tab.id), ['a']);
|
|
expect(viewModel.closedTabs.map((tab) => tab.id), ['b']);
|
|
});
|
|
|
|
test('opening a tab shows and selects it', () async {
|
|
viewModel.ensureLoaded();
|
|
openTabs.add([_tab('a'), _tab('b')]);
|
|
await pumpEventQueue();
|
|
|
|
viewModel.openTab('b');
|
|
viewModel.deselectTab();
|
|
|
|
expect(viewModel.tabs.map((tab) => tab.id), ['b']);
|
|
expect(viewModel.closedTabs.map((tab) => tab.id), ['a']);
|
|
});
|
|
|
|
test('a tab settled elsewhere goes back to the closed tabs', () async {
|
|
viewModel.ensureLoaded();
|
|
openTabs.add([_tab('a', drinks: 2)]);
|
|
await pumpEventQueue();
|
|
viewModel.openTab('a');
|
|
viewModel.deselectTab();
|
|
|
|
openTabs.add([_tab('a')]);
|
|
await pumpEventQueue();
|
|
|
|
expect(viewModel.tabs, isEmpty);
|
|
expect(viewModel.closedTabs.map((tab) => tab.id), ['a']);
|
|
});
|
|
|
|
test('closing the selected tab moves it back and deselects', () async {
|
|
when(
|
|
() => barTabService.closeTab(
|
|
'a',
|
|
paymentMethod: PaymentMethod.cash,
|
|
),
|
|
).thenAnswer((_) async {});
|
|
viewModel.ensureLoaded();
|
|
openTabs.add([_tab('a'), _tab('b')]);
|
|
await pumpEventQueue();
|
|
viewModel.openTab('a');
|
|
|
|
await viewModel.closeSelectedTab();
|
|
|
|
expect(viewModel.selectedTabId, isNull);
|
|
expect(viewModel.tabs, isEmpty);
|
|
});
|
|
|
|
test('keeps the selection while that tab still exists', () async {
|
|
viewModel.ensureLoaded();
|
|
openTabs.add([_tab('a'), _tab('b')]);
|
|
await pumpEventQueue();
|
|
viewModel.selectTab('b');
|
|
|
|
openTabs.add([_tab('c'), _tab('b')]);
|
|
await pumpEventQueue();
|
|
|
|
expect(viewModel.selectedTabId, 'b');
|
|
});
|
|
|
|
test('selects nothing when the selected tab disappears', () async {
|
|
viewModel.ensureLoaded();
|
|
openTabs.add([_tab('a'), _tab('b')]);
|
|
await pumpEventQueue();
|
|
viewModel.selectTab('b');
|
|
|
|
openTabs.add([_tab('a')]);
|
|
await pumpEventQueue();
|
|
|
|
expect(viewModel.selectedTabId, isNull);
|
|
});
|
|
|
|
test('exposes a load error when the stream fails', () async {
|
|
viewModel.ensureLoaded();
|
|
openTabs.addError(StateError('database closed'));
|
|
await pumpEventQueue();
|
|
|
|
expect(viewModel.loadError, isNotNull);
|
|
expect(viewModel.isLoading, false);
|
|
});
|
|
|
|
test('propagates an error from the transaction', () async {
|
|
const item = TabItem(
|
|
id: 'item-1',
|
|
tabId: 'tab-1',
|
|
productId: 'prod-1',
|
|
productName: 'Chips',
|
|
quantity: 20,
|
|
unitPriceInCents: 150,
|
|
);
|
|
|
|
when(
|
|
() => barTabService.adjustTabItemQuantity(
|
|
tabItemId: 'item-1',
|
|
delta: 1,
|
|
order: any(named: 'order'),
|
|
),
|
|
).thenAnswer((_) async => throw const TabNotFoundException('tab-1'));
|
|
|
|
await expectLater(
|
|
viewModel.changeItemQuantity(item, 1, null),
|
|
throwsA(isA<TabNotFoundException>()),
|
|
);
|
|
|
|
expect(viewModel.loadError, isNull);
|
|
});
|
|
|
|
test('selects a newly created tab', () async {
|
|
when(
|
|
() => barTabService.createTab(customerName: 'New', email: null),
|
|
).thenAnswer((_) async => _tab('new'));
|
|
|
|
await viewModel.createTab('New');
|
|
|
|
expect(viewModel.selectedTabId, 'new');
|
|
});
|
|
|
|
test(
|
|
'rethrows failed actions without turning them into a load error',
|
|
() async {
|
|
when(
|
|
() => barTabService.deleteTab('tab-1'),
|
|
).thenAnswer((_) async => throw StateError('boom'));
|
|
|
|
await expectLater(viewModel.deleteTab('tab-1'), throwsStateError);
|
|
|
|
expect(viewModel.loadError, isNull);
|
|
},
|
|
);
|
|
|
|
test('finds an open tab by name, ignoring case and spaces', () async {
|
|
viewModel.ensureLoaded();
|
|
openTabs.add([_tab('Jan'), _tab('Piet')]);
|
|
await pumpEventQueue();
|
|
|
|
expect(viewModel.hasOpenTabNamed(' jan '), true);
|
|
expect(viewModel.hasOpenTabNamed('Jan', exceptTabId: 'Jan'), false);
|
|
expect(viewModel.hasOpenTabNamed('Joris'), false);
|
|
});
|
|
}
|