Files
hidalyx-drluiscastro/test/features/patient_test.dart
T

398 lines
14 KiB
Dart
Raw Normal View History

2026-09-21 16:33:51 -04:00
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');
});
});
}