From 7a894e9ae85eb0274f02ae42322368ebaf7eeea2 Mon Sep 17 00:00:00 2001 From: pythongosssss <125205205+pythongosssss@users.noreply.github.com> Date: Fri, 16 Jan 2026 11:38:38 -0800 Subject: [PATCH 1/6] decouple node help between sidebar and right panel - Extract useNodeHelpContent composable so NodeHelpContent fetches its own content, allowing multiple panels to show help independently - Remove sync behavior from NodeHelpPage that caused left sidebar to change when selecting different graph nodes - Add telemetry tracking for node library help button --- .../graph/selectionToolbox/InfoButton.test.ts | 96 +---- src/components/node/NodeHelpContent.vue | 10 +- .../rightSidePanel/info/TabInfo.vue | 12 - .../tabs/nodeLibrary/NodeHelpPage.test.ts | 100 ----- .../sidebar/tabs/nodeLibrary/NodeHelpPage.vue | 19 - .../sidebar/tabs/nodeLibrary/NodeTreeLeaf.vue | 10 +- src/composables/useNodeHelpContent.test.ts | 365 ++++++++++++++++++ src/composables/useNodeHelpContent.ts | 69 ++++ src/stores/workspace/nodeHelpStore.test.ts | 356 +---------------- src/stores/workspace/nodeHelpStore.ts | 49 +-- 10 files changed, 458 insertions(+), 628 deletions(-) delete mode 100644 src/components/sidebar/tabs/nodeLibrary/NodeHelpPage.test.ts create mode 100644 src/composables/useNodeHelpContent.test.ts create mode 100644 src/composables/useNodeHelpContent.ts diff --git a/src/components/graph/selectionToolbox/InfoButton.test.ts b/src/components/graph/selectionToolbox/InfoButton.test.ts index da2a13831d5..1d0e92f9138 100644 --- a/src/components/graph/selectionToolbox/InfoButton.test.ts +++ b/src/components/graph/selectionToolbox/InfoButton.test.ts @@ -6,67 +6,15 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' import { createI18n } from 'vue-i18n' import InfoButton from '@/components/graph/selectionToolbox/InfoButton.vue' -// NOTE: The component import must come after mocks so they take effect. -import { useCanvasStore } from '@/renderer/core/canvas/canvasStore' -import { useNodeDefStore } from '@/stores/nodeDefStore' -const mockLGraphNode = { - type: 'TestNode', - title: 'Test Node' -} - -vi.mock('@/utils/litegraphUtil', () => ({ - isLGraphNode: vi.fn(() => true) -})) - -vi.mock('@/composables/sidebarTabs/useNodeLibrarySidebarTab', () => ({ - useNodeLibrarySidebarTab: () => ({ - id: 'node-library' - }) -})) - -const openHelpMock = vi.fn() -const closeHelpMock = vi.fn() -const nodeHelpState: { currentHelpNode: any } = { currentHelpNode: null } -vi.mock('@/stores/workspace/nodeHelpStore', () => ({ - useNodeHelpStore: () => ({ - openHelp: (def: any) => { - nodeHelpState.currentHelpNode = def - openHelpMock(def) - }, - closeHelp: () => { - nodeHelpState.currentHelpNode = null - closeHelpMock() - }, - get currentHelpNode() { - return nodeHelpState.currentHelpNode - }, - get isHelpOpen() { - return nodeHelpState.currentHelpNode !== null - } - }) -})) - -const toggleSidebarTabMock = vi.fn((id: string) => { - sidebarState.activeSidebarTabId = - sidebarState.activeSidebarTabId === id ? null : id -}) -const sidebarState: { activeSidebarTabId: string | null } = { - activeSidebarTabId: 'other-tab' -} -vi.mock('@/stores/workspace/sidebarTabStore', () => ({ - useSidebarTabStore: () => ({ - get activeSidebarTabId() { - return sidebarState.activeSidebarTabId - }, - toggleSidebarTab: toggleSidebarTabMock +const openPanelMock = vi.fn() +vi.mock('@/stores/workspace/rightSidePanelStore', () => ({ + useRightSidePanelStore: () => ({ + openPanel: openPanelMock }) })) describe('InfoButton', () => { - let canvasStore: ReturnType - let nodeDefStore: ReturnType - const i18n = createI18n({ legacy: false, locale: 'en', @@ -81,9 +29,6 @@ describe('InfoButton', () => { beforeEach(() => { setActivePinia(createPinia()) - canvasStore = useCanvasStore() - nodeDefStore = useNodeDefStore() - vi.clearAllMocks() }) @@ -96,7 +41,7 @@ describe('InfoButton', () => { 'i-lucide:info': true, Button: { template: - '', + '', props: ['severity', 'text', 'class'], emits: ['click'] } @@ -105,45 +50,18 @@ describe('InfoButton', () => { }) } - it('should handle click without errors', async () => { - const mockNodeDef = { - nodePath: 'test/node', - display_name: 'Test Node' - } - canvasStore.selectedItems = [mockLGraphNode] as any - vi.spyOn(nodeDefStore, 'fromLGraphNode').mockReturnValue(mockNodeDef as any) + it('should open the info panel on click', async () => { const wrapper = mountComponent() const button = wrapper.find('button') await button.trigger('click') - expect(button.exists()).toBe(true) + expect(openPanelMock).toHaveBeenCalledWith('info') }) it('should have correct CSS classes', () => { - const mockNodeDef = { - nodePath: 'test/node', - display_name: 'Test Node' - } - canvasStore.selectedItems = [mockLGraphNode] as any - vi.spyOn(nodeDefStore, 'fromLGraphNode').mockReturnValue(mockNodeDef as any) - const wrapper = mountComponent() const button = wrapper.find('button') expect(button.classes()).toContain('help-button') expect(button.attributes('severity')).toBe('secondary') }) - - it('should have correct tooltip', () => { - const mockNodeDef = { - nodePath: 'test/node', - display_name: 'Test Node' - } - canvasStore.selectedItems = [mockLGraphNode] as any - vi.spyOn(nodeDefStore, 'fromLGraphNode').mockReturnValue(mockNodeDef as any) - - const wrapper = mountComponent() - const button = wrapper.find('button') - - expect(button.exists()).toBe(true) - }) }) diff --git a/src/components/node/NodeHelpContent.vue b/src/components/node/NodeHelpContent.vue index 8cf058f813e..ad1f75e3df4 100644 --- a/src/components/node/NodeHelpContent.vue +++ b/src/components/node/NodeHelpContent.vue @@ -70,17 +70,17 @@ diff --git a/src/components/sidebar/tabs/nodeLibrary/NodeTreeLeaf.vue b/src/components/sidebar/tabs/nodeLibrary/NodeTreeLeaf.vue index fc3cabfcc54..325cf44f599 100644 --- a/src/components/sidebar/tabs/nodeLibrary/NodeTreeLeaf.vue +++ b/src/components/sidebar/tabs/nodeLibrary/NodeTreeLeaf.vue @@ -57,7 +57,7 @@ variant="muted-textonly" size="icon-sm" :aria-label="$t('g.learnMore')" - @click.stop="props.openNodeHelp(nodeDef)" + @click.stop="onHelpClick" > @@ -85,6 +85,7 @@ import TreeExplorerTreeNode from '@/components/common/TreeExplorerTreeNode.vue' import NodePreview from '@/components/node/NodePreview.vue' import Button from '@/components/ui/button/Button.vue' import { useSettingStore } from '@/platform/settings/settingStore' +import { useTelemetry } from '@/platform/telemetry' import { useNodeBookmarkStore } from '@/stores/nodeBookmarkStore' import type { ComfyNodeDefImpl } from '@/stores/nodeDefStore' import { useSubgraphStore } from '@/stores/subgraphStore' @@ -112,6 +113,13 @@ const sidebarLocation = computed<'left' | 'right'>(() => const toggleBookmark = async () => { await nodeBookmarkStore.toggleBookmark(nodeDef.value) } + +const onHelpClick = () => { + useTelemetry()?.trackUiButtonClicked({ + button_id: 'node_library_help_button' + }) + props.openNodeHelp(nodeDef.value) +} const editBlueprint = async () => { if (!props.node.data) throw new Error( diff --git a/src/composables/useNodeHelpContent.test.ts b/src/composables/useNodeHelpContent.test.ts new file mode 100644 index 00000000000..104ca5de15e --- /dev/null +++ b/src/composables/useNodeHelpContent.test.ts @@ -0,0 +1,365 @@ +import { flushPromises } from '@vue/test-utils' +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { nextTick, ref } from 'vue' + +import { useNodeHelpContent } from '@/composables/useNodeHelpContent' + +vi.mock('@/scripts/api', () => ({ + api: { + fileURL: vi.fn((url) => url) + } +})) + +vi.mock('vue-i18n', () => ({ + useI18n: () => ({ + locale: ref('en') + }) +})) + +vi.mock('@/types/nodeSource', () => ({ + NodeSourceType: { + Core: 'core', + CustomNodes: 'custom_nodes' + }, + getNodeSource: vi.fn((pythonModule) => { + if (pythonModule?.startsWith('custom_nodes.')) { + return { type: 'custom_nodes' } + } + return { type: 'core' } + }) +})) + +vi.mock('dompurify', () => ({ + default: { + sanitize: vi.fn((html) => html) + } +})) + +vi.mock('marked', () => ({ + marked: { + parse: vi.fn((markdown, options) => { + if (options?.renderer) { + if (markdown.includes('![')) { + const matches = markdown.match(/!\[(.*?)\]\((.*?)\)/) + if (matches) { + const [, text, href] = matches + return options.renderer.image({ href, text, title: '' }) + } + } + } + return `

${markdown}

` + }) + }, + Renderer: class Renderer { + image = vi.fn( + ({ href, title, text }) => + `${text}` + ) + link = vi.fn( + ({ href, title, text }) => + `${text}` + ) + } +})) + +describe('useNodeHelpContent', () => { + // Define a mock node for testing + const mockCoreNode = { + name: 'TestNode', + display_name: 'Test Node', + description: 'A test node', + inputs: {}, + outputs: [], + python_module: 'comfy.test_node' + } + + const mockCustomNode = { + name: 'CustomNode', + display_name: 'Custom Node', + description: 'A custom node', + inputs: {}, + outputs: [], + python_module: 'custom_nodes.test_module.custom@1.0.0' + } + + // Mock fetch responses + const mockFetch = vi.fn() + global.fetch = mockFetch + + beforeEach(() => { + mockFetch.mockReset() + }) + + it('should generate correct baseUrl for core nodes', async () => { + const nodeRef = ref(mockCoreNode as any) + mockFetch.mockResolvedValueOnce({ + ok: true, + text: async () => '# Test' + }) + + const { baseUrl } = useNodeHelpContent(nodeRef) + await nextTick() + + expect(baseUrl.value).toBe(`/docs/${mockCoreNode.name}/`) + }) + + it('should generate correct baseUrl for custom nodes', async () => { + const nodeRef = ref(mockCustomNode as any) + mockFetch.mockResolvedValueOnce({ + ok: true, + text: async () => '# Test' + }) + + const { baseUrl } = useNodeHelpContent(nodeRef) + await nextTick() + + expect(baseUrl.value).toBe('/extensions/test_module/docs/') + }) + + it('should render markdown content correctly', async () => { + const nodeRef = ref(mockCoreNode as any) + mockFetch.mockResolvedValueOnce({ + ok: true, + text: async () => '# Test Help\nThis is test help content' + }) + + const { renderedHelpHtml } = useNodeHelpContent(nodeRef) + await flushPromises() + + expect(renderedHelpHtml.value).toContain('This is test help content') + }) + + it('should handle fetch errors and fall back to description', async () => { + const nodeRef = ref(mockCoreNode as any) + mockFetch.mockResolvedValueOnce({ + ok: false, + statusText: 'Not Found' + }) + + const { error, renderedHelpHtml } = useNodeHelpContent(nodeRef) + await flushPromises() + + expect(error.value).toBe('Not Found') + expect(renderedHelpHtml.value).toContain(mockCoreNode.description) + }) + + it('should include alt attribute for images', async () => { + const nodeRef = ref(mockCustomNode as any) + mockFetch.mockResolvedValueOnce({ + ok: true, + text: async () => '![image](test.jpg)' + }) + + const { renderedHelpHtml } = useNodeHelpContent(nodeRef) + await flushPromises() + + expect(renderedHelpHtml.value).toContain('alt="image"') + }) + + it('should prefix relative video src in custom nodes', async () => { + const nodeRef = ref(mockCustomNode as any) + mockFetch.mockResolvedValueOnce({ + ok: true, + text: async () => '' + }) + + const { renderedHelpHtml } = useNodeHelpContent(nodeRef) + await flushPromises() + + expect(renderedHelpHtml.value).toContain( + 'src="/extensions/test_module/docs/video.mp4"' + ) + }) + + it('should prefix relative video src for core nodes with node-specific base URL', async () => { + const nodeRef = ref(mockCoreNode as any) + mockFetch.mockResolvedValueOnce({ + ok: true, + text: async () => '' + }) + + const { renderedHelpHtml } = useNodeHelpContent(nodeRef) + await flushPromises() + + expect(renderedHelpHtml.value).toContain( + `src="/docs/${mockCoreNode.name}/video.mp4"` + ) + }) + + it('should handle loading state', async () => { + const nodeRef = ref(mockCoreNode as any) + mockFetch.mockImplementationOnce(() => new Promise(() => {})) // Never resolves + + const { isLoading } = useNodeHelpContent(nodeRef) + await nextTick() + + expect(isLoading.value).toBe(true) + }) + + it('should try fallback URL for custom nodes', async () => { + const nodeRef = ref(mockCustomNode as any) + mockFetch + .mockResolvedValueOnce({ + ok: false, + statusText: 'Not Found' + }) + .mockResolvedValueOnce({ + ok: true, + text: async () => '# Fallback content' + }) + + useNodeHelpContent(nodeRef) + await flushPromises() + + expect(mockFetch).toHaveBeenCalledTimes(2) + expect(mockFetch).toHaveBeenCalledWith( + '/extensions/test_module/docs/CustomNode/en.md' + ) + expect(mockFetch).toHaveBeenCalledWith( + '/extensions/test_module/docs/CustomNode.md' + ) + }) + + it('should prefix relative source src in custom nodes', async () => { + const nodeRef = ref(mockCustomNode as any) + mockFetch.mockResolvedValueOnce({ + ok: true, + text: async () => + '' + }) + + const { renderedHelpHtml } = useNodeHelpContent(nodeRef) + await flushPromises() + + expect(renderedHelpHtml.value).toContain( + 'src="/extensions/test_module/docs/video.mp4"' + ) + }) + + it('should prefix relative source src for core nodes with node-specific base URL', async () => { + const nodeRef = ref(mockCoreNode as any) + mockFetch.mockResolvedValueOnce({ + ok: true, + text: async () => + '' + }) + + const { renderedHelpHtml } = useNodeHelpContent(nodeRef) + await flushPromises() + + expect(renderedHelpHtml.value).toContain( + `src="/docs/${mockCoreNode.name}/video.webm"` + ) + }) + + it('should prefix relative img src in raw HTML for custom nodes', async () => { + const nodeRef = ref(mockCustomNode as any) + mockFetch.mockResolvedValueOnce({ + ok: true, + text: async () => '# Test\nTest image' + }) + + const { renderedHelpHtml } = useNodeHelpContent(nodeRef) + await flushPromises() + + expect(renderedHelpHtml.value).toContain( + 'src="/extensions/test_module/docs/image.png"' + ) + expect(renderedHelpHtml.value).toContain('alt="Test image"') + }) + + it('should prefix relative img src in raw HTML for core nodes', async () => { + const nodeRef = ref(mockCoreNode as any) + mockFetch.mockResolvedValueOnce({ + ok: true, + text: async () => '# Test\nTest image' + }) + + const { renderedHelpHtml } = useNodeHelpContent(nodeRef) + await flushPromises() + + expect(renderedHelpHtml.value).toContain( + `src="/docs/${mockCoreNode.name}/image.png"` + ) + expect(renderedHelpHtml.value).toContain('alt="Test image"') + }) + + it('should not prefix absolute img src in raw HTML', async () => { + const nodeRef = ref(mockCustomNode as any) + mockFetch.mockResolvedValueOnce({ + ok: true, + text: async () => 'Absolute' + }) + + const { renderedHelpHtml } = useNodeHelpContent(nodeRef) + await flushPromises() + + expect(renderedHelpHtml.value).toContain('src="/absolute/image.png"') + expect(renderedHelpHtml.value).toContain('alt="Absolute"') + }) + + it('should not prefix external img src in raw HTML', async () => { + const nodeRef = ref(mockCustomNode as any) + mockFetch.mockResolvedValueOnce({ + ok: true, + text: async () => + 'External' + }) + + const { renderedHelpHtml } = useNodeHelpContent(nodeRef) + await flushPromises() + + expect(renderedHelpHtml.value).toContain( + 'src="https://example.com/image.png"' + ) + expect(renderedHelpHtml.value).toContain('alt="External"') + }) + + it('should handle various quote styles in media src attributes', async () => { + const nodeRef = ref(mockCoreNode as any) + mockFetch.mockResolvedValueOnce({ + ok: true, + text: async () => `# Media Test + +Testing quote styles in properly formed HTML: + + + +Double quotes +Single quotes + + + +The MEDIA_SRC_REGEX handles both single and double quotes in img, video and source tags.` + }) + + const { renderedHelpHtml } = useNodeHelpContent(nodeRef) + await flushPromises() + + // Check that all media elements with different quote styles are prefixed correctly + // Double quotes remain as double quotes + expect(renderedHelpHtml.value).toContain( + `src="/docs/${mockCoreNode.name}/video1.mp4"` + ) + expect(renderedHelpHtml.value).toContain( + `src="/docs/${mockCoreNode.name}/image1.png"` + ) + expect(renderedHelpHtml.value).toContain( + `src="/docs/${mockCoreNode.name}/video3.mp4"` + ) + + // Single quotes remain as single quotes in the output + expect(renderedHelpHtml.value).toContain( + `src='/docs/${mockCoreNode.name}/video2.mp4'` + ) + expect(renderedHelpHtml.value).toContain( + `src='/docs/${mockCoreNode.name}/image2.png'` + ) + expect(renderedHelpHtml.value).toContain( + `src='/docs/${mockCoreNode.name}/video3.webm'` + ) + }) +}) diff --git a/src/composables/useNodeHelpContent.ts b/src/composables/useNodeHelpContent.ts new file mode 100644 index 00000000000..72053399c1c --- /dev/null +++ b/src/composables/useNodeHelpContent.ts @@ -0,0 +1,69 @@ +import type { MaybeRefOrGetter } from 'vue' +import { computed, ref, toValue, watch } from 'vue' +import { useI18n } from 'vue-i18n' + +import { nodeHelpService } from '@/services/nodeHelpService' +import type { ComfyNodeDefImpl } from '@/stores/nodeDefStore' +import { renderMarkdownToHtml } from '@/utils/markdownRendererUtil' +import { getNodeHelpBaseUrl } from '@/workbench/utils/nodeHelpUtil' + +/** + * Composable for fetching and rendering node help content. + * Creates independent state for each usage, allowing multiple panels + * to show help content without interfering with each other. + * + * @param nodeRef - Reactive reference to the node to show help for + * @returns Reactive help content state and rendered HTML + */ +export function useNodeHelpContent( + nodeRef: MaybeRefOrGetter +) { + const { locale } = useI18n() + + const helpContent = ref('') + const isLoading = ref(false) + const error = ref(null) + + const baseUrl = computed(() => { + const node = toValue(nodeRef) + if (!node) return '' + return getNodeHelpBaseUrl(node) + }) + + const renderedHelpHtml = computed(() => { + return renderMarkdownToHtml(helpContent.value, baseUrl.value) + }) + + // Watch for node changes and fetch help content + watch( + () => toValue(nodeRef), + async (node) => { + helpContent.value = '' + error.value = null + + if (node) { + isLoading.value = true + try { + helpContent.value = await nodeHelpService.fetchNodeHelp( + node, + locale.value || 'en' + ) + } catch (e: unknown) { + error.value = e instanceof Error ? e.message : String(e) + helpContent.value = node.description || '' + } finally { + isLoading.value = false + } + } + }, + { immediate: true } + ) + + return { + helpContent, + isLoading, + error, + baseUrl, + renderedHelpHtml + } +} diff --git a/src/stores/workspace/nodeHelpStore.test.ts b/src/stores/workspace/nodeHelpStore.test.ts index dc8b7b466ab..0514e9029d7 100644 --- a/src/stores/workspace/nodeHelpStore.test.ts +++ b/src/stores/workspace/nodeHelpStore.test.ts @@ -1,74 +1,9 @@ -import { flushPromises } from '@vue/test-utils' import { createPinia, setActivePinia } from 'pinia' -import { beforeEach, describe, expect, it, vi } from 'vitest' -import { nextTick } from 'vue' +import { beforeEach, describe, expect, it } from 'vitest' import { useNodeHelpStore } from '@/stores/workspace/nodeHelpStore' -vi.mock('@/scripts/api', () => ({ - api: { - fileURL: vi.fn((url) => url) - } -})) - -vi.mock('@/i18n', () => ({ - i18n: { - global: { - locale: { - value: 'en' - } - } - } -})) - -vi.mock('@/types/nodeSource', () => ({ - NodeSourceType: { - Core: 'core', - CustomNodes: 'custom_nodes' - }, - getNodeSource: vi.fn((pythonModule) => { - if (pythonModule?.startsWith('custom_nodes.')) { - return { type: 'custom_nodes' } - } - return { type: 'core' } - }) -})) - -vi.mock('dompurify', () => ({ - default: { - sanitize: vi.fn((html) => html) - } -})) - -vi.mock('marked', () => ({ - marked: { - parse: vi.fn((markdown, options) => { - if (options?.renderer) { - if (markdown.includes('![')) { - const matches = markdown.match(/!\[(.*?)\]\((.*?)\)/) - if (matches) { - const [, text, href] = matches - return options.renderer.image({ href, text, title: '' }) - } - } - } - return `

${markdown}

` - }) - }, - Renderer: class Renderer { - image = vi.fn( - ({ href, title, text }) => - `${text}` - ) - link = vi.fn( - ({ href, title, text }) => - `${text}` - ) - } -})) - describe('nodeHelpStore', () => { - // Define a mock node for testing const mockCoreNode = { name: 'TestNode', display_name: 'Test Node', @@ -78,23 +13,8 @@ describe('nodeHelpStore', () => { python_module: 'comfy.test_node' } - const mockCustomNode = { - name: 'CustomNode', - display_name: 'Custom Node', - description: 'A custom node', - inputs: {}, - outputs: [], - python_module: 'custom_nodes.test_module.custom@1.0.0' - } - - // Mock fetch responses - const mockFetch = vi.fn() - global.fetch = mockFetch - beforeEach(() => { - // Setup Pinia setActivePinia(createPinia()) - mockFetch.mockReset() }) it('should initialize with empty state', () => { @@ -122,278 +42,4 @@ describe('nodeHelpStore', () => { expect(nodeHelpStore.currentHelpNode).toBeNull() expect(nodeHelpStore.isHelpOpen).toBe(false) }) - - it('should generate correct baseUrl for core nodes', async () => { - const nodeHelpStore = useNodeHelpStore() - - nodeHelpStore.openHelp(mockCoreNode as any) - await nextTick() - - expect(nodeHelpStore.baseUrl).toBe(`/docs/${mockCoreNode.name}/`) - }) - - it('should generate correct baseUrl for custom nodes', async () => { - const nodeHelpStore = useNodeHelpStore() - - nodeHelpStore.openHelp(mockCustomNode as any) - await nextTick() - - expect(nodeHelpStore.baseUrl).toBe('/extensions/test_module/docs/') - }) - - it('should render markdown content correctly', async () => { - const nodeHelpStore = useNodeHelpStore() - - mockFetch.mockResolvedValueOnce({ - ok: true, - text: async () => '# Test Help\nThis is test help content' - }) - - nodeHelpStore.openHelp(mockCoreNode as any) - await flushPromises() - - expect(nodeHelpStore.renderedHelpHtml).toContain( - 'This is test help content' - ) - }) - - it('should handle fetch errors and fall back to description', async () => { - const nodeHelpStore = useNodeHelpStore() - - mockFetch.mockResolvedValueOnce({ - ok: false, - statusText: 'Not Found' - }) - - nodeHelpStore.openHelp(mockCoreNode as any) - await flushPromises() - - expect(nodeHelpStore.error).toBe('Not Found') - expect(nodeHelpStore.renderedHelpHtml).toContain(mockCoreNode.description) - }) - - it('should include alt attribute for images', async () => { - const nodeHelpStore = useNodeHelpStore() - - mockFetch.mockResolvedValueOnce({ - ok: true, - text: async () => '![image](test.jpg)' - }) - - nodeHelpStore.openHelp(mockCustomNode as any) - await flushPromises() - expect(nodeHelpStore.renderedHelpHtml).toContain('alt="image"') - }) - - it('should prefix relative video src in custom nodes', async () => { - const nodeHelpStore = useNodeHelpStore() - - mockFetch.mockResolvedValueOnce({ - ok: true, - text: async () => '' - }) - - nodeHelpStore.openHelp(mockCustomNode as any) - await flushPromises() - expect(nodeHelpStore.renderedHelpHtml).toContain( - 'src="/extensions/test_module/docs/video.mp4"' - ) - }) - - it('should prefix relative video src for core nodes with node-specific base URL', async () => { - const nodeHelpStore = useNodeHelpStore() - - mockFetch.mockResolvedValueOnce({ - ok: true, - text: async () => '' - }) - - nodeHelpStore.openHelp(mockCoreNode as any) - await flushPromises() - expect(nodeHelpStore.renderedHelpHtml).toContain( - `src="/docs/${mockCoreNode.name}/video.mp4"` - ) - }) - - it('should prefix relative source src in custom nodes', async () => { - const nodeHelpStore = useNodeHelpStore() - - mockFetch.mockResolvedValueOnce({ - ok: true, - text: async () => - '' - }) - - nodeHelpStore.openHelp(mockCustomNode as any) - await flushPromises() - expect(nodeHelpStore.renderedHelpHtml).toContain( - 'src="/extensions/test_module/docs/video.mp4"' - ) - }) - - it('should prefix relative source src for core nodes with node-specific base URL', async () => { - const nodeHelpStore = useNodeHelpStore() - - mockFetch.mockResolvedValueOnce({ - ok: true, - text: async () => - '' - }) - - nodeHelpStore.openHelp(mockCoreNode as any) - await flushPromises() - expect(nodeHelpStore.renderedHelpHtml).toContain( - `src="/docs/${mockCoreNode.name}/video.webm"` - ) - }) - - it('should handle loading state', async () => { - const nodeHelpStore = useNodeHelpStore() - - mockFetch.mockImplementationOnce(() => new Promise(() => {})) // Never resolves - - nodeHelpStore.openHelp(mockCoreNode as any) - await nextTick() - - expect(nodeHelpStore.isLoading).toBe(true) - }) - - it('should try fallback URL for custom nodes', async () => { - const nodeHelpStore = useNodeHelpStore() - - mockFetch - .mockResolvedValueOnce({ - ok: false, - statusText: 'Not Found' - }) - .mockResolvedValueOnce({ - ok: true, - text: async () => '# Fallback content' - }) - - nodeHelpStore.openHelp(mockCustomNode as any) - await flushPromises() - - expect(mockFetch).toHaveBeenCalledTimes(2) - expect(mockFetch).toHaveBeenCalledWith( - '/extensions/test_module/docs/CustomNode/en.md' - ) - expect(mockFetch).toHaveBeenCalledWith( - '/extensions/test_module/docs/CustomNode.md' - ) - }) - - it('should prefix relative img src in raw HTML for custom nodes', async () => { - const nodeHelpStore = useNodeHelpStore() - - mockFetch.mockResolvedValueOnce({ - ok: true, - text: async () => '# Test\nTest image' - }) - - nodeHelpStore.openHelp(mockCustomNode as any) - await flushPromises() - expect(nodeHelpStore.renderedHelpHtml).toContain( - 'src="/extensions/test_module/docs/image.png"' - ) - expect(nodeHelpStore.renderedHelpHtml).toContain('alt="Test image"') - }) - - it('should prefix relative img src in raw HTML for core nodes', async () => { - const nodeHelpStore = useNodeHelpStore() - - mockFetch.mockResolvedValueOnce({ - ok: true, - text: async () => '# Test\nTest image' - }) - - nodeHelpStore.openHelp(mockCoreNode as any) - await flushPromises() - expect(nodeHelpStore.renderedHelpHtml).toContain( - `src="/docs/${mockCoreNode.name}/image.png"` - ) - expect(nodeHelpStore.renderedHelpHtml).toContain('alt="Test image"') - }) - - it('should not prefix absolute img src in raw HTML', async () => { - const nodeHelpStore = useNodeHelpStore() - - mockFetch.mockResolvedValueOnce({ - ok: true, - text: async () => 'Absolute' - }) - - nodeHelpStore.openHelp(mockCustomNode as any) - await flushPromises() - expect(nodeHelpStore.renderedHelpHtml).toContain( - 'src="/absolute/image.png"' - ) - expect(nodeHelpStore.renderedHelpHtml).toContain('alt="Absolute"') - }) - - it('should not prefix external img src in raw HTML', async () => { - const nodeHelpStore = useNodeHelpStore() - - mockFetch.mockResolvedValueOnce({ - ok: true, - text: async () => - 'External' - }) - - nodeHelpStore.openHelp(mockCustomNode as any) - await flushPromises() - expect(nodeHelpStore.renderedHelpHtml).toContain( - 'src="https://example.com/image.png"' - ) - expect(nodeHelpStore.renderedHelpHtml).toContain('alt="External"') - }) - - it('should handle various quote styles in media src attributes', async () => { - const nodeHelpStore = useNodeHelpStore() - - mockFetch.mockResolvedValueOnce({ - ok: true, - text: async () => `# Media Test - -Testing quote styles in properly formed HTML: - - - -Double quotes -Single quotes - - - -The MEDIA_SRC_REGEX handles both single and double quotes in img, video and source tags.` - }) - - nodeHelpStore.openHelp(mockCoreNode as any) - await flushPromises() - - // Check that all media elements with different quote styles are prefixed correctly - // Double quotes remain as double quotes - expect(nodeHelpStore.renderedHelpHtml).toContain( - `src="/docs/${mockCoreNode.name}/video1.mp4"` - ) - expect(nodeHelpStore.renderedHelpHtml).toContain( - `src="/docs/${mockCoreNode.name}/image1.png"` - ) - expect(nodeHelpStore.renderedHelpHtml).toContain( - `src="/docs/${mockCoreNode.name}/video3.mp4"` - ) - - // Single quotes remain as single quotes in the output - expect(nodeHelpStore.renderedHelpHtml).toContain( - `src='/docs/${mockCoreNode.name}/video2.mp4'` - ) - expect(nodeHelpStore.renderedHelpHtml).toContain( - `src='/docs/${mockCoreNode.name}/image2.png'` - ) - expect(nodeHelpStore.renderedHelpHtml).toContain( - `src='/docs/${mockCoreNode.name}/video3.webm'` - ) - }) }) diff --git a/src/stores/workspace/nodeHelpStore.ts b/src/stores/workspace/nodeHelpStore.ts index a04d6d82951..4e67650405c 100644 --- a/src/stores/workspace/nodeHelpStore.ts +++ b/src/stores/workspace/nodeHelpStore.ts @@ -1,18 +1,11 @@ import { defineStore } from 'pinia' -import { computed, ref, watch } from 'vue' +import { computed, ref } from 'vue' -import { i18n } from '@/i18n' -import { nodeHelpService } from '@/services/nodeHelpService' import type { ComfyNodeDefImpl } from '@/stores/nodeDefStore' -import { renderMarkdownToHtml } from '@/utils/markdownRendererUtil' -import { getNodeHelpBaseUrl } from '@/workbench/utils/nodeHelpUtil' export const useNodeHelpStore = defineStore('nodeHelp', () => { const currentHelpNode = ref(null) const isHelpOpen = computed(() => currentHelpNode.value !== null) - const helpContent = ref('') - const isLoading = ref(false) - const errorMsg = ref(null) function openHelp(nodeDef: ComfyNodeDefImpl) { currentHelpNode.value = nodeDef @@ -22,48 +15,10 @@ export const useNodeHelpStore = defineStore('nodeHelp', () => { currentHelpNode.value = null } - // Base URL for relative assets in node docs markdown - const baseUrl = computed(() => { - const node = currentHelpNode.value - if (!node) return '' - return getNodeHelpBaseUrl(node) - }) - - // Watch for help node changes and fetch its docs markdown - watch( - () => currentHelpNode.value, - async (node) => { - helpContent.value = '' - errorMsg.value = null - - if (node) { - isLoading.value = true - try { - const locale = i18n.global.locale.value || 'en' - helpContent.value = await nodeHelpService.fetchNodeHelp(node, locale) - } catch (e: any) { - errorMsg.value = e.message - helpContent.value = node.description || '' - } finally { - isLoading.value = false - } - } - }, - { immediate: true } - ) - - const renderedHelpHtml = computed(() => { - return renderMarkdownToHtml(helpContent.value, baseUrl.value) - }) - return { currentHelpNode, isHelpOpen, openHelp, - closeHelp, - baseUrl, - renderedHelpHtml, - isLoading, - error: errorMsg + closeHelp } }) From e15f5328996b9112f8c26114192f9ca83435025a Mon Sep 17 00:00:00 2001 From: pythongosssss <125205205+pythongosssss@users.noreply.github.com> Date: Fri, 16 Jan 2026 13:24:15 -0800 Subject: [PATCH 2/6] fix leaky test --- src/composables/useNodeHelpContent.test.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/composables/useNodeHelpContent.test.ts b/src/composables/useNodeHelpContent.test.ts index 104ca5de15e..1b588bbc4ec 100644 --- a/src/composables/useNodeHelpContent.test.ts +++ b/src/composables/useNodeHelpContent.test.ts @@ -1,5 +1,5 @@ import { flushPromises } from '@vue/test-utils' -import { beforeEach, describe, expect, it, vi } from 'vitest' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { nextTick, ref } from 'vue' import { useNodeHelpContent } from '@/composables/useNodeHelpContent' @@ -82,12 +82,15 @@ describe('useNodeHelpContent', () => { python_module: 'custom_nodes.test_module.custom@1.0.0' } - // Mock fetch responses const mockFetch = vi.fn() - global.fetch = mockFetch beforeEach(() => { mockFetch.mockReset() + vi.stubGlobal('fetch', mockFetch) + }) + + afterEach(() => { + vi.unstubAllGlobals() }) it('should generate correct baseUrl for core nodes', async () => { From 7f546ca37473313235d97d41394391b4b552fef3 Mon Sep 17 00:00:00 2001 From: pythongosssss <125205205+pythongosssss@users.noreply.github.com> Date: Fri, 16 Jan 2026 13:40:48 -0800 Subject: [PATCH 3/6] remove any --- src/composables/useNodeHelpContent.test.ts | 65 +++++++++++++--------- 1 file changed, 40 insertions(+), 25 deletions(-) diff --git a/src/composables/useNodeHelpContent.test.ts b/src/composables/useNodeHelpContent.test.ts index 1b588bbc4ec..caf85c41deb 100644 --- a/src/composables/useNodeHelpContent.test.ts +++ b/src/composables/useNodeHelpContent.test.ts @@ -3,6 +3,26 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { nextTick, ref } from 'vue' import { useNodeHelpContent } from '@/composables/useNodeHelpContent' +import type { ComfyNodeDefImpl } from '@/stores/nodeDefStore' + +function createMockNode( + overrides: Partial +): ComfyNodeDefImpl { + return { + name: 'TestNode', + display_name: 'Test Node', + description: 'A test node', + category: 'test', + python_module: 'comfy.test_node', + inputs: {}, + outputs: [], + deprecated: false, + experimental: false, + output_node: false, + api_node: false, + ...overrides + } as ComfyNodeDefImpl +} vi.mock('@/scripts/api', () => ({ api: { @@ -63,24 +83,19 @@ vi.mock('marked', () => ({ })) describe('useNodeHelpContent', () => { - // Define a mock node for testing - const mockCoreNode = { + const mockCoreNode = createMockNode({ name: 'TestNode', display_name: 'Test Node', description: 'A test node', - inputs: {}, - outputs: [], python_module: 'comfy.test_node' - } + }) - const mockCustomNode = { + const mockCustomNode = createMockNode({ name: 'CustomNode', display_name: 'Custom Node', description: 'A custom node', - inputs: {}, - outputs: [], python_module: 'custom_nodes.test_module.custom@1.0.0' - } + }) const mockFetch = vi.fn() @@ -94,7 +109,7 @@ describe('useNodeHelpContent', () => { }) it('should generate correct baseUrl for core nodes', async () => { - const nodeRef = ref(mockCoreNode as any) + const nodeRef = ref(mockCoreNode) mockFetch.mockResolvedValueOnce({ ok: true, text: async () => '# Test' @@ -107,7 +122,7 @@ describe('useNodeHelpContent', () => { }) it('should generate correct baseUrl for custom nodes', async () => { - const nodeRef = ref(mockCustomNode as any) + const nodeRef = ref(mockCustomNode) mockFetch.mockResolvedValueOnce({ ok: true, text: async () => '# Test' @@ -120,7 +135,7 @@ describe('useNodeHelpContent', () => { }) it('should render markdown content correctly', async () => { - const nodeRef = ref(mockCoreNode as any) + const nodeRef = ref(mockCoreNode) mockFetch.mockResolvedValueOnce({ ok: true, text: async () => '# Test Help\nThis is test help content' @@ -133,7 +148,7 @@ describe('useNodeHelpContent', () => { }) it('should handle fetch errors and fall back to description', async () => { - const nodeRef = ref(mockCoreNode as any) + const nodeRef = ref(mockCoreNode) mockFetch.mockResolvedValueOnce({ ok: false, statusText: 'Not Found' @@ -147,7 +162,7 @@ describe('useNodeHelpContent', () => { }) it('should include alt attribute for images', async () => { - const nodeRef = ref(mockCustomNode as any) + const nodeRef = ref(mockCustomNode) mockFetch.mockResolvedValueOnce({ ok: true, text: async () => '![image](test.jpg)' @@ -160,7 +175,7 @@ describe('useNodeHelpContent', () => { }) it('should prefix relative video src in custom nodes', async () => { - const nodeRef = ref(mockCustomNode as any) + const nodeRef = ref(mockCustomNode) mockFetch.mockResolvedValueOnce({ ok: true, text: async () => '' @@ -175,7 +190,7 @@ describe('useNodeHelpContent', () => { }) it('should prefix relative video src for core nodes with node-specific base URL', async () => { - const nodeRef = ref(mockCoreNode as any) + const nodeRef = ref(mockCoreNode) mockFetch.mockResolvedValueOnce({ ok: true, text: async () => '' @@ -190,7 +205,7 @@ describe('useNodeHelpContent', () => { }) it('should handle loading state', async () => { - const nodeRef = ref(mockCoreNode as any) + const nodeRef = ref(mockCoreNode) mockFetch.mockImplementationOnce(() => new Promise(() => {})) // Never resolves const { isLoading } = useNodeHelpContent(nodeRef) @@ -200,7 +215,7 @@ describe('useNodeHelpContent', () => { }) it('should try fallback URL for custom nodes', async () => { - const nodeRef = ref(mockCustomNode as any) + const nodeRef = ref(mockCustomNode) mockFetch .mockResolvedValueOnce({ ok: false, @@ -224,7 +239,7 @@ describe('useNodeHelpContent', () => { }) it('should prefix relative source src in custom nodes', async () => { - const nodeRef = ref(mockCustomNode as any) + const nodeRef = ref(mockCustomNode) mockFetch.mockResolvedValueOnce({ ok: true, text: async () => @@ -240,7 +255,7 @@ describe('useNodeHelpContent', () => { }) it('should prefix relative source src for core nodes with node-specific base URL', async () => { - const nodeRef = ref(mockCoreNode as any) + const nodeRef = ref(mockCoreNode) mockFetch.mockResolvedValueOnce({ ok: true, text: async () => @@ -256,7 +271,7 @@ describe('useNodeHelpContent', () => { }) it('should prefix relative img src in raw HTML for custom nodes', async () => { - const nodeRef = ref(mockCustomNode as any) + const nodeRef = ref(mockCustomNode) mockFetch.mockResolvedValueOnce({ ok: true, text: async () => '# Test\nTest image' @@ -272,7 +287,7 @@ describe('useNodeHelpContent', () => { }) it('should prefix relative img src in raw HTML for core nodes', async () => { - const nodeRef = ref(mockCoreNode as any) + const nodeRef = ref(mockCoreNode) mockFetch.mockResolvedValueOnce({ ok: true, text: async () => '# Test\nTest image' @@ -288,7 +303,7 @@ describe('useNodeHelpContent', () => { }) it('should not prefix absolute img src in raw HTML', async () => { - const nodeRef = ref(mockCustomNode as any) + const nodeRef = ref(mockCustomNode) mockFetch.mockResolvedValueOnce({ ok: true, text: async () => 'Absolute' @@ -302,7 +317,7 @@ describe('useNodeHelpContent', () => { }) it('should not prefix external img src in raw HTML', async () => { - const nodeRef = ref(mockCustomNode as any) + const nodeRef = ref(mockCustomNode) mockFetch.mockResolvedValueOnce({ ok: true, text: async () => @@ -319,7 +334,7 @@ describe('useNodeHelpContent', () => { }) it('should handle various quote styles in media src attributes', async () => { - const nodeRef = ref(mockCoreNode as any) + const nodeRef = ref(mockCoreNode) mockFetch.mockResolvedValueOnce({ ok: true, text: async () => `# Media Test From 924868984541eb45411698af7b7c83c84b95d917 Mon Sep 17 00:00:00 2001 From: pythongosssss <125205205+pythongosssss@users.noreply.github.com> Date: Fri, 16 Jan 2026 13:43:20 -0800 Subject: [PATCH 4/6] feedback fixes, merge tests --- .../graph/selectionToolbox/InfoButton.test.ts | 26 +++++-------------- 1 file changed, 7 insertions(+), 19 deletions(-) diff --git a/src/components/graph/selectionToolbox/InfoButton.test.ts b/src/components/graph/selectionToolbox/InfoButton.test.ts index 1d0e92f9138..61e29292027 100644 --- a/src/components/graph/selectionToolbox/InfoButton.test.ts +++ b/src/components/graph/selectionToolbox/InfoButton.test.ts @@ -6,8 +6,12 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' import { createI18n } from 'vue-i18n' import InfoButton from '@/components/graph/selectionToolbox/InfoButton.vue' +import Button from '@/components/ui/button/Button.vue' + +const { openPanelMock } = vi.hoisted(() => ({ + openPanelMock: vi.fn() +})) -const openPanelMock = vi.fn() vi.mock('@/stores/workspace/rightSidePanelStore', () => ({ useRightSidePanelStore: () => ({ openPanel: openPanelMock @@ -37,31 +41,15 @@ describe('InfoButton', () => { global: { plugins: [i18n, PrimeVue], directives: { tooltip: Tooltip }, - stubs: { - 'i-lucide:info': true, - Button: { - template: - '', - props: ['severity', 'text', 'class'], - emits: ['click'] - } - } + components: { Button } } }) } it('should open the info panel on click', async () => { const wrapper = mountComponent() - const button = wrapper.find('button') + const button = wrapper.find('[data-testid="info-button"]') await button.trigger('click') expect(openPanelMock).toHaveBeenCalledWith('info') }) - - it('should have correct CSS classes', () => { - const wrapper = mountComponent() - const button = wrapper.find('button') - - expect(button.classes()).toContain('help-button') - expect(button.attributes('severity')).toBe('secondary') - }) }) From e5528992a2b9d1d369210308558f346c15a06dc3 Mon Sep 17 00:00:00 2001 From: pythongosssss <125205205+pythongosssss@users.noreply.github.com> Date: Fri, 16 Jan 2026 13:48:15 -0800 Subject: [PATCH 5/6] add stale request guard --- src/composables/useNodeHelpContent.test.ts | 33 ++++++++++++++++++++++ src/composables/useNodeHelpContent.ts | 20 +++++++++---- 2 files changed, 48 insertions(+), 5 deletions(-) diff --git a/src/composables/useNodeHelpContent.test.ts b/src/composables/useNodeHelpContent.test.ts index caf85c41deb..31a50dd631a 100644 --- a/src/composables/useNodeHelpContent.test.ts +++ b/src/composables/useNodeHelpContent.test.ts @@ -380,4 +380,37 @@ The MEDIA_SRC_REGEX handles both single and double quotes in img, video and sour `src='/docs/${mockCoreNode.name}/video3.webm'` ) }) + + it('should ignore stale requests when node changes', async () => { + const nodeRef = ref(mockCoreNode) + let resolveFirst: (value: unknown) => void + const firstRequest = new Promise((resolve) => { + resolveFirst = resolve + }) + + mockFetch + .mockImplementationOnce(() => firstRequest) + .mockResolvedValueOnce({ + ok: true, + text: async () => '# Second node content' + }) + + const { helpContent } = useNodeHelpContent(nodeRef) + await nextTick() + + // Change node before first request completes + nodeRef.value = mockCustomNode + await nextTick() + await flushPromises() + + // Now resolve the first (stale) request + resolveFirst!({ + ok: true, + text: async () => '# First node content' + }) + await flushPromises() + + // Should have second node's content, not first + expect(helpContent.value).toBe('# Second node content') + }) }) diff --git a/src/composables/useNodeHelpContent.ts b/src/composables/useNodeHelpContent.ts index 72053399c1c..81ef9ae473f 100644 --- a/src/composables/useNodeHelpContent.ts +++ b/src/composables/useNodeHelpContent.ts @@ -24,6 +24,8 @@ export function useNodeHelpContent( const isLoading = ref(false) const error = ref(null) + let currentRequest: Promise | null = null + const baseUrl = computed(() => { const node = toValue(nodeRef) if (!node) return '' @@ -43,16 +45,24 @@ export function useNodeHelpContent( if (node) { isLoading.value = true + const request = (currentRequest = nodeHelpService.fetchNodeHelp( + node, + locale.value || 'en' + )) + try { - helpContent.value = await nodeHelpService.fetchNodeHelp( - node, - locale.value || 'en' - ) + const content = await request + if (currentRequest !== request) return + helpContent.value = content } catch (e: unknown) { + if (currentRequest !== request) return error.value = e instanceof Error ? e.message : String(e) helpContent.value = node.description || '' } finally { - isLoading.value = false + if (currentRequest === request) { + currentRequest = null + isLoading.value = false + } } } }, From bad835d11a382b3af0ca9f56747fe6648e09e282 Mon Sep 17 00:00:00 2001 From: pythongosssss <125205205+pythongosssss@users.noreply.github.com> Date: Fri, 16 Jan 2026 13:58:01 -0800 Subject: [PATCH 6/6] remove unnecessary mocks --- src/composables/useNodeHelpContent.test.ts | 49 ++++------------------ 1 file changed, 7 insertions(+), 42 deletions(-) diff --git a/src/composables/useNodeHelpContent.test.ts b/src/composables/useNodeHelpContent.test.ts index 31a50dd631a..02b3d102f9a 100644 --- a/src/composables/useNodeHelpContent.test.ts +++ b/src/composables/useNodeHelpContent.test.ts @@ -49,39 +49,6 @@ vi.mock('@/types/nodeSource', () => ({ }) })) -vi.mock('dompurify', () => ({ - default: { - sanitize: vi.fn((html) => html) - } -})) - -vi.mock('marked', () => ({ - marked: { - parse: vi.fn((markdown, options) => { - if (options?.renderer) { - if (markdown.includes('![')) { - const matches = markdown.match(/!\[(.*?)\]\((.*?)\)/) - if (matches) { - const [, text, href] = matches - return options.renderer.image({ href, text, title: '' }) - } - } - } - return `

${markdown}

` - }) - }, - Renderer: class Renderer { - image = vi.fn( - ({ href, title, text }) => - `${text}` - ) - link = vi.fn( - ({ href, title, text }) => - `${text}` - ) - } -})) - describe('useNodeHelpContent', () => { const mockCoreNode = createMockNode({ name: 'TestNode', @@ -357,27 +324,25 @@ The MEDIA_SRC_REGEX handles both single and double quotes in img, video and sour const { renderedHelpHtml } = useNodeHelpContent(nodeRef) await flushPromises() - // Check that all media elements with different quote styles are prefixed correctly - // Double quotes remain as double quotes + // All media src attributes should be prefixed correctly + // Note: marked normalizes quotes to double quotes in output expect(renderedHelpHtml.value).toContain( `src="/docs/${mockCoreNode.name}/video1.mp4"` ) expect(renderedHelpHtml.value).toContain( - `src="/docs/${mockCoreNode.name}/image1.png"` + `src="/docs/${mockCoreNode.name}/video2.mp4"` ) expect(renderedHelpHtml.value).toContain( - `src="/docs/${mockCoreNode.name}/video3.mp4"` + `src="/docs/${mockCoreNode.name}/image1.png"` ) - - // Single quotes remain as single quotes in the output expect(renderedHelpHtml.value).toContain( - `src='/docs/${mockCoreNode.name}/video2.mp4'` + `src="/docs/${mockCoreNode.name}/image2.png"` ) expect(renderedHelpHtml.value).toContain( - `src='/docs/${mockCoreNode.name}/image2.png'` + `src="/docs/${mockCoreNode.name}/video3.mp4"` ) expect(renderedHelpHtml.value).toContain( - `src='/docs/${mockCoreNode.name}/video3.webm'` + `src="/docs/${mockCoreNode.name}/video3.webm"` ) })