diff --git a/packages/astro/src/default/utils/__snapshots__/single-part-chapter-and-lesson.json b/packages/astro/src/default/utils/__snapshots__/single-part-chapter-and-lesson.json new file mode 100644 index 00000000..6ce2dd63 --- /dev/null +++ b/packages/astro/src/default/utils/__snapshots__/single-part-chapter-and-lesson.json @@ -0,0 +1,61 @@ +{ + "parts": { + "1-part": { + "id": "1-part", + "order": 0, + "data": { + "type": "part", + "title": "Basics" + }, + "slug": "part-slug", + "chapters": { + "1-chapter": { + "id": "1-chapter", + "order": 0, + "data": { + "title": "The first chapter in part 1", + "type": "chapter" + }, + "slug": "chapter-slug", + "lessons": { + "1-lesson": { + "data": { + "type": "lesson", + "title": "Welcome to TutorialKit", + "template": "default", + "i18n": { + "mocked": "default localization" + }, + "openInStackBlitz": true + }, + "id": "1-lesson", + "filepath": "1-part/1-chapter/1-lesson/content.md", + "order": 0, + "part": { + "id": "1-part", + "title": "Basics" + }, + "chapter": { + "id": "1-chapter", + "title": "The first chapter in part 1" + }, + "Markdown": "Markdown for tutorial", + "slug": "lesson-slug", + "files": [ + "1-part-1-chapter-1-lesson-files.json", + [] + ], + "solution": [ + "1-part-1-chapter-1-lesson-solution.json", + [] + ] + } + }, + "firstLessonId": "1-lesson" + } + }, + "firstChapterId": "1-chapter" + } + }, + "firstPartId": "1-part" +} \ No newline at end of file diff --git a/packages/astro/src/default/utils/__snapshots__/single-part-chapter-and-multiple-lessons.json b/packages/astro/src/default/utils/__snapshots__/single-part-chapter-and-multiple-lessons.json new file mode 100644 index 00000000..605f6155 --- /dev/null +++ b/packages/astro/src/default/utils/__snapshots__/single-part-chapter-and-multiple-lessons.json @@ -0,0 +1,141 @@ +{ + "parts": { + "1-part": { + "id": "1-part", + "order": 0, + "data": { + "type": "part", + "title": "Basics" + }, + "slug": "part-slug", + "chapters": { + "1-chapter": { + "id": "1-chapter", + "order": 0, + "data": { + "title": "The first chapter in part 1", + "type": "chapter" + }, + "slug": "chapter-slug", + "lessons": { + "1-first": { + "data": { + "type": "lesson", + "title": "Welcome to TutorialKit", + "template": "default", + "i18n": { + "mocked": "default localization" + }, + "openInStackBlitz": true + }, + "id": "1-first", + "filepath": "1-part/1-chapter/1-first/content.md", + "order": 0, + "part": { + "id": "1-part", + "title": "Basics" + }, + "chapter": { + "id": "1-chapter", + "title": "The first chapter in part 1" + }, + "Markdown": "Markdown for tutorial", + "slug": "lesson-slug", + "files": [ + "1-part-1-chapter-1-first-files.json", + [] + ], + "solution": [ + "1-part-1-chapter-1-first-solution.json", + [] + ], + "next": { + "title": "Welcome to TutorialKit", + "href": "/part-slug/chapter-slug/lesson-slug" + } + }, + "2-second": { + "data": { + "type": "lesson", + "title": "Welcome to TutorialKit", + "template": "default", + "i18n": { + "mocked": "default localization" + }, + "openInStackBlitz": true + }, + "id": "2-second", + "filepath": "1-part/1-chapter/2-second/content.md", + "order": 1, + "part": { + "id": "1-part", + "title": "Basics" + }, + "chapter": { + "id": "1-chapter", + "title": "The first chapter in part 1" + }, + "Markdown": "Markdown for tutorial", + "slug": "lesson-slug", + "files": [ + "1-part-1-chapter-2-second-files.json", + [] + ], + "solution": [ + "1-part-1-chapter-2-second-solution.json", + [] + ], + "prev": { + "title": "Welcome to TutorialKit", + "href": "/part-slug/chapter-slug/lesson-slug" + }, + "next": { + "title": "Welcome to TutorialKit", + "href": "/part-slug/chapter-slug/lesson-slug" + } + }, + "3-third": { + "data": { + "type": "lesson", + "title": "Welcome to TutorialKit", + "template": "default", + "i18n": { + "mocked": "default localization" + }, + "openInStackBlitz": true + }, + "id": "3-third", + "filepath": "1-part/1-chapter/3-third/content.md", + "order": 2, + "part": { + "id": "1-part", + "title": "Basics" + }, + "chapter": { + "id": "1-chapter", + "title": "The first chapter in part 1" + }, + "Markdown": "Markdown for tutorial", + "slug": "lesson-slug", + "files": [ + "1-part-1-chapter-3-third-files.json", + [] + ], + "solution": [ + "1-part-1-chapter-3-third-solution.json", + [] + ], + "prev": { + "title": "Welcome to TutorialKit", + "href": "/part-slug/chapter-slug/lesson-slug" + } + } + }, + "firstLessonId": "1-first" + } + }, + "firstChapterId": "1-chapter" + } + }, + "firstPartId": "1-part" +} \ No newline at end of file diff --git a/packages/astro/src/default/utils/content.spec.ts b/packages/astro/src/default/utils/content.spec.ts new file mode 100644 index 00000000..277490c2 --- /dev/null +++ b/packages/astro/src/default/utils/content.spec.ts @@ -0,0 +1,106 @@ +import * as content from 'astro:content'; +import { expect, test, vi, type TaskContext } from 'vitest'; +import { getTutorial, type CollectionEntryTutorial } from './content'; + +const getCollection = vi.mocked(content.getCollection); +vi.mock('astro:content', () => ({ getCollection: vi.fn() })); + +// mock DEFAULT_LOCALIZATION so that we don't need to update test results everytime new keys are added there +vi.mock(import('@tutorialkit/types'), async (importOriginal) => ({ + ...(await importOriginal()), + DEFAULT_LOCALIZATION: { mocked: 'default localization' } as any, +})); + +expect.addSnapshotSerializer({ + serialize: (val) => JSON.stringify(val, null, 2), + test: (value) => !(value instanceof Error), +}); + +test('single part, chapter and lesson', async (ctx) => { + getCollection.mockReturnValueOnce([ + { id: 'meta.md', ...tutorial }, + { id: '1-part/meta.md', ...part }, + { id: '1-part/1-chapter/meta.md', ...chapter }, + { id: '1-part/1-chapter/1-lesson/content.md', ...lesson }, + ]); + + const collection = await getTutorial(); + await expect(collection).toMatchFileSnapshot(snapshotName(ctx)); +}); + +test('single part, chapter and multiple lessons', async (ctx) => { + getCollection.mockReturnValueOnce([ + { id: 'meta.md', ...tutorial }, + { id: '1-part/meta.md', ...part }, + { id: '1-part/1-chapter/meta.md', ...chapter }, + + // 3 lessons + { id: '1-part/1-chapter/1-first/content.md', ...lesson }, + { id: '1-part/1-chapter/2-second/content.md', ...lesson }, + { id: '1-part/1-chapter/3-third/content.md', ...lesson }, + ]); + + const collection = await getTutorial(); + + const lessons = collection.parts['1-part'].chapters['1-chapter'].lessons; + expect(Object.keys(lessons)).toHaveLength(3); + + await expect(collection).toMatchFileSnapshot(snapshotName(ctx)); +}); + +test('throws when part not found', async () => { + getCollection.mockReturnValueOnce([ + { id: 'meta.md', ...tutorial }, + { id: '2-part/meta.md', ...part }, + { id: '1-part/1-chapter/meta.md', ...chapter }, + { id: '1-part/1-chapter/1-first/content.md', ...lesson }, + ]); + + await expect(getTutorial).rejects.toThrowErrorMatchingInlineSnapshot(`[Error: Could not find part '1-part']`); +}); + +test('throws when chapter not found', async () => { + getCollection.mockReturnValueOnce([ + { id: 'meta.md', ...tutorial }, + { id: '1-part/meta.md', ...part }, + { id: '1-part/2-chapter/meta.md', ...chapter }, + { id: '1-part/1-chapter/1-first/content.md', ...lesson }, + ]); + + await expect(getTutorial).rejects.toThrowErrorMatchingInlineSnapshot(`[Error: Could not find chapter '1-chapter']`); +}); + +const tutorial: Omit = { + slug: 'tutorial-slug', + body: 'Hello world', + collection: 'tutorial', + data: { type: 'tutorial' }, +}; + +const part: Omit = { + slug: 'part-slug', + body: 'Hello world', + collection: 'tutorial', + data: { type: 'part', title: 'Basics' }, +}; + +const chapter: Omit = { + slug: 'chapter-slug', + body: 'body here', + collection: 'tutorial', + data: { title: 'The first chapter in part 1', type: 'chapter' }, +}; + +const lesson: Omit = { + slug: 'lesson-slug', + body: 'body here', + collection: 'tutorial', + data: { type: 'lesson', title: 'Welcome to TutorialKit' }, + render: () => ({ Content: 'Markdown for tutorial' }) as unknown as ReturnType, +}; + +function snapshotName(ctx: TaskContext) { + const testName = ctx.task.name.replaceAll(',', '').replaceAll(' ', '-'); + + return `__snapshots__/${testName}.json`; +} diff --git a/packages/astro/src/default/utils/content.ts b/packages/astro/src/default/utils/content.ts index 46e19fe9..b1665ba5 100644 --- a/packages/astro/src/default/utils/content.ts +++ b/packages/astro/src/default/utils/content.ts @@ -58,7 +58,7 @@ export async function getTutorial(): Promise { } if (!_tutorial.parts[partId].chapters[chapterId]) { - throw new Error(`Could not find chapter '${partId}'`); + throw new Error(`Could not find chapter '${chapterId}'`); } const { Content } = await entry.render(); @@ -321,7 +321,7 @@ function getSlug(entry: CollectionEntryTutorial) { return slug; } -interface CollectionEntryTutorial { +export interface CollectionEntryTutorial { id: string; slug: string; body: string;