@redacto.io/consent-sdk-react 0.0.1

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/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "@redacto.io/consent-sdk-react",
3
+ "version": "0.0.1",
4
+ "main": "dist/index.cjs",
5
+ "module": "dist/index.mjs",
6
+ "types": "dist/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "require": "./dist/index.cjs",
11
+ "import": "./dist/index.mjs"
12
+ }
13
+ },
14
+ "keywords": [],
15
+ "author": "",
16
+ "license": "Apache-2.0",
17
+ "description": "",
18
+ "publishConfig": {
19
+ "access": "public"
20
+ },
21
+ "dependencies": {
22
+ "jwt-decode": "^4.0.0",
23
+ "react": "^19.1.0",
24
+ "react-dom": "^19.1.0"
25
+ },
26
+ "devDependencies": {
27
+ "@changesets/cli": "^2.29.4",
28
+ "@testing-library/jest-dom": "^6.6.3",
29
+ "@testing-library/react": "^16.3.0",
30
+ "@types/react": "^19.1.3",
31
+ "@types/react-dom": "^19.1.3",
32
+ "@vitest/coverage-v8": "3.1.3",
33
+ "jsdom": "^26.1.0",
34
+ "tsup": "^8.4.0",
35
+ "typescript": "5.8.2",
36
+ "vitest": "^3.1.3"
37
+ },
38
+ "scripts": {
39
+ "build": "tsup",
40
+ "dev": "tsup --watch",
41
+ "test": "vitest",
42
+ "test:watch": "vitest watch",
43
+ "release": "pnpm run build && changeset publish",
44
+ "coverage": "vitest run --coverage"
45
+ }
46
+ }
@@ -0,0 +1,368 @@
1
+ import {
2
+ render,
3
+ screen,
4
+ fireEvent,
5
+ waitFor,
6
+ act,
7
+ } from "@testing-library/react";
8
+ import { vi, expect, it, describe, beforeEach, afterEach } from "vitest";
9
+ import { RedactoNoticeConsent } from "./RedactoNoticeConsent";
10
+ import * as api from "./api";
11
+ import type { ConsentContent } from "./api/types";
12
+ import { defaultProps, mockConsentContent } from "../../tests/mocks";
13
+ import "@testing-library/jest-dom";
14
+
15
+ // Mock the API functions
16
+ vi.mock("./api", () => ({
17
+ fetchConsentContent: vi.fn(),
18
+ submitConsentEvent: vi.fn(),
19
+ }));
20
+
21
+ // Mock the logo import correctly for ES modules
22
+ vi.mock("./assets/redacto-logo.png", () => ({
23
+ default: "mocked-logo.png",
24
+ }));
25
+
26
+ describe("RedactoNoticeConsent", () => {
27
+ const mockedApi = vi.mocked(api, { partial: true });
28
+ beforeEach(() => {
29
+ vi.clearAllMocks();
30
+ mockedApi.fetchConsentContent.mockResolvedValue(mockConsentContent);
31
+ mockedApi.submitConsentEvent.mockResolvedValue(undefined);
32
+ });
33
+
34
+ afterEach(() => {
35
+ vi.restoreAllMocks();
36
+ });
37
+
38
+ it("renders loading state initially", () => {
39
+ render(<RedactoNoticeConsent {...defaultProps} />);
40
+ expect(screen.getByText("Loading...")).toBeInTheDocument();
41
+ });
42
+
43
+ it("renders consent content after loading", async () => {
44
+ render(<RedactoNoticeConsent {...defaultProps} />);
45
+ await waitFor(
46
+ () => {
47
+ expect(screen.getByText("Your Privacy Matters")).toBeInTheDocument();
48
+ expect(
49
+ screen.getByText(/We care about your privacy/i)
50
+ ).toBeInTheDocument();
51
+ expect(
52
+ screen.getByRole("link", { name: /Privacy Policy/i })
53
+ ).toBeInTheDocument();
54
+ expect(
55
+ screen.getByRole("link", { name: /Vendors List/i })
56
+ ).toBeInTheDocument();
57
+ expect(screen.getByText("Manage What You Share")).toBeInTheDocument();
58
+ expect(screen.getByText("Marketing")).toBeInTheDocument();
59
+ },
60
+ { timeout: 3000 }
61
+ );
62
+ });
63
+
64
+ it("displays logo from content", async () => {
65
+ const { unmount } = render(<RedactoNoticeConsent {...defaultProps} />);
66
+ await waitFor(
67
+ () => {
68
+ const logo = screen.getByAltText("logo");
69
+ expect(logo).toHaveAttribute("src", "https://example.com/logo.png");
70
+ },
71
+ { timeout: 3000 }
72
+ );
73
+ unmount();
74
+ });
75
+
76
+ it("displays fallback logo when no content logo provided", async () => {
77
+ const noLogoContent: ConsentContent = {
78
+ ...mockConsentContent,
79
+ detail: {
80
+ ...mockConsentContent.detail,
81
+ active_config: {
82
+ ...mockConsentContent.detail.active_config,
83
+ logo_url: "",
84
+ },
85
+ },
86
+ };
87
+ mockedApi.fetchConsentContent.mockResolvedValueOnce(noLogoContent);
88
+ const { unmount } = render(<RedactoNoticeConsent {...defaultProps} />);
89
+ await waitFor(
90
+ () => {
91
+ const logo = screen.getByAltText("logo");
92
+ expect(logo).toHaveAttribute("src", "mocked-logo.png");
93
+ },
94
+ { timeout: 3000 }
95
+ );
96
+ unmount();
97
+ });
98
+
99
+ it("handles language selection", async () => {
100
+ render(<RedactoNoticeConsent {...defaultProps} />);
101
+ await waitFor(
102
+ () => {
103
+ expect(screen.getByRole("button", { name: "en" })).toBeInTheDocument();
104
+ },
105
+ { timeout: 3000 }
106
+ );
107
+
108
+ await act(async () => {
109
+ fireEvent.click(screen.getByRole("button", { name: "en" }));
110
+ });
111
+ await waitFor(
112
+ () => {
113
+ expect(screen.getByRole("option", { name: "es" })).toBeInTheDocument();
114
+ },
115
+ { timeout: 3000 }
116
+ );
117
+ await act(async () => {
118
+ fireEvent.click(screen.getByText("es"));
119
+ });
120
+
121
+ await waitFor(
122
+ () => {
123
+ expect(
124
+ screen.getByText(/Nos importa tu privacidad/i)
125
+ ).toBeInTheDocument();
126
+ expect(
127
+ screen.getByRole("link", { name: /Política de Privacidad/i })
128
+ ).toBeInTheDocument();
129
+ expect(
130
+ screen.getByText("Gestionar Consentimiento")
131
+ ).toBeInTheDocument();
132
+ },
133
+ { timeout: 3000 }
134
+ );
135
+ });
136
+
137
+ it("toggles purpose collapse", async () => {
138
+ render(<RedactoNoticeConsent {...defaultProps} />);
139
+ await waitFor(
140
+ () => {
141
+ expect(screen.getByText("Marketing")).toBeInTheDocument();
142
+ },
143
+ { timeout: 3000 }
144
+ );
145
+
146
+ const marketingHeading = screen.getByText("Marketing");
147
+ const collapseDiv = marketingHeading
148
+ .closest('div[style*="cursor: pointer"]')
149
+ ?.querySelector("svg");
150
+ if (!collapseDiv) throw new Error("Collapse div not found");
151
+
152
+ await act(async () => {
153
+ fireEvent.click(collapseDiv);
154
+ });
155
+ await waitFor(
156
+ () => {
157
+ expect(screen.getByText("Email")).toBeInTheDocument();
158
+ expect(screen.getByText("Phone")).toBeInTheDocument();
159
+ },
160
+ { timeout: 3000 }
161
+ );
162
+
163
+ await act(async () => {
164
+ fireEvent.click(collapseDiv);
165
+ });
166
+ await waitFor(
167
+ () => {
168
+ expect(screen.queryByText("Email")).not.toBeInTheDocument();
169
+ },
170
+ { timeout: 3000 }
171
+ );
172
+ });
173
+
174
+ it("handles purpose checkbox interaction", async () => {
175
+ render(<RedactoNoticeConsent {...defaultProps} />);
176
+ await waitFor(
177
+ () => {
178
+ expect(screen.getByText("Marketing")).toBeInTheDocument();
179
+ },
180
+ { timeout: 3000 }
181
+ );
182
+
183
+ const marketingHeading = screen.getByText("Marketing");
184
+ const purposeCheckbox = marketingHeading
185
+ .closest('div[style*="justify-content: space-between"]')
186
+ ?.querySelector('input[type="checkbox"]');
187
+ if (!purposeCheckbox) throw new Error("Purpose checkbox not found");
188
+
189
+ expect(purposeCheckbox).not.toBeChecked();
190
+ await act(async () => {
191
+ fireEvent.click(purposeCheckbox);
192
+ });
193
+ expect(purposeCheckbox).toBeChecked();
194
+
195
+ await act(async () => {
196
+ fireEvent.click(purposeCheckbox);
197
+ });
198
+ expect(purposeCheckbox).not.toBeChecked();
199
+ });
200
+
201
+ it("disables required data element checkboxes", async () => {
202
+ render(<RedactoNoticeConsent {...defaultProps} />);
203
+ await waitFor(
204
+ () => {
205
+ expect(screen.getByText("Marketing")).toBeInTheDocument();
206
+ },
207
+ { timeout: 3000 }
208
+ );
209
+
210
+ const marketingHeading = screen.getByText("Marketing");
211
+ const collapseDiv = marketingHeading
212
+ .closest('div[style*="cursor: pointer"]')
213
+ ?.querySelector("svg");
214
+ if (!collapseDiv) throw new Error("Collapse div not found");
215
+ await act(async () => {
216
+ fireEvent.click(collapseDiv);
217
+ });
218
+
219
+ await waitFor(
220
+ () => {
221
+ const emailText = screen.getByText("Email");
222
+ const emailCheckbox = emailText
223
+ .closest('div[style*="align-items: center"]')
224
+ ?.querySelector('input[type="checkbox"]');
225
+ if (!emailCheckbox) throw new Error("Email checkbox not found");
226
+
227
+ const phoneText = screen.getByText(/Phone/);
228
+ const phoneCheckbox = phoneText
229
+ .closest('div[style*="align-items: center"]')
230
+ ?.querySelector('input[type="checkbox"]');
231
+ if (!phoneCheckbox) throw new Error("Phone checkbox not found");
232
+
233
+ expect(phoneCheckbox).toBeDisabled();
234
+ expect(phoneCheckbox).toBeChecked();
235
+
236
+ expect(emailCheckbox).not.toBeDisabled();
237
+ fireEvent.click(emailCheckbox);
238
+ expect(emailCheckbox).toBeChecked();
239
+ },
240
+ { timeout: 3000 }
241
+ );
242
+ });
243
+
244
+ it("calls submitConsentEvent on accept", async () => {
245
+ render(<RedactoNoticeConsent {...defaultProps} />);
246
+ await waitFor(
247
+ () => {
248
+ expect(screen.getByText("Accept")).toBeInTheDocument();
249
+ },
250
+ { timeout: 3000 }
251
+ );
252
+
253
+ await act(async () => {
254
+ fireEvent.click(screen.getByText("Accept"));
255
+ });
256
+ await waitFor(
257
+ () => {
258
+ expect(api.submitConsentEvent).toHaveBeenCalledWith({
259
+ accessToken: "mock-token",
260
+ noticeUuid: "notice-123",
261
+ purposes: [
262
+ {
263
+ uuid: "purpose-1",
264
+ name: "Marketing",
265
+ description: "We use data for marketing purposes.",
266
+ industries: "Marketing",
267
+ data_elements: [
268
+ {
269
+ uuid: "element-1",
270
+ name: "Email",
271
+ required: false,
272
+ description: null,
273
+ industries: null,
274
+ enabled: true,
275
+ selected: false,
276
+ },
277
+ {
278
+ uuid: "element-2",
279
+ name: "Phone",
280
+ required: true,
281
+ description: null,
282
+ industries: null,
283
+ enabled: true,
284
+ selected: true,
285
+ },
286
+ ],
287
+ selected: false,
288
+ },
289
+ ],
290
+ declined: false,
291
+ });
292
+ expect(defaultProps.onAccept).toHaveBeenCalled();
293
+ },
294
+ { timeout: 3000 }
295
+ );
296
+ });
297
+
298
+ it("calls onDecline on decline", async () => {
299
+ render(<RedactoNoticeConsent {...defaultProps} />);
300
+ await waitFor(
301
+ () => {
302
+ expect(screen.getByText("Decline")).toBeInTheDocument();
303
+ },
304
+ { timeout: 3000 }
305
+ );
306
+
307
+ await act(async () => {
308
+ fireEvent.click(screen.getByText("Decline"));
309
+ });
310
+ await waitFor(
311
+ () => {
312
+ expect(defaultProps.onDecline).toHaveBeenCalled();
313
+ },
314
+ { timeout: 3000 }
315
+ );
316
+ });
317
+
318
+ it("handles 409 conflict error (already consented)", async () => {
319
+ mockedApi.fetchConsentContent.mockRejectedValueOnce({ status: 409 });
320
+ await act(async () => {
321
+ render(<RedactoNoticeConsent {...defaultProps} />);
322
+ });
323
+ await waitFor(
324
+ () => {
325
+ expect(defaultProps.onAccept).toHaveBeenCalled();
326
+ expect(
327
+ screen.queryByText("Your Privacy Matters")
328
+ ).not.toBeInTheDocument();
329
+ },
330
+ { timeout: 3000 }
331
+ );
332
+ });
333
+
334
+ it("handles other errors", async () => {
335
+ mockedApi.fetchConsentContent.mockRejectedValueOnce(
336
+ new Error("Network Error")
337
+ );
338
+ await act(async () => {
339
+ render(<RedactoNoticeConsent {...defaultProps} />);
340
+ });
341
+ await waitFor(
342
+ () => {
343
+ expect(defaultProps.onError).toHaveBeenCalledWith(expect.any(Error));
344
+ },
345
+ { timeout: 3000 }
346
+ );
347
+ });
348
+
349
+ it("applies custom styles from settings", async () => {
350
+ render(<RedactoNoticeConsent {...defaultProps} />);
351
+ await waitFor(
352
+ () => {
353
+ const modal = screen.getByRole("dialog");
354
+ expect(modal).toHaveStyle({
355
+ backgroundColor: "#ffffff",
356
+ borderRadius: "8px",
357
+ });
358
+
359
+ const acceptButton = screen.getByText("Accept");
360
+ expect(acceptButton).toHaveStyle({
361
+ backgroundColor: "#4f87ff",
362
+ color: "#ffffff",
363
+ });
364
+ },
365
+ { timeout: 3000 }
366
+ );
367
+ });
368
+ });