tasq/test/task_assignment_section_test.dart

271 lines
7.8 KiB
Dart

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 <Profile>[]),
),
tasksProvider.overrideWith(
(ref) => Stream.value(<Task>[
Task.fromMap({
'id': 't',
'status': 'queued',
'title': 't',
'description': '',
'created_at': DateTime.now().toIso8601String(),
'priority': 1,
}),
]),
),
taskAssignmentsProvider.overrideWith(
(ref) => Stream.value(const <TaskAssignment>[]),
),
],
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<void>();
await tester.pumpWidget(
ProviderScope(
overrides: [
profilesProvider.overrideWith((ref) => Stream.value([profile])),
tasksProvider.overrideWith((ref) => Stream.value([task])),
taskAssignmentsProvider.overrideWith(
(ref) => Stream.value(const <TaskAssignment>[]),
),
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 <TaskAssignment>[]),
),
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<void> _completer;
@override
Future<void> replaceAssignments({
required String taskId,
required String? ticketId,
required List<String> newUserIds,
required List<String> currentUserIds,
}) {
return _completer.future;
}
@override
Future<void> removeAssignment({
required String taskId,
required String userId,
}) {
// not needed for these tests
return Future.value();
}
}
class _FailingController implements TaskAssignmentsController {
@override
Future<void> replaceAssignments({
required String taskId,
required String? ticketId,
required List<String> newUserIds,
required List<String> currentUserIds,
}) async {
throw Exception('server error');
}
@override
Future<void> removeAssignment({
required String taskId,
required String userId,
}) {
return Future.value();
}
}