You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
import'package:flutter/material.dart';
classCampoNombreAccesibleextendsStatelessWidget {
finalTextEditingController controller;
finalFunction(String)? onChanged;
constCampoNombreAccesible({
Key? key,
requiredthis.controller,
this.onChanged,
}) :super(key: key);
@overrideWidgetbuild(BuildContext context) {
returnSemantics(
label:'Campo de texto para ingresar el nombre completo',
hint:'Escribe tu nombre y apellidos aquí. Doble toque para editar.',
textDirection:TextDirection.ltr,
child:TextFormField(
controller: controller,
decoration:InputDecoration(
labelText:'Nombre completo *',
border:OutlineInputBorder(),
prefixIcon:Icon(Icons.person),
errorMaxLines:2,
),
validator: (value) => value?.isEmpty ??true?'El nombre es requerido':null,
autovalidateMode:AutovalidateMode.onUserInteraction,
onChanged: onChanged,
),
);
}
}
CampoTelefonoAccesible.dart
import'package:flutter/material.dart';
classCampoTelefonoAccesibleextendsStatelessWidget {
finalTextEditingController controller;
finalFunction(String)? onChanged;
constCampoTelefonoAccesible({
Key? key,
requiredthis.controller,
this.onChanged,
}) :super(key: key);
@overrideWidgetbuild(BuildContext context) {
returnSemantics(
label:'Campo de texto para ingresar el número de teléfono',
hint:'Ingresa tu número con código de área. Ejemplo: +34 123 456 789',
textDirection:TextDirection.ltr,
child:TextFormField(
controller: controller,
decoration:InputDecoration(
labelText:'Teléfono *',
border:OutlineInputBorder(),
prefixIcon:Icon(Icons.phone),
errorMaxLines:2,
),
keyboardType:TextInputType.phone,
validator: (value) {
if (value?.isEmpty ??true) return'El teléfono es requerido';
if (!RegExp(r'^\+?\d{9,15}$').hasMatch(value!)) {
return'Teléfono inválido (9-15 dígitos)';
}
returnnull;
},
autovalidateMode:AutovalidateMode.onUserInteraction,
onChanged: onChanged,
),
);
}
}
main.dart
import'package:flutter/material.dart';
import'package:practica_examen_3_7_semantics_textfield_button/presentation/widgets/BotonEnviarAccesible.dart';
import'package:practica_examen_3_7_semantics_textfield_button/presentation/widgets/CampoNombreAccesible.dart';
import'package:practica_examen_3_7_semantics_textfield_button/presentation/widgets/CampoTelefonoAccesible.dart';
voidmain() =>runApp(MyApp());
classMyAppextendsStatelessWidget {
@overrideWidgetbuild(BuildContext context) {
returnMaterialApp(
title:'Formulario Accesible',
theme:ThemeData(primarySwatch:Colors.blue),
home:FormularioAccesible(),
);
}
}
classFormularioAccesibleextendsStatefulWidget {
@override_FormularioAccesibleStatecreateState() =>_FormularioAccesibleState();
}
class_FormularioAccesibleStateextendsState<FormularioAccesible> {
final _formKey =GlobalKey<FormState>();
final _nombreController =TextEditingController();
final _telefonoController =TextEditingController();
@overrideWidgetbuild(BuildContext context) {
returnScaffold(
appBar:AppBar(title:Text('Formulario Accesible')),
body:Padding(
padding:EdgeInsets.all(24.0),
child:Form(
key: _formKey,
child:Column(
crossAxisAlignment:CrossAxisAlignment.stretch,
children: [
CampoNombreAccesible(
controller: _nombreController,
onChanged: (value) =>setState(() {}),
),
SizedBox(height:20),
CampoTelefonoAccesible(
controller: _telefonoController,
// Sin setState(), el botón no se entera de que escribiste// Con setState(), el botón se revisa y se habilita cuando todo está bien
onChanged: (value) =>setState(() {}),
),
SizedBox(height:32),
BotonEnviarAccesible(
// El botón mira si los campos son válidos: _formKey.currentState?.validate()
onPressed: _formKey.currentState?.validate() ==true? () =>_enviarFormulario()
:null,
),
],
),
),
),
);
}
void_enviarFormulario() {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content:Text('✅ Formulario enviado')),
);
}
/** * Los TextEditingController (_nombreController, _telefonoController) guardan texto en memoria Cuando sales de la pantalla (el widget se destruye), dispose() les dice: "¡Libera esa memoria!" super.dispose() limpia el resto del widget */@overridevoiddispose() {
_nombreController.dispose();
_telefonoController.dispose();
super.dispose();
}
}
widget_test.dart
import'package:flutter/material.dart';
import'package:flutter_test/flutter_test.dart';
import'package:practica_examen_3_7_semantics_textfield_button/presentation/widgets/BotonEnviarAccesible.dart';
import'package:practica_examen_3_7_semantics_textfield_button/presentation/widgets/CampoNombreAccesible.dart';
voidmain() {
// Punto de entrada para todas las pruebas del archivo de test// main() ejecuta todos los testWidgets() definidos aquítestWidgets('BotonEnviarAccesible funciona y es accesible', (WidgetTester tester) async {
// WidgetTester simula interacciones reales (tap, scroll, etc.) en widgets// Activa el motor de accesibilidad para detectar Semantics correctamentefinalSemanticsHandle handle = tester.ensureSemantics();
// PRUEBA 1: Botón habilitado (onPressed tiene función)// pumpWidget() construye el widget completo en el entorno de testawait tester.pumpWidget(
MaterialApp(
// MaterialApp provee ThemeData, MediaQuery y Localizations
home:BotonEnviarAccesible(onPressed: () {}), // Función vacía simula botón activo
),
);
// Verifica que el texto del botón se renderiza correctamenteexpect(find.text('Enviar Datos'), findsOneWidget);
// Busca el widget por su label de accesibilidad (lo que lee TalkBack)expect(find.bySemanticsLabel('Botón para enviar el formulario de contacto'), findsOneWidget);
// Obtiene el nodo Semantics y verifica sus propiedades específicasexpect(
tester.getSemantics(find.bySemanticsLabel('Botón para enviar el formulario de contacto')),
matchesSemantics(
label:'Botón para enviar el formulario de contacto', // Texto exacto que lee el screen reader
isButton:true, // Flutter marca este nodo como botón interactivo
),
);
// PRUEBA 2: Botón deshabilitado (onPressed = null)// Reconstruye el widget con estado deshabilitadoawait tester.pumpWidget(MaterialApp(home:BotonEnviarAccesible(onPressed:null)));
// Verifica que el ElevatedButton físico sigue existiendo (solo cambia color)expect(find.byType(ElevatedButton), findsOneWidget);
// Semantics debe seguir accesible aunque el botón esté visualmente grisexpect(find.bySemanticsLabel('Botón para enviar el formulario de contacto'), findsOneWidget);
// Desactiva el motor de accesibilidad (OBLIGATORIO al final)
handle.dispose();
});
testWidgets('CampoNombreAccesible muestra UI y valida correctamente', (WidgetTester tester) async {
// Controller local exclusivo para este test (se autodestruye al final)final controller =TextEditingController();
// Construye el widget con Scaffold (proveedor de Material requerido por TextFormField)await tester.pumpWidget(MaterialApp(
home:Scaffold(
body:CampoNombreAccesible(controller: controller, onChanged: (value) {}),
),
));
// Verifica widgets visuales básicos del campoexpect(find.byType(TextFormField), findsOneWidget); // Campo de texto existeexpect(find.byIcon(Icons.person), findsOneWidget); // Icono de persona existe// Simula escritura de texto válido → validator pasaawait tester.enterText(find.byType(TextFormField), 'Juan Perez');
await tester.pumpAndSettle(); // Espera animaciones y validación completa// Simula borrar texto → validator fallaawait tester.enterText(find.byType(TextFormField), '');
await tester.pumpAndSettle(); // autovalidateMode muestra error// Campo sigue funcional después de múltiples interaccionesexpect(find.byType(TextFormField), findsOneWidget);
});
testWidgets('CampoNombreAccesible accesibilidad CON Semantics', (WidgetTester tester) async {
// Controller local para este test específicofinal controller =TextEditingController();
// Construye el widget baseawait tester.pumpWidget(MaterialApp(
home:Scaffold(
body:CampoNombreAccesible(controller: controller, onChanged: (v) {}),
),
));
// pump() fuerza un frame de renderizado (TextFormField necesita múltiples frames)await tester.pump(); // Frame 1: construye TextFormFieldawait tester.pump(); // Frame 2: TextFormField inicializa completamente// Activa motor de accesibilidad DESPUÉS de que los widgets estén listosfinalSemanticsHandle handle = tester.ensureSemantics();
await tester.pump(); // Frame 3: Semantics se actualiza// debugDumpApp() imprime el árbol completo de widgets en consola (útil para debug)debugDumpApp();
// TextFormField existe y está renderizadoexpect(find.byType(TextFormField), findsOneWidget);
// Variable auxiliar para reutilizar el finder (buena práctica)final textFieldFinder = find.byType(TextFormField);
expect(textFieldFinder, findsOneWidget); // Verificación redundante para robustez// Desactiva motor de accesibilidad
handle.dispose();
});
}