8c8327df92
- 重构 DesktopPreferences 为分栏式设置面板,整合连接、外观、通知、更新与诊断信息分区,并补充过渡动效与暗色主题样式 - DesktopLayout 侧边栏导航分组支持展开折叠,调整管理/项目区块顺序并统一图标与标题 - 新增 fileTaskFeedback 工具,统一 pickFiles/saveFile/openFile 的成功/取消提示,替换审计导出、权限日志、附件、文档、线程、项目配置等处的直接调用 - desktopUpdateManager 暴露更新状态快照与状态变更监听,区分检查中、安装中、已推迟、失败等状态 - DesktopServerSettings 增加连接诊断信息(检查时间、健康地址、耗时、HTTP 状态) - unified-page.css 与 ProjectMilestones 引入 CSS 变量以适配暗色主题 - WebLayout 将服务器设置入口改为打开系统偏好面板,管理菜单中邮件服务归入系统设置分组 - ProfileSettings 移除已迁入偏好面板的桌面端专属区块 - 补充 Layout.desktop 布局与偏好面板契约测试
233 lines
14 KiB
TypeScript
233 lines
14 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import { readFileSync } from "node:fs";
|
|
import { resolve } from "node:path";
|
|
|
|
const readLayoutSource = () => readFileSync(resolve(__dirname, "./Layout.vue"), "utf8");
|
|
const readDesktopLayoutSource = () => readFileSync(resolve(__dirname, "./DesktopLayout.vue"), "utf8");
|
|
const readWebLayoutSource = () => readFileSync(resolve(__dirname, "./WebLayout.vue"), "utf8");
|
|
const readNavigationSource = () => readFileSync(resolve(__dirname, "./layout/navigation.ts"), "utf8");
|
|
const readDesktopPreferencesSource = () => readFileSync(resolve(__dirname, "../views/DesktopPreferences.vue"), "utf8");
|
|
const readDesktopServerSettingsSource = () => readFileSync(resolve(__dirname, "../views/DesktopServerSettings.vue"), "utf8");
|
|
const readDesktopUpdateManagerSource = () => readFileSync(resolve(__dirname, "../session/desktopUpdateManager.ts"), "utf8");
|
|
const readProfileSettingsSource = () => readFileSync(resolve(__dirname, "../views/ProfileSettings.vue"), "utf8");
|
|
const readFileTaskFeedbackSource = () => readFileSync(resolve(__dirname, "../utils/fileTaskFeedback.ts"), "utf8");
|
|
const readAttachmentListSource = () => readFileSync(resolve(__dirname, "./attachments/AttachmentList.vue"), "utf8");
|
|
const readDocumentDetailSource = () => readFileSync(resolve(__dirname, "../views/documents/DocumentDetail.vue"), "utf8");
|
|
|
|
describe("desktop layout shell", () => {
|
|
it("routes desktop and web shells through the runtime flag", () => {
|
|
const source = readLayoutSource();
|
|
|
|
expect(source).toContain("const isDesktop = isTauriRuntime()");
|
|
expect(source).toContain("<DesktopLayout v-if=\"isDesktop\" />");
|
|
expect(source).toContain("<WebLayout v-else />");
|
|
});
|
|
|
|
it("uses route-only desktop preference storage", () => {
|
|
const source = readDesktopLayoutSource();
|
|
|
|
expect(source).toContain("readDesktopRecentRoutes");
|
|
expect(source).toContain("readDesktopFavoriteRoutes");
|
|
expect(source).toContain("recordDesktopRecentRoute");
|
|
expect(source).toContain("currentDesktopRoutePreference");
|
|
});
|
|
|
|
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 desktop context labels and command entry points de-duplicated", () => {
|
|
const source = readDesktopLayoutSource();
|
|
|
|
expect(source).toContain("<h1>{{ TEXT.common.appName }} Desktop</h1>");
|
|
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: 48px auto minmax(0, 1fr);");
|
|
expect(source).toContain("const adminDesktopNavigationItems");
|
|
expect(source).toContain("const projectDesktopNavigationItems");
|
|
expect(source).not.toContain("const root = study.currentStudy?.name || TEXT.menu.currentProject");
|
|
});
|
|
|
|
it("renders desktop preferences as a split settings panel", () => {
|
|
const preferences = readDesktopPreferencesSource();
|
|
const desktopLayout = readDesktopLayoutSource();
|
|
const webLayout = readWebLayoutSource();
|
|
|
|
expect(preferences).toContain('class="preferences-sidebar"');
|
|
expect(preferences).toContain('class="preferences-pane"');
|
|
expect(preferences).toContain("const activeSectionId = ref<PreferenceSectionId>");
|
|
expect(preferences).toContain('grid-template-columns: 232px minmax(0, 1fr);');
|
|
expect(preferences).toContain("height: min(700px, calc(100vh - 96px));");
|
|
expect(preferences).toContain("scrollbar-gutter: stable;");
|
|
expect(preferences).toContain('{ id: "connection", label: "连接"');
|
|
expect(preferences).toContain('{ id: "appearance", label: "外观"');
|
|
expect(preferences).toContain('{ id: "notifications", label: "通知"');
|
|
expect(preferences).toContain('{ id: "updates", label: "更新"');
|
|
expect(preferences).toContain('{ id: "diagnostics", label: "诊断信息"');
|
|
expect(preferences).toContain("testDesktopServerConnection");
|
|
expect(preferences).toContain("saveDesktopServerConfig");
|
|
expect(preferences).toContain("测试连通性");
|
|
expect(preferences).not.toContain("保存服务器");
|
|
expect(preferences).toContain('ElMessage.success("服务器设置已保存")');
|
|
expect(preferences).toContain("ElMessage.error(`保存失败:${message}`)");
|
|
expect(preferences).toContain("normalizeDesktopServerUrl");
|
|
expect(preferences).toContain("确认切换服务器");
|
|
expect(preferences).toContain('<Transition name="preferences-header" mode="out-in">');
|
|
expect(preferences).toContain('<Transition name="preferences-panel" mode="out-in">');
|
|
expect(preferences).toContain('<Transition name="connection-feedback">');
|
|
expect(preferences).toContain("const selectSection = (sectionId: PreferenceSectionId)");
|
|
expect(preferences).toContain("const setConnectionPending = (message: string)");
|
|
expect(preferences).toContain("const scheduleConnectionPending = (message: string)");
|
|
expect(preferences).toContain("}, 180);");
|
|
expect(preferences).toContain('scrollTo({ top: 0, behavior: "smooth" })');
|
|
expect(preferences).not.toContain("connectionStatus.value = null");
|
|
expect(preferences).toContain("min-width: 100px;");
|
|
expect(preferences).toContain("connection-feedback-enter-active");
|
|
expect(preferences).toContain("@media (prefers-reduced-motion: reduce)");
|
|
expect(preferences).toContain(':global([data-ctms-theme="dark"] .desktop-preferences)');
|
|
expect(preferences).not.toContain(':global([data-ctms-theme="dark"]) .desktop-preferences');
|
|
expect(desktopLayout).toContain(':global([data-ctms-theme="dark"] .desktop-workbench)');
|
|
expect(desktopLayout).not.toContain(':global([data-ctms-theme="dark"]) .desktop-workbench');
|
|
expect(desktopLayout).toContain('width="940px"');
|
|
expect(webLayout).toContain('width="940px"');
|
|
expect(desktopLayout).toContain('title: "连接设置"');
|
|
expect(webLayout).toContain('title: "连接设置"');
|
|
expect(desktopLayout).not.toContain('router.push("/desktop/server-settings")');
|
|
expect(webLayout).not.toContain('router.push("/desktop/server-settings")');
|
|
expect(desktopLayout).not.toContain('width="720px"');
|
|
expect(webLayout).not.toContain('width="720px"');
|
|
});
|
|
|
|
it("keeps desktop notification and diagnostics settings out of profile settings", () => {
|
|
const profile = readProfileSettingsSource();
|
|
const preferences = readDesktopPreferencesSource();
|
|
|
|
expect(profile).not.toContain("form-section--desktop");
|
|
expect(profile).not.toContain("客户端与通知");
|
|
expect(profile).not.toContain("getDesktopNotificationSubscription");
|
|
expect(profile).not.toContain("setDesktopNotificationSubscription");
|
|
expect(profile).not.toContain("checkDesktopUpdateAndPrompt");
|
|
expect(profile).not.toContain("clientMetadataRows");
|
|
expect(profile).not.toContain("desktopNotificationsEnabled");
|
|
expect(preferences).toContain("getDesktopNotificationSubscription");
|
|
expect(preferences).toContain("checkDesktopUpdateAndPrompt");
|
|
expect(preferences).toContain("clientMetadataRows");
|
|
expect(preferences).not.toContain('{ label: "主题"');
|
|
expect(preferences).not.toContain('{ label: "服务器"');
|
|
});
|
|
|
|
it("keeps workspace tabs stable and draggable like browser tabs", () => {
|
|
const source = readDesktopLayoutSource();
|
|
const tabsStart = source.indexOf('<div class="workspace-tabs"');
|
|
const tabsEnd = source.indexOf('<main class="desktop-content"', tabsStart);
|
|
const tabsTemplate = source.slice(tabsStart, tabsEnd);
|
|
|
|
expect(tabsTemplate).toContain('@click="navigateWorkspaceTab(item.path)"');
|
|
expect(tabsTemplate).toContain('draggable="true"');
|
|
expect(tabsTemplate).toContain('@dragstart="startWorkspaceTabDrag(item.path, $event)"');
|
|
expect(tabsTemplate).toContain('@drop.prevent="dropWorkspaceTab(item.path, $event)"');
|
|
expect(source).toContain("const draggingWorkspaceTabPath = ref(\"\")");
|
|
expect(source).toContain("const navigateWorkspaceTab = (path: string) => {");
|
|
expect(source).toContain("if (activeMenu.value === path) return;");
|
|
expect(source).toContain("next[existingIndex] = { ...next[existingIndex], ...item };");
|
|
expect(source).toContain("const [moved] = next.splice(sourceIndex, 1);");
|
|
expect(source).toContain("<KeepAlive :max=\"DESKTOP_WORKSPACE_TAB_CACHE_MAX\">");
|
|
expect(source).toContain(":key=\"desktopRouteCacheKey(currentRoute)\"");
|
|
expect(source).toContain("const desktopRouteCacheKey = (currentRoute: { fullPath: string }) => currentRoute.fullPath;");
|
|
expect(source).not.toContain('<div :key="currentRoute.fullPath" class="desktop-route-shell">');
|
|
expect(tabsTemplate).not.toContain('@click="router.push(item.path)"');
|
|
expect(source).not.toContain(".slice(-7)");
|
|
expect(tabsTemplate).toContain('@contextmenu.prevent.stop="openWorkspaceTabMenu(item, $event)"');
|
|
expect(source).toContain("const lastClosedWorkspaceTab = ref<DesktopRoutePreference | null>(null);");
|
|
expect(source).toContain("const workspaceTabMenu = ref<");
|
|
expect(source).toContain("const closeOtherWorkspaceTabs = (path: string) => {");
|
|
expect(source).toContain("const restoreLastClosedWorkspaceTab = () => {");
|
|
expect(source).toContain('id: `workspace-tab:${item.path}`');
|
|
expect(source).toContain('title: `切换标签:${item.title}`');
|
|
expect(source).toContain('title: "重新打开最近关闭标签"');
|
|
});
|
|
|
|
it("keeps desktop navigation hierarchy aligned with the web sidebar", () => {
|
|
const desktopLayout = readDesktopLayoutSource();
|
|
const navigation = readNavigationSource();
|
|
const sidebarStart = desktopLayout.indexOf('<div class="sidebar-scroll">');
|
|
const sidebarEnd = desktopLayout.indexOf("</aside>", sidebarStart);
|
|
const sidebarTemplate = desktopLayout.slice(sidebarStart, sidebarEnd);
|
|
|
|
expect(sidebarTemplate.indexOf('v-if="adminNavigationItems.length"')).toBeLessThan(
|
|
sidebarTemplate.indexOf('v-if="projectNavigationItems.length"'),
|
|
);
|
|
expect(sidebarTemplate).toContain('@click="toggleNavigationGroup(item)"');
|
|
expect(sidebarTemplate).toContain(':aria-expanded="isNavigationGroupExpanded(item)"');
|
|
expect(sidebarTemplate).toContain('v-show="isNavigationGroupExpanded(item)"');
|
|
expect(desktopLayout).toContain("const expandedNavigationGroups = ref<Set<string>>(new Set());");
|
|
expect(navigation).toContain('label: "系统设置"');
|
|
expect(navigation).toContain('label: "邮件服务"');
|
|
expect(navigation).toContain('group: "系统设置"');
|
|
expect(navigation).toContain("item.children?.length ? item.children : [item]");
|
|
});
|
|
|
|
it("keeps notification and update preferences lightweight", () => {
|
|
const serverSettings = readDesktopServerSettingsSource();
|
|
const preferences = readDesktopPreferencesSource();
|
|
const desktopUpdateManager = readDesktopUpdateManagerSource();
|
|
const desktopLayout = readDesktopLayoutSource();
|
|
|
|
expect(serverSettings).toContain("connectionDiagnostic");
|
|
expect(serverSettings).not.toContain("复制连接诊断");
|
|
expect(serverSettings).not.toContain("copyConnectionDiagnostic");
|
|
expect(serverSettings).toContain("确认切换服务器");
|
|
expect(serverSettings).toContain("切换服务器会退出当前会话并清除当前项目上下文");
|
|
expect(serverSettings).not.toContain("服务器连接已确认");
|
|
expect(preferences).toContain("connectionDiagnostic");
|
|
expect(preferences).not.toContain("复制连接诊断");
|
|
expect(preferences).not.toContain("copyConnectionDiagnostic");
|
|
expect(preferences).not.toContain("服务器连通性正常");
|
|
expect(preferences).not.toContain("服务器连接已确认");
|
|
expect(preferences).not.toContain("系统通知已开启");
|
|
expect(preferences).not.toContain("系统通知已关闭");
|
|
expect(preferences).not.toContain("通知诊断");
|
|
expect(preferences).not.toContain("更新诊断");
|
|
expect(preferences).not.toContain("listenDesktopNotificationDiagnostics");
|
|
expect(preferences).not.toContain("发送测试通知");
|
|
expect(preferences).toContain("listenDesktopUpdateStatus");
|
|
expect(preferences).toContain("promptForPendingDesktopUpdate");
|
|
expect(preferences).toContain("当前已是最新版本");
|
|
expect(desktopUpdateManager).not.toContain("当前构建未启用桌面端自动更新");
|
|
expect(desktopUpdateManager).not.toContain("该版本已选择稍后提醒");
|
|
expect(desktopUpdateManager).not.toContain("当前已是最新版本");
|
|
expect(desktopUpdateManager).toContain("桌面端更新检查失败,请稍后重试或联系管理员。");
|
|
expect(desktopLayout).toContain("desktopUpdateNoticeVisible");
|
|
expect(desktopLayout).toContain("listenDesktopUpdateStatus");
|
|
expect(desktopLayout).toContain("新版本 ${pending.version}");
|
|
});
|
|
|
|
it("routes user-facing file actions through feedback helpers", () => {
|
|
const helper = readFileTaskFeedbackSource();
|
|
const attachments = readAttachmentListSource();
|
|
const documentDetail = readDocumentDetailSource();
|
|
|
|
expect(helper).toContain("export const pickFilesWithFeedback");
|
|
expect(helper).toContain("export const saveFileWithFeedback");
|
|
expect(helper).toContain("export const openFileWithFeedback");
|
|
expect(attachments).toContain("pickFilesWithFeedback");
|
|
expect(attachments).toContain("saveFileWithFeedback");
|
|
expect(attachments).toContain("openFileWithFeedback");
|
|
expect(documentDetail).toContain("pickFilesWithFeedback");
|
|
expect(documentDetail).toContain("saveFileWithFeedback");
|
|
expect(documentDetail).toContain("openFileWithFeedback");
|
|
expect(attachments).not.toContain("openFile, pickFiles, saveFile");
|
|
expect(documentDetail).not.toContain("openFile, pickFiles, saveFile");
|
|
});
|
|
});
|