From 11f5f003eef12bff9633ca8190e3c43c7dab6708 Mon Sep 17 00:00:00 2001 From: Craig Jennings Date: Sun, 19 Apr 2026 15:16:46 -0500 Subject: feat: adopt lackeyjb/playwright-skill (MIT verbatim fork) + deps target MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Browser automation + UI testing skill forked verbatim from github.com/lackeyjb/playwright-skill (MIT, 2458 stars, active through Dec 2025). LICENSE preserved in skill dir with attribution footer added to SKILL.md. Bundle contents (from upstream): playwright-skill/SKILL.md playwright-skill/API_REFERENCE.md playwright-skill/run.js (universal executor with module resolution) playwright-skill/package.json playwright-skill/lib/helpers.js (detectDevServers, safeClick, safeType, takeScreenshot, handleCookieBanner, extractTableData, createContext with env-driven header injection) playwright-skill/LICENSE (MIT, lackeyjb) Makefile updates: - SKILLS extended with playwright-skill; make install symlinks it globally into ~/.claude/skills/ - deps target extended to check node + npm, and to run the skill's own `npm run setup` (installs Playwright + Chromium ~300 MB on first run). Idempotent: skipped if node_modules/playwright already exists. Stack fit: JavaScript Playwright aligns with Craig's TypeScript/React frontend work. Python-side (Django) browser tests would be better served by Anthropic's official webapp-testing skill (Python Playwright bindings), noted in the evaluation memory but not adopted here — minimal overlap, easy to add later if the need arises. --- playwright-skill/run.js | 228 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 228 insertions(+) create mode 100755 playwright-skill/run.js (limited to 'playwright-skill/run.js') diff --git a/playwright-skill/run.js b/playwright-skill/run.js new file mode 100755 index 0000000..10f2616 --- /dev/null +++ b/playwright-skill/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