@saooti/octopus-sdk 42.0.0-beta → 42.0.0-beta2
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/eslint-config.d.ts +3 -0
- package/eslint-config.mjs +54 -0
- package/package.json +5 -2
- package/tests/api/podcastApi.spec.ts +43 -0
- package/tests/api/transcriptionApi.spec.ts +58 -0
- package/tests/components/composable/player/usePlayerTranscript.spec.ts +238 -0
- package/tests/components/composable/useAdvancedParamInit.spec.ts +91 -0
- package/tests/components/composable/usePresentationItem.spec.ts +164 -0
- package/tests/components/composable/useRights.spec.ts +838 -0
- package/tests/components/composable/useSeasonsManagement.spec.ts +35 -0
- package/tests/components/composable/useTranslation.spec.ts +209 -0
- package/tests/components/display/RightsIndicator.spec.ts +481 -0
- package/tests/components/display/comments/item/CommentMoreActions.spec.ts +75 -0
- package/tests/components/display/comments/modal/RecaptchaModal.spec.ts +43 -0
- package/tests/components/display/emission/EmissionItem.spec.ts +54 -0
- package/tests/components/display/emission/EmissionPresentationItem.spec.ts +46 -0
- package/tests/components/display/filter/AdvancedSearch.spec.ts +187 -0
- package/tests/components/display/podcasts/PodcastFilterList.spec.ts +34 -0
- package/tests/components/display/podcasts/PodcastInlineListTemplate.spec.ts +24 -0
- package/tests/components/display/podcasts/PodcastItemInfo.spec.ts +99 -0
- package/tests/components/display/podcasts/PodcastModuleBox.spec.ts +279 -0
- package/tests/components/display/podcasts/PodcastPresentationList.spec.ts +108 -0
- package/tests/components/display/podcasts/PodcastRawTranscript.spec.ts +254 -0
- package/tests/components/display/sharing/ShareNewsletter.spec.ts +68 -0
- package/tests/components/display/sharing/SharePlayer.spec.ts +59 -0
- package/tests/components/display/sharing/SharePlayerRadio.spec.ts +55 -0
- package/tests/components/form/ClassicButtonGroup.spec.ts +78 -0
- package/tests/components/form/ClassicInputText.spec.ts +24 -0
- package/tests/components/form/OctopusMultiselect.spec.ts +506 -0
- package/tests/components/form/OctopusSelect.spec.ts +240 -0
- package/tests/components/layouts/PresentationItem.spec.ts +37 -0
- package/tests/components/misc/ClassicAvatar.spec.ts +68 -0
- package/tests/components/misc/ClassicPopover.spec.ts +260 -0
- package/tests/components/misc/FooterSection.spec.ts +49 -0
- package/tests/components/misc/HomeDropdown.spec.ts +44 -0
- package/tests/components/misc/modal/ClipboardModal.spec.ts +51 -0
- package/tests/components/misc/player/PlayerLarge.spec.ts +72 -0
- package/tests/components/pages/EmissionPage.spec.ts +253 -0
- package/tests/components/pages/EmissionsPage.spec.ts +18 -0
- package/tests/components/pages/ParticipantPage.spec.ts +65 -0
- package/tests/components/pages/PlaylistPage.spec.ts +60 -0
- package/tests/components/pages/PodcastPage.spec.ts +89 -0
- package/tests/components/pages/PodcastsPage.spec.ts +18 -0
- package/tests/components/pages/RadioPage.spec.ts +65 -0
- package/tests/components/pages/SmartLinkPage.spec.ts +90 -0
- package/tests/helper/language.spec.ts +45 -0
- package/tests/index.ts +9 -0
- package/tests/localisation.ts +10 -0
- package/tests/mocks/i18n.ts +13 -0
- package/tests/mocks/index.ts +11 -0
- package/tests/mocks/rights.ts +17 -0
- package/tests/mocks/useAdvancedParamInit.ts +22 -0
- package/tests/mocks/useRouter.ts +11 -0
- package/tests/stores/CacheStore.spec.ts +110 -0
- package/tests/utils.ts +178 -0
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import eslint from '@eslint/js';
|
|
2
|
+
import eslintPluginVue from 'eslint-plugin-vue';
|
|
3
|
+
import globals from 'globals';
|
|
4
|
+
import typescriptEslint from 'typescript-eslint';
|
|
5
|
+
import stylistic from '@stylistic/eslint-plugin';
|
|
6
|
+
|
|
7
|
+
export default typescriptEslint.config(
|
|
8
|
+
{ ignores: ['*.d.ts', '**/coverage', '**/dist'] },
|
|
9
|
+
{
|
|
10
|
+
extends: [
|
|
11
|
+
eslint.configs.recommended,
|
|
12
|
+
...typescriptEslint.configs.recommended,
|
|
13
|
+
...eslintPluginVue.configs['flat/recommended'],
|
|
14
|
+
],
|
|
15
|
+
plugins: {
|
|
16
|
+
'@stylistic': stylistic,
|
|
17
|
+
},
|
|
18
|
+
files: ['**/*.{ts,vue}'],
|
|
19
|
+
languageOptions: {
|
|
20
|
+
ecmaVersion: 'latest',
|
|
21
|
+
sourceType: 'module',
|
|
22
|
+
globals: globals.browser,
|
|
23
|
+
parserOptions: {
|
|
24
|
+
parser: typescriptEslint.parser,
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
rules: {
|
|
28
|
+
// your rules
|
|
29
|
+
"curly": ['error'],
|
|
30
|
+
|
|
31
|
+
// Please don't use console statements and duplicated imports, TODOs are marked
|
|
32
|
+
"no-console": ['warn', { allow: ['warn', 'error'] }],
|
|
33
|
+
"no-warning-comments": ['warn'],
|
|
34
|
+
"no-duplicate-imports": ['warn'],
|
|
35
|
+
|
|
36
|
+
// Prevent errors when testing on refs (instead of value of ref)
|
|
37
|
+
"vue/no-ref-as-operand": ['error'],
|
|
38
|
+
|
|
39
|
+
// Indentation
|
|
40
|
+
"@stylistic/indent": ['warn', 4, { "SwitchCase": 0 }],
|
|
41
|
+
"vue/html-indent": ['warn', 4],
|
|
42
|
+
"vue/script-indent": ['warn', 4],
|
|
43
|
+
"@stylistic/no-mixed-spaces-and-tabs": ["error", "smart-tabs"],
|
|
44
|
+
|
|
45
|
+
// Number of attributes per line (increase because sometimes two is not a lot)
|
|
46
|
+
"vue/max-attributes-per-line": ['warn', { singleline: 2 } ],
|
|
47
|
+
|
|
48
|
+
// Disable default values required for props
|
|
49
|
+
"vue/require-default-prop": ['off'],
|
|
50
|
+
|
|
51
|
+
"@typescript-eslint/no-unused-vars": "warn"
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@saooti/octopus-sdk",
|
|
3
|
-
"version": "42.0.0-
|
|
3
|
+
"version": "42.0.0-beta2",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Javascript SDK for using octopus",
|
|
6
6
|
"author": "Saooti",
|
|
@@ -123,6 +123,9 @@
|
|
|
123
123
|
},
|
|
124
124
|
"files": [
|
|
125
125
|
"dist",
|
|
126
|
-
"src"
|
|
126
|
+
"src",
|
|
127
|
+
"tests",
|
|
128
|
+
"eslint-config.mjs",
|
|
129
|
+
"eslint-config.d.ts"
|
|
127
130
|
]
|
|
128
131
|
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { describe, expect, it, vi, beforeEach } from 'vitest';
|
|
2
|
+
|
|
3
|
+
vi.mock('../../src/api/classicApi', () => ({
|
|
4
|
+
default: {
|
|
5
|
+
fetchData: vi.fn()
|
|
6
|
+
}
|
|
7
|
+
}));
|
|
8
|
+
|
|
9
|
+
import classicApi from '../../src/api/classicApi';
|
|
10
|
+
import { podcastApi } from '../../src/api/podcastApi';
|
|
11
|
+
|
|
12
|
+
const baseOptions = { organisationId: ['org-1'] };
|
|
13
|
+
|
|
14
|
+
describe('podcastApi', () => {
|
|
15
|
+
describe('count', () => {
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
vi.mocked(classicApi.fetchData).mockResolvedValue({ count: 42, result: [], sort: null });
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('returns the count from search results', async () => {
|
|
21
|
+
const result = await podcastApi.count(baseOptions);
|
|
22
|
+
expect(result).toBe(42);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('calls search with size 0', async () => {
|
|
26
|
+
await podcastApi.count(baseOptions);
|
|
27
|
+
expect(classicApi.fetchData).toHaveBeenCalledWith(
|
|
28
|
+
expect.objectContaining({
|
|
29
|
+
parameters: expect.objectContaining({ size: 0 })
|
|
30
|
+
})
|
|
31
|
+
);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('forwards other options to search', async () => {
|
|
35
|
+
await podcastApi.count({ ...baseOptions, emissionId: 5 });
|
|
36
|
+
expect(classicApi.fetchData).toHaveBeenCalledWith(
|
|
37
|
+
expect.objectContaining({
|
|
38
|
+
parameters: expect.objectContaining({ organisationId: ['org-1'], emissionId: 5 })
|
|
39
|
+
})
|
|
40
|
+
);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
});
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { describe, expect, it, vi, beforeEach } from 'vitest';
|
|
2
|
+
|
|
3
|
+
vi.mock('../../src/api/classicApi', () => ({
|
|
4
|
+
default: {
|
|
5
|
+
fetchData: vi.fn()
|
|
6
|
+
}
|
|
7
|
+
}));
|
|
8
|
+
|
|
9
|
+
import classicApi from '../../src/api/classicApi';
|
|
10
|
+
import { transcriptionApi } from '../../src/api/transcriptionApi';
|
|
11
|
+
import { ModuleApi } from '../../src/api/apiConnection';
|
|
12
|
+
|
|
13
|
+
describe('transcriptionApi', () => {
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
vi.mocked(classicApi.fetchData).mockResolvedValue('');
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
describe('getTranslations', () => {
|
|
19
|
+
it('calls the SPEECHTOTEXT api with the correct path', async () => {
|
|
20
|
+
await transcriptionApi.getTranslations(42);
|
|
21
|
+
expect(classicApi.fetchData).toHaveBeenCalledWith({
|
|
22
|
+
api: ModuleApi.SPEECHTOTEXT,
|
|
23
|
+
path: 'transcription/42/languages',
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
describe('getTranslation', () => {
|
|
29
|
+
it('calls the SPEECHTOTEXT api with the correct path', async () => {
|
|
30
|
+
await transcriptionApi.getTranslation(42, 'en');
|
|
31
|
+
expect(classicApi.fetchData).toHaveBeenCalledWith({
|
|
32
|
+
api: ModuleApi.SPEECHTOTEXT,
|
|
33
|
+
path: 'transcription/42/languages/en/srt',
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it.each([
|
|
38
|
+
[true, 'true'],
|
|
39
|
+
[false, 'false'],
|
|
40
|
+
])('appends mayCreateIfNotExists=%s to path when mayCreate is %s', async (mayCreate, suffix) => {
|
|
41
|
+
await transcriptionApi.getTranslation(42, 'en', mayCreate);
|
|
42
|
+
expect(classicApi.fetchData).toHaveBeenCalledWith({
|
|
43
|
+
api: ModuleApi.SPEECHTOTEXT,
|
|
44
|
+
path: `transcription/42/languages/en/srt?mayCreateIfNotExists=${suffix}`,
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
describe('getRawTranscription', () => {
|
|
50
|
+
it('calls the SPEECHTOTEXT api with the correct path', async () => {
|
|
51
|
+
await transcriptionApi.getRawTranscription(42);
|
|
52
|
+
expect(classicApi.fetchData).toHaveBeenCalledWith({
|
|
53
|
+
api: ModuleApi.SPEECHTOTEXT,
|
|
54
|
+
path: 'transcription/text/42',
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
});
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import { defineComponent } from 'vue';
|
|
3
|
+
import { mount as _mount } from '@vue/test-utils';
|
|
4
|
+
import { flushPromises } from '@vue/test-utils';
|
|
5
|
+
import { setupPinia } from '@tests/utils';
|
|
6
|
+
import { usePlayerStore } from '@/stores/PlayerStore';
|
|
7
|
+
import { useVastStore } from '@/stores/VastStore';
|
|
8
|
+
import { usePlayerTranscript } from '@/components/composable/player/usePlayerTranscript';
|
|
9
|
+
|
|
10
|
+
vi.mock('@/components/composable/useTranslation', () => ({
|
|
11
|
+
useTranslation: vi.fn(),
|
|
12
|
+
}));
|
|
13
|
+
|
|
14
|
+
vi.mock('@/api/classicApi', () => ({
|
|
15
|
+
default: { fetchData: vi.fn() },
|
|
16
|
+
}));
|
|
17
|
+
|
|
18
|
+
vi.mock('@/api/transcriptionApi', () => ({
|
|
19
|
+
transcriptionApi: {
|
|
20
|
+
getTranslations: vi.fn().mockResolvedValue({
|
|
21
|
+
podcastId: 42,
|
|
22
|
+
nativeLanguage: 'fr',
|
|
23
|
+
translations: [],
|
|
24
|
+
}),
|
|
25
|
+
getTranslation: vi.fn().mockResolvedValue(''),
|
|
26
|
+
},
|
|
27
|
+
TranslationState: { FINISHED: 'FINISHED' },
|
|
28
|
+
}));
|
|
29
|
+
|
|
30
|
+
import { useTranslation } from '@/components/composable/useTranslation';
|
|
31
|
+
import { transcriptionApi } from '@/api/transcriptionApi';
|
|
32
|
+
import classicApi from '@/api/classicApi';
|
|
33
|
+
|
|
34
|
+
const SRT_FROM_ZERO = '1\n00:00:00,000 --> 00:00:02,000\nHello\n\n2\n00:00:02,000 --> 00:00:04,000\nWorld\n\n';
|
|
35
|
+
const SRT_DELAYED_START = '1\n00:00:01,000 --> 00:00:02,000\nHello\n\n';
|
|
36
|
+
|
|
37
|
+
describe('usePlayerTranscript', () => {
|
|
38
|
+
let composable: ReturnType<typeof usePlayerTranscript>;
|
|
39
|
+
let playerStore: ReturnType<typeof usePlayerStore>;
|
|
40
|
+
let mockGetMostRelevantLanguage: ReturnType<typeof vi.fn>;
|
|
41
|
+
|
|
42
|
+
beforeEach(() => {
|
|
43
|
+
vi.clearAllMocks();
|
|
44
|
+
mockGetMostRelevantLanguage = vi.fn().mockResolvedValue({ ready: 'fr' });
|
|
45
|
+
vi.mocked(useTranslation).mockReturnValue({
|
|
46
|
+
getMostRelevantLanguage: mockGetMostRelevantLanguage,
|
|
47
|
+
} as unknown as ReturnType<typeof useTranslation>);
|
|
48
|
+
|
|
49
|
+
const pinia = setupPinia();
|
|
50
|
+
|
|
51
|
+
let result!: ReturnType<typeof usePlayerTranscript>;
|
|
52
|
+
_mount(defineComponent({
|
|
53
|
+
setup() {
|
|
54
|
+
playerStore = usePlayerStore();
|
|
55
|
+
result = usePlayerTranscript();
|
|
56
|
+
return {};
|
|
57
|
+
},
|
|
58
|
+
template: '<div/>',
|
|
59
|
+
}), { global: { plugins: [pinia] } });
|
|
60
|
+
composable = result;
|
|
61
|
+
composable.generatingTranscriptLanguage.value = null;
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
describe('getTranscription', () => {
|
|
65
|
+
it('clears transcript when playerPodcast is undefined', async () => {
|
|
66
|
+
const spy = vi.spyOn(playerStore, 'playerUpdateTranscript');
|
|
67
|
+
|
|
68
|
+
await composable.getTranscription();
|
|
69
|
+
|
|
70
|
+
expect(spy).toHaveBeenCalledWith();
|
|
71
|
+
expect(mockGetMostRelevantLanguage).not.toHaveBeenCalled();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('sets actualText from the first entry when it starts at 0', async () => {
|
|
75
|
+
playerStore.$patch({ playerPodcast: { podcastId: 42 } as never });
|
|
76
|
+
vi.mocked(transcriptionApi.getTranslation).mockResolvedValue(SRT_FROM_ZERO);
|
|
77
|
+
const spy = vi.spyOn(playerStore, 'playerUpdateTranscript');
|
|
78
|
+
|
|
79
|
+
await composable.getTranscription();
|
|
80
|
+
|
|
81
|
+
expect(transcriptionApi.getTranslations).toHaveBeenCalledWith(42);
|
|
82
|
+
expect(transcriptionApi.getTranslation).toHaveBeenCalledWith(42, 'fr');
|
|
83
|
+
expect(spy).toHaveBeenCalledWith({
|
|
84
|
+
actual: 0,
|
|
85
|
+
actualText: 'Hello',
|
|
86
|
+
value: [
|
|
87
|
+
{ startTime: 0, endTime: 2, text: 'Hello' },
|
|
88
|
+
{ startTime: 2, endTime: 4, text: 'World' },
|
|
89
|
+
],
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('sets empty actualText when the first entry starts after 0', async () => {
|
|
94
|
+
playerStore.$patch({ playerPodcast: { podcastId: 42 } as never });
|
|
95
|
+
vi.mocked(transcriptionApi.getTranslation).mockResolvedValue(SRT_DELAYED_START);
|
|
96
|
+
const spy = vi.spyOn(playerStore, 'playerUpdateTranscript');
|
|
97
|
+
|
|
98
|
+
await composable.getTranscription();
|
|
99
|
+
|
|
100
|
+
expect(spy).toHaveBeenCalledWith(expect.objectContaining({ actualText: '' }));
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
describe('generatingTranscriptLanguage', () => {
|
|
105
|
+
it('is null initially', () => {
|
|
106
|
+
expect(composable.generatingTranscriptLanguage.value).toBeNull();
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('is reset to null when getTranscription starts', async () => {
|
|
110
|
+
composable.generatingTranscriptLanguage.value = 'de';
|
|
111
|
+
|
|
112
|
+
await composable.getTranscription(); // no podcast → returns early
|
|
113
|
+
|
|
114
|
+
expect(composable.generatingTranscriptLanguage.value).toBeNull();
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('is set to the available language while a better translation is being generated', async () => {
|
|
118
|
+
playerStore.$patch({ playerPodcast: { podcastId: 42 } as never });
|
|
119
|
+
mockGetMostRelevantLanguage.mockResolvedValue({ ready: 'fr', available: 'en' });
|
|
120
|
+
|
|
121
|
+
let resolveGeneration!: (value: string) => void;
|
|
122
|
+
vi.mocked(transcriptionApi.getTranslation)
|
|
123
|
+
.mockResolvedValueOnce('') // first call: ready='fr'
|
|
124
|
+
.mockReturnValueOnce(new Promise(resolve => { resolveGeneration = resolve; }));
|
|
125
|
+
|
|
126
|
+
const promise = composable.getTranscription();
|
|
127
|
+
await flushPromises();
|
|
128
|
+
|
|
129
|
+
expect(composable.generatingTranscriptLanguage.value).toBe('en');
|
|
130
|
+
|
|
131
|
+
resolveGeneration('');
|
|
132
|
+
await flushPromises();
|
|
133
|
+
|
|
134
|
+
expect(composable.generatingTranscriptLanguage.value).toBeNull();
|
|
135
|
+
await promise;
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('updates transcript with the better translation once generated', async () => {
|
|
139
|
+
playerStore.$patch({ playerPodcast: { podcastId: 42 } as never });
|
|
140
|
+
mockGetMostRelevantLanguage.mockResolvedValue({ ready: 'fr', available: 'en' });
|
|
141
|
+
vi.mocked(transcriptionApi.getTranslation)
|
|
142
|
+
.mockResolvedValueOnce('') // for ready='fr'
|
|
143
|
+
.mockResolvedValueOnce(SRT_FROM_ZERO); // for available='en'
|
|
144
|
+
|
|
145
|
+
const spy = vi.spyOn(playerStore, 'playerUpdateTranscript');
|
|
146
|
+
|
|
147
|
+
await composable.getTranscription();
|
|
148
|
+
|
|
149
|
+
expect(transcriptionApi.getTranslation).toHaveBeenCalledWith(42, 'en', true);
|
|
150
|
+
expect(spy).toHaveBeenCalledWith(expect.objectContaining({
|
|
151
|
+
actualText: 'Hello',
|
|
152
|
+
value: [
|
|
153
|
+
{ startTime: 0, endTime: 2, text: 'Hello' },
|
|
154
|
+
{ startTime: 2, endTime: 4, text: 'World' },
|
|
155
|
+
],
|
|
156
|
+
}));
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
describe('onTimeUpdateTranscript', () => {
|
|
161
|
+
it('does nothing when there is no transcript', () => {
|
|
162
|
+
composable.onTimeUpdateTranscript(1);
|
|
163
|
+
expect(playerStore.playerTranscript).toBeUndefined();
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('sets actualText when currentTime reaches startTime', () => {
|
|
167
|
+
playerStore.playerUpdateTranscript({
|
|
168
|
+
actual: 0, actualText: '',
|
|
169
|
+
value: [{ startTime: 1, endTime: 3, text: 'Hello' }],
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
composable.onTimeUpdateTranscript(1.5);
|
|
173
|
+
|
|
174
|
+
expect(playerStore.playerTranscript?.actualText).toBe('Hello');
|
|
175
|
+
expect(playerStore.playerTranscript?.actual).toBe(0);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it('advances to the next entry when currentTime exceeds endTime', () => {
|
|
179
|
+
playerStore.playerUpdateTranscript({
|
|
180
|
+
actual: 0, actualText: '',
|
|
181
|
+
value: [
|
|
182
|
+
{ startTime: 0, endTime: 2, text: 'Hello' },
|
|
183
|
+
{ startTime: 2, endTime: 4, text: 'World' },
|
|
184
|
+
],
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
composable.onTimeUpdateTranscript(3);
|
|
188
|
+
|
|
189
|
+
expect(playerStore.playerTranscript?.actual).toBe(1);
|
|
190
|
+
expect(playerStore.playerTranscript?.actualText).toBe('World');
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
describe('onSeekedTranscript', () => {
|
|
195
|
+
it('does nothing when there is no transcript', () => {
|
|
196
|
+
composable.onSeekedTranscript(5);
|
|
197
|
+
expect(playerStore.playerTranscript).toBeUndefined();
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it('seeks to the correct entry index', () => {
|
|
201
|
+
playerStore.playerUpdateTranscript({
|
|
202
|
+
actual: 0, actualText: '',
|
|
203
|
+
value: [
|
|
204
|
+
{ startTime: 0, endTime: 2, text: 'Hello' },
|
|
205
|
+
{ startTime: 2, endTime: 4, text: 'World' },
|
|
206
|
+
{ startTime: 4, endTime: 6, text: 'Bye' },
|
|
207
|
+
],
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
composable.onSeekedTranscript(3);
|
|
211
|
+
|
|
212
|
+
expect(playerStore.playerTranscript?.actual).toBe(1);
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
describe('checkDelaytWithStitching', () => {
|
|
217
|
+
it('always resets the stitching delay to 0', async () => {
|
|
218
|
+
const spy = vi.spyOn(playerStore, 'playerUpdateDelayStitching');
|
|
219
|
+
useVastStore().$patch({ useVastPlayerPodcast: true });
|
|
220
|
+
|
|
221
|
+
await composable.checkDelaytWithStitching();
|
|
222
|
+
|
|
223
|
+
expect(spy).toHaveBeenCalledWith(0);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it.each([
|
|
227
|
+
['useVastPlayerPodcast is true', () => useVastStore().$patch({ useVastPlayerPodcast: true })],
|
|
228
|
+
['no #audio-player element exists', () => playerStore.$patch({
|
|
229
|
+
playerPodcast: { podcastId: 1 } as never,
|
|
230
|
+
playerTranscript: { actual: 0, actualText: '', value: [] },
|
|
231
|
+
})],
|
|
232
|
+
])('returns early when %s', async (_, setup) => {
|
|
233
|
+
setup();
|
|
234
|
+
await composable.checkDelaytWithStitching();
|
|
235
|
+
expect(classicApi.fetchData).not.toHaveBeenCalled();
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
});
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { mockUseRouter } from '@tests/mocks';
|
|
2
|
+
vi.mock('vue-router', () => mockUseRouter());
|
|
3
|
+
|
|
4
|
+
import { describe, expect, it, vi, beforeEach } from 'vitest';
|
|
5
|
+
import { defineComponent, nextTick } from 'vue';
|
|
6
|
+
import { mount as _mount } from '@vue/test-utils';
|
|
7
|
+
|
|
8
|
+
import { useAdvancedParamInit } from '@/components/composable/route/useAdvancedParamInit';
|
|
9
|
+
import { state } from '@/stores/ParamSdkStore';
|
|
10
|
+
import { setupPinia, setupAuthStore } from '@tests/utils';
|
|
11
|
+
import type { RouteProps } from '@/components/composable/route/types';
|
|
12
|
+
|
|
13
|
+
vi.mock('@/api/groupsApi', () => ({
|
|
14
|
+
groupsApi: { getAllById: vi.fn().mockResolvedValue({}) }
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
async function setupComposable(
|
|
18
|
+
props: RouteProps,
|
|
19
|
+
isEmission: boolean,
|
|
20
|
+
patchStores?: () => void | Promise<void>
|
|
21
|
+
): Promise<ReturnType<typeof useAdvancedParamInit>> {
|
|
22
|
+
let result!: ReturnType<typeof useAdvancedParamInit>;
|
|
23
|
+
|
|
24
|
+
const pinia = setupPinia();
|
|
25
|
+
await patchStores?.();
|
|
26
|
+
|
|
27
|
+
_mount(defineComponent({
|
|
28
|
+
setup() { result = useAdvancedParamInit(props, isEmission); return {}; },
|
|
29
|
+
template: '<div/>'
|
|
30
|
+
}), { global: { plugins: [pinia] } });
|
|
31
|
+
|
|
32
|
+
await nextTick(); // allow onMounted
|
|
33
|
+
await nextTick(); // allow isInit via nextTick in initAdvancedParams
|
|
34
|
+
|
|
35
|
+
return result;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
describe('useAdvancedParamInit', () => {
|
|
39
|
+
beforeEach(() => {
|
|
40
|
+
state.generalParameters.podcastmaker = false;
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
describe('initValidity', () => {
|
|
44
|
+
const propsWithHidden: RouteProps = { routeValidity: 'false', routeIncludeHidden: 'true' };
|
|
45
|
+
const withOrg = (roles: string[]) => setupAuthStore({ roles, organisationId: 'test-org-id' });
|
|
46
|
+
|
|
47
|
+
describe('forces validity to "true"', () => {
|
|
48
|
+
it('when isEmission is true', async () => {
|
|
49
|
+
const { validity } = await setupComposable(propsWithHidden, true, withOrg(['PRODUCTION']));
|
|
50
|
+
expect(validity.value).toBe('true');
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('when isPodcastmaker is true', async () => {
|
|
54
|
+
state.generalParameters.podcastmaker = true;
|
|
55
|
+
const { validity } = await setupComposable(propsWithHidden, false, withOrg(['PRODUCTION']));
|
|
56
|
+
expect(validity.value).toBe('true');
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('when includeHidden is false (no org)', async () => {
|
|
60
|
+
// authOrgaId starts undefined → filterOrgaId undefined → organisation undefined → includeHidden false
|
|
61
|
+
const { validity } = await setupComposable(propsWithHidden, false);
|
|
62
|
+
expect(validity.value).toBe('true');
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('when routeIncludeHidden is "false"', async () => {
|
|
66
|
+
const { validity } = await setupComposable(
|
|
67
|
+
{ routeValidity: 'false', routeIncludeHidden: 'false' },
|
|
68
|
+
false,
|
|
69
|
+
withOrg(['PRODUCTION'])
|
|
70
|
+
);
|
|
71
|
+
expect(validity.value).toBe('true');
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
(['PODCAST_CRUD', 'RESTRICTED_PRODUCTION', 'PLAYLISTS'] as const).forEach(role => {
|
|
75
|
+
it(`when role cannot validate (${role})`, async () => {
|
|
76
|
+
const { validity } = await setupComposable(propsWithHidden, false, withOrg([role]));
|
|
77
|
+
expect(validity.value).toBe('true');
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
describe('uses routeValidity when canValidatePodcast() is true', () => {
|
|
83
|
+
(['ADMIN', 'ORGANISATION', 'PRODUCTION', 'PODCAST_VALIDATION'] as const).forEach(role => {
|
|
84
|
+
it(`allows role ${role}`, async () => {
|
|
85
|
+
const { validity } = await setupComposable(propsWithHidden, false, withOrg([role]));
|
|
86
|
+
expect(validity.value).toBe('false');
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
});
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { mockI18n } from '@tests/mocks';
|
|
2
|
+
vi.mock('vue-i18n', () => mockI18n());
|
|
3
|
+
|
|
4
|
+
vi.mock('@/api', () => ({
|
|
5
|
+
rubriquesApi: { getCachedRubrique: vi.fn() },
|
|
6
|
+
}));
|
|
7
|
+
|
|
8
|
+
import { rubriquesApi } from '@/api';
|
|
9
|
+
import { usePresentationItem } from '@/components/composable/usePresentationItem';
|
|
10
|
+
import { emptyEmissionData, Emission } from '@/stores/class/general/emission';
|
|
11
|
+
import { emptyPodcastData, Podcast } from '@/stores/class/general/podcast';
|
|
12
|
+
import { Rubrique } from '@/stores/class/rubrique/rubrique';
|
|
13
|
+
import { useGeneralStore } from '@/stores/GeneralStore';
|
|
14
|
+
import { state } from '@/stores/ParamSdkStore';
|
|
15
|
+
import { setupPinia } from '@tests/utils';
|
|
16
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
17
|
+
|
|
18
|
+
function rubrique(rubriqueId: number, name: string, rubriquageId?: number): Rubrique {
|
|
19
|
+
return { rubriqueId, name, rubriquageId };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
beforeEach(() => {
|
|
23
|
+
setupPinia();
|
|
24
|
+
state.presentationItems = { tags: 'none' };
|
|
25
|
+
vi.mocked(rubriquesApi.getCachedRubrique).mockReset();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
describe('usePresentationItem', () => {
|
|
29
|
+
describe('tagsFor', () => {
|
|
30
|
+
it('returns undefined when tags type is none', async () => {
|
|
31
|
+
const { tagsFor } = usePresentationItem();
|
|
32
|
+
expect(await tagsFor(emptyEmissionData())).toBeUndefined();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
describe('iab type', () => {
|
|
36
|
+
beforeEach(() => {
|
|
37
|
+
state.presentationItems.tags = 'iab';
|
|
38
|
+
useGeneralStore().storedUpdateCategories([
|
|
39
|
+
{ id: 1, name: 'News' },
|
|
40
|
+
{ id: 2, name: 'Sport' },
|
|
41
|
+
]);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('returns the names of categories matching the emission iabIds', async () => {
|
|
45
|
+
const { tagsFor } = usePresentationItem();
|
|
46
|
+
const emission: Emission = { ...emptyEmissionData(), iabIds: [2] };
|
|
47
|
+
expect(await tagsFor(emission)).toEqual(['Sport']);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('reads iabIds from the podcast emission', async () => {
|
|
51
|
+
const { tagsFor } = usePresentationItem();
|
|
52
|
+
const podcast: Podcast = emptyPodcastData();
|
|
53
|
+
podcast.emission = { ...emptyEmissionData(), iabIds: [1, 2] };
|
|
54
|
+
expect(await tagsFor(podcast)).toEqual(['News', 'Sport']);
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
describe('rubrique type', () => {
|
|
59
|
+
beforeEach(() => {
|
|
60
|
+
state.presentationItems.tags = 'rubrique';
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('fetches tags for an emission rubriqueIds', async () => {
|
|
64
|
+
vi.mocked(rubriquesApi.getCachedRubrique).mockImplementation(
|
|
65
|
+
(id) => Promise.resolve(rubrique(id, `Rubrique ${id}`))
|
|
66
|
+
);
|
|
67
|
+
const { tagsFor } = usePresentationItem();
|
|
68
|
+
const emission: Emission = { ...emptyEmissionData(), rubriqueIds: [1, 2] };
|
|
69
|
+
expect(await tagsFor(emission)).toEqual(['Rubrique 1', 'Rubrique 2']);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('combines a podcast rubriqueIds with its emission rubriqueIds', async () => {
|
|
73
|
+
vi.mocked(rubriquesApi.getCachedRubrique).mockImplementation(
|
|
74
|
+
(id) => Promise.resolve(rubrique(id, `Rubrique ${id}`))
|
|
75
|
+
);
|
|
76
|
+
const { tagsFor } = usePresentationItem();
|
|
77
|
+
const podcast: Podcast = emptyPodcastData();
|
|
78
|
+
podcast.rubriqueIds = [1];
|
|
79
|
+
podcast.emission = { ...emptyEmissionData(), rubriqueIds: [2] };
|
|
80
|
+
expect(await tagsFor(podcast)).toEqual(['Rubrique 1', 'Rubrique 2']);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('filters rubriques by tagsRubriquageId when set', async () => {
|
|
84
|
+
vi.mocked(rubriquesApi.getCachedRubrique).mockImplementation((id) => {
|
|
85
|
+
if (id === 1) { return Promise.resolve(rubrique(1, 'Kept', 10)); }
|
|
86
|
+
return Promise.resolve(rubrique(2, 'Discarded', 20));
|
|
87
|
+
});
|
|
88
|
+
state.presentationItems.tagsRubriquageId = 10;
|
|
89
|
+
const { tagsFor } = usePresentationItem();
|
|
90
|
+
const emission: Emission = { ...emptyEmissionData(), rubriqueIds: [1, 2] };
|
|
91
|
+
expect(await tagsFor(emission)).toEqual(['Kept']);
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('limits the number of tags when tagsLimit is set', async () => {
|
|
96
|
+
state.presentationItems.tags = 'iab';
|
|
97
|
+
state.presentationItems.tagsLimit = 1;
|
|
98
|
+
useGeneralStore().storedUpdateCategories([
|
|
99
|
+
{ id: 1, name: 'News' },
|
|
100
|
+
{ id: 2, name: 'Sport' },
|
|
101
|
+
]);
|
|
102
|
+
const { tagsFor } = usePresentationItem();
|
|
103
|
+
const emission: Emission = { ...emptyEmissionData(), iabIds: [1, 2] };
|
|
104
|
+
expect(await tagsFor(emission)).toEqual(['News']);
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
describe('additionalInfoFor', () => {
|
|
109
|
+
it('returns undefined when additionalInfo is not configured', () => {
|
|
110
|
+
const { additionalInfoFor } = usePresentationItem();
|
|
111
|
+
expect(additionalInfoFor(emptyEmissionData())).toBeUndefined();
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('returns undefined when additionalInfo is empty', () => {
|
|
115
|
+
state.presentationItems.additionalInfo = [];
|
|
116
|
+
const { additionalInfoFor } = usePresentationItem();
|
|
117
|
+
expect(additionalInfoFor(emptyEmissionData())).toBeUndefined();
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('resolves productor from the emission organisation name for an emission', () => {
|
|
121
|
+
state.presentationItems.additionalInfo = ['productor'];
|
|
122
|
+
const { additionalInfoFor } = usePresentationItem();
|
|
123
|
+
const emission: Emission = { ...emptyEmissionData(), orga: { id: '1', name: 'Saooti', imageUrl: '' } };
|
|
124
|
+
expect(additionalInfoFor(emission)).toEqual(['Saooti']);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('resolves productor from the podcast emission organisation name', () => {
|
|
128
|
+
state.presentationItems.additionalInfo = ['productor'];
|
|
129
|
+
const { additionalInfoFor } = usePresentationItem();
|
|
130
|
+
const podcast: Podcast = emptyPodcastData();
|
|
131
|
+
podcast.emission = { ...emptyEmissionData(), orga: { id: '1', name: 'Saooti', imageUrl: '' } };
|
|
132
|
+
expect(additionalInfoFor(podcast)).toEqual(['Saooti']);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('formats the podcast pubDate for the date info', () => {
|
|
136
|
+
state.presentationItems.additionalInfo = ['date'];
|
|
137
|
+
const { additionalInfoFor } = usePresentationItem();
|
|
138
|
+
const podcast: Podcast = emptyPodcastData();
|
|
139
|
+
podcast.pubDate = '2025-12-01T10:21:31.000+00:00';
|
|
140
|
+
expect(additionalInfoFor(podcast)).toEqual(['1 décembre 2025']);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('omits the date info for an emission (no pubDate)', () => {
|
|
144
|
+
state.presentationItems.additionalInfo = ['date'];
|
|
145
|
+
const { additionalInfoFor } = usePresentationItem();
|
|
146
|
+
expect(additionalInfoFor(emptyEmissionData())).toEqual([]);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('omits the date info for a podcast without a pubDate', () => {
|
|
150
|
+
state.presentationItems.additionalInfo = ['date'];
|
|
151
|
+
const { additionalInfoFor } = usePresentationItem();
|
|
152
|
+
expect(additionalInfoFor(emptyPodcastData())).toEqual([]);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('combines multiple info entries in order', () => {
|
|
156
|
+
state.presentationItems.additionalInfo = ['date', 'productor'];
|
|
157
|
+
const { additionalInfoFor } = usePresentationItem();
|
|
158
|
+
const podcast: Podcast = emptyPodcastData();
|
|
159
|
+
podcast.pubDate = '2025-12-01T10:21:31.000+00:00';
|
|
160
|
+
podcast.emission = { ...emptyEmissionData(), orga: { id: '1', name: 'Saooti', imageUrl: '' } };
|
|
161
|
+
expect(additionalInfoFor(podcast)).toEqual(['1 décembre 2025', 'Saooti']);
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
});
|