diff --git a/src/composables/useDownload.ts b/src/composables/useDownload.ts index 2b1d8924a..a3e0004b9 100644 --- a/src/composables/useDownload.ts +++ b/src/composables/useDownload.ts @@ -2,6 +2,7 @@ import { whenever } from '@vueuse/core' import { onMounted, ref } from 'vue' import { useCivitaiModel } from '@/composables/useCivitaiModel' +import { fetchWithHeaders } from '@/services/networkClientAdapter' import { downloadUrlToHfRepoUrl, isCivitaiModelUrl } from '@/utils/formatUtil' export function useDownload(url: string, fileName?: string) { @@ -14,7 +15,7 @@ export function useDownload(url: string, fileName?: string) { const fetchFileSize = async () => { try { - const response = await fetch(url, { method: 'HEAD' }) + const response = await fetchWithHeaders(url, { method: 'HEAD' }) if (!response.ok) throw new Error('Failed to fetch file size') const size = response.headers.get('content-length') diff --git a/src/composables/useTemplateWorkflows.ts b/src/composables/useTemplateWorkflows.ts index 39fd6bd60..29af0b094 100644 --- a/src/composables/useTemplateWorkflows.ts +++ b/src/composables/useTemplateWorkflows.ts @@ -3,6 +3,7 @@ import { useI18n } from 'vue-i18n' import { api } from '@/scripts/api' import { app } from '@/scripts/app' +import { fetchWithHeaders } from '@/services/networkClientAdapter' import { useDialogStore } from '@/stores/dialogStore' import { useWorkflowTemplatesStore } from '@/stores/workflowTemplatesStore' import type { @@ -161,7 +162,9 @@ export function useTemplateWorkflows() { const fetchTemplateJson = async (id: string, sourceModule: string) => { if (sourceModule === 'default') { // Default templates provided by frontend are served as static files - const response = await fetch(api.fileURL(`/templates/${id}.json`)) + const response = await fetchWithHeaders( + api.fileURL(`/templates/${id}.json`) + ) return await response.json() } else { // Custom node templates served via API diff --git a/src/composables/widgets/useRemoteWidget.ts b/src/composables/widgets/useRemoteWidget.ts index 0fde7592a..3a10cdbbb 100644 --- a/src/composables/widgets/useRemoteWidget.ts +++ b/src/composables/widgets/useRemoteWidget.ts @@ -1,14 +1,16 @@ -import axios from 'axios' - import { useChainCallback } from '@/composables/functional/useChainCallback' import { LGraphNode } from '@/lib/litegraph/src/litegraph' import { IWidget } from '@/lib/litegraph/src/litegraph' import type { RemoteWidgetConfig } from '@/schemas/nodeDefSchema' import { api } from '@/scripts/api' +import { createAxiosWithHeaders } from '@/services/networkClientAdapter' const MAX_RETRIES = 5 const TIMEOUT = 4096 +// Create axios client with header injection +const axiosClient = createAxiosWithHeaders() + export interface CacheEntry { data: T timestamp?: number @@ -58,7 +60,7 @@ const fetchData = async ( controller: AbortController ) => { const { route, response_key, query_params, timeout = TIMEOUT } = config - const res = await axios.get(route, { + const res = await axiosClient.get(route, { params: query_params, signal: controller.signal, timeout diff --git a/src/extensions/core/load3d/Load3dUtils.ts b/src/extensions/core/load3d/Load3dUtils.ts index 213019293..ebd3430f0 100644 --- a/src/extensions/core/load3d/Load3dUtils.ts +++ b/src/extensions/core/load3d/Load3dUtils.ts @@ -1,6 +1,7 @@ import { t } from '@/i18n' import { api } from '@/scripts/api' import { app } from '@/scripts/app' +import { fetchWithHeaders } from '@/services/networkClientAdapter' import { useToastStore } from '@/stores/toastStore' class Load3dUtils { @@ -9,7 +10,7 @@ class Load3dUtils { prefix: string, fileType: string = 'png' ) { - const blob = await fetch(imageData).then((r) => r.blob()) + const blob = await fetchWithHeaders(imageData).then((r) => r.blob()) const name = `${prefix}_${Date.now()}.${fileType}` const file = new File([blob], name, { type: fileType === 'mp4' ? 'video/mp4' : 'image/png' diff --git a/src/extensions/core/load3d/ModelExporter.ts b/src/extensions/core/load3d/ModelExporter.ts index f676ea4d9..2c735b1c0 100644 --- a/src/extensions/core/load3d/ModelExporter.ts +++ b/src/extensions/core/load3d/ModelExporter.ts @@ -5,6 +5,7 @@ import { STLExporter } from 'three/examples/jsm/exporters/STLExporter' import { t } from '@/i18n' import { api } from '@/scripts/api' +import { fetchWithHeaders } from '@/services/networkClientAdapter' import { useToastStore } from '@/stores/toastStore' export class ModelExporter { @@ -43,10 +44,10 @@ export class ModelExporter { let response: Response if (isComfyUrl) { // Use ComfyUI API client for internal URLs - response = await fetch(api.apiURL(url)) + response = await fetchWithHeaders(api.apiURL(url)) } else { // Use direct fetch for external URLs - response = await fetch(url) + response = await fetchWithHeaders(url) } const blob = await response.blob() diff --git a/src/scripts/api.ts b/src/scripts/api.ts index e83eb9f8b..f43a5f116 100644 --- a/src/scripts/api.ts +++ b/src/scripts/api.ts @@ -1,5 +1,3 @@ -import axios from 'axios' - import defaultClientFeatureFlags from '@/config/clientFeatureFlags.json' import type { DisplayComponentWsMessage, @@ -35,6 +33,7 @@ import type { NodeId } from '@/schemas/comfyWorkflowSchema' import type { ComfyNodeDef } from '@/schemas/nodeDefSchema' +import { fetchWithHeaders } from '@/services/networkClientAdapter' import type { NodeExecutionId } from '@/types/nodeIdentification' import { WorkflowTemplates } from '@/types/workflowTemplateTypes' @@ -329,7 +328,7 @@ export class ComfyApi extends EventTarget { } else { options.headers['Comfy-User'] = this.user } - return fetch(this.apiURL(route), options) + return fetchWithHeaders(this.apiURL(route), options) } override addEventListener( @@ -599,9 +598,9 @@ export class ComfyApi extends EventTarget { * Gets the index of core workflow templates. */ async getCoreWorkflowTemplates(): Promise { - const res = await axios.get(this.fileURL('/templates/index.json')) - const contentType = res.headers['content-type'] - return contentType?.includes('application/json') ? res.data : [] + const res = await fetchWithHeaders(this.fileURL('/templates/index.json')) + const contentType = res.headers.get('content-type') + return contentType?.includes('application/json') ? await res.json() : [] } /** @@ -1002,22 +1001,31 @@ export class ComfyApi extends EventTarget { } async getLogs(): Promise { - return (await axios.get(this.internalURL('/logs'))).data + const response = await fetchWithHeaders(this.internalURL('/logs')) + return response.text() } async getRawLogs(): Promise { - return (await axios.get(this.internalURL('/logs/raw'))).data + const response = await fetchWithHeaders(this.internalURL('/logs/raw')) + return response.json() } async subscribeLogs(enabled: boolean): Promise { - return await axios.patch(this.internalURL('/logs/subscribe'), { - enabled, - clientId: this.clientId + await fetchWithHeaders(this.internalURL('/logs/subscribe'), { + method: 'PATCH', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + enabled, + clientId: this.clientId + }) }) } async getFolderPaths(): Promise> { - return (await axios.get(this.internalURL('/folder_paths'))).data + const response = await fetchWithHeaders(this.internalURL('/folder_paths')) + return response.json() } /** @@ -1026,7 +1034,8 @@ export class ComfyApi extends EventTarget { * @returns The custom nodes i18n data */ async getCustomNodesI18n(): Promise> { - return (await axios.get(this.apiURL('/i18n'))).data + const response = await fetchWithHeaders(this.apiURL('/i18n')) + return response.json() } /** diff --git a/src/scripts/app.ts b/src/scripts/app.ts index ca784b66f..88b0406dd 100644 --- a/src/scripts/app.ts +++ b/src/scripts/app.ts @@ -40,6 +40,7 @@ import { getSvgMetadata } from '@/scripts/metadata/svg' import { useDialogService } from '@/services/dialogService' import { useExtensionService } from '@/services/extensionService' import { useLitegraphService } from '@/services/litegraphService' +import { fetchWithHeaders } from '@/services/networkClientAdapter' import { useSubgraphService } from '@/services/subgraphService' import { useWorkflowService } from '@/services/workflowService' import { useApiKeyAuthStore } from '@/stores/apiKeyAuthStore' @@ -533,7 +534,7 @@ export class ComfyApp { if (match) { const uri = event.dataTransfer.getData(match)?.split('\n')?.[0] if (uri) { - const blob = await (await fetch(uri)).blob() + const blob = await (await fetchWithHeaders(uri)).blob() await this.handleFile(new File([blob], uri, { type: blob.type })) } } diff --git a/src/services/comfyManagerService.ts b/src/services/comfyManagerService.ts index b98be5958..c28a48968 100644 --- a/src/services/comfyManagerService.ts +++ b/src/services/comfyManagerService.ts @@ -2,6 +2,7 @@ import axios, { AxiosError, AxiosResponse } from 'axios' import { ref } from 'vue' import { api } from '@/scripts/api' +import { createAxiosWithHeaders } from '@/services/networkClientAdapter' import { type InstallPackParams, type InstalledPacksResponse, @@ -35,7 +36,7 @@ enum ManagerRoute { REBOOT = 'manager/reboot' } -const managerApiClient = axios.create({ +const managerApiClient = createAxiosWithHeaders({ baseURL: api.apiURL(''), headers: { 'Content-Type': 'application/json' diff --git a/src/services/comfyRegistryService.ts b/src/services/comfyRegistryService.ts index 20b418cfa..c35243f0e 100644 --- a/src/services/comfyRegistryService.ts +++ b/src/services/comfyRegistryService.ts @@ -1,12 +1,13 @@ import axios, { AxiosError, AxiosResponse } from 'axios' import { ref } from 'vue' +import { createAxiosWithHeaders } from '@/services/networkClientAdapter' import type { components, operations } from '@/types/comfyRegistryTypes' import { isAbortError } from '@/utils/typeGuardUtil' const API_BASE_URL = 'https://api.comfy.org' -const registryApiClient = axios.create({ +const registryApiClient = createAxiosWithHeaders({ baseURL: API_BASE_URL, headers: { 'Content-Type': 'application/json' diff --git a/src/services/customerEventsService.ts b/src/services/customerEventsService.ts index a951bacf7..994158f23 100644 --- a/src/services/customerEventsService.ts +++ b/src/services/customerEventsService.ts @@ -3,6 +3,7 @@ import { ref } from 'vue' import { useI18n } from 'vue-i18n' import { COMFY_API_BASE_URL } from '@/config/comfyApi' +import { createAxiosWithHeaders } from '@/services/networkClientAdapter' import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore' import { type components, operations } from '@/types/comfyRegistryTypes' import { isAbortError } from '@/utils/typeGuardUtil' @@ -22,7 +23,7 @@ type CustomerEventsResponseQuery = export type AuditLog = components['schemas']['AuditLog'] -const customerApiClient = axios.create({ +const customerApiClient = createAxiosWithHeaders({ baseURL: COMFY_API_BASE_URL, headers: { 'Content-Type': 'application/json' diff --git a/src/services/nodeHelpService.ts b/src/services/nodeHelpService.ts index 8d26f9457..545de408c 100644 --- a/src/services/nodeHelpService.ts +++ b/src/services/nodeHelpService.ts @@ -1,4 +1,5 @@ import { api } from '@/scripts/api' +import { fetchWithHeaders } from '@/services/networkClientAdapter' import type { ComfyNodeDefImpl } from '@/stores/nodeDefStore' import { NodeSourceType, getNodeSource } from '@/types/nodeSource' import { extractCustomNodeName } from '@/utils/nodeHelpUtil' @@ -25,12 +26,12 @@ export class NodeHelpService { // Try locale-specific path first const localePath = `/extensions/${customNodeName}/docs/${node.name}/${locale}.md` - let res = await fetch(api.fileURL(localePath)) + let res = await fetchWithHeaders(api.fileURL(localePath)) if (!res.ok) { // Fall back to non-locale path const fallbackPath = `/extensions/${customNodeName}/docs/${node.name}.md` - res = await fetch(api.fileURL(fallbackPath)) + res = await fetchWithHeaders(api.fileURL(fallbackPath)) } if (!res.ok) { @@ -45,7 +46,7 @@ export class NodeHelpService { locale: string ): Promise { const mdUrl = `/docs/${node.name}/${locale}.md` - const res = await fetch(api.fileURL(mdUrl)) + const res = await fetchWithHeaders(api.fileURL(mdUrl)) if (!res.ok) { throw new Error(res.statusText) diff --git a/src/services/releaseService.ts b/src/services/releaseService.ts index 2f2871bd1..60384434e 100644 --- a/src/services/releaseService.ts +++ b/src/services/releaseService.ts @@ -2,10 +2,11 @@ import axios, { AxiosError, AxiosResponse } from 'axios' import { ref } from 'vue' import { COMFY_API_BASE_URL } from '@/config/comfyApi' +import { createAxiosWithHeaders } from '@/services/networkClientAdapter' import type { components, operations } from '@/types/comfyRegistryTypes' import { isAbortError } from '@/utils/typeGuardUtil' -const releaseApiClient = axios.create({ +const releaseApiClient = createAxiosWithHeaders({ baseURL: COMFY_API_BASE_URL, headers: { 'Content-Type': 'application/json' diff --git a/src/stores/firebaseAuthStore.ts b/src/stores/firebaseAuthStore.ts index b6859fdb7..5d8b1e71a 100644 --- a/src/stores/firebaseAuthStore.ts +++ b/src/stores/firebaseAuthStore.ts @@ -21,6 +21,7 @@ import { useFirebaseAuth } from 'vuefire' import { COMFY_API_BASE_URL } from '@/config/comfyApi' import { t } from '@/i18n' +import { createAxiosWithHeaders } from '@/services/networkClientAdapter' import { useApiKeyAuthStore } from '@/stores/apiKeyAuthStore' import { type AuthHeader } from '@/types/authTypes' import { operations } from '@/types/comfyRegistryTypes' @@ -46,7 +47,8 @@ export class FirebaseAuthStoreError extends Error { } // Customer API client - follows the same pattern as other services -const customerApiClient = axios.create({ +// Now with automatic header injection from the registry +const customerApiClient = createAxiosWithHeaders({ baseURL: COMFY_API_BASE_URL, headers: { 'Content-Type': 'application/json' diff --git a/tests-ui/tests/composables/useTemplateWorkflows.test.ts b/tests-ui/tests/composables/useTemplateWorkflows.test.ts index b7c507faf..77f4b3820 100644 --- a/tests-ui/tests/composables/useTemplateWorkflows.test.ts +++ b/tests-ui/tests/composables/useTemplateWorkflows.test.ts @@ -2,6 +2,7 @@ import { flushPromises } from '@vue/test-utils' import { beforeEach, describe, expect, it, vi } from 'vitest' import { useTemplateWorkflows } from '@/composables/useTemplateWorkflows' +import { fetchWithHeaders } from '@/services/networkClientAdapter' import { useWorkflowTemplatesStore } from '@/stores/workflowTemplatesStore' // Mock the store @@ -41,6 +42,11 @@ vi.mock('@/stores/dialogStore', () => ({ // Mock fetch global.fetch = vi.fn() +// Mock fetchWithHeaders +vi.mock('@/services/networkClientAdapter', () => ({ + fetchWithHeaders: vi.fn() +})) + describe('useTemplateWorkflows', () => { let mockWorkflowTemplatesStore: any @@ -100,6 +106,11 @@ describe('useTemplateWorkflows', () => { vi.mocked(fetch).mockResolvedValue({ json: vi.fn().mockResolvedValue({ workflow: 'data' }) } as unknown as Response) + + // Also mock fetchWithHeaders + vi.mocked(fetchWithHeaders).mockResolvedValue({ + json: vi.fn().mockResolvedValue({ workflow: 'data' }) + } as unknown as Response) }) it('should load templates from store', async () => { @@ -258,7 +269,9 @@ describe('useTemplateWorkflows', () => { await flushPromises() expect(result).toBe(true) - expect(fetch).toHaveBeenCalledWith('mock-file-url/templates/template1.json') + expect(vi.mocked(fetchWithHeaders)).toHaveBeenCalledWith( + 'mock-file-url/templates/template1.json' + ) expect(loadingTemplateId.value).toBe(null) // Should reset after loading }) @@ -273,7 +286,9 @@ describe('useTemplateWorkflows', () => { await flushPromises() expect(result).toBe(true) - expect(fetch).toHaveBeenCalledWith('mock-file-url/templates/template1.json') + expect(vi.mocked(fetchWithHeaders)).toHaveBeenCalledWith( + 'mock-file-url/templates/template1.json' + ) }) it('should handle errors when loading templates', async () => { @@ -282,8 +297,10 @@ describe('useTemplateWorkflows', () => { // Set the store as loaded mockWorkflowTemplatesStore.isLoaded = true - // Mock fetch to throw an error - vi.mocked(fetch).mockRejectedValueOnce(new Error('Failed to fetch')) + // Mock fetchWithHeaders to throw an error + vi.mocked(fetchWithHeaders).mockRejectedValueOnce( + new Error('Failed to fetch') + ) // Spy on console.error const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}) diff --git a/tests-ui/tests/composables/widgets/useRemoteWidget.test.ts b/tests-ui/tests/composables/widgets/useRemoteWidget.test.ts index 0bf729453..bceef95b3 100644 --- a/tests-ui/tests/composables/widgets/useRemoteWidget.test.ts +++ b/tests-ui/tests/composables/widgets/useRemoteWidget.test.ts @@ -1,17 +1,35 @@ -import axios from 'axios' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { useRemoteWidget } from '@/composables/widgets/useRemoteWidget' import { RemoteWidgetConfig } from '@/schemas/nodeDefSchema' +// Hoist the mock to avoid hoisting issues +const mockAxiosInstance = vi.hoisted(() => ({ + get: vi.fn(), + interceptors: { + request: { + use: vi.fn() + }, + response: { + use: vi.fn() + } + } +})) + vi.mock('axios', () => { return { default: { - get: vi.fn() + get: vi.fn(), + create: vi.fn(() => mockAxiosInstance) } } }) +// Mock networkClientAdapter to return the same axios instance +vi.mock('@/services/networkClientAdapter', () => ({ + createAxiosWithHeaders: vi.fn(() => mockAxiosInstance) +})) + vi.mock('@/i18n', () => ({ i18n: { global: { @@ -63,12 +81,12 @@ const createMockOptions = (inputOverrides = {}) => ({ }) function mockAxiosResponse(data: unknown, status = 200) { - vi.mocked(axios.get).mockResolvedValueOnce({ data, status }) + vi.mocked(mockAxiosInstance.get).mockResolvedValueOnce({ data, status }) } function mockAxiosError(error: Error | string) { const err = error instanceof Error ? error : new Error(error) - vi.mocked(axios.get).mockRejectedValueOnce(err) + vi.mocked(mockAxiosInstance.get).mockRejectedValueOnce(err) } function createHookWithData(data: unknown, inputOverrides = {}) { @@ -96,7 +114,7 @@ describe('useRemoteWidget', () => { beforeEach(() => { vi.clearAllMocks() // Reset mocks - vi.mocked(axios.get).mockReset() + vi.mocked(mockAxiosInstance.get).mockReset() // Reset cache between tests vi.spyOn(Map.prototype, 'get').mockClear() vi.spyOn(Map.prototype, 'set').mockClear() @@ -137,7 +155,7 @@ describe('useRemoteWidget', () => { const mockData = ['optionA', 'optionB'] const { hook, result } = await setupHookWithResponse(mockData) expect(result).toEqual(mockData) - expect(vi.mocked(axios.get)).toHaveBeenCalledWith( + expect(vi.mocked(mockAxiosInstance.get)).toHaveBeenCalledWith( hook.cacheKey.split(';')[0], // Get the route part from cache key expect.any(Object) ) @@ -216,7 +234,7 @@ describe('useRemoteWidget', () => { await getResolvedValue(hook) await getResolvedValue(hook) - expect(vi.mocked(axios.get)).toHaveBeenCalledTimes(1) + expect(vi.mocked(mockAxiosInstance.get)).toHaveBeenCalledTimes(1) }) it('permanent widgets should re-fetch if refreshValue is called', async () => { @@ -237,12 +255,12 @@ describe('useRemoteWidget', () => { const hook = useRemoteWidget(createMockOptions()) await getResolvedValue(hook) - expect(vi.mocked(axios.get)).toHaveBeenCalledTimes(1) + expect(vi.mocked(mockAxiosInstance.get)).toHaveBeenCalledTimes(1) vi.setSystemTime(Date.now() + FIRST_BACKOFF) const secondData = await getResolvedValue(hook) expect(secondData).toBe('Loading...') - expect(vi.mocked(axios.get)).toHaveBeenCalledTimes(2) + expect(vi.mocked(mockAxiosInstance.get)).toHaveBeenCalledTimes(2) }) it('should treat empty refresh field as permanent', async () => { @@ -251,7 +269,7 @@ describe('useRemoteWidget', () => { await getResolvedValue(hook) await getResolvedValue(hook) - expect(vi.mocked(axios.get)).toHaveBeenCalledTimes(1) + expect(vi.mocked(mockAxiosInstance.get)).toHaveBeenCalledTimes(1) }) }) @@ -267,7 +285,7 @@ describe('useRemoteWidget', () => { const newData = await getResolvedValue(hook) expect(newData).toEqual(mockData2) - expect(vi.mocked(axios.get)).toHaveBeenCalledTimes(2) + expect(vi.mocked(mockAxiosInstance.get)).toHaveBeenCalledTimes(2) }) it('should not refresh when data is not stale', async () => { @@ -278,7 +296,7 @@ describe('useRemoteWidget', () => { vi.setSystemTime(Date.now() + 128) await getResolvedValue(hook) - expect(vi.mocked(axios.get)).toHaveBeenCalledTimes(1) + expect(vi.mocked(mockAxiosInstance.get)).toHaveBeenCalledTimes(1) }) it('should use backoff instead of refresh after error', async () => { @@ -290,13 +308,13 @@ describe('useRemoteWidget', () => { mockAxiosError('Network error') vi.setSystemTime(Date.now() + refresh) await getResolvedValue(hook) - expect(vi.mocked(axios.get)).toHaveBeenCalledTimes(2) + expect(vi.mocked(mockAxiosInstance.get)).toHaveBeenCalledTimes(2) mockAxiosResponse(['second success']) vi.setSystemTime(Date.now() + FIRST_BACKOFF) const thirdData = await getResolvedValue(hook) expect(thirdData).toEqual(['second success']) - expect(vi.mocked(axios.get)).toHaveBeenCalledTimes(3) + expect(vi.mocked(mockAxiosInstance.get)).toHaveBeenCalledTimes(3) }) it('should use last valid value after error', async () => { @@ -310,7 +328,7 @@ describe('useRemoteWidget', () => { const secondData = await getResolvedValue(hook) expect(secondData).toEqual(['a valid value']) - expect(vi.mocked(axios.get)).toHaveBeenCalledTimes(2) + expect(vi.mocked(mockAxiosInstance.get)).toHaveBeenCalledTimes(2) }) }) @@ -332,15 +350,15 @@ describe('useRemoteWidget', () => { expect(entry1?.error).toBeTruthy() await getResolvedValue(hook) - expect(vi.mocked(axios.get)).toHaveBeenCalledTimes(1) + expect(vi.mocked(mockAxiosInstance.get)).toHaveBeenCalledTimes(1) vi.setSystemTime(Date.now() + 500) await getResolvedValue(hook) - expect(vi.mocked(axios.get)).toHaveBeenCalledTimes(1) // Still backing off + expect(vi.mocked(mockAxiosInstance.get)).toHaveBeenCalledTimes(1) // Still backing off vi.setSystemTime(Date.now() + 3000) await getResolvedValue(hook) - expect(vi.mocked(axios.get)).toHaveBeenCalledTimes(2) + expect(vi.mocked(mockAxiosInstance.get)).toHaveBeenCalledTimes(2) expect(entry1?.data).toBeDefined() }) @@ -418,7 +436,9 @@ describe('useRemoteWidget', () => { it('should prevent duplicate in-flight requests', async () => { const promise = Promise.resolve({ data: ['non-duplicate'] }) - vi.mocked(axios.get).mockImplementationOnce(() => promise as any) + vi.mocked(mockAxiosInstance.get).mockImplementationOnce( + () => promise as any + ) const hook = useRemoteWidget(createMockOptions()) const [result1, result2] = await Promise.all([ @@ -427,7 +447,7 @@ describe('useRemoteWidget', () => { ]) expect(result1).toBe(result2) - expect(vi.mocked(axios.get)).toHaveBeenCalledTimes(1) + expect(vi.mocked(mockAxiosInstance.get)).toHaveBeenCalledTimes(1) }) }) @@ -446,7 +466,7 @@ describe('useRemoteWidget', () => { expect(data1).toEqual(['shared data']) expect(data2).toEqual(['shared data']) - expect(vi.mocked(axios.get)).toHaveBeenCalledTimes(1) + expect(vi.mocked(mockAxiosInstance.get)).toHaveBeenCalledTimes(1) expect(hook1.getCachedValue()).toBe(hook2.getCachedValue()) }) @@ -467,7 +487,7 @@ describe('useRemoteWidget', () => { expect(data2).toBe(data1) expect(data3).toBe(data1) expect(data4).toBe(data1) - expect(vi.mocked(axios.get)).toHaveBeenCalledTimes(1) + expect(vi.mocked(mockAxiosInstance.get)).toHaveBeenCalledTimes(1) expect(hook1.getCachedValue()).toBe(hook2.getCachedValue()) expect(hook2.getCachedValue()).toBe(hook3.getCachedValue()) expect(hook3.getCachedValue()).toBe(hook4.getCachedValue()) @@ -479,7 +499,9 @@ describe('useRemoteWidget', () => { resolvePromise = resolve }) - vi.mocked(axios.get).mockImplementationOnce(() => delayedPromise as any) + vi.mocked(mockAxiosInstance.get).mockImplementationOnce( + () => delayedPromise as any + ) const hook = useRemoteWidget(createMockOptions()) hook.getValue() @@ -500,7 +522,9 @@ describe('useRemoteWidget', () => { resolvePromise = resolve }) - vi.mocked(axios.get).mockImplementationOnce(() => delayedPromise as any) + vi.mocked(mockAxiosInstance.get).mockImplementationOnce( + () => delayedPromise as any + ) let hook = useRemoteWidget(createMockOptions()) const fetchPromise = hook.getValue() diff --git a/tests-ui/tests/services/customerEventsService.test.ts b/tests-ui/tests/services/customerEventsService.test.ts index abc54332a..cbbc649e5 100644 --- a/tests-ui/tests/services/customerEventsService.test.ts +++ b/tests-ui/tests/services/customerEventsService.test.ts @@ -27,6 +27,11 @@ vi.mock('axios', () => ({ } })) +// Mock networkClientAdapter to return the same axios instance +vi.mock('@/services/networkClientAdapter', () => ({ + createAxiosWithHeaders: vi.fn(() => mockAxiosInstance) +})) + vi.mock('@/stores/firebaseAuthStore', () => ({ useFirebaseAuthStore: vi.fn(() => mockFirebaseAuthStore) })) diff --git a/tests-ui/tests/services/releaseService.test.ts b/tests-ui/tests/services/releaseService.test.ts index 57913a5d5..155cb2650 100644 --- a/tests-ui/tests/services/releaseService.test.ts +++ b/tests-ui/tests/services/releaseService.test.ts @@ -15,6 +15,11 @@ vi.mock('axios', () => ({ } })) +// Mock networkClientAdapter to return the same axios instance +vi.mock('@/services/networkClientAdapter', () => ({ + createAxiosWithHeaders: vi.fn(() => mockAxiosInstance) +})) + describe('useReleaseService', () => { let service: ReturnType diff --git a/tests-ui/tests/store/firebaseAuthStore.test.ts b/tests-ui/tests/store/firebaseAuthStore.test.ts index f7c9f94c7..d47aae9fe 100644 --- a/tests-ui/tests/store/firebaseAuthStore.test.ts +++ b/tests-ui/tests/store/firebaseAuthStore.test.ts @@ -6,19 +6,27 @@ import * as vuefire from 'vuefire' import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore' -// Mock axios before any imports that use it -vi.mock('axios', () => { - const mockAxiosInstance = { - get: vi.fn().mockResolvedValue({ - data: { balance: { credits: 0 } }, - status: 200 - }), - post: vi.fn().mockResolvedValue({ - data: { id: 'test-customer-id' }, - status: 201 - }) +// Hoist the mock to avoid hoisting issues +const mockAxiosInstance = vi.hoisted(() => ({ + get: vi.fn().mockResolvedValue({ + data: { balance: { credits: 0 } }, + status: 200 + }), + post: vi.fn().mockResolvedValue({ + data: { id: 'test-customer-id' }, + status: 201 + }), + interceptors: { + request: { + use: vi.fn() + }, + response: { + use: vi.fn() + } } +})) +vi.mock('axios', () => { return { default: { create: vi.fn().mockReturnValue(mockAxiosInstance), @@ -27,8 +35,12 @@ vi.mock('axios', () => { } }) +// Mock networkClientAdapter +vi.mock('@/services/networkClientAdapter', () => ({ + createAxiosWithHeaders: vi.fn(() => mockAxiosInstance) +})) + const mockedAxios = vi.mocked(axios) -const mockAxiosInstance = mockedAxios.create() as any // Mock fetch const mockFetch = vi.fn() diff --git a/tests-ui/tests/store/nodeHelpStore.test.ts b/tests-ui/tests/store/nodeHelpStore.test.ts index dc8b7b466..37d9e9e53 100644 --- a/tests-ui/tests/store/nodeHelpStore.test.ts +++ b/tests-ui/tests/store/nodeHelpStore.test.ts @@ -3,6 +3,7 @@ import { createPinia, setActivePinia } from 'pinia' import { beforeEach, describe, expect, it, vi } from 'vitest' import { nextTick } from 'vue' +import { fetchWithHeaders } from '@/services/networkClientAdapter' import { useNodeHelpStore } from '@/stores/workspace/nodeHelpStore' vi.mock('@/scripts/api', () => ({ @@ -67,6 +68,11 @@ vi.mock('marked', () => ({ } })) +// Mock fetchWithHeaders +vi.mock('@/services/networkClientAdapter', () => ({ + fetchWithHeaders: vi.fn() +})) + describe('nodeHelpStore', () => { // Define a mock node for testing const mockCoreNode = { @@ -91,10 +97,17 @@ describe('nodeHelpStore', () => { const mockFetch = vi.fn() global.fetch = mockFetch + // Helper to mock both fetch and fetchWithHeaders + const mockFetchResponse = (response: any) => { + mockFetch.mockResolvedValueOnce(response) + vi.mocked(fetchWithHeaders).mockResolvedValueOnce(response) + } + beforeEach(() => { // Setup Pinia setActivePinia(createPinia()) mockFetch.mockReset() + vi.mocked(fetchWithHeaders).mockReset() }) it('should initialize with empty state', () => { @@ -144,7 +157,7 @@ describe('nodeHelpStore', () => { it('should render markdown content correctly', async () => { const nodeHelpStore = useNodeHelpStore() - mockFetch.mockResolvedValueOnce({ + mockFetchResponse({ ok: true, text: async () => '# Test Help\nThis is test help content' }) @@ -160,7 +173,7 @@ describe('nodeHelpStore', () => { it('should handle fetch errors and fall back to description', async () => { const nodeHelpStore = useNodeHelpStore() - mockFetch.mockResolvedValueOnce({ + mockFetchResponse({ ok: false, statusText: 'Not Found' }) @@ -175,7 +188,7 @@ describe('nodeHelpStore', () => { it('should include alt attribute for images', async () => { const nodeHelpStore = useNodeHelpStore() - mockFetch.mockResolvedValueOnce({ + mockFetchResponse({ ok: true, text: async () => '![image](test.jpg)' }) @@ -188,7 +201,7 @@ describe('nodeHelpStore', () => { it('should prefix relative video src in custom nodes', async () => { const nodeHelpStore = useNodeHelpStore() - mockFetch.mockResolvedValueOnce({ + mockFetchResponse({ ok: true, text: async () => '' }) @@ -203,7 +216,7 @@ describe('nodeHelpStore', () => { it('should prefix relative video src for core nodes with node-specific base URL', async () => { const nodeHelpStore = useNodeHelpStore() - mockFetch.mockResolvedValueOnce({ + mockFetchResponse({ ok: true, text: async () => '' }) @@ -218,7 +231,7 @@ describe('nodeHelpStore', () => { it('should prefix relative source src in custom nodes', async () => { const nodeHelpStore = useNodeHelpStore() - mockFetch.mockResolvedValueOnce({ + mockFetchResponse({ ok: true, text: async () => '' @@ -234,7 +247,7 @@ describe('nodeHelpStore', () => { it('should prefix relative source src for core nodes with node-specific base URL', async () => { const nodeHelpStore = useNodeHelpStore() - mockFetch.mockResolvedValueOnce({ + mockFetchResponse({ ok: true, text: async () => '' @@ -250,7 +263,9 @@ describe('nodeHelpStore', () => { it('should handle loading state', async () => { const nodeHelpStore = useNodeHelpStore() - mockFetch.mockImplementationOnce(() => new Promise(() => {})) // Never resolves + vi.mocked(fetchWithHeaders).mockImplementationOnce( + () => new Promise(() => {}) + ) // Never resolves nodeHelpStore.openHelp(mockCoreNode as any) await nextTick() @@ -261,24 +276,24 @@ describe('nodeHelpStore', () => { it('should try fallback URL for custom nodes', async () => { const nodeHelpStore = useNodeHelpStore() - mockFetch + vi.mocked(fetchWithHeaders) .mockResolvedValueOnce({ ok: false, statusText: 'Not Found' - }) + } as Response) .mockResolvedValueOnce({ ok: true, text: async () => '# Fallback content' - }) + } as Response) nodeHelpStore.openHelp(mockCustomNode as any) await flushPromises() - expect(mockFetch).toHaveBeenCalledTimes(2) - expect(mockFetch).toHaveBeenCalledWith( + expect(vi.mocked(fetchWithHeaders)).toHaveBeenCalledTimes(2) + expect(vi.mocked(fetchWithHeaders)).toHaveBeenCalledWith( '/extensions/test_module/docs/CustomNode/en.md' ) - expect(mockFetch).toHaveBeenCalledWith( + expect(vi.mocked(fetchWithHeaders)).toHaveBeenCalledWith( '/extensions/test_module/docs/CustomNode.md' ) }) @@ -286,7 +301,7 @@ describe('nodeHelpStore', () => { it('should prefix relative img src in raw HTML for custom nodes', async () => { const nodeHelpStore = useNodeHelpStore() - mockFetch.mockResolvedValueOnce({ + mockFetchResponse({ ok: true, text: async () => '# Test\nTest image' }) @@ -302,7 +317,7 @@ describe('nodeHelpStore', () => { it('should prefix relative img src in raw HTML for core nodes', async () => { const nodeHelpStore = useNodeHelpStore() - mockFetch.mockResolvedValueOnce({ + mockFetchResponse({ ok: true, text: async () => '# Test\nTest image' }) @@ -318,7 +333,7 @@ describe('nodeHelpStore', () => { it('should not prefix absolute img src in raw HTML', async () => { const nodeHelpStore = useNodeHelpStore() - mockFetch.mockResolvedValueOnce({ + mockFetchResponse({ ok: true, text: async () => 'Absolute' }) @@ -334,7 +349,7 @@ describe('nodeHelpStore', () => { it('should not prefix external img src in raw HTML', async () => { const nodeHelpStore = useNodeHelpStore() - mockFetch.mockResolvedValueOnce({ + mockFetchResponse({ ok: true, text: async () => 'External' @@ -351,7 +366,7 @@ describe('nodeHelpStore', () => { it('should handle various quote styles in media src attributes', async () => { const nodeHelpStore = useNodeHelpStore() - mockFetch.mockResolvedValueOnce({ + mockFetchResponse({ ok: true, text: async () => `# Media Test