Repository: obsidian-tasks-group/obsidian-tasks (ts)

Issue:
Failure to redraw Tasks block results when signifier dates change from valid to invalid - and the reverse
### Expected Behavior

1. When I edit a Task's due date, or any other date - and change the date from **valid to invalid**, I expect any Tasks code block results that are displayed to show the modified date.
1. When I edit a Task's due date, or any other date - and change the date from **invalid to valid**, I expect any Tasks code block results that are displayed to show the modified date.


### Current behaviour

When I edit a Task's due date, or any other date - and change the date from **valid to invalid**, or the reverse, any Tasks code block results that are displaying the changed task do not update.

I have to edit a character in the description to trigger the update of the date.

[Video demoing the problem](https://youtu.be/voZWnfZx2Xk).

### Steps to reproduce

1. Create this task:

```text
- [ ] #task Testing refresh of search results when date validity changes 📅 2023-02-28
```

2. Add a tasks block in the same file:

````text
```tasks
path includes <name of the file containing the above task>
hide backlink
```
````

3. Open the file in Source, and Reading views, side-by-side.

4. Change the due date from `2023-02-28` to `2023-02-29`

Expected result:
The task showing in the Tasks block in Reading mode should be updated to show that the date is invalid.

Actual result:
The task showing in the Tasks block in Reading mode retains the previous, valid date.

5. Edit a character in the description of the task.

You will then see the task in the tasks block be updated to show the edited character and the invalid date.

### Which Operating Systems are you using?

- [ ] Android
- [ ] iPhone/iPad
- [ ] Linux
- [X] macOS
- [ ] Windows

### Obsidian Version

0.16.5

### Tasks Plugin Version

1.15.1

### Checks

- [X] I have tried it with all other plugins disabled and the error still occurs

### Possible solution

The problem will be in the code for checking whether tasks are identical or not.

It is not accounting for invalid dates.

Pull request description:
fix: Redraw Tasks results when dates change from valid to invalid 
<!--- Provide a general summary of your changes in the Title above -->

# Description

Fixes an error that valid and invalid dates were considered to be identical when sorting, which meant that if a user edited a task to change its due date from invalid to valid, no tasks blocks were updated to show the new data.

## Motivation and Context

This fixes #1227:
"Failure to redraw Tasks block results when signifier dates change from valid to invalid - and the reverse"

## How has this been tested?

- Running existing tests
- Adding new tests
- Manually testing a new build of the plugin, to confirm that the bug is fixed

## Screenshots (if appropriate)

## Types of changes

<!--- What types of changes does your code introduce? Put an `x` in all the boxes that apply: -->

Changes visible to users:

- [x] **Bug fix** (prefix: `fix` - non-breaking change which fixes an issue)

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`.
- [ ] My change requires a change to the documentation.
- [ ] I have [updated the documentation](https://github.com/obsidian-tasks-group/obsidian-tasks/blob/main/CONTRIBUTING.md#updating-documentation) accordingly.
- [x] My change has adequate [Unit Test coverage](https://github.com/obsidian-tasks-group/obsidian-tasks/blob/main/CONTRIBUTING.md#maintaining-the-tests).

## Terms

<!--
By submitting this pull request, you must agree to follow our
[contributing guide](https://github.com/obsidian-tasks-group/obsidian-tasks/blob/main/CONTRIBUTING.md) 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://github.com/obsidian-tasks-group/obsidian-tasks/blob/main/CONTRIBUTING.md)
- [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/Query/Sort.ts b/src/Query/Sort.ts
index d085d873..638e446e 100644
--- a/src/Query/Sort.ts
+++ b/src/Query/Sort.ts
@@ -132,6 +132,12 @@ export class Sort {
         } else if (a === null && b !== null) {
             return 1;
         } else if (a !== null && b !== null) {
+            if (a.isValid() && !b.isValid()) {
+                return -1;
+            } else if (!a.isValid() && b.isValid()) {
+                return 1;
+            }
+
             if (a.isAfter(b)) {
                 return 1;
             } else if (a.isBefore(b)) {


Test patch:
diff --git a/tests/Sort.test.ts b/tests/Sort.test.ts
index 67e61c81..06553743 100644
--- a/tests/Sort.test.ts
+++ b/tests/Sort.test.ts
@@ -7,6 +7,7 @@ window.moment = moment;
 
 import { Sort } from '../src/Query/Sort';
 import { resetSettings, updateSettings } from '../src/Config/Settings';
+import { DateParser } from '../src/Query/DateParser';
 import { fromLine } from './TestHelpers';
 
 describe('Sort', () => {
@@ -289,6 +290,67 @@ describe('Sort', () => {
     });
 });
 
+expect.extend({
+    toGiveCompareToResult(dates: (string | null)[], expected: -1 | 0 | 1) {
+        expect(dates.length).toEqual(2);
+
+        const dateA = dates[0];
+        const dateB = dates[1];
+
+        let a: moment.Moment | null = null;
+        if (dateA !== null) a = DateParser.parseDate(dateA);
+
+        let b: moment.Moment | null = null;
+        if (dateB !== null) b = DateParser.parseDate(dateB);
+
+        const actual = Sort.compareByDate(a, b);
+
+        const pass = actual === expected;
+        const message = () => `${dateA} < ${dateB}: expected=${expected} actual=${actual}`;
+
+        return { pass, message };
+    },
+});
+
+declare global {
+    namespace jest {
+        interface Matchers<R> {
+            toGiveCompareToResult(expected: number): R;
+        }
+    }
+}
+
+// These are lower-level tests that the Task-based ones above, for ease of test coverage.
+describe('compareBy', () => {
+    it('compares correctly by date', () => {
+        const equal = 0;
+        const after = 1;
+        const before = -1;
+
+        const earlierDate = '2022-01-01';
+        const latererDate = '2022-02-01'; // intentional type - laterer - so all variable names align in code
+        const invalidDate = '2022-02-30';
+
+        testCompareByDateBothWays(earlierDate, latererDate, before);
+        testCompareByDateBothWays(earlierDate, earlierDate, equal);
+        testCompareByDateBothWays(latererDate, earlierDate, after);
+
+        testCompareByDateBothWays(null, earlierDate, after); // no date sorts after valid dates
+        testCompareByDateBothWays(null, null, equal);
+
+        testCompareByDateBothWays(invalidDate, null, before); // invalid dates sort before no date
+        testCompareByDateBothWays(invalidDate, invalidDate, equal);
+        testCompareByDateBothWays(invalidDate, earlierDate, after); // invalid dates sort after valid ones
+
+        function testCompareByDateBothWays(dateA: string | null, dateB: string | null, expected: -1 | 0 | 1) {
+            expect([dateA, dateB]).toGiveCompareToResult(expected);
+
+            const reverseExpected = expected === equal ? equal : -expected;
+            expect([dateB, dateA]).toGiveCompareToResult(reverseExpected);
+        }
+    });
+});
+
 /*
  * All the test cases below have tasks with 0 or more tags against them. This is to
  * ensure that the sorting can handle the ordering correctly when there are no tags or