diff --git a/apps/pwa/src/routes/SetupPage.test.tsx b/apps/pwa/src/routes/SetupPage.test.tsx index 32ca942..80caf21 100644 --- a/apps/pwa/src/routes/SetupPage.test.tsx +++ b/apps/pwa/src/routes/SetupPage.test.tsx @@ -210,9 +210,14 @@ describe('SetupPage — Already Locked screen', () => { describe('SetupPage — Step 2 VAPID validation (CR-01 gap)', () => { let queryClient: QueryClient; - beforeEach(() => { + beforeEach(async () => { queryClient = makeQueryClient(); vi.resetAllMocks(); + const { fetchSetupStatus } = await import('../api/client.js'); + (fetchSetupStatus as ReturnType).mockResolvedValue({ + setupComplete: false, + dbName: 'familysync', + }); }); /** @@ -305,3 +310,66 @@ describe('SetupPage — Step 2 VAPID validation (CR-01 gap)', () => { }); }); }); + +// ── Tests: Step 2 Instance copy + read-only DB-name field (gaps 1, 3-frontend) ── + +describe('SetupPage — Step 2 Instance copy + DB-name field (gaps 1, 3)', () => { + let queryClient: QueryClient; + + beforeEach(async () => { + queryClient = makeQueryClient(); + vi.resetAllMocks(); + const { fetchSetupStatus } = await import('../api/client.js'); + (fetchSetupStatus as ReturnType).mockResolvedValue({ + setupComplete: false, + dbName: 'familysync', + }); + }); + + async function advanceToStep2() { + renderSetupPage(queryClient); + const continueBtn = await screen.findByRole('button', { name: 'Continue' }); + fireEvent.click(continueBtn); + await screen.findByText('Instance Configuration'); + } + + it('Instance step intro no longer contains the DB-vs-env-file aside (gap 1)', async () => { + await advanceToStep2(); + expect(screen.queryByText(/not your environment file/i)).toBeNull(); + // First sentence is preserved + expect(screen.getByText(/Enter your instance/i)).toBeInTheDocument(); + }); + + it('renders a read-only, disabled DB-name field populated from status dbName (gap 3)', async () => { + await advanceToStep2(); + const dbField = await screen.findByLabelText('Database'); + await waitFor(() => { + expect(dbField).toHaveValue('familysync'); + }); + expect(dbField).toHaveAttribute('readonly'); + expect(dbField).toBeDisabled(); + expect(dbField).toHaveAttribute('aria-readonly', 'true'); + }); + + it('keeps the existing "Database connection verified" validation row available', async () => { + const { postSetupConfig, validateSetupDb, validateSetupOidc, validateSetupVapid } = + await import('../api/client.js'); + (postSetupConfig as ReturnType).mockResolvedValue(undefined); + (validateSetupDb as ReturnType).mockResolvedValue(undefined); + (validateSetupOidc as ReturnType).mockResolvedValue(undefined); + (validateSetupVapid as ReturnType).mockResolvedValue(undefined); + + await advanceToStep2(); + fireEvent.change(screen.getByLabelText('App URL'), { target: { value: 'https://app.example.com' } }); + fireEvent.change(screen.getByLabelText('OIDC issuer URL'), { target: { value: 'https://auth.example.com' } }); + fireEvent.change(screen.getByLabelText('OIDC client ID'), { target: { value: 'familysync' } }); + fireEvent.change(screen.getByLabelText('VAPID public key'), { target: { value: 'BHtest123' } }); + fireEvent.click(screen.getByRole('button', { name: 'Save & Validate' })); + + await waitFor(() => { + expect(screen.getByText('Database connection verified.')).toBeInTheDocument(); + }); + }); +}); + +// (gap-4 Back-navigation tests added in Task 2) diff --git a/apps/pwa/src/routes/SetupPage.tsx b/apps/pwa/src/routes/SetupPage.tsx index 92f7757..23da0fa 100644 --- a/apps/pwa/src/routes/SetupPage.tsx +++ b/apps/pwa/src/routes/SetupPage.tsx @@ -21,9 +21,10 @@ */ import { useState, useRef, useEffect } from 'react'; -import { useMutation } from '@tanstack/react-query'; +import { useMutation, useQuery } from '@tanstack/react-query'; import { ShieldCheck, CheckCircle, AlertCircle, Loader2 } from 'lucide-react'; import { + fetchSetupStatus, postSetupConfig, validateSetupDb, validateSetupOidc, @@ -442,6 +443,17 @@ function Step2Config({ onBack, onSuccess, stepHeadingRef }: Step2Props) { const [vapidPublicKey, setVapidPublicKey] = useState(''); const [fieldError, setFieldError] = useState(null); + // Gap 3 (frontend): fetch the env-derived, non-secret DB name so the + // "database connection verified" row below has an on-screen referent. + // Only dbName is surfaced — DB_HOST/DB_USER/DB_PASSWORD are never fetched (T-12-3DB). + const { data: setupStatus } = useQuery({ + queryKey: ['setupStatus'], + queryFn: fetchSetupStatus, + staleTime: 0, + retry: false, + }); + const dbName = setupStatus?.dbName ?? ''; + const [validationRows, setValidationRows] = useState>({ db: 'idle', oidc: 'idle', @@ -553,8 +565,7 @@ function Step2Config({ onBack, onSuccess, stepHeadingRef }: Step2Props) { lineHeight: 1.5, }} > - Enter your instance's connection details. These are written to the database — not - your environment file. + Enter your instance's connection details.

{/* App URL */} @@ -574,6 +585,32 @@ function Step2Config({ onBack, onSuccess, stepHeadingRef }: Step2Props) {
The public URL where FamilySync is reachable.
+ {/* Database (read-only, env-derived) — gives the DB validation row below a referent */} +
+ + +
+ Configured via the server's Docker environment (DB_HOST,{' '} + DB_PORT, DB_USER, DB_PASSWORD) — not entered here. +
+
+ {/* OIDC Issuer */}