Use when automating browsers with Playwright/Puppeteer, testing Chrome extensions, using Chrome DevTools Protocol (CDP), handling dynamic content/SPAs, or debugging automation issues
Automates browsers using Playwright/Puppeteer for testing Chrome extensions, handling dynamic/SPA content, and debugging automation issues with CDP.
/plugin marketplace add SecurityRonin/ronin-marketplace/plugin install securityronin-automation-skills-plugins-automation-skills@SecurityRonin/ronin-marketplaceThis skill inherits all available tools. When active, it can use any tool Claude has access to.
npm install -D @playwright/test
Pros:
Use when:
npm install puppeteer
Pros:
Use when:
npm install selenium-webdriver
Use when:
Stagehand
npm install @anthropic-ai/stagehand
AI agent that automates web tasks using Claude + CDP.
Use when:
Not suitable for:
Browser-Use
pip install browser-use
Python library for LLM-controlled browser automation.
Use when:
Skyvern
Vision-based web automation using computer vision + LLMs.
Use when:
For Manifest V3 Extensions:
// playwright.config.ts
export default defineConfig({
use: {
headless: false,
args: [
`--disable-extensions-except=${extensionPath}`,
`--load-extension=${extensionPath}`,
],
},
})
Find extension ID via CDP:
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]
Navigate to extension pages:
await page.goto(`chrome-extension://${extensionId}/popup.html`)
await page.goto(`chrome-extension://${extensionId}/options.html`)
await page.goto(`chrome-extension://${extensionId}/sidepanel.html`)
What works:
What doesn't work:
chrome-extension:// URLsnet::ERR_BLOCKED_BY_CLIENTWhy: Cloud platforms block extension URLs for security in shared environments.
Verdict: Use local testing for extension UI testing. Cloud for content script testing only.
const 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
})
Key insight: Some sites load content based on time elapsed, not scroll position.
Testing approach:
// 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
Benefits of detecting time-based loading:
// 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'
})
})
Key insight: Googlebot doesn't scroll - it uses a 12,140px viewport and manipulates IntersectionObserver.
// 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;
}
Pros: No visible scrolling, works with standard IntersectionObserver Cons: Won't work with scroll-event listeners or virtualized lists
Patch IntersectionObserver before page loads to force everything to "intersect":
// 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);
Pros: Elegant, works at the source, no DOM manipulation Cons: Must inject before page JS runs, may break other functionality
Force lazy elements to load by modifying their attributes:
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);
});
}
}
Watch for DOM changes and extract content as it loads:
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 |
Recommendation: Start with IO Override + Tall Viewport for most cases. Use MutationObserver when user scrolling is acceptable.
Problem: Some sites use vanity URLs that differ from internal identifiers.
URL: /user/john-smith
Internal ID: john-smith-a2b3c4d5
Solution: Match by displayed content, not URL:
// 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.zip
const 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
}
}
Creating algorithmic art using p5.js with seeded randomness and interactive parameter exploration. Use this when users request creating art using code, generative art, algorithmic art, flow fields, or particle systems. Create original algorithmic art rather than copying existing artists' work to avoid copyright violations.
Applies Anthropic's official brand colors and typography to any sort of artifact that may benefit from having Anthropic's look-and-feel. Use it when brand colors or style guidelines, visual formatting, or company design standards apply.
Create beautiful visual art in .png and .pdf documents using design philosophy. You should use this skill when the user asks to create a poster, piece of art, design, or other static piece. Create original visual designs, never copying existing artists' work to avoid copyright violations.