feat: expected conditions mejoradas

Implementa condiciones de espera específicas similares a Selenium.

Incluye:
- WaitUntilVisible() y WaitUntilHidden()
- WaitUntilClickable() y WaitUntilEnabled()
- WaitUntilDisabled() y WaitUntilSelected()
- WaitUntilTextMatches() y WaitUntilAttributeContains()
- WaitUntilURLContains() y WaitUntilTitleContains()

Todas con polling configurable y opciones de timeout.
WaitOptions con Timeout, PollInterval y ThrowOnError.

Archivo: pkg/browser/expected_conditions.go
This commit is contained in:
Developer
2026-03-25 00:48:40 +01:00
parent 6de1b08aa3
commit 3a0250f7fb
+438
View File
@@ -0,0 +1,438 @@
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
}
}
}
}