@truedat/df 8.13.5 → 8.13.6
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/components/DynamicForm.js +3 -0
- package/src/components/DynamicFormWithTranslations.js +3 -0
- package/src/components/FieldGroupSegment.js +3 -0
- package/src/components/FieldGroupWithTranslations.js +13 -0
- package/src/components/SelectDynamicFormWithTranslations.js +7 -1
- package/src/components/SelectableDynamicForm.js +7 -1
- package/src/components/__tests__/__snapshots__/SelectableDynamicForm.spec.js.snap +18 -1
- package/src/components/__tests__/originalContentFlow.spec.js +131 -0
- package/src/components/widgets/DateField.js +18 -9
- package/src/components/widgets/DateTimeField.js +26 -9
- package/src/components/widgets/DynamicField.js +54 -1
- package/src/components/widgets/DynamicTableField.js +41 -7
- package/src/components/widgets/FieldByWidget.js +36 -6
- package/src/components/widgets/__tests__/dateBoundsByWidget.spec.js +133 -0
- package/src/components/widgets/__tests__/dateBoundsEndToEnd.spec.js +83 -0
- package/src/components/widgets/__tests__/datePickerBoundaries.spec.js +124 -0
- package/src/components/widgets/__tests__/dateValueValidation.spec.js +215 -0
- package/src/components/widgets/__tests__/existingDateRestrictions.spec.js +354 -0
- package/src/templates/components/templateForm/DateRestrictionsForm.js +217 -0
- package/src/templates/components/templateForm/DefaultValue.js +79 -11
- package/src/templates/components/templateForm/FieldDefinition.js +15 -1
- package/src/templates/components/templateForm/TemplateForm.js +5 -0
- package/src/templates/components/templateForm/ValuesField.js +7 -0
- package/src/templates/components/templateForm/__tests__/FieldDefinition.spec.js +48 -0
- package/src/templates/components/templateForm/__tests__/TemplateForm.spec.js +107 -0
- package/src/templates/components/templateForm/__tests__/dateRestrictionsForm.spec.js +429 -0
- package/src/templates/components/templateForm/__tests__/dateRestrictionsValidation.spec.js +166 -0
- package/src/templates/components/templateForm/__tests__/defaultValueBounds.spec.js +191 -0
- package/src/templates/components/templateForm/__tests__/defaultValueRestrictions.spec.js +175 -0
- package/src/templates/components/templateForm/contentValidation.js +184 -0
- package/src/templates/utils/__tests__/dateRestrictions.spec.js +578 -0
- package/src/templates/utils/__tests__/originalTableRows.spec.js +129 -0
- package/src/templates/utils/__tests__/validateContentDateRestrictions.spec.js +519 -0
- package/src/templates/utils/applyTemplate.js +27 -20
- package/src/templates/utils/dateRestrictions.js +497 -0
- package/src/templates/utils/dateValueUnchanged.js +12 -0
- package/src/templates/utils/originalTableRows.js +41 -0
- package/src/templates/utils/validateContent.js +75 -4
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
import PropTypes from "prop-types";
|
|
2
|
+
import { dateValueUnchanged } from "../../templates/utils/dateValueUnchanged";
|
|
3
|
+
import {
|
|
4
|
+
publishedBounds,
|
|
5
|
+
resolveRestrictions,
|
|
6
|
+
} from "../../templates/utils/dateRestrictions";
|
|
2
7
|
import DateTimeField from "./DateTimeField";
|
|
3
8
|
import CheckboxField from "./CheckboxField";
|
|
4
9
|
import ColorPickerField from "./ColorPickerField";
|
|
@@ -16,7 +21,16 @@ import TextField from "./TextField";
|
|
|
16
21
|
import NumberField from "./NumberField";
|
|
17
22
|
import DateField from "./DateField";
|
|
18
23
|
|
|
19
|
-
const
|
|
24
|
+
const boundsFor = (field, originalField) => {
|
|
25
|
+
const unchanged = dateValueUnchanged(field.value, originalField);
|
|
26
|
+
const bounds = unchanged
|
|
27
|
+
? {}
|
|
28
|
+
: publishedBounds(field?.restrictions) ||
|
|
29
|
+
resolveRestrictions(field?.restrictions, field?.type);
|
|
30
|
+
return { min: bounds.min, max: bounds.max };
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const FieldByWidget = ({ scope, widget, onChange, field, originalField }) => {
|
|
20
34
|
switch (widget) {
|
|
21
35
|
case "identifier":
|
|
22
36
|
return <IdentifierField field={field} />;
|
|
@@ -37,17 +51,32 @@ const FieldByWidget = ({ scope, widget, onChange, field }) => {
|
|
|
37
51
|
case "table":
|
|
38
52
|
return <TableField field={field} onChange={onChange} />;
|
|
39
53
|
case "dynamic_table":
|
|
40
|
-
return
|
|
54
|
+
return (
|
|
55
|
+
<DynamicTableField
|
|
56
|
+
field={field}
|
|
57
|
+
originalField={originalField}
|
|
58
|
+
onChange={onChange}
|
|
59
|
+
scope={scope}
|
|
60
|
+
/>
|
|
61
|
+
);
|
|
41
62
|
case "password":
|
|
42
63
|
return <PasswordField field={field} onChange={onChange} />;
|
|
43
64
|
case "image":
|
|
44
65
|
return <ImageField field={field} onChange={onChange} />;
|
|
45
66
|
case "number":
|
|
46
67
|
return <NumberField field={field} onChange={onChange} />;
|
|
47
|
-
case "date":
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
68
|
+
case "date": {
|
|
69
|
+
const { min, max } = boundsFor(field, originalField);
|
|
70
|
+
return (
|
|
71
|
+
<DateField field={field} onChange={onChange} min={min} max={max} />
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
case "datetime": {
|
|
75
|
+
const { min, max } = boundsFor(field, originalField);
|
|
76
|
+
return (
|
|
77
|
+
<DateTimeField field={field} onChange={onChange} min={min} max={max} />
|
|
78
|
+
);
|
|
79
|
+
}
|
|
51
80
|
default:
|
|
52
81
|
return <StringField field={field} onChange={onChange} />;
|
|
53
82
|
}
|
|
@@ -56,6 +85,7 @@ const FieldByWidget = ({ scope, widget, onChange, field }) => {
|
|
|
56
85
|
FieldByWidget.propTypes = {
|
|
57
86
|
onChange: PropTypes.func.isRequired,
|
|
58
87
|
field: PropTypes.object.isRequired,
|
|
88
|
+
originalField: PropTypes.object,
|
|
59
89
|
scope: PropTypes.string,
|
|
60
90
|
widget: PropTypes.string,
|
|
61
91
|
};
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { render, screen } from "@testing-library/react";
|
|
2
|
+
import { IntlProvider } from "react-intl";
|
|
3
|
+
import FieldByWidget from "../FieldByWidget";
|
|
4
|
+
|
|
5
|
+
const renderWith = (ui) =>
|
|
6
|
+
render(<IntlProvider locale="en">{ui}</IntlProvider>);
|
|
7
|
+
|
|
8
|
+
const fieldWith = (restrictions, extra = {}) => ({
|
|
9
|
+
name: "fecha",
|
|
10
|
+
value: "2026-06-15",
|
|
11
|
+
type: "date",
|
|
12
|
+
...extra,
|
|
13
|
+
restrictions,
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
describe("FieldByWidget passes the published bounds to date widgets", () => {
|
|
17
|
+
describe("a date field", () => {
|
|
18
|
+
it("reads min and max out of resolved", () => {
|
|
19
|
+
renderWith(
|
|
20
|
+
<FieldByWidget
|
|
21
|
+
widget="date"
|
|
22
|
+
onChange={() => {}}
|
|
23
|
+
field={fieldWith({
|
|
24
|
+
between: {
|
|
25
|
+
mode: "static",
|
|
26
|
+
start: "2026-01-01",
|
|
27
|
+
end: "2026-12-31",
|
|
28
|
+
},
|
|
29
|
+
resolved: { min: "2026-01-01", max: "2026-12-31" },
|
|
30
|
+
})}
|
|
31
|
+
/>,
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
const input = screen.getByPlaceholderText("Date");
|
|
35
|
+
expect(input).toHaveAttribute("min", "2026-01-01");
|
|
36
|
+
expect(input).toHaveAttribute("max", "2026-12-31");
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("resolves declared bounds when the serving endpoint did not publish them", () => {
|
|
40
|
+
renderWith(
|
|
41
|
+
<FieldByWidget
|
|
42
|
+
widget="date"
|
|
43
|
+
onChange={() => {}}
|
|
44
|
+
field={fieldWith({
|
|
45
|
+
between: {
|
|
46
|
+
mode: "static",
|
|
47
|
+
start: "2026-01-01",
|
|
48
|
+
end: "2026-12-31",
|
|
49
|
+
},
|
|
50
|
+
})}
|
|
51
|
+
/>,
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
const input = screen.getByPlaceholderText("Date");
|
|
55
|
+
expect(input).toHaveAttribute("min", "2026-01-01");
|
|
56
|
+
expect(input).toHaveAttribute("max", "2026-12-31");
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("leaves the picker unrestricted when there are no restrictions at all", () => {
|
|
60
|
+
renderWith(
|
|
61
|
+
<FieldByWidget
|
|
62
|
+
widget="date"
|
|
63
|
+
onChange={() => {}}
|
|
64
|
+
field={fieldWith(undefined)}
|
|
65
|
+
/>,
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
const input = screen.getByPlaceholderText("Date");
|
|
69
|
+
expect(input).not.toHaveAttribute("min");
|
|
70
|
+
expect(input).not.toHaveAttribute("max");
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("an open bound stays open", () => {
|
|
74
|
+
renderWith(
|
|
75
|
+
<FieldByWidget
|
|
76
|
+
widget="date"
|
|
77
|
+
onChange={() => {}}
|
|
78
|
+
field={fieldWith({
|
|
79
|
+
max_date: { mode: "static", value: "2026-12-31" },
|
|
80
|
+
resolved: { min: null, max: "2026-12-31" },
|
|
81
|
+
})}
|
|
82
|
+
/>,
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
const input = screen.getByPlaceholderText("Date");
|
|
86
|
+
expect(input).toHaveAttribute("max", "2026-12-31");
|
|
87
|
+
expect(input).not.toHaveAttribute("min");
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
describe("a datetime field", () => {
|
|
92
|
+
it("passes the published minute-precision bounds", () => {
|
|
93
|
+
renderWith(
|
|
94
|
+
<FieldByWidget
|
|
95
|
+
widget="datetime"
|
|
96
|
+
onChange={() => {}}
|
|
97
|
+
field={fieldWith(
|
|
98
|
+
{
|
|
99
|
+
max_date: { mode: "static", value: "2026-06-30T10:00:00" },
|
|
100
|
+
resolved: { min: null, max: "2026-06-30T10:00" },
|
|
101
|
+
},
|
|
102
|
+
{ name: "momento", type: "datetime", value: "2026-06-15T10:30" },
|
|
103
|
+
)}
|
|
104
|
+
/>,
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
const input = screen.getByPlaceholderText("Date Time");
|
|
108
|
+
expect(input).toHaveAttribute("max", "2026-06-30T10:00");
|
|
109
|
+
expect(input).not.toHaveAttribute("min");
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
describe("fields that are not dates", () => {
|
|
114
|
+
it("a string field with restrictions is rendered unchanged", () => {
|
|
115
|
+
renderWith(
|
|
116
|
+
<FieldByWidget
|
|
117
|
+
widget="string"
|
|
118
|
+
onChange={() => {}}
|
|
119
|
+
field={{
|
|
120
|
+
name: "s",
|
|
121
|
+
value: "hola",
|
|
122
|
+
type: "string",
|
|
123
|
+
restrictions: { resolved: { min: "a", max: "b" } },
|
|
124
|
+
}}
|
|
125
|
+
/>,
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
const input = screen.getByDisplayValue("hola");
|
|
129
|
+
expect(input).not.toHaveAttribute("min");
|
|
130
|
+
expect(input).not.toHaveAttribute("max");
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
});
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { render, screen } from "@testing-library/react";
|
|
2
|
+
import { IntlProvider } from "react-intl";
|
|
3
|
+
import DynamicField from "../DynamicField";
|
|
4
|
+
|
|
5
|
+
const templateField = (extra = {}) => ({
|
|
6
|
+
name: "fecha",
|
|
7
|
+
label: "Fecha",
|
|
8
|
+
type: "date",
|
|
9
|
+
widget: "date",
|
|
10
|
+
cardinality: "?",
|
|
11
|
+
values: null,
|
|
12
|
+
restrictions: {
|
|
13
|
+
between: { mode: "static", start: "2026-01-01", end: "2026-12-31" },
|
|
14
|
+
resolved: { min: "2026-01-01", max: "2026-12-31" },
|
|
15
|
+
},
|
|
16
|
+
default: { value: "", origin: "default" },
|
|
17
|
+
...extra,
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
const renderField = (field) =>
|
|
21
|
+
render(
|
|
22
|
+
<IntlProvider locale="en">
|
|
23
|
+
<DynamicField field={field} onChange={() => {}} />
|
|
24
|
+
</IntlProvider>,
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
describe("the published bounds reach the calendar", () => {
|
|
28
|
+
it("a date field is restricted by the resolved block", () => {
|
|
29
|
+
renderField({
|
|
30
|
+
...templateField(),
|
|
31
|
+
value: { value: "2026-06-15", origin: "user" },
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const input = screen.getByPlaceholderText("Date");
|
|
35
|
+
expect(input).toHaveAttribute("min", "2026-01-01");
|
|
36
|
+
expect(input).toHaveAttribute("max", "2026-12-31");
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("a datetime field is restricted by the resolved block", () => {
|
|
40
|
+
renderField({
|
|
41
|
+
...templateField(
|
|
42
|
+
{
|
|
43
|
+
name: "momento",
|
|
44
|
+
type: "datetime",
|
|
45
|
+
widget: "datetime",
|
|
46
|
+
restrictions: {
|
|
47
|
+
max_date: { mode: "static", value: "2026-06-30T10:00:00" },
|
|
48
|
+
resolved: { min: null, max: "2026-06-30T10:00" },
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
{},
|
|
52
|
+
),
|
|
53
|
+
value: { value: "2026-06-15T10:30", origin: "user" },
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const input = screen.getByPlaceholderText("Date Time");
|
|
57
|
+
expect(input).toHaveAttribute("max", "2026-06-30T10:00");
|
|
58
|
+
expect(input).not.toHaveAttribute("min");
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("a template predating the feature is left unrestricted", () => {
|
|
62
|
+
renderField({
|
|
63
|
+
...templateField({ restrictions: undefined }),
|
|
64
|
+
value: { value: "1999-01-01", origin: "user" },
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const input = screen.getByPlaceholderText("Date");
|
|
68
|
+
expect(input).not.toHaveAttribute("min");
|
|
69
|
+
expect(input).not.toHaveAttribute("max");
|
|
70
|
+
expect(input).toHaveValue("1999-01-01");
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("a stored value already outside the range is still shown, not silently dropped", () => {
|
|
74
|
+
renderField({
|
|
75
|
+
...templateField(),
|
|
76
|
+
value: { value: "1999-01-01", origin: "user" },
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
const input = screen.getByPlaceholderText("Date");
|
|
80
|
+
expect(input).toHaveValue("1999-01-01");
|
|
81
|
+
expect(input).toHaveAttribute("max", "2026-12-31");
|
|
82
|
+
});
|
|
83
|
+
});
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { render, screen, fireEvent } from "@testing-library/react";
|
|
2
|
+
import { IntlProvider } from "react-intl";
|
|
3
|
+
import { DateField } from "../DateField";
|
|
4
|
+
import { DateTimeField } from "../DateTimeField";
|
|
5
|
+
|
|
6
|
+
const renderWith = (ui) =>
|
|
7
|
+
render(<IntlProvider locale="en">{ui}</IntlProvider>);
|
|
8
|
+
|
|
9
|
+
describe("date widgets expose the published bounds to the native input", () => {
|
|
10
|
+
describe("DateField", () => {
|
|
11
|
+
it("passes min and max through", () => {
|
|
12
|
+
renderWith(
|
|
13
|
+
<DateField
|
|
14
|
+
field={{ name: "fecha", value: "2026-06-15" }}
|
|
15
|
+
min="2026-01-01"
|
|
16
|
+
max="2026-12-31"
|
|
17
|
+
onChange={() => {}}
|
|
18
|
+
/>,
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
const input = screen.getByPlaceholderText("Date");
|
|
22
|
+
expect(input).toHaveAttribute("type", "date");
|
|
23
|
+
expect(input).toHaveAttribute("min", "2026-01-01");
|
|
24
|
+
expect(input).toHaveAttribute("max", "2026-12-31");
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("applies a one-sided restriction", () => {
|
|
28
|
+
renderWith(
|
|
29
|
+
<DateField
|
|
30
|
+
field={{ name: "fecha", value: "" }}
|
|
31
|
+
max="2026-12-31"
|
|
32
|
+
onChange={() => {}}
|
|
33
|
+
/>,
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
const input = screen.getByPlaceholderText("Date");
|
|
37
|
+
expect(input).toHaveAttribute("max", "2026-12-31");
|
|
38
|
+
expect(input).not.toHaveAttribute("min");
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("renders no bounds attribute when there is no restriction", () => {
|
|
42
|
+
renderWith(
|
|
43
|
+
<DateField field={{ name: "fecha", value: "" }} onChange={() => {}} />,
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
const input = screen.getByPlaceholderText("Date");
|
|
47
|
+
expect(input).not.toHaveAttribute("min");
|
|
48
|
+
expect(input).not.toHaveAttribute("max");
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("still reports what the user typed", () => {
|
|
52
|
+
const onChange = jest.fn();
|
|
53
|
+
renderWith(
|
|
54
|
+
<DateField
|
|
55
|
+
field={{ name: "fecha", value: "" }}
|
|
56
|
+
max="2026-12-31"
|
|
57
|
+
onChange={onChange}
|
|
58
|
+
/>,
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
fireEvent.change(screen.getByPlaceholderText("Date"), {
|
|
62
|
+
target: { value: "2026-03-01" },
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
expect(onChange).toHaveBeenCalledWith(
|
|
66
|
+
expect.anything(),
|
|
67
|
+
expect.objectContaining({ name: "fecha", value: "2026-03-01" }),
|
|
68
|
+
);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("lets the user clear the field on purpose", () => {
|
|
72
|
+
const onChange = jest.fn();
|
|
73
|
+
renderWith(
|
|
74
|
+
<DateField
|
|
75
|
+
field={{ name: "fecha", value: "2026-03-01" }}
|
|
76
|
+
max="2026-12-31"
|
|
77
|
+
onChange={onChange}
|
|
78
|
+
/>,
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
fireEvent.change(screen.getByPlaceholderText("Date"), {
|
|
82
|
+
target: { value: "" },
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
expect(onChange).toHaveBeenCalledWith(
|
|
86
|
+
expect.anything(),
|
|
87
|
+
expect.objectContaining({ name: "fecha", value: "" }),
|
|
88
|
+
);
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
describe("DateTimeField", () => {
|
|
93
|
+
it("passes the minute-precision bounds through", () => {
|
|
94
|
+
renderWith(
|
|
95
|
+
<DateTimeField
|
|
96
|
+
field={{ name: "momento", value: "2026-06-15T10:30" }}
|
|
97
|
+
min="2026-06-01T00:00"
|
|
98
|
+
max="2026-06-30T23:59"
|
|
99
|
+
onChange={() => {}}
|
|
100
|
+
/>,
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
const input = screen.getByPlaceholderText("Date Time");
|
|
104
|
+
expect(input).toHaveAttribute("type", "datetime-local");
|
|
105
|
+
expect(input).toHaveAttribute("min", "2026-06-01T00:00");
|
|
106
|
+
expect(input).toHaveAttribute("max", "2026-06-30T23:59");
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("a date-only bound reaches the input as minute precision, not dropped", () => {
|
|
110
|
+
renderWith(
|
|
111
|
+
<DateTimeField
|
|
112
|
+
field={{ name: "momento", value: "" }}
|
|
113
|
+
max="2026-06-30"
|
|
114
|
+
onChange={() => {}}
|
|
115
|
+
/>,
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
const input = screen.getByPlaceholderText("Date Time");
|
|
119
|
+
expect(input.getAttribute("max")).toMatch(
|
|
120
|
+
/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}$/,
|
|
121
|
+
);
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
});
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import { screen, fireEvent } from "@testing-library/react";
|
|
2
|
+
import { renderWithIntl as render } from "@truedat/test/renderWithIntl";
|
|
3
|
+
import DynamicField from "../DynamicField";
|
|
4
|
+
|
|
5
|
+
const mockFormatMessage = jest.fn(({ id }) => id);
|
|
6
|
+
|
|
7
|
+
jest.mock("react-intl", () => ({
|
|
8
|
+
...jest.requireActual("react-intl"),
|
|
9
|
+
useIntl: () => ({ formatMessage: mockFormatMessage }),
|
|
10
|
+
}));
|
|
11
|
+
|
|
12
|
+
beforeEach(() => mockFormatMessage.mockClear());
|
|
13
|
+
|
|
14
|
+
const field = (restrictions, value) => ({
|
|
15
|
+
name: "fecha",
|
|
16
|
+
label: "Fecha",
|
|
17
|
+
type: "date",
|
|
18
|
+
widget: "date",
|
|
19
|
+
cardinality: "?",
|
|
20
|
+
restrictions,
|
|
21
|
+
value: { value, origin: "user" },
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const mount = (f) => render(<DynamicField field={f} onChange={jest.fn()} />);
|
|
25
|
+
|
|
26
|
+
describe("a date field with a published restriction", () => {
|
|
27
|
+
const range = {
|
|
28
|
+
between: { mode: "static", start: "2026-01-01", end: "2026-12-31" },
|
|
29
|
+
resolved: { min: "2026-01-01", max: "2026-12-31" },
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
it("marks the field as wrong when a typed date is past the bound", () => {
|
|
33
|
+
const { container } = mount(field(range, "2027-01-01"));
|
|
34
|
+
|
|
35
|
+
expect(container.querySelector(".field.error")).not.toBeNull();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("says which bound was broken", () => {
|
|
39
|
+
mount(field(range, "2027-01-01"));
|
|
40
|
+
|
|
41
|
+
expect(
|
|
42
|
+
screen.getByText("template.field.values.default_before_max"),
|
|
43
|
+
).toBeInTheDocument();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("names the minimum when the value is too early", () => {
|
|
47
|
+
mount(field(range, "2025-12-31"));
|
|
48
|
+
|
|
49
|
+
expect(
|
|
50
|
+
screen.getByText("template.field.values.default_after_min"),
|
|
51
|
+
).toBeInTheDocument();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it.each([
|
|
55
|
+
["2027-01-01", "default_before_max", "2026-12-31"],
|
|
56
|
+
["2025-12-31", "default_after_min", "2026-01-01"],
|
|
57
|
+
])(
|
|
58
|
+
"passes the breached limit for %s to the formatter",
|
|
59
|
+
(value, key, limit) => {
|
|
60
|
+
mount(field(range, value));
|
|
61
|
+
|
|
62
|
+
expect(mockFormatMessage).toHaveBeenCalledWith(
|
|
63
|
+
{ id: `template.field.values.${key}` },
|
|
64
|
+
{ limit },
|
|
65
|
+
);
|
|
66
|
+
},
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
it("does not mark partial keyboard input", () => {
|
|
70
|
+
const { container } = mount(field(range, "2026-0"));
|
|
71
|
+
|
|
72
|
+
expect(container.querySelector(".field.error")).toBeNull();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it.each([
|
|
76
|
+
["2026-02-30", "template.field.restrictions.validation.invalid_date"],
|
|
77
|
+
["2027-01-01T00:00:00Z", "template.field.values.default_before_max"],
|
|
78
|
+
])("checks a complete imported value (%s)", (value, messageId) => {
|
|
79
|
+
const { container } = mount(field(range, value));
|
|
80
|
+
|
|
81
|
+
expect(container.querySelector(".field.error")).not.toBeNull();
|
|
82
|
+
expect(screen.getByText(messageId)).toBeInTheDocument();
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("does not mark a date inside the range", () => {
|
|
86
|
+
const { container } = mount(field(range, "2026-06-15"));
|
|
87
|
+
|
|
88
|
+
expect(container.querySelector(".field.error")).toBeNull();
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("accepts the bound's own day", () => {
|
|
92
|
+
const { container } = mount(field(range, "2026-12-31"));
|
|
93
|
+
|
|
94
|
+
expect(container.querySelector(".field.error")).toBeNull();
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("never marks an empty value: these records are fillable in part", () => {
|
|
98
|
+
const { container } = mount(field(range, ""));
|
|
99
|
+
|
|
100
|
+
expect(container.querySelector(".field.error")).toBeNull();
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("still bounds the picker", () => {
|
|
104
|
+
mount(field(range, "2026-06-15"));
|
|
105
|
+
|
|
106
|
+
const input = screen.getByPlaceholderText("Date");
|
|
107
|
+
expect(input).toHaveAttribute("min", "2026-01-01");
|
|
108
|
+
expect(input).toHaveAttribute("max", "2026-12-31");
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("does not swallow what was typed", () => {
|
|
112
|
+
const onChange = jest.fn();
|
|
113
|
+
render(
|
|
114
|
+
<DynamicField field={field(range, "2026-06-15")} onChange={onChange} />,
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
fireEvent.change(screen.getByPlaceholderText("Date"), {
|
|
118
|
+
target: { value: "2027-01-01" },
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
expect(onChange).toHaveBeenCalledWith(
|
|
122
|
+
expect.anything(),
|
|
123
|
+
expect.objectContaining({ value: "2027-01-01" }),
|
|
124
|
+
);
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
describe("a datetime field", () => {
|
|
129
|
+
const dt = {
|
|
130
|
+
max_date: { mode: "static", value: "2026-06-30T10:00" },
|
|
131
|
+
resolved: { min: null, max: "2026-06-30T10:00" },
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
it("checks at minute precision", () => {
|
|
135
|
+
const { container } = mount({
|
|
136
|
+
...field(dt, "2026-06-30T10:01"),
|
|
137
|
+
type: "datetime",
|
|
138
|
+
widget: "datetime",
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
expect(container.querySelector(".field.error")).not.toBeNull();
|
|
142
|
+
expect(screen.getByPlaceholderText("Date Time")).toHaveAttribute(
|
|
143
|
+
"max",
|
|
144
|
+
"2026-06-30T10:00",
|
|
145
|
+
);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it("accepts the bound's own minute", () => {
|
|
149
|
+
const { container } = mount({
|
|
150
|
+
...field(dt, "2026-06-30T10:00"),
|
|
151
|
+
type: "datetime",
|
|
152
|
+
widget: "datetime",
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
expect(container.querySelector(".field.error")).toBeNull();
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
describe("a stored value that no longer fits", () => {
|
|
160
|
+
it("is shown as wrong rather than hidden", () => {
|
|
161
|
+
const { container } = mount({
|
|
162
|
+
...field(
|
|
163
|
+
{
|
|
164
|
+
max_date: { mode: "static", value: "2026-05-31" },
|
|
165
|
+
resolved: { min: null, max: "2026-05-31" },
|
|
166
|
+
},
|
|
167
|
+
"2026-06-15",
|
|
168
|
+
),
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
expect(screen.getByPlaceholderText("Date")).toHaveValue("2026-06-15");
|
|
172
|
+
expect(container.querySelector(".field.error")).not.toBeNull();
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
describe("fields with no restriction", () => {
|
|
177
|
+
it("are never marked", () => {
|
|
178
|
+
const { container } = mount(field(undefined, "2027-01-01"));
|
|
179
|
+
|
|
180
|
+
expect(container.querySelector(".field.error")).toBeNull();
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it("are never marked when the bounds are open", () => {
|
|
184
|
+
const { container } = mount(
|
|
185
|
+
field(
|
|
186
|
+
{
|
|
187
|
+
min_date: { mode: "static", value: "2026-01-01" },
|
|
188
|
+
resolved: { min: "2026-01-01", max: null },
|
|
189
|
+
},
|
|
190
|
+
"2020-01-01",
|
|
191
|
+
),
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
expect(container.querySelector(".field.error")).not.toBeNull();
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it("a string field with restrictions is not treated as a date", () => {
|
|
198
|
+
const { container } = mount({
|
|
199
|
+
name: "s",
|
|
200
|
+
label: "S",
|
|
201
|
+
type: "string",
|
|
202
|
+
widget: "string",
|
|
203
|
+
restrictions: { resolved: { min: "a", max: "b" } },
|
|
204
|
+
value: { value: "hola", origin: "user" },
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
expect(container.querySelector(".field.error")).toBeNull();
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it("does not show date-format validation for fields with no date bounds", () => {
|
|
211
|
+
const { container } = mount(field({}, "2026-02-30"));
|
|
212
|
+
|
|
213
|
+
expect(container.querySelector(".field.error")).toBeNull();
|
|
214
|
+
});
|
|
215
|
+
});
|