@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,203 +0,0 @@
1
- import { existsSync, readFileSync } from 'node:fs';
2
- import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
3
-
4
- const { posthogFlagsPath } = vi.hoisted(() => ({
5
- posthogFlagsPath: new URL(
6
- '../../../../funnel-runtime/src/runtime/posthog-flags.ts',
7
- import.meta.url,
8
- ).pathname,
9
- }));
10
-
11
- // This suite mocks the runtime's internal posthog-flags module by source path,
12
- // which only exists inside the funnelsgrove monorepo. Standalone funnels
13
- // scaffolded from the template resolve @funnelsgrove/runtime from node_modules
14
- // and cover the same routing logic through use-funnel-flow-controller.test.ts.
15
- const insideMonorepo = existsSync(posthogFlagsPath);
16
-
17
- vi.mock(posthogFlagsPath, () => ({
18
- resolveExperimentVariant: vi.fn(),
19
- bootstrapPostHog: vi.fn(),
20
- isPostHogReady: vi.fn(() => true),
21
- identifyPostHog: vi.fn(),
22
- getPostHog: vi.fn(() => null),
23
- }));
24
-
25
- import type {
26
- FunnelManifest,
27
- FunnelManifestExperiment,
28
- } from '@funnelsgrove/runtime';
29
- import {
30
- resolveExperimentAssignment,
31
- resolveExperimentVariant,
32
- resolveNextStepId,
33
- } from '@funnelsgrove/runtime';
34
- import { computeRenderSuspended } from '../../../src/runtime/use-funnel-flow-controller';
35
-
36
- const mockedResolveVariant = vi.mocked(resolveExperimentVariant);
37
-
38
- const heroExperiment: FunnelManifestExperiment = {
39
- experimentId: 'exp_hero',
40
- stepId: 'hero',
41
- variants: [
42
- { variantKey: 'control', routeToStepId: 'hero_v1' },
43
- { variantKey: 'variant_a', routeToStepId: 'hero_v2' },
44
- ],
45
- };
46
-
47
- const manifest: FunnelManifest = {
48
- templateArchitectureVersion: 1,
49
- stepContractVersion: 2,
50
- meta: { title: 'Hero Experiment Funnel', description: 'integration fixture' },
51
- viewport: { width: 390, height: 844 },
52
- assets: {},
53
- steps: [
54
- {
55
- id: 'hero',
56
- name: 'hero',
57
- path: '/hero',
58
- filePath: 'src/steps/hero.tsx',
59
- componentKey: 'hero',
60
- type: 'intro_hero',
61
- title: 'Hero',
62
- },
63
- {
64
- id: 'hero_v1',
65
- name: 'hero-v1',
66
- path: '/hero-v1',
67
- filePath: 'src/steps/hero-v1.tsx',
68
- componentKey: 'heroV1',
69
- type: 'intro_hero',
70
- title: 'Hero v1',
71
- },
72
- {
73
- id: 'hero_v2',
74
- name: 'hero-v2',
75
- path: '/hero-v2',
76
- filePath: 'src/steps/hero-v2.tsx',
77
- componentKey: 'heroV2',
78
- type: 'intro_hero',
79
- title: 'Hero v2',
80
- },
81
- ],
82
- edgesByStepId: {
83
- hero: [{ toStepId: 'hero_v1' }],
84
- },
85
- experiments: [heroExperiment],
86
- };
87
-
88
- // The controller's renderedStepId is derived by passing the active step's
89
- // experiment through resolveExperimentAssignment and routing to the matched
90
- // variant's routeToStepId (falling back to control). That is exactly the
91
- // function under test here. See apps/funnel-template/src/runtime/use-funnel-flow-controller.ts
92
- // (renderedStepId useMemo).
93
- const computeRenderedStepIdForActiveExperiment = (
94
- experiment: FunnelManifestExperiment,
95
- ): string => {
96
- const assignment = resolveExperimentAssignment({
97
- experimentId: experiment.experimentId,
98
- variants: experiment.variants,
99
- });
100
- return assignment?.routeToStepId ?? experiment.stepId;
101
- };
102
-
103
- describe.skipIf(!insideMonorepo)('PostHog variant resolution (integration)', () => {
104
- beforeEach(() => {
105
- mockedResolveVariant.mockReset();
106
- });
107
-
108
- afterEach(() => {
109
- vi.clearAllMocks();
110
- });
111
-
112
- it('routes to the variant_a target when PostHog resolves exp_hero to variant_a', () => {
113
- mockedResolveVariant.mockReturnValue('variant_a');
114
-
115
- expect(computeRenderedStepIdForActiveExperiment(heroExperiment)).toBe('hero_v2');
116
-
117
- expect(
118
- resolveNextStepId({
119
- manifest,
120
- currentStepId: 'hero',
121
- attributes: {},
122
- resolveExperimentVariantKey: (experiment) =>
123
- resolveExperimentVariant(experiment.experimentId) ?? null,
124
- }),
125
- ).toBe('hero_v2');
126
- });
127
-
128
- it('falls back to control (hero_v1) when PostHog is not ready / returns undefined', () => {
129
- mockedResolveVariant.mockReturnValue(undefined);
130
-
131
- expect(computeRenderedStepIdForActiveExperiment(heroExperiment)).toBe('hero_v1');
132
-
133
- expect(
134
- resolveNextStepId({
135
- manifest,
136
- currentStepId: 'hero',
137
- attributes: {},
138
- resolveExperimentVariantKey: (experiment) =>
139
- resolveExperimentVariant(experiment.experimentId) ?? null,
140
- }),
141
- ).toBe('hero_v1');
142
- });
143
-
144
- it('falls back to control (hero_v1) when PostHog resolves to an unknown variant key', () => {
145
- mockedResolveVariant.mockReturnValue('phantom');
146
-
147
- expect(computeRenderedStepIdForActiveExperiment(heroExperiment)).toBe('hero_v1');
148
-
149
- expect(
150
- resolveNextStepId({
151
- manifest,
152
- currentStepId: 'hero',
153
- attributes: {},
154
- resolveExperimentVariantKey: (experiment) =>
155
- resolveExperimentVariant(experiment.experimentId) ?? null,
156
- }),
157
- ).toBe('hero_v1');
158
- });
159
-
160
- it('suspends render on an experiment step while PostHog is not ready', () => {
161
- expect(
162
- computeRenderSuspended({
163
- activeStepId: 'hero',
164
- postHogReady: false,
165
- experiments: manifest.experiments,
166
- }),
167
- ).toBe(true);
168
-
169
- expect(
170
- computeRenderSuspended({
171
- activeStepId: 'hero',
172
- postHogReady: true,
173
- experiments: manifest.experiments,
174
- }),
175
- ).toBe(false);
176
- });
177
-
178
- it('does not suspend render on an experiment step in preview runtime', () => {
179
- expect(
180
- computeRenderSuspended({
181
- activeStepId: 'hero',
182
- postHogReady: false,
183
- experiments: manifest.experiments,
184
- isPreviewRuntime: true,
185
- }),
186
- ).toBe(false);
187
- });
188
-
189
- it('renders experiment variants on the source URL before normal continuation', () => {
190
- const adapterSource = readFileSync(new URL('../../../src/runtime/use-funnel-flow-controller.ts', import.meta.url), 'utf8');
191
- const runtimeSource = readFileSync(
192
- new URL('../../../../funnel-runtime/src/runtime/use-funnel-flow-controller.ts', import.meta.url),
193
- 'utf8',
194
- );
195
-
196
- expect(adapterSource).toContain('useFunnelFlowController as useSharedFunnelFlowController');
197
- expect(runtimeSource).toContain('activeExperimentForStep');
198
- expect(runtimeSource).not.toContain('goToStep(renderedStepId)');
199
- expect(runtimeSource).toContain('goToStep(resolveNextStepId(renderedStepId');
200
- expect(runtimeSource).toContain('ignoreExperiment: Boolean(activeExperimentForStep && renderedStepId === safeActiveStepId)');
201
- expect(runtimeSource).toContain('if (prevId === renderedStepId)');
202
- });
203
- });
@@ -1,76 +0,0 @@
1
- import { describe, expect, it } from 'vitest';
2
- import { readFileSync } from 'node:fs';
3
- import { computeRenderSuspended } from '../../../src/runtime/use-funnel-flow-controller';
4
-
5
- describe('computeRenderSuspended', () => {
6
- const experiments = [
7
- { stepId: 'step-paywall' },
8
- { stepId: 'step-headline' },
9
- ];
10
-
11
- it('renderSuspended is true when active step has experiment and posthog is not ready', () => {
12
- expect(
13
- computeRenderSuspended({
14
- activeStepId: 'step-paywall',
15
- postHogReady: false,
16
- experiments,
17
- }),
18
- ).toBe(true);
19
- });
20
-
21
- it('renderSuspended is false when active step has no experiment', () => {
22
- expect(
23
- computeRenderSuspended({
24
- activeStepId: 'step-intro',
25
- postHogReady: false,
26
- experiments,
27
- }),
28
- ).toBe(false);
29
- });
30
-
31
- it('renderSuspended is false after posthog becomes ready', () => {
32
- expect(
33
- computeRenderSuspended({
34
- activeStepId: 'step-paywall',
35
- postHogReady: true,
36
- experiments,
37
- }),
38
- ).toBe(false);
39
- });
40
-
41
- it('renderSuspended is false in preview runtime even when posthog is not ready', () => {
42
- expect(
43
- computeRenderSuspended({
44
- activeStepId: 'step-paywall',
45
- postHogReady: false,
46
- experiments,
47
- isPreviewRuntime: true,
48
- }),
49
- ).toBe(false);
50
- });
51
- });
52
-
53
- describe('qualified visitor analytics', () => {
54
- it('delegates flow behavior to the shared runtime controller with template analytics', () => {
55
- const source = readFileSync(new URL('../../../src/runtime/use-funnel-flow-controller.ts', import.meta.url), 'utf8');
56
-
57
- expect(source).toContain('useFunnelFlowController as useSharedFunnelFlowController');
58
- expect(source).toContain('return useSharedFunnelFlowController<FunnelStepId>({');
59
- expect(source).toContain('analytics: publicAnalyticsSdk');
60
- expect(source).toContain('stepContractVersion: funnelManifest.stepContractVersion');
61
- expect(source).toContain('defaultStepId: defaultFunnelStepId');
62
- expect(source).toContain('stepSequence: funnelStepSequence');
63
- expect(source).toContain('stepById');
64
- expect(source).toContain('stepComponentById');
65
- expect(source).toContain('funnelExperiments');
66
- });
67
-
68
- it('does not carry a local copy of shared controller internals', () => {
69
- const source = readFileSync(new URL('../../../src/runtime/use-funnel-flow-controller.ts', import.meta.url), 'utf8');
70
-
71
- expect(source).not.toContain('FIRST_STEP_ENGAGEMENT_THRESHOLD_MS');
72
- expect(source).not.toContain('apiService.bootstrapSession');
73
- expect(source).not.toContain('apiService.pingUserContext');
74
- expect(source).not.toContain('window.setTimeout');
75
- });
76
- });
@@ -1,104 +0,0 @@
1
- import { describe, expect, it } from 'vitest';
2
- import {
3
- buildPaywallCheckoutBillingDetails,
4
- buildPaywallCheckoutExpressOptions,
5
- buildPaywallCheckoutPaymentElementOptions,
6
- } from '../../../src/steps/paywall-checkout-options';
7
-
8
- describe('paywall checkout options', () => {
9
- it('prioritizes Link and stacked express checkout buttons for the checkout dialog', () => {
10
- const options = buildPaywallCheckoutExpressOptions({
11
- amountCents: 3599,
12
- summaryLabel: 'ClaimBee Pro · 3 Months',
13
- });
14
-
15
- expect(options.paymentMethodOrder).toEqual(['link', 'apple_pay', 'google_pay']);
16
- expect(options.buttonHeight).toBe(55);
17
- expect(options.layout).toEqual({
18
- maxColumns: 1,
19
- maxRows: 0,
20
- overflow: 'never',
21
- });
22
- expect(options.paymentMethods).toMatchObject({
23
- applePay: 'always',
24
- googlePay: 'always',
25
- amazonPay: 'never',
26
- klarna: 'never',
27
- paypal: 'never',
28
- });
29
- expect(options.lineItems).toEqual([
30
- {
31
- name: 'ClaimBee Pro · 3 Months',
32
- amount: 3599,
33
- },
34
- ]);
35
- });
36
-
37
- it('keeps the card form focused on manual card entry while prefilling the known email', () => {
38
- const options = buildPaywallCheckoutPaymentElementOptions({
39
- email: 'jane@example.com',
40
- });
41
-
42
- expect(options.paymentMethodOrder).toEqual(['card']);
43
- expect(options.wallets).toEqual({
44
- applePay: 'never',
45
- googlePay: 'never',
46
- link: 'never',
47
- });
48
- expect(options.fields).toEqual({
49
- billingDetails: {
50
- address: 'if_required',
51
- email: 'never',
52
- name: 'never',
53
- phone: 'auto',
54
- },
55
- });
56
- expect(options.defaultValues).toEqual({
57
- billingDetails: {
58
- email: 'jane@example.com',
59
- },
60
- });
61
- expect(options.layout).toEqual({
62
- type: 'tabs',
63
- });
64
- });
65
-
66
- it('shows the email field when the funnel has not collected one yet', () => {
67
- const options = buildPaywallCheckoutPaymentElementOptions({});
68
-
69
- expect(options.fields).toEqual({
70
- billingDetails: {
71
- address: 'if_required',
72
- email: 'auto',
73
- name: 'never',
74
- phone: 'auto',
75
- },
76
- });
77
- expect(options.defaultValues).toBeUndefined();
78
- });
79
-
80
- it('supplies hidden billing details when the checkout form omits the name field', () => {
81
- expect(
82
- buildPaywallCheckoutBillingDetails({
83
- email: 'jane.doe@example.com',
84
- name: 'Jane Doe',
85
- }),
86
- ).toEqual({
87
- email: 'jane.doe@example.com',
88
- name: 'Jane Doe',
89
- });
90
-
91
- expect(
92
- buildPaywallCheckoutBillingDetails({
93
- email: 'jane.doe@example.com',
94
- }),
95
- ).toEqual({
96
- email: 'jane.doe@example.com',
97
- name: 'jane doe',
98
- });
99
-
100
- expect(buildPaywallCheckoutBillingDetails({})).toEqual({
101
- name: 'Customer',
102
- });
103
- });
104
- });
@@ -1,29 +0,0 @@
1
- import { readFileSync } from 'node:fs';
2
- import path from 'node:path';
3
- import { describe, expect, it } from 'vitest';
4
-
5
- const sourceStepsDirectory = path.resolve(__dirname, '../../../src/steps');
6
-
7
- const readStepSource = (fileName: string): string =>
8
- readFileSync(path.resolve(sourceStepsDirectory, fileName), 'utf8');
9
-
10
- describe('template shared header contract', () => {
11
- it('renders template step headers through the shared FunnelStepHeader component', () => {
12
- const sharedHeaderSource = readStepSource('../components/step/FunnelStepHeader.tsx');
13
-
14
- expect(sharedHeaderSource).toContain('export type FunnelStepHeaderProps');
15
- expect(sharedHeaderSource).toContain('export function FunnelStepHeader');
16
-
17
- const headerStepFiles = [
18
- 'step-01.tsx',
19
- 'step-02.tsx',
20
- 'step-04.tsx',
21
- ];
22
-
23
- headerStepFiles.forEach((fileName) => {
24
- const stepSource = readStepSource(fileName);
25
- expect(stepSource).toContain("from '@/components/step/FunnelStepHeader'");
26
- expect(stepSource).toContain('<FunnelStepHeader');
27
- });
28
- });
29
- });
@@ -1,38 +0,0 @@
1
- import { describe, expect, it } from 'vitest';
2
- import { stepOneContent } from '@/steps/content/step-01.content';
3
- import {
4
- resolveStepOneArtworkSrc,
5
- resolveStepOneImportedAssetSrc,
6
- } from '@/steps/step-01';
7
-
8
- describe('StepOne helpers', () => {
9
- it('falls back to the bundled sprite when configured artwork failed to load', () => {
10
- expect(
11
- resolveStepOneArtworkSrc(
12
- {
13
- src: 'https://example.com/missing-step-one.png',
14
- alt: 'Broken step one artwork',
15
- },
16
- '/claimbee/steps-sprite-default.png',
17
- true,
18
- ),
19
- ).toBe('/claimbee/steps-sprite-default.png');
20
- });
21
-
22
- it('normalizes imported sprite assets from both preview and Next runtimes', () => {
23
- expect(resolveStepOneImportedAssetSrc('/claimbee/steps-sprite-default.png')).toBe(
24
- '/claimbee/steps-sprite-default.png',
25
- );
26
- expect(resolveStepOneImportedAssetSrc({ src: '/claimbee/steps-sprite-default.png' })).toBe(
27
- '/claimbee/steps-sprite-default.png',
28
- );
29
- });
30
-
31
- it('ships ClaimBee-style default headline copy', () => {
32
- expect(stepOneContent.locales.en.headline).toEqual([
33
- 'People getting',
34
- 'free money',
35
- 'know one secret',
36
- ]);
37
- });
38
- });
@@ -1,43 +0,0 @@
1
- import { readFileSync } from 'node:fs';
2
- import path from 'node:path';
3
- import { describe, expect, it } from 'vitest';
4
- import { stepTwoContent, stepTwoOptions } from '../../../src/steps/content/step-02.content';
5
- import { stepTwo } from '../../../src/steps/step-02';
6
-
7
- const stepSource = readFileSync(path.resolve(__dirname, '../../../src/steps/step-02.tsx'), 'utf8');
8
-
9
- describe('StepTwo', () => {
10
- it('declares its typed multi-select answer contract and immutable option IDs', () => {
11
- expect(stepTwo.choice).toEqual({
12
- answerKey: 'selectedDevices',
13
- allowEmpty: true,
14
- });
15
- expect(stepTwoOptions.map((option) => option.id)).toEqual([
16
- 'iphone',
17
- 'laptop',
18
- 'watch',
19
- 'none',
20
- ]);
21
- expect(Object.isFrozen(stepTwoOptions)).toBe(true);
22
- expect(stepTwoOptions.every((option) => Object.isFrozen(option))).toBe(true);
23
- });
24
-
25
- it('owns explicit typed completion instead of writing choices continuously', () => {
26
- expect(stepTwo.actionBar).toEqual({ hidden: true });
27
- expect(stepTwoContent.locales.en.continueLabel).toBe('Continue');
28
- expect(stepSource).toContain('useStepChoices(stepTwo, stepTwoOptions)');
29
- expect(stepSource).toContain('<PrimaryButton');
30
- expect(stepSource).toContain('{content.continueLabel}');
31
- expect(stepSource).toContain('completeMulti(selectedIds)');
32
- expect(stepSource).not.toContain('useEffect');
33
- expect(stepSource).not.toContain('setAnswer');
34
- });
35
-
36
- it('exposes each native choice button and its selected state to assistive technology', () => {
37
- expect(stepSource).toContain('role="group"');
38
- expect(stepSource).not.toContain('role="list"');
39
- expect(stepSource).not.toContain('role="listitem"');
40
- expect(stepSource).toContain('aria-pressed={isSelected}');
41
- expect(stepSource).toContain('aria-hidden="true"');
42
- });
43
- });
@@ -1,15 +0,0 @@
1
- import { describe, expect, it } from 'vitest';
2
- import { resolveStepThreeAutoAdvanceMs, stepThree } from '../../../src/steps/step-03';
3
-
4
- describe('StepThree', () => {
5
- it('keeps the shell action bar hidden because the step owns its own auto-advance behavior', () => {
6
- expect(stepThree.actionBar).toEqual({
7
- hidden: true,
8
- });
9
- });
10
-
11
- it('disables auto-advance when the funnel is rendered inside builder preview', () => {
12
- expect(resolveStepThreeAutoAdvanceMs(true)).toBeNull();
13
- expect(resolveStepThreeAutoAdvanceMs(false)).toBe(2200);
14
- });
15
- });
@@ -1,32 +0,0 @@
1
- import { readFileSync } from 'node:fs';
2
- import path from 'node:path';
3
- import { describe, expect, it } from 'vitest';
4
- import { stepFourContent } from '@/steps/content/step-04.content';
5
-
6
- describe('StepFour scratchpad', () => {
7
- it('stores scratchpad and reward copy as structured editable content', () => {
8
- expect(stepFourContent.locales.en.header.title).toEqual([
9
- 'Scratch & Unlock',
10
- 'Your Claim Advantage',
11
- ]);
12
- expect(stepFourContent.locales.en.ticket.offerLabel).toBe('50% OFF');
13
- expect(stepFourContent.locales.en.reward.ctaLabel).toBe('Claim Discount →');
14
- });
15
-
16
- it('reveals the scratch ticket before advancing through the manifest edge', () => {
17
- const stepSource = readFileSync(path.resolve(__dirname, '../../../src/steps/step-04.tsx'), 'utf8');
18
-
19
- expect(stepSource).toContain("data-node-id='7390:1456'");
20
- expect(stepSource).toContain("data-node-id='6992:6168'");
21
- expect(stepSource).toContain('scratchThreshold');
22
- expect(stepSource).not.toContain("from '@funnelsgrove/analytics'");
23
- expect(stepSource).not.toContain('publicAnalyticsSdk');
24
- expect(stepSource).not.toContain('trackFunnelCompleted');
25
- expect(stepSource).not.toContain('trackRegistrationCompleted');
26
- expect(stepSource).not.toContain('.flush()');
27
- // Manifest edges own routing (step-4 -> email-capture -> paywall);
28
- // the CTA must not hardcode a jump past email capture.
29
- expect(stepSource).toContain('goNext()');
30
- expect(stepSource).not.toContain('goToStep(stepPaywallId)');
31
- });
32
- });
@@ -1,22 +0,0 @@
1
- import { readFileSync } from 'node:fs';
2
- import { describe, expect, it } from 'vitest';
3
-
4
- describe('email capture workflow ownership', () => {
5
- it('only validates UI state and calls the shared context helper', () => {
6
- const source = readFileSync(new URL('../../../src/steps/step-31-email-capture.tsx', import.meta.url), 'utf8');
7
-
8
- expect(source).toContain('submitEmailCapture } = useFunnel();');
9
- expect(source).toContain('await submitEmailCapture(email);');
10
- expect(source.match(/submitEmailCapture\(email\)/g)).toHaveLength(1);
11
-
12
- expect(source).not.toContain('apiService');
13
- expect(source).not.toContain('publicAnalyticsSdk');
14
- expect(source).not.toContain('trackLead');
15
- expect(source).not.toContain('trackEmailCaptured');
16
- expect(source).not.toContain('metaPixel');
17
- expect(source).not.toContain('googleTag');
18
- expect(source).not.toContain('stepPaywallId');
19
- expect(source).not.toContain('goToStep');
20
- expect(source).not.toContain('/sdk/public/users/email-capture');
21
- });
22
- });