@funnelsgrove/cli 0.1.87 → 0.1.89

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.
Files changed (121) hide show
  1. package/dist/apiClient.d.ts +1 -0
  2. package/dist/apiClient.js +32 -1
  3. package/funnel-contract-compatibility.json +28 -28
  4. package/package.json +3 -3
  5. package/template_docs/.funnelsgrove-docs.json +3 -3
  6. package/template_docs/docs/funnelsgrove/migrations/step-contract-v3.md +1 -1
  7. package/template_docs/funnel-docs.config.json +1 -1
  8. package/template_scaffold/.funnelsgrove-docs.json +3 -3
  9. package/template_scaffold/.funnelsgrove-scaffold.json +26 -471
  10. package/template_scaffold/PLAN.md +15 -29
  11. package/template_scaffold/context/build-context.md +3 -3
  12. package/template_scaffold/docs/funnelsgrove/migrations/step-contract-v3.md +1 -1
  13. package/template_scaffold/funnel-docs.config.json +1 -1
  14. package/template_scaffold/package-lock.json +9 -9
  15. package/template_scaffold/package.json +2 -2
  16. package/template_scaffold/public/assets/email-capture-preview.svg +5 -0
  17. package/template_scaffold/public/assets/paywall-preview.svg +6 -0
  18. package/template_scaffold/src/app/sdk/page.tsx +14 -26
  19. package/template_scaffold/src/components/FunnelFlow.tsx +8 -1
  20. package/template_scaffold/src/config/billing.plans.ts +71 -21
  21. package/template_scaffold/src/config/funnel.manifest.ts +20 -59
  22. package/template_scaffold/src/runtime/checkout-runtime-config.ts +0 -7
  23. package/template_scaffold/src/runtime/step-registry.ts +0 -28
  24. package/template_scaffold/src/steps/content/step-01.content.ts +4 -9
  25. package/template_scaffold/src/steps/content/step-32-paywall.content.ts +12 -50
  26. package/template_scaffold/src/steps/step-01.tsx +43 -50
  27. package/template_scaffold/src/steps/step-31-email-capture.tsx +18 -1
  28. package/template_scaffold/src/steps/step-32-paywall.tsx +93 -299
  29. package/template_scaffold/src/steps/step-33-subscription-started.tsx +1 -1
  30. package/template_scaffold/src/steps/step-content.registry.ts +1 -26
  31. package/template_scaffold/public/claimbee/bee.png +0 -0
  32. package/template_scaffold/public/claimbee/device-iphone.png +0 -0
  33. package/template_scaffold/public/claimbee/device-macbook.png +0 -0
  34. package/template_scaffold/public/claimbee/device-none.png +0 -0
  35. package/template_scaffold/public/claimbee/device-watch.png +0 -0
  36. package/template_scaffold/public/claimbee/glow.svg +0 -3
  37. package/template_scaffold/public/claimbee/steps-sprite-alt.png +0 -0
  38. package/template_scaffold/public/claimbee/steps-sprite-default.png +0 -0
  39. package/template_scaffold/public/claimbee/testimonial-avatar.png +0 -0
  40. package/template_scaffold/public/claimbee/testimonial-star.svg +0 -3
  41. package/template_scaffold/public/paywall/ai-claim-assistant.png +0 -0
  42. package/template_scaffold/public/paywall/claim-history.png +0 -0
  43. package/template_scaffold/public/paywall/clear-progress.png +0 -0
  44. package/template_scaffold/public/paywall/hero.png +0 -0
  45. package/template_scaffold/public/paywall/money-alerts.png +0 -0
  46. package/template_scaffold/public/paywall/money-back-badge.png +0 -0
  47. package/template_scaffold/public/paywall/paywall-image.png +0 -0
  48. package/template_scaffold/public/paywall/r1.png +0 -0
  49. package/template_scaffold/public/paywall/r2.png +0 -0
  50. package/template_scaffold/public/paywall/r3.png +0 -0
  51. package/template_scaffold/public/paywall/r4.png +0 -0
  52. package/template_scaffold/public/paywall/r5.png +0 -0
  53. package/template_scaffold/public/paywall/r6.png +0 -0
  54. package/template_scaffold/public/paywall/special-offer-gift.png +0 -0
  55. package/template_scaffold/public/please/bee.png +0 -0
  56. package/template_scaffold/public/please/device-iphone.png +0 -0
  57. package/template_scaffold/public/please/device-macbook.png +0 -0
  58. package/template_scaffold/public/please/device-none.png +0 -0
  59. package/template_scaffold/public/please/device-watch.png +0 -0
  60. package/template_scaffold/public/please/glow.svg +0 -3
  61. package/template_scaffold/public/please/steps-sprite-alt.png +0 -0
  62. package/template_scaffold/public/please/steps-sprite-default.png +0 -0
  63. package/template_scaffold/public/please/testimonial-avatar.png +0 -0
  64. package/template_scaffold/public/please/testimonial-star.svg +0 -3
  65. package/template_scaffold/public/upsell/upgrade-hero.png +0 -0
  66. package/template_scaffold/src/assets/claimbee/bee.png +0 -0
  67. package/template_scaffold/src/assets/claimbee/device-iphone.png +0 -0
  68. package/template_scaffold/src/assets/claimbee/device-macbook.png +0 -0
  69. package/template_scaffold/src/assets/claimbee/device-none.png +0 -0
  70. package/template_scaffold/src/assets/claimbee/device-watch.png +0 -0
  71. package/template_scaffold/src/assets/claimbee/glow.svg +0 -3
  72. package/template_scaffold/src/assets/claimbee/steps-sprite-alt.png +0 -0
  73. package/template_scaffold/src/assets/claimbee/steps-sprite-default.png +0 -0
  74. package/template_scaffold/src/assets/claimbee/testimonial-avatar.png +0 -0
  75. package/template_scaffold/src/assets/claimbee/testimonial-star.svg +0 -3
  76. package/template_scaffold/src/components/FunnelStepImage.tsx +0 -28
  77. package/template_scaffold/src/config/offer-sets.generated.ts +0 -4
  78. package/template_scaffold/src/steps/content/manage-subscription.content.ts +0 -62
  79. package/template_scaffold/src/steps/content/paywall.content.ts +0 -191
  80. package/template_scaffold/src/steps/content/step-02.content.ts +0 -77
  81. package/template_scaffold/src/steps/content/step-03.content.ts +0 -40
  82. package/template_scaffold/src/steps/content/step-04.content.ts +0 -49
  83. package/template_scaffold/src/steps/editor/manage-subscription.editor.ts +0 -179
  84. package/template_scaffold/src/steps/editor/paywall.editor.ts +0 -232
  85. package/template_scaffold/src/steps/editor/step-02.editor.ts +0 -41
  86. package/template_scaffold/src/steps/editor/step-03.editor.ts +0 -58
  87. package/template_scaffold/src/steps/editor/step-04.editor.ts +0 -98
  88. package/template_scaffold/src/steps/pricing/paywall-b.pricing.ts +0 -12
  89. package/template_scaffold/src/steps/step-02.tsx +0 -140
  90. package/template_scaffold/src/steps/step-03.tsx +0 -262
  91. package/template_scaffold/src/steps/step-04.tsx +0 -673
  92. package/template_scaffold/src/steps/step-32b-paywall-b.tsx +0 -35
  93. package/template_scaffold/src/steps/step-35-manage-subscription.tsx +0 -46
  94. package/template_scaffold/src/steps/use-claimbee-step-content.ts +0 -15
  95. package/template_scaffold/tests/funnel-agent-docs.test.ts +0 -751
  96. package/template_scaffold/tests/package.test.ts +0 -37
  97. package/template_scaffold/tests/src/app/[stepId]/page.test.tsx +0 -59
  98. package/template_scaffold/tests/src/app/layout.test.ts +0 -26
  99. package/template_scaffold/tests/src/components/FunnelFlow.test.tsx +0 -38
  100. package/template_scaffold/tests/src/components/FunnelStepImage.test.ts +0 -15
  101. package/template_scaffold/tests/src/config/billing.plans.test.ts +0 -143
  102. package/template_scaffold/tests/src/config/funnel.manifest.test.ts +0 -166
  103. package/template_scaffold/tests/src/contract/funnel-project.validation.test.ts +0 -79
  104. package/template_scaffold/tests/src/contract/golden/golden-fixtures.test.ts +0 -190
  105. package/template_scaffold/tests/src/contract/validate-funnel.cli.test.ts +0 -191
  106. package/template_scaffold/tests/src/runtime/checkout-runtime-config.test.ts +0 -60
  107. package/template_scaffold/tests/src/runtime/funnel-runtime.test.ts +0 -25
  108. package/template_scaffold/tests/src/runtime/step-content-context.test.ts +0 -38
  109. package/template_scaffold/tests/src/runtime/use-funnel-flow-controller.integration.test.ts +0 -203
  110. package/template_scaffold/tests/src/runtime/use-funnel-flow-controller.test.ts +0 -76
  111. package/template_scaffold/tests/src/steps/paywall-checkout-options.test.ts +0 -104
  112. package/template_scaffold/tests/src/steps/shared-header-contract.test.ts +0 -29
  113. package/template_scaffold/tests/src/steps/step-01.test.ts +0 -38
  114. package/template_scaffold/tests/src/steps/step-02.test.ts +0 -43
  115. package/template_scaffold/tests/src/steps/step-03.test.ts +0 -15
  116. package/template_scaffold/tests/src/steps/step-04.test.ts +0 -32
  117. package/template_scaffold/tests/src/steps/step-31-email-capture.test.ts +0 -22
  118. package/template_scaffold/tests/src/steps/step-32-paywall.test.ts +0 -773
  119. package/template_scaffold/tests/src/steps/step-33-subscription-started.test.ts +0 -44
  120. package/template_scaffold/tests/src/steps/step-34-upsell-form.test.ts +0 -27
  121. package/template_scaffold/tests/src/steps/step-image-migration.test.ts +0 -20
@@ -1,190 +0,0 @@
1
- import { describe, expect, it } from 'vitest';
2
- import {
3
- CURRENT_STEP_CONTRACT_VERSION,
4
- FUNNEL_STEP_TYPES,
5
- resolveStepTypeContract,
6
- validateFunnelManifestContract,
7
- validateFunnelStepMetadataParity,
8
- type FunnelContextValue,
9
- type FunnelManifest,
10
- } from '@funnelsgrove/runtime';
11
- import { publicAnalyticsSdk } from '@funnelsgrove/analytics';
12
- import { goldenFixture as cancellationOffer } from '../../../../src/contract/golden/cancellation_offer.fixture';
13
- import { goldenFixture as checkout } from '../../../../src/contract/golden/checkout.fixture';
14
- import { goldenFixture as purchaseCompleted } from '../../../../src/contract/golden/purchase_completed.fixture';
15
- import { goldenFixture as formInput } from '../../../../src/contract/golden/form_input.fixture';
16
- import { goldenFixture as introHero } from '../../../../src/contract/golden/intro_hero.fixture';
17
- import { goldenFixture as multiSelectChoice } from '../../../../src/contract/golden/multi_select_choice.fixture';
18
- import { goldenFixture as paywallOffer } from '../../../../src/contract/golden/paywall_offer.fixture';
19
- import { goldenFixture as progressInterstitial } from '../../../../src/contract/golden/progress_interstitial.fixture';
20
- import { goldenFixture as singleStepChoice } from '../../../../src/contract/golden/single_step_choice.fixture';
21
- import { goldenFixture as singleStepChoiceEmoji } from '../../../../src/contract/golden/single_step_choice_emoji.fixture';
22
- import { goldenFixture as socialProof } from '../../../../src/contract/golden/social_proof.fixture';
23
- import { goldenFixture as subscriptionHandoff } from '../../../../src/contract/golden/subscription_handoff.fixture';
24
- import { goldenFixture as subscriptionManagement } from '../../../../src/contract/golden/subscription_management.fixture';
25
- import { goldenFixture as summaryConfirmation } from '../../../../src/contract/golden/summary_confirmation.fixture';
26
- import { goldenFixture as upsellOffer } from '../../../../src/contract/golden/upsell_offer.fixture';
27
- import { goldenFixture as valuePropStory } from '../../../../src/contract/golden/value_prop_story.fixture';
28
-
29
- const fixtures = [
30
- cancellationOffer,
31
- checkout,
32
- purchaseCompleted,
33
- formInput,
34
- introHero,
35
- multiSelectChoice,
36
- paywallOffer,
37
- progressInterstitial,
38
- singleStepChoice,
39
- singleStepChoiceEmoji,
40
- socialProof,
41
- subscriptionHandoff,
42
- subscriptionManagement,
43
- summaryConfirmation,
44
- upsellOffer,
45
- valuePropStory,
46
- ];
47
-
48
- const validationFixtures = [
49
- introHero,
50
- valuePropStory,
51
- singleStepChoice,
52
- singleStepChoiceEmoji,
53
- multiSelectChoice,
54
- formInput,
55
- socialProof,
56
- progressInterstitial,
57
- paywallOffer,
58
- upsellOffer,
59
- checkout,
60
- subscriptionManagement,
61
- subscriptionHandoff,
62
- cancellationOffer,
63
- summaryConfirmation,
64
- purchaseCompleted,
65
- ];
66
-
67
- const commerceSemanticHelpers = [
68
- 'trackCheckoutStarted',
69
- 'trackPaymentInfoSubmitted',
70
- 'trackCheckoutCompleted',
71
- ] as const satisfies readonly (keyof typeof publicAnalyticsSdk)[];
72
- const terminalSemanticHelpers = (
73
- purchaseCompleted.semanticHelpers
74
- ) satisfies readonly (keyof FunnelContextValue)[];
75
-
76
- describe('step contract golden fixtures', () => {
77
- it('covers all 16 step types exactly once with component/manifest metadata parity', () => {
78
- expect(fixtures).toHaveLength(16);
79
- expect(new Set(fixtures.map((fixture) => fixture.stepType)).size).toBe(16);
80
- expect(fixtures.map((fixture) => fixture.stepType).sort()).toEqual(
81
- FUNNEL_STEP_TYPES.filter((type) => (
82
- resolveStepTypeContract(CURRENT_STEP_CONTRACT_VERSION, type)
83
- )).sort(),
84
- );
85
- for (const fixture of fixtures) {
86
- expect(fixture.manifestStep.type).toBe(fixture.stepType);
87
- expect(fixture.componentMeta).toMatchObject({
88
- id: fixture.manifestStep.id,
89
- name: fixture.manifestStep.name,
90
- type: fixture.manifestStep.type,
91
- });
92
- expect(fixture.manifestStep.filePath).toMatch(/^src\/steps\/.+\.tsx$/);
93
- expect(fixture.manifestStep.componentKey).toBeTruthy();
94
- expect(fixture.manifestStep.title).toBeTruthy();
95
- expect(fixture.componentMeta.figmaNodeId).toBeTruthy();
96
- expect(fixture.componentMeta.title).toBeTruthy();
97
- expect(fixture.componentMeta.description).toBeTruthy();
98
- expect(validateFunnelStepMetadataParity(
99
- fixture.manifestStep,
100
- fixture.componentMeta,
101
- )).toEqual([]);
102
- }
103
- });
104
-
105
- it('forms one valid self-contained contract-v3 manifest', () => {
106
- const manifest = {
107
- templateArchitectureVersion: 1,
108
- stepContractVersion: CURRENT_STEP_CONTRACT_VERSION,
109
- meta: { title: 'Golden fixture funnel', description: 'Validates all canonical types.' },
110
- viewport: { width: 430, height: 932 },
111
- assets: {},
112
- steps: validationFixtures.map((fixture) => fixture.manifestStep),
113
- edgesByStepId: Object.fromEntries(validationFixtures.slice(0, -1).map((fixture, index) => [
114
- fixture.manifestStep.id,
115
- [{ toStepId: validationFixtures[index + 1].manifestStep.id }],
116
- ])),
117
- experiments: [],
118
- } satisfies FunnelManifest;
119
-
120
- expect(validateFunnelManifestContract(manifest, { mode: 'authoring' })).toEqual([]);
121
- });
122
-
123
- it('uses stable scalar and array IDs for choice answers', () => {
124
- expect(singleStepChoice.persistedAnswer).toBe('daily');
125
- expect(singleStepChoiceEmoji.persistedAnswer).toBe('focused');
126
- expect(singleStepChoiceEmoji.options.every((option) => Boolean(option.emoji))).toBe(true);
127
- expect(multiSelectChoice.persistedAnswer).toEqual(['sleep', 'focus']);
128
- expect(multiSelectChoice.emptyAnswer).toEqual([]);
129
- expect(multiSelectChoice.manifestStep.choice.allowEmpty).toBe(true);
130
- for (const fixture of [singleStepChoice, singleStepChoiceEmoji, multiSelectChoice]) {
131
- expect(fixture.manifestStep.choice.answerKey).toBeTruthy();
132
- expect(fixture.helper).toBe('useStepChoices');
133
- }
134
- });
135
-
136
- it('locks reserved identities and trusted conversion ownership', () => {
137
- expect(formInput.manifestStep).toMatchObject({ id: 'email-capture', name: 'email-capture' });
138
- expect(formInput.helper).toBe('submitEmailCapture');
139
- expect(formInput.forbiddenCalls).toEqual(expect.arrayContaining([
140
- 'trackLead',
141
- 'provider APIs',
142
- 'hard-coded paywall navigation',
143
- ]));
144
- expect(paywallOffer.manifestStep).toMatchObject({
145
- id: 'paywall',
146
- name: 'paywall',
147
- type: 'paywall_offer',
148
- kind: 'paywall',
149
- });
150
- expect(subscriptionManagement.manifestStep).toMatchObject({
151
- id: 'manage-subscription',
152
- name: 'manage-subscription',
153
- kind: 'manage-subscription',
154
- });
155
- expect(purchaseCompleted.manifestStep).toMatchObject({
156
- id: 'subscription-started',
157
- name: 'subscription-started',
158
- type: 'purchase_completed',
159
- kind: 'subscription-handoff',
160
- });
161
- expect(purchaseCompleted.forbiddenBrowserEvents).toEqual([
162
- 'purchase_completed',
163
- 'registration_completed',
164
- ]);
165
- expect(terminalSemanticHelpers).toEqual(['completeFunnel']);
166
- expect(subscriptionHandoff.isRegistrationCompletion).toBe(false);
167
- expect(cancellationOffer.manifestStep.kind).toBe('cancellation');
168
- expect(upsellOffer.manifestStep.kind).toBe('upsell');
169
- });
170
-
171
- it('uses named checkout helpers only on commerce steps', () => {
172
- for (const fixture of [paywallOffer, upsellOffer, checkout]) {
173
- expect(fixture.semanticHelpers).toEqual(commerceSemanticHelpers);
174
- }
175
- expect(typeof publicAnalyticsSdk.trackPaymentInfoSubmitted).toBe('function');
176
- expect('trackAddPaymentInfo' in publicAnalyticsSdk).toBe(false);
177
- expect(progressInterstitial.semanticHelpers).toEqual([]);
178
- expect(progressInterstitial.isPaywall).toBe(false);
179
- expect(checkout.requiredEventMetadata).toEqual([
180
- 'funnelId',
181
- 'funnelVersionId',
182
- 'environment',
183
- 'stepContractVersion',
184
- 'stepId',
185
- 'stepName',
186
- 'stepType',
187
- 'eventId',
188
- ]);
189
- });
190
- });
@@ -1,191 +0,0 @@
1
- import { execFile } from 'node:child_process';
2
- import { existsSync } from 'node:fs';
3
- import path from 'node:path';
4
- import { promisify } from 'node:util';
5
- import { fileURLToPath } from 'node:url';
6
- import { describe, expect, it } from 'vitest';
7
- import {
8
- createFunnelContractDiagnostic,
9
- CURRENT_STEP_CONTRACT_VERSION,
10
- type FunnelContractDiagnostic,
11
- } from '@funnelsgrove/runtime';
12
-
13
- const execFileAsync = promisify(execFile);
14
-
15
- const findExecutableOnPath = (name: string): string => {
16
- for (const directory of (process.env.PATH || '').split(path.delimiter)) {
17
- const candidate = path.join(directory, name);
18
- if (existsSync(candidate)) {
19
- return candidate;
20
- }
21
- }
22
- throw new Error(`${name} executable not found on PATH`);
23
- };
24
-
25
- const loadCli = async () => {
26
- return import('../../../src/contract/validate-funnel.cli');
27
- };
28
-
29
- const diagnostic = createFunnelContractDiagnostic({
30
- code: 'FG-PARITY-001',
31
- file: 'src/steps/example.tsx',
32
- stepId: 'example',
33
- expected: { name: 'example' },
34
- received: { name: 'renamed-example' },
35
- reason: 'Manifest and component metadata differ.',
36
- guide: 'docs/funnelsgrove/contracts/step-metadata.md',
37
- repair: 'Restore exact metadata parity.',
38
- });
39
-
40
- type RunResult = {
41
- exitCode: number;
42
- stdout: string;
43
- stderr: string;
44
- };
45
-
46
- const runWith = async (
47
- diagnostics: readonly FunnelContractDiagnostic[],
48
- ): Promise<RunResult> => {
49
- const cli = await loadCli();
50
- let stdout = '';
51
- let stderr = '';
52
- const exitCode = await cli.runValidateFunnelCli({
53
- args: ['--json'],
54
- loadValidation: async () => ({
55
- contractVersion: cli.VALIDATOR_CONTRACT_VERSION,
56
- diagnostics,
57
- }),
58
- writeStdout: (text: string) => {
59
- stdout += text;
60
- },
61
- writeStderr: (text: string) => {
62
- stderr += text;
63
- },
64
- });
65
- return { exitCode, stdout, stderr };
66
- };
67
-
68
- describe('validate funnel CLI protocol', () => {
69
- it('writes exactly one JSON object and exits zero for a valid project', async () => {
70
- const result = await runWith([]);
71
-
72
- expect(result).toEqual({
73
- exitCode: 0,
74
- stdout: `${JSON.stringify({
75
- schemaVersion: 1,
76
- contractVersion: CURRENT_STEP_CONTRACT_VERSION,
77
- valid: true,
78
- diagnostics: [],
79
- })}\n`,
80
- stderr: '',
81
- });
82
- expect(result.stdout.trim().split('\n')).toHaveLength(1);
83
- });
84
-
85
- it('preserves blocker diagnostics exactly and renders human detail to stderr', async () => {
86
- const result = await runWith([diagnostic]);
87
-
88
- expect(result.exitCode).toBe(1);
89
- expect(JSON.parse(result.stdout)).toEqual({
90
- schemaVersion: 1,
91
- contractVersion: CURRENT_STEP_CONTRACT_VERSION,
92
- valid: false,
93
- diagnostics: [diagnostic],
94
- });
95
- expect(result.stderr).toContain('[FG-PARITY-001]');
96
- expect(result.stderr).toContain('Manifest and component metadata differ.');
97
- expect(result.stderr).toContain('src/steps/example.tsx');
98
- });
99
-
100
- it('keeps import log noise off stdout and maps malformed imports to exit two', async () => {
101
- const cli = await loadCli();
102
- let stdout = '';
103
- let stderr = '';
104
- const exitCode = await cli.runValidateFunnelCli({
105
- args: ['--json'],
106
- loadValidation: async () => {
107
- process.stdout.write('project import noise\n');
108
- throw new Error('malformed project import');
109
- },
110
- writeStdout: (text: string) => {
111
- stdout += text;
112
- },
113
- writeStderr: (text: string) => {
114
- stderr += text;
115
- },
116
- });
117
-
118
- expect(exitCode).toBe(2);
119
- expect(stdout).not.toContain('project import noise');
120
- expect(stdout.trim().split('\n')).toHaveLength(1);
121
- expect(JSON.parse(stdout)).toMatchObject({
122
- schemaVersion: 1,
123
- contractVersion: CURRENT_STEP_CONTRACT_VERSION,
124
- valid: false,
125
- diagnostics: [{
126
- code: 'FG-CLIENT-001',
127
- severity: 'error',
128
- }],
129
- });
130
- expect(stderr).toContain('project import noise');
131
- expect(stderr).toContain('malformed project import');
132
- });
133
-
134
- it('rejects unsupported arguments as a protocol error without running imports', async () => {
135
- const cli = await loadCli();
136
- let loaded = false;
137
- let stdout = '';
138
- let stderr = '';
139
- const exitCode = await cli.runValidateFunnelCli({
140
- args: ['--json', '--unknown'],
141
- loadValidation: async () => {
142
- loaded = true;
143
- return { contractVersion: CURRENT_STEP_CONTRACT_VERSION, diagnostics: [] };
144
- },
145
- writeStdout: (text: string) => {
146
- stdout += text;
147
- },
148
- writeStderr: (text: string) => {
149
- stderr += text;
150
- },
151
- });
152
-
153
- expect(exitCode).toBe(2);
154
- expect(loaded).toBe(false);
155
- expect(JSON.parse(stdout)).toMatchObject({
156
- schemaVersion: 1,
157
- contractVersion: CURRENT_STEP_CONTRACT_VERSION,
158
- valid: false,
159
- });
160
- expect(stderr).toContain('Usage: validate-funnel.cli.ts [--json]');
161
- });
162
-
163
- it('runs through vite-node with one clean stdout protocol object', async () => {
164
- const templateRoot = path.resolve(fileURLToPath(new URL('../../..', import.meta.url)));
165
- const viteNode = findExecutableOnPath('vite-node');
166
- const script = path.resolve(templateRoot, 'src/contract/validate-funnel.cli.ts');
167
-
168
- const result = await execFileAsync(viteNode, [
169
- '--config',
170
- 'src/contract/funnel-validator.vite.config.ts',
171
- script,
172
- '--json',
173
- ], {
174
- cwd: templateRoot,
175
- env: {
176
- ...process.env,
177
- VITE_CJS_IGNORE_WARNING: 'true',
178
- },
179
- timeout: 20_000,
180
- });
181
-
182
- expect(result.stderr).toBe('');
183
- expect(result.stdout.trim().split('\n')).toHaveLength(1);
184
- expect(JSON.parse(result.stdout)).toEqual({
185
- schemaVersion: 1,
186
- contractVersion: CURRENT_STEP_CONTRACT_VERSION,
187
- valid: true,
188
- diagnostics: [],
189
- });
190
- });
191
- });
@@ -1,60 +0,0 @@
1
- import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2
-
3
- const ORIGINAL_ENV = { ...process.env };
4
-
5
- const resetClaimbeeCheckoutEnv = () => {
6
- process.env = { ...ORIGINAL_ENV };
7
- delete process.env.NEXT_PUBLIC_FUNNEL_PUBLISHABLE_KEY;
8
- delete process.env.NEXT_PUBLIC_CLAIMBEE_TEST_STRIPE_PUBLISHABLE_KEY;
9
- delete process.env.NEXT_PUBLIC_STRIPE_TEST_PUBLISHABLE_KEY;
10
- };
11
-
12
- describe('checkoutRuntimeConfig', () => {
13
- beforeEach(() => {
14
- vi.resetModules();
15
- resetClaimbeeCheckoutEnv();
16
- });
17
-
18
- afterEach(() => {
19
- vi.resetModules();
20
- process.env = { ...ORIGINAL_ENV };
21
- });
22
-
23
- it('does not invent a checkout SDK publishable key from generic Stripe env', async () => {
24
- process.env.NEXT_PUBLIC_STRIPE_TEST_PUBLISHABLE_KEY = 'pk_test_generic_runtime_value';
25
-
26
- const { checkoutRuntimeConfig } = await import('../../../src/runtime/checkout-runtime-config');
27
-
28
- expect(checkoutRuntimeConfig.funnelSdkPublishableKey).toBeNull();
29
- });
30
-
31
- it('prefers the funnel sdk publishable key when the project runtime provides one', async () => {
32
- vi.doMock('@funnelsgrove/runtime', async () => {
33
- const actual = await vi.importActual<typeof import('@funnelsgrove/runtime')>(
34
- '@funnelsgrove/runtime',
35
- );
36
-
37
- return {
38
- ...actual,
39
- runtimePublicConfig: {
40
- ...actual.runtimePublicConfig,
41
- funnelSdkPublishableKey: 'sdk_pub_claimbee_project',
42
- },
43
- };
44
- });
45
-
46
- const { checkoutRuntimeConfig, resolveClaimbeeCheckoutMode } = await import('../../../src/runtime/checkout-runtime-config');
47
-
48
- expect(checkoutRuntimeConfig.funnelSdkPublishableKey).toBe(
49
- 'sdk_pub_claimbee_project',
50
- );
51
- expect(resolveClaimbeeCheckoutMode('live')).toBe('live');
52
- });
53
-
54
- it('uses the server-provided runtime mode when no preview key is injected', async () => {
55
- const { resolveClaimbeeCheckoutMode } = await import('../../../src/runtime/checkout-runtime-config');
56
-
57
- expect(resolveClaimbeeCheckoutMode('live')).toBe('live');
58
- expect(resolveClaimbeeCheckoutMode('test')).toBe('test');
59
- });
60
- });
@@ -1,25 +0,0 @@
1
- import { describe, expect, it } from 'vitest';
2
- import { funnelManifest } from '@/config/funnel.manifest';
3
- import {
4
- defaultFunnelStepId,
5
- resolveConfiguredNextStep,
6
- resolveRuntimeInitialStepId,
7
- } from '../../../src/runtime/funnel-runtime';
8
-
9
- describe('funnel runtime', () => {
10
- it('falls back to the first manifest step when no valid step is requested', () => {
11
- expect(funnelManifest.steps[0]?.id).toBe(defaultFunnelStepId);
12
- expect(resolveRuntimeInitialStepId({ requestedStepId: null })).toBe(defaultFunnelStepId);
13
- expect(resolveRuntimeInitialStepId({ requestedStepId: 'missing-step' })).toBe(defaultFunnelStepId);
14
- });
15
-
16
- it('resolves the normal edge after an experiment source route is active', () => {
17
- expect(
18
- resolveConfiguredNextStep('paywall', {
19
- attributes: {},
20
- ignoreExperiment: true,
21
- resolveExperimentVariantKey: () => 'control',
22
- }),
23
- ).toBe('subscription-started');
24
- });
25
- });
@@ -1,38 +0,0 @@
1
- import { describe, expect, it } from 'vitest';
2
- import {
3
- getStepContentLocale,
4
- getStepCountryCode,
5
- } from '../../../src/runtime/step-content-context';
6
-
7
- describe('step content context helpers', () => {
8
- it('prefers stored browser language and explicit country when present', () => {
9
- expect(
10
- getStepContentLocale(
11
- {
12
- browserLanguage: 'pt-PT',
13
- language: 'en-US',
14
- },
15
- 'de-DE',
16
- ),
17
- ).toBe('pt-PT');
18
-
19
- expect(
20
- getStepCountryCode(
21
- {
22
- country: 'pt',
23
- },
24
- 'en-US',
25
- ),
26
- ).toBe('PT');
27
- });
28
-
29
- it('falls back to the runtime locale and derives the region when no country is stored', () => {
30
- expect(getStepContentLocale({}, 'en-US')).toBe('en-US');
31
- expect(getStepCountryCode({}, 'en-US')).toBe('US');
32
- });
33
-
34
- it('returns null when no locale or country signal is available', () => {
35
- expect(getStepContentLocale({}, null)).toBeNull();
36
- expect(getStepCountryCode({}, null)).toBeNull();
37
- });
38
- });