fix(测试跟踪): 测试计划报告导出UI报告无法展示

This commit is contained in:
chenjianxing 2022-10-13 11:00:33 +08:00 committed by jianxing
parent 09a755ebde
commit 804a9c9037
20 changed files with 3654 additions and 9 deletions

View File

@ -20,7 +20,8 @@ import javax.servlet.http.HttpServletRequest;
"/api/project",
"/test/case/relevance/api",
"/test/case/relevance/scenario",
"home"
"/share/api/definition",
"/home"
})
public class TrackApiTestController {
@Resource

View File

@ -0,0 +1,21 @@
import {get, post} from "metersphere-frontend/src/plugins/request"
export function getScenarioReport(reportId) {
return reportId ? get('/ui/scenario/report/get/' + reportId) : {};
}
export function getScenarioReportAll(reportId) {
return reportId ? get('/ui/scenario/report/getAll/' + reportId) : {};
}
export function getApiReport(testId) {
return testId ? get('/api/definition/report/getReport/' + testId) : {};
}
export function getShareApiReport(shareId, testId) {
return testId ? get('/share/api/definition/report/getReport/' + shareId + '/' + testId) : {};
}
export function getShareScenarioReport(shareId, reportId) {
return reportId ? get('/share/ui/scenario/report/get/' + shareId + '/' + reportId) : {};
}

View File

@ -49,10 +49,11 @@
</el-card>
</ms-aside-container>
<el-main>
<micro-app v-if="showResponse"
route-name="ApiReportView"
service="ui"
:route-params="{
<div v-if="showResponse">
<micro-app v-if="!isTemplate"
service="ui"
route-name="ApiReportView"
:route-params="{
reportId,
isShare,
shareId,
@ -60,13 +61,23 @@
isTemplate,
response
}"/>
<UiShareReportDetail
v-else
:report-id="reportId"
:share-id="shareId"
:is-share="isShare"
:template-report="response"
:is-template="true"
:is-plan="true"
:show-cancel-button="false"/>
</div>
<div class="empty" v-else>{{ $t('test_track.plan.load_case.content_empty') }}</div>
</el-main>
</el-container>
</template>
<script>
import UiShareReportDetail from "../ui/UiShareReportDetail"
import PriorityTableItem from "../../../../../../common/tableItems/planview/PriorityTableItem";
import TypeTableItem from "../../../../../../common/tableItems/planview/TypeTableItem";
import MethodTableItem from "../../../../../../common/tableItems/planview/MethodTableItem";
@ -85,7 +96,8 @@ export default {
MsMainContainer,
MsAsideContainer,
MicroApp,
MsTableColumn, MsTable, StatusTableItem, MethodTableItem, TypeTableItem, PriorityTableItem
MsTableColumn, MsTable, StatusTableItem, MethodTableItem, TypeTableItem, PriorityTableItem,
UiShareReportDetail
},
props: {
planId: String,

View File

@ -0,0 +1,219 @@
<template>
<header class="report-header">
<el-row>
<el-col>
<span v-if="!debug">
<el-input v-if="nameIsEdit" size="mini" @blur="handleSave(report.name)" @keyup.enter.native="handleSaveKeyUp"
style="width: 200px" v-model="report.name" maxlength="60" show-word-limit/>
<span v-else>
<router-link v-if="isSingleScenario"
:to="{name: isUi ? 'uiAutomation' : 'ApiAutomation', params: { dataSelectRange: 'edit:' + scenarioId }}">
{{ report.name }}
</router-link>
<span v-else>
{{ report.name }}
</span>
<i v-if="showCancelButton" class="el-icon-edit" style="cursor:pointer" @click="nameIsEdit = true"
@click.stop/>
</span>
</span>
<span v-if="report.endTime || report.createTime">
<span style="margin-left: 10px">{{ $t('report.test_start_time') }}</span>
<span class="time"> {{ report.createTime | timestampFormatDate }}</span>
<span style="margin-left: 10px">{{ $t('report.test_end_time') }}</span>
<span class="time"> {{ report.endTime | timestampFormatDate }}</span>
</span>
<div style="float: right">
<el-button v-if="!isPlan && (!debug || exportFlag) && !isTemplate && !isUi"
v-permission="['PROJECT_API_REPORT:READ+EXPORT']" :disabled="isReadOnly" class="export-button"
plain type="primary" size="mini" @click="handleExport(report.name)" style="margin-right: 10px">
{{ $t('test_track.plan_view.export_report') }}
</el-button>
<el-popover
v-if="!isPlan && (!debug || exportFlag) && !isTemplate"
v-permission="['PROJECT_PERFORMANCE_REPORT:READ+EXPORT']"
style="margin-right: 10px;float: right;"
placement="bottom"
width="300">
<p>{{ shareUrl }}</p>
<span style="color: red;float: left;margin-left: 10px;" v-if="application.typeValue">{{
$t('commons.validity_period') + application.typeValue
}}</span>
<div style="text-align: right; margin: 0">
<el-button type="primary" size="mini" :disabled="!shareUrl"
v-clipboard:copy="shareUrl">{{ $t("commons.copy") }}
</el-button>
</div>
<el-button slot="reference" :disabled="isReadOnly" type="danger" plain size="mini"
@click="handleShare(report)">
{{ $t('test_track.plan_view.share_report') }}
</el-button>
</el-popover>
<el-button v-if="showRerunButton" class="rerun-button" plain size="mini" @click="rerun">
{{ $t('api_test.automation.rerun') }}
</el-button>
<el-button v-if="showCancelButton" class="export-button" plain size="mini" @click="returnView">
{{ $t('commons.cancel') }}
</el-button>
</div>
</el-col>
</el-row>
<el-row v-if="showProjectEnv" type="flex">
<span> {{ $t('commons.environment') + ':' }} </span>
<div v-for="(values,key) in projectEnvMap" :key="key" style="margin-right: 10px">
{{ key + ":" }}
<ms-tag v-for="(item,index) in values" :key="index" type="success" :content="item"
style="margin-left: 2px"/>
</div>
</el-row>
</header>
</template>
<script>
import {generateShareInfoWithExpired, getShareRedirectUrl} from "@/api/share";
import MsTag from "metersphere-frontend/src/components/MsTag";
import {getCurrentProjectID} from "@/business/utils/sdk-utils";
export default {
name: "MsApiReportViewHeader",
components: {MsTag},
props: {
report: {},
projectEnvMap: {},
debug: Boolean,
showCancelButton: {
type: Boolean,
default: true,
},
showRerunButton: {
type: Boolean,
default: false,
},
isTemplate: Boolean,
exportFlag: {
type: Boolean,
default: false,
},
isPlan: Boolean
},
computed: {
showProjectEnv() {
return this.projectEnvMap && JSON.stringify(this.projectEnvMap) !== '{}';
},
path() {
return "/api/test/edit?id=" + this.report.testId;
},
scenarioId() {
if (typeof this.report.scenarioId === 'string') {
return this.report.scenarioId;
} else {
return "";
}
},
isSingleScenario() {
try {
JSON.parse(this.report.scenarioId);
return false;
} catch (e) {
return true;
}
},
isUi() {
return this.report.reportType && this.report.reportType.startsWith("UI");
},
},
data() {
return {
isReadOnly: false,
nameIsEdit: false,
shareUrl: "",
application: {}
}
},
created() {
},
methods: {
handleExport(name) {
this.$emit('reportExport', name);
},
handleSave(name) {
this.nameIsEdit = false;
this.$emit('reportSave', name);
},
handleSaveKeyUp($event) {
$event.target.blur();
},
rerun() {
let type = this.report.reportType;
let rerunObj = {type: type, reportId: this.report.id}
this.$post('/api/test/exec/rerun', rerunObj, res => {
if (res.data !== 'SUCCESS') {
this.$error(res.data);
} else {
this.$success(this.$t('api_test.automation.rerun_success'));
this.returnView();
}
});
},
returnView() {
if (this.isUi) {
this.$router.push('/ui/report');
} else {
this.$router.push('/api/automation/report');
}
},
handleShare(report) {
this.getProjectApplication();
let pram = {};
pram.customData = report.id;
pram.shareType = 'UI_REPORT';
let thisHost = window.location.host;
generateShareInfoWithExpired(pram).then((res) => {
this.shareUrl = getShareRedirectUrl(res.data);
});
},
getProjectApplication() {
let path = "/API_SHARE_REPORT_TIME";
if(this.isUi){
path = "/UI_SHARE_REPORT_TIME";
}
this.$get('/project_application/get/' + getCurrentProjectID() + path).then(res => {
if (res.data && res.data.typeValue) {
let quantity = res.data.typeValue.substring(0, res.data.typeValue.length - 1);
let unit = res.data.typeValue.substring(res.data.typeValue.length - 1);
if (unit === 'H') {
res.data.typeValue = quantity + this.$t('commons.date_unit.hour');
} else if (unit === 'D') {
res.data.typeValue = quantity + this.$t('commons.date_unit.day');
} else if (unit === 'M') {
res.data.typeValue = quantity + this.$t('commons.workspace_unit') + this.$t('commons.date_unit.month');
} else if (unit === 'Y') {
res.data.typeValue = quantity + this.$t('commons.date_unit.year');
}
this.application = res.data;
}
});
},
}
}
</script>
<style scoped>
.export-button {
float: right;
margin-right: 10px;
}
.rerun-button {
float: right;
margin-right: 10px;
background-color: #F2F9EF;
color: #87C45D;
}
</style>

View File

@ -0,0 +1,99 @@
<template>
<el-table :data="assertions" :row-style="getRowStyle" :header-cell-style="getRowStyle">
<el-table-column prop="name" :label="$t('api_report.assertions_name')" width="150" show-overflow-tooltip>
<template v-slot:default="scope">
<span>{{ !scope.row.name || scope.row.name === 'null' ? "" : scope.row.name }}</span>
</template>
</el-table-column>
<el-table-column prop="content" v-if="showContent" :label="$t('api_report.assertions_content')" width="300" show-overflow-tooltip/>
<el-table-column prop="message" :label="$t('api_report.assertions_error_message')"/>
<el-table-column prop="pass" :label="$t('api_report.assertions_is_success')" width="180">
<template v-slot:default="{row}">
<el-tag size="mini" type="success" v-if="row.pass">
{{ $t('api_report.success') }}
</el-tag>
<el-tag size="mini" type="danger" v-else>
{{ $t('api_report.fail') }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="script">
<template v-slot:default="{row}">
<div class="assertion-item btn circle" v-if="row.script">
<i class="el-icon-view el-button el-button--primary el-button--mini is-circle" circle
@click="showPage(row.script)"/>
</div>
</template>
</el-table-column>
<el-dialog :title="$t('api_test.request.assertions.script')" :visible.sync="visible" width="900px" append-to-body>
<el-row type="flex" justify="space-between" align="middle" class="quick-script-block">
<el-col :span="codeSpan" class="script-content">
<ms-code-edit v-if="isCodeEditAlive"
:read-only="disabled"
:data.sync="scriptContent" theme="eclipse" :modes="['java','python']"
ref="codeEdit"/>
</el-col>
</el-row>
</el-dialog>
</el-table>
</template>
<script>
import MsCodeEdit from "metersphere-frontend/src/components/MsCodeEdit";
export default {
name: "MsAssertionResults",
components: {MsCodeEdit},
props: {
assertions: Array,
showContent: {
type: Boolean,
default: true
}
},
data() {
return {
visible: false,
disabled: false,
codeSpan: 20,
isCodeEditAlive: true,
scriptContent: '',
}
},
methods: {
getRowStyle() {
return {backgroundColor: "#F5F5F5"};
},
showPage(script) {
this.disabled = true;
this.visible = true;
this.scriptContent = script;
this.reload();
},
reload() {
this.isCodeEditAlive = false;
this.$nextTick(() => (this.isCodeEditAlive = true));
},
},
}
</script>
<style scoped>
.assertion-item.btn.circle {
text-align: right;
min-width: 80px;
}
.script-content {
height: calc(100vh - 570px);
min-height: 440px;
}
.quick-script-block {
margin-bottom: 10px;
}
</style>

View File

@ -0,0 +1,481 @@
<template>
<div class="metric-container">
<el-row type="flex" align="middle">
<div style="width: 50%">
<el-row type="flex" justify="center" align="middle">
<el-row>
<div class="metric-time">
<div class="value" style="margin-right: 50px">{{ time }}</div>
</div>
</el-row>
<div v-if="isExport">
<span class="ms-req ms-req-error"
v-if="(content.error && content.error>0 )|| (content.errorCode && content.errorCode>0)|| (content.unExecute && content.unExecute>0)">
<span class="ms-req-span"> {{ content.success + content.error + content.errorCode + content.unExecute }} {{
isUi ? '指令' : $t('api_report.request')
}}</span>
</span>
<span class="ms-req ms-req-success" v-else>
<span class="ms-req-span"> {{
content.success ? content.success + content.error : 0
}} {{ isUi ? '指令' : $t('api_report.request') }}</span>
</span>
</div>
<ms-chart id="chart" ref="chart" :options="options" :height="220" style="margin-right: 10px"
:autoresize="true" v-else/>
<el-row type="flex" justify="center" align="middle" style="width: 150px">
<div>
<div class="metric-icon-box" style="height: 26px">
<span class="ms-point-success" style="margin: 7px;float: left;"/>
<div class="metric-box">
<div class="value" style="font-size: 12px">{{ content.success }} {{ $t('api_report.success') }}</div>
</div>
</div>
<el-divider></el-divider>
<div class="metric-icon-box" style="height: 26px">
<span class="ms-point-error" style="margin: 7px;float: left;"/>
<div class="metric-box">
<div class="value" style="font-size: 12px">{{ content.error }} {{ $t('api_report.fail') }}</div>
</div>
</div>
<el-divider v-if="content.errorCode > 0"></el-divider>
<div class="metric-icon-box" v-if="content.errorCode > 0" style="height: 26px">
<span class="ms-point-error-code" style="margin: 7px;float: left;"/>
<div class="metric-box" v-if="content.errorCode > 0">
<div class="value" style="font-size: 12px">{{ content.errorCode }}
{{ $t('error_report_library.option.name') }}
</div>
</div>
</div>
<el-divider v-if="content.unExecute > 0"></el-divider>
<div class="metric-icon-box" style="height: 26px" v-if="content.unExecute > 0">
<span class="ms-point-unexecute" style="margin: 7px;float: left;"/>
<div class="metric-box">
<div class="value" style="font-size: 12px">{{ content.unExecute }}
{{ $t('api_test.home_page.detail_card.unexecute') }}
</div>
</div>
</div>
</div>
</el-row>
</el-row>
</div>
<div class="split"></div>
<!-- 场景统计 -->
<div style="width: 50%">
<el-row type="flex" justify="center" align="middle" v-if="report.reportType !== 'API_INTEGRATED'">
<div class="metric-box">
<div class="value">{{ content.scenarioTotal ? content.scenarioTotal : 0 }}</div>
<div class="name">{{ $t('api_test.scenario.scenario') }}</div>
</div>
<span class="ms-point-success"/>
<div class="metric-box">
<div class="value">{{ content.scenarioSuccess ? content.scenarioSuccess : 0 }}</div>
<div class="name">{{ $t('api_report.success') }}</div>
</div>
<span class="ms-point-error"/>
<div class="metric-box">
<div class="value">{{ content.scenarioError ? content.scenarioError : 0 }}</div>
<div class="name">{{ $t('api_report.fail') }}</div>
</div>
<span class="ms-point-error-code"
v-if="content.scenarioErrorReport > 0 || content.scenarioStepErrorReport > 0 "/>
<div class="metric-box" v-if="content.scenarioErrorReport > 0 || content.scenarioStepErrorReport > 0 ">
<div class="value">{{ content.scenarioErrorReport ? content.scenarioErrorReport : 0 }}</div>
<div class="name">{{ $t('error_report_library.option.name') }}</div>
</div>
<span v-show="showUnExecuteReport" class="ms-point-unexecute"/>
<div v-show="showUnExecuteReport" class="metric-box">
<div class="value">{{ content.scenarioUnExecute ? content.scenarioUnExecute : 0 }}</div>
<div class="name">{{ $t('api_test.home_page.detail_card.unexecute') }}</div>
</div>
</el-row>
<el-divider v-if="report.reportType !== 'API_INTEGRATED'"/>
<el-row type="flex" justify="center" align="middle">
<el-row type="flex" justify="center" align="middle">
<div class="metric-box">
<div class="value">{{ content.scenarioStepTotal ? content.scenarioStepTotal : 0 }}</div>
<div class="name" v-if="report.reportType === 'API_INTEGRATED'">
{{ $t('api_test.definition.request.case') }}
</div>
<div class="name" v-else>{{ $t('test_track.plan_view.step') }}</div>
</div>
<span class="ms-point-success"/>
<div class="metric-box">
<div class="value">{{ content.scenarioStepSuccess ? content.scenarioStepSuccess : 0 }}</div>
<div class="name">{{ $t('api_report.success') }}</div>
</div>
<span class="ms-point-error"/>
<div class="metric-box">
<div class="value">{{ content.scenarioStepError ? content.scenarioStepError : 0 }}</div>
<div class="name">{{ $t('api_report.fail') }}</div>
</div>
<span class="ms-point-error-code"
v-if="content.scenarioErrorReport > 0 || content.scenarioStepErrorReport > 0 "/>
<div class="metric-box" v-if="content.scenarioErrorReport > 0 || content.scenarioStepErrorReport > 0 ">
<div class="value">{{ content.scenarioStepErrorReport ? content.scenarioStepErrorReport : 0 }}</div>
<div class="name">{{ $t('error_report_library.option.name') }}</div>
</div>
<span v-show="showUnExecuteReport && !isUi" class="ms-point-unexecute"/>
<div v-show="showUnExecuteReport && !isUi" class="metric-box">
<div class="value">{{
content.scenarioStepUnExecuteReport ? content.scenarioStepUnExecuteReport : 0
}}
</div>
<div class="name">{{ $t('api_test.home_page.detail_card.unexecute') }}</div>
</div>
<span v-show="showUnExecuteReport && isUi" class="ms-point-unexecute"/>
<div v-show="showUnExecuteReport && isUi" class="metric-box">
<div class="value">{{
content.scenarioStepUnExecuteReport ? content.scenarioStepUnExecuteReport : 0
}}
</div>
<div class="name">{{ $t('api_test.home_page.detail_card.unexecute') }}</div>
</div>
</el-row>
</el-row>
</div>
<div class="split"></div>
<div style="width: 50%">
<el-row type="flex" justify="space-around" align="middle">
<div class="metric-icon-box">
<i class="el-icon-warning-outline fail"></i>
<div class="value">{{ fail }}</div>
<div class="name">{{ $t('api_report.fail') }}</div>
</div>
<div class="metric-icon-box">
<i class="el-icon-document-checked assertions"></i>
<div class="value">{{ assertions }}</div>
<div class="name">{{ $t('api_report.assertions_pass') }}</div>
</div>
<div class="metric-icon-box" v-if="content.errorCode > 0">
<i class="el-icon-document-checked assertions"></i>
<div class="value">{{ errorCodeAssertions }}</div>
<div class="name">{{ $t('error_report_library.assertion') }}</div>
</div>
<div class="metric-icon-box" v-if="!isUi">
<i class="el-icon-document-copy total"></i>
<div class="value">{{ this.content.total }}</div>
<div class="name">{{ isUi ? '指令' : $t('api_report.request') }}</div>
</div>
</el-row>
</div>
</el-row>
</div>
</template>
<script>
import MsChart from "metersphere-frontend/src/components/chart/MsChart";
export default {
name: "MsMetricChart",
components: {MsChart},
props: {
report: Object,
content: Object,
totalTime: Number,
isExport: {
type: Boolean,
default: false,
}
},
data() {
return {
hour: 0,
minutes: 0,
seconds: 0,
time: 0,
scenarioTotal: 0,
scenarioSuccess: 0,
scenarioError: 0,
reqTotal: 0,
}
},
created() {
this.initTime();
},
methods: {
initTime() {
this.time = this.totalTime;
this.seconds = (this.time) / 1000;
if (this.seconds >= 1) {
if (this.seconds < 60) {
this.seconds = Math.round(this.seconds * 100 / 1) / 100;
this.time = this.seconds + "s"
}
if (this.seconds > 60) {
this.minutes = Math.round(this.seconds / 60)
this.seconds = Math.round(this.seconds * 100 % 60) / 100;
this.time = this.minutes + "min" + this.seconds + "s"
}
if (this.minutes > 60) {
this.hour = Math.round(this.minutes / 60)
this.minutes = Math.round(this.minutes % 60)
this.time = this.hour + "hour" + this.minutes + "min" + this.seconds + "s"
}
} else {
this.time = this.totalTime + "ms"
}
},
},
computed: {
totalCount() {
let total = 0;
if (this.content.success) {
total += this.content.success;
}
if (this.content.error) {
total += this.content.error;
}
if (this.content.errorCode) {
total += this.content.errorCode;
}
if (this.content.unExecute) {
total += this.content.unExecute;
}
return total;
},
options() {
return {
color: ['#67C23A', '#F56C6C', '#F6972A', '#9C9B9A'],
tooltip: {
trigger: 'item',
formatter: '{b}: {c} ({d}%)'
},
title: [{
text: this.totalCount,
subtext: this.isUi ? '指令' : this.$t('api_report.request'),
top: 'center',
left: 'center',
textStyle: {
rich: {
align: 'center',
value: {
fontSize: 32,
fontWeight: 'bold',
padding: [10, 0]
},
name: {
fontSize: 14,
fontWeight: 'normal',
color: '#7F7F7F',
}
}
}
}],
series: [
{
type: 'pie',
radius: ['80%', '90%'],
avoidLabelOverlap: false,
hoverAnimation: false,
label: {
show: false,
},
itemStyle: {
borderColor: "#FFF",
shadowColor: '#E1E1E1',
shadowBlur: 10
},
labelLine: {
show: false
},
data: [
{value: this.content.success, name: this.$t('api_report.success')},
{value: this.content.error, name: this.$t('api_report.fail')},
{value: this.content.errorCode, name: this.$t('error_report_library.option.name')},
{value: this.content.unExecute, name: this.$t('api_test.home_page.detail_card.unexecute')},
]
}
]
};
},
fail() {
return (this.content.error / this.content.total * 100).toFixed(0) + "%";
},
assertions() {
return this.content.passAssertions + " / " + this.content.totalAssertions;
},
errorCodeAssertions() {
return this.content.errorCode + " / " + this.content.totalAssertions;
},
isUi() {
return this.report.reportType && this.report.reportType.startsWith("UI");
},
showUnExecuteReport() {
return (this.content.scenarioStepUnExecuteReport && this.content.scenarioStepUnExecuteReport > 0)
|| (this.content.scenarioUnExecute && this.content.scenarioUnExecute > 0) || (this.content.unExecute && this.content.unExecute > 0);
},
},
}
</script>
<style scoped>
.metric-container {
padding: 5px 10px;
}
.metric-container #chart {
width: 140px;
height: 140px;
margin-right: 40px;
}
.metric-container .split {
margin: 20px;
height: 100px;
border-left: 1px solid #D8DBE1;
}
.metric-container .circle {
width: 12px;
height: 12px;
border-radius: 50%;
box-shadow: 0 0 20px 1px rgba(200, 216, 226, .42);
display: inline-block;
margin-right: 10px;
vertical-align: middle;
}
.metric-container .circle.success {
background-color: #67C23A;
}
.metric-container .circle.fail {
background-color: #F56C6C;
}
.metric-box {
display: inline-block;
text-align: center;
min-width: 62px;
}
.metric-box .value {
font-size: 32px;
font-weight: 600;
letter-spacing: -.5px;
}
.metric-time .value {
font-size: 25px;
font-weight: 400;
letter-spacing: -.5px;
}
.metric-box .name {
font-size: 16px;
letter-spacing: -.2px;
color: #404040;
}
.metric-icon-box {
text-align: center;
margin: 0 10px;
}
.metric-icon-box .value {
font-size: 20px;
font-weight: 600;
letter-spacing: -.4px;
line-height: 28px;
vertical-align: middle;
}
.metric-icon-box .name {
font-size: 13px;
letter-spacing: 1px;
color: #BFBFBF;
line-height: 18px;
}
.metric-icon-box .fail {
color: #F56C6C;
font-size: 40px;
}
.metric-icon-box .assertions {
font-size: 40px;
}
.metric-icon-box .total {
font-size: 40px;
}
.ms-req {
border-radius: 50%;
height: 110px;
width: 110px;
display: inline-block;
vertical-align: top;
margin-right: 30px;
}
.ms-req-error {
border: 5px #F56C6C solid;
}
.ms-req-success {
border: 5px #67C23A solid;
}
.ms-req-span {
display: block;
color: black;
height: 110px;
line-height: 110px;
text-align: center;
}
.ms-point-success {
border-radius: 50%;
height: 12px;
width: 12px;
min-width: 12px;
display: inline-block;
vertical-align: top;
margin-left: 20px;
margin-right: 20px;
background-color: #67C23A;
}
.ms-point-error {
border-radius: 50%;
height: 12px;
width: 12px;
min-width: 12px;
display: inline-block;
vertical-align: top;
margin-left: 20px;
margin-right: 20px;
background-color: #F56C6C;
}
.ms-point-error-code {
border-radius: 50%;
height: 12px;
width: 12px;
min-width: 12px;
display: inline-block;
vertical-align: top;
margin-left: 20px;
margin-right: 20px;
background-color: #F6972A;
}
.ms-point-unexecute {
border-radius: 50%;
height: 12px;
width: 12px;
min-width: 12px;
display: inline-block;
vertical-align: top;
margin-left: 20px;
margin-right: 20px;
background-color: #9C9B9A;
}
</style>

View File

@ -0,0 +1,435 @@
<template>
<el-card class="ms-cards" v-if="request && request.responseResult">
<div class="request-result">
<div @click="active">
<el-row :gutter="18" type="flex" align="middle" class="info">
<el-col class="ms-req-name-col" :span="18" v-if="indexNumber!=undefined">
<el-tooltip :content="getName(request.name)" placement="top">
<div class="method ms-req-name">
<div class="el-step__icon is-text ms-api-col-create">
<div class="el-step__icon-inner"> {{ indexNumber }}</div>
</div>
<i class="icon el-icon-arrow-right" :class="{'is-active': showActive}" @click="active" @click.stop/>
<el-link class="report-label-req" @click="isLink" v-if="redirect && resourceId">
{{ request.name }}
</el-link>
<span v-else>{{ getName(request.name) }}</span>
</div>
</el-tooltip>
</el-col>
<el-col :span="3">
<div v-if="totalStatus">
<el-tooltip effect="dark" v-if="baseErrorCode && baseErrorCode!==''" :content="baseErrorCode"
style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap;" placement="bottom"
:open-delay="800">
<div v-if="totalStatus === 'Success'|| totalStatus === 'success'" style="color: #5daf34">
{{ baseErrorCode }}
</div>
<div v-else-if="totalStatus === 'errorReportResult'" style="color: #F6972A">
{{ baseErrorCode }}
</div>
<div v-else style="color: #FE6F71">
{{ baseErrorCode }}
</div>
</el-tooltip>
</div>
<div v-else>
<el-tooltip effect="dark" v-if="baseErrorCode && baseErrorCode!==''" :content="baseErrorCode"
style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap;" placement="bottom"
:open-delay="800">
<div v-if="request.success" style="color: #F6972A">
{{ baseErrorCode }}
</div>
<div v-else style="color: #FE6F71">
{{ baseErrorCode }}
</div>
</el-tooltip>
</div>
</el-col>
<el-col :span="6">
<div v-if="totalStatus">
<el-tooltip effect="dark" :content="request.responseResult.responseCode"
style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap;" placement="bottom"
:open-delay="800">
<div v-if="totalStatus === 'Success'|| totalStatus === 'success'" style="color: #5daf34">
{{ request.responseResult.responseCode }}
</div>
<div v-else-if="totalStatus === 'errorReportResult'" style="color: #F6972A">
{{ request.responseResult.responseCode }}
</div>
<div style="color: #FE6F71" v-else>
{{ request.responseResult.responseCode }}
</div>
</el-tooltip>
</div>
<div v-else>
<el-tooltip effect="dark" :content="request.responseResult.responseCode"
style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap;" placement="bottom"
:open-delay="800">
<div style="color: #F6972A" v-if="baseErrorCode && baseErrorCode!=='' && request.success">
{{ request.responseResult.responseCode }}
</div>
<div style="color: #5daf34" v-else-if="request.success">
{{ request.responseResult.responseCode }}
</div>
<div style="color: #FE6F71" v-else>
{{ request.responseResult.responseCode }}
</div>
</el-tooltip>
</div>
</el-col>
<el-col :span="3">
<div v-if="totalStatus">
<div v-if="totalStatus === 'Success'|| totalStatus === 'success'" style="color: #5daf34">
{{ request.responseResult.responseTime }}
</div>
<div v-else-if="totalStatus === 'errorReportResult'" style="color: #F6972A">
{{ request.responseResult.responseTime }}
</div>
<div style="color: #FE6F71" v-else>
{{ request.responseResult.responseTime }}
</div>
</div>
<div v-else>
<span v-if="request.success">
{{ request.responseResult.responseTime }} ms
</span>
<span style="color: #FE6F71" v-else>
{{ request.responseResult.responseTime }} ms
</span>
</div>
</el-col>
<el-col :span="2">
<div v-if="totalStatus">
<el-tag size="mini" v-if="totalStatus === 'unexecute'">{{
$t('api_test.home_page.detail_card.unexecute')
}}
</el-tag>
<el-tag v-else-if="totalStatus === 'errorReportResult' " class="ms-test-error_code"
size="mini">
{{ $t('error_report_library.option.name') }}
</el-tag>
<el-tag size="mini" type="success" v-else-if="totalStatus === 'Success' || totalStatus === 'success'">
{{ $t('api_report.success') }}
</el-tag>
<el-tag size="mini" type="danger" v-else> {{ $t('api_report.fail') }}</el-tag>
</div>
<div v-else>
<el-tag v-if="request.testing" class="ms-test-running" size="mini">
<i class="el-icon-loading" style="font-size: 16px"/>
{{ $t('commons.testing') }}
</el-tag>
<el-tag size="mini" v-else-if="request.unexecute">{{
$t('api_test.home_page.detail_card.unexecute')
}}
</el-tag>
<el-tag size="mini" v-else-if="!request.success && request.status && request.status==='unexecute'">{{
$t('api_test.home_page.detail_card.unexecute')
}}
</el-tag>
<el-tag v-else-if="baseErrorCode && baseErrorCode!== '' && request.success" class="ms-test-error_code"
size="mini">
{{ $t('error_report_library.option.name') }}
</el-tag>
<el-tag size="mini" type="success" v-else-if="request.success"> {{ $t('api_report.success') }}</el-tag>
<el-tag size="mini" type="danger" v-else> {{ $t('api_report.fail') }}</el-tag>
</div>
</el-col>
</el-row>
</div>
<el-collapse-transition>
<div v-show="showActive && !request.unexecute" style="width: 99%">
<ms-request-result-tail
v-loading="requestInfo.loading"
:scenario-name="scenarioName"
:request-type="requestType"
:request="requestInfo"
:console="console"
v-if="showActive"/>
</div>
</el-collapse-transition>
</div>
</el-card>
</template>
<script>
import MsRequestResultTail from "./RequestResultTail";
export default {
name: "MsRequestResult",
components: {
MsRequestResultTail
},
props: {
request: Object,
resourceId: String,
scenarioName: String,
stepId: String,
indexNumber: Number,
console: String,
totalStatus: String,
redirect: Boolean,
errorCode: {
type: String,
default: ""
},
isActive: {
type: Boolean,
default: false
},
isShare: Boolean,
shareId: String,
},
created() {
this.showActive = this.isActive;
this.baseErrorCode = this.errorCode;
},
data() {
return {
requestType: "",
color: {
type: String,
default() {
return "#B8741A";
}
},
requestInfo: {
loading: true,
hasData: false,
responseResult: {},
subRequestResults: [],
},
baseErrorCode: "",
backgroundColor: {
type: String,
default() {
return "#F9F1EA";
}
},
showActive: false,
}
},
watch: {
isActive() {
this.loadRequestInfoExpand();
this.showActive = this.isActive;
},
errorCode() {
this.baseErrorCode = this.errorCode;
},
request: {
deep: true,
handler(n) {
if (this.request.errorCode) {
this.baseErrorCode = this.request.errorCode;
} else if (this.request.attachInfoMap && this.request.attachInfoMap.errorReportResult) {
if (this.request.attachInfoMap.errorReportResult !== "") {
this.baseErrorCode = this.request.attachInfoMap.errorReportResult;
}
}
},
}
},
methods: {
isLink() {
let uri = "/#/api/definition?caseId=" + this.resourceId;
this.clickResource(uri)
},
clickResource(uri) {
this.$get('/user/update/currentByResourceId/' + this.resourceId).then(() => {
this.toPage(uri);
});
},
toPage(uri) {
let id = "new_a";
let a = document.createElement("a");
a.setAttribute("href", uri);
a.setAttribute("target", "_blank");
a.setAttribute("id", id);
document.body.appendChild(a);
a.click();
let element = document.getElementById(id);
element.parentNode.removeChild(element);
},
loadRequestInfoExpand() {
if (!this.request.responseResult || this.request.responseResult.body === null || this.request.responseResult.body === undefined) {
if (this.isShare) {
this.$get("/share/" + this.shareId + "/scenario/report/selectReportContent/" + this.stepId).then(response => {
this.requestInfo = response.data;
this.$nextTick(() => {
this.requestInfo.loading = false;
});
});
} else {
this.$get("/ui/scenario/report/selectReportContent/" + this.stepId).then(response => {
this.requestInfo = response.data;
this.$nextTick(() => {
this.requestInfo.loading = false;
});
});
}
} else {
this.requestInfo = this.request;
}
},
active() {
if (this.request.unexecute) {
this.showActive = false;
} else {
this.showActive = !this.showActive;
}
if (this.showActive) {
this.loadRequestInfoExpand();
}
},
getName(name) {
if (name && name.indexOf("<->") !== -1) {
return name.split("<->")[0];
}
if (name && name.indexOf("^@~@^") !== -1) {
let arr = name.split("^@~@^");
let value = arr[arr.length - 1];
if (value.indexOf("UUID=") !== -1) {
return value.split("UUID=")[0];
}
if (value && value.startsWith("UUID=")) {
return "";
}
if (value && value.indexOf("<->") !== -1) {
return value.split("<->")[0];
}
return value;
}
if (name && name.startsWith("UUID=")) {
return "";
}
return name;
}
},
}
</script>
<style scoped>
.request-result {
min-height: 30px;
padding: 2px 0;
}
.request-result .info {
margin-left: 20px;
cursor: pointer;
}
.request-result .method {
color: #1E90FF;
font-size: 14px;
font-weight: 500;
line-height: 35px;
padding-left: 5px;
width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.request-result .url {
color: #7f7f7f;
font-size: 12px;
font-weight: 400;
margin-top: 4px;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
word-break: break-all;
}
.request-result .tab .el-tabs__header {
margin: 0;
}
.request-result .text {
height: 300px;
overflow-y: auto;
}
.sub-result .info {
background-color: #FFF;
}
.sub-result .method {
border-left: 5px solid #1E90FF;
padding-left: 20px;
}
.ms-cards :deep(.el-card__body) {
padding: 1px;
}
.sub-result:last-child {
border-bottom: 1px solid #EBEEF5;
}
.ms-test-running {
color: #783887;
}
.ms-test-error_code {
color: #F6972A;
background-color: #FDF5EA;
border-color: #FDF5EA;
}
.ms-api-col {
background-color: #EFF0F0;
border-color: #EFF0F0;
margin-right: 10px;
font-size: 12px;
color: #64666A;
}
.ms-api-col-create {
background-color: #EBF2F2;
border-color: #008080;
margin-right: 10px;
font-size: 12px;
color: #008080;
}
:deep(.el-step__icon) {
width: 20px;
height: 20px;
font-size: 12px;
}
.el-divider--horizontal {
margin: 2px 0;
background: 0 0;
border-top: 1px solid #e8eaec;
}
.icon.is-active {
transform: rotate(90deg);
}
.ms-req-name {
display: inline-block;
margin: 0 5px;
padding-bottom: 0;
text-overflow: ellipsis;
vertical-align: middle;
white-space: nowrap;
width: 350px;
}
.ms-req-name-col {
overflow-x: hidden;
}
.report-label-req {
height: 20px;
border-bottom: 1px solid #303133;
}
</style>

View File

@ -0,0 +1,146 @@
<template>
<div class="request-result">
<div>
<el-row :gutter="8" type="flex" align="middle" class="info">
<el-col :span="2">
<div class="method">
{{ request.method }}
</div>
</el-col>
<el-col :span="8">
<el-tooltip effect="dark" :content="request.url" placement="bottom" :open-delay="800">
<div class="url">{{ request.url }}</div>
</el-tooltip>
</el-col>
<el-col :span="8">
<div class="url">
{{ $t('api_report.start_time') }}{{ request.startTime | timestampFormatDate(true) }}
{{ $t('report.test_end_time') }}{{ request.endTime | timestampFormatDate(true) }}
</div>
</el-col>
</el-row>
</div>
<el-collapse-transition>
<div v-show="isActive">
<el-tabs v-model="activeName" v-show="isActive" v-if="hasSub">
<el-tab-pane :label="$t('api_report.sub_result')" name="sub">
<ms-request-sub-result class="sub-result" v-for="(sub, index) in request.subRequestResults"
:key="index" :indexNumber="index" :request="sub"/>
</el-tab-pane>
<el-tab-pane :label="$t('api_report.request_result')" name="result">
<ms-response-text :console="console" :request-type="requestType" :response="request.responseResult" :request="request"/>
</el-tab-pane>
</el-tabs>
<div v-else>
<ms-response-text :console="console" :request-type="requestType" v-if="isCodeEditAlive" :response="request.responseResult" :request="request"/>
</div>
</div>
</el-collapse-transition>
</div>
</template>
<script>
import MsResponseText from "./ResponseText";
import MsRequestSubResult from "./RequestSubResult";
export default {
name: "MsRequestResultTail",
components: {MsResponseText, MsRequestSubResult},
props: {
request: Object,
scenarioName: String,
requestType: String,
console: String,
},
data() {
return {
isActive: true,
activeName: "sub",
isCodeEditAlive: true
}
},
methods: {
active() {
this.isActive = !this.isActive;
},
reload() {
this.isCodeEditAlive = false;
this.$nextTick(() => (this.isCodeEditAlive = true));
}
},
watch: {
'request.responseResult'() {
this.reload();
}
},
computed: {
hasSub() {
return this.request.subRequestResults.length > 0;
},
}
}
</script>
<style scoped>
.request-result {
width: 100%;
min-height: 40px;
padding: 2px 0;
}
.request-result .info {
background-color: #F9F9F9;
margin-left: 20px;
cursor: pointer;
}
.request-result .method {
color: #1E90FF;
font-size: 14px;
font-weight: 500;
line-height: 40px;
padding-left: 5px;
}
.request-result .url {
color: #7f7f7f;
font-size: 12px;
font-weight: 400;
margin-top: 4px;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
word-break: break-all;
}
.request-result .tab .el-tabs__header {
margin: 0;
}
.request-result .text {
height: 300px;
overflow-y: auto;
}
.sub-result .info {
background-color: #FFF;
}
.sub-result .method {
border-left: 5px solid #1E90FF;
padding-left: 20px;
}
.sub-result:last-child {
border-bottom: 1px solid #EBEEF5;
}
.request-result .icon.is-active {
transform: rotate(90deg);
}
</style>

View File

@ -0,0 +1,191 @@
<template>
<div class="request-result">
<p class="el-divider--horizontal"></p>
<div @click="active">
<el-row :gutter="10" type="flex" align="middle" class="info">
<el-col :span="6" v-if="indexNumber!=undefined">
<div class="method">
<div style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">
<div class="el-step__icon is-text ms-api-col" v-if="indexNumber%2 ==0">
<div class="el-step__icon-inner"> {{ indexNumber + 1 }}</div>
</div>
<div class="el-step__icon is-text ms-api-col-create" v-else>
<div class="el-step__icon-inner"> {{ indexNumber + 1 }}</div>
</div>
<i class="icon el-icon-arrow-right" :class="{'is-active': isActive}" @click="active" @click.stop/>
{{ getName(request.name) }}
</div>
</div>
</el-col>
<el-col :span="2">
<div>
{{ request.method }}
</div>
</el-col>
<el-col :span="6">
<div class="url">
<el-tooltip :content="request.url " style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap;" placement="bottom" :open-delay="800">
<div>
{{ request.url }}
</div>
</el-tooltip>
</div>
</el-col>
<el-col :span="5">
<el-tooltip effect="dark" :content="request.responseResult.responseCode" placement="bottom" :open-delay="800">
<div class="url" style="color: #5daf34">{{ request.responseResult.responseCode }}</div>
</el-tooltip>
</el-col>
<el-col :span="3">
{{ request.responseResult.responseTime }} ms
</el-col>
<el-col :span="2">
<div class="success">
<el-tag size="mini" type="success" v-if="request.success">
{{ $t('api_report.success') }}
</el-tag>
<el-tag size="mini" type="danger" v-else>
{{ $t('api_report.fail') }}
</el-tag>
</div>
</el-col>
</el-row>
</div>
<el-collapse-transition>
<div v-show="isActive" style="width: 99%">
<ms-request-sub-result-tail :scenario-name="scenarioName"
:request-type="requestType" v-if="isActive"
:request="request"/>
</div>
</el-collapse-transition>
</div>
</template>
<script>
import MsRequestSubResultTail from "./RequestSubResultTail";
export default {
name: "MsRequestSubResult",
components: {
MsRequestSubResultTail
},
props: {
request: Object,
scenarioName: String,
indexNumber: Number,
},
data() {
return {isActive: false, requestType: undefined,}
},
methods: {
active() {
this.isActive = !this.isActive;
},
getName(name) {
if (name && name.indexOf("<->") !== -1) {
return name.split("<->")[0];
}
if (name && name.indexOf("^@~@^") !== -1) {
let arr = name.split("^@~@^");
let value = arr[arr.length - 1];
if (value.indexOf("UUID=") !== -1) {
return value.split("UUID=")[0];
}
if (value && value.startsWith("UUID=")) {
return "";
}
if (value && value.indexOf("<->") !== -1) {
return value.split("<->")[0];
}
return value;
}
if (name && name.startsWith("UUID=")) {
return "";
}
return name;
}
},
}
</script>
<style scoped>
.request-result {
width: 100%;
min-height: 40px;
padding: 2px 0;
}
.request-result .info {
margin-left: 20px;
cursor: pointer;
}
.request-result .method {
color: #1E90FF;
font-size: 14px;
font-weight: 500;
line-height: 40px;
padding-left: 5px;
}
.request-result .url {
color: #7f7f7f;
font-size: 12px;
font-weight: 400;
margin-top: 4px;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
word-break: break-all;
}
.request-result .tab .el-tabs__header {
margin: 0;
}
.request-result .text {
height: 300px;
overflow-y: auto;
}
.sub-result .info {
background-color: #FFF;
}
.sub-result .method {
border-left: 5px solid #1E90FF;
padding-left: 20px;
}
.sub-result:last-child {
border-bottom: 1px solid #EBEEF5;
}
.ms-api-col {
background-color: #EFF0F0;
border-color: #EFF0F0;
margin-right: 10px;
font-size: 12px;
color: #64666A;
}
.ms-api-col-create {
background-color: #EBF2F2;
border-color: #008080;
margin-right: 10px;
font-size: 12px;
color: #008080;
}
.el-divider--horizontal {
margin: 2px 0;
background: 0 0;
border-top: 1px solid #e8eaec;
}
.icon.is-active {
transform: rotate(90deg);
}
</style>

View File

@ -0,0 +1,133 @@
<template>
<div class="request-result">
<div>
<el-row :gutter="8" type="flex" align="middle" class="info">
<el-col :span="2">
<div class="method">
{{request.method}}
</div>
</el-col>
<el-col :span="8">
<el-tooltip effect="dark" :content="request.url" placement="bottom" :open-delay="800">
<div class="url">{{request.url}}</div>
</el-tooltip>
</el-col>
<el-col :span="8">
<div class="url"> {{$t('api_report.start_time')}}{{request.startTime | timestampFormatDate(true) }}
</div>
</el-col>
</el-row>
</div>
<el-collapse-transition>
<div v-show="isActive">
<ms-response-text :request-type="requestType" v-if="isCodeEditAlive" :response="request.responseResult" :request="request"/>
</div>
</el-collapse-transition>
</div>
</template>
<script>
import MsResponseText from "./ResponseText";
export default {
name: "MsRequestSubResultTail",
components: {MsResponseText},
props: {
request: Object,
scenarioName: String,
requestType: String
},
data() {
return {
isActive: true,
activeName: "sub",
isCodeEditAlive: true
}
},
methods: {
active() {
this.isActive = !this.isActive;
},
reload() {
this.isCodeEditAlive = false;
this.$nextTick(() => (this.isCodeEditAlive = true));
}
},
watch: {
'request.responseResult'() {
this.reload();
}
},
computed: {
assertion() {
return this.request.passAssertions + " / " + this.request.totalAssertions;
},
hasSub() {
return this.request.subRequestResults.length > 0;
},
}
}
</script>
<style scoped>
.request-result {
width: 100%;
min-height: 40px;
padding: 2px 0;
}
.request-result .info {
background-color: #F9F9F9;
margin-left: 20px;
cursor: pointer;
}
.request-result .method {
color: #1E90FF;
font-size: 14px;
font-weight: 500;
line-height: 40px;
padding-left: 5px;
}
.request-result .url {
color: #7f7f7f;
font-size: 12px;
font-weight: 400;
margin-top: 4px;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
word-break: break-all;
}
.request-result .tab .el-tabs__header {
margin: 0;
}
.request-result .text {
height: 300px;
overflow-y: auto;
}
.sub-result .info {
background-color: #FFF;
}
.sub-result .method {
border-left: 5px solid #1E90FF;
padding-left: 20px;
}
.sub-result:last-child {
border-bottom: 1px solid #EBEEF5;
}
.request-result .icon.is-active {
transform: rotate(90deg);
}
</style>

View File

@ -0,0 +1,144 @@
<template>
<div class="text-container">
<el-collapse-transition>
<el-tabs v-model="activeName" v-show="isActive">
<el-tab-pane :class="'body-pane'" :label="$t('api_test.definition.request.response_body')" name="body" class="pane">
<ms-code-edit :mode="mode" :read-only="true" :data="response.body" :modes="modes" ref="codeEdit"/>
</el-tab-pane>
<el-tab-pane :label="$t('api_test.definition.request.response_header')" name="headers" class="pane">
<pre>{{ response.headers }}</pre>
</el-tab-pane>
<el-tab-pane :label="$t('api_report.assertions')" name="assertions" class="pane assertions">
<ms-assertion-results :assertions="response.assertions"/>
</el-tab-pane>
<el-tab-pane :label="$t('api_test.request.extract.label')" name="label" class="pane">
<pre>{{ response.vars }}</pre>
</el-tab-pane>
<el-tab-pane :label="$t('api_report.request_body')" name="request_body" class="pane">
<div class="ms-div">
{{ $t('api_test.request.address') }} :
<pre>{{ request.url }}</pre>
</div>
<div class="ms-div">
{{ $t('api_test.scenario.headers') }} :
<pre>{{ request.headers }}</pre>
</div>
<div class="ms-div">
Cookies :
<pre>{{ request.cookies }}</pre>
</div>
<div class="ms-div">
Body :
<pre>{{ request.body }}</pre>
</div>
</el-tab-pane>
<el-tab-pane v-if="activeName == 'body'" :disabled="true" name="mode" class="pane assertions">
<template v-slot:label>
<ms-dropdown v-if="request.method==='SQL'" :commands="sqlModes" :default-command="mode" @command="sqlModeChange"/>
<ms-dropdown v-else :commands="modes" :default-command="mode" @command="modeChange" ref="modeDropdown"/>
</template>
</el-tab-pane>
</el-tabs>
</el-collapse-transition>
</div>
</template>
<script>
import MsAssertionResults from "./AssertionResults";
import MsCodeEdit from "metersphere-frontend/src/components/MsCodeEdit";
import MsDropdown from "metersphere-frontend/src/components/MsDropdown";
export default {
name: "MsResponseText",
components: {
MsDropdown,
MsCodeEdit,
MsAssertionResults,
},
props: {
requestType: String,
request: {},
response: Object,
console: String,
},
data() {
return {
isActive: true,
activeName: "body",
modes: ['text', 'json', 'xml', 'html'],
sqlModes: ['text', 'table'],
mode: BODY_FORMAT.TEXT
}
},
methods: {
active() {
this.isActive = !this.isActive;
},
modeChange(mode) {
this.mode = mode;
},
sqlModeChange(mode) {
this.mode = mode;
}
},
mounted() {
if (!this.response.headers) {
return;
}
},
}
</script>
<style scoped>
.body-pane {
padding: 10px !important;
background: white !important;
}
.text-container .icon {
padding: 5px;
}
.text-container .collapse {
cursor: pointer;
}
.text-container .collapse:hover {
opacity: 0.8;
}
.text-container .icon.is-active {
transform: rotate(90deg);
}
.text-container .pane {
background-color: #F5F5F5;
padding: 1px 0;
height: 250px;
overflow-y: auto;
}
.text-container .pane.assertions {
padding: 0;
}
pre {
margin: 0;
}
.ms-div {
margin-top: 20px;
}
</style>

View File

@ -0,0 +1,184 @@
<template>
<div class="scenario-result">
<div v-if="(node.children && node.children.length >0) || node.unsolicited
|| (node.type && this.stepFilter.get('AllSamplerProxy').indexOf(node.type) === -1)">
<el-card class="ms-card">
<div class="el-step__icon is-text ms-api-col">
<div class="el-step__icon-inner">
{{ node.index }}
</div>
</div>
<el-tooltip effect="dark" :content="node.label" placement="top">
<el-link v-if="node.redirect" class="report-label-head" @click="isLink">
{{ getLabel(node.label) }}
</el-link>
<span v-else>{{ getLabel(node.label) }}</span>
</el-tooltip>
</el-card>
</div>
<div v-else-if="node.type === 'MsUiCommand'">
<ui-command-result
:step-id="node.stepId"
:index-number="node.index"
:tree-node="treeNode"
:command="node"
:isActive="isActive"
:result="node.value"/>
</div>
<div v-else>
<ms-request-result
:step-id="node.stepId"
:request="node.value"
:redirect="node.redirect"
:indexNumber="node.index"
:error-code="node.errorCode"
:scenarioName="node.label"
:resourceId="node.resourceId"
:total-status="node.totalStatus"
:console="console"
:isActive="isActive"
:is-share="isShare"
:share-id="shareId"
v-on:requestResult="requestResult"
/>
</div>
</div>
</template>
<script>
import MsRequestResult from "./RequestResult";
import {STEP} from "./Setting";
import UiCommandResult from "./UiCommandResult";
export default {
name: "MsScenarioResult",
components: {
UiCommandResult,
MsRequestResult
},
props: {
scenario: Object,
node: Object,
treeNode: Object,
console: String,
isActive: Boolean,
isShare: Boolean,
shareId: String,
},
data() {
return {
stepFilter: new STEP,
}
},
methods: {
getLabel(label) {
switch (label) {
case "ConstantTimer":
return "等待控制器";
case "LoopController":
return "循环控制器";
case "Assertion":
return "场景断言";
default:
return label;
}
},
isLink() {
let uri = "/#/api/automation?resourceId=" + this.node.resourceId;
this.clickResource(uri)
},
clickResource(uri) {
this.$get('/user/update/currentByResourceId/' + this.node.resourceId).then(() => {
this.toPage(uri);
});
},
toPage(uri) {
let id = "new_a";
let a = document.createElement("a");
a.setAttribute("href", uri);
a.setAttribute("target", "_blank");
a.setAttribute("id", id);
document.body.appendChild(a);
a.click();
let element = document.getElementById(id);
element.parentNode.removeChild(element);
},
active() {
this.isActive = !this.isActive;
},
requestResult(requestResult) {
this.$emit("requestResult", requestResult);
}
},
computed: {
assertion() {
return this.scenario.passAssertions + " / " + this.scenario.totalAssertions;
},
success() {
return this.scenario.error === 0;
}
}
}
</script>
<style scoped>
.scenario-result {
width: 100%;
padding: 2px 0;
}
.scenario-result + .scenario-result {
border-top: 1px solid #DCDFE6;
}
.ms-card :deep(.el-card__body) {
padding: 10px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.scenario-result .info {
height: 40px;
cursor: pointer;
}
.scenario-result .icon {
padding: 5px;
}
.scenario-result .icon.is-active {
transform: rotate(90deg);
}
.ms-api-col {
background-color: #EFF0F0;
border-color: #EFF0F0;
margin-right: 10px;
font-size: 12px;
color: #64666A;
}
.ms-card .ms-api-col-create {
background-color: #EBF2F2;
border-color: #008080;
margin-right: 10px;
font-size: 12px;
color: #008080;
}
.report-label-head {
border-bottom: 1px solid #303133;
color: #303133;
font-family: "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB", Arial, sans-serif;
font-size: 13px;
}
:deep(.el-step__icon) {
width: 20px;
height: 20px;
font-size: 12px;
}
</style>

View File

@ -0,0 +1,179 @@
<template>
<el-card class="scenario-results">
<div v-if="errorReport > 0">
<el-tooltip :content="$t('api_test.automation.open_expansion')" placement="top" effect="light">
<i class="el-icon-circle-plus-outline ms-open-btn ms-open-btn-left" v-prevent-re-click @click="openExpansion"/>
</el-tooltip>
<el-tooltip :content="$t('api_test.automation.close_expansion')" placement="top" effect="light">
<i class="el-icon-remove-outline ms-open-btn" size="mini" @click="closeExpansion"/>
</el-tooltip>
</div>
<el-tree :data="treeData"
:expand-on-click-node="false"
:default-expand-all="defaultExpand"
:filter-node-method="filterNode"
highlight-current
class="ms-tree ms-report-tree" ref="resultsTree">
<span slot-scope="{ node, data}" style="width: 99%" @click="nodeClick(node)">
<ms-scenario-result :node="data" :tree-node="node" :console="console" v-on:requestResult="requestResult"
:isActive="isActive" :is-share="isShare" :share-id="shareId"/>
</span>
</el-tree>
</el-card>
</template>
<script>
import MsScenarioResult from "./ScenarioResult";
export default {
name: "MsScenarioResults",
components: {MsScenarioResult},
props: {
scenarios: Array,
treeData: Array,
console: String,
errorReport: Number,
report: Object,
defaultExpand: {
default: false,
type: Boolean,
},
isShare: Boolean,
shareId: String,
},
data() {
return {
isActive: false
}
},
created() {
if (this.$refs.resultsTree && this.$refs.resultsTree.root) {
this.$refs.resultsTree.root.expanded = true;
}
},
computed: {
isUi() {
return this.report && this.report.reportType && this.report.reportType.startsWith("UI");
},
},
methods: {
filterNode(value, data) {
if (!data.value && (!data.children || data.children.length === 0)) {
return false;
}
if (!value) return true;
if (data.value) {
if (value === 'errorReport') {
if (data.errorCode && data.errorCode !== "" && data.value.status === "errorReportResult") {
return true;
}
} else if (value === 'unexecute') {
if (data.value.status === 'unexecute') {
return true;
}
} else {
if (this.isUi) {
return data.value.success === false && data.value.startTime > 0;
} else {
return data.totalStatus !== 'errorReportResult' && data.value.error > 0;
}
}
}
return false;
},
filter(val) {
this.$nextTick(() => {
this.$refs.resultsTree.filter(val);
});
},
requestResult(requestResult) {
this.$emit("requestResult", requestResult);
},
nodeClick(node) {
node.expanded = !node.expanded;
},
//
changeTreeNodeStatus(node, expandCount) {
node.expanded = this.expandAll
for (let i = 0; i < node.childNodes.length; i++) {
// expanded
node.childNodes[i].expanded = this.expandAll
//
if (node.childNodes[i].childNodes.length > 0) {
this.changeTreeNodeStatus(node.childNodes[i])
}
}
},
closeExpansion() {
this.isActive = false;
this.expandAll = false;
this.changeTreeNodeStatus(this.$refs.resultsTree.store.root, 0);
},
openExpansion() {
this.isActive = true;
this.expandAll = true;
//
this.changeTreeNodeStatus(this.$refs.resultsTree.store.root, 0)
},
}
}
</script>
<style scoped>
.scenario-results {
height: 100%;
}
.ms-report-tree :deep(.el-tree-node__content) {
height: 100%;
vertical-align: center;
}
:deep(.el-drawer__body) {
overflow: auto;
}
:deep(.el-step__icon.is-text) {
border: 1px solid;
}
:deep(.el-drawer__header) {
margin-bottom: 0px;
}
:deep(.el-link) {
font-weight: normal;
}
:deep(.el-checkbox) {
color: #303133;
font-family: "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB", Arial, sans-serif;
font-size: 13px;
font-weight: normal;
}
:deep(.el-checkbox__label) {
padding-left: 5px;
}
.ms-sc-variable-header :deep(.el-dialog__body) {
padding: 0px 20px;
}
.ms-open-btn {
margin: 5px 5px 0px;
color: #783887;
font-size: 20px;
}
.ms-open-btn:hover {
background-color: #F2F9EE;
cursor: pointer;
color: #67C23A;
}
.ms-open-btn-left {
margin-left: 30px;
}
</style>

View File

@ -0,0 +1,120 @@
export function STEP() {
let map = new Map([
['ALL', init()],
['scenario', init()],
['HTTPSamplerProxy', getDefaultSamplerMenu()],
['DubboSampler', getDefaultSamplerMenu()],
['JDBCSampler', getDefaultSamplerMenu()],
['TCPSampler', getDefaultSamplerMenu()],
['OT_IMPORT', getDefaultSamplerMenu()],
['AbstractSampler', getDefaultSamplerMenu()],
['IfController', getAll()],
['TransactionController', getAll()],
['LoopController', getAll()],
['ConstantTimer', []],
['JSR223Processor', getDefaultSamplerMenu()],
['JSR223PreProcessor', []],
['JSR223PostProcessor', []],
['JDBCPreProcessor', []],
['JDBCPostProcessor', []],
['Assertions', []],
['Extract', []],
['JmeterElement', []],
['CustomizeReq', getDefaultSamplerMenu()],
['MaxSamplerProxy', getDefaultSamplerMenu()],
['GenericController', getAll()],
['SpecialSteps', ['HTTPSamplerProxy', 'Assertions', 'DubboSampler', 'JDBCSampler', 'TCPSampler', 'Sampler', 'AbstractSampler', 'JSR223Processor', 'API', 'MsUiCommand']],
['AllSamplerProxy', ['GenericController','HTTPSamplerProxy', 'DubboSampler', 'JDBCSampler', 'TCPSampler', 'Sampler', 'AbstractSampler', 'JSR223Processor', 'API', 'MsUiCommand']],
['DEFINITION', ['HTTPSamplerProxy', 'DubboSampler', 'JDBCSampler', 'TCPSampler']],
['ALlSamplerStep', ['JSR223PreProcessor', 'JSR223PostProcessor', 'JDBCPreProcessor', 'JDBCPostProcessor', 'Assertions', 'Extract', 'ConstantTimer']],
['AllCanExecType', ['HTTPSamplerProxy', 'DubboSampler', 'JDBCSampler', 'TCPSampler', 'JSR223Processor', 'AbstractSampler']]]);
return map
}
export const ELEMENT_TYPE = {
scenario: 'scenario',
HTTPSamplerProxy: 'HTTPSamplerProxy',
OT_IMPORT: 'OT_IMPORT',
IfController: 'IfController',
TransactionController: 'TransactionController',
ConstantTimer: 'ConstantTimer',
JSR223Processor: 'JSR223Processor',
JSR223PreProcessor: 'JSR223PreProcessor',
JSR223PostProcessor: 'JSR223PostProcessor',
JDBCPostProcessor: 'JDBCPostProcessor',
JDBCPreProcessor: 'JDBCPreProcessor',
Assertions: 'Assertions',
Extract: 'Extract',
CustomizeReq: 'CustomizeReq',
LoopController: 'LoopController',
Plugin: 'Plugin'
}
export const TYPE_TO_C = new Map([
['scenario', 'io.metersphere.hashtree.MsScenario'],
['customCommand', 'io.metersphere.xpack.ui.hashtree.MsUiCustomCommand'],
['UiScenario', 'io.metersphere.hashtree.MsUiScenario'],
['HTTPSamplerProxy', 'io.metersphere.api.dto.definition.request.sampler.MsHTTPSamplerProxy'],
['DubboSampler', 'io.metersphere.api.dto.definition.request.sampler.MsDubboSampler'],
['JDBCSampler', 'io.metersphere.api.dto.definition.request.sampler.MsJDBCSampler'],
['TCPSampler', 'io.metersphere.api.dto.definition.request.sampler.MsTCPSampler'],
['IfController', 'io.metersphere.dto.request.controller.MsIfController'],
['TransactionController', 'io.metersphere.dto.request.controller.MsTransactionController'],
['LoopController', 'io.metersphere.dto.request.controller.MsLoopController'],
['ConstantTimer', 'io.metersphere.dto.request.timer.MsConstantTimer'],
['JSR223Processor', 'io.metersphere.api.dto.definition.request.processors.MsJSR223Processor'],
['JSR223PreProcessor', 'io.metersphere.api.dto.definition.request.processors.pre.MsJSR223PreProcessor'],
['JSR223PostProcessor', 'io.metersphere.api.dto.definition.request.processors.post.MsJSR223PostProcessor'],
['JDBCPreProcessor', 'io.metersphere.api.dto.definition.request.processors.pre.MsJDBCPreProcessor'],
['JDBCPostProcessor', 'io.metersphere.api.dto.definition.request.processors.post.MsJDBCPostProcessor'],
['Assertions', 'io.metersphere.api.dto.definition.request.assertions.MsAssertions'],
['Extract', 'io.metersphere.dto.request.extract.MsExtract'],
['JmeterElement', 'io.metersphere.dto.request.unknown.MsJmeterElement'],
['TestPlan', 'io.metersphere.hashtree.MsTestPlan'],
['ThreadGroup', 'io.metersphere.hashtree.MsThreadGroup'],
['DNSCacheManager', 'io.metersphere.api.dto.definition.request.dns.MsDNSCacheManager'],
['DebugSampler', 'io.metersphere.hashtree.MsDebugSampler'],
['AuthManager', 'io.metersphere.dto.request.auth.MsAuthManager']
])
export const PLUGIN_ELEMENTS = new Map([
['menu_post_processors', ['HtmlExtractor', 'JMESPathExtractor', 'JSONPostProcessor', 'RegexExtractor', 'BoundaryExtractor', 'Separator', 'XPath2Extractor', 'XPathExtractor', 'ResultAction', 'DebugPostProcessor', 'BeanShellPostProcessor']],
['menu_assertions', ['JSONPathAssertion', 'SizeAssertion', 'JSR223Assertion', 'XPath2Assertion', 'Separator', 'HTMLAssertion', 'JMESPathAssertion', 'MD5HexAssertion', 'SMIMEAssertion', 'XMLSchemaAssertion', 'XMLAssertion', 'XPathAssertion', 'DurationAssertion', 'CompareAssertion', 'BeanShellAssertion']],
['menu_listener', ['AbstractVisualizer', 'AbstractListener', 'ViewResultsFullVisualizer', 'SummaryReport', 'StatVisualizer', 'BackendListener', 'Separator', 'JSR223Listener', 'ResultSaver', 'RespTimeGraphVisualizer', 'GraphVisualizer', 'AssertionVisualizer', 'ComparisonVisualizer', 'StatGraphVisualizer', 'Summariser', 'TableVisualizer', 'SimpleDataWriter', 'MailerVisualizer', 'BeanShellListener']],
['menu_pre_processors', ['AbstractPostProcessor', 'UserParameters', 'Separator', 'AnchorModifier', 'URLRewritingModifier', 'SampleTimeout', 'RegExUserParameters', 'BeanShellPreProcessor']],
['menu_logic_controller', ['GenericController', 'scenario', 'IfController', 'LoopController', 'IfControllerPanel', 'TransactionController', 'LoopControlPanel', 'WhileController', 'Separator', 'ForeachControlPanel', 'IncludeController', 'RunTime', 'CriticalSectionController', 'InterleaveControl', 'OnceOnlyController', 'RecordController', 'LogicController', 'RandomControl', 'RandomOrderController', 'ThroughputController', 'SwitchController', 'ModuleController']],
['menu_fragments', ['TestFragmentController']],
['menu_non_test_elements', ['ProxyControl', 'HttpMirrorControl', 'GenerateTree', 'PropertyControl']],
['menu_generative_controller', ['HTTPSamplerProxy', 'JSR223Processor', 'DubboSampler', 'JDBCSampler', 'TCPSampler', 'Sampler', 'AbstractSampler', 'CustomizeReq', 'HttpTestSample', 'TestAction', 'DebugSampler', 'JSR223Sampler', 'Separator', 'AjpSampler', 'AccessLogSampler', 'BeanShellSampler', 'BoltSampler', 'FtpTestSampler', 'GraphQLHTTPSampler', 'JDBCSampler', 'JMSPublisher', 'JMSSampler', 'JMSSubscriber', 'JUnitTestSampler', 'JavaTestSampler', 'LdapExtTestSampler', 'LdapTestSampler', 'SystemSampler', 'SmtpSampler', 'TCPSampler', 'MailReaderSampler']],
['menu_threads', ['SetupThreadGroup', 'PostThreadGroup', 'ThreadGroup']],
['menu_timer', ['ConstantTimer', 'UniformRandomTimer', 'PreciseThroughputTimer', 'ConstantThroughputTimer', 'Separator', 'JSR223Timer', 'SyncTimer', 'PoissonRandomTimer', 'GaussianRandomTimer', 'BeanShellTimer']],
['menu_config_element', ['CSVDataSet', 'HeaderPanel', 'CookiePanel', 'CacheManager', 'HttpDefaults', 'Separator', 'BoltConnectionElement', 'DNSCachePanel', 'FtpConfig', 'AuthPanel', 'DataSourceElement', 'JavaConfig', 'LdapExtConfig', 'LdapConfig', 'TCPConfig', 'KeystoreConfig', 'ArgumentsPanel', 'LoginConfig', 'SimpleConfig', 'CounterConfig', 'RandomVariableConfig']],
])
export function getDefaultSamplerMenu() {
let array = [];
array = array.concat(PLUGIN_ELEMENTS.get('menu_assertions'));
array = array.concat(PLUGIN_ELEMENTS.get('menu_pre_processors'));
array = array.concat(PLUGIN_ELEMENTS.get('menu_post_processors'));
array = array.concat(PLUGIN_ELEMENTS.get('menu_config_element'));
array = array.concat(PLUGIN_ELEMENTS.get('menu_listener'));
return array;
}
export function init() {
let allArray = [];
allArray = allArray.concat(PLUGIN_ELEMENTS.get('menu_generative_controller'));
allArray = allArray.concat(PLUGIN_ELEMENTS.get('menu_logic_controller'));
allArray = allArray.concat(['scenario', 'ConstantTimer', 'JSR223Processor', 'Assertions']);
return allArray;
}
export function getAll() {
let allArray = [];
allArray = allArray.concat(getDefaultSamplerMenu());
allArray = allArray.concat(PLUGIN_ELEMENTS.get('menu_logic_controller'));
allArray = allArray.concat(PLUGIN_ELEMENTS.get('menu_non_test_elements'));
allArray = allArray.concat(PLUGIN_ELEMENTS.get('menu_generative_controller'));
allArray = allArray.concat(PLUGIN_ELEMENTS.get('menu_threads'));
return allArray;
}

View File

@ -0,0 +1,284 @@
<template>
<el-card class="ms-cards">
<div class="request-result">
<div @click="active">
<el-row :gutter="16" type="flex" align="middle" class="info">
<el-col class="ms-req-name-col" :span="18" v-if="indexNumber != undefined">
<div class="method ms-req-name">
<div class="el-step__icon is-text ms-api-col-create">
<div class="el-step__icon-inner"> {{ indexNumber }}</div>
</div>
<i class="icon el-icon-arrow-right" :class="{'is-active': showActive}" @click="active" @click.stop/>
<span>{{ label }}</span>
</div>
</el-col>
<el-col :span="3">
<span v-if="!isUnexecute" :style="result && !result.success ? 'color: #FE6F71' : ''">
{{ result.time }} ms
</span>
</el-col>
<el-col :span="3">
<!-- 兼容旧数据 报告截图 -->
<el-popover
placement="right"
trigger="hover"
popper-class="issues-popover"
v-if="!isUnexecute && result.uiImg">
<el-image
style="width: 100px; height: 100px"
:src="'/resource/ui/get?fileName=' + result.uiImg + '&reportId=' + result.reportId"
:preview-src-list="['/resource/ui/get?fileName=' + result.uiImg + '&reportId=' + result.reportId]">
</el-image>
<el-button slot="reference" type="text">{{ $t('ui.screenshot') }}</el-button>
</el-popover>
<div @click.stop="triggerViewer" v-if="!isUnexecute && uiScreenshots && result.combinationImg"
style="color: #783887;"> {{ $t('ui.screenshot') }}
</div>
</el-col>
<el-col :span="2">
<div>
<el-tag size="mini" v-if="isUnexecute">
{{ $t('api_test.home_page.detail_card.unexecute') }}
</el-tag>
<el-tag size="mini" type="success" v-else-if="result && result.success">
{{ $t('api_report.success') }}
</el-tag>
<el-tag v-else size="mini" type="danger">
{{ $t('api_report.fail') }}
</el-tag>
</div>
</el-col>
</el-row>
</div>
<el-collapse-transition>
<div v-show="showActive" style="width: 99%">
<ui-command-result-detail
v-loading="detail.loading"
:result="detail"
/>
</div>
</el-collapse-transition>
<ui-screenshot-viewer ref="screenshotViewer" v-if="!isUnexecute && uiScreenshots && result.combinationImg"
:src="'/resource/ui/get?fileName=' + result.combinationImg + '&reportId=' + result.reportId"/>
</div>
</el-card>
</template>
<script>
import UiCommandResultDetail from "./UiCommandResultDetail";
import UiScreenshotViewer from "./UiScreenshotViewer";
export default {
name: "UiCommandResult",
components: {UiCommandResultDetail, UiScreenshotViewer},
props: {
indexNumber: Number,
result: Object,
command: Object,
stepId: String,
isActive: {
type: Boolean,
default: false
},
treeNode: Object,
},
data() {
return {
showActive: false,
detail: {
loading: false
},
uiScreenshots: []
}
},
mounted() {
if (this.result.uiScreenshots) {
this.uiScreenshots = this.result.uiScreenshots || [];
}
},
computed: {
label() {
if (!this.$t("ui." + this.command.label).startsWith("ui")) {
return this.$t("ui." + this.command.label);
}
return this.command.label;
},
isUnexecute() {
return !this.result || this.result.status === 'unexecute';
}
},
watch: {
isActive() {
this.showActive = this.isActive;
},
errorCode() {
this.baseErrorCode = this.errorCode;
},
'treeNode.expanded'() {
if (this.treeNode.expanded) {
this.loadRequestInfoExpand();
}
}
},
methods: {
triggerViewer() {
this.$refs.screenshotViewer.open();
},
active() {
if (this.isUnexecute) {
this.showActive = false;
} else {
this.showActive = !this.showActive;
}
if (this.showActive) {
this.loadRequestInfoExpand();
}
},
loadRequestInfoExpand() {
if (this.command && this.command.value) {
if (!this.command.value.time && this.command.value.startTime && this.command.value.endTime) {
this.command.value.time = this.command.value.endTime - this.command.value.startTime;
}
this.result = this.command.value;
this.detail = this.command.value;
} else {
if (!this.detail.hasData) {
this.detail.loading = true;
this.$get("/ui/automation/selectReportContent/" + this.stepId).then(response => {
let requestResult = response.data;
if (requestResult) {
this.detail = requestResult;
}
this.$nextTick(() => {
this.detail.loading = false;
this.detail.hasData = true;
});
});
}
}
},
},
}
</script>
<style scoped>
.request-result {
min-height: 30px;
padding: 2px 0;
}
.request-result .info {
margin-left: 20px;
cursor: pointer;
}
.request-result .method {
color: #1E90FF;
font-size: 14px;
font-weight: 500;
line-height: 35px;
padding-left: 5px;
width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.request-result .url {
color: #7f7f7f;
font-size: 12px;
font-weight: 400;
margin-top: 4px;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
word-break: break-all;
}
.request-result .tab .el-tabs__header {
margin: 0;
}
.request-result .text {
height: 300px;
overflow-y: auto;
}
.sub-result .info {
background-color: #FFF;
}
.sub-result .method {
border-left: 5px solid #1E90FF;
padding-left: 20px;
}
.ms-cards :deep(.el-card__body) {
padding: 1px;
}
.sub-result:last-child {
border-bottom: 1px solid #EBEEF5;
}
.ms-test-running {
color: #783887;
}
.ms-test-error_code {
color: #F6972A;
background-color: #FDF5EA;
border-color: #FDF5EA;
}
.ms-api-col {
background-color: #EFF0F0;
border-color: #EFF0F0;
margin-right: 10px;
font-size: 12px;
color: #64666A;
}
.ms-api-col-create {
background-color: #EBF2F2;
border-color: #008080;
margin-right: 10px;
font-size: 12px;
color: #008080;
}
:deep(.el-step__icon) {
width: 20px;
height: 20px;
font-size: 12px;
}
.el-divider--horizontal {
margin: 2px 0;
background: 0 0;
border-top: 1px solid #e8eaec;
}
.ms-req-name {
display: inline-block;
margin: 0 5px;
padding-bottom: 0;
text-overflow: ellipsis;
vertical-align: middle;
white-space: nowrap;
width: 350px;
}
.ms-req-name-col {
overflow-x: hidden;
}
.icon.is-active {
transform: rotate(90deg);
}
</style>

View File

@ -0,0 +1,175 @@
<template>
<div class="request-result">
<div>
<el-row type="flex" justify="end" class="info">
<el-col :span="24">
<div class="time">
{{ $t('api_report.start_time') }}{{ result.startTime | timestampFormatDate(true) }}
{{ $t('report.test_end_time') }}{{ result.endTime | timestampFormatDate(true) }}
</div>
</el-col>
</el-row>
</div>
<el-collapse-transition>
<div v-show="isActive">
<div class="text-container">
<el-collapse-transition>
<el-tabs value="body" v-show="isActive">
<el-tab-pane :label="$t('ui.log')" name="body" class="pane">
<div class="ms-div">
<pre>{{ getBody() }}</pre>
</div>
</el-tab-pane>
<el-tab-pane :label="$t('api_report.assertions')" name="assertions" class="pane assertions">
<ms-assertion-results :show-content="false" :assertions="result.assertions"/>
</el-tab-pane>
<el-tab-pane :label="$t('api_test.request.extract.label')" name="label" class="pane">
<pre>{{ result.vars }}</pre>
</el-tab-pane>
</el-tabs>
</el-collapse-transition>
</div>
</div>
</el-collapse-transition>
</div>
</template>
<script>
export default {
name: "UiCommandResultDetail",
components: {
},
props: {
request: Object,
scenarioName: String,
requestType: String,
console: String,
result: {
type: Object,
default() {
return {}
}
},
},
data() {
return {
isActive: true,
isCodeEditAlive: true,
modes: ['text', 'json', 'xml', 'html'],
sqlModes: ['text', 'table'],
mode: 'text'
}
},
methods: {
active() {
this.isActive = !this.isActive;
},
reload() {
this.isCodeEditAlive = false;
this.$nextTick(() => (this.isCodeEditAlive = true));
},
modeChange(mode) {
this.mode = mode;
},
sqlModeChange(mode) {
this.mode = mode;
},
getBody(){
if (!this.result.success) {
return this.result.body ? this.result.body : ""
} else {
return "OK";
}
}
},
watch: {
'request.responseResult'() {
this.reload();
}
},
computed: {
assertion() {
return this.request.passAssertions + " / " + this.request.totalAssertions;
},
hasSub() {
return this.request.subRequestResults.length > 0;
},
}
}
</script>
<style scoped>
.request-result .info {
background-color: #F9F9F9;
margin-left: 20px;
cursor: pointer;
}
.request-result {
width: 100%;
min-height: 40px;
padding: 2px 0;
}
.request-result .time {
color: #7f7f7f;
font-size: 12px;
font-weight: 400;
margin-top: 4px;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
word-break: break-all;
}
.text-container .icon {
padding: 5px;
}
.text-container .collapse {
cursor: pointer;
}
.text-container .collapse:hover {
opacity: 0.8;
}
.text-container .icon.is-active {
transform: rotate(90deg);
}
.text-container .pane {
background-color: #F5F5F5;
padding: 1px 0;
height: 250px;
overflow-y: auto;
}
.text-container .pane.cookie {
padding: 0;
}
:deep(.el-tabs__nav-wrap::after) {
height: 0px;
}
.ms-div {
margin-top: 20px;
}
pre {
margin: 0;
}
.time {
text-align: end;
margin-right: 20px;
}
</style>

View File

@ -0,0 +1,59 @@
<template>
<div class="el-image-viewer__wrapper" style="z-index: 999" v-show="active">
<div class="viewer-mask">
<div class="el-image-viewer__canvas">
<div class="image-wrap">
<el-image
fit="contain"
:src="src"
class="img-content"
></el-image>
</div>
</div>
</div>
<span
class="el-image-viewer__btn el-image-viewer__close"
style="color: #fff"
@click.stop="close"
><i class="el-icon-close"></i
></span>
</div>
</template>
<script>
export default {
name: "UiScreenshotViewer",
props:['src'],
data() {
return {
active: false,
};
},
methods: {
open() {
this.active = true;
},
close() {
this.active = false;
},
},
};
</script>
<style scoped>
.image-wrap {
width: 80%;
height: 100%;
background-color: #fff;
overflow-y: scroll;
}
.viewer-mask {
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
background-color: rgba(0, 0, 0, 0.5);
}
.img-content.el-image {
width: 100%;
}
</style>

View File

@ -0,0 +1,762 @@
<template>
<ms-container v-loading="loading">
<ms-main-container class="api-report-content">
<el-card class="report-body">
<section class="report-container" v-if="this.report.testId">
<!-- header -->
<ms-api-report-view-header
:show-cancel-button="showCancelButton"
:show-rerun-button="showRerunButton"
:is-plan="isPlan"
:is-template="isTemplate"
:debug="debug"
:report="report"
:project-env-map="projectEnvMap"
@reportExport="handleExport"
@reportSave="handleSave"/>
<!-- content -->
<main v-if="isNotRunning">
<!-- content header chart -->
<ms-metric-chart :content="content" :totalTime="totalTime" :report="report"/>
<el-tabs v-model="activeName" @tab-click="handleClick">
<!-- all step-->
<el-tab-pane :label="$t('api_report.total')" name="total">
<ms-scenario-results
:treeData="fullTreeNodes"
:console="content.console"
:report="report"
:is-share="isShare"
:share-id="shareId"
v-on:requestResult="requestResult"
ref="resultsTree"/>
</el-tab-pane>
<!-- fail step -->
<el-tab-pane name="fail">
<template slot="label">
<span class="fail">{{ $t('api_report.fail') }}</span>
</template>
<ms-scenario-results
v-on:requestResult="requestResult"
:console="content.console"
:report="report"
:is-share="isShare"
:share-id="shareId"
:treeData="fullTreeNodes" ref="failsTree"
:errorReport="content.error"/>
</el-tab-pane>
<!--error step -->
<el-tab-pane name="errorReport" v-if="content.errorCode > 0">
<template slot="label">
<span class="fail" style="color: #F6972A">
{{ $t('error_report_library.option.name') }}
</span>
</template>
<ms-scenario-results
v-on:requestResult="requestResult"
:report="report"
:is-share="isShare"
:share-id="shareId"
:console="content.console"
:treeData="fullTreeNodes" ref="errorReportTree"/>
</el-tab-pane>
<!-- Not performed step -->
<el-tab-pane name="unExecute" v-if="content.unExecute > 0">
<template slot="label">
<span class="fail"
style="color: #9C9B9A">
{{ $t('api_test.home_page.detail_card.unexecute') }}
</span>
</template>
<ms-scenario-results
v-on:requestResult="requestResult"
:report="report"
:is-share="isShare"
:share-id="shareId"
:console="content.console"
:treeData="fullTreeNodes" ref="unExecuteTree"/>
</el-tab-pane>
<!-- console -->
<el-tab-pane name="console">
<template slot="label">
<span class="console">{{ $t('api_test.definition.request.console') }}</span>
</template>
<ms-code-edit
:mode="'text'"
:read-only="true"
:data.sync="content.console"
height="calc(100vh - 500px)"/>
</el-tab-pane>
</el-tabs>
</main>
</section>
</el-card>
</ms-main-container>
</ms-container>
</template>
<script>
import MsRequestResult from "./RequestResult";
import MsRequestResultTail from "./RequestResultTail";
import MsScenarioResult from "./ScenarioResult";
import MsMetricChart from "./MetricChart";
import MsScenarioResults from "./ScenarioResults";
import MsContainer from "metersphere-frontend/src/components/MsContainer";
import MsMainContainer from "metersphere-frontend/src/components/MsMainContainer";
import MsApiReportViewHeader from "./ApiReportViewHeader";
import {STEP} from "./Setting";
import MsCodeEdit from "metersphere-frontend/src/components/MsCodeEdit";
import {getCurrentProjectID, getUUID, hasLicense, windowPrint} from "@/business/utils/sdk-utils";
import {getScenarioReport, getScenarioReportAll, getShareScenarioReport} from "@/api/ui-report";
export default {
name: "UiShareReportDetail",
components: {
MsApiReportViewHeader,
MsMainContainer,
MsCodeEdit,
MsContainer, MsScenarioResults, MsRequestResultTail, MsMetricChart, MsScenarioResult, MsRequestResult
},
data() {
return {
activeName: "total",
content: {},
report: {},
loading: true,
fails: [],
failsTreeNodes: [],
totalTime: 0,
isRequestResult: false,
request: {},
isActive: false,
scenarioName: null,
reportExportVisible: false,
fullTreeNodes: [],
showRerunButton: false,
stepFilter: new STEP,
exportReportIsOk: false,
tempResult: [],
projectEnvMap: {},
}
},
activated() {
this.isRequestResult = false;
},
props: {
reportId: String,
currentProjectId: String,
infoDb: Boolean,
debug: Boolean,
isTemplate: Boolean,
templateReport: Object,
isShare: Boolean,
shareId: String,
isPlan: Boolean,
showCancelButton: {
type: Boolean,
default: true
}
},
watch: {
reportId() {
if (!this.isTemplate) {
this.getReport();
}
},
templateReport() {
if (this.isTemplate) {
this.getReport();
}
}
},
methods: {
filter(index) {
if (index === "1") {
this.$refs.failsTree.filter(index);
} else if (this.activeName === "errorReport") {
this.$refs.errorReportTree.filter("errorReport");
} else if (this.activeName === "unExecute") {
this.$refs.unExecuteTree.filter("unexecute");
}
},
init() {
this.loading = true;
this.projectEnvMap = {};
this.content = {};
this.fails = [];
this.report = {};
this.fullTreeNodes = [];
this.failsTreeNodes = [];
this.isRequestResult = false;
this.activeName = "total";
this.showRerunButton = false;
},
rerunVerify() {
if (hasLicense() && this.fullTreeNodes && this.fullTreeNodes.length > 0 && !this.isShare) {
this.fullTreeNodes.forEach(item => {
item.redirect = true;
if (item.totalStatus === 'fail' || item.totalStatus === 'error' || item.unExecuteTotal > 0
|| (item.type === "API" && item.totalStatus === 'unexecute')) {
this.showRerunButton = true;
}
}
)
}
},
handleClick(tab, event) {
this.isRequestResult = false;
if (this.report && this.report.reportVersion && this.report.reportVersion > 1) {
this.filter(tab.index);
}
},
active() {
this.isActive = !this.isActive;
},
formatResult(res) {
let resMap = new Map;
let array = [];
if (res && res.scenarios) {
res.scenarios.forEach(item => {
if (item && item.requestResults) {
item.requestResults.forEach(req => {
req.responseResult.console = res.console;
resMap.set(req.id + req.name, req);
req.name = item.name + "^@~@^" + req.name + "UUID=" + getUUID();
array.push(req);
})
}
})
}
this.formatTree(array, this.fullTreeNodes);
this.sort(this.fullTreeNodes);
this.$emit('refresh', resMap);
},
formatTree(array, tree) {
array.map((item) => {
let key = item.name;
let nodeArray = key.split('^@~@^');
let children = tree;
//1
//hashTreeID
let scenarioId = "";
let scenarioName = "";
if (item.scenario) {
let scenarioArr = JSON.parse(item.scenario);
if (scenarioArr.length > 1) {
let scenarioIdArr = scenarioArr[0].split("_");
scenarioId = scenarioIdArr[0];
scenarioName = scenarioIdArr[1];
}
}
//
for (let i = 0; i < nodeArray.length; i++) {
if (!nodeArray[i]) {
continue;
}
let node = {
label: nodeArray[i],
value: item,
};
if (i !== (nodeArray.length - 1)) {
node.children = [];
} else {
if (item.subRequestResults && item.subRequestResults.length > 0) {
let itemChildren = this.deepFormatTreeNode(item.subRequestResults);
node.children = itemChildren;
if (node.label.indexOf("UUID=")) {
node.label = node.label.split("UUID=")[0];
}
}
}
if (children.length === 0) {
children.push(node);
}
let isExist = false;
for (let j in children) {
if (children[j].label === node.label) {
let idIsPath = true;
//ID
//
if (i === nodeArray.length - 2) {
idIsPath = false;
let childId = "";
let childName = "";
if (children[j].value && children[j].value.scenario) {
let scenarioArr = JSON.parse(children[j].value.scenario);
if (scenarioArr.length > 1) {
let childArr = scenarioArr[0].split("_");
childId = childArr[0];
if (childArr.length > 1) {
childName = childArr[1];
}
}
}
if (scenarioId === "") {
idIsPath = true;
} else if (scenarioId === childId) {
idIsPath = true;
} else if (scenarioName !== childName) {
//ID
idIsPath = true;
}
}
if (idIsPath) {
if (i !== nodeArray.length - 1 && !children[j].children) {
children[j].children = [];
}
children = (i === nodeArray.length - 1 ? children : children[j].children);
isExist = true;
break;
}
}
}
if (!isExist) {
children.push(node);
if (i !== nodeArray.length - 1 && !children[children.length - 1].children) {
children[children.length - 1].children = [];
}
children = (i === nodeArray.length - 1 ? children : children[children.length - 1].children);
}
}
})
},
deepFormatTreeNode(array) {
let returnChildren = [];
array.map((item) => {
let children = [];
let key = item.name.split('^@~@^')[0];
let nodeArray = key.split('<->');
//1
//hashTreeID
let scenarioId = "";
let scenarioName = "";
if (item.scenario) {
let scenarioArr = JSON.parse(item.scenario);
if (scenarioArr.length > 1) {
let scenarioIdArr = scenarioArr[0].split("_");
scenarioId = scenarioIdArr[0];
scenarioName = scenarioIdArr[1];
}
}
//
let node = {
label: nodeArray[0],
value: item,
children: []
};
if (item.subRequestResults && item.subRequestResults.length > 0) {
let itemChildren = this.deepFormatTreeNode(item.subRequestResults);
node.children = itemChildren;
}
children.push(node);
children.forEach(itemNode => {
returnChildren.push(itemNode);
});
});
return returnChildren;
},
recursiveSorting(arr) {
for (let i in arr) {
if (arr[i]) {
arr[i].index = Number(i) + 1;
if (arr[i].children && arr[i].children.length > 0) {
this.recursiveSorting(arr[i].children);
}
}
}
},
sort(scenarioDefinition) {
for (let i in scenarioDefinition) {
//
if (scenarioDefinition[i]) {
scenarioDefinition[i].index = Number(i) + 1;
if (scenarioDefinition[i].children && scenarioDefinition[i].children.length > 0) {
this.recursiveSorting(scenarioDefinition[i].children);
}
}
}
},
getReportByExport() {
if (this.exportReportIsOk) {
this.startExport();
} else {
getScenarioReportAll(this.reportId).then(data => {
if (data && data.data.content) {
let report = JSON.parse(data.data.content);
if (report.projectEnvMap) {
this.projectEnvMap = report.projectEnvMap;
}
this.content = report;
this.fullTreeNodes = report.steps;
this.content.console = report.console;
this.content.error = report.error;
let successCount = (report.total - report.error - report.errorCode - report.unExecute);
this.content.success = successCount;
this.totalTime = report.totalTime;
}
this.exportReportIsOk = true;
setTimeout(this.startExport, 500)
});
}
},
getReport() {
this.init();
if (this.isTemplate) {
//
if (this.templateReport) {
this.handleGetScenarioReport(this.templateReport);
} else {
this.report = this.templateReport;
this.buildReport();
}
} else if (this.isShare) {
if (this.reportId) {
getShareScenarioReport(this.shareId, this.reportId).then(data => {
this.checkReport(data.data);
this.handleGetScenarioReport(data.data);
});
}
} else {
getScenarioReport(this.reportId).then(data => {
this.checkReport(data.data);
this.handleGetScenarioReport(data.data);
});
}
},
checkReport(data) {
if (!data) {
this.$emit('reportNotExist');
}
},
handleGetScenarioReport(data) {
if (data) {
this.report = data;
if (this.report.reportVersion && this.report.reportVersion > 1) {
this.report.status = data.status;
if (!this.isNotRunning) {
setTimeout(this.getReport, 2000)
} else {
if (data.content) {
let report = JSON.parse(data.content);
this.content = report;
if (report.projectEnvMap) {
this.projectEnvMap = report.projectEnvMap;
}
if (data.reportType === "UI_INDEPENDENT") {
this.tempResult = report.steps;
//
try {
this.checkOrder(this.tempResult);
this.fullTreeNodes = this.tempResult;
} catch (e) {
this.fullTreeNodes = report.steps;
}
} else {
this.fullTreeNodes = report.steps;
}
this.content.console = report.console;
this.content.error = report.error;
let successCount = (report.total - report.error - report.errorCode - report.unExecute);
this.content.success = successCount;
this.totalTime = report.totalTime;
}
//
if (this.report && this.report.reportType === 'SCENARIO_INTEGRATED' || this.report.reportType === 'API_INTEGRATED') {
this.rerunVerify();
}
this.loading = false;
}
} else {
this.buildReport();
}
} else {
this.$emit('invisible');
this.$warning(this.$t('commons.report_delete'));
}
},
checkOrder(origin) {
if (!origin) {
return;
}
if (Array.isArray(origin)) {
this.sortChildren(origin);
origin.forEach(v => {
if (v.children) {
this.checkOrder(v.children)
}
})
}
},
sortChildren(source) {
if (!source) {
return;
}
source.forEach(item => {
let children = item.children;
if (children && children.length > 0) {
let tempArr = new Array(children.length);
let tempMap = new Map();
for (let i = 0; i < children.length; i++) {
if (!children[i].value || !children[i].value.startTime || children[i].value.startTime === 0) {
//valuestep
tempArr[i] = children[i];
//
tempMap.set(children[i].stepId, children[i])
}
}
//step
let arr = children.filter(m => {
return !tempMap.get(m.stepId);
}).sort((m, n) => {
//
return m.value.startTime - n.value.startTime;
});
//arr() tempArr
for (let j = 0, i = 0; j < tempArr.length; j++) {
if (!tempArr[j]) {
//
tempArr[j] = arr[i];
i++;
}
//
tempArr[j].index = j + 1;
}
//
item.children = tempArr;
}
})
},
buildReport() {
if (this.report) {
if (this.isNotRunning) {
this.content = JSON.parse(this.report.content);
if (!this.content) {
this.content = {scenarios: []};
}
this.formatResult(this.content);
this.getFails();
this.computeTotalTime();
this.loading = false;
} else {
setTimeout(this.getReport, 2000)
}
} else {
this.loading = false;
this.$error(this.$t('api_report.not_exist'));
}
},
getFails() {
if (this.isNotRunning) {
this.fails = [];
let array = [];
this.totalTime = 0
if (this.content.scenarios) {
this.content.scenarios.forEach((scenario) => {
this.totalTime = this.totalTime + Number(scenario.responseTime)
let failScenario = Object.assign({}, scenario);
if (scenario.error > 0) {
this.fails.push(failScenario);
failScenario.requestResults = [];
scenario.requestResults.forEach((request) => {
if (!request.success) {
let failRequest = Object.assign({}, request);
failScenario.requestResults.push(failRequest);
array.push(request);
}
})
}
})
}
this.formatTree(array, this.failsTreeNodes);
this.sort(this.failsTreeNodes);
}
},
computeTotalTime() {
if (this.content.scenarios) {
let startTime = 0;
let endTime = 0;
let requestTime = 0;
this.content.scenarios.forEach((scenario) => {
scenario.requestResults.forEach((request) => {
if (request.startTime && Number(request.startTime)) {
startTime = request.startTime;
}
if (request.endTime && Number(request.endTime)) {
endTime = request.endTime;
}
let resTime;
if (startTime === 0 || endTime === 0) {
resTime = 0
} else {
resTime = endTime - startTime
}
requestTime = requestTime + resTime;
})
})
this.totalTime = requestTime
}
},
requestResult(requestResult) {
this.active();
this.isRequestResult = false;
this.$nextTick(function () {
this.isRequestResult = true;
this.request = requestResult.request;
this.scenarioName = requestResult.scenarioName;
});
},
formatExportApi(array, scenario) {
array.forEach(item => {
if (this.stepFilter && this.stepFilter.get("AllSamplerProxy").indexOf(item.type) !== -1) {
if (item.errorCode) {
item.value.errorCode = item.errorCode;
}
scenario.requestResults.push(item.value);
}
if (item.children && item.children.length > 0) {
this.formatExportApi(item.children, scenario);
}
})
},
handleExport() {
this.getReportByExport();
},
startExport() {
if (this.report.reportVersion && this.report.reportVersion > 1) {
if (this.report.reportType === 'API_INTEGRATED' || this.report.reportType === 'UI_INTEGRATED') {
let scenario = {name: "", requestResults: []};
this.content.scenarios = [scenario];
this.formatExportApi(this.fullTreeNodes, scenario);
} else {
if (this.fullTreeNodes) {
this.fullTreeNodes.forEach(item => {
if (item.type === "scenario" || item.type === "UiScenario") {
let scenario = {name: item.label, requestResults: []};
if (this.content.scenarios && this.content.scenarios.length > 0) {
this.content.scenarios.push(scenario);
} else {
this.content.scenarios = [scenario];
}
this.formatExportApi(item.children, scenario);
}
})
}
}
}
this.reportExportVisible = true;
let reset = this.exportReportReset;
this.$nextTick(() => {
windowPrint('apiTestReport', 0.57);
reset();
});
},
handleSave() {
if (!this.report.name) {
this.$warning(this.$t('api_test.automation.report_name_info'));
return;
}
this.loading = true;
let url = "/ui/scenario/report/reName";
this.result = this.$post(url, {
id: this.report.id,
name: this.report.name,
reportType: this.report.reportType
}, response => {
this.$success(this.$t('commons.save_success'));
this.loading = false;
this.$emit('refresh');
}, error => {
this.loading = false;
});
},
exportReportReset() {
this.$router.go(0);
},
handleProjectChange() {
this.$router.push('/api/automation/report');
},
},
created() {
this.getReport();
},
destroyed() {
},
computed: {
path() {
return "/api/test/edit?id=" + this.report.testId;
},
isNotRunning() {
return "Running" !== this.report.status;
},
projectId() {
return getCurrentProjectID();
},
}
}
</script>
<style>
.report-container .el-tabs__header {
margin-bottom: 1px;
}
</style>
<style scoped>
.report-container {
height: calc(100vh - 155px);
min-height: 600px;
overflow-y: auto;
}
.report-header {
font-size: 15px;
}
.report-header a {
text-decoration: none;
}
.report-header .time {
color: #909399;
margin-left: 10px;
}
.report-container .fail {
color: #F56C6C;
}
.report-container .is-active .fail {
color: inherit;
}
.report-console {
height: calc(100vh - 270px);
overflow-y: auto;
}
.export-button {
float: right;
}
.scenario-result .icon.is-active {
transform: rotate(90deg);
}
.report-body{
min-width: 750px!important;
}
</style>

View File

@ -165,7 +165,7 @@
<!-- 执行结果 -->
<el-drawer :visible.sync="runVisible" :destroy-on-close="true" direction="ltr" :withHeader="true" :modal="false"
size="90%">
<micro-app :to="`/report/view/${reportId}`" service="ui"/>
<micro-app :to="`/ui/report/view/${reportId}`" service="ui"/>
</el-drawer>
</div>
</el-card>

View File

@ -1,4 +1,4 @@
export {operationConfirm, removeGoBackListener, handleCtrlSEvent, byteToSize, getTypeByFileName, strMapToObj} from "metersphere-frontend/src/utils";
export {operationConfirm, removeGoBackListener, handleCtrlSEvent, byteToSize, getTypeByFileName, strMapToObj, getUUID, windowPrint} from "metersphere-frontend/src/utils";
export {parseCustomFilesForList, getCustomFieldFilter, buildBatchParam} from "metersphere-frontend/src/utils/tableUtils";
export {getCurrentProjectID, getCurrentWorkspaceId, getCurrentUser} from "metersphere-frontend/src/utils/token";
export {hasLicense, hasPermissions, hasPermission} from "metersphere-frontend/src/utils/permission";