11import * as content from 'astro:content' ;
22import { describe , expect , test , vi , type TaskContext } from 'vitest' ;
33import { getTutorial , type CollectionEntryTutorial } from './content' ;
4+ import { logger } from './logger' ;
45
56const getCollection = vi . mocked < ( ) => Omit < CollectionEntryTutorial , 'render' > [ ] > ( content . getCollection ) ;
67vi . mock ( 'astro:content' , ( ) => ( { getCollection : vi . fn ( ) } ) ) ;
@@ -11,6 +12,8 @@ vi.mock(import('@tutorialkit/types'), async (importOriginal) => ({
1112 DEFAULT_LOCALIZATION : { mocked : 'default localization' } as any ,
1213} ) ) ;
1314
15+ vi . mock ( import ( './logger' ) , async ( importOriginal ) => importOriginal ( ) ) ;
16+
1417expect . addSnapshotSerializer ( {
1518 serialize : ( val ) => JSON . stringify ( val , null , 2 ) ,
1619 test : ( value ) => ! ( value instanceof Error ) ,
@@ -95,6 +98,34 @@ test('multiple parts', async (ctx) => {
9598 await expect ( collection ) . toMatchFileSnapshot ( snapshotName ( ctx ) ) ;
9699} ) ;
97100
101+ test ( 'lessons with identical names in different chapters' , async ( ) => {
102+ getCollection . mockReturnValueOnce ( [
103+ { id : 'meta.md' , ...tutorial } ,
104+ { id : '1-part/meta.md' , ...part } ,
105+
106+ { id : '1-part/1-chapter/meta.md' , ...chapter } ,
107+ {
108+ id : '1-part/1-chapter/identical-lesson-name/content.md' ,
109+ ...lesson ,
110+ data : { ...lesson . data , focus : '/first.js' } ,
111+ } ,
112+
113+ { id : '1-part/2-chapter/meta.md' , ...chapter } ,
114+ {
115+ id : '1-part/2-chapter/identical-lesson-name/content.md' ,
116+ ...lesson ,
117+ data : { ...lesson . data , focus : '/second.js' } ,
118+ } ,
119+ ] ) ;
120+
121+ const collection = await getTutorial ( ) ;
122+ const chapters = collection . parts [ '1-part' ] . chapters ;
123+
124+ // verify that lesson.id is not used to define what makes a lesson unique (part.id + chapter.id too)
125+ expect ( chapters [ '1-chapter' ] . lessons [ 'identical-lesson-name' ] ) . toBeDefined ( ) ;
126+ expect ( chapters [ '2-chapter' ] . lessons [ 'identical-lesson-name' ] ) . toBeDefined ( ) ;
127+ } ) ;
128+
98129describe ( 'metadata inheriting' , ( ) => {
99130 test ( 'lesson inherits metadata from tutorial' , async ( ) => {
100131 const data : CollectionEntryTutorial [ 'data' ] = {
@@ -212,6 +243,33 @@ describe('ordering', () => {
212243 expect ( parts [ '2-part' ] . order ) . toBe ( 2 ) ;
213244 } ) ;
214245
246+ test ( 'parts not mention in order are excluded ' , async ( ) => {
247+ vi . spyOn ( logger , 'warn' ) . mockImplementationOnce ( vi . fn ( ) ) ;
248+
249+ getCollection . mockReturnValueOnce ( [
250+ {
251+ id : 'meta.md' ,
252+ ...tutorial ,
253+ data : { ...tutorial . data , parts : [ '2-part' , '1-part' ] } ,
254+ } ,
255+ { id : '2-part/meta.md' , ...part } ,
256+ { id : 'excluded-part/meta.md' , ...part } ,
257+ { id : '1-part/meta.md' , ...part } ,
258+ ] ) ;
259+
260+ const collection = await getTutorial ( ) ;
261+ const parts = collection . parts ;
262+
263+ expect ( Object . keys ( parts ) ) . toHaveLength ( 2 ) ;
264+ expect ( parts [ 'excluded-part' ] ) . toBeUndefined ( ) ;
265+ expect ( parts [ '1-part' ] ) . toBeDefined ( ) ;
266+ expect ( parts [ '2-part' ] ) . toBeDefined ( ) ;
267+
268+ expect ( vi . mocked ( logger . warn ) . mock . calls [ 0 ] [ 0 ] ) . toMatchInlineSnapshot (
269+ `"An order was specified for the parts of the tutorial but 'excluded-part' is not included so it won't be visible."` ,
270+ ) ;
271+ } ) ;
272+
215273 test ( 'chapters are ordered by default' , async ( ) => {
216274 getCollection . mockReturnValueOnce ( [
217275 { id : 'meta.md' , ...tutorial } ,
@@ -246,6 +304,30 @@ describe('ordering', () => {
246304 expect ( chapters [ '2-chapter' ] . order ) . toBe ( 2 ) ;
247305 } ) ;
248306
307+ test ( 'chapters not mention in order are excluded ' , async ( ) => {
308+ vi . spyOn ( logger , 'warn' ) . mockImplementationOnce ( vi . fn ( ) ) ;
309+
310+ getCollection . mockReturnValueOnce ( [
311+ { id : 'meta.md' , ...tutorial } ,
312+ { id : '1-part/meta.md' , ...part , data : { ...part . data , chapters : [ '2-chapter' , '1-chapter' ] } } ,
313+ { id : '1-part/2-chapter/meta.md' , ...chapter } ,
314+ { id : '1-part/excluded-chapter/meta.md' , ...chapter } ,
315+ { id : '1-part/1-chapter/meta.md' , ...chapter } ,
316+ ] ) ;
317+
318+ const collection = await getTutorial ( ) ;
319+ const chapters = collection . parts [ '1-part' ] . chapters ;
320+
321+ expect ( Object . keys ( chapters ) ) . toHaveLength ( 2 ) ;
322+ expect ( chapters [ 'excluded-part' ] ) . toBeUndefined ( ) ;
323+ expect ( chapters [ '1-chapter' ] ) . toBeDefined ( ) ;
324+ expect ( chapters [ '2-chapter' ] ) . toBeDefined ( ) ;
325+
326+ expect ( vi . mocked ( logger . warn ) . mock . calls [ 0 ] [ 0 ] ) . toMatchInlineSnapshot (
327+ `"An order was specified for part '1-part' but chapter 'excluded-chapter' is not included, so it won't be visible."` ,
328+ ) ;
329+ } ) ;
330+
249331 test ( 'lessons are ordered by default' , async ( ) => {
250332 getCollection . mockReturnValueOnce ( [
251333 { id : 'meta.md' , ...tutorial } ,
@@ -288,6 +370,34 @@ describe('ordering', () => {
288370 expect ( lessons [ '1-lesson' ] . order ) . toBe ( 1 ) ;
289371 expect ( lessons [ '2-lesson' ] . order ) . toBe ( 2 ) ;
290372 } ) ;
373+
374+ test ( 'lessons not mention in order are excluded ' , async ( ) => {
375+ vi . spyOn ( logger , 'warn' ) . mockImplementationOnce ( vi . fn ( ) ) ;
376+
377+ getCollection . mockReturnValueOnce ( [
378+ { id : 'meta.md' , ...tutorial } ,
379+ { id : '1-part/meta.md' , ...part } ,
380+ {
381+ id : '1-part/1-chapter/meta.md' ,
382+ ...chapter ,
383+ data : { ...chapter . data , lessons : [ '2-lesson' , '1-lesson' ] } ,
384+ } ,
385+ { id : '1-part/1-chapter/excluded-lesson/meta.md' , ...lesson } ,
386+ { id : '1-part/1-chapter/1-lesson/meta.md' , ...lesson } ,
387+ { id : '1-part/1-chapter/2-lesson/meta.md' , ...lesson } ,
388+ ] ) ;
389+
390+ const collection = await getTutorial ( ) ;
391+ const lessons = collection . parts [ '1-part' ] . chapters [ '1-chapter' ] . lessons ;
392+
393+ expect ( Object . keys ( lessons ) ) . toHaveLength ( 2 ) ;
394+ expect ( lessons [ '1-lesson' ] ) . toBeDefined ( ) ;
395+ expect ( lessons [ '2-lesson' ] ) . toBeDefined ( ) ;
396+
397+ expect ( vi . mocked ( logger . warn ) . mock . calls [ 0 ] [ 0 ] ) . toMatchInlineSnapshot (
398+ `"An order was specified for chapter '1-chapter' but lesson 'excluded-lesson' is not included, so it won't be visible."` ,
399+ ) ;
400+ } ) ;
291401} ) ;
292402
293403describe ( 'missing parts' , ( ) => {
0 commit comments