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> openTabs; late BarScreenViewModel viewModel; setUp(() { barTabService = MockBarTabService(); openTabs = StreamController>.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()), ); 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); }); }