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>
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.tsfiles ine2e/directories - Unit/Integration: Vitest with
.test.ts/.test.tsxfiles co-located with source - Visual Testing: Storybook stories serve as E2E test fixtures via
playwright.config.tswebServer - UI Library:
@suaveplan/react-ui(shadcn/ui-based) withcn()utility for Tailwind class merging - CSS Prefix: All Suaveplan components use
sp-prefix indata-testidattributes
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)
data-testidwithsp-prefix -- Primary selector for Suaveplan components- ARIA roles and labels -- For accessible elements
- Text content -- For user-visible labels
- 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.tsfiles 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
.jsor.mjstest files - Biome linting -- all test files must pass
bun run lint - Every component must have
data-testidattributes withsp-prefix - Every
.spec.tsfile must have a corresponding.stories.tsxfixture - 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
- All
.spec.tsfiles pass:bun run e2e --filter=@suaveplan/<package> - All
.test.tsxfiles pass:bun run test --filter=@suaveplan/<package> - Coverage at 95%+:
bun run test:coverage --filter=@suaveplan/<package> - Biome lint clean:
bun run lint --filter=@suaveplan/<package> - TypeScript types pass:
bun run typecheck --filter=@suaveplan/<package> - Accessibility spec exists for interactive components
- No
vi.mock()orjest.mock()calls - All
data-testidattributes usesp-prefix