diff --git a/bun.lock b/bun.lock index 08d047a6..ec864745 100644 --- a/bun.lock +++ b/bun.lock @@ -5,7 +5,7 @@ "": { "name": "tanstack-start-app", "dependencies": { - "@clickhouse/click-ui": "0.2.0-rc.4", + "@clickhouse/click-ui": "0.9.1", "@librechat/data-schemas": "^0.0.56", "@radix-ui/react-dialog": "1.1.15", "@tailwindcss/vite": "^4.3.1", @@ -160,7 +160,7 @@ "@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "7.27.1", "@babel/helper-validator-identifier": "7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="], - "@clickhouse/click-ui": ["@clickhouse/click-ui@0.2.0-rc.4", "", { "dependencies": { "@h6s/calendar": "2.0.1", "@radix-ui/react-accordion": "1.2.12", "@radix-ui/react-avatar": "1.1.1", "@radix-ui/react-checkbox": "1.1.2", "@radix-ui/react-context-menu": "2.2.2", "@radix-ui/react-dialog": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.1", "@radix-ui/react-dropdown-menu": "2.1.2", "@radix-ui/react-hover-card": "1.1.2", "@radix-ui/react-popover": "1.1.2", "@radix-ui/react-popper": "1.2.1", "@radix-ui/react-radio-group": "1.2.1", "@radix-ui/react-separator": "1.1.1", "@radix-ui/react-switch": "1.1.1", "@radix-ui/react-tabs": "1.1.1", "@radix-ui/react-toast": "1.2.2", "@radix-ui/react-tooltip": "1.1.2", "dayjs": "^1.11.19", "lodash-es": "^4.17.23", "react-sortablejs": "^6.1.4", "react-syntax-highlighter": "^16.1.0", "react-virtualized-auto-sizer": "^1.0.20", "react-window": "^1.8.9", "sortablejs": "^1.15.0", "styled-components": "^6.1.11" }, "peerDependencies": { "react": "^18.3.1 || ^19.0.0", "react-dom": "^18.3.1 || ^19.0.0" } }, "sha512-WcPjvS7W/InvHP/JVeMIHA/30jl3Ks6CN+zch7kfqogjcbTc4byOzSUSkOJlYblzLxfdins0z0oeBF+jVsBMDA=="], + "@clickhouse/click-ui": ["@clickhouse/click-ui@0.9.1", "", { "dependencies": { "@h6s/calendar": "2.2.0", "@radix-ui/react-accordion": "1.2.12", "@radix-ui/react-avatar": "1.1.1", "@radix-ui/react-checkbox": "1.1.2", "@radix-ui/react-context-menu": "2.2.2", "@radix-ui/react-dialog": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.1", "@radix-ui/react-dropdown-menu": "2.1.2", "@radix-ui/react-hover-card": "1.1.2", "@radix-ui/react-popover": "1.1.2", "@radix-ui/react-popper": "1.2.1", "@radix-ui/react-radio-group": "1.2.1", "@radix-ui/react-separator": "1.1.1", "@radix-ui/react-switch": "1.1.1", "@radix-ui/react-tabs": "1.1.1", "@radix-ui/react-toast": "1.2.2", "@radix-ui/react-tooltip": "1.1.2", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "dayjs": "^1.11.19", "lodash-es": "^4.17.23", "react-sortablejs": "^6.1.4", "react-syntax-highlighter": "^16.1.0", "react-virtualized-auto-sizer": "^1.0.20", "react-window": "^1.8.9", "sortablejs": "^1.15.0", "styled-components": "^6.1.11" }, "peerDependencies": { "react": "^18.3.1 || ^19.0.0", "react-dom": "^18.3.1 || ^19.0.0" } }, "sha512-gAyIlzIDYqnXcmHM2rOWNpGAoC0UAeacT5yJpDgu09I+TjmuOdzh9tYaJlS938SmUyLsg2U6t8oMA7do108Hgg=="], "@colors/colors": ["@colors/colors@1.6.0", "", {}, "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA=="], @@ -188,8 +188,6 @@ "@emotion/memoize": ["@emotion/memoize@0.9.0", "", {}, "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ=="], - "@emotion/unitless": ["@emotion/unitless@0.10.0", "", {}, "sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg=="], - "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.3", "", { "os": "aix", "cpu": "ppc64" }, "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg=="], "@esbuild/android-arm": ["@esbuild/android-arm@0.27.3", "", { "os": "android", "cpu": "arm" }, "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA=="], @@ -270,7 +268,7 @@ "@floating-ui/utils": ["@floating-ui/utils@0.2.10", "", {}, "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ=="], - "@h6s/calendar": ["@h6s/calendar@2.0.1", "", { "peerDependencies": { "date-fns": ">= 2", "react": ">= 18" } }, "sha512-9q5ksdnUsLDeuSm5arXzkxHyS22u7yi5TtVr4DZgW514AjdL+76kx7d+ScX9sKusasRQVxrhM2LTVmd8A/u42Q=="], + "@h6s/calendar": ["@h6s/calendar@2.2.0", "", { "peerDependencies": { "react": ">= 18" } }, "sha512-o1fb4RlMWDRIOiMEX65vN5qDTiD/OyC++EkJzmoz9l1vUOkN89gTE+wNE83XZBi3hbYqMV3CNhgKf51n/0hcBw=="], "@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="], @@ -644,8 +642,6 @@ "@types/sortablejs": ["@types/sortablejs@1.15.9", "", {}, "sha512-7HP+rZGE2p886PKV9c9OJzLBI6BBJu1O7lJGYnPyG3fS4/duUCcngkNCjsLwIMV+WMqANe3tt4irrXHSIe68OQ=="], - "@types/stylis": ["@types/stylis@4.2.7", "", {}, "sha512-VgDNokpBoKF+wrdvhAAfS55OMQpL6QRglwTwNC3kIgBrzZxA4WsFj+2eLfEA/uMUDzBcEhYmjSbwQakn/i3ajA=="], - "@types/triple-beam": ["@types/triple-beam@1.3.5", "", {}, "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw=="], "@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="], @@ -824,6 +820,8 @@ "ci-info": ["ci-info@3.9.0", "", {}, "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ=="], + "class-variance-authority": ["class-variance-authority@0.7.1", "", { "dependencies": { "clsx": "^2.1.1" } }, "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg=="], + "classnames": ["classnames@2.3.1", "", {}, "sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA=="], "cli-cursor": ["cli-cursor@5.0.0", "", { "dependencies": { "restore-cursor": "^5.0.0" } }, "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw=="], @@ -1454,8 +1452,6 @@ "set-function-length": ["set-function-length@1.2.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", "has-property-descriptors": "^1.0.2" } }, "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg=="], - "shallowequal": ["shallowequal@1.1.0", "", {}, "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ=="], - "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], @@ -1654,10 +1650,6 @@ "@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], - "@clickhouse/click-ui/dayjs": ["dayjs@1.11.19", "", {}, "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw=="], - - "@clickhouse/click-ui/styled-components": ["styled-components@6.3.11", "", { "dependencies": { "@emotion/is-prop-valid": "1.4.0", "@emotion/unitless": "0.10.0", "@types/stylis": "4.2.7", "css-to-react-native": "3.2.0", "csstype": "3.2.3", "postcss": "8.4.49", "shallowequal": "1.1.0", "stylis": "4.3.6", "tslib": "2.8.1" }, "peerDependencies": { "react": ">= 16.8.0", "react-dom": ">= 16.8.0" }, "optionalPeers": ["react-dom"] }, "sha512-opzgceGlQ5rdZdGwf9ddLW7EM2F4L7tgsgLn6fFzQ2JgE5EVQ4HZwNkcgB1p8WfOBx1GEZP3fa66ajJmtXhSrA=="], - "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], "@eslint/eslintrc/espree": ["espree@9.6.1", "", { "dependencies": { "acorn": "^8.9.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^3.4.1" } }, "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ=="], @@ -2020,8 +2012,6 @@ "xmlbuilder2/js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], - "@clickhouse/click-ui/styled-components/postcss": ["postcss@8.4.49", "", { "dependencies": { "nanoid": "^3.3.7", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA=="], - "@eslint/eslintrc/espree/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], "@eslint/eslintrc/minimatch/brace-expansion": ["brace-expansion@1.1.13", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w=="], @@ -2136,8 +2126,6 @@ "wrap-ansi/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], - "@clickhouse/click-ui/styled-components/postcss/nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], - "@eslint/eslintrc/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], "@humanwhocodes/config-array/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], diff --git a/package.json b/package.json index 78d3e020..f46add31 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "format:check": "prettier --check 'src/**/*.{ts,tsx,css,json}'" }, "dependencies": { - "@clickhouse/click-ui": "0.2.0-rc.4", + "@clickhouse/click-ui": "0.9.1", "@librechat/data-schemas": "^0.0.56", "@radix-ui/react-dialog": "1.1.15", "@tailwindcss/vite": "^4.3.1", diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx index ce99c1e2..c34a38aa 100644 --- a/src/components/Sidebar.tsx +++ b/src/components/Sidebar.tsx @@ -87,9 +87,23 @@ export function Sidebar({ user, collapsed, onToggle }: t.SidebarProps) { )} >
-
- {localize('com_a11y_logo_alt')} - +
+ {localize('com_a11y_logo_alt')} + {localize('com_auth_title')}
@@ -105,7 +119,8 @@ export function Sidebar({ user, collapsed, onToggle }: t.SidebarProps) { aria-label={collapsed ? localize(item.labelKey) : undefined} title={collapsed ? localize(item.labelKey) : undefined} className={cn( - 'flex h-8 items-center gap-2.5 overflow-hidden rounded-md px-2.5 text-sm whitespace-nowrap no-underline transition-colors duration-100', + 'flex h-8 items-center overflow-hidden rounded-md px-2.5 text-sm whitespace-nowrap no-underline transition-colors duration-100', + collapsed ? 'w-10 justify-center gap-0' : 'gap-2.5', isActive(item.path) ? 'bg-(--cui-color-background-active) font-medium text-(--cui-color-text-default)' : 'font-normal text-(--cui-color-text-muted) hover:bg-(--cui-color-background-hover) hover:text-(--cui-color-text-default)', @@ -114,7 +129,9 @@ export function Sidebar({ user, collapsed, onToggle }: t.SidebarProps) { - {localize(item.labelKey)} + + {localize(item.labelKey)} + ))}
@@ -122,7 +139,12 @@ export function Sidebar({ user, collapsed, onToggle }: t.SidebarProps) { {initials && (
-
+
diff --git a/src/components/__tests__/Sidebar.test.tsx b/src/components/__tests__/Sidebar.test.tsx new file mode 100644 index 00000000..907449f7 --- /dev/null +++ b/src/components/__tests__/Sidebar.test.tsx @@ -0,0 +1,84 @@ +import { describe, it, expect, vi } from 'vitest'; +import { screen, fireEvent, waitFor } from '@testing-library/react'; +import { renderWithLayoutProviders } from '@/test/layout-test-utils'; +import { Sidebar } from '../Sidebar'; + +vi.mock('@/server', async () => { + const { SystemCapabilities } = await import('@/constants'); + return { + adminLogoutFn: vi.fn().mockResolvedValue({ error: false, redirect: '' }), + getEffectiveCapabilitiesFn: vi.fn().mockResolvedValue({ + capabilities: [ + SystemCapabilities.ACCESS_ADMIN, + SystemCapabilities.READ_CONFIGS, + SystemCapabilities.READ_ROLES, + ], + }), + }; +}); + +const user = { name: 'Ada Lovelace', email: 'ada@example.com' }; + +const renderSidebar = (collapsed: boolean, onToggle: () => void = () => {}) => + renderWithLayoutProviders(, { + user, + }); + +describe('Sidebar', () => { + it('centers the logo, nav icons, and avatar in a fixed-width rail when collapsed', async () => { + renderSidebar(true); + const dashboardLink = await screen.findByRole('link', { name: 'Dashboard' }); + expect(dashboardLink).toHaveClass('w-10', 'justify-center', 'gap-0'); + expect(dashboardLink).not.toHaveClass('gap-2.5'); + const logoRow = screen.getByAltText('LibreChat logo').parentElement; + expect(logoRow).toHaveClass('w-10', 'justify-center', 'gap-0'); + expect(logoRow).not.toHaveClass('w-full'); + const menuButton = screen.getByRole('button', { name: /User menu/ }); + const avatarRow = menuButton.closest('.border-t')?.firstElementChild; + expect(avatarRow).toHaveClass('w-10', 'justify-center', 'gap-0'); + }); + + it('strips aria-expanded from the real dropdown trigger wrapper', async () => { + renderSidebar(true); + const menuButton = await screen.findByRole('button', { name: /User menu/ }); + await waitFor(() => expect(menuButton.parentElement).not.toHaveAttribute('aria-expanded')); + }); + + it('renders capability-gated nav items once effective capabilities resolve', async () => { + renderSidebar(true); + expect(await screen.findByRole('link', { name: 'Configuration' })).toBeInTheDocument(); + expect(await screen.findByRole('link', { name: 'Access' })).toBeInTheDocument(); + expect(screen.getByRole('link', { name: 'Grants' })).toBeInTheDocument(); + expect(screen.getByRole('link', { name: 'Help' })).toBeInTheDocument(); + }); + + it('restores gaps and left alignment when expanded', async () => { + renderSidebar(false); + const dashboardLink = await screen.findByRole('link', { name: 'Dashboard' }); + expect(dashboardLink).toHaveClass('gap-2.5'); + expect(dashboardLink).not.toHaveClass('justify-center'); + expect(dashboardLink).not.toHaveClass('w-10'); + const logoRow = screen.getByAltText('LibreChat logo').parentElement; + expect(logoRow).toHaveClass('gap-2.5', 'px-1.5'); + expect(logoRow).not.toHaveClass('justify-center'); + expect(screen.getByText('ada@example.com')).toBeInTheDocument(); + }); + + it('opens the real user menu with settings and sign out actions', async () => { + renderSidebar(true); + const menuButton = await screen.findByRole('button', { name: /User menu/ }); + fireEvent.pointerDown(menuButton, { button: 0, ctrlKey: false }); + fireEvent.click(menuButton); + expect(await screen.findByText('Sign out')).toBeInTheDocument(); + expect(screen.getByText('Settings')).toBeInTheDocument(); + }); + + it('applies the inset focus-ring class to the toggle and calls onToggle', async () => { + const onToggle = vi.fn(); + renderSidebar(true, onToggle); + const toggle = await screen.findByRole('button', { name: 'Expand sidebar' }); + expect(toggle).toHaveClass('sidebar-toggle'); + fireEvent.click(toggle); + expect(onToggle).toHaveBeenCalledTimes(1); + }); +}); diff --git a/src/styles.css b/src/styles.css index 91e8d01f..1ecc0556 100644 --- a/src/styles.css +++ b/src/styles.css @@ -419,6 +419,13 @@ textarea:focus-visible { border-radius: var(--cui-radii-sm) !important; } +/* Sidebar collapse toggle is flush with the panel edges and viewport bottom, + so ancestors clip the default outside ring; draw it inset instead. */ +.sidebar-toggle:focus-visible { + outline-offset: -2px !important; + border-radius: var(--cui-radii-sm); +} + .scope-selector-dialog { width: min(640px, calc(100vw - 2rem)); } diff --git a/src/test/layout-test-utils.tsx b/src/test/layout-test-utils.tsx new file mode 100644 index 00000000..55872b04 --- /dev/null +++ b/src/test/layout-test-utils.tsx @@ -0,0 +1,47 @@ +import '@/locales/i18n'; +import { render } from '@testing-library/react'; +import { ClickUIProvider } from '@clickhouse/click-ui'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { + RouterProvider, + createMemoryHistory, + createRootRoute, + createRouter, + createRoute, +} from '@tanstack/react-router'; +import type { RenderResult } from '@testing-library/react'; +import type { ReactNode } from 'react'; +import type * as t from '@/types'; +import { ThemeProvider } from '@/contexts/ThemeContext'; + +const APP_PATHS = ['/', '/configuration', '/access', '/grants', '/help']; + +export function renderWithLayoutProviders( + ui: ReactNode, + { user = null, path = '/' }: t.LayoutRenderOptions = {}, +): RenderResult { + const rootRoute = createRootRoute(); + const appRoute = createRoute({ + getParentRoute: () => rootRoute, + id: '_app', + beforeLoad: () => ({ user }), + }); + const pageRoutes = APP_PATHS.map((pagePath) => + createRoute({ getParentRoute: () => appRoute, path: pagePath, component: () => ui }), + ); + const router = createRouter({ + routeTree: rootRoute.addChildren([appRoute.addChildren(pageRoutes)]), + history: createMemoryHistory({ initialEntries: [path] }), + }); + const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } }); + + return render( + + + + + + + , + ); +} diff --git a/src/test/setup.ts b/src/test/setup.ts index bb02c60c..aa9762a4 100644 --- a/src/test/setup.ts +++ b/src/test/setup.ts @@ -1 +1,25 @@ import '@testing-library/jest-dom/vitest'; + +if (typeof window !== 'undefined') { + window.scrollTo = () => {}; + + window.matchMedia ??= (query: string): MediaQueryList => + ({ + matches: false, + media: query, + onchange: null, + addListener: () => {}, + removeListener: () => {}, + addEventListener: () => {}, + removeEventListener: () => {}, + dispatchEvent: () => false, + }) as MediaQueryList; + + window.ResizeObserver ??= class { + observe() {} + unobserve() {} + disconnect() {} + }; + + Element.prototype.scrollIntoView ??= () => {}; +} diff --git a/src/types/layout.ts b/src/types/layout.ts index 7563e578..80833cd9 100644 --- a/src/types/layout.ts +++ b/src/types/layout.ts @@ -39,3 +39,8 @@ export interface SettingsDialogProps { open: boolean; onClose: () => void; } + +export interface LayoutRenderOptions { + user?: SidebarProps['user']; + path?: string; +} diff --git a/vitest.config.ts b/vitest.config.ts index c7e8e233..25a36ac3 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -7,6 +7,11 @@ export default defineConfig({ environment: 'jsdom', globals: true, setupFiles: ['./src/test/setup.ts'], - exclude: ['e2e/**', 'node_modules/**', 'tools/**'], + exclude: ['e2e/**', 'node_modules/**', 'tools/**', '.claude/**'], + server: { + deps: { + inline: ['@clickhouse/click-ui'], + }, + }, }, })