import { readFileSync } from "node:fs"; import { resolve } from "node:path"; import { describe, expect, it } from "vitest"; const readLayout = () => readFileSync(resolve(__dirname, "./Layout.vue"), "utf8"); describe("Layout breadcrumbs", () => { it("shows the file version module before document detail titles", () => { const source = readLayout(); expect(source).toContain('match: ["/file-versions", "/documents", "/trial/"]'); expect(source).toContain("study.viewContext?.pageTitle"); expect(source).toContain("items.push({ label: study.viewContext.pageTitle, path: route.path })"); }); it("uses context page titles instead of generic route detail titles", () => { const source = readLayout(); expect(source).toContain("const hasContextPageTitle = Boolean(study.viewContext?.pageTitle)"); expect(source).toContain("if (!hasContextPageTitle && title"); }); it("keeps project management as the parent for admin project detail breadcrumbs", () => { const source = readLayout(); expect(source).toContain('if (route.path.startsWith("/admin/projects/"))'); expect(source).toContain("label: TEXT.menu.projectManagement"); expect(source).toContain('path: "/admin/projects"'); }); it("restores the permission management parent for admin permission sub-pages", () => { const source = readLayout(); expect(source).toContain('if (route.path.startsWith("/admin/permissions/"))'); expect(source).toContain("label: TEXT.menu.permissionManagement"); }); it("injects the project name level on the admin sites page", () => { const source = readLayout(); expect(source).toContain('if (route.path.includes("/sites"))'); expect(source).toContain("studies.value.find((s) => String(s.id) === pid)"); expect(source).toContain("items.push({ label: proj.name, path: `/admin/projects/${pid}` })"); }); it("keeps the admin root breadcrumb as a non-clickable label", () => { const source = readLayout(); expect(source).toContain("items.push({ label: TEXT.menu.admin });"); expect(source).not.toContain('path: isAdmin.value ? "/admin/users" : "/admin/projects"'); }); it("enables sibling navigation dropdowns on admin breadcrumbs", () => { const source = readLayout(); expect(source).toContain("item.navDropdown && item.siblings?.length"); expect(source).toContain('{ type: \'nav\', value: sib.path }'); expect(source).toContain("if (cmd.type === 'nav')"); expect(source).toContain("siblings: topItems"); }); it("builds project breadcrumbs from a permission-filtered nav tree", () => { const source = readLayout(); expect(source).toContain("const navTree: NavNode[] = ["); expect(source).toContain("label: TEXT.menu.sharedLibrary"); expect(source).toContain("label: TEXT.menu.riskIssues"); expect(source).toContain("label: TEXT.menu.materialManagement"); }); it("renders shared-library items as a third-level group, not top-level", () => { const source = readLayout(); // 三级子项(医学咨询等)必须挂在顶级分组的 children 内,而非顶级数组 expect(source).toContain("children: ["); expect(source).toContain("canAccessProjectPath(c.path)"); expect(source).toContain("childSiblings.length > 1"); }); it("renders a header center region with project status but not ambiguous phase", () => { const source = readLayout(); expect(source).toContain('class="header-center"'); expect(source).toContain("const headerProjectInfo = computed"); expect(source).toContain("statusTone"); expect(source).toContain('class="header-status-pill"'); expect(source).toContain("header-status-dot"); expect(source).not.toContain("headerProjectInfo.roleLabel"); expect(source).not.toContain("headerProjectInfo.phase"); expect(source).not.toContain("TEXT.common.labels.phase"); }); it("renders the project role next to the account menu", () => { const source = readLayout(); expect(source).toContain('class="header-account-role"'); expect(source).toContain("accountProjectRoleLabel"); expect(source).toContain("v-if=\"accountProjectRoleLabel\""); }); it("uses chip styling instead of vertical divider lines for header metadata", () => { const source = readLayout(); expect(source).toContain("class=\"header-meta-item header-metric\""); expect(source).toContain("class=\"header-context-rail\""); expect(source).toContain("border-radius: 14px"); expect(source).toContain("border: 1px solid transparent"); expect(source).toContain("background: transparent"); expect(source).toContain("box-shadow: none"); expect(source).toContain("overflow: visible"); expect(source).toContain("class=\"header-meta-value\""); expect(source).toContain("font-variant-numeric: tabular-nums"); expect(source).toContain("border: 1px solid rgba(213, 224, 237, 0.94)"); expect(source).not.toContain("0 5px 14px rgba(31, 109, 80, 0.14)"); expect(source).not.toContain("border-left: 1px solid #e2e8f0"); expect(source).not.toContain("border-right: 1px solid #e2e8f0"); }); it("does not duplicate the account identity in the admin header center", () => { const source = readLayout(); expect(source).not.toContain("const headerAdminInfo = computed"); expect(source).not.toContain("headerAdminInfo.email"); expect(source).not.toContain("header-admin-email"); }); it("renders a denser project context rail with planned site and enrollment metrics", () => { const source = readLayout(); expect(source).toContain('class="header-context-rail"'); expect(source).toContain("headerProjectInfo.metrics"); expect(source).toContain("plannedSiteCount"); expect(source).toContain("plannedEnrollmentCount"); expect(source).toContain("headerOverviewStats"); expect(source).toContain("fetchProjectOverview"); expect(source).toContain("formatHeaderProgress"); expect(source).toContain("headerMetric.key"); expect(source).toContain("TEXT.common.labels.plannedSites"); expect(source).toContain("TEXT.common.labels.plannedEnrollment"); expect(source).toContain("TEXT.common.labels.dataUpdatedAt"); expect(source).toContain("loadHeaderOverviewStats"); expect(source).toContain("study.currentStudy?.id !== studyId"); expect(source).toContain("headerClockNow"); expect(source).toContain("formatHeaderDateTime"); expect(source).toContain("window.setInterval"); expect(source).toContain("window.clearInterval"); }); it("adds a compact project reminder dropdown for overdue risks", () => { const source = readLayout(); expect(source).toContain('class="header-reminder-dropdown"'); expect(source).toContain("headerReminderTotal"); expect(source).toContain("headerReminderItems"); expect(source).toContain("fetchOverdueAesCount"); expect(source).toContain("listMonitoringVisitIssues"); expect(source).toContain("TEXT.common.labels.projectReminders"); expect(source).toContain("TEXT.common.labels.overdueAes"); expect(source).toContain("TEXT.common.labels.overdueMonitoringIssues"); expect(source).toContain("handleReminderCommand"); }); it("styles the app header as a fixed chrome layer above page content", () => { const source = readLayout(); expect(source).toContain("position: sticky"); expect(source).toContain("top: 0"); expect(source).toContain("z-index: 30"); expect(source).toContain(".layout-header::after"); expect(source).toContain("0 10px 26px rgba(15, 23, 42, 0.08)"); expect(source).toContain("border-bottom: 1px solid rgba(203, 213, 225, 0.78)"); }); it("omits visual breadcrumb separators and truncates long breadcrumb labels", () => { const source = readLayout(); expect(source).not.toContain("breadcrumb-separator"); expect(source).not.toContain("TEXT.common.separators.dot"); expect(source).toContain("text-overflow: ellipsis"); expect(source).toContain("max-width: 52vw"); }); it("styles header breadcrumbs as compact navigation controls", () => { const source = readLayout(); expect(source).toContain('class="breadcrumb-item-wrapper is-link is-study"'); expect(source).toContain("'is-current': index === breadcrumbs.length - 1"); expect(source).toContain(".breadcrumb-item-wrapper.is-study"); expect(source).toContain(".breadcrumb-item-wrapper.is-current"); expect(source).toContain("border-color: rgba(198, 214, 235, 0.92)"); expect(source).toContain("height: 26px"); expect(source).toContain("border-radius: 7px"); expect(source).toContain("gap: 2px"); }); it("uses stable breadcrumb keys when context titles change", () => { const source = readLayout(); expect(source).toContain(':key="breadcrumbKey(item, index)"'); expect(source).not.toContain(':key="index"'); expect(source).toContain("const breadcrumbKey = (item: any, index: number)"); }); it("guards route component rendering while router resolves async views", () => { const source = readLayout(); expect(source).toContain(''); }); it("confirms manual logout and records the logout reason", () => { const source = readLayout(); expect(source).toContain('command="logout" :disabled="loggingOut" divided'); expect(source).toContain("ElMessageBox.confirm("); expect(source).toContain("确认退出登录"); expect(source).toContain("退出后需要重新登录才能继续访问项目数据。"); expect(source).toContain("forceLogout(LOGOUT_REASON_MANUAL)"); expect(source).toContain('loggingOut ? "正在退出..." : TEXT.menu.logout'); }); it("opens profile settings as a desktop-style dialog instead of navigating away", () => { const source = readLayout(); expect(source).toContain('v-model="profileDialogVisible"'); expect(source).toContain('class="profile-settings-dialog"'); expect(source).toContain(':close-on-click-modal="false"'); expect(source).toContain(':close-on-press-escape="false"'); expect(source).toContain('@close-request="requestProfileDialogClose"'); expect(source).toContain('@dirty-change="profileDialogDirty = $event"'); expect(source).toContain('@saved="profileDialogVisible = false"'); expect(source).toContain("profileDialogVisible.value = true"); expect(source).toContain("confirmDiscardProfileChanges"); expect(source).not.toContain('router.push("/profile")'); }); });