@mandujs/core 0.12.2 → 0.13.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.ko.md +304 -304
- package/README.md +653 -653
- package/package.json +1 -1
- package/src/brain/architecture/analyzer.ts +28 -26
- package/src/brain/doctor/analyzer.ts +1 -1
- package/src/bundler/build.ts +91 -91
- package/src/bundler/css.ts +302 -302
- package/src/bundler/dev.ts +0 -1
- package/src/change/history.ts +3 -3
- package/src/change/snapshot.ts +10 -9
- package/src/change/transaction.ts +2 -2
- package/src/client/Link.tsx +227 -227
- package/src/client/globals.ts +44 -44
- package/src/client/hooks.ts +267 -267
- package/src/client/index.ts +5 -5
- package/src/client/island.ts +8 -8
- package/src/client/router.ts +435 -435
- package/src/client/runtime.ts +23 -23
- package/src/client/serialize.ts +404 -404
- package/src/client/window-state.ts +101 -101
- package/src/config/mandu.ts +94 -96
- package/src/config/validate.ts +213 -215
- package/src/config/watcher.ts +311 -311
- package/src/constants.ts +40 -40
- package/src/content/content-layer.ts +314 -314
- package/src/content/content.test.ts +433 -433
- package/src/content/data-store.ts +245 -245
- package/src/content/digest.ts +133 -133
- package/src/content/index.ts +164 -164
- package/src/content/loader-context.ts +172 -172
- package/src/content/loaders/api.ts +216 -216
- package/src/content/loaders/file.ts +169 -169
- package/src/content/loaders/glob.ts +252 -252
- package/src/content/loaders/index.ts +34 -34
- package/src/content/loaders/types.ts +137 -137
- package/src/content/meta-store.ts +209 -209
- package/src/content/types.ts +282 -282
- package/src/content/watcher.ts +135 -135
- package/src/contract/client-safe.test.ts +42 -42
- package/src/contract/client-safe.ts +114 -114
- package/src/contract/client.ts +16 -16
- package/src/contract/define.ts +459 -459
- package/src/contract/handler.ts +10 -10
- package/src/contract/normalize.test.ts +276 -276
- package/src/contract/normalize.ts +404 -404
- package/src/contract/registry.test.ts +206 -206
- package/src/contract/registry.ts +568 -568
- package/src/contract/schema.ts +48 -48
- package/src/contract/types.ts +58 -58
- package/src/contract/validator.ts +32 -32
- package/src/devtools/ai/context-builder.ts +375 -375
- package/src/devtools/ai/index.ts +25 -25
- package/src/devtools/ai/mcp-connector.ts +465 -465
- package/src/devtools/client/catchers/error-catcher.ts +327 -327
- package/src/devtools/client/catchers/index.ts +18 -18
- package/src/devtools/client/catchers/network-proxy.ts +363 -363
- package/src/devtools/client/components/index.ts +39 -39
- package/src/devtools/client/components/kitchen-root.tsx +362 -362
- package/src/devtools/client/components/mandu-character.tsx +241 -241
- package/src/devtools/client/components/overlay.tsx +368 -368
- package/src/devtools/client/components/panel/errors-panel.tsx +259 -259
- package/src/devtools/client/components/panel/guard-panel.tsx +244 -244
- package/src/devtools/client/components/panel/index.ts +32 -32
- package/src/devtools/client/components/panel/islands-panel.tsx +304 -304
- package/src/devtools/client/components/panel/network-panel.tsx +292 -292
- package/src/devtools/client/components/panel/panel-container.tsx +259 -259
- package/src/devtools/client/filters/context-filters.ts +282 -282
- package/src/devtools/client/filters/index.ts +16 -16
- package/src/devtools/client/index.ts +63 -63
- package/src/devtools/client/persistence.ts +335 -335
- package/src/devtools/client/state-manager.ts +478 -478
- package/src/devtools/design-tokens.ts +263 -263
- package/src/devtools/hook/create-hook.ts +207 -207
- package/src/devtools/hook/index.ts +13 -13
- package/src/devtools/index.ts +439 -439
- package/src/devtools/init.ts +266 -266
- package/src/devtools/protocol.ts +237 -237
- package/src/devtools/server/index.ts +17 -17
- package/src/devtools/server/source-context.ts +444 -444
- package/src/devtools/types.ts +319 -319
- package/src/devtools/worker/index.ts +25 -25
- package/src/devtools/worker/redaction-worker.ts +222 -222
- package/src/devtools/worker/worker-manager.ts +409 -409
- package/src/error/classifier.ts +2 -2
- package/src/error/domains.ts +265 -265
- package/src/error/formatter.ts +32 -32
- package/src/error/result.ts +46 -46
- package/src/error/stack-analyzer.ts +5 -0
- package/src/error/types.ts +6 -6
- package/src/errors/extractor.ts +409 -409
- package/src/errors/index.ts +19 -19
- package/src/filling/auth.ts +308 -308
- package/src/filling/context.ts +569 -569
- package/src/filling/deps.ts +238 -238
- package/src/generator/contract-glue.ts +2 -1
- package/src/generator/generate.ts +12 -10
- package/src/generator/index.ts +3 -3
- package/src/generator/templates.ts +80 -79
- package/src/guard/analyzer.ts +360 -360
- package/src/guard/ast-analyzer.ts +806 -806
- package/src/guard/auto-correct.ts +1 -1
- package/src/guard/check.ts +128 -128
- package/src/guard/contract-guard.ts +9 -9
- package/src/guard/file-type.test.ts +24 -24
- package/src/guard/presets/atomic.ts +70 -70
- package/src/guard/presets/clean.ts +77 -77
- package/src/guard/presets/cqrs.test.ts +35 -14
- package/src/guard/presets/fsd.ts +79 -79
- package/src/guard/presets/hexagonal.ts +68 -68
- package/src/guard/presets/index.ts +291 -291
- package/src/guard/reporter.ts +445 -445
- package/src/guard/rules.ts +12 -12
- package/src/guard/statistics.ts +578 -578
- package/src/guard/suggestions.ts +358 -358
- package/src/guard/types.ts +348 -348
- package/src/guard/validator.ts +834 -834
- package/src/guard/watcher.ts +404 -404
- package/src/index.ts +1 -0
- package/src/intent/index.ts +310 -310
- package/src/island/index.ts +304 -304
- package/src/logging/index.ts +22 -22
- package/src/logging/transports.ts +365 -365
- package/src/paths.test.ts +47 -0
- package/src/paths.ts +47 -0
- package/src/plugins/index.ts +38 -38
- package/src/plugins/registry.ts +377 -377
- package/src/plugins/types.ts +363 -363
- package/src/report/build.ts +1 -1
- package/src/report/index.ts +1 -1
- package/src/router/fs-patterns.ts +387 -387
- package/src/router/fs-routes.ts +344 -401
- package/src/router/fs-scanner.ts +497 -497
- package/src/router/fs-types.ts +270 -278
- package/src/router/index.ts +81 -81
- package/src/runtime/boundary.tsx +232 -232
- package/src/runtime/compose.ts +222 -222
- package/src/runtime/lifecycle.ts +381 -381
- package/src/runtime/logger.test.ts +345 -345
- package/src/runtime/logger.ts +677 -677
- package/src/runtime/router.test.ts +476 -476
- package/src/runtime/router.ts +105 -105
- package/src/runtime/security.ts +155 -155
- package/src/runtime/server.ts +24 -24
- package/src/runtime/session-key.ts +328 -328
- package/src/runtime/ssr.ts +367 -367
- package/src/runtime/streaming-ssr.ts +1245 -1245
- package/src/runtime/trace.ts +144 -144
- package/src/seo/index.ts +214 -214
- package/src/seo/integration/ssr.ts +307 -307
- package/src/seo/render/basic.ts +427 -427
- package/src/seo/render/index.ts +143 -143
- package/src/seo/render/jsonld.ts +539 -539
- package/src/seo/render/opengraph.ts +191 -191
- package/src/seo/render/robots.ts +116 -116
- package/src/seo/render/sitemap.ts +137 -137
- package/src/seo/render/twitter.ts +126 -126
- package/src/seo/resolve/index.ts +353 -353
- package/src/seo/resolve/opengraph.ts +143 -143
- package/src/seo/resolve/robots.ts +73 -73
- package/src/seo/resolve/title.ts +94 -94
- package/src/seo/resolve/twitter.ts +73 -73
- package/src/seo/resolve/url.ts +97 -97
- package/src/seo/routes/index.ts +290 -290
- package/src/seo/types.ts +575 -575
- package/src/slot/validator.ts +39 -39
- package/src/spec/index.ts +3 -3
- package/src/spec/load.ts +76 -76
- package/src/spec/lock.ts +56 -56
- package/src/utils/bun.ts +8 -8
- package/src/utils/lru-cache.ts +75 -75
- package/src/utils/safe-io.ts +188 -188
- package/src/utils/string-safe.ts +298 -298
- package/src/watcher/rules.ts +5 -5
|
@@ -1,433 +1,433 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Content Layer Tests
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { describe, test, expect, beforeEach, afterEach } from "bun:test";
|
|
6
|
-
import { generateDigest, generateFileDigest, combineDigests, digestsMatch } from "./digest";
|
|
7
|
-
import { createDataStore, type ContentDataStore } from "./data-store";
|
|
8
|
-
import { createMetaStore, type ContentMetaStore } from "./meta-store";
|
|
9
|
-
import { createLoaderContext, createSimpleMarkdownRenderer } from "./loader-context";
|
|
10
|
-
import { z } from "zod";
|
|
11
|
-
import fs from "fs";
|
|
12
|
-
import path from "path";
|
|
13
|
-
import os from "os";
|
|
14
|
-
|
|
15
|
-
// ============================================================================
|
|
16
|
-
// Digest Tests
|
|
17
|
-
// ============================================================================
|
|
18
|
-
|
|
19
|
-
describe("digest", () => {
|
|
20
|
-
test("generateDigest creates consistent hash for same data", () => {
|
|
21
|
-
const data = { title: "Hello", count: 42 };
|
|
22
|
-
const hash1 = generateDigest(data);
|
|
23
|
-
const hash2 = generateDigest(data);
|
|
24
|
-
|
|
25
|
-
expect(hash1).toBe(hash2);
|
|
26
|
-
expect(hash1.length).toBe(16);
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
test("generateDigest creates different hash for different data", () => {
|
|
30
|
-
const hash1 = generateDigest({ a: 1 });
|
|
31
|
-
const hash2 = generateDigest({ a: 2 });
|
|
32
|
-
|
|
33
|
-
expect(hash1).not.toBe(hash2);
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
test("generateDigest handles nested objects", () => {
|
|
37
|
-
const data = {
|
|
38
|
-
user: { name: "John", age: 30 },
|
|
39
|
-
tags: ["a", "b"],
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
const hash = generateDigest(data);
|
|
43
|
-
expect(hash.length).toBe(16);
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
test("generateDigest is order-independent for object keys", () => {
|
|
47
|
-
const hash1 = generateDigest({ a: 1, b: 2 });
|
|
48
|
-
const hash2 = generateDigest({ b: 2, a: 1 });
|
|
49
|
-
|
|
50
|
-
expect(hash1).toBe(hash2);
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
test("generateFileDigest works with string content", () => {
|
|
54
|
-
const content = "# Hello World\n\nThis is content.";
|
|
55
|
-
const hash = generateFileDigest(content);
|
|
56
|
-
|
|
57
|
-
expect(hash.length).toBe(16);
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
test("combineDigests combines multiple digests", () => {
|
|
61
|
-
const d1 = generateDigest({ a: 1 });
|
|
62
|
-
const d2 = generateDigest({ b: 2 });
|
|
63
|
-
const combined = combineDigests([d1, d2]);
|
|
64
|
-
|
|
65
|
-
expect(combined.length).toBe(16);
|
|
66
|
-
expect(combined).not.toBe(d1);
|
|
67
|
-
expect(combined).not.toBe(d2);
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
test("digestsMatch compares digests correctly", () => {
|
|
71
|
-
const d1 = generateDigest({ x: 1 });
|
|
72
|
-
const d2 = generateDigest({ x: 1 });
|
|
73
|
-
const d3 = generateDigest({ x: 2 });
|
|
74
|
-
|
|
75
|
-
expect(digestsMatch(d1, d2)).toBe(true);
|
|
76
|
-
expect(digestsMatch(d1, d3)).toBe(false);
|
|
77
|
-
expect(digestsMatch(undefined, d1)).toBe(false);
|
|
78
|
-
});
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
// ============================================================================
|
|
82
|
-
// DataStore Tests
|
|
83
|
-
// ============================================================================
|
|
84
|
-
|
|
85
|
-
describe("DataStore", () => {
|
|
86
|
-
let store: ContentDataStore;
|
|
87
|
-
let tempDir: string;
|
|
88
|
-
|
|
89
|
-
beforeEach(() => {
|
|
90
|
-
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "mandu-test-"));
|
|
91
|
-
store = createDataStore({
|
|
92
|
-
filePath: path.join(tempDir, "store.json"),
|
|
93
|
-
autoSave: false,
|
|
94
|
-
});
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
afterEach(() => {
|
|
98
|
-
store.dispose();
|
|
99
|
-
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
test("getStore returns a DataStore interface", () => {
|
|
103
|
-
const posts = store.getStore("posts");
|
|
104
|
-
|
|
105
|
-
expect(posts.get).toBeDefined();
|
|
106
|
-
expect(posts.set).toBeDefined();
|
|
107
|
-
expect(posts.delete).toBeDefined();
|
|
108
|
-
expect(posts.entries).toBeDefined();
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
test("set and get work correctly", () => {
|
|
112
|
-
const posts = store.getStore("posts");
|
|
113
|
-
|
|
114
|
-
posts.set({
|
|
115
|
-
id: "hello-world",
|
|
116
|
-
data: { title: "Hello World" },
|
|
117
|
-
digest: "abc123",
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
const entry = posts.get("hello-world");
|
|
121
|
-
expect(entry).toBeDefined();
|
|
122
|
-
expect(entry?.data.title).toBe("Hello World");
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
test("set returns true for new/changed entries", () => {
|
|
126
|
-
const posts = store.getStore("posts");
|
|
127
|
-
|
|
128
|
-
const result1 = posts.set({
|
|
129
|
-
id: "post1",
|
|
130
|
-
data: { title: "Post 1" },
|
|
131
|
-
digest: "digest1",
|
|
132
|
-
});
|
|
133
|
-
expect(result1).toBe(true);
|
|
134
|
-
|
|
135
|
-
// Same digest, no change
|
|
136
|
-
const result2 = posts.set({
|
|
137
|
-
id: "post1",
|
|
138
|
-
data: { title: "Post 1" },
|
|
139
|
-
digest: "digest1",
|
|
140
|
-
});
|
|
141
|
-
expect(result2).toBe(false);
|
|
142
|
-
|
|
143
|
-
// Different digest, changed
|
|
144
|
-
const result3 = posts.set({
|
|
145
|
-
id: "post1",
|
|
146
|
-
data: { title: "Post 1 Updated" },
|
|
147
|
-
digest: "digest2",
|
|
148
|
-
});
|
|
149
|
-
expect(result3).toBe(true);
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
test("delete removes entry", () => {
|
|
153
|
-
const posts = store.getStore("posts");
|
|
154
|
-
|
|
155
|
-
posts.set({ id: "to-delete", data: {}, digest: "x" });
|
|
156
|
-
expect(posts.has("to-delete")).toBe(true);
|
|
157
|
-
|
|
158
|
-
posts.delete("to-delete");
|
|
159
|
-
expect(posts.has("to-delete")).toBe(false);
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
test("entries returns all entries", () => {
|
|
163
|
-
const posts = store.getStore("posts");
|
|
164
|
-
|
|
165
|
-
posts.set({ id: "p1", data: { n: 1 }, digest: "a" });
|
|
166
|
-
posts.set({ id: "p2", data: { n: 2 }, digest: "b" });
|
|
167
|
-
|
|
168
|
-
const entries = posts.entries();
|
|
169
|
-
expect(entries.length).toBe(2);
|
|
170
|
-
expect(entries.map(([id]) => id).sort()).toEqual(["p1", "p2"]);
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
test("size returns entry count", () => {
|
|
174
|
-
const posts = store.getStore("posts");
|
|
175
|
-
|
|
176
|
-
expect(posts.size()).toBe(0);
|
|
177
|
-
|
|
178
|
-
posts.set({ id: "p1", data: {}, digest: "a" });
|
|
179
|
-
posts.set({ id: "p2", data: {}, digest: "b" });
|
|
180
|
-
|
|
181
|
-
expect(posts.size()).toBe(2);
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
test("clear removes all entries", () => {
|
|
185
|
-
const posts = store.getStore("posts");
|
|
186
|
-
|
|
187
|
-
posts.set({ id: "p1", data: {}, digest: "a" });
|
|
188
|
-
posts.set({ id: "p2", data: {}, digest: "b" });
|
|
189
|
-
|
|
190
|
-
posts.clear();
|
|
191
|
-
expect(posts.size()).toBe(0);
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
test("save and load persist data", async () => {
|
|
195
|
-
const posts = store.getStore("posts");
|
|
196
|
-
|
|
197
|
-
posts.set({
|
|
198
|
-
id: "persistent",
|
|
199
|
-
data: { title: "Persistent Post" },
|
|
200
|
-
digest: "persistent-digest",
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
await store.save();
|
|
204
|
-
|
|
205
|
-
// Create new store and load
|
|
206
|
-
const store2 = createDataStore({
|
|
207
|
-
filePath: path.join(tempDir, "store.json"),
|
|
208
|
-
autoSave: false,
|
|
209
|
-
});
|
|
210
|
-
|
|
211
|
-
await store2.load();
|
|
212
|
-
|
|
213
|
-
const posts2 = store2.getStore("posts");
|
|
214
|
-
const entry = posts2.get("persistent");
|
|
215
|
-
|
|
216
|
-
expect(entry).toBeDefined();
|
|
217
|
-
expect(entry?.data.title).toBe("Persistent Post");
|
|
218
|
-
|
|
219
|
-
store2.dispose();
|
|
220
|
-
});
|
|
221
|
-
});
|
|
222
|
-
|
|
223
|
-
// ============================================================================
|
|
224
|
-
// MetaStore Tests
|
|
225
|
-
// ============================================================================
|
|
226
|
-
|
|
227
|
-
describe("MetaStore", () => {
|
|
228
|
-
let metaStore: ContentMetaStore;
|
|
229
|
-
let tempDir: string;
|
|
230
|
-
|
|
231
|
-
beforeEach(() => {
|
|
232
|
-
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "mandu-meta-test-"));
|
|
233
|
-
metaStore = createMetaStore({
|
|
234
|
-
filePath: path.join(tempDir, "meta.json"),
|
|
235
|
-
autoSave: false,
|
|
236
|
-
});
|
|
237
|
-
});
|
|
238
|
-
|
|
239
|
-
afterEach(() => {
|
|
240
|
-
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
241
|
-
});
|
|
242
|
-
|
|
243
|
-
test("getStore returns MetaStore interface", () => {
|
|
244
|
-
const meta = metaStore.getStore("posts");
|
|
245
|
-
|
|
246
|
-
expect(meta.get).toBeDefined();
|
|
247
|
-
expect(meta.set).toBeDefined();
|
|
248
|
-
expect(meta.has).toBeDefined();
|
|
249
|
-
});
|
|
250
|
-
|
|
251
|
-
test("set and get work correctly", () => {
|
|
252
|
-
const meta = metaStore.getStore("api-data");
|
|
253
|
-
|
|
254
|
-
meta.set("lastSync", "2024-01-15T10:00:00Z");
|
|
255
|
-
meta.set("cursor", "abc123");
|
|
256
|
-
|
|
257
|
-
expect(meta.get("lastSync")).toBe("2024-01-15T10:00:00Z");
|
|
258
|
-
expect(meta.get("cursor")).toBe("abc123");
|
|
259
|
-
expect(meta.get("nonexistent")).toBeUndefined();
|
|
260
|
-
});
|
|
261
|
-
|
|
262
|
-
test("has checks existence", () => {
|
|
263
|
-
const meta = metaStore.getStore("collection");
|
|
264
|
-
|
|
265
|
-
expect(meta.has("key")).toBe(false);
|
|
266
|
-
|
|
267
|
-
meta.set("key", "value");
|
|
268
|
-
expect(meta.has("key")).toBe(true);
|
|
269
|
-
});
|
|
270
|
-
|
|
271
|
-
test("delete removes key", () => {
|
|
272
|
-
const meta = metaStore.getStore("collection");
|
|
273
|
-
|
|
274
|
-
meta.set("toDelete", "value");
|
|
275
|
-
expect(meta.has("toDelete")).toBe(true);
|
|
276
|
-
|
|
277
|
-
meta.delete("toDelete");
|
|
278
|
-
expect(meta.has("toDelete")).toBe(false);
|
|
279
|
-
});
|
|
280
|
-
|
|
281
|
-
test("entries returns all key-value pairs", () => {
|
|
282
|
-
const meta = metaStore.getStore("collection");
|
|
283
|
-
|
|
284
|
-
meta.set("k1", "v1");
|
|
285
|
-
meta.set("k2", "v2");
|
|
286
|
-
|
|
287
|
-
const entries = meta.entries();
|
|
288
|
-
expect(entries.length).toBe(2);
|
|
289
|
-
});
|
|
290
|
-
});
|
|
291
|
-
|
|
292
|
-
// ============================================================================
|
|
293
|
-
// LoaderContext Tests
|
|
294
|
-
// ============================================================================
|
|
295
|
-
|
|
296
|
-
describe("LoaderContext", () => {
|
|
297
|
-
let dataStore: ContentDataStore;
|
|
298
|
-
let metaStore: ContentMetaStore;
|
|
299
|
-
let tempDir: string;
|
|
300
|
-
|
|
301
|
-
beforeEach(() => {
|
|
302
|
-
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "mandu-ctx-test-"));
|
|
303
|
-
dataStore = createDataStore({ autoSave: false });
|
|
304
|
-
metaStore = createMetaStore({ autoSave: false });
|
|
305
|
-
});
|
|
306
|
-
|
|
307
|
-
afterEach(() => {
|
|
308
|
-
dataStore.dispose();
|
|
309
|
-
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
310
|
-
});
|
|
311
|
-
|
|
312
|
-
test("creates context with all properties", () => {
|
|
313
|
-
const context = createLoaderContext({
|
|
314
|
-
collection: "posts",
|
|
315
|
-
store: dataStore.getStore("posts"),
|
|
316
|
-
meta: metaStore.getStore("posts"),
|
|
317
|
-
config: { root: tempDir },
|
|
318
|
-
});
|
|
319
|
-
|
|
320
|
-
expect(context.collection).toBe("posts");
|
|
321
|
-
expect(context.store).toBeDefined();
|
|
322
|
-
expect(context.meta).toBeDefined();
|
|
323
|
-
expect(context.logger).toBeDefined();
|
|
324
|
-
expect(context.parseData).toBeDefined();
|
|
325
|
-
expect(context.generateDigest).toBeDefined();
|
|
326
|
-
});
|
|
327
|
-
|
|
328
|
-
test("parseData validates with schema", async () => {
|
|
329
|
-
const schema = z.object({
|
|
330
|
-
title: z.string(),
|
|
331
|
-
count: z.number(),
|
|
332
|
-
});
|
|
333
|
-
|
|
334
|
-
const context = createLoaderContext({
|
|
335
|
-
collection: "posts",
|
|
336
|
-
store: dataStore.getStore("posts"),
|
|
337
|
-
meta: metaStore.getStore("posts"),
|
|
338
|
-
config: { root: tempDir },
|
|
339
|
-
schema,
|
|
340
|
-
});
|
|
341
|
-
|
|
342
|
-
// Valid data
|
|
343
|
-
const valid = await context.parseData({
|
|
344
|
-
id: "test",
|
|
345
|
-
data: { title: "Hello", count: 5 },
|
|
346
|
-
});
|
|
347
|
-
|
|
348
|
-
expect(valid.title).toBe("Hello");
|
|
349
|
-
expect(valid.count).toBe(5);
|
|
350
|
-
|
|
351
|
-
// Invalid data should throw
|
|
352
|
-
await expect(
|
|
353
|
-
context.parseData({
|
|
354
|
-
id: "invalid",
|
|
355
|
-
data: { title: 123, count: "not a number" },
|
|
356
|
-
})
|
|
357
|
-
).rejects.toThrow();
|
|
358
|
-
});
|
|
359
|
-
|
|
360
|
-
test("parseData passes through without schema", async () => {
|
|
361
|
-
const context = createLoaderContext({
|
|
362
|
-
collection: "posts",
|
|
363
|
-
store: dataStore.getStore("posts"),
|
|
364
|
-
meta: metaStore.getStore("posts"),
|
|
365
|
-
config: { root: tempDir },
|
|
366
|
-
});
|
|
367
|
-
|
|
368
|
-
const data = await context.parseData({
|
|
369
|
-
id: "test",
|
|
370
|
-
data: { anything: "goes" },
|
|
371
|
-
});
|
|
372
|
-
|
|
373
|
-
expect(data.anything).toBe("goes");
|
|
374
|
-
});
|
|
375
|
-
|
|
376
|
-
test("generateDigest creates hash", () => {
|
|
377
|
-
const context = createLoaderContext({
|
|
378
|
-
collection: "posts",
|
|
379
|
-
store: dataStore.getStore("posts"),
|
|
380
|
-
meta: metaStore.getStore("posts"),
|
|
381
|
-
config: { root: tempDir },
|
|
382
|
-
});
|
|
383
|
-
|
|
384
|
-
const hash = context.generateDigest({ test: "data" });
|
|
385
|
-
expect(hash.length).toBe(16);
|
|
386
|
-
});
|
|
387
|
-
});
|
|
388
|
-
|
|
389
|
-
// ============================================================================
|
|
390
|
-
// Markdown Renderer Tests
|
|
391
|
-
// ============================================================================
|
|
392
|
-
|
|
393
|
-
describe("SimpleMarkdownRenderer", () => {
|
|
394
|
-
const render = createSimpleMarkdownRenderer();
|
|
395
|
-
|
|
396
|
-
test("renders headings", async () => {
|
|
397
|
-
const result = await render("# Heading 1\n## Heading 2");
|
|
398
|
-
|
|
399
|
-
expect(result.html).toContain("<h1>Heading 1</h1>");
|
|
400
|
-
expect(result.html).toContain("<h2>Heading 2</h2>");
|
|
401
|
-
});
|
|
402
|
-
|
|
403
|
-
test("extracts headings", async () => {
|
|
404
|
-
const result = await render("# Title\n## Section 1\n### Subsection");
|
|
405
|
-
|
|
406
|
-
expect(result.headings).toBeDefined();
|
|
407
|
-
expect(result.headings?.length).toBe(3);
|
|
408
|
-
expect(result.headings?.[0]).toEqual({
|
|
409
|
-
depth: 1,
|
|
410
|
-
text: "Title",
|
|
411
|
-
slug: "title",
|
|
412
|
-
});
|
|
413
|
-
});
|
|
414
|
-
|
|
415
|
-
test("renders bold and italic", async () => {
|
|
416
|
-
const result = await render("**bold** and *italic*");
|
|
417
|
-
|
|
418
|
-
expect(result.html).toContain("<strong>bold</strong>");
|
|
419
|
-
expect(result.html).toContain("<em>italic</em>");
|
|
420
|
-
});
|
|
421
|
-
|
|
422
|
-
test("renders links", async () => {
|
|
423
|
-
const result = await render("[Link Text](https://example.com)");
|
|
424
|
-
|
|
425
|
-
expect(result.html).toContain('<a href="https://example.com">Link Text</a>');
|
|
426
|
-
});
|
|
427
|
-
|
|
428
|
-
test("renders code", async () => {
|
|
429
|
-
const result = await render("`inline code`");
|
|
430
|
-
|
|
431
|
-
expect(result.html).toContain("<code>inline code</code>");
|
|
432
|
-
});
|
|
433
|
-
});
|
|
1
|
+
/**
|
|
2
|
+
* Content Layer Tests
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, test, expect, beforeEach, afterEach } from "bun:test";
|
|
6
|
+
import { generateDigest, generateFileDigest, combineDigests, digestsMatch } from "./digest";
|
|
7
|
+
import { createDataStore, type ContentDataStore } from "./data-store";
|
|
8
|
+
import { createMetaStore, type ContentMetaStore } from "./meta-store";
|
|
9
|
+
import { createLoaderContext, createSimpleMarkdownRenderer } from "./loader-context";
|
|
10
|
+
import { z } from "zod";
|
|
11
|
+
import fs from "fs";
|
|
12
|
+
import path from "path";
|
|
13
|
+
import os from "os";
|
|
14
|
+
|
|
15
|
+
// ============================================================================
|
|
16
|
+
// Digest Tests
|
|
17
|
+
// ============================================================================
|
|
18
|
+
|
|
19
|
+
describe("digest", () => {
|
|
20
|
+
test("generateDigest creates consistent hash for same data", () => {
|
|
21
|
+
const data = { title: "Hello", count: 42 };
|
|
22
|
+
const hash1 = generateDigest(data);
|
|
23
|
+
const hash2 = generateDigest(data);
|
|
24
|
+
|
|
25
|
+
expect(hash1).toBe(hash2);
|
|
26
|
+
expect(hash1.length).toBe(16);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("generateDigest creates different hash for different data", () => {
|
|
30
|
+
const hash1 = generateDigest({ a: 1 });
|
|
31
|
+
const hash2 = generateDigest({ a: 2 });
|
|
32
|
+
|
|
33
|
+
expect(hash1).not.toBe(hash2);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("generateDigest handles nested objects", () => {
|
|
37
|
+
const data = {
|
|
38
|
+
user: { name: "John", age: 30 },
|
|
39
|
+
tags: ["a", "b"],
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const hash = generateDigest(data);
|
|
43
|
+
expect(hash.length).toBe(16);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("generateDigest is order-independent for object keys", () => {
|
|
47
|
+
const hash1 = generateDigest({ a: 1, b: 2 });
|
|
48
|
+
const hash2 = generateDigest({ b: 2, a: 1 });
|
|
49
|
+
|
|
50
|
+
expect(hash1).toBe(hash2);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test("generateFileDigest works with string content", () => {
|
|
54
|
+
const content = "# Hello World\n\nThis is content.";
|
|
55
|
+
const hash = generateFileDigest(content);
|
|
56
|
+
|
|
57
|
+
expect(hash.length).toBe(16);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("combineDigests combines multiple digests", () => {
|
|
61
|
+
const d1 = generateDigest({ a: 1 });
|
|
62
|
+
const d2 = generateDigest({ b: 2 });
|
|
63
|
+
const combined = combineDigests([d1, d2]);
|
|
64
|
+
|
|
65
|
+
expect(combined.length).toBe(16);
|
|
66
|
+
expect(combined).not.toBe(d1);
|
|
67
|
+
expect(combined).not.toBe(d2);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("digestsMatch compares digests correctly", () => {
|
|
71
|
+
const d1 = generateDigest({ x: 1 });
|
|
72
|
+
const d2 = generateDigest({ x: 1 });
|
|
73
|
+
const d3 = generateDigest({ x: 2 });
|
|
74
|
+
|
|
75
|
+
expect(digestsMatch(d1, d2)).toBe(true);
|
|
76
|
+
expect(digestsMatch(d1, d3)).toBe(false);
|
|
77
|
+
expect(digestsMatch(undefined, d1)).toBe(false);
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// ============================================================================
|
|
82
|
+
// DataStore Tests
|
|
83
|
+
// ============================================================================
|
|
84
|
+
|
|
85
|
+
describe("DataStore", () => {
|
|
86
|
+
let store: ContentDataStore;
|
|
87
|
+
let tempDir: string;
|
|
88
|
+
|
|
89
|
+
beforeEach(() => {
|
|
90
|
+
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "mandu-test-"));
|
|
91
|
+
store = createDataStore({
|
|
92
|
+
filePath: path.join(tempDir, "store.json"),
|
|
93
|
+
autoSave: false,
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
afterEach(() => {
|
|
98
|
+
store.dispose();
|
|
99
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test("getStore returns a DataStore interface", () => {
|
|
103
|
+
const posts = store.getStore("posts");
|
|
104
|
+
|
|
105
|
+
expect(posts.get).toBeDefined();
|
|
106
|
+
expect(posts.set).toBeDefined();
|
|
107
|
+
expect(posts.delete).toBeDefined();
|
|
108
|
+
expect(posts.entries).toBeDefined();
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test("set and get work correctly", () => {
|
|
112
|
+
const posts = store.getStore("posts");
|
|
113
|
+
|
|
114
|
+
posts.set({
|
|
115
|
+
id: "hello-world",
|
|
116
|
+
data: { title: "Hello World" },
|
|
117
|
+
digest: "abc123",
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
const entry = posts.get("hello-world");
|
|
121
|
+
expect(entry).toBeDefined();
|
|
122
|
+
expect(entry?.data.title).toBe("Hello World");
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test("set returns true for new/changed entries", () => {
|
|
126
|
+
const posts = store.getStore("posts");
|
|
127
|
+
|
|
128
|
+
const result1 = posts.set({
|
|
129
|
+
id: "post1",
|
|
130
|
+
data: { title: "Post 1" },
|
|
131
|
+
digest: "digest1",
|
|
132
|
+
});
|
|
133
|
+
expect(result1).toBe(true);
|
|
134
|
+
|
|
135
|
+
// Same digest, no change
|
|
136
|
+
const result2 = posts.set({
|
|
137
|
+
id: "post1",
|
|
138
|
+
data: { title: "Post 1" },
|
|
139
|
+
digest: "digest1",
|
|
140
|
+
});
|
|
141
|
+
expect(result2).toBe(false);
|
|
142
|
+
|
|
143
|
+
// Different digest, changed
|
|
144
|
+
const result3 = posts.set({
|
|
145
|
+
id: "post1",
|
|
146
|
+
data: { title: "Post 1 Updated" },
|
|
147
|
+
digest: "digest2",
|
|
148
|
+
});
|
|
149
|
+
expect(result3).toBe(true);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test("delete removes entry", () => {
|
|
153
|
+
const posts = store.getStore("posts");
|
|
154
|
+
|
|
155
|
+
posts.set({ id: "to-delete", data: {}, digest: "x" });
|
|
156
|
+
expect(posts.has("to-delete")).toBe(true);
|
|
157
|
+
|
|
158
|
+
posts.delete("to-delete");
|
|
159
|
+
expect(posts.has("to-delete")).toBe(false);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test("entries returns all entries", () => {
|
|
163
|
+
const posts = store.getStore("posts");
|
|
164
|
+
|
|
165
|
+
posts.set({ id: "p1", data: { n: 1 }, digest: "a" });
|
|
166
|
+
posts.set({ id: "p2", data: { n: 2 }, digest: "b" });
|
|
167
|
+
|
|
168
|
+
const entries = posts.entries();
|
|
169
|
+
expect(entries.length).toBe(2);
|
|
170
|
+
expect(entries.map(([id]) => id).sort()).toEqual(["p1", "p2"]);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
test("size returns entry count", () => {
|
|
174
|
+
const posts = store.getStore("posts");
|
|
175
|
+
|
|
176
|
+
expect(posts.size()).toBe(0);
|
|
177
|
+
|
|
178
|
+
posts.set({ id: "p1", data: {}, digest: "a" });
|
|
179
|
+
posts.set({ id: "p2", data: {}, digest: "b" });
|
|
180
|
+
|
|
181
|
+
expect(posts.size()).toBe(2);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
test("clear removes all entries", () => {
|
|
185
|
+
const posts = store.getStore("posts");
|
|
186
|
+
|
|
187
|
+
posts.set({ id: "p1", data: {}, digest: "a" });
|
|
188
|
+
posts.set({ id: "p2", data: {}, digest: "b" });
|
|
189
|
+
|
|
190
|
+
posts.clear();
|
|
191
|
+
expect(posts.size()).toBe(0);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
test("save and load persist data", async () => {
|
|
195
|
+
const posts = store.getStore("posts");
|
|
196
|
+
|
|
197
|
+
posts.set({
|
|
198
|
+
id: "persistent",
|
|
199
|
+
data: { title: "Persistent Post" },
|
|
200
|
+
digest: "persistent-digest",
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
await store.save();
|
|
204
|
+
|
|
205
|
+
// Create new store and load
|
|
206
|
+
const store2 = createDataStore({
|
|
207
|
+
filePath: path.join(tempDir, "store.json"),
|
|
208
|
+
autoSave: false,
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
await store2.load();
|
|
212
|
+
|
|
213
|
+
const posts2 = store2.getStore("posts");
|
|
214
|
+
const entry = posts2.get("persistent");
|
|
215
|
+
|
|
216
|
+
expect(entry).toBeDefined();
|
|
217
|
+
expect(entry?.data.title).toBe("Persistent Post");
|
|
218
|
+
|
|
219
|
+
store2.dispose();
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
// ============================================================================
|
|
224
|
+
// MetaStore Tests
|
|
225
|
+
// ============================================================================
|
|
226
|
+
|
|
227
|
+
describe("MetaStore", () => {
|
|
228
|
+
let metaStore: ContentMetaStore;
|
|
229
|
+
let tempDir: string;
|
|
230
|
+
|
|
231
|
+
beforeEach(() => {
|
|
232
|
+
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "mandu-meta-test-"));
|
|
233
|
+
metaStore = createMetaStore({
|
|
234
|
+
filePath: path.join(tempDir, "meta.json"),
|
|
235
|
+
autoSave: false,
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
afterEach(() => {
|
|
240
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
test("getStore returns MetaStore interface", () => {
|
|
244
|
+
const meta = metaStore.getStore("posts");
|
|
245
|
+
|
|
246
|
+
expect(meta.get).toBeDefined();
|
|
247
|
+
expect(meta.set).toBeDefined();
|
|
248
|
+
expect(meta.has).toBeDefined();
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
test("set and get work correctly", () => {
|
|
252
|
+
const meta = metaStore.getStore("api-data");
|
|
253
|
+
|
|
254
|
+
meta.set("lastSync", "2024-01-15T10:00:00Z");
|
|
255
|
+
meta.set("cursor", "abc123");
|
|
256
|
+
|
|
257
|
+
expect(meta.get("lastSync")).toBe("2024-01-15T10:00:00Z");
|
|
258
|
+
expect(meta.get("cursor")).toBe("abc123");
|
|
259
|
+
expect(meta.get("nonexistent")).toBeUndefined();
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
test("has checks existence", () => {
|
|
263
|
+
const meta = metaStore.getStore("collection");
|
|
264
|
+
|
|
265
|
+
expect(meta.has("key")).toBe(false);
|
|
266
|
+
|
|
267
|
+
meta.set("key", "value");
|
|
268
|
+
expect(meta.has("key")).toBe(true);
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
test("delete removes key", () => {
|
|
272
|
+
const meta = metaStore.getStore("collection");
|
|
273
|
+
|
|
274
|
+
meta.set("toDelete", "value");
|
|
275
|
+
expect(meta.has("toDelete")).toBe(true);
|
|
276
|
+
|
|
277
|
+
meta.delete("toDelete");
|
|
278
|
+
expect(meta.has("toDelete")).toBe(false);
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
test("entries returns all key-value pairs", () => {
|
|
282
|
+
const meta = metaStore.getStore("collection");
|
|
283
|
+
|
|
284
|
+
meta.set("k1", "v1");
|
|
285
|
+
meta.set("k2", "v2");
|
|
286
|
+
|
|
287
|
+
const entries = meta.entries();
|
|
288
|
+
expect(entries.length).toBe(2);
|
|
289
|
+
});
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
// ============================================================================
|
|
293
|
+
// LoaderContext Tests
|
|
294
|
+
// ============================================================================
|
|
295
|
+
|
|
296
|
+
describe("LoaderContext", () => {
|
|
297
|
+
let dataStore: ContentDataStore;
|
|
298
|
+
let metaStore: ContentMetaStore;
|
|
299
|
+
let tempDir: string;
|
|
300
|
+
|
|
301
|
+
beforeEach(() => {
|
|
302
|
+
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "mandu-ctx-test-"));
|
|
303
|
+
dataStore = createDataStore({ autoSave: false });
|
|
304
|
+
metaStore = createMetaStore({ autoSave: false });
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
afterEach(() => {
|
|
308
|
+
dataStore.dispose();
|
|
309
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
test("creates context with all properties", () => {
|
|
313
|
+
const context = createLoaderContext({
|
|
314
|
+
collection: "posts",
|
|
315
|
+
store: dataStore.getStore("posts"),
|
|
316
|
+
meta: metaStore.getStore("posts"),
|
|
317
|
+
config: { root: tempDir },
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
expect(context.collection).toBe("posts");
|
|
321
|
+
expect(context.store).toBeDefined();
|
|
322
|
+
expect(context.meta).toBeDefined();
|
|
323
|
+
expect(context.logger).toBeDefined();
|
|
324
|
+
expect(context.parseData).toBeDefined();
|
|
325
|
+
expect(context.generateDigest).toBeDefined();
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
test("parseData validates with schema", async () => {
|
|
329
|
+
const schema = z.object({
|
|
330
|
+
title: z.string(),
|
|
331
|
+
count: z.number(),
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
const context = createLoaderContext({
|
|
335
|
+
collection: "posts",
|
|
336
|
+
store: dataStore.getStore("posts"),
|
|
337
|
+
meta: metaStore.getStore("posts"),
|
|
338
|
+
config: { root: tempDir },
|
|
339
|
+
schema,
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
// Valid data
|
|
343
|
+
const valid = await context.parseData({
|
|
344
|
+
id: "test",
|
|
345
|
+
data: { title: "Hello", count: 5 },
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
expect(valid.title).toBe("Hello");
|
|
349
|
+
expect(valid.count).toBe(5);
|
|
350
|
+
|
|
351
|
+
// Invalid data should throw
|
|
352
|
+
await expect(
|
|
353
|
+
context.parseData({
|
|
354
|
+
id: "invalid",
|
|
355
|
+
data: { title: 123, count: "not a number" },
|
|
356
|
+
})
|
|
357
|
+
).rejects.toThrow();
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
test("parseData passes through without schema", async () => {
|
|
361
|
+
const context = createLoaderContext({
|
|
362
|
+
collection: "posts",
|
|
363
|
+
store: dataStore.getStore("posts"),
|
|
364
|
+
meta: metaStore.getStore("posts"),
|
|
365
|
+
config: { root: tempDir },
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
const data = await context.parseData({
|
|
369
|
+
id: "test",
|
|
370
|
+
data: { anything: "goes" },
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
expect(data.anything).toBe("goes");
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
test("generateDigest creates hash", () => {
|
|
377
|
+
const context = createLoaderContext({
|
|
378
|
+
collection: "posts",
|
|
379
|
+
store: dataStore.getStore("posts"),
|
|
380
|
+
meta: metaStore.getStore("posts"),
|
|
381
|
+
config: { root: tempDir },
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
const hash = context.generateDigest({ test: "data" });
|
|
385
|
+
expect(hash.length).toBe(16);
|
|
386
|
+
});
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
// ============================================================================
|
|
390
|
+
// Markdown Renderer Tests
|
|
391
|
+
// ============================================================================
|
|
392
|
+
|
|
393
|
+
describe("SimpleMarkdownRenderer", () => {
|
|
394
|
+
const render = createSimpleMarkdownRenderer();
|
|
395
|
+
|
|
396
|
+
test("renders headings", async () => {
|
|
397
|
+
const result = await render("# Heading 1\n## Heading 2");
|
|
398
|
+
|
|
399
|
+
expect(result.html).toContain("<h1>Heading 1</h1>");
|
|
400
|
+
expect(result.html).toContain("<h2>Heading 2</h2>");
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
test("extracts headings", async () => {
|
|
404
|
+
const result = await render("# Title\n## Section 1\n### Subsection");
|
|
405
|
+
|
|
406
|
+
expect(result.headings).toBeDefined();
|
|
407
|
+
expect(result.headings?.length).toBe(3);
|
|
408
|
+
expect(result.headings?.[0]).toEqual({
|
|
409
|
+
depth: 1,
|
|
410
|
+
text: "Title",
|
|
411
|
+
slug: "title",
|
|
412
|
+
});
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
test("renders bold and italic", async () => {
|
|
416
|
+
const result = await render("**bold** and *italic*");
|
|
417
|
+
|
|
418
|
+
expect(result.html).toContain("<strong>bold</strong>");
|
|
419
|
+
expect(result.html).toContain("<em>italic</em>");
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
test("renders links", async () => {
|
|
423
|
+
const result = await render("[Link Text](https://example.com)");
|
|
424
|
+
|
|
425
|
+
expect(result.html).toContain('<a href="https://example.com">Link Text</a>');
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
test("renders code", async () => {
|
|
429
|
+
const result = await render("`inline code`");
|
|
430
|
+
|
|
431
|
+
expect(result.html).toContain("<code>inline code</code>");
|
|
432
|
+
});
|
|
433
|
+
});
|