import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:nearle_pos/app/providers.dart'; import 'package:nearle_pos/data/datasources/local_store.dart'; import 'package:nearle_pos/data/datasources/seed_data.dart'; import 'package:nearle_pos/domain/entities/store_account.dart'; import 'package:nearle_pos/presentation/auth/providers/auth_controller.dart'; import 'package:nearle_pos/presentation/modules/widgets/staff_dialogs.dart'; import 'package:nearle_pos/presentation/modules/widgets/store_details_dialog.dart'; /// The two admin screens that change what a till is and who may use it. /// /// Both guard on role, and both refuse input that would break something a shop /// cannot recover from on its own — a locked-out account, or a GSTIN that is /// wrong on every invoice printed after it. void main() { setUpAll(() { GoogleFonts.config.allowRuntimeFetching = false; LocalStore.registerSeed( products: SeedData.products, customers: SeedData.customers, ); }); setUp(() async { await LocalStore.instance.reset(withCatalogue: true); }); const admin = StaffUser(id: 'u1', name: 'Suriya', role: StaffRole.admin); const cashier = StaffUser(id: 'u3', name: 'Rahul', role: StaffRole.cashier); StoreAccount storeWith(List staff) => StoreAccount( id: 'store-001', name: 'Nearle Daily', email: 'admin@nearle.in', address: '1 Test Street', gstin: '33AABCU9603R1ZM', phone: '9840000000', staff: staff, ); /// Mounts a dialog with [who] signed in. Future open( WidgetTester tester, { required StaffUser who, required Future Function(BuildContext) show, List staff = const [admin, cashier], }) async { await tester.pumpWidget( ProviderScope( overrides: [ storeAccountProvider.overrideWith((ref) async => storeWith(staff)), authControllerProvider.overrideWith( (ref) => _StubAuth(storeWith(staff), who), ), ], child: MaterialApp( home: Builder( builder: (context) => Scaffold( body: TextButton( onPressed: () => show(context), child: const Text('open'), ), ), ), ), ), ); await tester.tap(find.text('open')); await tester.pumpAndSettle(); } group('staff', () { testWidgets('a cashier is refused', (tester) async { // Anyone who can edit staff can make themselves an admin, so the check // has to be at the door rather than on each action. await open(tester, who: cashier, show: showStaffDialog); expect(find.textContaining('Only an admin'), findsOneWidget); expect(find.text('Add staff member'), findsNothing); }); testWidgets('an admin can manage staff', (tester) async { await open(tester, who: admin, show: showStaffDialog); expect(find.text('Add staff member'), findsOneWidget); expect(find.text('Suriya'), findsOneWidget); expect(find.text('Rahul'), findsOneWidget); }); testWidgets('a mistyped confirmation is caught before it locks an account', (tester) async { // There is no email to reset a PIN with. A typo nobody can verify means // the account is simply gone until an admin resets it. await open(tester, who: admin, show: showStaffDialog); await tester.tap(find.text('Add staff member')); await tester.pumpAndSettle(); await tester.enterText(find.byType(TextFormField).first, 'Meena'); await tester.enterText(find.byType(TextFormField).at(1), '7391'); await tester.enterText(find.byType(TextFormField).at(2), '7392'); await tester.tap(find.text('Add')); await tester.pumpAndSettle(); expect(find.text('The two PINs do not match'), findsOneWidget); }); testWidgets('a too-short PIN is refused', (tester) async { await open(tester, who: admin, show: showStaffDialog); await tester.tap(find.text('Add staff member')); await tester.pumpAndSettle(); await tester.enterText(find.byType(TextFormField).first, 'Meena'); await tester.enterText(find.byType(TextFormField).at(1), '73'); await tester.enterText(find.byType(TextFormField).at(2), '73'); await tester.tap(find.text('Add')); await tester.pumpAndSettle(); expect(find.text('At least four digits'), findsOneWidget); }); testWidgets('an account still on a shipped PIN is flagged', (tester) async { await open( tester, who: admin, show: showStaffDialog, staff: const [ admin, StaffUser( id: 'u9', name: 'Newbie', role: StaffRole.cashier, mustChangePin: true, ), ], ); expect(find.byIcon(Icons.warning_amber_rounded), findsOneWidget); }); }); group('store details', () { testWidgets('a cashier cannot change what invoices claim', (tester) async { await open(tester, who: cashier, show: showStoreDetailsDialog); expect(find.textContaining('Only an admin'), findsOneWidget); }); testWidgets('a malformed GSTIN is refused', (tester) async { // It prints on every invoice as a legal requirement, so a typo is a // compliance problem across hundreds of bills before anyone notices. await open(tester, who: admin, show: showStoreDetailsDialog); await tester.enterText(find.byType(TextFormField).at(2), '99AABCU9603R1ZM'); await tester.tap(find.text('Save')); await tester.pumpAndSettle(); expect( find.text('The first two digits are not a valid state code.'), findsOneWidget, ); }); testWidgets('an empty seller name is refused', (tester) async { await open(tester, who: admin, show: showStoreDetailsDialog); await tester.enterText(find.byType(TextFormField).first, ''); await tester.tap(find.text('Save')); await tester.pumpAndSettle(); expect(find.text('An invoice must name the seller'), findsOneWidget); }); }); } /// Holds a fixed session so a test can choose who is signed in. class _StubAuth extends AuthController { _StubAuth(StoreAccount store, StaffUser user) : super(_throwingRef) { state = Authenticated(store: store, user: user); } @override Future refreshStore() async {} } /// The stub never reaches the real container, so a Ref is never used. final Ref _throwingRef = _UnusedRef(); class _UnusedRef implements Ref { @override dynamic noSuchMethod(Invocation invocation) => throw UnsupportedError('The stubbed AuthController does not read providers.'); }