Repository: obsidian-tasks-group/obsidian-tasks
Pull request #3164 (2024-11-01 17:40:47)

refactor: test `DateEditor` svelte component
# Types of changes

Done by pairing with @claremacrae 

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)

## Description

- Make `DateEditor.parsedDate` available for testing
- Create `DateEditorWrapper` component to test the `DateEditor`
- Add tests showing current behaviour

## Motivation and Context

- Prepare to TDD date picker's behaviour

## How has this been tested?

- Unit tests

## Checklist

- [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

- [x] My contribution follows 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/ui/DateEditor.svelte b/src/ui/DateEditor.svelte
index 048f0ea2..a3c92e86 100644
--- a/src/ui/DateEditor.svelte
+++ b/src/ui/DateEditor.svelte
@@ -10,7 +10,8 @@
     export let forwardOnly: boolean;
     export let accesskey: string | null;
 
-    let parsedDate: string;
+    // Use this for testing purposes only
+    export let parsedDate: string = '';
     // let inputElement: HTMLInputElement;
     // let flatpickrInstance: any;
     //


Test patch:
diff --git a/tests/ui/DateEditor.test.ts b/tests/ui/DateEditor.test.ts
new file mode 100644
index 00000000..f6867911
--- /dev/null
+++ b/tests/ui/DateEditor.test.ts
@@ -0,0 +1,117 @@
+/**
+ * @jest-environment jsdom
+ */
+import { fireEvent, render } from '@testing-library/svelte';
+import moment from 'moment/moment';
+import DateEditorWrapper from './DateEditorWrapper.svelte';
+
+import { getAndCheckRenderedElement } from './RenderingTestHelpers';
+
+window.moment = moment;
+
+function renderDateEditorWrapper(componentOptions: { forwardOnly: boolean }) {
+    const { container } = render(DateEditorWrapper, componentOptions);
+
+    expect(() => container).toBeTruthy();
+
+    return container;
+}
+
+function testInputValue(container: HTMLElement, inputId: string, expectedText: string) {
+    const input = getAndCheckRenderedElement<HTMLInputElement>(container, inputId);
+    expect(input.value).toEqual(expectedText);
+}
+
+async function testTypingInput(
+    {
+        userTyped,
+        expectedLeftText,
+        expectedRightText,
+        expectedReturnedDate,
+    }: {
+        userTyped: string;
+        expectedLeftText: string;
+        expectedRightText: string;
+        expectedReturnedDate: string;
+    },
+    { forwardOnly }: { forwardOnly: boolean } = { forwardOnly: true },
+) {
+    const container = renderDateEditorWrapper({ forwardOnly });
+
+    const dueDateInput = getAndCheckRenderedElement<HTMLInputElement>(container, 'due');
+    await fireEvent.input(dueDateInput, { target: { value: userTyped } });
+
+    testInputValue(container, 'due', expectedLeftText);
+    testInputValue(container, 'parsedDateFromDateEditor', expectedRightText);
+    testInputValue(container, 'dueDateFromDateEditor', expectedReturnedDate);
+}
+
+beforeEach(() => {
+    jest.useFakeTimers();
+    jest.setSystemTime(new Date('2024-04-20'));
+});
+
+afterEach(() => {
+    jest.useRealTimers();
+});
+
+describe('date editor wrapper tests', () => {
+    it('should initialise fields correctly', () => {
+        const container = renderDateEditorWrapper({ forwardOnly: true });
+
+        testInputValue(container, 'due', '');
+        testInputValue(container, 'parsedDateFromDateEditor', '<i>no due date</i>');
+        testInputValue(container, 'dueDateFromDateEditor', '');
+    });
+
+    it('should replace an empty date field with typed date value', async () => {
+        await testTypingInput({
+            userTyped: '2024-10-01',
+            expectedLeftText: '2024-10-01',
+            expectedRightText: '2024-10-01',
+            expectedReturnedDate: '2024-10-01',
+        });
+    });
+
+    it('should replace an empty date field with typed abbreviation', async () => {
+        await testTypingInput({
+            userTyped: 'tm ',
+            expectedLeftText: 'tomorrow',
+            expectedRightText: '2024-04-21',
+            expectedReturnedDate: 'tomorrow',
+        });
+    });
+
+    it('should show an error message for invalid date', async () => {
+        await testTypingInput({
+            userTyped: 'blah',
+            expectedLeftText: 'blah',
+            expectedRightText: '<i>invalid due date</i>',
+            expectedReturnedDate: 'blah',
+        });
+    });
+
+    it('should select a forward date', async () => {
+        await testTypingInput(
+            {
+                userTyped: 'friday',
+                expectedLeftText: 'friday',
+                expectedRightText: '2024-04-26',
+                expectedReturnedDate: 'friday',
+            },
+            { forwardOnly: true },
+        );
+    });
+
+    it('should select a backward/earlier date', async () => {
+        await testTypingInput(
+            {
+                userTyped: 'friday',
+                expectedLeftText: 'friday',
+                expectedRightText: '2024-04-19',
+                expectedReturnedDate: 'friday',
+            },
+            { forwardOnly: false },
+        );
+    });
+});
diff --git a/tests/ui/DateEditorWrapper.svelte b/tests/ui/DateEditorWrapper.svelte
new file mode 100644
index 00000000..026caa07
--- /dev/null
+++ b/tests/ui/DateEditorWrapper.svelte
@@ -0,0 +1,27 @@
+<script lang="ts">
+    import { TASK_FORMATS } from '../../src/Config/Settings';
+    import DateEditor from '../../src/ui/DateEditor.svelte';
+
+    export let forwardOnly: boolean;
+
+    let dateFromDateEditor: string = '';
+    let parsedDateFromDateEditor: string = '';
+    let isDueDateValid = true;
+</script>
+
+<DateEditor
+    id="due"
+    dateSymbol={TASK_FORMATS.tasksPluginEmoji.taskSerializer.symbols.dueDateSymbol}
+    bind:date={dateFromDateEditor}
+    bind:isDateValid={isDueDateValid}
+    {forwardOnly}
+    accesskey={null}
+    on:open={() => {}}
+    on:close={() => {}}
+    bind:parsedDate={parsedDateFromDateEditor}
+/>
+<input bind:value={dateFromDateEditor} id="dueDateFromDateEditor" />
+<input bind:value={parsedDateFromDateEditor} id="parsedDateFromDateEditor" />
+
+<style>
+</style>
diff --git a/tests/ui/EditTask.test.ts b/tests/ui/EditTask.test.ts
index bf9dca77..47a83898 100644
--- a/tests/ui/EditTask.test.ts
+++ b/tests/ui/EditTask.test.ts
@@ -14,6 +14,11 @@ import { verifyWithFileExtension } from '../TestingTools/ApprovalTestHelpers';
 import { verifyAllCombinations3Async } from '../TestingTools/CombinationApprovalsAsync';
 import { prettifyHTML } from '../TestingTools/HTMLHelpers';
 import { TaskBuilder } from '../TestingTools/TaskBuilder';
+import {
+    getAndCheckApplyButton,
+    getAndCheckRenderedDescriptionElement,
+    getAndCheckRenderedElement,
+} from './RenderingTestHelpers';
 
 window.moment = moment;
 /**
@@ -50,28 +55,6 @@ function renderAndCheckModal(task: Task, onSubmit: (updatedTasks: Task[]) => voi
     return { result, container };
 }
 
-/**
- * Find the element with the given id.
- * Template type T might be, for example, HTMLInputElement or HTMLSelectElement
- * @param container
- * @param elementId
- */
-function getAndCheckRenderedElement<T>(container: HTMLElement, elementId: string) {
-    const element = container.ownerDocument.getElementById(elementId) as T;
-    expect(() => element).toBeTruthy();
-    return element;
-}
-
-function getAndCheckRenderedDescriptionElement(container: HTMLElement): HTMLInputElement {
-    return getAndCheckRenderedElement<HTMLInputElement>(container, 'description');
-}
-
-function getAndCheckApplyButton(result: RenderResult<EditTask>): HTMLButtonElement {
-    const submit = result.getByText('Apply') as HTMLButtonElement;
-    expect(submit).toBeTruthy();
-    return submit;
-}
-
 async function editInputElement(inputElement: HTMLInputElement, newValue: string) {
     await fireEvent.input(inputElement, { target: { value: newValue } });
 }
diff --git a/tests/ui/RenderingTestHelpers.ts b/tests/ui/RenderingTestHelpers.ts
new file mode 100644
index 00000000..1da76842
--- /dev/null
+++ b/tests/ui/RenderingTestHelpers.ts
@@ -0,0 +1,24 @@
+import type { RenderResult } from '@testing-library/svelte';
+import type EditTask from '../../src/ui/EditTask.svelte';
+
+/**
+ * Find the element with the given id.
+ * Template type T might be, for example, HTMLInputElement or HTMLSelectElement
+ * @param container
+ * @param elementId
+ */
+export function getAndCheckRenderedElement<T>(container: HTMLElement, elementId: string) {
+    const element = container.ownerDocument.getElementById(elementId) as T;
+    expect(() => element).toBeTruthy();
+    return element;
+}
+
+export function getAndCheckRenderedDescriptionElement(container: HTMLElement): HTMLInputElement {
+    return getAndCheckRenderedElement<HTMLInputElement>(container, 'description');
+}
+
+export function getAndCheckApplyButton(result: RenderResult<EditTask>): HTMLButtonElement {
+    const submit = result.getByText('Apply') as HTMLButtonElement;
+    expect(submit).toBeTruthy();
+    return submit;
+}