@truedat/auth 8.14.1 → 8.14.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.
Files changed (65) hide show
  1. package/package.json +4 -4
  2. package/src/groups/components/EditGroup.js +64 -20
  3. package/src/groups/components/Group.js +39 -41
  4. package/src/groups/components/GroupCard.js +51 -54
  5. package/src/groups/components/GroupCards.js +60 -25
  6. package/src/groups/components/GroupDeleteModal.js +98 -0
  7. package/src/groups/components/GroupDropdown.js +92 -0
  8. package/src/groups/components/GroupForm.js +165 -84
  9. package/src/groups/components/GroupFormModal.js +137 -0
  10. package/src/groups/components/GroupInfoModal.js +114 -0
  11. package/src/groups/components/GroupMemberPicker.js +364 -0
  12. package/src/groups/components/GroupRouteModal.js +46 -0
  13. package/src/groups/components/GroupUsers.js +31 -14
  14. package/src/groups/components/GroupUsersTable.js +136 -83
  15. package/src/groups/components/NewGroup.js +38 -10
  16. package/src/groups/components/__tests__/EditGroup.spec.js +105 -0
  17. package/src/groups/components/__tests__/Group.spec.js +29 -47
  18. package/src/groups/components/__tests__/GroupCard.spec.js +8 -20
  19. package/src/groups/components/__tests__/GroupDropdown.spec.js +27 -0
  20. package/src/groups/components/__tests__/GroupForm.spec.js +32 -5
  21. package/src/groups/components/__tests__/GroupMemberPicker.spec.js +378 -0
  22. package/src/groups/components/__tests__/GroupModals.spec.js +210 -0
  23. package/src/groups/components/__tests__/GroupUsers.spec.js +17 -2
  24. package/src/groups/components/__tests__/GroupUsersTable.spec.js +34 -2
  25. package/src/groups/components/__tests__/__snapshots__/Group.spec.js.snap +1 -0
  26. package/src/groups/components/__tests__/__snapshots__/GroupCard.spec.js.snap +32 -30
  27. package/src/groups/components/__tests__/__snapshots__/GroupCards.spec.js.snap +90 -77
  28. package/src/groups/components/__tests__/__snapshots__/GroupUsers.spec.js.snap +75 -11
  29. package/src/groups/components/__tests__/__snapshots__/GroupUsersTable.spec.js.snap +70 -13
  30. package/src/groups/components/index.js +0 -4
  31. package/src/groups/hooks/__tests__/useGroupOperation.spec.js +167 -0
  32. package/src/groups/hooks/__tests__/useGroups.spec.js +90 -0
  33. package/src/groups/hooks/useGroupOperation.js +83 -0
  34. package/src/groups/hooks/useGroups.js +64 -0
  35. package/src/groups/reducers/index.js +0 -4
  36. package/src/groups/routines.js +0 -7
  37. package/src/groups/sagas/index.js +2 -24
  38. package/src/hooks/useRoles.js +6 -3
  39. package/src/styles/Groups.less +1009 -0
  40. package/src/users/components/AdminUserRoutes.js +1 -14
  41. package/src/users/components/GroupUserCrumbs.js +7 -12
  42. package/src/users/components/UserRoutes.js +12 -35
  43. package/src/users/components/UsersAndGroups.js +3 -6
  44. package/src/users/components/__tests__/GroupUserCrumbs.spec.js +26 -30
  45. package/src/users/components/__tests__/UserRoutes.spec.js +4 -6
  46. package/src/users/components/__tests__/UsersAndGroups.spec.js +5 -7
  47. package/src/users/components/__tests__/__snapshots__/UserRoutes.spec.js.snap +9 -55
  48. package/src/groups/components/GroupLoader.js +0 -27
  49. package/src/groups/components/GroupsLoader.js +0 -30
  50. package/src/groups/components/__tests__/GroupLoader.spec.js +0 -71
  51. package/src/groups/components/__tests__/__snapshots__/GroupLoader.spec.js.snap +0 -11
  52. package/src/groups/reducers/__tests__/groupRedirect.spec.js +0 -51
  53. package/src/groups/reducers/__tests__/groupUsers.spec.js +0 -60
  54. package/src/groups/reducers/group.js +0 -56
  55. package/src/groups/reducers/groupRedirect.js +0 -20
  56. package/src/groups/reducers/groupUsers.js +0 -16
  57. package/src/groups/reducers/groups.js +0 -29
  58. package/src/groups/sagas/__tests__/deleteGroup.spec.js +0 -93
  59. package/src/groups/sagas/__tests__/deleteGroupUser.spec.js +0 -80
  60. package/src/groups/sagas/createGroup.js +0 -27
  61. package/src/groups/sagas/deleteGroup.js +0 -31
  62. package/src/groups/sagas/deleteGroupUser.js +0 -35
  63. package/src/groups/sagas/fetchGroup.js +0 -30
  64. package/src/groups/sagas/fetchGroups.js +0 -26
  65. package/src/groups/sagas/updateGroup.js +0 -28
@@ -4,6 +4,28 @@ import { render, waitForLoad } from "@truedat/test/render";
4
4
  import GroupForm from "../GroupForm";
5
5
 
6
6
  describe("<GroupForm />", () => {
7
+ it("preserves unsaved fields when server data is revalidated", async () => {
8
+ const group = { id: 1, name: "Team" };
9
+ const ui = render(
10
+ <GroupForm
11
+ group={group}
12
+ users={[]}
13
+ groupUsers={[]}
14
+ onSubmit={jest.fn()}
15
+ />,
16
+ );
17
+ const input = ui.getByPlaceholderText("group.form.name.placeholder");
18
+ await userEvent.setup({ delay: null }).type(input, " draft");
19
+ ui.rerender(
20
+ <GroupForm
21
+ group={{ ...group, name: "Server update" }}
22
+ users={[]}
23
+ groupUsers={[]}
24
+ onSubmit={jest.fn()}
25
+ />,
26
+ );
27
+ expect(input).toHaveValue("Team draft");
28
+ });
7
29
  const renderOpts = {
8
30
  state: {
9
31
  users: [{ id: 101, full_name: "John Doe", user_name: "jdoe" }],
@@ -38,7 +60,12 @@ describe("<GroupForm />", () => {
38
60
  it("updates name, description, alias and users fields", async () => {
39
61
  const onSubmit = jest.fn();
40
62
  const rendered = render(
41
- <GroupForm group={{}} groupUsers={[]} onSubmit={onSubmit} />,
63
+ <GroupForm
64
+ group={{}}
65
+ groupUsers={[]}
66
+ users={renderOpts.state.users}
67
+ onSubmit={onSubmit}
68
+ />,
42
69
  renderOpts,
43
70
  );
44
71
 
@@ -51,7 +78,7 @@ describe("<GroupForm />", () => {
51
78
  "Team Alpha",
52
79
  );
53
80
  await user.type(
54
- rendered.getByPlaceholderText(/group.form.description.placeholder/i),
81
+ rendered.getByPlaceholderText(/Descripción del grupo/i),
55
82
  "Alpha description",
56
83
  );
57
84
  await user.type(
@@ -67,9 +94,9 @@ describe("<GroupForm />", () => {
67
94
  rendered.getByPlaceholderText(/group.form.name.placeholder/i),
68
95
  ).toHaveValue("Team Alpha"),
69
96
  );
70
- expect(
71
- rendered.getByPlaceholderText(/group.form.description.placeholder/i),
72
- ).toHaveValue("Alpha description");
97
+ expect(rendered.getByPlaceholderText(/Descripción del grupo/i)).toHaveValue(
98
+ "Alpha description",
99
+ );
73
100
  expect(
74
101
  rendered.getByPlaceholderText(/group.form.alias.placeholder/i),
75
102
  ).toHaveValue("alpha-team");
@@ -0,0 +1,378 @@
1
+ import { SWRConfig } from "swr";
2
+ import { useQuery } from "@apollo/client";
3
+ import { useState } from "react";
4
+ import { act, waitFor } from "@testing-library/react";
5
+ import userEvent from "@testing-library/user-event";
6
+ import { render as renderComponent } from "@truedat/test/render";
7
+ import { apiJson, apiJsonPost } from "@truedat/core/services/api";
8
+ import GroupMemberPicker from "../GroupMemberPicker";
9
+
10
+ const render = (component) => {
11
+ const cache = new Map();
12
+ return renderComponent(
13
+ <SWRConfig
14
+ value={{
15
+ provider: () => cache,
16
+ shouldRetryOnError: false,
17
+ dedupingInterval: 0,
18
+ }}
19
+ >
20
+ {component}
21
+ </SWRConfig>,
22
+ );
23
+ };
24
+
25
+ jest.mock("@truedat/core/services/api", () => ({
26
+ apiJson: jest.fn(),
27
+ apiJsonPost: jest.fn(),
28
+ JSON_OPTS: {},
29
+ }));
30
+ jest.mock("@apollo/client", () => ({
31
+ ...jest.requireActual("@apollo/client"),
32
+ useQuery: jest.fn(),
33
+ }));
34
+ const users = [
35
+ { id: 1, user_name: "ana", role: "user" },
36
+ { id: 2, user_name: "admin", role: "admin" },
37
+ { id: 3, user_name: "agent", role: "agent" },
38
+ ];
39
+ const options = users.map(({ id, user_name }) => ({
40
+ value: id,
41
+ text: user_name,
42
+ key: id,
43
+ }));
44
+ beforeEach(() => {
45
+ jest.clearAllMocks();
46
+ useQuery.mockReturnValue({
47
+ data: { domains: [{ id: "12", name: "Finance", parentId: null }] },
48
+ loading: false,
49
+ refetch: jest.fn().mockResolvedValue({}),
50
+ });
51
+ apiJson.mockResolvedValue({ data: { data: [{ id: 1, name: "Owner" }] } });
52
+ });
53
+ it("disables hidden filter requests while allowing member search and selection", async () => {
54
+ const onChange = jest.fn();
55
+ useQuery.mockReturnValue({ error: new Error("offline") });
56
+ const ui = render(
57
+ <GroupMemberPicker
58
+ showFilters={false}
59
+ users={users}
60
+ options={options}
61
+ value={[1]}
62
+ onChange={onChange}
63
+ />,
64
+ );
65
+ expect(ui.queryByLabelText("Filtros de miembros")).not.toBeInTheDocument();
66
+ expect(
67
+ ui.queryByText("No se han podido cargar los dominios."),
68
+ ).not.toBeInTheDocument();
69
+ expect(
70
+ ui.queryByRole("button", { name: "Reintentar" }),
71
+ ).not.toBeInTheDocument();
72
+ expect(useQuery).toHaveBeenCalledWith(
73
+ expect.anything(),
74
+ expect.objectContaining({ skip: true }),
75
+ );
76
+ expect(apiJson).not.toHaveBeenCalled();
77
+ expect(apiJsonPost).not.toHaveBeenCalled();
78
+ const user = userEvent.setup({ delay: null });
79
+ const picker = ui.getByRole("combobox", { name: "Añadir miembros" });
80
+ await user.click(picker);
81
+ await user.type(picker.querySelector("input"), "admin");
82
+ await user.keyboard("{Enter}");
83
+ expect(onChange).toHaveBeenCalledWith([2, 1]);
84
+ });
85
+ it("filters account types without removing selected members", async () => {
86
+ const onChange = jest.fn();
87
+ const ui = render(
88
+ <GroupMemberPicker
89
+ users={users}
90
+ options={options}
91
+ value={[1]}
92
+ onChange={onChange}
93
+ />,
94
+ );
95
+ const user = userEvent.setup({ delay: null });
96
+ await user.click(ui.getByRole("listbox", { name: "Tipo de usuario" }));
97
+ await user.click(
98
+ ui.getByRole("option", { name: "user.type.admin", exact: true }),
99
+ );
100
+ await user.click(ui.getByRole("combobox", { name: "Añadir miembros" }));
101
+ expect(
102
+ ui.getByRole("option", { name: "admin", exact: true }),
103
+ ).toBeInTheDocument();
104
+ expect(
105
+ ui.queryByRole("option", { name: "agent", exact: true }),
106
+ ).not.toBeInTheDocument();
107
+ expect(ui.getByText("ana")).toBeInTheDocument();
108
+ expect(onChange).not.toHaveBeenCalled();
109
+ });
110
+ it("retries failed role searches and uses full users for combined type filtering", async () => {
111
+ apiJsonPost
112
+ .mockRejectedValueOnce(new Error("failed"))
113
+ .mockResolvedValueOnce({ data: { data: [{ id: 2 }] } });
114
+ const ui = render(
115
+ <GroupMemberPicker
116
+ users={users}
117
+ options={options}
118
+ value={[]}
119
+ onChange={jest.fn()}
120
+ />,
121
+ );
122
+ const user = userEvent.setup({ delay: null });
123
+ await waitFor(() =>
124
+ expect(ui.getByRole("option", { name: "Owner" })).toBeInTheDocument(),
125
+ );
126
+ await user.click(ui.getByRole("combobox", { name: "Rol" }));
127
+ await user.click(ui.getByRole("option", { name: "Owner", exact: true }));
128
+ await waitFor(() =>
129
+ expect(
130
+ ui.getByText("No se han podido cargar los usuarios."),
131
+ ).toBeInTheDocument(),
132
+ );
133
+ await user.click(ui.getByRole("button", { name: "Reintentar" }));
134
+ await waitFor(() =>
135
+ expect(
136
+ ui.queryByText("No se han podido cargar los usuarios."),
137
+ ).not.toBeInTheDocument(),
138
+ );
139
+ await user.click(ui.getByRole("listbox", { name: "Tipo de usuario" }));
140
+ await user.click(
141
+ ui.getByRole("option", { name: "user.type.admin", exact: true }),
142
+ );
143
+ await user.click(ui.getByRole("combobox", { name: "Añadir miembros" }));
144
+ expect(
145
+ ui.getByRole("option", { name: "admin", exact: true }),
146
+ ).toBeInTheDocument();
147
+ expect(apiJsonPost).toHaveBeenLastCalledWith(
148
+ "/api/users/search",
149
+ { roles: ["1"] },
150
+ {},
151
+ );
152
+ });
153
+
154
+ it.each(["click outside", "Tab", "Escape"])(
155
+ "does not add the highlighted candidate on %s",
156
+ async (action) => {
157
+ const onChange = jest.fn();
158
+ const ui = render(
159
+ <GroupMemberPicker
160
+ users={users}
161
+ options={options}
162
+ value={[]}
163
+ onChange={onChange}
164
+ />,
165
+ );
166
+ const user = userEvent.setup({ delay: null });
167
+ const picker = ui.getByRole("combobox", { name: "Añadir miembros" });
168
+ await user.click(picker);
169
+ await user.type(picker.querySelector("input"), "admin");
170
+ if (action === "click outside") {
171
+ await user.click(ui.getByText("Miembros seleccionados"));
172
+ } else {
173
+ await user.keyboard(`{${action}}`);
174
+ }
175
+ expect(onChange).not.toHaveBeenCalled();
176
+ expect(picker).toHaveAttribute("aria-expanded", "false");
177
+ },
178
+ );
179
+
180
+ it("adds the highlighted candidate with Enter", async () => {
181
+ const onChange = jest.fn();
182
+ const ui = render(
183
+ <GroupMemberPicker
184
+ users={users}
185
+ options={options}
186
+ value={[]}
187
+ onChange={onChange}
188
+ />,
189
+ );
190
+ const user = userEvent.setup({ delay: null });
191
+ const picker = ui.getByRole("combobox", { name: "Añadir miembros" });
192
+ await user.click(picker);
193
+ await user.type(picker.querySelector("input"), "admin");
194
+ await user.keyboard("{Enter}");
195
+ expect(onChange).toHaveBeenCalledWith([2]);
196
+ });
197
+
198
+ it("adds from search and only removes from the selected container", async () => {
199
+ const Picker = () => {
200
+ const [value, onChange] = useState([1]);
201
+ return (
202
+ <GroupMemberPicker
203
+ users={users}
204
+ options={options}
205
+ value={value}
206
+ onChange={onChange}
207
+ />
208
+ );
209
+ };
210
+ const ui = render(<Picker />);
211
+ const user = userEvent.setup({ delay: null });
212
+ const selected = ui.container.querySelector(".group-selected-members");
213
+ expect(selected.querySelector("input")).toBeNull();
214
+ await user.click(ui.getByRole("combobox", { name: "Añadir miembros" }));
215
+ await user.click(ui.getByRole("option", { name: "admin", exact: true }));
216
+ expect(selected.querySelector("li:first-child")).toHaveTextContent("admin");
217
+ expect(selected.querySelector("li:first-child")).toHaveClass(
218
+ "group-member-added",
219
+ );
220
+ await waitFor(
221
+ () =>
222
+ expect(selected.querySelector("li:first-child")).not.toHaveClass(
223
+ "group-member-added",
224
+ ),
225
+ { timeout: 2000 },
226
+ );
227
+ expect(
228
+ ui.container.querySelector(
229
+ ".group-member-selection .selection.dropdown .ui.label",
230
+ ),
231
+ ).toBeNull();
232
+ await user.click(selected.querySelector("li:first-child button"));
233
+ expect(selected).not.toHaveTextContent("admin");
234
+ expect(selected).toHaveTextContent("ana");
235
+ });
236
+
237
+ it("filters by domain with string user IDs and preserves selected members", async () => {
238
+ apiJsonPost.mockResolvedValue({ data: { data: [{ id: "2" }] } });
239
+ const ui = render(
240
+ <GroupMemberPicker
241
+ users={users}
242
+ options={options}
243
+ value={[1]}
244
+ onChange={jest.fn()}
245
+ />,
246
+ );
247
+ const user = userEvent.setup({ delay: null });
248
+ await user.click(ui.getByRole("combobox", { name: "Dominio" }));
249
+ await user.click(ui.getByRole("option", { name: "Finance" }));
250
+ await waitFor(() =>
251
+ expect(apiJsonPost).toHaveBeenCalledWith(
252
+ "/api/users/search",
253
+ { domains: ["12"] },
254
+ expect.anything(),
255
+ ),
256
+ );
257
+ await user.click(ui.getByRole("combobox", { name: "Añadir miembros" }));
258
+ expect(
259
+ ui.getByRole("option", { name: "admin", exact: true }),
260
+ ).toBeInTheDocument();
261
+ expect(
262
+ ui.container.querySelector(".group-selected-members"),
263
+ ).toHaveTextContent("ana");
264
+ });
265
+ it("keeps a failed domain filter visible and allows retry", async () => {
266
+ const refetch = jest.fn().mockResolvedValue({});
267
+ useQuery.mockReturnValue({ error: new Error("offline"), refetch });
268
+ const ui = render(
269
+ <GroupMemberPicker
270
+ users={users}
271
+ options={options}
272
+ value={[]}
273
+ onChange={jest.fn()}
274
+ />,
275
+ );
276
+ expect(ui.getByRole("combobox", { name: "Dominio" })).toHaveAttribute(
277
+ "aria-disabled",
278
+ "true",
279
+ );
280
+ expect(
281
+ ui.getByText("No se han podido cargar los dominios."),
282
+ ).toBeInTheDocument();
283
+ await userEvent
284
+ .setup({ delay: null })
285
+ .click(ui.getByRole("button", { name: "Reintentar" }));
286
+ expect(refetch).toHaveBeenCalledTimes(1);
287
+ });
288
+
289
+ it("adds all filtered candidates once without duplicating selected users", async () => {
290
+ apiJsonPost.mockResolvedValue({ data: { data: users } });
291
+ const Picker = () => {
292
+ const [value, onChange] = useState([1]);
293
+ return (
294
+ <GroupMemberPicker
295
+ users={users}
296
+ options={options}
297
+ value={value}
298
+ onChange={onChange}
299
+ />
300
+ );
301
+ };
302
+ const ui = render(<Picker />);
303
+ const user = userEvent.setup({ delay: null });
304
+ expect(
305
+ ui.queryByRole("button", { name: "Añadir todos" }),
306
+ ).not.toBeInTheDocument();
307
+ await waitFor(() =>
308
+ expect(ui.getByRole("option", { name: "Owner" })).toBeInTheDocument(),
309
+ );
310
+ await user.click(ui.getByRole("combobox", { name: "Rol" }));
311
+ await user.click(ui.getByRole("option", { name: "Owner" }));
312
+ const addAll = await ui.findByRole("button", {
313
+ name: "Añadir todos",
314
+ });
315
+ await waitFor(() => expect(addAll).not.toBeDisabled());
316
+ await user.click(addAll);
317
+ const rows = ui.container.querySelectorAll(".group-selected-members li");
318
+ expect(
319
+ [...rows].map((row) => row.querySelector("strong").textContent),
320
+ ).toEqual(["admin", "agent", "ana"]);
321
+ expect(rows[0]).toHaveClass("group-member-added");
322
+ expect(rows[1]).toHaveClass("group-member-added");
323
+ await waitFor(
324
+ () => expect(ui.container.querySelector(".group-member-added")).toBeNull(),
325
+ { timeout: 2000 },
326
+ );
327
+ expect(
328
+ ui.queryByRole("button", { name: "Añadir todos" }),
329
+ ).not.toBeInTheDocument();
330
+ });
331
+
332
+ it("restarts batch feedback on rapid additions, survives removal and clears on unmount", async () => {
333
+ jest.useFakeTimers();
334
+ const Picker = () => {
335
+ const [value, onChange] = useState([]);
336
+ return (
337
+ <GroupMemberPicker
338
+ users={users}
339
+ options={options}
340
+ value={value}
341
+ onChange={onChange}
342
+ />
343
+ );
344
+ };
345
+ let ui;
346
+ try {
347
+ ui = render(<Picker />);
348
+ const user = userEvent.setup({ advanceTimers: jest.advanceTimersByTime });
349
+ const add = async (name) => {
350
+ await user.click(ui.getByRole("combobox", { name: "Añadir miembros" }));
351
+ await user.click(ui.getByRole("option", { name, exact: true }));
352
+ };
353
+ await add("ana");
354
+ act(() => jest.advanceTimersByTime(500));
355
+ await add("admin");
356
+ const rows = ui.container.querySelectorAll(".group-selected-members li");
357
+ expect(rows[0]).toHaveTextContent("admin");
358
+ expect(rows[0]).toHaveClass("group-member-added");
359
+ expect(rows[1]).not.toHaveClass("group-member-added");
360
+ act(() => jest.advanceTimersByTime(400));
361
+ expect(rows[0]).toHaveClass("group-member-added");
362
+ await user.click(rows[0].querySelector("button"));
363
+ act(() => jest.advanceTimersByTime(500));
364
+ expect(ui.container.querySelector(".group-member-added")).toBeNull();
365
+ expect(
366
+ ui.container.querySelector(".group-selected-members"),
367
+ ).toHaveTextContent("ana");
368
+ await add("agent");
369
+ const clear = jest.spyOn(global, "clearTimeout");
370
+ ui.unmount();
371
+ expect(clear).toHaveBeenCalled();
372
+ clear.mockRestore();
373
+ } finally {
374
+ ui?.unmount();
375
+ jest.clearAllTimers();
376
+ jest.useRealTimers();
377
+ }
378
+ });
@@ -0,0 +1,210 @@
1
+ import { useLocation } from "react-router";
2
+ import { SWRConfig, useSWRConfig } from "swr";
3
+ import { act, waitFor, within } from "@testing-library/react";
4
+ import userEvent from "@testing-library/user-event";
5
+ import { render } from "@truedat/test/render";
6
+ import { apiJson, apiJsonPut, apiJsonDelete } from "@truedat/core/services/api";
7
+ import { UserRoutes } from "../../../users/components/UserRoutes";
8
+
9
+ jest.mock("@truedat/core/hooks/useAuthorized", () => ({
10
+ useAuthorized: () => true,
11
+ }));
12
+ jest.mock("@truedat/core/services/api", () => ({
13
+ apiJson: jest.fn(),
14
+ apiJsonPut: jest.fn(),
15
+ apiJsonPost: jest.fn(),
16
+ apiJsonDelete: jest.fn(),
17
+ JSON_OPTS: {},
18
+ }));
19
+ const members = [{ id: 101, user_name: "ana", full_name: "Ana" }];
20
+ const group = {
21
+ id: 1,
22
+ name: "Team",
23
+ alias: "Analytics",
24
+ description: "Full description",
25
+ users: members,
26
+ };
27
+ const Capture = () => {
28
+ const location = useLocation();
29
+ const { mutate } = useSWRConfig();
30
+ return (
31
+ <>
32
+ <output>{location.pathname}</output>
33
+ <button type="button" onClick={() => mutate("/api/users")}>
34
+ Refresh users
35
+ </button>
36
+ <UserRoutes />
37
+ </>
38
+ );
39
+ };
40
+ const setup = (route) => {
41
+ const cache = new Map();
42
+ return render(
43
+ <SWRConfig
44
+ value={{
45
+ provider: () => cache,
46
+ shouldRetryOnError: false,
47
+ dedupingInterval: 0,
48
+ }}
49
+ >
50
+ <Capture />
51
+ </SWRConfig>,
52
+ { routes: [route] },
53
+ );
54
+ };
55
+ beforeEach(() => {
56
+ jest.resetAllMocks();
57
+ apiJson.mockImplementation((url) =>
58
+ Promise.resolve({
59
+ data: {
60
+ data:
61
+ url === "/api/users"
62
+ ? members
63
+ : url === "/api/groups"
64
+ ? [group]
65
+ : group,
66
+ },
67
+ }),
68
+ );
69
+ });
70
+
71
+ describe("Group modals", () => {
72
+ it.each(["/groups/new", "/groups/1", "/groups/1/edit"])(
73
+ "opens %s directly",
74
+ async (route) => {
75
+ const ui = setup(route);
76
+ await waitFor(() =>
77
+ expect(
78
+ ui.container.querySelector(".td-group-card"),
79
+ ).toBeInTheDocument(),
80
+ );
81
+ if (route === "/groups/1")
82
+ expect(
83
+ await ui.findByText("Full description", {
84
+ selector: ".true-modal-subtitle",
85
+ }),
86
+ ).toBeInTheDocument();
87
+ else
88
+ expect(
89
+ await ui.findByRole("textbox", { name: "Nombre del grupo" }),
90
+ ).toBeInTheDocument();
91
+ },
92
+ );
93
+
94
+ it("closes back to the group list", async () => {
95
+ const ui = setup("/groups/1");
96
+ await userEvent
97
+ .setup({ delay: null })
98
+ .click(ui.getByRole("button", { name: "Close" }));
99
+ await waitFor(() =>
100
+ expect(ui.queryByRole("dialog")).not.toBeInTheDocument(),
101
+ );
102
+ expect(ui.getByRole("status")).toHaveTextContent("/groups");
103
+ });
104
+
105
+ it("preserves edits after a failed write", async () => {
106
+ apiJsonPut.mockRejectedValue(new Error("offline"));
107
+ const ui = setup("/groups/1/edit");
108
+ const input = await ui.findByRole("textbox", { name: "Nombre del grupo" });
109
+ const user = userEvent.setup({ delay: null });
110
+ await user.clear(input);
111
+ await user.type(input, "Updated team");
112
+ await user.click(ui.getByRole("button", { name: "actions.save" }));
113
+ await ui.findByText(
114
+ "No se ha podido completar la operación. Inténtalo de nuevo.",
115
+ );
116
+ expect(input).toHaveValue("Updated team");
117
+ });
118
+
119
+ it("preserves an edit draft if directory revalidation fails", async () => {
120
+ const ui = setup("/groups/1/edit");
121
+ const input = await ui.findByRole("textbox", { name: "Nombre del grupo" });
122
+ const user = userEvent.setup({ delay: null });
123
+ await user.clear(input);
124
+ await user.type(input, "Draft team");
125
+
126
+ apiJson.mockRejectedValue(new Error("offline"));
127
+ await user.click(ui.getByRole("button", { name: "Refresh users" }));
128
+
129
+ expect(
130
+ await ui.findByText("No se han podido cargar los usuarios."),
131
+ ).toBeInTheDocument();
132
+ expect(input).toHaveValue("Draft team");
133
+ });
134
+
135
+ it("blocks closing while a write is pending", async () => {
136
+ const deferred = {};
137
+ apiJsonPut.mockReturnValue(
138
+ new Promise((resolve) => {
139
+ deferred.resolve = resolve;
140
+ }),
141
+ );
142
+ const ui = setup("/groups/1/edit");
143
+ const input = await ui.findByRole("textbox", { name: "Nombre del grupo" });
144
+ const user = userEvent.setup({ delay: null });
145
+ await user.type(input, " updated");
146
+ await waitFor(() =>
147
+ expect(
148
+ ui.getByRole("button", { name: "actions.save" }),
149
+ ).not.toBeDisabled(),
150
+ );
151
+ await user.click(ui.getByRole("button", { name: "actions.save" }));
152
+ await waitFor(() =>
153
+ expect(ui.getByRole("button", { name: "Close" })).toBeDisabled(),
154
+ );
155
+ await act(async () => deferred.resolve({ data: { data: group } }));
156
+ });
157
+
158
+ it("closes only after successful deletion", async () => {
159
+ const deferred = {};
160
+ apiJsonDelete.mockReturnValue(
161
+ new Promise((resolve) => {
162
+ deferred.resolve = resolve;
163
+ }),
164
+ );
165
+ const ui = setup("/groups/1/edit");
166
+ await ui.findByRole("textbox", { name: "Nombre del grupo" });
167
+ const user = userEvent.setup({ delay: null });
168
+ await user.click(document.querySelector(".group-delete-action"));
169
+ const confirmation = document.querySelector(".group-modal--delete");
170
+ await user.click(
171
+ within(confirmation).getByRole("button", { name: "actions.delete" }),
172
+ );
173
+ expect(
174
+ within(confirmation).getByRole("button", { name: "Close" }),
175
+ ).toBeDisabled();
176
+ await act(async () => deferred.resolve({}));
177
+ await waitFor(() =>
178
+ expect(ui.queryByRole("dialog")).not.toBeInTheDocument(),
179
+ );
180
+ });
181
+
182
+ it("shows an edit invitation for an empty group", async () => {
183
+ apiJson.mockImplementation((url) =>
184
+ Promise.resolve({
185
+ data: {
186
+ data:
187
+ url === "/api/users"
188
+ ? members
189
+ : url === "/api/groups"
190
+ ? [group]
191
+ : { ...group, users: [] },
192
+ },
193
+ }),
194
+ );
195
+ const ui = setup("/groups/1");
196
+ expect(
197
+ await ui.findByText("Este grupo no tiene miembros"),
198
+ ).toBeInTheDocument();
199
+ });
200
+
201
+ it("offers retry after detail loading fails", async () => {
202
+ apiJson.mockRejectedValue(new Error("offline"));
203
+ const ui = setup("/groups/1");
204
+ expect(
205
+ await within(ui.getByRole("dialog")).findByRole("button", {
206
+ name: "Reintentar",
207
+ }),
208
+ ).toBeInTheDocument();
209
+ });
210
+ });
@@ -1,12 +1,27 @@
1
1
  import { render, waitForLoad } from "@truedat/test/render";
2
+ import { useGroup, useGroupUsers } from "../../hooks/useGroups";
2
3
  import { GroupUsers } from "../GroupUsers";
3
4
 
5
+ jest.mock("../../hooks/useGroups", () => ({
6
+ useGroup: jest.fn(),
7
+ useGroupUsers: jest.fn(),
8
+ }));
9
+
4
10
  describe("<GroupUsers />", () => {
5
11
  const group = { id: "2", name: "grupo1", description: "aaa bbb cc" };
6
- const props = { group };
12
+
13
+ beforeEach(() => {
14
+ useGroup.mockReturnValue({
15
+ group,
16
+ members: [],
17
+ loading: false,
18
+ retry: jest.fn(),
19
+ });
20
+ useGroupUsers.mockReturnValue({ users: [] });
21
+ });
7
22
 
8
23
  it("matches the latest snapshot", async () => {
9
- const rendered = render(<GroupUsers {...props} />);
24
+ const rendered = render(<GroupUsers />);
10
25
  await waitForLoad(rendered);
11
26
  expect(rendered.container).toMatchSnapshot();
12
27
  });