Test Engineer logo

Test Engineer

Community
wasintoh
test-engineer

Automated E2E testing with Playwright including the auto-fix loop — generate test cases from the UI, run them, fix failures and re-run until passing, then produce a human-readable report. Test until it passes, not just test and report. Drives /toh-test; use whenever tests must be written, run, or made green.

Overview

Publisherwasintoh
Repositorytoh-framework
Skill nametest-engineer
Stars
96
Forks
19
Bundled files
Instructions only
LicenseMIT
Links
  • Markdown instructions

    A SKILL.md file the model loads on demand, so it only costs tokens when a request actually matches.

  • Works with any LLM

    AI skills are plain Markdown, not provider-specific code, so this works with GPT, Claude, Gemini, Grok, or a local model.

  • Self-contained

    Everything the model needs lives in the instructions — no extra files to sync.

  • Open source

    Published by wasintoh on GitHub. Read the source before you install it.

Installation

Install the Test Engineer AI skill in TypingMind to use it with any LLM, or drop it into another agent that reads SKILL.md.

1

Install in TypingMind

TypingMind installs a skill straight from its GitHub folder — it reads SKILL.md, bundles the resource files, and stores the result locally.

  1. Open the app and go to Plugins → Skills.
  2. Choose "Install from GitHub".
  3. Paste the skill folder URL below and confirm.
  4. Enable the skill in any chat where you want it available.
Plugins → Skills → Add skill → From GitHub URL, then paste the folder URL and press Continue.
2

Install in another agent

Any agent that reads the Agent Skills format can use this skill — copy the folder into that agent's skills directory.

Claude Code — .claude/skills
git clone --depth 1 https://github.com/wasintoh/toh-framework.git /tmp/toh-framework
mkdir -p .claude/skills
cp -r /tmp/toh-framework/src/skills/test-engineer .claude/skills/test-engineer
Restart Claude Code after copying so it picks up the new skill.

Use it in TypingMind

Enable Test Engineer in any TypingMind chat and the model takes it from there. Its name and description sit in the system prompt, and the moment a request matches, the model loads the full instructions itself — you never invoke it by hand, and it costs no tokens until it is actually used.

The model loads Test Engineer on its own as soon as a request matches it.

Works with any AI model

AI skills are plain Markdown instructions rather than provider-specific code, so Test Engineer is not tied to the model it was written for. Install it once in TypingMind and use it with GPT-5, Claude, Gemini, Grok, DeepSeek, Mistral, Llama, or a local model you run yourself — all on your own API keys.

  • Loaded only when it is needed

    The system prompt carries just the name and description. The instructions are fetched on the first matching request, so an idle skill costs nothing.

  • Switch models mid-chat

    Because the skill is instructions rather than code, changing model does not break it — the next model reads the same SKILL.md.

Skill instructions

This is the SKILL.md content the model loads. Read it before installing — a skill is instructions your model will follow.

Test Engineer Skill

Overview

Skill for automated testing with Playwright, including auto-fix loop capability.

Core Philosophy

"Test until it passes, not just test and report"

  1. Auto-Generate Tests - Generate test cases from UI automatically
  2. Auto-Fix Loop - If fails, fix and re-test until passing
  3. Human-Readable Reports - Easy to understand reports
  4. Language-Adaptive - Error messages adapt to project language setting

Tech Stack

ToolPurpose
PlaywrightE2E Testing
@playwright/testTest Runner
playwright-reportHTML Reports

Setup

1. Install Playwright

bash
npm install -D @playwright/test
npx playwright install

2. Config File

Create playwright.config.ts:

typescript
import { defineConfig, devices } from '@playwright/test'

export default defineConfig({
  testDir: './tests',
  fullyParallel: true,
  forbidOnly: !!process.env.CI,
  retries: process.env.CI ? 2 : 0,
  workers: process.env.CI ? 1 : undefined,
  reporter: [
    ['html'],
    ['list']
  ],
  use: {
    baseURL: 'http://localhost:3000',
    trace: 'on-first-retry',
    screenshot: 'only-on-failure',
    video: 'retain-on-failure',
  },
  projects: [
    {
      name: 'chromium',
      use: { ...devices['Desktop Chrome'] },
    },
    {
      name: 'Mobile Chrome',
      use: { ...devices['Pixel 5'] },
    },
  ],
  webServer: {
    command: 'npm run dev',
    url: 'http://localhost:3000',
    reuseExistingServer: !process.env.CI,
    timeout: 120 * 1000,
  },
})

3. Test Directory Structure

tests/
├── auth/
│   ├── login.spec.ts
│   └── register.spec.ts
├── dashboard/
│   └── dashboard.spec.ts
├── products/
│   ├── list.spec.ts
│   └── detail.spec.ts
├── checkout/
│   └── flow.spec.ts
└── fixtures/
    └── test-data.ts

Test Generation Patterns

Pattern 1: Page Render Test

Every page must have a test to verify correct rendering:

typescript
import { test, expect } from '@playwright/test'

test.describe('Products Page', () => {
  test('should render correctly', async ({ page }) => {
    await page.goto('/products')
    
    // Check title
    await expect(page).toHaveTitle(/Products/)
    
    // Check main heading
    await expect(
      page.getByRole('heading', { name: 'All Products' })
    ).toBeVisible()
    
    // Check key elements exist
    await expect(page.getByTestId('product-grid')).toBeVisible()
    await expect(page.getByRole('searchbox')).toBeVisible()
  })
})

Pattern 2: Form Validation Test

Every form must have validation tests:

typescript
test.describe('Register Form', () => {
  test.beforeEach(async ({ page }) => {
    await page.goto('/register')
  })

  test('should show validation errors for empty fields', async ({ page }) => {
    // Click submit without filling
    await page.getByRole('button', { name: 'Register' }).click()
    
    // Check error messages
    await expect(page.getByText('Name is required')).toBeVisible()
    await expect(page.getByText('Email is required')).toBeVisible()
    await expect(page.getByText('Password is required')).toBeVisible()
  })

  test('should validate email format', async ({ page }) => {
    await page.getByLabel('Email').fill('invalid-email')
    await page.getByRole('button', { name: 'Register' }).click()
    
    await expect(page.getByText('Invalid email format')).toBeVisible()
  })

  test('should validate password strength', async ({ page }) => {
    await page.getByLabel('Password').fill('123')
    await page.getByRole('button', { name: 'Register' }).click()
    
    await expect(page.getByText('Password must be at least 8 characters')).toBeVisible()
  })
})

Pattern 3: User Flow Test

Test complete user journey:

typescript
test.describe('Checkout Flow', () => {
  test('should complete purchase successfully', async ({ page }) => {
    // Step 1: Browse products
    await page.goto('/products')
    await expect(page.getByTestId('product-card')).toHaveCount.greaterThan(0)
    
    // Step 2: Add to cart
    await page.getByTestId('product-card').first().click()
    await page.getByRole('button', { name: 'Add to Cart' }).click()
    await expect(page.getByTestId('cart-count')).toHaveText('1')
    
    // Step 3: Go to cart
    await page.getByTestId('cart-icon').click()
    await expect(page).toHaveURL('/cart')
    await expect(page.getByTestId('cart-item')).toHaveCount(1)
    
    // Step 4: Checkout
    await page.getByRole('button', { name: 'Checkout' }).click()
    await expect(page).toHaveURL('/checkout')
    
    // Step 5: Fill shipping info
    await page.getByLabel('Full Name').fill('John Smith')
    await page.getByLabel('Address').fill('123 Main Street')
    await page.getByLabel('Phone').fill('555-123-4567')
    
    // Step 6: Confirm order
    await page.getByRole('button', { name: 'Confirm Order' }).click()
    
    // Step 7: Success
    await expect(page).toHaveURL(/\/order\//)
    await expect(page.getByText('Order Successful')).toBeVisible()
  })
})

Pattern 4: Responsive Test

Test on multiple viewports:

typescript
test.describe('Responsive Design', () => {
  const viewports = [
    { name: 'mobile', width: 375, height: 667 },
    { name: 'tablet', width: 768, height: 1024 },
    { name: 'desktop', width: 1440, height: 900 },
  ]

  for (const viewport of viewports) {
    test(`should display correctly on ${viewport.name}`, async ({ page }) => {
      await page.setViewportSize({ 
        width: viewport.width, 
        height: viewport.height 
      })
      
      await page.goto('/products')
      
      // Check layout adapts
      if (viewport.name === 'mobile') {
        await expect(page.getByTestId('mobile-menu')).toBeVisible()
        await expect(page.getByTestId('desktop-nav')).not.toBeVisible()
      } else {
        await expect(page.getByTestId('desktop-nav')).toBeVisible()
      }
      
      // Screenshot for visual comparison
      await page.screenshot({ 
        path: `screenshots/products-${viewport.name}.png`,
        fullPage: true 
      })
    })
  }
})

Auto-Fix Loop Strategy

Loop Flow

┌─────────────────────────────────────────────────────┐
│  Run Tests                                          │
└─────────────────────────────────────────────────────┘
            ┌───────────┴───────────┐
            │                       │
            ▼                       ▼
      ┌──────────┐           ┌──────────┐
      │  PASS ✅ │           │  FAIL ❌ │
      └──────────┘           └──────────┘
            │                       │
            ▼                       ▼
      ┌──────────┐           ┌──────────────────┐
      │  Done!   │           │  Analyze Error   │
      └──────────┘           └──────────────────┘
                            ┌──────────────────┐
                            │  Call /toh-fix   │
                            └──────────────────┘
                            ┌──────────────────┐
                            │  Re-run Tests    │
                            │  (max 3 loops)   │
                            └──────────────────┘
                            ┌──────────────────┐
                            │  Still failing?  │
                            └──────────────────┘
                    ┌───────────────┴───────────────┐
                    │                               │
                    ▼                               ▼
              ┌──────────┐                   ┌──────────────┐
              │  PASS ✅ │                   │  Report to   │
              └──────────┘                   │  Human 🧑‍💻    │
                                             └──────────────┘

Error Analysis Matrix

Error PatternRoot CauseAuto-Fix Strategy
strict mode violationMultiple elements match selectorUse more specific selector
Timeout waiting for selectorElement doesn't appearAdd wait or check condition
expect.toBeVisible failedElement hidden/not renderedCheck state/condition
Navigation timeoutPage loads slowlyIncrease timeout or optimize
net::ERR_CONNECTION_REFUSEDServer not startedCheck webServer config
Element is not clickableElement is overlaidScroll into view or wait

Fix Context Template

When calling /toh-fix, send this context:

markdown
## Test Failure Report

**File:** tests/login.spec.ts
**Test:** should login successfully
**Line:** 25

### Error Message

Error: locator.click: Error: strict mode violation: getByRole('button', { name: 'Login' }) resolved to 2 elements


### Code Context
```typescript
// Line 23-27
await page.getByLabel('Password').fill('password123')
await page.getByRole('button', { name: 'Login' }).click() // ← Error here
await expect(page).toHaveURL('/dashboard')

Screenshot

failure

Suggested Fixes

  1. Use getByRole('button', { name: 'Login', exact: true })
  2. Use getByTestId('login-submit-button')
  3. Use .first() or .nth(0)

## Report Format

### Console Output (Short & Concise)

🧪 Running tests...

✓ auth/login.spec.ts (3 tests) - 2.1s ✓ auth/register.spec.ts (4 tests) - 3.2s ✗ products/list.spec.ts (5 tests) - 4.5s └── ❌ should filter by category (attempt 1/3) 🔧 Auto-fixing... └── ✓ Fixed! Re-running... └── ✓ should filter by category (passed) ✓ checkout/flow.spec.ts (2 tests) - 5.1s

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ✅ All tests passed! Total: 14 | Passed: 14 | Fixed: 1 Duration: 15.2s ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━


### Full Report (HTML)

Generate HTML report at:
- `playwright-report/index.html`

View with:
```bash
npx playwright show-report

Best Practices

1. Use data-testid

Add data-testid to important elements:

tsx
// ✅ Good
<button data-testid="submit-order">Order Now</button>

// ❌ Bad - text might change
<button>Order Now</button>

2. Wait for Network Idle

For pages that load data:

typescript
await page.goto('/products', { waitUntil: 'networkidle' })

3. Use Locator Assertions

typescript
// ✅ Good - Auto-retry
await expect(page.getByText('Success')).toBeVisible()

// ❌ Bad - No retry
const text = await page.textContent('.message')
expect(text).toBe('Success')

4. Group Related Tests

typescript
test.describe('Product Management', () => {
  test.describe('Create', () => {
    test('should create new product', ...)
    test('should validate required fields', ...)
  })
  
  test.describe('Edit', () => {
    test('should edit existing product', ...)
  })
  
  test.describe('Delete', () => {
    test('should delete product', ...)
    test('should confirm before delete', ...)
  })
})

5. Use Fixtures for Test Data

typescript
// tests/fixtures/test-data.ts
export const testUser = {
  email: 'test@example.com',
  password: 'TestPassword123!',
  name: 'Test User',
}

export const testProduct = {
  name: 'Drip Coffee',
  price: 4.50,
  category: 'Beverages',
}

Integration Commands

bash
# Run all tests
/toh-test

# Run specific file
/toh-test auth/login

# Run with UI mode (debug)
/toh-test --debug

# Update snapshots
/toh-test --update-snapshots

# Run on CI
/toh-test --ci

MCP Integration

Use Playwright MCP for:

  • Browser automation
  • Screenshot capture
  • Network interception
  • Console log capture
typescript
// Example: Using Playwright MCP
const browser = await playwright.chromium.launch()
const page = await browser.newPage()

// MCP handles the rest...

Frequently asked questions

What does the Test Engineer AI skill do?

Automated E2E testing with Playwright including the auto-fix loop — generate test cases from the UI, run them, fix failures and re-run until passing, then produce a human-readable report. Test until it passes, not just test and report. Drives /toh-test; use whenever tests must be written, run, or made green.

Why use Test Engineer on TypingMind?

Because you install it once and use it with any model. Test Engineer is plain Markdown rather than provider-specific code, so the same skill runs on GPT-5, Claude, Gemini, Grok, or a local model — and you can switch model mid-chat without it breaking. TypingMind runs on your own API keys, so you pay providers directly instead of a per-seat subscription, and your skills and chats stay in your own storage.

How do I install Test Engineer in TypingMind?

Open Plugins → Skills → Install from GitHub in TypingMind and paste https://github.com/wasintoh/toh-framework/tree/main/src/skills/test-engineer. TypingMind reads its SKILL.md and installs it as a skill you can enable per chat.

Which AI models can use Test Engineer?

Any model you connect in TypingMind. AI skills are plain Markdown instructions rather than provider-specific code, so GPT, Claude, Gemini, Grok, and local models can all load this skill when a request matches it.

How many AI models can I use with Test Engineer?

As many as you like. As long as a model supports skills, you can use Test Engineer with it — GPT, Claude, Gemini, Grok, DeepSeek, Mistral, Llama and more — all on TypingMind with your own API keys.

Is the Test Engineer AI skill free?

Yes. It is published on GitHub by wasintoh under the MIT license. You only pay your own AI provider for the tokens you use.

What are AI skills?

An AI skill is a reusable instruction bundle that teaches an AI model how to do one specific task. It follows the open Agent Skills format: a SKILL.md file with a name and description, plus any scripts, templates or reference files the model may need. The model reads the instructions only when your request matches the skill, so an installed skill costs nothing until it is used.

How are AI skills different from plugins or MCP servers?

A plugin or MCP server gives a model new tools to call — code that runs somewhere and returns a result. An AI skill gives the model knowledge and process instead: how to approach a task, which steps to follow, what good output looks like. Skills are plain Markdown, so they need no server, no API key and no runtime, and they work with any model.

View all

Set up your own AI workspace now

Get notified about new features and future giveaways by subscribing to our newsletter 👇