refactor(接口自动化): 重构报告页面,按照层级显示

This commit is contained in:
fit2-zhao 2021-04-02 16:28:19 +08:00 committed by fit2-zhao
parent d811c6bf79
commit a6e9a27f05
8 changed files with 273 additions and 132 deletions

View File

@ -201,7 +201,7 @@ public abstract class MsTestElement {
return null; return null;
} }
protected void addCsvDataSet(HashTree tree, List<ScenarioVariable> variables,ParameterConfig config) { protected void addCsvDataSet(HashTree tree, List<ScenarioVariable> variables, ParameterConfig config) {
if (CollectionUtils.isNotEmpty(variables)) { if (CollectionUtils.isNotEmpty(variables)) {
List<ScenarioVariable> list = variables.stream().filter(ScenarioVariable::isCSVValid).collect(Collectors.toList()); List<ScenarioVariable> list = variables.stream().filter(ScenarioVariable::isCSVValid).collect(Collectors.toList());
if (CollectionUtils.isNotEmpty(list)) { if (CollectionUtils.isNotEmpty(list)) {
@ -276,10 +276,7 @@ public abstract class MsTestElement {
if (element.getParent() == null) { if (element.getParent() == null) {
return; return;
} }
if (MsTestElementConstants.LoopController.name().equals(element.getType())) { path.append(StringUtils.isEmpty(element.getName()) ? element.getType() : element.getName()).append("^@~@^");
return;
}
path.append(element.getResourceId()).append("/");
getFullPath(element.getParent(), path); getFullPath(element.getParent(), path);
} }
@ -300,7 +297,6 @@ public abstract class MsTestElement {
// 获取全路径以备后面使用 // 获取全路径以备后面使用
StringBuilder fullPath = new StringBuilder(); StringBuilder fullPath = new StringBuilder();
getFullPath(parent, fullPath); getFullPath(parent, fullPath);
return fullPath + "<->" + parent.getName(); return fullPath + "<->" + parent.getName();
} }
return ""; return "";

View File

@ -66,6 +66,8 @@ public class MsJmeterElement extends MsTestElement {
} }
if (CollectionUtils.isNotEmpty(hashTree)) { if (CollectionUtils.isNotEmpty(hashTree)) {
for (MsTestElement el : hashTree) { for (MsTestElement el : hashTree) {
// 给所有孩子加一个父亲标志
el.setParent(this);
el.toHashTree(elementTree, el.getHashTree(), config); el.toHashTree(elementTree, el.getHashTree(), config);
} }
} }

View File

@ -43,50 +43,16 @@ public class TestResult {
private static final String SEPARATOR = "<->"; private static final String SEPARATOR = "<->";
public void addScenario(ScenarioResult result) { public void addScenario(ScenarioResult result) {
Map<String, List<RequestResult>> requestResultMap = new LinkedHashMap<>();
if (result != null && CollectionUtils.isNotEmpty(result.getRequestResults())) { if (result != null && CollectionUtils.isNotEmpty(result.getRequestResults())) {
result.getRequestResults().forEach(item -> { result.getRequestResults().forEach(item -> {
if (StringUtils.isNotEmpty(item.getName()) && item.getName().indexOf(SEPARATOR) != -1) { if (StringUtils.isNotEmpty(item.getName()) && item.getName().indexOf(SEPARATOR) != -1) {
String array[] = item.getName().split(SEPARATOR); String array[] = item.getName().split(SEPARATOR);
String scenarioName = item.getName().replace(array[0] + SEPARATOR, ""); item.setName(array[1] + array[0]);
item.setName(array[0]);
if (requestResultMap.containsKey(scenarioName)) {
requestResultMap.get(scenarioName).add(item);
} else {
List<RequestResult> requestResults = new LinkedList<>();
requestResults.add(item);
requestResultMap.put(scenarioName, requestResults);
}
item.getSubRequestResults().forEach(subItem -> { item.getSubRequestResults().forEach(subItem -> {
subItem.setName(item.getName()); subItem.setName(array[0]);
}); });
} else {
if (requestResultMap.containsKey(result.getName())) {
requestResultMap.get(result.getName()).add(item);
} else {
List<RequestResult> requestResults = new LinkedList<>();
requestResults.add(item);
requestResultMap.put(result.getName(), requestResults);
}
item.getSubRequestResults().forEach(subItem -> {
subItem.setName(item.getName());
});
} }
}); });
}
if (!requestResultMap.isEmpty()) {
requestResultMap.forEach((k, v) -> {
ScenarioResult scenarioResult = new ScenarioResult();
BeanUtils.copyBean(scenarioResult, result);
scenarioResult.setName(k);
if (k.indexOf(SEPARATOR) != -1) {
scenarioResult.setName(k.split(SEPARATOR)[1]);
}
scenarioResult.setRequestResults(v);
scenarios.add(scenarioResult);
});
} else {
scenarios.add(result); scenarios.add(result);
} }
} }

View File

@ -3,26 +3,21 @@
<ms-main-container> <ms-main-container>
<el-card> <el-card>
<section class="report-container" v-if="this.report.testId"> <section class="report-container" v-if="this.report.testId">
<ms-api-report-view-header :debug="debug" :report="report" @reportExport="handleExport" @reportSave="handleSave"/> <ms-api-report-view-header :debug="debug" :report="report" @reportExport="handleExport" @reportSave="handleSave"/>
<main v-if="isNotRunning">
<main v-if="this.isNotRunning">
<ms-metric-chart :content="content" :totalTime="totalTime"/> <ms-metric-chart :content="content" :totalTime="totalTime"/>
<div> <div>
<!--<ms-scenario-results :scenarios="content.scenarios" v-on:requestResult="requestResult"/>-->
<el-tabs v-model="activeName" @tab-click="handleClick"> <el-tabs v-model="activeName" @tab-click="handleClick">
<el-tab-pane :label="$t('api_report.total')" name="total"> <el-tab-pane :label="$t('api_report.total')" name="total">
<ms-scenario-results :scenarios="content.scenarios" v-on:requestResult="requestResult"/> <ms-scenario-results :treeData="fullTreeNodes" v-on:requestResult="requestResult"/>
</el-tab-pane> </el-tab-pane>
<el-tab-pane name="fail"> <el-tab-pane name="fail">
<template slot="label"> <template slot="label">
<span class="fail">{{ $t('api_report.fail') }}</span> <span class="fail">{{ $t('api_report.fail') }}</span>
</template> </template>
<ms-scenario-results v-on:requestResult="requestResult" :scenarios="fails"/> <ms-scenario-results v-on:requestResult="requestResult" :treeData="failsTreeNodes"/>
</el-tab-pane> </el-tab-pane>
</el-tabs> </el-tabs>
</div> </div>
<ms-api-report-export v-if="reportExportVisible" id="apiTestReport" :title="report.testName" <ms-api-report-export v-if="reportExportVisible" id="apiTestReport" :title="report.testName"
:content="content" :total-time="totalTime"/> :content="content" :total-time="totalTime"/>
@ -62,6 +57,7 @@
report: {}, report: {},
loading: true, loading: true,
fails: [], fails: [],
failsTreeNodes: [],
totalTime: 0, totalTime: 0,
isRequestResult: false, isRequestResult: false,
request: {}, request: {},
@ -69,6 +65,7 @@
scenarioName: null, scenarioName: null,
reportExportVisible: false, reportExportVisible: false,
requestType: undefined, requestType: undefined,
fullTreeNodes: [],
} }
}, },
activated() { activated() {
@ -92,6 +89,8 @@
this.content = {}; this.content = {};
this.fails = []; this.fails = [];
this.report = {}; this.report = {};
this.fullTreeNodes = [];
this.failsTreeNodes = [];
this.isRequestResult = false; this.isRequestResult = false;
}, },
handleClick(tab, event) { handleClick(tab, event) {
@ -102,17 +101,88 @@
}, },
formatResult(res) { formatResult(res) {
let resMap = new Map; let resMap = new Map;
let array = [];
let i = 0;
if (res && res.scenarios) { if (res && res.scenarios) {
res.scenarios.forEach(item => { res.scenarios.forEach(item => {
if (item && item.requestResults) { if (item && item.requestResults) {
item.requestResults.forEach(req => { item.requestResults.forEach(req => {
resMap.set(req.id, req); resMap.set(req.id, req);
req.index = i;
i++;
array.push(req);
}) })
} }
}) })
} }
this.formatTree(array, this.fullTreeNodes);
this.sort(this.fullTreeNodes);
this.$emit('refresh', resMap); this.$emit('refresh', resMap);
}, },
formatTree(array, tree) {
array.map((item) => {
let key = item.name;
let nodeArray = key.split('^@~@^');
let children = tree;
//
for (let i in nodeArray) {
if (!nodeArray[i]) {
continue;
}
let node = {
label: nodeArray[i],
value: item,
};
if (i !== nodeArray.length) {
node.children = [];
}
if (children.length === 0) {
children.push(node);
}
let isExist = false;
for (let j in children) {
if (children[j].label === node.label) {
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);
}
}
})
},
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);
}
}
}
},
getReport() { getReport() {
this.init(); this.init();
if (this.reportId) { if (this.reportId) {
@ -146,6 +216,7 @@
getFails() { getFails() {
if (this.isNotRunning) { if (this.isNotRunning) {
this.fails = []; this.fails = [];
let array = [];
this.totalTime = 0 this.totalTime = 0
if (this.content.scenarios) { if (this.content.scenarios) {
this.content.scenarios.forEach((scenario) => { this.content.scenarios.forEach((scenario) => {
@ -158,11 +229,14 @@
if (!request.success) { if (!request.success) {
let failRequest = Object.assign({}, request); let failRequest = Object.assign({}, request);
failScenario.requestResults.push(failRequest); failScenario.requestResults.push(failRequest);
array.push(request);
} }
}) })
} }
}) })
} }
this.formatTree(array, this.failsTreeNodes);
this.sort(this.failsTreeNodes);
} }
}, },
computeTotalTime() { computeTotalTime() {

View File

@ -1,55 +1,58 @@
<template> <template>
<div class="request-result"> <el-card class="ms-cards">
<p class="el-divider--horizontal"></p> <div class="request-result">
<div @click="active"> <div @click="active">
<el-row :gutter="10" type="flex" align="middle" class="info"> <el-row :gutter="10" type="flex" align="middle" class="info">
<el-col :span="14" v-if="indexNumber!=undefined"> <el-col :span="10" v-if="indexNumber!=undefined">
<div class="method"> <div class="method">
<div class="el-step__icon is-text ms-api-col" v-if="indexNumber%2 ==0"> <div class="el-step__icon is-text ms-api-col-create">
<div class="el-step__icon-inner"> {{ indexNumber+1 }}</div> <div class="el-step__icon-inner"> {{ indexNumber }}</div>
</div>
<i class="icon el-icon-arrow-right" :class="{'is-active': isActive}" @click="active" @click.stop/>
{{ getName(request.name) }}
</div> </div>
<div class="el-step__icon is-text ms-api-col-create" v-else> </el-col>
<div class="el-step__icon-inner"> {{ indexNumber+1 }}</div> <el-col :span="9">
</div> <el-tooltip effect="dark" :content="request.responseResult.responseCode" placement="bottom" :open-delay="800">
{{ request.name }} <div style="color: #5daf34" v-if="request.success">
</div> {{ request.responseResult.responseCode }}
</el-col> </div>
<div style="color: #FE6F71" v-else>
<el-col :span="5"> {{ request.responseResult.responseCode }}
<el-tooltip effect="dark" :content="request.responseResult.responseCode" placement="bottom" :open-delay="800"> </div>
<div style="color: #5daf34" v-if="request.success">{{ request.responseResult.responseCode }}</div> </el-tooltip>
<div style="color: #FE6F71" v-else>{{ request.responseResult.responseCode }}</div> </el-col>
</el-tooltip> <el-col :span="3">
</el-col>
<el-col :span="3">
<span v-if="request.success"> <span v-if="request.success">
{{request.responseResult.responseTime}} ms {{request.responseResult.responseTime}} ms
</span> </span>
<span style="color: #FE6F71" v-else> <span style="color: #FE6F71" v-else>
{{request.responseResult.responseTime}} ms {{request.responseResult.responseTime}} ms
</span> </span>
</el-col> </el-col>
<el-col :span="2">
<el-col :span="2"> <div>
<div> <el-tag size="mini" type="success" v-if="request.success">
<el-tag size="mini" type="success" v-if="request.success"> {{ $t('api_report.success') }}
{{ $t('api_report.success') }} </el-tag>
</el-tag> <el-tag size="mini" type="danger" v-else>
<el-tag size="mini" type="danger" v-else> {{ $t('api_report.fail') }}
{{ $t('api_report.fail') }} </el-tag>
</el-tag> </div>
</div> </el-col>
</el-col> </el-row>
</el-row>
</div>
<el-collapse-transition>
<div v-show="isActive" style="width: 99%">
<ms-request-result-tail v-if="isActive" :request-type="requestType" :request="request"
:scenario-name="scenarioName"/>
</div> </div>
</el-collapse-transition>
</div> <el-collapse-transition>
<div v-show="isActive" style="width: 99%">
<ms-request-result-tail :scenario-name="scenarioName"
:request-type="requestType"
:request="request"
v-if="isActive"/>
</div>
</el-collapse-transition>
</div>
</el-card>
</template> </template>
<script> <script>
@ -61,19 +64,46 @@
export default { export default {
name: "MsRequestResult", name: "MsRequestResult",
components: {MsResponseText, MsRequestText, MsAssertionResults, MsRequestMetric, MsRequestResultTail}, components: {
MsResponseText,
MsRequestText,
MsAssertionResults,
MsRequestMetric,
MsRequestResultTail
},
props: { props: {
request: Object, request: Object,
scenarioName: String, scenarioName: String,
indexNumber: Number, indexNumber: Number,
}, },
data() { data() {
return {isActive: false, requestType: undefined,} return {
isActive: false,
requestType: "",
color: {
type: String,
default() {
return "#B8741A";
}
},
backgroundColor: {
type: String,
default() {
return "#F9F1EA";
}
},
}
}, },
methods: { methods: {
active() { active() {
this.isActive = !this.isActive; this.isActive = !this.isActive;
//this.$emit("requestResult", {request: this.request, scenarioName: this.scenarioName}); },
getName(name) {
if (name && name.indexOf("^@~@^") !== -1) {
let arr = name.split("^@~@^");
return arr[arr.length - 1];
}
return name;
} }
}, },
} }
@ -81,8 +111,7 @@
<style scoped> <style scoped>
.request-result { .request-result {
width: 100%; min-height: 30px;
min-height: 40px;
padding: 2px 0; padding: 2px 0;
} }
@ -95,7 +124,7 @@
color: #1E90FF; color: #1E90FF;
font-size: 14px; font-size: 14px;
font-weight: 500; font-weight: 500;
line-height: 40px; line-height: 35px;
padding-left: 5px; padding-left: 5px;
} }
@ -128,6 +157,10 @@
padding-left: 20px; padding-left: 20px;
} }
.ms-cards >>> .el-card__body {
padding: 1px;
}
.sub-result:last-child { .sub-result:last-child {
border-bottom: 1px solid #EBEEF5; border-bottom: 1px solid #EBEEF5;
} }
@ -148,10 +181,20 @@
color: #008080; color: #008080;
} }
/deep/ .el-step__icon {
width: 20px;
height: 20px;
font-size: 12px;
}
.el-divider--horizontal { .el-divider--horizontal {
margin: 2px 0; margin: 2px 0;
background: 0 0; background: 0 0;
border-top: 1px solid #e8eaec; border-top: 1px solid #e8eaec;
} }
.icon.is-active {
transform: rotate(90deg);
}
</style> </style>

View File

@ -1,24 +1,19 @@
<template> <template>
<div class="scenario-result" v-if="scenario && scenario.requestResults && scenario.requestResults.length>0"> <div class="scenario-result">
<div v-if="node.children && node.children.length >0 ">
<div @click="active"> <el-card class="ms-card">
<el-row :gutter="10" type="flex" align="middle" class="info"> <div class="el-step__icon is-text ms-api-col">
<el-col :span="16"> <div class="el-step__icon-inner"> {{ node.index }}</div>
<i class="icon el-icon-arrow-right" :class="{'is-active': isActive}"/>
{{scenario.name}}
</el-col>
</el-row>
</div>
<el-collapse-transition>
<div v-show="isActive">
<div v-for="(request, index) in scenario.requestResults" :key="index">
<ms-request-result :key="index" :request="request" :indexNumber="index"
v-on:requestResult="requestResult"
:scenarioName="scenario.name"/>
</div> </div>
{{node.label}}
</div> </el-card>
</el-collapse-transition> </div>
<div v-else>
<ms-request-result :request="node.value" :indexNumber="node.index"
v-on:requestResult="requestResult"
:scenarioName="node.label"/>
</div>
</div> </div>
</template> </template>
@ -27,11 +22,10 @@
export default { export default {
name: "MsScenarioResult", name: "MsScenarioResult",
components: {MsRequestResult}, components: {MsRequestResult},
props: { props: {
scenario: Object, scenario: Object,
node: Object,
}, },
data() { data() {
@ -69,6 +63,10 @@
border-top: 1px solid #DCDFE6; border-top: 1px solid #DCDFE6;
} }
.ms-card >>> .el-card__body {
padding: 10px;
}
.scenario-result .info { .scenario-result .info {
height: 40px; height: 40px;
cursor: pointer; cursor: pointer;
@ -82,4 +80,25 @@
transform: rotate(90deg); transform: rotate(90deg);
} }
.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;
}
</style> </style>

View File

@ -1,7 +1,13 @@
<template> <template>
<el-card class="scenario-results"> <el-card class="scenario-results">
<ms-scenario-result v-for="(scenario, index) in scenarios" :key="index" :scenario="scenario" :indexNumber="index" <el-tree :data="treeData"
v-on:requestResult="requestResult"/> :expand-on-click-node="false"
highlight-current
class="ms-tree ms-report-tree">
<span slot-scope="{ node, data}" style="width: 99%" @click="nodeClick(node)">
<ms-scenario-result :node="data" v-on:requestResult="requestResult"/>
</span>
</el-tree>
</el-card> </el-card>
</template> </template>
@ -10,26 +16,61 @@
export default { export default {
name: "MsScenarioResults", name: "MsScenarioResults",
components: {MsScenarioResult}, components: {MsScenarioResult},
props: { props: {
scenarios: Array scenarios: Array,
treeData: Array,
}, },
methods: { methods: {
requestResult(requestResult) { requestResult(requestResult) {
this.$emit("requestResult", requestResult); this.$emit("requestResult", requestResult);
},
nodeClick(node) {
node.expanded = !node.expanded;
} }
} }
} }
</script> </script>
<style scoped> <style scoped>
.scenario-header { .scenario-results {
border: 1px solid #EBEEF5; height: 100%;
background-color: #F9FCFF;
border-left: 0;
border-right: 0;
padding: 5px 0;
} }
.ms-report-tree >>> .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 >>> .el-dialog__body {
padding: 0px 20px;
}
</style> </style>

View File

@ -1199,7 +1199,7 @@
/deep/ .el-tree-node__content { /deep/ .el-tree-node__content {
height: 100%; height: 100%;
margin-top: 8px; margin-top: 3px;
vertical-align: center; vertical-align: center;
} }