Files
ComfyUI_frontend/tests-ui/tests/audioService.test.ts
Christian Byrne 27ab355f9c [refactor] Improve updates/notifications domain organization (#5590)
* [refactor] Move update-related functionality to platform/updates domain

Reorganizes release management, version compatibility, and notification functionality
following Domain-Driven Design principles, mirroring VSCode's architecture pattern.

- Move releaseService.ts to platform/updates/common/
- Move releaseStore.ts to platform/updates/common/
- Move versionCompatibilityStore.ts to platform/updates/common/
- Move useFrontendVersionMismatchWarning.ts to platform/updates/common/
- Move toastStore.ts to platform/updates/common/
- Move ReleaseNotificationToast.vue to platform/updates/components/
- Move WhatsNewPopup.vue to platform/updates/components/
- Update 25+ import paths across codebase and tests

This creates a cohesive "updates" domain containing all functionality related to
software updates, version checking, release notifications, and user communication
about application state changes.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* fix imports

---------

Co-authored-by: Claude <noreply@anthropic.com>
2025-09-15 04:34:37 -07:00

333 lines
10 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from 'vitest'
import {
type AudioRecordingError,
useAudioService
} from '@/services/audioService'
const mockRegister = vi.hoisted(() => vi.fn())
const mockConnect = vi.hoisted(() => vi.fn())
const mockApi = vi.hoisted(() => ({
fetchApi: vi.fn()
}))
const mockToastStore = vi.hoisted(() => ({
addAlert: vi.fn()
}))
vi.mock('extendable-media-recorder', () => ({
register: mockRegister
}))
vi.mock('extendable-media-recorder-wav-encoder', () => ({
connect: mockConnect
}))
vi.mock('@/scripts/api', () => ({
api: mockApi
}))
vi.mock('@/platform/updates/common/toastStore', () => ({
useToastStore: vi.fn(() => mockToastStore)
}))
describe('useAudioService', () => {
let service: ReturnType<typeof useAudioService>
const mockBlob = new Blob(['test audio data'], { type: 'audio/wav' })
const mockUploadResponse = {
name: 'test-audio-123.wav'
}
beforeEach(() => {
vi.clearAllMocks()
vi.spyOn(console, 'error').mockImplementation(() => {})
mockConnect.mockResolvedValue('mock-encoder')
mockRegister.mockResolvedValue(undefined)
mockApi.fetchApi.mockResolvedValue({
status: 200,
json: () => Promise.resolve(mockUploadResponse)
})
service = useAudioService()
})
describe('initialization', () => {
it('should initialize service with required methods', () => {
expect(service).toHaveProperty('registerWavEncoder')
expect(service).toHaveProperty('stopAllTracks')
expect(service).toHaveProperty('convertBlobToFileAndSubmit')
expect(typeof service.registerWavEncoder).toBe('function')
expect(typeof service.stopAllTracks).toBe('function')
expect(typeof service.convertBlobToFileAndSubmit).toBe('function')
})
})
describe('registerWavEncoder', () => {
it('should register WAV encoder successfully on first call', async () => {
await service.registerWavEncoder()
expect(mockConnect).toHaveBeenCalledTimes(1)
expect(mockRegister).toHaveBeenCalledWith('mock-encoder')
})
it('should not register again if already registered', async () => {
await service.registerWavEncoder()
mockConnect.mockClear()
mockRegister.mockClear()
await service.registerWavEncoder()
expect(mockConnect).not.toHaveBeenCalled()
expect(mockRegister).not.toHaveBeenCalled()
})
it('should handle "already an encoder stored" error gracefully', async () => {
const error = new Error(
'There is already an encoder stored which handles exactly the same mime types.'
)
mockRegister.mockRejectedValueOnce(error)
await service.registerWavEncoder()
expect(mockConnect).toHaveBeenCalledTimes(0)
expect(mockRegister).toHaveBeenCalledTimes(0)
expect(console.error).not.toHaveBeenCalled()
})
})
describe('stopAllTracks', () => {
it('should stop all tracks in a stream', () => {
const mockTrack1 = { stop: vi.fn() }
const mockTrack2 = { stop: vi.fn() }
const mockStream = {
getTracks: vi.fn().mockReturnValue([mockTrack1, mockTrack2])
} as unknown as MediaStream
service.stopAllTracks(mockStream)
expect(mockStream.getTracks).toHaveBeenCalledTimes(1)
expect(mockTrack1.stop).toHaveBeenCalledTimes(1)
expect(mockTrack2.stop).toHaveBeenCalledTimes(1)
})
it('should handle null stream gracefully', () => {
expect(() => service.stopAllTracks(null)).not.toThrow()
})
it('should handle stream with no tracks', () => {
const mockStream = {
getTracks: vi.fn().mockReturnValue([])
} as unknown as MediaStream
expect(() => service.stopAllTracks(mockStream)).not.toThrow()
expect(mockStream.getTracks).toHaveBeenCalledTimes(1)
})
it('should handle tracks that throw on stop', () => {
const mockTrack1 = { stop: vi.fn() }
const mockTrack2 = {
stop: vi.fn().mockImplementation(() => {
throw new Error('Stop failed')
})
}
const mockStream = {
getTracks: vi.fn().mockReturnValue([mockTrack1, mockTrack2])
} as unknown as MediaStream
expect(() => service.stopAllTracks(mockStream)).toThrow()
expect(mockTrack1.stop).toHaveBeenCalledTimes(1)
expect(mockTrack2.stop).toHaveBeenCalledTimes(1)
})
})
describe('convertBlobToFileAndSubmit', () => {
it('should convert blob to file and upload successfully', async () => {
const result = await service.convertBlobToFileAndSubmit(mockBlob)
expect(mockApi.fetchApi).toHaveBeenCalledWith('/upload/image', {
method: 'POST',
body: expect.any(FormData)
})
expect(result).toBe('audio/test-audio-123.wav [temp]')
})
it('should create file with correct name and type', async () => {
const mockTimestamp = 1640995200000
vi.spyOn(Date, 'now').mockReturnValue(mockTimestamp)
await service.convertBlobToFileAndSubmit(mockBlob)
const formDataCall = mockApi.fetchApi.mock.calls[0][1].body as FormData
const uploadedFile = formDataCall.get('image') as File
expect(uploadedFile).toBeInstanceOf(File)
expect(uploadedFile.name).toBe(`recording-${mockTimestamp}.wav`)
expect(uploadedFile.type).toBe('audio/wav')
})
it('should set correct form data fields', async () => {
await service.convertBlobToFileAndSubmit(mockBlob)
const formDataCall = mockApi.fetchApi.mock.calls[0][1].body as FormData
expect(formDataCall.get('subfolder')).toBe('audio')
expect(formDataCall.get('type')).toBe('temp')
expect(formDataCall.get('image')).toBeInstanceOf(File)
})
it('should handle blob with different type', async () => {
const customBlob = new Blob(['test'], { type: 'audio/ogg' })
await service.convertBlobToFileAndSubmit(customBlob)
const formDataCall = mockApi.fetchApi.mock.calls[0][1].body as FormData
const uploadedFile = formDataCall.get('image') as File
expect(uploadedFile.type).toBe('audio/ogg')
})
it('should handle blob with no type', async () => {
const customBlob = new Blob(['test'])
await service.convertBlobToFileAndSubmit(customBlob)
const formDataCall = mockApi.fetchApi.mock.calls[0][1].body as FormData
const uploadedFile = formDataCall.get('image') as File
expect(uploadedFile.type).toBe('audio/wav') // Should default to audio/wav
})
it('should handle upload failure with error status', async () => {
mockApi.fetchApi.mockResolvedValueOnce({
status: 500,
statusText: 'Internal Server Error'
})
await expect(
service.convertBlobToFileAndSubmit(mockBlob)
).rejects.toThrow(
'Error uploading temp file: 500 - Internal Server Error'
)
expect(mockToastStore.addAlert).toHaveBeenCalledWith(
'Error uploading temp file: 500 - Internal Server Error'
)
})
it('should handle network errors', async () => {
const networkError = new Error('Network Error')
mockApi.fetchApi.mockRejectedValueOnce(networkError)
await expect(
service.convertBlobToFileAndSubmit(mockBlob)
).rejects.toThrow('Network Error')
})
it('should handle different status codes', async () => {
const testCases = [
{ status: 400, statusText: 'Bad Request' },
{ status: 403, statusText: 'Forbidden' },
{ status: 404, statusText: 'Not Found' },
{ status: 413, statusText: 'Payload Too Large' }
]
for (const testCase of testCases) {
mockApi.fetchApi.mockResolvedValueOnce(testCase)
await expect(
service.convertBlobToFileAndSubmit(mockBlob)
).rejects.toThrow(
`Error uploading temp file: ${testCase.status} - ${testCase.statusText}`
)
expect(mockToastStore.addAlert).toHaveBeenCalledWith(
`Error uploading temp file: ${testCase.status} - ${testCase.statusText}`
)
mockToastStore.addAlert.mockClear()
}
})
it('should handle malformed response JSON', async () => {
mockApi.fetchApi.mockResolvedValueOnce({
status: 200,
json: () => Promise.reject(new Error('Invalid JSON'))
})
await expect(
service.convertBlobToFileAndSubmit(mockBlob)
).rejects.toThrow('Invalid JSON')
})
it('should handle empty response', async () => {
mockApi.fetchApi.mockResolvedValueOnce({
status: 200,
json: () => Promise.resolve({})
})
const result = await service.convertBlobToFileAndSubmit(mockBlob)
expect(result).toBe('audio/undefined [temp]')
})
})
describe('error handling', () => {
it('should handle AudioRecordingError interface correctly', () => {
const error: AudioRecordingError = {
type: 'permission',
message: 'Microphone access denied',
originalError: new Error('Permission denied')
}
expect(error.type).toBe('permission')
expect(error.message).toBe('Microphone access denied')
expect(error.originalError).toBeInstanceOf(Error)
})
it('should support all error types', () => {
const errorTypes = [
'permission',
'not_supported',
'encoder',
'recording',
'unknown'
] as const
errorTypes.forEach((type) => {
const error: AudioRecordingError = {
type,
message: `Test error for ${type}`
}
expect(error.type).toBe(type)
})
})
})
describe('edge cases', () => {
it('should handle very large blobs', async () => {
const largeData = new Array(1000000).fill('a').join('')
const largeBlob = new Blob([largeData], { type: 'audio/wav' })
const result = await service.convertBlobToFileAndSubmit(largeBlob)
expect(result).toBe('audio/test-audio-123.wav [temp]')
expect(mockApi.fetchApi).toHaveBeenCalledTimes(1)
})
it('should handle empty blob', async () => {
const emptyBlob = new Blob([], { type: 'audio/wav' })
const result = await service.convertBlobToFileAndSubmit(emptyBlob)
expect(result).toBe('audio/test-audio-123.wav [temp]')
})
})
})