browser-manager.test.tsā¢17.9 kB
/**
* Unit Tests for Browser Manager
*
* Following TDD Red-Green-Refactor methodology with 2025 best practices:
* - AAA Pattern (Arrange-Act-Assert)
* - Behavior-focused testing with proper mocking
* - Error categorization and circuit breaker testing
* - Chrome detection and network utilities testing
*/
import { describe, it, expect, beforeEach, vi } from 'vitest';
import * as fs from 'fs';
import * as net from 'net';
import {
BrowserErrorType,
categorizeError,
withTimeout,
isPortAvailable,
testHostConnectivity,
findAvailablePort,
updateCircuitBreakerOnFailure,
updateCircuitBreakerOnSuccess,
isCircuitBreakerOpen,
detectChromePath,
validateSession,
findAuthElements,
getBrowserInstance,
getPageInstance,
getContentPriorityConfig,
updateContentPriorityConfig,
forceKillAllChromeProcesses
} from './browser-manager.js';
// Mock external dependencies
vi.mock('fs');
vi.mock('net');
vi.mock('child_process');
vi.mock('puppeteer-real-browser', () => ({
connect: vi.fn()
}));
describe('Browser Manager', () => {
beforeEach(() => {
vi.clearAllMocks();
vi.resetModules();
});
// Helper function to create mock server for port testing
const createMockServer = (shouldSucceed: boolean = true) => {
return {
listen: vi.fn((port, host, callback) => {
if (shouldSucceed) {
callback();
}
return createMockServer(shouldSucceed);
}),
close: vi.fn(),
once: vi.fn((event, callback) => {
if (event === 'close') callback();
}),
on: vi.fn((event, callback) => {
if (!shouldSucceed && event === 'error') {
callback(new Error('EADDRINUSE'));
}
})
};
};
describe('Error Categorization', () => {
it('should categorize FRAME_DETACHED error correctly', () => {
// Arrange: Create error with frame detached message
const error = new Error('Navigating frame was detached');
// Act: Categorize the error
const result = categorizeError(error);
// Assert: Should return FRAME_DETACHED type
expect(result).toBe(BrowserErrorType.FRAME_DETACHED);
});
it('should categorize SESSION_CLOSED error correctly', () => {
// Arrange: Create error with session closed message
const error = new Error('Session closed');
// Act: Categorize the error
const result = categorizeError(error);
// Assert: Should return SESSION_CLOSED type
expect(result).toBe(BrowserErrorType.SESSION_CLOSED);
});
it('should categorize TARGET_CLOSED error correctly', () => {
// Arrange: Create error with target closed message
const error = new Error('Target closed');
// Act: Categorize the error
const result = categorizeError(error);
// Assert: Should return TARGET_CLOSED type
expect(result).toBe(BrowserErrorType.TARGET_CLOSED);
});
it('should categorize PROTOCOL_ERROR correctly', () => {
// Arrange: Create error with protocol error message
const error = new Error('Protocol error');
// Act: Categorize the error
const result = categorizeError(error);
// Assert: Should return PROTOCOL_ERROR type
expect(result).toBe(BrowserErrorType.PROTOCOL_ERROR);
});
it('should categorize NAVIGATION_TIMEOUT error correctly', () => {
// Arrange: Create error with navigation timeout message
const error = new Error('Navigation timeout exceeded');
// Act: Categorize the error
const result = categorizeError(error);
// Assert: Should return NAVIGATION_TIMEOUT type
expect(result).toBe(BrowserErrorType.NAVIGATION_TIMEOUT);
});
it('should categorize ELEMENT_NOT_FOUND error correctly', () => {
// Arrange: Create error with element not found message
const error = new Error('Element not found');
// Act: Categorize the error
const result = categorizeError(error);
// Assert: Should return ELEMENT_NOT_FOUND type
expect(result).toBe(BrowserErrorType.ELEMENT_NOT_FOUND);
});
it('should categorize unknown errors as UNKNOWN', () => {
// Arrange: Create error with unrecognized message
const error = new Error('Some random error message');
// Act: Categorize the error
const result = categorizeError(error);
// Assert: Should return UNKNOWN type
expect(result).toBe(BrowserErrorType.UNKNOWN);
});
it('should handle case-insensitive error message matching', () => {
// Arrange: Create error with uppercase message
const error = new Error('SESSION CLOSED');
// Act: Categorize the error
const result = categorizeError(error);
// Assert: Should still categorize correctly
expect(result).toBe(BrowserErrorType.SESSION_CLOSED);
});
});
describe('Timeout Wrapper', () => {
it('should resolve when operation completes within timeout', async () => {
// Arrange: Create operation that resolves quickly
const operation = vi.fn().mockResolvedValue('success');
// Act: Execute with timeout
const result = await withTimeout(operation, 1000, 'test-context');
// Assert: Should return operation result
expect(result).toBe('success');
expect(operation).toHaveBeenCalledOnce();
});
it('should reject when operation times out', async () => {
// Arrange: Create operation that never resolves
const operation = vi.fn().mockImplementation(() => new Promise(() => {}));
// Act & Assert: Should throw timeout error
await expect(withTimeout(operation, 100, 'test-context'))
.rejects.toThrow('Operation timed out after 100ms in context: test-context');
expect(operation).toHaveBeenCalledOnce();
});
it('should reject when operation throws error', async () => {
// Arrange: Create operation that throws
const operation = vi.fn().mockRejectedValue(new Error('operation failed'));
// Act & Assert: Should propagate operation error
await expect(withTimeout(operation, 1000, 'test-context'))
.rejects.toThrow('operation failed');
expect(operation).toHaveBeenCalledOnce();
});
it('should clear timeout when operation completes', async () => {
// Arrange: Create operation that resolves
const operation = vi.fn().mockResolvedValue('success');
const clearTimeoutSpy = vi.spyOn(global, 'clearTimeout');
// Act: Execute with timeout
await withTimeout(operation, 1000, 'test-context');
// Assert: Should clear timeout
expect(clearTimeoutSpy).toHaveBeenCalled();
});
});
describe('Port Availability', () => {
it('should return true when port is available', async () => {
// Arrange: Mock net.createServer to succeed
const mockServer = createMockServer(true);
vi.mocked(net.createServer).mockReturnValue(mockServer as any);
// Act: Check port availability
const result = await isPortAvailable(9222);
// Assert: Should return true
expect(result).toBe(true);
expect(mockServer.listen).toHaveBeenCalledWith(9222, '127.0.0.1', expect.any(Function));
});
it('should return false when port is not available', async () => {
// Arrange: Mock net.createServer to fail
const mockServer = createMockServer(false);
vi.mocked(net.createServer).mockReturnValue(mockServer as any);
// Act: Check port availability
const result = await isPortAvailable(9222);
// Assert: Should return false
expect(result).toBe(false);
});
it('should use custom host when provided', async () => {
// Arrange: Mock net.createServer to succeed
const mockServer = createMockServer(true);
vi.mocked(net.createServer).mockReturnValue(mockServer as any);
// Act: Check port availability with custom host
await isPortAvailable(9222, 'localhost');
// Assert: Should use custom host
expect(mockServer.listen).toHaveBeenCalledWith(9222, 'localhost', expect.any(Function));
});
});
describe('Host Connectivity Testing', () => {
it('should return connectivity results structure', async () => {
// Arrange & Act: Test host connectivity (real implementation)
const result = await testHostConnectivity();
// Assert: Should return expected structure regardless of actual connectivity
expect(result).toHaveProperty('localhost');
expect(result).toHaveProperty('ipv4');
expect(result).toHaveProperty('recommendedHost');
expect(typeof result.localhost).toBe('boolean');
expect(typeof result.ipv4).toBe('boolean');
expect(typeof result.recommendedHost).toBe('string');
expect(['localhost', '127.0.0.1']).toContain(result.recommendedHost);
});
});
describe('Available Port Finding', () => {
it('should return a valid port number or null', async () => {
// Arrange & Act: Find available port in a reasonable range
const result = await findAvailablePort(9222, 9224);
// Assert: Should return valid port number or null
if (result !== null) {
expect(result).toBeGreaterThanOrEqual(9222);
expect(result).toBeLessThanOrEqual(9224);
} else {
expect(result).toBe(null);
}
});
it('should handle empty port range', async () => {
// Arrange & Act: Find available port in impossible range
const result = await findAvailablePort(9999, 9998);
// Assert: Should return null for invalid range
expect(result).toBe(null);
});
});
describe('Circuit Breaker', () => {
it('should start in closed state', () => {
// Arrange & Act: Check initial circuit breaker state
const isOpen = isCircuitBreakerOpen();
// Assert: Should be closed initially
expect(isOpen).toBe(false);
});
it('should open circuit breaker after threshold failures', () => {
// Arrange: Clear circuit breaker state first
updateCircuitBreakerOnSuccess(); // Reset to closed state
// Act: Trigger multiple failures
for (let i = 0; i < 5; i++) {
updateCircuitBreakerOnFailure();
}
// Assert: Should be open after threshold
const isOpen = isCircuitBreakerOpen();
expect(isOpen).toBe(true);
});
it('should reset circuit breaker on success', () => {
// Arrange: Open circuit breaker first
for (let i = 0; i < 5; i++) {
updateCircuitBreakerOnFailure();
}
expect(isCircuitBreakerOpen()).toBe(true);
// Act: Record success
updateCircuitBreakerOnSuccess();
// Assert: Should be closed again
expect(isCircuitBreakerOpen()).toBe(false);
});
it('should enter half-open state after timeout', () => {
// Arrange: Open circuit breaker
for (let i = 0; i < 5; i++) {
updateCircuitBreakerOnFailure();
}
expect(isCircuitBreakerOpen()).toBe(true);
// Mock Date.now to simulate timeout passage
const originalNow = Date.now;
Date.now = vi.fn().mockReturnValue(originalNow() + 35000); // 35 seconds later
// Act: Check circuit breaker state
const isOpen = isCircuitBreakerOpen();
// Assert: Should transition to half-open (returns false)
expect(isOpen).toBe(false);
// Cleanup: Restore Date.now
Date.now = originalNow;
});
});
describe('Chrome Path Detection', () => {
it('should return environment variable path when available', () => {
// Arrange: Set environment variable and mock file exists
const chromePath = '/custom/chrome/path';
process.env.CHROME_PATH = chromePath;
vi.mocked(fs.existsSync).mockReturnValue(true);
// Act: Detect Chrome path
const result = detectChromePath();
// Assert: Should return environment path
expect(result).toBe(chromePath);
expect(fs.existsSync).toHaveBeenCalledWith(chromePath);
// Cleanup
delete process.env.CHROME_PATH;
});
it('should return null when Chrome is not found', () => {
// Arrange: Mock file system to return false for all paths
vi.mocked(fs.existsSync).mockReturnValue(false);
delete process.env.CHROME_PATH;
delete process.env.PUPPETEER_EXECUTABLE_PATH;
// Act: Detect Chrome path
const result = detectChromePath();
// Assert: Should return null
expect(result).toBe(null);
});
it('should detect Chrome on macOS platform', () => {
// Arrange: Mock platform and file system
Object.defineProperty(process, 'platform', { value: 'darwin' });
const expectedPath = '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome';
vi.mocked(fs.existsSync).mockImplementation((path) => path === expectedPath);
delete process.env.CHROME_PATH;
// Act: Detect Chrome path
const result = detectChromePath();
// Assert: Should return macOS Chrome path
expect(result).toBe(expectedPath);
});
it('should detect Chrome on Linux platform', () => {
// Arrange: Mock platform and file system
Object.defineProperty(process, 'platform', { value: 'linux' });
const expectedPath = '/usr/bin/google-chrome';
vi.mocked(fs.existsSync).mockImplementation((path) => path === expectedPath);
delete process.env.CHROME_PATH;
// Act: Detect Chrome path
const result = detectChromePath();
// Assert: Should return Linux Chrome path
expect(result).toBe(expectedPath);
});
it('should return null for unsupported platform', () => {
// Arrange: Mock unsupported platform
Object.defineProperty(process, 'platform', { value: 'freebsd' });
delete process.env.CHROME_PATH;
// Act: Detect Chrome path
const result = detectChromePath();
// Assert: Should return null
expect(result).toBe(null);
});
});
describe('Session Validation', () => {
it('should return false when no browser instance exists', async () => {
// Arrange: No browser instance (default state)
// Act: Validate session
const result = await validateSession();
// Assert: Should return false
expect(result).toBe(false);
});
it('should return false when validation is already in progress', async () => {
// Arrange: We'll test this by calling validateSession twice quickly
// This requires mocking the internal state or testing the behavior indirectly
// For now, we'll test the basic case - this could be expanded with more complex mocking
const result = await validateSession();
// Assert: Should handle concurrent validation gracefully
expect(result).toBe(false);
});
});
describe('Auth Elements Finding', () => {
it('should find authentication elements in page content', async () => {
// Arrange: Mock page instance with evaluate method
const mockPage = {
evaluate: vi.fn().mockResolvedValue(['#login-button', '.signin-link'])
};
// Act: Find auth elements
const result = await findAuthElements(mockPage);
// Assert: Should return auth selectors
expect(result).toEqual(['#login-button', '.signin-link']);
expect(mockPage.evaluate).toHaveBeenCalledWith(expect.any(Function));
});
});
describe('Content Priority Configuration', () => {
it('should return current content priority config', () => {
// Arrange & Act: Get content priority config
const config = getContentPriorityConfig();
// Assert: Should return configuration object
expect(config).toBeDefined();
expect(config).toHaveProperty('prioritizeContent');
expect(config).toHaveProperty('autoSuggestGetContent');
});
it('should update content priority config', () => {
// Arrange: Get initial config
const initialConfig = getContentPriorityConfig();
const updates = { prioritizeContent: !initialConfig.prioritizeContent };
// Act: Update config
updateContentPriorityConfig(updates);
const updatedConfig = getContentPriorityConfig();
// Assert: Should reflect updates
expect(updatedConfig.prioritizeContent).toBe(updates.prioritizeContent);
expect(updatedConfig.autoSuggestGetContent).toBe(initialConfig.autoSuggestGetContent);
});
});
describe('Browser Instance Getters', () => {
it('should return browser instance', () => {
// Arrange & Act: Get browser instance
const browser = getBrowserInstance();
// Assert: Should return browser (null initially)
expect(browser).toBe(null);
});
it('should return page instance', () => {
// Arrange & Act: Get page instance
const page = getPageInstance();
// Assert: Should return page (null initially)
expect(page).toBe(null);
});
});
describe('Force Kill Chrome Processes', () => {
it('should execute without throwing errors', async () => {
// Arrange & Act: Force kill Chrome processes
// Act & Assert: Should not throw error regardless of platform
await expect(forceKillAllChromeProcesses()).resolves.toBeUndefined();
});
it('should handle different platforms', async () => {
// Arrange: Test with current platform
const originalPlatform = process.platform;
// Act: Execute force kill
await forceKillAllChromeProcesses();
// Assert: Should complete without error
expect(process.platform).toBe(originalPlatform);
});
});
});