import { describe, expect, it } from "vitest"; import { readFileSync } from "node:fs"; import { resolve } from "node:path"; const normalizeSource = (source: string) => source.replace(/\r\n/g, "\n"); const readSource = (path: string) => normalizeSource(readFileSync(path, "utf8")); const readLayoutSource = () => readSource(resolve(__dirname, "./Layout.vue")); const readAppSource = () => readSource(resolve(__dirname, "../App.vue")); const readDesktopLayoutSource = () => readSource(resolve(__dirname, "./DesktopLayout.vue")); const readWebLayoutSource = () => readSource(resolve(__dirname, "./WebLayout.vue")); const readNavigationSource = () => readSource(resolve(__dirname, "./layout/navigation.ts")); const readTauriConfigSource = () => readSource(resolve(__dirname, "../../src-tauri/tauri.conf.json")); const readTauriLibSource = () => readSource(resolve(__dirname, "../../src-tauri/src/lib.rs")); const readDesktopPreferencesSource = () => readSource(resolve(__dirname, "../views/DesktopPreferences.vue")); const readDesktopServerSettingsSource = () => readSource(resolve(__dirname, "../views/DesktopServerSettings.vue")); const readDesktopUpdateManagerSource = () => readSource(resolve(__dirname, "../session/desktopUpdateManager.ts")); const readDesktopNotificationManagerSource = () => readSource(resolve(__dirname, "../session/desktopNotificationManager.ts")); const readDesktopActivityCenterSource = () => readSource(resolve(__dirname, "../session/desktopActivityCenter.ts")); const readProfileSettingsSource = () => readSource(resolve(__dirname, "../views/ProfileSettings.vue")); const readLoginSource = () => readSource(resolve(__dirname, "../views/Login.vue")); const readRegisterSource = () => readSource(resolve(__dirname, "../views/Register.vue")); const readForgotPasswordSource = () => readSource(resolve(__dirname, "../views/ForgotPassword.vue")); const readFileTaskFeedbackSource = () => readSource(resolve(__dirname, "../utils/fileTaskFeedback.ts")); const readAttachmentListSource = () => readSource(resolve(__dirname, "./attachments/AttachmentList.vue")); const readDocumentDetailSource = () => readSource(resolve(__dirname, "../views/documents/DocumentDetail.vue")); const readMainStyleSource = () => readSource(resolve(__dirname, "../styles/main.css")); const readFaqSource = () => readSource(resolve(__dirname, "../views/Faq.vue")); const readFaqListSource = () => readSource(resolve(__dirname, "./FaqList.vue")); const readProjectOverviewSource = () => readSource(resolve(__dirname, "../views/ia/ProjectOverview.vue")); const readProjectMilestonesSource = () => readSource(resolve(__dirname, "../views/ia/ProjectMilestones.vue")); describe("desktop layout shell", () => { it("routes desktop and web shells through the runtime flag", () => { const source = readLayoutSource(); const app = readAppSource(); expect(source).toContain("const isDesktop = isTauriRuntime()"); expect(source).toContain('defineAsyncComponent(() => import("./DesktopLayout.vue"))'); expect(source).toContain('defineAsyncComponent(() => import("./WebLayout.vue"))'); expect(source).toContain(""); expect(source).toContain(""); expect(app).toContain("const isDesktopRuntime = isTauriRuntime();"); expect(app).toContain("initializeDesktopThemePreference();"); expect(app).toContain("initializeDesktopMenuShortcutSync();"); expect(app).toContain('document.body.classList.add("is-desktop-runtime")'); expect(app).toContain('document.body.classList.remove("is-desktop-runtime")'); }); it("uses route-only desktop preference storage", () => { const source = readDesktopLayoutSource(); const webSource = readWebLayoutSource(); expect(source).toContain("readDesktopFavoriteRoutes"); expect(source).toContain("currentDesktopRoutePreference"); expect(source).toContain("toggleDesktopFavoriteRoute"); expect(source).not.toContain("readDesktopRecentRoutes"); expect(source).not.toContain("recordDesktopRecentRoute"); expect(source).not.toContain("desktopRecentRoutes"); expect(source).not.toContain("最近访问"); expect(webSource).toContain("readDesktopFavoriteRoutes"); expect(webSource).toContain("toggleDesktopFavoriteRoute"); expect(webSource).not.toContain("readDesktopRecentRoutes"); expect(webSource).not.toContain("recordDesktopRecentRoute"); expect(webSource).not.toContain("desktopRecentRoutes"); expect(webSource).not.toContain("最近访问"); }); it("shares active route mapping between web and desktop shells", () => { const webLayout = readWebLayoutSource(); const desktopLayout = readDesktopLayoutSource(); const navigation = readNavigationSource(); expect(webLayout).toContain("getActiveLayoutPath(route.path)"); expect(desktopLayout).toContain("getActiveLayoutPath(route.path)"); expect(navigation).toContain("export const getActiveLayoutPath"); }); it("keeps web project switching routed through the workbench entry", () => { const webLayout = readWebLayoutSource(); expect(webLayout).not.toContain("workbench-return-button"); expect(webLayout).toContain(''); expect(webLayout).toContain('await router.push("/workbench")'); expect(webLayout).toContain("const hasProjectContext = computed(() => Boolean(study.currentStudy) && !isAdminContext.value);"); expect(webLayout).toContain('v-if="hasProjectContext && hasAnyProjectModuleAccess"'); expect(webLayout).toContain("const showAdminNavigation = computed(() => Boolean(auth.user) && !hasProjectContext.value)"); expect(webLayout).not.toContain("hasDropdown: true"); expect(webLayout).not.toContain("type: 'study'"); expect(webLayout).not.toContain('type: "study"'); expect(webLayout).not.toContain("`切换项目:${item.name}`"); expect(webLayout).not.toContain("study.setCurrentStudy(cmd.value)"); expect(webLayout).not.toContain('router.push("/project/overview");'); }); it("keeps desktop context labels and command entry points de-duplicated", () => { const source = readDesktopLayoutSource(); expect(source).toContain("

{{ TEXT.common.appName }}

"); expect(source).toContain('const DESKTOP_ADMIN_SECTION_LABEL = "系统管理";'); expect(source).toContain('data-tauri-drag-region'); expect(source).toContain('class="toolbar-title"'); expect(source).toContain('class="toolbar-subtitle"'); expect(source).toContain("desktopToolbarTitle"); expect(source).toContain("desktopToolbarSubtitle"); expect(source).not.toContain("desktopBreadcrumbs"); expect(source).not.toContain("breadcrumb-chip"); expect(source).toContain('class="workspace-history-controls"'); expect(source).toContain('title="后退(当前任务)"'); expect(source).toContain('title="前进(当前任务)"'); expect(source).toContain(':disabled="!canNavigateWorkspaceTabBack"'); expect(source).toContain(':disabled="!canNavigateWorkspaceTabForward"'); expect(source).toContain('class="toolbar-history-divider"'); expect(source).toContain('v-for="(item, index) in desktopToolbarContextItems"'); expect(source).toContain('class="toolbar-context-link"'); expect(source).not.toContain('title="刷新当前视图"'); expect(source).not.toContain("const sidebarTitle"); expect(source).not.toContain('title="搜索命令"'); expect(source).not.toContain("desktop-statusbar"); expect(source).not.toContain("status-item"); expect(source).not.toContain("accountProjectRoleLabel"); expect(source).not.toContain("projectStatusLabel"); expect(source).toContain("grid-template-rows: 52px auto minmax(0, 1fr);"); expect(source).toContain("const adminDesktopNavigationItems"); expect(source).toContain("const projectDesktopNavigationItems"); expect(source).toContain("hasCurrentStudy: Boolean(study.currentStudy) && !isAdminContext.value"); expect(source).toContain('id: "desktop:refresh"'); expect(source).toContain("shortcut: formatDesktopShortcut(desktopShortcutPreferences.value.refresh)"); expect(source).toContain('command === "ctms.desktop.back"'); expect(source).toContain('command === "ctms.desktop.forward"'); expect(source).not.toContain("const root = study.currentStudy?.name || TEXT.menu.currentProject"); }); it("uses macOS overlay titlebar without adding window permissions", () => { const tauriConfig = readTauriConfigSource(); const layout = readDesktopLayoutSource(); expect(tauriConfig).toContain('"decorations": true'); expect(tauriConfig).toContain('"titleBarStyle": "Overlay"'); expect(tauriConfig).toContain('"hiddenTitle": true'); expect(tauriConfig).toContain('"backgroundColor": "#f9fafb"'); expect(tauriConfig).toContain('"backgroundThrottling": "disabled"'); expect(tauriConfig).toContain('"trafficLightPosition": { "x": 16, "y": 18 }'); expect(layout).toContain("padding: 44px 10px 10px;"); expect(layout).toContain("-webkit-app-region: drag;"); expect(layout).toContain("-webkit-app-region: no-drag;"); expect(layout).not.toContain(["@tauri-apps", "api", "window"].join("/")); }); it("uses a native macOS application menu and rebuilds a closed main window from the Dock", () => { const tauriLib = readTauriLibSource(); expect(tauriLib.indexOf("package.name.clone()")).toBeLessThan(tauriLib.indexOf('"文件"')); expect(tauriLib).toContain('Some("关于 CTMS")'); expect(tauriLib).toContain("short_version: Some(String::new())"); expect(tauriLib).toContain("icon: handle.default_window_icon().cloned()"); expect(tauriLib).toContain('"ctms.desktop.preferences"'); expect(tauriLib).toContain("PredefinedMenuItem::services"); expect(tauriLib).toContain("PredefinedMenuItem::hide_others"); expect(tauriLib).toContain("PredefinedMenuItem::show_all"); expect(tauriLib).toContain("WINDOW_SUBMENU_ID"); expect(tauriLib).toContain("HELP_SUBMENU_ID"); expect(tauriLib).toContain("TOGGLE_SIDEBAR_COMMAND_ID"); expect(tauriLib).toContain('"显示/隐藏导航栏"'); expect(tauriLib).toContain("RunEvent::Reopen"); expect(tauriLib).toContain("WebviewWindowBuilder::from_config"); expect(tauriLib).toContain('find(|config| config.label == "main")'); expect(tauriLib).not.toContain("WindowEvent::CloseRequested"); expect(tauriLib).not.toContain("api.prevent_close()"); expect(tauriLib).not.toContain("window.hide()"); }); it("keeps the desktop sidebar dense enough for navigation-heavy screens", () => { const layout = readDesktopLayoutSource(); const sidebarHeadStart = layout.indexOf('", sidebarHeadStart); const sidebarHeadTemplate = layout.slice(sidebarHeadStart, sidebarHeadEnd); expect(layout).toContain("grid-template-columns: 248px minmax(0, 1fr);"); expect(layout).toContain("padding: 44px 10px 10px;"); expect(layout).toContain("grid-template-columns: auto minmax(0, 1fr);"); expect(layout).toContain("margin-top: 0;"); expect(layout).toContain("const desktopProjectSectionLabel = computed(() => study.currentStudy?.name || TEXT.menu.currentProject)"); expect(layout).toContain("{{ desktopProjectSectionLabel }}"); expect(layout).toContain('command="projectEntry"'); expect(layout).toContain("projectEntryMenuLabel"); expect(layout).toContain("openDesktopProjectEntry"); expect(layout).toContain("const showSidebarProjectChooser = computed(() => !study.currentStudy && !isAdminContext.value);"); expect(layout).toContain("padding: 10px 8px 24px;"); expect(layout).toContain("margin-top: 12px;"); expect(layout).toContain("font-size: 14px;"); expect(sidebarHeadTemplate).toContain('