popeye-cli 1.5.0 → 1.6.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/CHANGELOG.md +54 -0
- package/README.md +50 -8
- package/dist/cli/commands/create.d.ts.map +1 -1
- package/dist/cli/commands/create.js +54 -4
- package/dist/cli/commands/create.js.map +1 -1
- package/dist/cli/interactive.d.ts +29 -0
- package/dist/cli/interactive.d.ts.map +1 -1
- package/dist/cli/interactive.js +90 -7
- package/dist/cli/interactive.js.map +1 -1
- package/dist/generators/all.d.ts +4 -1
- package/dist/generators/all.d.ts.map +1 -1
- package/dist/generators/all.js +36 -316
- package/dist/generators/all.js.map +1 -1
- package/dist/generators/doc-parser.d.ts +18 -3
- package/dist/generators/doc-parser.d.ts.map +1 -1
- package/dist/generators/doc-parser.js +81 -10
- package/dist/generators/doc-parser.js.map +1 -1
- package/dist/generators/frontend-design-analyzer.d.ts +30 -0
- package/dist/generators/frontend-design-analyzer.d.ts.map +1 -0
- package/dist/generators/frontend-design-analyzer.js +208 -0
- package/dist/generators/frontend-design-analyzer.js.map +1 -0
- package/dist/generators/shared-packages.d.ts +45 -0
- package/dist/generators/shared-packages.d.ts.map +1 -0
- package/dist/generators/shared-packages.js +456 -0
- package/dist/generators/shared-packages.js.map +1 -0
- package/dist/generators/templates/index.d.ts +4 -0
- package/dist/generators/templates/index.d.ts.map +1 -1
- package/dist/generators/templates/index.js +4 -0
- package/dist/generators/templates/index.js.map +1 -1
- package/dist/generators/templates/website-components.d.ts.map +1 -1
- package/dist/generators/templates/website-components.js +36 -11
- package/dist/generators/templates/website-components.js.map +1 -1
- package/dist/generators/templates/website-config.d.ts +15 -1
- package/dist/generators/templates/website-config.d.ts.map +1 -1
- package/dist/generators/templates/website-config.js +155 -13
- package/dist/generators/templates/website-config.js.map +1 -1
- package/dist/generators/templates/website-landing.d.ts +24 -0
- package/dist/generators/templates/website-landing.d.ts.map +1 -0
- package/dist/generators/templates/website-landing.js +276 -0
- package/dist/generators/templates/website-landing.js.map +1 -0
- package/dist/generators/templates/website-layout.d.ts +42 -0
- package/dist/generators/templates/website-layout.d.ts.map +1 -0
- package/dist/generators/templates/website-layout.js +408 -0
- package/dist/generators/templates/website-layout.js.map +1 -0
- package/dist/generators/templates/website-pricing.d.ts +11 -0
- package/dist/generators/templates/website-pricing.d.ts.map +1 -0
- package/dist/generators/templates/website-pricing.js +313 -0
- package/dist/generators/templates/website-pricing.js.map +1 -0
- package/dist/generators/templates/website-sections.d.ts +102 -0
- package/dist/generators/templates/website-sections.d.ts.map +1 -0
- package/dist/generators/templates/website-sections.js +444 -0
- package/dist/generators/templates/website-sections.js.map +1 -0
- package/dist/generators/templates/website.d.ts +10 -50
- package/dist/generators/templates/website.d.ts.map +1 -1
- package/dist/generators/templates/website.js +12 -788
- package/dist/generators/templates/website.js.map +1 -1
- package/dist/generators/website-content-scanner.d.ts +37 -0
- package/dist/generators/website-content-scanner.d.ts.map +1 -0
- package/dist/generators/website-content-scanner.js +165 -0
- package/dist/generators/website-content-scanner.js.map +1 -0
- package/dist/generators/website-context.d.ts +38 -2
- package/dist/generators/website-context.d.ts.map +1 -1
- package/dist/generators/website-context.js +179 -19
- package/dist/generators/website-context.js.map +1 -1
- package/dist/generators/website-debug.d.ts +68 -0
- package/dist/generators/website-debug.d.ts.map +1 -0
- package/dist/generators/website-debug.js +93 -0
- package/dist/generators/website-debug.js.map +1 -0
- package/dist/generators/website.d.ts +2 -0
- package/dist/generators/website.d.ts.map +1 -1
- package/dist/generators/website.js +66 -4
- package/dist/generators/website.js.map +1 -1
- package/dist/generators/workspace-root.d.ts +27 -0
- package/dist/generators/workspace-root.d.ts.map +1 -0
- package/dist/generators/workspace-root.js +100 -0
- package/dist/generators/workspace-root.js.map +1 -0
- package/dist/state/index.d.ts +8 -0
- package/dist/state/index.d.ts.map +1 -1
- package/dist/state/index.js +10 -0
- package/dist/state/index.js.map +1 -1
- package/dist/types/workflow.d.ts +6 -0
- package/dist/types/workflow.d.ts.map +1 -1
- package/dist/types/workflow.js +2 -0
- package/dist/types/workflow.js.map +1 -1
- package/dist/upgrade/handlers.d.ts +15 -0
- package/dist/upgrade/handlers.d.ts.map +1 -1
- package/dist/upgrade/handlers.js +52 -0
- package/dist/upgrade/handlers.js.map +1 -1
- package/dist/workflow/auto-fix-bundler.d.ts +37 -0
- package/dist/workflow/auto-fix-bundler.d.ts.map +1 -0
- package/dist/workflow/auto-fix-bundler.js +320 -0
- package/dist/workflow/auto-fix-bundler.js.map +1 -0
- package/dist/workflow/auto-fix.d.ts.map +1 -1
- package/dist/workflow/auto-fix.js +10 -3
- package/dist/workflow/auto-fix.js.map +1 -1
- package/dist/workflow/index.d.ts +1 -0
- package/dist/workflow/index.d.ts.map +1 -1
- package/dist/workflow/index.js +12 -0
- package/dist/workflow/index.js.map +1 -1
- package/dist/workflow/overview.d.ts.map +1 -1
- package/dist/workflow/overview.js +4 -0
- package/dist/workflow/overview.js.map +1 -1
- package/dist/workflow/plan-mode.d.ts +4 -3
- package/dist/workflow/plan-mode.d.ts.map +1 -1
- package/dist/workflow/plan-mode.js +69 -5
- package/dist/workflow/plan-mode.js.map +1 -1
- package/dist/workflow/website-strategy.d.ts +9 -0
- package/dist/workflow/website-strategy.d.ts.map +1 -1
- package/dist/workflow/website-strategy.js +73 -1
- package/dist/workflow/website-strategy.js.map +1 -1
- package/dist/workflow/website-updater.d.ts.map +1 -1
- package/dist/workflow/website-updater.js +15 -4
- package/dist/workflow/website-updater.js.map +1 -1
- package/package.json +1 -1
- package/src/cli/commands/create.ts +58 -4
- package/src/cli/interactive.ts +96 -7
- package/src/generators/all.ts +44 -332
- package/src/generators/doc-parser.ts +87 -10
- package/src/generators/frontend-design-analyzer.ts +261 -0
- package/src/generators/shared-packages.ts +500 -0
- package/src/generators/templates/index.ts +4 -0
- package/src/generators/templates/website-components.ts +36 -11
- package/src/generators/templates/website-config.ts +166 -13
- package/src/generators/templates/website-landing.ts +331 -0
- package/src/generators/templates/website-layout.ts +443 -0
- package/src/generators/templates/website-pricing.ts +330 -0
- package/src/generators/templates/website-sections.ts +541 -0
- package/src/generators/templates/website.ts +38 -851
- package/src/generators/website-content-scanner.ts +208 -0
- package/src/generators/website-context.ts +248 -20
- package/src/generators/website-debug.ts +130 -0
- package/src/generators/website.ts +71 -3
- package/src/generators/workspace-root.ts +113 -0
- package/src/state/index.ts +14 -0
- package/src/types/workflow.ts +6 -0
- package/src/upgrade/handlers.ts +65 -0
- package/src/workflow/auto-fix-bundler.ts +392 -0
- package/src/workflow/auto-fix.ts +11 -3
- package/src/workflow/index.ts +12 -0
- package/src/workflow/overview.ts +6 -0
- package/src/workflow/plan-mode.ts +81 -7
- package/src/workflow/website-strategy.ts +75 -1
- package/src/workflow/website-updater.ts +17 -6
- package/tests/cli/project-naming.test.ts +136 -0
- package/tests/generators/doc-parser.test.ts +121 -0
- package/tests/generators/frontend-design-analyzer.test.ts +90 -0
- package/tests/generators/quality-gate.test.ts +183 -0
- package/tests/generators/shared-packages.test.ts +83 -0
- package/tests/generators/website-components.test.ts +1 -1
- package/tests/generators/website-config.test.ts +84 -0
- package/tests/generators/website-content-scanner.test.ts +181 -0
- package/tests/generators/website-context.test.ts +109 -0
- package/tests/generators/website-debug.test.ts +77 -0
- package/tests/generators/website-landing.test.ts +188 -0
- package/tests/generators/website-pricing.test.ts +98 -0
- package/tests/generators/website-sections.test.ts +245 -0
- package/tests/generators/workspace-root.test.ts +105 -0
- package/tests/upgrade/handlers.test.ts +162 -0
- package/tests/workflow/auto-fix-bundler.test.ts +242 -0
- package/tests/workflow/plan-mode.test.ts +111 -1
- package/tests/workflow/website-strategy.test.ts +55 -0
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for website-landing module
|
|
3
|
+
* Verifies 10-section landing page generation with strategy data flow
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { describe, it, expect } from 'vitest';
|
|
7
|
+
import { generateWebsiteLandingPageWithInfo } from '../../src/generators/templates/website-landing.js';
|
|
8
|
+
import type { WebsiteContentContext } from '../../src/generators/website-context.js';
|
|
9
|
+
import type { WebsiteStrategyDocument } from '../../src/types/website-strategy.js';
|
|
10
|
+
|
|
11
|
+
function makeStrategy(overrides?: Partial<WebsiteStrategyDocument>): WebsiteStrategyDocument {
|
|
12
|
+
return {
|
|
13
|
+
icp: {
|
|
14
|
+
primaryPersona: 'Engineering teams',
|
|
15
|
+
painPoints: ['Data leaks', 'No access control', 'Compliance gaps'],
|
|
16
|
+
goals: ['Secure AI access'],
|
|
17
|
+
objections: ['Is it secure?', 'Does it scale?'],
|
|
18
|
+
},
|
|
19
|
+
positioning: {
|
|
20
|
+
category: 'AI Security',
|
|
21
|
+
differentiators: ['Zero-trust by default', 'Vector DB agnostic'],
|
|
22
|
+
valueProposition: 'Secure AI data access without complexity',
|
|
23
|
+
proofPoints: ['SOC2 compliant', '10K+ queries/sec'],
|
|
24
|
+
},
|
|
25
|
+
messaging: {
|
|
26
|
+
headline: 'Secure AI Retrieval',
|
|
27
|
+
subheadline: 'Permission-aware access for AI agents',
|
|
28
|
+
elevatorPitch: 'Stop worrying about data leaks',
|
|
29
|
+
longDescription: 'A permission-aware retrieval layer for AI systems.',
|
|
30
|
+
},
|
|
31
|
+
seoStrategy: {
|
|
32
|
+
primaryKeywords: ['AI security'],
|
|
33
|
+
secondaryKeywords: [],
|
|
34
|
+
longTailKeywords: [],
|
|
35
|
+
titleTemplates: { home: 'Gateco - AI Security' },
|
|
36
|
+
metaDescriptions: { home: 'Secure AI retrieval' },
|
|
37
|
+
},
|
|
38
|
+
siteArchitecture: {
|
|
39
|
+
pages: [],
|
|
40
|
+
navigation: [{ label: 'Features', href: '/#features' }],
|
|
41
|
+
footerSections: [],
|
|
42
|
+
},
|
|
43
|
+
conversionStrategy: {
|
|
44
|
+
primaryCta: { text: 'Try Free', href: '/signup' },
|
|
45
|
+
secondaryCta: { text: 'View Docs', href: '/docs' },
|
|
46
|
+
trustSignals: ['Enterprise ready', 'SOC2 compliant'],
|
|
47
|
+
socialProof: ['Great product!'],
|
|
48
|
+
leadCapture: 'none',
|
|
49
|
+
},
|
|
50
|
+
competitiveContext: { category: 'Security', competitors: [], differentiators: [] },
|
|
51
|
+
...overrides,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function makeContext(overrides?: Partial<WebsiteContentContext>): WebsiteContentContext {
|
|
56
|
+
return {
|
|
57
|
+
productName: 'Gateco',
|
|
58
|
+
features: [
|
|
59
|
+
{ title: 'Access Control', description: 'Fine-grained permissions' },
|
|
60
|
+
{ title: 'Vector Search', description: 'Fast semantic search' },
|
|
61
|
+
{ title: 'Audit Trail', description: 'Full audit logging' },
|
|
62
|
+
],
|
|
63
|
+
rawDocs: 'docs content here',
|
|
64
|
+
description: 'A permission-aware retrieval layer for AI systems.',
|
|
65
|
+
pricing: [
|
|
66
|
+
{ name: 'Free', price: 'Free', description: 'Dev & POC', features: ['Basic'], cta: 'Start', featured: false },
|
|
67
|
+
{ name: 'Pro', price: '$99', period: '/month', description: 'Production', features: ['All'], cta: 'Subscribe', featured: true },
|
|
68
|
+
],
|
|
69
|
+
strategy: makeStrategy(),
|
|
70
|
+
...overrides,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
describe('generateWebsiteLandingPageWithInfo', () => {
|
|
75
|
+
it('generates page with all 10 sections when full context provided', () => {
|
|
76
|
+
const result = generateWebsiteLandingPageWithInfo('gateco', makeContext());
|
|
77
|
+
|
|
78
|
+
// Hero
|
|
79
|
+
expect(result.code).toContain('Secure AI Retrieval');
|
|
80
|
+
expect(result.code).toContain('AI Security'); // eyebrow category
|
|
81
|
+
|
|
82
|
+
// Sections rendered info should have all sections
|
|
83
|
+
const sectionNames = result.sections.map(s => s.name);
|
|
84
|
+
expect(sectionNames).toContain('Hero');
|
|
85
|
+
expect(sectionNames).toContain('PainPoints');
|
|
86
|
+
expect(sectionNames).toContain('Differentiators');
|
|
87
|
+
expect(sectionNames).toContain('Features');
|
|
88
|
+
expect(sectionNames).toContain('HowItWorks');
|
|
89
|
+
expect(sectionNames).toContain('Stats');
|
|
90
|
+
expect(sectionNames).toContain('SocialProof');
|
|
91
|
+
expect(sectionNames).toContain('PricingTeaser');
|
|
92
|
+
expect(sectionNames).toContain('FAQ');
|
|
93
|
+
expect(sectionNames).toContain('FinalCTA');
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('includes strategy headline in hero', () => {
|
|
97
|
+
const result = generateWebsiteLandingPageWithInfo('gateco', makeContext());
|
|
98
|
+
expect(result.code).toContain('Secure AI Retrieval');
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('includes pain points from strategy', () => {
|
|
102
|
+
const result = generateWebsiteLandingPageWithInfo('gateco', makeContext());
|
|
103
|
+
expect(result.code).toContain('Data leaks');
|
|
104
|
+
expect(result.code).toContain('No access control');
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('includes differentiators from strategy', () => {
|
|
108
|
+
const result = generateWebsiteLandingPageWithInfo('gateco', makeContext());
|
|
109
|
+
expect(result.code).toContain('Zero-trust by default');
|
|
110
|
+
expect(result.code).toContain('Vector DB agnostic');
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('renders features with lucide icons', () => {
|
|
114
|
+
const result = generateWebsiteLandingPageWithInfo('gateco', makeContext());
|
|
115
|
+
expect(result.code).toContain('Access Control');
|
|
116
|
+
expect(result.code).toContain('lucide-react');
|
|
117
|
+
expect(result.code).toContain('ICON_MAP');
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('renders FAQ from strategy objections', () => {
|
|
121
|
+
const result = generateWebsiteLandingPageWithInfo('gateco', makeContext());
|
|
122
|
+
expect(result.code).toContain('Is it secure?');
|
|
123
|
+
expect(result.code).toContain('Does it scale?');
|
|
124
|
+
expect(result.code).toContain('FaqItem');
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('renders pricing teaser from context pricing', () => {
|
|
128
|
+
const result = generateWebsiteLandingPageWithInfo('gateco', makeContext());
|
|
129
|
+
expect(result.code).toContain('View full pricing');
|
|
130
|
+
expect(result.code).toContain('$99');
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('skips sections when data is missing', () => {
|
|
134
|
+
const ctx = makeContext({
|
|
135
|
+
strategy: makeStrategy({
|
|
136
|
+
icp: { primaryPersona: 'devs', painPoints: [], goals: [], objections: [] },
|
|
137
|
+
positioning: { category: 'SaaS', differentiators: [], valueProposition: '', proofPoints: [] },
|
|
138
|
+
conversionStrategy: {
|
|
139
|
+
primaryCta: { text: 'Go', href: '/' },
|
|
140
|
+
secondaryCta: { text: 'More', href: '/' },
|
|
141
|
+
trustSignals: [],
|
|
142
|
+
socialProof: [],
|
|
143
|
+
leadCapture: 'none',
|
|
144
|
+
},
|
|
145
|
+
}),
|
|
146
|
+
pricing: undefined,
|
|
147
|
+
});
|
|
148
|
+
const result = generateWebsiteLandingPageWithInfo('gateco', ctx);
|
|
149
|
+
|
|
150
|
+
const skipped = result.sections.filter(s => s.dataSource === 'skipped');
|
|
151
|
+
expect(skipped.map(s => s.name)).toContain('PainPoints');
|
|
152
|
+
expect(skipped.map(s => s.name)).toContain('SocialProof');
|
|
153
|
+
expect(skipped.map(s => s.name)).toContain('FAQ');
|
|
154
|
+
expect(skipped.map(s => s.name)).toContain('PricingTeaser');
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('uses product name as headline when no strategy', () => {
|
|
158
|
+
const ctx = makeContext({ strategy: undefined });
|
|
159
|
+
const result = generateWebsiteLandingPageWithInfo('gateco', ctx);
|
|
160
|
+
expect(result.code).toContain('Gateco');
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('renders only a single H1', () => {
|
|
164
|
+
const result = generateWebsiteLandingPageWithInfo('gateco', makeContext());
|
|
165
|
+
const h1Count = (result.code.match(/<h1/g) || []).length;
|
|
166
|
+
expect(h1Count).toBe(1);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('includes trust signals in hero', () => {
|
|
170
|
+
const result = generateWebsiteLandingPageWithInfo('gateco', makeContext());
|
|
171
|
+
expect(result.code).toContain('Enterprise ready');
|
|
172
|
+
expect(result.code).toContain('SOC2 compliant');
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it('includes dual CTAs from strategy', () => {
|
|
176
|
+
const result = generateWebsiteLandingPageWithInfo('gateco', makeContext());
|
|
177
|
+
expect(result.code).toContain('Try Free');
|
|
178
|
+
expect(result.code).toContain('View Docs');
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it('includes JSON-LD schemas', () => {
|
|
182
|
+
const result = generateWebsiteLandingPageWithInfo('gateco', makeContext());
|
|
183
|
+
expect(result.code).toContain('Organization');
|
|
184
|
+
expect(result.code).toContain('SoftwareApplication');
|
|
185
|
+
// FAQ schema when objections exist
|
|
186
|
+
expect(result.code).toContain('FAQPage');
|
|
187
|
+
});
|
|
188
|
+
});
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for website-pricing module
|
|
3
|
+
* Verifies pricing page generation with tiers, comparison table, and FAQ
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { describe, it, expect } from 'vitest';
|
|
7
|
+
import { generateWebsitePricingPage } from '../../src/generators/templates/website-pricing.js';
|
|
8
|
+
import type { WebsiteContentContext } from '../../src/generators/website-context.js';
|
|
9
|
+
|
|
10
|
+
function makeContext(): WebsiteContentContext {
|
|
11
|
+
return {
|
|
12
|
+
productName: 'Gateco',
|
|
13
|
+
features: [],
|
|
14
|
+
rawDocs: 'docs',
|
|
15
|
+
pricing: [
|
|
16
|
+
{
|
|
17
|
+
name: 'Free',
|
|
18
|
+
price: 'Free',
|
|
19
|
+
period: '',
|
|
20
|
+
description: 'Dev & POC',
|
|
21
|
+
features: ['Basic access', 'Community support'],
|
|
22
|
+
cta: 'Get started',
|
|
23
|
+
featured: false,
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
name: 'Pro',
|
|
27
|
+
price: '$99',
|
|
28
|
+
period: '/month',
|
|
29
|
+
description: 'Production workloads',
|
|
30
|
+
features: ['Basic access', 'Priority support', 'Advanced analytics'],
|
|
31
|
+
cta: 'Start free trial',
|
|
32
|
+
featured: true,
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
name: 'Enterprise',
|
|
36
|
+
price: 'Custom',
|
|
37
|
+
period: '',
|
|
38
|
+
description: 'Regulated environments',
|
|
39
|
+
features: ['Basic access', 'Priority support', 'Advanced analytics', 'SLA guarantee'],
|
|
40
|
+
cta: 'Contact sales',
|
|
41
|
+
featured: false,
|
|
42
|
+
},
|
|
43
|
+
],
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
describe('generateWebsitePricingPage', () => {
|
|
48
|
+
it('generates page with tier cards', () => {
|
|
49
|
+
const code = generateWebsitePricingPage('gateco', makeContext());
|
|
50
|
+
expect(code).toContain('Free');
|
|
51
|
+
expect(code).toContain('$99');
|
|
52
|
+
expect(code).toContain('Custom');
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('includes monthly/annual toggle', () => {
|
|
56
|
+
const code = generateWebsitePricingPage('gateco', makeContext());
|
|
57
|
+
expect(code).toContain('Annual');
|
|
58
|
+
expect(code).toContain('Save 20%');
|
|
59
|
+
expect(code).toContain('useState(false)');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('marks featured tier as Most Popular', () => {
|
|
63
|
+
const code = generateWebsitePricingPage('gateco', makeContext());
|
|
64
|
+
expect(code).toContain('Most Popular');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('includes feature comparison table when tiers have features', () => {
|
|
68
|
+
const code = generateWebsitePricingPage('gateco', makeContext());
|
|
69
|
+
expect(code).toContain('Compare plans');
|
|
70
|
+
expect(code).toContain('Basic access');
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('includes pricing FAQ', () => {
|
|
74
|
+
const code = generateWebsitePricingPage('gateco', makeContext());
|
|
75
|
+
expect(code).toContain('Pricing FAQ');
|
|
76
|
+
expect(code).toContain('switch plans');
|
|
77
|
+
expect(code).toContain('free trial');
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('includes enterprise CTA section', () => {
|
|
81
|
+
const code = generateWebsitePricingPage('gateco', makeContext());
|
|
82
|
+
expect(code).toContain('custom plan');
|
|
83
|
+
expect(code).toContain('/contact');
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('renders single H1', () => {
|
|
87
|
+
const code = generateWebsitePricingPage('gateco', makeContext());
|
|
88
|
+
const h1Count = (code.match(/<h1/g) || []).length;
|
|
89
|
+
expect(h1Count).toBe(1);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('generates defaults when no pricing provided', () => {
|
|
93
|
+
const code = generateWebsitePricingPage('gateco');
|
|
94
|
+
expect(code).toContain('Starter');
|
|
95
|
+
expect(code).toContain('Pro');
|
|
96
|
+
expect(code).toContain('Enterprise');
|
|
97
|
+
});
|
|
98
|
+
});
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for website-sections module
|
|
3
|
+
* Verifies FAQ, Stats (no fake numbers), HowItWorks, PainPoints
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { describe, it, expect } from 'vitest';
|
|
7
|
+
import {
|
|
8
|
+
mapFeatureIcon,
|
|
9
|
+
isNumericMetric,
|
|
10
|
+
generatePainPointsSection,
|
|
11
|
+
generateDifferentiatorsSection,
|
|
12
|
+
generateHowItWorksSection,
|
|
13
|
+
generateStatsSection,
|
|
14
|
+
generateSocialProofSection,
|
|
15
|
+
generateFaqSection,
|
|
16
|
+
buildFaqItemsDeclaration,
|
|
17
|
+
generateFaqItemComponent,
|
|
18
|
+
generatePricingTeaserSection,
|
|
19
|
+
} from '../../src/generators/templates/website-sections.js';
|
|
20
|
+
import type { WebsiteStrategyDocument } from '../../src/types/website-strategy.js';
|
|
21
|
+
|
|
22
|
+
function makeStrategy(overrides?: Partial<WebsiteStrategyDocument>): WebsiteStrategyDocument {
|
|
23
|
+
return {
|
|
24
|
+
icp: { primaryPersona: 'devs', painPoints: ['No ACL', 'Slow queries'], goals: [], objections: ['Is it secure?'] },
|
|
25
|
+
positioning: {
|
|
26
|
+
category: 'Security',
|
|
27
|
+
differentiators: ['Zero-trust', 'DB agnostic'],
|
|
28
|
+
valueProposition: 'Secure AI access',
|
|
29
|
+
proofPoints: ['SOC2 compliant', '10K+ queries/sec', '99.9% uptime'],
|
|
30
|
+
},
|
|
31
|
+
messaging: { headline: 'h', subheadline: 's', elevatorPitch: 'e', longDescription: 'l' },
|
|
32
|
+
seoStrategy: { primaryKeywords: [], secondaryKeywords: [], longTailKeywords: [], titleTemplates: {}, metaDescriptions: {} },
|
|
33
|
+
siteArchitecture: { pages: [{ path: '/', title: 'Home', purpose: '', pageType: 'landing', sections: ['Sign Up', 'Configure', 'Deploy'], seoKeywords: [], conversionGoal: '' }], navigation: [], footerSections: [] },
|
|
34
|
+
conversionStrategy: { primaryCta: { text: 'Go', href: '/' }, secondaryCta: { text: 'More', href: '/' }, trustSignals: [], socialProof: ['Great tool!'], leadCapture: 'none' },
|
|
35
|
+
competitiveContext: { category: 'sec', competitors: [], differentiators: [] },
|
|
36
|
+
...overrides,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
describe('mapFeatureIcon', () => {
|
|
41
|
+
it('maps security-related features to Shield icon', () => {
|
|
42
|
+
expect(mapFeatureIcon('Access Control')).toBe('Shield');
|
|
43
|
+
expect(mapFeatureIcon('Authentication')).toBe('Shield');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('maps search features to Search icon', () => {
|
|
47
|
+
expect(mapFeatureIcon('Vector Search')).toBe('Search');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('returns Star for unknown features', () => {
|
|
51
|
+
expect(mapFeatureIcon('Something Unique')).toBe('Star');
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
describe('isNumericMetric', () => {
|
|
56
|
+
it('detects numeric metrics', () => {
|
|
57
|
+
expect(isNumericMetric('10K+ queries/sec')).toBe(true);
|
|
58
|
+
expect(isNumericMetric('99.9% uptime')).toBe(true);
|
|
59
|
+
expect(isNumericMetric('500+ customers')).toBe(true);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('rejects qualitative metrics', () => {
|
|
63
|
+
expect(isNumericMetric('SOC2 compliant')).toBe(false);
|
|
64
|
+
expect(isNumericMetric('Enterprise ready')).toBe(false);
|
|
65
|
+
expect(isNumericMetric('Audit-ready')).toBe(false);
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
describe('generatePainPointsSection', () => {
|
|
70
|
+
it('renders pain points from strategy', () => {
|
|
71
|
+
const { jsx, info } = generatePainPointsSection(makeStrategy());
|
|
72
|
+
expect(jsx).toContain('No ACL');
|
|
73
|
+
expect(jsx).toContain('Slow queries');
|
|
74
|
+
expect(info.dataSource).toBe('strategy');
|
|
75
|
+
expect(info.itemCount).toBe(2);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('skips when no pain points', () => {
|
|
79
|
+
const strategy = makeStrategy({ icp: { primaryPersona: 'devs', painPoints: [], goals: [], objections: [] } });
|
|
80
|
+
const { jsx, info } = generatePainPointsSection(strategy);
|
|
81
|
+
expect(jsx).toBe('');
|
|
82
|
+
expect(info.dataSource).toBe('skipped');
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
describe('generateDifferentiatorsSection', () => {
|
|
87
|
+
it('renders differentiators with value proposition heading', () => {
|
|
88
|
+
const { jsx, info } = generateDifferentiatorsSection(makeStrategy());
|
|
89
|
+
expect(jsx).toContain('Secure AI access');
|
|
90
|
+
expect(jsx).toContain('Zero-trust');
|
|
91
|
+
expect(info.dataSource).toBe('strategy');
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('skips when no differentiators', () => {
|
|
95
|
+
const strategy = makeStrategy({
|
|
96
|
+
positioning: { category: 'x', differentiators: [], valueProposition: '', proofPoints: [] },
|
|
97
|
+
});
|
|
98
|
+
const { jsx, info } = generateDifferentiatorsSection(strategy);
|
|
99
|
+
expect(jsx).toBe('');
|
|
100
|
+
expect(info.dataSource).toBe('skipped');
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
describe('generateHowItWorksSection', () => {
|
|
105
|
+
it('renders with strategy sections', () => {
|
|
106
|
+
const { jsx, info } = generateHowItWorksSection(makeStrategy());
|
|
107
|
+
expect(jsx).toContain('How it works');
|
|
108
|
+
expect(info.dataSource).toBe('strategy');
|
|
109
|
+
expect(info.itemCount).toBe(3);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('uses defaults when no strategy', () => {
|
|
113
|
+
const { jsx, info } = generateHowItWorksSection(undefined);
|
|
114
|
+
expect(jsx).toContain('Sign Up');
|
|
115
|
+
expect(jsx).toContain('Configure');
|
|
116
|
+
expect(jsx).toContain('Deploy');
|
|
117
|
+
expect(info.dataSource).toBe('defaults');
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
describe('generateStatsSection', () => {
|
|
122
|
+
it('renders numeric metrics as stats and qualitative as badges', () => {
|
|
123
|
+
const { jsx, info } = generateStatsSection(makeStrategy());
|
|
124
|
+
|
|
125
|
+
// Numeric: rendered as big stat
|
|
126
|
+
expect(jsx).toContain('10K+ queries/sec');
|
|
127
|
+
expect(jsx).toContain('99.9% uptime');
|
|
128
|
+
|
|
129
|
+
// Qualitative: rendered as badge
|
|
130
|
+
expect(jsx).toContain('SOC2 compliant');
|
|
131
|
+
|
|
132
|
+
expect(info.dataSource).toBe('strategy');
|
|
133
|
+
expect(info.itemCount).toBe(3);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('does NOT fabricate numeric stats from qualitative data', () => {
|
|
137
|
+
const strategy = makeStrategy({
|
|
138
|
+
positioning: {
|
|
139
|
+
category: 'x',
|
|
140
|
+
differentiators: [],
|
|
141
|
+
valueProposition: '',
|
|
142
|
+
proofPoints: ['Audit-ready', 'HIPAA compliant'],
|
|
143
|
+
},
|
|
144
|
+
});
|
|
145
|
+
const { jsx } = generateStatsSection(strategy);
|
|
146
|
+
|
|
147
|
+
// Should NOT contain fabricated numbers
|
|
148
|
+
expect(jsx).not.toContain('10K+');
|
|
149
|
+
expect(jsx).not.toContain('99.9%');
|
|
150
|
+
// Should contain qualitative badges
|
|
151
|
+
expect(jsx).toContain('Audit-ready');
|
|
152
|
+
expect(jsx).toContain('HIPAA compliant');
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('skips when no proof points', () => {
|
|
156
|
+
const strategy = makeStrategy({
|
|
157
|
+
positioning: { category: 'x', differentiators: [], valueProposition: '', proofPoints: [] },
|
|
158
|
+
});
|
|
159
|
+
const { jsx, info } = generateStatsSection(strategy);
|
|
160
|
+
expect(jsx).toBe('');
|
|
161
|
+
expect(info.dataSource).toBe('skipped');
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
describe('generateSocialProofSection', () => {
|
|
166
|
+
it('renders social proof quotes', () => {
|
|
167
|
+
const { jsx, info } = generateSocialProofSection(makeStrategy());
|
|
168
|
+
expect(jsx).toContain('Great tool!');
|
|
169
|
+
expect(info.dataSource).toBe('strategy');
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it('skips when empty', () => {
|
|
173
|
+
const strategy = makeStrategy({
|
|
174
|
+
conversionStrategy: {
|
|
175
|
+
primaryCta: { text: 'Go', href: '/' },
|
|
176
|
+
secondaryCta: { text: 'More', href: '/' },
|
|
177
|
+
trustSignals: [],
|
|
178
|
+
socialProof: [],
|
|
179
|
+
leadCapture: 'none',
|
|
180
|
+
},
|
|
181
|
+
});
|
|
182
|
+
const { jsx, info } = generateSocialProofSection(strategy);
|
|
183
|
+
expect(jsx).toBe('');
|
|
184
|
+
expect(info.dataSource).toBe('skipped');
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
describe('generateFaqSection', () => {
|
|
189
|
+
it('renders FAQ from objections', () => {
|
|
190
|
+
const { jsx, info, needsClientDirective } = generateFaqSection(makeStrategy());
|
|
191
|
+
// FAQ section JSX references faqItems via runtime mapping
|
|
192
|
+
expect(jsx).toContain('FaqItem');
|
|
193
|
+
expect(jsx).toContain('faqItems.map');
|
|
194
|
+
expect(info.dataSource).toBe('strategy');
|
|
195
|
+
expect(needsClientDirective).toBe(true);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it('builds FAQ items declaration with objection content', () => {
|
|
199
|
+
const decl = buildFaqItemsDeclaration(makeStrategy());
|
|
200
|
+
expect(decl).toContain('Is it secure?');
|
|
201
|
+
expect(decl).toContain('faqItems');
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it('includes keyboard-accessible accordion in FaqItem component', () => {
|
|
205
|
+
const component = generateFaqItemComponent();
|
|
206
|
+
expect(component).toContain('aria-expanded');
|
|
207
|
+
expect(component).toContain('onClick');
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it('skips when no objections', () => {
|
|
211
|
+
const strategy = makeStrategy({
|
|
212
|
+
icp: { primaryPersona: 'devs', painPoints: [], goals: [], objections: [] },
|
|
213
|
+
});
|
|
214
|
+
const { jsx, info } = generateFaqSection(strategy);
|
|
215
|
+
expect(jsx).toBe('');
|
|
216
|
+
expect(info.dataSource).toBe('skipped');
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
describe('generatePricingTeaserSection', () => {
|
|
221
|
+
it('renders teaser when pricing exists', () => {
|
|
222
|
+
const { jsx, info } = generatePricingTeaserSection({
|
|
223
|
+
productName: 'Test',
|
|
224
|
+
features: [],
|
|
225
|
+
rawDocs: '',
|
|
226
|
+
pricing: [
|
|
227
|
+
{ name: 'Free', price: 'Free', description: 'Basic', features: [], cta: 'Start' },
|
|
228
|
+
{ name: 'Pro', price: '$99', period: '/mo', description: 'Full', features: [], cta: 'Buy', featured: true },
|
|
229
|
+
],
|
|
230
|
+
});
|
|
231
|
+
expect(jsx).toContain('$99');
|
|
232
|
+
expect(jsx).toContain('View full pricing');
|
|
233
|
+
expect(info.dataSource).toBe('docs');
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it('skips when no pricing', () => {
|
|
237
|
+
const { jsx, info } = generatePricingTeaserSection({
|
|
238
|
+
productName: 'Test',
|
|
239
|
+
features: [],
|
|
240
|
+
rawDocs: '',
|
|
241
|
+
});
|
|
242
|
+
expect(jsx).toBe('');
|
|
243
|
+
expect(info.dataSource).toBe('skipped');
|
|
244
|
+
});
|
|
245
|
+
});
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for workspace root detection
|
|
3
|
+
* Verifies .popeye/ detection, workspaces in package.json, and fallback behavior
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
7
|
+
import { promises as fs } from 'node:fs';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
import os from 'node:os';
|
|
10
|
+
import { resolveWorkspaceRoot, getScanDirectories } from '../../src/generators/workspace-root.js';
|
|
11
|
+
|
|
12
|
+
describe('resolveWorkspaceRoot', () => {
|
|
13
|
+
let tmpDir: string;
|
|
14
|
+
|
|
15
|
+
beforeEach(async () => {
|
|
16
|
+
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'popeye-ws-root-'));
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
afterEach(async () => {
|
|
20
|
+
await fs.rm(tmpDir, { recursive: true, force: true });
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('detects .popeye/ directory as workspace root', async () => {
|
|
24
|
+
// Create nested structure: tmpDir/project/.popeye/ with cwd = tmpDir/project/sub
|
|
25
|
+
const projectDir = path.join(tmpDir, 'project');
|
|
26
|
+
const subDir = path.join(projectDir, 'sub');
|
|
27
|
+
await fs.mkdir(path.join(projectDir, '.popeye'), { recursive: true });
|
|
28
|
+
await fs.mkdir(subDir, { recursive: true });
|
|
29
|
+
|
|
30
|
+
const root = await resolveWorkspaceRoot(subDir);
|
|
31
|
+
|
|
32
|
+
expect(root).toBe(projectDir);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('detects package.json with workspaces field', async () => {
|
|
36
|
+
const projectDir = path.join(tmpDir, 'monorepo');
|
|
37
|
+
const subDir = path.join(projectDir, 'apps', 'website');
|
|
38
|
+
await fs.mkdir(subDir, { recursive: true });
|
|
39
|
+
await fs.writeFile(
|
|
40
|
+
path.join(projectDir, 'package.json'),
|
|
41
|
+
JSON.stringify({ name: 'root', workspaces: ['apps/*', 'packages/*'] })
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
const root = await resolveWorkspaceRoot(subDir);
|
|
45
|
+
|
|
46
|
+
expect(root).toBe(projectDir);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('falls back to cwd when no workspace indicators found', async () => {
|
|
50
|
+
const cwd = path.join(tmpDir, 'standalone');
|
|
51
|
+
await fs.mkdir(cwd, { recursive: true });
|
|
52
|
+
|
|
53
|
+
const root = await resolveWorkspaceRoot(cwd);
|
|
54
|
+
|
|
55
|
+
expect(root).toBe(cwd);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('detects turbo.json or pnpm-workspace.yaml', async () => {
|
|
59
|
+
const projectDir = path.join(tmpDir, 'turborepo');
|
|
60
|
+
const subDir = path.join(projectDir, 'apps', 'web');
|
|
61
|
+
await fs.mkdir(subDir, { recursive: true });
|
|
62
|
+
await fs.writeFile(path.join(projectDir, 'turbo.json'), '{}');
|
|
63
|
+
|
|
64
|
+
const root = await resolveWorkspaceRoot(subDir);
|
|
65
|
+
|
|
66
|
+
expect(root).toBe(projectDir);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
describe('getScanDirectories', () => {
|
|
71
|
+
let tmpDir: string;
|
|
72
|
+
|
|
73
|
+
beforeEach(async () => {
|
|
74
|
+
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'popeye-scan-'));
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
afterEach(async () => {
|
|
78
|
+
await fs.rm(tmpDir, { recursive: true, force: true });
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('includes workspace root, parent, and subdirectories', async () => {
|
|
82
|
+
const projectDir = path.join(tmpDir, 'project');
|
|
83
|
+
await fs.mkdir(path.join(projectDir, '.popeye'), { recursive: true });
|
|
84
|
+
await fs.mkdir(path.join(projectDir, 'docs'), { recursive: true });
|
|
85
|
+
await fs.mkdir(path.join(projectDir, 'brand'), { recursive: true });
|
|
86
|
+
|
|
87
|
+
const dirs = await getScanDirectories(projectDir);
|
|
88
|
+
|
|
89
|
+
expect(dirs).toContain(projectDir);
|
|
90
|
+
expect(dirs).toContain(tmpDir); // parent
|
|
91
|
+
expect(dirs).toContain(path.join(projectDir, 'docs'));
|
|
92
|
+
expect(dirs).toContain(path.join(projectDir, 'brand'));
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('deduplicates directories', async () => {
|
|
96
|
+
const projectDir = path.join(tmpDir, 'project');
|
|
97
|
+
await fs.mkdir(projectDir, { recursive: true });
|
|
98
|
+
|
|
99
|
+
const dirs = await getScanDirectories(projectDir);
|
|
100
|
+
|
|
101
|
+
// No duplicates
|
|
102
|
+
const unique = new Set(dirs);
|
|
103
|
+
expect(unique.size).toBe(dirs.length);
|
|
104
|
+
});
|
|
105
|
+
});
|