十一、【核心功能篇】测试用例管理:设计用例新增编辑界面

news2025/6/4 0:16:38

【核心功能篇】测试用例管理:设计用例新增&编辑界面

    • 前言
      • 准备工作
      • 第一步:创建测试用例相关的 API 服务 (`src/api/testcase.ts`)
      • 第二步:创建测试用例编辑页面组件 (`src/views/testcase/TestCaseEditView.vue`)
      • 第三步:配置测试用例编辑页的路由
      • 第四步:测试用例新增&编辑功能
    • 总结

前言

一个好的测试用例编辑界面应该具备以下特点:

  • 清晰直观: 用户能够快速理解各个字段的含义和作用。
  • 高效易用: 能够方便地输入和修改信息,特别是对于重复性的测试步骤。
  • 结构化: 能够清晰地展示和管理测试步骤等复杂结构。
  • 可扩展性: (虽然本篇可能不完全覆盖)未来可以方便地增加对参数化、数据驱动、自定义关键字等高级功能的支持。

在我们的 Django 后端模型 (api/models.py中的 TestCase) 中,我们暂时将 steps_text 定义为一个 TextField,用于存储文本描述的测试步骤。为了实现一个更结构化的步骤编辑界面,前端在提交时需要将这些步骤组织成一种格式(例如 JSON 数组或特定分隔符的文本),而后端在保存或解析时需要能处理这种格式。

这篇文章将带你

  1. 规划测试用例编辑页面的整体布局和交互。
  2. 设计并实现一个能够动态添加、删除和编辑测试步骤的表单区域。
  3. 将测试用例的创建和编辑功能与后端 API 进行联调。

我们将重点放在前端界面的设计与实现,以及与后端 API 的数据交互上。

准备工作

  1. 前端项目就绪: test-platform/frontend 项目可以正常运行 (npm run dev)。
  2. 后端 API 运行中: Django 后端服务运行(python manage.py runserver),测试用例的 API (/api/testcases/) 可用。
  3. Axios 和 API 服务已封装: utils/request.tsapi/project.ts, api/module.ts 已配置好。
  4. 项目和模块管理功能可用: 我们需要先有项目和模块,才能创建测试用例。
  5. Element Plus 集成完毕。

第一步:创建测试用例相关的 API 服务 (src/api/testcase.ts)

与项目和模块类似,我们先为测试用例创建 API 服务文件。
在这里插入图片描述

// test-platform/frontend/src/api/testcase.ts
import request from '@/utils/request'
import type { AxiosPromise } from 'axios'

// 与后端 TestCase model 和 TestCaseSerializer 对应
export interface TestCase {
  id: number;
  name: string;
  description: string | null;
  module: number; // 所属模块 ID
  module_name?: string; // 可选,如果 API 返回
  project_id?: number; // 可选,如果 API 返回
  project_name?: string; // 可选,如果 API 返回
  priority: 'P0' | 'P1' | 'P2' | 'P3';
  priority_display?: string;
  precondition: string | null;
  steps_text: string; // 后端存储的是合并后的文本
  expected_result: string;
  case_type: 'functional' | 'api' | 'ui';
  case_type_display?: string;
  maintainer: string | null;
  create_time: string;
  update_time: string;
}

export type TestCaseListResponse = TestCase[] // 假设列表直接返回数组

// 创建或更新测试用例时发送的数据类型
export interface UpsertTestCaseData {
  name: string;
  description?: string | null;
  module: number; // 必须
  priority?: 'P0' | 'P1' | 'P2' | 'P3';
  precondition?: string | null;
  steps_text: string; // 前端会将步骤数组合并为这个文本
  expected_result?: string;
  case_type?: 'functional' | 'api' | 'ui';
  maintainer?: string | null;
}

// 1. 获取测试用例列表 (支持按模块或项目过滤)
export function getTestCaseList(params?: { module_id?: number, project_id?: number, search?: string }): AxiosPromise<TestCaseListResponse> {
  return request({
    url: '/testcases/',
    method: 'get',
    params
  })
}

// 2. 创建测试用例
export function createTestCase(data: UpsertTestCaseData): AxiosPromise<TestCase> {
  return request({
    url: '/testcases/',
    method: 'post',
    data
  })
}

// 3. 获取单个测试用例详情
export function getTestCaseDetail(testCaseId: number): AxiosPromise<TestCase> {
  return request({
    url: `/testcases/${testCaseId}/`,
    method: 'get'
  })
}

// 4. 更新测试用例
export function updateTestCase(testCaseId: number, data: Partial<UpsertTestCaseData>): AxiosPromise<TestCase> {
  return request({
    url: `/testcases/${testCaseId}/`,
    method: 'put', // 或者 patch
    data
  })
}

// 5. 删除测试用例
export function deleteTestCase(testCaseId: number): AxiosPromise<void> {
  return request({
    url: `/testcases/${testCaseId}/`,
    method: 'delete'
  })
}

关键点:

  • UpsertTestCaseData 中的 steps_text 字段,前端会将动态编辑的多个步骤描述合并成一个字符串传递给它。
  • 类型定义应尽量与后端 DRF Serializer 的输入输出保持一致。

第二步:创建测试用例编辑页面组件 (src/views/testcase/TestCaseEditView.vue)

我们将创建一个新的路由页面专门用于新建和编辑测试用例。

a. 创建文件:
src/views/ 目录下创建 testcase 文件夹,并在其中创建 TestCaseEditView.vue
在这里插入图片描述

b. 编写 TestCaseEditView.vue 的基本结构和表单:
在这里插入图片描述

<!-- test-platform/frontend/src/views/testcase/TestCaseEditView.vue -->
<template>
  <div class="testcase-edit-view" v-loading="pageLoading">
    <el-page-header @back="goBack" :content="pageTitle" class="page-header-custom" />

    <el-card class="form-card">
      <el-form
        ref="testCaseFormRef"
        :model="formData"
        :rules="formRules"
        label-width="120px"
        label-position="right"
      >
        <el-row :gutter="20">
          <el-col :span="12">
            <el-form-item label="用例名称" prop="name">
              <el-input v-model="formData.name" placeholder="请输入用例名称" />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="所属模块" prop="module">
              <!-- 这里需要一个模块选择器,先用 Input 占位,后续改进 -->
              <el-select 
                v-model="formData.module" 
                placeholder="请选择所属模块" 
                filterable
                style="width: 100%;"
                @focus="fetchModulesForSelect" 
                :loading="moduleSelectLoading"
              >
                <el-option
                  v-for="item in moduleOptions"
                  :key="item.id"
                  :label="`${item.project_name} - ${item.name}`"
                  :value="item.id"
                />
              </el-select>
            </el-form-item>
          </el-col>
        </el-row>

        <el-form-item label="用例描述" prop="description">
          <el-input v-model="formData.description" type="textarea" placeholder="请输入用例描述" />
        </el-form-item>

        <el-row :gutter="20">
          <el-col :span="12">
            <el-form-item label="优先级" prop="priority">
              <el-select v-model="formData.priority" placeholder="请选择优先级" style="width: 100%;">
                <el-option label="P0 - 最高" value="P0" />
                <el-option label="P1 - 高" value="P1" />
                <el-option label="P2 - 中" value="P2" />
                <el-option label="P3 - 低" value="P3" />
              </el-select>
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="用例类型" prop="case_type">
              <el-select v-model="formData.case_type" placeholder="请选择用例类型" style="width: 100%;">
                <el-option label="功能测试" value="functional" />
                <el-option label="接口测试" value="api" />
                <el-option label="UI测试" value="ui" />
              </el-select>
            </el-form-item>
          </el-col>
        </el-row>
        
        <el-form-item label="维护人" prop="maintainer">
          <el-input v-model="formData.maintainer" placeholder="请输入维护人名称" />
        </el-form-item>

        <el-form-item label="前置条件" prop="precondition">
          <el-input v-model="formData.precondition" type="textarea" :rows="2" placeholder="请输入前置条件" />
        </el-form-item>

        <!-- 测试步骤区域 -->
        <el-form-item label="测试步骤" prop="steps_text_ignored"> <!-- steps_text_ignored 仅用于触发表单项样式 -->
          <div class="steps-editor">
            <div v-for="(step, index) in formData.steps" :key="index" class="step-item">
              <el-input
                v-model="step.description"
                type="textarea"
                :autosize="{ minRows: 1, maxRows: 4 }"
                placeholder="请输入步骤描述"
                class="step-input"
              />
              <el-button
                type="danger"
                :icon="Delete"
                circle
                size="small"
                @click="removeStep(index)"
                class="step-action-btn"
                v-if="formData.steps.length > 1"
              />
            </div>
            <el-button type="primary" :icon="Plus" @click="addStep" plain size="small">
              添加步骤
            </el-button>
          </div>
           <!-- 隐藏的表单项,用于实际提交 steps_text,由 steps 数组生成 -->
          <el-input v-model="computedStepsText" style="display: none;"></el-input>
        </el-form-item>


        <el-form-item label="预期结果" prop="expected_result">
          <el-input v-model="formData.expected_result" type="textarea" :rows="3" placeholder="请输入预期结果" />
        </el-form-item>

        <el-form-item>
          <el-button type="primary" @click="handleSubmit" :loading="submitLoading">
            {{ isEditMode ? '更新用例' : '创建用例' }}
          </el-button>
          <el-button @click="goBack">取消</el-button>
        </el-form-item>
      </el-form>
    </el-card>
  </div>
</template>

<script setup lang="ts">
import { ref, reactive, computed, onMounted, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ElMessage, ElPageHeader } from 'element-plus' // 确保导入 ElPageHeader
import type { FormInstance, FormRules } from 'element-plus'
import { Plus, Delete } from '@element-plus/icons-vue'
import { 
  createTestCase, 
  getTestCaseDetail, 
  updateTestCase, 
  type UpsertTestCaseData,
  type TestCase
} from '@/api/testcase'
import { getModuleList, type Module as ApiModule } from '@/api/module' // 获取所有模块用于选择器

interface FormStep {
  description: string;
}

interface TestCaseFormData extends Omit<UpsertTestCaseData, 'steps_text'> {
  steps: FormStep[]; // 前端用步骤数组来编辑
}


const route = useRoute()
const router = useRouter()

const pageLoading = ref(false)
const submitLoading = ref(false)
const testCaseFormRef = ref<FormInstance>()

const testCaseId = computed(() => route.params.id ? Number(route.params.id) : null)
const isEditMode = computed(() => !!testCaseId.value)
const pageTitle = computed(() => (isEditMode.value ? '编辑测试用例' : '新建测试用例'))

// 所属模块选择器的数据
const moduleOptions = ref<ApiModule[]>([])
const moduleSelectLoading = ref(false)


const initialFormData: TestCaseFormData = {
  name: '',
  description: null,
  module: undefined as number | undefined, // 确保初始为 undefined 以便 placeholder 显示
  priority: 'P1',
  precondition: null,
  steps: [{ description: '' }], // 至少有一个空步骤
  expected_result: '',
  case_type: 'functional',
  maintainer: null,
}
const formData = reactive<TestCaseFormData>({ ...initialFormData })

const formRules = reactive<FormRules>({
  name: [{ required: true, message: '用例名称不能为空', trigger: 'blur' }],
  module: [{ required: true, message: '请选择所属模块', trigger: 'change' }],
  priority: [{ required: true, message: '请选择优先级', trigger: 'change' }],
  steps: [{ // 对 steps 数组的校验 (虽然我们主要校验合并后的 steps_text)
    type: 'array', 
    required: true, 
    validator: (rule, value, callback) => {
      if (!value || value.length === 0 || value.every((step: FormStep) => !step.description.trim())) {
        callback(new Error('测试步骤不能为空'));
      } else {
        callback();
      }
    }, 
    trigger: 'change' 
  }],
  expected_result: [{ required: true, message: '预期结果不能为空', trigger: 'blur' }],
})

// 将步骤数组转换为提交给后端的 steps_text 字符串
const computedStepsText = computed(() => {
  return formData.steps.map(step => step.description.trim()).filter(desc => desc).join('\n');
})

// 加载模块列表给选择器
const fetchModulesForSelect = async () => {
  if (moduleOptions.value.length > 0 && !isEditMode.value) return; // 避免重复加载 (新建时如果已有数据则不重载)
                                                                   // 编辑时可能需要强制重载,或当模块列表不常变时缓存
  moduleSelectLoading.value = true;
  try {
    // 这里获取所有模块,实际项目中可能需要分页或搜索
    // 如果模块非常多,这里需要优化,例如使用远程搜索的 Select
    const response = await getModuleList(); // 这个 getModuleList 需要支持不传 projectId 获取所有
    moduleOptions.value = response.data;
  } catch (error) {
    console.error('获取模块列表失败:', error);
    ElMessage.error('获取模块列表失败');
  } finally {
    moduleSelectLoading.value = false;
  }
}


// 加载用例详情 (编辑模式)
const loadTestCaseDetail = async () => {
  if (!isEditMode.value || !testCaseId.value) return
  pageLoading.value = true
  try {
    const response = await getTestCaseDetail(testCaseId.value)
    const dataFromServer = response.data
    
    // 回填表单数据
    formData.name = dataFromServer.name
    formData.description = dataFromServer.description
    formData.module = dataFromServer.module
    formData.priority = dataFromServer.priority
    formData.precondition = dataFromServer.precondition
    formData.expected_result = dataFromServer.expected_result
    formData.case_type = dataFromServer.case_type
    formData.maintainer = dataFromServer.maintainer

    // 将 steps_text 解析回步骤数组
    if (dataFromServer.steps_text) {
      formData.steps = dataFromServer.steps_text.split('\n').map(desc => ({ description: desc }))
      if (formData.steps.length === 0) { // 保证至少有一个空步骤输入框
          formData.steps.push({ description: '' });
      }
    } else {
      formData.steps = [{ description: '' }]
    }
    // 确保模块选择器中有当前模块的选项,如果没有,需要手动获取一次模块列表(或者在 fetchModulesForSelect 中处理)
    if (formData.module && !moduleOptions.value.find(m => m.id === formData.module)) {
        await fetchModulesForSelect(); // 重新获取模块列表以确保包含当前模块
    }


  } catch (error) {
    ElMessage.error('获取用例详情失败')
    console.error(error)
  } finally {
    pageLoading.value = false
  }
}


onMounted(async () => {
  await fetchModulesForSelect(); // 先加载模块选项
  if (isEditMode.value) {
    await loadTestCaseDetail()
  }
})


// 动态步骤管理
const addStep = () => {
  formData.steps.push({ description: '' })
}
const removeStep = (index: number) => {
  if (formData.steps.length > 1) {
    formData.steps.splice(index, 1)
  } else {
    ElMessage.warning('至少需要一个测试步骤')
  }
}


const handleSubmit = async () => {
  if (!testCaseFormRef.value) return
  await testCaseFormRef.value.validate(async (valid) => {
    if (valid) {
      // 再次校验 steps 是否真的有内容(因为 formRules 对数组的校验可能不够精细)
      if (!computedStepsText.value.trim()) {
        ElMessage.error('测试步骤描述不能为空');
        return;
      }

      submitLoading.value = true
      const dataToSubmit: UpsertTestCaseData = {
        name: formData.name,
        description: formData.description,
        module: formData.module!, // module 是必填的,这里可以用 ! 断言
        priority: formData.priority,
        precondition: formData.precondition,
        steps_text: computedStepsText.value, // 使用合并后的文本
        expected_result: formData.expected_result,
        case_type: formData.case_type,
        maintainer: formData.maintainer,
      }

      try {
        if (isEditMode.value && testCaseId.value) {
          await updateTestCase(testCaseId.value, dataToSubmit)
          ElMessage.success('测试用例更新成功!')
        } else {
          await createTestCase(dataToSubmit)
          ElMessage.success('测试用例创建成功!')
        }
        // 成功后可以跳转到用例列表页或详情页
        // router.push(`/testcases/list?moduleId=${formData.module}`) // 假设有列表页
        router.push({ name: 'testcases', query: { moduleId: formData.module } }) // 假设列表页路由名为 'testcases'
      } catch (error) {
        console.error('用例操作失败:', error)
        // 全局错误提示已处理
      } finally {
        submitLoading.value = false
      }
    } else {
      ElMessage.error('请检查表单填写是否正确!')
      return false
    }
  })
}

const goBack = () => {
  router.back()
}
</script>

<style scoped lang="scss">
.testcase-edit-view {
  padding: 20px;
}
.page-header-custom {
  margin-bottom: 20px;
  background-color: #fff;
  padding: 16px 24px;
  border-radius: 4px;
  box-shadow: 0 2px 12px 0 rgba(0,0,0,0.1);
}
.form-card {
  padding: 20px;
}

.steps-editor {
  width: 100%;
  .step-item {
    display: flex;
    align-items: center;
    margin-bottom: 10px;
    .step-input {
      flex-grow: 1;
      margin-right: 10px;
    }
    .step-action-btn {
      // flex-shrink: 0; // 防止按钮被压缩
    }
  }
}
</style>

代码解释与关键点:

  • 整体布局: 使用 ElPageHeader 提供返回和标题,ElCard 包裹表单。
  • 表单字段: 根据 TestCase 模型定义了各个输入项。
    • 所属模块 (formData.module):
      • 使用 el-select
      • moduleOptions 通过调用 getModuleList() API (在 api/module.ts 中) 来获取所有模块(这里暂时获取所有,实际项目可能需要优化为按项目筛选或远程搜索)。
      • fetchModulesForSelect 在组件挂载时以及 Select 获得焦点时(如果选项为空)被调用。
      • 重要: api/module.ts 中的 getModuleList 函数需要能够不传 projectId 时返回所有模块,或者你需要一个新的 API 来获取所有模块。这里假设 getModuleList() 可以无参数调用以获取所有模块。
  • 动态测试步骤 (formData.steps):
    • formData.steps 是一个响应式数组,每个元素是 { description: string } 对象。
    • 使用 v-for 渲染每个步骤的输入框和删除按钮。
    • addStep() 方法向数组中添加一个新的空步骤。
    • removeStep(index) 方法从数组中删除指定索引的步骤(至少保留一个)。
    • computedStepsText 这是一个计算属性,它将 formData.steps 数组中的 description 合并成一个用换行符 \n 分隔的字符串。这个计算属性的值将用于赋值给提交给后端的 steps_text 字段。我们在模板中添加了一个隐藏的 el-input 绑定到它,主要是为了方便调试时查看,实际提交时我们直接用这个计算属性的值。
    • 表单校验 (formRules.steps): 我们为 steps 数组添加了一个自定义校验器,确保至少有一个步骤并且步骤描述不全为空。
  • 编辑模式 (isEditMode):
    • 通过 route.params.id 判断当前是新建还是编辑模式。
    • pageTitle 动态显示。
    • onMounted 中,如果是编辑模式,则调用 loadTestCaseDetail()
  • loadTestCaseDetail()
    • 调用 getTestCaseDetail API 获取用例数据。
    • 回填表单各个字段。
    • 解析 steps_text 将从后端获取的 steps_text 字符串按换行符分割,转换回 formData.steps 数组。
    • 模块选择器回显: 确保在编辑时,如果 formData.module 有值,moduleOptions 中包含该选项,否则 Select 可能无法正确显示已选模块。如果 moduleOptions 中没有,则重新调用 fetchModulesForSelect
  • handleSubmit()
    • 表单校验。
    • 特别校验 computedStepsText 确保合并后的步骤文本不为空。
    • 构造提交给后端的数据 dataToSubmit,其中 steps_text 使用 computedStepsText.value
    • 根据 isEditMode 调用 createTestCaseupdateTestCase API。
    • 成功后跳转到用例列表页 (我们暂时假设用例列表页的路由名为 testcases,并且可以通过 moduleId 查询参数筛选)。

修改 frontend/src/api/module.ts 中的 getModuleList

为了让模块选择器能获取所有模块,我们需要修改 api/module.ts 中的 getModuleList 函数,使其在不传递 projectId 时能获取所有模块。

假设后端 /api/modules/ 在没有 project_id 参数时返回所有模块,那么前端 getModuleList 可以这样:
在这里插入图片描述

// test-platform/frontend/src/api/module.ts
// ...
// 1. 获取模块列表 (支持按项目ID过滤,不传则获取所有)
export function getModuleList(projectId?: number): AxiosPromise<ModuleListResponse> {
  const params: { project_id?: number } = {};
  if (projectId) {
    params.project_id = projectId;
  }
  return request({
    url: '/modules/',
    method: 'get',
    params // 如果 projectId 未定义,则 params 为空对象,不传 project_id 参数
  })
}
// ...

后端 DRF ModuleViewSet 的相应调整:

确保 ModuleViewSetget_queryset 方法在 project_id 未提供时返回所有模块。目前的实现(在上一篇文章中修改的)已经是这样了,如果 project_idNone,则不过滤。
在这里插入图片描述

# api/views.py -> ModuleViewSet
class ModuleViewSet(viewsets.ModelViewSet):
    # ...
    def get_queryset(self):
        queryset = super().get_queryset()
        project_id = self.request.query_params.get('project_id', None) # 修改这里,如果没传,project_id 为 None
        if project_id is not None: # 只有当 project_id 实际传递了才过滤
            try:
                queryset = queryset.filter(project_id=int(project_id))
            except ValueError:
                pass 
        return queryset.order_by('-create_time')

第三步:配置测试用例编辑页的路由

打开 frontend/src/router/index.ts,添加新建和编辑测试用例的路由。
在这里插入图片描述

// test-platform/frontend/src/router/index.ts
// ... (在 Layout 的 children 中添加)
            {
              path: '/testcases', // 用例列表页 (我们将在下一篇创建)
              name: 'testcases',
              component: () => import('../views/project/TestCaseListView.vue'),
              meta: { title: '用例管理', requiresAuth: true }
            },
            {
              path: '/testcase/create', // 新建用例
              name: 'testcaseCreate',
              component: () => import('../views/testcase/TestCaseEditView.vue'),
              meta: { title: '新建测试用例', requiresAuth: true }
            },
            {
              path: '/testcase/edit/:id', // 编辑用例,:id 是用例ID
              name: 'testcaseEdit',
              component: () => import('../views/testcase/TestCaseEditView.vue'),
              meta: { title: '编辑测试用例', requiresAuth: true },
              props: true // 将路由参数 id 作为 props 传递给组件 (虽然我们组件内主要用 route.params)
            },
// ...

说明:

  • 我们为新建 (/testcase/create) 和编辑 (/testcase/edit/:id) 都指向了同一个 TestCaseEditView.vue 组件。组件内部通过 route.params.id 是否存在来区分模式。
  • 为编辑路由启用了 props: true,虽然我们当前组件实现主要依赖 useRoute(),但这是一个好习惯。

第四步:测试用例新增&编辑功能

  1. 确保前后端服务运行,CORS 和 API 正常。
  2. 测试新建用例:
    • 访问 http://127.0.0.1:5173/testcase/create

    • 填写表单,包括选择所属模块,添加几个测试步骤。

    • 点击“创建用例”。
      在这里插入图片描述

    • 观察 Network 面板的 API 请求 (POST /api/testcases/),查看请求体中的 steps_text 是否是合并后的字符串。
      在这里插入图片描述

    • 看是否成功创建并跳转 (跳转目标页 TestCaseListView.vue 尚不存在,会显示空白,但 API 调用是会成功的)。
      在这里插入图片描述

    • 去 Django Admin 或通过 API 确认用例已创建,steps_text 已保存。

  3. 测试编辑用例 (需要先通过 API 或 Django Admin 创建一个用例):
    • 在上面我创建了一个 ID 为 3 的用例。

    • 访问 http://127.0.0.1:5173/testcase/edit/3

    • 页面应加载该用例的数据,表单应被回填,测试步骤应被正确解析并显示。
      在这里插入图片描述

    • 修改数据,例如增删步骤,修改其他字段。

    • 点击“更新用例”。

    • 观察 Network 面板的 API 请求 (PUT /api/testcases/3/)。

    • 确认更新成功。
      在这里插入图片描述

总结

在这篇文章中,我们攻克了测试用例管理中复杂的编辑界面设计与实现:

  • 为测试用例创建了相应的 API 服务函数 (api/testcase.ts) 和 TypeScript 类型。
  • 设计并实现了 TestCaseEditView.vue 组件,用于新建和编辑测试用例,其核心特性包括:
    • 一个包含用例名称、描述、所属模块选择器、优先级、类型、前置条件、预期结果等字段的综合表单。
    • 一个动态的测试步骤编辑区域,用户可以方便地添加、删除和编辑多个步骤描述。
    • 将前端编辑的步骤数组通过计算属性 computedStepsText 合并为后端 steps_text 字段所需的单一字符串。
    • 在编辑模式下,能够从后端 API 获取用例详情,并将 steps_text 解析回步骤数组以供编辑。
  • 配置了新建和编辑测试用例的路由。
  • 指导了如何测试新建和编辑用例的完整流程,并与后端 API 进行了联调。
  • 解决了模块选择器的数据加载和编辑时选项回显的问题。

测试用例的创建和编辑是测试平台的核心功能,一个良好设计的界面能极大地提升测试人员的效率。

在下一篇文章中,我们将实现测试用例的列表展示与搜索功能 (TestCaseListView.vue),让用户能够方便地查看、筛选和查找已创建的测试用例,并从列表跳转到编辑页面。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2395297.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Spring是如何实现属性占位符解析

Spring属性占位符解析 核心实现思路1️⃣ 定义占位符处理器类2️⃣ 处理 BeanDefinition 中的属性3️⃣ 替换具体的占位符4️⃣ 加载配置文件5️⃣ Getter / Setter 方法 源码见&#xff1a;mini-spring 在使用 Spring 框架开发过程中&#xff0c;为了实现配置的灵活性&#xf…

DDR4读写压力测试

1.1测试环境 1.1.1整体环境介绍 板卡&#xff1a; pcie-403板卡 主控芯片&#xff1a; Xilinx xcvu13p-fhgb2104-2 调试软件&#xff1a; Vivado 2018.3 代码环境&#xff1a; Vscode utf-8 测试工程&#xff1a; pcie403_user_top 1.1.2硬件介绍 UD PCIe-403…

编写测试用例

测试用例&#xff08;Test Case&#xff09;是用于测试系统的要素集合 目录 编写测试用例作用 编写测试用例要包含七大元素 测试用例的设计方法 1、等价类法 2、边界值法 3、正交表法 4、判定表法 5、错误推测法 6、场景法 编写测试用例作用 1、确保功能全面覆盖…

每日Prompt:隐形人

提示词 黑色棒球帽&#xff0c;白色抹胸、粉色低腰短裙、白色襪子&#xff0c;黑色鞋子&#xff0c;粉紅色背包&#xff0c;衣服悬浮在空中呈现动态姿势&#xff0c;虚幻引擎渲染风格&#xff0c;高清晰游戏CG质感&#xff0c;户外山林背景&#xff0c;画面聚焦在漂浮的衣服上…

TensorFlow深度学习实战(19)——受限玻尔兹曼机

TensorFlow深度学习实战&#xff08;19&#xff09;——受限玻尔兹曼机 0. 前言1. 受限玻尔兹曼机1.1 受限玻尔兹曼机架构1.2 受限玻尔兹曼机的数学原理 2. 使用受限玻尔兹曼机重建图像3. 深度信念网络小结系列链接 0. 前言 受限玻尔兹曼机 (Restricted Boltzmann Machine, RB…

告别手动绘图!基于AI的Smart Mermaid自动可视化图表工具搭建与使用指南

以下是对Smart Mermaid的简单介绍&#xff1a; 一款基于 AI 技术的 Web 应用程序&#xff0c;可将文本内容智能转换为 Mermaid 格式的代码&#xff0c;并将其渲染成可视化图表可以智能制作流程图、序列图、甘特图、状态图等等&#xff0c;并且支持在线调整、图片导出可以Docke…

【Oracle】安装单实例

个人主页&#xff1a;Guiat 归属专栏&#xff1a;Oracle 文章目录 1. 安装前的准备工作1.1 硬件和系统要求1.2 检查系统环境1.3 下载Oracle软件 2. 系统配置2.1 创建Oracle用户和组2.2 配置内核参数2.3 配置用户资源限制2.4 安装必要的软件包 3. 目录结构和环境变量3.1 创建Ora…

QT中更新或添加组件时出现“”qt操作至少需要一个处于启用状态的有效资料档案库“解决方法”

在MaintenanceTool.exe中点击下一步 第一个&#xff1a; 第二个&#xff1a; 第三个&#xff1a; 以上任意一个放入资料库中

论文速读《UAV-Flow Colosseo: 自然语言控制无人机系统》

论文链接&#xff1a;https://arxiv.org/abs/2505.15725项目主页&#xff1a;https://prince687028.github.io/UAV-Flow/ 0. 简介 近年来&#xff0c;无人机技术蓬勃发展&#xff0c;但如何让无人机像智能助手一样理解并执行人类语言指令&#xff0c;仍是一个前沿挑战。现有研…

ES6+中Promise 中错误捕捉详解——链式调用catch()或者async/await+try/catch

通过 unhandledrejection 捕捉未处理的 Promise 异常&#xff0c;手动将其抛出&#xff0c;最终让 window.onerror 捕捉&#xff0c;从而统一所有异常的处理逻辑 规范代码&#xff1a;catch&#xff08;onRejected&#xff09;、async...awaittry...catch 在 JavaScript 的 Pro…

解常微分方程组

Euler法 function euler_method % 参数设置 v_missile 450; % 导弹速度 km/h v_enemy 90; % 敌艇速度 km/h % 初始条件 x0 0; % 导弹初始位置 x y0 0; % 导弹初始位置 y xe0 120; % 敌艇初始位置 y t0 0; % 初始时间 % 时间步长和总时间 dt 0.01; % 时间步长 t_final …

C++实现汉诺塔游戏自动完成

目录 一、汉诺塔的规则二、数学递归推导式三、步骤实现(一)汉诺塔模型(二)递归实现(三)显示1.命令行显示2.SDL图形显示 四、处理用户输入及SDL环境配置五、总结六、源码下载 一、汉诺塔的规则 游戏由3根柱子和若干大小不一的圆盘组成&#xff0c;初始状态下&#xff0c;所有的…

pikachu靶场通关笔记07 XSS关卡03-存储型XSS

目录 一、XSS 二、存储型XSS 三、源码分析 四、渗透实战 1、输入mooyuan试一试 2、注入Payload 3、查看数据库 4、再次进入留言板页面 本系列为通过《pikachu靶场通关笔记》的XSS关卡(共10关&#xff09;渗透集合&#xff0c;通过对XSS关卡源码的代码审计找到XSS风险的…

OpenCV CUDA模块直方图计算------用于在 GPU 上执行对比度受限的自适应直方图均衡类cv::cuda::CLAHE

操作系统&#xff1a;ubuntu22.04 OpenCV版本&#xff1a;OpenCV4.9 IDE:Visual Studio Code 编程语言&#xff1a;C11 算法描述 cv::cuda::CLAHE 是 OpenCV 的 CUDA 模块中提供的一个类&#xff0c;用于在 GPU 上执行对比度受限的自适应直方图均衡&#xff08;Contrast Limi…

华为OD机试真题——矩形绘制(2025A卷:200分)Java/python/JavaScript/C/C++/GO最佳实现

2025 A卷 200分 题型 本专栏内全部题目均提供Java、python、JavaScript、C、C++、GO六种语言的最佳实现方式; 并且每种语言均涵盖详细的问题分析、解题思路、代码实现、代码详解、3个测试用例以及综合分析; 本文收录于专栏:《2025华为OD真题目录+全流程解析+备考攻略+经验分…

JDBC连不上mysql:Unable to load authentication plugin ‘caching_sha2_password‘.

最近为一个spring-boot项目下了mysql-9.3.0&#xff0c;结果因为mysql版本太新一直报错连不上。 错误如下&#xff1a; 2025-06-01 16:19:43.516 ERROR 22088 --- [http-nio-8080-exec-2] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispat…

MacOS安装Docker Desktop并汉化

1. 安装Docker Desktop 到Docker Desktop For Mac下载对应系统的Docker Desktop 安装包&#xff0c;下载后安装&#xff0c;没有账号需要注册&#xff0c;然后登陆即可。 2. 汉化 前往汉化包下载链接下载对应系统的.asar文件 然后将安装好的文件覆盖原先的文件app.asar文件…

Centos系统搭建主备DNS服务

目录 一、主DNS服务器配置 1.安装 BIND 软件包 2.配置主配置文件 3.创建正向区域文件 4.创建区域数据文件 5.检查配置语法并重启服务 二、从DNS服务配置 1.安装 BIND 软件包 2.配置主配置文件 3.创建缓存目录 4.启动并设置开机自启 一、主DNS服务器配置 1.安装 BIN…

使用 HTML + JavaScript 实现在线考试系统

在现代的在线教育平台中&#xff0c;在线考试系统是不可或缺的一部分。本文将通过一个完整的示例&#xff0c;演示如何使用 HTML、CSS 和 JavaScript 构建一个支持多种题型的在线考试系统。 效果演示 项目概述 本项目主要包含以下核心功能&#xff1a; 支持4种常见题型&…

谷歌工作自动化——仙盟大衍灵机——仙盟创梦IDE

下载地址 https://chromewebstore.google.com/detail/selenium-ide/mooikfkahbdckldjjndioackbalphokd https://chrome.zzzmh.cn/info/mooikfkahbdckldjjndioackbalphokd