test: add comprehensive WebRTC test suite

Add test coverage for WebRTC signaling and detection:

Backend tests (socket-webrtc.test.js):
- WebRTC offer/answer relay via Socket.IO
- ICE candidate exchange
- Authorization checks for match access
- Full WebRTC signaling flow
- All 7 tests passing

Frontend tests (ready for test runner):
- webrtcDetection.test.js: Browser WebRTC capability detection
- WebRTCWarning.test.jsx: Warning component rendering and interaction

Note: Frontend tests require test runner setup (e.g., Vitest)
This commit is contained in:
Radosław Gierwiało
2025-11-15 16:29:15 +01:00
parent 44839e0317
commit 91962ab4d5
3 changed files with 789 additions and 0 deletions

View File

@@ -0,0 +1,195 @@
import { render, screen, fireEvent } from '@testing-library/react';
import '@testing-library/jest-dom';
import WebRTCWarning from '../WebRTCWarning';
describe('WebRTCWarning Component', () => {
describe('Rendering', () => {
it('should not render when WebRTC is working correctly', () => {
const detection = {
supported: true,
hasIceCandidates: true,
error: null,
};
const { container } = render(<WebRTCWarning detection={detection} />);
expect(container.firstChild).toBeNull();
});
it('should render warning when RTCPeerConnection not supported', () => {
const detection = {
supported: false,
hasIceCandidates: false,
error: 'RTCPeerConnection not available',
};
render(<WebRTCWarning detection={detection} />);
expect(screen.getByText('WebRTC Not Supported')).toBeInTheDocument();
expect(screen.getByText(/does not support WebRTC/i)).toBeInTheDocument();
});
it('should render warning when ICE candidates blocked', () => {
const detection = {
supported: true,
hasIceCandidates: false,
error: 'ICE candidates not generated',
};
render(<WebRTCWarning detection={detection} />);
expect(screen.getByText('WebRTC Blocked')).toBeInTheDocument();
expect(screen.getByText(/blocked by browser settings/i)).toBeInTheDocument();
});
it('should show fix suggestions for unsupported browser', () => {
const detection = {
supported: false,
hasIceCandidates: false,
error: 'RTCPeerConnection not available',
};
render(<WebRTCWarning detection={detection} />);
expect(screen.getByText(/Update your browser/i)).toBeInTheDocument();
expect(screen.getByText(/Try using Chrome, Firefox, or Edge/i)).toBeInTheDocument();
});
it('should show fix suggestions for blocked WebRTC', () => {
const detection = {
supported: true,
hasIceCandidates: false,
error: 'ICE candidates not generated',
};
render(<WebRTCWarning detection={detection} />);
expect(screen.getByText(/privacy settings/i)).toBeInTheDocument();
expect(screen.getByText(/VPN extensions/i)).toBeInTheDocument();
expect(screen.getByText(/incognito/i)).toBeInTheDocument();
});
it('should show fallback option message', () => {
const detection = {
supported: true,
hasIceCandidates: false,
error: 'ICE candidates not generated',
};
render(<WebRTCWarning detection={detection} />);
expect(screen.getByText(/You can still send video links/i)).toBeInTheDocument();
expect(screen.getByText(/Google Drive, Dropbox/i)).toBeInTheDocument();
});
});
describe('Dismiss Functionality', () => {
it('should call onDismiss callback when dismiss button clicked', () => {
const detection = {
supported: true,
hasIceCandidates: false,
error: 'blocked',
};
const mockOnDismiss = jest.fn();
render(<WebRTCWarning detection={detection} onDismiss={mockOnDismiss} />);
const dismissButton = screen.getByLabelText('Dismiss warning');
fireEvent.click(dismissButton);
expect(mockOnDismiss).toHaveBeenCalledTimes(1);
});
it('should hide warning after dismissal', () => {
const detection = {
supported: true,
hasIceCandidates: false,
error: 'blocked',
};
const { container } = render(<WebRTCWarning detection={detection} />);
// Should be visible initially
expect(screen.getByText('WebRTC Blocked')).toBeInTheDocument();
// Click dismiss
const dismissButton = screen.getByLabelText('Dismiss warning');
fireEvent.click(dismissButton);
// Should be hidden
expect(container.firstChild).toBeNull();
});
it('should work without onDismiss callback', () => {
const detection = {
supported: true,
hasIceCandidates: false,
error: 'blocked',
};
render(<WebRTCWarning detection={detection} />);
const dismissButton = screen.getByLabelText('Dismiss warning');
// Should not throw error
expect(() => fireEvent.click(dismissButton)).not.toThrow();
});
});
describe('Different Error Messages', () => {
it('should show generic error when error is unknown', () => {
const detection = {
supported: true,
hasIceCandidates: false,
error: 'Some unknown error',
};
render(<WebRTCWarning detection={detection} />);
expect(screen.getByText('WebRTC Error')).toBeInTheDocument();
expect(screen.getByText(/Some unknown error/i)).toBeInTheDocument();
});
it('should handle null error gracefully', () => {
const detection = {
supported: true,
hasIceCandidates: false,
error: null,
};
render(<WebRTCWarning detection={detection} />);
// Should still show warning for blocked ICE candidates
expect(screen.getByText('WebRTC Blocked')).toBeInTheDocument();
});
});
describe('Accessibility', () => {
it('should have accessible dismiss button', () => {
const detection = {
supported: true,
hasIceCandidates: false,
error: 'blocked',
};
render(<WebRTCWarning detection={detection} />);
const dismissButton = screen.getByLabelText('Dismiss warning');
expect(dismissButton).toBeInTheDocument();
expect(dismissButton).toHaveAttribute('aria-label', 'Dismiss warning');
});
it('should have proper heading structure', () => {
const detection = {
supported: true,
hasIceCandidates: false,
error: 'blocked',
};
render(<WebRTCWarning detection={detection} />);
const heading = screen.getByRole('heading', { level: 3 });
expect(heading).toHaveTextContent('WebRTC Blocked');
});
});
});

View File

@@ -0,0 +1,210 @@
import { detectWebRTCSupport, getWebRTCErrorMessage, getWebRTCFixSuggestions } from '../webrtcDetection';
describe('WebRTC Detection', () => {
let mockRTCPeerConnection;
let mockCreateDataChannel;
let mockCreateOffer;
let mockSetLocalDescription;
let mockOnIceCandidate;
let mockClose;
beforeEach(() => {
// Mock RTCPeerConnection
mockCreateDataChannel = jest.fn();
mockCreateOffer = jest.fn();
mockSetLocalDescription = jest.fn();
mockClose = jest.fn();
mockRTCPeerConnection = jest.fn(function() {
this.createDataChannel = mockCreateDataChannel;
this.createOffer = mockCreateOffer;
this.setLocalDescription = mockSetLocalDescription;
this.close = mockClose;
this.onicecandidate = null;
});
global.RTCPeerConnection = mockRTCPeerConnection;
});
afterEach(() => {
jest.clearAllMocks();
delete global.RTCPeerConnection;
});
describe('detectWebRTCSupport', () => {
it('should detect when RTCPeerConnection is not available', async () => {
delete global.RTCPeerConnection;
const result = await detectWebRTCSupport();
expect(result.supported).toBe(false);
expect(result.hasIceCandidates).toBe(false);
expect(result.error).toContain('not available');
});
it('should detect when ICE candidates are generated successfully', async () => {
// Mock successful ICE candidate generation
mockCreateOffer.mockResolvedValue({ type: 'offer', sdp: 'mock-sdp' });
mockSetLocalDescription.mockResolvedValue();
const promise = detectWebRTCSupport();
// Simulate ICE candidate event after a short delay
setTimeout(() => {
const pc = mockRTCPeerConnection.mock.results[0].value;
if (pc.onicecandidate) {
// Simulate candidate
pc.onicecandidate({ candidate: { type: 'host', candidate: 'mock-candidate' } });
// Simulate gathering complete
pc.onicecandidate({ candidate: null });
}
}, 50);
const result = await promise;
expect(result.supported).toBe(true);
expect(result.hasIceCandidates).toBe(true);
expect(result.error).toBeNull();
expect(mockCreateDataChannel).toHaveBeenCalledWith('test');
expect(mockCreateOffer).toHaveBeenCalled();
expect(mockSetLocalDescription).toHaveBeenCalled();
expect(mockClose).toHaveBeenCalled();
});
it('should detect when ICE candidates are NOT generated (blocked)', async () => {
mockCreateOffer.mockResolvedValue({ type: 'offer', sdp: 'mock-sdp' });
mockSetLocalDescription.mockResolvedValue();
const promise = detectWebRTCSupport();
// Simulate only gathering complete, no actual candidates
setTimeout(() => {
const pc = mockRTCPeerConnection.mock.results[0].value;
if (pc.onicecandidate) {
pc.onicecandidate({ candidate: null }); // Only null = gathering complete
}
}, 50);
const result = await promise;
expect(result.supported).toBe(true);
expect(result.hasIceCandidates).toBe(false);
expect(result.error).toContain('ICE candidates not generated');
});
it('should handle timeout when no ICE events occur', async () => {
mockCreateOffer.mockResolvedValue({ type: 'offer', sdp: 'mock-sdp' });
mockSetLocalDescription.mockResolvedValue();
// Don't trigger any ICE events - let it timeout
const result = await detectWebRTCSupport();
expect(result.supported).toBe(true);
expect(result.hasIceCandidates).toBe(false);
expect(mockClose).toHaveBeenCalled();
});
it('should handle errors during detection', async () => {
mockCreateOffer.mockRejectedValue(new Error('Mock error'));
const result = await detectWebRTCSupport();
expect(result.supported).toBe(true);
expect(result.hasIceCandidates).toBe(false);
expect(result.error).toContain('Mock error');
});
});
describe('getWebRTCErrorMessage', () => {
it('should return correct message when not supported', () => {
const detection = {
supported: false,
hasIceCandidates: false,
error: 'Not available',
};
const message = getWebRTCErrorMessage(detection);
expect(message).toContain('does not support WebRTC');
expect(message).toContain('modern browser');
});
it('should return correct message when ICE candidates blocked', () => {
const detection = {
supported: true,
hasIceCandidates: false,
error: 'ICE candidates not generated',
};
const message = getWebRTCErrorMessage(detection);
expect(message).toContain('WebRTC is blocked');
expect(message).toContain('browser settings');
});
it('should return success message when working correctly', () => {
const detection = {
supported: true,
hasIceCandidates: true,
error: null,
};
const message = getWebRTCErrorMessage(detection);
expect(message).toContain('working correctly');
});
it('should return generic error message when there is an unknown error', () => {
const detection = {
supported: true,
hasIceCandidates: false,
error: 'Unknown error occurred',
};
const message = getWebRTCErrorMessage(detection);
expect(message).toContain('Unknown error occurred');
});
});
describe('getWebRTCFixSuggestions', () => {
it('should return browser update suggestions when not supported', () => {
const detection = {
supported: false,
hasIceCandidates: false,
error: null,
};
const suggestions = getWebRTCFixSuggestions(detection);
expect(suggestions).toContain('Update your browser to the latest version');
expect(suggestions).toContain('Try using Chrome, Firefox, or Edge');
});
it('should return privacy settings suggestions when ICE candidates blocked', () => {
const detection = {
supported: true,
hasIceCandidates: false,
error: null,
};
const suggestions = getWebRTCFixSuggestions(detection);
expect(suggestions.some(s => s.includes('privacy settings'))).toBe(true);
expect(suggestions.some(s => s.includes('VPN'))).toBe(true);
expect(suggestions.some(s => s.includes('incognito'))).toBe(true);
});
it('should return empty array when working correctly', () => {
const detection = {
supported: true,
hasIceCandidates: true,
error: null,
};
const suggestions = getWebRTCFixSuggestions(detection);
expect(suggestions).toEqual([]);
});
});
});