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/app.dart'; import 'package:nearle_pos/data/datasources/local_store.dart'; import 'package:nearle_pos/data/datasources/seed_data.dart'; import 'package:nearle_pos/app/providers.dart'; import 'package:nearle_pos/domain/entities/shift_report.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/pos/providers/cart_controller.dart'; import 'package:nearle_pos/presentation/pos/screens/pos_dashboard_screen.dart'; import 'package:nearle_pos/presentation/sync/providers/sync_controller.dart'; /// Boots the real application widget. /// /// The unit suite proves the money is right; this proves the thing actually /// assembles — router guard, provider graph, theme and shell — which type /// checking alone cannot tell you. void main() { setUpAll(() { // Tests have no network, and an attempted font fetch throws. GoogleFonts.config.allowRuntimeFetching = false; LocalStore.registerSeed( products: SeedData.products, customers: SeedData.customers, ); }); setUp(() async { await LocalStore.instance.reset(withCatalogue: true); }); const testStore = StoreAccount( id: 'store-001', name: 'Nearle Daily', email: DemoCredentials.email, address: '1 Test Street', gstin: '33AABCU9603R1ZM', phone: '9840000000', staff: [ StaffUser(id: 'u1', name: 'Suriya', role: StaffRole.admin), ], ); ShiftReport blankReport() => ShiftReport.blank( businessDate: DateTime(2026, 7, 31), terminalId: 'TERM-01', cashierName: 'Suriya', ); Future bootApp(WidgetTester tester) async { await tester.pumpWidget( ProviderScope( overrides: [ // The background drain would open a broker connection and hit the // disk on a clock this test controls. Neither is what these tests // measure, and a half-driven timer would leak into the next one. syncBootstrapProvider.overrideWith((ref) async {}), // Sign-in now reads staff and store details from SQLite. Real disk // I/O cannot complete inside a fixed number of pumps on a fake // clock, so the sign-in would hang and every later assertion would // fail on a screen that never arrived. storeAccountProvider.overrideWith((ref) async => testStore), // Catalogue reads come from the in-memory cache and resolve on the // spot, but these four go to SQLite. Real disk I/O cannot be driven // by the fake clock a widget test runs on: sqflite's own lock-warning // timer is left pending and trips the binding's leak check. Stubbed // so this test measures rendering, which is what it is for. unsyncedCountProvider.overrideWith((ref) async => 0), parkedBillsProvider.overrideWith((ref) async => []), orderSyncRowsProvider.overrideWith((ref) async => []), todayReportProvider.overrideWith((ref) async => blankReport()), myShiftReportProvider.overrideWith((ref) async => blankReport()), ], child: const NearlePosApp(), ), ); await tester.pumpAndSettle(); } /// Pumps a fixed number of frames instead of settling. /// /// Once past login the header subscribes to a periodic clock, so there is /// always another frame pending and `pumpAndSettle` never returns. Future settle(WidgetTester tester, {int frames = 15}) async { for (var i = 0; i < frames; i++) { await tester.pump(const Duration(milliseconds: 100)); } } Future signIn(WidgetTester tester) async { final fields = find.byType(TextFormField); await tester.enterText(fields.first, DemoCredentials.email); await tester.enterText(fields.at(1), DemoCredentials.password); await tester.pump(); await tester.tap(find.text('Sign in').last); await settle(tester); } testWidgets('the terminal starts on the login screen', (tester) async { await bootApp(tester); expect(find.byType(TextFormField), findsWidgets); expect(find.text('Sign in'), findsWidgets); expect(tester.takeException(), isNull); }); testWidgets('the auth guard keeps an unauthenticated terminal out', (tester) async { await bootApp(tester); expect(find.byType(PosDashboardScreen), findsNothing); }); testWidgets('signing in reaches the billing terminal', (tester) async { await bootApp(tester); await signIn(tester); expect(find.byType(PosDashboardScreen), findsOneWidget); expect(tester.takeException(), isNull); }); testWidgets('the shell renders every module without throwing', (tester) async { // Wide enough that the sidebar shows labels and the bill stays docked — // the layout where a button label and its total compete for width. tester.view.physicalSize = const Size(1800, 1200); tester.view.devicePixelRatio = 1; addTearDown(tester.view.reset); await bootApp(tester); await signIn(tester); for (final label in [ 'Customers', 'Product Import', 'Promo', 'Events', 'Settings', 'POS', ]) { final target = find.text(label); expect(target, findsWidgets, reason: 'no sidebar entry for "$label"'); await tester.tap(target.first); await settle(tester); expect(tester.takeException(), isNull, reason: 'opening "$label" threw'); } }); testWidgets('the back office connection dialog opens and validates', (tester) async { // The only way a shop can point a till at a broker. Until it existed a // store was wired up by editing a provider and rebuilding. tester.view.physicalSize = const Size(1800, 1200); tester.view.devicePixelRatio = 1; addTearDown(tester.view.reset); await bootApp(tester); await signIn(tester); await tester.tap(find.text('Settings').first); await settle(tester); await tester.tap(find.text('Configure').first); await settle(tester); // "Back office connection" is both the dialog title and the About card's // button, so match the dialog itself. expect(find.byType(AlertDialog), findsOneWidget); expect(find.text('MQTT'), findsOneWidget); // Switching to MQTT and saving with no host must be refused, not silently // accepted — a terminal pointed at nothing looks identical to one that is // simply offline. await tester.tap(find.text('MQTT')); await settle(tester); await tester.tap(find.text('Save')); await settle(tester); expect(find.text('A broker host is required'), findsOneWidget); expect(find.byType(AlertDialog), findsOneWidget, reason: 'the dialog must stay open on a validation failure',); expect(tester.takeException(), isNull); }); }