voice-ai-workforce
Version:
Voice AI assistant for workforce management - hands-free task updates, time tracking, and team communication. Built by VenueBoost Inc.
726 lines (636 loc) • 21 kB
text/typescript
// packages/react/src/__tests__/useVoiceAI.test.ts - FIXED
import { renderHook, act } from '@testing-library/react';
import { useVoiceAI } from '../hooks/useVoiceAI';
import { VoiceAIConfig, SpeechProvider, AIProvider, ResponseMode, VoiceInterfaceMode } from '../../../types/src/types';
// Create a mock constructor that resolves immediately
const mockVoiceAI = {
startListening: jest.fn().mockResolvedValue(undefined),
stopListening: jest.fn().mockResolvedValue(undefined),
processTextInput: jest.fn().mockResolvedValue({
success: true,
text: 'Command processed',
data: {}
}),
speak: jest.fn().mockResolvedValue(undefined),
updateConfig: jest.fn(),
updateContext: jest.fn(),
getState: jest.fn(() => ({
isListening: false,
isProcessing: false,
isAvailable: true,
activeProvider: AIProvider.OPENAI,
providerStatus: {
[AIProvider.OPENAI]: 'available',
[AIProvider.ANTHROPIC]: 'error',
[AIProvider.GOOGLE]: 'error',
[AIProvider.KEYWORDS]: 'available'
},
commandHistory: [],
suggestedCommands: []
})),
};
// Mock the VoiceAI module
jest.mock('../../../core/src/VoiceAI', () => ({
VoiceAI: jest.fn().mockImplementation((config, events) => {
// Simulate immediate initialization
setTimeout(() => {
events?.onStateChange?.({
isListening: false,
isProcessing: false,
isAvailable: true,
activeProvider: AIProvider.OPENAI,
providerStatus: {
[AIProvider.OPENAI]: 'available',
[AIProvider.ANTHROPIC]: 'error',
[AIProvider.GOOGLE]: 'error',
[AIProvider.KEYWORDS]: 'available'
},
commandHistory: [],
suggestedCommands: []
});
}, 0);
return mockVoiceAI;
}),
}));
// Mock the useVoiceVisibility hook - FIXED to use correct import path
jest.mock('../hooks/useVoiceVisibility', () => ({
useVoiceVisibility: jest.fn(() => ({
visibility: {
showProviders: true,
showProviderStatus: true,
showDebugInfo: true,
showConfidenceScores: true,
showTechnicalErrors: true,
showAdvancedSettings: true,
showCommandHistory: true,
showMiniCenter: true,
showStatusIndicator: true,
useGenericLabels: false
},
labels: {
voiceButton: {
startText: 'Start Listening',
stopText: 'Stop Listening',
processingText: 'Processing voice...',
errorText: 'Voice error'
},
status: {
online: 'Online',
offline: 'Offline',
listening: 'Listening',
processing: 'Processing',
error: 'Error'
},
providers: {
generic: 'AI Provider',
fallback: 'Keywords'
},
errors: {
generic: 'An error occurred',
connection: 'Connection failed',
permission: 'Permission denied'
}
}
}))
}));
// Get the mocked constructor for assertions
const { VoiceAI: MockVoiceAIConstructor } = jest.requireMock('../../../core/src/VoiceAI');
const { useVoiceVisibility: mockUseVoiceVisibility } = jest.requireMock('../hooks/useVoiceVisibility');
describe('useVoiceAI Hook', () => {
let mockConfig: VoiceAIConfig;
beforeEach(() => {
jest.clearAllMocks();
mockConfig = {
aiProviders: {
primary: {
provider: AIProvider.OPENAI,
apiKey: 'test-key',
model: 'gpt-3.5-turbo'
},
fallbacks: [{
provider: AIProvider.KEYWORDS,
fallbackMode: true
}]
},
speechToText: {
provider: SpeechProvider.WEB_SPEECH,
language: 'en-US',
},
textToSpeech: {
provider: SpeechProvider.WEB_SPEECH,
speed: 1.0,
},
responseMode: ResponseMode.BOTH,
interfaceMode: 'developer' as VoiceInterfaceMode
};
// Reset the mock to return default values
mockUseVoiceVisibility.mockReturnValue({
visibility: {
showProviders: true,
showProviderStatus: true,
showDebugInfo: true,
showConfidenceScores: true,
showTechnicalErrors: true,
showAdvancedSettings: true,
showCommandHistory: true,
showMiniCenter: true,
showStatusIndicator: true,
useGenericLabels: false
},
labels: {
voiceButton: {
startText: 'Start Listening',
stopText: 'Stop Listening',
processingText: 'Processing voice...',
errorText: 'Voice error'
},
status: {
online: 'Online',
offline: 'Offline',
listening: 'Listening',
processing: 'Processing',
error: 'Error'
},
providers: {
generic: 'AI Provider',
fallback: 'Keywords'
},
errors: {
generic: 'An error occurred',
connection: 'Connection failed',
permission: 'Permission denied'
}
}
});
});
describe('Initialization', () => {
it('should initialize and create VoiceAI instance', async () => {
const { result } = renderHook(() =>
useVoiceAI({ config: mockConfig })
);
// Should call useVoiceVisibility hook
expect(mockUseVoiceVisibility).toHaveBeenCalledWith(
mockConfig,
undefined, // no component mode
undefined // no overrides
);
// Should create VoiceAI instance with mode-aware config
expect(MockVoiceAIConstructor).toHaveBeenCalledWith(
expect.objectContaining({
...mockConfig,
interfaceMode: 'developer',
visibility: expect.objectContaining({
showProviders: true,
showDebugInfo: true,
customLabels: expect.any(Object)
})
}),
expect.objectContaining({
onCommand: expect.any(Function),
onResponse: expect.any(Function),
onError: expect.any(Function),
onStateChange: expect.any(Function),
})
);
// Wait for state update
await act(async () => {
await new Promise(resolve => setTimeout(resolve, 10));
});
expect(result.current.isAvailable).toBe(true);
expect(result.current.visibility).toBeDefined();
expect(result.current.labels).toBeDefined();
});
it('should provide all required functions and properties', async () => {
const { result } = renderHook(() =>
useVoiceAI({ config: mockConfig })
);
await act(async () => {
await new Promise(resolve => setTimeout(resolve, 10));
});
// Check all functions are defined
expect(typeof result.current.startListening).toBe('function');
expect(typeof result.current.stopListening).toBe('function');
expect(typeof result.current.processText).toBe('function');
expect(typeof result.current.speak).toBe('function');
expect(typeof result.current.updateConfig).toBe('function');
expect(typeof result.current.updateContext).toBe('function');
expect(typeof result.current.getState).toBe('function');
// Check mode-aware properties
expect(result.current.visibility).toBeDefined();
expect(result.current.labels).toBeDefined();
});
it('should handle component mode override', async () => {
const { result } = renderHook(() =>
useVoiceAI({
config: mockConfig,
mode: 'end-user'
})
);
// Should call useVoiceVisibility with component mode
expect(mockUseVoiceVisibility).toHaveBeenCalledWith(
mockConfig,
'end-user', // component mode override
undefined // no overrides
);
await act(async () => {
await new Promise(resolve => setTimeout(resolve, 10));
});
expect(result.current.visibility).toBeDefined();
expect(result.current.labels).toBeDefined();
});
it('should handle visibility overrides', async () => {
const visibilityOverrides = { showDebugInfo: false };
const { result } = renderHook(() =>
useVoiceAI({
config: mockConfig,
visibilityOverrides
})
);
// Should call useVoiceVisibility with overrides
expect(mockUseVoiceVisibility).toHaveBeenCalledWith(
mockConfig,
undefined, // no component mode
visibilityOverrides // visibility overrides
);
await act(async () => {
await new Promise(resolve => setTimeout(resolve, 10));
});
expect(result.current.visibility).toBeDefined();
});
});
describe('Mode-Aware Error Handling', () => {
it('should filter errors based on visibility settings', async () => {
// Mock visibility to hide technical errors
mockUseVoiceVisibility.mockReturnValue({
visibility: {
showTechnicalErrors: false,
showProviders: false,
showDebugInfo: false,
showConfidenceScores: false,
showAdvancedSettings: false,
showCommandHistory: true,
showMiniCenter: true,
showStatusIndicator: true,
useGenericLabels: true
},
labels: {
voiceButton: {
startText: 'Start Voice',
stopText: 'Stop Voice',
processingText: 'Processing...',
errorText: 'Voice Unavailable'
},
status: {
online: 'Voice Ready',
offline: 'Voice Unavailable',
listening: 'Listening...',
processing: 'Processing...',
error: 'Voice Error'
},
providers: {
generic: 'Voice Assistant',
fallback: 'Voice Assistant'
},
errors: {
generic: 'Voice assistant is temporarily unavailable',
connection: 'Please check your connection',
permission: 'Microphone permission required'
}
}
});
const onError = jest.fn();
const { result } = renderHook(() =>
useVoiceAI({
config: mockConfig,
mode: 'end-user',
onError
})
);
await act(async () => {
await new Promise(resolve => setTimeout(resolve, 10));
});
// Get the event handlers that were passed to VoiceAI
const voiceAICall = MockVoiceAIConstructor.mock.calls[0];
const eventHandlers = voiceAICall[1];
// Simulate an error from VoiceAI
const mockError = {
code: 'TECHNICAL_ERROR',
message: 'Complex technical error with stack trace',
details: { stack: 'Error stack trace...' }
};
act(() => {
eventHandlers.onError(mockError);
});
// Should call onError with filtered error (generic message)
expect(onError).toHaveBeenCalledWith({
code: 'TECHNICAL_ERROR',
message: 'Voice assistant is temporarily unavailable',
details: undefined // Technical details should be filtered out
});
});
it('should preserve technical errors when visibility allows', async () => {
// Mock visibility to show technical errors
mockUseVoiceVisibility.mockReturnValue({
visibility: {
showTechnicalErrors: true,
showProviders: true,
showDebugInfo: true,
showConfidenceScores: true,
showAdvancedSettings: true,
showCommandHistory: true,
showMiniCenter: true,
showStatusIndicator: true,
useGenericLabels: false
},
labels: {
voiceButton: {
startText: 'Start Listening',
stopText: 'Stop Listening',
processingText: 'Processing voice...',
errorText: 'Voice error'
},
status: {
online: 'Online',
offline: 'Offline',
listening: 'Listening',
processing: 'Processing',
error: 'Error'
},
providers: {
generic: 'AI Provider',
fallback: 'Keywords'
},
errors: {
generic: 'An error occurred',
connection: 'Connection failed',
permission: 'Permission denied'
}
}
});
const onError = jest.fn();
const { result } = renderHook(() =>
useVoiceAI({
config: mockConfig,
mode: 'developer',
onError
})
);
await act(async () => {
await new Promise(resolve => setTimeout(resolve, 10));
});
// Get the event handlers
const voiceAICall = MockVoiceAIConstructor.mock.calls[0];
const eventHandlers = voiceAICall[1];
// Simulate an error from VoiceAI
const mockError = {
code: 'TECHNICAL_ERROR',
message: 'Complex technical error with stack trace',
details: { stack: 'Error stack trace...' }
};
act(() => {
eventHandlers.onError(mockError);
});
// Should call onError with full error details
expect(onError).toHaveBeenCalledWith(mockError);
});
});
describe('Voice Control Functions', () => {
it('should call startListening on VoiceAI instance', async () => {
const { result } = renderHook(() =>
useVoiceAI({ config: mockConfig })
);
await act(async () => {
await new Promise(resolve => setTimeout(resolve, 10));
await result.current.startListening();
});
expect(mockVoiceAI.startListening).toHaveBeenCalled();
});
it('should call stopListening on VoiceAI instance', async () => {
const { result } = renderHook(() =>
useVoiceAI({ config: mockConfig })
);
await act(async () => {
await new Promise(resolve => setTimeout(resolve, 10));
await result.current.stopListening();
});
expect(mockVoiceAI.stopListening).toHaveBeenCalled();
});
it('should call processText and return filtered response', async () => {
const { result } = renderHook(() =>
useVoiceAI({ config: mockConfig })
);
let response: any;
await act(async () => {
await new Promise(resolve => setTimeout(resolve, 10));
response = await result.current.processText('test command');
});
expect(mockVoiceAI.processTextInput).toHaveBeenCalledWith('test command');
expect(response).toEqual({
success: true,
text: 'Command processed',
data: {}
});
});
it('should call speak on VoiceAI instance', async () => {
const { result } = renderHook(() =>
useVoiceAI({ config: mockConfig })
);
await act(async () => {
await new Promise(resolve => setTimeout(resolve, 10));
await result.current.speak('Hello world');
});
expect(mockVoiceAI.speak).toHaveBeenCalledWith('Hello world');
});
});
describe('Configuration Functions', () => {
it('should call updateConfig with mode-aware config on VoiceAI instance', async () => {
const { result } = renderHook(() =>
useVoiceAI({ config: mockConfig, mode: 'project' })
);
const newConfig = { responseMode: ResponseMode.TEXT };
await act(async () => {
await new Promise(resolve => setTimeout(resolve, 10));
result.current.updateConfig(newConfig);
});
expect(mockVoiceAI.updateConfig).toHaveBeenCalledWith(
expect.objectContaining({
...newConfig,
interfaceMode: 'project',
visibility: expect.any(Object)
})
);
});
it('should call updateContext on VoiceAI instance', async () => {
const { result } = renderHook(() =>
useVoiceAI({ config: mockConfig })
);
const context = { userRole: 'manager' };
await act(async () => {
await new Promise(resolve => setTimeout(resolve, 10));
result.current.updateContext(context);
});
expect(mockVoiceAI.updateContext).toHaveBeenCalledWith(context);
});
it('should filter context in end-user mode', async () => {
// Mock end-user mode visibility
mockUseVoiceVisibility.mockReturnValue({
visibility: {
showDebugInfo: false,
showProviders: false,
showTechnicalErrors: false,
showAdvancedSettings: false,
showCommandHistory: true,
showMiniCenter: true,
useGenericLabels: true
},
labels: {
voiceButton: { startText: 'Start Voice' },
status: { online: 'Voice Ready' },
providers: { generic: 'Voice Assistant' },
errors: { generic: 'Voice unavailable' }
}
});
const { result } = renderHook(() =>
useVoiceAI({ config: mockConfig, mode: 'end-user' })
);
const context = {
userRole: 'manager',
debug_info: 'should be filtered',
internal_data: 'should be filtered',
public_data: 'should remain'
};
await act(async () => {
await new Promise(resolve => setTimeout(resolve, 10));
result.current.updateContext(context);
});
// Should filter out debug_ and internal_ prefixed properties
expect(mockVoiceAI.updateContext).toHaveBeenCalledWith({
userRole: 'manager',
public_data: 'should remain'
});
});
});
describe('State Management', () => {
it('should return filtered state from getState', async () => {
const { result } = renderHook(() =>
useVoiceAI({ config: mockConfig })
);
await act(async () => {
await new Promise(resolve => setTimeout(resolve, 10));
});
const state = result.current.getState();
expect(state).toHaveProperty('isListening');
expect(state).toHaveProperty('isProcessing');
expect(state).toHaveProperty('isAvailable');
expect(state).toHaveProperty('activeProvider'); // Should be visible in developer mode
});
it('should filter state in end-user mode', async () => {
// Mock end-user mode visibility
mockUseVoiceVisibility.mockReturnValue({
visibility: {
showProviders: false,
showProviderStatus: false,
showDebugInfo: false,
showAdvancedSettings: false,
showCommandHistory: true
},
labels: {
voiceButton: { startText: 'Start Voice' },
status: { online: 'Voice Ready' },
providers: { generic: 'Voice Assistant' },
errors: { generic: 'Voice unavailable' }
}
});
const { result } = renderHook(() =>
useVoiceAI({ config: mockConfig, mode: 'end-user' })
);
await act(async () => {
await new Promise(resolve => setTimeout(resolve, 10));
});
const state = result.current.getState();
expect(state.activeProvider).toBeUndefined(); // Should be filtered out
expect(state.providerStatus).toBeUndefined(); // Should be filtered out
expect(state.commandHistory).toBeDefined(); // Should remain (allowed in end-user)
});
});
describe('Event Handlers', () => {
it('should call event handlers when provided', () => {
const onCommand = jest.fn();
const onResponse = jest.fn();
const onError = jest.fn();
renderHook(() =>
useVoiceAI({
config: mockConfig,
onCommand,
onResponse,
onError,
})
);
// Verify VoiceAI was created with event handlers
expect(MockVoiceAIConstructor).toHaveBeenCalledWith(
expect.any(Object),
expect.objectContaining({
onCommand: expect.any(Function),
onResponse: expect.any(Function),
onError: expect.any(Function),
onStateChange: expect.any(Function),
})
);
});
});
describe('Cleanup', () => {
it('should call stopListening on unmount', async () => {
const { unmount } = renderHook(() =>
useVoiceAI({ config: mockConfig })
);
await act(async () => {
await new Promise(resolve => setTimeout(resolve, 10));
});
unmount();
// stopListening should be called during cleanup
expect(mockVoiceAI.stopListening).toHaveBeenCalled();
});
});
describe('Auto Start', () => {
it('should auto start when autoStart is true', async () => {
renderHook(() =>
useVoiceAI({
config: mockConfig,
autoStart: true
})
);
await act(async () => {
await new Promise(resolve => setTimeout(resolve, 10));
});
expect(mockVoiceAI.startListening).toHaveBeenCalled();
});
it('should not auto start when mini center is disabled', async () => {
// Mock visibility with mini center disabled
mockUseVoiceVisibility.mockReturnValue({
visibility: {
showMiniCenter: false,
showProviders: false,
showDebugInfo: false
},
labels: {
voiceButton: { startText: 'Start Voice' },
status: { online: 'Voice Ready' },
providers: { generic: 'Voice Assistant' },
errors: { generic: 'Voice unavailable' }
}
});
renderHook(() =>
useVoiceAI({
config: mockConfig,
autoStart: true,
mode: 'end-user'
})
);
await act(async () => {
await new Promise(resolve => setTimeout(resolve, 10));
});
// Should not auto-start when mini center is disabled
expect(mockVoiceAI.startListening).not.toHaveBeenCalled();
});
});
});