@homebound/form-state 2.33.0 → 3.0.0
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/dist/index.d.ts +437 -9
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1109 -14
- package/dist/index.js.map +1 -0
- package/package.json +32 -35
- package/dist/FormStateApp.d.ts +0 -5
- package/dist/FormStateApp.js +0 -54
- package/dist/FormStateApp.test.d.ts +0 -1
- package/dist/FormStateApp.test.js +0 -26
- package/dist/config.d.ts +0 -78
- package/dist/config.js +0 -2
- package/dist/configBuilders.d.ts +0 -67
- package/dist/configBuilders.js +0 -115
- package/dist/configBuilders.test.d.ts +0 -1
- package/dist/configBuilders.test.js +0 -65
- package/dist/fields/deepEquals.d.ts +0 -7
- package/dist/fields/deepEquals.js +0 -53
- package/dist/fields/fragmentField.d.ts +0 -4
- package/dist/fields/fragmentField.js +0 -58
- package/dist/fields/listField.d.ts +0 -11
- package/dist/fields/listField.js +0 -355
- package/dist/fields/objectField.d.ts +0 -63
- package/dist/fields/objectField.js +0 -277
- package/dist/fields/valueField.d.ts +0 -81
- package/dist/fields/valueField.js +0 -244
- package/dist/formState.stories.d.ts +0 -4
- package/dist/formState.stories.js +0 -12
- package/dist/formState.test.d.ts +0 -11
- package/dist/formState.test.js +0 -2109
- package/dist/formStateDomain.d.ts +0 -47
- package/dist/formStateDomain.js +0 -28
- package/dist/mobx-behavior.test.d.ts +0 -1
- package/dist/mobx-behavior.test.js +0 -51
- package/dist/proxies.d.ts +0 -5
- package/dist/proxies.js +0 -20
- package/dist/rules.d.ts +0 -10
- package/dist/rules.js +0 -11
- package/dist/setupTests.d.ts +0 -1
- package/dist/setupTests.js +0 -15
- package/dist/stackoverlfow.d.ts +0 -17
- package/dist/stackoverlfow.js +0 -17
- package/dist/temporal.d.ts +0 -6
- package/dist/temporal.js +0 -34
- package/dist/temporal.test.d.ts +0 -1
- package/dist/temporal.test.js +0 -97
- package/dist/useFormState.d.ts +0 -82
- package/dist/useFormState.js +0 -158
- package/dist/useFormState.test.d.ts +0 -1
- package/dist/useFormState.test.js +0 -787
- package/dist/useFormStates.d.ts +0 -68
- package/dist/useFormStates.js +0 -101
- package/dist/useFormStates.test.d.ts +0 -1
- package/dist/useFormStates.test.js +0 -223
- package/dist/utils.d.ts +0 -52
- package/dist/utils.js +0 -198
- package/dist/utils.test.d.ts +0 -1
- package/dist/utils.test.js +0 -102
|
@@ -1,787 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
4
|
-
const rtl_utils_1 = require("@homebound/rtl-utils");
|
|
5
|
-
const react_1 = require("@testing-library/react");
|
|
6
|
-
const mobx_1 = require("mobx");
|
|
7
|
-
const mobx_react_1 = require("mobx-react");
|
|
8
|
-
const react_2 = require("react");
|
|
9
|
-
const FormStateApp_1 = require("./FormStateApp");
|
|
10
|
-
const objectField_1 = require("./fields/objectField");
|
|
11
|
-
const rules_1 = require("./rules");
|
|
12
|
-
const useFormState_1 = require("./useFormState");
|
|
13
|
-
describe("useFormState", () => {
|
|
14
|
-
it("calls init.map if init.input is defined", async () => {
|
|
15
|
-
// Given a component
|
|
16
|
-
function TestComponent() {
|
|
17
|
-
const config = { firstName: { type: "value" } };
|
|
18
|
-
// And we have query data that may or may not be defined
|
|
19
|
-
const data = { firstName: "bob" };
|
|
20
|
-
const form = (0, useFormState_1.useFormState)({
|
|
21
|
-
config,
|
|
22
|
-
// Then the lambda is passed the "de-undefined" data, i.e. `d.firstName` is not a compile error
|
|
23
|
-
init: { input: data, map: (d) => ({ firstName: d.firstName }) },
|
|
24
|
-
});
|
|
25
|
-
return (0, jsx_runtime_1.jsx)("div", { children: form.firstName.value });
|
|
26
|
-
}
|
|
27
|
-
const r = await (0, rtl_utils_1.render)((0, jsx_runtime_1.jsx)(TestComponent, {}));
|
|
28
|
-
expect(r.baseElement).toHaveTextContent("bob");
|
|
29
|
-
});
|
|
30
|
-
it("memoizes on init.input being an array", async () => {
|
|
31
|
-
const config = { firstName: { type: "value" } };
|
|
32
|
-
function TestComponent() {
|
|
33
|
-
const [, setTick] = (0, react_2.useState)(0);
|
|
34
|
-
const form = (0, useFormState_1.useFormState)({
|
|
35
|
-
config,
|
|
36
|
-
init: { input: ["a", "b"], map: ([a, b]) => ({ firstName: a + b }) },
|
|
37
|
-
});
|
|
38
|
-
const onClick = () => [form.firstName.set("fred"), setTick(1)];
|
|
39
|
-
return ((0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("button", { "data-testid": "change", onClick: onClick }), (0, jsx_runtime_1.jsx)("div", { "data-testid": "firstName", children: form.firstName.value })] }));
|
|
40
|
-
}
|
|
41
|
-
const r = await (0, rtl_utils_1.render)((0, jsx_runtime_1.jsx)(TestComponent, {}));
|
|
42
|
-
expect(r.firstName).toHaveTextContent("ab");
|
|
43
|
-
(0, rtl_utils_1.click)(r.change);
|
|
44
|
-
expect(r.firstName).toHaveTextContent("fred");
|
|
45
|
-
});
|
|
46
|
-
it("uses default if init.input is undefined", async () => {
|
|
47
|
-
// Given a component
|
|
48
|
-
function TestComponent() {
|
|
49
|
-
const config = { firstName: { type: "value" } };
|
|
50
|
-
// And we have query data that may or may not be defined (but is actually undefined)
|
|
51
|
-
const data = Math.random() >= 0 ? undefined : { firstName: "bob" };
|
|
52
|
-
const form = (0, useFormState_1.useFormState)({
|
|
53
|
-
config,
|
|
54
|
-
init: { input: data, map: (d) => ({ firstName: d.firstName }) },
|
|
55
|
-
});
|
|
56
|
-
return (0, jsx_runtime_1.jsx)("div", { children: form.firstName.value });
|
|
57
|
-
}
|
|
58
|
-
const r = await (0, rtl_utils_1.render)((0, jsx_runtime_1.jsx)(TestComponent, {}));
|
|
59
|
-
// Then we init.map wasn't called, and we used {} instead
|
|
60
|
-
expect(r.baseElement.textContent).toEqual("");
|
|
61
|
-
});
|
|
62
|
-
it("uses custom init.ifUndefined if init.input is undefined", async () => {
|
|
63
|
-
// Given a component
|
|
64
|
-
function TestComponent() {
|
|
65
|
-
const config = {
|
|
66
|
-
id: { type: "value" },
|
|
67
|
-
firstName: { type: "value" },
|
|
68
|
-
};
|
|
69
|
-
// And we have query data that may or may not be defined (but is actually undefined)
|
|
70
|
-
const data = Math.random() >= 0 ? undefined : { firstName: "bob" };
|
|
71
|
-
const form = (0, useFormState_1.useFormState)({
|
|
72
|
-
config,
|
|
73
|
-
// And we pass `ifUndefined`
|
|
74
|
-
init: {
|
|
75
|
-
input: data,
|
|
76
|
-
map: (d) => ({ firstName: d.firstName }),
|
|
77
|
-
ifUndefined: { firstName: "default" },
|
|
78
|
-
},
|
|
79
|
-
});
|
|
80
|
-
return ((0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("div", { "data-testid": "firstName", children: form.firstName.value }), (0, jsx_runtime_1.jsx)("div", { "data-testid": "changedValue", children: JSON.stringify(form.changedValue) })] }));
|
|
81
|
-
}
|
|
82
|
-
const r = await (0, rtl_utils_1.render)((0, jsx_runtime_1.jsx)(TestComponent, {}));
|
|
83
|
-
// Then we use the ifUndefined value
|
|
84
|
-
expect(r.firstName.textContent).toEqual("default");
|
|
85
|
-
expect(r.changedValue.textContent).toEqual(JSON.stringify({ firstName: "default" }));
|
|
86
|
-
});
|
|
87
|
-
it("doesn't required an init value", async () => {
|
|
88
|
-
function TestComponent() {
|
|
89
|
-
const config = { firstName: { type: "value" } };
|
|
90
|
-
const form = (0, useFormState_1.useFormState)({ config });
|
|
91
|
-
return (0, jsx_runtime_1.jsx)("div", { children: form.firstName.value });
|
|
92
|
-
}
|
|
93
|
-
const r = await (0, rtl_utils_1.render)((0, jsx_runtime_1.jsx)(TestComponent, {}));
|
|
94
|
-
expect(r.baseElement.textContent).toEqual("");
|
|
95
|
-
});
|
|
96
|
-
it("uses init.onlyOnce to not react to identity changes", async () => {
|
|
97
|
-
const config = { firstName: { type: "value" } };
|
|
98
|
-
const TestComponent = (0, mobx_react_1.observer)(() => {
|
|
99
|
-
// And we have an `init` value that is dynamic, so we can observe whether init reruns
|
|
100
|
-
const renderCount = (0, react_2.useRef)(0).current++;
|
|
101
|
-
const form = (0, useFormState_1.useFormState)({
|
|
102
|
-
config,
|
|
103
|
-
init: { input: { firstName: `${renderCount}` }, onlyOnce: true },
|
|
104
|
-
});
|
|
105
|
-
const [, setTick] = (0, react_2.useState)(0);
|
|
106
|
-
return ((0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("button", { "data-testid": "change", onClick: () => setTick(1) }), (0, jsx_runtime_1.jsx)("div", { "data-testid": "firstName", children: form.firstName.value })] }));
|
|
107
|
-
});
|
|
108
|
-
const r = await (0, rtl_utils_1.render)((0, jsx_runtime_1.jsx)(TestComponent, {}));
|
|
109
|
-
expect(r.firstName).toHaveTextContent("0");
|
|
110
|
-
(0, rtl_utils_1.click)(r.change);
|
|
111
|
-
expect(r.firstName).toHaveTextContent("0");
|
|
112
|
-
});
|
|
113
|
-
it("can init with fragments", async () => {
|
|
114
|
-
// Given a component
|
|
115
|
-
function TestComponent() {
|
|
116
|
-
var _a, _b, _c, _d;
|
|
117
|
-
// And we have query data that may or may not be defined
|
|
118
|
-
const form = (0, useFormState_1.useFormState)({
|
|
119
|
-
config: authorWithBookFragments,
|
|
120
|
-
init: {
|
|
121
|
-
input: {},
|
|
122
|
-
map: () => ({ firstName: "a1", books: [{ id: "b:1", data: (0, objectField_1.fragment)({ misc: "data" }) }] }),
|
|
123
|
-
},
|
|
124
|
-
});
|
|
125
|
-
return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("div", { "data-testid": "fieldValue", children: form.books.rows[0].data.value.misc }), (0, jsx_runtime_1.jsx)("div", { "data-testid": "rowValue", children: (_b = (_a = form.books.rows[0].value.data) === null || _a === void 0 ? void 0 : _a.misc) !== null && _b !== void 0 ? _b : "unset" }), (0, jsx_runtime_1.jsx)("div", { "data-testid": "listValue", children: (_d = (_c = form.books.value[0].data) === null || _c === void 0 ? void 0 : _c.misc) !== null && _d !== void 0 ? _d : "unset" })] }));
|
|
126
|
-
}
|
|
127
|
-
const r = await (0, rtl_utils_1.render)((0, jsx_runtime_1.jsx)(TestComponent, {}));
|
|
128
|
-
expect(r.fieldValue).toHaveTextContent("data");
|
|
129
|
-
expect(r.rowValue).toHaveTextContent("unset");
|
|
130
|
-
expect(r.listValue).toHaveTextContent("unset");
|
|
131
|
-
});
|
|
132
|
-
it("keeps local changed values when a query refreshes", async () => {
|
|
133
|
-
// Given a component
|
|
134
|
-
function TestComponent() {
|
|
135
|
-
const config = authorWithAddressAndBooksConfig;
|
|
136
|
-
// And we have two sets of data
|
|
137
|
-
const data1 = {
|
|
138
|
-
id: "a:1",
|
|
139
|
-
firstName: "f1",
|
|
140
|
-
lastName: "l1",
|
|
141
|
-
address: { id: "address:1", street: "s1", city: "c1" },
|
|
142
|
-
books: [
|
|
143
|
-
{ id: "b:1", title: "a1" },
|
|
144
|
-
{ id: "b:2", title: "b1" },
|
|
145
|
-
],
|
|
146
|
-
};
|
|
147
|
-
const data2 = {
|
|
148
|
-
id: "a:1",
|
|
149
|
-
firstName: "f2", // we'll have a local WIP change
|
|
150
|
-
lastName: "l2",
|
|
151
|
-
address: {
|
|
152
|
-
id: "address:1",
|
|
153
|
-
street: "s2", // we'll have a local WIP change
|
|
154
|
-
city: "c2",
|
|
155
|
-
},
|
|
156
|
-
books: [
|
|
157
|
-
{
|
|
158
|
-
id: "b:1",
|
|
159
|
-
title: "a2", // we'll have a local WIP change
|
|
160
|
-
},
|
|
161
|
-
{ id: "b:2", title: "b2" },
|
|
162
|
-
{ id: "b:3", title: "b3" },
|
|
163
|
-
],
|
|
164
|
-
};
|
|
165
|
-
// Eventually our local values are saved
|
|
166
|
-
const data3 = {
|
|
167
|
-
id: "a:1",
|
|
168
|
-
firstName: "local",
|
|
169
|
-
lastName: "l2",
|
|
170
|
-
address: { id: "address:1", street: "local", city: "c2" },
|
|
171
|
-
books: [
|
|
172
|
-
{ id: "b:1", title: "local" },
|
|
173
|
-
{ id: "b:2", title: "b2" },
|
|
174
|
-
{ id: "b:3", title: "b3" },
|
|
175
|
-
],
|
|
176
|
-
};
|
|
177
|
-
// And we start out with data1
|
|
178
|
-
const [data, setData] = (0, react_2.useState)(data1);
|
|
179
|
-
const form = (0, useFormState_1.useFormState)({ config, init: { input: data } });
|
|
180
|
-
function makeLocalChanges() {
|
|
181
|
-
form.firstName.value = "local";
|
|
182
|
-
form.address.street.value = "local";
|
|
183
|
-
form.books.rows[0].title.value = "local";
|
|
184
|
-
}
|
|
185
|
-
return ((0, jsx_runtime_1.jsx)(mobx_react_1.Observer, { children: () => ((0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("div", { "data-testid": "firstName", children: form.firstName.value }), (0, jsx_runtime_1.jsx)("div", { "data-testid": "lastName", children: form.lastName.value }), (0, jsx_runtime_1.jsx)("div", { "data-testid": "street", children: form.address.street.value }), (0, jsx_runtime_1.jsx)("div", { "data-testid": "city", children: form.address.city.value }), (0, jsx_runtime_1.jsx)("div", { "data-testid": "title1", children: form.books.rows[0].title.value }), (0, jsx_runtime_1.jsx)("div", { "data-testid": "title2", children: form.books.rows[1].title.value }), (0, jsx_runtime_1.jsx)("div", { "data-testid": "booksLength", children: form.books.rows.length }), (0, jsx_runtime_1.jsx)("button", { "data-testid": "makeLocalChanges", onClick: makeLocalChanges }), (0, jsx_runtime_1.jsx)("button", { "data-testid": "refreshData", onClick: () => setData(data2) }), (0, jsx_runtime_1.jsx)("button", { "data-testid": "saveData", onClick: () => setData(data3) }), (0, jsx_runtime_1.jsx)("div", { "data-testid": "dirty", children: String(form.dirty) })] })) }));
|
|
186
|
-
}
|
|
187
|
-
// And we start out with the initial query data
|
|
188
|
-
const r = await (0, rtl_utils_1.render)((0, jsx_runtime_1.jsx)(TestComponent, {}));
|
|
189
|
-
expect(r.firstName.textContent).toEqual("f1");
|
|
190
|
-
expect(r.street.textContent).toEqual("s1");
|
|
191
|
-
expect(r.title1.textContent).toEqual("a1");
|
|
192
|
-
// When we make some local changes
|
|
193
|
-
(0, rtl_utils_1.click)(r.makeLocalChanges);
|
|
194
|
-
// Then we see them
|
|
195
|
-
expect(r.firstName.textContent).toEqual("local");
|
|
196
|
-
expect(r.street.textContent).toEqual("local");
|
|
197
|
-
expect(r.title1.textContent).toEqual("local");
|
|
198
|
-
expect(r.dirty.textContent).toEqual("true");
|
|
199
|
-
// And when the new query is ran i.e. due to a cache refresh
|
|
200
|
-
(0, rtl_utils_1.click)(r.refreshData);
|
|
201
|
-
// Then we kept our local changes
|
|
202
|
-
expect(r.firstName.textContent).toEqual("local");
|
|
203
|
-
expect(r.street.textContent).toEqual("local");
|
|
204
|
-
expect(r.title1.textContent).toEqual("local");
|
|
205
|
-
// But we also see the new data for fields we have not changed
|
|
206
|
-
expect(r.lastName.textContent).toEqual("l2");
|
|
207
|
-
expect(r.city.textContent).toEqual("c2");
|
|
208
|
-
expect(r.title2.textContent).toEqual("b2");
|
|
209
|
-
expect(r.booksLength.textContent).toEqual("3");
|
|
210
|
-
expect(r.dirty.textContent).toEqual("true");
|
|
211
|
-
// And then when our mutation results come back
|
|
212
|
-
(0, rtl_utils_1.click)(r.saveData);
|
|
213
|
-
// And we're not dirty anymore
|
|
214
|
-
expect(r.dirty.textContent).toEqual("false");
|
|
215
|
-
});
|
|
216
|
-
it("accepts server values if client has no further changes", async () => {
|
|
217
|
-
function TestComponent() {
|
|
218
|
-
const config = authorWithAddressAndBooksConfig;
|
|
219
|
-
// The initial resposne
|
|
220
|
-
const data1 = { id: "a:1", firstName: "F1" };
|
|
221
|
-
// The acked response, which uppercases our submission
|
|
222
|
-
const data2 = { id: "a:1", firstName: "F2" };
|
|
223
|
-
// And we start out with data1
|
|
224
|
-
const [data, setData] = (0, react_2.useState)(data1);
|
|
225
|
-
const form = (0, useFormState_1.useFormState)({ config, init: { input: data } });
|
|
226
|
-
function makeLocalChanges() {
|
|
227
|
-
form.firstName.value = "f2";
|
|
228
|
-
}
|
|
229
|
-
return ((0, jsx_runtime_1.jsx)(mobx_react_1.Observer, { children: () => ((0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("div", { "data-testid": "firstName", children: form.firstName.value }), (0, jsx_runtime_1.jsx)("button", { "data-testid": "makeLocalChanges", onClick: makeLocalChanges }), (0, jsx_runtime_1.jsx)("button", { "data-testid": "refreshData", onClick: () => {
|
|
230
|
-
noop(form.changedValue); // pretend we put this on the wire
|
|
231
|
-
setData(data2);
|
|
232
|
-
} })] })) }));
|
|
233
|
-
}
|
|
234
|
-
// And we start out with the initial query data
|
|
235
|
-
const r = await (0, rtl_utils_1.render)((0, jsx_runtime_1.jsx)(TestComponent, {}));
|
|
236
|
-
expect(r.firstName.textContent).toEqual("F1");
|
|
237
|
-
// When we make some local changes
|
|
238
|
-
(0, rtl_utils_1.click)(r.makeLocalChanges);
|
|
239
|
-
// Then we see them
|
|
240
|
-
expect(r.firstName.textContent).toEqual("f2");
|
|
241
|
-
// And when the server acks a slightly different result
|
|
242
|
-
(0, rtl_utils_1.click)(r.refreshData);
|
|
243
|
-
// Then we accept it
|
|
244
|
-
expect(r.firstName.textContent).toEqual("F2");
|
|
245
|
-
});
|
|
246
|
-
it("can immediately update from a server ack before init input rerenders", async () => {
|
|
247
|
-
const config = {
|
|
248
|
-
firstName: { type: "value" },
|
|
249
|
-
lastName: { type: "value" },
|
|
250
|
-
};
|
|
251
|
-
const initialAuthor = { legalFirstName: "First", legalLastName: "Last" };
|
|
252
|
-
const savedAuthor = { legalFirstName: "Saved", legalLastName: "Author" };
|
|
253
|
-
function TestComponent() {
|
|
254
|
-
const form = (0, useFormState_1.useFormState)({
|
|
255
|
-
config,
|
|
256
|
-
init: { input: initialAuthor, map: mapServerAuthor },
|
|
257
|
-
});
|
|
258
|
-
function makeLocalChanges() {
|
|
259
|
-
form.firstName.set("Saved");
|
|
260
|
-
form.lastName.set("Author");
|
|
261
|
-
}
|
|
262
|
-
function saveWithoutUpdating() {
|
|
263
|
-
noop(form.changedValue);
|
|
264
|
-
}
|
|
265
|
-
function saveAndUpdate() {
|
|
266
|
-
noop(form.changedValue);
|
|
267
|
-
form.update(savedAuthor);
|
|
268
|
-
}
|
|
269
|
-
return ((0, jsx_runtime_1.jsx)(mobx_react_1.Observer, { children: () => ((0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("div", { "data-testid": "firstName", children: form.firstName.value }), (0, jsx_runtime_1.jsx)("div", { "data-testid": "lastName", children: form.lastName.value }), (0, jsx_runtime_1.jsx)("div", { "data-testid": "dirty", children: String(form.dirty) }), (0, jsx_runtime_1.jsx)("button", { "data-testid": "makeLocalChanges", onClick: makeLocalChanges }), (0, jsx_runtime_1.jsx)("button", { "data-testid": "saveWithoutUpdating", onClick: saveWithoutUpdating }), (0, jsx_runtime_1.jsx)("button", { "data-testid": "saveAndUpdate", onClick: saveAndUpdate })] })) }));
|
|
270
|
-
}
|
|
271
|
-
const r = await (0, rtl_utils_1.render)((0, jsx_runtime_1.jsx)(TestComponent, {}));
|
|
272
|
-
expect(r.firstName.textContent).toEqual("First");
|
|
273
|
-
expect(r.lastName.textContent).toEqual("Last");
|
|
274
|
-
expect(r.dirty.textContent).toEqual("false");
|
|
275
|
-
(0, rtl_utils_1.click)(r.makeLocalChanges);
|
|
276
|
-
expect(r.firstName.textContent).toEqual("Saved");
|
|
277
|
-
expect(r.lastName.textContent).toEqual("Author");
|
|
278
|
-
expect(r.dirty.textContent).toEqual("true");
|
|
279
|
-
(0, rtl_utils_1.click)(r.saveWithoutUpdating);
|
|
280
|
-
expect(r.dirty.textContent).toEqual("true");
|
|
281
|
-
(0, rtl_utils_1.click)(r.saveAndUpdate);
|
|
282
|
-
expect(r.firstName.textContent).toEqual("Saved");
|
|
283
|
-
expect(r.lastName.textContent).toEqual("Author");
|
|
284
|
-
expect(r.dirty.textContent).toEqual("false");
|
|
285
|
-
function mapServerAuthor(input) {
|
|
286
|
-
return { firstName: input.legalFirstName, lastName: input.legalLastName };
|
|
287
|
-
}
|
|
288
|
-
});
|
|
289
|
-
it("becomes clean when update acks an array value with the same contents", async () => {
|
|
290
|
-
const config = { colors: { type: "value" } };
|
|
291
|
-
function TestComponent() {
|
|
292
|
-
const form = (0, useFormState_1.useFormState)({ config, init: { input: { colors: ["red", "green", "blue"] } } });
|
|
293
|
-
// Note: we deliberately do not read `form.changedValue` (the way an auto-save would), so the only
|
|
294
|
-
// thing that can let `update` accept the server value is content equality, not the changedValue hint.
|
|
295
|
-
return ((0, jsx_runtime_1.jsx)(mobx_react_1.Observer, { children: () => {
|
|
296
|
-
var _a;
|
|
297
|
-
return ((0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("div", { "data-testid": "colors", children: ((_a = form.colors.value) !== null && _a !== void 0 ? _a : []).join(",") }), (0, jsx_runtime_1.jsx)("div", { "data-testid": "dirty", children: String(form.dirty) }), (0, jsx_runtime_1.jsx)("button", { "data-testid": "edit", onClick: () => form.colors.set(["red", "green"]) }), (0, jsx_runtime_1.jsx)("button", { "data-testid": "saveAndUpdate", onClick: () => form.update({ colors: ["red", "green"] }) })] }));
|
|
298
|
-
} }));
|
|
299
|
-
}
|
|
300
|
-
const r = await (0, rtl_utils_1.render)((0, jsx_runtime_1.jsx)(TestComponent, {}));
|
|
301
|
-
expect(r.dirty.textContent).toEqual("false");
|
|
302
|
-
// When the user edits the array
|
|
303
|
-
(0, rtl_utils_1.click)(r.edit);
|
|
304
|
-
expect(r.dirty.textContent).toEqual("true");
|
|
305
|
-
// And the server acks the same contents in a brand-new array instance
|
|
306
|
-
(0, rtl_utils_1.click)(r.saveAndUpdate);
|
|
307
|
-
// Then the form is clean (we don't get stuck dirty on array reference inequality)
|
|
308
|
-
expect(r.colors.textContent).toEqual("red,green");
|
|
309
|
-
expect(r.dirty.textContent).toEqual("false");
|
|
310
|
-
});
|
|
311
|
-
it("can accept new data while read only", async () => {
|
|
312
|
-
// Given a component
|
|
313
|
-
function TestComponent() {
|
|
314
|
-
const config = authorWithAddressAndBooksConfig;
|
|
315
|
-
// And we have two sets of data
|
|
316
|
-
const data1 = {
|
|
317
|
-
firstName: "f1",
|
|
318
|
-
address: { street: "s1" },
|
|
319
|
-
books: [{ title: "a1" }],
|
|
320
|
-
};
|
|
321
|
-
const data2 = {
|
|
322
|
-
firstName: "f2",
|
|
323
|
-
address: { street: "s2" },
|
|
324
|
-
books: [{ title: "a2" }],
|
|
325
|
-
};
|
|
326
|
-
// And we start out with data1
|
|
327
|
-
const [data, setData] = (0, react_2.useState)(data1);
|
|
328
|
-
const form = (0, useFormState_1.useFormState)({
|
|
329
|
-
config,
|
|
330
|
-
init: { input: data },
|
|
331
|
-
// And the form is read only
|
|
332
|
-
readOnly: true,
|
|
333
|
-
});
|
|
334
|
-
return ((0, jsx_runtime_1.jsx)(mobx_react_1.Observer, { children: () => ((0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("div", { "data-testid": "firstName", children: form.firstName.value }), (0, jsx_runtime_1.jsx)("div", { "data-testid": "street", children: form.address.street.value }), (0, jsx_runtime_1.jsx)("div", { "data-testid": "title1", children: form.books.rows[0].title.value }), (0, jsx_runtime_1.jsx)("button", { "data-testid": "refreshData", onClick: () => setData(data2) })] })) }));
|
|
335
|
-
}
|
|
336
|
-
// And we start out with the initial query data
|
|
337
|
-
const r = await (0, rtl_utils_1.render)((0, jsx_runtime_1.jsx)(TestComponent, {}));
|
|
338
|
-
expect(r.firstName.textContent).toEqual("f1");
|
|
339
|
-
expect(r.street.textContent).toEqual("s1");
|
|
340
|
-
expect(r.title1.textContent).toEqual("a1");
|
|
341
|
-
// When the new query is ran i.e. due to a cache refresh
|
|
342
|
-
(0, rtl_utils_1.click)(r.refreshData);
|
|
343
|
-
// Then we see the latest data
|
|
344
|
-
expect(r.firstName.textContent).toEqual("f2");
|
|
345
|
-
expect(r.street.textContent).toEqual("s2");
|
|
346
|
-
expect(r.title1.textContent).toEqual("a2");
|
|
347
|
-
});
|
|
348
|
-
it("can accept new data with computed fields", async () => {
|
|
349
|
-
// Given a component
|
|
350
|
-
// And it's using a class/mobx proxy as the basis for the data
|
|
351
|
-
class AuthorRow {
|
|
352
|
-
constructor(firstName, lastName, books) {
|
|
353
|
-
this.firstName = firstName;
|
|
354
|
-
this.lastName = lastName;
|
|
355
|
-
this.books = books;
|
|
356
|
-
(0, mobx_1.makeAutoObservable)(this);
|
|
357
|
-
}
|
|
358
|
-
get fullName() {
|
|
359
|
-
return this.firstName + " " + this.lastName;
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
function TestComponent() {
|
|
363
|
-
// And we have two sets of data
|
|
364
|
-
const data1 = { firstName: "f1", lastName: "l1", books: [] };
|
|
365
|
-
const data2 = { firstName: "f2", lastName: "l2", books: [{ title: "t1" }] };
|
|
366
|
-
// And we start out with data1
|
|
367
|
-
const [data, setData] = (0, react_2.useState)(data1);
|
|
368
|
-
const author = (0, react_2.useMemo)(() => new AuthorRow(data.firstName, data.lastName, data.books), [data]);
|
|
369
|
-
const config = (0, react_2.useMemo)(() => ({
|
|
370
|
-
firstName: { type: "value" },
|
|
371
|
-
lastName: { type: "value" },
|
|
372
|
-
fullName: { type: "value", computed: true },
|
|
373
|
-
books: { type: "list", config: { title: { type: "value" } } },
|
|
374
|
-
}), []);
|
|
375
|
-
const form = (0, useFormState_1.useFormState)({ config, init: { input: author, map: (a) => a } });
|
|
376
|
-
return ((0, jsx_runtime_1.jsx)(mobx_react_1.Observer, { children: () => ((0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("div", { "data-testid": "firstName", children: form.firstName.value }), (0, jsx_runtime_1.jsx)("div", { "data-testid": "fullName", children: form.fullName.value }), (0, jsx_runtime_1.jsx)("div", { "data-testid": "booksLength", children: form.books.rows.length }), (0, jsx_runtime_1.jsx)("div", { "data-testid": "booksDirty", children: String(form.books.touched) }), (0, jsx_runtime_1.jsx)("button", { "data-testid": "refreshData", onClick: () => setData(data2) })] })) }));
|
|
377
|
-
}
|
|
378
|
-
// And we start out with the initial query data
|
|
379
|
-
const r = await (0, rtl_utils_1.render)((0, jsx_runtime_1.jsx)(TestComponent, {}));
|
|
380
|
-
expect(r.firstName.textContent).toEqual("f1");
|
|
381
|
-
expect(r.fullName.textContent).toEqual("f1 l1");
|
|
382
|
-
expect(r.booksLength.textContent).toEqual("0");
|
|
383
|
-
expect(r.booksDirty.textContent).toEqual("false");
|
|
384
|
-
// When the new query is ran i.e. due to a cache refresh
|
|
385
|
-
(0, rtl_utils_1.click)(r.refreshData);
|
|
386
|
-
expect(r.firstName.textContent).toEqual("f2");
|
|
387
|
-
expect(r.fullName.textContent).toEqual("f2 l2");
|
|
388
|
-
expect(r.booksLength.textContent).toEqual("1");
|
|
389
|
-
expect(r.booksDirty.textContent).toEqual("false");
|
|
390
|
-
});
|
|
391
|
-
it("can trigger auto-save when underlying observable is changed", async () => {
|
|
392
|
-
// Given a component
|
|
393
|
-
// And it's using a class/mobx proxy as the basis for the data
|
|
394
|
-
class AuthorRow {
|
|
395
|
-
constructor(firstName) {
|
|
396
|
-
this.firstName = firstName;
|
|
397
|
-
(0, mobx_1.makeAutoObservable)(this);
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
|
-
const autoSave = jest.fn();
|
|
401
|
-
function TestComponent() {
|
|
402
|
-
// And the author starts out with f1
|
|
403
|
-
const author = (0, react_2.useMemo)(() => new AuthorRow("f1"), []);
|
|
404
|
-
const config = (0, react_2.useMemo)(() => ({ firstName: { type: "value" } }), []);
|
|
405
|
-
const form = (0, useFormState_1.useFormState)({ config, init: { input: author, map: (a) => a }, autoSave });
|
|
406
|
-
return ((0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("button", { "data-testid": "changeFirstName", onClick: () => (author.firstName = "f2") }), (0, jsx_runtime_1.jsx)("button", { "data-testid": "clearFirstName", onClick: () => (author.firstName = undefined) })] }));
|
|
407
|
-
}
|
|
408
|
-
// When we change the underlying observable
|
|
409
|
-
const r = await (0, rtl_utils_1.render)((0, jsx_runtime_1.jsx)(TestComponent, {}));
|
|
410
|
-
expect(autoSave).toBeCalledTimes(0);
|
|
411
|
-
await (0, rtl_utils_1.clickAndWait)(r.changeFirstName);
|
|
412
|
-
// Then auto save is triggered
|
|
413
|
-
expect(autoSave).toBeCalledTimes(1);
|
|
414
|
-
await (0, rtl_utils_1.clickAndWait)(r.clearFirstName);
|
|
415
|
-
expect(autoSave).toBeCalledTimes(2);
|
|
416
|
-
});
|
|
417
|
-
it("can trigger auto save for fields in list that were initially undefined", async () => {
|
|
418
|
-
const autoSave = jest.fn();
|
|
419
|
-
// Given a component
|
|
420
|
-
function TestComponent() {
|
|
421
|
-
// When the data is initially undefined
|
|
422
|
-
const [data, setData] = (0, react_2.useState)();
|
|
423
|
-
const data2 = { books: [{ title: "Title 1" }] };
|
|
424
|
-
const form = (0, useFormState_1.useFormState)({
|
|
425
|
-
config: authorWithBooksConfig,
|
|
426
|
-
init: { input: data, ifUndefined: { books: [] } },
|
|
427
|
-
autoSave,
|
|
428
|
-
});
|
|
429
|
-
return ((0, jsx_runtime_1.jsx)(mobx_react_1.Observer, { children: () => ((0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("button", { "data-testid": "refreshData", onClick: () => setData(data2) }), (0, jsx_runtime_1.jsx)("button", { "data-testid": "add", onClick: () => form.books.add({ title: "New Book" }) }), (0, jsx_runtime_1.jsx)("button", { "data-testid": "blurBookOne", onClick: () => focusAndBlur(form.books.rows[0].title) }), (0, jsx_runtime_1.jsx)("button", { "data-testid": "blurBookTwo", onClick: () => focusAndBlur(form.books.rows[1].title) })] })) }));
|
|
430
|
-
}
|
|
431
|
-
// Given a formState with `onBlur` set
|
|
432
|
-
const r = await (0, rtl_utils_1.render)((0, jsx_runtime_1.jsx)(TestComponent, {}));
|
|
433
|
-
// When the data/child is now available
|
|
434
|
-
(0, rtl_utils_1.click)(r.refreshData);
|
|
435
|
-
// And the field is blurred
|
|
436
|
-
await (0, rtl_utils_1.clickAndWait)(r.blurBookOne);
|
|
437
|
-
// Then we don't auto-save because nothing has changed
|
|
438
|
-
expect(autoSave).toBeCalledTimes(0);
|
|
439
|
-
// And when adding a new book
|
|
440
|
-
await (0, rtl_utils_1.clickAndWait)(r.add);
|
|
441
|
-
// We autoSave the new row right away (because we don't have any validation rules
|
|
442
|
-
// that say the new row can't be empty)
|
|
443
|
-
expect(autoSave).toBeCalledTimes(1);
|
|
444
|
-
// And the new book is blurred
|
|
445
|
-
await (0, rtl_utils_1.clickAndWait)(r.blurBookTwo);
|
|
446
|
-
// Then we auto save again
|
|
447
|
-
expect(autoSave).toBeCalledTimes(2);
|
|
448
|
-
});
|
|
449
|
-
it("can revert to last-saved-state after a failed auto save", async () => {
|
|
450
|
-
// Given a component
|
|
451
|
-
function TestComponent() {
|
|
452
|
-
// And the firstName is initially "f1"
|
|
453
|
-
const [data, setData] = (0, react_2.useState)({ firstName: "f1" });
|
|
454
|
-
const query = (0, react_2.useMemo)(() => {
|
|
455
|
-
return { data, loading: false, error: undefined };
|
|
456
|
-
}, [data]);
|
|
457
|
-
const form = (0, useFormState_1.useFormState)({
|
|
458
|
-
config: authorConfig,
|
|
459
|
-
init: { query, map: (d) => d },
|
|
460
|
-
async autoSave(fs) {
|
|
461
|
-
// If the user types "f2", we'll pretend the backend accepted the update, and acks it in the mutation response
|
|
462
|
-
if (fs.firstName.value === "f2") {
|
|
463
|
-
await new Promise((resolve) => setTimeout(resolve, 1));
|
|
464
|
-
setData(fs.value);
|
|
465
|
-
}
|
|
466
|
-
else {
|
|
467
|
-
// If the user types anything else, pretend the backend rejected it, and revert
|
|
468
|
-
await new Promise((resolve) => setTimeout(resolve, 1));
|
|
469
|
-
fs.revertChanges();
|
|
470
|
-
}
|
|
471
|
-
},
|
|
472
|
-
});
|
|
473
|
-
return ((0, jsx_runtime_1.jsx)(mobx_react_1.Observer, { children: () => ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("div", { "data-testid": "firstNameOriginal", children: form.firstName.originalValue }), (0, jsx_runtime_1.jsx)(FormStateApp_1.TextField, { field: form.firstName }), ";"] })) }));
|
|
474
|
-
}
|
|
475
|
-
// So we start out with f1
|
|
476
|
-
const r = await (0, rtl_utils_1.render)((0, jsx_runtime_1.jsx)(TestComponent, {}));
|
|
477
|
-
expect(r.firstName).toHaveValue("f1");
|
|
478
|
-
// When we type f2, it should be accepted & stay
|
|
479
|
-
await (0, rtl_utils_1.typeAndWait)(r.firstName, "f2");
|
|
480
|
-
expect(r.firstName).toHaveValue("f2");
|
|
481
|
-
expect(r.firstNameOriginal).toHaveTextContent("f2");
|
|
482
|
-
// But if we type f3, we want it reverted, back to f2 (the last good value)
|
|
483
|
-
await (0, rtl_utils_1.typeAndWait)(r.firstName, "f3");
|
|
484
|
-
expect(r.firstName).toHaveValue("f2");
|
|
485
|
-
expect(r.firstNameOriginal).toHaveTextContent("f2");
|
|
486
|
-
});
|
|
487
|
-
it("returns empty lists even when inputs are undefined", async () => {
|
|
488
|
-
const autoSave = jest.fn();
|
|
489
|
-
// Given a component
|
|
490
|
-
function TestComponent() {
|
|
491
|
-
// And there is no initial data (i.e. we're creating the author)
|
|
492
|
-
const form = (0, useFormState_1.useFormState)({
|
|
493
|
-
config: authorWithBooksConfig,
|
|
494
|
-
init: { input: undefined, map: (d) => d },
|
|
495
|
-
autoSave,
|
|
496
|
-
});
|
|
497
|
-
return ((0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("div", { "data-testid": "len", children: form.books.value.length }), (0, jsx_runtime_1.jsx)("div", { "data-testid": "dirty", children: String(form.books.dirty) })] }));
|
|
498
|
-
}
|
|
499
|
-
// Then we can still read it as empty
|
|
500
|
-
const r = await (0, rtl_utils_1.render)((0, jsx_runtime_1.jsx)(TestComponent, {}));
|
|
501
|
-
expect(r.len).toHaveTextContent("0");
|
|
502
|
-
expect(r.dirty).toHaveTextContent("false");
|
|
503
|
-
});
|
|
504
|
-
it("does not infinite loop when calling set inside of auto-save", async () => {
|
|
505
|
-
// Given a component
|
|
506
|
-
let autoSaves = 0;
|
|
507
|
-
function TestComponent() {
|
|
508
|
-
const config = authorConfig;
|
|
509
|
-
const data = { firstName: "f1", lastName: "f1" };
|
|
510
|
-
const form = (0, useFormState_1.useFormState)({
|
|
511
|
-
config,
|
|
512
|
-
init: { input: data },
|
|
513
|
-
// And there is reactive business logic in the `autoSave` method
|
|
514
|
-
async autoSave(state) {
|
|
515
|
-
state.lastName.set("l2");
|
|
516
|
-
autoSaves++;
|
|
517
|
-
},
|
|
518
|
-
});
|
|
519
|
-
return ((0, jsx_runtime_1.jsx)("div", { children: (0, jsx_runtime_1.jsx)("button", { "data-testid": "setFirst", onClick: () => form.firstName.set("f2") }) }));
|
|
520
|
-
}
|
|
521
|
-
const r = await (0, rtl_utils_1.render)((0, jsx_runtime_1.jsx)(TestComponent, {}));
|
|
522
|
-
// When we change firstName
|
|
523
|
-
await (0, rtl_utils_1.clickAndWait)(r.setFirst);
|
|
524
|
-
// When autoSave didn't infinite loop
|
|
525
|
-
expect(autoSaves).toEqual(1);
|
|
526
|
-
});
|
|
527
|
-
it("batches calls to auto-save", async () => {
|
|
528
|
-
// Given a component
|
|
529
|
-
const autoSave = jest.fn();
|
|
530
|
-
function TestComponent() {
|
|
531
|
-
const config = authorConfig;
|
|
532
|
-
const data = { firstName: "f1", lastName: "f1" };
|
|
533
|
-
const form = (0, useFormState_1.useFormState)({
|
|
534
|
-
config,
|
|
535
|
-
init: { input: data },
|
|
536
|
-
autoSave: (form) => autoSave(form.changedValue),
|
|
537
|
-
});
|
|
538
|
-
return ((0, jsx_runtime_1.jsx)("div", { children: (0, jsx_runtime_1.jsx)("button", { "data-testid": "set", onClick: () => {
|
|
539
|
-
// And it sets two fields in a single callback
|
|
540
|
-
form.firstName.set("f2");
|
|
541
|
-
form.lastName.set("l2");
|
|
542
|
-
} }) }));
|
|
543
|
-
}
|
|
544
|
-
const r = await (0, rtl_utils_1.render)((0, jsx_runtime_1.jsx)(TestComponent, {}));
|
|
545
|
-
// When we change both
|
|
546
|
-
await (0, rtl_utils_1.clickAndWait)(r.set);
|
|
547
|
-
// Then we only call autoSave once
|
|
548
|
-
expect(autoSave).toBeCalledTimes(1);
|
|
549
|
-
expect(autoSave).toBeCalledWith({ firstName: "f2", lastName: "l2" });
|
|
550
|
-
});
|
|
551
|
-
it("queues changes for auto save if a save is already in progress", async () => {
|
|
552
|
-
const autoSaveStub = jest.fn();
|
|
553
|
-
const config = {
|
|
554
|
-
id: { type: "value" },
|
|
555
|
-
firstName: { type: "value" },
|
|
556
|
-
lastName: { type: "value" },
|
|
557
|
-
};
|
|
558
|
-
// Given a component using `getObjectState` for lazily creating ObjectStates
|
|
559
|
-
function TestComponent() {
|
|
560
|
-
const [apiData, setApiData] = (0, react_2.useState)({ id: "a:1", firstName: "Brandon", lastName: "Dow" });
|
|
561
|
-
const state = (0, useFormState_1.useFormState)({ config, autoSave, init: { input: apiData, map: (v) => v } });
|
|
562
|
-
async function autoSave(form) {
|
|
563
|
-
autoSaveStub(form.changedValue);
|
|
564
|
-
const changed = form.changedValue;
|
|
565
|
-
// Pretend to make an API call and update the local state
|
|
566
|
-
await Promise.resolve(1);
|
|
567
|
-
setApiData((prevState) => ({ ...prevState, ...changed }));
|
|
568
|
-
}
|
|
569
|
-
return ((0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsxs)("div", { "data-testid": "name", children: [apiData.firstName, " ", apiData.lastName] }), (0, jsx_runtime_1.jsx)("button", { "data-testid": "focusSetAndSaveField", onClick: () => {
|
|
570
|
-
state.firstName.focus();
|
|
571
|
-
state.firstName.set("Foo");
|
|
572
|
-
state.firstName.maybeAutoSave();
|
|
573
|
-
state.firstName.blur();
|
|
574
|
-
} }), (0, jsx_runtime_1.jsx)("button", { "data-testid": "focusSetAndSaveFieldLastName", onClick: () => {
|
|
575
|
-
state.lastName.focus();
|
|
576
|
-
state.lastName.set("Bar");
|
|
577
|
-
state.lastName.maybeAutoSave();
|
|
578
|
-
} })] }));
|
|
579
|
-
}
|
|
580
|
-
const r = await (0, rtl_utils_1.render)((0, jsx_runtime_1.jsx)(TestComponent, {}));
|
|
581
|
-
// And triggering the auto save behavior before awaiting the initial promise to
|
|
582
|
-
// resolve so we have pending changes.
|
|
583
|
-
(0, rtl_utils_1.click)(r.focusSetAndSaveField, { allowAsync: true });
|
|
584
|
-
// Let the initial autoSave be called
|
|
585
|
-
(0, react_1.act)(() => {
|
|
586
|
-
jest.runOnlyPendingTimers();
|
|
587
|
-
});
|
|
588
|
-
expect(r.name).toHaveTextContent("Brandon Dow");
|
|
589
|
-
expect(autoSaveStub).toBeCalledTimes(1);
|
|
590
|
-
expect(autoSaveStub).toBeCalledWith({ id: "a:1", firstName: "Foo" });
|
|
591
|
-
// And while that is in flight, trigger another user action
|
|
592
|
-
(0, rtl_utils_1.click)(r.focusSetAndSaveFieldLastName, { allowAsync: true });
|
|
593
|
-
// (Use `wait` so that our timer flushes before the Promise.resolve(1) is ran)
|
|
594
|
-
await (0, rtl_utils_1.wait)();
|
|
595
|
-
// Then expect the auto save to only have been called twice. Once with each changedValues.
|
|
596
|
-
expect(autoSaveStub).toBeCalledTimes(2);
|
|
597
|
-
expect(autoSaveStub).toBeCalledWith({ id: "a:1", firstName: "Foo" });
|
|
598
|
-
expect(autoSaveStub).toBeCalledWith({ id: "a:1", lastName: "Bar" });
|
|
599
|
-
});
|
|
600
|
-
it("calls autoSave with results of calculations in addRules", async () => {
|
|
601
|
-
const autoSaveStub = jest.fn();
|
|
602
|
-
const config = {
|
|
603
|
-
id: { type: "value" },
|
|
604
|
-
firstName: { type: "value" },
|
|
605
|
-
lastName: { type: "value" },
|
|
606
|
-
};
|
|
607
|
-
// Given a component that is using autoSave
|
|
608
|
-
function TestComponent() {
|
|
609
|
-
const fs = (0, useFormState_1.useFormState)({
|
|
610
|
-
config,
|
|
611
|
-
addRules(fs) {
|
|
612
|
-
// And also has calculated values
|
|
613
|
-
(0, mobx_1.reaction)(() => fs.firstName.value, (curr) => (fs.lastName.value = curr));
|
|
614
|
-
},
|
|
615
|
-
autoSave: (fs) => autoSaveStub(fs.changedValue),
|
|
616
|
-
init: { input: { id: "a:1" } },
|
|
617
|
-
});
|
|
618
|
-
return (0, jsx_runtime_1.jsx)(FormStateApp_1.TextField, { field: fs.firstName });
|
|
619
|
-
}
|
|
620
|
-
const r = await (0, rtl_utils_1.render)((0, jsx_runtime_1.jsx)(TestComponent, {}));
|
|
621
|
-
// When the user sets one field
|
|
622
|
-
await (0, rtl_utils_1.typeAndWait)(r.firstName, "first");
|
|
623
|
-
// Then we only called autoSave once
|
|
624
|
-
expect(autoSaveStub).toBeCalledTimes(1);
|
|
625
|
-
expect(autoSaveStub).toBeCalledWith({ id: "a:1", firstName: "first", lastName: "first" });
|
|
626
|
-
});
|
|
627
|
-
it("sets loading if opts.loading is true", async () => {
|
|
628
|
-
const config = { firstName: { type: "value" } };
|
|
629
|
-
function TestComponent({ loading }) {
|
|
630
|
-
const form = (0, useFormState_1.useFormState)({ config, loading });
|
|
631
|
-
return (0, jsx_runtime_1.jsx)(mobx_react_1.Observer, { children: () => (0, jsx_runtime_1.jsx)("div", { "data-testid": "loading", children: String(form.loading) }) });
|
|
632
|
-
}
|
|
633
|
-
// And we initially pass in `init.query.loading: true`
|
|
634
|
-
const r = await (0, rtl_utils_1.render)((0, jsx_runtime_1.jsx)(TestComponent, { loading: true }));
|
|
635
|
-
// Then the form is marked as loading
|
|
636
|
-
expect(r.loading).toHaveTextContent("true");
|
|
637
|
-
// And when the query is not loading
|
|
638
|
-
await r.rerender((0, jsx_runtime_1.jsx)(TestComponent, { loading: false }));
|
|
639
|
-
// Then the form is marked as not loading
|
|
640
|
-
expect(r.loading).toHaveTextContent("false");
|
|
641
|
-
});
|
|
642
|
-
it("sets loading if input.data is undefined", async () => {
|
|
643
|
-
const config = { firstName: { type: "value" } };
|
|
644
|
-
function TestComponent({ data }) {
|
|
645
|
-
const form = (0, useFormState_1.useFormState)({ config, init: { input: data } });
|
|
646
|
-
return (0, jsx_runtime_1.jsx)(mobx_react_1.Observer, { children: () => (0, jsx_runtime_1.jsx)("div", { "data-testid": "loading", children: String(form.loading) }) });
|
|
647
|
-
}
|
|
648
|
-
// And we initially pass in `init.input: undefined`
|
|
649
|
-
const r = await (0, rtl_utils_1.render)((0, jsx_runtime_1.jsx)(TestComponent, { data: undefined }));
|
|
650
|
-
// Then the form is marked as loading
|
|
651
|
-
expect(r.loading).toHaveTextContent("true");
|
|
652
|
-
// And when the data is no longer undefined
|
|
653
|
-
await r.rerender((0, jsx_runtime_1.jsx)(TestComponent, { data: { firstName: "first" } }));
|
|
654
|
-
// Then the form is marked as not loading
|
|
655
|
-
expect(r.loading).toHaveTextContent("false");
|
|
656
|
-
});
|
|
657
|
-
it("sets loading if query.loading is true", async () => {
|
|
658
|
-
const config = { firstName: { type: "value" } };
|
|
659
|
-
function TestComponent({ loading, data }) {
|
|
660
|
-
const form = (0, useFormState_1.useFormState)({ config, init: { query: { data, loading, error: null }, map: (d) => d } });
|
|
661
|
-
return (0, jsx_runtime_1.jsx)(mobx_react_1.Observer, { children: () => (0, jsx_runtime_1.jsx)("div", { "data-testid": "loading", children: String(form.loading) }) });
|
|
662
|
-
}
|
|
663
|
-
// And we initially pass in `init.query.loading: true`
|
|
664
|
-
const r = await (0, rtl_utils_1.render)((0, jsx_runtime_1.jsx)(TestComponent, { loading: true, data: undefined }));
|
|
665
|
-
// Then the form is marked as loading
|
|
666
|
-
expect(r.loading).toHaveTextContent("true");
|
|
667
|
-
// And when the query is not loading
|
|
668
|
-
await r.rerender((0, jsx_runtime_1.jsx)(TestComponent, { loading: false, data: { firstName: "first" } }));
|
|
669
|
-
// Then the form is marked as not loading
|
|
670
|
-
expect(r.loading).toHaveTextContent("false");
|
|
671
|
-
});
|
|
672
|
-
it("treats the id changing as a whole new entity instead of a delete", async () => {
|
|
673
|
-
const config = { firstName: { type: "value" } };
|
|
674
|
-
function TestComponent() {
|
|
675
|
-
// Given an initial author a1
|
|
676
|
-
const [author, setAuthor] = (0, react_2.useState)({ id: "a:1", firstName: "a1" });
|
|
677
|
-
const form = (0, useFormState_1.useFormState)({ config, init: { input: author, map: (a) => a } });
|
|
678
|
-
return ((0, jsx_runtime_1.jsx)(mobx_react_1.Observer, { children: () => ((0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("div", { "data-testid": "value", children: String(form.firstName.value) }), (0, jsx_runtime_1.jsx)("div", { "data-testid": "dirty", children: String(form.firstName.dirty) }), (0, jsx_runtime_1.jsx)("div", { "data-testid": "originalValue", children: String(form.firstName.originalValue) }), (0, jsx_runtime_1.jsx)("div", { "data-testid": "objectValue", children: String(form.value.firstName) }), (0, jsx_runtime_1.jsx)("div", { "data-testid": "a1", onClick: () => setAuthor({ id: "a:1", firstName: "a1" }) }), (0, jsx_runtime_1.jsx)("div", { "data-testid": "a2", onClick: () => setAuthor({ id: "a:2", firstName: undefined }) })] })) }));
|
|
679
|
-
}
|
|
680
|
-
const r = await (0, rtl_utils_1.render)((0, jsx_runtime_1.jsx)(TestComponent, {}));
|
|
681
|
-
// And the value is initially a1/and not dirty
|
|
682
|
-
expect(r.value).toHaveTextContent("a1");
|
|
683
|
-
expect(r.dirty).toHaveTextContent("false");
|
|
684
|
-
expect(r.originalValue).toHaveTextContent("a1");
|
|
685
|
-
expect(r.objectValue).toHaveTextContent("a1");
|
|
686
|
-
// When we switch to a completely separate author
|
|
687
|
-
(0, rtl_utils_1.click)(r.a2);
|
|
688
|
-
// Then it switches to the next author
|
|
689
|
-
expect(r.value).toHaveTextContent("undefined");
|
|
690
|
-
// And the field is not dirty (which had been the case before this bug fix)
|
|
691
|
-
expect(r.dirty).toHaveTextContent("false");
|
|
692
|
-
expect(r.originalValue).toHaveTextContent("undefined");
|
|
693
|
-
expect(r.objectValue).toHaveTextContent("undefined");
|
|
694
|
-
// And when we switch back to the original author
|
|
695
|
-
(0, rtl_utils_1.click)(r.a1);
|
|
696
|
-
// It again restores the value and does not think an edit was WIP
|
|
697
|
-
expect(r.value).toHaveTextContent("a1");
|
|
698
|
-
expect(r.dirty).toHaveTextContent("false");
|
|
699
|
-
expect(r.originalValue).toHaveTextContent("a1");
|
|
700
|
-
expect(r.objectValue).toHaveTextContent("a1");
|
|
701
|
-
});
|
|
702
|
-
it("doesn't allow calling commitChanges", async () => {
|
|
703
|
-
// Given a component
|
|
704
|
-
const autoSave = jest.fn();
|
|
705
|
-
function TestComponent() {
|
|
706
|
-
const config = authorConfig;
|
|
707
|
-
const form = (0, useFormState_1.useFormState)({
|
|
708
|
-
config,
|
|
709
|
-
init: { input: { firstName: "f1", lastName: "f1" } },
|
|
710
|
-
autoSave: async (form) => {
|
|
711
|
-
autoSave(form.changedValue);
|
|
712
|
-
// And the autoSave functions erroneously calls commitChanges
|
|
713
|
-
form.commitChanges();
|
|
714
|
-
},
|
|
715
|
-
});
|
|
716
|
-
return (0, jsx_runtime_1.jsx)("button", { "data-testid": "set", onClick: () => form.firstName.set("f2") });
|
|
717
|
-
}
|
|
718
|
-
await (0, rtl_utils_1.render)((0, jsx_runtime_1.jsx)(TestComponent, {}));
|
|
719
|
-
// When we change a field, and try to commit, it blows up
|
|
720
|
-
// await expect(clickAndWait(r.set)).rejects.toThrow(
|
|
721
|
-
// "When using autoSave, you should not manually call commitChanges",
|
|
722
|
-
// );
|
|
723
|
-
//
|
|
724
|
-
// ...for some reason, I think maybe due to rtl-utils calling runOnlyPendingTimers
|
|
725
|
-
// instead of runOnlyPendingTimers async, the `clickAndWait` actually resolves
|
|
726
|
-
// successfully (it's not a rejected promise), and our test technically keeps running,
|
|
727
|
-
// except then somewhere in the guts of Jest, the fake timer infra realizes that a tick
|
|
728
|
-
// failed (our autosave is actually fired from a setTimout tick), and so the Jest test
|
|
729
|
-
// fails anyway, but without anyway for us to catch and do a `.toThrow` assertion.
|
|
730
|
-
});
|
|
731
|
-
});
|
|
732
|
-
const authorConfig = {
|
|
733
|
-
id: { type: "value" },
|
|
734
|
-
firstName: { type: "value" },
|
|
735
|
-
lastName: { type: "value" },
|
|
736
|
-
};
|
|
737
|
-
const authorWithBooksConfig = {
|
|
738
|
-
id: { type: "value" },
|
|
739
|
-
firstName: { type: "value" },
|
|
740
|
-
lastName: { type: "value" },
|
|
741
|
-
books: {
|
|
742
|
-
type: "list",
|
|
743
|
-
config: {
|
|
744
|
-
id: { type: "value" },
|
|
745
|
-
title: { type: "value", rules: [rules_1.required] },
|
|
746
|
-
classification: { type: "value" },
|
|
747
|
-
},
|
|
748
|
-
},
|
|
749
|
-
};
|
|
750
|
-
const authorWithAddressAndBooksConfig = {
|
|
751
|
-
id: { type: "value" },
|
|
752
|
-
firstName: { type: "value" },
|
|
753
|
-
lastName: { type: "value" },
|
|
754
|
-
address: {
|
|
755
|
-
type: "object",
|
|
756
|
-
config: {
|
|
757
|
-
id: { type: "value" },
|
|
758
|
-
street: { type: "value", rules: [rules_1.required] },
|
|
759
|
-
city: { type: "value" },
|
|
760
|
-
},
|
|
761
|
-
},
|
|
762
|
-
books: {
|
|
763
|
-
type: "list",
|
|
764
|
-
config: {
|
|
765
|
-
id: { type: "value" },
|
|
766
|
-
title: { type: "value", rules: [rules_1.required] },
|
|
767
|
-
classification: { type: "value" },
|
|
768
|
-
},
|
|
769
|
-
},
|
|
770
|
-
};
|
|
771
|
-
const authorWithBookFragments = {
|
|
772
|
-
id: { type: "value" },
|
|
773
|
-
firstName: { type: "value" },
|
|
774
|
-
books: {
|
|
775
|
-
type: "list",
|
|
776
|
-
config: {
|
|
777
|
-
id: { type: "value" },
|
|
778
|
-
data: { type: "fragment" },
|
|
779
|
-
},
|
|
780
|
-
},
|
|
781
|
-
};
|
|
782
|
-
/** Emulates a user focusing and then blurring a field. */
|
|
783
|
-
function focusAndBlur(state) {
|
|
784
|
-
state.focus();
|
|
785
|
-
state.blur();
|
|
786
|
-
}
|
|
787
|
-
function noop(value) { }
|