Repository: obsidian-tasks-group/obsidian-tasks
Pull request #2013 (2023-06-03 14:32:10)

test: Add TaskBuilder.createFullyPopulatedTask()
<!--- Provide a general summary of your changes in the Title above -->

# Description

<!--- Describe your changes in detail -->

**Main:**

- `TaskBuilder.createFullyPopulatedTask()` returns a `Task` object with a realistic value in every field.
    - And its tests should ensure that when new fields are added to `Task`, new values will be added here too.
- `TaskBuilder.build()` now sets `Task.originalMarkdown`.

**Other:**

- `Status` and `StatusConfiguration` now have `identicalTo()` methods
- Fixed a bug in `Task.identicalTo()` that two tasks were considered different if they had different instances of `Status` that had identical values
    - As far as I can see, from manual testing, this does not affect the released Tasks plugin, as statuses are obtained from the `StatusRegistry`, so the same `Status` instance is shared between tasks with the same status symbol.
- `Recurrence.fromText()` logs an error if there was an error cloning dates, due to malformed test code not setting up `window.moment`

## Motivation and Context

<!--- Why is this change required? What problem does it solve? -->
<!--- If it fixes an open issue, please link to the issue here. -->

I want to generate some documentation that contains representative values for a Task object.

## 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. -->

By adding new tests

## 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] **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`.
- [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/Recurrence.ts b/src/Recurrence.ts
index a984321c..a4670e29 100644
--- a/src/Recurrence.ts
+++ b/src/Recurrence.ts
@@ -97,8 +97,12 @@ export class Recurrence {
                     dueDate,
                 });
             }
-        } catch (error) {
+        } catch (e) {
             // Could not read recurrence rule. User possibly not done typing.
+            // Print error message, as it is useful if a test file has not set up window.moment
+            if (e instanceof Error) {
+                console.log(e.message);
+            }
         }
 
         return null;
diff --git a/src/Status.ts b/src/Status.ts
index 6efa4475..ccb24cc2 100644
--- a/src/Status.ts
+++ b/src/Status.ts
@@ -211,6 +211,17 @@ export class Status {
         return this.type === StatusType.DONE;
     }
 
+    /**
+     * Compare all the fields in another Status, to detect any differences from this one.
+     *
+     * If any field is different in any way, it will return false.
+     *
+     * @param other
+     */
+    public identicalTo(other: Status): boolean {
+        return this.configuration.identicalTo(other.configuration);
+    }
+
     /**
      * Return a one-line summary of the status, for presentation to users.
      */
diff --git a/src/StatusConfiguration.ts b/src/StatusConfiguration.ts
index 0d9e1f94..a3aa49b2 100644
--- a/src/StatusConfiguration.ts
+++ b/src/StatusConfiguration.ts
@@ -79,4 +79,25 @@ export class StatusConfiguration {
         this.availableAsCommand = availableAsCommand;
         this.type = type;
     }
+
+    /**
+     * Compare all the fields in another StatusConfiguration, to detect any differences from this one.
+     *
+     * If any field is different in any way, it will return false.
+     *
+     * @param other
+     */
+    public identicalTo(other: StatusConfiguration): boolean {
+        const args: Array<keyof StatusConfiguration> = [
+            'symbol',
+            'name',
+            'nextStatusSymbol',
+            'availableAsCommand',
+            'type',
+        ];
+        for (const el of args) {
+            if (this[el] !== other[el]) return false;
+        }
+        return true;
+    }
 }
diff --git a/src/Task.ts b/src/Task.ts
index 35760513..b222038a 100644
--- a/src/Task.ts
+++ b/src/Task.ts
@@ -117,7 +117,8 @@ export class Task {
     public readonly doneDate: Moment | null;
 
     public readonly recurrence: Recurrence | null;
-    /** The blockLink is a "^" annotation after the dates/recurrence rules. */
+    /** The blockLink is a "^" annotation after the dates/recurrence rules.
+     * Any non-empty value must begin with ' ^'. */
     public readonly blockLink: string;
 
     /** The original line read from file.
@@ -189,7 +190,7 @@ export class Task {
     }
 
     /**
-     * Takes the given line from a obsidian note and returns a Task object.
+     * Takes the given line from an obsidian note and returns a Task object.
      *
      * @static
      * @param {string} line - The full line in the note to parse.
@@ -268,7 +269,7 @@ export class Task {
     /**
      * Create an HTML rendered List Item element (LI) for the current task.
      * @note Output is based on the {@link DefaultTaskSerializer}'s format, with default (emoji) symbols
-     * @param {renderTails}
+     * @param renderDetails
      */
     public async toLi(renderDetails: TaskLineRenderDetails): Promise<HTMLLIElement> {
         return renderTaskLine(this, renderDetails);
@@ -358,7 +359,7 @@ export class Task {
                 // New occurrences cannot have the same block link.
                 // And random block links don't help.
                 blockLink: '',
-                // add new createdDate on reccuring tasks
+                // add new createdDate on recurring tasks
                 createdDate,
             });
             newTasks.push(nextTask);
@@ -502,7 +503,6 @@ export class Task {
         //       any of the tasks in a file. This does mean that redrawing of tasks blocks
         //       happens more often than is ideal.
         let args: Array<keyof Task> = [
-            'status',
             'description',
             'path',
             'indentation',
@@ -519,6 +519,10 @@ export class Task {
             if (this[el] !== other[el]) return false;
         }
 
+        if (!this.status.identicalTo(other.status)) {
+            return false;
+        }
+
         // Compare tags
         if (this.tags.length !== other.tags.length) {
             return false;


Test patch:
diff --git a/tests/Status.test.ts b/tests/Status.test.ts
index 13679dc8..da8446c3 100644
--- a/tests/Status.test.ts
+++ b/tests/Status.test.ts
@@ -124,3 +124,13 @@ describe('Status', () => {
         expect(status.availableAsCommand).toEqual(false);
     });
 });
+
+describe('identicalTo', () => {
+    it('should check StatusConfiguration', () => {
+        const lhs = new Status(new StatusConfiguration('P', 'Pro', 'C', true, StatusType.TODO));
+        expect(lhs.identicalTo(lhs)).toEqual(true);
+
+        const rhs = new Status(new StatusConfiguration('P', 'Maybe', 'C', true, StatusType.TODO));
+        expect(lhs.identicalTo(rhs)).toEqual(false);
+    });
+});
diff --git a/tests/StatusConfiguration.test.ts b/tests/StatusConfiguration.test.ts
new file mode 100644
index 00000000..4f8622cb
--- /dev/null
+++ b/tests/StatusConfiguration.test.ts
@@ -0,0 +1,45 @@
+import { StatusConfiguration, StatusType } from '../src/StatusConfiguration';
+
+describe('identicalTo', () => {
+    const symbol = 'P';
+    const name = 'Pro';
+    const nextStatusSymbol = 'C';
+    const availableAsCommand = true;
+    const type = StatusType.TODO;
+
+    it('should detect identical objects', () => {
+        const lhs = new StatusConfiguration(symbol, name, nextStatusSymbol, availableAsCommand, type);
+        const rhs = new StatusConfiguration(symbol, name, nextStatusSymbol, availableAsCommand, type);
+        expect(lhs.identicalTo(rhs)).toEqual(true);
+    });
+
+    it('should check symbol', () => {
+        const lhs = new StatusConfiguration(symbol, name, nextStatusSymbol, availableAsCommand, type);
+        const rhs = new StatusConfiguration('Q', name, nextStatusSymbol, availableAsCommand, type);
+        expect(lhs.identicalTo(rhs)).toEqual(false);
+    });
+
+    it('should check name', () => {
+        const lhs = new StatusConfiguration(symbol, name, nextStatusSymbol, availableAsCommand, type);
+        const rhs = new StatusConfiguration(symbol, 'Con', nextStatusSymbol, availableAsCommand, type);
+        expect(lhs.identicalTo(rhs)).toEqual(false);
+    });
+
+    it('should check nextStatusSymbol', () => {
+        const lhs = new StatusConfiguration(symbol, name, nextStatusSymbol, availableAsCommand, type);
+        const rhs = new StatusConfiguration(symbol, name, ' ', availableAsCommand, type);
+        expect(lhs.identicalTo(rhs)).toEqual(false);
+    });
+
+    it('should check availableAsCommand', () => {
+        const lhs = new StatusConfiguration(symbol, name, nextStatusSymbol, availableAsCommand, type);
+        const rhs = new StatusConfiguration(symbol, name, nextStatusSymbol, false, type);
+        expect(lhs.identicalTo(rhs)).toEqual(false);
+    });
+
+    it('should check type', () => {
+        const lhs = new StatusConfiguration(symbol, name, nextStatusSymbol, availableAsCommand, StatusType.CANCELLED);
+        const rhs = new StatusConfiguration(symbol, name, nextStatusSymbol, availableAsCommand, type);
+        expect(lhs.identicalTo(rhs)).toEqual(false);
+    });
+});
diff --git a/tests/Task.test.ts b/tests/Task.test.ts
index b3d945c2..7e3ac67f 100644
--- a/tests/Task.test.ts
+++ b/tests/Task.test.ts
@@ -1114,7 +1114,7 @@ describe('order of recurring tasks', () => {
 
 describe('identicalTo', () => {
     it('should check status', () => {
-        const lhs = new TaskBuilder().status(Status.TODO);
+        const lhs = new TaskBuilder().status(Status.makeTodo());
         expect(lhs).toBeIdenticalTo(new TaskBuilder().status(Status.TODO));
         expect(lhs).not.toBeIdenticalTo(new TaskBuilder().status(Status.DONE));
     });
diff --git a/tests/TestingTools/TaskBuilder.test.ts b/tests/TestingTools/TaskBuilder.test.ts
index 3d0576b8..3394ca06 100644
--- a/tests/TestingTools/TaskBuilder.test.ts
+++ b/tests/TestingTools/TaskBuilder.test.ts
@@ -1,11 +1,64 @@
+/**
+ * @jest-environment jsdom
+ */
+
+import moment from 'moment';
+import type { Task } from '../../src/Task';
 import { TaskBuilder } from './TaskBuilder';
 
 export {};
 
+window.moment = moment;
+
 describe('TaskBuilder', () => {
     it('should add the tags to the description', () => {
         const builder = new TaskBuilder().description('hello').tags(['#tag1', '#tag2']);
         const task = builder.build();
         expect(task.toFileLineString()).toStrictEqual('- [ ] hello #tag1 #tag2');
     });
+
+    it('should populate originalMarkdown', () => {
+        const builder = new TaskBuilder();
+        const task = builder.description('hello').build();
+        expect(task.originalMarkdown).toEqual('- [ ] hello');
+    });
+
+    function hasValue<Type>(value: Type[keyof Type]): boolean {
+        if (typeof value === 'boolean') {
+            // false is valid for booleans...
+            return true;
+        }
+
+        if (Array.isArray(value)) {
+            // Check for empty arrays:
+            return value.length > 0;
+        }
+
+        // Check that values are non-null, strings are not empty...
+        return !!value;
+    }
+
+    function getNullOrUnsetFields<Type>(type: Type) {
+        // @ts-ignore
+        const args: Array<keyof Type> = Object.getOwnPropertyDescriptors(type);
+        const nullOrUnsetFields: string[] = [];
+        for (const key in args) {
+            const value = type[key as keyof Type];
+            if (!hasValue(value)) {
+                nullOrUnsetFields.push(key);
+            }
+        }
+        return nullOrUnsetFields;
+    }
+
+    it('createFullyPopulatedTask() should populate every field', () => {
+        const task: Task = TaskBuilder.createFullyPopulatedTask();
+
+        expect(getNullOrUnsetFields(task)).toEqual([]);
+        expect(getNullOrUnsetFields(task.taskLocation)).toEqual([]);
+
+        expect(task.originalMarkdown).toEqual(
+            '  - [ ] Do exercises #todo #health 🔼 🔁 every day when done ➕ 2023-07-01 🛫 2023-07-02 ⏳ 2023-07-03 📅 2023-07-04 ✅ 2023-07-05 ^dcf64c',
+        );
+    });
 });
diff --git a/tests/TestingTools/TaskBuilder.ts b/tests/TestingTools/TaskBuilder.ts
index 0b5a7815..187641b9 100644
--- a/tests/TestingTools/TaskBuilder.ts
+++ b/tests/TestingTools/TaskBuilder.ts
@@ -2,7 +2,7 @@
 import type { Moment } from 'moment';
 import { Status } from '../../src/Status';
 import { Priority, Task } from '../../src/Task';
-import type { Recurrence } from '../../src/Recurrence';
+import { Recurrence } from '../../src/Recurrence';
 import { DateParser } from '../../src/Query/DateParser';
 import { StatusConfiguration, StatusType } from '../../src/StatusConfiguration';
 import { TaskLocation } from '../../src/TaskLocation';
@@ -20,7 +20,7 @@ import { TaskLocation } from '../../src/TaskLocation';
  *            Create a new TaskBuilder object to start from a clean state,
  */
 export class TaskBuilder {
-    private _status: Status = Status.TODO;
+    private _status: Status = Status.makeTodo();
     private _description: string = 'my description';
     private _path: string = '';
     private _indentation: string = '';
@@ -62,7 +62,7 @@ export class TaskBuilder {
         if (this._tags.length > 0) {
             description += ' ' + this._tags.join(' ');
         }
-        return new Task({
+        const task = new Task({
             status: this._status,
             description: description,
             taskLocation: new TaskLocation(
@@ -86,6 +86,51 @@ export class TaskBuilder {
             originalMarkdown: '',
             scheduledDateIsInferred: this._scheduledDateIsInferred,
         });
+        const markdown = task.toFileLineString();
+        return new Task({
+            ...task,
+            originalMarkdown: markdown,
+        });
+    }
+
+    /**
+     * Create a Task that has all fields populated.
+     */
+    public static createFullyPopulatedTask(): Task {
+        const taskBuilder = new TaskBuilder()
+            .indentation('  ')
+            .description('Do exercises')
+            .tags(['#todo', '#health'])
+            .priority(Priority.Medium)
+            .createdDate('2023-07-01')
+            .startDate('2023-07-02')
+            .scheduledDate('2023-07-03')
+            .dueDate('2023-07-04')
+            .doneDate('2023-07-05')
+            .blockLink(' ^dcf64c')
+            // Values in TaskLocation:
+            .path('/some/folder/fileName.md')
+            .lineNumber(17)
+            .sectionStart(5)
+            .sectionIndex(3)
+            .precedingHeader('My Header');
+
+        taskBuilder.recurrence(
+            Recurrence.fromText({
+                recurrenceRuleText: 'every day when done',
+                startDate: taskBuilder._startDate,
+                scheduledDate: taskBuilder._scheduledDate,
+                dueDate: taskBuilder._dueDate,
+            }),
+        );
+
+        const task = taskBuilder.build();
+
+        // Force urgency value to be cached:
+        // @ts-ignore
+        const unused = task!.urgency;
+
+        return task;
     }
 
     /**