@truedat/auth 8.14.0 → 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
@@ -1,21 +1,22 @@
1
1
  import _ from "lodash/fp";
2
- import { useState } from "react";
2
+ import { useMemo, useState } from "react";
3
3
  import PropTypes from "prop-types";
4
4
  import { useIntl } from "react-intl";
5
- import { connect } from "react-redux";
6
- import { useNavigate } from "react-router";
7
- import { Table, Header, Icon, Button } from "semantic-ui-react";
8
- import ConfirmModal from "@truedat/core/components/ConfirmModal";
5
+ import { Link } from "react-router";
6
+ import { Table, Icon, Button } from "semantic-ui-react";
7
+ import SearchInput from "@truedat/core/components/SearchInput";
9
8
  import { linkTo } from "@truedat/core/routes";
10
9
  import { columnDecorator } from "@truedat/core/services";
11
10
  import { accentInsensitivePathOrder } from "@truedat/core/services/sort";
12
- import { deleteGroupUser } from "../routines";
11
+ import GroupDeleteModal from "./GroupDeleteModal";
12
+
13
+ const EMPTY_USERS = [];
13
14
 
14
15
  const toSearchable = _.flow(_.deburr, _.toLower, _.trim);
15
16
 
16
17
  const matchesFilter = (filter) =>
17
18
  _.flow(
18
- _.at(["user_name", "full_name", "email"]),
19
+ _.at(["user_name", "full_name", "email", "external_id"]),
19
20
  _.map(toSearchable),
20
21
  _.some(_.includes(toSearchable(filter))),
21
22
  );
@@ -24,15 +25,18 @@ const columns = [
24
25
  { name: "full_name", width: 2, sort: { name: "full_name" } },
25
26
  { name: "user_name", width: 2, sort: { name: "user_name" } },
26
27
  { name: "email", width: 2, sort: { name: "email" } },
28
+ { name: "external_id", width: 2, sort: { name: "external_id" } },
27
29
  ];
28
30
 
29
31
  export const GroupUsersTable = ({
30
32
  filter,
33
+ onFilterChange,
31
34
  groupUsers,
32
- deleteGroupUser,
35
+ users = EMPTY_USERS,
33
36
  group,
37
+ readOnly = false,
34
38
  }) => {
35
- const navigate = useNavigate();
39
+ const [removing, setRemoving] = useState(null);
36
40
  const [sortBy, setSortBy] = useState("full_name");
37
41
  const [ascending, setAscending] = useState(true);
38
42
  const { formatMessage } = useIntl();
@@ -44,97 +48,146 @@ export const GroupUsersTable = ({
44
48
  setAscending(true);
45
49
  }
46
50
  };
51
+ const members = useMemo(() => {
52
+ const externalIds = new Map(
53
+ users.map(({ id, external_id }) => [String(id), external_id]),
54
+ );
55
+ return (groupUsers || EMPTY_USERS).map((user) => ({
56
+ ...user,
57
+ external_id: user.external_id ?? externalIds.get(String(user.id)),
58
+ }));
59
+ }, [groupUsers, users]);
47
60
  const direction = ascending ? "ascending" : "descending";
48
61
  const data = _.flow(
49
62
  _.trim(filter) ? _.filter(matchesFilter(filter)) : _.identity,
50
63
  _.sortBy(accentInsensitivePathOrder(sortBy)),
51
64
  ascending ? _.identity : _.reverse,
52
- )(groupUsers);
53
- return _.isEmpty(data) ? (
54
- <Header
55
- as="h4"
56
- icon="search"
57
- content={formatMessage({ id: "groupUsers.search.results.empty" })}
58
- />
59
- ) : (
60
- <Table sortable>
61
- <Table.Header>
62
- <Table.Row>
63
- {columns.map((columnSpec, key) => (
64
- <Table.HeaderCell
65
- key={key}
66
- width={columnSpec.width}
67
- content={formatMessage({
68
- id: `user.props.${columnSpec.header || columnSpec.name}`,
69
- })}
70
- sorted={
71
- _.pathEq("sort.name", sortBy)(columnSpec) ? direction : null
72
- }
73
- className={_.path("sort.name")(columnSpec) ? "" : "disabled"}
74
- onClick={() => handleSort(columnSpec)}
75
- />
76
- ))}
77
- <Table.HeaderCell key="delete_action" width={1} />
78
- </Table.Row>
79
- </Table.Header>
80
- <Table.Body>
81
- {data.map((user, i) => (
82
- <Table.Row key={`${user.id}_${i}`}>
83
- {columns.map((column, i) =>
84
- column.name == "full_name" ? (
85
- <Table.Cell
86
- onClick={() => navigate(linkTo.USER({ id: user.id }))}
87
- width={_.prop("width")(column)}
88
- key={i}
89
- textAlign={column.textAlign}
90
- content={<a>{columnDecorator(column)(user)}</a>}
91
- className="pointer"
92
- />
93
- ) : (
65
+ )(members);
66
+ return (
67
+ <>
68
+ {removing && (
69
+ <GroupDeleteModal
70
+ group={group}
71
+ member={removing}
72
+ onClose={() => setRemoving(null)}
73
+ />
74
+ )}
75
+ <Table sortable unstackable className="group-users-table">
76
+ <Table.Header>
77
+ <Table.Row>
78
+ {columns.map((columnSpec, key) => (
79
+ <Table.HeaderCell
80
+ key={key}
81
+ width={columnSpec.width}
82
+ content={
83
+ <div className="group-column-heading">
84
+ <span>
85
+ {formatMessage({
86
+ id:
87
+ columnSpec.name === "external_id"
88
+ ? "user.form.external_id"
89
+ : `user.props.${columnSpec.header || columnSpec.name}`,
90
+ })}
91
+ </span>
92
+ {columnSpec.name === "full_name" && onFilterChange && (
93
+ <span
94
+ onClick={(event) => event.stopPropagation()}
95
+ onKeyDown={(event) => event.stopPropagation()}
96
+ >
97
+ <SearchInput
98
+ value={filter}
99
+ aria-label={formatMessage({
100
+ id: "user.search.placeholder",
101
+ })}
102
+ placeholder={formatMessage({
103
+ id: "user.search.placeholder",
104
+ })}
105
+ onChange={(_event, { value }) =>
106
+ onFilterChange(value)
107
+ }
108
+ />
109
+ </span>
110
+ )}
111
+ </div>
112
+ }
113
+ sorted={
114
+ _.pathEq("sort.name", sortBy)(columnSpec) ? direction : null
115
+ }
116
+ className={_.path("sort.name")(columnSpec) ? "" : "disabled"}
117
+ tabIndex={0}
118
+ onKeyDown={(event) => {
119
+ if (event.key === "Enter" || event.key === " ") {
120
+ event.preventDefault();
121
+ handleSort(columnSpec);
122
+ }
123
+ }}
124
+ onClick={() => handleSort(columnSpec)}
125
+ />
126
+ ))}
127
+ {!readOnly && <Table.HeaderCell key="delete_action" width={1} />}
128
+ </Table.Row>
129
+ </Table.Header>
130
+ <Table.Body>
131
+ {_.isEmpty(data) && (
132
+ <Table.Row>
133
+ <Table.Cell colSpan={columns.length + (readOnly ? 0 : 1)}>
134
+ {formatMessage({ id: "groupUsers.search.results.empty" })}
135
+ </Table.Cell>
136
+ </Table.Row>
137
+ )}
138
+ {data.map((user) => (
139
+ <Table.Row key={user.id}>
140
+ {columns.map((column, i) => (
94
141
  <Table.Cell
95
142
  width={_.prop("width")(column)}
96
143
  key={i}
97
144
  textAlign={column.textAlign}
98
- content={columnDecorator(column)(user)}
145
+ content={
146
+ column.name === "full_name" ? (
147
+ <Link to={linkTo.USER({ id: user.id })}>
148
+ {columnDecorator(column)(user)}
149
+ </Link>
150
+ ) : (
151
+ columnDecorator(column)(user)
152
+ )
153
+ }
154
+ className={
155
+ column.name === "full_name" ? "pointer" : undefined
156
+ }
99
157
  />
100
- ),
101
- )}
102
- <Table.Cell className="pointer" textAlign="center">
103
- <ConfirmModal
104
- icon="trash"
105
- trigger={
106
- <Button icon color="red" basic size="mini">
107
- <Icon name="trash alternate outline" color="red" />
158
+ ))}
159
+ {!readOnly && (
160
+ <Table.Cell className="pointer" textAlign="center">
161
+ <Button
162
+ type="button"
163
+ icon
164
+ negative
165
+ basic
166
+ size="mini"
167
+ aria-label={formatMessage({
168
+ id: "groupUsers.actions.delete.confirmation.header",
169
+ })}
170
+ onClick={() => setRemoving(user)}
171
+ >
172
+ <Icon name="trash alternate outline" />
108
173
  </Button>
109
- }
110
- header={formatMessage({
111
- id: "groupUsers.actions.delete.confirmation.header",
112
- })}
113
- content={formatMessage({
114
- id: "groupUsers.actions.delete.confirmation.content",
115
- })}
116
- onConfirm={() => deleteGroupUser({ user, group, groupUsers })}
117
- onOpen={(e) => e.stopPropagation()}
118
- onClose={(e) => e.stopPropagation()}
119
- />
120
- </Table.Cell>
121
- </Table.Row>
122
- ))}
123
- </Table.Body>
124
- </Table>
174
+ </Table.Cell>
175
+ )}
176
+ </Table.Row>
177
+ ))}
178
+ </Table.Body>
179
+ </Table>
180
+ </>
125
181
  );
126
182
  };
127
183
 
128
184
  GroupUsersTable.propTypes = {
129
185
  filter: PropTypes.string,
186
+ readOnly: PropTypes.bool,
187
+ onFilterChange: PropTypes.func,
130
188
  groupUsers: PropTypes.array,
131
- deleteGroupUser: PropTypes.func,
189
+ users: PropTypes.array,
132
190
  group: PropTypes.object,
133
191
  };
134
192
 
135
- const mapStateToProps = (state) => ({
136
- groupUsers: state.groupUsers,
137
- group: state.group,
138
- });
139
-
140
- export default connect(mapStateToProps, { deleteGroupUser })(GroupUsersTable);
193
+ export default GroupUsersTable;
@@ -1,13 +1,19 @@
1
- import PropTypes from "prop-types";
1
+ import { useNavigate } from "react-router";
2
2
  import { Header, Icon, Container, Segment } from "semantic-ui-react";
3
- import { connect } from "react-redux";
4
3
  import { FormattedMessage, useIntl } from "react-intl";
5
- import { createGroup } from "../routines";
4
+ import { GROUPS } from "@truedat/core/routes";
5
+ import { useGroupUsers } from "../hooks/useGroups";
6
+ import { useGroupOperation } from "../hooks/useGroupOperation";
6
7
  import GroupForm from "./GroupForm";
7
8
  import GroupBreadcrumbs from "./GroupBreadcrumbs";
8
9
 
9
- const NewGroup = ({ createGroup }) => {
10
+ const NewGroup = () => {
10
11
  const { formatMessage } = useIntl();
12
+ const navigate = useNavigate();
13
+ const { users, hasData, loading, error, retry } = useGroupUsers();
14
+ const { submit, pending, failed } = useGroupOperation("create", null, () =>
15
+ navigate(GROUPS),
16
+ );
11
17
  return (
12
18
  <>
13
19
  <GroupBreadcrumbs name="groups.actions.create" />
@@ -26,14 +32,36 @@ const NewGroup = ({ createGroup }) => {
26
32
  />
27
33
  </Header.Content>
28
34
  </Header>
29
- <GroupForm onSubmit={createGroup} />
35
+ {loading && !hasData ? (
36
+ <p role="status">Cargando usuarios…</p>
37
+ ) : !hasData ? (
38
+ <div role="alert">
39
+ No se han podido cargar los usuarios.
40
+ <button type="button" onClick={() => retry().catch(() => {})}>
41
+ Reintentar
42
+ </button>
43
+ </div>
44
+ ) : (
45
+ <>
46
+ {error && (
47
+ <div role="alert">
48
+ No se han podido actualizar los usuarios.
49
+ <button type="button" onClick={() => retry().catch(() => {})}>
50
+ Reintentar
51
+ </button>
52
+ </div>
53
+ )}
54
+ {failed && (
55
+ <p role="alert">
56
+ No se ha podido crear el grupo. Inténtalo de nuevo.
57
+ </p>
58
+ )}
59
+ <GroupForm onSubmit={submit} users={users} isSubmitting={pending} />
60
+ </>
61
+ )}
30
62
  </Container>
31
63
  </>
32
64
  );
33
65
  };
34
66
 
35
- NewGroup.propTypes = {
36
- createGroup: PropTypes.func,
37
- };
38
-
39
- export default connect(null, { createGroup })(NewGroup);
67
+ export default NewGroup;
@@ -0,0 +1,105 @@
1
+ import { useParams } from "react-router";
2
+ import { SWRConfig, useSWRConfig } from "swr";
3
+ import { act, waitFor } from "@testing-library/react";
4
+ import userEvent from "@testing-library/user-event";
5
+ import { render } from "@truedat/test/render";
6
+ import { apiJson } from "@truedat/core/services/api";
7
+ import EditGroup from "../EditGroup";
8
+
9
+ jest.mock("react-router", () => ({
10
+ ...jest.requireActual("react-router"),
11
+ useParams: jest.fn(),
12
+ useNavigate: () => jest.fn(),
13
+ }));
14
+ jest.mock("@truedat/core/services/api", () => ({
15
+ apiJson: jest.fn(),
16
+ apiJsonPut: jest.fn(),
17
+ JSON_OPTS: {},
18
+ }));
19
+
20
+ const groups = {
21
+ 1: { id: 1, name: "Team one", users: [{ id: 11 }] },
22
+ 2: { id: 2, name: "Team two", users: [{ id: 22 }] },
23
+ };
24
+
25
+ const RefreshGroup = () => {
26
+ const { mutate } = useSWRConfig();
27
+ return (
28
+ <button type="button" onClick={() => mutate("/api/groups/1")}>
29
+ Refresh group
30
+ </button>
31
+ );
32
+ };
33
+
34
+ const setup = () => {
35
+ const cache = new Map();
36
+ const swrConfig = { provider: () => cache, dedupingInterval: 0 };
37
+ return {
38
+ cache,
39
+ swrConfig,
40
+ ui: render(
41
+ <SWRConfig value={swrConfig}>
42
+ <RefreshGroup />
43
+ <EditGroup />
44
+ </SWRConfig>,
45
+ ),
46
+ };
47
+ };
48
+
49
+ describe("<EditGroup />", () => {
50
+ beforeEach(() => {
51
+ jest.resetAllMocks();
52
+ useParams.mockReturnValue({ id: "1" });
53
+ apiJson.mockImplementation((url) =>
54
+ Promise.resolve({
55
+ data: {
56
+ data:
57
+ url === "/api/users"
58
+ ? []
59
+ : url === "/api/groups"
60
+ ? Object.values(groups)
61
+ : groups[url.split("/").pop()],
62
+ },
63
+ }),
64
+ );
65
+ });
66
+
67
+ it("reinitializes the form when navigating between cached groups", async () => {
68
+ const { ui, swrConfig } = setup();
69
+ const user = userEvent.setup({ delay: null });
70
+ const name = await ui.findByRole("textbox", { name: "group.props.name" });
71
+ await user.clear(name);
72
+ await user.type(name, "Unsaved team one");
73
+
74
+ useParams.mockReturnValue({ id: "2" });
75
+ ui.rerender(
76
+ <SWRConfig value={swrConfig}>
77
+ <EditGroup />
78
+ </SWRConfig>,
79
+ );
80
+
81
+ await waitFor(() =>
82
+ expect(ui.getByRole("textbox", { name: "group.props.name" })).toHaveValue(
83
+ "Team two",
84
+ ),
85
+ );
86
+ });
87
+
88
+ it("keeps an edit draft when a background group refresh fails", async () => {
89
+ const { ui } = setup();
90
+ const user = userEvent.setup({ delay: null });
91
+ const name = await ui.findByRole("textbox", { name: "group.props.name" });
92
+ await user.clear(name);
93
+ await user.type(name, "Unsaved draft");
94
+
95
+ apiJson.mockRejectedValue(new Error("offline"));
96
+ await user.click(ui.getByRole("button", { name: "Refresh group" }));
97
+ expect(
98
+ await ui.findByText(
99
+ "No se ha podido actualizar el grupo o sus usuarios.",
100
+ ),
101
+ ).toBeInTheDocument();
102
+
103
+ expect(name).toHaveValue("Unsaved draft");
104
+ });
105
+ });
@@ -1,15 +1,23 @@
1
- import { fireEvent, waitFor } from "@testing-library/react";
2
1
  import userEvent from "@testing-library/user-event";
2
+ import { waitFor } from "@testing-library/react";
3
3
  import { render } from "@truedat/test/render";
4
- import ConnectedGroup, { Group } from "../Group";
4
+ import { useGroup } from "../../hooks/useGroups";
5
+ import { Group } from "../Group";
5
6
 
6
- const group = { id: 2, name: "grupo1 ", description: "aaa bbb cc" };
7
+ jest.mock("../../hooks/useGroups", () => ({ useGroup: jest.fn() }));
8
+ jest.mock("../GroupDeleteModal", () => ({ group }) => (
9
+ <div>Delete group {group.name}</div>
10
+ ));
11
+
12
+ const group = { id: "2", name: "grupo1 ", description: "aaa bbb cc" };
7
13
 
8
14
  describe("<Group />", () => {
15
+ beforeEach(() => {
16
+ useGroup.mockReturnValue({ group, loading: false, retry: jest.fn() });
17
+ });
18
+
9
19
  it("matches the latest snapshot", () => {
10
- const { container } = render(
11
- <Group group={group} deleteGroup={jest.fn()} groupLoading={false} />,
12
- );
20
+ const { container } = render(<Group />);
13
21
  expect(container).toMatchSnapshot();
14
22
  });
15
23
 
@@ -20,58 +28,32 @@ describe("<Group />", () => {
20
28
  alias: "Grupo Alias",
21
29
  description: "aaa bbb cc",
22
30
  };
23
- const { getByText } = render(
24
- <Group group={group} deleteGroup={jest.fn()} />,
25
- );
31
+ useGroup.mockReturnValue({ group, loading: false, retry: jest.fn() });
32
+ const { getByText } = render(<Group />);
26
33
 
27
34
  expect(getByText("Grupo Alias")).toBeInTheDocument();
28
35
  expect(getByText("grupo1")).toBeInTheDocument();
29
36
  });
30
37
 
31
- it("renders nothing while the group is loading", () => {
32
- const rendered = render(
33
- <Group group={group} deleteGroup={jest.fn()} groupLoading={true} />,
34
- );
35
- expect(rendered.container).toBeEmptyDOMElement();
36
- });
37
-
38
- it("renders nothing when the group has no id", () => {
39
- const rendered = render(
40
- <Group
41
- group={{ name: "no id", description: "x" }}
42
- deleteGroup={jest.fn()}
43
- groupLoading={false}
44
- />,
45
- );
46
- expect(rendered.container).toBeEmptyDOMElement();
38
+ it("shows a loading status while the group is loading", () => {
39
+ useGroup.mockReturnValue({ group: undefined, loading: true });
40
+ const rendered = render(<Group />);
41
+ expect(rendered.getByRole("status")).toBeInTheDocument();
47
42
  });
48
43
 
49
- it("maps group state from the store for the connected component", () => {
50
- const rendered = render(<ConnectedGroup />, {
51
- state: {
52
- group: { id: 2, name: "from store", description: "stored desc" },
53
- groupLoading: false,
54
- },
55
- });
56
- expect(rendered.getByText(/from store/i)).toBeInTheDocument();
44
+ it("shows an unavailable message when the group cannot be loaded", () => {
45
+ useGroup.mockReturnValue({ group: undefined, loading: false });
46
+ const rendered = render(<Group />);
47
+ expect(rendered.getByRole("alert")).toBeInTheDocument();
57
48
  });
58
49
 
59
- it("calls deleteGroup with the group id when delete is confirmed", async () => {
50
+ it("opens the delete dialog from the group actions menu", async () => {
60
51
  const user = userEvent.setup({ delay: null });
61
- const deleteGroup = jest.fn();
62
- const rendered = render(
63
- <Group group={group} deleteGroup={deleteGroup} groupLoading={false} />,
64
- );
65
- const dropdown = rendered.getByRole("listbox");
66
- fireEvent.click(dropdown);
67
- await waitFor(() =>
68
- expect(rendered.getByText(/actions.delete/i)).toBeInTheDocument(),
69
- );
70
- await user.click(rendered.getByText(/actions.delete/i));
52
+ const rendered = render(<Group />);
53
+ await user.click(rendered.getByRole("listbox"));
54
+ await user.click(rendered.getByText("actions.delete"));
71
55
  await waitFor(() =>
72
- expect(rendered.getByText(/confirmation.yes/i)).toBeInTheDocument(),
56
+ expect(rendered.getByText(/Delete group grupo1/)).toBeInTheDocument(),
73
57
  );
74
- await user.click(rendered.getByText(/confirmation.yes/i));
75
- expect(deleteGroup).toHaveBeenCalledWith({ id: group.id });
76
58
  });
77
59
  });
@@ -1,15 +1,13 @@
1
- import userEvent from "@testing-library/user-event";
2
1
  import { render, waitForLoad } from "@truedat/test/render";
3
2
  import { GroupCard } from "../GroupCard";
4
3
 
5
4
  describe("<GroupCard />", () => {
6
- const deleteGroup = jest.fn();
7
5
  const group = {
8
6
  id: "1",
9
7
  name: "administrators",
10
8
  description: "Administrators",
11
9
  };
12
- const props = { group, deleteGroup };
10
+ const props = { group };
13
11
 
14
12
  it("matches the latest snapshot", async () => {
15
13
  const rendered = render(<GroupCard {...props} />);
@@ -22,27 +20,17 @@ describe("<GroupCard />", () => {
22
20
  ...group,
23
21
  alias: "Admins",
24
22
  };
25
- const rendered = render(
26
- <GroupCard group={aliasGroup} deleteGroup={deleteGroup} />,
27
- );
23
+ const rendered = render(<GroupCard group={aliasGroup} />);
28
24
  await waitForLoad(rendered);
29
25
 
30
26
  expect(rendered.getByText("Admins")).toBeInTheDocument();
31
- expect(rendered.queryByText("administrators")).not.toBeInTheDocument();
27
+ expect(rendered.getByText("administrators")).toBeInTheDocument();
32
28
  });
33
29
 
34
- it("calls deleteGroup when delete button is clicked", async () => {
35
- const rendered = render(<GroupCard {...props} />);
36
- await waitForLoad(rendered);
37
-
38
- const user = userEvent.setup({ delay: null });
39
-
40
- await user.click(rendered.getByRole("button", { name: /delete/i }));
41
-
42
- await user.click(
43
- rendered.getByRole("button", { name: /modal-affirmative-action/i }),
44
- );
45
-
46
- expect(deleteGroup).toHaveBeenCalledTimes(1);
30
+ it("does not offer deletion from the card", () => {
31
+ const ui = render(<GroupCard {...props} />);
32
+ expect(
33
+ ui.queryByRole("button", { name: /delete/i }),
34
+ ).not.toBeInTheDocument();
47
35
  });
48
36
  });
@@ -0,0 +1,27 @@
1
+ import { fireEvent } from "@testing-library/react";
2
+ import { render } from "@truedat/test/render";
3
+ import GroupDropdown from "../GroupDropdown";
4
+
5
+ it("closes only the dropdown with Escape and restores the menu styles", () => {
6
+ const ui = render(
7
+ <GroupDropdown
8
+ selection
9
+ aria-label="Filter"
10
+ options={[{ key: 1, value: 1, text: "One" }]}
11
+ />,
12
+ );
13
+ const trigger = ui.getByRole("listbox", { name: "Filter" });
14
+ fireEvent.click(trigger);
15
+ const menu = trigger.querySelector(".menu");
16
+ expect(menu).toHaveAttribute("popover", "manual");
17
+ expect(menu.style.position).toBe("fixed");
18
+ const outerEscape = jest.fn();
19
+ document.addEventListener("keydown", outerEscape);
20
+ fireEvent.keyDown(trigger, { key: "Escape" });
21
+ expect(outerEscape).not.toHaveBeenCalled();
22
+ expect(menu).not.toHaveAttribute("popover");
23
+ expect(menu.style.position).toBe("");
24
+ fireEvent.keyDown(trigger, { key: "Escape" });
25
+ expect(outerEscape).toHaveBeenCalledTimes(1);
26
+ document.removeEventListener("keydown", outerEscape);
27
+ });