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:
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"name": "suaveplan-webapp-testing",
|
||||
"version": "1.0.0",
|
||||
"description": "Playwright E2E testing toolkit for Suaveplan browser packages"
|
||||
}
|
||||
302
plugins/suaveplan-webapp-testing/skills/webapp-testing/SKILL.md
Normal file
302
plugins/suaveplan-webapp-testing/skills/webapp-testing/SKILL.md
Normal 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
|
||||
Reference in New Issue
Block a user