first commit
This commit is contained in:
@@ -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');
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user