import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'dart:async'; import 'package:tasq/models/profile.dart'; import 'package:tasq/models/task.dart'; import 'package:tasq/models/task_assignment.dart'; import 'package:tasq/providers/profile_provider.dart'; import 'package:tasq/providers/tasks_provider.dart'; import 'package:tasq/utils/app_time.dart'; import 'package:tasq/widgets/task_assignment_section.dart'; void main() { setUp(() { AppTime.initialize(); }); testWidgets( 'allows assigning IT staff to a task even when they are already on another active task', (tester) async { // prepare two it_staff profiles final profile1 = Profile( id: 'u1', fullName: 'User One', role: 'it_staff', ); final profile2 = Profile( id: 'u2', fullName: 'User Two', role: 'it_staff', ); // create two active tasks; staff u1 is already assigned to the first final now = DateTime.now(); final task1 = Task.fromMap({ 'id': 't1', 'status': 'queued', 'title': 'First task', 'description': '', 'created_at': now.toIso8601String(), 'priority': 1, }); final task2 = Task.fromMap({ 'id': 't2', 'status': 'queued', 'title': 'Second task', 'description': '', 'created_at': now.toIso8601String(), 'priority': 1, }); final assignment1 = TaskAssignment( taskId: 't1', userId: 'u1', createdAt: now, ); await tester.pumpWidget( ProviderScope( overrides: [ profilesProvider.overrideWith( (ref) => Stream.value([profile1, profile2]), ), tasksProvider.overrideWith((ref) => Stream.value([task1, task2])), taskAssignmentsProvider.overrideWith( (ref) => Stream.value([assignment1]), ), ], child: MaterialApp( home: Scaffold( body: TaskAssignmentSection(taskId: 't2', canAssign: true), ), ), ), ); // allow provider streams to deliver their first value await tester.pumpAndSettle(); // open the assignment dialog await tester.tap(find.text('Assign')); await tester.pumpAndSettle(); // both IT staff users should be listed even though u1 is already busy expect(find.byType(CheckboxListTile), findsNWidgets(2)); expect(find.widgetWithText(CheckboxListTile, 'User One'), findsOneWidget); expect(find.widgetWithText(CheckboxListTile, 'User Two'), findsOneWidget); }, ); testWidgets('dialog shows message when there are no IT staff at all', ( tester, ) async { await tester.pumpWidget( ProviderScope( overrides: [ profilesProvider.overrideWith( (ref) => Stream.value(const []), ), tasksProvider.overrideWith( (ref) => Stream.value([ Task.fromMap({ 'id': 't', 'status': 'queued', 'title': 't', 'description': '', 'created_at': DateTime.now().toIso8601String(), 'priority': 1, }), ]), ), taskAssignmentsProvider.overrideWith( (ref) => Stream.value(const []), ), ], child: MaterialApp( home: Scaffold( body: TaskAssignmentSection(taskId: 't', canAssign: true), ), ), ), ); await tester.pumpAndSettle(); await tester.tap(find.text('Assign')); await tester.pumpAndSettle(); expect(find.text('No IT staff available.'), findsOneWidget); }); testWidgets('save button shows spinner and snack on success', (tester) async { final profile = Profile(id: 'u1', fullName: 'User One', role: 'it_staff'); final task = Task.fromMap({ 'id': 't', 'status': 'queued', 'title': 't', 'description': '', 'created_at': DateTime.now().toIso8601String(), 'priority': 1, }); final completer = Completer(); await tester.pumpWidget( ProviderScope( overrides: [ profilesProvider.overrideWith((ref) => Stream.value([profile])), tasksProvider.overrideWith((ref) => Stream.value([task])), taskAssignmentsProvider.overrideWith( (ref) => Stream.value(const []), ), taskAssignmentsControllerProvider.overrideWith( (ref) => _DelayedController(completer), ), ], child: MaterialApp( home: Scaffold( body: TaskAssignmentSection(taskId: 't', canAssign: true), ), ), ), ); await tester.pumpAndSettle(); // open dialog and select the only staff await tester.tap(find.text('Assign')); await tester.pumpAndSettle(); await tester.tap(find.byType(CheckboxListTile)); await tester.pump(); // tap save, spinner should appear await tester.tap(find.text('Save')); await tester.pump(); expect(find.byType(CircularProgressIndicator), findsOneWidget); // resolve future and let dialog close completer.complete(); await tester.pumpAndSettle(); // snackbar with success message should be visible expect(find.text('Assignment saved successfully'), findsOneWidget); }); testWidgets('error snackbar shows when save fails', (tester) async { final profile = Profile(id: 'u1', fullName: 'User One', role: 'it_staff'); final task = Task.fromMap({ 'id': 't', 'status': 'queued', 'title': 't', 'description': '', 'created_at': DateTime.now().toIso8601String(), 'priority': 1, }); await tester.pumpWidget( ProviderScope( overrides: [ profilesProvider.overrideWith((ref) => Stream.value([profile])), tasksProvider.overrideWith((ref) => Stream.value([task])), taskAssignmentsProvider.overrideWith( (ref) => Stream.value(const []), ), taskAssignmentsControllerProvider.overrideWith( (ref) => _FailingController(), ), ], child: MaterialApp( home: Scaffold( body: TaskAssignmentSection(taskId: 't', canAssign: true), ), ), ), ); await tester.pumpAndSettle(); await tester.tap(find.text('Assign')); await tester.pumpAndSettle(); await tester.tap(find.byType(CheckboxListTile)); await tester.pump(); await tester.tap(find.text('Save')); await tester.pumpAndSettle(); expect(find.text('Failed to save assignment'), findsOneWidget); }); } // helper fakes used by the tests below class _DelayedController implements TaskAssignmentsController { _DelayedController(this._completer); final Completer _completer; @override Future replaceAssignments({ required String taskId, required String? ticketId, required List newUserIds, required List currentUserIds, }) { return _completer.future; } @override Future removeAssignment({ required String taskId, required String userId, }) { // not needed for these tests return Future.value(); } } class _FailingController implements TaskAssignmentsController { @override Future replaceAssignments({ required String taskId, required String? ticketId, required List newUserIds, required List currentUserIds, }) async { throw Exception('server error'); } @override Future removeAssignment({ required String taskId, required String userId, }) { return Future.value(); } }