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()); +}