@effinrich/forgekit-storybook-plugin 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +221 -0
- package/bin/forgekit.js +2 -0
- package/dist/chunk-C2HX5UGS.mjs +704 -0
- package/dist/chunk-C2HX5UGS.mjs.map +1 -0
- package/dist/chunk-D2RQPIRR.mjs +413 -0
- package/dist/chunk-D2RQPIRR.mjs.map +1 -0
- package/dist/chunk-T4UFXGMC.js +704 -0
- package/dist/chunk-T4UFXGMC.js.map +1 -0
- package/dist/chunk-WUKJNZOF.js +413 -0
- package/dist/chunk-WUKJNZOF.js.map +1 -0
- package/dist/cli.d.mts +2 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +523 -0
- package/dist/cli.js.map +1 -0
- package/dist/cli.mjs +523 -0
- package/dist/cli.mjs.map +1 -0
- package/dist/forge-test-EGP3AGFI.mjs +7 -0
- package/dist/forge-test-EGP3AGFI.mjs.map +1 -0
- package/dist/forge-test-FLCVDJFR.js +7 -0
- package/dist/forge-test-FLCVDJFR.js.map +1 -0
- package/dist/index.d.mts +206 -0
- package/dist/index.d.ts +206 -0
- package/dist/index.js +29 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +29 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +81 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/core/scan-directory.ts","../src/core/generate-interaction-tests.ts","../src/core/generate-story-content.ts","../src/core/score-coverage.ts","../src/core/infer-title.ts","../src/api/forge-story.ts","../src/core/watch.ts","../src/api/forge-stories.ts"],"sourcesContent":["import fg from 'fast-glob';\nimport * as path from 'node:path';\nimport * as fs from 'node:fs';\n\nimport {\n COMPONENT_EXTENSIONS,\n IGNORED_DIRS,\n STORY_FILE_SUFFIX,\n} from '../utils/constants';\nimport type { ScanResult, ScannedComponent } from '../utils/types';\nimport { analyzeComponent } from './analyze-component';\n\n/**\n * Scan a directory for React components and classify them by story coverage.\n */\nexport async function scanDirectory(dir: string): Promise<ScanResult> {\n const absoluteDir = path.resolve(dir);\n\n if (!fs.existsSync(absoluteDir)) {\n throw new Error(`Directory not found: ${absoluteDir}`);\n }\n\n const extensions = COMPONENT_EXTENSIONS.map((e) => e.replace('.', '')).join(',');\n const ignorePatterns = [\n ...IGNORED_DIRS.map((d) => `**/${d}/**`),\n '**/*.spec.*',\n '**/*.test.*',\n '**/*.stories.*',\n '**/*.styles.*',\n '**/*.style.*',\n '**/index.ts',\n '**/index.tsx',\n ];\n\n const files = await fg(`**/*.{${extensions}}`, {\n cwd: absoluteDir,\n ignore: ignorePatterns,\n absolute: true,\n });\n\n const components: ScannedComponent[] = [];\n const withStories: string[] = [];\n const withoutStories: string[] = [];\n const notAnalyzable: string[] = [];\n\n for (const filePath of files) {\n const analysis = analyzeComponent(filePath);\n const storyPath = filePath.replace(/\\.tsx?$/, STORY_FILE_SUFFIX);\n const hasStory = fs.existsSync(storyPath);\n\n components.push({ filePath, analysis, hasStory });\n\n if (!analysis) {\n notAnalyzable.push(filePath);\n } else if (hasStory) {\n withStories.push(filePath);\n } else {\n withoutStories.push(filePath);\n }\n }\n\n return {\n components,\n withStories,\n withoutStories,\n notAnalyzable,\n total: files.length,\n };\n}\n","import type { ComponentAnalysis, PropInfo } from '../utils/types';\n\n/**\n * Generate interaction test stories (play functions) based on component analysis.\n * Returns an array of lines to append to the story file.\n */\nexport function generateInteractionTests(\n analysis: ComponentAnalysis,\n): string[] {\n const lines: string[] = [];\n const { props, name, hasChildren } = analysis;\n\n const clickHandlers = props.filter(\n (p) =>\n p.isCallback &&\n (p.name === 'onClick' ||\n p.name === 'onPress' ||\n p.name === 'onSubmit' ||\n p.name === 'onClose')\n );\n\n const toggleProps = props.filter(\n (p) =>\n !p.isCallback &&\n (p.type.toLowerCase() === 'boolean' || p.type.toLowerCase() === 'bool') &&\n (p.name === 'checked' ||\n p.name === 'isChecked' ||\n p.name === 'selected' ||\n p.name === 'open' ||\n p.name === 'isOpen')\n );\n\n // Click interaction test\n if (clickHandlers.length > 0) {\n lines.push(...buildClickTest(name, clickHandlers));\n }\n\n // Render test (always generate)\n lines.push(...buildRenderTest(name, hasChildren));\n\n // Keyboard navigation test\n if (clickHandlers.length > 0 || toggleProps.length > 0) {\n lines.push('');\n lines.push(...buildKeyboardTest(name, clickHandlers));\n }\n\n // Accessibility test (always generate)\n lines.push('');\n lines.push(...buildA11yTest(name, hasChildren));\n\n return lines;\n}\n\nfunction buildClickTest(\n componentName: string,\n clickHandlers: PropInfo[],\n): string[] {\n const lines: string[] = [];\n const handler = clickHandlers[0];\n\n lines.push(`export const ClickInteraction: Story = {`);\n lines.push(` args: {`);\n lines.push(` ${handler.name}: fn(),`);\n lines.push(` },`);\n lines.push(` play: async ({ canvasElement, args }) => {`);\n lines.push(` const canvas = within(canvasElement);`);\n lines.push('');\n\n if (handler.name === 'onSubmit') {\n lines.push(\n ` const submitButton = canvas.getByRole('button', { name: /submit/i });`\n );\n lines.push(` await userEvent.click(submitButton);`);\n lines.push('');\n lines.push(` await expect(args.${handler.name}).toHaveBeenCalledTimes(1);`);\n } else if (handler.name === 'onClose') {\n lines.push(\n ` const closeButton = canvas.getByRole('button', { name: /close/i });`\n );\n lines.push(` await userEvent.click(closeButton);`);\n lines.push('');\n lines.push(` await expect(args.${handler.name}).toHaveBeenCalledTimes(1);`);\n } else {\n lines.push(\n ` const element = canvas.getByRole('button');`\n );\n lines.push(` await userEvent.click(element);`);\n lines.push('');\n lines.push(` await expect(args.${handler.name}).toHaveBeenCalledTimes(1);`);\n }\n\n lines.push(` },`);\n lines.push(`};`);\n\n return lines;\n}\n\nfunction buildRenderTest(\n componentName: string,\n hasChildren: boolean,\n): string[] {\n const lines: string[] = [];\n\n lines.push('');\n lines.push(`export const RendersCorrectly: Story = {`);\n\n if (hasChildren) {\n lines.push(` args: {`);\n lines.push(` children: 'Test content',`);\n lines.push(` },`);\n }\n\n lines.push(` play: async ({ canvasElement }) => {`);\n lines.push(` const canvas = within(canvasElement);`);\n lines.push('');\n\n if (hasChildren) {\n lines.push(` const element = canvas.getByText('Test content');`);\n lines.push(` await expect(element).toBeInTheDocument();`);\n } else {\n lines.push(` // Verify the component renders without crashing`);\n lines.push(` await expect(canvasElement.firstChild).toBeInTheDocument();`);\n }\n\n lines.push(` },`);\n lines.push(`};`);\n\n return lines;\n}\n\nfunction buildA11yTest(\n componentName: string,\n hasChildren: boolean,\n): string[] {\n const lines: string[] = [];\n\n lines.push(`export const AccessibilityAudit: Story = {`);\n lines.push(` tags: ['a11y'],`);\n\n if (hasChildren) {\n lines.push(` args: {`);\n lines.push(` children: '${componentName} content',`);\n lines.push(` },`);\n }\n\n lines.push(` play: async ({ canvasElement }) => {`);\n lines.push(` const canvas = within(canvasElement);`);\n lines.push('');\n lines.push(` // Verify component is rendered and visible`);\n\n if (hasChildren) {\n lines.push(\n ` const element = canvas.getByText('${componentName} content');`\n );\n lines.push(` await expect(element).toBeInTheDocument();`);\n } else {\n lines.push(\n ` await expect(canvasElement.firstChild).toBeInTheDocument();`\n );\n }\n\n lines.push('');\n lines.push(` // Verify no implicit ARIA role violations`);\n lines.push(` // The @storybook/addon-a11y will run axe-core checks on this story`);\n lines.push(` // Configure rules in .storybook/preview.tsx via the a11y addon parameter`);\n lines.push(` },`);\n lines.push(`};`);\n\n return lines;\n}\n\nfunction buildKeyboardTest(\n _componentName: string,\n clickHandlers: PropInfo[],\n): string[] {\n const lines: string[] = [];\n const handler = clickHandlers[0];\n\n if (!handler) return lines;\n\n lines.push(`export const KeyboardNavigation: Story = {`);\n lines.push(` args: {`);\n lines.push(` ${handler.name}: fn(),`);\n lines.push(` },`);\n lines.push(` play: async ({ canvasElement, args }) => {`);\n lines.push(` const canvas = within(canvasElement);`);\n lines.push('');\n lines.push(` const element = canvas.getByRole('button');`);\n lines.push(` await userEvent.tab();`);\n lines.push(` await expect(element).toHaveFocus();`);\n lines.push('');\n lines.push(` await userEvent.keyboard('{Enter}');`);\n lines.push(` await expect(args.${handler.name}).toHaveBeenCalled();`);\n lines.push(` },`);\n lines.push(`};`);\n\n return lines;\n}\n","import type { ComponentAnalysis, PropInfo, StoryContentOptions } from '../utils/types';\nimport {\n STORYBOOK_META_IMPORT,\n STORYBOOK_TEST_IMPORT,\n} from '../utils/constants';\nimport { generateInteractionTests } from './generate-interaction-tests';\n\n/**\n * Generate the full content of a .stories.tsx file from component analysis.\n */\nexport function generateStoryContent(options: StoryContentOptions): string {\n const { analysis, storyTitle, importPath, skipInteractionTests } = options;\n const { name } = analysis;\n\n const lines: string[] = [];\n\n lines.push(...buildImports(analysis, importPath, skipInteractionTests));\n lines.push('');\n\n lines.push(...buildMeta(analysis, storyTitle));\n lines.push('');\n\n lines.push(`type Story = StoryObj<typeof ${name}>;`);\n lines.push('');\n\n lines.push(...buildDefaultStory(analysis));\n\n const variantStories = buildVariantStories(analysis);\n if (variantStories.length > 0) {\n lines.push('');\n lines.push(...variantStories);\n }\n\n if (!skipInteractionTests) {\n const interactionStories = generateInteractionTests(analysis);\n if (interactionStories.length > 0) {\n lines.push('');\n lines.push(...interactionStories);\n }\n }\n\n lines.push('');\n return lines.join('\\n');\n}\n\nfunction buildImports(\n analysis: ComponentAnalysis,\n importPath: string,\n skipInteractionTests: boolean,\n): string[] {\n const lines: string[] = [];\n const metaImports = ['Meta', 'StoryObj'];\n\n lines.push(\n `import type { ${metaImports.join(', ')} } from '${STORYBOOK_META_IMPORT}';`\n );\n\n if (!skipInteractionTests) {\n const testImports = buildTestImports(analysis);\n if (testImports.length > 0) {\n lines.push(\n `import { ${testImports.join(', ')} } from '${STORYBOOK_TEST_IMPORT}';`\n );\n }\n }\n\n if (analysis.usesRouter) {\n lines.push(\n `import { withRouter } from 'storybook-addon-react-router-v6';`\n );\n }\n\n lines.push(`import React from 'react';`);\n\n if (analysis.exportType === 'default') {\n lines.push(`import ${analysis.name} from '${importPath}';`);\n } else {\n lines.push(`import { ${analysis.name} } from '${importPath}';`);\n }\n\n return lines;\n}\n\nfunction buildTestImports(analysis: ComponentAnalysis): string[] {\n const imports = new Set<string>();\n const { props } = analysis;\n\n const hasCallbacks = props.some((p) => p.isCallback);\n const hasInteractiveProps = props.some((p) =>\n ['string', 'number'].includes(inferControlType(p) ?? '')\n );\n const hasClickable = props.some(\n (p) => p.name === 'onClick' || p.name === 'onPress'\n );\n\n if (hasCallbacks) imports.add('fn');\n if (hasClickable || hasInteractiveProps) {\n imports.add('expect');\n imports.add('within');\n }\n if (hasClickable) imports.add('userEvent');\n if (hasInteractiveProps) imports.add('userEvent');\n\n return Array.from(imports);\n}\n\nfunction buildMeta(\n analysis: ComponentAnalysis,\n storyTitle: string,\n): string[] {\n const lines: string[] = [];\n const { name, props, usesRouter } = analysis;\n\n lines.push(`const meta: Meta<typeof ${name}> = {`);\n lines.push(` component: ${name},`);\n lines.push(` title: '${storyTitle}',`);\n lines.push(` tags: ['autodocs'],`);\n\n if (usesRouter) {\n lines.push(` decorators: [withRouter],`);\n }\n\n const argTypes = buildArgTypes(props);\n if (argTypes.length > 0) {\n lines.push(` argTypes: {`);\n lines.push(...argTypes);\n lines.push(` },`);\n }\n\n const defaultArgs = buildDefaultArgs(props);\n if (defaultArgs.length > 0) {\n lines.push(` args: {`);\n lines.push(...defaultArgs);\n lines.push(` },`);\n }\n\n lines.push(`};`);\n lines.push('');\n lines.push(`export default meta;`);\n\n return lines;\n}\n\nfunction buildArgTypes(props: PropInfo[]): string[] {\n const lines: string[] = [];\n\n for (const prop of props) {\n if (prop.name === 'children') continue;\n\n if (prop.isCallback) {\n lines.push(` ${prop.name}: { action: '${prop.name}' },`);\n continue;\n }\n\n if (prop.unionValues && prop.unionValues.length > 0) {\n const options = prop.unionValues.map((v) => `'${v}'`).join(', ');\n lines.push(` ${prop.name}: {`);\n lines.push(` options: [${options}],`);\n lines.push(` control: { type: 'select' },`);\n lines.push(` },`);\n continue;\n }\n\n const controlType = inferControlType(prop);\n if (controlType) {\n lines.push(` ${prop.name}: { control: { type: '${controlType}' } },`);\n }\n }\n\n return lines;\n}\n\nfunction buildDefaultArgs(props: PropInfo[]): string[] {\n const lines: string[] = [];\n\n for (const prop of props) {\n if (!prop.required) continue;\n\n if (prop.isCallback) {\n lines.push(` ${prop.name}: fn(),`);\n continue;\n }\n\n const defaultValue = inferDefaultValue(prop);\n if (defaultValue !== undefined) {\n lines.push(` ${prop.name}: ${defaultValue},`);\n }\n }\n\n return lines;\n}\n\nfunction buildDefaultStory(analysis: ComponentAnalysis): string[] {\n const lines: string[] = [];\n const { hasChildren } = analysis;\n\n lines.push(`export const Default: Story = {`);\n\n if (hasChildren) {\n lines.push(` args: {`);\n lines.push(` children: '${analysis.name} content',`);\n lines.push(` },`);\n }\n\n lines.push(`};`);\n return lines;\n}\n\nfunction buildVariantStories(analysis: ComponentAnalysis): string[] {\n const lines: string[] = [];\n const { props, name } = analysis;\n\n const sizeProp = props.find((p) => p.name === 'size');\n if (sizeProp?.unionValues && sizeProp.unionValues.length > 0) {\n lines.push(`export const Sizes: Story = {`);\n lines.push(` render: (args) => (`);\n lines.push(` <>`);\n for (const size of sizeProp.unionValues) {\n lines.push(\n ` <${name} {...args} size=\"${size}\">${analysis.hasChildren ? `Size ${size}` : ''}</${name}>`\n );\n }\n lines.push(` </>`);\n lines.push(` ),`);\n lines.push(`};`);\n lines.push('');\n }\n\n const variantProp = props.find((p) => p.name === 'variant');\n if (variantProp?.unionValues && variantProp.unionValues.length > 0) {\n lines.push(`export const Variants: Story = {`);\n lines.push(` render: (args) => (`);\n lines.push(` <>`);\n for (const variant of variantProp.unionValues) {\n lines.push(\n ` <${name} {...args} variant=\"${variant}\">${analysis.hasChildren ? `Variant ${variant}` : ''}</${name}>`\n );\n }\n lines.push(` </>`);\n lines.push(` ),`);\n lines.push(`};`);\n lines.push('');\n }\n\n const colorPaletteProp = props.find((p) => p.name === 'colorPalette');\n if (colorPaletteProp?.unionValues && colorPaletteProp.unionValues.length > 0) {\n lines.push(`export const ColorPalettes: Story = {`);\n lines.push(` render: (args) => (`);\n lines.push(` <>`);\n for (const palette of colorPaletteProp.unionValues) {\n lines.push(\n ` <${name} {...args} colorPalette=\"${palette}\">${analysis.hasChildren ? palette : ''}</${name}>`\n );\n }\n lines.push(` </>`);\n lines.push(` ),`);\n lines.push(`};`);\n lines.push('');\n }\n\n const disabledProp = props.find(\n (p) => p.name === 'disabled' || p.name === 'isDisabled'\n );\n if (disabledProp) {\n lines.push(`export const Disabled: Story = {`);\n lines.push(` args: {`);\n lines.push(` ${disabledProp.name}: true,`);\n lines.push(` },`);\n lines.push(`};`);\n }\n\n return lines;\n}\n\nfunction inferControlType(prop: PropInfo): string | null {\n const type = prop.type.toLowerCase();\n\n if (type === 'boolean' || type === 'bool') return 'boolean';\n if (type === 'string') return 'text';\n if (type === 'number') return 'number';\n if (type.includes('react.reactnode') || type.includes('reactnode'))\n return null;\n if (prop.isCallback) return null;\n\n return null;\n}\n\nfunction inferDefaultValue(prop: PropInfo): string | undefined {\n if (prop.defaultValue) return prop.defaultValue;\n\n const type = prop.type.toLowerCase();\n\n if (type === 'string') return `'Example ${prop.name}'`;\n if (type === 'number') return '0';\n if (type === 'boolean' || type === 'bool') return 'false';\n\n if (prop.unionValues && prop.unionValues.length > 0) {\n return `'${prop.unionValues[0]}'`;\n }\n\n if (prop.isCallback) return 'fn()';\n\n return undefined;\n}\n","import type { CoverageReport } from '../utils/types';\n\n/**\n * Calculate a coverage score and letter grade from covered/total counts.\n */\nexport function scoreCoverage(covered: number, total: number): CoverageReport {\n if (total === 0) {\n return { covered: 0, total: 0, percentage: 0, grade: 'F' };\n }\n\n const percentage = Math.round((covered / total) * 100);\n\n let grade: CoverageReport['grade'];\n if (percentage >= 90) grade = 'A';\n else if (percentage >= 75) grade = 'B';\n else if (percentage >= 50) grade = 'C';\n else if (percentage >= 25) grade = 'D';\n else grade = 'F';\n\n return { covered, total, percentage, grade };\n}\n","import * as path from 'node:path';\n\n/**\n * Infer a Storybook title from a component's file path.\n *\n * Strategy:\n * 1. Compute relative path from the target directory\n * 2. Strip common prefixes: src/, lib/, components/\n * 3. Convert directory segments to PascalCase\n * 4. Deduplicate consecutive identical segments\n *\n * Examples:\n * src/components/Button/Button.tsx → \"Components / Button\"\n * src/components/forms/TextInput.tsx → \"Components / Forms / TextInput\"\n * src/lib/ui/Modal/Modal.tsx → \"UI / Modal\"\n * libs/shared/ui/src/lib/button/button.tsx → \"Button\"\n */\nexport function inferStoryTitle(filePath: string, baseDir?: string): string {\n const resolvedBase = baseDir ? path.resolve(baseDir) : path.dirname(filePath);\n const resolvedFile = path.resolve(filePath);\n\n let relative = path.relative(resolvedBase, resolvedFile);\n\n // Strip the filename — we only want directory segments\n const fileName = path.basename(relative, path.extname(relative));\n const dirPart = path.dirname(relative);\n\n if (dirPart === '.') {\n return `Components / ${toPascalCase(fileName)}`;\n }\n\n // Split into segments and clean up\n let segments = dirPart.split(path.sep);\n\n // Strip common prefixes\n const stripPrefixes = ['src', 'lib', 'source'];\n while (segments.length > 0 && stripPrefixes.includes(segments[0].toLowerCase())) {\n segments.shift();\n }\n\n // Convert to PascalCase\n const parts = segments.map((seg) => toPascalCase(seg));\n\n // Add the component name\n const componentName = toPascalCase(fileName);\n parts.push(componentName);\n\n // Deduplicate consecutive identical segments\n const deduped: string[] = [];\n for (const part of parts) {\n if (part !== deduped[deduped.length - 1]) {\n deduped.push(part);\n }\n }\n\n if (deduped.length === 0) {\n return `Components / ${componentName}`;\n }\n\n return deduped.join(' / ');\n}\n\nfunction toPascalCase(str: string): string {\n return str\n .split(/[-_]/)\n .map((part) => part.charAt(0).toUpperCase() + part.slice(1))\n .join('');\n}\n","import * as fs from 'node:fs';\nimport * as path from 'node:path';\n\nimport { COMPONENT_EXTENSIONS, STORY_FILE_SUFFIX } from '../utils/constants';\nimport type { ForgeStoryOptions, ComponentAnalysis } from '../utils/types';\nimport { analyzeComponent } from '../core/analyze-component';\nimport { generateStoryContent } from '../core/generate-story-content';\nimport { inferStoryTitle } from '../core/infer-title';\n\nexport interface ForgeStoryResult {\n storyPath: string;\n content: string;\n analysis: ComponentAnalysis;\n storiesGenerated: string[];\n written: boolean;\n}\n\n/**\n * High-level: analyze a component, generate a story, and write it to disk.\n */\nexport async function forgeStory(options: ForgeStoryOptions): Promise<ForgeStoryResult> {\n const { componentPath, storyTitle, skipInteractionTests = false, overwrite = false, dryRun = false } = options;\n\n // Resolve component file\n const resolvedPath = resolveComponentPath(componentPath);\n if (!resolvedPath) {\n throw new Error(`Component file not found: ${componentPath}`);\n }\n\n // Check for existing story\n const storyPath = resolvedPath.replace(/\\.tsx?$/, STORY_FILE_SUFFIX);\n if (fs.existsSync(storyPath) && !overwrite) {\n throw new Error(`Story already exists: ${storyPath}. Use --overwrite to replace.`);\n }\n\n // Analyze\n const analysis = analyzeComponent(resolvedPath);\n if (!analysis) {\n throw new Error(`Could not analyze component at ${resolvedPath}. Ensure it exports a React component.`);\n }\n\n // Infer title\n const title = storyTitle ?? inferStoryTitle(resolvedPath);\n const importPath = `./${analysis.fileName}`;\n\n // Generate\n const content = generateStoryContent({\n analysis,\n storyTitle: title,\n importPath,\n skipInteractionTests,\n });\n\n // Determine which stories were generated\n const storiesGenerated = collectStoriesGenerated(analysis, skipInteractionTests);\n\n // Write\n let written = false;\n if (!dryRun) {\n const dir = path.dirname(storyPath);\n fs.mkdirSync(dir, { recursive: true });\n fs.writeFileSync(storyPath, content, 'utf-8');\n written = true;\n }\n\n return { storyPath, content, analysis, storiesGenerated, written };\n}\n\nfunction resolveComponentPath(componentPath: string): string | null {\n const resolved = path.resolve(componentPath);\n\n if (fs.existsSync(resolved)) return resolved;\n\n for (const ext of COMPONENT_EXTENSIONS) {\n const withExt = resolved + ext;\n if (fs.existsSync(withExt)) return withExt;\n }\n\n return null;\n}\n\nfunction collectStoriesGenerated(analysis: ComponentAnalysis, skipInteractionTests: boolean): string[] {\n const stories = ['Default'];\n\n if (analysis.props.some((p) => p.name === 'size' && p.unionValues?.length)) {\n stories.push('Sizes');\n }\n if (analysis.props.some((p) => p.name === 'variant' && p.unionValues?.length)) {\n stories.push('Variants');\n }\n if (analysis.props.some((p) => p.name === 'colorPalette' && p.unionValues?.length)) {\n stories.push('ColorPalettes');\n }\n if (analysis.props.some((p) => p.name === 'disabled' || p.name === 'isDisabled')) {\n stories.push('Disabled');\n }\n\n if (!skipInteractionTests) {\n stories.push('RendersCorrectly', 'AccessibilityAudit');\n if (analysis.props.some(\n (p) => p.isCallback && ['onClick', 'onPress', 'onSubmit', 'onClose'].includes(p.name)\n )) {\n stories.push('ClickInteraction', 'KeyboardNavigation');\n }\n }\n\n return stories;\n}\n","import { watch as chokidarWatch } from 'chokidar';\nimport * as path from 'node:path';\nimport * as fs from 'node:fs';\n\nimport {\n COMPONENT_EXTENSIONS,\n IGNORED_DIRS,\n STORY_FILE_SUFFIX,\n DEFAULT_DEBOUNCE_MS,\n} from '../utils/constants';\nimport type { WatchOptions } from '../utils/types';\nimport { forgeStory } from '../api/forge-story';\n\nexport interface WatchHandle {\n close(): Promise<void>;\n}\n\nexport type WatchEvent =\n | { type: 'ready' }\n | { type: 'generate'; file: string; storyPath: string }\n | { type: 'update'; file: string; storyPath: string }\n | { type: 'error'; file: string; error: Error };\n\nexport type WatchCallback = (event: WatchEvent) => void;\n\n/**\n * Watch a directory for component changes and auto-generate stories.\n * Returns a handle with .close() for cleanup.\n */\nexport function watchDirectory(\n options: WatchOptions,\n callback?: WatchCallback,\n): WatchHandle {\n const {\n dir,\n ignore = [],\n debounceMs = DEFAULT_DEBOUNCE_MS,\n skipInteractionTests = false,\n } = options;\n\n const absoluteDir = path.resolve(dir);\n\n if (!fs.existsSync(absoluteDir)) {\n throw new Error(`Watch directory not found: ${absoluteDir}`);\n }\n\n const extensions = COMPONENT_EXTENSIONS.map((e) => e.replace('.', ''));\n const globPattern = `**/*.{${extensions.join(',')}}`;\n\n const ignored = [\n ...IGNORED_DIRS.map((d) => `**/${d}/**`),\n '**/*.spec.*',\n '**/*.test.*',\n '**/*.stories.*',\n '**/*.styles.*',\n '**/*.style.*',\n '**/index.{ts,tsx}',\n ...ignore,\n ];\n\n const pending = new Map<string, ReturnType<typeof setTimeout>>();\n\n const watcher = chokidarWatch(globPattern, {\n cwd: absoluteDir,\n ignored,\n ignoreInitial: true,\n awaitWriteFinish: {\n stabilityThreshold: 100,\n pollInterval: 50,\n },\n });\n\n const processChange = async (relativePath: string) => {\n const filePath = path.join(absoluteDir, relativePath);\n const storyPath = filePath.replace(/\\.tsx?$/, STORY_FILE_SUFFIX);\n const isUpdate = fs.existsSync(storyPath);\n\n try {\n await forgeStory({\n componentPath: filePath,\n skipInteractionTests,\n overwrite: true,\n quiet: true,\n });\n\n callback?.({\n type: isUpdate ? 'update' : 'generate',\n file: filePath,\n storyPath,\n });\n } catch (err) {\n callback?.({\n type: 'error',\n file: filePath,\n error: err as Error,\n });\n }\n };\n\n const debouncedProcess = (relativePath: string) => {\n if (pending.has(relativePath)) {\n clearTimeout(pending.get(relativePath)!);\n }\n\n pending.set(\n relativePath,\n setTimeout(() => {\n pending.delete(relativePath);\n processChange(relativePath);\n }, debounceMs),\n );\n };\n\n watcher.on('change', debouncedProcess);\n watcher.on('add', debouncedProcess);\n watcher.on('ready', () => callback?.({ type: 'ready' }));\n\n return {\n async close() {\n for (const timeout of pending.values()) {\n clearTimeout(timeout);\n }\n pending.clear();\n await watcher.close();\n },\n };\n}\n","import * as path from 'node:path';\n\nimport type { ForgeStoriesOptions, CoverageReport } from '../utils/types';\nimport { scanDirectory } from '../core/scan-directory';\nimport { scoreCoverage } from '../core/score-coverage';\nimport { forgeStory } from './forge-story';\n\nexport interface ForgeStoriesResult {\n generated: number;\n failed: number;\n alreadyCovered: number;\n notAnalyzable: number;\n total: number;\n coverage: CoverageReport;\n errors: Array<{ file: string; error: string }>;\n}\n\n/**\n * High-level: scan a directory, generate stories for all uncovered components.\n */\nexport async function forgeStories(options: ForgeStoriesOptions): Promise<ForgeStoriesResult> {\n const {\n dir,\n skipInteractionTests = false,\n overwrite = false,\n dryRun = false,\n includeComponentTests = false,\n } = options;\n\n // Import forgeTest lazily to avoid circular dependency at startup\n const { forgeTest } = includeComponentTests\n ? await import('./forge-test')\n : { forgeTest: null };\n\n const absoluteDir = path.resolve(dir);\n const scan = await scanDirectory(absoluteDir);\n\n let generated = 0;\n let failed = 0;\n const errors: Array<{ file: string; error: string }> = [];\n\n const filesToProcess = overwrite\n ? [...scan.withoutStories, ...scan.withStories]\n : scan.withoutStories;\n\n for (const filePath of filesToProcess) {\n try {\n await forgeStory({\n componentPath: filePath,\n skipInteractionTests,\n overwrite: true,\n dryRun,\n quiet: true,\n });\n generated++;\n\n // Generate Playwright component tests if requested\n if (forgeTest) {\n try {\n await forgeTest({\n componentPath: filePath,\n overwrite: true,\n dryRun,\n quiet: true,\n });\n } catch {\n // Non-fatal — story is the primary artifact\n }\n }\n } catch (err) {\n failed++;\n errors.push({\n file: filePath,\n error: (err as Error).message,\n });\n }\n }\n\n const totalAnalyzable = scan.total - scan.notAnalyzable.length;\n // When overwriting, generated count includes previously-covered files\n const totalCovered = overwrite\n ? generated\n : scan.withStories.length + generated;\n const coverage = scoreCoverage(totalCovered, totalAnalyzable);\n\n return {\n generated,\n failed,\n alreadyCovered: scan.withStories.length,\n notAnalyzable: scan.notAnalyzable.length,\n total: totalAnalyzable,\n coverage,\n errors,\n };\n}\n"],"mappings":"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","names":["path","relative","fs","path","path","fs","path"]}
|
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
// src/api/forge-test.ts
|
|
2
|
+
import * as fs2 from "fs";
|
|
3
|
+
import * as path2 from "path";
|
|
4
|
+
|
|
5
|
+
// src/utils/constants.ts
|
|
6
|
+
var STORY_FILE_SUFFIX = ".stories.tsx";
|
|
7
|
+
var CT_FILE_SUFFIX = ".ct.tsx";
|
|
8
|
+
var COMPONENT_EXTENSIONS = [".tsx", ".ts"];
|
|
9
|
+
var IGNORED_DIRS = [
|
|
10
|
+
"node_modules",
|
|
11
|
+
"dist",
|
|
12
|
+
"build",
|
|
13
|
+
".storybook",
|
|
14
|
+
".next",
|
|
15
|
+
".vite",
|
|
16
|
+
"coverage",
|
|
17
|
+
"__tests__",
|
|
18
|
+
"__mocks__"
|
|
19
|
+
];
|
|
20
|
+
var CHAKRA_IMPORTS = ["@chakra-ui/react", "@redesignhealth/ui"];
|
|
21
|
+
var ROUTER_IMPORTS = ["react-router-dom", "react-router"];
|
|
22
|
+
var QUERY_IMPORTS = ["@tanstack/react-query", "react-query"];
|
|
23
|
+
var DEFAULT_DEBOUNCE_MS = 300;
|
|
24
|
+
var STORYBOOK_TEST_IMPORT = "@storybook/test";
|
|
25
|
+
var STORYBOOK_META_IMPORT = "@storybook/react-vite";
|
|
26
|
+
|
|
27
|
+
// src/core/analyze-component.ts
|
|
28
|
+
import * as fs from "fs";
|
|
29
|
+
import * as path from "path";
|
|
30
|
+
function analyzeComponent(filePath, content) {
|
|
31
|
+
if (!content) {
|
|
32
|
+
try {
|
|
33
|
+
content = fs.readFileSync(filePath, "utf-8");
|
|
34
|
+
} catch {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
if (!content.trim()) return null;
|
|
39
|
+
const name = extractComponentName(content, filePath);
|
|
40
|
+
if (!name) return null;
|
|
41
|
+
const imports = extractImports(content);
|
|
42
|
+
const props = extractProps(content, name);
|
|
43
|
+
return {
|
|
44
|
+
name,
|
|
45
|
+
fileName: path.basename(filePath).replace(/\.tsx?$/, ""),
|
|
46
|
+
filePath,
|
|
47
|
+
props,
|
|
48
|
+
hasChildren: props.some((p) => p.name === "children"),
|
|
49
|
+
imports,
|
|
50
|
+
usesRouter: imports.some(
|
|
51
|
+
(i) => ROUTER_IMPORTS.some((r) => i.source.includes(r))
|
|
52
|
+
),
|
|
53
|
+
usesReactQuery: imports.some(
|
|
54
|
+
(i) => QUERY_IMPORTS.some((r) => i.source.includes(r))
|
|
55
|
+
),
|
|
56
|
+
usesChakra: imports.some(
|
|
57
|
+
(i) => CHAKRA_IMPORTS.some((r) => i.source.includes(r))
|
|
58
|
+
),
|
|
59
|
+
exportType: detectExportType(content, name)
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
function extractComponentName(content, filePath) {
|
|
63
|
+
const namedExport = content.match(
|
|
64
|
+
/export\s+(?:const|function)\s+([A-Z][A-Za-z0-9]*)/
|
|
65
|
+
);
|
|
66
|
+
if (namedExport) return namedExport[1];
|
|
67
|
+
const defaultFn = content.match(
|
|
68
|
+
/export\s+default\s+function\s+([A-Z][A-Za-z0-9]*)/
|
|
69
|
+
);
|
|
70
|
+
if (defaultFn) return defaultFn[1];
|
|
71
|
+
const constThenDefault = content.match(
|
|
72
|
+
/(?:const|function)\s+([A-Z][A-Za-z0-9]*)\s*=[\s\S]*?export\s+default\s+\1/
|
|
73
|
+
);
|
|
74
|
+
if (constThenDefault) return constThenDefault[1];
|
|
75
|
+
const forwardRef = content.match(
|
|
76
|
+
/export\s+(?:const|default)\s+(?:const\s+)?([A-Z][A-Za-z0-9]*)\s*=\s*(?:React\.)?(?:forwardRef|memo)\s*[<(]/
|
|
77
|
+
);
|
|
78
|
+
if (forwardRef) return forwardRef[1];
|
|
79
|
+
const fileName = filePath.split("/").pop()?.replace(/\.tsx?$/, "");
|
|
80
|
+
if (fileName && /[a-zA-Z]/.test(fileName)) {
|
|
81
|
+
return fileName.split("-").map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
|
|
82
|
+
}
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
function extractImports(content) {
|
|
86
|
+
const imports = [];
|
|
87
|
+
const importRegex = /import\s+(?:(?:(\{[^}]+\})|(\w+))\s+from\s+)?['"]([^'"]+)['"]/g;
|
|
88
|
+
let match;
|
|
89
|
+
while ((match = importRegex.exec(content)) !== null) {
|
|
90
|
+
const namedImports = match[1];
|
|
91
|
+
const defaultImport = match[2];
|
|
92
|
+
const source = match[3];
|
|
93
|
+
const specifiers = [];
|
|
94
|
+
if (namedImports) {
|
|
95
|
+
const names = namedImports.replace(/[{}]/g, "").split(",").map((s) => s.trim().split(/\s+as\s+/)[0].trim()).filter(Boolean);
|
|
96
|
+
specifiers.push(...names);
|
|
97
|
+
}
|
|
98
|
+
if (defaultImport) {
|
|
99
|
+
specifiers.push(defaultImport);
|
|
100
|
+
}
|
|
101
|
+
imports.push({ source, specifiers });
|
|
102
|
+
}
|
|
103
|
+
return imports;
|
|
104
|
+
}
|
|
105
|
+
function extractProps(content, componentName) {
|
|
106
|
+
const props = [];
|
|
107
|
+
const propsTypeName = findPropsTypeName(content, componentName);
|
|
108
|
+
if (!propsTypeName) return props;
|
|
109
|
+
const interfaceMatch = content.match(
|
|
110
|
+
new RegExp(
|
|
111
|
+
`(?:interface|type)\\s+${propsTypeName}\\s*(?:extends\\s+[^{]+)?\\s*(?:=\\s*)?(?:[^{]*&\\s*)?\\{([^}]*)\\}`,
|
|
112
|
+
"s"
|
|
113
|
+
)
|
|
114
|
+
);
|
|
115
|
+
if (!interfaceMatch) return props;
|
|
116
|
+
const body = interfaceMatch[1];
|
|
117
|
+
const propLines = body.split("\n").filter((line) => {
|
|
118
|
+
const trimmed = line.trim();
|
|
119
|
+
return trimmed && !trimmed.startsWith("//") && !trimmed.startsWith("*");
|
|
120
|
+
});
|
|
121
|
+
for (const line of propLines) {
|
|
122
|
+
const propMatch = line.match(
|
|
123
|
+
/^\s*(\w+)(\??):\s*(.+?)(?:\s*\/\/.*)?;?\s*$/
|
|
124
|
+
);
|
|
125
|
+
if (!propMatch) continue;
|
|
126
|
+
const [, name, optional, rawType] = propMatch;
|
|
127
|
+
const type = rawType.trim().replace(/;$/, "");
|
|
128
|
+
const isCallback = type.includes("=>") || type.startsWith("(") || /^on[A-Z]/.test(name);
|
|
129
|
+
const unionValues = extractUnionValues(type);
|
|
130
|
+
props.push({
|
|
131
|
+
name,
|
|
132
|
+
type,
|
|
133
|
+
required: optional !== "?",
|
|
134
|
+
isCallback,
|
|
135
|
+
unionValues: unionValues.length > 0 ? unionValues : void 0
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
return props;
|
|
139
|
+
}
|
|
140
|
+
function findPropsTypeName(content, componentName) {
|
|
141
|
+
const destructuredProps = content.match(
|
|
142
|
+
new RegExp(
|
|
143
|
+
`(?:const|function)\\s+${componentName}\\s*(?:=\\s*)?\\([^)]*:\\s*([A-Z]\\w*)`
|
|
144
|
+
)
|
|
145
|
+
);
|
|
146
|
+
if (destructuredProps) return destructuredProps[1];
|
|
147
|
+
const regularProps = content.match(
|
|
148
|
+
new RegExp(
|
|
149
|
+
`(?:const|function)\\s+${componentName}\\s*(?:=\\s*)?\\(\\s*(?:props|\\w+)\\s*:\\s*([A-Z]\\w*)`
|
|
150
|
+
)
|
|
151
|
+
);
|
|
152
|
+
if (regularProps) return regularProps[1];
|
|
153
|
+
const fcProps = content.match(
|
|
154
|
+
new RegExp(`${componentName}[^=]*=\\s*(?:React\\.)?FC<([A-Z]\\w*)>`)
|
|
155
|
+
);
|
|
156
|
+
if (fcProps) return fcProps[1];
|
|
157
|
+
const defaultPropsName = `${componentName}Props`;
|
|
158
|
+
if (content.includes(`interface ${defaultPropsName}`) || content.includes(`type ${defaultPropsName}`)) {
|
|
159
|
+
return defaultPropsName;
|
|
160
|
+
}
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
function extractUnionValues(type) {
|
|
164
|
+
const singleQuote = type.match(/^'[^']*'(?:\s*\|\s*'[^']*')+$/);
|
|
165
|
+
if (singleQuote) {
|
|
166
|
+
return type.split("|").map((v) => v.trim().replace(/'/g, "")).filter(Boolean);
|
|
167
|
+
}
|
|
168
|
+
const doubleQuote = type.match(/^"[^"]*"(?:\s*\|\s*"[^"]*")+$/);
|
|
169
|
+
if (doubleQuote) {
|
|
170
|
+
return type.split("|").map((v) => v.trim().replace(/"/g, "")).filter(Boolean);
|
|
171
|
+
}
|
|
172
|
+
return [];
|
|
173
|
+
}
|
|
174
|
+
function detectExportType(content, componentName) {
|
|
175
|
+
const hasDefault = content.includes(`export default ${componentName}`) || content.includes(`export default function ${componentName}`);
|
|
176
|
+
const hasNamed = content.includes(`export const ${componentName}`) || content.includes(`export function ${componentName}`);
|
|
177
|
+
if (hasDefault && hasNamed) return "both";
|
|
178
|
+
if (hasDefault) return "default";
|
|
179
|
+
return "named";
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// src/core/generate-playwright-test.ts
|
|
183
|
+
function generatePlaywrightTest(options) {
|
|
184
|
+
const { analysis, importPath, hasStories, storyImportPath } = options;
|
|
185
|
+
const { name, props, exportType, hasChildren } = analysis;
|
|
186
|
+
const lines = [];
|
|
187
|
+
lines.push(`import { test, expect } from '@playwright/experimental-ct-react';`);
|
|
188
|
+
lines.push(`import AxeBuilder from '@axe-core/playwright';`);
|
|
189
|
+
if (exportType === "default") {
|
|
190
|
+
lines.push(`import ${name} from '${importPath}';`);
|
|
191
|
+
} else {
|
|
192
|
+
lines.push(`import { ${name} } from '${importPath}';`);
|
|
193
|
+
}
|
|
194
|
+
if (hasStories && storyImportPath) {
|
|
195
|
+
lines.push(`import * as stories from '${storyImportPath}';`);
|
|
196
|
+
}
|
|
197
|
+
lines.push("");
|
|
198
|
+
lines.push(`test.describe('${name}', () => {`);
|
|
199
|
+
lines.push(` test('mounts and renders without crashing', async ({ mount }) => {`);
|
|
200
|
+
lines.push(` const component = await mount(`);
|
|
201
|
+
lines.push(` <${name}${buildMinimalProps(props, hasChildren, name)} />`);
|
|
202
|
+
lines.push(` );`);
|
|
203
|
+
lines.push(` await expect(component).toBeVisible();`);
|
|
204
|
+
lines.push(` });`);
|
|
205
|
+
lines.push("");
|
|
206
|
+
lines.push(` test('matches visual snapshot', async ({ mount }) => {`);
|
|
207
|
+
lines.push(` const component = await mount(`);
|
|
208
|
+
lines.push(` <${name}${buildMinimalProps(props, hasChildren, name)} />`);
|
|
209
|
+
lines.push(` );`);
|
|
210
|
+
lines.push(` await expect(component).toHaveScreenshot('${toKebab(name)}-default.png');`);
|
|
211
|
+
lines.push(` });`);
|
|
212
|
+
lines.push("");
|
|
213
|
+
const clickHandler = props.find(
|
|
214
|
+
(p) => p.isCallback && (p.name === "onClick" || p.name === "onPress")
|
|
215
|
+
);
|
|
216
|
+
if (clickHandler) {
|
|
217
|
+
lines.push(` test('handles click interaction', async ({ mount }) => {`);
|
|
218
|
+
lines.push(` let clicked = false;`);
|
|
219
|
+
lines.push(` const component = await mount(`);
|
|
220
|
+
lines.push(` <${name}`);
|
|
221
|
+
lines.push(` ${clickHandler.name}={() => { clicked = true; }}`);
|
|
222
|
+
lines.push(...buildRequiredPropsLines(props, hasChildren, name, " "));
|
|
223
|
+
lines.push(` />`);
|
|
224
|
+
lines.push(` );`);
|
|
225
|
+
lines.push(` await component.click();`);
|
|
226
|
+
lines.push(` expect(clicked).toBe(true);`);
|
|
227
|
+
lines.push(` });`);
|
|
228
|
+
lines.push("");
|
|
229
|
+
}
|
|
230
|
+
const onChangeHandler = props.find(
|
|
231
|
+
(p) => p.isCallback && (p.name === "onChange" || p.name === "onValueChange")
|
|
232
|
+
);
|
|
233
|
+
if (onChangeHandler) {
|
|
234
|
+
lines.push(` test('handles value change', async ({ mount }) => {`);
|
|
235
|
+
lines.push(` let changed = false;`);
|
|
236
|
+
lines.push(` const component = await mount(`);
|
|
237
|
+
lines.push(` <${name}`);
|
|
238
|
+
lines.push(` ${onChangeHandler.name}={() => { changed = true; }}`);
|
|
239
|
+
lines.push(...buildRequiredPropsLines(props, hasChildren, name, " "));
|
|
240
|
+
lines.push(` />`);
|
|
241
|
+
lines.push(` );`);
|
|
242
|
+
lines.push(` // Trigger a change event appropriate to the component type`);
|
|
243
|
+
lines.push(` await component.locator('input, select, textarea').first().fill('test');`);
|
|
244
|
+
lines.push(` expect(changed).toBe(true);`);
|
|
245
|
+
lines.push(` });`);
|
|
246
|
+
lines.push("");
|
|
247
|
+
}
|
|
248
|
+
const disabledProp = props.find(
|
|
249
|
+
(p) => p.name === "disabled" || p.name === "isDisabled"
|
|
250
|
+
);
|
|
251
|
+
if (disabledProp) {
|
|
252
|
+
lines.push(` test('renders disabled state correctly', async ({ mount }) => {`);
|
|
253
|
+
lines.push(` const component = await mount(`);
|
|
254
|
+
lines.push(` <${name}`);
|
|
255
|
+
lines.push(` ${disabledProp.name}={true}`);
|
|
256
|
+
lines.push(...buildRequiredPropsLines(props, hasChildren, name, " "));
|
|
257
|
+
lines.push(` />`);
|
|
258
|
+
lines.push(` );`);
|
|
259
|
+
lines.push(` await expect(component).toHaveScreenshot('${toKebab(name)}-disabled.png');`);
|
|
260
|
+
if (clickHandler) {
|
|
261
|
+
lines.push(` // Verify click handler is not triggered when disabled`);
|
|
262
|
+
lines.push(` let clicked = false;`);
|
|
263
|
+
lines.push(` const disabledComponent = await mount(`);
|
|
264
|
+
lines.push(` <${name}`);
|
|
265
|
+
lines.push(` ${disabledProp.name}={true}`);
|
|
266
|
+
lines.push(` ${clickHandler.name}={() => { clicked = true; }}`);
|
|
267
|
+
lines.push(...buildRequiredPropsLines(props, hasChildren, name, " "));
|
|
268
|
+
lines.push(` />`);
|
|
269
|
+
lines.push(` );`);
|
|
270
|
+
lines.push(` await disabledComponent.click({ force: true });`);
|
|
271
|
+
lines.push(` expect(clicked).toBe(false);`);
|
|
272
|
+
}
|
|
273
|
+
lines.push(` });`);
|
|
274
|
+
lines.push("");
|
|
275
|
+
}
|
|
276
|
+
const variantProps = props.filter(
|
|
277
|
+
(p) => (p.name === "size" || p.name === "variant" || p.name === "colorPalette") && p.unionValues && p.unionValues.length > 0
|
|
278
|
+
);
|
|
279
|
+
for (const vp of variantProps) {
|
|
280
|
+
lines.push(` test.describe('${vp.name} variants', () => {`);
|
|
281
|
+
for (const val of vp.unionValues) {
|
|
282
|
+
lines.push(` test('${vp.name}="${val}" matches snapshot', async ({ mount }) => {`);
|
|
283
|
+
lines.push(` const component = await mount(`);
|
|
284
|
+
lines.push(` <${name}`);
|
|
285
|
+
lines.push(` ${vp.name}="${val}"`);
|
|
286
|
+
lines.push(...buildRequiredPropsLines(props, hasChildren, name, " "));
|
|
287
|
+
lines.push(` />`);
|
|
288
|
+
lines.push(` );`);
|
|
289
|
+
lines.push(` await expect(component).toHaveScreenshot('${toKebab(name)}-${vp.name}-${val}.png');`);
|
|
290
|
+
lines.push(` });`);
|
|
291
|
+
}
|
|
292
|
+
lines.push(` });`);
|
|
293
|
+
lines.push("");
|
|
294
|
+
}
|
|
295
|
+
lines.push(` test('meets accessibility standards', async ({ mount, page }) => {`);
|
|
296
|
+
lines.push(` await mount(`);
|
|
297
|
+
lines.push(` <${name}${buildMinimalProps(props, hasChildren, name)} />`);
|
|
298
|
+
lines.push(` );`);
|
|
299
|
+
lines.push(` const a11yResults = await new AxeBuilder({ page }).analyze();`);
|
|
300
|
+
lines.push(` expect(a11yResults.violations).toEqual([]);`);
|
|
301
|
+
lines.push(` });`);
|
|
302
|
+
if (hasStories && storyImportPath) {
|
|
303
|
+
lines.push("");
|
|
304
|
+
lines.push(` test.describe('story-driven tests', () => {`);
|
|
305
|
+
lines.push(` test('renders Default story', async ({ mount }) => {`);
|
|
306
|
+
lines.push(` const args = stories.Default?.args ?? {};`);
|
|
307
|
+
lines.push(` const component = await mount(<${name} {...args} />);`);
|
|
308
|
+
lines.push(` await expect(component).toBeVisible();`);
|
|
309
|
+
lines.push(` });`);
|
|
310
|
+
lines.push(` });`);
|
|
311
|
+
}
|
|
312
|
+
lines.push(`});`);
|
|
313
|
+
lines.push("");
|
|
314
|
+
return lines.join("\n");
|
|
315
|
+
}
|
|
316
|
+
function buildMinimalProps(props, hasChildren, componentName) {
|
|
317
|
+
const required = props.filter(
|
|
318
|
+
(p) => p.required && p.name !== "children"
|
|
319
|
+
);
|
|
320
|
+
if (required.length === 0 && !hasChildren) return "";
|
|
321
|
+
const propStrings = [];
|
|
322
|
+
for (const p of required) {
|
|
323
|
+
propStrings.push(`${p.name}={${inferTestValue(p)}}`);
|
|
324
|
+
}
|
|
325
|
+
if (hasChildren) {
|
|
326
|
+
return ` ${propStrings.join(" ")}>${componentName} content</${componentName}`;
|
|
327
|
+
}
|
|
328
|
+
if (propStrings.length === 0) return "";
|
|
329
|
+
return ` ${propStrings.join(" ")}`;
|
|
330
|
+
}
|
|
331
|
+
function buildRequiredPropsLines(props, hasChildren, componentName, indent) {
|
|
332
|
+
const lines = [];
|
|
333
|
+
const required = props.filter(
|
|
334
|
+
(p) => p.required && p.name !== "children" && !p.isCallback
|
|
335
|
+
);
|
|
336
|
+
for (const p of required) {
|
|
337
|
+
lines.push(`${indent}${p.name}={${inferTestValue(p)}}`);
|
|
338
|
+
}
|
|
339
|
+
if (hasChildren) {
|
|
340
|
+
lines.push(`${indent}children="${componentName} content"`);
|
|
341
|
+
}
|
|
342
|
+
return lines;
|
|
343
|
+
}
|
|
344
|
+
function inferTestValue(prop) {
|
|
345
|
+
if (prop.isCallback) return "() => {}";
|
|
346
|
+
if (prop.unionValues && prop.unionValues.length > 0)
|
|
347
|
+
return `"${prop.unionValues[0]}"`;
|
|
348
|
+
const type = prop.type.toLowerCase();
|
|
349
|
+
if (type === "string") return `"Test ${prop.name}"`;
|
|
350
|
+
if (type === "number") return "42";
|
|
351
|
+
if (type === "boolean" || type === "bool") return "true";
|
|
352
|
+
return `undefined as any`;
|
|
353
|
+
}
|
|
354
|
+
function toKebab(str) {
|
|
355
|
+
return str.replace(/([a-z])([A-Z])/g, "$1-$2").replace(/([A-Z]+)([A-Z][a-z])/g, "$1-$2").toLowerCase();
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// src/api/forge-test.ts
|
|
359
|
+
async function forgeTest(options) {
|
|
360
|
+
const { componentPath, overwrite = false, dryRun = false } = options;
|
|
361
|
+
const resolvedPath = resolveComponentPath(componentPath);
|
|
362
|
+
if (!resolvedPath) {
|
|
363
|
+
throw new Error(`Component file not found: ${componentPath}`);
|
|
364
|
+
}
|
|
365
|
+
const testPath = resolvedPath.replace(/\.tsx?$/, CT_FILE_SUFFIX);
|
|
366
|
+
if (fs2.existsSync(testPath) && !overwrite) {
|
|
367
|
+
throw new Error(`Test already exists: ${testPath}. Use --overwrite to replace.`);
|
|
368
|
+
}
|
|
369
|
+
const analysis = analyzeComponent(resolvedPath);
|
|
370
|
+
if (!analysis) {
|
|
371
|
+
throw new Error(`Could not analyze component at ${resolvedPath}. Ensure it exports a React component.`);
|
|
372
|
+
}
|
|
373
|
+
const storyPath = resolvedPath.replace(/\.tsx?$/, STORY_FILE_SUFFIX);
|
|
374
|
+
const hasStories = fs2.existsSync(storyPath);
|
|
375
|
+
const importPath = `./${analysis.fileName}`;
|
|
376
|
+
const storyImportPath = hasStories ? `./${path2.basename(storyPath, ".tsx")}` : void 0;
|
|
377
|
+
const content = generatePlaywrightTest({
|
|
378
|
+
analysis,
|
|
379
|
+
importPath,
|
|
380
|
+
hasStories,
|
|
381
|
+
storyImportPath
|
|
382
|
+
});
|
|
383
|
+
let written = false;
|
|
384
|
+
if (!dryRun) {
|
|
385
|
+
const dir = path2.dirname(testPath);
|
|
386
|
+
fs2.mkdirSync(dir, { recursive: true });
|
|
387
|
+
fs2.writeFileSync(testPath, content, "utf-8");
|
|
388
|
+
written = true;
|
|
389
|
+
}
|
|
390
|
+
return { testPath, content, analysis, written };
|
|
391
|
+
}
|
|
392
|
+
function resolveComponentPath(componentPath) {
|
|
393
|
+
const resolved = path2.resolve(componentPath);
|
|
394
|
+
if (fs2.existsSync(resolved)) return resolved;
|
|
395
|
+
for (const ext of COMPONENT_EXTENSIONS) {
|
|
396
|
+
const withExt = resolved + ext;
|
|
397
|
+
if (fs2.existsSync(withExt)) return withExt;
|
|
398
|
+
}
|
|
399
|
+
return null;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
export {
|
|
403
|
+
STORY_FILE_SUFFIX,
|
|
404
|
+
COMPONENT_EXTENSIONS,
|
|
405
|
+
IGNORED_DIRS,
|
|
406
|
+
DEFAULT_DEBOUNCE_MS,
|
|
407
|
+
STORYBOOK_TEST_IMPORT,
|
|
408
|
+
STORYBOOK_META_IMPORT,
|
|
409
|
+
analyzeComponent,
|
|
410
|
+
generatePlaywrightTest,
|
|
411
|
+
forgeTest
|
|
412
|
+
};
|
|
413
|
+
//# sourceMappingURL=chunk-D2RQPIRR.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/api/forge-test.ts","../src/utils/constants.ts","../src/core/analyze-component.ts","../src/core/generate-playwright-test.ts"],"sourcesContent":["import * as fs from 'node:fs';\nimport * as path from 'node:path';\n\nimport { COMPONENT_EXTENSIONS, CT_FILE_SUFFIX, STORY_FILE_SUFFIX } from '../utils/constants';\nimport type { ForgeTestOptions, ComponentAnalysis } from '../utils/types';\nimport { analyzeComponent } from '../core/analyze-component';\nimport { generatePlaywrightTest } from '../core/generate-playwright-test';\n\nexport interface ForgeTestResult {\n testPath: string;\n content: string;\n analysis: ComponentAnalysis;\n written: boolean;\n}\n\n/**\n * High-level: analyze a component, generate a Playwright component test, and write it.\n */\nexport async function forgeTest(options: ForgeTestOptions): Promise<ForgeTestResult> {\n const { componentPath, overwrite = false, dryRun = false } = options;\n\n const resolvedPath = resolveComponentPath(componentPath);\n if (!resolvedPath) {\n throw new Error(`Component file not found: ${componentPath}`);\n }\n\n const testPath = resolvedPath.replace(/\\.tsx?$/, CT_FILE_SUFFIX);\n if (fs.existsSync(testPath) && !overwrite) {\n throw new Error(`Test already exists: ${testPath}. Use --overwrite to replace.`);\n }\n\n const analysis = analyzeComponent(resolvedPath);\n if (!analysis) {\n throw new Error(`Could not analyze component at ${resolvedPath}. Ensure it exports a React component.`);\n }\n\n const storyPath = resolvedPath.replace(/\\.tsx?$/, STORY_FILE_SUFFIX);\n const hasStories = fs.existsSync(storyPath);\n const importPath = `./${analysis.fileName}`;\n const storyImportPath = hasStories\n ? `./${path.basename(storyPath, '.tsx')}`\n : undefined;\n\n const content = generatePlaywrightTest({\n analysis,\n importPath,\n hasStories,\n storyImportPath,\n });\n\n let written = false;\n if (!dryRun) {\n const dir = path.dirname(testPath);\n fs.mkdirSync(dir, { recursive: true });\n fs.writeFileSync(testPath, content, 'utf-8');\n written = true;\n }\n\n return { testPath, content, analysis, written };\n}\n\nfunction resolveComponentPath(componentPath: string): string | null {\n const resolved = path.resolve(componentPath);\n\n if (fs.existsSync(resolved)) return resolved;\n\n for (const ext of COMPONENT_EXTENSIONS) {\n const withExt = resolved + ext;\n if (fs.existsSync(withExt)) return withExt;\n }\n\n return null;\n}\n","export const STORY_FILE_SUFFIX = '.stories.tsx';\nexport const CT_FILE_SUFFIX = '.ct.tsx';\nexport const COMPONENT_EXTENSIONS = ['.tsx', '.ts'];\n\nexport const IGNORED_FILES = [\n '*.spec.*',\n '*.test.*',\n '*.stories.*',\n '*.styles.*',\n '*.style.*',\n 'index.ts',\n 'index.tsx',\n];\n\nexport const IGNORED_DIRS = [\n 'node_modules',\n 'dist',\n 'build',\n '.storybook',\n '.next',\n '.vite',\n 'coverage',\n '__tests__',\n '__mocks__',\n];\n\nexport const CHAKRA_IMPORTS = ['@chakra-ui/react', '@redesignhealth/ui'];\nexport const ROUTER_IMPORTS = ['react-router-dom', 'react-router'];\nexport const QUERY_IMPORTS = ['@tanstack/react-query', 'react-query'];\nexport const ZUSTAND_IMPORTS = ['zustand'];\nexport const FORMIK_IMPORTS = ['formik'];\nexport const RHF_IMPORTS = ['react-hook-form'];\n\nexport const DEFAULT_DEBOUNCE_MS = 300;\n\nexport const STORYBOOK_TEST_IMPORT = '@storybook/test';\nexport const STORYBOOK_META_IMPORT = '@storybook/react-vite';\n","import * as fs from 'node:fs';\nimport * as path from 'node:path';\n\nimport {\n CHAKRA_IMPORTS,\n QUERY_IMPORTS,\n ROUTER_IMPORTS,\n} from '../utils/constants';\nimport type {\n ComponentAnalysis,\n ImportInfo,\n PropInfo,\n} from '../utils/types';\n\n/**\n * Analyze a React component file to extract metadata for story generation.\n *\n * @param filePath - Absolute or relative path to the component file\n * @param content - Optional file content (reads from disk if omitted)\n */\nexport function analyzeComponent(\n filePath: string,\n content?: string,\n): ComponentAnalysis | null {\n if (!content) {\n try {\n content = fs.readFileSync(filePath, 'utf-8');\n } catch {\n return null;\n }\n }\n\n if (!content.trim()) return null;\n\n const name = extractComponentName(content, filePath);\n if (!name) return null;\n\n const imports = extractImports(content);\n const props = extractProps(content, name);\n\n return {\n name,\n fileName: path.basename(filePath).replace(/\\.tsx?$/, ''),\n filePath,\n props,\n hasChildren: props.some((p) => p.name === 'children'),\n imports,\n usesRouter: imports.some((i) =>\n ROUTER_IMPORTS.some((r) => i.source.includes(r))\n ),\n usesReactQuery: imports.some((i) =>\n QUERY_IMPORTS.some((r) => i.source.includes(r))\n ),\n usesChakra: imports.some((i) =>\n CHAKRA_IMPORTS.some((r) => i.source.includes(r))\n ),\n exportType: detectExportType(content, name),\n };\n}\n\nfunction extractComponentName(\n content: string,\n filePath: string,\n): string | null {\n // Match: export const ComponentName = ... or export function ComponentName(\n const namedExport = content.match(\n /export\\s+(?:const|function)\\s+([A-Z][A-Za-z0-9]*)/\n );\n if (namedExport) return namedExport[1];\n\n // Match: export default function ComponentName\n const defaultFn = content.match(\n /export\\s+default\\s+function\\s+([A-Z][A-Za-z0-9]*)/\n );\n if (defaultFn) return defaultFn[1];\n\n // Match: const ComponentName = ... followed by export default ComponentName\n const constThenDefault = content.match(\n /(?:const|function)\\s+([A-Z][A-Za-z0-9]*)\\s*=[\\s\\S]*?export\\s+default\\s+\\1/\n );\n if (constThenDefault) return constThenDefault[1];\n\n // Match: React.forwardRef / React.memo wrapping\n const forwardRef = content.match(\n /export\\s+(?:const|default)\\s+(?:const\\s+)?([A-Z][A-Za-z0-9]*)\\s*=\\s*(?:React\\.)?(?:forwardRef|memo)\\s*[<(]/\n );\n if (forwardRef) return forwardRef[1];\n\n // Fallback: derive from file name\n const fileName = filePath.split('/').pop()?.replace(/\\.tsx?$/, '');\n if (fileName && /[a-zA-Z]/.test(fileName)) {\n return fileName\n .split('-')\n .map((part) => part.charAt(0).toUpperCase() + part.slice(1))\n .join('');\n }\n\n return null;\n}\n\nfunction extractImports(content: string): ImportInfo[] {\n const imports: ImportInfo[] = [];\n const importRegex =\n /import\\s+(?:(?:(\\{[^}]+\\})|(\\w+))\\s+from\\s+)?['\"]([^'\"]+)['\"]/g;\n let match;\n\n while ((match = importRegex.exec(content)) !== null) {\n const namedImports = match[1];\n const defaultImport = match[2];\n const source = match[3];\n const specifiers: string[] = [];\n\n if (namedImports) {\n const names = namedImports\n .replace(/[{}]/g, '')\n .split(',')\n .map((s) => s.trim().split(/\\s+as\\s+/)[0].trim())\n .filter(Boolean);\n specifiers.push(...names);\n }\n if (defaultImport) {\n specifiers.push(defaultImport);\n }\n\n imports.push({ source, specifiers });\n }\n\n return imports;\n}\n\nfunction extractProps(content: string, componentName: string): PropInfo[] {\n const props: PropInfo[] = [];\n\n const propsTypeName = findPropsTypeName(content, componentName);\n if (!propsTypeName) return props;\n\n // Match interface or type body — handle intersection types with &\n const interfaceMatch = content.match(\n new RegExp(\n `(?:interface|type)\\\\s+${propsTypeName}\\\\s*(?:extends\\\\s+[^{]+)?\\\\s*(?:=\\\\s*)?(?:[^{]*&\\\\s*)?\\\\{([^}]*)\\\\}`,\n 's'\n )\n );\n if (!interfaceMatch) return props;\n\n const body = interfaceMatch[1];\n const propLines = body.split('\\n').filter((line) => {\n const trimmed = line.trim();\n return trimmed && !trimmed.startsWith('//') && !trimmed.startsWith('*');\n });\n\n for (const line of propLines) {\n const propMatch = line.match(\n /^\\s*(\\w+)(\\??):\\s*(.+?)(?:\\s*\\/\\/.*)?;?\\s*$/\n );\n if (!propMatch) continue;\n\n const [, name, optional, rawType] = propMatch;\n const type = rawType.trim().replace(/;$/, '');\n const isCallback =\n type.includes('=>') ||\n type.startsWith('(') ||\n /^on[A-Z]/.test(name);\n\n const unionValues = extractUnionValues(type);\n\n props.push({\n name,\n type,\n required: optional !== '?',\n isCallback,\n unionValues: unionValues.length > 0 ? unionValues : undefined,\n });\n }\n\n return props;\n}\n\nfunction findPropsTypeName(\n content: string,\n componentName: string,\n): string | null {\n // Pattern: ComponentName = ({ ... }: PropsType)\n const destructuredProps = content.match(\n new RegExp(\n `(?:const|function)\\\\s+${componentName}\\\\s*(?:=\\\\s*)?\\\\([^)]*:\\\\s*([A-Z]\\\\w*)`\n )\n );\n if (destructuredProps) return destructuredProps[1];\n\n // Pattern: ComponentName(props: PropsType)\n const regularProps = content.match(\n new RegExp(\n `(?:const|function)\\\\s+${componentName}\\\\s*(?:=\\\\s*)?\\\\(\\\\s*(?:props|\\\\w+)\\\\s*:\\\\s*([A-Z]\\\\w*)`\n )\n );\n if (regularProps) return regularProps[1];\n\n // Pattern: React.FC<PropsType>\n const fcProps = content.match(\n new RegExp(`${componentName}[^=]*=\\\\s*(?:React\\\\.)?FC<([A-Z]\\\\w*)>`)\n );\n if (fcProps) return fcProps[1];\n\n // Fallback: look for interface named ComponentNameProps\n const defaultPropsName = `${componentName}Props`;\n if (content.includes(`interface ${defaultPropsName}`) || content.includes(`type ${defaultPropsName}`)) {\n return defaultPropsName;\n }\n\n return null;\n}\n\nfunction extractUnionValues(type: string): string[] {\n // Match single or double-quoted string literal unions: 'a' | 'b' or \"a\" | \"b\"\n const singleQuote = type.match(/^'[^']*'(?:\\s*\\|\\s*'[^']*')+$/);\n if (singleQuote) {\n return type\n .split('|')\n .map((v) => v.trim().replace(/'/g, ''))\n .filter(Boolean);\n }\n const doubleQuote = type.match(/^\"[^\"]*\"(?:\\s*\\|\\s*\"[^\"]*\")+$/);\n if (doubleQuote) {\n return type\n .split('|')\n .map((v) => v.trim().replace(/\"/g, ''))\n .filter(Boolean);\n }\n return [];\n}\n\nfunction detectExportType(\n content: string,\n componentName: string,\n): 'default' | 'named' | 'both' {\n const hasDefault =\n content.includes(`export default ${componentName}`) ||\n content.includes(`export default function ${componentName}`);\n const hasNamed =\n content.includes(`export const ${componentName}`) ||\n content.includes(`export function ${componentName}`);\n\n if (hasDefault && hasNamed) return 'both';\n if (hasDefault) return 'default';\n return 'named';\n}\n","import type { ComponentAnalysis, PropInfo, PlaywrightTestOptions } from '../utils/types';\n\n/**\n * Generate a co-located Playwright component test (.ct.tsx) that:\n * - Mounts the component using @playwright/experimental-ct-react\n * - Tests rendering, visual regression (screenshot), interactions, and a11y\n * - Optionally imports stories for story-driven testing\n */\nexport function generatePlaywrightTest(options: PlaywrightTestOptions): string {\n const { analysis, importPath, hasStories, storyImportPath } = options;\n const { name, props, exportType, hasChildren } = analysis;\n\n const lines: string[] = [];\n\n // Imports\n lines.push(`import { test, expect } from '@playwright/experimental-ct-react';`);\n lines.push(`import AxeBuilder from '@axe-core/playwright';`);\n\n if (exportType === 'default') {\n lines.push(`import ${name} from '${importPath}';`);\n } else {\n lines.push(`import { ${name} } from '${importPath}';`);\n }\n\n if (hasStories && storyImportPath) {\n lines.push(`import * as stories from '${storyImportPath}';`);\n }\n\n lines.push('');\n\n // Test suite\n lines.push(`test.describe('${name}', () => {`);\n\n // Mount & render test\n lines.push(` test('mounts and renders without crashing', async ({ mount }) => {`);\n lines.push(` const component = await mount(`);\n lines.push(` <${name}${buildMinimalProps(props, hasChildren, name)} />`);\n lines.push(` );`);\n lines.push(` await expect(component).toBeVisible();`);\n lines.push(` });`);\n lines.push('');\n\n // Screenshot / visual regression test\n lines.push(` test('matches visual snapshot', async ({ mount }) => {`);\n lines.push(` const component = await mount(`);\n lines.push(` <${name}${buildMinimalProps(props, hasChildren, name)} />`);\n lines.push(` );`);\n lines.push(` await expect(component).toHaveScreenshot('${toKebab(name)}-default.png');`);\n lines.push(` });`);\n lines.push('');\n\n // Interaction tests\n const clickHandler = props.find(\n (p) => p.isCallback && (p.name === 'onClick' || p.name === 'onPress')\n );\n if (clickHandler) {\n lines.push(` test('handles click interaction', async ({ mount }) => {`);\n lines.push(` let clicked = false;`);\n lines.push(` const component = await mount(`);\n lines.push(` <${name}`);\n lines.push(` ${clickHandler.name}={() => { clicked = true; }}`);\n lines.push(...buildRequiredPropsLines(props, hasChildren, name, ' '));\n lines.push(` />`);\n lines.push(` );`);\n lines.push(` await component.click();`);\n lines.push(` expect(clicked).toBe(true);`);\n lines.push(` });`);\n lines.push('');\n }\n\n const onChangeHandler = props.find(\n (p) => p.isCallback && (p.name === 'onChange' || p.name === 'onValueChange')\n );\n if (onChangeHandler) {\n lines.push(` test('handles value change', async ({ mount }) => {`);\n lines.push(` let changed = false;`);\n lines.push(` const component = await mount(`);\n lines.push(` <${name}`);\n lines.push(` ${onChangeHandler.name}={() => { changed = true; }}`);\n lines.push(...buildRequiredPropsLines(props, hasChildren, name, ' '));\n lines.push(` />`);\n lines.push(` );`);\n lines.push(` // Trigger a change event appropriate to the component type`);\n lines.push(` await component.locator('input, select, textarea').first().fill('test');`);\n lines.push(` expect(changed).toBe(true);`);\n lines.push(` });`);\n lines.push('');\n }\n\n // Disabled state\n const disabledProp = props.find(\n (p) => p.name === 'disabled' || p.name === 'isDisabled'\n );\n if (disabledProp) {\n lines.push(` test('renders disabled state correctly', async ({ mount }) => {`);\n lines.push(` const component = await mount(`);\n lines.push(` <${name}`);\n lines.push(` ${disabledProp.name}={true}`);\n lines.push(...buildRequiredPropsLines(props, hasChildren, name, ' '));\n lines.push(` />`);\n lines.push(` );`);\n lines.push(` await expect(component).toHaveScreenshot('${toKebab(name)}-disabled.png');`);\n if (clickHandler) {\n lines.push(` // Verify click handler is not triggered when disabled`);\n lines.push(` let clicked = false;`);\n lines.push(` const disabledComponent = await mount(`);\n lines.push(` <${name}`);\n lines.push(` ${disabledProp.name}={true}`);\n lines.push(` ${clickHandler.name}={() => { clicked = true; }}`);\n lines.push(...buildRequiredPropsLines(props, hasChildren, name, ' '));\n lines.push(` />`);\n lines.push(` );`);\n lines.push(` await disabledComponent.click({ force: true });`);\n lines.push(` expect(clicked).toBe(false);`);\n }\n lines.push(` });`);\n lines.push('');\n }\n\n // Variant visual regression\n const variantProps = props.filter(\n (p) =>\n (p.name === 'size' || p.name === 'variant' || p.name === 'colorPalette') &&\n p.unionValues &&\n p.unionValues.length > 0\n );\n for (const vp of variantProps) {\n lines.push(` test.describe('${vp.name} variants', () => {`);\n for (const val of vp.unionValues!) {\n lines.push(` test('${vp.name}=\"${val}\" matches snapshot', async ({ mount }) => {`);\n lines.push(` const component = await mount(`);\n lines.push(` <${name}`);\n lines.push(` ${vp.name}=\"${val}\"`);\n lines.push(...buildRequiredPropsLines(props, hasChildren, name, ' '));\n lines.push(` />`);\n lines.push(` );`);\n lines.push(` await expect(component).toHaveScreenshot('${toKebab(name)}-${vp.name}-${val}.png');`);\n lines.push(` });`);\n }\n lines.push(` });`);\n lines.push('');\n }\n\n // Accessibility test\n lines.push(` test('meets accessibility standards', async ({ mount, page }) => {`);\n lines.push(` await mount(`);\n lines.push(` <${name}${buildMinimalProps(props, hasChildren, name)} />`);\n lines.push(` );`);\n lines.push(` const a11yResults = await new AxeBuilder({ page }).analyze();`);\n lines.push(` expect(a11yResults.violations).toEqual([]);`);\n lines.push(` });`);\n\n // Story-driven tests\n if (hasStories && storyImportPath) {\n lines.push('');\n lines.push(` test.describe('story-driven tests', () => {`);\n lines.push(` test('renders Default story', async ({ mount }) => {`);\n lines.push(` const args = stories.Default?.args ?? {};`);\n lines.push(` const component = await mount(<${name} {...args} />);`);\n lines.push(` await expect(component).toBeVisible();`);\n lines.push(` });`);\n lines.push(` });`);\n }\n\n lines.push(`});`);\n lines.push('');\n\n return lines.join('\\n');\n}\n\nfunction buildMinimalProps(\n props: PropInfo[],\n hasChildren: boolean,\n componentName: string,\n): string {\n const required = props.filter(\n (p) => p.required && p.name !== 'children'\n );\n\n if (required.length === 0 && !hasChildren) return '';\n\n const propStrings: string[] = [];\n for (const p of required) {\n propStrings.push(`${p.name}={${inferTestValue(p)}}`);\n }\n\n if (hasChildren) {\n return ` ${propStrings.join(' ')}>${componentName} content</${componentName}`;\n }\n\n if (propStrings.length === 0) return '';\n return ` ${propStrings.join(' ')}`;\n}\n\nfunction buildRequiredPropsLines(\n props: PropInfo[],\n hasChildren: boolean,\n componentName: string,\n indent: string,\n): string[] {\n const lines: string[] = [];\n const required = props.filter(\n (p) => p.required && p.name !== 'children' && !p.isCallback\n );\n\n for (const p of required) {\n lines.push(`${indent}${p.name}={${inferTestValue(p)}}`);\n }\n\n if (hasChildren) {\n lines.push(`${indent}children=\"${componentName} content\"`);\n }\n\n return lines;\n}\n\nfunction inferTestValue(prop: PropInfo): string {\n if (prop.isCallback) return '() => {}';\n if (prop.unionValues && prop.unionValues.length > 0)\n return `\"${prop.unionValues[0]}\"`;\n\n const type = prop.type.toLowerCase();\n if (type === 'string') return `\"Test ${prop.name}\"`;\n if (type === 'number') return '42';\n if (type === 'boolean' || type === 'bool') return 'true';\n\n return `undefined as any`;\n}\n\nfunction toKebab(str: string): string {\n return str\n .replace(/([a-z])([A-Z])/g, '$1-$2')\n .replace(/([A-Z]+)([A-Z][a-z])/g, '$1-$2')\n .toLowerCase();\n}\n"],"mappings":"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","names":["fs","path"]}
|