main
ts 166 lines 4.91 KB
Raw
1 /**
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 */
7
8 import buildMakeReadOnly from '../makeReadOnly';
9
10 describe('makeReadOnly', () => {
11 let logger: jest.Func;
12 let makeReadOnly: <T>(value: T, source: string) => T;
13
14 beforeEach(() => {
15 logger = jest.fn();
16 makeReadOnly = buildMakeReadOnly(logger, []);
17 });
18
19 describe('Tracking mutations', () => {
20 it('can be called with all primitives', () => {
21 const a = 5;
22 const b = true;
23 const c = null;
24 expect(makeReadOnly(a, 'test1')).toBe(a);
25 expect(makeReadOnly(b, 'test1')).toBe(b);
26 expect(makeReadOnly(c, 'test1')).toBe(c);
27 });
28
29 it('retains referential equality', () => {
30 const valA = {};
31 const valB = {a: valA, _: valA};
32 const o = {a: valA, b: valB, c: 'c'};
33 expect(makeReadOnly(o, 'test2')).toBe(o);
34 expect(makeReadOnly(o.a, 'test2')).toBe(valA);
35 expect(makeReadOnly(o.b, 'test2')).toBe(valB);
36 expect(makeReadOnly(o.b.a, 'test2')).toBe(valA);
37 expect(makeReadOnly(o.b._, 'test2')).toBe(valA);
38 expect(makeReadOnly(o.c, 'test2')).toBe('c');
39 });
40
41 it('deals with cyclic references', () => {
42 const o: any = {};
43 o.self_ref = o;
44 expect(makeReadOnly(o, 'test3')).toBe(o);
45 expect(makeReadOnly(o.self_ref, 'test3')).toBe(o);
46 });
47 it('logs direct interior mutability', () => {
48 const o = {a: 0};
49 makeReadOnly(o, 'test4');
50 o.a = 42;
51 expect(logger).toBeCalledWith('FORGET_MUTATE_IMMUT', 'test4', 'a', 42);
52 });
53
54 it('tracks changes to known RO properties', () => {
55 const o: any = {a: {}};
56 makeReadOnly(o, 'test5');
57 o.a = 42;
58 expect(logger).toBeCalledWith('FORGET_MUTATE_IMMUT', 'test5', 'a', 42);
59 expect(o.a).toBe(42);
60 const newVal = {x: 0};
61 o.a = newVal;
62 expect(logger).toBeCalledWith(
63 'FORGET_MUTATE_IMMUT',
64 'test5',
65 'a',
66 newVal,
67 );
68 expect(o.a).toBe(newVal);
69 });
70
71 it('logs aliased mutations', () => {
72 const o: any = {a: {x: 4}};
73
74 const alias = o;
75 makeReadOnly(o, 'test6');
76 const newVal = {};
77 alias.a = newVal;
78 expect(logger).toBeCalledWith(
79 'FORGET_MUTATE_IMMUT',
80 'test6',
81 'a',
82 newVal,
83 );
84 expect(o.a).toBe(newVal);
85 });
86
87 it('logs transitive interior mutability', () => {
88 const o: any = {a: {x: 0}};
89 makeReadOnly(o, 'test7');
90 o.a.x = 42;
91 expect(logger).toBeCalledWith('FORGET_MUTATE_IMMUT', 'test7', 'x', 42);
92 });
93
94 describe('todo', () => {
95 it('does not track newly added or deleted vals if makeReadOnly is only called once', () => {
96 // this is a limitation of the current "proxy" approach,
97 // which overwrites object properties with getters and setters
98 const x: any = {a: {}};
99 makeReadOnly(x, 'test8');
100
101 delete x.a;
102 x.b = 0;
103 expect(logger).toBeCalledTimes(0);
104 });
105 it('does not log aliased indirect mutations', () => {
106 // this could be easily implemented by making caching eager
107 const innerObj = {x: 0};
108 const o = {a: innerObj};
109 makeReadOnly(o, 'test9');
110 innerObj.x = 42;
111 expect(o.a.x).toBe(42);
112
113 const o1 = {a: {x: 0}};
114 const innerObj1 = o1.a;
115 makeReadOnly(o1, 'test9');
116 innerObj1.x = 42;
117
118 expect(o1.a.x).toBe(42);
119 expect(logger).toBeCalledTimes(0);
120 });
121
122 it('does not track objects with getter/setters', () => {
123 let backedX: string | null = null;
124 const o = {
125 set val(val: string | null) {
126 backedX = val;
127 },
128 get val(): string | null {
129 return backedX;
130 },
131 };
132 expect(makeReadOnly(o, 'test10')).toBe(o);
133 expect(makeReadOnly(o.val, 'test10')).toBe(null);
134
135 o.val = '40';
136 expect(logger).toBeCalledTimes(0);
137 });
138 });
139 });
140
141 describe('Tracking adding or deleting properties', () => {
142 it('tracks new properties added between calls to makeReadOnly', () => {
143 const o: any = {};
144 makeReadOnly(o, 'test11');
145 o.a = 'new value';
146 makeReadOnly(o, 'test11');
147 expect(logger).toBeCalledWith('FORGET_ADD_PROP_IMMUT', 'test11', 'a');
148 });
149 it('tracks properties deleted between calls to makeReadOnly', () => {
150 const o: any = {a: 0};
151 makeReadOnly(o, 'test12');
152 delete o.a;
153 makeReadOnly(o, 'test12');
154 expect(logger).toBeCalledWith('FORGET_DELETE_PROP_IMMUT', 'test12', 'a');
155 });
156
157 // it("tracks properties deleted and re-added between calls to makeReadOnly", () => {
158 // const o: any = { a: 0 };
159 // makeReadOnly(o);
160 // delete o.a;
161 // o.a = {};
162 // makeReadOnly(o);
163 // expect(logger).toBeCalledWith("FORGET_CHANGE_PROP_IMMUT", "a");
164 // });
165 });
166 });