mirror of
https://github.com/johndoe6345789/postgres.git
synced 2026-04-24 13:55:00 +00:00
Add Playwright playbook runner, Storybook generator, and comprehensive documentation
Co-authored-by: johndoe6345789 <224850594+johndoe6345789@users.noreply.github.com>
This commit is contained in:
@@ -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
|
||||
|
||||
422
docs/PLAYWRIGHT_PLAYBOOKS.md
Normal file
422
docs/PLAYWRIGHT_PLAYBOOKS.md
Normal file
@@ -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/)
|
||||
185
docs/STORYBOOK.md
Normal file
185
docs/STORYBOOK.md
Normal file
@@ -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<typeof Button>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
// Generate all stories for the component
|
||||
const stories = generateStories<typeof Button>('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<typeof Button>;
|
||||
```
|
||||
|
||||
### 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<T>(component, componentName, customMeta?)`
|
||||
Generates Storybook meta configuration from features.json.
|
||||
|
||||
#### `generateStory<T>(storyConfig)`
|
||||
Generates a single story from a story configuration.
|
||||
|
||||
#### `generateStories<T>(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
|
||||
* <Button variant="contained" color="primary" text="Click Me" />
|
||||
*/
|
||||
export default function Button({ text, ...props }: ButtonProps) {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
See these files for examples:
|
||||
- `src/components/atoms/Button.generated.stories.tsx` - Generated stories example
|
||||
- `src/components/atoms/Button.stories.tsx` - Manual stories example
|
||||
- `src/components/admin/DataGrid.stories.tsx` - Complex component stories
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Stories not appearing
|
||||
|
||||
1. Check that the component is in `src/**/*.stories.@(js|jsx|ts|tsx)`
|
||||
2. Verify the story configuration in features.json
|
||||
3. Check console for errors
|
||||
|
||||
### Type errors
|
||||
|
||||
Make sure your story definitions match the component's prop types:
|
||||
|
||||
```typescript
|
||||
// features.json
|
||||
{
|
||||
"args": {
|
||||
"variant": "contained", // Must be a valid variant value
|
||||
"color": "primary" // Must be a valid color value
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Additional Resources
|
||||
|
||||
- [Storybook Documentation](https://storybook.js.org/)
|
||||
- [Storybook Best Practices](https://storybook.js.org/docs/react/writing-stories/introduction)
|
||||
- [Component Story Format](https://storybook.js.org/docs/react/api/csf)
|
||||
81
tests/e2e/Playbooks.e2e.ts
Normal file
81
tests/e2e/Playbooks.e2e.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { runPlaybook, listPlaybooks, getPlaybooksByTag } from '../utils/playbookRunner';
|
||||
|
||||
/**
|
||||
* Example test using playbookRunner to execute tests from features.json
|
||||
*/
|
||||
|
||||
test.describe('Playbook-driven tests', () => {
|
||||
test('should list available playbooks', () => {
|
||||
const playbooks = listPlaybooks();
|
||||
|
||||
expect(playbooks).toBeDefined();
|
||||
expect(playbooks.length).toBeGreaterThan(0);
|
||||
|
||||
// Check for expected playbooks from features.json
|
||||
expect(playbooks).toContain('adminLogin');
|
||||
expect(playbooks).toContain('createTable');
|
||||
expect(playbooks).toContain('queryBuilder');
|
||||
});
|
||||
|
||||
test('should filter playbooks by tag', () => {
|
||||
const adminPlaybooks = getPlaybooksByTag('admin');
|
||||
|
||||
expect(Object.keys(adminPlaybooks).length).toBeGreaterThan(0);
|
||||
|
||||
// All returned playbooks should have the 'admin' tag
|
||||
for (const playbook of Object.values(adminPlaybooks)) {
|
||||
expect(playbook.tags).toContain('admin');
|
||||
}
|
||||
});
|
||||
|
||||
// Example test using a playbook from features.json
|
||||
test.skip('should execute query builder playbook', async ({ page }) => {
|
||||
// Note: This test is skipped as it requires a running application
|
||||
// To enable, remove test.skip and ensure the app is running
|
||||
|
||||
await runPlaybook(page, 'queryBuilder', {
|
||||
tableName: 'users',
|
||||
columnName: 'name',
|
||||
});
|
||||
|
||||
// The playbook includes assertions, so if we get here, the test passed
|
||||
expect(true).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* These tests demonstrate the playbook system but are skipped by default
|
||||
* because they require a running application. In a real CI/CD environment,
|
||||
* you would remove the .skip and ensure the app is running before tests.
|
||||
*/
|
||||
test.describe.skip('Full playbook integration tests', () => {
|
||||
test('admin login flow', async ({ page }) => {
|
||||
await runPlaybook(page, 'adminLogin', {
|
||||
username: 'admin',
|
||||
password: 'testpassword',
|
||||
});
|
||||
});
|
||||
|
||||
test('create table workflow', async ({ page }) => {
|
||||
await runPlaybook(page, 'createTable', {
|
||||
tableName: 'test_table_' + Date.now(),
|
||||
}, { runCleanup: true });
|
||||
});
|
||||
|
||||
test('add column workflow', async ({ page }) => {
|
||||
await runPlaybook(page, 'addColumn', {
|
||||
tableName: 'users',
|
||||
columnName: 'test_column',
|
||||
dataType: 'VARCHAR',
|
||||
});
|
||||
});
|
||||
|
||||
test('create index workflow', async ({ page }) => {
|
||||
await runPlaybook(page, 'createIndex', {
|
||||
tableName: 'users',
|
||||
indexName: 'idx_test_' + Date.now(),
|
||||
columnName: 'name',
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user