From da5affd6136a10f8ba654a296d86ee0cd3c288ed Mon Sep 17 00:00:00 2001 From: Felipe Cardoso Date: Sat, 3 Jan 2026 01:34:53 +0100 Subject: [PATCH] fix(frontend): remove locale-dependent routing and migrate to centralized locale-aware router - Replaced `next/navigation` with `@/lib/i18n/routing` across components, pages, and tests. - Removed redundant `locale` props from `ProjectWizard` and related pages. - Updated navigation to exclude explicit `locale` in paths. - Refactored tests to use mocks from `next-intl/navigation`. --- .../(authenticated)/agents/[id]/page.tsx | 3 +- .../[locale]/(authenticated)/agents/page.tsx | 2 +- .../projects/[id]/issues/page.tsx | 6 +- .../(authenticated)/projects/new/page.tsx | 10 +--- .../(authenticated)/projects/page.tsx | 2 +- .../components/projects/ProjectDashboard.tsx | 2 +- .../projects/wizard/ProjectWizard.tsx | 9 ++- .../projects/ProjectDashboard.test.tsx | 14 +---- .../projects/wizard/ProjectWizard.test.tsx | 56 +++++++++---------- 9 files changed, 42 insertions(+), 62 deletions(-) diff --git a/frontend/src/app/[locale]/(authenticated)/agents/[id]/page.tsx b/frontend/src/app/[locale]/(authenticated)/agents/[id]/page.tsx index 6bcac09..31db268 100644 --- a/frontend/src/app/[locale]/(authenticated)/agents/[id]/page.tsx +++ b/frontend/src/app/[locale]/(authenticated)/agents/[id]/page.tsx @@ -8,7 +8,8 @@ 'use client'; import { useCallback, useState } from 'react'; -import { useRouter, useParams } from 'next/navigation'; +import { useParams } from 'next/navigation'; +import { useRouter } from '@/lib/i18n/routing'; import { toast } from 'sonner'; import { AgentTypeDetail, AgentTypeForm } from '@/components/agents'; import { diff --git a/frontend/src/app/[locale]/(authenticated)/agents/page.tsx b/frontend/src/app/[locale]/(authenticated)/agents/page.tsx index f05efe2..72b2f7d 100644 --- a/frontend/src/app/[locale]/(authenticated)/agents/page.tsx +++ b/frontend/src/app/[locale]/(authenticated)/agents/page.tsx @@ -8,7 +8,7 @@ 'use client'; import { useState, useCallback, useMemo } from 'react'; -import { useRouter } from 'next/navigation'; +import { useRouter } from '@/lib/i18n/routing'; import { toast } from 'sonner'; import { AgentTypeList } from '@/components/agents'; import { useAgentTypes } from '@/lib/api/hooks/useAgentTypes'; diff --git a/frontend/src/app/[locale]/(authenticated)/projects/[id]/issues/page.tsx b/frontend/src/app/[locale]/(authenticated)/projects/[id]/issues/page.tsx index 0c83d98..afe230d 100644 --- a/frontend/src/app/[locale]/(authenticated)/projects/[id]/issues/page.tsx +++ b/frontend/src/app/[locale]/(authenticated)/projects/[id]/issues/page.tsx @@ -10,7 +10,7 @@ */ import { useState, use } from 'react'; -import { useRouter } from 'next/navigation'; +import { useRouter } from '@/lib/i18n/routing'; import { Plus, Upload } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Skeleton } from '@/components/ui/skeleton'; @@ -25,7 +25,7 @@ interface ProjectIssuesPageProps { } export default function ProjectIssuesPage({ params }: ProjectIssuesPageProps) { - const { locale, id: projectId } = use(params); + const { id: projectId } = use(params); const router = useRouter(); // Filter state @@ -49,7 +49,7 @@ export default function ProjectIssuesPage({ params }: ProjectIssuesPageProps) { const { data, isLoading, error } = useIssues(projectId, filters, sort); const handleIssueClick = (issueId: string) => { - router.push(`/${locale}/projects/${projectId}/issues/${issueId}`); + router.push(`/projects/${projectId}/issues/${issueId}`); }; const handleBulkChangeStatus = () => { diff --git a/frontend/src/app/[locale]/(authenticated)/projects/new/page.tsx b/frontend/src/app/[locale]/(authenticated)/projects/new/page.tsx index 95ae345..f7fe379 100644 --- a/frontend/src/app/[locale]/(authenticated)/projects/new/page.tsx +++ b/frontend/src/app/[locale]/(authenticated)/projects/new/page.tsx @@ -13,17 +13,11 @@ export const metadata: Metadata = { description: 'Create a new Syndarix project with AI-powered agents', }; -interface NewProjectPageProps { - params: Promise<{ locale: string }>; -} - -export default async function NewProjectPage({ params }: NewProjectPageProps) { - const { locale } = await params; - +export default function NewProjectPage() { return (
- +
); diff --git a/frontend/src/app/[locale]/(authenticated)/projects/page.tsx b/frontend/src/app/[locale]/(authenticated)/projects/page.tsx index 4f259b7..a537af2 100644 --- a/frontend/src/app/[locale]/(authenticated)/projects/page.tsx +++ b/frontend/src/app/[locale]/(authenticated)/projects/page.tsx @@ -10,7 +10,7 @@ 'use client'; import { useState, useCallback, useMemo } from 'react'; -import { useRouter } from 'next/navigation'; +import { useRouter } from '@/lib/i18n/routing'; import { toast } from 'sonner'; import { Plus } from 'lucide-react'; import { Button } from '@/components/ui/button'; diff --git a/frontend/src/components/projects/ProjectDashboard.tsx b/frontend/src/components/projects/ProjectDashboard.tsx index 9f217a8..36f3ed5 100644 --- a/frontend/src/components/projects/ProjectDashboard.tsx +++ b/frontend/src/components/projects/ProjectDashboard.tsx @@ -10,7 +10,7 @@ 'use client'; import { useCallback, useMemo, useState } from 'react'; -import { useRouter } from 'next/navigation'; +import { useRouter } from '@/lib/i18n/routing'; import { AlertCircle, RefreshCw } from 'lucide-react'; import { cn } from '@/lib/utils'; import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; diff --git a/frontend/src/components/projects/wizard/ProjectWizard.tsx b/frontend/src/components/projects/wizard/ProjectWizard.tsx index 6bf6549..39570c9 100644 --- a/frontend/src/components/projects/wizard/ProjectWizard.tsx +++ b/frontend/src/components/projects/wizard/ProjectWizard.tsx @@ -8,7 +8,7 @@ */ import { useState } from 'react'; -import { useRouter } from 'next/navigation'; +import { useRouter } from '@/lib/i18n/routing'; import { ArrowLeft, ArrowRight, Check, CheckCircle2, Loader2 } from 'lucide-react'; import { useMutation } from '@tanstack/react-query'; @@ -49,11 +49,10 @@ interface ProjectResponse { } interface ProjectWizardProps { - locale: string; className?: string; } -export function ProjectWizard({ locale, className }: ProjectWizardProps) { +export function ProjectWizard({ className }: ProjectWizardProps) { const router = useRouter(); const [isCreated, setIsCreated] = useState(false); @@ -106,9 +105,9 @@ export function ProjectWizard({ locale, className }: ProjectWizardProps) { const handleGoToProject = () => { // Navigate to project dashboard - using slug from successful creation if (createProjectMutation.data) { - router.push(`/${locale}/projects/${createProjectMutation.data.slug}`); + router.push(`/projects/${createProjectMutation.data.slug}`); } else { - router.push(`/${locale}/projects`); + router.push(`/projects`); } }; diff --git a/frontend/tests/components/projects/ProjectDashboard.test.tsx b/frontend/tests/components/projects/ProjectDashboard.test.tsx index 7ec2efc..6ed8464 100644 --- a/frontend/tests/components/projects/ProjectDashboard.test.tsx +++ b/frontend/tests/components/projects/ProjectDashboard.test.tsx @@ -92,18 +92,8 @@ jest.mock('@/lib/hooks/useProjectEvents', () => ({ useProjectEvents: jest.fn(() => mockUseProjectEventsResult), })); -// Mock next/navigation -const mockPush = jest.fn(); -jest.mock('next/navigation', () => ({ - useRouter: () => ({ - push: mockPush, - back: jest.fn(), - forward: jest.fn(), - refresh: jest.fn(), - replace: jest.fn(), - prefetch: jest.fn(), - }), -})); +// Import mock from next-intl/navigation mock (used by @/lib/i18n/routing) +import { mockPush } from 'next-intl/navigation'; describe('ProjectDashboard', () => { const projectId = 'test-project-123'; diff --git a/frontend/tests/components/projects/wizard/ProjectWizard.test.tsx b/frontend/tests/components/projects/wizard/ProjectWizard.test.tsx index 0b1ab9b..2c06ba5 100644 --- a/frontend/tests/components/projects/wizard/ProjectWizard.test.tsx +++ b/frontend/tests/components/projects/wizard/ProjectWizard.test.tsx @@ -77,13 +77,8 @@ jest.mock('@/components/projects/wizard/useWizardState', () => ({ })), })); -// Mock router -const mockPush = jest.fn(); -jest.mock('next/navigation', () => ({ - useRouter: () => ({ - push: mockPush, - }), -})); +// Import mock from next-intl/navigation mock (used by @/lib/i18n/routing) +import { mockPush } from 'next-intl/navigation'; // Mock API client const mockPost = jest.fn(); @@ -131,36 +126,36 @@ describe('ProjectWizard', () => { describe('Rendering', () => { it('renders the step indicator', () => { - render(, { wrapper: createWrapper() }); + render(, { wrapper: createWrapper() }); expect(screen.getByTestId('step-indicator')).toBeInTheDocument(); }); it('renders BasicInfoStep on step 1', () => { mockWizardState.step = 1; - render(, { wrapper: createWrapper() }); + render(, { wrapper: createWrapper() }); expect(screen.getByTestId('basic-info-step')).toBeInTheDocument(); }); it('renders ComplexityStep on step 2', () => { mockWizardState.step = 2; - render(, { wrapper: createWrapper() }); + render(, { wrapper: createWrapper() }); expect(screen.getByTestId('complexity-step')).toBeInTheDocument(); }); it('renders AgentChatStep on step 5', () => { mockWizardState.step = 5; - render(, { wrapper: createWrapper() }); + render(, { wrapper: createWrapper() }); expect(screen.getByTestId('agent-chat-step')).toBeInTheDocument(); }); it('renders ReviewStep on step 6', () => { mockWizardState.step = 6; - render(, { wrapper: createWrapper() }); + render(, { wrapper: createWrapper() }); expect(screen.getByTestId('review-step')).toBeInTheDocument(); }); it('applies custom className', () => { - const { container } = render(, { + const { container } = render(, { wrapper: createWrapper(), }); expect(container.firstChild).toHaveClass('custom-class'); @@ -171,7 +166,7 @@ describe('ProjectWizard', () => { it('calls goNext when Next button is clicked', async () => { const user = userEvent.setup(); mockWizardState.step = 1; - render(, { wrapper: createWrapper() }); + render(, { wrapper: createWrapper() }); await user.click(screen.getByRole('button', { name: /next/i })); expect(mockGoNext).toHaveBeenCalled(); @@ -180,7 +175,7 @@ describe('ProjectWizard', () => { it('calls goBack when Back button is clicked', async () => { const user = userEvent.setup(); mockWizardState.step = 2; - render(, { wrapper: createWrapper() }); + render(, { wrapper: createWrapper() }); await user.click(screen.getByRole('button', { name: /back/i })); expect(mockGoBack).toHaveBeenCalled(); @@ -188,21 +183,21 @@ describe('ProjectWizard', () => { it('hides Back button on step 1', () => { mockWizardState.step = 1; - render(, { wrapper: createWrapper() }); + render(, { wrapper: createWrapper() }); const backButton = screen.getByRole('button', { name: /back/i }); expect(backButton).toHaveClass('invisible'); }); it('shows Back button visible on step 2', () => { mockWizardState.step = 2; - render(, { wrapper: createWrapper() }); + render(, { wrapper: createWrapper() }); const backButton = screen.getByRole('button', { name: /back/i }); expect(backButton).not.toHaveClass('invisible'); }); it('shows Create Project button on review step', () => { mockWizardState.step = 6; - render(, { wrapper: createWrapper() }); + render(, { wrapper: createWrapper() }); expect(screen.getByRole('button', { name: /create project/i })).toBeInTheDocument(); }); }); @@ -211,7 +206,7 @@ describe('ProjectWizard', () => { it('skips client mode step in script mode', () => { mockWizardState.step = 3; mockWizardState.complexity = 'script'; - render(, { wrapper: createWrapper() }); + render(, { wrapper: createWrapper() }); // ClientModeStep should not render for script mode expect(screen.queryByTestId('client-mode-step')).not.toBeInTheDocument(); }); @@ -219,14 +214,14 @@ describe('ProjectWizard', () => { it('skips autonomy step in script mode', () => { mockWizardState.step = 4; mockWizardState.complexity = 'script'; - render(, { wrapper: createWrapper() }); + render(, { wrapper: createWrapper() }); // AutonomyStep should not render for script mode expect(screen.queryByTestId('autonomy-step')).not.toBeInTheDocument(); }); it('shows script mode indicator', () => { mockWizardState.complexity = 'script'; - render(, { wrapper: createWrapper() }); + render(, { wrapper: createWrapper() }); expect(screen.getByText(/script mode/i)).toBeInTheDocument(); }); }); @@ -235,7 +230,7 @@ describe('ProjectWizard', () => { it('shows success screen after creation', async () => { const user = userEvent.setup(); mockWizardState.step = 6; - render(, { wrapper: createWrapper() }); + render(, { wrapper: createWrapper() }); await user.click(screen.getByRole('button', { name: /create project/i })); @@ -247,7 +242,7 @@ describe('ProjectWizard', () => { it('displays project name in success message', async () => { const user = userEvent.setup(); mockWizardState.step = 6; - render(, { wrapper: createWrapper() }); + render(, { wrapper: createWrapper() }); await user.click(screen.getByRole('button', { name: /create project/i })); @@ -259,7 +254,7 @@ describe('ProjectWizard', () => { it('navigates to project dashboard on success', async () => { const user = userEvent.setup(); mockWizardState.step = 6; - render(, { wrapper: createWrapper() }); + render(, { wrapper: createWrapper() }); await user.click(screen.getByRole('button', { name: /create project/i })); @@ -270,13 +265,14 @@ describe('ProjectWizard', () => { }); await user.click(screen.getByRole('button', { name: /go to project dashboard/i })); - expect(mockPush).toHaveBeenCalledWith('/en/projects/test-project'); + // Locale-aware router adds locale prefix automatically + expect(mockPush).toHaveBeenCalledWith('/projects/test-project'); }); it('allows creating another project', async () => { const user = userEvent.setup(); mockWizardState.step = 6; - render(, { wrapper: createWrapper() }); + render(, { wrapper: createWrapper() }); await user.click(screen.getByRole('button', { name: /create project/i })); @@ -294,7 +290,7 @@ describe('ProjectWizard', () => { mockPost.mockRejectedValue(new Error('Network error')); const user = userEvent.setup(); mockWizardState.step = 6; - render(, { wrapper: createWrapper() }); + render(, { wrapper: createWrapper() }); await user.click(screen.getByRole('button', { name: /create project/i })); @@ -307,13 +303,13 @@ describe('ProjectWizard', () => { describe('Button States', () => { it('disables Next button when cannot proceed', () => { mockWizardState.projectName = ''; - render(, { wrapper: createWrapper() }); + render(, { wrapper: createWrapper() }); expect(screen.getByRole('button', { name: /next/i })).toBeDisabled(); }); it('enables Next button when can proceed', () => { mockWizardState.projectName = 'Valid Name'; - render(, { wrapper: createWrapper() }); + render(, { wrapper: createWrapper() }); expect(screen.getByRole('button', { name: /next/i })).not.toBeDisabled(); }); @@ -323,7 +319,7 @@ describe('ProjectWizard', () => { ); const user = userEvent.setup(); mockWizardState.step = 6; - render(, { wrapper: createWrapper() }); + render(, { wrapper: createWrapper() }); await user.click(screen.getByRole('button', { name: /create project/i })); expect(screen.getByText(/creating/i)).toBeInTheDocument();