330 lines
12 KiB
Dart
330 lines
12 KiB
Dart
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<FakeApi> 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<void> 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<Switch>(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<Switch>(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<Switch>(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<Switch>(find.byType(Switch).last).onChanged, isNull);
|
|
});
|
|
|
|
group('dates and orders', () {
|
|
testWidgets('opens on today, even before its results are out', (t) async {
|
|
await boot(t);
|
|
await signIn(t, 'owner', 'admin123');
|
|
expect(find.textContaining('Fri, 16 Oct 2026'), findsOneWidget);
|
|
await t.tap(find.byTooltip('Next draw'));
|
|
await t.pumpAndSettle();
|
|
expect(find.text('No result yet'), findsOneWidget);
|
|
expect(find.text('AWAITING RESULTS'), findsOneWidget);
|
|
expect(find.textContaining('3:00 PM'), findsNothing);
|
|
});
|
|
|
|
testWidgets('orders can be searched by shop, message or ending', (t) async {
|
|
await boot(t);
|
|
await signIn(t, 'owner', 'admin123');
|
|
await t.tap(find.text('Orders').last);
|
|
await t.pumpAndSettle();
|
|
expect(find.text('Kazhakkoottam'), findsWidgets);
|
|
await t.enterText(find.byType(TextField), 'ulloor');
|
|
await t.pumpAndSettle();
|
|
expect(find.text('Kazhakkoottam'), findsNothing);
|
|
await t.enterText(find.byType(TextField), 'nothing like this');
|
|
await t.pumpAndSettle();
|
|
expect(find.text('No matching orders'), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('a slip sent with a note shows both', (t) async {
|
|
await boot(t);
|
|
await signIn(t, 'owner', 'admin123');
|
|
await t.tap(find.text('Orders').last);
|
|
await t.pumpAndSettle();
|
|
await t.tap(find.textContaining('Photo + text').first);
|
|
await t.pumpAndSettle();
|
|
expect(find.text('Handwritten slip'), findsOneWidget);
|
|
expect(find.textContaining('please add 4410'), findsOneWidget);
|
|
});
|
|
});
|
|
|
|
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);
|
|
});
|
|
});
|
|
|
|
group('automatic refresh', () {
|
|
Future<void> leaveAndReturn(WidgetTester t) async {
|
|
final b = t.binding;
|
|
b.handleAppLifecycleStateChanged(AppLifecycleState.inactive);
|
|
b.handleAppLifecycleStateChanged(AppLifecycleState.hidden);
|
|
b.handleAppLifecycleStateChanged(AppLifecycleState.paused);
|
|
b.handleAppLifecycleStateChanged(AppLifecycleState.hidden);
|
|
b.handleAppLifecycleStateChanged(AppLifecycleState.inactive);
|
|
b.handleAppLifecycleStateChanged(AppLifecycleState.resumed);
|
|
await t.pump();
|
|
await t.pump(const Duration(milliseconds: 400));
|
|
}
|
|
|
|
testWidgets('coming back reloads, but not more than once per 30 s', (
|
|
t,
|
|
) async {
|
|
final api = await boot(t);
|
|
await signIn(t, 'owner', 'admin123');
|
|
final start = api.resultsCalls;
|
|
|
|
await leaveAndReturn(t);
|
|
expect(api.resultsCalls, start + 1);
|
|
expect(find.text('₹7,700'), findsOneWidget);
|
|
|
|
await leaveAndReturn(t);
|
|
expect(api.resultsCalls, start + 1);
|
|
|
|
await t.pump(const Duration(seconds: 31));
|
|
await leaveAndReturn(t);
|
|
expect(api.resultsCalls, start + 2);
|
|
});
|
|
|
|
testWidgets('Orders reloads every minute, other screens do not', (t) async {
|
|
final api = await boot(t);
|
|
await signIn(t, 'owner', 'admin123');
|
|
await t.tap(find.text('Orders').last);
|
|
await t.pumpAndSettle();
|
|
final orders = api.ordersCalls;
|
|
final results = api.resultsCalls;
|
|
|
|
await t.pump(const Duration(seconds: 61));
|
|
await t.pump(const Duration(milliseconds: 400));
|
|
expect(api.ordersCalls, greaterThan(orders));
|
|
|
|
await t.tap(find.text('Shops').last);
|
|
await t.pumpAndSettle();
|
|
final after = api.ordersCalls;
|
|
await t.pump(const Duration(seconds: 61));
|
|
await t.pump(const Duration(milliseconds: 400));
|
|
expect(api.ordersCalls, after);
|
|
expect(api.resultsCalls, results);
|
|
});
|
|
|
|
testWidgets('a failed background reload is silent', (t) async {
|
|
final api = await boot(t);
|
|
await signIn(t, 'owner', 'admin123');
|
|
api.failResults = true;
|
|
await leaveAndReturn(t);
|
|
await t.pump(const Duration(seconds: 1));
|
|
expect(find.text('₹7,700'), findsOneWidget);
|
|
expect(find.textContaining("Couldn't refresh"), findsNothing);
|
|
api.failResults = false;
|
|
await t.pumpAndSettle(const Duration(seconds: 1));
|
|
});
|
|
});
|
|
}
|