20230827-测试平台前端实战

课堂PPT

https://material.hogwarts.ceshiren.com/resource/test_platform_fronted/ppt/测试平台训练营前端实战.html

源码地址

Sign in · GitLab

训练营目标

  • 项目初始化
  • api接口封装
  • 布局实现
  • 路由封装
  • 测试用例组件实现
  • 测试计划组件实现
  • 测试记录组件实现

项目初始化1-详情

image


项目初始化2-预设

  • 进入预设界面
  • 默认第一个是vue3,选择手动配置项目
  • 点击下一步

image


项目初始化3-功能

  • 进入功能界面
  • 只勾选babel和router,其他全部取消
  • 点击下一步

image


项目初始化4-配置

  • 进入配置界面
  • 切换vue2.x,默认是vue3.x
  • 点击创建项目

image


项目初始化5-插件

  • 点击左侧插件,进入插件页面
  • 右上角输入vuetify,搜索后选择第一个进行安装
  • 安装完成界面会展示该插件


项目初始化6-依赖

  • 点击左侧依赖,进入依赖页面
  • 右上角输入axios,搜索后选择第一个进行安装
  • 安装完成界面会展示该依赖


启动项目

  • 点击左侧任务,进入任务页面
  • 选择serve
  • 点击运行
  • 点击启动app,即可打开页面


默认代码改造

  • 目录结构
    image

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>

测试记录组件-效果


v-slot插槽指令,为什么一直是标红的呀,老师

想起来了,要加上中括号
v-slot:[item.actions]=“{ item }”