Files
MyGoNavi/frontend/src/App.tool-center.test.ts
Syngnat d210b3f39c 🐛 fix(window): 加固 Windows 冷启动半窗,原生最大化并兜底铺满工作区
- Windows 启动 WindowStartState 改为 Maximised,默认尺寸提升到 1440×900
- 无记忆/历史 1024×768/覆盖率过低的普通窗记忆改为启动最大化
- Maximise 多次失败时铺满工作区兜底,避免残留浮动半窗
- 用户刻意拉大的普通窗仍先 Unmaximise 再恢复尺寸位置
2026-07-09 14:02:53 +08:00

534 lines
35 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { describe, expect, it } from 'vitest';
import { readFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
const appSource = readFileSync(
fileURLToPath(new globalThis.URL('./App.tsx', import.meta.url)),
'utf8',
);
const appCss = readFileSync(
fileURLToPath(new globalThis.URL('./App.css', import.meta.url)),
'utf8',
);
const v2ThemeCss = readFileSync(
fileURLToPath(new globalThis.URL('./v2-theme.css', import.meta.url)),
'utf8',
);
const linuxCJKFontBannerSource = readFileSync(
fileURLToPath(new globalThis.URL('./components/LinuxCJKFontBanner.tsx', import.meta.url)),
'utf8',
);
const appUtilityStylesSource = readFileSync(
fileURLToPath(new globalThis.URL('./hooks/useAppUtilityStyles.tsx', import.meta.url)),
'utf8',
);
const appSidebarResizeSource = readFileSync(
fileURLToPath(new globalThis.URL('./hooks/useAppSidebarResize.ts', import.meta.url)),
'utf8',
);
const sidebarLayoutSource = readFileSync(
fileURLToPath(new globalThis.URL('./utils/sidebarLayout.ts', import.meta.url)),
'utf8',
);
const getGlobalShortcutCaseBlock = (action: string) => {
const caseToken = `case '${action}':`;
const start = appSource.indexOf(caseToken);
expect(start).toBeGreaterThan(-1);
const afterCase = appSource.slice(start + caseToken.length);
const nextCaseIndex = afterCase.search(/\n\s+case '[^']+':/);
const switchEndIndex = afterCase.indexOf("window.addEventListener('keydown', handleGlobalShortcut, true);");
const endIndex = nextCaseIndex >= 0 ? nextCaseIndex : switchEndIndex;
expect(endIndex).toBeGreaterThan(-1);
return afterCase.slice(0, endIndex);
};
describe('tool center menu entries', () => {
it('exposes snippet management next to shortcut management', () => {
expect(appSource).toContain("key: 'snippet-settings'");
expect(appSource).toContain("title: t('app.tools.entry.snippets.title')");
expect(appSource).toContain("description: t('app.tools.entry.snippets.description')");
expect(appSource).toContain("handleOpenToolCenterPane('workspace', 'snippet-settings')");
expect(appSource).toContain('gonavi:open-snippet-settings');
expect(appSource).toContain("setIsSnippetModalOpen(false);");
expect(appSource).not.toContain('setIsSnippetModalOpen(true)');
const snippetIndex = appSource.indexOf("key: 'snippet-settings'");
const shortcutIndex = appSource.indexOf("key: 'shortcut-settings'", snippetIndex);
expect(snippetIndex).toBeGreaterThan(-1);
expect(shortcutIndex).toBeGreaterThan(snippetIndex);
});
it('uses scalable side navigation for the tool center instead of horizontal segmented switching', () => {
expect(appSource).toContain("type ToolCenterGroupKey = 'config' | 'workflow' | 'workspace';");
expect(appSource).toContain("const [activeToolCenterGroupKey, setActiveToolCenterGroupKey] = useState<ToolCenterGroupKey>('config');");
expect(appSource).toContain("const [toolCenterBackGroupKey, setToolCenterBackGroupKey] = useState<ToolCenterGroupKey | null>(null);");
expect(appSource).toContain("title: t('app.tools.group.config.title')");
expect(appSource).toContain("title: t('app.tools.group.workflow.title')");
expect(appSource).toContain("title: t('app.tools.group.workspace.title')");
expect(appSource).toContain("filteredToolCenterGroups.find((group) => group.key === activeToolCenterGroupKey)");
expect(appUtilityStylesSource).toContain("const toolCenterModalSplitStyle = useMemo<React.CSSProperties>(() => ({");
expect(appUtilityStylesSource).toContain("gridTemplateColumns: '232px minmax(0, 1fr)'");
expect(appUtilityStylesSource).toContain("const toolCenterNavPanelStyle = useMemo<React.CSSProperties>(() => ({");
expect(appUtilityStylesSource).toContain("const toolCenterNavScrollStyle = useMemo<React.CSSProperties>(() => ({");
expect(appUtilityStylesSource).toContain("const toolCenterContentPanelStyle = useMemo<React.CSSProperties>(() => ({");
expect(appUtilityStylesSource).toContain("const toolCenterDetailPanelStyle = useMemo<React.CSSProperties>(() => ({");
expect(appUtilityStylesSource).toContain("const toolCenterDetailBodyStyle = useMemo<React.CSSProperties>(() => ({");
expect(appSource).toContain('role="tablist" aria-orientation="vertical"');
expect(appSource).toContain('role="tab"');
expect(appSource).toContain('aria-selected={active}');
expect(appSource).toContain('title={`${group.title} - ${group.description}`}');
expect(appUtilityStylesSource).toContain("borderRight: `1px solid ${overlayTheme.divider}`");
expect(appSource).toContain('setActiveToolCenterPane(null);');
expect(appSource).toContain('group.items.length');
expect(appSource).toContain("const handleOpenToolCenterPane = useCallback((group: ToolCenterGroupKey, key: ToolCenterPaneKey) => {");
expect(appSource).toContain("const [activeToolCenterPane, setActiveToolCenterPane] = useState<ToolCenterPaneState | null>(null);");
expect(appSource).toContain("const handleReturnToToolCenter = useCallback((closeChild?: () => void) => {");
expect(appSource).toContain("t('common.back_to_previous')");
expect(appSource).toContain("width={1080}");
expect(appSource).toContain('centered');
});
it('keeps the tool center modal height fixed across group switches and scrolls the list area internally', () => {
expect(appUtilityStylesSource).toContain('const toolCenterModalContentStyle = useMemo<React.CSSProperties>(() => ({');
expect(appUtilityStylesSource).toContain("height: 'min(820px, calc(100vh - 64px))'");
expect(appUtilityStylesSource).toContain("const toolCenterModalWorkspaceStyle = useMemo<React.CSSProperties>(() => ({");
expect(appUtilityStylesSource).toContain("const toolCenterModalSplitStyle = useMemo<React.CSSProperties>(() => ({");
expect(appUtilityStylesSource).toContain("const toolCenterScrollableListStyle = useMemo<React.CSSProperties>(() => ({");
expect(appSource).toContain("body: { paddingTop: 8, paddingBottom: 8, overflow: 'hidden', flex: 1, minHeight: 0 }");
expect(appSource).toContain('style={toolCenterModalWorkspaceStyle}');
expect(appSource).toContain('style={toolCenterModalSplitStyle}');
expect(appSource).toContain('style={toolCenterNavPanelStyle}');
expect(appSource).toContain('style={toolCenterNavScrollStyle}');
expect(appSource).toContain('style={toolCenterContentPanelStyle}');
expect(appSource).toContain('style={toolCenterDetailPanelStyle}');
expect(appSource).toContain('style={toolCenterDetailBodyStyle}');
expect(appSource).toContain('style={toolCenterScrollableListStyle}');
expect(appUtilityStylesSource).toContain("overflowY: 'auto'");
expect(appSource).toContain("borderTop: index === 0 ? `1px solid ${overlayTheme.divider}` : 'none'");
expect(appSource).toContain("borderBottom: `1px solid ${overlayTheme.divider}`");
});
it('lets the tool center detail header own embedded tool titles', () => {
const renderPaneStart = appSource.indexOf('const renderToolCenterPane = () => {');
const renderPaneSource = appSource.slice(
renderPaneStart,
appSource.indexOf('};\n\n return (', renderPaneStart),
);
const connectionPackageSource = renderPaneSource.slice(
renderPaneSource.indexOf("if (activeToolCenterPane.key === 'connection-package')"),
renderPaneSource.indexOf("if (activeToolCenterPane.key === 'data-root')"),
);
const dataRootSource = renderPaneSource.slice(
renderPaneSource.indexOf("if (activeToolCenterPane.key === 'data-root')"),
renderPaneSource.indexOf("if (activeToolCenterPane.key === 'security-update')"),
);
const securityUpdateSource = renderPaneSource.slice(
renderPaneSource.indexOf("if (activeToolCenterPane.key === 'security-update')"),
renderPaneSource.indexOf("activeToolCenterPane.key === 'schema-compare'"),
);
const dataSyncSource = renderPaneSource.slice(
renderPaneSource.indexOf("activeToolCenterPane.key === 'schema-compare'"),
renderPaneSource.indexOf("if (activeToolCenterPane.key === 'drivers')"),
);
const driverSource = renderPaneSource.slice(
renderPaneSource.indexOf("if (activeToolCenterPane.key === 'drivers')"),
renderPaneSource.indexOf("if (activeToolCenterPane.key === 'snippet-settings')"),
);
const snippetSource = renderPaneSource.slice(
renderPaneSource.indexOf("if (activeToolCenterPane.key === 'snippet-settings')"),
renderPaneSource.indexOf("if (activeToolCenterPane.key === 'shortcut-settings')"),
);
const shortcutSource = renderPaneSource.slice(
renderPaneSource.indexOf("if (activeToolCenterPane.key === 'shortcut-settings')"),
renderPaneSource.indexOf('return null;', renderPaneSource.indexOf("if (activeToolCenterPane.key === 'shortcut-settings')")),
);
expect(appSource).toContain('activeToolCenterPaneItem?.title ?? activeToolCenterGroup.title');
expect(connectionPackageSource).toContain('<ConnectionPackagePasswordModal');
expect(connectionPackageSource).not.toContain('renderUtilityModalTitle');
expect(dataRootSource).toContain('title={null}');
expect(dataRootSource).toContain('closable={false}');
expect(dataRootSource).not.toContain('renderUtilityModalTitle');
expect(securityUpdateSource).toContain('<SecurityUpdateSettingsModal');
expect(securityUpdateSource).not.toContain('renderUtilityModalTitle');
expect(dataSyncSource).toContain('<DataSyncModal');
expect(dataSyncSource).not.toContain('renderUtilityModalTitle');
expect(driverSource).toContain('<DriverManagerModal');
expect(driverSource).not.toContain('renderUtilityModalTitle');
expect(snippetSource).toContain('<SnippetSettingsModal');
expect(snippetSource).not.toContain('renderUtilityModalTitle');
expect(shortcutSource).toContain('title={null}');
expect(shortcutSource).toContain('closable={false}');
expect(shortcutSource).not.toContain('renderUtilityModalTitle');
});
it('does not render an extra top back button in the tool center detail header', () => {
const detailHeaderSource = appSource.slice(
appSource.indexOf("{activeToolCenterPane ? ("),
appSource.indexOf('<div style={toolCenterDetailBodyStyle}>', appSource.indexOf("{activeToolCenterPane ? (")),
);
expect(detailHeaderSource).toContain('activeToolCenterPaneItem?.title ?? activeToolCenterGroup.title');
expect(detailHeaderSource).toContain('activeToolCenterPaneItem?.description ?? activeToolCenterGroup.description');
expect(detailHeaderSource).not.toContain('<Button onClick={closeToolCenterPane}>');
expect(detailHeaderSource).not.toContain("{t('common.back_to_previous')}");
});
it('keeps the v2 AI entry in the sidebar and the legacy AI entry on the content edge', () => {
expect(appSource).toContain('onToggleAI={toggleAIPanel}');
expect(appSource).toContain('renderLegacyAIEdgeHandle');
expect(appSource).toContain('resolveLegacyAIEdgeHandleDockStyle');
expect(appSource).toContain('data-gonavi-legacy-ai-edge-action="true"');
expect(appSource).toContain('{!isV2Ui && !aiPanelVisible && (');
expect(appSource).toContain('{!isV2Ui && (');
expect(appSource).not.toContain('data-gonavi-ai-entry-action="true"');
});
it('keeps sidebar utility handlers stable so v2 button clicks do not repaint the workspace', () => {
expect(appSource).toContain('const handleOpenToolsModal = useCallback(');
expect(appSource).toContain('const handleOpenSettingsModal = useCallback(');
expect(appSource).toContain('const handleToggleLogPanel = useCallback(');
expect(appSource).toContain('new CustomEvent');
expect(appSource).toContain("'gonavi:show-sql-execution-log'");
expect(appSource).toContain("detail: { mode: 'open' }");
expect(appSource).toContain('toggleAppLogPanel();');
expect(appSource).toContain('const handleFocusSidebarSearch = useCallback(');
expect(appSource).toContain('const antdTheme = useMemo(() => ({');
expect(appSource).toContain('theme={antdTheme}');
expect(appSource).toContain('onOpenTools={handleOpenToolsModal}');
expect(appSource).toContain('onOpenSettings={handleOpenSettingsModal}');
expect(appSource).toContain('onToggleLogPanel={handleToggleLogPanel}');
expect(appSource).toContain('onFocusCommandSearch={handleFocusSidebarSearch}');
expect(appSource).not.toContain('onOpenTools={() => setIsToolsModalOpen(true)}');
expect(appSource).not.toContain('onOpenSettings={() => setIsSettingsModalOpen(true)}');
expect(appSource).not.toContain('onToggleLogPanel={() => setIsLogPanelOpen((prev) => !prev)}');
expect(appSource).not.toContain('sqlLogCount={sqlLogCount}');
expect(appSource).not.toContain('theme={{');
expect(appSource).not.toContain('const sqlLogs = useStore(state => state.sqlLogs);');
});
it('renders the shared SQL log panel only for legacy layouts', () => {
const logPanelIndex = appSource.indexOf('<LogPanel', appSource.indexOf('<Content'));
const logPanelGuardIndex = appSource.lastIndexOf('{isLogPanelOpen && (', logPanelIndex);
const legacyOnlyGuardIndex = appSource.lastIndexOf('{!isV2Ui && isLogPanelOpen && (', logPanelIndex);
expect(logPanelIndex).toBeGreaterThan(-1);
expect(logPanelGuardIndex).toBe(-1);
expect(legacyOnlyGuardIndex).toBeGreaterThan(-1);
expect(appSource).toContain('onClose={handleCloseLogPanel}');
expect(appSource).toContain('onResizeStart={handleLogResizeStart}');
});
it('lets the v2 Sidebar own the entire left layout instead of stacking legacy controls above it', () => {
const siderIndex = appSource.indexOf("className={isV2Ui ? 'gn-v2-app-sider' : undefined}");
const legacyGuardIndex = appSource.indexOf('{!isV2Ui && (', siderIndex);
const legacyCreateIndex = appSource.indexOf('<Button icon={<PlusOutlined />} onClick={handleCreateConnection}', legacyGuardIndex);
const legacyCreateTitleIndex = appSource.indexOf("title={t('connection.new')}", legacyCreateIndex);
const legacyQueryIndex = appSource.indexOf('<Button icon={<ConsoleSqlOutlined />} onClick={handleNewQuery}', legacyGuardIndex);
const legacyQueryTitleIndex = appSource.indexOf("title={t('query.new')}", legacyQueryIndex);
const sidebarIndex = appSource.indexOf('<Sidebar', legacyGuardIndex);
const floatingLogIndex = appSource.indexOf('Floating SQL Log Toggle', sidebarIndex);
const floatingLogGuardIndex = appSource.indexOf('{!isV2Ui && (', floatingLogIndex);
expect(siderIndex).toBeGreaterThan(-1);
expect(legacyGuardIndex).toBeGreaterThan(siderIndex);
expect(legacyCreateIndex).toBeGreaterThan(legacyGuardIndex);
expect(legacyCreateIndex).toBeLessThan(sidebarIndex);
expect(legacyCreateTitleIndex).toBeGreaterThan(legacyCreateIndex);
expect(legacyQueryIndex).toBeGreaterThan(legacyCreateIndex);
expect(legacyQueryIndex).toBeLessThan(sidebarIndex);
expect(legacyQueryTitleIndex).toBeGreaterThan(legacyQueryIndex);
expect(appSource).toContain('paddingBottom: isV2Ui ? 0 : 58');
expect(floatingLogIndex).toBeGreaterThan(sidebarIndex);
expect(floatingLogGuardIndex).toBeGreaterThan(floatingLogIndex);
});
it('uses the v2 green accent for sidebar and log resize guide lines', () => {
expect(appSource).toContain('const resizeGuideColor = isV2Ui');
expect(appSource).toContain("'var(--gn-accent, #16a34a)'");
expect(appSource).toContain("darkMode ? 'rgba(246, 196, 83, 0.55)' : 'rgba(24, 144, 255, 0.5)'");
});
it('keeps tool center and settings v2 accents on the green palette instead of legacy yellow or blue tokens', () => {
expect(appSource).toContain("const v2AntPrimaryColor = darkMode ? '#22c55e' : '#16a34a';");
expect(appSource).toContain("colorPrimary: isV2Ui ? v2AntPrimaryColor : (darkMode ? '#f6c453' : '#1677ff')");
expect(appSource).toContain("background: active\n ? overlayTheme.selectedBg");
expect(appSource).toContain("background: active\n ? overlayTheme.selectedText");
expect(appSource).toContain("background: active\n ? overlayTheme.iconBg");
expect(appSource).toContain("color: active\n ? overlayTheme.iconColor");
expect(appSource).toContain("background: isV2Ui ? v2AntPrimaryBgColor : (darkMode ? 'rgba(255,214,102,0.16)' : 'rgba(24,144,255,0.10)')");
expect(appSource).toContain("color: isV2Ui ? v2AntPrimaryColor : (darkMode ? '#ffd666' : '#1677ff')");
});
it('does not start sidebar resize from right-clicking the resize handle', () => {
expect(appSidebarResizeSource).toContain('if (e.button !== 0)');
expect(appSource).toContain('onContextMenu={(event) => {');
expect(appSource).toContain('event.preventDefault();');
expect(appSource).toContain('event.stopPropagation();');
const guardIndex = appSidebarResizeSource.indexOf('if (e.button !== 0)');
const ghostDisplayIndex = appSidebarResizeSource.indexOf("ghostRef.current.style.display = 'block'", guardIndex);
const dragStartIndex = appSidebarResizeSource.indexOf('sidebarDragRef.current = {', guardIndex);
expect(guardIndex).toBeGreaterThan(-1);
expect(ghostDisplayIndex).toBeGreaterThan(guardIndex);
expect(dragStartIndex).toBeGreaterThan(guardIndex);
});
it('positions sidebar resize guide from the rendered sider edge', () => {
expect(appSidebarResizeSource).toContain('const siderRef = useRef<HTMLDivElement | null>(null);');
expect(appSource).toContain('ref={siderRef}');
expect(appSidebarResizeSource).toContain('const siderRect = siderRef.current?.getBoundingClientRect();');
expect(appSidebarResizeSource).toContain('const startGuideLeft = siderRect?.right ?? sidebarWidth;');
expect(appSidebarResizeSource).toContain('const startWidth = siderRect?.width ?? sidebarWidth;');
expect(appSidebarResizeSource).toContain('resolveSidebarResizeBounds(siderRef.current)');
expect(appSidebarResizeSource).toContain('ghostRef.current.style.left = `${startGuideLeft}px`;');
expect(appSidebarResizeSource).toContain('ghostRef.current.style.left = `${startGuideLeft + (newWidth - startWidth)}px`;');
});
it('keeps sidebar resize bounds aligned across drag logic and sider CSS limits', () => {
expect(sidebarLayoutSource).toContain('export const SIDEBAR_RESIZE_MAX_WIDTH = 960;');
expect(sidebarLayoutSource).toContain('export const SIDEBAR_MIN_WORKBENCH_WIDTH = 360;');
expect(appSidebarResizeSource).toContain('resolveSidebarResizeMaxWidth(window.innerWidth, minWidth)');
expect(appCss).toMatch(/body\[data-ui-version="legacy"\]\s+\.ant-layout-sider\s*\{[^}]*min-width:\s*232px\s*!important;[^}]*max-width:\s*min\(960px,\s*calc\(100vw - 360px\)\)\s*!important;/s);
expect(v2ThemeCss).toMatch(/body\[data-ui-version="v2"\]\s+\.gn-v2-app-sider\s*\{[^}]*min-width:\s*232px\s*!important;[^}]*max-width:\s*min\(960px,\s*calc\(100vw - 360px\)\)\s*!important;/s);
});
it('keeps connection modal warm-mounted while leaving the other heavyweight modals conditional', () => {
expect(appSource).toContain('const [isConnectionModalMounted, setIsConnectionModalMounted] = useState(false);');
expect(appSource).toContain('{isConnectionModalMounted && (');
expect(appSource).toContain('{isToolsModalOpen && (');
expect(appSource).toContain('{isSettingsModalOpen && (');
expect(appSource).toContain('{isThemeModalOpen && (');
expect(appSource).toContain('{isShortcutModalOpen && (');
expect(appSource).not.toContain('{isAISettingsOpen && (');
expect(appSource).toContain('{isDriverModalOpen && (');
expect(appSource).toContain('{isSyncModalOpen && (');
});
it('loads editable connection details before opening the edit modal so stored secrets can be shown', () => {
expect(appSource).toContain("typeof backendApp?.GetEditableSavedConnection === 'function'");
expect(appSource).toContain('const editableConnection = await backendApp.GetEditableSavedConnection(conn.id);');
expect(appSource).toContain('const errorMessage = error?.message;');
expect(appSource).toContain("typeof errorMessage === 'string'");
expect(appSource).toContain("t('app.connection.message.editable_load_failed_with_detail', { detail })");
expect(appSource).toContain("t('app.connection.message.editable_load_failed')");
expect(appSource).toContain('setEditingConnection(nextConnection);');
expect(appSource).toContain('setIsModalOpen(true);');
});
it('loads editable AI provider details inside settings-center AI pane content', () => {
// 聊天/入口打开 AI 配置走设置中心 AISettingsContent不再挂独立 AISettingsModal
expect(appSource).toContain('<AISettingsContent');
expect(appSource).toContain("activeSettingsCenterPane.key === 'ai'");
expect(appSource).not.toContain('<AISettingsModal');
const modalSource = readFileSync(new URL('./components/AISettingsModal.tsx', import.meta.url), 'utf8');
expect(modalSource).toContain("typeof Service?.AIGetEditableProvider === 'function'");
expect(modalSource).toContain('await Service.AIGetEditableProvider(p.id)');
});
it('keeps edit-mode passwords masked by default instead of forcing the eye toggle open', () => {
expect(appSource).not.toContain('setPrimaryPasswordVisible(String(config.password || "").trim() !== "")');
});
it('keeps shortcut manager scrolling inside the modal body', () => {
expect(appSource).toContain('centered');
expect(appSource).toContain("height: 'min(760px, calc(100vh - 80px))'");
expect(appSource).toContain("maxHeight: 'calc(100vh - 80px)'");
expect(appSource).toContain("body: { paddingTop: 8, overflow: 'hidden', flex: 1, minHeight: 0 }");
expect(appSource).toContain('data-gonavi-shortcut-modal-scroll="true"');
expect(appSource).toContain("height: '100%'");
expect(appSource).toContain("overflowY: 'auto'");
});
it('renders recorded shortcuts with platform-specific display labels', () => {
expect(appSource).toContain('getShortcutDisplayLabel');
expect(appSource).toContain('getShortcutDisplayLabel(binding.combo, activeShortcutPlatform)');
});
it('executes every global shortcut action exposed in the shortcut manager', () => {
const expectedHandlers = new Map([
['runQuery', 'gonavi:run-active-query'],
['focusSidebarSearch', 'gonavi:focus-sidebar-search'],
['newQueryTab', 'handleNewQuery();'],
['switchToNextTab', 'switchActiveTabByOffset(1);'],
['switchToPreviousTab', 'switchActiveTabByOffset(-1);'],
['newConnection', 'handleCreateConnection();'],
['toggleAIPanel', 'toggleAIPanel();'],
['toggleLogPanel', 'handleToggleLogPanel();'],
['toggleTheme', 'setThemePreference('],
['openShortcutManager', 'setIsShortcutModalOpen(true);'],
['toggleMacFullscreen', 'handleTitleBarWindowToggle({ allowMacNativeFullscreen: true });'],
['resetWindowZoom', 'handleManualResetWindowZoom();'],
]);
for (const [action, handler] of expectedHandlers) {
expect(getGlobalShortcutCaseBlock(action)).toContain(handler);
}
expect(appSource).toContain('const switchActiveTabByOffset = useCallback((offset: 1 | -1) => {');
expect(appSource).toContain('const nextIndex = (baseIndex + offset + tabs.length) % tabs.length;');
expect(appSource).toContain('setActiveTab(tabs[nextIndex].id);');
expect(appSource).toContain('handleCreateConnection, handleManualResetWindowZoom');
expect(appSource).toContain('switchActiveTabByOffset, themeMode');
});
it('automatically resets WebView2 zoom when a Windows taskbar restore returns focus', () => {
expect(appSource).toContain('shouldResetWebViewZoomForScaleFix(reason, hasViewportScaleDrift)');
expect(appSource).toContain('const shouldResetWebViewZoom = shouldResetWebViewZoomForScaleFix(reason, hasViewportScaleDrift);');
expect(appSource).toContain('if (shouldResetWebViewZoom && !isMaximised)');
expect(appSource).toContain('const res = await (window as any).go?.app?.App?.ResetWebViewZoom?.();');
expect(appSource).toContain('if (!shouldApplyWindowsScaleFix(reason, hasViewportScaleDrift))');
expect(appSource).toContain('const nudgedWidth = getWindowsScaleFixNudgedWidth(width);');
expect(appSource).toContain('WindowSetSize(nudgedWidth, height);');
expect(appSource).toContain('该异常不一定表现为 viewport ratio drift');
});
it('settles Windows cold-start window layout without requiring a taskbar restore', () => {
expect(appSource).toContain("const applyStartupWindowChrome = (attempt: number, mode: 'maximised' | 'fullscreen') => {");
expect(appSource).toContain('markStartupWindowRestorePending(3200)');
expect(appSource).toContain("applyStartupWindowChrome(1, 'maximised');");
expect(appSource).toContain('const delayMs = attempt <= 1 ? 0 : applyRetryDelayMs');
expect(appSource).toContain('markAppliedMaximisedOrFullscreen');
expect(appSource).toContain('shouldPreferWindowsStartupMaximise(bounds, viewport)');
expect(appSource).toContain('applyWindowsWorkAreaFillFallback');
expect(appSource).toContain('resolveWorkAreaFillWindowBounds(readCurrentVisibleViewport())');
expect(appSource).toContain('restoreNormalWindowBounds');
expect(appSource).toContain("void fixWindowScaleIfNeeded('startup');");
expect(appSource).toContain('const startupLayoutFixTimers = [220, 1000, 1900].map((delayMs) => (');
expect(appSource).toContain('if (isStartupWindowRestorePending())');
expect(appSource).toContain('clearStartupWindowRestorePending();');
// 启动恢复顺序:开关最大化 → 记忆最大化 → 记忆尺寸
expect(appSource).toContain('// 1) 「启动时最大化」开关优先Windows 按 Maximize 处理)');
expect(appSource).toContain('// 2) 记忆用户上次窗口态:最大化/全屏');
expect(appSource).toContain('// 3) 普通窗口:恢复用户调整过的尺寸和位置');
});
it('captures window state on startup and lifecycle events instead of waiting only for the polling interval', () => {
expect(appSource).toContain('const scheduleWindowStateSave = (delayMs = 120) => {');
expect(appSource).toContain('const scheduleWindowBoundsRepair = (delayMs = 80) => {');
expect(appSource).toContain('if (hydrated) {');
expect(appSource).toContain('scheduleWindowBoundsRepair(360);');
expect(appSource).toContain('scheduleWindowStateSave(320);');
expect(appSource).toContain('const unsubscribeHydration = useStore.persist.onFinishHydration(() => {');
expect(appSource).toContain('scheduleWindowBoundsRepair();');
expect(appSource).toContain('scheduleWindowStateSave(260);');
expect(appSource).toContain("window.addEventListener('resize', handleWindowRuntimeChange);");
expect(appSource).toContain("window.addEventListener('focus', handleWindowRuntimeChange);");
expect(appSource).toContain("window.addEventListener('pageshow', handleWindowRuntimeChange);");
expect(appSource).toContain("window.addEventListener('pagehide', handleWindowLifecycleFlush, { capture: true });");
expect(appSource).toContain("window.addEventListener('beforeunload', handleWindowLifecycleFlush, { capture: true });");
});
it('clamps normal runtime window bounds back into the visible screen after display changes', () => {
expect(appSource).toContain('const readCurrentVisibleViewport = () => ({');
expect(appSource).toContain('const repairRuntimeWindowBounds = async () => {');
expect(appSource).toContain('const nextBounds = resolveVisibleStartupWindowBounds(currentBounds, readCurrentVisibleViewport());');
expect(appSource).toContain("void emitWindowDiagnostic('adjust:runtime-window-bounds'");
expect(appSource).toContain('WindowSetSize(nextBounds.width, nextBounds.height);');
expect(appSource).toContain('WindowSetPosition(nextBounds.x, nextBounds.y);');
});
it('keeps titlebar double-click on maximise while shortcuts may enter macOS fullscreen', () => {
expect(appSource).toContain('const handleTitleBarWindowToggle = async (options?: { allowMacNativeFullscreen?: boolean }) => {');
expect(appSource).toContain('const allowMacNativeFullscreen = options?.allowMacNativeFullscreen === true;');
expect(appSource).toContain('if (allowMacNativeFullscreen && useNativeMacWindowControls && isMacRuntime) {');
expect(appSource).toContain('void handleTitleBarWindowToggle({ allowMacNativeFullscreen: false });');
expect(getGlobalShortcutCaseBlock('toggleMacFullscreen')).toContain('handleTitleBarWindowToggle({ allowMacNativeFullscreen: true });');
});
it('captures global shortcuts before Monaco/editor defaults consume them', () => {
expect(appSource).toContain("window.addEventListener('keydown', handleGlobalShortcut, true);");
expect(appSource).toContain("window.removeEventListener('keydown', handleGlobalShortcut, true);");
});
it('skips the native mac titlebar bridge when the current runtime does not expose it', () => {
expect(appSource).toContain("const backendApp = (window as any).go?.app?.App;");
expect(appSource).toContain("if (typeof backendApp?.SetMacNativeWindowControls !== 'function') {");
expect(appSource).toContain('void safeWindowRuntimeCall(() => SetMacNativeWindowControls(useNativeMacWindowControls), undefined);');
});
it('listens for command search query-tab events and routes them through handleNewQuery', () => {
expect(appSource).toContain("window.addEventListener('gonavi:create-query-tab', handleCreateQueryTabEvent as EventListener);");
expect(appSource).toContain("window.removeEventListener('gonavi:create-query-tab', handleCreateQueryTabEvent as EventListener);");
expect(appSource).toContain('const handleCreateQueryTabEvent = () => {');
expect(appSource).toContain('handleNewQuery();');
});
});
describe('global appearance tokens', () => {
it('publishes v2 font and scale variables for non-AntD chrome', () => {
expect(appSource).toContain("setProperty('--gonavi-font-size'");
expect(appSource).toContain("setProperty('--gn-ui-scale'");
expect(appSource).toContain("setProperty('--gn-font-size'");
expect(appSource).toContain("setProperty('--gn-font-size-sm'");
expect(appSource).toContain("setProperty('--gn-font-size-xs'");
expect(appSource).toContain("setProperty('--gn-font-size-mono'");
expect(appSource).toContain("setProperty('--gn-data-table-font-size'");
expect(appSource).toContain("setProperty('--gn-sidebar-tree-font-size'");
expect(appSource).toContain("setProperty('--gn-sidebar-rail-scale'");
expect(appSource).toContain("setProperty('--gn-control-height'");
expect(appSource).toContain("setProperty('--gn-control-height-sm'");
expect(appSource).toContain('fontFamily: resolvedUiFontFamily');
expect(appSource).toContain('fontFamilyCode: resolvedMonoFontFamily');
expect(appSource).toContain('const effectiveSidebarRailScale = sanitizeV2SidebarRailScale(appearance.v2SidebarRailScale);');
expect(appSource).toContain("t('app.theme.appearance.sidebar_rail_scale_title')");
expect(appSource).toContain("t('app.theme.appearance.sidebar_rail_scale_hint')");
expect(appSource).toContain('v2SidebarRailScale: sanitizeV2SidebarRailScale(value)');
expect(appSource).toContain("t('app.theme.data_table.font_size')");
expect(appSource).toContain("t('app.theme.data_table.sidebar_tree_font_size')");
expect(v2ThemeCss).toContain('--gn-sidebar-rail-scale');
expect(v2ThemeCss).toContain('font-size: calc(var(--gn-font-size-sm, 12px) * var(--gn-sidebar-rail-scale, 1));');
expect(v2ThemeCss).toContain('width: calc(38px * var(--gn-v2-rail-scale));');
expect(appSource).toContain("const tableDoubleClickAction = appearance.tableDoubleClickAction === 'open-design' ? 'open-design' : 'open-data';");
expect(appSource).toContain("t('app.theme.data_table.table_double_click_action')");
expect(appSource).toContain("t('app.theme.data_table.table_double_click_action.open_data')");
expect(appSource).toContain("t('app.theme.data_table.table_double_click_action.open_design')");
expect(appSource).toContain("t('app.theme.data_table.table_double_click_action_hint')");
expect(appSource).toContain("setAppearance({ tableDoubleClickAction: value as 'open-data' | 'open-design' })");
expect(appSource).toContain('buildFontFamilyOptions(runtimePlatform, \'ui\', installedFontFamilies, t)');
expect(appSource).toContain('buildFontFamilyOptions(runtimePlatform, \'mono\', installedFontFamilies, t)');
expect(appSource).toContain('ListInstalledFontFamilies()');
expect(appSource).toContain('const [installedFontFamilies, setInstalledFontFamilies] = useState<InstalledFontFamily[]>(EMPTY_INSTALLED_FONT_FAMILIES);');
expect(appSource).toContain("import LinuxCJKFontBanner from './components/LinuxCJKFontBanner';");
expect(appSource).toContain('<LinuxCJKFontBanner');
expect(linuxCJKFontBannerSource).toContain('data-gonavi-linux-cjk-font-banner="true"');
expect(linuxCJKFontBannerSource).toContain('useI18n');
expect(linuxCJKFontBannerSource).toContain("t('app.linux_cjk_font_banner.title')");
expect(linuxCJKFontBannerSource).toContain("t('app.linux_cjk_font_banner.description')");
expect(linuxCJKFontBannerSource).toContain("t('app.linux_cjk_font_banner.action.open_font_settings')");
expect(linuxCJKFontBannerSource).toContain("t('common.close')");
expect(linuxCJKFontBannerSource).not.toContain('Linux CJK fonts missing / Ubuntu 中文字体缺失');
expect(linuxCJKFontBannerSource).not.toContain('Chinese text may render as');
expect(linuxCJKFontBannerSource).not.toContain('Font Settings');
expect(appSource).toContain("t('app.theme.font_family.linux_cjk_install_prefix')");
expect(appSource).toContain("t('app.theme.font_family.linux_cjk_install_suffix')");
expect(appSource).toContain("t('app.theme.query_template.title')");
expect(appSource).toContain("t('app.theme.query_template.description')");
expect(appSource).toContain("t('app.theme.query_template.hint')");
expect(appSource).toContain("t('app.theme.query_template.reset_default')");
expect(appSource).toContain("const newQuerySqlTemplate = appearance.newQuerySqlTemplate ?? DEFAULT_QUERY_TEMPLATE;");
expect(appSource).toContain("onChange={(event) => setAppearance({ newQuerySqlTemplate: event.target.value })}");
expect(appSource).toContain("onClick={() => setAppearance({ newQuerySqlTemplate: null })}");
expect(appSource).not.toContain('Ubuntu/Linux 未检测到中文 CJK 字体');
expect(appSource).not.toContain(',然后重启 GoNavi。');
expect(appSource).not.toContain('新建查询默认 SQL');
expect(appSource).not.toContain('清空后新建查询将保持空白');
expect(appSource).toContain('setIsLinuxCJKFontBannerDismissed(true)');
expect(appSource).toContain('matchFontFamilyOption');
expect(appSource).toContain('showSearch');
expect(appSource).toContain('const dataTableFontSizeFollowsGlobal = appearance.dataTableFontSizeFollowGlobal !== false;');
expect(appSource).toContain('const sidebarTreeFontSizeFollowsGlobal = appearance.sidebarTreeFontSizeFollowGlobal !== false;');
expect(appSource).toContain('disabled={dataTableFontSizeFollowsGlobal}');
expect(appSource).toContain('disabled={sidebarTreeFontSizeFollowsGlobal}');
expect(appSource).toContain("type={dataTableFontSizeFollowsGlobal ? 'primary' : 'default'}");
expect(appSource).toContain("type={sidebarTreeFontSizeFollowsGlobal ? 'primary' : 'default'}");
expect(appSource).toContain('dataTableFontSizeFollowGlobal: !dataTableFontSizeFollowsGlobal');
expect(appSource).toContain('sidebarTreeFontSizeFollowGlobal: !sidebarTreeFontSizeFollowsGlobal');
expect(appSource).toContain('dataTableFontSize: dataTableFontSizeFollowsGlobal');
expect(appSource).toContain('sidebarTreeFontSize: sidebarTreeFontSizeFollowsGlobal');
});
});