c165f2f788
Agrega 19 issues técnicas documentando funcionalidades implementadas y pendientes. Issues completadas (movidas a dev/issues/completed/): - 001-conversor-web-markdown.md - 002-accessibility-tree.md - 003-gestion-cookies-perfil.md - 004-gestion-extensiones-chrome.md - 005-eliminar-timeouts-innecesarios.md Issues implementadas: - 006-manejo-tabs-ventanas.md - 016-manejo-iframes.md - 017-actions-api.md - 018-file-uploads.md - 019-expected-conditions-mejoradas.md Issues pendientes (media prioridad): - 007-alert-prompt-confirm-handling.md - 008-screenshot-elementos-especificos.md - 009-pdf-generation.md - 010-device-emulation-completo.md - 011-downloads-handling.md Issues pendientes (baja prioridad / avanzado): - 012-browser-contexts-multi-sesion.md - 013-video-recording.md - 014-network-mocking-avanzado.md - 015-geolocation-permissions.md Incluye también dev/NUEVAS_FUNCIONALIDADES.md con resumen completo. Directorio: dev/
310 lines
8.3 KiB
Markdown
310 lines
8.3 KiB
Markdown
# Issue #008: Screenshot de Elementos Específicos
|
|
|
|
**Tipo**: Enhancement
|
|
**Prioridad**: Media
|
|
**Estado**: Pendiente
|
|
|
|
## Descripción
|
|
|
|
Implementar capacidad de tomar screenshots de elementos específicos de la página en lugar de solo página completa.
|
|
|
|
## Funcionalidad deseada
|
|
|
|
### Operaciones
|
|
- Screenshot de elemento específico por selector CSS
|
|
- Screenshot de región (coordenadas x, y, width, height)
|
|
- Screenshot con padding/margin alrededor del elemento
|
|
- Scroll automático al elemento antes de capturar
|
|
- Esperar a que elemento sea visible antes de capturar
|
|
- Captura de múltiples elementos en batch
|
|
- Captura con o sin sombras CSS
|
|
|
|
## Implementación técnica
|
|
|
|
### Archivo sugerido
|
|
Extender `pkg/browser/navigation.go` o crear `pkg/browser/screenshots.go`
|
|
|
|
### CDP Methods
|
|
- **DOM.getBoxModel** - Obtener dimensiones del elemento
|
|
- **Page.captureScreenshot** - Capturar con clip region
|
|
|
|
### API propuesta
|
|
|
|
```go
|
|
// ScreenshotElementOptions opciones para screenshot de elemento
|
|
type ScreenshotElementOptions struct {
|
|
Format string // "png" o "jpeg" (default: png)
|
|
Quality int // 0-100 para JPEG (default: 80)
|
|
Padding int // Padding en pixels alrededor del elemento
|
|
WaitVisible bool // Esperar a que sea visible (default: true)
|
|
ScrollIntoView bool // Scroll al elemento antes (default: true)
|
|
OmitBackground bool // Fondo transparente (default: false)
|
|
}
|
|
|
|
// DefaultScreenshotElementOptions retorna opciones por defecto
|
|
func DefaultScreenshotElementOptions() *ScreenshotElementOptions
|
|
|
|
// ScreenshotElement toma screenshot de un elemento específico
|
|
func (b *Browser) ScreenshotElement(ctx context.Context, selector string, opts *ScreenshotElementOptions) ([]byte, error)
|
|
|
|
// ScreenshotElementToFile guarda screenshot de elemento a archivo
|
|
func (b *Browser) ScreenshotElementToFile(ctx context.Context, selector string, filepath string, opts *ScreenshotElementOptions) error
|
|
|
|
// ScreenshotRegion toma screenshot de región específica
|
|
func (b *Browser) ScreenshotRegion(ctx context.Context, x, y, width, height int) ([]byte, error)
|
|
|
|
// ScreenshotElements toma screenshots de múltiples elementos
|
|
func (b *Browser) ScreenshotElements(ctx context.Context, selectors []string, opts *ScreenshotElementOptions) (map[string][]byte, error)
|
|
```
|
|
|
|
## Casos de uso
|
|
|
|
### Caso 1: Screenshot de botón específico
|
|
```go
|
|
opts := browser.DefaultScreenshotElementOptions()
|
|
opts.Padding = 10 // 10px de margen
|
|
|
|
screenshot, _ := b.ScreenshotElement(ctx, "#submit-button", opts)
|
|
os.WriteFile("button.png", screenshot, 0644)
|
|
```
|
|
|
|
### Caso 2: Screenshot de cada producto
|
|
```go
|
|
products := []string{
|
|
".product:nth-child(1)",
|
|
".product:nth-child(2)",
|
|
".product:nth-child(3)",
|
|
}
|
|
|
|
screenshots, _ := b.ScreenshotElements(ctx, products, nil)
|
|
for selector, data := range screenshots {
|
|
filename := strings.ReplaceAll(selector, ":", "-") + ".png"
|
|
os.WriteFile(filename, data, 0644)
|
|
}
|
|
```
|
|
|
|
### Caso 3: Screenshot con fondo transparente
|
|
```go
|
|
opts := &browser.ScreenshotElementOptions{
|
|
Format: "png",
|
|
OmitBackground: true, // PNG transparente
|
|
}
|
|
|
|
screenshot, _ := b.ScreenshotElement(ctx, ".icon", opts)
|
|
```
|
|
|
|
### Caso 4: Screenshot de región específica
|
|
```go
|
|
// Capturar área de 300x200 en posición (100, 150)
|
|
screenshot, _ := b.ScreenshotRegion(ctx, 100, 150, 300, 200)
|
|
```
|
|
|
|
## Implementación interna
|
|
|
|
```go
|
|
func (b *Browser) ScreenshotElement(ctx context.Context, selector string, opts *ScreenshotElementOptions) ([]byte, error) {
|
|
if opts == nil {
|
|
opts = DefaultScreenshotElementOptions()
|
|
}
|
|
|
|
// 1. Esperar a que elemento sea visible si se especificó
|
|
if opts.WaitVisible {
|
|
if err := b.WaitForElement(ctx, selector, nil); err != nil {
|
|
return nil, fmt.Errorf("element not visible: %w", err)
|
|
}
|
|
}
|
|
|
|
// 2. Scroll al elemento si se especificó
|
|
if opts.ScrollIntoView {
|
|
script := fmt.Sprintf(`
|
|
document.querySelector('%s').scrollIntoView({
|
|
behavior: 'instant',
|
|
block: 'center'
|
|
})
|
|
`, selector)
|
|
b.Evaluate(ctx, script)
|
|
}
|
|
|
|
// 3. Obtener dimensiones del elemento
|
|
var result struct {
|
|
Model struct {
|
|
Content []float64 `json:"content"` // [x1, y1, x2, y2, x3, y3, x4, y4]
|
|
} `json:"model"`
|
|
}
|
|
|
|
// Primero obtener nodeId
|
|
nodeID, err := b.querySelector(ctx, selector)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Obtener box model
|
|
if err := b.cdpClient.Execute(ctx, "DOM.getBoxModel", map[string]interface{}{
|
|
"nodeId": nodeID,
|
|
}, &result); err != nil {
|
|
return nil, fmt.Errorf("failed to get box model: %w", err)
|
|
}
|
|
|
|
// Calcular clip region
|
|
content := result.Model.Content
|
|
x := content[0]
|
|
y := content[1]
|
|
width := content[4] - content[0]
|
|
height := content[5] - content[1]
|
|
|
|
// Aplicar padding
|
|
if opts.Padding > 0 {
|
|
x -= float64(opts.Padding)
|
|
y -= float64(opts.Padding)
|
|
width += float64(opts.Padding * 2)
|
|
height += float64(opts.Padding * 2)
|
|
}
|
|
|
|
// 4. Capturar screenshot con clip
|
|
params := map[string]interface{}{
|
|
"format": opts.Format,
|
|
"clip": map[string]interface{}{
|
|
"x": x,
|
|
"y": y,
|
|
"width": width,
|
|
"height": height,
|
|
"scale": 1,
|
|
},
|
|
}
|
|
|
|
if opts.OmitBackground {
|
|
params["captureBeyondViewport"] = true
|
|
params["fromSurface"] = true
|
|
}
|
|
|
|
if opts.Format == "jpeg" && opts.Quality > 0 {
|
|
params["quality"] = opts.Quality
|
|
}
|
|
|
|
var screenshotResult struct {
|
|
Data string `json:"data"`
|
|
}
|
|
|
|
if err := b.cdpClient.Execute(ctx, "Page.captureScreenshot", params, &screenshotResult); err != nil {
|
|
return nil, fmt.Errorf("failed to capture screenshot: %w", err)
|
|
}
|
|
|
|
// 5. Decodificar base64
|
|
data, err := base64.StdEncoding.DecodeString(screenshotResult.Data)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to decode screenshot: %w", err)
|
|
}
|
|
|
|
return data, nil
|
|
}
|
|
```
|
|
|
|
## Comandos CDP
|
|
|
|
### Obtener dimensiones del elemento
|
|
```json
|
|
{
|
|
"method": "DOM.getBoxModel",
|
|
"params": {
|
|
"nodeId": 123
|
|
}
|
|
}
|
|
|
|
// Response:
|
|
{
|
|
"model": {
|
|
"content": [x1, y1, x2, y2, x3, y3, x4, y4],
|
|
"padding": [...],
|
|
"border": [...],
|
|
"margin": [...],
|
|
"width": 200,
|
|
"height": 100
|
|
}
|
|
}
|
|
```
|
|
|
|
### Capturar con clip
|
|
```json
|
|
{
|
|
"method": "Page.captureScreenshot",
|
|
"params": {
|
|
"format": "png",
|
|
"clip": {
|
|
"x": 100,
|
|
"y": 200,
|
|
"width": 300,
|
|
"height": 150,
|
|
"scale": 1
|
|
},
|
|
"captureBeyondViewport": true
|
|
}
|
|
}
|
|
```
|
|
|
|
## Casos de uso avanzados
|
|
|
|
### Comparación visual
|
|
```go
|
|
// Capturar antes y después de una acción
|
|
before, _ := b.ScreenshotElement(ctx, "#component", nil)
|
|
|
|
b.Click(ctx, "#toggle-button")
|
|
|
|
after, _ := b.ScreenshotElement(ctx, "#component", nil)
|
|
|
|
// Comparar imágenes
|
|
if !bytes.Equal(before, after) {
|
|
log.Println("El componente cambió visualmente")
|
|
}
|
|
```
|
|
|
|
### Generación de thumbnails
|
|
```go
|
|
opts := &browser.ScreenshotElementOptions{
|
|
Format: "jpeg",
|
|
Quality: 60, // Compresión para thumbnails
|
|
}
|
|
|
|
// Capturar todos los artículos
|
|
articles := []string{".article-1", ".article-2", ".article-3"}
|
|
thumbnails, _ := b.ScreenshotElements(ctx, articles, opts)
|
|
```
|
|
|
|
### Screenshot de elemento fuera de viewport
|
|
```go
|
|
// Elemento muy abajo en la página
|
|
opts := &browser.ScreenshotElementOptions{
|
|
ScrollIntoView: true, // Scroll automático
|
|
WaitVisible: true,
|
|
}
|
|
|
|
screenshot, _ := b.ScreenshotElement(ctx, "#footer-logo", opts)
|
|
```
|
|
|
|
## Mejoras adicionales
|
|
|
|
### Screenshot de elemento con sombra
|
|
```go
|
|
// Incluir box-shadow en captura
|
|
opts.IncludeShadow = true
|
|
```
|
|
|
|
### Screenshot de elemento rotado
|
|
```go
|
|
// Calcular bounding box considerando rotación CSS
|
|
opts.ConsiderTransform = true
|
|
```
|
|
|
|
### Screenshot de SVG específico
|
|
```go
|
|
// Elementos SVG pueden necesitar manejo especial
|
|
screenshot, _ := b.ScreenshotElement(ctx, "svg#chart", opts)
|
|
```
|
|
|
|
## Referencias
|
|
|
|
- CDP DOM.getBoxModel: https://chromedevtools.github.io/devtools-protocol/tot/DOM/#method-getBoxModel
|
|
- CDP Page.captureScreenshot: https://chromedevtools.github.io/devtools-protocol/tot/Page/#method-captureScreenshot
|
|
- Playwright elementHandle.screenshot: https://playwright.dev/docs/api/class-elementhandle#element-handle-screenshot
|
|
- Puppeteer element screenshots: https://pptr.dev/api/puppeteer.elementhandle.screenshot
|