Files
MyGoNavi/frontend/src/App.sidebar-collapse.test.ts
Syngnat d520c9b806 ️ perf(sidebar): 优化大型资源树折叠动画
- 延迟隐藏资源树内容,使侧栏宽度收起动画保持连续
- 稳定 Sidebar 属性并保留已加载节点,避免大量对象触发重复渲染
- 覆盖 V2、旧版与减少动态效果场景,补充折叠回归测试
- 关联问题:#697
2026-07-24 15:16:15 +08:00

128 lines
9.6 KiB
TypeScript

import { readFileSync } from 'node:fs';
import { describe, expect, it } from 'vitest';
const appSource = readFileSync(new URL('./App.tsx', import.meta.url), 'utf8');
const appCssSource = readFileSync(new URL('./App.css', import.meta.url), 'utf8');
const sidebarSource = readFileSync(new URL('./components/Sidebar.tsx', import.meta.url), 'utf8');
const connectionRailSource = readFileSync(new URL('./components/sidebar/SidebarConnectionRail.tsx', import.meta.url), 'utf8');
describe('app sidebar tree panel collapse', () => {
it('collapses v2 to the scaled fixed rail while preserving the saved expanded width', () => {
expect(appSource).toContain('const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(false);');
expect(appSource).toContain('const sidebarCollapsedWidth = isV2Ui ? 38 * effectiveUiScale * effectiveSidebarRailScale : 0;');
expect(appSource).toContain('const renderedSidebarWidth = isSidebarCollapsed ? sidebarCollapsedWidth : sidebarWidth;');
expect(appSource).toContain('width={sidebarWidth}');
expect(appSource).toContain('collapsed={isSidebarCollapsed}');
expect(appSource).toContain('collapsedWidth={sidebarCollapsedWidth}');
expect(appSource).toContain("['--gonavi-sidebar-collapsed-width' as any]: `${sidebarCollapsedWidth}px`");
expect(appSource).toContain('trigger={null}');
expect(appSource).not.toContain('setSidebarWidth(0)');
});
it('keeps loaded tree nodes mounted and Sidebar props stable during the width transition', () => {
expect(appSource).toContain("const sidebarPanelCollapseLabel = t('app.sidebar.collapse');");
expect(appSource).toContain("const sidebarPanelExpandLabel = t('app.sidebar.expand');");
expect(appSource).toContain('onCollapseSidebar={isV2Ui ? handleCollapseSidebarPanel : undefined}');
expect(appSource).toContain('onExpandSidebar={isV2Ui ? handleExpandSidebarPanel : undefined}');
expect(appSource).not.toContain('isTreePanelCollapsed={isV2Ui && isSidebarCollapsed}');
expect(sidebarSource).not.toContain('isTreePanelCollapsed?: boolean;');
expect(sidebarSource).not.toContain("display: isV2Ui && isTreePanelCollapsed ? 'none' : 'flex'");
expect(appSource).toContain('data-sidebar-content="true"');
expect(appCssSource).toContain('--gonavi-sidebar-collapse-duration: 200ms;');
expect(appCssSource).toMatch(
/\.ant-layout-sider\[data-sidebar-collapsed='true'\]\s+\[data-sidebar-tree-panel='true'\][^{]*\{[^}]*visibility:\s*hidden;[^}]*transition:\s*visibility 0s linear var\(--gonavi-sidebar-collapse-duration\);/s,
);
expect(appCssSource).toMatch(
/\.ant-layout-sider\[data-sidebar-collapsed='false'\]\s+\.gn-v2-rail-sidebar-toggle-slot\s*\{[^}]*display:\s*none;/s,
);
});
it('keeps the fixed rail visible and hides only the v2 explorer tree panel', () => {
expect(sidebarSource).toContain("id={isV2Ui ? 'gonavi-sidebar-tree-panel' : undefined}");
expect(sidebarSource).toContain("data-sidebar-tree-panel={isV2Ui ? 'true' : undefined}");
expect(sidebarSource).toContain("style={{ display: 'flex', flexDirection: 'column'");
expect(connectionRailSource).toContain('data-sidebar-fixed-rail="true"');
expect(appSource).not.toContain('isTreePanelCollapsed={isV2Ui && isSidebarCollapsed}');
expect(appSource).not.toContain("visibility: !isV2Ui && isSidebarCollapsed ? 'hidden' : 'visible'");
expect(appSource).toContain('data-sidebar-collapse-trigger="true"');
expect(appSource).toContain('data-titlebar-brand-region="true"');
expect(appSource).toContain('data-sidebar-toggle-placement="titlebar"');
expect(connectionRailSource).toContain('data-sidebar-toggle-placement="fixed-rail"');
expect(appSource).toContain('data-no-titlebar-toggle="true"');
expect(appSource).toContain('aria-controls="gonavi-sidebar-tree-panel"');
expect(appSource).toContain('aria-expanded={!isSidebarCollapsed}');
expect(appSource).toContain('<MenuFoldOutlined />');
expect(appSource).toContain('<MenuUnfoldOutlined />');
expect(appSource).toContain("'app.sidebar.collapse'");
expect(appSource).toContain("'app.sidebar.expand'");
expect(appSource).toContain("case 'focusSidebarSearch':");
expect(appSource).toContain('handleFocusSidebarSearch();');
expect(appSource).toContain('<TabManager onFocusSidebarSearch={handleFocusSidebarSearch} />');
expect(appSource).toContain('{!isV2Ui && (');
expect(appSource).toContain('onCollapseSidebar={isV2Ui ? handleCollapseSidebarPanel : undefined}');
expect(appSource).toContain('onExpandSidebar={isV2Ui ? handleExpandSidebarPanel : undefined}');
expect(appSource).toContain('collapseSidebarButtonRef={sidebarExplorerToggleRef}');
expect(appSource).toContain('expandSidebarButtonRef={sidebarCollapsedToggleRef}');
expect(appSource).toContain('ref={sidebarCollapsedToggleRef}');
expect(appSource).toContain("pendingSidebarToggleFocusRef.current = 'collapsed'");
expect(appSource).toContain("pendingSidebarToggleFocusRef.current = 'explorer'");
expect(appSource).toContain("(target === 'collapsed' ? sidebarCollapsedToggleRef : sidebarExplorerToggleRef).current?.focus()");
expect(sidebarSource).toContain('data-sidebar-toggle-placement="explorer-header"');
const titlebarToggleIndex = appSource.indexOf('data-sidebar-collapse-trigger="true"');
const siderIndex = appSource.indexOf('<Sider');
const siderEndIndex = appSource.indexOf('</Sider>', siderIndex);
const triggerStartIndex = appSource.lastIndexOf('<Button', titlebarToggleIndex);
const triggerEndIndex = appSource.indexOf('</Tooltip>', titlebarToggleIndex);
const triggerSource = appSource.slice(triggerStartIndex, triggerEndIndex);
const siderSource = appSource.slice(siderIndex, siderEndIndex);
const explorerActionsIndex = sidebarSource.indexOf('<div className="gn-v2-active-connection-actions">');
const fixedRailIndex = sidebarSource.indexOf('<SidebarConnectionRail');
const explorerPanelIndex = sidebarSource.indexOf("id={isV2Ui ? 'gonavi-sidebar-tree-panel' : undefined}");
const connectionMenuIndex = sidebarSource.indexOf('<Tooltip title={v2ConnectionActionsLabel}>', explorerActionsIndex);
const explorerToggleIndex = sidebarSource.indexOf('data-sidebar-toggle-placement="explorer-header"', explorerActionsIndex);
const explorerToggleEndIndex = sidebarSource.indexOf('</Tooltip>', explorerToggleIndex);
const explorerToggleStartIndex = sidebarSource.lastIndexOf('<Button', explorerToggleIndex);
const explorerToggleSource = sidebarSource.slice(explorerToggleStartIndex, explorerToggleEndIndex);
const fixedRailToggleIndex = connectionRailSource.indexOf('data-sidebar-toggle-placement="fixed-rail"');
const railItemsIndex = connectionRailSource.indexOf('<div className="gn-v2-rail-items">');
const railPrimaryActionsIndex = connectionRailSource.indexOf('<div className="gn-v2-rail-primary-actions"');
const firstRailObjectActionIndex = connectionRailSource.indexOf('data-sidebar-create-group-action="true"');
expect(titlebarToggleIndex).toBeGreaterThan(appSource.indexOf('data-titlebar-brand-region="true"'));
expect(titlebarToggleIndex).toBeLessThan(siderIndex);
expect(triggerSource).toContain('type="text"');
expect(triggerSource).toContain("WebkitAppRegion: 'no-drag'");
expect(triggerSource).toContain("'--wails-draggable': 'no-drag'");
expect(siderSource).toContain('onCollapseSidebar={isV2Ui ? handleCollapseSidebarPanel : undefined}');
expect(fixedRailIndex).toBeGreaterThan(-1);
expect(explorerPanelIndex).toBeGreaterThan(fixedRailIndex);
expect(explorerToggleIndex).toBeGreaterThan(connectionMenuIndex);
expect(explorerToggleSource).toContain('ref={collapseSidebarButtonRef}');
expect(explorerToggleSource).not.toContain('disabled=');
expect(fixedRailToggleIndex).toBeGreaterThan(-1);
expect(fixedRailToggleIndex).toBeGreaterThan(railItemsIndex);
expect(fixedRailToggleIndex).toBeGreaterThan(railPrimaryActionsIndex);
expect(fixedRailToggleIndex).toBeLessThan(firstRailObjectActionIndex);
});
it('overrides normal resize bounds with the retained rail width and removes the collapsed resize target', () => {
expect(appCssSource).toMatch(
/body\[data-ui-version\]\s+\.ant-layout-sider\[data-sidebar-collapsed='true'\]\s*\{[^}]*min-width:\s*var\(--gonavi-sidebar-collapsed-width, 0px\)\s*!important;[^}]*max-width:\s*var\(--gonavi-sidebar-collapsed-width, 0px\)\s*!important;[^}]*width:\s*var\(--gonavi-sidebar-collapsed-width, 0px\)\s*!important;[^}]*flex:\s*0 0 var\(--gonavi-sidebar-collapsed-width, 0px\)\s*!important;/s,
);
expect(appSource).toContain('paddingRight: isSidebarCollapsed ? 0 : sidebarResizeHandleWidth');
expect(appSource).toContain('{!isSidebarCollapsed && <div');
expect(appCssSource).toMatch(
/body\[data-ui-version\]\s+\.gonavi-sidebar-collapse-trigger\.ant-btn\s*\{[^}]*width:\s*26px;[^}]*height:\s*26px(?:\s*!important)?;[^}]*border:\s*0\s*!important;/s,
);
expect(appCssSource).toMatch(
/body\[data-ui-version='v2'\]\s+\.gonavi-sidebar-collapse-trigger\.ant-btn\[data-sidebar-toggle-placement='explorer-header'\]\s*\{[^}]*width:\s*24px;[^}]*height:\s*24px(?:\s*!important)?;[^}]*border:\s*1px solid var\(--gn-br-2\)\s*!important;[^}]*border-radius:\s*7px\s*!important;/s,
);
expect(appCssSource).not.toMatch(
/\.ant-layout-sider\[data-sidebar-panel='true'\]\s*\{[^}]*(?:min-width|max-width|\bwidth\s*:|\bflex\s*:)/s,
);
expect(appCssSource).not.toMatch(/\.gonavi-sidebar-collapse-trigger[^}]*position:\s*absolute/s);
expect(appCssSource).not.toMatch(/\.gonavi-sidebar-collapse-trigger[^}]*right:\s*-\d+px/s);
expect(appCssSource).not.toMatch(/\.gonavi-sidebar-collapse-trigger[^}]*translateY/s);
});
});