Repository: obsidian-tasks-group/obsidian-tasks
Pull request #1555 (2023-01-20 11:19:07)

feat: Nicer syntax and better help for 'status.type'
<!--- Provide a general summary of your changes in the Title above -->

# Description

`status.type includes IN_PROGRESS`
=>
`status.type is IN_PROGRESS`

The code in `StatusTypeField` is a lot more complicated, but I'm really proud of the help message it generates if presented with an invalid instruction. (See Screenshot below).


## 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. -->
This change was prompted by the valid confusion in [this feedback](https://github.com/obsidian-tasks-group/obsidian-tasks/discussions/1530#discussioncomment-4703974) on a recent test release.

## 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. -->

- Unit tests
- Manual testing (which showed the need to format the error text now, so lines wrap.)

## Screenshots (if appropriate)

Sample output if there is an error in the instruction:

![image](https://user-images.githubusercontent.com/4840096/213682601-932ecf45-b800-4a9c-b4dc-ce154785a515.png)

## 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)- [x] **New feature** (prefix: `feat` - non-breaking change which adds functionality)
- [x] **Documentation** (prefix: `docs` - improvements to any documentation content)

Technically this is a breaking change for the few users of the recent test release. However, it's a major improvement both in the syntax and also the ability to write a useful help message for invalid `status.type` instructions.


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`.
- [x] My change requires a change to the documentation.
- [x] 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/docs/getting-started/statuses.md b/docs/getting-started/statuses.md
index b1c2d4b0..d7a0e387 100644
--- a/docs/getting-started/statuses.md
+++ b/docs/getting-started/statuses.md
@@ -27,13 +27,13 @@ has_toc: false
 - `status name`
   - a name for the status.
   - this is flexible: for custom statuses, you can use any name you wish.
-  - is searchable with `status.name`, for example `status.name includes todo`.
+  - is searchable with `status.name`, for example `status.name includes My custom in-progress status`.
 - `next status symbol`
   - the status symbol to use when the task is toggled.
 - `status type`
   - one of `TODO`, `IN_PROGRESS`, `DONE`, `CANCELLED`, `NON_TASK`.
-  - by assigning one of these types to your custom statuses, you can control some aspects of Tasks' behaviour for all task lines that use that particular custom status.
-  - is searchable with `status.type`, for example `status.type includes IN_PROGRESS`.
+  - Tasks needs to know the type of each custom status, so that it knows how to treat them when searching, and what to do when tasks with the status are toggled.
+  - is searchable with `status.type`, for example `status.type is IN_PROGRESS`.
   - you can have any number of custom statuses with the same status type, and then search them conveniently with `status.type`
 
 ### Status Types
@@ -51,14 +51,14 @@ The tasks shown are purely examples for context. The `~` column is just an arbit
 | Matches `done` | no | no | YES | YES | no |
 | Matches `not done` | YES | YES | no | no | no |
 | Matches `status.name includes todo` | YES | no | no | no | no |
-| Matches `status.type includes TODO` | YES | no | no | no | no |
+| Matches `status.type is TODO` | YES | no | no | no | no |
 | Matches `status.name includes in progress` | no | YES | no | no | no |
-| Matches `status.type includes IN_PROGRESS` | no | YES | no | no | no |
+| Matches `status.type is IN_PROGRESS` | no | YES | no | no | no |
 | Matches `status.name includes done` | no | no | YES | no | no |
-| Matches `status.type includes DONE` | no | no | YES | no | no |
+| Matches `status.type is DONE` | no | no | YES | no | no |
 | Matches `status.name includes cancelled` | no | no | no | YES | no |
-| Matches `status.type includes CANCELLED` | no | no | no | YES | no |
-| Matches `status.type includes NON_TASK` | no | no | no | no | YES |
+| Matches `status.type is CANCELLED` | no | no | no | YES | no |
+| Matches `status.type is NON_TASK` | no | no | no | no | YES |
 | Name for `group by status` | Todo | Done | Done | Done | Done |
 | Name for `group by status.type` | 2 TODO | 1 IN_PROGRESS | 3 DONE | 4 CANCELLED | 5 NON_TASK |
 | Name for `group by status.name` | Todo | In Progress | Done | Cancelled | My custom status |
@@ -92,7 +92,7 @@ This table shows the statuses provided by default:
 
 <!-- placeholder to force blank line before table --> <!-- include: DocsSamplesForStatuses.test.DefaultStatuses_core-statuses.approved.md -->
 
-| Status Character | Status Name<br>`status.name includes...`<br>`sort by status.name`<br>`group by status.name` | Next Status Character | Status Type<br>`status.type includes...`<br>`sort by status.type`<br>`group by status.type` | Needs Custom Styling |
+| Status Character | Status Name<br>`status.name includes...`<br>`sort by status.name`<br>`group by status.name` | Next Status Character | Status Type<br>`status.type is...`<br>`sort by status.type`<br>`group by status.type` | Needs Custom Styling |
 | ----- | ----- | ----- | ----- | ----- |
 | `space` | Todo | `x` | `TODO` | No |
 | `/` | In Progress | `x` | `IN_PROGRESS` | Yes |
@@ -137,7 +137,7 @@ Remember to set up your chosen CSS Snippet or Theme before setting up the custom
 
 <!-- placeholder to force blank line before table --> <!-- include: DocsSamplesForStatuses.test.DefaultStatuses_minimal-supported-statuses.approved.md -->
 
-| Status Character | Status Name<br>`status.name includes...`<br>`sort by status.name`<br>`group by status.name` | Next Status Character | Status Type<br>`status.type includes...`<br>`sort by status.type`<br>`group by status.type` | Needs Custom Styling |
+| Status Character | Status Name<br>`status.name includes...`<br>`sort by status.name`<br>`group by status.name` | Next Status Character | Status Type<br>`status.type is...`<br>`sort by status.type`<br>`group by status.type` | Needs Custom Styling |
 | ----- | ----- | ----- | ----- | ----- |
 | `>` | Forwarded | `x` | `TODO` | Yes |
 | `<` | Schedule | `x` | `TODO` | Yes |
@@ -165,7 +165,7 @@ Remember to set up your chosen CSS Snippet or Theme before setting up the custom
 
 <!-- placeholder to force blank line before table --> <!-- include: DocsSamplesForStatuses.test.DefaultStatuses_its-theme-supported-statuses.approved.md -->
 
-| Status Character | Status Name<br>`status.name includes...`<br>`sort by status.name`<br>`group by status.name` | Next Status Character | Status Type<br>`status.type includes...`<br>`sort by status.type`<br>`group by status.type` | Needs Custom Styling |
+| Status Character | Status Name<br>`status.name includes...`<br>`sort by status.name`<br>`group by status.name` | Next Status Character | Status Type<br>`status.type is...`<br>`sort by status.type`<br>`group by status.type` | Needs Custom Styling |
 | ----- | ----- | ----- | ----- | ----- |
 | `>` | Forward | `x` | `TODO` | Yes |
 | `D` | Deferred/Scheduled | `x` | `TODO` | Yes |
diff --git a/docs/how-to/set-up-custom-statuses.md b/docs/how-to/set-up-custom-statuses.md
index 3ff91595..43f1ef07 100644
--- a/docs/how-to/set-up-custom-statuses.md
+++ b/docs/how-to/set-up-custom-statuses.md
@@ -73,7 +73,7 @@ Suppose that you wanted to create a set of 3 statuses that cycle between each ot
 
 <!-- placeholder to force blank line before table --> <!-- include: DocsSamplesForStatuses.test.DefaultStatuses_important-cycle.approved.md -->
 
-| Status Character | Status Name<br>`status.name includes...`<br>`sort by status.name`<br>`group by status.name` | Next Status Character | Status Type<br>`status.type includes...`<br>`sort by status.type`<br>`group by status.type` | Needs Custom Styling |
+| Status Character | Status Name<br>`status.name includes...`<br>`sort by status.name`<br>`group by status.name` | Next Status Character | Status Type<br>`status.type is...`<br>`sort by status.type`<br>`group by status.type` | Needs Custom Styling |
 | ----- | ----- | ----- | ----- | ----- |
 | `!` | Important | `D` | `TODO` | Yes |
 | `D` | Doing - Important | `X` | `IN_PROGRESS` | Yes |
diff --git a/docs/queries/filters.md b/docs/queries/filters.md
index 16242e77..ef1b642e 100644
--- a/docs/queries/filters.md
+++ b/docs/queries/filters.md
@@ -220,19 +220,12 @@ For more information, including adding your own customised statuses, see [Status
 
 ### Status Type
 
+- `status.type (is|is not) (TODO|DONE|IN_PROGRESS|CANCELLED|NON_TASK)`
+  - The values `TODO` etc are case-insensitive: you can use `in_progress`, for example
 - This searches the types you have given to your custom statuses.
-- The following status types are available:
-  - `TODO`
-  - `IN_PROGRESS`
-  - `DONE`
-  - `CANCELLED`
-  - `NON_TASK`
 - This search is efficient if you wish to find all tasks that are `IN_PROGRESS`, and you have set up your statuses to have `[/]`, `[d]` and perhaps several other all treated as `IN_PROGRESS`.
-- `status.type (includes|does not include) <string>`
-  - Matches case-insensitive (disregards capitalization).
-- `status.type (regex matches|regex does not match) /<JavaScript-style Regex>/`
-  - Does regular expression match (case-sensitive by default).
-  - Essential reading: [Regular Expression Searches]({{ site.baseurl }}{% link queries/regular-expressions.md %}).
+- To exclude multiple values, you can use multiple `status.type is not` lines.
+- To allow multiple values, use a boolean combination, for example: `( status.type is TODO ) OR ( status.type is IN_PROGRESS )`.
 
 {: .released }
 `status.type` text searching was introduced in Tasks X.Y.Z.
diff --git a/docs/quick-reference/index.md b/docs/quick-reference/index.md
index e5d3dd16..544d7a8e 100644
--- a/docs/quick-reference/index.md
+++ b/docs/quick-reference/index.md
@@ -20,7 +20,7 @@ This table summarizes the filters and other options available inside a `tasks` b
 | `done`<br>`not done`                                                                                                                                                                                | `sort by status`                            | `group by status`      |                        |
 | `done (before, after, on) <date>`<br>`has done date`<br>`no  done date`<br>`done date is invalid`                                                                                                   | `sort by done`                              | `group by done`        | `hide done date`       |
 | `status.name (includes, does not include) <string>`<br>`status.name (regex matches, regex does not match) /regex/i`                                                                                 | `sort by status.name`                       | `group by status.name` |                        |
-| `status.type (includes, does not include) <string>`<br>`status.type (regex matches, regex does not match) /regex/i`                                                                                 | `sort by status.type`                       | `group by status.type` |                        |
+| `status.type (is, is not) (TODO, DONE, IN_PROGRESS, CANCELLED, NON_TASK)`                                                                                                                           | `sort by status.type`                       | `group by status.type` |                        |
 | `starts (before, after, on) <date>`<br>`has start date`<br>`no  start date`<br>`start date is invalid`                                                                                              | `sort by start`                             | `group by start`       | `hide start date`      |
 | `scheduled (before, after, on) <date>`<br>`has scheduled date`<br>`no  scheduled date`<br>`scheduled date is invalid`                                                                               | `sort by scheduled`                         | `group by scheduled`   | `hide scheduled date`  |
 | `due (before, after, on) <date>`<br>`has due date`<br>`no  due date`<br>`due date is invalid`                                                                                                       | `sort by due`                               | `group by due`         | `hide due date`        |
diff --git a/src/Query/Filter/StatusTypeField.ts b/src/Query/Filter/StatusTypeField.ts
index 6f423488..fd6aad93 100644
--- a/src/Query/Filter/StatusTypeField.ts
+++ b/src/Query/Filter/StatusTypeField.ts
@@ -1,14 +1,76 @@
 import type { Task } from '../../Task';
 import type { GrouperFunction } from '../Grouper';
 import { StatusType } from '../../StatusConfiguration';
-import { TextField } from './TextField';
+import type { Comparator } from '../Sorter';
+import { Explanation } from '../Explain/Explanation';
+import { Field } from './Field';
+import { Filter, FilterOrErrorMessage } from './Filter';
+import type { FilterFunction } from './Filter';
 
 /**
  * A ${@link Field} implementation for searching status.type
  */
-export class StatusTypeField extends TextField {
-    constructor() {
-        super();
+export class StatusTypeField extends Field {
+    // -----------------------------------------------------------------------------------------------------------------
+    // Filtering
+    // -----------------------------------------------------------------------------------------------------------------
+    public canCreateFilterForLine(line: string): boolean {
+        // Use a relaxed regexp, just checking field name and not the contents,
+        // so that we can parse the line later and give meaningful errors if user uses invalid values.
+        const relaxedRegExp = new RegExp(`^(?:${this.fieldNameSingularEscaped()})`);
+        return Field.lineMatchesFilter(relaxedRegExp, line);
+    }
+
+    createFilterOrErrorMessage(line: string): FilterOrErrorMessage {
+        const match = Field.getMatch(this.filterRegExp(), line);
+        if (match === null) {
+            // It's OK to get here, because canCreateFilterForLine() uses a more relaxed regexp.
+            return this.helpMessage(line);
+        }
+
+        const [_, filterOperator, statusTypeAsString] = match;
+
+        const statusTypeElement = StatusType[statusTypeAsString.toUpperCase() as keyof typeof StatusType];
+        if (!statusTypeElement) {
+            return this.helpMessage(line);
+        }
+
+        let filterFunction: FilterFunction;
+
+        switch (filterOperator) {
+            case 'is':
+                filterFunction = (task: Task) => {
+                    return task.status.type === statusTypeElement;
+                };
+                break;
+            case 'is not':
+                filterFunction = (task: Task) => {
+                    return task.status.type !== statusTypeElement;
+                };
+                break;
+            default:
+                return this.helpMessage(line);
+        }
+
+        return FilterOrErrorMessage.fromFilter(new Filter(line, filterFunction, new Explanation(line)));
+    }
+
+    protected filterRegExp(): RegExp | null {
+        return new RegExp(`^(?:${this.fieldNameSingularEscaped()}) (is|is not) ([^ ]+)$`);
+    }
+
+    private helpMessage(line: string): FilterOrErrorMessage {
+        const allowedTypes = Object.values(StatusType)
+            .filter((t) => t !== StatusType.EMPTY)
+            .join(' ');
+
+        const message = `Invalid ${this.fieldNameSingular()} instruction: '${line}'.
+    Allowed options: 'is' and 'is not' (without quotes).
+    Allowed values:  ${allowedTypes}
+                     Note: values are case-insensitive,
+                           so 'in_progress' works too, for example.
+    Example:         ${this.fieldNameSingular()} is not NON_TASK`;
+        return FilterOrErrorMessage.fromError(line, message);
     }
 
     public fieldName(): string {
@@ -19,10 +81,24 @@ export class StatusTypeField extends TextField {
         return task.status.type;
     }
 
+    // -----------------------------------------------------------------------------------------------------------------
+    // Sorting
+    // -----------------------------------------------------------------------------------------------------------------
+
     supportsSorting(): boolean {
         return true;
     }
 
+    comparator(): Comparator {
+        return (a: Task, b: Task) => {
+            return this.value(a).localeCompare(this.value(b), undefined, { numeric: true });
+        };
+    }
+
+    // -----------------------------------------------------------------------------------------------------------------
+    // Grouping
+    // -----------------------------------------------------------------------------------------------------------------
+
     public supportsGrouping(): boolean {
         return true;
     }
diff --git a/src/QueryRenderer.ts b/src/QueryRenderer.ts
index 4002c2db..5aff71df 100644
--- a/src/QueryRenderer.ts
+++ b/src/QueryRenderer.ts
@@ -157,7 +157,8 @@ class QueryRenderChild extends MarkdownRenderChild {
             console.debug(`${totalTasksCount} of ${tasks.length} tasks displayed in a block in "${this.filePath}"`);
             this.addTaskCount(content, totalTasksCount);
         } else if (this.query.error !== undefined) {
-            content.setText(`Tasks query: ${this.query.error}`);
+            content.createDiv().innerHTML =
+                '<pre>' + `Tasks query: ${this.query.error.replace(/\n/g, '<br>')}` + '</pre>';
         } else {
             content.setText('Loading Tasks ...');
         }


Test patch:
diff --git a/tests/CustomMatchers/CustomMatchersForFilters.ts b/tests/CustomMatchers/CustomMatchersForFilters.ts
index b3fcc79b..09a7968b 100644
--- a/tests/CustomMatchers/CustomMatchersForFilters.ts
+++ b/tests/CustomMatchers/CustomMatchersForFilters.ts
@@ -93,7 +93,8 @@ declare global {
 export function toBeValid(filter: FilterOrErrorMessage) {
     if (filter.filterFunction === undefined) {
         return {
-            message: () => 'unexpected null filter: check your instruction matches your filter class',
+            message: () =>
+                `unexpected null filter: check your instruction matches your filter class.\n       Error message is "${filter.error}".`,
             pass: false,
         };
     }
diff --git a/tests/DocsSamplesForStatuses.test.DefaultStatuses_core-statuses.approved.md b/tests/DocsSamplesForStatuses.test.DefaultStatuses_core-statuses.approved.md
index a7fb618d..c6edc6b2 100644
--- a/tests/DocsSamplesForStatuses.test.DefaultStatuses_core-statuses.approved.md
+++ b/tests/DocsSamplesForStatuses.test.DefaultStatuses_core-statuses.approved.md
@@ -1,6 +1,6 @@
 <!-- placeholder to force blank line before table -->
 
-| Status Character | Status Name<br>`status.name includes...`<br>`sort by status.name`<br>`group by status.name` | Next Status Character | Status Type<br>`status.type includes...`<br>`sort by status.type`<br>`group by status.type` | Needs Custom Styling |
+| Status Character | Status Name<br>`status.name includes...`<br>`sort by status.name`<br>`group by status.name` | Next Status Character | Status Type<br>`status.type is...`<br>`sort by status.type`<br>`group by status.type` | Needs Custom Styling |
 | ----- | ----- | ----- | ----- | ----- |
 | `space` | Todo | `x` | `TODO` | No |
 | `/` | In Progress | `x` | `IN_PROGRESS` | Yes |
diff --git a/tests/DocsSamplesForStatuses.test.DefaultStatuses_important-cycle.approved.md b/tests/DocsSamplesForStatuses.test.DefaultStatuses_important-cycle.approved.md
index e0fb76d5..ee8366fe 100644
--- a/tests/DocsSamplesForStatuses.test.DefaultStatuses_important-cycle.approved.md
+++ b/tests/DocsSamplesForStatuses.test.DefaultStatuses_important-cycle.approved.md
@@ -1,6 +1,6 @@
 <!-- placeholder to force blank line before table -->
 
-| Status Character | Status Name<br>`status.name includes...`<br>`sort by status.name`<br>`group by status.name` | Next Status Character | Status Type<br>`status.type includes...`<br>`sort by status.type`<br>`group by status.type` | Needs Custom Styling |
+| Status Character | Status Name<br>`status.name includes...`<br>`sort by status.name`<br>`group by status.name` | Next Status Character | Status Type<br>`status.type is...`<br>`sort by status.type`<br>`group by status.type` | Needs Custom Styling |
 | ----- | ----- | ----- | ----- | ----- |
 | `!` | Important | `D` | `TODO` | Yes |
 | `D` | Doing - Important | `X` | `IN_PROGRESS` | Yes |
diff --git a/tests/DocsSamplesForStatuses.test.DefaultStatuses_its-theme-supported-statuses.approved.md b/tests/DocsSamplesForStatuses.test.DefaultStatuses_its-theme-supported-statuses.approved.md
index 428b0088..f3c4a244 100644
--- a/tests/DocsSamplesForStatuses.test.DefaultStatuses_its-theme-supported-statuses.approved.md
+++ b/tests/DocsSamplesForStatuses.test.DefaultStatuses_its-theme-supported-statuses.approved.md
@@ -1,6 +1,6 @@
 <!-- placeholder to force blank line before table -->
 
-| Status Character | Status Name<br>`status.name includes...`<br>`sort by status.name`<br>`group by status.name` | Next Status Character | Status Type<br>`status.type includes...`<br>`sort by status.type`<br>`group by status.type` | Needs Custom Styling |
+| Status Character | Status Name<br>`status.name includes...`<br>`sort by status.name`<br>`group by status.name` | Next Status Character | Status Type<br>`status.type is...`<br>`sort by status.type`<br>`group by status.type` | Needs Custom Styling |
 | ----- | ----- | ----- | ----- | ----- |
 | `>` | Forward | `x` | `TODO` | Yes |
 | `D` | Deferred/Scheduled | `x` | `TODO` | Yes |
diff --git a/tests/DocsSamplesForStatuses.test.DefaultStatuses_minimal-supported-statuses.approved.md b/tests/DocsSamplesForStatuses.test.DefaultStatuses_minimal-supported-statuses.approved.md
index 3cd4596b..cd742aa6 100644
--- a/tests/DocsSamplesForStatuses.test.DefaultStatuses_minimal-supported-statuses.approved.md
+++ b/tests/DocsSamplesForStatuses.test.DefaultStatuses_minimal-supported-statuses.approved.md
@@ -1,6 +1,6 @@
 <!-- placeholder to force blank line before table -->
 
-| Status Character | Status Name<br>`status.name includes...`<br>`sort by status.name`<br>`group by status.name` | Next Status Character | Status Type<br>`status.type includes...`<br>`sort by status.type`<br>`group by status.type` | Needs Custom Styling |
+| Status Character | Status Name<br>`status.name includes...`<br>`sort by status.name`<br>`group by status.name` | Next Status Character | Status Type<br>`status.type is...`<br>`sort by status.type`<br>`group by status.type` | Needs Custom Styling |
 | ----- | ----- | ----- | ----- | ----- |
 | `>` | Forwarded | `x` | `TODO` | Yes |
 | `<` | Schedule | `x` | `TODO` | Yes |
diff --git a/tests/DocsSamplesForStatuses.test.Status_Transitions status-types.approved.md b/tests/DocsSamplesForStatuses.test.Status_Transitions status-types.approved.md
index de7f45ed..df3eff02 100644
--- a/tests/DocsSamplesForStatuses.test.Status_Transitions status-types.approved.md
+++ b/tests/DocsSamplesForStatuses.test.Status_Transitions status-types.approved.md
@@ -6,14 +6,14 @@
 | Matches `done` | no | no | YES | YES | no |
 | Matches `not done` | YES | YES | no | no | no |
 | Matches `status.name includes todo` | YES | no | no | no | no |
-| Matches `status.type includes TODO` | YES | no | no | no | no |
+| Matches `status.type is TODO` | YES | no | no | no | no |
 | Matches `status.name includes in progress` | no | YES | no | no | no |
-| Matches `status.type includes IN_PROGRESS` | no | YES | no | no | no |
+| Matches `status.type is IN_PROGRESS` | no | YES | no | no | no |
 | Matches `status.name includes done` | no | no | YES | no | no |
-| Matches `status.type includes DONE` | no | no | YES | no | no |
+| Matches `status.type is DONE` | no | no | YES | no | no |
 | Matches `status.name includes cancelled` | no | no | no | YES | no |
-| Matches `status.type includes CANCELLED` | no | no | no | YES | no |
-| Matches `status.type includes NON_TASK` | no | no | no | no | YES |
+| Matches `status.type is CANCELLED` | no | no | no | YES | no |
+| Matches `status.type is NON_TASK` | no | no | no | no | YES |
 | Name for `group by status` | Todo | Done | Done | Done | Done |
 | Name for `group by status.type` | 2 TODO | 1 IN_PROGRESS | 3 DONE | 4 CANCELLED | 5 NON_TASK |
 | Name for `group by status.name` | Todo | In Progress | Done | Cancelled | My custom status |
diff --git a/tests/DocsSamplesForStatuses.test.ts b/tests/DocsSamplesForStatuses.test.ts
index 0a4dd426..189913f3 100644
--- a/tests/DocsSamplesForStatuses.test.ts
+++ b/tests/DocsSamplesForStatuses.test.ts
@@ -67,7 +67,7 @@ function verifyStatusesAsMarkdownTable(statuses: Status[]) {
         'Status Character',
         'Status Name<br>`status.name includes...`<br>`sort by status.name`<br>`group by status.name`',
         'Next Status Character',
-        'Status Type<br>`status.type includes...`<br>`sort by status.type`<br>`group by status.type`',
+        'Status Type<br>`status.type is...`<br>`sort by status.type`<br>`group by status.type`',
         'Needs Custom Styling',
     ]);
 
@@ -148,14 +148,14 @@ function verifyTransitionsAsMarkdownTable(statuses: Status[]) {
     filterAllStatuses(FilterParser.parseFilter('done')!);
     filterAllStatuses(FilterParser.parseFilter('not done')!);
     filterAllStatuses(FilterParser.parseFilter('status.name includes todo')!);
-    filterAllStatuses(FilterParser.parseFilter('status.type includes TODO')!);
+    filterAllStatuses(FilterParser.parseFilter('status.type is TODO')!);
     filterAllStatuses(FilterParser.parseFilter('status.name includes in progress')!);
-    filterAllStatuses(FilterParser.parseFilter('status.type includes IN_PROGRESS')!);
+    filterAllStatuses(FilterParser.parseFilter('status.type is IN_PROGRESS')!);
     filterAllStatuses(FilterParser.parseFilter('status.name includes done')!);
-    filterAllStatuses(FilterParser.parseFilter('status.type includes DONE')!);
+    filterAllStatuses(FilterParser.parseFilter('status.type is DONE')!);
     filterAllStatuses(FilterParser.parseFilter('status.name includes cancelled')!);
-    filterAllStatuses(FilterParser.parseFilter('status.type includes CANCELLED')!);
-    filterAllStatuses(FilterParser.parseFilter('status.type includes NON_TASK')!);
+    filterAllStatuses(FilterParser.parseFilter('status.type is CANCELLED')!);
+    filterAllStatuses(FilterParser.parseFilter('status.type is NON_TASK')!);
 
     function showGroupNamesForAllTasks(groupName: string, grouperFunction: (task: Task) => string[]) {
         const cells: string[] = ['Name for `group by ' + groupName + '`'];
diff --git a/tests/Query.test.ts b/tests/Query.test.ts
index c8bb3223..aedd2001 100644
--- a/tests/Query.test.ts
+++ b/tests/Query.test.ts
@@ -69,7 +69,7 @@ describe('Query parsing', () => {
         'starts before 2021-12-27',
         'starts on 2021-12-27',
         'status.name includes cancelled',
-        'status.type includes IN_PROGRESS',
+        'status.type is IN_PROGRESS',
         'tag does not include #sometag',
         'tag does not include sometag',
         'tag includes #sometag',
diff --git a/tests/Query/Filter/StatusTypeField.test.ts b/tests/Query/Filter/StatusTypeField.test.ts
index abd77790..810927af 100644
--- a/tests/Query/Filter/StatusTypeField.test.ts
+++ b/tests/Query/Filter/StatusTypeField.test.ts
@@ -9,6 +9,7 @@ import {
 import { TaskBuilder } from '../../TestingTools/TaskBuilder';
 import { StatusType } from '../../../src/StatusConfiguration';
 import { Status } from '../../../src/Status';
+import * as FilterParser from '../../../src/Query/FilterParser';
 
 expect.extend({
     toBeValid,
@@ -39,9 +40,29 @@ describe('status.name', () => {
         expect(filter.value(unknTask)).toStrictEqual('TODO');
     });
 
-    it('status.type includes', () => {
+    it('status.type is', () => {
         // Arrange
-        const filter = new StatusTypeField().createFilterOrErrorMessage('status.type includes IN_PROGRESS');
+        const filter = new StatusTypeField().createFilterOrErrorMessage('status.type is IN_PROGRESS');
+
+        // Assert
+        expect(filter).toBeValid();
+        expect(filter).toMatchTask(inprTask);
+        expect(filter).not.toMatchTask(todoTask);
+    });
+
+    it('status.type is not', () => {
+        // Arrange
+        const filter = new StatusTypeField().createFilterOrErrorMessage('status.type is not IN_PROGRESS');
+
+        // Assert
+        expect(filter).toBeValid();
+        expect(filter).not.toMatchTask(inprTask);
+        expect(filter).toMatchTask(todoTask);
+    });
+
+    it('status.type is - works with incorrect case', () => {
+        // Arrange
+        const filter = new StatusTypeField().createFilterOrErrorMessage('status.type is in_progress');
 
         // Assert
         expect(filter).toBeValid();
@@ -51,12 +72,28 @@ describe('status.name', () => {
 
     it('status-name is not valid', () => {
         // Arrange
-        const filter = new StatusTypeField().createFilterOrErrorMessage('status-type includes NON_TASK');
+        const filter = new StatusTypeField().createFilterOrErrorMessage('status-type is NON_TASK');
 
         // Assert
         // Check that the '.' in status.name is interpreted exactly as a dot.
         expect(filter).not.toBeValid();
     });
+
+    it('status.name with invalid line is parsed and user sees helpful message', () => {
+        // Arrange
+        const filter = FilterParser.parseFilter('status.type gobbledygook');
+
+        // Assert
+        expect(filter).not.toBeValid();
+        expect(filter?.error).toMatchInlineSnapshot(`
+            "Invalid status.type instruction: 'status.type gobbledygook'.
+                Allowed options: 'is' and 'is not' (without quotes).
+                Allowed values:  TODO DONE IN_PROGRESS CANCELLED NON_TASK
+                                 Note: values are case-insensitive,
+                                       so 'in_progress' works too, for example.
+                Example:         status.type is not NON_TASK"
+        `);
+    });
 });
 
 describe('sorting by status.name', () => {