fix(测试跟踪): 测试计划报告导出接口测试报告无法打开

--bug=1018685 --user=陈建星 【测试跟踪】测试计划-测试报告-导出-接口和性能的报告无法打开 https://www.tapd.cn/55049933/s/1273189
This commit is contained in:
chenjianxing 2022-10-23 21:42:41 +08:00 committed by jianxing
parent df34484cc6
commit 2990b81492
18 changed files with 3270 additions and 6 deletions

View File

@ -0,0 +1,210 @@
<template>
<div>
<span class="kv-description" v-if="description">
{{ description }}
</span>
<div class="kv-row" v-for="(item, index) in parameters" :key="index">
<el-row type="flex" :gutter="20" justify="space-between" align="middle">
<el-col class="kv-checkbox" v-if="isShowEnable">
<input type="checkbox" v-model="item.enable" :disabled="isReadOnly"/>
</el-col>
<el-col>
<el-input v-model="item.description" size="small" maxlength="200"
:placeholder="$t('commons.description')" show-word-limit>
</el-input>
<el-autocomplete :disabled="isReadOnly" v-if="suggestions" v-model="item.name" size="small"
:fetch-suggestions="querySearch" @change="change" :placeholder="keyText" show-word-limit/>
</el-col>
<el-col>
<ms-api-body-file-upload :parameter="item"/>
</el-col>
<el-col class="kv-delete">
<el-button size="mini" class="el-icon-delete-solid" circle @click="remove(index)"
:disabled="isDisable(index) || isReadOnly"/>
</el-col>
</el-row>
</div>
<ms-api-variable-advance ref="variableAdvance" :environment="environment" :scenario="scenario"
:parameters="parameters"
:current-item="currentItem"/>
</div>
</template>
<script>
import {KeyValue, Scenario} from "metersphere-frontend/src/model/ApiTestModel";
import {JMETER_FUNC, MOCKJS_FUNC} from "metersphere-frontend/src/utils/constants";
import MsApiVariableAdvance from "metersphere-frontend/src/components/environment/commons/ApiVariableAdvance";
import MsApiBodyFileUpload from "metersphere-frontend/src/components/environment/commons/ApiBodyFileUpload";
import {REQUIRED} from "metersphere-frontend/src/model/JsonData";
export default {
name: "MsApiVariable",
components: {MsApiBodyFileUpload, MsApiVariableAdvance},
props: {
keyPlaceholder: String,
valuePlaceholder: String,
description: String,
parameters: Array,
rest: Array,
environment: Object,
scenario: Scenario,
type: {
type: String,
default: ''
},
isReadOnly: {
type: Boolean,
default: false
},
isShowEnable: {
type: Boolean,
default: true
},
suggestions: Array
},
data() {
return {
currentItem: null,
requireds: REQUIRED
}
},
computed: {
keyText() {
return this.keyPlaceholder || this.$t("api_test.key");
},
valueText() {
return this.valuePlaceholder || this.$t("api_test.value");
}
},
methods: {
remove: function (index) {
//
this.parameters.splice(index, 1);
this.$emit('change', this.parameters);
},
change: function () {
let isNeedCreate = true;
let removeIndex = -1;
this.parameters.forEach((item, index) => {
if (!item.name && !item.value) {
//
if (index !== this.parameters.length - 1) {
removeIndex = index;
}
//
isNeedCreate = false;
}
});
if (isNeedCreate) {
this.parameters.push(new KeyValue({
type: 'text',
enable: true,
uuid: this.uuid(),
contentType: 'text/plain'
}));
}
this.$emit('change', this.parameters);
// TODO key
},
isDisable: function (index) {
return this.parameters.length - 1 === index;
},
querySearch(queryString, cb) {
let suggestions = this.suggestions;
let results = queryString ? suggestions.filter(this.createFilter(queryString)) : suggestions;
cb(results);
},
createFilter(queryString) {
return (restaurant) => {
return (restaurant.value.toLowerCase().indexOf(queryString.toLowerCase()) === 0);
};
},
funcSearch(queryString, cb) {
let funcs = MOCKJS_FUNC.concat(JMETER_FUNC);
let results = queryString ? funcs.filter(this.funcFilter(queryString)) : funcs;
// callback
cb(results);
},
funcFilter(queryString) {
return (func) => {
return (func.name.toLowerCase().indexOf(queryString.toLowerCase()) > -1);
};
},
uuid: function () {
return (((1 + Math.random()) * 0x100000) | 0).toString(16).substring(1);
},
advanced(item) {
this.$refs.variableAdvance.open();
this.currentItem = item;
},
typeChange(item) {
if (item.type === 'file') {
item.contentType = 'application/octet-stream';
} else {
item.contentType = 'text/plain';
}
}
},
created() {
if (this.parameters.length === 0 || this.parameters[this.parameters.length - 1].name) {
this.parameters.push(new KeyValue({
type: 'file',
enable: true,
required: true,
uuid: this.uuid(),
contentType: 'application/octet-stream'
}));
}
}
}
</script>
<style scoped>
.kv-description {
font-size: 13px;
}
.kv-row {
margin-top: 10px;
}
.kv-delete {
width: 60px;
}
.kv-select {
width: 50%;
}
.el-autocomplete {
width: 100%;
}
.kv-checkbox {
width: 20px;
margin-right: 10px;
}
.advanced-item-value :deep(.el-dialog__body) {
padding: 15px 25px;
}
.el-row {
margin-bottom: 5px;
}
.kv-type {
width: 70px;
}
.pointer {
cursor: pointer;
color: #1E90FF;
}
</style>

View File

@ -0,0 +1,343 @@
<template>
<div>
<el-radio-group v-model="body.type" size="mini">
<el-radio :disabled="isReadOnly" :label="type.FORM_DATA" @change="modeChange">
{{ $t('api_test.definition.request.body_form_data') }}
</el-radio>
<el-radio :disabled="isReadOnly" :label="type.WWW_FORM" @change="modeChange">
{{ $t('api_test.definition.request.body_x_www_from_urlencoded') }}
</el-radio>
<el-radio :disabled="isReadOnly" :label="type.JSON" @change="modeChange">
{{ $t('api_test.definition.request.body_json') }}
</el-radio>
<el-radio :disabled="isReadOnly" :label="type.XML" @change="modeChange">
{{ $t('api_test.definition.request.body_xml') }}
</el-radio>
<el-radio :disabled="isReadOnly" :label="type.RAW" @change="modeChange">
{{ $t('api_test.definition.request.body_raw') }}
</el-radio>
<el-radio :disabled="isReadOnly" :label="type.BINARY" @change="modeChange">
{{ $t('api_test.definition.request.body_binary') }}
</el-radio>
</el-radio-group>
<div v-if="body.type == 'Form Data' || body.type == 'WWW_FORM'">
<el-row v-if="body.type == 'Form Data' || body.type == 'WWW_FORM'">
<el-link class="ms-el-link" @click="batchAdd"> {{ $t("commons.batch_add") }}</el-link>
</el-row>
<ms-api-variable
:with-more-setting="true"
:is-read-only="isReadOnly"
:parameters="body.kvs"
:urlEncode="body.type == 'WWW_FORM'"
:isShowEnable="isShowEnable"
:scenario-definition="scenarioDefinition"
:id="id"
@editScenarioAdvance="editScenarioAdvance"
type="body"/>
</div>
<div class="ms-body" v-if="body.type == 'XML'">
<ms-code-edit
:read-only="isReadOnly"
:data.sync="body.raw"
:modes="modes"
:mode="'text'"
ref="codeEdit"/>
</div>
<div class="ms-body" v-if="body.type == 'Raw'">
<ms-code-edit
:read-only="isReadOnly"
:data.sync="body.raw"
:modes="modes"
ref="codeEdit"/>
</div>
<ms-api-binary-variable
:is-read-only="isReadOnly"
:parameters="body.binary"
:isShowEnable="isShowEnable"
type="body"
v-if="body.type == 'BINARY'"/>
<batch-add-parameter @batchSave="batchSave" ref="batchAddParameter"/>
</div>
</template>
<script>
import MsApiKeyValue from "metersphere-frontend/src/components/environment/commons/ApiKeyValue";
import {BODY_TYPE, KeyValue} from "metersphere-frontend/src//model/ApiTestModel";
import MsCodeEdit from "metersphere-frontend/src/components/MsCodeEdit";
import MsDropdown from "metersphere-frontend/src/components/MsDropdown";
import MsApiVariable from "metersphere-frontend/src/components/environment/commons/ApiVariable";
import MsApiBinaryVariable from "./ApiBinaryVariable";
import MsApiFromUrlVariable from "./ApiFromUrlVariable";
import BatchAddParameter from "metersphere-frontend/src/components/environment/commons/BatchAddParameter";
export default {
name: "MsApiBody",
components: {
MsApiVariable,
MsDropdown,
MsCodeEdit,
MsApiKeyValue,
MsApiBinaryVariable,
MsApiFromUrlVariable,
BatchAddParameter
},
props: {
body: {
type: Object,
default() {
return {
json: true,
kV: false,
kvs: [],
oldKV: false,
type: "JSON",
valid: false,
xml: false
}
}
},
headers: Array,
isReadOnly: {
type: Boolean,
default: false
},
isShowEnable: {
type: Boolean,
default: true
},
scenarioDefinition: Array,
id: String,
},
data() {
return {
type: BODY_TYPE,
modes: ['text', 'json', 'xml', 'html'],
jsonSchema: "JSON",
codeEditActive: true,
hasOwnProperty: Object.prototype.hasOwnProperty,
propIsEnumerable: Object.prototype.propertyIsEnumerable,
};
},
watch: {
'body.typeChange'() {
this.reloadCodeEdit();
},
},
methods: {
isObj(x) {
let type = typeof x;
return x !== null && (type === 'object' || type === 'function');
},
toObject(val) {
if (val === null || val === undefined) {
return;
}
return Object(val);
},
assignKey(to, from, key) {
let val = from[key];
if (val === undefined || val === null) {
return;
}
if (!this.hasOwnProperty.call(to, key) || !this.isObj(val)) {
to[key] = val;
} else {
to[key] = this.assign(Object(to[key]), from[key]);
}
},
assign(to, from) {
if (to === from) {
return to;
}
from = Object(from);
for (let key in from) {
if (this.hasOwnProperty.call(from, key)) {
this.assignKey(to, from, key);
}
}
//
for (let key in to) {
if (!this.hasOwnProperty.call(from, key) && key !== 'description') {
delete to[key]
}
}
if (Object.getOwnPropertySymbols) {
let symbols = Object.getOwnPropertySymbols(from);
for (let i = 0; i < symbols.length; i++) {
if (this.propIsEnumerable.call(from, symbols[i])) {
this.assignKey(to, from, symbols[i]);
}
}
}
return to;
},
deepAssign(target) {
target = this.toObject(target);
for (let s = 1; s < arguments.length; s++) {
this.assign(target, arguments[s]);
}
return target;
},
reloadCodeEdit() {
this.codeEditActive = false;
this.$nextTick(() => {
this.codeEditActive = true;
});
},
modeChange(mode) {
switch (this.body.type) {
case "JSON":
this.setContentType("application/json");
break;
case "XML":
this.setContentType("text/xml");
break;
case "WWW_FORM":
this.setContentType("application/x-www-form-urlencoded");
break;
// todo from data
case "BINARY":
this.setContentType("application/octet-stream");
break;
default:
this.removeContentType();
break;
}
},
setContentType(value) {
let isType = false;
this.headers.forEach(item => {
if (item.name === "Content-Type" || item.name == "contentType") {
item.value = value;
isType = true;
}
})
if (this.body && this.body.kvs && value === "application/x-www-form-urlencoded") {
this.body.kvs.forEach(item => {
item.urlEncode = true;
});
}
if (!isType) {
this.headers.unshift(new KeyValue({name: "Content-Type", value: value}));
this.$emit('headersChange');
}
},
removeContentType() {
for (let index in this.headers) {
if (this.headers[index].name === "Content-Type") {
this.headers.splice(index, 1);
this.$emit('headersChange');
return;
}
}
},
batchAdd() {
this.$refs.batchAddParameter.open();
},
format(array, obj) {
if (array) {
let isAdd = true;
for (let i in array) {
let item = array[i];
if (item.name === obj.name) {
item.value = obj.value;
isAdd = false;
}
}
if (isAdd) {
this.body.kvs.splice(this.body.kvs.indexOf(kv => !kv.name), 0, obj);
}
}
},
batchSave(data) {
if (data) {
let params = data.split("\n");
let keyValues = [];
params.forEach(item => {
if (item) {
let line = [];
line[0] = item.substring(0, item.indexOf(":"));
line[1] = item.substring(item.indexOf(":") + 1, item.length);
let required = false;
keyValues.push(new KeyValue({
name: line[0],
required: required,
value: line[1],
description: line[2],
type: "text",
valid: false,
file: false,
encode: true,
enable: true,
contentType: "text/plain"
}));
}
})
keyValues.forEach(item => {
this.format(this.body.kvs, item);
})
}
},
editScenarioAdvance(data) {
this.$emit('editScenarioAdvance', data);
},
},
created() {
if (!this.body.type) {
this.body.type = BODY_TYPE.FORM_DATA;
}
if (this.body.kvs) {
this.body.kvs.forEach(param => {
if (!param.type) {
param.type = 'text';
}
});
}
}
}
</script>
<style scoped>
.textarea {
margin-top: 10px;
}
.ms-body {
padding: 15px 0;
height: 400px;
}
.el-dropdown {
margin-left: 20px;
line-height: 30px;
}
.ace_editor {
border-radius: 5px;
}
.el-radio-group {
margin: 10px 10px;
margin-top: 15px;
}
.ms-el-link {
float: right;
margin-right: 45px;
}
</style>

View File

@ -0,0 +1,222 @@
<template>
<div style="min-width: 1200px;margin-bottom: 20px">
<span class="kv-description" v-if="description">
{{ description }}
</span>
<div class="kv-row" v-for="(item, index) in parameters" :key="index">
<el-row type="flex" :gutter="20" justify="space-between" align="middle">
<el-col>
<el-input v-if="!suggestions" :disabled="isReadOnly" v-model="item.name" size="small" maxlength="200"
@change="change" :placeholder="keyText" show-word-limit>
</el-input>
<el-autocomplete :disabled="isReadOnly" v-if="suggestions" v-model="item.name" size="small"
:fetch-suggestions="querySearch" @change="change" :placeholder="keyText" show-word-limit/>
</el-col>
<el-col class="kv-select">
<el-select v-model="item.required" size="small">
<el-option v-for="req in requireds" :key="req.id" :label="req.name" :value="req.id"/>
</el-select>
</el-col>
<el-col v-if="item.type !== 'file'">
<el-autocomplete
:disabled="isReadOnly"
size="small"
class="input-with-autocomplete"
v-model="item.value"
:fetch-suggestions="funcSearch"
:placeholder="valueText"
value-key="name"
highlight-first-item
@select="change">
<i slot="suffix" class="el-input__icon el-icon-edit pointer" @click="advanced(item)"></i>
</el-autocomplete>
</el-col>
<el-col>
<el-input v-model="item.description" size="small" maxlength="200"
:placeholder="$t('commons.description')" show-word-limit>
</el-input>
<el-autocomplete :disabled="isReadOnly" v-if="suggestions" v-model="item.name" size="small"
:fetch-suggestions="querySearch" @change="change" :placeholder="keyText" show-word-limit/>
</el-col>
<el-col class="kv-delete">
<el-button size="mini" class="el-icon-delete-solid" circle @click="remove(index)"
:disabled="isDisable(index) || isReadOnly"/>
</el-col>
</el-row>
</div>
<ms-api-variable-advance ref="variableAdvance" :environment="environment" :scenario="scenario"
:parameters="parameters"
:current-item="currentItem"/>
</div>
</template>
<script>
import {KeyValue, Scenario} from "metersphere-frontend/src/model/ApiTestModel";
import {JMETER_FUNC, MOCKJS_FUNC} from "metersphere-frontend/src/utils/constants";
import MsApiVariableAdvance from "metersphere-frontend/src/components/environment/commons/ApiVariableAdvance";
import MsApiBodyFileUpload from "metersphere-frontend/src/components/environment/commons/ApiBodyFileUpload";
import {REQUIRED} from "metersphere-frontend/src/model/JsonData";
export default {
name: "MsApiVariable",
components: {MsApiBodyFileUpload, MsApiVariableAdvance},
props: {
keyPlaceholder: String,
valuePlaceholder: String,
description: String,
parameters: Array,
rest: Array,
environment: Object,
scenario: Scenario,
type: {
type: String,
default: ''
},
isReadOnly: {
type: Boolean,
default: false
},
suggestions: Array
},
data() {
return {
currentItem: null,
requireds: REQUIRED
}
},
computed: {
keyText() {
return this.keyPlaceholder || this.$t("api_test.key");
},
valueText() {
return this.valuePlaceholder || this.$t("api_test.value");
}
},
methods: {
remove: function (index) {
//
this.parameters.splice(index, 1);
this.$emit('change', this.parameters);
},
change: function () {
let isNeedCreate = true;
let removeIndex = -1;
this.parameters.forEach((item, index) => {
if (!item.name && !item.value) {
//
if (index !== this.parameters.length - 1) {
removeIndex = index;
}
//
isNeedCreate = false;
}
});
if (isNeedCreate) {
this.parameters.push(new KeyValue({
type: 'text',
enable: true,
uuid: this.uuid(),
contentType: 'application/x-www-from-urlencoded'
}));
}
this.$emit('change', this.parameters);
// TODO key
},
isDisable: function (index) {
return this.parameters.length - 1 === index;
},
querySearch(queryString, cb) {
let suggestions = this.suggestions;
let results = queryString ? suggestions.filter(this.createFilter(queryString)) : suggestions;
cb(results);
},
createFilter(queryString) {
return (restaurant) => {
return (restaurant.value.toLowerCase().indexOf(queryString.toLowerCase()) === 0);
};
},
funcSearch(queryString, cb) {
let funcs = MOCKJS_FUNC.concat(JMETER_FUNC);
let results = queryString ? funcs.filter(this.funcFilter(queryString)) : funcs;
// callback
cb(results);
},
funcFilter(queryString) {
return (func) => {
return (func.name.toLowerCase().indexOf(queryString.toLowerCase()) > -1);
};
},
uuid: function () {
return (((1 + Math.random()) * 0x100000) | 0).toString(16).substring(1);
},
advanced(item) {
this.$refs.variableAdvance.open();
this.currentItem = item;
},
typeChange(item) {
item.contentType = 'application/x-www-from-urlencoded';
}
},
created() {
if (this.parameters.length === 0 || this.parameters[this.parameters.length - 1].name) {
this.parameters.push(new KeyValue({type: 'text', enable: true, required: true, uuid: this.uuid(), contentType: 'application/x-www-from-urlencoded'}));
}
}
}
</script>
<style scoped>
.kv-description {
font-size: 13px;
}
.kv-row {
margin-top: 10px;
}
.kv-delete {
width: 60px;
}
.kv-select {
width: 50%;
}
.el-autocomplete {
width: 100%;
}
.kv-checkbox {
width: 20px;
margin-right: 10px;
}
.advanced-item-value :deep(.el-dialog__body) {
padding: 15px 25px;
}
.el-row {
margin-bottom: 5px;
}
.kv-type {
width: 70px;
}
.pointer {
cursor: pointer;
color: #1E90FF;
}
</style>

View File

@ -0,0 +1,767 @@
<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="showCancel"
: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" style="min-width: 1000px">
<!-- all step-->
<el-tab-pane label="All" 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">
Error
</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">
FakeError
</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">
Pending
</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">Console</span>
</template>
<ms-code-edit
:mode="'text'"
:read-only="true"
:data.sync="content.console"
height="calc(100vh - 500px)"/>
</el-tab-pane>
</el-tabs>
<!--export report-->
<ms-api-report-export
v-if="reportExportVisible"
id="apiTestReport"
:project-env-map="projectEnvMap"
:title="report.name"
:content="content"
:report="report"
:total-time="totalTime"/>
</main>
</section>
</el-card>
</ms-main-container>
</ms-container>
</template>
<script>
import MsMetricChart from "../ui/MetricChart";
import MsScenarioResults from "../ui/ScenarioResults";
import MsContainer from "metersphere-frontend/src/components/MsContainer";
import MsMainContainer from "metersphere-frontend/src/components/MsMainContainer";
import MsApiReportViewHeader from "./ApiReportViewHeader";
import {RequestFactory} from "metersphere-frontend/src/model/ApiTestModel";
import {getCurrentProjectID} from "metersphere-frontend/src/utils/token";
import {getUUID, windowPrint} from "metersphere-frontend/src/utils";
import {hasLicense} from "metersphere-frontend/src/utils/permission";
import {STEP} from "metersphere-frontend/src/model/Setting";
import MsCodeEdit from "metersphere-frontend/src/components/MsCodeEdit";
export default {
name: "MsApiReport",
components: {
MsApiReportViewHeader,
MsMainContainer,
MsCodeEdit,
MsContainer,
MsScenarioResults,
MsMetricChart
},
data() {
return {
activeName: "total",
content: {},
report: {},
loading: true,
fails: [],
failsTreeNodes: [],
totalTime: 0,
isRequestResult: false,
request: {},
isActive: false,
scenarioName: null,
reportExportVisible: false,
requestType: undefined,
fullTreeNodes: [],
showRerunButton: false,
stepFilter: new STEP,
exportReportIsOk: false,
tempResult: [],
projectEnvMap: {},
showCancel: false
}
},
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: false
}
},
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("FAKE_ERROR");
} else if (this.activeName === "unExecute") {
this.$refs.unExecuteTree.filter("PENDING");
}
},
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;
if (this.$route && this.$route.path.startsWith('/api/automation/report')
&& this.$route.query && this.$route.query.list) {
this.$nextTick(() => {
this.showCancel = true;
});
}
},
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 === 'PENDING')) {
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 {
getScenarioReportDetail(this.reportId).then((res) => {
let data = res.data;
if (data && data.content) {
let report = JSON.parse(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();
}
}
},
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;
}
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.requestType = undefined;
if (requestResult.request.body.indexOf('[Callable Statement]') > -1) {
this.requestType = RequestFactory.TYPES.SQL;
}
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;
reportReName({
id: this.report.id,
name: this.report.name,
reportType: this.report.reportType
}).then(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.showCancel = this.showCancelButton;
this.getReport();
if (this.$EventBus) {
this.$EventBus.$on('projectChange', this.handleProjectChange);
}
},
destroyed() {
if (this.$EventBus) {
this.$EventBus.$off('projectChange', this.handleProjectChange);
}
},
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

@ -0,0 +1,452 @@
<template>
<ms-container>
<ms-main-container>
<el-card class="table-card" v-loading="result">
<template v-slot:header>
<ms-table-header :condition.sync="condition" v-if="loadIsOver" @search="search" :show-create="false">
<template v-slot:button>
<el-button-group>
<el-tooltip class="item" effect="dark" content="left" :disabled="true" placement="left">
<el-button plain :class="{active: leftActive}" @click="changeTab('left')">
{{ $t('commons.scenario') }}
</el-button>
</el-tooltip>
<el-tooltip class="item" effect="dark" content="right" :disabled="true" placement="right">
<el-button plain :class="{active: rightActive}" @click="changeTab('right')">
{{ $t('api_test.definition.request.case') }}
</el-button>
</el-tooltip>
</el-button-group>
</template>
</ms-table-header>
</template>
<el-table ref="reportListTable" border :data="tableData" class="adjust-table table-content" @sort-change="sort"
@select-all="handleSelectAll"
@select="handleSelect"
:height="screenHeight"
@filter-change="filter" @row-click="handleView" v-if="loadIsOver">
<el-table-column
type="selection"/>
<el-table-column width="40" :resizable="false" align="center">
<el-dropdown slot="header" style="width: 14px">
<span class="el-dropdown-link" style="width: 14px">
<i class="el-icon-arrow-down el-icon--right" style="margin-left: 0px"></i>
</span>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item @click.native.stop="isSelectDataAll(true)">
{{ $t('api_test.batch_menus.select_all_data', [total]) }}
</el-dropdown-item>
<el-dropdown-item @click.native.stop="isSelectDataAll(false)">
{{ $t('api_test.batch_menus.select_show_data', [tableData.length]) }}
</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
<template v-slot:default="scope">
<show-more-btn :is-show="scope.row.showMore" :buttons="buttons" :size="selectDataCounts"/>
</template>
</el-table-column>
<ms-table-column
prop="name"
sortable
:label="$t('commons.name')"
:show-overflow-tooltip="false"
:editable="true"
:edit-content="$t('report.rename_report')"
@editColumn="openReNameDialog"
min-width="200px">
</ms-table-column>
<el-table-column prop="reportType" :label="$t('load_test.report_type')" width="150"
column-key="reportType"
:filters="reportTypeFilters">
<template v-slot:default="scope">
<div v-if="scope.row.reportType === 'SCENARIO_INTEGRATED'">
<el-tag size="mini" type="primary">
{{ $t('api_test.scenario.integrated') }}
</el-tag>
{{ $t('commons.scenario') }}
</div>
<div v-else-if="scope.row.reportType === 'API_INDEPENDENT'">
<el-tag size="mini" type="primary">
{{ $t('api_test.scenario.independent') }}
</el-tag>
case
</div>
<div v-else-if="scope.row.reportType === 'API_INTEGRATED'">
<el-tag size="mini" type="primary">
{{ $t('api_test.scenario.integrated') }}
</el-tag>
case
</div>
<div v-else>
<el-tag size="mini" type="primary">
{{ $t('api_test.scenario.independent') }}
</el-tag>
{{ $t('commons.scenario') }}
</div>
</template>
</el-table-column>
<ms-table-column prop="userName" :label="$t('api_test.creator')" width="150" show-overflow-tooltip
:filters="userFilters"/>
<el-table-column prop="createTime" min-width="120" :label="$t('commons.create_time')" sortable>
<template v-slot:default="scope">
<span>{{ scope.row.createTime | datetimeFormat }}</span>
</template>
</el-table-column>
<el-table-column prop="endTime" min-width="120" :label="$t('report.test_end_time')" sortable>
<template v-slot:default="scope">
<span v-if="scope.row.endTime && scope.row.endTime > 0">
{{ scope.row.endTime | datetimeFormat }}
</span>
<span v-else>
{{ scope.row.updateTime | datetimeFormat }}
</span>
</template>
</el-table-column>
<el-table-column prop="triggerMode" width="150" :label="$t('commons.trigger_mode.name')"
column-key="triggerMode" :filters="triggerFilters">
<template v-slot:default="scope">
<report-trigger-mode-item :trigger-mode="scope.row.triggerMode"/>
</template>
</el-table-column>
<el-table-column
:label="$t('commons.status')"
:filters="statusFilters"
column-key="status"
prop="status">
<template v-slot:default="{row}">
<ms-api-report-status :status="row.status"/>
</template>
</el-table-column>
<el-table-column width="150" :label="$t('commons.operating')">
<template v-slot:default="scope">
<div>
<ms-table-operator-button
:tip="$t('api_report.detail')" icon="el-icon-s-data"
@exec="handleView(scope.row)" type="primary"/>
<ms-table-operator-button
:tip="$t('api_report.delete')"
v-permission="['PROJECT_API_REPORT:READ+DELETE']"
icon="el-icon-delete" @exec="handleDelete(scope.row)" type="danger"/>
</div>
</template>
</el-table-column>
</el-table>
<ms-table-pagination :change="search" :current-page.sync="currentPage" :page-size.sync="pageSize"
:total="total"/>
</el-card>
<ms-rename-report-dialog ref="renameDialog" @submit="rename($event)"></ms-rename-report-dialog>
<el-dialog :close-on-click-modal="false" :title="$t('test_track.plan_view.test_result')" width="60%"
:visible.sync="resVisible" class="api-import" destroy-on-close @close="resVisible=false">
<ms-request-result-tail :report-id="reportId" :response="response" ref="debugResult"/>
</el-dialog>
</ms-main-container>
</ms-container>
</template>
<script>
import {getCurrentProjectID} from "metersphere-frontend/src/utils/token";
import {REPORT_CASE_CONFIGS, REPORT_CONFIGS} from "metersphere-frontend/src/components/search/search-components";
import {_filter, _sort} from "metersphere-frontend/src/utils/tableUtils";
import MsRenameReportDialog from "metersphere-frontend/src/components/report/MsRenameReportDialog";
import MsTableColumn from "metersphere-frontend/src/components/table/MsTableColumn";
import MsRequestResultTail from "@/business/definition/components/response/RequestResultTail";
import MsTabButton from "@/business/commons/MsTabs";
import {getMaintainer} from "@/api/project";
import {delBatchReport, delReport, getReportPage, reportReName} from "@/api/scenario-report";
import {getApiReportPage} from "@/api/definition-report";
import {REPORT_STATUS} from "@/business/commons/js/commons";
export default {
components: {
ReportTriggerModeItem: () => import("metersphere-frontend/src/components/tableItem/ReportTriggerModeItem"),
MsTableOperatorButton: () => import("metersphere-frontend/src/components/MsTableOperatorButton"),
MsApiReportStatus: () => import("./ApiReportStatus"),
MsMainContainer: () => import("metersphere-frontend/src/components/MsMainContainer"),
MsContainer: () => import("metersphere-frontend/src/components/MsContainer"),
MsTableHeader: () => import("metersphere-frontend/src/components/MsTableHeader"),
MsTablePagination: () => import("metersphere-frontend/src/components/pagination/TablePagination"),
ShowMoreBtn: () => import("@/business/commons/ShowMoreBtn"),
MsRenameReportDialog,
MsTableColumn,
MsTabButton,
MsRequestResultTail,
},
props: {
reportType: String
},
data() {
return {
result: false,
resVisible: false,
response: {},
reportId: "",
debugVisible: false,
condition: {
components: REPORT_CONFIGS
},
tableData: [],
multipleSelection: [],
currentPage: 1,
pageSize: 10,
loadIsOver: true,
total: 0,
loading: false,
currentProjectId: "",
statusFilters: REPORT_STATUS,
reportTypeFilters: [],
reportScenarioFilters: [
{text: this.$t('api_test.scenario.independent') + this.$t('commons.scenario'), value: 'SCENARIO_INDEPENDENT'},
{text: this.$t('api_test.scenario.integrated') + this.$t('commons.scenario'), value: 'SCENARIO_INTEGRATED'}
],
reportCaseFilters: [
{text: this.$t('api_test.scenario.independent') + 'case', value: 'API_INDEPENDENT'},
{text: this.$t('api_test.scenario.integrated') + 'case', value: 'API_INTEGRATED'},
],
triggerFilters: [
{text: this.$t('commons.trigger_mode.manual'), value: 'MANUAL'},
{text: this.$t('commons.trigger_mode.schedule'), value: 'SCHEDULE'},
{text: this.$t('commons.trigger_mode.api'), value: 'API'},
{text: this.$t('api_test.automation.batch_execute'), value: 'BATCH'},
],
buttons: [
{
name: this.$t('api_report.batch_delete'),
handleClick: this.handleBatchDelete,
permissions: ['PROJECT_API_REPORT:READ+DELETE']
}
],
selectRows: new Set(),
selectAll: false,
unSelection: [],
selectDataCounts: 0,
screenHeight: 'calc(100vh - 160px)',
trashActiveDom: 'left',
userFilters: [],
}
},
watch: {
'$route'(to, from) {
if (to.path.startsWith('/api/automation/report')) {
this.init();
}
},
trashActiveDom() {
this.condition.filters = {report_type: []};
this.search();
}
},
computed: {
leftActive() {
return this.trashActiveDom === 'left';
},
rightActive() {
return this.trashActiveDom === 'right';
},
},
methods: {
getMaintainerOptions() {
getMaintainer().then(response => {
this.userFilters = response.data.map(u => {
return {text: u.name, value: u.id};
});
});
},
search() {
if (this.testId !== 'all') {
this.condition.testId = this.testId;
}
this.condition.projectId = getCurrentProjectID();
this.selectAll = false;
this.unSelection = [];
this.selectDataCounts = 0;
this.condition.reportType = this.reportType;
if (this.condition.orders && this.condition.orders.length > 0) {
let order = this.condition.orders[this.condition.orders.length - 1];
this.condition.orders = [];
this.condition.orders.push(order);
}
if (this.trashActiveDom === 'left') {
this.reportTypeFilters = this.reportScenarioFilters;
this.result = getReportPage(this.currentPage, this.pageSize, this.condition).then(res => {
this.setData(res);
})
} else {
this.reportTypeFilters = this.reportCaseFilters;
this.result = getApiReportPage(this.currentPage, this.pageSize, this.condition).then(res => {
this.setData(res);
})
}
},
setData(response) {
let data = response.data;
this.total = data.itemCount;
this.tableData = data.listObject;
this.selectRows.clear();
this.unSelection = data.listObject.map(s => s.id);
},
handleView(report) {
this.reportId = report.id;
if (report.status === 'RUNNING' || report.status === 'RERUNNING') {
this.$warning(this.$t('commons.run_warning'))
return;
}
if (report.reportType.indexOf('SCENARIO') !== -1 || report.reportType === 'API_INTEGRATED') {
this.currentProjectId = report.projectId;
this.$router.push({
path: 'report/view/' + report.id,
query: {list: true}
});
} else {
this.resVisible = true;
}
},
handleDelete(report) {
this.$alert(this.$t('api_report.delete_confirm') + report.name + "", '', {
confirmButtonText: this.$t('commons.confirm'),
callback: (action) => {
if (action === 'confirm') {
delReport(report.id).then(() => {
this.$success(this.$t('commons.delete_success'));
this.search();
});
}
}
});
},
init() {
this.testId = this.$route.params.testId;
this.search();
},
sort(column) {
_sort(column, this.condition);
this.init();
},
filter(filters) {
_filter(filters, this.condition);
this.init();
},
handleSelect(selection, row) {
if (this.selectRows.has(row)) {
this.$set(row, "showMore", false);
this.selectRows.delete(row);
} else {
this.$set(row, "showMore", true);
this.selectRows.add(row);
}
this.selectRowsCount(this.selectRows)
},
handleSelectAll(selection) {
if (selection.length > 0) {
this.tableData.forEach(item => {
this.$set(item, "showMore", true);
this.selectRows.add(item);
});
} else {
this.selectRows.clear();
this.tableData.forEach(row => {
this.$set(row, "showMore", false);
})
}
this.selectRowsCount(this.selectRows)
},
handleBatchDelete() {
this.$alert(this.$t('api_report.delete_batch_confirm') + "", '', {
confirmButtonText: this.$t('commons.confirm'),
callback: (action) => {
if (action === 'confirm') {
let ids = Array.from(this.selectRows).map(row => row.id);
let sendParam = {};
sendParam.ids = ids;
sendParam.selectAllDate = this.isSelectAllDate;
sendParam.unSelectIds = this.unSelection;
sendParam = Object.assign(sendParam, this.condition);
sendParam.caseType = this.trashActiveDom === 'right' ? 'API' : 'SCENARIO';
delBatchReport(sendParam).then(() => {
this.selectRows.clear();
this.$success(this.$t('commons.delete_success'));
this.search();
});
}
}
});
},
selectRowsCount(selection) {
let selectedIDs = this.getIds(selection);
let allIDs = this.tableData.map(s => s.id);
this.unSelection = allIDs.filter(function (val) {
return selectedIDs.indexOf(val) === -1
});
if (this.isSelectAllDate) {
this.selectDataCounts = this.total - this.unSelection.length;
} else {
this.selectDataCounts = selection.size;
}
},
isSelectDataAll(dataType) {
this.isSelectAllDate = dataType;
this.selectRowsCount(this.selectRows)
//
if (this.selectRows.size != this.tableData.length) {
this.$refs.reportListTable.toggleAllSelection(true);
}
},
getIds(rowSets) {
let rowArray = Array.from(rowSets)
let ids = rowArray.map(s => s.id);
return ids;
},
openReNameDialog($event) {
this.$refs.renameDialog.open($event);
},
rename(data) {
reportReName(data).then(() => {
this.$success(this.$t("organization.integration.successful_operation"));
this.init();
this.$refs.renameDialog.close();
});
},
changeTab(tabType) {
this.trashActiveDom = tabType;
if (tabType === 'right') {
this.condition.components = REPORT_CASE_CONFIGS;
} else {
this.condition.components = REPORT_CONFIGS;
}
this.loadIsOver = false;
this.$nextTick(() => {
this.loadIsOver = true;
});
},
},
created() {
this.init();
this.getMaintainerOptions();
}
}
</script>
<style scoped>
.table-content {
width: 100%;
}
.active {
border: solid 1px #6d317c !important;
background-color: var(--primary_color) !important;
color: #FFFFFF !important;
}
.item {
height: 32px;
padding: 5px 8px;
border: solid 1px var(--primary_color);
}
</style>

View File

@ -0,0 +1,31 @@
<template>
<div class="item">
<div class="item-title">
{{title}}
</div>
<div>
<slot></slot>
</div>
</div>
</template>
<script>
export default {
name: "ApiReportRequestHeaderItem",
props: {title: String}
}
</script>
<style scoped>
.item {
width: 120px;
height: 50px;
display: inline-block;
}
.item-title {
margin-bottom: 20px;
}
</style>

View File

@ -0,0 +1,51 @@
<template>
<div>
<el-tag size="mini" type="primary" effect="plain" v-if="getStatus(status) === 'running'">
{{ showStatus(status) }}
</el-tag>
<el-tag size="mini" type="success" v-else-if="getStatus(status) === 'success'">
{{ showStatus(status) }}
</el-tag>
<el-tag size="mini" type="danger" v-else-if="getStatus(status) === 'error'">
{{ showStatus(status) }}
</el-tag>
<el-tag size="mini" type="danger" style="background-color: #F6972A; color: #FFFFFF"
v-else-if="getStatus(status) === 'fake_error'">
FakeError
</el-tag>
<span v-else-if="status === '-'" size="mini" type="info">
-
</span>
<el-tag v-else size="mini" type="info">
{{ showStatus(status) }}
</el-tag>
</div>
</template>
<script>
export default {
name: "MsApiReportStatus",
props: {
status: String
},
methods: {
getStatus(status) {
if (status) {
return status.toLowerCase();
}
return "PENDING";
},
showStatus(status) {
if (!status) {
status = 'PENDING';
}
return status.toLowerCase()[0].toUpperCase() + status.toLowerCase().substr(1);
}
}
}
</script>
<style scoped>
</style>

View File

@ -0,0 +1,140 @@
<template>
<header class="report-header">
<el-row>
<el-col>
<span v-if="!debug">
<span>
<el-link v-if="isSingleScenario"
type="primary"
class="report-name"
@click="redirect">
{{ report.name }}
</el-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 | datetimeFormat }}</span>
<span style="margin-left: 10px">{{ $t('report.test_end_time') }}</span>
<span class="time"> {{ report.endTime | datetimeFormat }}</span>
</span>
</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 {getCurrentProjectID, getCurrentWorkspaceId} from "metersphere-frontend/src/utils/token";
import MsTag from "metersphere-frontend/src/components/MsTag";
import {getUUID} from "metersphere-frontend/src/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;
}
},
},
data() {
return {
isReadOnly: false,
nameIsEdit: false,
shareUrl: "",
application: {}
}
},
methods: {
handleSaveKeyUp($event) {
$event.target.blur();
},
redirect() {
let uuid = getUUID().substring(1, 5);
let projectId = getCurrentProjectID();
let workspaceId = getCurrentWorkspaceId();
let path = `/api/automation/?redirectID=${uuid}&dataType=scenario&projectId=${projectId}&workspaceId=${workspaceId}&resourceId=${this.scenarioId}`;
let data = this.$router.resolve({
path: path
});
window.open(data.href, '_blank');
},
returnView() {
this.$router.push('/api/automation/report');
},
}
}
</script>
<style scoped>
.export-button {
float: right;
margin-right: 10px;
}
.rerun-button {
float: right;
margin-right: 10px;
background-color: #F2F9EF;
color: #87C45D;
}
.report-name {
border-bottom: 1px solid var(--primary_color);
}
.report-header {
min-width: 1000px;
}
</style>

View File

@ -0,0 +1,116 @@
<template>
<div class="metric-container">
<el-row type="flex">
<el-col>
<div style="font-size: 14px;color: #AAAAAA;float: left">{{ $t('api_report.response_code') }} :</div>
<el-tooltip
v-if="responseResult.responseCode"
:content="responseResult.responseCode"
placement="top">
<div
v-if="response.attachInfoMap && response.attachInfoMap.FAKE_ERROR
&& response.attachInfoMap.status === 'FAKE_ERROR'" class="node-title" :class="'ms-req-error-report-result'">
{{ responseResult && responseResult.responseCode ? responseResult.responseCode : '0' }}
</div>
<div v-else class="node-title" :class="response && response.success ?'ms-req-success':'ms-req-error'">
{{ responseResult && responseResult.responseCode ? responseResult.responseCode : '0' }}
</div>
</el-tooltip>
<div v-else class="node-title" :class="response && response.success ?'ms-req-success':'ms-req-error'">
{{ responseResult && responseResult.responseCode ? responseResult.responseCode : '0' }}
</div>
<div v-if="response && response.attachInfoMap && response.attachInfoMap.FAKE_ERROR">
<div class="node-title ms-req-error-report-result"
v-if="response.attachInfoMap.status === 'FAKE_ERROR'" style="margin-left: 0px;padding-left: 0px">
{{ response.attachInfoMap.FAKE_ERROR }}
</div>
<div class="node-title ms-req-success" v-else-if="response.success"
style="margin-left: 0px;padding-left: 0px">
{{ response.attachInfoMap.FAKE_ERROR }}
</div>
<div class="node-title ms-req-error" v-else style="margin-left: 0px;padding-left: 0px">
{{ response.attachInfoMap.FAKE_ERROR }}
</div>
</div>
</el-col>
<el-col>
<div style="font-size: 14px;color: #AAAAAA;float: left">{{ $t('api_report.response_time') }} :</div>
<div style="font-size: 14px;color:#61C550;margin-top:2px;margin-left:10px;float: left">
{{ responseResult && responseResult.responseTime ? responseResult.responseTime : 0 }} ms
</div>
</el-col>
<el-col>
<div style="font-size: 14px;color: #AAAAAA;float: left">{{ $t('api_report.response_size') }} :</div>
<div style="font-size: 14px;color:#61C550; margin-top:2px;margin-left:10px;float: left">
{{ responseResult && responseResult.responseSize ? responseResult.responseSize : 0 }} bytes
</div>
</el-col>
</el-row>
<el-row type="flex" v-if="response && response.envName">
<div style="font-size: 14px;color: #AAAAAA;float: left">
<span> {{ $t('commons.environment') + ':' }} </span>
</div>
<div style="font-size: 14px;color:#61C550; margin-left:10px;float: left">
{{ response.envName }}
</div>
</el-row>
</div>
</template>
<script>
export default {
name: "MsRequestMetric",
props: {
response: {
type: Object,
default() {
return {}
}
}
},
computed: {
responseResult() {
return this.response && this.response.responseResult ? this.response.responseResult : {};
},
error() {
return this.response && this.response.responseCode && this.response.responseCode >= 400;
}
}
}
</script>
<style scoped>
.metric-container {
padding-bottom: 10px;
}
.node-title {
/*width: 150px;*/
text-overflow: ellipsis;
white-space: nowrap;
flex: 1 1 auto;
padding: 0px 5px;
overflow: hidden;
font-size: 14px;
color: #61C550;
margin-top: 2px;
margin-left: 10px;
margin-right: 10px;
float: left
}
.ms-req-error {
color: #F56C6C;
}
.ms-req-error-report-result {
color: #F6972A;
}
.ms-req-success {
color: #67C23A;
}
</style>

View File

@ -0,0 +1,353 @@
<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">
<span 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 class="report-label-req" @click="isLink" v-if="redirect && resourceId">
{{ request.name }}
</span>
<span v-else>{{ getName(request.name) }}</span>
</span>
</el-tooltip>
</el-col>
<!-- 误报 / 异常状态显示 -->
<el-col :span="3">
<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 :style="{color: statusColor(totalStatus ? totalStatus : request.status)}">
{{ baseErrorCode }}
</div>
</el-tooltip>
</el-col>
<!-- 请求返回状态 -->
<el-col :span="6">
<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: statusColor(totalStatus ? totalStatus : request.status)}">
{{ request.responseResult.responseCode }}
</div>
</el-tooltip>
</el-col>
<!-- 请求响应时间 -->
<el-col :span="3">
<div :style="{color: statusColor(totalStatus ? totalStatus : request.status)}">
{{ request.responseResult.responseTime }}
</div>
</el-col>
<el-col :span="2">
<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>
<ms-api-report-status :status="totalStatus || request.status" v-else/>
</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 MsRequestMetric from "./RequestMetric";
import MsAssertionResults from "../ui/AssertionResults";
import MsRequestText from "./RequestText";
import MsResponseText from "./ResponseText";
import MsRequestResultTail from "./RequestResultTail";
import MsApiReportStatus from "./ApiReportStatus";
export default {
name: "MsRequestResult",
components: {
MsApiReportStatus,
MsResponseText,
MsRequestText,
MsAssertionResults,
MsRequestMetric,
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.FAKE_ERROR) {
if (this.request.attachInfoMap.FAKE_ERROR !== "") {
this.baseErrorCode = this.request.attachInfoMap.FAKE_ERROR;
}
}
},
}
},
methods: {
statusColor(status) {
return this.getReportStatusColor(status);
},
getReportStatusColor(status) {
if (status) {
status = status.toUpperCase();
}
if (status === 'SUCCESS') {
return '#5daf34';
} else if (status === 'FAKE_ERROR') {
return '#F6972A';
} else if (status === 'ERROR') {
return '#FE6F71';
} else {
return '';
}
},
isLink() {
let uri = "/#/api/definition?caseId=" + this.resourceId;
this.clickResource(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() {
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,105 @@
<template>
<div class="request-result" v-loading="loading">
<ms-request-metric v-if="showMetric" :response="response"/>
<ms-response-result :currentProtocol="currentProtocol" :response="response"
:isTestPlan="isTestPlan"/>
</div>
</template>
<script>
import MsResponseResult from "./ResponseResult";
import MsRequestMetric from "./RequestMetric";
export default {
name: "MsRequestResultTail",
components: {MsRequestMetric, MsResponseResult},
props: {
response: Object,
currentProtocol: String,
reportId: String,
showMetric: {
type: Boolean,
default() {
return true;
}
},
isTestPlan: {
type: Boolean,
default() {
return false;
}
}
},
watch: {
reportId: {
immediate: true,
},
},
data() {
return {
loading: false,
report: {},
}
},
}
</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,73 @@
<template>
<div class="text-container">
<div @click="active" class="collapse">
<i class="icon el-icon-arrow-right" :class="{'is-active': isActive}"/>
{{$t('api_report.request')}}
</div>
<el-collapse-transition>
<el-tabs v-model="activeName" v-show="isActive">
<el-tab-pane label="Body" name="body" class="pane">
<pre>{{request.body}}</pre>
</el-tab-pane>
<el-tab-pane label="Headers" name="headers" class="pane">
<pre>{{request.headers}}</pre>
</el-tab-pane>
<el-tab-pane label="Cookies" name="cookies" class="pane">
<pre>{{request.cookies}}</pre>
</el-tab-pane>
</el-tabs>
</el-collapse-transition>
</div>
</template>
<script>
export default {
name: "MsRequestText",
props: {
request: Object
},
data() {
return {
isActive: true,
activeName: "body",
}
},
methods: {
active() {
this.isActive = !this.isActive;
}
},
}
</script>
<style scoped>
.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: #F9F9F9;
padding: 1px 0;
height: 250px;
overflow-y: auto;
}
pre {
margin: 0;
}
</style>

View File

@ -0,0 +1,197 @@
<template>
<div class="text-container" v-if="responseResult">
<el-tabs v-model="activeName" v-show="isActive">
<el-tab-pane :label="$t('api_test.definition.request.response_body')" name="body" class="pane">
<ms-sql-result-table v-if="isSqlType && activeName === 'body'" :body="responseResult.body"/>
<ms-code-edit v-if="!isSqlType && isMsCodeEditShow && activeName === 'body'" :mode="mode" :read-only="true"
:modes="modes" :data.sync="responseResult.body" ref="codeEdit"/>
</el-tab-pane>
<el-tab-pane :label="$t('api_test.definition.request.response_header')" name="headers" class="pane">
<ms-code-edit :mode="'text'" :read-only="true" :data.sync="responseResult.headers"
v-if="activeName === 'headers'"/>
</el-tab-pane>
<el-tab-pane v-if="isTestPlan" :label="$t('api_test.definition.request.console')" name="console" class="pane">
<ms-code-edit :mode="'text'" :read-only="true" :data.sync="responseResult.console"
v-if="activeName === 'console'" height="calc(100vh - 300px)"/>
</el-tab-pane>
<el-tab-pane v-if="!isTestPlan" :label="$t('api_test.definition.request.console')" name="console" class="pane">
<ms-code-edit :mode="'text'" :read-only="true" :data.sync="responseResult.console"
v-if="activeName === 'console'"/>
</el-tab-pane>
<el-tab-pane :label="$t('api_report.assertions')" name="assertions" class="pane assertions">
<ms-assertion-results :assertions="responseResult.assertions" v-if="activeName === 'assertions'"/>
</el-tab-pane>
<el-tab-pane :label="$t('api_test.request.extract.label')" name="label" class="pane">
<ms-code-edit :mode="'text'" :read-only="true" :data.sync="responseResult.vars" v-if="activeName === 'label'"/>
</el-tab-pane>
<el-tab-pane :label="$t('api_report.request_body')" name="request_body" class="pane">
<ms-code-edit :mode="'text'" :read-only="true" :data.sync="reqMessages" v-if="activeName === 'request_body'"/>
</el-tab-pane>
<el-tab-pane v-if="activeName == 'body'" :disabled="true" name="mode" class="pane cookie">
<template v-slot:label>
<ms-dropdown v-if="currentProtocol==='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>
</div>
</template>
<script>
import MsAssertionResults from "../ui/AssertionResults";
import MsCodeEdit from "metersphere-frontend/src/components/MsCodeEdit";
import MsDropdown from "metersphere-frontend/src/components/MsDropdown";
import {BODY_FORMAT} from "metersphere-frontend/src/model/ApiTestModel";
import MsSqlResultTable from "./SqlResultTable";
export default {
name: "MsResponseResult",
components: {
MsDropdown,
MsCodeEdit,
MsAssertionResults,
MsSqlResultTable
},
props: {
response: Object,
currentProtocol: String,
isTestPlan: {
type: Boolean,
default() {
return false;
}
}
},
data() {
return {
isActive: true,
activeName: "body",
modes: ['text', 'json', 'xml', 'html'],
sqlModes: ['text', 'table'],
mode: BODY_FORMAT.TEXT,
isMsCodeEditShow: true,
reqMessages: "",
}
},
watch: {
response() {
this.setBodyType();
this.setReqMessage();
}
},
methods: {
modeChange(mode) {
this.mode = mode;
},
sqlModeChange(mode) {
this.mode = mode;
},
setBodyType() {
if (this.response && this.response.responseResult && this.response.responseResult.headers
&& this.response.responseResult.headers.indexOf("Content-Type: application/json") > 0) {
this.mode = BODY_FORMAT.JSON;
this.$nextTick(() => {
if (this.$refs.modeDropdown) {
this.$refs.modeDropdown.handleCommand(BODY_FORMAT.JSON);
this.msCodeReload();
}
})
}
},
msCodeReload() {
this.isMsCodeEditShow = false;
this.$nextTick(() => {
this.isMsCodeEditShow = true;
});
},
setReqMessage() {
if (this.response) {
if (!this.response.url) {
this.response.url = "";
}
if (!this.response.headers) {
this.response.headers = "";
}
if (!this.response.cookies) {
this.response.cookies = "";
}
if (!this.response.body) {
this.response.body = "";
}
if (!this.response.responseResult) {
this.response.responseResult = {};
}
if (!this.response.responseResult.vars) {
this.response.responseResult.vars = "";
}
this.reqMessages = this.$t('api_test.request.address') + ":\n" + this.response.url + "\n" +
this.$t('api_test.scenario.headers') + ":\n" + this.response.headers + "\n" + "Cookies :\n" +
this.response.cookies + "\n" + "Body:" + "\n" + this.response.body;
}
},
},
mounted() {
this.setBodyType();
this.setReqMessage();
},
computed: {
isSqlType() {
return (this.currentProtocol === "SQL" && this.response.responseResult.responseCode === '200' && this.mode === 'table');
},
responseResult() {
return this.response && this.response.responseResult ? this.response.responseResult : {};
}
}
}
</script>
<style scoped>
.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;
}
</style>

View File

@ -0,0 +1,99 @@
<template>
<div class="text-container" style="border:1px #DCDFE6 solid; height: 100%;border-radius: 4px ;width: 100%">
<el-form :model="response" ref="response" label-width="100px">
<el-collapse-transition>
<el-tabs v-model="activeName" v-show="isActive" style="margin: 20px">
<el-tab-pane :label="$t('api_test.definition.request.response_header')" name="headers" class="pane">
<ms-api-key-value :isShowEnable="false" :suggestions="headerSuggestions" :items="response.headers"/>
</el-tab-pane>
<el-tab-pane :label="$t('api_test.definition.request.response_body')" name="body" class="pane">
<ms-api-body :isReadOnly="false" :isShowEnable="false" :body="response.body" :headers="response.headers"/>
</el-tab-pane>
<el-tab-pane :label="$t('api_test.definition.request.status_code')" name="status_code" class="pane" >
<ms-api-key-value :isShowEnable="false" :suggestions="headerSuggestions"
:items="response.statusCode" :unShowSelect = "true"/>
</el-tab-pane>
</el-tabs>
</el-collapse-transition>
</el-form>
</div>
</template>
<script>
import MsAssertionResults from "../ui/AssertionResults";
import MsCodeEdit from "metersphere-frontend/src/components/MsCodeEdit";
import MsDropdown from "metersphere-frontend/src/components/MsDropdown";
import MsApiKeyValue from "metersphere-frontend/src/components/environment/commons/ApiKeyValue";
import {REQUEST_HEADERS} from "metersphere-frontend/src/utils/constants";
import MsApiBody from "./ApiBody";
export default {
name: "MsResponseText",
components: {
MsDropdown,
MsCodeEdit,
MsAssertionResults,
MsApiKeyValue,
MsApiBody,
},
props: {
response: Object
},
data() {
return {
isActive: true,
activeName: "headers",
headerSuggestions: REQUEST_HEADERS
}
},
methods: {
active() {
this.isActive = !this.isActive;
},
},
mounted() {
if (!this.response.headers) {
return;
}
}
}
</script>
<style scoped>
.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: white;
padding: 1px 0;
height: 250px;
overflow-y: auto;
}
.text-container .pane.cookie {
padding: 0;
}
pre {
margin: 0;
}
</style>

View File

@ -0,0 +1,106 @@
<template>
<div>
<el-table
v-for="(table, index) in tables"
:key="index"
:data="table.tableData"
border
size="mini"
highlight-current-row>
<el-table-column v-for="(title, index) in table.titles" :key="index" :label="title" min-width="150px">
<template v-slot:default="scope">
<el-popover
placement="top"
trigger="click">
<el-container>
<div>{{ scope.row[title] }}</div>
</el-container>
<span class="table-content" slot="reference">{{ scope.row[title] }}</span>
</el-popover>
</template>
</el-table-column>
</el-table>
</div>
</template>
<script>
export default {
name: "MsSqlResultTable",
data() {
return {
tables: [],
titles: []
}
},
props: {
body: String
},
created() {
if (!this.body) {
return;
}
let rowArray = this.body.split("\n");
//
if (rowArray.length > 100) {
rowArray = rowArray.slice(0, 100);
}
this.getTableData(rowArray);
},
methods: {
getTableData(rowArray) {
let titles;
let result = [];
for (let i = 0; i < rowArray.length; i++) {
let colArray = rowArray[i].split("\t");
if (i === 0) {
titles = colArray;
} else {
if (colArray.length != titles.length) {
//
if (colArray.length === 1 && colArray[0] === '') {
this.getTableData(rowArray.slice(i + 1));
} else {
this.getTableData(rowArray.slice(i));
}
break;
} else {
let item = {};
for (let j = 0; j < colArray.length; j++) {
item[titles[j]] = (colArray[j] ? colArray[j] : "");
}
//
if (result.length < 100) {
result.push(item);
}
}
}
}
this.tables.splice(0, 0, {
titles: titles,
tableData: result
});
}
}
}
</script>
<style scoped>
.el-table {
margin-bottom: 20px;
}
.el-table :deep(.cell) {
white-space: nowrap;
}
.table-content {
cursor: pointer;
}
.el-container {
overflow: auto;
max-height: 500px;
}
</style>

View File

@ -71,8 +71,7 @@
</template>
<script>
//
// import MsRequestResultTail from "../../../../../../../../../../api-test/frontend/src/business/definition/components/response/RequestResultTail";
import MsRequestResultTail from "../api/RequestResultTail";
import PriorityTableItem from "../../../../../../common/tableItems/planview/PriorityTableItem";
import TypeTableItem from "../../../../../../common/tableItems/planview/TypeTableItem";
@ -105,7 +104,7 @@ export default {
MsAsideContainer,
MicroApp,
MsTableColumn, MsTable, StatusTableItem, MethodTableItem, TypeTableItem, PriorityTableItem,
// MsRequestResultTail
MsRequestResultTail
},
props: {
planId: String,

View File

@ -80,8 +80,7 @@
<script>
//
// import MsApiReport from "../../../../../../../../../../api-test/frontend/src/business/automation/report/ApiReportDetail";
import MsApiReport from "../api/ApiReportDetail";
import PriorityTableItem from "../../../../../../common/tableItems/planview/PriorityTableItem";
import TypeTableItem from "../../../../../../common/tableItems/planview/TypeTableItem";
@ -112,7 +111,7 @@ export default {
MsAsideContainer,
MicroApp,
MsTableColumn, MsTable, StatusTableItem, MethodTableItem, TypeTableItem, PriorityTableItem,
// MsApiReport
MsApiReport
},
props: {
planId: String,

View File

@ -53,6 +53,7 @@
import MsAssertionResults from "./AssertionResults";
import MsCodeEdit from "metersphere-frontend/src/components/MsCodeEdit";
import MsDropdown from "metersphere-frontend/src/components/MsDropdown";
import {BODY_FORMAT} from "metersphere-frontend/src/model/ApiTestModel";
export default {
name: "MsResponseText",