Repository: obsidian-tasks-group/obsidian-tasks
Pull request #3109 (2024-10-05 08:49:35)

refactor: test renderering descriptions
# Types of changes

Internal changes:

- [x] **Refactor** (prefix: `refactor` - non-breaking change which only improves the design or structure of existing code, and making no changes to its external behaviour)
- [x] **Tests** (prefix: `test` - additions and improvements to unit tests and the smoke tests)

## Description

Done by pairing with @claremacrae 

- Allow `QueryResultsRenderer.test.ts` to visualise task descriptions

## Motivation and Context

- Preparation for later parent-child test rendering

## How has this been tested?

- automated tests

## Checklist

- [x] My code follows the code style of this project and passes `yarn run lint`.
- [x] My change has adequate [Unit Test coverage](https://publish.obsidian.md/tasks-contributing/Testing/About+Testing).

## Terms

- [x] My contribution follows this project's [contributing guide](https://publish.obsidian.md/tasks-contributing)
- [x] I agree to follow this project's [Code of Conduct](https://github.com/obsidian-tasks-group/obsidian-tasks/blob/main/CODE_OF_CONDUCT.md)



Patch:
diff --git a/src/Renderer/QueryResultsRenderer.ts b/src/Renderer/QueryResultsRenderer.ts
index 6f541c8b..338e4f33 100644
--- a/src/Renderer/QueryResultsRenderer.ts
+++ b/src/Renderer/QueryResultsRenderer.ts
@@ -14,7 +14,7 @@ import { postponeButtonTitle, shouldShowPostponeButton } from '../DateTime/Postp
 import type { TasksFile } from '../Scripting/TasksFile';
 import type { Task } from '../Task/Task';
 import { PostponeMenu } from '../ui/Menus/PostponeMenu';
-import { TaskLineRenderer, createAndAppendElement } from './TaskLineRenderer';
+import { TaskLineRenderer, type TextRenderer, createAndAppendElement } from './TaskLineRenderer';
 
 type BacklinksEventHandler = (ev: MouseEvent, task: Task) => Promise<void>;
 type EditButtonClickHandler = (event: MouseEvent, task: Task, allTasks: Task[]) => void;
@@ -46,6 +46,9 @@ export class QueryResultsRenderer {
     public query: IQuery;
     protected queryType: string; // whilst there is only one query type, there is no point logging this value
 
+    // Renders the description in TaskLineRenderer:
+    private readonly textRenderer;
+    // Renders the group heading in this class:
     private readonly renderMarkdown;
     private readonly obsidianComponent: Component | null;
 
@@ -55,11 +58,13 @@ export class QueryResultsRenderer {
         tasksFile: TasksFile,
         renderMarkdown: (markdown: string, el: HTMLElement, sourcePath: string, component: Component) => Promise<void>,
         obsidianComponent: Component | null,
+        textRenderer: TextRenderer = TaskLineRenderer.obsidianMarkdownRenderer,
     ) {
         this.source = source;
         this.tasksFile = tasksFile;
         this.renderMarkdown = renderMarkdown;
         this.obsidianComponent = obsidianComponent;
+        this.textRenderer = textRenderer;
 
         // The engine is chosen on the basis of the code block language. Currently,
         // there is only the main engine for the plugin, this allows others to be
@@ -204,6 +209,7 @@ export class QueryResultsRenderer {
         if (groupingAttribute && groupingAttribute.length > 0) taskList.dataset.taskGroupBy = groupingAttribute;
 
         const taskLineRenderer = new TaskLineRenderer({
+            textRenderer: this.textRenderer,
             obsidianComponent: this.obsidianComponent,
             parentUlElement: taskList,
             taskLayoutOptions: this.query.taskLayoutOptions,
diff --git a/src/Renderer/TaskLineRenderer.ts b/src/Renderer/TaskLineRenderer.ts
index 78b34f41..c0854f70 100644
--- a/src/Renderer/TaskLineRenderer.ts
+++ b/src/Renderer/TaskLineRenderer.ts
@@ -59,7 +59,7 @@ export class TaskLineRenderer {
     private readonly taskLayoutOptions: TaskLayoutOptions;
     private readonly queryLayoutOptions: QueryLayoutOptions;
 
-    private static async obsidianMarkdownRenderer(
+    public static async obsidianMarkdownRenderer(
         text: string,
         element: HTMLSpanElement,
         path: string,


Test patch:
diff --git a/resources/sample_vaults/Tasks-Demo/Test Data/inheritance_rendering_sample.md b/resources/sample_vaults/Tasks-Demo/Test Data/inheritance_rendering_sample.md
new file mode 100644
index 00000000..6753633e
--- /dev/null
+++ b/resources/sample_vaults/Tasks-Demo/Test Data/inheritance_rendering_sample.md
@@ -0,0 +1,9 @@
+-  grandparent list item
+    - [ ] parent 1
+        - [ ] child 1
+            - [ ] grandchild 1
+                - list item grand grand child
+        - [ ] child 2
+            - non task grandchild
+                - [ ] grand grand child
+    - [ ] parent 2
diff --git a/tests/Obsidian/AllCacheSampleData.ts b/tests/Obsidian/AllCacheSampleData.ts
index 05c8ddaa..e91e4d9e 100644
--- a/tests/Obsidian/AllCacheSampleData.ts
+++ b/tests/Obsidian/AllCacheSampleData.ts
@@ -26,6 +26,7 @@ import { inheritance_2siblings } from './__test_data__/inheritance_2siblings';
 import { inheritance_listitem_listitem_task } from './__test_data__/inheritance_listitem_listitem_task';
 import { inheritance_listitem_task } from './__test_data__/inheritance_listitem_task';
 import { inheritance_listitem_task_siblings } from './__test_data__/inheritance_listitem_task_siblings';
+import { inheritance_rendering_sample } from './__test_data__/inheritance_rendering_sample';
 import { inheritance_task_2listitem_3task } from './__test_data__/inheritance_task_2listitem_3task';
 import { inheritance_task_listitem } from './__test_data__/inheritance_task_listitem';
 import { inheritance_task_listitem_mixed_grandchildren } from './__test_data__/inheritance_task_listitem_mixed_grandchildren';
@@ -89,6 +90,7 @@ export function allCacheSampleData() {
         inheritance_listitem_listitem_task,
         inheritance_listitem_task,
         inheritance_listitem_task_siblings,
+        inheritance_rendering_sample,
         inheritance_task_2listitem_3task,
         inheritance_task_listitem,
         inheritance_task_listitem_mixed_grandchildren,
diff --git a/tests/Obsidian/__test_data__/inheritance_rendering_sample.ts b/tests/Obsidian/__test_data__/inheritance_rendering_sample.ts
new file mode 100644
index 00000000..d051a353
--- /dev/null
+++ b/tests/Obsidian/__test_data__/inheritance_rendering_sample.ts
@@ -0,0 +1,178 @@
+export const inheritance_rendering_sample = {
+    filePath: 'Test Data/inheritance_rendering_sample.md',
+    fileContents:
+        '-  grandparent list item\n' +
+        '    - [ ] parent 1\n' +
+        '        - [ ] child 1\n' +
+        '            - [ ] grandchild 1\n' +
+        '                - list item grand grand child\n' +
+        '        - [ ] child 2\n' +
+        '            - non task grandchild\n' +
+        '                - [ ] grand grand child\n' +
+        '    - [ ] parent 2\n',
+    cachedMetadata: {
+        sections: [
+            {
+                type: 'list',
+                position: {
+                    start: {
+                        line: 0,
+                        col: 0,
+                        offset: 0,
+                    },
+                    end: {
+                        line: 8,
+                        col: 18,
+                        offset: 257,
+                    },
+                },
+            },
+        ],
+        listItems: [
+            {
+                position: {
+                    start: {
+                        line: 0,
+                        col: 0,
+                        offset: 0,
+                    },
+                    end: {
+                        line: 0,
+                        col: 24,
+                        offset: 24,
+                    },
+                },
+                parent: -1,
+            },
+            {
+                position: {
+                    start: {
+                        line: 1,
+                        col: 3,
+                        offset: 28,
+                    },
+                    end: {
+                        line: 1,
+                        col: 18,
+                        offset: 43,
+                    },
+                },
+                parent: 0,
+                task: ' ',
+            },
+            {
+                position: {
+                    start: {
+                        line: 2,
+                        col: 6,
+                        offset: 50,
+                    },
+                    end: {
+                        line: 2,
+                        col: 21,
+                        offset: 65,
+                    },
+                },
+                parent: 1,
+                task: ' ',
+            },
+            {
+                position: {
+                    start: {
+                        line: 3,
+                        col: 10,
+                        offset: 76,
+                    },
+                    end: {
+                        line: 3,
+                        col: 30,
+                        offset: 96,
+                    },
+                },
+                parent: 2,
+                task: ' ',
+            },
+            {
+                position: {
+                    start: {
+                        line: 4,
+                        col: 14,
+                        offset: 111,
+                    },
+                    end: {
+                        line: 4,
+                        col: 45,
+                        offset: 142,
+                    },
+                },
+                parent: 3,
+            },
+            {
+                position: {
+                    start: {
+                        line: 5,
+                        col: 6,
+                        offset: 149,
+                    },
+                    end: {
+                        line: 5,
+                        col: 21,
+                        offset: 164,
+                    },
+                },
+                parent: 1,
+                task: ' ',
+            },
+            {
+                position: {
+                    start: {
+                        line: 6,
+                        col: 10,
+                        offset: 175,
+                    },
+                    end: {
+                        line: 6,
+                        col: 33,
+                        offset: 198,
+                    },
+                },
+                parent: 5,
+            },
+            {
+                position: {
+                    start: {
+                        line: 7,
+                        col: 14,
+                        offset: 213,
+                    },
+                    end: {
+                        line: 7,
+                        col: 39,
+                        offset: 238,
+                    },
+                },
+                parent: 6,
+                task: ' ',
+            },
+            {
+                position: {
+                    start: {
+                        line: 8,
+                        col: 3,
+                        offset: 242,
+                    },
+                    end: {
+                        line: 8,
+                        col: 18,
+                        offset: 257,
+                    },
+                },
+                parent: 0,
+                task: ' ',
+            },
+        ],
+    },
+    obsidianApiVersion: '1.7.3',
+    getAllTags: [],
+    parseFrontMatterTags: null,
+};
diff --git a/tests/Renderer/QueryResultsRenderer.test.QueryResultsRenderer_tests_fully_populated_task.approved.html b/tests/Renderer/QueryResultsRenderer.test.QueryResultsRenderer_tests_fully_populated_task.approved.html
index cf058098..8693b924 100644
--- a/tests/Renderer/QueryResultsRenderer.test.QueryResultsRenderer_tests_fully_populated_task.approved.html
+++ b/tests/Renderer/QueryResultsRenderer.test.QueryResultsRenderer_tests_fully_populated_task.approved.html
@@ -15,7 +15,7 @@
       data-task-status-type="TODO">
       <input class="task-list-item-checkbox" type="checkbox" title="Right-click for options" data-line="0" />
       <span class="tasks-list-text">
-        <span class="task-description"><span></span></span>
+        <span class="task-description"><span>Do exercises #todo #health</span></span>
         <span class="task-id"><span>🆔 abcdef</span></span>
         <span class="task-dependsOn"><span>⛔ 123456,abc123</span></span>
         <span class="task-priority" data-task-priority="medium"><span>🔼</span></span>
diff --git a/tests/Renderer/QueryResultsRenderer.test.QueryResultsRenderer_tests_parent-child_items.approved.html b/tests/Renderer/QueryResultsRenderer.test.QueryResultsRenderer_tests_parent-child_items.approved.html
new file mode 100644
index 00000000..78627a4a
--- /dev/null
+++ b/tests/Renderer/QueryResultsRenderer.test.QueryResultsRenderer_tests_parent-child_items.approved.html
@@ -0,0 +1,125 @@
+<div>
+  <ul class="contains-task-list plugin-tasks-query-result tasks-layout-hide-urgency">
+    <li
+      class="task-list-item plugin-tasks-list-item"
+      data-task-priority="normal"
+      data-task=""
+      data-line="0"
+      data-task-status-name="Todo"
+      data-task-status-type="TODO">
+      <input class="task-list-item-checkbox" type="checkbox" title="Right-click for options" data-line="0" />
+      <span class="tasks-list-text">
+        <span class="task-description"><span>parent 1</span></span>
+      </span>
+      <span class="task-extras">
+        <span class="tasks-backlink">
+          (
+          <a rel="noopener" target="_blank" class="internal-link">inheritance_rendering_sample</a>
+          )
+        </span>
+        <a class="tasks-edit" title="Edit task" href="#"></a>
+      </span>
+    </li>
+    <li
+      class="task-list-item plugin-tasks-list-item"
+      data-task-priority="normal"
+      data-task=""
+      data-line="1"
+      data-task-status-name="Todo"
+      data-task-status-type="TODO">
+      <input class="task-list-item-checkbox" type="checkbox" title="Right-click for options" data-line="1" />
+      <span class="tasks-list-text">
+        <span class="task-description"><span>child 1</span></span>
+      </span>
+      <span class="task-extras">
+        <span class="tasks-backlink">
+          (
+          <a rel="noopener" target="_blank" class="internal-link">inheritance_rendering_sample</a>
+          )
+        </span>
+        <a class="tasks-edit" title="Edit task" href="#"></a>
+      </span>
+    </li>
+    <li
+      class="task-list-item plugin-tasks-list-item"
+      data-task-priority="normal"
+      data-task=""
+      data-line="2"
+      data-task-status-name="Todo"
+      data-task-status-type="TODO">
+      <input class="task-list-item-checkbox" type="checkbox" title="Right-click for options" data-line="2" />
+      <span class="tasks-list-text">
+        <span class="task-description"><span>grandchild 1</span></span>
+      </span>
+      <span class="task-extras">
+        <span class="tasks-backlink">
+          (
+          <a rel="noopener" target="_blank" class="internal-link">inheritance_rendering_sample</a>
+          )
+        </span>
+        <a class="tasks-edit" title="Edit task" href="#"></a>
+      </span>
+    </li>
+    <li
+      class="task-list-item plugin-tasks-list-item"
+      data-task-priority="normal"
+      data-task=""
+      data-line="3"
+      data-task-status-name="Todo"
+      data-task-status-type="TODO">
+      <input class="task-list-item-checkbox" type="checkbox" title="Right-click for options" data-line="3" />
+      <span class="tasks-list-text">
+        <span class="task-description"><span>child 2</span></span>
+      </span>
+      <span class="task-extras">
+        <span class="tasks-backlink">
+          (
+          <a rel="noopener" target="_blank" class="internal-link">inheritance_rendering_sample</a>
+          )
+        </span>
+        <a class="tasks-edit" title="Edit task" href="#"></a>
+      </span>
+    </li>
+    <li
+      class="task-list-item plugin-tasks-list-item"
+      data-task-priority="normal"
+      data-task=""
+      data-line="4"
+      data-task-status-name="Todo"
+      data-task-status-type="TODO">
+      <input class="task-list-item-checkbox" type="checkbox" title="Right-click for options" data-line="4" />
+      <span class="tasks-list-text">
+        <span class="task-description"><span>grand grand child</span></span>
+      </span>
+      <span class="task-extras">
+        <span class="tasks-backlink">
+          (
+          <a rel="noopener" target="_blank" class="internal-link">inheritance_rendering_sample</a>
+          )
+        </span>
+        <a class="tasks-edit" title="Edit task" href="#"></a>
+      </span>
+    </li>
+    <li
+      class="task-list-item plugin-tasks-list-item"
+      data-task-priority="normal"
+      data-task=""
+      data-line="5"
+      data-task-status-name="Todo"
+      data-task-status-type="TODO">
+      <input class="task-list-item-checkbox" type="checkbox" title="Right-click for options" data-line="5" />
+      <span class="tasks-list-text">
+        <span class="task-description"><span>parent 2</span></span>
+      </span>
+      <span class="task-extras">
+        <span class="tasks-backlink">
+          (
+          <a rel="noopener" target="_blank" class="internal-link">inheritance_rendering_sample</a>
+          )
+        </span>
+        <a class="tasks-edit" title="Edit task" href="#"></a>
+      </span>
+    </li>
+  </ul>
+  <div class="task-count">6 tasks</div>
+</div>
diff --git a/tests/Renderer/QueryResultsRenderer.test.ts b/tests/Renderer/QueryResultsRenderer.test.ts
index 9865b7db..70eed720 100644
--- a/tests/Renderer/QueryResultsRenderer.test.ts
+++ b/tests/Renderer/QueryResultsRenderer.test.ts
@@ -2,12 +2,16 @@
  * @jest-environment jsdom
  */
 import moment from 'moment';
+import type { Task } from 'Task/Task';
 import { State } from '../../src/Obsidian/Cache';
 import { QueryResultsRenderer } from '../../src/Renderer/QueryResultsRenderer';
 import { TasksFile } from '../../src/Scripting/TasksFile';
+import { inheritance_rendering_sample } from '../Obsidian/__test_data__/inheritance_rendering_sample';
+import { readTasksFromSimulatedFile } from '../Obsidian/SimulatedFile';
 import { verifyWithFileExtension } from '../TestingTools/ApprovalTestHelpers';
 import { prettifyHTML } from '../TestingTools/HTMLHelpers';
 import { TaskBuilder } from '../TestingTools/TaskBuilder';
+import { mockHTMLRenderer } from './RenderingTestHelpers';
 
 window.moment = moment;
 
@@ -21,15 +25,15 @@ afterEach(() => {
 });
 
 describe('QueryResultsRenderer tests', () => {
-    it('fully populated task', async () => {
+    async function verifyRenderedTasksHTML(allTasks: Task[]) {
         const renderer = new QueryResultsRenderer(
             'block-language-tasks',
             '',
             new TasksFile('query.md'),
             () => Promise.resolve(),
             null,
+            mockHTMLRenderer,
         );
-        const allTasks = [TaskBuilder.createFullyPopulatedTask()];
         const queryRendererParameters = {
             allTasks,
             allMarkdownFiles: [],
@@ -43,5 +47,15 @@ describe('QueryResultsRenderer tests', () => {
 
         const prettyHTML = prettifyHTML(container.outerHTML);
         verifyWithFileExtension(prettyHTML, 'html');
+    }
+
+    it('fully populated task', async () => {
+        const allTasks = [TaskBuilder.createFullyPopulatedTask()];
+        await verifyRenderedTasksHTML(allTasks);
+    });
+
+    it('parent-child items', async () => {
+        const allTasks = readTasksFromSimulatedFile(inheritance_rendering_sample);
+        await verifyRenderedTasksHTML(allTasks);
     });
 });
diff --git a/tests/Renderer/RenderingTestHelpers.ts b/tests/Renderer/RenderingTestHelpers.ts
new file mode 100644
index 00000000..25d6df40
--- /dev/null
+++ b/tests/Renderer/RenderingTestHelpers.ts
@@ -0,0 +1,11 @@
+export const mockHTMLRenderer = async (text: string, element: HTMLSpanElement, _path: string) => {
+    // Contrary to the default mockTextRenderer(),
+    // instead of the rendered HTMLSpanElement.innerText,
+    // we need the plain HTML here like in TaskLineRenderer.renderComponentText(),
+    // to ensure that description and tags are retained.
+    element.innerHTML = text;
+};
+
+export const mockTextRenderer = async (text: string, element: HTMLSpanElement, _path: string) => {
+    element.innerText = text;
+};
diff --git a/tests/Renderer/TaskLineRenderer.test.ts b/tests/Renderer/TaskLineRenderer.test.ts
index 85824acd..acbdaf29 100644
--- a/tests/Renderer/TaskLineRenderer.test.ts
+++ b/tests/Renderer/TaskLineRenderer.test.ts
@@ -17,6 +17,7 @@ import { verifyWithFileExtension } from '../TestingTools/ApprovalTestHelpers';
 import { prettifyHTML } from '../TestingTools/HTMLHelpers';
 import { TaskBuilder } from '../TestingTools/TaskBuilder';
 import { fromLine } from '../TestingTools/TestHelpers';
+import { mockHTMLRenderer, mockTextRenderer } from './RenderingTestHelpers';
 
 jest.mock('obsidian');
 window.moment = moment;
@@ -48,18 +49,6 @@ async function renderListItem(
     return await taskLineRenderer.renderTaskLine(task, 0);
 }
 
-const mockTextRenderer = async (text: string, element: HTMLSpanElement, _path: string) => {
-    element.innerText = text;
-};
-
-const mockHTMLRenderer = async (text: string, element: HTMLSpanElement, _path: string) => {
-    // Contrary to the default mockTextRenderer(),
-    // instead of the rendered HTMLSpanElement.innerText,
-    // we need the plain HTML here like in TaskLineRenderer.renderComponentText(),
-    // to ensure that description and tags are retained.
-    element.innerHTML = text;
-};
-
 function getTextSpan(listItem: HTMLElement) {
     return listItem.children[1] as HTMLSpanElement;
 }