@raketa-cloud/admin 1.10.12 → 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.
- package/dist/application.css +699 -244
- package/package.json +4 -3
- package/src/cloud/Comments.js +88 -0
- package/src/cloud/Editors.js +20 -0
- package/src/cloud/documents/commentsDocuments.js +103 -0
- package/src/cloud/documents/editorsDocuments.js +27 -0
- package/src/handlers/resources/DiscussionsHandler.js +203 -0
- package/src/handlers/resources/HistoryHandler.js +7 -5
- package/src/handlers/resources/ResourcesHandler.js +6 -5
- package/src/helpers/commentMarkdown.js +94 -0
- package/src/helpers/fetchCommentCounts.js +14 -0
- package/src/helpers/historyUtils.js +11 -0
- package/src/helpers/utils.js +24 -0
- package/src/index.js +1 -0
- package/src/routeResourceModule.js +42 -0
- package/src/setupCloud.js +4 -0
- package/src/styles/application.css +56 -0
- package/src/tests/cloud/Comments.test.js +141 -0
- package/src/tests/cloud/Editors.test.js +47 -0
- package/src/tests/handlers/resources/DiscussionsHandler.test/create.test.js +108 -0
- package/src/tests/handlers/resources/DiscussionsHandler.test/destroy.test.js +55 -0
- package/src/tests/handlers/resources/DiscussionsHandler.test/index.test.js +195 -0
- package/src/tests/handlers/resources/DiscussionsHandler.test/update.test.js +87 -0
- package/src/tests/handlers/resources/HistoryHandler.test/index.test.js +9 -0
- package/src/tests/handlers/resources/ResourcesHandler.test/show.test.js +33 -0
- package/src/tests/handlers/resources/ResourcesHandler.test/show_reports.test.js +3 -0
- package/src/tests/helpers/commentMarkdown.test.js +166 -0
- package/src/tests/helpers/fetchCommentCounts.test.js +42 -0
- package/src/tests/helpers/historyUtils.test.js +26 -0
- package/src/tests/helpers/utils.test.js +21 -0
- package/src/tests/routeResourceModule.test.js +82 -2
- package/src/tests/setupCloud.test.js +4 -0
- package/src/tests/views/discussions.test.js +519 -0
- package/src/views/resources/_shared/_history_toolbar.ejs +10 -0
- package/src/views/resources/_shared/_tabs.ejs +21 -0
- package/src/views/resources/_shared/_toolbar.ejs +1 -1
- package/src/views/resources/discussions/_avatar.ejs +13 -0
- package/src/views/resources/discussions/_discussion_tabs.ejs +22 -0
- package/src/views/resources/discussions/index.ejs +729 -0
- package/src/views/resources/show.ejs +17 -0
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import Comments from "../../../../cloud/Comments";
|
|
2
|
+
import Editors from "../../../../cloud/Editors";
|
|
3
|
+
import ResourceRevisions from "../../../../cloud/ResourceRevisions";
|
|
4
|
+
import Resources from "../../../../cloud/Resources";
|
|
5
|
+
import DiscussionsHandler from "../../../../handlers/resources/DiscussionsHandler";
|
|
6
|
+
import Factory from "../../../support/factory";
|
|
7
|
+
|
|
8
|
+
describe("Resources Discussions Handler index", () => {
|
|
9
|
+
const editor = {
|
|
10
|
+
id: "5",
|
|
11
|
+
name: "Jane Doe",
|
|
12
|
+
email: "jane@example.com",
|
|
13
|
+
avatar: "https://example.com/jane.png",
|
|
14
|
+
};
|
|
15
|
+
const editors = [
|
|
16
|
+
editor,
|
|
17
|
+
{ id: "6", name: "John Smith", email: "john@example.com", avatar: "" },
|
|
18
|
+
];
|
|
19
|
+
const revisions = [
|
|
20
|
+
{ id: "200", title: "Homepage", meta: { title: "Hero update" } },
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
function buildCtx(params = {}) {
|
|
24
|
+
const ctx = {
|
|
25
|
+
client: {
|
|
26
|
+
resources: new Resources(() => ({}), "token"),
|
|
27
|
+
comments: new Comments(() => ({}), "token"),
|
|
28
|
+
editors: new Editors(() => ({}), "token"),
|
|
29
|
+
resourceRevisions: new ResourceRevisions(() => ({}), "token"),
|
|
30
|
+
},
|
|
31
|
+
authentication: {
|
|
32
|
+
user: { name: "Jane", email: "jane@example.com", avatar: "" },
|
|
33
|
+
},
|
|
34
|
+
query: { page: "2" },
|
|
35
|
+
locale: "en",
|
|
36
|
+
locales: { en: {}, de: {}, fr: {} },
|
|
37
|
+
params: { parent_id: "123", ...params },
|
|
38
|
+
resourceConfig: {
|
|
39
|
+
resourceType: "article",
|
|
40
|
+
resourceTypePlural: "articles",
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
jest.spyOn(ctx.client.resources, "find").mockImplementation(() => ({
|
|
45
|
+
...Factory.build("resource"),
|
|
46
|
+
translations: { en: { id: "123" }, de: { id: "456" } },
|
|
47
|
+
}));
|
|
48
|
+
jest
|
|
49
|
+
.spyOn(ctx.client.comments, "list")
|
|
50
|
+
.mockImplementation(() => ({ list: [], pagination: {} }));
|
|
51
|
+
jest.spyOn(ctx.client.editors, "current").mockResolvedValue(editor);
|
|
52
|
+
jest.spyOn(ctx.client.editors, "website").mockResolvedValue(editors);
|
|
53
|
+
jest
|
|
54
|
+
.spyOn(ctx.client.resourceRevisions, "all")
|
|
55
|
+
.mockResolvedValue({ list: revisions });
|
|
56
|
+
|
|
57
|
+
return ctx;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
test("returns the general discussion of the resource", async () => {
|
|
61
|
+
const ctx = buildCtx();
|
|
62
|
+
const comments = { list: [{ id: "1", body: "Hello" }], pagination: {} };
|
|
63
|
+
ctx.client.comments.list.mockImplementation(() => comments);
|
|
64
|
+
|
|
65
|
+
const result = await DiscussionsHandler.index(ctx);
|
|
66
|
+
|
|
67
|
+
expect(ctx.client.resources.find).toHaveBeenCalledWith(
|
|
68
|
+
"123",
|
|
69
|
+
"en",
|
|
70
|
+
"article",
|
|
71
|
+
);
|
|
72
|
+
expect(ctx.client.comments.list).toHaveBeenCalledWith(
|
|
73
|
+
{ page: "2" },
|
|
74
|
+
"123",
|
|
75
|
+
"en",
|
|
76
|
+
"article",
|
|
77
|
+
null,
|
|
78
|
+
);
|
|
79
|
+
expect(ctx.client.resourceRevisions.all).toHaveBeenCalledWith(
|
|
80
|
+
"123",
|
|
81
|
+
"en",
|
|
82
|
+
"article",
|
|
83
|
+
);
|
|
84
|
+
expect(result.assigns).toEqual({
|
|
85
|
+
resource: expect.objectContaining({ id: expect.anything() }),
|
|
86
|
+
comments,
|
|
87
|
+
currentEditor: editor,
|
|
88
|
+
editors,
|
|
89
|
+
revisions,
|
|
90
|
+
discussion: {
|
|
91
|
+
revisionId: null,
|
|
92
|
+
revision: null,
|
|
93
|
+
archived: false,
|
|
94
|
+
path: "/en/articles/123/discussions",
|
|
95
|
+
},
|
|
96
|
+
_translations: [
|
|
97
|
+
{ locale: "en", path: "/en/articles/123/discussions" },
|
|
98
|
+
{ locale: "de", path: "/de/articles/456/discussions" },
|
|
99
|
+
],
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("returns the discussion of an open revision", async () => {
|
|
104
|
+
const ctx = buildCtx({ revision_id: "200" });
|
|
105
|
+
|
|
106
|
+
const result = await DiscussionsHandler.index(ctx);
|
|
107
|
+
|
|
108
|
+
expect(ctx.client.comments.list).toHaveBeenCalledWith(
|
|
109
|
+
{ page: "2" },
|
|
110
|
+
"123",
|
|
111
|
+
"en",
|
|
112
|
+
"article",
|
|
113
|
+
"200",
|
|
114
|
+
);
|
|
115
|
+
expect(result.assigns.discussion).toEqual({
|
|
116
|
+
revisionId: "200",
|
|
117
|
+
revision: revisions[0],
|
|
118
|
+
archived: false,
|
|
119
|
+
path: "/en/articles/123/discussions/200",
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test("returns the discussion of a published revision as archived", async () => {
|
|
124
|
+
const ctx = buildCtx({ revision_id: "150" });
|
|
125
|
+
|
|
126
|
+
const result = await DiscussionsHandler.index(ctx);
|
|
127
|
+
|
|
128
|
+
expect(result.assigns.discussion).toEqual({
|
|
129
|
+
revisionId: "150",
|
|
130
|
+
revision: null,
|
|
131
|
+
archived: true,
|
|
132
|
+
path: "/en/articles/123/discussions/150",
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test("handles resources without revisions", async () => {
|
|
137
|
+
const ctx = buildCtx({ revision_id: "150" });
|
|
138
|
+
ctx.client.resourceRevisions.all.mockResolvedValue(null);
|
|
139
|
+
|
|
140
|
+
const result = await DiscussionsHandler.index(ctx);
|
|
141
|
+
|
|
142
|
+
expect(result.assigns.revisions).toEqual([]);
|
|
143
|
+
expect(result.assigns.discussion.archived).toBe(true);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test("throws for invalid revision ids", async () => {
|
|
147
|
+
const ctx = buildCtx({ revision_id: "abc" });
|
|
148
|
+
|
|
149
|
+
await expect(DiscussionsHandler.index(ctx)).rejects.toThrow(
|
|
150
|
+
"Missing discussion for revision #abc",
|
|
151
|
+
);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
test("falls back to the login token user when the editor cannot be fetched", async () => {
|
|
155
|
+
const ctx = buildCtx();
|
|
156
|
+
const logError = jest.spyOn(console, "error").mockImplementation(() => {});
|
|
157
|
+
ctx.client.editors.current.mockRejectedValue(new Error("Network error"));
|
|
158
|
+
|
|
159
|
+
const result = await DiscussionsHandler.index(ctx);
|
|
160
|
+
|
|
161
|
+
expect(result.assigns.currentEditor).toEqual(ctx.authentication.user);
|
|
162
|
+
expect(logError).toHaveBeenCalled();
|
|
163
|
+
logError.mockRestore();
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test("returns no editors to mention when they cannot be fetched", async () => {
|
|
167
|
+
const ctx = buildCtx();
|
|
168
|
+
const logError = jest.spyOn(console, "error").mockImplementation(() => {});
|
|
169
|
+
ctx.client.editors.website.mockRejectedValue(new Error("Network error"));
|
|
170
|
+
|
|
171
|
+
const result = await DiscussionsHandler.index(ctx);
|
|
172
|
+
|
|
173
|
+
expect(result.assigns.editors).toEqual([]);
|
|
174
|
+
expect(logError).toHaveBeenCalled();
|
|
175
|
+
logError.mockRestore();
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test("defaults the current editor when there is no user", async () => {
|
|
179
|
+
const ctx = { ...buildCtx(), authentication: {} };
|
|
180
|
+
ctx.client.editors.current.mockResolvedValue(null);
|
|
181
|
+
|
|
182
|
+
const result = await DiscussionsHandler.index(ctx);
|
|
183
|
+
|
|
184
|
+
expect(result.assigns.currentEditor).toEqual({});
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
test("throws error when resource is not found", async () => {
|
|
188
|
+
const ctx = buildCtx();
|
|
189
|
+
ctx.client.resources.find.mockImplementation(() => null);
|
|
190
|
+
|
|
191
|
+
await expect(DiscussionsHandler.index(ctx)).rejects.toThrow(
|
|
192
|
+
"Missing article #123",
|
|
193
|
+
);
|
|
194
|
+
});
|
|
195
|
+
});
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import Comments from "../../../../cloud/Comments";
|
|
2
|
+
import DiscussionsHandler from "../../../../handlers/resources/DiscussionsHandler";
|
|
3
|
+
|
|
4
|
+
describe("Resources Discussions Handler update", () => {
|
|
5
|
+
function buildCtx(comment) {
|
|
6
|
+
return {
|
|
7
|
+
client: {
|
|
8
|
+
comments: new Comments(() => ({}), "token"),
|
|
9
|
+
},
|
|
10
|
+
resourceParams: { comment },
|
|
11
|
+
locale: "en",
|
|
12
|
+
params: { id: "9", parent_id: "123" },
|
|
13
|
+
resourceConfig: {
|
|
14
|
+
resourceType: "article",
|
|
15
|
+
resourceTypePlural: "articles",
|
|
16
|
+
},
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
test("updates the comment and redirects back to the discussions page", async () => {
|
|
21
|
+
const ctx = buildCtx({ body: " Edited " });
|
|
22
|
+
const update = jest
|
|
23
|
+
.spyOn(ctx.client.comments, "update")
|
|
24
|
+
.mockImplementation(() => ({ comment: { id: "9" }, errors: null }));
|
|
25
|
+
|
|
26
|
+
const result = await DiscussionsHandler.update(ctx);
|
|
27
|
+
|
|
28
|
+
expect(update).toHaveBeenCalledWith("9", { body: "Edited" });
|
|
29
|
+
expect(result).toEqual({
|
|
30
|
+
headers: { "HX-Redirect": "/en/articles/123/discussions" },
|
|
31
|
+
json: {},
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("redirects back to the revision discussion", async () => {
|
|
36
|
+
const ctx = buildCtx({ body: "Edited" });
|
|
37
|
+
ctx.resourceParams.revision_id = "200";
|
|
38
|
+
jest
|
|
39
|
+
.spyOn(ctx.client.comments, "update")
|
|
40
|
+
.mockImplementation(() => ({ comment: { id: "9" }, errors: null }));
|
|
41
|
+
|
|
42
|
+
const result = await DiscussionsHandler.update(ctx);
|
|
43
|
+
|
|
44
|
+
expect(result.headers["HX-Redirect"]).toEqual(
|
|
45
|
+
"/en/articles/123/discussions/200",
|
|
46
|
+
);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("redirects with an error for a blank body", async () => {
|
|
50
|
+
const ctx = buildCtx({ body: " " });
|
|
51
|
+
const update = jest.spyOn(ctx.client.comments, "update");
|
|
52
|
+
|
|
53
|
+
const result = await DiscussionsHandler.update(ctx);
|
|
54
|
+
|
|
55
|
+
expect(update).not.toHaveBeenCalled();
|
|
56
|
+
expect(result.headers["HX-Redirect"]).toEqual(
|
|
57
|
+
`/en/articles/123/discussions?comment_error=${encodeURIComponent("Comment can't be blank.")}`,
|
|
58
|
+
);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test("redirects with an error when there are no params", async () => {
|
|
62
|
+
const ctx = { ...buildCtx(), resourceParams: {} };
|
|
63
|
+
|
|
64
|
+
const result = await DiscussionsHandler.update(ctx);
|
|
65
|
+
|
|
66
|
+
expect(result.headers["HX-Redirect"]).toContain("comment_error=");
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test("redirects with the API errors", async () => {
|
|
70
|
+
const ctx = buildCtx({ body: "Edited" });
|
|
71
|
+
jest.spyOn(ctx.client.comments, "update").mockImplementation(() => ({
|
|
72
|
+
comment: null,
|
|
73
|
+
errors: [
|
|
74
|
+
{
|
|
75
|
+
key: "generalError",
|
|
76
|
+
message: "Comments of published revisions cannot be changed",
|
|
77
|
+
},
|
|
78
|
+
],
|
|
79
|
+
}));
|
|
80
|
+
|
|
81
|
+
const result = await DiscussionsHandler.update(ctx);
|
|
82
|
+
|
|
83
|
+
expect(result.headers["HX-Redirect"]).toEqual(
|
|
84
|
+
`/en/articles/123/discussions?comment_error=${encodeURIComponent("Comments of published revisions cannot be changed")}`,
|
|
85
|
+
);
|
|
86
|
+
});
|
|
87
|
+
});
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import Comments from "../../../../cloud/Comments";
|
|
1
2
|
import ResourceChanges from "../../../../cloud/ResourceChanges";
|
|
2
3
|
import Resources from "../../../../cloud/Resources";
|
|
3
4
|
import HistoryHandler from "../../../../handlers/resources/HistoryHandler";
|
|
@@ -9,6 +10,7 @@ describe("Resources History Handler", () => {
|
|
|
9
10
|
client: {
|
|
10
11
|
resources: new Resources(() => ({}), "token"),
|
|
11
12
|
resourceChanges: new ResourceChanges(() => ({}), "token"),
|
|
13
|
+
comments: new Comments(() => ({}), "token"),
|
|
12
14
|
},
|
|
13
15
|
query: { foo: "bar" },
|
|
14
16
|
locale: "en",
|
|
@@ -17,6 +19,7 @@ describe("Resources History Handler", () => {
|
|
|
17
19
|
},
|
|
18
20
|
resourceConfig: {
|
|
19
21
|
resourceType: "article",
|
|
22
|
+
comments: true,
|
|
20
23
|
},
|
|
21
24
|
};
|
|
22
25
|
}
|
|
@@ -40,13 +43,19 @@ describe("Resources History Handler", () => {
|
|
|
40
43
|
.spyOn(ctx.client.resourceChanges, "all_editors")
|
|
41
44
|
.mockImplementation(() => ["John Travolta", "Generic Jane Smith"]);
|
|
42
45
|
|
|
46
|
+
const counts = jest
|
|
47
|
+
.spyOn(ctx.client.comments, "counts")
|
|
48
|
+
.mockResolvedValue({ 7: 2 });
|
|
49
|
+
|
|
43
50
|
const result = await HistoryHandler.index(ctx);
|
|
44
51
|
|
|
52
|
+
expect(counts).toHaveBeenCalledWith("123", "en", "article");
|
|
45
53
|
expect(result).toEqual({
|
|
46
54
|
assigns: {
|
|
47
55
|
resource,
|
|
48
56
|
changes,
|
|
49
57
|
editors: ["John Travolta", "Generic Jane Smith"],
|
|
58
|
+
commentCounts: { 7: 2 },
|
|
50
59
|
},
|
|
51
60
|
});
|
|
52
61
|
});
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import Comments from "../../../../cloud/Comments";
|
|
1
2
|
import ResourceChanges from "../../../../cloud/ResourceChanges";
|
|
2
3
|
import ResourceRevisions from "../../../../cloud/ResourceRevisions";
|
|
3
4
|
import Resources from "../../../../cloud/Resources";
|
|
@@ -12,6 +13,7 @@ describe("Resources Handler", () => {
|
|
|
12
13
|
resources: new Resources(() => ({}), "token"),
|
|
13
14
|
resourceChanges: new ResourceChanges(() => ({}), "token"),
|
|
14
15
|
resourceRevisions: new ResourceRevisions(() => ({}), "token"),
|
|
16
|
+
comments: new Comments(() => ({}), "token"),
|
|
15
17
|
},
|
|
16
18
|
params: { id: resource.id },
|
|
17
19
|
locale: "en",
|
|
@@ -35,6 +37,7 @@ describe("Resources Handler", () => {
|
|
|
35
37
|
resourceConfig: {
|
|
36
38
|
resourceType: "article",
|
|
37
39
|
resourceTypePlural: "articles",
|
|
40
|
+
comments: true,
|
|
38
41
|
},
|
|
39
42
|
};
|
|
40
43
|
}
|
|
@@ -64,8 +67,14 @@ describe("Resources Handler", () => {
|
|
|
64
67
|
.spyOn(ctx.client.resourceChanges, "list")
|
|
65
68
|
.mockImplementation(() => ({ list: [change] }));
|
|
66
69
|
|
|
70
|
+
const mockedCounts = jest
|
|
71
|
+
.spyOn(ctx.client.comments, "counts")
|
|
72
|
+
.mockResolvedValue({ [revision.id]: 3 });
|
|
73
|
+
|
|
67
74
|
const result = await ResourcesHandler.show(ctx);
|
|
68
75
|
|
|
76
|
+
expect(mockedCounts).toHaveBeenCalledWith(resource.id, "en", "article");
|
|
77
|
+
|
|
69
78
|
expect(mockedFind).toHaveBeenCalledWith(resource.id, "en", "article");
|
|
70
79
|
expect(mockedRevisions).toHaveBeenCalledWith(resource.id, "en", "article");
|
|
71
80
|
expect(mockedChanges).toHaveBeenCalledWith(
|
|
@@ -94,6 +103,7 @@ describe("Resources Handler", () => {
|
|
|
94
103
|
vitalsMeta: [],
|
|
95
104
|
},
|
|
96
105
|
revisions: [revision],
|
|
106
|
+
commentCounts: { [revision.id]: 3 },
|
|
97
107
|
changes: { list: [change] },
|
|
98
108
|
_translations: [
|
|
99
109
|
{ locale: "bg", path: "/bg/articles/69" },
|
|
@@ -103,6 +113,29 @@ describe("Resources Handler", () => {
|
|
|
103
113
|
});
|
|
104
114
|
});
|
|
105
115
|
|
|
116
|
+
test("show renders without the comment counts when they fail", async () => {
|
|
117
|
+
const resource = Factory.build("resource", { translations: {} });
|
|
118
|
+
const ctx = buildCtx(resource);
|
|
119
|
+
const logError = jest.spyOn(console, "error").mockImplementation(() => {});
|
|
120
|
+
|
|
121
|
+
jest.spyOn(ctx.client.resources, "find").mockImplementation(() => resource);
|
|
122
|
+
jest
|
|
123
|
+
.spyOn(ctx.client.resourceRevisions, "all")
|
|
124
|
+
.mockImplementation(() => ({ list: [] }));
|
|
125
|
+
jest
|
|
126
|
+
.spyOn(ctx.client.resourceChanges, "list")
|
|
127
|
+
.mockImplementation(() => ({ list: [] }));
|
|
128
|
+
jest
|
|
129
|
+
.spyOn(ctx.client.comments, "counts")
|
|
130
|
+
.mockRejectedValue(new Error("Unknown field"));
|
|
131
|
+
|
|
132
|
+
const result = await ResourcesHandler.show(ctx);
|
|
133
|
+
|
|
134
|
+
expect(result.assigns.commentCounts).toEqual({});
|
|
135
|
+
expect(logError).toHaveBeenCalled();
|
|
136
|
+
logError.mockRestore();
|
|
137
|
+
});
|
|
138
|
+
|
|
106
139
|
test("show for non existing resource", async () => {
|
|
107
140
|
const ctx = buildCtx({ id: "42" });
|
|
108
141
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import Comments from "../../../../cloud/Comments";
|
|
1
2
|
import ResourceChanges from "../../../../cloud/ResourceChanges";
|
|
2
3
|
import ResourceRevisions from "../../../../cloud/ResourceRevisions";
|
|
3
4
|
import Resources from "../../../../cloud/Resources";
|
|
@@ -19,6 +20,7 @@ describe("Resources Handler show reports section", () => {
|
|
|
19
20
|
resources: new Resources(() => ({}), "token"),
|
|
20
21
|
resourceChanges: new ResourceChanges(() => ({}), "token"),
|
|
21
22
|
resourceRevisions: new ResourceRevisions(() => ({}), "token"),
|
|
23
|
+
comments: new Comments(() => ({}), "token"),
|
|
22
24
|
},
|
|
23
25
|
params: { id: resource.id },
|
|
24
26
|
locale: "en",
|
|
@@ -34,6 +36,7 @@ describe("Resources Handler show reports section", () => {
|
|
|
34
36
|
|
|
35
37
|
function mockClient(ctx, resource) {
|
|
36
38
|
jest.spyOn(ctx.client.resources, "find").mockImplementation(() => resource);
|
|
39
|
+
jest.spyOn(ctx.client.comments, "counts").mockResolvedValue({});
|
|
37
40
|
jest
|
|
38
41
|
.spyOn(ctx.client.resourceRevisions, "all")
|
|
39
42
|
.mockImplementation(() => ({ list: [] }));
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import commentMarkdown, {
|
|
2
|
+
commentMentionIds,
|
|
3
|
+
commentMentionsText,
|
|
4
|
+
} from "../../helpers/commentMarkdown";
|
|
5
|
+
|
|
6
|
+
describe("commentMarkdown", () => {
|
|
7
|
+
test("renders bold and italics", () => {
|
|
8
|
+
expect(commentMarkdown("**bold** *italic* _also italic_")).toEqual(
|
|
9
|
+
"<p><strong>bold</strong> <em>italic</em> <em>also italic</em></p>\n",
|
|
10
|
+
);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
test("renders unordered and ordered lists", () => {
|
|
14
|
+
const rendered = commentMarkdown("- one\n- two\n\n1. first\n2. second");
|
|
15
|
+
|
|
16
|
+
expect(rendered).toContain("<ul>\n<li>one</li>\n<li>two</li>\n</ul>");
|
|
17
|
+
expect(rendered).toContain("<ol>\n<li>first</li>\n<li>second</li>\n</ol>");
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test("renders quotes", () => {
|
|
21
|
+
expect(commentMarkdown("> quoted\n\nafter")).toEqual(
|
|
22
|
+
"<blockquote>\n<p>quoted</p>\n</blockquote>\n<p>after</p>\n",
|
|
23
|
+
);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test("keeps single line breaks", () => {
|
|
27
|
+
expect(commentMarkdown("first\nsecond")).toEqual(
|
|
28
|
+
"<p>first<br>\nsecond</p>\n",
|
|
29
|
+
);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test("turns URLs and domains into links opening in a new tab", () => {
|
|
33
|
+
const rendered = commentMarkdown(
|
|
34
|
+
"see https://raketa.cloud, www.example.io and example.com",
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
expect(rendered).toContain(
|
|
38
|
+
'<a href="https://raketa.cloud" target="_blank" rel="noopener noreferrer">https://raketa.cloud</a>',
|
|
39
|
+
);
|
|
40
|
+
expect(rendered).toContain(
|
|
41
|
+
'<a href="http://www.example.io" target="_blank" rel="noopener noreferrer">www.example.io</a>',
|
|
42
|
+
);
|
|
43
|
+
expect(rendered).toContain(
|
|
44
|
+
'<a href="http://example.com" target="_blank" rel="noopener noreferrer">example.com</a>',
|
|
45
|
+
);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test("links bare domains with newer TLDs and any URL with a protocol", () => {
|
|
49
|
+
const rendered = commentMarkdown(
|
|
50
|
+
"raketa.cloud, raketa.design and https://example.unknowntld",
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
expect(rendered).toContain('href="http://raketa.cloud"');
|
|
54
|
+
expect(rendered).toContain('href="http://raketa.design"');
|
|
55
|
+
expect(rendered).toContain('href="https://example.unknowntld"');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("does not link emails or file names", () => {
|
|
59
|
+
expect(commentMarkdown("mail me@example.com about file.js")).toEqual(
|
|
60
|
+
"<p>mail me@example.com about file.js</p>\n",
|
|
61
|
+
);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("keeps the markdown link syntax as text", () => {
|
|
65
|
+
expect(commentMarkdown("[click](javascript:alert(1))")).toEqual(
|
|
66
|
+
"<p>[click](javascript:alert(1))</p>\n",
|
|
67
|
+
);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("escapes HTML", () => {
|
|
71
|
+
expect(
|
|
72
|
+
commentMarkdown('<script>alert(1)</script> <img src=x onerror="x">'),
|
|
73
|
+
).toEqual(
|
|
74
|
+
"<p><script>alert(1)</script> <img src=x onerror="x"></p>\n",
|
|
75
|
+
);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("leaves other markdown as text", () => {
|
|
79
|
+
expect(commentMarkdown("# heading\n`code` ")).toEqual(
|
|
80
|
+
"<p># heading<br>\n`code` </p>\n",
|
|
81
|
+
);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("handles empty bodies", () => {
|
|
85
|
+
expect(commentMarkdown(null)).toEqual("");
|
|
86
|
+
expect(commentMarkdown("")).toEqual("");
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
describe("mentions", () => {
|
|
90
|
+
const mentions = [
|
|
91
|
+
{ editorId: "12", name: "Jane Doe", email: "jane@example.com" },
|
|
92
|
+
{ editorId: "13", name: " ", email: "ops@example.com" },
|
|
93
|
+
{ editorId: "14", name: "<b>Bold</b>", email: "bold@example.com" },
|
|
94
|
+
];
|
|
95
|
+
const mention = (name) => `<span class="comment-mention">@${name}</span>`;
|
|
96
|
+
const unknown =
|
|
97
|
+
'<span class="comment-mention-unknown">@Unknown editor</span>';
|
|
98
|
+
|
|
99
|
+
test("renders the mentions with the names of the editors", () => {
|
|
100
|
+
expect(
|
|
101
|
+
commentMarkdown("Hey @editor-12 and @editor-13!", { mentions }),
|
|
102
|
+
).toEqual(
|
|
103
|
+
`<p>Hey ${mention("Jane Doe")} and ${mention("ops@example.com")}!</p>\n`,
|
|
104
|
+
);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test("renders mentions inside formatting and lists", () => {
|
|
108
|
+
const rendered = commentMarkdown("**@editor-12**\n\n- @editor-12", {
|
|
109
|
+
mentions,
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
expect(rendered).toContain(`<strong>${mention("Jane Doe")}</strong>`);
|
|
113
|
+
expect(rendered).toContain(`<li>${mention("Jane Doe")}</li>`);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test("renders unknown editors", () => {
|
|
117
|
+
expect(commentMarkdown("cc @editor-99", { mentions })).toEqual(
|
|
118
|
+
`<p>cc ${unknown}</p>\n`,
|
|
119
|
+
);
|
|
120
|
+
expect(commentMarkdown("@editor-12")).toEqual(`<p>${unknown}</p>\n`);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test("ignores tokens inside words, emails and paths", () => {
|
|
124
|
+
expect(
|
|
125
|
+
commentMarkdown("mail@editor-12 x/@editor-12 @editor-12abc @Jane", {
|
|
126
|
+
mentions,
|
|
127
|
+
}),
|
|
128
|
+
).toEqual("<p>mail@editor-12 x/@editor-12 @editor-12abc @Jane</p>\n");
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test("escapes the names and the text", () => {
|
|
132
|
+
expect(commentMarkdown("<i>@editor-14</i>", { mentions })).toEqual(
|
|
133
|
+
`<p><i>${mention("<b>Bold</b>")}</i></p>\n`,
|
|
134
|
+
);
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
describe("commentMentionsText", () => {
|
|
139
|
+
test("writes the known mentions with their names", () => {
|
|
140
|
+
expect(
|
|
141
|
+
commentMentionsText("**@editor-12** @editor-13 @editor-99", [
|
|
142
|
+
{ editorId: "12", name: "Jane Doe" },
|
|
143
|
+
{ editorId: "13", name: "", email: "ops@example.com" },
|
|
144
|
+
]),
|
|
145
|
+
).toEqual("**@Jane Doe** @ops@example.com @editor-99");
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test("handles missing text and mentions", () => {
|
|
149
|
+
expect(commentMentionsText(null)).toEqual("");
|
|
150
|
+
expect(commentMentionsText("@editor-1", undefined)).toEqual("@editor-1");
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
describe("commentMentionIds", () => {
|
|
155
|
+
test("maps the names to the editor ids", () => {
|
|
156
|
+
expect(
|
|
157
|
+
commentMentionIds([
|
|
158
|
+
{ editorId: 12, name: "Jane Doe" },
|
|
159
|
+
{ editorId: "13", name: null, email: "ops@example.com" },
|
|
160
|
+
{ editorId: "14", name: "", email: "" },
|
|
161
|
+
]),
|
|
162
|
+
).toEqual({ "Jane Doe": "12", "ops@example.com": "13" });
|
|
163
|
+
expect(commentMentionIds()).toEqual({});
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
});
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import Comments from "../../cloud/Comments";
|
|
2
|
+
import fetchCommentCounts from "../../helpers/fetchCommentCounts";
|
|
3
|
+
|
|
4
|
+
describe("fetchCommentCounts", () => {
|
|
5
|
+
function buildCtx() {
|
|
6
|
+
return {
|
|
7
|
+
client: { comments: new Comments(() => ({}), "token") },
|
|
8
|
+
locale: "en",
|
|
9
|
+
resourceConfig: { comments: true },
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
test("returns the comment counts of the resource", async () => {
|
|
14
|
+
const ctx = buildCtx();
|
|
15
|
+
const counts = jest
|
|
16
|
+
.spyOn(ctx.client.comments, "counts")
|
|
17
|
+
.mockResolvedValue({ 7: 2 });
|
|
18
|
+
|
|
19
|
+
expect(await fetchCommentCounts(ctx, "42", "page")).toEqual({ 7: 2 });
|
|
20
|
+
expect(counts).toHaveBeenCalledWith("42", "en", "page");
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test("returns no counts when they cannot be fetched", async () => {
|
|
24
|
+
const ctx = buildCtx();
|
|
25
|
+
const logError = jest.spyOn(console, "error").mockImplementation(() => {});
|
|
26
|
+
jest
|
|
27
|
+
.spyOn(ctx.client.comments, "counts")
|
|
28
|
+
.mockRejectedValue(new Error("Unknown field"));
|
|
29
|
+
|
|
30
|
+
expect(await fetchCommentCounts(ctx, "42", "page")).toEqual({});
|
|
31
|
+
expect(logError).toHaveBeenCalled();
|
|
32
|
+
logError.mockRestore();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("skips the request when comments are disabled", async () => {
|
|
36
|
+
const ctx = { ...buildCtx(), resourceConfig: { comments: false } };
|
|
37
|
+
const counts = jest.spyOn(ctx.client.comments, "counts");
|
|
38
|
+
|
|
39
|
+
expect(await fetchCommentCounts(ctx, "42", "page")).toEqual({});
|
|
40
|
+
expect(counts).not.toHaveBeenCalled();
|
|
41
|
+
});
|
|
42
|
+
});
|
|
@@ -105,3 +105,29 @@ describe("isChangeRevertable", () => {
|
|
|
105
105
|
testReversableForMaxVersion("update", false);
|
|
106
106
|
});
|
|
107
107
|
});
|
|
108
|
+
|
|
109
|
+
describe("hasDiscussion", () => {
|
|
110
|
+
const change = { changeType: "revision_publish", revisionId: 7 };
|
|
111
|
+
|
|
112
|
+
it("is true for published revisions with comments", () => {
|
|
113
|
+
expect(history.hasDiscussion(change, { 7: 2 })).toBe(true);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("is false for published revisions without comments", () => {
|
|
117
|
+
expect(history.hasDiscussion(change, { 8: 2 })).toBe(false);
|
|
118
|
+
expect(history.hasDiscussion(change)).toBe(false);
|
|
119
|
+
expect(history.hasDiscussion(change, null)).toBe(false);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("is false for other changes and changes without a revision", () => {
|
|
123
|
+
expect(
|
|
124
|
+
history.hasDiscussion(
|
|
125
|
+
{ changeType: "revision_delete", revisionId: 7 },
|
|
126
|
+
{ 7: 2 },
|
|
127
|
+
),
|
|
128
|
+
).toBe(false);
|
|
129
|
+
expect(
|
|
130
|
+
history.hasDiscussion({ changeType: "revision_publish" }, { 7: 2 }),
|
|
131
|
+
).toBe(false);
|
|
132
|
+
});
|
|
133
|
+
});
|
|
@@ -111,3 +111,24 @@ describe("feature", () => {
|
|
|
111
111
|
expect(utils.feature("contentTasks", null)).toEqual(false);
|
|
112
112
|
});
|
|
113
113
|
});
|
|
114
|
+
|
|
115
|
+
describe("initials", () => {
|
|
116
|
+
it("uses the first letters of the first two words of the name", () => {
|
|
117
|
+
expect(utils.initials("Jane van Doe", "jane@example.com")).toEqual("JV");
|
|
118
|
+
expect(utils.initials(" todor argirov ")).toEqual("TA");
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("uses a single letter for single word names", () => {
|
|
122
|
+
expect(utils.initials("Jane")).toEqual("J");
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("falls back to the email when the name is blank", () => {
|
|
126
|
+
expect(utils.initials(" ", "jane@example.com")).toEqual("J");
|
|
127
|
+
expect(utils.initials(null, "jane@example.com")).toEqual("J");
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("falls back to a question mark without a name and email", () => {
|
|
131
|
+
expect(utils.initials()).toEqual("?");
|
|
132
|
+
expect(utils.initials("", "")).toEqual("?");
|
|
133
|
+
});
|
|
134
|
+
});
|