diff --git a/src/renderer/extensions/vueNodes/widgets/components/WidgetMarkdown.test.ts b/src/renderer/extensions/vueNodes/widgets/components/WidgetMarkdown.test.ts
new file mode 100644
index 000000000..836f2b357
--- /dev/null
+++ b/src/renderer/extensions/vueNodes/widgets/components/WidgetMarkdown.test.ts
@@ -0,0 +1,432 @@
+import { mount } from '@vue/test-utils'
+import PrimeVue from 'primevue/config'
+import Textarea from 'primevue/textarea'
+import { describe, expect, it, vi } from 'vitest'
+import { nextTick } from 'vue'
+
+import type { SimplifiedWidget } from '@/types/simplifiedWidget'
+
+import WidgetMarkdown from './WidgetMarkdown.vue'
+
+// Mock the markdown renderer utility
+vi.mock('@/utils/markdownRendererUtil', () => ({
+ renderMarkdownToHtml: vi.fn((markdown: string) => {
+ // Simple mock that converts some markdown to HTML
+ return markdown
+ .replace(/\*\*(.*?)\*\*/g, '$1')
+ .replace(/\*(.*?)\*/g, '$1')
+ .replace(/^# (.*?)$/gm, '
$1
')
+ .replace(/^## (.*?)$/gm, '$1
')
+ .replace(/\n/g, '
')
+ })
+}))
+
+describe('WidgetMarkdown Dual Mode Display', () => {
+ const createMockWidget = (
+ value: string = '# Default Heading\nSome **bold** text.',
+ options: Record = {},
+ callback?: (value: string) => void
+ ): SimplifiedWidget => ({
+ name: 'test_markdown',
+ type: 'string',
+ value,
+ options,
+ callback
+ })
+
+ const mountComponent = (
+ widget: SimplifiedWidget,
+ modelValue: string,
+ readonly = false
+ ) => {
+ return mount(WidgetMarkdown, {
+ global: {
+ plugins: [PrimeVue],
+ components: { Textarea }
+ },
+ props: {
+ widget,
+ modelValue,
+ readonly
+ }
+ })
+ }
+
+ const clickToEdit = async (wrapper: ReturnType) => {
+ const container = wrapper.find('.widget-markdown')
+ await container.trigger('click')
+ await nextTick()
+ return container
+ }
+
+ const blurTextarea = async (wrapper: ReturnType) => {
+ const textarea = wrapper.find('textarea')
+ if (textarea.exists()) {
+ await textarea.trigger('blur')
+ await nextTick()
+ }
+ return textarea
+ }
+
+ describe('Display Mode', () => {
+ it('renders markdown content as HTML in display mode', () => {
+ const markdown = '# Heading\nSome **bold** and *italic* text.'
+ const widget = createMockWidget(markdown)
+ const wrapper = mountComponent(widget, markdown)
+
+ const displayDiv = wrapper.find('.comfy-markdown-content')
+ expect(displayDiv.exists()).toBe(true)
+ expect(displayDiv.html()).toContain('Heading
')
+ expect(displayDiv.html()).toContain('bold')
+ expect(displayDiv.html()).toContain('italic')
+ })
+
+ it('starts in display mode by default', () => {
+ const widget = createMockWidget('# Test')
+ const wrapper = mountComponent(widget, '# Test')
+
+ expect(wrapper.find('.comfy-markdown-content').exists()).toBe(true)
+ expect(wrapper.find('textarea').exists()).toBe(false)
+ })
+
+ it('applies styling classes to display container', () => {
+ const widget = createMockWidget('# Test')
+ const wrapper = mountComponent(widget, '# Test')
+
+ const displayDiv = wrapper.find('.comfy-markdown-content')
+ expect(displayDiv.classes()).toContain('text-xs')
+ expect(displayDiv.classes()).toContain('min-h-[60px]')
+ expect(displayDiv.classes()).toContain('rounded-lg')
+ expect(displayDiv.classes()).toContain('px-4')
+ expect(displayDiv.classes()).toContain('py-2')
+ expect(displayDiv.classes()).toContain('overflow-y-auto')
+ })
+
+ it('handles empty markdown content', () => {
+ const widget = createMockWidget('')
+ const wrapper = mountComponent(widget, '')
+
+ const displayDiv = wrapper.find('.comfy-markdown-content')
+ expect(displayDiv.exists()).toBe(true)
+ expect(displayDiv.text()).toBe('')
+ })
+ })
+
+ describe('Edit Mode Toggle', () => {
+ it('switches to edit mode when clicked', async () => {
+ const widget = createMockWidget('# Test')
+ const wrapper = mountComponent(widget, '# Test')
+
+ expect(wrapper.find('.comfy-markdown-content').exists()).toBe(true)
+
+ await clickToEdit(wrapper)
+
+ expect(wrapper.find('.comfy-markdown-content').exists()).toBe(false)
+ expect(wrapper.find('textarea').exists()).toBe(true)
+ })
+
+ it('does not switch to edit mode when readonly', async () => {
+ const widget = createMockWidget('# Test')
+ const wrapper = mountComponent(widget, '# Test', true)
+
+ await clickToEdit(wrapper)
+
+ expect(wrapper.find('.comfy-markdown-content').exists()).toBe(true)
+ expect(wrapper.find('textarea').exists()).toBe(false)
+ })
+
+ it('does not switch to edit mode when already editing', async () => {
+ const widget = createMockWidget('# Test')
+ const wrapper = mountComponent(widget, '# Test')
+
+ // First click to enter edit mode
+ await clickToEdit(wrapper)
+ expect(wrapper.find('textarea').exists()).toBe(true)
+
+ // Second click should not have any effect
+ await clickToEdit(wrapper)
+ expect(wrapper.find('textarea').exists()).toBe(true)
+ })
+
+ it('switches back to display mode on textarea blur', async () => {
+ const widget = createMockWidget('# Test')
+ const wrapper = mountComponent(widget, '# Test')
+
+ await clickToEdit(wrapper)
+ expect(wrapper.find('textarea').exists()).toBe(true)
+
+ await blurTextarea(wrapper)
+ expect(wrapper.find('.comfy-markdown-content').exists()).toBe(true)
+ expect(wrapper.find('textarea').exists()).toBe(false)
+ })
+ })
+
+ describe('Edit Mode', () => {
+ it('displays textarea with current value when editing', async () => {
+ const markdown = '# Original Content'
+ const widget = createMockWidget(markdown)
+ const wrapper = mountComponent(widget, markdown)
+
+ await clickToEdit(wrapper)
+
+ const textarea = wrapper.find('textarea')
+ expect(textarea.exists()).toBe(true)
+ expect(textarea.element.value).toBe('# Original Content')
+ })
+
+ it('applies styling and configuration to textarea', async () => {
+ const widget = createMockWidget('# Test')
+ const wrapper = mountComponent(widget, '# Test')
+
+ await clickToEdit(wrapper)
+
+ const textarea = wrapper.findComponent({ name: 'Textarea' })
+ expect(textarea.props('size')).toBe('small')
+ // Check rows attribute in the DOM instead of props
+ const textareaElement = wrapper.find('textarea')
+ expect(textareaElement.attributes('rows')).toBe('6')
+ expect(textarea.classes()).toContain('text-xs')
+ expect(textarea.classes()).toContain('w-full')
+ })
+
+ it('disables textarea when readonly', async () => {
+ const widget = createMockWidget('# Test')
+ const wrapper = mountComponent(widget, '# Test', true)
+
+ // Readonly should prevent entering edit mode
+ await clickToEdit(wrapper)
+ expect(wrapper.find('textarea').exists()).toBe(false)
+ })
+
+ it('stops click and keydown event propagation in edit mode', async () => {
+ const widget = createMockWidget('# Test')
+ const wrapper = mountComponent(widget, '# Test')
+
+ await clickToEdit(wrapper)
+
+ const textarea = wrapper.find('textarea')
+ const clickSpy = vi.fn()
+ const keydownSpy = vi.fn()
+
+ wrapper.element.addEventListener('click', clickSpy)
+ wrapper.element.addEventListener('keydown', keydownSpy)
+
+ await textarea.trigger('click')
+ await textarea.trigger('keydown', { key: 'Enter' })
+
+ // Events should be stopped from propagating
+ expect(clickSpy).not.toHaveBeenCalled()
+ expect(keydownSpy).not.toHaveBeenCalled()
+ })
+ })
+
+ describe('Value Updates', () => {
+ it('emits update:modelValue when textarea content changes', async () => {
+ const widget = createMockWidget('# Original')
+ const wrapper = mountComponent(widget, '# Original')
+
+ await clickToEdit(wrapper)
+
+ const textarea = wrapper.find('textarea')
+ await textarea.setValue('# Updated Content')
+ await textarea.trigger('input')
+
+ const emitted = wrapper.emitted('update:modelValue')
+ expect(emitted).toBeDefined()
+ expect(emitted![emitted!.length - 1]).toEqual(['# Updated Content'])
+ })
+
+ it('renders updated HTML after value change and blur', async () => {
+ const widget = createMockWidget('# Original')
+ const wrapper = mountComponent(widget, '# Original')
+
+ await clickToEdit(wrapper)
+
+ const textarea = wrapper.find('textarea')
+ await textarea.setValue('## New Heading\nWith **bold** text')
+ await textarea.trigger('input')
+ await blurTextarea(wrapper)
+
+ const displayDiv = wrapper.find('.comfy-markdown-content')
+ expect(displayDiv.html()).toContain('New Heading
')
+ expect(displayDiv.html()).toContain('bold')
+ })
+
+ it('emits update:modelValue for callback handling at parent level', async () => {
+ const widget = createMockWidget('# Test', {})
+ const wrapper = mountComponent(widget, '# Test')
+
+ await clickToEdit(wrapper)
+
+ const textarea = wrapper.find('textarea')
+ await textarea.setValue('# Changed')
+ await textarea.trigger('input')
+
+ // The widget should emit the change for parent (NodeWidgets) to handle callbacks
+ const emitted = wrapper.emitted('update:modelValue')
+ expect(emitted).toBeDefined()
+ expect(emitted![emitted!.length - 1]).toEqual(['# Changed'])
+ })
+
+ it('handles missing callback gracefully', async () => {
+ const widget = createMockWidget('# Test', {}, undefined)
+ const wrapper = mountComponent(widget, '# Test')
+
+ await clickToEdit(wrapper)
+
+ const textarea = wrapper.find('textarea')
+ await textarea.setValue('# Changed')
+
+ // Should not throw error and should still emit Vue event
+ await expect(textarea.trigger('input')).resolves.not.toThrow()
+
+ const emitted = wrapper.emitted('update:modelValue')
+ expect(emitted).toBeDefined()
+ })
+ })
+
+ describe('Complex Markdown Rendering', () => {
+ it('handles multiple markdown elements', () => {
+ const complexMarkdown = `# Main Heading
+## Subheading
+This paragraph has **bold** and *italic* text.
+Another line with more content.`
+
+ const widget = createMockWidget(complexMarkdown)
+ const wrapper = mountComponent(widget, complexMarkdown)
+
+ const displayDiv = wrapper.find('.comfy-markdown-content')
+ expect(displayDiv.html()).toContain('Main Heading
')
+ expect(displayDiv.html()).toContain('Subheading
')
+ expect(displayDiv.html()).toContain('bold')
+ expect(displayDiv.html()).toContain('italic')
+ })
+
+ it('handles line breaks in markdown', () => {
+ const markdownWithBreaks = 'Line 1\nLine 2\nLine 3'
+ const widget = createMockWidget(markdownWithBreaks)
+ const wrapper = mountComponent(widget, markdownWithBreaks)
+
+ const displayDiv = wrapper.find('.comfy-markdown-content')
+ expect(displayDiv.html()).toContain('
')
+ })
+
+ it('handles empty or whitespace-only markdown', () => {
+ const whitespaceMarkdown = ' \n\n '
+ const widget = createMockWidget(whitespaceMarkdown)
+ const wrapper = mountComponent(widget, whitespaceMarkdown)
+
+ const displayDiv = wrapper.find('.comfy-markdown-content')
+ expect(displayDiv.exists()).toBe(true)
+ })
+ })
+
+ describe('Edge Cases', () => {
+ it('handles very long markdown content', async () => {
+ const longMarkdown = '# Heading\n' + 'Lorem ipsum '.repeat(1000)
+ const widget = createMockWidget(longMarkdown)
+ const wrapper = mountComponent(widget, longMarkdown)
+
+ // Should render without issues
+ const displayDiv = wrapper.find('.comfy-markdown-content')
+ expect(displayDiv.exists()).toBe(true)
+
+ // Should switch to edit mode
+ await clickToEdit(wrapper)
+ const textarea = wrapper.find('textarea')
+ expect(textarea.exists()).toBe(true)
+ expect(textarea.element.value).toBe(longMarkdown)
+ })
+
+ it('handles special characters in markdown', async () => {
+ const specialChars = '# Special: @#$%^&*()[]{}|\\:";\'<>?,./'
+ const widget = createMockWidget(specialChars)
+ const wrapper = mountComponent(widget, specialChars)
+
+ await clickToEdit(wrapper)
+ const textarea = wrapper.find('textarea')
+ expect(textarea.element.value).toBe(specialChars)
+ })
+
+ it('handles unicode characters', async () => {
+ const unicode = '# Unicode: 🎨 αβγ 䏿–‡ العربية 🚀'
+ const widget = createMockWidget(unicode)
+ const wrapper = mountComponent(widget, unicode)
+
+ await clickToEdit(wrapper)
+ const textarea = wrapper.find('textarea')
+ expect(textarea.element.value).toBe(unicode)
+
+ await textarea.setValue(unicode + ' more unicode')
+ await textarea.trigger('input')
+
+ const emitted = wrapper.emitted('update:modelValue')
+ expect(emitted).toBeDefined()
+ expect(emitted![emitted!.length - 1]).toEqual([unicode + ' more unicode'])
+ })
+
+ it('handles rapid edit mode toggling', async () => {
+ const widget = createMockWidget('# Test')
+ const wrapper = mountComponent(widget, '# Test')
+
+ // Rapid toggling
+ await clickToEdit(wrapper)
+ expect(wrapper.find('textarea').exists()).toBe(true)
+
+ await blurTextarea(wrapper)
+ expect(wrapper.find('.comfy-markdown-content').exists()).toBe(true)
+
+ await clickToEdit(wrapper)
+ expect(wrapper.find('textarea').exists()).toBe(true)
+ })
+ })
+
+ describe('Styling and Layout', () => {
+ it('applies widget-markdown class to container', () => {
+ const widget = createMockWidget('# Test')
+ const wrapper = mountComponent(widget, '# Test')
+
+ const container = wrapper.find('.widget-markdown')
+ expect(container.exists()).toBe(true)
+ expect(container.classes()).toContain('relative')
+ expect(container.classes()).toContain('w-full')
+ expect(container.classes()).toContain('cursor-text')
+ })
+
+ it('applies overflow handling to display mode', () => {
+ const widget = createMockWidget(
+ '# Long Content\n' + 'Content '.repeat(100)
+ )
+ const wrapper = mountComponent(
+ widget,
+ '# Long Content\n' + 'Content '.repeat(100)
+ )
+
+ const displayDiv = wrapper.find('.comfy-markdown-content')
+ expect(displayDiv.classes()).toContain('overflow-y-auto')
+ })
+ })
+
+ describe('Focus Management', () => {
+ it('creates textarea reference when entering edit mode', async () => {
+ const widget = createMockWidget('# Test')
+ const wrapper = mountComponent(widget, '# Test')
+ const vm = wrapper.vm as InstanceType
+
+ // Test that the component creates a textarea reference when entering edit mode
+ // @ts-expect-error - isEditing is not exposed
+ expect(vm.isEditing).toBe(false)
+
+ // @ts-expect-error - startEditing is not exposed
+ await vm.startEditing()
+
+ // @ts-expect-error - isEditing is not exposed
+ expect(vm.isEditing).toBe(true)
+ await wrapper.vm.$nextTick()
+
+ // Check that textarea exists after entering edit mode
+ const textarea = wrapper.findComponent({ name: 'Textarea' })
+ expect(textarea.exists()).toBe(true)
+ })
+ })
+})