A better state and alerts
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
import 'dart:async';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:awesome_snackbar_content/awesome_snackbar_content.dart';
|
||||
|
||||
import 'package:tasq/models/notification_item.dart';
|
||||
import 'package:tasq/models/office.dart';
|
||||
@@ -48,6 +50,73 @@ class FakeNotificationsController implements NotificationsController {
|
||||
Future<void> markReadForTask(String taskId) async {}
|
||||
}
|
||||
|
||||
// test doubles for controllers that allow us to intercept create operations
|
||||
class _FakeTicketsController implements TicketsController {
|
||||
Future<void> Function({
|
||||
required String subject,
|
||||
required String description,
|
||||
required String officeId,
|
||||
})?
|
||||
onCreate;
|
||||
|
||||
@override
|
||||
Future<void> createTicket({
|
||||
required String subject,
|
||||
required String description,
|
||||
required String officeId,
|
||||
}) async {
|
||||
if (onCreate != null) {
|
||||
await onCreate!(
|
||||
subject: subject,
|
||||
description: description,
|
||||
officeId: officeId,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
|
||||
}
|
||||
|
||||
class _FakeTasksController implements TasksController {
|
||||
Future<void> Function({
|
||||
required String title,
|
||||
required String description,
|
||||
String? officeId,
|
||||
String? ticketId,
|
||||
String? requestType,
|
||||
String? requestTypeOther,
|
||||
String? requestCategory,
|
||||
})?
|
||||
onCreate;
|
||||
|
||||
@override
|
||||
Future<void> createTask({
|
||||
required String title,
|
||||
required String description,
|
||||
String? officeId,
|
||||
String? ticketId,
|
||||
String? requestType,
|
||||
String? requestTypeOther,
|
||||
String? requestCategory,
|
||||
}) async {
|
||||
if (onCreate != null) {
|
||||
await onCreate!(
|
||||
title: title,
|
||||
description: description,
|
||||
officeId: officeId,
|
||||
ticketId: ticketId,
|
||||
requestType: requestType,
|
||||
requestTypeOther: requestTypeOther,
|
||||
requestCategory: requestCategory,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
|
||||
}
|
||||
|
||||
void main() {
|
||||
final now = DateTime(2026, 2, 10, 12, 0, 0);
|
||||
final office = Office(id: 'office-1', name: 'HQ');
|
||||
@@ -247,6 +316,182 @@ void main() {
|
||||
find.text('Assign at least one office to the team'),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(find.byType(AwesomeSnackbarContent), findsOneWidget);
|
||||
expect(find.byIcon(Icons.warning_amber_rounded), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('Office creation shows descriptive success message', (
|
||||
tester,
|
||||
) async {
|
||||
await _setSurfaceSize(tester, const Size(600, 800));
|
||||
await _pumpScreen(
|
||||
tester,
|
||||
const OfficesScreen(),
|
||||
overrides: baseOverrides(),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.tap(find.byType(FloatingActionButton));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.enterText(find.byType(TextField).first, 'PACD');
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.tap(find.text('Create'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.textContaining('PACD'), findsOneWidget);
|
||||
expect(find.byType(AwesomeSnackbarContent), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('Ticket creation message includes subject', (tester) async {
|
||||
await _setSurfaceSize(tester, const Size(600, 800));
|
||||
await _pumpScreen(
|
||||
tester,
|
||||
const TicketsListScreen(),
|
||||
overrides: baseOverrides(),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.tap(find.byType(FloatingActionButton));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.enterText(
|
||||
find.widgetWithText(TextField, 'Subject'),
|
||||
'Test ticket',
|
||||
);
|
||||
await tester.enterText(
|
||||
find.widgetWithText(TextField, 'Description'),
|
||||
'Desc',
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.tap(find.text('Create'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.textContaining('Test ticket'), findsOneWidget);
|
||||
expect(find.byType(AwesomeSnackbarContent), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('Ticket dialog shows spinner while saving', (tester) async {
|
||||
final fake = _FakeTicketsController();
|
||||
final completer = Completer<void>();
|
||||
fake.onCreate =
|
||||
({
|
||||
required String subject,
|
||||
required String description,
|
||||
required String officeId,
|
||||
}) async {
|
||||
await completer.future;
|
||||
};
|
||||
|
||||
await _setSurfaceSize(tester, const Size(600, 800));
|
||||
await _pumpScreen(
|
||||
tester,
|
||||
const TicketsListScreen(),
|
||||
overrides: [
|
||||
...baseOverrides(),
|
||||
ticketsControllerProvider.overrideWithValue(fake),
|
||||
],
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.tap(find.byType(FloatingActionButton));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.enterText(
|
||||
find.widgetWithText(TextField, 'Subject'),
|
||||
'Spinner test',
|
||||
);
|
||||
await tester.enterText(
|
||||
find.widgetWithText(TextField, 'Description'),
|
||||
'Help',
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.tap(find.text('Create'));
|
||||
await tester.pump(); // start saving
|
||||
expect(find.byType(CircularProgressIndicator), findsOneWidget);
|
||||
|
||||
completer.complete();
|
||||
await tester.pumpAndSettle();
|
||||
});
|
||||
|
||||
testWidgets('Task creation message includes title', (tester) async {
|
||||
await _setSurfaceSize(tester, const Size(600, 800));
|
||||
await _pumpScreen(
|
||||
tester,
|
||||
const TasksListScreen(),
|
||||
overrides: baseOverrides(),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.tap(find.byType(FloatingActionButton));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.enterText(
|
||||
find.widgetWithText(TextField, 'Task title'),
|
||||
'Do work',
|
||||
);
|
||||
await tester.enterText(
|
||||
find.widgetWithText(TextField, 'Description'),
|
||||
'Details',
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.tap(find.text('Create'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.textContaining('Do work'), findsOneWidget);
|
||||
expect(find.byType(AwesomeSnackbarContent), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('Task dialog shows spinner while saving', (tester) async {
|
||||
final fake = _FakeTasksController();
|
||||
final completer = Completer<void>();
|
||||
fake.onCreate =
|
||||
({
|
||||
required String title,
|
||||
required String description,
|
||||
String? officeId,
|
||||
String? ticketId,
|
||||
String? requestType,
|
||||
String? requestTypeOther,
|
||||
String? requestCategory,
|
||||
}) async {
|
||||
await completer.future;
|
||||
};
|
||||
|
||||
await _setSurfaceSize(tester, const Size(600, 800));
|
||||
await _pumpScreen(
|
||||
tester,
|
||||
const TasksListScreen(),
|
||||
overrides: [
|
||||
...baseOverrides(),
|
||||
tasksControllerProvider.overrideWithValue(fake),
|
||||
],
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.tap(find.byType(FloatingActionButton));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.enterText(
|
||||
find.widgetWithText(TextField, 'Task title'),
|
||||
'Saving test',
|
||||
);
|
||||
await tester.enterText(
|
||||
find.widgetWithText(TextField, 'Description'),
|
||||
'Stuff',
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.tap(find.text('Create'));
|
||||
await tester.pump();
|
||||
expect(find.byType(CircularProgressIndicator), findsOneWidget);
|
||||
|
||||
completer.complete();
|
||||
await tester.pumpAndSettle();
|
||||
});
|
||||
|
||||
testWidgets('Add Team dialog: opening Offices dropdown does not overflow', (
|
||||
|
||||
@@ -10,6 +10,7 @@ import 'package:tasq/providers/user_offices_provider.dart';
|
||||
import 'package:tasq/providers/auth_provider.dart';
|
||||
import 'package:tasq/screens/profile/profile_screen.dart';
|
||||
import 'package:tasq/widgets/multi_select_picker.dart';
|
||||
import 'package:awesome_snackbar_content/awesome_snackbar_content.dart';
|
||||
|
||||
class _FakeProfileController implements ProfileController {
|
||||
String? lastFullName;
|
||||
@@ -121,6 +122,11 @@ void main() {
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(fake.lastFullName, equals('New Name'));
|
||||
|
||||
// should show a success snackbar using the awesome_snackbar_content package
|
||||
expect(find.byType(AwesomeSnackbarContent), findsOneWidget);
|
||||
// our helper adds a leading icon for even short messages
|
||||
expect(find.byIcon(Icons.check_circle), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('save offices assigns selected office', (tester) async {
|
||||
|
||||
@@ -1,209 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:tasq/models/task.dart';
|
||||
import 'package:tasq/models/profile.dart';
|
||||
import 'package:tasq/screens/tasks/task_detail_screen.dart';
|
||||
import 'package:tasq/providers/tasks_provider.dart';
|
||||
import 'package:tasq/providers/profile_provider.dart';
|
||||
import 'package:tasq/providers/notifications_provider.dart';
|
||||
import 'package:tasq/providers/tickets_provider.dart';
|
||||
import 'package:tasq/utils/app_time.dart';
|
||||
|
||||
// Fake controller to capture updates
|
||||
class FakeTasksController extends TasksController {
|
||||
FakeTasksController() : super(null);
|
||||
|
||||
String? lastStatus;
|
||||
Map<String, dynamic>? lastUpdates;
|
||||
|
||||
@override
|
||||
Future<void> createTask({
|
||||
required String title,
|
||||
required String description,
|
||||
String? officeId,
|
||||
String? ticketId,
|
||||
String? requestType,
|
||||
String? requestTypeOther,
|
||||
String? requestCategory,
|
||||
}) async {}
|
||||
|
||||
@override
|
||||
Future<void> updateTask({
|
||||
required String taskId,
|
||||
String? requestType,
|
||||
String? requestTypeOther,
|
||||
String? requestCategory,
|
||||
String? status,
|
||||
String? requestedBy,
|
||||
String? notedBy,
|
||||
String? receivedBy,
|
||||
String? actionTaken,
|
||||
}) async {
|
||||
final m = <String, dynamic>{};
|
||||
if (requestType != null) {
|
||||
m['requestType'] = requestType;
|
||||
}
|
||||
if (requestTypeOther != null) {
|
||||
m['requestTypeOther'] = requestTypeOther;
|
||||
}
|
||||
if (requestCategory != null) {
|
||||
m['requestCategory'] = requestCategory;
|
||||
}
|
||||
if (requestedBy != null) {
|
||||
m['requestedBy'] = requestedBy;
|
||||
}
|
||||
if (notedBy != null) {
|
||||
m['notedBy'] = notedBy;
|
||||
}
|
||||
if (receivedBy != null) {
|
||||
m['receivedBy'] = receivedBy;
|
||||
}
|
||||
if (actionTaken != null) {
|
||||
m['actionTaken'] = actionTaken;
|
||||
}
|
||||
if (status != null) {
|
||||
m['status'] = status;
|
||||
}
|
||||
lastUpdates = m;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> updateTaskStatus({
|
||||
required String taskId,
|
||||
required String status,
|
||||
}) async {
|
||||
lastStatus = status;
|
||||
}
|
||||
}
|
||||
|
||||
// lightweight notifications controller stub used in widget tests
|
||||
class _FakeNotificationsController implements NotificationsController {
|
||||
@override
|
||||
Future<void> createMentionNotifications({
|
||||
required List<String> userIds,
|
||||
required String actorId,
|
||||
required int messageId,
|
||||
String? ticketId,
|
||||
String? taskId,
|
||||
}) async {}
|
||||
|
||||
@override
|
||||
Future<void> markRead(String id) async {}
|
||||
|
||||
@override
|
||||
Future<void> markReadForTicket(String ticketId) async {}
|
||||
|
||||
@override
|
||||
Future<void> markReadForTask(String taskId) async {}
|
||||
}
|
||||
|
||||
void main() {
|
||||
testWidgets('details badges show when metadata present', (tester) async {
|
||||
AppTime.initialize();
|
||||
|
||||
// initial task without metadata
|
||||
final emptyTask = Task(
|
||||
id: 'tsk-1',
|
||||
ticketId: null,
|
||||
taskNumber: '2026-02-00002',
|
||||
title: 'No metadata',
|
||||
description: '',
|
||||
officeId: 'office-1',
|
||||
status: 'queued',
|
||||
priority: 1,
|
||||
queueOrder: null,
|
||||
createdAt: DateTime.now(),
|
||||
creatorId: 'u1',
|
||||
startedAt: null,
|
||||
completedAt: null,
|
||||
requestType: null,
|
||||
requestTypeOther: null,
|
||||
requestCategory: null,
|
||||
);
|
||||
|
||||
await tester.pumpWidget(
|
||||
ProviderScope(
|
||||
overrides: [
|
||||
tasksProvider.overrideWith((ref) => Stream.value([emptyTask])),
|
||||
taskAssignmentsProvider.overrideWith((ref) => const Stream.empty()),
|
||||
currentProfileProvider.overrideWith(
|
||||
(ref) => Stream.value(
|
||||
Profile(id: 'u1', role: 'admin', fullName: 'Admin'),
|
||||
),
|
||||
),
|
||||
notificationsControllerProvider.overrideWithValue(
|
||||
_FakeNotificationsController(),
|
||||
),
|
||||
notificationsProvider.overrideWith((ref) => const Stream.empty()),
|
||||
ticketsProvider.overrideWith((ref) => const Stream.empty()),
|
||||
officesProvider.overrideWith((ref) => const Stream.empty()),
|
||||
profilesProvider.overrideWith(
|
||||
(ref) => Stream.value(const <Profile>[]),
|
||||
),
|
||||
taskMessagesProvider.overrideWith((ref, id) => const Stream.empty()),
|
||||
],
|
||||
child: MaterialApp(home: TaskDetailScreen(taskId: 'tsk-1')),
|
||||
),
|
||||
);
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
|
||||
// metadata absent; editor controls may still show 'Type'/'Category' labels but
|
||||
// there should be no actual values present.
|
||||
expect(find.text('Repair'), findsNothing);
|
||||
expect(find.text('Hardware'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('badges show when metadata provided', (tester) async {
|
||||
AppTime.initialize();
|
||||
final task = Task(
|
||||
id: 'tsk-1',
|
||||
ticketId: null,
|
||||
taskNumber: '2026-02-00002',
|
||||
title: 'Has metadata',
|
||||
description: '',
|
||||
officeId: 'office-1',
|
||||
status: 'queued',
|
||||
priority: 1,
|
||||
queueOrder: null,
|
||||
createdAt: DateTime.now(),
|
||||
creatorId: 'u1',
|
||||
startedAt: null,
|
||||
completedAt: null,
|
||||
requestType: 'Repair',
|
||||
requestTypeOther: null,
|
||||
requestCategory: 'Hardware',
|
||||
);
|
||||
|
||||
await tester.pumpWidget(
|
||||
ProviderScope(
|
||||
overrides: [
|
||||
tasksProvider.overrideWith((ref) => Stream.value([task])),
|
||||
taskAssignmentsProvider.overrideWith((ref) => const Stream.empty()),
|
||||
currentProfileProvider.overrideWith(
|
||||
(ref) => Stream.value(
|
||||
Profile(id: 'u1', role: 'admin', fullName: 'Admin'),
|
||||
),
|
||||
),
|
||||
notificationsControllerProvider.overrideWithValue(
|
||||
_FakeNotificationsController(),
|
||||
),
|
||||
notificationsProvider.overrideWith((ref) => const Stream.empty()),
|
||||
ticketsProvider.overrideWith((ref) => const Stream.empty()),
|
||||
officesProvider.overrideWith((ref) => const Stream.empty()),
|
||||
profilesProvider.overrideWith(
|
||||
(ref) => Stream.value(const <Profile>[]),
|
||||
),
|
||||
taskMessagesProvider.overrideWith((ref, id) => const Stream.empty()),
|
||||
],
|
||||
child: MaterialApp(home: TaskDetailScreen(taskId: 'tsk-1')),
|
||||
),
|
||||
);
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
// the selected values should be visible
|
||||
expect(find.text('Repair'), findsOneWidget);
|
||||
expect(find.text('Hardware'), findsOneWidget);
|
||||
});
|
||||
}
|
||||
@@ -103,6 +103,50 @@ void main() {
|
||||
},
|
||||
);
|
||||
|
||||
// new regression tests for desktop layout adjustments
|
||||
testWidgets(
|
||||
'Desktop adaptive list responsive width and horizontal scrollbar',
|
||||
(tester) async {
|
||||
final theme = AppTheme.light();
|
||||
|
||||
Future<double> contentWidthFor(double screenWidth) async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
theme: theme,
|
||||
home: MediaQuery(
|
||||
data: MediaQueryData(size: Size(screenWidth, 800)),
|
||||
child: Scaffold(
|
||||
body: TasQAdaptiveList<int>(
|
||||
items: List.generate(100, (i) => i),
|
||||
columns: List.generate(
|
||||
20,
|
||||
(i) => TasQColumn<int>(
|
||||
header: 'C$i',
|
||||
cellBuilder: (c, t) => Text('$t'),
|
||||
),
|
||||
),
|
||||
mobileTileBuilder: (c, t, a) => const SizedBox.shrink(),
|
||||
rowActions: (_) => [],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
final box = tester.widget<SizedBox>(
|
||||
find.byKey(const Key('adaptive_list_content')),
|
||||
);
|
||||
return box.width!;
|
||||
}
|
||||
|
||||
final narrow = await contentWidthFor(1000);
|
||||
final wide = await contentWidthFor(2000);
|
||||
|
||||
expect(find.byType(Scrollbar), findsWidgets);
|
||||
expect(narrow / 1000, greaterThan(wide / 2000));
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets('AppSurfaces tokens are present and dialog/card radii differ', (
|
||||
WidgetTester tester,
|
||||
) async {
|
||||
|
||||
Reference in New Issue
Block a user