Skip to content

Commit 2f85d15

Browse files
committed
test: pin Immer 11.1.18 failures that Mutative handles
1 parent 766cbb9 commit 2f85d15

2 files changed

Lines changed: 201 additions & 1 deletion

File tree

‎test/fast-paths.test.ts‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,21 @@ describe('draft fast paths keep the original semantics', () => {
206206
expect(apply(next, inversePatches)).toEqual(base);
207207
});
208208

209+
test('a Set of array drafts can be iterated twice', () => {
210+
const base = { s: new Set([[1, 2], [3]]) };
211+
const next = create(base, (draft) => {
212+
for (const arr of draft.s) arr.push(0);
213+
let total = 0;
214+
for (const arr of draft.s) total += arr.length;
215+
expect(total).toBe(5);
216+
});
217+
expect([...next.s]).toEqual([
218+
[1, 2, 0],
219+
[3, 0],
220+
]);
221+
expect([...base.s]).toEqual([[1, 2], [3]]);
222+
});
223+
209224
test('wide object copies keep an own __proto__ key as a data property', () => {
210225
const base = JSON.parse('{"__proto__":{"polluted":1},"k":1}');
211226
for (let index = 0; index < 200; index += 1) base[`p${index}`] = index;

‎test/immer-non-support.test.ts‎

Lines changed: 186 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
/* eslint-disable prefer-destructuring */
77
/* eslint-disable no-param-reassign */
88
/* eslint-disable no-lone-blocks */
9+
/* eslint-disable no-extend-native */
910
import {
1011
produce,
1112
enableMapSet,
@@ -20,7 +21,7 @@ import {
2021
finishDraft,
2122
immerable,
2223
} from 'immer';
23-
import { create, apply, current } from '../src';
24+
import { create, apply, current, isDraft } from '../src';
2425
import { deepClone } from '../src/utils';
2526

2627
enableMapSet();
@@ -877,3 +878,187 @@ test('replays patches for a shared draft array', () => {
877878
checkImmerPatches(state, fn);
878879
checkMutativePatches(state, fn);
879880
});
881+
882+
// Immer's current() copies a plain Set and then iterates the copy while
883+
// set() appends the current value of every element to that same copy, so a
884+
// Set holding drafts or unfrozen draftable objects grows forever. The guard
885+
// below turns that runaway growth into an error instead of a hang.
886+
function withSetAddLimit<T>(limit: number, fn: () => T): T {
887+
const originalAdd = Set.prototype.add;
888+
let calls = 0;
889+
Set.prototype.add = function add(this: Set<unknown>, value: unknown) {
890+
calls += 1;
891+
if (calls > limit) {
892+
throw new Error(`Set.prototype.add was called more than ${limit} times`);
893+
}
894+
return originalAdd.call(this, value);
895+
} as typeof Set.prototype.add;
896+
try {
897+
return fn();
898+
} finally {
899+
Set.prototype.add = originalAdd;
900+
}
901+
}
902+
903+
test('current() on a plain Set that holds drafts', () => {
904+
const makeBase = (): any => ({ a: { child: { x: 1 } } });
905+
{
906+
// ! it should return a snapshot; instead current() never finishes
907+
expect(() =>
908+
withSetAddLimit(1000, () =>
909+
produce(makeBase(), (draft: any) => {
910+
draft.a.child.x = 2;
911+
draft.a.s = new Set([draft.a.child]);
912+
immerCurrent(draft.a);
913+
})
914+
)
915+
).toThrow('Set.prototype.add was called more than 1000 times');
916+
}
917+
{
918+
let snapshot: any;
919+
create(makeBase(), (draft: any) => {
920+
draft.a.child.x = 2;
921+
draft.a.s = new Set([draft.a.child]);
922+
snapshot = current(draft.a);
923+
});
924+
expect(snapshot.s.size).toBe(1);
925+
const [item] = [...snapshot.s];
926+
expect(isDraft(item)).toBe(false);
927+
expect(item).toEqual({ x: 2 });
928+
}
929+
});
930+
931+
test('current() on a plain Set of unfrozen objects', () => {
932+
{
933+
// ! it should return a snapshot; instead current() never finishes
934+
expect(() =>
935+
withSetAddLimit(1000, () =>
936+
produce({ a: { t: 0 } } as any, (draft: any) => {
937+
draft.a.s = new Set([{ q: 1 }]);
938+
draft.a.t = 1;
939+
immerCurrent(draft.a);
940+
})
941+
)
942+
).toThrow('Set.prototype.add was called more than 1000 times');
943+
}
944+
{
945+
const inner = new Set([{ q: 1 }]);
946+
let snapshot: any;
947+
create({ a: { t: 0 } } as any, (draft: any) => {
948+
draft.a.s = inner;
949+
draft.a.t = 1;
950+
snapshot = current(draft.a);
951+
});
952+
expect(snapshot.s).toBe(inner);
953+
expect(snapshot.s.size).toBe(1);
954+
}
955+
});
956+
957+
const isRevokedProxy = (value: unknown) => {
958+
try {
959+
Object.getPrototypeOf(value);
960+
return false;
961+
} catch {
962+
return true;
963+
}
964+
};
965+
966+
test('a plain Set that holds a draft is finalized', () => {
967+
type State = { a: { child: { x: number }; s?: Set<unknown> } };
968+
const makeBase = (): State => ({ a: { child: { x: 1 } } });
969+
const recipe = (draft: State) => {
970+
draft.a.child.x = 2;
971+
draft.a.s = new Set([draft.a.child]);
972+
};
973+
{
974+
enablePatches();
975+
// Immer adds the finalized child to the Set but leaves the revoked draft
976+
// proxy in it, and the add patch carries that Set.
977+
// ! the Set should hold only the finalized child and the patches should replay
978+
const base = makeBase();
979+
const [state, patches] = produceWithPatches(base, recipe);
980+
expect(state.a.s!.size).toBe(2);
981+
expect(Array.from(state.a.s!).filter(isRevokedProxy)).toHaveLength(1);
982+
expect(state.a.s!.has(state.a.child)).toBe(true);
983+
expect(() => applyPatches(base, patches)).toThrow(/revoked/);
984+
}
985+
{
986+
const base = makeBase();
987+
const [state, patches, inversePatches] = create(base, recipe, {
988+
enablePatches: true,
989+
});
990+
expect(state.a.s!.size).toBe(1);
991+
expect(state.a.s!.has(state.a.child)).toBe(true);
992+
expect(state.a.child).toEqual({ x: 2 });
993+
expect(apply(base, patches)).toEqual(state);
994+
expect(apply(state, inversePatches)).toEqual(base);
995+
}
996+
});
997+
998+
test('assigning a draft from an inner scope to an outer draft', () => {
999+
const makeBases = () => ({
1000+
outerBase: { slot: null as any, k: 0 },
1001+
innerBase: { item: { v: 1 } },
1002+
});
1003+
{
1004+
// The inner produce revokes its proxies when it returns, so the outer
1005+
// draft keeps a revoked proxy in `slot`, with or without patches.
1006+
// ! it should finalize the inner draft into the outer state
1007+
const plain = makeBases();
1008+
expect(() =>
1009+
produce(plain.outerBase, (outer: any) => {
1010+
produce(plain.innerBase, (inner: any) => {
1011+
inner.item.v = 2;
1012+
outer.slot = inner.item;
1013+
outer.k = 1;
1014+
});
1015+
})
1016+
).toThrow(/revoked/);
1017+
enablePatches();
1018+
const withPatches = makeBases();
1019+
expect(() =>
1020+
produceWithPatches(withPatches.outerBase, (outer: any) => {
1021+
produceWithPatches(withPatches.innerBase, (inner: any) => {
1022+
inner.item.v = 2;
1023+
outer.slot = inner.item;
1024+
outer.k = 1;
1025+
});
1026+
})
1027+
).toThrow(/revoked/);
1028+
}
1029+
{
1030+
const { outerBase, innerBase } = makeBases();
1031+
let innerResult: any;
1032+
const [outer, outerPatches, outerInversePatches] = create(
1033+
outerBase,
1034+
(o: any) => {
1035+
innerResult = create(
1036+
innerBase,
1037+
(i: any) => {
1038+
i.item.v = 2;
1039+
o.slot = i.item;
1040+
o.k = 1;
1041+
},
1042+
{ enablePatches: true }
1043+
);
1044+
},
1045+
{ enablePatches: true }
1046+
);
1047+
const [inner, innerPatches, innerInversePatches] = innerResult;
1048+
expect(outer).toEqual({ slot: { v: 2 }, k: 1 });
1049+
expect(inner).toEqual({ item: { v: 2 } });
1050+
expect(isDraft(outer.slot)).toBe(false);
1051+
// Each scope records only its own changes.
1052+
expect(outerPatches.map((patch: any) => patch.path)).toEqual([
1053+
['slot'],
1054+
['k'],
1055+
]);
1056+
expect(innerPatches.map((patch: any) => patch.path)).toEqual([
1057+
['item', 'v'],
1058+
]);
1059+
expect(apply(outerBase, outerPatches)).toEqual(outer);
1060+
expect(apply(outer, outerInversePatches)).toEqual(outerBase);
1061+
expect(apply(innerBase, innerPatches)).toEqual(inner);
1062+
expect(apply(inner, innerInversePatches)).toEqual(innerBase);
1063+
}
1064+
});

0 commit comments

Comments
 (0)