课堂PPT
https://material.hogwarts.ceshiren.com/resource/test_platform_fronted/ppt/测试平台训练营前端实战.html
源码地址
训练营目标
- 项目初始化
- api接口封装
- 布局实现
- 路由封装
- 测试用例组件实现
- 测试计划组件实现
- 测试记录组件实现
项目初始化1-详情
- 执行vue ui -启动可视化项目管理界面
-
http://localhost:8000/project/create
-进入创建项目详情界面 - 输入项目名称后,点击下一步
项目初始化2-预设
- 进入预设界面
- 默认第一个是vue3,选择手动配置项目
- 点击下一步
项目初始化3-功能
- 进入功能界面
- 只勾选babel和router,其他全部取消
- 点击下一步
项目初始化4-配置
- 进入配置界面
- 切换vue2.x,默认是vue3.x
- 点击创建项目
项目初始化5-插件
- 点击左侧插件,进入插件页面
- 右上角输入vuetify,搜索后选择第一个进行安装
- 安装完成界面会展示该插件
项目初始化6-依赖
- 点击左侧依赖,进入依赖页面
- 右上角输入axios,搜索后选择第一个进行安装
- 安装完成界面会展示该依赖
启动项目
- 点击左侧任务,进入任务页面
- 选择serve
- 点击运行
- 点击启动app,即可打开页面
默认代码改造
- 目录结构
api封装-http.js
// 完成http请求的基本配置
// 导入axios
import axios from "axios"
// 创建axios实例
var instance = axios.create({
// 请求体
headers: {
'Content-Type': 'application/json'
},
// 超时时间
timeout: 2500,
// 基础url,后端的接口服务地址
// baseURL: 'https://hogwarts-platform-backend.hogwarts.ceshiren.com'
baseURL: 'http://127.0.0.1:5001'
})
// 添加请求拦截器,在请求头中加入token
instance.interceptors.request.use(
config => {
const token = localStorage.getItem('token')
console.log('token', token)
if (token) {
// 设置请求头中的 Authorization 字段
config.headers.Authorization = `Bearer ${token}`;
console.log('token', config.headers.Authorization)
}
return config
},
error => {
return Promise.reject(error)
})
export default instance
api封装-testcase.js
// 测试用例
import instance from './http'
const testcase = {
// 查询
getTestcase(params) {
return instance({
method: 'GET',
url: '/testcase',
params: params
})
},
// 新增
addTestcase(data) {
return instance({
method: 'POST',
url: '/testcase',
data: data
})
},
// 修改
updateTestcase(data) {
return instance({
method: 'PUT',
url: '/testcase',
data: data
})
},
// 删除
deleteTestcase(data) {
return instance({
method: 'DELETE',
url: '/testcase',
data: data
})
}
}
export default testcase
api封装-plan.js
// 测试计划
import instance from './http'
const plan = {
// 查询
getPlan(params) {
return instance({
method: 'GET',
url: '/plan',
params: params
})
},
// 新增
addPlan(data) {
return instance({
method: 'POST',
url: '/plan',
data: data
})
},
// 删除
deletePlan(data) {
return instance({
method: 'DELETE',
url: '/plan',
data: data
})
}
}
export default plan
api封装-record.js
// 测试记录
import instance from './http'
const record = {
// 查询
getRecord(params) {
return instance({
method: 'GET',
url: '/record',
params: params
})
},
// 新增
addRecord(data) {
return instance({
method: 'POST',
url: '/record',
data: data
})
},
}
export default record
api封装-api.js
// 所有接口的入口,相当于目录
import testcase from "./testcase"
import plan from "./plan"
import record from "./record"
import user from "./user"
const api = {
testcase,
plan,
record,
user,
}
export default api
启动入口-main.js
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import vuetify from './plugins/vuetify'
import api from './api/api'
Vue.prototype.$api = api
Vue.config.productionTip = false
new Vue({
router,
vuetify,
render: h => h(App)
}).$mount('#app')
最外层组件-App.vue
<template>
<router-view></router-view>
</template>
<script>
export default {
name: 'App',
data: () => ({
//
}),
};
</script>
布局组件-Index.vue
<template>
<v-app id="inspire">
<!-- 左侧导航栏 -->
<v-navigation-drawer v-model="drawer" app>
<!-- 左侧logo区域 -->
<v-list-item>
<v-list-item-content>
<v-list-item-title class="text-h6">
测试平台
</v-list-item-title>
<v-list-item-subtitle>
霍格沃兹
</v-list-item-subtitle>
</v-list-item-content>
</v-list-item>
<v-divider></v-divider>
<!-- 导航抽屉 -->
<v-list dense nav>
<v-list-item v-for="item in items" :key="item.title" link :href="item.link">
<!-- 菜单icon -->
<v-list-item-icon>
<v-icon>{{ item.icon }}</v-icon>
</v-list-item-icon>
<!-- 菜单内容 -->
<v-list-item-content>
<v-list-item-title>{{ item.title }}</v-list-item-title>
</v-list-item-content>
</v-list-item>
</v-list>
</v-navigation-drawer>
<!-- 顶部栏 -->
<v-app-bar app>
<v-app-bar-nav-icon @click="drawer = !drawer"></v-app-bar-nav-icon>
<v-toolbar-title>测试平台</v-toolbar-title>
<!-- 占满剩余空间 -->
<v-spacer></v-spacer>
<v-btn color="primary" @click="layout">退出</v-btn>
</v-app-bar>
<v-main>
<!-- 主题内容区域 -->
<router-view></router-view>
</v-main>
</v-app>
</template>
<script>
export default {
data() {
return {
drawer: null,
items: [
{ title: '测试用例', icon: 'mdi-book', link: '#/index/testcase' },
{ title: '测试计划', icon: 'mdi-target', link: '#/index/plan' },
{ title: '测试记录', icon: 'mdi-format-list-numbered-rtl', link: '#/index/record' },
],
}
},
methods: {
// 退出
layout() {
localStorage.removeItem('token')
this.$router.push('/user')
}
}
}
</script>
路由封装-index.js
import Vue from 'vue'
import VueRouter from 'vue-router'
Vue.use(VueRouter)
const routes = [
{
path: '/',
redirect: '/index'
},
{
path: '/index',
name: 'Index',
component: () => import(/* webpackChunkName: "index" */ '../views/Index.vue'),
children: [
{
path: 'testcase',
name: 'Testcase',
component: () => import(/* webpackChunkName: "index" */ '../views/Testcase.vue'),
},
{
path: 'plan',
name: 'Plan',
component: () => import(/* webpackChunkName: "index" */ '../views/Plan.vue'),
},
{
path: 'record',
name: 'Record',
component: () => import(/* webpackChunkName: "index" */ '../views/Record.vue'),
}
]
},
{
path: '/user',
name: 'User',
component: () => import(/* webpackChunkName: "index" */ '../views/User.vue'),
},
]
const router = new VueRouter({
routes
})
// 路由守卫
router.beforeEach((to, from, next) => {
// 判断是否要去登录页面
if (to.path == '/user') {
// 放行
next()
} else {
// 去其他路由页面,判断是否有登录的token
const token = localStorage.getItem('token')
// 如果没有token(未登录)
if (!token) {
// 强制进入登录页面
next('/user')
} else {
// 放行
next()
}
}
})
export default router
测试用例组件-TestCase.vue
<template>
<!-- 表格数据 -->
<v-data-table :headers="headers" :items="desserts" class="elevation-1" v-model="selected" show-select>
<!-- 顶部插槽 -->
<template v-slot:top>
<!-- 顶部导航渔区 -->
<v-toolbar flat>
<!-- 标题 -->
<v-toolbar-title>测试用例</v-toolbar-title>
<!-- 垂直分割线 -->
<v-divider class="mx-4" inset vertical></v-divider>
<!-- 占满剩余空间 -->
<v-spacer></v-spacer>
<!-- 创建计划弹框 -->
<v-dialog v-model="dialogPlan" max-width="500px">
<!-- 右侧按钮区域 -->
<template v-slot:activator="{ on, attrs }">
<v-btn color="green" dark class="mb-2 mr-2" v-bind="attrs" v-on="on">
创建计划
</v-btn>
</template>
<!-- 创建计划卡片 -->
<v-card>
<!-- 卡片标题 -->
<v-card-title>
<span class="text-h5">创建计划</span>
</v-card-title>
<!-- 卡片内容 -->
<v-card-text>
<v-container>
<!-- 表单输入框 -->
<v-text-field v-model="planName" label="测试计划名称"></v-text-field>
</v-container>
</v-card-text>
<!-- 弹框操作 -->
<v-card-actions>
<v-spacer></v-spacer>
<v-btn color="blue darken-1" text @click="savePlan">
保存
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<!-- 新增修改弹框 -->
<v-dialog v-model="dialog" max-width="500px">
<!-- 右侧按钮区域 -->
<template v-slot:activator="{ on, attrs }">
<v-btn color="primary" dark class="mb-2" v-bind="attrs" v-on="on">
创建用例
</v-btn>
</template>
<!-- 新增修改卡片 -->
<v-card>
<!-- 卡片标题 -->
<v-card-title>
<span class="text-h5">{{ formTitle }}</span>
</v-card-title>
<!-- 卡片内容 -->
<v-card-text>
<v-container>
<!-- 表单输入框 -->
<v-text-field disabled v-if="editedItem.id" v-model="editedItem.id"
label="用例id"></v-text-field>
<v-text-field v-model="editedItem.name" label="用例名称"></v-text-field>
<v-text-field v-model="editedItem.step" label="用例步骤"></v-text-field>
<v-text-field v-model="editedItem.method" label="用例方法"></v-text-field>
<v-text-field v-model="editedItem.remark" label="备注"></v-text-field>
</v-container>
</v-card-text>
<!-- 弹框操作 -->
<v-card-actions>
<v-spacer></v-spacer>
<v-btn color="blue darken-1" text @click="close">
取消
</v-btn>
<v-btn color="blue darken-1" text @click="save">
保存
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<!-- 删除弹框 -->
<v-dialog v-model="dialogDelete" max-width="500px">
<v-card>
<v-card-title class="text-h5">确定删除用例?</v-card-title>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn color="blue darken-1" text @click="closeDelete">取消</v-btn>
<v-btn color="blue darken-1" text @click="deleteItemConfirm">确定</v-btn>
<v-spacer></v-spacer>
</v-card-actions>
</v-card>
</v-dialog>
</v-toolbar>
</template>
<!-- 操作列插槽 -->
<template v-slot:item.actions="{ item }">
<!-- 修改 -->
<v-icon small class="mr-2" @click="editItem(item)">
mdi-pencil
</v-icon>
<!-- 删除 -->
<v-icon small @click="deleteItem(item)">
mdi-delete
</v-icon>
</template>
</v-data-table>
</template>
<script>
export default {
data: () => ({
// 新增修改弹框是否展示
dialog: false,
// 删除弹框是否展示
dialogDelete: false,
// 测试计划弹框是否展示
dialogPlan: false,
// 测试计划名称
planName: '',
// 勾选的用例
selected: [],
// 表头
headers: [
{ text: '用例Id', align: 'start', sortable: false, value: 'id' },
{ text: '用例名称', value: 'name' },
{ text: '用例步骤', value: 'step' },
{ text: '用例方法', value: 'method' },
{ text: '备注', value: 'remark' },
{ text: '操作', value: 'actions', sortable: false },
],
// 数据源
desserts: [],
// 弹框类型
editedIndex: -1,
// 修改表单数据
editedItem: {
id: '',
name: '',
step: '',
method: '',
remark: '',
},
// 默认表单数据
defaultItem: {
id: '',
name: '',
step: '',
method: '',
remark: '',
},
}),
// 计算属性
computed: {
formTitle() {
// 三元表达式返回弹框标题
return this.editedIndex === -1 ? '新增用例' : '修改用例'
},
},
// 侦听器
watch: {
// 侦听新增修改弹框,val为真时展开弹框,否则关闭
dialog(val) {
val || this.close()
},
// 侦听修改弹框,val为真时展开弹框,否则关闭
dialogDelete(val) {
val || this.closeDelete()
},
},
// 生命周期函数
created() {
this.initData()
},
// 方法
methods: {
// 初始化数据
initData() {
console.log('initData')
this.$api.testcase.getTestcase().then((result) => {
// 将接口返回数据赋值给表格
this.desserts = result.data.data
}).catch((err) => {
console.log('err', err)
})
},
// 修改列表项
editItem(item) {
this.editedIndex = this.desserts.indexOf(item)
this.editedItem = Object.assign({}, item)
this.dialog = true
},
// 删除列表项
deleteItem(item) {
this.editedIndex = this.desserts.indexOf(item)
this.editedItem = Object.assign({}, item)
this.dialogDelete = true
},
// 删除弹框确认操作
deleteItemConfirm() {
this.$api.testcase.deleteTestcase({ 'id': this.editedItem.id }).then((result) => {
// 删除成功时,发起初始化数据操作
if (result.data.code == 0) {
this.initData()
}
}).catch((err) => {
console.log('err', err)
})
this.closeDelete()
},
// 关闭新增修改用例弹框
close() {
this.dialog = false
// 等待Vue实例更新视图后再执行
// 使用this.$nextTick方法是有效处理Vue组件中DOM更新后的操作的一种方式
// 例如获取更新后的DOM元素的尺寸、操作更新后的DOM节点等
this.$nextTick(() => {
this.editedItem = Object.assign({}, this.defaultItem)
this.editedIndex = -1
})
},
// 关闭删除用例弹框
closeDelete() {
this.dialogDelete = false
this.$nextTick(() => {
this.editedItem = Object.assign({}, this.defaultItem)
this.editedIndex = -1
})
},
// 保存操作
save() {
if (this.editedIndex > -1) {
// 编辑操作
this.$api.testcase.updateTestcase(this.editedItem).then((result) => {
// 修改成功时,发起初始化数据操作
if (result.data.code == 0) {
this.initData()
}
}).catch((err) => {
console.log('err', err)
})
} else {
// 新增操作
// 解构复制获取除id之外的数据
const { id, ...newEditedItem } = this.editedItem
this.$api.testcase.addTestcase(newEditedItem).then((result) => {
// 新增成功时,发起初始化数据操作
if (result.data.code == 0) {
this.initData()
}
}).catch((err) => {
console.log('err', err)
})
}
this.close()
},
// 创建测试计划
savePlan() {
console.log('planName', this.planName)
console.log('selected', this.selected)
// this.selected是完整的测试用例列表,接口只需要id列表
const idList = this.selected.map((item) => item.id)
// 测试计划新增
this.$api.plan.addPlan({ 'name': this.planName, 'testcase_ids': idList }).then((result) => {
console.log('result', result.data)
}).catch((err) => {
console.log('err', err)
})
// 新增完成之后需要初始化
this.planName = ''
this.selected = []
this.dialogPlan = false
}
},
}
</script>
测试用例组件-效果
测试计划组件-Plan.vue
<template>
<div>
<v-dialog v-model="dialog" max-width="700px">
<v-data-table :headers="recordHeaders" :items="recordDesserts" :items-per-page="5"
class="elevation-1"></v-data-table>
</v-dialog>
<v-data-table :headers="headers" :items="desserts" :items-per-page="5" class="elevation-1">
<!-- 顶部插槽 -->
<template v-slot:top>
<v-toolbar flat>
<v-toolbar-title>测试计划</v-toolbar-title>
<v-divider class="mx-4" inset vertical></v-divider>
<v-spacer></v-spacer>
</v-toolbar>
</template>
<!-- 关联用例插槽 -->
<template v-slot:item.testcases="{ item }">
<span v-for="i in item.testcases">{{ i.name }}-</span>
</template>
<!-- 操作插槽 -->
<template v-slot:item.actions="{ item }">
<v-btn small color="success" class="mr-2" @click="buildPlan(item)">执行</v-btn>
<v-btn small color="primary" class="mr-2" @click="getRecord(item)">历史记录</v-btn>
<v-btn small color="warning" @click="deletePlan(item)">删除</v-btn>
</template>
</v-data-table>
</div>
</template>
<script>
export default {
data() {
return {
dialog: false,
recordHeaders: [
{ text: '记录ID', align: 'start', sortable: false, value: 'id', },
{ text: '报告地址', value: 'report' },
{ text: '创建时间', value: 'create_time' },
],
recordDesserts: [],
headers: [
{ text: '计划ID', align: 'start', sortable: false, value: 'id', },
{ text: '计划名称', value: 'name' },
{ text: '关联用例', value: 'testcases' },
{ text: '操作', value: 'actions' }
],
desserts: [],
buildList: []
}
},
created() {
this.initData()
},
methods: {
// 初始化数据
initData() {
this.$api.plan.getPlan().then((result) => {
// 将接口返回数据赋值给表格
this.desserts = result.data.data
}).catch((err) => {
console.log('err', err)
})
},
// 执行计划
buildPlan(item) {
this.$api.record.addRecord({ 'plan_id': item.id }).then((result) => {
// 执行成功
console.log('result', result)
}).catch((err) => {
console.log('err', err)
})
this.closeDelete()
},
// 删除计划
deletePlan(item) {
this.$api.plan.deletePlan({ 'id': item.id }).then((result) => {
// 删除成功时,发起初始化数据操作
if (result.data.code == 0) {
this.initData()
}
}).catch((err) => {
console.log('err', err)
})
this.closeDelete()
},
// 获取指定计划的报告
getRecord(item) {
this.$api.record.getRecord({ 'plan_id': item.id }).then((result) => {
// 获取成功时,发起初始化数据操作
if (result.data.code == 0) {
this.recordDesserts = result.data.data
console.log('this.buildList', this.buildList)
}
}).catch((err) => {
console.log('err', err)
})
this.dialog = true
}
}
}
</script>
测试计划组件-效果
测试记录组件-Record.vue
<template>
<v-data-table :headers="headers" :items="desserts" :items-per-page="5" class="elevation-1">
<!-- 顶部插槽 -->
<template v-slot:top>
<v-toolbar flat>
<v-toolbar-title>测试记录</v-toolbar-title>
<v-divider class="mx-4" inset vertical></v-divider>
<v-spacer></v-spacer>
</v-toolbar>
</template>
<!-- 报告插槽 -->
<template v-slot:item.report="{ item }">
<a :href="item.report" target="_blank">{{ item.report }}</a>
</template>
</v-data-table>
</template>
<script>
export default {
data() {
return {
headers: [
{ text: '记录ID', align: 'start', sortable: false, value: 'id', },
{ text: '计划Id', value: 'plan_id' },
{ text: '报告地址', value: 'report' },
{ text: '创建时间', value: 'create_time' },
],
desserts: []
}
},
created() {
this.initData()
},
methods: {
// 初始化数据
initData() {
this.$api.record.getRecord().then((result) => {
// 将接口返回数据赋值给表格
this.desserts = result.data.data
}).catch((err) => {
console.log('err', err)
})
},
}
}
</script>