@redacto.io/consent-sdk-react 9.2.0-beta.1 → 9.2.0-beta.2
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/CHANGELOG.md +6 -0
- package/dist/privacy-center.d.mts +6 -10
- package/dist/privacy-center.d.ts +6 -10
- package/dist/privacy-center.js +208 -850
- package/dist/privacy-center.mjs +208 -850
- package/package.json +1 -1
- package/src/RedactoPrivacyCenter/components/DataRequest/ProductSelectionSection.tsx +4 -4
- package/src/RedactoPrivacyCenter/components/Form.tsx +214 -866
- package/src/RedactoPrivacyCenter/components/RequestSubmit.tsx +44 -219
- package/src/RedactoPrivacyCenter/components/types.ts +14 -20
- package/src/RedactoPrivacyCenter/lib/api.ts +23 -58
- package/src/RedactoPrivacyCenter/lib/types.ts +0 -31
- package/src/RedactoPrivacyCenter/locales/as/translation.json +2 -2
- package/src/RedactoPrivacyCenter/locales/bho/translation.json +2 -2
- package/src/RedactoPrivacyCenter/locales/bn/translation.json +2 -2
- package/src/RedactoPrivacyCenter/locales/brx/translation.json +2 -2
- package/src/RedactoPrivacyCenter/locales/doi/translation.json +2 -2
- package/src/RedactoPrivacyCenter/locales/en/translation.json +2 -2
- package/src/RedactoPrivacyCenter/locales/gom/translation.json +2 -2
- package/src/RedactoPrivacyCenter/locales/gu/translation.json +2 -2
- package/src/RedactoPrivacyCenter/locales/hi/translation.json +2 -2
- package/src/RedactoPrivacyCenter/locales/kn/translation.json +2 -2
- package/src/RedactoPrivacyCenter/locales/ks/translation.json +2 -2
- package/src/RedactoPrivacyCenter/locales/mai/translation.json +2 -2
- package/src/RedactoPrivacyCenter/locales/ml/translation.json +2 -2
- package/src/RedactoPrivacyCenter/locales/mni-Mtei/translation.json +2 -2
- package/src/RedactoPrivacyCenter/locales/mr/translation.json +2 -2
- package/src/RedactoPrivacyCenter/locales/ne/translation.json +2 -2
- package/src/RedactoPrivacyCenter/locales/or/translation.json +2 -2
- package/src/RedactoPrivacyCenter/locales/pa/translation.json +2 -2
- package/src/RedactoPrivacyCenter/locales/sa/translation.json +2 -2
- package/src/RedactoPrivacyCenter/locales/sat/translation.json +2 -2
- package/src/RedactoPrivacyCenter/locales/sd/translation.json +2 -2
- package/src/RedactoPrivacyCenter/locales/ta/translation.json +2 -2
- package/src/RedactoPrivacyCenter/locales/te/translation.json +2 -2
- package/src/RedactoPrivacyCenter/locales/ur/translation.json +2 -2
- package/tests/Form.test.tsx +123 -149
- package/tests/ProductSelectionSection.test.tsx +39 -4
- package/tests/RequestSubmit.test.tsx +27 -187
|
@@ -314,8 +314,8 @@
|
|
|
314
314
|
"selectProductsForRequestGeneric": "Select one or more products to continue",
|
|
315
315
|
"yourData": "Your data",
|
|
316
316
|
"actingOnBehalfOf": "Acting on behalf of {{nominator}}",
|
|
317
|
-
"activePurposes_one": "{{count}}
|
|
318
|
-
"activePurposes_other": "{{count}}
|
|
317
|
+
"activePurposes_one": "{{count}} उद्देश",
|
|
318
|
+
"activePurposes_other": "{{count}} उद्देश",
|
|
319
319
|
"autoSelected": "Auto-selected",
|
|
320
320
|
"noProductsAvailable": "No products available",
|
|
321
321
|
"noProductsAvailableDescription": "You have no consents to manage yet.",
|
|
@@ -314,8 +314,8 @@
|
|
|
314
314
|
"selectProductsForRequestGeneric": "Select one or more products to continue",
|
|
315
315
|
"yourData": "Your data",
|
|
316
316
|
"actingOnBehalfOf": "Acting on behalf of {{nominator}}",
|
|
317
|
-
"activePurposes_one": "{{count}}
|
|
318
|
-
"activePurposes_other": "{{count}}
|
|
317
|
+
"activePurposes_one": "{{count}} उद्देश्य",
|
|
318
|
+
"activePurposes_other": "{{count}} उद्देश्य",
|
|
319
319
|
"autoSelected": "Auto-selected",
|
|
320
320
|
"noProductsAvailable": "No products available",
|
|
321
321
|
"noProductsAvailableDescription": "You have no consents to manage yet.",
|
|
@@ -314,8 +314,8 @@
|
|
|
314
314
|
"selectProductsForRequestGeneric": "Select one or more products to continue",
|
|
315
315
|
"yourData": "Your data",
|
|
316
316
|
"actingOnBehalfOf": "Acting on behalf of {{nominator}}",
|
|
317
|
-
"activePurposes_one": "{{count}}
|
|
318
|
-
"activePurposes_other": "{{count}}
|
|
317
|
+
"activePurposes_one": "{{count}} ଉଦ୍ଦେଶ୍ୟ",
|
|
318
|
+
"activePurposes_other": "{{count}} ଉଦ୍ଦେଶ୍ୟଗୁଡ଼ିକ",
|
|
319
319
|
"autoSelected": "Auto-selected",
|
|
320
320
|
"noProductsAvailable": "No products available",
|
|
321
321
|
"noProductsAvailableDescription": "You have no consents to manage yet.",
|
|
@@ -314,8 +314,8 @@
|
|
|
314
314
|
"selectProductsForRequestGeneric": "Select one or more products to continue",
|
|
315
315
|
"yourData": "Your data",
|
|
316
316
|
"actingOnBehalfOf": "Acting on behalf of {{nominator}}",
|
|
317
|
-
"activePurposes_one": "{{count}}
|
|
318
|
-
"activePurposes_other": "{{count}}
|
|
317
|
+
"activePurposes_one": "{{count}} ਮਕਸਦ",
|
|
318
|
+
"activePurposes_other": "{{count}} ਮਕਸਦ",
|
|
319
319
|
"autoSelected": "Auto-selected",
|
|
320
320
|
"noProductsAvailable": "No products available",
|
|
321
321
|
"noProductsAvailableDescription": "You have no consents to manage yet.",
|
|
@@ -314,8 +314,8 @@
|
|
|
314
314
|
"selectProductsForRequestGeneric": "Select one or more products to continue",
|
|
315
315
|
"yourData": "Your data",
|
|
316
316
|
"actingOnBehalfOf": "Acting on behalf of {{nominator}}",
|
|
317
|
-
"activePurposes_one": "{{count}}
|
|
318
|
-
"activePurposes_other": "{{count}}
|
|
317
|
+
"activePurposes_one": "{{count}} प्रयोजनम्",
|
|
318
|
+
"activePurposes_other": "{{count}} प्रयोजनानि",
|
|
319
319
|
"autoSelected": "Auto-selected",
|
|
320
320
|
"noProductsAvailable": "No products available",
|
|
321
321
|
"noProductsAvailableDescription": "You have no consents to manage yet.",
|
|
@@ -314,8 +314,8 @@
|
|
|
314
314
|
"selectProductsForRequestGeneric": "Select one or more products to continue",
|
|
315
315
|
"yourData": "Your data",
|
|
316
316
|
"actingOnBehalfOf": "Acting on behalf of {{nominator}}",
|
|
317
|
-
"activePurposes_one": "{{count}}
|
|
318
|
-
"activePurposes_other": "{{count}}
|
|
317
|
+
"activePurposes_one": "{{count}} ᱯᱷᱮᱰᱟᱛ",
|
|
318
|
+
"activePurposes_other": "{{count}} ᱯᱷᱮᱰᱟᱛ",
|
|
319
319
|
"autoSelected": "Auto-selected",
|
|
320
320
|
"noProductsAvailable": "No products available",
|
|
321
321
|
"noProductsAvailableDescription": "You have no consents to manage yet.",
|
|
@@ -314,8 +314,8 @@
|
|
|
314
314
|
"selectProductsForRequestGeneric": "Select one or more products to continue",
|
|
315
315
|
"yourData": "Your data",
|
|
316
316
|
"actingOnBehalfOf": "Acting on behalf of {{nominator}}",
|
|
317
|
-
"activePurposes_one": "{{count}}
|
|
318
|
-
"activePurposes_other": "{{count}}
|
|
317
|
+
"activePurposes_one": "{{count}} مقصد",
|
|
318
|
+
"activePurposes_other": "{{count}} مقصد",
|
|
319
319
|
"autoSelected": "Auto-selected",
|
|
320
320
|
"noProductsAvailable": "No products available",
|
|
321
321
|
"noProductsAvailableDescription": "You have no consents to manage yet.",
|
|
@@ -314,8 +314,8 @@
|
|
|
314
314
|
"selectProductsForRequestGeneric": "Select one or more products to continue",
|
|
315
315
|
"yourData": "Your data",
|
|
316
316
|
"actingOnBehalfOf": "Acting on behalf of {{nominator}}",
|
|
317
|
-
"activePurposes_one": "{{count}}
|
|
318
|
-
"activePurposes_other": "{{count}}
|
|
317
|
+
"activePurposes_one": "{{count}} நோக்கம்",
|
|
318
|
+
"activePurposes_other": "{{count}} நோக்கங்கள்",
|
|
319
319
|
"autoSelected": "Auto-selected",
|
|
320
320
|
"noProductsAvailable": "No products available",
|
|
321
321
|
"noProductsAvailableDescription": "You have no consents to manage yet.",
|
|
@@ -314,8 +314,8 @@
|
|
|
314
314
|
"selectProductsForRequestGeneric": "Select one or more products to continue",
|
|
315
315
|
"yourData": "Your data",
|
|
316
316
|
"actingOnBehalfOf": "Acting on behalf of {{nominator}}",
|
|
317
|
-
"activePurposes_one": "{{count}}
|
|
318
|
-
"activePurposes_other": "{{count}}
|
|
317
|
+
"activePurposes_one": "{{count}} ఉద్దేశ్యం",
|
|
318
|
+
"activePurposes_other": "{{count}} ఉద్దేశ్యాలు",
|
|
319
319
|
"autoSelected": "Auto-selected",
|
|
320
320
|
"noProductsAvailable": "No products available",
|
|
321
321
|
"noProductsAvailableDescription": "You have no consents to manage yet.",
|
|
@@ -314,8 +314,8 @@
|
|
|
314
314
|
"selectProductsForRequestGeneric": "Select one or more products to continue",
|
|
315
315
|
"yourData": "Your data",
|
|
316
316
|
"actingOnBehalfOf": "Acting on behalf of {{nominator}}",
|
|
317
|
-
"activePurposes_one": "{{count}}
|
|
318
|
-
"activePurposes_other": "{{count}}
|
|
317
|
+
"activePurposes_one": "{{count}} مقصد",
|
|
318
|
+
"activePurposes_other": "{{count}} مقاصد",
|
|
319
319
|
"autoSelected": "Auto-selected",
|
|
320
320
|
"noProductsAvailable": "No products available",
|
|
321
321
|
"noProductsAvailableDescription": "You have no consents to manage yet.",
|
package/tests/Form.test.tsx
CHANGED
|
@@ -38,8 +38,8 @@ vi.mock('react-i18next', () => {
|
|
|
38
38
|
nominationChipSuffix: '· {{nominator}}',
|
|
39
39
|
yourData: 'Your data',
|
|
40
40
|
actingOnBehalfOf: 'Acting on behalf of {{nominator}}',
|
|
41
|
-
activePurposes_one: '{{count}}
|
|
42
|
-
activePurposes_other: '{{count}}
|
|
41
|
+
activePurposes_one: '{{count}} purpose',
|
|
42
|
+
activePurposes_other: '{{count}} purposes',
|
|
43
43
|
autoSelected: 'Auto-selected',
|
|
44
44
|
noProductsAvailable: 'No products available',
|
|
45
45
|
noProductsAvailableDescription: 'You have no consents to manage yet.',
|
|
@@ -95,16 +95,11 @@ vi.mock('../src/RedactoPrivacyCenter/lib/i18n', () => ({
|
|
|
95
95
|
}));
|
|
96
96
|
|
|
97
97
|
// ─── API stubs ───────────────────────────────────────────────────────────────
|
|
98
|
-
// `createCaseRequest`
|
|
99
|
-
// (batch path) are both shipped at runtime. We route them through one
|
|
100
|
-
// shared mock so existing tests can keep using `createCaseRequest.mock...`
|
|
101
|
-
// regardless of which code path the Form picks. Tests that need to assert
|
|
102
|
-
// throw-vs-null semantics override the relevant mock individually.
|
|
98
|
+
// 1-form -> 1-case: only `createCaseRequest` is consumed by Form.tsx now.
|
|
103
99
|
const createCaseRequest = vi.fn();
|
|
104
100
|
const getFormData = vi.fn();
|
|
105
101
|
vi.mock('../src/RedactoPrivacyCenter/lib/api', () => ({
|
|
106
102
|
createCaseRequest: (...args: unknown[]) => createCaseRequest(...args),
|
|
107
|
-
createCaseRequestUnsafe: (...args: unknown[]) => createCaseRequest(...args),
|
|
108
103
|
getFormData: (...args: unknown[]) => getFormData(...args),
|
|
109
104
|
}));
|
|
110
105
|
|
|
@@ -355,54 +350,45 @@ vi.mock(
|
|
|
355
350
|
vi.mock(
|
|
356
351
|
'../src/RedactoPrivacyCenter/components/DataRequest/NominationDataSection',
|
|
357
352
|
() => ({
|
|
358
|
-
|
|
353
|
+
// Test-only stub: exposes an email input that calls the real
|
|
354
|
+
// `setNominationData` setter, so tests can drive the form to a
|
|
355
|
+
// valid NOMINATION state and exercise the actual submit path
|
|
356
|
+
// (not just the "button disabled" gate).
|
|
357
|
+
default: ({ nominationData, setNominationData }: any) => (
|
|
358
|
+
<div data-testid="nomination-section">
|
|
359
|
+
<input
|
|
360
|
+
data-testid="nominee-email-input"
|
|
361
|
+
value={nominationData?.nominee_email ?? ''}
|
|
362
|
+
onChange={(e) =>
|
|
363
|
+
setNominationData((prev: any) => ({
|
|
364
|
+
...prev,
|
|
365
|
+
nominee_email: e.target.value,
|
|
366
|
+
}))
|
|
367
|
+
}
|
|
368
|
+
/>
|
|
369
|
+
</div>
|
|
370
|
+
),
|
|
359
371
|
}),
|
|
360
372
|
);
|
|
361
373
|
vi.mock('../src/RedactoPrivacyCenter/components/RequestSubmit', () => ({
|
|
362
374
|
default: ({
|
|
363
375
|
result,
|
|
364
|
-
onRetry,
|
|
365
376
|
}: {
|
|
366
377
|
result: {
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
group_key: string;
|
|
372
|
-
}>;
|
|
373
|
-
failed: Array<{
|
|
374
|
-
group_key: string;
|
|
375
|
-
product_uuid: string;
|
|
376
|
-
product_name: string;
|
|
377
|
-
error: string;
|
|
378
|
-
}>;
|
|
378
|
+
case_id: string;
|
|
379
|
+
uuid: string;
|
|
380
|
+
product_uuid: string;
|
|
381
|
+
product_name: string;
|
|
379
382
|
};
|
|
380
|
-
onRetry?: (groupKey: string) => void;
|
|
381
383
|
}) => (
|
|
382
384
|
<div data-testid="request-submitted">
|
|
383
|
-
<
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
</
|
|
390
|
-
<ul data-testid="request-submitted-failed">
|
|
391
|
-
{result.failed.map((f) => (
|
|
392
|
-
<li key={f.group_key} data-group-key={f.group_key}>
|
|
393
|
-
{f.product_name}
|
|
394
|
-
{onRetry !== undefined && (
|
|
395
|
-
<button
|
|
396
|
-
type="button"
|
|
397
|
-
onClick={() => onRetry(f.group_key)}
|
|
398
|
-
aria-label={`retry-${f.group_key}`}
|
|
399
|
-
>
|
|
400
|
-
Retry
|
|
401
|
-
</button>
|
|
402
|
-
)}
|
|
403
|
-
</li>
|
|
404
|
-
))}
|
|
405
|
-
</ul>
|
|
385
|
+
<span data-testid="request-submitted-case-id">{result.case_id}</span>
|
|
386
|
+
<span data-testid="request-submitted-product-uuid">
|
|
387
|
+
{result.product_uuid}
|
|
388
|
+
</span>
|
|
389
|
+
<span data-testid="request-submitted-product-name">
|
|
390
|
+
{result.product_name}
|
|
391
|
+
</span>
|
|
406
392
|
</div>
|
|
407
393
|
),
|
|
408
394
|
}));
|
|
@@ -497,40 +483,35 @@ describe('Form (P2-SDK-3)', () => {
|
|
|
497
483
|
sessionStorage.clear();
|
|
498
484
|
});
|
|
499
485
|
|
|
500
|
-
it('
|
|
486
|
+
it('blocks submit for product-scoped types when the DP has no consent groups', async () => {
|
|
487
|
+
// 1-form -> 1-case -> 1-product means product-scoped request types
|
|
488
|
+
// (ACCESS/ERASURE/CORRECTION) MUST have a product anchor. When the
|
|
489
|
+
// DP has zero consent groups (direct + nominated), the picker shows
|
|
490
|
+
// its empty state and submit must be hard-blocked at the validity
|
|
491
|
+
// gate so the case can never be created without an anchor.
|
|
501
492
|
setupSession();
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
name: 'Jane',
|
|
506
|
-
contact: 'jane@example.com',
|
|
507
|
-
grievance_options: [],
|
|
508
|
-
purposes: [{ purpose: basePurpose('purp-1', 'Marketing') }],
|
|
509
|
-
} as unknown as FormData;
|
|
510
|
-
getFormData.mockResolvedValue(legacyData);
|
|
511
|
-
|
|
512
|
-
// The Form emits a one-time console.warn when it falls back to the
|
|
513
|
-
// legacy render so customer developers notice their backend hasn't
|
|
514
|
-
// shipped Phase 1. Spy on it so we can both silence and assert.
|
|
515
|
-
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
493
|
+
getFormData.mockResolvedValue(
|
|
494
|
+
makeFormData({ direct_groups: [], nominated_groups: [] }),
|
|
495
|
+
);
|
|
516
496
|
|
|
517
497
|
render(<Form onCreateRequest={vi.fn()} />);
|
|
518
498
|
await waitFor(() => expect(getFormData).toHaveBeenCalled());
|
|
519
499
|
|
|
520
500
|
pickRequestType(RequestTypeEnum.ERASURE);
|
|
521
501
|
|
|
522
|
-
//
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
502
|
+
// Confirm checkbox alone is not enough — the validity gate has to
|
|
503
|
+
// see at least one product selected.
|
|
504
|
+
const confirmCheckbox = (
|
|
505
|
+
screen.getAllByRole('checkbox') as HTMLInputElement[]
|
|
506
|
+
).find((c) => c.closest('label')?.textContent?.includes('I confirm'));
|
|
507
|
+
expect(confirmCheckbox).toBeTruthy();
|
|
508
|
+
fireEvent.click(confirmCheckbox!);
|
|
529
509
|
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
510
|
+
const submitButton = screen.getByRole(
|
|
511
|
+
'button',
|
|
512
|
+
{ name: /^submit$/i },
|
|
513
|
+
) as HTMLButtonElement;
|
|
514
|
+
expect(submitButton.disabled).toBe(true);
|
|
534
515
|
});
|
|
535
516
|
|
|
536
517
|
it('renders the product step for DSAR types when direct_groups is present', async () => {
|
|
@@ -722,7 +703,11 @@ describe('Form (P2-SDK-3)', () => {
|
|
|
722
703
|
expect('data_principal_uuid' in payload).toBe(false);
|
|
723
704
|
expect(payload.uuid).toBe('form-session-uuid');
|
|
724
705
|
expect(payload.request_type).toBe('erasure');
|
|
725
|
-
|
|
706
|
+
// ERASURE submits must set this flag so the backend's
|
|
707
|
+
// auto-revoke task fires when the case is fulfilled (DPDPA Sec 12:
|
|
708
|
+
// erasing data implies revoking the consent that authorized
|
|
709
|
+
// processing it).
|
|
710
|
+
expect(payload.revoke_consent_on_fulfilment).toBe(true);
|
|
726
711
|
|
|
727
712
|
await waitFor(() => {
|
|
728
713
|
expect(screen.getByTestId('request-submitted')).toBeInTheDocument();
|
|
@@ -936,97 +921,78 @@ describe('Form (P2-SDK-3)', () => {
|
|
|
936
921
|
});
|
|
937
922
|
});
|
|
938
923
|
|
|
939
|
-
it('
|
|
940
|
-
//
|
|
941
|
-
//
|
|
942
|
-
//
|
|
943
|
-
//
|
|
944
|
-
//
|
|
924
|
+
it('submits NOMINATION as an org-level case without product_uuid or data_principal_uuid', async () => {
|
|
925
|
+
// NOMINATION is org-level — the picker is not rendered and the
|
|
926
|
+
// payload must omit `product_uuid` (backend rejects it on these
|
|
927
|
+
// types) AND `data_principal_uuid` (this is a direct grant, not a
|
|
928
|
+
// nominee acting on someone's behalf). Even when the contact has
|
|
929
|
+
// consent groups available, NOMINATION must not leak them onto
|
|
930
|
+
// the case anchor.
|
|
945
931
|
setupSession();
|
|
946
|
-
const
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
grievance_options: [],
|
|
951
|
-
purposes: [
|
|
952
|
-
{
|
|
953
|
-
purpose: basePurpose('purp-1', 'Marketing', [
|
|
954
|
-
baseDataElement('de-1', 'email'),
|
|
955
|
-
]),
|
|
956
|
-
},
|
|
957
|
-
],
|
|
958
|
-
} as unknown as FormData;
|
|
959
|
-
getFormData.mockResolvedValue(legacyData);
|
|
932
|
+
const direct = makeGroup();
|
|
933
|
+
getFormData.mockResolvedValue(
|
|
934
|
+
makeFormData({ direct_groups: [direct], nominated_groups: [] }),
|
|
935
|
+
);
|
|
960
936
|
createCaseRequest.mockResolvedValueOnce({
|
|
961
|
-
case_id: 'CASE-
|
|
962
|
-
uuid: 'uuid-
|
|
937
|
+
case_id: 'CASE-NOM',
|
|
938
|
+
uuid: 'uuid-nom',
|
|
963
939
|
});
|
|
964
940
|
|
|
965
|
-
// Silence the legacy-mode warning emitted by Form.tsx so test output
|
|
966
|
-
// stays focused on the assertions.
|
|
967
|
-
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
968
|
-
|
|
969
941
|
render(<Form onCreateRequest={vi.fn()} />);
|
|
970
942
|
await waitFor(() => expect(getFormData).toHaveBeenCalled());
|
|
971
943
|
|
|
972
|
-
pickRequestType(RequestTypeEnum.
|
|
944
|
+
pickRequestType(RequestTypeEnum.NOMINATION);
|
|
973
945
|
|
|
974
|
-
|
|
946
|
+
await waitFor(() => {
|
|
947
|
+
expect(screen.getByTestId('nomination-section')).toBeInTheDocument();
|
|
948
|
+
});
|
|
949
|
+
// Product picker must not render for NOMINATION even with groups.
|
|
975
950
|
expect(
|
|
976
951
|
screen.queryByText(/Select a product/i),
|
|
977
952
|
).not.toBeInTheDocument();
|
|
978
953
|
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
).
|
|
954
|
+
const submitButton = screen.getByRole(
|
|
955
|
+
'button',
|
|
956
|
+
{ name: /^submit$/i },
|
|
957
|
+
) as HTMLButtonElement;
|
|
958
|
+
// Sanity: gate is closed before a valid nominee email is provided.
|
|
959
|
+
expect(submitButton.disabled).toBe(true);
|
|
985
960
|
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
);
|
|
989
|
-
fireEvent.change(
|
|
990
|
-
within(correctionSection).getByLabelText('correction-current-email'),
|
|
991
|
-
{ target: { value: 'old' } },
|
|
992
|
-
);
|
|
993
|
-
fireEvent.change(
|
|
994
|
-
within(correctionSection).getByLabelText('correction-updated-email'),
|
|
995
|
-
{ target: { value: 'new' } },
|
|
996
|
-
);
|
|
961
|
+
// Drive the form to a valid NOMINATION state via the stubbed
|
|
962
|
+
// nomination input (see vi.mock above).
|
|
963
|
+
const emailInput = screen.getByTestId('nominee-email-input');
|
|
964
|
+
fireEvent.change(emailInput, { target: { value: 'nominee@example.com' } });
|
|
997
965
|
|
|
998
|
-
const
|
|
999
|
-
'checkbox'
|
|
1000
|
-
)
|
|
1001
|
-
const confirmCheckbox = allCheckboxes.find((c) =>
|
|
1002
|
-
c.closest('label')?.textContent?.includes('I confirm'),
|
|
1003
|
-
);
|
|
966
|
+
const confirmCheckbox = (
|
|
967
|
+
screen.getAllByRole('checkbox') as HTMLInputElement[]
|
|
968
|
+
).find((c) => c.closest('label')?.textContent?.includes('I confirm'));
|
|
1004
969
|
expect(confirmCheckbox).toBeTruthy();
|
|
1005
970
|
fireEvent.click(confirmCheckbox!);
|
|
1006
971
|
|
|
1007
|
-
const submitButton = screen.getByRole('button', { name: /^submit$/i });
|
|
1008
|
-
expect(submitButton).not.toBeDisabled();
|
|
1009
|
-
await act(async () => {
|
|
1010
|
-
fireEvent.click(submitButton);
|
|
1011
|
-
});
|
|
1012
|
-
|
|
1013
972
|
await waitFor(() => {
|
|
1014
|
-
|
|
973
|
+
const btn = screen.getByRole(
|
|
974
|
+
'button',
|
|
975
|
+
{ name: /^submit$/i },
|
|
976
|
+
) as HTMLButtonElement;
|
|
977
|
+
expect(btn.disabled).toBe(false);
|
|
1015
978
|
});
|
|
1016
979
|
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
expect(payload.request_details.correction_data).toEqual([
|
|
1023
|
-
{ name: 'email', curr_value: 'old', new_value: 'new' },
|
|
1024
|
-
]);
|
|
980
|
+
fireEvent.click(
|
|
981
|
+
screen.getByRole('button', { name: /^submit$/i }),
|
|
982
|
+
);
|
|
983
|
+
|
|
984
|
+
await waitFor(() => expect(createCaseRequest).toHaveBeenCalled());
|
|
1025
985
|
|
|
1026
|
-
|
|
986
|
+
const payload = createCaseRequest.mock.calls[0][2];
|
|
987
|
+
expect(payload.request_type).toBe('nomination');
|
|
988
|
+
expect(payload).not.toHaveProperty('product_uuid');
|
|
989
|
+
expect(payload).not.toHaveProperty('data_principal_uuid');
|
|
990
|
+
expect(payload.request_details.nomination_data).toMatchObject({
|
|
991
|
+
nominee_email: 'nominee@example.com',
|
|
992
|
+
});
|
|
1027
993
|
});
|
|
1028
994
|
|
|
1029
|
-
it('
|
|
995
|
+
it('sends data_principal_uuid and product anchors on a nominated-card submit', async () => {
|
|
1030
996
|
setupSession();
|
|
1031
997
|
const otherDirect = makeGroup({
|
|
1032
998
|
product_uuid: 'prod-other',
|
|
@@ -1087,16 +1053,24 @@ describe('Form (P2-SDK-3)', () => {
|
|
|
1087
1053
|
});
|
|
1088
1054
|
|
|
1089
1055
|
await waitFor(() => {
|
|
1090
|
-
expect(
|
|
1056
|
+
expect(createCaseRequest).toHaveBeenCalledTimes(1);
|
|
1091
1057
|
});
|
|
1092
1058
|
|
|
1093
|
-
const
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
expect(
|
|
1098
|
-
|
|
1099
|
-
)
|
|
1059
|
+
const payload = createCaseRequest.mock.calls[0][2];
|
|
1060
|
+
expect(payload.product_uuid).toBe('prod-cc');
|
|
1061
|
+
// Nominated cards send data_principal_uuid sourced from the
|
|
1062
|
+
// nominator's ConsentUser uuid — NOT from formData.uuid.
|
|
1063
|
+
expect(payload.data_principal_uuid).toBe('alice-uuid');
|
|
1064
|
+
|
|
1065
|
+
await waitFor(() => {
|
|
1066
|
+
expect(screen.getByTestId('request-submitted')).toBeInTheDocument();
|
|
1067
|
+
});
|
|
1068
|
+
expect(
|
|
1069
|
+
screen.getByTestId('request-submitted-product-uuid').textContent,
|
|
1070
|
+
).toBe('prod-cc');
|
|
1071
|
+
expect(
|
|
1072
|
+
screen.getByTestId('request-submitted-case-id').textContent,
|
|
1073
|
+
).toBe('CASE-NOMINATED');
|
|
1100
1074
|
});
|
|
1101
1075
|
|
|
1102
1076
|
it('disables submit when the selected CORRECTION product has incomplete entries', async () => {
|
|
@@ -3,8 +3,8 @@ import { describe, it, expect, vi } from 'vitest';
|
|
|
3
3
|
|
|
4
4
|
vi.mock('react-i18next', () => {
|
|
5
5
|
const en: Record<string, string> = {
|
|
6
|
-
activePurposes_one: '{{count}}
|
|
7
|
-
activePurposes_other: '{{count}}
|
|
6
|
+
activePurposes_one: '{{count}} purpose',
|
|
7
|
+
activePurposes_other: '{{count}} purposes',
|
|
8
8
|
noProductsAvailable: 'No products available',
|
|
9
9
|
noProductsAvailableDescription: 'You have no consents to manage yet.',
|
|
10
10
|
nominationChipSuffix: '· {{nominator}}',
|
|
@@ -96,7 +96,42 @@ describe('ProductSelectionSection', () => {
|
|
|
96
96
|
expect(screen.getByTestId('product-selection-chips')).toHaveClass(
|
|
97
97
|
'pc-product-select-chips',
|
|
98
98
|
);
|
|
99
|
-
expect(screen.getByText('2
|
|
99
|
+
expect(screen.getByText('2 purposes')).toBeInTheDocument();
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('chip count reflects total raisable purposes regardless of status', () => {
|
|
103
|
+
// Per TECH-4089, withdrawn purposes still appear in the picker (revoking
|
|
104
|
+
// consent is not deletion — DP must still be able to raise requests for
|
|
105
|
+
// them). The chip label is a plain count of raisable purposes — no
|
|
106
|
+
// active/withdrawn distinction is surfaced in the label.
|
|
107
|
+
const direct = makeGroup({
|
|
108
|
+
product_uuid: 'prod-mixed',
|
|
109
|
+
product_name: 'Acme Loyalty',
|
|
110
|
+
purposes: [
|
|
111
|
+
{
|
|
112
|
+
uuid: 'p-active',
|
|
113
|
+
data_elements: [{ uuid: 'de-a', selected: true }],
|
|
114
|
+
status: 'ACTIVE',
|
|
115
|
+
} as never,
|
|
116
|
+
{
|
|
117
|
+
uuid: 'p-withdrawn',
|
|
118
|
+
data_elements: [{ uuid: 'de-w', selected: false }],
|
|
119
|
+
status: 'WITHDRAW',
|
|
120
|
+
} as never,
|
|
121
|
+
],
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
render(
|
|
125
|
+
<ProductSelectionSection
|
|
126
|
+
directGroups={[direct]}
|
|
127
|
+
nominatedGroups={[]}
|
|
128
|
+
selectedProductKey={null}
|
|
129
|
+
onSelect={vi.fn()}
|
|
130
|
+
/>,
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
expect(screen.getByText('2 purposes')).toBeInTheDocument();
|
|
134
|
+
expect(screen.queryByText(/active/)).not.toBeInTheDocument();
|
|
100
135
|
});
|
|
101
136
|
|
|
102
137
|
it('marks the selected chip with aria-selected and selected style class', () => {
|
|
@@ -148,7 +183,7 @@ describe('ProductSelectionSection', () => {
|
|
|
148
183
|
|
|
149
184
|
expect(screen.getByText('Partner Bank')).toBeInTheDocument();
|
|
150
185
|
expect(screen.getByText('· Alice Johnson')).toBeInTheDocument();
|
|
151
|
-
expect(screen.getByText('1
|
|
186
|
+
expect(screen.getByText('1 purpose')).toBeInTheDocument();
|
|
152
187
|
});
|
|
153
188
|
|
|
154
189
|
it('falls back to nominator email in the chip suffix when name is null', () => {
|