agentsclimarketplace

Vibe e2e

Skill aakashdhar/vibe-skill/vibe-e2e

vibe-* is a collection of Claude Code CLI skills. Each skill is a structured instruction set that tells Claude Code exactly what to do at a specific moment in a project — how to plan it, build it, review it, test it, deploy it, and hand it off.

Install
npx -y skills add aakashdhar/vibe-skill --skill vibe-e2e

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

2 things to look at

  • no licenseNo license file was found in the repository. Code published without one is not open source by default, so using it at work is a question for whoever answers licensing questions where you are.
  • 6 stars6 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

What its author says it does

Copied from the file, not written here

Generates and runs full end-to-end UI tests using Playwright against a live URL. Auto-installs Playwright and Chromium if not present — isolated in e2e/ folder, never pollutes app dependencies. Reads SPEC.md and FEATURES.md to automatically generate test flows covering every critical user journey. Runs tests with video recording on failure and HTML report. Two-device flows (poster + joiner, sender + receiver) use parallel browser contexts. OAuth flows are skipped with clear warnings. Generates a cleanup script that deletes all test data after the suite runs. e2e: regenerate rewrites all tests from current SPEC.md. Triggers: "e2e: https://url" to run against a live URL, "e2e: regenerate" to rewrite tests from spec.

SKILL.md

20.9 KB, as published. Nobody here has run it

Vibe E2E Skill

Generates Playwright E2E tests from your SPEC.md and FEATURES.md. Runs them against a live URL. Records video on failure. Cleans up test data after every run.


Triggers

TriggerWhat it does
e2e: https://urlGenerate tests (if none exist) + run against URL
e2e: run https://urlRun existing tests against URL without regenerating
e2e: regenerateRewrite all test files from current SPEC.md + FEATURES.md
e2e: cleanup https://urlRun cleanup script only — delete test data

Step 0 — Install Playwright if missing

Before anything else, check if Playwright is available and install if not. Install into an isolated e2e/ folder — never touch the app's package.json.

# Check if e2e folder exists with Playwright already installed
ls e2e/node_modules/@playwright/test 2>/dev/null && echo "INSTALLED" || echo "NOT_INSTALLED"

If not installed:

# Create e2e folder if it doesn't exist
mkdir -p e2e

# Detect project package manager
ls yarn.lock 2>/dev/null && echo "yarn" || ls pnpm-lock.yaml 2>/dev/null && echo "pnpm" || echo "npm"

# Install Playwright into e2e/ only
cd e2e

# If npm:
npm init -y
npm install -D @playwright/test

# If yarn:
yarn init -y
yarn add -D @playwright/test

# If pnpm:
pnpm init
pnpm add -D @playwright/test

# Install Chromium browser only (sufficient for QA, smallest download)
npx playwright install chromium --with-deps

If project is Python-only (no package.json anywhere):

pip install playwright
playwright install chromium

Tell the user: "Installing Playwright into e2e/ — this takes ~30 seconds for the first install."


Step 1 — Read project context

cat vibe/SPEC.md 2>/dev/null
cat vibe/FEATURES.md 2>/dev/null || cat FEATURES.md 2>/dev/null
cat vibe/ARCHITECTURE.md 2>/dev/null
cat BRIEF.md 2>/dev/null

Extract from these files:

  • Every user-facing feature
  • Acceptance criteria (from SPEC.md) — these become assertions
  • Auth method — email/password, OAuth providers, phone OTP
  • Multi-user flows — any flow requiring two users simultaneously
  • Admin-gated features
  • Time-sensitive behaviour (e.g. "ride appears within 10 seconds")
  • API endpoints used by the frontend (for cleanup script)

Step 2 — Generate flow list

Before writing any test code, show the user the flows Claude has identified. Group by domain. Number sequentially. Flag multi-device flows and OAuth flows explicitly.

Flows detected from SPEC.md + FEATURES.md:

  AUTH
  01 · Register → complete profile → land on home          [single device]
  02 · Login with email and password                        [single device]
  03 · Login with Google OAuth                              [⚠️ SKIP — OAuth]
  04 · Login with Apple OAuth                               [⚠️ SKIP — OAuth]
  05 · Forgot password flow                                 [single device]

  CORE
  06 · Post a ride → appears in discover feed within 10s    [single device]
  07 · Discover → filter by vibe tag → request to join      [single device]
  08 · Poster approves join → joiner sees approval          [TWO DEVICES]
  09 · Approved joiner opens group chat → messages appear   [TWO DEVICES]

  TRIBE
  10 · Send tribe invite → other user accepts               [TWO DEVICES]
  11 · Direct message between tribe members                 [TWO DEVICES]

  ROUTES
  12 · Browse routes → filter → tap route → Plan a Ride    [single device]
  13 · Submit a route → appears in feed                     [single device]

  ADMIN
  14 · Admin: paste URL → Gemini extracts → publish route   [single device]

  PROFILE
  15 · Edit profile → change vibe tags → verify saved       [single device]
  16 · Post-ride rating → poster profile shows rating       [TWO DEVICES]

  ──────────────────────────────────────────────
  Total: 16 flows · 4 skipped (OAuth) · 6 two-device · 6 single-device

Generating Playwright tests for 12 active flows...

Wait briefly (2-3 seconds) then proceed with generation — no need for user confirmation unless the flow list looks wrong.


Step 3 — Generate file structure

e2e/
├── package.json
├── playwright.config.ts
├── tests/
│   ├── 01-auth.spec.ts
│   ├── 02-core-flow.spec.ts
│   ├── 03-tribe.spec.ts
│   ├── 04-routes.spec.ts
│   ├── 05-admin.spec.ts
│   ├── 06-profile.spec.ts
│   └── helpers/
│       ├── auth.ts          ← login/register helpers
│       ├── fixtures.ts      ← test data with timestamps
│       ├── selectors.ts     ← centralised UI selectors
│       └── cleanup.ts       ← delete all test data via API
├── cleanup.ts               ← standalone cleanup script
├── .gitignore
└── test-results/            ← generated on run, gitignored

Step 4 — Generate each file

e2e/package.json

{
  "name": "[project]-e2e",
  "version": "1.0.0",
  "private": true,
  "scripts": {
    "test": "playwright test",
    "test:headed": "playwright test --headed",
    "report": "playwright show-report e2e/report",
    "cleanup": "npx ts-node cleanup.ts"
  },
  "devDependencies": {
    "@playwright/test": "^1.44.0",
    "ts-node": "^10.9.2",
    "typescript": "^5.4.0"
  }
}

e2e/playwright.config.ts

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

export default defineConfig({
  testDir: './tests',
  timeout: 45000,
  expect: { timeout: 10000 },
  retries: process.env.CI ? 2 : 1,
  workers: process.env.CI ? 1 : 2,
  fullyParallel: false,

  use: {
    baseURL: process.env.BASE_URL,
    headless: true,
    screenshot: 'only-on-failure',
    video: 'retain-on-failure',
    trace: 'retain-on-failure',
    actionTimeout: 15000,
    navigationTimeout: 30000,
  },

  projects: [
    { name: 'chromium', use: { ...devices['Desktop Chrome'] } },
  ],

  reporter: [
    ['list'],
    ['html', { outputFolder: 'report', open: 'never' }],
    ['json', { outputFile: 'report/results.json' }],
  ],

  outputDir: 'test-results',
})

e2e/tests/helpers/fixtures.ts

Generate timestamps so every test run creates unique data and never conflicts:

const RUN_ID = Date.now()

export const USER_A = {
  email: `qa-a-${RUN_ID}@e2e-test.invalid`,
  password: 'E2eTest123!',
  name: `QA Rider A ${RUN_ID}`,
  bike: 'Royal Enfield Classic 350',
  city: 'Pune',
}

export const USER_B = {
  email: `qa-b-${RUN_ID}@e2e-test.invalid`,
  password: 'E2eTest123!',
  name: `QA Rider B ${RUN_ID}`,
  bike: 'KTM Duke 390',
  city: 'Pune',
}

export const TEST_RIDE = {
  destination: `E2E Test Ride ${RUN_ID}`,
  date: (() => {
    const d = new Date()
    d.setDate(d.getDate() + 1)
    return d.toISOString().split('T')[0]
  })(),
  maxRiders: 3,
}

export const TEST_ROUTE = {
  name: `E2E Test Route ${RUN_ID}`,
  distance: '45km',
  duration: '2h',
  startLocation: 'Pune',
}

// Track created entity IDs for cleanup
export const CREATED: {
  userIds: string[]
  rideIds: string[]
  routeIds: string[]
} = {
  userIds: [],
  rideIds: [],
  routeIds: [],
}

e2e/tests/helpers/auth.ts

import { Page } from '@playwright/test'
import { USER_A, USER_B, CREATED } from './fixtures'

export async function register(page: Page, user = USER_A) {
  await page.goto('/register')

  // Step 1 — email + password
  await page.getByLabel(/email/i).fill(user.email)
  await page.getByLabel(/password/i).first().fill(user.password)
  await page.getByRole('button', { name: /continue|next|sign up/i }).click()

  // Step 2 — profile setup (adjust selectors to match actual UI)
  await page.getByLabel(/display name|name/i).fill(user.name)

  // Bike field
  const bikeField = page.getByLabel(/bike/i)
  if (await bikeField.isVisible()) await bikeField.fill(user.bike)

  // City field
  const cityField = page.getByLabel(/city/i)
  if (await cityField.isVisible()) await cityField.fill(user.city)

  await page.getByRole('button', { name: /create|finish|done/i }).click()
  await page.waitForURL(/discover|home|dashboard/, { timeout: 15000 })

  // Extract and store user ID for cleanup
  const cookies = await page.context().cookies()
  const token = cookies.find(c => c.name === 'auth-token' || c.name === 'token')
  if (token) {
    // Decode JWT to get user ID (base64 middle segment)
    try {
      const payload = JSON.parse(atob(token.value.split('.')[1]))
      if (payload.sub || payload.user_id || payload.id) {
        CREATED.userIds.push(payload.sub || payload.user_id || payload.id)
      }
    } catch {}
  }
}

export async function login(page: Page, user = USER_A) {
  await page.goto('/login')
  await page.getByLabel(/email/i).fill(user.email)
  await page.getByLabel(/password/i).fill(user.password)
  await page.getByRole('button', { name: /log in|sign in|continue/i }).click()
  await page.waitForURL(/discover|home|dashboard/, { timeout: 15000 })
}

export async function registerAndLogin(page: Page, user = USER_A) {
  await register(page, user)
}

e2e/tests/helpers/cleanup.ts

import { APIRequestContext } from '@playwright/test'
import { CREATED } from './fixtures'

export async function cleanupTestData(request: APIRequestContext, baseURL: string, adminToken?: string) {
  const headers = adminToken
    ? { Authorization: `Bearer ${adminToken}` }
    : {}

  console.log(`\nCleaning up test data...`)

  // Delete test rides
  for (const rideId of CREATED.rideIds) {
    try {
      await request.delete(`${baseURL}/api/rides/${rideId}`, { headers })
      console.log(`  ✓ Deleted ride ${rideId}`)
    } catch (e) {
      console.log(`  ⚠ Could not delete ride ${rideId}: ${e}`)
    }
  }

  // Delete test routes
  for (const routeId of CREATED.routeIds) {
    try {
      await request.delete(`${baseURL}/api/routes/${routeId}`, { headers })
      console.log(`  ✓ Deleted route ${routeId}`)
    } catch (e) {
      console.log(`  ⚠ Could not delete route ${routeId}: ${e}`)
    }
  }

  // Delete test users (last — after rides/routes are gone)
  for (const userId of CREATED.userIds) {
    try {
      await request.delete(`${baseURL}/api/users/${userId}`, { headers })
      console.log(`  ✓ Deleted user ${userId}`)
    } catch (e) {
      console.log(`  ⚠ Could not delete user ${userId}: ${e}`)
    }
  }

  console.log(`Cleanup complete.\n`)
}

e2e/cleanup.ts (standalone script)

import { chromium } from '@playwright/test'
import { CREATED } from './tests/helpers/fixtures'
import { cleanupTestData } from './tests/helpers/cleanup'

const BASE_URL = process.env.BASE_URL
if (!BASE_URL) {
  console.error('ERROR: BASE_URL environment variable required')
  console.error('Usage: BASE_URL=https://your-app.railway.app npm run cleanup')
  process.exit(1)
}

;(async () => {
  const browser = await chromium.launch()
  const context = await browser.newContext()
  const request = context.request

  await cleanupTestData(request, BASE_URL)
  await browser.close()
})()

Test specs — one per domain

Generate each spec fully from the actual flow list detected in Step 2. Every assertion comes directly from acceptance criteria in SPEC.md.

Pattern for single-device tests:

// 01-auth.spec.ts
import { test, expect, request } from '@playwright/test'
import { register, login } from './helpers/auth'
import { USER_A, USER_B } from './fixtures'
import { cleanupTestData } from './helpers/cleanup'

const BASE_URL = process.env.BASE_URL!

test.afterAll(async () => {
  const ctx = await request.newContext()
  await cleanupTestData(ctx, BASE_URL)
})

test.describe('Authentication', () => {

  test('01 · register → complete profile → land on discover', async ({ page }) => {
    await register(page, USER_A)
    await expect(page).toHaveURL(/discover/)
    await expect(page.getByText(/upcoming rides|discover/i)).toBeVisible()
  })

  test('02 · login with email and password', async ({ page }) => {
    // Register first if not done
    await register(page, USER_B)
    await page.goto('/logout')
    await login(page, USER_B)
    await expect(page).toHaveURL(/discover/)
  })

  // OAuth tests — skipped with explanation
  test.skip('03 · login with Google OAuth — skipped: requires real OAuth credentials', async ({ page }) => {
    // To enable: set E2E_GOOGLE_EMAIL and E2E_GOOGLE_PASSWORD env vars
    // and implement OAuth flow using those credentials
  })

  test.skip('04 · login with Apple OAuth — skipped: requires real Apple credentials', async ({ page }) => {
    // Apple OAuth cannot be automated without a real Apple Developer account
    // Test manually: Sign in with Apple button → Apple ID login → redirects to app
  })

})

Pattern for two-device flows (parallel browser contexts):

// 02-core-flow.spec.ts — excerpt showing two-device pattern
test('08 · poster approves join → joiner sees approval notification', async ({ browser }) => {
  // Create two independent browser contexts — simulates two real devices
  const posterCtx  = await browser.newContext()
  const joinerCtx  = await browser.newContext()
  const posterPage = await posterCtx.newPage()
  const joinerPage = await joinerCtx.newPage()

  try {
    // Register both users
    await register(posterPage, USER_A)
    await register(joinerPage, USER_B)

    // USER_A posts a ride
    await posterPage.goto('/post-ride')
    await posterPage.getByLabel(/destination/i).fill(TEST_RIDE.destination)
    // ... fill other fields
    await posterPage.getByRole('button', { name: /post|create/i }).click()
    await expect(posterPage.getByText(TEST_RIDE.destination)).toBeVisible()

    // USER_B discovers the ride and requests to join
    await joinerPage.goto('/discover')
    await joinerPage.getByText(TEST_RIDE.destination).click()
    await joinerPage.getByRole('button', { name: /request to join/i }).click()
    await expect(joinerPage.getByText(/request sent|pending/i)).toBeVisible()

    // USER_A approves the join request
    await posterPage.goto('/my-rides')
    await posterPage.getByText(/pending request|join request/i).first().click()
    await posterPage.getByRole('button', { name: /approve|accept/i }).click()

    // USER_B should see approval
    await joinerPage.reload()
    await expect(joinerPage.getByText(/approved|open chat/i)).toBeVisible({ timeout: 15000 })

  } finally {
    await posterCtx.close()
    await joinerCtx.close()
  }
})

Pattern for time-sensitive assertions (from SPEC acceptance criteria):

test('06 · post a ride → appears in discover feed within 10 seconds', async ({ page }) => {
  await login(page, USER_A)

  await page.goto('/post-ride')
  await page.getByLabel(/destination/i).fill(TEST_RIDE.destination)
  await page.getByLabel(/date/i).fill(TEST_RIDE.date)
  await page.getByRole('button', { name: /post|create/i }).click()

  // SPEC says: ride appears within 10 seconds
  await page.goto('/discover')
  await expect(
    page.getByText(TEST_RIDE.destination)
  ).toBeVisible({ timeout: 10000 })  // exact timeout from acceptance criteria
})

e2e/.gitignore

node_modules/
test-results/
report/
*.webm
*.zip
playwright-report/

Step 5 — Run the suite

cd e2e && BASE_URL=[url] npx playwright test --project=chromium 2>&1

Stream output to terminal in real time as tests run. After run completes, automatically run cleanup:

cd e2e && BASE_URL=[url] npx ts-node cleanup.ts

Step 6 — Report results

Present a clean summary after every run:

E2E REPORT — [Project Name]
URL: [url]
Run: [timestamp]
──────────────────────────────────────────────
01 auth    · Register → profile → discover      ✅  4.2s
02 auth    · Login email/password               ✅  1.8s
03 auth    · Google OAuth                       ⚠️   SKIPPED
04 auth    · Apple OAuth                        ⚠️   SKIPPED
05 core    · Post ride → discover in 10s        ✅  7.1s
06 core    · Discover → filter → join request   ✅  5.4s
07 core    · Approve join → notification        ❌  FAILED  8.2s
08 core    · Group chat send/receive            ⏭   SKIPPED (depends on 07)
09 tribe   · Send invite → accept               ✅  6.8s
10 tribe   · Direct message                     ✅  9.3s
11 routes  · Browse → filter → plan ride        ✅  3.9s
12 routes  · Submit route → appears in feed     ✅  4.1s
13 admin   · URL → Gemini → publish             ✅  11.2s
14 profile · Edit profile → verify saved        ✅  2.3s
──────────────────────────────────────────────
12 passed · 1 failed · 2 skipped (OAuth) · 1 skipped (dependency)

FAILURES
  07 · core · Approve join → joiner notification
       Step: joiner reloads page → expected "approved" text
       Got:  "pending" still showing
       Likely cause: real-time update not propagating — WebSocket or polling issue
       Video:  e2e/test-results/07-core-approve/video.webm
       Trace:  e2e/test-results/07-core-approve/trace.zip
       Open trace: npx playwright show-trace e2e/test-results/07-core-approve/trace.zip

OAUTH WARNINGS (manual testing required)
  03 · Google OAuth — test manually: tap "Sign in with Google" → complete flow → verify redirect
  04 · Apple OAuth  — test manually: tap "Sign in with Apple" → complete flow → verify redirect

CLEANUP
  ✓ Deleted 2 test rides
  ✓ Deleted 2 test users
  Cleanup complete.

VERDICT: ❌ 1 failure — fix before deploy
HTML report: e2e/report/index.html

Step 7 — e2e: regenerate

Rewrites all spec files from current SPEC.md + FEATURES.md. Preserves helpers/ (auth, fixtures, cleanup, selectors) — only rewrites spec files.

Show new flow list first:

Regenerating from SPEC.md + FEATURES.md...

New flows detected (current SPEC):
  [updated flow list]

Changes from previous run:
  + Flow 17 added: Weather badge on discover
  ~ Flow 06 updated: ride now appears in 5s (was 10s per updated SPEC)
  - Flow 14 removed: Admin scraper UI (moved to separate admin suite)

Rewriting spec files... (helpers preserved)
✅ 6 spec files rewritten

Step 8 — Update vibe docs

After a successful run, log to TASKS.md:

✅ E2E suite passed — [date]
   URL: [url]
   Flows: [N] passed · [N] failed · [N] skipped
   Report: e2e/report/index.html

If failures:

⚠️  E2E suite — [N] failures — [date]
   URL: [url]
   Failed: [flow names]
   Videos: e2e/test-results/

Absolute rules

OAuth flows are always skipped — never attempt to automate them. Google and Apple OAuth involve third-party login screens that actively block automation. Mark them as test.skip() with a manual testing note. Never try to interact with Google or Apple login screens.

Test data uses .invalid domain emails. The .invalid TLD is reserved and guaranteed to never deliver email. Use qa-[role]-[timestamp]@[project]-test.invalid — never a real domain.

Cleanup always runs after the suite. Even if tests fail. Wrap cleanup in afterAll hooks AND run the standalone cleanup script. Failing tests still create data on the server.

Two-device tests always use browser.newContext(). Never share auth state between USER_A and USER_B in the same test. Each context is a fresh, independent browser — simulates two real devices. Always close both contexts in a finally block.

Selectors prefer accessible roles over CSS. Use getByRole, getByLabel, getByText before locator('.class'). This makes tests more resilient to UI changes and works across frameworks.

Time-sensitive assertions use exact timeouts from SPEC. If SPEC says "within 10 seconds" — toBeVisible({ timeout: 10000 }). Don't use arbitrary timeouts — they hide real performance regressions.

Never hardcode the base URL. Always read from process.env.BASE_URL. Never fallback to localhost in production test runs.

Video and trace only on failure. video: 'retain-on-failure' — don't record passing tests. Videos and traces are large. Only keep them when they're needed.

Keep looking

Skills are one crate of 328,083. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.