@saooti/octopus-sdk 41.12.0-beta3 → 41.12.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/CHANGELOG.md +5 -1
- package/index.ts +1 -1
- package/package.json +1 -1
- package/src/api/index.ts +1 -0
- package/src/api/radioApi.ts +18 -2
- package/src/api/rubriquesApi.ts +68 -0
- package/src/components/composable/usePresentationItem.ts +78 -0
- package/src/components/display/emission/EmissionInlineList.vue +1 -1
- package/src/components/display/emission/EmissionPresentationItem.vue +25 -16
- package/src/components/display/podcastmaker/PodcastmakerHeader.vue +60 -45
- package/src/components/display/podcasts/PodcastPresentationList.vue +103 -89
- package/src/components/form/ClassicButtonGroup.vue +12 -6
- package/src/components/layouts/PresentationItem.vue +149 -104
- package/src/stores/CacheStore.ts +17 -11
- package/src/stores/ParamSdkStore.ts +22 -0
- package/src/stores/class/general/organisation.ts +7 -3
- package/src/stores/class/radio/canal.ts +30 -8
- package/src/stores/class/radio/recurrence.ts +6 -5
- package/tests/components/composable/usePresentationItem.spec.ts +163 -0
- package/tests/components/display/emission/EmissionPresentationItem.spec.ts +45 -0
- package/tests/components/display/podcasts/PodcastPresentationList.spec.ts +107 -0
- package/tests/components/form/ClassicButtonGroup.spec.ts +26 -0
- package/tests/components/layouts/PresentationItem.spec.ts +36 -0
- package/tests/stores/CacheStore.spec.ts +109 -0
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import '@tests/mocks/i18n';
|
|
2
|
+
|
|
3
|
+
vi.mock('@/api', () => ({
|
|
4
|
+
rubriquesApi: { getCachedRubrique: vi.fn() },
|
|
5
|
+
}));
|
|
6
|
+
|
|
7
|
+
import { rubriquesApi } from '@/api';
|
|
8
|
+
import { usePresentationItem } from '@/components/composable/usePresentationItem';
|
|
9
|
+
import { emptyEmissionData, Emission } from '@/stores/class/general/emission';
|
|
10
|
+
import { emptyPodcastData, Podcast } from '@/stores/class/general/podcast';
|
|
11
|
+
import { Rubrique } from '@/stores/class/rubrique/rubrique';
|
|
12
|
+
import { useGeneralStore } from '@/stores/GeneralStore';
|
|
13
|
+
import { state } from '@/stores/ParamSdkStore';
|
|
14
|
+
import { setupPinia } from '@tests/utils';
|
|
15
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
16
|
+
|
|
17
|
+
function rubrique(rubriqueId: number, name: string, rubriquageId?: number): Rubrique {
|
|
18
|
+
return { rubriqueId, name, rubriquageId };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
setupPinia();
|
|
23
|
+
state.presentationItems = { tags: 'none' };
|
|
24
|
+
vi.mocked(rubriquesApi.getCachedRubrique).mockReset();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
describe('usePresentationItem', () => {
|
|
28
|
+
describe('tagsFor', () => {
|
|
29
|
+
it('returns undefined when tags type is none', async () => {
|
|
30
|
+
const { tagsFor } = usePresentationItem();
|
|
31
|
+
expect(await tagsFor(emptyEmissionData())).toBeUndefined();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
describe('iab type', () => {
|
|
35
|
+
beforeEach(() => {
|
|
36
|
+
state.presentationItems.tags = 'iab';
|
|
37
|
+
useGeneralStore().storedUpdateCategories([
|
|
38
|
+
{ id: 1, name: 'News' },
|
|
39
|
+
{ id: 2, name: 'Sport' },
|
|
40
|
+
]);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('returns the names of categories matching the emission iabIds', async () => {
|
|
44
|
+
const { tagsFor } = usePresentationItem();
|
|
45
|
+
const emission: Emission = { ...emptyEmissionData(), iabIds: [2] };
|
|
46
|
+
expect(await tagsFor(emission)).toEqual(['Sport']);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('reads iabIds from the podcast emission', async () => {
|
|
50
|
+
const { tagsFor } = usePresentationItem();
|
|
51
|
+
const podcast: Podcast = emptyPodcastData();
|
|
52
|
+
podcast.emission = { ...emptyEmissionData(), iabIds: [1, 2] };
|
|
53
|
+
expect(await tagsFor(podcast)).toEqual(['News', 'Sport']);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
describe('rubrique type', () => {
|
|
58
|
+
beforeEach(() => {
|
|
59
|
+
state.presentationItems.tags = 'rubrique';
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('fetches tags for an emission rubriqueIds', async () => {
|
|
63
|
+
vi.mocked(rubriquesApi.getCachedRubrique).mockImplementation(
|
|
64
|
+
(id) => Promise.resolve(rubrique(id, `Rubrique ${id}`))
|
|
65
|
+
);
|
|
66
|
+
const { tagsFor } = usePresentationItem();
|
|
67
|
+
const emission: Emission = { ...emptyEmissionData(), rubriqueIds: [1, 2] };
|
|
68
|
+
expect(await tagsFor(emission)).toEqual(['Rubrique 1', 'Rubrique 2']);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('combines a podcast rubriqueIds with its emission rubriqueIds', async () => {
|
|
72
|
+
vi.mocked(rubriquesApi.getCachedRubrique).mockImplementation(
|
|
73
|
+
(id) => Promise.resolve(rubrique(id, `Rubrique ${id}`))
|
|
74
|
+
);
|
|
75
|
+
const { tagsFor } = usePresentationItem();
|
|
76
|
+
const podcast: Podcast = emptyPodcastData();
|
|
77
|
+
podcast.rubriqueIds = [1];
|
|
78
|
+
podcast.emission = { ...emptyEmissionData(), rubriqueIds: [2] };
|
|
79
|
+
expect(await tagsFor(podcast)).toEqual(['Rubrique 1', 'Rubrique 2']);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('filters rubriques by tagsRubriquageId when set', async () => {
|
|
83
|
+
vi.mocked(rubriquesApi.getCachedRubrique).mockImplementation((id) => {
|
|
84
|
+
if (id === 1) { return Promise.resolve(rubrique(1, 'Kept', 10)); }
|
|
85
|
+
return Promise.resolve(rubrique(2, 'Discarded', 20));
|
|
86
|
+
});
|
|
87
|
+
state.presentationItems.tagsRubriquageId = 10;
|
|
88
|
+
const { tagsFor } = usePresentationItem();
|
|
89
|
+
const emission: Emission = { ...emptyEmissionData(), rubriqueIds: [1, 2] };
|
|
90
|
+
expect(await tagsFor(emission)).toEqual(['Kept']);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('limits the number of tags when tagsLimit is set', async () => {
|
|
95
|
+
state.presentationItems.tags = 'iab';
|
|
96
|
+
state.presentationItems.tagsLimit = 1;
|
|
97
|
+
useGeneralStore().storedUpdateCategories([
|
|
98
|
+
{ id: 1, name: 'News' },
|
|
99
|
+
{ id: 2, name: 'Sport' },
|
|
100
|
+
]);
|
|
101
|
+
const { tagsFor } = usePresentationItem();
|
|
102
|
+
const emission: Emission = { ...emptyEmissionData(), iabIds: [1, 2] };
|
|
103
|
+
expect(await tagsFor(emission)).toEqual(['News']);
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
describe('additionalInfoFor', () => {
|
|
108
|
+
it('returns undefined when additionalInfo is not configured', () => {
|
|
109
|
+
const { additionalInfoFor } = usePresentationItem();
|
|
110
|
+
expect(additionalInfoFor(emptyEmissionData())).toBeUndefined();
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('returns undefined when additionalInfo is empty', () => {
|
|
114
|
+
state.presentationItems.additionalInfo = [];
|
|
115
|
+
const { additionalInfoFor } = usePresentationItem();
|
|
116
|
+
expect(additionalInfoFor(emptyEmissionData())).toBeUndefined();
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('resolves productor from the emission organisation name for an emission', () => {
|
|
120
|
+
state.presentationItems.additionalInfo = ['productor'];
|
|
121
|
+
const { additionalInfoFor } = usePresentationItem();
|
|
122
|
+
const emission: Emission = { ...emptyEmissionData(), orga: { id: '1', name: 'Saooti', imageUrl: '' } };
|
|
123
|
+
expect(additionalInfoFor(emission)).toEqual(['Saooti']);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('resolves productor from the podcast emission organisation name', () => {
|
|
127
|
+
state.presentationItems.additionalInfo = ['productor'];
|
|
128
|
+
const { additionalInfoFor } = usePresentationItem();
|
|
129
|
+
const podcast: Podcast = emptyPodcastData();
|
|
130
|
+
podcast.emission = { ...emptyEmissionData(), orga: { id: '1', name: 'Saooti', imageUrl: '' } };
|
|
131
|
+
expect(additionalInfoFor(podcast)).toEqual(['Saooti']);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('formats the podcast pubDate for the date info', () => {
|
|
135
|
+
state.presentationItems.additionalInfo = ['date'];
|
|
136
|
+
const { additionalInfoFor } = usePresentationItem();
|
|
137
|
+
const podcast: Podcast = emptyPodcastData();
|
|
138
|
+
podcast.pubDate = '2025-12-01T10:21:31.000+00:00';
|
|
139
|
+
expect(additionalInfoFor(podcast)).toEqual(['1 décembre 2025']);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('omits the date info for an emission (no pubDate)', () => {
|
|
143
|
+
state.presentationItems.additionalInfo = ['date'];
|
|
144
|
+
const { additionalInfoFor } = usePresentationItem();
|
|
145
|
+
expect(additionalInfoFor(emptyEmissionData())).toEqual([]);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('omits the date info for a podcast without a pubDate', () => {
|
|
149
|
+
state.presentationItems.additionalInfo = ['date'];
|
|
150
|
+
const { additionalInfoFor } = usePresentationItem();
|
|
151
|
+
expect(additionalInfoFor(emptyPodcastData())).toEqual([]);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('combines multiple info entries in order', () => {
|
|
155
|
+
state.presentationItems.additionalInfo = ['date', 'productor'];
|
|
156
|
+
const { additionalInfoFor } = usePresentationItem();
|
|
157
|
+
const podcast: Podcast = emptyPodcastData();
|
|
158
|
+
podcast.pubDate = '2025-12-01T10:21:31.000+00:00';
|
|
159
|
+
podcast.emission = { ...emptyEmissionData(), orga: { id: '1', name: 'Saooti', imageUrl: '' } };
|
|
160
|
+
expect(additionalInfoFor(podcast)).toEqual(['1 décembre 2025', 'Saooti']);
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
});
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import '@tests/mocks/i18n';
|
|
2
|
+
|
|
3
|
+
const tagsFor = vi.fn();
|
|
4
|
+
const additionalInfoFor = vi.fn();
|
|
5
|
+
|
|
6
|
+
vi.mock('@/components/composable/usePresentationItem', () => ({
|
|
7
|
+
usePresentationItem: () => ({ tagsFor, additionalInfoFor }),
|
|
8
|
+
}));
|
|
9
|
+
|
|
10
|
+
import EmissionPresentationItem from '@/components/display/emission/EmissionPresentationItem.vue';
|
|
11
|
+
import { emptyEmissionData } from '@/stores/class/general/emission';
|
|
12
|
+
import { mount as testMount } from '@tests/utils';
|
|
13
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
14
|
+
import { nextTick } from 'vue';
|
|
15
|
+
|
|
16
|
+
const mount = (props: Record<string, unknown>) => testMount(EmissionPresentationItem, { shallow: true, props });
|
|
17
|
+
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
tagsFor.mockReset().mockResolvedValue([]);
|
|
20
|
+
additionalInfoFor.mockReset().mockReturnValue(undefined);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
describe('EmissionPresentationItem', () => {
|
|
24
|
+
it('calls tagsFor with the emission prop', async () => {
|
|
25
|
+
const emission = { ...emptyEmissionData(), emissionId: 42 };
|
|
26
|
+
await mount({ emission });
|
|
27
|
+
expect(tagsFor).toHaveBeenCalledWith(emission);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('resolves tagsFor asynchronously and forwards the result as the tags prop', async () => {
|
|
31
|
+
tagsFor.mockResolvedValue(['News']);
|
|
32
|
+
const wrapper = await mount({ emission: emptyEmissionData() });
|
|
33
|
+
await nextTick();
|
|
34
|
+
await nextTick();
|
|
35
|
+
const presentationItem = wrapper.findComponent({ name: 'PresentationItem' });
|
|
36
|
+
expect(presentationItem.props('tags')).toEqual(['News']);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('forwards additionalInfoFor result as the additional-info prop', async () => {
|
|
40
|
+
additionalInfoFor.mockReturnValue(['Saooti']);
|
|
41
|
+
const wrapper = await mount({ emission: emptyEmissionData() });
|
|
42
|
+
const presentationItem = wrapper.findComponent({ name: 'PresentationItem' });
|
|
43
|
+
expect(presentationItem.props('additionalInfo')).toEqual(['Saooti']);
|
|
44
|
+
});
|
|
45
|
+
});
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import '@tests/mocks/i18n';
|
|
2
|
+
import '@tests/mocks/useRouter';
|
|
3
|
+
|
|
4
|
+
vi.mock('@/api/classicApi', () => ({
|
|
5
|
+
default: { fetchData: vi.fn() },
|
|
6
|
+
}));
|
|
7
|
+
vi.mock('@/api/podcastApi', () => ({
|
|
8
|
+
podcastApi: { search: vi.fn() },
|
|
9
|
+
PodcastSort: { DATE: 'DATE' },
|
|
10
|
+
}));
|
|
11
|
+
|
|
12
|
+
const tagsFor = vi.fn();
|
|
13
|
+
const additionalInfoFor = vi.fn();
|
|
14
|
+
vi.mock('@/components/composable/usePresentationItem', () => ({
|
|
15
|
+
usePresentationItem: () => ({ tagsFor, additionalInfoFor }),
|
|
16
|
+
}));
|
|
17
|
+
|
|
18
|
+
import classicApi from '@/api/classicApi';
|
|
19
|
+
import { podcastApi } from '@/api/podcastApi';
|
|
20
|
+
import PodcastPresentationList from '@/components/display/podcasts/PodcastPresentationList.vue';
|
|
21
|
+
import { emptyEmissionData, Emission } from '@/stores/class/general/emission';
|
|
22
|
+
import { PodcastType, SimplifiedPodcast } from '@/stores/class/general/podcast';
|
|
23
|
+
import { mount as testMount } from '@tests/utils';
|
|
24
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
25
|
+
import { flushPromises } from '@vue/test-utils';
|
|
26
|
+
|
|
27
|
+
function makeEmission(emissionId: number): Emission {
|
|
28
|
+
return { ...emptyEmissionData(), emissionId, name: `Emission ${emissionId}`, orga: { id: 'org-1', name: 'Orga', imageUrl: '' } };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function makeSimplifiedPodcast(emissionId: number, podcastId: number, pubDate: string): SimplifiedPodcast {
|
|
32
|
+
return {
|
|
33
|
+
podcastId,
|
|
34
|
+
emissionId,
|
|
35
|
+
organisationId: 'org-1',
|
|
36
|
+
audioUrl: '',
|
|
37
|
+
audioStorageUrl: '',
|
|
38
|
+
article: '',
|
|
39
|
+
imageUrl: '',
|
|
40
|
+
title: `Podcast ${podcastId}`,
|
|
41
|
+
description: undefined,
|
|
42
|
+
tags: [],
|
|
43
|
+
beneficiaries: [],
|
|
44
|
+
availability: { visibility: true, date: undefined },
|
|
45
|
+
monetisable: 'UNDEFINED',
|
|
46
|
+
pubDate,
|
|
47
|
+
conferenceId: undefined,
|
|
48
|
+
duration: 0,
|
|
49
|
+
seasonEpisodeType: PodcastType.FULL,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const mount = (props: Record<string, unknown> = {}) => testMount(PodcastPresentationList, {
|
|
54
|
+
props,
|
|
55
|
+
stubs: ['PresentationItem', 'PodcastPlayButton'],
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
beforeEach(() => {
|
|
59
|
+
tagsFor.mockReset();
|
|
60
|
+
additionalInfoFor.mockReset().mockReturnValue(undefined);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
describe('PodcastPresentationList', () => {
|
|
64
|
+
it('populates the tags prop of each item once tagsFor resolves', async () => {
|
|
65
|
+
vi.mocked(classicApi.fetchData).mockResolvedValue({ count: 1, sort: '', result: [makeEmission(1)] });
|
|
66
|
+
vi.mocked(podcastApi.search).mockResolvedValue({
|
|
67
|
+
count: 1, sort: '', result: [makeSimplifiedPodcast(1, 10, '2025-01-01T00:00:00.000Z')],
|
|
68
|
+
});
|
|
69
|
+
tagsFor.mockResolvedValue(['News']);
|
|
70
|
+
|
|
71
|
+
const wrapper = await mount();
|
|
72
|
+
await flushPromises();
|
|
73
|
+
|
|
74
|
+
const items = wrapper.findAllComponents({ name: 'PresentationItem' });
|
|
75
|
+
expect(items).toHaveLength(1);
|
|
76
|
+
expect(items[0].props('tags')).toEqual(['News']);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('fetches tags only once per podcast', async () => {
|
|
80
|
+
vi.mocked(classicApi.fetchData).mockResolvedValue({ count: 1, sort: '', result: [makeEmission(1)] });
|
|
81
|
+
vi.mocked(podcastApi.search).mockResolvedValue({
|
|
82
|
+
count: 1, sort: '', result: [makeSimplifiedPodcast(1, 10, '2025-01-01T00:00:00.000Z')],
|
|
83
|
+
});
|
|
84
|
+
tagsFor.mockResolvedValue(['News']);
|
|
85
|
+
|
|
86
|
+
await mount();
|
|
87
|
+
await flushPromises();
|
|
88
|
+
await flushPromises();
|
|
89
|
+
|
|
90
|
+
expect(tagsFor).toHaveBeenCalledTimes(1);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('forwards additionalInfoFor result as the additional-info prop of each item', async () => {
|
|
94
|
+
vi.mocked(classicApi.fetchData).mockResolvedValue({ count: 1, sort: '', result: [makeEmission(1)] });
|
|
95
|
+
vi.mocked(podcastApi.search).mockResolvedValue({
|
|
96
|
+
count: 1, sort: '', result: [makeSimplifiedPodcast(1, 10, '2025-01-01T00:00:00.000Z')],
|
|
97
|
+
});
|
|
98
|
+
tagsFor.mockResolvedValue([]);
|
|
99
|
+
additionalInfoFor.mockReturnValue(['Orga']);
|
|
100
|
+
|
|
101
|
+
const wrapper = await mount();
|
|
102
|
+
await flushPromises();
|
|
103
|
+
|
|
104
|
+
const items = wrapper.findAllComponents({ name: 'PresentationItem' });
|
|
105
|
+
expect(items[0].props('additionalInfo')).toEqual(['Orga']);
|
|
106
|
+
});
|
|
107
|
+
});
|
|
@@ -49,4 +49,30 @@ describe('ClassicButtonGroup', () => {
|
|
|
49
49
|
await wrapper.findAll('button')[0].trigger('click');
|
|
50
50
|
expect(wrapper.emitted('update:value')).toBeUndefined();
|
|
51
51
|
});
|
|
52
|
+
|
|
53
|
+
describe('solo prop', () => {
|
|
54
|
+
it('emits only the clicked value when clicking an inactive button', async () => {
|
|
55
|
+
const wrapper = await mount(ClassicButtonGroup, {
|
|
56
|
+
props: { options, value: ['PODCAST'], solo: true },
|
|
57
|
+
});
|
|
58
|
+
await wrapper.findAll('button')[1].trigger('click');
|
|
59
|
+
expect(wrapper.emitted('update:value')?.[0]).toEqual([['VIDEO']]);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('emits only the clicked value when clicking the already active button', async () => {
|
|
63
|
+
const wrapper = await mount(ClassicButtonGroup, {
|
|
64
|
+
props: { options, value: ['PODCAST'], solo: true },
|
|
65
|
+
});
|
|
66
|
+
await wrapper.findAll('button')[0].trigger('click');
|
|
67
|
+
expect(wrapper.emitted('update:value')?.[0]).toEqual([['PODCAST']]);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('replaces multiple active values with the clicked one', async () => {
|
|
71
|
+
const wrapper = await mount(ClassicButtonGroup, {
|
|
72
|
+
props: { options, value: ['PODCAST', 'VIDEO'], solo: true },
|
|
73
|
+
});
|
|
74
|
+
await wrapper.findAll('button')[2].trigger('click');
|
|
75
|
+
expect(wrapper.emitted('update:value')?.[0]).toEqual([['LIVE']]);
|
|
76
|
+
});
|
|
77
|
+
});
|
|
52
78
|
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import '@tests/mocks/i18n';
|
|
2
|
+
import '@tests/mocks/useRouter';
|
|
3
|
+
|
|
4
|
+
import PresentationItem from '@/components/layouts/PresentationItem.vue';
|
|
5
|
+
import { mount as testMount } from '@tests/utils';
|
|
6
|
+
import { describe, expect, it } from 'vitest';
|
|
7
|
+
|
|
8
|
+
const baseProps = {
|
|
9
|
+
route: 'podcast',
|
|
10
|
+
imageUrl: 'https://example.com/img.png',
|
|
11
|
+
name: 'Item name',
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const mount = (props: Record<string, unknown>) => testMount(PresentationItem, {
|
|
15
|
+
props: { ...baseProps, ...props },
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
describe('PresentationItem', () => {
|
|
19
|
+
describe('additionalInfo', () => {
|
|
20
|
+
it('renders each additionalInfo entry when a description is set', async () => {
|
|
21
|
+
const wrapper = await mount({ description: 'Some description', additionalInfo: ['1 décembre 2025', 'Saooti'] });
|
|
22
|
+
const info = wrapper.findAll('.text-secondary');
|
|
23
|
+
expect(info.map(el => el.text())).toEqual(['1 décembre 2025', 'Saooti']);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('does not render additionalInfo when it is not provided', async () => {
|
|
27
|
+
const wrapper = await mount({ description: 'Some description' });
|
|
28
|
+
expect(wrapper.find('.text-secondary').exists()).toBe(false);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('does not render additionalInfo when there is no description', async () => {
|
|
32
|
+
const wrapper = await mount({ additionalInfo: ['Saooti'] });
|
|
33
|
+
expect(wrapper.find('.text-secondary').exists()).toBe(false);
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
});
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import '@tests/mocks/i18n';
|
|
2
|
+
|
|
3
|
+
import { useCacheStore } from '@/stores/CacheStore';
|
|
4
|
+
import { setupPinia } from '@tests/utils';
|
|
5
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
6
|
+
|
|
7
|
+
describe('CacheStore', () => {
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
setupPinia();
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
describe('getData', () => {
|
|
13
|
+
it('returns the value resolved by the callback', async () => {
|
|
14
|
+
const store = useCacheStore();
|
|
15
|
+
const result = await store.getData('key', () => Promise.resolve('value'));
|
|
16
|
+
expect(result).toBe('value');
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('does not call the callback again for a cached key', async () => {
|
|
20
|
+
const store = useCacheStore();
|
|
21
|
+
const callback = vi.fn().mockResolvedValue('value');
|
|
22
|
+
await store.getData('key', callback);
|
|
23
|
+
await store.getData('key', callback);
|
|
24
|
+
expect(callback).toHaveBeenCalledTimes(1);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('shares the in-flight promise between concurrent callers', async () => {
|
|
28
|
+
const store = useCacheStore();
|
|
29
|
+
const callback = vi.fn().mockResolvedValue('value');
|
|
30
|
+
const [a, b] = await Promise.all([
|
|
31
|
+
store.getData('key', callback),
|
|
32
|
+
store.getData('key', callback),
|
|
33
|
+
]);
|
|
34
|
+
expect(a).toBe('value');
|
|
35
|
+
expect(b).toBe('value');
|
|
36
|
+
expect(callback).toHaveBeenCalledTimes(1);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('keeps separate cache entries per key', async () => {
|
|
40
|
+
const store = useCacheStore();
|
|
41
|
+
const callbackA = vi.fn().mockResolvedValue('value-a');
|
|
42
|
+
const callbackB = vi.fn().mockResolvedValue('value-b');
|
|
43
|
+
expect(await store.getData('key-a', callbackA)).toBe('value-a');
|
|
44
|
+
expect(await store.getData('key-b', callbackB)).toBe('value-b');
|
|
45
|
+
expect(callbackA).toHaveBeenCalledTimes(1);
|
|
46
|
+
expect(callbackB).toHaveBeenCalledTimes(1);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('does not cache a rejected fetch, allowing the next call to retry', async () => {
|
|
50
|
+
const store = useCacheStore();
|
|
51
|
+
const callback = vi.fn()
|
|
52
|
+
.mockRejectedValueOnce(new Error('boom'))
|
|
53
|
+
.mockResolvedValueOnce('value');
|
|
54
|
+
|
|
55
|
+
await expect(store.getData('key', callback)).rejects.toThrow('boom');
|
|
56
|
+
// Let the internal `.catch(() => invalidate(key))` microtask run
|
|
57
|
+
await Promise.resolve();
|
|
58
|
+
await Promise.resolve();
|
|
59
|
+
|
|
60
|
+
const result = await store.getData('key', callback);
|
|
61
|
+
expect(result).toBe('value');
|
|
62
|
+
expect(callback).toHaveBeenCalledTimes(2);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
describe('expiration', () => {
|
|
66
|
+
beforeEach(() => {
|
|
67
|
+
vi.useFakeTimers();
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
afterEach(() => {
|
|
71
|
+
vi.useRealTimers();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('does not refetch before the 30 minute expiration', async () => {
|
|
75
|
+
const store = useCacheStore();
|
|
76
|
+
const callback = vi.fn().mockResolvedValue('value');
|
|
77
|
+
await store.getData('key', callback);
|
|
78
|
+
vi.advanceTimersByTime(29 * 60 * 1000);
|
|
79
|
+
await store.getData('key', callback);
|
|
80
|
+
expect(callback).toHaveBeenCalledTimes(1);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('refetches after the 30 minute expiration', async () => {
|
|
84
|
+
const store = useCacheStore();
|
|
85
|
+
const callback = vi.fn().mockResolvedValue('value');
|
|
86
|
+
await store.getData('key', callback);
|
|
87
|
+
vi.advanceTimersByTime(31 * 60 * 1000);
|
|
88
|
+
await store.getData('key', callback);
|
|
89
|
+
expect(callback).toHaveBeenCalledTimes(2);
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
describe('invalidate', () => {
|
|
95
|
+
it('forces the next call to refetch', async () => {
|
|
96
|
+
const store = useCacheStore();
|
|
97
|
+
const callback = vi.fn().mockResolvedValue('value');
|
|
98
|
+
await store.getData('key', callback);
|
|
99
|
+
store.invalidate('key');
|
|
100
|
+
await store.getData('key', callback);
|
|
101
|
+
expect(callback).toHaveBeenCalledTimes(2);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('does nothing for an unknown key', () => {
|
|
105
|
+
const store = useCacheStore();
|
|
106
|
+
expect(() => store.invalidate('unknown')).not.toThrow();
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
});
|