@antelopejs/dms-marketing 0.4.0 → 0.5.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/dist/hooks/index.js +1 -5
- package/dist/hooks/index.js.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/pages/funnels/page.js +5 -5
- package/dist/pages/funnels/page.js.map +1 -1
- package/dist/routes/public-endpoints.d.ts +2 -0
- package/dist/routes/public-endpoints.js +14 -0
- package/dist/routes/public-endpoints.js.map +1 -0
- package/frontend-vue/app/components/Context.vue +37 -61
- package/frontend-vue/app/components/FunnelBuilder.vue +22 -19
- package/frontend-vue/app/components/FunnelReport.vue +1 -1
- package/frontend-vue/app/components/FunnelTemplates.vue +16 -27
- package/frontend-vue/app/components/InstallGuide.vue +40 -57
- package/frontend-vue/app/components/PagePreview.vue +60 -76
- package/frontend-vue/app/components/PagesExplorer.vue +86 -80
- package/frontend-vue/app/components/WebsitesGrid.vue +32 -23
- package/frontend-vue/app/composables/useMarketingApi.ts +7 -120
- package/frontend-vue/app/composables/useMarketingContext.ts +1 -10
- package/frontend-vue/app/composables/useMarketingNow.ts +29 -0
- package/frontend-vue/app/composables/useMarketingReads.ts +314 -0
- package/frontend-vue/app/utils/format.ts +37 -1
- package/frontend-vue/package.json +1 -2
- package/frontend-vue/pnpm-lock.yaml +51 -19
- package/frontend-vue/tests/format.test.ts +21 -0
- package/frontend-vue/tests/marketing-reads.test.ts +252 -0
- package/frontend-vue/tests/registration.test.ts +0 -4
- package/frontend-vue/vitest.config.ts +3 -1
- package/package.json +3 -3
|
@@ -101,10 +101,46 @@ export function formatDateRange(from: Date, to: Date, locale: string): string {
|
|
|
101
101
|
}).formatRange(from, to)
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
-
|
|
104
|
+
/**
|
|
105
|
+
* When the statistics' day (a UTC day) starts on the reader's clock today,
|
|
106
|
+
* "02:00"; empty where it starts at their midnight. `timeZone` is the
|
|
107
|
+
* reader's (`undefined`: the zone the code runs in).
|
|
108
|
+
*/
|
|
109
|
+
export function utcDayStart(
|
|
110
|
+
now: number,
|
|
111
|
+
locale: string,
|
|
112
|
+
timeZone?: string,
|
|
113
|
+
): string {
|
|
114
|
+
const today = new Date(now)
|
|
115
|
+
const utcMidnight = new Date(
|
|
116
|
+
Date.UTC(today.getUTCFullYear(), today.getUTCMonth(), today.getUTCDate()),
|
|
117
|
+
)
|
|
118
|
+
const clock = new Intl.DateTimeFormat('en-US', {
|
|
119
|
+
hour: 'numeric',
|
|
120
|
+
minute: 'numeric',
|
|
121
|
+
hourCycle: 'h23',
|
|
122
|
+
timeZone,
|
|
123
|
+
}).format(utcMidnight)
|
|
124
|
+
if (clock === '0:00' || clock === '00:00') {
|
|
125
|
+
return ''
|
|
126
|
+
}
|
|
127
|
+
return new Intl.DateTimeFormat(locale, {
|
|
128
|
+
hour: '2-digit',
|
|
129
|
+
minute: '2-digit',
|
|
130
|
+
timeZone,
|
|
131
|
+
}).format(utcMidnight)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** "Sep 29", in `timeZone` (`undefined`: the zone the code runs in). */
|
|
135
|
+
export function formatShortDate(
|
|
136
|
+
at: number | Date,
|
|
137
|
+
locale: string,
|
|
138
|
+
timeZone?: string,
|
|
139
|
+
): string {
|
|
105
140
|
return new Intl.DateTimeFormat(locale, {
|
|
106
141
|
month: 'short',
|
|
107
142
|
day: 'numeric',
|
|
143
|
+
timeZone,
|
|
108
144
|
}).format(at)
|
|
109
145
|
}
|
|
110
146
|
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
"license": "Apache-2.0",
|
|
8
8
|
"packageManager": "pnpm@10.6.5",
|
|
9
9
|
"engines": {
|
|
10
|
-
"@antelopejs/dms-frontend": ">=0.
|
|
10
|
+
"@antelopejs/dms-frontend": ">=0.6.0 <0.7.0"
|
|
11
11
|
},
|
|
12
12
|
"scripts": {
|
|
13
13
|
"dev": "pnpm --dir ../playground frontend:dev",
|
|
@@ -24,7 +24,6 @@
|
|
|
24
24
|
"@vitejs/plugin-vue": "^6.0.0",
|
|
25
25
|
"eslint": "^9.0.0",
|
|
26
26
|
"eslint-plugin-vue": "^10.0.0",
|
|
27
|
-
"jsdom": "^26",
|
|
28
27
|
"prettier": "^3.5.3",
|
|
29
28
|
"typescript": "^5.9.3",
|
|
30
29
|
"typescript-eslint": "^8.0.0",
|
|
@@ -24,9 +24,6 @@ importers:
|
|
|
24
24
|
eslint-plugin-vue:
|
|
25
25
|
specifier: ^10.0.0
|
|
26
26
|
version: 10.11.0(@typescript-eslint/parser@8.70.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.5(jiti@2.7.0))(vue-eslint-parser@10.4.1(eslint@9.39.5(jiti@2.7.0)))
|
|
27
|
-
jsdom:
|
|
28
|
-
specifier: ^26
|
|
29
|
-
version: 26.1.0
|
|
30
27
|
prettier:
|
|
31
28
|
specifier: ^3.5.3
|
|
32
29
|
version: 3.9.7
|
|
@@ -2653,6 +2650,7 @@ snapshots:
|
|
|
2653
2650
|
'@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4)
|
|
2654
2651
|
'@csstools/css-tokenizer': 3.0.4
|
|
2655
2652
|
lru-cache: 10.4.3
|
|
2653
|
+
optional: true
|
|
2656
2654
|
|
|
2657
2655
|
'@babel/helper-string-parser@7.29.7': {}
|
|
2658
2656
|
|
|
@@ -2671,12 +2669,14 @@ snapshots:
|
|
|
2671
2669
|
dependencies:
|
|
2672
2670
|
fontkitten: 1.0.3
|
|
2673
2671
|
|
|
2674
|
-
'@csstools/color-helpers@5.1.0':
|
|
2672
|
+
'@csstools/color-helpers@5.1.0':
|
|
2673
|
+
optional: true
|
|
2675
2674
|
|
|
2676
2675
|
'@csstools/css-calc@2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)':
|
|
2677
2676
|
dependencies:
|
|
2678
2677
|
'@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4)
|
|
2679
2678
|
'@csstools/css-tokenizer': 3.0.4
|
|
2679
|
+
optional: true
|
|
2680
2680
|
|
|
2681
2681
|
'@csstools/css-color-parser@3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)':
|
|
2682
2682
|
dependencies:
|
|
@@ -2684,12 +2684,15 @@ snapshots:
|
|
|
2684
2684
|
'@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)
|
|
2685
2685
|
'@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4)
|
|
2686
2686
|
'@csstools/css-tokenizer': 3.0.4
|
|
2687
|
+
optional: true
|
|
2687
2688
|
|
|
2688
2689
|
'@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4)':
|
|
2689
2690
|
dependencies:
|
|
2690
2691
|
'@csstools/css-tokenizer': 3.0.4
|
|
2692
|
+
optional: true
|
|
2691
2693
|
|
|
2692
|
-
'@csstools/css-tokenizer@3.0.4':
|
|
2694
|
+
'@csstools/css-tokenizer@3.0.4':
|
|
2695
|
+
optional: true
|
|
2693
2696
|
|
|
2694
2697
|
'@esbuild/aix-ppc64@0.27.7':
|
|
2695
2698
|
optional: true
|
|
@@ -3830,7 +3833,8 @@ snapshots:
|
|
|
3830
3833
|
|
|
3831
3834
|
acorn@8.18.0: {}
|
|
3832
3835
|
|
|
3833
|
-
agent-base@7.1.4:
|
|
3836
|
+
agent-base@7.1.4:
|
|
3837
|
+
optional: true
|
|
3834
3838
|
|
|
3835
3839
|
ajv@6.15.0:
|
|
3836
3840
|
dependencies:
|
|
@@ -3946,6 +3950,7 @@ snapshots:
|
|
|
3946
3950
|
dependencies:
|
|
3947
3951
|
'@asamuzakjp/css-color': 3.2.0
|
|
3948
3952
|
rrweb-cssom: 0.8.0
|
|
3953
|
+
optional: true
|
|
3949
3954
|
|
|
3950
3955
|
csstype@3.2.3: {}
|
|
3951
3956
|
|
|
@@ -3953,12 +3958,14 @@ snapshots:
|
|
|
3953
3958
|
dependencies:
|
|
3954
3959
|
whatwg-mimetype: 4.0.0
|
|
3955
3960
|
whatwg-url: 14.2.0
|
|
3961
|
+
optional: true
|
|
3956
3962
|
|
|
3957
3963
|
debug@4.4.3:
|
|
3958
3964
|
dependencies:
|
|
3959
3965
|
ms: 2.1.3
|
|
3960
3966
|
|
|
3961
|
-
decimal.js@10.6.0:
|
|
3967
|
+
decimal.js@10.6.0:
|
|
3968
|
+
optional: true
|
|
3962
3969
|
|
|
3963
3970
|
deep-is@0.1.4: {}
|
|
3964
3971
|
|
|
@@ -4012,7 +4019,8 @@ snapshots:
|
|
|
4012
4019
|
graceful-fs: 4.2.11
|
|
4013
4020
|
tapable: 2.3.3
|
|
4014
4021
|
|
|
4015
|
-
entities@6.0.1:
|
|
4022
|
+
entities@6.0.1:
|
|
4023
|
+
optional: true
|
|
4016
4024
|
|
|
4017
4025
|
entities@7.0.1: {}
|
|
4018
4026
|
|
|
@@ -4293,6 +4301,7 @@ snapshots:
|
|
|
4293
4301
|
html-encoding-sniffer@4.0.0:
|
|
4294
4302
|
dependencies:
|
|
4295
4303
|
whatwg-encoding: 3.1.1
|
|
4304
|
+
optional: true
|
|
4296
4305
|
|
|
4297
4306
|
http-proxy-agent@7.0.2:
|
|
4298
4307
|
dependencies:
|
|
@@ -4300,6 +4309,7 @@ snapshots:
|
|
|
4300
4309
|
debug: 4.4.3
|
|
4301
4310
|
transitivePeerDependencies:
|
|
4302
4311
|
- supports-color
|
|
4312
|
+
optional: true
|
|
4303
4313
|
|
|
4304
4314
|
https-proxy-agent@7.0.6:
|
|
4305
4315
|
dependencies:
|
|
@@ -4307,12 +4317,14 @@ snapshots:
|
|
|
4307
4317
|
debug: 4.4.3
|
|
4308
4318
|
transitivePeerDependencies:
|
|
4309
4319
|
- supports-color
|
|
4320
|
+
optional: true
|
|
4310
4321
|
|
|
4311
4322
|
human-signals@5.0.0: {}
|
|
4312
4323
|
|
|
4313
4324
|
iconv-lite@0.6.3:
|
|
4314
4325
|
dependencies:
|
|
4315
4326
|
safer-buffer: 2.1.2
|
|
4327
|
+
optional: true
|
|
4316
4328
|
|
|
4317
4329
|
ignore@5.3.2: {}
|
|
4318
4330
|
|
|
@@ -4335,7 +4347,8 @@ snapshots:
|
|
|
4335
4347
|
dependencies:
|
|
4336
4348
|
is-extglob: 2.1.1
|
|
4337
4349
|
|
|
4338
|
-
is-potential-custom-element-name@1.0.1:
|
|
4350
|
+
is-potential-custom-element-name@1.0.1:
|
|
4351
|
+
optional: true
|
|
4339
4352
|
|
|
4340
4353
|
is-stream@3.0.0: {}
|
|
4341
4354
|
|
|
@@ -4377,6 +4390,7 @@ snapshots:
|
|
|
4377
4390
|
- bufferutil
|
|
4378
4391
|
- supports-color
|
|
4379
4392
|
- utf-8-validate
|
|
4393
|
+
optional: true
|
|
4380
4394
|
|
|
4381
4395
|
json-buffer@3.0.1: {}
|
|
4382
4396
|
|
|
@@ -4513,7 +4527,8 @@ snapshots:
|
|
|
4513
4527
|
|
|
4514
4528
|
lodash.merge@4.6.2: {}
|
|
4515
4529
|
|
|
4516
|
-
lru-cache@10.4.3:
|
|
4530
|
+
lru-cache@10.4.3:
|
|
4531
|
+
optional: true
|
|
4517
4532
|
|
|
4518
4533
|
lru-cache@11.5.2: {}
|
|
4519
4534
|
|
|
@@ -4600,7 +4615,8 @@ snapshots:
|
|
|
4600
4615
|
dependencies:
|
|
4601
4616
|
boolbase: 1.0.0
|
|
4602
4617
|
|
|
4603
|
-
nwsapi@2.2.27:
|
|
4618
|
+
nwsapi@2.2.27:
|
|
4619
|
+
optional: true
|
|
4604
4620
|
|
|
4605
4621
|
obug@2.2.1: {}
|
|
4606
4622
|
|
|
@@ -4649,6 +4665,7 @@ snapshots:
|
|
|
4649
4665
|
parse5@7.3.0:
|
|
4650
4666
|
dependencies:
|
|
4651
4667
|
entities: 6.0.1
|
|
4668
|
+
optional: true
|
|
4652
4669
|
|
|
4653
4670
|
path-exists@4.0.0: {}
|
|
4654
4671
|
|
|
@@ -4823,13 +4840,16 @@ snapshots:
|
|
|
4823
4840
|
|
|
4824
4841
|
rope-sequence@1.3.4: {}
|
|
4825
4842
|
|
|
4826
|
-
rrweb-cssom@0.8.0:
|
|
4843
|
+
rrweb-cssom@0.8.0:
|
|
4844
|
+
optional: true
|
|
4827
4845
|
|
|
4828
|
-
safer-buffer@2.1.2:
|
|
4846
|
+
safer-buffer@2.1.2:
|
|
4847
|
+
optional: true
|
|
4829
4848
|
|
|
4830
4849
|
saxes@6.0.0:
|
|
4831
4850
|
dependencies:
|
|
4832
4851
|
xmlchars: 2.2.0
|
|
4852
|
+
optional: true
|
|
4833
4853
|
|
|
4834
4854
|
scule@1.3.0: {}
|
|
4835
4855
|
|
|
@@ -4869,7 +4889,8 @@ snapshots:
|
|
|
4869
4889
|
dependencies:
|
|
4870
4890
|
has-flag: 4.0.0
|
|
4871
4891
|
|
|
4872
|
-
symbol-tree@3.2.4:
|
|
4892
|
+
symbol-tree@3.2.4:
|
|
4893
|
+
optional: true
|
|
4873
4894
|
|
|
4874
4895
|
tailwind-merge@3.7.0: {}
|
|
4875
4896
|
|
|
@@ -4895,21 +4916,25 @@ snapshots:
|
|
|
4895
4916
|
|
|
4896
4917
|
tinyrainbow@3.1.1: {}
|
|
4897
4918
|
|
|
4898
|
-
tldts-core@6.1.86:
|
|
4919
|
+
tldts-core@6.1.86:
|
|
4920
|
+
optional: true
|
|
4899
4921
|
|
|
4900
4922
|
tldts@6.1.86:
|
|
4901
4923
|
dependencies:
|
|
4902
4924
|
tldts-core: 6.1.86
|
|
4925
|
+
optional: true
|
|
4903
4926
|
|
|
4904
4927
|
totalist@3.0.1: {}
|
|
4905
4928
|
|
|
4906
4929
|
tough-cookie@5.1.2:
|
|
4907
4930
|
dependencies:
|
|
4908
4931
|
tldts: 6.1.86
|
|
4932
|
+
optional: true
|
|
4909
4933
|
|
|
4910
4934
|
tr46@5.1.1:
|
|
4911
4935
|
dependencies:
|
|
4912
4936
|
punycode: 2.3.1
|
|
4937
|
+
optional: true
|
|
4913
4938
|
|
|
4914
4939
|
ts-api-utils@2.5.0(typescript@5.9.3):
|
|
4915
4940
|
dependencies:
|
|
@@ -5179,21 +5204,26 @@ snapshots:
|
|
|
5179
5204
|
w3c-xmlserializer@5.0.0:
|
|
5180
5205
|
dependencies:
|
|
5181
5206
|
xml-name-validator: 5.0.0
|
|
5207
|
+
optional: true
|
|
5182
5208
|
|
|
5183
|
-
webidl-conversions@7.0.0:
|
|
5209
|
+
webidl-conversions@7.0.0:
|
|
5210
|
+
optional: true
|
|
5184
5211
|
|
|
5185
5212
|
webpack-virtual-modules@0.6.2: {}
|
|
5186
5213
|
|
|
5187
5214
|
whatwg-encoding@3.1.1:
|
|
5188
5215
|
dependencies:
|
|
5189
5216
|
iconv-lite: 0.6.3
|
|
5217
|
+
optional: true
|
|
5190
5218
|
|
|
5191
|
-
whatwg-mimetype@4.0.0:
|
|
5219
|
+
whatwg-mimetype@4.0.0:
|
|
5220
|
+
optional: true
|
|
5192
5221
|
|
|
5193
5222
|
whatwg-url@14.2.0:
|
|
5194
5223
|
dependencies:
|
|
5195
5224
|
tr46: 5.1.1
|
|
5196
5225
|
webidl-conversions: 7.0.0
|
|
5226
|
+
optional: true
|
|
5197
5227
|
|
|
5198
5228
|
wheel-gestures@2.3.0: {}
|
|
5199
5229
|
|
|
@@ -5208,11 +5238,13 @@ snapshots:
|
|
|
5208
5238
|
|
|
5209
5239
|
word-wrap@1.2.5: {}
|
|
5210
5240
|
|
|
5211
|
-
ws@8.21.3:
|
|
5241
|
+
ws@8.21.3:
|
|
5242
|
+
optional: true
|
|
5212
5243
|
|
|
5213
5244
|
xml-name-validator@5.0.0: {}
|
|
5214
5245
|
|
|
5215
|
-
xmlchars@2.2.0:
|
|
5246
|
+
xmlchars@2.2.0:
|
|
5247
|
+
optional: true
|
|
5216
5248
|
|
|
5217
5249
|
y-protocols@1.0.7(yjs@13.6.32):
|
|
5218
5250
|
dependencies:
|
|
@@ -2,9 +2,11 @@ import { describe, expect, it } from 'vitest'
|
|
|
2
2
|
import {
|
|
3
3
|
displayName,
|
|
4
4
|
formatDuration,
|
|
5
|
+
formatShortDate,
|
|
5
6
|
formatSignedDelta,
|
|
6
7
|
initials,
|
|
7
8
|
percentChangeTrend,
|
|
9
|
+
utcDayStart,
|
|
8
10
|
} from '../app/utils/format'
|
|
9
11
|
import { sampleSizePerArm } from '../app/utils/sample-size'
|
|
10
12
|
import { normalizeFunnelStepPath } from '../app/utils/step-path'
|
|
@@ -71,3 +73,22 @@ describe('helpers', () => {
|
|
|
71
73
|
).toContain('"data-website-id": "site-1"')
|
|
72
74
|
})
|
|
73
75
|
})
|
|
76
|
+
|
|
77
|
+
describe('dates in the reader zone', () => {
|
|
78
|
+
// 16 Oct 2026: Paris on summer time (UTC+2), New York on UTC-4.
|
|
79
|
+
const now = Date.UTC(2026, 9, 16, 12)
|
|
80
|
+
|
|
81
|
+
it('says when the statistics day starts on the reader clock, nothing at UTC', () => {
|
|
82
|
+
expect(utcDayStart(now, 'en-GB', 'Europe/Paris')).toBe('02:00')
|
|
83
|
+
expect(utcDayStart(now, 'en-GB', 'America/New_York')).toBe('20:00')
|
|
84
|
+
expect(utcDayStart(now, 'en-GB', 'UTC')).toBe('')
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
it('writes a short date on the reader day, wherever the code runs', () => {
|
|
88
|
+
const lateEvening = Date.UTC(2026, 9, 16, 2)
|
|
89
|
+
expect(formatShortDate(lateEvening, 'en', 'Europe/Paris')).toBe('Oct 16')
|
|
90
|
+
expect(formatShortDate(lateEvening, 'en', 'America/New_York')).toBe(
|
|
91
|
+
'Oct 15',
|
|
92
|
+
)
|
|
93
|
+
})
|
|
94
|
+
})
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import { createSSRApp, defineComponent, h, ref, toValue } from 'vue'
|
|
2
|
+
import { renderToString } from 'vue/server-renderer'
|
|
3
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
4
|
+
import {
|
|
5
|
+
useFunnelTemplates,
|
|
6
|
+
useHeatmapCountRead,
|
|
7
|
+
useMarketingContextRead,
|
|
8
|
+
usePageHeatmap,
|
|
9
|
+
usePageInventory,
|
|
10
|
+
usePageSnapshot,
|
|
11
|
+
useWebsiteConnection,
|
|
12
|
+
useWebsiteFunnels,
|
|
13
|
+
} from '../app/composables/useMarketingReads'
|
|
14
|
+
import { useMarketingNow } from '../app/composables/useMarketingNow'
|
|
15
|
+
|
|
16
|
+
/** What a read asked `useApiData` for. */
|
|
17
|
+
interface ReadCall {
|
|
18
|
+
url: () => string | null
|
|
19
|
+
options: Record<string, unknown>
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
let calls: ReadCall[]
|
|
23
|
+
|
|
24
|
+
const PERIOD = {
|
|
25
|
+
preset: 'last-7-days',
|
|
26
|
+
comparison: 'none',
|
|
27
|
+
range: {
|
|
28
|
+
from: new Date('2026-10-09T00:00:00.000Z'),
|
|
29
|
+
to: new Date('2026-10-15T23:59:59.999Z'),
|
|
30
|
+
},
|
|
31
|
+
compareRange: null,
|
|
32
|
+
key: 'last-7-days',
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
beforeEach(() => {
|
|
36
|
+
calls = []
|
|
37
|
+
vi.stubGlobal(
|
|
38
|
+
'useApiData',
|
|
39
|
+
(url: unknown, options: Record<string, unknown> = {}) => {
|
|
40
|
+
calls.push({ url: () => toValue(url as () => string | null), options })
|
|
41
|
+
return { data: ref(null) }
|
|
42
|
+
},
|
|
43
|
+
)
|
|
44
|
+
// The DMS's own, as the module's blocks append a period.
|
|
45
|
+
vi.stubGlobal(
|
|
46
|
+
'appendPeriodToUrl',
|
|
47
|
+
(url: string, state: typeof PERIOD | null) =>
|
|
48
|
+
state?.range
|
|
49
|
+
? `${url}?from=${state.range.from.toISOString()}&to=${state.range.to.toISOString()}`
|
|
50
|
+
: url,
|
|
51
|
+
)
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
afterEach(() => {
|
|
55
|
+
vi.unstubAllGlobals()
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
const query = (call: ReadCall | undefined) =>
|
|
59
|
+
toValue(call?.options.query as () => Record<string, unknown>)
|
|
60
|
+
|
|
61
|
+
describe('the reads the pages render', () => {
|
|
62
|
+
it('reads nothing bound to the period before there is one, nor without a website', () => {
|
|
63
|
+
const website = ref('')
|
|
64
|
+
const period = ref<typeof PERIOD | null>(null)
|
|
65
|
+
usePageInventory({
|
|
66
|
+
websiteId: website,
|
|
67
|
+
period,
|
|
68
|
+
search: '',
|
|
69
|
+
sort: 'views',
|
|
70
|
+
})
|
|
71
|
+
useFunnelTemplates(website, period)
|
|
72
|
+
const [pages, templates] = calls
|
|
73
|
+
expect(pages?.url()).toBeNull()
|
|
74
|
+
expect(templates?.url()).toBeNull()
|
|
75
|
+
website.value = 'acme-shop'
|
|
76
|
+
expect(pages?.url()).toBeNull()
|
|
77
|
+
period.value = PERIOD
|
|
78
|
+
expect(pages?.url()).toBe(
|
|
79
|
+
'/api/marketing/stats/pages?from=2026-10-09T00:00:00.000Z&to=2026-10-15T23:59:59.999Z',
|
|
80
|
+
)
|
|
81
|
+
expect(query(pages)).toEqual({
|
|
82
|
+
website: 'acme-shop',
|
|
83
|
+
search: '',
|
|
84
|
+
sort: 'views',
|
|
85
|
+
})
|
|
86
|
+
expect(templates?.url()).toContain('/api/marketing/funnels/templates?from=')
|
|
87
|
+
expect(query(templates)).toEqual({ website: 'acme-shop' })
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
it('lists a website funnels oldest first, read again with the page refresh', () => {
|
|
91
|
+
useWebsiteFunnels('acme-shop')
|
|
92
|
+
const [funnels] = calls
|
|
93
|
+
expect(funnels?.url()).toBe('/api/marketing/tables/funnels/list')
|
|
94
|
+
expect(query(funnels)).toMatchObject({
|
|
95
|
+
filter_websiteId: 'is:acme-shop',
|
|
96
|
+
sortKey: 'createdAt',
|
|
97
|
+
sortDirection: 'asc',
|
|
98
|
+
})
|
|
99
|
+
// No trigger of its own: the page's refreshes read it again.
|
|
100
|
+
expect(funnels?.options.watch).toBeUndefined()
|
|
101
|
+
expect(funnels?.options.refreshOnPageRefresh).toBeUndefined()
|
|
102
|
+
const transform = funnels?.options.transform as (value: unknown) => unknown
|
|
103
|
+
expect(transform({ results: [{ _id: 'f1' }], total: 1 })).toEqual([
|
|
104
|
+
{ _id: 'f1' },
|
|
105
|
+
])
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
it('checks a website connection since a server time, once there is a website', () => {
|
|
109
|
+
const website = ref('')
|
|
110
|
+
const since = ref<number | null>(null)
|
|
111
|
+
useWebsiteConnection(website, since)
|
|
112
|
+
const [connection] = calls
|
|
113
|
+
expect(connection?.url()).toBeNull()
|
|
114
|
+
website.value = 'shop/eu'
|
|
115
|
+
expect(connection?.url()).toBe(
|
|
116
|
+
'/api/marketing/websites/shop%2Feu/connection',
|
|
117
|
+
)
|
|
118
|
+
expect(query(connection)).toEqual({ since: undefined })
|
|
119
|
+
since.value = 1_700_000_000_000
|
|
120
|
+
expect(query(connection)).toEqual({ since: 1_700_000_000_000 })
|
|
121
|
+
})
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
describe('useMarketingNow', () => {
|
|
125
|
+
it('starts from the time the server rendered at, handed to the browser', async () => {
|
|
126
|
+
const state = new Map<string, { value: unknown }>()
|
|
127
|
+
const transferred: string[] = []
|
|
128
|
+
vi.stubGlobal(
|
|
129
|
+
'useDmsState',
|
|
130
|
+
(key: string, init: () => unknown, options?: { transfer?: boolean }) => {
|
|
131
|
+
if (options?.transfer) {
|
|
132
|
+
transferred.push(key)
|
|
133
|
+
}
|
|
134
|
+
if (!state.has(key)) {
|
|
135
|
+
state.set(key, ref(init()))
|
|
136
|
+
}
|
|
137
|
+
return state.get(key)
|
|
138
|
+
},
|
|
139
|
+
)
|
|
140
|
+
state.set('marketing-rendered-at', ref(1_000))
|
|
141
|
+
const app = createSSRApp(
|
|
142
|
+
defineComponent({
|
|
143
|
+
setup: () => {
|
|
144
|
+
const now = useMarketingNow(15_000)
|
|
145
|
+
return () => h('span', String(now.value))
|
|
146
|
+
},
|
|
147
|
+
}),
|
|
148
|
+
)
|
|
149
|
+
expect(await renderToString(app)).toBe('<span>1000</span>')
|
|
150
|
+
expect(transferred).toEqual(['marketing-rendered-at'])
|
|
151
|
+
})
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
describe('the reads with triggers of their own', () => {
|
|
155
|
+
it('reads the context bar on its own clock, not with the page', () => {
|
|
156
|
+
useMarketingContextRead({ refreshInterval: 60_000 })
|
|
157
|
+
const [context] = calls
|
|
158
|
+
expect(context?.options).toMatchObject({
|
|
159
|
+
refreshOnPageRefresh: false,
|
|
160
|
+
refreshInterval: 60_000,
|
|
161
|
+
})
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
it('polls the connection check when asked to', () => {
|
|
165
|
+
useWebsiteConnection('acme-shop', null, { refreshInterval: 4000 })
|
|
166
|
+
const [connection] = calls
|
|
167
|
+
expect(connection?.options.refreshInterval).toBe(4000)
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
it('follows a reactive polling period, which stops the check at 0', () => {
|
|
171
|
+
const interval = ref(4000)
|
|
172
|
+
useWebsiteConnection('acme-shop', null, { refreshInterval: interval })
|
|
173
|
+
// One read: the polling is the check's own interval, not a second caller.
|
|
174
|
+
expect(calls).toHaveLength(1)
|
|
175
|
+
const [connection] = calls
|
|
176
|
+
const polling = () => toValue(connection?.options.refreshInterval as number)
|
|
177
|
+
expect(polling()).toBe(4000)
|
|
178
|
+
interval.value = 0
|
|
179
|
+
expect(polling()).toBe(0)
|
|
180
|
+
})
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
describe('the page preview', () => {
|
|
184
|
+
it('reads the clicks in the browser only, once there is a site and a path', () => {
|
|
185
|
+
const website = ref<string | undefined>(undefined)
|
|
186
|
+
const period = ref<typeof PERIOD | null>(null)
|
|
187
|
+
usePageHeatmap({ websiteId: website, path: '/pricing', period })
|
|
188
|
+
const [heatmap] = calls
|
|
189
|
+
expect(heatmap?.options.server).toBe(false)
|
|
190
|
+
expect(heatmap?.url()).toBeNull()
|
|
191
|
+
website.value = 'acme-shop'
|
|
192
|
+
// Without a period, the route's default window.
|
|
193
|
+
expect(heatmap?.url()).toBe('/api/marketing/stats/heatmap')
|
|
194
|
+
period.value = PERIOD
|
|
195
|
+
expect(heatmap?.url()).toBe(
|
|
196
|
+
'/api/marketing/stats/heatmap?from=2026-10-09T00:00:00.000Z&to=2026-10-15T23:59:59.999Z',
|
|
197
|
+
)
|
|
198
|
+
expect(query(heatmap)).toEqual({ website: 'acme-shop', path: '/pricing' })
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
it('reads no capture while the site keeps none', () => {
|
|
202
|
+
const enabled = ref(false)
|
|
203
|
+
usePageSnapshot({
|
|
204
|
+
websiteId: 'acme-shop',
|
|
205
|
+
path: '/pricing',
|
|
206
|
+
layout: 'phone',
|
|
207
|
+
enabled,
|
|
208
|
+
})
|
|
209
|
+
const [snapshot] = calls
|
|
210
|
+
expect(snapshot?.url()).toBeNull()
|
|
211
|
+
enabled.value = true
|
|
212
|
+
expect(snapshot?.url()).toBe('/api/marketing/stats/snapshot')
|
|
213
|
+
expect(query(snapshot)).toEqual({
|
|
214
|
+
website: 'acme-shop',
|
|
215
|
+
path: '/pricing',
|
|
216
|
+
layout: 'phone',
|
|
217
|
+
})
|
|
218
|
+
})
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
describe('the reads a dialog makes when it asks', () => {
|
|
222
|
+
it('reads nothing before, then answers or rejects with the read', async () => {
|
|
223
|
+
const data = ref<unknown>(null)
|
|
224
|
+
const error = ref<unknown>(null)
|
|
225
|
+
const execute = vi.fn(async () => {
|
|
226
|
+
data.value = { events: 3, snapshots: 1 }
|
|
227
|
+
})
|
|
228
|
+
vi.stubGlobal(
|
|
229
|
+
'useApiData',
|
|
230
|
+
(url: unknown, options: Record<string, unknown> = {}) => {
|
|
231
|
+
calls.push({ url: () => toValue(url as () => string | null), options })
|
|
232
|
+
return { data, error, execute }
|
|
233
|
+
},
|
|
234
|
+
)
|
|
235
|
+
const countHeatmap = useHeatmapCountRead('acme-shop', '/pricing')
|
|
236
|
+
const [count] = calls
|
|
237
|
+
expect(count?.options).toMatchObject({
|
|
238
|
+
immediate: false,
|
|
239
|
+
refreshOnPageRefresh: false,
|
|
240
|
+
})
|
|
241
|
+
expect(execute).not.toHaveBeenCalled()
|
|
242
|
+
expect(count?.url()).toBe('/api/marketing/stats/heatmap/count')
|
|
243
|
+
expect(query(count)).toEqual({ website: 'acme-shop', path: '/pricing' })
|
|
244
|
+
await expect(countHeatmap()).resolves.toEqual({ events: 3, snapshots: 1 })
|
|
245
|
+
|
|
246
|
+
const failure = new Error('gone')
|
|
247
|
+
execute.mockImplementationOnce(async () => {
|
|
248
|
+
error.value = failure
|
|
249
|
+
})
|
|
250
|
+
await expect(countHeatmap()).rejects.toBe(failure)
|
|
251
|
+
})
|
|
252
|
+
})
|
|
@@ -22,10 +22,6 @@ const BACKEND_ADDRESSED = [
|
|
|
22
22
|
'ExperimentInput',
|
|
23
23
|
]
|
|
24
24
|
|
|
25
|
-
it('declares the DmsMarketing prefix', () => {
|
|
26
|
-
expect(frontendModule.componentPrefix).toBe('DmsMarketing')
|
|
27
|
-
})
|
|
28
|
-
|
|
29
25
|
it('registers every backend-addressed component, once, without the prefix', async () => {
|
|
30
26
|
const registerComponent = vi.fn()
|
|
31
27
|
const registerPlugin = vi.fn()
|
|
@@ -14,7 +14,9 @@ export default defineConfig({
|
|
|
14
14
|
},
|
|
15
15
|
},
|
|
16
16
|
test: {
|
|
17
|
-
|
|
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
|
+
"version": "0.5.0",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"@antelopejs/interface-data-api": ">=0.2.0 <1.0.0",
|
|
35
35
|
"@antelopejs/interface-database": ">=0.1.8 <1.0.0",
|
|
36
36
|
"@antelopejs/interface-database-decorators": ">=0.1.7 <1.0.0",
|
|
37
|
-
"@antelopejs/interface-dms": ">=0.
|
|
37
|
+
"@antelopejs/interface-dms": ">=0.7.0 <1.0.0",
|
|
38
38
|
"@antelopejs/interface-dms-marketing": ">=0.4.0 <0.5.0",
|
|
39
39
|
"defu": "^6.1.4",
|
|
40
40
|
"maxmind": "^5.0.7",
|
|
@@ -76,7 +76,7 @@
|
|
|
76
76
|
"scripts": {
|
|
77
77
|
"build": "pnpm --filter @antelopejs/interface-dms-marketing build && rimraf dist && tsc",
|
|
78
78
|
"test": "pnpm run test:backend && pnpm run test:layer",
|
|
79
|
-
"test:backend": "pnpm run build && ajs module test .",
|
|
79
|
+
"test:backend": "([ -n \"$DMS_PREBUILT\" ] || pnpm run build) && ajs module test .",
|
|
80
80
|
"test:layer": "cd frontend-vue && pnpm test",
|
|
81
81
|
"dev": "ajs project run -w -p playground",
|
|
82
82
|
"frontend:dev": "cd playground && pnpm frontend:dev",
|