fix(ssr): resolve EventSource SSR violations and implement best practices
- Fix EventSource is not defined error in queue dashboard - Add browser guards for all EventSource usage - Replace static constants (EventSource.OPEN/CLOSED) with numeric values - Fix setInterval SSR violation in LLM health indicator - Replace $effect anti-pattern with onMount in share page - Add comprehensive SvelteKit SSR best practices documentation - Add SSR audit and testing verification All changes follow SvelteKit best practices and are verified against official documentation. Production build succeeds with no SSR errors. Closes: FixEventSourceSSR See: docs/outcomes/FixEventSourceSSR.md
This commit is contained in:
141
src/tests/queue-sse.spec.ts
Normal file
141
src/tests/queue-sse.spec.ts
Normal file
@@ -0,0 +1,141 @@
|
||||
/**
|
||||
* Integration tests for Queue SSE Stream endpoint
|
||||
*
|
||||
* Tests the Server-Sent Events stream for real-time queue updates.
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import { queueManager } from '$lib/server/queue/QueueManager';
|
||||
import { GET as streamGET } from '../routes/api/queue/stream/+server.js';
|
||||
|
||||
describe('Queue SSE Stream Endpoint', () => {
|
||||
beforeEach(() => {
|
||||
// Clear queue between tests
|
||||
queueManager.getAll().forEach(item => queueManager.remove(item.id));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// Clean up after tests
|
||||
queueManager.getAll().forEach(item => queueManager.remove(item.id));
|
||||
});
|
||||
|
||||
describe('GET /api/queue/stream', () => {
|
||||
it('should return SSE response with correct headers', async () => {
|
||||
const url = new URL('http://localhost/api/queue/stream');
|
||||
const request = new Request(url);
|
||||
|
||||
const response = await streamGET({
|
||||
url,
|
||||
request: {
|
||||
...request,
|
||||
signal: new AbortController().signal
|
||||
}
|
||||
} as any);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.headers.get('Content-Type')).toBe('text/event-stream');
|
||||
expect(response.headers.get('Cache-Control')).toBe('no-cache');
|
||||
expect(response.headers.get('Connection')).toBe('keep-alive');
|
||||
});
|
||||
|
||||
it('should reject invalid status filter', async () => {
|
||||
const url = new URL('http://localhost/api/queue/stream?status=invalid');
|
||||
const request = new Request(url);
|
||||
|
||||
const response = await streamGET({
|
||||
url,
|
||||
request: {
|
||||
...request,
|
||||
signal: new AbortController().signal
|
||||
}
|
||||
} as any);
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
const text = await response.text();
|
||||
expect(text).toContain('Invalid status filter');
|
||||
});
|
||||
|
||||
it('should reject invalid item ID format', async () => {
|
||||
const url = new URL('http://localhost/api/queue/stream?id=invalid-id');
|
||||
const request = new Request(url);
|
||||
|
||||
const response = await streamGET({
|
||||
url,
|
||||
request: {
|
||||
...request,
|
||||
signal: new AbortController().signal
|
||||
}
|
||||
} as any);
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
const text = await response.text();
|
||||
expect(text).toBe('Invalid queue item ID format');
|
||||
});
|
||||
|
||||
it('should accept valid status filter', async () => {
|
||||
const url = new URL('http://localhost/api/queue/stream?status=pending');
|
||||
const request = new Request(url);
|
||||
|
||||
const response = await streamGET({
|
||||
url,
|
||||
request: {
|
||||
...request,
|
||||
signal: new AbortController().signal
|
||||
}
|
||||
} as any);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.headers.get('Content-Type')).toBe('text/event-stream');
|
||||
});
|
||||
|
||||
it('should accept valid item ID filter', async () => {
|
||||
// Add a test item first
|
||||
const item = queueManager.enqueue('https://instagram.com/p/TEST123');
|
||||
|
||||
const url = new URL(`http://localhost/api/queue/stream?id=${item.id}`);
|
||||
const request = new Request(url);
|
||||
|
||||
const response = await streamGET({
|
||||
url,
|
||||
request: {
|
||||
...request,
|
||||
signal: new AbortController().signal
|
||||
}
|
||||
} as any);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.headers.get('Content-Type')).toBe('text/event-stream');
|
||||
});
|
||||
|
||||
it('should handle stream initialization without errors', async () => {
|
||||
// Add some test items
|
||||
queueManager.enqueue('https://instagram.com/p/TEST1');
|
||||
queueManager.enqueue('https://instagram.com/p/TEST2');
|
||||
|
||||
const url = new URL('http://localhost/api/queue/stream');
|
||||
const abortController = new AbortController();
|
||||
const request = new Request(url, {
|
||||
signal: abortController.signal
|
||||
});
|
||||
|
||||
const response = await streamGET({
|
||||
url,
|
||||
request
|
||||
} as any);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body).toBeInstanceOf(ReadableStream);
|
||||
|
||||
// Abort the request to clean up
|
||||
abortController.abort();
|
||||
});
|
||||
});
|
||||
|
||||
// Note: Full SSE stream testing would require more complex setup with
|
||||
// ReadableStream readers and async iteration, which is beyond the scope
|
||||
// of these basic endpoint validation tests. The above tests verify that:
|
||||
// 1. The endpoint responds correctly
|
||||
// 2. Headers are set properly for SSE
|
||||
// 3. Parameter validation works
|
||||
// 4. Stream initialization succeeds
|
||||
});
|
||||
Reference in New Issue
Block a user