From ab0574465dd8f6657ca9719760ed304a898b7768 Mon Sep 17 00:00:00 2001 From: aleclarson Date: Thu, 13 Dec 2018 21:52:08 -0500 Subject: [PATCH] test: hooks (onAssign, onDelete, onCopy) --- __tests__/__snapshots__/hooks.js.snap | 146 +++++++++++++++++ __tests__/hooks.js | 225 ++++++++++++++++++++++++++ 2 files changed, 371 insertions(+) create mode 100644 __tests__/__snapshots__/hooks.js.snap create mode 100644 __tests__/hooks.js diff --git a/__tests__/__snapshots__/hooks.js.snap b/__tests__/__snapshots__/hooks.js.snap new file mode 100644 index 00000000..654c197d --- /dev/null +++ b/__tests__/__snapshots__/hooks.js.snap @@ -0,0 +1,146 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`hooks - onAssign() when draft is an array assign 1`] = ` +Array [ + Array [ + 0, + 0, + ], +] +`; + +exports[`hooks - onAssign() when draft is an array push 1`] = ` +Array [ + Array [ + 0, + 4, + ], +] +`; + +exports[`hooks - onAssign() when draft is an array splice (length += 0) 1`] = ` +Array [ + Array [ + 1, + 0, + ], +] +`; + +exports[`hooks - onAssign() when draft is an array splice (length += 1) 1`] = ` +Array [ + Array [ + 1, + 0, + ], + Array [ + 2, + 0, + ], + Array [ + 3, + 3, + ], +] +`; + +exports[`hooks - onAssign() when draft is an array splice (length -= 1) 1`] = ` +Array [ + Array [ + 0, + 6, + ], + Array [ + 1, + 3, + ], +] +`; + +exports[`hooks - onAssign() when draft is an array unshift 1`] = ` +Array [ + Array [ + 0, + 0, + ], + Array [ + 1, + 1, + ], +] +`; + +exports[`hooks - onAssign() when draft is an object assign 1`] = ` +Array [ + Array [ + "a", + 1, + ], + Array [ + "c", + 1, + ], +] +`; + +exports[`hooks - onAssign() when draft is an object nested assignments 1`] = ` +Array [ + Array [ + "c", + 2, + ], + Array [ + "b", + Object { + "c": 2, + "e": 1, + }, + ], + Array [ + "a", + Object { + "b": Object { + "c": 2, + "e": 1, + }, + }, + ], +] +`; + +exports[`hooks - onDelete() when draft is an array - length = 0 1`] = `Array []`; + +exports[`hooks - onDelete() when draft is an array - pop 1`] = ` +Array [ + Array [ + "0", + ], +] +`; + +exports[`hooks - onDelete() when draft is an array - splice (length -= 1) 1`] = ` +Array [ + Array [ + "2", + ], +] +`; + +exports[`hooks - onDelete() when draft is an object - delete 1`] = ` +Array [ + Array [ + "a", + ], + Array [ + "c", + ], +] +`; + +exports[`hooks - onDelete() when draft is an object - nested deletions 1`] = ` +Array [ + Array [ + "c", + ], +] +`; diff --git a/__tests__/hooks.js b/__tests__/hooks.js new file mode 100644 index 00000000..2c43dc33 --- /dev/null +++ b/__tests__/hooks.js @@ -0,0 +1,225 @@ +"use strict" +import {Immer} from "../src/index" +import matchers from "expect/build/matchers" + +describe("hooks -", () => { + let produce, onAssign, onDelete, onCopy + + const reset = () => + ({produce, onAssign, onDelete, onCopy} = new Immer({ + autoFreeze: true, + onAssign: defuseProxies(jest.fn().mockName("onAssign")), + onDelete: defuseProxies(jest.fn().mockName("onDelete")), + onCopy: defuseProxies(jest.fn().mockName("onCopy")) + })) + + describe("onAssign()", () => { + beforeEach(reset) + useSharedTests(() => onAssign) + describe("when draft is an object", () => { + test("assign", () => { + produce({a: 0, b: 0, c: 0}, s => { + s.a++ + s.c++ + }) + expectCalls(onAssign) + }) + test("assign (no change)", () => { + produce({a: 0}, s => { + s.a = 0 + }) + expect(onAssign).not.toBeCalled() + }) + test("delete", () => { + produce({a: 1}, s => { + delete s.a + }) + expect(onAssign).not.toBeCalled() + }) + test("nested assignments", () => { + produce({a: {b: {c: 1, d: 1, e: 1}}}, s => { + const {b} = s.a + b.c = 2 + delete b.d + b.e = 1 // no-op + }) + expectCalls(onAssign) + }) + }) + describe("when draft is an array", () => { + test("assign", () => { + produce([1], s => { + s[0] = 0 + }) + expectCalls(onAssign) + }) + test("push", () => { + produce([], s => { + s.push(4) + }) + expectCalls(onAssign) + }) + test("pop", () => { + produce([1], s => { + s.pop() + }) + expect(onAssign).not.toBeCalled() + }) + test("unshift", () => { + produce([1], s => { + s.unshift(0) + }) + expectCalls(onAssign) + }) + test("length = 0", () => { + produce([1], s => { + s.length = 0 + }) + expect(onAssign).not.toBeCalled() + }) + test("splice (length += 1)", () => { + produce([1, 2, 3], s => { + s.splice(1, 1, 0, 0) + }) + expectCalls(onAssign) + }) + test("splice (length += 0)", () => { + produce([1, 2, 3], s => { + s.splice(1, 1, 0) + }) + expectCalls(onAssign) + }) + test("splice (length -= 1)", () => { + produce([1, 2, 3], s => { + s.splice(0, 2, 6) + }) + expectCalls(onAssign) + }) + }) + describe("when a draft is moved into a new object", () => { + it("is called in the right order", () => { + const calls = [] + onAssign.mockImplementation((_, prop) => { + calls.push(prop) + }) + produce({a: {b: 1, c: {}}}, s => { + s.a.b = 0 + s.a.c.d = 1 + s.x = {y: {z: s.a}} + delete s.a + }) + // Sibling properties use enumeration order, which means new + // properties come last among their siblings. The deepest + // properties always come first in their ancestor chain. + expect(calls).toEqual(["b", "d", "c", "x"]) + }) + }) + }) + + describe("onDelete()", () => { + beforeEach(reset) + useSharedTests(() => onDelete) + describe("when draft is an object -", () => { + test("delete", () => { + produce({a: 1, b: 1, c: 1}, s => { + delete s.a + delete s.c + }) + expectCalls(onDelete) + }) + test("delete (no change)", () => { + produce({}, s => { + delete s.a + }) + expect(onDelete).not.toBeCalled() + }) + test("nested deletions", () => { + produce({a: {b: {c: 1}}}, s => { + delete s.a.b.c + }) + expectCalls(onDelete) + }) + }) + describe("when draft is an array -", () => { + test("pop", () => { + produce([1], s => { + s.pop() + }) + expectCalls(onDelete) + }) + test("length = 0", () => { + produce([1], s => { + s.length = 0 + }) + expectCalls(onDelete) + }) + test("splice (length -= 1)", () => { + produce([1, 2, 3], s => { + s.splice(0, 2, 6) + }) + expectCalls(onDelete) + }) + }) + }) + + describe("onCopy()", () => { + beforeEach(reset) + useSharedTests(() => onCopy) + it("is called in the right order", () => { + const calls = [] + onCopy.mockImplementation(s => { + calls.push(s.base) + }) + const base = {a: {b: {c: 1}}} + produce(base, s => { + delete s.a.b.c + }) + expect(calls).toShallowEqual([base.a.b, base.a, base]) + }) + }) + + function useSharedTests(getHook) { + it("is called before the parent is frozen", () => { + const hook = getHook() + hook.mockImplementation(s => { + // Parent object must not be frozen. + expect(Object.isFrozen(s.base)).toBeFalsy() + }) + produce({a: {b: {c: 0}}}, s => { + if (hook == onDelete) delete s.a.b.c + else s.a.b.c = 1 + }) + expect(hook).toHaveBeenCalledTimes(hook == onDelete ? 1 : 3) + }) + } +}) + +// Produce a snapshot of the hook arguments (minus any draft state). +function expectCalls(hook) { + expect( + hook.mock.calls.map(call => { + return call.slice(1) + }) + ).toMatchSnapshot() +} + +// For defusing draft proxies. +function defuseProxies(fn) { + return Object.assign((...args) => { + expect(args[0].finalized).toBeTruthy() + args[0].draft = args[0].drafts = null + fn(...args) + }, fn) +} + +expect.extend({ + toShallowEqual(received, expected) { + const match = matchers.toBe(received, expected) + return match.pass || !received || typeof received !== "object" + ? match + : !Array.isArray(expected) || + (Array.isArray(received) && received.length === expected.length) + ? matchers.toEqual(received, expected) + : match + } +})