Loading...
Loading...
Use when automating browsers with Playwright/Puppeteer, testing Chrome extensions, using Chrome DevTools Protocol (CDP), handling dynamic content/SPAs, or debugging automation issues
npx skill4agent add securityronin/ronin-marketplace browser-automationnpm install -D @playwright/testnpm install puppeteernpm install selenium-webdrivernpm install @anthropic-ai/stagehandpip install browser-use// playwright.config.ts
export default defineConfig({
use: {
headless: false,
args: [
`--disable-extensions-except=${extensionPath}`,
`--load-extension=${extensionPath}`,
],
},
})const client = await context.newCDPSession(page)
const { targetInfos } = await client.send('Target.getTargets')
const extensionTarget = targetInfos.find((target: any) =>
target.type === 'service_worker' &&
target.url.startsWith('chrome-extension://')
)
const extensionId = extensionTarget.url.match(/chrome-extension:\/\/([^\/]+)/)?.[1]await page.goto(`chrome-extension://${extensionId}/popup.html`)
await page.goto(`chrome-extension://${extensionId}/options.html`)
await page.goto(`chrome-extension://${extensionId}/sidepanel.html`)chrome-extension://net::ERR_BLOCKED_BY_CLIENTconst client = await context.newCDPSession(page)
const { targetInfos } = await client.send('Target.getTargets')
const extensions = targetInfos.filter(t => t.type === 'service_worker')
const pages = targetInfos.filter(t => t.type === 'page')
const workers = targetInfos.filter(t => t.type === 'worker')// Attach to extension service worker
const swTarget = await client.send('Target.attachToTarget', {
targetId: extensionTarget.targetId,
flatten: true,
})
// Execute in service worker context
await client.send('Runtime.evaluate', {
expression: `
chrome.storage.local.get(['key']).then(console.log)
`,
awaitPromise: true,
})await client.send('Network.enable')
await client.send('Network.setRequestInterception', {
patterns: [{ urlPattern: '*' }],
})
client.on('Network.requestIntercepted', async (event) => {
await client.send('Network.continueInterceptedRequest', {
interceptionId: event.interceptionId,
headers: { ...event.request.headers, 'X-Custom': 'value' },
})
})await client.send('Runtime.enable')
await client.send('Log.enable')
client.on('Runtime.consoleAPICalled', (event) => {
console.log('Console:', event.args.map(a => a.value))
})
client.on('Runtime.exceptionThrown', (event) => {
console.error('Exception:', event.exceptionDetails)
})// Wait for specific content
await page.waitForSelector('.product-price', { timeout: 10000 })
// Wait for network to be idle
await page.goto(url, { waitUntil: 'networkidle' })
// Wait for custom condition
await page.waitForFunction(() => {
return document.querySelectorAll('.item').length > 10
})// Test 1: Wait with no scroll
await page.goto(url)
await page.waitForTimeout(3000)
const sectionsNoScroll = await page.$$('.section').length
// Test 2: Scroll immediately
await page.goto(url)
await page.evaluate(() => window.scrollTo(0, 5000))
await page.waitForTimeout(500)
const sectionsWithScroll = await page.$$('.section').length
// If same result: site uses time-based loading
// No scroll automation needed - just wait// Force lazy images to load
await page.evaluate(() => {
// Handle data-src → src pattern
document.querySelectorAll('[data-src]').forEach(el => {
if (!el.src) el.src = el.dataset.src
})
// Handle loading="lazy" attribute
document.querySelectorAll('[loading="lazy"]').forEach(el => {
el.loading = 'eager'
})
})// Temporarily expand document for IntersectionObserver
async function triggerLazyLoadViaViewport() {
const originalHeight = document.documentElement.style.height;
const originalOverflow = document.documentElement.style.overflow;
// Googlebot uses 12,140px mobile / 9,307px desktop
document.documentElement.style.height = '20000px';
document.documentElement.style.overflow = 'visible';
// Wait for observers to trigger
await new Promise(r => setTimeout(r, 500));
// Restore
document.documentElement.style.height = originalHeight;
document.documentElement.style.overflow = originalOverflow;
}// Must inject at document_start (before page JS runs)
const script = document.createElement('script');
script.textContent = `
const OriginalIO = window.IntersectionObserver;
window.IntersectionObserver = function(callback, options) {
// Override rootMargin to include everything off-screen
const modifiedOptions = {
...options,
rootMargin: '10000px 10000px 10000px 10000px'
};
return new OriginalIO(callback, modifiedOptions);
};
window.IntersectionObserver.prototype = OriginalIO.prototype;
`;
document.documentElement.prepend(script);function forceLoadLazyContent() {
// Handle data-src → src pattern
document.querySelectorAll('[data-src]').forEach(el => {
if (!el.src) el.src = el.dataset.src;
});
document.querySelectorAll('[data-srcset]').forEach(el => {
if (!el.srcset) el.srcset = el.dataset.srcset;
});
// Handle background images
document.querySelectorAll('[data-background]').forEach(el => {
el.style.backgroundImage = `url(${el.dataset.background})`;
});
// Trigger lazysizes library if present
if (window.lazySizes) {
document.querySelectorAll('.lazyload').forEach(el => {
window.lazySizes.loader.unveil(el);
});
}
}function setupProgressiveExtraction(onNewContent) {
let debounceTimer = null;
const observer = new MutationObserver((mutations) => {
clearTimeout(debounceTimer);
debounceTimer = setTimeout(() => {
const addedNodes = mutations
.flatMap(m => Array.from(m.addedNodes))
.filter(n => n.nodeType === Node.ELEMENT_NODE);
if (addedNodes.length > 0) {
onNewContent(addedNodes);
}
}, 300);
});
observer.observe(document.body, {
childList: true,
subtree: true
});
return () => observer.disconnect();
}| Approach | Scrolling? | Reliability | Complexity |
|---|---|---|---|
| Tall Viewport | No | Medium | Low |
| IO Override | No | Medium | Medium |
| Attribute Manipulation | No | Low | Low |
| MutationObserver | User-initiated | High | Low |
URL: /user/john-smith
Internal ID: john-smith-a2b3c4d5// Strategy 1: Try URL-based ID
const urlId = location.pathname.split('/').pop()
let profile = findById(urlId)
// Strategy 2: Fall back to displayed name
if (!profile) {
const displayedName = document.querySelector('h1')?.textContent?.trim()
profile = findByName(displayedName)
}// playwright.lambdatest.config.ts
const capabilities = {
'LT:Options': {
'username': process.env.LT_USERNAME,
'accessKey': process.env.LT_ACCESS_KEY,
'platformName': 'Windows 10',
'browserName': 'Chrome',
'browserVersion': 'latest',
}
}
export default defineConfig({
projects: [{
name: 'lambdatest',
use: {
connectOptions: {
wsEndpoint: `wss://cdp.lambdatest.com/playwright?capabilities=${encodeURIComponent(JSON.stringify(capabilities))}`,
},
},
}],
})await page.route('**/*', route => {
const type = route.request().resourceType()
if (['image', 'font', 'media'].includes(type)) {
route.abort()
} else {
route.continue()
}
})// Good: Reuse browser, create new contexts
const browser = await chromium.launch()
for (const url of urls) {
const context = await browser.newContext()
const page = await context.newPage()
// ...
await context.close()
}
await browser.close()import pLimit from 'p-limit'
const limit = pLimit(5) // Max 5 concurrent
await Promise.all(
urls.map(url => limit(() => processUrl(url)))
)// Screenshots
await page.screenshot({ path: 'debug.png' })
// Video recording
const context = await browser.newContext({
recordVideo: { dir: 'videos/' }
})await context.tracing.start({ screenshots: true, snapshots: true })
// ... run test
await context.tracing.stop({ path: 'trace.zip' })
// View: npx playwright show-trace trace.zipconst browser = await chromium.launch({
headless: false,
slowMo: 1000,
})
await page.pause() // Opens Playwright Inspector// CSS
await page.locator('.class')
await page.locator('#id')
await page.locator('[data-testid="value"]')
// Text
await page.locator('text="Exact text"')
// Playwright-specific
await page.getByRole('button', { name: 'Submit' })
await page.getByText('Welcome')
await page.getByLabel('Email')// Single element
const text = await page.textContent('.element')
const attr = await page.getAttribute('.element', 'href')
// Multiple elements
const texts = await page.$$eval('.item', els => els.map(e => e.textContent))
// Complex extraction
const data = await page.evaluate(() => {
return Array.from(document.querySelectorAll('.product')).map(el => ({
title: el.querySelector('.title')?.textContent,
price: el.querySelector('.price')?.textContent,
}))
})// Wait for element
await page.waitForSelector('.element', { state: 'visible' })
// Check if in iframe
const frame = page.frame({ url: /example\.com/ })
if (frame) {
await frame.waitForSelector('.element')
}try {
await page.goto(url)
} catch (error) {
if (error.message.includes('Browser closed')) {
browser = await chromium.launch()
// retry
}
}