package browser import ( "context" "fmt" "time" ) // WaitOptions opciones para métodos de espera con condiciones type WaitOptions struct { Timeout time.Duration // Timeout máximo (default: 30s) PollInterval time.Duration // Intervalo entre comprobaciones (default: 100ms) ThrowOnError bool // Lanzar error si timeout (default: true) } // DefaultWaitOptions retorna opciones por defecto para esperas func DefaultWaitOptions() *WaitOptions { return &WaitOptions{ Timeout: 30 * time.Second, PollInterval: 100 * time.Millisecond, ThrowOnError: true, } } // WaitUntilVisible espera a que un elemento sea visible func (b *Browser) WaitUntilVisible(ctx context.Context, selector string, opts *WaitOptions) error { if opts == nil { opts = DefaultWaitOptions() } ctx, cancel := context.WithTimeout(ctx, opts.Timeout) defer cancel() ticker := time.NewTicker(opts.PollInterval) defer ticker.Stop() for { select { case <-ctx.Done(): if opts.ThrowOnError { return fmt.Errorf("timeout waiting for element to be visible: %s", selector) } return nil case <-ticker.C: script := fmt.Sprintf(` (() => { const el = document.querySelector('%s'); if (!el) return false; const style = window.getComputedStyle(el); const rect = el.getBoundingClientRect(); return style.display !== 'none' && style.visibility !== 'hidden' && style.opacity !== '0' && rect.width > 0 && rect.height > 0; })() `, selector) result, err := b.Evaluate(ctx, script) if err != nil { continue } if visible, ok := result.Value.(bool); ok && visible { return nil } } } } // WaitUntilHidden espera a que un elemento esté oculto o no exista func (b *Browser) WaitUntilHidden(ctx context.Context, selector string, opts *WaitOptions) error { if opts == nil { opts = DefaultWaitOptions() } ctx, cancel := context.WithTimeout(ctx, opts.Timeout) defer cancel() ticker := time.NewTicker(opts.PollInterval) defer ticker.Stop() for { select { case <-ctx.Done(): if opts.ThrowOnError { return fmt.Errorf("timeout waiting for element to be hidden: %s", selector) } return nil case <-ticker.C: script := fmt.Sprintf(` (() => { const el = document.querySelector('%s'); if (!el) return true; // No existe = oculto const style = window.getComputedStyle(el); return style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0'; })() `, selector) result, err := b.Evaluate(ctx, script) if err != nil { continue } if hidden, ok := result.Value.(bool); ok && hidden { return nil } } } } // WaitUntilClickable espera a que un elemento sea clickeable func (b *Browser) WaitUntilClickable(ctx context.Context, selector string, opts *WaitOptions) error { if opts == nil { opts = DefaultWaitOptions() } ctx, cancel := context.WithTimeout(ctx, opts.Timeout) defer cancel() ticker := time.NewTicker(opts.PollInterval) defer ticker.Stop() for { select { case <-ctx.Done(): if opts.ThrowOnError { return fmt.Errorf("timeout waiting for element to be clickable: %s", selector) } return nil case <-ticker.C: script := fmt.Sprintf(` (() => { const el = document.querySelector('%s'); if (!el) return false; const style = window.getComputedStyle(el); const rect = el.getBoundingClientRect(); return style.display !== 'none' && style.visibility !== 'hidden' && style.pointerEvents !== 'none' && !el.disabled && rect.width > 0 && rect.height > 0; })() `, selector) result, err := b.Evaluate(ctx, script) if err != nil { continue } if clickable, ok := result.Value.(bool); ok && clickable { return nil } } } } // WaitUntilEnabled espera a que un elemento esté habilitado func (b *Browser) WaitUntilEnabled(ctx context.Context, selector string, opts *WaitOptions) error { if opts == nil { opts = DefaultWaitOptions() } ctx, cancel := context.WithTimeout(ctx, opts.Timeout) defer cancel() ticker := time.NewTicker(opts.PollInterval) defer ticker.Stop() for { select { case <-ctx.Done(): if opts.ThrowOnError { return fmt.Errorf("timeout waiting for element to be enabled: %s", selector) } return nil case <-ticker.C: script := fmt.Sprintf(` (() => { const el = document.querySelector('%s'); return el && !el.disabled; })() `, selector) result, err := b.Evaluate(ctx, script) if err != nil { continue } if enabled, ok := result.Value.(bool); ok && enabled { return nil } } } } // WaitUntilDisabled espera a que un elemento esté deshabilitado func (b *Browser) WaitUntilDisabled(ctx context.Context, selector string, opts *WaitOptions) error { if opts == nil { opts = DefaultWaitOptions() } ctx, cancel := context.WithTimeout(ctx, opts.Timeout) defer cancel() ticker := time.NewTicker(opts.PollInterval) defer ticker.Stop() for { select { case <-ctx.Done(): if opts.ThrowOnError { return fmt.Errorf("timeout waiting for element to be disabled: %s", selector) } return nil case <-ticker.C: script := fmt.Sprintf(` (() => { const el = document.querySelector('%s'); return el && el.disabled === true; })() `, selector) result, err := b.Evaluate(ctx, script) if err != nil { continue } if disabled, ok := result.Value.(bool); ok && disabled { return nil } } } } // WaitUntilTextMatches espera a que el texto de un elemento contenga un patrón func (b *Browser) WaitUntilTextMatches(ctx context.Context, selector, text string, opts *WaitOptions) error { if opts == nil { opts = DefaultWaitOptions() } ctx, cancel := context.WithTimeout(ctx, opts.Timeout) defer cancel() ticker := time.NewTicker(opts.PollInterval) defer ticker.Stop() for { select { case <-ctx.Done(): if opts.ThrowOnError { return fmt.Errorf("timeout waiting for text '%s' in element: %s", text, selector) } return nil case <-ticker.C: script := fmt.Sprintf(` (() => { const el = document.querySelector('%s'); return el && el.textContent.includes('%s'); })() `, selector, text) result, err := b.Evaluate(ctx, script) if err != nil { continue } if matches, ok := result.Value.(bool); ok && matches { return nil } } } } // WaitUntilAttributeContains espera a que un atributo contenga un valor func (b *Browser) WaitUntilAttributeContains(ctx context.Context, selector, attribute, value string, opts *WaitOptions) error { if opts == nil { opts = DefaultWaitOptions() } ctx, cancel := context.WithTimeout(ctx, opts.Timeout) defer cancel() ticker := time.NewTicker(opts.PollInterval) defer ticker.Stop() for { select { case <-ctx.Done(): if opts.ThrowOnError { return fmt.Errorf("timeout waiting for attribute '%s' to contain '%s' in element: %s", attribute, value, selector) } return nil case <-ticker.C: script := fmt.Sprintf(` (() => { const el = document.querySelector('%s'); if (!el) return false; const attrValue = el.getAttribute('%s'); return attrValue && attrValue.includes('%s'); })() `, selector, attribute, value) result, err := b.Evaluate(ctx, script) if err != nil { continue } if contains, ok := result.Value.(bool); ok && contains { return nil } } } } // WaitUntilURLContains espera a que la URL contenga un patrón func (b *Browser) WaitUntilURLContains(ctx context.Context, pattern string, opts *WaitOptions) error { if opts == nil { opts = DefaultWaitOptions() } ctx, cancel := context.WithTimeout(ctx, opts.Timeout) defer cancel() ticker := time.NewTicker(opts.PollInterval) defer ticker.Stop() for { select { case <-ctx.Done(): if opts.ThrowOnError { return fmt.Errorf("timeout waiting for URL to contain: %s", pattern) } return nil case <-ticker.C: script := fmt.Sprintf(`window.location.href.includes('%s')`, pattern) result, err := b.Evaluate(ctx, script) if err != nil { continue } if contains, ok := result.Value.(bool); ok && contains { return nil } } } } // WaitUntilTitleContains espera a que el título contenga un patrón func (b *Browser) WaitUntilTitleContains(ctx context.Context, pattern string, opts *WaitOptions) error { if opts == nil { opts = DefaultWaitOptions() } ctx, cancel := context.WithTimeout(ctx, opts.Timeout) defer cancel() ticker := time.NewTicker(opts.PollInterval) defer ticker.Stop() for { select { case <-ctx.Done(): if opts.ThrowOnError { return fmt.Errorf("timeout waiting for title to contain: %s", pattern) } return nil case <-ticker.C: script := fmt.Sprintf(`document.title.includes('%s')`, pattern) result, err := b.Evaluate(ctx, script) if err != nil { continue } if contains, ok := result.Value.(bool); ok && contains { return nil } } } } // WaitUntilSelected espera a que un checkbox/radio esté seleccionado func (b *Browser) WaitUntilSelected(ctx context.Context, selector string, opts *WaitOptions) error { if opts == nil { opts = DefaultWaitOptions() } ctx, cancel := context.WithTimeout(ctx, opts.Timeout) defer cancel() ticker := time.NewTicker(opts.PollInterval) defer ticker.Stop() for { select { case <-ctx.Done(): if opts.ThrowOnError { return fmt.Errorf("timeout waiting for element to be selected: %s", selector) } return nil case <-ticker.C: script := fmt.Sprintf(` (() => { const el = document.querySelector('%s'); return el && el.checked === true; })() `, selector) result, err := b.Evaluate(ctx, script) if err != nil { continue } if selected, ok := result.Value.(bool); ok && selected { return nil } } } }