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/notification_item.dart'; import 'package:tasq/models/office.dart'; import 'package:tasq/models/profile.dart'; import 'package:tasq/models/task.dart'; import 'package:tasq/models/ticket.dart'; import 'package:tasq/models/user_office.dart'; import 'package:tasq/providers/notifications_provider.dart'; import 'package:tasq/providers/profile_provider.dart'; import 'package:tasq/providers/tasks_provider.dart'; import 'package:tasq/providers/tickets_provider.dart'; import 'package:tasq/providers/typing_provider.dart'; import 'package:tasq/providers/user_offices_provider.dart'; import 'package:tasq/providers/supabase_provider.dart'; import 'package:tasq/screens/admin/offices_screen.dart'; import 'package:tasq/screens/admin/user_management_screen.dart'; import 'package:tasq/screens/tasks/tasks_list_screen.dart'; import 'package:tasq/screens/tickets/tickets_list_screen.dart'; import 'package:tasq/screens/tickets/ticket_detail_screen.dart'; // Test double for NotificationsController so widget tests don't initialize // a real Supabase client. class FakeNotificationsController implements NotificationsController { @override Future createMentionNotifications({ required List userIds, required String actorId, required int messageId, String? ticketId, String? taskId, }) async {} @override Future markRead(String id) async {} @override Future markReadForTicket(String ticketId) async {} @override Future markReadForTask(String taskId) async {} } SupabaseClient _fakeSupabaseClient() { return SupabaseClient('http://localhost', 'test-key'); } void main() { final now = DateTime(2026, 2, 10, 12, 0, 0); final office = Office(id: 'office-1', name: 'HQ'); final admin = Profile(id: 'user-1', role: 'admin', fullName: 'Alex Admin'); final tech = Profile(id: 'user-2', role: 'it_staff', fullName: 'Jamie Tech'); final ticket = Ticket( id: 'TCK-1', subject: 'Printer down', description: 'Paper jam and offline', officeId: 'office-1', status: 'open', createdAt: now, creatorId: 'user-1', respondedAt: null, promotedAt: null, closedAt: null, ); final task = Task( id: 'TSK-1', ticketId: 'TCK-1', title: 'Reboot printer', description: 'Clear queue and reboot', officeId: 'office-1', status: 'queued', priority: 1, queueOrder: 1, createdAt: now, creatorId: 'user-2', startedAt: null, completedAt: null, ); final notification = NotificationItem( id: 'N-1', userId: 'user-1', actorId: 'user-2', ticketId: 'TCK-1', taskId: null, messageId: 1, type: 'mention', createdAt: now, readAt: null, ); List baseOverrides() { return [ supabaseClientProvider.overrideWithValue(_fakeSupabaseClient()), currentProfileProvider.overrideWith((ref) => Stream.value(admin)), profilesProvider.overrideWith((ref) => Stream.value([admin, tech])), officesProvider.overrideWith((ref) => Stream.value([office])), notificationsProvider.overrideWith((ref) => Stream.value([notification])), ticketsProvider.overrideWith((ref) => Stream.value([ticket])), tasksProvider.overrideWith((ref) => Stream.value([task])), userOfficesProvider.overrideWith( (ref) => Stream.value([UserOffice(userId: 'user-1', officeId: 'office-1')]), ), ticketMessagesAllProvider.overrideWith((ref) => const Stream.empty()), isAdminProvider.overrideWith((ref) => true), notificationsControllerProvider.overrideWithValue( FakeNotificationsController(), ), typingIndicatorProvider.overrideWithProvider( AutoDisposeStateNotifierProvider.family< TypingIndicatorController, TypingIndicatorState, String >((ref, id) => TypingIndicatorController(_fakeSupabaseClient(), id)), ), ]; } List userManagementOverrides() { return [ currentProfileProvider.overrideWith((ref) => Stream.value(admin)), profilesProvider.overrideWith((ref) => Stream.value(const [])), officesProvider.overrideWith((ref) => Stream.value([office])), userOfficesProvider.overrideWith( (ref) => Stream.value(const []), ), ticketMessagesAllProvider.overrideWith((ref) => const Stream.empty()), isAdminProvider.overrideWith((ref) => true), ]; } group('Layout smoke tests', () { testWidgets('Tickets list renders without layout exceptions', ( tester, ) async { await _setSurfaceSize(tester, const Size(1024, 800)); await _pumpScreen( tester, const TicketsListScreen(), overrides: baseOverrides(), ); await tester.pump(); await tester.pump(const Duration(milliseconds: 16)); expect(tester.takeException(), isNull); }); testWidgets('Tasks list renders without layout exceptions', (tester) async { await _setSurfaceSize(tester, const Size(1024, 800)); await _pumpScreen( tester, const TasksListScreen(), overrides: baseOverrides(), ); await tester.pump(); await tester.pump(const Duration(milliseconds: 16)); expect(tester.takeException(), isNull); }); testWidgets('Offices screen renders without layout exceptions', ( tester, ) async { await _setSurfaceSize(tester, const Size(1024, 800)); await _pumpScreen( tester, const OfficesScreen(), overrides: baseOverrides(), ); await tester.pump(); await tester.pump(const Duration(milliseconds: 16)); expect(tester.takeException(), isNull); }); testWidgets('User management renders without layout exceptions', ( tester, ) async { await _setSurfaceSize(tester, const Size(1024, 800)); await _pumpScreen( tester, const UserManagementScreen(), overrides: userManagementOverrides(), ); await tester.pump(); await tester.pump(const Duration(milliseconds: 16)); expect(tester.takeException(), isNull); }); testWidgets('Typing indicator: no post-dispose state mutation', ( tester, ) async { await _setSurfaceSize(tester, const Size(600, 800)); // Show TicketDetailScreen with the base overrides (includes typing controller). await _pumpScreen( tester, const TicketDetailScreen(ticketId: 'TCK-1'), overrides: baseOverrides(), ); await tester.pump(); // Find message TextField and simulate typing. final finder = find.byType(TextField); expect(finder, findsWidgets); await tester.enterText(finder.first, 'Hello'); // Immediately remove the screen (navigate away / dispose). await tester.pumpWidget(Container()); // Let pending timers (typing stop, remote timeouts) run. await tester.pump(const Duration(milliseconds: 500)); await tester.pumpAndSettle(); // No unhandled exceptions should have been thrown. expect(tester.takeException(), isNull); }); }); } Future _pumpScreen( WidgetTester tester, Widget child, { required List overrides, }) async { await tester.pumpWidget( ProviderScope( overrides: overrides, child: MaterialApp(home: Scaffold(body: child)), ), ); } Future _setSurfaceSize(WidgetTester tester, Size size) async { await tester.binding.setSurfaceSize(size); addTearDown(() async { await tester.binding.setSurfaceSize(null); }); }