From 4ffa7417a359ef4eae09f61d7da4de06539462ca Mon Sep 17 00:00:00 2001 From: Craig Jennings Date: Sun, 19 Apr 2026 15:24:51 -0500 Subject: refactor(playwright): split into playwright-js + playwright-py variants MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rename `playwright-skill/` → `playwright-js/` and add `playwright-py/` as a verbatim fork of Anthropic's official `webapp-testing` skill (Apache-2.0). Cross-pollinate: each skill gains patterns and helpers inspired by the other's strengths, with upstream semantics preserved. ## playwright-js (JS/TS stack) Renamed from playwright-skill; upstream lackeyjb MIT content untouched. New sections added (clearly marked, preserving upstream semantics): - Static HTML vs Dynamic Webapp decision tree (core Anthropic methodology) - Reconnaissance-Then-Action pattern (navigate → networkidle → inspect → act) - Console Log Capture snippet (page.on console/pageerror/requestfailed) Description updated to clarify JS/TS stack fit (React/Next/Vue/Svelte/Node) and reference `/playwright-py` as the Python sibling. ## playwright-py (Python stack) Verbatim fork of anthropics/skills/skills/webapp-testing; upstream SKILL.md and bundled `scripts/with_server.py` + examples kept intact. New scripts and examples added (all lackeyjb-style conveniences in Python): Scripts: scripts/detect_dev_servers.py Probe common localhost ports for HTTP servers; outputs JSON of found services. scripts/safe_actions.py safe_click, safe_type (retry-wrapped), handle_cookie_banner (common selectors), build_context_with_headers (env-var- driven: PW_HEADER_NAME / PW_HEADER_VALUE / PW_EXTRA_HEADERS='{…json…}'). Examples: examples/login_flow.py Login form + wait_for_url. examples/broken_links.py Scan visible external hrefs via HEAD. examples/responsive_sweep.py Multi-viewport screenshots to /tmp. SKILL.md gains 5 "Added:" sections documenting the new scripts, retry helpers, env-header injection, and /tmp script discipline. Attribution notes explicitly mark upstream vs local additions. ## Makefile SKILLS: playwright-skill → playwright-js + playwright-py deps target: extended Playwright step to install Python package + Chromium via `python3 -m pip install --user playwright && python3 -m playwright install chromium` when playwright-py/ is present. Idempotent (detected via `python3 -c "import playwright"`). ## Usage Both skills symlinked globally via `make install`. Invoke whichever matches the project stack — cross-references in descriptions route you to the right one. Run `make deps` once to install both runtimes. --- playwright-js/run.js | 228 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 228 insertions(+) create mode 100755 playwright-js/run.js (limited to 'playwright-js/run.js') diff --git a/playwright-js/run.js b/playwright-js/run.js new file mode 100755 index 0000000..10f2616 --- /dev/null +++ b/playwright-js/run.js @@ -0,0 +1,228 @@ +#!/usr/bin/env node +/** + * Universal Playwright Executor for Claude Code + * + * Executes Playwright automation code from: + * - File path: node run.js script.js + * - Inline code: node run.js 'await page.goto("...")' + * - Stdin: cat script.js | node run.js + * + * Ensures proper module resolution by running from skill directory. + */ + +const fs = require('fs'); +const path = require('path'); +const { execSync } = require('child_process'); + +// Change to skill directory for proper module resolution +process.chdir(__dirname); + +/** + * Check if Playwright is installed + */ +function checkPlaywrightInstalled() { + try { + require.resolve('playwright'); + return true; + } catch (e) { + return false; + } +} + +/** + * Install Playwright if missing + */ +function installPlaywright() { + console.log('📦 Playwright not found. Installing...'); + try { + execSync('npm install', { stdio: 'inherit', cwd: __dirname }); + execSync('npx playwright install chromium', { stdio: 'inherit', cwd: __dirname }); + console.log('✅ Playwright installed successfully'); + return true; + } catch (e) { + console.error('❌ Failed to install Playwright:', e.message); + console.error('Please run manually: cd', __dirname, '&& npm run setup'); + return false; + } +} + +/** + * Get code to execute from various sources + */ +function getCodeToExecute() { + const args = process.argv.slice(2); + + // Case 1: File path provided + if (args.length > 0 && fs.existsSync(args[0])) { + const filePath = path.resolve(args[0]); + console.log(`📄 Executing file: ${filePath}`); + return fs.readFileSync(filePath, 'utf8'); + } + + // Case 2: Inline code provided as argument + if (args.length > 0) { + console.log('⚡ Executing inline code'); + return args.join(' '); + } + + // Case 3: Code from stdin + if (!process.stdin.isTTY) { + console.log('📥 Reading from stdin'); + return fs.readFileSync(0, 'utf8'); + } + + // No input + console.error('❌ No code to execute'); + console.error('Usage:'); + console.error(' node run.js script.js # Execute file'); + console.error(' node run.js "code here" # Execute inline'); + console.error(' cat script.js | node run.js # Execute from stdin'); + process.exit(1); +} + +/** + * Clean up old temporary execution files from previous runs + */ +function cleanupOldTempFiles() { + try { + const files = fs.readdirSync(__dirname); + const tempFiles = files.filter(f => f.startsWith('.temp-execution-') && f.endsWith('.js')); + + if (tempFiles.length > 0) { + tempFiles.forEach(file => { + const filePath = path.join(__dirname, file); + try { + fs.unlinkSync(filePath); + } catch (e) { + // Ignore errors - file might be in use or already deleted + } + }); + } + } catch (e) { + // Ignore directory read errors + } +} + +/** + * Wrap code in async IIFE if not already wrapped + */ +function wrapCodeIfNeeded(code) { + // Check if code already has require() and async structure + const hasRequire = code.includes('require('); + const hasAsyncIIFE = code.includes('(async () => {') || code.includes('(async()=>{'); + + // If it's already a complete script, return as-is + if (hasRequire && hasAsyncIIFE) { + return code; + } + + // If it's just Playwright commands, wrap in full template + if (!hasRequire) { + return ` +const { chromium, firefox, webkit, devices } = require('playwright'); +const helpers = require('./lib/helpers'); + +// Extra headers from environment variables (if configured) +const __extraHeaders = helpers.getExtraHeadersFromEnv(); + +/** + * Utility to merge environment headers into context options. + * Use when creating contexts with raw Playwright API instead of helpers.createContext(). + * @param {Object} options - Context options + * @returns {Object} Options with extraHTTPHeaders merged in + */ +function getContextOptionsWithHeaders(options = {}) { + if (!__extraHeaders) return options; + return { + ...options, + extraHTTPHeaders: { + ...__extraHeaders, + ...(options.extraHTTPHeaders || {}) + } + }; +} + +(async () => { + try { + ${code} + } catch (error) { + console.error('❌ Automation error:', error.message); + if (error.stack) { + console.error(error.stack); + } + process.exit(1); + } +})(); +`; + } + + // If has require but no async wrapper + if (!hasAsyncIIFE) { + return ` +(async () => { + try { + ${code} + } catch (error) { + console.error('❌ Automation error:', error.message); + if (error.stack) { + console.error(error.stack); + } + process.exit(1); + } +})(); +`; + } + + return code; +} + +/** + * Main execution + */ +async function main() { + console.log('🎭 Playwright Skill - Universal Executor\n'); + + // Clean up old temp files from previous runs + cleanupOldTempFiles(); + + // Check Playwright installation + if (!checkPlaywrightInstalled()) { + const installed = installPlaywright(); + if (!installed) { + process.exit(1); + } + } + + // Get code to execute + const rawCode = getCodeToExecute(); + const code = wrapCodeIfNeeded(rawCode); + + // Create temporary file for execution + const tempFile = path.join(__dirname, `.temp-execution-${Date.now()}.js`); + + try { + // Write code to temp file + fs.writeFileSync(tempFile, code, 'utf8'); + + // Execute the code + console.log('🚀 Starting automation...\n'); + require(tempFile); + + // Note: Temp file will be cleaned up on next run + // This allows long-running async operations to complete safely + + } catch (error) { + console.error('❌ Execution failed:', error.message); + if (error.stack) { + console.error('\n📋 Stack trace:'); + console.error(error.stack); + } + process.exit(1); + } +} + +// Run main function +main().catch(error => { + console.error('❌ Fatal error:', error.message); + process.exit(1); +}); -- cgit v1.2.3