@homebound/form-state 2.5.0 → 2.6.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/formState.js +16 -4
- package/dist/formState.test.js +92 -0
- package/dist/formStateDomain.d.ts +1 -0
- package/dist/setupTests.js +7 -2
- package/dist/useFormState.d.ts +1 -1
- package/dist/useFormState.js +26 -12
- package/dist/useFormState.test.js +55 -19
- package/package.json +5 -5
package/dist/formState.js
CHANGED
|
@@ -431,10 +431,22 @@ function newListFieldState(parentInstance, parentState, key, rules, listConfig,
|
|
|
431
431
|
},
|
|
432
432
|
get changedValue() {
|
|
433
433
|
const result = [];
|
|
434
|
-
const
|
|
434
|
+
const hasOpKey = Object.keys(listConfig.config).includes("op");
|
|
435
|
+
const hasLegacyOpKey = Object.keys(listConfig.config).some((key) => key === "delete" || key === "remove");
|
|
436
|
+
const incremental = listConfig.update === "incremental" ||
|
|
437
|
+
// Implicitly enable incremental mode if we see an op key
|
|
438
|
+
(listConfig.update === undefined && (hasOpKey || hasLegacyOpKey));
|
|
439
|
+
const exhaustive = !incremental;
|
|
435
440
|
this.rows.forEach((r) => {
|
|
436
|
-
|
|
437
|
-
|
|
441
|
+
var _a;
|
|
442
|
+
var _b;
|
|
443
|
+
if (exhaustive || r.dirty || r.isNewEntity) {
|
|
444
|
+
const changed = r.changedValue;
|
|
445
|
+
// Ensure we have an `op: include` key, following https://joist-orm.io/docs/features/partial-update-apis
|
|
446
|
+
if (incremental && hasOpKey) {
|
|
447
|
+
(_a = (_b = changed).op) !== null && _a !== void 0 ? _a : (_b.op = "include");
|
|
448
|
+
}
|
|
449
|
+
result.push(changed);
|
|
438
450
|
}
|
|
439
451
|
});
|
|
440
452
|
return result;
|
|
@@ -512,7 +524,7 @@ function newListFieldState(parentInstance, parentState, key, rules, listConfig,
|
|
|
512
524
|
this.rows.forEach((r) => {
|
|
513
525
|
r.save();
|
|
514
526
|
});
|
|
515
|
-
originalCopy = parentInstance[key];
|
|
527
|
+
originalCopy = [...(parentInstance[key] || [])];
|
|
516
528
|
_tick.value++;
|
|
517
529
|
},
|
|
518
530
|
ensureSet() {
|
package/dist/formState.test.js
CHANGED
|
@@ -979,6 +979,81 @@ describe("formState", () => {
|
|
|
979
979
|
books: [{ id: "b:1", title: "t1b" }],
|
|
980
980
|
});
|
|
981
981
|
});
|
|
982
|
+
it("uses the op key as a hint to use incremental list behavior", () => {
|
|
983
|
+
// Given an author
|
|
984
|
+
const formState = formState_1.createObjectState({
|
|
985
|
+
id: { type: "value" },
|
|
986
|
+
// And the books collection is not explicitly marked as incremental
|
|
987
|
+
books: {
|
|
988
|
+
type: "list",
|
|
989
|
+
config: {
|
|
990
|
+
id: { type: "value" },
|
|
991
|
+
title: { type: "value", rules: [formState_1.required] },
|
|
992
|
+
// But it does have an op key
|
|
993
|
+
op: { type: "value" },
|
|
994
|
+
},
|
|
995
|
+
},
|
|
996
|
+
}, {
|
|
997
|
+
id: "a:1",
|
|
998
|
+
firstName: "f",
|
|
999
|
+
books: [
|
|
1000
|
+
// And the books start out as included
|
|
1001
|
+
{ id: "b:1", title: "t1", op: "include" },
|
|
1002
|
+
{ id: "b:2", title: "t2", op: "include" },
|
|
1003
|
+
],
|
|
1004
|
+
});
|
|
1005
|
+
// And initially nothing is changed
|
|
1006
|
+
expect(formState.changedValue).toEqual({ id: "a:1" });
|
|
1007
|
+
// When we delete the 1st book
|
|
1008
|
+
formState.books.rows[0].op.value = "delete";
|
|
1009
|
+
// Then only the 1st book is included
|
|
1010
|
+
expect(formState.changedValue).toEqual({
|
|
1011
|
+
id: "a:1",
|
|
1012
|
+
books: [{ id: "b:1", op: "delete" }],
|
|
1013
|
+
});
|
|
1014
|
+
});
|
|
1015
|
+
it("defaults the incremental op key to included when not set", () => {
|
|
1016
|
+
// Given an author
|
|
1017
|
+
const formState = formState_1.createObjectState({
|
|
1018
|
+
id: { type: "value" },
|
|
1019
|
+
// And the books collection is not explicitly marked as incremental
|
|
1020
|
+
books: {
|
|
1021
|
+
type: "list",
|
|
1022
|
+
config: {
|
|
1023
|
+
id: { type: "value" },
|
|
1024
|
+
title: { type: "value", rules: [formState_1.required] },
|
|
1025
|
+
// But it does have an op key
|
|
1026
|
+
op: { type: "value" },
|
|
1027
|
+
},
|
|
1028
|
+
},
|
|
1029
|
+
}, {
|
|
1030
|
+
id: "a:1",
|
|
1031
|
+
firstName: "f",
|
|
1032
|
+
books: [
|
|
1033
|
+
// And no op keys are initially set
|
|
1034
|
+
{ id: "b:1", title: "t1" },
|
|
1035
|
+
{ id: "b:2", title: "t2" },
|
|
1036
|
+
],
|
|
1037
|
+
});
|
|
1038
|
+
// And initially nothing is changed
|
|
1039
|
+
expect(formState.changedValue).toEqual({ id: "a:1" });
|
|
1040
|
+
// When we delete the 1st book
|
|
1041
|
+
formState.books.rows[0].op.value = "delete";
|
|
1042
|
+
// And add a 3rd book
|
|
1043
|
+
formState.books.add({ title: "t3" });
|
|
1044
|
+
// Then the 3rd book doesn't have an explicit op yet
|
|
1045
|
+
expect(formState.books.rows[2].op.value).toBeUndefined();
|
|
1046
|
+
// But when we create changedValue
|
|
1047
|
+
expect(formState.changedValue).toEqual({
|
|
1048
|
+
id: "a:1",
|
|
1049
|
+
books: [
|
|
1050
|
+
// Then the deleted book is included
|
|
1051
|
+
{ id: "b:1", op: "delete" },
|
|
1052
|
+
// And the 3rd book is included w/the `op: include` flag added
|
|
1053
|
+
{ title: "t3", op: "include" },
|
|
1054
|
+
],
|
|
1055
|
+
});
|
|
1056
|
+
});
|
|
982
1057
|
it("can observe value changes", () => {
|
|
983
1058
|
const formState = formState_1.createObjectState(authorWithBooksConfig, { firstName: "f" });
|
|
984
1059
|
let ticks = 0;
|
|
@@ -1145,6 +1220,23 @@ describe("formState", () => {
|
|
|
1145
1220
|
expect(formState.books.rows[1].isNewEntity).toBeTruthy();
|
|
1146
1221
|
expect(formState.books.isNewEntity).toBeFalsy();
|
|
1147
1222
|
});
|
|
1223
|
+
it("can properly evaluate dirty lists after calling 'save'", () => {
|
|
1224
|
+
// Given an empty list state
|
|
1225
|
+
const a1 = createAuthorInputState({ books: [] });
|
|
1226
|
+
expect(a1.books.dirty).toBeFalsy();
|
|
1227
|
+
// When adding a book
|
|
1228
|
+
a1.books.add({ title: "t2" });
|
|
1229
|
+
// Then the list state is dirty
|
|
1230
|
+
expect(a1.books.dirty).toBeTruthy();
|
|
1231
|
+
// When saving the form state
|
|
1232
|
+
a1.save();
|
|
1233
|
+
// Then the list is no longer dirty
|
|
1234
|
+
expect(a1.books.dirty).toBeFalsy();
|
|
1235
|
+
// When adding a new book
|
|
1236
|
+
a1.books.add({ title: "t3" });
|
|
1237
|
+
// The list is again dirty
|
|
1238
|
+
expect(a1.books.dirty).toBeTruthy();
|
|
1239
|
+
});
|
|
1148
1240
|
});
|
|
1149
1241
|
class ObservableObject {
|
|
1150
1242
|
constructor() {
|
package/dist/setupTests.js
CHANGED
|
@@ -4,5 +4,10 @@ require("@testing-library/jest-dom");
|
|
|
4
4
|
const mobx_1 = require("mobx");
|
|
5
5
|
// formState doesn't use actions
|
|
6
6
|
mobx_1.configure({ enforceActions: "never" });
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
jest.useFakeTimers("modern");
|
|
9
|
+
});
|
|
10
|
+
afterEach(() => {
|
|
11
|
+
jest.runOnlyPendingTimers();
|
|
12
|
+
jest.useRealTimers();
|
|
13
|
+
});
|
package/dist/useFormState.d.ts
CHANGED
|
@@ -37,7 +37,7 @@ export declare type UseFormStateOpts<T, I> = {
|
|
|
37
37
|
*
|
|
38
38
|
* Does not need to be stable/useMemo'd.
|
|
39
39
|
*/
|
|
40
|
-
autoSave?: (state: ObjectState<T>) => Promise<
|
|
40
|
+
autoSave?: (state: ObjectState<T>) => Promise<unknown>;
|
|
41
41
|
};
|
|
42
42
|
/**
|
|
43
43
|
* Creates a formState instance for editing in a form.
|
package/dist/useFormState.js
CHANGED
|
@@ -12,6 +12,7 @@ let isAutoSaving = false;
|
|
|
12
12
|
// `pendingAutoSave` is a flag for determining if we need to immediately call `maybeAutoSave` again after the initial Promise finishes
|
|
13
13
|
// This could happen if a field triggers auto-save while another field's auto-save is already in progress.
|
|
14
14
|
let pendingAutoSave = false;
|
|
15
|
+
let timerId;
|
|
15
16
|
/**
|
|
16
17
|
* Creates a formState instance for editing in a form.
|
|
17
18
|
*/
|
|
@@ -27,23 +28,36 @@ function useFormState(opts) {
|
|
|
27
28
|
// If they're using init.input, useMemo on it (and it might be an array), otherwise allow the identity of init be unstable
|
|
28
29
|
const dep = init && "input" in init && "map" in init ? (Array.isArray(init.input) ? init.input : [init.input]) : [];
|
|
29
30
|
const form = react_1.useMemo(() => {
|
|
30
|
-
|
|
31
|
-
if (isAutoSaving) {
|
|
31
|
+
function maybeAutoSave() {
|
|
32
|
+
if (isAutoSaving === "in-flight") {
|
|
32
33
|
pendingAutoSave = true;
|
|
33
34
|
}
|
|
35
|
+
else if (isAutoSaving === "queued") {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
34
38
|
// Don't use canSave() because we don't want to set touched for all the fields
|
|
35
39
|
if (autoSaveRef.current && form.dirty && form.valid && !isAutoSaving) {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
await
|
|
40
|
+
isAutoSaving = "queued";
|
|
41
|
+
// We use setTimeout as a cheap way to wait until the end of the current event listener
|
|
42
|
+
timerId = setTimeout(async () => {
|
|
43
|
+
try {
|
|
44
|
+
// We technically don't flip to in-flight until after the call in case the
|
|
45
|
+
// user's autoSave function itself wants to call a .set.
|
|
46
|
+
const promise = autoSaveRef.current(form);
|
|
47
|
+
isAutoSaving = "in-flight";
|
|
48
|
+
await promise;
|
|
49
|
+
}
|
|
50
|
+
finally {
|
|
51
|
+
isAutoSaving = false;
|
|
52
|
+
if (pendingAutoSave) {
|
|
53
|
+
pendingAutoSave = false;
|
|
54
|
+
// Push out the follow-up by 1 tick to allow refreshes to happen to potentially
|
|
55
|
+
// un-dirty the just-saved data (e.g. if we run right away, the caller's maybeAutoSave
|
|
56
|
+
// will see a form.changedValue that thinks the just-saved data is still dirty).
|
|
57
|
+
setTimeout(maybeAutoSave, 0);
|
|
58
|
+
}
|
|
45
59
|
}
|
|
46
|
-
}
|
|
60
|
+
}, 0);
|
|
47
61
|
}
|
|
48
62
|
}
|
|
49
63
|
const value = firstRunRef.current ? firstInitValue : utils_1.initValue(config, init);
|
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
4
4
|
const rtl_utils_1 = require("@homebound/rtl-utils");
|
|
5
|
+
const react_1 = require("@testing-library/react");
|
|
5
6
|
const mobx_react_1 = require("mobx-react");
|
|
6
|
-
const
|
|
7
|
+
const react_2 = require("react");
|
|
7
8
|
const formState_1 = require("./formState");
|
|
8
9
|
const useFormState_1 = require("./useFormState");
|
|
9
10
|
describe("useFormState", () => {
|
|
@@ -26,7 +27,7 @@ describe("useFormState", () => {
|
|
|
26
27
|
it("memoizes on init.input being an array", async () => {
|
|
27
28
|
const config = { firstName: { type: "value" } };
|
|
28
29
|
function TestComponent() {
|
|
29
|
-
const [, setTick] =
|
|
30
|
+
const [, setTick] = react_2.useState(0);
|
|
30
31
|
const form = useFormState_1.useFormState({
|
|
31
32
|
config,
|
|
32
33
|
init: { input: ["a", "b"], map: ([a, b]) => ({ firstName: a + b }) },
|
|
@@ -85,7 +86,7 @@ describe("useFormState", () => {
|
|
|
85
86
|
it("uses init if set as a value", async () => {
|
|
86
87
|
const config = { firstName: { type: "value" } };
|
|
87
88
|
function TestComponent() {
|
|
88
|
-
const [, setTick] =
|
|
89
|
+
const [, setTick] = react_2.useState(0);
|
|
89
90
|
const form = useFormState_1.useFormState({
|
|
90
91
|
config,
|
|
91
92
|
// That's using a raw init value
|
|
@@ -153,7 +154,7 @@ describe("useFormState", () => {
|
|
|
153
154
|
],
|
|
154
155
|
};
|
|
155
156
|
// And we start out with data1
|
|
156
|
-
const [data, setData] =
|
|
157
|
+
const [data, setData] = react_2.useState(data1);
|
|
157
158
|
const form = useFormState_1.useFormState({ config, init: { input: data, map: (d) => d } });
|
|
158
159
|
function makeLocalChanges() {
|
|
159
160
|
form.firstName.value = "local";
|
|
@@ -229,7 +230,7 @@ describe("useFormState", () => {
|
|
|
229
230
|
books: [{ title: "a2" }],
|
|
230
231
|
};
|
|
231
232
|
// And we start out with data1
|
|
232
|
-
const [data, setData] =
|
|
233
|
+
const [data, setData] = react_2.useState(data1);
|
|
233
234
|
const form = useFormState_1.useFormState({
|
|
234
235
|
config,
|
|
235
236
|
init: { input: data, map: (d) => d },
|
|
@@ -270,9 +271,9 @@ describe("useFormState", () => {
|
|
|
270
271
|
const data1 = { firstName: "f1", lastName: "l1" };
|
|
271
272
|
const data2 = { firstName: "f2", lastName: "l2" };
|
|
272
273
|
// And we start out with data1
|
|
273
|
-
const [data, setData] =
|
|
274
|
-
const author =
|
|
275
|
-
const config =
|
|
274
|
+
const [data, setData] = react_2.useState(data1);
|
|
275
|
+
const author = react_2.useMemo(() => new AuthorRow(data.firstName, data.lastName), [data]);
|
|
276
|
+
const config = react_2.useMemo(() => ({
|
|
276
277
|
firstName: { type: "value" },
|
|
277
278
|
lastName: { type: "value" },
|
|
278
279
|
fullName: { type: "value", computed: true },
|
|
@@ -296,7 +297,7 @@ describe("useFormState", () => {
|
|
|
296
297
|
// Given a component
|
|
297
298
|
function TestComponent() {
|
|
298
299
|
// When the data is initially undefined
|
|
299
|
-
const [data, setData] =
|
|
300
|
+
const [data, setData] = react_2.useState();
|
|
300
301
|
const data2 = { books: [{ title: "Title 1" }] };
|
|
301
302
|
const form = useFormState_1.useFormState({
|
|
302
303
|
config: authorWithBooksConfig,
|
|
@@ -317,7 +318,7 @@ describe("useFormState", () => {
|
|
|
317
318
|
// Then we don't auto-save because nothing has changed
|
|
318
319
|
expect(autoSave).toBeCalledTimes(0);
|
|
319
320
|
// And when adding a new book
|
|
320
|
-
rtl_utils_1.
|
|
321
|
+
await rtl_utils_1.clickAndWait(r.add);
|
|
321
322
|
// We autoSave the new row right away (because we don't have any validation rules
|
|
322
323
|
// that say the new row can't be empty)
|
|
323
324
|
expect(autoSave).toBeCalledTimes(1);
|
|
@@ -345,11 +346,35 @@ describe("useFormState", () => {
|
|
|
345
346
|
}
|
|
346
347
|
const r = await rtl_utils_1.render(jsx_runtime_1.jsx(TestComponent, {}, void 0));
|
|
347
348
|
// When we change firstName
|
|
348
|
-
rtl_utils_1.
|
|
349
|
+
await rtl_utils_1.clickAndWait(r.setFirst);
|
|
349
350
|
// When autoSave didn't infinite loop
|
|
350
351
|
expect(autoSaves).toEqual(1);
|
|
351
352
|
});
|
|
352
|
-
it("
|
|
353
|
+
it("batches calls to auto-save", async () => {
|
|
354
|
+
// Given a component
|
|
355
|
+
const autoSave = jest.fn();
|
|
356
|
+
function TestComponent() {
|
|
357
|
+
const config = authorConfig;
|
|
358
|
+
const data = { firstName: "f1", lastName: "f1" };
|
|
359
|
+
const form = useFormState_1.useFormState({
|
|
360
|
+
config,
|
|
361
|
+
init: data,
|
|
362
|
+
autoSave: (form) => autoSave(form.changedValue),
|
|
363
|
+
});
|
|
364
|
+
return (jsx_runtime_1.jsx("div", { children: jsx_runtime_1.jsx("button", { "data-testid": "set", onClick: () => {
|
|
365
|
+
// And it sets two fields in a single callback
|
|
366
|
+
form.firstName.set("f2");
|
|
367
|
+
form.lastName.set("l2");
|
|
368
|
+
} }, void 0) }, void 0));
|
|
369
|
+
}
|
|
370
|
+
const r = await rtl_utils_1.render(jsx_runtime_1.jsx(TestComponent, {}, void 0));
|
|
371
|
+
// When we change both
|
|
372
|
+
await rtl_utils_1.clickAndWait(r.set);
|
|
373
|
+
// Then we only call autoSave once
|
|
374
|
+
expect(autoSave).toBeCalledTimes(1);
|
|
375
|
+
expect(autoSave).toBeCalledWith({ firstName: "f2", lastName: "l2" });
|
|
376
|
+
});
|
|
377
|
+
it("queues changes for auto save if a save is already in progress", async () => {
|
|
353
378
|
const autoSaveStub = jest.fn();
|
|
354
379
|
const config = {
|
|
355
380
|
id: { type: "value" },
|
|
@@ -358,15 +383,17 @@ describe("useFormState", () => {
|
|
|
358
383
|
};
|
|
359
384
|
// Given a component using `getObjectState` for lazily creating ObjectStates
|
|
360
385
|
function TestComponent() {
|
|
361
|
-
const [apiData, setApiData] =
|
|
386
|
+
const [apiData, setApiData] = react_2.useState({ id: "a:1", firstName: "Brandon", lastName: "Dow" });
|
|
362
387
|
const state = useFormState_1.useFormState({ config, autoSave, init: { input: apiData, map: (v) => v } });
|
|
363
388
|
async function autoSave(form) {
|
|
364
389
|
autoSaveStub(form.changedValue);
|
|
390
|
+
const changed = form.changedValue;
|
|
365
391
|
// Pretend to make an API call and update the local state
|
|
366
|
-
setApiData((prevState) => ({ ...prevState, ...form.changedValue }));
|
|
367
392
|
await Promise.resolve(1);
|
|
393
|
+
setApiData((prevState) => ({ ...prevState, ...changed }));
|
|
368
394
|
}
|
|
369
|
-
return (jsx_runtime_1.jsxs("div", { children: [jsx_runtime_1.
|
|
395
|
+
return (jsx_runtime_1.jsxs("div", { children: [jsx_runtime_1.jsxs("div", Object.assign({ "data-testid": "name" }, { children: [apiData.firstName, " ", apiData.lastName] }), void 0),
|
|
396
|
+
jsx_runtime_1.jsx("button", { "data-testid": "focusSetAndSaveField", onClick: () => {
|
|
370
397
|
state.firstName.focus();
|
|
371
398
|
state.firstName.set("Foo");
|
|
372
399
|
state.firstName.maybeAutoSave();
|
|
@@ -379,10 +406,19 @@ describe("useFormState", () => {
|
|
|
379
406
|
} }, void 0)] }, void 0));
|
|
380
407
|
}
|
|
381
408
|
const r = await rtl_utils_1.render(jsx_runtime_1.jsx(TestComponent, {}, void 0));
|
|
382
|
-
// And triggering the auto save behavior before awaiting the initial promise to
|
|
383
|
-
|
|
384
|
-
rtl_utils_1.click(r.
|
|
385
|
-
//
|
|
409
|
+
// And triggering the auto save behavior before awaiting the initial promise to
|
|
410
|
+
// resolve so we have pending changes.
|
|
411
|
+
rtl_utils_1.click(r.focusSetAndSaveField(), { allowAsync: true });
|
|
412
|
+
// Let the initial autoSave be called
|
|
413
|
+
react_1.act(() => {
|
|
414
|
+
jest.runOnlyPendingTimers();
|
|
415
|
+
});
|
|
416
|
+
expect(r.name()).toHaveTextContent("Brandon Dow");
|
|
417
|
+
expect(autoSaveStub).toBeCalledTimes(1);
|
|
418
|
+
expect(autoSaveStub).toBeCalledWith({ id: "a:1", firstName: "Foo" });
|
|
419
|
+
// And while that is in flight, trigger another user action
|
|
420
|
+
rtl_utils_1.click(r.focusSetAndSaveFieldLastName(), { allowAsync: true });
|
|
421
|
+
// (Use `wait` so that our timer flushes before the Promise.resolve(1) is ran)
|
|
386
422
|
await rtl_utils_1.wait();
|
|
387
423
|
// Then expect the auto save to only have been called twice. Once with each changedValues.
|
|
388
424
|
expect(autoSaveStub).toBeCalledTimes(2);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@homebound/form-state",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.6.0",
|
|
4
4
|
"author": "Homebound",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -40,16 +40,16 @@
|
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
42
|
"@babel/core": "^7.13.1",
|
|
43
|
-
"@homebound/rtl-utils": "^2.
|
|
43
|
+
"@homebound/rtl-utils": "^2.59.2",
|
|
44
44
|
"@semantic-release/git": "^9.0.0",
|
|
45
45
|
"@storybook/addon-essentials": "^6.2.9",
|
|
46
46
|
"@storybook/addon-info": "^5.3.21",
|
|
47
47
|
"@storybook/addon-links": "^6.2.9",
|
|
48
48
|
"@storybook/addons": "^6.2.9",
|
|
49
49
|
"@storybook/react": "^6.2.9",
|
|
50
|
-
"@testing-library/dom": "^
|
|
51
|
-
"@testing-library/jest-dom": "^5.
|
|
52
|
-
"@testing-library/react": "^
|
|
50
|
+
"@testing-library/dom": "^8.11.3",
|
|
51
|
+
"@testing-library/jest-dom": "^5.16.2",
|
|
52
|
+
"@testing-library/react": "^12.1.2",
|
|
53
53
|
"@tsconfig/recommended": "^1.0.1",
|
|
54
54
|
"@types/jest": "^26.0.20",
|
|
55
55
|
"@types/react": "^16.14.5",
|