feat(测试计划): 创建测试计划抽屉

This commit is contained in:
teukkk 2024-05-08 10:57:25 +08:00 committed by Craftsman
parent 9b0f27215a
commit 1fba073345
11 changed files with 315 additions and 10 deletions

View File

@ -1,6 +1,7 @@
import MSR from '@/api/http/index'; import MSR from '@/api/http/index';
import { import {
addTestPlanModuleUrl, addTestPlanModuleUrl,
AddTestPlanUrl,
DeleteTestPlanModuleUrl, DeleteTestPlanModuleUrl,
GetTestPlanListUrl, GetTestPlanListUrl,
GetTestPlanModuleCountUrl, GetTestPlanModuleCountUrl,
@ -12,7 +13,7 @@ import {
import type { CreateOrUpdateModule, UpdateModule } from '@/models/caseManagement/featureCase'; import type { CreateOrUpdateModule, UpdateModule } from '@/models/caseManagement/featureCase';
import type { CommonList, MoveModules, TableQueryParams } from '@/models/common'; import type { CommonList, MoveModules, TableQueryParams } from '@/models/common';
import { ModuleTreeNode } from '@/models/common'; import { ModuleTreeNode } from '@/models/common';
import type { TestPlanItem } from '@/models/testPlan/testPlan'; import type { AddTestPlanParams, TestPlanItem } from '@/models/testPlan/testPlan';
// 获取模块树 // 获取模块树
export function getTestPlanModule(params: TableQueryParams) { export function getTestPlanModule(params: TableQueryParams) {
@ -48,3 +49,8 @@ export function getPlanModulesCounts(data: TableQueryParams) {
export function getTestPlanList(data: TableQueryParams) { export function getTestPlanList(data: TableQueryParams) {
return MSR.post<CommonList<TestPlanItem>>({ url: GetTestPlanListUrl, data }); return MSR.post<CommonList<TestPlanItem>>({ url: GetTestPlanListUrl, data });
} }
// 创建测试计划
export function addTestPlan(data: AddTestPlanParams) {
return MSR.post({ url: AddTestPlanUrl, data });
}

View File

@ -12,3 +12,5 @@ export const DeleteTestPlanModuleUrl = '/test-plan/module/delete';
export const GetTestPlanModuleCountUrl = '/test-plan/module/count'; export const GetTestPlanModuleCountUrl = '/test-plan/module/count';
// 测试计划列表 // 测试计划列表
export const GetTestPlanListUrl = '/test-plan/page'; export const GetTestPlanListUrl = '/test-plan/page';
// 创建测试计划
export const AddTestPlanUrl = '/test-plan/add';

View File

@ -0,0 +1,38 @@
<template>
<a-collapse v-model:active-key="moreSettingActive" :bordered="false" :show-expand-icon="false">
<a-collapse-item :key="1">
<template #header>
<MsButton
type="text"
@click="() => (moreSettingActive.length > 0 ? (moreSettingActive = []) : (moreSettingActive = [1]))"
>
{{ t('common.moreSetting') }}
<icon-down v-if="moreSettingActive.length > 0" class="text-rgb(var(--primary-5))" />
<icon-right v-else class="text-rgb(var(--primary-5))" />
</MsButton>
</template>
<div class="mt-[24px]">
<slot name="content"></slot>
</div>
</a-collapse-item>
</a-collapse>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import MsButton from '@/components/pure/ms-button/index.vue';
import { useI18n } from '@/hooks/useI18n';
const { t } = useI18n();
const moreSettingActive = ref<number[]>([]);
function clearMoreSettingActive() {
moreSettingActive.value = [];
}
defineExpose({
clearMoreSettingActive,
});
</script>

View File

@ -164,4 +164,6 @@ export default {
'common.unExecute': 'Not executed', 'common.unExecute': 'Not executed',
'common.pass': 'Pass', 'common.pass': 'Pass',
'common.unPass': 'Fail pass', 'common.unPass': 'Fail pass',
'common.belongModule': 'Belong module',
'common.moreSetting': 'More settings',
}; };

View File

@ -164,4 +164,6 @@ export default {
'common.unExecute': '未执行', 'common.unExecute': '未执行',
'common.pass': '通过', 'common.pass': '通过',
'common.unPass': '不通过', 'common.unPass': '不通过',
'common.belongModule': '所属模块',
'common.moreSetting': '更多设置',
}; };

View File

@ -1,3 +1,5 @@
import { BatchApiParams } from '../common';
// 计划分页 // 计划分页
export interface TestPlanItem { export interface TestPlanItem {
id?: string; id?: string;
@ -24,4 +26,31 @@ export interface ResourcesItem {
status: boolean; status: boolean;
} }
export interface AssociateCaseRequest extends BatchApiParams {
functionalSelectIds?: string[];
apiSelectIds?: string[];
apiCaseSelectIds?: string[];
apiScenarioSelectIds?: string[];
}
export interface AddTestPlanParams {
id?: string;
name: string;
projectId: string;
groupId?: string;
moduleId: string;
cycle?: number[];
plannedStartTime?: number;
plannedEndTime?: number;
tags: string[];
description?: string;
testPlanning: boolean; // 是否开启测试规划
automaticStatusUpdate: boolean; // 是否自定更新功能用例状态
repeatCase: boolean; // 是否允许重复添加用例
passThreshold: number;
type: string;
baseAssociateCaseRequest: AssociateCaseRequest;
groupOption?: boolean;
}
export default {}; export default {};

View File

@ -164,10 +164,7 @@
if (isSetDefaultKey) { if (isSetDefaultKey) {
selectedNodeKeys.value = [caseTree.value[0].id]; selectedNodeKeys.value = [caseTree.value[0].id];
} }
emits( emits('init', caseTree.value);
'init',
caseTree.value.map((e) => e.name)
);
} catch (error) { } catch (error) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.log(error); console.log(error);

View File

@ -0,0 +1,201 @@
<template>
<MsDrawer
v-model:visible="innerVisible"
:title="form.id ? t('case.updateCase') : t('testPlan.testPlanIndex.createTestPlan')"
:width="800"
unmount-on-close
:ok-text="form.id ? 'common.update' : 'common.create'"
:save-continue-text="t('case.saveContinueText')"
:show-continue="!form.id"
:ok-loading="drawerLoading"
@confirm="handleDrawerConfirm(false)"
@continue="handleDrawerConfirm(true)"
@cancel="handleCancel"
>
<a-form ref="formRef" :model="form" layout="vertical">
<a-form-item
field="name"
:label="t('caseManagement.featureCase.planName')"
:rules="[{ required: true, message: t('testPlan.planForm.nameRequired') }]"
class="w-[732px]"
>
<a-input v-model="form.name" :max-length="255" :placeholder="t('testPlan.planForm.namePlaceholder')" />
</a-form-item>
<a-form-item field="description" :label="t('common.desc')" class="w-[732px]">
<a-textarea v-model:model-value="form.description" :placeholder="t('common.pleaseInput')" :max-length="1000" />
</a-form-item>
<a-form-item :label="t('common.belongModule')" class="w-[436px]">
<a-tree-select
v-model:modelValue="form.moduleId"
:data="props.moduleTree"
:field-names="{ title: 'name', key: 'id', children: 'children' }"
:tree-props="{
virtualListProps: {
height: 200,
threshold: 200,
},
}"
allow-search
:filter-tree-node="filterTreeNode"
>
<template #tree-slot-title="node">
<a-tooltip :content="`${node.name}`" position="tl">
<div class="inline-flex w-full">
<div class="one-line-text w-[240px] text-[var(--color-text-1)]">
{{ node.name }}
</div>
</div>
</a-tooltip>
</template>
</a-tree-select>
</a-form-item>
<a-form-item
field="cycle"
:label="t('testPlan.planForm.planStartAndEndTime')"
asterisk-position="end"
class="w-[436px]"
>
<a-range-picker
v-model:model-value="form.cycle"
show-time
value-format="timestamp"
:separator="t('common.to')"
:time-picker-props="{
defaultValue: ['00:00:00', '00:00:00'],
}"
/>
</a-form-item>
<a-form-item field="tags" :label="t('common.tag')" class="w-[436px]">
<MsTagsInput v-model:model-value="form.tags" :max-tag-count="10" :max-length="50" />
</a-form-item>
<MsMoreSettingCollapse>
<template #content>
<div v-for="item in switchList" :key="item.key" class="mb-[24px] flex items-center gap-[8px]">
<a-switch v-model="form[item.key as keyof AddTestPlanParams] as boolean" size="small" />
{{ t(item.label) }}
<a-tooltip :position="item.tooltipPosition">
<template #content>
<div v-for="descItem in item.desc" :key="descItem">{{ t(descItem) }}</div>
</template>
<IconQuestionCircle class="h-[16px] w-[16px] text-[--color-text-4] hover:text-[rgb(var(--primary-5))]" />
</a-tooltip>
</div>
<a-form-item field="passThreshold" :label="t('testPlan.planForm.passThreshold')">
<a-input-number
v-model:model-value="form.passThreshold"
size="small"
mode="button"
class="w-[120px]"
:min="1"
:max="100"
:default-value="100"
/>
</a-form-item>
</template>
</MsMoreSettingCollapse>
</a-form>
</MsDrawer>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { FormInstance, Message, TreeNodeData } from '@arco-design/web-vue';
import { cloneDeep } from 'lodash-es';
import MsDrawer from '@/components/pure/ms-drawer/index.vue';
import MsMoreSettingCollapse from '@/components/pure/ms-more-setting-collapse/index.vue';
import MsTagsInput from '@/components/pure/ms-tags-input/index.vue';
import { addTestPlan } from '@/api/modules/test-plan/testPlan';
import { useI18n } from '@/hooks/useI18n';
import useAppStore from '@/store/modules/app';
import { ModuleTreeNode } from '@/models/common';
import type { AddTestPlanParams } from '@/models/testPlan/testPlan';
import { testPlanTypeEnum } from '@/enums/testPlanEnum';
interface SwitchListModel {
key: string;
label: string;
desc: string[];
tooltipPosition: 'top' | 'tl' | 'tr' | 'bottom' | 'bl' | 'br' | 'left' | 'lt' | 'lb' | 'right' | 'rt' | 'rb';
}
const { t } = useI18n();
const appStore = useAppStore();
const props = defineProps<{
moduleTree?: ModuleTreeNode[];
}>();
const innerVisible = defineModel<boolean>('visible', {
required: true,
});
const drawerLoading = ref(false);
const formRef = ref<FormInstance>();
const initForm: AddTestPlanParams = {
name: '',
projectId: '',
moduleId: 'root',
cycle: [],
tags: [],
description: '',
testPlanning: false,
automaticStatusUpdate: true,
repeatCase: false,
passThreshold: 100,
type: testPlanTypeEnum.TEST_PLAN,
baseAssociateCaseRequest: { selectIds: [], selectAll: false, condition: {} },
};
const form = ref<AddTestPlanParams>(cloneDeep(initForm));
function filterTreeNode(searchValue: string, nodeData: TreeNodeData) {
return (nodeData as ModuleTreeNode).name.toLowerCase().indexOf(searchValue.toLowerCase()) > -1;
}
const switchList: SwitchListModel[] = [
{
key: 'repeatCase',
label: 'testPlan.planForm.associateRepeatCase',
tooltipPosition: 'bl',
desc: ['testPlan.planForm.repeatCaseTip1', 'testPlan.planForm.repeatCaseTip2'],
},
];
function handleCancel() {
innerVisible.value = false;
formRef.value?.resetFields();
form.value = cloneDeep(initForm);
}
function handleDrawerConfirm(isContinue: boolean) {
formRef.value?.validate(async (errors) => {
if (!errors) {
drawerLoading.value = true;
try {
// TODO
const params: AddTestPlanParams = {
...cloneDeep(form.value),
plannedStartTime: form.value.cycle ? form.value.cycle[0] : undefined,
plannedEndTime: form.value.cycle ? form.value.cycle[1] : undefined,
projectId: appStore.currentProjectId,
};
if (!form.value?.id) {
await addTestPlan(params);
Message.success(t('common.createSuccess'));
}
// TODO
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
} finally {
drawerLoading.value = false;
}
if (!isContinue) {
handleCancel();
}
form.value.name = '';
}
});
}
</script>

View File

@ -9,7 +9,7 @@
:placeholder="t('caseManagement.featureCase.searchTip')" :placeholder="t('caseManagement.featureCase.searchTip')"
allow-clear allow-clear
/> />
<a-dropdown-button class="ml-2" type="primary" @click="handleSelect"> <a-dropdown-button class="ml-2" type="primary" @click="handleSelect('createPlan')">
{{ t('common.newCreate') }} {{ t('common.newCreate') }}
<template #icon> <template #icon>
<icon-down /> <icon-down />
@ -87,6 +87,7 @@
</div> </div>
</template> </template>
</MsSplitBox> </MsSplitBox>
<CreateAndEditPlanDrawer v-model:visible="showPlanDrawer" :module-tree="folderTree" />
</MsCard> </MsCard>
</template> </template>
@ -101,12 +102,14 @@
import MsSplitBox from '@/components/pure/ms-split-box/index.vue'; import MsSplitBox from '@/components/pure/ms-split-box/index.vue';
import PlanTable from './components/planTable.vue'; import PlanTable from './components/planTable.vue';
import TestPlanTree from './components/testPlanTree.vue'; import TestPlanTree from './components/testPlanTree.vue';
import CreateAndEditPlanDrawer from './createAndEditPlanDrawer.vue';
import { createPlanModuleTree } from '@/api/modules/test-plan/testPlan'; import { createPlanModuleTree } from '@/api/modules/test-plan/testPlan';
import { useI18n } from '@/hooks/useI18n'; import { useI18n } from '@/hooks/useI18n';
import useAppStore from '@/store/modules/app'; import useAppStore from '@/store/modules/app';
import type { CaseModuleQueryParams, CreateOrUpdateModule, ValidateInfo } from '@/models/caseManagement/featureCase'; import type { CaseModuleQueryParams, CreateOrUpdateModule, ValidateInfo } from '@/models/caseManagement/featureCase';
import type { ModuleTreeNode } from '@/models/common';
import Message from '@arco-design/web-vue/es/message'; import Message from '@arco-design/web-vue/es/message';
@ -141,7 +144,6 @@
}; };
const addSubVisible = ref(false); const addSubVisible = ref(false);
const rootModulesName = ref<string[]>([]);
const planTreeRef = ref(); const planTreeRef = ref();
const confirmLoading = ref(false); const confirmLoading = ref(false);
const confirmRef = ref(); const confirmRef = ref();
@ -186,8 +188,11 @@
* 设置根模块名称列表 * 设置根模块名称列表
* @param names 根模块名称列表 * @param names 根模块名称列表
*/ */
function setRootModules(names: string[]) { const rootModulesName = ref<string[]>([]);
rootModulesName.value = names; const folderTree = ref<ModuleTreeNode[]>([]);
function setRootModules(treeNode: ModuleTreeNode[]) {
folderTree.value = treeNode;
rootModulesName.value = treeNode.map((e) => e.name);
} }
/** /**
@ -195,7 +200,16 @@
*/ */
function initModulesCount(params: any) {} function initModulesCount(params: any) {}
function handleSelect() {} const showPlanDrawer = ref(false);
function handleSelect(value: string | number | Record<string, any> | undefined) {
switch (value) {
case 'createPlan':
showPlanDrawer.value = true;
break;
default:
break;
}
}
</script> </script>
<style scoped lang="less"> <style scoped lang="less">

View File

@ -60,4 +60,11 @@ export default {
'project.testPlanIndex.functionalUseCase': 'case', 'project.testPlanIndex.functionalUseCase': 'case',
'project.testPlanIndex.apiCase': 'Api use case', 'project.testPlanIndex.apiCase': 'Api use case',
'project.testPlanIndex.apiScenarioCase': 'Api scenario use cases', 'project.testPlanIndex.apiScenarioCase': 'Api scenario use cases',
'testPlan.planForm.namePlaceholder': 'Please enter the name of the test plan',
'testPlan.planForm.nameRequired': 'Test plan name cannot be empty',
'testPlan.planForm.planStartAndEndTime': 'Planned start and end time',
'testPlan.planForm.associateRepeatCase': 'Allow associated duplicate cases',
'testPlan.planForm.passThreshold': 'Pass threshold',
'testPlan.planForm.repeatCaseTip1': 'Enable: Repeatedly associate the same case',
'testPlan.planForm.repeatCaseTip2': 'Close: Cannot be associated with the same case repeatedly',
}; };

View File

@ -60,4 +60,11 @@ export default {
'project.testPlanIndex.functionalUseCase': '功能用例', 'project.testPlanIndex.functionalUseCase': '功能用例',
'project.testPlanIndex.apiCase': '接口用例', 'project.testPlanIndex.apiCase': '接口用例',
'project.testPlanIndex.apiScenarioCase': '接口场景用例', 'project.testPlanIndex.apiScenarioCase': '接口场景用例',
'testPlan.planForm.namePlaceholder': '请输入测试计划名称',
'testPlan.planForm.nameRequired': '测试计划名称不能为空',
'testPlan.planForm.planStartAndEndTime': '计划起止时间',
'testPlan.planForm.associateRepeatCase': '允许关联重复用例',
'testPlan.planForm.passThreshold': '通过阀值',
'testPlan.planForm.repeatCaseTip1': '开启:可重复关联同一个用例',
'testPlan.planForm.repeatCaseTip2': '关闭:不可重复关联同一用例',
}; };