import buildMakeReadOnly from '../makeReadOnly';
describe('makeReadOnly', () => {
let logger: jest.Func;
let makeReadOnly: <T>(value: T, source: string) => T;
beforeEach(() => {
logger = jest.fn();
makeReadOnly = buildMakeReadOnly(logger, []);
});
describe('Tracking mutations', () => {
it('can be called with all primitives', () => {
const a = 5;
const b = true;
const c = null;
expect(makeReadOnly(a, 'test1')).toBe(a);
expect(makeReadOnly(b, 'test1')).toBe(b);
expect(makeReadOnly(c, 'test1')).toBe(c);
});
it('retains referential equality', () => {
const valA = {};
const valB = {a: valA, _: valA};
const o = {a: valA, b: valB, c: 'c'};
expect(makeReadOnly(o, 'test2')).toBe(o);
expect(makeReadOnly(o.a, 'test2')).toBe(valA);
expect(makeReadOnly(o.b, 'test2')).toBe(valB);
expect(makeReadOnly(o.b.a, 'test2')).toBe(valA);
expect(makeReadOnly(o.b._, 'test2')).toBe(valA);
expect(makeReadOnly(o.c, 'test2')).toBe('c');
});
it('deals with cyclic references', () => {
const o: any = {};
o.self_ref = o;
expect(makeReadOnly(o, 'test3')).toBe(o);
expect(makeReadOnly(o.self_ref, 'test3')).toBe(o);
});
it('logs direct interior mutability', () => {
const o = {a: 0};
makeReadOnly(o, 'test4');
o.a = 42;
expect(logger).toBeCalledWith('FORGET_MUTATE_IMMUT', 'test4', 'a', 42);
});
it('tracks changes to known RO properties', () => {
const o: any = {a: {}};
makeReadOnly(o, 'test5');
o.a = 42;
expect(logger).toBeCalledWith('FORGET_MUTATE_IMMUT', 'test5', 'a', 42);
expect(o.a).toBe(42);
const newVal = {x: 0};
o.a = newVal;
expect(logger).toBeCalledWith(
'FORGET_MUTATE_IMMUT',
'test5',
'a',
newVal,
);
expect(o.a).toBe(newVal);
});
it('logs aliased mutations', () => {
const o: any = {a: {x: 4}};
const alias = o;
makeReadOnly(o, 'test6');
const newVal = {};
alias.a = newVal;
expect(logger).toBeCalledWith(
'FORGET_MUTATE_IMMUT',
'test6',
'a',
newVal,
);
expect(o.a).toBe(newVal);
});
it('logs transitive interior mutability', () => {
const o: any = {a: {x: 0}};
makeReadOnly(o, 'test7');
o.a.x = 42;
expect(logger).toBeCalledWith('FORGET_MUTATE_IMMUT', 'test7', 'x', 42);
});
describe('todo', () => {
it('does not track newly added or deleted vals if makeReadOnly is only called once', () => {
const x: any = {a: {}};
makeReadOnly(x, 'test8');
delete x.a;
x.b = 0;
expect(logger).toBeCalledTimes(0);
});
it('does not log aliased indirect mutations', () => {
const innerObj = {x: 0};
const o = {a: innerObj};
makeReadOnly(o, 'test9');
innerObj.x = 42;
expect(o.a.x).toBe(42);
const o1 = {a: {x: 0}};
const innerObj1 = o1.a;
makeReadOnly(o1, 'test9');
innerObj1.x = 42;
expect(o1.a.x).toBe(42);
expect(logger).toBeCalledTimes(0);
});
it('does not track objects with getter/setters', () => {
let backedX: string | null = null;
const o = {
set val(val: string | null) {
backedX = val;
},
get val(): string | null {
return backedX;
},
};
expect(makeReadOnly(o, 'test10')).toBe(o);
expect(makeReadOnly(o.val, 'test10')).toBe(null);
o.val = '40';
expect(logger).toBeCalledTimes(0);
});
});
});
describe('Tracking adding or deleting properties', () => {
it('tracks new properties added between calls to makeReadOnly', () => {
const o: any = {};
makeReadOnly(o, 'test11');
o.a = 'new value';
makeReadOnly(o, 'test11');
expect(logger).toBeCalledWith('FORGET_ADD_PROP_IMMUT', 'test11', 'a');
});
it('tracks properties deleted between calls to makeReadOnly', () => {
const o: any = {a: 0};
makeReadOnly(o, 'test12');
delete o.a;
makeReadOnly(o, 'test12');
expect(logger).toBeCalledWith('FORGET_DELETE_PROP_IMMUT', 'test12', 'a');
});
});
});