效果图及简单说明
左边选择用例,右侧就显示该用例的详细信息。
使用el-collapse折叠组件,将请求到的用例详情数据展示到页面中。
所有数据内容,绑定到caseData中
// 页面绑定的用例编辑数据
const caseData = reactive({
title: "",
interface: {
method: "get",
url: ""
},
headers: '',
request: {
json: '{}',
data: '{}',
params: '{}'
},
file: [],
setup_script: '',
teardown_script: ''
})
页面内容实现
1. 请求API信息
<el-collapse-item name="1">
<template #title>
<img src="@/assets/icons/icon-api-a.png" width="20">
<b>API信息</b>
</template>
<el-input v-model="caseData.interface.url" readonly>
<template #prepend>
<el-select v-model="caseData.interface.method" placeholder="选择请求方法" style="width: 115px">
<el-option label="GET" value="get"/>
<el-option label="POST" value="post"/>
<el-option label="PUT" value="put"/>
<el-option label="PATCH" value="patch"/>
<el-option label="DELETE" value="delete"/>
</el-select>
</template>
</el-input>
</el-collapse-item>
一个下拉选择(请求方法) + 一个只读的input框
2.用例title
<el-collapse-item name="2">
<template #title>
<img src="@/assets/icons/case.png" width="20">
<b>用例名称</b>
</template>
<el-input v-model="caseData.title">
<template #prepend>
<span>用例名称</span>
</template>
</el-input>
</el-collapse-item>
一个带前缀的input输入框
3. 前置脚本
前置脚本是一个python代码编辑器
<div class="code">
<Editor v-model="caseData.setup_script" lang="python" height='300px' class="scriptEdit"></Editor>
</div>
但是,可以准备一些预制脚本,方便使用。
可以创建编辑一些脚本保存在数据库,然后读取。这里没有选择那么复杂的内容,直接写死在了前端页面点击添加。
<div class='script_code'>
<div class="code">
<Editor v-model="caseData.setup_script" lang="python" height='300px' class="scriptEdit"></Editor>
</div>
<div class='mod'>
<div class="add_code">
<el-button @click='addSetupScript("func")' plain size="small">调用全局工具函数</el-button>
</div>
<div class="add_code">
<el-button @click='addSetupScript("global")' plain size="small">设置全局变量</el-button>
</div>
<div class="add_code">
<el-button @click='addSetupScript("env")' plain size="small">设置局部变量</el-button>
</div>
<div class="add_code">
<el-button @click='addSetupScript("sql")' plain size="small">执行sql查询</el-button>
</div>
</div>
</div>
// 生成前置脚本
function addSetupScript(item) {
if (item === "func") {
caseData.setup_script += '# 调用全局工具函数random_mobile随机生成一个手机号码\nmobile = global_func.random_mobile()\n'
} else if (item === "global") {
caseData.setup_script += '# 设置局部变量\ntest.save_global_variable("变量名","变量值")\n'
} else if (item === "env") {
caseData.setup_script += '# 设置局部变量\ntest.save_env_variable("变量名","变量值")\n'
} else if (item === "sql") {
caseData.setup_script +=
'# ----执行sql语句(需要在环境中配置数据库连接信息)----\n # db.连接名.execute_all(sql语句) \nsql = "SELECT count(*) as count FROM futureloan.member"\nres = db.hwyun.execute_all(sql)\n'
}
}
4. 请求头
同样代码编辑器
<el-collapse-item name="4">
<template #title>
<img src="@/assets/icons/keyhole.png" width="20">
<b>请求头</b>
</template>
<Editor lang="json" v-model="caseData.headers"></Editor>
</el-collapse-item>
5.查询参数
<el-collapse-item name="5">
<template #title>
<img src="@/assets/icons/API_api.png" width="20">
<b>查询参数</b>
</template>
<Editor lang="json" v-model="caseData.request.params"></Editor>
</el-collapse-item>
6. 请求体(Get请求不可点击)
get请求禁用了,然后如果是文件类型数据,复用之前的FormData组件
<el-collapse-item name="6" :disabled="caseData.interface.method=='get'">
<template #title>
<img src="@/assets/icons/body.png" width="20">
<b>请求体</b>
</template>
<el-radio-group v-model="bodyType">
<el-radio-button label="json">Json</el-radio-button>
<el-radio-button label="data">X-www-form-urlencoded</el-radio-button>
<el-radio-button label="form-data">Form-data</el-radio-button>
</el-radio-group>
<!-- json参数 -->
<div v-if='bodyType==="json"'>
<Editor lang="json" v-model="caseData.request.json"></Editor>
</div>
<div v-else-if='bodyType==="data"'>
<Editor lang="json" v-model="caseData.request.data"></Editor>
</div>
<div v-else>
<FromData v-model="file"></FromData>
</div>
</el-collapse-item>
7.后置脚本(同前置脚本)
<el-collapse-item name="7">
<template #title>
<img src="@/assets/icons/instruction.png" width="20">
<b>后置断言脚本</b>
</template>
<div class='script_code'>
<div class="code">
<Editor v-model="caseData.teardown_script" lang="python" height='300px' ></Editor>
</div>
<div class='mod'>
<el-scrollbar height="300px">
<div class="add_code">
<el-button @click="addTearDownCodeMod('getBody')" plain size="small">获取响应体</el-button>
</div>
<div class="add_code">
<el-button @click="addTearDownCodeMod('http')" plain size="small">HTTP状态码断言</el-button>
</div>
<div class="add_code">
<el-button @click="addTearDownCodeMod('contain')" plain size="small">断言包含</el-button>
<!-- 省略 .... -->
</div>
</el-scrollbar>
</div>
</div>
</el-collapse-item>
8.悬浮操作按钮
与测试环境页面一样,使用 Affix 固钉 组件
https://element-plus.org/zh-CN/component/affix.html#affix-%E5%9B%BA%E9%92%89
<el-affix :offset="40" position="bottom">
<div class="btns">
<el-button @click='runCase' type="primary" plain size="small" icon='Promotion'>运行</el-button>
<el-button @click='copyCase' type="primary" plain size="small" icon='DocumentCopy'>复制</el-button>
<el-button @click='saveCase' type="primary" plain size="small" icon='FolderChecked'>保存</el-button>
<el-button @click='clickDelete' type="danger" plain size="small" icon='Delete'>删除</el-button>
</div>
</el-affix>
页面功能实现
1. 点击获取接口用例详情
在之前一节,左侧显示了用例列表。并且点击某个用例,实现了点击方法@click='selectCase(_case.id)'
,也就是function selectCase(id) { activeCase.value = id }
将activeCase 赋值了。
而在右侧,显示用例详情页,把activeCase值传给了CaseEditor这个组件
<div class="card right_box">
<CaseEditor :case_id='activeCase'></CaseEditor>
</div>
然后在CaseEditor组件定义props接收传递的值,然后监听这个值变化,就请求获取用例详情。
const props = defineProps({
case_id: ""
})
// 侦听case_id的变化
watch(() => props.case_id, (val) => {
if (val !== '') {
getCaseInfo(val)
}
})
if (props.case_id != undefined) {
getCaseInfo(props.case_id)
}
// 调用获取详情的接口
async function getCaseInfo(id) {
// console.log("获取详情:"+id)
const response = await http.pro.getCaseInfoAPi(id)
if (response.status === 200) {
// 保存用例对象
caseObj = response.data
// 把用例数据绑定到编辑页面
caseData.title = caseObj.title
caseData.interface = caseObj.interface
caseData.setup_script = caseObj.setup_script
caseData.file = caseObj.file
caseData.teardown_script = caseObj.teardown_script
caseData.headers = JSON.stringify(caseObj.headers, 0, 4)
caseData.request.json = JSON.stringify(caseObj.request.json || {}, 0, 4)
caseData.request.data = JSON.stringify(caseObj.request.data || {}, 0, 4)
caseData.request.params = JSON.stringify(caseObj.request.params || {}, 0, 4)
file.value = caseObj.file
}
// get请求默认不展示请求体,其他默认自动展开
if (caseData.interface.method == 'get') {
activeNames.value = ['1', '2']
} else {
activeNames.value = ['1', '2', '6']
}
// console.log(activeNames)
// 根据请求体信息,默认选中对应的拦
if (caseData.request.json != '{}') {
bodyType.value = 'json'
} else if (caseData.request.data != '{}') {
bodyType.value = 'data'
} else if (caseData.file.length != 0) {
bodyType.value = 'form-data'
}
}
2.保存用例
编辑好数据后,都是双向绑定的,直接组成参数调用api就行了。
// 保存用例
async function saveCase() {
if (caseData.headers == '') {
caseData.headers = '{}'
} else if (caseData.request.params == '') {
caseData.request.params = '{}'
} else if (caseData.request.json == '') {
caseData.request.json = '{}'
} else if (caseData.request.data == '') {
caseData.request.data = '{}'
}
// 准备参数
const params = {
title: caseData.title,
headers: JSON.parse(caseData.headers),
request: {
params: JSON.parse(caseData.request.params),
},
setup_script: caseData.setup_script,
teardown_script: caseData.teardown_script,
}
if (caseData.interface.method != 'get') {
// console.log('!get')
if (bodyType.value === 'json') {
params.request.json = JSON.parse(caseData.request.json)
} else if (bodyType.value === 'data') {
params.request.data = JSON.parse(caseData.request.data)
} else {
params.file = file.value
}
}
// 调用修改用例的接口
const response = await api.updateCaseApi(props.case_id, params)
if (response.status === 200) {
ElNotification({
title: '保存成功',
type: 'success',
})
// 刷新页面数据
pstore.getInterFaceList()
}
}
3. 删除用例
和之前的删除方法没有什么区别,有个二次确认弹窗。然后删除过后,把页面数据清空。
// 删除测试用例的方法
function clickDelete() {
ElMessageBox.confirm(
'删除操作不可恢复,请确认是否要删除该测试用例?',
'提示', {
confirmButtonText: '确认',
cancelButtonText: '取消',
type: 'warning',
}
).then(async () => {
// 调用后端接口进行删除
const response = await http.pro.deleteCaseApi(props.case_id)
if (response.status === 204) {
ElNotification({
title: '删除成功',
type: 'success',
})
// 刷新页面数据
pstore.getInterFaceList()
resetData()
}
}).catch(() => {
ElNotification({
type: 'info',
title: '已取消删除操作',
})
})
}
function resetData() {
// 清空页面编辑的数据
caseData.title = ''
caseData.interface = {
method: "get",
url: ""
}
caseData.setup_script = ''
caseData.file = []
caseData.teardown_script = ''
caseData.headers = ''
caseData.request.json = "{}"
caseData.request.data = "{}"
caseData.request.params = "{}"
caseObj = {}
}
4. 复制用例
与新增测试环境一样,在名字后面+copy,其他内容都保持不变
// 复制用例
async function copyCase() {
const response = await http.pro.createCaseApi({
title: caseObj.title + '-COPY',
interface: caseObj.interface.id
})
if (response.status === 201) {
ElNotification({
title: '复制成功',
type: 'success',
})
// 刷新页面数据
pstore.getInterFaceList()
}
}
5. 运行用例
运行结果用抽屉组件展示,展示内容就是之前调试运行时的Result组件
<!-- 测试用例运行的结果 -->
<el-drawer v-model="isShowDrawer" size="50%">
<template #header>
<b>运行结果</b>
</template>
<template #default>
<Result :result='responseData'></Result>
</template>
</el-drawer>
// 保存用例运行的结果
let responseData = ref({})
// 是否显示结果的窗口
let isShowDrawer = ref(false)
async function runCase() {
// 准备参数
const params = {
env: pstore.env,
cases: {
title: caseData.title,
interface: caseData.interface,
headers: JSON.parse(caseData.headers),
request: {
params: JSON.parse(caseData.request.params),
},
setup_script: caseData.setup_script,
teardown_script: caseData.teardown_script,
}
}
// console.log(params.cases.interface.method)
if (params.cases.interface.method != 'get') {
if (bodyType.value === 'json') {
params.cases.request.json = JSON.parse(caseData.request.json)
} else if (bodyType.value === 'data') {
params.cases.request.data = JSON.parse(caseData.request.data)
} else {
params.cases.file = caseData.file
}
}
// console.log(params.cases.request)
// 调用运行用例的接口
const response = await http.run.runInterFaceCaseApi(params)
if (response.status === 200) {
// console.log('运行成功')
responseData.value = response.data
// 展示执行结果
isShowDrawer.value = true
}
}
到此页面功能实现完成。
小优化,增加loading
刚才运行时,受网络问题,一直没结果。所以,就增加一个loading效果
同样使用了element的loading组件
https://element-plus.org/zh-CN/component/loading.html#%E8%87%AA%E5%AE%9A%E4%B9%89%E5%8A%A0%E8%BD%BD%E4%B8%AD%E7%BB%84%E4%BB%B6%E5%86%85%E5%AE%B9
给页面绑定loading相关数据
<el-collapse v-model="activeNames" v-loading="scrennLoading" element-loading-text="运行中..." element-loading-background="rgba(122, 122, 122, 0.8)">
然后运行时,显示loading,运行结束再隐藏loading
const scrennLoading = ref(false)
async function runCase() {
// 显示loading
scrennLoading.value = true
。。。
// 隐藏loading
scrennLoading.value = false
}
总结
复用之前的组件以减少开发量。所以一个经常会用到的组件,最好单独抽出来。以便后续使用。
然后用了cursor,没次数了。又尝试了一下Trae,也是挺不错的。遇到问题向Ai求助真是泰裤辣!