This commit is contained in:
chenjianxing 2020-08-04 16:38:06 +08:00
commit 4ad4a698cd
32 changed files with 480 additions and 97 deletions

View File

@ -15,11 +15,12 @@ MeterSphere 是一站式的开源企业级持续测试平台,涵盖测试跟
![产品定位](https://metersphere.io/images/icon/ct-devops.png)
UI 展示:
> 如需进一步了解 MeterSphere 开源项目,推荐阅读 [MeterSphere 的初心和使命](https://mp.weixin.qq.com/s/DpCt3BNgBTlV3sJ5qtPmZw)
## UI 展示
![UI](https://metersphere.io/images/screenshot/ss07.png)
## 快速开始
仅需两步快速安装 MeterSphere
@ -191,6 +192,7 @@ v1.1.0 是 v1.0.0 之后的功能版本。
- 测试引擎: [JMeter](https://jmeter.apache.org/)
## 加入 MeterSphere 团队
我们正在招聘 MeterSphere 技术布道师,一起打造开源明星项目,请发简历到 metersphere@fit2cloud.com
点击查看 [岗位详情](https://www.zhipin.com/job_detail/b151c4b3d594688733Ny3dy1GFI~.html)

View File

@ -49,10 +49,6 @@ public class APITestController {
return apiTestService.getApiTestByProjectId(projectId);
}
@GetMapping("/state/get/{testId}")
public ApiTest apiState(@PathVariable String testId) {
return apiTestService.getApiTestByTestId(testId);
}
@PostMapping(value = "/schedule/update")
public void updateSchedule(@RequestBody Schedule request) {

View File

@ -128,9 +128,6 @@ public class APITestService {
return null;
}
public ApiTest getApiTestByTestId(String testId) {
return apiTestMapper.selectByPrimaryKey(testId);
}
public List<ApiTest> getApiTestByProjectId(String projectId) {
return extApiTestMapper.getApiTestByProjectId(projectId);

View File

@ -35,5 +35,7 @@ public class TestCase implements Serializable {
private Integer num;
private String otherTestName;
private static final long serialVersionUID = 1L;
}

View File

@ -1113,6 +1113,76 @@ public class TestCaseExample {
addCriterion("num not between", value1, value2, "num");
return (Criteria) this;
}
public Criteria andOtherTestNameIsNull() {
addCriterion("other_test_name is null");
return (Criteria) this;
}
public Criteria andOtherTestNameIsNotNull() {
addCriterion("other_test_name is not null");
return (Criteria) this;
}
public Criteria andOtherTestNameEqualTo(String value) {
addCriterion("other_test_name =", value, "otherTestName");
return (Criteria) this;
}
public Criteria andOtherTestNameNotEqualTo(String value) {
addCriterion("other_test_name <>", value, "otherTestName");
return (Criteria) this;
}
public Criteria andOtherTestNameGreaterThan(String value) {
addCriterion("other_test_name >", value, "otherTestName");
return (Criteria) this;
}
public Criteria andOtherTestNameGreaterThanOrEqualTo(String value) {
addCriterion("other_test_name >=", value, "otherTestName");
return (Criteria) this;
}
public Criteria andOtherTestNameLessThan(String value) {
addCriterion("other_test_name <", value, "otherTestName");
return (Criteria) this;
}
public Criteria andOtherTestNameLessThanOrEqualTo(String value) {
addCriterion("other_test_name <=", value, "otherTestName");
return (Criteria) this;
}
public Criteria andOtherTestNameLike(String value) {
addCriterion("other_test_name like", value, "otherTestName");
return (Criteria) this;
}
public Criteria andOtherTestNameNotLike(String value) {
addCriterion("other_test_name not like", value, "otherTestName");
return (Criteria) this;
}
public Criteria andOtherTestNameIn(List<String> values) {
addCriterion("other_test_name in", values, "otherTestName");
return (Criteria) this;
}
public Criteria andOtherTestNameNotIn(List<String> values) {
addCriterion("other_test_name not in", values, "otherTestName");
return (Criteria) this;
}
public Criteria andOtherTestNameBetween(String value1, String value2) {
addCriterion("other_test_name between", value1, value2, "otherTestName");
return (Criteria) this;
}
public Criteria andOtherTestNameNotBetween(String value1, String value2) {
addCriterion("other_test_name not between", value1, value2, "otherTestName");
return (Criteria) this;
}
}
public static class Criteria extends GeneratedCriteria {

View File

@ -17,6 +17,7 @@
<result column="test_id" jdbcType="VARCHAR" property="testId" />
<result column="sort" jdbcType="INTEGER" property="sort" />
<result column="num" jdbcType="INTEGER" property="num" />
<result column="other_test_name" jdbcType="VARCHAR" property="otherTestName" />
</resultMap>
<resultMap extends="BaseResultMap" id="ResultMapWithBLOBs" type="io.metersphere.base.domain.TestCaseWithBLOBs">
<result column="remark" jdbcType="LONGVARCHAR" property="remark" />
@ -82,7 +83,7 @@
</sql>
<sql id="Base_Column_List">
id, node_id, node_path, project_id, `name`, `type`, maintainer, priority, `method`,
prerequisite, create_time, update_time, test_id, sort, num
prerequisite, create_time, update_time, test_id, sort, num, other_test_name
</sql>
<sql id="Blob_Column_List">
remark, steps
@ -141,13 +142,15 @@
maintainer, priority, `method`,
prerequisite, create_time, update_time,
test_id, sort, num,
remark, steps)
other_test_name, remark, steps
)
values (#{id,jdbcType=VARCHAR}, #{nodeId,jdbcType=VARCHAR}, #{nodePath,jdbcType=VARCHAR},
#{projectId,jdbcType=VARCHAR}, #{name,jdbcType=VARCHAR}, #{type,jdbcType=VARCHAR},
#{maintainer,jdbcType=VARCHAR}, #{priority,jdbcType=VARCHAR}, #{method,jdbcType=VARCHAR},
#{prerequisite,jdbcType=VARCHAR}, #{createTime,jdbcType=BIGINT}, #{updateTime,jdbcType=BIGINT},
#{testId,jdbcType=VARCHAR}, #{sort,jdbcType=INTEGER}, #{num,jdbcType=INTEGER},
#{remark,jdbcType=LONGVARCHAR}, #{steps,jdbcType=LONGVARCHAR})
#{otherTestName,jdbcType=VARCHAR}, #{remark,jdbcType=LONGVARCHAR}, #{steps,jdbcType=LONGVARCHAR}
)
</insert>
<insert id="insertSelective" parameterType="io.metersphere.base.domain.TestCaseWithBLOBs">
insert into test_case
@ -197,6 +200,9 @@
<if test="num != null">
num,
</if>
<if test="otherTestName != null">
other_test_name,
</if>
<if test="remark != null">
remark,
</if>
@ -250,6 +256,9 @@
<if test="num != null">
#{num,jdbcType=INTEGER},
</if>
<if test="otherTestName != null">
#{otherTestName,jdbcType=VARCHAR},
</if>
<if test="remark != null">
#{remark,jdbcType=LONGVARCHAR},
</if>
@ -312,6 +321,9 @@
<if test="record.num != null">
num = #{record.num,jdbcType=INTEGER},
</if>
<if test="record.otherTestName != null">
other_test_name = #{record.otherTestName,jdbcType=VARCHAR},
</if>
<if test="record.remark != null">
remark = #{record.remark,jdbcType=LONGVARCHAR},
</if>
@ -340,6 +352,7 @@
test_id = #{record.testId,jdbcType=VARCHAR},
sort = #{record.sort,jdbcType=INTEGER},
num = #{record.num,jdbcType=INTEGER},
other_test_name = #{record.otherTestName,jdbcType=VARCHAR},
remark = #{record.remark,jdbcType=LONGVARCHAR},
steps = #{record.steps,jdbcType=LONGVARCHAR}
<if test="_parameter != null">
@ -362,7 +375,8 @@
update_time = #{record.updateTime,jdbcType=BIGINT},
test_id = #{record.testId,jdbcType=VARCHAR},
sort = #{record.sort,jdbcType=INTEGER},
num = #{record.num,jdbcType=INTEGER}
num = #{record.num,jdbcType=INTEGER},
other_test_name = #{record.otherTestName,jdbcType=VARCHAR}
<if test="_parameter != null">
<include refid="Update_By_Example_Where_Clause" />
</if>
@ -412,6 +426,9 @@
<if test="num != null">
num = #{num,jdbcType=INTEGER},
</if>
<if test="otherTestName != null">
other_test_name = #{otherTestName,jdbcType=VARCHAR},
</if>
<if test="remark != null">
remark = #{remark,jdbcType=LONGVARCHAR},
</if>
@ -437,6 +454,7 @@
test_id = #{testId,jdbcType=VARCHAR},
sort = #{sort,jdbcType=INTEGER},
num = #{num,jdbcType=INTEGER},
other_test_name = #{otherTestName,jdbcType=VARCHAR},
remark = #{remark,jdbcType=LONGVARCHAR},
steps = #{steps,jdbcType=LONGVARCHAR}
where id = #{id,jdbcType=VARCHAR}
@ -456,7 +474,8 @@
update_time = #{updateTime,jdbcType=BIGINT},
test_id = #{testId,jdbcType=VARCHAR},
sort = #{sort,jdbcType=INTEGER},
num = #{num,jdbcType=INTEGER}
num = #{num,jdbcType=INTEGER},
other_test_name = #{otherTestName,jdbcType=VARCHAR}
where id = #{id,jdbcType=VARCHAR}
</update>
</mapper>

View File

@ -225,7 +225,12 @@
</foreach>
</if>
</where>
order by update_time desc
<if test="request.orders != null and request.orders.size() > 0">
order by
<foreach collection="request.orders" separator="," item="order">
${order.name} ${order.type}
</foreach>
</if>
</select>
<select id="getMaxNumByProjectId" resultType="io.metersphere.base.domain.TestCase">

View File

@ -128,7 +128,6 @@ public class UserController {
}
@GetMapping("/list")
@RequiresRoles(value = {RoleConstants.ADMIN, RoleConstants.ORG_ADMIN, RoleConstants.TEST_MANAGER}, logical = Logical.OR)
public List<User> getUserList() {
return userService.getUserList();
}

View File

@ -1,6 +1,7 @@
package io.metersphere.track.request.testcase;
import io.metersphere.base.domain.TestCaseWithBLOBs;
import io.metersphere.controller.request.OrderRequest;
import lombok.Getter;
import lombok.Setter;
@ -10,4 +11,5 @@ import java.util.List;
@Setter
public class TestCaseBatchRequest extends TestCaseWithBLOBs {
private List<String> ids;
private List<OrderRequest> orders;
}

View File

@ -356,6 +356,12 @@ public class TestCaseService {
}
private List<TestCaseExcelData> generateTestCaseExcel(TestCaseBatchRequest request) {
List<OrderRequest> orderList = ServiceUtils.getDefaultOrder(request.getOrders());
OrderRequest order = new OrderRequest();
order.setName("sort");
order.setType("desc");
orderList.add(order);
request.setOrders(orderList);
List<TestCaseDTO> TestCaseList = extTestCaseMapper.listByTestCaseIds(request);
List<TestCaseExcelData> list = new ArrayList<>();
StringBuilder step = new StringBuilder("");
@ -466,6 +472,7 @@ public class TestCaseService {
/**
* 导入用例前检查数据库是否存在此用例
*
* @param testCaseWithBLOBs
* @return
*/

View File

@ -0,0 +1,2 @@
ALTER TABLE test_case
ADD other_test_name varchar(25);

View File

@ -0,0 +1,2 @@
ALTER TABLE load_test_report_result
MODIFY report_value LONGTEXT NULL;

View File

@ -62,17 +62,22 @@ export default {
let url = "/api/report/get/" + this.reportId;
this.$get(url, response => {
this.report = response.data || {};
if (this.isNotRunning) {
try {
this.content = JSON.parse(this.report.content);
} catch (e) {
console.log(this.report.content)
throw e;
if (response.data) {
if (this.isNotRunning) {
try {
this.content = JSON.parse(this.report.content);
} catch (e) {
console.log(this.report.content)
throw e;
}
this.getFails();
this.loading = false;
} else {
setTimeout(this.getReport, 2000)
}
this.getFails();
this.loading = false;
} else {
setTimeout(this.getReport, 2000)
this.loading = false;
this.$error(this.$t('api_report.not_exist'));
}
});
}

View File

@ -52,7 +52,7 @@
<ms-api-report-dialog :test-id="id" ref="reportDialog"/>
<ms-schedule-config :schedule="test.schedule" :save="saveCronExpression" @scheduleChange="saveSchedule" :check-open="checkScheduleEdit"/>
<ms-schedule-config :schedule="test.schedule" :is-read-only="isReadOnly" :save="saveCronExpression" @scheduleChange="saveSchedule" :check-open="checkScheduleEdit"/>
</el-row>
</el-header>
<ms-api-scenario-config :is-read-only="isReadOnly" :scenarios="test.scenarioDefinition" :project-id="test.projectId" ref="config"/>

View File

@ -9,13 +9,13 @@
<el-input v-if="!suggestions" :disabled="isReadOnly" v-model="item.name" size="small" maxlength="200"
@change="change"
:placeholder="keyText" show-word-limit/>
<el-autocomplete :maxlength="200" v-if="suggestions" v-model="item.name" size="small"
<el-autocomplete :disabled="isReadOnly" :maxlength="200" v-if="suggestions" v-model="item.name" size="small"
:fetch-suggestions="querySearch" @change="change" :placeholder="keyText"
show-word-limit/>
</el-col>
<el-col>
<el-input :disabled="isReadOnly" v-model="item.value" size="small" maxlength="2000" @change="change"
<el-input :disabled="isReadOnly" v-model="item.value" size="small" @change="change"
:placeholder="valueText" show-word-limit/>
</el-col>
<el-col class="kv-delete">

View File

@ -10,7 +10,7 @@
:placeholder="$t('api_test.variable_name')" show-word-limit/>
</el-col>
<el-col>
<el-input :disabled="isReadOnly" v-model="item.value" size="small" maxlength="2000" @change="change"
<el-input :disabled="isReadOnly" v-model="item.value" size="small" @change="change"
:placeholder="$t('api_test.value')" show-word-limit/>
</el-col>
<el-col class="kv-delete">

View File

@ -6,7 +6,7 @@
:placeholder="$t('api_test.request.extract.json_path_expression')"/>
</el-col>
<el-col>
<el-input :disabled="isReadOnly" v-model="jsonPath.expect" maxlength="2000" size="small" show-word-limit
<el-input :disabled="isReadOnly" v-model="jsonPath.expect" size="small" show-word-limit
:placeholder="$t('api_test.request.assertions.expect')"/>
</el-col>
<el-col class="assertion-btn">

View File

@ -10,7 +10,7 @@
</el-select>
</el-col>
<el-col>
<el-input :disabled="isReadOnly" v-model="regex.expression" maxlength="2000" size="small" show-word-limit
<el-input :disabled="isReadOnly" v-model="regex.expression" size="small" show-word-limit
:placeholder="$t('api_test.request.assertions.expression')"/>
</el-col>
<el-col class="assertion-btn">

View File

@ -11,7 +11,7 @@
@change="change" show-word-limit :placeholder="$t('api_test.variable_name')"/>
</el-col>
<el-col>
<el-input :disabled="isReadOnly" v-model="common.expression" maxlength="2000" size="small" show-word-limit
<el-input :disabled="isReadOnly" v-model="common.expression" size="small" show-word-limit
:placeholder="expression"/>
</el-col>
<el-col class="extract-btn">

View File

@ -1,18 +1,22 @@
import {
Element,
TestElement,
HashTree,
TestPlan,
ThreadGroup,
HeaderManager,
HTTPSamplerProxy,
HTTPSamplerArguments,
Arguments,
DubboSample,
DurationAssertion,
Element,
HashTree,
HeaderManager,
HTTPSamplerArguments,
HTTPSamplerProxy,
JSONPathAssertion,
JSONPostProcessor,
RegexExtractor,
ResponseCodeAssertion,
ResponseDataAssertion,
ResponseHeadersAssertion,
RegexExtractor, JSONPostProcessor, XPath2Extractor, DubboSample, JSONPathAssertion,
TestElement,
TestPlan,
ThreadGroup,
XPath2Extractor,
} from "./JMX";
export const uuid = function () {
@ -285,9 +289,14 @@ export class HttpRequest extends Request {
isValid: false,
info: 'api_test.request.please_configure_environment_in_scenario'
}
} else if (!this.path) {
return {
isValid: false,
info: 'api_test.request.input_path'
}
}
} else {
if (!this.url) {
if (!this.url) {
return {
isValid: false,
info: 'api_test.request.input_url'
@ -663,11 +672,12 @@ class JMXHttpRequest {
this.protocol = url.protocol.split(":")[0];
this.pathname = this.getPostQueryParameters(request, decodeURIComponent(url.pathname));
} else {
this.port = environment.port;
this.protocol = environment.protocol;
this.domain = environment.domain;
let url = new URL(environment.protocol + "://" + environment.socket);
this.path = this.getPostQueryParameters(request, decodeURIComponent(url.pathname));
if (environment) {
this.port = environment.port;
this.protocol = environment.protocol;
this.domain = environment.domain;
}
this.path = this.getPostQueryParameters(request, decodeURIComponent(request.path));
}
}
}
@ -683,8 +693,8 @@ class JMXHttpRequest {
});
for (let i = 0; i < parameters.length; i++) {
let parameter = parameters[i];
path += (encodeURIComponent(parameter.name) + '=' + encodeURIComponent(parameter.value));
if (i != parameters.length -1) {
path += (parameter.name + '=' + parameter.value);
if (i != parameters.length - 1) {
path += '&';
}
}
@ -860,9 +870,11 @@ class JMXGenerator {
addContentType(request, type) {
for (let index in request.headers) {
if (request.headers[index].name == 'Content-Type') {
request.headers.splice(index, 1);
break;
if (request.headers.hasOwnProperty(index)) {
if (request.headers[index].name === 'Content-Type') {
request.headers.splice(index, 1);
break;
}
}
}
request.headers.push(new KeyValue('Content-Type', type));

View File

@ -5,8 +5,8 @@
<i class="el-icon-date" size="small"></i>
<span class="character" @click="scheduleEdit">SCHEDULER</span>
</span>
<el-switch :disabled="!schedule.value" v-model="schedule.enable" @change="scheduleChange"/>
<ms-schedule-edit :schedule="schedule" :save="save" :custom-validate="customValidate" ref="scheduleEdit"/>
<el-switch :disabled="!schedule.value && isReadOnly" v-model="schedule.enable" @change="scheduleChange"/>
<ms-schedule-edit :is-read-only="isReadOnly" :schedule="schedule" :save="save" :custom-validate="customValidate" ref="scheduleEdit"/>
<crontab-result v-show="false" :ex="schedule.value" ref="crontabResult" @resultListChange="resultListChange"/>
</div>
<div>
@ -44,6 +44,10 @@
type: Function,
default: defaultCustomValidate
},
isReadOnly: {
type: Boolean,
default: false
}
},
methods: {
scheduleEdit() {

View File

@ -4,12 +4,12 @@
<el-form :model="form" :rules="rules" ref="from">
<el-form-item
prop="cronValue">
<el-input v-model="form.cronValue" class="inp" :placeholder="$t('schedule.please_input_cron_expression')"/>
<el-input :disabled="isReadOnly" v-model="form.cronValue" class="inp" :placeholder="$t('schedule.please_input_cron_expression')"/>
<!-- <el-button type="primary" @click="showCronDialog">{{$t('schedule.generate_expression')}}</el-button>-->
<el-button type="primary" @click="saveCron">{{$t('commons.save')}}</el-button>
<el-button :disabled="isReadOnly" type="primary" @click="saveCron">{{$t('commons.save')}}</el-button>
</el-form-item>
<el-form-item>
<el-link type="primary" @click="showCronDialog">{{$t('schedule.generate_expression')}}</el-link>
<el-link :disabled="isReadOnly" type="primary" @click="showCronDialog">{{$t('schedule.generate_expression')}}</el-link>
</el-form-item>
<crontab-result :ex="form.cronValue" ref="crontabResult" />
</el-form>
@ -38,6 +38,10 @@
type: Function,
default: defaultCustomValidate
},
isReadOnly: {
type: Boolean,
default: false
}
},
watch: {
'schedule.value'() {

View File

@ -43,12 +43,12 @@
</el-col>
</el-row>
<el-divider></el-divider>
<el-divider/>
<el-tabs v-model="active" type="border-card" :stretch="true">
<el-tab-pane :label="$t('report.test_overview')">
<!-- <ms-report-test-overview :id="reportId" :status="status"/>-->
<ms-report-test-overview :report="report"/>
<ms-report-test-overview :report="report" ref="testOverview"/>
</el-tab-pane>
<el-tab-pane :label="$t('report.test_request_statistics')">
<ms-report-request-statistics :report="report"/>
@ -63,8 +63,8 @@
</el-card>
<el-dialog :title="$t('report.test_stop_now_confirm')" :visible.sync="dialogFormVisible" width="30%">
<p v-html="$t('report.force_stop_tips')"></p>
<p v-html="$t('report.stop_tips')"></p>
<p v-html="$t('report.force_stop_tips')"/>
<p v-html="$t('report.stop_tips')"/>
<div slot="footer" class="dialog-footer">
<el-button type="danger" size="small" @click="stopTest(true)">{{$t('report.force_stop_btn')}}
</el-button>
@ -190,7 +190,7 @@
} else {
this.report.status = 'Completed';
}
})
});
this.dialogFormVisible = false;
},
rerun(testId) {
@ -201,7 +201,7 @@
}).then(() => {
this.result = this.$post('/performance/run', {id: testId, triggerMode: 'MANUAL'}, (response) => {
this.reportId = response.data;
this.$router.push({path: '/performance/report/view/' + this.reportId})
this.$router.push({path: '/performance/report/view/' + this.reportId});
// socket
this.initWebSocket();
})
@ -235,16 +235,21 @@
this.reportId = this.$route.path.split('/')[4];
this.result = this.$get("/performance/report/" + this.reportId, res => {
let data = res.data;
this.status = data.status;
this.$set(this.report, "id", this.reportId);
this.$set(this.report, "status", data.status);
this.checkReportStatus(data.status);
if (this.status === "Completed" || this.status === "Running") {
this.initReportTimeInfo();
if (data) {
this.status = data.status;
this.$set(this.report, "id", this.reportId);
this.$set(this.report, "status", data.status);
this.checkReportStatus(data.status);
if (this.status === "Completed" || this.status === "Running") {
this.initReportTimeInfo();
}
this.initBreadcrumb();
this.initWebSocket();
} else {
this.$error(this.$t('report.not_exist'))
}
})
this.initBreadcrumb();
this.initWebSocket();
});
},
beforeDestroy() {
this.websocket.close() //websocket
@ -288,6 +293,8 @@
} else {
this.clearData();
}
} else {
this.$error(this.$t('report.not_exist'));
}
});

View File

@ -66,8 +66,8 @@
<el-table-column label="Throughput">
<el-table-column
prop="transactions"
label="Transactions"
width="100"
label="Transactions/s"
width="150"
/>
</el-table-column>
@ -76,13 +76,13 @@
prop="received"
label="Received"
align="center"
width="200"
width="150"
/>
<el-table-column
prop="sent"
label="Sent"
align="center"
width="200"
width="150"
/>
</el-table-column>

View File

@ -8,7 +8,11 @@
</template>
<el-table border class="adjust-table" @row-click="link" :data="items" style="width: 100%" @sort-change="sort">
<el-table-column prop="name" :label="$t('commons.name')" width="250" show-overflow-tooltip/>
<el-table-column prop="description" :label="$t('commons.description')" show-overflow-tooltip/>
<el-table-column prop="description" :label="$t('commons.description')" show-overflow-tooltip>
<template v-slot:default="scope">
<pre>{{scope.row.description}}</pre>
</template>
</el-table-column>
<!--<el-table-column prop="workspaceName" :label="$t('project.owning_workspace')"/>-->
<el-table-column
sortable

View File

@ -0,0 +1,78 @@
<template>
<div>
<el-dialog
title="批量编辑用例"
:visible.sync="dialogVisible"
width="25%"
class="batch-edit-dialog"
:destroy-on-close="true"
@close="handleClose"
>
<el-form :model="form" label-position="right" label-width="150px" size="medium" ref="form" :rules="rules">
<el-form-item :label="$t('test_track.case.batch_update', [size])" prop="type">
<el-select v-model="form.type" style="width: 80%">
<el-option label="用例等级" value="priority"/>
<el-option label="类型" value="type"/>
<el-option label="测试方式" value="method"/>
<el-option label="维护人" value="maintainer"/>
</el-select>
</el-form-item>
<el-form-item label="更新后属性值为" prop="value">
<el-select v-model="form.value" style="width: 80%">
<el-option label="值1" value="value1"/>
<el-option label="值2" value="value2"/>
</el-select>
</el-form-item>
</el-form>
<template v-slot:footer>
<ms-dialog-footer
@cancel="dialogVisible = false"
@confirm="submit('form')"/>
</template>
</el-dialog>
</div>
</template>
<script>
import MsDialogFooter from "../../../common/components/MsDialogFooter";
export default {
name: "BatchEdit",
components: {
MsDialogFooter
},
data() {
return {
dialogVisible: false,
form: {},
size: 0,
rules: {
type: {required: true, message: "请选择属性", trigger: ['blur','change']},
value: {required: true, message: "请选择属性对应的值", trigger: ['blur','change']}
},
}
},
methods: {
submit(form) {
this.$refs[form].validate((valid) => {
if (valid) {
this.$emit("submit", this.form);
} else {
return false;
}
});
},
open() {
this.dialogVisible = true;
this.size = this.$parent.selectRows.size;
},
handleClose() {
this.form = {};
}
}
}
</script>
<style scoped>
</style>

View File

@ -0,0 +1,67 @@
<template>
<div v-if="isShow">
<el-dropdown placement="bottom" trigger="click" size="medium">
<div @click.stop="click" class="show-more-btn">
<i class="el-icon-more ms-icon-more"/>
</div>
<el-dropdown-menu slot="dropdown" class="dropdown-menu-class">
<div class="show-more-btn-title">{{$t('test_track.case.batch_handle', [size])}}</div>
<el-dropdown-item v-for="(btn,index) in buttons" :key="index" @click.native.stop="clickStop(btn)">
{{btn.name}}
</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</div>
</template>
<script>
export default {
name: "ShowMoreBtn",
data() {
return {}
},
methods: {
click() {
console.log("click");
},
clickStop(btn) {
if (btn.stop instanceof Function) {
btn.stop();
}
}
},
props: {
isShow: {
type: Boolean,
default: false
},
buttons: Array,
row: Object,
size: Number
}
}
</script>
<style scoped>
.ms-icon-more {
transform: rotate(90deg);
}
.show-more-btn {
width: 20px;
height: 25px;
line-height: 25px;
background-color: #FFF;
}
.show-more-btn-title {
color: #696969;
background-color: #C0C0C0;
padding: 5px;
}
.dropdown-menu-class {
padding: 1px 0;
text-align: center;
}
</style>

View File

@ -90,9 +90,9 @@
</el-row>
<el-row v-if="form.method && form.method == 'auto'">
<el-col :span="10" :offset="1">
<el-col :span="9" :offset="1">
<el-form-item :label="$t('test_track.case.relate_test')" :label-width="formLabelWidth" prop="testId">
<el-select filterable :disabled="readOnly" v-model="form.testId"
<el-select filterable :disabled="readOnly" v-model="form.testId"
:placeholder="$t('test_track.case.input_type')">
<el-option
v-for="item in testOptions"
@ -103,8 +103,12 @@
</el-select>
</el-form-item>
</el-col>
<el-col :span="9" :offset="1" v-if="form.testId=='other'">
<el-form-item :label="$t('test_track.case.test_name')" :label-width="formLabelWidth" prop="testId">
<el-input v-model="form.otherTestName" :placeholder="$t('test_track.case.input_test_case')" ></el-input>
</el-form-item>
</el-col>
</el-row>
<el-row style="margin-top: 15px;">
<el-col :offset="2">{{$t('test_track.case.prerequisite')}}:</el-col>
</el-row>
@ -237,6 +241,7 @@
method: '',
prerequisite: '',
testId: '',
otherTestName:'',
steps: [{
num: 1,
desc: '',
@ -446,6 +451,7 @@
if (this.currentProject && this.form.type != '' && this.form.type != 'functional') {
this.result = this.$get('/' + this.form.type + '/list/' + this.currentProject.id, response => {
this.testOptions = response.data;
this.testOptions.unshift({id:'other',name:this.$t('test_track.case.other')})
});
}
},
@ -491,6 +497,7 @@
this.form.prerequisite = '';
this.form.remark = '';
this.form.testId = '';
this.form.testName='';
this.form.steps = [{
num: 1,
desc: '',

View File

@ -39,6 +39,11 @@
class="test-content adjust-table">
<el-table-column
type="selection"/>
<el-table-column width="40" :resizable="false" align="center">
<template v-slot:default="scope">
<show-more-btn :is-show="scope.row.showMore" :buttons="buttons" :size="selectRows.size"/>
</template>
</el-table-column>
<el-table-column
prop="num"
sortable="custom"
@ -96,7 +101,7 @@
</template>
</el-table-column>
<el-table-column
:label="$t('commons.operating')">
:label="$t('commons.operating')" min-width="100">
<template v-slot:default="scope">
<ms-table-operator :is-tester-permission="true" @editClick="handleEdit(scope.row)"
@deleteClick="handleDelete(scope.row)">
@ -114,6 +119,8 @@
:total="total"/>
</el-card>
<batch-edit ref="batchEdit"/>
</div>
</template>
@ -133,6 +140,8 @@
import MsTableButton from "../../../common/components/MsTableButton";
import {_filter, _sort} from "../../../../../common/js/utils";
import {TEST_CASE_CONFIGS} from "../../../common/components/search/search-components";
import ShowMoreBtn from "./ShowMoreBtn";
import BatchEdit from "./BatchEdit";
export default {
name: "TestCaseList",
@ -143,7 +152,14 @@
MethodTableItem,
TypeTableItem,
PriorityTableItem,
MsCreateBox, TestCaseImport, TestCaseExport, MsTablePagination, NodeBreadcrumb, MsTableHeader
MsCreateBox,
TestCaseImport,
TestCaseExport,
MsTablePagination,
NodeBreadcrumb,
MsTableHeader,
ShowMoreBtn,
BatchEdit
},
data() {
return {
@ -156,7 +172,8 @@
currentPage: 1,
pageSize: 10,
total: 0,
selectIds: new Set(),
// selectIds: new Set(),
selectRows: new Set(),
priorityFilters: [
{text: 'P0', value: 'P0'},
{text: 'P1', value: 'P1'},
@ -171,6 +188,16 @@
{text: this.$t('commons.functional'), value: 'functional'},
{text: this.$t('commons.performance'), value: 'performance'},
{text: this.$t('commons.api'), value: 'api'}
],
showMore: false,
buttons: [
{
name: '批量编辑用例', stop: this.handleClickStop
}, {
name: '批量移动用例', stop: this.handleClickStop
}, {
name: '批量删除用例', stop: this.handleClickStop
}
]
}
},
@ -212,7 +239,8 @@
let data = response.data;
this.total = data.itemCount;
this.tableData = data.listObject;
this.selectIds.clear();
// this.selectIds.clear();
this.selectRows.clear();
});
}
},
@ -246,8 +274,15 @@
confirmButtonText: this.$t('commons.confirm'),
callback: (action) => {
if (action === 'confirm') {
this.$post('/test/case/batch/delete', {ids: [...this.selectIds]}, () => {
this.selectIds.clear();
// this.$post('/test/case/batch/delete', {ids: [...this.selectIds]}, () => {
// this.selectIds.clear();
// this.$emit("refresh");
// this.$success(this.$t('commons.delete_success'));
// });
let ids = Array.from(this.selectRows).map(row => row.id);
this.$post('/test/case/batch/delete', {ids: ids}, () => {
// this.selectIds.clear();
this.selectRows.clear();
this.$emit("refresh");
this.$success(this.$t('commons.delete_success'));
});
@ -264,7 +299,8 @@
},
refresh() {
this.condition = {components: TEST_CASE_CONFIGS};
this.selectIds.clear();
// this.selectIds.clear();
this.selectRows.clear();
this.$emit('refresh');
},
showDetail(row, event, column) {
@ -272,29 +308,55 @@
},
handleSelectAll(selection) {
if (selection.length > 0) {
this.tableData.forEach(item => {
this.selectIds.add(item.id);
});
if (selection.length === 1) {
this.selectRows.add(selection[0]);
} else {
this.tableData.forEach(item => {
this.$set(item, "showMore", true);
this.selectRows.add(item);
});
}
} else {
this.selectIds.clear();
this.selectRows.clear();
this.tableData.forEach(row => {
this.$set(row, "showMore", false);
})
}
},
handleSelectionChange(selection, row) {
if (this.selectIds.has(row.id)) {
this.selectIds.delete(row.id);
// if (this.selectIds.has(row.id)) {
// this.selectIds.delete(row.id);
// } else {
// this.selectIds.add(row.id);
// }
if (this.selectRows.has(row)) {
this.$set(row, "showMore", false);
this.selectRows.delete(row);
} else {
this.selectIds.add(row.id);
this.selectRows.add(row);
}
// todo
if (this.selectRows.size > 1) {
Array.from(this.selectRows).forEach(row => {
this.$set(row, "showMore", true);
})
} else if (this.selectRows.size === 1) {
let arr = Array.from(this.selectRows);
this.$set(arr[0], "showMore", false);
}
},
importTestCase() {
this.$refs.testCaseImport.open();
},
exportTestCase() {
let ids = Array.from(this.selectRows).map(row => row.id);
let config = {
url: '/test/case/export/testcase',
method: 'post',
responseType: 'blob',
data: {ids: [...this.selectIds]}
// data: {ids: [...this.selectIds]}
data: {ids: ids}
};
this.result = this.$request(config).then(response => {
const filename = this.$t('test_track.case.test_case') + ".xlsx";
@ -311,12 +373,18 @@
});
},
handleBatch(type) {
if (this.selectIds.size < 1) {
// if (this.selectIds.size < 1) {
// this.$warning(this.$t('test_track.plan_view.select_manipulate'));
// return;
// }
if (this.selectRows.size < 1) {
this.$warning(this.$t('test_track.plan_view.select_manipulate'));
return;
}
if (type === 'move') {
this.$emit('moveToNode', this.selectIds);
let ids = Array.from(this.selectRows).map(row => row.id);
// this.$emit('moveToNode', this.selectIds);
this.$emit('moveToNode', ids);
} else if (type === 'delete') {
this.handleDeleteBatch();
} else {
@ -334,6 +402,9 @@
}
_sort(column, this.condition);
this.initTableData();
},
handleClickStop() {
this.$refs.batchEdit.open();
}
}
}

View File

@ -266,6 +266,7 @@ export default {
stop_tips: 'A <strong>Graceful shutdown</strong> will archive the JTL files and then stop the servers.',
force_stop_btn: 'Terminating',
stop_btn: 'Graceful shutdown',
not_exist: "Test report does not exist",
},
load_test: {
operating: 'Operating',
@ -476,6 +477,7 @@ export default {
detail: "Report detail",
delete: "Delete report",
running: "The test is running",
not_exist: "Test report does not exist",
},
test_track: {
test_track: "Track",
@ -491,6 +493,9 @@ export default {
execution_result: ": Please select the execution result",
actual_result: ": The actual result is empty",
case: {
input_test_case:'Please enter the associated case name',
test_name:'TestName',
other:'--Other--',
test_case: "Case",
move: "Move case",
case_list: "Test case list",
@ -527,6 +532,8 @@ export default {
create_module_first: "Please create module first",
relate_test: "Relate test",
relate_test_not_find: 'The associated test does not exist, please check the test case',
batch_handle: 'Batch processing (select {0} item)',
batch_update: 'Update the attributes of {0} cases',
import: {
import: "Import test case",
case_import: "Import test case",

View File

@ -264,6 +264,7 @@ export default {
stop_tips: '<strong>停止</strong>测试会结束当前测试并保留报告数据',
force_stop_btn: '强制停止',
stop_btn: '停止',
not_exist: "测试报告不存在",
},
load_test: {
operating: '操作',
@ -475,6 +476,7 @@ export default {
detail: "报告详情",
delete: "删除报告",
running: "测试执行中",
not_exist: "测试报告不存在",
},
test_track: {
test_track: "测试跟踪",
@ -491,6 +493,9 @@ export default {
actual_result: ": 实际结果为空",
case: {
input_test_case:'请输入关联用例名称',
test_name:'测试名称',
other:"--其他--",
test_case: "测试用例",
move: "移动用例",
case_list: "用例列表",
@ -527,6 +532,8 @@ export default {
create_module_first: "请先新建模块",
relate_test: "关联测试",
relate_test_not_find: '关联的测试不存在,请检查用例',
batch_handle: '批量处理 (选中{0}项)',
batch_update: '更新{0}个用例的属性',
import: {
import: "导入用例",
case_import: "导入测试用例",

View File

@ -264,6 +264,7 @@ export default {
stop_tips: '<strong>停止</strong>測試會結束當前測試並保留報告數據',
force_stop_btn: '強制停止',
stop_btn: '停止',
not_exist: "測試報告不存在",
},
load_test: {
operating: '操作',
@ -475,6 +476,7 @@ export default {
detail: "報告詳情",
delete: "刪除報告",
running: "測試執行中",
not_exist: "測試報告不存在",
},
test_track: {
test_track: "測試跟踪",
@ -490,6 +492,9 @@ export default {
execution_result: ": 請選擇執行結果",
actual_result: ": 實際結果為空",
case: {
input_test_case:'請輸入關聯用例名稱',
test_name:'測試名稱',
other:'--其他--',
test_case: "測試用例",
move: "移動用例",
case_list: "用例列表",
@ -526,6 +531,8 @@ export default {
create_module_first: "請先新建模塊",
relate_test: "關聯測試",
relate_test_not_find: '關聯的測試不存在,請檢查用例',
batch_handle: '批量處理 (選中{0}項)',
batch_update: '更新{0}個用例的屬性',
import: {
import: "導入用例",
case_import: "導入測試用例",