import {afterEach, describe, expect, it} from 'vitest' import {flushPromises, mount} from '@vue/test-utils' import type {DefineComponent} from 'vue' import InputRichText from './InputRichText.vue' type InputRichTextProps = { id?: string label?: string modelValue?: string | null placeholder?: string minHeight?: string editable?: boolean disabled?: boolean readonly?: boolean hint?: string error?: string success?: string outputFormat?: 'markdown' | 'html' groupClass?: string labelClass?: string editorClass?: string } const InputRichTextForTest = InputRichText as DefineComponent const mountComponent = async (props: InputRichTextProps = {}) => { const wrapper = mount(InputRichTextForTest, { props, attachTo: document.body, }) await flushPromises() return wrapper } afterEach(() => { document.body.replaceChildren() }) describe('MalioInputRichText', () => { it('renders the label and reuses a provided id', async () => { const wrapper = await mountComponent({id: 'custom-rt-id', label: 'Description'}) const label = wrapper.get('label') expect(label.text()).toBe('Description') expect(label.attributes('for')).toBe('custom-rt-id') expect(wrapper.get('#custom-rt-id').exists()).toBe(true) }) it('generates an id when missing', async () => { const wrapper = await mountComponent({label: 'Description'}) const labelFor = wrapper.get('label').attributes('for') expect(labelFor?.startsWith('malio-input-rich-text-')).toBe(true) }) it('renders the toolbar buttons in editable mode', async () => { const wrapper = await mountComponent({modelValue: ''}) const buttons = wrapper.findAll('button[type="button"]') expect(buttons.length).toBeGreaterThanOrEqual(13) expect(wrapper.find('button[title="Gras"]').exists()).toBe(true) expect(wrapper.find('button[title="Italique"]').exists()).toBe(true) expect(wrapper.find('button[title="Lien"]').exists()).toBe(true) expect(wrapper.find('button[title="Couleur du texte"]').exists()).toBe(true) expect(wrapper.find('button[title="Surlignage"]').exists()).toBe(true) expect(wrapper.find('button[title="Annuler"]').exists()).toBe(true) expect(wrapper.find('button[title="Rétablir"]').exists()).toBe(true) }) it('opens and closes the text color palette', async () => { const wrapper = await mountComponent({modelValue: ''}) expect(wrapper.find('[aria-label="Palette couleur du texte"]').exists()).toBe(false) await wrapper.get('button[title="Couleur du texte"]').trigger('click') expect(wrapper.find('[aria-label="Palette couleur du texte"]').exists()).toBe(true) await wrapper.get('button[title="Couleur du texte"]').trigger('click') expect(wrapper.find('[aria-label="Palette couleur du texte"]').exists()).toBe(false) }) it('opens the highlight palette and closes the color palette', async () => { const wrapper = await mountComponent({modelValue: ''}) await wrapper.get('button[title="Couleur du texte"]').trigger('click') expect(wrapper.find('[aria-label="Palette couleur du texte"]').exists()).toBe(true) await wrapper.get('button[title="Surlignage"]').trigger('click') expect(wrapper.find('[aria-label="Palette de surlignage"]').exists()).toBe(true) expect(wrapper.find('[aria-label="Palette couleur du texte"]').exists()).toBe(false) }) it('disables color and highlight buttons when readonly', async () => { const wrapper = await mountComponent({readonly: true, modelValue: ''}) expect(wrapper.get('button[title="Couleur du texte"]').attributes('disabled')).toBeDefined() expect(wrapper.get('button[title="Surlignage"]').attributes('disabled')).toBeDefined() }) it('does not render the toolbar in readonly display mode (editable=false)', async () => { const wrapper = await mountComponent({editable: false, modelValue: '**hi**'}) expect(wrapper.find('button[title="Gras"]').exists()).toBe(false) }) it('disables toolbar buttons when disabled', async () => { const wrapper = await mountComponent({disabled: true, modelValue: ''}) const boldBtn = wrapper.get('button[title="Gras"]') expect(boldBtn.attributes('disabled')).toBeDefined() }) it('disables toolbar buttons when readonly', async () => { const wrapper = await mountComponent({readonly: true, modelValue: ''}) const boldBtn = wrapper.get('button[title="Gras"]') expect(boldBtn.attributes('disabled')).toBeDefined() }) it('shows hint message in muted color', async () => { const wrapper = await mountComponent({hint: 'Helpful hint'}) expect(wrapper.get('p.text-m-muted').text()).toBe('Helpful hint') }) it('shows error state on wrapper, label and message', async () => { const wrapper = await mountComponent({label: 'Description', error: 'Editor error'}) expect(wrapper.get('label').classes()).toContain('text-m-danger') expect(wrapper.get('p.text-m-danger').text()).toBe('Editor error') expect(wrapper.get('.rich-text-wrapper').classes()).toContain('border-m-danger') }) it('shows success state on wrapper, label and message', async () => { const wrapper = await mountComponent({label: 'Description', success: 'Editor success'}) expect(wrapper.get('label').classes()).toContain('text-m-success') expect(wrapper.get('p.text-m-success').text()).toBe('Editor success') expect(wrapper.get('.rich-text-wrapper').classes()).toContain('border-m-success') }) it('prioritizes error over success', async () => { const wrapper = await mountComponent({error: 'Editor error', success: 'Editor success'}) expect(wrapper.get('.rich-text-wrapper').classes()).toContain('border-m-danger') expect(wrapper.find('p.text-m-success').exists()).toBe(false) expect(wrapper.get('p.text-m-danger').text()).toBe('Editor error') }) it('sets aria-invalid and aria-describedby on the editor content when error', async () => { const wrapper = await mountComponent({id: 'rt-aria', error: 'Boom'}) const editorContent = wrapper.find('[aria-invalid="true"]') expect(editorContent.exists()).toBe(true) expect(editorContent.attributes('aria-describedby')).toBe('rt-aria-describedby') }) it('renders initial markdown content visually', async () => { const wrapper = await mountComponent({modelValue: '## Mon titre\n\nUn paragraphe.'}) const html = wrapper.html() expect(html).toContain('Mon titre') expect(html).toContain('Un paragraphe.') }) })