GUÍA DE STEPS PERSONALIZADOS - HAKALAB FRAMEWORK
================================================

Esta guía explica cómo crear steps personalizados para interactuar con elementos 
web en tus pruebas automatizadas.

ÍNDICE
======
1. Introducción
2. Estructura Básica
3. Localización de Elementos
4. Interacción con Elementos
5. Manejo de Errores
6. Ejemplos Completos
7. Mejores Prácticas
8. Integración con el Framework

================================================

1. INTRODUCCIÓN
===============

Los steps personalizados te permiten extender el framework con funcionalidades 
específicas para tu aplicación.

¿Cuándo crear steps personalizados?
- Cuando necesitas interacciones específicas de tu aplicación
- Para simplificar secuencias complejas de acciones
- Para encapsular lógica de negocio específica
- Cuando los steps existentes no cubren tu caso de uso

================================================

2. ESTRUCTURA BÁSICA
====================

Crear el Archivo de Steps:

# features/steps/custom_steps.py

from behave import step, given, when, then
from playwright.sync_api import expect

@step('mi step personalizado con "{parametro}"')
def mi_step_personalizado(context, parametro):
    """
    Descripción de lo que hace el step.
    
    Args:
        context: Contexto de Behave con acceso a page, variables, etc.
        parametro: Parámetro que recibe el step
    """
    try:
        # Tu lógica aquí
        page = context.page
        print(f"Ejecutando step con parámetro: {parametro}")
        
    except Exception as e:
        raise AssertionError(f"Error en mi_step_personalizado: {str(e)}")

Decoradores Disponibles:

# @step - Funciona con Given, When, Then
@step('hago algo con "{valor}"')
def hacer_algo(context, valor):
    pass

# @given - Solo para Given
@given('tengo un elemento "{elemento}"')
def tengo_elemento(context, elemento):
    pass

# @when - Solo para When
@when('interactúo con "{elemento}"')
def interactuar(context, elemento):
    pass

# @then - Solo para Then
@then('debería ver "{texto}"')
def deberia_ver(context, texto):
    pass

================================================

3. LOCALIZACIÓN DE ELEMENTOS
============================

Método 1: Usar el Element Locator del Framework

from behave import step

@step('hago click en mi elemento JSON "{identifier}"')
def click_elemento_json(context, identifier):
    """
    Click en un elemento usando el localizador JSON del framework.
    
    El identifier debe tener formato: $.ARCHIVO.elemento
    Ejemplo: $.LOGIN.username_field
    """
    try:
        # El ElementLocator ya está disponible en context.element_locator
        element = context.element_locator.find(context.page, identifier)
        element.click()
        
        print(f"✓ Click exitoso en: {identifier}")
        
    except Exception as e:
        raise AssertionError(f"Error al hacer click en {identifier}: {str(e)}")

Método 2: Localización Directa con Playwright

@step('interactúo con el elemento CSS "{selector}"')
def interactuar_css(context, selector):
    """Interacción directa usando selector CSS."""
    try:
        page = context.page
        
        # Esperar a que el elemento sea visible
        element = page.wait_for_selector(selector, state="visible", timeout=30000)
        
        # Interactuar con el elemento
        element.click()
        
    except Exception as e:
        raise AssertionError(f"Error al interactuar con {selector}: {str(e)}")

Método 3: Múltiples Estrategias de Localización

@step('busco y hago click en "{texto_o_selector}"')
def buscar_y_click(context, texto_o_selector):
    """
    Intenta múltiples estrategias para encontrar y hacer click.
    """
    page = context.page
    
    try:
        # Estrategia 1: Por texto
        try:
            page.get_by_text(texto_o_selector).click(timeout=5000)
            print(f"✓ Click por texto: {texto_o_selector}")
            return
        except:
            pass
        
        # Estrategia 2: Por role y nombre
        try:
            page.get_by_role("button", name=texto_o_selector).click(timeout=5000)
            print(f"✓ Click por role: {texto_o_selector}")
            return
        except:
            pass
        
        # Estrategia 3: Por selector CSS
        try:
            page.locator(texto_o_selector).click(timeout=5000)
            print(f"✓ Click por selector: {texto_o_selector}")
            return
        except:
            pass
        
        # Si ninguna estrategia funcionó
        raise Exception(f"No se pudo encontrar el elemento: {texto_o_selector}")
        
    except Exception as e:
        raise AssertionError(f"Error en buscar_y_click: {str(e)}")

================================================

4. INTERACCIÓN CON ELEMENTOS
============================

Click Avanzado:

@step('hago click avanzado en "{selector}" con opciones')
def click_avanzado(context, selector):
    """
    Click con opciones avanzadas: espera, scroll, hover previo.
    """
    try:
        page = context.page
        element = page.locator(selector)
        
        # Scroll al elemento
        element.scroll_into_view_if_needed()
        
        # Hover previo (útil para menús)
        element.hover()
        
        # Esperar a que sea clickeable
        element.wait_for(state="visible", timeout=10000)
        
        # Click con force si es necesario
        try:
            element.click(timeout=5000)
        except:
            element.click(force=True)
        
        print(f"✓ Click avanzado exitoso en: {selector}")
        
    except Exception as e:
        raise AssertionError(f"Error en click avanzado: {str(e)}")

Entrada de Texto Personalizada:

@step('escribo en mi campo personalizado "{selector}" el valor "{texto}"')
def escribir_campo_personalizado(context, selector, texto):
    """
    Entrada de texto con limpieza previa y validación.
    """
    try:
        page = context.page
        element = page.locator(selector)
        
        # Esperar a que el campo esté visible
        element.wait_for(state="visible", timeout=10000)
        
        # Limpiar el campo primero
        element.fill("")
        
        # Escribir el texto
        element.fill(texto)
        
        # Validar que el texto se escribió correctamente
        valor_actual = element.input_value()
        if valor_actual != texto:
            raise Exception(f"El texto no se escribió correctamente")
        
        print(f"✓ Texto escrito correctamente: {texto}")
        
    except Exception as e:
        raise AssertionError(f"Error al escribir en {selector}: {str(e)}")

Selección de Dropdown Personalizado:

@step('selecciono en mi dropdown "{selector}" la opción "{opcion}"')
def seleccionar_dropdown_personalizado(context, selector, opcion):
    """
    Selección en dropdown con múltiples estrategias.
    """
    try:
        page = context.page
        
        # Estrategia 1: Select nativo
        try:
            page.select_option(selector, label=opcion, timeout=5000)
            print(f"✓ Opción seleccionada (select nativo): {opcion}")
            return
        except:
            pass
        
        # Estrategia 2: Dropdown personalizado
        try:
            # Abrir dropdown
            page.click(selector)
            page.wait_for_timeout(500)
            
            # Buscar y hacer click en la opción
            page.get_by_text(opcion, exact=True).click()
            print(f"✓ Opción seleccionada (dropdown custom): {opcion}")
            return
        except:
            pass
        
        # Estrategia 3: Por data-value
        try:
            page.click(selector)
            page.wait_for_timeout(500)
            page.click(f"[data-value='{opcion}']")
            print(f"✓ Opción seleccionada (data-value): {opcion}")
            return
        except:
            pass
        
        raise Exception(f"No se pudo seleccionar la opción: {opcion}")
        
    except Exception as e:
        raise AssertionError(f"Error al seleccionar dropdown: {str(e)}")

Espera Condicional Personalizada:

@step('espero hasta que mi elemento "{selector}" cumpla la condición "{condicion}"')
def esperar_condicion_personalizada(context, selector, condicion):
    """
    Espera hasta que se cumpla una condición específica.
    
    Condiciones: visible, oculto, habilitado, deshabilitado, contiene_texto
    """
    try:
        page = context.page
        element = page.locator(selector)
        
        if condicion == "visible":
            element.wait_for(state="visible", timeout=30000)
            
        elif condicion == "oculto":
            element.wait_for(state="hidden", timeout=30000)
            
        elif condicion == "habilitado":
            page.wait_for_function(
                f"document.querySelector('{selector}').disabled === false",
                timeout=30000
            )
            
        elif condicion == "deshabilitado":
            page.wait_for_function(
                f"document.querySelector('{selector}').disabled === true",
                timeout=30000
            )
        
        print(f"✓ Condición cumplida: {condicion}")
        
    except Exception as e:
        raise AssertionError(f"Error esperando condición: {str(e)}")

================================================

5. MANEJO DE ERRORES
====================

Manejo Robusto de Errores:

@step('interactúo de forma robusta con "{selector}"')
def interaccion_robusta(context, selector):
    """
    Interacción con manejo robusto de errores y reintentos.
    """
    page = context.page
    max_intentos = 3
    
    for intento in range(max_intentos):
        try:
            # Intentar la interacción
            element = page.locator(selector)
            element.wait_for(state="visible", timeout=10000)
            element.click()
            
            print(f"✓ Interacción exitosa en intento {intento + 1}")
            return
            
        except Exception as e:
            if intento < max_intentos - 1:
                print(f"⚠ Intento {intento + 1} falló, reintentando...")
                page.wait_for_timeout(1000)
            else:
                # Capturar screenshot del error
                screenshot_path = f"screenshots/error_{selector.replace('/', '_')}.png"
                page.screenshot(path=screenshot_path)
                
                raise AssertionError(
                    f"Error después de {max_intentos} intentos: {str(e)}\n"
                    f"Screenshot guardado en: {screenshot_path}"
                )

Validación de Precondiciones:

@step('valido y hago click en "{selector}"')
def validar_y_click(context, selector):
    """
    Valida precondiciones antes de hacer click.
    """
    try:
        page = context.page
        element = page.locator(selector)
        
        # Validar que el elemento existe
        if element.count() == 0:
            raise Exception(f"El elemento no existe: {selector}")
        
        # Validar que es visible
        if not element.is_visible():
            raise Exception(f"El elemento no es visible: {selector}")
        
        # Validar que está habilitado
        if not element.is_enabled():
            raise Exception(f"El elemento está deshabilitado: {selector}")
        
        # Si todas las validaciones pasan, hacer click
        element.click()
        print(f"✓ Click exitoso después de validaciones: {selector}")
        
    except Exception as e:
        raise AssertionError(f"Error en validación: {str(e)}")

================================================

6. EJEMPLOS COMPLETOS
=====================

Ejemplo 1: Step para Componente Personalizado

@step('interactúo con mi componente de búsqueda usando "{termino}"')
def interactuar_componente_busqueda(context, termino):
    """
    Interacción completa con un componente de búsqueda personalizado.
    """
    try:
        page = context.page
        
        # 1. Abrir el componente de búsqueda
        page.click("#search-trigger")
        page.wait_for_selector("#search-modal", state="visible")
        
        # 2. Escribir el término de búsqueda
        page.fill("#search-input", termino)
        
        # 3. Esperar resultados
        page.wait_for_selector(".search-results", state="visible")
        
        # 4. Validar que hay resultados
        resultados = page.locator(".search-result-item").count()
        if resultados == 0:
            raise Exception(f"No se encontraron resultados para: {termino}")
        
        # 5. Hacer click en el primer resultado
        page.click(".search-result-item:first-child")
        
        print(f"✓ Búsqueda completada: {termino} ({resultados} resultados)")
        
    except Exception as e:
        raise AssertionError(f"Error en componente de búsqueda: {str(e)}")

Ejemplo 2: Step con Variables del Framework

from hakalab_framework.core.variable_manager import get_variable, set_variable

@step('extraigo datos de mi tabla y los guardo')
def extraer_datos_tabla(context):
    """
    Extrae datos de una tabla y los guarda en variables del framework.
    """
    try:
        page = context.page
        
        # Obtener todas las filas de la tabla
        filas = page.locator("table tbody tr").all()
        
        datos_extraidos = []
        for fila in filas:
            # Extraer celdas de cada fila
            celdas = fila.locator("td").all()
            
            fila_datos = {
                "nombre": celdas[0].inner_text(),
                "email": celdas[1].inner_text(),
                "rol": celdas[2].inner_text()
            }
            datos_extraidos.append(fila_datos)
        
        # Guardar en variables del framework
        set_variable(context, "datos_tabla", datos_extraidos)
        set_variable(context, "total_filas", len(datos_extraidos))
        
        print(f"✓ Datos extraídos: {len(datos_extraidos)} filas")
        
    except Exception as e:
        raise AssertionError(f"Error extrayendo datos: {str(e)}")

Ejemplo 3: Step con Screenshot Automático

from hakalab_framework.core.screenshot_manager import take_screenshot

@step('valido mi formulario complejo y capturo evidencia')
def validar_formulario_complejo(context):
    """
    Valida un formulario complejo y captura screenshots de evidencia.
    """
    try:
        page = context.page
        
        # 1. Capturar estado inicial
        take_screenshot(context, "formulario_inicial")
        
        # 2. Validar campos requeridos
        campos_requeridos = [
            "#nombre",
            "#email",
            "#telefono",
            "#direccion"
        ]
        
        for campo in campos_requeridos:
            element = page.locator(campo)
            if not element.is_visible():
                raise Exception(f"Campo requerido no visible: {campo}")
        
        # 3. Capturar después de validación
        take_screenshot(context, "formulario_validado")
        
        print("✓ Formulario validado correctamente")
        
    except Exception as e:
        # Capturar screenshot del error
        take_screenshot(context, "formulario_error")
        raise AssertionError(f"Error validando formulario: {str(e)}")

================================================

7. MEJORES PRÁCTICAS
====================

Nomenclatura Clara:

# ❌ Mal
@step('hago cosa')
def hacer_cosa(context):
    pass

# ✅ Bien
@step('hago click en el botón de envío del formulario de contacto')
def click_boton_envio_contacto(context):
    pass

Documentación Completa:

@step('mi step bien documentado con "{parametro}"')
def step_documentado(context, parametro):
    """
    Descripción clara de lo que hace el step.
    
    Este step realiza las siguientes acciones:
    1. Localiza el elemento usando el parámetro
    2. Valida que el elemento es interactuable
    3. Realiza la acción principal
    4. Valida el resultado
    
    Args:
        context: Contexto de Behave con acceso a page, variables, etc.
        parametro: Selector o identificador del elemento
        
    Raises:
        AssertionError: Si el elemento no se encuentra o la acción falla
        
    Example:
        When mi step bien documentado con "#mi-elemento"
    """
    pass

Reutilización de Código:

# Crear funciones auxiliares
def _esperar_elemento_visible(page, selector, timeout=30000):
    """Función auxiliar para esperar elementos."""
    return page.wait_for_selector(selector, state="visible", timeout=timeout)

def _validar_elemento_clickeable(page, selector):
    """Función auxiliar para validar que un elemento es clickeable."""
    element = page.locator(selector)
    return element.is_visible() and element.is_enabled()

# Usar en múltiples steps
@step('hago click en "{selector}"')
def click_elemento(context, selector):
    page = context.page
    _esperar_elemento_visible(page, selector)
    if not _validar_elemento_clickeable(page, selector):
        raise Exception(f"Elemento no clickeable: {selector}")
    page.click(selector)

Logging Informativo:

@step('proceso complejo con logging')
def proceso_con_logging(context):
    """Step con logging detallado."""
    try:
        print("🔄 Iniciando proceso complejo...")
        
        # Paso 1
        print("  ├─ Paso 1: Localizando elemento...")
        # código
        print("  ├─ ✓ Elemento localizado")
        
        # Paso 2
        print("  ├─ Paso 2: Validando condiciones...")
        # código
        print("  ├─ ✓ Condiciones validadas")
        
        # Paso 3
        print("  └─ Paso 3: Ejecutando acción...")
        # código
        print("✓ Proceso completado exitosamente")
        
    except Exception as e:
        print(f"✗ Error en el proceso: {str(e)}")
        raise AssertionError(f"Error: {str(e)}")

================================================

8. INTEGRACIÓN CON EL FRAMEWORK
===============================

Archivo Completo de Ejemplo:

# features/steps/mi_aplicacion_steps.py

"""
Steps personalizados para Mi Aplicación.

Este módulo contiene steps específicos para interactuar con
los componentes personalizados de Mi Aplicación.
"""

from behave import step, given, when, then
from playwright.sync_api import expect
from hakalab_framework.core.variable_manager import get_variable, set_variable
from hakalab_framework.core.screenshot_manager import take_screenshot

@given('estoy en la página principal de mi aplicación')
def navegar_pagina_principal(context):
    """Navega a la página principal de la aplicación."""
    try:
        base_url = get_variable(context, "BASE_URL", "https://mi-app.com")
        context.page.goto(base_url)
        context.page.wait_for_load_state("networkidle")
        print(f"✓ Navegado a: {base_url}")
    except Exception as e:
        raise AssertionError(f"Error navegando a página principal: {str(e)}")

@when('interactúo con mi componente especial "{nombre_componente}"')
def interactuar_componente_especial(context, nombre_componente):
    """
    Interactúa con un componente especial de la aplicación.
    """
    try:
        page = context.page
        selector = f"[data-component='{nombre_componente}']"
        
        # Esperar y hacer click
        element = page.wait_for_selector(selector, state="visible")
        element.click()
        
        # Esperar que el componente se active
        page.wait_for_selector(f"{selector}.active", state="visible")
        
        print(f"✓ Componente activado: {nombre_componente}")
        
    except Exception as e:
        take_screenshot(context, f"error_componente_{nombre_componente}")
        raise AssertionError(f"Error en componente {nombre_componente}: {str(e)}")

@then('mi componente "{nombre}" debería mostrar el estado "{estado}"')
def validar_estado_componente(context, nombre, estado):
    """Valida el estado de un componente personalizado."""
    try:
        page = context.page
        selector = f"[data-component='{nombre}'][data-state='{estado}']"
        
        element = page.locator(selector)
        expect(element).to_be_visible(timeout=10000)
        
        print(f"✓ Componente {nombre} en estado: {estado}")
        
    except Exception as e:
        raise AssertionError(f"Estado incorrecto en {nombre}: {str(e)}")

Uso en Features:

# features/mi_aplicacion.feature

Feature: Funcionalidades personalizadas de Mi Aplicación

  Background:
    Given estoy en la página principal de mi aplicación

  Scenario: Interactuar con componente especial
    When interactúo con mi componente especial "menu-principal"
    Then mi componente "menu-principal" debería mostrar el estado "expandido"

================================================

RECURSOS ADICIONALES
====================

Documentación Útil:
- Playwright Python: https://playwright.dev/python/docs/intro
- Behave: https://behave.readthedocs.io/
- Selectores CSS: https://developer.mozilla.org/es/docs/Web/CSS/CSS_Selectors
- XPath: https://developer.mozilla.org/es/docs/Web/XPath

Ejemplos del Framework:
Revisa los steps existentes del framework para inspiración:
- hakalab_framework/steps/interaction_steps.py
- hakalab_framework/steps/navigation_steps.py
- hakalab_framework/steps/validation_steps.py

================================================

CONCLUSIÓN

¡Ahora estás listo para crear tus propios steps personalizados!

Recuerda:
- Mantén los steps simples y enfocados
- Documenta bien tu código
- Maneja errores apropiadamente
- Reutiliza código cuando sea posible
- Prueba tus steps exhaustivamente

================================================
Documento generado automáticamente - Hakalab Framework v1.3.0
Guía de Steps Personalizados
Fecha: Enero 2026
================================================
