fix: 展示 JavaScript 代码节点执行错误
- 配置脚本执行超时并保留可定位的 JavaScript 异常信息 - 在试运行步骤和终态结果中展示并展开失败节点
This commit is contained in:
@@ -0,0 +1,39 @@
|
|||||||
|
package tech.easyflow.ai.config;
|
||||||
|
|
||||||
|
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JavaScript 代码执行引擎配置。
|
||||||
|
*/
|
||||||
|
@Configuration
|
||||||
|
@ConfigurationProperties(prefix = "node.code-engine.javascript")
|
||||||
|
public class JavascriptCodeEngineProps {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 单次脚本执行超时时间,单位毫秒。
|
||||||
|
*/
|
||||||
|
private long timeoutMs = 5000L;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取单次脚本执行超时时间。
|
||||||
|
*
|
||||||
|
* @return 超时时间,单位毫秒
|
||||||
|
*/
|
||||||
|
public long getTimeoutMs() {
|
||||||
|
return timeoutMs;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置单次脚本执行超时时间。
|
||||||
|
*
|
||||||
|
* @param timeoutMs 超时时间,单位毫秒
|
||||||
|
*/
|
||||||
|
public void setTimeoutMs(long timeoutMs) {
|
||||||
|
if (timeoutMs <= 0L) {
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
"node.code-engine.javascript.timeout-ms 必须大于 0");
|
||||||
|
}
|
||||||
|
this.timeoutMs = timeoutMs;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,6 +17,7 @@ import org.slf4j.LoggerFactory;
|
|||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
import tech.easyflow.ai.config.BochaaiProps;
|
import tech.easyflow.ai.config.BochaaiProps;
|
||||||
import tech.easyflow.ai.config.CodeEngineProps;
|
import tech.easyflow.ai.config.CodeEngineProps;
|
||||||
|
import tech.easyflow.ai.config.JavascriptCodeEngineProps;
|
||||||
import tech.easyflow.ai.easyagentsflow.code.PythonRuntimeEngine;
|
import tech.easyflow.ai.easyagentsflow.code.PythonRuntimeEngine;
|
||||||
import tech.easyflow.ai.easyagentsflow.code.UnavailableCodeRuntimeEngine;
|
import tech.easyflow.ai.easyagentsflow.code.UnavailableCodeRuntimeEngine;
|
||||||
import tech.easyflow.ai.node.*;
|
import tech.easyflow.ai.node.*;
|
||||||
@@ -42,6 +43,8 @@ public class TinyFlowConfigService {
|
|||||||
@Resource
|
@Resource
|
||||||
private CodeEngineProps codeEngineProps;
|
private CodeEngineProps codeEngineProps;
|
||||||
@Resource
|
@Resource
|
||||||
|
private JavascriptCodeEngineProps javascriptCodeEngineProps;
|
||||||
|
@Resource
|
||||||
private CodeEngineCapabilityService codeEngineCapabilityService;
|
private CodeEngineCapabilityService codeEngineCapabilityService;
|
||||||
|
|
||||||
public void initProvidersAndNodeParsers(ChainParser chainParser) {
|
public void initProvidersAndNodeParsers(ChainParser chainParser) {
|
||||||
@@ -107,6 +110,9 @@ public class TinyFlowConfigService {
|
|||||||
|
|
||||||
public void setCodeRuntimeEngineProvider() {
|
public void setCodeRuntimeEngineProvider() {
|
||||||
CodeRuntimeEngineManager manager = CodeRuntimeEngineManager.getInstance();
|
CodeRuntimeEngineManager manager = CodeRuntimeEngineManager.getInstance();
|
||||||
|
manager.configureJavascriptRuntimeEngine(javascriptCodeEngineProps.getTimeoutMs());
|
||||||
|
log.info("已配置 JavaScript 代码执行引擎: timeoutMs={}",
|
||||||
|
javascriptCodeEngineProps.getTimeoutMs());
|
||||||
|
|
||||||
PythonRuntimeEngine.ProbeResult probeResult;
|
PythonRuntimeEngine.ProbeResult probeResult;
|
||||||
String pythonCommand = codeEngineProps.getCommand();
|
String pythonCommand = codeEngineProps.getCommand();
|
||||||
|
|||||||
@@ -7,9 +7,11 @@ import com.easyagents.flow.core.chain.NodeStatus;
|
|||||||
import com.easyagents.flow.core.chain.repository.ChainStateRepository;
|
import com.easyagents.flow.core.chain.repository.ChainStateRepository;
|
||||||
import com.easyagents.flow.core.chain.repository.NodeStateRepository;
|
import com.easyagents.flow.core.chain.repository.NodeStateRepository;
|
||||||
import com.easyagents.flow.core.chain.runtime.ChainExecutor;
|
import com.easyagents.flow.core.chain.runtime.ChainExecutor;
|
||||||
|
import com.easyagents.flow.core.code.impl.JavascriptExecutionException;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
import tech.easyflow.ai.easyagentsflow.entity.ChainInfo;
|
import tech.easyflow.ai.easyagentsflow.entity.ChainInfo;
|
||||||
import tech.easyflow.ai.easyagentsflow.entity.NodeInfo;
|
import tech.easyflow.ai.easyagentsflow.entity.NodeInfo;
|
||||||
|
import tech.easyflow.common.util.StringUtil;
|
||||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||||
|
|
||||||
import javax.annotation.Resource;
|
import javax.annotation.Resource;
|
||||||
@@ -89,7 +91,7 @@ public class TinyFlowService {
|
|||||||
res.setStatus(chainState.getStatus().getValue());
|
res.setStatus(chainState.getStatus().getValue());
|
||||||
ExceptionSummary chainError = chainState.getError();
|
ExceptionSummary chainError = chainState.getError();
|
||||||
if (chainError != null) {
|
if (chainError != null) {
|
||||||
res.setMessage(chainError.getRootCauseClass() + " --> " + chainError.getRootCauseMessage());
|
res.setMessage(formatError(chainError));
|
||||||
}
|
}
|
||||||
Map<String, Object> executeResult = chainState.getExecuteResult();
|
Map<String, Object> executeResult = chainState.getExecuteResult();
|
||||||
if (executeResult != null && !executeResult.isEmpty()) {
|
if (executeResult != null && !executeResult.isEmpty()) {
|
||||||
@@ -118,7 +120,7 @@ public class TinyFlowService {
|
|||||||
if (nodeState != null) {
|
if (nodeState != null) {
|
||||||
ExceptionSummary error = nodeState.getError();
|
ExceptionSummary error = nodeState.getError();
|
||||||
if (error != null) {
|
if (error != null) {
|
||||||
node.setMessage(error.getRootCauseClass() + " --> " + error.getRootCauseMessage());
|
node.setMessage(formatError(error));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,4 +137,31 @@ public class TinyFlowService {
|
|||||||
node.setSuspendForParameters(chainState.getSuspendForParameters());
|
node.setSuspendForParameters(chainState.getSuspendForParameters());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将执行异常转换为试运行界面可读的错误信息。
|
||||||
|
*
|
||||||
|
* @param error 持久化的异常摘要
|
||||||
|
* @return 可展示的错误信息
|
||||||
|
*/
|
||||||
|
private String formatError(ExceptionSummary error) {
|
||||||
|
if (JavascriptExecutionException.class.getName()
|
||||||
|
.equals(error.getExceptionClass())
|
||||||
|
&& StringUtil.hasText(error.getMessage())) {
|
||||||
|
return error.getMessage();
|
||||||
|
}
|
||||||
|
String rootClass = StringUtil.hasText(error.getRootCauseClass())
|
||||||
|
? error.getRootCauseClass()
|
||||||
|
: error.getExceptionClass();
|
||||||
|
String rootMessage = StringUtil.hasText(error.getRootCauseMessage())
|
||||||
|
? error.getRootCauseMessage()
|
||||||
|
: error.getMessage();
|
||||||
|
if (StringUtil.noText(rootClass)) {
|
||||||
|
return rootMessage;
|
||||||
|
}
|
||||||
|
if (StringUtil.noText(rootMessage)) {
|
||||||
|
return rootClass;
|
||||||
|
}
|
||||||
|
return rootClass + " --> " + rootMessage;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
package tech.easyflow.ai.config;
|
||||||
|
|
||||||
|
import org.junit.Assert;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JavaScript 代码执行引擎配置测试。
|
||||||
|
*/
|
||||||
|
public class JavascriptCodeEnginePropsTest {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证正数超时配置可以正常写入。
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void shouldAcceptPositiveTimeout() {
|
||||||
|
JavascriptCodeEngineProps props =
|
||||||
|
new JavascriptCodeEngineProps();
|
||||||
|
|
||||||
|
props.setTimeoutMs(1000L);
|
||||||
|
|
||||||
|
Assert.assertEquals(1000L, props.getTimeoutMs());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证非正数超时配置会立即失败。
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void shouldRejectNonPositiveTimeout() {
|
||||||
|
assertInvalidTimeout(0L);
|
||||||
|
assertInvalidTimeout(-1L);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证指定超时值无法写入配置。
|
||||||
|
*
|
||||||
|
* @param timeoutMs 非法超时值
|
||||||
|
*/
|
||||||
|
private void assertInvalidTimeout(long timeoutMs) {
|
||||||
|
JavascriptCodeEngineProps props =
|
||||||
|
new JavascriptCodeEngineProps();
|
||||||
|
try {
|
||||||
|
props.setTimeoutMs(timeoutMs);
|
||||||
|
Assert.fail("非正数超时配置应执行失败");
|
||||||
|
} catch (IllegalArgumentException exception) {
|
||||||
|
Assert.assertTrue(exception.getMessage().contains(
|
||||||
|
"必须大于 0"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,11 +2,13 @@ package tech.easyflow.ai.easyagentsflow.service;
|
|||||||
|
|
||||||
import com.easyagents.flow.core.chain.ChainState;
|
import com.easyagents.flow.core.chain.ChainState;
|
||||||
import com.easyagents.flow.core.chain.ChainStatus;
|
import com.easyagents.flow.core.chain.ChainStatus;
|
||||||
|
import com.easyagents.flow.core.chain.ExceptionSummary;
|
||||||
import com.easyagents.flow.core.chain.NodeState;
|
import com.easyagents.flow.core.chain.NodeState;
|
||||||
import com.easyagents.flow.core.chain.NodeStatus;
|
import com.easyagents.flow.core.chain.NodeStatus;
|
||||||
import com.easyagents.flow.core.chain.repository.ChainStateRepository;
|
import com.easyagents.flow.core.chain.repository.ChainStateRepository;
|
||||||
import com.easyagents.flow.core.chain.repository.NodeStateRepository;
|
import com.easyagents.flow.core.chain.repository.NodeStateRepository;
|
||||||
import com.easyagents.flow.core.chain.runtime.ChainExecutor;
|
import com.easyagents.flow.core.chain.runtime.ChainExecutor;
|
||||||
|
import com.easyagents.flow.core.code.impl.JavascriptExecutionException;
|
||||||
import org.junit.Assert;
|
import org.junit.Assert;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import tech.easyflow.ai.easyagentsflow.entity.ChainInfo;
|
import tech.easyflow.ai.easyagentsflow.entity.ChainInfo;
|
||||||
@@ -137,6 +139,50 @@ public class TinyFlowServiceTest {
|
|||||||
.load(EXECUTE_ID, NODE_ID);
|
.load(EXECUTE_ID, NODE_ID);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证 JavaScript 执行错误使用面向试运行用户的定位信息。
|
||||||
|
*
|
||||||
|
* @throws Exception 测试依赖注入失败时抛出
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void shouldExposeJavascriptExecutionMessage()
|
||||||
|
throws Exception {
|
||||||
|
ChainExecutor chainExecutor = mock(ChainExecutor.class);
|
||||||
|
ChainStateRepository chainStateRepository =
|
||||||
|
mock(ChainStateRepository.class);
|
||||||
|
NodeStateRepository nodeStateRepository =
|
||||||
|
mock(NodeStateRepository.class);
|
||||||
|
String message =
|
||||||
|
"JavaScript 语法错误(第 2 行,第 3 列):Unexpected token";
|
||||||
|
ExceptionSummary error = new ExceptionSummary(
|
||||||
|
new JavascriptExecutionException(
|
||||||
|
message,
|
||||||
|
new IllegalArgumentException("raw parser error")));
|
||||||
|
ChainState chainState = new ChainState();
|
||||||
|
chainState.setStatus(ChainStatus.FAILED);
|
||||||
|
chainState.setError(error);
|
||||||
|
NodeState nodeState = new NodeState();
|
||||||
|
nodeState.setStatus(NodeStatus.FAILED);
|
||||||
|
nodeState.setError(error);
|
||||||
|
when(chainExecutor.getChainStateRepository())
|
||||||
|
.thenReturn(chainStateRepository);
|
||||||
|
when(chainExecutor.getNodeStateRepository())
|
||||||
|
.thenReturn(nodeStateRepository);
|
||||||
|
when(chainStateRepository.load(EXECUTE_ID))
|
||||||
|
.thenReturn(chainState);
|
||||||
|
when(nodeStateRepository.load(EXECUTE_ID, NODE_ID))
|
||||||
|
.thenReturn(nodeState);
|
||||||
|
TinyFlowService service = service(chainExecutor);
|
||||||
|
|
||||||
|
ChainInfo result = service.getChainStatus(
|
||||||
|
EXECUTE_ID, List.of(node(NodeStatus.READY)));
|
||||||
|
|
||||||
|
Assert.assertEquals(message, result.getMessage());
|
||||||
|
Assert.assertEquals(
|
||||||
|
message,
|
||||||
|
result.getNodes().get(NODE_ID).getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 创建带指定初始状态的设计器节点。
|
* 创建带指定初始状态的设计器节点。
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -37,11 +37,15 @@ watch(
|
|||||||
() => props.initSignal,
|
() => props.initSignal,
|
||||||
() => {
|
() => {
|
||||||
result.value = '';
|
result.value = '';
|
||||||
|
success.value = false;
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
watch(
|
watch(
|
||||||
() => props.pollingData,
|
() => props.pollingData,
|
||||||
(newVal) => {
|
(newVal) => {
|
||||||
|
if (!newVal) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (newVal.status === 20) {
|
if (newVal.status === 20) {
|
||||||
ElMessage.success($t('message.success'));
|
ElMessage.success($t('message.success'));
|
||||||
result.value = newVal.result;
|
result.value = newVal.result;
|
||||||
@@ -83,7 +87,7 @@ function getResult(res: any) {
|
|||||||
</template>
|
</template>
|
||||||
</ElRow>
|
</ElRow>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="finalNode.type !== 'endNode' && !success">
|
<div v-if="!success && result">
|
||||||
<ShowJson :value="result" />
|
<ShowJson :value="result" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import {
|
|||||||
CircleCloseFilled,
|
CircleCloseFilled,
|
||||||
SuccessFilled,
|
SuccessFilled,
|
||||||
VideoPause,
|
VideoPause,
|
||||||
WarningFilled,
|
|
||||||
} from '@element-plus/icons-vue';
|
} from '@element-plus/icons-vue';
|
||||||
import {
|
import {
|
||||||
ElAlert,
|
ElAlert,
|
||||||
@@ -35,7 +34,6 @@ const props = defineProps<WorkflowStepsProps>();
|
|||||||
const emit = defineEmits(['resume']);
|
const emit = defineEmits(['resume']);
|
||||||
const nodes = ref<any[]>([]);
|
const nodes = ref<any[]>([]);
|
||||||
const nodeStatusMap = ref<Record<string, any>>({});
|
const nodeStatusMap = ref<Record<string, any>>({});
|
||||||
const isChainError = ref(false);
|
|
||||||
const activeNames = ref<string[]>([]);
|
const activeNames = ref<string[]>([]);
|
||||||
const userControlledExpansion = ref(false);
|
const userControlledExpansion = ref(false);
|
||||||
const confirmParams = ref<any>({});
|
const confirmParams = ref<any>({});
|
||||||
@@ -91,13 +89,14 @@ watch(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const currentNodes = newVal.nodes || {};
|
const currentNodes = newVal.nodes || {};
|
||||||
if (newVal.status === 21) {
|
chainErrMsg.value = newVal.status === 21 ? newVal.message : '';
|
||||||
isChainError.value = true;
|
|
||||||
chainErrMsg.value = newVal.message;
|
|
||||||
}
|
|
||||||
if (![20, 21].includes(newVal.status)) {
|
if (![20, 21].includes(newVal.status)) {
|
||||||
confirmBtnLoading.value = false;
|
confirmBtnLoading.value = false;
|
||||||
}
|
}
|
||||||
|
let autoExpandNodeId: string | undefined;
|
||||||
|
const failedNodeId = Object.keys(currentNodes).find(
|
||||||
|
(nodeId) => Number(currentNodes[nodeId]?.status) === 21,
|
||||||
|
);
|
||||||
for (const nodeId in currentNodes) {
|
for (const nodeId in currentNodes) {
|
||||||
const previousNodeState = nodeStatusMap.value[nodeId];
|
const previousNodeState = nodeStatusMap.value[nodeId];
|
||||||
const currentNodeState = currentNodes[nodeId];
|
const currentNodeState = currentNodes[nodeId];
|
||||||
@@ -113,7 +112,13 @@ watch(
|
|||||||
previousStatus !== currentStatus &&
|
previousStatus !== currentStatus &&
|
||||||
shouldAutoExpandStatus(currentStatus)
|
shouldAutoExpandStatus(currentStatus)
|
||||||
) {
|
) {
|
||||||
activeNames.value = [nodeId];
|
autoExpandNodeId = nodeId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!userControlledExpansion.value && !props.expandAll) {
|
||||||
|
const nextActiveNodeId = failedNodeId || autoExpandNodeId;
|
||||||
|
if (nextActiveNodeId) {
|
||||||
|
activeNames.value = [nextActiveNodeId];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -123,7 +128,6 @@ watch(
|
|||||||
() => props.initSignal,
|
() => props.initSignal,
|
||||||
() => {
|
() => {
|
||||||
nodeStatusMap.value = {};
|
nodeStatusMap.value = {};
|
||||||
isChainError.value = false;
|
|
||||||
confirmBtnLoading.value = false;
|
confirmBtnLoading.value = false;
|
||||||
chainErrMsg.value = '';
|
chainErrMsg.value = '';
|
||||||
userControlledExpansion.value = false;
|
userControlledExpansion.value = false;
|
||||||
@@ -191,7 +195,13 @@ function handleConfirm(node: any) {
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div class="mb-1">
|
<div class="mb-1">
|
||||||
<ElAlert v-if="chainErrMsg" :title="chainErrMsg" type="error" />
|
<ElAlert
|
||||||
|
v-if="chainErrMsg"
|
||||||
|
:closable="false"
|
||||||
|
show-icon
|
||||||
|
:title="chainErrMsg"
|
||||||
|
type="error"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<ElCollapse
|
<ElCollapse
|
||||||
v-model="activeNames"
|
v-model="activeNames"
|
||||||
@@ -210,31 +220,16 @@ function handleConfirm(node: any) {
|
|||||||
{{ node.label }}
|
{{ node.label }}
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<ElIcon
|
<ElIcon v-if="node.status === 20" color="green" size="20">
|
||||||
v-if="node.status === 20 && !isChainError"
|
|
||||||
color="green"
|
|
||||||
size="20"
|
|
||||||
>
|
|
||||||
<SuccessFilled />
|
<SuccessFilled />
|
||||||
</ElIcon>
|
</ElIcon>
|
||||||
<div v-if="node.status === 1" class="spinner"></div>
|
<div v-if="node.status === 1" class="spinner"></div>
|
||||||
<ElIcon
|
<ElIcon v-if="node.status === 21" color="red" size="20">
|
||||||
v-if="node.status === 21 && !isChainError"
|
|
||||||
color="red"
|
|
||||||
size="20"
|
|
||||||
>
|
|
||||||
<CircleCloseFilled />
|
<CircleCloseFilled />
|
||||||
</ElIcon>
|
</ElIcon>
|
||||||
<ElIcon
|
<ElIcon v-if="node.status === 5" color="orange" size="20">
|
||||||
v-if="node.status === 5 && !isChainError"
|
|
||||||
color="orange"
|
|
||||||
size="20"
|
|
||||||
>
|
|
||||||
<VideoPause />
|
<VideoPause />
|
||||||
</ElIcon>
|
</ElIcon>
|
||||||
<ElIcon v-if="isChainError" color="orange" size="20">
|
|
||||||
<WarningFilled />
|
|
||||||
</ElIcon>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import { mount } from '@vue/test-utils';
|
||||||
|
|
||||||
|
import { describe, expect, it, vi } from 'vitest';
|
||||||
|
|
||||||
|
import ExecResult from '../ExecResult.vue';
|
||||||
|
|
||||||
|
vi.mock('#/locales', () => ({
|
||||||
|
$t: (key: string) => key,
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('execResult', () => {
|
||||||
|
it('结束节点执行失败时展示工作流错误信息', async () => {
|
||||||
|
const wrapper = mount(ExecResult, {
|
||||||
|
props: {
|
||||||
|
initSignal: false,
|
||||||
|
nodeJson: [
|
||||||
|
{
|
||||||
|
original: {
|
||||||
|
data: {
|
||||||
|
outputDefs: [],
|
||||||
|
},
|
||||||
|
type: 'endNode',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
pollingData: undefined,
|
||||||
|
workflowId: 'workflow-1',
|
||||||
|
},
|
||||||
|
global: {
|
||||||
|
stubs: {
|
||||||
|
ShowJson: {
|
||||||
|
props: ['value'],
|
||||||
|
template: '<div data-test="show-json">{{ value }}</div>',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await wrapper.setProps({
|
||||||
|
pollingData: {
|
||||||
|
message: 'JavaScript 执行失败(第 3 行,第 5 列):boom',
|
||||||
|
status: 21,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(wrapper.get('[data-test="show-json"]').text()).toContain(
|
||||||
|
'JavaScript 执行失败(第 3 行,第 5 列):boom',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -134,4 +134,29 @@ describe('workflowSteps', () => {
|
|||||||
});
|
});
|
||||||
expect(getCollapseItems(wrapper)[0]?.classes()).toContain('is-active');
|
expect(getCollapseItems(wrapper)[0]?.classes()).toContain('is-active');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('工作流失败时优先展开失败节点并展示错误信息', async () => {
|
||||||
|
const wrapper = mountWorkflowSteps();
|
||||||
|
|
||||||
|
await wrapper.setProps({
|
||||||
|
pollingData: {
|
||||||
|
message: 'JavaScript 语法错误(第 2 行,第 3 列):Unexpected token',
|
||||||
|
nodes: {
|
||||||
|
'node-a': { result: { text: 'A 完成' }, status: 20 },
|
||||||
|
'node-b': {
|
||||||
|
message:
|
||||||
|
'JavaScript 语法错误(第 2 行,第 3 列):Unexpected token',
|
||||||
|
status: 21,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
status: 21,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(getCollapseItems(wrapper)[0]?.classes()).not.toContain('is-active');
|
||||||
|
expect(getCollapseItems(wrapper)[1]?.classes()).toContain('is-active');
|
||||||
|
expect(wrapper.text()).toContain(
|
||||||
|
'JavaScript 语法错误(第 2 行,第 3 列):Unexpected token',
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user