@antelopejs/dms-mailing 0.4.1 → 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/README.md +1 -1
- package/frontend-vue/app/components/CategoriesInput.vue +7 -14
- package/frontend-vue/app/components/DuplicateModal.vue +9 -3
- package/frontend-vue/app/components/Editor.vue +3 -3
- package/frontend-vue/app/components/NewTemplateModal.vue +9 -3
- package/frontend-vue/app/components/OverviewGate.vue +20 -29
- package/frontend-vue/app/components/PageHeader.vue +8 -12
- package/frontend-vue/app/components/RealSendModal.vue +3 -1
- package/frontend-vue/app/components/RenderingModal.vue +5 -7
- package/frontend-vue/app/components/RetentionInput.vue +13 -23
- package/frontend-vue/app/components/SendDrawer.vue +5 -2
- package/frontend-vue/app/components/StarterGallery.vue +11 -9
- package/frontend-vue/app/components/TemplateCard.vue +19 -4
- package/frontend-vue/app/components/TemplateDrawer.vue +8 -2
- package/frontend-vue/app/components/TemplateGallery.vue +25 -20
- package/frontend-vue/app/components/TemplatePreviewFrame.vue +8 -6
- package/frontend-vue/app/components/TestSendModal.vue +3 -1
- package/frontend-vue/app/components/VersionHistory.vue +3 -3
- package/frontend-vue/app/components/WebhookStatus.vue +20 -17
- package/frontend-vue/app/components/editor/EditorWorkspace.vue +5 -0
- package/frontend-vue/app/composables/useEditorActions.ts +3 -1
- package/frontend-vue/app/composables/useEditorSession.ts +6 -4
- package/frontend-vue/app/composables/useMailingApi.ts +5 -58
- package/frontend-vue/app/composables/useMailingReads.ts +229 -0
- package/frontend-vue/app/composables/useOverviewSetup.ts +15 -7
- package/frontend-vue/app/composables/useTemplateCategories.ts +8 -27
- package/frontend-vue/app/utils/gallery.ts +19 -7
- package/frontend-vue/app/utils/period.ts +0 -5
- package/frontend-vue/package.json +1 -1
- package/frontend-vue/tests/categories-input.test.ts +43 -13
- package/frontend-vue/tests/editor-session.test.ts +4 -0
- package/frontend-vue/tests/gallery.test.ts +16 -0
- package/frontend-vue/tests/helpers/api-data.ts +173 -0
- package/frontend-vue/tests/new-template-modal.test.ts +17 -5
- package/frontend-vue/tests/on-demand-reads.test.ts +81 -0
- package/frontend-vue/tests/overview-gate.test.ts +59 -21
- package/frontend-vue/tests/page-reads.test.ts +157 -0
- package/frontend-vue/tests/period.test.ts +0 -9
- package/frontend-vue/tests/retention-input.test.ts +30 -9
- package/frontend-vue/tests/send-modals.test.ts +8 -1
- package/frontend-vue/tests/template-drawer.test.ts +30 -14
- package/frontend-vue/tests/template-gallery.test.ts +45 -24
- package/package.json +2 -2
|
@@ -10,15 +10,16 @@ import {
|
|
|
10
10
|
typeInto,
|
|
11
11
|
type NuxtStubs,
|
|
12
12
|
} from './helpers/nuxt-globals'
|
|
13
|
+
import { mailingReads, stubApiData } from './helpers/api-data'
|
|
13
14
|
|
|
14
15
|
const api = {
|
|
15
16
|
listTemplates: vi.fn(),
|
|
16
17
|
starters: vi.fn(),
|
|
17
|
-
listCategories: vi.fn(),
|
|
18
18
|
createTemplate: vi.fn(),
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
vi.mock('../app/composables/useMailingApi', () => ({
|
|
21
|
+
vi.mock('../app/composables/useMailingApi', async (importOriginal) => ({
|
|
22
|
+
...(await importOriginal<object>()),
|
|
22
23
|
useMailingApi: () => api,
|
|
23
24
|
}))
|
|
24
25
|
|
|
@@ -54,9 +55,20 @@ beforeEach(() => {
|
|
|
54
55
|
},
|
|
55
56
|
],
|
|
56
57
|
})
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
58
|
+
stubApiData().respond(
|
|
59
|
+
mailingReads(
|
|
60
|
+
{ listTemplates: api.listTemplates, starters: api.starters },
|
|
61
|
+
({ url }) => {
|
|
62
|
+
if (url !== '/api/mailing/templates/categories')
|
|
63
|
+
throw new Error(`unexpected read ${url}`)
|
|
64
|
+
return {
|
|
65
|
+
categories: [
|
|
66
|
+
{ id: 'billing', label: 'Billing', icon: 'i-ph-file-text' },
|
|
67
|
+
],
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
),
|
|
71
|
+
)
|
|
60
72
|
api.createTemplate.mockResolvedValue(['new-id'])
|
|
61
73
|
})
|
|
62
74
|
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
2
|
+
import { effectScope, ref } from 'vue'
|
|
3
|
+
import {
|
|
4
|
+
useStarterPreviewRead,
|
|
5
|
+
useTemplateContentRead,
|
|
6
|
+
useTemplateListRead,
|
|
7
|
+
} from '../app/composables/useMailingReads'
|
|
8
|
+
import { stubApiData, type ApiDataStub } from './helpers/api-data'
|
|
9
|
+
|
|
10
|
+
let reads: ApiDataStub
|
|
11
|
+
const options: Record<string, unknown>[] = []
|
|
12
|
+
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
reads = stubApiData()
|
|
15
|
+
options.length = 0
|
|
16
|
+
const stubbed = globalThis.useApiData
|
|
17
|
+
vi.stubGlobal(
|
|
18
|
+
'useApiData',
|
|
19
|
+
(url: unknown, readOptions: Record<string, unknown>) => {
|
|
20
|
+
options.push(readOptions)
|
|
21
|
+
return stubbed(url as string, readOptions)
|
|
22
|
+
},
|
|
23
|
+
)
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
afterEach(() => {
|
|
27
|
+
vi.unstubAllGlobals()
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
const inScope = <T>(setup: () => T): T => effectScope().run(setup) as T
|
|
31
|
+
|
|
32
|
+
describe('reads made on demand', () => {
|
|
33
|
+
it('read nothing until asked, then the current URL each time', async () => {
|
|
34
|
+
reads.respond(({ url }) => ({ url }))
|
|
35
|
+
const id = ref('t1')
|
|
36
|
+
const readContent = inScope(() => useTemplateContentRead(id))
|
|
37
|
+
await Promise.resolve()
|
|
38
|
+
expect(reads.reads).toEqual([])
|
|
39
|
+
expect(options[0]).toMatchObject({
|
|
40
|
+
immediate: false,
|
|
41
|
+
refreshOnPageRefresh: false,
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
await expect(readContent()).resolves.toEqual({
|
|
45
|
+
url: '/api/mailing/templates/t1/content',
|
|
46
|
+
})
|
|
47
|
+
id.value = 't2'
|
|
48
|
+
await readContent()
|
|
49
|
+
expect(reads.reads.map((read) => read.url)).toEqual([
|
|
50
|
+
'/api/mailing/templates/t1/content',
|
|
51
|
+
'/api/mailing/templates/t2/content',
|
|
52
|
+
])
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('reject with what the read failed with', async () => {
|
|
56
|
+
const failure = Object.assign(new Error('gone'), { statusCode: 404 })
|
|
57
|
+
reads.respond(() => Promise.reject(failure))
|
|
58
|
+
const readContent = inScope(() => useTemplateContentRead('t1'))
|
|
59
|
+
await expect(readContent()).rejects.toBe(failure)
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('send their query with the read', async () => {
|
|
63
|
+
reads.respond(() => ({}))
|
|
64
|
+
const readList = inScope(() => useTemplateListRead())
|
|
65
|
+
const readPreview = inScope(() =>
|
|
66
|
+
useStarterPreviewRead('invoice-ready', 'fr'),
|
|
67
|
+
)
|
|
68
|
+
await readList()
|
|
69
|
+
await readPreview()
|
|
70
|
+
expect(reads.reads).toEqual([
|
|
71
|
+
{
|
|
72
|
+
url: '/api/mailing/tables/templates/list',
|
|
73
|
+
query: { limit: '100', sortKey: 'name' },
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
url: '/api/mailing/templates/starters/invoice-ready/preview',
|
|
77
|
+
query: { locale: 'fr' },
|
|
78
|
+
},
|
|
79
|
+
])
|
|
80
|
+
})
|
|
81
|
+
})
|
|
@@ -1,20 +1,14 @@
|
|
|
1
1
|
// @vitest-environment jsdom
|
|
2
2
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
3
|
-
import { defineComponent, h, onBeforeUnmount
|
|
3
|
+
import { createApp, defineComponent, h, onBeforeUnmount } from 'vue'
|
|
4
4
|
import OverviewGate from '../app/components/OverviewGate.vue'
|
|
5
|
+
import { useSetupHidesPeriod } from '../app/composables/useOverviewSetup'
|
|
5
6
|
import type { SetupFacts } from '../app/utils/overview-setup'
|
|
7
|
+
import { stubApiData, type ApiDataStub } from './helpers/api-data'
|
|
6
8
|
import { flush, mount, slotStub, stubNuxtGlobals } from './helpers/nuxt-globals'
|
|
7
9
|
|
|
8
|
-
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
vi.mock('../app/composables/useMailingApi', () => ({
|
|
12
|
-
useMailingApi: () => api,
|
|
13
|
-
}))
|
|
14
|
-
|
|
15
|
-
vi.mock('../app/composables/useOverviewSetup', () => ({
|
|
16
|
-
useSetupHidesPeriod: () => hidesPeriod,
|
|
17
|
-
}))
|
|
10
|
+
let api: ApiDataStub
|
|
11
|
+
const SETUP_URL = '/api/mailing/metrics/setup'
|
|
18
12
|
|
|
19
13
|
interface ButtonAction {
|
|
20
14
|
label: string
|
|
@@ -63,6 +57,15 @@ const Gate = defineComponent({
|
|
|
63
57
|
setup: () => () => h(OverviewGate, null, { default: () => h(Dashboard) }),
|
|
64
58
|
})
|
|
65
59
|
|
|
60
|
+
/** What the page header renders of the period selector. */
|
|
61
|
+
const PeriodHolder = defineComponent({
|
|
62
|
+
props: { scope: { type: String, default: undefined } },
|
|
63
|
+
setup: (props) => {
|
|
64
|
+
const hidesPeriod = useSetupHidesPeriod(props.scope)
|
|
65
|
+
return () => h('span', { 'data-hides-period': String(hidesPeriod.value) })
|
|
66
|
+
},
|
|
67
|
+
})
|
|
68
|
+
|
|
66
69
|
const GLOBALS = {
|
|
67
70
|
DmsCard: slotStub('DmsCard'),
|
|
68
71
|
DmsEmptyState: EmptyStateStub,
|
|
@@ -85,8 +88,10 @@ const facts = (overrides: Partial<SetupFacts> = {}): SetupFacts => ({
|
|
|
85
88
|
beforeEach(() => {
|
|
86
89
|
stubNuxtGlobals()
|
|
87
90
|
vi.stubGlobal('onBeforeUnmount', onBeforeUnmount)
|
|
88
|
-
|
|
89
|
-
|
|
91
|
+
vi.stubGlobal('holdPageHeaderActions', () =>
|
|
92
|
+
defineComponent({ setup: () => () => null }),
|
|
93
|
+
)
|
|
94
|
+
api = stubApiData()
|
|
90
95
|
})
|
|
91
96
|
|
|
92
97
|
afterEach(() => {
|
|
@@ -96,34 +101,44 @@ afterEach(() => {
|
|
|
96
101
|
|
|
97
102
|
describe('OverviewGate', () => {
|
|
98
103
|
it('mounts nothing of the dashboard before the facts are known', () => {
|
|
99
|
-
api.
|
|
104
|
+
api.respond(() => new Promise(() => undefined))
|
|
100
105
|
const { host } = mount(Gate, {}, GLOBALS)
|
|
101
106
|
expect(host.querySelector('[data-dashboard]')).toBeNull()
|
|
102
107
|
expect(host.querySelector('[data-stub="USkeleton"]')).not.toBeNull()
|
|
103
108
|
})
|
|
104
109
|
|
|
105
110
|
it('says the facts failed to load and retries', async () => {
|
|
106
|
-
api.
|
|
107
|
-
api.setupFacts.mockResolvedValueOnce(facts())
|
|
111
|
+
api.respond(() => Promise.reject(new Error('500')))
|
|
108
112
|
const { host } = mount(Gate, {}, GLOBALS)
|
|
109
113
|
await flush()
|
|
110
114
|
expect(host.querySelector('[data-dashboard]')).toBeNull()
|
|
115
|
+
expect(api.reads).toEqual([{ url: SETUP_URL, query: {} }])
|
|
116
|
+
api.respond(() => facts())
|
|
111
117
|
host.querySelector<HTMLButtonElement>('[data-empty-state] button')?.click()
|
|
112
118
|
await flush()
|
|
113
119
|
expect(host.querySelector('[data-dashboard]')).not.toBeNull()
|
|
114
120
|
})
|
|
115
121
|
|
|
116
122
|
it('holds the dashboard and the period back while nothing can be sent', async () => {
|
|
117
|
-
api.
|
|
123
|
+
api.respond(() => facts({ liveTemplateCount: 0 }))
|
|
118
124
|
const { host } = mount(Gate, {}, GLOBALS)
|
|
119
125
|
await flush()
|
|
120
126
|
expect(host.querySelector('[data-dashboard]')).toBeNull()
|
|
121
127
|
expect(host.querySelectorAll('[data-step]')).toHaveLength(4)
|
|
122
|
-
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
it('hides the overview header period from the same facts, on no other page', async () => {
|
|
131
|
+
api.respond(() => facts({ liveTemplateCount: 0 }))
|
|
132
|
+
const overview = mount(PeriodHolder, { scope: 'mailing-overview' })
|
|
133
|
+
const sends = mount(PeriodHolder, { scope: 'mailing-sends' })
|
|
134
|
+
await flush()
|
|
135
|
+
expect(overview.host.innerHTML).toContain('data-hides-period="true"')
|
|
136
|
+
expect(sends.host.innerHTML).toContain('data-hides-period="false"')
|
|
137
|
+
expect(api.reads).toEqual([{ url: SETUP_URL, query: {} }])
|
|
123
138
|
})
|
|
124
139
|
|
|
125
140
|
it('lets an unfinished setup be resumed above the dashboard', async () => {
|
|
126
|
-
api.
|
|
141
|
+
api.respond(() => facts({ hasTestSend: false }))
|
|
127
142
|
const { host } = mount(Gate, {}, GLOBALS)
|
|
128
143
|
await flush()
|
|
129
144
|
expect(host.querySelector('[data-dashboard]')).not.toBeNull()
|
|
@@ -135,11 +150,34 @@ describe('OverviewGate', () => {
|
|
|
135
150
|
await flush()
|
|
136
151
|
expect(host.querySelectorAll('[data-step]')).toHaveLength(4)
|
|
137
152
|
expect(host.querySelector('[data-dashboard]')).not.toBeNull()
|
|
138
|
-
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
it('takes the props the page renderer adds without a fragment warning', async () => {
|
|
156
|
+
api.respond(() => facts())
|
|
157
|
+
const warnings: string[] = []
|
|
158
|
+
const app = createApp(() =>
|
|
159
|
+
h(
|
|
160
|
+
OverviewGate,
|
|
161
|
+
{ childCount: 0, routeParams: {} },
|
|
162
|
+
{ default: () => h(Dashboard) },
|
|
163
|
+
),
|
|
164
|
+
)
|
|
165
|
+
Object.entries(GLOBALS).forEach(([name, value]) =>
|
|
166
|
+
app.component(name, value),
|
|
167
|
+
)
|
|
168
|
+
app.config.warnHandler = (message) => warnings.push(message)
|
|
169
|
+
app.mount(document.body.appendChild(document.createElement('div')))
|
|
170
|
+
await flush()
|
|
171
|
+
app.unmount()
|
|
172
|
+
expect(
|
|
173
|
+
warnings.filter((message) =>
|
|
174
|
+
message.includes('Extraneous non-props attributes'),
|
|
175
|
+
),
|
|
176
|
+
).toEqual([])
|
|
139
177
|
})
|
|
140
178
|
|
|
141
179
|
it('offers no resume once every step is done', async () => {
|
|
142
|
-
api.
|
|
180
|
+
api.respond(() => facts())
|
|
143
181
|
const { host } = mount(Gate, {}, GLOBALS)
|
|
144
182
|
await flush()
|
|
145
183
|
expect(host.textContent).not.toContain('dms_mailing.setup.resume')
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
3
|
+
import { defineComponent, h, type VNodeChild } from 'vue'
|
|
4
|
+
import PageHeader from '../app/components/PageHeader.vue'
|
|
5
|
+
import WebhookStatus from '../app/components/WebhookStatus.vue'
|
|
6
|
+
import type { WebhookStatus as Status } from '../app/utils/webhook-status'
|
|
7
|
+
import {
|
|
8
|
+
stubApiData,
|
|
9
|
+
stubRegionalFormat,
|
|
10
|
+
type ApiDataStub,
|
|
11
|
+
} from './helpers/api-data'
|
|
12
|
+
import { flush, mount, slotStub, stubNuxtGlobals } from './helpers/nuxt-globals'
|
|
13
|
+
|
|
14
|
+
const api = { webhookTest: vi.fn() }
|
|
15
|
+
|
|
16
|
+
vi.mock('../app/composables/useMailingApi', async (importOriginal) => ({
|
|
17
|
+
...(await importOriginal<object>()),
|
|
18
|
+
useMailingApi: () => api,
|
|
19
|
+
}))
|
|
20
|
+
|
|
21
|
+
const PROVIDER_URL = '/api/mailing/provider'
|
|
22
|
+
const STATUS_URL = '/api/mailing/settings/webhook-status'
|
|
23
|
+
|
|
24
|
+
const NEVER: Status = { lastReceived: null, lastRefused: null }
|
|
25
|
+
const TESTED: Status = {
|
|
26
|
+
lastReceived: {
|
|
27
|
+
at: new Date().toISOString(),
|
|
28
|
+
provider: 'smtp',
|
|
29
|
+
type: 'delivered',
|
|
30
|
+
isMatched: false,
|
|
31
|
+
isTest: true,
|
|
32
|
+
},
|
|
33
|
+
lastRefused: null,
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const PillStub = defineComponent({
|
|
37
|
+
props: { label: { type: String, default: '' } },
|
|
38
|
+
setup: (props) => () => h('span', { 'data-pill': props.label }),
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
const ButtonStub = defineComponent({
|
|
42
|
+
props: { label: { type: String, default: '' } },
|
|
43
|
+
emits: ['click'],
|
|
44
|
+
setup:
|
|
45
|
+
(props, { emit }) =>
|
|
46
|
+
() =>
|
|
47
|
+
h('button', { onClick: () => emit('click') }, props.label),
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
let api$: ApiDataStub
|
|
51
|
+
let headerActions: (() => VNodeChild) | null
|
|
52
|
+
|
|
53
|
+
/** The page header: renders the actions `PageHeader` set up. */
|
|
54
|
+
const Header = defineComponent({
|
|
55
|
+
setup: () => () => h('header', headerActions?.()),
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
const HeaderOutlet = defineComponent({
|
|
59
|
+
props: { showProvider: { type: Boolean, default: true } },
|
|
60
|
+
setup: (props) => () =>
|
|
61
|
+
h('div', [h(PageHeader, { showProvider: props.showProvider }), h(Header)]),
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
beforeEach(() => {
|
|
65
|
+
stubNuxtGlobals()
|
|
66
|
+
stubRegionalFormat()
|
|
67
|
+
headerActions = null
|
|
68
|
+
vi.stubGlobal('resolveComponent', (name: string) =>
|
|
69
|
+
name === 'DmsStatusPill' ? PillStub : slotStub(name),
|
|
70
|
+
)
|
|
71
|
+
vi.stubGlobal('usePageHeaderActions', (render: () => VNodeChild) => {
|
|
72
|
+
headerActions = render
|
|
73
|
+
})
|
|
74
|
+
vi.stubGlobal('useApiError', vi.fn())
|
|
75
|
+
api$ = stubApiData()
|
|
76
|
+
api.webhookTest.mockReset()
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
afterEach(() => {
|
|
80
|
+
vi.unstubAllGlobals()
|
|
81
|
+
document.body.innerHTML = ''
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
describe('PageHeader', () => {
|
|
85
|
+
it('names the provider read with the page', async () => {
|
|
86
|
+
api$.respond(() => ({ name: 'smtp', connected: true, features: null }))
|
|
87
|
+
const view = mount(HeaderOutlet, {})
|
|
88
|
+
await flush()
|
|
89
|
+
expect(api$.reads).toEqual([{ url: PROVIDER_URL, query: {} }])
|
|
90
|
+
expect(
|
|
91
|
+
view.host.querySelector('[data-pill]')?.getAttribute('data-pill'),
|
|
92
|
+
).toBe('dms_mailing.page_header.provider_connected name=smtp')
|
|
93
|
+
view.unmount()
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
it('says no provider when the read fails, and reads nothing when hidden', async () => {
|
|
97
|
+
api$.respond(() => Promise.reject(new Error('503')))
|
|
98
|
+
const failed = mount(HeaderOutlet, {})
|
|
99
|
+
await flush()
|
|
100
|
+
expect(
|
|
101
|
+
failed.host.querySelector('[data-pill]')?.getAttribute('data-pill'),
|
|
102
|
+
).toBe('dms_mailing.page_header.no_provider')
|
|
103
|
+
failed.unmount()
|
|
104
|
+
api$.reads.length = 0
|
|
105
|
+
const hidden = mount(HeaderOutlet, { showProvider: false })
|
|
106
|
+
await flush()
|
|
107
|
+
expect(api$.reads).toEqual([])
|
|
108
|
+
expect(hidden.host.querySelector('[data-pill]')).toBeNull()
|
|
109
|
+
hidden.unmount()
|
|
110
|
+
})
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
describe('WebhookStatus', () => {
|
|
114
|
+
it('shows the status read, then the one a test event answers', async () => {
|
|
115
|
+
api$.respond(() => NEVER)
|
|
116
|
+
api.webhookTest.mockResolvedValue({
|
|
117
|
+
isReceived: true,
|
|
118
|
+
url: 'https://example.test/hook',
|
|
119
|
+
durationMs: 12,
|
|
120
|
+
status: TESTED,
|
|
121
|
+
})
|
|
122
|
+
const view = mount(
|
|
123
|
+
WebhookStatus,
|
|
124
|
+
{},
|
|
125
|
+
{
|
|
126
|
+
UButton: ButtonStub,
|
|
127
|
+
UIcon: slotStub('UIcon'),
|
|
128
|
+
USkeleton: slotStub('USkeleton'),
|
|
129
|
+
},
|
|
130
|
+
)
|
|
131
|
+
await flush()
|
|
132
|
+
expect(view.host.textContent).toContain(
|
|
133
|
+
'dms_mailing.settings.webhook_status.never_received',
|
|
134
|
+
)
|
|
135
|
+
view.host.querySelector('button')?.click()
|
|
136
|
+
await flush()
|
|
137
|
+
expect(view.host.textContent).toContain(
|
|
138
|
+
'dms_mailing.settings.webhook_status.received_test',
|
|
139
|
+
)
|
|
140
|
+
expect(api$.reads).toEqual([{ url: STATUS_URL, query: {} }])
|
|
141
|
+
view.unmount()
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
it('says the status is unavailable when its read fails', async () => {
|
|
145
|
+
api$.respond(() => Promise.reject(new Error('500')))
|
|
146
|
+
const view = mount(
|
|
147
|
+
WebhookStatus,
|
|
148
|
+
{},
|
|
149
|
+
{ UButton: ButtonStub, USkeleton: slotStub('USkeleton') },
|
|
150
|
+
)
|
|
151
|
+
await flush()
|
|
152
|
+
expect(view.host.textContent).toContain(
|
|
153
|
+
'dms_mailing.settings.webhook_status.unavailable',
|
|
154
|
+
)
|
|
155
|
+
view.unmount()
|
|
156
|
+
})
|
|
157
|
+
})
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest'
|
|
2
2
|
import {
|
|
3
3
|
clampRetention,
|
|
4
|
-
formatClock,
|
|
5
4
|
isWithinADay,
|
|
6
5
|
minutesSince,
|
|
7
6
|
presetShortLabelKey,
|
|
@@ -9,14 +8,6 @@ import {
|
|
|
9
8
|
retentionChoice,
|
|
10
9
|
} from '../app/utils/period'
|
|
11
10
|
|
|
12
|
-
describe('formatClock', () => {
|
|
13
|
-
it('reads a 24-hour clock whatever the locale', () => {
|
|
14
|
-
const afternoon = new Date(2026, 9, 11, 17, 5)
|
|
15
|
-
expect(formatClock(afternoon, 'en')).toBe('17:05')
|
|
16
|
-
expect(formatClock(afternoon, 'fr-FR')).toBe('17:05')
|
|
17
|
-
})
|
|
18
|
-
})
|
|
19
|
-
|
|
20
11
|
describe('presetShortLabelKey', () => {
|
|
21
12
|
it('maps a preset id onto an i18n key', () => {
|
|
22
13
|
expect(presetShortLabelKey('last-24h')).toBe(
|
|
@@ -8,15 +8,14 @@ import {
|
|
|
8
8
|
stubNuxtGlobals,
|
|
9
9
|
type NuxtStubs,
|
|
10
10
|
} from './helpers/nuxt-globals'
|
|
11
|
+
import {
|
|
12
|
+
stubApiData,
|
|
13
|
+
stubRegionalFormat,
|
|
14
|
+
type ApiDataStub,
|
|
15
|
+
} from './helpers/api-data'
|
|
11
16
|
import type { RetentionPreview } from '../app/types/mailing'
|
|
12
17
|
|
|
13
|
-
const
|
|
14
|
-
retentionPreview: vi.fn(),
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
vi.mock('../app/composables/useMailingApi', () => ({
|
|
18
|
-
useMailingApi: () => api,
|
|
19
|
-
}))
|
|
18
|
+
const PREVIEW_URL = '/api/mailing/settings/retention-preview'
|
|
20
19
|
|
|
21
20
|
const SAVED_DAYS = 90
|
|
22
21
|
const DEBOUNCE_MS = 400
|
|
@@ -71,15 +70,37 @@ function segmented(host: HTMLElement): HTMLElement {
|
|
|
71
70
|
|
|
72
71
|
describe('RetentionInput', () => {
|
|
73
72
|
let stubs: NuxtStubs
|
|
73
|
+
let api: ApiDataStub
|
|
74
74
|
let harness: Harness | undefined
|
|
75
75
|
|
|
76
76
|
beforeEach(() => {
|
|
77
77
|
vi.useFakeTimers()
|
|
78
78
|
stubs = stubNuxtGlobals()
|
|
79
79
|
vi.stubGlobal('computed', computed)
|
|
80
|
-
|
|
81
|
-
|
|
80
|
+
stubRegionalFormat()
|
|
81
|
+
api = stubApiData()
|
|
82
|
+
api.respond(({ query }) => {
|
|
83
|
+
const days = Number(query.days)
|
|
84
|
+
return preview(days, days < SAVED_DAYS ? 3 : 0)
|
|
85
|
+
})
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it('reads the saved days at once, then the days picked once they hold still', async () => {
|
|
89
|
+
harness = mountRetention()
|
|
90
|
+
await flush()
|
|
91
|
+
expect(api.reads).toEqual([
|
|
92
|
+
{ url: PREVIEW_URL, query: { days: String(SAVED_DAYS) } },
|
|
93
|
+
])
|
|
94
|
+
expect(harness.host.textContent).toContain(
|
|
95
|
+
'dms_mailing.settings.retention.preview_none',
|
|
82
96
|
)
|
|
97
|
+
harness.state.days = 30
|
|
98
|
+
await nextTick()
|
|
99
|
+
harness.state.days = 365
|
|
100
|
+
await nextTick()
|
|
101
|
+
await vi.advanceTimersByTimeAsync(DEBOUNCE_MS)
|
|
102
|
+
await flush()
|
|
103
|
+
expect(api.reads.map((read) => read.query.days)).toEqual(['90', '365'])
|
|
83
104
|
})
|
|
84
105
|
|
|
85
106
|
afterEach(() => {
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
typeInto,
|
|
18
18
|
type NuxtStubs,
|
|
19
19
|
} from './helpers/nuxt-globals'
|
|
20
|
+
import { mailingReads, stubApiData } from './helpers/api-data'
|
|
20
21
|
|
|
21
22
|
const api = {
|
|
22
23
|
templateContent: vi.fn(),
|
|
@@ -25,7 +26,8 @@ const api = {
|
|
|
25
26
|
realSend: vi.fn(),
|
|
26
27
|
}
|
|
27
28
|
|
|
28
|
-
vi.mock('../app/composables/useMailingApi', () => ({
|
|
29
|
+
vi.mock('../app/composables/useMailingApi', async (importOriginal) => ({
|
|
30
|
+
...(await importOriginal<object>()),
|
|
29
31
|
useMailingApi: () => api,
|
|
30
32
|
}))
|
|
31
33
|
|
|
@@ -102,6 +104,11 @@ let stubs: NuxtStubs
|
|
|
102
104
|
|
|
103
105
|
beforeEach(() => {
|
|
104
106
|
stubs = stubNuxtGlobals('camille@acme.com')
|
|
107
|
+
stubApiData().respond(
|
|
108
|
+
mailingReads({ templateContent: api.templateContent }, ({ url }) => {
|
|
109
|
+
throw new Error(`unexpected read ${url}`)
|
|
110
|
+
}),
|
|
111
|
+
)
|
|
105
112
|
api.preview.mockResolvedValue({
|
|
106
113
|
html: '',
|
|
107
114
|
subject: '20% off every chair',
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
typeInto,
|
|
18
18
|
type NuxtStubs,
|
|
19
19
|
} from './helpers/nuxt-globals'
|
|
20
|
+
import { mailingReads, stubApiData } from './helpers/api-data'
|
|
20
21
|
|
|
21
22
|
const api = {
|
|
22
23
|
templateContent: vi.fn(),
|
|
@@ -29,12 +30,12 @@ const api = {
|
|
|
29
30
|
listTemplates: vi.fn(),
|
|
30
31
|
duplicate: vi.fn(),
|
|
31
32
|
starters: vi.fn(),
|
|
32
|
-
listCategories: vi.fn(),
|
|
33
33
|
createTemplate: vi.fn(),
|
|
34
34
|
saveTemplateDetails: vi.fn(),
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
vi.mock('../app/composables/useMailingApi', () => ({
|
|
37
|
+
vi.mock('../app/composables/useMailingApi', async (importOriginal) => ({
|
|
38
|
+
...(await importOriginal<object>()),
|
|
38
39
|
useMailingApi: () => api,
|
|
39
40
|
}))
|
|
40
41
|
|
|
@@ -108,9 +109,27 @@ const globals = {
|
|
|
108
109
|
|
|
109
110
|
let stubs: NuxtStubs
|
|
110
111
|
|
|
112
|
+
let starters: unknown[] = []
|
|
113
|
+
|
|
111
114
|
beforeEach(() => {
|
|
112
115
|
stubs = stubNuxtGlobals()
|
|
113
116
|
menuGroups = []
|
|
117
|
+
starters = []
|
|
118
|
+
stubApiData().respond(
|
|
119
|
+
mailingReads(
|
|
120
|
+
{
|
|
121
|
+
templateContent: api.templateContent,
|
|
122
|
+
performance: api.performance,
|
|
123
|
+
listTemplates: api.listTemplates,
|
|
124
|
+
},
|
|
125
|
+
({ url }) => {
|
|
126
|
+
if (url === '/api/mailing/templates/categories')
|
|
127
|
+
return { categories: [] }
|
|
128
|
+
if (url === '/api/mailing/templates/starters') return { starters }
|
|
129
|
+
throw new Error(`unexpected read ${url}`)
|
|
130
|
+
},
|
|
131
|
+
),
|
|
132
|
+
)
|
|
114
133
|
api.performance.mockResolvedValue({
|
|
115
134
|
id: 't1',
|
|
116
135
|
slug: 'order-confirmed',
|
|
@@ -403,18 +422,15 @@ describe('DuplicateModal', () => {
|
|
|
403
422
|
|
|
404
423
|
describe('StarterGallery', () => {
|
|
405
424
|
beforeEach(() => {
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
],
|
|
416
|
-
})
|
|
417
|
-
api.listCategories.mockResolvedValue({ categories: [] })
|
|
425
|
+
starters = [
|
|
426
|
+
{
|
|
427
|
+
id: 'order-confirmed',
|
|
428
|
+
name: 'Order confirmation',
|
|
429
|
+
slug: 'order-confirmed',
|
|
430
|
+
category: 'orders',
|
|
431
|
+
locales: ['en'],
|
|
432
|
+
},
|
|
433
|
+
]
|
|
418
434
|
})
|
|
419
435
|
|
|
420
436
|
it('creates a starter as a draft and opens it', async () => {
|