class RunTest extends HTMLElement { constructor() { super(); this.attachShadow({ mode: 'open' }); this.scenarioFiles = []; this.currentScenario = null; this.modelGroups = []; this.services = []; this.eventSource = null; } connectedCallback() { this.fetchScenarioFiles(); this.render(); } async fetchScenarioFiles() { try { const response = await fetch('/api/scenario-files'); if (!response.ok) { throw new Error('无法获取场景文件'); } this.scenarioFiles = await response.json(); this.updateScenarioDropdown(); } catch (error) { console.error('获取场景文件失败:', error); this.showError('获取场景文件失败: ' + error.message); } } updateScenarioDropdown() { const dropdown = this.shadowRoot.querySelector('#scenario-select'); if (!dropdown) return; // 清空现有选项 dropdown.innerHTML = ''; // 添加提示选项 const defaultOption = document.createElement('option'); defaultOption.value = ''; defaultOption.textContent = '-- 选择配置文件 --'; defaultOption.disabled = true; defaultOption.selected = true; dropdown.appendChild(defaultOption); // 添加场景文件选项 this.scenarioFiles.forEach(file => { const option = document.createElement('option'); option.value = file.path; option.textContent = file.name; dropdown.appendChild(option); }); } async onScenarioSelected(event) { const selectedScenarioPath = event.target.value; if (!selectedScenarioPath) { return; } try { this.showMessage('加载配置文件内容...'); // 使用file-content API获取XML内容 const response = await fetch(`/api/file-content?path=${encodeURIComponent(selectedScenarioPath)}`); if (!response.ok) { throw new Error('无法获取配置文件内容'); } const xmlContent = await response.text(); this.parseScenarioXML(xmlContent); this.currentScenario = selectedScenarioPath; this.updateModelAndServiceLists(); this.showMessage('配置文件加载完成'); } catch (error) { console.error('加载配置文件失败:', error); this.showError('加载配置文件失败: ' + error.message); } } parseScenarioXML(xmlContent) { try { const parser = new DOMParser(); const xmlDoc = parser.parseFromString(xmlContent, "text/xml"); // 解析模型组和模型 this.modelGroups = []; const modelGroupElements = xmlDoc.querySelectorAll('ModelGroup'); modelGroupElements.forEach(groupElem => { const groupName = groupElem.getAttribute('Name'); const models = []; groupElem.querySelectorAll('Model').forEach(modelElem => { models.push({ name: modelElem.getAttribute('Name'), className: modelElem.getAttribute('ClassName') }); }); this.modelGroups.push({ name: groupName, models: models }); }); // 解析服务 this.services = []; const serviceElements = xmlDoc.querySelectorAll('ServicesList > Service'); serviceElements.forEach(serviceElem => { this.services.push({ name: serviceElem.getAttribute('Name'), className: serviceElem.getAttribute('ClassName') }); }); } catch (error) { console.error('解析XML失败:', error); throw new Error('解析XML失败: ' + error.message); } } updateModelAndServiceLists() { // 更新模型列表 const modelListContainer = this.shadowRoot.querySelector('#model-list'); modelListContainer.innerHTML = ''; this.modelGroups.forEach(group => { const groupItem = document.createElement('div'); groupItem.className = 'list-group'; const groupHeader = document.createElement('div'); groupHeader.className = 'list-group-header'; groupHeader.textContent = group.name; groupItem.appendChild(groupHeader); const modelsList = document.createElement('div'); modelsList.className = 'list-items'; group.models.forEach(model => { const modelItem = document.createElement('div'); modelItem.className = 'list-item'; modelItem.textContent = model.name; modelsList.appendChild(modelItem); }); groupItem.appendChild(modelsList); modelListContainer.appendChild(groupItem); }); // 更新服务列表 const serviceListContainer = this.shadowRoot.querySelector('#service-list'); serviceListContainer.innerHTML = ''; this.services.forEach(service => { const serviceItem = document.createElement('div'); serviceItem.className = 'list-item'; serviceItem.textContent = service.name; serviceListContainer.appendChild(serviceItem); }); } async runTest() { const selectedScenario = this.shadowRoot.querySelector('#scenario-select').value; if (!selectedScenario) { this.showError('请先选择配置文件'); return; } try { this.showMessage('准备运行测试...'); const runButton = this.shadowRoot.querySelector('#run-button'); runButton.disabled = true; runButton.textContent = '运行中...'; // 清空并初始化输出框 const outputContent = this.shadowRoot.querySelector('#output-content'); outputContent.innerHTML = '开始执行测试...\n'; // 关闭之前的EventSource连接 if (this.eventSource) { this.eventSource.close(); this.eventSource = null; } // 准备启动参数 const simulationArgs = ['-f', selectedScenario, '-test']; // 调用后端API执行测试 const response = await fetch('/api/run-simulation', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ args: simulationArgs, timeout: 120000 // 2分钟超时 }) }); if (!response.ok) { const errorData = await response.json(); outputContent.innerHTML += `\n错误: ${errorData.message || '测试执行失败'}\n`; if (errorData.output) outputContent.innerHTML += this.convertAnsiToHtml(errorData.output); if (errorData.errorOutput) outputContent.innerHTML += this.convertAnsiToHtml(errorData.errorOutput); throw new Error(errorData.message || '测试执行失败'); } const responseData = await response.json(); // 连接到SSE获取实时输出 this.connectToEventSource(responseData.simulationId); // 根据测试结果更新UI if (responseData.success) { this.showSuccess('测试已启动'); } else { this.showError(`测试启动失败: ${responseData.message || '未知错误'}`); } } catch (error) { console.error('执行测试失败:', error); this.showError('执行测试失败: ' + error.message); // 显示错误详情 const outputContent = this.shadowRoot.querySelector('#output-content'); outputContent.innerHTML += `\n\n执行错误: ${error.message}`; } finally { // 重置UI const runButton = this.shadowRoot.querySelector('#run-button'); runButton.disabled = false; runButton.textContent = '运行测试'; } } // 连接到SSE事件源获取实时输出 connectToEventSource(simulationId) { // 关闭之前的连接 if (this.eventSource) { this.eventSource.close(); } // 创建新的SSE连接 const url = `/api/simulation-output/${simulationId}`; this.eventSource = new EventSource(url); // 标准输出和错误输出 this.eventSource.addEventListener('output', (event) => { try { const data = JSON.parse(event.data); const outputContent = this.shadowRoot.querySelector('#output-content'); // 添加新输出并应用ANSI颜色 if (data.data) { const html = this.convertAnsiToHtml(data.data); outputContent.innerHTML += html; // 自动滚动到底部 outputContent.scrollTop = outputContent.scrollHeight; } } catch (error) { console.error('处理输出事件失败:', error); } }); // 仿真状态 this.eventSource.addEventListener('status', (event) => { try { const data = JSON.parse(event.data); if (data.running === false) { // 测试已经结束 this.showMessage(data.message || '测试已结束'); this.closeEventSource(); } } catch (error) { console.error('处理状态事件失败:', error); } }); // 仿真完成 this.eventSource.addEventListener('completed', (event) => { try { const data = JSON.parse(event.data); if (data.success) { this.showSuccess('测试执行成功'); } else { this.showError(`测试执行失败: ${data.message}`); } this.closeEventSource(); } catch (error) { console.error('处理完成事件失败:', error); } }); // 仿真错误 this.eventSource.addEventListener('error', (event) => { try { const data = JSON.parse(event.data); this.showError(`测试错误: ${data.message}`); this.closeEventSource(); } catch (error) { console.error('处理错误事件失败:', error); } }); // 连接错误处理 this.eventSource.onerror = (error) => { console.error('SSE连接错误:', error); this.showError('实时输出连接已断开'); this.closeEventSource(); }; } // 关闭SSE连接 closeEventSource() { if (this.eventSource) { this.eventSource.close(); this.eventSource = null; } } showError(message) { const messageElement = this.shadowRoot.querySelector('#message'); messageElement.textContent = message; messageElement.style.color = 'red'; } showMessage(message) { const messageElement = this.shadowRoot.querySelector('#message'); messageElement.textContent = message; messageElement.style.color = 'blue'; } showSuccess(message) { const messageElement = this.shadowRoot.querySelector('#message'); messageElement.textContent = message; messageElement.style.color = 'green'; } render() { this.shadowRoot.innerHTML = `
选择运行环境配置:
模型列表
服务列表
运行输出
等待测试运行...
`; // 添加事件监听器 const runButton = this.shadowRoot.querySelector('#run-button'); runButton.addEventListener('click', () => this.runTest()); const scenarioSelect = this.shadowRoot.querySelector('#scenario-select'); scenarioSelect.addEventListener('change', (event) => this.onScenarioSelected(event)); } // ANSI终端颜色转换为HTML convertAnsiToHtml(text) { if (!text) return ''; // 映射ANSI颜色代码到CSS类名 const ansiToHtmlClass = { // 前景色 (30-37) 30: 'ansi-black', 31: 'ansi-red', 32: 'ansi-green', 33: 'ansi-yellow', 34: 'ansi-blue', 35: 'ansi-magenta', 36: 'ansi-cyan', 37: 'ansi-white', // 明亮前景色 (90-97) 90: 'ansi-bright-black', 91: 'ansi-bright-red', 92: 'ansi-bright-green', 93: 'ansi-bright-yellow', 94: 'ansi-bright-blue', 95: 'ansi-bright-magenta', 96: 'ansi-bright-cyan', 97: 'ansi-bright-white', // 背景色 (40-47) 40: 'ansi-bg-black', 41: 'ansi-bg-red', 42: 'ansi-bg-green', 43: 'ansi-bg-yellow', 44: 'ansi-bg-blue', 45: 'ansi-bg-magenta', 46: 'ansi-bg-cyan', 47: 'ansi-bg-white', // 文字样式 1: 'ansi-bold', 3: 'ansi-italic', 4: 'ansi-underline' }; // 替换ANSI转义序列 const parts = []; let currentSpan = null; let currentClasses = []; // 先分割ANSI序列和文本 const regex = /\x1b\[([\d;]*)m/g; let lastIndex = 0; let match; while ((match = regex.exec(text)) !== null) { // 添加匹配前的文本 if (match.index > lastIndex) { parts.push({ text: text.substring(lastIndex, match.index), classes: [...currentClasses] }); } // 处理ANSI代码 const codes = match[1].split(';').map(Number); // 重置所有样式 if (codes.includes(0) || codes.length === 0) { currentClasses = []; } else { // 添加样式 for (const code of codes) { if (ansiToHtmlClass[code]) { currentClasses.push(ansiToHtmlClass[code]); } } } lastIndex = regex.lastIndex; } // 添加最后一段文本 if (lastIndex < text.length) { parts.push({ text: text.substring(lastIndex), classes: [...currentClasses] }); } // 生成HTML let html = ''; for (const part of parts) { if (part.text) { if (part.classes.length > 0) { html += `${this.escapeHtml(part.text)}`; } else { html += this.escapeHtml(part.text); } } } return html; } // 转义HTML特殊字符 escapeHtml(text) { return text .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } } customElements.define('run-test', RunTest);