playwright-skill
Playwright Web Testing & Automation
Comprehensive web testing skill using Playwright. Write custom JavaScript code for any testing or automation task.
Prerequisites
- Node.js: required — install from https://nodejs.org
- playwright npm package:
npm install playwright(installs headless Chromium automatically)
Decision Tree: Choosing Your Approach
User task → Is server already running?
├─ Yes → Direct Testing
│ ├─ Static HTML? → Navigate directly (file:// or http://)
│ └─ Dynamic webapp? → Use Reconnaissance-Then-Action pattern
│
└─ No → Server Management Required
├─ Single server → Start server, then test
└─ Multiple servers → Start all servers, coordinate testing
CRITICAL WORKFLOW
- CheckTesting if server is running - Detect running dev servers OR start servers if needed
- Write scripts to /tmp - NEVER write test files to skill directory; always use
/tmp/playwright-test-*.js - Use visible browser by default - Always use
headless: falseunless user specifically requests headless mode - Wait for dynamic content - Use
waitForLoadState('networkidle')before inspecting dynamic webapps - Parameterize URLs - Always make URLs configurable via constant at top of script
Reconnaissance-Then-Action Pattern
For dynamic webapps where you don't know the DOM structure upfront:
// /tmp/playwright-test-reconnaissance.js
const { chromium } = require('playwright');
const TARGET_URL = 'http://localhost:3000';
(async () => {
const browser = await chromium.launch({ headless: false });
const page = await browser.newPage();
// STEP 1: Navigate and wait for dynamic content
await page.goto(TARGET_URL);
await page.waitForLoadState('networkidle'); // CRITICAL for dynamic apps
// STEP 2: Reconnaissance - discover what's on the page
await page.screenshot({ path: '/tmp/inspect.png', fullPage: true });
const buttons = await page.locator('button').all();
console.log(`Found ${buttons.length} buttons`);
for (let i = 0; i < buttons.length; i++) {
const text = await buttons[i].textContent();
console.log(` Button ${i}: "${text}"`);
}
// STEP 3: Action - interact with discovered elements
const loginButton = page.locator('button:has-text("Login")');
if (await loginButton.isVisible()) {
await loginButton.click();
console.log('✅ Clicked login button');
}
await browser.close();
})();
Server Management
Check for Running Servers
# Check if port is in use
lsof -i :3000 # Mac/Linux
netstat -ano | findstr :3000 # Windows
Start Server Before Testing
// /tmp/playwright-test-with-server.js
const { chromium } = require('playwright');
const { spawn } = require('child_process');
const TARGET_URL = 'http://localhost:3000';
(async () => {
// Start server
console.log('Starting server...');
const server = spawn('npm', ['run', 'dev'], { shell: true });
server.stdout.on('data', (data) => console.log(`Server: ${data}`));
server.stderr.on('data', (data) => console.error(`Server Error: ${data}`));
// Wait for server to be ready
await new Promise(resolve => setTimeout(resolve, 3000));
// Run tests
const browser = await chromium.launch({ headless: false });
const page = await browser.newPage();
await page.goto(TARGET_URL);
await page.waitForLoadState('networkidle');
// Your test logic here
console.log('Title:', await page.title());
await browser.close();
// Clean up server
server.kill();
console.log('✅ Tests complete, server stopped');
})();
Common Patterns
Test a Page (Multiple Viewports)
// /tmp/playwright-test-responsive.js
const { chromium } = require('playwright');
const TARGET_URL = 'http://localhost:3001'; // Auto-detected
(async () => {
const browser = await chromium.launch({ headless: false, slowMo: 100 });
const page = await browser.newPage();
// Desktop test
await page.setViewportSize({ width: 1920, height: 1080 });
await page.goto(TARGET_URL);
console.log('Desktop - Title:', await page.title());
await page.screenshot({ path: '/tmp/desktop.png', fullPage: true });
// Mobile test
await page.setViewportSize({ width: 375, height: 667 });
await page.screenshot({ path: '/tmp/mobile.png', fullPage: true });
await browser.close();
})();
Test Login Flow
// /tmp/playwright-test-login.js
const { chromium } = require('playwright');
const TARGET_URL = 'http://localhost:3001'; // Auto-detected
(async () => {
const browser = await chromium.launch({ headless: false });
const page = await browser.newPage();
await page.goto(`${TARGET_URL}/login`);
await page.fill('input[name="email"]', 'test@example.com');
await page.fill('input[name="password"]', 'password123');
await page.click('button[type="submit"]');
// Wait for redirect
await page.waitForURL('**/dashboard');
console.log('✅ Login successful, redirected to dashboard');
await browser.close();
})();
Fill and Submit Form
// /tmp/playwright-test-form.js
const { chromium } = require('playwright');
const TARGET_URL = 'http://localhost:3001'; // Auto-detected
(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"]', 'john@example.com');
await page.fill('textarea[name="message"]', 'Test message');
await page.click('button[type="submit"]');
// Verify submission
await page.waitForSelector('.success-message');
console.log('✅ Form submitted successfully');
await browser.close();
})();
Check for Broken Links
const { chromium } = require('playwright');
(async () => {
const browser = await chromium.launch({ headless: false });
const page = await browser.newPage();
await page.goto('http://localhost:3000');
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();
})();
Take Screenshot with Error Handling
const { chromium } = require('playwright');
(async () => {
const browser = await chromium.launch({ headless: false });
const page = await browser.newPage();
try {
await page.goto('http://localhost:3000', {
waitUntil: 'networkidle',
timeout: 10000,
});
await page.screenshot({
path: '/tmp/screenshot.png',
fullPage: true,
});
console.log('📸 Screenshot saved to /tmp/screenshot.png');
} catch (error) {
console.error('❌ Error:', error.message);
} finally {
await browser.close();
}
})();
Test Responsive Design
// /tmp/playwright-test-responsive-full.js
const { chromium } = require('playwright');
const TARGET_URL = 'http://localhost:3001'; // Auto-detected
(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) {
console.log(
`Testing ${viewport.name} (${viewport.width}x${viewport.height})`,
);
& Discovery
```javascript
// Check visibility
const isVisible = await page.locator('button').isVisible();
// Get text
const text = await page.locator('h1').textContent();
// Get attribute
const href = await page.locator('a').getAttribute('href');
// Find all elements
const allButtons = await page.locator('button').all();
const allLinks = await page.locator('a').all();
// Check if element exists
const count = await page.locator('.modal').count();
console.log(`Found ${count} modals`);
Network & Console
// Intercept requests
await page.route('**/api/**', route => {
route.fulfill({
status: 200,
body: JSON.stringify({ mocked: true })
});
});
// Wait for response
const response = await page.waitForResponse('**/api/data');
console.log(await response.json());
// Capture console logs
page.on('console', msg => {
console.log(`Browser console [${msg.type()}]:`, msg.text());
});
Best Practices
✅ DO
- Wait for networkidle on dynamic apps - Always use
page.waitForLoadState('networkidle')before inspecting DOM on SPAs/React/Vue apps - Use reconnaissance pattern - Take screenshot, inspect DOM, then act based on what you find
- Visible browser by default - Use
headless: falsefor easier debugging - Descriptive selectors - Use
text=,role=, or data attributes over brittle CSS selectors - Error handling - Wrap automation in try-catch blocks
- Clean up resources - Always close browser and kill servers
❌ DON'T
- Don't inspect before networkidle - On dynamic webapps, DOM may not be ready yet
- Don't use fixed timeouts - Use
waitForSelector()orwaitForLoadState()instead of arbitrary waits - Don't write to skill directory - Always use
/tmpfor test scripts - Don't hardcode URLs - Use constants at top of script for easy modificationeadless: false, // Visible browser slowMo: 50 // Slow down by 50ms });
const page = await browser.newPage();
// Navigate await page.goto('https://example.com', { waitUntil: 'networkidle' // Wait for network to be idle });
// Close await browser.close();
### Selectors & Interactions
```javascript
// Click
await page.click('button.submit');
await page.dblclick('.item');
// Fill input
await page.fill('input[name="email"]', 'test@example.com');
await page.getByLabel('Email').fill('user@example.com');
// Checkbox
await page.check('input[type="checkbox"]');
await page.uncheck('input[type="checkbox"]');
// Select dropdown
await page.selectOption('select#country', 'usa');
// Type with delay
await page.type('#username', 'testuser', { delay: 100 });
Waiting Strategies
// Wait for navigation
await page.waitForURL('**/dashboard');
await page.waitForLoadState('networkidle');
// Wait for element
await page.waitForSelector('.success-message');
await page.waitForSelector('.spinner', { state: 'hidden' });
// Wait for timeout (use sparingly)
await page.waitForTimeout(1000);
Screenshots
// Full page screenshot
await page.screenshot({
path: '/tmp/screenshot.png',
fullPage: true
});
// Element screenshot
await page.locator('.chart').screenshot({
path: '/tmp/chart.png'
});
```Quick Tips
- **Visible browser** - Always `headless: false` unless explicitly requested
- **Write to /tmp** - Scripts go to `/tmp/playwright-test-*.js`, never to project directories
- **Parameterize URLs** - Use `TARGET_URL` constant at top of script
- **Slow down** - Use `slowMo: 100` to see actions in real-time
- **Wait smart** - Use `waitForLoadState('networkidle')` for dynamic apps before inspecting
- **Error handling** - Wrap in try-catch with proper cleanup in finally block
- **Progress feedback** - Use `console.log()` to
const text = await page.locator('h1').textContent();
// Get attribute
const href = await page.locator('a').getAttribute('href');
Network
// Intercept requests
await page.route('**/api/**', route => {
route.fulfill({
status: 200,
body: JSON.stringify({ mocked: true })
});
});
// Wait for response
const response = await page.waitForResponse('**/api/data');
console.log(await response.json());
Tips
- DEFAULT: Visible browser - Always use
headless: falseunless user explicitly asks for headless mode - Use /tmp for test files - Write to
/tmp/playwright-test-*.js, never to skill directory or user's project - Parameterize URLs - Put detected/provided URL in a
TARGET_URLconstant at the top of every script - Slow down: Use
slowMo: 100to make actions visible and easier to follow - Wait strategies: Use
waitForURL,waitForSelector,waitForLoadStateinstead of fixed timeouts - Error handling: Always use try-catch for robust automation
- Console output: Use
console.log()to track progress and show what's happening
Common Use Cases
Visual Testing:
- Take screenshots at different viewports
- Compare visual changes
- Test responsive design
Functional Testing:
- Test login flows
- Form validation
- Navigation flows
- Error handling
Validation:
- Check for broken links
- Verify images load
- Test page load times
- Check accessibility
Automation:
- Fill forms automatically
- Click through user flows
- Extract data from pages
- Generate reports
Notes
- Each automation is custom-written for your specific request
- Not limited to pre-built scripts - any browser task possible
- Auto-detects running dev servers to eliminate hardcoded URLs
- Test scripts written to
/tmpfor automatic cleanup (no clutter) - Progressive disclosure - load advanced documentation only when needed
References
More from dedalus-erp-pas/hexagone-foundation-skills
vue-best-practices
Guide des bonnes pratiques Vue.js 3 couvrant la Composition API, la conception de composants, les patrons de réactivité, le styling utility-first avec Tailwind CSS, l'intégration native de la bibliothèque de composants PrimeVue et l'organisation du code. À utiliser lors de l'écriture, la revue ou le refactoring de code Vue.js pour garantir des patrons idiomatiques et un code maintenable.
23ubiquitous-language
Extrait un glossaire de langage ubiquitaire style DDD de la conversation en cours, signale les ambiguïtés et propose des termes canoniques. Sauvegarde dans UBIQUITOUS_LANGUAGE.md. À utiliser quand l'utilisateur veut définir des termes métier, construire un glossaire, durcir la terminologie, créer un langage ubiquitaire ou mentionne « domain model », « DDD », « glossaire » ou « langage ubiquitaire ».
23grill-me
Interroge l'utilisateur sans relâche sur un plan ou un design jusqu'à atteindre une compréhension partagée, en résolvant chaque branche de l'arbre de décision. À utiliser quand l'utilisateur veut stress-tester un plan, se faire challenger sur son design, ou mentionne « grill me » / « interroge-moi » / « challenge-moi » / « questionne-moi ».
22meeting
Lance une réunion simulée avec plusieurs personas experts pour analyser un sujet sous des perspectives diverses, prendre une décision et proposer une solution avant implémentation. Peut optionnellement publier l'analyse de la réunion sur une issue GitLab ou GitHub liée.
22changelog-generator
Crée automatiquement des changelogs orientés utilisateur à partir des commits git en analysant l'historique, catégorisant les changements et transformant les commits techniques en notes de version claires et compréhensibles. Transforme des heures de rédaction manuelle en minutes de génération automatisée.
22github-issues
Crée, récupère, met à jour et gère les issues GitHub avec collecte complète du contexte. À utiliser quand l'utilisateur veut créer une nouvelle issue, voir les détails d'une issue, mettre à jour des issues existantes, lister les issues du projet, ajouter des commentaires ou gérer les workflows d'issues dans GitHub.
22