整理测试代码并清理文档引用

This commit is contained in:
Cheng Zhou
2026-06-22 17:27:11 +08:00
parent 6056c8364a
commit c351ca59c4
75 changed files with 15 additions and 4415 deletions
@@ -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, "./FaqCategoryForm.vue"), "utf8");
describe("FaqCategoryForm drawer editor", () => {
it("uses the shared right-side drawer pattern instead of a dialog", () => {
const source = readSource();
expect(source).toContain("<el-drawer");
expect(source).toContain("faq-category-editor-drawer");
expect(source).toContain('direction="rtl"');
expect(source).toContain("editor-header");
expect(source).toContain("drawer-footer");
expect(source).not.toContain("<el-dialog");
});
it("labels the required name field as category name", () => {
const source = readSource();
expect(source).toContain("TEXT.modules.knowledgeMedicalConsult.categoryName");
expect(source).not.toContain(':label="TEXT.common.fields.name"');
});
});
@@ -1,18 +0,0 @@
import { describe, expect, it } from "vitest";
import { readFileSync } from "node:fs";
import { resolve } from "node:path";
const readSource = () => readFileSync(resolve(__dirname, "./FaqItemForm.vue"), "utf8");
describe("FaqItemForm drawer", () => {
it("uses the unified drawer pattern instead of a dialog", () => {
const source = readSource();
expect(source).toContain("<el-drawer");
expect(source).toContain('class="faq-item-editor-drawer"');
expect(source).toContain('class="editor-header"');
expect(source).toContain('class="drawer-footer"');
expect(source).not.toContain("<el-dialog");
expect(source).not.toContain('append-to=".layout-main .content-wrapper"');
});
});
@@ -1,235 +0,0 @@
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")');
});
});
@@ -1,22 +0,0 @@
import { describe, expect, it } from "vitest";
import { readFileSync } from "node:fs";
import { resolve } from "node:path";
const readSource = () => readFileSync(resolve(__dirname, "./Layout.vue"), "utf8");
describe("layout system monitoring navigation", () => {
it("shows system monitoring as an admin-only peer module outside permission management", () => {
const source = readSource();
const permissionMenuStart = source.indexOf('<el-sub-menu v-if="canAccessAdminPermissions" index="permissions">');
const permissionMenuEnd = source.indexOf("</el-sub-menu>", permissionMenuStart);
const permissionMenu = source.slice(permissionMenuStart, permissionMenuEnd);
expect(source).toContain('v-if="isAdmin" index="/admin/system-monitoring"');
expect(source).toContain("{{ TEXT.menu.systemMonitoring }}");
expect(permissionMenu).not.toContain("/admin/system-monitoring");
expect(permissionMenu).not.toContain("/admin/permission-monitoring");
expect(permissionMenu).not.toContain("/admin/permissions/monitoring");
expect(source).not.toContain('index="/admin/permissions/monitoring"');
expect(source).toContain('return "/admin/system-monitoring";');
});
});
@@ -1,347 +0,0 @@
import { describe, expect, it } from "vitest";
import { readFileSync } from "node:fs";
import { resolve } from "node:path";
const readSource = () => readFileSync(resolve(__dirname, "./PermissionIpLocations.vue"), "utf8");
const readMapSource = () => readFileSync(resolve(__dirname, "./chinaProvinceMap.ts"), "utf8");
const readWorldMapSource = () => readFileSync(resolve(__dirname, "./worldMapSource.ts"), "utf8");
describe("PermissionIpLocations", () => {
it("renders IP location visualization without duplicate region detail table", () => {
const source = readSource();
expect(source).toContain("fetchIpLocations");
expect(source).toContain("unique_ip_count");
expect(source).not.toContain("来源地域明细");
expect(source).not.toContain("table-card");
expect(source).not.toContain("<el-table :data=\"items\"");
});
it("renders an interactive China map and primary metric cards", () => {
const source = readSource();
expect(source).toContain("VChart");
expect(source).toContain("sourceMapOption");
expect(source).toContain("来源分布");
expect(source).toContain("访问次数");
expect(source).toContain("访问用户数");
expect(source).toContain("来源 IP 数");
expect(source).toContain("summary");
});
it("defaults the access source period to 90 days", () => {
const source = readSource();
expect(source).toContain("const days = ref(90);");
expect(source).not.toContain("const days = ref(7);");
expect(source).toContain('<el-radio-button :value="90">90天</el-radio-button>');
expect(source).toContain('90: "近 90 天"');
});
it("does not overlay the source distribution card with a loading mask", () => {
const source = readSource();
expect(source).toContain('<section class="distribution-card">');
expect(source).toContain('<el-button :loading="loading" size="small" :disabled="loading" @click="loadData">');
expect(source).not.toContain('<section v-loading="loading" class="distribution-card">');
});
it("supports switching source distribution between China and global flow views without the global heat map", () => {
const source = readSource();
const worldMapSource = readWorldMapSource();
expect(source).toContain("../assets/world.json");
expect(source).toContain('registerMap("ctms-world"');
expect(source).toContain('const mapView = ref<"china" | "flow">("china");');
expect(source).toContain("mapViewOptions");
expect(source).toContain("<el-segmented v-model=\"mapView\"");
expect(source).toContain("sourceMapOption");
expect(source).toContain('label: "中国"');
expect(source).toContain('label: "全球"');
expect(source).not.toContain('label: "全球", value: "world"');
expect(source).not.toContain("worldData");
expect(source).not.toContain("buildMapOption");
expect(worldMapSource).toContain("unpkg.com/echarts@4.9.0/map/json/world.json");
expect(worldMapSource).toContain("ECharts 世界地图 GeoJSON");
expect(worldMapSource).toContain('"United States": "United States"');
});
it("supports a global access flow view pointing sources to the server location", () => {
const source = readSource();
const worldMapSource = readWorldMapSource();
expect(source).toContain('const mapView = ref<"china" | "flow">("china");');
expect(source).toContain('label: "全球"');
expect(source).toContain("LinesChart");
expect(source).toContain("EffectScatterChart");
expect(source).toContain("GeoComponent");
expect(source).toContain("buildFlowMapOption");
expect(source).toContain("serverLocation");
expect(source).toContain("resolveSourceCoordinate");
expect(source).toContain('coordinateSystem: "geo"');
expect(source).toContain("coords: [point.coord, serverLocation.coord]");
expect(source).toContain("服务器所在地");
expect(worldMapSource).toContain("resolveSourceCoordinate");
expect(worldMapSource).toContain('"United States": [-95.7129, 37.0902]');
});
it("uses a China access flow map for the China view", () => {
const source = readSource();
const worldMapSource = readWorldMapSource();
expect(source).toContain("chinaFlowSourceData");
expect(source).toContain('buildFlowMapOption(chinaFlowSourceData.value, "ctms-china", 1.08, "中国访问链路")');
expect(source).not.toContain('buildMapOption("ctms-china", provinceData.value, 1.08)');
expect(source).toContain('mapView.value === "china" ? chinaFlowSourceData.value');
expect(worldMapSource).toContain('"江苏省": [118.7633, 32.0617]');
expect(worldMapSource).toContain('"上海市": [121.4737, 31.2304]');
});
it("uses the same source rows for the global flow map and the source ranking", () => {
const source = readSource();
const worldMapSource = readWorldMapSource();
expect(source).toContain("flowSourceData");
expect(source).toContain('mapView.value === "flow" ? flowSourceData.value : rankedSourceRows.value');
expect(source).toContain("buildFlowMapOption(flowSourceData.value)");
expect(source).not.toContain("buildFlowMapOption(worldData.value)");
expect(source).toContain('const buildFlowMapOption = (data: IpLocationStatItem[], mapName = "ctms-world"');
expect(source).toContain("coord: resolveSourceCoordinate(item)");
expect(source).toContain("name: formatLocation(point)");
expect(worldMapSource).toContain("cityCoordinates");
expect(worldMapSource).toContain('"重庆市": [106.5516, 29.563]');
expect(worldMapSource).toContain('"南京市": [118.7969, 32.0603]');
expect(worldMapSource).toContain('"Istanbul": [28.9784, 41.0082]');
expect(worldMapSource).toContain('"South Holland": [4.493, 52.0208]');
});
it("keeps global flow lines thin and readable", () => {
const source = readSource();
expect(source).toContain("const lineWidth = point.abnormal ? 1.1 : 0.8");
expect(source).toContain("width: lineWidth");
expect(source).toContain("opacity: point.abnormal ? 0.58 : 0.38");
expect(source).not.toContain("width: Math.min(5");
expect(source).not.toContain("Math.sqrt(point.total_count");
});
it("keeps domestic flow markers compact so nearby China locations stay visible", () => {
const source = readSource();
expect(source).toContain("const isDomestic = !params?.data?.abnormal");
expect(source).toContain("const maxSize = isDomestic ? 12 : 16");
expect(source).toContain("const minSize = isDomestic ? 5 : 7");
expect(source).toContain("scale: 1.7");
expect(source).toContain("scale: 1.5");
expect(source).toContain("symbolSize: 9");
expect(source).not.toContain("scale: 3");
expect(source).not.toContain("scale: 2.4");
expect(source).not.toContain("symbolSize: 14");
});
it("shows a compact flow legend and keeps the server tooltip address-only", () => {
const source = readSource();
expect(source).toContain("source-map-wrap");
expect(source).toContain("fullscreen-map-wrap");
expect(source).toContain("flow-legend");
expect(source).toContain("map-legend");
expect(source).toContain("服务器");
expect(source).toContain("正常访问点");
expect(source).toContain("异常访问点");
expect(source).toContain('class="legend-dot server"');
expect(source).toContain('class="legend-dot normal"');
expect(source).toContain('class="legend-dot abnormal"');
expect(source).toContain('if (row.isServer) return "中国 / 上海";');
expect(source).not.toContain('if (row.isServer) return `${serverLocation.name}<br/>中国 / 上海`;');
expect(source).not.toContain("fullscreen-legend");
});
it("supports fullscreen preview for the current source map", () => {
const source = readSource();
expect(source).toContain("fullscreenVisible");
expect(source).toContain("openFullscreenPreview");
expect(source).toContain("resizeFullscreenChart");
expect(source).toContain("<el-dialog");
expect(source).toContain("fullscreen");
expect(source).toContain("destroy-on-close");
expect(source).toContain(':close-on-click-modal="false"');
expect(source).toContain('aria-label="全屏预览"');
expect(source).toContain("fullscreenMapKey");
expect(source).toContain("fullscreen-source-map");
expect(source).toContain("全屏预览");
expect(source).toContain("退出全屏");
expect(source).toContain(":option=\"sourceMapOption\"");
});
it("raises canvas pixel ratio for sharper fullscreen map rendering", () => {
const source = readSource();
expect(source).toContain("getDevicePixelRatio");
expect(source).toContain("chartInitOptions");
expect(source).toContain("fullscreenChartInitOptions");
expect(source).toContain(":init-options=\"chartInitOptions\"");
expect(source).toContain(":init-options=\"fullscreenChartInitOptions\"");
expect(source).toContain("Math.min(getDevicePixelRatio(), 2)");
expect(source).toContain("Math.min(getDevicePixelRatio() * 1.5, 3)");
});
it("uses a flexible full-page layout so the source map fills the remaining viewport", () => {
const source = readSource();
expect(source).toContain("height: 100%");
expect(source).toContain("min-height: 0");
expect(source).toContain("flex: 1 1 auto");
expect(source).toContain("grid-template-rows: minmax(0, 1fr)");
expect(source).toContain("height: 100%");
expect(source).toContain("overflow-y: auto");
expect(source).not.toContain("height: 320px");
expect(source).not.toContain("min-height: 430px");
});
it("prevents horizontal scrolling in the source ranking panel", () => {
const source = readSource();
expect(source).toContain(".rank-panel");
expect(source).toContain("overflow: hidden");
expect(source).toContain("overflow-x: hidden");
expect(source).toContain("overflow-y: auto");
expect(source).toContain("box-sizing: border-box");
expect(source).toContain("grid-template-columns: 36px minmax(0, 1fr) minmax(72px, max-content)");
expect(source).toContain("max-width: 100%");
expect(source).toContain("text-overflow: ellipsis");
expect(source).toContain("text-align: right");
});
it("recreates the chart when switching map modes to avoid stale geo state", () => {
const source = readSource();
expect(source).toContain(':key="mapView"');
expect(source).toContain(':update-options="chartUpdateOptions"');
expect(source).toContain("const chartUpdateOptions = { notMerge: true }");
});
it("resizes the source map after data load and keeps the chart container full width", () => {
const source = readSource();
expect(source).toContain('ref="sourceChartRef"');
expect(source).toContain("const sourceChartRef = ref<{ resize?: () => void } | null>(null)");
expect(source).toContain("const resizeSourceCharts = async () =>");
expect(source).toContain("sourceChartRef.value?.resize?.()");
expect(source).toContain("await resizeSourceCharts()");
expect(source).toContain("defineExpose({ refresh: loadData, resize: resizeSourceCharts })");
expect(source).not.toContain(".source-map :deep(> div)");
});
it("uses source-analysis copy instead of IP-location tab wording", () => {
const source = readSource();
expect(source).toContain("访问来源分析");
expect(source).toContain("来源分布");
expect(source).toContain("热点来源地");
expect(source).not.toContain("查看访问来源的地理分布");
expect(source).not.toContain("访问来源地理分布热力图");
expect(source).not.toContain("按访问次数排序");
expect(source).not.toContain("IP 属地分析");
expect(source).not.toContain("中国地图");
expect(source).not.toContain("热点属地");
expect(source).not.toContain("用户分布图");
});
it("uses the standard geojson.cn China map data instead of generated rectangles", () => {
const source = readSource();
const mapSource = readMapSource();
expect(source).toContain("../assets/china.json");
expect(mapSource).toContain("geojson.cn/api/china/1.6.3/china.json");
expect(mapSource).not.toContain("ProvinceBox");
expect(mapSource).not.toContain("provinceBoxes");
});
it("removes heat-map visual legends and keeps map zoom interactions disabled", () => {
const source = readSource();
expect(source).not.toContain("visualMap:");
expect(source).not.toContain("VisualMapComponent");
expect(source).not.toContain("hasMapData");
expect(source).not.toContain("maxValue");
expect(source).not.toContain('color: ["#93c5fd", "#60a5fa", "#3b82f6", "#2563eb", "#1d4ed8"]');
expect(source).not.toContain("outOfRange:");
expect(source).toContain("roam: false");
expect(source).not.toContain("scaleLimit");
});
it("uses a stronger base map style so borders remain visible behind flow lines", () => {
const source = readSource();
expect(source).toContain('areaColor: "#e6edf8"');
expect(source).toContain('borderColor: "#aebed2"');
expect(source).toContain("borderWidth: 1");
expect(source).toContain('shadowColor: "rgba(37, 99, 235, 0.08)"');
expect(source).toContain("shadowBlur: 10");
expect(source).toContain("shadowOffsetY: 2");
expect(source).toContain('areaColor: "#dbeafe", borderColor: "#64748b", borderWidth: 1.3');
expect(source).not.toContain('areaColor: "#eef4ff"');
expect(source).not.toContain('borderColor: "#dbe7f7"');
expect(source).not.toContain("borderWidth: 0.8");
});
it("keeps geo hover interaction while avoiding empty white tooltip panels", () => {
const source = readSource();
expect(source).toContain("const formatGeoTooltip = (params: any) =>");
expect(source).toContain('return name || "暂无访问数据";');
expect(source).toContain('trigger: "item"');
expect(source).toContain("confine: true");
expect(source).toContain("formatter: formatGeoTooltip");
expect(source).toContain("tooltip: {\n show: true,\n formatter: formatGeoTooltip,\n }");
expect(source).toContain("formatLineTooltip");
expect(source).toContain("formatPointTooltip");
expect(source).toContain("tooltip: {\n show: true,\n formatter: formatLineTooltip,\n }");
expect(source).toContain("tooltip: {\n show: true,\n formatter: formatPointTooltip,\n }");
expect(source).not.toContain("silent: true");
expect(source).not.toContain("tooltip: { show: false }");
});
it("uses narrow metric cards without helper subtitles", () => {
const source = readSource();
expect(source).toContain("min-height: 64px");
expect(source).toContain("padding: 10px 16px");
expect(source).toContain("right: -34px");
expect(source).toContain("bottom: -42px");
expect(source).toContain("border-radius: 18px");
expect(source).not.toContain("<small>{{ card.hint }}</small>");
expect(source).not.toContain("hint:");
expect(source).not.toContain("权限检查总量");
});
it("removes the duplicated hero copy above the map", () => {
const source = readSource();
expect(source).toContain("ip-locations-toolbar");
expect(source).not.toContain("权限监控 / IP属地");
expect(source).not.toContain("按访问日志聚合省市、用户与来源 IP");
expect(source).not.toContain("悬停省份查看访问次数、访问用户数和来源 IP 数");
expect(source).not.toContain("hero-desc");
expect(source).not.toContain("eyebrow");
});
it("places the period filter toolbar on the right", () => {
const source = readSource();
expect(source).toContain(".ip-hero");
expect(source).toContain("justify-content: space-between");
expect(source).toContain("margin-left: auto");
});
it("keeps non-China source labels visible and lists top 10 regions", () => {
const source = readSource();
expect(source).toContain('row.country && row.country !== "中国" ? row.country : ""');
expect(source).toContain("row.isp");
expect(source).toContain('parts.join(" / ")');
expect(source).toContain(".slice(0, 10)");
});
});
@@ -1,52 +0,0 @@
import { describe, expect, it } from "vitest";
import { readFileSync } from "node:fs";
import { resolve } from "node:path";
const readSource = () => readFileSync(resolve(__dirname, "./PermissionTemplateSelector.vue"), "utf8");
describe("PermissionTemplateSelector", () => {
it("renders compact role permission cards without footer placeholder space", () => {
const source = readSource();
expect(source).toContain("角色权限概览");
expect(source).toContain("align-items: start");
expect(source).toContain("padding: 12px 14px");
expect(source).not.toContain("card-footer");
expect(source).not.toContain("点击编辑权限");
expect(source).not.toContain("inactive-label");
expect(source).not.toContain("edit-hint");
});
it("reads current project permissions by role key, not by role label", () => {
const source = readSource();
expect(source).toContain('v-for="template in sortedTemplates"');
expect(source).toContain('<span class="card-name">{{ template.name }}</span>');
expect(source).toContain("refreshKey?: number;");
expect(source).toContain("watch(() => props.refreshKey, loadTemplates);");
expect(source).not.toContain("ROLE_LABELS");
expect(source).toContain("template.category && props.currentPermissions[template.category]");
expect(source).toContain("enabledPercent(template.category)");
expect(source).toContain("countCurrentEnabled(template.category)");
expect(source).toContain("countCurrentDisabled(template.category)");
expect(source).toContain("const perms = props.currentPermissions?.[role];");
expect(source).toContain("emit('edit-role', template.category!)");
});
it("sorts role cards with the shared role template order", () => {
const source = readSource();
expect(source).toContain("useRoleTemplateMeta");
expect(source).toContain("compareRolesByTemplateOrder");
expect(source).toContain("const sortedTemplates = computed");
expect(source).toContain("compareRolesByTemplateOrder(a.category, b.category)");
});
it("does not keep hard-coded preset role labels or icons", () => {
const source = readSource();
expect(source).not.toContain('QA: "QA"');
expect(source).not.toContain('CTA: "CTA"');
expect(source).not.toContain('QA: "✅"');
});
});
@@ -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, "./PermissionTrendCharts.vue"), "utf8");
describe("PermissionTrendCharts", () => {
it("uses operation-oriented trend copy", () => {
const source = readSource();
expect(source).toContain("系统性能趋势");
expect(source).toContain("监测事件趋势");
expect(source).toContain("响应时间趋势");
expect(source).toContain("缓存命中率");
expect(source).toContain("异常拒绝率趋势");
expect(source).toContain('name: "异常拒绝率"');
expect(source).not.toContain("数据趋势");
expect(source).not.toContain("查看系统运行监测指标的变化趋势");
expect(source).not.toContain("查看权限系统各项指标的变化趋势");
expect(source).not.toContain("检查量趋势");
expect(source).not.toContain("<h4>拒绝率趋势</h4>");
expect(source).not.toContain('name: "拒绝率"');
});
});
@@ -1,20 +0,0 @@
import { describe, expect, it } from "vitest";
import { readFileSync } from "node:fs";
import { resolve } from "node:path";
const readQuickActionsSource = () => readFileSync(resolve(__dirname, "./QuickActions.vue"), "utf8");
describe("QuickActions project permissions", () => {
it("filters quick action entries with the same route permission contract as sidebar and router", () => {
const source = readQuickActionsSource();
expect(source).toContain("useAuthStore");
expect(source).toContain("useStudyStore");
expect(source).toContain("getProjectRoutePermission");
expect(source).toContain("hasProjectPermission");
expect(source).toContain("isSystemAdmin");
expect(source).toContain("visibleActions");
expect(source).toContain("v-for=\"item in visibleActions\"");
expect(source).not.toContain("v-for=\"item in actions\"");
});
});
@@ -1,117 +0,0 @@
import { describe, expect, it } from "vitest";
import { readFileSync } from "node:fs";
import { resolve } from "node:path";
const readAttachmentList = () => readFileSync(resolve(__dirname, "./AttachmentList.vue"), "utf8");
describe("AttachmentList permissions", () => {
it("does not require project member list permission to render uploaders", () => {
const source = readAttachmentList();
expect(source).not.toContain("listMembers");
});
it("matches the contract-fee attachment table layout", () => {
const source = readAttachmentList();
expect(source).toContain('prop="filename" :label="TEXT.common.labels.filename" min-width="360" show-overflow-tooltip');
expect(source).toContain(':label="TEXT.common.labels.size" min-width="180"');
expect(source).toContain(':label="TEXT.common.labels.uploader" min-width="180"');
expect(source).toContain('prop="uploaded_at" :label="TEXT.common.labels.uploadedAt" min-width="180" show-overflow-tooltip');
expect(source).toContain(':label="TEXT.common.labels.actions" min-width="180"');
expect(source).not.toContain('width="160"');
expect(source).toContain('class="attachment-actions"');
expect(source).toContain("flex-wrap: nowrap;");
expect(source).toContain("gap: 4px;");
});
it("can aggregate multiple attachment entity types into one table with an attachment type column", () => {
const source = readAttachmentList();
expect(source).toContain("entityGroups?: AttachmentEntityGroup[]");
expect(source).toContain('v-if="hasEntityGroups"');
expect(source).toContain('label="附件类型"');
expect(source).toContain("scope.row.attachment_type_label");
expect(source).toContain("fetchAttachments(props.studyId, group.entityType, props.entityId)");
expect(source).toContain("attachment_type_label: group.label");
expect(source).toContain("attachment_entity_type: group.entityType");
});
it("can reload attachments when the parent refresh key changes", () => {
const source = readAttachmentList();
expect(source).toContain("refreshKey?: number");
expect(source).toContain("() => props.refreshKey");
expect(source).toContain("load();");
});
it("can hide the uploader while keeping the attachment table visible", () => {
const source = readAttachmentList();
expect(source).toContain("hideUploader?: boolean");
expect(source).toContain("canShowUploader");
expect(source).toContain("!props.hideUploader && canCreate.value");
});
it("hides the default attachment title row when uploader is hidden and no explicit title is provided", () => {
const source = readAttachmentList();
expect(source).toContain("const showHeader = computed(() => !!props.title || canShowUploader.value)");
expect(source).toContain('<div v-if="showHeader" class="header">');
});
it("uses the shared upload implementation for table-mode immediate uploads", () => {
const source = readAttachmentList();
expect(source).not.toContain("AttachmentUploader");
expect(source).toContain(':http-request="uploadImmediate"');
expect(source).toContain("const uploadImmediate = async");
expect(source).toContain("await uploadFile(group, file, props.entityId)");
expect(source).toContain("ElMessage.success(TEXT.common.messages.uploadSuccess)");
expect(source).toContain("load();");
});
it("supports upload-card mode without rendering the table header", () => {
const source = readAttachmentList();
expect(source).toContain('mode?: "table" | "upload"');
expect(source).toContain("displayMode");
expect(source).toContain('v-if="displayMode === \'table\'"');
expect(source).toContain("v-else");
expect(source).toContain('class="attachment-upload-card"');
expect(source).toContain(':auto-upload="false"');
expect(source).toContain("queuePendingUpload(group.key, file)");
expect(source).toContain("点击上传文件");
});
it("allows create forms to choose attachments before the entity id exists", () => {
const source = readAttachmentList();
expect(source).toContain("const pendingSnapshot = () =>");
expect(source).toContain("const uploadPending = async");
expect(source).toContain("const targetEntityId = entityId || props.entityId");
expect(source).toContain("defineExpose({");
expect(source).toContain("pendingSnapshot");
expect(source).toContain("uploadPending");
expect(source).toContain("pendingMap[group.key] = remainItems");
expect(source).toContain("throw new Error(TEXT.common.messages.uploadFailed)");
});
it("fails pending upload when selected files have no saved entity id", () => {
const source = readAttachmentList();
expect(source).toContain("const hasPendingUploads = computed");
expect(source).toContain("if (!targetEntityId && hasPendingUploads.value)");
expect(source).toContain("throw new Error(TEXT.common.messages.uploadFailed)");
});
it("uses entity groups as upload groups when an editor has multiple attachment types", () => {
const source = readAttachmentList();
expect(source).toContain("const uploadGroups = computed");
expect(source).toContain("props.entityGroups.map((group)");
expect(source).toContain("key: group.entityType");
expect(source).toContain("entityType: group.entityType");
expect(source).toContain("uploadAttachment(props.studyId, group.entityType, targetEntityId, file");
});
});