Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Async recipes #307

Merged
merged 15 commits into from Feb 2, 2019
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
101 changes: 101 additions & 0 deletions __tests__/__snapshots__/manual.js.snap
@@ -0,0 +1,101 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`manual - es5 cannot modify after finish 1`] = `"Cannot use a proxy that has been revoked. Did you pass an object from inside an immer function to an async process? {\\"a\\":2}"`;

exports[`manual - es5 should check arguments 1`] = `"First argument to createDraft should be a plain object, an array, or an immerable object."`;

exports[`manual - es5 should check arguments 2`] = `"First argument to createDraft should be a plain object, an array, or an immerable object."`;

exports[`manual - es5 should check arguments 3`] = `"First argument to finishDraft should be an object from createDraft."`;

exports[`manual - es5 should not finish drafts from produce 1`] = `"The draft provided was not created using \`createDraft\`"`;

exports[`manual - es5 should not finish twice 1`] = `"The draft provided was has already been finished"`;

exports[`manual - es5 should support patches drafts 1`] = `
Array [
Array [
Array [
Object {
"op": "replace",
"path": Array [
"a",
],
"value": 2,
},
Object {
"op": "add",
"path": Array [
"b",
],
"value": 3,
},
],
Array [
Object {
"op": "replace",
"path": Array [
"a",
],
"value": 1,
},
Object {
"op": "remove",
"path": Array [
"b",
],
},
],
],
]
`;

exports[`manual - proxy cannot modify after finish 1`] = `"Cannot use a proxy that has been revoked. Did you pass an object from inside an immer function to an async process? {\\"a\\":2}"`;

exports[`manual - proxy should check arguments 1`] = `"First argument to createDraft should be a plain object, an array, or an immerable object."`;

exports[`manual - proxy should check arguments 2`] = `"First argument to createDraft should be a plain object, an array, or an immerable object."`;

exports[`manual - proxy should check arguments 3`] = `"First argument to finishDraft should be an object from createDraft."`;

exports[`manual - proxy should not finish drafts from produce 1`] = `"The draft provided was not created using \`createDraft\`"`;

exports[`manual - proxy should not finish twice 1`] = `"The draft provided was has already been finished"`;

exports[`manual - proxy should support patches drafts 1`] = `
Array [
Array [
Array [
Object {
"op": "replace",
"path": Array [
"a",
],
"value": 2,
},
Object {
"op": "add",
"path": Array [
"b",
],
"value": 3,
},
],
Array [
Object {
"op": "replace",
"path": Array [
"a",
],
"value": 1,
},
Object {
"op": "remove",
"path": Array [
"b",
],
},
],
],
]
`;
61 changes: 60 additions & 1 deletion __tests__/base.js
Expand Up @@ -815,7 +815,7 @@ function runBaseTest(name, useProxies, autoFreeze, useListener) {
expect(next.obj).toBe(next.arr[0])
})

it("can return an object with two references to any pristine draft", () => {
it("can return an object with two references to an unmodified draft", () => {
const base = {a: {}}
const next = produce(base, d => {
return [d.a, d.a]
Expand All @@ -833,6 +833,65 @@ function runBaseTest(name, useProxies, autoFreeze, useListener) {
})
})

// TODO: rewrite tests with async/await once node 6 support is dropped
describe("async recipe function", () => {
it("can modify the draft", () => {
const base = {a: 0, b: 0}
return produce(base, d => {
d.a = 1
return Promise.resolve().then(() => {
d.b = 1
})
}).then(res => {
expect(res).not.toBe(base)
expect(res).toEqual({a: 1, b: 1})
})
})

it("works with rejected promises", () => {
let draft
const base = {a: 0, b: 0}
const err = new Error("passed")
return produce(base, d => {
draft = d
draft.b = 1
return Promise.reject(err)
}).then(
() => {
throw "failed"
},
e => {
expect(e).toBe(err)
expect(() => draft.a).toThrowError(/revoked/)
}
)
})

it("supports recursive produce calls after await", () => {
const base = {obj: {k: 1}}
return produce(base, d => {
const obj = d.obj
delete d.obj
return Promise.resolve().then(() => {
d.a = produce({}, d => {
d.b = obj // Assign a draft owned by the parent scope.
})

// Auto-freezing is prevented when an unowned draft exists.
expect(Object.isFrozen(d.a)).toBeFalsy()

// Ensure `obj` is not revoked.
obj.c = 1
})
}).then(res => {
expect(res).not.toBe(base)
expect(res).toEqual({
a: {b: {k: 1, c: 1}}
})
})
})
})

it("throws when the draft is modified and another object is returned", () => {
const base = {x: 3}
expect(() => {
Expand Down
136 changes: 136 additions & 0 deletions __tests__/manual.js
@@ -0,0 +1,136 @@
"use strict"
import {
setUseProxies,
createDraft,
finishDraft,
produce,
isDraft
} from "../src/index"

runTests("proxy", true)
runTests("es5", false)

function runTests(name, useProxies) {
describe("manual - " + name, () => {
setUseProxies(useProxies)

it("should check arguments", () => {
expect(() => createDraft(3)).toThrowErrorMatchingSnapshot()
const buf = new Buffer([])
expect(() => createDraft(buf)).toThrowErrorMatchingSnapshot()
expect(() => finishDraft({})).toThrowErrorMatchingSnapshot()
})

it("should support manual drafts", () => {
const state = [{}, {}, {}]

const draft = createDraft(state)
draft.forEach((item, index) => {
item.index = index
})

const result = finishDraft(draft)

expect(result).not.toBe(state)
expect(result).toEqual([{index: 0}, {index: 1}, {index: 2}])
expect(state).toEqual([{}, {}, {}])
})

it("cannot modify after finish", () => {
const state = {a: 1}

const draft = createDraft(state)
draft.a = 2
expect(finishDraft(draft)).toEqual({a: 2})
expect(() => {
draft.a = 3
}).toThrowErrorMatchingSnapshot()
})

it("should support patches drafts", () => {
const state = {a: 1}

const draft = createDraft(state)
draft.a = 2
draft.b = 3

const listener = jest.fn()
const result = finishDraft(draft, listener)

expect(result).not.toBe(state)
expect(result).toEqual({a: 2, b: 3})
expect(listener.mock.calls).toMatchSnapshot()
})

it("should handle multiple create draft calls", () => {
const state = {a: 1}

const draft = createDraft(state)
draft.a = 2

const draft2 = createDraft(state)
draft2.b = 3

const result = finishDraft(draft)

expect(result).not.toBe(state)
expect(result).toEqual({a: 2})

draft2.a = 4
const result2 = finishDraft(draft2)
expect(result2).not.toBe(result)
expect(result2).toEqual({a: 4, b: 3})
})

it("combines with produce - 1", () => {
const state = {a: 1}

const draft = createDraft(state)
draft.a = 2
const res1 = produce(draft, d => {
d.b = 3
})
draft.b = 4
const res2 = finishDraft(draft)
expect(res1).toEqual({a: 2, b: 3})
expect(res2).toEqual({a: 2, b: 4})
})

it("combines with produce - 2", () => {
const state = {a: 1}

const res1 = produce(state, draft => {
draft.b = 3
const draft2 = createDraft(draft)
draft.c = 4
draft2.d = 5
const res2 = finishDraft(draft2)
expect(res2).toEqual({
a: 1,
b: 3,
d: 5
})
draft.d = 2
})
expect(res1).toEqual({
a: 1,
b: 3,
c: 4,
d: 2
})
})

it("should not finish drafts from produce", () => {
produce({x: 1}, draft => {
expect(() => finishDraft(draft)).toThrowErrorMatchingSnapshot()
})
})

it("should not finish twice", () => {
const draft = createDraft({a: 1})
draft.a++
finishDraft(draft)
expect(() => finishDraft(draft)).toThrowErrorMatchingSnapshot()
})
})
}
47 changes: 42 additions & 5 deletions __tests__/produce.ts
Expand Up @@ -196,15 +196,52 @@ it("does not enforce immutability at the type level", () => {
exactType(result, {} as any[])
})

it("can produce nothing", () => {
let result = produce({}, _ => nothing)
it("can produce an undefined value", () => {
let base = {} as {readonly a: number}

// Return only nothing.
let result = produce(base, _ => nothing)
exactType(result, undefined)

// Return maybe nothing.
let result2 = produce(base, draft => {
if (draft.a > 0) return nothing
})
exactType(result2, {} as typeof base | undefined)
})

it("can return the draft itself", () => {
let base = {} as {readonly a: number}
let result = produce(base, draft => draft)

// Currently, the `readonly` modifier is lost.
exactType(result, {} as {a: number} | undefined)
})

it("can return a promise", () => {
let base = {} as {readonly a: number}

// Return a promise only.
exactType(
produce(base, draft => {
return Promise.resolve(draft.a > 0 ? null : undefined)
}),
{} as Promise<{readonly a: number} | null>
)

// Return a promise or undefined.
exactType(
produce(base, draft => {
if (draft.a > 0) return Promise.resolve()
}),
{} as (Promise<{readonly a: number}> | {readonly a: number})
)
})

it("works with `void` hack", () => {
let obj = {} as {readonly a: number}
let res = produce(obj, s => void s.a++)
exactType(res, obj)
let base = {} as {readonly a: number}
let copy = produce(base, s => void s.a++)
exactType(copy, base)
})

it("works with generic parameters", () => {
Expand Down