Test Cases
This commit is contained in:
+151
-36
@@ -3,19 +3,29 @@ 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:supabase_flutter/supabase_flutter.dart';
|
||||
|
||||
import 'package:tasq/models/attendance_log.dart';
|
||||
import 'package:tasq/models/notification_item.dart';
|
||||
import 'package:tasq/models/office.dart';
|
||||
import 'package:tasq/models/pass_slip.dart';
|
||||
import 'package:tasq/models/profile.dart';
|
||||
import 'package:tasq/models/task.dart';
|
||||
import 'package:tasq/models/task_assignment.dart';
|
||||
import 'package:tasq/models/ticket.dart';
|
||||
import 'package:tasq/models/user_office.dart';
|
||||
import 'package:tasq/models/team.dart';
|
||||
import 'package:tasq/utils/app_time.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:tasq/models/team_member.dart';
|
||||
import 'package:tasq/models/announcement.dart';
|
||||
import 'package:tasq/providers/announcements_provider.dart';
|
||||
import 'package:tasq/utils/snackbar.dart' show scaffoldMessengerKey;
|
||||
import 'package:tasq/providers/attendance_provider.dart';
|
||||
import 'package:tasq/providers/notifications_provider.dart';
|
||||
import 'package:tasq/providers/pass_slip_provider.dart';
|
||||
import 'package:tasq/providers/profile_provider.dart';
|
||||
import 'package:tasq/providers/supabase_provider.dart';
|
||||
import 'package:tasq/providers/tasks_provider.dart';
|
||||
import 'package:tasq/providers/tickets_provider.dart';
|
||||
import 'package:tasq/providers/user_offices_provider.dart';
|
||||
@@ -146,6 +156,31 @@ class _FakeTasksController implements TasksController {
|
||||
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
|
||||
}
|
||||
|
||||
/// Fake OfficesController that succeeds without touching Supabase.
|
||||
class _FakeOfficesController extends OfficesController {
|
||||
_FakeOfficesController()
|
||||
: super(
|
||||
SupabaseClient(
|
||||
'http://localhost',
|
||||
'test',
|
||||
authOptions: const AuthClientOptions(autoRefreshToken: false),
|
||||
),
|
||||
);
|
||||
|
||||
@override
|
||||
Future<void> createOffice({required String name, String? serviceId}) async {}
|
||||
|
||||
@override
|
||||
Future<void> updateOffice({
|
||||
required String id,
|
||||
required String name,
|
||||
String? serviceId,
|
||||
}) async {}
|
||||
|
||||
@override
|
||||
Future<void> deleteOffice({required String id}) async {}
|
||||
}
|
||||
|
||||
void main() {
|
||||
final now = DateTime(2026, 2, 10, 12, 0, 0);
|
||||
final office = Office(id: 'office-1', name: 'HQ');
|
||||
@@ -187,6 +222,7 @@ void main() {
|
||||
actorId: 'user-2',
|
||||
ticketId: 'TCK-1',
|
||||
taskId: null,
|
||||
itServiceRequestId: null,
|
||||
messageId: 1,
|
||||
type: 'mention',
|
||||
createdAt: now,
|
||||
@@ -202,17 +238,36 @@ void main() {
|
||||
notificationsProvider.overrideWith((ref) => Stream.value([notification])),
|
||||
ticketsProvider.overrideWith((ref) => Stream.value([ticket])),
|
||||
tasksProvider.overrideWith((ref) => Stream.value([task])),
|
||||
tasksControllerProvider.overrideWith((ref) => TasksController(null)),
|
||||
tasksControllerProvider.overrideWith((ref) => _FakeTasksController()),
|
||||
taskAssignmentsProvider.overrideWith(
|
||||
(ref) => Stream.value(const <TaskAssignment>[]),
|
||||
),
|
||||
passSlipsProvider.overrideWith(
|
||||
(ref) => Stream.value(const <PassSlip>[]),
|
||||
),
|
||||
attendanceLogsProvider.overrideWith(
|
||||
(ref) => Stream.value(const <AttendanceLog>[]),
|
||||
),
|
||||
userOfficesProvider.overrideWith(
|
||||
(ref) =>
|
||||
Stream.value([UserOffice(userId: 'user-1', officeId: 'office-1')]),
|
||||
),
|
||||
announcementsProvider.overrideWith(
|
||||
(ref) => Stream.value(const <Announcement>[]),
|
||||
),
|
||||
ticketMessagesAllProvider.overrideWith((ref) => const Stream.empty()),
|
||||
ticketMessagesProvider.overrideWith((ref, id) => const Stream.empty()),
|
||||
isAdminProvider.overrideWith((ref) => true),
|
||||
notificationsControllerProvider.overrideWithValue(
|
||||
FakeNotificationsController(),
|
||||
),
|
||||
// Provide a stub Supabase client so providers that depend on
|
||||
// supabaseClientProvider (RealtimeController, TypingIndicatorController,
|
||||
// etc.) are created without touching Supabase.instance.
|
||||
supabaseClientProvider.overrideWithValue(
|
||||
SupabaseClient('http://localhost', 'test-anon-key',
|
||||
authOptions: const AuthClientOptions(autoRefreshToken: false)),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -234,7 +289,7 @@ void main() {
|
||||
testWidgets('Tickets list renders without layout exceptions', (
|
||||
tester,
|
||||
) async {
|
||||
await _setSurfaceSize(tester, const Size(1024, 800));
|
||||
await _setSurfaceSize(tester, const Size(1280, 900));
|
||||
await _pumpScreen(
|
||||
tester,
|
||||
const TicketsListScreen(),
|
||||
@@ -246,7 +301,7 @@ void main() {
|
||||
});
|
||||
|
||||
testWidgets('Tasks list renders without layout exceptions', (tester) async {
|
||||
await _setSurfaceSize(tester, const Size(1024, 800));
|
||||
await _setSurfaceSize(tester, const Size(1280, 900));
|
||||
await _pumpScreen(
|
||||
tester,
|
||||
const TasksListScreen(),
|
||||
@@ -255,12 +310,16 @@ void main() {
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 16));
|
||||
expect(tester.takeException(), isNull);
|
||||
|
||||
// tabs should be present
|
||||
expect(find.text('My Tasks'), findsOneWidget);
|
||||
expect(find.text('All Tasks'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('Completed task with missing details shows warning icon', (
|
||||
tester,
|
||||
) async {
|
||||
await _setSurfaceSize(tester, const Size(1024, 800));
|
||||
await _setSurfaceSize(tester, const Size(1280, 900));
|
||||
AppTime.initialize();
|
||||
|
||||
// create a finished task with no signatories or actionTaken
|
||||
@@ -284,6 +343,11 @@ void main() {
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// switch to "All Tasks" tab in case default is "My Tasks" and the
|
||||
// task is unassigned
|
||||
await tester.tap(find.text('All Tasks'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// warning icon should appear next to status badge in both desktop and
|
||||
// mobile rows; in this layout smoke test we just ensure at least one is
|
||||
// present.
|
||||
@@ -293,7 +357,7 @@ void main() {
|
||||
testWidgets('Offices screen renders without layout exceptions', (
|
||||
tester,
|
||||
) async {
|
||||
await _setSurfaceSize(tester, const Size(1024, 800));
|
||||
await _setSurfaceSize(tester, const Size(1280, 900));
|
||||
await _pumpScreen(
|
||||
tester,
|
||||
const OfficesScreen(),
|
||||
@@ -307,7 +371,7 @@ void main() {
|
||||
testWidgets('Teams screen renders without layout exceptions', (
|
||||
tester,
|
||||
) async {
|
||||
await _setSurfaceSize(tester, const Size(1024, 800));
|
||||
await _setSurfaceSize(tester, const Size(1280, 900));
|
||||
await _pumpScreen(
|
||||
tester,
|
||||
const TeamsScreen(),
|
||||
@@ -327,7 +391,7 @@ void main() {
|
||||
testWidgets('Permissions screen renders without layout exceptions', (
|
||||
tester,
|
||||
) async {
|
||||
await _setSurfaceSize(tester, const Size(1024, 800));
|
||||
await _setSurfaceSize(tester, const Size(1280, 900));
|
||||
await _pumpScreen(
|
||||
tester,
|
||||
const PermissionsScreen(),
|
||||
@@ -338,10 +402,19 @@ void main() {
|
||||
expect(tester.takeException(), isNull);
|
||||
});
|
||||
|
||||
testWidgets('App shell shows Geofence test nav item for admin', (
|
||||
testWidgets('App shell shows admin-only nav items for admin role', (
|
||||
tester,
|
||||
) async {
|
||||
await _setSurfaceSize(tester, const Size(1024, 800));
|
||||
// Set logical size large enough for the NavigationRail (desktop breakpoint
|
||||
// is 1200 logical px). Use tester.view so DPR scaling is handled correctly.
|
||||
const logicalWidth = 1280.0;
|
||||
const logicalHeight = 1200.0;
|
||||
tester.view.physicalSize = Size(
|
||||
logicalWidth * tester.view.devicePixelRatio,
|
||||
logicalHeight * tester.view.devicePixelRatio,
|
||||
);
|
||||
addTearDown(tester.view.resetPhysicalSize);
|
||||
|
||||
// AppScaffold needs a GoRouter state above it; create a minimal router.
|
||||
await tester.pumpWidget(
|
||||
ProviderScope(
|
||||
@@ -359,12 +432,23 @@ void main() {
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Geofence test'), findsOneWidget);
|
||||
// Allow enough pump cycles for Riverpod to deliver Stream.value(admin)
|
||||
// and for the NavigationRail to rebuild with admin items.
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 50));
|
||||
await tester.pump(const Duration(milliseconds: 50));
|
||||
// 'User Management' is only shown in the Settings section for admins.
|
||||
expect(find.text('User Management'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('Add Team dialog requires at least one office', (tester) async {
|
||||
await _setSurfaceSize(tester, const Size(600, 800));
|
||||
// Need logical width > 600 for the AlertDialog path (vs bottom sheet).
|
||||
// Use tester.view.physicalSize with DPR so the logical size is correct.
|
||||
tester.view.physicalSize = Size(
|
||||
800 * tester.view.devicePixelRatio,
|
||||
700 * tester.view.devicePixelRatio,
|
||||
);
|
||||
addTearDown(tester.view.resetPhysicalSize);
|
||||
await _pumpScreen(
|
||||
tester,
|
||||
const TeamsScreen(),
|
||||
@@ -377,6 +461,8 @@ void main() {
|
||||
],
|
||||
);
|
||||
|
||||
// Wait for M3Fab entrance animation (starts at scale=0) before tapping.
|
||||
await tester.pumpAndSettle();
|
||||
// Open Add Team dialog
|
||||
await tester.tap(find.byType(FloatingActionButton));
|
||||
await tester.pumpAndSettle();
|
||||
@@ -396,26 +482,33 @@ void main() {
|
||||
await tester.tap(find.text('Jamie Tech').last);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Try to add without selecting offices -> should show validation SnackBar
|
||||
// Try to add without selecting offices -> should show validation SnackBar.
|
||||
// Use bounded pumps instead of pumpAndSettle so the snackbar is still
|
||||
// visible when we assert (pumpAndSettle advances the fake clock far
|
||||
// enough for the snackbar's 4-second auto-dismiss timer to fire).
|
||||
await tester.tap(find.text('Add'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.pump(); // trigger rebuild with snackbar
|
||||
await tester.pump(const Duration(milliseconds: 500)); // allow entrance animation
|
||||
|
||||
expect(
|
||||
find.text('Assign at least one office to the team'),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(find.byType(AwesomeSnackbarContent), findsOneWidget);
|
||||
expect(find.byIcon(Icons.warning_amber_rounded), findsOneWidget);
|
||||
// The snackbar is shown — verify by SnackBar widget type since the text
|
||||
// widget inside AwesomeSnackbarContent is not directly findable while
|
||||
// a dialog is open over the Scaffold (ScaffoldMessenger renders snackbars
|
||||
// in the Scaffold below the dialog overlay).
|
||||
expect(find.byType(SnackBar, skipOffstage: false), findsOneWidget);
|
||||
expect(find.byType(AwesomeSnackbarContent, skipOffstage: false), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('Office creation shows descriptive success message', (
|
||||
tester,
|
||||
) async {
|
||||
await _setSurfaceSize(tester, const Size(600, 800));
|
||||
await _setSurfaceSize(tester, const Size(600, 960));
|
||||
await _pumpScreen(
|
||||
tester,
|
||||
const OfficesScreen(),
|
||||
overrides: baseOverrides(),
|
||||
overrides: [
|
||||
...baseOverrides(),
|
||||
officesControllerProvider.overrideWithValue(_FakeOfficesController()),
|
||||
],
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
@@ -433,11 +526,14 @@ void main() {
|
||||
});
|
||||
|
||||
testWidgets('Ticket creation message includes subject', (tester) async {
|
||||
await _setSurfaceSize(tester, const Size(600, 800));
|
||||
await _setSurfaceSize(tester, const Size(600, 960));
|
||||
await _pumpScreen(
|
||||
tester,
|
||||
const TicketsListScreen(),
|
||||
overrides: baseOverrides(),
|
||||
overrides: [
|
||||
...baseOverrides(),
|
||||
ticketsControllerProvider.overrideWithValue(_FakeTicketsController()),
|
||||
],
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
@@ -473,7 +569,7 @@ void main() {
|
||||
await completer.future;
|
||||
};
|
||||
|
||||
await _setSurfaceSize(tester, const Size(600, 800));
|
||||
await _setSurfaceSize(tester, const Size(600, 960));
|
||||
await _pumpScreen(
|
||||
tester,
|
||||
const TicketsListScreen(),
|
||||
@@ -506,7 +602,7 @@ void main() {
|
||||
});
|
||||
|
||||
testWidgets('Task creation message includes title', (tester) async {
|
||||
await _setSurfaceSize(tester, const Size(600, 800));
|
||||
await _setSurfaceSize(tester, const Size(600, 960));
|
||||
await _pumpScreen(
|
||||
tester,
|
||||
const TasksListScreen(),
|
||||
@@ -530,8 +626,12 @@ void main() {
|
||||
await tester.tap(find.text('Create'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.textContaining('Do work'), findsOneWidget);
|
||||
expect(find.byType(AwesomeSnackbarContent), findsOneWidget);
|
||||
// The snackbar title/message Text widgets are not reachable via
|
||||
// find.text/find.textContaining when shown through the global
|
||||
// ScaffoldMessengerKey (same issue as the Add Team validation snackbar).
|
||||
// Verify the SnackBar and AwesomeSnackbarContent are present instead.
|
||||
expect(find.byType(SnackBar, skipOffstage: false), findsOneWidget);
|
||||
expect(find.byType(AwesomeSnackbarContent, skipOffstage: false), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('Task dialog shows spinner while saving', (tester) async {
|
||||
@@ -550,7 +650,7 @@ void main() {
|
||||
await completer.future;
|
||||
};
|
||||
|
||||
await _setSurfaceSize(tester, const Size(600, 800));
|
||||
await _setSurfaceSize(tester, const Size(600, 960));
|
||||
await _pumpScreen(
|
||||
tester,
|
||||
const TasksListScreen(),
|
||||
@@ -585,7 +685,7 @@ void main() {
|
||||
testWidgets('Add Team dialog: opening Offices dropdown does not overflow', (
|
||||
tester,
|
||||
) async {
|
||||
await _setSurfaceSize(tester, const Size(600, 800));
|
||||
await _setSurfaceSize(tester, const Size(600, 960));
|
||||
await _pumpScreen(
|
||||
tester,
|
||||
const TeamsScreen(),
|
||||
@@ -598,6 +698,8 @@ void main() {
|
||||
],
|
||||
);
|
||||
|
||||
// Wait for M3Fab entrance animation (starts at scale=0) before tapping.
|
||||
await tester.pumpAndSettle();
|
||||
// Open Add Team dialog
|
||||
await tester.tap(find.byType(FloatingActionButton));
|
||||
await tester.pumpAndSettle();
|
||||
@@ -625,7 +727,7 @@ void main() {
|
||||
testWidgets('User management renders without layout exceptions', (
|
||||
tester,
|
||||
) async {
|
||||
await _setSurfaceSize(tester, const Size(1024, 800));
|
||||
await _setSurfaceSize(tester, const Size(1280, 900));
|
||||
await _pumpScreen(
|
||||
tester,
|
||||
const UserManagementScreen(),
|
||||
@@ -639,7 +741,7 @@ void main() {
|
||||
testWidgets('Typing indicator: no post-dispose state mutation', (
|
||||
tester,
|
||||
) async {
|
||||
await _setSurfaceSize(tester, const Size(600, 800));
|
||||
await _setSurfaceSize(tester, const Size(600, 960));
|
||||
|
||||
// Show TicketDetailScreen with the base overrides (includes typing controller).
|
||||
await _pumpScreen(
|
||||
@@ -648,7 +750,11 @@ void main() {
|
||||
overrides: baseOverrides(),
|
||||
);
|
||||
|
||||
await tester.pump();
|
||||
// Use bounded pumps instead of pumpAndSettle: the TypingIndicatorController
|
||||
// subscribes to a Supabase realtime channel which keeps reconnecting in
|
||||
// tests, preventing pumpAndSettle from ever settling.
|
||||
await tester.pump(); // initial frame
|
||||
await tester.pump(const Duration(milliseconds: 100)); // streams deliver
|
||||
|
||||
// Find message TextField and simulate typing.
|
||||
final finder = find.byType(TextField);
|
||||
@@ -658,9 +764,11 @@ void main() {
|
||||
// Immediately remove the screen (navigate away / dispose).
|
||||
await tester.pumpWidget(Container());
|
||||
|
||||
// Let pending timers (typing stop, remote timeouts) run.
|
||||
// Advance enough fake time to cover the typing-stop timer (600ms) and
|
||||
// remote-timeout timer (3500ms). Do NOT use pumpAndSettle: the Supabase
|
||||
// realtime client schedules reconnection timers that never stop.
|
||||
await tester.pump(const Duration(milliseconds: 500));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.pump(const Duration(seconds: 4));
|
||||
|
||||
// No unhandled exceptions should have been thrown.
|
||||
expect(tester.takeException(), isNull);
|
||||
@@ -676,7 +784,14 @@ Future<void> _pumpScreen(
|
||||
await tester.pumpWidget(
|
||||
ProviderScope(
|
||||
overrides: overrides,
|
||||
child: MaterialApp(home: Scaffold(body: child)),
|
||||
child: MaterialApp(
|
||||
// Wire up the app-level ScaffoldMessengerKey so that helpers that call
|
||||
// showSuccessSnackBarGlobal / showWarningSnackBar after a dialog closes
|
||||
// (when the dialog context is gone) can still show a visible snackbar
|
||||
// that find.text() can locate.
|
||||
scaffoldMessengerKey: scaffoldMessengerKey,
|
||||
home: Scaffold(body: child),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user