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:
384
backend/src/__tests__/socket-webrtc.test.js
Normal file
384
backend/src/__tests__/socket-webrtc.test.js
Normal file
@@ -0,0 +1,384 @@
|
||||
const { initializeSocket } = require('../socket');
|
||||
const { createServer } = require('http');
|
||||
const { Server } = require('socket.io');
|
||||
const Client = require('socket.io-client');
|
||||
const { generateToken } = require('../utils/auth');
|
||||
const { prisma } = require('../utils/db');
|
||||
|
||||
describe('Socket.IO WebRTC Signaling', () => {
|
||||
let httpServer;
|
||||
let io;
|
||||
let clientSocket1;
|
||||
let clientSocket2;
|
||||
let token1;
|
||||
let token2;
|
||||
let testUser1;
|
||||
let testUser2;
|
||||
let testEvent;
|
||||
let testMatch;
|
||||
|
||||
beforeAll(async () => {
|
||||
// Create test users
|
||||
testUser1 = await prisma.user.create({
|
||||
data: {
|
||||
username: 'webrtc_user1',
|
||||
email: 'webrtc1@test.com',
|
||||
passwordHash: 'hash',
|
||||
emailVerified: true,
|
||||
},
|
||||
});
|
||||
|
||||
testUser2 = await prisma.user.create({
|
||||
data: {
|
||||
username: 'webrtc_user2',
|
||||
email: 'webrtc2@test.com',
|
||||
passwordHash: 'hash',
|
||||
emailVerified: true,
|
||||
},
|
||||
});
|
||||
|
||||
// Create test event
|
||||
testEvent = await prisma.event.create({
|
||||
data: {
|
||||
name: 'WebRTC Test Event',
|
||||
location: 'Test Location',
|
||||
startDate: new Date('2025-01-01'),
|
||||
endDate: new Date('2025-01-02'),
|
||||
},
|
||||
});
|
||||
|
||||
// Create chat room for the match
|
||||
const chatRoom = await prisma.chatRoom.create({
|
||||
data: {
|
||||
type: 'private',
|
||||
},
|
||||
});
|
||||
|
||||
// Create test match
|
||||
testMatch = await prisma.match.create({
|
||||
data: {
|
||||
user1Id: testUser1.id,
|
||||
user2Id: testUser2.id,
|
||||
eventId: testEvent.id,
|
||||
roomId: chatRoom.id,
|
||||
status: 'accepted',
|
||||
},
|
||||
});
|
||||
|
||||
// Generate tokens
|
||||
token1 = generateToken({ userId: testUser1.id });
|
||||
token2 = generateToken({ userId: testUser2.id });
|
||||
|
||||
// Create HTTP server and Socket.IO
|
||||
httpServer = createServer();
|
||||
io = initializeSocket(httpServer);
|
||||
|
||||
await new Promise((resolve) => {
|
||||
httpServer.listen(() => resolve());
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await prisma.match.deleteMany({
|
||||
where: {
|
||||
OR: [
|
||||
{ user1Id: testUser1.id },
|
||||
{ user2Id: testUser2.id },
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
await prisma.user.deleteMany({
|
||||
where: {
|
||||
id: {
|
||||
in: [testUser1.id, testUser2.id],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await prisma.event.deleteMany({
|
||||
where: {
|
||||
id: testEvent.id,
|
||||
},
|
||||
});
|
||||
|
||||
io.close();
|
||||
httpServer.close();
|
||||
});
|
||||
|
||||
beforeEach((done) => {
|
||||
const port = httpServer.address().port;
|
||||
|
||||
// Connect first client
|
||||
clientSocket1 = Client(`http://localhost:${port}`, {
|
||||
auth: { token: token1 },
|
||||
transports: ['websocket'],
|
||||
});
|
||||
|
||||
clientSocket1.on('connect', () => {
|
||||
// Connect second client
|
||||
clientSocket2 = Client(`http://localhost:${port}`, {
|
||||
auth: { token: token2 },
|
||||
transports: ['websocket'],
|
||||
});
|
||||
|
||||
clientSocket2.on('connect', () => {
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (clientSocket1?.connected) clientSocket1.disconnect();
|
||||
if (clientSocket2?.connected) clientSocket2.disconnect();
|
||||
});
|
||||
|
||||
describe('WebRTC Offer', () => {
|
||||
it('should relay WebRTC offer to the other user in match room', (done) => {
|
||||
const offerData = {
|
||||
type: 'offer',
|
||||
sdp: 'mock-sdp-offer-data',
|
||||
};
|
||||
|
||||
// User 1 and User 2 join match room
|
||||
clientSocket1.emit('join_match_room', { matchId: testMatch.id });
|
||||
clientSocket2.emit('join_match_room', { matchId: testMatch.id });
|
||||
|
||||
// User 2 listens for offer
|
||||
clientSocket2.on('webrtc_offer', (data) => {
|
||||
expect(data.from).toBe(testUser1.id);
|
||||
expect(data.offer).toEqual(offerData);
|
||||
done();
|
||||
});
|
||||
|
||||
// User 1 sends offer
|
||||
setTimeout(() => {
|
||||
clientSocket1.emit('webrtc_offer', {
|
||||
matchId: testMatch.id,
|
||||
offer: offerData,
|
||||
});
|
||||
}, 100);
|
||||
});
|
||||
|
||||
it('should not relay offer to self', (done) => {
|
||||
const offerData = {
|
||||
type: 'offer',
|
||||
sdp: 'mock-sdp-offer-data',
|
||||
};
|
||||
|
||||
clientSocket1.emit('join_match_room', { matchId: testMatch.id });
|
||||
|
||||
let receivedOffer = false;
|
||||
clientSocket1.on('webrtc_offer', () => {
|
||||
receivedOffer = true;
|
||||
});
|
||||
|
||||
clientSocket1.emit('webrtc_offer', {
|
||||
matchId: testMatch.id,
|
||||
offer: offerData,
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
expect(receivedOffer).toBe(false);
|
||||
done();
|
||||
}, 200);
|
||||
});
|
||||
|
||||
it('should require authorization for the match', async () => {
|
||||
// Create a chat room for the unauthorized match
|
||||
const unauthorizedRoom = await prisma.chatRoom.create({
|
||||
data: {
|
||||
type: 'private',
|
||||
},
|
||||
});
|
||||
|
||||
// Create a match that user1 is NOT part of
|
||||
const unauthorizedMatch = await prisma.match.create({
|
||||
data: {
|
||||
user1Id: testUser2.id,
|
||||
user2Id: testUser2.id, // Both same user
|
||||
eventId: testEvent.id,
|
||||
roomId: unauthorizedRoom.id,
|
||||
status: 'accepted',
|
||||
},
|
||||
});
|
||||
|
||||
// Wait for error event
|
||||
const errorPromise = new Promise((resolve) => {
|
||||
clientSocket1.on('error', (error) => {
|
||||
resolve(error);
|
||||
});
|
||||
});
|
||||
|
||||
clientSocket1.emit('webrtc_offer', {
|
||||
matchId: unauthorizedMatch.id,
|
||||
offer: { type: 'offer', sdp: 'test' },
|
||||
});
|
||||
|
||||
const error = await errorPromise;
|
||||
expect(error.message).toContain('authorized');
|
||||
|
||||
// Cleanup
|
||||
await prisma.match.delete({ where: { id: unauthorizedMatch.id } });
|
||||
});
|
||||
});
|
||||
|
||||
describe('WebRTC Answer', () => {
|
||||
it('should relay WebRTC answer to the other user in match room', (done) => {
|
||||
const answerData = {
|
||||
type: 'answer',
|
||||
sdp: 'mock-sdp-answer-data',
|
||||
};
|
||||
|
||||
clientSocket1.emit('join_match_room', { matchId: testMatch.id });
|
||||
clientSocket2.emit('join_match_room', { matchId: testMatch.id });
|
||||
|
||||
// User 1 listens for answer
|
||||
clientSocket1.on('webrtc_answer', (data) => {
|
||||
expect(data.from).toBe(testUser2.id);
|
||||
expect(data.answer).toEqual(answerData);
|
||||
done();
|
||||
});
|
||||
|
||||
// User 2 sends answer
|
||||
setTimeout(() => {
|
||||
clientSocket2.emit('webrtc_answer', {
|
||||
matchId: testMatch.id,
|
||||
answer: answerData,
|
||||
});
|
||||
}, 100);
|
||||
});
|
||||
});
|
||||
|
||||
describe('WebRTC ICE Candidate', () => {
|
||||
it('should relay ICE candidate to the other user in match room', (done) => {
|
||||
const candidateData = {
|
||||
candidate: 'mock-ice-candidate',
|
||||
sdpMid: 'data',
|
||||
sdpMLineIndex: 0,
|
||||
};
|
||||
|
||||
clientSocket1.emit('join_match_room', { matchId: testMatch.id });
|
||||
clientSocket2.emit('join_match_room', { matchId: testMatch.id });
|
||||
|
||||
// User 2 listens for ICE candidate
|
||||
clientSocket2.on('webrtc_ice_candidate', (data) => {
|
||||
expect(data.from).toBe(testUser1.id);
|
||||
expect(data.candidate).toEqual(candidateData);
|
||||
done();
|
||||
});
|
||||
|
||||
// User 1 sends ICE candidate
|
||||
setTimeout(() => {
|
||||
clientSocket1.emit('webrtc_ice_candidate', {
|
||||
matchId: testMatch.id,
|
||||
candidate: candidateData,
|
||||
});
|
||||
}, 100);
|
||||
});
|
||||
|
||||
it('should not relay ICE candidate to self', (done) => {
|
||||
const candidateData = {
|
||||
candidate: 'mock-ice-candidate',
|
||||
sdpMid: 'data',
|
||||
sdpMLineIndex: 0,
|
||||
};
|
||||
|
||||
clientSocket1.emit('join_match_room', { matchId: testMatch.id });
|
||||
|
||||
let receivedCandidate = false;
|
||||
clientSocket1.on('webrtc_ice_candidate', () => {
|
||||
receivedCandidate = true;
|
||||
});
|
||||
|
||||
clientSocket1.emit('webrtc_ice_candidate', {
|
||||
matchId: testMatch.id,
|
||||
candidate: candidateData,
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
expect(receivedCandidate).toBe(false);
|
||||
done();
|
||||
}, 200);
|
||||
});
|
||||
});
|
||||
|
||||
describe('WebRTC Full Flow', () => {
|
||||
it('should successfully exchange offer, answer, and ICE candidates', (done) => {
|
||||
const offerData = { type: 'offer', sdp: 'offer-sdp' };
|
||||
const answerData = { type: 'answer', sdp: 'answer-sdp' };
|
||||
const candidateData = { candidate: 'ice-candidate' };
|
||||
|
||||
let user1ReceivedAnswer = false;
|
||||
let user1ReceivedCandidate = false;
|
||||
let user2ReceivedOffer = false;
|
||||
let user2ReceivedCandidate = false;
|
||||
|
||||
clientSocket1.emit('join_match_room', { matchId: testMatch.id });
|
||||
clientSocket2.emit('join_match_room', { matchId: testMatch.id });
|
||||
|
||||
// User 1 listeners
|
||||
clientSocket1.on('webrtc_answer', (data) => {
|
||||
expect(data.from).toBe(testUser2.id);
|
||||
user1ReceivedAnswer = true;
|
||||
checkCompletion();
|
||||
});
|
||||
|
||||
clientSocket1.on('webrtc_ice_candidate', (data) => {
|
||||
expect(data.from).toBe(testUser2.id);
|
||||
user1ReceivedCandidate = true;
|
||||
checkCompletion();
|
||||
});
|
||||
|
||||
// User 2 listeners
|
||||
clientSocket2.on('webrtc_offer', (data) => {
|
||||
expect(data.from).toBe(testUser1.id);
|
||||
user2ReceivedOffer = true;
|
||||
|
||||
// User 2 sends answer
|
||||
clientSocket2.emit('webrtc_answer', {
|
||||
matchId: testMatch.id,
|
||||
answer: answerData,
|
||||
});
|
||||
|
||||
// User 2 sends ICE candidate
|
||||
clientSocket2.emit('webrtc_ice_candidate', {
|
||||
matchId: testMatch.id,
|
||||
candidate: candidateData,
|
||||
});
|
||||
|
||||
checkCompletion();
|
||||
});
|
||||
|
||||
clientSocket2.on('webrtc_ice_candidate', (data) => {
|
||||
expect(data.from).toBe(testUser1.id);
|
||||
user2ReceivedCandidate = true;
|
||||
checkCompletion();
|
||||
});
|
||||
|
||||
function checkCompletion() {
|
||||
if (
|
||||
user1ReceivedAnswer &&
|
||||
user1ReceivedCandidate &&
|
||||
user2ReceivedOffer &&
|
||||
user2ReceivedCandidate
|
||||
) {
|
||||
done();
|
||||
}
|
||||
}
|
||||
|
||||
// User 1 starts: send offer and ICE candidate
|
||||
setTimeout(() => {
|
||||
clientSocket1.emit('webrtc_offer', {
|
||||
matchId: testMatch.id,
|
||||
offer: offerData,
|
||||
});
|
||||
|
||||
clientSocket1.emit('webrtc_ice_candidate', {
|
||||
matchId: testMatch.id,
|
||||
candidate: candidateData,
|
||||
});
|
||||
}, 100);
|
||||
});
|
||||
});
|
||||
});
|
||||
195
frontend/src/components/__tests__/WebRTCWarning.test.jsx
Normal file
195
frontend/src/components/__tests__/WebRTCWarning.test.jsx
Normal 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');
|
||||
});
|
||||
});
|
||||
});
|
||||
210
frontend/src/utils/__tests__/webrtcDetection.test.js
Normal file
210
frontend/src/utils/__tests__/webrtcDetection.test.js
Normal 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([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user