zoonk-testing

Follow TDD (Test-Driven Development) for all features and bug fixes. Always write failing tests first.

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "zoonk-testing" with this command: npx skills add zoonk/zoonk/zoonk-zoonk-zoonk-testing

Testing Guidelines

Follow TDD (Test-Driven Development) for all features and bug fixes. Always write failing tests first.

How to Think About Tests

Before writing any test, answer these questions:

  1. What behavior am I verifying?

State it in one sentence. If you can't, the test is too complex.

  • ✅ "Verify locale persists when navigating between pages"

  • ✅ "Verify users can create a course with a title"

  • ❌ "Verify the course description popover opens in the correct language when clicking from a Portuguese page"

  • ❌ "Verify the sidebar collapses, remembers state, and shows tooltips on hover when collapsed"

  1. What's the simplest proof?

Find the minimum actions to verify the behavior:

Behavior Simplest Proof NOT This

Locale persistence Navigate → click link → check URL contains /pt

Navigate → click course → open popover → verify translation

Course creation Fill title → submit → verify title appears Fill all fields → verify each field → check database → verify list

Login works Enter credentials → submit → see dashboard heading Enter credentials → verify button enabled → submit → check cookie

Item appears in list Create item → verify it's visible Create item → scroll list → filter → sort → find item

  1. Am I testing the right thing?

Ask: "If this test passes, am I confident the feature works?"

  • If "yes" requires trusting other unrelated UI mechanics, you're testing the wrong thing

  • If the test could pass with broken code, it's too loose

  • If the test could fail with working code, it's testing implementation details

Example: To test locale preservation, you don't need to verify translated content renders correctly. That's a translation test, not a locale persistence test. Just verify the URL maintains the locale segment.

TDD Workflow

  • Write a failing test that describes the expected behavior

  • Run the test to confirm it fails - this is non-negotiable

  • Write the minimum code to make the test pass

  • Run the test to confirm it passes

  • Refactor while keeping tests green

If the Test Passes Before Your Fix

The test is wrong. A passing test means one of:

  • The bug doesn't exist (investigate further)

  • The test is matching existing/seeded data instead of new behavior

  • The test assertion is too loose

Never use workarounds to make a "failing" test pass:

// BAD: Using .first() to avoid multiple matches await expect(page.getByText(courseTitle).first()).toBeVisible(); // This passes even if the item existed before your fix!

// GOOD: Use unique identifiers so only ONE element can match const uniqueId = randomUUID().slice(0, 8); const courseTitle = Test Course ${uniqueId}; await expect(page.getByText(courseTitle)).toBeVisible(); // This ONLY passes if your code actually created this specific item

Test Isolation Principle

Core Rule: Tests must be completely self-contained.

This means:

  • Create your own data - Don't rely on seed data existing or having specific values

  • No cleanup needed - If you need afterEach cleanup, your test isn't isolated

  • Parallel safe - Tests should run in any order, even simultaneously

Why Not Seed Data?

Seed data creates hidden dependencies:

  • Tests break when seed data changes

  • Tests pass locally but fail in CI (different seeds)

  • Tests can't run in parallel (shared state)

  • Debugging requires knowing what's seeded

The Pattern

Every test that needs data should create it:

// Create unique data for THIS test const uniqueId = randomUUID().slice(0, 8); const course = await courseFixture({ slug: e2e-${uniqueId}, title: E2E Course ${uniqueId}, });

// Test uses only data it created await page.goto(/courses/${course.slug});

Exception: Structural Dependencies

Using a seeded organization as a container is acceptable because:

  • It's a structural dependency, not a content assertion

  • Tests create their own content within it

  • The org is guaranteed to exist in all environments

// ACCEPTABLE: Using seeded org as container const org = await prisma.organization.findUniqueOrThrow({ where: { slug: "ai" }, }); const course = await courseFixture({ organizationId: org.id });

Exception: Read-Only Route Verification

For verifying that a page renders at all (not specific content), you may use known paths:

// OK: Just verifying the route works test("course detail page renders", async ({ page }) => { await page.goto("/b/ai/c/machine-learning"); // Seeded course await expect(page.getByRole("heading", { level: 1 })).toBeVisible(); });

// NOT OK: Relying on specific seeded content test("shows machine learning description", async ({ page }) => { await page.goto("/b/ai/c/machine-learning"); await expect(page.getByText("patterns|predictions")).toBeVisible(); // Fragile! });

Test Types

When Test Type Framework Location

Apps/UI features E2E Playwright apps/{app}/e2e/

Data functions (Prisma) Integration Vitest apps/{app}/src/data/ or packages/

Utils/helpers Unit Vitest packages/{pkg}/*.test.ts

E2E Testing (Playwright)

Query Priority

Use semantic queries that reflect how users interact with the page:

// GOOD: Semantic queries (in order of preference) page.getByRole("button", { name: "Submit" }); page.getByRole("heading", { name: "Welcome" }); page.getByLabel("Email address"); page.getByText("Sign up for free"); page.getByPlaceholder("Search...");

// BAD: Implementation details page.locator(".btn-primary"); page.locator("#submit-button"); page.locator("[data-testid='submit']"); page.locator("[data-slot='media-card-icon']");

If you can't use getByRole , the component likely has accessibility issues. Fix the component first.

Wait Patterns

// GOOD: Wait for visible state await expect(page.getByRole("heading")).toBeVisible(); await expect(page.getByText("Success")).toBeVisible();

// GOOD: Wait for URL change await page.waitForURL(//dashboard/);

// BAD: Arbitrary delays await page.waitForTimeout(2000);

Animated Elements

Elements with CSS transitions can cause "element is not stable" errors. Pattern: wait for visibility, then use force: true :

// Wait for submenu content to be visible (animation complete) await expect(page.getByRole("menuitem", { name: "English" })).toBeVisible(); // Force click bypasses stability check - safe because we confirmed visibility await page.getByRole("menuitem", { name: "Español" }).click({ force: true });

When to use force: true :

  • After confirming the element is visible via toBeVisible()

  • When CSS animations cause repeated "element is not stable" errors

  • Never as a first resort—always investigate why the element is unstable first

Authentication Fixtures

Use pre-configured fixtures from your test setup:

import { expect, test } from "./fixtures";

test("authenticated user sees dashboard", async ({ authenticatedPage }) => { await authenticatedPage.goto("/"); await expect(authenticatedPage.getByRole("heading", { name: "Dashboard" })).toBeVisible(); });

Creating Test Data

Use Prisma fixtures for tests that need specific data states:

import { postFixture } from "@/tests/fixtures/posts";

async function createTestPost() { const uniqueId = randomUUID().slice(0, 8); return postFixture({ slug: e2e-${uniqueId}, title: E2E Post ${uniqueId}, }); }

test("edits post title", async ({ authenticatedPage }) => { const post = await createTestPost(); await authenticatedPage.goto(/posts/${post.slug}); // ... test editing behavior });

Create unique users for user-specific state:

When testing features that depend on user state (subscriptions, permissions), create a unique user per test:

test("works with subscription", async () => { const uniqueId = randomUUID().slice(0, 8); const email = e2e-test-${uniqueId}@zoonk.test;

// Create unique user via sign-up API const signupContext = await request.newContext({ baseURL }); await signupContext.post("/v1/auth/sign-up/email", { data: { email, name: E2E User ${uniqueId}, password: "password123" }, });

// Create user-specific state const user = await prisma.user.findUniqueOrThrow({ where: { email } }); await prisma.subscription.create({ data: { referenceId: String(user.id), status: "active", plan: "hobby" }, });

// No cleanup needed - user is unique to this test });

Preventing Flaky Tests

Run new tests multiple times before considering them done:

for i in {1..5}; do pnpm e2e -- -g "test name" --reporter=line; done

High-risk scenarios:

Scenario Prevention

Clicking dropdown items Wait for visibility, use force: true

Actions triggering navigation Use waitForLoadState or waitForURL after click

Form submissions Wait for success indicator before next action

Inputs with debounced validation Use waitForLoadState("networkidle") after fill

Server action persistence Assert UI immediately, then DB query with toPass

Server Actions

Server Actions run server-side, so page.route() cannot intercept them. To test error states, trigger real validation errors:

// Whitespace passes HTML5 "required" but fails server-side when trimmed await nameInput.fill(" "); await page.getByRole("button", { name: /submit/i }).click(); await expect(page.getByRole("alert")).toBeVisible();

Verifying Persistence in E2E Tests

When a server action mutates data (toggle, add, remove, reorder), verify two things separately:

  • UI updates immediately (no reload) — tests the user experience

  • Data persisted to DB (via Prisma query) — tests the server action, fast and deterministic

// GOOD: UI assertion + DB assertion await openCategoryPopover(page); await getCategoryOption(page, /technology/i).click(); await page.keyboard.press("Escape");

// 1. Badge appears immediately — user doesn't need to refresh await expect(page.getByText("Technology")).toBeVisible();

// 2. Server action persisted — fast DB check with retry await expect(async () => { const record = await prisma.courseCategory.findUnique({ where: { courseCategory: { category: "tech", courseId: course.id } }, }); expect(record).not.toBeNull(); }).toPass({ timeout: 10_000 });

// BAD: Reloading to verify persistence await page.reload(); await expect(page.getByText("Technology")).toBeVisible(); // This is slow, flaky (caching), and doesn't catch "user must refresh" bugs

When reload IS appropriate: Auto-save flows (type → debounce → "saved" indicator → persist) where the reload verifies the complete UX cycle end-to-end. The user types, sees "saved", and expects data to survive a refresh — that IS the behavior being tested.

// Auto-save flow: reload is the right tool await titleInput.fill(uniqueTitle); await expect(page.getByText(/^saved$/i)).toBeVisible(); await page.reload(); await expect(titleInput).toHaveValue(uniqueTitle);

Decision guide:

Action type Immediate check Persistence check

Server action (click → mutation) UI assertion (no reload) DB query with toPass retry

Auto-save (type → debounce → save) "saved" indicator visible Reload + verify value

URL/cookie state (locale, filters) URL assertion Reload + verify URL

Drag and Drop (dnd-kit)

Use locator.dragTo() with the steps parameter. The steps option emits intermediate mousemove events, which dnd-kit's PointerSensor requires to activate a drag:

const firstHandle = page.getByRole("button", { name: "Drag to reorder" }).first(); const secondHandle = page.getByRole("button", { name: "Drag to reorder" }).nth(1);

await firstHandle.dragTo(secondHandle, { steps: 20 });

Why steps matters: Without steps , Playwright emits a single mousemove at the destination, which isn't enough for dnd-kit's PointerSensor to recognize a drag gesture. Use steps: 20 for smooth, reliable drags.

Non-deterministic landing position: dragTo between adjacent items can produce different results across runs (swap vs move-to-end). This happens because dnd-kit's closestCenter collision detection is sensitive to layout shifts — when an item is "lifted" into the DragOverlay, remaining items shift to fill the gap, moving the drop target. Assert that the order changed, not a specific final order:

// BAD: Asserts a specific order — flaky because drag can land in different positions const reorderedItems = [ { position: 1, title: "Item 2" }, { position: 2, title: "Item 1" }, { position: 3, title: "Item 3" }, ]; await expectItemsVisible(page, reorderedItems);

// GOOD: Asserts that reordering happened — stable regardless of exact landing position const firstItem = page.getByRole("listitem").filter({ hasText: /01/ }); await expect(firstItem.getByRole("link", { name: /item 1/i })).not.toBeVisible();

Common Thinking Mistakes

Over-Testing Through UI Mechanics

Mistake: Testing locale preservation by opening popovers and verifying their content.

Why it's wrong: You're testing popover behavior, not locale persistence.

Fix: Test the simplest proof - URL changes preserve locale.

// BAD: Tests translation rendering, not locale persistence test("preserves locale", async ({ page }) => { await page.goto("/pt/courses/intro"); await page.getByRole("button", { name: /detalhes/i }).click(); await expect(page.getByText(/descrição em português/i)).toBeVisible(); });

// GOOD: Tests actual locale persistence test("preserves locale when navigating", async ({ page }) => { await page.goto("/pt/courses"); await page.getByRole("link", { name: /machine learning/i }).click(); await expect(page).toHaveURL(/^/pt//); });

Testing Implementation Instead of Behavior

Mistake: await expect(page.locator('[data-slot="badge"]')).toBeVisible()

Why it's wrong: You're testing that an attribute exists, not user-visible behavior.

Fix: What does the user see? Test that.

// BAD: Testing CSS implementation await expect(page.locator('[data-slot="badge"]')).toBeVisible();

// GOOD: Testing what user sees await expect(page.getByRole("img", { name: /course thumbnail/i })).toBeVisible();

Relying on Seed Data Content

Mistake: Asserting specific seeded values appear in results.

Why it's wrong: Test breaks if seed data changes; can't run in parallel.

Fix: Create test data with unique identifiers.

// BAD: Depends on seed data test("finds course by title", async ({ page }) => { await page.getByRole("textbox").fill("Machine Learning"); await expect(page.getByText("Introduction to ML")).toBeVisible(); });

// GOOD: Creates its own data test("finds course by title", async ({ page }) => { const uniqueId = randomUUID().slice(0, 8); await courseFixture({ title: Search Test ${uniqueId} });

await page.getByRole("textbox").fill(Search Test ${uniqueId}); await expect(page.getByText(Search Test ${uniqueId})).toBeVisible(); });

Reloading to Verify Server Action Persistence

Mistake: Reloading the page to verify a server action persisted data.

Why it's wrong: Slow, flaky (caching/timing), and doesn't catch bugs where the UI doesn't update without a refresh.

Fix: Assert the UI updates immediately, then query the DB for persistence.

// BAD: Reload for persistence — slow, flaky, misses "must refresh" bugs await page.getByRole("button", { name: /publish/i }).click(); await expect(toggle).toBeChecked(); await page.reload(); await expect(toggle).toBeChecked();

// GOOD: UI check + DB check — fast, reliable, catches UI bugs await page.getByRole("button", { name: /publish/i }).click(); await expect(toggle).toBeChecked(); await expect(async () => { const course = await prisma.course.findUniqueOrThrow({ where: { id: course.id } }); expect(course.isPublished).toBe(true); }).toPass({ timeout: 10_000 });

Redundant Tests

Mistake: Writing separate tests when a higher-level test already covers the behavior.

Why it's wrong: More tests to maintain without more confidence.

Fix: If a test proves the final outcome, intermediate steps are implicitly verified.

// BAD: Two redundant tests test("auto-saves title changes", async ({ page }) => { await page.getByRole("textbox").fill("New Title"); await expect(page.getByText(/saved/i)).toBeVisible(); });

test("persists title after reload", async ({ page }) => { await page.getByRole("textbox").fill("New Title"); await page.reload(); await expect(page.getByRole("textbox")).toHaveValue("New Title"); });

// GOOD: Single test proves both test("auto-saves and persists title", async ({ page }) => { await page.getByRole("textbox").fill("New Title"); await expect(page.getByText(/saved/i)).toBeVisible(); await page.reload(); await expect(page.getByRole("textbox")).toHaveValue("New Title"); });

Integration Testing (Vitest + Prisma)

Structure

import { prisma } from "@/lib/db"; import { postFixture, memberFixture, signInAs } from "@/tests/fixtures";

describe("createComment", () => { describe("unauthenticated users", () => { test("returns unauthorized error", async () => { const result = await createComment({ headers: new Headers(), postId: 1, content: "Test" }); expect(result.error?.message).toBe(ErrorCode.unauthorized); }); });

describe("admin users", () => { let post: Post; let headers: Headers;

beforeAll(async () => {
  const { organization, user } = await memberFixture({ role: "admin" });
  post = await postFixture({ organizationId: organization.id });
  headers = await signInAs(user.email, user.password);
});

test("creates comment successfully", async () => {
  const result = await createComment({ headers, postId: post.id, content: "New Comment" });
  expect(result.data?.content).toBe("New Comment");
});

}); });

Test All Permission Levels

describe("unauthenticated users", () => { /* ... / }); describe("members", () => { / ... / }); describe("admins", () => { / ... */ });

Unit Testing (Vitest)

When to Add Unit Tests

  • Edge cases not covered by e2e tests

  • Complex utility functions

  • Error boundary conditions

import { removeAccents } from "./string";

describe("removeAccents", () => { test("removes diacritics from string", () => { expect(removeAccents("café")).toBe("cafe"); expect(removeAccents("São Paulo")).toBe("Sao Paulo"); }); });

Commands

Unit/Integration tests

pnpm test # Run all tests once pnpm test -- --run src/data/posts/create-post.test.ts # Run specific file

E2E tests

pnpm --filter {app} build:e2e # Always run before e2e tests pnpm --filter {app} e2e # Run all e2e tests

Best Practices

  • Read Playwright best practices before writing e2e tests

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Coding

zoonk-github-issues

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

zoonk-code-simplification

No summary provided by upstream source.

Repository SourceNeeds Review
General

zoonk-translations

No summary provided by upstream source.

Repository SourceNeeds Review
General

cache-components

No summary provided by upstream source.

Repository SourceNeeds Review