/**
* SerialPortAdapter 单元测试
*/
import { SerialPortAdapter } from '@/adapters/SerialPortAdapter';
import { PlatformAdapter } from '@/adapters/PlatformAdapter';
import { SerialPort } from 'serialport';
import { SerialError } from '@/types';
import { ErrorCode } from '@/utils/error-codes';
// Mock serialport
jest.mock('serialport');
const MockSerialPort = SerialPort as jest.MockedClass<typeof SerialPort>;
// Mock PlatformAdapter
jest.mock('@/adapters/PlatformAdapter');
const MockPlatformAdapter = PlatformAdapter as jest.MockedClass<typeof PlatformAdapter>;
describe('SerialPortAdapter', () => {
let adapter: SerialPortAdapter;
let mockPlatformAdapter: jest.Mocked<PlatformAdapter>;
let mockSerialPort: jest.Mocked<SerialPort>;
beforeEach(() => {
// 创建mock实例
mockPlatformAdapter = new MockPlatformAdapter() as jest.Mocked<PlatformAdapter>;
mockSerialPort = {
open: jest.fn(),
close: jest.fn(),
write: jest.fn(),
drain: jest.fn(),
flush: jest.fn(),
set: jest.fn(),
get: jest.fn(),
on: jest.fn(),
off: jest.fn(),
isOpen: false,
destroy: jest.fn()
} as any;
MockSerialPort.mockImplementation(() => mockSerialPort);
adapter = new SerialPortAdapter(mockPlatformAdapter);
// 设置默认mock行为
mockPlatformAdapter.normalizePortPath.mockImplementation(path => path);
mockPlatformAdapter.validatePortPath.mockReturnValue(true);
mockPlatformAdapter.checkPermissions.mockResolvedValue(true);
mockPlatformAdapter.isPortInUse.mockResolvedValue(false);
mockPlatformAdapter.getStandardBaudRates.mockReturnValue([9600, 115200]);
mockPlatformAdapter.getSupportedDataBits.mockReturnValue([5, 6, 7, 8]);
mockPlatformAdapter.getSupportedStopBits.mockReturnValue([1, 2]);
mockPlatformAdapter.getSupportedParity.mockReturnValue(['none', 'even', 'odd']);
mockPlatformAdapter.getSupportedFlowControl.mockReturnValue(['none', 'rts_cts', 'xon_xoff']);
jest.clearAllMocks();
});
afterEach(async () => {
if (adapter.isOpenPort()) {
await adapter.close();
}
});
describe('constructor', () => {
it('should initialize with default platform adapter', () => {
const adapterWithoutMock = new SerialPortAdapter();
expect(adapterWithoutMock).toBeDefined();
});
it('should initialize with custom platform adapter', () => {
expect(adapter).toBeDefined();
});
});
describe('open', () => {
const validConfig = {
port: 'COM1',
baudrate: 9600,
dataBits: 8 as const,
parity: 'none' as const,
stopBits: 1 as const,
flowControl: 'none' as const
};
it('should open port successfully', async () => {
(mockSerialPort.open as jest.Mock).mockImplementation(callback => callback(null));
await adapter.open(validConfig);
expect(mockPlatformAdapter.normalizePortPath).toHaveBeenCalledWith('COM1');
expect(mockPlatformAdapter.validatePortPath).toHaveBeenCalledWith('COM1');
expect(mockPlatformAdapter.checkPermissions).toHaveBeenCalled();
expect(mockSerialPort.open).toHaveBeenCalled();
expect(adapter.isOpenPort()).toBe(true);
});
it('should throw error if already open', async () => {
(mockSerialPort.open as jest.Mock).mockImplementation(callback => callback(null));
await adapter.open(validConfig);
await expect(adapter.open(validConfig)).rejects.toThrow(SerialError);
});
it('should throw error for invalid port path', async () => {
mockPlatformAdapter.validatePortPath.mockReturnValue(false);
await expect(adapter.open(validConfig)).rejects.toThrow(SerialError);
});
it('should throw error for insufficient permissions', async () => {
mockPlatformAdapter.checkPermissions.mockResolvedValue(false);
await expect(adapter.open(validConfig)).rejects.toThrow(SerialError);
});
it('should validate baud rate', async () => {
mockPlatformAdapter.getStandardBaudRates.mockReturnValue([9600]);
const invalidConfig = { ...validConfig, baudrate: 115200 };
await expect(adapter.open(invalidConfig)).rejects.toThrow(SerialError);
});
it('should handle open timeout', async () => {
(mockSerialPort.open as jest.Mock).mockImplementation(() => {
// 不调用callback,模拟超时
});
await expect(adapter.open(validConfig)).rejects.toThrow(SerialError);
});
it('should handle open error', async () => {
const error = new Error('Access denied');
(mockSerialPort.open as jest.Mock).mockImplementation(callback => callback(error));
await expect(adapter.open(validConfig)).rejects.toThrow(SerialError);
});
it('should clean up on open failure', async () => {
const error = new Error('Test error');
(mockSerialPort.open as jest.Mock).mockImplementation(callback => callback(error));
try {
await adapter.open(validConfig);
} catch (e) {
// Expected
}
expect(mockSerialPort.destroy).toHaveBeenCalled();
});
});
describe('close', () => {
const validConfig = {
port: 'COM1',
baudrate: 9600,
dataBits: 8 as const,
parity: 'none' as const,
stopBits: 1 as const,
flowControl: 'none' as const
};
beforeEach(async () => {
(mockSerialPort.open as jest.Mock).mockImplementation(callback => callback(null));
(mockSerialPort.close as jest.Mock).mockImplementation(callback => callback(null));
await adapter.open(validConfig);
});
it('should close port successfully', async () => {
await adapter.close();
expect(mockSerialPort.close).toHaveBeenCalled();
expect(adapter.isOpenPort()).toBe(false);
});
it('should handle close error', async () => {
const error = new Error('Close error');
(mockSerialPort.close as jest.Mock).mockImplementation(callback => callback(error));
await expect(adapter.close()).rejects.toThrow(SerialError);
});
it('should handle close when not open', async () => {
await adapter.close(); // First close
await adapter.close(); // Second close should not throw
expect(mockSerialPort.close).toHaveBeenCalledTimes(1);
});
});
describe('write', () => {
const validConfig = {
port: 'COM1',
baudrate: 9600,
dataBits: 8 as const,
parity: 'none' as const,
stopBits: 1 as const,
flowControl: 'none' as const
};
const testData = Buffer.from('test data');
beforeEach(async () => {
(mockSerialPort.open as jest.Mock).mockImplementation(callback => callback(null));
(mockSerialPort.write as jest.Mock).mockImplementation((data, callback) => callback(null));
(mockSerialPort.drain as jest.Mock).mockImplementation(callback => callback(null));
await adapter.open(validConfig);
});
it('should write data successfully', async () => {
await adapter.write(testData);
expect(mockSerialPort.write).toHaveBeenCalledWith(testData, expect.any(Function));
expect(mockSerialPort.drain).toHaveBeenCalled();
});
it('should throw error if port not open', async () => {
await adapter.close();
await expect(adapter.write(testData)).rejects.toThrow(SerialError);
});
it('should throw error for invalid data', async () => {
await expect(adapter.write(null as any)).rejects.toThrow(SerialError);
});
it('should handle empty buffer', async () => {
await adapter.write(Buffer.alloc(0));
expect(mockSerialPort.write).not.toHaveBeenCalled();
});
it('should handle write error', async () => {
const error = new Error('Write failed');
(mockSerialPort.write as jest.Mock).mockImplementation((data, callback) => callback(error));
await expect(adapter.write(testData)).rejects.toThrow(SerialError);
});
it('should handle write timeout', async () => {
(mockSerialPort.write as jest.Mock).mockImplementation(() => {
// 不调用callback,模拟超时
});
await expect(adapter.write(testData)).rejects.toThrow(SerialError);
});
it('should queue multiple writes', async () => {
const promises = [
adapter.write(Buffer.from('data1')),
adapter.write(Buffer.from('data2')),
adapter.write(Buffer.from('data3'))
];
// 模拟异步写入
setTimeout(() => {
(mockSerialPort.write.mock.calls[0][1] as any)(null);
setTimeout(() => {
(mockSerialPort.write.mock.calls[1][1] as any)(null);
setTimeout(() => {
(mockSerialPort.write.mock.calls[2][1] as any)(null);
}, 10);
}, 10);
}, 10);
await Promise.all(promises);
expect(mockSerialPort.write).toHaveBeenCalledTimes(3);
});
});
describe('writeBatch', () => {
const validConfig = {
port: 'COM1',
baudrate: 9600,
dataBits: 8 as const,
parity: 'none' as const,
stopBits: 1 as const,
flowControl: 'none' as const
};
beforeEach(async () => {
(mockSerialPort.open as jest.Mock).mockImplementation(callback => callback(null));
(mockSerialPort.write as jest.Mock).mockImplementation((data, callback) => callback(null));
(mockSerialPort.drain as jest.Mock).mockImplementation(callback => callback(null));
await adapter.open(validConfig);
});
it('should write batch successfully', async () => {
const buffers = [
Buffer.from('data1'),
Buffer.from('data2'),
Buffer.from('data3')
];
await adapter.writeBatch(buffers);
expect(mockSerialPort.write).toHaveBeenCalledWith(
Buffer.concat(buffers),
expect.any(Function)
);
});
it('should throw error for empty batch', async () => {
await expect(adapter.writeBatch([])).rejects.toThrow(SerialError);
});
it('should throw error for oversized batch', async () => {
const largeBuffer = Buffer.alloc(2 * 1024 * 1024); // 2MB
await expect(adapter.writeBatch([largeBuffer])).rejects.toThrow(SerialError);
});
});
describe('event handling', () => {
const validConfig = {
port: 'COM1',
baudrate: 9600,
dataBits: 8 as const,
parity: 'none' as const,
stopBits: 1 as const,
flowControl: 'none' as const
};
beforeEach(async () => {
(mockSerialPort.open as jest.Mock).mockImplementation(callback => callback(null));
await adapter.open(validConfig);
});
it('should register event handlers', () => {
const handler = jest.fn();
adapter.on('data', handler);
expect(mockSerialPort.on).toHaveBeenCalledWith('data', handler);
});
it('should remove event handlers', () => {
const handler = jest.fn();
adapter.on('data', handler);
adapter.off('data', handler);
expect(mockSerialPort.off).toHaveBeenCalledWith('data', handler);
});
it('should handle data events', () => {
const handler = jest.fn();
adapter.on('data', handler);
// 模拟数据接收
const testData = Buffer.from('test');
const dataHandler = mockSerialPort.on.mock.calls.find(call => call[0] === 'data')?.[1];
dataHandler?.(testData);
expect(handler).toHaveBeenCalledWith(testData);
});
it('should handle error events', () => {
const handler = jest.fn();
adapter.on('error', handler);
const error = new Error('Test error');
const errorHandler = mockSerialPort.on.mock.calls.find(call => call[0] === 'error')?.[1];
errorHandler?.(error);
expect(handler).toHaveBeenCalledWith(error, expect.any(Boolean));
});
});
describe('statistics', () => {
const validConfig = {
port: 'COM1',
baudrate: 9600,
dataBits: 8 as const,
parity: 'none' as const,
stopBits: 1 as const,
flowControl: 'none' as const
};
beforeEach(async () => {
(mockSerialPort.open as jest.Mock).mockImplementation(callback => callback(null));
(mockSerialPort.write as jest.Mock).mockImplementation((data, callback) => callback(null));
(mockSerialPort.drain as jest.Mock).mockImplementation(callback => callback(null));
await adapter.open(validConfig);
});
it('should track write statistics', async () => {
await adapter.write(Buffer.from('test'));
const stats = adapter.getStats();
expect(stats.bytesWritten).toBe(4);
expect(stats.writeCount).toBe(1);
});
it('should reset statistics', async () => {
await adapter.write(Buffer.from('test'));
adapter.resetStats();
const stats = adapter.getStats();
expect(stats.bytesWritten).toBe(0);
expect(stats.writeCount).toBe(0);
});
});
describe('getSignals and setSignals', () => {
const validConfig = {
port: 'COM1',
baudrate: 9600,
dataBits: 8 as const,
parity: 'none' as const,
stopBits: 1 as const,
flowControl: 'none' as const
};
beforeEach(async () => {
(mockSerialPort.open as jest.Mock).mockImplementation(callback => callback(null));
(mockSerialPort.get as jest.Mock).mockImplementation(callback => callback(null, { cts: true, dsr: false }));
(mockSerialPort.set as jest.Mock).mockImplementation(callback => callback(null));
await adapter.open(validConfig);
});
it('should get signals', async () => {
const signals = await adapter.getSignals();
expect(mockSerialPort.get).toHaveBeenCalled();
expect(signals).toEqual({ cts: true, dsr: false });
});
it('should set signals', async () => {
const signals = { dtr: true, rts: false };
await adapter.setSignals(signals);
expect(mockSerialPort.set).toHaveBeenCalledWith(signals, expect.any(Function));
});
it('should throw error when port not open', async () => {
await adapter.close();
await expect(adapter.getSignals()).rejects.toThrow(SerialError);
await expect(adapter.setSignals({})).rejects.toThrow(SerialError);
});
});
describe('destroy', () => {
const validConfig = {
port: 'COM1',
baudrate: 9600,
dataBits: 8 as const,
parity: 'none' as const,
stopBits: 1 as const,
flowControl: 'none' as const
};
beforeEach(async () => {
(mockSerialPort.open as jest.Mock).mockImplementation(callback => callback(null));
(mockSerialPort.close as jest.Mock).mockImplementation(callback => callback(null));
await adapter.open(validConfig);
});
it('should destroy adapter', async () => {
await adapter.destroy();
expect(mockSerialPort.close).toHaveBeenCalled();
expect(mockSerialPort.destroy).toHaveBeenCalled();
expect(adapter.isOpenPort()).toBe(false);
});
});
});