@saulwade/swl-ses 1.6.3 → 1.6.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +3 -3
- package/README.md +2 -2
- package/agentes/gh-fix-ci-swl.md +275 -0
- package/agentes/nemesis-auditor-swl.md +90 -1
- package/comandos/swl/exportar-vault.md +106 -14
- package/comandos/swl/nemesis.md +70 -3
- package/comandos/swl/release.md +62 -2
- package/comandos/swl/salud.md +32 -0
- package/comandos/swl/verificar.md +116 -2
- package/habilidades/agent-browser/SKILL.md +111 -4
- package/habilidades/agent-deep-links/SKILL.md +148 -0
- package/habilidades/backend-async-postgres-testing/SKILL.md +215 -0
- package/habilidades/backend-error-design/SKILL.md +221 -0
- package/habilidades/browser-interaction-patterns/SKILL.md +514 -0
- package/habilidades/browser-research-domains/SKILL.md +635 -0
- package/habilidades/changelog-generator/SKILL.md +172 -0
- package/habilidades/changelog-generator/scripts/parse-commits.js +354 -0
- package/habilidades/devsecops-pipeline-security/SKILL.md +3 -0
- package/habilidades/fastapi-experto/SKILL.md +49 -4
- package/habilidades/harness-claude-code/SKILL.md +4 -1
- package/habilidades/postgresql-experto/SKILL.md +80 -4
- package/habilidades/proceso-discovery-machote/SKILL.md +157 -0
- package/habilidades/proceso-modular-split/SKILL.md +256 -0
- package/habilidades/tdd-workflow/SKILL.md +12 -5
- package/hooks/extraccion-aprendizajes.js +8 -0
- package/hooks/lib/deep-links.js +185 -0
- package/hooks/lib/evolution-tracker.js +115 -18
- package/hooks/lib/gateway-notify.js +70 -7
- package/manifiestos/modulos.json +13 -3
- package/manifiestos/skills-lock.json +1247 -1191
- package/package.json +3 -3
- package/plugin.json +11 -2
- package/reglas/arquitectura.md +38 -0
- package/reglas/arreglar-al-detectar.md +93 -0
- package/reglas/auditorias-documentales-estructurales.md +38 -0
- package/reglas/registro-componentes-nuevos.md +14 -0
- package/reglas/tests-cleanup.md +220 -0
- package/scripts/lib/mcp_config.py +29 -14
- package/scripts/mcp-orchestrator.py +153 -131
- package/scripts/mcp-pool-manager.py +132 -107
- package/scripts/mcp-telemetry.py +139 -120
- package/scripts/verificar-release.js +199 -1
|
@@ -0,0 +1,514 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: browser-interaction-patterns
|
|
3
|
+
description: >
|
|
4
|
+
Patrones canónicos para automatización de browser via CDP (Chrome DevTools Protocol):
|
|
5
|
+
dialogs nativos, iframes y shadow DOM, dropdowns, downloads y uploads, screenshots,
|
|
6
|
+
scrolling, cookies, network requests, tabs y viewport. Cargar cuando una sesión
|
|
7
|
+
de browser automation tropieza con mecánica específica de UI (frame stuck, dropdown
|
|
8
|
+
invisible, dialog colgando JS, upload sin progresar) y agent-browser no resuelve
|
|
9
|
+
por defecto. Complementa a agent-browser con patrones de bajo nivel CDP.
|
|
10
|
+
version: "1.0.0"
|
|
11
|
+
herramientasPermitidas: [Read, Bash, WebFetch]
|
|
12
|
+
evolved: false
|
|
13
|
+
fuente: "browser-use/browser-harness — interaction-skills (MIT License, 2026)"
|
|
14
|
+
evolvable: true
|
|
15
|
+
exclusiones:
|
|
16
|
+
- "No cargar para scraping de páginas estáticas sin interacción — usar WebFetch o web-fetcher-routing."
|
|
17
|
+
- "No cargar para research técnico de research-domains (github, arxiv, etc.) — esos viven en browser-research-domains."
|
|
18
|
+
- "No cargar para automatización Playwright/Selenium tradicional — este skill asume CDP directo o agent-browser CLI."
|
|
19
|
+
- "No cargar como reemplazo de agent-browser — es complemento; siempre cargar primero agent-browser y solo escalar aquí ante tropiezos específicos."
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
# Skill: browser-interaction-patterns
|
|
23
|
+
|
|
24
|
+
Patrones de browser automation a nivel CDP para resolver tropiezos comunes que
|
|
25
|
+
`agent-browser` por sí solo no documenta. Cada sección entrega: anti-patrón
|
|
26
|
+
observable, regla canónica, ejemplo concreto y gotcha clave.
|
|
27
|
+
|
|
28
|
+
Adaptado de `browser-use/browser-harness` (MIT License) — los patrones son
|
|
29
|
+
agnósticos al runtime CLI específico (`browser-harness` o `agent-browser`).
|
|
30
|
+
|
|
31
|
+
## Cuándo cargar
|
|
32
|
+
|
|
33
|
+
- El agente tropieza con un dialog `alert/confirm/prompt` que congela la página.
|
|
34
|
+
- Una página tiene iframe cross-origin o shadow DOM y el selector falla.
|
|
35
|
+
- Un dropdown nativo `<select>` no responde a click sintético.
|
|
36
|
+
- Una descarga arranca pero no hay forma de verificar que terminó.
|
|
37
|
+
- Un upload con `<input type="file">` no acepta el archivo.
|
|
38
|
+
- El agente abre una pestaña y todas las acciones siguientes ocurren "invisibles" al usuario.
|
|
39
|
+
- Una SPA cambia de ruta y el agente actúa antes de que termine la hidratación.
|
|
40
|
+
|
|
41
|
+
## Cuándo NO cargar
|
|
42
|
+
|
|
43
|
+
- Páginas estáticas HTML sin interacción → `WebFetch` o `web-fetcher-routing`.
|
|
44
|
+
- Tareas de research técnico contra APIs documentadas → `browser-research-domains`.
|
|
45
|
+
- Tests E2E con framework dedicado (Playwright, Cypress, Detox) → skill correspondiente.
|
|
46
|
+
|
|
47
|
+
## Regla universal — verificar con screenshot
|
|
48
|
+
|
|
49
|
+
Tras cualquier acción que modifique estado (click, navigation, form submit,
|
|
50
|
+
upload, dialog dismiss), **re-capturar screenshot antes de asumir éxito**.
|
|
51
|
+
La verificación visual es la única señal confiable de que el compositor de Chrome
|
|
52
|
+
aplicó el cambio. `page_info()` o assertions del DOM pueden mentir si la página
|
|
53
|
+
está en transición.
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## 1. Dialogs nativos (alert / confirm / prompt / beforeunload)
|
|
58
|
+
|
|
59
|
+
Los dialogs nativos congelan el thread de JS hasta resolverse. Si el agente
|
|
60
|
+
intenta cualquier acción mientras hay dialog abierto, todo cuelga.
|
|
61
|
+
|
|
62
|
+
### Detección — `page_info()` auto-surface
|
|
63
|
+
|
|
64
|
+
Cuando hay dialog pendiente, `page_info()` retorna `{"dialog": {type, message, ...}}`
|
|
65
|
+
en lugar del viewport dict habitual. Verificar antes de actuar.
|
|
66
|
+
|
|
67
|
+
### Dismiss reactivo via CDP (preferido)
|
|
68
|
+
|
|
69
|
+
Funciona aún con JS frozen. Maneja todos los tipos incluyendo `beforeunload`.
|
|
70
|
+
NO detectable por antibot — no inyecta JS al page.
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
cdp("Page.handleJavaScriptDialog", accept=True) # OK
|
|
74
|
+
cdp("Page.handleJavaScriptDialog", accept=False) # Cancel
|
|
75
|
+
|
|
76
|
+
events = drain_events()
|
|
77
|
+
for e in events:
|
|
78
|
+
if e["method"] == "Page.javascriptDialogOpening":
|
|
79
|
+
print(e["params"]["type"]) # alert/confirm/prompt/beforeunload
|
|
80
|
+
print(e["params"]["message"])
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Stub proactivo via JS (cuando esperas múltiples alerts)
|
|
84
|
+
|
|
85
|
+
Tradeoffs: detectable por antibot, NO maneja `beforeunload`, se pierde al navegar.
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
js("""
|
|
89
|
+
window.__dialogs__=[];
|
|
90
|
+
window.alert=m=>window.__dialogs__.push(String(m));
|
|
91
|
+
window.confirm=m=>{window.__dialogs__.push(String(m));return true;};
|
|
92
|
+
window.prompt=(m,d)=>{window.__dialogs__.push(String(m));return d||'';};
|
|
93
|
+
""")
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
**Gotcha**: `beforeunload` aparece al navegar con cambios sin guardar. Manejar
|
|
97
|
+
SIEMPRE con CDP, nunca con stub JS.
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
## 2. Iframes y cross-origin
|
|
102
|
+
|
|
103
|
+
### Same-origin
|
|
104
|
+
|
|
105
|
+
Acceder via `contentDocument` / `contentWindow` desde el contexto padre.
|
|
106
|
+
Las coordenadas de click son **frame-local**, no de la página completa.
|
|
107
|
+
|
|
108
|
+
### Cross-origin — usar `iframe_target`
|
|
109
|
+
|
|
110
|
+
El iframe tiene su propio target CDP. Attach con `iframe_target(url_substr)` y
|
|
111
|
+
pasar el `target_id` a `js(..., target_id=tid)`.
|
|
112
|
+
|
|
113
|
+
```python
|
|
114
|
+
tid = iframe_target("stripe.com/checkout")
|
|
115
|
+
saldo = js("document.querySelector('.balance').textContent", target_id=tid)
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### Mejor opción para clicks en iframe — coordinate clicks
|
|
119
|
+
|
|
120
|
+
`Input.dispatchMouseEvent` opera a nivel **compositor** de Chrome y pasa por
|
|
121
|
+
iframes cross-origin sin trabajo extra. Es preferible a navegar el árbol de
|
|
122
|
+
targets cuando el objetivo es visible.
|
|
123
|
+
|
|
124
|
+
```python
|
|
125
|
+
# Click en botón visible dentro de iframe cross-origin
|
|
126
|
+
click_at_xy(x, y) # las coordenadas del screenshot funcionan tal cual
|
|
127
|
+
capture_screenshot() # verificar
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
**Gotcha**: solo bajar a DOM iframe cuando el target NO tiene geometría visible
|
|
131
|
+
(hidden input, 0×0 node).
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
## 3. Shadow DOM
|
|
136
|
+
|
|
137
|
+
DOM oculto en componentes Web. `querySelector` desde el documento root NO
|
|
138
|
+
penetra `shadowRoot`.
|
|
139
|
+
|
|
140
|
+
### Traversal recursivo cuando hay que penetrar
|
|
141
|
+
|
|
142
|
+
```javascript
|
|
143
|
+
function deepQuerySelector(root, selector) {
|
|
144
|
+
const found = root.querySelector(selector);
|
|
145
|
+
if (found) return found;
|
|
146
|
+
for (const el of root.querySelectorAll('*')) {
|
|
147
|
+
if (el.shadowRoot) {
|
|
148
|
+
const inShadow = deepQuerySelector(el.shadowRoot, selector);
|
|
149
|
+
if (inShadow) return inShadow;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### Mejor opción — coordinate clicks
|
|
157
|
+
|
|
158
|
+
Si el elemento es visible, `click_at_xy(x, y)` cruza shadow DOM sin necesidad
|
|
159
|
+
de penetrar. Mismo principio que iframes cross-origin: operar a nivel
|
|
160
|
+
compositor evita la complejidad de los árboles anidados.
|
|
161
|
+
|
|
162
|
+
**Gotcha**: componentes Polymer/Lit/Stencil que componen 5+ shadow roots
|
|
163
|
+
anidados son frágiles para selectors. Default a screenshots + coordinate clicks.
|
|
164
|
+
|
|
165
|
+
---
|
|
166
|
+
|
|
167
|
+
## 4. Dropdowns
|
|
168
|
+
|
|
169
|
+
Cuatro variantes con tratamiento distinto:
|
|
170
|
+
|
|
171
|
+
### Native `<select>`
|
|
172
|
+
|
|
173
|
+
NO responde a `click` sintético. Usar:
|
|
174
|
+
|
|
175
|
+
```python
|
|
176
|
+
js("""
|
|
177
|
+
const sel = document.querySelector('select#country');
|
|
178
|
+
sel.value = 'MX';
|
|
179
|
+
sel.dispatchEvent(new Event('change', {bubbles:true}));
|
|
180
|
+
""")
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
### Custom overlay (Material, Ant Design, Bootstrap)
|
|
184
|
+
|
|
185
|
+
Render con `position: fixed` o `position: absolute`, geometría aparece **después**
|
|
186
|
+
de click en el trigger. Patrón obligatorio:
|
|
187
|
+
|
|
188
|
+
```python
|
|
189
|
+
click_at_xy(trigger_x, trigger_y)
|
|
190
|
+
wait(0.3) # esperar render del overlay
|
|
191
|
+
capture_screenshot() # re-medir geometría
|
|
192
|
+
# leer la posición de la opción del screenshot nuevo
|
|
193
|
+
click_at_xy(option_x, option_y)
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
### Searchable combobox (React Select, Algolia)
|
|
197
|
+
|
|
198
|
+
Escribir texto en el input filtra opciones. `type_text` puede no disparar el
|
|
199
|
+
filtro si el componente espera eventos `input`/`change` reales. Usar
|
|
200
|
+
`fill_input` que dispara los eventos sintéticos.
|
|
201
|
+
|
|
202
|
+
### Virtualized menu (react-window, AG-Grid)
|
|
203
|
+
|
|
204
|
+
La opción que buscas puede no estar en el DOM hasta hacer scroll dentro del
|
|
205
|
+
overlay. Hacer `scroll(x, y, dy=N)` sobre las coordenadas del overlay.
|
|
206
|
+
|
|
207
|
+
---
|
|
208
|
+
|
|
209
|
+
## 5. Downloads
|
|
210
|
+
|
|
211
|
+
Dos modos: browser-triggered (con CDP) vs HTTP directo.
|
|
212
|
+
|
|
213
|
+
### HTTP directo cuando se conoce la URL del archivo
|
|
214
|
+
|
|
215
|
+
```python
|
|
216
|
+
# Para PDFs, CSVs, ZIP con URL conocida — más rápido y observable
|
|
217
|
+
import urllib.request
|
|
218
|
+
urllib.request.urlretrieve(url, "/tmp/file.pdf")
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
### Browser-triggered (click en "Download")
|
|
222
|
+
|
|
223
|
+
```python
|
|
224
|
+
cdp("Page.setDownloadBehavior", behavior="allow", downloadPath="/tmp/dl")
|
|
225
|
+
click_at_xy(download_btn_x, download_btn_y)
|
|
226
|
+
# Verificar archivo aparece en /tmp/dl/ con tamaño > 0
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
**Gotcha**: muchos sites generan el archivo on-the-fly (POST + redirect a S3
|
|
230
|
+
con presigned URL). El `setDownloadBehavior` captura, pero la verificación es
|
|
231
|
+
por filesystem polling, no por DOM signal.
|
|
232
|
+
|
|
233
|
+
---
|
|
234
|
+
|
|
235
|
+
## 6. Uploads
|
|
236
|
+
|
|
237
|
+
`<input type="file">` rechaza interacción sintética por sandbox del browser.
|
|
238
|
+
Usar CDP `DOM.setFileInputFiles`:
|
|
239
|
+
|
|
240
|
+
```python
|
|
241
|
+
upload_file("input[type=file]", "/abs/path/file.pdf")
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
### Drag-and-drop como alternativa
|
|
245
|
+
|
|
246
|
+
Si el site solo acepta drop (no input file visible), construir eventos
|
|
247
|
+
`DragEvent` con `DataTransfer` poblada. Si el site usa un componente complejo
|
|
248
|
+
(Filepond, Dropzone), preferir descubrir el `<input>` oculto (siempre existe
|
|
249
|
+
para fallback A11y) y usar `setFileInputFiles` sobre ese.
|
|
250
|
+
|
|
251
|
+
**Gotcha**: path DEBE ser absoluto. Path relativo silently falla sin error
|
|
252
|
+
visible.
|
|
253
|
+
|
|
254
|
+
---
|
|
255
|
+
|
|
256
|
+
## 7. Screenshots
|
|
257
|
+
|
|
258
|
+
`capture_screenshot()` escribe PNG del viewport actual. El archivo está en
|
|
259
|
+
**device pixels**: en una pantalla 2× un viewport 2296×1143 CSS produce PNG
|
|
260
|
+
4592×2286.
|
|
261
|
+
|
|
262
|
+
### Coordenadas
|
|
263
|
+
|
|
264
|
+
Click coordinates son **CSS pixels**. NO leas posición del PNG y pases directo
|
|
265
|
+
a `click_at_xy()` sin dividir por `devicePixelRatio`:
|
|
266
|
+
|
|
267
|
+
```python
|
|
268
|
+
dpr = js("window.devicePixelRatio") or 1
|
|
269
|
+
# Si lees (px, py) del PNG, convertir:
|
|
270
|
+
click_at_xy(px / dpr, py / dpr)
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
### Límite 2000 px para LLMs vision
|
|
274
|
+
|
|
275
|
+
Algunos modelos rechazan imágenes > 2000 px/lado. Pasar `max_dim=1800` para
|
|
276
|
+
downscale automático:
|
|
277
|
+
|
|
278
|
+
```python
|
|
279
|
+
capture_screenshot("/tmp/shot.png", max_dim=1800)
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
Solo downscalea si excede; seguro dejar siempre activo.
|
|
283
|
+
|
|
284
|
+
### Full-page vs viewport
|
|
285
|
+
|
|
286
|
+
`full=True` captura todo el documento (puede ser MB). Usar solo cuando
|
|
287
|
+
necesites contenido bajo el fold; viewport es mucho más rápido.
|
|
288
|
+
|
|
289
|
+
---
|
|
290
|
+
|
|
291
|
+
## 8. Scrolling
|
|
292
|
+
|
|
293
|
+
Identificar qué elemento consume wheel events ANTES de hacer scroll.
|
|
294
|
+
|
|
295
|
+
| Caso | Comando |
|
|
296
|
+
|------|---------|
|
|
297
|
+
| Scroll de page | `scroll(x, y, dy=-300)` con coordenadas dentro del viewport principal |
|
|
298
|
+
| Scroll de contenedor anidado (div con overflow) | `scroll(x, y, ...)` con coordenadas dentro del contenedor |
|
|
299
|
+
| Scroll de virtualized list (react-window) | Igual que contenedor; pero verificar con screenshot que la lista realmente avanzó (puede tener su propio scroll handler) |
|
|
300
|
+
| Scroll de dropdown menu | Coordenadas dentro del overlay tras abrirlo |
|
|
301
|
+
|
|
302
|
+
**Gotcha**: `js("window.scrollBy(0, 300)")` funciona en page pero NO en
|
|
303
|
+
contenedores anidados. Para contenedores anidados, usar `Input.dispatchMouseEvent`
|
|
304
|
+
con `type=mouseWheel` (que es lo que hace `scroll(...)`).
|
|
305
|
+
|
|
306
|
+
---
|
|
307
|
+
|
|
308
|
+
## 9. Cookies
|
|
309
|
+
|
|
310
|
+
Cookies son **browser state**, no page state. Acceder via CDP, no via
|
|
311
|
+
`document.cookie` que solo ve cookies del path actual sin `HttpOnly`.
|
|
312
|
+
|
|
313
|
+
```python
|
|
314
|
+
cookies = cdp("Network.getCookies")["cookies"]
|
|
315
|
+
cdp("Network.setCookie", name="session", value="abc", domain=".example.com", path="/")
|
|
316
|
+
cdp("Network.clearBrowserCookies")
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
**Gotcha**: cookies `HttpOnly` y `Secure` NO aparecen en `document.cookie`.
|
|
320
|
+
Para auth tokens, SIEMPRE usar `Network.getCookies` CDP.
|
|
321
|
+
|
|
322
|
+
---
|
|
323
|
+
|
|
324
|
+
## 10. Network requests
|
|
325
|
+
|
|
326
|
+
Cuando una acción no produce cambio DOM visible (form submit, SPA route change,
|
|
327
|
+
SSE keepalive), inferir desde tráfico de red.
|
|
328
|
+
|
|
329
|
+
### Patrón — esperar idle de red post-acción
|
|
330
|
+
|
|
331
|
+
```python
|
|
332
|
+
# El helper canónico — espera N ms sin requests in-flight
|
|
333
|
+
wait_for_network_idle(timeout=10.0, idle_ms=500)
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
Equivalente conceptual usando `drain_events`:
|
|
337
|
+
|
|
338
|
+
```python
|
|
339
|
+
deadline = time.time() + 10
|
|
340
|
+
in_flight = set()
|
|
341
|
+
last_activity = time.time()
|
|
342
|
+
while time.time() < deadline:
|
|
343
|
+
for e in drain_events():
|
|
344
|
+
m = e.get("method", "")
|
|
345
|
+
if m == "Network.requestWillBeSent":
|
|
346
|
+
in_flight.add(e["params"]["requestId"])
|
|
347
|
+
last_activity = time.time()
|
|
348
|
+
elif m in ("Network.loadingFinished", "Network.loadingFailed"):
|
|
349
|
+
in_flight.discard(e["params"]["requestId"])
|
|
350
|
+
last_activity = time.time()
|
|
351
|
+
if not in_flight and (time.time() - last_activity) * 1000 >= 500:
|
|
352
|
+
break
|
|
353
|
+
time.sleep(0.1)
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
**Gotcha**: filtrar eventos por `session_id` activo. Tabs en background
|
|
357
|
+
(polling/SSE) inyectan eventos al buffer global y envenenan el idle check.
|
|
358
|
+
|
|
359
|
+
---
|
|
360
|
+
|
|
361
|
+
## 11. Tabs
|
|
362
|
+
|
|
363
|
+
**Regla**: CDP para control, UI automation para orden visible.
|
|
364
|
+
|
|
365
|
+
### CDP — qué hace bien
|
|
366
|
+
|
|
367
|
+
- Crear tab (`new_tab(url)`)
|
|
368
|
+
- Attach a target conocido
|
|
369
|
+
- Activar tab conocida (`Target.activateTarget`)
|
|
370
|
+
- Capturar screenshot del tab attached incluso si otro está al frente visible
|
|
371
|
+
- Inspeccionar URL/title/viewport
|
|
372
|
+
|
|
373
|
+
### CDP — qué hace MAL
|
|
374
|
+
|
|
375
|
+
- Matchear el orden visible left-to-right del tab strip del usuario
|
|
376
|
+
- Distinguir target real vs omnibox popup sin filtrar URLs
|
|
377
|
+
|
|
378
|
+
### Orden visible
|
|
379
|
+
|
|
380
|
+
- **macOS**: AppleScript (`tell application "Google Chrome" / every tab of front window`)
|
|
381
|
+
- **Linux**: `xdotool`, `wmctrl`, scripting del WM
|
|
382
|
+
- **Windows**: UI Automation framework
|
|
383
|
+
|
|
384
|
+
### Reglas confirmadas
|
|
385
|
+
|
|
386
|
+
- `switch_tab()` NO es suficiente si el usuario espera ver el cambio en Chrome → llamar también `Target.activateTarget`.
|
|
387
|
+
- `list_tabs()` incluye `chrome://newtab/` por default → `include_chrome=False` para solo páginas reales.
|
|
388
|
+
- `chrome://omnibox-popup.top-chrome/` aparece como page target falso → ignorar.
|
|
389
|
+
- Si una page reporta `w=0 h=0`, probablemente attached al target equivocado.
|
|
390
|
+
|
|
391
|
+
---
|
|
392
|
+
|
|
393
|
+
## 12. Viewport
|
|
394
|
+
|
|
395
|
+
Cambios de viewport afectan layout, coordenadas y workflows que dependen de
|
|
396
|
+
geometría estable.
|
|
397
|
+
|
|
398
|
+
```python
|
|
399
|
+
cdp("Emulation.setDeviceMetricsOverride",
|
|
400
|
+
width=1280, height=800, deviceScaleFactor=1, mobile=False)
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
**Gotcha**: tras cambiar viewport, **re-capturar screenshot y re-medir todas
|
|
404
|
+
las coordenadas**. Posiciones de elementos cambian incluso con cambios mínimos
|
|
405
|
+
de ancho. Para tests responsive, capturar 320/375/768/1024/1440 y verificar
|
|
406
|
+
cada uno.
|
|
407
|
+
|
|
408
|
+
---
|
|
409
|
+
|
|
410
|
+
## 13. Print as PDF
|
|
411
|
+
|
|
412
|
+
CDP genera PDF sin necesidad de click en botón de impresión:
|
|
413
|
+
|
|
414
|
+
```python
|
|
415
|
+
result = cdp("Page.printToPDF", printBackground=True, preferCSSPageSize=True)
|
|
416
|
+
with open("/tmp/page.pdf", "wb") as f:
|
|
417
|
+
f.write(base64.b64decode(result["data"]))
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
**Gotcha**: si el site fuerza un botón "Print" custom que abre dialog del
|
|
421
|
+
sistema, el botón debe clickearse PRIMERO y luego CDP captura la página
|
|
422
|
+
renderizada. La regla de oro: `Page.printToPDF` siempre funciona en la página
|
|
423
|
+
visible; el botón solo lo necesitas cuando el site ajusta layout para print
|
|
424
|
+
(`@media print`).
|
|
425
|
+
|
|
426
|
+
---
|
|
427
|
+
|
|
428
|
+
## 14. Connection y tab visibility
|
|
429
|
+
|
|
430
|
+
Al arrancar Chrome fresh, los únicos targets `type: "page"` pueden ser
|
|
431
|
+
`chrome://inspect` y `chrome://omnibox-popup.top-chrome/` (1px invisible). Si
|
|
432
|
+
el daemon attached al omnibox popup, **TODAS** las acciones siguientes —
|
|
433
|
+
incluyendo `new_tab()` y `goto_url()` — ocurren en tabs CDP pero NO visibles
|
|
434
|
+
al usuario.
|
|
435
|
+
|
|
436
|
+
### Sequence de bootstrap
|
|
437
|
+
|
|
438
|
+
```python
|
|
439
|
+
if not daemon_alive():
|
|
440
|
+
cleanup_sockets() # limpiar PIDs/sockets stale
|
|
441
|
+
ensure_daemon()
|
|
442
|
+
|
|
443
|
+
tabs = list_tabs()
|
|
444
|
+
for t in tabs:
|
|
445
|
+
print(t["url"][:60])
|
|
446
|
+
|
|
447
|
+
tab = ensure_real_tab() # attach a tab real, no chrome://
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
### Llevar Chrome al frente
|
|
451
|
+
|
|
452
|
+
```python
|
|
453
|
+
# macOS
|
|
454
|
+
subprocess.run(["osascript", "-e",
|
|
455
|
+
'tell application "Google Chrome" to activate'])
|
|
456
|
+
# Linux con wmctrl
|
|
457
|
+
subprocess.run(["wmctrl", "-a", "Google Chrome"])
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
---
|
|
461
|
+
|
|
462
|
+
## Patrones transversales
|
|
463
|
+
|
|
464
|
+
### Coordinate clicks > selector clicks
|
|
465
|
+
|
|
466
|
+
`Input.dispatchMouseEvent` opera a nivel compositor de Chrome y pasa por
|
|
467
|
+
iframes, shadow DOM y cross-origin sin trabajo extra. Default a coordenadas
|
|
468
|
+
si el elemento es visible; bajar a DOM solo cuando NO hay geometría.
|
|
469
|
+
|
|
470
|
+
### Screenshots primero para verificación
|
|
471
|
+
|
|
472
|
+
Tras cualquier acción que modifica estado, capturar screenshot ANTES de
|
|
473
|
+
asumir éxito. Es la única señal confiable de que el compositor aplicó el
|
|
474
|
+
cambio.
|
|
475
|
+
|
|
476
|
+
### `wait_for_network_idle` > `wait(N)` fijo
|
|
477
|
+
|
|
478
|
+
Esperar event-driven (CDP Network events) en lugar de timeout ciego. Más
|
|
479
|
+
rápido en páginas rápidas, más confiable en lentas.
|
|
480
|
+
|
|
481
|
+
### Re-medir geometría post-acción
|
|
482
|
+
|
|
483
|
+
Tras abrir dropdown / modal / overlay, screenshot + leer coordenadas nuevas.
|
|
484
|
+
NO reutilizar posiciones medidas antes de la acción — la geometría cambió.
|
|
485
|
+
|
|
486
|
+
---
|
|
487
|
+
|
|
488
|
+
## Gotchas que cuestan horas
|
|
489
|
+
|
|
490
|
+
- **`click_at_xy(x, y)` falla sin error** cuando coordenadas caen en zona
|
|
491
|
+
cubierta por dialog/modal/overlay invisible. Verificar con screenshot ANTES.
|
|
492
|
+
- **`document.cookie` no ve `HttpOnly` cookies**. Auth tokens viven ahí —
|
|
493
|
+
usar `Network.getCookies` CDP.
|
|
494
|
+
- **`wait_for_load()` retorna True antes de hidratación React/Vue**. Para SPAs,
|
|
495
|
+
añadir `wait_for_element(selector_clave)` después.
|
|
496
|
+
- **`new_tab()` puede abrir tab que no es la activa**. Llamar
|
|
497
|
+
`Target.activateTarget` para que sea visible al usuario.
|
|
498
|
+
- **Path absoluto OBLIGATORIO** en `setFileInputFiles`. Path relativo falla
|
|
499
|
+
silencioso.
|
|
500
|
+
- **Tabs en background siguen emitiendo Network events** al buffer global.
|
|
501
|
+
Filtrar por `session_id` activo en `wait_for_network_idle`.
|
|
502
|
+
|
|
503
|
+
---
|
|
504
|
+
|
|
505
|
+
## Relación con otras skills
|
|
506
|
+
|
|
507
|
+
- **`agent-browser`**: skill base. Cargar primero. Solo escalar aquí ante
|
|
508
|
+
tropiezos específicos.
|
|
509
|
+
- **`web-fetcher-routing`**: orquesta WebFetch vs agent-browser vs markitdown.
|
|
510
|
+
Si la página es estática, no necesitas este skill.
|
|
511
|
+
- **`browser-research-domains`**: patrones específicos por dominio (github,
|
|
512
|
+
arxiv, etc.) que evitan browser cuando hay API.
|
|
513
|
+
|
|
514
|
+
<!-- Adaptado de browser-use/browser-harness bajo MIT License (browser-use, 2026). -->
|