@raketa-cloud/admin 1.10.11 → 1.10.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/dist/application.css +699 -244
  2. package/package.json +4 -3
  3. package/src/cloud/Comments.js +88 -0
  4. package/src/cloud/Editors.js +20 -0
  5. package/src/cloud/ResourceChanges.js +1 -1
  6. package/src/cloud/documents/commentsDocuments.js +103 -0
  7. package/src/cloud/documents/editorsDocuments.js +27 -0
  8. package/src/handlers/resources/DiscussionsHandler.js +203 -0
  9. package/src/handlers/resources/HistoryHandler.js +7 -5
  10. package/src/handlers/resources/ResourcesHandler.js +6 -5
  11. package/src/helpers/commentMarkdown.js +94 -0
  12. package/src/helpers/fetchCommentCounts.js +14 -0
  13. package/src/helpers/historyUtils.js +11 -0
  14. package/src/helpers/utils.js +24 -0
  15. package/src/index.js +1 -0
  16. package/src/routeResourceModule.js +42 -0
  17. package/src/setupCloud.js +4 -0
  18. package/src/styles/application.css +56 -0
  19. package/src/tests/cloud/Comments.test.js +141 -0
  20. package/src/tests/cloud/Editors.test.js +47 -0
  21. package/src/tests/cloud/ResourceChanges.test.js +25 -0
  22. package/src/tests/handlers/resources/DiscussionsHandler.test/create.test.js +108 -0
  23. package/src/tests/handlers/resources/DiscussionsHandler.test/destroy.test.js +55 -0
  24. package/src/tests/handlers/resources/DiscussionsHandler.test/index.test.js +195 -0
  25. package/src/tests/handlers/resources/DiscussionsHandler.test/update.test.js +87 -0
  26. package/src/tests/handlers/resources/HistoryHandler.test/index.test.js +9 -0
  27. package/src/tests/handlers/resources/ResourcesHandler.test/show.test.js +33 -0
  28. package/src/tests/handlers/resources/ResourcesHandler.test/show_reports.test.js +3 -0
  29. package/src/tests/helpers/commentMarkdown.test.js +166 -0
  30. package/src/tests/helpers/fetchCommentCounts.test.js +42 -0
  31. package/src/tests/helpers/historyUtils.test.js +26 -0
  32. package/src/tests/helpers/utils.test.js +21 -0
  33. package/src/tests/routeResourceModule.test.js +82 -2
  34. package/src/tests/setupCloud.test.js +4 -0
  35. package/src/tests/views/discussions.test.js +519 -0
  36. package/src/views/resources/_shared/_history_toolbar.ejs +10 -0
  37. package/src/views/resources/_shared/_tabs.ejs +21 -0
  38. package/src/views/resources/_shared/_toolbar.ejs +1 -1
  39. package/src/views/resources/discussions/_avatar.ejs +13 -0
  40. package/src/views/resources/discussions/_discussion_tabs.ejs +22 -0
  41. package/src/views/resources/discussions/index.ejs +729 -0
  42. package/src/views/resources/show.ejs +17 -0
@@ -1,3 +1,8 @@
1
+ import commentMarkdown, {
2
+ commentMentionIds,
3
+ commentMentionsText,
4
+ } from "./commentMarkdown";
5
+
1
6
  function includedInCollection(value, collection) {
2
7
  if (!collection) {
3
8
  return false;
@@ -43,6 +48,21 @@ export function isFeatureEnabled(value) {
43
48
  return value === true;
44
49
  }
45
50
 
51
+ // Initials of the first two words of the name, e.g. "Jane van Doe" -> "JV".
52
+ // Falls back to the first letter of the email when there is no name.
53
+ function initials(name, email = "") {
54
+ const words = (name || "").trim().split(/\s+/).filter(Boolean);
55
+
56
+ if (words.length) {
57
+ return words
58
+ .slice(0, 2)
59
+ .map((word) => word[0].toUpperCase())
60
+ .join("");
61
+ }
62
+
63
+ return (email || "?").trim().charAt(0).toUpperCase() || "?";
64
+ }
65
+
46
66
  function feature(featureName, context) {
47
67
  return isFeatureEnabled(context?.globalConfig?.features?.[featureName]);
48
68
  }
@@ -52,9 +72,13 @@ function contentTaskProviderEnabled(provider, context) {
52
72
  }
53
73
 
54
74
  export default {
75
+ commentMarkdown,
76
+ commentMentionIds,
77
+ commentMentionsText,
55
78
  includedInCollection,
56
79
  formattedDate,
57
80
  formattedDateTime,
58
81
  feature,
59
82
  contentTaskProviderEnabled,
83
+ initials,
60
84
  };
package/src/index.js CHANGED
@@ -40,6 +40,7 @@ export { default as ResourcesAPIHandler } from "./handlers/resources/ResourcesAP
40
40
  export { default as ResourceChangeHandler } from "./handlers/resources/ChangeHandler";
41
41
  export { default as ResourceClonesHandler } from "./handlers/resources/ClonesHandler";
42
42
  export { default as ResourceDeletesHandler } from "./handlers/resources/DeletesHandler";
43
+ export { default as ResourceDiscussionsHandler } from "./handlers/resources/DiscussionsHandler";
43
44
  export { default as ResourceExportsHandler } from "./handlers/resources/ExportsHandler";
44
45
  export { default as ResourceHistoryHandler } from "./handlers/resources/HistoryHandler";
45
46
  export { default as ResourceTranslationsHandler } from "./handlers/resources/TranslationsHandler";
@@ -13,6 +13,7 @@ import WidgetHandler from "./handlers/resource_revisions/WidgetHandler";
13
13
  import ChangeHandler from "./handlers/resources/ChangeHandler";
14
14
  import ClonesHandler from "./handlers/resources/ClonesHandler";
15
15
  import DeletesHandler from "./handlers/resources/DeletesHandler";
16
+ import DiscussionsHandler from "./handlers/resources/DiscussionsHandler";
16
17
  import ExportsHandler from "./handlers/resources/ExportsHandler";
17
18
  import HistoryHandler from "./handlers/resources/HistoryHandler";
18
19
  import PartialTranslationsHandler from "./handlers/resources/PartialTranslationsHandler";
@@ -43,12 +44,15 @@ function routeResourceModule(
43
44
 
44
45
  const reportsEnabled =
45
46
  adapter.globalConfig?.features?.reports?.[resourceType] || false;
47
+ const commentsEnabled =
48
+ adapter.globalConfig?.features?.comments?.[resourceType] || false;
46
49
 
47
50
  const resourceConfig = {
48
51
  resourceType,
49
52
  resourceTypePlural,
50
53
  dateFields,
51
54
  reports: reportsEnabled,
55
+ comments: commentsEnabled,
52
56
  };
53
57
 
54
58
  const exportsMapping = [
@@ -110,6 +114,44 @@ function routeResourceModule(
110
114
  viewsFallback: "resources/history",
111
115
  });
112
116
 
117
+ if (commentsEnabled) {
118
+ // Every route is scoped by the resource type, as the discussions handler
119
+ // builds its redirects from the resource config of the route.
120
+ const discussionsPath = `/${baseResource}/:parent_id/discussions`;
121
+ const commentPath = `/${baseResource}/:parent_id/comments/:id`;
122
+ const discussionsViews = `${baseResource}/discussions`;
123
+
124
+ adapter.routeNestedCollection(
125
+ [
126
+ ["GET", discussionsPath, "index", DiscussionsHandler.index],
127
+ ["POST", discussionsPath, "create_form", DiscussionsHandler.create],
128
+ // The discussions of revisions, next to the general one above.
129
+ [
130
+ "GET",
131
+ `${discussionsPath}/:revision_id`,
132
+ "index",
133
+ DiscussionsHandler.index,
134
+ ],
135
+ [
136
+ "POST",
137
+ `${discussionsPath}/:revision_id`,
138
+ "create_form",
139
+ DiscussionsHandler.create,
140
+ ],
141
+ ["PUT", commentPath, "update", DiscussionsHandler.update],
142
+ ["DELETE", commentPath, "destroy", DiscussionsHandler.destroy],
143
+ ].map(([verb, path, action, handler]) => [
144
+ verb,
145
+ path,
146
+ "discussions",
147
+ discussionsViews,
148
+ action,
149
+ handler,
150
+ ]),
151
+ { ...resourceConfig, viewsFallback: "resources/discussions" },
152
+ );
153
+ }
154
+
113
155
  routeNestedCollection(
114
156
  adapter,
115
157
  baseResource,
package/src/setupCloud.js CHANGED
@@ -1,5 +1,7 @@
1
1
  import Categories from "./cloud/Categories";
2
+ import Comments from "./cloud/Comments";
2
3
  import ContentTasks from "./cloud/ContentTasks";
4
+ import Editors from "./cloud/Editors";
3
5
  import Redirects from "./cloud/Redirects";
4
6
  import ResourceChanges from "./cloud/ResourceChanges";
5
7
  import ResourceRevisions from "./cloud/ResourceRevisions";
@@ -26,6 +28,8 @@ function setupCloud(ctx, getConfig) {
26
28
  ctx.authentication.token,
27
29
  );
28
30
  ctx.client.categories = new Categories(getConfig, ctx.authentication.token);
31
+ ctx.client.comments = new Comments(getConfig, ctx.authentication.token);
32
+ ctx.client.editors = new Editors(getConfig, ctx.authentication.token);
29
33
  ctx.client.contentTasks = new ContentTasks(
30
34
  getConfig,
31
35
  ctx.authentication.token,
@@ -357,6 +357,62 @@ body {
357
357
  }
358
358
 
359
359
  /* Dashboard specific */
360
+ /* Markdown of the discussion comments */
361
+ .comment-body {
362
+ @apply text-sm leading-relaxed break-words;
363
+
364
+ p,
365
+ ul,
366
+ ol,
367
+ blockquote {
368
+ @apply my-1.5 first:mt-0 last:mb-0;
369
+ }
370
+
371
+ ul {
372
+ @apply pl-5 list-disc;
373
+ }
374
+
375
+ ol {
376
+ @apply pl-5 list-decimal;
377
+ }
378
+
379
+ blockquote {
380
+ @apply pl-3 border-l-4 border-raketa-primary/40 text-raketa-foreground/70;
381
+ }
382
+
383
+ a {
384
+ @apply underline underline-offset-2 text-raketa-primary hover:text-raketa-foreground;
385
+ }
386
+
387
+ strong {
388
+ @apply font-semibold;
389
+ }
390
+
391
+ .comment-mention {
392
+ @apply px-1 font-semibold text-blue-700 bg-blue-100 rounded dark:bg-blue-900/50 dark:text-blue-300;
393
+ }
394
+
395
+ .comment-mention-unknown {
396
+ @apply italic text-raketa-foreground/50;
397
+ }
398
+ }
399
+
400
+ /* Textareas can't style parts of their text, so it is drawn transparent and
401
+ a copy of it with the mentions highlighted is laid over it. The copy must
402
+ keep the same text metrics, so no padding or font weight changes. */
403
+ .mention-input {
404
+ color: transparent;
405
+ caret-color: var(--color-raketa-foreground);
406
+ }
407
+
408
+ .mention-backdrop {
409
+ @apply absolute overflow-hidden border-solid pointer-events-none whitespace-pre-wrap break-words;
410
+
411
+ mark {
412
+ @apply text-blue-700 bg-blue-100 rounded-sm dark:bg-blue-900/50 dark:text-blue-300;
413
+ }
414
+ }
415
+
360
416
  .card-button {
361
417
  @apply flex items-center gap-2 mb-2 max-w-[400px] border p-4 rounded font-semibold text-raketa-primary hover:border-raketa-primary hover:bg-raketa-primary/5;
362
418
  }
@@ -0,0 +1,141 @@
1
+ import Comments from "../../cloud/Comments";
2
+ import {
3
+ CreateResourceCommentDocument,
4
+ DeleteResourceCommentDocument,
5
+ ResourceCommentCountsDocument,
6
+ ResourceCommentsDocument,
7
+ UpdateResourceCommentDocument,
8
+ } from "../../cloud/documents/commentsDocuments";
9
+
10
+ describe("Comments client", () => {
11
+ function buildClient() {
12
+ return new Comments(() => ({}), "token");
13
+ }
14
+
15
+ test("list requests the comments of the resource", async () => {
16
+ const client = buildClient();
17
+ const items = { list: [], pagination: null };
18
+ const query = jest
19
+ .spyOn(client, "query")
20
+ .mockResolvedValue({ resourceComments: items });
21
+
22
+ const result = await client.list({ page: "2" }, "7", "en", "page");
23
+
24
+ expect(result).toBe(items);
25
+ expect(query).toHaveBeenCalledWith(ResourceCommentsDocument, {
26
+ input: {
27
+ id: "7",
28
+ locale: "en",
29
+ resourceType: "page",
30
+ revisionId: null,
31
+ pagination: { page: 2, pageSize: 25 },
32
+ },
33
+ });
34
+ });
35
+
36
+ test("create posts the comment with the quoted comment", async () => {
37
+ const client = buildClient();
38
+ const comment = { id: "1" };
39
+ const query = jest.spyOn(client, "query").mockResolvedValue({
40
+ createResourceComment: { comment, errors: null },
41
+ });
42
+
43
+ const result = await client.create("7", "en", "page", {
44
+ body: "Hello",
45
+ replyId: "3",
46
+ revisionId: "200",
47
+ });
48
+
49
+ expect(result).toEqual({ comment, errors: null });
50
+ expect(query).toHaveBeenCalledWith(CreateResourceCommentDocument, {
51
+ input: {
52
+ id: "7",
53
+ locale: "en",
54
+ resourceType: "page",
55
+ revisionId: "200",
56
+ comment: { body: "Hello", replyId: "3" },
57
+ },
58
+ });
59
+ });
60
+
61
+ test("list requests the comments of a revision discussion", async () => {
62
+ const client = buildClient();
63
+ const query = jest
64
+ .spyOn(client, "query")
65
+ .mockResolvedValue({ resourceComments: { list: [] } });
66
+
67
+ await client.list({}, "7", "en", "page", "200");
68
+
69
+ expect(query.mock.calls[0][1].input.revisionId).toEqual("200");
70
+ });
71
+
72
+ test("create sends a null reply id when not quoting", async () => {
73
+ const client = buildClient();
74
+ const query = jest.spyOn(client, "query").mockResolvedValue({
75
+ createResourceComment: { comment: null, errors: [] },
76
+ });
77
+
78
+ await client.create("7", "en", "page", { body: "Hello", replyId: "" });
79
+
80
+ expect(query.mock.calls[0][1].input.comment).toEqual({
81
+ body: "Hello",
82
+ replyId: null,
83
+ });
84
+ expect(query.mock.calls[0][1].input.revisionId).toBeNull();
85
+ });
86
+
87
+ test("counts maps the comment counts by discussion", async () => {
88
+ const client = buildClient();
89
+ const query = jest.spyOn(client, "query").mockResolvedValue({
90
+ resourceCommentCounts: [
91
+ { revisionId: null, count: 2 },
92
+ { revisionId: "200", count: 5 },
93
+ ],
94
+ });
95
+
96
+ const result = await client.counts("7", "en", "page");
97
+
98
+ expect(result).toEqual({ general: 2, 200: 5 });
99
+ expect(query).toHaveBeenCalledWith(ResourceCommentCountsDocument, {
100
+ input: { id: "7", locale: "en", resourceType: "page" },
101
+ });
102
+ });
103
+
104
+ test("counts handles a missing list", async () => {
105
+ const client = buildClient();
106
+ jest
107
+ .spyOn(client, "query")
108
+ .mockResolvedValue({ resourceCommentCounts: null });
109
+
110
+ expect(await client.counts("7", "en", "page")).toEqual({});
111
+ });
112
+
113
+ test("update changes the body of the comment", async () => {
114
+ const client = buildClient();
115
+ const comment = { id: "1", body: "Edited" };
116
+ const query = jest.spyOn(client, "query").mockResolvedValue({
117
+ updateResourceComment: { comment, errors: null },
118
+ });
119
+
120
+ const result = await client.update("1", { body: "Edited" });
121
+
122
+ expect(result).toEqual({ comment, errors: null });
123
+ expect(query).toHaveBeenCalledWith(UpdateResourceCommentDocument, {
124
+ input: { id: "1", comment: { body: "Edited" } },
125
+ });
126
+ });
127
+
128
+ test("delete removes the comment", async () => {
129
+ const client = buildClient();
130
+ const query = jest.spyOn(client, "query").mockResolvedValue({
131
+ deleteResourceComment: { comment: { id: "1" }, errors: null },
132
+ });
133
+
134
+ const result = await client.delete("1");
135
+
136
+ expect(result).toEqual({ comment: { id: "1" }, errors: null });
137
+ expect(query).toHaveBeenCalledWith(DeleteResourceCommentDocument, {
138
+ input: { id: "1" },
139
+ });
140
+ });
141
+ });
@@ -0,0 +1,47 @@
1
+ import Editors from "../../cloud/Editors";
2
+ import {
3
+ CurrentEditorDocument,
4
+ WebsiteEditorsDocument,
5
+ } from "../../cloud/documents/editorsDocuments";
6
+
7
+ describe("Editors client", () => {
8
+ test("current requests the editor behind the identity", async () => {
9
+ const client = new Editors(() => ({}), "token");
10
+ const editor = {
11
+ id: "5",
12
+ name: "Jane",
13
+ email: "jane@example.com",
14
+ avatar: "",
15
+ };
16
+ const query = jest
17
+ .spyOn(client, "query")
18
+ .mockResolvedValue({ currentEditor: editor });
19
+
20
+ const result = await client.current();
21
+
22
+ expect(result).toBe(editor);
23
+ expect(query).toHaveBeenCalledWith(CurrentEditorDocument);
24
+ });
25
+
26
+ test("website requests the editors of the website", async () => {
27
+ const client = new Editors(() => ({}), "token");
28
+ const editors = [
29
+ { id: "5", name: "Jane", email: "jane@example.com", avatar: "" },
30
+ ];
31
+ const query = jest
32
+ .spyOn(client, "query")
33
+ .mockResolvedValue({ websiteEditors: { list: editors } });
34
+
35
+ const result = await client.website();
36
+
37
+ expect(result).toBe(editors);
38
+ expect(query).toHaveBeenCalledWith(WebsiteEditorsDocument);
39
+ });
40
+
41
+ test("website returns an empty list without editors", async () => {
42
+ const client = new Editors(() => ({}), "token");
43
+ jest.spyOn(client, "query").mockResolvedValue({ websiteEditors: null });
44
+
45
+ expect(await client.website()).toEqual([]);
46
+ });
47
+ });
@@ -0,0 +1,25 @@
1
+ import ResourceChanges from "../../cloud/ResourceChanges";
2
+ import { ResourceHistoryDocument } from "../../cloud/documents/resourcesDocuments";
3
+
4
+ describe("ResourceChanges", () => {
5
+ describe("list", () => {
6
+ test("uses camelCase pageSize when requesting resource history", async () => {
7
+ const client = new ResourceChanges(() => ({}), "token");
8
+ const query = jest
9
+ .spyOn(client, "query")
10
+ .mockResolvedValue({ resourceHistory: { list: [] } });
11
+
12
+ await client.list({ page: "2" }, "resource-1", "en", "page");
13
+
14
+ expect(query).toHaveBeenCalledWith(ResourceHistoryDocument, {
15
+ input: {
16
+ id: "resource-1",
17
+ locale: "en",
18
+ resourceType: "page",
19
+ filters: {},
20
+ pagination: { page: 2, pageSize: 50 },
21
+ },
22
+ });
23
+ });
24
+ });
25
+ });
@@ -0,0 +1,108 @@
1
+ import Comments from "../../../../cloud/Comments";
2
+ import DiscussionsHandler from "../../../../handlers/resources/DiscussionsHandler";
3
+
4
+ describe("Resources Discussions Handler create", () => {
5
+ function buildCtx(comment) {
6
+ return {
7
+ client: {
8
+ comments: new Comments(() => ({}), "token"),
9
+ },
10
+ resourceParams: { comment },
11
+ locale: "en",
12
+ params: { parent_id: "123" },
13
+ resourceConfig: {
14
+ resourceType: "article",
15
+ resourceTypePlural: "articles",
16
+ },
17
+ };
18
+ }
19
+
20
+ test("creates the comment and redirects back to the discussions page", async () => {
21
+ const ctx = buildCtx({ body: " Hello ", reply_id: "5" });
22
+ const create = jest
23
+ .spyOn(ctx.client.comments, "create")
24
+ .mockImplementation(() => ({ comment: { id: "9" }, errors: null }));
25
+
26
+ const result = await DiscussionsHandler.create(ctx);
27
+
28
+ expect(create).toHaveBeenCalledWith("123", "en", "article", {
29
+ body: "Hello",
30
+ replyId: "5",
31
+ revisionId: null,
32
+ });
33
+ expect(result).toEqual({
34
+ redirect: "/en/articles/123/discussions",
35
+ });
36
+ });
37
+
38
+ test("creates the comment in a revision discussion", async () => {
39
+ const ctx = buildCtx({ body: "Hello" });
40
+ ctx.params.revision_id = "200";
41
+ const create = jest
42
+ .spyOn(ctx.client.comments, "create")
43
+ .mockImplementation(() => ({ comment: { id: "9" }, errors: null }));
44
+
45
+ const result = await DiscussionsHandler.create(ctx);
46
+
47
+ expect(create).toHaveBeenCalledWith("123", "en", "article", {
48
+ body: "Hello",
49
+ replyId: undefined,
50
+ revisionId: "200",
51
+ });
52
+ expect(result).toEqual({
53
+ redirect: "/en/articles/123/discussions/200",
54
+ });
55
+ });
56
+
57
+ test("redirects with an error for a blank body", async () => {
58
+ const ctx = buildCtx({ body: " " });
59
+ const create = jest.spyOn(ctx.client.comments, "create");
60
+
61
+ const result = await DiscussionsHandler.create(ctx);
62
+
63
+ expect(create).not.toHaveBeenCalled();
64
+ expect(result).toEqual({
65
+ redirect: `/en/articles/123/discussions?comment_error=${encodeURIComponent("Comment can't be blank.")}`,
66
+ });
67
+ });
68
+
69
+ test("redirects with an error when there are no params", async () => {
70
+ const ctx = { ...buildCtx(), resourceParams: {} };
71
+
72
+ const result = await DiscussionsHandler.create(ctx);
73
+
74
+ expect(result.redirect).toContain("comment_error=");
75
+ });
76
+
77
+ test("redirects with the API errors", async () => {
78
+ const ctx = buildCtx({ body: "Hello", reply_id: "99" });
79
+ jest.spyOn(ctx.client.comments, "create").mockImplementation(() => ({
80
+ comment: null,
81
+ errors: [
82
+ { key: "replyId", message: "does not exist" },
83
+ { key: "generalError", message: "Article not found" },
84
+ ],
85
+ }));
86
+
87
+ const result = await DiscussionsHandler.create(ctx);
88
+
89
+ expect(result).toEqual({
90
+ redirect: `/en/articles/123/discussions?comment_error=${encodeURIComponent("does not exist, Article not found")}`,
91
+ });
92
+ });
93
+
94
+ test("redirects with the read-only error for a published revision", async () => {
95
+ const ctx = buildCtx({ body: "Hello" });
96
+ ctx.params.revision_id = "200";
97
+ jest.spyOn(ctx.client.comments, "create").mockImplementation(() => ({
98
+ comment: null,
99
+ errors: [{ key: "revisionId", message: "does not exist" }],
100
+ }));
101
+
102
+ const result = await DiscussionsHandler.create(ctx);
103
+
104
+ expect(result).toEqual({
105
+ redirect: `/en/articles/123/discussions/200?comment_error=${encodeURIComponent("This revision was published, its discussion is read-only.")}`,
106
+ });
107
+ });
108
+ });
@@ -0,0 +1,55 @@
1
+ import Comments from "../../../../cloud/Comments";
2
+ import DiscussionsHandler from "../../../../handlers/resources/DiscussionsHandler";
3
+
4
+ describe("Resources Discussions Handler destroy", () => {
5
+ function buildCtx() {
6
+ return {
7
+ client: {
8
+ comments: new Comments(() => ({}), "token"),
9
+ },
10
+ locale: "en",
11
+ params: { id: "9" },
12
+ };
13
+ }
14
+
15
+ test("deletes the comment and refreshes the page", async () => {
16
+ const ctx = buildCtx();
17
+ const remove = jest
18
+ .spyOn(ctx.client.comments, "delete")
19
+ .mockImplementation(() => ({ comment: { id: "9" }, errors: null }));
20
+
21
+ const result = await DiscussionsHandler.destroy(ctx);
22
+
23
+ expect(remove).toHaveBeenCalledWith("9");
24
+ expect(result).toEqual({ headers: { "HX-Refresh": "true" }, json: {} });
25
+ });
26
+
27
+ test("throws when the comment cannot be deleted", async () => {
28
+ const ctx = buildCtx();
29
+ jest.spyOn(ctx.client.comments, "delete").mockImplementation(() => ({
30
+ comment: null,
31
+ errors: [{ key: "generalError", message: "Comment not found" }],
32
+ }));
33
+
34
+ await expect(DiscussionsHandler.destroy(ctx)).rejects.toMatch(
35
+ "Failed to delete comment #9: Comment not found",
36
+ );
37
+ });
38
+
39
+ test("throws with the reason when the comment is frozen", async () => {
40
+ const ctx = buildCtx();
41
+ jest.spyOn(ctx.client.comments, "delete").mockImplementation(() => ({
42
+ comment: null,
43
+ errors: [
44
+ {
45
+ key: "generalError",
46
+ message: "Comments of published revisions cannot be changed",
47
+ },
48
+ ],
49
+ }));
50
+
51
+ await expect(DiscussionsHandler.destroy(ctx)).rejects.toMatch(
52
+ "Failed to delete comment #9: Comments of published revisions cannot be changed",
53
+ );
54
+ });
55
+ });