Skip to content

Instantly share code, notes, and snippets.

@desmonHak
Last active January 27, 2026 20:19
Show Gist options
  • Select an option

  • Save desmonHak/7a52de893d9e899adf48fd85ccb0413b to your computer and use it in GitHub Desktop.

Select an option

Save desmonHak/7a52de893d9e899adf48fd85ccb0413b to your computer and use it in GitHub Desktop.
Ejemplo de Test Widgets en flutter

BotonEnviarAccesible.dart

import 'package:flutter/material.dart';

class BotonEnviarAccesible extends StatelessWidget {
  final VoidCallback? onPressed;

  const BotonEnviarAccesible({Key? key, this.onPressed}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Semantics(
      button: true,
      label: 'Botón para enviar el formulario de contacto',
      hint:
          'Doble toque para enviar tus datos. Campos obligatorios tienen asterisco.',
      onTapHint: 'Formulario enviado correctamente',
      child: SizedBox(
        height: 50,
        child: ElevatedButton(
          onPressed: onPressed,
          style: ElevatedButton.styleFrom(
            shape: RoundedRectangleBorder(
              borderRadius: BorderRadius.circular(8),
              side: BorderSide(color: Colors.blue, width: 2), // ← Borde azul
            ),
            backgroundColor: onPressed != null ? null : Colors.grey,
          ),
          child: Row(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              Icon(Icons.send),
              SizedBox(width: 8),
              Text('Enviar Datos'),
            ],
          ),
        ),
      ),
    );
  }
}

CampoNombreAccesible.dart

import 'package:flutter/material.dart';

class CampoNombreAccesible extends StatelessWidget {
  final TextEditingController controller;
  final Function(String)? onChanged;

  const CampoNombreAccesible({
    Key? key,
    required this.controller,
    this.onChanged,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Semantics(
      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';

class CampoTelefonoAccesible extends StatelessWidget {
  final TextEditingController controller;
  final Function(String)? onChanged;

  const CampoTelefonoAccesible({
    Key? key,
    required this.controller,
    this.onChanged,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Semantics(
      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)';
          }
          return null;
        },
        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';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Formulario Accesible',
      theme: ThemeData(primarySwatch: Colors.blue),
      home: FormularioAccesible(),
    );
  }
}

class FormularioAccesible extends StatefulWidget {
  @override
  _FormularioAccesibleState createState() => _FormularioAccesibleState();
}

class _FormularioAccesibleState extends State<FormularioAccesible> {
  final _formKey = GlobalKey<FormState>();
  final _nombreController = TextEditingController();
  final _telefonoController = TextEditingController();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      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
   */
  @override
  void dispose() {
    _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';

void main() {
  // 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 correctamente
    final SemanticsHandle handle = tester.ensureSemantics();

    // PRUEBA 1: Botón habilitado (onPressed tiene función)
    // pumpWidget() construye el widget completo en el entorno de test
    await 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 correctamente
    expect(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íficas
    expect(
      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 deshabilitado
    await 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 gris
    expect(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 campo
    expect(find.byType(TextFormField), findsOneWidget); // Campo de texto existe
    expect(find.byIcon(Icons.person), findsOneWidget);  // Icono de persona existe

    // Simula escritura de texto válido → validator pasa
    await tester.enterText(find.byType(TextFormField), 'Juan Perez');
    await tester.pumpAndSettle(); // Espera animaciones y validación completa

    // Simula borrar texto → validator falla
    await tester.enterText(find.byType(TextFormField), '');
    await tester.pumpAndSettle(); // autovalidateMode muestra error

    // Campo sigue funcional después de múltiples interacciones
    expect(find.byType(TextFormField), findsOneWidget);
  });

  testWidgets('CampoNombreAccesible accesibilidad CON Semantics', (WidgetTester tester) async {
    // Controller local para este test específico
    final controller = TextEditingController();

    // Construye el widget base
    await 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 TextFormField
    await tester.pump(); // Frame 2: TextFormField inicializa completamente

    // Activa motor de accesibilidad DESPUÉS de que los widgets estén listos
    final SemanticsHandle 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á renderizado
    expect(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();
  });
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment