整理测试代码并清理文档引用
This commit is contained in:
@@ -1,73 +0,0 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
|
||||
const read = (relativePath: string) => readFileSync(resolve(__dirname, relativePath), "utf8");
|
||||
|
||||
describe("FAQ project permissions", () => {
|
||||
it("maps FAQ, category, and reply actions to backend operation permissions", () => {
|
||||
const source = read("../utils/permission.ts");
|
||||
|
||||
expect(source).toContain('"faq.update": "faq:update"');
|
||||
expect(source).toContain('"faq.delete": "faq:delete"');
|
||||
expect(source).toContain('"faq.category.read": "faq_category:read"');
|
||||
expect(source).toContain('"faq.category.create": "faq_category:create"');
|
||||
expect(source).toContain('"faq.category.update": "faq_category:update"');
|
||||
expect(source).toContain('"faq.category.delete": "faq_category:delete"');
|
||||
expect(source).toContain('"faq.reply.delete": "faq_reply:delete"');
|
||||
});
|
||||
|
||||
it("passes item update and delete permissions separately to the FAQ list", () => {
|
||||
const source = read("./Faq.vue");
|
||||
|
||||
expect(source).toContain('const canReadCategories = computed(() => can("faq.category.read"))');
|
||||
expect(source).toContain('const canUpdateFaq = computed(() => can("faq.update"))');
|
||||
expect(source).toContain('const canDeleteFaq = computed(() => can("faq.delete"))');
|
||||
expect(source).toContain('v-if="canReadCategories"');
|
||||
expect(source).toContain('if (!canReadCategories.value)');
|
||||
expect(source).toContain(':can-update="canUpdateFaq"');
|
||||
expect(source).toContain(':can-delete="canDeleteFaq"');
|
||||
});
|
||||
|
||||
it("does not expose FAQ item delete through creator ownership when backend requires delete permission", () => {
|
||||
const source = read("../components/FaqList.vue");
|
||||
|
||||
expect(source).toContain("canDelete: boolean");
|
||||
expect(source).toContain('v-if="canDelete"');
|
||||
expect(source).not.toContain("row.created_by === auth.user?.id");
|
||||
});
|
||||
|
||||
it("opens FAQ detail from any row cell while preserving action button click isolation", () => {
|
||||
const source = read("../components/FaqList.vue");
|
||||
|
||||
expect(source).toContain('@row-click="onRowClick"');
|
||||
expect(source).toContain("const onRowClick = (row: FaqItem) =>");
|
||||
expect(source).not.toContain('column?.property !== "question"');
|
||||
expect(source).toContain('@click.stop="remove(scope.row)"');
|
||||
});
|
||||
|
||||
it("splits FAQ category create update and delete controls by backend operation permissions", () => {
|
||||
const source = read("../components/FaqCategoryPanel.vue");
|
||||
|
||||
expect(source).toContain('const canCreateCategory = computed(() => can("faq.category.create"))');
|
||||
expect(source).toContain('const canUpdateCategory = computed(() => can("faq.category.update"))');
|
||||
expect(source).toContain('const canDeleteCategory = computed(() => can("faq.category.delete"))');
|
||||
expect(source).toContain(':can-create="canCreateCategory"');
|
||||
expect(source).toContain(':can-update="canUpdateCategory"');
|
||||
expect(source).toContain(':can-delete="canDeleteCategory"');
|
||||
expect(source).not.toContain("const canDelete = computed(() => isAdmin.value)");
|
||||
});
|
||||
|
||||
it("uses FAQ update and reply delete permissions for detail actions that hit protected endpoints", () => {
|
||||
const source = read("./FaqDetail.vue");
|
||||
|
||||
expect(source).toContain('return can("faq.update")');
|
||||
expect(source).toContain('return can("faq.reply.delete")');
|
||||
expect(source).toContain('const canReadCategories = computed(() => can("faq.category.read"))');
|
||||
expect(source).toContain('const canReadMembers = computed(() => can("project.members.list"))');
|
||||
expect(source).toContain("if (canReadCategories.value)");
|
||||
expect(source).toContain("if (!canReadMembers.value)");
|
||||
expect(source).not.toContain("item.value.created_by === auth.user?.id");
|
||||
expect(source).not.toContain("reply.created_by === auth.user?.id");
|
||||
});
|
||||
});
|
||||
@@ -88,4 +88,11 @@ describe("Login protocol agreement", () => {
|
||||
expect(source).toContain("已安全退出");
|
||||
expect(source).toContain(".logout-notice");
|
||||
});
|
||||
|
||||
it("keeps the login card wide enough on desktop while remaining responsive", () => {
|
||||
const source = readLoginView();
|
||||
|
||||
expect(source).toContain("width: clamp(480px, 34vw, 560px);");
|
||||
expect(source).toContain("max-width: calc(100vw - 40px);");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -305,9 +305,8 @@ const onSubmit = async () => {
|
||||
.login-container {
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
width: 100%;
|
||||
max-width: 440px;
|
||||
padding: 0 20px;
|
||||
width: clamp(480px, 34vw, 560px);
|
||||
max-width: calc(100vw - 40px);
|
||||
}
|
||||
|
||||
.login-card {
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
|
||||
const readProfileView = () => readFileSync(resolve(__dirname, "./ProfileSettings.vue"), "utf8");
|
||||
|
||||
describe("profile settings autocomplete", () => {
|
||||
it("disables browser autofill for profile name, department, and current password", () => {
|
||||
const source = readProfileView();
|
||||
|
||||
expect(source).toContain('<el-form ref="formRef" :model="form" :rules="rules" label-width="112px" class="form" autocomplete="off">');
|
||||
expect(source).toContain('autocomplete="off"');
|
||||
expect(source).toContain('autocomplete="new-password"');
|
||||
expect(source).toContain('name="ctms-profile-current-password"');
|
||||
});
|
||||
});
|
||||
|
||||
describe("profile settings layout", () => {
|
||||
it("uses a settings-window layout, grouped sections, and aligned actions", () => {
|
||||
const source = readProfileView();
|
||||
|
||||
expect(source).not.toContain("<el-card");
|
||||
expect(source).toContain('class="profile-layout"');
|
||||
expect(source).toContain('class="avatar-panel"');
|
||||
expect(source).toContain('aria-label="关闭个人中心"');
|
||||
expect(source).toContain("@click=\"emit('close-request')\"");
|
||||
expect(source).toContain('saved: []');
|
||||
expect(source).toContain('emit("saved")');
|
||||
expect(source).toContain('class="form-section"');
|
||||
expect(source).toContain('class="form-section form-section--password"');
|
||||
expect(source).toContain("grid-template-columns: 260px minmax(0, 1fr);");
|
||||
expect(source).toContain("min-height: 620px;");
|
||||
expect(source).toContain("padding-left: 112px;");
|
||||
});
|
||||
});
|
||||
|
||||
describe("profile avatar upload", () => {
|
||||
it("limits avatar uploads to supported image MIME types", () => {
|
||||
const source = readProfileView();
|
||||
|
||||
expect(source).toContain('accept="image/png,image/jpeg,image/gif,image/webp"');
|
||||
expect(source).toContain(':before-upload="beforeAvatarUpload"');
|
||||
expect(source).toContain('const avatarAcceptedTypes = new Set(["image/png", "image/jpeg", "image/gif", "image/webp"])');
|
||||
expect(source).toContain("avatarAcceptedTypes.has(file.type)");
|
||||
expect(source).toContain("TEXT.modules.profile.avatarTypeInvalid");
|
||||
});
|
||||
});
|
||||
@@ -1,36 +0,0 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
|
||||
const readRegisterView = () => readFileSync(resolve(__dirname, "./Register.vue"), "utf8");
|
||||
|
||||
describe("Register protocol agreement", () => {
|
||||
it("opens readable service terms and privacy policy dialogs from the agreement text", () => {
|
||||
const source = readRegisterView();
|
||||
|
||||
expect(source).toContain('@click.stop.prevent="openProtocolDialog(\'terms\')"');
|
||||
expect(source).toContain('@click.stop.prevent="openProtocolDialog(\'privacy\')"');
|
||||
expect(source).toContain('v-model="protocolDialogVisible"');
|
||||
expect(source).toContain("activeProtocolTitle");
|
||||
expect(source).toContain("activeProtocolSections");
|
||||
expect(source).toContain("closeProtocolDialog");
|
||||
expect(source).toContain("serviceTermsSections");
|
||||
expect(source).toContain("privacyPolicySections");
|
||||
});
|
||||
|
||||
it("shows an in-page pending admin approval notice after successful registration", () => {
|
||||
const source = readRegisterView();
|
||||
const successFlagIndex = source.indexOf("registrationSubmitted.value = true");
|
||||
const resetIndex = source.indexOf("Object.assign(form");
|
||||
|
||||
expect(source).toContain('v-if="registrationSubmitted"');
|
||||
expect(source).toContain("注册申请已提交");
|
||||
expect(source).toContain("待管理员审核通过后方可登录");
|
||||
expect(source).toContain("管理员审核通过前,请勿重复提交注册申请");
|
||||
expect(source).toContain("registrationSubmitted");
|
||||
expect(source).not.toContain("approval-login-link");
|
||||
expect(successFlagIndex).toBeGreaterThan(-1);
|
||||
expect(resetIndex).toBeGreaterThan(-1);
|
||||
expect(successFlagIndex).toBeLessThan(resetIndex);
|
||||
});
|
||||
});
|
||||
@@ -1,27 +0,0 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
|
||||
const readSource = () => readFileSync(resolve(__dirname, "./StudyHome.vue"), "utf8");
|
||||
|
||||
describe("StudyHome role labels", () => {
|
||||
it("uses permission template names for the current project role label", () => {
|
||||
const source = readSource();
|
||||
|
||||
expect(source).toContain("useRoleTemplateMeta");
|
||||
expect(source).toContain("const { roleLabel: displayRoleLabel, loadRoleTemplates } = useRoleTemplateMeta();");
|
||||
expect(source).toContain("const roleLabel = computed(() => displayRoleLabel(projectRole.value));");
|
||||
expect(source).toContain("loadRoleTemplates();");
|
||||
expect(source).not.toContain("displayEnum(TEXT.enums.userRole, projectRole.value)");
|
||||
});
|
||||
|
||||
it("gates the finance KPI by the contract fee read permission", () => {
|
||||
const source = readSource();
|
||||
|
||||
expect(source).toContain('v-if="canReadFinanceContracts"');
|
||||
expect(source).toContain('const canReadFinanceContracts = computed');
|
||||
expect(source).toContain("isApiPermissionAllowed");
|
||||
expect(source).toContain('study.currentPermissions?.[role]?.["fees_contracts:read"]');
|
||||
expect(source).toContain('canReadFinanceContracts.value ? fetchFinanceSummary(studyId) : Promise.resolve(null)');
|
||||
});
|
||||
});
|
||||
@@ -1,65 +0,0 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
|
||||
const readProjectMembers = () => readFileSync(resolve(__dirname, "./ProjectMembers.vue"), "utf8");
|
||||
|
||||
describe("ProjectMembers user directory access", () => {
|
||||
it("does not load the admin-only global user directory", () => {
|
||||
const source = readProjectMembers();
|
||||
|
||||
expect(source).not.toContain("fetchUsers");
|
||||
expect(source).not.toContain("../../api/users");
|
||||
expect(source).toContain('const canListMembers = computed(() => permission.can("project.members.list"));');
|
||||
expect(source).toContain('const canListMemberCandidates = computed(() => permission.can("project.members.candidates"));');
|
||||
expect(source).toContain('const canCreateMember = computed(() => permission.can("project.members.create"));');
|
||||
expect(source).toContain('const canUpdateMember = computed(() => permission.can("project.members.update"));');
|
||||
expect(source).toContain('const canDeleteMember = computed(() => permission.can("project.members.delete"));');
|
||||
expect(source).toContain("if (!canListMembers.value");
|
||||
expect(source).toContain("if (!canListMemberCandidates.value");
|
||||
expect(source).toContain("return;");
|
||||
expect(source).toContain('v-if="canAddMember"');
|
||||
expect(source).toContain("listMemberCandidates(projectId.value, { limit: 500 })");
|
||||
expect(source).not.toContain("project.members.manage");
|
||||
});
|
||||
|
||||
it("uses the member API embedded user data when the global directory is unavailable", () => {
|
||||
const source = readProjectMembers();
|
||||
|
||||
expect(source).toContain("users.value = members.value");
|
||||
expect(source).toContain(".map((member) => member.user)");
|
||||
expect(source).toContain("users.value.find((u) => u.id === m.user_id) || m.user");
|
||||
});
|
||||
|
||||
it("prevents project managers from editing themselves or assigning higher roles", () => {
|
||||
const source = readProjectMembers();
|
||||
|
||||
expect(source).toContain("const roleRank: Record<string, number>");
|
||||
expect(source).toContain("if (row.user_id === auth.user?.id) return false;");
|
||||
expect(source).toContain("if (row.user?.is_admin) return false;");
|
||||
expect(source).toContain("const canAssignRole = (role: string)");
|
||||
expect(source).toContain(":disabled=\"!canAssignRole(role.value)\"");
|
||||
expect(source).toContain(':disabled="!canEditMember(scope.row)');
|
||||
expect(source).toContain(':disabled="!canDeleteProjectMember(scope.row)"');
|
||||
});
|
||||
|
||||
it("uses permission template names for project role display options", () => {
|
||||
const source = readProjectMembers();
|
||||
|
||||
expect(source).toContain("useRoleTemplateMeta");
|
||||
expect(source).toContain("const { roleLabel, roleOptionsFor, loadRoleTemplates } = useRoleTemplateMeta();");
|
||||
expect(source).toContain("const roleOptions = computed(() => roleOptionsFor(ROLE_KEYS));");
|
||||
expect(source).toContain("{{ roleLabel(scope.row.role_in_study) }}");
|
||||
expect(source).not.toContain("displayEnum(TEXT.enums.userRole, scope.row.role_in_study)");
|
||||
expect(source).not.toContain(':label="TEXT.enums.userRole.PM"');
|
||||
});
|
||||
|
||||
it("offers QA and CTA as project member role presets", () => {
|
||||
const source = readProjectMembers();
|
||||
|
||||
expect(source).toContain('"QA"');
|
||||
expect(source).toContain('"CTA"');
|
||||
expect(source).toContain("QA: 60");
|
||||
expect(source).toContain("CTA: 40");
|
||||
});
|
||||
});
|
||||
@@ -1,31 +0,0 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
|
||||
const readSiteForm = () => readFileSync(resolve(__dirname, "./SiteForm.vue"), "utf8");
|
||||
const readSiteTypes = () => readFileSync(resolve(__dirname, "../../types/api.ts"), "utf8");
|
||||
|
||||
describe("Admin site form phone field", () => {
|
||||
it("hydrates and submits the phone field", () => {
|
||||
const formSource = readSiteForm();
|
||||
const typeSource = readSiteTypes();
|
||||
|
||||
expect(formSource).toContain("form.phone = props.site.phone || (props.site as any)?.contact_phone || \"\"");
|
||||
expect(formSource).toContain("phone: form.phone");
|
||||
expect(formSource).toContain('v-model="form.phone"');
|
||||
expect(typeSource).toContain("phone?: string | null;");
|
||||
expect(typeSource).toContain("contact_phone?: string | null;");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Admin site PM user loading", () => {
|
||||
it("uses project member embedded user data instead of the global user directory", () => {
|
||||
const sitesSource = readFileSync(resolve(__dirname, "./Sites.vue"), "utf8");
|
||||
const formSource = readSiteForm();
|
||||
|
||||
expect(sitesSource).not.toContain("../../api/users");
|
||||
expect(sitesSource).not.toContain("fetchUsers");
|
||||
expect(sitesSource).toContain(".map((member) => member.user)");
|
||||
expect(formSource).toContain("m.user?.full_name");
|
||||
});
|
||||
});
|
||||
@@ -1,17 +0,0 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
|
||||
const readSource = () => readFileSync(resolve(__dirname, "./SystemMonitoringPage.vue"), "utf8");
|
||||
|
||||
describe("SystemMonitoringPage", () => {
|
||||
it("uses a fixed viewport shell so monitoring tabs manage their own scrolling", () => {
|
||||
const source = readSource();
|
||||
|
||||
expect(source).toContain("display: flex");
|
||||
expect(source).toContain("height: calc(100dvh - 48px)");
|
||||
expect(source).toContain("min-height: 0");
|
||||
expect(source).toContain("overflow: hidden");
|
||||
expect(source).not.toContain("min-height: calc(100dvh - 52px)");
|
||||
});
|
||||
});
|
||||
@@ -1,17 +0,0 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
|
||||
const readResetForm = () => readFileSync(resolve(__dirname, "./UserResetPassword.vue"), "utf8");
|
||||
|
||||
describe("Admin reset password autofill", () => {
|
||||
it("disables browser autofill for confirm username and manual password", () => {
|
||||
const source = readResetForm();
|
||||
|
||||
expect(source).toContain('autocomplete="off" name="ctms-reset-confirm-username"');
|
||||
expect(source).toContain('autocomplete="new-password"');
|
||||
expect(source).toContain('name="ctms-reset-temp-password"');
|
||||
expect(source).toContain('name="ctms-reset-manual-password"');
|
||||
expect(source).toContain(":prop=\"form.mode === 'auto' ? 'tempPassword' : 'manualPassword'\"");
|
||||
});
|
||||
});
|
||||
@@ -1,111 +0,0 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
|
||||
const readUsersView = () => readFileSync(resolve(__dirname, "./Users.vue"), "utf8");
|
||||
const readUserForm = () => readFileSync(resolve(__dirname, "./UserForm.vue"), "utf8");
|
||||
const readLocale = () => readFileSync(resolve(__dirname, "../../locales/zh-CN.ts"), "utf8");
|
||||
|
||||
describe("Admin users table labels", () => {
|
||||
it("shows clinical_department as 科室 in the account management table", () => {
|
||||
const viewSource = readUsersView();
|
||||
const localeSource = readLocale();
|
||||
|
||||
expect(viewSource).toContain(':label="TEXT.modules.adminUsers.clinicalDepartmentLabel"');
|
||||
expect(viewSource).not.toContain(':label="TEXT.common.fields.clinicalDepartment"');
|
||||
expect(localeSource).toContain('clinicalDepartmentLabel: "科室"');
|
||||
});
|
||||
});
|
||||
|
||||
describe("Admin users table layout", () => {
|
||||
it("uses a wider name column, even remaining columns, and compact icon actions", () => {
|
||||
const viewSource = readUsersView();
|
||||
|
||||
expect(viewSource).toContain('table-layout="fixed"');
|
||||
expect(viewSource).toContain(':label="TEXT.common.fields.name" width="360"');
|
||||
expect(viewSource).not.toContain('width="20%"');
|
||||
expect(viewSource).not.toContain('width="180"');
|
||||
expect(viewSource).not.toContain('width="160"');
|
||||
expect(viewSource).not.toContain('width="100"');
|
||||
expect(viewSource).not.toContain('fixed="right"');
|
||||
expect(viewSource).toContain('class="action-btn"');
|
||||
expect(viewSource).toContain("width: 28px;");
|
||||
expect(viewSource).toContain("gap: 4px;");
|
||||
});
|
||||
|
||||
it("shows names without avatar icons in the account management table", () => {
|
||||
const viewSource = readUsersView();
|
||||
|
||||
expect(viewSource).not.toContain("user-avatar");
|
||||
expect(viewSource).not.toContain("getInitials(scope.row.full_name)");
|
||||
expect(viewSource).not.toContain("linear-gradient(135deg, #3f8f6b, #2d7a5a)");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Admin users summary header", () => {
|
||||
it("keeps the management summary header compact", () => {
|
||||
const viewSource = readUsersView();
|
||||
|
||||
expect(viewSource).toContain("padding: 10px 16px;");
|
||||
expect(viewSource).toContain("padding: 9px 12px;");
|
||||
expect(viewSource).toContain("width: 32px;");
|
||||
expect(viewSource).toContain("height: 32px;");
|
||||
expect(viewSource).toContain("font-size: 20px;");
|
||||
expect(viewSource).toContain("font-size: 11px;");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Admin users filters", () => {
|
||||
it("applies keyword automatically and status filters from the first page", () => {
|
||||
const viewSource = readUsersView();
|
||||
|
||||
expect(viewSource).toContain("@keyup.enter=\"applyFilters\"");
|
||||
expect(viewSource).toContain("@change=\"applyFilters\"");
|
||||
expect(viewSource).not.toContain("@click=\"applyFilters\"");
|
||||
expect(viewSource).not.toContain(":icon=\"Refresh\"");
|
||||
expect(viewSource).not.toContain("Refresh } from");
|
||||
expect(viewSource).toContain("watch(searchKeyword, () => {");
|
||||
expect(viewSource).toContain("scheduleKeywordSearch();");
|
||||
expect(viewSource).toContain("keywordSearchTimer = setTimeout(() => {");
|
||||
expect(viewSource).toContain("}, 300);");
|
||||
expect(viewSource).toContain("page.value = 1");
|
||||
expect(viewSource).toContain("keyword: searchKeyword.value || undefined");
|
||||
expect(viewSource).toContain("status: statusFilter.value || undefined");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Admin user form autocomplete", () => {
|
||||
it("prevents browser credential autofill when creating a user", () => {
|
||||
const formSource = readUserForm();
|
||||
|
||||
expect(formSource).toContain('autocomplete="off"');
|
||||
expect(formSource).toContain('name="ctms-create-initial-password"');
|
||||
expect(formSource).toContain('autocomplete="new-password"');
|
||||
expect(formSource).not.toContain("current_password");
|
||||
expect(formSource).not.toContain("confirmPassword");
|
||||
expect(formSource).not.toContain("TEXT.modules.profile.currentPassword");
|
||||
expect(formSource).not.toContain("TEXT.modules.profile.confirmPassword");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Admin user create form password", () => {
|
||||
it("requires an initial password before creating an account", () => {
|
||||
const formSource = readUserForm();
|
||||
|
||||
expect(formSource).toContain('v-if="!user" :label="TEXT.modules.adminUsers.passwordInitial" prop="password"');
|
||||
expect(formSource).toContain("TEXT.modules.adminUsers.passwordInitialRequired");
|
||||
expect(formSource).toContain("/^(?=.*[A-Za-z])(?=.*\\d).{8,}$/");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Admin user edit form defaults", () => {
|
||||
it("hydrates the selected user immediately when the dialog is mounted open", () => {
|
||||
const formSource = readUserForm();
|
||||
|
||||
expect(formSource).toContain("immediate: true");
|
||||
expect(formSource).not.toContain("current_password");
|
||||
expect(formSource).not.toContain("confirmPassword");
|
||||
expect(formSource).not.toContain("TEXT.modules.profile.currentPassword");
|
||||
expect(formSource).not.toContain("TEXT.modules.profile.confirmPassword");
|
||||
});
|
||||
});
|
||||
@@ -1,66 +0,0 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
const readView = (path: string) => readFileSync(resolve(__dirname, path), "utf8");
|
||||
|
||||
describe("detail breadcrumb contexts", () => {
|
||||
it("uses contract numbers for contract fee detail breadcrumbs", () => {
|
||||
const source = readView("./fees/ContractFeeDetail.vue");
|
||||
|
||||
expect(source).toContain("study.setViewContext({");
|
||||
expect(source).toContain("siteName");
|
||||
expect(source).toContain("pageTitle: detail.contract_no || TEXT.menu.feeContracts");
|
||||
});
|
||||
|
||||
it("uses shipment tracking or batch numbers for drug shipment detail breadcrumbs", () => {
|
||||
const source = readView("./drug/ShipmentDetail.vue");
|
||||
|
||||
expect(source).toContain("study.setViewContext({");
|
||||
expect(source).toContain("siteName: detail.site_name || TEXT.common.fallback");
|
||||
expect(source).toContain("pageTitle: detail.tracking_no || detail.batch_no || TEXT.modules.drugShipments.detailTitle");
|
||||
});
|
||||
|
||||
it("uses equipment names for material equipment detail breadcrumbs", () => {
|
||||
const source = readView("./materials/MaterialEquipmentDetail.vue");
|
||||
|
||||
expect(source).toContain("study.setViewContext({");
|
||||
expect(source).toContain("pageTitle: detail.name || TEXT.modules.materialEquipment.detailTitle");
|
||||
});
|
||||
|
||||
it("uses subject numbers and centers for subject detail breadcrumbs", () => {
|
||||
const source = readView("./subjects/SubjectDetail.vue");
|
||||
|
||||
expect(source).toContain("study.setViewContext({");
|
||||
expect(source).toContain("siteName: siteMap.value[detail.site_id] || TEXT.common.fallback");
|
||||
expect(source).toContain("pageTitle: detail.subject_no || TEXT.modules.subjectManagement.screeningNo");
|
||||
});
|
||||
|
||||
it("uses startup record identifiers for startup detail breadcrumbs", () => {
|
||||
const feasibility = readView("./startup/FeasibilityDetail.vue");
|
||||
const ethics = readView("./startup/EthicsDetail.vue");
|
||||
const kickoff = readView("./startup/KickoffDetail.vue");
|
||||
const training = readView("./startup/TrainingDetail.vue");
|
||||
|
||||
expect(feasibility).toContain("pageTitle: detail.project_no ||");
|
||||
expect(ethics).toContain("pageTitle: detail.approval_no ||");
|
||||
expect(kickoff).toContain("pageTitle: siteInfo.name ? `${siteInfo.name} ${TEXT.modules.startupMeetingAuth.kickoffTab}`");
|
||||
expect(training).toContain("pageTitle: detail.name || TEXT.modules.startupMeetingAuth.trainingDetailTitle");
|
||||
});
|
||||
|
||||
it("uses knowledge item titles for knowledge detail breadcrumbs", () => {
|
||||
const precaution = readView("./knowledge/PrecautionDetail.vue");
|
||||
const faq = readView("./FaqDetail.vue");
|
||||
|
||||
expect(precaution).toContain("pageTitle: detail.title || TEXT.modules.knowledgeNotes.detailTitle");
|
||||
expect(faq).toContain("const questionTitle = String(faqData.question || \"\").trim()");
|
||||
expect(faq).toContain("pageTitle: questionTitle ? questionTitle.slice(0, 24) : TEXT.modules.knowledgeMedicalConsult.detailTitle");
|
||||
});
|
||||
|
||||
it("uses project names for admin project detail breadcrumbs", () => {
|
||||
const source = readView("./admin/ProjectDetail.vue");
|
||||
|
||||
expect(source).toContain("studyStore.setViewContext({");
|
||||
expect(source).toContain("pageTitle: project.value.name || TEXT.modules.adminProjects.detailTitle");
|
||||
});
|
||||
});
|
||||
@@ -1,31 +0,0 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
const readView = (relativePath: string) => readFileSync(resolve(__dirname, relativePath), "utf8");
|
||||
|
||||
const detailViews = [
|
||||
"./fees/ContractFeeDetail.vue",
|
||||
"./drug/ShipmentDetail.vue",
|
||||
"./materials/MaterialEquipmentDetail.vue",
|
||||
"./documents/DocumentDetail.vue",
|
||||
"./subjects/SubjectDetail.vue",
|
||||
"./knowledge/PrecautionDetail.vue",
|
||||
"./startup/FeasibilityDetail.vue",
|
||||
"./startup/EthicsDetail.vue",
|
||||
"./startup/KickoffDetail.vue",
|
||||
"./startup/TrainingDetail.vue",
|
||||
];
|
||||
|
||||
describe("detail page navigation", () => {
|
||||
it("uses header breadcrumbs instead of page-local back actions", () => {
|
||||
for (const viewPath of detailViews) {
|
||||
const source = readView(viewPath);
|
||||
|
||||
expect(source, viewPath).not.toContain("TEXT.common.actions.back");
|
||||
expect(source, viewPath).not.toContain('@click="goBack"');
|
||||
expect(source, viewPath).not.toContain("const goBack =");
|
||||
expect(source, viewPath).not.toContain("function goBack");
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,21 +0,0 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
|
||||
const readSource = () => readFileSync(resolve(__dirname, "./DocumentList.vue"), "utf8");
|
||||
|
||||
describe("DocumentList project permissions", () => {
|
||||
it("hides document create/update/delete controls and guards direct mutation calls by backend permissions", () => {
|
||||
const source = readSource();
|
||||
|
||||
expect(source).toContain('v-if="canCreate"');
|
||||
expect(source).toContain('v-if="canUpdate"');
|
||||
expect(source).toContain('v-if="canDelete"');
|
||||
expect(source).toContain("if (!canCreate.value)");
|
||||
expect(source).toContain("if (!canUpdate.value)");
|
||||
expect(source).toContain("if (!canDelete.value)");
|
||||
expect(source).toContain("if (isInactiveSite(row.site_id))");
|
||||
expect(source).toContain('@click.stop="openEdit(row)"');
|
||||
expect(source).not.toContain(':disabled="!canCreate"');
|
||||
});
|
||||
});
|
||||
@@ -1,60 +0,0 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
|
||||
const readSource = () => readFileSync(resolve(__dirname, "./DrugShipmentEditorDrawer.vue"), "utf8");
|
||||
|
||||
describe("DrugShipmentEditorDrawer attachments", () => {
|
||||
it("uses a compact upload card instead of the full attachment table", () => {
|
||||
const source = readSource();
|
||||
|
||||
expect(source).toContain("AttachmentList");
|
||||
expect(source).toContain('ref="attachmentPanelRef"');
|
||||
expect(source).toContain('entity-type="drug_shipment"');
|
||||
expect(source).toContain(':mode="\'upload\'"');
|
||||
expect(source).toContain(':center-upload-card-content="true"');
|
||||
expect(source).toContain("attachmentPanelRef.value?.pendingSnapshot() || []");
|
||||
expect(source).toContain("await attachmentPanelRef.value?.uploadPending(props.shipmentId)");
|
||||
expect(source).toContain("uploadPending");
|
||||
expect(source).not.toContain("uploadAttachment");
|
||||
expect(source).not.toContain("pendingFiles");
|
||||
expect(source).not.toContain("upload-card-label");
|
||||
expect(source).not.toContain("<el-table");
|
||||
});
|
||||
});
|
||||
|
||||
describe("DrugShipmentEditorDrawer shipment validation", () => {
|
||||
it("keeps shipment execution fields optional while status is pending", () => {
|
||||
const source = readSource();
|
||||
|
||||
expect(source).toContain("requiresShipmentDetails");
|
||||
expect(source).toContain('status !== "PENDING"');
|
||||
expect(source).not.toContain('ship_date: [{ required: true');
|
||||
expect(source).not.toContain('batch_no: [{ required: true');
|
||||
expect(source).not.toContain('carrier: [{ required: true');
|
||||
expect(source).not.toContain('tracking_no: [{ required: true');
|
||||
});
|
||||
|
||||
it("does not expose the removed returned status", () => {
|
||||
const source = readSource();
|
||||
|
||||
expect(source).not.toContain("RETURNED");
|
||||
expect(source).not.toContain("已回收");
|
||||
});
|
||||
|
||||
it("requires receive date when shipment is signed", () => {
|
||||
const source = readSource();
|
||||
|
||||
expect(source).toContain("requiresReceiveDate");
|
||||
expect(source).toContain('status === "SIGNED"');
|
||||
expect(source).not.toContain('receive_date: [{ required: true');
|
||||
});
|
||||
|
||||
it("keeps remark optional unless shipment is exceptional", () => {
|
||||
const source = readSource();
|
||||
|
||||
expect(source).toContain("requiresRemark");
|
||||
expect(source).toContain('status === "EXCEPTION"');
|
||||
expect(source).not.toContain('remark: [{ required: true');
|
||||
});
|
||||
});
|
||||
@@ -1,25 +0,0 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
|
||||
const readSource = () => readFileSync(resolve(__dirname, "./ShipmentDetail.vue"), "utf8");
|
||||
|
||||
describe("ShipmentDetail edit drawer", () => {
|
||||
it("shows an edit action and opens the shipment editor drawer on the detail page", () => {
|
||||
const source = readSource();
|
||||
|
||||
expect(source).toContain("<DrugShipmentEditorDrawer");
|
||||
expect(source).toContain('v-if="canUpdateShipment"');
|
||||
expect(source).toContain("editorVisible.value = true");
|
||||
expect(source).toContain("const handleEditorSaved = () =>");
|
||||
expect(source).toContain('"drug_shipments:update"');
|
||||
expect(source).toContain(":readonly=\"isReadOnly\"");
|
||||
});
|
||||
|
||||
it("hides the upload button in the detail attachment table", () => {
|
||||
const source = readSource();
|
||||
|
||||
expect(source).toContain("<AttachmentList");
|
||||
expect(source).toContain(":hide-uploader=\"true\"");
|
||||
});
|
||||
});
|
||||
@@ -1,68 +0,0 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
|
||||
const readDetail = () => readFileSync(resolve(__dirname, "./ContractFeeDetail.vue"), "utf8");
|
||||
|
||||
describe("ContractFeeDetail permissions", () => {
|
||||
it("suppresses optional site lookup permission errors on the detail page", () => {
|
||||
const source = readDetail();
|
||||
|
||||
expect(source).toContain('fetchSites(studyId.value, { limit: 500 }, { suppressErrorMessage: true })');
|
||||
});
|
||||
|
||||
it("opens the edit drawer on the detail page without navigating back to the list", () => {
|
||||
const source = readDetail();
|
||||
|
||||
expect(source).toContain("<ContractFeeEditorDrawer");
|
||||
expect(source).toContain("editorVisible.value = true");
|
||||
expect(source).toContain("const handleEditorSaved = () =>");
|
||||
expect(source).not.toContain('router.push({ path: "/fees/contracts"');
|
||||
expect(source).not.toContain("editContractId");
|
||||
});
|
||||
|
||||
it("renders contract fee attachments as one aggregated table with attachment type labels", () => {
|
||||
const source = readDetail();
|
||||
|
||||
expect(source).toContain("<AttachmentList");
|
||||
expect(source).toContain(":entity-groups=\"attachmentGroups\"");
|
||||
expect(source).toContain(":refresh-key=\"attachmentRefreshKey\"");
|
||||
expect(source).toContain("attachmentRefreshKey.value += 1");
|
||||
expect(source).not.toContain("<FeeAttachmentPanel");
|
||||
expect(source).toContain('entityType: "contract_fee_contract"');
|
||||
expect(source).toContain('entityType: "contract_fee_voucher"');
|
||||
expect(source).toContain('entityType: "contract_fee_invoice"');
|
||||
});
|
||||
|
||||
it("does not show attachment group descriptions on the detail page", () => {
|
||||
const source = readDetail();
|
||||
|
||||
expect(source).not.toContain("attachmentContractDesc");
|
||||
expect(source).not.toContain("attachmentVoucherDesc");
|
||||
expect(source).not.toContain("attachmentInvoiceDesc");
|
||||
});
|
||||
|
||||
it("keeps payment table columns evenly distributed with clear labels", () => {
|
||||
const source = readDetail();
|
||||
|
||||
expect(source).toContain('v-for="(payment, index) in detail.payments"');
|
||||
expect(source).toContain("TEXT.modules.feeContracts.paymentSeq");
|
||||
expect(source).toContain("TEXT.modules.feeContracts.paidFlag");
|
||||
expect(source).toContain("TEXT.modules.feeContracts.verifiedFlag");
|
||||
expect(source).toContain("tl-panel-amount");
|
||||
expect(source).not.toContain('width="20%"');
|
||||
expect(source).not.toContain(".payment-table :deep(col:nth-child(-n + 5))");
|
||||
});
|
||||
|
||||
it("uses ten-thousand yuan as the only amount unit and hides currency", () => {
|
||||
const source = readDetail();
|
||||
|
||||
expect(source).toContain("formatAmountWan(detail.contract_amount)");
|
||||
expect(source).toContain("formatAmountWan(payment.amount)");
|
||||
expect(source).toContain('<span class="stat-tile-unit">万</span>');
|
||||
expect(source).toContain('<span class="amount-unit-sm">万</span>');
|
||||
expect(source).not.toContain("TEXT.common.fields.currency");
|
||||
expect(source).not.toContain("detail.currency");
|
||||
expect(source).not.toContain("currencyDefault");
|
||||
});
|
||||
});
|
||||
@@ -1,88 +0,0 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
|
||||
const readDrawer = () => readFileSync(resolve(__dirname, "./ContractFeeEditorDrawer.vue"), "utf8");
|
||||
const readLocale = () => readFileSync(resolve(__dirname, "../../locales/zh-CN.ts"), "utf8");
|
||||
|
||||
describe("ContractFeeEditorDrawer attachments", () => {
|
||||
it("uses upload-card mode for attachments instead of the detail table", () => {
|
||||
const source = readDrawer();
|
||||
|
||||
expect(source).toContain('ref="attachmentPanelRef"');
|
||||
expect(source).toContain("<AttachmentList");
|
||||
expect(source).toContain(":entity-groups=\"attachmentGroups\"");
|
||||
expect(source).not.toContain("PendingAttachmentUpload");
|
||||
expect(source).not.toContain("FeeAttachmentPanel");
|
||||
expect(source).not.toContain("attachmentContractDesc");
|
||||
expect(source).not.toContain("attachmentVoucherDesc");
|
||||
expect(source).not.toContain("attachmentInvoiceDesc");
|
||||
});
|
||||
|
||||
it("uploads selected attachments only after the form save succeeds", () => {
|
||||
const source = readDrawer();
|
||||
|
||||
expect(source).toContain("const attachmentPanelRef = ref<InstanceType<typeof AttachmentList> | null>(null)");
|
||||
expect(source).toContain("await attachmentPanelRef.value?.uploadPending(savedId)");
|
||||
expect(source).toContain("attachments: attachmentPanelRef.value?.pendingSnapshot() || []");
|
||||
expect(source.indexOf("await attachmentPanelRef.value?.uploadPending(savedId)")).toBeLessThan(
|
||||
source.indexOf('emit("update:modelValue", false)')
|
||||
);
|
||||
});
|
||||
|
||||
it("shows attachment upload cards while creating a new contract fee", () => {
|
||||
const source = readDrawer();
|
||||
|
||||
expect(source).toContain('class="form-section"');
|
||||
expect(source).toContain(':entity-id="form.id"');
|
||||
expect(source).toContain("<AttachmentList");
|
||||
expect(source).toContain(':center-upload-card-content="true"');
|
||||
expect(source).toContain(':upload-card-columns="3"');
|
||||
expect(source).not.toContain(':hide-upload-card-labels="true"');
|
||||
expect(source).not.toContain('<div v-if="form.id" class="form-group">');
|
||||
});
|
||||
|
||||
it("uses centered three-column upload cards with contract-specific copy", () => {
|
||||
const source = readDrawer();
|
||||
|
||||
expect(source).toContain('uploadText: "点击上传合同"');
|
||||
expect(source).toContain('uploadText: "点击上传凭证"');
|
||||
expect(source).toContain('uploadText: "点击上传发票"');
|
||||
expect(source).toContain(':center-upload-card-content="true"');
|
||||
expect(source).toContain(':upload-card-columns="3"');
|
||||
expect(source).not.toContain(':hide-upload-card-labels="true"');
|
||||
});
|
||||
|
||||
it("labels the section as attachments instead of contract attachments", () => {
|
||||
const source = readLocale();
|
||||
|
||||
expect(source).toContain('attachmentTitle: "附件"');
|
||||
expect(source).not.toContain('attachmentTitle: "合同附件"');
|
||||
});
|
||||
|
||||
it("uses a payment grid that keeps date pickers inside their columns", () => {
|
||||
const source = readDrawer();
|
||||
|
||||
expect(source).toContain('class="field-grid field-grid--3"');
|
||||
expect(source).toContain('class="status-row"');
|
||||
expect(source).toContain('class="status-datepicker"');
|
||||
expect(source).toContain("grid-template-columns: 1fr 1fr 1fr;");
|
||||
expect(source).toContain("min-width: 0;");
|
||||
expect(source).toContain(".status-datepicker :deep(.el-date-editor.el-input)");
|
||||
});
|
||||
|
||||
it("uses ten-thousand yuan as the only amount unit and removes currency selection", () => {
|
||||
const source = readDrawer();
|
||||
|
||||
expect(source).toContain(':label="`${TEXT.modules.feeContracts.contractAmount}(万元)`"');
|
||||
expect(source).toContain(':label="`${TEXT.common.fields.amount}(万元)`"');
|
||||
expect(source).toContain("contract_amount: Number(detail.contract_amount || 0) / 10000");
|
||||
expect(source).toContain("amount: Number(payment.amount || 0) / 10000");
|
||||
expect(source).toContain("contract_amount: Number(form.contract_amount || 0) * 10000");
|
||||
expect(source).toContain("amount: Number(payment.amount || 0) * 10000");
|
||||
expect(source).not.toContain("prop=\"currency\"");
|
||||
expect(source).not.toContain("form.currency");
|
||||
expect(source).not.toContain("payload.currency");
|
||||
expect(source).not.toContain("currency:");
|
||||
});
|
||||
});
|
||||
@@ -1,13 +0,0 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { TEXT } from "../../locales";
|
||||
|
||||
describe("contract fee attachment labels", () => {
|
||||
it("uses a concise attachment section title", () => {
|
||||
expect(TEXT.modules.feeContracts.attachmentTitle).toBe("附件");
|
||||
});
|
||||
|
||||
it("uses a clear payment sequence column label", () => {
|
||||
expect(TEXT.modules.feeContracts.paymentSeqColumn).toBe("付款期次");
|
||||
expect(TEXT.modules.feeContracts.paymentSeq).toBe("第");
|
||||
});
|
||||
});
|
||||
@@ -1,25 +0,0 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
|
||||
const read = (relativePath: string) => readFileSync(resolve(__dirname, relativePath), "utf8");
|
||||
|
||||
describe("Contract fee project permissions", () => {
|
||||
it("hides list create/delete controls when the matching backend operation is not allowed", () => {
|
||||
const source = read("./ContractFees.vue");
|
||||
|
||||
expect(source).toContain('v-if="canCreate"');
|
||||
expect(source).toContain('v-if="canDelete"');
|
||||
expect(source).toContain("if (!canCreate.value)");
|
||||
expect(source).toContain("if (!canDelete.value)");
|
||||
expect(source).not.toContain(':disabled="!canCreate"');
|
||||
expect(source).not.toContain(':disabled="!canDelete || isInactiveSite(scope.row.center_id)"');
|
||||
});
|
||||
|
||||
it("hides detail edit controls and blocks direct navigation without update permission", () => {
|
||||
const source = read("./ContractFeeDetail.vue");
|
||||
|
||||
expect(source).toContain('v-if="canWrite"');
|
||||
expect(source).toContain("if (!canWrite.value)");
|
||||
});
|
||||
});
|
||||
@@ -1,16 +0,0 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
|
||||
const readSource = () => readFileSync(resolve(__dirname, "./EtmfPlaceholder.vue"), "utf8");
|
||||
|
||||
describe("EtmfPlaceholder project permissions", () => {
|
||||
it("hides eTMF create controls and guards dialog submissions with document create permission", () => {
|
||||
const source = readSource();
|
||||
|
||||
expect(source).toContain('v-if="canCreate"');
|
||||
expect(source).toContain('v-if="canCreate && selectedNode"');
|
||||
expect(source).toContain("if (!canCreate.value)");
|
||||
expect(source).not.toContain(':disabled="!canCreate"');
|
||||
});
|
||||
});
|
||||
@@ -1,71 +0,0 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
|
||||
const readSource = () => readFileSync(resolve(__dirname, "./MaterialEquipment.vue"), "utf8");
|
||||
|
||||
describe("MaterialEquipment project permissions", () => {
|
||||
it("projects material equipment create update and delete permissions into all write actions", () => {
|
||||
const source = readSource();
|
||||
|
||||
expect(source).toContain("useAuthStore");
|
||||
expect(source).toContain("isSystemAdmin");
|
||||
expect(source).toContain("isApiPermissionAllowed");
|
||||
expect(source).toContain('["material_equipments:create"]');
|
||||
expect(source).toContain('["material_equipments:update"]');
|
||||
expect(source).toContain('["material_equipments:delete"]');
|
||||
expect(source).toContain('v-if="canCreate"');
|
||||
expect(source).toContain('v-if="canUpdate"');
|
||||
expect(source).toContain('v-if="canDelete"');
|
||||
expect(source).toContain("if (!canDelete.value)");
|
||||
});
|
||||
|
||||
it("keeps the visible table columns evenly distributed", () => {
|
||||
const source = readSource();
|
||||
|
||||
expect(source).toContain('class="equipment-table"');
|
||||
expect(source).toContain('style="width: 100%"');
|
||||
expect(source).toContain('table-layout="fixed"');
|
||||
expect(source).toContain('label="操作"');
|
||||
expect(source).not.toMatch(/<el-table-column[^>]+prop="(?:name|specModel|unit|brand)"[^>]+(?:width|min-width)=/);
|
||||
});
|
||||
|
||||
it("opens the detail page from row clicks instead of a detail action button", () => {
|
||||
const source = readSource();
|
||||
|
||||
expect(source).toContain('@row-click="onRowClick"');
|
||||
expect(source).toContain(':row-class-name="equipmentRowClass"');
|
||||
expect(source).toContain('name: "MaterialEquipmentDetail"');
|
||||
expect(source).toContain("equipmentId: row.id");
|
||||
expect(source).not.toContain(">详情</el-button>");
|
||||
expect(source).toContain('@click.stop="openEdit(row)"');
|
||||
expect(source).toContain('@click.stop="removeRow(row)"');
|
||||
});
|
||||
|
||||
it("uploads qualification files as equipment attachments after saving the drawer form", () => {
|
||||
const source = readSource();
|
||||
const calibrationIndex = source.indexOf("校准设置");
|
||||
const qualificationIndex = source.indexOf("附件");
|
||||
|
||||
expect(calibrationIndex).toBeGreaterThan(-1);
|
||||
expect(qualificationIndex).toBeGreaterThan(calibrationIndex);
|
||||
expect(source).toContain("AttachmentList");
|
||||
expect(source).toContain('ref="attachmentPanelRef"');
|
||||
expect(source).toContain('entity-type="material_equipment"');
|
||||
expect(source).toContain(':entity-id="editingId"');
|
||||
expect(source).toContain(':mode="\'upload\'"');
|
||||
expect(source).toContain("attachmentPanelRef.value?.pendingSnapshot() || []");
|
||||
expect(source).toContain("await attachmentPanelRef.value?.uploadPending(equipmentId)");
|
||||
expect(source).toContain("uploadPending");
|
||||
expect(source).toContain("material_equipment");
|
||||
expect(source).not.toContain("uploadAttachment");
|
||||
expect(source).not.toContain("pendingFiles");
|
||||
expect(source).not.toContain("qualificationRows");
|
||||
expect(source).not.toContain("material_equipment_production_permit");
|
||||
expect(source).not.toContain("material_equipment_tech_index");
|
||||
expect(source).not.toContain("资质文件");
|
||||
expect(source).not.toContain("生产许可证");
|
||||
expect(source).not.toContain("技术指标");
|
||||
expect(source).not.toContain("handleUploadChange");
|
||||
});
|
||||
});
|
||||
@@ -1,56 +0,0 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
|
||||
const readSource = (relativePath: string) => readFileSync(resolve(__dirname, relativePath), "utf8");
|
||||
|
||||
describe("precautions project permissions", () => {
|
||||
it("splits list create and delete controls by backend operation permissions", () => {
|
||||
const source = readSource("./Precautions.vue");
|
||||
|
||||
expect(source).toContain('const canCreatePrecaution = computed(() => can("precautions.create"))');
|
||||
expect(source).toContain('const canDeletePrecaution = computed(() => can("precautions.delete"))');
|
||||
expect(source).toContain('v-if="canCreatePrecaution"');
|
||||
expect(source).toContain('v-if="canDeletePrecaution"');
|
||||
expect(source).toContain("if (!canDeletePrecaution.value)");
|
||||
expect(source).not.toContain("canWritePrecautions");
|
||||
});
|
||||
});
|
||||
|
||||
describe("precautions drawer editor", () => {
|
||||
it("opens create form in a drawer instead of routing to the form page", () => {
|
||||
const source = readSource("./Precautions.vue");
|
||||
|
||||
expect(source).toContain("PrecautionEditorDrawer");
|
||||
expect(source).toContain("precautionDrawerVisible");
|
||||
expect(source).not.toContain('router.push("/knowledge/precautions/new")');
|
||||
});
|
||||
|
||||
it("uses selects for site and level fields in the create drawer", () => {
|
||||
const source = readSource("../knowledge/PrecautionEditorDrawer.vue");
|
||||
|
||||
expect(source).toContain('<el-select v-model="form.site_name"');
|
||||
expect(source).toContain('v-for="site in sites"');
|
||||
expect(source).toContain(':value="site.name"');
|
||||
expect(source).toContain('<el-select v-model="form.level"');
|
||||
expect(source).toContain("const levelOptions = precautionLevelOptions");
|
||||
expect(source).toContain('v-for="level in levelOptions"');
|
||||
expect(source).not.toContain('<el-input v-model="form.site_name"');
|
||||
expect(source).not.toContain('<el-input v-model="form.level"');
|
||||
});
|
||||
|
||||
it("moves precaution attachment upload into the editor drawer", () => {
|
||||
const drawer = readSource("../knowledge/PrecautionEditorDrawer.vue");
|
||||
const detail = readSource("../knowledge/PrecautionDetail.vue");
|
||||
|
||||
expect(drawer).toContain("AttachmentList");
|
||||
expect(drawer).toContain('ref="attachmentPanelRef"');
|
||||
expect(drawer).toContain(':entity-type="\'precaution\'"');
|
||||
expect(drawer).toContain(':mode="\'upload\'"');
|
||||
expect(drawer).toContain("attachments: attachmentPanelRef.value?.pendingSnapshot() || []");
|
||||
expect(drawer).toContain("await attachmentPanelRef.value?.uploadPending(id)");
|
||||
expect(detail).toContain(":hide-uploader=\"true\"");
|
||||
expect(detail).toContain(":refresh-key=\"attachmentRefreshKey\"");
|
||||
expect(detail).toContain("attachmentRefreshKey.value += 1");
|
||||
});
|
||||
});
|
||||
@@ -1,15 +0,0 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
|
||||
const readSource = () => readFileSync(resolve(__dirname, "./ProjectMilestones.vue"), "utf8");
|
||||
|
||||
describe("ProjectMilestones project permissions", () => {
|
||||
it("hides edit controls and blocks saves without project milestone update permission", () => {
|
||||
const source = readSource();
|
||||
|
||||
expect(source).toContain('const canUpdateMilestones = computed(() => can("project.milestones.update"))');
|
||||
expect(source).toContain('v-if="canUpdateMilestones"');
|
||||
expect(source).toContain("if (!canUpdateMilestones.value)");
|
||||
});
|
||||
});
|
||||
@@ -1,34 +0,0 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
|
||||
const readRiskIssueMonitoringVisits = () => readFileSync(resolve(__dirname, "./RiskIssueMonitoringVisits.vue"), "utf8");
|
||||
|
||||
describe("RiskIssueMonitoringVisits permissions", () => {
|
||||
it("gates monitoring visit issue actions by concrete API permissions", () => {
|
||||
const source = readRiskIssueMonitoringVisits();
|
||||
|
||||
expect(source).toContain("useAuthStore");
|
||||
expect(source).toContain("isSystemAdmin");
|
||||
expect(source).toContain("getProjectRole");
|
||||
expect(source).toContain("isApiPermissionAllowed");
|
||||
[
|
||||
"monitoring_issues:read",
|
||||
"monitoring_issues:read",
|
||||
"monitoring_issues:create",
|
||||
"monitoring_issues:update",
|
||||
"monitoring_issues:delete",
|
||||
].forEach((operationKey) => {
|
||||
expect(source).toContain(operationKey);
|
||||
});
|
||||
expect(source).toContain('v-if="canCreateIssue"');
|
||||
expect(source).toContain('v-if="canListIssues"');
|
||||
expect(source).toContain('v-if="canReadIssue"');
|
||||
expect(source).toContain('v-if="canUpdateIssue"');
|
||||
expect(source).toContain('v-if="canDeleteIssue"');
|
||||
expect(source).toContain('v-if="canSaveIssue"');
|
||||
expect(source).not.toContain(':disabled="!canSaveIssue"');
|
||||
expect(source).toContain("const canSaveIssue = computed(() => (formMode.value === \"edit\" ? canUpdateIssue.value : canCreateIssue.value));");
|
||||
expect(source).toContain('ElMessage.warning("权限不足")');
|
||||
});
|
||||
});
|
||||
@@ -1,17 +0,0 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
|
||||
const readRiskIssueSae = () => readFileSync(resolve(__dirname, "./RiskIssueSae.vue"), "utf8");
|
||||
|
||||
describe("RiskIssueSae status", () => {
|
||||
it("only exposes follow-up and closed statuses in the risk issue status column", () => {
|
||||
const source = readRiskIssueSae();
|
||||
|
||||
expect(source).toContain("const aeStatusOptions = [");
|
||||
expect(source).toContain('{ value: "FOLLOW_UP", label: TEXT.enums.aeStatus.FOLLOW_UP }');
|
||||
expect(source).toContain('{ value: "CLOSED", label: TEXT.enums.aeStatus.CLOSED }');
|
||||
expect(source).toContain("normalizeAeStatus");
|
||||
expect(source).not.toContain("Object.entries(TEXT.enums.aeStatus)");
|
||||
});
|
||||
});
|
||||
@@ -1,56 +0,0 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
|
||||
const readSource = () => readFileSync(resolve(__dirname, "./StartupFeasibilityEthics.vue"), "utf8");
|
||||
|
||||
describe("StartupFeasibilityEthics project permissions", () => {
|
||||
it("hides initiation and ethics create/delete actions by their backend operation permissions", () => {
|
||||
const source = readSource();
|
||||
|
||||
expect(source).toContain('canUseApiPermission("startup_initiation:create")');
|
||||
expect(source).toContain('canUseApiPermission("startup_initiation:read")');
|
||||
expect(source).toContain('canUseApiPermission("startup_initiation:update")');
|
||||
expect(source).toContain('canUseApiPermission("startup_initiation:delete")');
|
||||
expect(source).toContain('canUseApiPermission("startup_ethics:read")');
|
||||
expect(source).toContain('canUseApiPermission("startup_ethics:create")');
|
||||
expect(source).toContain('canUseApiPermission("startup_ethics:update")');
|
||||
expect(source).toContain('canUseApiPermission("startup_ethics:delete")');
|
||||
expect(source).toContain('v-if="canReadFeasibility"');
|
||||
expect(source).toContain('v-if="canReadEthics"');
|
||||
expect(source).toContain("if (!canReadFeasibility.value)");
|
||||
expect(source).toContain("if (!canReadEthics.value)");
|
||||
expect(source).toContain('v-if="activeTab === \'feasibility\' && canCreateFeasibility"');
|
||||
expect(source).toContain('v-if="canUpdateFeasibility"');
|
||||
expect(source).toContain('v-if="canDeleteFeasibility"');
|
||||
expect(source).toContain('v-if="activeTab === \'ethics\' && canCreateEthics"');
|
||||
expect(source).toContain('v-if="canUpdateEthics"');
|
||||
expect(source).toContain('v-if="canDeleteEthics"');
|
||||
});
|
||||
});
|
||||
|
||||
describe("StartupFeasibilityEthics drawer editors", () => {
|
||||
it("opens initiation and ethics create forms in drawers instead of routing to form pages", () => {
|
||||
const source = readSource();
|
||||
|
||||
expect(source).toContain("FeasibilityEditorDrawer");
|
||||
expect(source).toContain("EthicsEditorDrawer");
|
||||
expect(source).toContain("feasibilityDrawerVisible");
|
||||
expect(source).toContain("ethicsDrawerVisible");
|
||||
expect(source).not.toContain('router.push("/startup/feasibility/new")');
|
||||
expect(source).not.toContain('router.push("/startup/ethics/new")');
|
||||
});
|
||||
|
||||
it("opens initiation and ethics edit drawers from table action columns", () => {
|
||||
const source = readSource();
|
||||
|
||||
expect(source).toContain(':record-id="editingFeasibilityId"');
|
||||
expect(source).toContain(':record-id="editingEthicsId"');
|
||||
expect(source).toContain("const editingFeasibilityId = ref<string | undefined>();");
|
||||
expect(source).toContain("const editingEthicsId = ref<string | undefined>();");
|
||||
expect(source).toContain("@click.stop=\"openFeasibilityEditor(scope.row)\"");
|
||||
expect(source).toContain("@click.stop=\"openEthicsEditor(scope.row)\"");
|
||||
expect(source).toContain("editingFeasibilityId.value = row?.id;");
|
||||
expect(source).toContain("editingEthicsId.value = row?.id;");
|
||||
});
|
||||
});
|
||||
@@ -1,32 +0,0 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
|
||||
const readStageNode = () => readFileSync(resolve(__dirname, "./StageNode.vue"), "utf8");
|
||||
const readCenterProgressRow = () => readFileSync(resolve(__dirname, "./CenterProgressRow.vue"), "utf8");
|
||||
const readProjectOverview = () => readFileSync(resolve(__dirname, "../ProjectOverview.vue"), "utf8");
|
||||
|
||||
describe("project overview stage progress styling", () => {
|
||||
it("makes the in-progress stage visually distinct from pending stages", () => {
|
||||
const source = readStageNode();
|
||||
|
||||
expect(source).toContain(".stage-in_progress .stage-dot");
|
||||
expect(source).toContain("border: 3px solid #2f5f86");
|
||||
expect(source).toContain(".stage-in_progress .stage-dot::after");
|
||||
expect(source).toContain(".stage-in_progress .stage-dot::before");
|
||||
expect(source).toContain("animation: progress-halo");
|
||||
expect(source).toContain(".stage-in_progress .stage-label");
|
||||
expect(source).toContain("font-weight: 800");
|
||||
});
|
||||
|
||||
it("highlights the active connector and legend consistently", () => {
|
||||
const rowSource = readCenterProgressRow();
|
||||
const overviewSource = readProjectOverview();
|
||||
|
||||
expect(rowSource).toContain(".connector-in_progress");
|
||||
expect(rowSource).toContain("height: 3px");
|
||||
expect(rowSource).toContain("linear-gradient(90deg, #2f5f86");
|
||||
expect(overviewSource).toContain(".legend-dot.active::after");
|
||||
expect(overviewSource).toContain("background: #2f5f86");
|
||||
});
|
||||
});
|
||||
@@ -1,27 +0,0 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
|
||||
const readSource = () => readFileSync(resolve(__dirname, "./PrecautionDetail.vue"), "utf8");
|
||||
|
||||
describe("PrecautionDetail project permissions", () => {
|
||||
it("hides edit controls unless the backend update operation is allowed", () => {
|
||||
const source = readSource();
|
||||
|
||||
expect(source).toContain('const canUpdatePrecaution = computed(() => can("precautions.update"))');
|
||||
expect(source).toContain('v-if="canUpdatePrecaution"');
|
||||
expect(source).toContain("if (!canUpdatePrecaution.value)");
|
||||
expect(source).not.toContain("canWritePrecautions");
|
||||
});
|
||||
});
|
||||
|
||||
describe("PrecautionDetail drawer editor", () => {
|
||||
it("opens edit in the shared drawer editor instead of routing to the form page", () => {
|
||||
const source = readSource();
|
||||
|
||||
expect(source).toContain("PrecautionEditorDrawer");
|
||||
expect(source).toContain("precautionEditorVisible");
|
||||
expect(source).toContain("@success=\"handlePrecautionEditorSuccess\"");
|
||||
expect(source).not.toContain("router.push(`/knowledge/precautions/${precautionId}/edit`)");
|
||||
});
|
||||
});
|
||||
@@ -1,43 +0,0 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
|
||||
const readSource = () => readFileSync(resolve(__dirname, "./PrecautionForm.vue"), "utf8");
|
||||
|
||||
describe("PrecautionForm project permissions", () => {
|
||||
it("uses create permission for new records and update permission for edits", () => {
|
||||
const source = readSource();
|
||||
|
||||
expect(source).toContain('const canCreatePrecaution = computed(() => can("precautions.create"))');
|
||||
expect(source).toContain('const canUpdatePrecaution = computed(() => can("precautions.update"))');
|
||||
expect(source).toContain("const canSavePrecaution = computed(() => (isEdit.value ? canUpdatePrecaution.value : canCreatePrecaution.value))");
|
||||
expect(source).toContain("if (!canSavePrecaution.value)");
|
||||
expect(source).not.toContain("canWritePrecautions");
|
||||
});
|
||||
|
||||
it("uses selects for site and level fields", () => {
|
||||
const source = readSource();
|
||||
|
||||
expect(source).toContain('<el-select v-model="form.site_name"');
|
||||
expect(source).toContain('v-for="site in sites"');
|
||||
expect(source).toContain(':value="site.name"');
|
||||
expect(source).toContain('<el-select v-model="form.level"');
|
||||
expect(source).toContain("const levelOptions = precautionLevelOptions");
|
||||
expect(source).toContain('v-for="level in levelOptions"');
|
||||
expect(source).not.toContain('<el-input v-model="form.site_name"');
|
||||
expect(source).not.toContain('<el-input v-model="form.level"');
|
||||
});
|
||||
|
||||
it("allows attachments to be selected before creating a precaution", () => {
|
||||
const source = readSource();
|
||||
|
||||
expect(source).toContain("<AttachmentList");
|
||||
expect(source).toContain('ref="attachmentPanelRef"');
|
||||
expect(source).toContain('entity-type="precaution"');
|
||||
expect(source).toContain(':entity-id="precautionId || \'\'"');
|
||||
expect(source).toContain(':mode="\'upload\'"');
|
||||
expect(source).toContain("await attachmentPanelRef.value?.uploadPending(savedId)");
|
||||
expect(source).not.toContain('v-if="isEdit && precautionId"');
|
||||
expect(source).not.toContain("StateEmpty");
|
||||
});
|
||||
});
|
||||
@@ -1,47 +0,0 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
|
||||
const readSource = () => readFileSync(resolve(__dirname, "./MaterialEquipmentDetail.vue"), "utf8");
|
||||
|
||||
describe("MaterialEquipmentDetail edit drawer", () => {
|
||||
it("shows an edit action and opens the equipment editor drawer on the detail page", () => {
|
||||
const source = readSource();
|
||||
|
||||
expect(source).toContain("<MaterialEquipmentEditorDrawer");
|
||||
expect(source).toContain('v-if="canUpdateEquipment"');
|
||||
expect(source).toContain("editorVisible.value = true");
|
||||
expect(source).toContain("const handleEditorSaved = () =>");
|
||||
expect(source).toContain('"material_equipments:update"');
|
||||
});
|
||||
|
||||
it("places attachments below calibration settings and renders them as one attachment table", () => {
|
||||
const source = readSource();
|
||||
const calibrationIndex = source.indexOf("校准设置");
|
||||
const qualificationIndex = source.indexOf("TEXT.common.labels.attachments");
|
||||
|
||||
expect(calibrationIndex).toBeGreaterThan(-1);
|
||||
expect(qualificationIndex).toBeGreaterThan(calibrationIndex);
|
||||
expect(source).toContain("<AttachmentList");
|
||||
expect(source).toContain('entity-type="material_equipment"');
|
||||
expect(source).toContain(':hide-uploader="true"');
|
||||
expect(source).toContain(':refresh-key="attachmentRefreshKey"');
|
||||
expect(source).toContain("attachmentRefreshKey.value += 1");
|
||||
expect(source).not.toContain("fetchAttachments");
|
||||
expect(source).not.toContain("deleteAttachment");
|
||||
expect(source).not.toContain("formatFileSize");
|
||||
expect(source).not.toContain("qualificationAttachments");
|
||||
expect(source).not.toContain('class="attachment-table qualification-table"');
|
||||
expect(source).not.toContain("previewVisible");
|
||||
expect(source).not.toContain("getDownloadUrl");
|
||||
expect(source).not.toContain("removeAttachment");
|
||||
expect(source).toContain("material_equipment");
|
||||
expect(source).not.toContain("material_equipment_production_permit");
|
||||
expect(source).not.toContain("material_equipment_tech_index");
|
||||
expect(source).not.toContain("资质文件");
|
||||
expect(source).not.toContain("生产许可证");
|
||||
expect(source).not.toContain("技术指标");
|
||||
expect(source).not.toContain('label="生产许可证">{{ displayValue(detail.production_permit_file_name) }}');
|
||||
expect(source).not.toContain('label="技术指标">{{ displayValue(detail.tech_index_file_name) }}');
|
||||
});
|
||||
});
|
||||
@@ -1,34 +0,0 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
|
||||
const readSource = () => readFileSync(resolve(__dirname, "./MaterialEquipmentEditorDrawer.vue"), "utf8");
|
||||
|
||||
describe("MaterialEquipmentEditorDrawer attachments", () => {
|
||||
it("queues qualification files and uploads them as business attachments on save", () => {
|
||||
const source = readSource();
|
||||
const calibrationIndex = source.indexOf("校准设置");
|
||||
const qualificationIndex = source.indexOf("附件");
|
||||
|
||||
expect(calibrationIndex).toBeGreaterThan(-1);
|
||||
expect(qualificationIndex).toBeGreaterThan(calibrationIndex);
|
||||
expect(source).toContain("AttachmentList");
|
||||
expect(source).toContain('ref="attachmentPanelRef"');
|
||||
expect(source).toContain('entity-type="material_equipment"');
|
||||
expect(source).toContain(':mode="\'upload\'"');
|
||||
expect(source).toContain(':center-upload-card-content="true"');
|
||||
expect(source).toContain("attachmentPanelRef.value?.pendingSnapshot() || []");
|
||||
expect(source).toContain("await attachmentPanelRef.value?.uploadPending(props.equipmentId)");
|
||||
expect(source).toContain("uploadPending");
|
||||
expect(source).toContain("material_equipment");
|
||||
expect(source).not.toContain("uploadAttachment");
|
||||
expect(source).not.toContain("pendingFiles");
|
||||
expect(source).not.toContain("qualificationRows");
|
||||
expect(source).not.toContain("material_equipment_production_permit");
|
||||
expect(source).not.toContain("material_equipment_tech_index");
|
||||
expect(source).not.toContain("资质文件");
|
||||
expect(source).not.toContain("生产许可证");
|
||||
expect(source).not.toContain("技术指标");
|
||||
expect(source).not.toContain("handleUploadChange");
|
||||
});
|
||||
});
|
||||
@@ -1,141 +0,0 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
|
||||
const readView = (relativePath: string) =>
|
||||
readFileSync(resolve(__dirname, relativePath), "utf8");
|
||||
|
||||
const normalizeWhitespace = (source: string) => source.replace(/\s+/g, " ").trim();
|
||||
|
||||
describe("route view overlays are mounted only when visible", () => {
|
||||
it("gates high-risk admin and subject overlays behind visible flags", () => {
|
||||
const checks = [
|
||||
{
|
||||
file: "./admin/ProjectMembers.vue",
|
||||
snippets: ['<el-dialog v-if="addVisible"', 'v-model="addVisible"'],
|
||||
},
|
||||
{
|
||||
file: "./admin/Sites.vue",
|
||||
snippets: ['<SiteForm', 'v-if="projectId && formVisible"'],
|
||||
},
|
||||
{
|
||||
file: "./admin/SiteForm.vue",
|
||||
snippets: ['<el-dialog v-if="visibleProxy"', 'v-model="visibleProxy"'],
|
||||
},
|
||||
{
|
||||
file: "./admin/SiteCraBinding.vue",
|
||||
snippets: ['<el-dialog v-if="visibleProxy"', 'v-model="visibleProxy"'],
|
||||
},
|
||||
{
|
||||
file: "./subjects/SubjectDetail.vue",
|
||||
snippets: [
|
||||
'<el-dialog v-if="historyDialogVisible"',
|
||||
'<VisitEditorDrawer',
|
||||
'<el-drawer v-if="adherenceDrawerVisible"',
|
||||
'<el-drawer v-if="aeDrawerVisible"',
|
||||
'<el-drawer v-if="pdDrawerVisible"',
|
||||
],
|
||||
},
|
||||
{
|
||||
file: "./ia/ProjectMilestones.vue",
|
||||
snippets: ['<el-drawer v-if="editorVisible"', 'v-model="editorVisible"'],
|
||||
},
|
||||
{
|
||||
file: "./ia/MaterialEquipment.vue",
|
||||
snippets: ['<el-drawer v-if="drawerVisible"', 'v-model="drawerVisible"'],
|
||||
},
|
||||
{
|
||||
file: "./ia/RiskIssueMonitoringVisits.vue",
|
||||
snippets: [
|
||||
'<el-drawer v-if="formDialogVisible"',
|
||||
'v-model="formDialogVisible"',
|
||||
'<el-dialog v-if="viewDialogVisible"',
|
||||
'v-model="viewDialogVisible"',
|
||||
],
|
||||
},
|
||||
{
|
||||
file: "../components/attachments/AttachmentList.vue",
|
||||
snippets: ['<el-dialog v-if="previewVisible"', 'append-to-body', 'v-model="previewVisible"'],
|
||||
},
|
||||
{
|
||||
file: "./admin/ProjectDetail.vue",
|
||||
snippets: [
|
||||
'<el-dialog v-if="publishConfirmVisible"',
|
||||
'v-model="publishConfirmVisible"',
|
||||
'v-if="rollbackDialogVisible"',
|
||||
'v-model="rollbackDialogVisible"',
|
||||
'<el-drawer v-if="projectMilestoneEditorVisible"',
|
||||
'<el-drawer v-if="siteMilestoneEditorVisible"',
|
||||
'<el-drawer v-if="siteEnrollmentEditorVisible"',
|
||||
'const getRollbackAxisLeftStyle = (axisX?: number | null) => ({',
|
||||
':style="getRollbackAxisLeftStyle(lane.x)"',
|
||||
':style="getRollbackAxisLeftStyle(row.axisX)"',
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
for (const check of checks) {
|
||||
const source = normalizeWhitespace(readView(check.file));
|
||||
for (const snippet of check.snippets) {
|
||||
expect(source, `${check.file} should include ${snippet}`).toContain(normalizeWhitespace(snippet));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it("allows mask close on drawers while guarding dirty editor forms", () => {
|
||||
const checks = [
|
||||
{
|
||||
file: "./ia/DrugShipments.vue",
|
||||
snippets: [':close-on-click-modal="true"', ':before-close="drawerDirtyGuard.beforeClose"'],
|
||||
},
|
||||
{
|
||||
file: "./ia/MaterialEquipment.vue",
|
||||
snippets: [':close-on-click-modal="true"', ':before-close="drawerDirtyGuard.beforeClose"'],
|
||||
},
|
||||
{
|
||||
file: "./ia/ProjectMilestones.vue",
|
||||
snippets: [':close-on-click-modal="true"', ':before-close="editorDirtyGuard.beforeClose"'],
|
||||
},
|
||||
{
|
||||
file: "./ia/RiskIssueMonitoringVisits.vue",
|
||||
snippets: [':close-on-click-modal="true"', ':before-close="formDirtyGuard.beforeClose"'],
|
||||
},
|
||||
{
|
||||
file: "./admin/ProjectDetail.vue",
|
||||
snippets: [
|
||||
':before-close="projectMilestoneEditorDirtyGuard.beforeClose"',
|
||||
':before-close="siteMilestoneEditorDirtyGuard.beforeClose"',
|
||||
':before-close="siteEnrollmentEditorDirtyGuard.beforeClose"',
|
||||
],
|
||||
},
|
||||
{
|
||||
file: "./admin/PermissionManagement.vue",
|
||||
snippets: [':close-on-click-modal="true"', ':before-close="handleTemplateDrawerBeforeClose"'],
|
||||
},
|
||||
{
|
||||
file: "./documents/DocumentList.vue",
|
||||
snippets: [':close-on-click-modal="true"', ':before-close="editorDirtyGuard.beforeClose"'],
|
||||
},
|
||||
{
|
||||
file: "./documents/DocumentDetail.vue",
|
||||
snippets: [
|
||||
':close-on-click-modal="true"',
|
||||
':before-close="editorDirtyGuard.beforeClose"',
|
||||
':before-close="uploadDirtyGuard.beforeClose"',
|
||||
':before-close="distributeDirtyGuard.beforeClose"',
|
||||
],
|
||||
},
|
||||
{
|
||||
file: "./admin/AuditLogs.vue",
|
||||
snippets: [':close-on-click-modal="true"'],
|
||||
},
|
||||
];
|
||||
|
||||
for (const check of checks) {
|
||||
const source = normalizeWhitespace(readView(check.file));
|
||||
for (const snippet of check.snippets) {
|
||||
expect(source, `${check.file} should include ${snippet}`).toContain(normalizeWhitespace(snippet));
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,22 +0,0 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
|
||||
const readForm = () => readFileSync(resolve(__dirname, "./KickoffForm.vue"), "utf8");
|
||||
const readDetail = () => readFileSync(resolve(__dirname, "./KickoffDetail.vue"), "utf8");
|
||||
|
||||
describe("Kickoff member permissions", () => {
|
||||
it("does not load project members in the form unless the role can read project members", () => {
|
||||
const source = readForm();
|
||||
|
||||
expect(source).toContain('can("project.members.list")');
|
||||
expect(source).toContain("if (canReadMembers.value)");
|
||||
});
|
||||
|
||||
it("does not load project members in the detail page unless the role can read project members", () => {
|
||||
const source = readDetail();
|
||||
|
||||
expect(source).toContain('can("project.members.list")');
|
||||
expect(source).toContain("if (canReadMembers.value)");
|
||||
});
|
||||
});
|
||||
@@ -1,76 +0,0 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
|
||||
const read = (relativePath: string) => readFileSync(resolve(__dirname, relativePath), "utf8");
|
||||
|
||||
describe("startup auth editor drawer attachments", () => {
|
||||
it("uses upload cards for kickoff attachments instead of full attachment tables", () => {
|
||||
const source = read("./KickoffEditorDrawer.vue");
|
||||
|
||||
expect(source).toContain("AttachmentList");
|
||||
expect(source).toContain('ref="attachmentPanelRef"');
|
||||
expect(source).toContain(':mode="\'upload\'"');
|
||||
expect(source).toContain(":entity-groups=\"kickoffAttachmentGroups\"");
|
||||
expect(source).toContain("attachments: attachmentPanelRef.value?.pendingSnapshot() || []");
|
||||
expect(source).toContain("await attachmentPanelRef.value?.uploadPending(props.meetingId)");
|
||||
expect(source).toContain('entityType: "startup_kickoff_minutes"');
|
||||
expect(source).toContain('entityType: "startup_kickoff_signin"');
|
||||
expect(source).toContain('entityType: "startup_kickoff_ppt"');
|
||||
expect(source).toContain('entityType: "startup_kickoff"');
|
||||
expect(source).not.toContain("PendingAttachmentUpload");
|
||||
expect(source).not.toContain("attendees");
|
||||
expect(source).not.toContain("parseAttendees");
|
||||
expect(source).not.toContain("attendeesPlaceholder");
|
||||
});
|
||||
|
||||
it("does not expose attachments in the training authorization editor", () => {
|
||||
const source = read("./TrainingEditorDrawer.vue");
|
||||
|
||||
expect(source).not.toContain("PendingAttachmentUpload");
|
||||
expect(source).not.toContain("AttachmentList");
|
||||
expect(source).not.toContain("training_authorization");
|
||||
expect(source).not.toContain("attachments:");
|
||||
expect(source).not.toContain("uploadPending");
|
||||
});
|
||||
|
||||
it("supports batch creating and single-row updating training authorizations in the same drawer", () => {
|
||||
const source = read("./TrainingEditorDrawer.vue");
|
||||
|
||||
expect(source).toContain("recordId?: string");
|
||||
expect(source).toContain('editorTitle');
|
||||
expect(source).toContain("createTrainingAuthorization");
|
||||
expect(source).toContain("updateTrainingAuthorization");
|
||||
expect(source).toContain("const isCreateMode = computed(() => !props.recordId)");
|
||||
expect(source).toContain("const canCreateAuth = computed(() => can(\"startup.auth.create\"))");
|
||||
expect(source).toContain("const canSaveAuth = computed(() => (isCreateMode.value ? canCreateAuth.value : canUpdateAuth.value))");
|
||||
expect(source).toContain("batchRows");
|
||||
expect(source).toContain("addBatchRow");
|
||||
expect(source).toContain("removeBatchRow");
|
||||
expect(source).toContain("for (const row of batchRows.value)");
|
||||
});
|
||||
|
||||
it("keeps training authorization editor compact and hides site input while preserving site context", () => {
|
||||
const source = read("./TrainingEditorDrawer.vue");
|
||||
|
||||
expect(source).not.toContain(':label="TEXT.common.fields.site"');
|
||||
expect(source).not.toContain('prop="site_name"');
|
||||
expect(source).toContain("site_name: props.initialSiteName || form.site_name || null");
|
||||
expect(source).toContain('class="compact-row-grid"');
|
||||
expect(source).toContain('class="compact-field compact-field--name"');
|
||||
expect(source).toContain('class="compact-field compact-field--status"');
|
||||
expect(source).toContain('class="compact-field compact-field--remark"');
|
||||
});
|
||||
|
||||
it("uses a single-layer batch editor layout for training authorization creation", () => {
|
||||
const source = read("./TrainingEditorDrawer.vue");
|
||||
|
||||
expect(source).toContain('class="form-section training-batch-editor"');
|
||||
expect(source).toContain('class="batch-editor"');
|
||||
expect(source).toContain('class="batch-entry"');
|
||||
expect(source).toContain('class="status-inline"');
|
||||
expect(source).toContain('class="status-date"');
|
||||
expect(source).not.toContain('class="batch-row"');
|
||||
expect(source).not.toContain(".batch-row {");
|
||||
});
|
||||
});
|
||||
@@ -1,125 +0,0 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
|
||||
const read = (relativePath: string) => readFileSync(resolve(__dirname, relativePath), "utf8");
|
||||
|
||||
describe("startup auth form/detail permissions", () => {
|
||||
it("maps startup auth actions to backend operation permissions", () => {
|
||||
const source = read("../../utils/permission.ts");
|
||||
|
||||
expect(source).toContain('"startup.auth.create": "startup_auth:create"');
|
||||
expect(source).toContain('"startup.auth.update": "startup_auth:update"');
|
||||
expect(source).toContain('"startup.auth.delete": "startup_auth:delete"');
|
||||
});
|
||||
|
||||
it("uses create/update permissions in kickoff and training forms", () => {
|
||||
const kickoffForm = read("./KickoffForm.vue");
|
||||
const trainingForm = read("./TrainingForm.vue");
|
||||
|
||||
expect(kickoffForm).toContain('const canCreateAuth = computed(() => can("startup.auth.create"))');
|
||||
expect(kickoffForm).toContain('const canUpdateAuth = computed(() => can("startup.auth.update"))');
|
||||
expect(kickoffForm).toContain("const canSaveAuth = computed(() => (isEdit.value ? canUpdateAuth.value : canCreateAuth.value))");
|
||||
expect(kickoffForm).toContain("if (!canSaveAuth.value)");
|
||||
expect(trainingForm).toContain('const canCreateAuth = computed(() => can("startup.auth.create"))');
|
||||
expect(trainingForm).toContain('const canUpdateAuth = computed(() => can("startup.auth.update"))');
|
||||
expect(trainingForm).toContain("const canSaveAuth = computed(() => (isEdit.value ? canUpdateAuth.value : canCreateAuth.value))");
|
||||
expect(trainingForm).toContain("if (!canSaveAuth.value)");
|
||||
});
|
||||
|
||||
it("opens kickoff detail edits in a drawer and keeps training actions split by backend operations", () => {
|
||||
const source = read("./KickoffDetail.vue");
|
||||
|
||||
expect(source).toContain("<KickoffEditorDrawer");
|
||||
expect(source).toContain("<TrainingEditorDrawer");
|
||||
expect(source).toContain("editorVisible.value = true");
|
||||
expect(source).toContain("trainingEditorVisible.value = true");
|
||||
expect(source).toContain("const handleEditorSaved = () =>");
|
||||
expect(source).toContain(":refresh-key=\"attachmentRefreshKey\"");
|
||||
expect(source).toContain("attachmentRefreshKey.value += 1");
|
||||
expect(source).toContain("const handleTrainingEditorSaved = () =>");
|
||||
expect(source).toContain('const canCreateAuth = computed(() => can("startup.auth.create"))');
|
||||
expect(source).toContain('const canUpdateAuth = computed(() => can("startup.auth.update"))');
|
||||
expect(source).toContain('const canDeleteAuth = computed(() => can("startup.auth.delete"))');
|
||||
expect(source).toContain('v-if="canUpdateAuth"');
|
||||
expect(source).toContain('v-if="canCreateAuth || canUpdateAuth || canDeleteAuth"');
|
||||
expect(source).toContain("if (!canCreateAuth.value)");
|
||||
expect(source).toContain("if (!canUpdateAuth.value)");
|
||||
expect(source).toContain("if (!canDeleteAuth.value)");
|
||||
expect(source).not.toContain("trainingRowDraft");
|
||||
expect(source).not.toContain("isTrainingEditing");
|
||||
expect(source).not.toContain("saveTraining");
|
||||
expect(source).not.toContain("cancelTrainingEdit");
|
||||
expect(source).not.toContain("editMode");
|
||||
expect(source).not.toContain("saveEdit");
|
||||
});
|
||||
|
||||
it("opens training detail edits in a drawer without navigating to the old edit page", () => {
|
||||
const source = read("./TrainingDetail.vue");
|
||||
|
||||
expect(source).toContain("<TrainingEditorDrawer");
|
||||
expect(source).toContain("editorVisible.value = true");
|
||||
expect(source).toContain("const handleEditorSaved = () =>");
|
||||
expect(source).toContain('const canUpdateAuth = computed(() => can("startup.auth.update"))');
|
||||
expect(source).toContain('v-if="canUpdateAuth"');
|
||||
expect(source).toContain("if (!canUpdateAuth.value)");
|
||||
expect(source).not.toContain("AttachmentList");
|
||||
expect(source).not.toContain("training_authorization");
|
||||
expect(source).not.toContain("attachmentRefreshKey");
|
||||
expect(source).not.toContain("router.push(`/startup/training/${recordId}/edit`)");
|
||||
});
|
||||
|
||||
it("renders kickoff attachments as one aggregated table with attachment type labels", () => {
|
||||
const source = read("./KickoffDetail.vue");
|
||||
|
||||
expect(source).toContain(":entity-groups=\"kickoffAttachmentGroups\"");
|
||||
expect(source).not.toContain("v-for=\"group in kickoffAttachmentGroups\"");
|
||||
expect(source).toContain('label: TEXT.modules.startupMeetingAuth.kickoffMinutesLabel');
|
||||
expect(source).toContain('label: TEXT.modules.startupMeetingAuth.kickoffSignInLabel');
|
||||
expect(source).toContain('label: TEXT.modules.startupMeetingAuth.kickoffPptLabel');
|
||||
expect(source).toContain('label: TEXT.modules.startupMeetingAuth.kickoffOtherLabel');
|
||||
});
|
||||
|
||||
it("allows legacy kickoff and training forms to queue attachments before saving", () => {
|
||||
const kickoffForm = read("./KickoffForm.vue");
|
||||
const trainingForm = read("./TrainingForm.vue");
|
||||
|
||||
expect(kickoffForm).toContain("<AttachmentList");
|
||||
expect(kickoffForm).toContain('ref="attachmentPanelRef"');
|
||||
expect(kickoffForm).toContain(":entity-groups=\"kickoffAttachmentGroups\"");
|
||||
expect(kickoffForm).toContain(':entity-id="meetingId || \'\'"');
|
||||
expect(kickoffForm).toContain(':mode="\'upload\'"');
|
||||
expect(kickoffForm).toContain("await attachmentPanelRef.value?.uploadPending(savedId)");
|
||||
expect(kickoffForm).not.toContain('v-for="group in kickoffAttachmentGroups"');
|
||||
expect(kickoffForm).not.toContain("StateEmpty");
|
||||
|
||||
expect(trainingForm).toContain("<AttachmentList");
|
||||
expect(trainingForm).toContain('ref="attachmentPanelRef"');
|
||||
expect(trainingForm).toContain('entity-type="training_authorization"');
|
||||
expect(trainingForm).toContain(':entity-id="recordId || \'\'"');
|
||||
expect(trainingForm).toContain(':mode="\'upload\'"');
|
||||
expect(trainingForm).toContain("await attachmentPanelRef.value?.uploadPending(savedId)");
|
||||
expect(trainingForm).not.toContain('v-if="isEdit && recordId"');
|
||||
expect(trainingForm).not.toContain("StateEmpty");
|
||||
});
|
||||
|
||||
it("shows training and authorization dates under positive status in the training table", () => {
|
||||
const source = read("./KickoffDetail.vue");
|
||||
|
||||
expect(source).toContain("status-dot--yes");
|
||||
expect(source).toContain("status-dot--no");
|
||||
expect(source).toContain("scope.row.trained && scope.row.trained_date");
|
||||
expect(source).toContain("scope.row.authorized && scope.row.authorized_date");
|
||||
expect(source).toContain('class="status-date"');
|
||||
expect(source).toContain("scope.row.trained ? TEXT.common.boolean.yes : TEXT.common.boolean.no");
|
||||
expect(source).toContain("scope.row.authorized ? TEXT.common.boolean.yes : TEXT.common.boolean.no");
|
||||
});
|
||||
|
||||
it("loads training authorizations for the current kickoff site only", () => {
|
||||
const source = read("./KickoffDetail.vue");
|
||||
|
||||
expect(source).toContain("if (!studyId || !siteInfo.name) return;");
|
||||
expect(source).toContain("listTrainingAuthorizations(studyId, { site_name: siteInfo.name })");
|
||||
expect(source).not.toContain("listTrainingAuthorizations(studyId);");
|
||||
});
|
||||
});
|
||||
@@ -1,84 +0,0 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
|
||||
const read = (relativePath: string) => readFileSync(resolve(__dirname, relativePath), "utf8");
|
||||
|
||||
describe("startup initiation and ethics form/detail permissions", () => {
|
||||
it("uses startup initiation create/update permissions in feasibility form and detail", () => {
|
||||
const formSource = read("./FeasibilityForm.vue");
|
||||
const detailSource = read("./FeasibilityDetail.vue");
|
||||
|
||||
expect(formSource).toContain('const canCreateInitiation = computed(() => can("startup.initiation.create"))');
|
||||
expect(formSource).toContain('const canUpdateInitiation = computed(() => can("startup.initiation.update"))');
|
||||
expect(formSource).toContain("const canSaveInitiation = computed(() => (isEdit.value ? canUpdateInitiation.value : canCreateInitiation.value))");
|
||||
expect(formSource).toContain("if (!canSaveInitiation.value)");
|
||||
expect(detailSource).toContain('const canUpdateInitiation = computed(() => can("startup.initiation.update"))');
|
||||
expect(detailSource).toContain('v-if="canUpdateInitiation"');
|
||||
expect(detailSource).toContain("if (!canUpdateInitiation.value)");
|
||||
});
|
||||
|
||||
it("uses startup ethics create/update permissions in ethics form and detail", () => {
|
||||
const formSource = read("./EthicsForm.vue");
|
||||
const detailSource = read("./EthicsDetail.vue");
|
||||
|
||||
expect(formSource).toContain('const canCreateEthics = computed(() => can("startup.ethics.create"))');
|
||||
expect(formSource).toContain('const canUpdateEthics = computed(() => can("startup.ethics.update"))');
|
||||
expect(formSource).toContain("const canSaveEthics = computed(() => (isEdit.value ? canUpdateEthics.value : canCreateEthics.value))");
|
||||
expect(formSource).toContain("if (!canSaveEthics.value)");
|
||||
expect(detailSource).toContain('const canUpdateEthics = computed(() => can("startup.ethics.update"))');
|
||||
expect(detailSource).toContain('v-if="canUpdateEthics"');
|
||||
expect(detailSource).toContain("if (!canUpdateEthics.value)");
|
||||
});
|
||||
|
||||
it("keeps detail attachment areas display-only and moves upload into editor drawers", () => {
|
||||
const feasibilityDetail = read("./FeasibilityDetail.vue");
|
||||
const ethicsDetail = read("./EthicsDetail.vue");
|
||||
const feasibilityDrawer = read("./FeasibilityEditorDrawer.vue");
|
||||
const ethicsDrawer = read("./EthicsEditorDrawer.vue");
|
||||
|
||||
expect(feasibilityDetail).toContain(":hide-uploader=\"true\"");
|
||||
expect(ethicsDetail).toContain(":hide-uploader=\"true\"");
|
||||
expect(feasibilityDrawer).toContain("AttachmentList");
|
||||
expect(feasibilityDrawer).toContain('ref="attachmentPanelRef"');
|
||||
expect(feasibilityDrawer).toContain('entity-type="startup_feasibility"');
|
||||
expect(feasibilityDrawer).toContain(":entity-id=\"recordId || ''\"");
|
||||
expect(feasibilityDrawer).toContain(':mode="\'upload\'"');
|
||||
expect(feasibilityDrawer).toContain("attachments: attachmentPanelRef.value?.pendingSnapshot() || []");
|
||||
expect(feasibilityDrawer).toContain("await attachmentPanelRef.value?.uploadPending(id)");
|
||||
expect(feasibilityDrawer).not.toContain("PendingAttachmentUpload");
|
||||
expect(ethicsDrawer).toContain("AttachmentList");
|
||||
expect(ethicsDrawer).toContain('ref="attachmentPanelRef"');
|
||||
expect(ethicsDrawer).toContain('entity-type="startup_ethics"');
|
||||
expect(ethicsDrawer).toContain(":entity-id=\"recordId || ''\"");
|
||||
expect(ethicsDrawer).toContain(':mode="\'upload\'"');
|
||||
expect(ethicsDrawer).toContain("attachments: attachmentPanelRef.value?.pendingSnapshot() || []");
|
||||
expect(ethicsDrawer).toContain("await attachmentPanelRef.value?.uploadPending(id)");
|
||||
expect(ethicsDrawer).not.toContain("PendingAttachmentUpload");
|
||||
expect(feasibilityDrawer).not.toContain('class="attachment-group"');
|
||||
expect(ethicsDrawer).not.toContain('class="attachment-group"');
|
||||
});
|
||||
|
||||
it("allows legacy feasibility and ethics forms to queue attachments before the record exists", () => {
|
||||
const feasibilityForm = read("./FeasibilityForm.vue");
|
||||
const ethicsForm = read("./EthicsForm.vue");
|
||||
|
||||
expect(feasibilityForm).toContain("<AttachmentList");
|
||||
expect(feasibilityForm).toContain('ref="attachmentPanelRef"');
|
||||
expect(feasibilityForm).toContain('entity-type="startup_feasibility"');
|
||||
expect(feasibilityForm).toContain(':entity-id="recordId || \'\'"');
|
||||
expect(feasibilityForm).toContain(':mode="\'upload\'"');
|
||||
expect(feasibilityForm).toContain("await attachmentPanelRef.value?.uploadPending(savedId)");
|
||||
expect(feasibilityForm).not.toContain('v-if="isEdit && recordId"');
|
||||
expect(feasibilityForm).not.toContain("StateEmpty");
|
||||
|
||||
expect(ethicsForm).toContain("<AttachmentList");
|
||||
expect(ethicsForm).toContain('ref="attachmentPanelRef"');
|
||||
expect(ethicsForm).toContain('entity-type="startup_ethics"');
|
||||
expect(ethicsForm).toContain(':entity-id="recordId || \'\'"');
|
||||
expect(ethicsForm).toContain(':mode="\'upload\'"');
|
||||
expect(ethicsForm).toContain("await attachmentPanelRef.value?.uploadPending(savedId)");
|
||||
expect(ethicsForm).not.toContain('v-if="isEdit && recordId"');
|
||||
expect(ethicsForm).not.toContain("StateEmpty");
|
||||
});
|
||||
});
|
||||
@@ -1,28 +0,0 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
|
||||
const readSubjectEditorDrawer = () => readFileSync(resolve(__dirname, "./SubjectEditorDrawer.vue"), "utf8");
|
||||
|
||||
describe("SubjectEditorDrawer screening date editing", () => {
|
||||
it("keeps screening date editable when editing an existing participant", () => {
|
||||
const source = readSubjectEditorDrawer();
|
||||
const screeningDateField = source.slice(
|
||||
source.indexOf('v-model="form.screening_date"'),
|
||||
source.indexOf('v-model="form.consent_date"')
|
||||
);
|
||||
|
||||
expect(screeningDateField).toContain(':disabled="isReadOnly"');
|
||||
expect(screeningDateField).not.toContain(':disabled="isEdit || isReadOnly"');
|
||||
});
|
||||
|
||||
it("submits screening date in the edit payload", () => {
|
||||
const source = readSubjectEditorDrawer();
|
||||
const editPayload = source.slice(
|
||||
source.indexOf("await updateSubject(studyId.value, props.subjectId, {"),
|
||||
source.indexOf("});", source.indexOf("await updateSubject(studyId.value, props.subjectId, {"))
|
||||
);
|
||||
|
||||
expect(editPayload).toContain("screening_date: form.screening_date || null");
|
||||
});
|
||||
});
|
||||
@@ -1,28 +0,0 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
|
||||
const readSubjectForm = () => readFileSync(resolve(__dirname, "./SubjectForm.vue"), "utf8");
|
||||
|
||||
describe("SubjectForm screening date editing", () => {
|
||||
it("keeps screening date editable when editing an existing participant", () => {
|
||||
const source = readSubjectForm();
|
||||
const screeningDateField = source.slice(
|
||||
source.indexOf('v-model="form.screening_date"'),
|
||||
source.indexOf('v-model="form.consent_date"')
|
||||
);
|
||||
|
||||
expect(screeningDateField).toContain(':disabled="isReadOnly"');
|
||||
expect(screeningDateField).not.toContain(':disabled="isEdit || isReadOnly"');
|
||||
});
|
||||
|
||||
it("submits screening date in the edit payload", () => {
|
||||
const source = readSubjectForm();
|
||||
const editPayload = source.slice(
|
||||
source.indexOf("const payload = {"),
|
||||
source.indexOf("};", source.indexOf("const payload = {"))
|
||||
);
|
||||
|
||||
expect(editPayload).toContain("screening_date: form.screening_date || null");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user