feat(测试计划): 脑图执行用例-执行

This commit is contained in:
teukkk 2024-08-01 12:59:33 +08:00 committed by Craftsman
parent 46259da117
commit b5a3592e46
10 changed files with 309 additions and 77 deletions

View File

@ -154,7 +154,7 @@
const emit = defineEmits<{
(e: 'operation', type: string, node: MinderJsonNode): void;
(e: 'handleReviewDone', refreshTree?: boolean): void;
(e: 'handleReviewDone'): void;
}>();
const route = useRoute();
@ -484,10 +484,10 @@
selectNode.value.data?.resource?.includes(caseTag)
) {
window.minder.execCommand('resource', [statusTagMap[status], caseTag]);
emit('handleReviewDone');
} else {
emit('handleReviewDone', true);
initCaseTree();
}
emit('handleReviewDone');
}
//

View File

@ -13,7 +13,7 @@ import { getGenerateId } from '@/utils';
*
* @returns API
*/
export default function useMinderBaseApi({ hasEditPermission }: { hasEditPermission: boolean }) {
export default function useMinderBaseApi({ hasEditPermission }: { hasEditPermission?: boolean }) {
const { t } = useI18n();
const minderStore = useMinderStore();

View File

@ -42,14 +42,33 @@
</template>
</a-dropdown>
<!-- 执行 -->
<a-tooltip
v-if="props.canEdit && hasAnyPermission(['PROJECT_TEST_PLAN:READ+EXECUTE'])"
:content="t('common.execute')"
<a-trigger
v-model:popup-visible="executeVisible"
trigger="click"
position="bl"
:click-outside-to-close="false"
popup-container=".ms-minder-container"
>
<MsButton type="icon" class="ms-minder-node-float-menu-icon-button">
<MsIcon type="icon-icon_play-round_filled" class="text-[var(--color-text-4)]" />
</MsButton>
</a-tooltip>
<a-tooltip
v-if="props.canEdit && hasAnyPermission(['PROJECT_TEST_PLAN:READ+EXECUTE'])"
:content="t('common.execute')"
>
<MsButton
type="icon"
:class="[
'ms-minder-node-float-menu-icon-button',
`${executeVisible ? 'ms-minder-node-float-menu-icon-button--focus' : ''}`,
]"
>
<MsIcon type="icon-icon_play-round_filled" class="text-[var(--color-text-4)]" />
</MsButton>
</a-tooltip>
<template #content>
<div class="w-[440px] rounded bg-white p-[16px] shadow-[0_0_10px_rgba(0,0,0,0.05)]">
<ExecuteSubmit :select-node="selectNode" :test-plan-id="props.planId" @done="handleExecuteDone" />
</div>
</template>
</a-trigger>
<!-- 查看详情 -->
<a-tooltip v-if="canShowDetail" :content="t('common.detail')">
<MsButton
@ -113,6 +132,29 @@
}"
@success="handleAddBugDone"
/>
<a-modal
v-model:visible="stepExecuteModelVisible"
:title="t('common.executionResult')"
class="p-[4px]"
title-align="start"
body-class="p-0"
:width="800"
:cancel-button-props="{ disabled: submitStepExecuteLoading }"
:ok-loading="submitStepExecuteLoading"
:ok-text="t('caseManagement.caseReview.commitResult')"
@before-ok="submitStepExecute"
@cancel="cancelStepExecute"
>
<AddStep
v-model:step-list="stepData"
is-scroll-y
is-test-plan
:scroll-y="190"
:is-disabled-test-plan="false"
is-disabled
/>
<ExecuteForm v-model:form="executeForm" class="mt-[24px]" rich-text-max-height="150px" />
</a-modal>
</div>
</template>
@ -134,21 +176,29 @@
import { MsFileItem } from '@/components/pure/ms-upload/types';
import Attachment from '@/components/business/ms-minders/featureCaseMinder/attachment.vue';
import BugList from '@/components/business/ms-minders/featureCaseMinder/bugList.vue';
import useMinderBaseApi from '@/components/business/ms-minders/featureCaseMinder/useMinderBaseApi';
import AddStep from '@/views/case-management/caseManagementFeature/components/addStep.vue';
import AddDefectDrawer from '@/views/case-management/caseManagementFeature/components/tabContent/tabBug/addDefectDrawer.vue';
import LinkDefectDrawer from '@/views/case-management/caseManagementFeature/components/tabContent/tabBug/linkDefectDrawer.vue';
import ReviewCommentList from '@/views/case-management/caseManagementFeature/components/tabContent/tabComment/reviewCommentList.vue';
import ExecuteForm from '@/views/test-plan/testPlan/detail/featureCase/components/executeForm.vue';
import ExecuteSubmit from '@/views/test-plan/testPlan/detail/featureCase/detail/executeSubmit.vue';
import { getCasePlanMinder } from '@/api/modules/case-management/caseReview';
import { associateBugToPlan, executeHistory, getCaseDetail } from '@/api/modules/test-plan/testPlan';
import { associateBugToPlan, executeHistory, getCaseDetail, runFeatureCase } from '@/api/modules/test-plan/testPlan';
import { defaultExecuteForm } from '@/config/testPlan';
import { useI18n } from '@/hooks/useI18n';
import useAppStore from '@/store/modules/app';
import useMinderStore from '@/store/modules/components/minder-editor/index';
import useTestPlanFeatureCaseStore from '@/store/modules/testPlan/testPlanFeatureCase';
import { findNodeByKey, mapTree, replaceNodeInTree } from '@/utils';
import { findNodeByKey, getGenerateId, mapTree, replaceNodeInTree } from '@/utils';
import { hasAllPermission, hasAnyPermission } from '@/utils/permission';
import type { StepList } from '@/models/caseManagement/featureCase';
import type { TableQueryParams } from '@/models/common';
import { ModuleTreeNode } from '@/models/common';
import type { ExecuteHistoryItem } from '@/models/testPlan/testPlan';
import type { ExecuteFeatureCaseFormParams, ExecuteHistoryItem } from '@/models/testPlan/testPlan';
import { LastExecuteResults } from '@/enums/caseEnum';
import { MinderEventName, MinderKeyEnum } from '@/enums/minderEnum';
import {
@ -166,15 +216,16 @@
const emit = defineEmits<{
(e: 'operation', type: string, node: MinderJsonNode): void;
(e: 'handleAddBugDone'): void;
(e: 'refreshPlan'): void;
}>();
const { t } = useI18n();
const appStore = useAppStore();
const minderStore = useMinderStore();
const testPlanFeatureCaseStore = useTestPlanFeatureCaseStore();
const caseTag = t('common.case');
const moduleTag = t('common.module');
const { caseTag, moduleTag, stepTag, stepExpectTag } = useMinderBaseApi({});
const actualResultTag = t('system.orgTemplate.actualResult');
const importJson = ref<MinderJson>({
root: {} as MinderJsonNode,
treePath: [],
@ -455,6 +506,172 @@
}
}
const selectNode = ref();
//
const showLinkDefectDrawer = ref(false);
const showAddDefectDrawer = ref(false);
const linkDrawerLoading = ref(false);
const bugListRef = ref<InstanceType<typeof BugList>>();
function handleAddBugDone() {
if (extraVisible.value && activeExtraKey.value === 'bug') {
bugListRef.value?.handleShowTypeChange();
}
emit('refreshPlan');
}
async function associateSuccessHandler(params: TableQueryParams) {
try {
linkDrawerLoading.value = true;
await associateBugToPlan({
...params,
testPlanCaseId: selectNode.value.data?.id,
caseId: selectNode.value.data?.caseId,
testPlanId: props.planId,
});
Message.success(t('caseManagement.featureCase.associatedSuccess'));
linkDrawerLoading.value = false;
handleAddBugDone();
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
} finally {
linkDrawerLoading.value = false;
}
}
//
const executeVisible = ref(false);
//
function updateCaseActualResultNode(node: MinderJsonNode, content: string) {
let actualResultNode;
actualResultNode = node.children?.find((item: MinderJsonNode) => item.data?.id === `actualResult-${node.data?.id}`);
if (actualResultNode) {
actualResultNode
.setData('resource', [actualResultTag])
.setData('text', content ?? '')
.render();
} else {
actualResultNode = createNode(
{ resource: [actualResultTag], text: content ?? '', id: `actualResult-${node.data?.id}` },
node
);
handleRenderNode(node, [actualResultNode]);
}
}
// /
function handleExecuteDone(status: LastExecuteResults, content: string) {
executeVisible.value = false;
const resource = selectNode.value.data?.resource;
if (resource?.includes(caseTag)) {
//
window.minder.execCommand('resource', [executionResultMap[status].statusText, caseTag]);
//
updateCaseActualResultNode(selectNode.value, content);
//
if (extraVisible.value && activeExtraKey.value === 'history') {
initExecuteHistory(selectNode.value.data);
}
} else if (resource?.includes(moduleTag)) {
initCaseTree();
}
emit('refreshPlan');
}
const stepExecuteModelVisible = ref(false);
const caseNodeAboveSelectStep = ref(); //
const submitStepExecuteLoading = ref(false);
const executeForm = ref<ExecuteFeatureCaseFormParams>({ ...defaultExecuteForm });
const stepData = ref<StepList[]>([
{
id: getGenerateId(),
step: '',
expected: '',
showStep: false,
showExpected: false,
},
]);
async function getStepData(id: string) {
try {
const res = await getCaseDetail(id);
if (res.steps) {
stepData.value = JSON.parse(res.steps).map((item: any) => {
return {
id: item.id,
step: item.desc,
expected: item.result,
actualResult: item.actualResult,
executeResult: item.executeResult,
};
});
} else {
stepData.value = [];
}
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
}
}
watch(
() => stepData.value,
() => {
const executionResultList = stepData.value?.map((item) => item.executeResult);
if (executionResultList?.includes(LastExecuteResults.ERROR)) {
executeForm.value.lastExecResult = LastExecuteResults.ERROR;
} else if (executionResultList?.includes(LastExecuteResults.BLOCKED)) {
executeForm.value.lastExecResult = LastExecuteResults.BLOCKED;
} else {
executeForm.value.lastExecResult = LastExecuteResults.SUCCESS;
}
},
{ deep: true }
);
function cancelStepExecute() {
executeForm.value = { ...defaultExecuteForm };
}
function submitStepExecuteDone(status: string, content: string) {
//
caseNodeAboveSelectStep.value.setData('resource', [executionResultMap[status].statusText, caseTag]).render();
//
updateCaseActualResultNode(caseNodeAboveSelectStep.value, content);
//
caseNodeAboveSelectStep.value.children.forEach((child: MinderJsonNode) => {
const step = stepData.value.find((item) => item.id === child.data?.id);
if (step?.executeResult?.length) {
child.setData('resource', [executionResultMap[step?.executeResult].statusText, stepTag]).render();
}
if (step?.actualResult?.length) {
child.children?.[0].children?.[0].setData('text', step?.actualResult).render();
}
});
caseNodeAboveSelectStep.value.layout();
}
async function submitStepExecute() {
try {
submitStepExecuteLoading.value = true;
const params = {
projectId: appStore.currentProjectId,
testPlanId: props.planId,
caseId: caseNodeAboveSelectStep.value.data.caseId,
id: caseNodeAboveSelectStep.value.data.id,
...executeForm.value,
notifier: executeForm.value?.commentIds?.join(';'),
stepsExecResult: JSON.stringify(stepData.value),
};
await runFeatureCase(params);
stepExecuteModelVisible.value = false;
Message.success(t('common.updateSuccess'));
cancelStepExecute();
emit('refreshPlan');
submitStepExecuteDone(params.lastExecResult, params.content ?? '');
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
} finally {
submitStepExecuteLoading.value = false;
}
}
//
const hasOperationPermission = computed(
() => hasAnyPermission(['PROJECT_TEST_PLAN:READ+UPDATE', 'PROJECT_TEST_PLAN:READ+ASSOCIATION']) && props.canEdit
);
@ -485,8 +702,25 @@
];
}
const selectNode = ref();
/**
* 获取步骤节点对应的用例节点
* @param node 选中节点
* @param resource 标签
*/
function getCaseNodeWithResource(node: MinderJsonNode, resource: string) {
while (node.parent) {
if (node?.data?.resource?.includes(resource)) {
return node.parent;
}
if (node.data?.resource?.includes(caseTag)) {
return null;
}
node = node.parent;
}
return null;
}
//
async function handleNodeSelect(node: MinderJsonNode) {
const { data } = node;
//
@ -511,6 +745,16 @@
canShowFloatMenu.value = false;
}
// //
if ([actualResultTag, stepTag, stepExpectTag].some((item) => node.data?.resource?.includes(item))) {
caseNodeAboveSelectStep.value = getCaseNodeWithResource(node, stepTag);
if (caseNodeAboveSelectStep.value.data.id) {
getStepData(caseNodeAboveSelectStep.value.data.id);
stepExecuteModelVisible.value = true;
}
return;
}
//
if (node.data?.resource?.includes(caseTag) && !hasOperationPermission.value) {
canShowMoreMenu.value = false;
@ -525,6 +769,8 @@
canShowEnterNode.value = false;
}
executeVisible.value = false;
if (data?.resource?.includes(caseTag)) {
canShowDetail.value = true;
showAssociateBugMenu.value = true;
@ -555,37 +801,6 @@
extraVisible.value = false;
}
//
const showLinkDefectDrawer = ref(false);
const showAddDefectDrawer = ref(false);
const linkDrawerLoading = ref(false);
const bugListRef = ref<InstanceType<typeof BugList>>();
function handleAddBugDone() {
if (extraVisible.value && activeExtraKey.value === 'bug') {
bugListRef.value?.handleShowTypeChange();
}
emit('handleAddBugDone');
}
async function associateSuccessHandler(params: TableQueryParams) {
try {
linkDrawerLoading.value = true;
await associateBugToPlan({
...params,
testPlanCaseId: selectNode.value.data?.id,
caseId: selectNode.value.data?.caseId,
testPlanId: props.planId,
});
Message.success(t('caseManagement.featureCase.associatedSuccess'));
linkDrawerLoading.value = false;
handleAddBugDone();
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
} finally {
linkDrawerLoading.value = false;
}
}
defineExpose({
initCaseTree,
});
@ -599,4 +814,10 @@
margin: 0;
height: 100%;
}
:deep(.execute-form) .rich-wrapper .halo-rich-text-editor .editor-content {
max-height: 54px !important;
.ProseMirror {
min-height: 38px;
}
}
</style>

View File

@ -134,7 +134,7 @@ export interface BatchReviewCaseParams extends BatchApiParams {
reviewId: string; // 评审id
userId: string; // 用户id, 用来判断是否只看我的
reviewPassRule: ReviewPassRule; // 评审规则
status: ReviewResult; // 评审结果
status: StartReviewStatus; // 评审结果
content: string; // 评论内容
notifier: string; // 评论@的人的Id, 多个以';'隔开
reviewCommentFileIds?: string[]; // 富文本ids

View File

@ -105,6 +105,7 @@
stepList: any;
isDisabled?: boolean;
isScrollY?: boolean;
scrollY?: number;
isTestPlan?: boolean;
isDisabledTestPlan?: boolean;
isPreview?: boolean; //
@ -212,7 +213,7 @@
const tableProps = ref<Partial<MsTableProps<StepList>>>({
columns: templateFieldColumns.value,
scroll: { x: '100%', y: props.isScrollY ? 400 : '' },
scroll: { x: '100%', y: props.isScrollY ? props.scrollY ?? 400 : '' },
selectable: false,
noDisable: true,
showSetting: false,

View File

@ -175,7 +175,7 @@
:review-progress="props.reviewProgress"
:review-pass-rule="props.reviewPassRule"
@operation="handleMinderOperation"
@handle-review-done="handleReviewDone"
@handle-review-done="emit('refresh')"
/>
</div>
<a-modal
@ -372,6 +372,7 @@
import { ReviewCaseItem, ReviewItem, ReviewPassRule, ReviewResult } from '@/models/caseManagement/caseReview';
import { BatchApiParams, TableQueryParams } from '@/models/common';
import { StartReviewStatus } from '@/enums/caseEnum';
import { CaseManagementRouteEnum } from '@/enums/routeEnum';
import { TableKeyEnum } from '@/enums/tableEnum';
import { FilterSlotNameEnum } from '@/enums/tableFilterEnum';
@ -907,7 +908,7 @@
reviewId: route.query.id as string,
userId: props.onlyMine ? userStore.id || '' : '',
reviewPassRule: props.reviewPassRule,
status: dialogForm.value.result as ReviewResult,
status: dialogForm.value.result as StartReviewStatus,
content: dialogForm.value.reason,
notifier: dialogForm.value.commentIds.join(';'),
moduleIds: props.activeFolder === 'all' ? [] : [props.activeFolder, ...props.offspringIds],
@ -989,13 +990,6 @@
handleOperation(type);
}
function handleReviewDone(refreshTree?: boolean) {
if (refreshTree) {
refresh(false);
}
emit('refresh');
}
/**
* 处理表格选中后批量操作
* @param event 批量操作事件对象

View File

@ -73,10 +73,11 @@
const modalVisible = ref(false);
const submitLoading = ref(false);
const submitForm = computed(() => (modalVisible.value ? dialogForm.value : form.value));
const submitDisabled = computed(
() =>
form.value.status !== StartReviewStatus.PASS &&
(form.value.content === '' || form.value.content.trim() === '<p style=""></p>')
submitForm.value.status !== StartReviewStatus.PASS &&
(submitForm.value.content === '' || submitForm.value.content.trim() === '<p style=""></p>')
);
//
@ -114,14 +115,14 @@
userId: props.userId,
reviewId: props.reviewId,
reviewPassRule: props.reviewPassRule,
...(modalVisible.value ? dialogForm.value : form.value),
notifier: form.value.notifiers?.join(';') ?? '',
...submitForm.value,
notifier: submitForm.value.notifiers?.join(';') ?? '',
...getMinderOperationParams(props.selectNode),
};
await batchReview(params);
modalVisible.value = false;
Message.success(t('caseManagement.caseReview.reviewSuccess'));
emit('done', form.value.status);
emit('done', params.status);
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);

View File

@ -117,7 +117,7 @@
:plan-id="props.planId"
:can-edit="props.canEdit"
@operation="handleMinderOperation"
@handle-add-bug-done="emit('refresh')"
@refresh-plan="emit('refresh')"
/>
</div>
<!-- 批量执行 -->

View File

@ -17,6 +17,7 @@
:preview-url="PreviewEditorImageUrl"
:auto-height="false"
class="w-full"
:max-height="props.richTextMaxHeight"
:placeholder="
props.isDblclickPlaceholder
? t('testPlan.featureCase.richTextDblclickPlaceholder')
@ -44,6 +45,7 @@
const props = defineProps<{
isDblclickPlaceholder?: boolean;
richTextMaxHeight?: string;
}>();
const form = defineModel<ExecuteFeatureCaseFormParams>('form', {

View File

@ -26,26 +26,29 @@
import { Message } from '@arco-design/web-vue';
import { cloneDeep } from 'lodash-es';
import type { MinderJsonNode } from '@/components/pure/ms-minder-editor/props';
import { getMinderOperationParams } from '@/components/business/ms-minders/caseReviewMinder/utils';
import ExecuteForm from '@/views/test-plan/testPlan/detail/featureCase/components/executeForm.vue';
import { runFeatureCase } from '@/api/modules/test-plan/testPlan';
import { batchExecuteCase, runFeatureCase } from '@/api/modules/test-plan/testPlan';
import { defaultExecuteForm } from '@/config/testPlan';
import { useI18n } from '@/hooks/useI18n';
import useAppStore from '@/store/modules/app';
import type { StepExecutionResult } from '@/models/caseManagement/featureCase';
import type { ExecuteFeatureCaseFormParams } from '@/models/testPlan/testPlan';
import type { BatchExecuteFeatureCaseParams, ExecuteFeatureCaseFormParams } from '@/models/testPlan/testPlan';
import { LastExecuteResults } from '@/enums/caseEnum';
const props = defineProps<{
caseId: string;
caseId?: string;
testPlanId: string;
id: string;
id?: string;
selectNode?: MinderJsonNode;
stepExecutionResult?: StepExecutionResult[];
}>();
const emit = defineEmits<{
(e: 'done'): void;
(e: 'done', status: LastExecuteResults, content: string): void;
}>();
const { t } = useI18n();
@ -104,18 +107,28 @@
submitLoading.value = true;
const params = {
projectId: appStore.currentProjectId,
caseId: props.caseId,
testPlanId: props.testPlanId,
id: props.id,
...(modalVisible.value ? dialogForm.value : form.value),
stepsExecResult: JSON.stringify(props.stepExecutionResult) ?? '',
notifier: form.value?.commentIds?.join(';'),
stepsExecResult: JSON.stringify(props.stepExecutionResult),
notifier: (modalVisible.value ? dialogForm.value : form.value)?.commentIds?.join(';'),
};
await runFeatureCase(params);
//
if (props.selectNode) {
await batchExecuteCase({
...params,
...getMinderOperationParams(props.selectNode),
} as BatchExecuteFeatureCaseParams);
} else {
await runFeatureCase({
...params,
caseId: props.caseId ?? '',
id: props.id ?? '',
});
}
modalVisible.value = false;
Message.success(t('common.updateSuccess'));
form.value = { ...defaultExecuteForm };
emit('done');
emit('done', params.lastExecResult, params.content ?? '');
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);