@@ -241,4 +241,183 @@ describe('Emoji task collection', () => {
241241 '## Header 1\n\n- [ ] Task 1\n- [ ] Task 2\n\n## Header 2\n\n- [ ] Task 3\n- [ ] Task 4\n- [ ] [>] Task 5\n\n'
242242 ) ;
243243 } ) ;
244+
245+ describe ( 'sub-tasks' , ( ) => {
246+ it ( 'parses sub-tasks with tab indentation' , ( ) => {
247+ sut = new EmojiTaskCollection (
248+ '## Header 1\n\n- [ ] Parent task\n\t- [ ] Child task 1\n\t- [ ] Child task 2\n'
249+ ) ;
250+
251+ const tasks = sut . getAllTasks ( ) ;
252+ expect ( tasks . length ) . toEqual ( 1 ) ;
253+ expect ( tasks [ 0 ] . getName ( ) ) . toEqual ( 'Parent task' ) ;
254+ expect ( tasks [ 0 ] . hasChildren ( ) ) . toBe ( true ) ;
255+ expect ( tasks [ 0 ] . getChildren ( ) . length ) . toEqual ( 2 ) ;
256+ expect ( tasks [ 0 ] . getChildren ( ) [ 0 ] . getName ( ) ) . toEqual ( 'Child task 1' ) ;
257+ expect ( tasks [ 0 ] . getChildren ( ) [ 1 ] . getName ( ) ) . toEqual ( 'Child task 2' ) ;
258+ } ) ;
259+
260+ it ( 'parses sub-tasks with space indentation' , ( ) => {
261+ sut = new EmojiTaskCollection (
262+ '## Header 1\n\n- [ ] Parent task\n - [ ] Child task 1\n - [ ] Child task 2\n'
263+ ) ;
264+
265+ const tasks = sut . getAllTasks ( ) ;
266+ expect ( tasks . length ) . toEqual ( 1 ) ;
267+ expect ( tasks [ 0 ] . hasChildren ( ) ) . toBe ( true ) ;
268+ expect ( tasks [ 0 ] . getChildren ( ) . length ) . toEqual ( 2 ) ;
269+ } ) ;
270+
271+ it ( 'parses deeply nested sub-tasks' , ( ) => {
272+ sut = new EmojiTaskCollection (
273+ '## Header 1\n\n- [ ] Level 0\n\t- [ ] Level 1\n\t\t- [ ] Level 2\n\t\t\t- [ ] Level 3\n'
274+ ) ;
275+
276+ const tasks = sut . getAllTasks ( ) ;
277+ expect ( tasks . length ) . toEqual ( 1 ) ;
278+ expect ( tasks [ 0 ] . getName ( ) ) . toEqual ( 'Level 0' ) ;
279+ expect ( tasks [ 0 ] . getChildren ( ) [ 0 ] . getName ( ) ) . toEqual ( 'Level 1' ) ;
280+ expect ( tasks [ 0 ] . getChildren ( ) [ 0 ] . getChildren ( ) [ 0 ] . getName ( ) ) . toEqual ( 'Level 2' ) ;
281+ expect ( tasks [ 0 ] . getChildren ( ) [ 0 ] . getChildren ( ) [ 0 ] . getChildren ( ) [ 0 ] . getName ( ) ) . toEqual (
282+ 'Level 3'
283+ ) ;
284+ } ) ;
285+
286+ it ( 'parses multiple parent tasks with their own sub-tasks' , ( ) => {
287+ sut = new EmojiTaskCollection (
288+ '## Header 1\n\n- [ ] Parent 1\n\t- [ ] Child 1a\n- [ ] Parent 2\n\t- [ ] Child 2a\n\t- [ ] Child 2b\n'
289+ ) ;
290+
291+ const tasks = sut . getAllTasks ( ) ;
292+ expect ( tasks . length ) . toEqual ( 2 ) ;
293+ expect ( tasks [ 0 ] . getName ( ) ) . toEqual ( 'Parent 1' ) ;
294+ expect ( tasks [ 0 ] . getChildren ( ) . length ) . toEqual ( 1 ) ;
295+ expect ( tasks [ 1 ] . getName ( ) ) . toEqual ( 'Parent 2' ) ;
296+ expect ( tasks [ 1 ] . getChildren ( ) . length ) . toEqual ( 2 ) ;
297+ } ) ;
298+
299+ it ( 'outputs sub-tasks with correct indentation' , ( ) => {
300+ sut = new EmojiTaskCollection (
301+ '## Header 1\n\n- [ ] Parent task\n\t- [ ] Child task 1\n\t- [x] Child task 2\n'
302+ ) ;
303+
304+ const result = sut . toString ( ) ;
305+
306+ expect ( result ) . toContain ( '- [ ] Parent task' ) ;
307+ expect ( result ) . toContain ( '\t- [ ] Child task 1' ) ;
308+ expect ( result ) . toContain ( '\t- [x] Child task 2' ) ;
309+ } ) ;
310+
311+ it ( 'parses sub-tasks with metadata' , ( ) => {
312+ sut = new EmojiTaskCollection (
313+ '## Header 1\n\n- [ ] Parent task 📅 2024-01-01\n\t- [ ] Child task 📅 2024-01-02\n'
314+ ) ;
315+
316+ const tasks = sut . getAllTasks ( ) ;
317+ expect ( tasks [ 0 ] . getDueDate ( ) ) . toEqual ( '2024-01-01' ) ;
318+ expect ( tasks [ 0 ] . getChildren ( ) [ 0 ] . getDueDate ( ) ) . toEqual ( '2024-01-02' ) ;
319+ } ) ;
320+
321+ it ( 'filters incomplete children correctly' , ( ) => {
322+ sut = new EmojiTaskCollection (
323+ '## Header 1\n\n- [ ] Parent task\n\t- [x] Complete child\n\t- [ ] Incomplete child\n'
324+ ) ;
325+
326+ const tasks = sut . getAllTasks ( ) ;
327+ const parent = tasks [ 0 ] ;
328+ expect ( parent . getChildren ( ) . length ) . toEqual ( 2 ) ;
329+
330+ parent . filterIncompleteChildren ( ) ;
331+
332+ expect ( parent . getChildren ( ) . length ) . toEqual ( 1 ) ;
333+ expect ( parent . getChildren ( ) [ 0 ] . getName ( ) ) . toEqual ( 'Incomplete child' ) ;
334+ } ) ;
335+
336+ it ( 'filters incomplete children recursively' , ( ) => {
337+ sut = new EmojiTaskCollection (
338+ '## Header 1\n\n- [ ] Parent\n\t- [ ] Child\n\t\t- [x] Complete grandchild\n\t\t- [ ] Incomplete grandchild\n'
339+ ) ;
340+
341+ const tasks = sut . getAllTasks ( ) ;
342+ const parent = tasks [ 0 ] ;
343+
344+ parent . filterIncompleteChildren ( ) ;
345+
346+ expect ( parent . getChildren ( ) [ 0 ] . getChildren ( ) . length ) . toEqual ( 1 ) ;
347+ expect ( parent . getChildren ( ) [ 0 ] . getChildren ( ) [ 0 ] . getName ( ) ) . toEqual ( 'Incomplete grandchild' ) ;
348+ } ) ;
349+
350+ it ( 'marks children as carried over when parent is marked' , ( ) => {
351+ settings . carryOverPrefix = '[>]' ;
352+ sut = new EmojiTaskCollection ( '## Header 1\n\n- [ ] Parent task\n\t- [ ] Child task\n' ) ;
353+
354+ const tasks = sut . getAllTasks ( ) ;
355+ const parent = tasks [ 0 ] ;
356+ parent . markCarriedOver ( ) ;
357+
358+ const result = parent . toString ( ) ;
359+ expect ( result ) . toContain ( '- [ ] [>] Parent task' ) ;
360+ expect ( result ) . toContain ( '\t- [ ] [>] Child task' ) ;
361+ } ) ;
362+
363+ it ( 'resets indent levels when setIndentLevel is called' , ( ) => {
364+ sut = new EmojiTaskCollection (
365+ '## Header 1\n\n- [ ] Parent\n\t- [ ] Child\n\t\t- [ ] Grandchild\n'
366+ ) ;
367+
368+ const tasks = sut . getAllTasks ( ) ;
369+ const parent = tasks [ 0 ] ;
370+
371+ // Reset to level 0 (simulating carry-over)
372+ parent . setIndentLevel ( 0 ) ;
373+
374+ const result = parent . toString ( ) ;
375+ expect ( result ) . toContain ( '- [ ] Parent' ) ;
376+ expect ( result ) . toContain ( '\t- [ ] Child' ) ;
377+ expect ( result ) . toContain ( '\t\t- [ ] Grandchild' ) ;
378+ } ) ;
379+
380+ it ( 'preserves space indentation pattern when outputting' , ( ) => {
381+ // Use 2-space indentation (2 spaces = level 1, 4 spaces = level 2)
382+ sut = new EmojiTaskCollection (
383+ '## Header 1\n\n- [ ] Parent\n - [ ] Child level 1\n - [ ] Child level 2\n'
384+ ) ;
385+
386+ const tasks = sut . getAllTasks ( ) ;
387+ expect ( tasks . length ) . toEqual ( 1 ) ;
388+ expect ( tasks [ 0 ] . hasChildren ( ) ) . toBe ( true ) ;
389+
390+ const result = tasks [ 0 ] . toString ( ) ;
391+ // Should preserve the 2-space pattern
392+ expect ( result ) . toContain ( '- [ ] Parent' ) ;
393+ expect ( result ) . toContain ( ' - [ ] Child level 1' ) ;
394+ expect ( result ) . toContain ( ' - [ ] Child level 2' ) ;
395+ } ) ;
396+
397+ it ( 'handles orphaned sub-tasks when parent indent is skipped' , ( ) => {
398+ // Task at level 2 without a level 1 parent - should be added as top-level
399+ sut = new EmojiTaskCollection (
400+ '## Header 1\n\n- [ ] Parent\n\t\t- [ ] Orphaned child at level 2\n'
401+ ) ;
402+
403+ const tasks = sut . getAllTasks ( ) ;
404+ // The orphaned task should be added as a top-level task since there's no level 1 parent
405+ expect ( tasks . length ) . toEqual ( 2 ) ;
406+ expect ( tasks [ 0 ] . getName ( ) ) . toEqual ( 'Parent' ) ;
407+ expect ( tasks [ 1 ] . getName ( ) ) . toEqual ( 'Orphaned child at level 2' ) ;
408+ } ) ;
409+
410+ it ( 'handles task with due date in toString when dueDate is set' , ( ) => {
411+ sut = new EmojiTaskCollection ( '## Header 1\n\n- [ ] Task with due 📅 2024-01-15\n' ) ;
412+
413+ const tasks = sut . getAllTasks ( ) ;
414+ const task = tasks [ 0 ] ;
415+
416+ // Call isDue to set the dueDate property
417+ task . isDue ( ) ;
418+
419+ const result = task . toString ( ) ;
420+ expect ( result ) . toContain ( '📅 2024-01-15' ) ;
421+ } ) ;
422+ } ) ;
244423} ) ;
0 commit comments