Repository: obsidian-tasks-group/obsidian-tasks
Pull request #2516 (2023-12-18 14:07:24)

fix: Cycle through statuses until TODO is found when marking recurring tasks as DONE
# Description
I looped through the statuses until a TODO status is found. I realize that there's a possibility of getting stuck in an infinite loop, another approach could be to loop for the length of the registered statuses and fallback if no appropriate status is found.

## Motivation and Context
Issue #2304 

## How has this been tested?
I wrote a test in `Commands/ToggleDone.test.ts` (not sure if this is the appropriate file to add the test to)

## Screenshots (if appropriate)

## Types of changes

Changes visible to users:

- [x] **Bug fix** (prefix: `fix` - non-breaking change which fixes an issue)
- [ ] **New feature** (prefix: `feat` - non-breaking change which adds functionality)
- [ ] **Breaking change** (prefix: `feat!!` or `fix!!` - fix or feature that would cause existing functionality to not work as expected)
- [ ] **Documentation** (prefix: `docs` - improvements to any documentation content **for users**)
- [ ] **Sample vault** (prefix: `vault` - improvements to the [Tasks-Demo sample vault](https://github.com/obsidian-tasks-group/obsidian-tasks/tree/main/resources/sample_vaults/Tasks-Demo))
- [ ] **Contributing Guidelines** (prefix: `contrib` - any improvements to documentation content **for contributors** - see [Contributing to Tasks](https://publish.obsidian.md/tasks-contributing/))

Internal changes:

- [ ] **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)
- [ ] **Infrastructure** (prefix: `chore` - examples include GitHub Actions, issue templates)

## Checklist
- [x] My code follows the code style of this project and passes `yarn run lint`.
- [ ] My change requires a change to the documentation.
- [ ] I have [updated the documentation](https://publish.obsidian.md/tasks-contributing/Documentation/About+Documentation) accordingly.
- [x] My change has adequate [Unit Test coverage](https://publish.obsidian.md/tasks-contributing/Testing/About+Testing).

## Terms

<!--
By submitting this pull request, you must agree to follow our
[contributing guide](https://publish.obsidian.md/tasks-contributing) and
[Code of Conduct](https://github.com/obsidian-tasks-group/obsidian-tasks/blob/main/CODE_OF_CONDUCT.md).
Put an x in the boxes to confirm you agree.
-->

- [x] My contribution follow 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/IQuery.ts b/src/IQuery.ts
index 5e7d1ef1..a1e29b36 100644
--- a/src/IQuery.ts
+++ b/src/IQuery.ts
@@ -1,3 +1,4 @@
+import type { QueryLayoutOptions } from './QueryLayoutOptions';
 import type { LayoutOptions } from './TaskLayout';
 import type { Task } from './Task';
 import type { Grouper } from './Query/Grouper';
@@ -48,6 +49,15 @@ export interface IQuery {
      */
     layoutOptions: LayoutOptions;
 
+    /**
+     * Any layout options the query engine should be aware of or
+     * used in the query.
+     *
+     * @type {QueryLayoutOptions}
+     * @memberof IQuery
+     */
+    queryLayoutOptions: QueryLayoutOptions;
+
     /**
      * Main method for executing the query. This will be called by the
      * code block processor registered in Obsidian. It takes the Task collection
diff --git a/src/InlineRenderer.ts b/src/InlineRenderer.ts
index 75941898..eff1f58a 100644
--- a/src/InlineRenderer.ts
+++ b/src/InlineRenderer.ts
@@ -1,6 +1,7 @@
 import type { MarkdownPostProcessorContext, Plugin } from 'obsidian';
 import { MarkdownRenderChild } from 'obsidian';
 import { GlobalFilter } from './Config/GlobalFilter';
+import { QueryLayoutOptions } from './QueryLayoutOptions';
 import { Task } from './Task';
 import { LayoutOptions } from './TaskLayout';
 import { TaskLineRenderer } from './TaskLineRenderer';
@@ -91,6 +92,7 @@ export class InlineRenderer {
             obsidianComponent: childComponent,
             parentUlElement: element,
             layoutOptions: new LayoutOptions(),
+            queryLayoutOptions: new QueryLayoutOptions(),
         });
 
         // The section index is the nth task within this section.
diff --git a/src/Query/Query.ts b/src/Query/Query.ts
index e4ff014e..c98c536d 100644
--- a/src/Query/Query.ts
+++ b/src/Query/Query.ts
@@ -1,3 +1,4 @@
+import { QueryLayoutOptions } from '../QueryLayoutOptions';
 import { expandPlaceholders } from '../Scripting/ExpandPlaceholders';
 import { makeQueryContext } from '../Scripting/QueryContext';
 import { LayoutOptions } from '../TaskLayout';
@@ -24,6 +25,7 @@ export class Query implements IQuery {
     private _limit: number | undefined = undefined;
     private _taskGroupLimit: number | undefined = undefined;
     private _layoutOptions: LayoutOptions = new LayoutOptions();
+    private _queryLayoutOptions: QueryLayoutOptions = new QueryLayoutOptions();
     private _filters: Filter[] = [];
     private _error: string | undefined = undefined;
     private _sorting: Sorter[] = [];
@@ -61,10 +63,10 @@ export class Query implements IQuery {
 
             switch (true) {
                 case this.shortModeRegexp.test(line):
-                    this._layoutOptions.shortMode = true;
+                    this._queryLayoutOptions.shortMode = true;
                     break;
                 case this.explainQueryRegexp.test(line):
-                    this._layoutOptions.explainQuery = true;
+                    this._queryLayoutOptions.explainQuery = true;
                     break;
                 case this.ignoreGlobalQueryRegexp.test(line):
                     this._ignoreGlobalQuery = true;
@@ -217,6 +219,10 @@ ${source}`;
         return this._layoutOptions;
     }
 
+    public get queryLayoutOptions(): QueryLayoutOptions {
+        return this._queryLayoutOptions;
+    }
+
     public get filters(): Filter[] {
         return this._filters;
     }
@@ -290,13 +296,13 @@ Problem line: "${line}"`;
 
             switch (option) {
                 case 'task count':
-                    this._layoutOptions.hideTaskCount = hide;
+                    this._queryLayoutOptions.hideTaskCount = hide;
                     break;
                 case 'backlink':
-                    this._layoutOptions.hideBacklinks = hide;
+                    this._queryLayoutOptions.hideBacklinks = hide;
                     break;
                 case 'postpone button':
-                    this._layoutOptions.hidePostponeButton = hide;
+                    this._queryLayoutOptions.hidePostponeButton = hide;
                     break;
                 case 'priority':
                     this._layoutOptions.hidePriority = hide;
@@ -320,10 +326,10 @@ Problem line: "${line}"`;
                     this._layoutOptions.hideRecurrenceRule = hide;
                     break;
                 case 'edit button':
-                    this._layoutOptions.hideEditButton = hide;
+                    this._queryLayoutOptions.hideEditButton = hide;
                     break;
                 case 'urgency':
-                    this._layoutOptions.hideUrgency = hide;
+                    this._queryLayoutOptions.hideUrgency = hide;
                     break;
                 case 'tags':
                     this._layoutOptions.hideTags = hide;
diff --git a/src/QueryLayoutOptions.ts b/src/QueryLayoutOptions.ts
new file mode 100644
index 00000000..7eb5711b
--- /dev/null
+++ b/src/QueryLayoutOptions.ts
@@ -0,0 +1,14 @@
+/**
+ * Options to control {@link QueryRenderer} behaviour.
+ *
+ * @see LayoutOptions
+ */
+export class QueryLayoutOptions {
+    hidePostponeButton: boolean = false;
+    hideTaskCount: boolean = false;
+    hideBacklinks: boolean = false;
+    hideEditButton: boolean = false;
+    hideUrgency: boolean = true;
+    shortMode: boolean = false;
+    explainQuery: boolean = false;
+}
diff --git a/src/QueryRenderer.ts b/src/QueryRenderer.ts
index cea019ce..21f23f9c 100644
--- a/src/QueryRenderer.ts
+++ b/src/QueryRenderer.ts
@@ -178,7 +178,7 @@ class QueryRenderChild extends MarkdownRenderChild {
         // See https://github.com/obsidian-tasks-group/obsidian-tasks/issues/2160
         this.query.debug(`[render] Render called: plugin state: ${state}; searching ${tasks.length} tasks`);
 
-        if (this.query.layoutOptions.explainQuery) {
+        if (this.query.queryLayoutOptions.explainQuery) {
             this.createExplanation(content);
         }
 
@@ -221,7 +221,7 @@ class QueryRenderChild extends MarkdownRenderChild {
     }
 
     private async createTaskList(tasks: Task[], content: HTMLDivElement): Promise<void> {
-        const layout = new TaskLayout(this.query.layoutOptions);
+        const layout = new TaskLayout(this.query.layoutOptions, this.query.queryLayoutOptions);
         const taskList = content.createEl('ul');
         taskList.addClasses(['contains-task-list', 'plugin-tasks-query-result']);
         taskList.addClasses(layout.taskListHiddenClasses);
@@ -232,6 +232,7 @@ class QueryRenderChild extends MarkdownRenderChild {
             obsidianComponent: this,
             parentUlElement: taskList,
             layoutOptions: this.query.layoutOptions,
+            queryLayoutOptions: this.query.queryLayoutOptions,
         });
 
         for (const [taskIndex, task] of tasks.entries()) {
@@ -244,21 +245,21 @@ class QueryRenderChild extends MarkdownRenderChild {
 
             const extrasSpan = listItem.createSpan('task-extras');
 
-            if (!this.query.layoutOptions.hideUrgency) {
+            if (!this.query.queryLayoutOptions.hideUrgency) {
                 this.addUrgency(extrasSpan, task);
             }
 
-            const shortMode = this.query.layoutOptions.shortMode;
+            const shortMode = this.query.queryLayoutOptions.shortMode;
 
-            if (!this.query.layoutOptions.hideBacklinks) {
+            if (!this.query.queryLayoutOptions.hideBacklinks) {
                 this.addBacklinks(extrasSpan, task, shortMode, isFilenameUnique);
             }
 
-            if (!this.query.layoutOptions.hideEditButton) {
+            if (!this.query.queryLayoutOptions.hideEditButton) {
                 this.addEditButton(extrasSpan, task);
             }
 
-            if (!this.query.layoutOptions.hidePostponeButton && shouldShowPostponeButton(task)) {
+            if (!this.query.queryLayoutOptions.hidePostponeButton && shouldShowPostponeButton(task)) {
                 this.addPostponeButton(extrasSpan, task, shortMode);
             }
 
@@ -420,12 +421,17 @@ class QueryRenderChild extends MarkdownRenderChild {
         button.addClasses(classNames);
         button.setText(' ⏩');
 
-        button.addEventListener('click', () => this.postponeOnClickCallback(button, task, amount, timeUnit));
+        button.addEventListener('click', (ev: MouseEvent) => {
+            ev.preventDefault(); // suppress the default click behavior
+            this.postponeOnClickCallback(button, task, amount, timeUnit);
+        });
 
         /** Open a context menu on right-click.
          * Give a choice of postponing for a week, month, or quarter.
          */
         button.addEventListener('contextmenu', async (ev: MouseEvent) => {
+            ev.stopPropagation(); // suppress the default context menu
+
             const menu = new Menu();
 
             const postponeMenuItemCallback = (item: MenuItem, timeUnit: unitOfTime.DurationConstructor, amount = 1) => {
@@ -449,7 +455,7 @@ class QueryRenderChild extends MarkdownRenderChild {
     }
 
     private addTaskCount(content: HTMLDivElement, queryResult: QueryResult) {
-        if (!this.query.layoutOptions.hideTaskCount) {
+        if (!this.query.queryLayoutOptions.hideTaskCount) {
             content.createDiv({
                 text: queryResult.totalTasksCountDisplayText(),
                 cls: 'tasks-count',
diff --git a/src/Task.ts b/src/Task.ts
index 7f3b64ed..5563ecc9 100644
--- a/src/Task.ts
+++ b/src/Task.ts
@@ -428,7 +428,18 @@ export class Task {
             if (setCreatedDate) {
                 createdDate = window.moment();
             }
-            const nextStatus = StatusRegistry.getInstance().getNextStatusOrCreate(newStatus);
+            const statusRegistry = StatusRegistry.getInstance();
+            let nextStatus = statusRegistry.getNextStatusOrCreate(newStatus);
+            if (nextStatus.type !== StatusType.TODO) {
+                let searchStatus = nextStatus;
+                for (let i = 0; i < statusRegistry.registeredStatuses.length - 1; i++) {
+                    searchStatus = statusRegistry.getNextStatusOrCreate(searchStatus);
+                    if (searchStatus.type === StatusType.TODO) {
+                        nextStatus = searchStatus;
+                        break;
+                    }
+                }
+            }
             const nextTask = new Task({
                 ...this,
                 ...nextOccurrence,
@@ -467,7 +478,6 @@ export class Task {
      */
     public toggleWithRecurrenceInUsersOrder(): Task[] {
         const newTasks = this.toggle();
-
         const { recurrenceOnNextLine: recurrenceOnNextLine } = getSettings();
         return recurrenceOnNextLine ? newTasks.reverse() : newTasks;
     }
diff --git a/src/TaskLayout.ts b/src/TaskLayout.ts
index b840ff8f..e3977fcb 100644
--- a/src/TaskLayout.ts
+++ b/src/TaskLayout.ts
@@ -1,11 +1,12 @@
+import { QueryLayoutOptions } from './QueryLayoutOptions';
+
 /**
- * Various rendering options for a query.
+ * Various rendering options of tasks in a query.
  * See applyOptions below when adding options here.
+ *
+ * @see QueryLayoutOptions
  */
 export class LayoutOptions {
-    hidePostponeButton: boolean = false;
-    hideTaskCount: boolean = false;
-    hideBacklinks: boolean = false;
     hidePriority: boolean = false;
     hideCreatedDate: boolean = false;
     hideStartDate: boolean = false;
@@ -13,11 +14,7 @@ export class LayoutOptions {
     hideDoneDate: boolean = false;
     hideDueDate: boolean = false;
     hideRecurrenceRule: boolean = false;
-    hideEditButton: boolean = false;
-    hideUrgency: boolean = true;
     hideTags: boolean = false;
-    shortMode: boolean = false;
-    explainQuery: boolean = false;
 }
 
 export type TaskLayoutComponent =
@@ -53,14 +50,21 @@ export class TaskLayout {
     public shownTaskLayoutComponents: TaskLayoutComponent[];
     public hiddenTaskLayoutComponents: TaskLayoutComponent[] = [];
     public options: LayoutOptions;
+    public queryLayoutOptions: QueryLayoutOptions;
     public taskListHiddenClasses: string[] = [];
 
-    constructor(options?: LayoutOptions) {
+    constructor(options?: LayoutOptions, queryLayoutOptions?: QueryLayoutOptions) {
         if (options) {
             this.options = options;
         } else {
             this.options = new LayoutOptions();
         }
+        if (queryLayoutOptions) {
+            this.queryLayoutOptions = queryLayoutOptions;
+        } else {
+            this.queryLayoutOptions = new QueryLayoutOptions();
+        }
+
         this.shownTaskLayoutComponents = this.defaultLayout;
         this.applyOptions();
     }
@@ -92,16 +96,16 @@ export class TaskLayout {
             // (see https://github.com/obsidian-tasks-group/obsidian-tasks/issues/1866).
             // This can benefit from some refactoring, i.e. render these components in a similar flow rather than
             // separately.
-            [this.options.hideUrgency, 'urgency'],
-            [this.options.hideBacklinks, 'backlinks'],
-            [this.options.hideEditButton, 'edit-button'],
-            [this.options.hidePostponeButton, 'postpone-button'],
+            [this.queryLayoutOptions.hideUrgency, 'urgency'],
+            [this.queryLayoutOptions.hideBacklinks, 'backlinks'],
+            [this.queryLayoutOptions.hideEditButton, 'edit-button'],
+            [this.queryLayoutOptions.hidePostponeButton, 'postpone-button'],
         ];
         for (const [hide, component] of componentsToGenerateClassesOnly) {
             this.generateHiddenClassForTaskList(hide, component);
         }
 
-        if (this.options.shortMode) this.taskListHiddenClasses.push('tasks-layout-short-mode');
+        if (this.queryLayoutOptions.shortMode) this.taskListHiddenClasses.push('tasks-layout-short-mode');
     }
 
     private generateHiddenClassForTaskList(hide: boolean, component: string) {
diff --git a/src/TaskLineRenderer.ts b/src/TaskLineRenderer.ts
index b942565b..dcc61a35 100644
--- a/src/TaskLineRenderer.ts
+++ b/src/TaskLineRenderer.ts
@@ -3,6 +3,7 @@ import { Component, MarkdownRenderer } from 'obsidian';
 import { GlobalFilter } from './Config/GlobalFilter';
 import { TASK_FORMATS, getSettings } from './Config/Settings';
 import { replaceTaskWithTasks } from './File';
+import type { QueryLayoutOptions } from './QueryLayoutOptions';
 import type { Task } from './Task';
 import * as taskModule from './Task';
 import { TaskFieldRenderer } from './TaskFieldRenderer';
@@ -28,6 +29,7 @@ export class TaskLineRenderer {
     private readonly obsidianComponent: Component | null;
     private readonly parentUlElement: HTMLElement;
     private readonly layoutOptions: LayoutOptions;
+    private readonly queryLayoutOptions: QueryLayoutOptions;
 
     private static async obsidianMarkdownRenderer(
         text: string,
@@ -51,22 +53,27 @@ export class TaskLineRenderer {
      * @param parentUlElement HTML element where the task shall be rendered.
      *
      * @param layoutOptions See {@link LayoutOptions}.
+     *
+     * @param queryLayoutOptions See {@link QueryLayoutOptions}.
      */
     constructor({
         textRenderer = TaskLineRenderer.obsidianMarkdownRenderer,
         obsidianComponent,
         parentUlElement,
         layoutOptions,
+        queryLayoutOptions,
     }: {
         textRenderer?: TextRenderer;
         obsidianComponent: Component | null;
         parentUlElement: HTMLElement;
         layoutOptions: LayoutOptions;
+        queryLayoutOptions: QueryLayoutOptions;
     }) {
         this.textRenderer = textRenderer;
         this.obsidianComponent = obsidianComponent;
         this.parentUlElement = parentUlElement;
         this.layoutOptions = layoutOptions;
+        this.queryLayoutOptions = queryLayoutOptions;
     }
 
     /**
@@ -140,7 +147,7 @@ export class TaskLineRenderer {
         li.setAttribute('data-task-status-type', task.status.type);
         checkbox.setAttribute('data-line', taskIndex.toString());
 
-        if (this.layoutOptions.shortMode) {
+        if (this.queryLayoutOptions.shortMode) {
             this.addTooltip(task, textSpan, isFilenameUnique);
         }
 
@@ -148,11 +155,15 @@ export class TaskLineRenderer {
     }
 
     private async taskToHtml(task: Task, parentElement: HTMLElement, li: HTMLLIElement): Promise<void> {
-        const taskLayout = new TaskLayout(this.layoutOptions);
+        const taskLayout = new TaskLayout(this.layoutOptions, this.queryLayoutOptions);
         const emojiSerializer = TASK_FORMATS.tasksPluginEmoji.taskSerializer;
         // Render and build classes for all the task's visible components
         for (const component of taskLayout.shownTaskLayoutComponents) {
-            const componentString = emojiSerializer.componentToString(task, taskLayout, component);
+            const componentString = emojiSerializer.componentToString(
+                task,
+                taskLayout.queryLayoutOptions.shortMode,
+                component,
+            );
             if (componentString) {
                 // Create the text span that will hold the rendered component
                 const span = document.createElement('span');
diff --git a/src/TaskSerializer/DataviewTaskSerializer.ts b/src/TaskSerializer/DataviewTaskSerializer.ts
index 6a95bd6c..b5f2a784 100644
--- a/src/TaskSerializer/DataviewTaskSerializer.ts
+++ b/src/TaskSerializer/DataviewTaskSerializer.ts
@@ -1,4 +1,4 @@
-import type { TaskLayout, TaskLayoutComponent } from '../TaskLayout';
+import type { TaskLayoutComponent } from '../TaskLayout';
 import type { Task } from '../Task';
 import { Priority } from '../Task';
 import { DefaultTaskSerializer } from './DefaultTaskSerializer';
@@ -111,8 +111,8 @@ export class DataviewTaskSerializer extends DefaultTaskSerializer {
         }
     }
 
-    public componentToString(task: Task, layout: TaskLayout, component: TaskLayoutComponent) {
-        const stringComponent = super.componentToString(task, layout, component);
+    public componentToString(task: Task, shortMode: boolean, component: TaskLayoutComponent) {
+        const stringComponent = super.componentToString(task, shortMode, component);
         const notInlineFieldComponents: TaskLayoutComponent[] = ['blockLink', 'description'];
         const shouldMakeInlineField = stringComponent !== '' && !notInlineFieldComponents.includes(component);
         return shouldMakeInlineField
diff --git a/src/TaskSerializer/DefaultTaskSerializer.ts b/src/TaskSerializer/DefaultTaskSerializer.ts
index d66f51bb..be51c728 100644
--- a/src/TaskSerializer/DefaultTaskSerializer.ts
+++ b/src/TaskSerializer/DefaultTaskSerializer.ts
@@ -84,7 +84,7 @@ export class DefaultTaskSerializer implements TaskSerializer {
         const taskLayout = new TaskLayout();
         let taskString = '';
         for (const component of taskLayout.shownTaskLayoutComponents) {
-            taskString += this.componentToString(task, taskLayout, component);
+            taskString += this.componentToString(task, taskLayout.queryLayoutOptions.shortMode, component);
         }
         return taskString;
     }
@@ -92,7 +92,7 @@ export class DefaultTaskSerializer implements TaskSerializer {
     /**
      * Renders a specific TaskLayoutComponent of the task (its description, priority, etc) as a string.
      */
-    public componentToString(task: Task, layout: TaskLayout, component: TaskLayoutComponent) {
+    public componentToString(task: Task, shortMode: boolean, component: TaskLayoutComponent) {
         const {
             // NEW_TASK_FIELD_EDIT_REQUIRED
             prioritySymbols,
@@ -126,34 +126,32 @@ export class DefaultTaskSerializer implements TaskSerializer {
             }
             case 'startDate':
                 if (!task.startDate) return '';
-                return layout.options.shortMode
+                return shortMode
                     ? ' ' + startDateSymbol
                     : ` ${startDateSymbol} ${task.startDate.format(TaskRegularExpressions.dateFormat)}`;
             case 'createdDate':
                 if (!task.createdDate) return '';
-                return layout.options.shortMode
+                return shortMode
                     ? ' ' + createdDateSymbol
                     : ` ${createdDateSymbol} ${task.createdDate.format(TaskRegularExpressions.dateFormat)}`;
             case 'scheduledDate':
                 if (!task.scheduledDate || task.scheduledDateIsInferred) return '';
-                return layout.options.shortMode
+                return shortMode
                     ? ' ' + scheduledDateSymbol
                     : ` ${scheduledDateSymbol} ${task.scheduledDate.format(TaskRegularExpressions.dateFormat)}`;
             case 'doneDate':
                 if (!task.doneDate) return '';
-                return layout.options.shortMode
+                return shortMode
                     ? ' ' + doneDateSymbol
                     : ` ${doneDateSymbol} ${task.doneDate.format(TaskRegularExpressions.dateFormat)}`;
             case 'dueDate':
                 if (!task.dueDate) return '';
-                return layout.options.shortMode
+                return shortMode
                     ? ' ' + dueDateSymbol
                     : ` ${dueDateSymbol} ${task.dueDate.format(TaskRegularExpressions.dateFormat)}`;
             case 'recurrenceRule':
                 if (!task.recurrence) return '';
-                return layout.options.shortMode
-                    ? ' ' + recurrenceSymbol
-                    : ` ${recurrenceSymbol} ${task.recurrence.toText()}`;
+                return shortMode ? ' ' + recurrenceSymbol : ` ${recurrenceSymbol} ${task.recurrence.toText()}`;
             case 'blockLink':
                 return task.blockLink ?? '';
             default:


Test patch:
diff --git a/tests/Commands/ToggleDone.test.ts b/tests/Commands/ToggleDone.test.ts
index b9deff09..7bf2cbaa 100644
--- a/tests/Commands/ToggleDone.test.ts
+++ b/tests/Commands/ToggleDone.test.ts
@@ -8,7 +8,7 @@ import { getNewCursorPosition, toggleLine } from '../../src/Commands/ToggleDone'
 import { GlobalFilter } from '../../src/Config/GlobalFilter';
 import { StatusRegistry } from '../../src/StatusRegistry';
 import { Status } from '../../src/Status';
-import { StatusConfiguration } from '../../src/StatusConfiguration';
+import { StatusConfiguration, StatusType } from '../../src/StatusConfiguration';
 
 window.moment = moment;
 
@@ -227,5 +227,39 @@ describe('ToggleDone', () => {
         });
     });
 
+    describe('should proceed through the statuses until a TODO status is reached', () => {
+        it('should proceed through the statuses until a TODO status is reached when a task is completed', () => {
+            // Arrange
+            const statusRegistry = StatusRegistry.getInstance();
+            statusRegistry.resetToDefaultStatuses();
+            statusRegistry.set([
+                new Status(new StatusConfiguration('x', 'Done', '-', false, StatusType.DONE)),
+                ...statusRegistry.registeredStatuses,
+            ]);
+
+            testToggleLine(
+                '- [ ] Recurring task should start with TODO| 🔁 every day 📅 2022-09-04 ',
+                `- [ ] Recurring task should start with TODO 🔁 every day 📅 2022-09-05
+- [x] Recurring task should start with TODO| 🔁 every day 📅 2022-09-04 ✅ 2022-09-04`,
+            );
+        });
+
+        it('should not get stuck in a loop when a task is completed', () => {
+            // Arrange
+            const statusRegistry = StatusRegistry.getInstance();
+            statusRegistry.resetToDefaultStatuses();
+            statusRegistry.set([
+                new Status(new StatusConfiguration('1', '1', '2', false, StatusType.IN_PROGRESS)),
+                new Status(new StatusConfiguration('2', '2', '1', false, StatusType.DONE)),
+            ]);
+
+            testToggleLine(
+                '- [1] Recurring task should start with TODO| 🔁 every day 📅 2022-09-04 ',
+                `- [1] Recurring task should start with TODO 🔁 every day 📅 2022-09-05
+- [2] Recurring task should start with TODO| 🔁 every day 📅 2022-09-04 ✅ 2022-09-04`,
+            );
+        });
+    });
+
     todaySpy.mockClear();
 });
diff --git a/tests/CustomMatchers/CustomMatchersForRendering.ts b/tests/CustomMatchers/CustomMatchersForRendering.ts
new file mode 100644
index 00000000..d94e47d0
--- /dev/null
+++ b/tests/CustomMatchers/CustomMatchersForRendering.ts
@@ -0,0 +1,38 @@
+import { diff } from 'jest-diff';
+
+declare global {
+    namespace jest {
+        interface Matchers<R> {
+            toHaveDataAttributes(expectedDataAttributes: string): R;
+        }
+
+        interface Expect {
+            toHaveDataAttributes(expectedDataAttributes: string): any;
+        }
+
+        interface InverseAsymmetricMatchers {
+            toHaveDataAttributes(expectedDataAttributes: string): any;
+        }
+    }
+}
+
+function getDataAttributesAsString(element: HTMLElement): string {
+    const dataAttributes = element.dataset;
+    const keys = Object.keys(dataAttributes);
+
+    return keys.map((key) => `${key}: ${dataAttributes[key]}`).join('\n');
+}
+
+export function toHaveDataAttributes(htmlElement: HTMLElement, expectedDataAttributes: string) {
+    const renderedDataAttributes = getDataAttributesAsString(htmlElement);
+
+    const pass: boolean = renderedDataAttributes === expectedDataAttributes;
+    const message: () => string = () =>
+        pass
+            ? `Data attributes should not be\n${renderedDataAttributes}`
+            : `Data attributes are not the same as expected:\n${diff(expectedDataAttributes, renderedDataAttributes)}`;
+    return {
+        message,
+        pass,
+    };
+}
diff --git a/tests/CustomMatchers/jest.custom_matchers.setup.ts b/tests/CustomMatchers/jest.custom_matchers.setup.ts
index 057f4c56..2cd90978 100644
--- a/tests/CustomMatchers/jest.custom_matchers.setup.ts
+++ b/tests/CustomMatchers/jest.custom_matchers.setup.ts
@@ -49,6 +49,14 @@ expect.extend({
     toSupportGroupingWithProperty,
 });
 
+// ---------------------------------------------------------------------
+// CustomMatchersForRendering
+// ---------------------------------------------------------------------
+import { toHaveDataAttributes } from './CustomMatchersForRendering';
+expect.extend({
+    toHaveDataAttributes,
+});
+
 // ---------------------------------------------------------------------
 // CustomMatchersForTaskBuilder
 // ---------------------------------------------------------------------
diff --git a/tests/StatusRegistry.test.ts b/tests/StatusRegistry.test.ts
index 59b885b7..1588c436 100644
--- a/tests/StatusRegistry.test.ts
+++ b/tests/StatusRegistry.test.ts
@@ -163,13 +163,52 @@ describe('StatusRegistry', () => {
         expect(status2).toStrictEqual(status);
     });
 
-    it('should create a mermaid diagram of default statuses', () => {
+    it('should find unknown statuses from tasks in the vault, sorted by symbol', () => {
         // Arrange
-        const statusRegistry = new StatusRegistry();
+        const registry = new StatusRegistry();
+        expect(registry.bySymbol('!').type).toEqual(StatusType.EMPTY);
+        expect(registry.bySymbol('X').type).toEqual(StatusType.EMPTY);
+        expect(registry.bySymbol('d').type).toEqual(StatusType.EMPTY);
+        const allStatuses = [
+            new Status(new StatusConfiguration('!', 'Unknown', 'X', false, StatusType.TODO)),
+            new Status(new StatusConfiguration('X', 'Unknown', '!', false, StatusType.DONE)),
+            new Status(new StatusConfiguration('d', 'Unknown', '!', false, StatusType.IN_PROGRESS)),
+            // Include some tasks with duplicate statuses, to make sure duplicates are discarded
+            new Status(new StatusConfiguration('!', 'Unknown', 'X', false, StatusType.TODO)),
+            new Status(new StatusConfiguration('X', 'Unknown', '!', false, StatusType.DONE)),
+            new Status(new StatusConfiguration('d', 'Unknown', '!', false, StatusType.IN_PROGRESS)),
+            // Check that it does not add copies of any core statuses
+            new Status(new StatusConfiguration('-', 'Unknown', '!', false, StatusType.IN_PROGRESS)),
+        ];
+
+        // Act
+        const unknownStatuses = registry.findUnknownStatuses(allStatuses);
 
         // Assert
-        // Without detail:
-        expect(statusRegistry.mermaidDiagram(false)).toMatchInlineSnapshot(`
+        expect(unknownStatuses.length).toEqual(3);
+
+        let s1;
+        s1 = unknownStatuses[0];
+        expect(s1.type).toEqual(StatusType.TODO);
+        expect(s1.name).toEqual('Unknown (!)');
+
+        s1 = unknownStatuses[1];
+        expect(s1.type).toEqual(StatusType.IN_PROGRESS);
+        expect(s1.name).toEqual('Unknown (d)');
+
+        s1 = unknownStatuses[2];
+        expect(s1.type).toEqual(StatusType.DONE);
+        expect(s1.name).toEqual('Unknown (X)');
+    });
+
+    describe('mermaid diagrams', () => {
+        it('should create a mermaid diagram of default statuses', () => {
+            // Arrange
+            const statusRegistry = new StatusRegistry();
+
+            // Assert
+            // Without detail:
+            expect(statusRegistry.mermaidDiagram(false)).toMatchInlineSnapshot(`
             "
             \`\`\`mermaid
             flowchart LR
@@ -194,8 +233,8 @@ describe('StatusRegistry', () => {
             "
         `);
 
-        // With detail:
-        expect(statusRegistry.mermaidDiagram(true)).toMatchInlineSnapshot(`
+            // With detail:
+            expect(statusRegistry.mermaidDiagram(true)).toMatchInlineSnapshot(`
             "
             \`\`\`mermaid
             flowchart LR
@@ -219,24 +258,24 @@ describe('StatusRegistry', () => {
             \`\`\`
             "
         `);
-    });
+        });
 
-    it('should encode symbols in mermaid diagrams when necessary', () => {
-        // This tests the fix for:
-        //      https://github.com/obsidian-tasks-group/obsidian-tasks/issues/2355
-        //      Fix the handling of special characters in Mermaid status diagrams
+        it('should encode symbols in mermaid diagrams when necessary', () => {
+            // This tests the fix for:
+            //      https://github.com/obsidian-tasks-group/obsidian-tasks/issues/2355
+            //      Fix the handling of special characters in Mermaid status diagrams
 
-        // Arrange
-        const statusRegistry = new StatusRegistry();
-        statusRegistry.clearStatuses();
-        statusRegistry.add(new StatusConfiguration('<', 'Todo <', '<', false, StatusType.TODO));
-        statusRegistry.add(new StatusConfiguration('>', 'Todo >', '>', false, StatusType.TODO));
-        statusRegistry.add(new StatusConfiguration('"', 'Todo "', '"', false, StatusType.TODO));
-        statusRegistry.add(new StatusConfiguration('&', 'Todo &', '&', false, StatusType.TODO));
+            // Arrange
+            const statusRegistry = new StatusRegistry();
+            statusRegistry.clearStatuses();
+            statusRegistry.add(new StatusConfiguration('<', 'Todo <', '<', false, StatusType.TODO));
+            statusRegistry.add(new StatusConfiguration('>', 'Todo >', '>', false, StatusType.TODO));
+            statusRegistry.add(new StatusConfiguration('"', 'Todo "', '"', false, StatusType.TODO));
+            statusRegistry.add(new StatusConfiguration('&', 'Todo &', '&', false, StatusType.TODO));
 
-        // Assert
-        // Without detail:
-        expect(statusRegistry.mermaidDiagram(false)).toMatchInlineSnapshot(`
+            // Assert
+            // Without detail:
+            expect(statusRegistry.mermaidDiagram(false)).toMatchInlineSnapshot(`
             "
             \`\`\`mermaid
             flowchart LR
@@ -261,8 +300,8 @@ describe('StatusRegistry', () => {
             "
         `);
 
-        // With detail:
-        expect(statusRegistry.mermaidDiagram(true)).toMatchInlineSnapshot(`
+            // With detail:
+            expect(statusRegistry.mermaidDiagram(true)).toMatchInlineSnapshot(`
             "
             \`\`\`mermaid
             flowchart LR
@@ -286,22 +325,22 @@ describe('StatusRegistry', () => {
             \`\`\`
             "
         `);
-    });
+        });
 
-    it('should not include unknown nextStatusSymbols in mermaid diagrams', () => {
-        // Arrange
-        const statusRegistry = new StatusRegistry();
-        statusRegistry.clearStatuses();
-        statusRegistry.add(new StatusConfiguration(' ', 'Todo', '/', false, StatusType.TODO));
-        // Leave '/' as not registered
-        const originalNumberOfStatuses = statusRegistry.registeredStatuses.length;
+        it('should not include unknown nextStatusSymbols in mermaid diagrams', () => {
+            // Arrange
+            const statusRegistry = new StatusRegistry();
+            statusRegistry.clearStatuses();
+            statusRegistry.add(new StatusConfiguration(' ', 'Todo', '/', false, StatusType.TODO));
+            // Leave '/' as not registered
+            const originalNumberOfStatuses = statusRegistry.registeredStatuses.length;
 
-        // Act
-        const mermaidText = statusRegistry.mermaidDiagram();
+            // Act
+            const mermaidText = statusRegistry.mermaidDiagram();
 
-        // Assert
-        expect(statusRegistry.registeredStatuses.length).toEqual(originalNumberOfStatuses);
-        expect(mermaidText).toMatchInlineSnapshot(`
+            // Assert
+            expect(statusRegistry.registeredStatuses.length).toEqual(originalNumberOfStatuses);
+            expect(mermaidText).toMatchInlineSnapshot(`
             "
             \`\`\`mermaid
             flowchart LR
@@ -319,23 +358,27 @@ describe('StatusRegistry', () => {
             \`\`\`
             "
         `);
+        });
     });
 
     describe('toggling', () => {
+        const path = 'file.md';
+        const lineNumber = 3456;
+        const sectionStart = 1337;
+        const sectionIndex = 1209;
+        const precedingHeader = 'Eloquent Section';
+        const taskLocation = new TaskLocation(path, lineNumber, sectionStart, sectionIndex, precedingHeader);
+        const fallbackDate = null;
+
         it('should allow task to toggle through standard transitions', () => {
             // Arrange
             // Global statusRegistry instance - which controls toggling - will have been reset
             // in beforeEach() above.
             const line = '- [ ] this is a task starting at A';
-            const path = 'file.md';
-            const lineNumber = 3456;
-            const sectionStart = 1337;
-            const sectionIndex = 1209;
-            const precedingHeader = 'Eloquent Section';
             const task = Task.fromLine({
                 line,
-                taskLocation: new TaskLocation(path, lineNumber, sectionStart, sectionIndex, precedingHeader),
-                fallbackDate: null,
+                taskLocation,
+                fallbackDate,
             });
 
             // Act
@@ -356,15 +399,10 @@ describe('StatusRegistry', () => {
             // Global statusRegistry instance - which controls toggling - will have been reset
             // in beforeEach() above.
             const line = '- [-] This is a cancelled task';
-            const path = 'file.md';
-            const lineNumber = 3456;
-            const sectionStart = 1337;
-            const sectionIndex = 1209;
-            const precedingHeader = 'Eloquent Section';
             const task = Task.fromLine({
                 line,
-                taskLocation: new TaskLocation(path, lineNumber, sectionStart, sectionIndex, precedingHeader),
-                fallbackDate: null,
+                taskLocation,
+                fallbackDate,
             });
 
             // Act
@@ -391,15 +429,10 @@ describe('StatusRegistry', () => {
             globalStatusRegistry.add(statusC);
             globalStatusRegistry.add(statusD);
             const line = '- [a] this is a task starting at A';
-            const path = 'file.md';
-            const lineNumber = 3456;
-            const sectionStart = 1337;
-            const sectionIndex = 1209;
-            const precedingHeader = 'Eloquent Section';
             const task = Task.fromLine({
                 line,
-                taskLocation: new TaskLocation(path, lineNumber, sectionStart, sectionIndex, precedingHeader),
-                fallbackDate: null,
+                taskLocation,
+                fallbackDate,
             });
 
             // Act
@@ -440,43 +473,5 @@ describe('StatusRegistry', () => {
             // Ensure that the next status was applied, even though it is an unrecognised status
             expect(toggled?.status.symbol).toEqual('D');
         });
-
-        it('should find unknown statuses from tasks in the vault, sorted by symbol', () => {
-            // Arrange
-            const registry = new StatusRegistry();
-            expect(registry.bySymbol('!').type).toEqual(StatusType.EMPTY);
-            expect(registry.bySymbol('X').type).toEqual(StatusType.EMPTY);
-            expect(registry.bySymbol('d').type).toEqual(StatusType.EMPTY);
-            const allStatuses = [
-                new Status(new StatusConfiguration('!', 'Unknown', 'X', false, StatusType.TODO)),
-                new Status(new StatusConfiguration('X', 'Unknown', '!', false, StatusType.DONE)),
-                new Status(new StatusConfiguration('d', 'Unknown', '!', false, StatusType.IN_PROGRESS)),
-                // Include some tasks with duplicate statuses, to make sure duplicates are discarded
-                new Status(new StatusConfiguration('!', 'Unknown', 'X', false, StatusType.TODO)),
-                new Status(new StatusConfiguration('X', 'Unknown', '!', false, StatusType.DONE)),
-                new Status(new StatusConfiguration('d', 'Unknown', '!', false, StatusType.IN_PROGRESS)),
-                // Check that it does not add copies of any core statuses
-                new Status(new StatusConfiguration('-', 'Unknown', '!', false, StatusType.IN_PROGRESS)),
-            ];
-
-            // Act
-            const unknownStatuses = registry.findUnknownStatuses(allStatuses);
-
-            // Assert
-            expect(unknownStatuses.length).toEqual(3);
-
-            let s1;
-            s1 = unknownStatuses[0];
-            expect(s1.type).toEqual(StatusType.TODO);
-            expect(s1.name).toEqual('Unknown (!)');
-
-            s1 = unknownStatuses[1];
-            expect(s1.type).toEqual(StatusType.IN_PROGRESS);
-            expect(s1.name).toEqual('Unknown (d)');
-
-            s1 = unknownStatuses[2];
-            expect(s1.type).toEqual(StatusType.DONE);
-            expect(s1.name).toEqual('Unknown (X)');
-        });
     });
 });
diff --git a/tests/TaskFieldRenderer.test.ts b/tests/TaskFieldRenderer.test.ts
index f376e998..697df9c8 100644
--- a/tests/TaskFieldRenderer.test.ts
+++ b/tests/TaskFieldRenderer.test.ts
@@ -25,8 +25,7 @@ describe('Field Layouts Container tests', () => {
 
         fieldRenderer.addDataAttribute(span, task, 'dueDate');
 
-        expect(Object.keys(span.dataset).length).toEqual(1);
-        expect(span.dataset['taskDue']).toEqual('future-1d');
+        expect(span).toHaveDataAttributes('taskDue: future-1d');
     });
 
     it('should add a data attribute for an existing component (not date)', () => {
@@ -35,8 +34,7 @@ describe('Field Layouts Container tests', () => {
 
         fieldRenderer.addDataAttribute(span, task, 'priority');
 
-        expect(Object.keys(span.dataset).length).toEqual(1);
-        expect(span.dataset['taskPriority']).toEqual('medium');
+        expect(span).toHaveDataAttributes('taskPriority: medium');
     });
     it('should not add any data attributes for a missing component', () => {
         const task = new TaskBuilder().build();
@@ -44,27 +42,48 @@ describe('Field Layouts Container tests', () => {
 
         fieldRenderer.addDataAttribute(span, task, 'recurrenceRule');
 
-        expect(Object.keys(span.dataset).length).toEqual(0);
+        expect(span).toHaveDataAttributes('');
     });
 });
 
 describe('Field Layout Detail tests', () => {
     it('should supply a class name', () => {
-        const fieldLayoutDetail = new TaskFieldHTMLData('stuff', 'taskAttribute', () => {
+        const fieldLayoutDetail = new TaskFieldHTMLData('task-description', '', () => {
             return '';
         });
-        expect(fieldLayoutDetail.className).toEqual('stuff');
+        expect(fieldLayoutDetail.className).toEqual('task-description');
     });
 
-    it('should add a data attribute for an HTML element', () => {
-        const fieldLayoutDetail = new TaskFieldHTMLData('dataAttributeTest', 'aKey', () => {
-            return 'aValue';
+    it('should add a data attribute with a value and a name', () => {
+        const fieldLayoutDetail = new TaskFieldHTMLData('task-priority', 'taskPriority', () => {
+            return 'highest';
         });
         const span = document.createElement('span');
 
-        fieldLayoutDetail.addDataAttribute(span, new TaskBuilder().build(), 'description');
+        fieldLayoutDetail.addDataAttribute(span, new TaskBuilder().build(), 'priority');
 
-        expect(Object.keys(span.dataset).length).toEqual(1);
-        expect(span.dataset['aKey']).toEqual('aValue');
+        expect(span).toHaveDataAttributes('taskPriority: highest');
+    });
+
+    it('should not add a data attribute without a name', () => {
+        const fieldLayoutDetail = new TaskFieldHTMLData('task-due', '', () => {
+            return 'past-far';
+        });
+        const span = document.createElement('span');
+
+        fieldLayoutDetail.addDataAttribute(span, new TaskBuilder().build(), 'dueDate');
+
+        expect(span).toHaveDataAttributes('');
+    });
+
+    it.failing('should not add a data attribute with a name but without value', () => {
+        const fieldLayoutDetail = new TaskFieldHTMLData('task-start', 'taskStart', () => {
+            return '';
+        });
+        const span = document.createElement('span');
+
+        fieldLayoutDetail.addDataAttribute(span, new TaskBuilder().build(), 'startDate');
+
+        expect(span).toHaveDataAttributes('');
     });
 });
diff --git a/tests/TaskLayout.test.ts b/tests/TaskLayout.test.ts
index 0cef973e..3e8d8872 100644
--- a/tests/TaskLayout.test.ts
+++ b/tests/TaskLayout.test.ts
@@ -2,6 +2,7 @@
  * @jest-environment jsdom
  */
 
+import { QueryLayoutOptions } from '../src/QueryLayoutOptions';
 import { LayoutOptions, TaskLayout } from '../src/TaskLayout';
 
 describe('TaskLayout tests', () => {
@@ -24,14 +25,20 @@ describe('TaskLayout tests', () => {
 
     it('should generate expected CSS components with all default option reversed', () => {
         const layoutOptions = new LayoutOptions();
-
-        // Negate all the boolean values:
+        // Negate all the task layout boolean values:
         Object.keys(layoutOptions).forEach((key) => {
             const key2 = key as keyof LayoutOptions;
             layoutOptions[key2] = !layoutOptions[key2];
         });
 
-        const taskLayout = new TaskLayout(layoutOptions);
+        const queryLayoutOptions = new QueryLayoutOptions();
+        // Negate all the query layout boolean values:
+        Object.keys(queryLayoutOptions).forEach((key) => {
+            const key2 = key as keyof QueryLayoutOptions;
+            queryLayoutOptions[key2] = !queryLayoutOptions[key2];
+        });
+
+        const taskLayout = new TaskLayout(layoutOptions, queryLayoutOptions);
 
         expect(taskLayout.shownTaskLayoutComponents.join('\n')).toMatchInlineSnapshot(`
             "description
diff --git a/tests/TaskLineRenderer.test.ts b/tests/TaskLineRenderer.test.ts
index 31a038bb..156c75cf 100644
--- a/tests/TaskLineRenderer.test.ts
+++ b/tests/TaskLineRenderer.test.ts
@@ -6,6 +6,7 @@ import { DebugSettings } from '../src/Config/DebugSettings';
 import { GlobalFilter } from '../src/Config/GlobalFilter';
 import { resetSettings, updateSettings } from '../src/Config/Settings';
 import { DateParser } from '../src/Query/DateParser';
+import { QueryLayoutOptions } from '../src/QueryLayoutOptions';
 import type { Task } from '../src/Task';
 import { TaskRegularExpressions } from '../src/Task';
 import { TaskFieldRenderer } from '../src/TaskFieldRenderer';
@@ -31,13 +32,21 @@ const fieldRenderer = new TaskFieldRenderer();
  * @param layoutOptions for the task rendering. Skip for default options. See {@link LayoutOptions}.
  *
  * @param testRenderer imitates Obsidian rendering. Skip for the default {@link mockTextRenderer}.
+ *
+ * @param queryLayoutOptions for the task rendering. Skip for default options. See {@link QueryLayoutOptions}.
  */
-async function renderListItem(task: Task, layoutOptions?: LayoutOptions, testRenderer?: TextRenderer) {
+async function renderListItem(
+    task: Task,
+    layoutOptions?: LayoutOptions,
+    queryLayoutOptions?: QueryLayoutOptions,
+    testRenderer?: TextRenderer,
+) {
     const taskLineRenderer = new TaskLineRenderer({
         textRenderer: testRenderer ?? mockTextRenderer,
         obsidianComponent: null,
         parentUlElement: document.createElement('div'),
         layoutOptions: layoutOptions ?? new LayoutOptions(),
+        queryLayoutOptions: queryLayoutOptions ?? new QueryLayoutOptions(),
     });
     return await taskLineRenderer.renderTaskLine(task, 0);
 }
@@ -63,15 +72,17 @@ function getDescriptionText(listItem: HTMLElement) {
     return (textSpan.children[0].children[0] as HTMLElement).innerText;
 }
 
-/*
- * Returns a list of the task components that are not the description, as strings.
+/**
+ * Returns an array with the components of a List Item as strings.
  */
-function getOtherLayoutComponents(listItem: HTMLElement): string[] {
+function getListItemComponents(listItem: HTMLElement): string[] {
+    const components: string[] = [getDescriptionText(listItem)];
+
     const textSpan = getTextSpan(listItem);
-    const components: string[] = [];
     for (const childSpan of Array.from(textSpan.children)) {
-        if (childSpan.classList.contains(fieldRenderer.className('description'))) continue;
-        if (childSpan?.textContent) components.push(childSpan.textContent);
+        if (childSpan.textContent) {
+            components.push(childSpan.textContent);
+        }
     }
     return components;
 }
@@ -90,6 +101,7 @@ describe('task line rendering - HTML', () => {
             obsidianComponent: null,
             parentUlElement: ulElement,
             layoutOptions: new LayoutOptions(),
+            queryLayoutOptions: new QueryLayoutOptions(),
         });
         const listItem = await taskLineRenderer.renderTaskLine(new TaskBuilder().build(), 0);
 
@@ -171,7 +183,6 @@ describe('task line rendering - layout options', () => {
     const testLayoutOptions = async (
         taskLine: string,
         layoutOptions: Partial<LayoutOptions>,
-        expectedDescription: string,
         expectedComponents: string[],
     ) => {
         const task = fromLine({
@@ -179,27 +190,26 @@ describe('task line rendering - layout options', () => {
         });
         const fullLayoutOptions = { ...new LayoutOptions(), ...layoutOptions };
         const listItem = await renderListItem(task, fullLayoutOptions);
-        const renderedDescription = getDescriptionText(listItem);
-        const renderedComponents = getOtherLayoutComponents(listItem);
-        expect(renderedDescription).toEqual(expectedDescription);
+        const renderedComponents = getListItemComponents(listItem);
         expect(renderedComponents).toEqual(expectedComponents);
     };
 
     it('renders correctly with the default layout options', async () => {
-        await testLayoutOptions(
-            '- [ ] Full task ⏫ 📅 2022-07-02 ⏳ 2022-07-03 🛫 2022-07-04 🔁 every day',
-            {},
+        await testLayoutOptions('- [ ] Full task ⏫ 📅 2022-07-02 ⏳ 2022-07-03 🛫 2022-07-04 🔁 every day', {}, [
             'Full task',
-            [' ⏫', ' 🔁 every day', ' 🛫 2022-07-04', ' ⏳ 2022-07-03', ' 📅 2022-07-02'],
-        );
+            ' ⏫',
+            ' 🔁 every day',
+            ' 🛫 2022-07-04',
+            ' ⏳ 2022-07-03',
+            ' 📅 2022-07-02',
+        ]);
     });
 
     it('renders without priority', async () => {
         await testLayoutOptions(
             '- [ ] Full task ⏫ 📅 2022-07-02 ⏳ 2022-07-03 🛫 2022-07-04 🔁 every day',
             { hidePriority: true },
-            'Full task',
-            [' 🔁 every day', ' 🛫 2022-07-04', ' ⏳ 2022-07-03', ' 📅 2022-07-02'],
+            ['Full task', ' 🔁 every day', ' 🛫 2022-07-04', ' ⏳ 2022-07-03', ' 📅 2022-07-02'],
         );
     });
 
@@ -207,8 +217,7 @@ describe('task line rendering - layout options', () => {
         await testLayoutOptions(
             '- [ ] Full task ⏫ 📅 2022-07-02 ⏳ 2022-07-03 🛫 2022-07-04 ➕ 2022-07-05 🔁 every day',
             { hideCreatedDate: true },
-            'Full task',
-            [' ⏫', ' 🔁 every day', ' 🛫 2022-07-04', ' ⏳ 2022-07-03', ' 📅 2022-07-02'],
+            ['Full task', ' ⏫', ' 🔁 every day', ' 🛫 2022-07-04', ' ⏳ 2022-07-03', ' 📅 2022-07-02'],
         );
     });
 
@@ -216,8 +225,7 @@ describe('task line rendering - layout options', () => {
         await testLayoutOptions(
             '- [ ] Full task ⏫ 📅 2022-07-02 ⏳ 2022-07-03 🛫 2022-07-04 🔁 every day',
             { hideStartDate: true },
-            'Full task',
-            [' ⏫', ' 🔁 every day', ' ⏳ 2022-07-03', ' 📅 2022-07-02'],
+            ['Full task', ' ⏫', ' 🔁 every day', ' ⏳ 2022-07-03', ' 📅 2022-07-02'],
         );
     });
 
@@ -225,8 +233,7 @@ describe('task line rendering - layout options', () => {
         await testLayoutOptions(
             '- [ ] Full task ⏫ 📅 2022-07-02 ⏳ 2022-07-03 🛫 2022-07-04 🔁 every day',
             { hideScheduledDate: true },
-            'Full task',
-            [' ⏫', ' 🔁 every day', ' 🛫 2022-07-04', ' 📅 2022-07-02'],
+            ['Full task', ' ⏫', ' 🔁 every day', ' 🛫 2022-07-04', ' 📅 2022-07-02'],
         );
     });
 
@@ -234,8 +241,7 @@ describe('task line rendering - layout options', () => {
         await testLayoutOptions(
             '- [ ] Full task ⏫ 📅 2022-07-02 ⏳ 2022-07-03 🛫 2022-07-04 🔁 every day',
             { hideDueDate: true },
-            'Full task',
-            [' ⏫', ' 🔁 every day', ' 🛫 2022-07-04', ' ⏳ 2022-07-03'],
+            ['Full task', ' ⏫', ' 🔁 every day', ' 🛫 2022-07-04', ' ⏳ 2022-07-03'],
         );
     });
 
@@ -243,8 +249,7 @@ describe('task line rendering - layout options', () => {
         await testLayoutOptions(
             '- [ ] Full task ⏫ 📅 2022-07-02 ⏳ 2022-07-03 🛫 2022-07-04 🔁 every day',
             { hideRecurrenceRule: true },
-            'Full task',
-            [' ⏫', ' 🛫 2022-07-04', ' ⏳ 2022-07-03', ' 📅 2022-07-02'],
+            ['Full task', ' ⏫', ' 🛫 2022-07-04', ' ⏳ 2022-07-03', ' 📅 2022-07-02'],
         );
     });
 
@@ -252,8 +257,8 @@ describe('task line rendering - layout options', () => {
         await testLayoutOptions(
             '- [x] Full task ✅ 2022-07-05 ⏫ 📅 2022-07-02 ⏳ 2022-07-03 🛫 2022-07-04 ➕ 2022-07-05 🔁 every day',
             {},
-            'Full task',
             [
+                'Full task',
                 ' ⏫',
                 ' 🔁 every day',
                 ' ➕ 2022-07-05',
@@ -269,24 +274,30 @@ describe('task line rendering - layout options', () => {
         await testLayoutOptions(
             '- [x] Full task ✅ 2022-07-05 ⏫ 📅 2022-07-02 ⏳ 2022-07-03 🛫 2022-07-04 ➕ 2022-07-05 🔁 every day',
             { hideDoneDate: true },
-            'Full task',
-            [' ⏫', ' 🔁 every day', ' ➕ 2022-07-05', ' 🛫 2022-07-04', ' ⏳ 2022-07-03', ' 📅 2022-07-02'],
+            [
+                'Full task',
+                ' ⏫',
+                ' 🔁 every day',
+                ' ➕ 2022-07-05',
+                ' 🛫 2022-07-04',
+                ' ⏳ 2022-07-03',
+                ' 📅 2022-07-02',
+            ],
         );
     });
 
     it('writes a placeholder message if a date is invalid', async () => {
-        await testLayoutOptions('- [ ] Task with invalid due date 📅 2023-13-02', {}, 'Task with invalid due date', [
+        await testLayoutOptions('- [ ] Task with invalid due date 📅 2023-13-02', {}, [
+            'Task with invalid due date',
             ' 📅 Invalid date',
         ]);
     });
 
     it('standardise the recurrence rule, even if the rule is invalid', async () => {
-        await testLayoutOptions(
-            '- [ ] Task with invalid recurrence rule 🔁 every month on the 32nd',
-            {},
+        await testLayoutOptions('- [ ] Task with invalid recurrence rule 🔁 every month on the 32nd', {}, [
             'Task with invalid recurrence rule',
-            [' 🔁 every month on the 32th'],
-        );
+            ' 🔁 every month on the 32th',
+        ]);
     });
 });
 
@@ -567,7 +578,7 @@ describe('task line rendering - classes and data attributes', () => {
         const task = fromLine({
             line: taskLine,
         });
-        const listItem = await renderListItem(task, new LayoutOptions(), mockHTMLRenderer);
+        const listItem = await renderListItem(task, new LayoutOptions(), new QueryLayoutOptions(), mockHTMLRenderer);
 
         const textSpan = getTextSpan(listItem);
         const descriptionSpan = textSpan.children[0].children[0] as HTMLElement;
@@ -583,7 +594,7 @@ describe('task line rendering - classes and data attributes', () => {
         const task = fromLine({
             line: taskLine,
         });
-        const listItem = await renderListItem(task, new LayoutOptions(), mockHTMLRenderer);
+        const listItem = await renderListItem(task, new LayoutOptions(), new QueryLayoutOptions(), mockHTMLRenderer);
 
         const textSpan = getTextSpan(listItem);
         const descriptionSpan = textSpan.children[0].children[0] as HTMLElement;
@@ -642,8 +653,11 @@ describe('task line rendering - classes and data attributes', () => {
 });
 
 describe('Visualise HTML', () => {
-    async function renderAndVerifyHTML(task: Task, layoutOptions: LayoutOptions) {
-        const listItem = await renderListItem(task, layoutOptions, mockHTMLRenderer);
+    async function renderAndVerifyHTML(
+        task: Task,
+        { layoutOptions, queryLayoutOptions }: { layoutOptions: LayoutOptions; queryLayoutOptions: QueryLayoutOptions },
+    ) {
+        const listItem = await renderListItem(task, layoutOptions, queryLayoutOptions, mockHTMLRenderer);
 
         const taskAsMarkdown = `<!--
 ${task.toFileLineString()}
@@ -656,7 +670,7 @@ ${task.toFileLineString()}
     const fullTask = TaskBuilder.createFullyPopulatedTask();
     const minimalTask = fromLine({ line: '- [-] empty' });
 
-    function layoutOptionsFullMode(): LayoutOptions {
+    function layoutOptionsFullMode() {
         const layoutOptions = new LayoutOptions();
 
         // Show every Task field, disable short mode, do not explain the query
@@ -668,15 +682,14 @@ ${task.toFileLineString()}
             layoutOptions[key2] = false;
         });
 
-        return layoutOptions;
+        return { layoutOptions, queryLayoutOptions: new QueryLayoutOptions() };
     }
 
-    function layoutOptionsShortMode(): LayoutOptions {
-        const layoutOptions = layoutOptionsFullMode();
-
-        layoutOptions.shortMode = true;
+    function layoutOptionsShortMode() {
+        const queryLayoutOptions = new QueryLayoutOptions();
+        queryLayoutOptions.shortMode = true;
 
-        return layoutOptions;
+        return { layoutOptions: new LayoutOptions(), queryLayoutOptions };
     }
 
     it('Full task - full mode', async () => {