Skip to content

Eagle 插件测试规范

概述

本文档定义了 Eagle2Ae Eagle 插件的测试规范和最佳实践,确保代码质量、功能正确性和系统稳定性。

测试策略

测试金字塔

        /\     端到端测试 (10%)
       /  \    - 完整用户场景
      /____\   - 系统集成测试
     /      \  
    /        \ 集成测试 (20%)
   /          \ - 模块间交互
  /____________\ - API 集成测试
 /              \
/________________\ 单元测试 (70%)
                   - 函数级测试
                   - 类级测试

测试类型

单元测试 (Unit Tests)

  • 目标: 测试单个函数、类或模块的功能
  • 范围: 独立的代码单元
  • 工具: Jest
  • 覆盖率要求: ≥ 90%

集成测试 (Integration Tests)

  • 目标: 测试模块间的交互和数据流
  • 范围: 多个模块的协作
  • 工具: Jest + Supertest
  • 覆盖率要求: ≥ 80%

端到端测试 (E2E Tests)

  • 目标: 测试完整的用户工作流
  • 范围: 整个系统的功能
  • 工具: Jest + 模拟 Eagle 环境
  • 覆盖率要求: ≥ 70%

测试框架

Jest 配置

jest.config.js

javascript
module.exports = {
    // 测试环境
    testEnvironment: 'node',
    
    // 测试文件匹配模式
    testMatch: [
        '**/tests/**/*.test.js',
        '**/tests/**/*.spec.js',
        '**/__tests__/**/*.js'
    ],
    
    // 覆盖率收集
    collectCoverageFrom: [
        'src/**/*.js',
        '!src/index.js',
        '!src/**/index.js',
        '!src/**/*.config.js'
    ],
    
    // 覆盖率目录
    coverageDirectory: 'coverage',
    
    // 覆盖率报告格式
    coverageReporters: [
        'text',
        'lcov',
        'html',
        'json-summary'
    ],
    
    // 覆盖率阈值
    coverageThreshold: {
        global: {
            branches: 70,
            functions: 90,
            lines: 85,
            statements: 85
        }
    },
    
    // 设置文件
    setupFilesAfterEnv: [
        '<rootDir>/tests/helpers/setup.js'
    ],
    
    // 模块路径映射
    moduleNameMapping: {
        '^@/(.*)$': '<rootDir>/src/$1',
        '^@tests/(.*)$': '<rootDir>/tests/$1'
    },
    
    // 测试超时
    testTimeout: 10000,
    
    // 并行测试
    maxWorkers: '50%',
    
    // 详细输出
    verbose: true
};

测试设置文件 (tests/helpers/setup.js)

javascript
/**
 * Jest 测试设置文件
 * 配置全局测试环境和工具
 */

const fs = require('fs-extra');
const path = require('path');

// 全局测试配置
global.TEST_CONFIG = {
    timeout: 5000,
    tempDir: path.join(__dirname, '../temp'),
    fixturesDir: path.join(__dirname, '../fixtures')
};

// 测试前设置
beforeAll(async () => {
    // 创建临时目录
    await fs.ensureDir(global.TEST_CONFIG.tempDir);
    
    // 设置环境变量
    process.env.NODE_ENV = 'test';
    process.env.LOG_LEVEL = 'error';
});

// 测试后清理
afterAll(async () => {
    // 清理临时文件
    await fs.remove(global.TEST_CONFIG.tempDir);
});

// 每个测试前的设置
beforeEach(() => {
    // 清理模拟函数
    jest.clearAllMocks();
});

// 自定义匹配器
expect.extend({
    toBeValidFilePath(received) {
        const pass = typeof received === 'string' && received.length > 0;
        return {
            message: () => `expected ${received} to be a valid file path`,
            pass
        };
    },
    
    toBeValidWebSocketMessage(received) {
        const pass = received && 
                    typeof received.type === 'string' &&
                    received.messageId &&
                    received.timestamp;
        return {
            message: () => `expected ${received} to be a valid WebSocket message`,
            pass
        };
    }
});

// 全局错误处理
process.on('unhandledRejection', (reason, promise) => {
    console.error('Unhandled Rejection at:', promise, 'reason:', reason);
});

单元测试

工具函数测试

tests/unit/utils/file-utils.test.js

javascript
/**
 * 文件工具函数单元测试
 */

const {
    getFileExtension,
    validateFilePath,
    getFileSize,
    isImageFile,
    normalizeFilePath
} = require('@/utils/file-utils');

const fs = require('fs-extra');
const path = require('path');

describe('文件工具函数', () => {
    describe('getFileExtension', () => {
        test('应该正确提取文件扩展名', () => {
            expect(getFileExtension('image.jpg')).toBe('jpg');
            expect(getFileExtension('document.pdf')).toBe('pdf');
            expect(getFileExtension('archive.tar.gz')).toBe('gz');
            expect(getFileExtension('file.JPEG')).toBe('jpeg');
        });
        
        test('应该处理没有扩展名的文件', () => {
            expect(getFileExtension('filename')).toBe('');
            expect(getFileExtension('')).toBe('');
            expect(getFileExtension('.')).toBe('');
        });
        
        test('应该处理路径中的文件', () => {
            expect(getFileExtension('/path/to/file.png')).toBe('png');
            expect(getFileExtension('C:\\Users\\file.txt')).toBe('txt');
            expect(getFileExtension('./relative/path/file.mp4')).toBe('mp4');
        });
        
        test('应该处理特殊字符', () => {
            expect(getFileExtension('file name with spaces.jpg')).toBe('jpg');
            expect(getFileExtension('文件名.png')).toBe('png');
            expect(getFileExtension('file-with-dashes.pdf')).toBe('pdf');
        });
    });
    
    describe('validateFilePath', () => {
        let tempFile;
        
        beforeEach(async () => {
            tempFile = path.join(global.TEST_CONFIG.tempDir, 'test-file.txt');
            await fs.writeFile(tempFile, 'test content');
        });
        
        afterEach(async () => {
            if (await fs.pathExists(tempFile)) {
                await fs.remove(tempFile);
            }
        });
        
        test('应该验证存在的文件', async () => {
            const result = await validateFilePath(tempFile);
            expect(result.valid).toBe(true);
            expect(result.error).toBeUndefined();
        });
        
        test('应该拒绝不存在的文件', async () => {
            const result = await validateFilePath('/non/existent/file.txt');
            expect(result.valid).toBe(false);
            expect(result.error).toContain('文件不存在');
        });
        
        test('应该拒绝空路径', async () => {
            const result = await validateFilePath('');
            expect(result.valid).toBe(false);
            expect(result.error).toContain('路径不能为空');
        });
        
        test('应该拒绝 null 或 undefined', async () => {
            const nullResult = await validateFilePath(null);
            const undefinedResult = await validateFilePath(undefined);
            
            expect(nullResult.valid).toBe(false);
            expect(undefinedResult.valid).toBe(false);
        });
    });
    
    describe('getFileSize', () => {
        let tempFile;
        const testContent = 'Hello, World!';
        
        beforeEach(async () => {
            tempFile = path.join(global.TEST_CONFIG.tempDir, 'size-test.txt');
            await fs.writeFile(tempFile, testContent);
        });
        
        test('应该返回正确的文件大小', async () => {
            const size = await getFileSize(tempFile);
            expect(size).toBe(Buffer.byteLength(testContent, 'utf8'));
        });
        
        test('应该处理不存在的文件', async () => {
            await expect(getFileSize('/non/existent/file.txt'))
                .rejects.toThrow('文件不存在');
        });
    });
    
    describe('isImageFile', () => {
        test('应该识别图片文件', () => {
            expect(isImageFile('image.jpg')).toBe(true);
            expect(isImageFile('photo.jpeg')).toBe(true);
            expect(isImageFile('picture.png')).toBe(true);
            expect(isImageFile('icon.gif')).toBe(true);
            expect(isImageFile('vector.svg')).toBe(true);
            expect(isImageFile('bitmap.bmp')).toBe(true);
        });
        
        test('应该拒绝非图片文件', () => {
            expect(isImageFile('document.pdf')).toBe(false);
            expect(isImageFile('video.mp4')).toBe(false);
            expect(isImageFile('audio.mp3')).toBe(false);
            expect(isImageFile('text.txt')).toBe(false);
        });
        
        test('应该处理大小写', () => {
            expect(isImageFile('IMAGE.JPG')).toBe(true);
            expect(isImageFile('Photo.JPEG')).toBe(true);
            expect(isImageFile('Picture.PNG')).toBe(true);
        });
    });
    
    describe('normalizeFilePath', () => {
        test('应该标准化路径分隔符', () => {
            expect(normalizeFilePath('C:\\Users\\file.txt'))
                .toBe('C:/Users/file.txt');
            expect(normalizeFilePath('/home/user/file.txt'))
                .toBe('/home/user/file.txt');
        });
        
        test('应该处理相对路径', () => {
            expect(normalizeFilePath('./file.txt'))
                .toBe('./file.txt');
            expect(normalizeFilePath('../parent/file.txt'))
                .toBe('../parent/file.txt');
        });
        
        test('应该移除多余的分隔符', () => {
            expect(normalizeFilePath('/path//to///file.txt'))
                .toBe('/path/to/file.txt');
            expect(normalizeFilePath('C:\\\\Users\\\\file.txt'))
                .toBe('C:/Users/file.txt');
        });
    });
});

服务类测试

tests/unit/services/websocket-server.test.js

javascript
/**
 * WebSocket 服务器单元测试
 */

const WebSocketServer = require('@/services/websocket-server');
const EventEmitter = require('events');

// 模拟 ws 模块
jest.mock('ws', () => {
    const mockWebSocket = {
        on: jest.fn(),
        send: jest.fn(),
        close: jest.fn(),
        readyState: 1 // OPEN
    };
    
    const mockServer = {
        on: jest.fn(),
        close: jest.fn(),
        clients: new Set()
    };
    
    return {
        Server: jest.fn(() => mockServer),
        WebSocket: jest.fn(() => mockWebSocket)
    };
});

describe('WebSocketServer', () => {
    let server;
    let mockWsServer;
    
    beforeEach(() => {
        server = new WebSocketServer({
            port: 8080,
            host: 'localhost'
        });
        
        // 获取模拟的 WebSocket 服务器
        const WS = require('ws');
        mockWsServer = new WS.Server();
    });
    
    afterEach(async () => {
        if (server) {
            await server.stop();
        }
        jest.clearAllMocks();
    });
    
    describe('构造函数', () => {
        test('应该使用默认配置', () => {
            const defaultServer = new WebSocketServer();
            expect(defaultServer.options.port).toBe(8080);
            expect(defaultServer.options.host).toBe('localhost');
        });
        
        test('应该使用自定义配置', () => {
            const customServer = new WebSocketServer({
                port: 9090,
                host: '127.0.0.1',
                heartbeatInterval: 60000
            });
            
            expect(customServer.options.port).toBe(9090);
            expect(customServer.options.host).toBe('127.0.0.1');
            expect(customServer.options.heartbeatInterval).toBe(60000);
        });
    });
    
    describe('start', () => {
        test('应该成功启动服务器', async () => {
            const startPromise = server.start();
            
            // 模拟服务器启动成功
            const listenCallback = mockWsServer.on.mock.calls
                .find(call => call[0] === 'listening')[1];
            listenCallback();
            
            const result = await startPromise;
            
            expect(result.success).toBe(true);
            expect(server.isRunning()).toBe(true);
        });
        
        test('应该处理启动错误', async () => {
            const startPromise = server.start();
            
            // 模拟服务器启动错误
            const errorCallback = mockWsServer.on.mock.calls
                .find(call => call[0] === 'error')[1];
            errorCallback(new Error('端口被占用'));
            
            await expect(startPromise).rejects.toThrow('端口被占用');
            expect(server.isRunning()).toBe(false);
        });
        
        test('应该防止重复启动', async () => {
            // 第一次启动
            const firstStart = server.start();
            const listenCallback = mockWsServer.on.mock.calls
                .find(call => call[0] === 'listening')[1];
            listenCallback();
            await firstStart;
            
            // 第二次启动应该抛出错误
            await expect(server.start()).rejects.toThrow('服务器已在运行');
        });
    });
    
    describe('stop', () => {
        beforeEach(async () => {
            const startPromise = server.start();
            const listenCallback = mockWsServer.on.mock.calls
                .find(call => call[0] === 'listening')[1];
            listenCallback();
            await startPromise;
        });
        
        test('应该成功停止服务器', async () => {
            const stopPromise = server.stop();
            
            // 模拟服务器关闭
            const closeCallback = mockWsServer.close.mock.calls[0][0];
            closeCallback();
            
            await stopPromise;
            
            expect(server.isRunning()).toBe(false);
        });
        
        test('应该清理所有连接', async () => {
            // 添加模拟连接
            const mockConnection = { id: 'test-1', close: jest.fn() };
            server.connections.set('test-1', mockConnection);
            
            const stopPromise = server.stop();
            const closeCallback = mockWsServer.close.mock.calls[0][0];
            closeCallback();
            
            await stopPromise;
            
            expect(mockConnection.close).toHaveBeenCalled();
            expect(server.connections.size).toBe(0);
        });
    });
    
    describe('消息处理', () => {
        test('应该注册消息处理器', () => {
            const handler = jest.fn();
            server.registerMessageHandler('test_message', handler);
            
            expect(server.messageHandlers.has('test_message')).toBe(true);
            expect(server.messageHandlers.get('test_message')).toBe(handler);
        });
        
        test('应该注销消息处理器', () => {
            const handler = jest.fn();
            server.registerMessageHandler('test_message', handler);
            server.unregisterMessageHandler('test_message');
            
            expect(server.messageHandlers.has('test_message')).toBe(false);
        });
        
        test('应该处理未知消息类型', async () => {
            const mockConnection = {
                id: 'test-1',
                send: jest.fn()
            };
            
            const message = {
                type: 'unknown_message',
                messageId: 'msg-001',
                data: {}
            };
            
            await server.handleMessage(mockConnection, message);
            
            expect(mockConnection.send).toHaveBeenCalledWith(
                expect.stringContaining('unknown_message_type')
            );
        });
    });
    
    describe('广播功能', () => {
        test('应该向所有连接广播消息', () => {
            const mockConnection1 = { id: 'test-1', send: jest.fn() };
            const mockConnection2 = { id: 'test-2', send: jest.fn() };
            
            server.connections.set('test-1', mockConnection1);
            server.connections.set('test-2', mockConnection2);
            
            const message = { type: 'broadcast', data: 'test' };
            server.broadcast(message);
            
            expect(mockConnection1.send).toHaveBeenCalledWith(
                JSON.stringify(message)
            );
            expect(mockConnection2.send).toHaveBeenCalledWith(
                JSON.stringify(message)
            );
        });
        
        test('应该跳过断开的连接', () => {
            const mockConnection1 = { 
                id: 'test-1', 
                send: jest.fn(),
                readyState: 1 // OPEN
            };
            const mockConnection2 = { 
                id: 'test-2', 
                send: jest.fn(),
                readyState: 3 // CLOSED
            };
            
            server.connections.set('test-1', mockConnection1);
            server.connections.set('test-2', mockConnection2);
            
            const message = { type: 'broadcast', data: 'test' };
            server.broadcast(message);
            
            expect(mockConnection1.send).toHaveBeenCalled();
            expect(mockConnection2.send).not.toHaveBeenCalled();
        });
    });
});

集成测试

WebSocket 通信集成测试

tests/integration/websocket-communication.test.js

javascript
/**
 * WebSocket 通信集成测试
 */

const WebSocketServer = require('@/services/websocket-server');
const WebSocket = require('ws');
const { promisify } = require('util');

describe('WebSocket 通信集成测试', () => {
    let server;
    let client;
    const testPort = 8081;
    
    beforeAll(async () => {
        server = new WebSocketServer({ port: testPort });
        await server.start();
    });
    
    afterAll(async () => {
        if (server) {
            await server.stop();
        }
    });
    
    afterEach(() => {
        if (client && client.readyState === WebSocket.OPEN) {
            client.close();
        }
    });
    
    test('应该建立 WebSocket 连接', (done) => {
        client = new WebSocket(`ws://localhost:${testPort}`);
        
        client.on('open', () => {
            expect(client.readyState).toBe(WebSocket.OPEN);
            expect(server.getConnections().length).toBe(1);
            done();
        });
        
        client.on('error', done);
    });
    
    test('应该处理状态查询消息', (done) => {
        client = new WebSocket(`ws://localhost:${testPort}`);
        
        client.on('open', () => {
            const message = {
                type: 'status_query',
                messageId: 'test_001',
                timestamp: Date.now(),
                data: {}
            };
            
            client.send(JSON.stringify(message));
        });
        
        client.on('message', (data) => {
            const response = JSON.parse(data);
            
            expect(response).toBeValidWebSocketMessage();
            expect(response.type).toBe('status_response');
            expect(response.messageId).toBe('test_001');
            expect(response.data.status).toBe('running');
            
            done();
        });
        
        client.on('error', done);
    });
    
    test('应该处理文件信息查询', (done) => {
        client = new WebSocket(`ws://localhost:${testPort}`);
        
        client.on('open', () => {
            const message = {
                type: 'file_info_query',
                messageId: 'test_002',
                timestamp: Date.now(),
                data: {
                    filePath: '/path/to/test/file.jpg'
                }
            };
            
            client.send(JSON.stringify(message));
        });
        
        client.on('message', (data) => {
            const response = JSON.parse(data);
            
            expect(response.type).toBe('file_info_response');
            expect(response.messageId).toBe('test_002');
            expect(response.data).toHaveProperty('fileInfo');
            
            done();
        });
        
        client.on('error', done);
    });
    
    test('应该处理心跳消息', (done) => {
        client = new WebSocket(`ws://localhost:${testPort}`);
        
        client.on('open', () => {
            const message = {
                type: 'heartbeat',
                messageId: 'heartbeat_001',
                timestamp: Date.now(),
                data: {}
            };
            
            client.send(JSON.stringify(message));
        });
        
        client.on('message', (data) => {
            const response = JSON.parse(data);
            
            expect(response.type).toBe('heartbeat_response');
            expect(response.messageId).toBe('heartbeat_001');
            
            done();
        });
        
        client.on('error', done);
    });
    
    test('应该处理多个并发连接', async () => {
        const clients = [];
        const connectionPromises = [];
        
        // 创建多个客户端连接
        for (let i = 0; i < 5; i++) {
            const client = new WebSocket(`ws://localhost:${testPort}`);
            clients.push(client);
            
            const connectionPromise = new Promise((resolve, reject) => {
                client.on('open', resolve);
                client.on('error', reject);
            });
            
            connectionPromises.push(connectionPromise);
        }
        
        // 等待所有连接建立
        await Promise.all(connectionPromises);
        
        // 验证连接数
        expect(server.getConnections().length).toBe(5);
        
        // 关闭所有连接
        clients.forEach(client => client.close());
        
        // 等待连接关闭
        await new Promise(resolve => setTimeout(resolve, 100));
        
        expect(server.getConnections().length).toBe(0);
    });
    
    test('应该处理连接断开', (done) => {
        client = new WebSocket(`ws://localhost:${testPort}`);
        
        client.on('open', () => {
            expect(server.getConnections().length).toBe(1);
            client.close();
        });
        
        client.on('close', () => {
            // 等待服务器处理断开事件
            setTimeout(() => {
                expect(server.getConnections().length).toBe(0);
                done();
            }, 100);
        });
        
        client.on('error', done);
    });
});

Eagle 数据库集成测试

tests/integration/eagle-database.test.js

javascript
/**
 * Eagle 数据库集成测试
 */

const EagleDatabase = require('@/database/eagle-database');
const fs = require('fs-extra');
const path = require('path');

describe('Eagle 数据库集成测试', () => {
    let database;
    let mockLibraryPath;
    
    beforeAll(async () => {
        // 创建模拟的 Eagle 库
        mockLibraryPath = path.join(global.TEST_CONFIG.tempDir, 'mock-eagle-library');
        await createMockEagleLibrary(mockLibraryPath);
    });
    
    beforeEach(() => {
        database = new EagleDatabase(mockLibraryPath);
    });
    
    afterEach(async () => {
        if (database && database.isConnected()) {
            await database.disconnect();
        }
    });
    
    afterAll(async () => {
        await fs.remove(mockLibraryPath);
    });
    
    describe('连接管理', () => {
        test('应该成功连接到 Eagle 库', async () => {
            const result = await database.connect();
            
            expect(result.success).toBe(true);
            expect(database.isConnected()).toBe(true);
        });
        
        test('应该处理无效的库路径', async () => {
            const invalidDatabase = new EagleDatabase('/invalid/path');
            
            await expect(invalidDatabase.connect())
                .rejects.toThrow('Eagle 库路径无效');
        });
        
        test('应该成功断开连接', async () => {
            await database.connect();
            await database.disconnect();
            
            expect(database.isConnected()).toBe(false);
        });
    });
    
    describe('项目查询', () => {
        beforeEach(async () => {
            await database.connect();
        });
        
        test('应该获取所有项目', async () => {
            const items = await database.getAllItems();
            
            expect(Array.isArray(items)).toBe(true);
            expect(items.length).toBeGreaterThan(0);
            
            // 验证项目结构
            items.forEach(item => {
                expect(item).toHaveProperty('id');
                expect(item).toHaveProperty('name');
                expect(item).toHaveProperty('ext');
                expect(item).toHaveProperty('size');
                expect(item).toHaveProperty('filePath');
            });
        });
        
        test('应该根据 ID 获取项目', async () => {
            const allItems = await database.getAllItems();
            const firstItem = allItems[0];
            
            const item = await database.getItemById(firstItem.id);
            
            expect(item).toBeDefined();
            expect(item.id).toBe(firstItem.id);
            expect(item.name).toBe(firstItem.name);
        });
        
        test('应该处理不存在的项目 ID', async () => {
            const item = await database.getItemById('non-existent-id');
            expect(item).toBeNull();
        });
        
        test('应该搜索项目', async () => {
            const searchResults = await database.searchItems({
                keyword: 'test',
                ext: 'jpg'
            });
            
            expect(Array.isArray(searchResults)).toBe(true);
            
            // 验证搜索结果
            searchResults.forEach(item => {
                expect(
                    item.name.toLowerCase().includes('test') ||
                    item.ext.toLowerCase() === 'jpg'
                ).toBe(true);
            });
        });
    });
    
    describe('文件夹管理', () => {
        beforeEach(async () => {
            await database.connect();
        });
        
        test('应该获取所有文件夹', async () => {
            const folders = await database.getAllFolders();
            
            expect(Array.isArray(folders)).toBe(true);
            
            folders.forEach(folder => {
                expect(folder).toHaveProperty('id');
                expect(folder).toHaveProperty('name');
                expect(folder).toHaveProperty('children');
            });
        });
        
        test('应该获取文件夹中的项目', async () => {
            const folders = await database.getAllFolders();
            if (folders.length > 0) {
                const folderId = folders[0].id;
                const items = await database.getFolderItems(folderId);
                
                expect(Array.isArray(items)).toBe(true);
            }
        });
    });
    
    describe('标签管理', () => {
        beforeEach(async () => {
            await database.connect();
        });
        
        test('应该获取所有标签', async () => {
            const tags = await database.getAllTags();
            
            expect(Array.isArray(tags)).toBe(true);
            
            tags.forEach(tag => {
                expect(tag).toHaveProperty('id');
                expect(tag).toHaveProperty('name');
                expect(tag).toHaveProperty('color');
            });
        });
        
        test('应该根据标签获取项目', async () => {
            const tags = await database.getAllTags();
            if (tags.length > 0) {
                const tagId = tags[0].id;
                const items = await database.getItemsByTag(tagId);
                
                expect(Array.isArray(items)).toBe(true);
            }
        });
    });
});

/**
 * 创建模拟的 Eagle 库
 */
async function createMockEagleLibrary(libraryPath) {
    await fs.ensureDir(libraryPath);
    
    // 创建库信息文件
    const libraryInfo = {
        version: '3.0',
        name: 'Mock Eagle Library',
        created: Date.now()
    };
    
    await fs.writeJson(
        path.join(libraryPath, 'metadata.json'),
        libraryInfo
    );
    
    // 创建模拟数据库文件
    const mockData = {
        items: [
            {
                id: 'item-001',
                name: 'test-image-1',
                ext: 'jpg',
                size: 1024000,
                filePath: '/mock/path/test-image-1.jpg',
                tags: ['tag-001'],
                folderId: 'folder-001'
            },
            {
                id: 'item-002',
                name: 'test-image-2',
                ext: 'png',
                size: 2048000,
                filePath: '/mock/path/test-image-2.png',
                tags: ['tag-002'],
                folderId: 'folder-001'
            }
        ],
        folders: [
            {
                id: 'folder-001',
                name: 'Test Folder',
                children: []
            }
        ],
        tags: [
            {
                id: 'tag-001',
                name: 'Test Tag 1',
                color: '#FF0000'
            },
            {
                id: 'tag-002',
                name: 'Test Tag 2',
                color: '#00FF00'
            }
        ]
    };
    
    await fs.writeJson(
        path.join(libraryPath, 'data.json'),
        mockData
    );
}

端到端测试

完整工作流测试

tests/e2e/file-import-workflow.test.js

javascript
/**
 * 文件导入工作流端到端测试
 */

const EaglePlugin = require('@/plugin');
const WebSocket = require('ws');
const fs = require('fs-extra');
const path = require('path');

describe('文件导入工作流 E2E 测试', () => {
    let plugin;
    let client;
    let testFiles;
    
    beforeAll(async () => {
        // 创建测试文件
        testFiles = await createTestFiles();
        
        // 启动插件
        plugin = new EaglePlugin({
            port: 8082,
            eagleLibraryPath: path.join(global.TEST_CONFIG.tempDir, 'test-library')
        });
        
        await plugin.start();
    });
    
    afterAll(async () => {
        if (client) {
            client.close();
        }
        
        if (plugin) {
            await plugin.stop();
        }
        
        // 清理测试文件
        await cleanupTestFiles(testFiles);
    });
    
    test('完整的文件导入工作流', async () => {
        // 1. 建立 WebSocket 连接
        client = new WebSocket('ws://localhost:8082');
        
        await new Promise((resolve, reject) => {
            client.on('open', resolve);
            client.on('error', reject);
        });
        
        // 2. 查询插件状态
        const statusResponse = await sendMessage(client, {
            type: 'status_query',
            messageId: 'status_001',
            data: {}
        });
        
        expect(statusResponse.data.status).toBe('running');
        expect(statusResponse.data.eagleConnected).toBe(true);
        
        // 3. 获取选中的文件
        const selectedFilesResponse = await sendMessage(client, {
            type: 'eagle_selected_items',
            messageId: 'selected_001',
            data: {}
        });
        
        expect(selectedFilesResponse.data.items).toBeDefined();
        expect(Array.isArray(selectedFilesResponse.data.items)).toBe(true);
        
        // 4. 收集文件信息
        const fileInfoResponse = await sendMessage(client, {
            type: 'file_info_batch',
            messageId: 'fileinfo_001',
            data: {
                filePaths: testFiles.map(f => f.path)
            }
        });
        
        expect(fileInfoResponse.data.fileInfos).toBeDefined();
        expect(fileInfoResponse.data.fileInfos.length).toBe(testFiles.length);
        
        // 验证文件信息
        fileInfoResponse.data.fileInfos.forEach((fileInfo, index) => {
            expect(fileInfo.name).toBe(testFiles[index].name);
            expect(fileInfo.extension).toBe(testFiles[index].extension);
            expect(fileInfo.size).toBeGreaterThan(0);
        });
        
        // 5. 发送文件到 AE
        const sendToAeResponse = await sendMessage(client, {
            type: 'send_to_ae',
            messageId: 'sendae_001',
            data: {
                files: fileInfoResponse.data.fileInfos,
                importOptions: {
                    createComposition: true,
                    organizeFolders: true
                }
            }
        });
        
        expect(sendToAeResponse.data.success).toBe(true);
        expect(sendToAeResponse.data.importedCount).toBe(testFiles.length);
    }, 30000);
    
    test('处理文件导入错误', async () => {
        client = new WebSocket('ws://localhost:8082');
        
        await new Promise((resolve, reject) => {
            client.on('open', resolve);
            client.on('error', reject);
        });
        
        // 发送无效文件路径
        const errorResponse = await sendMessage(client, {
            type: 'file_info_batch',
            messageId: 'error_001',
            data: {
                filePaths: ['/invalid/path/file.jpg']
            }
        });
        
        expect(errorResponse.type).toBe('error');
        expect(errorResponse.data.code).toBe('FILE_NOT_FOUND');
    });
    
    test('处理大量文件的批量导入', async () => {
        client = new WebSocket('ws://localhost:8082');
        
        await new Promise((resolve, reject) => {
            client.on('open', resolve);
            client.on('error', reject);
        });
        
        // 创建大量测试文件
        const largeFileSet = await createLargeTestFileSet(50);
        
        try {
            const batchResponse = await sendMessage(client, {
                type: 'file_info_batch',
                messageId: 'batch_001',
                data: {
                    filePaths: largeFileSet.map(f => f.path)
                }
            }, 60000); // 增加超时时间
            
            expect(batchResponse.data.fileInfos.length).toBe(50);
            expect(batchResponse.data.processingTime).toBeLessThan(30000);
            
        } finally {
            // 清理大量测试文件
            await cleanupTestFiles(largeFileSet);
        }
    }, 90000);
});

/**
 * 发送消息并等待响应
 */
function sendMessage(client, message, timeout = 10000) {
    return new Promise((resolve, reject) => {
        const timer = setTimeout(() => {
            reject(new Error('消息响应超时'));
        }, timeout);
        
        const messageHandler = (data) => {
            const response = JSON.parse(data);
            if (response.messageId === message.messageId) {
                clearTimeout(timer);
                client.removeListener('message', messageHandler);
                resolve(response);
            }
        };
        
        client.on('message', messageHandler);
        client.send(JSON.stringify({
            ...message,
            timestamp: Date.now()
        }));
    });
}

/**
 * 创建测试文件
 */
async function createTestFiles() {
    const testDir = path.join(global.TEST_CONFIG.tempDir, 'test-files');
    await fs.ensureDir(testDir);
    
    const files = [
        { name: 'test-image-1.jpg', content: 'fake-jpg-content' },
        { name: 'test-image-2.png', content: 'fake-png-content' },
        { name: 'test-video.mp4', content: 'fake-mp4-content' }
    ];
    
    const createdFiles = [];
    
    for (const file of files) {
        const filePath = path.join(testDir, file.name);
        await fs.writeFile(filePath, file.content);
        
        createdFiles.push({
            name: file.name,
            path: filePath,
            extension: path.extname(file.name).slice(1)
        });
    }
    
    return createdFiles;
}

/**
 * 创建大量测试文件
 */
async function createLargeTestFileSet(count) {
    const testDir = path.join(global.TEST_CONFIG.tempDir, 'large-test-files');
    await fs.ensureDir(testDir);
    
    const files = [];
    
    for (let i = 0; i < count; i++) {
        const fileName = `test-file-${i.toString().padStart(3, '0')}.jpg`;
        const filePath = path.join(testDir, fileName);
        
        await fs.writeFile(filePath, `fake-content-${i}`);
        
        files.push({
            name: fileName,
            path: filePath,
            extension: 'jpg'
        });
    }
    
    return files;
}

/**
 * 清理测试文件
 */
async function cleanupTestFiles(files) {
    for (const file of files) {
        if (await fs.pathExists(file.path)) {
            await fs.remove(file.path);
        }
    }
}

性能测试

性能基准测试

tests/performance/websocket-performance.test.js

javascript
/**
 * WebSocket 性能测试
 */

const WebSocketServer = require('@/services/websocket-server');
const WebSocket = require('ws');

describe('WebSocket 性能测试', () => {
    let server;
    const testPort = 8083;
    
    beforeAll(async () => {
        server = new WebSocketServer({ port: testPort });
        await server.start();
    });
    
    afterAll(async () => {
        if (server) {
            await server.stop();
        }
    });
    
    test('并发连接性能测试', async () => {
        const connectionCount = 100;
        const clients = [];
        const startTime = Date.now();
        
        // 创建并发连接
        const connectionPromises = [];
        for (let i = 0; i < connectionCount; i++) {
            const client = new WebSocket(`ws://localhost:${testPort}`);
            clients.push(client);
            
            const promise = new Promise((resolve, reject) => {
                client.on('open', resolve);
                client.on('error', reject);
            });
            
            connectionPromises.push(promise);
        }
        
        await Promise.all(connectionPromises);
        const connectionTime = Date.now() - startTime;
        
        // 验证性能指标
        expect(connectionTime).toBeLessThan(5000); // 5秒内完成
        expect(server.getConnections().length).toBe(connectionCount);
        
        // 清理连接
        clients.forEach(client => client.close());
        
        console.log(`并发连接测试: ${connectionCount} 个连接在 ${connectionTime}ms 内建立`);
    }, 30000);
    
    test('消息吞吐量测试', async () => {
        const client = new WebSocket(`ws://localhost:${testPort}`);
        
        await new Promise((resolve, reject) => {
            client.on('open', resolve);
            client.on('error', reject);
        });
        
        const messageCount = 1000;
        const messages = [];
        const startTime = Date.now();
        
        // 发送大量消息
        for (let i = 0; i < messageCount; i++) {
            const message = {
                type: 'performance_test',
                messageId: `perf_${i}`,
                timestamp: Date.now(),
                data: { index: i }
            };
            
            client.send(JSON.stringify(message));
        }
        
        // 等待所有响应
        let receivedCount = 0;
        const responsePromise = new Promise((resolve) => {
            client.on('message', () => {
                receivedCount++;
                if (receivedCount === messageCount) {
                    resolve();
                }
            });
        });
        
        await responsePromise;
        const totalTime = Date.now() - startTime;
        const throughput = messageCount / (totalTime / 1000);
        
        // 验证性能指标
        expect(throughput).toBeGreaterThan(100); // 每秒至少100条消息
        expect(totalTime).toBeLessThan(10000); // 10秒内完成
        
        client.close();
        
        console.log(`消息吞吐量测试: ${messageCount} 条消息,耗时 ${totalTime}ms,吞吐量 ${throughput.toFixed(2)} msg/s`);
    }, 30000);
    
    test('内存使用测试', async () => {
        const initialMemory = process.memoryUsage();
        const clients = [];
        
        // 创建大量连接
        for (let i = 0; i < 50; i++) {
            const client = new WebSocket(`ws://localhost:${testPort}`);
            clients.push(client);
            
            await new Promise((resolve, reject) => {
                client.on('open', resolve);
                client.on('error', reject);
            });
        }
        
        // 发送大量消息
        for (const client of clients) {
            for (let i = 0; i < 100; i++) {
                client.send(JSON.stringify({
                    type: 'memory_test',
                    messageId: `mem_${i}`,
                    data: { payload: 'x'.repeat(1000) } // 1KB payload
                }));
            }
        }
        
        // 等待处理完成
        await new Promise(resolve => setTimeout(resolve, 2000));
        
        const peakMemory = process.memoryUsage();
        const memoryIncrease = peakMemory.heapUsed - initialMemory.heapUsed;
        
        // 清理连接
        clients.forEach(client => client.close());
        
        // 等待垃圾回收
        global.gc && global.gc();
        await new Promise(resolve => setTimeout(resolve, 1000));
        
        const finalMemory = process.memoryUsage();
        const memoryLeak = finalMemory.heapUsed - initialMemory.heapUsed;
        
        // 验证内存使用
        expect(memoryIncrease).toBeLessThan(100 * 1024 * 1024); // 100MB
        expect(memoryLeak).toBeLessThan(10 * 1024 * 1024); // 10MB 内存泄漏阈值
        
        console.log(`内存使用测试: 峰值增加 ${(memoryIncrease / 1024 / 1024).toFixed(2)}MB,最终泄漏 ${(memoryLeak / 1024 / 1024).toFixed(2)}MB`);
    }, 30000);
});

测试数据管理

测试数据管理器

tests/helpers/test-data-manager.js

javascript
/**
 * 测试数据管理器
 * 负责创建、管理和清理测试数据
 */

const fs = require('fs-extra');
const path = require('path');
const { v4: uuidv4 } = require('uuid');

class TestDataManager {
    constructor(baseDir = global.TEST_CONFIG?.tempDir) {
        this.baseDir = baseDir || path.join(__dirname, '../temp');
        this.createdFiles = new Set();
        this.createdDirs = new Set();
    }
    
    /**
     * 创建临时文件
     */
    async createTempFile(fileName, content = '', subDir = '') {
        const dir = subDir ? path.join(this.baseDir, subDir) : this.baseDir;
        await fs.ensureDir(dir);
        
        const filePath = path.join(dir, fileName);
        await fs.writeFile(filePath, content);
        
        this.createdFiles.add(filePath);
        return filePath;
    }
    
    /**
     * 创建临时目录
     */
    async createTempDir(dirName = uuidv4()) {
        const dirPath = path.join(this.baseDir, dirName);
        await fs.ensureDir(dirPath);
        
        this.createdDirs.add(dirPath);
        return dirPath;
    }
    
    /**
     * 创建测试图片文件
     */
    async createTestImage(fileName = 'test-image.jpg', size = 1024) {
        const content = Buffer.alloc(size, 0xFF); // 创建假的图片数据
        return this.createTempFile(fileName, content);
    }
    
    /**
     * 创建测试视频文件
     */
    async createTestVideo(fileName = 'test-video.mp4', size = 10240) {
        const content = Buffer.alloc(size, 0x00); // 创建假的视频数据
        return this.createTempFile(fileName, content);
    }
    
    /**
     * 创建 Eagle 库结构
     */
    async createMockEagleLibrary(libraryName = 'test-library') {
        const libraryDir = await this.createTempDir(libraryName);
        
        // 创建库元数据
        const metadata = {
            version: '3.0',
            name: libraryName,
            created: Date.now(),
            modified: Date.now()
        };
        
        await fs.writeJson(path.join(libraryDir, 'metadata.json'), metadata);
        
        // 创建数据库文件
        const dbData = {
            items: await this.generateMockItems(10),
            folders: await this.generateMockFolders(3),
            tags: await this.generateMockTags(5)
        };
        
        await fs.writeJson(path.join(libraryDir, 'data.json'), dbData);
        
        return {
            path: libraryDir,
            metadata,
            data: dbData
        };
    }
    
    /**
     * 生成模拟项目数据
     */
    async generateMockItems(count = 10) {
        const items = [];
        const extensions = ['jpg', 'png', 'gif', 'mp4', 'mov', 'pdf'];
        
        for (let i = 0; i < count; i++) {
            const ext = extensions[i % extensions.length];
            const item = {
                id: `item-${i.toString().padStart(3, '0')}`,
                name: `test-file-${i}`,
                ext,
                size: Math.floor(Math.random() * 10000000) + 1000,
                width: ext.includes('jpg|png|gif') ? 1920 : undefined,
                height: ext.includes('jpg|png|gif') ? 1080 : undefined,
                duration: ext.includes('mp4|mov') ? 30000 : undefined,
                filePath: `/mock/path/test-file-${i}.${ext}`,
                tags: [`tag-${Math.floor(Math.random() * 5)}`],
                folderId: `folder-${Math.floor(Math.random() * 3)}`,
                created: Date.now() - Math.floor(Math.random() * 86400000),
                modified: Date.now() - Math.floor(Math.random() * 3600000)
            };
            
            items.push(item);
        }
        
        return items;
    }
    
    /**
     * 生成模拟文件夹数据
     */
    async generateMockFolders(count = 3) {
        const folders = [];
        
        for (let i = 0; i < count; i++) {
            const folder = {
                id: `folder-${i}`,
                name: `Test Folder ${i + 1}`,
                description: `Description for folder ${i + 1}`,
                children: [],
                created: Date.now() - Math.floor(Math.random() * 86400000),
                modified: Date.now() - Math.floor(Math.random() * 3600000)
            };
            
            folders.push(folder);
        }
        
        return folders;
    }
    
    /**
     * 生成模拟标签数据
     */
    async generateMockTags(count = 5) {
        const tags = [];
        const colors = ['#FF0000', '#00FF00', '#0000FF', '#FFFF00', '#FF00FF'];
        
        for (let i = 0; i < count; i++) {
            const tag = {
                id: `tag-${i}`,
                name: `Test Tag ${i + 1}`,
                color: colors[i % colors.length],
                created: Date.now() - Math.floor(Math.random() * 86400000)
            };
            
            tags.push(tag);
        }
        
        return tags;
    }
    
    /**
     * 创建 WebSocket 测试消息
     */
    createTestMessage(type, data = {}, messageId = uuidv4()) {
        return {
            type,
            messageId,
            timestamp: Date.now(),
            data
        };
    }
    
    /**
     * 创建批量测试文件
     */
    async createBatchTestFiles(count = 10, fileType = 'image') {
        const files = [];
        
        for (let i = 0; i < count; i++) {
            let fileName, content;
            
            switch (fileType) {
                case 'image':
                    fileName = `batch-image-${i}.jpg`;
                    content = Buffer.alloc(1024, 0xFF);
                    break;
                case 'video':
                    fileName = `batch-video-${i}.mp4`;
                    content = Buffer.alloc(10240, 0x00);
                    break;
                case 'document':
                    fileName = `batch-doc-${i}.pdf`;
                    content = `Mock PDF content ${i}`;
                    break;
                default:
                    fileName = `batch-file-${i}.txt`;
                    content = `Test content ${i}`;
            }
            
            const filePath = await this.createTempFile(fileName, content, 'batch-files');
            files.push({
                name: fileName,
                path: filePath,
                type: fileType,
                size: Buffer.byteLength(content)
            });
        }
        
        return files;
    }
    
    /**
     * 清理所有创建的文件和目录
     */
    async cleanup() {
        // 清理文件
        for (const filePath of this.createdFiles) {
            try {
                if (await fs.pathExists(filePath)) {
                    await fs.remove(filePath);
                }
            } catch (error) {
                console.warn(`清理文件失败: ${filePath}`, error.message);
            }
        }
        
        // 清理目录
        for (const dirPath of this.createdDirs) {
            try {
                if (await fs.pathExists(dirPath)) {
                    await fs.remove(dirPath);
                }
            } catch (error) {
                console.warn(`清理目录失败: ${dirPath}`, error.message);
            }
        }
        
        // 清空记录
        this.createdFiles.clear();
        this.createdDirs.clear();
    }
    
    /**
     * 获取创建的文件列表
     */
    getCreatedFiles() {
        return Array.from(this.createdFiles);
    }
    
    /**
     * 获取创建的目录列表
     */
    getCreatedDirs() {
        return Array.from(this.createdDirs);
    }
}

module.exports = TestDataManager;

测试报告和覆盖率

测试报告生成器

tests/helpers/test-report-generator.js

javascript
/**
 * 测试报告生成器
 * 生成详细的测试报告和覆盖率分析
 */

const fs = require('fs-extra');
const path = require('path');

class TestReportGenerator {
    constructor(outputDir = 'test-reports') {
        this.outputDir = outputDir;
        this.testResults = [];
        this.coverageData = null;
    }
    
    /**
     * 添加测试结果
     */
    addTestResult(result) {
        this.testResults.push({
            ...result,
            timestamp: Date.now()
        });
    }
    
    /**
     * 设置覆盖率数据
     */
    setCoverageData(coverage) {
        this.coverageData = coverage;
    }
    
    /**
     * 生成 HTML 报告
     */
    async generateHtmlReport() {
        await fs.ensureDir(this.outputDir);
        
        const htmlContent = this.generateHtmlContent();
        const reportPath = path.join(this.outputDir, 'test-report.html');
        
        await fs.writeFile(reportPath, htmlContent);
        return reportPath;
    }
    
    /**
     * 生成 JSON 报告
     */
    async generateJsonReport() {
        await fs.ensureDir(this.outputDir);
        
        const report = {
            summary: this.generateSummary(),
            testResults: this.testResults,
            coverage: this.coverageData,
            generatedAt: new Date().toISOString()
        };
        
        const reportPath = path.join(this.outputDir, 'test-report.json');
        await fs.writeJson(reportPath, report, { spaces: 2 });
        
        return reportPath;
    }
    
    /**
     * 生成测试摘要
     */
    generateSummary() {
        const total = this.testResults.length;
        const passed = this.testResults.filter(r => r.status === 'passed').length;
        const failed = this.testResults.filter(r => r.status === 'failed').length;
        const skipped = this.testResults.filter(r => r.status === 'skipped').length;
        
        return {
            total,
            passed,
            failed,
            skipped,
            passRate: total > 0 ? (passed / total * 100).toFixed(2) : 0,
            duration: this.calculateTotalDuration()
        };
    }
    
    /**
     * 计算总执行时间
     */
    calculateTotalDuration() {
        return this.testResults.reduce((total, result) => {
            return total + (result.duration || 0);
        }, 0);
    }
    
    /**
     * 生成 HTML 内容
     */
    generateHtmlContent() {
        const summary = this.generateSummary();
        
        return `
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Eagle 插件测试报告</title>
    <style>
        body { font-family: Arial, sans-serif; margin: 20px; }
        .header { background: #f5f5f5; padding: 20px; border-radius: 5px; }
        .summary { display: flex; gap: 20px; margin: 20px 0; }
        .metric { background: white; padding: 15px; border-radius: 5px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
        .metric h3 { margin: 0 0 10px 0; color: #333; }
        .metric .value { font-size: 24px; font-weight: bold; }
        .passed { color: #28a745; }
        .failed { color: #dc3545; }
        .skipped { color: #ffc107; }
        .test-results { margin-top: 30px; }
        .test-item { background: white; margin: 10px 0; padding: 15px; border-radius: 5px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
        .test-item.failed { border-left: 4px solid #dc3545; }
        .test-item.passed { border-left: 4px solid #28a745; }
        .test-item.skipped { border-left: 4px solid #ffc107; }
        .coverage { margin-top: 30px; }
        .coverage-bar { background: #e9ecef; height: 20px; border-radius: 10px; overflow: hidden; }
        .coverage-fill { height: 100%; background: #28a745; transition: width 0.3s ease; }
    </style>
</head>
<body>
    <div class="header">
        <h1>Eagle 插件测试报告</h1>
        <p>生成时间: ${new Date().toLocaleString('zh-CN')}</p>
    </div>
    
    <div class="summary">
        <div class="metric">
            <h3>总测试数</h3>
            <div class="value">${summary.total}</div>
        </div>
        <div class="metric">
            <h3>通过</h3>
            <div class="value passed">${summary.passed}</div>
        </div>
        <div class="metric">
            <h3>失败</h3>
            <div class="value failed">${summary.failed}</div>
        </div>
        <div class="metric">
            <h3>跳过</h3>
            <div class="value skipped">${summary.skipped}</div>
        </div>
        <div class="metric">
            <h3>通过率</h3>
            <div class="value">${summary.passRate}%</div>
        </div>
        <div class="metric">
            <h3>执行时间</h3>
            <div class="value">${(summary.duration / 1000).toFixed(2)}s</div>
        </div>
    </div>
    
    ${this.coverageData ? this.generateCoverageHtml() : ''}
    
    <div class="test-results">
        <h2>测试结果详情</h2>
        ${this.testResults.map(result => this.generateTestItemHtml(result)).join('')}
    </div>
</body>
</html>
        `;
    }
    
    /**
     * 生成覆盖率 HTML
     */
    generateCoverageHtml() {
        if (!this.coverageData || !this.coverageData.total) {
            return '';
        }
        
        const { total } = this.coverageData;
        
        return `
    <div class="coverage">
        <h2>代码覆盖率</h2>
        <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 20px;">
            <div class="metric">
                <h3>行覆盖率</h3>
                <div class="coverage-bar">
                    <div class="coverage-fill" style="width: ${total.lines.pct}%"></div>
                </div>
                <div class="value">${total.lines.pct}%</div>
            </div>
            <div class="metric">
                <h3>函数覆盖率</h3>
                <div class="coverage-bar">
                    <div class="coverage-fill" style="width: ${total.functions.pct}%"></div>
                </div>
                <div class="value">${total.functions.pct}%</div>
            </div>
            <div class="metric">
                <h3>分支覆盖率</h3>
                <div class="coverage-bar">
                    <div class="coverage-fill" style="width: ${total.branches.pct}%"></div>
                </div>
                <div class="value">${total.branches.pct}%</div>
            </div>
            <div class="metric">
                <h3>语句覆盖率</h3>
                <div class="coverage-bar">
                    <div class="coverage-fill" style="width: ${total.statements.pct}%"></div>
                </div>
                <div class="value">${total.statements.pct}%</div>
            </div>
        </div>
    </div>
        `;
    }
    
    /**
     * 生成测试项 HTML
     */
    generateTestItemHtml(result) {
        return `
        <div class="test-item ${result.status}">
            <h3>${result.name}</h3>
            <p><strong>状态:</strong> ${result.status}</p>
            <p><strong>执行时间:</strong> ${result.duration || 0}ms</p>
            ${result.error ? `<p><strong>错误:</strong> <code>${result.error}</code></p>` : ''}
            ${result.description ? `<p><strong>描述:</strong> ${result.description}</p>` : ''}
        </div>
        `;
    }
}

module.exports = TestReportGenerator;

持续集成测试

GitHub Actions 配置

.github/workflows/test.yml

yaml
name: Eagle 插件测试

on:
  push:
    branches: [ main, develop ]
  pull_request:
    branches: [ main ]

jobs:
  test:
    runs-on: ubuntu-latest
    
    strategy:
      matrix:
        node-version: [14.x, 16.x, 18.x]
    
    steps:
    - name: 检出代码
      uses: actions/checkout@v3
    
    - name: 设置 Node.js ${{ matrix.node-version }}
      uses: actions/setup-node@v3
      with:
        node-version: ${{ matrix.node-version }}
        cache: 'npm'
    
    - name: 安装依赖
      run: npm ci
    
    - name: 运行代码检查
      run: npm run lint
    
    - name: 运行单元测试
      run: npm run test:unit
    
    - name: 运行集成测试
      run: npm run test:integration
    
    - name: 生成覆盖率报告
      run: npm run test:coverage
    
    - name: 上传覆盖率到 Codecov
      uses: codecov/codecov-action@v3
      with:
        file: ./coverage/lcov.info
        flags: unittests
        name: codecov-umbrella
    
    - name: 上传测试报告
      uses: actions/upload-artifact@v3
      if: always()
      with:
        name: test-reports-${{ matrix.node-version }}
        path: |
          coverage/
          test-reports/

更新记录

日期版本更新内容作者
2024-01-051.0初始 Eagle 插件测试规范文档开发团队

相关文档:

Released under the MIT License.