import { mockI18n } from '@tests/mocks';
vi.mock('vue-i18n', () => mockI18n());
import { vi, describe, it, expect, beforeEach } from 'vitest';
import { flushPromises } from '@vue/test-utils';
import { defineComponent, nextTick } from 'vue';
import { mount as testMount } from '@tests/utils';
import PodcastRawTranscript from '@/components/display/podcasts/PodcastRawTranscript.vue';
vi.mock('@/api/transcriptionApi', () => ({
transcriptionApi: {
getTranslations: vi.fn().mockResolvedValue({
podcastId: 1,
nativeLanguage: 'fr',
translations: [],
}),
getTranslation: vi.fn().mockResolvedValue(''),
},
TranslationState: {
TRANSLATING: 'TRANSLATING',
FINISHED: 'FINISHED',
FAILED: 'FAILED',
AI_LIMIT_EXCEEDED: 'AI_LIMIT_EXCEEDED',
},
}));
vi.mock('@/components/composable/useTranslation', () => ({
useTranslation: vi.fn(),
}));
vi.mock('@/helper/cookiesHelper', () => ({
default: {
getCookie: vi.fn().mockReturnValue(null),
setCookie: vi.fn(),
},
}));
import { transcriptionApi, TranslationState } from '@/api/transcriptionApi';
import { useTranslation } from '@/components/composable/useTranslation';
import cookiesHelper from '@/helper/cookiesHelper';
const AccessibilityModalStub = defineComponent({
name: 'AccessibilityModal',
emits: ['save', 'close'],
template: '
',
});
const mount = (props: Record = {}) =>
testMount(PodcastRawTranscript, {
shallow: true,
props,
stubs: { AccessibilityModal: AccessibilityModalStub },
});
const open = (wrapper: Awaited>) =>
wrapper.find('.btn-transcript').trigger('click');
const mountAndOpen = async (podcastId = 1) => {
const wrapper = await mount({ podcastId });
await open(wrapper);
return wrapper;
};
describe('PodcastRawTranscript', () => {
let mockConvertSrtToPlainText: ReturnType;
let mockGetMostRelevantLanguage: ReturnType;
beforeEach(() => {
vi.clearAllMocks();
mockConvertSrtToPlainText = vi.fn().mockReturnValue('Converted text');
mockGetMostRelevantLanguage = vi.fn().mockResolvedValue({ ready: 'fr' });
vi.mocked(useTranslation).mockReturnValue({
convertSrtToPlainText: mockConvertSrtToPlainText,
getMostRelevantLanguage: mockGetMostRelevantLanguage,
getMostRelevantTranslation: vi.fn(),
} as ReturnType);
vi.mocked(transcriptionApi.getTranslations).mockResolvedValue({
podcastId: 1,
nativeLanguage: 'fr',
translations: [],
});
vi.mocked(transcriptionApi.getTranslation).mockResolvedValue('');
vi.mocked(cookiesHelper.getCookie).mockReturnValue(null);
});
it('shows "View transcript" button, hides body and accessibility button initially', async () => {
const wrapper = await mount({ podcastId: 1 });
expect(wrapper.find('.btn-transcript').text()).toBe('View transcript');
expect(wrapper.find('.transcription-body').exists()).toBe(false);
expect(wrapper.find('.btn-primary').exists()).toBe(false);
});
describe('toggling', () => {
it('shows body, language selector, button text and accessibility button when opened', async () => {
const wrapper = await mountAndOpen();
await flushPromises();
expect(wrapper.find('.transcription-body').exists()).toBe(true);
expect(wrapper.find('.language-selector').exists()).toBe(true);
expect(wrapper.find('.btn-transcript').text()).toBe('Hide transcript');
expect(wrapper.find('.btn-primary').exists()).toBe(true);
});
it('hides transcript body and language selector when closed again', async () => {
const wrapper = await mountAndOpen();
await open(wrapper);
expect(wrapper.find('.transcription-body').exists()).toBe(false);
expect(wrapper.find('.language-selector').exists()).toBe(false);
});
});
describe('transcript fetching', () => {
it('fetches translations when opened for the first time', async () => {
await mountAndOpen(42);
expect(transcriptionApi.getTranslations).toHaveBeenCalledWith(42);
});
it('does not fetch when podcastId is undefined', async () => {
const wrapper = await mount({});
await open(wrapper);
expect(transcriptionApi.getTranslations).not.toHaveBeenCalled();
});
it('fetches only once when closed and reopened', async () => {
const wrapper = await mountAndOpen();
await flushPromises();
await open(wrapper); // close
await open(wrapper); // reopen
expect(transcriptionApi.getTranslations).toHaveBeenCalledTimes(1);
});
it.each([
['Converted text', 'Converted text'],
['', 'Transcript does not yet exist for this episode'],
])('displays correct content for converted text %j', async (convertedText, expected) => {
mockConvertSrtToPlainText.mockReturnValue(convertedText);
const wrapper = await mountAndOpen();
await flushPromises();
expect(wrapper.find('.transcription-text').text()).toContain(expected);
});
});
describe('language selector', () => {
it('populates language options from finished translations', async () => {
vi.mocked(transcriptionApi.getTranslations).mockResolvedValue({
podcastId: 1,
nativeLanguage: 'fr',
translations: [
{ language: 'en', state: TranslationState.FINISHED },
{ language: 'es', state: TranslationState.TRANSLATING },
{ language: 'de', state: TranslationState.FINISHED },
],
});
const wrapper = await mountAndOpen();
await flushPromises();
const select = wrapper.findComponent({ name: 'ClassicSelect' });
const options = select.props('options') as Array<{ title: string; value: string }>;
expect(options.map(o => o.value)).toEqual(['fr', 'en', 'de']);
});
it('sets the most relevant language as the initial selected language', async () => {
const wrapper = await mountAndOpen();
await flushPromises();
const select = wrapper.findComponent({ name: 'ClassicSelect' });
expect(select.props('textInit')).toBe('fr');
});
it('fetches translation and converts SRT when language changes', async () => {
const srtContent = '1\n00:00:01,000 --> 00:00:02,000\nHello world\n\n';
vi.mocked(transcriptionApi.getTranslation).mockResolvedValue(srtContent);
mockConvertSrtToPlainText.mockReturnValue('Hello world ');
const wrapper = await mountAndOpen();
await flushPromises();
const select = wrapper.findComponent({ name: 'ClassicSelect' });
await select.vm.$emit('update:textInit', 'en');
await flushPromises();
expect(transcriptionApi.getTranslation).toHaveBeenCalledWith(1, 'en');
expect(mockConvertSrtToPlainText).toHaveBeenCalledWith(srtContent);
expect(wrapper.find('.transcription-text').text()).toContain('Hello world');
});
it('disables language select while loading', async () => {
let resolveTranslation!: (value: string) => void;
// First call (fetchTranscripts) resolves to make the selector visible
vi.mocked(transcriptionApi.getTranslation).mockResolvedValueOnce('');
// Subsequent call (changeLanguage) hangs
vi.mocked(transcriptionApi.getTranslation).mockReturnValue(
new Promise(resolve => { resolveTranslation = resolve; })
);
const wrapper = await mountAndOpen();
await flushPromises();
const select = wrapper.findComponent({ name: 'ClassicSelect' });
await select.vm.$emit('update:textInit', 'en');
await nextTick();
expect(select.props('isDisabled')).toBe(true);
resolveTranslation('');
await flushPromises();
expect(select.props('isDisabled')).toBe(false);
});
});
describe('accessibility', () => {
it('reads cookies and applies CSS properties on open', async () => {
vi.mocked(cookiesHelper.getCookie).mockImplementation((name: string) => {
if (name === 'octopus-font-size') { return '20px'; }
if (name === 'octopus-background') { return '#000'; }
if (name === 'octopus-color') { return '#fff'; }
return null;
});
const spy = vi.spyOn(document.documentElement.style, 'setProperty');
await mountAndOpen();
expect(cookiesHelper.getCookie).toHaveBeenCalledWith('octopus-font-size');
expect(cookiesHelper.getCookie).toHaveBeenCalledWith('octopus-background');
expect(cookiesHelper.getCookie).toHaveBeenCalledWith('octopus-color');
expect(spy).toHaveBeenCalledWith('--octopus-accessibility-font-size', '20px');
expect(spy).toHaveBeenCalledWith('--octopus-accessibility-background', '#000');
expect(spy).toHaveBeenCalledWith('--octopus-accessibility-color', '#fff');
});
it('does not set CSS properties when cookies are absent', async () => {
const spy = vi.spyOn(document.documentElement.style, 'setProperty');
await mountAndOpen();
expect(spy).not.toHaveBeenCalled();
});
it('shows accessibility modal when its button is clicked', async () => {
const wrapper = await mountAndOpen();
await wrapper.find('.btn-primary').trigger('click');
expect(wrapper.findComponent(AccessibilityModalStub).exists()).toBe(true);
});
it('closes accessibility modal on close event', async () => {
const wrapper = await mountAndOpen();
await wrapper.find('.btn-primary').trigger('click');
wrapper.findComponent(AccessibilityModalStub).vm.$emit('close');
await nextTick();
expect(wrapper.findComponent(AccessibilityModalStub).exists()).toBe(false);
});
it('saves settings to cookies and CSS and closes modal on save', async () => {
const spy = vi.spyOn(document.documentElement.style, 'setProperty');
const wrapper = await mountAndOpen();
await wrapper.find('.btn-primary').trigger('click');
wrapper.findComponent(AccessibilityModalStub).vm.$emit('save', {
fontSize: 18, background: '#fff', color: '#000',
});
await nextTick();
expect(cookiesHelper.setCookie).toHaveBeenCalledWith('octopus-font-size', '18px');
expect(cookiesHelper.setCookie).toHaveBeenCalledWith('octopus-background', '#fff');
expect(cookiesHelper.setCookie).toHaveBeenCalledWith('octopus-color', '#000');
expect(spy).toHaveBeenCalledWith('--octopus-accessibility-font-size', '18px');
expect(spy).toHaveBeenCalledWith('--octopus-accessibility-background', '#fff');
expect(spy).toHaveBeenCalledWith('--octopus-accessibility-color', '#000');
expect(wrapper.findComponent(AccessibilityModalStub).exists()).toBe(false);
});
});
});