nixos/shared/linked-dotfiles/opencode/skills/browser-automation/references/advanced.md
2025-10-29 18:46:16 -06:00

679 lines
15 KiB
Markdown

# Advanced Chrome DevTools Protocol Techniques
Advanced patterns for complex browser automation scenarios.
## Network Interception
### Monitor Network Requests
```json
// Get all network requests via Performance API
{action: "eval", payload: `
performance.getEntriesByType('resource').map(r => ({
name: r.name,
type: r.initiatorType,
duration: r.duration,
size: r.transferSize
}))
`}
```
### Wait for Specific Request
```json
// Wait for API call to complete
{action: "eval", payload: `
new Promise(resolve => {
const check = () => {
const apiCall = performance.getEntriesByType('resource')
.find(r => r.name.includes('/api/data'));
if (apiCall) {
resolve(apiCall);
} else {
setTimeout(check, 100);
}
};
check();
})
`}
```
### Check Response Status
```json
// Fetch API to check endpoint
{action: "eval", payload: `
fetch('https://api.example.com/status')
.then(r => ({ status: r.status, ok: r.ok }))
`}
```
---
## JavaScript Injection
### Add Helper Functions
```json
// Inject utility functions into page
{action: "eval", payload: `
window.waitForElement = (selector, timeout = 5000) => {
return new Promise((resolve, reject) => {
const startTime = Date.now();
const check = () => {
const elem = document.querySelector(selector);
if (elem) {
resolve(elem);
} else if (Date.now() - startTime > timeout) {
reject(new Error('Timeout'));
} else {
setTimeout(check, 100);
}
};
check();
});
};
'Helper injected'
`}
// Use injected helper
{action: "eval", payload: "window.waitForElement('.lazy-content')"}
```
### Modify Page Behavior
```json
// Disable animations for faster testing
{action: "eval", payload: `
const style = document.createElement('style');
style.textContent = '* { animation: none !important; transition: none !important; }';
document.head.appendChild(style);
'Animations disabled'
`}
// Override fetch to log requests
{action: "eval", payload: `
const originalFetch = window.fetch;
window.fetch = function(...args) {
console.log('Fetch:', args[0]);
return originalFetch.apply(this, arguments);
};
'Fetch override installed'
`}
```
---
## Complex Waiting Patterns
### Wait for Multiple Conditions
```json
{action: "eval", payload: `
Promise.all([
new Promise(r => {
const check = () => document.querySelector('.element1') ? r() : setTimeout(check, 100);
check();
}),
new Promise(r => {
const check = () => document.querySelector('.element2') ? r() : setTimeout(check, 100);
check();
})
])
`}
```
### Wait with Mutation Observer
```json
{action: "eval", payload: `
new Promise(resolve => {
const observer = new MutationObserver((mutations) => {
const target = document.querySelector('.dynamic-content');
if (target && target.textContent.trim() !== '') {
observer.disconnect();
resolve(target.textContent);
}
});
observer.observe(document.body, {
childList: true,
subtree: true,
characterData: true
});
})
`}
```
### Wait for Idle State
```json
// Wait for network idle
{action: "eval", payload: `
new Promise(resolve => {
let lastActivity = Date.now();
// Monitor network activity
const originalFetch = window.fetch;
window.fetch = function(...args) {
lastActivity = Date.now();
return originalFetch.apply(this, arguments);
};
// Check if idle for 500ms
const checkIdle = () => {
if (Date.now() - lastActivity > 500) {
window.fetch = originalFetch;
resolve('idle');
} else {
setTimeout(checkIdle, 100);
}
};
setTimeout(checkIdle, 100);
})
`}
```
---
## Data Manipulation
### Parse and Transform Table
```json
{action: "eval", payload: `
(() => {
const table = document.querySelector('table');
const headers = Array.from(table.querySelectorAll('thead th'))
.map(th => th.textContent.trim());
const rows = Array.from(table.querySelectorAll('tbody tr'))
.map(tr => {
const cells = Array.from(tr.cells).map(td => td.textContent.trim());
return Object.fromEntries(headers.map((h, i) => [h, cells[i]]));
});
// Filter and transform
return rows
.filter(row => parseFloat(row['Price'].replace('$', '')) > 100)
.map(row => ({
...row,
priceNum: parseFloat(row['Price'].replace('$', ''))
}))
.sort((a, b) => b.priceNum - a.priceNum);
})()
`}
```
### Extract Nested JSON from Script Tags
```json
{action: "eval", payload: `
(() => {
const scripts = Array.from(document.querySelectorAll('script[type="application/ld+json"]'));
return scripts.map(s => JSON.parse(s.textContent));
})()
`}
```
### Aggregate Multiple Elements
```json
{action: "eval", payload: `
(() => {
const sections = Array.from(document.querySelectorAll('section.category'));
return sections.map(section => ({
category: section.querySelector('h2').textContent,
items: Array.from(section.querySelectorAll('.item')).map(item => ({
name: item.querySelector('.name').textContent,
price: item.querySelector('.price').textContent,
inStock: !item.querySelector('.out-of-stock')
})),
total: section.querySelectorAll('.item').length
}));
})()
`}
```
---
## State Management
### Save and Restore Form State
```json
// Save form state
{action: "eval", payload: `
(() => {
const form = document.querySelector('form');
const data = {};
new FormData(form).forEach((value, key) => data[key] = value);
localStorage.setItem('formBackup', JSON.stringify(data));
return data;
})()
`}
// Restore form state
{action: "eval", payload: `
(() => {
const data = JSON.parse(localStorage.getItem('formBackup'));
const form = document.querySelector('form');
Object.entries(data).forEach(([name, value]) => {
const input = form.querySelector(\`[name="\${name}"]\`);
if (input) input.value = value;
});
return 'Form restored';
})()
`}
```
### Session Management
```json
// Save session state
{action: "eval", payload: `
({
cookies: document.cookie,
localStorage: JSON.stringify(localStorage),
sessionStorage: JSON.stringify(sessionStorage),
url: window.location.href
})
`}
// Restore session (on new page load)
{action: "eval", payload: `
(() => {
const session = {/* saved session data */};
// Restore cookies
session.cookies.split('; ').forEach(cookie => {
document.cookie = cookie;
});
// Restore localStorage
Object.entries(JSON.parse(session.localStorage)).forEach(([k, v]) => {
localStorage.setItem(k, v);
});
return 'Session restored';
})()
`}
```
---
## Visual Testing
### Check Element Visibility
```json
{action: "eval", payload: `
(selector) => {
const elem = document.querySelector(selector);
if (!elem) return { visible: false, reason: 'not found' };
const rect = elem.getBoundingClientRect();
const style = window.getComputedStyle(elem);
return {
visible: rect.width > 0 && rect.height > 0 && style.display !== 'none' && style.visibility !== 'hidden' && style.opacity !== '0',
rect: rect,
computed: {
display: style.display,
visibility: style.visibility,
opacity: style.opacity
}
};
}
`}
```
### Get Element Colors
```json
{action: "eval", payload: `
(() => {
const elem = document.querySelector('.button');
const style = window.getComputedStyle(elem);
return {
backgroundColor: style.backgroundColor,
color: style.color,
borderColor: style.borderColor
};
})()
`}
```
### Measure Element Positions
```json
{action: "eval", payload: `
(() => {
const elements = Array.from(document.querySelectorAll('.item'));
return elements.map(elem => {
const rect = elem.getBoundingClientRect();
return {
id: elem.id,
x: rect.x,
y: rect.y,
width: rect.width,
height: rect.height,
inViewport: rect.top >= 0 && rect.bottom <= window.innerHeight
};
});
})()
`}
```
---
## Performance Monitoring
### Get Page Load Metrics
```json
{action: "eval", payload: `
(() => {
const nav = performance.getEntriesByType('navigation')[0];
const paint = performance.getEntriesByType('paint');
return {
dns: nav.domainLookupEnd - nav.domainLookupStart,
tcp: nav.connectEnd - nav.connectStart,
request: nav.responseStart - nav.requestStart,
response: nav.responseEnd - nav.responseStart,
domLoad: nav.domContentLoadedEventEnd - nav.domContentLoadedEventStart,
pageLoad: nav.loadEventEnd - nav.loadEventStart,
firstPaint: paint.find(p => p.name === 'first-paint')?.startTime,
firstContentfulPaint: paint.find(p => p.name === 'first-contentful-paint')?.startTime
};
})()
`}
```
### Monitor Memory Usage
```json
{action: "eval", payload: `
performance.memory ? {
usedJSHeapSize: performance.memory.usedJSHeapSize,
totalJSHeapSize: performance.memory.totalJSHeapSize,
jsHeapSizeLimit: performance.memory.jsHeapSizeLimit
} : 'Memory API not available'
`}
```
### Get Resource Timing
```json
{action: "eval", payload: `
(() => {
const resources = performance.getEntriesByType('resource');
// Group by type
const byType = {};
resources.forEach(r => {
if (!byType[r.initiatorType]) byType[r.initiatorType] = [];
byType[r.initiatorType].push({
name: r.name,
duration: r.duration,
size: r.transferSize
});
});
return {
total: resources.length,
byType: Object.fromEntries(
Object.entries(byType).map(([type, items]) => [
type,
{
count: items.length,
totalDuration: items.reduce((sum, i) => sum + i.duration, 0),
totalSize: items.reduce((sum, i) => sum + i.size, 0)
}
])
)
};
})()
`}
```
---
## Accessibility Testing
### Check ARIA Labels
```json
{action: "eval", payload: `
Array.from(document.querySelectorAll('button, a, input')).map(elem => ({
tag: elem.tagName,
text: elem.textContent.trim(),
ariaLabel: elem.getAttribute('aria-label'),
ariaDescribedBy: elem.getAttribute('aria-describedby'),
title: elem.getAttribute('title'),
hasAccessibleName: !!(elem.getAttribute('aria-label') || elem.textContent.trim() || elem.getAttribute('title'))
}))
`}
```
### Find Focus Order
```json
{action: "eval", payload: `
Array.from(document.querySelectorAll('a, button, input, select, textarea, [tabindex]'))
.filter(elem => {
const style = window.getComputedStyle(elem);
return style.display !== 'none' && style.visibility !== 'hidden';
})
.map((elem, index) => ({
index: index,
tag: elem.tagName,
tabIndex: elem.tabIndex,
text: elem.textContent.trim().substring(0, 50)
}))
`}
```
---
## Frame Handling
### List Frames
```json
{action: "eval", payload: `
Array.from(document.querySelectorAll('iframe, frame')).map((frame, i) => ({
index: i,
src: frame.src,
name: frame.name,
id: frame.id
}))
`}
```
### Access Frame Content
Note: Cross-origin frames cannot be accessed due to security restrictions.
```json
// For same-origin frames only
{action: "eval", payload: `
(() => {
const frame = document.querySelector('iframe');
try {
return {
title: frame.contentDocument.title,
body: frame.contentDocument.body.textContent.substring(0, 100)
};
} catch (e) {
return { error: 'Cross-origin frame - cannot access' };
}
})()
`}
```
---
## Custom Events
### Trigger Custom Events
```json
{action: "eval", payload: `
(() => {
const event = new CustomEvent('myCustomEvent', {
detail: { message: 'Hello from automation' }
});
document.dispatchEvent(event);
return 'Event dispatched';
})()
`}
```
### Listen for Events
```json
{action: "eval", payload: `
new Promise(resolve => {
const handler = (e) => {
document.removeEventListener('myCustomEvent', handler);
resolve(e.detail);
};
document.addEventListener('myCustomEvent', handler);
// Timeout after 5 seconds
setTimeout(() => {
document.removeEventListener('myCustomEvent', handler);
resolve({ timeout: true });
}, 5000);
})
`}
```
---
## Browser Detection
### Get Browser Info
```json
{action: "eval", payload: `
({
userAgent: navigator.userAgent,
platform: navigator.platform,
language: navigator.language,
cookiesEnabled: navigator.cookieEnabled,
doNotTrack: navigator.doNotTrack,
viewport: {
width: window.innerWidth,
height: window.innerHeight
},
screen: {
width: screen.width,
height: screen.height,
colorDepth: screen.colorDepth
}
})
`}
```
---
## Testing Helpers
### Get All Interactive Elements
```json
{action: "eval", payload: `
Array.from(document.querySelectorAll('a, button, input, select, textarea, [onclick], [role=button]'))
.filter(elem => {
const style = window.getComputedStyle(elem);
return style.display !== 'none' && style.visibility !== 'hidden';
})
.map(elem => ({
tag: elem.tagName,
type: elem.type,
id: elem.id,
class: elem.className,
text: elem.textContent.trim().substring(0, 50),
selector: elem.id ? \`#\${elem.id}\` : \`\${elem.tagName.toLowerCase()}\${elem.className ? '.' + elem.className.split(' ').join('.') : ''}\`
}))
`}
```
### Validate Forms
```json
{action: "eval", payload: `
(() => {
const forms = Array.from(document.querySelectorAll('form'));
return forms.map(form => ({
id: form.id,
action: form.action,
method: form.method,
fields: Array.from(form.elements).map(elem => ({
name: elem.name,
type: elem.type,
required: elem.required,
value: elem.value,
valid: elem.checkValidity()
}))
}));
})()
`}
```
---
## Debugging Tools
### Log Element Path
```json
{action: "eval", payload: `
(selector) => {
const elem = document.querySelector(selector);
if (!elem) return null;
const path = [];
let current = elem;
while (current && current !== document.body) {
let selector = current.tagName.toLowerCase();
if (current.id) selector += '#' + current.id;
if (current.className) selector += '.' + current.className.split(' ').join('.');
path.unshift(selector);
current = current.parentElement;
}
return path.join(' > ');
}
`}
```
### Find Element by Text
```json
{action: "eval", payload: `
(text) => {
const elements = Array.from(document.querySelectorAll('*'));
const matches = elements.filter(elem =>
elem.textContent.includes(text) &&
!Array.from(elem.children).some(child => child.textContent.includes(text))
);
return matches.map(elem => ({
tag: elem.tagName,
id: elem.id,
class: elem.className,
text: elem.textContent.trim().substring(0, 100)
}));
}
`}
```