@antelopejs/dms-marketing 0.6.1 → 0.7.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.
- package/README.md +2 -2
- package/dist/config.d.ts +3 -0
- package/dist/config.js +11 -0
- package/dist/config.js.map +1 -1
- package/dist/data-api/websites.d.ts +10 -4
- package/dist/data-api/websites.js +79 -3
- package/dist/data-api/websites.js.map +1 -1
- package/dist/db/models/funnels.model.d.ts +0 -1
- package/dist/db/models/funnels.model.js +0 -18
- package/dist/db/models/funnels.model.js.map +1 -1
- package/dist/db/models/page_snapshots.model.d.ts +2 -1
- package/dist/db/models/page_snapshots.model.js +6 -14
- package/dist/db/models/page_snapshots.model.js.map +1 -1
- package/dist/db/models/websites.model.d.ts +1 -1
- package/dist/db/models/websites.model.js +9 -29
- package/dist/db/models/websites.model.js.map +1 -1
- package/dist/db/tables/funnels.table.d.ts +0 -1
- package/dist/db/tables/funnels.table.js +0 -4
- package/dist/db/tables/funnels.table.js.map +1 -1
- package/dist/db/tables/page_snapshots.table.d.ts +1 -1
- package/dist/db/tables/page_snapshots.table.js +4 -4
- package/dist/db/tables/page_snapshots.table.js.map +1 -1
- package/dist/db/tables/websites.table.d.ts +2 -1
- package/dist/db/tables/websites.table.js +4 -4
- package/dist/db/tables/websites.table.js.map +1 -1
- package/dist/frontend-module.d.ts +6 -0
- package/dist/frontend-module.js +46 -0
- package/dist/frontend-module.js.map +1 -0
- package/dist/implementations/dms-marketing/index.js +0 -5
- package/dist/implementations/dms-marketing/index.js.map +1 -1
- package/dist/index.js +3 -13
- package/dist/index.js.map +1 -1
- package/dist/pages/acquisition/page.d.ts +1 -0
- package/dist/pages/acquisition/page.js +9 -4
- package/dist/pages/acquisition/page.js.map +1 -1
- package/dist/pages/blocks.d.ts +11 -2
- package/dist/pages/blocks.js +43 -5
- package/dist/pages/blocks.js.map +1 -1
- package/dist/pages/funnel/page.d.ts +1 -0
- package/dist/pages/funnel/page.js +1 -0
- package/dist/pages/funnel/page.js.map +1 -1
- package/dist/pages/funnels/page.d.ts +1 -0
- package/dist/pages/funnels/page.js +4 -2
- package/dist/pages/funnels/page.js.map +1 -1
- package/dist/pages/overview/page.d.ts +1 -0
- package/dist/pages/overview/page.js +5 -6
- package/dist/pages/overview/page.js.map +1 -1
- package/dist/pages/pages/page.d.ts +1 -0
- package/dist/pages/pages/page.js +1 -0
- package/dist/pages/pages/page.js.map +1 -1
- package/dist/pages/settings/form.js +1 -0
- package/dist/pages/settings/form.js.map +1 -1
- package/dist/pages/websites/page.d.ts +2 -3
- package/dist/pages/websites/page.js +63 -5
- package/dist/pages/websites/page.js.map +1 -1
- package/dist/routes/dashboard-tracking.js +5 -1
- package/dist/routes/dashboard-tracking.js.map +1 -1
- package/dist/routes/funnels.d.ts +2 -0
- package/dist/routes/funnels.js +6 -5
- package/dist/routes/funnels.js.map +1 -1
- package/dist/routes/public-endpoints.js +1 -1
- package/dist/routes/public-endpoints.js.map +1 -1
- package/dist/routes/snapshot.d.ts +1 -1
- package/dist/routes/snapshot.js +30 -13
- package/dist/routes/snapshot.js.map +1 -1
- package/dist/routes/stats.d.ts +10 -4
- package/dist/routes/stats.js +77 -23
- package/dist/routes/stats.js.map +1 -1
- package/dist/routes/websites.d.ts +5 -1
- package/dist/routes/websites.js +31 -13
- package/dist/routes/websites.js.map +1 -1
- package/dist/services/acquisition-report.d.ts +1 -0
- package/dist/services/acquisition-report.js +1 -0
- package/dist/services/acquisition-report.js.map +1 -1
- package/dist/services/collect.js +7 -2
- package/dist/services/collect.js.map +1 -1
- package/dist/services/dashboard-tracking.d.ts +7 -14
- package/dist/services/dashboard-tracking.js +42 -26
- package/dist/services/dashboard-tracking.js.map +1 -1
- package/dist/services/declarations.d.ts +0 -2
- package/dist/services/declarations.js +0 -11
- package/dist/services/declarations.js.map +1 -1
- package/dist/services/declared-funnel.js +11 -99
- package/dist/services/declared-funnel.js.map +1 -1
- package/dist/services/declared-snapshot.js +8 -37
- package/dist/services/declared-snapshot.js.map +1 -1
- package/dist/services/declared-website.js +5 -46
- package/dist/services/declared-website.js.map +1 -1
- package/dist/services/funnels-table.d.ts +3 -2
- package/dist/services/funnels-table.js +9 -9
- package/dist/services/funnels-table.js.map +1 -1
- package/dist/services/heatmap.d.ts +6 -0
- package/dist/services/heatmap.js +82 -17
- package/dist/services/heatmap.js.map +1 -1
- package/dist/services/index.d.ts +1 -0
- package/dist/services/index.js +1 -0
- package/dist/services/index.js.map +1 -1
- package/dist/services/prune.js +6 -1
- package/dist/services/prune.js.map +1 -1
- package/dist/services/realtime.d.ts +3 -0
- package/dist/services/realtime.js +49 -0
- package/dist/services/realtime.js.map +1 -0
- package/dist/services/request-body.d.ts +1 -0
- package/dist/services/request-body.js +18 -0
- package/dist/services/request-body.js.map +1 -1
- package/dist/services/sessionize.d.ts +1 -0
- package/dist/services/sessionize.js +9 -0
- package/dist/services/sessionize.js.map +1 -1
- package/dist/services/snapshots.d.ts +29 -1
- package/dist/services/snapshots.js +151 -26
- package/dist/services/snapshots.js.map +1 -1
- package/dist/services/tenant-website.d.ts +11 -0
- package/dist/services/tenant-website.js +27 -5
- package/dist/services/tenant-website.js.map +1 -1
- package/dist/services/website-report.d.ts +5 -0
- package/dist/services/website-report.js +13 -1
- package/dist/services/website-report.js.map +1 -1
- package/dist/services/workspace-scope.d.ts +31 -0
- package/dist/services/workspace-scope.js +72 -0
- package/dist/services/workspace-scope.js.map +1 -0
- package/dist/types/constants.d.ts +7 -0
- package/dist/types/constants.js +8 -1
- package/dist/types/constants.js.map +1 -1
- package/dist/types/events.d.ts +25 -25
- package/dist/types/snapshots.d.ts +29 -29
- package/dist/types/snapshots.js +1 -1
- package/dist/types/snapshots.js.map +1 -1
- package/dist/utils/funnel-steps-type.d.ts +13 -13
- package/frontend-vue/app/components/ChannelsCard.vue +19 -2
- package/frontend-vue/app/components/Context.vue +50 -376
- package/frontend-vue/app/components/FunnelBuilder.vue +28 -22
- package/frontend-vue/app/components/FunnelFigure.vue +15 -2
- package/frontend-vue/app/components/FunnelReport.vue +3 -0
- package/frontend-vue/app/components/FunnelTemplates.vue +15 -13
- package/frontend-vue/app/components/InstallGuide.vue +1 -1
- package/frontend-vue/app/components/PagePreview.vue +330 -74
- package/frontend-vue/app/components/PagesExplorer.vue +230 -85
- package/frontend-vue/app/components/TopListTabs.vue +51 -18
- package/frontend-vue/app/components/WebsiteCard.vue +394 -0
- package/frontend-vue/app/components/WebsiteCreate.vue +159 -0
- package/frontend-vue/app/components/WebsitePicker.vue +210 -0
- package/frontend-vue/app/components/WebsiteSettings.vue +539 -0
- package/frontend-vue/app/components/WorkspaceScope.vue +119 -0
- package/frontend-vue/app/composables/useContextWebsite.ts +105 -0
- package/frontend-vue/app/composables/useCopyDraft.ts +47 -0
- package/frontend-vue/app/composables/useMarketingApi.ts +92 -4
- package/frontend-vue/app/composables/useMarketingReads.ts +32 -23
- package/frontend-vue/app/composables/useMarketingRoutes.ts +25 -2
- package/frontend-vue/app/composables/useRecordCrumb.ts +17 -0
- package/frontend-vue/app/composables/useSnapshotFrame.ts +35 -1
- package/frontend-vue/app/utils/anchor-drift.ts +58 -0
- package/frontend-vue/app/utils/drift-dismissals.ts +20 -0
- package/frontend-vue/app/utils/fill-height.ts +141 -0
- package/frontend-vue/app/utils/format.ts +21 -2
- package/frontend-vue/app/utils/funnel-draft.ts +22 -0
- package/frontend-vue/app/utils/heatmap.ts +17 -0
- package/frontend-vue/app/utils/page-snapshot.ts +75 -0
- package/frontend-vue/app/utils/snapshot-options.ts +148 -0
- package/frontend-vue/app/utils/snapshotScrollRoot.ts +93 -0
- package/frontend-vue/app/utils/website-card.ts +93 -0
- package/frontend-vue/app/utils/website-purge.ts +95 -0
- package/frontend-vue/app/utils/website-settings.ts +121 -0
- package/frontend-vue/app/utils/website-state.ts +93 -0
- package/frontend-vue/app/utils/workspace-scope.ts +51 -0
- package/frontend-vue/i18n/locales/marketing-en-GB.json +112 -36
- package/frontend-vue/i18n/locales/marketing-fr-FR.json +112 -36
- package/frontend-vue/package.json +1 -1
- package/frontend-vue/tests/anchor-drift.test.ts +59 -0
- package/frontend-vue/tests/build-flags.test.ts +38 -0
- package/frontend-vue/tests/context-bar.test.ts +226 -0
- package/frontend-vue/tests/copy-draft.test.ts +64 -0
- package/frontend-vue/tests/drift-dismissals.test.ts +12 -0
- package/frontend-vue/tests/fill-height.test.ts +110 -0
- package/frontend-vue/tests/format.test.ts +14 -0
- package/frontend-vue/tests/funnel-draft.test.ts +37 -0
- package/frontend-vue/tests/heatmap.test.ts +25 -0
- package/frontend-vue/tests/install-guide.test.ts +25 -0
- package/frontend-vue/tests/marketing-reads.test.ts +20 -6
- package/frontend-vue/tests/page-snapshot.test.ts +62 -0
- package/frontend-vue/tests/record-crumb.test.ts +43 -0
- package/frontend-vue/tests/registration.test.ts +4 -1
- package/frontend-vue/tests/routes.test.ts +21 -0
- package/frontend-vue/tests/save-funnel.test.ts +47 -0
- package/frontend-vue/tests/server-parity.test.ts +3 -0
- package/frontend-vue/tests/snapshot-options.test.ts +148 -0
- package/frontend-vue/tests/snapshot-scroll-root.test.ts +84 -0
- package/frontend-vue/tests/stubs/frontend-module.ts +5 -1
- package/frontend-vue/tests/top-list-tabs.test.ts +158 -0
- package/frontend-vue/tests/website-card.test.ts +119 -0
- package/frontend-vue/tests/website-purge.test.ts +70 -0
- package/frontend-vue/tests/website-settings.test.ts +133 -0
- package/frontend-vue/tests/website-state.test.ts +96 -0
- package/frontend-vue/tests/workspace-scope-control.test.ts +113 -0
- package/frontend-vue/tests/workspace-scope.test.ts +55 -0
- package/package.json +8 -7
- package/skills/dms-marketing/SKILL.md +54 -12
- package/static/snapshot.js +539 -48
- package/static/tracker.js +154 -5
- package/dist/services/readiness.d.ts +0 -2
- package/dist/services/readiness.js +0 -15
- package/dist/services/readiness.js.map +0 -1
- package/frontend-vue/app/components/WebsitesGrid.vue +0 -812
|
@@ -0,0 +1,148 @@
|
|
|
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
|
+
import {
|
|
5
|
+
changesSnapshotOptions,
|
|
6
|
+
confirmSnapshotChange,
|
|
7
|
+
snapshotDiscardConfirmation,
|
|
8
|
+
snapshotOptionItems,
|
|
9
|
+
} from '../app/utils/snapshot-options'
|
|
10
|
+
|
|
11
|
+
/** A `t` that shows which key, count and plural form were asked for. */
|
|
12
|
+
function t(key: string, named: Record<string, unknown> = {}, plural?: number) {
|
|
13
|
+
const name = key.split('.').at(-1)
|
|
14
|
+
return plural === undefined ? `${name}` : `${name}:${named.count}/${plural}`
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
describe('snapshot options', () => {
|
|
18
|
+
const site = { snapshotsEnabled: true, snapshotMaskText: false }
|
|
19
|
+
|
|
20
|
+
it('names the changes the server discards snapshots on', () => {
|
|
21
|
+
expect(changesSnapshotOptions(site, { snapshotsEnabled: false })).toBe(true)
|
|
22
|
+
expect(changesSnapshotOptions(site, { snapshotMaskText: true })).toBe(true)
|
|
23
|
+
// Unchanged values and absent keys discard nothing.
|
|
24
|
+
expect(changesSnapshotOptions(site, { snapshotsEnabled: true })).toBe(false)
|
|
25
|
+
expect(changesSnapshotOptions(site, { snapshotMaskText: false })).toBe(
|
|
26
|
+
false,
|
|
27
|
+
)
|
|
28
|
+
expect(changesSnapshotOptions(site, {})).toBe(false)
|
|
29
|
+
// A site saved before the mask existed has it off.
|
|
30
|
+
expect(
|
|
31
|
+
changesSnapshotOptions(
|
|
32
|
+
{ snapshotsEnabled: true },
|
|
33
|
+
{ snapshotMaskText: false },
|
|
34
|
+
),
|
|
35
|
+
).toBe(false)
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('asks with the count, or without it when it could not be read', () => {
|
|
39
|
+
expect(snapshotDiscardConfirmation(t, 12)).toMatchObject({
|
|
40
|
+
title: 'title',
|
|
41
|
+
description: 'description:12/12',
|
|
42
|
+
confirmLabel: 'confirm',
|
|
43
|
+
cancelLabel: 'cancel',
|
|
44
|
+
color: 'warning',
|
|
45
|
+
})
|
|
46
|
+
expect(snapshotDiscardConfirmation(t, null).description).toBe(
|
|
47
|
+
'description_unknown',
|
|
48
|
+
)
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('says in both languages that clicks and scroll depth are kept', () => {
|
|
52
|
+
const en_ = en.page.marketing.websites.discard_confirm
|
|
53
|
+
const fr_ = fr.page.marketing.websites.discard_confirm
|
|
54
|
+
for (const text of [en_.description, en_.description_unknown]) {
|
|
55
|
+
expect(text).toMatch(/Clicks and scroll depth are kept/)
|
|
56
|
+
}
|
|
57
|
+
for (const text of [fr_.description, fr_.description_unknown]) {
|
|
58
|
+
expect(text).toMatch(
|
|
59
|
+
/clics et la profondeur de défilement sont conservés/,
|
|
60
|
+
)
|
|
61
|
+
}
|
|
62
|
+
// Singular | plural, the count in the plural.
|
|
63
|
+
expect(en_.description.split(' | ')[1]).toContain('{count}')
|
|
64
|
+
expect(fr_.description.split(' | ')[1]).toContain('{count}')
|
|
65
|
+
})
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
describe('the heatmap menu snapshot checkboxes', () => {
|
|
69
|
+
const site = { snapshotsEnabled: true, snapshotMaskText: false }
|
|
70
|
+
|
|
71
|
+
/** The page's flow, as PagePreview wires it: ask, then save or not. */
|
|
72
|
+
function menu(
|
|
73
|
+
website: { snapshotsEnabled: boolean; snapshotMaskText: boolean },
|
|
74
|
+
answer: boolean,
|
|
75
|
+
count: number | null = 3,
|
|
76
|
+
) {
|
|
77
|
+
const asked: string[] = []
|
|
78
|
+
const saved: Array<Record<string, boolean>> = []
|
|
79
|
+
const settled: Array<Promise<void>> = []
|
|
80
|
+
const items = snapshotOptionItems(
|
|
81
|
+
website,
|
|
82
|
+
(patch) =>
|
|
83
|
+
void settled.push(
|
|
84
|
+
confirmSnapshotChange(website, patch, {
|
|
85
|
+
count: async () => count,
|
|
86
|
+
confirm: async (options) => {
|
|
87
|
+
asked.push(options.title)
|
|
88
|
+
return answer
|
|
89
|
+
},
|
|
90
|
+
t,
|
|
91
|
+
}).then((go) => void (go && saved.push(patch))),
|
|
92
|
+
),
|
|
93
|
+
t,
|
|
94
|
+
)
|
|
95
|
+
return { items, asked, saved, settled }
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
it('asks before "Mask all text" and saves nothing on Cancel', async () => {
|
|
99
|
+
const { items, asked, saved, settled } = menu(site, false)
|
|
100
|
+
const mask = items[1]!
|
|
101
|
+
expect(mask.checked).toBe(false)
|
|
102
|
+
mask.onUpdateChecked(true)
|
|
103
|
+
await Promise.all(settled)
|
|
104
|
+
expect(asked).toEqual(['title'])
|
|
105
|
+
expect(saved).toEqual([])
|
|
106
|
+
// Rebuilt from the website, which nothing changed: still unchecked.
|
|
107
|
+
expect(snapshotOptionItems(site, () => {}, t)[1]!.checked).toBe(false)
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
it('asks even when the site holds no snapshot yet', async () => {
|
|
111
|
+
const { items, asked, saved, settled } = menu(site, true, 0)
|
|
112
|
+
items[1]!.onUpdateChecked(true)
|
|
113
|
+
items[0]!.onUpdateChecked(false)
|
|
114
|
+
await Promise.all(settled)
|
|
115
|
+
expect(asked).toEqual(['title_none', 'title_none'])
|
|
116
|
+
expect(saved).toEqual([
|
|
117
|
+
{ snapshotMaskText: true },
|
|
118
|
+
{ snapshotsEnabled: false },
|
|
119
|
+
])
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
it('turns snapshots on without asking when there are none', async () => {
|
|
123
|
+
const off = { snapshotsEnabled: false, snapshotMaskText: false }
|
|
124
|
+
const { items, asked, saved, settled } = menu(off, false, 0)
|
|
125
|
+
expect(items[1]!.disabled).toBe(true)
|
|
126
|
+
items[0]!.onUpdateChecked(true)
|
|
127
|
+
await Promise.all(settled)
|
|
128
|
+
expect(asked).toEqual([])
|
|
129
|
+
expect(saved).toEqual([{ snapshotsEnabled: true }])
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
it('asks without a number when the count fails', async () => {
|
|
133
|
+
let description = ''
|
|
134
|
+
await confirmSnapshotChange(
|
|
135
|
+
site,
|
|
136
|
+
{ snapshotMaskText: true },
|
|
137
|
+
{
|
|
138
|
+
count: () => Promise.reject(new Error('offline')),
|
|
139
|
+
confirm: async (options) => {
|
|
140
|
+
description = options.description
|
|
141
|
+
return false
|
|
142
|
+
},
|
|
143
|
+
t,
|
|
144
|
+
},
|
|
145
|
+
)
|
|
146
|
+
expect(description).toBe('description_unknown')
|
|
147
|
+
})
|
|
148
|
+
})
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import {
|
|
3
|
+
unrollScrollRoot,
|
|
4
|
+
type UnrollElement,
|
|
5
|
+
type UnrollStyle,
|
|
6
|
+
} from '../app/utils/snapshotScrollRoot'
|
|
7
|
+
|
|
8
|
+
interface FakeElement extends UnrollElement {
|
|
9
|
+
name: string
|
|
10
|
+
parentElement: FakeElement | null
|
|
11
|
+
computed: UnrollStyle
|
|
12
|
+
set: Record<string, string>
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function element(
|
|
16
|
+
name: string,
|
|
17
|
+
parent: FakeElement | null,
|
|
18
|
+
computed: Partial<UnrollStyle> = {},
|
|
19
|
+
): FakeElement {
|
|
20
|
+
const set: Record<string, string> = {}
|
|
21
|
+
return {
|
|
22
|
+
name,
|
|
23
|
+
parentElement: parent,
|
|
24
|
+
computed: {
|
|
25
|
+
position: 'static',
|
|
26
|
+
overflowX: 'visible',
|
|
27
|
+
display: 'block',
|
|
28
|
+
flexDirection: 'row',
|
|
29
|
+
...computed,
|
|
30
|
+
},
|
|
31
|
+
set,
|
|
32
|
+
style: {
|
|
33
|
+
setProperty(property, value, priority) {
|
|
34
|
+
expect(priority).toBe('important')
|
|
35
|
+
set[property] = value
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
describe('snapshot scroll root', () => {
|
|
42
|
+
// The dashboard shape: a fixed shell, a sidebar row, a column panel whose
|
|
43
|
+
// body scrolls.
|
|
44
|
+
const html = element('html', null)
|
|
45
|
+
const body = element('body', html, { overflowX: 'hidden' })
|
|
46
|
+
const shell = element('shell', body, {
|
|
47
|
+
position: 'fixed',
|
|
48
|
+
overflowX: 'hidden',
|
|
49
|
+
display: 'flex',
|
|
50
|
+
})
|
|
51
|
+
const panel = element('panel', shell, {
|
|
52
|
+
display: 'flex',
|
|
53
|
+
flexDirection: 'column',
|
|
54
|
+
})
|
|
55
|
+
const scroller = element('scroller', panel, { overflowX: 'hidden' })
|
|
56
|
+
unrollScrollRoot(scroller, (node) => node.computed)
|
|
57
|
+
|
|
58
|
+
it('lets the scroll root and every ancestor grow with the content', () => {
|
|
59
|
+
for (const node of [scroller, panel, shell, body, html]) {
|
|
60
|
+
expect(node.set.height, node.name).toBe('auto')
|
|
61
|
+
expect(node.set['max-height'], node.name).toBe('none')
|
|
62
|
+
expect(node.set['overflow-y'], node.name).toBe('visible')
|
|
63
|
+
}
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
it('keeps a horizontal clip without making a scroll container', () => {
|
|
67
|
+
expect(scroller.set['overflow-x']).toBe('clip')
|
|
68
|
+
expect(body.set['overflow-x']).toBe('clip')
|
|
69
|
+
expect(panel.set['overflow-x']).toBeUndefined()
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
it('puts a fixed shell back in the document, unpinned at the bottom', () => {
|
|
73
|
+
expect(shell.set.position).toBe('absolute')
|
|
74
|
+
expect(shell.set.bottom).toBe('auto')
|
|
75
|
+
expect(panel.set.position).toBeUndefined()
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
it('touches the flex basis in a column only', () => {
|
|
79
|
+
expect(scroller.set['flex-basis']).toBe('auto')
|
|
80
|
+
expect(scroller.set['flex-shrink']).toBe('0')
|
|
81
|
+
// The panel is an item of a row: its basis is a width.
|
|
82
|
+
expect(panel.set['flex-basis']).toBeUndefined()
|
|
83
|
+
})
|
|
84
|
+
})
|
|
@@ -20,7 +20,11 @@ export interface DmsFrontendSdk {
|
|
|
20
20
|
setup: DmsPluginSetup,
|
|
21
21
|
options?: { clientOnly?: boolean },
|
|
22
22
|
) => void
|
|
23
|
-
registerMiddleware: (
|
|
23
|
+
registerMiddleware: (
|
|
24
|
+
name: string,
|
|
25
|
+
handler: unknown,
|
|
26
|
+
options?: { global?: boolean },
|
|
27
|
+
) => void
|
|
24
28
|
provide: (key: string, value: unknown) => void
|
|
25
29
|
use: (plugin: Plugin) => void
|
|
26
30
|
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { createSSRApp, defineComponent, h, onServerPrefetch, ref } from 'vue'
|
|
2
|
+
import { renderToString } from 'vue/server-renderer'
|
|
3
|
+
import { beforeAll, describe, expect, it } from 'vitest'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The overview top lists as a server renders them: with their data, the
|
|
7
|
+
* rows of the first tab (a watcher never runs there once data lands, so
|
|
8
|
+
* the active tab cannot come from one); without, a skeleton the size of a
|
|
9
|
+
* full card.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
interface ChartState {
|
|
13
|
+
data: unknown
|
|
14
|
+
isLoading: boolean
|
|
15
|
+
error: unknown
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
let chart: ChartState = { data: null, isLoading: false, error: null }
|
|
19
|
+
|
|
20
|
+
const globals = globalThis as Record<string, unknown>
|
|
21
|
+
|
|
22
|
+
beforeAll(() => {
|
|
23
|
+
// As `useAsyncData` on a server: the answer lands in `serverPrefetch`,
|
|
24
|
+
// after setup, before the render.
|
|
25
|
+
globals.useChartFetch = () => {
|
|
26
|
+
const data = ref<unknown>(null)
|
|
27
|
+
const isLoading = ref(true)
|
|
28
|
+
const error = ref<unknown>(null)
|
|
29
|
+
onServerPrefetch(async () => {
|
|
30
|
+
data.value = chart.data
|
|
31
|
+
isLoading.value = chart.isLoading
|
|
32
|
+
error.value = chart.error
|
|
33
|
+
})
|
|
34
|
+
return { data, isLoading, error, refresh: async () => {} }
|
|
35
|
+
}
|
|
36
|
+
globals.useTranslation = () => ({ processI18n: (key: string) => key })
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
/** An auto-import the template reads: an app global, as Nuxt provides it. */
|
|
40
|
+
const formatPercentage = (value: number) => `${Math.round(value * 100)}%`
|
|
41
|
+
|
|
42
|
+
const slotStub = (tag: string) =>
|
|
43
|
+
defineComponent({
|
|
44
|
+
inheritAttrs: false,
|
|
45
|
+
setup:
|
|
46
|
+
(_, { slots, attrs }) =>
|
|
47
|
+
() =>
|
|
48
|
+
h(tag, { class: attrs.class }, [
|
|
49
|
+
String(attrs.title ?? attrs.label ?? ''),
|
|
50
|
+
slots.default?.(),
|
|
51
|
+
]),
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
const STUBS = {
|
|
55
|
+
DmsEyebrow: slotStub('p'),
|
|
56
|
+
DmsEmptyState: slotStub('div'),
|
|
57
|
+
DmsStatusPill: slotStub('span'),
|
|
58
|
+
DmsAutoLink: slotStub('a'),
|
|
59
|
+
UTabs: slotStub('div'),
|
|
60
|
+
UButton: slotStub('button'),
|
|
61
|
+
UIcon: slotStub('i'),
|
|
62
|
+
USkeleton: defineComponent({
|
|
63
|
+
inheritAttrs: false,
|
|
64
|
+
setup:
|
|
65
|
+
(_, { attrs }) =>
|
|
66
|
+
() =>
|
|
67
|
+
h('div', { class: ['skeleton', attrs.class] }),
|
|
68
|
+
}),
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function render(state: ChartState): Promise<string> {
|
|
72
|
+
chart = state
|
|
73
|
+
const { default: TopListTabs } =
|
|
74
|
+
await import('../app/components/TopListTabs.vue')
|
|
75
|
+
const app = createSSRApp(TopListTabs, {
|
|
76
|
+
fetchUrl: '/api/marketing/blocks/tops?group=content',
|
|
77
|
+
eyebrow: 'Content',
|
|
78
|
+
valueLabel: 'Views',
|
|
79
|
+
})
|
|
80
|
+
app.config.globalProperties.formatPercentage = formatPercentage
|
|
81
|
+
for (const [name, component] of Object.entries(STUBS)) {
|
|
82
|
+
app.component(name, component)
|
|
83
|
+
}
|
|
84
|
+
return renderToString(app)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const TABS = {
|
|
88
|
+
tabs: [
|
|
89
|
+
{
|
|
90
|
+
id: 'pages',
|
|
91
|
+
label: 'Top pages',
|
|
92
|
+
labelKind: 'raw',
|
|
93
|
+
total: 3,
|
|
94
|
+
items: [
|
|
95
|
+
{ id: '/home', title: '/home', value: 2, share: 66.7 },
|
|
96
|
+
{ id: '/pricing', title: '/pricing', value: 1, share: 33.3 },
|
|
97
|
+
],
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
id: 'entry',
|
|
101
|
+
label: 'Entry pages',
|
|
102
|
+
labelKind: 'raw',
|
|
103
|
+
total: 1,
|
|
104
|
+
items: [{ id: '/blog', title: '/blog', value: 1, share: 100 }],
|
|
105
|
+
},
|
|
106
|
+
],
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function count(html: string, text: string): number {
|
|
110
|
+
return html.split(text).length - 1
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
describe('TopListTabs server render', () => {
|
|
114
|
+
it('draws the first tab’s rows with the page, never its empty state', async () => {
|
|
115
|
+
const html = await render({ data: TABS, isLoading: false, error: null })
|
|
116
|
+
expect(html).toContain('/home')
|
|
117
|
+
expect(html).toContain('/pricing')
|
|
118
|
+
expect(html).not.toContain('/blog')
|
|
119
|
+
expect(html).not.toContain('page.marketing.overview.groups.empty')
|
|
120
|
+
expect(html).not.toContain('skeleton')
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
it('keeps the rows on screen while a newer read loads', async () => {
|
|
124
|
+
const html = await render({ data: TABS, isLoading: true, error: null })
|
|
125
|
+
expect(html).toContain('/home')
|
|
126
|
+
expect(html).not.toContain('skeleton')
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
it('draws a full card of row-sized skeletons while nothing is there yet', async () => {
|
|
130
|
+
const html = await render({ data: null, isLoading: true, error: null })
|
|
131
|
+
// Eight rows (the route's limit), each on a list row's grid and padding.
|
|
132
|
+
expect(
|
|
133
|
+
count(
|
|
134
|
+
html,
|
|
135
|
+
'grid grid-cols-[1.5rem_minmax(0,1fr)_auto_3rem] items-center gap-x-3 px-4 py-2',
|
|
136
|
+
),
|
|
137
|
+
).toBe(8)
|
|
138
|
+
// The label line keeps a row's text height, its share bar under it.
|
|
139
|
+
expect(count(html, 'flex h-[19.5px] items-center')).toBe(8)
|
|
140
|
+
expect(count(html, 'mt-1 h-0.5')).toBe(8)
|
|
141
|
+
// The tab strip's place, at a tab's height, never wrapping the header.
|
|
142
|
+
expect(count(html, 'class="px-2 py-3"')).toBe(3)
|
|
143
|
+
expect(html).toContain(
|
|
144
|
+
'overflow-hidden sm:order-none sm:mx-0 sm:flex-1 sm:basis-0',
|
|
145
|
+
)
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
it('shows a failed read as an error, not as a skeleton or an empty list', async () => {
|
|
149
|
+
const html = await render({
|
|
150
|
+
data: null,
|
|
151
|
+
isLoading: false,
|
|
152
|
+
error: new Error('down'),
|
|
153
|
+
})
|
|
154
|
+
expect(html).toContain('page.marketing.common.load_error')
|
|
155
|
+
expect(html).not.toContain('skeleton')
|
|
156
|
+
expect(html).not.toContain('page.marketing.overview.groups.empty')
|
|
157
|
+
})
|
|
158
|
+
})
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { websiteCardOf } from '../app/utils/website-card'
|
|
3
|
+
import {
|
|
4
|
+
WEBSITE_LIVE_WINDOW_MS,
|
|
5
|
+
websiteDisplayState,
|
|
6
|
+
} from '../app/utils/website-state'
|
|
7
|
+
|
|
8
|
+
/** A row as the websites table lists it: stored fields, card merged in. */
|
|
9
|
+
function listedRow(overrides: Record<string, unknown> = {}) {
|
|
10
|
+
return {
|
|
11
|
+
_id: 'site-1',
|
|
12
|
+
name: 'Shop',
|
|
13
|
+
domain: 'shop.example.com',
|
|
14
|
+
trackingEnabled: true,
|
|
15
|
+
snapshotsEnabled: true,
|
|
16
|
+
createdAt: Date.UTC(2026, 8, 1),
|
|
17
|
+
id: 'site-1',
|
|
18
|
+
state: 'live',
|
|
19
|
+
lastActivityAt: Date.UTC(2026, 9, 17),
|
|
20
|
+
extraDomains: ['staging.example.com'],
|
|
21
|
+
snapshotMaskText: true,
|
|
22
|
+
sessions: 120,
|
|
23
|
+
pages: 14,
|
|
24
|
+
funnels: 2,
|
|
25
|
+
snapshots: 5,
|
|
26
|
+
sampleRate: 0.25,
|
|
27
|
+
rejected: [{ hostname: 'evil.test', count: 3, lastAt: 1 }],
|
|
28
|
+
...overrides,
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
describe('websiteCardOf', () => {
|
|
33
|
+
it('reads a listed row into the card it draws', () => {
|
|
34
|
+
expect(websiteCardOf(listedRow())).toEqual({
|
|
35
|
+
id: 'site-1',
|
|
36
|
+
name: 'Shop',
|
|
37
|
+
domain: 'shop.example.com',
|
|
38
|
+
state: 'live',
|
|
39
|
+
lastActivityAt: Date.UTC(2026, 9, 17),
|
|
40
|
+
extraDomains: ['staging.example.com'],
|
|
41
|
+
snapshotsEnabled: true,
|
|
42
|
+
snapshotMaskText: true,
|
|
43
|
+
createdAt: Date.UTC(2026, 8, 1),
|
|
44
|
+
sessions: 120,
|
|
45
|
+
pages: 14,
|
|
46
|
+
funnels: 2,
|
|
47
|
+
snapshots: 5,
|
|
48
|
+
sampleRate: 0.25,
|
|
49
|
+
rejected: [{ hostname: 'evil.test', count: 3, lastAt: 1 }],
|
|
50
|
+
dashboard: false,
|
|
51
|
+
})
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('takes the id of the row and reads an ISO date', () => {
|
|
55
|
+
const card = websiteCardOf(
|
|
56
|
+
listedRow({ id: undefined, createdAt: '2026-09-01T00:00:00.000Z' }),
|
|
57
|
+
)
|
|
58
|
+
expect(card.id).toBe('site-1')
|
|
59
|
+
expect(card.createdAt).toBe(Date.UTC(2026, 8, 1))
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('defaults a row read without its card', () => {
|
|
63
|
+
const card = websiteCardOf({
|
|
64
|
+
_id: 'site-2',
|
|
65
|
+
name: 'Blog',
|
|
66
|
+
domain: 'blog.example.com',
|
|
67
|
+
trackingEnabled: false,
|
|
68
|
+
})
|
|
69
|
+
expect(card).toMatchObject({
|
|
70
|
+
id: 'site-2',
|
|
71
|
+
state: 'paused',
|
|
72
|
+
lastActivityAt: null,
|
|
73
|
+
extraDomains: [],
|
|
74
|
+
snapshotsEnabled: false,
|
|
75
|
+
snapshotMaskText: false,
|
|
76
|
+
sessions: 0,
|
|
77
|
+
snapshots: 0,
|
|
78
|
+
rejected: [],
|
|
79
|
+
dashboard: false,
|
|
80
|
+
})
|
|
81
|
+
expect(websiteCardOf({ _id: 'site-3' }).state).toBe('waiting')
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it("flags the dashboard's own site only when the backend says so", () => {
|
|
85
|
+
expect(websiteCardOf(listedRow({ dashboard: true })).dashboard).toBe(true)
|
|
86
|
+
expect(websiteCardOf(listedRow({ dashboard: 'yes' })).dashboard).toBe(false)
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
it('drops what is not a host or a refused source', () => {
|
|
90
|
+
const card = websiteCardOf(
|
|
91
|
+
listedRow({
|
|
92
|
+
extraDomains: ['a.example.com', 3, null],
|
|
93
|
+
rejected: [{ hostname: 'b.test', count: 1, lastAt: 1 }, 'x', null],
|
|
94
|
+
sessions: Number.NaN,
|
|
95
|
+
}),
|
|
96
|
+
)
|
|
97
|
+
expect(card.extraDomains).toEqual(['a.example.com'])
|
|
98
|
+
expect(card.rejected).toHaveLength(1)
|
|
99
|
+
expect(card.sessions).toBe(0)
|
|
100
|
+
})
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
// QA m10: a card read off the table shows the same state as everywhere else,
|
|
104
|
+
// not the backend's sticky \`live\`.
|
|
105
|
+
describe('a listed card', () => {
|
|
106
|
+
const lastActivityAt = Date.UTC(2026, 9, 17, 12)
|
|
107
|
+
|
|
108
|
+
it('is not shown live once its last event is over 5 minutes old', () => {
|
|
109
|
+
const card = websiteCardOf(listedRow({ lastActivityAt }))
|
|
110
|
+
const later = lastActivityAt + WEBSITE_LIVE_WINDOW_MS + 1
|
|
111
|
+
expect(card.state).toBe('live')
|
|
112
|
+
expect(websiteDisplayState(card, later)).toBe('idle')
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
it('is shown live while its last event is recent', () => {
|
|
116
|
+
const card = websiteCardOf(listedRow({ lastActivityAt }))
|
|
117
|
+
expect(websiteDisplayState(card, lastActivityAt + 60_000)).toBe('live')
|
|
118
|
+
})
|
|
119
|
+
})
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import type { MarketingWebsiteDeletionImpact } from '../app/composables/useMarketingApi'
|
|
3
|
+
import {
|
|
4
|
+
websitePurgeImpact,
|
|
5
|
+
websitePurgeResult,
|
|
6
|
+
} from '../app/utils/website-purge'
|
|
7
|
+
|
|
8
|
+
const t = (key: string, named?: Record<string, unknown>) =>
|
|
9
|
+
named ? `${key} ${JSON.stringify(named)}` : key
|
|
10
|
+
|
|
11
|
+
const IMPACT: MarketingWebsiteDeletionImpact = {
|
|
12
|
+
sessions: 1200,
|
|
13
|
+
pageviews: 3400,
|
|
14
|
+
since: null,
|
|
15
|
+
funnels: 2,
|
|
16
|
+
snapshots: 3,
|
|
17
|
+
heatmapEvents: 560,
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
describe('a purge confirmation', () => {
|
|
21
|
+
it('counts the clicks, scrolls and snapshots of the heatmaps, and keeps the traffic', () => {
|
|
22
|
+
const lines = websitePurgeImpact('heatmaps', IMPACT, t, 'en-GB')
|
|
23
|
+
expect(lines.map((line) => line.count)).toEqual(['560', '3', undefined])
|
|
24
|
+
expect(lines.at(-1)?.label).toBe(
|
|
25
|
+
'page.marketing.websites.purge.kept_traffic',
|
|
26
|
+
)
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('counts the traffic too for all the data, since the oldest day', () => {
|
|
30
|
+
const lines = websitePurgeImpact(
|
|
31
|
+
'all',
|
|
32
|
+
{ ...IMPACT, since: Date.UTC(2026, 0, 5) },
|
|
33
|
+
t,
|
|
34
|
+
'en-GB',
|
|
35
|
+
)
|
|
36
|
+
expect(lines.map((line) => line.count)).toEqual([
|
|
37
|
+
'1,200',
|
|
38
|
+
'3,400',
|
|
39
|
+
'560',
|
|
40
|
+
'3',
|
|
41
|
+
undefined,
|
|
42
|
+
])
|
|
43
|
+
expect(lines[0]?.label).toContain(
|
|
44
|
+
'page.marketing.websites.delete.sessions_since',
|
|
45
|
+
)
|
|
46
|
+
expect(lines.at(-1)?.label).toBe('page.marketing.websites.purge.kept')
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('says what a purge deleted, in its scope', () => {
|
|
50
|
+
const deleted = {
|
|
51
|
+
events: 9,
|
|
52
|
+
sessions: 2,
|
|
53
|
+
days: 1,
|
|
54
|
+
snapshots: 0,
|
|
55
|
+
imports: 0,
|
|
56
|
+
}
|
|
57
|
+
expect(
|
|
58
|
+
websitePurgeResult(
|
|
59
|
+
{ purged: 'site', scope: 'heatmaps', deleted },
|
|
60
|
+
t,
|
|
61
|
+
'en-GB',
|
|
62
|
+
),
|
|
63
|
+
).toBe(
|
|
64
|
+
'page.marketing.websites.purge.done_heatmaps_counts {"events":"9","snapshots":"0"}',
|
|
65
|
+
)
|
|
66
|
+
expect(
|
|
67
|
+
websitePurgeResult({ purged: 'site', scope: 'all', deleted }, t, 'en-GB'),
|
|
68
|
+
).toContain('"sessions":"2"')
|
|
69
|
+
})
|
|
70
|
+
})
|