mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
The Compute entry in the project sidebar was the only product route without a `shortcutId`, so it had neither a `G`-chord nor the hover keybind tooltip every sibling gets. Bound it to `G` then `C` (previously unused) and added a test asserting every product route carries a shortcut. Fixes FE-4389 <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Added a keyboard shortcut for quick navigation to Compute: press **G**, then **C**. - Compute navigation now includes a discoverable shortcut for consistent access from the navigation interface. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
212 lines
8.0 KiB
TypeScript
212 lines
8.0 KiB
TypeScript
import { renderHook } from '@testing-library/react'
|
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
|
|
import {
|
|
generateOtherRoutes,
|
|
generateProductRoutes,
|
|
generateSettingsRoutes,
|
|
useGenerateToolRoutes,
|
|
} from './NavigationBar.utils'
|
|
import type { Project } from '@/data/projects/project-detail-query'
|
|
|
|
const REF = 'test-project-ref'
|
|
|
|
const activeProject = { status: 'ACTIVE_HEALTHY' } as Project
|
|
const buildingProject = { status: 'COMING_UP' } as Project
|
|
const inactiveProject = { status: 'INACTIVE' } as Project
|
|
|
|
const keys = (routes: { key: string }[]) => routes.map((r) => r.key)
|
|
|
|
const mockUseParams = vi.fn()
|
|
const mockUseSelectedProjectQuery = vi.fn()
|
|
const mockUseIsExplorerEnabled = vi.fn()
|
|
|
|
vi.mock('common', async (importOriginal) => ({
|
|
...(await importOriginal<typeof import('common')>()),
|
|
useParams: () => mockUseParams(),
|
|
}))
|
|
|
|
vi.mock('@/hooks/misc/useSelectedProject', () => ({
|
|
useSelectedProjectQuery: () => mockUseSelectedProjectQuery(),
|
|
}))
|
|
|
|
vi.mock('@/components/interfaces/App/FeaturePreview/FeaturePreviewContext', () => ({
|
|
useIsExplorerEnabled: () => mockUseIsExplorerEnabled(),
|
|
useUnifiedLogsPreview: () => ({ isEnabled: false }),
|
|
}))
|
|
|
|
describe('useGenerateToolRoutes', () => {
|
|
beforeEach(() => {
|
|
mockUseParams.mockReturnValue({ ref: REF })
|
|
mockUseSelectedProjectQuery.mockReturnValue({ data: activeProject })
|
|
mockUseIsExplorerEnabled.mockReturnValue(false)
|
|
})
|
|
|
|
it('always returns Table Editor and SQL Editor', () => {
|
|
const { result } = renderHook(() => useGenerateToolRoutes())
|
|
expect(keys(result.current)).toEqual(['editor', 'sql'])
|
|
})
|
|
|
|
it('marks routes as disabled when project is not active', () => {
|
|
mockUseSelectedProjectQuery.mockReturnValue({ data: inactiveProject })
|
|
|
|
const { result } = renderHook(() => useGenerateToolRoutes())
|
|
expect(result.current.every((r) => r.disabled)).toBe(true)
|
|
})
|
|
|
|
it('points links to the building URL when project is building', () => {
|
|
mockUseSelectedProjectQuery.mockReturnValue({ data: buildingProject })
|
|
|
|
const { result } = renderHook(() => useGenerateToolRoutes())
|
|
expect(result.current.every((r) => r.link === `/project/${REF}`)).toBe(true)
|
|
})
|
|
|
|
it('returns links as false when ref is undefined', () => {
|
|
mockUseParams.mockReturnValue({ ref: undefined })
|
|
|
|
const { result } = renderHook(() => useGenerateToolRoutes())
|
|
expect(result.current.every((r) => r.link === undefined)).toBe(true)
|
|
})
|
|
})
|
|
|
|
describe('generateProductRoutes', () => {
|
|
it('includes all product routes when all features are enabled', () => {
|
|
const routes = generateProductRoutes(REF, activeProject, {
|
|
auth: true,
|
|
storage: true,
|
|
edgeFunctions: true,
|
|
realtime: true,
|
|
})
|
|
expect(keys(routes)).toEqual(['database', 'auth', 'storage', 'functions', 'realtime'])
|
|
})
|
|
|
|
it('includes all product routes by default (features default to true)', () => {
|
|
const routes = generateProductRoutes(REF, activeProject)
|
|
expect(keys(routes)).toEqual(['database', 'auth', 'storage', 'functions', 'realtime'])
|
|
})
|
|
|
|
it('includes compute only when the compute flag is enabled', () => {
|
|
expect(keys(generateProductRoutes(REF, activeProject))).not.toContain('compute')
|
|
expect(keys(generateProductRoutes(REF, activeProject, { compute: true }))).toContain('compute')
|
|
})
|
|
|
|
it('gives every product route a navigation shortcut', () => {
|
|
const routes = generateProductRoutes(REF, activeProject, { compute: true })
|
|
expect(routes.filter((r) => !r.shortcutId)).toEqual([])
|
|
})
|
|
|
|
it('excludes auth when auth feature is disabled', () => {
|
|
const routes = generateProductRoutes(REF, activeProject, { auth: false })
|
|
expect(keys(routes)).not.toContain('auth')
|
|
|
|
expect(keys(routes)).toContain('database')
|
|
expect(keys(routes)).toContain('storage')
|
|
})
|
|
|
|
it('excludes storage when storage feature is disabled', () => {
|
|
const routes = generateProductRoutes(REF, activeProject, { storage: false })
|
|
expect(keys(routes)).not.toContain('storage')
|
|
})
|
|
|
|
it('excludes edge functions when edgeFunctions feature is disabled', () => {
|
|
const routes = generateProductRoutes(REF, activeProject, { edgeFunctions: false })
|
|
expect(keys(routes)).not.toContain('functions')
|
|
})
|
|
|
|
it('excludes realtime when realtime feature is disabled', () => {
|
|
const routes = generateProductRoutes(REF, activeProject, { realtime: false })
|
|
expect(keys(routes)).not.toContain('realtime')
|
|
})
|
|
|
|
it('links auth to overview page when authOverviewPage is enabled', () => {
|
|
const routes = generateProductRoutes(REF, activeProject, { authOverviewPage: true })
|
|
const authRoute = routes.find((r) => r.key === 'auth')
|
|
expect(authRoute?.link).toBe(`/project/${REF}/auth/overview`)
|
|
})
|
|
|
|
it('links auth to users page by default', () => {
|
|
const routes = generateProductRoutes(REF, activeProject)
|
|
const authRoute = routes.find((r) => r.key === 'auth')
|
|
expect(authRoute?.link).toBe(`/project/${REF}/auth/users`)
|
|
})
|
|
|
|
it('always includes database even when all optional features are disabled', () => {
|
|
const routes = generateProductRoutes(REF, activeProject, {
|
|
auth: false,
|
|
storage: false,
|
|
edgeFunctions: false,
|
|
realtime: false,
|
|
})
|
|
expect(keys(routes)).toEqual(['database'])
|
|
})
|
|
})
|
|
|
|
describe('generateOtherRoutes', () => {
|
|
it('always includes advisors, logs, and integrations', () => {
|
|
const routes = generateOtherRoutes(REF, activeProject)
|
|
expect(keys(routes)).toContain('advisors')
|
|
expect(keys(routes)).toContain('logs')
|
|
expect(keys(routes)).toContain('integrations')
|
|
})
|
|
|
|
it('includes observability when reports are enabled', () => {
|
|
const routes = generateOtherRoutes(REF, activeProject, { showReports: true })
|
|
expect(keys(routes)).toContain('observability')
|
|
})
|
|
|
|
it('excludes observability when reports are disabled', () => {
|
|
const routes = generateOtherRoutes(REF, activeProject, { showReports: false })
|
|
expect(keys(routes)).not.toContain('observability')
|
|
})
|
|
|
|
it('links observability to the query performance page in self-hosted mode', () => {
|
|
// NEXT_PUBLIC_IS_PLATFORM is false in .env.test, so IS_PLATFORM is false here
|
|
const routes = generateOtherRoutes(REF, activeProject, { showReports: true })
|
|
const observabilityRoute = routes.find((r) => r.key === 'observability')
|
|
expect(observabilityRoute?.link).toBe(`/project/${REF}/query-performance`)
|
|
})
|
|
|
|
it('does not include API Docs nav item', () => {
|
|
const routes = generateOtherRoutes(REF, activeProject)
|
|
expect(keys(routes)).not.toContain('api')
|
|
})
|
|
|
|
it('links logs to unified logs page when unifiedLogs is enabled', () => {
|
|
const routes = generateOtherRoutes(REF, activeProject, { unifiedLogs: true })
|
|
const logsRoute = routes.find((r) => r.key === 'logs')
|
|
expect(logsRoute?.link).toBe(`/project/${REF}/logs`)
|
|
})
|
|
|
|
it('links logs to explorer page by default', () => {
|
|
const routes = generateOtherRoutes(REF, activeProject)
|
|
const logsRoute = routes.find((r) => r.key === 'logs')
|
|
expect(logsRoute?.link).toBe(`/project/${REF}/logs/explorer`)
|
|
})
|
|
|
|
it('points links to building URL when project is building', () => {
|
|
const routes = generateOtherRoutes(REF, buildingProject, { showReports: true })
|
|
const observabilityRoute = routes.find((r) => r.key === 'observability')
|
|
expect(observabilityRoute?.link).toBe(`/project/${REF}`)
|
|
})
|
|
|
|
it('marks routes as disabled when project is not active', () => {
|
|
const routes = generateOtherRoutes(REF, inactiveProject)
|
|
const advisorsRoute = routes.find((r) => r.key === 'advisors')
|
|
expect(advisorsRoute?.disabled).toBe(true)
|
|
})
|
|
})
|
|
|
|
describe('generateSettingsRoutes', () => {
|
|
it('links to general settings', () => {
|
|
const routes = generateSettingsRoutes(REF)
|
|
const settingsRoute = routes.find((r) => r.key === 'settings')
|
|
expect(settingsRoute?.link).toBe(`/project/${REF}/settings/general`)
|
|
})
|
|
|
|
it('returns a link as false when ref is undefined', () => {
|
|
const routes = generateSettingsRoutes(undefined)
|
|
const settingsRoute = routes.find((r) => r.key === 'settings')
|
|
expect(settingsRoute?.link).toBe(undefined)
|
|
})
|
|
})
|