first commit
This commit is contained in:
@@ -0,0 +1,272 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:hidalyx_drluiscastro/core/widgets/medical_modal.dart';
|
||||
import 'package:hidalyx_drluiscastro/features/dashboard/data/models/patient_summary_model.dart';
|
||||
import 'package:hidalyx_drluiscastro/features/dashboard/data/services/dashboard_service.dart';
|
||||
import 'package:hidalyx_drluiscastro/features/dashboard/presentation/views/appointments_modal_view.dart';
|
||||
import 'package:hidalyx_drluiscastro/features/dashboard/presentation/widgets/today_appointments_card.dart';
|
||||
|
||||
class MockDashboardService extends DashboardService {
|
||||
final Map<String, List<PatientSummaryModel>> appointmentsByDate;
|
||||
|
||||
MockDashboardService({Map<String, List<PatientSummaryModel>>? data})
|
||||
: appointmentsByDate = data ?? {};
|
||||
|
||||
String _formatKey(DateTime d) {
|
||||
final day = d.day.toString().padLeft(2, '0');
|
||||
final month = d.month.toString().padLeft(2, '0');
|
||||
final year = d.year.toString();
|
||||
return '$day/$month/$year';
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<PatientSummaryModel>> getAppointmentsByDate(DateTime date, {int limit = 100}) async {
|
||||
final key = _formatKey(date);
|
||||
return appointmentsByDate[key] ?? [];
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<PatientSummaryModel>> getTodayAppointments({int limit = 5}) async {
|
||||
return getAppointmentsByDate(DateTime.now(), limit: limit);
|
||||
}
|
||||
}
|
||||
|
||||
void main() {
|
||||
group('AppointmentsModalView Tests', () {
|
||||
final now = DateTime.now();
|
||||
final todayDay = now.day.toString().padLeft(2, '0');
|
||||
final todayMonth = now.month.toString().padLeft(2, '0');
|
||||
final todayKey = '$todayDay/$todayMonth/${now.year}';
|
||||
|
||||
final List<PatientSummaryModel> mockTodayPatients = [
|
||||
const PatientSummaryModel(
|
||||
id: 101,
|
||||
nombre: 'CARLOS',
|
||||
apellido: 'MARTINEZ',
|
||||
cedula: '001-9999999-1',
|
||||
telefono: '809-555-1122',
|
||||
fecReg: '20/09/2026',
|
||||
fecProxCita: '20/09/2026',
|
||||
seguro: 'Sí',
|
||||
motCons: 'Seguimiento postquirúrgico',
|
||||
),
|
||||
const PatientSummaryModel(
|
||||
id: 102,
|
||||
nombre: 'ANA',
|
||||
apellido: 'PERDOMO',
|
||||
cedula: '402-8888888-2',
|
||||
telefono: '809-555-3344',
|
||||
fecReg: '20/09/2026',
|
||||
fecProxCita: '20/09/2026',
|
||||
seguro: 'No',
|
||||
motCons: 'Chequeo urológico anual',
|
||||
),
|
||||
];
|
||||
|
||||
final otherDate = DateTime(2026, 10, 25);
|
||||
const otherDateKey = '25/10/2026';
|
||||
final List<PatientSummaryModel> mockOtherPatients = [
|
||||
const PatientSummaryModel(
|
||||
id: 201,
|
||||
nombre: 'RAMON',
|
||||
apellido: 'VALDEZ',
|
||||
cedula: '001-7777777-3',
|
||||
telefono: '809-555-5566',
|
||||
fecReg: '25/10/2026',
|
||||
fecProxCita: '25/10/2026',
|
||||
seguro: 'Sí',
|
||||
motCons: 'Control de litiasis renal',
|
||||
),
|
||||
];
|
||||
|
||||
late MockDashboardService mockService;
|
||||
|
||||
setUp(() {
|
||||
mockService = MockDashboardService(data: {
|
||||
todayKey: mockTodayPatients,
|
||||
otherDateKey: mockOtherPatients,
|
||||
});
|
||||
});
|
||||
|
||||
testWidgets('renders modal header, footer, and active filter indicator as "Hoy"', (tester) async {
|
||||
tester.view.physicalSize = const Size(1280, 900);
|
||||
tester.view.devicePixelRatio = 1.0;
|
||||
addTearDown(() => tester.view.resetPhysicalSize());
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Scaffold(
|
||||
body: AppointmentsModalView(
|
||||
service: mockService,
|
||||
initialDate: now,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Check titles & subtitles
|
||||
expect(find.text('Agenda de Citas Médicas'), findsOneWidget);
|
||||
expect(find.text('Consulte y gestione los pacientes citados por fecha de consulta'), findsOneWidget);
|
||||
|
||||
// Verify that when filtered by today, it displays "Hoy (DD/MM/YYYY)"
|
||||
expect(find.textContaining('Hoy ($todayKey)'), findsOneWidget);
|
||||
|
||||
// Verify patient records for today are rendered
|
||||
expect(find.text('CARLOS MARTINEZ'), findsOneWidget);
|
||||
expect(find.text('ANA PERDOMO'), findsOneWidget);
|
||||
expect(find.text('Seguimiento postquirúrgico'), findsOneWidget);
|
||||
expect(find.text('Chequeo urológico anual'), findsOneWidget);
|
||||
|
||||
// Verify footer buttons & chip
|
||||
expect(find.text('ESC para salir'), findsOneWidget);
|
||||
expect(find.text('Cerrar'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('displays specific date filter when viewing date other than today', (tester) async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Scaffold(
|
||||
body: AppointmentsModalView(
|
||||
service: mockService,
|
||||
initialDate: otherDate,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Should show the specific date without "Hoy" in the filter badge
|
||||
expect(find.textContaining('Filtro: 25/10/2026'), findsOneWidget);
|
||||
expect(find.textContaining('Hoy'), findsWidgets); // The "Hoy" quick button is present, but the filter text is 25/10/2026
|
||||
|
||||
// Verify patient for that specific date
|
||||
expect(find.text('RAMON VALDEZ'), findsOneWidget);
|
||||
expect(find.text('Control de litiasis renal'), findsOneWidget);
|
||||
expect(find.text('CARLOS MARTINEZ'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('navigation buttons change date and "Hoy" button resets to current day', (tester) async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Scaffold(
|
||||
body: AppointmentsModalView(
|
||||
service: mockService,
|
||||
initialDate: otherDate,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.textContaining('Filtro: 25/10/2026'), findsOneWidget);
|
||||
|
||||
// Tap Día siguiente chevron
|
||||
await tester.tap(find.byTooltip('Día siguiente'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.textContaining('Filtro: 26/10/2026'), findsOneWidget);
|
||||
expect(find.text('No hay citas registradas para el 26/10/2026'), findsOneWidget);
|
||||
|
||||
// Tap Día anterior chevron twice
|
||||
await tester.tap(find.byTooltip('Día anterior'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.textContaining('Filtro: 25/10/2026'), findsOneWidget);
|
||||
|
||||
// Tap "Hoy" button to return to today
|
||||
await tester.tap(find.text('Hoy'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Must now show "Hoy (DD/MM/YYYY)" again
|
||||
expect(find.textContaining('Filtro: Hoy ($todayKey)'), findsOneWidget);
|
||||
expect(find.text('CARLOS MARTINEZ'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('footer container adheres to institutional padding (horizontal: 20, vertical: 12)', (tester) async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Scaffold(
|
||||
body: AppointmentsModalView(
|
||||
service: mockService,
|
||||
initialDate: now,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final footerFinder = find.ancestor(
|
||||
of: find.text('Cerrar'),
|
||||
matching: find.byType(Container),
|
||||
);
|
||||
expect(footerFinder, findsWidgets);
|
||||
|
||||
final footerContainer = tester.widget<Container>(footerFinder.first);
|
||||
expect(footerContainer.padding, equals(const EdgeInsets.symmetric(horizontal: 20, vertical: 12)));
|
||||
});
|
||||
|
||||
testWidgets('closes modal with Cerrar button and with ESC key', (tester) async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Scaffold(
|
||||
body: Builder(
|
||||
builder: (ctx) => ElevatedButton(
|
||||
onPressed: () => showMedicalModal(
|
||||
context: ctx,
|
||||
builder: (_) => AppointmentsModalView(
|
||||
service: mockService,
|
||||
initialDate: now,
|
||||
),
|
||||
),
|
||||
child: const Text('Abrir Citas'),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// 1. Open modal and close via Cerrar button
|
||||
await tester.tap(find.text('Abrir Citas'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Agenda de Citas Médicas'), findsOneWidget);
|
||||
|
||||
await tester.tap(find.text('Cerrar'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Agenda de Citas Médicas'), findsNothing);
|
||||
|
||||
// 2. Open modal and close via ESC key
|
||||
await tester.tap(find.text('Abrir Citas'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Agenda de Citas Médicas'), findsOneWidget);
|
||||
|
||||
await tester.sendKeyEvent(LogicalKeyboardKey.escape);
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Agenda de Citas Médicas'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('TodayAppointmentsCard item tap invokes onPatientTap callback', (tester) async {
|
||||
PatientSummaryModel? tappedPatient;
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Scaffold(
|
||||
body: TodayAppointmentsCard(
|
||||
appointments: mockTodayPatients,
|
||||
onPatientTap: (p) => tappedPatient = p,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('CARLOS MARTINEZ'), findsOneWidget);
|
||||
|
||||
await tester.tap(find.text('CARLOS MARTINEZ'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(tappedPatient, isNotNull);
|
||||
expect(tappedPatient!.id, equals(101));
|
||||
expect(tappedPatient!.nombre, equals('CARLOS'));
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:hidalyx_drluiscastro/core/network/api_client.dart';
|
||||
import 'package:hidalyx_drluiscastro/features/auth/data/models/auth_session_model.dart';
|
||||
import 'package:hidalyx_drluiscastro/features/auth/data/services/auth_service.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
class MockAdapter implements HttpClientAdapter {
|
||||
int attempts = 0;
|
||||
|
||||
@override
|
||||
Future<ResponseBody> fetch(
|
||||
RequestOptions options,
|
||||
Stream<Uint8List>? requestStream,
|
||||
Future<void>? cancelFuture,
|
||||
) async {
|
||||
attempts++;
|
||||
if (options.headers['Authorization'] == 'Bearer refreshed_token') {
|
||||
return ResponseBody.fromString(
|
||||
'{"status":"success"}',
|
||||
200,
|
||||
headers: {
|
||||
Headers.contentTypeHeader: [Headers.jsonContentType],
|
||||
},
|
||||
);
|
||||
}
|
||||
return ResponseBody.fromString(
|
||||
'{"errors":[{"message":"Token expired."}]}',
|
||||
401,
|
||||
headers: {
|
||||
Headers.contentTypeHeader: [Headers.jsonContentType],
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void close({bool force = false}) {}
|
||||
}
|
||||
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
setUp(() {
|
||||
SharedPreferences.setMockInitialValues({});
|
||||
});
|
||||
|
||||
group('AuthSessionModel copyWith', () {
|
||||
test('updates accessToken and preserves other fields', () {
|
||||
const session = AuthSessionModel(
|
||||
accessToken: 'old_access',
|
||||
refreshToken: 'old_refresh',
|
||||
userId: 'usr_1',
|
||||
firstName: 'Luis',
|
||||
lastName: 'Castro',
|
||||
email: '[email protected]',
|
||||
);
|
||||
|
||||
final updated = session.copyWith(
|
||||
accessToken: 'new_access',
|
||||
refreshToken: 'new_refresh',
|
||||
);
|
||||
|
||||
expect(updated.accessToken, 'new_access');
|
||||
expect(updated.refreshToken, 'new_refresh');
|
||||
expect(updated.userId, 'usr_1');
|
||||
expect(updated.fullName, 'Luis Castro');
|
||||
});
|
||||
});
|
||||
|
||||
group('AuthService Token Refresh flow', () {
|
||||
test('refreshToken updates SharedPreferences and ApiClient when refresh succeeds', () async {
|
||||
final dio = Dio();
|
||||
final apiClient = ApiClient.withDio(dio);
|
||||
final authService = AuthService(client: apiClient);
|
||||
|
||||
// Pre-guardar sesión con refresh token
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
const initialSession = AuthSessionModel(
|
||||
accessToken: 'expired_access_token',
|
||||
refreshToken: 'valid_mock_refresh_token',
|
||||
userId: 'u1',
|
||||
firstName: 'Luis',
|
||||
lastName: 'Castro',
|
||||
email: '[email protected]',
|
||||
);
|
||||
await prefs.setString('hidalyx_med_auth_session', jsonEncode(initialSession.toJson()));
|
||||
|
||||
expect(authService.apiClient.onTokenRefresh, isNotNull);
|
||||
});
|
||||
|
||||
test('loadSavedSession returns null when session contains empty tokens', () async {
|
||||
final authService = AuthService();
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
const emptySession = AuthSessionModel(
|
||||
accessToken: '',
|
||||
refreshToken: null,
|
||||
userId: 'u1',
|
||||
firstName: 'Luis',
|
||||
lastName: 'Castro',
|
||||
email: '[email protected]',
|
||||
);
|
||||
await prefs.setString('hidalyx_med_auth_session', jsonEncode(emptySession.toJson()));
|
||||
|
||||
final result = await authService.loadSavedSession();
|
||||
expect(result, isNull);
|
||||
});
|
||||
|
||||
test('ApiClient QueuedInterceptorsWrapper intercepts 401 and retries with refreshed token', () async {
|
||||
final dio = Dio(BaseOptions(baseUrl: 'https://mock.api'));
|
||||
final mockAdapter = MockAdapter();
|
||||
dio.httpClientAdapter = mockAdapter;
|
||||
|
||||
final client = ApiClient.withDio(dio);
|
||||
bool refreshCalled = false;
|
||||
|
||||
// Configurar callback de refresco
|
||||
client.onTokenRefresh = () async {
|
||||
refreshCalled = true;
|
||||
return 'refreshed_token';
|
||||
};
|
||||
|
||||
// Realizar una petición protegida
|
||||
final response = await client.dio.get('/items/pacientes');
|
||||
|
||||
expect(response.statusCode, 200);
|
||||
expect(response.data['status'], 'success');
|
||||
expect(refreshCalled, isTrue);
|
||||
expect(mockAdapter.attempts, 2);
|
||||
expect(client.dio.options.headers['Authorization'], 'Bearer refreshed_token');
|
||||
});
|
||||
|
||||
test('ApiClient strips Authorization header on /auth/login request', () async {
|
||||
final dio = Dio(BaseOptions(baseUrl: 'https://mock.api'));
|
||||
RequestOptions? interceptedOptions;
|
||||
dio.httpClientAdapter = _CustomAdapter((options) {
|
||||
interceptedOptions = options;
|
||||
return ResponseBody.fromString('{"data":{"access_token":"token_abc"}}', 200);
|
||||
});
|
||||
|
||||
final client = ApiClient.withDio(dio);
|
||||
client.setAuthToken('stale_expired_token');
|
||||
|
||||
// POST to /auth/login
|
||||
await client.dio.post('/auth/login', data: {'email': '[email protected]'});
|
||||
|
||||
expect(interceptedOptions, isNotNull);
|
||||
expect(interceptedOptions!.headers.containsKey('Authorization'), isFalse);
|
||||
});
|
||||
|
||||
test('ApiClient does NOT trigger onTokenRefresh when /auth/logout fails with 401', () async {
|
||||
final dio = Dio(BaseOptions(baseUrl: 'https://mock.api'));
|
||||
dio.httpClientAdapter = _CustomAdapter((options) {
|
||||
return ResponseBody.fromString('{"errors":[{"message":"Token expired."}]}', 401);
|
||||
});
|
||||
|
||||
final client = ApiClient.withDio(dio);
|
||||
bool refreshCalled = false;
|
||||
client.onTokenRefresh = () async {
|
||||
refreshCalled = true;
|
||||
return 'should_not_be_called';
|
||||
};
|
||||
|
||||
try {
|
||||
await client.dio.post('/auth/logout', data: {'refresh_token': 'expired_rf'});
|
||||
} catch (_) {}
|
||||
|
||||
expect(refreshCalled, isFalse);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
class _CustomAdapter implements HttpClientAdapter {
|
||||
final ResponseBody Function(RequestOptions options) handler;
|
||||
_CustomAdapter(this.handler);
|
||||
|
||||
@override
|
||||
Future<ResponseBody> fetch(RequestOptions options, Stream<Uint8List>? requestStream, Future<void>? cancelFuture) async {
|
||||
return handler(options);
|
||||
}
|
||||
|
||||
@override
|
||||
void close({bool force = false}) {}
|
||||
}
|
||||
@@ -0,0 +1,351 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:hidalyx_drluiscastro/core/widgets/medical_modal.dart';
|
||||
import 'package:hidalyx_drluiscastro/features/config/data/services/config_service.dart';
|
||||
import 'package:hidalyx_drluiscastro/features/config/presentation/store/config_store.dart';
|
||||
import 'package:hidalyx_drluiscastro/features/config/presentation/views/config_modal_view.dart';
|
||||
import 'package:hidalyx_drluiscastro/features/dashboard/data/models/doctor_info_model.dart';
|
||||
|
||||
class MockConfigService extends ConfigService {
|
||||
DoctorInfoModel currentConfig;
|
||||
bool shouldThrowOnSave = false;
|
||||
bool shouldThrowOnLoad = false;
|
||||
|
||||
MockConfigService({
|
||||
DoctorInfoModel? initialConfig,
|
||||
}) : currentConfig = initialConfig ??
|
||||
const DoctorInfoModel(
|
||||
id: 1,
|
||||
nombreDoctor: 'Dr. Luis A. Castro Sosa',
|
||||
especialidad: 'Cirujano Urólogo',
|
||||
descripEspecialidad: 'Enfermedades Vías Urinarias, Riñones',
|
||||
descripGeneral: 'Santiago, R.D. Tel: 809-583-2394',
|
||||
);
|
||||
|
||||
@override
|
||||
Future<DoctorInfoModel> getConfig() async {
|
||||
if (shouldThrowOnLoad) {
|
||||
throw Exception('Error al cargar configuración de prueba');
|
||||
}
|
||||
return currentConfig;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<DoctorInfoModel> updateConfig(DoctorInfoModel model) async {
|
||||
if (shouldThrowOnSave) {
|
||||
throw Exception('Error al guardar configuración de prueba');
|
||||
}
|
||||
currentConfig = model.copyWith(id: 1);
|
||||
return currentConfig;
|
||||
}
|
||||
}
|
||||
|
||||
void main() {
|
||||
group('DoctorInfoModel Serialization & Immutability', () {
|
||||
test('serializes and deserializes properly from Directus format', () {
|
||||
final json = {
|
||||
'id': 1,
|
||||
'nombre_doctor': 'Dr. Luis A. Castro Sosa',
|
||||
'especialidad': 'Cirujano Urólogo',
|
||||
'descrip_especialidad': 'Urología Oncológica y Laparoscopía',
|
||||
'descrip_general': 'Consultorio 204, Santiago. Tel: 809-555-0100',
|
||||
};
|
||||
|
||||
final model = DoctorInfoModel.fromJson(json);
|
||||
|
||||
expect(model.id, equals(1));
|
||||
expect(model.nombreDoctor, equals('Dr. Luis A. Castro Sosa'));
|
||||
expect(model.especialidad, equals('Cirujano Urólogo'));
|
||||
expect(model.descripEspecialidad, equals('Urología Oncológica y Laparoscopía'));
|
||||
expect(model.descripGeneral, equals('Consultorio 204, Santiago. Tel: 809-555-0100'));
|
||||
|
||||
// toJson without ID for Directus PATCH singleton
|
||||
final jsonWithoutId = model.toJson();
|
||||
expect(jsonWithoutId.containsKey('id'), isFalse);
|
||||
expect(jsonWithoutId['nombre_doctor'], equals('Dr. Luis A. Castro Sosa'));
|
||||
expect(jsonWithoutId['especialidad'], equals('Cirujano Urólogo'));
|
||||
|
||||
// toJson with ID
|
||||
final jsonWithId = model.toJson(includeId: true);
|
||||
expect(jsonWithId['id'], equals(1));
|
||||
});
|
||||
|
||||
test('copyWith properly modifies target fields while preserving others', () {
|
||||
const original = DoctorInfoModel(
|
||||
id: 1,
|
||||
nombreDoctor: 'Dr. Luis Castro',
|
||||
especialidad: 'Urólogo',
|
||||
descripEspecialidad: 'General',
|
||||
descripGeneral: 'Santiago',
|
||||
);
|
||||
|
||||
final updated = original.copyWith(
|
||||
especialidad: 'Urología Robótica',
|
||||
);
|
||||
|
||||
expect(updated.id, equals(1));
|
||||
expect(updated.nombreDoctor, equals('Dr. Luis Castro'));
|
||||
expect(updated.especialidad, equals('Urología Robótica'));
|
||||
expect(updated.descripEspecialidad, equals('General'));
|
||||
expect(updated.descripGeneral, equals('Santiago'));
|
||||
});
|
||||
});
|
||||
|
||||
group('ConfigStore State Management & Validation', () {
|
||||
test('validation requires nombreDoctor and especialidad', () {
|
||||
final mockService = MockConfigService();
|
||||
final store = ConfigStore(configService: mockService);
|
||||
|
||||
expect(store.isValid, isFalse);
|
||||
|
||||
store.nombreDoctor = 'Dr. Luis Castro';
|
||||
expect(store.isValid, isFalse);
|
||||
|
||||
store.especialidad = 'Cirujano Urólogo';
|
||||
expect(store.isValid, isTrue);
|
||||
|
||||
store.nombreDoctor = ' ';
|
||||
expect(store.isValid, isFalse);
|
||||
});
|
||||
|
||||
test('loadConfig successfully loads data into store observables', () async {
|
||||
final mockService = MockConfigService();
|
||||
final store = ConfigStore(configService: mockService);
|
||||
|
||||
expect(store.isLoading, isFalse);
|
||||
final loadFuture = store.loadConfig();
|
||||
expect(store.isLoading, isTrue);
|
||||
await loadFuture;
|
||||
|
||||
expect(store.isLoading, isFalse);
|
||||
expect(store.id, equals(1));
|
||||
expect(store.nombreDoctor, equals('Dr. Luis A. Castro Sosa'));
|
||||
expect(store.especialidad, equals('Cirujano Urólogo'));
|
||||
expect(store.errorMessage, isNull);
|
||||
});
|
||||
|
||||
test('loadConfig sets errorMessage on failure', () async {
|
||||
final mockService = MockConfigService()..shouldThrowOnLoad = true;
|
||||
final store = ConfigStore(configService: mockService);
|
||||
|
||||
await store.loadConfig();
|
||||
|
||||
expect(store.isLoading, isFalse);
|
||||
expect(store.errorMessage, isNotNull);
|
||||
expect(store.errorMessage, contains('Error al cargar configuración'));
|
||||
});
|
||||
|
||||
test('saveConfig validates and updates via service', () async {
|
||||
final mockService = MockConfigService();
|
||||
final store = ConfigStore(configService: mockService);
|
||||
|
||||
// Attempt save with invalid state
|
||||
final invalidResult = await store.saveConfig();
|
||||
expect(invalidResult, isNull);
|
||||
expect(store.errorMessage, isNotNull);
|
||||
|
||||
// Set valid state and save
|
||||
store.nombreDoctor = 'Dr. Juan Perez';
|
||||
store.especialidad = 'Cardiólogo';
|
||||
store.descripEspecialidad = 'Arritmias y Marcapasos';
|
||||
store.descripGeneral = 'Clínica Corominas';
|
||||
|
||||
final savedResult = await store.saveConfig();
|
||||
expect(savedResult, isNotNull);
|
||||
expect(savedResult!.nombreDoctor, equals('Dr. Juan Perez'));
|
||||
expect(savedResult.especialidad, equals('Cardiólogo'));
|
||||
expect(store.successMessage, isNotNull);
|
||||
expect(store.isSaving, isFalse);
|
||||
expect(store.errorMessage, isNull);
|
||||
});
|
||||
|
||||
test('saveConfig handles service error gracefully', () async {
|
||||
final mockService = MockConfigService()..shouldThrowOnSave = true;
|
||||
final store = ConfigStore(configService: mockService);
|
||||
|
||||
store.nombreDoctor = 'Dr. Luis Castro';
|
||||
store.especialidad = 'Urología';
|
||||
|
||||
final result = await store.saveConfig();
|
||||
expect(result, isNull);
|
||||
expect(store.isSaving, isFalse);
|
||||
expect(store.errorMessage, contains('Error al guardar configuración'));
|
||||
});
|
||||
});
|
||||
|
||||
group('ConfigModalView Widget Tests', () {
|
||||
testWidgets('renders all fields and sections properly', (tester) async {
|
||||
final mockService = MockConfigService();
|
||||
final store = ConfigStore(configService: mockService);
|
||||
await store.loadConfig();
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Scaffold(
|
||||
body: ConfigModalView(
|
||||
store: store,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Check titles & subtitles
|
||||
expect(find.text('Configuración del Consultorio'), findsOneWidget);
|
||||
expect(find.text('Identidad del Profesional'), findsOneWidget);
|
||||
expect(find.text('Áreas de Enfoque y Subespecialidades'), findsOneWidget);
|
||||
expect(find.text('Datos del Consultorio y Contacto'), findsOneWidget);
|
||||
|
||||
// Check buttons and footer elements
|
||||
expect(find.text('Cancelar'), findsOneWidget);
|
||||
expect(find.text('Guardar Configuración'), findsOneWidget);
|
||||
expect(find.text('ESC para salir'), findsOneWidget);
|
||||
|
||||
// Check populated initial values
|
||||
expect(find.text('Dr. Luis A. Castro Sosa'), findsOneWidget);
|
||||
expect(find.text('Cirujano Urólogo'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('footer container has established padding separating buttons from extremities', (tester) async {
|
||||
final mockService = MockConfigService();
|
||||
final store = ConfigStore(configService: mockService);
|
||||
await store.loadConfig();
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Scaffold(
|
||||
body: ConfigModalView(
|
||||
store: store,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Find the footer Container that holds the buttons and ESC hint
|
||||
final footerFinder = find.ancestor(
|
||||
of: find.text('Guardar Configuración'),
|
||||
matching: find.byType(Container),
|
||||
);
|
||||
expect(footerFinder, findsWidgets);
|
||||
|
||||
final footerContainer = tester.widget<Container>(footerFinder.first);
|
||||
expect(footerContainer.padding, equals(const EdgeInsets.symmetric(horizontal: 20, vertical: 12)));
|
||||
});
|
||||
|
||||
testWidgets('cancels and closes when Cancelar button is pressed', (tester) async {
|
||||
final mockService = MockConfigService();
|
||||
final store = ConfigStore(configService: mockService);
|
||||
await store.loadConfig();
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Scaffold(
|
||||
body: Builder(
|
||||
builder: (ctx) => ElevatedButton(
|
||||
onPressed: () => showMedicalModal(
|
||||
context: ctx,
|
||||
builder: (_) => ConfigModalView(store: store),
|
||||
),
|
||||
child: const Text('Abrir Modal'),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// Open modal
|
||||
await tester.tap(find.text('Abrir Modal'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Configuración del Consultorio'), findsOneWidget);
|
||||
|
||||
// Tap Cancelar
|
||||
await tester.tap(find.text('Cancelar'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Modal is dismissed
|
||||
expect(find.text('Configuración del Consultorio'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('closes modal when ESC key is pressed', (tester) async {
|
||||
final mockService = MockConfigService();
|
||||
final store = ConfigStore(configService: mockService);
|
||||
await store.loadConfig();
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Scaffold(
|
||||
body: Builder(
|
||||
builder: (ctx) => ElevatedButton(
|
||||
onPressed: () => showMedicalModal(
|
||||
context: ctx,
|
||||
builder: (_) => ConfigModalView(store: store),
|
||||
),
|
||||
child: const Text('Abrir Modal'),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// Open modal
|
||||
await tester.tap(find.text('Abrir Modal'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Configuración del Consultorio'), findsOneWidget);
|
||||
|
||||
// Simulate ESC key
|
||||
await tester.sendKeyEvent(LogicalKeyboardKey.escape);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Modal must be dismissed via ESC
|
||||
expect(find.text('Configuración del Consultorio'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('saves modified configuration and triggers onSaved callback', (tester) async {
|
||||
final mockService = MockConfigService();
|
||||
final store = ConfigStore(configService: mockService);
|
||||
await store.loadConfig();
|
||||
|
||||
DoctorInfoModel? savedModel;
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Scaffold(
|
||||
body: Builder(
|
||||
builder: (ctx) => ElevatedButton(
|
||||
onPressed: () => showMedicalModal(
|
||||
context: ctx,
|
||||
builder: (_) => ConfigModalView(
|
||||
store: store,
|
||||
onSaved: (m) => savedModel = m,
|
||||
),
|
||||
),
|
||||
child: const Text('Abrir Modal'),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// Open modal
|
||||
await tester.tap(find.text('Abrir Modal'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Enter updated doctor name
|
||||
final nameField = find.widgetWithText(TextField, 'Dr. Luis A. Castro Sosa');
|
||||
await tester.enterText(nameField, 'Dr. Luis Alberto Castro');
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Tap Guardar
|
||||
await tester.tap(find.text('Guardar Configuración'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Verify callback was called with updated name and modal closed
|
||||
expect(savedModel, isNotNull);
|
||||
expect(savedModel!.nombreDoctor, equals('Dr. Luis Alberto Castro'));
|
||||
expect(find.text('Configuración del Consultorio'), findsNothing);
|
||||
expect(find.text('Configuración del consultorio guardada correctamente'), findsOneWidget);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:hidalyx_drluiscastro/features/config/presentation/store/config_store.dart';
|
||||
import 'package:hidalyx_drluiscastro/features/referrals/presentation/views/department_referral_modal_view.dart';
|
||||
import 'package:hidalyx_drluiscastro/features/referrals/services/department_referral_pdf_service.dart';
|
||||
|
||||
void main() {
|
||||
group('DepartmentReferralModalView Tests', () {
|
||||
testWidgets('renders modal pre-populated with AlfaMedical department template', (tester) async {
|
||||
final configStore = ConfigStore();
|
||||
configStore.nombreDoctor = 'Dr. Luis A. Castro Sosa';
|
||||
configStore.especialidad = 'Cirujano Urólogo';
|
||||
configStore.descripEspecialidad = 'Enfermedades Vías Urinarias';
|
||||
configStore.descripGeneral = 'Santiago, RD';
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Scaffold(
|
||||
body: DepartmentReferralModalView(
|
||||
patientName: 'CARLOS DIAZ',
|
||||
configStore: configStore,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('Referir a Departamento'), findsOneWidget);
|
||||
expect(find.text('Dr. Luis A. Castro Sosa'), findsOneWidget);
|
||||
expect(find.text('CIRUJANO URÓLOGO'), findsOneWidget);
|
||||
expect(find.text('Crear / Imprimir Referimiento'), findsOneWidget);
|
||||
expect(find.text('ESC para salir'), findsOneWidget);
|
||||
|
||||
// Verify default AlfaMedical department template content in the text field
|
||||
final textField = tester.widget<TextField>(find.byType(TextField));
|
||||
expect(textField.controller?.text, contains('Paciente: CARLOS DIAZ'));
|
||||
expect(textField.controller?.text, contains('Referido al departamento de anestesiologia'));
|
||||
expect(textField.controller?.text, contains('Plan: Realizar evaluación pre-anestesica'));
|
||||
expect(textField.controller?.text, contains('Para realizar:'));
|
||||
expect(textField.controller?.text, contains('Suite 408, 4ta planta.'));
|
||||
expect(textField.controller?.text, contains('Gracias.'));
|
||||
});
|
||||
|
||||
testWidgets('shows validation message when text is empty', (tester) async {
|
||||
final configStore = ConfigStore();
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Scaffold(
|
||||
body: DepartmentReferralModalView(
|
||||
patientName: 'CARLOS DIAZ',
|
||||
configStore: configStore,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Clear text field
|
||||
await tester.enterText(find.byType(TextField), '');
|
||||
await tester.pump();
|
||||
|
||||
final printBtn = find.text('Crear / Imprimir Referimiento');
|
||||
await tester.tap(printBtn);
|
||||
await tester.pump();
|
||||
|
||||
expect(find.text('Debe redactar el contenido del referimiento.'), findsOneWidget);
|
||||
});
|
||||
});
|
||||
|
||||
group('DepartmentReferralPdfService Tests', () {
|
||||
test('builds department referral pdf bytes with 8.5x5.5 inch format', () async {
|
||||
final bytes = await DepartmentReferralPdfService.buildDepartmentReferralPdf(
|
||||
doctorName: 'Dr. Luis A. Castro Sosa',
|
||||
specialty: 'Cirujano Urólogo',
|
||||
subspecialty: 'Enfermedades Vias Urinarias',
|
||||
generalContact: 'Santiago, RD. Tel: 809-583-2394',
|
||||
referralText: 'Paciente: CARLOS DIAZ\n\nReferido al departamento de anestesiologia\nPlan: Realizar evaluación pre-anestesica\n\nPara realizar: Cistoscopia\n\nSuite 408, 4ta planta.\nGracias.',
|
||||
);
|
||||
|
||||
expect(bytes, isNotEmpty);
|
||||
expect(bytes.sublist(0, 4), equals([0x25, 0x50, 0x44, 0x46]));
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:hidalyx_drluiscastro/features/config/presentation/store/config_store.dart';
|
||||
import 'package:hidalyx_drluiscastro/features/referrals/presentation/views/doctor_referral_modal_view.dart';
|
||||
import 'package:hidalyx_drluiscastro/features/referrals/services/doctor_referral_pdf_service.dart';
|
||||
|
||||
void main() {
|
||||
group('DoctorReferralModalView Tests', () {
|
||||
testWidgets('renders modal pre-populated with AlfaMedical template', (tester) async {
|
||||
final configStore = ConfigStore();
|
||||
configStore.nombreDoctor = 'Dr. Luis A. Castro Sosa';
|
||||
configStore.especialidad = 'Cirujano Urólogo';
|
||||
configStore.descripEspecialidad = 'Enfermedades Vías Urinarias';
|
||||
configStore.descripGeneral = 'Santiago, RD';
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Scaffold(
|
||||
body: DoctorReferralModalView(
|
||||
patientName: 'CARLOS DIAZ',
|
||||
configStore: configStore,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('Referir a Doctor (a)'), findsOneWidget);
|
||||
expect(find.text('Dr. Luis A. Castro Sosa'), findsOneWidget);
|
||||
expect(find.text('CIRUJANO URÓLOGO'), findsOneWidget);
|
||||
expect(find.text('Crear / Imprimir Referimiento'), findsOneWidget);
|
||||
expect(find.text('ESC para salir'), findsOneWidget);
|
||||
|
||||
// Verify default AlfaMedical template content in the text field
|
||||
final textField = tester.widget<TextField>(find.byType(TextField));
|
||||
expect(textField.controller?.text, contains('Paciente: CARLOS DIAZ'));
|
||||
expect(textField.controller?.text, contains('Referido a Doctor (a) Miguelina Pichardo'));
|
||||
expect(textField.controller?.text, contains('Medicina Interna'));
|
||||
expect(textField.controller?.text, contains('Plan: Realizar evaluación cardiovascular para realizar:'));
|
||||
expect(textField.controller?.text, contains('Gracias.'));
|
||||
});
|
||||
|
||||
testWidgets('shows validation message when text is empty', (tester) async {
|
||||
final configStore = ConfigStore();
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Scaffold(
|
||||
body: DoctorReferralModalView(
|
||||
patientName: 'CARLOS DIAZ',
|
||||
configStore: configStore,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Clear text field
|
||||
await tester.enterText(find.byType(TextField), '');
|
||||
await tester.pump();
|
||||
|
||||
final printBtn = find.text('Crear / Imprimir Referimiento');
|
||||
await tester.tap(printBtn);
|
||||
await tester.pump();
|
||||
|
||||
expect(find.text('Debe redactar el contenido del referimiento.'), findsOneWidget);
|
||||
});
|
||||
});
|
||||
|
||||
group('DoctorReferralPdfService Tests', () {
|
||||
test('builds doctor referral pdf bytes with 8.5x5.5 inch format', () async {
|
||||
final bytes = await DoctorReferralPdfService.buildDoctorReferralPdf(
|
||||
doctorName: 'Dr. Luis A. Castro Sosa',
|
||||
specialty: 'Cirujano Urólogo',
|
||||
subspecialty: 'Enfermedades Vias Urinarias',
|
||||
generalContact: 'Santiago, RD. Tel: 809-583-2394',
|
||||
referralText: 'Paciente: CARLOS DIAZ\n\nReferido a Doctor (a) Miguelina Pichardo\nMedicina Interna\n\nPlan: Evaluación cardiovascular previa.',
|
||||
);
|
||||
|
||||
expect(bytes, isNotEmpty);
|
||||
expect(bytes.sublist(0, 4), equals([0x25, 0x50, 0x44, 0x46]));
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:hidalyx_drluiscastro/features/config/presentation/store/config_store.dart';
|
||||
import 'package:hidalyx_drluiscastro/features/insurance_auth/presentation/views/insurance_auth_modal_view.dart';
|
||||
import 'package:hidalyx_drluiscastro/features/insurance_auth/services/insurance_auth_pdf_service.dart';
|
||||
|
||||
void main() {
|
||||
group('InsuranceAuthModalView Tests', () {
|
||||
testWidgets('renders all fields matching AlfaMedical specification', (tester) async {
|
||||
final configStore = ConfigStore();
|
||||
configStore.nombreDoctor = 'Dr. Luis A. Castro Sosa';
|
||||
configStore.especialidad = 'Cirujano Urólogo';
|
||||
configStore.descripEspecialidad = 'Enfermedades Vías Urinarias';
|
||||
configStore.descripGeneral = 'Santiago, RD';
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Scaffold(
|
||||
body: InsuranceAuthModalView(
|
||||
patientName: 'MARIA SANTOS',
|
||||
initialDiagnosis: 'Litiasis renal derecha',
|
||||
configStore: configStore,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('Autorización De Seguro (ARS)'), findsOneWidget);
|
||||
expect(find.text('Dr. Luis A. Castro Sosa'), findsOneWidget);
|
||||
expect(find.text('CIRUJANO URÓLOGO'), findsOneWidget);
|
||||
expect(find.text('MARIA SANTOS'), findsOneWidget);
|
||||
expect(find.text('Litiasis renal derecha'), findsOneWidget);
|
||||
expect(find.text('Crear / Imprimir Autorización'), findsOneWidget);
|
||||
expect(find.text('ESC para salir'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('shows validation message when required fields are missing', (tester) async {
|
||||
final configStore = ConfigStore();
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Scaffold(
|
||||
body: InsuranceAuthModalView(
|
||||
patientName: '',
|
||||
configStore: configStore,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final printBtn = find.text('Crear / Imprimir Autorización');
|
||||
await tester.tap(printBtn);
|
||||
await tester.pump();
|
||||
|
||||
expect(find.text('Debe indicar el nombre del paciente.'), findsOneWidget);
|
||||
});
|
||||
});
|
||||
|
||||
group('InsuranceAuthPdfService Tests', () {
|
||||
test('builds insurance auth pdf bytes with 8.5x5.5 inch format', () async {
|
||||
final bytes = await InsuranceAuthPdfService.buildInsuranceAuthPdf(
|
||||
doctorName: 'Dr. Luis A. Castro Sosa',
|
||||
specialty: 'Cirujano Urólogo',
|
||||
subspecialty: 'Enfermedades Vias Urinarias',
|
||||
generalContact: 'Santiago, RD. Tel: 809-583-2394',
|
||||
arsName: 'Humano Seguros, S.A.',
|
||||
patientName: 'MARIA SANTOS',
|
||||
surgeryText: 'Ureterolitotomía endoscópica láser',
|
||||
diagnosisText: 'Cálculo ureteral obstructivo',
|
||||
anesthesia: 'Raquídea',
|
||||
daysOfStay: '1',
|
||||
);
|
||||
|
||||
expect(bytes, isNotEmpty);
|
||||
expect(bytes.sublist(0, 4), equals([0x25, 0x50, 0x44, 0x46]));
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,222 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:hidalyx_drluiscastro/core/widgets/medical_modal.dart';
|
||||
import 'package:hidalyx_drluiscastro/features/patient_search/data/models/patient_search_item_model.dart';
|
||||
import 'package:hidalyx_drluiscastro/features/patient_search/data/services/patient_search_service.dart';
|
||||
import 'package:hidalyx_drluiscastro/features/patient_search/presentation/store/patient_search_store.dart';
|
||||
import 'package:hidalyx_drluiscastro/features/patient_search/presentation/widgets/search_input_bar.dart';
|
||||
|
||||
class MockPatientSearchService extends PatientSearchService {
|
||||
@override
|
||||
Future<List<PatientSearchItemModel>> searchPatients(String rawQuery, {int limit = 25}) async {
|
||||
if (rawQuery == 'error') {
|
||||
throw Exception('Simulated network error');
|
||||
}
|
||||
if (rawQuery == 'Carlos') {
|
||||
return [
|
||||
const PatientSearchItemModel(
|
||||
id: 13717,
|
||||
nombre: 'CARLOS JUAN',
|
||||
apellido: 'ARIAS GOMEZ',
|
||||
cedula: '001-1234567-8',
|
||||
telefono: '809-555-0101',
|
||||
edad: '45',
|
||||
sexo: 'M',
|
||||
fecUltcon: '15/09/2026',
|
||||
seguro: 'SENASA',
|
||||
motCons: 'Control Rutinario',
|
||||
),
|
||||
];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
void main() {
|
||||
group('PatientSearchItemModel', () {
|
||||
test('correctly parses JSON and provides full name', () {
|
||||
final json = {
|
||||
'id': '123',
|
||||
'nombre': 'JUAN',
|
||||
'apellido': 'PEREZ',
|
||||
'cedula': '001-0000000-0',
|
||||
'telefono': '8090000000',
|
||||
'edad': '30',
|
||||
'sexo': 'M',
|
||||
'fec_ultcon': '01/01/2026',
|
||||
'seguro': 'HUMANO',
|
||||
'mot_cons': 'Chequeo',
|
||||
};
|
||||
|
||||
final model = PatientSearchItemModel.fromJson(json);
|
||||
expect(model.id, equals(123));
|
||||
expect(model.fullName, equals('JUAN PEREZ'));
|
||||
expect(model.cedula, equals('001-0000000-0'));
|
||||
});
|
||||
});
|
||||
|
||||
group('PatientSearchStore', () {
|
||||
test('successful search populates results and updates hasSearched', () async {
|
||||
final store = PatientSearchStore(searchService: MockPatientSearchService());
|
||||
expect(store.hasSearched, isFalse);
|
||||
expect(store.resultsCount, equals(0));
|
||||
|
||||
await store.search('Carlos');
|
||||
|
||||
expect(store.hasSearched, isTrue);
|
||||
expect(store.isLoading, isFalse);
|
||||
expect(store.errorMessage, isNull);
|
||||
expect(store.resultsCount, equals(1));
|
||||
expect(store.results.first.fullName, equals('CARLOS JUAN ARIAS GOMEZ'));
|
||||
});
|
||||
|
||||
test('error handling updates errorMessage', () async {
|
||||
final store = PatientSearchStore(searchService: MockPatientSearchService());
|
||||
await store.search('error');
|
||||
|
||||
expect(store.hasSearched, isTrue);
|
||||
expect(store.isLoading, isFalse);
|
||||
expect(store.errorMessage, contains('Simulated network error'));
|
||||
expect(store.resultsCount, equals(0));
|
||||
});
|
||||
|
||||
test('clear resets the store state', () async {
|
||||
final store = PatientSearchStore(searchService: MockPatientSearchService());
|
||||
await store.search('Carlos');
|
||||
expect(store.resultsCount, equals(1));
|
||||
|
||||
store.clear();
|
||||
expect(store.resultsCount, equals(0));
|
||||
expect(store.hasSearched, isFalse);
|
||||
expect(store.query, isEmpty);
|
||||
});
|
||||
});
|
||||
|
||||
group('SearchInputBar widget', () {
|
||||
testWidgets('triggers onSearch ONLY when Enter is pressed or Buscar is clicked', (tester) async {
|
||||
String? searchedQuery;
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Scaffold(
|
||||
body: SearchInputBar(
|
||||
onSearch: (q) => searchedQuery = q,
|
||||
onClear: () {},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// Typing alone should not trigger search
|
||||
await tester.enterText(find.byType(TextField), 'Carlos Arias');
|
||||
await tester.pump();
|
||||
expect(searchedQuery, isNull);
|
||||
|
||||
// Submitting via Enter key
|
||||
await tester.testTextInput.receiveAction(TextInputAction.search);
|
||||
await tester.pump();
|
||||
expect(searchedQuery, equals('Carlos Arias'));
|
||||
});
|
||||
});
|
||||
|
||||
group('MedicalModalDialog and showMedicalModal behavior', () {
|
||||
testWidgets('modal does NOT close when tapping outside (barrierDismissible is false)', (tester) async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Scaffold(
|
||||
body: Builder(
|
||||
builder: (context) => ElevatedButton(
|
||||
onPressed: () {
|
||||
showMedicalModal(
|
||||
context: context,
|
||||
builder: (_) => const Text('Modal Body'),
|
||||
);
|
||||
},
|
||||
child: const Text('Open Modal'),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
await tester.tap(find.text('Open Modal'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Modal Body'), findsOneWidget);
|
||||
|
||||
// Tap on the outside barrier (top-left corner outside dialog)
|
||||
await tester.tapAt(const Offset(5, 5));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Modal MUST still be present
|
||||
expect(find.text('Modal Body'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('modal closes when close icon (X) button is clicked', (tester) async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Scaffold(
|
||||
body: Builder(
|
||||
builder: (context) => ElevatedButton(
|
||||
onPressed: () {
|
||||
showMedicalModal(
|
||||
context: context,
|
||||
builder: (_) => const MedicalModalDialog(
|
||||
title: 'Test Modal',
|
||||
child: Text('Content'),
|
||||
),
|
||||
);
|
||||
},
|
||||
child: const Text('Open Modal'),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
await tester.tap(find.text('Open Modal'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Test Modal'), findsOneWidget);
|
||||
|
||||
// Click the top-right close button (X)
|
||||
await tester.tap(find.byTooltip('Cerrar (Esc)'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Modal must be closed
|
||||
expect(find.text('Test Modal'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('modal closes when ESC key is pressed', (tester) async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Scaffold(
|
||||
body: Builder(
|
||||
builder: (context) => ElevatedButton(
|
||||
onPressed: () {
|
||||
showMedicalModal(
|
||||
context: context,
|
||||
builder: (_) => const MedicalModalDialog(
|
||||
title: 'Test Modal',
|
||||
child: Text('Content'),
|
||||
),
|
||||
);
|
||||
},
|
||||
child: const Text('Open Modal'),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
await tester.tap(find.text('Open Modal'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Test Modal'), findsOneWidget);
|
||||
|
||||
// Send ESC key
|
||||
await tester.sendKeyEvent(LogicalKeyboardKey.escape);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Modal must be closed
|
||||
expect(find.text('Test Modal'), findsNothing);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,397 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:hidalyx_drluiscastro/core/widgets/medical_modal.dart';
|
||||
import 'package:hidalyx_drluiscastro/features/patient/data/models/patient_model.dart';
|
||||
import 'package:hidalyx_drluiscastro/features/patient/data/services/patient_service.dart';
|
||||
import 'package:hidalyx_drluiscastro/features/patient/presentation/store/patient_store.dart';
|
||||
import 'package:hidalyx_drluiscastro/features/patient/presentation/views/patient_modal_view.dart';
|
||||
import 'package:hidalyx_drluiscastro/features/patient/presentation/widgets/patient_demographics_form.dart';
|
||||
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
||||
import 'package:toggle_switch/toggle_switch.dart';
|
||||
|
||||
class MockPatientService extends PatientService {
|
||||
PatientModel? storedPatient;
|
||||
|
||||
@override
|
||||
Future<PatientModel> getPatientById(int id) async {
|
||||
return const PatientModel(
|
||||
id: 99,
|
||||
nombre: 'MARIA',
|
||||
apellido: 'SANTOS',
|
||||
cedula: '402-0000000-1',
|
||||
telefono: '809-555-9999',
|
||||
direccion: 'Santiago',
|
||||
ocupacion: 'Docente',
|
||||
edad: '35',
|
||||
sexo: 'Femenino',
|
||||
seguro: 'Sí',
|
||||
estCivil: 'Casado',
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<PatientModel> createPatient(PatientModel model) async {
|
||||
storedPatient = model;
|
||||
return PatientModel.fromJson({
|
||||
...model.toJson(),
|
||||
'id': 101,
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Future<PatientModel> updatePatient(int id, Map<String, dynamic> updates) async {
|
||||
return PatientModel.fromJson({
|
||||
...updates,
|
||||
'id': id,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void main() {
|
||||
group('PatientModel serialization', () {
|
||||
test('serializes and deserializes properly from Directus schema', () {
|
||||
final json = {
|
||||
'id': 50,
|
||||
'nombre': 'CARLOS',
|
||||
'apellido': 'ARIAS',
|
||||
'telefono': '809-555-1234',
|
||||
'direccion': 'Calle 1 #2',
|
||||
'cedula': '001-0000000-0',
|
||||
'ocupacion': 'Ingeniero',
|
||||
'edad': '40',
|
||||
'sexo': 'Masculino',
|
||||
'seguro': 'Sí',
|
||||
'est_civil': 'Casado',
|
||||
'mot_cons': 'Dolor de cabeza',
|
||||
};
|
||||
|
||||
final model = PatientModel.fromJson(json);
|
||||
expect(model.id, equals(50));
|
||||
expect(model.fullName, equals('CARLOS ARIAS'));
|
||||
expect(model.sexo, equals('Masculino'));
|
||||
expect(model.seguro, equals('Sí'));
|
||||
|
||||
final outJson = model.toJson(includeId: true);
|
||||
expect(outJson['id'], equals(50));
|
||||
expect(outJson['nombre'], equals('CARLOS'));
|
||||
expect(outJson['mot_cons'], equals('Dolor de cabeza'));
|
||||
});
|
||||
});
|
||||
|
||||
group('PatientStore business logic', () {
|
||||
test('validation requires nombre, apellido, sexo and seguro', () {
|
||||
final store = PatientStore(patientService: MockPatientService());
|
||||
store.initForCreate();
|
||||
expect(store.isValid, isFalse);
|
||||
|
||||
store.nombre = 'Ana';
|
||||
expect(store.isValid, isFalse);
|
||||
|
||||
store.apellido = 'Gomez';
|
||||
expect(store.isValid, isTrue); // sexo and seguro default to 'Masculino'/'Sí'
|
||||
});
|
||||
|
||||
test('calculates remaining days for proxima cita correctly', () {
|
||||
final store = PatientStore(patientService: MockPatientService());
|
||||
store.initForCreate();
|
||||
|
||||
expect(store.diasParaProximaCita, isNull);
|
||||
|
||||
final now = DateTime.now();
|
||||
final futureDate = now.add(const Duration(days: 10));
|
||||
final day = futureDate.day.toString().padLeft(2, '0');
|
||||
final month = futureDate.month.toString().padLeft(2, '0');
|
||||
final year = futureDate.year.toString();
|
||||
|
||||
store.fecProxcita = '$day/$month/$year';
|
||||
expect(store.diasParaProximaCita, equals(10));
|
||||
});
|
||||
|
||||
test('creates new patient successfully', () async {
|
||||
final mock = MockPatientService();
|
||||
final store = PatientStore(patientService: mock);
|
||||
store.initForCreate();
|
||||
store.nombre = 'Pedro';
|
||||
store.apellido = 'Martinez';
|
||||
|
||||
final success = await store.savePatient();
|
||||
expect(success, isTrue);
|
||||
expect(store.id, equals(101));
|
||||
expect(store.successMessage, contains('guardado correctamente'));
|
||||
});
|
||||
|
||||
test('updates existing patient successfully', () async {
|
||||
final mock = MockPatientService();
|
||||
final store = PatientStore(patientService: mock);
|
||||
await store.loadPatient(99);
|
||||
|
||||
expect(store.isEditing, isTrue);
|
||||
expect(store.nombre, equals('MARIA'));
|
||||
expect(store.apellido, equals('SANTOS'));
|
||||
|
||||
store.ocupacion = 'Directora';
|
||||
final success = await store.savePatient();
|
||||
expect(success, isTrue);
|
||||
expect(store.successMessage, contains('actualizado correctamente'));
|
||||
});
|
||||
});
|
||||
|
||||
group('PatientModalView strict closure tests', () {
|
||||
testWidgets('modal adheres to strict ESC and (X) close rules', (tester) async {
|
||||
tester.view.physicalSize = const Size(1280, 800);
|
||||
tester.view.devicePixelRatio = 1.0;
|
||||
addTearDown(tester.view.resetPhysicalSize);
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Scaffold(
|
||||
body: Builder(
|
||||
builder: (context) => ElevatedButton(
|
||||
onPressed: () {
|
||||
showMedicalModal(
|
||||
context: context,
|
||||
builder: (_) => const PatientModalView(),
|
||||
);
|
||||
},
|
||||
child: const Text('Registrar'),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
await tester.tap(find.text('Registrar'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Registro De Paciente'), findsOneWidget);
|
||||
|
||||
// Outside tap does NOT close
|
||||
await tester.tapAt(const Offset(5, 5));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Registro De Paciente'), findsOneWidget);
|
||||
|
||||
// ESC key closes the modal
|
||||
await tester.sendKeyEvent(LogicalKeyboardKey.escape);
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Registro De Paciente'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('date button inserts current date at cursor in Consultas', (tester) async {
|
||||
tester.view.physicalSize = const Size(1280, 800);
|
||||
tester.view.devicePixelRatio = 1.0;
|
||||
addTearDown(tester.view.resetPhysicalSize);
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Scaffold(
|
||||
body: Builder(
|
||||
builder: (context) => ElevatedButton(
|
||||
onPressed: () {
|
||||
showMedicalModal(
|
||||
context: context,
|
||||
builder: (_) => const PatientModalView(),
|
||||
);
|
||||
},
|
||||
child: const Text('Registrar'),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
await tester.tap(find.text('Registrar'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Find the Fecha actual button
|
||||
final insertBtn = find.widgetWithText(ElevatedButton, 'Fecha actual');
|
||||
expect(insertBtn, findsOneWidget);
|
||||
|
||||
await tester.tap(insertBtn);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Verify that current year is present in the field
|
||||
final currentYear = DateTime.now().year.toString();
|
||||
expect(find.textContaining(currentYear), findsWidgets);
|
||||
});
|
||||
|
||||
testWidgets('renders and switches ToggleSwitch for Sexo, Seguro, and Est. Civil', (tester) async {
|
||||
await tester.binding.setSurfaceSize(const Size(1200, 900));
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Scaffold(
|
||||
body: Builder(
|
||||
builder: (ctx) => ElevatedButton(
|
||||
onPressed: () {
|
||||
showMedicalModal(
|
||||
context: ctx,
|
||||
builder: (_) => const PatientModalView(),
|
||||
);
|
||||
},
|
||||
child: const Text('Registrar'),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
await tester.tap(find.text('Registrar'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Verify ToggleSwitch instances exist for Sexo, Seguro, and Est. Civil
|
||||
expect(find.byType(ToggleSwitch), findsNWidgets(3));
|
||||
|
||||
// Tap Femenino
|
||||
await tester.tap(find.text('Femenino'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Tap Sin Seguro
|
||||
await tester.tap(find.text('Sin Seguro'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Tap Casado
|
||||
await tester.tap(find.text('Casado'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Verify the 8 demographic text fields have unifiedRowHeight matching toggle containers and save button
|
||||
final textFields = find.byType(FormBuilderTextField);
|
||||
|
||||
// Verify all 8 fields, 3 toggle selector boxes, and the Save button have unifiedRowHeight
|
||||
for (int i = 0; i < 8; i++) {
|
||||
final decFinder = find.descendant(of: textFields.at(i), matching: find.byType(InputDecorator));
|
||||
final customPaintFinder = find.descendant(of: decFinder, matching: find.byType(CustomPaint));
|
||||
final rb = customPaintFinder.evaluate().first.renderObject as RenderBox;
|
||||
final translation = rb.localToGlobal(Offset.zero);
|
||||
final rect = translation & rb.size;
|
||||
expect(rect.height, equals(PatientDemographicsForm.unifiedRowHeight));
|
||||
}
|
||||
|
||||
// Verify the 3 toggle selector visible boxes have unifiedRowHeight
|
||||
final selectorBoxes = find.byWidgetPredicate((w) => w is Container && w.decoration is BoxDecoration && (w.decoration as BoxDecoration).color == const Color(0xFFF8FAFC));
|
||||
for (int i = 0; i < 3; i++) {
|
||||
final boxRect = tester.getRect(selectorBoxes.at(i));
|
||||
expect(boxRect.height, equals(PatientDemographicsForm.unifiedRowHeight));
|
||||
}
|
||||
|
||||
// Verify the Save button has unifiedRowHeight
|
||||
final saveBtn = find.widgetWithText(ElevatedButton, 'Guardar Paciente');
|
||||
expect(tester.getRect(saveBtn).height, equals(PatientDemographicsForm.unifiedRowHeight));
|
||||
|
||||
// Row-by-row pixel-exact alignment check for all 4 rows
|
||||
for (int row = 0; row < 4; row++) {
|
||||
// Col 1 field
|
||||
final f1Dec = find.descendant(of: textFields.at(row), matching: find.byType(InputDecorator));
|
||||
final f1Cp = find.descendant(of: f1Dec, matching: find.byType(CustomPaint));
|
||||
final f1Rb = f1Cp.evaluate().first.renderObject as RenderBox;
|
||||
final f1Rect = f1Rb.localToGlobal(Offset.zero) & f1Rb.size;
|
||||
|
||||
// Col 2 field
|
||||
final f2Dec = find.descendant(of: textFields.at(row + 4), matching: find.byType(InputDecorator));
|
||||
final f2Cp = find.descendant(of: f2Dec, matching: find.byType(CustomPaint));
|
||||
final f2Rb = f2Cp.evaluate().first.renderObject as RenderBox;
|
||||
final f2Rect = f2Rb.localToGlobal(Offset.zero) & f2Rb.size;
|
||||
|
||||
// Col 3 item (selector box for rows 0..2, saveBtn for row 3)
|
||||
final Rect f3Rect = row < 3 ? tester.getRect(selectorBoxes.at(row)) : tester.getRect(saveBtn);
|
||||
|
||||
// Verify tops match
|
||||
expect(f1Rect.top, equals(f2Rect.top), reason: 'Row $row tops Col1 vs Col2');
|
||||
expect(f1Rect.top, equals(f3Rect.top), reason: 'Row $row tops Col1 vs Col3');
|
||||
|
||||
// Verify bottoms match
|
||||
expect(f1Rect.bottom, equals(f2Rect.bottom), reason: 'Row $row bottoms Col1 vs Col2');
|
||||
expect(f1Rect.bottom, equals(f3Rect.bottom), reason: 'Row $row bottoms Col1 vs Col3');
|
||||
|
||||
// Verify heights match exactly
|
||||
expect(f1Rect.height, equals(f3Rect.height), reason: 'Row $row heights Col1 vs Col3');
|
||||
}
|
||||
});
|
||||
|
||||
testWidgets('OutlineInputBorder and Toggle Container alignment check', (tester) async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Scaffold(
|
||||
body: Center(
|
||||
child: SizedBox(
|
||||
width: 800,
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: SizedBox(
|
||||
height: 51,
|
||||
child: FormBuilder(
|
||||
child: FormBuilderTextField(
|
||||
name: 'test',
|
||||
initialValue: 'Texto',
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Nombre *',
|
||||
isDense: true,
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 11, vertical: 12),
|
||||
border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Container(
|
||||
margin: const EdgeInsets.only(top: 7),
|
||||
height: 44,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF8FAFC),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: const Color(0xFFE2E8F0)),
|
||||
),
|
||||
child: const Text('Toggle Container'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.byType(FormBuilderTextField), findsOneWidget);
|
||||
expect(find.text('Toggle Container'), findsOneWidget);
|
||||
});
|
||||
});
|
||||
|
||||
group('PatientStore consultation workflow', () {
|
||||
test('savePatient transitions estadoEspera from en_consulta to atendido', () async {
|
||||
final mockService = MockPatientService();
|
||||
final store = PatientStore(patientService: mockService);
|
||||
store.id = 10;
|
||||
store.nombre = 'JUAN';
|
||||
store.apellido = 'PEREZ';
|
||||
store.sexo = 'Masculino';
|
||||
store.seguro = 'Sí';
|
||||
store.estadoEspera = 'en_consulta';
|
||||
|
||||
final success = await store.savePatient();
|
||||
|
||||
expect(success, isTrue);
|
||||
expect(store.estadoEspera, 'atendido');
|
||||
});
|
||||
|
||||
test('savePatient transitions estadoEspera from en_espera to atendido', () async {
|
||||
final mockService = MockPatientService();
|
||||
final store = PatientStore(patientService: mockService);
|
||||
store.id = 11;
|
||||
store.nombre = 'PEDRO';
|
||||
store.apellido = 'GOMEZ';
|
||||
store.sexo = 'Masculino';
|
||||
store.seguro = 'Sí';
|
||||
store.estadoEspera = 'en_espera';
|
||||
|
||||
final success = await store.savePatient();
|
||||
|
||||
expect(success, isTrue);
|
||||
expect(store.estadoEspera, 'atendido');
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:hidalyx_drluiscastro/features/config/presentation/store/config_store.dart';
|
||||
import 'package:hidalyx_drluiscastro/features/patient/presentation/widgets/clinical_documents_dropdown.dart';
|
||||
import 'package:hidalyx_drluiscastro/features/prescription/presentation/views/prescription_modal_view.dart';
|
||||
import 'package:hidalyx_drluiscastro/features/prescription/services/prescription_pdf_service.dart';
|
||||
|
||||
void main() {
|
||||
group('ClinicalDocumentsDropdown Tests', () {
|
||||
testWidgets('renders dropdown button with title and icon', (tester) async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Scaffold(
|
||||
body: ClinicalDocumentsDropdown(
|
||||
onSelect: (_) {},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(find.text('Documentos Clínicos'), findsOneWidget);
|
||||
expect(find.byIcon(Icons.description_outlined), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('opens menu and displays Receta Médica option', (tester) async {
|
||||
ClinicalDocumentType? selected;
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Scaffold(
|
||||
body: ClinicalDocumentsDropdown(
|
||||
onSelect: (type) => selected = type,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
await tester.tap(find.text('Documentos Clínicos'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('Receta Médica'), findsOneWidget);
|
||||
expect(find.text('Formato media carta institucional'), findsOneWidget);
|
||||
expect(find.text('Autorización De Seguro'), findsOneWidget);
|
||||
expect(find.text('Referir a Doctor(a)'), findsOneWidget);
|
||||
expect(find.text('Referir a Departamento'), findsOneWidget);
|
||||
|
||||
await tester.tap(find.text('Receta Médica'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(selected, equals(ClinicalDocumentType.recetaMedica));
|
||||
});
|
||||
});
|
||||
|
||||
group('PrescriptionModalView Tests', () {
|
||||
testWidgets('renders patient name, doctor header and prescription fields', (tester) async {
|
||||
final configStore = ConfigStore();
|
||||
configStore.nombreDoctor = 'Dr. Luis A. Castro Sosa';
|
||||
configStore.especialidad = 'Cirujano Urólogo';
|
||||
configStore.descripEspecialidad = 'Enfermedades Vías Urinarias';
|
||||
configStore.descripGeneral = 'Santiago, RD';
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Scaffold(
|
||||
body: PrescriptionModalView(
|
||||
patientName: 'JUAN PEREZ',
|
||||
configStore: configStore,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('Emisión de Receta Médica'), findsOneWidget);
|
||||
expect(find.text('Dr. Luis A. Castro Sosa'), findsOneWidget);
|
||||
expect(find.text('CIRUJANO URÓLOGO'), findsOneWidget);
|
||||
expect(find.text('ENFERMEDADES VÍAS URINARIAS'), findsOneWidget);
|
||||
expect(find.text('JUAN PEREZ'), findsOneWidget);
|
||||
expect(find.text('Crear / Imprimir Receta'), findsOneWidget);
|
||||
expect(find.text('ESC para salir'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('inserts bullet when clicking viñeta button', (tester) async {
|
||||
final configStore = ConfigStore();
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Scaffold(
|
||||
body: PrescriptionModalView(
|
||||
patientName: 'CARLOS ARIAS',
|
||||
configStore: configStore,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final bulletBtn = find.text('Insertar viñeta (•)');
|
||||
expect(bulletBtn, findsOneWidget);
|
||||
|
||||
await tester.tap(bulletBtn);
|
||||
await tester.pump();
|
||||
|
||||
expect(find.text('• '), findsOneWidget);
|
||||
});
|
||||
});
|
||||
|
||||
group('PrescriptionPdfService Tests', () {
|
||||
test('builds prescription pdf bytes with correct page format', () async {
|
||||
final bytes = await PrescriptionPdfService.buildPrescriptionPdf(
|
||||
doctorName: 'Dr. Luis A. Castro Sosa',
|
||||
specialty: 'Cirujano Urólogo',
|
||||
subspecialty: 'Enfermedades Vias Urinarias',
|
||||
generalContact: 'Santiago, RD. Tel: 809-583-2394',
|
||||
patientName: 'JUAN PEREZ',
|
||||
prescriptionText: '1. Ciprofloxacina 500mg\nTomar 1 tableta cada 12 horas por 7 días.',
|
||||
);
|
||||
|
||||
expect(bytes, isNotEmpty);
|
||||
// Valid PDF magic header %PDF
|
||||
expect(bytes.sublist(0, 4), equals([0x25, 0x50, 0x44, 0x46]));
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:hidalyx_drluiscastro/features/auth/data/models/auth_session_model.dart';
|
||||
import 'package:hidalyx_drluiscastro/features/dashboard/presentation/widgets/dashboard_app_bar.dart';
|
||||
import 'package:hidalyx_drluiscastro/features/patient/data/models/patient_model.dart';
|
||||
import 'package:hidalyx_drluiscastro/features/patient/presentation/views/patient_modal_view.dart';
|
||||
import 'package:hidalyx_drluiscastro/features/patient/presentation/widgets/clinical_documents_dropdown.dart';
|
||||
import 'package:hidalyx_drluiscastro/features/patient/presentation/widgets/patient_clinical_tabs.dart';
|
||||
import 'package:hidalyx_drluiscastro/features/patient/presentation/widgets/secretary_appointment_section.dart';
|
||||
|
||||
void main() {
|
||||
group('AuthSessionModel Role Detection Tests', () {
|
||||
test('correctly identifies Doctor vs Secretary roles', () {
|
||||
const doctorSession = AuthSessionModel(
|
||||
accessToken: 'token_123',
|
||||
userId: 'u1',
|
||||
firstName: 'Luis A.',
|
||||
lastName: 'Castro Sosa',
|
||||
email: '[email protected]',
|
||||
roleName: 'Doctor',
|
||||
);
|
||||
expect(doctorSession.isSecretary, isFalse);
|
||||
expect(doctorSession.isDoctorOrAdmin, isTrue);
|
||||
|
||||
const secretarySession = AuthSessionModel(
|
||||
accessToken: 'token_456',
|
||||
userId: 'u2',
|
||||
firstName: 'Ana',
|
||||
lastName: 'Pérez',
|
||||
email: '[email protected]',
|
||||
roleName: 'Secretaria',
|
||||
);
|
||||
expect(secretarySession.isSecretary, isTrue);
|
||||
expect(secretarySession.isDoctorOrAdmin, isFalse);
|
||||
|
||||
const receptionistSession = AuthSessionModel(
|
||||
accessToken: 'token_789',
|
||||
userId: 'u3',
|
||||
firstName: 'Maria',
|
||||
lastName: 'Gomez',
|
||||
email: '[email protected]',
|
||||
roleName: 'Recepcionista',
|
||||
);
|
||||
expect(receptionistSession.isSecretary, isTrue);
|
||||
|
||||
const assistantSession = AuthSessionModel(
|
||||
accessToken: 'token_101',
|
||||
userId: 'u4',
|
||||
firstName: 'Carmen',
|
||||
lastName: 'Díaz',
|
||||
email: '[email protected]',
|
||||
roleName: 'Asistente Médico',
|
||||
);
|
||||
expect(assistantSession.isSecretary, isTrue);
|
||||
});
|
||||
});
|
||||
|
||||
group('DashboardAppBar Role-based Tests', () {
|
||||
testWidgets('renders RECEPCIÓN badge and hides Configuración for secretary', (tester) async {
|
||||
await tester.pumpWidget(
|
||||
const MaterialApp(
|
||||
home: Scaffold(
|
||||
appBar: DashboardAppBar(
|
||||
doctorName: 'Ana Pérez',
|
||||
specialty: 'Recepción y Consultas',
|
||||
isSecretary: true,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('RECEPCIÓN'), findsOneWidget);
|
||||
expect(find.text('Ana Pérez'), findsOneWidget);
|
||||
|
||||
// Direct quick action buttons are immediately visible without opening a menu
|
||||
expect(find.text('Nuevo Paciente'), findsOneWidget);
|
||||
expect(find.text('Buscar Paciente'), findsOneWidget);
|
||||
expect(find.byIcon(Icons.settings_outlined), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('shows Configuración in QuickActions for Doctor', (tester) async {
|
||||
await tester.pumpWidget(
|
||||
const MaterialApp(
|
||||
home: Scaffold(
|
||||
appBar: DashboardAppBar(
|
||||
doctorName: 'Dr. Luis A. Castro Sosa',
|
||||
specialty: 'Cirujano Urólogo',
|
||||
isSecretary: false,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('RECEPCIÓN'), findsNothing);
|
||||
|
||||
// Direct quick action buttons are immediately visible
|
||||
expect(find.text('Nuevo Paciente'), findsOneWidget);
|
||||
expect(find.text('Buscar Paciente'), findsOneWidget);
|
||||
expect(find.byIcon(Icons.settings_outlined), findsOneWidget);
|
||||
});
|
||||
});
|
||||
|
||||
group('PatientModalView Role-based Streamlined View Tests', () {
|
||||
testWidgets('renders streamlined reception view for secretary', (tester) async {
|
||||
final patient = PatientModel(
|
||||
id: 101,
|
||||
nombre: 'JUAN',
|
||||
apellido: 'MARTINEZ',
|
||||
fecProxcita: '25/09/2026',
|
||||
);
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Scaffold(
|
||||
body: PatientModalView(
|
||||
initialPatient: patient,
|
||||
isSecretary: true,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Verify title reflects reception
|
||||
expect(find.text('Ficha de Recepción'), findsOneWidget);
|
||||
expect(find.text('Recepción, datos personales y programación de citas'), findsOneWidget);
|
||||
|
||||
// Confidential clinical documents and clinical tabs MUST NOT exist
|
||||
expect(find.byType(ClinicalDocumentsDropdown), findsNothing);
|
||||
expect(find.byType(PatientClinicalTabs), findsNothing);
|
||||
|
||||
// Secretary appointment section MUST exist
|
||||
expect(find.byType(SecretaryAppointmentSection), findsOneWidget);
|
||||
expect(find.text('Gestión de Citas y Turno del Paciente'), findsOneWidget);
|
||||
expect(find.text('Próxima Cita Programada: 25/09/2026'), findsOneWidget);
|
||||
expect(find.text('Elegir en Calendario'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('renders full clinical tabs and documents dropdown for Doctor', (tester) async {
|
||||
final patient = PatientModel(
|
||||
id: 102,
|
||||
nombre: 'PEDRO',
|
||||
apellido: 'RAMIREZ',
|
||||
);
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Scaffold(
|
||||
body: PatientModalView(
|
||||
initialPatient: patient,
|
||||
isSecretary: false,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Verify title reflects doctor editing
|
||||
expect(find.text('Editar Paciente'), findsOneWidget);
|
||||
|
||||
// Doctor gets clinical documents dropdown and clinical tabs
|
||||
expect(find.byType(ClinicalDocumentsDropdown), findsOneWidget);
|
||||
expect(find.byType(PatientClinicalTabs), findsOneWidget);
|
||||
expect(find.byType(SecretaryAppointmentSection), findsNothing);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:hidalyx_drluiscastro/features/dashboard/data/models/patient_summary_model.dart';
|
||||
import 'package:hidalyx_drluiscastro/features/dashboard/presentation/widgets/today_appointments_card.dart';
|
||||
|
||||
void main() {
|
||||
group('Sala de Espera (Waiting Room) Logic Tests', () {
|
||||
test('PatientSummaryModel correctly handles waiting room status and helpers', () {
|
||||
const p1 = PatientSummaryModel(
|
||||
id: 1,
|
||||
nombre: 'Carlos',
|
||||
apellido: 'Santana',
|
||||
cedula: '001-1234567-8',
|
||||
telefono: '809-555-0101',
|
||||
fecReg: '21/09/2026',
|
||||
fecProxCita: '21/09/2026',
|
||||
seguro: 'Senasa',
|
||||
motCons: 'Dolor lumbar',
|
||||
estadoEspera: 'no_presente',
|
||||
);
|
||||
|
||||
expect(p1.isNoPresente, isTrue);
|
||||
expect(p1.isEnEspera, isFalse);
|
||||
expect(p1.isEnConsulta, isFalse);
|
||||
expect(p1.isAtendido, isFalse);
|
||||
expect(p1.estadoEsperaLabel, 'No Presente');
|
||||
|
||||
final pEnEspera = p1.copyWith(estadoEspera: 'en_espera');
|
||||
expect(pEnEspera.isEnEspera, isTrue);
|
||||
expect(pEnEspera.isNoPresente, isFalse);
|
||||
expect(pEnEspera.estadoEsperaLabel, 'En Sala de Espera');
|
||||
|
||||
final pEnConsulta = p1.copyWith(estadoEspera: 'en_consulta');
|
||||
expect(pEnConsulta.isEnConsulta, isTrue);
|
||||
expect(pEnConsulta.estadoEsperaLabel, 'En Consulta');
|
||||
|
||||
final pAtendido = p1.copyWith(estadoEspera: 'atendido');
|
||||
expect(pAtendido.isAtendido, isTrue);
|
||||
expect(pAtendido.estadoEsperaLabel, 'Atendido');
|
||||
});
|
||||
|
||||
test('PatientSummaryModel serialization includes estado_espera', () {
|
||||
final json = {
|
||||
'id': 42,
|
||||
'nombre': 'Elena',
|
||||
'apellido': 'Gomez',
|
||||
'cedula': '001-9999999-9',
|
||||
'telefono': '809-555-9999',
|
||||
'fec_reg': '21/09/2026',
|
||||
'fec_proxcita': '21/09/2026',
|
||||
'seguro': 'Humano',
|
||||
'mot_cons': 'Evaluación general',
|
||||
'estado_espera': 'en_espera',
|
||||
};
|
||||
|
||||
final model = PatientSummaryModel.fromJson(json);
|
||||
expect(model.id, 42);
|
||||
expect(model.estadoEspera, 'en_espera');
|
||||
expect(model.isEnEspera, isTrue);
|
||||
|
||||
final outJson = model.toJson();
|
||||
expect(outJson['estado_espera'], 'en_espera');
|
||||
});
|
||||
});
|
||||
|
||||
group('TodayAppointmentsCard Waiting Room UI Tests', () {
|
||||
testWidgets('renders "Llegó" button for secretary when patient is not present', (tester) async {
|
||||
const patient = PatientSummaryModel(
|
||||
id: 10,
|
||||
nombre: 'Mario',
|
||||
apellido: 'Bros',
|
||||
cedula: '001-0000000-1',
|
||||
telefono: '809-111-2222',
|
||||
fecReg: '21/09/2026',
|
||||
fecProxCita: '21/09/2026',
|
||||
seguro: 'Universal',
|
||||
motCons: 'Revisión',
|
||||
estadoEspera: 'no_presente',
|
||||
);
|
||||
|
||||
String? triggeredStatus;
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Scaffold(
|
||||
body: TodayAppointmentsCard(
|
||||
appointments: const [patient],
|
||||
isSecretary: true,
|
||||
onStatusChange: (p, newStatus) async {
|
||||
triggeredStatus = newStatus;
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(find.text('Llegó'), findsOneWidget);
|
||||
expect(find.text('Mario Bros'), findsOneWidget);
|
||||
|
||||
await tester.tap(find.text('Llegó'));
|
||||
await tester.pump();
|
||||
|
||||
expect(triggeredStatus, 'en_espera');
|
||||
});
|
||||
|
||||
testWidgets('renders "En Espera" badge and Doctor action "Atender"', (tester) async {
|
||||
const patient = PatientSummaryModel(
|
||||
id: 11,
|
||||
nombre: 'Luigi',
|
||||
apellido: 'Verde',
|
||||
cedula: '001-0000000-2',
|
||||
telefono: '809-111-3333',
|
||||
fecReg: '21/09/2026',
|
||||
fecProxCita: '21/09/2026',
|
||||
seguro: 'Palic',
|
||||
motCons: 'Control',
|
||||
estadoEspera: 'en_espera',
|
||||
);
|
||||
|
||||
PatientSummaryModel? selectedPatient;
|
||||
String? updatedStatus;
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Scaffold(
|
||||
body: TodayAppointmentsCard(
|
||||
appointments: const [patient],
|
||||
isSecretary: false, // Doctor
|
||||
onPatientTap: (p) => selectedPatient = p,
|
||||
onStatusChange: (p, status) async => updatedStatus = status,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(find.text('En Espera'), findsOneWidget);
|
||||
expect(find.text('Atender'), findsOneWidget);
|
||||
|
||||
await tester.tap(find.text('Atender'));
|
||||
await tester.pump();
|
||||
|
||||
expect(updatedStatus, 'en_consulta');
|
||||
expect(selectedPatient?.id, 11);
|
||||
});
|
||||
|
||||
testWidgets('renders "En Consulta" badge and Doctor action "Finalizar"', (tester) async {
|
||||
const patient = PatientSummaryModel(
|
||||
id: 12,
|
||||
nombre: 'Peach',
|
||||
apellido: 'Toadstool',
|
||||
cedula: '001-0000000-3',
|
||||
telefono: '809-111-4444',
|
||||
fecReg: '21/09/2026',
|
||||
fecProxCita: '21/09/2026',
|
||||
seguro: 'Universal',
|
||||
motCons: 'Consulta en proceso',
|
||||
estadoEspera: 'en_consulta',
|
||||
);
|
||||
|
||||
String? updatedStatus;
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Scaffold(
|
||||
body: TodayAppointmentsCard(
|
||||
appointments: const [patient],
|
||||
isSecretary: false, // Doctor
|
||||
onStatusChange: (p, status) async => updatedStatus = status,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(find.text('En Consulta'), findsOneWidget);
|
||||
expect(find.text('Finalizar'), findsOneWidget);
|
||||
|
||||
await tester.tap(find.text('Finalizar'));
|
||||
await tester.pump();
|
||||
|
||||
expect(updatedStatus, 'atendido');
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user