@redacto.io/consent-sdk-react 10.0.0-beta.9 → 10.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/NOTICE.md +17 -0
- package/README.md +141 -3
- package/dist/chunk-FEGTW73Q.mjs +182 -0
- package/dist/fonts.d.mts +35 -0
- package/dist/fonts.d.ts +35 -0
- package/dist/fonts.js +183 -0
- package/dist/fonts.mjs +10 -0
- package/dist/index.d.mts +12 -1
- package/dist/index.d.ts +12 -1
- package/dist/index.js +724 -365
- package/dist/index.mjs +346 -201
- package/dist/montserrat.generated-NBTECYRW.mjs +22 -0
- package/dist/privacy-center.d.mts +0 -1
- package/dist/privacy-center.d.ts +0 -1
- package/dist/privacy-center.js +2 -66
- package/dist/privacy-center.mjs +2 -66
- package/dist/types-6Aj7faxL.d.mts +57 -0
- package/dist/types-6Aj7faxL.d.ts +57 -0
- package/package.json +10 -1
- package/.turbo/turbo-build.log +0 -22
- package/.turbo/turbo-test.log +0 -203
- package/.turbo/turbo-verify-pack.log +0 -6
- package/CHANGELOG.md +0 -353
- package/src/RedactoNoticeAssisted/RedactoNoticeAssisted.test.tsx +0 -934
- package/src/RedactoNoticeAssisted/RedactoNoticeAssisted.tsx +0 -2160
- package/src/RedactoNoticeAssisted/api/index.ts +0 -275
- package/src/RedactoNoticeAssisted/api/types.ts +0 -182
- package/src/RedactoNoticeAssisted/assets/redacto-logo.png +0 -0
- package/src/RedactoNoticeAssisted/constants.ts +0 -51
- package/src/RedactoNoticeAssisted/i18n.ts +0 -1408
- package/src/RedactoNoticeAssisted/index.ts +0 -1
- package/src/RedactoNoticeAssisted/injectStyles.ts +0 -251
- package/src/RedactoNoticeAssisted/styles.ts +0 -730
- package/src/RedactoNoticeAssisted/types.ts +0 -26
- package/src/RedactoNoticeAssisted/useMediaQuery.ts +0 -36
- package/src/RedactoNoticeConsent/RedactoNoticeConsent.test.tsx +0 -1152
- package/src/RedactoNoticeConsent/RedactoNoticeConsent.tsx +0 -4308
- package/src/RedactoNoticeConsent/api/index.ts +0 -542
- package/src/RedactoNoticeConsent/api/sandbox.ts +0 -150
- package/src/RedactoNoticeConsent/api/types.ts +0 -515
- package/src/RedactoNoticeConsent/assets/redacto-logo.png +0 -0
- package/src/RedactoNoticeConsent/components/DigiLockerCallback.tsx +0 -602
- package/src/RedactoNoticeConsent/components/index.ts +0 -7
- package/src/RedactoNoticeConsent/constants.ts +0 -20
- package/src/RedactoNoticeConsent/index.ts +0 -1
- package/src/RedactoNoticeConsent/injectStyles.ts +0 -151
- package/src/RedactoNoticeConsent/language-codes.ts +0 -95
- package/src/RedactoNoticeConsent/services/storage.ts +0 -122
- package/src/RedactoNoticeConsent/styles.ts +0 -409
- package/src/RedactoNoticeConsent/types.ts +0 -80
- package/src/RedactoNoticeConsent/useMediaQuery.ts +0 -36
- package/src/RedactoNoticeConsentInline/RedactoNoticeConsentInline.test.tsx +0 -369
- package/src/RedactoNoticeConsentInline/RedactoNoticeConsentInline.tsx +0 -687
- package/src/RedactoNoticeConsentInline/api/index.ts +0 -106
- package/src/RedactoNoticeConsentInline/api/types.ts +0 -183
- package/src/RedactoNoticeConsentInline/assets/redacto-logo.png +0 -0
- package/src/RedactoNoticeConsentInline/index.ts +0 -1
- package/src/RedactoNoticeConsentInline/injectStyles.ts +0 -40
- package/src/RedactoNoticeConsentInline/styles.ts +0 -397
- package/src/RedactoNoticeConsentInline/types.ts +0 -51
- package/src/RedactoNoticeConsentInline/useMediaQuery.ts +0 -36
- package/src/RedactoPrivacyCenter/NotificationsPage.tsx +0 -182
- package/src/RedactoPrivacyCenter/PrivacyCenterActivityPage.tsx +0 -19
- package/src/RedactoPrivacyCenter/PrivacyCenterCaseDetailsPage.tsx +0 -156
- package/src/RedactoPrivacyCenter/PrivacyCenterConsentManagerPage.tsx +0 -17
- package/src/RedactoPrivacyCenter/PrivacyCenterFormPage.tsx +0 -79
- package/src/RedactoPrivacyCenter/PrivacyCenterHomePage.tsx +0 -36
- package/src/RedactoPrivacyCenter/PrivacyCenterLayout.tsx +0 -177
- package/src/RedactoPrivacyCenter/PrivacyCenterMyReceiptPage.tsx +0 -26
- package/src/RedactoPrivacyCenter/RedactoPrivacyCenter.tsx +0 -93
- package/src/RedactoPrivacyCenter/api/actions.ts +0 -695
- package/src/RedactoPrivacyCenter/api/client.ts +0 -1003
- package/src/RedactoPrivacyCenter/api/config.ts +0 -33
- package/src/RedactoPrivacyCenter/api/fetcher.ts +0 -275
- package/src/RedactoPrivacyCenter/api/orgBranding.ts +0 -16
- package/src/RedactoPrivacyCenter/api/upload.ts +0 -93
- package/src/RedactoPrivacyCenter/assets/character.svg +0 -31
- package/src/RedactoPrivacyCenter/assets/fileAttach.svg +0 -9
- package/src/RedactoPrivacyCenter/assets/greenTick.svg +0 -4
- package/src/RedactoPrivacyCenter/assets/mail.svg +0 -3
- package/src/RedactoPrivacyCenter/assets/poweredByRedacto.svg +0 -9
- package/src/RedactoPrivacyCenter/assets/sentImg.svg +0 -9
- package/src/RedactoPrivacyCenter/assets/uploadFileIcon.svg +0 -9
- package/src/RedactoPrivacyCenter/components/ActivitiesList.tsx +0 -252
- package/src/RedactoPrivacyCenter/components/ActivityTimeline.tsx +0 -239
- package/src/RedactoPrivacyCenter/components/AppShell/AppShell.tsx +0 -268
- package/src/RedactoPrivacyCenter/components/AppShell/MobileNavDrawer.tsx +0 -218
- package/src/RedactoPrivacyCenter/components/AppShell/NavItem.tsx +0 -205
- package/src/RedactoPrivacyCenter/components/AppShell/PageHeader.tsx +0 -107
- package/src/RedactoPrivacyCenter/components/AppShell/SidebarNav.tsx +0 -100
- package/src/RedactoPrivacyCenter/components/AppShell/TopAppBar.tsx +0 -339
- package/src/RedactoPrivacyCenter/components/AppShell/UserMenu.tsx +0 -245
- package/src/RedactoPrivacyCenter/components/AppShell/hooks/useIsTruncated.ts +0 -28
- package/src/RedactoPrivacyCenter/components/AppShell/hooks/useKeyboardShortcut.ts +0 -40
- package/src/RedactoPrivacyCenter/components/AppShell/hooks/useSidebarState.ts +0 -62
- package/src/RedactoPrivacyCenter/components/AppShell/usePrivacyCenterExitAction.ts +0 -36
- package/src/RedactoPrivacyCenter/components/CaseDetails/AttachmentCard.tsx +0 -38
- package/src/RedactoPrivacyCenter/components/CaseDetails/ChatBubble.tsx +0 -31
- package/src/RedactoPrivacyCenter/components/CaseDetails/DocumentRequestCard.tsx +0 -79
- package/src/RedactoPrivacyCenter/components/CaseDetails/FinalizedTooltipWrapper.tsx +0 -25
- package/src/RedactoPrivacyCenter/components/CaseDetails/MessageComposer.tsx +0 -96
- package/src/RedactoPrivacyCenter/components/CaseDetails/MessageItem.tsx +0 -97
- package/src/RedactoPrivacyCenter/components/CaseDetails/MessagesTab.tsx +0 -156
- package/src/RedactoPrivacyCenter/components/CaseDetails/RequestDetailsTab.tsx +0 -171
- package/src/RedactoPrivacyCenter/components/CaseDetails/StatusEvent.tsx +0 -23
- package/src/RedactoPrivacyCenter/components/CaseDetails/useFileUpload.ts +0 -116
- package/src/RedactoPrivacyCenter/components/CaseDetails/useMessagePolling.ts +0 -43
- package/src/RedactoPrivacyCenter/components/CaseDetails/utils.ts +0 -71
- package/src/RedactoPrivacyCenter/components/CaseDetailsModal.tsx +0 -469
- package/src/RedactoPrivacyCenter/components/CaseHistory.tsx +0 -426
- package/src/RedactoPrivacyCenter/components/ConsentManager.tsx +0 -1206
- package/src/RedactoPrivacyCenter/components/ConsentManagerCells.tsx +0 -218
- package/src/RedactoPrivacyCenter/components/ConsentManagerColumns.tsx +0 -138
- package/src/RedactoPrivacyCenter/components/ConsentManagerConstants.ts +0 -7
- package/src/RedactoPrivacyCenter/components/ConsentManagerExpandedRow.tsx +0 -179
- package/src/RedactoPrivacyCenter/components/ConsentManagerGroupedColumns.tsx +0 -318
- package/src/RedactoPrivacyCenter/components/ConsentManagerProductCards.tsx +0 -333
- package/src/RedactoPrivacyCenter/components/ConsentManagerPurposeCards.tsx +0 -102
- package/src/RedactoPrivacyCenter/components/ConsentManagerTableSkeleton.tsx +0 -159
- package/src/RedactoPrivacyCenter/components/DataRequest/CorrectionDataSection.tsx +0 -189
- package/src/RedactoPrivacyCenter/components/DataRequest/GrievanceDataSection.tsx +0 -118
- package/src/RedactoPrivacyCenter/components/DataRequest/NominationDataSection.tsx +0 -66
- package/src/RedactoPrivacyCenter/components/DataRequest/ProductSelectionSection.tsx +0 -222
- package/src/RedactoPrivacyCenter/components/DataRequest/PurposeSelectionSection.tsx +0 -331
- package/src/RedactoPrivacyCenter/components/Form.tsx +0 -1821
- package/src/RedactoPrivacyCenter/components/GrievanceTable.tsx +0 -174
- package/src/RedactoPrivacyCenter/components/GrievanceTablePagination.tsx +0 -337
- package/src/RedactoPrivacyCenter/components/ModifyConsentModal.tsx +0 -334
- package/src/RedactoPrivacyCenter/components/NotificationHub/NotificationBell.tsx +0 -173
- package/src/RedactoPrivacyCenter/components/NotificationHub/NotificationCTAButton.tsx +0 -18
- package/src/RedactoPrivacyCenter/components/NotificationHub/NotificationDropdown.tsx +0 -54
- package/src/RedactoPrivacyCenter/components/NotificationHub/NotificationDropdownItem.tsx +0 -57
- package/src/RedactoPrivacyCenter/components/NotificationHub/NotificationIcon.tsx +0 -19
- package/src/RedactoPrivacyCenter/components/NotificationHub/NotificationListPanel.tsx +0 -92
- package/src/RedactoPrivacyCenter/components/NotificationHub/NotificationRefreshButton.tsx +0 -20
- package/src/RedactoPrivacyCenter/components/NotificationHub/NotificationRow.tsx +0 -58
- package/src/RedactoPrivacyCenter/components/NotificationHub/StatCard.tsx +0 -10
- package/src/RedactoPrivacyCenter/components/NotificationHub/useNotificationActions.ts +0 -112
- package/src/RedactoPrivacyCenter/components/NotificationHub/utils.ts +0 -27
- package/src/RedactoPrivacyCenter/components/Pagination.tsx +0 -140
- package/src/RedactoPrivacyCenter/components/PrivacyCenterEmptyState.tsx +0 -76
- package/src/RedactoPrivacyCenter/components/PrivacyCenterSessionExpired.tsx +0 -97
- package/src/RedactoPrivacyCenter/components/ReceiptsList.tsx +0 -454
- package/src/RedactoPrivacyCenter/components/RequestSubmit.tsx +0 -270
- package/src/RedactoPrivacyCenter/components/SelectUser.tsx +0 -95
- package/src/RedactoPrivacyCenter/components/SelectUserGate.tsx +0 -195
- package/src/RedactoPrivacyCenter/components/SideMenuBarPC/constants.tsx +0 -25
- package/src/RedactoPrivacyCenter/components/SideMenuBarPC/types.ts +0 -21
- package/src/RedactoPrivacyCenter/components/caseHistoryColumns.tsx +0 -159
- package/src/RedactoPrivacyCenter/components/constant.ts +0 -52
- package/src/RedactoPrivacyCenter/components/groupHelpers.ts +0 -155
- package/src/RedactoPrivacyCenter/components/receiptColumns.tsx +0 -203
- package/src/RedactoPrivacyCenter/components/types.ts +0 -123
- package/src/RedactoPrivacyCenter/components/utils.tsx +0 -10
- package/src/RedactoPrivacyCenter/config/routes.ts +0 -9
- package/src/RedactoPrivacyCenter/config/table.ts +0 -25
- package/src/RedactoPrivacyCenter/context/AuthContext.tsx +0 -252
- package/src/RedactoPrivacyCenter/context/BaseUrlContext.tsx +0 -47
- package/src/RedactoPrivacyCenter/context/BrandingContext.tsx +0 -52
- package/src/RedactoPrivacyCenter/context/DataAvailabilityContext.session-expired.test.tsx +0 -92
- package/src/RedactoPrivacyCenter/context/DataAvailabilityContext.tsx +0 -181
- package/src/RedactoPrivacyCenter/context/LocaleContext.tsx +0 -11
- package/src/RedactoPrivacyCenter/context/NavigationContext.tsx +0 -96
- package/src/RedactoPrivacyCenter/context/OrganizationContext.tsx +0 -30
- package/src/RedactoPrivacyCenter/context/PageChromeContext.tsx +0 -80
- package/src/RedactoPrivacyCenter/context/ProfileSwitchContext.tsx +0 -15
- package/src/RedactoPrivacyCenter/context/types.ts +0 -57
- package/src/RedactoPrivacyCenter/lib/api.ts +0 -759
- package/src/RedactoPrivacyCenter/lib/consent-manager/utils.tsx +0 -67
- package/src/RedactoPrivacyCenter/lib/constants.ts +0 -283
- package/src/RedactoPrivacyCenter/lib/i18n.ts +0 -81
- package/src/RedactoPrivacyCenter/lib/locales.ts +0 -102
- package/src/RedactoPrivacyCenter/lib/queries.ts +0 -141
- package/src/RedactoPrivacyCenter/lib/sandbox.ts +0 -104
- package/src/RedactoPrivacyCenter/lib/types.ts +0 -896
- package/src/RedactoPrivacyCenter/lib/useCurrentLanguage.ts +0 -37
- package/src/RedactoPrivacyCenter/lib/utils.refresh-circuit.test.ts +0 -127
- package/src/RedactoPrivacyCenter/lib/utils.refresh-storage.test.ts +0 -85
- package/src/RedactoPrivacyCenter/lib/utils.session-expired.test.ts +0 -109
- package/src/RedactoPrivacyCenter/lib/utils.ts +0 -798
- package/src/RedactoPrivacyCenter/locales/as/translation.json +0 -342
- package/src/RedactoPrivacyCenter/locales/bn/translation.json +0 -342
- package/src/RedactoPrivacyCenter/locales/brx/translation.json +0 -342
- package/src/RedactoPrivacyCenter/locales/doi/translation.json +0 -342
- package/src/RedactoPrivacyCenter/locales/en/translation.json +0 -357
- package/src/RedactoPrivacyCenter/locales/gom/translation.json +0 -342
- package/src/RedactoPrivacyCenter/locales/gu/translation.json +0 -342
- package/src/RedactoPrivacyCenter/locales/hi/translation.json +0 -345
- package/src/RedactoPrivacyCenter/locales/kn/translation.json +0 -342
- package/src/RedactoPrivacyCenter/locales/ks/translation.json +0 -342
- package/src/RedactoPrivacyCenter/locales/mai/translation.json +0 -342
- package/src/RedactoPrivacyCenter/locales/ml/translation.json +0 -342
- package/src/RedactoPrivacyCenter/locales/mni-Mtei/translation.json +0 -342
- package/src/RedactoPrivacyCenter/locales/mr/translation.json +0 -342
- package/src/RedactoPrivacyCenter/locales/ne/translation.json +0 -342
- package/src/RedactoPrivacyCenter/locales/or/translation.json +0 -342
- package/src/RedactoPrivacyCenter/locales/pa/translation.json +0 -342
- package/src/RedactoPrivacyCenter/locales/sa/translation.json +0 -342
- package/src/RedactoPrivacyCenter/locales/sat/translation.json +0 -342
- package/src/RedactoPrivacyCenter/locales/sd/translation.json +0 -342
- package/src/RedactoPrivacyCenter/locales/ta/translation.json +0 -342
- package/src/RedactoPrivacyCenter/locales/te/translation.json +0 -342
- package/src/RedactoPrivacyCenter/locales/ur/translation.json +0 -342
- package/src/RedactoPrivacyCenter/styles/applyBrandingTheme.ts +0 -66
- package/src/RedactoPrivacyCenter/styles/base.css +0 -117
- package/src/RedactoPrivacyCenter/styles/injectStyles.ts +0 -3108
- package/src/RedactoPrivacyCenter/styles/pcStyles.ts +0 -831
- package/src/RedactoPrivacyCenter/styles/responsive.ts +0 -138
- package/src/RedactoPrivacyCenter/styles/variables.css +0 -96
- package/src/RedactoPrivacyCenter/table/AdvancedTable.tsx +0 -387
- package/src/RedactoPrivacyCenter/table/AdvancedTablePagination.tsx +0 -468
- package/src/RedactoPrivacyCenter/table/index.ts +0 -2
- package/src/RedactoPrivacyCenter/types/images.d.ts +0 -29
- package/src/RedactoPrivacyCenter/ui/Footer.tsx +0 -69
- package/src/RedactoPrivacyCenter/ui/FormBlockWrapper.tsx +0 -54
- package/src/RedactoPrivacyCenter/ui/PCAccordion.tsx +0 -93
- package/src/RedactoPrivacyCenter/ui/PCActionIcon.tsx +0 -47
- package/src/RedactoPrivacyCenter/ui/PCAvatar.test.tsx +0 -83
- package/src/RedactoPrivacyCenter/ui/PCAvatar.tsx +0 -93
- package/src/RedactoPrivacyCenter/ui/PCBadge.tsx +0 -48
- package/src/RedactoPrivacyCenter/ui/PCButton.tsx +0 -77
- package/src/RedactoPrivacyCenter/ui/PCCheckbox.tsx +0 -55
- package/src/RedactoPrivacyCenter/ui/PCCollapse.tsx +0 -107
- package/src/RedactoPrivacyCenter/ui/PCEmpty.tsx +0 -35
- package/src/RedactoPrivacyCenter/ui/PCInput.tsx +0 -132
- package/src/RedactoPrivacyCenter/ui/PCLoader.tsx +0 -31
- package/src/RedactoPrivacyCenter/ui/PCModal.tsx +0 -106
- package/src/RedactoPrivacyCenter/ui/PCPopover.tsx +0 -206
- package/src/RedactoPrivacyCenter/ui/PCSelect.tsx +0 -433
- package/src/RedactoPrivacyCenter/ui/PCTable.tsx +0 -149
- package/src/RedactoPrivacyCenter/ui/PCText.tsx +0 -41
- package/src/RedactoPrivacyCenter/ui/PCTextarea.tsx +0 -43
- package/src/RedactoPrivacyCenter/ui/PCTitle.tsx +0 -37
- package/src/RedactoPrivacyCenter/ui/PCTooltip.tsx +0 -121
- package/src/RedactoPrivacyCenter/ui/SimpleBar.tsx +0 -25
- package/src/RedactoPrivacyCenter/ui/StatusBadge.tsx +0 -53
- package/src/RedactoPrivacyCenter/ui/index.ts +0 -18
- package/src/RedactoPrivacyCenter/utils/cn.ts +0 -28
- package/src/RedactoPrivacyCenter/utils/date.ts +0 -48
- package/src/RedactoPrivacyCenter/utils/status.ts +0 -111
- package/src/RedactoPrivacyCenter/utils/toast.ts +0 -130
- package/src/index.ts +0 -19
- package/src/privacy-center.ts +0 -113
- package/src/shared/api-errors.ts +0 -180
- package/src/shared/notice-consent-status.ts +0 -190
- package/src/shared/sandbox.ts +0 -6
- package/src/types/css.d.ts +0 -4
- package/src/types/images.d.ts +0 -19
- package/src/types/react-jsx.d.ts +0 -26
- package/tests/DataAvailabilityContext.test.tsx +0 -123
- package/tests/Form.test.tsx +0 -1424
- package/tests/ManageConsentErrorMapping.test.tsx +0 -85
- package/tests/ProductSelectionSection.test.tsx +0 -328
- package/tests/RequestSubmit.test.tsx +0 -135
- package/tests/RevocablePurpose.test.tsx +0 -146
- package/tests/RevokeWarningMessage.test.tsx +0 -151
- package/tests/consentClient.test.ts +0 -217
- package/tests/manageConsentServer.test.ts +0 -38
- package/tests/mocks.ts +0 -257
- package/tests/resolveContactFromJwt.test.ts +0 -44
- package/tests/setup.ts +0 -16
- package/tsconfig.json +0 -16
- package/tsup.config.ts +0 -31
- package/vitest.config.ts +0 -16
|
@@ -1,934 +0,0 @@
|
|
|
1
|
-
import "@testing-library/jest-dom";
|
|
2
|
-
import {
|
|
3
|
-
fireEvent,
|
|
4
|
-
render,
|
|
5
|
-
screen,
|
|
6
|
-
waitFor,
|
|
7
|
-
within,
|
|
8
|
-
} from "@testing-library/react";
|
|
9
|
-
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
10
|
-
import { RedactoNoticeAssisted } from "./RedactoNoticeAssisted";
|
|
11
|
-
import { ASSISTED_CONFIG } from "./constants";
|
|
12
|
-
import {
|
|
13
|
-
fetchAssistedNotice,
|
|
14
|
-
fetchAssistedTTSAudioUrls,
|
|
15
|
-
createOtp,
|
|
16
|
-
verifyOtp,
|
|
17
|
-
submitAssistedConsent,
|
|
18
|
-
} from "./api";
|
|
19
|
-
import type { ConsentContent, TTSAudioUrlsResponse } from "./api/types";
|
|
20
|
-
import { ApiError } from "../shared/api-errors";
|
|
21
|
-
import { fetchNoticeConsentStatusForScope } from "../shared/notice-consent-status";
|
|
22
|
-
import { makeStaticT } from "./i18n";
|
|
23
|
-
|
|
24
|
-
vi.mock("./api", () => ({
|
|
25
|
-
fetchAssistedNotice: vi.fn(),
|
|
26
|
-
fetchAssistedTTSAudioUrls: vi.fn(),
|
|
27
|
-
createOtp: vi.fn(),
|
|
28
|
-
verifyOtp: vi.fn(),
|
|
29
|
-
submitAssistedConsent: vi.fn(),
|
|
30
|
-
}));
|
|
31
|
-
|
|
32
|
-
// Mock only the ledger check-consent network read; keep the real
|
|
33
|
-
// isNoticeFullyConsented predicate so the already-consented decision is exercised.
|
|
34
|
-
vi.mock("../shared/notice-consent-status", async (importActual) => {
|
|
35
|
-
const actual =
|
|
36
|
-
await importActual<typeof import("../shared/notice-consent-status")>();
|
|
37
|
-
return {
|
|
38
|
-
...actual,
|
|
39
|
-
fetchNoticeConsentStatusForScope: vi.fn(),
|
|
40
|
-
};
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
const LEDGER_BASE_URL = "https://ledger.example.test/consent-ledger";
|
|
44
|
-
|
|
45
|
-
const otpLength = ASSISTED_CONFIG.otpLength;
|
|
46
|
-
const mockedFetch = vi.mocked(fetchAssistedNotice);
|
|
47
|
-
const mockedFetchTTS = vi.mocked(fetchAssistedTTSAudioUrls);
|
|
48
|
-
const mockedCreateOtp = vi.mocked(createOtp);
|
|
49
|
-
const mockedVerifyOtp = vi.mocked(verifyOtp);
|
|
50
|
-
const mockedSubmitConsent = vi.mocked(submitAssistedConsent);
|
|
51
|
-
const mockedFetchScope = vi.mocked(fetchNoticeConsentStatusForScope);
|
|
52
|
-
|
|
53
|
-
const OTP_DETAIL = {
|
|
54
|
-
uuid: "otp-1",
|
|
55
|
-
to: "+919876543210",
|
|
56
|
-
channel: "sms",
|
|
57
|
-
expire_at: "2026-06-24T09:55:34Z",
|
|
58
|
-
success: true,
|
|
59
|
-
message: "OTP created successfully",
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
const VERIFY_DETAIL = {
|
|
63
|
-
token: null,
|
|
64
|
-
access_token: "access-token-1",
|
|
65
|
-
refresh_token: "refresh-token-1",
|
|
66
|
-
expires_in: 3600,
|
|
67
|
-
token_type: "Bearer",
|
|
68
|
-
success: true,
|
|
69
|
-
message: "OTP verified successfully",
|
|
70
|
-
};
|
|
71
|
-
|
|
72
|
-
const PROPS = {
|
|
73
|
-
organisationUuid: "org-1",
|
|
74
|
-
workspaceUuid: "ws-1",
|
|
75
|
-
noticeUuid: "ntc-1",
|
|
76
|
-
baseUrl: "https://api.example.test/consent",
|
|
77
|
-
};
|
|
78
|
-
|
|
79
|
-
// Trimmed EA-Sports notice payload (shape of the public API response).
|
|
80
|
-
const NOTICE = {
|
|
81
|
-
detail: {
|
|
82
|
-
active_config: {
|
|
83
|
-
notice_text: "We use data to personalise your experience.",
|
|
84
|
-
additional_text: "Manage or withdraw consent anytime at",
|
|
85
|
-
confirm_button_text: "Agree",
|
|
86
|
-
decline_button_text: "Decline",
|
|
87
|
-
logo_url: "https://cdn.example.test/ea.svg",
|
|
88
|
-
privacy_policy_url: "https://www.ea.com/legal/privacy",
|
|
89
|
-
privacy_center_url: "https://privacy.example.test/",
|
|
90
|
-
primary_color: "#9c0716",
|
|
91
|
-
secondary_color: "#000bdb",
|
|
92
|
-
font_preference: "Arial",
|
|
93
|
-
privacy_policy_prefix_text: "Learn more in our",
|
|
94
|
-
privacy_policy_anchor_text: "Privacy Policy",
|
|
95
|
-
privacy_center_anchor_text: "Privacy Center",
|
|
96
|
-
purpose_section_heading: "Manage What You Share",
|
|
97
|
-
notice_banner_heading: "EA Sports",
|
|
98
|
-
default_language: "English",
|
|
99
|
-
purposes: [
|
|
100
|
-
{
|
|
101
|
-
uuid: "p-account",
|
|
102
|
-
name: "Account Management",
|
|
103
|
-
description: "To maintain and update your account information.",
|
|
104
|
-
data_elements: [
|
|
105
|
-
{ uuid: "e-email", name: "Email", enabled: true, required: true },
|
|
106
|
-
{
|
|
107
|
-
uuid: "e-phone",
|
|
108
|
-
name: "Phone Number",
|
|
109
|
-
enabled: true,
|
|
110
|
-
required: false,
|
|
111
|
-
},
|
|
112
|
-
],
|
|
113
|
-
},
|
|
114
|
-
{
|
|
115
|
-
uuid: "p-identity",
|
|
116
|
-
name: "Identity Verification",
|
|
117
|
-
description: "To authenticate your identity securely.",
|
|
118
|
-
data_elements: [
|
|
119
|
-
{
|
|
120
|
-
uuid: "e-govid",
|
|
121
|
-
name: "Government ID",
|
|
122
|
-
enabled: true,
|
|
123
|
-
required: true,
|
|
124
|
-
},
|
|
125
|
-
],
|
|
126
|
-
},
|
|
127
|
-
],
|
|
128
|
-
dpo_info: {
|
|
129
|
-
grievance_text: "If you have any grievances",
|
|
130
|
-
grievance_url: "https://google.com",
|
|
131
|
-
grievance_anchor_text: "click here",
|
|
132
|
-
grievance_email_connector_text: "or email to",
|
|
133
|
-
grievance_email: "help@redacto.io",
|
|
134
|
-
dp_board_text: "You can escalate to the DP board",
|
|
135
|
-
dp_board_url: "https://google.com",
|
|
136
|
-
dp_board_anchor_text: "click here",
|
|
137
|
-
dpo_text: "To connect with the DPO",
|
|
138
|
-
dpo_email: "sadan@redacto.ai",
|
|
139
|
-
dpo_anchor_text: "Click here",
|
|
140
|
-
},
|
|
141
|
-
supported_languages_and_translations: {
|
|
142
|
-
Hindi: {
|
|
143
|
-
notice_banner_heading: "ईए स्पोर्ट्स",
|
|
144
|
-
notice_text: "हम डेटा का उपयोग करते हैं।",
|
|
145
|
-
purpose_section_heading: "आप जो साझा करते हैं उसे प्रबंधित करें",
|
|
146
|
-
purposes: {
|
|
147
|
-
"p-account": { name: "खाता प्रबंधन", description: "..." },
|
|
148
|
-
},
|
|
149
|
-
data_elements: { "e-email": "ईमेल" },
|
|
150
|
-
},
|
|
151
|
-
},
|
|
152
|
-
},
|
|
153
|
-
},
|
|
154
|
-
} as unknown as ConsentContent;
|
|
155
|
-
|
|
156
|
-
// Notice variant carrying a notice_uuid — the audio effect only fires when one is
|
|
157
|
-
// present, so the base NOTICE (no uuid) leaves TTS disabled.
|
|
158
|
-
const NOTICE_WITH_AUDIO = {
|
|
159
|
-
detail: {
|
|
160
|
-
active_config: {
|
|
161
|
-
...NOTICE.detail.active_config,
|
|
162
|
-
notice_uuid: "ntc-1",
|
|
163
|
-
},
|
|
164
|
-
},
|
|
165
|
-
} as unknown as ConsentContent;
|
|
166
|
-
|
|
167
|
-
// Minimal audio response in the new endpoint shape (incl. language_name +
|
|
168
|
-
// full_notice_audio_url). Only a couple of clip URLs are needed to exercise the
|
|
169
|
-
// player; the sequence builder skips any missing ones.
|
|
170
|
-
const TTS_RESPONSE = {
|
|
171
|
-
code: 200,
|
|
172
|
-
status: "OK",
|
|
173
|
-
detail: {
|
|
174
|
-
language_name: "English",
|
|
175
|
-
uuid: "tts-1",
|
|
176
|
-
language: "en",
|
|
177
|
-
notice_text_audio_url: "https://cdn.example.test/notice.mp3",
|
|
178
|
-
additional_text_audio_url: "https://cdn.example.test/additional.mp3",
|
|
179
|
-
notice_summary_audio_url: null,
|
|
180
|
-
confirm_button_text_audio_url: "https://cdn.example.test/confirm.mp3",
|
|
181
|
-
decline_button_text_audio_url: "https://cdn.example.test/decline.mp3",
|
|
182
|
-
privacy_policy_prefix_text_audio_url: "https://cdn.example.test/pp-prefix.mp3",
|
|
183
|
-
privacy_policy_anchor_text_audio_url: "https://cdn.example.test/pp-anchor.mp3",
|
|
184
|
-
privacy_center_anchor_text_audio_url: "https://cdn.example.test/pc.mp3",
|
|
185
|
-
purpose_section_heading_audio_url: "https://cdn.example.test/section.mp3",
|
|
186
|
-
notice_banner_heading_audio_url: "https://cdn.example.test/banner.mp3",
|
|
187
|
-
purposes_audio: {
|
|
188
|
-
"p-account": {
|
|
189
|
-
name_audio_url: "https://cdn.example.test/p-account-name.mp3",
|
|
190
|
-
description_audio_url: "https://cdn.example.test/p-account-desc.mp3",
|
|
191
|
-
},
|
|
192
|
-
},
|
|
193
|
-
data_elements_audio: {
|
|
194
|
-
"e-email": { name_audio_url: "https://cdn.example.test/e-email.mp3" },
|
|
195
|
-
},
|
|
196
|
-
dpo_info_audio: {
|
|
197
|
-
dpo_text_audio_url: "https://cdn.example.test/dpo.mp3",
|
|
198
|
-
},
|
|
199
|
-
full_notice_audio_url: "https://cdn.example.test/full.mp3",
|
|
200
|
-
},
|
|
201
|
-
} as unknown as TTSAudioUrlsResponse;
|
|
202
|
-
|
|
203
|
-
const typeOtp = (value: string) => {
|
|
204
|
-
const boxes = screen.getAllByLabelText(/OTP digit/);
|
|
205
|
-
expect(boxes).toHaveLength(otpLength);
|
|
206
|
-
value.split("").forEach((digit, i) => {
|
|
207
|
-
fireEvent.change(boxes[i], { target: { value: digit } });
|
|
208
|
-
});
|
|
209
|
-
};
|
|
210
|
-
|
|
211
|
-
const renderLoaded = async () => {
|
|
212
|
-
const result = render(<RedactoNoticeAssisted {...PROPS} />);
|
|
213
|
-
await screen.findByRole("heading", { name: "EA Sports" });
|
|
214
|
-
return result;
|
|
215
|
-
};
|
|
216
|
-
|
|
217
|
-
// Ticking both purposes selects their required data elements (Email, Government
|
|
218
|
-
// ID), which the Confirm CTA now requires before the verify step can open.
|
|
219
|
-
const selectRequired = () => {
|
|
220
|
-
fireEvent.click(
|
|
221
|
-
screen.getByRole("checkbox", {
|
|
222
|
-
name: /Select all data elements for Account Management/,
|
|
223
|
-
}),
|
|
224
|
-
);
|
|
225
|
-
fireEvent.click(
|
|
226
|
-
screen.getByRole("checkbox", {
|
|
227
|
-
name: /Select all data elements for Identity Verification/,
|
|
228
|
-
}),
|
|
229
|
-
);
|
|
230
|
-
};
|
|
231
|
-
|
|
232
|
-
describe("RedactoNoticeAssisted", () => {
|
|
233
|
-
beforeEach(() => {
|
|
234
|
-
mockedFetch.mockReset();
|
|
235
|
-
mockedFetchTTS.mockReset();
|
|
236
|
-
mockedCreateOtp.mockReset();
|
|
237
|
-
mockedVerifyOtp.mockReset();
|
|
238
|
-
mockedSubmitConsent.mockReset();
|
|
239
|
-
mockedFetch.mockResolvedValue(NOTICE);
|
|
240
|
-
mockedFetchTTS.mockResolvedValue(TTS_RESPONSE);
|
|
241
|
-
mockedCreateOtp.mockResolvedValue(OTP_DETAIL);
|
|
242
|
-
mockedVerifyOtp.mockResolvedValue(VERIFY_DETAIL);
|
|
243
|
-
mockedSubmitConsent.mockResolvedValue(null);
|
|
244
|
-
// Default: ledger reports no prior full consent, so flows proceed to submit.
|
|
245
|
-
mockedFetchScope.mockReset();
|
|
246
|
-
mockedFetchScope.mockResolvedValue(null);
|
|
247
|
-
});
|
|
248
|
-
|
|
249
|
-
it("fetches with the public-notice props (no auth)", async () => {
|
|
250
|
-
await renderLoaded();
|
|
251
|
-
expect(mockedFetch).toHaveBeenCalledWith(
|
|
252
|
-
expect.objectContaining({
|
|
253
|
-
organisationUuid: "org-1",
|
|
254
|
-
workspaceUuid: "ws-1",
|
|
255
|
-
noticeUuid: "ntc-1",
|
|
256
|
-
baseUrl: "https://api.example.test/consent",
|
|
257
|
-
}),
|
|
258
|
-
);
|
|
259
|
-
});
|
|
260
|
-
|
|
261
|
-
it("renders the fetched notice (logo, title, purposes, footer)", async () => {
|
|
262
|
-
await renderLoaded();
|
|
263
|
-
expect(screen.getByAltText("Logo")).toHaveAttribute(
|
|
264
|
-
"src",
|
|
265
|
-
"https://cdn.example.test/ea.svg",
|
|
266
|
-
);
|
|
267
|
-
expect(
|
|
268
|
-
screen.getByRole("link", { name: /Privacy Policy/ }),
|
|
269
|
-
).toHaveAttribute("href", "https://www.ea.com/legal/privacy");
|
|
270
|
-
expect(
|
|
271
|
-
screen.getByRole("heading", { name: "Manage What You Share" }),
|
|
272
|
-
).toBeInTheDocument();
|
|
273
|
-
expect(screen.getByText("Account Management")).toBeInTheDocument();
|
|
274
|
-
expect(screen.getByText("Identity Verification")).toBeInTheDocument();
|
|
275
|
-
expect(screen.getByRole("button", { name: "Agree" })).toBeInTheDocument();
|
|
276
|
-
expect(screen.getByRole("button", { name: "Decline" })).toBeInTheDocument();
|
|
277
|
-
});
|
|
278
|
-
|
|
279
|
-
it("keeps Confirm disabled until every required data element is selected", async () => {
|
|
280
|
-
await renderLoaded();
|
|
281
|
-
const confirm = screen.getByRole("button", { name: "Agree" });
|
|
282
|
-
// Required elements (Email, Government ID) start unchecked → CTA disabled.
|
|
283
|
-
expect(confirm).toBeDisabled();
|
|
284
|
-
|
|
285
|
-
// Selecting only one purpose still leaves a required element unchecked.
|
|
286
|
-
fireEvent.click(
|
|
287
|
-
screen.getByRole("checkbox", {
|
|
288
|
-
name: /Select all data elements for Account Management/,
|
|
289
|
-
}),
|
|
290
|
-
);
|
|
291
|
-
expect(confirm).toBeDisabled();
|
|
292
|
-
|
|
293
|
-
// Once both required elements are selected, the CTA enables.
|
|
294
|
-
fireEvent.click(
|
|
295
|
-
screen.getByRole("checkbox", {
|
|
296
|
-
name: /Select all data elements for Identity Verification/,
|
|
297
|
-
}),
|
|
298
|
-
);
|
|
299
|
-
expect(confirm).toBeEnabled();
|
|
300
|
-
});
|
|
301
|
-
|
|
302
|
-
it("checks the parent purpose once its required element is selected", async () => {
|
|
303
|
-
await renderLoaded();
|
|
304
|
-
// Expand Account Management (Email = required, Phone = optional).
|
|
305
|
-
fireEvent.click(screen.getByRole("button", { name: /Account Management/ }));
|
|
306
|
-
const purpose = screen.getByRole("checkbox", {
|
|
307
|
-
name: "Select all data elements for Account Management",
|
|
308
|
-
});
|
|
309
|
-
const email = screen.getByRole("checkbox", {
|
|
310
|
-
name: "Select Email (required)",
|
|
311
|
-
});
|
|
312
|
-
expect(purpose).not.toBeChecked();
|
|
313
|
-
// Email is the only required element → ticking it checks the purpose.
|
|
314
|
-
fireEvent.click(email);
|
|
315
|
-
expect(email).toBeChecked();
|
|
316
|
-
expect(purpose).toBeChecked();
|
|
317
|
-
});
|
|
318
|
-
|
|
319
|
-
it("unchecks the parent purpose when a required element is deselected", async () => {
|
|
320
|
-
await renderLoaded();
|
|
321
|
-
fireEvent.click(screen.getByRole("button", { name: /Account Management/ }));
|
|
322
|
-
const purpose = screen.getByRole("checkbox", {
|
|
323
|
-
name: "Select all data elements for Account Management",
|
|
324
|
-
});
|
|
325
|
-
const email = screen.getByRole("checkbox", {
|
|
326
|
-
name: "Select Email (required)",
|
|
327
|
-
});
|
|
328
|
-
// Ticking the purpose selects every element.
|
|
329
|
-
fireEvent.click(purpose);
|
|
330
|
-
expect(purpose).toBeChecked();
|
|
331
|
-
expect(email).toBeChecked();
|
|
332
|
-
// Removing the required Email element drops the purpose.
|
|
333
|
-
fireEvent.click(email);
|
|
334
|
-
expect(email).not.toBeChecked();
|
|
335
|
-
expect(purpose).not.toBeChecked();
|
|
336
|
-
});
|
|
337
|
-
|
|
338
|
-
it("selects and clears every data element when the purpose is toggled", async () => {
|
|
339
|
-
await renderLoaded();
|
|
340
|
-
fireEvent.click(screen.getByRole("button", { name: /Account Management/ }));
|
|
341
|
-
const purpose = screen.getByRole("checkbox", {
|
|
342
|
-
name: "Select all data elements for Account Management",
|
|
343
|
-
});
|
|
344
|
-
const email = screen.getByRole("checkbox", {
|
|
345
|
-
name: "Select Email (required)",
|
|
346
|
-
});
|
|
347
|
-
const phone = screen.getByRole("checkbox", {
|
|
348
|
-
name: "Select Phone Number",
|
|
349
|
-
});
|
|
350
|
-
fireEvent.click(purpose);
|
|
351
|
-
expect(email).toBeChecked();
|
|
352
|
-
expect(phone).toBeChecked();
|
|
353
|
-
fireEvent.click(purpose);
|
|
354
|
-
expect(email).not.toBeChecked();
|
|
355
|
-
expect(phone).not.toBeChecked();
|
|
356
|
-
});
|
|
357
|
-
|
|
358
|
-
it("lists the API languages in the dropdown", async () => {
|
|
359
|
-
await renderLoaded();
|
|
360
|
-
fireEvent.click(screen.getByRole("button", { name: /Select language/ }));
|
|
361
|
-
const list = screen.getByRole("listbox");
|
|
362
|
-
expect(
|
|
363
|
-
within(list).getByRole("option", { name: "English" }),
|
|
364
|
-
).toBeInTheDocument();
|
|
365
|
-
expect(
|
|
366
|
-
within(list).getByRole("option", { name: /Hindi/ }),
|
|
367
|
-
).toBeInTheDocument();
|
|
368
|
-
});
|
|
369
|
-
|
|
370
|
-
it("renders a non-English default_language from the translations", async () => {
|
|
371
|
-
// default_language points at Hindi; the base active_config fields are English,
|
|
372
|
-
// so the Hindi copy must come from supported_languages_and_translations.
|
|
373
|
-
const hindiDefault = {
|
|
374
|
-
detail: {
|
|
375
|
-
active_config: {
|
|
376
|
-
...NOTICE.detail.active_config,
|
|
377
|
-
default_language: "Hindi",
|
|
378
|
-
},
|
|
379
|
-
},
|
|
380
|
-
} as unknown as ConsentContent;
|
|
381
|
-
mockedFetch.mockResolvedValue(hindiDefault);
|
|
382
|
-
|
|
383
|
-
render(<RedactoNoticeAssisted {...PROPS} />);
|
|
384
|
-
// Heading + notice text render in Hindi, not the English base copy.
|
|
385
|
-
expect(
|
|
386
|
-
await screen.findByRole("heading", { name: "ईए स्पोर्ट्स" }),
|
|
387
|
-
).toBeInTheDocument();
|
|
388
|
-
expect(screen.getByText("हम डेटा का उपयोग करते हैं।")).toBeInTheDocument();
|
|
389
|
-
expect(
|
|
390
|
-
screen.queryByText("We use data to personalise your experience."),
|
|
391
|
-
).not.toBeInTheDocument();
|
|
392
|
-
});
|
|
393
|
-
|
|
394
|
-
it("expands a purpose accordion to reveal its data elements", async () => {
|
|
395
|
-
await renderLoaded();
|
|
396
|
-
expect(screen.queryByText("Email")).not.toBeInTheDocument();
|
|
397
|
-
fireEvent.click(screen.getByRole("button", { name: /Account Management/ }));
|
|
398
|
-
expect(screen.getByText("Email")).toBeInTheDocument();
|
|
399
|
-
expect(screen.getByText("Phone Number")).toBeInTheDocument();
|
|
400
|
-
});
|
|
401
|
-
|
|
402
|
-
it("walks the single-screen flow: Confirm → mobile → Send OTP → Confirm OTP → onComplete", async () => {
|
|
403
|
-
const onComplete = vi.fn();
|
|
404
|
-
render(<RedactoNoticeAssisted {...PROPS} onComplete={onComplete} />);
|
|
405
|
-
await screen.findByRole("heading", { name: "EA Sports" });
|
|
406
|
-
|
|
407
|
-
// The notice stays mounted throughout; clicking Confirm reveals the inline
|
|
408
|
-
// verify panel below it. (API confirm label is "Agree" in this fixture.)
|
|
409
|
-
selectRequired();
|
|
410
|
-
fireEvent.click(screen.getByRole("button", { name: "Agree" }));
|
|
411
|
-
expect(
|
|
412
|
-
screen.getByRole("heading", { name: "Manage What You Share" }),
|
|
413
|
-
).toBeInTheDocument();
|
|
414
|
-
const mobileInput = screen.getByLabelText("Mobile number");
|
|
415
|
-
expect(screen.queryByLabelText(/OTP digit/)).not.toBeInTheDocument();
|
|
416
|
-
|
|
417
|
-
const sendOtpBtn = screen.getByRole("button", { name: "Send OTP" });
|
|
418
|
-
expect(sendOtpBtn).toBeDisabled();
|
|
419
|
-
fireEvent.change(mobileInput, { target: { value: "9876543210" } });
|
|
420
|
-
expect(sendOtpBtn).toBeEnabled();
|
|
421
|
-
|
|
422
|
-
fireEvent.click(sendOtpBtn);
|
|
423
|
-
// Create-OTP is called with the E.164 recipient + sms channel.
|
|
424
|
-
await waitFor(() =>
|
|
425
|
-
expect(mockedCreateOtp).toHaveBeenCalledWith(
|
|
426
|
-
expect.objectContaining({ to: "+919876543210", channel: "sms" }),
|
|
427
|
-
),
|
|
428
|
-
);
|
|
429
|
-
expect(await screen.findByText(/OTP sent to/)).toHaveTextContent(
|
|
430
|
-
"OTP sent to +91 9876543210",
|
|
431
|
-
);
|
|
432
|
-
expect(
|
|
433
|
-
screen.getByRole("button", { name: "Change Mobile Number" }),
|
|
434
|
-
).toBeInTheDocument();
|
|
435
|
-
|
|
436
|
-
const confirmOtpBtn = screen.getByRole("button", { name: "Confirm OTP" });
|
|
437
|
-
expect(confirmOtpBtn).toBeDisabled();
|
|
438
|
-
typeOtp("1".repeat(otpLength));
|
|
439
|
-
expect(confirmOtpBtn).toBeEnabled();
|
|
440
|
-
|
|
441
|
-
fireEvent.click(confirmOtpBtn);
|
|
442
|
-
// Verify is called with the create-OTP uuid, then consent is submitted with
|
|
443
|
-
// the access_token from verify.
|
|
444
|
-
await waitFor(() =>
|
|
445
|
-
expect(mockedVerifyOtp).toHaveBeenCalledWith(
|
|
446
|
-
expect.objectContaining({
|
|
447
|
-
uuid: "otp-1",
|
|
448
|
-
otp: "1".repeat(otpLength),
|
|
449
|
-
purpose: "login",
|
|
450
|
-
}),
|
|
451
|
-
),
|
|
452
|
-
);
|
|
453
|
-
expect(mockedSubmitConsent).toHaveBeenCalledWith(
|
|
454
|
-
expect.objectContaining({
|
|
455
|
-
accessToken: "access-token-1",
|
|
456
|
-
organisationUuid: "org-1",
|
|
457
|
-
workspaceUuid: "ws-1",
|
|
458
|
-
}),
|
|
459
|
-
);
|
|
460
|
-
// No confirmation screen — onComplete fires immediately on success.
|
|
461
|
-
await waitFor(() => expect(onComplete).toHaveBeenCalledTimes(1));
|
|
462
|
-
expect(
|
|
463
|
-
screen.queryByRole("heading", { name: "Consent confirmed" }),
|
|
464
|
-
).not.toBeInTheDocument();
|
|
465
|
-
});
|
|
466
|
-
|
|
467
|
-
it("verifies by email: Confirm → Email toggle → Send OTP (channel email) → Confirm OTP → onComplete", async () => {
|
|
468
|
-
const onComplete = vi.fn();
|
|
469
|
-
render(<RedactoNoticeAssisted {...PROPS} onComplete={onComplete} />);
|
|
470
|
-
await screen.findByRole("heading", { name: "EA Sports" });
|
|
471
|
-
|
|
472
|
-
selectRequired();
|
|
473
|
-
fireEvent.click(screen.getByRole("button", { name: "Agree" }));
|
|
474
|
-
|
|
475
|
-
// Mobile is the default; its field is shown. Switch to email via the link.
|
|
476
|
-
expect(screen.getByLabelText("Mobile number")).toBeInTheDocument();
|
|
477
|
-
fireEvent.click(screen.getByRole("button", { name: /Use email instead/ }));
|
|
478
|
-
|
|
479
|
-
// The mobile field is replaced by the email field.
|
|
480
|
-
expect(screen.queryByLabelText("Mobile number")).not.toBeInTheDocument();
|
|
481
|
-
const emailInput = screen.getByLabelText("Email address");
|
|
482
|
-
|
|
483
|
-
const sendOtpBtn = screen.getByRole("button", { name: "Send OTP" });
|
|
484
|
-
// An invalid address keeps Send OTP disabled; a valid one enables it.
|
|
485
|
-
fireEvent.change(emailInput, { target: { value: "not-an-email" } });
|
|
486
|
-
expect(sendOtpBtn).toBeDisabled();
|
|
487
|
-
fireEvent.change(emailInput, { target: { value: "user@example.com" } });
|
|
488
|
-
expect(sendOtpBtn).toBeEnabled();
|
|
489
|
-
|
|
490
|
-
fireEvent.click(sendOtpBtn);
|
|
491
|
-
// Create-OTP is called with the raw email recipient + the "email" channel.
|
|
492
|
-
await waitFor(() =>
|
|
493
|
-
expect(mockedCreateOtp).toHaveBeenCalledWith(
|
|
494
|
-
expect.objectContaining({ to: "user@example.com", channel: "email" }),
|
|
495
|
-
),
|
|
496
|
-
);
|
|
497
|
-
expect(await screen.findByText(/OTP sent to/)).toHaveTextContent(
|
|
498
|
-
"OTP sent to user@example.com",
|
|
499
|
-
);
|
|
500
|
-
expect(
|
|
501
|
-
screen.getByRole("button", { name: "Change Email" }),
|
|
502
|
-
).toBeInTheDocument();
|
|
503
|
-
|
|
504
|
-
const confirmOtpBtn = screen.getByRole("button", { name: "Confirm OTP" });
|
|
505
|
-
expect(confirmOtpBtn).toBeDisabled();
|
|
506
|
-
typeOtp("1".repeat(otpLength));
|
|
507
|
-
expect(confirmOtpBtn).toBeEnabled();
|
|
508
|
-
|
|
509
|
-
fireEvent.click(confirmOtpBtn);
|
|
510
|
-
await waitFor(() =>
|
|
511
|
-
expect(mockedVerifyOtp).toHaveBeenCalledWith(
|
|
512
|
-
expect.objectContaining({
|
|
513
|
-
uuid: "otp-1",
|
|
514
|
-
otp: "1".repeat(otpLength),
|
|
515
|
-
purpose: "login",
|
|
516
|
-
}),
|
|
517
|
-
),
|
|
518
|
-
);
|
|
519
|
-
expect(mockedSubmitConsent).toHaveBeenCalledWith(
|
|
520
|
-
expect.objectContaining({
|
|
521
|
-
accessToken: "access-token-1",
|
|
522
|
-
organisationUuid: "org-1",
|
|
523
|
-
workspaceUuid: "ws-1",
|
|
524
|
-
}),
|
|
525
|
-
);
|
|
526
|
-
await waitFor(() => expect(onComplete).toHaveBeenCalledTimes(1));
|
|
527
|
-
});
|
|
528
|
-
|
|
529
|
-
it("locks the notice selections once Confirm is pressed", async () => {
|
|
530
|
-
await renderLoaded();
|
|
531
|
-
// Editable before Confirm.
|
|
532
|
-
const accountCheckbox = screen.getByRole("checkbox", {
|
|
533
|
-
name: /Select all data elements for Account Management/,
|
|
534
|
-
});
|
|
535
|
-
expect(accountCheckbox).toBeEnabled();
|
|
536
|
-
selectRequired();
|
|
537
|
-
fireEvent.click(screen.getByRole("button", { name: "Agree" }));
|
|
538
|
-
// Locked after Confirm; the notice is still visible.
|
|
539
|
-
expect(accountCheckbox).toBeDisabled();
|
|
540
|
-
expect(screen.getByText("Account Management")).toBeInTheDocument();
|
|
541
|
-
});
|
|
542
|
-
|
|
543
|
-
it("reopens the notice for editing via Edit selections", async () => {
|
|
544
|
-
await renderLoaded();
|
|
545
|
-
selectRequired();
|
|
546
|
-
fireEvent.click(screen.getByRole("button", { name: "Agree" }));
|
|
547
|
-
expect(screen.getByLabelText("Mobile number")).toBeInTheDocument();
|
|
548
|
-
fireEvent.click(screen.getByRole("button", { name: "Edit selections" }));
|
|
549
|
-
// Back on the notice step: panel gone, CTA reverts, selections editable.
|
|
550
|
-
expect(screen.queryByLabelText("Mobile number")).not.toBeInTheDocument();
|
|
551
|
-
expect(screen.getByRole("button", { name: "Agree" })).toBeInTheDocument();
|
|
552
|
-
expect(
|
|
553
|
-
screen.getByRole("checkbox", {
|
|
554
|
-
name: /Select all data elements for Account Management/,
|
|
555
|
-
}),
|
|
556
|
-
).toBeEnabled();
|
|
557
|
-
});
|
|
558
|
-
|
|
559
|
-
it("returns to the mobile field via Change Mobile Number", async () => {
|
|
560
|
-
await renderLoaded();
|
|
561
|
-
selectRequired();
|
|
562
|
-
fireEvent.click(screen.getByRole("button", { name: "Agree" }));
|
|
563
|
-
fireEvent.change(screen.getByLabelText("Mobile number"), {
|
|
564
|
-
target: { value: "9876543210" },
|
|
565
|
-
});
|
|
566
|
-
fireEvent.click(screen.getByRole("button", { name: "Send OTP" }));
|
|
567
|
-
await screen.findByText(/OTP sent to/);
|
|
568
|
-
typeOtp("12");
|
|
569
|
-
fireEvent.click(
|
|
570
|
-
screen.getByRole("button", { name: "Change Mobile Number" }),
|
|
571
|
-
);
|
|
572
|
-
expect(screen.queryByLabelText(/OTP digit/)).not.toBeInTheDocument();
|
|
573
|
-
expect(screen.getByLabelText("Mobile number")).toBeEnabled();
|
|
574
|
-
expect(
|
|
575
|
-
screen.getByRole("button", { name: "Send OTP" }),
|
|
576
|
-
).toBeInTheDocument();
|
|
577
|
-
});
|
|
578
|
-
|
|
579
|
-
it("shows a localized inline error when create-OTP fails", async () => {
|
|
580
|
-
mockedCreateOtp.mockRejectedValue(new Error("SMS service unavailable"));
|
|
581
|
-
await renderLoaded();
|
|
582
|
-
selectRequired();
|
|
583
|
-
fireEvent.click(screen.getByRole("button", { name: "Agree" }));
|
|
584
|
-
fireEvent.change(screen.getByLabelText("Mobile number"), {
|
|
585
|
-
target: { value: "9876543210" },
|
|
586
|
-
});
|
|
587
|
-
fireEvent.click(screen.getByRole("button", { name: "Send OTP" }));
|
|
588
|
-
// Server copy is English-only, so the SDK shows a localized generic message.
|
|
589
|
-
expect(
|
|
590
|
-
await screen.findByText("Could not send the OTP. Please try again."),
|
|
591
|
-
).toBeInTheDocument();
|
|
592
|
-
// Stays on the mobile-entry step (no OTP boxes).
|
|
593
|
-
expect(screen.queryByLabelText(/OTP digit/)).not.toBeInTheDocument();
|
|
594
|
-
});
|
|
595
|
-
|
|
596
|
-
it("treats an already-consented 409 as success (no error)", async () => {
|
|
597
|
-
mockedSubmitConsent.mockRejectedValue(
|
|
598
|
-
new ApiError({
|
|
599
|
-
status: 409,
|
|
600
|
-
code: "CONSENT_ALREADY_PROVIDED",
|
|
601
|
-
message: "You have already provided consent for this notice.",
|
|
602
|
-
}),
|
|
603
|
-
);
|
|
604
|
-
const onComplete = vi.fn();
|
|
605
|
-
render(<RedactoNoticeAssisted {...PROPS} onComplete={onComplete} />);
|
|
606
|
-
await screen.findByRole("heading", { name: "EA Sports" });
|
|
607
|
-
|
|
608
|
-
selectRequired();
|
|
609
|
-
fireEvent.click(screen.getByRole("button", { name: "Agree" }));
|
|
610
|
-
fireEvent.change(screen.getByLabelText("Mobile number"), {
|
|
611
|
-
target: { value: "9876543210" },
|
|
612
|
-
});
|
|
613
|
-
fireEvent.click(screen.getByRole("button", { name: "Send OTP" }));
|
|
614
|
-
await screen.findByText(/OTP sent to/);
|
|
615
|
-
typeOtp("1".repeat(otpLength));
|
|
616
|
-
fireEvent.click(screen.getByRole("button", { name: "Confirm OTP" }));
|
|
617
|
-
|
|
618
|
-
// Treated as success: onComplete fires and the 409 message is never shown.
|
|
619
|
-
await waitFor(() => expect(onComplete).toHaveBeenCalledTimes(1));
|
|
620
|
-
expect(
|
|
621
|
-
screen.queryByText("You have already provided consent for this notice."),
|
|
622
|
-
).not.toBeInTheDocument();
|
|
623
|
-
});
|
|
624
|
-
|
|
625
|
-
// ── Go ledger (ledgerBaseUrl) ─────────────────────────────────────────────
|
|
626
|
-
// Drives the full notice → OTP → Confirm flow with a notice that carries a
|
|
627
|
-
// notice_uuid (so the check-consent read runs), and returns the onComplete spy.
|
|
628
|
-
const runConfirmFlow = async (extraProps: Record<string, unknown>) => {
|
|
629
|
-
mockedFetch.mockResolvedValue(NOTICE_WITH_AUDIO);
|
|
630
|
-
const onComplete = vi.fn();
|
|
631
|
-
render(
|
|
632
|
-
<RedactoNoticeAssisted {...PROPS} onComplete={onComplete} {...extraProps} />,
|
|
633
|
-
);
|
|
634
|
-
await screen.findByRole("heading", { name: "EA Sports" });
|
|
635
|
-
selectRequired();
|
|
636
|
-
fireEvent.click(screen.getByRole("button", { name: "Agree" }));
|
|
637
|
-
fireEvent.change(screen.getByLabelText("Mobile number"), {
|
|
638
|
-
target: { value: "9876543210" },
|
|
639
|
-
});
|
|
640
|
-
fireEvent.click(screen.getByRole("button", { name: "Send OTP" }));
|
|
641
|
-
await screen.findByText(/OTP sent to/);
|
|
642
|
-
typeOtp("1".repeat(otpLength));
|
|
643
|
-
fireEvent.click(screen.getByRole("button", { name: "Confirm OTP" }));
|
|
644
|
-
return onComplete;
|
|
645
|
-
};
|
|
646
|
-
|
|
647
|
-
it("routes the write through ledgerBaseUrl and submits when not already consented", async () => {
|
|
648
|
-
const onComplete = await runConfirmFlow({ ledgerBaseUrl: LEDGER_BASE_URL });
|
|
649
|
-
|
|
650
|
-
await waitFor(() => expect(mockedSubmitConsent).toHaveBeenCalledTimes(1));
|
|
651
|
-
// The check-consent read runs with props org/workspace + the OTP token.
|
|
652
|
-
expect(mockedFetchScope).toHaveBeenCalledWith(
|
|
653
|
-
expect.objectContaining({
|
|
654
|
-
accessToken: "access-token-1",
|
|
655
|
-
ledgerBaseUrl: LEDGER_BASE_URL,
|
|
656
|
-
organisationUuid: "org-1",
|
|
657
|
-
workspaceUuid: "ws-1",
|
|
658
|
-
noticeUuid: "ntc-1",
|
|
659
|
-
}),
|
|
660
|
-
);
|
|
661
|
-
// The submit carries ledgerBaseUrl so the api layer routes it to the ledger.
|
|
662
|
-
expect(mockedSubmitConsent).toHaveBeenCalledWith(
|
|
663
|
-
expect.objectContaining({ ledgerBaseUrl: LEDGER_BASE_URL }),
|
|
664
|
-
);
|
|
665
|
-
await waitFor(() => expect(onComplete).toHaveBeenCalledTimes(1));
|
|
666
|
-
});
|
|
667
|
-
|
|
668
|
-
it("short-circuits an already fully-consented principal via the ledger (no submit)", async () => {
|
|
669
|
-
mockedFetchScope.mockResolvedValue({
|
|
670
|
-
consented: true,
|
|
671
|
-
all_mandatory_active: true,
|
|
672
|
-
purposes: [],
|
|
673
|
-
});
|
|
674
|
-
const onComplete = await runConfirmFlow({ ledgerBaseUrl: LEDGER_BASE_URL });
|
|
675
|
-
|
|
676
|
-
await waitFor(() => expect(onComplete).toHaveBeenCalledTimes(1));
|
|
677
|
-
expect(mockedSubmitConsent).not.toHaveBeenCalled();
|
|
678
|
-
});
|
|
679
|
-
|
|
680
|
-
it("skips the ledger check-consent and stays on baseUrl when ledgerBaseUrl is absent", async () => {
|
|
681
|
-
const onComplete = await runConfirmFlow({});
|
|
682
|
-
|
|
683
|
-
await waitFor(() => expect(mockedSubmitConsent).toHaveBeenCalledTimes(1));
|
|
684
|
-
expect(mockedFetchScope).not.toHaveBeenCalled();
|
|
685
|
-
expect(mockedSubmitConsent).toHaveBeenCalledWith(
|
|
686
|
-
expect.objectContaining({
|
|
687
|
-
ledgerBaseUrl: undefined,
|
|
688
|
-
baseUrl: "https://api.example.test/consent",
|
|
689
|
-
}),
|
|
690
|
-
);
|
|
691
|
-
await waitFor(() => expect(onComplete).toHaveBeenCalledTimes(1));
|
|
692
|
-
});
|
|
693
|
-
|
|
694
|
-
// The verify endpoint returns English copy with no error_code, so the SDK
|
|
695
|
-
// localizes by HTTP status + the `should_request_new` flag instead of echoing
|
|
696
|
-
// the server message.
|
|
697
|
-
const submitVerifyAttempt = async () => {
|
|
698
|
-
await renderLoaded();
|
|
699
|
-
selectRequired();
|
|
700
|
-
fireEvent.click(screen.getByRole("button", { name: "Agree" }));
|
|
701
|
-
fireEvent.change(screen.getByLabelText("Mobile number"), {
|
|
702
|
-
target: { value: "9876543210" },
|
|
703
|
-
});
|
|
704
|
-
fireEvent.click(screen.getByRole("button", { name: "Send OTP" }));
|
|
705
|
-
await screen.findByText(/OTP sent to/);
|
|
706
|
-
typeOtp("1".repeat(otpLength));
|
|
707
|
-
fireEvent.click(screen.getByRole("button", { name: "Confirm OTP" }));
|
|
708
|
-
};
|
|
709
|
-
|
|
710
|
-
it("shows a localized 'incorrect code' error on a 400 verify failure", async () => {
|
|
711
|
-
mockedVerifyOtp.mockRejectedValue(
|
|
712
|
-
new ApiError({ status: 400, message: "incorrect (server copy)" }),
|
|
713
|
-
);
|
|
714
|
-
await submitVerifyAttempt();
|
|
715
|
-
expect(
|
|
716
|
-
await screen.findByText(
|
|
717
|
-
"The verification code you entered is incorrect. Please check and try again.",
|
|
718
|
-
),
|
|
719
|
-
).toBeInTheDocument();
|
|
720
|
-
// Stays on the OTP step; consent is never submitted.
|
|
721
|
-
expect(mockedSubmitConsent).not.toHaveBeenCalled();
|
|
722
|
-
});
|
|
723
|
-
|
|
724
|
-
it("shows a localized 'expired code' error on 400 with should_request_new", async () => {
|
|
725
|
-
mockedVerifyOtp.mockRejectedValue(
|
|
726
|
-
new ApiError({
|
|
727
|
-
status: 400,
|
|
728
|
-
message: "expired (server copy)",
|
|
729
|
-
shouldRequestNew: true,
|
|
730
|
-
}),
|
|
731
|
-
);
|
|
732
|
-
await submitVerifyAttempt();
|
|
733
|
-
expect(
|
|
734
|
-
await screen.findByText(
|
|
735
|
-
"Your verification code has expired. Please request a new code.",
|
|
736
|
-
),
|
|
737
|
-
).toBeInTheDocument();
|
|
738
|
-
expect(mockedSubmitConsent).not.toHaveBeenCalled();
|
|
739
|
-
});
|
|
740
|
-
|
|
741
|
-
it("shows a localized 'not found' error on a 404 verify failure", async () => {
|
|
742
|
-
mockedVerifyOtp.mockRejectedValue(
|
|
743
|
-
new ApiError({ status: 404, message: "not found (server copy)" }),
|
|
744
|
-
);
|
|
745
|
-
await submitVerifyAttempt();
|
|
746
|
-
expect(
|
|
747
|
-
await screen.findByText(
|
|
748
|
-
"Verification code not found. Please request a new code.",
|
|
749
|
-
),
|
|
750
|
-
).toBeInTheDocument();
|
|
751
|
-
expect(mockedSubmitConsent).not.toHaveBeenCalled();
|
|
752
|
-
});
|
|
753
|
-
|
|
754
|
-
// ── Audio narration (talkback) ────────────────────────────────────────────
|
|
755
|
-
describe("audio narration", () => {
|
|
756
|
-
// jsdom doesn't implement media playback — stub play/pause so the player runs.
|
|
757
|
-
beforeEach(() => {
|
|
758
|
-
window.HTMLMediaElement.prototype.play = vi
|
|
759
|
-
.fn()
|
|
760
|
-
.mockResolvedValue(undefined);
|
|
761
|
-
window.HTMLMediaElement.prototype.pause = vi.fn();
|
|
762
|
-
});
|
|
763
|
-
|
|
764
|
-
const renderWithAudio = async () => {
|
|
765
|
-
mockedFetch.mockResolvedValue(NOTICE_WITH_AUDIO);
|
|
766
|
-
const result = render(<RedactoNoticeAssisted {...PROPS} />);
|
|
767
|
-
await screen.findByRole("heading", { name: "EA Sports" });
|
|
768
|
-
return result;
|
|
769
|
-
};
|
|
770
|
-
|
|
771
|
-
it("does not fetch audio or show the speaker without a notice_uuid", async () => {
|
|
772
|
-
// The base NOTICE has no notice_uuid → the audio effect returns early.
|
|
773
|
-
await renderLoaded();
|
|
774
|
-
expect(mockedFetchTTS).not.toHaveBeenCalled();
|
|
775
|
-
expect(
|
|
776
|
-
screen.queryByRole("button", { name: "Start audio playback" }),
|
|
777
|
-
).not.toBeInTheDocument();
|
|
778
|
-
});
|
|
779
|
-
|
|
780
|
-
it("fetches audio with the public props + language and shows the speaker", async () => {
|
|
781
|
-
await renderWithAudio();
|
|
782
|
-
await waitFor(() =>
|
|
783
|
-
expect(mockedFetchTTS).toHaveBeenCalledWith(
|
|
784
|
-
expect.objectContaining({
|
|
785
|
-
organisationUuid: "org-1",
|
|
786
|
-
workspaceUuid: "ws-1",
|
|
787
|
-
noticeUuid: "ntc-1",
|
|
788
|
-
language: "English",
|
|
789
|
-
baseUrl: "https://api.example.test/consent",
|
|
790
|
-
}),
|
|
791
|
-
),
|
|
792
|
-
);
|
|
793
|
-
expect(
|
|
794
|
-
await screen.findByRole("button", { name: "Start audio playback" }),
|
|
795
|
-
).toBeInTheDocument();
|
|
796
|
-
});
|
|
797
|
-
|
|
798
|
-
it("toggles play/pause when the speaker is clicked", async () => {
|
|
799
|
-
await renderWithAudio();
|
|
800
|
-
const speaker = await screen.findByRole("button", {
|
|
801
|
-
name: "Start audio playback",
|
|
802
|
-
});
|
|
803
|
-
fireEvent.click(speaker);
|
|
804
|
-
// Now playing → the same control flips to a pause affordance.
|
|
805
|
-
expect(
|
|
806
|
-
await screen.findByRole("button", { name: "Pause audio playback" }),
|
|
807
|
-
).toBeInTheDocument();
|
|
808
|
-
fireEvent.click(
|
|
809
|
-
screen.getByRole("button", { name: "Pause audio playback" }),
|
|
810
|
-
);
|
|
811
|
-
expect(
|
|
812
|
-
screen.getByRole("button", { name: "Start audio playback" }),
|
|
813
|
-
).toBeInTheDocument();
|
|
814
|
-
});
|
|
815
|
-
|
|
816
|
-
it("hides the speaker once the notice advances to verify", async () => {
|
|
817
|
-
await renderWithAudio();
|
|
818
|
-
await screen.findByRole("button", { name: "Start audio playback" });
|
|
819
|
-
selectRequired();
|
|
820
|
-
fireEvent.click(screen.getByRole("button", { name: "Agree" }));
|
|
821
|
-
// Narration is notice-only — the speaker is gone on the verify step.
|
|
822
|
-
expect(
|
|
823
|
-
screen.queryByRole("button", { name: "Start audio playback" }),
|
|
824
|
-
).not.toBeInTheDocument();
|
|
825
|
-
expect(
|
|
826
|
-
screen.queryByRole("button", { name: "Pause audio playback" }),
|
|
827
|
-
).not.toBeInTheDocument();
|
|
828
|
-
});
|
|
829
|
-
|
|
830
|
-
it("refetches audio in the newly selected language", async () => {
|
|
831
|
-
await renderWithAudio();
|
|
832
|
-
await waitFor(() => expect(mockedFetchTTS).toHaveBeenCalled());
|
|
833
|
-
mockedFetchTTS.mockClear();
|
|
834
|
-
fireEvent.click(screen.getByRole("button", { name: /Select language/ }));
|
|
835
|
-
fireEvent.click(
|
|
836
|
-
within(screen.getByRole("listbox")).getByRole("option", {
|
|
837
|
-
name: /Hindi/,
|
|
838
|
-
}),
|
|
839
|
-
);
|
|
840
|
-
await waitFor(() =>
|
|
841
|
-
expect(mockedFetchTTS).toHaveBeenCalledWith(
|
|
842
|
-
expect.objectContaining({ language: "Hindi" }),
|
|
843
|
-
),
|
|
844
|
-
);
|
|
845
|
-
});
|
|
846
|
-
});
|
|
847
|
-
|
|
848
|
-
it("fires onDecline from the notice screen", async () => {
|
|
849
|
-
const onDecline = vi.fn();
|
|
850
|
-
render(<RedactoNoticeAssisted {...PROPS} onDecline={onDecline} />);
|
|
851
|
-
await screen.findByRole("heading", { name: "EA Sports" });
|
|
852
|
-
fireEvent.click(screen.getByRole("button", { name: "Decline" }));
|
|
853
|
-
expect(onDecline).toHaveBeenCalledTimes(1);
|
|
854
|
-
});
|
|
855
|
-
|
|
856
|
-
it("shows a spinner while loading", () => {
|
|
857
|
-
mockedFetch.mockReturnValue(new Promise(() => {}));
|
|
858
|
-
render(<RedactoNoticeAssisted {...PROPS} />);
|
|
859
|
-
expect(screen.getByLabelText("Loading notice")).toBeInTheDocument();
|
|
860
|
-
});
|
|
861
|
-
|
|
862
|
-
it("shows an error state when the fetch fails", async () => {
|
|
863
|
-
mockedFetch.mockRejectedValue(new Error("Notice not found"));
|
|
864
|
-
render(<RedactoNoticeAssisted {...PROPS} />);
|
|
865
|
-
await waitFor(() =>
|
|
866
|
-
expect(
|
|
867
|
-
screen.getByText(/Couldn.t load the consent notice/),
|
|
868
|
-
).toBeInTheDocument(),
|
|
869
|
-
);
|
|
870
|
-
expect(screen.getByText("Notice not found")).toBeInTheDocument();
|
|
871
|
-
});
|
|
872
|
-
|
|
873
|
-
it("localizes the static OTP-flow strings when the language is switched", async () => {
|
|
874
|
-
await renderLoaded();
|
|
875
|
-
// Select required elements while labels are still English, then switch the
|
|
876
|
-
// in-notice picker to Hindi.
|
|
877
|
-
selectRequired();
|
|
878
|
-
fireEvent.click(screen.getByRole("button", { name: /Select language/ }));
|
|
879
|
-
fireEvent.click(
|
|
880
|
-
within(screen.getByRole("listbox")).getByRole("option", {
|
|
881
|
-
name: /Hindi/,
|
|
882
|
-
}),
|
|
883
|
-
);
|
|
884
|
-
|
|
885
|
-
// Agree label has no Hindi API translation → falls back to English "Agree".
|
|
886
|
-
fireEvent.click(screen.getByRole("button", { name: "Agree" }));
|
|
887
|
-
|
|
888
|
-
// Static strings now render in Hindi (from the i18n dictionary).
|
|
889
|
-
expect(screen.getByText("मोबाइल नंबर")).toBeInTheDocument();
|
|
890
|
-
// CTA now reads the localized "Send OTP".
|
|
891
|
-
expect(
|
|
892
|
-
screen.getByRole("button", { name: "ओटीपी (OTP) भेजें" }),
|
|
893
|
-
).toBeInTheDocument();
|
|
894
|
-
// The inline verify panel heading is localized too.
|
|
895
|
-
expect(
|
|
896
|
-
screen.getByText("ओटीपी (OTP) से सत्यापित करें"),
|
|
897
|
-
).toBeInTheDocument();
|
|
898
|
-
// English static text is gone.
|
|
899
|
-
expect(screen.queryByText("Mobile number")).not.toBeInTheDocument();
|
|
900
|
-
});
|
|
901
|
-
});
|
|
902
|
-
|
|
903
|
-
describe("makeStaticT", () => {
|
|
904
|
-
it("returns the translation for a known language and interpolates vars", () => {
|
|
905
|
-
const t = makeStaticT("hi");
|
|
906
|
-
expect(t("getOtp")).toBe("ओटीपी (OTP) प्राप्त करें");
|
|
907
|
-
expect(t("enterNDigitOtp", { count: 6 })).toBe(
|
|
908
|
-
"6-अंकों का ओटीपी (OTP) दर्ज करें",
|
|
909
|
-
);
|
|
910
|
-
});
|
|
911
|
-
|
|
912
|
-
it("falls back to English for an unknown language code", () => {
|
|
913
|
-
const t = makeStaticT("zz");
|
|
914
|
-
expect(t("getOtp")).toBe("Get OTP");
|
|
915
|
-
expect(t("otpDigit", { index: 3 })).toBe("OTP digit 3");
|
|
916
|
-
});
|
|
917
|
-
|
|
918
|
-
it("resolves the single-screen CTA + panel keys", () => {
|
|
919
|
-
const en = makeStaticT("en");
|
|
920
|
-
expect(en("sendOtp")).toBe("Send OTP");
|
|
921
|
-
expect(en("confirmOtp")).toBe("Confirm OTP");
|
|
922
|
-
expect(en("verifyWithOtp")).toBe("Verify with OTP");
|
|
923
|
-
expect(en("editSelections")).toBe("Edit selections");
|
|
924
|
-
// Localized (Hindi) — must not fall back to English.
|
|
925
|
-
const hi = makeStaticT("hi");
|
|
926
|
-
expect(hi("sendOtp")).toBe("ओटीपी (OTP) भेजें");
|
|
927
|
-
expect(hi("confirmOtp")).toBe("ओटीपी (OTP) की पुष्टि करें");
|
|
928
|
-
});
|
|
929
|
-
|
|
930
|
-
it("leaves unmatched placeholders untouched", () => {
|
|
931
|
-
const t = makeStaticT("en");
|
|
932
|
-
expect(t("enterNDigitOtp")).toBe("Enter {count}-digit OTP");
|
|
933
|
-
});
|
|
934
|
-
});
|