From d7d5bbfb2bb40ddd8be66a142669bb9be94d6b0f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 8 Jan 2026 15:13:45 +0000 Subject: [PATCH 1/6] Initial plan From ef1a9128331db0c226dde30746d5f92a3016e304 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 8 Jan 2026 15:20:10 +0000 Subject: [PATCH 2/6] Refactor admin components to use atomic component library and add utilities Co-authored-by: johndoe6345789 <224850594+johndoe6345789@users.noreply.github.com> --- src/components/admin/CreateTableDialog.tsx | 30 ++-- src/components/admin/DataGrid.tsx | 2 +- src/components/admin/DropTableDialog.tsx | 15 +- .../atoms/Button.generated.stories.tsx | 52 +++++++ src/components/atoms/index.ts | 1 + src/utils/storybook/storyGenerator.ts | 76 ++++++++++ tests/utils/playbookRunner.ts | 135 ++++++++++++++++++ 7 files changed, 283 insertions(+), 28 deletions(-) create mode 100644 src/components/atoms/Button.generated.stories.tsx create mode 100644 src/utils/storybook/storyGenerator.ts create mode 100644 tests/utils/playbookRunner.ts diff --git a/src/components/admin/CreateTableDialog.tsx b/src/components/admin/CreateTableDialog.tsx index a098827..4ed18d4 100644 --- a/src/components/admin/CreateTableDialog.tsx +++ b/src/components/admin/CreateTableDialog.tsx @@ -1,23 +1,21 @@ 'use client'; -import AddIcon from '@mui/icons-material/Add'; -import DeleteIcon from '@mui/icons-material/Delete'; +import { useState } from 'react'; import { Box, - Button, - Checkbox, Dialog, DialogActions, DialogContent, DialogTitle, FormControlLabel, - IconButton, + Checkbox, MenuItem, Select, - TextField, - Typography, -} from '@mui/material'; -import { useState } from 'react'; +} from '../atoms'; +import Button from '../atoms/Button'; +import TextField from '../atoms/TextField'; +import Typography from '../atoms/Typography'; +import IconButton from '../atoms/IconButton'; type Column = { name: string; @@ -141,21 +139,15 @@ export default function CreateTableDialog({ sx={{ mr: 1 }} /> {columns.length > 1 && ( - removeColumn(index)} color="error" size="small"> - - + removeColumn(index)} color="error" size="small" icon="Delete" /> )} ))} - + - + + + text="Drop Table" + /> ); diff --git a/src/components/atoms/Button.generated.stories.tsx b/src/components/atoms/Button.generated.stories.tsx new file mode 100644 index 0000000..43de289 --- /dev/null +++ b/src/components/atoms/Button.generated.stories.tsx @@ -0,0 +1,52 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import Button from './Button'; +import { generateMeta, generateStories } from '@/utils/storybook/storyGenerator'; + +/** + * Example of using story generator with features.json configuration + * This demonstrates how to leverage the storybookStories section from features.json + */ + +// Generate meta from features.json +const meta = generateMeta(Button, 'Button', { + title: 'Atoms/Button', +}) satisfies Meta; + +export default meta; +type Story = StoryObj; + +// Generate stories from features.json +const generatedStories = generateStories('Button'); + +// Export individual stories +export const Primary: Story = generatedStories.primary || { + args: { + variant: 'contained', + color: 'primary', + text: 'Primary Button', + }, +}; + +export const Secondary: Story = generatedStories.secondary || { + args: { + variant: 'outlined', + color: 'secondary', + text: 'Secondary Button', + }, +}; + +export const WithIcon: Story = generatedStories.withIcon || { + args: { + variant: 'contained', + startIcon: 'Add', + text: 'Add Item', + }, +}; + +export const Loading: Story = generatedStories.loading || { + args: { + variant: 'contained', + disabled: true, + text: 'Loading...', + }, +}; diff --git a/src/components/atoms/index.ts b/src/components/atoms/index.ts index 939a569..d0d1a60 100644 --- a/src/components/atoms/index.ts +++ b/src/components/atoms/index.ts @@ -44,4 +44,5 @@ export { AccordionSummary, AccordionDetails, Chip, + Tooltip, } from '@mui/material'; diff --git a/src/utils/storybook/storyGenerator.ts b/src/utils/storybook/storyGenerator.ts new file mode 100644 index 0000000..2054d6d --- /dev/null +++ b/src/utils/storybook/storyGenerator.ts @@ -0,0 +1,76 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { getAllStorybookStories, getStorybookStoriesForComponent, StorybookStory } from '@/utils/featureConfig'; + +/** + * Generate Storybook meta configuration from features.json + */ +export function generateMeta( + component: T, + componentName: string, + customMeta?: Partial> +): Meta { + const stories = getStorybookStoriesForComponent(componentName); + const defaultStory = stories.default; + + return { + title: `Components/${componentName}`, + component: component as any, + parameters: { + layout: 'centered', + ...defaultStory?.parameters, + }, + tags: ['autodocs'], + ...customMeta, + }; +} + +/** + * Generate a single story from features.json story definition + */ +export function generateStory( + storyConfig: StorybookStory +): StoryObj { + return { + name: storyConfig.name, + args: storyConfig.args || {}, + parameters: storyConfig.parameters, + // Note: play functions would need to be converted from strings to actual functions + // This is a limitation of JSON - we can only store the play steps as strings + }; +} + +/** + * Generate all stories for a component from features.json + */ +export function generateStories(componentName: string): Record> { + const stories = getStorybookStoriesForComponent(componentName); + const result: Record> = {}; + + for (const [key, storyConfig] of Object.entries(stories)) { + result[key] = generateStory(storyConfig); + } + + return result; +} + +/** + * Get all available story configurations + */ +export function listStorybookComponents(): string[] { + return Object.keys(getAllStorybookStories()); +} + +/** + * Helper to create mock handlers for stories + */ +export function createMockHandlers(handlerNames: string[]): Record void> { + const handlers: Record void> = {}; + + for (const name of handlerNames) { + handlers[name] = () => { + console.log(`Mock handler called: ${name}`); + }; + } + + return handlers; +} diff --git a/tests/utils/playbookRunner.ts b/tests/utils/playbookRunner.ts new file mode 100644 index 0000000..ca214f2 --- /dev/null +++ b/tests/utils/playbookRunner.ts @@ -0,0 +1,135 @@ +import { Page, expect } from '@playwright/test'; +import { getAllPlaywrightPlaybooks, PlaywrightPlaybook, PlaywrightStep } from '@/utils/featureConfig'; + +/** + * Execute a single Playwright step from a playbook + */ +export async function executeStep(page: Page, step: PlaywrightStep, variables: Record = {}) { + // Replace variables in step properties + const replaceVars = (str: string | undefined): string => { + if (!str) return ''; + return str.replace(/\{\{(\w+)\}\}/g, (_, key) => variables[key] || ''); + }; + + const selector = replaceVars(step.selector); + const value = replaceVars(step.value); + const url = replaceVars(step.url); + const text = replaceVars(step.text); + + switch (step.action) { + case 'goto': + if (url) { + await page.goto(url); + } + break; + + case 'click': + if (selector) { + await page.click(selector); + } + break; + + case 'fill': + if (selector && value) { + await page.fill(selector, value); + } + break; + + case 'select': + if (selector && value) { + await page.selectOption(selector, value); + } + break; + + case 'wait': + if (step.timeout) { + await page.waitForTimeout(step.timeout); + } + break; + + case 'expect': + if (url === 'redirected') { + await expect(page).toHaveURL(new RegExp(selector || '')); + } else if (text === 'visible' && selector) { + await expect(page.locator(selector)).toBeVisible(); + } else if (text && selector) { + await expect(page.locator(selector)).toContainText(text); + } else if (text) { + // Check for status code in text + const statusCode = parseInt(text, 10); + if (!isNaN(statusCode)) { + // This is a status code check - would need API interception + // For now, we skip this as it requires special handling + console.warn(`Status code check (${statusCode}) not yet implemented in playbook runner`); + } + } + break; + + case 'screenshot': + if (selector) { + await page.locator(selector).screenshot({ path: `screenshots/${Date.now()}-${selector.replace(/[^a-z0-9]/gi, '_')}.png` }); + } else { + await page.screenshot({ path: `screenshots/${Date.now()}-page.png` }); + } + break; + + default: + console.warn(`Unknown step action: ${step.action}`); + } +} + +/** + * Execute a full playbook from features.json + */ +export async function runPlaybook( + page: Page, + playbookName: string, + variables: Record = {}, + options: { runCleanup?: boolean } = {} +) { + const playbooks = getAllPlaywrightPlaybooks(); + const playbook = playbooks[playbookName]; + + if (!playbook) { + throw new Error(`Playbook not found: ${playbookName}`); + } + + console.log(`Running playbook: ${playbook.name}`); + console.log(`Description: ${playbook.description}`); + + // Execute main steps + for (const step of playbook.steps) { + await executeStep(page, step, variables); + } + + // Execute cleanup steps if requested and they exist + if (options.runCleanup && playbook.cleanup) { + console.log('Running cleanup steps...'); + for (const step of playbook.cleanup) { + await executeStep(page, step, variables); + } + } +} + +/** + * Get all playbooks by tag + */ +export function getPlaybooksByTag(tag: string): Record { + const allPlaybooks = getAllPlaywrightPlaybooks(); + const filtered: Record = {}; + + for (const [name, playbook] of Object.entries(allPlaybooks)) { + if (playbook.tags?.includes(tag)) { + filtered[name] = playbook; + } + } + + return filtered; +} + +/** + * List all available playbooks + */ +export function listPlaybooks(): string[] { + return Object.keys(getAllPlaywrightPlaybooks()); +} From 4136f3c50d23ae71cf7c0795e753e7d8efa06529 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 8 Jan 2026 15:23:41 +0000 Subject: [PATCH 3/6] Add Playwright playbook runner, Storybook generator, and comprehensive documentation Co-authored-by: johndoe6345789 <224850594+johndoe6345789@users.noreply.github.com> --- README.md | 4 + docs/PLAYWRIGHT_PLAYBOOKS.md | 422 +++++++++++++++++++++++++++++++++++ docs/STORYBOOK.md | 185 +++++++++++++++ tests/e2e/Playbooks.e2e.ts | 81 +++++++ 4 files changed, 692 insertions(+) create mode 100644 docs/PLAYWRIGHT_PLAYBOOKS.md create mode 100644 docs/STORYBOOK.md create mode 100644 tests/e2e/Playbooks.e2e.ts diff --git a/README.md b/README.md index 18446b7..76368c9 100644 --- a/README.md +++ b/README.md @@ -628,6 +628,10 @@ npm run dev - `npm run test` - Run unit tests with Vitest - `npm run test:e2e` - Run E2E tests with Playwright - `npm run storybook` - Start Storybook for component development +- `npm run build-storybook` - Build Storybook for production + +See [PLAYWRIGHT_PLAYBOOKS.md](./docs/PLAYWRIGHT_PLAYBOOKS.md) for Playwright playbook testing documentation. +See [STORYBOOK.md](./docs/STORYBOOK.md) for Storybook configuration and usage. #### Code Quality - `npm run lint` - Run ESLint diff --git a/docs/PLAYWRIGHT_PLAYBOOKS.md b/docs/PLAYWRIGHT_PLAYBOOKS.md new file mode 100644 index 0000000..f97495a --- /dev/null +++ b/docs/PLAYWRIGHT_PLAYBOOKS.md @@ -0,0 +1,422 @@ +# Playwright Playbook Testing + +This project uses Playwright for end-to-end testing with test playbooks defined in `features.json` for reusable test scenarios. + +## Getting Started + +### Running Playwright Tests + +```bash +# Run all tests +npm run test:e2e + +# Run in UI mode (interactive) +npx playwright test --ui + +# Run specific test file +npx playwright test tests/e2e/Playbooks.e2e.ts + +# Run tests in headed mode (see browser) +npx playwright test --headed +``` + +## Playbook Runner Utility + +The playbook runner (`tests/utils/playbookRunner.ts`) executes test scenarios defined in the `playwrightPlaybooks` section of `features.json`. + +### Why Use Playbooks? + +- **Reusability** - Define common workflows once, use in multiple tests +- **Consistency** - Ensure tests follow the same patterns +- **Maintainability** - Update test steps in one place +- **Documentation** - Playbooks serve as living documentation +- **Configuration-driven** - Non-developers can update test scenarios + +### Using the Playbook Runner + +#### Basic Usage + +```typescript +import { test } from '@playwright/test'; +import { runPlaybook } from '../utils/playbookRunner'; + +test('should execute login workflow', async ({ page }) => { + await runPlaybook(page, 'adminLogin', { + username: 'admin', + password: 'password123', + }); +}); +``` + +#### With Variables + +Playbooks support variable substitution using `{{variableName}}` syntax: + +```typescript +await runPlaybook(page, 'createTable', { + tableName: 'users', + columnName: 'id', + dataType: 'INTEGER', +}); +``` + +#### With Cleanup + +Some playbooks include cleanup steps: + +```typescript +await runPlaybook(page, 'createTable', + { tableName: 'test_table' }, + { runCleanup: true } // Runs cleanup steps after main steps +); +``` + +### Available Utilities + +#### `runPlaybook(page, playbookName, variables?, options?)` +Executes a complete playbook from features.json. + +**Parameters:** +- `page` - Playwright Page object +- `playbookName` - Name of the playbook in features.json +- `variables` - Object with variable values for substitution +- `options.runCleanup` - Whether to run cleanup steps + +#### `executeStep(page, step, variables?)` +Executes a single playbook step. + +#### `getPlaybooksByTag(tag)` +Returns all playbooks with a specific tag. + +```typescript +const adminPlaybooks = getPlaybooksByTag('admin'); +``` + +#### `listPlaybooks()` +Returns names of all available playbooks. + +```typescript +const playbooks = listPlaybooks(); +console.log('Available playbooks:', playbooks); +``` + +## Defining Playbooks in features.json + +Playbooks are defined in the `playwrightPlaybooks` section: + +```json +{ + "playwrightPlaybooks": { + "playbookName": { + "name": "Human-Readable Name", + "description": "What this playbook does", + "tags": ["admin", "crud"], + "steps": [ + { + "action": "goto", + "url": "/admin/dashboard" + }, + { + "action": "click", + "selector": "button:has-text('Create')" + }, + { + "action": "fill", + "selector": "input[name='name']", + "value": "{{name}}" + }, + { + "action": "expect", + "selector": "text={{name}}", + "text": "visible" + } + ], + "cleanup": [ + { + "action": "click", + "selector": "button:has-text('Delete')" + } + ] + } + } +} +``` + +### Supported Actions + +| Action | Description | Parameters | +|--------|-------------|------------| +| `goto` | Navigate to URL | `url` | +| `click` | Click element | `selector` | +| `fill` | Fill input | `selector`, `value` | +| `select` | Select dropdown option | `selector`, `value` | +| `wait` | Wait for timeout | `timeout` (ms) | +| `expect` | Assert condition | `selector`, `text` or `url` | +| `screenshot` | Take screenshot | `selector` (optional) | + +### Variable Substitution + +Use `{{variableName}}` in any string field: + +```json +{ + "action": "fill", + "selector": "input[name='{{fieldName}}']", + "value": "{{fieldValue}}" +} +``` + +When running the playbook: + +```typescript +await runPlaybook(page, 'myPlaybook', { + fieldName: 'username', + fieldValue: 'admin', +}); +``` + +## Pre-defined Playbooks + +The following playbooks are available in features.json: + +### adminLogin +Complete admin login flow. +- **Tags:** admin, auth, login +- **Variables:** username, password + +### createTable +Create a new database table through UI. +- **Tags:** admin, table, crud +- **Variables:** tableName +- **Cleanup:** Yes (drops the table) + +### addColumn +Add a column to an existing table. +- **Tags:** admin, column, crud +- **Variables:** tableName, columnName, dataType + +### createIndex +Create a database index. +- **Tags:** admin, index, performance +- **Variables:** tableName, indexName, columnName + +### queryBuilder +Build and execute a query. +- **Tags:** admin, query, select +- **Variables:** tableName, columnName + +### securityCheck +Verify API endpoints require authentication. +- **Tags:** security, api, auth +- **Variables:** None + +## Best Practices + +### 1. Tag Your Playbooks + +Use tags for organization and filtering: + +```json +{ + "tags": ["admin", "crud", "table"] +} +``` + +### 2. Use Meaningful Names + +Make playbook names descriptive: +- ✅ `createUserAndVerifyEmail` +- ❌ `test1` + +### 3. Add Cleanup Steps + +Clean up test data to keep tests independent: + +```json +{ + "cleanup": [ + { + "action": "click", + "selector": "button:has-text('Delete')" + } + ] +} +``` + +### 4. Make Playbooks Composable + +Break complex workflows into smaller playbooks: + +```typescript +// Login first +await runPlaybook(page, 'adminLogin', { username, password }); + +// Then run specific test +await runPlaybook(page, 'createTable', { tableName }); +``` + +### 5. Use Descriptive Selectors + +Prefer text selectors and test IDs: +- ✅ `button:has-text('Create')` +- ✅ `[data-testid="create-button"]` +- ❌ `.btn-primary` + +## Example Tests + +### Simple Playbook Test + +```typescript +import { test } from '@playwright/test'; +import { runPlaybook } from '../utils/playbookRunner'; + +test('create and delete table', async ({ page }) => { + const tableName = `test_${Date.now()}`; + + await runPlaybook(page, 'createTable', + { tableName }, + { runCleanup: true } + ); +}); +``` + +### Multiple Playbooks + +```typescript +test('complete workflow', async ({ page }) => { + // Step 1: Login + await runPlaybook(page, 'adminLogin', { + username: 'admin', + password: 'password', + }); + + // Step 2: Create table + const tableName = 'users'; + await runPlaybook(page, 'createTable', { tableName }); + + // Step 3: Add column + await runPlaybook(page, 'addColumn', { + tableName, + columnName: 'email', + dataType: 'VARCHAR', + }); + + // Step 4: Create index + await runPlaybook(page, 'createIndex', { + tableName, + indexName: 'idx_email', + columnName: 'email', + }); +}); +``` + +### Tag-based Testing + +```typescript +import { getPlaybooksByTag } from '../utils/playbookRunner'; + +test.describe('Admin CRUD operations', () => { + const crudPlaybooks = getPlaybooksByTag('crud'); + + for (const [name, playbook] of Object.entries(crudPlaybooks)) { + test(playbook.name, async ({ page }) => { + // Run each CRUD playbook + await runPlaybook(page, name, { + /* variables */ + }); + }); + } +}); +``` + +## Debugging + +### View Test Results + +```bash +# Show test report +npx playwright show-report + +# Open trace viewer +npx playwright show-trace trace.zip +``` + +### Debug Mode + +```bash +# Run in debug mode +npx playwright test --debug + +# Run specific test in debug mode +npx playwright test tests/e2e/Playbooks.e2e.ts --debug +``` + +### Screenshots + +Playbooks can take screenshots: + +```json +{ + "action": "screenshot", + "selector": ".query-results" +} +``` + +Screenshots are saved to `screenshots/` directory. + +## Continuous Integration + +In CI environments, tests run automatically: + +```yaml +# .github/workflows/test.yml +- name: Run Playwright tests + run: npm run test:e2e +``` + +The playwright.config.ts is configured to: +- Use different settings for CI vs local +- Record videos on failure +- Generate test reports + +## Troubleshooting + +### Playbook not found + +Make sure the playbook name matches exactly in features.json: + +```typescript +const playbooks = listPlaybooks(); +console.log('Available:', playbooks); +``` + +### Timeout errors + +Increase wait times in playbook steps: + +```json +{ + "action": "wait", + "timeout": 5000 +} +``` + +Or configure global timeout in playwright.config.ts. + +### Variable substitution not working + +Check variable names match exactly: + +```typescript +// In features.json: {{tableName}} +// In test: +await runPlaybook(page, 'createTable', { + tableName: 'users', // Must match: tableName +}); +``` + +## Additional Resources + +- [Playwright Documentation](https://playwright.dev/) +- [Playwright Best Practices](https://playwright.dev/docs/best-practices) +- [Test Examples](/tests/e2e/) diff --git a/docs/STORYBOOK.md b/docs/STORYBOOK.md new file mode 100644 index 0000000..fd0c893 --- /dev/null +++ b/docs/STORYBOOK.md @@ -0,0 +1,185 @@ +# Storybook Configuration and Usage + +This project uses Storybook for component development and documentation, with configurations driven by `features.json`. + +## Getting Started + +### Running Storybook + +```bash +npm run storybook +``` + +This will start Storybook on port 6006: http://localhost:6006 + +### Building Storybook + +```bash +npm run build-storybook +``` + +This creates a static build in the `storybook-static` directory. + +## Story Generator Utility + +The project includes a story generator utility (`src/utils/storybook/storyGenerator.ts`) that creates stories from the `storybookStories` section in `features.json`. + +### Using the Story Generator + +#### Basic Usage + +```typescript +import type { Meta, StoryObj } from '@storybook/react'; +import Button from './Button'; +import { generateMeta, generateStories } from '@/utils/storybook/storyGenerator'; + +// Generate meta from features.json +const meta = generateMeta(Button, 'Button') satisfies Meta; + +export default meta; +type Story = StoryObj; + +// Generate all stories for the component +const stories = generateStories('Button'); + +// Export individual stories +export const Primary: Story = stories.primary; +export const Secondary: Story = stories.secondary; +export const WithIcon: Story = stories.withIcon; +``` + +#### Custom Meta + +You can override or extend the generated meta: + +```typescript +const meta = generateMeta(Button, 'Button', { + title: 'Custom/Button/Path', + parameters: { + layout: 'fullscreen', + }, +}) satisfies Meta; +``` + +### Adding Stories to features.json + +Stories are defined in the `storybookStories` section of `features.json`: + +```json +{ + "storybookStories": { + "ComponentName": { + "storyName": { + "name": "Display Name", + "description": "Story description", + "args": { + "prop1": "value1", + "prop2": "value2" + }, + "parameters": { + "layout": "centered" + } + } + } + } +} +``` + +### Available Utilities + +#### `generateMeta(component, componentName, customMeta?)` +Generates Storybook meta configuration from features.json. + +#### `generateStory(storyConfig)` +Generates a single story from a story configuration. + +#### `generateStories(componentName)` +Generates all stories for a component. + +#### `listStorybookComponents()` +Returns an array of all components that have story definitions. + +#### `createMockHandlers(handlerNames)` +Creates mock event handlers for stories. + +## Component Stories + +Stories are organized by component category: + +- **Atoms** - Basic UI building blocks (Button, TextField, Typography, Icon, IconButton) +- **Components** - Composed components (DataGrid, ConfirmDialog, FormDialog) +- **Admin** - Admin-specific components + +## Best Practices + +1. **Use the story generator** - Define stories in features.json and use the generator utility +2. **Keep args simple** - Complex props should have reasonable defaults +3. **Add descriptions** - Help other developers understand the story's purpose +4. **Include multiple states** - Show default, loading, error, empty states +5. **Use mock handlers** - Use `createMockHandlers()` for event handlers + +## Testing Stories + +Run Storybook tests with: + +```bash +npm run storybook:test +``` + +This uses Vitest to test stories in isolation. + +## Component Documentation + +Storybook automatically generates documentation from: +- TypeScript prop types +- JSDoc comments +- Story configurations from features.json + +Add JSDoc comments to your components: + +```typescript +/** + * Button component for user interactions + * + * @example + *