@truedat/ai 6.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (27) hide show
  1. package/LICENSE +685 -0
  2. package/README.md +13 -0
  3. package/package.json +101 -0
  4. package/src/api.js +6 -0
  5. package/src/components/AiRoutes.js +24 -0
  6. package/src/components/constants.js +20 -0
  7. package/src/components/index.js +2 -0
  8. package/src/components/prompts/PromptEditor.js +365 -0
  9. package/src/components/prompts/Prompts.js +136 -0
  10. package/src/components/prompts/__tests__/Prompt.spec.js +77 -0
  11. package/src/components/prompts/__tests__/PromptEditor.spec.js +208 -0
  12. package/src/components/prompts/__tests__/__snapshots__/Prompt.spec.js.snap +77 -0
  13. package/src/components/prompts/__tests__/__snapshots__/PromptEditor.spec.js.snap +896 -0
  14. package/src/components/resourceMappings/ResourceMappingEditor.js +203 -0
  15. package/src/components/resourceMappings/ResourceMappingFields.js +119 -0
  16. package/src/components/resourceMappings/ResourceMappings.js +140 -0
  17. package/src/components/resourceMappings/__tests__/ResourceMappingEditor.spec.js +204 -0
  18. package/src/components/resourceMappings/__tests__/ResourceMappings.spec.js +79 -0
  19. package/src/components/resourceMappings/__tests__/__snapshots__/ResourceMappingEditor.spec.js.snap +748 -0
  20. package/src/components/resourceMappings/__tests__/__snapshots__/ResourceMappings.spec.js.snap +77 -0
  21. package/src/components/resourceMappings/selectors/DataStructureSelector.js +80 -0
  22. package/src/components/resourceMappings/selectors/index.js +11 -0
  23. package/src/hooks/__tests__/usePrompts.spec.js +101 -0
  24. package/src/hooks/__tests__/useResourceMappings.spec.js +101 -0
  25. package/src/hooks/usePrompts.js +31 -0
  26. package/src/hooks/useResourceMappings.js +33 -0
  27. package/src/index.js +3 -0
@@ -0,0 +1,203 @@
1
+ import React, { Fragment, useEffect } from "react";
2
+ import PropTypes from "prop-types";
3
+ import { useIntl } from "react-intl";
4
+ import {
5
+ Button,
6
+ Container,
7
+ Divider,
8
+ Grid,
9
+ Header,
10
+ Form,
11
+ Dropdown,
12
+ } from "semantic-ui-react";
13
+ import { FormProvider, useForm, Controller } from "react-hook-form";
14
+ import { ConfirmModal } from "@truedat/core/components";
15
+ import { useResourceTypeOptions } from "../constants";
16
+ import { selectorFor } from "./selectors";
17
+ import ResourceMappingFields from "./ResourceMappingFields";
18
+
19
+ export default function ResourceMappingEditor({
20
+ selectedResourceMapping,
21
+ onSubmit,
22
+ onCancel,
23
+ onDelete,
24
+ isSubmitting,
25
+ setDirty,
26
+ }) {
27
+ const { formatMessage } = useIntl();
28
+ const resourceTypeOptions = useResourceTypeOptions();
29
+ const form = useForm({
30
+ mode: "onTouched",
31
+ defaultValues: selectedResourceMapping,
32
+ });
33
+ const { control, handleSubmit, watch, formState } = form;
34
+ const { isDirty, isValid } = formState;
35
+
36
+ useEffect(() => {
37
+ setDirty(isDirty);
38
+ }, [setDirty, isDirty]);
39
+
40
+ if (!selectedResourceMapping) return null;
41
+
42
+ const name =
43
+ watch("name") ||
44
+ formatMessage({
45
+ id: "resourceMappings.form.name.new",
46
+ });
47
+ const resourceType = watch("resource_type");
48
+
49
+ return (
50
+ <Fragment>
51
+ <FormProvider {...form}>
52
+ <Form>
53
+ <Header as="h3" dividing>
54
+ {name}
55
+ </Header>
56
+ <Controller
57
+ control={control}
58
+ name="name"
59
+ rules={{
60
+ required: formatMessage(
61
+ { id: "form.validation.required" },
62
+ {
63
+ prop: formatMessage({
64
+ id: "resourceMappings.form.name",
65
+ }),
66
+ }
67
+ ),
68
+ }}
69
+ render={({
70
+ field: { onBlur, onChange, value },
71
+ fieldState: { error },
72
+ }) => (
73
+ <Form.Input
74
+ autoComplete="off"
75
+ placeholder={formatMessage({
76
+ id: "resourceMappings.form.name",
77
+ })}
78
+ error={error?.message}
79
+ label={formatMessage({
80
+ id: "resourceMappings.form.name",
81
+ })}
82
+ onBlur={onBlur}
83
+ onChange={(_e, { value }) => onChange(value)}
84
+ value={value}
85
+ required
86
+ />
87
+ )}
88
+ />
89
+ <Controller
90
+ control={control}
91
+ name="resource_type"
92
+ rules={{
93
+ required: formatMessage(
94
+ { id: "form.validation.required" },
95
+ {
96
+ prop: formatMessage({
97
+ id: "resourceMappings.form.resource_type",
98
+ }),
99
+ }
100
+ ),
101
+ }}
102
+ render={({ field: { onBlur, onChange, value } }) => (
103
+ <Form.Field required>
104
+ <label>
105
+ {formatMessage({
106
+ id: "resourceMappings.form.resource_type",
107
+ })}
108
+ </label>
109
+ <Dropdown
110
+ selection
111
+ onBlur={onBlur}
112
+ options={resourceTypeOptions}
113
+ onChange={(_e, { value }) => onChange(value)}
114
+ value={value}
115
+ />
116
+ </Form.Field>
117
+ )}
118
+ />
119
+
120
+ <Header as="h5" dividing>
121
+ {formatMessage({ id: "resourceMappings.form.selector" })}
122
+ </Header>
123
+ <Grid>
124
+ <Grid.Row>
125
+ <Grid.Column></Grid.Column>
126
+ <Grid.Column width={15}>{selectorFor(resourceType)}</Grid.Column>
127
+ </Grid.Row>
128
+ </Grid>
129
+
130
+ <Header as="h5" dividing>
131
+ {formatMessage({ id: "resourceMappings.form.fields" })}
132
+ </Header>
133
+ <ResourceMappingFields />
134
+
135
+ <Divider hidden />
136
+ <Container textAlign="right">
137
+ <Button
138
+ onClick={handleSubmit(onSubmit)}
139
+ primary
140
+ loading={isSubmitting}
141
+ disabled={!isValid || !isDirty}
142
+ content={formatMessage({ id: "actions.save" })}
143
+ />
144
+ {isDirty ? (
145
+ <ConfirmModal
146
+ trigger={
147
+ <Button
148
+ content={formatMessage({ id: "actions.cancel" })}
149
+ disabled={isSubmitting}
150
+ />
151
+ }
152
+ header={formatMessage({
153
+ id: "actions.discard.confirmation.header",
154
+ })}
155
+ content={formatMessage({
156
+ id: "actions.discard.confirmation.content",
157
+ })}
158
+ onConfirm={onCancel}
159
+ onOpen={(e) => e.stopPropagation()}
160
+ onClose={(e) => e.stopPropagation()}
161
+ />
162
+ ) : (
163
+ <Button
164
+ content={formatMessage({ id: "actions.cancel" })}
165
+ disabled={isSubmitting}
166
+ onClick={onCancel}
167
+ />
168
+ )}
169
+ {onDelete ? (
170
+ <ConfirmModal
171
+ trigger={
172
+ <Button
173
+ color="red"
174
+ content={formatMessage({ id: "actions.delete" })}
175
+ disabled={isSubmitting}
176
+ />
177
+ }
178
+ header={formatMessage({
179
+ id: "functions.action.delete.header",
180
+ })}
181
+ content={formatMessage({
182
+ id: "functions.action.delete.content",
183
+ })}
184
+ onConfirm={onDelete}
185
+ onOpen={(e) => e.stopPropagation()}
186
+ onClose={(e) => e.stopPropagation()}
187
+ />
188
+ ) : null}
189
+ </Container>
190
+ </Form>
191
+ </FormProvider>
192
+ </Fragment>
193
+ );
194
+ }
195
+
196
+ ResourceMappingEditor.propTypes = {
197
+ selectedResourceMapping: PropTypes.object,
198
+ onSubmit: PropTypes.func,
199
+ onCancel: PropTypes.func,
200
+ onDelete: PropTypes.func,
201
+ isSubmitting: PropTypes.bool,
202
+ setDirty: PropTypes.func,
203
+ };
@@ -0,0 +1,119 @@
1
+ import React, { useState } from "react";
2
+ import PropTypes from "prop-types";
3
+ import { useIntl } from "react-intl";
4
+ import { Controller, useFormContext, useFieldArray } from "react-hook-form";
5
+ import { Button, Grid, Form, List } from "semantic-ui-react";
6
+
7
+ export default function ResourceMappingFields() {
8
+ const { formatMessage } = useIntl();
9
+ const { fields, append, remove } = useFieldArray({
10
+ name: "fields",
11
+ keyName: "key",
12
+ });
13
+
14
+ const newField = () => ({
15
+ source: "",
16
+ target: "",
17
+ });
18
+
19
+ return (
20
+ <List>
21
+ {fields.map((field, index) => (
22
+ <List.Item key={field.key}>
23
+ <FieldItem index={index} onDelete={() => remove(index)} />
24
+ </List.Item>
25
+ ))}
26
+ <List.Item>
27
+ <Button onClick={() => append(newField())}>
28
+ {formatMessage({ id: "resourceMappings.form.fields.add" })}
29
+ </Button>
30
+ </List.Item>
31
+ </List>
32
+ );
33
+ }
34
+
35
+ const FieldItem = ({ index, onDelete }) => {
36
+ const { formatMessage } = useIntl();
37
+ const { control } = useFormContext();
38
+ const [isShownDelete, setIsShownDelete] = useState();
39
+ return (
40
+ <div
41
+ onMouseEnter={() => setIsShownDelete(true)}
42
+ onMouseLeave={() => setIsShownDelete(false)}
43
+ >
44
+ <Grid>
45
+ <Grid.Row>
46
+ <Grid.Column></Grid.Column>
47
+ <Grid.Column width={15}>
48
+ <Grid.Row>
49
+ <Form.Group inline className="no-margin">
50
+ <Controller
51
+ control={control}
52
+ name={`fields[${index}].source`}
53
+ rules={{
54
+ required: formatMessage(
55
+ { id: "form.validation.required" },
56
+ {
57
+ prop: formatMessage({
58
+ id: "resourceMappings.form.fields.source",
59
+ }),
60
+ }
61
+ ),
62
+ }}
63
+ render={({
64
+ field: { onBlur, onChange, value },
65
+ fieldState: { error },
66
+ }) => (
67
+ <Form.Input
68
+ autoComplete="off"
69
+ placeholder={formatMessage({
70
+ id: "resourceMappings.form.fields.source",
71
+ })}
72
+ error={error?.message}
73
+ onBlur={onBlur}
74
+ onChange={(_e, { value }) => onChange(value)}
75
+ value={value}
76
+ />
77
+ )}
78
+ />
79
+ <Controller
80
+ control={control}
81
+ name={`fields[${index}].target`}
82
+ render={({
83
+ field: { onBlur, onChange, value },
84
+ fieldState: { error },
85
+ }) => (
86
+ <Form.Input
87
+ autoComplete="off"
88
+ placeholder={formatMessage({
89
+ id: "resourceMappings.form.fields.target",
90
+ })}
91
+ error={error?.message}
92
+ onBlur={onBlur}
93
+ onChange={(_e, { value }) => onChange(value)}
94
+ value={value || ""}
95
+ />
96
+ )}
97
+ />
98
+ {isShownDelete ? (
99
+ <Button
100
+ onClick={onDelete}
101
+ icon="trash alternate outline"
102
+ basic
103
+ color="red"
104
+ aria-label="delete"
105
+ />
106
+ ) : null}
107
+ </Form.Group>
108
+ </Grid.Row>
109
+ </Grid.Column>
110
+ </Grid.Row>
111
+ </Grid>
112
+ </div>
113
+ );
114
+ };
115
+
116
+ FieldItem.propTypes = {
117
+ index: PropTypes.number,
118
+ onDelete: PropTypes.func,
119
+ };
@@ -0,0 +1,140 @@
1
+ import _ from "lodash/fp";
2
+ import React, { useState } from "react";
3
+ import { useIntl, FormattedMessage } from "react-intl";
4
+ import {
5
+ Button,
6
+ Grid,
7
+ GridColumn,
8
+ Header,
9
+ Icon,
10
+ List,
11
+ Segment,
12
+ } from "semantic-ui-react";
13
+
14
+ import {
15
+ useResourceMappings,
16
+ useResourceMappingCreate,
17
+ useResourceMappingDelete,
18
+ useResourceMappingUpdate,
19
+ } from "@truedat/ai/hooks/useResourceMappings";
20
+ import ResourceMappingEditor from "./ResourceMappingEditor";
21
+
22
+ const NEW_RESOURCE_MAPPING = {
23
+ name: "",
24
+ resource_type: "data_structure",
25
+ fields: [],
26
+ selector: {},
27
+ };
28
+
29
+ export default function ResourceMappings() {
30
+ const { formatMessage } = useIntl();
31
+ const { data, loading, mutate } = useResourceMappings();
32
+ const [selectedResourceMapping, setSelectedResourceMapping] = useState();
33
+ const [isDirty, setDirty] = useState(false);
34
+
35
+ const resourceMappings = data?.data;
36
+
37
+ const setStateNewResourceMapping = () =>
38
+ setSelectedResourceMapping(NEW_RESOURCE_MAPPING);
39
+ const clearForm = () => {
40
+ setSelectedResourceMapping(null);
41
+ setDirty(false);
42
+ };
43
+
44
+ const { trigger: createResourceMapping, isMutating: isCreating } =
45
+ useResourceMappingCreate();
46
+
47
+ const { trigger: updateResourceMapping, isMutating: isUpdating } =
48
+ useResourceMappingUpdate(selectedResourceMapping);
49
+
50
+ const { trigger: deleteResourceMapping, isMutating: isDeleting } =
51
+ useResourceMappingDelete(selectedResourceMapping);
52
+
53
+ const isSubmitting = isCreating || isUpdating || isDeleting;
54
+ const onResourceMappingCreate = async (resource_mapping) => {
55
+ const mutateResourceMapping = selectedResourceMapping?.id
56
+ ? updateResourceMapping
57
+ : createResourceMapping;
58
+ await mutateResourceMapping({ resource_mapping });
59
+ clearForm();
60
+ mutate();
61
+ };
62
+ const onResourceMappingDelete = async (resource_mapping) => {
63
+ await deleteResourceMapping({ resource_mapping });
64
+ clearForm();
65
+ mutate();
66
+ };
67
+ return (
68
+ <Segment loading={loading}>
69
+ <Header as="h2">
70
+ <Icon circular name="map signs" />
71
+ <Header.Content>
72
+ <FormattedMessage id="resourceMappings.header" />
73
+ <Header.Subheader>
74
+ <FormattedMessage id="resourceMappings.subheader" />
75
+ </Header.Subheader>
76
+ </Header.Content>
77
+ </Header>
78
+
79
+ <Grid>
80
+ <GridColumn width={4}>
81
+ <Button fluid onClick={setStateNewResourceMapping} disabled={isDirty}>
82
+ {formatMessage({ id: "resourceMappings.action.new" })}
83
+ </Button>
84
+ <List divided selection={!isDirty}>
85
+ {!_.isEmpty(resourceMappings) ? (
86
+ resourceMappings.map((resourceMapping, key) => (
87
+ <List.Item
88
+ key={key}
89
+ onClick={() =>
90
+ !isDirty && setSelectedResourceMapping(resourceMapping)
91
+ }
92
+ >
93
+ <List.Content>
94
+ <List.Header>
95
+ {resourceMapping.name ||
96
+ formatMessage({
97
+ id: "resourceMappings.form.name.new",
98
+ })}
99
+ </List.Header>
100
+ </List.Content>
101
+ </List.Item>
102
+ ))
103
+ ) : (
104
+ <List.Item>
105
+ <List.Content>
106
+ <List.Header>
107
+ {formatMessage({ id: "resourceMappings.empty_list" })}
108
+ </List.Header>
109
+ </List.Content>
110
+ </List.Item>
111
+ )}
112
+ </List>
113
+ </GridColumn>
114
+ <GridColumn width={11}>
115
+ {selectedResourceMapping ? (
116
+ <ResourceMappingEditor
117
+ key={selectedResourceMapping?.id || "new"}
118
+ selectedResourceMapping={selectedResourceMapping}
119
+ resourceMappings={resourceMappings}
120
+ onCancel={clearForm}
121
+ onSubmit={onResourceMappingCreate}
122
+ onDelete={
123
+ selectedResourceMapping?.id ? onResourceMappingDelete : null
124
+ }
125
+ isSubmitting={isSubmitting}
126
+ setDirty={setDirty}
127
+ />
128
+ ) : (
129
+ <Header as="h2" icon textAlign="center">
130
+ <Icon name="hand pointer outline" />
131
+ <Header.Subheader>
132
+ {formatMessage({ id: "resourceMappings.no_selection" })}
133
+ </Header.Subheader>
134
+ </Header>
135
+ )}
136
+ </GridColumn>
137
+ </Grid>
138
+ </Segment>
139
+ );
140
+ }
@@ -0,0 +1,204 @@
1
+ import React from "react";
2
+ import { act } from "react-dom/test-utils";
3
+ import { waitFor } from "@testing-library/react";
4
+ import { render } from "@truedat/test/render";
5
+ import userEvent from "@testing-library/user-event";
6
+ import ResourceMappingEditor from "../ResourceMappingEditor";
7
+
8
+ const renderOpts = {
9
+ messages: {
10
+ en: {
11
+ "resourceMappings.resourceType.data_structure":
12
+ "resourceMappings.resourceType.data_structure",
13
+ "form.validation.required": "form.validation.required",
14
+ "resourceMappings.form.selector": "resourceMappings.form.selector",
15
+ "resourceMappings.form.fields": "resourceMappings.form.fields",
16
+ "actions.save": "actions.save",
17
+ "actions.cancel": "actions.cancel",
18
+ "actions.delete": "actions.delete",
19
+ "functions.action.delete.header": "functions.action.delete.header",
20
+ "functions.action.delete.content": "functions.action.delete.content",
21
+ "resourceMappings.form.name": "resourceMappings.form.name",
22
+ "resourceMappings.form.name": "resourceMappings.form.name",
23
+ "resourceMappings.form.resource_type":
24
+ "resourceMappings.form.resource_type",
25
+ "resourceMappings.form.fields.add": "resourceMappings.form.fields.add",
26
+ "resourceMappings.form.fields.source":
27
+ "resourceMappings.form.fields.source",
28
+ "form.validation.required": "form.validation.required",
29
+ "resourceMappings.form.fields.source":
30
+ "resourceMappings.form.fields.source",
31
+ "resourceMappings.form.fields.target":
32
+ "resourceMappings.form.fields.target",
33
+ "actions.discard.confirmation.content":
34
+ "actions.discard.confirmation.content",
35
+ "actions.discard.confirmation.header":
36
+ "actions.discard.confirmation.header",
37
+ "confirmation.yes": "confirmation.yes",
38
+ "confirmation.no": "confirmation.no",
39
+ },
40
+ },
41
+ fallback: "lazy",
42
+ };
43
+
44
+ const props = {
45
+ selectedResourceMapping: {
46
+ id: 1,
47
+ name: "rm1",
48
+ resource_type: "boolean",
49
+ fields: [{ source: "s1", target: "t1" }],
50
+ selector: { system_external_id: 1 },
51
+ },
52
+ resourceMappings: [
53
+ {
54
+ id: 1,
55
+ name: "rm1",
56
+ resource_type: "boolean",
57
+ fields: [{ source: "s1", target: "t1" }],
58
+ selector: { system_external_id: 1 },
59
+ },
60
+ ],
61
+ onSubmit: jest.fn(),
62
+ onCancel: jest.fn(),
63
+ onDelete: jest.fn(),
64
+ isSubmitting: false,
65
+ setDirty: jest.fn(),
66
+ };
67
+
68
+ describe("<ResourceMappingEditor />", () => {
69
+ it("matches the latest snapshot", async () => {
70
+ const { container } = render(
71
+ <ResourceMappingEditor {...props} />,
72
+ renderOpts
73
+ );
74
+ await act(async () => {
75
+ expect(container).toMatchSnapshot();
76
+ });
77
+ });
78
+
79
+ it("matches snapshot without selected resource mapping", async () => {
80
+ const props = { setDirty: jest.fn() };
81
+ const { container, queryByText } = render(
82
+ <ResourceMappingEditor {...props} />,
83
+ renderOpts
84
+ );
85
+ await waitFor(() => expect(queryByText(/lazy/i)).not.toBeInTheDocument());
86
+ expect(container).toMatchSnapshot();
87
+ });
88
+
89
+ it("matches snapshot without onDelete", async () => {
90
+ const thisProps = {
91
+ ...props,
92
+ onDelete: null,
93
+ };
94
+ const { container } = render(
95
+ <ResourceMappingEditor {...thisProps} />,
96
+ renderOpts
97
+ );
98
+
99
+ await waitFor(() => expect(container).toMatchSnapshot());
100
+ });
101
+
102
+ it("test cancel button", async () => {
103
+ const onCancel = jest.fn();
104
+ const thisProps = { ...props, onCancel };
105
+ const { getByRole } = render(
106
+ <ResourceMappingEditor {...thisProps} />,
107
+ renderOpts
108
+ );
109
+
110
+ userEvent.click(getByRole("button", { name: /cancel/i }));
111
+
112
+ await waitFor(() => expect(onCancel).toHaveBeenCalled());
113
+ });
114
+
115
+ it("test cancel button with confirm", async () => {
116
+ const onCancel = jest.fn();
117
+ const thisProps = { ...props, onCancel };
118
+ const { container, getByRole, getAllByRole } = render(
119
+ <ResourceMappingEditor {...thisProps} />,
120
+ renderOpts
121
+ );
122
+
123
+ userEvent.type(getAllByRole("textbox")[0], "name");
124
+
125
+ await waitFor(() =>
126
+ expect(getByRole("button", { name: /save/i })).toBeEnabled()
127
+ );
128
+
129
+ userEvent.click(getByRole("button", { name: /cancel/i }));
130
+ expect(onCancel).toHaveBeenCalledTimes(0);
131
+
132
+ userEvent.click(getByRole("button", { name: /modal-negative-action/i }));
133
+ expect(onCancel).toHaveBeenCalledTimes(0);
134
+
135
+ userEvent.click(getByRole("button", { name: /cancel/i }));
136
+ expect(onCancel).toHaveBeenCalledTimes(0);
137
+
138
+ expect(container).toMatchSnapshot();
139
+
140
+ userEvent.click(getByRole("button", { name: /modal-affirmative-action/i }));
141
+ expect(onCancel).toHaveBeenCalledTimes(1);
142
+ });
143
+
144
+ it("test delete button", async () => {
145
+ const onDelete = jest.fn();
146
+ const thisProps = { ...props, onDelete };
147
+ const { container, getByRole } = render(
148
+ <ResourceMappingEditor {...thisProps} />,
149
+ renderOpts
150
+ );
151
+
152
+ userEvent.click(getByRole("button", { name: /delete/i }));
153
+ expect(onDelete).toHaveBeenCalledTimes(0);
154
+
155
+ userEvent.click(getByRole("button", { name: /modal-negative-action/i }));
156
+ expect(onDelete).toHaveBeenCalledTimes(0);
157
+
158
+ userEvent.click(getByRole("button", { name: /delete/i }));
159
+ expect(onDelete).toHaveBeenCalledTimes(0);
160
+
161
+ await waitFor(() => expect(container).toMatchSnapshot());
162
+
163
+ userEvent.click(getByRole("button", { name: /modal-affirmative-action/i }));
164
+ expect(onDelete).toHaveBeenCalledTimes(1);
165
+ });
166
+
167
+ it("test submit", async () => {
168
+ const onSubmit = jest.fn();
169
+ const thisProps = { ...props, onSubmit };
170
+ const { getByRole, getAllByRole } = render(
171
+ <ResourceMappingEditor {...thisProps} />,
172
+ renderOpts
173
+ );
174
+
175
+ expect(getByRole("button", { name: /save/i })).toBeDisabled();
176
+
177
+ userEvent.type(getAllByRole("textbox")[0], "name");
178
+
179
+ await waitFor(() =>
180
+ expect(getByRole("button", { name: /save/i })).toBeEnabled()
181
+ );
182
+
183
+ userEvent.click(getByRole("button", { name: /save/i }));
184
+
185
+ await waitFor(() =>
186
+ expect(getByRole("button", { name: /save/i })).toBeEnabled()
187
+ );
188
+
189
+ expect(onSubmit.mock.calls[0][0]).toEqual({
190
+ fields: [
191
+ {
192
+ source: "s1",
193
+ target: "t1",
194
+ },
195
+ ],
196
+ id: 1,
197
+ name: "rm1name",
198
+ resource_type: "boolean",
199
+ selector: {
200
+ system_external_id: 1,
201
+ },
202
+ });
203
+ });
204
+ });