import { generatePRD, extractPRDSections } from '@/templates/prd';
import { mockConversationState, mockPRDContent } from '@/__tests__/fixtures';
// Mock dependencies
jest.mock('@/claude/client');
jest.mock('@/utils/logger');
const mockClaudeClient = {
generateStructuredDocument: jest.fn().mockResolvedValue({
document: mockPRDContent,
sections: {
executive_summary: 'FoodieDelivery is a comprehensive food delivery platform...',
product_vision: 'To become the leading food delivery service...',
user_personas: 'Urban professionals aged 25-40...',
business_requirements: 'Core functionality includes restaurant browsing...',
feature_map: 'Key features with priorities and dependencies...',
success_criteria: 'Metrics, KPIs, and validation methods...',
implementation_notes: 'Technical requirements and constraints...',
},
}),
};
describe('PRD Template Generator', () => {
beforeEach(() => {
jest.clearAllMocks();
(require('@/claude/client') as any).claudeClient = mockClaudeClient;
});
describe('generatePRD', () => {
it('should generate a PRD from conversation data', async () => {
const projectName = 'FoodieDelivery';
const result = await generatePRD(mockConversationState, projectName);
expect(result).toBeTruthy();
expect(result).toContain('# Product Requirements Document: FoodieDelivery');
expect(result).toContain('Executive Summary');
expect(result).toContain('Product Vision');
expect(result).toContain('User Personas');
expect(result).toContain('Business Requirements');
expect(result).toContain('Feature Map');
expect(result).toContain('Success Criteria');
expect(mockClaudeClient.generateStructuredDocument).toHaveBeenCalledWith(
mockConversationState,
'prd',
projectName
);
});
it('should handle default project name when not provided', async () => {
await generatePRD(mockConversationState);
expect(mockClaudeClient.generateStructuredDocument).toHaveBeenCalledWith(
mockConversationState,
'prd',
'Untitled Project'
);
});
it('should include generation timestamp', async () => {
const result = await generatePRD(mockConversationState, 'TestProject');
expect(result).toMatch(/Document generated from conversation on/);
expect(result).toMatch(/\\d{4}-\\d{2}-\\d{2}/); // Date format
});
it('should handle empty conversation gracefully', async () => {
const emptyConversation = {
...mockConversationState,
messages: [],
};
const result = await generatePRD(emptyConversation, 'EmptyProject');
expect(result).toBeTruthy();
expect(result).toContain('# Product Requirements Document: EmptyProject');
expect(mockClaudeClient.generateStructuredDocument).toHaveBeenCalledWith(
emptyConversation,
'prd',
'EmptyProject'
);
});
it('should preserve conversation context in generation', async () => {
const conversationWithMultipleMessages = {
...mockConversationState,
messages: [
{
role: 'user' as const,
content: 'I want to build a food delivery app',
timestamp: new Date(),
},
{
role: 'assistant' as const,
content: 'Great! Let me help you define the product requirements...',
timestamp: new Date(),
},
{
role: 'user' as const,
content: 'The target users are busy professionals',
timestamp: new Date(),
},
],
};
await generatePRD(conversationWithMultipleMessages, 'FoodieApp');
expect(mockClaudeClient.generateStructuredDocument).toHaveBeenCalledWith(
conversationWithMultipleMessages,
'prd',
'FoodieApp'
);
});
it('should handle Claude client errors', async () => {
mockClaudeClient.generateStructuredDocument.mockRejectedValue(
new Error('Claude API error')
);
await expect(generatePRD(mockConversationState, 'FailProject'))
.rejects.toThrow('Claude API error');
});
it('should validate required PRD sections are present', async () => {
const result = await generatePRD(mockConversationState, 'ValidatedProject');
// Check all required sections are included
const requiredSections = [
'Executive Summary',
'Product Vision',
'User Personas',
'Business Requirements',
'Feature Map',
'Success Criteria',
];
requiredSections.forEach(section => {
expect(result).toContain(section);
});
});
});
describe('extractPRDSections', () => {
it('should extract structured sections from PRD content', () => {
const sections = extractPRDSections(mockPRDContent);
expect(sections).toHaveProperty('executive_summary');
expect(sections).toHaveProperty('product_vision');
expect(sections).toHaveProperty('user_personas');
expect(sections).toHaveProperty('business_requirements');
expect(sections).toHaveProperty('feature_map');
expect(sections).toHaveProperty('success_criteria');
expect(sections.executive_summary).toContain('comprehensive food delivery platform');
expect(sections.product_vision).toBeTruthy();
expect(sections.user_personas).toBeTruthy();
});
it('should handle PRD content without clear section breaks', () => {
const unstructuredPRD = `
This is a simple PRD without clear sections.
It mentions user requirements and business goals.
There are no clear markdown headers.
`;
const sections = extractPRDSections(unstructuredPRD);
// Should still return an object, even if sections are empty
expect(sections).toBeInstanceOf(Object);
expect(Object.keys(sections)).toEqual(expect.arrayContaining([
'executive_summary',
'product_vision',
'user_personas',
'business_requirements',
'feature_map',
'success_criteria',
]));
});
it('should handle empty or invalid content', () => {
expect(() => extractPRDSections('')).not.toThrow();
expect(() => extractPRDSections(' ')).not.toThrow();
expect(() => extractPRDSections(null as any)).not.toThrow();
expect(() => extractPRDSections(undefined as any)).not.toThrow();
});
});
describe('PRD template structure', () => {
it('should follow standard PRD format', async () => {
const result = await generatePRD(mockConversationState, 'StandardProject');
// Check document structure
expect(result).toMatch(/^# Product Requirements Document:/);
expect(result).toMatch(/## Executive Summary/);
expect(result).toMatch(/## \\d+\\. Product Vision/);
expect(result).toMatch(/## \\d+\\. User Personas/);
expect(result).toMatch(/## \\d+\\. Business Requirements/);
expect(result).toMatch(/## \\d+\\. Feature Map/);
expect(result).toMatch(/## \\d+\\. Success Criteria/);
});
it('should include proper markdown formatting', async () => {
const result = await generatePRD(mockConversationState, 'MarkdownProject');
// Check for proper markdown elements
expect(result).toMatch(/^#/m); // Headers
expect(result).toMatch(/^##/m); // Subheaders
expect(result).toMatch(/\\*.*\\*/); // Italics
expect(result).toMatch(/---/); // Horizontal rule
});
it('should be compatible with markdown parsers', async () => {
const result = await generatePRD(mockConversationState, 'CompatibleProject');
// Basic markdown validation
const headerCount = (result.match(/^#+/gm) || []).length;
expect(headerCount).toBeGreaterThan(0);
const listItems = (result.match(/^[-*+]/gm) || []).length;
// Should have some list items for features, requirements, etc.
expect(listItems).toBeGreaterThanOrEqual(0);
});
});
describe('data validation', () => {
it('should validate conversation state structure', async () => {
const invalidConversation = {
id: 'conv_invalid',
// Missing required fields
};
// Should handle gracefully or throw appropriate error
await expect(generatePRD(invalidConversation as any, 'InvalidProject'))
.resolves.toBeTruthy(); // Assuming graceful handling
});
it('should sanitize project name for document title', async () => {
const unsafeProjectName = 'Project';
const result = await generatePRD(mockConversationState, unsafeProjectName);
// Should not contain script tags
expect(result).not.toMatch(/