Skip to content

Commit cd4f77b

Browse files
committed
fix(astro): correct error message when chapter not found
1 parent e335d17 commit cd4f77b

File tree

4 files changed

+310
-2
lines changed

4 files changed

+310
-2
lines changed
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
{
2+
"parts": {
3+
"1-part": {
4+
"id": "1-part",
5+
"order": 0,
6+
"data": {
7+
"type": "part",
8+
"title": "Basics"
9+
},
10+
"slug": "part-slug",
11+
"chapters": {
12+
"1-chapter": {
13+
"id": "1-chapter",
14+
"order": 0,
15+
"data": {
16+
"title": "The first chapter in part 1",
17+
"type": "chapter"
18+
},
19+
"slug": "chapter-slug",
20+
"lessons": {
21+
"1-lesson": {
22+
"data": {
23+
"type": "lesson",
24+
"title": "Welcome to TutorialKit",
25+
"template": "default",
26+
"i18n": {
27+
"mocked": "default localization"
28+
},
29+
"openInStackBlitz": true
30+
},
31+
"id": "1-lesson",
32+
"filepath": "1-part/1-chapter/1-lesson/content.md",
33+
"order": 0,
34+
"part": {
35+
"id": "1-part",
36+
"title": "Basics"
37+
},
38+
"chapter": {
39+
"id": "1-chapter",
40+
"title": "The first chapter in part 1"
41+
},
42+
"Markdown": "Markdown for tutorial",
43+
"slug": "lesson-slug",
44+
"files": [
45+
"1-part-1-chapter-1-lesson-files.json",
46+
[]
47+
],
48+
"solution": [
49+
"1-part-1-chapter-1-lesson-solution.json",
50+
[]
51+
]
52+
}
53+
},
54+
"firstLessonId": "1-lesson"
55+
}
56+
},
57+
"firstChapterId": "1-chapter"
58+
}
59+
},
60+
"firstPartId": "1-part"
61+
}
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
{
2+
"parts": {
3+
"1-part": {
4+
"id": "1-part",
5+
"order": 0,
6+
"data": {
7+
"type": "part",
8+
"title": "Basics"
9+
},
10+
"slug": "part-slug",
11+
"chapters": {
12+
"1-chapter": {
13+
"id": "1-chapter",
14+
"order": 0,
15+
"data": {
16+
"title": "The first chapter in part 1",
17+
"type": "chapter"
18+
},
19+
"slug": "chapter-slug",
20+
"lessons": {
21+
"1-first": {
22+
"data": {
23+
"type": "lesson",
24+
"title": "Welcome to TutorialKit",
25+
"template": "default",
26+
"i18n": {
27+
"mocked": "default localization"
28+
},
29+
"openInStackBlitz": true
30+
},
31+
"id": "1-first",
32+
"filepath": "1-part/1-chapter/1-first/content.md",
33+
"order": 0,
34+
"part": {
35+
"id": "1-part",
36+
"title": "Basics"
37+
},
38+
"chapter": {
39+
"id": "1-chapter",
40+
"title": "The first chapter in part 1"
41+
},
42+
"Markdown": "Markdown for tutorial",
43+
"slug": "lesson-slug",
44+
"files": [
45+
"1-part-1-chapter-1-first-files.json",
46+
[]
47+
],
48+
"solution": [
49+
"1-part-1-chapter-1-first-solution.json",
50+
[]
51+
],
52+
"next": {
53+
"title": "Welcome to TutorialKit",
54+
"href": "/part-slug/chapter-slug/lesson-slug"
55+
}
56+
},
57+
"2-second": {
58+
"data": {
59+
"type": "lesson",
60+
"title": "Welcome to TutorialKit",
61+
"template": "default",
62+
"i18n": {
63+
"mocked": "default localization"
64+
},
65+
"openInStackBlitz": true
66+
},
67+
"id": "2-second",
68+
"filepath": "1-part/1-chapter/2-second/content.md",
69+
"order": 1,
70+
"part": {
71+
"id": "1-part",
72+
"title": "Basics"
73+
},
74+
"chapter": {
75+
"id": "1-chapter",
76+
"title": "The first chapter in part 1"
77+
},
78+
"Markdown": "Markdown for tutorial",
79+
"slug": "lesson-slug",
80+
"files": [
81+
"1-part-1-chapter-2-second-files.json",
82+
[]
83+
],
84+
"solution": [
85+
"1-part-1-chapter-2-second-solution.json",
86+
[]
87+
],
88+
"prev": {
89+
"title": "Welcome to TutorialKit",
90+
"href": "/part-slug/chapter-slug/lesson-slug"
91+
},
92+
"next": {
93+
"title": "Welcome to TutorialKit",
94+
"href": "/part-slug/chapter-slug/lesson-slug"
95+
}
96+
},
97+
"3-third": {
98+
"data": {
99+
"type": "lesson",
100+
"title": "Welcome to TutorialKit",
101+
"template": "default",
102+
"i18n": {
103+
"mocked": "default localization"
104+
},
105+
"openInStackBlitz": true
106+
},
107+
"id": "3-third",
108+
"filepath": "1-part/1-chapter/3-third/content.md",
109+
"order": 2,
110+
"part": {
111+
"id": "1-part",
112+
"title": "Basics"
113+
},
114+
"chapter": {
115+
"id": "1-chapter",
116+
"title": "The first chapter in part 1"
117+
},
118+
"Markdown": "Markdown for tutorial",
119+
"slug": "lesson-slug",
120+
"files": [
121+
"1-part-1-chapter-3-third-files.json",
122+
[]
123+
],
124+
"solution": [
125+
"1-part-1-chapter-3-third-solution.json",
126+
[]
127+
],
128+
"prev": {
129+
"title": "Welcome to TutorialKit",
130+
"href": "/part-slug/chapter-slug/lesson-slug"
131+
}
132+
}
133+
},
134+
"firstLessonId": "1-first"
135+
}
136+
},
137+
"firstChapterId": "1-chapter"
138+
}
139+
},
140+
"firstPartId": "1-part"
141+
}
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import * as content from 'astro:content';
2+
import { expect, test, vi, type TaskContext } from 'vitest';
3+
import { getTutorial, type CollectionEntryTutorial } from './content';
4+
5+
const getCollection = vi.mocked(content.getCollection);
6+
vi.mock('astro:content', () => ({ getCollection: vi.fn() }));
7+
8+
// mock DEFAULT_LOCALIZATION so that we don't need to update test results everytime new keys are added there
9+
vi.mock(import('@tutorialkit/types'), async (importOriginal) => ({
10+
...(await importOriginal()),
11+
DEFAULT_LOCALIZATION: { mocked: 'default localization' } as any,
12+
}));
13+
14+
expect.addSnapshotSerializer({
15+
serialize: (val) => JSON.stringify(val, null, 2),
16+
test: (value) => !(value instanceof Error),
17+
});
18+
19+
test('single part, chapter and lesson', async (ctx) => {
20+
getCollection.mockReturnValueOnce([
21+
{ id: 'meta.md', ...tutorial },
22+
{ id: '1-part/meta.md', ...part },
23+
{ id: '1-part/1-chapter/meta.md', ...chapter },
24+
{ id: '1-part/1-chapter/1-lesson/content.md', ...lesson },
25+
]);
26+
27+
const collection = await getTutorial();
28+
await expect(collection).toMatchFileSnapshot(snapshotName(ctx));
29+
});
30+
31+
test('single part, chapter and multiple lessons', async (ctx) => {
32+
getCollection.mockReturnValueOnce([
33+
{ id: 'meta.md', ...tutorial },
34+
{ id: '1-part/meta.md', ...part },
35+
{ id: '1-part/1-chapter/meta.md', ...chapter },
36+
37+
// 3 lessons
38+
{ id: '1-part/1-chapter/1-first/content.md', ...lesson },
39+
{ id: '1-part/1-chapter/2-second/content.md', ...lesson },
40+
{ id: '1-part/1-chapter/3-third/content.md', ...lesson },
41+
]);
42+
43+
const collection = await getTutorial();
44+
45+
const lessons = collection.parts['1-part'].chapters['1-chapter'].lessons;
46+
expect(Object.keys(lessons)).toHaveLength(3);
47+
48+
await expect(collection).toMatchFileSnapshot(snapshotName(ctx));
49+
});
50+
51+
test('throws when part not found', async () => {
52+
getCollection.mockReturnValueOnce([
53+
{ id: 'meta.md', ...tutorial },
54+
{ id: '2-part/meta.md', ...part },
55+
{ id: '1-part/1-chapter/meta.md', ...chapter },
56+
{ id: '1-part/1-chapter/1-first/content.md', ...lesson },
57+
]);
58+
59+
await expect(getTutorial).rejects.toThrowErrorMatchingInlineSnapshot(`[Error: Could not find part '1-part']`);
60+
});
61+
62+
test('throws when chapter not found', async () => {
63+
getCollection.mockReturnValueOnce([
64+
{ id: 'meta.md', ...tutorial },
65+
{ id: '1-part/meta.md', ...part },
66+
{ id: '1-part/2-chapter/meta.md', ...chapter },
67+
{ id: '1-part/1-chapter/1-first/content.md', ...lesson },
68+
]);
69+
70+
await expect(getTutorial).rejects.toThrowErrorMatchingInlineSnapshot(`[Error: Could not find chapter '1-chapter']`);
71+
});
72+
73+
const tutorial: Omit<CollectionEntryTutorial, 'render' | 'id'> = {
74+
slug: 'tutorial-slug',
75+
body: 'Hello world',
76+
collection: 'tutorial',
77+
data: { type: 'tutorial' },
78+
};
79+
80+
const part: Omit<CollectionEntryTutorial, 'render' | 'id'> = {
81+
slug: 'part-slug',
82+
body: 'Hello world',
83+
collection: 'tutorial',
84+
data: { type: 'part', title: 'Basics' },
85+
};
86+
87+
const chapter: Omit<CollectionEntryTutorial, 'render' | 'id'> = {
88+
slug: 'chapter-slug',
89+
body: 'body here',
90+
collection: 'tutorial',
91+
data: { title: 'The first chapter in part 1', type: 'chapter' },
92+
};
93+
94+
const lesson: Omit<CollectionEntryTutorial, 'id'> = {
95+
slug: 'lesson-slug',
96+
body: 'body here',
97+
collection: 'tutorial',
98+
data: { type: 'lesson', title: 'Welcome to TutorialKit' },
99+
render: () => ({ Content: 'Markdown for tutorial' }) as unknown as ReturnType<CollectionEntryTutorial['render']>,
100+
};
101+
102+
function snapshotName(ctx: TaskContext) {
103+
const testName = ctx.task.name.replaceAll(',', '').replaceAll(' ', '-');
104+
105+
return `__snapshots__/${testName}.json`;
106+
}

packages/astro/src/default/utils/content.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ export async function getTutorial(): Promise<Tutorial> {
5858
}
5959

6060
if (!_tutorial.parts[partId].chapters[chapterId]) {
61-
throw new Error(`Could not find chapter '${partId}'`);
61+
throw new Error(`Could not find chapter '${chapterId}'`);
6262
}
6363

6464
const { Content } = await entry.render();
@@ -321,7 +321,7 @@ function getSlug(entry: CollectionEntryTutorial) {
321321
return slug;
322322
}
323323

324-
interface CollectionEntryTutorial {
324+
export interface CollectionEntryTutorial {
325325
id: string;
326326
slug: string;
327327
body: string;

0 commit comments

Comments
 (0)