seed-skills/vibe-testing/SKILL.md
Natural language test automation methodology where tests are written as plain English instructions, leveraging AI agents to interpret intent, generate executable tests, and maintain test suites without traditional code-based selectors or assertions.
npx skillsauth add PramodDutta/qaskills Vibe Testing MethodologyInstall 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.
You are an expert in vibe testing, the methodology where tests are expressed as natural language instructions that AI agents interpret and execute. When the user asks you to implement vibe testing workflows, create natural language test specifications, or build intent-based test automation systems, follow these detailed instructions.
vibe-tests/
specs/
auth/
login.vibe.md
registration.vibe.md
password-reset.vibe.md
checkout/
add-to-cart.vibe.md
payment.vibe.md
order-confirmation.vibe.md
dashboard/
navigation.vibe.md
settings.vibe.md
engine/
interpreter.ts
action-resolver.ts
assertion-resolver.ts
element-finder.ts
step-executor.ts
runners/
vibe-runner.ts
parallel-runner.ts
ci-runner.ts
reporters/
execution-log.ts
step-trace.ts
failure-analyzer.ts
config/
vibe-config.ts
ai-config.ts
fixtures/
test-data.ts
environment.ts
<!-- specs/auth/login.vibe.md -->
# Login Flow
## Setup
- Navigate to the login page
- Ensure the page has loaded completely
## Test: Successful login with valid credentials
1. Enter "[email protected]" in the email field
2. Enter "SecurePassword123" in the password field
3. Click the login button
4. Verify you are redirected to the dashboard
5. Verify the welcome message contains "testuser"
## Test: Failed login with wrong password
1. Enter "[email protected]" in the email field
2. Enter "WrongPassword" in the password field
3. Click the login button
4. Verify an error message appears
5. Verify the error mentions invalid credentials
6. Verify you remain on the login page
## Test: Login form validation
1. Click the login button without filling any fields
2. Verify the email field shows a validation error
3. Enter "not-an-email" in the email field
4. Verify the email field shows an invalid format error
5. Clear the email field
6. Enter "[email protected]" in the email field
7. Verify the email validation error disappears
## Cleanup
- If logged in, click the logout button
// vibe-tests/engine/interpreter.ts
import Anthropic from '@anthropic-ai/sdk';
export interface VibeStep {
raw: string;
action: 'navigate' | 'click' | 'fill' | 'select' | 'assert' | 'wait' | 'clear' | 'hover' | 'scroll';
target?: string;
value?: string;
assertion?: {
type: 'visible' | 'text' | 'url' | 'hidden' | 'enabled' | 'disabled' | 'contains';
expected?: string;
};
confidence: number;
}
export interface VibeTest {
name: string;
setup: string[];
steps: string[];
cleanup: string[];
}
export class VibeInterpreter {
private client: Anthropic;
constructor() {
this.client = new Anthropic();
}
parseSpecFile(content: string): VibeTest[] {
const tests: VibeTest[] = [];
const sections = content.split(/^## /m).filter(Boolean);
let setup: string[] = [];
let cleanup: string[] = [];
for (const section of sections) {
const lines = section.trim().split('\n');
const heading = lines[0].trim();
if (heading.toLowerCase().startsWith('setup')) {
setup = this.extractSteps(lines.slice(1));
} else if (heading.toLowerCase().startsWith('cleanup')) {
cleanup = this.extractSteps(lines.slice(1));
} else if (heading.toLowerCase().startsWith('test:')) {
const testName = heading.replace(/^test:\s*/i, '').trim();
const steps = this.extractSteps(lines.slice(1));
tests.push({ name: testName, setup: [...setup], steps, cleanup: [...cleanup] });
}
}
return tests;
}
async interpretStep(step: string, pageContext?: string): Promise<VibeStep> {
const prompt = `Interpret this natural language test step into a structured action:
Step: "${step}"
${pageContext ? `Page context: ${pageContext}` : ''}
Return JSON: {"action": "navigate|click|fill|select|assert|wait|clear|hover|scroll", "target": "description of element", "value": "value if applicable", "assertion": {"type": "visible|text|url|hidden|contains", "expected": "value"}, "confidence": 0-1}`;
const response = await this.client.messages.create({
model: 'claude-haiku-35-20241022',
max_tokens: 256,
temperature: 0,
messages: [{ role: 'user', content: prompt }],
});
const text = response.content[0].type === 'text' ? response.content[0].text : '';
const jsonMatch = text.match(/\{[\s\S]*\}/);
if (!jsonMatch) {
return this.fallbackInterpret(step);
}
const parsed = JSON.parse(jsonMatch[0]);
return { raw: step, ...parsed };
}
private extractSteps(lines: string[]): string[] {
return lines
.map((line) => line.replace(/^[\d]+\.\s*/, '').replace(/^-\s*/, '').trim())
.filter((line) => line.length > 0);
}
private fallbackInterpret(step: string): VibeStep {
const lower = step.toLowerCase();
if (lower.startsWith('navigate') || lower.startsWith('go to') || lower.startsWith('open')) {
return { raw: step, action: 'navigate', target: step, confidence: 0.7 };
}
if (lower.startsWith('click') || lower.startsWith('press') || lower.startsWith('tap')) {
return { raw: step, action: 'click', target: step.replace(/^(click|press|tap)\s+(on\s+)?/i, ''), confidence: 0.7 };
}
if (lower.startsWith('enter') || lower.startsWith('type') || lower.startsWith('fill')) {
const match = step.match(/["']([^"']+)["']\s+(?:in|into)\s+(.+)/i);
return { raw: step, action: 'fill', target: match?.[2] || '', value: match?.[1] || '', confidence: 0.6 };
}
if (lower.startsWith('verify') || lower.startsWith('check') || lower.startsWith('ensure') || lower.startsWith('confirm')) {
return { raw: step, action: 'assert', target: step, assertion: { type: 'visible' }, confidence: 0.6 };
}
if (lower.startsWith('wait')) {
return { raw: step, action: 'wait', target: step, confidence: 0.7 };
}
if (lower.startsWith('clear')) {
return { raw: step, action: 'clear', target: step.replace(/^clear\s+(the\s+)?/i, ''), confidence: 0.7 };
}
return { raw: step, action: 'assert', target: step, confidence: 0.3 };
}
}
// vibe-tests/engine/element-finder.ts
import { Page, Locator } from '@playwright/test';
export interface FoundElement {
locator: Locator;
selector: string;
confidence: number;
method: 'role' | 'testid' | 'label' | 'text' | 'placeholder' | 'css';
}
export class ElementFinder {
async findElement(page: Page, description: string): Promise<FoundElement> {
const strategies: Array<() => Promise<FoundElement | null>> = [
() => this.findByRole(page, description),
() => this.findByTestId(page, description),
() => this.findByLabel(page, description),
() => this.findByText(page, description),
() => this.findByPlaceholder(page, description),
() => this.findByAccessibilityTree(page, description),
];
for (const strategy of strategies) {
const result = await strategy();
if (result && result.confidence > 0.5) {
return result;
}
}
throw new Error(`Could not find element matching: "${description}"`);
}
private async findByRole(page: Page, description: string): Promise<FoundElement | null> {
const roleMap: Record<string, string> = {
button: 'button', link: 'link', input: 'textbox', field: 'textbox',
checkbox: 'checkbox', radio: 'radio', dropdown: 'combobox', select: 'combobox',
heading: 'heading', tab: 'tab', menu: 'menu', dialog: 'dialog',
alert: 'alert', navigation: 'navigation', search: 'searchbox',
};
for (const [keyword, role] of Object.entries(roleMap)) {
if (description.toLowerCase().includes(keyword)) {
const nameMatch = description.match(/["']([^"']+)["']/);
const name = nameMatch ? nameMatch[1] : undefined;
const locator = name
? page.getByRole(role as any, { name: new RegExp(name, 'i') })
: page.getByRole(role as any);
try {
const count = await locator.count();
if (count === 1) {
return { locator, selector: `getByRole('${role}', { name: '${name || ''}' })`, confidence: 0.9, method: 'role' };
}
} catch {}
}
}
return null;
}
private async findByTestId(page: Page, description: string): Promise<FoundElement | null> {
const words = description.toLowerCase().split(/\s+/);
const possibleIds = [
words.join('-'),
words.join('_'),
words.filter((w) => !['the', 'a', 'an', 'in', 'on', 'at', 'to', 'for'].includes(w)).join('-'),
];
for (const id of possibleIds) {
const locator = page.getByTestId(id);
try {
const count = await locator.count();
if (count === 1) {
return { locator, selector: `getByTestId('${id}')`, confidence: 0.85, method: 'testid' };
}
} catch {}
}
return null;
}
private async findByLabel(page: Page, description: string): Promise<FoundElement | null> {
const labelMatch = description.match(/(?:the\s+)?["']?([^"']+?)["']?\s+(?:field|input|box)/i);
if (labelMatch) {
const locator = page.getByLabel(new RegExp(labelMatch[1], 'i'));
try {
const count = await locator.count();
if (count === 1) {
return { locator, selector: `getByLabel('${labelMatch[1]}')`, confidence: 0.8, method: 'label' };
}
} catch {}
}
return null;
}
private async findByText(page: Page, description: string): Promise<FoundElement | null> {
const textMatch = description.match(/["']([^"']+)["']/);
if (textMatch) {
const locator = page.getByText(textMatch[1], { exact: false });
try {
const count = await locator.count();
if (count === 1) {
return { locator, selector: `getByText('${textMatch[1]}')`, confidence: 0.7, method: 'text' };
}
} catch {}
}
return null;
}
private async findByPlaceholder(page: Page, description: string): Promise<FoundElement | null> {
const keywords = description.toLowerCase();
const placeholderGuesses = [
keywords.includes('email') ? 'email' : null,
keywords.includes('password') ? 'password' : null,
keywords.includes('search') ? 'search' : null,
keywords.includes('name') ? 'name' : null,
].filter(Boolean);
for (const guess of placeholderGuesses) {
const locator = page.getByPlaceholder(new RegExp(guess!, 'i'));
try {
const count = await locator.count();
if (count === 1) {
return { locator, selector: `getByPlaceholder('${guess}')`, confidence: 0.65, method: 'placeholder' };
}
} catch {}
}
return null;
}
private async findByAccessibilityTree(page: Page, description: string): Promise<FoundElement | null> {
const snapshot = await page.accessibility.snapshot();
if (!snapshot) return null;
const matches = this.searchTree(snapshot, description.toLowerCase());
if (matches.length > 0) {
const bestMatch = matches[0];
const locator = page.getByRole(bestMatch.role as any, { name: bestMatch.name });
return {
locator,
selector: `getByRole('${bestMatch.role}', { name: '${bestMatch.name}' })`,
confidence: 0.6,
method: 'role',
};
}
return null;
}
private searchTree(node: any, query: string): any[] {
const matches: any[] = [];
if (node.name && node.name.toLowerCase().includes(query)) {
matches.push(node);
}
if (node.children) {
for (const child of node.children) {
matches.push(...this.searchTree(child, query));
}
}
return matches;
}
}
// vibe-tests/runners/vibe-runner.ts
import { test, expect, Page } from '@playwright/test';
import { VibeInterpreter, VibeTest, VibeStep } from '../engine/interpreter';
import { ElementFinder } from '../engine/element-finder';
import { readFileSync } from 'fs';
export class VibeTestRunner {
private interpreter: VibeInterpreter;
private finder: ElementFinder;
private executionLog: Array<{ step: string; result: string; selector?: string; duration: number }> = [];
constructor() {
this.interpreter = new VibeInterpreter();
this.finder = new ElementFinder();
}
registerTests(specFile: string): void {
const content = readFileSync(specFile, 'utf-8');
const tests = this.interpreter.parseSpecFile(content);
for (const vibeTest of tests) {
test(vibeTest.name, async ({ page }) => {
// Execute setup steps
for (const step of vibeTest.setup) {
await this.executeStep(page, step);
}
// Execute test steps
for (const step of vibeTest.steps) {
await this.executeStep(page, step);
}
// Execute cleanup steps
for (const step of vibeTest.cleanup) {
try {
await this.executeStep(page, step);
} catch {
// Cleanup failures should not fail the test
}
}
});
}
}
private async executeStep(page: Page, step: string): Promise<void> {
const startTime = Date.now();
const interpreted = await this.interpreter.interpretStep(step);
try {
switch (interpreted.action) {
case 'navigate':
await this.executeNavigate(page, interpreted);
break;
case 'click':
await this.executeClick(page, interpreted);
break;
case 'fill':
await this.executeFill(page, interpreted);
break;
case 'assert':
await this.executeAssert(page, interpreted);
break;
case 'wait':
await this.executeWait(page, interpreted);
break;
case 'clear':
await this.executeClear(page, interpreted);
break;
case 'hover':
await this.executeHover(page, interpreted);
break;
default:
throw new Error(`Unknown action: ${interpreted.action}`);
}
this.executionLog.push({
step,
result: 'passed',
duration: Date.now() - startTime,
});
} catch (error: any) {
this.executionLog.push({
step,
result: `failed: ${error.message}`,
duration: Date.now() - startTime,
});
throw error;
}
}
private async executeNavigate(page: Page, step: VibeStep): Promise<void> {
const urlMatch = step.target?.match(/(https?:\/\/[^\s]+|\/[^\s]*)/);
if (urlMatch) {
await page.goto(urlMatch[1]);
} else if (step.target?.toLowerCase().includes('login')) {
await page.goto('/login');
} else if (step.target?.toLowerCase().includes('dashboard')) {
await page.goto('/dashboard');
} else {
await page.goto('/');
}
await page.waitForLoadState('networkidle');
}
private async executeClick(page: Page, step: VibeStep): Promise<void> {
const element = await this.finder.findElement(page, step.target || step.raw);
await element.locator.click();
}
private async executeFill(page: Page, step: VibeStep): Promise<void> {
const element = await this.finder.findElement(page, step.target || step.raw);
await element.locator.fill(step.value || '');
}
private async executeAssert(page: Page, step: VibeStep): Promise<void> {
if (step.assertion?.type === 'url') {
await expect(page).toHaveURL(new RegExp(step.assertion.expected || ''));
} else if (step.assertion?.type === 'visible' && step.target) {
const element = await this.finder.findElement(page, step.target);
await expect(element.locator).toBeVisible();
} else if (step.assertion?.type === 'hidden' && step.target) {
const element = await this.finder.findElement(page, step.target);
await expect(element.locator).toBeHidden();
} else if (step.assertion?.type === 'contains' && step.target) {
const element = await this.finder.findElement(page, step.target);
await expect(element.locator).toContainText(step.assertion.expected || '');
} else if (step.assertion?.type === 'text' && step.target) {
const element = await this.finder.findElement(page, step.target);
await expect(element.locator).toHaveText(step.assertion.expected || '');
}
}
private async executeWait(page: Page, step: VibeStep): Promise<void> {
const timeMatch = step.raw.match(/(\d+)\s*(seconds?|s|ms|milliseconds?)/i);
if (timeMatch) {
const ms = timeMatch[2].startsWith('s') ? parseInt(timeMatch[1]) * 1000 : parseInt(timeMatch[1]);
await page.waitForTimeout(ms);
} else {
await page.waitForLoadState('networkidle');
}
}
private async executeClear(page: Page, step: VibeStep): Promise<void> {
const element = await this.finder.findElement(page, step.target || step.raw);
await element.locator.clear();
}
private async executeHover(page: Page, step: VibeStep): Promise<void> {
const element = await this.finder.findElement(page, step.target || step.raw);
await element.locator.hover();
}
getExecutionLog() {
return [...this.executionLog];
}
}
development
Build WebdriverIO E2E suites — wdio.conf.ts setup, $ and $$ selectors, auto-wait and waitUntil, Mocha framework structure, page objects, parallel capabilities, and services for visual testing and Appium mobile.
testing
Test Vue 3 components with Vue Test Utils and Vitest — mount vs shallowMount, finding and triggering DOM, asserting props and emitted events, awaiting async updates, and mocking Pinia stores and Vue Router.
testing
Write fast unit and integration tests with Vitest — vitest.config.ts setup, vi.fn and vi.mock module mocking, fake timers, snapshots, V8 coverage with thresholds, workspaces for monorepos, and in-source testing.
development
Practice strict red-green-refactor test-driven development — write one failing test first, make it pass with the minimum code, then refactor under green, with worked cycles in Jest and pytest, AAA structure, and behavior-based test naming.