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>
This commit is contained in:
2026-05-30 01:10:00 +01:00
parent fe3fedf42e
commit 9bf56a806b
6022 changed files with 12437 additions and 0 deletions

View File

@@ -0,0 +1,5 @@
{
"name": "suaveplan-webapp-testing",
"version": "1.0.0",
"description": "Playwright E2E testing toolkit for Suaveplan browser packages"
}

View File

@@ -0,0 +1,302 @@
---
name: webapp-testing
description: 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
```bash
# 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:
```text
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:
```typescript
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:
```typescript
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-`
```typescript
// 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:
```typescript
// 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`:
```typescript
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:
```typescript
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:
```typescript
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