first commit

This commit is contained in:
=
2026-09-21 16:33:51 -04:00
commit a899479dc9
204 changed files with 21132 additions and 0 deletions
+272
View File
@@ -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'));
});
});
}
+185
View File
@@ -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}) {}
}
+351
View File
@@ -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]));
});
});
}
+83
View File
@@ -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]));
});
});
}
+79
View File
@@ -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]));
});
});
}
+222
View File
@@ -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);
});
});
}
+397
View File
@@ -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');
});
});
}
+122
View File
@@ -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]));
});
});
}
+169
View File
@@ -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);
});
});
}
+183
View File
@@ -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');
});
});
}