Files
ctms/frontend/src/views/admin/Projects.test.ts
T
Cheng Zhou 1dc10f569d
Client Quality Gates / Shared client and Web (push) Has been cancelled
Client Quality Gates / macOS Desktop (push) Has been cancelled
feat(网页端): 完善登录后工作台与项目管理体验
2026-07-09 14:59:06 +08:00

147 lines
7.2 KiB
TypeScript

import { describe, expect, it } from "vitest";
import { readFileSync } from "node:fs";
import { resolve } from "node:path";
const readProjects = () => readFileSync(resolve(__dirname, "./Projects.vue"), "utf8");
const readRouter = () => readFileSync(resolve(__dirname, "../../router/index.ts"), "utf8");
const readLayout = () => [
"../../components/WebLayout.vue",
"../../components/DesktopLayout.vue",
"../../components/layout/navigation.ts",
].map(path => readFileSync(resolve(__dirname, path), "utf8")).join("\n");
describe("project management access", () => {
it("shows project management to all signed-in users while keeping system operations admin-only", () => {
const projects = readProjects();
const layout = readLayout();
const router = readRouter();
expect(layout).toContain('v-if="auth.user"');
expect(layout).toContain('index="/admin/projects"');
expect(router).toContain('name: "AdminProjects"');
expect(router).toContain("meta: { title: TEXT.menu.projectManagement }");
expect(projects).toContain('v-if="isAdmin" type="primary"');
expect(projects).toContain('v-if="isAdmin && !scope.row.is_locked"');
expect(projects).toContain('v-if="isAdmin" :content="TEXT.common.actions.delete"');
});
it("keeps management backend modules under the project management page instead of sidebar shortcuts", () => {
const layout = readLayout();
expect(layout).not.toContain("canAccessPmAdminBackend");
expect(layout).not.toContain("canAccessPmAdminModule");
expect(layout).not.toContain("TEXT.modules.adminProjectMembers.memberLabel");
expect(layout).toContain('index="/admin/projects"');
});
it("removes the non-admin personal dashboard menu entry", () => {
const layout = readLayout();
const removedPath = `/${["work", "bench"].join("")}`;
const removedMenuKey = ["TEXT.menu.work", "bench"].join("");
expect(layout).not.toContain(`index="${removedPath}"`);
expect(layout).not.toContain(removedMenuKey);
});
it("uses each project permission matrix to show project management actions", () => {
const source = readProjects();
expect(source).toContain("fetchMyApiEndpointPermissions(item.id, { suppressErrorMessage: true })");
expect(source).toContain("permissionsByProject");
expect(source).toContain("suppressErrorMessage: true");
expect(source).toContain("const canProject = (row: Study, module: string, action: \"read\" | \"write\")");
expect(source).toContain("canProject(scope.row, 'project_members', 'read')");
expect(source).toContain("canConfigureProject(scope.row)");
expect(source).toContain("canManageProjectBackend(scope.row)");
expect(source).toContain("canProject(scope.row, 'audit_export', 'read')");
});
it("shows the current project role only for non-admin project lists", () => {
const source = readProjects();
expect(source).toContain('v-if="!isAdmin" label="我的角色" align="center"');
expect(source).toContain("roleLabel(scope.row.role_in_study) || '-'");
expect(source).toContain("const { roleLabel, loadRoleTemplates } = useRoleTemplateMeta();");
expect(source).toContain("loadRoleTemplates().catch(() => {});");
});
it("keeps project table columns evenly spaced after adding the role column", () => {
const source = readProjects();
expect(source).toContain('class="project-table" style="width: 100%" table-layout="fixed"');
expect(source).toContain(':label="TEXT.common.fields.projectName" min-width="260"');
expect(source).toContain(`:label="TEXT.common.fields.protocolNo || '方案号'" min-width="220" show-overflow-tooltip`);
expect(source).toContain('v-if="!isAdmin" label="我的角色" align="center" width="120"');
expect(source).toContain(':label="TEXT.common.fields.status" align="center" width="150"');
expect(source).toContain('label="锁定状态" align="center" width="150"');
expect(source).toContain(':label="TEXT.common.labels.actions" align="center" width="300" fixed="right"');
expect(source).not.toContain('projectColumnWidth');
expect(source).toContain("min-width: 252px;");
expect(source).toContain("min-width: 1060px;");
});
it("keeps the project summary header compact", () => {
const source = readProjects();
expect(source).toContain("padding: 10px 16px;");
expect(source).toContain("padding: 9px 12px;");
expect(source).toContain("width: 32px;");
expect(source).toContain("height: 32px;");
expect(source).toContain("font-size: 20px;");
expect(source).toContain("font-size: 11px;");
});
it("renders project names as plain text and moves setup configuration to a gear action", () => {
const source = readProjects();
expect(source).not.toContain("<el-link");
expect(source).not.toContain('@click="goProject(scope.row)" class="project-name"');
expect(source).toContain('<span class="project-name">{{ scope.row.name }}</span>');
expect(source).toContain('v-if="canProject(scope.row, \'setup_config\', \'write\')" content="项目配置"');
expect(source).toContain(':icon="Setting"');
expect(source).toContain('@click="goProject(scope.row)"');
expect(source).toContain('@click="enterStudy(scope.row)"');
expect(source).toContain('if (!canProject(row, "setup_config", "write"))');
});
it("shows project permission configuration only for admins and target project PMs", () => {
const source = readProjects();
expect(source).toContain('v-if="canConfigureProject(scope.row)" content="权限管理"');
expect(source).toContain("const canConfigureProject = (row: Study) => canManageProjectBackend(row);");
expect(source).not.toContain("canProject(scope.row, 'project_members', 'write')");
});
it("shows center management only for admins and target project PMs", () => {
const source = readProjects();
expect(source).toContain('v-if="canManageProjectBackend(scope.row)" :content="TEXT.modules.adminProjects.sites"');
expect(source).toContain("const canManageProjectBackend = (row: Study) => isAdmin.value || row.role_in_study === \"PM\";");
expect(source).not.toContain("canProject(scope.row, 'sites', 'read')");
});
it("checks project management actions against the current row role instead of the first permission role", () => {
const source = readProjects();
expect(source).toContain("const role = row.role_in_study || \"\";");
expect(source).toContain("const rolePerms = perms[role];");
expect(source).not.toContain("Object.values(perms)[0]");
});
it("keeps audit export write mappings on the export operation", () => {
const projects = readProjects();
const router = readRouter();
expect(projects).toContain('audit_export: { read: "audit_logs:read", write: "audit_logs:export" }');
expect(router).toContain('audit_export: { read: "audit_logs:read", write: "audit_logs:export" }');
expect(projects).not.toContain('audit_export: { read: "audit_logs:read", write: "audit_logs:read" }');
expect(router).not.toContain('audit_export: { read: "audit_logs:read", write: "audit_logs:read" }');
});
it("opens audit logs with an explicit project id", () => {
const projects = readProjects();
expect(projects).toContain('await enterStudy(row, `/admin/audit-logs?projectId=${row.id}`)');
});
});