@antelopejs/dms-marketing 0.3.2 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -15
- package/dist/config.d.ts +1 -0
- package/dist/config.js +4 -0
- package/dist/config.js.map +1 -1
- package/dist/data-api/funnels.d.ts +2 -0
- package/dist/data-api/funnels.js +8 -1
- package/dist/data-api/funnels.js.map +1 -1
- package/dist/db/models/funnels.model.d.ts +2 -0
- package/dist/db/models/funnels.model.js +9 -0
- package/dist/db/models/funnels.model.js.map +1 -1
- package/dist/db/models/index.d.ts +1 -0
- package/dist/db/models/index.js +1 -0
- package/dist/db/models/index.js.map +1 -1
- package/dist/db/models/marketing_events.model.d.ts +22 -0
- package/dist/db/models/marketing_events.model.js +70 -5
- package/dist/db/models/marketing_events.model.js.map +1 -1
- package/dist/db/models/marketing_preferences.model.d.ts +25 -0
- package/dist/db/models/marketing_preferences.model.js +29 -0
- package/dist/db/models/marketing_preferences.model.js.map +1 -0
- package/dist/db/models/marketing_sessions.model.d.ts +2 -1
- package/dist/db/models/marketing_sessions.model.js +11 -1
- package/dist/db/models/marketing_sessions.model.js.map +1 -1
- package/dist/db/models/page_snapshots.model.d.ts +2 -1
- package/dist/db/models/page_snapshots.model.js +7 -7
- package/dist/db/models/page_snapshots.model.js.map +1 -1
- package/dist/db/models/website_statistics.model.d.ts +4 -1
- package/dist/db/models/website_statistics.model.js +15 -1
- package/dist/db/models/website_statistics.model.js.map +1 -1
- package/dist/db/tables/index.d.ts +1 -0
- package/dist/db/tables/index.js +1 -0
- package/dist/db/tables/index.js.map +1 -1
- package/dist/db/tables/marketing_preferences.table.d.ts +6 -0
- package/dist/db/tables/marketing_preferences.table.js +31 -0
- package/dist/db/tables/marketing_preferences.table.js.map +1 -0
- package/dist/hooks/index.js +8 -0
- package/dist/hooks/index.js.map +1 -1
- package/dist/index.js +18 -2
- package/dist/index.js.map +1 -1
- package/dist/pages/acquisition/page.d.ts +9 -0
- package/dist/pages/acquisition/page.js +181 -0
- package/dist/pages/acquisition/page.js.map +1 -0
- package/dist/pages/all.d.ts +5 -1
- package/dist/pages/all.js +5 -1
- package/dist/pages/all.js.map +1 -1
- package/dist/pages/blocks.d.ts +18 -0
- package/dist/pages/blocks.js +56 -0
- package/dist/pages/blocks.js.map +1 -0
- package/dist/pages/funnel/page.d.ts +9 -0
- package/dist/pages/funnel/page.js +36 -0
- package/dist/pages/funnel/page.js.map +1 -0
- package/dist/pages/funnel-builder/page.d.ts +9 -0
- package/dist/pages/funnel-builder/page.js +29 -0
- package/dist/pages/funnel-builder/page.js.map +1 -0
- package/dist/pages/funnels/page.d.ts +1 -1
- package/dist/pages/funnels/page.js +200 -3
- package/dist/pages/funnels/page.js.map +1 -1
- package/dist/pages/index.d.ts +1 -0
- package/dist/pages/index.js +1 -0
- package/dist/pages/index.js.map +1 -1
- package/dist/pages/{campaigns → install}/page.d.ts +3 -3
- package/dist/pages/{campaigns → install}/page.js +14 -12
- package/dist/pages/install/page.js.map +1 -0
- package/dist/pages/module.d.ts +3 -0
- package/dist/pages/module.js +30 -3
- package/dist/pages/module.js.map +1 -1
- package/dist/pages/overview/page.d.ts +1 -1
- package/dist/pages/overview/page.js +122 -3
- package/dist/pages/overview/page.js.map +1 -1
- package/dist/pages/pages/page.d.ts +1 -1
- package/dist/pages/pages/page.js +9 -4
- package/dist/pages/pages/page.js.map +1 -1
- package/dist/pages/quick-actions.d.ts +1 -0
- package/dist/pages/quick-actions.js +25 -0
- package/dist/pages/quick-actions.js.map +1 -0
- package/dist/pages/settings/form.d.ts +2 -1
- package/dist/pages/settings/form.js +62 -20
- package/dist/pages/settings/form.js.map +1 -1
- package/dist/pages/settings/page.d.ts +4 -1
- package/dist/pages/settings/page.js +50 -4
- package/dist/pages/settings/page.js.map +1 -1
- package/dist/pages/websites/page.d.ts +11 -0
- package/dist/pages/websites/page.js +62 -0
- package/dist/pages/websites/page.js.map +1 -0
- package/dist/routes/blocks.d.ts +50 -0
- package/dist/routes/blocks.js +269 -0
- package/dist/routes/blocks.js.map +1 -0
- package/dist/routes/collect.d.ts +1 -2
- package/dist/routes/collect.js +19 -12
- package/dist/routes/collect.js.map +1 -1
- package/dist/routes/context.d.ts +8 -0
- package/dist/routes/context.js +54 -0
- package/dist/routes/context.js.map +1 -0
- package/dist/routes/experiments.js +2 -1
- package/dist/routes/experiments.js.map +1 -1
- package/dist/routes/funnels.d.ts +21 -3
- package/dist/routes/funnels.js +150 -7
- package/dist/routes/funnels.js.map +1 -1
- package/dist/routes/index.d.ts +2 -0
- package/dist/routes/index.js +2 -0
- package/dist/routes/index.js.map +1 -1
- package/dist/routes/settings.d.ts +6 -1
- package/dist/routes/settings.js +65 -4
- package/dist/routes/settings.js.map +1 -1
- package/dist/routes/snapshot.d.ts +1 -1
- package/dist/routes/snapshot.js +24 -25
- package/dist/routes/snapshot.js.map +1 -1
- package/dist/routes/stats.d.ts +14 -23
- package/dist/routes/stats.js +73 -97
- package/dist/routes/stats.js.map +1 -1
- package/dist/routes/tracker.js +6 -4
- package/dist/routes/tracker.js.map +1 -1
- package/dist/routes/websites.d.ts +4 -0
- package/dist/routes/websites.js +111 -12
- package/dist/routes/websites.js.map +1 -1
- package/dist/services/acquisition-report.d.ts +62 -0
- package/dist/services/acquisition-report.js +181 -0
- package/dist/services/acquisition-report.js.map +1 -0
- package/dist/services/acquisition.d.ts +9 -3
- package/dist/services/acquisition.js +35 -5
- package/dist/services/acquisition.js.map +1 -1
- package/dist/services/catalog.d.ts +3 -0
- package/dist/services/catalog.js +17 -0
- package/dist/services/catalog.js.map +1 -0
- package/dist/services/collect.d.ts +1 -2
- package/dist/services/collect.js +49 -24
- package/dist/services/collect.js.map +1 -1
- package/dist/services/composed.d.ts +9 -0
- package/dist/services/composed.js +62 -0
- package/dist/services/composed.js.map +1 -0
- package/dist/services/context.d.ts +20 -0
- package/dist/services/context.js +76 -0
- package/dist/services/context.js.map +1 -0
- package/dist/services/dashboard.d.ts +74 -0
- package/dist/services/dashboard.js +343 -0
- package/dist/services/dashboard.js.map +1 -0
- package/dist/services/experiments/results.d.ts +12 -1
- package/dist/services/experiments/results.js +19 -2
- package/dist/services/experiments/results.js.map +1 -1
- package/dist/services/experiments/stats.d.ts +7 -1
- package/dist/services/experiments/stats.js +43 -5
- package/dist/services/experiments/stats.js.map +1 -1
- package/dist/services/funnel-report.d.ts +64 -0
- package/dist/services/funnel-report.js +280 -0
- package/dist/services/funnel-report.js.map +1 -0
- package/dist/services/funnels/compute.d.ts +2 -0
- package/dist/services/funnels/compute.js +25 -13
- package/dist/services/funnels/compute.js.map +1 -1
- package/dist/services/funnels/results.js +4 -1
- package/dist/services/funnels/results.js.map +1 -1
- package/dist/services/funnels/step-path.d.ts +4 -0
- package/dist/services/funnels/step-path.js +47 -0
- package/dist/services/funnels/step-path.js.map +1 -0
- package/dist/services/funnels-table.d.ts +31 -0
- package/dist/services/funnels-table.js +230 -0
- package/dist/services/funnels-table.js.map +1 -0
- package/dist/services/heatmap.d.ts +15 -3
- package/dist/services/heatmap.js +54 -11
- package/dist/services/heatmap.js.map +1 -1
- package/dist/services/hourly-traffic.d.ts +4 -0
- package/dist/services/hourly-traffic.js +35 -0
- package/dist/services/hourly-traffic.js.map +1 -0
- package/dist/services/origin-guard.d.ts +9 -1
- package/dist/services/origin-guard.js +46 -1
- package/dist/services/origin-guard.js.map +1 -1
- package/dist/services/period.d.ts +26 -1
- package/dist/services/period.js +99 -9
- package/dist/services/period.js.map +1 -1
- package/dist/services/prune.d.ts +6 -0
- package/dist/services/prune.js +35 -0
- package/dist/services/prune.js.map +1 -1
- package/dist/services/request-body.d.ts +1 -2
- package/dist/services/request-body.js +10 -36
- package/dist/services/request-body.js.map +1 -1
- package/dist/services/request-meta.d.ts +0 -1
- package/dist/services/request-meta.js +0 -11
- package/dist/services/request-meta.js.map +1 -1
- package/dist/services/rollups.d.ts +25 -0
- package/dist/services/rollups.js +102 -0
- package/dist/services/rollups.js.map +1 -0
- package/dist/services/sessionize.d.ts +5 -1
- package/dist/services/sessionize.js +52 -10
- package/dist/services/sessionize.js.map +1 -1
- package/dist/services/settings.d.ts +41 -2
- package/dist/services/settings.js +219 -6
- package/dist/services/settings.js.map +1 -1
- package/dist/services/snapshots.d.ts +3 -2
- package/dist/services/snapshots.js +110 -9
- package/dist/services/snapshots.js.map +1 -1
- package/dist/services/tenant-website.js +1 -0
- package/dist/services/tenant-website.js.map +1 -1
- package/dist/services/url.js +12 -4
- package/dist/services/url.js.map +1 -1
- package/dist/services/website-hosts.d.ts +15 -0
- package/dist/services/website-hosts.js +67 -0
- package/dist/services/website-hosts.js.map +1 -0
- package/dist/services/website-report.d.ts +40 -0
- package/dist/services/website-report.js +84 -0
- package/dist/services/website-report.js.map +1 -0
- package/dist/types/constants.d.ts +14 -2
- package/dist/types/constants.js +17 -6
- package/dist/types/constants.js.map +1 -1
- package/dist/types/events.d.ts +221 -68
- package/dist/types/events.js +60 -11
- package/dist/types/events.js.map +1 -1
- package/dist/types/snapshots.d.ts +6 -0
- package/dist/types/snapshots.js +2 -0
- package/dist/types/snapshots.js.map +1 -1
- package/dist/types/statistics.d.ts +2 -1
- package/dist/types/statistics.js +9 -2
- package/dist/types/statistics.js.map +1 -1
- package/dist/utils/experiment-type.js +1 -1
- package/dist/utils/experiment-type.js.map +1 -1
- package/dist/utils/funnel-steps-type.d.ts +21 -3
- package/dist/utils/funnel-steps-type.js +7 -2
- package/dist/utils/funnel-steps-type.js.map +1 -1
- package/frontend-vue/app/components/ChannelsCard.vue +265 -0
- package/frontend-vue/app/components/Context.vue +479 -0
- package/frontend-vue/app/components/ExperimentInput.vue +3 -1
- package/frontend-vue/app/components/ExperimentReport.vue +736 -0
- package/frontend-vue/app/components/FirstRun.vue +60 -0
- package/frontend-vue/app/components/FunnelBuilder.vue +1595 -0
- package/frontend-vue/app/components/FunnelFigure.vue +640 -0
- package/frontend-vue/app/components/FunnelReport.vue +532 -0
- package/frontend-vue/app/components/FunnelStepsInput.vue +3 -1
- package/frontend-vue/app/components/FunnelTemplates.vue +161 -0
- package/frontend-vue/app/components/HeatmapCanvas.vue +11 -4
- package/frontend-vue/app/components/InstallGuide.vue +753 -0
- package/frontend-vue/app/components/PagePreview.vue +889 -416
- package/frontend-vue/app/components/PagesExplorer.vue +552 -0
- package/frontend-vue/app/components/ScrollDepthOverlay.vue +48 -20
- package/frontend-vue/app/components/TopListTabs.vue +317 -0
- package/frontend-vue/app/components/TrackerSnippet.vue +50 -16
- package/frontend-vue/app/components/VariationsInput.vue +15 -5
- package/frontend-vue/app/components/WebsitesGrid.vue +799 -0
- package/frontend-vue/app/composables/useContextLink.ts +16 -0
- package/frontend-vue/app/composables/useExperiment.ts +1 -2
- package/frontend-vue/app/composables/useFunnelSteps.ts +1 -2
- package/frontend-vue/app/composables/useLatestRequest.ts +2 -4
- package/frontend-vue/app/composables/useMarketingApi.ts +298 -133
- package/frontend-vue/app/composables/useMarketingContext.ts +55 -0
- package/frontend-vue/app/composables/useMarketingRoutes.ts +80 -27
- package/frontend-vue/app/composables/useSnapshotFrame.ts +26 -4
- package/frontend-vue/app/constants.ts +22 -0
- package/frontend-vue/app/plugins/funnel-steps-data-type.client.ts +2 -1
- package/frontend-vue/app/utils/channels.ts +37 -0
- package/frontend-vue/app/utils/format.ts +132 -0
- package/frontend-vue/app/utils/funnel-draft.ts +264 -0
- package/frontend-vue/app/utils/heatmap.ts +82 -0
- package/frontend-vue/app/utils/heatmapPalette.ts +10 -0
- package/frontend-vue/app/utils/host-conflict.ts +48 -0
- package/frontend-vue/app/utils/sample-size.ts +17 -0
- package/frontend-vue/app/utils/snapshotViewportUnits.ts +15 -5
- package/frontend-vue/app/utils/snippet.ts +53 -0
- package/frontend-vue/app/utils/step-path.ts +32 -0
- package/frontend-vue/dms.frontend.ts +4 -3
- package/frontend-vue/i18n/locales/marketing-en-GB.json +1083 -316
- package/frontend-vue/i18n/locales/marketing-fr-FR.json +1087 -320
- package/frontend-vue/package.json +3 -0
- package/frontend-vue/tests/drafts.test.ts +3 -3
- package/frontend-vue/tests/format.test.ts +73 -0
- package/frontend-vue/tests/funnel-draft.test.ts +194 -0
- package/frontend-vue/tests/heatmap.test.ts +32 -0
- package/frontend-vue/tests/i18n.test.ts +87 -4
- package/frontend-vue/tests/latest-request.test.ts +6 -3
- package/frontend-vue/tests/locale-typography.test.ts +115 -0
- package/frontend-vue/tests/registration.test.ts +21 -11
- package/frontend-vue/tests/routes.test.ts +67 -7
- package/frontend-vue/tests/server-parity.test.ts +77 -0
- package/frontend-vue/tests/stubs/frontend-module.ts +18 -3
- package/package.json +9 -7
- package/static/snapshot.js +46 -16
- package/static/tracker.js +139 -48
- package/dist/pages/campaigns/page.js.map +0 -1
- package/frontend-vue/app/components/CampaignsView.vue +0 -330
- package/frontend-vue/app/components/FunnelDetail.vue +0 -1026
- package/frontend-vue/app/components/FunnelsView.vue +0 -493
- package/frontend-vue/app/components/NoWebsiteState.vue +0 -18
- package/frontend-vue/app/components/OverviewView.vue +0 -493
- package/frontend-vue/app/components/PagesView.vue +0 -391
- package/frontend-vue/app/components/TextKpiCard.vue +0 -28
- package/frontend-vue/app/components/TopListTabsCard.vue +0 -162
- package/frontend-vue/app/composables/useChildId.ts +0 -13
- package/frontend-vue/app/composables/useMarketingPeriod.ts +0 -65
- package/frontend-vue/app/composables/useMarketingWebsiteForm.ts +0 -69
- package/frontend-vue/app/composables/useMarketingWebsites.ts +0 -80
- package/frontend-vue/app/utils/dmsComponent.ts +0 -18
- package/frontend-vue/tests/period.test.ts +0 -35
|
@@ -33,9 +33,9 @@ describe('parseExperimentValue', () => {
|
|
|
33
33
|
})
|
|
34
34
|
|
|
35
35
|
it('reads a value carrying no runs as a split that served no traffic', () => {
|
|
36
|
-
expect(
|
|
37
|
-
|
|
38
|
-
)
|
|
36
|
+
expect(parseExperimentValue({ key: 'cta', variations: [] })?.runs).toEqual(
|
|
37
|
+
[],
|
|
38
|
+
)
|
|
39
39
|
})
|
|
40
40
|
|
|
41
41
|
it('answers null on anything else', () => {
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import {
|
|
3
|
+
displayName,
|
|
4
|
+
formatDuration,
|
|
5
|
+
formatSignedDelta,
|
|
6
|
+
initials,
|
|
7
|
+
percentChangeTrend,
|
|
8
|
+
} from '../app/utils/format'
|
|
9
|
+
import { sampleSizePerArm } from '../app/utils/sample-size'
|
|
10
|
+
import { normalizeFunnelStepPath } from '../app/utils/step-path'
|
|
11
|
+
import { trackerSnippet } from '../app/utils/snippet'
|
|
12
|
+
|
|
13
|
+
describe('formatting', () => {
|
|
14
|
+
it('writes durations the way analytics tools do', () => {
|
|
15
|
+
expect(formatDuration(8)).toBe('8s')
|
|
16
|
+
expect(formatDuration(168)).toBe('2m 48s')
|
|
17
|
+
expect(formatDuration(3720)).toBe('1h 2m')
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
it('signs deltas after rounding, so a tiny change reads as flat', () => {
|
|
21
|
+
expect(formatSignedDelta(-2.34, 'points', 'en')).toBe('−2.3 pt')
|
|
22
|
+
expect(formatSignedDelta(8, 'seconds', 'en')).toBe('+8s')
|
|
23
|
+
expect(formatSignedDelta(0.04, 'number', 'en')).toBe('±0.0')
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it('reads a change in percent that rounds to 0 % as flat', () => {
|
|
27
|
+
expect(percentChangeTrend(0.3)).toBe('flat')
|
|
28
|
+
expect(percentChangeTrend(-0.49)).toBe('flat')
|
|
29
|
+
expect(percentChangeTrend(0)).toBe('flat')
|
|
30
|
+
expect(percentChangeTrend(0.5)).toBe('up')
|
|
31
|
+
expect(percentChangeTrend(-12)).toBe('down')
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('shows a pasted page step as the path the server matches', () => {
|
|
35
|
+
expect(
|
|
36
|
+
normalizeFunnelStepPath('https://shop.acme.test/Sale/Chairs/?utm=x'),
|
|
37
|
+
).toBe('/Sale/Chairs')
|
|
38
|
+
expect(normalizeFunnelStepPath('/blog/*')).toBe('/blog/*')
|
|
39
|
+
expect(normalizeFunnelStepPath('/')).toBe('/')
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('names websites by their initials', () => {
|
|
43
|
+
expect(initials('Acme shop')).toBe('AS')
|
|
44
|
+
expect(initials('docs')).toBe('DO')
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('falls back to the raw code on a malformed region', () => {
|
|
48
|
+
expect(displayName('FR', 'region', 'en')).toBe('France')
|
|
49
|
+
expect(displayName('not a code!', 'region', 'en')).toBe('not a code!')
|
|
50
|
+
})
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
describe('helpers', () => {
|
|
54
|
+
it('asks for more sessions on a rarer conversion', () => {
|
|
55
|
+
expect(sampleSizePerArm(0.03, 0.2)).toBeGreaterThan(
|
|
56
|
+
sampleSizePerArm(0.3, 0.2),
|
|
57
|
+
)
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('builds the tracker tag for every flavour', () => {
|
|
61
|
+
const html = trackerSnippet('html', 'https://dms.example.com/', 'site-1')
|
|
62
|
+
expect(html).toContain(
|
|
63
|
+
'src="https://dms.example.com/api/marketing/tracker.js"',
|
|
64
|
+
)
|
|
65
|
+
expect(html).toContain('data-website-id="site-1"')
|
|
66
|
+
expect(
|
|
67
|
+
trackerSnippet('gtm', 'https://dms.example.com', 'site-1'),
|
|
68
|
+
).toContain('setAttribute("data-website-id", "site-1")')
|
|
69
|
+
expect(
|
|
70
|
+
trackerSnippet('nuxt', 'https://dms.example.com', 'site-1'),
|
|
71
|
+
).toContain('"data-website-id": "site-1"')
|
|
72
|
+
})
|
|
73
|
+
})
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import {
|
|
3
|
+
draftIssues,
|
|
4
|
+
isExperimentKey,
|
|
5
|
+
splitSampleSize,
|
|
6
|
+
templateSteps,
|
|
7
|
+
variationSnippet,
|
|
8
|
+
type FunnelDraftValues,
|
|
9
|
+
} from '../app/utils/funnel-draft'
|
|
10
|
+
import { sampleSizePerArm } from '../app/utils/sample-size'
|
|
11
|
+
|
|
12
|
+
function draft(overrides: Partial<FunnelDraftValues> = {}): FunnelDraftValues {
|
|
13
|
+
return {
|
|
14
|
+
name: 'Checkout',
|
|
15
|
+
websiteId: 'acme-shop',
|
|
16
|
+
steps: [{ kind: 'url', value: '/cart' }],
|
|
17
|
+
windowHours: 24,
|
|
18
|
+
split: false,
|
|
19
|
+
experimentKey: '',
|
|
20
|
+
variations: [
|
|
21
|
+
{ key: 'control', weight: 1 },
|
|
22
|
+
{ key: 'bold', weight: 1 },
|
|
23
|
+
],
|
|
24
|
+
...overrides,
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
describe('experiment keys', () => {
|
|
29
|
+
it('follow the server rule: a slug starting with a letter or a digit, 64 max', () => {
|
|
30
|
+
expect(isExperimentKey('hero-cta')).toBe(true)
|
|
31
|
+
expect(isExperimentKey('a_1')).toBe(true)
|
|
32
|
+
expect(isExperimentKey('-abc')).toBe(false)
|
|
33
|
+
expect(isExperimentKey('_abc')).toBe(false)
|
|
34
|
+
expect(isExperimentKey('Hero CTA')).toBe(false)
|
|
35
|
+
expect(isExperimentKey('')).toBe(false)
|
|
36
|
+
expect(isExperimentKey('a'.repeat(64))).toBe(true)
|
|
37
|
+
expect(isExperimentKey('a'.repeat(65))).toBe(false)
|
|
38
|
+
})
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
describe('template links', () => {
|
|
42
|
+
it('keep the valid steps and drop the rest, never throwing', () => {
|
|
43
|
+
expect(templateSteps('[{"kind":"custom"}]')).toEqual([])
|
|
44
|
+
expect(templateSteps('not json')).toEqual([])
|
|
45
|
+
expect(templateSteps('{"kind":"url","value":"/"}')).toEqual([])
|
|
46
|
+
expect(templateSteps(undefined)).toEqual([])
|
|
47
|
+
expect(
|
|
48
|
+
templateSteps(
|
|
49
|
+
JSON.stringify([
|
|
50
|
+
{ kind: 'foo', value: '/a' },
|
|
51
|
+
{ kind: 'url', value: ' /b ' },
|
|
52
|
+
{ kind: 'url', value: `/${'x'.repeat(600)}` },
|
|
53
|
+
{ kind: 'custom', value: 'signup' },
|
|
54
|
+
]),
|
|
55
|
+
),
|
|
56
|
+
).toEqual([
|
|
57
|
+
{ kind: 'url', value: '/b' },
|
|
58
|
+
{ kind: 'custom', value: 'signup' },
|
|
59
|
+
])
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('stop at the step limit', () => {
|
|
63
|
+
const many = Array.from({ length: 12 }, (_, i) => ({
|
|
64
|
+
kind: 'url',
|
|
65
|
+
value: `/p${i}`,
|
|
66
|
+
}))
|
|
67
|
+
expect(templateSteps(JSON.stringify(many))).toHaveLength(10)
|
|
68
|
+
})
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
describe('draft issues', () => {
|
|
72
|
+
it('are empty for a valid funnel', () => {
|
|
73
|
+
expect(draftIssues(draft(), false)).toEqual({})
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it('name the field each refusal belongs to', () => {
|
|
77
|
+
const issues = draftIssues(
|
|
78
|
+
draft({
|
|
79
|
+
name: ' ',
|
|
80
|
+
websiteId: '',
|
|
81
|
+
steps: [{ kind: 'url', value: '' }],
|
|
82
|
+
windowHours: 1000,
|
|
83
|
+
}),
|
|
84
|
+
false,
|
|
85
|
+
)
|
|
86
|
+
expect(Object.keys(issues)).toEqual([
|
|
87
|
+
'name',
|
|
88
|
+
'websiteId',
|
|
89
|
+
'steps',
|
|
90
|
+
'conversionWindowHours',
|
|
91
|
+
])
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
it('flag the step that is too long', () => {
|
|
95
|
+
const issues = draftIssues(
|
|
96
|
+
draft({
|
|
97
|
+
steps: [
|
|
98
|
+
{ kind: 'url', value: '/a' },
|
|
99
|
+
{ kind: 'url', value: `/${'x'.repeat(500)}` },
|
|
100
|
+
],
|
|
101
|
+
}),
|
|
102
|
+
false,
|
|
103
|
+
)
|
|
104
|
+
expect(issues.steps).toMatchObject({ message: 'step_length', index: 1 })
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
it('refuse a window that is empty, fractional or out of bounds', () => {
|
|
108
|
+
for (const windowHours of ['', 0, 1.5, 721]) {
|
|
109
|
+
expect(draftIssues(draft({ windowHours }), false)).toHaveProperty(
|
|
110
|
+
'conversionWindowHours',
|
|
111
|
+
)
|
|
112
|
+
}
|
|
113
|
+
expect(draftIssues(draft({ windowHours: 720 }), false)).toEqual({})
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
it('check the split: key, arm keys, then weights', () => {
|
|
117
|
+
const split = { split: true, experimentKey: 'hero-cta' }
|
|
118
|
+
expect(draftIssues(draft(split), false)).toEqual({})
|
|
119
|
+
expect(
|
|
120
|
+
draftIssues(draft({ ...split, experimentKey: '-abc' }), false),
|
|
121
|
+
).toHaveProperty('experiment')
|
|
122
|
+
expect(
|
|
123
|
+
draftIssues(
|
|
124
|
+
draft({
|
|
125
|
+
...split,
|
|
126
|
+
variations: [
|
|
127
|
+
{ key: 'a', weight: 1 },
|
|
128
|
+
{ key: 'a', weight: 1 },
|
|
129
|
+
],
|
|
130
|
+
}),
|
|
131
|
+
false,
|
|
132
|
+
).variations,
|
|
133
|
+
).toMatchObject({ message: 'variations', index: 1 })
|
|
134
|
+
for (const weights of [
|
|
135
|
+
[0, 0],
|
|
136
|
+
[-3, 1],
|
|
137
|
+
['', 1],
|
|
138
|
+
]) {
|
|
139
|
+
expect(
|
|
140
|
+
draftIssues(
|
|
141
|
+
draft({
|
|
142
|
+
...split,
|
|
143
|
+
variations: [
|
|
144
|
+
{ key: 'control', weight: weights[0]! },
|
|
145
|
+
{ key: 'bold', weight: weights[1]! },
|
|
146
|
+
],
|
|
147
|
+
}),
|
|
148
|
+
false,
|
|
149
|
+
).variations,
|
|
150
|
+
).toMatchObject({ message: 'weights' })
|
|
151
|
+
}
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
it('leave a started split alone: its key and arms are frozen', () => {
|
|
155
|
+
expect(
|
|
156
|
+
draftIssues(draft({ split: true, experimentKey: '-abc' }), true),
|
|
157
|
+
).toEqual({})
|
|
158
|
+
})
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
describe('split sample size', () => {
|
|
162
|
+
it('is what the smallest arm needs, scaled to the whole traffic', () => {
|
|
163
|
+
const perArm = sampleSizePerArm(0.04, 0.2)
|
|
164
|
+
expect(splitSampleSize(0.04, 0.2, [1, 1])).toBeCloseTo(perArm * 2)
|
|
165
|
+
expect(splitSampleSize(0.04, 0.2, [1, 3])).toBeCloseTo(perArm * 4)
|
|
166
|
+
expect(splitSampleSize(0.04, 0.2, [1, 1, 1])).toBeCloseTo(perArm * 3)
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
it('ignores arms that receive no traffic, and needs two that do', () => {
|
|
170
|
+
const perArm = sampleSizePerArm(0.04, 0.2)
|
|
171
|
+
expect(splitSampleSize(0.04, 0.2, [1, 1, 0])).toBeCloseTo(perArm * 2)
|
|
172
|
+
expect(splitSampleSize(0.04, 0.2, [1, 0])).toBeNull()
|
|
173
|
+
expect(splitSampleSize(0, 0.2, [1, 1])).toBeNull()
|
|
174
|
+
})
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
describe('variation snippet', () => {
|
|
178
|
+
it('runs in a classic script: no top-level await', () => {
|
|
179
|
+
const code = variationSnippet('hero-cta', 'control', ['bold', 'quiet'])
|
|
180
|
+
expect(code).not.toMatch(/^\s*(const|let|var) .*= await/m)
|
|
181
|
+
expect(code).toContain('variation("hero-cta").then(')
|
|
182
|
+
expect(code).toContain('} else if (arm === "quiet") {')
|
|
183
|
+
expect(() => new Function(code)).not.toThrow()
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
it('writes its comments in the language it is given', () => {
|
|
187
|
+
const code = variationSnippet('hero-cta', 'control', ['bold'], {
|
|
188
|
+
show: (variation) => `afficher ${variation}`,
|
|
189
|
+
keep: (control) => `garder ${control}`,
|
|
190
|
+
})
|
|
191
|
+
expect(code).toContain('// afficher bold')
|
|
192
|
+
expect(code).toContain('// garder control')
|
|
193
|
+
})
|
|
194
|
+
})
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import {
|
|
3
|
+
heatmapDataLabel,
|
|
4
|
+
normalizePagePath,
|
|
5
|
+
} from '../app/utils/heatmap'
|
|
6
|
+
|
|
7
|
+
/** A `t` that shows which key and which plural form were asked for. */
|
|
8
|
+
function t(key: string, named: Record<string, unknown> = {}, plural?: number) {
|
|
9
|
+
const name = key.split('.').at(-1)
|
|
10
|
+
if (name === 'and') {
|
|
11
|
+
return `${named.first} + ${named.second}`
|
|
12
|
+
}
|
|
13
|
+
return `${named.count} ${name}${plural === 1 ? '' : '[n]'}`
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
describe('heatmap helpers', () => {
|
|
17
|
+
it('reduces a typed path the way the tracker stores it', () => {
|
|
18
|
+
expect(normalizePagePath('demo/pricing')).toBe('/demo/pricing')
|
|
19
|
+
expect(normalizePagePath(' /demo/pricing?x=1#top ')).toBe('/demo/pricing')
|
|
20
|
+
expect(normalizePagePath('https://shop.example/cart')).toBe('/cart')
|
|
21
|
+
expect(normalizePagePath(' ')).toBeUndefined()
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it('names what a reset deletes, snapshots only when there are some', () => {
|
|
25
|
+
expect(heatmapDataLabel({ events: 1, snapshots: 0 }, t, 'en-GB')).toBe(
|
|
26
|
+
'1 events',
|
|
27
|
+
)
|
|
28
|
+
expect(heatmapDataLabel({ events: 18, snapshots: 1 }, t, 'en-GB')).toBe(
|
|
29
|
+
'18 events[n] + 1 snapshots',
|
|
30
|
+
)
|
|
31
|
+
})
|
|
32
|
+
})
|
|
@@ -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
|
-
'
|
|
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(
|
|
84
|
-
|
|
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(
|
|
9
|
-
|
|
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,31 @@ import { expect, it, vi } from 'vitest'
|
|
|
2
2
|
import frontendModule from '../dms.frontend'
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
* The backend addresses this module's
|
|
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
|
|
8
|
-
*
|
|
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
|
-
'
|
|
12
|
-
'
|
|
13
|
-
'
|
|
14
|
-
'
|
|
15
|
-
'
|
|
16
|
-
'
|
|
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('
|
|
25
|
+
it('declares the DmsMarketing prefix', () => {
|
|
26
|
+
expect(frontendModule.componentPrefix).toBe('DmsMarketing')
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('registers every backend-addressed component, once, without the prefix', async () => {
|
|
20
30
|
const registerComponent = vi.fn()
|
|
21
31
|
const registerPlugin = vi.fn()
|
|
22
32
|
await frontendModule.setup({
|
|
@@ -36,7 +46,7 @@ it('registers every component under the DmsMarketing prefix', async () => {
|
|
|
36
46
|
expect(names.length).toBeGreaterThan(0)
|
|
37
47
|
expect(new Set(names).size).toBe(names.length)
|
|
38
48
|
for (const name of names) {
|
|
39
|
-
expect(name.startsWith('DmsMarketing')).toBe(
|
|
49
|
+
expect(name.startsWith('DmsMarketing')).toBe(false)
|
|
40
50
|
}
|
|
41
51
|
for (const name of BACKEND_ADDRESSED) {
|
|
42
52
|
expect(names).toContain(name)
|