Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 46 additions & 1 deletion skills/playwright-skill/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ General-purpose browser automation skill. I'll write custom Playwright code for
- If **multiple servers found**: Ask user which one to test
- If **no servers found**: Ask for URL or offer to help start dev server

2. **Write scripts to /tmp** - NEVER write test files to skill directory; always use `/tmp/playwright-test-*.js`
2. **Write scripts to /tmp** - NEVER write test files to skill directory; always use `/tmp/playwright-test-*.js`. If the user wants to save scripts, set `PW_SCRIPT_DIR` (see [Persisting Test Scripts](#persisting-test-scripts))

3. **Use visible browser by default** - Always use `headless: false` unless user specifically requests headless mode

Expand Down Expand Up @@ -326,6 +326,50 @@ const data = await helpers.extractTableData(page, 'table.results');

See `lib/helpers.js` for full list.

## Persisting Test Scripts

By default, scripts are written to `/tmp/` and auto-cleaned by the OS. To save scripts permanently, set the `PW_SCRIPT_DIR` environment variable to a directory of your choice.

When set, `run.js` automatically copies the executed script to that directory after each run.

### Configuration

```bash
# Save scripts to a folder in your project
export PW_SCRIPT_DIR=./playwright-tests

# Or inline per-run
PW_SCRIPT_DIR=./playwright-tests cd $SKILL_DIR && node run.js /tmp/playwright-test-login.js
```

### Workflow with persistence enabled

**Step 1: Set PW_SCRIPT_DIR (once, in your shell profile or .env)**

```bash
export PW_SCRIPT_DIR=~/my-project/playwright-tests
```

**Step 2: Run as normal — scripts are auto-saved**

```bash
cd $SKILL_DIR && node run.js /tmp/playwright-test-login.js
# Output: 💾 Script saved to: ~/my-project/playwright-tests/playwright-test-login.js
```

**Step 3: Re-run saved scripts directly**

```bash
cd $SKILL_DIR && node run.js ~/my-project/playwright-tests/playwright-test-login.js
```

### Notes

- The directory is created automatically if it doesn't exist
- If a script with the same filename already exists, a timestamp suffix is added (e.g. `playwright-test-login-1234567890.js`)
- Only file-based executions are saved; inline code and stdin are not persisted
- Scripts from `/tmp/` are still cleaned up by the OS as usual; the saved copy in `PW_SCRIPT_DIR` is permanent

## Custom HTTP Headers

Configure custom headers for all HTTP requests via environment variables. Useful for:
Expand Down Expand Up @@ -386,6 +430,7 @@ For comprehensive Playwright API documentation, see [API_REFERENCE.md](API_REFER
- **CRITICAL: Detect servers FIRST** - Always run `detectDevServers()` before writing test code for localhost testing
- **Custom headers** - Use `PW_HEADER_NAME`/`PW_HEADER_VALUE` env vars to identify automated traffic to your backend
- **Use /tmp for test files** - Write to `/tmp/playwright-test-*.js`, never to skill directory or user's project
- **Persist scripts** - Set `PW_SCRIPT_DIR=./playwright-tests` to auto-save scripts for reuse; remind users of this option when they ask to re-run or save tests
- **Parameterize URLs** - Put detected/provided URL in a `TARGET_URL` constant at the top of every script
- **DEFAULT: Visible browser** - Always use `headless: false` unless user explicitly asks for headless mode
- **Headless mode** - Only use `headless: true` when user specifically requests "headless" or "background" execution
Expand Down
41 changes: 36 additions & 5 deletions skills/playwright-skill/run.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,35 @@ function installPlaywright() {
}

/**
* Get code to execute from various sources
* Save script to PW_SCRIPT_DIR if configured
*/
function maybeSaveScript(sourceFile) {
const scriptDir = process.env.PW_SCRIPT_DIR;
if (!scriptDir || !sourceFile) return;

try {
if (!fs.existsSync(scriptDir)) {
fs.mkdirSync(scriptDir, { recursive: true });
}

const filename = path.basename(sourceFile);
const destPath = path.join(scriptDir, filename);

// Avoid overwriting an existing file with the same name
const finalPath = fs.existsSync(destPath)
? path.join(scriptDir, `${path.basename(filename, '.js')}-${Date.now()}.js`)
: destPath;

fs.copyFileSync(sourceFile, finalPath);
console.log(`💾 Script saved to: ${finalPath}`);
} catch (e) {
console.warn(`⚠️ Could not save script to ${scriptDir}: ${e.message}`);
}
}

/**
* Get code to execute from various sources.
* Returns { code, sourceFile } where sourceFile is set when input is a file path.
*/
function getCodeToExecute() {
const args = process.argv.slice(2);
Expand All @@ -56,19 +84,19 @@ function getCodeToExecute() {
if (args.length > 0 && fs.existsSync(args[0])) {
const filePath = path.resolve(args[0]);
console.log(`📄 Executing file: ${filePath}`);
return fs.readFileSync(filePath, 'utf8');
return { code: fs.readFileSync(filePath, 'utf8'), sourceFile: filePath };
}

// Case 2: Inline code provided as argument
if (args.length > 0) {
console.log('⚡ Executing inline code');
return args.join(' ');
return { code: args.join(' '), sourceFile: null };
}

// Case 3: Code from stdin
if (!process.stdin.isTTY) {
console.log('📥 Reading from stdin');
return fs.readFileSync(0, 'utf8');
return { code: fs.readFileSync(0, 'utf8'), sourceFile: null };
}

// No input
Expand Down Expand Up @@ -194,9 +222,12 @@ async function main() {
}

// Get code to execute
const rawCode = getCodeToExecute();
const { code: rawCode, sourceFile } = getCodeToExecute();
const code = wrapCodeIfNeeded(rawCode);

// Persist script to PW_SCRIPT_DIR if configured
maybeSaveScript(sourceFile);

// Create temporary file for execution
const tempFile = path.join(__dirname, `.temp-execution-${Date.now()}.js`);

Expand Down