Repository: obsidian-tasks-group/obsidian-tasks
Pull request #2453 (2023-11-28 14:27:24)

refactor: Extract functions from postpone code, and start testing
<!--- Provide a general summary of your changes in the Title above -->

# Description

Work started whilst pairing with @ilandikov

- Extract functions `getDateFieldToPostpone()` and `createPostponedTask()` from the postponing code in `QueryRenderer.ts`, to enable testing
- Added thorough tests of initial `getDateFieldToPostpone()` behaviour

## Motivation and Context

- In order to start adjusting the behaviour, as documented in https://github.com/obsidian-tasks-group/obsidian-tasks/discussions/2452

## How has this been tested?

<!--- Please describe in detail how you tested your changes. -->
<!--- Include details of your testing environment, tests ran to see how -->
<!--- your change affects other areas of the code, etc. -->

- Lots of tests added, of the current behaviour
- I have tested the extracting of code from `QueryRenderer.ts` manually, in the demo vault.

## Screenshots (if appropriate)

## Types of changes

<!--- What types of changes does your code introduce? Put an `x` in all the boxes that apply: -->

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)

## Checklist

<!--- Go over all the following points, and put an `x` in all the boxes that apply. -->
<!--- If you're unsure about any of these, don't hesitate to ask. We're here to help! -->

- [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/QueryRenderer.ts b/src/QueryRenderer.ts
index afc33495..12f31eaa 100644
--- a/src/QueryRenderer.ts
+++ b/src/QueryRenderer.ts
@@ -8,16 +8,20 @@ import { DateFallback } from './DateFallback';
 import { getTaskLineAndFile, replaceTaskWithTasks } from './File';
 
 import type { IQuery } from './IQuery';
-import { explainResults, getQueryForQueryRenderer } from './lib/QueryRendererHelper';
+import {
+    createPostponedTask,
+    explainResults,
+    getDateFieldToPostpone,
+    getQueryForQueryRenderer,
+} from './lib/QueryRendererHelper';
 import type { GroupDisplayHeading } from './Query/GroupDisplayHeading';
 import type { QueryResult } from './Query/QueryResult';
 import type { TaskGroups } from './Query/TaskGroups';
-import { Task } from './Task';
+import type { Task } from './Task';
 import { TaskLayout } from './TaskLayout';
 import { TaskLineRenderer } from './TaskLineRenderer';
 import { TaskModal } from './TaskModal';
 import type { TasksEvents } from './TasksEvents';
-import { TasksDate } from './Scripting/TasksDate';
 
 export class QueryRenderer {
     private readonly app: App;
@@ -480,13 +484,10 @@ class QueryRenderChild extends MarkdownRenderChild {
     ) {
         const errorMessage = '⚠️ Postponement requires a date: due or scheduled.';
         if (!task.dueDate && !task.scheduledDate) return new Notice(errorMessage, 10000);
-        const scheduledDateOrNull = task.scheduledDate ? 'scheduledDate' : null;
-        const dateTypeToUpdate = task.dueDate ? 'dueDate' : scheduledDateOrNull;
+        const dateTypeToUpdate = getDateFieldToPostpone(task);
         if (dateTypeToUpdate === null) return;
 
-        const dateToUpdate = task[dateTypeToUpdate];
-        const postponedDate = new TasksDate(dateToUpdate).postpone(timeUnit, amount);
-        const newTasks = new Task({ ...task, [dateTypeToUpdate]: postponedDate });
+        const { postponedDate, newTasks } = createPostponedTask(task, dateTypeToUpdate, timeUnit, amount);
 
         await replaceTaskWithTasks({
             originalTask: task,
diff --git a/src/lib/QueryRendererHelper.ts b/src/lib/QueryRendererHelper.ts
index 1221f2d8..56414a2b 100644
--- a/src/lib/QueryRendererHelper.ts
+++ b/src/lib/QueryRendererHelper.ts
@@ -1,6 +1,9 @@
+import type { Moment, unitOfTime } from 'moment';
 import type { GlobalFilter } from '../Config/GlobalFilter';
 import type { GlobalQuery } from '../Config/GlobalQuery';
 import { Query } from '../Query/Query';
+import { Task } from '../Task';
+import { TasksDate } from '../Scripting/TasksDate';
 
 /**
  * @summary
@@ -68,3 +71,21 @@ export function getQueryForQueryRenderer(source: string, globalQuery: GlobalQuer
 
     return globalQuery.query(path).append(tasksBlockQuery);
 }
+
+export function getDateFieldToPostpone(task: Task) {
+    const scheduledDateOrNull = task.scheduledDate ? 'scheduledDate' : null;
+    const dateTypeToUpdate = task.dueDate ? 'dueDate' : scheduledDateOrNull;
+    return dateTypeToUpdate;
+}
+
+export function createPostponedTask(
+    task: Task,
+    dateTypeToUpdate: keyof Task,
+    timeUnit: unitOfTime.DurationConstructor,
+    amount: number,
+) {
+    const dateToUpdate = task[dateTypeToUpdate] as Moment;
+    const postponedDate = new TasksDate(dateToUpdate).postpone(timeUnit, amount);
+    const newTasks = new Task({ ...task, [dateTypeToUpdate]: postponedDate });
+    return { postponedDate, newTasks };
+}


Test patch:
diff --git a/tests/lib/QueryRendererHelper.test.ts b/tests/lib/QueryRendererHelper.test.ts
index ec419e14..185365b6 100644
--- a/tests/lib/QueryRendererHelper.test.ts
+++ b/tests/lib/QueryRendererHelper.test.ts
@@ -3,9 +3,11 @@
  */
 import moment from 'moment';
 import { Query } from '../../src/Query/Query';
-import { explainResults, getQueryForQueryRenderer } from '../../src/lib/QueryRendererHelper';
+import { explainResults, getDateFieldToPostpone, getQueryForQueryRenderer } from '../../src/lib/QueryRendererHelper';
 import { GlobalFilter } from '../../src/Config/GlobalFilter';
 import { GlobalQuery } from '../../src/Config/GlobalQuery';
+import { TaskBuilder } from '../TestingTools/TaskBuilder';
+import type { Task } from '../../src/Task';
 
 window.moment = moment;
 
@@ -126,3 +128,58 @@ describe('query used for QueryRenderer', () => {
         expect(query.filePath).toEqual(filePath);
     });
 });
+
+describe('postpone - date field choice', () => {
+    function checkPostponeField(taskBuilder: TaskBuilder, expected: keyof Task | null) {
+        const task = taskBuilder.build();
+        expect(getDateFieldToPostpone(task)).toEqual(expected);
+    }
+
+    function checkDoesNotPostpone(taskBuilder: TaskBuilder) {
+        checkPostponeField(taskBuilder, null);
+    }
+
+    // Since the actual date values do not affect the calculation, we use the same value for all tests,
+    // so that the field names stand out when comparing tests.
+    const date = '2023-11-26';
+
+    it('should not postpone if no happens dates on task', () => {
+        const taskBuilder = new TaskBuilder();
+        checkDoesNotPostpone(taskBuilder);
+    });
+
+    it('should not postpone created or done dates', () => {
+        const taskBuilder = new TaskBuilder().createdDate(date).doneDate(date);
+        checkDoesNotPostpone(taskBuilder);
+    });
+
+    it('should postpone due date', () => {
+        const taskBuilder = new TaskBuilder().dueDate(date);
+        checkPostponeField(taskBuilder, 'dueDate');
+    });
+
+    it('should postpone scheduled date', () => {
+        const taskBuilder = new TaskBuilder().scheduledDate(date);
+        checkPostponeField(taskBuilder, 'scheduledDate');
+    });
+
+    it('should postpone when scheduled date is inferred', () => {
+        const taskBuilder = new TaskBuilder().scheduledDate(date).scheduledDateIsInferred(true);
+        checkPostponeField(taskBuilder, 'scheduledDate');
+    });
+
+    it('should not postpone start date', () => {
+        const taskBuilder = new TaskBuilder().startDate(date);
+        checkDoesNotPostpone(taskBuilder);
+    });
+
+    it('should postpone due date in preference to start and scheduled dates', () => {
+        const taskBuilder = new TaskBuilder().dueDate(date).scheduledDate(date).startDate(date);
+        checkPostponeField(taskBuilder, 'dueDate');
+    });
+
+    it('should postpone scheduled date in preference to start date', () => {
+        const taskBuilder = new TaskBuilder().scheduledDate(date).startDate(date);
+        checkPostponeField(taskBuilder, 'scheduledDate');
+    });
+});