From 1134871b8bf3d461e94f6757eb3d0141293a5237 Mon Sep 17 00:00:00 2001 From: Jordi Enric Date: Thu, 10 Sep 2026 13:57:55 +0200 Subject: [PATCH] fix(studio): preserve health advisor hook order --- .../pages/project/[ref]/advisors/health.tsx | 35 ++++---- .../tests/pages/health-advisor.test.tsx | 90 +++++++++++++++++++ 2 files changed, 110 insertions(+), 15 deletions(-) create mode 100644 apps/studio/tests/pages/health-advisor.test.tsx diff --git a/apps/studio/pages/project/[ref]/advisors/health.tsx b/apps/studio/pages/project/[ref]/advisors/health.tsx index 81d98f24158..3aea75bff73 100644 --- a/apps/studio/pages/project/[ref]/advisors/health.tsx +++ b/apps/studio/pages/project/[ref]/advisors/health.tsx @@ -20,24 +20,10 @@ import type { NextPageWithLayout } from '@/types' const ProjectHealthLints: NextPageWithLayout = () => { const router = useRouter() - const { preset, id, ref } = useParams() - const { data: project } = useSelectedProjectQuery() + const { ref } = useParams() const { hasLoaded: flagsLoaded } = useFeatureFlags() const isHealthAdvisorEnabled = useFlag('healthAdvisor') === true - const [filters, setFilters] = useState<{ level: LINTER_LEVELS; filters: string[] }[]>([ - { level: LINTER_LEVELS.ERROR, filters: [] }, - { level: LINTER_LEVELS.WARN, filters: [] }, - { level: LINTER_LEVELS.INFO, filters: [] }, - ]) - const [currentTab, setCurrentTab] = useState( - parseLinterLevel(preset) ?? LINTER_LEVELS.ERROR - ) - const { data, isPending, isRefetching, refetch } = useProjectHealthLintsQuery( - { projectRef: project?.ref }, - { enabled: isHealthAdvisorEnabled } - ) - const isFlagLoading = IS_PLATFORM && !flagsLoaded useEffect(() => { @@ -48,6 +34,25 @@ const ProjectHealthLints: NextPageWithLayout = () => { if (isFlagLoading || !isHealthAdvisorEnabled) return null + return +} + +const ProjectHealthLintsContent = () => { + const { preset, id } = useParams() + const { data: project } = useSelectedProjectQuery() + + const [filters, setFilters] = useState<{ level: LINTER_LEVELS; filters: string[] }[]>([ + { level: LINTER_LEVELS.ERROR, filters: [] }, + { level: LINTER_LEVELS.WARN, filters: [] }, + { level: LINTER_LEVELS.INFO, filters: [] }, + ]) + const [currentTab, setCurrentTab] = useState( + parseLinterLevel(preset) ?? LINTER_LEVELS.ERROR + ) + const { data, isPending, isRefetching, refetch } = useProjectHealthLintsQuery({ + projectRef: project?.ref, + }) + // Health checks are platform-only. If this page is opened self-hosted the query stays // disabled, and `isPending` would otherwise spin forever. const isLoading = IS_PLATFORM && isPending diff --git a/apps/studio/tests/pages/health-advisor.test.tsx b/apps/studio/tests/pages/health-advisor.test.tsx new file mode 100644 index 00000000000..5ee312206e8 --- /dev/null +++ b/apps/studio/tests/pages/health-advisor.test.tsx @@ -0,0 +1,90 @@ +import { render, screen } from '@testing-library/react' +import { beforeEach, describe, expect, it, vi } from 'vitest' + +import ProjectHealthLints from '@/pages/project/[ref]/advisors/health' + +const mocks = vi.hoisted(() => ({ + flags: { hasLoaded: false, enabled: false }, + router: { replace: vi.fn() }, + query: vi.fn(), + shortcuts: vi.fn(), +})) + +vi.mock('common', () => ({ + useFeatureFlags: () => ({ hasLoaded: mocks.flags.hasLoaded }), + useFlag: () => mocks.flags.enabled, + useParams: () => ({ ref: 'test-project' }), +})) +vi.mock('next/router', () => ({ useRouter: () => mocks.router })) +vi.mock('@/lib/constants', () => ({ IS_PLATFORM: true })) +vi.mock('@/hooks/misc/useSelectedProject', () => ({ + useSelectedProjectQuery: () => ({ data: { ref: 'test-project' } }), +})) +vi.mock('@/data/lint/health-lints-query', () => ({ + useProjectHealthLintsQuery: mocks.query, +})) +vi.mock('@/components/interfaces/Linter/useAdvisorPageShortcuts', () => ({ + useAdvisorPageShortcuts: mocks.shortcuts, +})) +vi.mock('@/components/interfaces/Linter/Linter.utils', () => ({ + lintInfoMap: [], + parseLinterLevel: () => undefined, +})) +vi.mock('@/components/interfaces/Linter/LinterDataGrid', () => ({ + LinterDataGrid: () => null, +})) +vi.mock('@/components/interfaces/Linter/LinterFilters', () => ({ default: () => null })) +vi.mock('@/components/interfaces/Linter/LintPageTabs', () => ({ default: () => null })) +vi.mock('@/components/layouts/AdvisorsLayout/AdvisorsLayout', () => ({ default: () => null })) +vi.mock('@/components/layouts/DefaultLayout', () => ({ DefaultLayout: () => null })) +vi.mock('@/components/ui/Forms/FormHeader', () => ({ + FormHeader: ({ title }: { title: string }) =>

{title}

, +})) +vi.mock('ui', () => ({ LoadingLine: () => null })) + +describe('Health Advisor feature flag', () => { + beforeEach(() => { + vi.clearAllMocks() + mocks.flags.hasLoaded = false + mocks.flags.enabled = false + mocks.query.mockReturnValue({ + data: [], + isPending: false, + isRefetching: false, + refetch: vi.fn(), + }) + }) + + it('renders after the flag loads without changing hook order', () => { + const { rerender } = render() + + expect(screen.queryByRole('heading', { name: 'Health Advisor' })).not.toBeInTheDocument() + expect(mocks.router.replace).not.toHaveBeenCalled() + expect(mocks.query).not.toHaveBeenCalled() + expect(mocks.shortcuts).not.toHaveBeenCalled() + + mocks.flags.hasLoaded = true + mocks.flags.enabled = true + rerender() + + expect(screen.getByRole('heading', { name: 'Health Advisor' })).toBeInTheDocument() + expect(mocks.query).toHaveBeenCalled() + expect(mocks.shortcuts).toHaveBeenCalled() + expect(mocks.router.replace).not.toHaveBeenCalled() + + mocks.flags.enabled = false + rerender() + + expect(screen.queryByRole('heading', { name: 'Health Advisor' })).not.toBeInTheDocument() + expect(mocks.router.replace).toHaveBeenCalledWith('/project/test-project/advisors/security') + }) + + it('redirects when the loaded flag is disabled without mounting health functionality', () => { + mocks.flags.hasLoaded = true + render() + + expect(mocks.router.replace).toHaveBeenCalledWith('/project/test-project/advisors/security') + expect(mocks.query).not.toHaveBeenCalled() + expect(mocks.shortcuts).not.toHaveBeenCalled() + }) +})