skills/dataflightsolutions/playwright-browser-automation/SKILL.md
Complete browser automation with Playwright. Auto-detects dev servers, writes clean test scripts to /tmp. Test pages, fill forms, take screenshots, check responsive design, validate UX, test login flows, check links, automate any browser task. Use when user wants to test websites, automate browser interactions, validate web functionality, or perform any browser-based testing.
npx skillsauth add aiskillstore/marketplace playwright-browser-automationInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
General-purpose browser automation skill. I write custom Playwright code for any automation task and execute it via the universal executor.
For common tasks, these slash commands are faster:
/screenshot - Take a quick screenshot of a webpage/check-links - Find broken links on a page/test-page - Basic page health check/test-responsive - Test across multiple viewportsFor custom automation beyond these common tasks, I write specialized Playwright code.
IMPORTANT - Path Resolution:
Use ${CLAUDE_PLUGIN_ROOT} for all paths. This resolves to the plugin installation directory.
cd ${CLAUDE_PLUGIN_ROOT} && node -e "require('./lib/helpers').detectDevServers().then(servers => console.log(JSON.stringify(servers, null, 2)))"
Decision tree:
NEVER write test files to plugin directory. Always use /tmp/playwright-test-*.js
Script template:
// /tmp/playwright-test-{descriptive-name}.js
const { chromium } = require('playwright');
const helpers = require('./lib/helpers');
// Parameterized URL (auto-detected or user-provided)
const TARGET_URL = 'http://localhost:3847';
(async () => {
const browser = await chromium.launch({ headless: false, slowMo: 100 });
const page = await browser.newPage();
try {
await page.goto(TARGET_URL, { waitUntil: 'networkidle' });
console.log('Page loaded:', await page.title());
// Test code here...
await page.screenshot({ path: '/tmp/screenshot.png', fullPage: true });
console.log('Screenshot saved to /tmp/screenshot.png');
} catch (error) {
console.error('Test failed:', error.message);
await page.screenshot({ path: '/tmp/error-screenshot.png' });
} finally {
await browser.close();
}
})();
cd ${CLAUDE_PLUGIN_ROOT} && node run.js /tmp/playwright-test-{name}.js
ALWAYS use headless: false unless user explicitly requests headless mode. This lets users see what's happening.
cd ${CLAUDE_PLUGIN_ROOT} && npm run setup
Installs Playwright and Chromium browser. Only needed once.
const { chromium } = require('playwright');
const TARGET_URL = 'http://localhost:3847';
(async () => {
const browser = await chromium.launch({ headless: false });
const page = await browser.newPage();
await page.goto(TARGET_URL);
console.log('Title:', await page.title());
console.log('URL:', page.url());
await page.screenshot({ path: '/tmp/page.png', fullPage: true });
await browser.close();
})();
const { chromium } = require('playwright');
const TARGET_URL = 'http://localhost:3847';
(async () => {
const browser = await chromium.launch({ headless: false });
const page = await browser.newPage();
const viewports = [
{ name: 'Desktop', width: 1920, height: 1080 },
{ name: 'Tablet', width: 768, height: 1024 },
{ name: 'Mobile', width: 375, height: 667 }
];
for (const viewport of viewports) {
await page.setViewportSize({ width: viewport.width, height: viewport.height });
await page.goto(TARGET_URL);
await page.screenshot({ path: `/tmp/${viewport.name.toLowerCase()}.png`, fullPage: true });
console.log(`${viewport.name} screenshot saved`);
}
await browser.close();
})();
const { chromium } = require('playwright');
const TARGET_URL = 'http://localhost:3847';
(async () => {
const browser = await chromium.launch({ headless: false, slowMo: 100 });
const page = await browser.newPage();
await page.goto(`${TARGET_URL}/login`);
await page.fill('input[name="email"]', '[email protected]');
await page.fill('input[name="password"]', 'password123');
await page.click('button[type="submit"]');
await page.waitForURL('**/dashboard');
console.log('Login successful, redirected to dashboard');
await browser.close();
})();
const { chromium } = require('playwright');
const TARGET_URL = 'http://localhost:3847';
(async () => {
const browser = await chromium.launch({ headless: false, slowMo: 50 });
const page = await browser.newPage();
await page.goto(`${TARGET_URL}/contact`);
await page.fill('input[name="name"]', 'John Doe');
await page.fill('input[name="email"]', '[email protected]');
await page.fill('textarea[name="message"]', 'Test message');
await page.click('button[type="submit"]');
await page.waitForSelector('.success-message');
console.log('Form submitted successfully');
await browser.close();
})();
const { chromium } = require('playwright');
const TARGET_URL = 'http://localhost:3847';
(async () => {
const browser = await chromium.launch({ headless: false });
const page = await browser.newPage();
await page.goto(TARGET_URL);
const links = await page.locator('a[href^="http"]').all();
const results = { working: 0, broken: [] };
for (const link of links) {
const href = await link.getAttribute('href');
try {
const response = await page.request.head(href);
if (response.ok()) {
results.working++;
} else {
results.broken.push({ url: href, status: response.status() });
}
} catch (e) {
results.broken.push({ url: href, error: e.message });
}
}
console.log(`Working links: ${results.working}`);
console.log(`Broken links:`, results.broken);
await browser.close();
})();
const { chromium } = require('playwright');
const helpers = require('./lib/helpers');
const TARGET_URL = 'http://localhost:3847';
(async () => {
const browser = await chromium.launch({ headless: false });
const page = await browser.newPage();
await page.goto(TARGET_URL);
const results = await helpers.checkAccessibility(page);
console.log('Accessibility audit complete');
console.log(`Critical issues: ${results.summary.critical}`);
console.log(`Serious issues: ${results.summary.serious}`);
await browser.close();
})();
const { chromium } = require('playwright');
const helpers = require('./lib/helpers');
const TARGET_URL = 'http://localhost:3847';
(async () => {
const browser = await chromium.launch({ headless: false });
const page = await browser.newPage();
const metrics = await helpers.measurePageLoad(page, TARGET_URL);
console.log('Load time:', metrics.loadTime, 'ms');
console.log('TTFB:', metrics.metrics.ttfb, 'ms');
console.log('DOM Content Loaded:', metrics.metrics.domContentLoaded, 'ms');
const lcp = await helpers.measureLCP(page);
console.log('LCP:', lcp, 'ms');
await browser.close();
})();
const { chromium } = require('playwright');
const helpers = require('./lib/helpers');
const TARGET_URL = 'http://localhost:3847';
(async () => {
const browser = await chromium.launch({ headless: false });
const page = await browser.newPage();
// Mock the API before navigating
await helpers.mockAPIResponse(page, '**/api/users', [
{ id: 1, name: 'Mock User 1' },
{ id: 2, name: 'Mock User 2' }
]);
await page.goto(TARGET_URL);
// Page will receive mocked data
await browser.close();
})();
const { chromium, devices } = require('playwright');
const TARGET_URL = 'http://localhost:3847';
(async () => {
const browser = await chromium.launch({ headless: false });
const context = await browser.newContext({
...devices['iPhone 12']
});
const page = await context.newPage();
await page.goto(TARGET_URL);
await page.screenshot({ path: '/tmp/iphone12.png' });
await browser.close();
})();
The lib/helpers.js provides 42 utility functions:
Browser & Context:
launchBrowser(browserType?, options?) - Launch browser with defaultscreateContext(browser, options?) - Create context with viewport/localecreatePage(context, options?) - Create page with timeoutsaveStorageState(context, path) - Save session for reuseloadStorageState(browser, path) - Restore saved sessiondetectDevServers(customPorts?) - Scan for running dev serversNavigation & Waiting:
waitForPageReady(page, options?) - Smart page ready detectionnavigateWithRetry(page, url, options?) - Navigate with automatic retrywaitForSPA(page, options?) - Wait for SPA route changeswaitForElement(page, selector, options?) - Wait for element stateSafe Interactions:
safeClick(page, selector, options?) - Click with retry logicsafeType(page, selector, text, options?) - Type with clear optionsafeSelect(page, selector, value, options?) - Safe dropdown selectionsafeCheck(page, selector, checked?, options?) - Safe checkbox/radioscrollPage(page, direction, distance?) - Scroll in any directionscrollToElement(page, selector, options?) - Scroll element into viewauthenticate(page, credentials, selectors?) - Handle login flowhandleCookieBanner(page, timeout?) - Dismiss cookie consentForm Helpers:
getFormFields(page, formSelector?) - Extract form field metadatagetRequiredFields(page, formSelector?) - Get required fieldsgetFieldErrors(page, formSelector?) - Get validation errorsvalidateFieldState(page, selector) - Check field validityfillFormFromData(page, formSelector, data, options?) - Auto-fill formsubmitAndValidate(page, formSelector, options?) - Submit and check errorsAccessibility:
checkAccessibility(page, options?) - Run axe-core auditgetARIAInfo(page, selector) - Extract ARIA attributescheckFocusOrder(page, options?) - Verify tab ordergetFocusableElements(page) - List focusable elementsPerformance:
measurePageLoad(page, url, options?) - Comprehensive load metricsmeasureLCP(page) - Largest Contentful PaintmeasureFCP(page) - First Contentful PaintmeasureCLS(page) - Cumulative Layout ShiftNetwork:
mockAPIResponse(page, urlPattern, response, options?) - Mock APIblockResources(page, resourceTypes) - Block images/fonts/etccaptureRequests(page, urlPattern?) - Capture network requestscaptureResponses(page, urlPattern?) - Capture responseswaitForAPI(page, urlPattern, options?) - Wait for API callVisual:
takeScreenshot(page, name, options?) - Timestamped screenshotcompareScreenshots(baseline, current, options?) - Visual difftakeElementScreenshot(page, selector, name, options?) - Element screenshotMobile:
emulateDevice(browser, deviceName) - Emulate iPhone/Pixel/etcsetGeolocation(context, coords) - Set GPS coordinatessimulateTouchEvent(page, type, coords) - Trigger touch eventsswipe(page, direction, distance?, options?) - Swipe gestureMulti-page:
handlePopup(page, triggerAction, options?) - Handle popup windowshandleNewTab(page, triggerAction, options?) - Handle new tabscloseAllPopups(context) - Close extra pageshandleDialog(page, action, text?) - Handle alert/confirm/promptData Extraction:
extractTexts(page, selector) - Get text from elementsextractTableData(page, tableSelector) - Parse table to JSONextractMetaTags(page) - Get meta tag infoextractOpenGraph(page) - Get OG metadataextractJsonLD(page) - Get structured dataextractLinks(page, options?) - Get all linksConsole Monitoring:
captureConsoleLogs(page, options?) - Capture console outputcapturePageErrors(page) - Capture JS errorsgetConsoleErrors(consoleCapture) - Get collected errorsassertNoConsoleErrors(consoleCapture) - Fail if errors existFiles:
uploadFile(page, selector, filePath, options?) - Upload fileuploadMultipleFiles(page, selector, filePaths) - Upload multipledownloadFile(page, triggerAction, options?) - Download and savewaitForDownload(page, triggerAction) - Wait for downloadUtilities:
retryWithBackoff(fn, maxRetries?, initialDelay?) - Retry with backoffdelay(ms) - Promise-based delayFor quick one-off tasks, execute code inline:
cd ${CLAUDE_PLUGIN_ROOT} && node run.js "
const browser = await chromium.launch({ headless: false });
const page = await browser.newPage();
await page.goto('http://localhost:3847');
console.log('Title:', await page.title());
await page.screenshot({ path: '/tmp/quick.png' });
await browser.close();
"
When to use:
detectDevServers() before localhost testing/tmp/playwright-test-*.js, never plugin directoryTARGET_URL constant at topheadless: false unless explicitly requestedslowMo: 100 to see actionswaitForURL, waitForSelector instead of timeoutsPlaywright not installed:
cd ${CLAUDE_PLUGIN_ROOT} && npm run setup
Module not found:
Run from plugin directory via run.js wrapper
Browser doesn't open:
Check headless: false and ensure display available
Element not found:
Add wait: await page.waitForSelector('.element', { timeout: 10000 })
For comprehensive Playwright API documentation, see API_REFERENCE.md:
development
Apple Human Interface Guidelines for content display components. Use this skill when the user asks about charts component, collection view, image view, web view, color well, image well, activity view, lockup, data visualization, content display, displaying images, rendering web content, color pickers, or presenting collections of items in Apple apps. Also use when the user says how should I display charts, what's the best way to show images, should I use a web view, how do I build a grid of items, what component shows media, or how do I present a share sheet. Cross-references: hig-foundations for color/typography/accessibility, hig-patterns for data visualization patterns, hig-components-layout for structural containers, hig-platforms for platform-specific component behavior.
tools
Automate HelpDesk tasks via Rube MCP (Composio): list tickets, manage views, use canned responses, and configure custom fields. Always search tools first for current schemas.
testing
Expert Haskell engineer specializing in advanced type systems, pure functional design, and high-reliability software. Use PROACTIVELY for type-level programming, concurrency, and architecture guidance.
tools
GraphQL gives clients exactly the data they need - no more, no less. One endpoint, typed schema, introspection. But the flexibility that makes it powerful also makes it dangerous. Without proper controls, clients can craft queries that bring down your server. This skill covers schema design, resolvers, DataLoader for N+1 prevention, federation for microservices, and client integration with Apollo/urql. Key insight: GraphQL is a contract. The schema is the API documentation. Design it carefully.