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:
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user