Add comprehensive performance monitoring and testing infrastructure
Some checks failed
Lint and Build / build (20) (pull_request) Failing after 36s
Lint and Build / build (22) (pull_request) Failing after 50s

- Implement performance dashboard with real-time metrics tracking
- Add React hooks for smart polling, debouncing, and active source lookup
- Create Jest testing framework with comprehensive test suites for components, API endpoints, and utilities
- Enhance UI components with optimized rendering and memoization
- Improve polling efficiency with visibility detection and adaptive intervals

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Decobus 2025-07-19 06:20:19 -04:00
parent a66979fb34
commit c259f0d943
15 changed files with 6320 additions and 94 deletions

View file

@ -0,0 +1,76 @@
import { GET } from '../streams/route';
// Mock the database module
jest.mock('@/lib/database', () => ({
getDatabase: jest.fn(),
}));
describe('/api/streams', () => {
let mockDb: any;
beforeEach(() => {
// Create mock database
mockDb = {
all: jest.fn(),
};
const { getDatabase } = require('@/lib/database');
getDatabase.mockResolvedValue(mockDb);
});
describe('GET /api/streams', () => {
it('returns all streams successfully', async () => {
const mockStreams = [
{ id: 1, name: 'Stream 1', url: 'http://example.com/1', obs_source_name: 'Source 1', team_id: 1 },
{ id: 2, name: 'Stream 2', url: 'http://example.com/2', obs_source_name: 'Source 2', team_id: 2 },
];
mockDb.all.mockResolvedValue(mockStreams);
const response = await GET();
expect(mockDb.all).toHaveBeenCalledWith(
expect.stringContaining('SELECT * FROM')
);
const { NextResponse } = require('next/server');
expect(NextResponse.json).toHaveBeenCalledWith(mockStreams);
});
it('returns empty array when no streams exist', async () => {
mockDb.all.mockResolvedValue([]);
const response = await GET();
const { NextResponse } = require('next/server');
expect(NextResponse.json).toHaveBeenCalledWith([]);
});
it('handles database errors gracefully', async () => {
const dbError = new Error('Database connection failed');
mockDb.all.mockRejectedValue(dbError);
const response = await GET();
const { NextResponse } = require('next/server');
expect(NextResponse.json).toHaveBeenCalledWith(
{ error: 'Failed to fetch streams' },
{ status: 500 }
);
});
it('handles database connection errors', async () => {
const connectionError = new Error('Failed to connect to database');
const { getDatabase } = require('@/lib/database');
getDatabase.mockRejectedValue(connectionError);
const response = await GET();
const { NextResponse } = require('next/server');
expect(NextResponse.json).toHaveBeenCalledWith(
{ error: 'Failed to fetch streams' },
{ status: 500 }
);
});
});
});

View file

@ -0,0 +1,89 @@
import { GET } from '../teams/route';
// Mock the database module
jest.mock('@/lib/database', () => ({
getDatabase: jest.fn(),
}));
// Mock the apiHelpers module
jest.mock('@/lib/apiHelpers', () => ({
withErrorHandling: jest.fn((handler) => handler),
createSuccessResponse: jest.fn((data, status = 200) => ({
data,
status,
json: async () => ({ success: true, data }),
})),
createDatabaseError: jest.fn((operation, error) => ({
error: 'Database Error',
status: 500,
json: async () => ({
error: 'Database Error',
message: `Database operation failed: ${operation}`,
}),
})),
}));
describe('/api/teams', () => {
let mockDb: any;
beforeEach(() => {
// Create mock database
mockDb = {
all: jest.fn(),
};
const { getDatabase } = require('@/lib/database');
getDatabase.mockResolvedValue(mockDb);
});
describe('GET /api/teams', () => {
it('returns all teams successfully', async () => {
const mockTeams = [
{ team_id: 1, team_name: 'Team Alpha' },
{ team_id: 2, team_name: 'Team Beta' },
{ team_id: 3, team_name: 'Team Gamma' },
];
mockDb.all.mockResolvedValue(mockTeams);
const response = await GET();
expect(mockDb.all).toHaveBeenCalledWith(
expect.stringContaining('SELECT * FROM')
);
const { createSuccessResponse } = require('@/lib/apiHelpers');
expect(createSuccessResponse).toHaveBeenCalledWith(mockTeams);
});
it('returns empty array when no teams exist', async () => {
mockDb.all.mockResolvedValue([]);
const response = await GET();
const { createSuccessResponse } = require('@/lib/apiHelpers');
expect(createSuccessResponse).toHaveBeenCalledWith([]);
});
it('handles database errors gracefully', async () => {
const dbError = new Error('Table does not exist');
mockDb.all.mockRejectedValue(dbError);
const response = await GET();
const { createDatabaseError } = require('@/lib/apiHelpers');
expect(createDatabaseError).toHaveBeenCalledWith('fetch teams', dbError);
});
it('handles database connection errors', async () => {
const connectionError = new Error('Failed to connect to database');
const { getDatabase } = require('@/lib/database');
getDatabase.mockRejectedValue(connectionError);
const response = await GET();
const { createDatabaseError } = require('@/lib/apiHelpers');
expect(createDatabaseError).toHaveBeenCalledWith('fetch teams', connectionError);
});
});
});

View file

@ -2,6 +2,7 @@ import './globals.css';
import Header from '@/components/Header';
import Footer from '@/components/Footer';
import { ErrorBoundary } from '@/components/ErrorBoundary';
import PerformanceDashboard from '@/components/PerformanceDashboard';
export const metadata = {
title: 'OBS Source Switcher',
@ -19,6 +20,7 @@ export default function RootLayout({ children }: { children: React.ReactNode })
</ErrorBoundary>
</main>
<Footer />
<PerformanceDashboard />
</body>
</html>
);

View file

@ -1,10 +1,11 @@
'use client';
import { useState, useEffect } from 'react';
import { useState, useEffect, useMemo, useCallback } from 'react';
import Link from 'next/link';
import Dropdown from '@/components/Dropdown';
import { useToast } from '@/lib/useToast';
import { ToastContainer } from '@/components/Toast';
import { useActiveSourceLookup, useDebounce, PerformanceMonitor } from '@/lib/performance';
type Stream = {
id: number;
@ -30,50 +31,21 @@ export default function Home() {
const [openDropdown, setOpenDropdown] = useState<string | null>(null);
const { toasts, removeToast, showSuccess, showError } = useToast();
useEffect(() => {
const fetchData = async () => {
try {
// Fetch streams and active sources in parallel
const [streamsRes, activeRes] = await Promise.all([
fetch('/api/streams'),
fetch('/api/getActive')
]);
const [streamsData, activeData] = await Promise.all([
streamsRes.json(),
activeRes.json()
]);
setStreams(streamsData);
setActiveSources(activeData);
} catch (error) {
console.error('Error fetching data:', error);
showError('Failed to Load Data', 'Could not fetch streams. Please refresh the page.');
} finally {
setIsLoading(false);
}
};
// Memoized active source lookup for performance
const activeSourceIds = useActiveSourceLookup(streams, activeSources);
fetchData();
}, []);
const handleSetActive = async (screen: ScreenType, id: number | null) => {
const selectedStream = streams.find((stream) => stream.id === id);
// Update local state immediately
setActiveSources((prev) => ({
...prev,
[screen]: selectedStream?.obs_source_name || null,
}));
// Update backend
// Debounced API calls to prevent excessive requests
const debouncedSetActive = useDebounce(async (screen: ScreenType, id: number | null) => {
if (id) {
const selectedStream = streams.find(stream => stream.id === id);
try {
const endTimer = PerformanceMonitor.startTimer('setActive_api');
const response = await fetch('/api/setActive', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ screen, id }),
});
endTimer();
if (!response.ok) {
throw new Error('Failed to set active stream');
@ -90,11 +62,61 @@ export default function Home() {
}));
}
}
};
}, 300);
const handleToggleDropdown = (screen: string) => {
const fetchData = useCallback(async () => {
const endTimer = PerformanceMonitor.startTimer('fetchData');
try {
// Fetch streams and active sources in parallel
const [streamsRes, activeRes] = await Promise.all([
fetch('/api/streams'),
fetch('/api/getActive')
]);
const [streamsData, activeData] = await Promise.all([
streamsRes.json(),
activeRes.json()
]);
setStreams(streamsData);
setActiveSources(activeData);
} catch (error) {
console.error('Error fetching data:', error);
showError('Failed to Load Data', 'Could not fetch streams. Please refresh the page.');
} finally {
setIsLoading(false);
endTimer();
}
}, [showError]);
useEffect(() => {
fetchData();
}, [fetchData]);
const handleSetActive = useCallback(async (screen: ScreenType, id: number | null) => {
const selectedStream = streams.find((stream) => stream.id === id);
// Update local state immediately for optimistic updates
setActiveSources((prev) => ({
...prev,
[screen]: selectedStream?.obs_source_name || null,
}));
// Debounced backend update
debouncedSetActive(screen, id);
}, [streams, debouncedSetActive]);
const handleToggleDropdown = useCallback((screen: string) => {
setOpenDropdown((prev) => (prev === screen ? null : screen));
};
}, []);
// Memoized corner displays to prevent re-renders
const cornerDisplays = useMemo(() => [
{ screen: 'topLeft' as const, label: 'Top Left' },
{ screen: 'topRight' as const, label: 'Top Right' },
{ screen: 'bottomLeft' as const, label: 'Bottom Left' },
{ screen: 'bottomRight' as const, label: 'Bottom Right' },
], []);
if (isLoading) {
return (
@ -123,9 +145,7 @@ export default function Home() {
<div className="max-w-md mx-auto">
<Dropdown
options={streams}
activeId={
streams.find((stream) => stream.obs_source_name === activeSources.large)?.id || null
}
activeId={activeSourceIds.large}
onSelect={(id) => handleSetActive('large', id)}
label="Select Primary Stream..."
isOpen={openDropdown === 'large'}
@ -142,9 +162,7 @@ export default function Home() {
<h3 className="text-lg font-semibold mb-4 text-center">Left Display</h3>
<Dropdown
options={streams}
activeId={
streams.find((stream) => stream.obs_source_name === activeSources.left)?.id || null
}
activeId={activeSourceIds.left}
onSelect={(id) => handleSetActive('left', id)}
label="Select Left Stream..."
isOpen={openDropdown === 'left'}
@ -155,9 +173,7 @@ export default function Home() {
<h3 className="text-lg font-semibold mb-4 text-center">Right Display</h3>
<Dropdown
options={streams}
activeId={
streams.find((stream) => stream.obs_source_name === activeSources.right)?.id || null
}
activeId={activeSourceIds.right}
onSelect={(id) => handleSetActive('right', id)}
label="Select Right Stream..."
isOpen={openDropdown === 'right'}
@ -171,19 +187,12 @@ export default function Home() {
<div className="glass p-6">
<h2 className="card-title">Corner Displays</h2>
<div className="grid-4">
{[
{ screen: 'topLeft' as const, label: 'Top Left' },
{ screen: 'topRight' as const, label: 'Top Right' },
{ screen: 'bottomLeft' as const, label: 'Bottom Left' },
{ screen: 'bottomRight' as const, label: 'Bottom Right' },
].map(({ screen, label }) => (
{cornerDisplays.map(({ screen, label }) => (
<div key={screen}>
<h3 className="text-md font-semibold mb-3 text-center">{label}</h3>
<Dropdown
options={streams}
activeId={
streams.find((stream) => stream.obs_source_name === activeSources[screen])?.id || null
}
activeId={activeSourceIds[screen]}
onSelect={(id) => handleSetActive(screen, id)}
label="Select Stream..."
isOpen={openDropdown === screen}