@truedat/df 7.12.5 → 7.12.7

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 (42) hide show
  1. package/package.json +4 -4
  2. package/src/components/DynamicFieldValue.js +1 -1
  3. package/src/components/DynamicFormViewer.js +4 -3
  4. package/src/components/DynamicFormWithTranslations.js +3 -3
  5. package/src/components/EditableDynamicFieldValue.js +1 -1
  6. package/src/components/FieldViewerValue.js +44 -3
  7. package/src/components/__tests__/FieldViewerValue.spec.js +10 -6
  8. package/src/components/__tests__/__snapshots__/FieldViewerValue.spec.js.snap +53 -0
  9. package/src/components/widgets/DynamicField.js +6 -62
  10. package/src/components/widgets/DynamicTableField.js +150 -0
  11. package/src/components/widgets/FieldByWidget.js +63 -0
  12. package/src/components/widgets/StandardDropdown.js +2 -2
  13. package/src/components/widgets/StringField.js +2 -1
  14. package/src/components/widgets/__tests__/DynamicField.spec.js +10 -1
  15. package/src/components/widgets/__tests__/DynamicTableField.spec.js +257 -0
  16. package/src/components/widgets/__tests__/__snapshots__/DynamicField.spec.js.snap +97 -0
  17. package/src/components/widgets/__tests__/__snapshots__/DynamicTableField.spec.js.snap +114 -0
  18. package/src/templates/components/templateForm/ActiveGroupForm.js +5 -16
  19. package/src/templates/components/templateForm/FieldDefinition.js +158 -0
  20. package/src/templates/components/templateForm/FieldForm.js +32 -135
  21. package/src/templates/components/templateForm/TableValuesForm.js +258 -0
  22. package/src/templates/components/templateForm/TemplateForm.js +43 -26
  23. package/src/templates/components/templateForm/ValuesConfiguration.js +67 -0
  24. package/src/templates/components/templateForm/ValuesField.js +60 -96
  25. package/src/templates/components/templateForm/ValuesListForm.js +1 -3
  26. package/src/templates/components/templateForm/__tests__/FieldDefinition.spec.js +227 -0
  27. package/src/templates/components/templateForm/__tests__/TableValuesForm.spec.js +215 -0
  28. package/src/templates/components/templateForm/__tests__/ValuesField.spec.js +28 -83
  29. package/src/templates/components/templateForm/__tests__/__snapshots__/ActiveGroupForm.spec.js.snap +17 -0
  30. package/src/templates/components/templateForm/__tests__/__snapshots__/FieldDefinition.spec.js.snap +443 -0
  31. package/src/templates/components/templateForm/__tests__/__snapshots__/FieldForm.spec.js.snap +51 -0
  32. package/src/templates/components/templateForm/__tests__/__snapshots__/TemplateForm.spec.js.snap +17 -0
  33. package/src/templates/components/templateForm/__tests__/__snapshots__/ValuesField.spec.js.snap +61 -387
  34. package/src/templates/components/templateForm/contentValidation.js +22 -3
  35. package/src/templates/components/templateForm/valueDefinitions.js +16 -2
  36. package/src/templates/components/templateForm/widgetDefinitions.js +28 -2
  37. package/src/templates/utils/__tests__/validateContent.spec.js +6 -6
  38. package/src/templates/utils/applyTemplate.js +72 -23
  39. package/src/templates/utils/filterValues.js +3 -3
  40. package/src/templates/utils/parseFieldOptions.js +73 -58
  41. package/src/templates/utils/parseGroups.js +47 -48
  42. package/src/templates/utils/validateContent.js +70 -25
@@ -3,20 +3,16 @@ import { useEffect } from "react";
3
3
  import PropTypes from "prop-types";
4
4
  import { Form, Segment } from "semantic-ui-react";
5
5
  import { useIntl } from "react-intl";
6
+ import { hasAiSuggestions, valueSegment, getKeyType } from "./valueDefinitions";
6
7
  import DefaultValue from "./DefaultValue";
7
8
  import ValuesSelector from "./ValuesSelector";
8
- import { valueSegment } from "./valueDefinitions";
9
-
10
- const searchableEnabledScopes = ["bg", "dq", "ie", "ri", "dd"];
9
+ import ValuesConfiguration from "./ValuesConfiguration";
11
10
 
12
11
  export const ValuesField = ({
13
12
  defaultField,
14
13
  field,
15
- fieldType,
16
- keyType,
17
14
  name,
18
15
  onChange,
19
- onSelectionChange,
20
16
  subscribableField,
21
17
  values,
22
18
  fieldNamePrefix,
@@ -25,9 +21,10 @@ export const ValuesField = ({
25
21
  const { formatMessage } = useIntl();
26
22
  const { ai_suggestion, editable = true, searchable = true } = field;
27
23
 
28
- const hasAiSuggestion =
29
- _.includes(fieldType)(["string", "enriched_text"]) &&
30
- _.includes(keyType)(["fixed", "fixed_tuple", undefined]);
24
+ const fieldType = field?.type;
25
+ const keyType = getKeyType(field);
26
+ const hasAiSuggestion = hasAiSuggestions(fieldType, keyType);
27
+
31
28
  useEffect(() => {
32
29
  if (!hasAiSuggestion)
33
30
  onChange(null, {
@@ -36,6 +33,19 @@ export const ValuesField = ({
36
33
  });
37
34
  }, [hasAiSuggestion]);
38
35
 
36
+ const changeValue = (name, value) =>
37
+ value == "null"
38
+ ? onChange(null, { name, value: null })
39
+ : onChange(null, {
40
+ name,
41
+ value: { [value]: null },
42
+ });
43
+
44
+ const onSelectionChange = (e, { value }) => {
45
+ onChange(e, { name: subscribableField, value: false });
46
+ changeValue(name, value);
47
+ };
48
+
39
49
  const valueTypes = (options) =>
40
50
  _.map(({ id, value }) => ({
41
51
  key: value || "null",
@@ -46,105 +56,59 @@ export const ValuesField = ({
46
56
  return field.cardinality == "0"
47
57
  ? null
48
58
  : valueSegment(values, keyType, fieldType) && (
49
- <Segment>
50
- {_.size(values) > 1 ? (
51
- <Form.Dropdown
52
- fluid
53
- selection
54
- label={formatMessage({ id: "template.field.values" })}
55
- onChange={onSelectionChange}
56
- value={keyType || "null"}
57
- required
58
- options={valueTypes(values)}
59
- />
60
- ) : null}
61
- {keyType ? (
62
- <ValuesSelector
63
- cardinality={field?.cardinality}
64
- defaultField={defaultField}
65
- defaultValue={field?.default}
66
- name={name}
67
- formatMessage={formatMessage}
68
- onChange={onChange}
69
- type={keyType}
70
- values={_.path(`values.${keyType}`)(field)}
71
- />
72
- ) : null}
73
- <Form.Group size="small" widths="equal">
74
- <Form.Field>
75
- <Form.Checkbox
76
- name={`${fieldNamePrefix}.editable`}
77
- label={formatMessage({
78
- id: "template.field.editable",
79
- defaultMessage: "Editable",
80
- })}
81
- checked={editable}
82
- onChange={(e, { name, checked: value }) =>
83
- onChange(null, { name, value })
84
- }
85
- />
86
- </Form.Field>
87
- {hasAiSuggestion ? (
88
- <Form.Field>
89
- <Form.Checkbox
90
- name={`${fieldNamePrefix}.ai_suggestion`}
91
- label={formatMessage({
92
- id: "template.field.ai_suggestion",
93
- defaultMessage: "AI Suggestions",
94
- })}
95
- checked={ai_suggestion}
96
- onChange={(_e, { name, checked: value }) =>
97
- onChange(null, { name, value })
98
- }
99
- />
100
- </Form.Field>
101
- ) : null}
102
- {_.includes(scope)(searchableEnabledScopes) ? (
103
- <Form.Field>
104
- <Form.Checkbox
105
- name={`${fieldNamePrefix}.searchable`}
106
- label={formatMessage({
107
- id: "template.field.searchable",
108
- defaultMessage: "Sercheable",
109
- })}
110
- checked={searchable}
111
- onChange={(_e, { name, checked: value }) =>
112
- onChange(null, { name, value })
113
- }
114
- />
115
- </Form.Field>
116
- ) : null}
117
- {_.includes(keyType)(["fixed", "fixed_tuple"]) ? (
118
- <Form.Checkbox
119
- name={subscribableField}
120
- label={formatMessage({ id: "template.field.subscribable" })}
121
- checked={_.prop("subscribable")(field) || false}
122
- onChange={(_e, { name, checked: value }) =>
123
- onChange(null, { name, value })
124
- }
125
- />
126
- ) : null}
127
- </Form.Group>
128
- <DefaultValue
59
+ <Segment>
60
+ {_.size(values) > 1 ? (
61
+ <Form.Dropdown
62
+ fluid
63
+ selection
64
+ label={formatMessage({ id: "template.field.values" })}
65
+ onChange={onSelectionChange}
66
+ value={keyType || "null"}
67
+ required
68
+ options={valueTypes(values)}
69
+ />
70
+ ) : null}
71
+ {keyType ? (
72
+ <ValuesSelector
73
+ cardinality={field?.cardinality}
129
74
  defaultField={defaultField}
130
- field={field}
131
- fieldType={fieldType}
75
+ defaultValue={field?.default}
76
+ name={name}
132
77
  formatMessage={formatMessage}
133
78
  onChange={onChange}
134
79
  type={keyType}
80
+ values={_.path(`values.${keyType}`)(field)}
135
81
  />
136
- </Segment>
137
- );
82
+ ) : null}
83
+ <ValuesConfiguration
84
+ aiSuggestion={ai_suggestion}
85
+ editable={editable}
86
+ keyType={keyType}
87
+ onChange={onChange}
88
+ searchable={searchable}
89
+ fieldNamePrefix={fieldNamePrefix}
90
+ hasAiSuggestion={hasAiSuggestion}
91
+ subscribableField={subscribableField}
92
+ subscribable={field?.subscribable}
93
+ scope={scope}
94
+ />
95
+ <DefaultValue
96
+ defaultField={defaultField}
97
+ field={field}
98
+ fieldType={fieldType}
99
+ formatMessage={formatMessage}
100
+ onChange={onChange}
101
+ type={keyType}
102
+ />
103
+ </Segment>
104
+ );
138
105
  };
139
106
 
140
107
  ValuesField.propTypes = {
141
108
  defaultField: PropTypes.string,
142
- keyType: PropTypes.string,
143
109
  field: PropTypes.object,
144
- fieldType: PropTypes.string,
145
110
  name: PropTypes.string,
146
111
  onChange: PropTypes.func.isRequired,
147
- onSelectionChange: PropTypes.func,
148
112
  subscribableField: PropTypes.string,
149
113
  values: PropTypes.array,
150
114
  fieldNamePrefix: PropTypes.string,
@@ -92,9 +92,6 @@ export class ValuesListForm extends Component {
92
92
  handleMoveDown = (key) => this.swapElements(key, key + 1);
93
93
 
94
94
  renderListContent = (value) => {
95
- const {
96
- intl: { formatMessage },
97
- } = this.props;
98
95
  if (this.props.type == "fixed") {
99
96
  return <List.Content>{value}</List.Content>;
100
97
  } else if (this.props.type == "table_columns") {
@@ -121,6 +118,7 @@ export class ValuesListForm extends Component {
121
118
  values,
122
119
  type,
123
120
  } = this.props;
121
+
124
122
  const { error } = this.state;
125
123
  const isNullOrEmpty = (value) =>
126
124
  !_.isNumber(value) && (!value || _.isEmpty(value));
@@ -0,0 +1,227 @@
1
+ // FieldDefinition.realDefs.test.tsx
2
+ import React from "react";
3
+ import { render, waitForLoad } from "@truedat/test/render";
4
+ import { within } from "@testing-library/react";
5
+ import userEvent from "@testing-library/user-event";
6
+ import FieldDefinition from "../FieldDefinition";
7
+
8
+ const mockFormatMessage = jest.fn(({ id }) => id);
9
+
10
+ jest.mock("react-intl", () => ({
11
+ ...jest.requireActual("react-intl"),
12
+ useIntl: () => ({
13
+ formatMessage: mockFormatMessage,
14
+ }),
15
+ }));
16
+
17
+ const user = userEvent.setup({ delay: null });
18
+
19
+ describe("FieldDefinition", () => {
20
+ const baseProps = {
21
+ fieldNamePrefix: "field",
22
+ defaultField: "field.default",
23
+ valueName: "field.value",
24
+ subscribableField: "field.subscribable",
25
+ widget: "string",
26
+ type: "string",
27
+ cardinality: "?",
28
+ onChange: jest.fn(),
29
+ allowedTypes: [],
30
+ allowedWidgets: [],
31
+ mandatory: {},
32
+ };
33
+
34
+ beforeEach(() => {
35
+ jest.clearAllMocks();
36
+ });
37
+
38
+ test("renders dropdowns with initial values", async () => {
39
+ const rendered = render(<FieldDefinition {...baseProps} onChange={jest.fn()} />);
40
+ waitForLoad(rendered);
41
+ expect(rendered.container).toMatchSnapshot();
42
+ const dropdowns = rendered.getAllByRole("listbox");
43
+ expect(dropdowns.length).toBe(3);
44
+
45
+ const widgetDropdown = dropdowns[0];
46
+ expect(widgetDropdown).toHaveAttribute("name", "field.widget");
47
+ const selectedWidget = within(widgetDropdown).getByRole("option", { name: "Text Input" });
48
+ expect(selectedWidget).toHaveAttribute("aria-checked", "true");
49
+
50
+ const typeDropdown = dropdowns[1];
51
+ expect(typeDropdown).toHaveAttribute("name", "field.type");
52
+ const selectedType = within(typeDropdown).getByRole("option", { name: "template.field.type.string" });
53
+ expect(selectedType).toHaveAttribute("aria-checked", "true");
54
+
55
+ const cardinalityDropdown = dropdowns[2];
56
+ expect(cardinalityDropdown).toHaveAttribute("name", "field.cardinality");
57
+ const selectedCardinality = within(cardinalityDropdown).getByRole("option", { name: "template.field.cardinality.?" });
58
+ expect(selectedCardinality).toHaveAttribute("aria-checked", "true");
59
+ });
60
+
61
+ test("handleWidgetChange: sets valueName to null (via getValues), resets default, sets subscribable=false, then emits type change", async () => {
62
+ const onChange = jest.fn();
63
+ const rendered = render(<FieldDefinition {...baseProps} onChange={onChange} />);
64
+ waitForLoad(rendered);
65
+
66
+ const numberOpt = within(rendered.getAllByRole("listbox")[0]).getByRole("option", { name: "Number" });
67
+ // Change type; for widget 'string' the only type is 'string', so switch the widget first to 'number'
68
+ await user.click(numberOpt);
69
+
70
+ // 1) reset value
71
+ expect(onChange).toHaveBeenNthCalledWith(
72
+ onChange.mock.calls.length - 3,
73
+ null,
74
+ { name: "field.value", value: null }
75
+ );
76
+
77
+ // // 2) reset defaultField
78
+ expect(onChange).toHaveBeenNthCalledWith(
79
+ onChange.mock.calls.length - 2,
80
+ expect.anything(),
81
+ { name: "field.default", value: { value: "", origin: "default" } }
82
+ );
83
+
84
+ // // 3) set subscribable=false
85
+ expect(onChange).toHaveBeenNthCalledWith(
86
+ onChange.mock.calls.length - 1,
87
+ expect.anything(),
88
+ { name: "field.subscribable", value: false }
89
+ );
90
+
91
+ // // 4) finally, the actual type widget event payload
92
+ expect(onChange).toHaveBeenLastCalledWith(
93
+ expect.anything(),
94
+ expect.objectContaining({ name: "field.widget", value: "number" })
95
+ );
96
+ });
97
+
98
+ test("handleTypeChange: recomputes default value via real getValues, resets default, sets subscribable=false, and emits widget change", async () => {
99
+ const onChange = jest.fn();
100
+ const rendered = render(<FieldDefinition {...{ ...baseProps, widget: "dropdown", type: "string" }} onChange={onChange} />);
101
+ waitForLoad(rendered);
102
+
103
+ // Switch Type from 'string' to 'domain'
104
+ const domainOpt = within(rendered.getAllByRole("listbox")[1]).getByRole("option", { name: "template.field.type.domain" });
105
+ await user.click(domainOpt);
106
+
107
+ // The first call in handleTypeChange is changeValue(valueName, ...) -> null
108
+ expect(onChange).toHaveBeenNthCalledWith(
109
+ 1,
110
+ null,
111
+ { name: "field.value", value: null }
112
+ );
113
+
114
+ // 2) default reset
115
+ expect(onChange).toHaveBeenNthCalledWith(
116
+ 2,
117
+ expect.anything(),
118
+ { name: "field.default", value: { value: "", origin: "default" } }
119
+ );
120
+
121
+ // 3) subscribable=false
122
+ expect(onChange).toHaveBeenNthCalledWith(
123
+ 3,
124
+ expect.anything(),
125
+ { name: "field.subscribable", value: false }
126
+ );
127
+
128
+ // 4) widget change payload
129
+ expect(onChange).toHaveBeenNthCalledWith(
130
+ 4,
131
+ expect.anything(),
132
+ expect.objectContaining({ name: "field.type", value: "domain" })
133
+ );
134
+ });
135
+
136
+ test("useEffect: auto-fixes invalid type/cardinality for a widget (real WIDGETS)", () => {
137
+ const onChange = jest.fn();
138
+
139
+ // For widget='number', valid types: ['integer','float']; cardinalities: ['?','1']
140
+ // Provide invalid type 'string' and invalid cardinality '+'
141
+ const rendered = render(<FieldDefinition {...baseProps}
142
+ onChange={onChange}
143
+ widget="number"
144
+ type="string"
145
+ cardinality="+" />);
146
+
147
+ waitForLoad(rendered);
148
+
149
+ // The effect should fire once and fix both:
150
+ expect(onChange).toHaveBeenCalledWith(
151
+ null,
152
+ { name: "field.type", value: "integer" } // first in WIDGETS types for 'number'
153
+ );
154
+ expect(onChange).toHaveBeenCalledWith(
155
+ null,
156
+ { name: "field.cardinality", value: "?" } // first in WIDGETS cardinalities for 'number'
157
+ );
158
+ });
159
+
160
+ test("handleCardinalityChange: selecting '1' (or '+') clears mandatory when it's non-empty", async () => {
161
+ const onChange = jest.fn();
162
+
163
+ const rendered = render(
164
+ <FieldDefinition
165
+ {...baseProps}
166
+ onChange={onChange}
167
+ widget="string"
168
+ type="string"
169
+ cardinality="?"
170
+ mandatory={{ some: "value" }}
171
+ />);
172
+
173
+ waitForLoad(rendered);
174
+
175
+ // Select exactly-one '1'
176
+ const cardOpt = within(rendered.getAllByRole("listbox")[2]).getByRole("option", { name: "template.field.cardinality.1" });
177
+ await user.click(cardOpt);
178
+
179
+ // First: direct change emission
180
+ expect(onChange).toHaveBeenNthCalledWith(
181
+ 1,
182
+ expect.anything(),
183
+ expect.objectContaining({ name: "field.cardinality", value: "1" })
184
+ );
185
+
186
+ // Then: clear mandatory because value ∈ ['1','+'] and mandatory was non-empty
187
+ expect(onChange).toHaveBeenNthCalledWith(
188
+ 2,
189
+ expect.anything(),
190
+ { name: "field.mandatory", value: null }
191
+ );
192
+ });
193
+
194
+ test("allowedWidgets & allowedTypes filter visible options (using real option generators)", () => {
195
+ const onChange = jest.fn();
196
+
197
+ const rendered = render(
198
+ <FieldDefinition
199
+ {...baseProps}
200
+ onChange={onChange}
201
+ allowedWidgets={["number"]}
202
+ allowedTypes={["float"]}
203
+ widget="number"
204
+ type="float"
205
+ />
206
+ );
207
+
208
+ waitForLoad(rendered);
209
+
210
+ const dropdowns = rendered.getAllByRole("listbox");
211
+ expect(dropdowns.length).toBe(3);
212
+
213
+ const widgetDropdown = dropdowns[0];
214
+ expect(widgetDropdown).toHaveAttribute("name", "field.widget");
215
+ const widgetOptions = within(widgetDropdown).getAllByRole("option");
216
+ expect(widgetOptions.length).toBe(1);
217
+ expect(widgetOptions[0]).toHaveAttribute("aria-checked", "true");
218
+ expect(widgetOptions[0]).toHaveTextContent("Number");
219
+
220
+ const typeDropdown = dropdowns[1];
221
+ expect(typeDropdown).toHaveAttribute("name", "field.type");
222
+ const typeOptions = within(typeDropdown).getAllByRole("option");
223
+ expect(typeOptions.length).toBe(1);
224
+ expect(typeOptions[0]).toHaveAttribute("aria-checked", "true");
225
+ expect(typeOptions[0]).toHaveTextContent("float");
226
+ });
227
+ });
@@ -0,0 +1,215 @@
1
+ import React from "react";
2
+ import { within } from "@testing-library/react";
3
+ import { render, waitForLoad } from "@truedat/test/render";
4
+ import userEvent from "@testing-library/user-event";
5
+ import TableValuesForm from "../TableValuesForm";
6
+
7
+ const mockFormatMessage = jest.fn(({ id }) => id);
8
+
9
+ jest.mock("react-intl", () => ({
10
+ ...jest.requireActual("react-intl"),
11
+ useIntl: () => ({
12
+ formatMessage: mockFormatMessage,
13
+ }),
14
+ }));
15
+
16
+ // --- Stub child components to isolate TableValuesForm behavior ---
17
+ jest.mock("../FieldDefinition", () => () => <div data-testid="FieldDefinition" />);
18
+ jest.mock("../ValuesField", () => () => <div data-testid="ValuesField" />);
19
+ jest.mock("../ValuesConfiguration", () => () => <div data-testid="ValuesConfiguration" />);
20
+
21
+ const user = userEvent.setup({ delay: null });
22
+
23
+ const values = [
24
+ { name: "colA", widget: "string", type: "string", errors: {} },
25
+ { name: "colB", widget: "string", type: "string", errors: { nameDuplicated: true, hasErrors: true } },
26
+ ];
27
+
28
+ const baseProps = {
29
+ name: "myField",
30
+ type: "table_columns", // used as the key in the onChange payload
31
+ values,
32
+ onChange: jest.fn(),
33
+ scope: "scope-x",
34
+ field: { values: { table_columns: values } },
35
+ fieldNamePrefix: "myPrefix",
36
+ };
37
+
38
+
39
+ describe("TableValuesForm", () => {
40
+ test("renders an Accordion with a panel per value, showing name and duplicated label", () => {
41
+ const rendered = render(<TableValuesForm {...baseProps} />);
42
+ waitForLoad(rendered);
43
+
44
+ // Two labels for two columns
45
+ expect(rendered.getByText("colA")).toBeInTheDocument();
46
+ expect(rendered.getByText("colB")).toBeInTheDocument();
47
+
48
+ // Duplicated name label for the second column only
49
+ // The label text comes from formatMessage
50
+ expect(rendered.getByText("template.form.validation.name_duplicated")).toBeInTheDocument();
51
+ });
52
+
53
+ test("clicking a panel title toggles it active and reveals its content", async () => {
54
+ const rendered = render(<TableValuesForm {...baseProps} />);
55
+ waitForLoad(rendered);
56
+
57
+ // Click on "colA" title to expand
58
+ await user.click(rendered.getByText("colA"));
59
+
60
+ // Content should render the stubs under the active panel
61
+ // (we don't assert which panel holds it; just that they are present)
62
+ expect(rendered.getAllByTestId("FieldDefinition").length).toBeGreaterThan(0);
63
+ expect(rendered.getAllByTestId("ValuesField").length).toBeGreaterThan(0);
64
+ });
65
+
66
+ test("move down moves the selected column forward and calls onChange with swapped array", async () => {
67
+ const rendered = render(<TableValuesForm {...baseProps} />);
68
+ waitForLoad(rendered);
69
+
70
+ // Expand panel 0 (colA)
71
+ await user.click(rendered.getByText("colA"));
72
+
73
+ // In the panel content, there is a Button.Group with three buttons (up, down, delete)
74
+ // We'll grab them by role=button within the expanded panel area.
75
+ // Since Semantic UI doesn't attach specific names by default to icon-only buttons,
76
+ // we'll pick by order: [up, down, delete].
77
+ const activePanel = rendered.getByText("colA").closest(".title")?.nextElementSibling;
78
+ expect(activePanel).toBeTruthy();
79
+
80
+ const buttons = within(activePanel).getAllByRole("button");
81
+ // up is disabled for index 0; down should move colA to index 1
82
+ const [, downButton] = buttons;
83
+ await user.click(downButton);
84
+
85
+ // Expect onChange called with swapped values for key "table_columns"
86
+ expect(baseProps.onChange).toHaveBeenCalledWith(
87
+ null,
88
+ expect.objectContaining({
89
+ name: "myField",
90
+ value: {
91
+ table_columns: [
92
+ expect.objectContaining({ name: "colB" }),
93
+ expect.objectContaining({ name: "colA" }),
94
+ ],
95
+ },
96
+ })
97
+ );
98
+ });
99
+
100
+ test("move up moves the selected column backward and calls onChange with swapped array", async () => {
101
+ const rendered = render(<TableValuesForm {...baseProps} />);
102
+ waitForLoad(rendered);
103
+
104
+ // Expand panel 1 (colB)
105
+ await user.click(rendered.getByText("colB"));
106
+
107
+ // Get the three buttons from this panel (up, down, delete)
108
+ const activePanel = rendered.getByText("colB").closest(".title")?.nextElementSibling;
109
+ expect(activePanel).toBeTruthy();
110
+
111
+ const buttons = within(activePanel).getAllByRole("button");
112
+ const [upButton] = buttons; // first button = up
113
+ await user.click(upButton);
114
+
115
+ // Expect swap: colB moves to index 0
116
+ expect(baseProps.onChange).toHaveBeenCalledWith(
117
+ null,
118
+ expect.objectContaining({
119
+ name: "myField",
120
+ value: {
121
+ table_columns: [
122
+ expect.objectContaining({ name: "colB" }),
123
+ expect.objectContaining({ name: "colA" }),
124
+ ],
125
+ },
126
+ })
127
+ );
128
+ });
129
+
130
+ test("delete removes the selected column and calls onChange with shortened array", async () => {
131
+ const rendered = render(<TableValuesForm {...baseProps} />);
132
+ waitForLoad(rendered);
133
+
134
+ // Expand panel 0 (colA)
135
+ await user.click(rendered.getByText("colA"));
136
+
137
+ const activePanel = rendered.getByText("colA").closest(".title")?.nextElementSibling;
138
+ const buttons = within(activePanel).getAllByRole("button");
139
+ const deleteButton = buttons[2]; // third button
140
+
141
+ await user.click(deleteButton);
142
+
143
+ expect(baseProps.onChange).toHaveBeenCalledWith(
144
+ null,
145
+ expect.objectContaining({
146
+ name: "myField",
147
+ value: {
148
+ table_columns: [
149
+ expect.objectContaining({ name: "colB" }), // only colB remains
150
+ ],
151
+ },
152
+ })
153
+ );
154
+ });
155
+
156
+ test("add column via Enter in input appends a default field with given name", async () => {
157
+ const rendered = render(<TableValuesForm {...baseProps} />);
158
+ waitForLoad(rendered);
159
+
160
+ // Input is at the bottom with an icon. We'll type and press Enter.
161
+ const input = rendered.getByRole("textbox");
162
+ await user.type(input, "newCol{enter}");
163
+
164
+ // The payload includes the new element at the end.
165
+ // It uses defaultFieldDefinition + overrides: name, label set to 'myField', ai_suggestion=false
166
+ expect(baseProps.onChange).toHaveBeenCalledWith(
167
+ null,
168
+ expect.objectContaining({
169
+ name: "myField",
170
+ value: {
171
+ table_columns: expect.arrayContaining([
172
+ expect.objectContaining({
173
+ name: "newCol",
174
+ ai_suggestion: false,
175
+ label: "newCol",
176
+ }),
177
+ ]),
178
+ },
179
+ })
180
+ );
181
+ });
182
+
183
+ test("add column via clicking the add icon triggers same append behavior", async () => {
184
+ const rendered = render(<TableValuesForm {...baseProps} />);
185
+ waitForLoad(rendered);
186
+
187
+ // Type value first so state.fieldValue has content
188
+ const input = rendered.getByRole("textbox");
189
+ await user.type(input, "anotherCol");
190
+
191
+ // Click the icon (it's rendered inside the input as a clickable Icon)
192
+ // Find by role button might not exist; the icon has role="img" with class "add circle".
193
+ // We'll click it via its container: the input's parent .input has an i.icon inside.
194
+ const inputWrapper = input.closest(".input");
195
+ const addIcon = inputWrapper?.querySelector(".icon");
196
+ expect(addIcon).toBeTruthy();
197
+ await user.click(addIcon);
198
+
199
+ expect(baseProps.onChange).toHaveBeenCalledWith(
200
+ null,
201
+ expect.objectContaining({
202
+ name: "myField",
203
+ value: {
204
+ table_columns: expect.arrayContaining([
205
+ expect.objectContaining({
206
+ name: "anotherCol",
207
+ ai_suggestion: false,
208
+ label: "anotherCol",
209
+ }),
210
+ ]),
211
+ },
212
+ })
213
+ );
214
+ });
215
+ });