UNPKG

lightweight-browser-load-tester

Version:

A lightweight load testing tool using real browsers for streaming applications with DRM support

459 lines (372 loc) 15.3 kB
import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest'; import { ResultsAggregator, ReportConfig } from './results-aggregator'; import { BrowserMetrics, DRMMetrics, NetworkMetrics, ErrorLog, PrometheusConfiguration } from '../types/index'; import fs from 'fs/promises'; import path from 'path'; // Mock fs module vi.mock('fs/promises'); vi.mock('path'); // Mock PrometheusExporter vi.mock('../exporters/prometheus-exporter', () => ({ PrometheusExporter: vi.fn().mockImplementation(() => ({ exportBrowserMetrics: vi.fn().mockResolvedValue(undefined), exportDRMMetrics: vi.fn().mockResolvedValue(undefined), exportNetworkMetrics: vi.fn().mockResolvedValue(undefined), exportErrorMetrics: vi.fn().mockResolvedValue(undefined), exportTestSummary: vi.fn().mockResolvedValue(undefined), shutdown: vi.fn().mockResolvedValue(undefined) })) })); describe('ResultsAggregator', () => { let aggregator: ResultsAggregator; let mockBrowserMetrics: BrowserMetrics; let mockDRMMetrics: DRMMetrics; let mockNetworkMetrics: NetworkMetrics[]; let mockErrorLog: ErrorLog; beforeEach(() => { aggregator = new ResultsAggregator(); mockBrowserMetrics = { instanceId: 'browser-1', memoryUsage: 150.5, cpuUsage: 25.3, requestCount: 100, errorCount: 2, uptime: 300 }; mockDRMMetrics = { licenseRequestCount: 50, averageLicenseTime: 250.5, licenseSuccessRate: 0.96, drmType: 'widevine', errors: [] }; mockNetworkMetrics = [ { url: 'https://example.com/api/test', method: 'GET', responseTime: 150, statusCode: 200, timestamp: new Date('2024-01-01T10:00:00Z'), requestSize: 1024, responseSize: 2048, isStreamingRelated: true, streamingType: 'manifest' }, { url: 'https://example.com/api/fail', method: 'POST', responseTime: 500, statusCode: 500, timestamp: new Date('2024-01-01T10:01:00Z'), requestSize: 512, responseSize: 256, isStreamingRelated: false } ]; mockErrorLog = { timestamp: new Date('2024-01-01T10:00:30Z'), level: 'error', message: 'Test error message', stack: 'Error stack trace', context: { userId: 'test-user' } }; // Reset mocks vi.clearAllMocks(); }); afterEach(() => { vi.restoreAllMocks(); }); describe('constructor', () => { it('should initialize with empty test results', () => { const results = aggregator.getResults(); expect(results.summary.totalRequests).toBe(0); expect(results.summary.successfulRequests).toBe(0); expect(results.summary.failedRequests).toBe(0); expect(results.browserMetrics).toHaveLength(0); expect(results.drmMetrics).toHaveLength(0); expect(results.networkMetrics).toHaveLength(0); expect(results.errors).toHaveLength(0); }); it('should initialize with Prometheus exporter when config provided', () => { const prometheusConfig: PrometheusConfiguration = { enabled: true, remoteWriteUrl: 'https://prometheus.example.com/api/v1/write' }; const aggregatorWithPrometheus = new ResultsAggregator(prometheusConfig); // Should not throw and should initialize properly expect(aggregatorWithPrometheus).toBeDefined(); }); it('should initialize without Prometheus exporter when config not provided', () => { const aggregatorWithoutPrometheus = new ResultsAggregator(); expect(aggregatorWithoutPrometheus).toBeDefined(); }); }); describe('addBrowserMetrics', () => { it('should add browser metrics to the results', () => { aggregator.addBrowserMetrics(mockBrowserMetrics); const results = aggregator.getResults(); expect(results.browserMetrics).toHaveLength(1); expect(results.browserMetrics[0]).toEqual(mockBrowserMetrics); }); it('should add multiple browser metrics', () => { const secondMetrics = { ...mockBrowserMetrics, instanceId: 'browser-2' }; aggregator.addBrowserMetrics(mockBrowserMetrics); aggregator.addBrowserMetrics(secondMetrics); const results = aggregator.getResults(); expect(results.browserMetrics).toHaveLength(2); expect(results.browserMetrics[0].instanceId).toBe('browser-1'); expect(results.browserMetrics[1].instanceId).toBe('browser-2'); }); }); describe('addDRMMetrics', () => { it('should add DRM metrics to the results', () => { aggregator.addDRMMetrics(mockDRMMetrics); const results = aggregator.getResults(); expect(results.drmMetrics).toHaveLength(1); expect(results.drmMetrics[0]).toEqual(mockDRMMetrics); }); }); describe('addNetworkMetrics', () => { it('should add network metrics to the results', () => { aggregator.addNetworkMetrics(mockNetworkMetrics); const results = aggregator.getResults(); expect(results.networkMetrics).toHaveLength(2); expect(results.networkMetrics).toEqual(mockNetworkMetrics); }); it('should handle empty network metrics array', () => { aggregator.addNetworkMetrics([]); const results = aggregator.getResults(); expect(results.networkMetrics).toHaveLength(0); }); }); describe('addError', () => { it('should add error log to the results', () => { aggregator.addError(mockErrorLog); const results = aggregator.getResults(); expect(results.errors).toHaveLength(1); expect(results.errors[0]).toEqual(mockErrorLog); }); }); describe('finalize', () => { beforeEach(() => { // Add test data aggregator.addBrowserMetrics(mockBrowserMetrics); aggregator.addNetworkMetrics(mockNetworkMetrics); aggregator.addDRMMetrics(mockDRMMetrics); aggregator.addError(mockErrorLog); }); it('should calculate summary statistics correctly', () => { // Add a small delay to ensure test duration is measurable const startTime = Date.now(); while (Date.now() - startTime < 10) { // Small delay } aggregator.finalize(); const results = aggregator.getResults(); const summary = results.summary; expect(summary.totalRequests).toBe(2); expect(summary.successfulRequests).toBe(1); // Only status 200 expect(summary.failedRequests).toBe(1); // Status 500 expect(summary.averageResponseTime).toBe(325); // (150 + 500) / 2 expect(summary.peakConcurrentUsers).toBe(1); // One browser instance expect(summary.testDuration).toBeGreaterThanOrEqual(0); }); it('should handle empty network metrics when calculating summary', () => { const emptyAggregator = new ResultsAggregator(); emptyAggregator.finalize(); const results = emptyAggregator.getResults(); const summary = results.summary; expect(summary.totalRequests).toBe(0); expect(summary.successfulRequests).toBe(0); expect(summary.failedRequests).toBe(0); expect(summary.averageResponseTime).toBe(0); }); }); describe('generateReports', () => { const mockConfig: ReportConfig = { outputPath: './test-output', formats: ['json', 'html', 'csv'], includeDetailedMetrics: true, includeErrorAnalysis: true }; beforeEach(() => { // Setup mocks vi.mocked(fs.mkdir).mockResolvedValue(undefined); vi.mocked(fs.writeFile).mockResolvedValue(undefined); vi.mocked(path.join).mockImplementation((...args) => args.join('/')); // Add test data aggregator.addBrowserMetrics(mockBrowserMetrics); aggregator.addNetworkMetrics(mockNetworkMetrics); aggregator.addDRMMetrics(mockDRMMetrics); aggregator.addError(mockErrorLog); }); it('should generate JSON report', async () => { const config: ReportConfig = { ...mockConfig, formats: ['json'] }; await aggregator.generateReports(config); expect(fs.mkdir).toHaveBeenCalledWith('./test-output', { recursive: true }); expect(fs.writeFile).toHaveBeenCalledWith( expect.stringContaining('load-test-results-'), expect.stringContaining('"totalRequests"') ); }); it('should generate HTML report', async () => { const config: ReportConfig = { ...mockConfig, formats: ['html'] }; await aggregator.generateReports(config); expect(fs.mkdir).toHaveBeenCalledWith('./test-output', { recursive: true }); expect(fs.writeFile).toHaveBeenCalledWith( expect.stringContaining('load-test-results-'), expect.stringContaining('<!DOCTYPE html>') ); }); it('should generate CSV reports', async () => { const config: ReportConfig = { ...mockConfig, formats: ['csv'] }; await aggregator.generateReports(config); expect(fs.mkdir).toHaveBeenCalledWith('./test-output', { recursive: true }); expect(fs.writeFile).toHaveBeenCalledTimes(3); // summary, network, browser CSVs // Check that CSV files are created const writeFileCalls = vi.mocked(fs.writeFile).mock.calls; expect(writeFileCalls.some(call => call[0].includes('summary'))).toBe(true); expect(writeFileCalls.some(call => call[0].includes('network'))).toBe(true); expect(writeFileCalls.some(call => call[0].includes('browser'))).toBe(true); }); it('should generate multiple report formats', async () => { await aggregator.generateReports(mockConfig); expect(fs.writeFile).toHaveBeenCalledTimes(5); // 1 JSON + 1 HTML + 3 CSV files }); it('should use default output path when not specified', async () => { const config: ReportConfig = { formats: ['json'] }; await aggregator.generateReports(config); expect(fs.mkdir).toHaveBeenCalledWith('./test-results', { recursive: true }); }); it('should finalize results if not already done', async () => { const config: ReportConfig = { formats: ['json'] }; // Don't call finalize manually await aggregator.generateReports(config); const results = aggregator.getResults(); expect(results.summary.totalRequests).toBe(2); // Should be calculated }); }); describe('HTML report generation', () => { beforeEach(() => { aggregator.addBrowserMetrics(mockBrowserMetrics); aggregator.addNetworkMetrics(mockNetworkMetrics); aggregator.addDRMMetrics(mockDRMMetrics); aggregator.addError(mockErrorLog); aggregator.finalize(); }); it('should include summary metrics in HTML', async () => { const config: ReportConfig = { formats: ['html'], includeDetailedMetrics: false, includeErrorAnalysis: false }; vi.mocked(fs.mkdir).mockResolvedValue(undefined); vi.mocked(fs.writeFile).mockResolvedValue(undefined); vi.mocked(path.join).mockImplementation((...args) => args.join('/')); await aggregator.generateReports(config); const htmlContent = vi.mocked(fs.writeFile).mock.calls[0][1] as string; expect(htmlContent).toContain('Total Requests'); expect(htmlContent).toContain('2'); // Total requests expect(htmlContent).toContain('50.0%'); // Success rate }); it('should include detailed metrics when requested', async () => { const config: ReportConfig = { formats: ['html'], includeDetailedMetrics: true, includeErrorAnalysis: false }; vi.mocked(fs.mkdir).mockResolvedValue(undefined); vi.mocked(fs.writeFile).mockResolvedValue(undefined); vi.mocked(path.join).mockImplementation((...args) => args.join('/')); await aggregator.generateReports(config); const htmlContent = vi.mocked(fs.writeFile).mock.calls[0][1] as string; expect(htmlContent).toContain('Browser Instance Metrics'); expect(htmlContent).toContain('browser-1'); }); it('should include error analysis when requested', async () => { const config: ReportConfig = { formats: ['html'], includeDetailedMetrics: false, includeErrorAnalysis: true }; vi.mocked(fs.mkdir).mockResolvedValue(undefined); vi.mocked(fs.writeFile).mockResolvedValue(undefined); vi.mocked(path.join).mockImplementation((...args) => args.join('/')); await aggregator.generateReports(config); const htmlContent = vi.mocked(fs.writeFile).mock.calls[0][1] as string; expect(htmlContent).toContain('Error Analysis'); expect(htmlContent).toContain('Test error message'); }); }); describe('CSV report generation', () => { beforeEach(() => { aggregator.addBrowserMetrics(mockBrowserMetrics); aggregator.addNetworkMetrics(mockNetworkMetrics); aggregator.finalize(); }); it('should generate valid CSV format for summary', async () => { const config: ReportConfig = { formats: ['csv'] }; vi.mocked(fs.mkdir).mockResolvedValue(undefined); vi.mocked(fs.writeFile).mockResolvedValue(undefined); vi.mocked(path.join).mockImplementation((...args) => args.join('/')); await aggregator.generateReports(config); const summaryCSV = vi.mocked(fs.writeFile).mock.calls .find(call => call[0].includes('summary'))?.[1] as string; expect(summaryCSV).toContain('Total Requests,Successful Requests'); expect(summaryCSV).toContain('2,1,1'); // totalRequests, successfulRequests, failedRequests }); it('should generate valid CSV format for network metrics', async () => { const config: ReportConfig = { formats: ['csv'] }; vi.mocked(fs.mkdir).mockResolvedValue(undefined); vi.mocked(fs.writeFile).mockResolvedValue(undefined); vi.mocked(path.join).mockImplementation((...args) => args.join('/')); await aggregator.generateReports(config); const networkCSV = vi.mocked(fs.writeFile).mock.calls .find(call => call[0].includes('network'))?.[1] as string; expect(networkCSV).toContain('URL,Method,Response Time'); expect(networkCSV).toContain('https://example.com/api/test'); expect(networkCSV).toContain('GET,150,200'); }); }); describe('Prometheus integration', () => { it('should not export metrics when Prometheus is not configured', () => { const aggregatorWithoutPrometheus = new ResultsAggregator(); expect(() => { aggregatorWithoutPrometheus.addBrowserMetrics(mockBrowserMetrics); aggregatorWithoutPrometheus.addDRMMetrics(mockDRMMetrics); aggregatorWithoutPrometheus.addNetworkMetrics(mockNetworkMetrics); aggregatorWithoutPrometheus.addError(mockErrorLog); aggregatorWithoutPrometheus.finalize(); }).not.toThrow(); }); }); describe('shutdown', () => { it('should shutdown without Prometheus exporter', async () => { const aggregatorWithoutPrometheus = new ResultsAggregator(); await expect(aggregatorWithoutPrometheus.shutdown()).resolves.not.toThrow(); }); }); });