mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-05 07:30:11 +00:00
[feat] Integrate header registry with all HTTP clients
- Replace direct fetch() with fetchWithHeaders across entire codebase - Replace axios.create() with createAxiosWithHeaders in all services - Update tests to properly mock network client adapters - Fix hoisting issues in test mocks for axios instances - Ensure all network calls now support header injection This completes Step 2: integrating the header registry infrastructure with all existing HTTP clients in the codebase.
This commit is contained in:
@@ -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')
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<T> {
|
||||
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
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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<TEvent extends keyof ApiEvents>(
|
||||
@@ -599,9 +598,9 @@ export class ComfyApi extends EventTarget {
|
||||
* Gets the index of core workflow templates.
|
||||
*/
|
||||
async getCoreWorkflowTemplates(): Promise<WorkflowTemplates[]> {
|
||||
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<string> {
|
||||
return (await axios.get(this.internalURL('/logs'))).data
|
||||
const response = await fetchWithHeaders(this.internalURL('/logs'))
|
||||
return response.text()
|
||||
}
|
||||
|
||||
async getRawLogs(): Promise<LogsRawResponse> {
|
||||
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<void> {
|
||||
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<Record<string, string[]>> {
|
||||
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<Record<string, any>> {
|
||||
return (await axios.get(this.apiURL('/i18n'))).data
|
||||
const response = await fetchWithHeaders(this.apiURL('/i18n'))
|
||||
return response.json()
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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 }))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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<string> {
|
||||
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)
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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(() => {})
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
}))
|
||||
|
||||
@@ -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<typeof useReleaseService>
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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 () => ''
|
||||
})
|
||||
@@ -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 () => '<video src="video.mp4"></video>'
|
||||
})
|
||||
@@ -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 () => '<video src="video.mp4"></video>'
|
||||
})
|
||||
@@ -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 () =>
|
||||
'<video><source src="video.mp4" type="video/mp4" /></video>'
|
||||
@@ -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 () =>
|
||||
'<video><source src="video.webm" type="video/webm" /></video>'
|
||||
@@ -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\n<img src="image.png" alt="Test 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\n<img src="image.png" alt="Test 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 () => '<img src="/absolute/image.png" alt="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 () =>
|
||||
'<img src="https://example.com/image.png" alt="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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user