@truedat/auth 8.14.2 → 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.
Files changed (66) hide show
  1. package/package.json +4 -4
  2. package/src/messages/en.js +6 -5
  3. package/src/messages/es.js +8 -7
  4. package/src/styles/UserCard.less +582 -0
  5. package/src/styles/UserModals.less +826 -0
  6. package/src/styles/Users.less +280 -0
  7. package/src/users/components/AdminUserRoutes.js +7 -20
  8. package/src/users/components/EditUser.js +7 -34
  9. package/src/users/components/EditUserModal.js +150 -0
  10. package/src/users/components/NewUser.js +128 -17
  11. package/src/users/components/Password.js +11 -189
  12. package/src/users/components/UserAcls.js +56 -6
  13. package/src/users/components/UserActions.js +99 -34
  14. package/src/users/components/UserCard.js +168 -22
  15. package/src/users/components/UserCards.js +273 -35
  16. package/src/users/components/UserFormFields.js +206 -0
  17. package/src/users/components/UserGroupAclRow.js +8 -2
  18. package/src/users/components/UserGroupAcls.js +2 -2
  19. package/src/users/components/UserGroupLink.js +21 -0
  20. package/src/users/components/UserInfoModal.js +268 -0
  21. package/src/users/components/UserPasswordField.js +66 -0
  22. package/src/users/components/UserPasswordModal.js +210 -0
  23. package/src/users/components/UserRoutes.js +4 -13
  24. package/src/users/components/UserTabs.js +5 -2
  25. package/src/users/components/UserTypeSelector.js +109 -0
  26. package/src/users/components/UsersAndGroups.js +39 -5
  27. package/src/users/components/UsersTable.js +234 -0
  28. package/src/users/components/__tests__/EditUser.spec.js +84 -10
  29. package/src/users/components/__tests__/NewUser.spec.js +34 -4
  30. package/src/users/components/__tests__/Password.spec.js +53 -146
  31. package/src/users/components/__tests__/UserAcls.spec.js +23 -2
  32. package/src/users/components/__tests__/UserCard.spec.js +111 -5
  33. package/src/users/components/__tests__/UserCards.spec.js +194 -0
  34. package/src/users/components/__tests__/UserGroupAclRow.spec.js +19 -1
  35. package/src/users/components/__tests__/UserModals.spec.js +507 -0
  36. package/src/users/components/__tests__/UserRoutes.spec.js +8 -3
  37. package/src/users/components/__tests__/UserTypeSelector.spec.js +32 -0
  38. package/src/users/components/__tests__/UsersAndGroups.spec.js +17 -0
  39. package/src/users/components/__tests__/__snapshots__/UserCard.spec.js.snap +64 -37
  40. package/src/users/components/__tests__/__snapshots__/UserCards.spec.js.snap +424 -75
  41. package/src/users/components/__tests__/__snapshots__/UserGroupAclRow.spec.js.snap +7 -3
  42. package/src/users/components/__tests__/__snapshots__/UserRoutes.spec.js.snap +4 -13
  43. package/src/users/hooks/useSortedGroups.js +13 -0
  44. package/src/users/hooks/useUserModalOperation.js +55 -0
  45. package/src/users/reducers/__tests__/userRedirect.spec.js +14 -3
  46. package/src/users/reducers/index.js +1 -0
  47. package/src/users/reducers/userModalOperation.js +29 -0
  48. package/src/users/reducers/userRedirect.js +5 -4
  49. package/src/users/sagas/__tests__/updateUser.spec.js +19 -1
  50. package/src/users/sagas/createUser.js +12 -5
  51. package/src/users/sagas/deleteUser.js +12 -5
  52. package/src/users/sagas/updateUser.js +19 -8
  53. package/src/users/selectors/__tests__/filterUsers.spec.js +11 -0
  54. package/src/users/selectors/__tests__/getUserAcls.spec.js +1 -0
  55. package/src/users/selectors/__tests__/getUserGroupAcls.spec.js +2 -0
  56. package/src/users/selectors/filterUsers.js +9 -1
  57. package/src/users/selectors/getUserGroupAcls.js +1 -0
  58. package/src/users/components/User.js +0 -68
  59. package/src/users/components/UserPassword.js +0 -24
  60. package/src/users/components/__tests__/User.spec.js +0 -20
  61. package/src/users/components/__tests__/UserPassword.spec.js +0 -24
  62. package/src/users/components/__tests__/__snapshots__/EditUser.spec.js.snap +0 -281
  63. package/src/users/components/__tests__/__snapshots__/NewUser.spec.js.snap +0 -321
  64. package/src/users/components/__tests__/__snapshots__/Password.spec.js.snap +0 -128
  65. package/src/users/components/__tests__/__snapshots__/User.spec.js.snap +0 -129
  66. package/src/users/components/__tests__/__snapshots__/UserPassword.spec.js.snap +0 -124
@@ -0,0 +1,109 @@
1
+ import PropTypes from "prop-types";
2
+ import { Icon } from "semantic-ui-react";
3
+ import { useIntl } from "react-intl";
4
+
5
+ export const userTypeOptions = [
6
+ { value: "admin", messageId: "user.type.admin", icon: "shield" },
7
+ { value: "user", messageId: "user.type.user", icon: "user" },
8
+ { value: "service", messageId: "user.type.service", icon: "settings" },
9
+ { value: "agent", messageId: "user.type.agent", icon: "send" },
10
+ ];
11
+
12
+ const UserTypeSelector = ({
13
+ id,
14
+ label,
15
+ value,
16
+ onChange,
17
+ onBlur,
18
+ required,
19
+ disabled,
20
+ }) => {
21
+ const { formatMessage } = useIntl();
22
+
23
+ return (
24
+ <div className={`field ${required ? "required" : ""}`}>
25
+ <label id={`${id}-label`} htmlFor={id}>
26
+ {label}
27
+ </label>
28
+ <div
29
+ id={id}
30
+ className="um-user-type-selector"
31
+ role="radiogroup"
32
+ aria-labelledby={`${id}-label`}
33
+ >
34
+ {userTypeOptions.map(({ value: typeValue, messageId, icon }) => {
35
+ const isSelected = value === typeValue;
36
+
37
+ return (
38
+ <button
39
+ key={typeValue}
40
+ type="button"
41
+ disabled={disabled}
42
+ tabIndex={isSelected ? 0 : -1}
43
+ onKeyDown={(event) => {
44
+ const keys = [
45
+ "ArrowRight",
46
+ "ArrowDown",
47
+ "ArrowLeft",
48
+ "ArrowUp",
49
+ "Home",
50
+ "End",
51
+ ];
52
+ if (!keys.includes(event.key)) return;
53
+ event.preventDefault();
54
+ const index = userTypeOptions.findIndex(
55
+ (option) => option.value === typeValue,
56
+ );
57
+ const next =
58
+ event.key === "Home"
59
+ ? 0
60
+ : event.key === "End"
61
+ ? userTypeOptions.length - 1
62
+ : (index +
63
+ (keys.indexOf(event.key) < 2 ? 1 : -1) +
64
+ userTypeOptions.length) %
65
+ userTypeOptions.length;
66
+ onChange(userTypeOptions[next].value);
67
+ event.currentTarget.parentElement
68
+ .querySelectorAll("button")
69
+ [next].focus();
70
+ }}
71
+ className={`um-user-type-button ${
72
+ isSelected ? "um-user-type-button--active" : ""
73
+ }`}
74
+ role="radio"
75
+ aria-checked={isSelected}
76
+ aria-label={formatMessage({ id: messageId })}
77
+ onClick={() => onChange(typeValue)}
78
+ onBlur={onBlur}
79
+ >
80
+ <Icon name={icon} />
81
+ <span>{formatMessage({ id: messageId })}</span>
82
+ </button>
83
+ );
84
+ })}
85
+ </div>
86
+ </div>
87
+ );
88
+ };
89
+
90
+ UserTypeSelector.propTypes = {
91
+ id: PropTypes.string,
92
+ label: PropTypes.string,
93
+ onBlur: PropTypes.func,
94
+ onChange: PropTypes.func,
95
+ required: PropTypes.bool,
96
+ disabled: PropTypes.bool,
97
+ value: PropTypes.string,
98
+ };
99
+
100
+ UserTypeSelector.defaultProps = {
101
+ id: "role",
102
+ label: "",
103
+ onBlur: () => {},
104
+ onChange: () => {},
105
+ required: false,
106
+ value: "user",
107
+ };
108
+
109
+ export default UserTypeSelector;
@@ -1,15 +1,37 @@
1
- import { Route, Routes } from "react-router";
2
1
  import { Header, Icon, Segment } from "semantic-ui-react";
3
2
  import { FormattedMessage, useIntl } from "react-intl";
4
- import { usePath } from "@truedat/core/hooks";
3
+ import { useLocation, useNavigate, useParams } from "react-router";
4
+ import { Loader } from "@truedat/core/router";
5
5
  import { GROUPS, USERS } from "@truedat/core/routes";
6
6
  import { GroupsList } from "../../groups/components/GroupCards";
7
+ import UserLoader from "./UserLoader";
7
8
  import UserCards from "./UserCards";
8
9
  import UserTabs from "./UserTabs";
10
+ import UserInfoModal from "./UserInfoModal";
11
+ import EditUserModal from "./EditUserModal";
12
+ import UserPasswordModal from "./UserPasswordModal";
13
+ import NewUser from "./NewUser";
14
+
15
+ const getModalType = (pathname, id) => {
16
+ if (pathname.endsWith("/new")) return "new";
17
+ if (!id) return null;
18
+ if (pathname.endsWith("/edit")) return "edit";
19
+ if (pathname.endsWith("/password")) return "password";
20
+ return "info";
21
+ };
9
22
 
10
23
  export const UsersAndGroups = () => {
11
24
  const { formatMessage } = useIntl();
12
- const path = usePath();
25
+ const { pathname } = useLocation();
26
+ const navigate = useNavigate();
27
+ const { id } = useParams();
28
+ const modalType = getModalType(pathname, id);
29
+ const isUsersPath = pathname === USERS || pathname.startsWith(USERS + "/");
30
+ const isGroupsPath = pathname === GROUPS || pathname.startsWith(GROUPS + "/");
31
+ const onClose = () => navigate(USERS);
32
+
33
+ const isNewModal = modalType === "new";
34
+ const isUserModal = Boolean(isUsersPath && (id || isNewModal));
13
35
 
14
36
  return (
15
37
  <Segment>
@@ -30,9 +52,21 @@ export const UsersAndGroups = () => {
30
52
  </Header>
31
53
  <UserTabs />
32
54
  <Segment attached="bottom">
33
- {path === USERS && <UserCards />}
34
- {(path === GROUPS || path.startsWith(`${GROUPS}/`)) && <GroupsList />}
55
+ {isUsersPath && <UserCards />}
56
+ {isGroupsPath && <GroupsList />}
35
57
  </Segment>
58
+
59
+ {id && isUserModal && <Loader loaders={<UserLoader />} />}
60
+ {isUserModal && modalType === "new" && <NewUser onClose={onClose} />}
61
+ {isUserModal && modalType === "info" && (
62
+ <UserInfoModal onClose={onClose} />
63
+ )}
64
+ {isUserModal && modalType === "edit" && (
65
+ <EditUserModal onClose={onClose} />
66
+ )}
67
+ {isUserModal && modalType === "password" && (
68
+ <UserPasswordModal onClose={onClose} />
69
+ )}
36
70
  </Segment>
37
71
  );
38
72
  };
@@ -0,0 +1,234 @@
1
+ import { useState } from "react";
2
+ import PropTypes from "prop-types";
3
+ import { Link } from "react-router";
4
+ import { useIntl } from "react-intl";
5
+ import { Button, Icon, Table } from "semantic-ui-react";
6
+ import { linkTo } from "@truedat/core/routes";
7
+ import { lowerDeburrTrim } from "@truedat/core/services/sort";
8
+ import { DeleteUserModal } from "./UserActions";
9
+ import { userTypeOptions } from "./UserTypeSelector";
10
+
11
+ const columns = [
12
+ "type",
13
+ "name",
14
+ "external_id",
15
+ "email",
16
+ "groups",
17
+ "roles",
18
+ "actions",
19
+ ];
20
+
21
+ export default function UsersTable({ users, sort, onSort }) {
22
+ const { formatMessage } = useIntl();
23
+ const [deleting, setDeleting] = useState(null);
24
+ const message = (key) => formatMessage({ id: `users.table.${key}` });
25
+ const sortValue = (user) => {
26
+ if (sort.column === "groups") return user.groups?.length || 0;
27
+ if (sort.column === "type")
28
+ return lowerDeburrTrim(
29
+ formatMessage({
30
+ id: `user.type.${user.role}`,
31
+ defaultMessage: user.role || "",
32
+ }),
33
+ );
34
+ return lowerDeburrTrim(
35
+ sort.column === "name"
36
+ ? user.full_name?.trim() || user.user_name
37
+ : user[sort.column],
38
+ );
39
+ };
40
+ const sorted = [...users].sort((a, b) => {
41
+ const left = sortValue(a);
42
+ const right = sortValue(b);
43
+ const order =
44
+ typeof left === "number" ? left - right : left.localeCompare(right);
45
+ return (
46
+ (sort.ascending ? order : -order) ||
47
+ String(a.id).localeCompare(String(b.id))
48
+ );
49
+ });
50
+ return (
51
+ <>
52
+ <div
53
+ className="users-table-scroll"
54
+ role="region"
55
+ aria-label={message("view")}
56
+ tabIndex={0}
57
+ >
58
+ <Table unstackable className="users-table">
59
+ <Table.Header>
60
+ <Table.Row>
61
+ {columns.map((column) => (
62
+ <Table.HeaderCell
63
+ key={column}
64
+ scope="col"
65
+ textAlign={
66
+ column === "actions"
67
+ ? "right"
68
+ : column === "type"
69
+ ? "center"
70
+ : undefined
71
+ }
72
+ aria-sort={
73
+ sort.column === column
74
+ ? sort.ascending
75
+ ? "ascending"
76
+ : "descending"
77
+ : undefined
78
+ }
79
+ >
80
+ {["roles", "actions"].includes(column) ? (
81
+ message(column)
82
+ ) : (
83
+ <button
84
+ type="button"
85
+ className="users-table-sort"
86
+ onClick={() =>
87
+ onSort({
88
+ column,
89
+ ascending:
90
+ sort.column === column ? !sort.ascending : true,
91
+ })
92
+ }
93
+ >
94
+ {message(column)}
95
+ <Icon
96
+ className={
97
+ sort.column === column ? "" : "users-sort-hidden"
98
+ }
99
+ name={sort.ascending ? "angle up" : "angle down"}
100
+ aria-hidden="true"
101
+ />
102
+ </button>
103
+ )}
104
+ </Table.HeaderCell>
105
+ ))}
106
+ </Table.Row>
107
+ </Table.Header>
108
+ <Table.Body>
109
+ {sorted.map((user) => {
110
+ const name = user.full_name?.trim() || user.user_name;
111
+ const type = userTypeOptions.find(
112
+ (option) => option.value === user.role,
113
+ );
114
+ const typeLabel = formatMessage({
115
+ id: `user.type.${user.role}`,
116
+ defaultMessage: user.role || message("unknown"),
117
+ });
118
+ const roles =
119
+ Array.isArray(user.acls) &&
120
+ user.acls.every(
121
+ (acl) => acl?.role?.id != null || acl?.role?.name,
122
+ )
123
+ ? new Set(
124
+ user.acls.map(({ role }) =>
125
+ role.id != null ? `id:${role.id}` : `name:${role.name}`,
126
+ ),
127
+ ).size
128
+ : null;
129
+ return (
130
+ <Table.Row key={user.id}>
131
+ <Table.Cell
132
+ textAlign="center"
133
+ verticalAlign="middle"
134
+ className="users-table-type"
135
+ >
136
+ <span title={typeLabel} role="img" aria-label={typeLabel}>
137
+ <Icon
138
+ name={type?.icon || "question circle"}
139
+ aria-hidden="true"
140
+ />
141
+ </span>
142
+ </Table.Cell>
143
+ <Table.Cell>
144
+ <Link
145
+ className="users-table-name"
146
+ to={linkTo.USER({ id: user.id })}
147
+ >
148
+ {name}
149
+ </Link>
150
+ {user.full_name?.trim() && user.user_name && (
151
+ <span className="users-table-username">
152
+ {user.user_name}
153
+ </span>
154
+ )}
155
+ </Table.Cell>
156
+ <Table.Cell>{user.external_id || "—"}</Table.Cell>
157
+ <Table.Cell>{user.email || "—"}</Table.Cell>
158
+ <Table.Cell>
159
+ {user.groups?.length ? (
160
+ <span
161
+ aria-label={`${message("groups")}: ${user.groups.length}`}
162
+ >
163
+ <Icon name="users" aria-hidden="true" />
164
+ {user.groups.length}
165
+ </span>
166
+ ) : (
167
+ "—"
168
+ )}
169
+ </Table.Cell>
170
+ <Table.Cell>
171
+ {roles === null ? (
172
+ message("unknown")
173
+ ) : roles ? (
174
+ <span aria-label={`${message("roles")}: ${roles}`}>
175
+ <Icon name="shield" aria-hidden="true" />
176
+ {roles}
177
+ </span>
178
+ ) : (
179
+ "—"
180
+ )}
181
+ </Table.Cell>
182
+ <Table.Cell textAlign="right" className="users-table-actions">
183
+ <Button
184
+ as={Link}
185
+ role="link"
186
+ to={linkTo.USER({ id: user.id })}
187
+ icon="eye"
188
+ basic
189
+ aria-label={message("show")}
190
+ title={message("show")}
191
+ />
192
+ <Button
193
+ as={Link}
194
+ role="link"
195
+ to={linkTo.USER_EDIT({ id: user.id })}
196
+ icon="pencil"
197
+ basic
198
+ aria-label={message("edit")}
199
+ title={message("edit")}
200
+ />
201
+ <Button
202
+ icon="trash alternate outline"
203
+ basic
204
+ color="red"
205
+ aria-label={message("delete")}
206
+ title={message("delete")}
207
+ onClick={() => setDeleting(user)}
208
+ />
209
+ </Table.Cell>
210
+ </Table.Row>
211
+ );
212
+ })}
213
+ </Table.Body>
214
+ </Table>
215
+ </div>
216
+ {deleting && (
217
+ <DeleteUserModal
218
+ id={deleting.id}
219
+ name={deleting.full_name || deleting.user_name}
220
+ onClose={() => setDeleting(null)}
221
+ />
222
+ )}
223
+ </>
224
+ );
225
+ }
226
+
227
+ UsersTable.propTypes = {
228
+ users: PropTypes.array.isRequired,
229
+ sort: PropTypes.shape({
230
+ column: PropTypes.string.isRequired,
231
+ ascending: PropTypes.bool.isRequired,
232
+ }).isRequired,
233
+ onSort: PropTypes.func.isRequired,
234
+ };
@@ -1,18 +1,92 @@
1
- import { render, waitForLoad } from "@truedat/test/render";
1
+ import { act, fireEvent, waitFor } from "@testing-library/react";
2
+ import { Provider } from "react-redux";
3
+ import { createStore, combineReducers, applyMiddleware } from "redux";
4
+ import { Route, Routes } from "react-router";
5
+ import { render } from "@truedat/test/render";
6
+ import { userModalOperation } from "../../reducers/userModalOperation";
7
+ import { updateUser } from "../../routines";
2
8
  import EditUser from "../EditUser";
3
9
 
4
- const renderOpts = {
5
- state: {
6
- user: { id: 123, user_name: "fred", full_name: "fredrik foobar" },
7
- },
10
+ jest.mock("../../hooks/useSortedGroups", () => ({
11
+ __esModule: true,
12
+ default: () => [],
13
+ }));
14
+
15
+ const account = {
16
+ id: 123,
17
+ user_name: "test.user",
18
+ full_name: "Test User",
19
+ role: "user",
20
+ };
21
+
22
+ const routes = (
23
+ <Routes>
24
+ <Route path="/users/:id/edit" element={<EditUser />} />
25
+ <Route path="/users" element={<div>User list</div>} />
26
+ </Routes>
27
+ );
28
+
29
+ const renderOptions = {
30
+ state: { user: account },
31
+ routes: ["/users/123/edit"],
8
32
  };
9
33
 
10
34
  describe("<EditUser />", () => {
11
- beforeEach(() => jest.useRealTimers());
35
+ it("shows the selected Redux user in the shared modal and cancels to the list", async () => {
36
+ const dispatch = jest.fn();
37
+ const rendered = render(routes, { ...renderOptions, dispatch });
38
+ expect(rendered.getByRole("dialog")).toHaveClass("user-modal--edit");
39
+ expect(document.getElementById("full_name")).toHaveValue(account.full_name);
40
+ fireEvent.click(rendered.getByRole("button", { name: "actions.cancel" }));
41
+ expect(await rendered.findByText("User list")).toBeVisible();
42
+ expect(dispatch).not.toHaveBeenCalledWith(
43
+ expect.objectContaining({ type: updateUser.TRIGGER }),
44
+ );
45
+ });
12
46
 
13
- it("matches the latest snapshot", async () => {
14
- const rendered = render(<EditUser />, renderOpts);
15
- await waitForLoad(rendered);
16
- expect(rendered.container).toMatchSnapshot();
47
+ it("retains errors and closes only after its own successful update", async () => {
48
+ const actions = [];
49
+ const store = createStore(
50
+ combineReducers({ user: () => account, userModalOperation }),
51
+ applyMiddleware(() => (next) => (action) => {
52
+ actions.push(action);
53
+ return next(action);
54
+ }),
55
+ );
56
+ const rendered = render(
57
+ <Provider store={store}>{routes}</Provider>,
58
+ renderOptions,
59
+ );
60
+ fireEvent.change(document.getElementById("full_name"), {
61
+ target: { value: "Updated User" },
62
+ });
63
+ fireEvent.submit(document.getElementById("edit-user-form"));
64
+ await waitFor(() =>
65
+ expect(
66
+ actions.filter(({ type }) => type === updateUser.TRIGGER),
67
+ ).toHaveLength(1),
68
+ );
69
+ const request = actions.find(({ type }) => type === updateUser.TRIGGER);
70
+ expect(request.payload).toEqual(
71
+ expect.objectContaining({ id: account.id, full_name: "Updated User" }),
72
+ );
73
+ act(() =>
74
+ store.dispatch({ ...updateUser.failure("error"), meta: request.meta }),
75
+ );
76
+ expect(rendered.getByRole("dialog")).toBeVisible();
77
+ expect(document.getElementById("full_name")).toHaveValue("Updated User");
78
+ fireEvent.submit(document.getElementById("edit-user-form"));
79
+ await waitFor(() =>
80
+ expect(
81
+ actions.filter(({ type }) => type === updateUser.TRIGGER),
82
+ ).toHaveLength(2),
83
+ );
84
+ const retry = actions.filter(({ type }) => type === updateUser.TRIGGER)[1];
85
+ act(() =>
86
+ store.dispatch({ ...updateUser.success({}), meta: request.meta }),
87
+ );
88
+ expect(rendered.getByRole("dialog")).toBeVisible();
89
+ act(() => store.dispatch({ ...updateUser.success({}), meta: retry.meta }));
90
+ expect(await rendered.findByText("User list")).toBeVisible();
17
91
  });
18
92
  });
@@ -1,10 +1,40 @@
1
1
  import { render, waitForLoad } from "@truedat/test/render";
2
- import NewUser from "../NewUser";
2
+ import { NewUser } from "../NewUser";
3
3
 
4
4
  describe("<NewUser />", () => {
5
- it("matches the latest snapshot", async () => {
6
- const rendered = render(<NewUser />);
5
+ const props = {
6
+ createUser: jest.fn(),
7
+ groups: [{ id: 1, name: "data", alias: "Data" }],
8
+ isSubmitting: false,
9
+ onClose: jest.fn(),
10
+ };
11
+
12
+ it("renders the new user modal fields", async () => {
13
+ const rendered = render(<NewUser {...props} />);
14
+ await waitForLoad(rendered);
15
+
16
+ expect(rendered.getByText(/users.actions.create/i)).toBeInTheDocument();
17
+ expect(
18
+ rendered.getByPlaceholderText(/user.form.placeholder.full_name/i),
19
+ ).toBeInTheDocument();
20
+ expect(
21
+ rendered.getByPlaceholderText(/user.form.placeholder.email/i),
22
+ ).toBeInTheDocument();
23
+ expect(
24
+ rendered.getByPlaceholderText(/user.form.placeholder.user_name/i),
25
+ ).toBeInTheDocument();
26
+ expect(rendered.baseElement.querySelector("#password")).toBeVisible();
27
+ expect(
28
+ rendered.getByLabelText(/user.form.password_confirmation/i),
29
+ ).toBeInTheDocument();
30
+ });
31
+
32
+ it("disables creation until the form is valid", async () => {
33
+ const rendered = render(<NewUser {...props} />);
7
34
  await waitForLoad(rendered);
8
- expect(rendered.container).toMatchSnapshot();
35
+
36
+ expect(
37
+ rendered.getByRole("button", { name: /user.create.submit/i }),
38
+ ).toBeDisabled();
9
39
  });
10
40
  });