import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:srs_app/app.dart'; import 'support/fake_api.dart'; import 'package:srs_app/core/providers.dart'; Future boot(WidgetTester t, {double width = 430}) async { final api = FakeApi(); t.view.physicalSize = Size(width * 2, 900 * 2); t.view.devicePixelRatio = 2; addTearDown(t.view.reset); await t.pumpWidget( ProviderScope( overrides: [ apiProvider.overrideWithValue(api), startDateProvider.overrideWithValue(DateTime(2026, 10, 16)), ], child: const SrsApp(), ), ); await t.pumpAndSettle(); return api; } Future signIn(WidgetTester t, String u, String p) async { await t.enterText(find.byType(TextField).at(0), u); await t.enterText(find.byType(TextField).at(1), p); await t.tap(find.text('Sign in').last); await t.pumpAndSettle(const Duration(milliseconds: 400)); } void main() { testWidgets('wrong password shows the error line', (t) async { await boot(t); expect(find.text('Welcome back'), findsOneWidget); expect( find.text('Forgot your password? Contact your administrator.'), findsOneWidget, ); await signIn(t, 'owner', 'bad-password'); expect(find.text('Wrong username or password'), findsOneWidget); }); testWidgets('admin sees results, four tabs and can open a shop', (t) async { await boot(t); await signIn(t, 'owner', 'admin123'); expect(find.text('₹7,700'), findsOneWidget); expect(find.text('Suvarna Keralam · SK-74'), findsOneWidget); expect(find.text('Users'), findsOneWidget); await t.tap(find.text('Shops').last); await t.pumpAndSettle(); expect(find.text('Add shop'), findsOneWidget); expect(find.text('180 tickets · 2 orders'), findsOneWidget); }); testWidgets('normal user has three tabs and no Add shop button', (t) async { await boot(t); await signIn(t, 'kzk.staff', 'staff123'); expect(find.text('₹6,300'), findsOneWidget); expect(find.text('Users'), findsNothing); await t.tap(find.text('Shops').last); await t.pumpAndSettle(); expect(find.text('Add shop'), findsNothing); }); testWidgets('order detail lets a user delete the order after confirming', ( t, ) async { await boot(t); await signIn(t, 'kzk.staff', 'staff123'); await t.tap(find.text('Orders').last); await t.pumpAndSettle(); expect(find.text('Orders'), findsWidgets); await t.tap(find.textContaining('Photo').first); await t.pumpAndSettle(); await t.scrollUntilVisible( find.text('Delete order'), 400, scrollable: find.byType(Scrollable).first, ); await t.tap(find.text('Delete order')); await t.pumpAndSettle(); expect(find.text('Delete this order?'), findsOneWidget); await t.tap(find.text('Delete order').last); await t.pumpAndSettle(const Duration(milliseconds: 500)); expect(find.text('Delete this order?'), findsNothing); }); testWidgets('admin: user toggle and delete both ask for confirmation', ( t, ) async { await boot(t); await signIn(t, 'owner', 'admin123'); await t.tap(find.text('Users').last); await t.pumpAndSettle(); await t.tap(find.text('skm.staff')); await t.pumpAndSettle(); await t.tap(find.byType(Switch).first); await t.pumpAndSettle(); expect(find.text('Deactivate skm.staff?'), findsOneWidget); await t.tap(find.text('Cancel')); await t.pumpAndSettle(); expect(t.widget(find.byType(Switch).first).value, isTrue); // To the end of the page, clear of the tab bar. await t.drag(find.byType(Scrollable).first, const Offset(0, -600)); await t.pumpAndSettle(); await t.tap(find.text('Delete user')); await t.pumpAndSettle(); expect(find.text('Delete skm.staff?'), findsOneWidget); await t.tap(find.text('Delete user').last); await t.pumpAndSettle(const Duration(milliseconds: 500)); expect(find.text('skm.staff'), findsNothing); }); testWidgets('admin: results messages switch, and no phone means no switch', ( t, ) async { await boot(t); await signIn(t, 'owner', 'admin123'); await t.tap(find.text('Users').last); await t.pumpAndSettle(); expect(find.byTooltip('Gets results on WhatsApp'), findsOneWidget); await t.tap(find.text('kzk.staff')); await t.pumpAndSettle(); final results = find.byType(Switch).last; await t.scrollUntilVisible( find.text('WhatsApp results'), 300, scrollable: find.byType(Scrollable).first, ); expect(t.widget(results).value, isTrue); expect(find.text('+91 90000 11111'), findsWidgets); await t.tap(results); await t.pumpAndSettle(const Duration(milliseconds: 400)); expect(find.text('WhatsApp results off'), findsOneWidget); expect(t.widget(find.byType(Switch).last).value, isFalse); await t.tap(find.byTooltip('Back')); await t.pumpAndSettle(const Duration(milliseconds: 400)); expect(find.byTooltip('Gets results on WhatsApp'), findsNothing); await t.tap(find.text('skm.staff')); await t.pumpAndSettle(); await t.scrollUntilVisible( find.text('Add a phone number first'), 300, scrollable: find.byType(Scrollable).first, ); expect(t.widget(find.byType(Switch).last).onChanged, isNull); }); group('loading and refresh', () { testWidgets('a new date shows a skeleton until its results arrive', ( t, ) async { await boot(t); await signIn(t, 'owner', 'admin123'); await t.tap(find.byTooltip('Previous draw')); await t.pump(); await t.pump(const Duration(milliseconds: 50)); expect(find.bySemanticsLabel('Loading'), findsOneWidget); // The draw buttons stay usable while the draw loads. expect(find.byTooltip('Next draw'), findsWidgets); await t.pumpAndSettle(); expect(find.bySemanticsLabel('Loading'), findsNothing); }); testWidgets('pulling down reloads results and keeps them on screen', ( t, ) async { final api = await boot(t); await signIn(t, 'owner', 'admin123'); final before = api.resultsCalls; await t.fling(find.text('₹7,700'), const Offset(0, 400), 1000); await t.pump(); await t.pump(const Duration(milliseconds: 50)); expect(find.text('₹7,700'), findsOneWidget); expect(find.bySemanticsLabel('Loading'), findsNothing); await t.pumpAndSettle(); expect(api.resultsCalls, before + 1); expect(find.text('₹7,700'), findsOneWidget); }); testWidgets('a failed pull keeps the data and says so quickly', (t) async { final api = await boot(t); await signIn(t, 'owner', 'admin123'); api.failResults = true; await t.fling(find.text('₹7,700'), const Offset(0, 400), 1000); await t.pump(); await t.pump(const Duration(seconds: 1)); // One frame for the error to reach listeners, one for the toast. await t.pump(); await t.pump(const Duration(milliseconds: 300)); expect( find.text("Couldn't refresh. Check your connection and try again."), findsOneWidget, ); expect(find.text('₹7,700'), findsOneWidget); api.failResults = false; await t.pumpAndSettle(const Duration(seconds: 1)); }); testWidgets('a failed first load shows Retry, which shows the skeleton', ( t, ) async { final api = await boot(t); await signIn(t, 'owner', 'admin123'); api.failResults = true; await t.tap(find.byTooltip('Previous draw')); await t.pump(); await t.pump(const Duration(seconds: 1)); expect(find.text('Unable to reach the server.'), findsOneWidget); api.failResults = false; await t.tap(find.text('Retry')); await t.pump(); await t.pump(const Duration(milliseconds: 50)); expect(find.text('Retry'), findsNothing); await t.pumpAndSettle(); expect(find.text('Unable to reach the server.'), findsNothing); expect(find.text('Retry'), findsNothing); }); testWidgets('Refresh button shows on wide windows only, and reloads', ( t, ) async { final api = await boot(t, width: 1280); await signIn(t, 'owner', 'admin123'); final before = api.resultsCalls; await t.tap(find.byTooltip('Refresh')); await t.pump(); await t.pump(const Duration(milliseconds: 50)); expect(find.bySemanticsLabel('Refreshing'), findsOneWidget); await t.pumpAndSettle(); expect(api.resultsCalls, before + 1); expect(find.bySemanticsLabel('Refreshing'), findsNothing); t.view.physicalSize = const Size(430 * 2, 900 * 2); await t.pumpAndSettle(); expect(find.byTooltip('Refresh'), findsNothing); }); }); }