contentbase 0.0.1 → 0.0.2
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 +117 -0
- package/bun.lock +2 -2
- package/package.json +4 -3
- package/scripts/examples/01-collection-setup.ts +46 -0
- package/scripts/examples/02-querying.ts +67 -0
- package/scripts/examples/03-sections.ts +36 -0
- package/scripts/examples/04-relationships.ts +54 -0
- package/scripts/examples/05-document-api.ts +54 -0
- package/scripts/examples/06-extract-sections.ts +55 -0
- package/scripts/examples/07-validation.ts +46 -0
- package/scripts/examples/08-serialization.ts +51 -0
- package/scripts/examples/lib/format.ts +87 -0
- package/scripts/examples/lib/setup.ts +21 -0
- package/scripts/examples/run-all.ts +43 -0
- package/src/collection.ts +98 -0
- package/src/define-model.ts +3 -3
- package/src/extract-sections.ts +222 -0
- package/src/index.ts +6 -0
- package/src/types.ts +1 -1
- package/test/extract-sections.test.ts +356 -0
- package/test/section.test.ts +4 -4
- package/test/table-of-contents.test.ts +91 -0
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from "vitest";
|
|
2
|
+
import { toString } from "mdast-util-to-string";
|
|
3
|
+
import type { Heading } from "mdast";
|
|
4
|
+
import { Collection } from "../src/collection";
|
|
5
|
+
import { parse, extractSections } from "../src/index";
|
|
6
|
+
import { createTestCollection, FIXTURES_PATH } from "./helpers";
|
|
7
|
+
import path from "path";
|
|
8
|
+
|
|
9
|
+
let collection: Collection;
|
|
10
|
+
|
|
11
|
+
beforeEach(async () => {
|
|
12
|
+
collection = await createTestCollection();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
describe("extractSections", () => {
|
|
16
|
+
describe("grouped mode (default)", () => {
|
|
17
|
+
it("groups sections under source document titles", () => {
|
|
18
|
+
const doc1 = collection.document("epics/authentication");
|
|
19
|
+
const doc2 = collection.document("epics/searching-and-browsing");
|
|
20
|
+
|
|
21
|
+
const combined = extractSections([
|
|
22
|
+
{ source: doc1, sections: "Stories" },
|
|
23
|
+
{ source: doc2, sections: "Stories" },
|
|
24
|
+
]);
|
|
25
|
+
|
|
26
|
+
const headings = combined.astQuery.selectAll("heading") as Heading[];
|
|
27
|
+
|
|
28
|
+
// Source titles become h1
|
|
29
|
+
expect(headings[0].depth).toBe(1);
|
|
30
|
+
expect(toString(headings[0])).toBe("Authentication");
|
|
31
|
+
|
|
32
|
+
// "Stories" section headings become h2
|
|
33
|
+
expect(headings[1].depth).toBe(2);
|
|
34
|
+
expect(toString(headings[1])).toBe("Stories");
|
|
35
|
+
|
|
36
|
+
// Sub-headings within Stories shift accordingly
|
|
37
|
+
expect(headings[2].depth).toBe(3);
|
|
38
|
+
expect(toString(headings[2])).toBe("A User should be able to register.");
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("with title, nests source titles under it", () => {
|
|
42
|
+
const doc1 = collection.document("epics/authentication");
|
|
43
|
+
const doc2 = collection.document("epics/searching-and-browsing");
|
|
44
|
+
|
|
45
|
+
const combined = extractSections(
|
|
46
|
+
[
|
|
47
|
+
{ source: doc1, sections: "Stories" },
|
|
48
|
+
{ source: doc2, sections: "Stories" },
|
|
49
|
+
],
|
|
50
|
+
{ title: "All Stories" },
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
const headings = combined.astQuery.selectAll("heading") as Heading[];
|
|
54
|
+
|
|
55
|
+
// Title is h1
|
|
56
|
+
expect(headings[0].depth).toBe(1);
|
|
57
|
+
expect(toString(headings[0])).toBe("All Stories");
|
|
58
|
+
|
|
59
|
+
// Source titles become h2
|
|
60
|
+
expect(headings[1].depth).toBe(2);
|
|
61
|
+
expect(toString(headings[1])).toBe("Authentication");
|
|
62
|
+
|
|
63
|
+
// Section headings become h3
|
|
64
|
+
expect(headings[2].depth).toBe(3);
|
|
65
|
+
expect(toString(headings[2])).toBe("Stories");
|
|
66
|
+
|
|
67
|
+
// Sub-headings shift to h4
|
|
68
|
+
expect(headings[3].depth).toBe(4);
|
|
69
|
+
expect(toString(headings[3])).toBe("A User should be able to register.");
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("extracts multiple sections from one source", () => {
|
|
73
|
+
const doc = collection.document("epics/authentication");
|
|
74
|
+
|
|
75
|
+
// The Authentication epic has "Stories" as a section.
|
|
76
|
+
// Within that, individual stories have "Acceptance Criteria" and "Mockups".
|
|
77
|
+
// Let's extract the top-level "Stories" section.
|
|
78
|
+
const combined = extractSections([
|
|
79
|
+
{ source: doc, sections: "Stories" },
|
|
80
|
+
]);
|
|
81
|
+
|
|
82
|
+
const headings = combined.astQuery.selectAll("heading") as Heading[];
|
|
83
|
+
|
|
84
|
+
// Source title is h1
|
|
85
|
+
expect(toString(headings[0])).toBe("Authentication");
|
|
86
|
+
expect(headings[0].depth).toBe(1);
|
|
87
|
+
|
|
88
|
+
// "Stories" is h2
|
|
89
|
+
expect(toString(headings[1])).toBe("Stories");
|
|
90
|
+
expect(headings[1].depth).toBe(2);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
describe("flat mode", () => {
|
|
95
|
+
it("places sections sequentially without source grouping", () => {
|
|
96
|
+
const doc1 = collection.document("epics/authentication");
|
|
97
|
+
const doc2 = collection.document("epics/searching-and-browsing");
|
|
98
|
+
|
|
99
|
+
const combined = extractSections(
|
|
100
|
+
[
|
|
101
|
+
{ source: doc1, sections: "Stories" },
|
|
102
|
+
{ source: doc2, sections: "Stories" },
|
|
103
|
+
],
|
|
104
|
+
{ mode: "flat" },
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
const headings = combined.astQuery.selectAll("heading") as Heading[];
|
|
108
|
+
|
|
109
|
+
// Both "Stories" sections become h1 (no source grouping)
|
|
110
|
+
expect(headings[0].depth).toBe(1);
|
|
111
|
+
expect(toString(headings[0])).toBe("Stories");
|
|
112
|
+
|
|
113
|
+
// Sub-headings shift accordingly
|
|
114
|
+
expect(headings[1].depth).toBe(2);
|
|
115
|
+
expect(toString(headings[1])).toBe("A User should be able to register.");
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("with title, sections start at h2", () => {
|
|
119
|
+
const doc1 = collection.document("epics/authentication");
|
|
120
|
+
const doc2 = collection.document("epics/searching-and-browsing");
|
|
121
|
+
|
|
122
|
+
const combined = extractSections(
|
|
123
|
+
[
|
|
124
|
+
{ source: doc1, sections: "Stories" },
|
|
125
|
+
{ source: doc2, sections: "Stories" },
|
|
126
|
+
],
|
|
127
|
+
{ mode: "flat", title: "Combined Stories" },
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
const headings = combined.astQuery.selectAll("heading") as Heading[];
|
|
131
|
+
|
|
132
|
+
// Title is h1
|
|
133
|
+
expect(headings[0].depth).toBe(1);
|
|
134
|
+
expect(toString(headings[0])).toBe("Combined Stories");
|
|
135
|
+
|
|
136
|
+
// Section headings are h2
|
|
137
|
+
expect(headings[1].depth).toBe(2);
|
|
138
|
+
expect(toString(headings[1])).toBe("Stories");
|
|
139
|
+
|
|
140
|
+
// Sub-headings are h3
|
|
141
|
+
expect(headings[2].depth).toBe(3);
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
describe("works with ParsedDocument", () => {
|
|
146
|
+
it("accepts ParsedDocument as source", async () => {
|
|
147
|
+
const parsed = await parse(
|
|
148
|
+
path.join(FIXTURES_PATH, "epics/authentication.mdx"),
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
const combined = extractSections([
|
|
152
|
+
{ source: parsed, sections: "Stories" },
|
|
153
|
+
]);
|
|
154
|
+
|
|
155
|
+
const headings = combined.astQuery.selectAll("heading") as Heading[];
|
|
156
|
+
expect(toString(headings[0])).toBe("Authentication");
|
|
157
|
+
expect(headings[0].depth).toBe(1);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it("accepts mixed Document and ParsedDocument sources", async () => {
|
|
161
|
+
const doc = collection.document("epics/authentication");
|
|
162
|
+
const parsed = await parse(
|
|
163
|
+
path.join(FIXTURES_PATH, "epics/searching-and-browsing.mdx"),
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
const combined = extractSections([
|
|
167
|
+
{ source: doc, sections: "Stories" },
|
|
168
|
+
{ source: parsed, sections: "Stories" },
|
|
169
|
+
]);
|
|
170
|
+
|
|
171
|
+
const headings = combined.astQuery.selectAll("heading") as Heading[];
|
|
172
|
+
|
|
173
|
+
// Both source titles present
|
|
174
|
+
const titles = headings
|
|
175
|
+
.filter((h) => h.depth === 1)
|
|
176
|
+
.map((h) => toString(h));
|
|
177
|
+
expect(titles).toContain("Authentication");
|
|
178
|
+
expect(titles).toContain("Searching And Browsing");
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
describe("onMissing", () => {
|
|
183
|
+
it("skips missing sections by default", () => {
|
|
184
|
+
const doc = collection.document("epics/authentication");
|
|
185
|
+
|
|
186
|
+
const combined = extractSections([
|
|
187
|
+
{ source: doc, sections: "Nonexistent Section" },
|
|
188
|
+
]);
|
|
189
|
+
|
|
190
|
+
// Should have only the source title heading, no section content
|
|
191
|
+
const headings = combined.astQuery.selectAll("heading") as Heading[];
|
|
192
|
+
expect(headings).toHaveLength(1);
|
|
193
|
+
expect(toString(headings[0])).toBe("Authentication");
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it("throws on missing section when onMissing is 'throw'", () => {
|
|
197
|
+
const doc = collection.document("epics/authentication");
|
|
198
|
+
|
|
199
|
+
expect(() =>
|
|
200
|
+
extractSections(
|
|
201
|
+
[{ source: doc, sections: "Nonexistent Section" }],
|
|
202
|
+
{ onMissing: "throw" },
|
|
203
|
+
),
|
|
204
|
+
).toThrow("Heading not found");
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
describe("edge cases", () => {
|
|
209
|
+
it("returns empty document for empty entries", () => {
|
|
210
|
+
const combined = extractSections([]);
|
|
211
|
+
|
|
212
|
+
expect(combined.content).toBe("");
|
|
213
|
+
expect(combined.ast.children).toHaveLength(0);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it("returns document with only title for empty entries with title", () => {
|
|
217
|
+
const combined = extractSections([], { title: "Empty Doc" });
|
|
218
|
+
|
|
219
|
+
const headings = combined.astQuery.selectAll("heading") as Heading[];
|
|
220
|
+
expect(headings).toHaveLength(1);
|
|
221
|
+
expect(toString(headings[0])).toBe("Empty Doc");
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it("clamps heading depths to max 6", async () => {
|
|
225
|
+
// Create a document with deeply nested headings (h4+ subsections)
|
|
226
|
+
// In grouped mode with title, h4 sections would shift by +3 → h7, should clamp to h6
|
|
227
|
+
const deepDoc = await parse(
|
|
228
|
+
"# Doc\n## Section\n### Sub\n#### Deep\n##### Deeper\n###### Deepest\n\nContent here.",
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
const combined = extractSections(
|
|
232
|
+
[{ source: deepDoc, sections: "Section" }],
|
|
233
|
+
{ title: "Wrapper" },
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
const headings = combined.astQuery.selectAll("heading") as Heading[];
|
|
237
|
+
// All heading depths should be <= 6
|
|
238
|
+
for (const h of headings) {
|
|
239
|
+
expect(h.depth).toBeLessThanOrEqual(6);
|
|
240
|
+
expect(h.depth).toBeGreaterThanOrEqual(1);
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it("handles source with no title using (Untitled)", async () => {
|
|
245
|
+
// A document with no heading — just content
|
|
246
|
+
const noTitleDoc = await parse("## Section\n\nSome content.");
|
|
247
|
+
|
|
248
|
+
const combined = extractSections([
|
|
249
|
+
{ source: noTitleDoc, sections: "Section" },
|
|
250
|
+
]);
|
|
251
|
+
|
|
252
|
+
const headings = combined.astQuery.selectAll("heading") as Heading[];
|
|
253
|
+
// In grouped mode, source title should be "(Untitled)" since doc title comes from first heading
|
|
254
|
+
// But actually, parse() sets title from first heading which is "Section" here
|
|
255
|
+
// Let's use a truly headingless doc
|
|
256
|
+
const bareDoc = await parse("Just a paragraph, no headings at all.");
|
|
257
|
+
const combined2 = extractSections([
|
|
258
|
+
{ source: bareDoc, sections: "Nonexistent" },
|
|
259
|
+
]);
|
|
260
|
+
const headings2 = combined2.astQuery.selectAll("heading") as Heading[];
|
|
261
|
+
expect(toString(headings2[0])).toBe("(Untitled)");
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
describe("returned ParsedDocument", () => {
|
|
266
|
+
it("has working content and stringify", () => {
|
|
267
|
+
const doc = collection.document("epics/authentication");
|
|
268
|
+
|
|
269
|
+
const combined = extractSections(
|
|
270
|
+
[{ source: doc, sections: "Stories" }],
|
|
271
|
+
{ title: "Test" },
|
|
272
|
+
);
|
|
273
|
+
|
|
274
|
+
expect(combined.content).toContain("# Test");
|
|
275
|
+
expect(combined.stringify()).toBe(combined.content);
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it("has working extractSection", () => {
|
|
279
|
+
const doc = collection.document("epics/authentication");
|
|
280
|
+
|
|
281
|
+
const combined = extractSections([
|
|
282
|
+
{ source: doc, sections: "Stories" },
|
|
283
|
+
]);
|
|
284
|
+
|
|
285
|
+
// The combined doc should have "Stories" as an h2 under "Authentication"
|
|
286
|
+
const section = combined.extractSection("Stories");
|
|
287
|
+
expect(section.length).toBeGreaterThan(0);
|
|
288
|
+
expect(section[0].type).toBe("heading");
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
it("has working querySection", () => {
|
|
292
|
+
const doc = collection.document("epics/authentication");
|
|
293
|
+
|
|
294
|
+
const combined = extractSections([
|
|
295
|
+
{ source: doc, sections: "Stories" },
|
|
296
|
+
]);
|
|
297
|
+
|
|
298
|
+
const query = combined.querySection("Stories");
|
|
299
|
+
const subHeadings = query.selectAll("heading") as Heading[];
|
|
300
|
+
expect(subHeadings.length).toBeGreaterThan(0);
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
it("has working nodes accessor", () => {
|
|
304
|
+
const doc = collection.document("epics/authentication");
|
|
305
|
+
|
|
306
|
+
const combined = extractSections(
|
|
307
|
+
[{ source: doc, sections: "Stories" }],
|
|
308
|
+
{ title: "Test" },
|
|
309
|
+
);
|
|
310
|
+
|
|
311
|
+
expect(combined.nodes).toBeDefined();
|
|
312
|
+
expect(combined.title).toBe("Test");
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
it("has empty meta", () => {
|
|
316
|
+
const doc = collection.document("epics/authentication");
|
|
317
|
+
|
|
318
|
+
const combined = extractSections([
|
|
319
|
+
{ source: doc, sections: "Stories" },
|
|
320
|
+
]);
|
|
321
|
+
|
|
322
|
+
expect(combined.meta).toEqual({});
|
|
323
|
+
});
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
describe("sections as array", () => {
|
|
327
|
+
it("accepts string for single section", () => {
|
|
328
|
+
const doc = collection.document("epics/authentication");
|
|
329
|
+
|
|
330
|
+
const combined = extractSections([
|
|
331
|
+
{ source: doc, sections: "Stories" },
|
|
332
|
+
]);
|
|
333
|
+
|
|
334
|
+
const headings = combined.astQuery.selectAll("heading") as Heading[];
|
|
335
|
+
expect(headings.length).toBeGreaterThan(1);
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
it("accepts array for multiple sections", async () => {
|
|
339
|
+
// Use a parsed doc with distinct top-level sections
|
|
340
|
+
const doc = await parse(
|
|
341
|
+
"# My Doc\n\n## Section A\n\nContent A.\n\n## Section B\n\nContent B.\n\n## Section C\n\nContent C.",
|
|
342
|
+
);
|
|
343
|
+
|
|
344
|
+
const combined = extractSections([
|
|
345
|
+
{ source: doc, sections: ["Section A", "Section B"] },
|
|
346
|
+
]);
|
|
347
|
+
|
|
348
|
+
const headings = combined.astQuery.selectAll("heading") as Heading[];
|
|
349
|
+
const headingTexts = headings.map((h) => toString(h));
|
|
350
|
+
|
|
351
|
+
expect(headingTexts).toContain("Section A");
|
|
352
|
+
expect(headingTexts).toContain("Section B");
|
|
353
|
+
expect(headingTexts).not.toContain("Section C");
|
|
354
|
+
});
|
|
355
|
+
});
|
|
356
|
+
});
|
package/test/section.test.ts
CHANGED
|
@@ -49,8 +49,8 @@ describe("section helper", () => {
|
|
|
49
49
|
const TestModel = {
|
|
50
50
|
name: "Test",
|
|
51
51
|
prefix: "test",
|
|
52
|
-
meta: z.
|
|
53
|
-
schema: z.
|
|
52
|
+
meta: z.looseObject({}),
|
|
53
|
+
schema: z.looseObject({}),
|
|
54
54
|
sections: {
|
|
55
55
|
items: section("Acceptance Criteria", {
|
|
56
56
|
extract: (q) => {
|
|
@@ -81,8 +81,8 @@ describe("section helper", () => {
|
|
|
81
81
|
const TestModel = {
|
|
82
82
|
name: "Test",
|
|
83
83
|
prefix: "test",
|
|
84
|
-
meta: z.
|
|
85
|
-
schema: z.
|
|
84
|
+
meta: z.looseObject({}),
|
|
85
|
+
schema: z.looseObject({}),
|
|
86
86
|
sections: {
|
|
87
87
|
missing: section("Nonexistent Section", {
|
|
88
88
|
extract: (q) => q.selectAll("listItem").map((n) => toString(n)),
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from "vitest";
|
|
2
|
+
import { Collection } from "../src/collection";
|
|
3
|
+
import { createTestCollection, FIXTURES_PATH } from "./helpers";
|
|
4
|
+
|
|
5
|
+
let collection: Collection;
|
|
6
|
+
|
|
7
|
+
beforeEach(async () => {
|
|
8
|
+
collection = await createTestCollection();
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
describe("Collection.tableOfContents", () => {
|
|
12
|
+
it("generates markdown with model group headings", () => {
|
|
13
|
+
const toc = collection.tableOfContents();
|
|
14
|
+
|
|
15
|
+
expect(toc).toContain("# Epic");
|
|
16
|
+
expect(toc).toContain("# Story");
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("includes document titles as link text", () => {
|
|
20
|
+
const toc = collection.tableOfContents();
|
|
21
|
+
|
|
22
|
+
expect(toc).toContain("[Authentication]");
|
|
23
|
+
expect(toc).toContain("[Searching And Browsing]");
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("generates relative links with file extensions", () => {
|
|
27
|
+
const toc = collection.tableOfContents();
|
|
28
|
+
|
|
29
|
+
expect(toc).toContain("(./epics/authentication.mdx)");
|
|
30
|
+
expect(toc).toContain("(./epics/searching-and-browsing.mdx)");
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("uses custom basePath for links", () => {
|
|
34
|
+
const toc = collection.tableOfContents({ basePath: "./content" });
|
|
35
|
+
|
|
36
|
+
expect(toc).toContain("(./content/epics/authentication.mdx)");
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("adds a title heading when provided", () => {
|
|
40
|
+
const toc = collection.tableOfContents({ title: "Project Docs" });
|
|
41
|
+
|
|
42
|
+
expect(toc).toContain("# Project Docs");
|
|
43
|
+
// Model group headings shift to h2
|
|
44
|
+
expect(toc).toContain("## Epic");
|
|
45
|
+
expect(toc).toContain("## Story");
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("sorts items alphabetically within groups", () => {
|
|
49
|
+
const toc = collection.tableOfContents();
|
|
50
|
+
const authIndex = toc.indexOf("[Authentication]");
|
|
51
|
+
const searchIndex = toc.indexOf("[Searching And Browsing]");
|
|
52
|
+
|
|
53
|
+
expect(authIndex).toBeLessThan(searchIndex);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("formats entries as markdown list items", () => {
|
|
57
|
+
const toc = collection.tableOfContents();
|
|
58
|
+
const lines = toc.split("\n");
|
|
59
|
+
const listLines = lines.filter((l) => l.startsWith("- ["));
|
|
60
|
+
|
|
61
|
+
expect(listLines.length).toBeGreaterThanOrEqual(3);
|
|
62
|
+
for (const line of listLines) {
|
|
63
|
+
expect(line).toMatch(/^- \[.+\]\(.+\.mdx?\)$/);
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("ends with a newline", () => {
|
|
68
|
+
const toc = collection.tableOfContents();
|
|
69
|
+
expect(toc.endsWith("\n")).toBe(true);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("works without models registered (flat list)", async () => {
|
|
73
|
+
const bare = new Collection({ rootPath: FIXTURES_PATH });
|
|
74
|
+
await bare.load();
|
|
75
|
+
|
|
76
|
+
const toc = bare.tableOfContents();
|
|
77
|
+
|
|
78
|
+
// No group headings — just list items
|
|
79
|
+
expect(toc).not.toContain("# Epic");
|
|
80
|
+
expect(toc).toContain("- [");
|
|
81
|
+
expect(toc).toContain("(./epics/authentication.mdx)");
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("throws if collection not loaded", () => {
|
|
85
|
+
const unloaded = new Collection({ rootPath: FIXTURES_PATH });
|
|
86
|
+
|
|
87
|
+
expect(() => unloaded.tableOfContents()).toThrow(
|
|
88
|
+
"Collection has not been loaded"
|
|
89
|
+
);
|
|
90
|
+
});
|
|
91
|
+
});
|