Repository: obsidian-tasks-group/obsidian-tasks
Pull request #2518 (2023-12-18 17:59:27)

test: test adding data attributes with only name & only value
<!--- Provide a general summary of your changes in the Title above -->

# Description

- test adding data attributes with only name & only value

## Motivation and Context

- Describe the current behaviour before any changes
- Increase coverage

## How has this been tested?

New unit tests, breaking code

## Types of changes

- [x] **Tests** (prefix: `test` - additions and improvements to unit tests and the smoke 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 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)


claremacrae: I’ve read this and don’t understand. Could you explain it when we chat tomorrow?
ilandikov: > I’ve read this and don’t understand. Could you explain it when we chat tomorrow?

I had several mistakes in the description. Hope it is fine now. If still unclear let's discuss tomorrow =)

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 fa624fdb..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);
             }
 
@@ -454,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/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/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/TaskFieldRenderer.test.ts b/tests/TaskFieldRenderer.test.ts
index f376e998..a45e30b9 100644
--- a/tests/TaskFieldRenderer.test.ts
+++ b/tests/TaskFieldRenderer.test.ts
@@ -50,21 +50,42 @@ describe('Field Layouts Container tests', () => {
 
 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 106008c2..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);
 }
@@ -92,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);
 
@@ -568,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;
@@ -584,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;
@@ -643,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()}
@@ -657,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
@@ -669,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 () => {