Files
skills/plugins/suaveplan-webapp-testing/skills/webapp-testing/SKILL.md
admin 9bf56a806b Move docs/ package mirror into document-navigator plugin
Relocate the ~6k-file docs tree from the repo root into
plugins/document-navigator/docs/ so the plugin is self-contained and
portable. docs/superpowers/ (specs and plans) stays at the repo root.

Moved via cp+rm rather than git mv because directory rename() fails with
EPERM on this WSL /mnt DrvFs mount; git rename-detection pairs the
identical-content files at commit time.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 01:10:00 +01:00

10 KiB

name, description
name description
webapp-testing End-to-end and visual regression testing for Suaveplan React 19 + TypeScript + Tailwind CSS web applications using Playwright and Storybook

Webapp Testing for Suaveplan

Write and maintain Playwright E2E tests and Storybook visual regression tests for Suaveplan browser packages. All tests are TypeScript .spec.ts files executed natively through Playwright -- no Python scripts, no wrapper layers.

Architecture

  • Runtime: Bun + Turborepo monorepo
  • E2E Framework: Playwright (TypeScript) with .spec.ts files in e2e/ directories
  • Unit/Integration: Vitest with .test.ts / .test.tsx files co-located with source
  • Visual Testing: Storybook stories serve as E2E test fixtures via playwright.config.ts webServer
  • UI Library: @suaveplan/react-ui (shadcn/ui-based) with cn() utility for Tailwind class merging
  • CSS Prefix: All Suaveplan components use sp- prefix in data-testid attributes

Running Tests

# Run E2E tests for a specific package
bun run e2e --filter=@suaveplan/<package-name>

# Run unit tests
bun run test --filter=@suaveplan/<package-name>

# Run all E2E tests across monorepo
bunx turbo e2e

# Run with coverage (95% minimum required)
bun run test:coverage --filter=@suaveplan/<package-name>

File Structure

Every testable browser package follows this layout:

packages/<category>/<sub-category>/<package-name>/
  playwright.config.ts          # Playwright configuration
  e2e/
    <feature>.spec.ts           # E2E test files
    accessibility.spec.ts       # Dedicated a11y E2E tests
  .storybook/
    main.ts                     # Storybook configuration
    preview.ts                  # Global decorators, Tailwind CSS import
  src/
    components/
      <component>/
        <component>.tsx         # Implementation
        <component>.test.tsx    # Unit tests (Vitest)
        <component>.stories.tsx # Storybook stories (E2E fixtures)
        <component>.types.ts   # TypeScript types
        <component>.md         # Documentation

Playwright Configuration Pattern

Each package has its own playwright.config.ts that launches Storybook as the web server:

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

export default defineConfig({
  testDir: './e2e',
  testMatch: '**/*.spec.ts',
  fullyParallel: true,
  forbidOnly: !!process.env.CI,
  retries: process.env.CI ? 2 : 0,
  workers: process.env.CI ? 1 : undefined,
  reporter: [['html'], ['json', { outputFile: 'test-results.json' }]],
  use: {
    baseURL: 'http://localhost:6030',
    viewport: { width: 1280, height: 720 },
    trace: 'on-first-retry',
    screenshot: 'only-on-failure',
    video: 'retain-on-failure',
  },
  projects: [{ name: 'chromium', use: { ...devices['Desktop Chrome'] } }],
  webServer: {
    command: 'bun run storybook',
    port: 6030,
    reuseExistingServer: !process.env.CI,
    timeout: 300 * 1000,
    stdout: 'pipe',
    stderr: 'pipe',
  },
});

E2E Test Patterns

Navigating to Storybook Stories

All E2E tests use Storybook iframe URLs as their entry point:

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

const storyUrl = (story: string) =>
  `/iframe.html?viewMode=story&id=components-<component>--${story}`;

test.describe('<Component>: Default story', () => {
  test.beforeEach(async ({ page }) => {
    await page.goto(storyUrl('default'));
    await page.waitForSelector('[data-testid="sp-<component>-container"]');
  });

  test('renders the component', async ({ page }) => {
    const container = page.getByTestId('sp-<component>-container');
    await expect(container).toBeVisible();
  });
});

Selector Strategy (Priority Order)

  1. data-testid with sp- prefix -- Primary selector for Suaveplan components
  2. ARIA roles and labels -- For accessible elements
  3. Text content -- For user-visible labels
  4. CSS class selectors -- Last resort only, prefix with sp-
// Preferred: data-testid with sp- prefix
page.getByTestId('sp-timeline-container');
page.getByTestId('sp-event-bar');
page.getByTestId('sp-data-table-row');

// Good: ARIA roles
page.getByRole('button', { name: 'Submit' });
page.getByRole('dialog', { name: 'Confirm deletion' });
page.getByRole('application', { name: 'Timeline' });

// Acceptable: text content
page.getByText('No results found');

// Avoid unless necessary: CSS classes
page.locator('.sp-card-header');

Testing shadcn/ui Components

shadcn/ui components from @suaveplan/react-ui use Radix UI primitives. Test their behavior through ARIA roles:

// Dialog (from @suaveplan/react-ui)
test('opens dialog on trigger click', async ({ page }) => {
  await page.getByRole('button', { name: 'Open dialog' }).click();
  const dialog = page.getByRole('dialog');
  await expect(dialog).toBeVisible();
  await expect(dialog).toHaveAttribute('aria-modal', 'true');
});

// Select (Radix-based)
test('selects an option from dropdown', async ({ page }) => {
  await page.getByRole('combobox').click();
  await page.getByRole('option', { name: 'Option A' }).click();
  await expect(page.getByRole('combobox')).toHaveText('Option A');
});

// Toast notifications (@suaveplan/react-notifications)
test('shows success toast after save', async ({ page }) => {
  await page.getByRole('button', { name: 'Save' }).click();
  const toast = page.getByTestId('sp-toast');
  await expect(toast).toBeVisible();
  await expect(toast).toContainText('Saved successfully');
});

// Data table (@suaveplan/react-data-table)
test('sorts table by column header', async ({ page }) => {
  await page.getByRole('columnheader', { name: 'Name' }).click();
  const firstRow = page.getByTestId('sp-data-table-row').first();
  await expect(firstRow).toContainText('Alpha');
});

Accessibility Testing Pattern

Every package should have a dedicated accessibility.spec.ts:

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

const storyUrl = (story: string) =>
  `/iframe.html?viewMode=story&id=components-<component>--${story}`;

test.describe('Accessibility', () => {
  test('all interactive elements are keyboard-focusable', async ({ page }) => {
    await page.goto(storyUrl('default'));
    await page.waitForSelector('[data-testid="sp-<component>-container"]');

    // Tab through interactive elements
    await page.keyboard.press('Tab');
    const focused = page.locator(':focus');
    await expect(focused).toHaveAttribute('tabindex', '0');
  });

  test('ARIA labels are present on all semantic elements', async ({ page }) => {
    await page.goto(storyUrl('default'));
    const container = page.getByTestId('sp-<component>-container');
    await expect(container).toHaveAttribute('aria-label');
  });

  test('focus is trapped inside modal dialogs', async ({ page }) => {
    await page.goto(storyUrl('with-dialog'));
    await page.getByRole('button', { name: 'Open' }).click();
    const dialog = page.getByRole('dialog');
    await expect(dialog).toBeFocused();
  });
});

Visual Regression Pattern

Use Playwright screenshot comparison against Storybook stories:

test('matches visual snapshot', async ({ page }) => {
  await page.goto(storyUrl('default'));
  await page.waitForSelector('[data-testid="sp-<component>-container"]');
  // Wait for animations to settle
  await page.waitForTimeout(300);
  await expect(page).toHaveScreenshot('<component>-default.png', {
    maxDiffPixelRatio: 0.01,
  });
});

test('matches dark mode snapshot', async ({ page }) => {
  await page.goto(storyUrl('dark-mode'));
  await page.waitForSelector('[data-testid="sp-<component>-container"]');
  await page.waitForTimeout(300);
  await expect(page).toHaveScreenshot('<component>-dark.png', {
    maxDiffPixelRatio: 0.01,
  });
});

Form Testing Pattern

For @suaveplan/react-forms components:

test('validates required fields on submit', async ({ page }) => {
  await page.goto(storyUrl('with-validation'));
  await page.getByRole('button', { name: 'Submit' }).click();

  // Check validation messages appear
  await expect(page.getByText('Name is required')).toBeVisible();
  await expect(page.getByText('Email is required')).toBeVisible();
});

test('submits form with valid data', async ({ page }) => {
  await page.goto(storyUrl('with-validation'));
  await page.getByLabel('Name').fill('Jane Doe');
  await page.getByLabel('Email').fill('jane@example.com');
  await page.getByRole('button', { name: 'Submit' }).click();

  // Verify success state
  await expect(page.getByTestId('sp-form-success')).toBeVisible();
});

Constraints

  • Max 500 lines per test file -- split large test suites into multiple .spec.ts files by feature
  • Max 250 lines per implementation file -- components exceeding this need refactoring
  • 95% minimum test coverage -- enforced across unit + E2E combined
  • No mocks -- all tests use real implementations and real Storybook stories
  • TypeScript only -- no .js or .mjs test files
  • Biome linting -- all test files must pass bun run lint
  • Every component must have data-testid attributes with sp- prefix
  • Every .spec.ts file must have a corresponding .stories.tsx fixture
  • Accessibility tests are mandatory for interactive components

Packages with E2E Tests

The following packages have Playwright configurations and should always maintain E2E coverage:

Package Storybook Port Test Directory
@suaveplan/react-ui 6006 e2e/
@suaveplan/react-forms 6007 e2e/
@suaveplan/react-data-table 6030 e2e/
@suaveplan/react-timeline 6030 e2e/
@suaveplan/react-notifications 6030 e2e/
@suaveplan/react-charts 6030 e2e/
@suaveplan/drive-react-sharing 6030 e2e/
@suaveplan/react-virtual 6030 e2e/
@suaveplan/react-auth 6030 e2e/
@suaveplan/react-media 6030 e2e/

Checklist Before Completing Tests

  1. All .spec.ts files pass: bun run e2e --filter=@suaveplan/<package>
  2. All .test.tsx files pass: bun run test --filter=@suaveplan/<package>
  3. Coverage at 95%+: bun run test:coverage --filter=@suaveplan/<package>
  4. Biome lint clean: bun run lint --filter=@suaveplan/<package>
  5. TypeScript types pass: bun run typecheck --filter=@suaveplan/<package>
  6. Accessibility spec exists for interactive components
  7. No vi.mock() or jest.mock() calls
  8. All data-testid attributes use sp- prefix