@decocms/apps-blog 8.0.0 → 8.1.0-next.0
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/README.md +21 -0
- package/package.json +14 -5
- package/src/README.md +69 -0
- package/src/__tests__/blogRecords.test.ts +270 -0
- package/src/__tests__/categoryTree.test.ts +209 -0
- package/src/__tests__/date.test.ts +134 -0
- package/src/__tests__/handlePosts.test.ts +256 -230
- package/src/__tests__/loaders.test.ts +198 -154
- package/src/__tests__/mod.test.ts +6 -6
- package/src/__tests__/publicationStatus.test.ts +176 -0
- package/src/__tests__/records.test.ts +68 -68
- package/src/__tests__/seoSections.test.ts +245 -0
- package/src/actions/submitRating.ts +36 -0
- package/src/actions/submitReview.ts +95 -0
- package/src/actions/submitView.ts +28 -0
- package/src/client.ts +70 -0
- package/src/core/blogRecords.ts +94 -0
- package/src/core/categoryTree.ts +210 -0
- package/src/core/handlePosts.ts +160 -64
- package/src/core/records.ts +147 -12
- package/src/index.ts +48 -14
- package/src/loaderMap.ts +28 -24
- package/src/loaders/BlogPostItem.ts +15 -5
- package/src/loaders/BlogPostPage.ts +61 -24
- package/src/loaders/BlogRelatedPosts.ts +45 -45
- package/src/loaders/BlogpostList.ts +93 -0
- package/src/loaders/BlogpostListing.ts +151 -85
- package/src/loaders/GetCategories.ts +51 -28
- package/src/loaders/extensions/BlogpostList/ratings.ts +36 -0
- package/src/loaders/extensions/BlogpostList/reviews.ts +36 -0
- package/src/loaders/extensions/BlogpostListing/ratings.ts +38 -0
- package/src/loaders/extensions/BlogpostListing/reviews.ts +38 -0
- package/src/loaders/extensions/BlogpostPage/ratings.ts +24 -0
- package/src/loaders/extensions/BlogpostPage/reviews.ts +31 -0
- package/src/loaders/extensions/types.ts +15 -0
- package/src/manifest.gen.ts +82 -14
- package/src/mod.ts +73 -20
- package/src/registry.ts +5 -5
- package/src/sections/Seo/SeoBlogPost.tsx +77 -0
- package/src/sections/Seo/SeoBlogPostListing.tsx +80 -0
- package/src/sections/Template.tsx +106 -0
- package/src/sections/blocks/BlockImage.tsx +42 -0
- package/src/sections/blocks/Callout.tsx +64 -0
- package/src/sections/blocks/CardGroup.tsx +60 -0
- package/src/sections/blocks/Checklist.tsx +58 -0
- package/src/sections/blocks/Code.tsx +29 -0
- package/src/sections/blocks/Comparison.tsx +77 -0
- package/src/sections/blocks/Cta.tsx +22 -0
- package/src/sections/blocks/Divider.tsx +3 -0
- package/src/sections/blocks/Heading.tsx +28 -0
- package/src/sections/blocks/List.tsx +39 -0
- package/src/sections/blocks/Paragraph.tsx +15 -0
- package/src/sections/blocks/Quote.tsx +32 -0
- package/src/sections/blocks/Stat.tsx +23 -0
- package/src/sections/blocks/StatGroup.tsx +38 -0
- package/src/sections/blocks/Steps.tsx +64 -0
- package/src/sections/blocks/Table.tsx +80 -0
- package/src/sections/blocks/Video.tsx +39 -0
- package/src/static/css.ts +178 -0
- package/src/types.ts +333 -77
- package/src/utils/blocksToSections.ts +175 -0
- package/src/utils/constants.ts +1 -0
- package/src/utils/date.ts +95 -0
- package/src/utils/hardSanitize.ts +106 -0
- package/src/utils/jsonLD.ts +118 -0
- package/src/utils/sanitizeHtml.ts +37 -0
|
@@ -1,285 +1,311 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest";
|
|
2
2
|
import handlePosts, {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
3
|
+
filterPostsByCategory,
|
|
4
|
+
filterPostsBySlugs,
|
|
5
|
+
filterPostsByTerm,
|
|
6
|
+
filterRelatedPosts,
|
|
7
|
+
slicePosts,
|
|
8
|
+
sortPosts,
|
|
9
9
|
} from "../core/handlePosts";
|
|
10
10
|
import type { BlogPost, SortBy } from "../types";
|
|
11
11
|
|
|
12
12
|
function makePost(overrides: Partial<BlogPost> = {}): BlogPost {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
13
|
+
return {
|
|
14
|
+
title: "Test Post",
|
|
15
|
+
slug: "test-post",
|
|
16
|
+
date: "2024-06-01",
|
|
17
|
+
excerpt: "A test post",
|
|
18
|
+
content: "Full content here",
|
|
19
|
+
categories: [{ name: "News", slug: "news" }],
|
|
20
|
+
authors: [{ name: "Author", email: "a@b.com" }],
|
|
21
|
+
...overrides,
|
|
22
|
+
};
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
// ---------------------------------------------------------------------------
|
|
26
26
|
// sortPosts
|
|
27
27
|
// ---------------------------------------------------------------------------
|
|
28
28
|
describe("sortPosts", () => {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
29
|
+
const posts = [
|
|
30
|
+
makePost({ title: "Alpha", slug: "a", date: "2024-01-01" }),
|
|
31
|
+
makePost({ title: "Charlie", slug: "c", date: "2024-03-01" }),
|
|
32
|
+
makePost({ title: "Bravo", slug: "b", date: "2024-02-01" }),
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
it("sorts date_desc — most recent first", async () => {
|
|
36
|
+
const sorted = await sortPosts(posts, "date_desc");
|
|
37
|
+
expect(sorted.map((p) => p.slug)).toEqual(["c", "b", "a"]);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("sorts date_asc — oldest first", async () => {
|
|
41
|
+
const sorted = await sortPosts(posts, "date_asc");
|
|
42
|
+
expect(sorted.map((p) => p.slug)).toEqual(["a", "b", "c"]);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("sorts title_asc", async () => {
|
|
46
|
+
const sorted = await sortPosts(posts, "title_asc");
|
|
47
|
+
// localeCompare(a,b) is negated for asc → Z-A
|
|
48
|
+
expect(sorted.map((p) => p.title)).toEqual(["Charlie", "Bravo", "Alpha"]);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("sorts title_desc", async () => {
|
|
52
|
+
const sorted = await sortPosts(posts, "title_desc");
|
|
53
|
+
// localeCompare(a,b) kept as-is for desc → A-Z
|
|
54
|
+
expect(sorted.map((p) => p.title)).toEqual(["Alpha", "Bravo", "Charlie"]);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("falls back to date for invalid sort field", async () => {
|
|
58
|
+
const sorted = await sortPosts(posts, "invalid_desc" as SortBy);
|
|
59
|
+
expect(sorted.map((p) => p.slug)).toEqual(["c", "b", "a"]);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("falls back to desc for invalid sort order", async () => {
|
|
63
|
+
const sorted = await sortPosts(posts, "date_wrong" as SortBy);
|
|
64
|
+
expect(sorted.map((p) => p.slug)).toEqual(["c", "b", "a"]);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("sorts posts with missing date to end", async () => {
|
|
68
|
+
const withMissing = [
|
|
69
|
+
makePost({ slug: "no-date", date: undefined as unknown as string }),
|
|
70
|
+
makePost({ slug: "has-date", date: "2024-06-01" }),
|
|
71
|
+
];
|
|
72
|
+
const sorted = await sortPosts(withMissing, "date_desc");
|
|
73
|
+
expect(sorted[0].slug).toBe("has-date");
|
|
74
|
+
expect(sorted[1].slug).toBe("no-date");
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("throws on empty array (accesses blogPosts[0])", async () => {
|
|
78
|
+
await expect(sortPosts([], "date_desc")).rejects.toThrow();
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("does not mutate original array", async () => {
|
|
82
|
+
const original = [...posts];
|
|
83
|
+
await sortPosts(posts, "date_asc");
|
|
84
|
+
expect(posts).toEqual(original);
|
|
85
|
+
});
|
|
86
86
|
});
|
|
87
87
|
|
|
88
88
|
// ---------------------------------------------------------------------------
|
|
89
89
|
// filterPostsByCategory
|
|
90
90
|
// ---------------------------------------------------------------------------
|
|
91
91
|
describe("filterPostsByCategory", () => {
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
92
|
+
const posts = [
|
|
93
|
+
makePost({ slug: "a", categories: [{ name: "News", slug: "news" }] }),
|
|
94
|
+
makePost({
|
|
95
|
+
slug: "b",
|
|
96
|
+
categories: [
|
|
97
|
+
{ name: "News", slug: "news" },
|
|
98
|
+
{ name: "Tech", slug: "tech" },
|
|
99
|
+
],
|
|
100
|
+
}),
|
|
101
|
+
makePost({ slug: "c", categories: [{ name: "Tech", slug: "tech" }] }),
|
|
102
|
+
];
|
|
103
|
+
|
|
104
|
+
it("returns all posts when no slug provided", () => {
|
|
105
|
+
expect(filterPostsByCategory(posts)).toHaveLength(3);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("filters by matching slug", () => {
|
|
109
|
+
const result = filterPostsByCategory(posts, "tech");
|
|
110
|
+
expect(result.map((p) => p.slug)).toEqual(["b", "c"]);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("returns empty when no match", () => {
|
|
114
|
+
expect(filterPostsByCategory(posts, "sports")).toEqual([]);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("excludes posts with no categories", () => {
|
|
118
|
+
const withNone = [...posts, makePost({ slug: "d", categories: undefined })];
|
|
119
|
+
expect(filterPostsByCategory(withNone, "news").map((p) => p.slug)).toEqual(["a", "b"]);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("includes post when one of multiple categories matches", () => {
|
|
123
|
+
const result = filterPostsByCategory(posts, "tech");
|
|
124
|
+
expect(result.find((p) => p.slug === "b")).toBeTruthy();
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* A parent category lists its descendants' posts by expanding its slug into
|
|
129
|
+
* the whole subtree, so the filter has to match on any slug of a list.
|
|
130
|
+
*/
|
|
131
|
+
it("matches a post in any slug of a list", () => {
|
|
132
|
+
const inCategory = (slug: string) =>
|
|
133
|
+
makePost({ slug: `post-${slug}`, categories: [{ name: slug, slug }] });
|
|
134
|
+
const tree = [inCategory("root"), inCategory("child"), inCategory("other")];
|
|
135
|
+
|
|
136
|
+
expect(filterPostsByCategory(tree, ["root", "child"]).map((p) => p.slug)).toEqual([
|
|
137
|
+
"post-root",
|
|
138
|
+
"post-child",
|
|
139
|
+
]);
|
|
140
|
+
expect(filterPostsByCategory(tree, "child").map((p) => p.slug)).toEqual(["post-child"]);
|
|
141
|
+
// No slug at all means no category filter, same as before.
|
|
142
|
+
expect(filterPostsByCategory(tree)).toHaveLength(3);
|
|
143
|
+
// An empty list is an explicit empty set of categories, which nothing
|
|
144
|
+
// belongs to — diverges from upstream, which answers every post here.
|
|
145
|
+
expect(filterPostsByCategory(tree, [])).toEqual([]);
|
|
146
|
+
});
|
|
126
147
|
});
|
|
127
148
|
|
|
128
149
|
// ---------------------------------------------------------------------------
|
|
129
150
|
// filterPostsBySlugs
|
|
130
151
|
// ---------------------------------------------------------------------------
|
|
131
152
|
describe("filterPostsBySlugs", () => {
|
|
132
|
-
|
|
153
|
+
const posts = [makePost({ slug: "a" }), makePost({ slug: "b" }), makePost({ slug: "c" })];
|
|
133
154
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
155
|
+
it("filters to only matching slugs", () => {
|
|
156
|
+
expect(filterPostsBySlugs(posts, ["a", "c"]).map((p) => p.slug)).toEqual(["a", "c"]);
|
|
157
|
+
});
|
|
137
158
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
159
|
+
it("returns empty for empty slugs array", () => {
|
|
160
|
+
expect(filterPostsBySlugs(posts, [])).toEqual([]);
|
|
161
|
+
});
|
|
141
162
|
});
|
|
142
163
|
|
|
143
164
|
// ---------------------------------------------------------------------------
|
|
144
165
|
// filterPostsByTerm
|
|
145
166
|
// ---------------------------------------------------------------------------
|
|
146
167
|
describe("filterPostsByTerm", () => {
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
168
|
+
const posts = [
|
|
169
|
+
makePost({ slug: "a", title: "Hello World", excerpt: "intro", content: "body" }),
|
|
170
|
+
makePost({ slug: "b", title: "Goodbye", excerpt: "farewell world", content: "end" }),
|
|
171
|
+
makePost({ slug: "c", title: "Nothing", excerpt: "nope", content: "empty" }),
|
|
172
|
+
];
|
|
173
|
+
|
|
174
|
+
it("matches in title", () => {
|
|
175
|
+
expect(filterPostsByTerm(posts, "Hello").map((p) => p.slug)).toEqual(["a"]);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it("matches in excerpt", () => {
|
|
179
|
+
expect(filterPostsByTerm(posts, "farewell").map((p) => p.slug)).toEqual(["b"]);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it("matches in content", () => {
|
|
183
|
+
expect(filterPostsByTerm(posts, "empty").map((p) => p.slug)).toEqual(["c"]);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it("is case-insensitive", () => {
|
|
187
|
+
expect(filterPostsByTerm(posts, "hELLo")).toHaveLength(1);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it("returns empty when no match", () => {
|
|
191
|
+
expect(filterPostsByTerm(posts, "zzz")).toEqual([]);
|
|
192
|
+
});
|
|
172
193
|
});
|
|
173
194
|
|
|
174
195
|
// ---------------------------------------------------------------------------
|
|
175
196
|
// filterRelatedPosts
|
|
176
197
|
// ---------------------------------------------------------------------------
|
|
177
198
|
describe("filterRelatedPosts", () => {
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
199
|
+
const posts = [
|
|
200
|
+
makePost({ slug: "a", categories: [{ name: "News", slug: "news" }] }),
|
|
201
|
+
makePost({ slug: "b", categories: [{ name: "Tech", slug: "tech" }] }),
|
|
202
|
+
makePost({ slug: "c", categories: undefined }),
|
|
203
|
+
];
|
|
204
|
+
|
|
205
|
+
it("includes post with category overlap", () => {
|
|
206
|
+
expect(filterRelatedPosts(posts, ["news"]).map((p) => p.slug)).toEqual(["a"]);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it("excludes post with no overlap", () => {
|
|
210
|
+
expect(filterRelatedPosts(posts, ["sports"])).toEqual([]);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it("excludes post with no categories", () => {
|
|
214
|
+
const result = filterRelatedPosts(posts, ["news"]);
|
|
215
|
+
expect(result.find((p) => p.slug === "c")).toBeUndefined();
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
// Opposite of filterPostsByCategory, which reads an empty list as "no filter".
|
|
219
|
+
it("relates nothing on an empty slug list", () => {
|
|
220
|
+
expect(filterRelatedPosts(posts, [])).toEqual([]);
|
|
221
|
+
});
|
|
196
222
|
});
|
|
197
223
|
|
|
198
224
|
// ---------------------------------------------------------------------------
|
|
199
225
|
// slicePosts
|
|
200
226
|
// ---------------------------------------------------------------------------
|
|
201
227
|
describe("slicePosts", () => {
|
|
202
|
-
|
|
228
|
+
const posts = Array.from({ length: 5 }, (_, i) => makePost({ slug: `p${i + 1}` }));
|
|
203
229
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
230
|
+
it("page 1 returns first N", () => {
|
|
231
|
+
expect(slicePosts(posts, 1, 2).map((p) => p.slug)).toEqual(["p1", "p2"]);
|
|
232
|
+
});
|
|
207
233
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
234
|
+
it("page 2 returns next N", () => {
|
|
235
|
+
expect(slicePosts(posts, 2, 2).map((p) => p.slug)).toEqual(["p3", "p4"]);
|
|
236
|
+
});
|
|
211
237
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
238
|
+
it("last page with fewer items", () => {
|
|
239
|
+
expect(slicePosts(posts, 3, 2).map((p) => p.slug)).toEqual(["p5"]);
|
|
240
|
+
});
|
|
215
241
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
242
|
+
it("page beyond total returns empty", () => {
|
|
243
|
+
expect(slicePosts(posts, 10, 2)).toEqual([]);
|
|
244
|
+
});
|
|
219
245
|
});
|
|
220
246
|
|
|
221
247
|
// ---------------------------------------------------------------------------
|
|
222
248
|
// handlePosts (composition pipeline)
|
|
223
249
|
// ---------------------------------------------------------------------------
|
|
224
250
|
describe("handlePosts", () => {
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
251
|
+
const posts = [
|
|
252
|
+
makePost({
|
|
253
|
+
slug: "a",
|
|
254
|
+
title: "Alpha",
|
|
255
|
+
date: "2024-01-01",
|
|
256
|
+
categories: [{ name: "News", slug: "news" }],
|
|
257
|
+
}),
|
|
258
|
+
makePost({
|
|
259
|
+
slug: "b",
|
|
260
|
+
title: "Bravo",
|
|
261
|
+
date: "2024-02-01",
|
|
262
|
+
categories: [{ name: "Tech", slug: "tech" }],
|
|
263
|
+
}),
|
|
264
|
+
makePost({
|
|
265
|
+
slug: "c",
|
|
266
|
+
title: "Charlie",
|
|
267
|
+
date: "2024-03-01",
|
|
268
|
+
categories: [
|
|
269
|
+
{ name: "News", slug: "news" },
|
|
270
|
+
{ name: "Tech", slug: "tech" },
|
|
271
|
+
],
|
|
272
|
+
}),
|
|
273
|
+
];
|
|
274
|
+
|
|
275
|
+
it("slug string + postSlugs — uses slug filtering", async () => {
|
|
276
|
+
const result = await handlePosts(posts, "date_desc", "news", ["a", "c"]);
|
|
277
|
+
// sorted by date_desc: c (March) before a (Jan)
|
|
278
|
+
expect(result?.map((p) => p.slug)).toEqual(["c", "a"]);
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it("slug string + term — chains category + term filters", async () => {
|
|
282
|
+
const result = await handlePosts(posts, "date_desc", "news", undefined, "Alpha");
|
|
283
|
+
expect(result?.map((p) => p.slug)).toEqual(["a"]);
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
it("slug as string[] — triggers related posts path", async () => {
|
|
287
|
+
const result = await handlePosts(posts, "date_desc", ["tech"]);
|
|
288
|
+
expect(result?.map((p) => p.slug)).toEqual(["c", "b"]);
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
it("no slug + term — term-only filtering", async () => {
|
|
292
|
+
const result = await handlePosts(posts, "date_asc", undefined, undefined, "Bravo");
|
|
293
|
+
expect(result).toHaveLength(1);
|
|
294
|
+
expect(result![0].slug).toBe("b");
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it("excludePostSlug removes matching post", async () => {
|
|
298
|
+
const result = await handlePosts(posts, "date_desc", undefined, undefined, undefined, "b");
|
|
299
|
+
expect(result?.find((p) => p.slug === "b")).toBeUndefined();
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
it("returns null when result is empty", async () => {
|
|
303
|
+
expect(await handlePosts(posts, "date_desc", "nonexistent")).toBeNull();
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
it("returns all posts sorted when no filters given", async () => {
|
|
307
|
+
const result = await handlePosts(posts, "date_desc");
|
|
308
|
+
expect(result).toHaveLength(3);
|
|
309
|
+
expect(result![0].slug).toBe("c");
|
|
310
|
+
});
|
|
285
311
|
});
|