Repository: obsidian-tasks-group/obsidian-tasks
Pull request #2011 (2023-06-02 06:48:32)

chore: Automate content generation of some Priority docs
<!--- Provide a general summary of your changes in the Title above -->

# Description

<!--- Describe your changes in detail -->

### Summary

Use approval test files as snippets for docs and sample markdown files.

Background:

- https://publish.obsidian.md/tasks-contributing/Testing/Approval+Tests
- https://publish.obsidian.md/tasks-contributing/Documentation/Embedding+code+and+data+in+documentation

### Detail

User visible changes:

- The file format samples for `Priority` emojis are now machine-generated in:
    - user docs
    - Tasks-Demo vault
- A side-effect is that some minor white-space errors were fixed

Code changes:

- Added helper functions in `PriorityField` to convert `Priority` enum values to string

Tests

- New file `tests/TaskSerializer/DocsSamplesForTaskFormats.test.ts` generates `*.approved.md` files showing how `Priority` values are represented in Emoji and dataview file formats. 
    - These files are designed to be embedded in to documentation and markdown files
    - When new formats are added, new sample files will be created automatically
    - When priorities are renamed, or more priorities are added, when the tests are run, the approved files will be updated, and mdsnippets can be run automatically for via the GitHub Action to update the docs and sample vault automatically
- `withAllPriorities()` is exported, and now includes a useful description for each task, so the tasks are suitable for inclusion in documentation
- Verify functions in `tests/TestingTools/VerifyMarkdownTable.ts` now use Visual Studio Code's diff tool for showing and approving differences in approval test files
    - This was the only diff tool I could find, in the ones I have available, that handles Emojis correctly, not corrupting them when editing lines containing Emojis.
    - **WARNING**: this may need more thought if there are any contributors who don't have VSCode installed, and are reviewing Approval Test diffs

Notes

- There are other locations in the docs that list the available priorities, such as the list of group names when grouping by priority. These may may be updated in separate steps.

## Motivation and Context

- Start of work towards reducing many repetitious edits in the user docs and sample vault

## How has this been tested?

- With new tests
- Viewing the changed markdown files in Obsidian, in Source and Reading modes

## 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] **Documentation** (prefix: `docs` - improvements to any documentation content **for users**)
- [x] **Sample vault** (prefix: `vault` - improvements to the [Tasks-Demo sample vault](https://github.com/obsidian-tasks-group/obsidian-tasks/tree/main/resources/sample_vaults/Tasks-Demo))

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://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)


claremacrae: @ilandikov You might be interested in this as:

- it's the start of what I was describing when we last paired
- it changes the behaviour of `withAllPriorities()`, which might conflict with what you are working on
- if you had time to look at it, I would welcome any feedback you had
ilandikov: Looks good =)

So the idea here is to use approvals as snippets for docs? Did I get it right?
claremacrae: > So the idea here is to use approvals as snippets for docs? Did I get it right?

Yes, nice summary. Also for including in markdown files in the Test Vault.

Patch:
diff --git a/docs/Reference/Task Formats/Dataview Format.md b/docs/Reference/Task Formats/Dataview Format.md
index 7e394a26..4658bc25 100644
--- a/docs/Reference/Task Formats/Dataview Format.md
+++ b/docs/Reference/Task Formats/Dataview Format.md
@@ -104,14 +104,16 @@ For more information, see [[Dates]].
 > [!Info]
 > These names were chosen for use in Tasks, and are not known to dataview. They can of course be searched in dataview.
 
-```markdown
-- [ ] #task Lowest priority [priority:: lowest]
-- [ ] #task Low priority [priority:: low]
+<!-- snippet: DocsSamplesForTaskFormats.test.Serializer_Priorities_dataview-snippet.approved.md -->
+```md
+- [ ] #task Lowest priority  [priority:: lowest]
+- [ ] #task Low priority  [priority:: low]
 - [ ] #task Normal priority
-- [ ] #task Medium priority [priority:: medium]
-- [ ] #task High priority [priority:: high]
-- [ ] #task Highest priority [priority:: highest]
+- [ ] #task Medium priority  [priority:: medium]
+- [ ] #task High priority  [priority:: high]
+- [ ] #task Highest priority  [priority:: highest]
 ```
+<!-- endSnippet -->
 
 For more information, see [[Priority]].
 
diff --git a/docs/Reference/Task Formats/Tasks Emoji Format.md b/docs/Reference/Task Formats/Tasks Emoji Format.md
index 741c3557..aa6f58b2 100644
--- a/docs/Reference/Task Formats/Tasks Emoji Format.md
+++ b/docs/Reference/Task Formats/Tasks Emoji Format.md
@@ -22,14 +22,16 @@ For more information, see [[Dates]].
 
 ## Tasks Emoji Format for Priorities
 
-```markdown
-- [ ] #task Lowest priority ⏬️
+<!-- snippet: DocsSamplesForTaskFormats.test.Serializer_Priorities_tasksPluginEmoji-snippet.approved.md -->
+```md
+- [ ] #task Lowest priority ⏬
 - [ ] #task Low priority 🔽
 - [ ] #task Normal priority
 - [ ] #task Medium priority 🔼
 - [ ] #task High priority ⏫
 - [ ] #task Highest priority 🔺
 ```
+<!-- endSnippet -->
 
 For more information, see [[Priority]].
 
diff --git a/resources/sample_vaults/Tasks-Demo/Formats/Dataview Format.md b/resources/sample_vaults/Tasks-Demo/Formats/Dataview Format.md
index 1c9b6ae3..3b0b8627 100644
--- a/resources/sample_vaults/Tasks-Demo/Formats/Dataview Format.md
+++ b/resources/sample_vaults/Tasks-Demo/Formats/Dataview Format.md
@@ -12,6 +12,8 @@ The fields shown below can be surrounded by either `[]` or `()`.
 
 ## Dataview Format for Priorities
 
+<!-- placeholder to force blank line before included text --> <!-- include: DocsSamplesForTaskFormats.test.Serializer_Priorities_dataview-include.approved.md -->
+
 - [ ] #task Lowest priority  [priority:: lowest]
 - [ ] #task Low priority  [priority:: low]
 - [ ] #task Normal priority
@@ -19,6 +21,8 @@ The fields shown below can be surrounded by either `[]` or `()`.
 - [ ] #task High priority  [priority:: high]
 - [ ] #task Highest priority  [priority:: highest]
 
+<!-- placeholder to force blank line after included text --> <!-- endInclude -->
+
 ## Dataview Format for Recurrence
 
 - [ ] #task Is a recurring task  [repeat:: every day when done]
diff --git a/resources/sample_vaults/Tasks-Demo/Formats/Tasks Format.md b/resources/sample_vaults/Tasks-Demo/Formats/Tasks Format.md
index 905953ca..9e02ba1a 100644
--- a/resources/sample_vaults/Tasks-Demo/Formats/Tasks Format.md
+++ b/resources/sample_vaults/Tasks-Demo/Formats/Tasks Format.md
@@ -10,6 +10,8 @@
 
 ## Tasks Format for Priorities
 
+<!-- placeholder to force blank line before included text --> <!-- include: DocsSamplesForTaskFormats.test.Serializer_Priorities_tasksPluginEmoji-include.approved.md -->
+
 - [ ] #task Lowest priority ⏬
 - [ ] #task Low priority 🔽
 - [ ] #task Normal priority
@@ -17,6 +19,8 @@
 - [ ] #task High priority ⏫
 - [ ] #task Highest priority 🔺
 
+<!-- placeholder to force blank line after included text --> <!-- endInclude -->
+
 ## Tasks Format for Recurrence
 
 - [ ] #task Is a recurring task 🔁 every day when done
diff --git a/src/Query/Filter/PriorityField.ts b/src/Query/Filter/PriorityField.ts
index 991797c7..6e30cd74 100644
--- a/src/Query/Filter/PriorityField.ts
+++ b/src/Query/Filter/PriorityField.ts
@@ -94,28 +94,47 @@ export class PriorityField extends Field {
 
     public grouper(): GrouperFunction {
         return (task: Task) => {
-            let priorityName = 'ERROR';
-            switch (task.priority) {
-                case Priority.High:
-                    priorityName = 'High';
-                    break;
-                case Priority.Highest:
-                    priorityName = 'Highest';
-                    break;
-                case Priority.Medium:
-                    priorityName = 'Medium';
-                    break;
-                case Priority.None:
-                    priorityName = 'None';
-                    break;
-                case Priority.Low:
-                    priorityName = 'Low';
-                    break;
-                case Priority.Lowest:
-                    priorityName = 'Lowest';
-                    break;
-            }
+            const priorityName = PriorityField.priorityNameUsingNone(task.priority);
             return [`Priority ${task.priority}: ${priorityName}`];
         };
     }
+
+    /**
+     * Get the name of a {@link Priority} value, returning 'None' for {@link Priority.None}
+     * @param priority
+     * @see priorityNameUsingNormal
+     */
+    public static priorityNameUsingNone(priority: Priority) {
+        let priorityName = 'ERROR';
+        switch (priority) {
+            case Priority.High:
+                priorityName = 'High';
+                break;
+            case Priority.Highest:
+                priorityName = 'Highest';
+                break;
+            case Priority.Medium:
+                priorityName = 'Medium';
+                break;
+            case Priority.None:
+                priorityName = 'None';
+                break;
+            case Priority.Low:
+                priorityName = 'Low';
+                break;
+            case Priority.Lowest:
+                priorityName = 'Lowest';
+                break;
+        }
+        return priorityName;
+    }
+
+    /**
+     * Get the name of a {@link Priority} value, returning 'Normal' for {@link Priority.None}
+     * @param priority
+     * @see priorityNameUsingNone
+     */
+    public static priorityNameUsingNormal(priority: Priority) {
+        return PriorityField.priorityNameUsingNone(priority).replace('None', 'Normal');
+    }
 }
diff --git a/src/Task.ts b/src/Task.ts
index c7074303..35760513 100644
--- a/src/Task.ts
+++ b/src/Task.ts
@@ -15,6 +15,10 @@ import { compareByDate } from './lib/DateTools';
  * When sorting, make sure low always comes after none. This way any tasks with low will be below any exiting
  * tasks that have no priority which would be the default.
  *
+ * Values can be converted to strings with:
+ * - {@link priorityNameUsingNone} in {@link PriorityField}
+ * - {@link priorityNameUsingNormal} in {@link PriorityField}
+ *
  * @export
  * @enum {number}
  */


Test patch:
diff --git a/tests/Query/Filter/PriorityField.test.ts b/tests/Query/Filter/PriorityField.test.ts
index 9c865ff9..003203c6 100644
--- a/tests/Query/Filter/PriorityField.test.ts
+++ b/tests/Query/Filter/PriorityField.test.ts
@@ -16,6 +16,19 @@ function testTaskFilterForTaskWithPriority(filter: string, priority: Priority, e
     testFilter(filterOrError, builder.priority(priority), expected);
 }
 
+describe('priority naming', () => {
+    it.each(Object.values(Priority))('should name priority value: "%i"', (priority) => {
+        const name = PriorityField.priorityNameUsingNone(priority);
+        expect(name).not.toEqual('ERROR'); // if this fails, code needs to be updated for a new priority
+    });
+
+    it('should name default priority correctly', () => {
+        const none = Priority.None;
+        expect(PriorityField.priorityNameUsingNone(none)).toEqual('None');
+        expect(PriorityField.priorityNameUsingNormal(none)).toEqual('Normal');
+    });
+});
+
 describe('priority is', () => {
     it('priority is highest', () => {
         const filter = 'priority is highest';
@@ -229,11 +242,16 @@ describe('grouping by priority', () => {
     });
 });
 
-function withAllPriorities(): Task[] {
+export function withAllPriorities(): Task[] {
     const tasks: Task[] = [];
     const allPriorities = Object.values(Priority);
     allPriorities.forEach((priority) => {
-        const task = new TaskBuilder().priority(priority).build();
+        // This description is chosen to be useful for including tasks in user docs, so
+        // changing it will change documentation and sample vault content.
+        const priorityName = PriorityField.priorityNameUsingNormal(priority);
+        const description = `#task ${priorityName} priority`;
+
+        const task = new TaskBuilder().priority(priority).description(description).build();
         tasks.push(task);
     });
     return tasks;
diff --git a/tests/TaskSerializer/DocsSamplesForTaskFormats.test.Serializer_Priorities_dataview-include.approved.md b/tests/TaskSerializer/DocsSamplesForTaskFormats.test.Serializer_Priorities_dataview-include.approved.md
new file mode 100644
index 00000000..98a76929
--- /dev/null
+++ b/tests/TaskSerializer/DocsSamplesForTaskFormats.test.Serializer_Priorities_dataview-include.approved.md
@@ -0,0 +1,10 @@
+<!-- placeholder to force blank line before included text -->
+
+- [ ] #task Lowest priority  [priority:: lowest]
+- [ ] #task Low priority  [priority:: low]
+- [ ] #task Normal priority
+- [ ] #task Medium priority  [priority:: medium]
+- [ ] #task High priority  [priority:: high]
+- [ ] #task Highest priority  [priority:: highest]
+
+<!-- placeholder to force blank line after included text -->
diff --git a/tests/TaskSerializer/DocsSamplesForTaskFormats.test.Serializer_Priorities_dataview-snippet.approved.md b/tests/TaskSerializer/DocsSamplesForTaskFormats.test.Serializer_Priorities_dataview-snippet.approved.md
new file mode 100644
index 00000000..617872c3
--- /dev/null
+++ b/tests/TaskSerializer/DocsSamplesForTaskFormats.test.Serializer_Priorities_dataview-snippet.approved.md
@@ -0,0 +1,6 @@
+- [ ] #task Lowest priority  [priority:: lowest]
+- [ ] #task Low priority  [priority:: low]
+- [ ] #task Normal priority
+- [ ] #task Medium priority  [priority:: medium]
+- [ ] #task High priority  [priority:: high]
+- [ ] #task Highest priority  [priority:: highest]
diff --git a/tests/TaskSerializer/DocsSamplesForTaskFormats.test.Serializer_Priorities_tasksPluginEmoji-include.approved.md b/tests/TaskSerializer/DocsSamplesForTaskFormats.test.Serializer_Priorities_tasksPluginEmoji-include.approved.md
new file mode 100644
index 00000000..d124d82c
--- /dev/null
+++ b/tests/TaskSerializer/DocsSamplesForTaskFormats.test.Serializer_Priorities_tasksPluginEmoji-include.approved.md
@@ -0,0 +1,10 @@
+<!-- placeholder to force blank line before included text -->
+
+- [ ] #task Lowest priority ⏬
+- [ ] #task Low priority 🔽
+- [ ] #task Normal priority
+- [ ] #task Medium priority 🔼
+- [ ] #task High priority ⏫
+- [ ] #task Highest priority 🔺
+
+<!-- placeholder to force blank line after included text -->
diff --git a/tests/TaskSerializer/DocsSamplesForTaskFormats.test.Serializer_Priorities_tasksPluginEmoji-snippet.approved.md b/tests/TaskSerializer/DocsSamplesForTaskFormats.test.Serializer_Priorities_tasksPluginEmoji-snippet.approved.md
new file mode 100644
index 00000000..9eec9559
--- /dev/null
+++ b/tests/TaskSerializer/DocsSamplesForTaskFormats.test.Serializer_Priorities_tasksPluginEmoji-snippet.approved.md
@@ -0,0 +1,6 @@
+- [ ] #task Lowest priority ⏬
+- [ ] #task Low priority 🔽
+- [ ] #task Normal priority
+- [ ] #task Medium priority 🔼
+- [ ] #task High priority ⏫
+- [ ] #task Highest priority 🔺
diff --git a/tests/TaskSerializer/DocsSamplesForTaskFormats.test.ts b/tests/TaskSerializer/DocsSamplesForTaskFormats.test.ts
new file mode 100644
index 00000000..1e8c7888
--- /dev/null
+++ b/tests/TaskSerializer/DocsSamplesForTaskFormats.test.ts
@@ -0,0 +1,22 @@
+import { TASK_FORMATS, updateSettings } from '../../src/Config/Settings';
+import { verifyMarkdown, verifyMarkdownForDocs } from '../TestingTools/VerifyMarkdownTable';
+import { withAllPriorities } from '../Query/Filter/PriorityField.test';
+
+describe('Serializer', () => {
+    describe('Priorities', () => {
+        function allPriorityLines() {
+            const tasks = withAllPriorities().reverse();
+            return tasks.map((t) => t.toFileLineString()).join('\n');
+        }
+
+        it.each(Object.keys(TASK_FORMATS))('%s-snippet', (key: string) => {
+            updateSettings({ taskFormat: key as keyof TASK_FORMATS });
+            verifyMarkdown(allPriorityLines());
+        });
+
+        it.each(Object.keys(TASK_FORMATS))('%s-include', (key: string) => {
+            updateSettings({ taskFormat: key as keyof TASK_FORMATS });
+            verifyMarkdownForDocs(allPriorityLines());
+        });
+    });
+});
diff --git a/tests/TestingTools/VerifyMarkdownTable.ts b/tests/TestingTools/VerifyMarkdownTable.ts
index f95e2e81..9a9856d8 100644
--- a/tests/TestingTools/VerifyMarkdownTable.ts
+++ b/tests/TestingTools/VerifyMarkdownTable.ts
@@ -1,12 +1,36 @@
-import { Options } from 'approvals/lib/Core/Options';
+import { type ConfigModifier, Options } from 'approvals/lib/Core/Options';
 import { verify } from 'approvals/lib/Providers/Jest/JestApprovals';
 
-function verifyMarkdown(output: string) {
+// This import fails with Approvals.NodeJS 6.2.1:
+// error TS1371: This import is never used as a value and must use 'import type' because 'importsNotUsedAsValues' is set to 'error'.
+// import { JestReporter } from 'approvals/lib/Providers/Jest/JestReporter';
+
+export function verifyMarkdown(output: string) {
     let options = new Options();
     options = options.forFile().withFileExtention('md');
-    verify(output, options);
+
+    const configModifier: ConfigModifier = (c) => {
+        c.reporters = [
+            // Built-in reporters listed at:
+            // https://github.com/approvals/Approvals.NodeJS#built-in-reporters
+            'vscode', // VS Code diff works well with files containing emojis
+            //-----------------
+            // Last one is jest reporter, that should write diffs to console in
+            // Continuous Integration builds, such as GitHub Actions
+            // new JestReporter(), // disabled due to import error - see above
+        ];
+        return c;
+    };
+
+    verify(output, options.withConfig(configModifier));
 }
 
+/**
+ * Write out markdown block for including directly in documentation.
+ * To embed the approved file directly in user docs, write line like this, and then run mdsnippets:
+ *      include: output-file-name.approved.md
+ * @param markdown
+ */
 export function verifyMarkdownForDocs(markdown: string) {
     let output = '<!-- placeholder to force blank line before included text -->\n\n';
     output += markdown;