This commit is contained in:
chenjianxing 2021-01-27 20:07:29 +08:00
commit 0fb4fdb8ae
36 changed files with 528 additions and 260 deletions

View File

@ -158,7 +158,8 @@ public class MsHTTPSamplerProxy extends MsTestElement {
if (!path.startsWith("/")) {
path = "/" + path;
}
path = sampler.getProtocol() + "://" + sampler.getDomain() + ":" + sampler.getPort() + path;
String port = sampler.getPort() != 80 ? ":" + sampler.getPort() : "";
path = sampler.getProtocol() + "://" + sampler.getDomain() + port + path;
}
sampler.setPath(path);
}

View File

@ -24,7 +24,6 @@ import org.apache.jmeter.testelement.TestElement;
import org.apache.jorphan.collections.HashTree;
import java.util.List;
import java.util.stream.Collectors;
@Data
@EqualsAndHashCode(callSuper = true)
@ -51,7 +50,7 @@ public class MsJDBCSampler extends MsTestElement {
@JSONField(ordinal = 28)
private String dataSourceId;
@JSONField(ordinal = 29)
private String protocol="SQL";
private String protocol = "SQL";
@Override
public void toHashTree(HashTree tree, List<MsTestElement> hashTree, ParameterConfig config) {
@ -62,7 +61,8 @@ public class MsJDBCSampler extends MsTestElement {
this.getRefElement(this);
}
if (StringUtils.isNotEmpty(dataSourceId)) {
initDataSource();
this.dataSource = null;
this.initDataSource();
}
if (this.dataSource == null) {
MSException.throwException("数据源为空无法执行");
@ -79,14 +79,16 @@ public class MsJDBCSampler extends MsTestElement {
private void initDataSource() {
ApiTestEnvironmentService environmentService = CommonBeanFactory.getBean(ApiTestEnvironmentService.class);
ApiTestEnvironmentWithBLOBs environment = environmentService.get(this.dataSourceId);
ApiTestEnvironmentWithBLOBs environment = environmentService.get(environmentId);
if (environment != null && environment.getConfig() != null) {
EnvironmentConfig config = JSONObject.parseObject(environment.getConfig(), EnvironmentConfig.class);
if (CollectionUtils.isNotEmpty(config.getDatabaseConfigs())) {
List<DatabaseConfig> databaseConfigs = config.getDatabaseConfigs().stream().filter((DatabaseConfig d) -> this.dataSourceId.equals(d.getId())).collect(Collectors.toList());
if (CollectionUtils.isNotEmpty(databaseConfigs)) {
this.dataSource = databaseConfigs.get(0);
}
EnvironmentConfig envConfig = JSONObject.parseObject(environment.getConfig(), EnvironmentConfig.class);
if (CollectionUtils.isNotEmpty(envConfig.getDatabaseConfigs())) {
envConfig.getDatabaseConfigs().forEach(item -> {
if (item.getId().equals(this.dataSourceId)) {
this.dataSource = item;
return;
}
});
}
}
}

View File

@ -297,6 +297,7 @@ public class APIBackendListenerClient extends AbstractBackendListenerClient impl
requestResult.setHeaders(result.getRequestHeaders());
requestResult.setRequestSize(result.getSentBytes());
requestResult.setStartTime(result.getStartTime());
requestResult.setEndTime(result.getEndTime());
requestResult.setTotalAssertions(result.getAssertionResults().length);
requestResult.setSuccess(result.isSuccessful());
requestResult.setError(result.getErrorCount());

View File

@ -18,6 +18,8 @@ public class RequestResult {
private long startTime;
private long endTime;
private int error;
private boolean success;

View File

@ -323,7 +323,9 @@ public class ApiDefinitionService {
apiDefinitionMapper.updateByPrimaryKeyWithBLOBs(apiDefinition);
}
} else if (StringUtils.equals("incrementalMerge", apiTestImportRequest.getModeId())) {
batchMapper.insert(apiDefinition);
if (CollectionUtils.isEmpty(sameRequest)) {
batchMapper.insert(apiDefinition);
}
} else {
if (CollectionUtils.isEmpty(sameRequest)) {
batchMapper.insert(apiDefinition);

View File

@ -163,6 +163,7 @@ public class ApiScenarioReportService {
String passRate = new DecimalFormat("0%").format((float) scenarioResult.getSuccess() / (scenarioResult.getSuccess() + scenarioResult.getError()));
testPlanApiScenario.setPassRate(passRate);
testPlanApiScenario.setReportId(report.getId());
testPlanApiScenario.setUpdateTime(System.currentTimeMillis());
testPlanApiScenarioMapper.updateByPrimaryKeySelective(testPlanApiScenario);
}
returnReport = report;
@ -220,6 +221,7 @@ public class ApiScenarioReportService {
apiScenarioReportDetailMapper.insert(detail);
testPlanApiScenario.setReportId(report.getId());
testPlanApiScenario.setUpdateTime(System.currentTimeMillis());
testPlanApiScenarioMapper.updateByPrimaryKeySelective(testPlanApiScenario);
lastReport = report;
@ -343,13 +345,41 @@ public class ApiScenarioReportService {
ids = allIds.stream().filter(id -> !reportRequest.getUnSelectIds().contains(id)).collect(Collectors.toList());
}
ApiScenarioReportDetailExample detailExample = new ApiScenarioReportDetailExample();
detailExample.createCriteria().andReportIdIn(ids);
apiScenarioReportDetailMapper.deleteByExample(detailExample);
//为预防数量太多调用删除方法时引起SQL过长的Bug此处采取分批执行的方式
//每次处理的数据数量
int handleCount = 7000;
//每次处理的集合
List<String> handleIdList = new ArrayList<>(handleCount);
while (ids.size() > handleCount){
handleIdList = new ArrayList<>(handleCount);
List<String> otherIdList = new ArrayList<>();
for (int index = 0;index < ids.size();index++){
if(index<handleCount){
handleIdList.add(ids.get(index));
}else{
otherIdList.add(ids.get(index));
}
}
//处理本次的数据
ApiScenarioReportDetailExample detailExample = new ApiScenarioReportDetailExample();
detailExample.createCriteria().andReportIdIn(handleIdList);
apiScenarioReportDetailMapper.deleteByExample(detailExample);
ApiScenarioReportExample apiTestReportExample = new ApiScenarioReportExample();
apiTestReportExample.createCriteria().andIdIn(handleIdList);
apiScenarioReportMapper.deleteByExample(apiTestReportExample);
//转存剩余的数据
ids = otherIdList;
}
ApiScenarioReportExample apiTestReportExample = new ApiScenarioReportExample();
apiTestReportExample.createCriteria().andIdIn(ids);
apiScenarioReportMapper.deleteByExample(apiTestReportExample);
//处理最后剩余的数据
if(!ids.isEmpty()){
ApiScenarioReportDetailExample detailExample = new ApiScenarioReportDetailExample();
detailExample.createCriteria().andReportIdIn(ids);
apiScenarioReportDetailMapper.deleteByExample(detailExample);
ApiScenarioReportExample apiTestReportExample = new ApiScenarioReportExample();
apiTestReportExample.createCriteria().andIdIn(ids);
apiScenarioReportMapper.deleteByExample(apiTestReportExample);
}
}
public long countByProjectID(String projectId) {

View File

@ -219,7 +219,8 @@
</if>
<if test="request.name != null">
and (api_definition.name like CONCAT('%', #{request.name},'%')
or api_definition.tags like CONCAT('%', #{request.name},'%'))
or api_definition.tags like CONCAT('%', #{request.name},'%')
or api_definition.num like CONCAT('%', #{request.name},'%'))
</if>
<if test="request.protocol != null">
AND api_definition.protocol = #{request.protocol}

View File

@ -143,7 +143,9 @@
</if>
<if test="request.name != null">
and (api_scenario.name like CONCAT('%', #{request.name},'%') or api_scenario.tags like CONCAT('%', #{request.name},'%'))
and (api_scenario.name like CONCAT('%', #{request.name},'%')
or api_scenario.tags like CONCAT('%', #{request.name},'%')
or api_scenario.num like CONCAT('%', #{request.name},'%'))
</if>
<if test="request.workspaceId != null">
AND project.workspace_id = #{request.workspaceId}

View File

@ -284,7 +284,9 @@
</foreach>
</if>
<if test="request.name != null and request.name!=''">
and (t1.name like CONCAT('%', #{request.name},'%') or t1.tags like CONCAT('%', #{request.name},'%'))
and (t1.name like CONCAT('%', #{request.name},'%')
or t1.tags like CONCAT('%', #{request.name},'%')
or t1.num like CONCAT('%', #{request.name},'%'))
</if>
<if test="request.createTime > 0">
and t1.create_time >= #{request.createTime}

View File

@ -122,7 +122,7 @@
<select id="list" resultType="io.metersphere.track.dto.TestPlanCaseDTO">
select test_plan_test_case.id as id, test_case.id as caseId, test_case.name, test_case.priority,
test_case.type,test_case.test_id as testId,test_case.node_id,
test_case.type,test_case.test_id as testId,test_case.node_id, test_case.tags,
test_case.node_path, test_case.method, test_case.num, test_plan_test_case.executor, test_plan_test_case.status,
test_plan_test_case.update_time, test_case_node.name as model, project.name as projectName,
test_plan_test_case.plan_id as planId
@ -139,7 +139,7 @@
</if>
<if test="request.name != null">
and (test_case.name like CONCAT('%', #{request.name},'%') or test_case.num like
CONCAT('%',#{request.name},'%'))
CONCAT('%',#{request.name},'%') or test_case.tags like CONCAT('%', #{request.name},'%'))
</if>
<if test="request.id != null">
and test_case.id = #{request.id}

@ -1 +1 @@
Subproject commit 387ca56312b62ae5edb3d7f34afa08946d86d621
Subproject commit 26d36f3f81e889f58eed7c6903252a129b301d98

View File

@ -1,11 +1,11 @@
create table swagger_url_project
(
id varchar(255) not null,
project_id varchar(255) null,
id varchar(30) not null,
project_id varchar(30) null,
swagger_url varchar(255) null,
module_id varchar(255) null,
module_id varchar(30) null,
module_path varchar(255) null,
mode_id varchar(255) null,
mode_id varchar(30) null,
primary key (id)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4;

View File

@ -14,7 +14,7 @@
<ms-main-container>
<el-tabs v-model="activeName" @tab-click="addTab" @tab-remove="removeTab">
<el-tab-pane name="default" :label="$t('api_test.automation.scenario_test')">
<el-tab-pane name="default" :label="$t('api_test.automation.scenario_list')">
<ms-api-scenario-list
:module-tree="nodeTree"
:module-options="moduleOptions"
@ -97,6 +97,7 @@
renderComponent: true,
isHide: true,
activeName: 'default',
redirectFlag: 'none',
currentModule: null,
moduleOptions: [],
tabs: [],
@ -150,6 +151,15 @@
},
changeRedirectParam(redirectIDParam) {
this.redirectID = redirectIDParam;
if(redirectIDParam!=null){
if(this.redirectFlag == "none"){
this.activeName = "default";
this.addListener();
this.redirectFlag = "redirected";
}
}else{
this.redirectFlag = "none";
}
},
addTab(tab) {
if (!getCurrentProjectID()) {
@ -227,8 +237,18 @@
this.$refs.apiScenarioList.search(data);
},
refresh(data) {
this.setTabTitle(data);
this.$refs.apiScenarioList.search(data);
},
setTabTitle(data) {
for (let index in this.tabs) {
let tab = this.tabs[index];
if (tab.name === this.activeName) {
tab.label = data.name;
break;
}
}
},
editScenario(row) {
this.addTab({name: 'edit', currentScenario: row});
},

View File

@ -118,6 +118,7 @@
throw e;
}
this.getFails();
this.computeTotalTime();
this.loading = false;
} else {
setTimeout(this.getReport, 2000)
@ -146,12 +147,30 @@
failScenario.requestResults.push(failRequest);
}
})
}
})
}
}
},
computeTotalTime() {
if (this.content.scenarios) {
let startTime = 99991611737506593;
let endTime = 0;
this.content.scenarios.forEach((scenario) => {
scenario.requestResults.forEach((request) => {
if (request.startTime && Number(request.startTime) < startTime) {
startTime = request.startTime;
}
if (request.endTime && Number(request.endTime) > endTime) {
endTime = request.endTime;
}
})
})
if (startTime < endTime) {
this.totalTime = endTime - startTime + 100;
}
}
},
requestResult(requestResult) {
this.active();
this.isRequestResult = false;

View File

@ -1,17 +1,21 @@
<template>
<div class="request-result">
<div>
<el-row :gutter="10" type="flex" align="middle" class="info">
<el-row :gutter="8" type="flex" align="middle" class="info">
<el-col :span="2">
<div class="method">
{{request.method}}
</div>
</el-col>
<el-col :span="10">
<el-col :span="8">
<el-tooltip effect="dark" :content="request.url" placement="bottom" :open-delay="800">
<div class="url">{{request.url}}</div>
</el-tooltip>
</el-col>
<el-col :span="8">
<div class="url"> {{$t('api_report.start_time')}}{{request.startTime | timestampFormatDate(true) }}
</div>
</el-col>
</el-row>
</div>
<el-collapse-transition>
@ -19,7 +23,7 @@
<el-tabs v-model="activeName" v-show="isActive" v-if="hasSub">
<el-tab-pane :label="$t('api_report.sub_result')" name="sub">
<ms-request-sub-result class="sub-result" v-for="(sub, index) in request.subRequestResults"
:key="index" :indexNumber="index" :request="sub"/>
:key="index" :indexNumber="index" :request="sub"/>
</el-tab-pane>
<el-tab-pane :label="$t('api_report.request_result')" name="result">
<ms-response-text :request-type="requestType" :response="request.responseResult" :request="request"/>
@ -43,7 +47,7 @@
export default {
name: "MsRequestResultTail",
components: {MsResponseText, MsRequestText, MsAssertionResults, MsRequestMetric, MsRequestResult,MsRequestSubResult},
components: {MsResponseText, MsRequestText, MsAssertionResults, MsRequestMetric, MsRequestResult, MsRequestSubResult},
props: {
request: Object,
scenarioName: String,

View File

@ -3,7 +3,7 @@
<el-card class="table-card" v-loading="loading">
<template v-slot:header>
<ms-table-header :condition.sync="condition" @search="selectByParam" title=""
:show-create="false"/>
:show-create="false" :tip="$t('commons.search_by_id_name_tag')"/>
</template>
<el-table ref="scenarioTable" border :data="tableData" class="adjust-table ms-select-all-fixed"

View File

@ -848,7 +848,7 @@
if (this.currentScenario.tags instanceof String) {
this.currentScenario.tags = JSON.parse(this.currentScenario.tags);
}
this.$emit('refresh');
this.$emit('refresh',this.currentScenario);
})
}
})

View File

@ -8,12 +8,22 @@
<el-form :model="form" :rules="rules" ref="from">
<el-form-item
prop="cronValue">
<el-input :disabled="isReadOnly" v-model="form.cronValue" class="inp"
:placeholder="$t('schedule.please_input_cron_expression')"/>
<el-button :disabled="isReadOnly" type="primary" @click="saveCron" v-tester>{{
$t('commons.save')
}}
</el-button>
<el-row>
<el-col :span="18">
<el-input :disabled="isReadOnly" v-model="form.cronValue" class="inp"
:placeholder="$t('schedule.please_input_cron_expression')"/>
<el-button :disabled="isReadOnly" type="primary" @click="saveCron" v-tester>{{
$t('commons.save')
}}
</el-button>
</el-col>
<el-col :span="6">
<schedule-switch :schedule="schedule" @scheduleChange="scheduleChange"></schedule-switch>
</el-col>
</el-row>
</el-form-item>
<el-form-item>
<el-link :disabled="isReadOnly" type="primary" @click="showCronDialog">
@ -44,6 +54,7 @@ import Crontab from "@/business/components/common/cron/Crontab";
import CrontabResult from "@/business/components/common/cron/CrontabResult";
import {cronValidate} from "@/common/js/cron";
import MsScheduleNotification from "./ScheduleNotification";
import ScheduleSwitch from "@/business/components/api/automation/schedule/ScheduleSwitch";
function defaultCustomValidate() {
return {pass: true};
@ -55,7 +66,7 @@ const noticeTemplate = requireComponent.keys().length > 0 ? requireComponent("./
export default {
name: "MsScheduleMaintain",
components: {CrontabResult, Crontab, MsScheduleNotification, "NoticeTemplate": noticeTemplate.default},
components: {CrontabResult, ScheduleSwitch,Crontab, MsScheduleNotification, "NoticeTemplate": noticeTemplate.default},
props: {
customValidate: {
@ -100,6 +111,7 @@ export default {
form: {
cronValue: ""
},
paramRow:{},
activeName: 'first',
rules: {
cronValue: [{required: true, validator: validateCron, trigger: 'blur'}],
@ -110,6 +122,35 @@ export default {
currentUser: () => {
return getCurrentUser();
},
scheduleChange(){
let flag = this.schedule.enable;
this.$confirm(this.$t('api_test.home_page.running_task_list.confirm.close_title'), this.$t('commons.prompt'), {
confirmButtonText: this.$t('commons.confirm'),
cancelButtonText: this.$t('commons.cancel'),
type: 'warning'
}).then(() => {
let param = {};
param.taskID = this.schedule.id;
param.enable = flag;
this.updateTask(param);
}).catch(() => {
});
},
updateTask(param){
this.result = this.$post('/api/schedule/updateEnableByPrimyKey', param, response => {
let paramTestId = "";
if (this.paramRow.redirectFrom == 'testPlan') {
paramTestId = this.paramRow.id;
this.scheduleTaskType = "TEST_PLAN_TEST";
} else {
paramTestId = this.paramRow.id;
this.scheduleTaskType = "API_SCENARIO_TEST";
}
this.taskID = paramTestId;
this.findSchedule(paramTestId);
});
},
initUserList() {
let param = {
name: '',
@ -132,6 +173,7 @@ export default {
open(row) {
//
let paramTestId = "";
this.paramRow = row;
if (row.redirectFrom == 'testPlan') {
paramTestId = row.id;
this.scheduleTaskType = "TEST_PLAN_TEST";

View File

@ -0,0 +1,82 @@
<template>
<div class="schedule-config">
<div>
<span class="cron-ico">
<i class="el-icon-date" size="small"></i>
<span class="character">SCHEDULER</span>
</span>
<!-- <el-switch :disabled="!schedule.value || isReadOnly" v-model="schedule.enable" @change="scheduleChange"/>-->
<!-- <el-switch :disabled="!schedule.value || isReadOnly" v-model="schedule.enable" />-->
<el-switch :disabled="!schedule.value" v-model="schedule.enable" @change="scheduleChange"/>
</div>
<div>
<span>
{{ $t('schedule.next_execution_time') }}
<span :class="{'disable-character': !schedule.enable}"
v-if="!schedule.enable">{{ $t('schedule.not_set') }}</span>
<crontab-result v-if="schedule.enable" :enable-simple-mode="true" :ex="schedule.value" ref="crontabResult"/>
</span>
</div>
</div>
</template>
<script>
import CrontabResult from "@/business/components/common/cron/CrontabResult";
export default {
name: "ScheduleSwitch",
components: {CrontabResult},
data() {
return {
}
},
props: {
testId: String,
schedule: Object,
isReadOnly: {
type: Boolean,
default: false
}
},
methods: {
scheduleChange() {
this.$emit('scheduleChange');
},
},
watch: {
}
}
</script>
<style scoped>
.schedule-config {
float: right;
width: 250px;
height: 15px;
line-height: 25px;
}
.el-icon-date {
font-size: 20px;
margin-left: 5px;
}
.character {
font-weight: bold;
margin: 0 5px;
}
.disable-character {
color: #cccccc;
}
.el-switch {
margin: 0 5px;
}
.cron-ico {
cursor: pointer;
}
</style>

View File

@ -23,7 +23,7 @@
</div>
</el-row>
<el-dialog :title="$t('api_test.request.assertions.script')" :visible.sync="visible" width="900px">
<el-dialog :title="$t('api_test.request.assertions.script')" :visible.sync="visible" width="900px" append-to-body>
<el-row type="flex" justify="space-between" align="middle" class="quick-script-block">
<div class="assertion-item input">
<el-input size="small" v-model="assertion.variable"
@ -55,206 +55,206 @@
</template>
<script>
import {AssertionJSR223} from "../../model/ApiTestModel";
import MsDialogFooter from "@/business/components/common/components/MsDialogFooter";
import MsJsr233Processor from "../../../automation/scenario/component/Jsr233Processor";
import {AssertionJSR223} from "../../model/ApiTestModel";
import MsDialogFooter from "@/business/components/common/components/MsDialogFooter";
import MsJsr233Processor from "../../../automation/scenario/common/Jsr233ProcessorContent";
export default {
name: "MsApiAssertionJsr223",
components: {MsJsr233Processor, MsDialogFooter},
props: {
assertion: {
default: () => {
return new AssertionJSR223();
}
},
edit: {
type: Boolean,
default: false
},
index: Number,
list: Array,
callback: Function,
isReadOnly: {
type: Boolean,
default: false
}
},
data() {
return {
visible: false,
operators: {
EQ: {
label: "commons.adv_search.operators.equals",
value: "=="
},
NE: {
label: "commons.adv_search.operators.not_equals",
value: "!="
},
CONTAINS: {
label: "commons.adv_search.operators.like",
value: "contains"
},
NOT_CONTAINS: {
label: "commons.adv_search.operators.not_like",
value: "not contains"
},
GT: {
label: "commons.adv_search.operators.gt",
value: ">"
},
LT: {
label: "commons.adv_search.operators.lt",
value: "<"
},
IS_EMPTY: {
label: "commons.adv_search.operators.is_empty",
value: "is empty"
},
IS_NOT_EMPTY: {
label: "commons.adv_search.operators.is_not_empty",
value: "is not empty"
export default {
name: "MsApiAssertionJsr223",
components: {MsJsr233Processor, MsDialogFooter},
props: {
assertion: {
default: () => {
return new AssertionJSR223();
}
},
templates: [
{
title: this.$t('api_test.request.assertions.set_failure_status'),
value: 'AssertionResult.setFailure(true)',
edit: {
type: Boolean,
default: false
},
index: Number,
list: Array,
callback: Function,
isReadOnly: {
type: Boolean,
default: false
}
},
data() {
return {
visible: false,
operators: {
EQ: {
label: "commons.adv_search.operators.equals",
value: "=="
},
NE: {
label: "commons.adv_search.operators.not_equals",
value: "!="
},
CONTAINS: {
label: "commons.adv_search.operators.like",
value: "contains"
},
NOT_CONTAINS: {
label: "commons.adv_search.operators.not_like",
value: "not contains"
},
GT: {
label: "commons.adv_search.operators.gt",
value: ">"
},
LT: {
label: "commons.adv_search.operators.lt",
value: "<"
},
IS_EMPTY: {
label: "commons.adv_search.operators.is_empty",
value: "is empty"
},
IS_NOT_EMPTY: {
label: "commons.adv_search.operators.is_not_empty",
value: "is not empty"
}
},
{
title: this.$t('api_test.request.assertions.set_failure_msg'),
value: 'AssertionResult.setFailureMessage("msg")',
templates: [
{
title: this.$t('api_test.request.assertions.set_failure_status'),
value: 'AssertionResult.setFailure(true)',
},
{
title: this.$t('api_test.request.assertions.set_failure_msg'),
value: 'AssertionResult.setFailureMessage("msg")',
}
],
}
},
methods: {
add() {
this.list.push(new AssertionJSR223(this.assertion));
this.callback();
},
remove() {
this.list.splice(this.index, 1);
},
changeOperator(value) {
if (value.indexOf("empty") > 0 && !!this.assertion.value) {
this.assertion.value = "";
}
],
}
},
this.quickScript();
},
quickScript() {
if (this.assertion.variable && this.assertion.operator) {
let variable = this.assertion.variable;
let operator = this.assertion.operator;
let value = this.assertion.value || "";
let desc = "${" + variable + "} " + operator + " '" + value + "'";
let script = "value = vars.get(\"" + variable + "\");\n"
switch (this.assertion.operator) {
case "==":
script += "result = \"" + value + "\".equals(value);\n";
break;
case "!=":
script += "result = !\"" + value + "\".equals(value);\n";
break;
case "contains":
script += "result = value.contains(\"" + value + "\");\n";
break;
case "not contains":
script += "result = !value.contains(\"" + value + "\");\n";
break;
case ">":
desc = "${" + variable + "} " + operator + " " + value;
script += "number = Integer.parseInt(value);\n" +
"result = number > " + value + ";\n";
break;
case "<":
desc = "${" + variable + "} " + operator + " " + value;
script += "number = Integer.parseInt(value);\n" +
"result = number < " + value + ";\n";
break;
case "is empty":
desc = "${" + variable + "} " + operator
script += "result = value == void || value.length() == 0;\n";
break;
case "is not empty":
desc = "${" + variable + "} " + operator
script += "result = value != void && value.length() > 0;\n";
break;
}
let msg = "assertion [" + desc + "]: false;"
script += "if (!result){\n" +
"\tmsg = \"" + msg + "\";\n" +
"\tAssertionResult.setFailureMessage(msg);\n" +
"\tAssertionResult.setFailure(true);\n" +
"}";
methods: {
add() {
this.list.push(new AssertionJSR223(this.assertion));
this.callback();
},
remove() {
this.list.splice(this.index, 1);
},
changeOperator(value) {
if (value.indexOf("empty") > 0 && !!this.assertion.value) {
this.assertion.value = "";
}
this.quickScript();
},
quickScript() {
if (this.assertion.variable && this.assertion.operator) {
let variable = this.assertion.variable;
let operator = this.assertion.operator;
let value = this.assertion.value || "";
let desc = "${" + variable + "} " + operator + " '" + value + "'";
let script = "value = vars.get(\"" + variable + "\");\n"
switch (this.assertion.operator) {
case "==":
script += "result = \"" + value + "\".equals(value);\n";
break;
case "!=":
script += "result = !\"" + value + "\".equals(value);\n";
break;
case "contains":
script += "result = value.contains(\"" + value + "\");\n";
break;
case "not contains":
script += "result = !value.contains(\"" + value + "\");\n";
break;
case ">":
desc = "${" + variable + "} " + operator + " " + value;
script += "number = Integer.parseInt(value);\n" +
"result = number > " + value + ";\n";
break;
case "<":
desc = "${" + variable + "} " + operator + " " + value;
script += "number = Integer.parseInt(value);\n" +
"result = number < " + value + ";\n";
break;
case "is empty":
desc = "${" + variable + "} " + operator
script += "result = value == void || value.length() == 0;\n";
break;
case "is not empty":
desc = "${" + variable + "} " + operator
script += "result = value != void && value.length() > 0;\n";
break;
this.assertion.desc = desc;
this.assertion.script = script;
this.$refs.jsr233.reload();
}
let msg = "assertion [" + desc + "]: false;"
script += "if (!result){\n" +
"\tmsg = \"" + msg + "\";\n" +
"\tAssertionResult.setFailureMessage(msg);\n" +
"\tAssertionResult.setFailure(true);\n" +
"}";
this.assertion.desc = desc;
this.assertion.script = script;
this.$refs.jsr233.reload();
},
detail() {
this.visible = true;
},
close() {
this.visible = false;
},
confirm() {
if (!this.edit) {
this.add();
}
if (!this.assertion.desc) {
this.assertion.desc = this.assertion.script;
}
this.close();
}
},
},
detail() {
this.visible = true;
},
close() {
this.visible = false;
},
confirm() {
if (!this.edit) {
this.add();
computed: {
hasEmptyOperator() {
return !!this.assertion.operator && this.assertion.operator.indexOf("empty") > 0;
}
if (!this.assertion.desc) {
this.assertion.desc = this.assertion.script;
}
this.close();
}
},
computed: {
hasEmptyOperator() {
return !!this.assertion.operator && this.assertion.operator.indexOf("empty") > 0;
}
}
}
</script>
<style scoped>
.assertion-item {
display: inline-block;
}
.assertion-item {
display: inline-block;
}
.assertion-item + .assertion-item {
margin-left: 10px;
}
.assertion-item + .assertion-item {
margin-left: 10px;
}
.assertion-item.input {
width: 100%;
}
.assertion-item.input {
width: 100%;
}
.assertion-item.select {
min-width: 150px;
}
.assertion-item.select {
min-width: 150px;
}
.assertion-item.label {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.assertion-item.label {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.assertion-item.btn {
min-width: 130px;
}
.assertion-item.btn {
min-width: 130px;
}
.assertion-item.btn.circle {
text-align: right;
min-width: 80px;
}
.assertion-item.btn.circle {
text-align: right;
min-width: 80px;
}
.quick-script-block {
margin-bottom: 10px;
}
.quick-script-block {
margin-bottom: 10px;
}
</style>

View File

@ -3,13 +3,13 @@
<el-dialog
:title="$t('api_test.environment.select_environment')"
:visible.sync="dialogVisible"
width="25%"
width="15%"
:destroy-on-close="true"
@close="handleClose"
>
<el-form label-position="right" label-width="150px" size="medium" ref="form">
<el-form ref="form">
<el-form-item prop="type">
<el-select v-model="environmentId" value-key="id" size="small" class="ms-htt-width"
<el-select v-model="environmentId" value-key="id" class="ms-htt-width"
:placeholder="$t('api_test.definition.request.run_env')"
clearable>
<el-option v-for="(environment, index) in environments" :key="index"
@ -22,6 +22,7 @@
</el-select>
</el-form-item>
</el-form>
<template v-slot:footer>
<!-- <el-button onclick="this.handleClose">{{ $t('commons.cancel') }}</el-button>-->
<el-button type="primary" @click="createPerformance" @keydown.enter.native.prevent>

View File

@ -213,7 +213,7 @@
},
copyCase(data) {
let uuid = getUUID();
let obj = {name: "copy_" + data.name, priority: data.priority, active: true, request: data.request, uuid: uuid};
let obj = {name: "copy_" + data.name, priority: data.priority, active: true, tags: data.tags, request: data.request, uuid: uuid};
this.$emit('copyCase', obj);
},
selectTestCase(item, $event) {

View File

@ -5,7 +5,7 @@
@isApiListEnableChange="isApiListEnableChange">
<el-link type="primary" style="float:right;margin-top: 5px" @click="open">{{$t('commons.adv_search.title')}}</el-link>
<el-input placeholder="搜索" @blur="search" @keyup.enter.native="search" class="search-input" size="small"
<el-input :placeholder="$t('commons.search_by_id_name_tag')" @blur="search" @keyup.enter.native="search" class="search-input" size="small"
v-model="condition.name"/>
<el-table v-loading="result.loading"

View File

@ -5,7 +5,7 @@
@isApiListEnableChange="isApiListEnableChange">
<el-link type="primary" @click="open" style="float: right;margin-top: 5px">{{$t('commons.adv_search.title')}}</el-link>
<el-input :placeholder="$t('api_monitor.please_search')" @blur="search" class="search-input" size="small" @keyup.enter.native="search"
<el-input :placeholder="$t('commons.search_by_id_name_tag')" @blur="search" class="search-input" size="small" @keyup.enter.native="search"
v-model="condition.name"/>
<el-table v-loading="result.loading"
@ -274,7 +274,12 @@
},
},
created: function () {
this.condition.filters = {status: ["Prepare", "Underway", "Completed"]};
if (this.trashEnable) {
this.condition.filters = {status: ["Trash"]};
}
else {
this.condition.filters = {status: ["Prepare", "Underway", "Completed"]};
}
this.initTable();
this.getMaintainerOptions();
},

View File

@ -180,10 +180,10 @@
}
},
//---使
createRootModel(){
createRootModel() {
let dataArr = this.$refs.nodeTree.extendTreeNodes;
if(dataArr.length>0){
this.$refs.nodeTree.append({},dataArr[0]);
if (dataArr.length > 0) {
this.$refs.nodeTree.append({}, dataArr[0]);
}
},
exportAPI() {
@ -197,7 +197,6 @@
},
refresh() {
this.list();
this.$emit("refreshTable");
},
}
}

View File

@ -35,8 +35,8 @@
</el-table-column>
</el-table>
<el-button class="ht-btn-add" size="mini" p="$t('commons.add')" icon="el-icon-circle-plus-outline" @click="add">添加
</el-button>
<el-button class="ht-btn-add" size="mini" p="$t('commons.add')" icon="el-icon-circle-plus-outline" @click="add">{{$t("commons.add")}}</el-button>
<el-button class="ht-btn-add" size="mini" p="$t('commons.add')" icon="el-icon-files" @click="copy">{{$t("commons.copy")}}</el-button>
</div>
</template>
@ -68,6 +68,16 @@
this.$emit('change', this.hostTable);
},
add: function (r) {
let row = {
ip: '',
domain: '',
status: 'edit',
annotation: '',
uuid: this.uuid(),
}
this.hostTable.push(row);
},
copy: function (r) {
let row = {
ip: '',
domain: '',

View File

@ -4,7 +4,7 @@
<span v-if="triggerMode === 'SCHEDULE'">{{$t('commons.trigger_mode.schedule')}}</span>
<span v-if="triggerMode === 'TEST_PLAN_SCHEDULE'">{{$t('commons.trigger_mode.schedule')}}</span>
<span v-if="triggerMode === 'API'">{{$t('commons.trigger_mode.api')}}</span>
<span v-if="triggerMode === 'CASE'">用例触发</span>
<span v-if="triggerMode === 'CASE'">{{$t('commons.trigger_mode.case')}}</span>
</span>
</template>

View File

@ -143,6 +143,7 @@ export default {
{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('commons.trigger_mode.case'), value: 'CASE'},
],
buttons: [
{

View File

@ -14,7 +14,7 @@
<el-tab-pane v-if="hasLicense()" :label="$t('display.title')" name="display">
<ms-display/>
</el-tab-pane>
<el-tab-pane v-if="hasLicense()" :label="'认证设置'" name="auth">
<el-tab-pane v-if="hasLicense()" :label="$t('auth_source.title')" name="auth">
<ms-auth/>
</el-tab-pane>
</el-tabs>

View File

@ -15,6 +15,7 @@
:filter-node-method="filterNode"
:expand-on-click-node="false"
highlight-current
style="overflow: auto"
@node-click="nodeClick"
ref="tree"
>

View File

@ -31,14 +31,14 @@
@select="handleSelect"
@cell-mouse-enter="showPopover"
row-key="id"
class="test-content adjust-table ms-select-all"
class="test-content adjust-table ms-select-all-fixed"
ref="table" @row-click="handleEdit">
<el-table-column
width="50"
type="selection"/>
<ms-table-select-all
<ms-table-header-select-popover v-show="total>0"
:page-size="pageSize > total ? total : pageSize"
:total="total"
@selectPageAll="isSelectDataAll(false)"
@ -78,6 +78,7 @@
prop="priority"
:filters="priorityFilters"
column-key="priority"
min-width="100px"
:label="$t('test_track.case.priority')"
show-overflow-tooltip>
<template v-slot:default="scope">
@ -98,6 +99,7 @@
prop="method"
column-key="method"
:filters="methodFilters"
min-width="100px"
:label="$t('test_track.case.method')"
show-overflow-tooltip>
<template v-slot:default="scope">
@ -108,6 +110,7 @@
<el-table-column
:filters="statusFilters"
column-key="status"
min-width="100px"
:label="$t('test_track.case.status')">
<template v-slot:default="scope">
<span class="el-dropdown-link">
@ -127,6 +130,7 @@
<el-table-column
prop="nodePath"
:label="$t('test_track.case.module')"
min-width="150px"
show-overflow-tooltip>
</el-table-column>
@ -134,12 +138,13 @@
prop="updateTime"
sortable="custom"
:label="$t('commons.update_time')"
min-width="150px"
show-overflow-tooltip>
<template v-slot:default="scope">
<span>{{ scope.row.updateTime | timestampFormatDate }}</span>
</template>
</el-table-column>
<el-table-column
<el-table-column fixed="right"
:label="$t('commons.operating')" min-width="150">
<template v-slot:default="scope">
<ms-table-operator :is-tester-permission="true" @editClick="handleEdit(scope.row)"
@ -169,6 +174,7 @@
<script>
import MsCreateBox from '../../../settings/CreateBox';
import MsTableHeaderSelectPopover from "@/business/components/common/components/table/MsTableHeaderSelectPopover";
import TestCaseImport from '../components/TestCaseImport';
import TestCaseExport from '../components/TestCaseExport';
import MsTablePagination from '../../../../components/common/pagination/TablePagination';
@ -191,7 +197,6 @@ import TestCaseDetail from "./TestCaseDetail";
import ReviewStatus from "@/business/components/track/case/components/ReviewStatus";
import {getCurrentProjectID} from "../../../../../common/js/utils";
import MsTag from "@/business/components/common/components/MsTag";
import MsTableSelectAll from "../../../common/components/table/MsTableSelectAll";
import {_handleSelect, _handleSelectAll} from "../../../../../common/js/tableUtils";
import BatchMove from "./BatchMove";
@ -199,7 +204,7 @@ export default {
name: "TestCaseList",
components: {
BatchMove,
MsTableSelectAll,
MsTableHeaderSelectPopover,
MsTableButton,
MsTableOperatorButton,
MsTableOperator,

View File

@ -19,8 +19,8 @@
</el-table-column>
<el-table-column prop="tagNames" :label="$t('api_test.automation.tag')" width="200px">
<template v-slot:default="scope">
<div v-for="itemName in scope.row.tagNames" :key="itemName">
<ms-tag type="success" effect="plain" :content="itemName"/>
<div v-for="itemName in getTagString(scope.row.tags)" :key="itemName">
<ms-tag type="success" effect="plain" :content="itemName.substring(1, itemName.length-1)"/>
</div>
</template>
</el-table-column>
@ -101,6 +101,15 @@
},
},
methods: {
getTagString(tagsString) {
// if(tagsString.length == 2) {
if(tagsString.length >= 2 && tagsString[0] == '[' && tagsString[1] == ']') {
return null;
}
tagsString = tagsString.substring(1, tagsString.length - 1);
let tagList = tagsString.split(',');
return tagList;
},
search() {
this.selectRows = new Set();
this.loading = true;

View File

@ -76,6 +76,14 @@
</template>
</el-table-column>
<el-table-column prop="tags" :label="$t('commons.tag')">
<template v-slot:default="scope">
<div v-for="(tag, index) in scope.row.showTags" :key="tag + '_' + index">
<ms-tag type="success" effect="plain" :content="tag"/>
</div>
</template>
</el-table-column>
<el-table-column
prop="method"
:filters="methodFilters"
@ -179,8 +187,9 @@
</template>
</el-table-column>
<el-table-column
min-width="100"
:label="$t('commons.operating')">
fixed="right"
min-width="100"
:label="$t('commons.operating')">
<template v-slot:default="scope">
<ms-table-operator-button :is-tester-permission="true" :tip="$t('commons.edit')" icon="el-icon-edit"
@exec="handleEdit(scope.row)"/>
@ -231,6 +240,7 @@ import ShowMoreBtn from "../../../../case/components/ShowMoreBtn";
import BatchEdit from "../../../../case/components/BatchEdit";
import ClassicEditor from "@ckeditor/ckeditor5-build-classic";
import {hub} from "@/business/components/track/plan/event-bus";
import MsTag from "@/business/components/common/components/MsTag";
export default {
name: "FunctionalTestCaseList",
@ -243,7 +253,7 @@ export default {
StatusTableItem,
PriorityTableItem, StatusEdit, ExecutorEdit, MsTipButton, MsTablePagination,
MsTableHeader, NodeBreadcrumb, MsTableButton, ShowMoreBtn,
BatchEdit
BatchEdit, MsTag
},
data() {
return {
@ -257,7 +267,7 @@ export default {
currentPage: 1,
pageSize: 10,
total: 0,
status:'default',
status: 'default',
selectRows: new Set(),
testPlan: {},
isReadOnly: false,
@ -319,7 +329,7 @@ export default {
planId: {
type: String
},
clickType:String,
clickType: String,
selectNodeIds: {
type: Array
},
@ -354,10 +364,10 @@ export default {
// param.planId = this.planId;
this.condition.planId = this.planId;
}
if(this.clickType){
if(this.status =='default'){
if (this.clickType) {
if (this.status == 'default') {
this.condition.status = this.clickType;
}else{
} else {
this.condition.status = null;
}
this.status = 'all';
@ -373,6 +383,7 @@ export default {
this.tableData = data.listObject;
for (let i = 0; i < this.tableData.length; i++) {
if (this.tableData[i]) {
this.$set(this.tableData[i], "showTags", JSON.parse(this.tableData[i].tags));
this.$set(this.tableData[i], "issuesSize", 0);
this.$get("/issues/get/" + this.tableData[i].caseId).then(response => {
let issues = response.data.data;
@ -381,7 +392,11 @@ export default {
this.$set(this.tableData[i], "issuesContent", issues);
}
}).catch(() => {
this.$set(this.tableData[i], "issuesContent", [{title: '获取缺陷失败',description: '获取缺陷失败',platform: '获取缺陷失败' }]);
this.$set(this.tableData[i], "issuesContent", [{
title: '获取缺陷失败',
description: '获取缺陷失败',
platform: '获取缺陷失败'
}]);
})
}
}
@ -597,4 +612,7 @@ export default {
cursor: pointer;
}
.el-tag {
margin-left: 10px;
}
</style>

View File

@ -150,7 +150,8 @@ export default {
name: "Trigger Mode",
manual: "Manual trigger",
schedule: "Scheduled Task",
api: "API call"
api: "API call",
case: "Case"
},
adv_search: {
title: 'Advanced Search',
@ -602,6 +603,7 @@ export default {
customize_req: "Customize req",
reference_info: "Reference info",
scenario_test: "Scenario test",
scenario_list: "Scenario List",
add_scenario: "Add scenario",
scenario_name: "Scenario name",
case_level: "Case level",
@ -1499,6 +1501,7 @@ export default {
format: "Output format",
},
auth_source: {
delete_prompt: 'This operation will delete the authentication source, do you want to continue? '
delete_prompt: 'This operation will delete the authentication source, do you want to continue? ',
title: 'Auth Source'
}
};

View File

@ -151,7 +151,8 @@ export default {
name: "触发方式",
manual: "手动触发",
schedule: "定时任务",
api: "API调用"
api: "API调用",
case: "用例触发"
},
adv_search: {
title: '高级搜索',
@ -603,6 +604,7 @@ export default {
customize_req: "自定义请求",
reference_info: "请选择接口或用例",
scenario_test: "场景",
scenario_list: "场景列表",
add_scenario: "创建场景",
scenario_name: "场景名称",
case_level: "用例等级",
@ -1502,6 +1504,7 @@ export default {
format: "输出格式",
},
auth_source: {
delete_prompt: '此操作会删除认证源,是否继续?'
delete_prompt: '此操作会删除认证源,是否继续?',
title: '认证设置'
}
};

View File

@ -151,7 +151,8 @@ export default {
name: "觸發方式",
manual: "手動觸發",
schedule: "定時任務",
api: "API調用"
api: "API調用",
case: "用例觸發"
},
adv_search: {
title: '高級搜索',
@ -602,6 +603,7 @@ export default {
customize_req: "自定義請求",
reference_info: "請選擇接口或用例",
scenario_test: "場景",
scenario_list: "場景列表",
add_scenario: "創建場景",
scenario_name: "場景名稱",
case_level: "用例等級",
@ -1500,6 +1502,7 @@ export default {
format: "輸出格式",
},
auth_source: {
delete_prompt: '此操作會刪除認證源,是否繼續? '
delete_prompt: '此操作會刪除認證源,是否繼續? ',
title: '認證設置'
}
};