mirror of
https://github.com/oxynote/oxynote.git
synced 2026-10-17 19:09:19 +02:00
717 lines
20 KiB
TypeScript
717 lines
20 KiB
TypeScript
import { afterEach, beforeEach, describe, it } from "vitest"
|
|
import {
|
|
ANY_DATE,
|
|
ANY_STRING,
|
|
clearQueryCache,
|
|
disposeMockEndpoints,
|
|
makeXid,
|
|
mockDeferredEndpoint,
|
|
mockEndpoint,
|
|
readQueryData,
|
|
runInApp,
|
|
seedAuthOrganization,
|
|
seedQueryData,
|
|
} from "./test-helpers"
|
|
import useDocumentHookAPI from "./useDocumentHookAPI"
|
|
|
|
const DOC_ID = makeXid("doc1")
|
|
const BRANCH_ID = makeXid("bra1")
|
|
const HOOK_ID = makeXid("hoo1")
|
|
const OTHER_HOOK_ID = makeXid("hoo2")
|
|
const MISSING_HOOK_ID = makeXid("hoo3")
|
|
const ORG_ID = makeXid("org1")
|
|
const NON_XID = "nano-id"
|
|
const HOOKS_KEY = ["documents", "hooks", DOC_ID, BRANCH_ID] as const
|
|
const LIST_URL = `/api/documents/${DOC_ID}/hooks`
|
|
|
|
const CREATE_REQ: DocumentHookCreateRequest = {
|
|
type: DocumentHookType.GitHubTracking,
|
|
branchId: BRANCH_ID,
|
|
blockId: "block1",
|
|
settings: { repository: "acme/docs", branch: "main", paths: ["a.md"] },
|
|
}
|
|
|
|
const UPDATE_REQ: DocumentHookUpdateRequest = {
|
|
settings: { repository: "acme/docs", branch: "dev", paths: ["b.md"] },
|
|
}
|
|
|
|
function makeDocumentHookAPI() {
|
|
return runInApp(() => useDocumentHookAPI())
|
|
}
|
|
|
|
// cache seeds pass through the composable's JSON-based clone, so the
|
|
// fixture uses string dates — exactly what a cache round-trip produces.
|
|
// The state is non-default so a reset is observable.
|
|
function makeHook(id: string): DocumentHook {
|
|
return {
|
|
id,
|
|
crossBranchId: id,
|
|
type: DocumentHookType.GitHubTracking,
|
|
documentId: DOC_ID,
|
|
organizationId: ORG_ID,
|
|
branchId: BRANCH_ID,
|
|
blockId: "block1",
|
|
settings: { repository: "acme/docs", branch: "main", paths: ["a.md"] },
|
|
state: { pathsChecksums: { "a.md": "sum-a" } },
|
|
status: "missing_branch",
|
|
score: "100",
|
|
createdAt: "2024-01-01T00:00:00.000Z",
|
|
updatedAt: "2024-01-01T00:00:00.000Z",
|
|
}
|
|
}
|
|
|
|
function seedOrganization() {
|
|
seedAuthOrganization(ORG_ID)
|
|
}
|
|
|
|
function seedHooks(hooks: DocumentHook[]) {
|
|
seedQueryData(HOOKS_KEY, hooks)
|
|
}
|
|
|
|
function getHooks() {
|
|
return readQueryData(HOOKS_KEY)
|
|
}
|
|
|
|
// creating the composable eagerly loads its queries once; refresh() joins
|
|
// that in-flight load (or reuses its fresh result) instead of forcing a
|
|
// second request, which keeps the call accounting deterministic
|
|
describe("useDocumentHookAPI", { concurrent: false }, () => {
|
|
// the tests share the app-wide query cache and the test-time endpoint
|
|
// registry, so they cannot interleave
|
|
beforeEach(clearQueryCache)
|
|
|
|
afterEach(disposeMockEndpoints)
|
|
|
|
describe("useFetchDocumentHooksByDocID", () => {
|
|
it.for([
|
|
{
|
|
name: "resolves no hooks without a document id",
|
|
docId: null,
|
|
branchId: BRANCH_ID,
|
|
},
|
|
{
|
|
name: "resolves no hooks without a branch id",
|
|
docId: DOC_ID,
|
|
branchId: null,
|
|
},
|
|
])("$name", async ({ docId, branchId }, { expect }) => {
|
|
const listCalls = mockEndpoint("GET", LIST_URL, () => [])
|
|
const api = makeDocumentHookAPI()
|
|
const hooks = runInApp(() =>
|
|
api.useFetchDocumentHooksByDocID(docId, branchId),
|
|
)
|
|
|
|
const result = await hooks.refresh()
|
|
|
|
expect(result.data).toEqual([])
|
|
expect(listCalls).toHaveLength(0)
|
|
})
|
|
|
|
it("fetches the hooks of the branch", async ({ expect }) => {
|
|
const listCalls = mockEndpoint("GET", LIST_URL, () => [makeHook(HOOK_ID)])
|
|
const api = makeDocumentHookAPI()
|
|
const hooks = runInApp(() =>
|
|
api.useFetchDocumentHooksByDocID(DOC_ID, BRANCH_ID),
|
|
)
|
|
|
|
const result = await hooks.refresh()
|
|
|
|
expect(result.data).toEqual([makeHook(HOOK_ID)])
|
|
expect(listCalls).toHaveLength(1)
|
|
expect(listCalls[0]?.query).toEqual({ branchId: BRANCH_ID })
|
|
})
|
|
})
|
|
|
|
describe("createDocumentHookByDocID", () => {
|
|
it.for([
|
|
{
|
|
name: "skips the create for a non-xid document id",
|
|
docId: NON_XID,
|
|
branchId: BRANCH_ID,
|
|
},
|
|
{
|
|
name: "skips the create for a non-xid branch id",
|
|
docId: DOC_ID,
|
|
branchId: NON_XID,
|
|
},
|
|
])("$name", async ({ docId, branchId }, { expect }) => {
|
|
seedOrganization()
|
|
seedHooks([makeHook(HOOK_ID)])
|
|
const listCalls = mockEndpoint("GET", LIST_URL, () => [])
|
|
const createCalls = mockEndpoint("POST", LIST_URL, () => ({}))
|
|
const api = makeDocumentHookAPI()
|
|
|
|
await api.createDocumentHookByDocID.mutateAsync({
|
|
docId,
|
|
req: { ...CREATE_REQ, branchId },
|
|
})
|
|
|
|
expect(getHooks()).toEqual([makeHook(HOOK_ID)])
|
|
expect(createCalls).toHaveLength(0)
|
|
expect(listCalls).toHaveLength(0)
|
|
})
|
|
|
|
it("sends the create without an optimistic insert when no organization is loaded and refetches the list", async ({
|
|
expect,
|
|
}) => {
|
|
const serverHooks = [makeHook(OTHER_HOOK_ID), makeHook(HOOK_ID)]
|
|
seedHooks([makeHook(HOOK_ID)])
|
|
const listCalls = mockEndpoint("GET", LIST_URL, () => serverHooks)
|
|
let hooksAtRequest: unknown
|
|
const createCalls = mockEndpoint("POST", LIST_URL, () => {
|
|
hooksAtRequest = getHooks()
|
|
|
|
return makeHook(OTHER_HOOK_ID)
|
|
})
|
|
const api = makeDocumentHookAPI()
|
|
runInApp(() => api.useFetchDocumentHooksByDocID(DOC_ID, BRANCH_ID))
|
|
|
|
await api.createDocumentHookByDocID.mutateAsync({
|
|
docId: DOC_ID,
|
|
req: CREATE_REQ,
|
|
})
|
|
|
|
expect(hooksAtRequest).toEqual([makeHook(HOOK_ID)])
|
|
expect(createCalls).toHaveLength(1)
|
|
expect(createCalls[0]?.body).toEqual(CREATE_REQ)
|
|
// the request went out, so the active hooks query is refetched
|
|
// even though the optimistic insert was skipped
|
|
expect(listCalls).toHaveLength(1)
|
|
expect(getHooks()).toEqual(serverHooks)
|
|
})
|
|
|
|
it("optimistically prepends the new hook and refetches the list on success", async ({
|
|
expect,
|
|
}) => {
|
|
const createdHook = {
|
|
...makeHook(OTHER_HOOK_ID),
|
|
settings: CREATE_REQ.settings,
|
|
}
|
|
const serverHooks = [createdHook, makeHook(HOOK_ID)]
|
|
|
|
seedOrganization()
|
|
seedHooks([makeHook(HOOK_ID)])
|
|
const listCalls = mockEndpoint("GET", LIST_URL, () => serverHooks)
|
|
const create = mockDeferredEndpoint("POST", LIST_URL)
|
|
const api = makeDocumentHookAPI()
|
|
const hooks = runInApp(() =>
|
|
api.useFetchDocumentHooksByDocID(DOC_ID, BRANCH_ID),
|
|
)
|
|
|
|
const pending = api.createDocumentHookByDocID.mutateAsync({
|
|
docId: DOC_ID,
|
|
req: CREATE_REQ,
|
|
})
|
|
await create.reached
|
|
|
|
// the optimistic insert stamps a nanoid id and fresh dates. A new
|
|
// hook is its own across branches
|
|
const [inserted] = getHooks() as DocumentHook[]
|
|
expect(inserted?.crossBranchId).toBe(inserted?.id)
|
|
expect(getHooks()).toEqual([
|
|
{
|
|
id: ANY_STRING,
|
|
crossBranchId: ANY_STRING,
|
|
type: DocumentHookType.GitHubTracking,
|
|
documentId: DOC_ID,
|
|
branchId: BRANCH_ID,
|
|
organizationId: ORG_ID,
|
|
blockId: "block1",
|
|
settings: CREATE_REQ.settings,
|
|
state: { pathsChecksums: {} },
|
|
status: "active",
|
|
score: "100",
|
|
createdAt: ANY_DATE,
|
|
updatedAt: ANY_DATE,
|
|
},
|
|
makeHook(HOOK_ID),
|
|
])
|
|
create.resolve(createdHook)
|
|
|
|
await pending
|
|
expect(create.calls).toHaveLength(1)
|
|
expect(create.calls[0]?.body).toEqual(CREATE_REQ)
|
|
// the success invalidation refetches the active hooks query
|
|
expect(listCalls).toHaveLength(1)
|
|
expect(hooks.data.value).toEqual(serverHooks)
|
|
})
|
|
|
|
it("rolls back the optimistic insert when the request fails", async ({
|
|
expect,
|
|
}) => {
|
|
seedOrganization()
|
|
seedHooks([makeHook(HOOK_ID)])
|
|
const listCalls = mockEndpoint("GET", LIST_URL, () => [])
|
|
const createCalls = mockEndpoint("POST", LIST_URL, () => {
|
|
throw createError({ statusCode: 500 })
|
|
})
|
|
const api = makeDocumentHookAPI()
|
|
|
|
await expect(
|
|
api.createDocumentHookByDocID.mutateAsync({
|
|
docId: DOC_ID,
|
|
req: CREATE_REQ,
|
|
}),
|
|
).rejects.toThrow()
|
|
|
|
expect(getHooks()).toEqual([makeHook(HOOK_ID)])
|
|
expect(createCalls).toHaveLength(1)
|
|
expect(listCalls).toHaveLength(0)
|
|
})
|
|
|
|
it("skips the rollback when the cache changed after the optimistic insert", async ({
|
|
expect,
|
|
}) => {
|
|
seedOrganization()
|
|
seedHooks([makeHook(HOOK_ID)])
|
|
const listCalls = mockEndpoint("GET", LIST_URL, () => [])
|
|
const create = mockDeferredEndpoint("POST", LIST_URL)
|
|
const api = makeDocumentHookAPI()
|
|
|
|
const pending = api.createDocumentHookByDocID.mutateAsync({
|
|
docId: DOC_ID,
|
|
req: CREATE_REQ,
|
|
})
|
|
await create.reached
|
|
|
|
// divergent data written after the optimistic insert must survive
|
|
// the failure
|
|
seedHooks([makeHook(OTHER_HOOK_ID)])
|
|
create.reject(createError({ statusCode: 500 }))
|
|
|
|
await expect(pending).rejects.toThrow()
|
|
expect(getHooks()).toEqual([makeHook(OTHER_HOOK_ID)])
|
|
expect(create.calls).toHaveLength(1)
|
|
expect(listCalls).toHaveLength(0)
|
|
})
|
|
})
|
|
|
|
describe("updateDocumentHookByDocID", () => {
|
|
it.for([
|
|
{
|
|
name: "skips the update for a non-xid document id",
|
|
docId: NON_XID,
|
|
branchId: BRANCH_ID,
|
|
hookId: HOOK_ID,
|
|
},
|
|
{
|
|
name: "skips the update for a non-xid branch id",
|
|
docId: DOC_ID,
|
|
branchId: NON_XID,
|
|
hookId: HOOK_ID,
|
|
},
|
|
{
|
|
name: "skips the update for a non-xid hook id",
|
|
docId: DOC_ID,
|
|
branchId: BRANCH_ID,
|
|
hookId: NON_XID,
|
|
},
|
|
])("$name", async ({ docId, branchId, hookId }, { expect }) => {
|
|
seedHooks([makeHook(HOOK_ID)])
|
|
const listCalls = mockEndpoint("GET", LIST_URL, () => [])
|
|
const updateCalls = mockEndpoint(
|
|
"PUT",
|
|
`${LIST_URL}/${HOOK_ID}`,
|
|
() => ({}),
|
|
)
|
|
const api = makeDocumentHookAPI()
|
|
|
|
await api.updateDocumentHookByDocID.mutateAsync({
|
|
docId,
|
|
branchId,
|
|
hookId,
|
|
req: UPDATE_REQ,
|
|
})
|
|
|
|
expect(getHooks()).toEqual([makeHook(HOOK_ID)])
|
|
expect(updateCalls).toHaveLength(0)
|
|
expect(listCalls).toHaveLength(0)
|
|
})
|
|
|
|
it("optimistically replaces the hook settings and refetches the list on success", async ({
|
|
expect,
|
|
}) => {
|
|
const updatedHook = {
|
|
...makeHook(HOOK_ID),
|
|
settings: UPDATE_REQ.settings,
|
|
updatedAt: "2024-02-01T00:00:00.000Z",
|
|
}
|
|
const serverHooks = [updatedHook, makeHook(OTHER_HOOK_ID)]
|
|
|
|
seedHooks([makeHook(HOOK_ID), makeHook(OTHER_HOOK_ID)])
|
|
const listCalls = mockEndpoint("GET", LIST_URL, () => serverHooks)
|
|
const update = mockDeferredEndpoint("PUT", `${LIST_URL}/${HOOK_ID}`)
|
|
const api = makeDocumentHookAPI()
|
|
const hooks = runInApp(() =>
|
|
api.useFetchDocumentHooksByDocID(DOC_ID, BRANCH_ID),
|
|
)
|
|
|
|
const pending = api.updateDocumentHookByDocID.mutateAsync({
|
|
docId: DOC_ID,
|
|
branchId: BRANCH_ID,
|
|
hookId: HOOK_ID,
|
|
req: UPDATE_REQ,
|
|
})
|
|
await update.reached
|
|
|
|
// only the matching hook gets the new settings and a bumped
|
|
// updatedAt; the other hook stays untouched
|
|
expect(getHooks()).toEqual([
|
|
{
|
|
...makeHook(HOOK_ID),
|
|
settings: UPDATE_REQ.settings,
|
|
updatedAt: ANY_DATE,
|
|
},
|
|
makeHook(OTHER_HOOK_ID),
|
|
])
|
|
update.resolve(updatedHook)
|
|
|
|
await pending
|
|
expect(update.calls).toHaveLength(1)
|
|
expect(update.calls[0]?.body).toEqual(UPDATE_REQ)
|
|
// the success invalidation refetches the active hooks query
|
|
expect(listCalls).toHaveLength(1)
|
|
expect(hooks.data.value).toEqual(serverHooks)
|
|
})
|
|
|
|
it("rolls back the optimistic update when the request fails", async ({
|
|
expect,
|
|
}) => {
|
|
seedHooks([makeHook(HOOK_ID)])
|
|
const listCalls = mockEndpoint("GET", LIST_URL, () => [])
|
|
const updateCalls = mockEndpoint("PUT", `${LIST_URL}/${HOOK_ID}`, () => {
|
|
throw createError({ statusCode: 500 })
|
|
})
|
|
const api = makeDocumentHookAPI()
|
|
|
|
await expect(
|
|
api.updateDocumentHookByDocID.mutateAsync({
|
|
docId: DOC_ID,
|
|
branchId: BRANCH_ID,
|
|
hookId: HOOK_ID,
|
|
req: UPDATE_REQ,
|
|
}),
|
|
).rejects.toThrow()
|
|
|
|
expect(getHooks()).toEqual([makeHook(HOOK_ID)])
|
|
expect(updateCalls).toHaveLength(1)
|
|
expect(listCalls).toHaveLength(0)
|
|
})
|
|
|
|
it("skips the rollback when the cache changed after the optimistic update", async ({
|
|
expect,
|
|
}) => {
|
|
seedHooks([makeHook(HOOK_ID)])
|
|
const listCalls = mockEndpoint("GET", LIST_URL, () => [])
|
|
const update = mockDeferredEndpoint("PUT", `${LIST_URL}/${HOOK_ID}`)
|
|
const api = makeDocumentHookAPI()
|
|
|
|
const pending = api.updateDocumentHookByDocID.mutateAsync({
|
|
docId: DOC_ID,
|
|
branchId: BRANCH_ID,
|
|
hookId: HOOK_ID,
|
|
req: UPDATE_REQ,
|
|
})
|
|
await update.reached
|
|
|
|
// divergent data written after the optimistic update must survive
|
|
// the failure
|
|
seedHooks([makeHook(OTHER_HOOK_ID)])
|
|
update.reject(createError({ statusCode: 500 }))
|
|
|
|
await expect(pending).rejects.toThrow()
|
|
expect(getHooks()).toEqual([makeHook(OTHER_HOOK_ID)])
|
|
expect(update.calls).toHaveLength(1)
|
|
expect(listCalls).toHaveLength(0)
|
|
})
|
|
})
|
|
|
|
describe("deleteDocumentHookByDocID", () => {
|
|
it.for([
|
|
{
|
|
name: "skips the delete for a non-xid document id",
|
|
docId: NON_XID,
|
|
branchId: BRANCH_ID,
|
|
hookId: HOOK_ID,
|
|
},
|
|
{
|
|
name: "skips the delete for a non-xid branch id",
|
|
docId: DOC_ID,
|
|
branchId: NON_XID,
|
|
hookId: HOOK_ID,
|
|
},
|
|
{
|
|
name: "skips the delete for a non-xid hook id",
|
|
docId: DOC_ID,
|
|
branchId: BRANCH_ID,
|
|
hookId: NON_XID,
|
|
},
|
|
])("$name", async ({ docId, branchId, hookId }, { expect }) => {
|
|
seedHooks([makeHook(HOOK_ID)])
|
|
const listCalls = mockEndpoint("GET", LIST_URL, () => [])
|
|
const deleteCalls = mockEndpoint(
|
|
"DELETE",
|
|
`${LIST_URL}/${HOOK_ID}`,
|
|
() => ({}),
|
|
)
|
|
const api = makeDocumentHookAPI()
|
|
|
|
await api.deleteDocumentHookByDocID.mutateAsync({
|
|
docId,
|
|
branchId,
|
|
hookId,
|
|
})
|
|
|
|
expect(getHooks()).toEqual([makeHook(HOOK_ID)])
|
|
expect(deleteCalls).toHaveLength(0)
|
|
expect(listCalls).toHaveLength(0)
|
|
})
|
|
|
|
it("optimistically removes the hook and refetches the list on success", async ({
|
|
expect,
|
|
}) => {
|
|
const serverHooks = [makeHook(OTHER_HOOK_ID)]
|
|
|
|
seedHooks([makeHook(HOOK_ID), makeHook(OTHER_HOOK_ID)])
|
|
const listCalls = mockEndpoint("GET", LIST_URL, () => serverHooks)
|
|
const remove = mockDeferredEndpoint("DELETE", `${LIST_URL}/${HOOK_ID}`)
|
|
const api = makeDocumentHookAPI()
|
|
const hooks = runInApp(() =>
|
|
api.useFetchDocumentHooksByDocID(DOC_ID, BRANCH_ID),
|
|
)
|
|
|
|
const pending = api.deleteDocumentHookByDocID.mutateAsync({
|
|
docId: DOC_ID,
|
|
branchId: BRANCH_ID,
|
|
hookId: HOOK_ID,
|
|
})
|
|
await remove.reached
|
|
|
|
expect(getHooks()).toEqual([makeHook(OTHER_HOOK_ID)])
|
|
remove.resolve({})
|
|
|
|
await pending
|
|
expect(remove.calls).toHaveLength(1)
|
|
// the success invalidation refetches the active hooks query
|
|
expect(listCalls).toHaveLength(1)
|
|
expect(hooks.data.value).toEqual(serverHooks)
|
|
})
|
|
|
|
it("leaves the list unchanged when the hook id is not in the cache", async ({
|
|
expect,
|
|
}) => {
|
|
seedHooks([makeHook(HOOK_ID)])
|
|
const listCalls = mockEndpoint("GET", LIST_URL, () => [])
|
|
const deleteCalls = mockEndpoint(
|
|
"DELETE",
|
|
`${LIST_URL}/${MISSING_HOOK_ID}`,
|
|
() => ({}),
|
|
)
|
|
const api = makeDocumentHookAPI()
|
|
|
|
await api.deleteDocumentHookByDocID.mutateAsync({
|
|
docId: DOC_ID,
|
|
branchId: BRANCH_ID,
|
|
hookId: MISSING_HOOK_ID,
|
|
})
|
|
|
|
expect(getHooks()).toEqual([makeHook(HOOK_ID)])
|
|
expect(deleteCalls).toHaveLength(1)
|
|
expect(listCalls).toHaveLength(0)
|
|
})
|
|
|
|
it("rolls back the optimistic removal when the request fails", async ({
|
|
expect,
|
|
}) => {
|
|
seedHooks([makeHook(HOOK_ID)])
|
|
const listCalls = mockEndpoint("GET", LIST_URL, () => [])
|
|
const deleteCalls = mockEndpoint(
|
|
"DELETE",
|
|
`${LIST_URL}/${HOOK_ID}`,
|
|
() => {
|
|
throw createError({ statusCode: 500 })
|
|
},
|
|
)
|
|
const api = makeDocumentHookAPI()
|
|
|
|
await expect(
|
|
api.deleteDocumentHookByDocID.mutateAsync({
|
|
docId: DOC_ID,
|
|
branchId: BRANCH_ID,
|
|
hookId: HOOK_ID,
|
|
}),
|
|
).rejects.toThrow()
|
|
|
|
expect(getHooks()).toEqual([makeHook(HOOK_ID)])
|
|
expect(deleteCalls).toHaveLength(1)
|
|
expect(listCalls).toHaveLength(0)
|
|
})
|
|
|
|
it("skips the rollback when the cache changed after the optimistic removal", async ({
|
|
expect,
|
|
}) => {
|
|
seedHooks([makeHook(HOOK_ID)])
|
|
const listCalls = mockEndpoint("GET", LIST_URL, () => [])
|
|
const remove = mockDeferredEndpoint("DELETE", `${LIST_URL}/${HOOK_ID}`)
|
|
const api = makeDocumentHookAPI()
|
|
|
|
const pending = api.deleteDocumentHookByDocID.mutateAsync({
|
|
docId: DOC_ID,
|
|
branchId: BRANCH_ID,
|
|
hookId: HOOK_ID,
|
|
})
|
|
await remove.reached
|
|
|
|
// divergent data written after the optimistic removal must survive
|
|
// the failure
|
|
seedHooks([makeHook(OTHER_HOOK_ID)])
|
|
remove.reject(createError({ statusCode: 500 }))
|
|
|
|
await expect(pending).rejects.toThrow()
|
|
expect(getHooks()).toEqual([makeHook(OTHER_HOOK_ID)])
|
|
expect(remove.calls).toHaveLength(1)
|
|
expect(listCalls).toHaveLength(0)
|
|
})
|
|
})
|
|
|
|
describe("resetDocumentHookByDocID", () => {
|
|
it.for([
|
|
{
|
|
name: "skips the reset for a non-xid document id",
|
|
docId: NON_XID,
|
|
branchId: BRANCH_ID,
|
|
hookId: HOOK_ID,
|
|
},
|
|
{
|
|
name: "skips the reset for a non-xid branch id",
|
|
docId: DOC_ID,
|
|
branchId: NON_XID,
|
|
hookId: HOOK_ID,
|
|
},
|
|
{
|
|
name: "skips the reset for a non-xid hook id",
|
|
docId: DOC_ID,
|
|
branchId: BRANCH_ID,
|
|
hookId: NON_XID,
|
|
},
|
|
])("$name", async ({ docId, branchId, hookId }, { expect }) => {
|
|
seedHooks([makeHook(HOOK_ID)])
|
|
const listCalls = mockEndpoint("GET", LIST_URL, () => [])
|
|
const resetCalls = mockEndpoint(
|
|
"PUT",
|
|
`${LIST_URL}/${HOOK_ID}/reset`,
|
|
() => ({}),
|
|
)
|
|
const api = makeDocumentHookAPI()
|
|
|
|
await api.resetDocumentHookByDocID.mutateAsync({
|
|
docId,
|
|
branchId,
|
|
hookId,
|
|
})
|
|
|
|
expect(getHooks()).toEqual([makeHook(HOOK_ID)])
|
|
expect(resetCalls).toHaveLength(0)
|
|
expect(listCalls).toHaveLength(0)
|
|
})
|
|
|
|
it("optimistically resets the hook state and refetches the list on success", async ({
|
|
expect,
|
|
}) => {
|
|
const resetHook = {
|
|
...makeHook(HOOK_ID),
|
|
state: { pathsChecksums: {} },
|
|
status: "active",
|
|
}
|
|
const serverHooks = [resetHook, makeHook(OTHER_HOOK_ID)]
|
|
|
|
seedHooks([makeHook(HOOK_ID), makeHook(OTHER_HOOK_ID)])
|
|
const listCalls = mockEndpoint("GET", LIST_URL, () => serverHooks)
|
|
const reset = mockDeferredEndpoint("PUT", `${LIST_URL}/${HOOK_ID}/reset`)
|
|
const api = makeDocumentHookAPI()
|
|
const hooks = runInApp(() =>
|
|
api.useFetchDocumentHooksByDocID(DOC_ID, BRANCH_ID),
|
|
)
|
|
|
|
const pending = api.resetDocumentHookByDocID.mutateAsync({
|
|
docId: DOC_ID,
|
|
branchId: BRANCH_ID,
|
|
hookId: HOOK_ID,
|
|
})
|
|
await reset.reached
|
|
|
|
// only the matching hook gets the default state for its type and a
|
|
// bumped updatedAt; the other hook stays untouched
|
|
expect(getHooks()).toEqual([
|
|
{
|
|
...makeHook(HOOK_ID),
|
|
state: { pathsChecksums: {} },
|
|
status: "active",
|
|
updatedAt: ANY_DATE,
|
|
},
|
|
makeHook(OTHER_HOOK_ID),
|
|
])
|
|
reset.resolve(resetHook)
|
|
|
|
await pending
|
|
expect(reset.calls).toHaveLength(1)
|
|
// the success invalidation refetches the active hooks query
|
|
expect(listCalls).toHaveLength(1)
|
|
expect(hooks.data.value).toEqual(serverHooks)
|
|
})
|
|
|
|
it("rolls back the optimistic reset when the request fails", async ({
|
|
expect,
|
|
}) => {
|
|
seedHooks([makeHook(HOOK_ID)])
|
|
const listCalls = mockEndpoint("GET", LIST_URL, () => [])
|
|
const resetCalls = mockEndpoint(
|
|
"PUT",
|
|
`${LIST_URL}/${HOOK_ID}/reset`,
|
|
() => {
|
|
throw createError({ statusCode: 500 })
|
|
},
|
|
)
|
|
const api = makeDocumentHookAPI()
|
|
|
|
await expect(
|
|
api.resetDocumentHookByDocID.mutateAsync({
|
|
docId: DOC_ID,
|
|
branchId: BRANCH_ID,
|
|
hookId: HOOK_ID,
|
|
}),
|
|
).rejects.toThrow()
|
|
|
|
expect(getHooks()).toEqual([makeHook(HOOK_ID)])
|
|
expect(resetCalls).toHaveLength(1)
|
|
expect(listCalls).toHaveLength(0)
|
|
})
|
|
|
|
it("skips the rollback when the cache changed after the optimistic reset", async ({
|
|
expect,
|
|
}) => {
|
|
seedHooks([makeHook(HOOK_ID)])
|
|
const listCalls = mockEndpoint("GET", LIST_URL, () => [])
|
|
const reset = mockDeferredEndpoint("PUT", `${LIST_URL}/${HOOK_ID}/reset`)
|
|
const api = makeDocumentHookAPI()
|
|
|
|
const pending = api.resetDocumentHookByDocID.mutateAsync({
|
|
docId: DOC_ID,
|
|
branchId: BRANCH_ID,
|
|
hookId: HOOK_ID,
|
|
})
|
|
await reset.reached
|
|
|
|
// divergent data written after the optimistic reset must survive
|
|
// the failure
|
|
seedHooks([makeHook(OTHER_HOOK_ID)])
|
|
reset.reject(createError({ statusCode: 500 }))
|
|
|
|
await expect(pending).rejects.toThrow()
|
|
expect(getHooks()).toEqual([makeHook(OTHER_HOOK_ID)])
|
|
expect(reset.calls).toHaveLength(1)
|
|
expect(listCalls).toHaveLength(0)
|
|
})
|
|
})
|
|
})
|