Skip to content

Commit

Permalink
refactor: OnCompletion (preparing to later add Delete Action) (obsidi…
Browse files Browse the repository at this point in the history
…an-tasks-group#2666)

* Add experimental test

* Non-recurring tasks, two tests and code

* Recurring tasks, two tests

* Handle invalid (unknown) 'On Completion' actions

* Show 'Invalid Action' Notification until user clicks to clear

* extract function applyStatusAndActOnCompletion()

* Transfer my code from Task.ts to OnCompletion.test.ts; refactor code and tests

* Add test, then passing code -- Action not executed when making task 'Done' leaves it's Status unchanged

* Rename applyStatusAndActOnCompletion function to applyStatusAndOnCompletionAction

* Added three new tests suggested by Clare and code to make them pass for right reason; removed TODOs

* Moved OnCompletion feature code out of OnCompletion.test.ts to src/task/OnCompletion.ts

* Add test for empty string Action following On Completion flag emoji

* Correct test for empty string Action following On Completion flag emoji

* Use tasks.filter to return tasks other than completedTask

* Simplify logic for early return; use more precise equality checks throughout

* Simplify logic for early return; use more precise equality checks throughout

* Remove 'if' statement that was always 'true'; comment out 'never reachable code'

* replace substring composition of ocAction with test for literal string

* Convert switch statement to if test

* Replace .tostring method with .description field

* Remove endStatus parameter from handleOnCompletion

* Remove startStatus parameter from handleOnCompletion

* Move function applyStatusAndOnCompletionAction to OnCompletion.test.ts

* Remove unnecessary Console import and console constant declaration

* Rename `completedTask` to `changedStatusTask`

---------

Co-authored-by: Thomas Herden <[email protected]>
  • Loading branch information
therden and Thomas Herden authored Feb 29, 2024
1 parent 7d7c5b7 commit 91cdc3e
Show file tree
Hide file tree
Showing 2 changed files with 230 additions and 0 deletions.
33 changes: 33 additions & 0 deletions src/Task/OnCompletion.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { StatusType } from '../Statuses/StatusConfiguration';
import type { Task } from './Task';

export function handleOnCompletion(task: Task, tasks: Task[]): Task[] {
const tasksArrayLength = tasks.length;
if (tasksArrayLength === 0) {
return tasks;
}
const startStatus = task.status;

const changedStatusTask = tasks[tasksArrayLength - 1];
const endStatus = changedStatusTask.status;

const ocTrigger = ' 🏁 ';
const taskString = changedStatusTask.description;

if (!taskString.includes(ocTrigger) || endStatus.type !== StatusType.DONE || endStatus.type === startStatus.type) {
return tasks;
}

if (taskString.includes('🏁 Delete')) {
return tasks.filter((task) => task !== changedStatusTask);
}
// const errorMessage = 'Unknown "On Completion" action: ' + ocAction;
const errorMessage = 'Unknown "On Completion" action';
console.log(errorMessage);
return tasks;
// const hint = '\nClick here to clear';
// const noticeMessage = errorMessage + hint;
// new Notice(noticeMessage, 0);
// console.log('Uh-oh -- we should never actually get here... :( ');
// throw new Error('Something went wrong');
}
197 changes: 197 additions & 0 deletions tests/Task/OnCompletion.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
/**
* @jest-environment jsdom
*/
import moment from 'moment';
import { RecurrenceBuilder } from '../TestingTools/RecurrenceBuilder';
import { Status } from '../../src/Statuses/Status';
import { StatusConfiguration, StatusType } from '../../src/Statuses/StatusConfiguration';
import { TaskBuilder } from '../TestingTools/TaskBuilder';
import { toLines } from '../TestingTools/TestHelpers';
import type { Task } from '../../src/Task/Task';
import { handleOnCompletion } from '../../src/Task/OnCompletion';

window.moment = moment;

beforeEach(() => {
jest.useFakeTimers();
jest.setSystemTime(new Date('2024-02-11'));
});

afterEach(() => {
jest.useRealTimers();
// resetSettings();
});

export function applyStatusAndOnCompletionAction(task: Task, newStatus: Status) {
const tasks = task.handleNewStatus(newStatus);
return handleOnCompletion(task, tasks);
}

describe('OnCompletion', () => {
it('should just return task if Action is not recognized', () => {
// Arrange
const dueDate = '2024-02-10';
const task = new TaskBuilder()
.description('A non-recurring task with invalid OC trigger 🏁 INVALID_ACTION')
.dueDate(dueDate)
.build();
expect(task.status.type).toEqual(StatusType.TODO);

// Act
const returnedTasks = applyStatusAndOnCompletionAction(task, Status.makeDone());

// Assert
expect(returnedTasks.length).toEqual(1);
expect(toLines(returnedTasks).join('\n')).toMatchInlineSnapshot(
'"- [x] A non-recurring task with invalid OC trigger 🏁 INVALID_ACTION πŸ“… 2024-02-10 βœ… 2024-02-11"',
);
});

it('should just return task if StatusType has not changed', () => {
// Arrange
const dueDate = '2024-02-10';
const task = new TaskBuilder()
.description('An already-DONE, non-recurring task 🏁 Delete')
.dueDate(dueDate)
.doneDate(dueDate)
.status(Status.DONE)
.build();
expect(task.status.type).toEqual(StatusType.DONE);

// Act
const returnedTasks = applyStatusAndOnCompletionAction(task, Status.makeDone());

// Assert
expect(returnedTasks.length).toEqual(1);
expect(toLines(returnedTasks).join('\n')).toMatchInlineSnapshot(
'"- [x] An already-DONE, non-recurring task 🏁 Delete πŸ“… 2024-02-10 βœ… 2024-02-10"',
);
});

it('should just return trigger-less, non-recurring task', () => {
// Arrange
const dueDate = '2024-02-10';
const task = new TaskBuilder().description('A non-recurring task with no trigger').dueDate(dueDate).build();
expect(task.status.type).toEqual(StatusType.TODO);

// Act
const tasks = applyStatusAndOnCompletionAction(task, Status.makeDone());

// Assert
expect(tasks.length).toEqual(1);
expect(toLines(tasks).join('\n')).toMatchInlineSnapshot(
'"- [x] A non-recurring task with no trigger πŸ“… 2024-02-10 βœ… 2024-02-11"',
);
});

it('should just return trigger-less recurring task', () => {
// Arrange
const dueDate = '2024-02-10';
const recurrence = new RecurrenceBuilder().rule('every day').dueDate(dueDate).build();
const task = new TaskBuilder()
.description('A recurring task with no trigger')
.recurrence(recurrence)
.dueDate(dueDate)
.build();
expect(task.status.type).toEqual(StatusType.TODO);

// Act
const tasks = applyStatusAndOnCompletionAction(task, Status.makeDone());

// Assert
expect(tasks.length).toEqual(2);
expect(toLines(tasks).join('\n')).toMatchInlineSnapshot(`
"- [ ] A recurring task with no trigger πŸ” every day πŸ“… 2024-02-11
- [x] A recurring task with no trigger πŸ” every day πŸ“… 2024-02-10 βœ… 2024-02-11"
`);
});

// TODO is there a better way to handle the following? does an 'empty' Task exist?
it('should return an empty Array for a non-recurring task with Delete Action', () => {
// Arrange
const dueDate = '2024-02-10';
const task = new TaskBuilder().description('A non-recurring task with 🏁 Delete').dueDate(dueDate).build();
expect(task.status.type).toEqual(StatusType.TODO);

// Act
const tasks = applyStatusAndOnCompletionAction(task, Status.makeDone());

// Assert
expect(tasks).toEqual([]);
});

it('should return only the next instance of a recurring task with Delete action', () => {
// Arrange
const dueDate = '2024-02-10';
const recurrence = new RecurrenceBuilder().rule('every day').dueDate(dueDate).build();
const task = new TaskBuilder()
.description('A recurring task with 🏁 Delete')
.recurrence(recurrence)
.dueDate(dueDate)
.build();
expect(task.status.type).toEqual(StatusType.TODO);

// Act
const tasks = applyStatusAndOnCompletionAction(task, Status.makeDone());

// Assert
expect(tasks.length).toEqual(1);
expect(toLines(tasks).join('\n')).toMatchInlineSnapshot(
'"- [ ] A recurring task with 🏁 Delete πŸ” every day πŸ“… 2024-02-11"',
);
});
it('should delete a simple task with flag on completion', () => {
// Arrange
const task = new TaskBuilder().description('A non-recurring task with 🏁 Delete').build();

// Act
const tasks = applyStatusAndOnCompletionAction(task, Status.makeDone());

// Assert
expect(tasks.length).toEqual(0);
});

it('should return the task when going from TODO to IN_PROGRESS', () => {
// Arrange
const dueDate = '2024-02-10';
const recurrence = new RecurrenceBuilder().rule('every day').dueDate(dueDate).build();
const task = new TaskBuilder()
.description('A recurring task with 🏁 Delete')
.recurrence(recurrence)
.dueDate(dueDate)
.build();

// Act
const tasks = applyStatusAndOnCompletionAction(task, Status.makeInProgress());

// Assert
expect(tasks.length).toEqual(1);
expect(tasks[0].status.type).toEqual(StatusType.IN_PROGRESS);
});

it('should return the task when going from one DONE status to another DONE status', () => {
// Arrange
const done1 = new Status(new StatusConfiguration('x', 'done', ' ', true, StatusType.DONE));
const done2 = new Status(new StatusConfiguration('X', 'DONE', ' ', true, StatusType.DONE));
const task = new TaskBuilder().description('A simple done task with 🏁 Delete').status(done1).build();

// Act
const tasks = applyStatusAndOnCompletionAction(task, done2);

// Assert
expect(tasks.length).toEqual(1);
expect(tasks[0].status.symbol).toEqual('X');
expect(tasks[0].status.type).toEqual(StatusType.DONE);
});

it('should return a task featuring the On Completion flag trigger but an empty string Action', () => {
// Arrange
const task = new TaskBuilder().description('A non-recurring task with 🏁').build();

// Act
const tasks = applyStatusAndOnCompletionAction(task, Status.makeDone());

// Assert
expect(tasks.length).toEqual(1);
});
});

0 comments on commit 91cdc3e

Please sign in to comment.