diff --git a/src/SEBT.Portal.Web/src/features/auth/components/id-proofing/IdProofingForm.test.tsx b/src/SEBT.Portal.Web/src/features/auth/components/id-proofing/IdProofingForm.test.tsx index 4d9684f4..5319da64 100644 --- a/src/SEBT.Portal.Web/src/features/auth/components/id-proofing/IdProofingForm.test.tsx +++ b/src/SEBT.Portal.Web/src/features/auth/components/id-proofing/IdProofingForm.test.tsx @@ -18,6 +18,9 @@ import userEvent from '@testing-library/user-event' import { http, HttpResponse } from 'msw' import { beforeEach, describe, expect, it, vi } from 'vitest' +import { i18n } from '@sebt/design-system/client' + +import enCoStepUpProcessing from '@/content/locales/en/co/step-upProcessing.json' import enDcValidation from '@/content/locales/en/dc/validation.json' import { server } from '@/mocks/server' @@ -1097,6 +1100,102 @@ describe('IdProofingForm', () => { }) }) + it('renders the titled LoadingInterstitial when step-upProcessing copy is present in the locale bundle', async () => { + // Forward-compatibility guard: when a state's content sheet ships + // step-upProcessing.title/body upstream, the form must pick it up and + // render the titled interstitial without any code change. We simulate + // that by adding the CO bundle for the duration of this test. + i18n.addResourceBundle('en', 'step-upProcessing', enCoStepUpProcessing, true, true) + try { + let resolveResponse: () => void = () => {} + const responsePromise = new Promise((resolve) => { + resolveResponse = resolve + }) + + server.use( + http.post('/api/id-proofing', async () => { + await responsePromise + return HttpResponse.json({ result: 'matched' }) + }) + ) + + const user = userEvent.setup() + renderWithProviders( + + ) + + await user.selectOptions(screen.getByRole('combobox', { name: /month/i }), '03') + await user.type(screen.getByRole('textbox', { name: INPUT_LABEL_DAY }), '10') + await user.type(screen.getByRole('textbox', { name: INPUT_LABEL_YEAR }), '1990') + await user.click(screen.getByRole('radio', { name: LABEL_SSN })) + await user.type(await screen.findByRole('textbox', { name: INPUT_LABEL_SSN }), '999999999') + await user.click(screen.getByRole('button', { name: /continue/i })) + + await waitFor(() => { + expect(screen.getByRole('status')).toBeInTheDocument() + }) + expect(screen.getByText(enCoStepUpProcessing.title)).toBeInTheDocument() + expect(screen.getByText(enCoStepUpProcessing.body)).toBeInTheDocument() + + resolveResponse() + await waitFor(() => { + expect(mockPush).toHaveBeenCalledWith('/dashboard') + }) + } finally { + i18n.removeResourceBundle('en', 'step-upProcessing') + } + }) + + it('renders a spinner-only status region when step-upProcessing copy is missing from the active locale bundle', async () => { + // DC's content sheet currently marks S10 - Step-up Processing rows as + // !N/A!, so the step-upProcessing namespace is not registered for DC. If + // the form rendered LoadingInterstitial with tProcessing('title') / + // tProcessing('body'), i18next would fall back to the literal key names + // — DC users would see "body" in a box. The loading state falls back to a + // spinner-only status region whenever the copy is missing, regardless of + // which state is active. + let resolveResponse: () => void = () => {} + const responsePromise = new Promise((resolve) => { + resolveResponse = resolve + }) + + server.use( + http.post('/api/id-proofing', async () => { + await responsePromise + return HttpResponse.json({ result: 'matched' }) + }) + ) + + const user = userEvent.setup() + renderWithProviders( + + ) + + await user.selectOptions(screen.getByRole('combobox', { name: /month/i }), '03') + await user.type(screen.getByRole('textbox', { name: INPUT_LABEL_DAY }), '10') + await user.type(screen.getByRole('textbox', { name: INPUT_LABEL_YEAR }), '1990') + await user.click(screen.getByRole('radio', { name: LABEL_SSN })) + await user.type(await screen.findByRole('textbox', { name: INPUT_LABEL_SSN }), '999999999') + await user.click(screen.getByRole('button', { name: /continue/i })) + + await waitFor(() => { + expect(screen.getByRole('status')).toBeInTheDocument() + }) + expect(screen.queryByText(/^title$/)).not.toBeInTheDocument() + expect(screen.queryByText(/^body$/)).not.toBeInTheDocument() + + resolveResponse() + await waitFor(() => { + expect(mockPush).toHaveBeenCalledWith('/dashboard') + }) + }) + it('restores the form and shows the submit error alert when the mutation throws', async () => { // The error path must NOT leave the loading interstitial stuck on screen — // the user needs to see the alert above the form so they can retry. diff --git a/src/SEBT.Portal.Web/src/features/auth/components/id-proofing/IdProofingForm.tsx b/src/SEBT.Portal.Web/src/features/auth/components/id-proofing/IdProofingForm.tsx index 3395993f..6de1ab1a 100644 --- a/src/SEBT.Portal.Web/src/features/auth/components/id-proofing/IdProofingForm.tsx +++ b/src/SEBT.Portal.Web/src/features/auth/components/id-proofing/IdProofingForm.tsx @@ -296,12 +296,36 @@ export function IdProofingForm({ idOptions, contactLink, getDiToken }: IdProofin // see the submit button text change to "Continue..." and any eventual outcome // (off-boarding navigation or an inline error) reads as "we just got an error // after waiting." + // + // The titled interstitial only renders when the active locale bundle has + // step-upProcessing copy. States whose content sheet omits those rows (DC + // marks them !N/A!) would otherwise see i18next leak the literal key names + // "title"/"body" through the fallback chain — they fall back to a spinner-only + // status region. Adding the copy upstream is enough to switch in the titled + // interstitial; no code change needed. if (isProcessing || submitIdProofing.isPending) { + const hasInterstitialCopy = + i18n.exists('step-upProcessing:title') && i18n.exists('step-upProcessing:body') + if (hasInterstitialCopy) { + return ( + + ) + } return ( - +
+
) }