Repository: obsidian-tasks-group/obsidian-tasks
Pull request #1928 (2023-05-01 16:23:22)

refactor, test: Re-enable 'show all suggested text' test

<!--- Provide a general summary of your changes in the Title above -->

# Description

The goal of this is to make visible future changes in behaviour of the auto-suggest code, before spending time manual testing.

The key refactoring to enable this was:

- replacing a hard-coded `maxGenericSuggestions` value inside `addDatesSuggestions()`...
- ... with a parameter of the same name that is passed in from the caller

The key testing change was:

- replacing the out-of-date expected text from `expect(...).toMatchInlineSnapshot()`
    - which was never going to pass once this test was parameterised on file format
- ... with an approval test
    - which produces one output file per file format.

For each format tested in `tests/Suggestor/Suggestor.test.ts`:

- Construct a list of all possible suggestions and the text they expand to.
- Generate a Markdown table for visualising all the suggestions.

TODO:

- [ ] Update the Approval Tests docs, as there's a lot more to say now: 
    https://publish.obsidian.md/tasks-contributing/Testing/Approval+Tests

## 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. -->

- To save some time when testing changes to the auto-suggest feature and additions to supported date types
- To make changes in behaviour visible
- Potentially to add to the user documentation in future

## How has this been tested?

- By running all automatic tests

## Screenshots (if appropriate)


<img width="471" alt="image" src="https://user-images.githubusercontent.com/4840096/235486306-ebc6903c-5175-4ca7-9d17-ea0f1e207db0.png">

<img width="560" alt="image" src="https://user-images.githubusercontent.com/4840096/235486354-095d988c-deb9-42b5-9753-96940c8a9d11.png">


## 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/Config/Settings.ts b/src/Config/Settings.ts
index 72f341e3..e0deb674 100644
--- a/src/Config/Settings.ts
+++ b/src/Config/Settings.ts
@@ -1,4 +1,4 @@
-import { makeDefaultSuggestionBuilder } from '../Suggestor/Suggestor';
+import { DEFAULT_MAX_GENERIC_SUGGESTIONS, makeDefaultSuggestionBuilder } from '../Suggestor/Suggestor';
 import { DEFAULT_SYMBOLS } from '../TaskSerializer/DefaultTaskSerializer';
 import { DATAVIEW_SYMBOLS } from '../TaskSerializer/DataviewTaskSerializer';
 import { StatusConfiguration } from '../StatusConfiguration';
@@ -38,12 +38,12 @@ export const TASK_FORMATS = {
     tasksPluginEmoji: {
         displayName: 'Tasks Emoji Format',
         taskSerializer: new DefaultTaskSerializer(DEFAULT_SYMBOLS),
-        buildSuggestions: makeDefaultSuggestionBuilder(DEFAULT_SYMBOLS),
+        buildSuggestions: makeDefaultSuggestionBuilder(DEFAULT_SYMBOLS, DEFAULT_MAX_GENERIC_SUGGESTIONS),
     },
     dataview: {
         displayName: 'Dataview',
         taskSerializer: new DataviewTaskSerializer(),
-        buildSuggestions: makeDefaultSuggestionBuilder(DATAVIEW_SYMBOLS),
+        buildSuggestions: makeDefaultSuggestionBuilder(DATAVIEW_SYMBOLS, DEFAULT_MAX_GENERIC_SUGGESTIONS),
     },
 } as const;
 
diff --git a/src/Suggestor/Suggestor.ts b/src/Suggestor/Suggestor.ts
index 0d1139d4..b30d2e1d 100644
--- a/src/Suggestor/Suggestor.ts
+++ b/src/Suggestor/Suggestor.ts
@@ -6,7 +6,16 @@ import type { DefaultTaskSerializerSymbols } from '../TaskSerializer/DefaultTask
 import { TaskRegularExpressions } from '../Task';
 import type { SuggestInfo, SuggestionBuilder } from '.';
 
-export function makeDefaultSuggestionBuilder(symbols: DefaultTaskSerializerSymbols): SuggestionBuilder {
+/**
+ * Recommended default value to pass in to {@link makeDefaultSuggestionBuilder} maxGenericSuggestions parameter
+ * for production code.
+ */
+export const DEFAULT_MAX_GENERIC_SUGGESTIONS = 5;
+
+export function makeDefaultSuggestionBuilder(
+    symbols: DefaultTaskSerializerSymbols,
+    maxGenericSuggestions: number /** See {@link DEFAULT_MAX_GENERIC_SUGGESTIONS} */,
+): SuggestionBuilder {
     const datePrefixRegex = [symbols.startDateSymbol, symbols.scheduledDateSymbol, symbols.dueDateSymbol].join('|');
     /*
      * Return a list of suggestions, either generic or more fine-grained to the words at the cursor.
@@ -15,7 +24,9 @@ export function makeDefaultSuggestionBuilder(symbols: DefaultTaskSerializerSymbo
         let suggestions: SuggestInfo[] = [];
 
         // Step 1: add date suggestions if relevant
-        suggestions = suggestions.concat(addDatesSuggestions(line, cursorPos, settings, datePrefixRegex));
+        suggestions = suggestions.concat(
+            addDatesSuggestions(line, cursorPos, settings, datePrefixRegex, maxGenericSuggestions),
+        );
 
         // Step 2: add recurrence suggestions if relevant
         suggestions = suggestions.concat(addRecurrenceSuggestions(line, cursorPos, settings, symbols.recurrenceSymbol));
@@ -145,6 +156,7 @@ function addDatesSuggestions(
     cursorPos: number,
     settings: Settings,
     datePrefixRegex: string,
+    maxGenericSuggestions: number,
 ): SuggestInfo[] {
     const genericSuggestions = [
         'today',
@@ -194,7 +206,6 @@ function addDatesSuggestions(
         // a max number. We want the max number to be around half the total allowed matches, to also allow
         // some global generic matches (e.g. task components) to find their way to the menu
         const minMatch = 1;
-        const maxGenericSuggestions = 5;
         let genericMatches = genericSuggestions
             .filter(
                 (value) =>


Test patch:
diff --git a/tests/Suggestor/Suggestor.test.auto-complete_with__dataview__symbols_show_all_suggested_text.approved.md b/tests/Suggestor/Suggestor.test.auto-complete_with__dataview__symbols_show_all_suggested_text.approved.md
new file mode 100644
index 00000000..0eb6427e
--- /dev/null
+++ b/tests/Suggestor/Suggestor.test.auto-complete_with__dataview__symbols_show_all_suggested_text.approved.md
@@ -0,0 +1,60 @@
+| suggestion | expansion |
+| ----- | ----- |
+| ⏎ | &lt;new line> |
+| due:: due date | due::  |
+| start:: start date | start::  |
+| scheduled:: scheduled date | scheduled::  |
+| priority:: high high priority | priority:: high  |
+| priority:: medium medium priority | priority:: medium  |
+| priority:: low low priority | priority:: low  |
+| repeat:: recurring (repeat) | repeat::  |
+| created:: created today (2022-07-11) | created:: 2022-07-11  |
+| every | repeat:: every  |
+| every day | repeat:: every day  |
+| every week | repeat:: every week  |
+| every month | repeat:: every month  |
+| every month on the | repeat:: every month on the  |
+| every year | repeat:: every year  |
+| every week on Sunday | repeat:: every week on Sunday  |
+| every week on Monday | repeat:: every week on Monday  |
+| every week on Tuesday | repeat:: every week on Tuesday  |
+| every week on Wednesday | repeat:: every week on Wednesday  |
+| every week on Thursday | repeat:: every week on Thursday  |
+| every week on Friday | repeat:: every week on Friday  |
+| every week on Saturday | repeat:: every week on Saturday  |
+| today (2022-07-11) | due:: 2022-07-11  |
+| tomorrow (2022-07-12) | due:: 2022-07-12  |
+| Sunday (2022-07-17) | due:: 2022-07-17  |
+| Monday (2022-07-18) | due:: 2022-07-18  |
+| Tuesday (2022-07-12) | due:: 2022-07-12  |
+| Wednesday (2022-07-13) | due:: 2022-07-13  |
+| Thursday (2022-07-14) | due:: 2022-07-14  |
+| Friday (2022-07-15) | due:: 2022-07-15  |
+| Saturday (2022-07-16) | due:: 2022-07-16  |
+| next week (2022-07-18) | due:: 2022-07-18  |
+| next month (2022-08-11) | due:: 2022-08-11  |
+| next year (2023-07-11) | due:: 2023-07-11  |
+| today (2022-07-11) | scheduled:: 2022-07-11  |
+| tomorrow (2022-07-12) | scheduled:: 2022-07-12  |
+| Sunday (2022-07-17) | scheduled:: 2022-07-17  |
+| Monday (2022-07-18) | scheduled:: 2022-07-18  |
+| Tuesday (2022-07-12) | scheduled:: 2022-07-12  |
+| Wednesday (2022-07-13) | scheduled:: 2022-07-13  |
+| Thursday (2022-07-14) | scheduled:: 2022-07-14  |
+| Friday (2022-07-15) | scheduled:: 2022-07-15  |
+| Saturday (2022-07-16) | scheduled:: 2022-07-16  |
+| next week (2022-07-18) | scheduled:: 2022-07-18  |
+| next month (2022-08-11) | scheduled:: 2022-08-11  |
+| next year (2023-07-11) | scheduled:: 2023-07-11  |
+| today (2022-07-11) | start:: 2022-07-11  |
+| tomorrow (2022-07-12) | start:: 2022-07-12  |
+| Sunday (2022-07-17) | start:: 2022-07-17  |
+| Monday (2022-07-18) | start:: 2022-07-18  |
+| Tuesday (2022-07-12) | start:: 2022-07-12  |
+| Wednesday (2022-07-13) | start:: 2022-07-13  |
+| Thursday (2022-07-14) | start:: 2022-07-14  |
+| Friday (2022-07-15) | start:: 2022-07-15  |
+| Saturday (2022-07-16) | start:: 2022-07-16  |
+| next week (2022-07-18) | start:: 2022-07-18  |
+| next month (2022-08-11) | start:: 2022-08-11  |
+| next year (2023-07-11) | start:: 2023-07-11  |
diff --git a/tests/Suggestor/Suggestor.test.auto-complete_with__emoji__symbols_show_all_suggested_text.approved.md b/tests/Suggestor/Suggestor.test.auto-complete_with__emoji__symbols_show_all_suggested_text.approved.md
new file mode 100644
index 00000000..aab2245f
--- /dev/null
+++ b/tests/Suggestor/Suggestor.test.auto-complete_with__emoji__symbols_show_all_suggested_text.approved.md
@@ -0,0 +1,60 @@
+| suggestion | expansion |
+| ----- | ----- |
+| ⏎ | &lt;new line> |
+| 📅 due date | 📅  |
+| 🛫 start date | 🛫  |
+| ⏳ scheduled date | ⏳  |
+| ⏫ high priority | ⏫  |
+| 🔼 medium priority | 🔼  |
+| 🔽 low priority | 🔽  |
+| 🔁 recurring (repeat) | 🔁  |
+| ➕ created today (2022-07-11) | ➕ 2022-07-11  |
+| every | 🔁 every  |
+| every day | 🔁 every day  |
+| every week | 🔁 every week  |
+| every month | 🔁 every month  |
+| every month on the | 🔁 every month on the  |
+| every year | 🔁 every year  |
+| every week on Sunday | 🔁 every week on Sunday  |
+| every week on Monday | 🔁 every week on Monday  |
+| every week on Tuesday | 🔁 every week on Tuesday  |
+| every week on Wednesday | 🔁 every week on Wednesday  |
+| every week on Thursday | 🔁 every week on Thursday  |
+| every week on Friday | 🔁 every week on Friday  |
+| every week on Saturday | 🔁 every week on Saturday  |
+| today (2022-07-11) | 📅 2022-07-11  |
+| tomorrow (2022-07-12) | 📅 2022-07-12  |
+| Sunday (2022-07-17) | 📅 2022-07-17  |
+| Monday (2022-07-18) | 📅 2022-07-18  |
+| Tuesday (2022-07-12) | 📅 2022-07-12  |
+| Wednesday (2022-07-13) | 📅 2022-07-13  |
+| Thursday (2022-07-14) | 📅 2022-07-14  |
+| Friday (2022-07-15) | 📅 2022-07-15  |
+| Saturday (2022-07-16) | 📅 2022-07-16  |
+| next week (2022-07-18) | 📅 2022-07-18  |
+| next month (2022-08-11) | 📅 2022-08-11  |
+| next year (2023-07-11) | 📅 2023-07-11  |
+| today (2022-07-11) | ⏳ 2022-07-11  |
+| tomorrow (2022-07-12) | ⏳ 2022-07-12  |
+| Sunday (2022-07-17) | ⏳ 2022-07-17  |
+| Monday (2022-07-18) | ⏳ 2022-07-18  |
+| Tuesday (2022-07-12) | ⏳ 2022-07-12  |
+| Wednesday (2022-07-13) | ⏳ 2022-07-13  |
+| Thursday (2022-07-14) | ⏳ 2022-07-14  |
+| Friday (2022-07-15) | ⏳ 2022-07-15  |
+| Saturday (2022-07-16) | ⏳ 2022-07-16  |
+| next week (2022-07-18) | ⏳ 2022-07-18  |
+| next month (2022-08-11) | ⏳ 2022-08-11  |
+| next year (2023-07-11) | ⏳ 2023-07-11  |
+| today (2022-07-11) | 🛫 2022-07-11  |
+| tomorrow (2022-07-12) | 🛫 2022-07-12  |
+| Sunday (2022-07-17) | 🛫 2022-07-17  |
+| Monday (2022-07-18) | 🛫 2022-07-18  |
+| Tuesday (2022-07-12) | 🛫 2022-07-12  |
+| Wednesday (2022-07-13) | 🛫 2022-07-13  |
+| Thursday (2022-07-14) | 🛫 2022-07-14  |
+| Friday (2022-07-15) | 🛫 2022-07-15  |
+| Saturday (2022-07-16) | 🛫 2022-07-16  |
+| next week (2022-07-18) | 🛫 2022-07-18  |
+| next month (2022-08-11) | 🛫 2022-08-11  |
+| next year (2023-07-11) | 🛫 2023-07-11  |
diff --git a/tests/Suggestor/Suggestor.test.ts b/tests/Suggestor/Suggestor.test.ts
index da21dd3b..a23dd714 100644
--- a/tests/Suggestor/Suggestor.test.ts
+++ b/tests/Suggestor/Suggestor.test.ts
@@ -8,6 +8,7 @@ import type { SuggestInfo } from '../../src/Suggestor';
 import { makeDefaultSuggestionBuilder } from '../../src/Suggestor/Suggestor';
 import { DEFAULT_SYMBOLS } from '../../src/TaskSerializer/DefaultTaskSerializer';
 import { DATAVIEW_SYMBOLS } from '../../src/TaskSerializer/DataviewTaskSerializer';
+import { MarkdownTable } from '../TestingTools/VerifyMarkdownTable';
 
 window.moment = moment;
 
@@ -22,11 +23,13 @@ afterAll(() => {
     chronoSpy.mockRestore();
 });
 
+const MAX_GENERIC_SUGGESTIONS_FOR_TESTS = 50;
+
 describe.each([
     { name: 'emoji', symbols: DEFAULT_SYMBOLS },
     { name: 'dataview', symbols: DATAVIEW_SYMBOLS },
 ])("auto-complete with '$name' symbols", ({ symbols }) => {
-    const buildSuggestions = makeDefaultSuggestionBuilder(symbols);
+    const buildSuggestions = makeDefaultSuggestionBuilder(symbols, MAX_GENERIC_SUGGESTIONS_FOR_TESTS);
     const {
         dueDateSymbol,
         scheduledDateSymbol,
@@ -115,10 +118,7 @@ describe.each([
         expect(suggestions[0].displayText).not.toContain('created today');
     });
 
-    // Until the maximum date suggestion number is no longer hardcoded,
-    // it's not possible to output all dates. Skipping for now.
-    it.skip('show all suggested text', () => {
-        // Arrange
+    it('show all suggested text', () => {
         const originalSettings = getSettings();
         originalSettings.autoSuggestMaxItems = 200;
 
@@ -129,92 +129,24 @@ describe.each([
             `- [ ] some task ${scheduledDateSymbol} `,
             `- [ ] some task ${startDateSymbol} `,
         ];
-        const allSuggestions: string[] = [];
+        const markdownTable = new MarkdownTable(['suggestion', 'expansion']);
         for (const line of lines) {
-            // allSuggestions.push(`| ${line} | |`);
             const suggestions: SuggestInfo[] = buildSuggestions(line, line.length - 1, originalSettings);
             for (const suggestion of suggestions) {
                 // The 'new line' replacement adds a trailing space at the end of a line,
                 // which causes auto-formatting to then make the test fail.
-                // So we replace the 'new line' character with some fixed text.
+                // So we replace the 'new line' character with some fixed text,
+                // that displays nicely when the output Markdown table is viewed.
                 let replacementText = `${suggestion.appendText}`;
                 if (replacementText === '\n') {
-                    replacementText = '<new line>';
-                }
-                // Format for pasting in to a Markdown table:
-                const suggestionAsText = `| ${suggestion.displayText} | ${replacementText} |`;
-                if (!allSuggestions.includes(suggestionAsText)) {
-                    allSuggestions.push(suggestionAsText);
+                    replacementText = '&lt;new line>';
                 }
+                markdownTable.addRowIfNew([suggestion.displayText, replacementText]);
             }
         }
 
-        // CAUTION: In obsidian, when using this feature on a 'Monday' expands to the execution date.
-        // In these tests, it expands to the Next Monday.
-
-        expect('\n' + allSuggestions.join('\n') + '\n').toMatchInlineSnapshot(`
-            "
-            | ⏎ | <new line> |
-            | ${dueDateSymbol} due date | ${dueDateSymbol}  |
-            | ${startDateSymbol} start date | ${startDateSymbol}  |
-            | ${scheduledDateSymbol} scheduled date | ${scheduledDateSymbol}  |
-            | ${prioritySymbols.High} high priority | ${prioritySymbols.High}  |
-            | ${prioritySymbols.Medium} medium priority | ${prioritySymbols.Medium}  |
-            | ${prioritySymbols.Low} low priority | ${prioritySymbols.Low}  |
-            | ${recurrenceSymbol} recurring (repeat) | ${recurrenceSymbol}  |
-            | ${createdDateSymbol} created today (2022-07-11) | ${createdDateSymbol} 2022-07-11  |
-            | every | ${recurrenceSymbol} every  |
-            | every day | ${recurrenceSymbol} every day  |
-            | every week | ${recurrenceSymbol} every week  |
-            | every month | ${recurrenceSymbol} every month  |
-            | every month on the | ${recurrenceSymbol} every month on the  |
-            | every year | ${recurrenceSymbol} every year  |
-            | every week on Sunday | ${recurrenceSymbol} every week on Sunday  |
-            | every week on Monday | ${recurrenceSymbol} every week on Monday  |
-            | every week on Tuesday | ${recurrenceSymbol} every week on Tuesday  |
-            | every week on Wednesday | ${recurrenceSymbol} every week on Wednesday  |
-            | every week on Thursday | ${recurrenceSymbol} every week on Thursday  |
-            | every week on Friday | ${recurrenceSymbol} every week on Friday  |
-            | every week on Saturday | ${recurrenceSymbol} every week on Saturday  |
-            | today (2022-07-11) | ${dueDateSymbol} 2022-07-11  |
-            | tomorrow (2022-07-12) | ${dueDateSymbol} 2022-07-12  |
-            | Sunday (2022-07-17) | ${dueDateSymbol} 2022-07-17  |
-            | Monday (2022-07-18) | ${dueDateSymbol} 2022-07-18  |
-            | Tuesday (2022-07-12) | ${dueDateSymbol} 2022-07-12  |
-            | Wednesday (2022-07-13) | ${dueDateSymbol} 2022-07-13  |
-            | Thursday (2022-07-14) | ${dueDateSymbol} 2022-07-14  |
-            | Friday (2022-07-15) | ${dueDateSymbol} 2022-07-15  |
-            | Saturday (2022-07-16) | ${dueDateSymbol} 2022-07-16  |
-            | next week (2022-07-18) | ${dueDateSymbol} 2022-07-18  |
-            | next month (2022-08-11) | ${dueDateSymbol} 2022-08-11  |
-            | next year (2023-07-11) | ${dueDateSymbol} 2023-07-11  |
-            | today (2022-07-11) | ${scheduledDateSymbol} 2022-07-11  |
-            | tomorrow (2022-07-12) | ${scheduledDateSymbol} 2022-07-12  |
-            | Sunday (2022-07-17) | ${scheduledDateSymbol} 2022-07-17  |
-            | Monday (2022-07-18) | ${scheduledDateSymbol} 2022-07-18  |
-            | Tuesday (2022-07-12) | ${scheduledDateSymbol} 2022-07-12  |
-            | Wednesday (2022-07-13) | ${scheduledDateSymbol} 2022-07-13  |
-            | Thursday (2022-07-14) | ${scheduledDateSymbol} 2022-07-14  |
-            | Friday (2022-07-15) | ${scheduledDateSymbol} 2022-07-15  |
-            | Saturday (2022-07-16) | ${scheduledDateSymbol} 2022-07-16  |
-            | next week (2022-07-18) | ${scheduledDateSymbol} 2022-07-18  |
-            | next month (2022-08-11) | ${scheduledDateSymbol} 2022-08-11  |
-            | next year (2023-07-11) | ${scheduledDateSymbol} 2023-07-11  |
-            | today (2022-07-11) | ${startDateSymbol} 2022-07-11  |
-            | tomorrow (2022-07-12) | ${startDateSymbol} 2022-07-12  |
-            | Sunday (2022-07-17) | ${startDateSymbol} 2022-07-17  |
-            | Monday (2022-07-18) | ${startDateSymbol} 2022-07-18  |
-            | Tuesday (2022-07-12) | ${startDateSymbol} 2022-07-12  |
-            | Wednesday (2022-07-13) | ${startDateSymbol} 2022-07-13  |
-            | Thursday (2022-07-14) | ${startDateSymbol} 2022-07-14  |
-            | Friday (2022-07-15) | ${startDateSymbol} 2022-07-15  |
-            | Saturday (2022-07-16) | ${startDateSymbol} 2022-07-16  |
-            | next week (2022-07-18) | ${startDateSymbol} 2022-07-18  |
-            | next month (2022-08-11) | ${startDateSymbol} 2022-08-11  |
-            | next year (2023-07-11) | ${startDateSymbol} 2023-07-11  |
-            "
-        `);
-
-        // todaySpy.mockClear();
+        // For help if this test fails and you are new to Approval Tests, see:
+        //    https://publish.obsidian.md/tasks-contributing/Testing/Approval+Tests
+        markdownTable.verify();
     });
 });
diff --git a/tests/TestingTools/VerifyMarkdownTable.ts b/tests/TestingTools/VerifyMarkdownTable.ts
index 61ddeb8a..f95e2e81 100644
--- a/tests/TestingTools/VerifyMarkdownTable.ts
+++ b/tests/TestingTools/VerifyMarkdownTable.ts
@@ -41,11 +41,23 @@ export class MarkdownTable {
     }
 
     public addRow(cells: string[]) {
+        const row = this.makeRowText(cells);
+        this._markdown += `${row}\n`;
+    }
+
+    public addRowIfNew(cells: string[]) {
+        const row = this.makeRowText(cells);
+        if (!this._markdown.includes(row)) {
+            this._markdown += `${row}\n`;
+        }
+    }
+
+    private makeRowText(cells: string[]) {
         let row = '|';
         cells.forEach((s) => {
             row += ` ${s} |`;
         });
-        this._markdown += `${row}\n`;
+        return row;
     }
 
     public verifyForDocs() {