mirror of
https://github.com/oxynote/oxynote.git
synced 2026-10-18 03:19:19 +02:00
* deps(web): bump the safe group across 1 directory with 29 updates Bumps the safe group with 29 updates in the /web directory: | Package | From | To | | --- | --- | --- | | [@internationalized/date](https://github.com/adobe/react-spectrum) | `3.12.3` | `3.12.4` | | [@pinia/colada](https://github.com/posva/pinia-colada) | `1.4.2` | `1.4.4` | | [@pinia/colada-nuxt](https://github.com/posva/pinia-colada) | `1.0.2` | `1.1.0` | | [@sentry/nuxt](https://github.com/getsentry/sentry-javascript) | `10.72.0` | `10.73.0` | | [@tiptap/extension-code-block-lowlight](https://github.com/ueberdosis/tiptap/tree/HEAD/packages/extension-code-block-lowlight) | `3.30.5` | `3.31.3` | | [@tiptap/extension-collaboration](https://github.com/ueberdosis/tiptap/tree/HEAD/packages/extension-collaboration) | `3.30.5` | `3.31.3` | | [@tiptap/extension-collaboration-caret](https://github.com/ueberdosis/tiptap/tree/HEAD/packages/extension-collaboration-caret) | `3.30.5` | `3.31.3` | | [@tiptap/extension-file-handler](https://github.com/ueberdosis/tiptap/tree/HEAD/packages/extension-file-handler) | `3.30.5` | `3.31.3` | | [@tiptap/extension-list](https://github.com/ueberdosis/tiptap/tree/HEAD/packages/extension-list) | `3.30.5` | `3.31.3` | | [@tiptap/extension-node-range](https://github.com/ueberdosis/tiptap/tree/HEAD/packages/extension-node-range) | `3.30.5` | `3.31.3` | | [@tiptap/extension-text-style](https://github.com/ueberdosis/tiptap/tree/HEAD/packages/extension-text-style) | `3.30.5` | `3.31.3` | | [@tiptap/extension-typography](https://github.com/ueberdosis/tiptap/tree/HEAD/packages/extension-typography) | `3.30.5` | `3.31.3` | | [@tiptap/extensions](https://github.com/ueberdosis/tiptap/tree/HEAD/packages/extension) | `3.30.5` | `3.31.3` | | [@tiptap/pm](https://github.com/ueberdosis/tiptap/tree/HEAD/packages/pm) | `3.30.5` | `3.31.3` | | [@tiptap/starter-kit](https://github.com/ueberdosis/tiptap/tree/HEAD/packages/starter-kit) | `3.30.5` | `3.31.3` | | [@tiptap/suggestion](https://github.com/ueberdosis/tiptap/tree/HEAD/packages/suggestion) | `3.30.5` | `3.31.3` | | [@tiptap/vue-3](https://github.com/ueberdosis/tiptap/tree/HEAD/packages/vue-3) | `3.30.5` | `3.31.3` | | [vue-codemirror6](https://github.com/logue/vue-codemirror6) | `1.6.1` | `1.6.2` | | [vue-echarts](https://github.com/ecomfe/vue-echarts) | `8.1.0` | `8.3.0` | | [@iconify-json/hugeicons](https://github.com/iconify/icon-sets) | `1.2.33` | `1.2.34` | | [@iconify-json/lucide](https://github.com/iconify/icon-sets) | `1.2.126` | `1.2.129` | | [@iconify-json/simple-icons](https://github.com/iconify/icon-sets) | `1.2.93` | `1.2.94` | | [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) | `8.68.0` | `8.69.0` | | [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) | `8.68.0` | `8.69.0` | | [electron](https://github.com/electron/electron) | `44.0.0` | `44.2.0` | | [eslint](https://github.com/eslint/eslint) | `10.9.1` | `10.10.0` | | [happy-dom](https://github.com/capricorn86/happy-dom) | `20.11.13` | `20.14.0` | | [knip](https://github.com/webpro-nl/knip/tree/HEAD/packages/knip) | `6.33.0` | `6.34.0` | | [playwright](https://github.com/microsoft/playwright) | `1.62.1` | `1.63.0` | Updates `@internationalized/date` from 3.12.3 to 3.12.4 - [Release notes](https://github.com/adobe/react-spectrum/releases) - [Commits](https://github.com/adobe/react-spectrum/compare/@internationalized/date@3.12.3...@internationalized/date@3.12.4) Updates `@pinia/colada` from 1.4.2 to 1.4.4 - [Release notes](https://github.com/posva/pinia-colada/releases) - [Changelog](https://github.com/posva/pinia-colada/blob/main/CHANGELOG.md) - [Commits](https://github.com/posva/pinia-colada/compare/v1.4.2...v1.4.4) Updates `@pinia/colada-nuxt` from 1.0.2 to 1.1.0 - [Release notes](https://github.com/posva/pinia-colada/releases) - [Changelog](https://github.com/posva/pinia-colada/blob/main/CHANGELOG.md) - [Commits](https://github.com/posva/pinia-colada/compare/@pinia/colada-nuxt@1.0.2...@pinia/colada-nuxt@1.1.0) Updates `@sentry/nuxt` from 10.72.0 to 10.73.0 - [Release notes](https://github.com/getsentry/sentry-javascript/releases) - [Changelog](https://github.com/getsentry/sentry-javascript/blob/10.73.0/CHANGELOG.md) - [Commits](https://github.com/getsentry/sentry-javascript/compare/10.72.0...10.73.0) Updates `@tiptap/extension-code-block-lowlight` from 3.30.5 to 3.31.3 - [Release notes](https://github.com/ueberdosis/tiptap/releases) - [Changelog](https://github.com/ueberdosis/tiptap/blob/v3.31.3/packages/extension-code-block-lowlight/CHANGELOG.md) - [Commits](https://github.com/ueberdosis/tiptap/commits/v3.31.3/packages/extension-code-block-lowlight) Updates `@tiptap/extension-collaboration` from 3.30.5 to 3.31.3 - [Release notes](https://github.com/ueberdosis/tiptap/releases) - [Changelog](https://github.com/ueberdosis/tiptap/blob/v3.31.3/packages/extension-collaboration/CHANGELOG.md) - [Commits](https://github.com/ueberdosis/tiptap/commits/v3.31.3/packages/extension-collaboration) Updates `@tiptap/extension-collaboration-caret` from 3.30.5 to 3.31.3 - [Release notes](https://github.com/ueberdosis/tiptap/releases) - [Changelog](https://github.com/ueberdosis/tiptap/blob/v3.31.3/packages/extension-collaboration-caret/CHANGELOG.md) - [Commits](https://github.com/ueberdosis/tiptap/commits/v3.31.3/packages/extension-collaboration-caret) Updates `@tiptap/extension-file-handler` from 3.30.5 to 3.31.3 - [Release notes](https://github.com/ueberdosis/tiptap/releases) - [Changelog](https://github.com/ueberdosis/tiptap/blob/v3.31.3/packages/extension-file-handler/CHANGELOG.md) - [Commits](https://github.com/ueberdosis/tiptap/commits/v3.31.3/packages/extension-file-handler) Updates `@tiptap/extension-list` from 3.30.5 to 3.31.3 - [Release notes](https://github.com/ueberdosis/tiptap/releases) - [Changelog](https://github.com/ueberdosis/tiptap/blob/v3.31.3/packages/extension-list/CHANGELOG.md) - [Commits](https://github.com/ueberdosis/tiptap/commits/v3.31.3/packages/extension-list) Updates `@tiptap/extension-node-range` from 3.30.5 to 3.31.3 - [Release notes](https://github.com/ueberdosis/tiptap/releases) - [Changelog](https://github.com/ueberdosis/tiptap/blob/v3.31.3/packages/extension-node-range/CHANGELOG.md) - [Commits](https://github.com/ueberdosis/tiptap/commits/v3.31.3/packages/extension-node-range) Updates `@tiptap/extension-text-style` from 3.30.5 to 3.31.3 - [Release notes](https://github.com/ueberdosis/tiptap/releases) - [Changelog](https://github.com/ueberdosis/tiptap/blob/v3.31.3/packages/extension-text-style/CHANGELOG.md) - [Commits](https://github.com/ueberdosis/tiptap/commits/v3.31.3/packages/extension-text-style) Updates `@tiptap/extension-typography` from 3.30.5 to 3.31.3 - [Release notes](https://github.com/ueberdosis/tiptap/releases) - [Changelog](https://github.com/ueberdosis/tiptap/blob/v3.31.3/packages/extension-typography/CHANGELOG.md) - [Commits](https://github.com/ueberdosis/tiptap/commits/v3.31.3/packages/extension-typography) Updates `@tiptap/extensions` from 3.30.5 to 3.31.3 - [Release notes](https://github.com/ueberdosis/tiptap/releases) - [Commits](https://github.com/ueberdosis/tiptap/commits/v3.31.3/packages/extension) Updates `@tiptap/pm` from 3.30.5 to 3.31.3 - [Release notes](https://github.com/ueberdosis/tiptap/releases) - [Changelog](https://github.com/ueberdosis/tiptap/blob/v3.31.3/packages/pm/CHANGELOG.md) - [Commits](https://github.com/ueberdosis/tiptap/commits/v3.31.3/packages/pm) Updates `@tiptap/starter-kit` from 3.30.5 to 3.31.3 - [Release notes](https://github.com/ueberdosis/tiptap/releases) - [Changelog](https://github.com/ueberdosis/tiptap/blob/v3.31.3/packages/starter-kit/CHANGELOG.md) - [Commits](https://github.com/ueberdosis/tiptap/commits/v3.31.3/packages/starter-kit) Updates `@tiptap/suggestion` from 3.30.5 to 3.31.3 - [Release notes](https://github.com/ueberdosis/tiptap/releases) - [Changelog](https://github.com/ueberdosis/tiptap/blob/v3.31.3/packages/suggestion/CHANGELOG.md) - [Commits](https://github.com/ueberdosis/tiptap/commits/v3.31.3/packages/suggestion) Updates `@tiptap/vue-3` from 3.30.5 to 3.31.3 - [Release notes](https://github.com/ueberdosis/tiptap/releases) - [Changelog](https://github.com/ueberdosis/tiptap/blob/v3.31.3/packages/vue-3/CHANGELOG.md) - [Commits](https://github.com/ueberdosis/tiptap/commits/v3.31.3/packages/vue-3) Updates `vue-codemirror6` from 1.6.1 to 1.6.2 - [Release notes](https://github.com/logue/vue-codemirror6/releases) - [Changelog](https://github.com/logue/vue-codemirror6/blob/master/CHANGELOG.md) - [Commits](https://github.com/logue/vue-codemirror6/commits) Updates `vue-echarts` from 8.1.0 to 8.3.0 - [Release notes](https://github.com/ecomfe/vue-echarts/releases) - [Changelog](https://github.com/ecomfe/vue-echarts/blob/main/CHANGELOG.md) - [Commits](https://github.com/ecomfe/vue-echarts/compare/v8.1.0...v8.3.0) Updates `@iconify-json/hugeicons` from 1.2.33 to 1.2.34 - [Commits](https://github.com/iconify/icon-sets/commits) Updates `@iconify-json/lucide` from 1.2.126 to 1.2.129 - [Commits](https://github.com/iconify/icon-sets/commits) Updates `@iconify-json/simple-icons` from 1.2.93 to 1.2.94 - [Commits](https://github.com/iconify/icon-sets/commits) Updates `@typescript-eslint/eslint-plugin` from 8.68.0 to 8.69.0 - [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases) - [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/CHANGELOG.md) - [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.69.0/packages/eslint-plugin) Updates `@typescript-eslint/parser` from 8.68.0 to 8.69.0 - [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases) - [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/parser/CHANGELOG.md) - [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.69.0/packages/parser) Updates `electron` from 44.0.0 to 44.2.0 - [Release notes](https://github.com/electron/electron/releases) - [Commits](https://github.com/electron/electron/compare/v44.0.0...v44.2.0) Updates `eslint` from 10.9.1 to 10.10.0 - [Release notes](https://github.com/eslint/eslint/releases) - [Commits](https://github.com/eslint/eslint/compare/v10.9.1...v10.10.0) Updates `happy-dom` from 20.11.13 to 20.14.0 - [Release notes](https://github.com/capricorn86/happy-dom/releases) - [Commits](https://github.com/capricorn86/happy-dom/compare/v20.11.13...v20.14.0) Updates `knip` from 6.33.0 to 6.34.0 - [Release notes](https://github.com/webpro-nl/knip/releases) - [Commits](https://github.com/webpro-nl/knip/commits/knip@6.34.0/packages/knip) Updates `playwright` from 1.62.1 to 1.63.0 - [Release notes](https://github.com/microsoft/playwright/releases) - [Commits](https://github.com/microsoft/playwright/compare/v1.62.1...v1.63.0) --- updated-dependencies: - dependency-name: "@iconify-json/hugeicons" dependency-version: 1.2.34 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: safe - dependency-name: "@iconify-json/lucide" dependency-version: 1.2.129 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: safe - dependency-name: "@iconify-json/simple-icons" dependency-version: 1.2.94 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: safe - dependency-name: "@internationalized/date" dependency-version: 3.12.4 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: safe - dependency-name: "@pinia/colada" dependency-version: 1.4.4 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: safe - dependency-name: "@pinia/colada-nuxt" dependency-version: 1.1.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: safe - dependency-name: "@sentry/nuxt" dependency-version: 10.73.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: safe - dependency-name: "@tiptap/extension-code-block-lowlight" dependency-version: 3.31.3 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: safe - dependency-name: "@tiptap/extension-collaboration" dependency-version: 3.31.3 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: safe - dependency-name: "@tiptap/extension-collaboration-caret" dependency-version: 3.31.3 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: safe - dependency-name: "@tiptap/extension-file-handler" dependency-version: 3.31.3 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: safe - dependency-name: "@tiptap/extension-list" dependency-version: 3.31.3 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: safe - dependency-name: "@tiptap/extension-node-range" dependency-version: 3.31.3 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: safe - dependency-name: "@tiptap/extension-text-style" dependency-version: 3.31.3 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: safe - dependency-name: "@tiptap/extension-typography" dependency-version: 3.31.3 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: safe - dependency-name: "@tiptap/extensions" dependency-version: 3.31.3 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: safe - dependency-name: "@tiptap/pm" dependency-version: 3.31.3 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: safe - dependency-name: "@tiptap/starter-kit" dependency-version: 3.31.3 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: safe - dependency-name: "@tiptap/suggestion" dependency-version: 3.31.3 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: safe - dependency-name: "@tiptap/vue-3" dependency-version: 3.31.3 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: safe - dependency-name: "@typescript-eslint/eslint-plugin" dependency-version: 8.69.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: safe - dependency-name: "@typescript-eslint/parser" dependency-version: 8.69.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: safe - dependency-name: electron dependency-version: 44.2.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: safe - dependency-name: eslint dependency-version: 10.10.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: safe - dependency-name: happy-dom dependency-version: 20.14.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: safe - dependency-name: knip dependency-version: 6.34.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: safe - dependency-name: playwright dependency-version: 1.63.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: safe - dependency-name: vue-codemirror6 dependency-version: 1.6.2 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: safe - dependency-name: vue-echarts dependency-version: 8.2.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: safe ... Signed-off-by: dependabot[bot] <support@github.com> * fix web lints --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: swithek <52840391+swithek@users.noreply.github.com>
1343 lines
40 KiB
TypeScript
1343 lines
40 KiB
TypeScript
import { afterEach, beforeEach, describe, it } from "vitest"
|
|
import type {
|
|
DocumentComment,
|
|
DocumentCommentReply,
|
|
DocumentCommentsResponse,
|
|
} from "~/utils/api/comment"
|
|
import {
|
|
ANY_DATE,
|
|
ANY_STRING,
|
|
clearQueryCache,
|
|
disposeMockEndpoints,
|
|
mockEndpoint,
|
|
readQueryData,
|
|
runInApp,
|
|
seedAuthOrganization,
|
|
seedAuthSession,
|
|
seedQueryData,
|
|
} from "./test-helpers"
|
|
import useDocumentCommentAPI from "./useDocumentCommentAPI"
|
|
|
|
// isXid() only checks for a 20-character length, so these constants pass
|
|
// the mutations' id guards; NON_XID_ID fails them like a nanoid would
|
|
const DOC_ID = "docid123456789012345"
|
|
const BRANCH_ID = "branchid123456789012"
|
|
const COMMENT_ID = "commentid10000000001"
|
|
const COMMENT_ID_2 = "commentid20000000002"
|
|
const REPLY_ID = "replyid1000000000001"
|
|
const REPLY_ID_2 = "replyid2000000000002"
|
|
const USER_ID = "userid10000000000001"
|
|
const USER_ID_2 = "userid20000000000002"
|
|
const ORG_ID = "orgid100000000000001"
|
|
const NON_XID_ID = "nano1"
|
|
|
|
const COMMENTS_KEY = ["documents", DOC_ID, "comments", BRANCH_ID] as const
|
|
|
|
const COMMENTS_URL = `/api/documents/${DOC_ID}/comments`
|
|
const COMMENT_URL = `${COMMENTS_URL}/${COMMENT_ID}`
|
|
const RESOLVE_URL = `${COMMENT_URL}/resolve`
|
|
const REPLIES_URL = `${COMMENT_URL}/replies`
|
|
const REPLY_URL = `${REPLIES_URL}/${REPLY_ID}`
|
|
|
|
function makeCommentAPI() {
|
|
return runInApp(() => useDocumentCommentAPI())
|
|
}
|
|
|
|
type DocumentCommentAPI = ReturnType<typeof makeCommentAPI>
|
|
|
|
function seedAuth() {
|
|
seedAuthSession(USER_ID)
|
|
seedAuthOrganization(ORG_ID)
|
|
}
|
|
|
|
function seedComments(comments: DocumentCommentsResponse) {
|
|
seedQueryData(COMMENTS_KEY, comments)
|
|
}
|
|
|
|
function readComments() {
|
|
return readQueryData(COMMENTS_KEY) as DocumentCommentsResponse | undefined
|
|
}
|
|
|
|
// dates are ISO strings because clone() is JSON-based — cache contents
|
|
// round-trip through it, so string dates keep the comparisons simple
|
|
function makeComment(
|
|
id: string,
|
|
overrides: Partial<DocumentComment> = {},
|
|
): DocumentComment {
|
|
return {
|
|
id,
|
|
organizationId: ORG_ID,
|
|
documentId: DOC_ID,
|
|
branchId: BRANCH_ID,
|
|
anchorBlockId: "block-1",
|
|
userId: USER_ID,
|
|
resolved: false,
|
|
resolvedBy: null,
|
|
content: { text: `comment ${id}` },
|
|
createdAt: "2024-01-01T00:00:00.000Z",
|
|
updatedAt: null,
|
|
diffDeletionContext: null,
|
|
...overrides,
|
|
}
|
|
}
|
|
|
|
function makeReply(
|
|
id: string,
|
|
overrides: Partial<DocumentCommentReply> = {},
|
|
): DocumentCommentReply {
|
|
return {
|
|
id,
|
|
organizationId: ORG_ID,
|
|
commentId: COMMENT_ID,
|
|
userId: USER_ID,
|
|
content: { text: `reply ${id}` },
|
|
createdAt: "2024-01-01T00:00:00.000Z",
|
|
updatedAt: null,
|
|
...overrides,
|
|
}
|
|
}
|
|
|
|
// 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("useDocumentCommentAPI", { 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("useFetchDocumentCommentsByDocId", () => {
|
|
it.for([
|
|
{ name: "a document id", docId: null, branchId: BRANCH_ID },
|
|
{ name: "a branch id", docId: DOC_ID, branchId: null },
|
|
])(
|
|
"returns no comments without $name",
|
|
async ({ docId, branchId }, { expect }) => {
|
|
seedAuth()
|
|
const getCalls = mockEndpoint("GET", COMMENTS_URL, () => [
|
|
makeComment(COMMENT_ID),
|
|
])
|
|
const api = makeCommentAPI()
|
|
const comments = runInApp(() =>
|
|
api.useFetchDocumentCommentsByDocId(docId, branchId),
|
|
)
|
|
|
|
const result = await comments.refresh()
|
|
|
|
expect(result.data).toEqual([])
|
|
expect(getCalls).toHaveLength(0)
|
|
},
|
|
)
|
|
|
|
it("fetches the comments of the branch", async ({ expect }) => {
|
|
seedAuth()
|
|
const serverComments = [makeComment(COMMENT_ID)]
|
|
const getCalls = mockEndpoint("GET", COMMENTS_URL, () => serverComments)
|
|
const api = makeCommentAPI()
|
|
const comments = runInApp(() =>
|
|
api.useFetchDocumentCommentsByDocId(DOC_ID, BRANCH_ID),
|
|
)
|
|
|
|
const result = await comments.refresh()
|
|
|
|
expect(result.data).toEqual(serverComments)
|
|
expect(getCalls).toHaveLength(1)
|
|
expect(getCalls[0]?.query).toEqual({ branchId: BRANCH_ID })
|
|
})
|
|
})
|
|
|
|
// every mutation guards each of its ids with isXid() in every hook, so
|
|
// one short id makes the whole mutation a no-op: no optimistic write,
|
|
// no request, no invalidation
|
|
it.for([
|
|
{
|
|
name: "createDocumentCommentByDocId",
|
|
method: "POST",
|
|
url: `/api/documents/${NON_XID_ID}/comments`,
|
|
invoke: (api: DocumentCommentAPI) =>
|
|
api.createDocumentCommentByDocId.mutateAsync({
|
|
docId: NON_XID_ID,
|
|
req: {
|
|
content: { text: "new" },
|
|
anchorBlockID: "block-9",
|
|
branchId: BRANCH_ID,
|
|
},
|
|
}),
|
|
},
|
|
{
|
|
name: "updateDocumentCommentByCommentId",
|
|
method: "PUT",
|
|
url: `/api/documents/${NON_XID_ID}/comments/${COMMENT_ID}`,
|
|
invoke: (api: DocumentCommentAPI) =>
|
|
api.updateDocumentCommentByCommentId.mutateAsync({
|
|
docId: NON_XID_ID,
|
|
branchId: BRANCH_ID,
|
|
commentId: COMMENT_ID,
|
|
req: {
|
|
content: { text: "edited" },
|
|
anchorBlockID: "block-9",
|
|
branchId: BRANCH_ID,
|
|
},
|
|
}),
|
|
},
|
|
{
|
|
name: "updateDocumentCommentResolveByCommentId",
|
|
method: "PUT",
|
|
url: `/api/documents/${NON_XID_ID}/comments/${COMMENT_ID}/resolve`,
|
|
invoke: (api: DocumentCommentAPI) =>
|
|
api.updateDocumentCommentResolveByCommentId.mutateAsync({
|
|
docId: NON_XID_ID,
|
|
branchId: BRANCH_ID,
|
|
commentId: COMMENT_ID,
|
|
}),
|
|
},
|
|
{
|
|
name: "deleteDocumentCommentByCommentId",
|
|
method: "DELETE",
|
|
url: `/api/documents/${NON_XID_ID}/comments/${COMMENT_ID}`,
|
|
invoke: (api: DocumentCommentAPI) =>
|
|
api.deleteDocumentCommentByCommentId.mutateAsync({
|
|
docId: NON_XID_ID,
|
|
branchId: BRANCH_ID,
|
|
commentId: COMMENT_ID,
|
|
}),
|
|
},
|
|
{
|
|
name: "createDocumentCommentReplyByCommentId",
|
|
method: "POST",
|
|
url: `/api/documents/${NON_XID_ID}/comments/${COMMENT_ID}/replies`,
|
|
invoke: (api: DocumentCommentAPI) =>
|
|
api.createDocumentCommentReplyByCommentId.mutateAsync({
|
|
docId: NON_XID_ID,
|
|
branchId: BRANCH_ID,
|
|
commentId: COMMENT_ID,
|
|
req: { content: { text: "reply" } },
|
|
}),
|
|
},
|
|
{
|
|
name: "updateDocumentCommentReplyByReplyId",
|
|
method: "PUT",
|
|
url: `/api/documents/${NON_XID_ID}/comments/${COMMENT_ID}/replies/${REPLY_ID}`,
|
|
invoke: (api: DocumentCommentAPI) =>
|
|
api.updateDocumentCommentReplyByReplyId.mutateAsync({
|
|
docId: NON_XID_ID,
|
|
branchId: BRANCH_ID,
|
|
commentId: COMMENT_ID,
|
|
replyId: REPLY_ID,
|
|
req: { content: { text: "edited" } },
|
|
}),
|
|
},
|
|
{
|
|
name: "deleteDocumentCommentReplyByReplyId",
|
|
method: "DELETE",
|
|
url: `/api/documents/${NON_XID_ID}/comments/${COMMENT_ID}/replies/${REPLY_ID}`,
|
|
invoke: (api: DocumentCommentAPI) =>
|
|
api.deleteDocumentCommentReplyByReplyId.mutateAsync({
|
|
docId: NON_XID_ID,
|
|
branchId: BRANCH_ID,
|
|
commentId: COMMENT_ID,
|
|
replyId: REPLY_ID,
|
|
}),
|
|
},
|
|
] as {
|
|
name: string
|
|
method: "GET" | "POST" | "PUT" | "DELETE"
|
|
url: string
|
|
invoke: (api: DocumentCommentAPI) => Promise<unknown>
|
|
}[])(
|
|
"$name sends no request and keeps the cache when the document id is not an xid",
|
|
async ({ method, url, invoke }, { expect }) => {
|
|
seedAuth()
|
|
const existing = makeComment(COMMENT_ID)
|
|
seedComments([existing])
|
|
const requestCalls = mockEndpoint(method, url, () => ({}))
|
|
const api = makeCommentAPI()
|
|
|
|
const result = await invoke(api)
|
|
|
|
expect(result).toBeUndefined()
|
|
expect(requestCalls).toHaveLength(0)
|
|
expect(readComments()).toEqual([existing])
|
|
},
|
|
)
|
|
|
|
describe("createDocumentCommentByDocId", () => {
|
|
it.for([
|
|
{
|
|
name: "prepends an optimistic comment and refetches once the create succeeds",
|
|
makeReq: () => ({
|
|
content: { text: "new" },
|
|
anchorBlockID: "block-9",
|
|
branchId: BRANCH_ID,
|
|
}),
|
|
expectedDiffCtx: null,
|
|
},
|
|
{
|
|
name: "keeps the given diff deletion context on the optimistic comment",
|
|
makeReq: () => ({
|
|
content: { text: "new" },
|
|
anchorBlockID: "block-9",
|
|
branchId: BRANCH_ID,
|
|
diffDeletionContext: {
|
|
textAnchors: [{ nodeUid: "uid-1", fromOffset: 1, toOffset: 3 }],
|
|
},
|
|
}),
|
|
expectedDiffCtx: {
|
|
textAnchors: [{ nodeUid: "uid-1", fromOffset: 1, toOffset: 3 }],
|
|
},
|
|
},
|
|
])("$name", async ({ makeReq, expectedDiffCtx }, { expect }) => {
|
|
seedAuth()
|
|
const existing = makeComment(COMMENT_ID)
|
|
seedComments([existing])
|
|
const serverComments = [
|
|
makeComment(COMMENT_ID_2, { content: { text: "server" } }),
|
|
]
|
|
const getCalls = mockEndpoint("GET", COMMENTS_URL, () => serverComments)
|
|
const serverComment = makeComment(COMMENT_ID_2)
|
|
let commentsAtRequest: DocumentCommentsResponse | undefined
|
|
const postCalls = mockEndpoint("POST", COMMENTS_URL, () => {
|
|
commentsAtRequest = readComments()
|
|
|
|
return serverComment
|
|
})
|
|
const api = makeCommentAPI()
|
|
runInApp(() => api.useFetchDocumentCommentsByDocId(DOC_ID, BRANCH_ID))
|
|
const req = makeReq()
|
|
|
|
const result = await api.createDocumentCommentByDocId.mutateAsync({
|
|
docId: DOC_ID,
|
|
req,
|
|
})
|
|
|
|
expect(result).toEqual(serverComment)
|
|
expect(postCalls).toHaveLength(1)
|
|
expect(postCalls[0]?.body).toEqual(req)
|
|
expect(commentsAtRequest).toEqual([
|
|
expect.objectContaining({
|
|
id: ANY_STRING,
|
|
organizationId: ORG_ID,
|
|
documentId: DOC_ID,
|
|
branchId: BRANCH_ID,
|
|
anchorBlockId: "block-9",
|
|
userId: USER_ID,
|
|
resolved: false,
|
|
resolvedBy: null,
|
|
content: { text: "new" },
|
|
createdAt: ANY_DATE,
|
|
updatedAt: null,
|
|
diffDeletionContext: expectedDiffCtx,
|
|
}),
|
|
existing,
|
|
])
|
|
expect(getCalls).toHaveLength(1)
|
|
expect(readComments()).toEqual(serverComments)
|
|
})
|
|
|
|
it("sends the create without an optimistic insert when session data is missing and refreshes the comments", async ({
|
|
expect,
|
|
}) => {
|
|
const existing = makeComment(COMMENT_ID)
|
|
seedComments([existing])
|
|
const getCalls = mockEndpoint("GET", COMMENTS_URL, () => [])
|
|
let commentsAtRequest: DocumentCommentsResponse | undefined
|
|
const postCalls = mockEndpoint("POST", COMMENTS_URL, () => {
|
|
commentsAtRequest = readComments()
|
|
|
|
return makeComment(COMMENT_ID_2)
|
|
})
|
|
const api = makeCommentAPI()
|
|
runInApp(() => api.useFetchDocumentCommentsByDocId(DOC_ID, BRANCH_ID))
|
|
|
|
await api.createDocumentCommentByDocId.mutateAsync({
|
|
docId: DOC_ID,
|
|
req: {
|
|
content: { text: "new" },
|
|
anchorBlockID: "block-9",
|
|
branchId: BRANCH_ID,
|
|
},
|
|
})
|
|
|
|
expect(postCalls).toHaveLength(1)
|
|
expect(commentsAtRequest).toEqual([existing])
|
|
// the request went out, so the active comments query is refetched
|
|
// even though the optimistic insert was skipped
|
|
expect(getCalls).toHaveLength(1)
|
|
expect(readComments()).toEqual([])
|
|
})
|
|
|
|
it("rolls back the optimistic comment when the create fails", async ({
|
|
expect,
|
|
}) => {
|
|
seedAuth()
|
|
const existing = makeComment(COMMENT_ID)
|
|
seedComments([existing])
|
|
const getCalls = mockEndpoint("GET", COMMENTS_URL, () => [])
|
|
const postCalls = mockEndpoint("POST", COMMENTS_URL, () => {
|
|
throw createError({ statusCode: 500 })
|
|
})
|
|
const api = makeCommentAPI()
|
|
runInApp(() => api.useFetchDocumentCommentsByDocId(DOC_ID, BRANCH_ID))
|
|
|
|
await expect(
|
|
api.createDocumentCommentByDocId.mutateAsync({
|
|
docId: DOC_ID,
|
|
req: {
|
|
content: { text: "new" },
|
|
anchorBlockID: "block-9",
|
|
branchId: BRANCH_ID,
|
|
},
|
|
}),
|
|
).rejects.toThrow()
|
|
|
|
expect(postCalls).toHaveLength(1)
|
|
expect(getCalls).toHaveLength(0)
|
|
expect(readComments()).toEqual([existing])
|
|
})
|
|
|
|
it("skips the rollback when the cache changed after the optimistic insert", async ({
|
|
expect,
|
|
}) => {
|
|
let rejectPost: (err: unknown) => void = () => undefined
|
|
let postReached: () => void = () => undefined
|
|
const postReachedSignal = new Promise<void>((resolve) => {
|
|
postReached = resolve
|
|
})
|
|
|
|
seedAuth()
|
|
seedComments([makeComment(COMMENT_ID)])
|
|
const getCalls = mockEndpoint("GET", COMMENTS_URL, () => [])
|
|
const postCalls = mockEndpoint("POST", COMMENTS_URL, () => {
|
|
postReached()
|
|
|
|
return new Promise((_resolve, reject) => {
|
|
rejectPost = reject
|
|
})
|
|
})
|
|
const api = makeCommentAPI()
|
|
runInApp(() => api.useFetchDocumentCommentsByDocId(DOC_ID, BRANCH_ID))
|
|
|
|
const pending = api.createDocumentCommentByDocId.mutateAsync({
|
|
docId: DOC_ID,
|
|
req: {
|
|
content: { text: "new" },
|
|
anchorBlockID: "block-9",
|
|
branchId: BRANCH_ID,
|
|
},
|
|
})
|
|
await postReachedSignal
|
|
|
|
// the optimistic insert landed; divergent data written afterwards
|
|
// must survive the failure
|
|
expect(readComments()).toHaveLength(2)
|
|
const divergent = makeComment(COMMENT_ID_2, {
|
|
content: { text: "divergent" },
|
|
})
|
|
seedComments([divergent])
|
|
rejectPost(createError({ statusCode: 500 }))
|
|
|
|
await expect(pending).rejects.toThrow()
|
|
expect(postCalls).toHaveLength(1)
|
|
expect(getCalls).toHaveLength(0)
|
|
expect(readComments()).toEqual([divergent])
|
|
})
|
|
})
|
|
|
|
describe("updateDocumentCommentByCommentId", () => {
|
|
it("updates the matching comment optimistically and refetches once the update succeeds", async ({
|
|
expect,
|
|
}) => {
|
|
seedAuth()
|
|
const target = makeComment(COMMENT_ID)
|
|
const other = makeComment(COMMENT_ID_2)
|
|
seedComments([target, other])
|
|
const serverComments = [
|
|
makeComment(COMMENT_ID, { content: { text: "server" } }),
|
|
]
|
|
const getCalls = mockEndpoint("GET", COMMENTS_URL, () => serverComments)
|
|
let commentsAtRequest: DocumentCommentsResponse | undefined
|
|
const putCalls = mockEndpoint("PUT", COMMENT_URL, () => {
|
|
commentsAtRequest = readComments()
|
|
|
|
return {}
|
|
})
|
|
const api = makeCommentAPI()
|
|
runInApp(() => api.useFetchDocumentCommentsByDocId(DOC_ID, BRANCH_ID))
|
|
const req = {
|
|
content: { text: "edited" },
|
|
anchorBlockID: "block-9",
|
|
branchId: BRANCH_ID,
|
|
}
|
|
|
|
await api.updateDocumentCommentByCommentId.mutateAsync({
|
|
docId: DOC_ID,
|
|
branchId: BRANCH_ID,
|
|
commentId: COMMENT_ID,
|
|
req,
|
|
})
|
|
|
|
expect(putCalls).toHaveLength(1)
|
|
expect(putCalls[0]?.body).toEqual(req)
|
|
expect(commentsAtRequest).toEqual([
|
|
{
|
|
...target,
|
|
content: { text: "edited" },
|
|
anchorBlockId: "block-9",
|
|
updatedAt: ANY_DATE,
|
|
},
|
|
other,
|
|
])
|
|
expect(getCalls).toHaveLength(1)
|
|
expect(readComments()).toEqual(serverComments)
|
|
})
|
|
|
|
it("rolls back the optimistic update when the update fails", async ({
|
|
expect,
|
|
}) => {
|
|
seedAuth()
|
|
const target = makeComment(COMMENT_ID)
|
|
seedComments([target])
|
|
const getCalls = mockEndpoint("GET", COMMENTS_URL, () => [])
|
|
const putCalls = mockEndpoint("PUT", COMMENT_URL, () => {
|
|
throw createError({ statusCode: 500 })
|
|
})
|
|
const api = makeCommentAPI()
|
|
runInApp(() => api.useFetchDocumentCommentsByDocId(DOC_ID, BRANCH_ID))
|
|
|
|
await expect(
|
|
api.updateDocumentCommentByCommentId.mutateAsync({
|
|
docId: DOC_ID,
|
|
branchId: BRANCH_ID,
|
|
commentId: COMMENT_ID,
|
|
req: {
|
|
content: { text: "edited" },
|
|
anchorBlockID: "block-9",
|
|
branchId: BRANCH_ID,
|
|
},
|
|
}),
|
|
).rejects.toThrow()
|
|
|
|
expect(putCalls).toHaveLength(1)
|
|
expect(getCalls).toHaveLength(0)
|
|
expect(readComments()).toEqual([target])
|
|
})
|
|
|
|
it("skips the rollback when the cache changed after the optimistic update", async ({
|
|
expect,
|
|
}) => {
|
|
let rejectPut: (err: unknown) => void = () => undefined
|
|
let putReached: () => void = () => undefined
|
|
const putReachedSignal = new Promise<void>((resolve) => {
|
|
putReached = resolve
|
|
})
|
|
|
|
seedAuth()
|
|
seedComments([makeComment(COMMENT_ID)])
|
|
const getCalls = mockEndpoint("GET", COMMENTS_URL, () => [])
|
|
const putCalls = mockEndpoint("PUT", COMMENT_URL, () => {
|
|
putReached()
|
|
|
|
return new Promise((_resolve, reject) => {
|
|
rejectPut = reject
|
|
})
|
|
})
|
|
const api = makeCommentAPI()
|
|
runInApp(() => api.useFetchDocumentCommentsByDocId(DOC_ID, BRANCH_ID))
|
|
|
|
const pending = api.updateDocumentCommentByCommentId.mutateAsync({
|
|
docId: DOC_ID,
|
|
branchId: BRANCH_ID,
|
|
commentId: COMMENT_ID,
|
|
req: {
|
|
content: { text: "edited" },
|
|
anchorBlockID: "block-9",
|
|
branchId: BRANCH_ID,
|
|
},
|
|
})
|
|
await putReachedSignal
|
|
|
|
// the optimistic update landed; divergent data written afterwards
|
|
// must survive the failure
|
|
expect(readComments()?.[0]?.content).toEqual({ text: "edited" })
|
|
const divergent = makeComment(COMMENT_ID_2, {
|
|
content: { text: "divergent" },
|
|
})
|
|
seedComments([divergent])
|
|
rejectPut(createError({ statusCode: 500 }))
|
|
|
|
await expect(pending).rejects.toThrow()
|
|
expect(putCalls).toHaveLength(1)
|
|
expect(getCalls).toHaveLength(0)
|
|
expect(readComments()).toEqual([divergent])
|
|
})
|
|
})
|
|
|
|
describe("updateDocumentCommentResolveByCommentId", () => {
|
|
it("removes the resolved comment optimistically and refetches once the resolve succeeds", async ({
|
|
expect,
|
|
}) => {
|
|
seedAuth()
|
|
const target = makeComment(COMMENT_ID)
|
|
const other = makeComment(COMMENT_ID_2)
|
|
seedComments([target, other])
|
|
const serverComments = [makeComment(COMMENT_ID_2)]
|
|
const getCalls = mockEndpoint("GET", COMMENTS_URL, () => serverComments)
|
|
let commentsAtRequest: DocumentCommentsResponse | undefined
|
|
const putCalls = mockEndpoint("PUT", RESOLVE_URL, () => {
|
|
commentsAtRequest = readComments()
|
|
|
|
return {}
|
|
})
|
|
const api = makeCommentAPI()
|
|
runInApp(() => api.useFetchDocumentCommentsByDocId(DOC_ID, BRANCH_ID))
|
|
|
|
await api.updateDocumentCommentResolveByCommentId.mutateAsync({
|
|
docId: DOC_ID,
|
|
branchId: BRANCH_ID,
|
|
commentId: COMMENT_ID,
|
|
})
|
|
|
|
expect(putCalls).toHaveLength(1)
|
|
expect(commentsAtRequest).toEqual([other])
|
|
expect(getCalls).toHaveLength(1)
|
|
expect(readComments()).toEqual(serverComments)
|
|
})
|
|
|
|
it("rolls back the optimistic removal when the resolve fails", async ({
|
|
expect,
|
|
}) => {
|
|
seedAuth()
|
|
const target = makeComment(COMMENT_ID)
|
|
seedComments([target])
|
|
const getCalls = mockEndpoint("GET", COMMENTS_URL, () => [])
|
|
const putCalls = mockEndpoint("PUT", RESOLVE_URL, () => {
|
|
throw createError({ statusCode: 500 })
|
|
})
|
|
const api = makeCommentAPI()
|
|
runInApp(() => api.useFetchDocumentCommentsByDocId(DOC_ID, BRANCH_ID))
|
|
|
|
await expect(
|
|
api.updateDocumentCommentResolveByCommentId.mutateAsync({
|
|
docId: DOC_ID,
|
|
branchId: BRANCH_ID,
|
|
commentId: COMMENT_ID,
|
|
}),
|
|
).rejects.toThrow()
|
|
|
|
expect(putCalls).toHaveLength(1)
|
|
expect(getCalls).toHaveLength(0)
|
|
expect(readComments()).toEqual([target])
|
|
})
|
|
|
|
it("skips the rollback when the cache changed after the optimistic removal", async ({
|
|
expect,
|
|
}) => {
|
|
let rejectPut: (err: unknown) => void = () => undefined
|
|
let putReached: () => void = () => undefined
|
|
const putReachedSignal = new Promise<void>((resolve) => {
|
|
putReached = resolve
|
|
})
|
|
|
|
seedAuth()
|
|
seedComments([makeComment(COMMENT_ID), makeComment(COMMENT_ID_2)])
|
|
const getCalls = mockEndpoint("GET", COMMENTS_URL, () => [])
|
|
const putCalls = mockEndpoint("PUT", RESOLVE_URL, () => {
|
|
putReached()
|
|
|
|
return new Promise((_resolve, reject) => {
|
|
rejectPut = reject
|
|
})
|
|
})
|
|
const api = makeCommentAPI()
|
|
runInApp(() => api.useFetchDocumentCommentsByDocId(DOC_ID, BRANCH_ID))
|
|
|
|
const pending = api.updateDocumentCommentResolveByCommentId.mutateAsync({
|
|
docId: DOC_ID,
|
|
branchId: BRANCH_ID,
|
|
commentId: COMMENT_ID,
|
|
})
|
|
await putReachedSignal
|
|
|
|
// the optimistic removal landed; divergent data written afterwards
|
|
// must survive the failure
|
|
expect(readComments()).toHaveLength(1)
|
|
const divergent = makeComment(COMMENT_ID_2, {
|
|
content: { text: "divergent" },
|
|
})
|
|
seedComments([divergent])
|
|
rejectPut(createError({ statusCode: 500 }))
|
|
|
|
await expect(pending).rejects.toThrow()
|
|
expect(putCalls).toHaveLength(1)
|
|
expect(getCalls).toHaveLength(0)
|
|
expect(readComments()).toEqual([divergent])
|
|
})
|
|
})
|
|
|
|
describe("deleteDocumentCommentByCommentId", () => {
|
|
it.for([
|
|
{
|
|
name: "replaces the deleted comment with its promoted oldest reply",
|
|
makeSeed: () => [
|
|
makeComment(COMMENT_ID, {
|
|
replies: [
|
|
makeReply(REPLY_ID_2, { createdAt: "2024-01-04T00:00:00.000Z" }),
|
|
makeReply(REPLY_ID, {
|
|
userId: USER_ID_2,
|
|
content: { text: "first reply" },
|
|
createdAt: "2024-01-02T00:00:00.000Z",
|
|
updatedAt: "2024-01-03T00:00:00.000Z",
|
|
}),
|
|
],
|
|
}),
|
|
makeComment(COMMENT_ID_2),
|
|
],
|
|
makeExpectedAtRequest: () => [
|
|
{
|
|
...makeComment(COMMENT_ID),
|
|
userId: USER_ID_2,
|
|
content: { text: "first reply" },
|
|
createdAt: "2024-01-02T00:00:00.000Z",
|
|
updatedAt: "2024-01-03T00:00:00.000Z",
|
|
replies: [
|
|
makeReply(REPLY_ID_2, { createdAt: "2024-01-04T00:00:00.000Z" }),
|
|
],
|
|
},
|
|
makeComment(COMMENT_ID_2),
|
|
],
|
|
},
|
|
{
|
|
name: "removes the deleted comment when it has no replies",
|
|
makeSeed: () => [makeComment(COMMENT_ID), makeComment(COMMENT_ID_2)],
|
|
makeExpectedAtRequest: () => [makeComment(COMMENT_ID_2)],
|
|
},
|
|
])("$name", async ({ makeSeed, makeExpectedAtRequest }, { expect }) => {
|
|
seedAuth()
|
|
seedComments(makeSeed())
|
|
const serverComments = [
|
|
makeComment(COMMENT_ID_2, { content: { text: "server" } }),
|
|
]
|
|
const getCalls = mockEndpoint("GET", COMMENTS_URL, () => serverComments)
|
|
let commentsAtRequest: DocumentCommentsResponse | undefined
|
|
const deleteCalls = mockEndpoint("DELETE", COMMENT_URL, () => {
|
|
commentsAtRequest = readComments()
|
|
|
|
return {}
|
|
})
|
|
const api = makeCommentAPI()
|
|
runInApp(() => api.useFetchDocumentCommentsByDocId(DOC_ID, BRANCH_ID))
|
|
|
|
await api.deleteDocumentCommentByCommentId.mutateAsync({
|
|
docId: DOC_ID,
|
|
branchId: BRANCH_ID,
|
|
commentId: COMMENT_ID,
|
|
})
|
|
|
|
expect(deleteCalls).toHaveLength(1)
|
|
expect(commentsAtRequest).toEqual(makeExpectedAtRequest())
|
|
expect(getCalls).toHaveLength(1)
|
|
expect(readComments()).toEqual(serverComments)
|
|
})
|
|
|
|
it("rolls back the optimistic removal when the delete fails", async ({
|
|
expect,
|
|
}) => {
|
|
seedAuth()
|
|
const target = makeComment(COMMENT_ID)
|
|
seedComments([target])
|
|
const getCalls = mockEndpoint("GET", COMMENTS_URL, () => [])
|
|
const deleteCalls = mockEndpoint("DELETE", COMMENT_URL, () => {
|
|
throw createError({ statusCode: 500 })
|
|
})
|
|
const api = makeCommentAPI()
|
|
runInApp(() => api.useFetchDocumentCommentsByDocId(DOC_ID, BRANCH_ID))
|
|
|
|
await expect(
|
|
api.deleteDocumentCommentByCommentId.mutateAsync({
|
|
docId: DOC_ID,
|
|
branchId: BRANCH_ID,
|
|
commentId: COMMENT_ID,
|
|
}),
|
|
).rejects.toThrow()
|
|
|
|
expect(deleteCalls).toHaveLength(1)
|
|
expect(getCalls).toHaveLength(0)
|
|
expect(readComments()).toEqual([target])
|
|
})
|
|
|
|
it("skips the rollback when the cache changed after the optimistic removal", async ({
|
|
expect,
|
|
}) => {
|
|
let rejectDelete: (err: unknown) => void = () => undefined
|
|
let deleteReached: () => void = () => undefined
|
|
const deleteReachedSignal = new Promise<void>((resolve) => {
|
|
deleteReached = resolve
|
|
})
|
|
|
|
seedAuth()
|
|
seedComments([makeComment(COMMENT_ID), makeComment(COMMENT_ID_2)])
|
|
const getCalls = mockEndpoint("GET", COMMENTS_URL, () => [])
|
|
const deleteCalls = mockEndpoint("DELETE", COMMENT_URL, () => {
|
|
deleteReached()
|
|
|
|
return new Promise((_resolve, reject) => {
|
|
rejectDelete = reject
|
|
})
|
|
})
|
|
const api = makeCommentAPI()
|
|
runInApp(() => api.useFetchDocumentCommentsByDocId(DOC_ID, BRANCH_ID))
|
|
|
|
const pending = api.deleteDocumentCommentByCommentId.mutateAsync({
|
|
docId: DOC_ID,
|
|
branchId: BRANCH_ID,
|
|
commentId: COMMENT_ID,
|
|
})
|
|
await deleteReachedSignal
|
|
|
|
// the optimistic removal landed; divergent data written afterwards
|
|
// must survive the failure
|
|
expect(readComments()).toHaveLength(1)
|
|
const divergent = makeComment(COMMENT_ID_2, {
|
|
content: { text: "divergent" },
|
|
})
|
|
seedComments([divergent])
|
|
rejectDelete(createError({ statusCode: 500 }))
|
|
|
|
await expect(pending).rejects.toThrow()
|
|
expect(deleteCalls).toHaveLength(1)
|
|
expect(getCalls).toHaveLength(0)
|
|
expect(readComments()).toEqual([divergent])
|
|
})
|
|
})
|
|
|
|
describe("createDocumentCommentReplyByCommentId", () => {
|
|
it("prepends an optimistic reply and refetches once the create succeeds", async ({
|
|
expect,
|
|
}) => {
|
|
seedAuth()
|
|
const existingReply = makeReply(REPLY_ID)
|
|
const target = makeComment(COMMENT_ID, { replies: [existingReply] })
|
|
const other = makeComment(COMMENT_ID_2)
|
|
seedComments([target, other])
|
|
const serverComments = [
|
|
makeComment(COMMENT_ID, { content: { text: "server" } }),
|
|
]
|
|
const getCalls = mockEndpoint("GET", COMMENTS_URL, () => serverComments)
|
|
const serverReply = makeReply(REPLY_ID_2)
|
|
let commentsAtRequest: DocumentCommentsResponse | undefined
|
|
const postCalls = mockEndpoint("POST", REPLIES_URL, () => {
|
|
commentsAtRequest = readComments()
|
|
|
|
return serverReply
|
|
})
|
|
const api = makeCommentAPI()
|
|
runInApp(() => api.useFetchDocumentCommentsByDocId(DOC_ID, BRANCH_ID))
|
|
|
|
const result =
|
|
await api.createDocumentCommentReplyByCommentId.mutateAsync({
|
|
docId: DOC_ID,
|
|
branchId: BRANCH_ID,
|
|
commentId: COMMENT_ID,
|
|
req: { content: { text: "reply" } },
|
|
})
|
|
|
|
expect(result).toEqual(serverReply)
|
|
expect(postCalls).toHaveLength(1)
|
|
expect(postCalls[0]?.body).toEqual({ content: { text: "reply" } })
|
|
expect(commentsAtRequest).toEqual([
|
|
{
|
|
...target,
|
|
replies: [
|
|
expect.objectContaining({
|
|
id: ANY_STRING,
|
|
organizationId: ORG_ID,
|
|
commentId: COMMENT_ID,
|
|
userId: USER_ID,
|
|
content: { text: "reply" },
|
|
createdAt: ANY_DATE,
|
|
updatedAt: null,
|
|
}),
|
|
existingReply,
|
|
],
|
|
},
|
|
other,
|
|
])
|
|
expect(getCalls).toHaveLength(1)
|
|
expect(readComments()).toEqual(serverComments)
|
|
})
|
|
|
|
it("sends the create without an optimistic insert when session data is missing and refreshes the comments", async ({
|
|
expect,
|
|
}) => {
|
|
const target = makeComment(COMMENT_ID, { replies: [] })
|
|
seedComments([target])
|
|
const getCalls = mockEndpoint("GET", COMMENTS_URL, () => [])
|
|
let commentsAtRequest: DocumentCommentsResponse | undefined
|
|
const postCalls = mockEndpoint("POST", REPLIES_URL, () => {
|
|
commentsAtRequest = readComments()
|
|
|
|
return makeReply(REPLY_ID)
|
|
})
|
|
const api = makeCommentAPI()
|
|
runInApp(() => api.useFetchDocumentCommentsByDocId(DOC_ID, BRANCH_ID))
|
|
|
|
await api.createDocumentCommentReplyByCommentId.mutateAsync({
|
|
docId: DOC_ID,
|
|
branchId: BRANCH_ID,
|
|
commentId: COMMENT_ID,
|
|
req: { content: { text: "reply" } },
|
|
})
|
|
|
|
expect(postCalls).toHaveLength(1)
|
|
expect(commentsAtRequest).toEqual([target])
|
|
// the request went out, so the active comments query is refetched
|
|
// even though the optimistic insert was skipped
|
|
expect(getCalls).toHaveLength(1)
|
|
expect(readComments()).toEqual([])
|
|
})
|
|
|
|
it("skips the optimistic insert but still refreshes the comments when the comment is not cached", async ({
|
|
expect,
|
|
}) => {
|
|
seedAuth()
|
|
const other = makeComment(COMMENT_ID_2)
|
|
seedComments([other])
|
|
const getCalls = mockEndpoint("GET", COMMENTS_URL, () => [])
|
|
let commentsAtRequest: DocumentCommentsResponse | undefined
|
|
const postCalls = mockEndpoint("POST", REPLIES_URL, () => {
|
|
commentsAtRequest = readComments()
|
|
|
|
return makeReply(REPLY_ID)
|
|
})
|
|
const api = makeCommentAPI()
|
|
runInApp(() => api.useFetchDocumentCommentsByDocId(DOC_ID, BRANCH_ID))
|
|
|
|
await api.createDocumentCommentReplyByCommentId.mutateAsync({
|
|
docId: DOC_ID,
|
|
branchId: BRANCH_ID,
|
|
commentId: COMMENT_ID,
|
|
req: { content: { text: "reply" } },
|
|
})
|
|
|
|
expect(postCalls).toHaveLength(1)
|
|
expect(commentsAtRequest).toEqual([other])
|
|
// the request went out, so the active comments query is refetched
|
|
// even though the target comment was not cached
|
|
expect(getCalls).toHaveLength(1)
|
|
expect(readComments()).toEqual([])
|
|
})
|
|
|
|
it("rolls back the optimistic reply when the create fails", async ({
|
|
expect,
|
|
}) => {
|
|
seedAuth()
|
|
const target = makeComment(COMMENT_ID, { replies: [makeReply(REPLY_ID)] })
|
|
seedComments([target])
|
|
const getCalls = mockEndpoint("GET", COMMENTS_URL, () => [])
|
|
const postCalls = mockEndpoint("POST", REPLIES_URL, () => {
|
|
throw createError({ statusCode: 500 })
|
|
})
|
|
const api = makeCommentAPI()
|
|
runInApp(() => api.useFetchDocumentCommentsByDocId(DOC_ID, BRANCH_ID))
|
|
|
|
await expect(
|
|
api.createDocumentCommentReplyByCommentId.mutateAsync({
|
|
docId: DOC_ID,
|
|
branchId: BRANCH_ID,
|
|
commentId: COMMENT_ID,
|
|
req: { content: { text: "reply" } },
|
|
}),
|
|
).rejects.toThrow()
|
|
|
|
expect(postCalls).toHaveLength(1)
|
|
expect(getCalls).toHaveLength(0)
|
|
expect(readComments()).toEqual([target])
|
|
})
|
|
|
|
it("skips the rollback when the cache changed after the optimistic reply", async ({
|
|
expect,
|
|
}) => {
|
|
let rejectPost: (err: unknown) => void = () => undefined
|
|
let postReached: () => void = () => undefined
|
|
const postReachedSignal = new Promise<void>((resolve) => {
|
|
postReached = resolve
|
|
})
|
|
|
|
seedAuth()
|
|
seedComments([
|
|
makeComment(COMMENT_ID, { replies: [makeReply(REPLY_ID)] }),
|
|
])
|
|
const getCalls = mockEndpoint("GET", COMMENTS_URL, () => [])
|
|
const postCalls = mockEndpoint("POST", REPLIES_URL, () => {
|
|
postReached()
|
|
|
|
return new Promise((_resolve, reject) => {
|
|
rejectPost = reject
|
|
})
|
|
})
|
|
const api = makeCommentAPI()
|
|
runInApp(() => api.useFetchDocumentCommentsByDocId(DOC_ID, BRANCH_ID))
|
|
|
|
const pending = api.createDocumentCommentReplyByCommentId.mutateAsync({
|
|
docId: DOC_ID,
|
|
branchId: BRANCH_ID,
|
|
commentId: COMMENT_ID,
|
|
req: { content: { text: "reply" } },
|
|
})
|
|
await postReachedSignal
|
|
|
|
// the optimistic reply landed; divergent data written afterwards
|
|
// must survive the failure
|
|
expect(readComments()?.[0]?.replies).toHaveLength(2)
|
|
const divergent = makeComment(COMMENT_ID_2, {
|
|
content: { text: "divergent" },
|
|
})
|
|
seedComments([divergent])
|
|
rejectPost(createError({ statusCode: 500 }))
|
|
|
|
await expect(pending).rejects.toThrow()
|
|
expect(postCalls).toHaveLength(1)
|
|
expect(getCalls).toHaveLength(0)
|
|
expect(readComments()).toEqual([divergent])
|
|
})
|
|
})
|
|
|
|
describe("updateDocumentCommentReplyByReplyId", () => {
|
|
it("updates the matching reply optimistically and refetches once the update succeeds", async ({
|
|
expect,
|
|
}) => {
|
|
seedAuth()
|
|
const targetReply = makeReply(REPLY_ID)
|
|
const otherReply = makeReply(REPLY_ID_2)
|
|
const target = makeComment(COMMENT_ID, {
|
|
replies: [targetReply, otherReply],
|
|
})
|
|
const other = makeComment(COMMENT_ID_2)
|
|
seedComments([target, other])
|
|
const serverComments = [
|
|
makeComment(COMMENT_ID, { content: { text: "server" } }),
|
|
]
|
|
const getCalls = mockEndpoint("GET", COMMENTS_URL, () => serverComments)
|
|
let commentsAtRequest: DocumentCommentsResponse | undefined
|
|
const putCalls = mockEndpoint("PUT", REPLY_URL, () => {
|
|
commentsAtRequest = readComments()
|
|
|
|
return {}
|
|
})
|
|
const api = makeCommentAPI()
|
|
runInApp(() => api.useFetchDocumentCommentsByDocId(DOC_ID, BRANCH_ID))
|
|
const req = { content: { text: "edited" } }
|
|
|
|
await api.updateDocumentCommentReplyByReplyId.mutateAsync({
|
|
docId: DOC_ID,
|
|
branchId: BRANCH_ID,
|
|
commentId: COMMENT_ID,
|
|
replyId: REPLY_ID,
|
|
req,
|
|
})
|
|
|
|
expect(putCalls).toHaveLength(1)
|
|
expect(putCalls[0]?.body).toEqual(req)
|
|
expect(commentsAtRequest).toEqual([
|
|
{
|
|
...target,
|
|
replies: [
|
|
{
|
|
...targetReply,
|
|
content: { text: "edited" },
|
|
updatedAt: ANY_DATE,
|
|
},
|
|
otherReply,
|
|
],
|
|
},
|
|
other,
|
|
])
|
|
expect(getCalls).toHaveLength(1)
|
|
expect(readComments()).toEqual(serverComments)
|
|
})
|
|
|
|
it("rolls back the optimistic update when the update fails", async ({
|
|
expect,
|
|
}) => {
|
|
seedAuth()
|
|
const target = makeComment(COMMENT_ID, { replies: [makeReply(REPLY_ID)] })
|
|
seedComments([target])
|
|
const getCalls = mockEndpoint("GET", COMMENTS_URL, () => [])
|
|
const putCalls = mockEndpoint("PUT", REPLY_URL, () => {
|
|
throw createError({ statusCode: 500 })
|
|
})
|
|
const api = makeCommentAPI()
|
|
runInApp(() => api.useFetchDocumentCommentsByDocId(DOC_ID, BRANCH_ID))
|
|
|
|
await expect(
|
|
api.updateDocumentCommentReplyByReplyId.mutateAsync({
|
|
docId: DOC_ID,
|
|
branchId: BRANCH_ID,
|
|
commentId: COMMENT_ID,
|
|
replyId: REPLY_ID,
|
|
req: { content: { text: "edited" } },
|
|
}),
|
|
).rejects.toThrow()
|
|
|
|
expect(putCalls).toHaveLength(1)
|
|
expect(getCalls).toHaveLength(0)
|
|
expect(readComments()).toEqual([target])
|
|
})
|
|
|
|
it("skips the rollback when the cache changed after the optimistic update", async ({
|
|
expect,
|
|
}) => {
|
|
let rejectPut: (err: unknown) => void = () => undefined
|
|
let putReached: () => void = () => undefined
|
|
const putReachedSignal = new Promise<void>((resolve) => {
|
|
putReached = resolve
|
|
})
|
|
|
|
seedAuth()
|
|
seedComments([
|
|
makeComment(COMMENT_ID, { replies: [makeReply(REPLY_ID)] }),
|
|
])
|
|
const getCalls = mockEndpoint("GET", COMMENTS_URL, () => [])
|
|
const putCalls = mockEndpoint("PUT", REPLY_URL, () => {
|
|
putReached()
|
|
|
|
return new Promise((_resolve, reject) => {
|
|
rejectPut = reject
|
|
})
|
|
})
|
|
const api = makeCommentAPI()
|
|
runInApp(() => api.useFetchDocumentCommentsByDocId(DOC_ID, BRANCH_ID))
|
|
|
|
const pending = api.updateDocumentCommentReplyByReplyId.mutateAsync({
|
|
docId: DOC_ID,
|
|
branchId: BRANCH_ID,
|
|
commentId: COMMENT_ID,
|
|
replyId: REPLY_ID,
|
|
req: { content: { text: "edited" } },
|
|
})
|
|
await putReachedSignal
|
|
|
|
// the optimistic update landed; divergent data written afterwards
|
|
// must survive the failure
|
|
expect(readComments()?.[0]?.replies?.[0]?.content).toEqual({
|
|
text: "edited",
|
|
})
|
|
const divergent = makeComment(COMMENT_ID_2, {
|
|
content: { text: "divergent" },
|
|
})
|
|
seedComments([divergent])
|
|
rejectPut(createError({ statusCode: 500 }))
|
|
|
|
await expect(pending).rejects.toThrow()
|
|
expect(putCalls).toHaveLength(1)
|
|
expect(getCalls).toHaveLength(0)
|
|
expect(readComments()).toEqual([divergent])
|
|
})
|
|
})
|
|
|
|
describe("deleteDocumentCommentReplyByReplyId", () => {
|
|
it("removes the matching reply optimistically and refetches once the delete succeeds", async ({
|
|
expect,
|
|
}) => {
|
|
seedAuth()
|
|
const otherReply = makeReply(REPLY_ID_2)
|
|
const target = makeComment(COMMENT_ID, {
|
|
replies: [makeReply(REPLY_ID), otherReply],
|
|
})
|
|
const other = makeComment(COMMENT_ID_2)
|
|
seedComments([target, other])
|
|
const serverComments = [
|
|
makeComment(COMMENT_ID, { content: { text: "server" } }),
|
|
]
|
|
const getCalls = mockEndpoint("GET", COMMENTS_URL, () => serverComments)
|
|
let commentsAtRequest: DocumentCommentsResponse | undefined
|
|
const deleteCalls = mockEndpoint("DELETE", REPLY_URL, () => {
|
|
commentsAtRequest = readComments()
|
|
|
|
return {}
|
|
})
|
|
const api = makeCommentAPI()
|
|
runInApp(() => api.useFetchDocumentCommentsByDocId(DOC_ID, BRANCH_ID))
|
|
|
|
await api.deleteDocumentCommentReplyByReplyId.mutateAsync({
|
|
docId: DOC_ID,
|
|
branchId: BRANCH_ID,
|
|
commentId: COMMENT_ID,
|
|
replyId: REPLY_ID,
|
|
})
|
|
|
|
expect(deleteCalls).toHaveLength(1)
|
|
expect(commentsAtRequest).toEqual([
|
|
{ ...target, replies: [otherReply] },
|
|
other,
|
|
])
|
|
expect(getCalls).toHaveLength(1)
|
|
expect(readComments()).toEqual(serverComments)
|
|
})
|
|
|
|
it("leaves the replies unchanged when the reply is not cached", async ({
|
|
expect,
|
|
}) => {
|
|
seedAuth()
|
|
const target = makeComment(COMMENT_ID, { replies: [makeReply(REPLY_ID)] })
|
|
seedComments([target])
|
|
const serverComments = [
|
|
makeComment(COMMENT_ID, { content: { text: "server" } }),
|
|
]
|
|
const getCalls = mockEndpoint("GET", COMMENTS_URL, () => serverComments)
|
|
let commentsAtRequest: DocumentCommentsResponse | undefined
|
|
const deleteCalls = mockEndpoint(
|
|
"DELETE",
|
|
`${REPLIES_URL}/${REPLY_ID_2}`,
|
|
() => {
|
|
commentsAtRequest = readComments()
|
|
|
|
return {}
|
|
},
|
|
)
|
|
const api = makeCommentAPI()
|
|
runInApp(() => api.useFetchDocumentCommentsByDocId(DOC_ID, BRANCH_ID))
|
|
|
|
await api.deleteDocumentCommentReplyByReplyId.mutateAsync({
|
|
docId: DOC_ID,
|
|
branchId: BRANCH_ID,
|
|
commentId: COMMENT_ID,
|
|
replyId: REPLY_ID_2,
|
|
})
|
|
|
|
expect(deleteCalls).toHaveLength(1)
|
|
expect(commentsAtRequest).toEqual([target])
|
|
// the target comment is cached, so onMutate still returns a
|
|
// context and onSuccess refetches even though nothing changed
|
|
expect(getCalls).toHaveLength(1)
|
|
expect(readComments()).toEqual(serverComments)
|
|
})
|
|
|
|
it("rolls back the optimistic removal when the delete fails", async ({
|
|
expect,
|
|
}) => {
|
|
seedAuth()
|
|
const target = makeComment(COMMENT_ID, { replies: [makeReply(REPLY_ID)] })
|
|
seedComments([target])
|
|
const getCalls = mockEndpoint("GET", COMMENTS_URL, () => [])
|
|
const deleteCalls = mockEndpoint("DELETE", REPLY_URL, () => {
|
|
throw createError({ statusCode: 500 })
|
|
})
|
|
const api = makeCommentAPI()
|
|
runInApp(() => api.useFetchDocumentCommentsByDocId(DOC_ID, BRANCH_ID))
|
|
|
|
await expect(
|
|
api.deleteDocumentCommentReplyByReplyId.mutateAsync({
|
|
docId: DOC_ID,
|
|
branchId: BRANCH_ID,
|
|
commentId: COMMENT_ID,
|
|
replyId: REPLY_ID,
|
|
}),
|
|
).rejects.toThrow()
|
|
|
|
expect(deleteCalls).toHaveLength(1)
|
|
expect(getCalls).toHaveLength(0)
|
|
expect(readComments()).toEqual([target])
|
|
})
|
|
|
|
it("skips the rollback when the cache changed after the optimistic removal", async ({
|
|
expect,
|
|
}) => {
|
|
let rejectDelete: (err: unknown) => void = () => undefined
|
|
let deleteReached: () => void = () => undefined
|
|
const deleteReachedSignal = new Promise<void>((resolve) => {
|
|
deleteReached = resolve
|
|
})
|
|
|
|
seedAuth()
|
|
seedComments([
|
|
makeComment(COMMENT_ID, {
|
|
replies: [makeReply(REPLY_ID), makeReply(REPLY_ID_2)],
|
|
}),
|
|
])
|
|
const getCalls = mockEndpoint("GET", COMMENTS_URL, () => [])
|
|
const deleteCalls = mockEndpoint("DELETE", REPLY_URL, () => {
|
|
deleteReached()
|
|
|
|
return new Promise((_resolve, reject) => {
|
|
rejectDelete = reject
|
|
})
|
|
})
|
|
const api = makeCommentAPI()
|
|
runInApp(() => api.useFetchDocumentCommentsByDocId(DOC_ID, BRANCH_ID))
|
|
|
|
const pending = api.deleteDocumentCommentReplyByReplyId.mutateAsync({
|
|
docId: DOC_ID,
|
|
branchId: BRANCH_ID,
|
|
commentId: COMMENT_ID,
|
|
replyId: REPLY_ID,
|
|
})
|
|
await deleteReachedSignal
|
|
|
|
// the optimistic removal landed; divergent data written afterwards
|
|
// must survive the failure
|
|
expect(readComments()?.[0]?.replies).toHaveLength(1)
|
|
const divergent = makeComment(COMMENT_ID_2, {
|
|
content: { text: "divergent" },
|
|
})
|
|
seedComments([divergent])
|
|
rejectDelete(createError({ statusCode: 500 }))
|
|
|
|
await expect(pending).rejects.toThrow()
|
|
expect(deleteCalls).toHaveLength(1)
|
|
expect(getCalls).toHaveLength(0)
|
|
expect(readComments()).toEqual([divergent])
|
|
})
|
|
})
|
|
|
|
describe("patchOptimisticCommentEntry", () => {
|
|
it("does nothing when no comments are cached", ({ expect }) => {
|
|
seedAuth()
|
|
const getCalls = mockEndpoint("GET", COMMENTS_URL, () => [])
|
|
const api = makeCommentAPI()
|
|
|
|
api.patchOptimisticCommentEntry(
|
|
DOC_ID,
|
|
BRANCH_ID,
|
|
makeComment(COMMENT_ID),
|
|
)
|
|
|
|
expect(readComments()).toBeUndefined()
|
|
expect(getCalls).toHaveLength(0)
|
|
})
|
|
|
|
it.for([
|
|
{
|
|
name: "leaves the cached comments untouched when no optimistic entry exists",
|
|
makeSeed: () => [makeComment(COMMENT_ID), makeComment(COMMENT_ID_2)],
|
|
makeExpected: () => [
|
|
makeComment(COMMENT_ID),
|
|
makeComment(COMMENT_ID_2),
|
|
],
|
|
},
|
|
{
|
|
name: "replaces the optimistic entry with the server comment",
|
|
makeSeed: () => [makeComment(NON_XID_ID), makeComment(COMMENT_ID_2)],
|
|
makeExpected: (server: DocumentComment) => [
|
|
server,
|
|
makeComment(COMMENT_ID_2),
|
|
],
|
|
},
|
|
])("$name", ({ makeSeed, makeExpected }, { expect }) => {
|
|
seedAuth()
|
|
seedComments(makeSeed())
|
|
const getCalls = mockEndpoint("GET", COMMENTS_URL, () => [])
|
|
const api = makeCommentAPI()
|
|
const serverComment = makeComment(COMMENT_ID, {
|
|
content: { text: "server" },
|
|
})
|
|
|
|
api.patchOptimisticCommentEntry(DOC_ID, BRANCH_ID, serverComment)
|
|
|
|
expect(readComments()).toEqual(makeExpected(serverComment))
|
|
expect(getCalls).toHaveLength(0)
|
|
})
|
|
})
|
|
})
|