效果图及简单说明
左边选择用例,右侧就显示该用例的详细信息。
使用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.titlecaseData.interface = caseObj.interfacecaseData.setup_script = caseObj.setup_scriptcaseData.file = caseObj.filecaseData.teardown_script = caseObj.teardown_scriptcaseData.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() {// 显示loadingscrennLoading.value = true。。。// 隐藏loadingscrennLoading.value = false
}
总结
复用之前的组件以减少开发量。所以一个经常会用到的组件,最好单独抽出来。以便后续使用。
然后用了cursor,没次数了。又尝试了一下Trae,也是挺不错的。遇到问题向Ai求助真是泰裤辣!