Skip to content

Commit 3e68e0d

Browse files
committed
CU-868f7hkrj Fix bug in audio setup, added translations.
1 parent e8c9b7f commit 3e68e0d

File tree

7 files changed

+402
-57
lines changed

7 files changed

+402
-57
lines changed

src/components/livekit/livekit-bottom-sheet.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -216,7 +216,7 @@ export const LiveKitBottomSheet = () => {
216216

217217
<Card className="mb-4 w-full p-4">
218218
<VStack space="sm">
219-
<Text className="text-center text-lg font-medium">{currentRoomInfo?.Name}</Text>
219+
<Text className="text-center text-lg font-medium">{currentRoomInfo?.Name || ''}</Text>
220220
{isTalking && <Text className="text-center text-green-500">{t('livekit.speaking')}</Text>}
221221

222222
{/* Audio Device Info */}
@@ -264,7 +264,7 @@ export const LiveKitBottomSheet = () => {
264264
<Text className="font-medium text-blue-500">{t('common.back')}</Text>
265265
</TouchableOpacity>
266266
<Text className="text-lg font-bold">{t('livekit.audio_settings')}</Text>
267-
<View style={{ width: 50 }} /> {/* Spacer for centering */}
267+
<View style={styles.spacer} />
268268
</HStack>
269269

270270
<AudioDeviceSelection showTitle={false} />
@@ -351,4 +351,7 @@ const styles = StyleSheet.create({
351351
color: 'white',
352352
fontWeight: '600',
353353
},
354+
spacer: {
355+
width: 50,
356+
},
354357
});
Lines changed: 283 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,283 @@
1+
import { describe, expect, it, jest, beforeEach } from '@jest/globals';
2+
import { render, screen, fireEvent } from '@testing-library/react-native';
3+
import React from 'react';
4+
5+
import { type AudioDeviceInfo } from '@/stores/app/bluetooth-audio-store';
6+
7+
import { AudioDeviceSelection } from '../audio-device-selection';
8+
9+
// Mock the translation hook
10+
jest.mock('react-i18next', () => ({
11+
useTranslation: () => ({
12+
t: (key: string) => {
13+
const translations: Record<string, string> = {
14+
'settings.audio_device_selection.title': 'Audio Device Selection',
15+
'settings.audio_device_selection.current_selection': 'Current Selection',
16+
'settings.audio_device_selection.microphone': 'Microphone',
17+
'settings.audio_device_selection.speaker': 'Speaker',
18+
'settings.audio_device_selection.none_selected': 'None selected',
19+
'settings.audio_device_selection.bluetooth_device': 'Bluetooth Device',
20+
'settings.audio_device_selection.wired_device': 'Wired Device',
21+
'settings.audio_device_selection.speaker_device': 'Speaker Device',
22+
'settings.audio_device_selection.unavailable': 'Unavailable',
23+
'settings.audio_device_selection.no_microphones_available': 'No microphones available',
24+
'settings.audio_device_selection.no_speakers_available': 'No speakers available',
25+
};
26+
return translations[key] || key;
27+
},
28+
}),
29+
}));
30+
31+
// Mock the bluetooth audio store
32+
const mockSetSelectedMicrophone = jest.fn();
33+
const mockSetSelectedSpeaker = jest.fn();
34+
35+
const mockStore = {
36+
availableAudioDevices: [] as AudioDeviceInfo[],
37+
selectedAudioDevices: {
38+
microphone: null as AudioDeviceInfo | null,
39+
speaker: null as AudioDeviceInfo | null,
40+
},
41+
setSelectedMicrophone: mockSetSelectedMicrophone,
42+
setSelectedSpeaker: mockSetSelectedSpeaker,
43+
};
44+
45+
jest.mock('@/stores/app/bluetooth-audio-store', () => ({
46+
useBluetoothAudioStore: () => mockStore,
47+
}));
48+
49+
describe('AudioDeviceSelection', () => {
50+
beforeEach(() => {
51+
jest.clearAllMocks();
52+
// Reset mock store to default state
53+
mockStore.availableAudioDevices = [];
54+
mockStore.selectedAudioDevices = {
55+
microphone: null,
56+
speaker: null,
57+
};
58+
});
59+
60+
const createMockDevice = (id: string, name: string, type: 'bluetooth' | 'wired' | 'speaker', isAvailable = true): AudioDeviceInfo => ({
61+
id,
62+
name,
63+
type,
64+
isAvailable,
65+
});
66+
67+
describe('rendering', () => {
68+
it('renders with title when showTitle is true', () => {
69+
render(<AudioDeviceSelection showTitle={true} />);
70+
71+
expect(screen.getByText('Audio Device Selection')).toBeTruthy();
72+
});
73+
74+
it('renders without title when showTitle is false', () => {
75+
render(<AudioDeviceSelection showTitle={false} />);
76+
77+
expect(screen.queryByText('Audio Device Selection')).toBeNull();
78+
});
79+
80+
it('renders current selection section', () => {
81+
render(<AudioDeviceSelection />);
82+
83+
expect(screen.getByText('Current Selection')).toBeTruthy();
84+
expect(screen.getByText('Microphone:')).toBeTruthy();
85+
expect(screen.getByText('Speaker:')).toBeTruthy();
86+
});
87+
88+
it('shows none selected when no devices are selected', () => {
89+
render(<AudioDeviceSelection />);
90+
91+
const noneSelectedTexts = screen.getAllByText('None selected');
92+
expect(noneSelectedTexts).toHaveLength(2); // One for microphone, one for speaker
93+
});
94+
95+
it('renders microphone and speaker sections', () => {
96+
render(<AudioDeviceSelection />);
97+
98+
// Check for section headers
99+
const microphoneHeaders = screen.getAllByText('Microphone');
100+
const speakerHeaders = screen.getAllByText('Speaker');
101+
102+
expect(microphoneHeaders.length).toBeGreaterThan(0);
103+
expect(speakerHeaders.length).toBeGreaterThan(0);
104+
});
105+
});
106+
107+
describe('device selection', () => {
108+
it('displays available microphones', () => {
109+
const bluetoothMic = createMockDevice('bt-mic-1', 'Bluetooth Headset', 'bluetooth');
110+
const wiredMic = createMockDevice('wired-mic-1', 'Built-in Microphone', 'wired');
111+
112+
mockStore.availableAudioDevices = [bluetoothMic, wiredMic];
113+
114+
render(<AudioDeviceSelection />);
115+
116+
// Check device names appear (may appear in multiple sections)
117+
expect(screen.getAllByText('Bluetooth Headset').length).toBeGreaterThan(0);
118+
expect(screen.getAllByText('Built-in Microphone').length).toBeGreaterThan(0);
119+
expect(screen.getAllByText('Bluetooth Device').length).toBeGreaterThan(0);
120+
expect(screen.getAllByText('Wired Device').length).toBeGreaterThan(0);
121+
});
122+
123+
it('displays available speakers', () => {
124+
const bluetoothSpeaker = createMockDevice('bt-speaker-1', 'Bluetooth Speaker', 'bluetooth');
125+
const builtinSpeaker = createMockDevice('builtin-speaker-1', 'Built-in Speaker', 'speaker');
126+
127+
mockStore.availableAudioDevices = [bluetoothSpeaker, builtinSpeaker];
128+
129+
render(<AudioDeviceSelection />);
130+
131+
// Check device names appear (may appear in multiple sections)
132+
expect(screen.getAllByText('Bluetooth Speaker').length).toBeGreaterThan(0);
133+
expect(screen.getAllByText('Built-in Speaker').length).toBeGreaterThan(0);
134+
expect(screen.getAllByText('Speaker Device').length).toBeGreaterThan(0);
135+
});
136+
137+
it('shows unavailable indicator for unavailable devices', () => {
138+
const unavailableDevice = createMockDevice('unavailable-1', 'Unavailable Device', 'bluetooth', false);
139+
140+
mockStore.availableAudioDevices = [unavailableDevice];
141+
142+
render(<AudioDeviceSelection />);
143+
144+
// Device should not appear in either section since it's unavailable bluetooth
145+
expect(screen.queryByText('Unavailable Device')).toBeNull();
146+
});
147+
148+
it('calls setSelectedMicrophone when microphone device is pressed', () => {
149+
const bluetoothMic = createMockDevice('bt-mic-1', 'Bluetooth Headset', 'bluetooth');
150+
151+
mockStore.availableAudioDevices = [bluetoothMic];
152+
153+
const { getAllByText } = render(<AudioDeviceSelection />);
154+
155+
// Find the first device card (should be in microphone section)
156+
const deviceCards = getAllByText('Bluetooth Headset');
157+
fireEvent.press(deviceCards[0].parent?.parent?.parent as any);
158+
159+
expect(mockSetSelectedMicrophone).toHaveBeenCalledWith(bluetoothMic);
160+
});
161+
162+
it('calls setSelectedSpeaker when speaker device is pressed', () => {
163+
const bluetoothSpeaker = createMockDevice('bt-speaker-1', 'Bluetooth Speaker', 'bluetooth');
164+
165+
mockStore.availableAudioDevices = [bluetoothSpeaker];
166+
167+
const { getAllByText } = render(<AudioDeviceSelection />);
168+
169+
// Find the second device card (should be in speaker section)
170+
const deviceCards = getAllByText('Bluetooth Speaker');
171+
fireEvent.press(deviceCards[1].parent?.parent?.parent as any);
172+
173+
expect(mockSetSelectedSpeaker).toHaveBeenCalledWith(bluetoothSpeaker);
174+
});
175+
176+
it('highlights selected devices', () => {
177+
const selectedMic = createMockDevice('selected-mic', 'Selected Microphone', 'bluetooth');
178+
const selectedSpeaker = createMockDevice('selected-speaker', 'Selected Speaker', 'bluetooth');
179+
180+
mockStore.availableAudioDevices = [selectedMic, selectedSpeaker];
181+
mockStore.selectedAudioDevices = {
182+
microphone: selectedMic,
183+
speaker: selectedSpeaker,
184+
};
185+
186+
render(<AudioDeviceSelection />);
187+
188+
// Check that selected device names are shown in current selection and device sections
189+
expect(screen.getAllByText('Selected Microphone').length).toBeGreaterThan(0);
190+
expect(screen.getAllByText('Selected Speaker').length).toBeGreaterThan(0);
191+
});
192+
});
193+
194+
describe('empty states', () => {
195+
it('shows no microphones available message when no microphones are available', () => {
196+
// Add an unavailable bluetooth device (should not show in microphones section)
197+
const unavailableBluetooth = createMockDevice('bt-1', 'BT Device', 'bluetooth', false);
198+
mockStore.availableAudioDevices = [unavailableBluetooth];
199+
200+
render(<AudioDeviceSelection />);
201+
202+
// Should show empty message since bluetooth device is unavailable
203+
expect(screen.getByText('No microphones available')).toBeTruthy();
204+
});
205+
206+
it('shows no speakers available message when no speakers are available', () => {
207+
// Only add unavailable speakers (which get filtered out)
208+
const unavailableSpeaker = createMockDevice('speaker-1', 'Speaker', 'speaker', false);
209+
mockStore.availableAudioDevices = [unavailableSpeaker];
210+
211+
render(<AudioDeviceSelection />);
212+
213+
expect(screen.getByText('No speakers available')).toBeTruthy();
214+
});
215+
216+
it('shows both empty messages when no devices are available', () => {
217+
mockStore.availableAudioDevices = [];
218+
219+
render(<AudioDeviceSelection />);
220+
221+
expect(screen.getByText('No microphones available')).toBeTruthy();
222+
expect(screen.getByText('No speakers available')).toBeTruthy();
223+
});
224+
});
225+
226+
describe('device filtering', () => {
227+
it('filters out unavailable bluetooth devices for microphones', () => {
228+
const availableBluetooth = createMockDevice('bt-available', 'Available BT', 'bluetooth', true);
229+
const unavailableBluetooth = createMockDevice('bt-unavailable', 'Unavailable BT', 'bluetooth', false);
230+
const wiredDevice = createMockDevice('wired-1', 'Wired Device', 'wired', false); // Should still show even if unavailable
231+
232+
mockStore.availableAudioDevices = [availableBluetooth, unavailableBluetooth, wiredDevice];
233+
234+
render(<AudioDeviceSelection />);
235+
236+
expect(screen.getAllByText('Available BT').length).toBeGreaterThan(0);
237+
expect(screen.queryByText('Unavailable BT')).toBeNull();
238+
expect(screen.getAllByText('Wired Device').length).toBeGreaterThan(0);
239+
});
240+
241+
it('filters out unavailable devices for speakers', () => {
242+
const availableDevice = createMockDevice('available', 'Available Device', 'speaker', true);
243+
const unavailableDevice = createMockDevice('unavailable', 'Unavailable Device', 'speaker', false);
244+
245+
mockStore.availableAudioDevices = [availableDevice, unavailableDevice];
246+
247+
render(<AudioDeviceSelection />);
248+
249+
expect(screen.getAllByText('Available Device').length).toBeGreaterThan(0);
250+
// Note: The component actually shows ALL devices in microphone section unless they are unavailable bluetooth
251+
// So the unavailable speaker will show in microphone section but not speaker section
252+
expect(screen.getAllByText('Unavailable Device').length).toBeGreaterThan(0); // Shows in microphone section
253+
});
254+
});
255+
256+
describe('device type labels', () => {
257+
it('shows correct labels for different device types', () => {
258+
const bluetoothDevice = createMockDevice('bt-1', 'BT Device', 'bluetooth');
259+
const wiredDevice = createMockDevice('wired-1', 'Wired Device', 'wired');
260+
const speakerDevice = createMockDevice('speaker-1', 'Speaker Device', 'speaker');
261+
262+
mockStore.availableAudioDevices = [bluetoothDevice, wiredDevice, speakerDevice];
263+
264+
render(<AudioDeviceSelection />);
265+
266+
expect(screen.getAllByText('Bluetooth Device').length).toBeGreaterThan(0);
267+
expect(screen.getAllByText('Wired Device').length).toBeGreaterThan(0);
268+
expect(screen.getAllByText('Speaker Device').length).toBeGreaterThan(0);
269+
});
270+
271+
it('shows fallback label for unknown device types', () => {
272+
const unknownDevice = createMockDevice('unknown-1', 'Unknown Device', 'unknown' as any);
273+
274+
mockStore.availableAudioDevices = [unknownDevice];
275+
276+
render(<AudioDeviceSelection />);
277+
278+
// Device should appear but with fallback label
279+
expect(screen.getAllByText('Unknown Device').length).toBeGreaterThan(0);
280+
expect(screen.getAllByText('Unknown Device').length).toBeGreaterThan(0);
281+
});
282+
});
283+
});

0 commit comments

Comments
 (0)