.claude/skills/playwright-tests/SKILL.md
Pattern for writing Playwright E2E tests that capture screenshots and narration for GIF/MP4 generation
npx skillsauth add clay-ferguson/mkbrowser playwright-testsInstall 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.
Demo tests follow a strict pattern: they walk through a user-visible workflow step by step, capturing screenshots at each step and writing companion narration text files. Downstream tooling assembles these into a GIF and an MP4 with audio narration. The goal is to create clear, engaging demo videos that show off features in a tutorial style. We also do use these tests for basic E2E verification, so they have dual purpose: video createion and automated testing.
You can look at the file named create-file-demo.spec.ts for a complete example of the pattern. Below are detailed instructions and guidelines for writing new tests in this style.
Place test files in tests/e2e/ and name them <demo-name>.spec.ts. The test name (derived from the filename via path.basename(__filename, '.spec.ts')) is used as the screenshot subdirectory under screenshots/.
import { test, expect } from './fixtures/electronApp';
import { takeStepScreenshot, takeStepScreenshotWithHighlight, writeNarration, demonstrateClickForDemo, insertTextForDemo, logScreenshotSummary } from './helpers/mediaUtils';
import * as fs from 'fs';
import * as path from 'path';
Every demo test begins with this exact setup block inside the test(...) callback:
// Create subfolder based on test file name
const testName = path.basename(__filename, '.spec.ts');
const screenshotDir = path.join(__dirname, '../../screenshots', testName);
// Clean and recreate screenshot directory on each run
fs.rmSync(screenshotDir, { recursive: true, force: true });
fs.mkdirSync(screenshotDir, { recursive: true });
// Clean up any previously created test files to avoid conflicts
const testDataDir = path.join(__dirname, '../../mkbrowser-test');
for (const file of fs.readdirSync(testDataDir).filter(f => /^my-.*\.md$/.test(f))) {
fs.unlinkSync(path.join(testDataDir, file));
}
let step = 1;
// Wait for initial load
await mainWindow.waitForTimeout(2000);
Adjust the cleanup filter regex (/^my-.*\.md$/) as needed for the specific test if it creates differently-named files.
Use a single let step = 1 counter, always incremented with step++ inline in every call. Screenshots and narration files interleave — a screenshot is typically followed immediately by its narration, both consuming a step number:
await takeStepScreenshot(mainWindow, screenshotDir, step++, 'descriptive-label');
writeNarration(screenshotDir, step++, 'Spoken narration for this moment in the demo.');
The output filenames are zero-padded to three digits (e.g. 001-files-visible.png, 002-narration.txt), so downstream tooling can sort them correctly.
All helpers are in tests/e2e/helpers/mediaUtils.ts.
takeStepScreenshot — plain screenshotawait takeStepScreenshot(mainWindow, screenshotDir, step++, 'label');
Use for general state captures: after navigation, after a save completes, etc.
takeStepScreenshotWithHighlight — screenshot with element highlightedawait takeStepScreenshotWithHighlight(mainWindow, locator, screenshotDir, step++, 'label');
Use just before clicking an element, or after typing into one, to draw the viewer's eye to it. Always take the highlight screenshot before demonstrateClickForDemo so the element is still visible without any transition state.
writeNarration — write companion narration filewriteNarration(screenshotDir, step++, 'Narration text that will be read aloud.');
writeNarration is synchronous. Write it immediately after the screenshot it accompanies. The narration should clearly describe what is visible on screen and what is about to happen next, in plain conversational language suitable for text-to-speech.
demonstrateClickForDemo — click with demo timingawait demonstrateClickForDemo(locator);
Adds 300 ms before and 1 000 ms after the click so a screen recorder captures the state change clearly.
insertTextForDemo — type text into the focused element// Into an explicit input:
await insertTextForDemo(mainWindow, 'filename-here', true, filenameInput);
// Into whatever has focus (e.g. a CodeMirror editor):
await insertTextForDemo(mainWindow, multiLineContent, true);
The third argument showHighlight should be true for demo tests. Pass an optional focusTarget locator when the element to type into is not already focused.
logScreenshotSummary — log counts at end of testlogScreenshotSummary(screenshotDir);
Always call this as the final statement of the test body.
Every demo test follows this rhythm:
takeStepScreenshotWithHighlight + narration.demonstrateClickForDemo or insertTextForDemo.takeStepScreenshot + narration describing what changed.expect(saveButton).not.toBeVisible()).logScreenshotSummary.[LaTeX](/lˈeɪtɛk/).Include lightweight expect assertions at key moments to guard against the demo silently failing:
// Verify the app is ready before starting
await expect(mainWindow.getByText('sample.md')).toBeVisible({ timeout: 10000 });
// Verify the final action completed
await expect(mainWindow.getByTestId('entry-save-button')).not.toBeVisible({ timeout: 5000 });
import { test, expect } from './fixtures/electronApp';
import { takeStepScreenshot, takeStepScreenshotWithHighlight, writeNarration, demonstrateClickForDemo, insertTextForDemo, logScreenshotSummary } from './helpers/mediaUtils';
import * as fs from 'fs';
import * as path from 'path';
test.describe('My Feature Demo', () => {
test('demonstrate my feature', async ({ mainWindow }) => {
const testName = path.basename(__filename, '.spec.ts');
const screenshotDir = path.join(__dirname, '../../screenshots', testName);
fs.rmSync(screenshotDir, { recursive: true, force: true });
fs.mkdirSync(screenshotDir, { recursive: true });
const testDataDir = path.join(__dirname, '../../mkbrowser-test');
for (const file of fs.readdirSync(testDataDir).filter(f => /^my-.*\.md$/.test(f))) {
fs.unlinkSync(path.join(testDataDir, file));
}
let step = 1;
await mainWindow.waitForTimeout(2000);
// Verify initial state
await expect(mainWindow.getByText('sample.md')).toBeVisible({ timeout: 10000 });
await takeStepScreenshot(mainWindow, screenshotDir, step++, 'initial-view');
writeNarration(screenshotDir, step++, 'Welcome to MkBrowser. Today we will demonstrate...');
// --- workflow steps here ---
logScreenshotSummary(screenshotDir);
});
});
tools
```skill --- name: popup-menus description: Pattern for creating popup menus anchored to icon buttons --- # Instructions ## Overview Popup menus are icon-triggered dropdown menus that appear over the page content, anchored below the button the user clicked. They automatically adjust position to avoid being clipped by viewport edges. Clicking a menu item fires a callback and dismisses the menu. Clicking outside or pressing Escape also dismisses it. ## Architecture: Two Layers ### 1. Base Com
tools
Pattern for application pages
tools
Pattern for making popup modal dialogs
testing
Create, edit, improve, or audit AgentSkills. Use when creating a new skill from scratch or when asked to improve, review, audit, tidy up, or clean up an existing skill or SKILL.md file. Also use when editing or restructuring a skill directory (moving files to references/ or scripts/, removing stale content, validating against the AgentSkills spec). Triggers on phrases like "create a skill", "author a skill", "tidy up a skill", "improve this skill", "review the skill", "clean up the skill", "audit the skill".