@antelopejs/dms-marketing 0.3.3 → 0.4.1

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 (289) hide show
  1. package/README.md +18 -15
  2. package/dist/config.d.ts +1 -0
  3. package/dist/config.js +4 -0
  4. package/dist/config.js.map +1 -1
  5. package/dist/data-api/funnels.d.ts +2 -0
  6. package/dist/data-api/funnels.js +8 -1
  7. package/dist/data-api/funnels.js.map +1 -1
  8. package/dist/db/models/funnels.model.d.ts +2 -0
  9. package/dist/db/models/funnels.model.js +9 -0
  10. package/dist/db/models/funnels.model.js.map +1 -1
  11. package/dist/db/models/index.d.ts +1 -0
  12. package/dist/db/models/index.js +1 -0
  13. package/dist/db/models/index.js.map +1 -1
  14. package/dist/db/models/marketing_events.model.d.ts +22 -0
  15. package/dist/db/models/marketing_events.model.js +70 -5
  16. package/dist/db/models/marketing_events.model.js.map +1 -1
  17. package/dist/db/models/marketing_preferences.model.d.ts +25 -0
  18. package/dist/db/models/marketing_preferences.model.js +29 -0
  19. package/dist/db/models/marketing_preferences.model.js.map +1 -0
  20. package/dist/db/models/marketing_sessions.model.d.ts +2 -1
  21. package/dist/db/models/marketing_sessions.model.js +11 -1
  22. package/dist/db/models/marketing_sessions.model.js.map +1 -1
  23. package/dist/db/models/page_snapshots.model.d.ts +2 -1
  24. package/dist/db/models/page_snapshots.model.js +7 -7
  25. package/dist/db/models/page_snapshots.model.js.map +1 -1
  26. package/dist/db/models/website_statistics.model.d.ts +4 -1
  27. package/dist/db/models/website_statistics.model.js +15 -1
  28. package/dist/db/models/website_statistics.model.js.map +1 -1
  29. package/dist/db/tables/index.d.ts +1 -0
  30. package/dist/db/tables/index.js +1 -0
  31. package/dist/db/tables/index.js.map +1 -1
  32. package/dist/db/tables/marketing_preferences.table.d.ts +6 -0
  33. package/dist/db/tables/marketing_preferences.table.js +31 -0
  34. package/dist/db/tables/marketing_preferences.table.js.map +1 -0
  35. package/dist/hooks/index.js +8 -4
  36. package/dist/hooks/index.js.map +1 -1
  37. package/dist/index.js +18 -3
  38. package/dist/index.js.map +1 -1
  39. package/dist/pages/acquisition/page.d.ts +9 -0
  40. package/dist/pages/acquisition/page.js +181 -0
  41. package/dist/pages/acquisition/page.js.map +1 -0
  42. package/dist/pages/all.d.ts +5 -1
  43. package/dist/pages/all.js +5 -1
  44. package/dist/pages/all.js.map +1 -1
  45. package/dist/pages/blocks.d.ts +18 -0
  46. package/dist/pages/blocks.js +56 -0
  47. package/dist/pages/blocks.js.map +1 -0
  48. package/dist/pages/funnel/page.d.ts +9 -0
  49. package/dist/pages/funnel/page.js +36 -0
  50. package/dist/pages/funnel/page.js.map +1 -0
  51. package/dist/pages/funnel-builder/page.d.ts +9 -0
  52. package/dist/pages/funnel-builder/page.js +29 -0
  53. package/dist/pages/funnel-builder/page.js.map +1 -0
  54. package/dist/pages/funnels/page.d.ts +1 -1
  55. package/dist/pages/funnels/page.js +200 -3
  56. package/dist/pages/funnels/page.js.map +1 -1
  57. package/dist/pages/index.d.ts +1 -0
  58. package/dist/pages/index.js +1 -0
  59. package/dist/pages/index.js.map +1 -1
  60. package/dist/pages/{campaigns → install}/page.d.ts +3 -3
  61. package/dist/pages/{campaigns → install}/page.js +14 -12
  62. package/dist/pages/install/page.js.map +1 -0
  63. package/dist/pages/module.d.ts +3 -0
  64. package/dist/pages/module.js +30 -3
  65. package/dist/pages/module.js.map +1 -1
  66. package/dist/pages/overview/page.d.ts +1 -1
  67. package/dist/pages/overview/page.js +122 -3
  68. package/dist/pages/overview/page.js.map +1 -1
  69. package/dist/pages/pages/page.d.ts +1 -1
  70. package/dist/pages/pages/page.js +9 -4
  71. package/dist/pages/pages/page.js.map +1 -1
  72. package/dist/pages/quick-actions.d.ts +1 -0
  73. package/dist/pages/quick-actions.js +25 -0
  74. package/dist/pages/quick-actions.js.map +1 -0
  75. package/dist/pages/settings/form.d.ts +2 -1
  76. package/dist/pages/settings/form.js +62 -20
  77. package/dist/pages/settings/form.js.map +1 -1
  78. package/dist/pages/settings/page.d.ts +4 -1
  79. package/dist/pages/settings/page.js +50 -4
  80. package/dist/pages/settings/page.js.map +1 -1
  81. package/dist/pages/websites/page.d.ts +11 -0
  82. package/dist/pages/websites/page.js +62 -0
  83. package/dist/pages/websites/page.js.map +1 -0
  84. package/dist/routes/blocks.d.ts +50 -0
  85. package/dist/routes/blocks.js +269 -0
  86. package/dist/routes/blocks.js.map +1 -0
  87. package/dist/routes/collect.d.ts +1 -2
  88. package/dist/routes/collect.js +19 -12
  89. package/dist/routes/collect.js.map +1 -1
  90. package/dist/routes/context.d.ts +8 -0
  91. package/dist/routes/context.js +54 -0
  92. package/dist/routes/context.js.map +1 -0
  93. package/dist/routes/experiments.js +2 -1
  94. package/dist/routes/experiments.js.map +1 -1
  95. package/dist/routes/funnels.d.ts +21 -3
  96. package/dist/routes/funnels.js +150 -7
  97. package/dist/routes/funnels.js.map +1 -1
  98. package/dist/routes/index.d.ts +2 -0
  99. package/dist/routes/index.js +2 -0
  100. package/dist/routes/index.js.map +1 -1
  101. package/dist/routes/settings.d.ts +6 -1
  102. package/dist/routes/settings.js +65 -4
  103. package/dist/routes/settings.js.map +1 -1
  104. package/dist/routes/snapshot.d.ts +1 -1
  105. package/dist/routes/snapshot.js +24 -25
  106. package/dist/routes/snapshot.js.map +1 -1
  107. package/dist/routes/stats.d.ts +14 -23
  108. package/dist/routes/stats.js +73 -97
  109. package/dist/routes/stats.js.map +1 -1
  110. package/dist/routes/tracker.js +6 -4
  111. package/dist/routes/tracker.js.map +1 -1
  112. package/dist/routes/websites.d.ts +4 -0
  113. package/dist/routes/websites.js +111 -12
  114. package/dist/routes/websites.js.map +1 -1
  115. package/dist/services/acquisition-report.d.ts +62 -0
  116. package/dist/services/acquisition-report.js +181 -0
  117. package/dist/services/acquisition-report.js.map +1 -0
  118. package/dist/services/acquisition.d.ts +9 -3
  119. package/dist/services/acquisition.js +35 -5
  120. package/dist/services/acquisition.js.map +1 -1
  121. package/dist/services/catalog.d.ts +3 -0
  122. package/dist/services/catalog.js +17 -0
  123. package/dist/services/catalog.js.map +1 -0
  124. package/dist/services/collect.d.ts +1 -2
  125. package/dist/services/collect.js +49 -24
  126. package/dist/services/collect.js.map +1 -1
  127. package/dist/services/composed.d.ts +9 -0
  128. package/dist/services/composed.js +62 -0
  129. package/dist/services/composed.js.map +1 -0
  130. package/dist/services/context.d.ts +20 -0
  131. package/dist/services/context.js +76 -0
  132. package/dist/services/context.js.map +1 -0
  133. package/dist/services/dashboard.d.ts +74 -0
  134. package/dist/services/dashboard.js +343 -0
  135. package/dist/services/dashboard.js.map +1 -0
  136. package/dist/services/experiments/results.d.ts +12 -1
  137. package/dist/services/experiments/results.js +19 -2
  138. package/dist/services/experiments/results.js.map +1 -1
  139. package/dist/services/experiments/stats.d.ts +7 -1
  140. package/dist/services/experiments/stats.js +43 -5
  141. package/dist/services/experiments/stats.js.map +1 -1
  142. package/dist/services/funnel-report.d.ts +64 -0
  143. package/dist/services/funnel-report.js +280 -0
  144. package/dist/services/funnel-report.js.map +1 -0
  145. package/dist/services/funnels/compute.d.ts +2 -0
  146. package/dist/services/funnels/compute.js +25 -13
  147. package/dist/services/funnels/compute.js.map +1 -1
  148. package/dist/services/funnels/results.js +4 -1
  149. package/dist/services/funnels/results.js.map +1 -1
  150. package/dist/services/funnels/step-path.d.ts +4 -0
  151. package/dist/services/funnels/step-path.js +47 -0
  152. package/dist/services/funnels/step-path.js.map +1 -0
  153. package/dist/services/funnels-table.d.ts +31 -0
  154. package/dist/services/funnels-table.js +230 -0
  155. package/dist/services/funnels-table.js.map +1 -0
  156. package/dist/services/heatmap.d.ts +15 -3
  157. package/dist/services/heatmap.js +54 -11
  158. package/dist/services/heatmap.js.map +1 -1
  159. package/dist/services/hourly-traffic.d.ts +4 -0
  160. package/dist/services/hourly-traffic.js +35 -0
  161. package/dist/services/hourly-traffic.js.map +1 -0
  162. package/dist/services/origin-guard.d.ts +9 -1
  163. package/dist/services/origin-guard.js +46 -1
  164. package/dist/services/origin-guard.js.map +1 -1
  165. package/dist/services/period.d.ts +26 -1
  166. package/dist/services/period.js +99 -9
  167. package/dist/services/period.js.map +1 -1
  168. package/dist/services/prune.d.ts +6 -0
  169. package/dist/services/prune.js +35 -0
  170. package/dist/services/prune.js.map +1 -1
  171. package/dist/services/request-body.d.ts +1 -2
  172. package/dist/services/request-body.js +10 -36
  173. package/dist/services/request-body.js.map +1 -1
  174. package/dist/services/request-meta.d.ts +0 -1
  175. package/dist/services/request-meta.js +0 -11
  176. package/dist/services/request-meta.js.map +1 -1
  177. package/dist/services/rollups.d.ts +25 -0
  178. package/dist/services/rollups.js +102 -0
  179. package/dist/services/rollups.js.map +1 -0
  180. package/dist/services/sessionize.d.ts +5 -1
  181. package/dist/services/sessionize.js +52 -10
  182. package/dist/services/sessionize.js.map +1 -1
  183. package/dist/services/settings.d.ts +41 -2
  184. package/dist/services/settings.js +219 -6
  185. package/dist/services/settings.js.map +1 -1
  186. package/dist/services/snapshots.d.ts +3 -2
  187. package/dist/services/snapshots.js +110 -9
  188. package/dist/services/snapshots.js.map +1 -1
  189. package/dist/services/tenant-website.js +1 -0
  190. package/dist/services/tenant-website.js.map +1 -1
  191. package/dist/services/url.js +12 -4
  192. package/dist/services/url.js.map +1 -1
  193. package/dist/services/website-hosts.d.ts +15 -0
  194. package/dist/services/website-hosts.js +67 -0
  195. package/dist/services/website-hosts.js.map +1 -0
  196. package/dist/services/website-report.d.ts +40 -0
  197. package/dist/services/website-report.js +84 -0
  198. package/dist/services/website-report.js.map +1 -0
  199. package/dist/types/constants.d.ts +14 -2
  200. package/dist/types/constants.js +17 -6
  201. package/dist/types/constants.js.map +1 -1
  202. package/dist/types/events.d.ts +221 -68
  203. package/dist/types/events.js +60 -11
  204. package/dist/types/events.js.map +1 -1
  205. package/dist/types/snapshots.d.ts +6 -0
  206. package/dist/types/snapshots.js +2 -0
  207. package/dist/types/snapshots.js.map +1 -1
  208. package/dist/types/statistics.d.ts +2 -1
  209. package/dist/types/statistics.js +9 -2
  210. package/dist/types/statistics.js.map +1 -1
  211. package/dist/utils/experiment-type.js +1 -1
  212. package/dist/utils/experiment-type.js.map +1 -1
  213. package/dist/utils/funnel-steps-type.d.ts +21 -3
  214. package/dist/utils/funnel-steps-type.js +7 -2
  215. package/dist/utils/funnel-steps-type.js.map +1 -1
  216. package/frontend-vue/app/components/ChannelsCard.vue +265 -0
  217. package/frontend-vue/app/components/Context.vue +479 -0
  218. package/frontend-vue/app/components/ExperimentInput.vue +3 -1
  219. package/frontend-vue/app/components/ExperimentReport.vue +736 -0
  220. package/frontend-vue/app/components/FirstRun.vue +60 -0
  221. package/frontend-vue/app/components/FunnelBuilder.vue +1595 -0
  222. package/frontend-vue/app/components/FunnelFigure.vue +640 -0
  223. package/frontend-vue/app/components/FunnelReport.vue +532 -0
  224. package/frontend-vue/app/components/FunnelStepsInput.vue +3 -1
  225. package/frontend-vue/app/components/FunnelTemplates.vue +161 -0
  226. package/frontend-vue/app/components/HeatmapCanvas.vue +11 -4
  227. package/frontend-vue/app/components/InstallGuide.vue +753 -0
  228. package/frontend-vue/app/components/PagePreview.vue +889 -416
  229. package/frontend-vue/app/components/PagesExplorer.vue +552 -0
  230. package/frontend-vue/app/components/ScrollDepthOverlay.vue +48 -20
  231. package/frontend-vue/app/components/TopListTabs.vue +317 -0
  232. package/frontend-vue/app/components/TrackerSnippet.vue +50 -16
  233. package/frontend-vue/app/components/VariationsInput.vue +15 -5
  234. package/frontend-vue/app/components/WebsitesGrid.vue +799 -0
  235. package/frontend-vue/app/composables/useContextLink.ts +16 -0
  236. package/frontend-vue/app/composables/useExperiment.ts +1 -2
  237. package/frontend-vue/app/composables/useFunnelSteps.ts +1 -2
  238. package/frontend-vue/app/composables/useLatestRequest.ts +2 -4
  239. package/frontend-vue/app/composables/useMarketingApi.ts +298 -133
  240. package/frontend-vue/app/composables/useMarketingContext.ts +55 -0
  241. package/frontend-vue/app/composables/useMarketingRoutes.ts +80 -27
  242. package/frontend-vue/app/composables/useSnapshotFrame.ts +26 -4
  243. package/frontend-vue/app/constants.ts +22 -0
  244. package/frontend-vue/app/plugins/funnel-steps-data-type.client.ts +2 -1
  245. package/frontend-vue/app/utils/channels.ts +37 -0
  246. package/frontend-vue/app/utils/format.ts +132 -0
  247. package/frontend-vue/app/utils/funnel-draft.ts +264 -0
  248. package/frontend-vue/app/utils/heatmap.ts +82 -0
  249. package/frontend-vue/app/utils/heatmapPalette.ts +10 -0
  250. package/frontend-vue/app/utils/host-conflict.ts +48 -0
  251. package/frontend-vue/app/utils/sample-size.ts +17 -0
  252. package/frontend-vue/app/utils/snapshotViewportUnits.ts +15 -5
  253. package/frontend-vue/app/utils/snippet.ts +53 -0
  254. package/frontend-vue/app/utils/step-path.ts +32 -0
  255. package/frontend-vue/dms.frontend.ts +4 -3
  256. package/frontend-vue/i18n/locales/marketing-en-GB.json +1083 -316
  257. package/frontend-vue/i18n/locales/marketing-fr-FR.json +1087 -320
  258. package/frontend-vue/package.json +1 -2
  259. package/frontend-vue/pnpm-lock.yaml +51 -19
  260. package/frontend-vue/tests/drafts.test.ts +3 -3
  261. package/frontend-vue/tests/format.test.ts +73 -0
  262. package/frontend-vue/tests/funnel-draft.test.ts +194 -0
  263. package/frontend-vue/tests/heatmap.test.ts +32 -0
  264. package/frontend-vue/tests/i18n.test.ts +87 -4
  265. package/frontend-vue/tests/latest-request.test.ts +6 -3
  266. package/frontend-vue/tests/locale-typography.test.ts +115 -0
  267. package/frontend-vue/tests/registration.test.ts +17 -11
  268. package/frontend-vue/tests/routes.test.ts +67 -7
  269. package/frontend-vue/tests/server-parity.test.ts +77 -0
  270. package/frontend-vue/tests/stubs/frontend-module.ts +18 -3
  271. package/frontend-vue/vitest.config.ts +3 -1
  272. package/package.json +10 -8
  273. package/static/snapshot.js +46 -16
  274. package/static/tracker.js +139 -48
  275. package/dist/pages/campaigns/page.js.map +0 -1
  276. package/frontend-vue/app/components/CampaignsView.vue +0 -330
  277. package/frontend-vue/app/components/FunnelDetail.vue +0 -1026
  278. package/frontend-vue/app/components/FunnelsView.vue +0 -493
  279. package/frontend-vue/app/components/NoWebsiteState.vue +0 -18
  280. package/frontend-vue/app/components/OverviewView.vue +0 -493
  281. package/frontend-vue/app/components/PagesView.vue +0 -391
  282. package/frontend-vue/app/components/TextKpiCard.vue +0 -28
  283. package/frontend-vue/app/components/TopListTabsCard.vue +0 -162
  284. package/frontend-vue/app/composables/useChildId.ts +0 -13
  285. package/frontend-vue/app/composables/useMarketingPeriod.ts +0 -65
  286. package/frontend-vue/app/composables/useMarketingWebsiteForm.ts +0 -69
  287. package/frontend-vue/app/composables/useMarketingWebsites.ts +0 -80
  288. package/frontend-vue/app/utils/dmsComponent.ts +0 -18
  289. package/frontend-vue/tests/period.test.ts +0 -35
@@ -46,6 +46,51 @@ function backendKeys(): string[] {
46
46
  return [...found].sort()
47
47
  }
48
48
 
49
+ const COMPONENT_DIR = join(process.cwd(), 'app')
50
+ // `t('page.marketing.a.b'` as the components call it; template-built keys
51
+ // are resolved at runtime and cannot be checked here.
52
+ const COMPONENT_KEY = /t\('(page\.marketing\.[\w.-]+)'/g
53
+
54
+ function filesUnder(directory: string, extension: string): string[] {
55
+ return readdirSync(directory, { withFileTypes: true }).flatMap((entry) => {
56
+ const path = join(directory, entry.name)
57
+ if (entry.isDirectory()) {
58
+ return filesUnder(path, extension)
59
+ }
60
+ return entry.isFile() && path.endsWith(extension) ? [path] : []
61
+ })
62
+ }
63
+
64
+ function componentKeys(): string[] {
65
+ const found = new Set<string>()
66
+ for (const file of [
67
+ ...filesUnder(COMPONENT_DIR, '.vue'),
68
+ ...filesUnder(COMPONENT_DIR, '.ts'),
69
+ ]) {
70
+ for (const match of readFileSync(file, 'utf8').matchAll(COMPONENT_KEY)) {
71
+ found.add(match[1]!)
72
+ }
73
+ }
74
+ return [...found].sort()
75
+ }
76
+
77
+ // The second argument of MarketingBlock(name, key, …) and of blockMeta(key, …):
78
+ // the key every block's permission title is read under.
79
+ const BLOCK_KEY =
80
+ /(?:MarketingBlock\(\s*"\w+",\s*|blockMeta\(\s*)[`"]([\w${}.-]+)[`"]/g
81
+
82
+ function blockKeys(): string[] {
83
+ const found = new Set<string>()
84
+ for (const file of typescriptFiles(join(BACKEND_SOURCE_DIR, 'pages'))) {
85
+ for (const match of readFileSync(file, 'utf8').matchAll(BLOCK_KEY)) {
86
+ if (!match[1]!.includes('$')) {
87
+ found.add(match[1]!)
88
+ }
89
+ }
90
+ }
91
+ return [...found].sort()
92
+ }
93
+
49
94
  function resolveKey(root: unknown, key: string): unknown {
50
95
  return key
51
96
  .split('.')
@@ -65,22 +110,60 @@ describe('locales', () => {
65
110
 
66
111
  it('cover every namespace the surfaces use', () => {
67
112
  for (const namespace of [
113
+ 'context',
68
114
  'overview',
115
+ 'acquisition',
69
116
  'pages',
70
- 'campaigns',
71
117
  'funnels',
72
- 'settings',
118
+ 'funnel',
119
+ 'experiment',
120
+ 'builder',
73
121
  'websites',
122
+ 'install',
123
+ 'settings',
124
+ 'blocks',
74
125
  'errors',
75
126
  ]) {
76
127
  expect(en.page.marketing).toHaveProperty(namespace)
77
128
  }
78
129
  })
79
130
 
131
+ it('resolve every static key the components ask for', () => {
132
+ const keys = componentKeys()
133
+ expect(keys.length).toBeGreaterThan(0)
134
+ expect(
135
+ keys.filter((key) => typeof resolveKey(en, key) !== 'string'),
136
+ ).toEqual([])
137
+ expect(
138
+ keys.filter((key) => typeof resolveKey(fr, key) !== 'string'),
139
+ ).toEqual([])
140
+ })
141
+
142
+ it('name every block the pages declare', () => {
143
+ for (const key of backendKeys().filter((key) =>
144
+ key.startsWith('page.marketing.blocks.'),
145
+ )) {
146
+ expect(typeof resolveKey(en, key)).toBe('string')
147
+ }
148
+ expect(blockKeys().length).toBeGreaterThan(20)
149
+ for (const block of blockKeys()) {
150
+ expect(typeof resolveKey(en, `page.marketing.blocks.${block}.name`)).toBe(
151
+ 'string',
152
+ )
153
+ expect(
154
+ typeof resolveKey(fr, `page.marketing.blocks.${block}.description`),
155
+ ).toBe('string')
156
+ }
157
+ })
158
+
80
159
  it('resolve every static key the backend emits', () => {
81
160
  const keys = backendKeys()
82
161
  expect(keys.length).toBeGreaterThan(0)
83
- expect(keys.filter((key) => typeof resolveKey(en, key) !== 'string')).toEqual([])
84
- expect(keys.filter((key) => typeof resolveKey(fr, key) !== 'string')).toEqual([])
162
+ expect(
163
+ keys.filter((key) => typeof resolveKey(en, key) !== 'string'),
164
+ ).toEqual([])
165
+ expect(
166
+ keys.filter((key) => typeof resolveKey(fr, key) !== 'string'),
167
+ ).toEqual([])
85
168
  })
86
169
  })
@@ -5,9 +5,12 @@ import { useLatestRequest } from '../app/composables/useLatestRequest'
5
5
  describe('useLatestRequest loading states', () => {
6
6
  it('is unsettled immediately and settles with the fetched path inventory', async () => {
7
7
  let resolve: ((value: { pages: { path: string }[] }) => void) | undefined
8
- const request = useLatestRequest(() => new Promise((done) => {
9
- resolve = done
10
- }))
8
+ const request = useLatestRequest(
9
+ () =>
10
+ new Promise((done) => {
11
+ resolve = done
12
+ }),
13
+ )
11
14
 
12
15
  const pending = request.run()
13
16
  expect(request.settled.value).toBe(false)
@@ -0,0 +1,115 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import en from '../i18n/locales/marketing-en-GB.json'
3
+ import fr from '../i18n/locales/marketing-fr-FR.json'
4
+
5
+ /**
6
+ * What vue-i18n and French typography expect of every message, checked on the
7
+ * files themselves so a new string cannot bring a broken one back.
8
+ */
9
+
10
+ const NO_BREAK_SPACE = '\u00A0'
11
+ const NARROW_NO_BREAK_SPACE = '\u202F'
12
+
13
+ // `{count}` messages whose count is never 1: a fixed minimum, a list cap.
14
+ const COUNT_NEVER_ONE = new Set([
15
+ 'page.marketing.experiment.insufficient_description',
16
+ 'page.marketing.pages.list_truncated',
17
+ ])
18
+
19
+ // What intlify warns about as HTML in a message (`warnHtmlMessage`).
20
+ const HTML_TAG = /<\/?[\w\s="/.':;#-]+>/
21
+ // A `@` that is not the literal `{'@'}` starts a linked message.
22
+ const BARE_AT = /@(?<!\{'@)/
23
+
24
+ function messages(value: unknown, prefix = ''): [string, string][] {
25
+ if (typeof value === 'string') {
26
+ return [[prefix, value]]
27
+ }
28
+ return Object.entries(value as Record<string, unknown>).flatMap(
29
+ ([key, child]) => messages(child, prefix ? `${prefix}.${key}` : key),
30
+ )
31
+ }
32
+
33
+ const english = messages(en)
34
+ const french = messages(fr)
35
+
36
+ function offending(
37
+ list: [string, string][],
38
+ test: (text: string, key: string) => boolean,
39
+ ): string[] {
40
+ return list.filter(([key, text]) => test(text, key)).map(([key]) => key)
41
+ }
42
+
43
+ describe('locale messages', () => {
44
+ it('pair every English key with a French one', () => {
45
+ expect(french.map(([key]) => key).sort()).toEqual(
46
+ english.map(([key]) => key).sort(),
47
+ )
48
+ })
49
+
50
+ it('carry no HTML, which vue-i18n warns about', () => {
51
+ expect(
52
+ offending([...english, ...french], (text) => HTML_TAG.test(text)),
53
+ ).toEqual([])
54
+ })
55
+
56
+ it("escape every @ as {'@'}", () => {
57
+ expect(
58
+ offending([...english, ...french], (text) => BARE_AT.test(text)),
59
+ ).toEqual([])
60
+ })
61
+
62
+ it('give a count its singular and plural forms', () => {
63
+ const needsForms = (text: string, key: string) =>
64
+ text.includes('{count}') &&
65
+ !text.includes('|') &&
66
+ !COUNT_NEVER_ONE.has(key)
67
+ expect(offending(english, needsForms)).toEqual([])
68
+ expect(offending(french, needsForms)).toEqual([])
69
+ })
70
+
71
+ it('pluralise the same keys in both languages', () => {
72
+ const plural = new Map(
73
+ english.map(([key, text]) => [key, text.includes('|')]),
74
+ )
75
+ expect(
76
+ offending(french, (text, key) => text.includes('|') !== plural.get(key)),
77
+ ).toEqual([])
78
+ })
79
+ })
80
+
81
+ describe('French typography', () => {
82
+ it('keeps : ; ! ? % and » off the start of a line', () => {
83
+ expect(offending(french, (text) => / [:;!?%»]/.test(text))).toEqual([])
84
+ expect(offending(french, (text) => /« /.test(text))).toEqual([])
85
+ })
86
+
87
+ it('puts a narrow no-break space before ; ! ? % and a no-break space before :', () => {
88
+ expect(
89
+ offending(french, (text) =>
90
+ // "(%)": a unit in a field label.
91
+ new RegExp(`[^${NARROW_NO_BREAK_SPACE}(][;!?%]`).test(text),
92
+ ),
93
+ ).toEqual([])
94
+ expect(
95
+ offending(french, (text) =>
96
+ new RegExp(`[^${NO_BREAK_SPACE}]:`).test(text),
97
+ ),
98
+ ).toEqual([])
99
+ })
100
+
101
+ it('writes French quotes and straight apostrophes', () => {
102
+ expect(offending(french, (text) => /[“”’]/.test(text))).toEqual([])
103
+ })
104
+
105
+ it('leaves no English product words in French prose', () => {
106
+ expect(
107
+ offending(french, (text) =>
108
+ /\b(analytics|first-party|tracker|snippet|capture|tagu)/i.test(
109
+ // utm_* parameters are typed as they are; placeholders are code.
110
+ text.replace(/utm_\w+|\{\w+\}|\(first-party\)/g, ''),
111
+ ),
112
+ ),
113
+ ).toEqual([])
114
+ })
115
+ })
@@ -2,21 +2,27 @@ import { expect, it, vi } from 'vitest'
2
2
  import frontendModule from '../dms.frontend'
3
3
 
4
4
  /**
5
- * The backend addresses this module's surfaces by component name — the pages
5
+ * The backend addresses this module's blocks by component name — the pages
6
6
  * through `CustomComponent('DmsMarketing…')`, the DataTypes through their
7
- * form components — so a name that stops being registered silently falls back
8
- * to the generic card renderer instead of failing.
7
+ * form components. The loader puts the module's `componentPrefix` in front of
8
+ * each registered name, so a name that stops being registered silently
9
+ * falls back to a bare `<div>` instead of failing.
9
10
  */
10
11
  const BACKEND_ADDRESSED = [
11
- 'DmsMarketingOverviewView',
12
- 'DmsMarketingPagesView',
13
- 'DmsMarketingCampaignsView',
14
- 'DmsMarketingFunnelsView',
15
- 'DmsMarketingFunnelStepsInput',
16
- 'DmsMarketingExperimentInput',
12
+ 'Context',
13
+ 'TopListTabs',
14
+ 'ChannelsCard',
15
+ 'PagesExplorer',
16
+ 'FunnelTemplates',
17
+ 'FunnelReport',
18
+ 'FunnelBuilder',
19
+ 'WebsitesGrid',
20
+ 'InstallGuide',
21
+ 'FunnelStepsInput',
22
+ 'ExperimentInput',
17
23
  ]
18
24
 
19
- it('registers every component under the DmsMarketing prefix', async () => {
25
+ it('registers every backend-addressed component, once, without the prefix', async () => {
20
26
  const registerComponent = vi.fn()
21
27
  const registerPlugin = vi.fn()
22
28
  await frontendModule.setup({
@@ -36,7 +42,7 @@ it('registers every component under the DmsMarketing prefix', async () => {
36
42
  expect(names.length).toBeGreaterThan(0)
37
43
  expect(new Set(names).size).toBe(names.length)
38
44
  for (const name of names) {
39
- expect(name.startsWith('DmsMarketing')).toBe(true)
45
+ expect(name.startsWith('DmsMarketing')).toBe(false)
40
46
  }
41
47
  for (const name of BACKEND_ADDRESSED) {
42
48
  expect(names).toContain(name)
@@ -1,23 +1,83 @@
1
1
  import { describe, expect, it } from 'vitest'
2
2
  import {
3
- campaignsLink,
3
+ acquisitionLink,
4
+ funnelBuilderLink,
5
+ funnelLink,
4
6
  funnelsLink,
7
+ installLink,
8
+ overviewLink,
5
9
  pagesLink,
10
+ withWebsite,
6
11
  } from '../app/composables/useMarketingRoutes'
7
12
 
8
13
  describe('module links', () => {
9
14
  it('stay bare when nothing is selected', () => {
10
15
  expect(pagesLink()).toBe('/modules/marketing/pages')
11
- expect(campaignsLink()).toBe('/modules/marketing/campaigns')
16
+ expect(acquisitionLink()).toBe('/modules/marketing/acquisition')
12
17
  expect(funnelsLink()).toBe('/modules/marketing/funnels')
18
+ expect(funnelBuilderLink()).toBe('/modules/marketing/funnel-builder')
19
+ expect(installLink()).toBe('/modules/marketing/install')
13
20
  })
14
21
 
15
- it('serializes the exact button route and encodes selected values', () => {
16
- expect(funnelsLink({ website: 'test-site-1' })).toBe(
17
- '/modules/marketing/funnels?website=test-site-1',
22
+ it('encode the values they carry', () => {
23
+ expect(pagesLink('/pricing?x=1')).toBe(
24
+ '/modules/marketing/pages?path=%2Fpricing%3Fx%3D1',
18
25
  )
19
- expect(pagesLink({ website: 'w 1', path: '/pricing?x=1', period: '7d' })).toBe(
20
- '/modules/marketing/pages?website=w+1&path=%2Fpricing%3Fx%3D1&period=7d',
26
+ expect(funnelLink('f 1')).toBe('/modules/marketing/funnel?id=f+1')
27
+ expect(installLink('site-1')).toBe(
28
+ '/modules/marketing/install?website=site-1',
29
+ )
30
+ })
31
+
32
+ it('carry a template and the split switch to the builder', () => {
33
+ const link = funnelBuilderLink(null, {
34
+ split: true,
35
+ steps: [{ kind: 'custom', value: 'signup' }],
36
+ })
37
+ const query = new URL(link, 'http://x').searchParams
38
+ expect(query.get('split')).toBe('1')
39
+ expect(JSON.parse(query.get('steps')!)).toEqual([
40
+ { kind: 'custom', value: 'signup' },
41
+ ])
42
+ })
43
+
44
+ it('opens the builder on a copy of a funnel (Duplicate)', () => {
45
+ expect(funnelBuilderLink(null, { copy: 'f1' })).toBe(
46
+ '/modules/marketing/funnel-builder?copy=f1',
47
+ )
48
+ })
49
+
50
+ it('open the overview on a website', () => {
51
+ expect(overviewLink()).toBe('/modules/marketing/overview')
52
+ expect(overviewLink('site-1')).toBe(
53
+ '/modules/marketing/overview?website=site-1',
54
+ )
55
+ })
56
+
57
+ it('carry the website of the page they are followed from', () => {
58
+ const query = {
59
+ website: 'site-1',
60
+ 'marketing.period': 'last-7-days',
61
+ path: '/ignored',
62
+ }
63
+ expect(withWebsite(pagesLink('/pricing'), query)).toBe(
64
+ '/modules/marketing/pages?path=%2Fpricing&website=site-1',
65
+ )
66
+ expect(withWebsite(acquisitionLink(), query)).toBe(
67
+ '/modules/marketing/acquisition?website=site-1',
68
+ )
69
+ })
70
+
71
+ it('keep the website a link names, and stay bare without one', () => {
72
+ const query = { website: 'site-1' }
73
+ expect(withWebsite(overviewLink('site-2'), query)).toBe(
74
+ '/modules/marketing/overview?website=site-2',
75
+ )
76
+ expect(withWebsite(acquisitionLink(), {})).toBe(
77
+ '/modules/marketing/acquisition',
78
+ )
79
+ expect(withWebsite(acquisitionLink(), { website: ['site-1'] })).toBe(
80
+ '/modules/marketing/acquisition',
21
81
  )
22
82
  })
23
83
  })
@@ -0,0 +1,77 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ // The server's own rules, read from its sources: the builder and the website
3
+ // forms check a draft before saving it, and must refuse what the API refuses.
4
+ import * as server from '../../src/types/constants'
5
+ import { normalizeStepPath } from '../../src/services/funnels/step-path'
6
+ import * as draft from '../app/utils/funnel-draft'
7
+ import { normalizeFunnelStepPath } from '../app/utils/step-path'
8
+ import {
9
+ MAX_WEBSITE_DOMAIN_LENGTH,
10
+ MAX_WEBSITE_NAME_LENGTH,
11
+ } from '../app/constants'
12
+
13
+ describe('front rules mirror the server', () => {
14
+ it('caps a funnel draft as the server does', () => {
15
+ expect(draft.MAX_NAME_LENGTH).toBe(server.MAX_NAME_LENGTH)
16
+ expect(draft.MAX_STEPS).toBe(server.MAX_FUNNEL_STEPS)
17
+ expect(draft.MAX_STEP_VALUE_LENGTH).toBe(
18
+ server.MAX_FUNNEL_STEP_VALUE_LENGTH,
19
+ )
20
+ expect(draft.MAX_WINDOW_HOURS).toBe(server.MAX_CONVERSION_WINDOW_HOURS)
21
+ expect(draft.MIN_VARIATIONS).toBe(server.MIN_EXPERIMENT_VARIATIONS)
22
+ expect(draft.MAX_VARIATIONS).toBe(server.MAX_EXPERIMENT_VARIATIONS)
23
+ expect(draft.MAX_EXPERIMENT_KEY_LENGTH).toBe(
24
+ server.MAX_EXPERIMENT_KEY_LENGTH,
25
+ )
26
+ })
27
+
28
+ it('accepts the experiment keys the server accepts', () => {
29
+ const keys = [
30
+ 'hero-cta',
31
+ 'v2_bold',
32
+ '0day',
33
+ '-lead',
34
+ '_lead',
35
+ 'Hero',
36
+ 'with space',
37
+ 'é',
38
+ 'a'.repeat(server.MAX_EXPERIMENT_KEY_LENGTH),
39
+ 'a'.repeat(server.MAX_EXPERIMENT_KEY_LENGTH + 1),
40
+ ]
41
+ for (const key of keys) {
42
+ expect(draft.isExperimentKey(key), key).toBe(
43
+ server.EXPERIMENT_KEY_PATTERN.test(key) &&
44
+ key.length <= server.MAX_EXPERIMENT_KEY_LENGTH,
45
+ )
46
+ }
47
+ })
48
+
49
+ it('normalizes a page step as the server does', () => {
50
+ const typed = [
51
+ '/pricing',
52
+ 'pricing',
53
+ '/Sale/Chairs/',
54
+ '/',
55
+ '',
56
+ ' /cart?step=2#top ',
57
+ 'https://shop.acme.test/Sale/Chairs/?utm=x',
58
+ 'shop.acme.test/checkout/',
59
+ 'HTTP://Shop.Acme.Test/a',
60
+ '/blog/*',
61
+ 'shop.acme.test/blog/*',
62
+ '*',
63
+ '/a b',
64
+ 'http://[bad',
65
+ ]
66
+ for (const value of typed) {
67
+ expect(normalizeFunnelStepPath(value), value).toBe(
68
+ normalizeStepPath(value),
69
+ )
70
+ }
71
+ })
72
+
73
+ it('caps a website name and domain as the server does', () => {
74
+ expect(MAX_WEBSITE_NAME_LENGTH).toBe(server.MAX_NAME_LENGTH)
75
+ expect(MAX_WEBSITE_DOMAIN_LENGTH).toBe(server.MAX_DOMAIN_LENGTH)
76
+ })
77
+ })
@@ -26,6 +26,7 @@ export interface DmsFrontendSdk {
26
26
  }
27
27
 
28
28
  export interface DmsFrontendModule {
29
+ componentPrefix?: string
29
30
  setup(sdk: DmsFrontendSdk): void | Promise<void>
30
31
  }
31
32
 
@@ -33,15 +34,22 @@ export function defineDmsPlugin(setup: DmsPluginSetup): DmsPluginSetup {
33
34
  return setup
34
35
  }
35
36
 
36
- export function useI18n(): { t: (key: string) => string, te: () => boolean, locale: { value: string } } {
37
+ export function useI18n(): {
38
+ t: (key: string) => string
39
+ te: () => boolean
40
+ locale: { value: string }
41
+ } {
37
42
  return { t: (key: string) => key, te: () => false, locale: { value: 'en' } }
38
43
  }
39
44
 
40
- export function useDmsRoute(): { query: Record<string, string>, path: string } {
45
+ export function useDmsRoute(): { query: Record<string, string>; path: string } {
41
46
  return { query: {}, path: '/' }
42
47
  }
43
48
 
44
- export function useDmsRouter(): { replace: () => Promise<void>, push: () => Promise<void> } {
49
+ export function useDmsRouter(): {
50
+ replace: () => Promise<void>
51
+ push: () => Promise<void>
52
+ } {
45
53
  return { replace: async () => {}, push: async () => {} }
46
54
  }
47
55
 
@@ -52,3 +60,10 @@ export function useDmsRuntimeConfig(): { public: Record<string, unknown> } {
52
60
  export function resolveDmsComponent(): Component | undefined {
53
61
  return undefined
54
62
  }
63
+
64
+ export function useDmsCookie<T>(
65
+ _name: string,
66
+ options: { default: () => T },
67
+ ): { value: T } {
68
+ return { value: options.default() }
69
+ }
@@ -14,7 +14,9 @@ export default defineConfig({
14
14
  },
15
15
  },
16
16
  test: {
17
- environment: 'jsdom',
17
+ // No test touches the DOM: the suites drive composables, pure helpers
18
+ // and the module's setup. jsdom only added its boot to every file.
19
+ environment: 'node',
18
20
  include: ['tests/**/*.test.ts'],
19
21
  },
20
22
  })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@antelopejs/dms-marketing",
3
- "version": "0.3.3",
3
+ "version": "0.4.1",
4
4
  "license": "Apache-2.0",
5
5
  "repository": {
6
6
  "type": "git",
@@ -31,23 +31,25 @@
31
31
  "@antelopejs/interface-api": ">=0.0.13 <1.0.0",
32
32
  "@antelopejs/interface-api-util": ">=0.1.2 <1.0.0",
33
33
  "@antelopejs/interface-core": ">=0.1.1 <1.0.0",
34
- "@antelopejs/interface-data-api": ">=0.1.9 <1.0.0",
34
+ "@antelopejs/interface-data-api": ">=0.2.0 <1.0.0",
35
35
  "@antelopejs/interface-database": ">=0.1.8 <1.0.0",
36
- "@antelopejs/interface-database-decorators": ">=0.1.6 <1.0.0",
37
- "@antelopejs/interface-dms": ">=0.2.0 <1.0.0",
38
- "@antelopejs/interface-dms-marketing": ">=0.3.1 <0.4.0",
36
+ "@antelopejs/interface-database-decorators": ">=0.1.7 <1.0.0",
37
+ "@antelopejs/interface-dms": ">=0.6.0 <1.0.0",
38
+ "@antelopejs/interface-dms-marketing": ">=0.4.0 <0.5.0",
39
39
  "defu": "^6.1.4",
40
40
  "maxmind": "^5.0.7",
41
41
  "node-cron": "^4.2.1",
42
+ "sanitize-html": "^2.18.0",
42
43
  "ua-parser-js": "^2.0.9",
43
44
  "zod": "~3.24.4"
44
45
  },
45
46
  "devDependencies": {
46
- "@antelopejs/core": ">=1.10.1 <2",
47
+ "@antelopejs/core": ">=1.13.5 <2",
47
48
  "@antelopejs/interface-mongodb": ">=0.0.5 <1.0.0",
48
- "@antelopejs/mongodb": "1.4.0",
49
+ "@antelopejs/mongodb": "1.4.2",
49
50
  "@antelopejs/tooling-configs": ">=0.0.7 <1.0.0",
50
51
  "@types/node": "^22.19.15",
52
+ "@types/sanitize-html": "^2.16.2",
51
53
  "knip": "^6.34.0",
52
54
  "mongodb": "^6.21.0",
53
55
  "mongodb-memory-server-core": "^10.4.3",
@@ -74,7 +76,7 @@
74
76
  "scripts": {
75
77
  "build": "pnpm --filter @antelopejs/interface-dms-marketing build && rimraf dist && tsc",
76
78
  "test": "pnpm run test:backend && pnpm run test:layer",
77
- "test:backend": "pnpm run build && ajs module test .",
79
+ "test:backend": "([ -n \"$DMS_PREBUILT\" ] || pnpm run build) && ajs module test .",
78
80
  "test:layer": "cd frontend-vue && pnpm test",
79
81
  "dev": "ajs project run -w -p playground",
80
82
  "frontend:dev": "cd playground && pnpm frontend:dev",