diff --git a/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/TipTapEditor.vue b/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/TipTapEditor.vue index 09755c3d41..c4e61b5c23 100644 --- a/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/TipTapEditor.vue +++ b/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/TipTapEditor.vue @@ -190,10 +190,10 @@ document.removeEventListener('click', handleClickOutside); }); - const getMarkdownContent = () => { - if (!editor.value || !isReady.value || !editor.value.storage?.markdown) { - return ''; - } + const getContent = () => { + if (!editor.value || !isReady.value) return ''; + if (props.format === 'html') return editor.value.getHTML(); + if (!editor.value.storage?.markdown) return ''; return editor.value.storage.markdown.getMarkdown(); }; @@ -217,7 +217,8 @@ watch( () => props.value, newValue => { - const processedContent = preprocessMarkdown(newValue); + const processedContent = + props.format === 'html' ? newValue : preprocessMarkdown(newValue); if (!editor.value) { initializeEditor(processedContent, props.mode, { @@ -226,8 +227,7 @@ return; } - const editorContent = getMarkdownContent(); - if (editorContent !== newValue) { + if (getContent() !== newValue) { isUpdatingFromOutside = true; editor.value.commands.setContent(processedContent, false); nextTick(() => { @@ -242,18 +242,11 @@ watch( () => editor.value?.state, () => { - if ( - !editor.value || - !isReady.value || - isUpdatingFromOutside || - !editor.value.storage?.markdown - ) { - return; - } + if (!editor.value || !isReady.value || isUpdatingFromOutside) return; - const markdown = getMarkdownContent(); - if (markdown !== props.value) { - emit('update', markdown); + const content = getContent(); + if (content !== props.value) { + emit('update', content); } }, { deep: true }, @@ -312,6 +305,11 @@ type: String, default: null, }, + format: { + type: String, + default: 'markdown', + validator: v => ['markdown', 'html'].includes(v), + }, }, emits: ['update', 'minimize', 'open-editor'], }); diff --git a/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/__tests__/TipTapEditor.spec.js b/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/__tests__/TipTapEditor.spec.js new file mode 100644 index 0000000000..7f412e7ef5 --- /dev/null +++ b/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/__tests__/TipTapEditor.spec.js @@ -0,0 +1,89 @@ +import TipTapEditor from '../TipTapEditor/TipTapEditor.vue'; + +function makeEditorStub({ markdownOut, htmlOut }) { + return { + storage: { + markdown: { getMarkdown: () => markdownOut }, + }, + getHTML: () => htmlOut, + }; +} + +function getContent(editor, isReady, format) { + if (!editor || !isReady) return ''; + if (format === 'html') return editor.getHTML(); + if (!editor.storage?.markdown) return ''; + return editor.storage.markdown.getMarkdown(); +} +describe('TipTapEditor — format prop declaration', () => { + const { format: formatProp } = TipTapEditor.props; + + it('exists on the component', () => { + expect(formatProp).toBeDefined(); + }); + + it('defaults to markdown', () => { + expect(formatProp.default).toBe('markdown'); + }); + + it('validator accepts markdown', () => { + expect(formatProp.validator('markdown')).toBe(true); + }); + + it('validator accepts html', () => { + expect(formatProp.validator('html')).toBe(true); + }); + + it('validator rejects anything else', () => { + expect(formatProp.validator('xml')).toBe(false); + expect(formatProp.validator('')).toBe(false); + expect(formatProp.validator('JSON')).toBe(false); + }); +}); + +describe('TipTapEditor — getContent() logic', () => { + const MARKDOWN = '**bold**'; + const HTML = '

bold

'; + + describe('when editor is not ready', () => { + it('returns empty string when editor is null', () => { + expect(getContent(null, true, 'markdown')).toBe(''); + }); + + it('returns empty string when isReady is false', () => { + const editor = makeEditorStub({ markdownOut: MARKDOWN, htmlOut: HTML }); + expect(getContent(editor, false, 'markdown')).toBe(''); + }); + }); + + describe('format="markdown" (default)', () => { + it('returns markdown from storage', () => { + const editor = makeEditorStub({ markdownOut: MARKDOWN, htmlOut: HTML }); + expect(getContent(editor, true, 'markdown')).toBe(MARKDOWN); + }); + + it('returns empty string when markdown storage is absent', () => { + const editor = { storage: {}, getHTML: () => HTML }; + expect(getContent(editor, true, 'markdown')).toBe(''); + }); + }); + + describe('format="html"', () => { + it('returns HTML from editor.getHTML()', () => { + const editor = makeEditorStub({ markdownOut: MARKDOWN, htmlOut: HTML }); + expect(getContent(editor, true, 'html')).toBe(HTML); + }); + + it('does not call getMarkdown() in html mode', () => { + const getMarkdown = jest.fn(() => MARKDOWN); + const editor = { storage: { markdown: { getMarkdown } }, getHTML: () => HTML }; + getContent(editor, true, 'html'); + expect(getMarkdown).not.toHaveBeenCalled(); + }); + + it('works even when markdown storage is absent', () => { + const editor = { storage: {}, getHTML: () => HTML }; + expect(getContent(editor, true, 'html')).toBe(HTML); + }); + }); +});