Files
ctms/frontend/src/components/LayoutBreadcrumb.test.ts
T
2026-06-17 17:22:22 +08:00

236 lines
10 KiB
TypeScript

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('<component v-if="Component" :is="Component" />');
});
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")');
});
});