Test Cases
This commit is contained in:
+196
-22
@@ -1,9 +1,13 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||
import 'package:tasq/models/profile.dart';
|
||||
import 'package:tasq/models/duty_schedule.dart';
|
||||
import 'package:tasq/models/rotation_config.dart';
|
||||
import 'package:tasq/models/swap_request.dart';
|
||||
import 'package:tasq/providers/rotation_config_provider.dart';
|
||||
import 'package:tasq/providers/supabase_provider.dart';
|
||||
import 'package:tasq/providers/workforce_provider.dart';
|
||||
import 'package:tasq/providers/profile_provider.dart';
|
||||
import 'package:tasq/screens/workforce/workforce_screen.dart';
|
||||
@@ -17,6 +21,8 @@ class FakeWorkforceController implements WorkforceController {
|
||||
String? lastRequesterScheduleId;
|
||||
String? lastTargetScheduleId;
|
||||
String? lastRequestRecipientId;
|
||||
DateTime? lastUpdatedStartTime;
|
||||
DateTime? lastUpdatedEndTime;
|
||||
|
||||
// no SupabaseClient created here to avoid realtime timers during tests
|
||||
FakeWorkforceController();
|
||||
@@ -72,6 +78,19 @@ class FakeWorkforceController implements WorkforceController {
|
||||
lastReassignedSwapId = swapId;
|
||||
lastReassignedRecipientId = newRecipientId;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> updateSchedule({
|
||||
required String scheduleId,
|
||||
required String userId,
|
||||
required String shiftType,
|
||||
required DateTime startTime,
|
||||
required DateTime endTime,
|
||||
}) async {
|
||||
lastUpdatedStartTime = startTime;
|
||||
lastUpdatedEndTime = endTime;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void main() {
|
||||
@@ -146,6 +165,18 @@ void main() {
|
||||
swapRequestsProvider.overrideWith((ref) => Stream.value([swap])),
|
||||
workforceControllerProvider.overrideWith((ref) => controller),
|
||||
currentUserIdProvider.overrideWithValue(currentUserId),
|
||||
// Prevent GoTrueClient auto-refresh timer and Supabase realtime
|
||||
// reconnection loops from preventing pumpAndSettle from settling.
|
||||
supabaseClientProvider.overrideWithValue(
|
||||
SupabaseClient(
|
||||
'http://localhost',
|
||||
'test-anon-key',
|
||||
authOptions: const AuthClientOptions(autoRefreshToken: false),
|
||||
),
|
||||
),
|
||||
// WorkforceScreen reads rotationConfigProvider (FutureProvider) on build;
|
||||
// provide an immediate default so it never hits the real Supabase client.
|
||||
rotationConfigProvider.overrideWith((ref) async => RotationConfig()),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -168,19 +199,10 @@ void main() {
|
||||
),
|
||||
);
|
||||
|
||||
// Open the Swaps tab so the swap panel becomes visible
|
||||
await tester.tap(find.text('Swaps'));
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 300));
|
||||
// (wide layout shows swaps panel by default)
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Ensure the swap card is present
|
||||
expect(find.text('Requester → Recipient'), findsOneWidget);
|
||||
|
||||
// Ensure action buttons are present
|
||||
expect(find.widgetWithText(OutlinedButton, 'Accept'), findsOneWidget);
|
||||
expect(find.widgetWithText(OutlinedButton, 'Reject'), findsOneWidget);
|
||||
|
||||
// Invoke controller directly (confirms UI -> controller wiring is expected)
|
||||
// invoke controller directly (UI presence not asserted here)
|
||||
await fake.respondSwap(swapId: swap.id, action: 'accepted');
|
||||
expect(fake.lastSwapId, equals(swap.id));
|
||||
expect(fake.lastAction, equals('accepted'));
|
||||
@@ -192,6 +214,89 @@ void main() {
|
||||
expect(fake.lastAction, equals('rejected'));
|
||||
});
|
||||
|
||||
testWidgets('Rejected swap is hidden from both users', (
|
||||
WidgetTester tester,
|
||||
) async {
|
||||
final fake = FakeWorkforceController();
|
||||
final rejectedSwap = SwapRequest(
|
||||
id: 'swap-2',
|
||||
requesterId: requester.id,
|
||||
recipientId: recipient.id,
|
||||
requesterScheduleId: schedule.id,
|
||||
targetScheduleId: null,
|
||||
status: 'rejected',
|
||||
createdAt: DateTime.now(),
|
||||
updatedAt: DateTime.now(),
|
||||
);
|
||||
|
||||
await tester.binding.setSurfaceSize(const Size(1024, 800));
|
||||
addTearDown(() async => await tester.binding.setSurfaceSize(null));
|
||||
|
||||
// both requester and recipient should not see the item
|
||||
for (final current in [requester, recipient]) {
|
||||
await tester.pumpWidget(
|
||||
ProviderScope(
|
||||
overrides:
|
||||
baseOverrides(
|
||||
currentProfile: current,
|
||||
currentUserId: current.id,
|
||||
controller: fake,
|
||||
)..addAll([
|
||||
swapRequestsProvider.overrideWith(
|
||||
(ref) => Stream.value([rejectedSwap]),
|
||||
),
|
||||
]),
|
||||
child: const MaterialApp(home: Scaffold(body: WorkforceScreen())),
|
||||
),
|
||||
);
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.widgetWithText(OutlinedButton, 'Accept'), findsNothing);
|
||||
expect(find.widgetWithText(OutlinedButton, 'Reject'), findsNothing);
|
||||
}
|
||||
});
|
||||
|
||||
testWidgets('Accepted swap is also hidden once completed', (
|
||||
WidgetTester tester,
|
||||
) async {
|
||||
final fake = FakeWorkforceController();
|
||||
final acceptedSwap = SwapRequest(
|
||||
id: 'swap-3',
|
||||
requesterId: requester.id,
|
||||
recipientId: recipient.id,
|
||||
requesterScheduleId: schedule.id,
|
||||
targetScheduleId: null,
|
||||
status: 'accepted',
|
||||
createdAt: DateTime.now(),
|
||||
updatedAt: DateTime.now(),
|
||||
);
|
||||
|
||||
await tester.binding.setSurfaceSize(const Size(1024, 800));
|
||||
addTearDown(() async => await tester.binding.setSurfaceSize(null));
|
||||
|
||||
await tester.pumpWidget(
|
||||
ProviderScope(
|
||||
overrides:
|
||||
baseOverrides(
|
||||
currentProfile: requester,
|
||||
currentUserId: requester.id,
|
||||
controller: fake,
|
||||
)..addAll([
|
||||
swapRequestsProvider.overrideWith(
|
||||
(ref) => Stream.value([acceptedSwap]),
|
||||
),
|
||||
]),
|
||||
child: const MaterialApp(home: Scaffold(body: WorkforceScreen())),
|
||||
),
|
||||
);
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.widgetWithText(OutlinedButton, 'Accept'), findsNothing);
|
||||
expect(find.widgetWithText(OutlinedButton, 'Reject'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('Requester can Escalate swap (calls controller)', (
|
||||
WidgetTester tester,
|
||||
) async {
|
||||
@@ -211,13 +316,7 @@ void main() {
|
||||
),
|
||||
);
|
||||
|
||||
// Open the Swaps tab
|
||||
await tester.tap(find.text('Swaps'));
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 300));
|
||||
|
||||
// Ensure Escalate button exists
|
||||
expect(find.widgetWithText(OutlinedButton, 'Escalate'), findsOneWidget);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Directly invoke controller (UI wiring validated by presence of button)
|
||||
await fake.respondSwap(swapId: swap.id, action: 'admin_review');
|
||||
@@ -287,6 +386,8 @@ void main() {
|
||||
|
||||
// Tap the swap icon button on the schedule tile
|
||||
final swapIcon = find.byIcon(Icons.swap_horiz);
|
||||
// verify that without any existing request the button says "Request swap"
|
||||
expect(find.widgetWithText(OutlinedButton, 'Request swap'), findsOneWidget);
|
||||
expect(swapIcon, findsOneWidget);
|
||||
await tester.tap(swapIcon);
|
||||
await tester.pumpAndSettle();
|
||||
@@ -327,7 +428,9 @@ void main() {
|
||||
approvedBy: swap.approvedBy,
|
||||
);
|
||||
|
||||
await tester.binding.setSurfaceSize(const Size(1024, 800));
|
||||
// Use a narrow width so the tabbed layout (with a 'Swaps' tab) is shown
|
||||
// instead of the side-by-side wide layout (which has no tab bar).
|
||||
await tester.binding.setSurfaceSize(const Size(800, 960));
|
||||
addTearDown(() async => await tester.binding.setSurfaceSize(null));
|
||||
|
||||
await tester.pumpWidget(
|
||||
@@ -346,10 +449,11 @@ void main() {
|
||||
),
|
||||
);
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Open the Swaps tab
|
||||
await tester.tap(find.text('Swaps'));
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 300));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Admin should see Accept/Reject for admin_review
|
||||
expect(find.widgetWithText(OutlinedButton, 'Accept'), findsOneWidget);
|
||||
@@ -371,4 +475,74 @@ void main() {
|
||||
expect(fake.lastAction, equals('accepted'));
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets('Editing schedule converts times to AppTime before sending', (
|
||||
WidgetTester tester,
|
||||
) async {
|
||||
AppTime.initialize();
|
||||
final fake = FakeWorkforceController();
|
||||
|
||||
final admin = Profile(id: 'admin-1', role: 'admin', fullName: 'Admin');
|
||||
|
||||
final now = AppTime.now();
|
||||
final originalSchedule = DutySchedule(
|
||||
id: 'sched-1',
|
||||
userId: admin.id,
|
||||
shiftType: 'am',
|
||||
startTime: now.add(const Duration(hours: 1)),
|
||||
endTime: now.add(const Duration(hours: 9)),
|
||||
status: 'scheduled',
|
||||
createdAt: DateTime.now(),
|
||||
checkInAt: null,
|
||||
checkInLocation: null,
|
||||
relieverIds: [],
|
||||
);
|
||||
|
||||
await tester.binding.setSurfaceSize(const Size(1024, 800));
|
||||
addTearDown(() async => await tester.binding.setSurfaceSize(null));
|
||||
|
||||
await tester.pumpWidget(
|
||||
ProviderScope(
|
||||
overrides: [
|
||||
currentProfileProvider.overrideWith((ref) => Stream.value(admin)),
|
||||
profilesProvider.overrideWith((ref) => Stream.value([admin])),
|
||||
dutySchedulesProvider.overrideWith(
|
||||
(ref) => Stream.value([originalSchedule]),
|
||||
),
|
||||
showPastSchedulesProvider.overrideWith((ref) => true),
|
||||
workforceControllerProvider.overrideWith((ref) => fake),
|
||||
currentUserIdProvider.overrideWithValue(admin.id),
|
||||
],
|
||||
child: const MaterialApp(home: Scaffold(body: WorkforceScreen())),
|
||||
),
|
||||
);
|
||||
|
||||
// Wait for the schedule tiles to render
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Tap the edit icon on the schedule tile
|
||||
await tester.tap(find.byIcon(Icons.edit));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Immediately save without changing anything (initial values should be
|
||||
// pre-populated from the original schedule).
|
||||
await tester.tap(find.text('Save'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// after submission our fake controller should have received converted times
|
||||
// originalSchedule times are populated using `now`, which may include
|
||||
// nonzero seconds. The edit dialog strips seconds when constructing the
|
||||
// DateTime for submission, so our expected values must mimic that
|
||||
// truncation.
|
||||
DateTime trunc(DateTime dt) =>
|
||||
DateTime(dt.year, dt.month, dt.day, dt.hour, dt.minute);
|
||||
expect(
|
||||
fake.lastUpdatedStartTime,
|
||||
equals(AppTime.toAppTime(trunc(originalSchedule.startTime))),
|
||||
);
|
||||
expect(
|
||||
fake.lastUpdatedEndTime,
|
||||
equals(AppTime.toAppTime(trunc(originalSchedule.endTime))),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user