Merge remote-tracking branch 'origin/dev' into dev

This commit is contained in:
wenyann 2020-06-01 12:11:37 +08:00
commit df8984d7ca
45 changed files with 469 additions and 175 deletions

View File

@ -61,6 +61,11 @@ public class APITestController {
apiTestService.update(request, files);
}
@PostMapping(value = "/copy")
public void copy(@RequestBody SaveAPITestRequest request) {
apiTestService.copy(request);
}
@GetMapping("/get/{testId}")
public ApiTestWithBLOBs get(@PathVariable String testId) {
return apiTestService.get(testId);

View File

@ -23,6 +23,7 @@ import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.util.List;
import java.util.Objects;
import java.util.UUID;
import java.util.stream.Collectors;
import javax.annotation.Resource;
@ -69,6 +70,26 @@ public class APITestService {
saveFile(test.getId(), files);
}
public void copy(SaveAPITestRequest request) {
// copy test
ApiTestWithBLOBs copy = get(request.getId());
copy.setId(UUID.randomUUID().toString());
copy.setName(copy.getName() + " Copy");
copy.setCreateTime(System.currentTimeMillis());
copy.setUpdateTime(System.currentTimeMillis());
copy.setStatus(APITestStatus.Saved.name());
copy.setUserId(Objects.requireNonNull(SessionUtils.getUser()).getId());
apiTestMapper.insert(copy);
// copy test file
ApiTestFile apiTestFile = getFileByTestId(request.getId());
if (apiTestFile != null) {
FileMetadata fileMetadata = fileService.copyFile(apiTestFile.getFileId());
apiTestFile.setTestId(copy.getId());
apiTestFile.setFileId(fileMetadata.getId());
apiTestFileMapper.insert(apiTestFile);
}
}
public ApiTestWithBLOBs get(String id) {
return apiTestMapper.selectByPrimaryKey(id);
}

View File

@ -5,12 +5,14 @@
<resultMap id="BaseResultMap" type="io.metersphere.dto.LoadTestDTO"
extends="io.metersphere.base.mapper.LoadTestMapper.BaseResultMap">
<result column="project_name" property="projectName"/>
<result column="user_name" property="userName"/>
</resultMap>
<select id="list" resultMap="BaseResultMap" parameterType="io.metersphere.track.request.testplan.QueryTestPlanRequest">
select load_test.*, project.name as project_name
select load_test.*, project.name as project_name, user.name as user_name
from load_test
left join project on load_test.project_id = project.id
left join user on load_test.user_id = user.id
<where>
<if test="request.name != null">
and load_test.name like CONCAT('%', #{request.name},'%')

View File

@ -16,9 +16,11 @@
</sql>
<select id="getReportList" resultType="io.metersphere.dto.ReportDTO">
select ltr.id, ltr.name, ltr.test_id as testId, ltr.description,
select ltr.id, ltr.name, ltr.test_id as testId, ltr.description, user.name as userName,
ltr.create_time as createTime, ltr.update_time as updateTime, ltr.status as status, lt.name as testName
from load_test_report ltr join load_test lt on ltr.test_id = lt.id
from load_test_report ltr
join load_test lt on ltr.test_id = lt.id
join user on ltr.user_id = user.id
<if test="reportRequest.workspaceId != null">
JOIN project on project.id = lt.project_id
</if>

View File

@ -8,4 +8,5 @@ import lombok.Setter;
@Setter
public class LoadTestDTO extends LoadTest {
private String projectName;
private String userName;
}

View File

@ -18,4 +18,5 @@ public class ReportDTO {
private String testName;
private String projectId;
private String projectName;
private String userName;
}

View File

@ -57,7 +57,9 @@ public class TestCaseDataListener extends EasyExcelListener<TestCaseExcelData> {
stringBuilder.append(Translator.get("user_not_exists") + "" + data.getMaintainer() + "; ");
}
if (testCaseNames.contains(data.getName())) {
stringBuilder.append(Translator.get("test_case_already_exists") + "" + data.getName() + "; ");
stringBuilder.append(Translator.get("test_case_already_exists_excel") + "" + data.getName() + "; ");
} else {
testCaseNames.add(data.getName());
}
return stringBuilder.toString();
}

View File

@ -3,22 +3,20 @@ package io.metersphere.service;
import io.metersphere.base.domain.*;
import io.metersphere.base.mapper.FileContentMapper;
import io.metersphere.base.mapper.FileMetadataMapper;
import io.metersphere.base.mapper.ApiTestFileMapper;
import io.metersphere.base.mapper.LoadTestFileMapper;
import io.metersphere.commons.constants.FileType;
import io.metersphere.commons.exception.MSException;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import org.springframework.web.multipart.MultipartFile;
import javax.annotation.Resource;
import java.io.IOException;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;
import javax.annotation.Resource;
@Service
public class FileService {
@Resource
@ -26,8 +24,6 @@ public class FileService {
@Resource
private LoadTestFileMapper loadTestFileMapper;
@Resource
private ApiTestFileMapper ApiTestFileMapper;
@Resource
private FileContentMapper fileContentMapper;
public byte[] loadFileAsBytes(String id) {
@ -50,17 +46,6 @@ public class FileService {
return fileMetadataMapper.selectByExample(example);
}
public FileMetadata getApiFileMetadataByTestId(String testId) {
ApiTestFileExample ApiTestFileExample = new ApiTestFileExample();
ApiTestFileExample.createCriteria().andTestIdEqualTo(testId);
final List<ApiTestFile> loadTestFiles = ApiTestFileMapper.selectByExample(ApiTestFileExample);
if (CollectionUtils.isEmpty(loadTestFiles)) {
return null;
}
return fileMetadataMapper.selectByPrimaryKey(loadTestFiles.get(0).getFileId());
}
public FileContent getFileContent(String fileId) {
return fileContentMapper.selectByPrimaryKey(fileId);
}
@ -101,6 +86,21 @@ public class FileService {
return fileMetadata;
}
public FileMetadata copyFile(String fileId) {
FileMetadata fileMetadata = fileMetadataMapper.selectByPrimaryKey(fileId);
FileContent fileContent = getFileContent(fileId);
if (fileMetadata != null && fileContent != null) {
fileMetadata.setId(UUID.randomUUID().toString());
fileMetadata.setCreateTime(System.currentTimeMillis());
fileMetadata.setUpdateTime(System.currentTimeMillis());
fileMetadataMapper.insert(fileMetadata);
fileContent.setFileId(fileMetadata.getId());
fileContentMapper.insert(fileContent);
}
return fileMetadata;
}
private FileType getFileType(String filename) {
int s = filename.lastIndexOf(".") + 1;
String type = filename.substring(s);

View File

@ -130,13 +130,15 @@ public class ProjectService {
}
private void checkProjectExist (Project project) {
ProjectExample example = new ProjectExample();
example.createCriteria()
.andNameEqualTo(project.getName())
.andWorkspaceIdEqualTo(SessionUtils.getCurrentWorkspaceId())
.andIdNotEqualTo(project.getId());
if (projectMapper.selectByExample(example).size() > 0) {
MSException.throwException(Translator.get("project_name_already_exists"));
if (project.getName() != null) {
ProjectExample example = new ProjectExample();
example.createCriteria()
.andNameEqualTo(project.getName())
.andWorkspaceIdEqualTo(SessionUtils.getCurrentWorkspaceId())
.andIdNotEqualTo(project.getId());
if (projectMapper.selectByExample(example).size() > 0) {
MSException.throwException(Translator.get("project_name_already_exists"));
}
}
}

View File

@ -97,13 +97,15 @@ public class TestCaseService {
}
private void checkTestCaseExist (TestCaseWithBLOBs testCase) {
TestCaseExample example = new TestCaseExample();
example.createCriteria()
.andNameEqualTo(testCase.getName())
.andProjectIdEqualTo(testCase.getProjectId())
.andIdNotEqualTo(testCase.getId());
if (testCaseMapper.selectByExample(example).size() > 0) {
MSException.throwException(Translator.get("test_case_already_exists"));
if (testCase.getName() != null) {
TestCaseExample example = new TestCaseExample();
example.createCriteria()
.andNameEqualTo(testCase.getName())
.andProjectIdEqualTo(testCase.getProjectId())
.andIdNotEqualTo(testCase.getId());
if (testCaseMapper.selectByExample(example).size() > 0) {
MSException.throwException(Translator.get("test_case_already_exists"));
}
}
}

View File

@ -95,13 +95,15 @@ public class TestPlanService {
}
private void checkTestPlanExist (TestPlan testPlan) {
TestPlanExample example = new TestPlanExample();
example.createCriteria()
.andNameEqualTo(testPlan.getName())
.andWorkspaceIdEqualTo(SessionUtils.getCurrentWorkspaceId())
.andIdNotEqualTo(testPlan.getId());
if (testPlanMapper.selectByExample(example).size() > 0) {
MSException.throwException(Translator.get("plan_name_already_exists"));
if (testPlan.getName() != null) {
TestPlanExample example = new TestPlanExample();
example.createCriteria()
.andNameEqualTo(testPlan.getName())
.andWorkspaceIdEqualTo(SessionUtils.getCurrentWorkspaceId())
.andIdNotEqualTo(testPlan.getId());
if (testPlanMapper.selectByExample(example).size() > 0) {
MSException.throwException(Translator.get("plan_name_already_exists"));
}
}
}

View File

@ -94,3 +94,4 @@ options=options
please_input_workspace_member=Please input workspace merber
test_case_report_template_repeat=The workspace has the same name template
plan_name_already_exists=Test plan name already exists
test_case_already_exists_excel=There are duplicate test cases in the import file

View File

@ -94,3 +94,4 @@ options=选项
please_input_workspace_member=请填写该工作空间相关人员
test_case_report_template_repeat=同一工作空间下不能存在同名模版
plan_name_already_exists=测试计划名称已存在
test_case_already_exists_excel=导入文件中存在重复用例

View File

@ -93,4 +93,5 @@ module_created_automatically=若無該模塊將自動創建
options=選項
please_input_workspace_member=請填寫該工作空間相關人員
test_case_report_template_repeat=同壹工作空間下不能存在同名模版
plan_name_already_exists=測試計劃名稱已存在
plan_name_already_exists=測試計劃名稱已存在
test_case_already_exists_excel=導入文件中存在重復用例

View File

@ -1,32 +1,28 @@
<template>
<div class="relate_report">
<el-button type="success" plain @click="search">{{$t('api_report.title')}}</el-button>
<el-dialog :title="$t('api_report.title')" :visible.sync="reportVisible">
<el-table :data="tableData" v-loading="result.loading">
<el-table-column :label="$t('commons.name')" width="150" show-overflow-tooltip>
<template v-slot:default="scope">
<el-link type="info" @click="link(scope.row)">{{ scope.row.name }}</el-link>
</template>
</el-table-column>
<el-table-column width="250" :label="$t('commons.create_time')">
<template v-slot:default="scope">
<span>{{ scope.row.createTime | timestampFormatDate }}</span>
</template>
</el-table-column>
<el-table-column width="250" :label="$t('commons.update_time')">
<template v-slot:default="scope">
<span>{{ scope.row.updateTime | timestampFormatDate }}</span>
</template>
</el-table-column>
<el-table-column prop="status" :label="$t('commons.status')">
<template v-slot:default="{row}">
<ms-api-report-status :row="row"/>
</template>
</el-table-column>
</el-table>
</el-dialog>
</div>
<el-dialog :title="$t('api_report.title')" :visible.sync="reportVisible">
<el-table :data="tableData" v-loading="result.loading">
<el-table-column :label="$t('commons.name')" width="150" show-overflow-tooltip>
<template v-slot:default="scope">
<el-link type="info" @click="link(scope.row)">{{ scope.row.name }}</el-link>
</template>
</el-table-column>
<el-table-column width="250" :label="$t('commons.create_time')">
<template v-slot:default="scope">
<span>{{ scope.row.createTime | timestampFormatDate }}</span>
</template>
</el-table-column>
<el-table-column width="250" :label="$t('commons.update_time')">
<template v-slot:default="scope">
<span>{{ scope.row.updateTime | timestampFormatDate }}</span>
</template>
</el-table-column>
<el-table-column prop="status" :label="$t('commons.status')">
<template v-slot:default="{row}">
<ms-api-report-status :row="row"/>
</template>
</el-table-column>
</el-table>
</el-dialog>
</template>
<script>
@ -49,7 +45,7 @@
},
methods: {
search() {
open() {
this.reportVisible = true;
let url = "/api/report/list/" + this.testId;
@ -69,7 +65,4 @@
</script>
<style scoped>
.relate_report {
margin-left: 10px;
}
</style>

View File

@ -27,7 +27,19 @@
<el-button type="warning" plain @click="cancel">{{$t('commons.cancel')}}</el-button>
<ms-api-report-dialog :test-id="id" v-if="test.status === 'Completed'"/>
<el-dropdown trigger="click" @command="handleCommand">
<el-button class="el-dropdown-link more" icon="el-icon-more" plain/>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item command="report" :disabled="test.status !== 'Completed'">
{{$t('api_report.title')}}
</el-dropdown-item>
<el-dropdown-item command="performance" :disabled="create">
{{$t('api_test.create_performance_test')}}
</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
<ms-api-report-dialog :test-id="id" ref="reportDialog"/>
</el-row>
</el-header>
<ms-api-scenario-config :scenarios="test.scenarioDefinition" ref="config"/>
@ -72,7 +84,7 @@
},
methods: {
init: function () {
init() {
this.result = this.$get("/project/listAll", response => {
this.projects = response.data;
})
@ -87,7 +99,7 @@
}
}
},
getTest: function (id) {
getTest(id) {
this.result = this.$get("/api/get/" + id, response => {
if (response.data) {
let item = response.data;
@ -103,7 +115,7 @@
}
});
},
save: function (callback) {
save(callback) {
this.change = false;
let url = this.create ? "/api/create" : "/api/update";
this.result = this.$request(this.getOptions(url), () => {
@ -111,7 +123,7 @@
if (callback) callback();
});
},
saveTest: function () {
saveTest() {
this.save(() => {
this.$success(this.$t('commons.save_success'));
if (this.create) {
@ -121,7 +133,7 @@
}
})
},
runTest: function () {
runTest() {
this.result = this.$post("/api/run", {id: this.test.id}, (response) => {
this.$success(this.$t('api_test.running'));
this.$router.push({
@ -129,7 +141,7 @@
})
});
},
saveRunTest: function () {
saveRunTest() {
this.change = false;
this.save(() => {
@ -137,11 +149,11 @@
this.runTest();
})
},
cancel: function () {
cancel() {
// console.log(this.test.toJMX().xml)
this.$router.push('/api/test/list/all');
},
getOptions: function (url) {
getOptions(url) {
let formData = new FormData();
let request = {
id: this.test.id,
@ -166,15 +178,32 @@
'Content-Type': undefined
}
};
},
handleCommand(command) {
switch (command) {
case "report":
this.$refs.reportDialog.open();
break;
case "performance":
this.$store.commit('setTest', {
projectId: this.test.projectId,
name: this.test.name,
jmx: this.test.toJMX()
})
this.$router.push({
path: "/performance/test/create"
})
break;
}
}
},
computed: {
isShowRun() {
return this.test.projectId && this.test.name && !this.change;
return this.test.isValid() && !this.change;
},
isDisabled() {
return !(this.test.projectId && this.test.name && this.change)
return !(this.test.isValid() && this.change)
}
},
@ -199,4 +228,8 @@
.test-project {
min-width: 150px;
}
.test-container .more {
margin-left: 10px;
}
</style>

View File

@ -3,16 +3,17 @@
<ms-main-container>
<el-card class="table-card" v-loading="result.loading">
<template v-slot:header>
<ms-table-header :is-tester-permission="true" :condition.sync="condition" @search="search" :title="$t('commons.test')"
<ms-table-header :is-tester-permission="true" :condition.sync="condition" @search="search"
:title="$t('commons.test')"
@create="create" :createTip="$t('load_test.create')"/>
</template>
<el-table :data="tableData" class="table-content">
<el-table-column :label="$t('commons.name')" width="150" show-overflow-tooltip>
<el-table-column :label="$t('commons.name')" width="250" show-overflow-tooltip>
<template v-slot:default="scope">
<el-link type="info" @click="handleEdit(scope.row)">{{ scope.row.name }}</el-link>
</template>
</el-table-column>
<el-table-column prop="projectName" :label="$t('load_test.project_name')" width="150" show-overflow-tooltip/>
<el-table-column prop="projectName" :label="$t('load_test.project_name')" width="200" show-overflow-tooltip/>
<el-table-column prop="userName" :label="$t('api_test.creator')" width="150" show-overflow-tooltip/>
<el-table-column width="250" :label="$t('commons.create_time')">
<template v-slot:default="scope">
@ -31,7 +32,7 @@
</el-table-column>
<el-table-column width="150" :label="$t('commons.operating')">
<template v-slot:default="scope">
<ms-table-operator :is-tester-permission="true" @editClick="handleEdit(scope.row)" @deleteClick="handleDelete(scope.row)"/>
<ms-table-operators :buttons="buttons" :row="scope.row"/>
</template>
</el-table-column>
</el-table>
@ -49,9 +50,13 @@
import MsContainer from "../../common/components/MsContainer";
import MsMainContainer from "../../common/components/MsMainContainer";
import MsApiTestStatus from "./ApiTestStatus";
import MsTableOperators from "../../common/components/MsTableOperators";
export default {
components: {MsApiTestStatus, MsMainContainer, MsContainer, MsTableHeader, MsTablePagination, MsTableOperator},
components: {
MsTableOperators,
MsApiTestStatus, MsMainContainer, MsContainer, MsTableHeader, MsTablePagination, MsTableOperator
},
data() {
return {
result: {},
@ -62,7 +67,19 @@
currentPage: 1,
pageSize: 5,
total: 0,
loading: false
loading: false,
buttons: [
{
tip: this.$t('commons.edit'), icon: "el-icon-edit",
exec: this.handleEdit
}, {
tip: this.$t('commons.copy'), icon: "el-icon-copy-document", type: "success",
exec: this.handleCopy
}, {
tip: this.$t('commons.delete'), icon: "el-icon-delete", type: "danger",
exec: this.handleDelete
}
]
}
},
@ -111,6 +128,12 @@
}
});
},
handleCopy(test) {
this.result = this.$post("/api/copy", {id: test.id}, () => {
this.$success(this.$t('commons.delete_success'));
this.search();
});
},
init() {
this.projectId = this.$route.params.projectId;
this.search();

View File

@ -7,11 +7,11 @@
<el-row type="flex" :gutter="20" justify="space-between" align="middle">
<el-col>
<el-input v-model="item.name" size="small" maxlength="100" @change="change"
:placeholder="$t('api_test.key')"/>
:placeholder="$t('api_test.key')" show-word-limit/>
</el-col>
<el-col>
<el-input v-model="item.value" size="small" maxlength="100" @change="change"
:placeholder="$t('api_test.value')"/>
:placeholder="$t('api_test.value')" show-word-limit/>
</el-col>
<el-col class="kv-delete">
<el-button size="mini" class="el-icon-delete-solid" circle @click="remove(index)"

View File

@ -1,7 +1,7 @@
<template>
<el-form :model="request" :rules="rules" ref="request" label-width="100px">
<el-form-item :label="$t('api_test.request.name')" prop="name">
<el-input v-model="request.name" maxlength="100" @input="valid"/>
<el-input v-model="request.name" maxlength="100" show-word-limit/>
</el-form-item>
<el-form-item :label="$t('api_test.request.url')" prop="url">
@ -56,6 +56,13 @@
},
data() {
let validateURL = (rule, value, callback) => {
try {
new URL(this.addProtocol(this.request.url));
} catch (e) {
callback(this.$t('api_test.request.url_invalid'));
}
};
return {
activeName: "parameters",
rules: {
@ -63,7 +70,8 @@
{max: 100, message: this.$t('commons.input_limit', [0, 100]), trigger: 'blur'}
],
url: [
{max: 100, message: this.$t('commons.input_limit', [0, 100]), trigger: 'blur'}
{max: 100, required: true, message: this.$t('commons.input_limit', [0, 100]), trigger: 'blur'},
{validator: validateURL, trigger: 'blur'}
]
}
}
@ -74,16 +82,21 @@
if (!this.request.url) return;
let parameters = [];
let url = new URL(this.addProtocol(this.request.url));
url.searchParams.forEach((value, key) => {
if (key && value) {
parameters.push(new KeyValue(key, value));
}
});
//
parameters.push(new KeyValue());
this.request.parameters = parameters;
this.request.url = url.toString();
try {
let url = new URL(this.addProtocol(this.request.url));
url.searchParams.forEach((value, key) => {
if (key && value) {
parameters.push(new KeyValue(key, value));
}
});
//
parameters.push(new KeyValue());
this.request.parameters = parameters;
this.request.url = url.toString();
} catch (e) {
this.$error(this.$t('api_test.request.url_invalid'), 2000)
}
},
methodChange(value) {
if (value === 'GET' && this.activeName === 'body') {
@ -108,10 +121,6 @@
}
}
return url;
},
valid(value) {
value = value.replace(/[`~!@#$%^&*()_\-+=<>?:"{}|,./;'\\[\]·!¥…()—\-《》?:“”【】、;‘’,。]/g, '').replace(/\s/g, "");
this.request.name = value;
}
},

View File

@ -1,7 +1,7 @@
<template>
<el-form :model="scenario" :rules="rules" ref="scenario" label-width="100px">
<el-form-item :label="$t('api_test.scenario.name')" prop="name">
<el-input v-model="scenario.name" maxlength="100" @input="valid"/>
<el-input v-model="scenario.name" maxlength="100" show-word-limit/>
</el-form-item>
<!-- <el-form-item :label="$t('api_test.scenario.base_url')" prop="url">-->
@ -43,13 +43,6 @@
]
}
}
},
methods: {
valid(value) {
value = value.replace(/[`~!@#$%^&*()\-+=<>?:"{}|,./;'\\[\]·!¥…()—\-《》?:“”【】、;‘’,。]/g, '').replace(/\s/g, "");
this.scenario.name = value;
}
}
}
</script>

View File

@ -7,11 +7,11 @@
<el-row type="flex" :gutter="20" justify="space-between" align="middle">
<el-col>
<ms-api-variable-input v-model="item.name" size="small" maxlength="100" @change="change"
:placeholder="$t('api_test.variable_name')"/>
:placeholder="$t('api_test.variable_name')" show-word-limit/>
</el-col>
<el-col>
<el-input v-model="item.value" size="small" maxlength="100" @change="change"
:placeholder="$t('api_test.value')"/>
:placeholder="$t('api_test.value')" show-word-limit/>
</el-col>
<el-col class="kv-delete">
<el-button size="mini" class="el-icon-delete-solid" circle @click="remove(index)"

View File

@ -66,7 +66,7 @@
line-height: 32px;
position: absolute;
top: 0;
right: 25px;
right: 70px;
margin-right: -20px;
display: flex;
align-items: center;

View File

@ -2,7 +2,7 @@
<div>
<el-row :gutter="10" type="flex" justify="space-between" align="middle">
<el-col>
<el-input v-model="time" step="100" size="small" type="number"
<el-input :value="value" v-bind="$attrs" step="100" size="small" type="number" @change="change" @input="input"
:placeholder="$t('api_test.request.assertions.response_in_time')"/>
</el-col>
<el-col class="assertion-btn">
@ -14,32 +14,32 @@
</template>
<script>
import {ResponseTime} from "../../model/ScenarioModel";
export default {
name: "MsApiAssertionResponseTime",
props: {
edit: Boolean,
duration: ResponseTime,
value: [Number, String],
edit: Boolean,
callback: Function
},
data() {
return {
time: this.duration.value
}
},
methods: {
add: function () {
setTimeout(() => {
this.duration.value = this.time;
this.callback();
})
add() {
this.duration.value = this.value;
this.callback();
},
remove: function () {
remove() {
this.duration.value = undefined;
},
change(value) {
this.$emit('change', value);
},
input(value) {
this.$emit('input', value);
}
}
}

View File

@ -12,8 +12,8 @@
<el-col :span="20">
<ms-api-assertion-text :list="assertions.regex" v-if="type === options.TEXT" :callback="after"/>
<ms-api-assertion-regex :list="assertions.regex" v-if="type === options.REGEX" :callback="after"/>
<ms-api-assertion-response-time :duration="assertions.duration" v-if="type === options.RESPONSE_TIME"
:callback="after"/>
<ms-api-assertion-response-time v-model="time" :duration="assertions.duration"
v-if="type === options.RESPONSE_TIME" :callback="after"/>
</el-col>
</el-row>
@ -25,7 +25,7 @@
import MsApiAssertionText from "./ApiAssertionText";
import MsApiAssertionRegex from "./ApiAssertionRegex";
import MsApiAssertionResponseTime from "./ApiAssertionResponseTime";
import {ASSERTION_TYPE, Assertions} from "../../model/ScenarioModel";
import {ASSERTION_TYPE, Assertions, ResponseTime} from "../../model/ScenarioModel";
import MsApiAssertionsEdit from "./ApiAssertionsEdit";
export default {
@ -40,6 +40,7 @@
data() {
return {
options: ASSERTION_TYPE,
time: "",
type: "",
}
},

View File

@ -13,7 +13,7 @@
<div>
{{$t("api_test.request.assertions.response_time")}}
</div>
<ms-api-assertion-response-time :duration="assertions.duration" :edit="true"/>
<ms-api-assertion-response-time v-model="assertions.duration.value" :duration="assertions.duration" :edit="true"/>
</div>
</div>

View File

@ -110,6 +110,15 @@ export class Test extends BaseConfig {
return options;
}
isValid() {
for (let i = 0; i < this.scenarioDefinition.length; i++) {
if (this.scenarioDefinition[i].isValid()) {
return this.projectId && this.name;
}
}
return false;
}
toJMX() {
return {
name: this.name + '.jmx',
@ -136,6 +145,19 @@ export class Scenario extends BaseConfig {
options.requests = options.requests || [new Request()];
return options;
}
clone() {
return new Scenario(this);
}
isValid() {
for (let i = 0; i < this.requests.length; i++) {
if (this.requests[i].isValid()) {
return true;
}
}
return false;
}
}
export class Request extends BaseConfig {
@ -394,7 +416,10 @@ class JMXGenerator {
}
addScenarios(testPlan, scenarios) {
scenarios.forEach(scenario => {
scenarios.forEach(s => {
let scenario = s.clone();
scenario.name = this.replace(scenario.name);
let threadGroup = new ThreadGroup(scenario.name || "");
this.addScenarioVariables(threadGroup, scenario);
@ -404,6 +429,8 @@ class JMXGenerator {
scenario.requests.forEach(request => {
if (!request.isValid()) return;
request.name = this.replace(request.name);
let httpSamplerProxy = new HTTPSamplerProxy(request.name || "", new JMXRequest(request));
this.addRequestHeader(httpSamplerProxy, request);

View File

@ -0,0 +1,43 @@
<template>
<span>
<ms-table-operator-button v-for="(btn, index) in buttons" :key="index" :isTesterPermission="isTesterPermission(btn)"
:tip="btn.tip" :icon="btn.icon" :type="btn.type"
@exec="click(btn)" @click.stop="clickStop(btn)"/>
</span>
</template>
<script>
import MsTableOperatorButton from "./MsTableOperatorButton";
export default {
name: "MsTableOperators",
components: {MsTableOperatorButton},
props: {
row: Object,
buttons: Array
},
methods: {
click(btn) {
if (btn.exec instanceof Function) {
btn.exec(this.row);
}
},
clickStop(btn) {
if (btn.stop instanceof Function) {
btn.stop(this.row);
}
},
},
computed: {
isTesterPermission() {
return function (btn) {
return btn.isTesterPermission !== false;
}
}
}
}
</script>
<style scoped>
</style>

View File

@ -44,6 +44,7 @@
},
methods: {
checkLanguage(lang) {
if (!lang) return;
this.$setLang(lang);
switch (lang) {
case ZH_CN:

View File

@ -204,6 +204,11 @@ const router = new VueRouter({
name: 'trackHome',
component: TrackHome,
},
{
path: 'case/create/',
name: 'testCaseCreate',
component: TestCase,
},
{
path: 'case/:projectId',
name: 'testCase',

View File

@ -131,13 +131,13 @@
this.$warning(this.$t('report.generation_error'));
break;
case 'Starting':
this.$warning("测试处于开始状态,请稍后查看报告!");
this.$warning(this.$t('report.start_status'));
break;
case 'Reporting':
this.$info(this.$t('report.being_generated'));
break;
case 'Running':
this.$warning("测试处于运行状态,请稍后查看报告!");
this.$warning(this.$t('report.run_status'));
break;
case 'Completed':
default:
@ -178,6 +178,7 @@
this.status = data.status;
this.reportName = data.name;
this.testName = data.testName;
this.testId = data.testId;
this.projectName = data.projectName;
this.$set(this.report, "id", reportId);

View File

@ -20,6 +20,7 @@
<el-table-column
prop="name"
:label="$t('commons.name')"
width="200"
show-overflow-tooltip>
</el-table-column>
<el-table-column
@ -28,6 +29,12 @@
width="150"
show-overflow-tooltip>
</el-table-column>
<el-table-column
prop="userName"
:label="$t('report.user_name')"
width="150"
show-overflow-tooltip>
</el-table-column>
<el-table-column
width="250"
:label="$t('commons.create_time')">

View File

@ -101,7 +101,7 @@
this.avgResponseTime = '0';
this.responseTime90 = '0';
this.avgBandwidth = '0';
this.$warning("报告生成错误!")
this.$warning(this.$t('report.generation_error'));
})
this.$get("/performance/report/content/load_chart/" + this.id).then(res => {
let data = res.data.data;

View File

@ -115,7 +115,23 @@
this.listProjects();
},
mounted() {
this.importAPITest();
},
methods: {
importAPITest() {
let apiTest = this.$store.state.api.test;
if (apiTest && apiTest.name) {
this.testPlan.projectId = apiTest.projectId;
this.testPlan.name = apiTest.name;
let blob = new Blob([apiTest.jmx.xml], {type: "application/octet-stream"});
let file = new File([blob], apiTest.jmx.name);
this.$refs.basicConfig.beforeUpload(file);
this.$refs.basicConfig.handleUpload({file: file});
this.active = '1';
this.$store.commit("clearTest");
}
},
listProjects() {
this.result = this.$get(this.listProjectPath, response => {
this.projects = response.data;

View File

@ -28,6 +28,12 @@
width="150"
show-overflow-tooltip>
</el-table-column>
<el-table-column
prop="userName"
:label="$t('load_test.user_name')"
width="150"
show-overflow-tooltip>
</el-table-column>
<el-table-column
width="250"
:label="$t('commons.create_time')">

View File

@ -4,7 +4,7 @@
accept=".jmx,.csv"
drag
action=""
:limit="5"
:limit="2"
multiple
:show-file-list="false"
:before-upload="beforeUpload"
@ -175,7 +175,7 @@
}
},
handleExceed() {
this.$error(this.$t('load_test.delete_file'));
this.$error(this.$t('load_test.file_size_limit'));
},
fileValidator(file) {
/// todo:

View File

@ -88,25 +88,23 @@
mounted() {
this.getProjects();
this.refresh();
if (this.$route.params.projectId){
this.getProjectById(this.$route.params.projectId)
}
if (this.$route.path.indexOf("/track/case/edit") >= 0){
if (this.$route.path.indexOf("/track/case/edit") >= 0 || this.$route.path.indexOf("/track/case/create") >= 0){
this.openRecentTestCaseEditDialog();
this.$router.push('/track/case/all');
} else if (this.$route.params.projectId){
this.getProjectById(this.$route.params.projectId)
}
},
watch: {
'$route'(to, from) {
let path = to.path;
if (to.params.projectId){
this.getProjectById(to.params.projectId)
this.getProjects();
}
if (path.indexOf("/track/case/edit") >= 0){
if (path.indexOf("/track/case/edit") >= 0 || path.indexOf("/track/case/create") >= 0){
this.openRecentTestCaseEditDialog();
this.$router.push('/track/case/all');
this.getProjects();
} else if (to.params.projectId){
this.getProjectById(to.params.projectId);
this.getProjects();
}
},
currentProject() {
@ -160,6 +158,10 @@
},
editTestCase(testCase) {
this.testCaseReadOnly = false;
if (this.treeNodes.length < 1) {
this.$warning(this.$t('test_track.case.create_module_first'));
return;
}
this.$refs.testCaseEditDialog.open(testCase);
},
copyTestCase(testCase) {
@ -187,13 +189,22 @@
},
openRecentTestCaseEditDialog() {
let caseId = this.$route.params.caseId;
this.getProjectByCaseId(caseId);
this.$get('/test/case/get/' + caseId, response => {
if (response.data) {
this.testCaseReadOnly = false;
this.$refs.testCaseEditDialog.open(response.data);
if (caseId) {
this.getProjectByCaseId(caseId);
this.$get('/test/case/get/' + caseId, response => {
if (response.data) {
this.testCaseReadOnly = false;
this.$refs.testCaseEditDialog.open(response.data);
}
});
} else {
this.testCaseReadOnly = false;
if (this.treeNodes.length < 1) {
this.$warning(this.$t('test_track.case.create_module_first'));
return;
}
});
this.$refs.testCaseEditDialog.open();
}
},
getProjectById(id) {
if (id && id != 'all') {

View File

@ -23,6 +23,7 @@
<el-divider/>
<ms-show-all :index="'/track/case/all'"/>
<el-menu-item :index="testCaseEditPath" class="blank_item"></el-menu-item>
<ms-create-button v-permission="['test_manager', 'test_user']" :index="'/track/case/create'" :title="$t('test_track.case.create_case')"/>
</el-submenu>
<el-submenu v-if="isCurrentWorkspaceUser" index="7" popper-class="submenu">

View File

@ -232,11 +232,23 @@
}
},
methods: {
handleClose(done) {
listenGoBack() {
//
if (window.history && window.history.pushState) {
history.pushState(null, null, document.URL);
window.addEventListener('popstate', this.goBack, false);
}
},
goBack() {
this.handleClose();
},
handleClose() {
//
window.removeEventListener('popstate', this.goBack, false);
this.showDialog = false;
},
cancel() {
this.showDialog = false;
this.handleClose();
this.$emit('refreshTable');
},
statusChange(status) {
@ -296,9 +308,11 @@
item.steptResults.push(item.steps[i]);
}
this.testCase = item;
this.initTest();
},
openTestCaseEdit(testCase) {
this.showDialog = true;
this.listenGoBack();
this.initData(testCase);
},
initTest() {
@ -327,7 +341,6 @@
this.index = i;
this.getTestCase(i);
this.getRelatedTest();
this.initTest();
}
}
});

View File

@ -91,6 +91,16 @@
}
},
methods: {
listenGoBack() {
//
if (window.history && window.history.pushState) {
history.pushState(null, null, document.URL);
window.addEventListener('popstate', this.goBack, false);
}
},
goBack() {
this.handleClose();
},
open(id, isReport) {
if (isReport) {
this.isReport = isReport;
@ -112,6 +122,7 @@
this.initComponents();
}
this.showDialog = true;
this.listenGoBack();
},
initComponents() {
this.componentMap.forEach((value, key) =>{
@ -134,6 +145,7 @@
});
},
handleClose() {
window.removeEventListener('popstate', this.goBack, false);
this.showDialog = false;
},
change(evt) {
@ -221,7 +233,7 @@
}
this.$post(url + this.type, param, () =>{
this.$success(this.$t('commons.save_success'));
this.showDialog = false;
this.handleClose();
this.$emit('refresh');
});
},

View File

@ -78,12 +78,23 @@
}
},
methods: {
listenGoBack() {
//
if (window.history && window.history.pushState) {
history.pushState(null, null, document.URL);
window.addEventListener('popstate', this.goBack, false);
}
},
goBack() {
this.handleClose();
},
open(id) {
if (id) {
this.reportId = id;
}
this.getReport();
this.showDialog = true;
this.listenGoBack();
},
getReport() {
this.result = this.$get('/case/report/get/' + this.reportId, response => {
@ -113,6 +124,7 @@
});
},
handleClose() {
window.removeEventListener('popstate', this.goBack, false);
this.showDialog = false;
},
handleEdit() {

View File

@ -3,7 +3,24 @@ import Vuex from 'vuex'
Vue.use(Vuex);
const API = {
state: {
test: {}
},
mutations: {
setTest(state, test) {
state.test = test;
},
clearTest(state) {
state.test = {};
}
},
actions: {},
getters: {}
}
export default new Vuex.Store({})
export default new Vuex.Store({
modules: {
api: API
}
})

View File

@ -34,12 +34,12 @@ export default {
})
};
Vue.prototype.$error = function (message) {
Vue.prototype.$error = function (message, duration) {
Message.error({
message: message,
type: "error",
showClose: true,
duration: 10000
duration: duration || 10000
})
};

View File

@ -11,6 +11,7 @@ export default {
'save_success': 'Saved successfully',
'delete_success': 'Deleted successfully',
'modify_success': 'Modify Success',
'copy_success': 'Copy Success',
'delete_cancel': 'Deleted Cancel',
'confirm': 'Confirm',
'cancel': 'Cancel',
@ -192,6 +193,9 @@ export default {
'generation_error': 'Report generation error, cannot be viewed!',
'being_generated': 'Report is being generated...',
'delete_confirm': 'Confirm delete: ',
'start_status': 'The test is starting, please check the report later!',
'run_status': 'The test is running, please check the report later',
'user_name': 'Creator'
},
load_test: {
'operating': 'Operating',
@ -210,7 +214,7 @@ export default {
'is_running': 'Test is running! ',
'test_name_is_null': 'Test name cannot be empty! ',
'project_is_null': 'Project cannot be empty! ',
'jmx_is_null': 'Can only contain one JMX file',
'jmx_is_null': 'Must contain a JMX file, and can only contain a JMX file!',
'file_name': 'File name',
'file_size': 'File size',
'file_type': 'File Type',
@ -220,7 +224,8 @@ export default {
'upload_type': 'Only JMX/CSV files can be uploaded',
'related_file_not_found': "No related test file found!",
'delete_file_confirm': 'Confirm delete file:',
'delete_file': "Please delete an existing file first!",
'file_size_limit': "The number of files exceeds the limit",
'delete_file': "The file already exists, please delete the file with the same name first!",
'thread_num': 'Concurrent users:',
'input_thread_num': 'Please enter the number of threads',
'duration': 'Duration time (minutes):',
@ -249,6 +254,7 @@ export default {
'select_resource_pool': 'Please Select Resource Pool',
'resource_pool_is_null': 'Resource Pool is empty',
'download_log_file': 'Download',
'user_name': 'Creator'
},
api_test: {
creator: "Creator",
@ -263,12 +269,14 @@ export default {
copied: "copied",
key: "Key",
value: "Value",
create_performance_test: "Create Performance Test",
scenario: {
config: "Scenario Config",
input_name: "Please enter the scenario name",
name: "Scenario Name",
base_url: "Base URL",
base_url_description: "Base URL as URL prefix for all requests",
url_invalid: "Invalid URL",
variables: "Variables",
headers: "Headers",
kv_description: "Variables are available for all requests",
@ -391,6 +399,7 @@ export default {
delete: "Delete case",
save_create_continue: "Save and create continue",
please_create_project: "No project available, please create the project first",
create_module_first: "Please create module first",
import: {
import: "Import test case",
case_import: "Import test case",

View File

@ -10,6 +10,7 @@ export default {
'save': '保存',
'save_success': '保存成功',
'delete_success': '删除成功',
'copy_success': '复制成功',
'modify_success': '修改成功',
'delete_cancel': '已取消删除',
'confirm': '确定',
@ -190,6 +191,9 @@ export default {
'generation_error': '报告生成错误,无法查看!',
'being_generated': '报告正在生成中...',
'delete_confirm': '确认删除报告: ',
'start_status': '测试处于开始状态,请稍后查看报告!',
'run_status': '测试处于运行状态,请稍后查看报告!',
'user_name': '创建人'
},
load_test: {
'operating': '操作',
@ -207,7 +211,7 @@ export default {
'is_running': '正在运行!',
'test_name_is_null': '测试名称不能为空!',
'project_is_null': '项目不能为空!',
'jmx_is_null': '只能包含一个JMX文件',
'jmx_is_null': '必需包含一个JMX文件只能包含一个JMX文件',
'file_name': '文件名',
'file_size': '文件大小',
'file_type': '文件类型',
@ -217,7 +221,8 @@ export default {
'upload_type': '只能上传JMX/CSV文件',
'related_file_not_found': "未找到关联的测试文件!",
'delete_file_confirm': '确认删除文件: ',
'delete_file': "请先删除已存在的文件!",
'file_size_limit': "文件个数超出限制!",
'delete_file': "文件已存在,请先删除同名文件!",
'thread_num': '并发用户数:',
'input_thread_num': '请输入线程数',
'duration': '压测时长(分钟):',
@ -247,6 +252,7 @@ export default {
'resource_pool_is_null': '资源池为空',
'download_log_file': '下载完整日志文件',
'pressure_prediction_chart': '压力预估图',
'user_name': '创建人'
},
api_test: {
creator: "创建人",
@ -260,6 +266,7 @@ export default {
copied: "已拷贝",
key: "键",
value: "值",
create_performance_test: "创建性能测试",
scenario: {
config: "场景配置",
input_name: "请输入场景名称",
@ -280,6 +287,7 @@ export default {
method: "请求方法",
url: "请求URL",
url_description: "例如: https://fit2cloud.com",
url_invalid: "URL无效",
parameters: "请求参数",
parameters_desc: "参数追加到URL例如https://fit2cloud.com/entries?key1=Value1&Key2=Value2",
headers: "请求头",
@ -388,6 +396,7 @@ export default {
delete: "删除用例",
save_create_continue: "保存并继续创建",
please_create_project: "暂无项目,请先创建项目",
create_module_first: "请先新建模块",
import: {
import: "导入用例",
case_import: "导入测试用例",

View File

@ -10,6 +10,7 @@ export default {
'save': '保存',
'save_success': '保存成功',
'delete_success': '刪除成功',
'copy_success': '複製成功',
'modify_success': '修改成功',
'delete_cancel': '已取消刪除',
'confirm': '確定',
@ -191,6 +192,9 @@ export default {
'generation_error': '報告生成錯誤,無法查看!',
'being_generated': '報告正在生成中...',
'delete_confirm': '確認刪除報告: ',
'start_status': '測試處於開始狀態,請稍後查看報告!',
'run_status': '測試處於運行狀態,請稍後查看報告!',
'user_name': '創建人'
},
load_test: {
'operating': '操作',
@ -208,7 +212,7 @@ export default {
'is_running': '正在運行! ',
'test_name_is_null': '測試名稱不能為空! ',
'project_is_null': '項目不能為空! ',
'jmx_is_null': '只能包含一個JMX文件 ',
'jmx_is_null': '必需包含一個JMX文件只能包含一個JMX文件',
'file_name': '文件名',
'file_size': '文件大小',
'file_type': '文件類型',
@ -218,7 +222,8 @@ export default {
'upload_type': '只能上傳JMX/CSV文件',
'related_file_not_found': "未找到關聯的測試文件!",
'delete_file_confirm': '確認刪除文件: ',
'delete_file': "請先刪除已存在的文件!",
'file_size_limit': "文件個數超出限制!",
'delete_file': "文件已存在,請先刪除同名文件!",
'thread_num': '並髮用戶數:',
'input_thread_num': '請輸入線程數',
'duration': '壓測時長(分鐘):',
@ -248,6 +253,7 @@ export default {
'resource_pool_is_null': '資源池為空',
'download_log_file': '下載完整日誌文件',
'pressure_prediction_chart': '壓力預估圖',
'user_name': '創建人'
},
api_test: {
title: "測試",
@ -261,6 +267,7 @@ export default {
copied: "已拷貝",
key: "鍵",
value: "值",
create_performance_test: "創建性能測試",
scenario: {
creator: "創建人",
config: "場景配寘",
@ -282,6 +289,7 @@ export default {
method: "請求方法",
url: "請求URL",
url_description: "例如https://fit2cloud.com",
url_invalid: "URL無效",
parameters: "請求參數",
parameters_desc: "參數追加到URL,例如https://fit2cloud.com/entrieskey1=Value1&amp;Key2=Value2",
headers: "請求頭",
@ -390,6 +398,7 @@ export default {
delete: "删除用例",
save_create_continue: "保存並繼續創建",
please_create_project: "暫無項目,請先創建項目",
create_module_first: "請先新建模塊",
import: {
import: "導入用例",
case_import: "導入測試用例",