mirror of
https://github.com/supabase/supabase.git
synced 2026-10-10 20:05:06 +03:00
## Problem The `_Shadcn_` suffix isn't needed anymore on `Command` components ## Solution - Remove the `_Shadcn_` suffix - Simplify UI package exports - Apply prettier <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Refactor** * Simplified command component imports and exports across the UI library by removing internal naming aliases and adopting direct component references. Updated the public UI package barrel export to use wildcard re-exports for cleaner API surface. <!-- review_stack_entry_start --> [](https://app.coderabbit.ai/change-stack/supabase/supabase/pull/46153?utm_source=github_walkthrough&utm_medium=github&utm_campaign=change_stack) <!-- review_stack_entry_end --> <!-- end of auto-generated comment: release notes by coderabbit.ai -->
408 lines
13 KiB
TypeScript
408 lines
13 KiB
TypeScript
import { render, screen, waitFor } from '@testing-library/react'
|
|
import { LOCAL_STORAGE_KEYS } from 'common'
|
|
import type { ReactNode } from 'react'
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
|
|
import { MobileSheetProvider } from '../Navigation/NavigationBar/MobileSheetContext'
|
|
import { ProjectLayout } from './index'
|
|
import { STUDIO_PAGE_TITLE_SEPARATOR } from '@/lib/page-title'
|
|
|
|
const { mockRouter, mockSetSelectedDatabaseId, mockSetMobileMenuOpen } = vi.hoisted(() => ({
|
|
mockRouter: {
|
|
pathname: '/project/[ref]/observability/query-performance',
|
|
asPath: '/project/default/observability/query-performance',
|
|
push: vi.fn(),
|
|
replace: vi.fn(),
|
|
},
|
|
mockSetSelectedDatabaseId: vi.fn(),
|
|
mockSetMobileMenuOpen: vi.fn(),
|
|
}))
|
|
|
|
const {
|
|
mockAddBanner,
|
|
mockDismissBanner,
|
|
mockProjectState,
|
|
mockResourceWarningsState,
|
|
mockBannerDismissedState,
|
|
mockUseLocalStorageQuery,
|
|
} = vi.hoisted(() => ({
|
|
mockAddBanner: vi.fn(),
|
|
mockDismissBanner: vi.fn(),
|
|
mockProjectState: {
|
|
current: {
|
|
ref: 'default',
|
|
name: 'Project 1',
|
|
status: 'ACTIVE_HEALTHY',
|
|
postgrestStatus: 'ONLINE',
|
|
infra_compute_size: undefined as string | undefined,
|
|
integration_source: null as string | null,
|
|
},
|
|
},
|
|
mockResourceWarningsState: { current: undefined as any[] | undefined },
|
|
mockBannerDismissedState: { current: false },
|
|
mockUseLocalStorageQuery: vi.fn(),
|
|
}))
|
|
|
|
vi.mock('next/router', () => ({
|
|
useRouter: () => mockRouter,
|
|
}))
|
|
|
|
vi.mock('next/head', async () => {
|
|
const React = await import('react')
|
|
|
|
const Head = ({ children }: { children?: ReactNode }) => {
|
|
React.useEffect(() => {
|
|
const titleElement = React.Children.toArray(children).find(
|
|
(child) => React.isValidElement(child) && child.type === 'title'
|
|
)
|
|
|
|
if (!React.isValidElement<{ children: ReactNode }>(titleElement)) return
|
|
|
|
const titleText = React.Children.toArray(titleElement.props.children).join('')
|
|
document.title = titleText
|
|
}, [children])
|
|
|
|
return null
|
|
}
|
|
|
|
return { default: Head }
|
|
})
|
|
|
|
vi.mock('common', () => ({
|
|
useParams: () => ({ ref: 'default' }),
|
|
mergeRefs:
|
|
(..._refs: any[]) =>
|
|
(_value: unknown) => {},
|
|
IS_PLATFORM: false,
|
|
LOCAL_STORAGE_KEYS: {
|
|
FREE_MICRO_UPGRADE_BANNER_DISMISSED: (ref: string) =>
|
|
`free-micro-upgrade-banner-dismissed-${ref}`,
|
|
PROJECT_INTEGRATION_BANNER_DISMISSED: (ref: string, integrationSource: string) =>
|
|
`project-integration-banner-dismissed-${ref}-${integrationSource}`,
|
|
},
|
|
isFeatureEnabled: () => false,
|
|
}))
|
|
|
|
vi.mock('framer-motion', () => ({
|
|
AnimatePresence: ({ children }: { children: ReactNode }) => <>{children}</>,
|
|
motion: {
|
|
div: ({ children, ...props }: any) => <div {...props}>{children}</div>,
|
|
create: (Component: any) => Component,
|
|
},
|
|
}))
|
|
|
|
vi.mock('ui', () => ({
|
|
cn: (...classes: Array<string | false | null | undefined>) => classes.filter(Boolean).join(' '),
|
|
Alert: ({ children, ...props }: any) => <div {...props}>{children}</div>,
|
|
AlertDescription: ({ children, ...props }: any) => <div {...props}>{children}</div>,
|
|
AlertTitle: ({ children, ...props }: any) => <div {...props}>{children}</div>,
|
|
CommandInput: { displayName: 'CommandInput' },
|
|
Command: { displayName: 'Command' },
|
|
CommandGroup: { displayName: 'CommandGroup' },
|
|
CommandItem: { displayName: 'CommandItem' },
|
|
CommandList: { displayName: 'CommandList' },
|
|
LogoLoader: () => <div data-testid="logo-loader" />,
|
|
ResizableHandle: (props: any) => <div {...props} />,
|
|
ResizablePanel: ({ children, ...props }: any) => <div {...props}>{children}</div>,
|
|
ResizablePanelGroup: ({ children, ...props }: any) => <div {...props}>{children}</div>,
|
|
Sidebar: ({ children, ...props }: any) => <div {...props}>{children}</div>,
|
|
SidebarContent: ({ children, ...props }: any) => <div {...props}>{children}</div>,
|
|
SidebarFooter: ({ children, ...props }: any) => <div {...props}>{children}</div>,
|
|
SidebarGroup: ({ children, ...props }: any) => <div {...props}>{children}</div>,
|
|
SidebarMenu: ({ children, ...props }: any) => <div {...props}>{children}</div>,
|
|
SidebarMenuButton: (props: any) => <div {...props} />,
|
|
SidebarMenuItem: (props: any) => <div {...props} />,
|
|
useIsMobile: () => false,
|
|
usePanelRef: () => undefined,
|
|
useSidebar: () => ({ setOpen: vi.fn() }),
|
|
}))
|
|
|
|
vi.mock('ui-patterns/MobileSheetNav/MobileSheetNav', () => ({
|
|
default: ({ children }: { children: ReactNode }) => <>{children}</>,
|
|
}))
|
|
|
|
vi.mock('../editors/EditorsLayout.hooks', () => ({
|
|
useEditorType: () => undefined,
|
|
}))
|
|
|
|
vi.mock('../MainScrollContainerContext', () => ({
|
|
useSetMainScrollContainer: () => () => {},
|
|
}))
|
|
|
|
vi.mock('./BuildingState', () => ({ default: () => null }))
|
|
vi.mock('./ConnectingState', () => ({ default: () => null }))
|
|
vi.mock('./LoadingState', () => ({ LoadingState: () => null }))
|
|
vi.mock('./PausedState/ProjectPausedState', () => ({ ProjectPausedState: () => null }))
|
|
vi.mock('./PauseFailedState', () => ({ PauseFailedState: () => null }))
|
|
vi.mock('./PausingState', () => ({ PausingState: () => null }))
|
|
vi.mock('./ProductMenuBar', () => ({
|
|
default: ({ children }: { children: ReactNode }) => <>{children}</>,
|
|
}))
|
|
vi.mock('./ResizingState', () => ({ ResizingState: () => null }))
|
|
vi.mock('./RestartingState', () => ({ default: () => null }))
|
|
vi.mock('./RestoreFailedState', () => ({ RestoreFailedState: () => null }))
|
|
vi.mock('./RestoringState', () => ({ RestoringState: () => null }))
|
|
vi.mock('./UpgradingState', () => ({ UpgradingState: () => null }))
|
|
|
|
vi.mock('@/components/interfaces/BranchManagement/CreateBranchModal', () => ({
|
|
CreateBranchModal: () => null,
|
|
}))
|
|
vi.mock('@/components/interfaces/ProjectAPIDocs/ProjectAPIDocs', () => ({
|
|
ProjectAPIDocs: () => null,
|
|
}))
|
|
vi.mock('@/components/ui/ResourceExhaustionWarningBanner/ResourceExhaustionWarningBanner', () => ({
|
|
ResourceExhaustionWarningBanner: () => null,
|
|
}))
|
|
vi.mock('@/components/ui/ButtonTooltip', () => ({
|
|
ButtonTooltip: ({ children, ...props }: any) => <button {...props}>{children}</button>,
|
|
}))
|
|
vi.mock('@/components/ui/PartnerIcon', () => ({
|
|
default: () => <div data-testid="partner-icon" />,
|
|
}))
|
|
|
|
vi.mock('@/hooks/custom-content/useCustomContent', () => ({
|
|
useCustomContent: () => ({ appTitle: 'Supabase' }),
|
|
}))
|
|
|
|
vi.mock('@/hooks/misc/useLocalStorage', () => ({
|
|
useLocalStorageQuery: (...args: unknown[]) => mockUseLocalStorageQuery(...args),
|
|
}))
|
|
|
|
vi.mock('@/components/ui/BannerStack/BannerStackProvider', () => ({
|
|
BANNER_ID: { FREE_MICRO_UPGRADE: 'free-micro-upgrade-banner' },
|
|
useBannerStack: () => ({
|
|
addBanner: mockAddBanner,
|
|
dismissBanner: mockDismissBanner,
|
|
banners: [],
|
|
}),
|
|
}))
|
|
|
|
vi.mock('@/components/ui/BannerStack/Banners/BannerFreeMicroUpgrade', () => ({
|
|
BannerFreeMicroUpgrade: () => null,
|
|
}))
|
|
|
|
vi.mock('@/data/usage/resource-warnings-query', () => ({
|
|
useResourceWarningsQuery: () => ({ data: mockResourceWarningsState.current }),
|
|
}))
|
|
|
|
vi.mock('@/hooks/misc/useSelectedOrganization', () => ({
|
|
useSelectedOrganizationQuery: () => ({
|
|
data: { name: 'Organization 1', slug: 'org-1' },
|
|
}),
|
|
}))
|
|
|
|
vi.mock('@/hooks/misc/useSelectedProject', () => ({
|
|
useSelectedProjectQuery: () => ({ data: mockProjectState.current }),
|
|
}))
|
|
|
|
vi.mock('@/hooks/misc/withAuth', () => ({
|
|
withAuth: (Component: any) => Component,
|
|
}))
|
|
|
|
vi.mock('@/hooks/ui/useFlag', () => ({
|
|
usePHFlag: () => undefined,
|
|
}))
|
|
|
|
vi.mock('@/state/app-state', () => ({
|
|
useAppStateSnapshot: () => ({
|
|
mobileMenuOpen: false,
|
|
showSidebar: false,
|
|
setMobileMenuOpen: mockSetMobileMenuOpen,
|
|
}),
|
|
}))
|
|
|
|
vi.mock('@/state/database-selector', () => ({
|
|
useDatabaseSelectorStateSnapshot: () => ({
|
|
setSelectedDatabaseId: mockSetSelectedDatabaseId,
|
|
}),
|
|
}))
|
|
|
|
const renderLayout = () =>
|
|
render(
|
|
<MobileSheetProvider>
|
|
<ProjectLayout product="Database" isBlocking={false}>
|
|
<div />
|
|
</ProjectLayout>
|
|
</MobileSheetProvider>
|
|
)
|
|
|
|
describe('ProjectLayout title', () => {
|
|
beforeEach(() => {
|
|
mockRouter.pathname = '/project/[ref]/observability/query-performance'
|
|
mockRouter.asPath = '/project/default/observability/query-performance'
|
|
document.title = ''
|
|
mockProjectState.current = {
|
|
ref: 'default',
|
|
name: 'Project 1',
|
|
status: 'ACTIVE_HEALTHY',
|
|
postgrestStatus: 'ONLINE',
|
|
infra_compute_size: undefined,
|
|
integration_source: null,
|
|
}
|
|
mockBannerDismissedState.current = false
|
|
mockUseLocalStorageQuery.mockImplementation(() => [mockBannerDismissedState.current, vi.fn()])
|
|
})
|
|
|
|
afterEach(() => {
|
|
vi.clearAllMocks()
|
|
document.title = ''
|
|
})
|
|
|
|
it('sets a composed document title and deduplicates identical section/surface labels', async () => {
|
|
render(
|
|
<MobileSheetProvider>
|
|
<ProjectLayout browserTitle={{ section: 'Settings' }} product="Settings" isBlocking={false}>
|
|
<div>Page Content</div>
|
|
</ProjectLayout>
|
|
</MobileSheetProvider>
|
|
)
|
|
|
|
await waitFor(() => {
|
|
expect(document.title).toBe(
|
|
['Settings', 'Project 1', 'Organization 1', 'Supabase'].join(STUDIO_PAGE_TITLE_SEPARATOR)
|
|
)
|
|
})
|
|
})
|
|
|
|
it('prefers entity-first browserTitle metadata when provided', async () => {
|
|
render(
|
|
<MobileSheetProvider>
|
|
<ProjectLayout
|
|
product="Database"
|
|
browserTitle={{ entity: 'users', section: 'Tables' }}
|
|
isBlocking={false}
|
|
>
|
|
<div>Page Content</div>
|
|
</ProjectLayout>
|
|
</MobileSheetProvider>
|
|
)
|
|
|
|
await waitFor(() => {
|
|
expect(document.title).toBe(
|
|
['users', 'Tables', 'Database', 'Project 1', 'Organization 1', 'Supabase'].join(
|
|
STUDIO_PAGE_TITLE_SEPARATOR
|
|
)
|
|
)
|
|
})
|
|
})
|
|
|
|
it('renders the Stripe project banner across project surfaces when the selected project is Stripe-connected', () => {
|
|
mockProjectState.current = {
|
|
...mockProjectState.current,
|
|
integration_source: 'stripe_projects',
|
|
}
|
|
|
|
renderLayout()
|
|
|
|
expect(screen.getByText('This project is connected to Stripe')).toBeTruthy()
|
|
expect(
|
|
screen.getByText('Changes made here may affect your connected Stripe project.')
|
|
).toBeTruthy()
|
|
expect(screen.getByTestId('partner-icon')).toBeTruthy()
|
|
})
|
|
|
|
it('uses a project-specific dismiss key for the Stripe project banner', () => {
|
|
mockProjectState.current = {
|
|
...mockProjectState.current,
|
|
integration_source: 'stripe_projects',
|
|
}
|
|
|
|
renderLayout()
|
|
|
|
expect(mockUseLocalStorageQuery).toHaveBeenCalledWith(
|
|
LOCAL_STORAGE_KEYS.PROJECT_INTEGRATION_BANNER_DISMISSED('default', 'stripe_projects'),
|
|
false
|
|
)
|
|
})
|
|
})
|
|
|
|
describe('FREE_MICRO_UPGRADE banner', () => {
|
|
beforeEach(() => {
|
|
mockRouter.pathname = '/project/[ref]'
|
|
mockRouter.asPath = '/project/default'
|
|
mockProjectState.current = {
|
|
ref: 'default',
|
|
name: 'Project 1',
|
|
status: 'ACTIVE_HEALTHY',
|
|
postgrestStatus: 'ONLINE',
|
|
infra_compute_size: 'nano',
|
|
integration_source: null,
|
|
}
|
|
mockResourceWarningsState.current = [
|
|
{
|
|
project: 'default',
|
|
cpu_exhaustion: true,
|
|
memory_and_swap_exhaustion: false,
|
|
disk_space_exhaustion: false,
|
|
},
|
|
]
|
|
mockBannerDismissedState.current = false
|
|
})
|
|
|
|
afterEach(() => {
|
|
vi.clearAllMocks()
|
|
mockRouter.pathname = '/project/[ref]/observability/query-performance'
|
|
mockRouter.asPath = '/project/default/observability/query-performance'
|
|
mockProjectState.current = {
|
|
ref: 'default',
|
|
name: 'Project 1',
|
|
status: 'ACTIVE_HEALTHY',
|
|
postgrestStatus: 'ONLINE',
|
|
infra_compute_size: undefined,
|
|
integration_source: null,
|
|
}
|
|
mockResourceWarningsState.current = undefined
|
|
mockBannerDismissedState.current = false
|
|
})
|
|
|
|
it('calls addBanner when project is nano and compute is near exhaustion', async () => {
|
|
renderLayout()
|
|
|
|
await waitFor(() => {
|
|
expect(mockAddBanner).toHaveBeenCalledWith(
|
|
expect.objectContaining({ id: 'free-micro-upgrade-banner' })
|
|
)
|
|
})
|
|
})
|
|
|
|
it('calls dismissBanner when banner was previously dismissed', async () => {
|
|
mockBannerDismissedState.current = true
|
|
|
|
renderLayout()
|
|
|
|
await waitFor(() => {
|
|
expect(mockDismissBanner).toHaveBeenCalledWith('free-micro-upgrade-banner')
|
|
})
|
|
expect(mockAddBanner).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('calls dismissBanner when compute warnings are cleared', async () => {
|
|
mockResourceWarningsState.current = [
|
|
{
|
|
project: 'default',
|
|
cpu_exhaustion: false,
|
|
memory_and_swap_exhaustion: false,
|
|
disk_space_exhaustion: false,
|
|
},
|
|
]
|
|
|
|
renderLayout()
|
|
|
|
await waitFor(() => {
|
|
expect(mockDismissBanner).toHaveBeenCalledWith('free-micro-upgrade-banner')
|
|
})
|
|
expect(mockAddBanner).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('calls dismissBanner when project is not nano compute', async () => {
|
|
mockProjectState.current = { ...mockProjectState.current, infra_compute_size: 'micro' }
|
|
|
|
renderLayout()
|
|
|
|
await waitFor(() => {
|
|
expect(mockDismissBanner).toHaveBeenCalledWith('free-micro-upgrade-banner')
|
|
})
|
|
expect(mockAddBanner).not.toHaveBeenCalled()
|
|
})
|
|
})
|