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