@truedat/auth 8.14.3 → 8.14.4
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 +4 -4
- package/src/messages/en.js +6 -5
- package/src/messages/es.js +8 -7
- package/src/styles/UserCard.less +582 -0
- package/src/styles/UserModals.less +826 -0
- package/src/styles/Users.less +280 -0
- package/src/users/components/AdminUserRoutes.js +7 -20
- package/src/users/components/EditUser.js +7 -34
- package/src/users/components/EditUserModal.js +150 -0
- package/src/users/components/NewUser.js +128 -17
- package/src/users/components/Password.js +11 -189
- package/src/users/components/UserAcls.js +56 -6
- package/src/users/components/UserActions.js +99 -34
- package/src/users/components/UserCard.js +168 -22
- package/src/users/components/UserCards.js +273 -35
- package/src/users/components/UserFormFields.js +206 -0
- package/src/users/components/UserGroupAclRow.js +8 -2
- package/src/users/components/UserGroupAcls.js +2 -2
- package/src/users/components/UserGroupLink.js +21 -0
- package/src/users/components/UserInfoModal.js +268 -0
- package/src/users/components/UserPasswordField.js +66 -0
- package/src/users/components/UserPasswordModal.js +210 -0
- package/src/users/components/UserRoutes.js +4 -13
- package/src/users/components/UserTabs.js +5 -2
- package/src/users/components/UserTypeSelector.js +109 -0
- package/src/users/components/UsersAndGroups.js +39 -5
- package/src/users/components/UsersTable.js +234 -0
- package/src/users/components/__tests__/EditUser.spec.js +84 -10
- package/src/users/components/__tests__/NewUser.spec.js +34 -4
- package/src/users/components/__tests__/Password.spec.js +53 -146
- package/src/users/components/__tests__/UserAcls.spec.js +23 -2
- package/src/users/components/__tests__/UserCard.spec.js +111 -5
- package/src/users/components/__tests__/UserCards.spec.js +194 -0
- package/src/users/components/__tests__/UserGroupAclRow.spec.js +19 -1
- package/src/users/components/__tests__/UserModals.spec.js +507 -0
- package/src/users/components/__tests__/UserRoutes.spec.js +8 -3
- package/src/users/components/__tests__/UserTypeSelector.spec.js +32 -0
- package/src/users/components/__tests__/UsersAndGroups.spec.js +17 -0
- package/src/users/components/__tests__/__snapshots__/UserCard.spec.js.snap +64 -37
- package/src/users/components/__tests__/__snapshots__/UserCards.spec.js.snap +424 -75
- package/src/users/components/__tests__/__snapshots__/UserGroupAclRow.spec.js.snap +7 -3
- package/src/users/components/__tests__/__snapshots__/UserRoutes.spec.js.snap +4 -13
- package/src/users/hooks/useSortedGroups.js +13 -0
- package/src/users/hooks/useUserModalOperation.js +55 -0
- package/src/users/reducers/__tests__/userRedirect.spec.js +14 -3
- package/src/users/reducers/index.js +1 -0
- package/src/users/reducers/userModalOperation.js +29 -0
- package/src/users/reducers/userRedirect.js +5 -4
- package/src/users/sagas/__tests__/updateUser.spec.js +19 -1
- package/src/users/sagas/createUser.js +12 -5
- package/src/users/sagas/deleteUser.js +12 -5
- package/src/users/sagas/updateUser.js +19 -8
- package/src/users/selectors/__tests__/filterUsers.spec.js +11 -0
- package/src/users/selectors/__tests__/getUserAcls.spec.js +1 -0
- package/src/users/selectors/__tests__/getUserGroupAcls.spec.js +2 -0
- package/src/users/selectors/filterUsers.js +9 -1
- package/src/users/selectors/getUserGroupAcls.js +1 -0
- package/src/users/components/User.js +0 -68
- package/src/users/components/UserPassword.js +0 -24
- package/src/users/components/__tests__/User.spec.js +0 -20
- package/src/users/components/__tests__/UserPassword.spec.js +0 -24
- package/src/users/components/__tests__/__snapshots__/EditUser.spec.js.snap +0 -281
- package/src/users/components/__tests__/__snapshots__/NewUser.spec.js.snap +0 -321
- package/src/users/components/__tests__/__snapshots__/Password.spec.js.snap +0 -128
- package/src/users/components/__tests__/__snapshots__/User.spec.js.snap +0 -129
- package/src/users/components/__tests__/__snapshots__/UserPassword.spec.js.snap +0 -124
|
@@ -1,158 +1,65 @@
|
|
|
1
|
-
import { waitFor } from "@testing-library/react";
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
4
|
-
import
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
const render = (component) =>
|
|
8
|
-
renderWithIntl(<MemoryRouter>{component}</MemoryRouter>);
|
|
1
|
+
import { fireEvent, waitFor } from "@testing-library/react";
|
|
2
|
+
import { Routes, Route } from "react-router";
|
|
3
|
+
import { render } from "@truedat/test/render";
|
|
4
|
+
import Password from "../Password";
|
|
5
|
+
import { updatePassword } from "../../routines";
|
|
9
6
|
|
|
10
7
|
describe("<Password />", () => {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
it("calls updatePassword for non admin user", async () => {
|
|
22
|
-
const props = { userName: "joe", authUserName: "joe", updatePassword };
|
|
23
|
-
const rendered = render(<Password {...props} />);
|
|
24
|
-
const user = userEvent.setup({ delay: null });
|
|
25
|
-
|
|
26
|
-
// Submit button should initially be disabled
|
|
27
|
-
await waitFor(() =>
|
|
28
|
-
expect(rendered.getByRole("button", { name: /save/i })).toBeDisabled(),
|
|
29
|
-
);
|
|
30
|
-
|
|
31
|
-
await user.type(
|
|
32
|
-
rendered.getByLabelText(/user.password.current/i, { selector: "input" }),
|
|
33
|
-
"oldPassword",
|
|
34
|
-
);
|
|
35
|
-
await user.type(
|
|
36
|
-
rendered.getByLabelText(/user.password.new_password/i, {
|
|
37
|
-
selector: "input",
|
|
38
|
-
}),
|
|
39
|
-
"Va1id.pa$$w0rd!",
|
|
40
|
-
);
|
|
41
|
-
await user.type(
|
|
42
|
-
rendered.getByLabelText(/login.form.re_password.label/i, {
|
|
43
|
-
selector: "input",
|
|
44
|
-
}),
|
|
45
|
-
"Va1id.pa$$w0rd!",
|
|
46
|
-
);
|
|
47
|
-
|
|
48
|
-
await waitFor(() =>
|
|
49
|
-
expect(
|
|
50
|
-
rendered.getByRole("button", { name: /save/i }),
|
|
51
|
-
).not.toBeDisabled(),
|
|
52
|
-
);
|
|
53
|
-
|
|
54
|
-
// Submit
|
|
55
|
-
await user.click(await rendered.findByRole("button", { name: /save/i }));
|
|
56
|
-
|
|
57
|
-
await waitFor(() =>
|
|
58
|
-
expect(updatePassword).toHaveBeenCalledWith({
|
|
59
|
-
user: {
|
|
60
|
-
old_password: "oldPassword",
|
|
61
|
-
new_password: "Va1id.pa$$w0rd!",
|
|
62
|
-
},
|
|
63
|
-
}),
|
|
64
|
-
);
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
it("calls updatePassword for admin user", async () => {
|
|
68
|
-
const props = {
|
|
69
|
-
id: 42,
|
|
70
|
-
userName: "joe",
|
|
71
|
-
authUserName: "bob",
|
|
72
|
-
updatePassword,
|
|
73
|
-
};
|
|
74
|
-
const rendered = render(<Password {...props} />);
|
|
75
|
-
const user = userEvent.setup({ delay: null });
|
|
76
|
-
|
|
77
|
-
// Submit button should initially be disabled
|
|
78
|
-
await waitFor(() =>
|
|
79
|
-
expect(rendered.getByRole("button", { name: /save/i })).toBeDisabled(),
|
|
80
|
-
);
|
|
81
|
-
|
|
82
|
-
// No current password id needed for admin users
|
|
83
|
-
expect(
|
|
84
|
-
await rendered.queryByLabelText(/user.password.current/i, {
|
|
85
|
-
selector: "input",
|
|
86
|
-
}),
|
|
87
|
-
).toBeNull();
|
|
88
|
-
|
|
89
|
-
await user.type(
|
|
90
|
-
rendered.getByLabelText(/user.password.new_password/i, {
|
|
91
|
-
selector: "input",
|
|
92
|
-
}),
|
|
93
|
-
"Va1id.pa$$w0rd!",
|
|
8
|
+
it("uses the shared modal for the authenticated user without an account ID", async () => {
|
|
9
|
+
const dispatch = jest.fn();
|
|
10
|
+
const rendered = render(<Password />, {
|
|
11
|
+
state: { authentication: { user_name: "test.user" } },
|
|
12
|
+
dispatch,
|
|
13
|
+
});
|
|
14
|
+
expect(rendered.getByRole("dialog")).toHaveClass("user-modal--password");
|
|
15
|
+
expect(document.getElementById("currentPassword")).toHaveAttribute(
|
|
16
|
+
"autocomplete",
|
|
17
|
+
"current-password",
|
|
94
18
|
);
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
19
|
+
Object.entries({
|
|
20
|
+
currentPassword: "old-example123",
|
|
21
|
+
password: "new-example123",
|
|
22
|
+
passwordConfirmation: "new-example123",
|
|
23
|
+
}).forEach(([id, value]) =>
|
|
24
|
+
fireEvent.change(document.getElementById(id), { target: { value } }),
|
|
100
25
|
);
|
|
101
|
-
|
|
26
|
+
fireEvent.submit(document.getElementById("password-modal-form"));
|
|
102
27
|
await waitFor(() =>
|
|
103
|
-
expect(
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
id: 42,
|
|
115
|
-
new_password: "Va1id.pa$$w0rd!",
|
|
116
|
-
},
|
|
117
|
-
}),
|
|
28
|
+
expect(dispatch).toHaveBeenCalledWith(
|
|
29
|
+
expect.objectContaining({
|
|
30
|
+
type: updatePassword.TRIGGER,
|
|
31
|
+
payload: {
|
|
32
|
+
user: {
|
|
33
|
+
old_password: "old-example123",
|
|
34
|
+
new_password: "new-example123",
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
}),
|
|
38
|
+
),
|
|
118
39
|
);
|
|
119
40
|
});
|
|
120
41
|
|
|
121
|
-
it("
|
|
122
|
-
const
|
|
123
|
-
const rendered = render(
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
42
|
+
it("returns to the previous page on cancel without submitting", async () => {
|
|
43
|
+
const dispatch = jest.fn();
|
|
44
|
+
const rendered = render(
|
|
45
|
+
<Routes>
|
|
46
|
+
<Route path="/previous" element={<div>Previous page</div>} />
|
|
47
|
+
<Route path="/password" element={<Password />} />
|
|
48
|
+
</Routes>,
|
|
49
|
+
{
|
|
50
|
+
routes: ["/previous", "/password"],
|
|
51
|
+
routeIndex: 1,
|
|
52
|
+
state: { authentication: { user_name: "test.user" } },
|
|
53
|
+
dispatch,
|
|
54
|
+
},
|
|
129
55
|
);
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
const props = { userName: "joe", authUserName: "joe", updatePassword };
|
|
138
|
-
const rendered = render(<Password {...props} />);
|
|
139
|
-
const user = userEvent.setup({ delay: null });
|
|
140
|
-
|
|
141
|
-
await user.type(
|
|
142
|
-
rendered.getByLabelText(/user.password.new_password/i, {
|
|
143
|
-
selector: "input",
|
|
144
|
-
}),
|
|
145
|
-
"Va1id.pa$$w0rd!",
|
|
56
|
+
fireEvent.change(document.getElementById("password"), {
|
|
57
|
+
target: { value: "discard-example" },
|
|
58
|
+
});
|
|
59
|
+
fireEvent.click(rendered.getByRole("button", { name: "actions.cancel" }));
|
|
60
|
+
expect(await rendered.findByText("Previous page")).toBeVisible();
|
|
61
|
+
expect(dispatch).not.toHaveBeenCalledWith(
|
|
62
|
+
expect.objectContaining({ type: updatePassword.TRIGGER }),
|
|
146
63
|
);
|
|
147
|
-
await user.type(
|
|
148
|
-
rendered.getByLabelText(/login.form.re_password.label/i, {
|
|
149
|
-
selector: "input",
|
|
150
|
-
}),
|
|
151
|
-
"different",
|
|
152
|
-
);
|
|
153
|
-
|
|
154
|
-
expect(
|
|
155
|
-
await rendered.findByText(/user.password.must.match/i),
|
|
156
|
-
).not.toBeNull();
|
|
157
64
|
});
|
|
158
65
|
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { waitFor } from "@testing-library/react";
|
|
1
|
+
import { screen, waitFor, within } from "@testing-library/react";
|
|
2
2
|
import { render } from "@truedat/test/render";
|
|
3
|
-
import UserAcls from "../UserAcls";
|
|
3
|
+
import UserAcls, { UserAcls as UserAclsTable } from "../UserAcls";
|
|
4
4
|
|
|
5
5
|
const renderOpts = {
|
|
6
6
|
messages: {
|
|
@@ -32,3 +32,24 @@ describe("<UserAcls />", () => {
|
|
|
32
32
|
await waitFor(() => expect(container).toMatchSnapshot());
|
|
33
33
|
});
|
|
34
34
|
});
|
|
35
|
+
|
|
36
|
+
it("groups roles by domain identity and deduplicates repeated assignments", () => {
|
|
37
|
+
const first = { id: 1, name: "Domain", type: "domain" };
|
|
38
|
+
const second = { id: 2, name: "Domain", type: "domain" };
|
|
39
|
+
const acls = [
|
|
40
|
+
{ resource: first, role: "Reader" },
|
|
41
|
+
{ resource: first, role: "Editor" },
|
|
42
|
+
{ resource: first, role: "Reader" },
|
|
43
|
+
{ resource: second, role: "Reader" },
|
|
44
|
+
];
|
|
45
|
+
const rendered = render(<UserAclsTable acls={acls} groupBy="domain" />);
|
|
46
|
+
expect(screen.getAllByRole("row")).toHaveLength(3);
|
|
47
|
+
expect(
|
|
48
|
+
within(screen.getAllByRole("row")[1]).getAllByRole("listitem"),
|
|
49
|
+
).toHaveLength(2);
|
|
50
|
+
rendered.rerender(<UserAclsTable acls={acls} groupBy="role" />);
|
|
51
|
+
expect(screen.getAllByRole("row")).toHaveLength(3);
|
|
52
|
+
expect(
|
|
53
|
+
within(screen.getAllByRole("row")[2]).getAllByRole("listitem"),
|
|
54
|
+
).toHaveLength(2);
|
|
55
|
+
});
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { render } from "@truedat/test/render";
|
|
2
|
+
import userEvent from "@testing-library/user-event";
|
|
3
|
+
import { fireEvent, waitFor } from "@testing-library/react";
|
|
4
|
+
import { useLocation } from "react-router";
|
|
2
5
|
import { UserCard } from "../UserCard";
|
|
3
6
|
|
|
7
|
+
const CurrentRoute = () => <output>{useLocation().pathname}</output>;
|
|
8
|
+
|
|
4
9
|
describe("<UserCard />", () => {
|
|
5
10
|
const deleteUser = jest.fn();
|
|
6
11
|
const user = {
|
|
@@ -12,15 +17,116 @@ describe("<UserCard />", () => {
|
|
|
12
17
|
uid: "uID00001",
|
|
13
18
|
};
|
|
14
19
|
|
|
15
|
-
it("matches the latest snapshot
|
|
16
|
-
const props = { user, deleteUser
|
|
20
|
+
it("matches the latest snapshot", () => {
|
|
21
|
+
const props = { user, deleteUser };
|
|
17
22
|
const rendered = render(<UserCard {...props} />);
|
|
18
23
|
expect(rendered.container).toMatchSnapshot();
|
|
19
24
|
});
|
|
20
25
|
|
|
21
|
-
it("
|
|
22
|
-
const
|
|
26
|
+
it("shows the trimmed external ID without replacing user metadata", () => {
|
|
27
|
+
const rendered = render(
|
|
28
|
+
<UserCard user={{ ...user, external_id: " employee-123 " }} />,
|
|
29
|
+
);
|
|
30
|
+
expect(rendered.getByText("employee-123")).toHaveAttribute(
|
|
31
|
+
"title",
|
|
32
|
+
"ID externo: employee-123",
|
|
33
|
+
);
|
|
34
|
+
expect(rendered.getByText(user.user_name)).toBeInTheDocument();
|
|
35
|
+
expect(rendered.getByText(user.email)).toBeInTheDocument();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("shows groups in the badge popup", async () => {
|
|
39
|
+
const props = {
|
|
40
|
+
user: { ...user, groups: ["admins", { alias: "Quality" }] },
|
|
41
|
+
deleteUser,
|
|
42
|
+
};
|
|
23
43
|
const rendered = render(<UserCard {...props} />);
|
|
24
|
-
|
|
44
|
+
|
|
45
|
+
expect(rendered.container.querySelector(".mail.icon")).toBeInTheDocument();
|
|
46
|
+
fireEvent.mouseOver(rendered.getByText("2"));
|
|
47
|
+
|
|
48
|
+
await waitFor(() => expect(rendered.getByRole("list")).toBeInTheDocument());
|
|
49
|
+
expect(rendered.getByText("admins")).toBeInTheDocument();
|
|
50
|
+
expect(rendered.getByText("Quality")).toBeInTheDocument();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("omits rows with missing or blank optional data", () => {
|
|
54
|
+
const props = {
|
|
55
|
+
user: {
|
|
56
|
+
id: "2",
|
|
57
|
+
user_name: "jhon.doe",
|
|
58
|
+
email: " ",
|
|
59
|
+
external_id: " ",
|
|
60
|
+
groups: [],
|
|
61
|
+
},
|
|
62
|
+
deleteUser,
|
|
63
|
+
};
|
|
64
|
+
const rendered = render(<UserCard {...props} />);
|
|
65
|
+
|
|
66
|
+
expect(rendered.getByText("jhon.doe")).toBeInTheDocument();
|
|
67
|
+
expect(
|
|
68
|
+
rendered.container.querySelector(".user.icon"),
|
|
69
|
+
).not.toBeInTheDocument();
|
|
70
|
+
expect(
|
|
71
|
+
rendered.container.querySelector(".mail.icon"),
|
|
72
|
+
).not.toBeInTheDocument();
|
|
73
|
+
expect(
|
|
74
|
+
rendered.container.querySelector(".groups-badge"),
|
|
75
|
+
).not.toBeInTheDocument();
|
|
76
|
+
expect(
|
|
77
|
+
rendered.container.querySelector(".uc-meta-grid"),
|
|
78
|
+
).toBeEmptyDOMElement();
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("does not render meta tooltips on hover", () => {
|
|
82
|
+
const longUserName = "very.long.user.name";
|
|
83
|
+
const props = {
|
|
84
|
+
user: {
|
|
85
|
+
...user,
|
|
86
|
+
user_name: longUserName,
|
|
87
|
+
full_name: "Very Long User",
|
|
88
|
+
},
|
|
89
|
+
deleteUser,
|
|
90
|
+
};
|
|
91
|
+
const rendered = render(<UserCard {...props} />);
|
|
92
|
+
|
|
93
|
+
fireEvent.mouseOver(rendered.getByText(longUserName));
|
|
94
|
+
|
|
95
|
+
expect(rendered.getAllByText(longUserName)).toHaveLength(1);
|
|
96
|
+
expect(rendered.container.querySelector(".inverted.popup")).toBeNull();
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("keeps card navigation separate from edit and password actions", async () => {
|
|
100
|
+
const rendered = render(
|
|
101
|
+
<>
|
|
102
|
+
<UserCard user={{ ...user, groups: ["Data"] }} />
|
|
103
|
+
<CurrentRoute />
|
|
104
|
+
</>,
|
|
105
|
+
{ routes: ["/users"] },
|
|
106
|
+
);
|
|
107
|
+
const keyboard = userEvent.setup({ delay: null });
|
|
108
|
+
await keyboard.tab();
|
|
109
|
+
expect(
|
|
110
|
+
rendered.getByRole("link", { name: "user.info.open" }),
|
|
111
|
+
).toHaveFocus();
|
|
112
|
+
await keyboard.keyboard("{Enter}");
|
|
113
|
+
expect(rendered.getByRole("status")).toHaveTextContent(/^\/users\/1$/);
|
|
114
|
+
await keyboard.tab();
|
|
115
|
+
expect(
|
|
116
|
+
rendered.getByRole("link", { name: "users.actions.edit" }),
|
|
117
|
+
).toHaveFocus();
|
|
118
|
+
await keyboard.keyboard("{Enter}");
|
|
119
|
+
expect(rendered.getByRole("status")).toHaveTextContent("/users/1/edit");
|
|
120
|
+
await keyboard.click(
|
|
121
|
+
rendered.getByRole("link", { name: "user.actions.edit_password" }),
|
|
122
|
+
);
|
|
123
|
+
expect(rendered.getByRole("status")).toHaveTextContent("/users/1/password");
|
|
124
|
+
expect(
|
|
125
|
+
rendered.queryByRole("button", {
|
|
126
|
+
name: "user.actions.delete.confirmation.header",
|
|
127
|
+
}),
|
|
128
|
+
).not.toBeInTheDocument();
|
|
129
|
+
await keyboard.click(rendered.getByRole("link", { name: "1" }));
|
|
130
|
+
expect(rendered.getByRole("status")).toHaveTextContent(/^\/users\/1$/);
|
|
25
131
|
});
|
|
26
132
|
});
|
|
@@ -1,8 +1,11 @@
|
|
|
1
|
+
import { screen, within } from "@testing-library/react";
|
|
1
2
|
import userEvent from "@testing-library/user-event";
|
|
2
3
|
import { render, waitForLoad } from "@truedat/test/render";
|
|
3
4
|
import { UserCards } from "../UserCards";
|
|
4
5
|
|
|
5
6
|
describe("<UserCards />", () => {
|
|
7
|
+
beforeEach(() => localStorage.clear());
|
|
8
|
+
afterEach(() => jest.restoreAllMocks());
|
|
6
9
|
const users = [
|
|
7
10
|
{
|
|
8
11
|
id: "1",
|
|
@@ -68,3 +71,194 @@ describe("<UserCards />", () => {
|
|
|
68
71
|
expect(rendered.getByText(/user.search.no_results/i)).toBeInTheDocument();
|
|
69
72
|
});
|
|
70
73
|
});
|
|
74
|
+
|
|
75
|
+
it("retains search when switching views and remembers the selected view", async () => {
|
|
76
|
+
const user = userEvent.setup({ delay: null });
|
|
77
|
+
const rendered = render(
|
|
78
|
+
<UserCards
|
|
79
|
+
users={[
|
|
80
|
+
{
|
|
81
|
+
id: 1,
|
|
82
|
+
full_name: "Alpha",
|
|
83
|
+
user_name: "a",
|
|
84
|
+
role: "user",
|
|
85
|
+
external_id: "EXT-1",
|
|
86
|
+
},
|
|
87
|
+
{ id: 2, full_name: "Beta", user_name: "b", role: "admin" },
|
|
88
|
+
]}
|
|
89
|
+
/>,
|
|
90
|
+
);
|
|
91
|
+
await user.type(
|
|
92
|
+
screen.getByPlaceholderText(/user.search.placeholder/),
|
|
93
|
+
"ext-1",
|
|
94
|
+
);
|
|
95
|
+
await user.click(screen.getByRole("button", { name: "users.view.table" }));
|
|
96
|
+
expect(screen.getAllByRole("row")).toHaveLength(2);
|
|
97
|
+
expect(screen.getByRole("link", { name: "Alpha" })).toHaveAttribute(
|
|
98
|
+
"href",
|
|
99
|
+
"/users/1",
|
|
100
|
+
);
|
|
101
|
+
expect(localStorage.getItem("truedat.users.view")).toBe("table");
|
|
102
|
+
expect(
|
|
103
|
+
screen.getByRole("button", { name: "users.view.table" }),
|
|
104
|
+
).toHaveAttribute("aria-pressed", "true");
|
|
105
|
+
await user.click(screen.getByRole("button", { name: "users.view.cards" }));
|
|
106
|
+
expect(rendered.container.querySelectorAll(".card")).toHaveLength(1);
|
|
107
|
+
await user.click(screen.getByRole("button", { name: "users.view.table" }));
|
|
108
|
+
await user.click(screen.getByRole("button", { name: "users.filter.clear" }));
|
|
109
|
+
expect(screen.getAllByRole("row")).toHaveLength(3);
|
|
110
|
+
await user.click(screen.getByRole("button", { name: /users.table.name/ }));
|
|
111
|
+
expect(
|
|
112
|
+
within(screen.getAllByRole("row")[1]).getByRole("link", { name: "Beta" }),
|
|
113
|
+
).toBeInTheDocument();
|
|
114
|
+
rendered.unmount();
|
|
115
|
+
render(<UserCards users={[]} />);
|
|
116
|
+
expect(screen.getByRole("table")).toBeInTheDocument();
|
|
117
|
+
localStorage.clear();
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("works when storage is unavailable and reuses the delete confirmation", async () => {
|
|
121
|
+
const get = jest
|
|
122
|
+
.spyOn(Storage.prototype, "getItem")
|
|
123
|
+
.mockImplementation(() => {
|
|
124
|
+
throw new Error("unavailable");
|
|
125
|
+
});
|
|
126
|
+
const set = jest
|
|
127
|
+
.spyOn(Storage.prototype, "setItem")
|
|
128
|
+
.mockImplementation(() => {
|
|
129
|
+
throw new Error("unavailable");
|
|
130
|
+
});
|
|
131
|
+
const user = userEvent.setup({ delay: null });
|
|
132
|
+
render(
|
|
133
|
+
<UserCards
|
|
134
|
+
users={[
|
|
135
|
+
{
|
|
136
|
+
id: 1,
|
|
137
|
+
user_name: "Alpha",
|
|
138
|
+
role: "user",
|
|
139
|
+
groups: ["Data"],
|
|
140
|
+
acls: [
|
|
141
|
+
{ role: { id: 7, name: "Reader" } },
|
|
142
|
+
{ role: { id: 7, name: "Reader" }, group: { name: "Data" } },
|
|
143
|
+
],
|
|
144
|
+
},
|
|
145
|
+
]}
|
|
146
|
+
/>,
|
|
147
|
+
);
|
|
148
|
+
await user.click(screen.getByRole("button", { name: "users.view.table" }));
|
|
149
|
+
expect(screen.getByLabelText("users.table.roles: 1")).toBeInTheDocument();
|
|
150
|
+
expect(
|
|
151
|
+
screen.getByRole("link", { name: "users.table.edit" }),
|
|
152
|
+
).toHaveAttribute("href", "/users/1/edit");
|
|
153
|
+
await user.click(screen.getByRole("button", { name: "users.table.delete" }));
|
|
154
|
+
expect(screen.getByRole("dialog")).toBeInTheDocument();
|
|
155
|
+
await user.click(screen.getByRole("button", { name: "actions.cancel" }));
|
|
156
|
+
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
|
|
157
|
+
expect(screen.getByRole("table")).toBeInTheDocument();
|
|
158
|
+
get.mockRestore();
|
|
159
|
+
set.mockRestore();
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it("combines type and group filters and keeps all group options available", async () => {
|
|
163
|
+
localStorage.clear();
|
|
164
|
+
const user = userEvent.setup({ delay: null });
|
|
165
|
+
render(
|
|
166
|
+
<UserCards
|
|
167
|
+
users={[
|
|
168
|
+
{ id: 1, user_name: "Alpha", role: "user", groups: ["Data"], acls: [] },
|
|
169
|
+
{
|
|
170
|
+
id: 2,
|
|
171
|
+
user_name: "Beta",
|
|
172
|
+
role: "admin",
|
|
173
|
+
groups: [{ name: "quality", alias: "Quality" }],
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
id: 3,
|
|
177
|
+
user_name: "Gamma",
|
|
178
|
+
role: "user",
|
|
179
|
+
groups: ["Data"],
|
|
180
|
+
acls: [{ role: {} }],
|
|
181
|
+
},
|
|
182
|
+
]}
|
|
183
|
+
/>,
|
|
184
|
+
);
|
|
185
|
+
await user.click(screen.getByRole("button", { name: "users.view.table" }));
|
|
186
|
+
await user.click(screen.getByText("Filters", { exact: true }));
|
|
187
|
+
await user.click(screen.getByRole("option", { name: "type", exact: true }));
|
|
188
|
+
await user.click(screen.getByRole("option", { name: "user.type.user" }));
|
|
189
|
+
await user.keyboard("{Escape}");
|
|
190
|
+
await user.click(screen.getByText("Filters", { exact: true }));
|
|
191
|
+
await user.click(screen.getByRole("option", { name: "groups", exact: true }));
|
|
192
|
+
expect(screen.getByRole("option", { name: "Quality" })).toBeInTheDocument();
|
|
193
|
+
await user.click(screen.getByRole("option", { name: "Data" }));
|
|
194
|
+
await user.keyboard("{Escape}");
|
|
195
|
+
expect(screen.getAllByRole("row")).toHaveLength(3);
|
|
196
|
+
expect(screen.getAllByText("users.table.unknown")).toHaveLength(1);
|
|
197
|
+
await user.type(
|
|
198
|
+
screen.getByPlaceholderText(/user.search.placeholder/),
|
|
199
|
+
"alpha",
|
|
200
|
+
);
|
|
201
|
+
expect(screen.getAllByRole("row")).toHaveLength(2);
|
|
202
|
+
await user.click(screen.getByRole("button", { name: "users.filter.clear" }));
|
|
203
|
+
expect(screen.getAllByRole("row")).toHaveLength(4);
|
|
204
|
+
localStorage.clear();
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
describe("<UserCards /> role sections", () => {
|
|
208
|
+
beforeEach(() => localStorage.clear());
|
|
209
|
+
const users = [
|
|
210
|
+
{ id: 1, user_name: "alpha", full_name: "Alpha", role: "admin" },
|
|
211
|
+
{ id: 2, user_name: "beta", full_name: "Beta", role: "user" },
|
|
212
|
+
];
|
|
213
|
+
|
|
214
|
+
it("exposes each role section as a button with its expanded state", async () => {
|
|
215
|
+
const rendered = render(<UserCards users={users} />);
|
|
216
|
+
await waitForLoad(rendered);
|
|
217
|
+
|
|
218
|
+
const adminToggle = screen.getByRole("button", { name: /user.type.admin/ });
|
|
219
|
+
expect(adminToggle).toHaveAttribute("aria-expanded", "true");
|
|
220
|
+
const panelId = adminToggle.getAttribute("aria-controls");
|
|
221
|
+
expect(panelId).toBeTruthy();
|
|
222
|
+
expect(document.getElementById(panelId)).toHaveClass("users-role-content");
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it.each([["{Enter}"], [" "]])(
|
|
226
|
+
"collapses and expands a role section with the keyboard (%j)",
|
|
227
|
+
async (key) => {
|
|
228
|
+
const user = userEvent.setup({ delay: null });
|
|
229
|
+
const rendered = render(<UserCards users={users} />);
|
|
230
|
+
await waitForLoad(rendered);
|
|
231
|
+
|
|
232
|
+
await user.tab();
|
|
233
|
+
const toggles = screen
|
|
234
|
+
.getAllByRole("button")
|
|
235
|
+
.filter((button) => button.hasAttribute("aria-expanded"));
|
|
236
|
+
const adminToggle = toggles.find((button) =>
|
|
237
|
+
/user.type.admin/.test(button.textContent),
|
|
238
|
+
);
|
|
239
|
+
adminToggle.focus();
|
|
240
|
+
expect(adminToggle).toHaveFocus();
|
|
241
|
+
|
|
242
|
+
await user.keyboard(key);
|
|
243
|
+
expect(adminToggle).toHaveAttribute("aria-expanded", "false");
|
|
244
|
+
expect(adminToggle).toHaveFocus();
|
|
245
|
+
|
|
246
|
+
await user.keyboard(key);
|
|
247
|
+
expect(adminToggle).toHaveAttribute("aria-expanded", "true");
|
|
248
|
+
},
|
|
249
|
+
);
|
|
250
|
+
|
|
251
|
+
it("keeps the other role sections open when one is collapsed", async () => {
|
|
252
|
+
const user = userEvent.setup({ delay: null });
|
|
253
|
+
const rendered = render(<UserCards users={users} />);
|
|
254
|
+
await waitForLoad(rendered);
|
|
255
|
+
|
|
256
|
+
await user.click(screen.getByRole("button", { name: /user.type.admin/ }));
|
|
257
|
+
expect(
|
|
258
|
+
screen.getByRole("button", { name: /user.type.admin/ }),
|
|
259
|
+
).toHaveAttribute("aria-expanded", "false");
|
|
260
|
+
expect(
|
|
261
|
+
screen.getByRole("button", { name: /user.type.user/ }),
|
|
262
|
+
).toHaveAttribute("aria-expanded", "true");
|
|
263
|
+
});
|
|
264
|
+
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { waitFor } from "@testing-library/react";
|
|
2
|
-
import {
|
|
2
|
+
import { render } from "@truedat/test/render";
|
|
3
3
|
import UserGroupAclRow from "../UserGroupAclRow";
|
|
4
4
|
|
|
5
5
|
describe("<UserGroupAclRow />", () => {
|
|
@@ -20,4 +20,22 @@ describe("<UserGroupAclRow />", () => {
|
|
|
20
20
|
);
|
|
21
21
|
await waitFor(() => expect(container).toMatchSnapshot());
|
|
22
22
|
});
|
|
23
|
+
|
|
24
|
+
it("renders group, domain and role in order with a new-tab group link", () => {
|
|
25
|
+
const rendered = render(
|
|
26
|
+
<table>
|
|
27
|
+
<tbody>
|
|
28
|
+
<UserGroupAclRow {...props} groupId={6} />
|
|
29
|
+
</tbody>
|
|
30
|
+
</table>,
|
|
31
|
+
{},
|
|
32
|
+
);
|
|
33
|
+
expect(
|
|
34
|
+
rendered.getAllByRole("cell").map((cell) => cell.textContent),
|
|
35
|
+
).toEqual(["grupo1", "resource1", "role1"]);
|
|
36
|
+
const link = rendered.getByRole("link", { name: "grupo1" });
|
|
37
|
+
expect(link).toHaveAttribute("href", "/groups/6");
|
|
38
|
+
expect(link).toHaveAttribute("target", "_blank");
|
|
39
|
+
expect(link).toHaveAttribute("rel", "noopener noreferrer");
|
|
40
|
+
});
|
|
23
41
|
});
|