| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407 |
- 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: 'Briven' }),
- }))
- 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', 'Briven'].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', 'Briven'].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()
- })
- })
|