152 lines
7.5 KiB
TypeScript
152 lines
7.5 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
||
import { readFileSync } from "node:fs";
|
||
import { resolve } from "node:path";
|
||
|
||
const readSubjectDetail = () => readFileSync(resolve(__dirname, "./SubjectDetail.vue"), "utf8");
|
||
|
||
describe("SubjectDetail medication adherence", () => {
|
||
it("adds a medication adherence tab with editable actual count", () => {
|
||
const source = readSubjectDetail();
|
||
|
||
expect(source).toContain('name="medicationAdherence"');
|
||
expect(source).toContain("actual_medication_count");
|
||
expect(source).toContain("saveMedicationAdherence");
|
||
expect(source).toContain("<div class=\"editor-title\">用药依从性</div>");
|
||
expect(source).toContain("实际用药次数为试验期间实际药物暴露次数");
|
||
expect(source).toContain("75%~125% 范围内(包含边界值)");
|
||
});
|
||
|
||
it("uses baseline and termination visit actual dates for adherence calculation", () => {
|
||
const source = readSubjectDetail();
|
||
|
||
expect(source).toContain("const medicationStartDate = computed(() => detail.baseline_date || \"\");");
|
||
expect(source).toContain('new Set(["终止治疗", "提前终止"])');
|
||
expect(source).toContain("return Math.floor((end.getTime() - start.getTime()) / 86400000) + 1;");
|
||
});
|
||
|
||
it("marks adherence outside 75 to 125 percent as abnormal", () => {
|
||
const source = readSubjectDetail();
|
||
|
||
expect(source).toContain("if (rate < 75 || rate > 125) return \"danger\";");
|
||
expect(source).not.toContain("if (rate < 80 || rate > 120) return \"danger\";");
|
||
});
|
||
|
||
it("provides a dedicated early termination workflow", () => {
|
||
const source = readSubjectDetail();
|
||
|
||
expect(source).toContain("openEarlyTerminationDialog");
|
||
expect(source).toContain("createEarlyTerminationVisit");
|
||
expect(source).toContain("return \"不适用\";");
|
||
expect(source).toContain("getVisitStatusLabel");
|
||
expect(source).toContain("ElMessage.success(TEXT.common.messages.saveSuccess);");
|
||
expect(source).toContain("提前终止已保存,刷新数据失败,请手动刷新页面");
|
||
expect(source).toContain("提前终止表示受试者在方案最后一个计划访视窗口期前停止治疗");
|
||
expect(source).toContain("validateEarlyTerminationDateInput");
|
||
expect(source).toContain("提前终止日期必须早于最后计划访视窗口期开始日");
|
||
expect(source).toContain("getVisitPlannedDateLabel");
|
||
expect(source).toContain("getVisitWindowRangeLabel");
|
||
expect(source).toContain("~");
|
||
expect(source).toContain("row.window_start === row.planned_date && row.window_end === row.planned_date");
|
||
expect(source).toContain("return \"不适用\";");
|
||
});
|
||
|
||
it("gates participant subresource mutations by concrete API permissions", () => {
|
||
const source = readSubjectDetail();
|
||
|
||
expect(source).toContain("isApiPermissionAllowed");
|
||
expect(source).toContain("getProjectRole");
|
||
expect(source).toContain("isSystemAdmin");
|
||
[
|
||
"subjects:update",
|
||
"subject_histories:read",
|
||
"subject_histories:create",
|
||
"subject_histories:update",
|
||
"subject_histories:delete",
|
||
"visits:list",
|
||
"visits:create",
|
||
"visits:update",
|
||
"visits:delete",
|
||
"subject_aes:create",
|
||
"subject_aes:read",
|
||
"subject_aes:update",
|
||
"subject_aes:delete",
|
||
"subject_pds:create",
|
||
"subject_pds:list",
|
||
"subject_pds:update",
|
||
"subject_pds:delete",
|
||
].forEach((operationKey) => {
|
||
expect(source).toContain(operationKey);
|
||
});
|
||
expect(source).toContain("v-if=\"currentTabAction.allowed\"");
|
||
expect(source).toContain("v-if=\"canReadHistory\"");
|
||
expect(source).toContain("v-if=\"canReadVisit\"");
|
||
expect(source).toContain("v-if=\"canListAe\"");
|
||
expect(source).toContain("v-if=\"canListPd\"");
|
||
expect(source).toContain("v-if=\"canUpdateHistory || canDeleteHistory\"");
|
||
expect(source).toContain("v-if=\"canUpdateVisit || canDeleteVisit\"");
|
||
expect(source).toContain("v-if=\"canUpdateAe || canDeleteAe\"");
|
||
expect(source).toContain("v-if=\"canUpdatePd || canDeletePd\"");
|
||
expect(source).toContain("const canSaveHistory = computed(() => (historyEditingId.value ? canUpdateHistory.value : canCreateHistory.value));");
|
||
expect(source).toContain(':can-create="canCreateVisit"');
|
||
expect(source).toContain(':can-update="canUpdateVisit"');
|
||
expect(source).toContain("const canSaveAe = computed(() => (aeEditingId.value ? canUpdateAe.value : canCreateAe.value));");
|
||
expect(source).toContain("const canSavePd = computed(() => (pdEditingId.value ? canUpdatePd.value : canCreatePd.value));");
|
||
expect(source).toContain("const canReadHistory = computed(() => canUseApiPermission(\"subject_histories:read\"));");
|
||
expect(source).toContain("const canReadVisit = computed(() => canUseApiPermission(\"visits:list\"));");
|
||
expect(source).toContain("if (!canReadHistory.value)");
|
||
expect(source).toContain("if (!canReadVisit.value)");
|
||
expect(source).toContain("const canCreateAe = computed(() => canUseApiPermission(\"subject_aes:create\"));");
|
||
expect(source).toContain("const canUpdateAe = computed(() => canUseApiPermission(\"subject_aes:update\"));");
|
||
expect(source).toContain("const canDeleteAe = computed(() => canUseApiPermission(\"subject_aes:delete\"));");
|
||
expect(source).toContain("const canListAe = computed(() => canUseApiPermission(\"subject_aes:read\"));");
|
||
expect(source).toContain("const canListPd = computed(() => canUseApiPermission(\"subject_pds:list\"));");
|
||
});
|
||
});
|
||
|
||
describe("SubjectDetail drawer editor", () => {
|
||
it("opens participant basic information edit in the shared drawer editor", () => {
|
||
const source = readSubjectDetail();
|
||
|
||
expect(source).toContain("SubjectEditorDrawer");
|
||
expect(source).toContain("subjectEditorVisible");
|
||
expect(source).toContain("@success=\"handleSubjectEditorSuccess\"");
|
||
expect(source).not.toContain("subjectEditing");
|
||
expect(source).not.toContain("saveSubjectEdit");
|
||
});
|
||
|
||
it("uses drawer editors for participant subresource create and edit flows", () => {
|
||
const source = readSubjectDetail();
|
||
|
||
expect(source).toContain("VisitEditorDrawer");
|
||
expect(source).toContain("adherenceDrawerVisible");
|
||
expect(source).toContain("aeDrawerVisible");
|
||
expect(source).toContain("pdDrawerVisible");
|
||
expect(source).toContain("openVisitDrawer");
|
||
expect(source).toContain("openAeDrawer");
|
||
expect(source).toContain("openPdDrawer");
|
||
expect(source).toContain("openMedicationAdherenceDrawer");
|
||
expect(source).not.toContain("visitDialogVisible");
|
||
expect(source).not.toContain("aeDialogVisible");
|
||
expect(source).not.toContain("pdDialogVisible");
|
||
expect(source).not.toContain("adherenceEditing");
|
||
expect(source).not.toContain("<el-dialog v-if=\"visitDialogVisible\"");
|
||
expect(source).not.toContain("<el-dialog v-if=\"aeDialogVisible\"");
|
||
expect(source).not.toContain("<el-dialog v-if=\"pdDialogVisible\"");
|
||
});
|
||
});
|
||
|
||
describe("VisitEditorDrawer business form", () => {
|
||
const readVisitEditorDrawer = () => readFileSync(resolve(__dirname, "./VisitEditorDrawer.vue"), "utf8");
|
||
|
||
it("keeps create and edit visit flows on the same business fields", () => {
|
||
const source = readVisitEditorDrawer();
|
||
|
||
expect(source).not.toContain("<template v-if=\"isEditing\">");
|
||
expect(source).not.toContain("<template v-else>");
|
||
expect(source).toContain("v-model=\"form.actual_date\"");
|
||
expect(source).toContain("v-model=\"form.notes\"");
|
||
expect(source).toContain("actual_date: form.actual_date || null");
|
||
expect(source).toContain("notes: form.notes || null");
|
||
});
|
||
});
|