Files
negotium/src/lib/tasks.test.js
T
Aculix Technologies 02c8d6f6b0 feat: edit a task in place
Fixing a typo meant deleting the task and typing it again, which also cost
you its position in the list. Click the text to edit it. Enter saves,
Escape cancels, clicking away saves.

The rename rule lives in tasks.js and is tested. It refuses blank input
rather than treating it as a delete, since selecting all and hitting enter
by accident should give you your task back, and it returns the original
array untouched when nothing changed so an idle edit does not write to
storage.

The interesting part was what editing collides with. Keystrokes are
stopped from reaching the row, or Delete would remove the task you are
typing into and Alt+Arrow would reorder it mid-edit. A drag cannot start
from inside the field, and the click a drag leaves behind is swallowed, or
dropping a row would open it for editing. Switching day, deferring a task
and the midnight rollover all close an open editor, so it cannot be left
hanging over a row that is no longer there.
2026-08-16 03:28:48 +05:30

183 lines
5.4 KiB
JavaScript

import { describe, it, expect } from 'vitest'
import { addTask, toggleTask, deleteTask, reorderTask, clearCompleted, renameTask } from './tasks.js'
const task = (id, completed = false) => ({ id, text: id, completed })
describe('addTask', () => {
it('appends a task', () => {
const result = addTask([], 'buy milk')
expect(result).toHaveLength(1)
expect(result[0].text).toBe('buy milk')
expect(result[0].completed).toBe(false)
})
it('trims surrounding whitespace', () => {
expect(addTask([], ' spaced ')[0].text).toBe('spaced')
})
it('rejects whitespace-only input', () => {
const before = [task('a')]
expect(addTask(before, ' ')).toBe(before)
})
it('rejects empty input', () => {
const before = [task('a')]
expect(addTask(before, '')).toBe(before)
})
it('assigns unique ids', () => {
const one = addTask([], 'a')
const two = addTask(one, 'b')
expect(two[0].id).not.toBe(two[1].id)
})
it('assigns unique ids even when added in the same millisecond', () => {
let tasks = []
for (let i = 0; i < 50; i += 1) tasks = addTask(tasks, `task ${i}`, 1_000_000)
expect(new Set(tasks.map((t) => t.id)).size).toBe(50)
})
it('records the supplied creation time', () => {
expect(addTask([], 'x', 1_234_567)[0].createdAt).toBe(1_234_567)
})
it('does not mutate the input array', () => {
const before = [task('a')]
addTask(before, 'b')
expect(before).toHaveLength(1)
})
})
describe('toggleTask', () => {
it('flips completion', () => {
expect(toggleTask([task('a')], 'a')[0].completed).toBe(true)
})
it('flips back', () => {
expect(toggleTask([task('a', true)], 'a')[0].completed).toBe(false)
})
it('ignores an unknown id', () => {
expect(toggleTask([task('a')], 'zzz')[0].completed).toBe(false)
})
it('does not mutate the input array', () => {
const before = [task('a')]
toggleTask(before, 'a')
expect(before[0].completed).toBe(false)
})
})
describe('deleteTask', () => {
it('removes the matching task', () => {
expect(deleteTask([task('a'), task('b')], 'a').map((t) => t.id)).toEqual(['b'])
})
it('ignores an unknown id', () => {
expect(deleteTask([task('a')], 'zzz')).toHaveLength(1)
})
})
describe('reorderTask', () => {
const three = [task('a'), task('b'), task('c')]
it('moves an item later', () => {
expect(reorderTask(three, 0, 2).map((t) => t.id)).toEqual(['b', 'c', 'a'])
})
it('moves an item earlier', () => {
expect(reorderTask(three, 2, 0).map((t) => t.id)).toEqual(['c', 'a', 'b'])
})
it('moves an item into the middle', () => {
expect(reorderTask(three, 0, 1).map((t) => t.id)).toEqual(['b', 'a', 'c'])
})
it('is a no-op when indices match', () => {
expect(reorderTask(three, 1, 1)).toBe(three)
})
it('is a no-op for an out-of-range destination', () => {
expect(reorderTask(three, 0, 9)).toBe(three)
})
it('is a no-op for an out-of-range source', () => {
expect(reorderTask(three, -1, 0)).toBe(three)
})
it('does not mutate the input array', () => {
const before = [task('a'), task('b')]
reorderTask(before, 0, 1)
expect(before.map((t) => t.id)).toEqual(['a', 'b'])
})
})
describe('clearCompleted', () => {
it('removes completed tasks', () => {
const result = clearCompleted([task('a', true), task('b'), task('c', true)])
expect(result.map((t) => t.id)).toEqual(['b'])
})
it('is a no-op when nothing is completed', () => {
expect(clearCompleted([task('a')])).toHaveLength(1)
})
it('can empty the list entirely', () => {
expect(clearCompleted([task('a', true)])).toEqual([])
})
})
describe('renameTask', () => {
const task = (id, text = id, completed = false) => ({ id, text, completed, createdAt: 1 })
it('replaces the text', () => {
expect(renameTask([task('a', 'old')], 'a', 'new')[0].text).toBe('new')
})
it('trims what it is given', () => {
expect(renameTask([task('a', 'old')], 'a', ' spaced ')[0].text).toBe('spaced')
})
it('refuses to blank a task', () => {
const before = [task('a', 'keep me')]
expect(renameTask(before, 'a', '')).toBe(before)
expect(renameTask(before, 'a', ' ')).toBe(before)
})
it('keeps completion, id and creation time', () => {
const before = [{ id: 'a', text: 'old', completed: true, createdAt: 99 }]
expect(renameTask(before, 'a', 'new')[0]).toEqual({ id: 'a', text: 'new', completed: true, createdAt: 99 })
})
it('leaves the other tasks alone', () => {
const result = renameTask([task('a'), task('b'), task('c')], 'b', 'changed')
expect(result.map(t => t.text)).toEqual(['a', 'changed', 'c'])
})
it('keeps the task in place', () => {
const result = renameTask([task('a'), task('b')], 'a', 'changed')
expect(result.map(t => t.id)).toEqual(['a', 'b'])
})
it('ignores an unknown id', () => {
const before = [task('a')]
expect(renameTask(before, 'zzz', 'nope')).toBe(before)
})
it('is a no-op when the text has not changed', () => {
const before = [task('a', 'same')]
expect(renameTask(before, 'a', 'same')).toBe(before)
})
it('handles emoji and non-latin text', () => {
expect(renameTask([task('a')], 'a', '買い物 🛒')[0].text).toBe('買い物 🛒')
expect(renameTask([task('a')], 'a', 'اشتر الحليب')[0].text).toBe('اشتر الحليب')
})
it('does not mutate the input array', () => {
const before = [task('a', 'old')]
renameTask(before, 'a', 'new')
expect(before[0].text).toBe('old')
})
})