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" />
)}
))}
- } onClick={addColumn} variant="outlined">
- Add Column
-
+
-
-
+
+
);
diff --git a/src/components/admin/DataGrid.tsx b/src/components/admin/DataGrid.tsx
index edb7f03..b755764 100644
--- a/src/components/admin/DataGrid.tsx
+++ b/src/components/admin/DataGrid.tsx
@@ -9,7 +9,7 @@ import {
TableHead,
TableRow,
Tooltip,
-} from '@mui/material';
+} from '../atoms';
import IconButton from '../atoms/IconButton';
type DataGridProps = {
diff --git a/src/components/admin/DropTableDialog.tsx b/src/components/admin/DropTableDialog.tsx
index b242ef4..485aad4 100644
--- a/src/components/admin/DropTableDialog.tsx
+++ b/src/components/admin/DropTableDialog.tsx
@@ -1,16 +1,16 @@
'use client';
+import { useState } from 'react';
import {
- Button,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
MenuItem,
Select,
- Typography,
-} from '@mui/material';
-import { useState } from 'react';
+} from '../atoms';
+import Button from '../atoms/Button';
+import Typography from '../atoms/Typography';
type DropTableDialogProps = {
open: boolean;
@@ -70,15 +70,14 @@ export default function DropTableDialog({
-
+
+ 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());
+}